- 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
233 lines
7.7 KiB
Go
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)
|
|
}
|