//go:build integration // +build integration package integration import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/zap/zaptest" "gorm.io/driver/sqlite" "gorm.io/gorm" "veza-backend-api/internal/config" "veza-backend-api/internal/core/auth" "veza-backend-api/internal/database" "veza-backend-api/internal/dto" apperrors "veza-backend-api/internal/errors" "veza-backend-api/internal/handlers" "veza-backend-api/internal/models" "veza-backend-api/internal/repositories" "veza-backend-api/internal/services" "veza-backend-api/internal/validators" "github.com/google/uuid" ) // setupTokenRefreshTestRouter creates a test router for token refresh E2E (cookie-based) func setupTokenRefreshTestRouter(t *testing.T) (*gin.Engine, *auth.AuthService, *config.Config, *gorm.DB, func()) { gin.SetMode(gin.TestMode) logger := zaptest.NewLogger(t) db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) require.NoError(t, err) db.Exec("PRAGMA foreign_keys = ON") err = db.AutoMigrate( &models.User{}, &models.RefreshToken{}, &models.Session{}, &models.Role{}, &models.Permission{}, &models.UserRole{}, &models.RolePermission{}, ) require.NoError(t, err) err = db.Exec(` CREATE TABLE IF NOT EXISTS email_verification_tokens ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, token TEXT NOT NULL UNIQUE, token_hash TEXT NOT NULL, email TEXT NOT NULL, verified INTEGER NOT NULL DEFAULT 0, used INTEGER NOT NULL DEFAULT 0, verified_at TIMESTAMP, expires_at TIMESTAMP NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ) `).Error require.NoError(t, err) sqlDB, err := db.DB() require.NoError(t, err) dbWrapper := &database.Database{DB: sqlDB, GormDB: db, Logger: logger} emailValidator := validators.NewEmailValidator(db) passwordValidator := validators.NewPasswordValidator() passwordService := services.NewPasswordService(dbWrapper, logger) jwtService, err := services.NewJWTService("", "", "test-secret-key-must-be-32-chars-long", "test-issuer", "test-audience") require.NoError(t, err) refreshTokenService := services.NewRefreshTokenService(db) emailVerificationService := services.NewEmailVerificationService(dbWrapper, logger) passwordResetService := services.NewPasswordResetService(dbWrapper, logger) emailService := services.NewEmailService(dbWrapper, logger) authService := auth.NewAuthService( db, emailValidator, passwordValidator, passwordService, jwtService, refreshTokenService, emailVerificationService, passwordResetService, emailService, nil, nil, logger, ) sessionService := services.NewSessionService(dbWrapper, logger) twoFactorService := services.NewTwoFactorService(dbWrapper, logger) userRepo := repositories.NewGormUserRepository(db) userService := services.NewUserServiceWithDB(userRepo, db) cfg := &config.Config{ CookiePath: "/", CookieDomain: "", CookieHttpOnly: true, CookieSecure: false, CookieSameSite: "lax", JWTService: jwtService, } router := gin.New() authGroup := router.Group("/auth") { authGroup.POST("/login", handlers.Login(authService, sessionService, twoFactorService, logger, cfg)) authGroup.POST("/register", handlers.Register(authService, sessionService, logger, cfg)) authGroup.POST("/refresh", handlers.Refresh(authService, sessionService, logger, cfg)) authGroup.POST("/logout", handlers.Logout(authService, sessionService, logger, cfg)) authGroup.POST("/verify-email", handlers.VerifyEmail(authService)) authGroup.GET("/check-username", handlers.CheckUsername(authService)) protected := authGroup.Group("") protected.Use(func(c *gin.Context) { authHeader := c.GetHeader("Authorization") if authHeader == "" { handlers.RespondWithAppError(c, apperrors.NewUnauthorizedError("Unauthorized")) c.Abort() return } if !strings.HasPrefix(authHeader, "Bearer ") { handlers.RespondWithAppError(c, apperrors.NewUnauthorizedError("Invalid authorization header")) c.Abort() return } token := strings.TrimPrefix(authHeader, "Bearer ") claims, err := authService.JWTService.ValidateToken(token) if err != nil { handlers.RespondWithAppError(c, apperrors.NewUnauthorizedError("Invalid token")) c.Abort() return } if claims.UserID == uuid.Nil { handlers.RespondWithAppError(c, apperrors.NewUnauthorizedError("Invalid token claims")) c.Abort() return } c.Set("user_id", claims.UserID) c.Next() }) protected.GET("/me", handlers.GetMe(userService)) } return router, authService, cfg, db, func() {} } // TestTokenRefreshViaCookies tests refresh flow using httpOnly cookies only (no body) func TestTokenRefreshViaCookies(t *testing.T) { router, _, _, db, cleanup := setupTokenRefreshTestRouter(t) defer cleanup() // 1. Register registerBody, _ := json.Marshal(dto.RegisterRequest{ Email: "refresh@test.com", Username: "refreshtest", Password: "SecurePassword123!", PasswordConfirm: "SecurePassword123!", }) registerReq := httptest.NewRequest(http.MethodPost, "/auth/register", bytes.NewBuffer(registerBody)) registerReq.Header.Set("Content-Type", "application/json") registerW := httptest.NewRecorder() router.ServeHTTP(registerW, registerReq) require.Equal(t, http.StatusCreated, registerW.Code) // 2. Verify email var user models.User require.NoError(t, db.Where("email = ?", "refresh@test.com").First(&user).Error) var token string err := db.Raw("SELECT token FROM email_verification_tokens WHERE user_id = ? AND used = 0 ORDER BY created_at DESC LIMIT 1", user.ID.String()).Scan(&token).Error if err != nil { t.Skip("email verification token not found") return } verifyReq := httptest.NewRequest(http.MethodPost, "/auth/verify-email?token="+token, nil) verifyW := httptest.NewRecorder() router.ServeHTTP(verifyW, verifyReq) require.Equal(t, http.StatusOK, verifyW.Code) // 3. Login loginBody, _ := json.Marshal(dto.LoginRequest{ Email: "refresh@test.com", Password: "SecurePassword123!", RememberMe: false, }) loginReq := httptest.NewRequest(http.MethodPost, "/auth/login", bytes.NewBuffer(loginBody)) loginReq.Header.Set("Content-Type", "application/json") loginW := httptest.NewRecorder() router.ServeHTTP(loginW, loginReq) require.Equal(t, http.StatusOK, loginW.Code) // Extract refresh_token from cookies loginCookies := loginW.Result().Cookies() var refreshCookie *http.Cookie for _, c := range loginCookies { if c.Name == "refresh_token" { refreshCookie = c break } } require.NotNil(t, refreshCookie, "refresh_token cookie should be set after login") require.NotEmpty(t, refreshCookie.Value) assert.True(t, refreshCookie.HttpOnly) // 4. Refresh using ONLY cookie (no body) refreshReq := httptest.NewRequest(http.MethodPost, "/auth/refresh", nil) refreshReq.AddCookie(refreshCookie) refreshW := httptest.NewRecorder() router.ServeHTTP(refreshW, refreshReq) require.Equal(t, http.StatusOK, refreshW.Code) // 5. Verify new cookies are set refreshCookies := refreshW.Result().Cookies() var newAccessCookie *http.Cookie for _, c := range refreshCookies { if c.Name == "access_token" { newAccessCookie = c break } } require.NotNil(t, newAccessCookie, "access_token cookie should be updated") require.NotEmpty(t, newAccessCookie.Value) assert.True(t, newAccessCookie.HttpOnly) // 6. Use new access token for protected route meReq := httptest.NewRequest(http.MethodGet, "/auth/me", nil) meReq.Header.Set("Authorization", "Bearer "+newAccessCookie.Value) meW := httptest.NewRecorder() router.ServeHTTP(meW, meReq) assert.Equal(t, http.StatusOK, meW.Code) }