Compare commits

...

3 Commits

Author SHA1 Message Date
132945342f feat: fixed lint errors 2025-11-22 23:52:35 +03:00
80ff5c501d fix: inderect require 2025-11-22 23:32:39 +03:00
659369c42b feat: ServerMux to gorilla/mux, time route 2025-11-22 23:25:00 +03:00
10 changed files with 224 additions and 19 deletions

View File

@@ -10,7 +10,6 @@ import (
// @title Some GoLang server // @title Some GoLang server
// @version 1.0 // @version 1.0
// @description This is some GoLang server. // @description This is some GoLang server.
// @termsOfService http://swagger.io/terms/
// @contact.name Sergey Elpashev // @contact.name Sergey Elpashev
// @contact.url https://nwaifu.su // @contact.url https://nwaifu.su

1
go.mod
View File

@@ -20,6 +20,7 @@ require (
github.com/go-openapi/jsonreference v0.20.0 // indirect github.com/go-openapi/jsonreference v0.20.0 // indirect
github.com/go-openapi/spec v0.20.6 // indirect github.com/go-openapi/spec v0.20.6 // indirect
github.com/go-openapi/swag v0.19.15 // indirect github.com/go-openapi/swag v0.19.15 // indirect
github.com/gorilla/mux v1.8.1
github.com/josharian/intern v1.0.0 // indirect github.com/josharian/intern v1.0.0 // indirect
github.com/mailru/easyjson v0.7.6 // indirect github.com/mailru/easyjson v0.7.6 // indirect
github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe // indirect github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe // indirect

2
go.sum
View File

@@ -20,6 +20,8 @@ github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyr
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=

View File

@@ -33,5 +33,8 @@ func (h *HomeHandler) handleHome(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(response) if err := json.NewEncoder(w).Encode(response); err != nil {
// Handle encoding error - we can't write an error response after headers
return
}
} }

View File

@@ -0,0 +1,46 @@
// It's just test file. I'll remove it later
package handlers
import (
"encoding/json"
"net/http"
"time"
"git.nwaifu.su/sergey/MyGoServer/internal/apiserver/models"
)
// TimeHandler handles the time endpoint
type TimeHandler struct{}
func NewTimeHandler() *TimeHandler {
return &TimeHandler{}
}
// ServeHTTP implements http.Handler
func (h *TimeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.handleTime(w, r)
}
// @Summary Get server time
// @Description Возвращает текущее серверное время в формате ISO 8601
// @Tags Time
// @Success 200 {object} models.Response "Текущее время сервера"
// @Router /time [get]
func (h *TimeHandler) handleTime(w http.ResponseWriter, r *http.Request) {
// Get current time in UTC
currentTime := time.Now().UTC()
// Create response with time data
response := models.NewSuccessResponse(map[string]interface{}{
"server_time": currentTime.Format(time.RFC3339),
"unix_timestamp": currentTime.Unix(),
"timezone": "UTC",
})
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
if err := json.NewEncoder(w).Encode(response); err != nil {
// Handle encoding error - we can't write an error response after headers
return
}
}

View File

@@ -0,0 +1,149 @@
package handlers
import (
"encoding/json"
"net/http"
"net/http/httptest"
"strconv"
"strings"
"testing"
"time"
"git.nwaifu.su/sergey/MyGoServer/internal/apiserver/models"
)
func TestTimeHandler_ServeHTTP(t *testing.T) {
// Create a new time handler
handler := NewTimeHandler()
// Create a test request
req := httptest.NewRequest("GET", "/time", nil)
// Create a test response recorder
rr := httptest.NewRecorder()
// Call the handler
handler.ServeHTTP(rr, req)
// Check status code
if status := rr.Code; status != http.StatusOK {
t.Errorf("handler returned wrong status code: got %v want %v",
status, http.StatusOK)
}
// Check content type
if ct := rr.Header().Get("Content-Type"); ct != "application/json" {
t.Errorf("handler returned wrong content type: got %v want %v",
ct, "application/json")
}
// Check response body contains expected structure
var response models.Response
if err := json.Unmarshal(rr.Body.Bytes(), &response); err != nil {
t.Errorf("handler returned invalid JSON: %v", err)
return
}
// Check success field
if !response.Success {
t.Errorf("handler returned success=false, want success=true")
}
// Check that data exists and is a map
if response.Data == nil {
t.Errorf("handler returned nil data")
return
}
// Type assert to map[string]interface{}
dataMap, ok := response.Data.(map[string]interface{})
if !ok {
t.Errorf("handler returned data of unexpected type")
return
}
// Check required fields exist
requiredFields := []string{"server_time", "unix_timestamp", "timezone"}
for _, field := range requiredFields {
if _, exists := dataMap[field]; !exists {
t.Errorf("handler response missing required field: %s", field)
}
}
// Check timezone is UTC
if timezone, ok := dataMap["timezone"].(string); ok {
if timezone != "UTC" {
t.Errorf("handler returned wrong timezone: got %v want %v",
timezone, "UTC")
}
} else {
t.Errorf("handler returned timezone of unexpected type")
}
// Check server_time format (should be RFC3339)
if serverTime, ok := dataMap["server_time"].(string); ok {
if _, err := time.Parse(time.RFC3339, serverTime); err != nil {
t.Errorf("handler returned invalid server_time format: got %v, error: %v",
serverTime, err)
}
} else {
t.Errorf("handler returned server_time of unexpected type")
}
// Check unix_timestamp is a number
if unixTimestamp, ok := dataMap["unix_timestamp"].(float64); ok {
// Verify it's a reasonable timestamp (should be close to current time)
now := time.Now().UTC().Unix()
// Allow 10 seconds difference for test execution time
if diff := now - int64(unixTimestamp); diff > 10 || diff < -10 {
t.Errorf("handler returned unreasonable unix_timestamp: got %v, current time: %v",
int64(unixTimestamp), now)
}
} else {
// Try as string and convert
if unixTimestampStr, ok := dataMap["unix_timestamp"].(string); ok {
if _, err := strconv.ParseInt(unixTimestampStr, 10, 64); err != nil {
t.Errorf("handler returned unix_timestamp of unexpected format: %v", err)
}
} else {
t.Errorf("handler returned unix_timestamp of unexpected type")
}
}
}
func TestTimeHandler_ResponseStructure(t *testing.T) {
handler := NewTimeHandler()
req := httptest.NewRequest("GET", "/time", nil)
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
// Test that response is valid JSON and contains all expected top-level fields
var response models.Response
if err := json.Unmarshal(rr.Body.Bytes(), &response); err != nil {
t.Fatalf("Response is not valid JSON: %v", err)
}
// Test response structure
if response.Success != true {
t.Errorf("Expected success=true, got success=%v", response.Success)
}
if response.Error != "" {
t.Errorf("Expected no error, got error=%v", response.Error)
}
if response.Data == nil {
t.Errorf("Expected data to be present, got nil")
}
// Test that response body contains JSON structure
body := rr.Body.String()
if !strings.Contains(body, `"success":true`) {
t.Errorf("Response body missing success field: %s", body)
}
if !strings.Contains(body, `"data"`) {
t.Errorf("Response body missing data field: %s", body)
}
}

View File

@@ -37,10 +37,8 @@ func Initialize(level string, format string, output string) {
}) })
} }
// Set output // Set output (currently not implemented)
if output != "" { // TODO: Implement file output support
//TODO: Use files
}
initialized = true initialized = true
}) })

