From 659369c42baf59875fbe105715e5976b209b5427 Mon Sep 17 00:00:00 2001 From: Sergey Elpashev Date: Sat, 22 Nov 2025 23:25:00 +0300 Subject: [PATCH] feat: ServerMux to gorilla/mux, time route --- cmd/apiserver/main.go | 1 - go.mod | 1 + go.sum | 2 + internal/apiserver/handlers/time.go | 43 +++++++ internal/apiserver/handlers/time_test.go | 149 +++++++++++++++++++++++ internal/apiserver/server/routes.go | 17 +-- internal/apiserver/server/server.go | 7 +- 7 files changed, 208 insertions(+), 12 deletions(-) create mode 100644 internal/apiserver/handlers/time.go create mode 100644 internal/apiserver/handlers/time_test.go diff --git a/cmd/apiserver/main.go b/cmd/apiserver/main.go index 8de1f24..024d580 100644 --- a/cmd/apiserver/main.go +++ b/cmd/apiserver/main.go @@ -10,7 +10,6 @@ import ( // @title Some GoLang server // @version 1.0 // @description This is some GoLang server. -// @termsOfService http://swagger.io/terms/ // @contact.name Sergey Elpashev // @contact.url https://nwaifu.su diff --git a/go.mod b/go.mod index b8c31ec..fbf37d0 100644 --- a/go.mod +++ b/go.mod @@ -20,6 +20,7 @@ require ( github.com/go-openapi/jsonreference v0.20.0 // indirect github.com/go-openapi/spec v0.20.6 // indirect github.com/go-openapi/swag v0.19.15 // indirect + github.com/gorilla/mux v1.8.1 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/mailru/easyjson v0.7.6 // indirect github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe // indirect diff --git a/go.sum b/go.sum index 425fca2..b8e066e 100644 --- a/go.sum +++ b/go.sum @@ -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/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/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/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= diff --git a/internal/apiserver/handlers/time.go b/internal/apiserver/handlers/time.go new file mode 100644 index 0000000..5ef1b33 --- /dev/null +++ b/internal/apiserver/handlers/time.go @@ -0,0 +1,43 @@ +// 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) + json.NewEncoder(w).Encode(response) +} diff --git a/internal/apiserver/handlers/time_test.go b/internal/apiserver/handlers/time_test.go new file mode 100644 index 0000000..d85ec56 --- /dev/null +++ b/internal/apiserver/handlers/time_test.go @@ -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) + } +} diff --git a/internal/apiserver/server/routes.go b/internal/apiserver/server/routes.go index ecc4196..482d81c 100644 --- a/internal/apiserver/server/routes.go +++ b/internal/apiserver/server/routes.go @@ -10,13 +10,14 @@ import ( // setupRoutes configures all routes func (s *Server) setupRoutes() { - // Add request ID middleware to all routes - s.router.Handle("/", middleware.RequestIDMiddleware( - middleware.LoggingMiddleware( - handlers.NewHomeHandler(), - ), - )) + // Apply global middleware to all routes + s.router.Use(middleware.RequestIDMiddleware) + s.router.Use(middleware.LoggingMiddleware) - // Swagger UI - s.router.Handle("/swagger/", httpSwagger.WrapHandler) + // Register routes + 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) } diff --git a/internal/apiserver/server/server.go b/internal/apiserver/server/server.go index ea30ecb..56239bd 100644 --- a/internal/apiserver/server/server.go +++ b/internal/apiserver/server/server.go @@ -7,6 +7,7 @@ import ( "git.nwaifu.su/sergey/MyGoServer/cmd/apiserver/config" "git.nwaifu.su/sergey/MyGoServer/internal/apiserver/logger" + "github.com/gorilla/mux" ) type contextKey struct { @@ -22,7 +23,7 @@ func saveConnInContext(ctx context.Context, c net.Conn) context.Context { // Server represents the HTTP server type Server struct { config *config.Config - router *http.ServeMux + router *mux.Router server *http.Server } @@ -36,7 +37,7 @@ func NewServer(cfg *config.Config) *Server { logger.Initialize(cfg.Logging.Level, cfg.Logging.Format, cfg.Logging.Output) // Create router - s.router = http.NewServeMux() + s.router = mux.NewRouter() s.setupRoutes() // Create HTTP server @@ -55,7 +56,7 @@ func (s *Server) Start() error { } // GetRouter returns the HTTP router -func (s *Server) GetRouter() *http.ServeMux { +func (s *Server) GetRouter() *mux.Router { return s.router }