Move all business-logic packages from internal/ root into internal/domain/: auth, cuisine, diary, dish, home, ingredient, language, menu, product, recipe, recognition, recommendation, savedrecipe, tag, units, user Rename model.go → entity.go in packages that hold domain entities: diary, dish, home, ingredient, menu, product, recipe, savedrecipe, user Update all import paths accordingly (adapters, infra/server, cmd/server, tests). No logic changes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
279 lines
8.6 KiB
Go
279 lines
8.6 KiB
Go
//go:build integration
|
|
|
|
package auth_test
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/food-ai/backend/internal/domain/auth"
|
|
"github.com/food-ai/backend/internal/domain/auth/mocks"
|
|
"github.com/food-ai/backend/internal/infra/middleware"
|
|
"github.com/food-ai/backend/internal/testutil"
|
|
"github.com/food-ai/backend/internal/domain/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)
|
|
}
|
|
}
|