package infra import ( "bytes" "context" "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "time" ) // --- HTTPJSONResponse --- func TestHTTPJSONResponse(t *testing.T) { t.Run("escribe status code correcto", func(t *testing.T) { rec := httptest.NewRecorder() HTTPJSONResponse(rec, http.StatusCreated, map[string]string{"ok": "true"}) if rec.Code != http.StatusCreated { t.Errorf("got status %d, want 201", rec.Code) } }) t.Run("setea Content-Type application/json", func(t *testing.T) { rec := httptest.NewRecorder() HTTPJSONResponse(rec, http.StatusOK, "test") ct := rec.Header().Get("Content-Type") if ct != "application/json" { t.Errorf("got Content-Type=%q, want application/json", ct) } }) t.Run("serializa datos correctamente a JSON", func(t *testing.T) { rec := httptest.NewRecorder() data := map[string]int{"count": 42} HTTPJSONResponse(rec, http.StatusOK, data) var got map[string]int if err := json.Unmarshal(rec.Body.Bytes(), &got); err != nil { t.Fatalf("failed to parse response JSON: %v", err) } if got["count"] != 42 { t.Errorf("got count=%d, want 42", got["count"]) } }) } // --- HTTPErrorResponse --- func TestHTTPErrorResponse(t *testing.T) { t.Run("escribe el status code del HTTPError", func(t *testing.T) { rec := httptest.NewRecorder() HTTPErrorResponse(rec, HTTPError{Status: http.StatusBadRequest, Code: "bad", Message: "err"}) if rec.Code != http.StatusBadRequest { t.Errorf("got status %d, want 400", rec.Code) } }) t.Run("setea Content-Type application/json", func(t *testing.T) { rec := httptest.NewRecorder() HTTPErrorResponse(rec, HTTPError{Status: 500, Code: "err", Message: "fail"}) ct := rec.Header().Get("Content-Type") if ct != "application/json" { t.Errorf("got Content-Type=%q, want application/json", ct) } }) t.Run("serializa Code y Message en el body JSON", func(t *testing.T) { rec := httptest.NewRecorder() HTTPErrorResponse(rec, HTTPError{Status: 422, Code: "validation_error", Message: "campo requerido"}) var got HTTPError if err := json.Unmarshal(rec.Body.Bytes(), &got); err != nil { t.Fatalf("failed to parse response JSON: %v", err) } if got.Code != "validation_error" { t.Errorf("got Code=%q, want validation_error", got.Code) } if got.Message != "campo requerido" { t.Errorf("got Message=%q, want 'campo requerido'", got.Message) } }) } // --- HTTPParseBody --- func TestHTTPParseBody(t *testing.T) { t.Run("decodifica JSON valido correctamente", func(t *testing.T) { body := `{"name":"Lucas","age":30}` req := httptest.NewRequest("POST", "/", strings.NewReader(body)) req.Header.Set("Content-Type", "application/json") var dst struct { Name string `json:"name"` Age int `json:"age"` } err := HTTPParseBody(req, &dst, 1<<20) if err != nil { t.Fatalf("unexpected error: %v", err) } if dst.Name != "Lucas" || dst.Age != 30 { t.Errorf("got {%q, %d}, want {Lucas, 30}", dst.Name, dst.Age) } }) t.Run("retorna error si el body supera maxBytes", func(t *testing.T) { body := strings.Repeat("x", 100) payload := `{"data":"` + body + `"}` req := httptest.NewRequest("POST", "/", strings.NewReader(payload)) var dst struct { Data string `json:"data"` } err := HTTPParseBody(req, &dst, 10) // limite de 10 bytes if err == nil { t.Error("expected error for oversized body, got nil") } }) t.Run("retorna error si el JSON es invalido", func(t *testing.T) { req := httptest.NewRequest("POST", "/", strings.NewReader(`{invalid json`)) var dst map[string]any err := HTTPParseBody(req, &dst, 1<<20) if err == nil { t.Error("expected error for invalid JSON, got nil") } }) t.Run("retorna error si hay campos desconocidos", func(t *testing.T) { req := httptest.NewRequest("POST", "/", strings.NewReader(`{"name":"test","unknown":"field"}`)) var dst struct { Name string `json:"name"` } err := HTTPParseBody(req, &dst, 1<<20) if err == nil { t.Error("expected error for unknown fields, got nil") } }) } // --- HTTPLoggerMiddleware --- func TestHTTPLoggerMiddleware(t *testing.T) { t.Run("loguea metodo y path del request", func(t *testing.T) { var buf bytes.Buffer mw := HTTPLoggerMiddleware(&buf) handler := mw(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) })) rec := httptest.NewRecorder() req := httptest.NewRequest("GET", "/api/test", nil) handler.ServeHTTP(rec, req) log := buf.String() if !strings.Contains(log, "GET") { t.Errorf("log does not contain method GET: %q", log) } if !strings.Contains(log, "/api/test") { t.Errorf("log does not contain path /api/test: %q", log) } }) t.Run("loguea el status code de la respuesta", func(t *testing.T) { var buf bytes.Buffer mw := HTTPLoggerMiddleware(&buf) handler := mw(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNotFound) })) rec := httptest.NewRecorder() req := httptest.NewRequest("GET", "/missing", nil) handler.ServeHTTP(rec, req) log := buf.String() if !strings.Contains(log, "404") { t.Errorf("log does not contain status 404: %q", log) } }) t.Run("loguea la duracion en milisegundos", func(t *testing.T) { var buf bytes.Buffer mw := HTTPLoggerMiddleware(&buf) handler := mw(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) })) rec := httptest.NewRecorder() req := httptest.NewRequest("GET", "/", nil) handler.ServeHTTP(rec, req) log := buf.String() if !strings.Contains(log, "ms") { t.Errorf("log does not contain duration in ms: %q", log) } }) } // --- HTTPRouter --- func TestHTTPRouter(t *testing.T) { t.Run("registra una ruta y responde correctamente", func(t *testing.T) { mux := HTTPRouter([]Route{ { Method: "GET", Path: "/api/ping", Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) w.Write([]byte("pong")) }), }, }) rec := httptest.NewRecorder() req := httptest.NewRequest("GET", "/api/ping", nil) mux.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Errorf("got status %d, want 200", rec.Code) } if rec.Body.String() != "pong" { t.Errorf("got body %q, want pong", rec.Body.String()) } }) t.Run("registra multiples rutas en el mismo mux", func(t *testing.T) { mux := HTTPRouter([]Route{ {Method: "GET", Path: "/a", Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("a")) })}, {Method: "GET", Path: "/b", Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("b")) })}, }) recA := httptest.NewRecorder() mux.ServeHTTP(recA, httptest.NewRequest("GET", "/a", nil)) if recA.Body.String() != "a" { t.Errorf("route /a: got %q, want a", recA.Body.String()) } recB := httptest.NewRecorder() mux.ServeHTTP(recB, httptest.NewRequest("GET", "/b", nil)) if recB.Body.String() != "b" { t.Errorf("route /b: got %q, want b", recB.Body.String()) } }) t.Run("ruta no registrada retorna 404", func(t *testing.T) { mux := HTTPRouter([]Route{ {Method: "GET", Path: "/known", Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) })}, }) rec := httptest.NewRecorder() req := httptest.NewRequest("GET", "/unknown", nil) mux.ServeHTTP(rec, req) if rec.Code != http.StatusNotFound { t.Errorf("got status %d, want 404", rec.Code) } }) } // --- HTTPServe --- func TestHTTPServe(t *testing.T) { t.Run("servidor arranca y responde peticiones", func(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() mux := HTTPRouter([]Route{ {Method: "GET", Path: "/ping", Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) w.Write([]byte("pong")) })}, }) errCh := make(chan error, 1) go func() { errCh <- HTTPServe("127.0.0.1:0", mux, ctx) }() // Cancelar el contexto rapidamente — el servidor deberia cerrarse limpiamente time.Sleep(10 * time.Millisecond) cancel() select { case err := <-errCh: if err != nil { t.Errorf("unexpected error from HTTPServe: %v", err) } case <-time.After(5 * time.Second): t.Error("HTTPServe did not stop after context cancel") } }) t.Run("graceful shutdown al cancelar contexto", func(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) }) done := make(chan error, 1) go func() { done <- HTTPServe("127.0.0.1:0", handler, ctx) }() time.Sleep(10 * time.Millisecond) cancel() select { case err := <-done: if err != nil { t.Errorf("shutdown error: %v", err) } case <-time.After(5 * time.Second): t.Error("server did not shutdown within 5 seconds") } }) }