//go:build integration package auth_test import ( "bytes" "context" "encoding/json" "net/http" "net/http/httptest" "testing" "time" "github.com/food-ai/backend/internal/auth" "github.com/food-ai/backend/internal/auth/mocks" "github.com/food-ai/backend/internal/middleware" "github.com/food-ai/backend/internal/testutil" "github.com/food-ai/backend/internal/user" "github.com/go-chi/chi/v5" ) // testRefreshRequest mirrors the unexported handler request type for test marshalling. type testRefreshRequest struct { RefreshToken string `json:"refresh_token"` } // testValidator adapts auth.JWTManager to middleware.AccessTokenValidator for tests. type testValidator struct { jm *auth.JWTManager } func (v *testValidator) ValidateAccessToken(tokenStr string) (*middleware.TokenClaims, error) { claims, validateError := v.jm.ValidateAccessToken(tokenStr) if validateError != nil { return nil, validateError } return &middleware.TokenClaims{UserID: claims.UserID, Plan: claims.Plan}, nil } func setupIntegrationTest(t *testing.T) (*chi.Mux, *auth.JWTManager) { t.Helper() pool := testutil.SetupTestDB(t) verifier := &mocks.MockTokenVerifier{ VerifyTokenFn: func(ctx context.Context, idToken string) (string, string, string, string, error) { return "fb-" + idToken, idToken + "@test.com", "Test User", "", nil }, } jm := auth.NewJWTManager("test-secret", 15*time.Minute, 720*time.Hour) repo := user.NewRepository(pool) svc := auth.NewService(verifier, repo, jm) handler := auth.NewHandler(svc) r := chi.NewRouter() r.Post("/auth/login", handler.Login) r.Post("/auth/refresh", handler.Refresh) r.Group(func(r chi.Router) { r.Use(middleware.Auth(&testValidator{jm: jm})) r.Post("/auth/logout", handler.Logout) }) return r, jm } func TestIntegration_Login(t *testing.T) { router, _ := setupIntegrationTest(t) body := `{"firebase_token":"user1"}` req := httptest.NewRequest("POST", "/auth/login", bytes.NewBufferString(body)) req.Header.Set("Content-Type", "application/json") rr := httptest.NewRecorder() router.ServeHTTP(rr, req) if rr.Code != http.StatusOK { t.Fatalf("expected 200, got %d: %s", rr.Code, rr.Body.String()) } var resp auth.LoginResponse json.NewDecoder(rr.Body).Decode(&resp) if resp.AccessToken == "" { t.Error("expected non-empty access token") } if resp.RefreshToken == "" { t.Error("expected non-empty refresh token") } if resp.User == nil { t.Fatal("expected user in response") } } func TestIntegration_Login_EmptyToken(t *testing.T) { router, _ := setupIntegrationTest(t) body := `{"firebase_token":""}` req := httptest.NewRequest("POST", "/auth/login", bytes.NewBufferString(body)) req.Header.Set("Content-Type", "application/json") rr := httptest.NewRecorder() router.ServeHTTP(rr, req) if rr.Code != http.StatusBadRequest { t.Errorf("expected 400, got %d", rr.Code) } } func TestIntegration_Login_InvalidBody(t *testing.T) { router, _ := setupIntegrationTest(t) req := httptest.NewRequest("POST", "/auth/login", bytes.NewBufferString("invalid")) req.Header.Set("Content-Type", "application/json") rr := httptest.NewRecorder() router.ServeHTTP(rr, req) if rr.Code != http.StatusBadRequest { t.Errorf("expected 400, got %d", rr.Code) } } func TestIntegration_Refresh(t *testing.T) { router, _ := setupIntegrationTest(t) // First login loginBody := `{"firebase_token":"user2"}` loginReq := httptest.NewRequest("POST", "/auth/login", bytes.NewBufferString(loginBody)) loginReq.Header.Set("Content-Type", "application/json") loginRR := httptest.NewRecorder() router.ServeHTTP(loginRR, loginReq) var loginResp auth.LoginResponse json.NewDecoder(loginRR.Body).Decode(&loginResp) // Then refresh refreshBody, _ := json.Marshal(testRefreshRequest{RefreshToken: loginResp.RefreshToken}) refreshReq := httptest.NewRequest("POST", "/auth/refresh", bytes.NewBuffer(refreshBody)) refreshReq.Header.Set("Content-Type", "application/json") refreshRR := httptest.NewRecorder() router.ServeHTTP(refreshRR, refreshReq) if refreshRR.Code != http.StatusOK { t.Fatalf("expected 200, got %d: %s", refreshRR.Code, refreshRR.Body.String()) } var resp auth.RefreshResponse json.NewDecoder(refreshRR.Body).Decode(&resp) if resp.AccessToken == "" { t.Error("expected non-empty access token") } if resp.RefreshToken == loginResp.RefreshToken { t.Error("expected rotated refresh token") } } func TestIntegration_Refresh_InvalidToken(t *testing.T) { router, _ := setupIntegrationTest(t) body := `{"refresh_token":"nonexistent"}` req := httptest.NewRequest("POST", "/auth/refresh", bytes.NewBufferString(body)) req.Header.Set("Content-Type", "application/json") rr := httptest.NewRecorder() router.ServeHTTP(rr, req) if rr.Code != http.StatusUnauthorized { t.Errorf("expected 401, got %d", rr.Code) } } func TestIntegration_Refresh_EmptyToken(t *testing.T) { router, _ := setupIntegrationTest(t) body := `{"refresh_token":""}` req := httptest.NewRequest("POST", "/auth/refresh", bytes.NewBufferString(body)) req.Header.Set("Content-Type", "application/json") rr := httptest.NewRecorder() router.ServeHTTP(rr, req) if rr.Code != http.StatusBadRequest { t.Errorf("expected 400, got %d", rr.Code) } } func TestIntegration_Logout(t *testing.T) { router, _ := setupIntegrationTest(t) // Login first loginBody := `{"firebase_token":"user3"}` loginReq := httptest.NewRequest("POST", "/auth/login", bytes.NewBufferString(loginBody)) loginReq.Header.Set("Content-Type", "application/json") loginRR := httptest.NewRecorder() router.ServeHTTP(loginRR, loginReq) var loginResp auth.LoginResponse json.NewDecoder(loginRR.Body).Decode(&loginResp) // Logout logoutReq := httptest.NewRequest("POST", "/auth/logout", nil) logoutReq.Header.Set("Authorization", "Bearer "+loginResp.AccessToken) logoutRR := httptest.NewRecorder() router.ServeHTTP(logoutRR, logoutReq) if logoutRR.Code != http.StatusOK { t.Errorf("expected 200, got %d: %s", logoutRR.Code, logoutRR.Body.String()) } } func TestIntegration_Logout_NoAuth(t *testing.T) { router, _ := setupIntegrationTest(t) req := httptest.NewRequest("POST", "/auth/logout", nil) rr := httptest.NewRecorder() router.ServeHTTP(rr, req) if rr.Code != http.StatusUnauthorized { t.Errorf("expected 401, got %d", rr.Code) } } func TestIntegration_RefreshAfterLogout(t *testing.T) { router, _ := setupIntegrationTest(t) // Login loginBody := `{"firebase_token":"user4"}` loginReq := httptest.NewRequest("POST", "/auth/login", bytes.NewBufferString(loginBody)) loginReq.Header.Set("Content-Type", "application/json") loginRR := httptest.NewRecorder() router.ServeHTTP(loginRR, loginReq) var loginResp auth.LoginResponse json.NewDecoder(loginRR.Body).Decode(&loginResp) // Logout logoutReq := httptest.NewRequest("POST", "/auth/logout", nil) logoutReq.Header.Set("Authorization", "Bearer "+loginResp.AccessToken) logoutRR := httptest.NewRecorder() router.ServeHTTP(logoutRR, logoutReq) // Try to refresh with old token refreshBody, _ := json.Marshal(testRefreshRequest{RefreshToken: loginResp.RefreshToken}) refreshReq := httptest.NewRequest("POST", "/auth/refresh", bytes.NewBuffer(refreshBody)) refreshReq.Header.Set("Content-Type", "application/json") refreshRR := httptest.NewRecorder() router.ServeHTTP(refreshRR, refreshReq) if refreshRR.Code != http.StatusUnauthorized { t.Errorf("expected 401 after logout, got %d", refreshRR.Code) } } func TestIntegration_OldRefreshTokenInvalid(t *testing.T) { router, _ := setupIntegrationTest(t) // Login loginBody := `{"firebase_token":"user5"}` loginReq := httptest.NewRequest("POST", "/auth/login", bytes.NewBufferString(loginBody)) loginReq.Header.Set("Content-Type", "application/json") loginRR := httptest.NewRecorder() router.ServeHTTP(loginRR, loginReq) var loginResp auth.LoginResponse json.NewDecoder(loginRR.Body).Decode(&loginResp) oldRefreshToken := loginResp.RefreshToken // Refresh (rotates token) refreshBody, _ := json.Marshal(testRefreshRequest{RefreshToken: oldRefreshToken}) refreshReq := httptest.NewRequest("POST", "/auth/refresh", bytes.NewBuffer(refreshBody)) refreshReq.Header.Set("Content-Type", "application/json") refreshRR := httptest.NewRecorder() router.ServeHTTP(refreshRR, refreshReq) // Try old refresh token again oldRefreshReq := httptest.NewRequest("POST", "/auth/refresh", bytes.NewBuffer(refreshBody)) oldRefreshReq.Header.Set("Content-Type", "application/json") oldRefreshRR := httptest.NewRecorder() router.ServeHTTP(oldRefreshRR, oldRefreshReq) if oldRefreshRR.Code != http.StatusUnauthorized { t.Errorf("expected 401 for old refresh token, got %d", oldRefreshRR.Code) } }