veza/veza-backend-api/tests/integration/token_refresh_test.go
senke 4720bb20b2
Some checks failed
Backend API CI / test-unit (push) Failing after 0s
Backend API CI / test-integration (push) Failing after 0s
feat(auth): v0.911 Keystone - OAuth and auth integration tests
- Add access token blacklist on logout (VEZA-SEC-006)
- Extend OAuthService for mock provider injection in tests
- Add oauth_google_test.go: full OAuth Google flow with mocked provider
- Add oauth_github_test.go: OAuth GitHub flow with PKCE verification
- Add token_refresh_test.go: E2E refresh via httpOnly cookies
- Add logout_blacklist_test.go: E2E logout + token blacklist
- Fix testutils import path in resume_upload_test, track_quota_test
- Fix CreatorID -> UserID in track_quota_test
- Add test:integration script to package.json

Release: v0.911 Keystone
2026-02-27 09:58:53 +01:00

233 lines
7.7 KiB
Go

//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)
}