From 6567ff490460a69d2d225bf5f134cc2b61d738e4 Mon Sep 17 00:00:00 2001 From: Stephen Adamson Date: Wed, 29 Jul 2026 23:31:57 +0100 Subject: [PATCH] fix: harden login validation with proper JSON error responses Change LoginHandler validation error from http.Error (plain text, mismatched Content-Type) to mw.RespondError (proper JSON {error:...} response). Add TestLogin_EmptyFields and TestLogin_MissingEmail tests verifying JSON error format. --- backend/handlers/auth/auth_test.go | 51 ++++++++++++++++++++++++++++++ backend/handlers/auth/local.go | 6 ++-- 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/backend/handlers/auth/auth_test.go b/backend/handlers/auth/auth_test.go index 7bd38fd..69a75ce 100644 --- a/backend/handlers/auth/auth_test.go +++ b/backend/handlers/auth/auth_test.go @@ -21,6 +21,7 @@ package auth import ( "bytes" "context" + "encoding/json" "fmt" "net/http" "net/http/httptest" @@ -713,6 +714,56 @@ func TestLogin_InvalidRequest(t *testing.T) { } } +// TestLogin_EmptyFields verifies that sending login with empty email/password +// returns HTTP 400 with a JSON error body containing an error field. +func TestLogin_EmptyFields(t *testing.T) { + _, _ = resetTestData(t) + + handler := http.HandlerFunc(LoginHandler) + + body := map[string]string{ + "email": "", + "password": "", + } + w := testutils.MakeRequestNoAuth(handler, "POST", "/api/login", body, context.Background()) + + if w.Code != http.StatusBadRequest { + t.Errorf("expected status 400, got %d", w.Code) + } + + var resp map[string]string + if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { + t.Fatalf("expected JSON error body, got: %s (parse error: %v)", w.Body.String(), err) + } + if _, ok := resp["error"]; !ok { + t.Errorf("expected JSON response with 'error' field, got: %v", resp) + } +} + +// TestLogin_MissingEmail verifies that login without email field returns 400. +func TestLogin_MissingEmail(t *testing.T) { + _, _ = resetTestData(t) + + handler := http.HandlerFunc(LoginHandler) + + body := map[string]string{ + "password": "secret123", + } + w := testutils.MakeRequestNoAuth(handler, "POST", "/api/login", body, context.Background()) + + if w.Code != http.StatusBadRequest { + t.Errorf("expected status 400, got %d", w.Code) + } + + var resp map[string]string + if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { + t.Fatalf("expected JSON error body, got: %s (parse error: %v)", w.Body.String(), err) + } + if _, ok := resp["error"]; !ok { + t.Errorf("expected JSON response with 'error' field, got: %v", resp) + } +} + // TestRegister_NameTooLong tests that registration fails when the first name // exceeds 50 characters (the maximum allowed length). func TestRegister_NameTooLong(t *testing.T) { diff --git a/backend/handlers/auth/local.go b/backend/handlers/auth/local.go index ef67e24..1cb276f 100644 --- a/backend/handlers/auth/local.go +++ b/backend/handlers/auth/local.go @@ -313,13 +313,13 @@ func ValidateUKPhoneNumber(phone string) (string, error) { func LoginHandler(w http.ResponseWriter, r *http.Request) { var req LoginRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, "invalid request", http.StatusBadRequest) + mw.RespondError(w, http.StatusBadRequest, "invalid request") return } if err := validators.Validate.Struct(&req); err != nil { - log.Printf("Failed to process request: %v", err) - http.Error(w, "Invalid request", http.StatusBadRequest) + log.Printf("Login validation failed: %v", err) + mw.RespondError(w, http.StatusBadRequest, "Email and password are required") return }