View File

@@ -8,6 +8,11 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
) )
// requestIDKey is a custom type for context key to avoid collisions
type requestIDKey struct{}
var _ requestIDKey
// RequestIDMiddleware adds a unique request ID to each request // RequestIDMiddleware adds a unique request ID to each request
func RequestIDMiddleware(next http.Handler) http.Handler { func RequestIDMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -17,9 +22,9 @@ func RequestIDMiddleware(next http.Handler) http.Handler {
// Set request ID in response header // Set request ID in response header
w.Header().Set("X-Request-ID", requestID) w.Header().Set("X-Request-ID", requestID)
// Add request ID to context // Add request ID to context using custom type
ctx := r.Context() ctx := r.Context()
ctx = context.WithValue(ctx, "request_id", requestID) ctx = context.WithValue(ctx, requestIDKey{}, requestID)
// Log the request ID assignment // Log the request ID assignment
logger := logger.GetLogger() logger := logger.GetLogger()

View File

@@ -10,13 +10,14 @@ import (
// setupRoutes configures all routes // setupRoutes configures all routes
func (s *Server) setupRoutes() { func (s *Server) setupRoutes() {
// Add request ID middleware to all routes // Apply global middleware to all routes
s.router.Handle("/", middleware.RequestIDMiddleware( s.router.Use(middleware.RequestIDMiddleware)
middleware.LoggingMiddleware( s.router.Use(middleware.LoggingMiddleware)
handlers.NewHomeHandler(),
),
))
// Swagger UI // Register routes
s.router.Handle("/swagger/", httpSwagger.WrapHandler) s.router.Handle("/", handlers.NewHomeHandler()).Methods("GET")
s.router.Handle("/time", handlers.NewTimeHandler()).Methods("GET")
// Swagger UI (no middleware needed)
s.router.PathPrefix("/swagger/").Handler(httpSwagger.WrapHandler)
} }

View File

@@ -7,6 +7,7 @@ import (
"git.nwaifu.su/sergey/MyGoServer/cmd/apiserver/config" "git.nwaifu.su/sergey/MyGoServer/cmd/apiserver/config"
"git.nwaifu.su/sergey/MyGoServer/internal/apiserver/logger" "git.nwaifu.su/sergey/MyGoServer/internal/apiserver/logger"
"github.com/gorilla/mux"
) )
type contextKey struct { type contextKey struct {
@@ -22,7 +23,7 @@ func saveConnInContext(ctx context.Context, c net.Conn) context.Context {
// Server represents the HTTP server // Server represents the HTTP server
type Server struct { type Server struct {
config *config.Config config *config.Config
router *http.ServeMux router *mux.Router
server *http.Server server *http.Server
} }
@@ -36,7 +37,7 @@ func NewServer(cfg *config.Config) *Server {
logger.Initialize(cfg.Logging.Level, cfg.Logging.Format, cfg.Logging.Output) logger.Initialize(cfg.Logging.Level, cfg.Logging.Format, cfg.Logging.Output)
// Create router // Create router
s.router = http.NewServeMux() s.router = mux.NewRouter()
s.setupRoutes() s.setupRoutes()
// Create HTTP server // Create HTTP server
@@ -55,7 +56,7 @@ func (s *Server) Start() error {
} }
// GetRouter returns the HTTP router // GetRouter returns the HTTP router
func (s *Server) GetRouter() *http.ServeMux { func (s *Server) GetRouter() *mux.Router {
return s.router return s.router
} }