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
This commit is contained in:
parent
f9120c322b
commit
4720bb20b2
10 changed files with 917 additions and 23 deletions
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
|||
0.903
|
||||
0.911
|
||||
|
|
|
|||
|
|
@ -632,6 +632,23 @@ func Logout(authService *auth.AuthService, sessionService *services.SessionServi
|
|||
}
|
||||
}
|
||||
|
||||
// VEZA-SEC-006: Add access token to blacklist so it is rejected immediately
|
||||
if cfg.TokenBlacklist != nil {
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
if authHeader != "" && strings.HasPrefix(authHeader, "Bearer ") {
|
||||
accessToken := strings.TrimPrefix(authHeader, "Bearer ")
|
||||
ttl := 5 * time.Minute // default if we cannot parse
|
||||
if claims, err := authService.JWTService.ValidateToken(accessToken); err == nil && claims.ExpiresAt != nil {
|
||||
if remaining := time.Until(claims.ExpiresAt.Time); remaining > 0 {
|
||||
ttl = remaining
|
||||
}
|
||||
}
|
||||
if err := cfg.TokenBlacklist.Add(c.Request.Context(), accessToken, ttl); err != nil {
|
||||
// Log but don't fail - logout should succeed even if blacklist fails
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SECURITY: Supprimer le cookie refresh_token lors du logout
|
||||
refreshTokenCookie := &http.Cookie{
|
||||
Name: "refresh_token",
|
||||
|
|
|
|||
|
|
@ -38,6 +38,8 @@ type OAuthService struct {
|
|||
cryptoService *CryptoService // v0.902: encrypt OAuth provider tokens at rest (nil = store plaintext)
|
||||
frontendURL string // v0.902: default redirect URL
|
||||
allowedDomains []string // v0.902: whitelist for OAuth redirect (OAUTH_ALLOWED_REDIRECT_DOMAINS)
|
||||
testTokenURL string // v0.911: override for integration tests (mock provider)
|
||||
testUserInfoURL string // v0.911: override for integration tests (mock userinfo)
|
||||
}
|
||||
|
||||
// OAuthAccount represents an OAuth account linking
|
||||
|
|
@ -69,16 +71,23 @@ type OAuthState struct {
|
|||
}
|
||||
|
||||
// OAuthServiceConfig holds optional config for OAuth (v0.902)
|
||||
// v0.911: TestTokenURL, TestUserInfoURL, TestHTTPClient for integration tests (mock provider)
|
||||
type OAuthServiceConfig struct {
|
||||
CryptoService *CryptoService
|
||||
FrontendURL string
|
||||
AllowedDomains []string
|
||||
TestTokenURL string // Override token exchange URL (for tests)
|
||||
TestUserInfoURL string // Override userinfo API URL (for tests)
|
||||
TestHTTPClient *http.Client // Override HTTP client (for tests)
|
||||
}
|
||||
|
||||
// NewOAuthService creates a new OAuth service
|
||||
// cfg: optional config for crypto, redirect validation (v0.902)
|
||||
// cfg: optional config for crypto, redirect validation (v0.902), test overrides (v0.911)
|
||||
func NewOAuthService(db *database.Database, logger *zap.Logger, jwtService *JWTService, sessionService *SessionService, userService *UserService, cfg *OAuthServiceConfig) *OAuthService {
|
||||
httpClient := &http.Client{Timeout: 10 * time.Second}
|
||||
if cfg != nil && cfg.TestHTTPClient != nil {
|
||||
httpClient = cfg.TestHTTPClient
|
||||
}
|
||||
svc := &OAuthService{
|
||||
db: db,
|
||||
logger: logger,
|
||||
|
|
@ -91,14 +100,25 @@ func NewOAuthService(db *database.Database, logger *zap.Logger, jwtService *JWTS
|
|||
svc.cryptoService = cfg.CryptoService
|
||||
svc.frontendURL = cfg.FrontendURL
|
||||
svc.allowedDomains = cfg.AllowedDomains
|
||||
svc.testTokenURL = cfg.TestTokenURL
|
||||
svc.testUserInfoURL = cfg.TestUserInfoURL
|
||||
}
|
||||
return svc
|
||||
}
|
||||
|
||||
// InitializeConfigs initializes OAuth configurations
|
||||
func (os *OAuthService) InitializeConfigs(googleClientID, googleClientSecret, githubClientID, githubClientSecret, discordClientID, discordClientSecret, spotifyClientID, spotifyClientSecret, baseURL string) {
|
||||
testEndpoint := oauth2.Endpoint{}
|
||||
if os.testTokenURL != "" {
|
||||
testEndpoint = oauth2.Endpoint{TokenURL: os.testTokenURL, AuthURL: os.testTokenURL}
|
||||
}
|
||||
|
||||
// Google OAuth
|
||||
if googleClientID != "" && googleClientSecret != "" {
|
||||
endpoint := google.Endpoint
|
||||
if os.testTokenURL != "" {
|
||||
endpoint = testEndpoint
|
||||
}
|
||||
os.googleConfig = &oauth2.Config{
|
||||
ClientID: googleClientID,
|
||||
ClientSecret: googleClientSecret,
|
||||
|
|
@ -107,21 +127,25 @@ func (os *OAuthService) InitializeConfigs(googleClientID, googleClientSecret, gi
|
|||
"https://www.googleapis.com/auth/userinfo.email",
|
||||
"https://www.googleapis.com/auth/userinfo.profile",
|
||||
},
|
||||
Endpoint: google.Endpoint,
|
||||
Endpoint: endpoint,
|
||||
}
|
||||
}
|
||||
|
||||
// GitHub OAuth
|
||||
if githubClientID != "" && githubClientSecret != "" {
|
||||
endpoint := oauth2.Endpoint{
|
||||
AuthURL: "https://github.com/login/oauth/authorize",
|
||||
TokenURL: "https://github.com/login/oauth/access_token",
|
||||
}
|
||||
if os.testTokenURL != "" {
|
||||
endpoint = testEndpoint
|
||||
}
|
||||
os.githubConfig = &oauth2.Config{
|
||||
ClientID: githubClientID,
|
||||
ClientSecret: githubClientSecret,
|
||||
RedirectURL: fmt.Sprintf("%s/api/v1/auth/oauth/github/callback", baseURL),
|
||||
Scopes: []string{"user:email", "read:user"},
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: "https://github.com/login/oauth/authorize",
|
||||
TokenURL: "https://github.com/login/oauth/access_token",
|
||||
},
|
||||
Endpoint: endpoint,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -434,17 +458,21 @@ type OAuthUserInfo struct {
|
|||
// getUserInfo fetches user information from the OAuth provider
|
||||
func (os *OAuthService) getUserInfo(provider, accessToken string) (*OAuthUser, error) {
|
||||
var apiURL string
|
||||
switch provider {
|
||||
case "google":
|
||||
apiURL = "https://www.googleapis.com/oauth2/v2/userinfo"
|
||||
case "github":
|
||||
apiURL = "https://api.github.com/user"
|
||||
case "discord":
|
||||
apiURL = "https://discord.com/api/users/@me"
|
||||
case "spotify":
|
||||
apiURL = "https://api.spotify.com/v1/me"
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown provider: %s", provider)
|
||||
if os.testUserInfoURL != "" {
|
||||
apiURL = os.testUserInfoURL
|
||||
} else {
|
||||
switch provider {
|
||||
case "google":
|
||||
apiURL = "https://www.googleapis.com/oauth2/v2/userinfo"
|
||||
case "github":
|
||||
apiURL = "https://api.github.com/user"
|
||||
case "discord":
|
||||
apiURL = "https://discord.com/api/users/@me"
|
||||
case "spotify":
|
||||
apiURL = "https://api.spotify.com/v1/me"
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown provider: %s", provider)
|
||||
}
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", apiURL, nil)
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
"scripts": {
|
||||
"build": "go build -v ./...",
|
||||
"test": "go test -v ./internal/handlers/... ./internal/services/... -short",
|
||||
"lint": "test -z \"$(gofmt -l .)\" || (echo 'gofmt needed on:'; gofmt -l .; exit 1)"
|
||||
"lint": "test -z \"$(gofmt -l .)\" || (echo 'gofmt needed on:'; gofmt -l .; exit 1)",
|
||||
"test:integration": "go test -tags=integration -v ./tests/integration/... -count=1"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
250
veza-backend-api/tests/integration/logout_blacklist_test.go
Normal file
250
veza-backend-api/tests/integration/logout_blacklist_test.go
Normal file
|
|
@ -0,0 +1,250 @@
|
|||
//go:build integration
|
||||
// +build integration
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"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"
|
||||
"veza-backend-api/internal/handlers"
|
||||
"veza-backend-api/internal/middleware"
|
||||
"veza-backend-api/internal/models"
|
||||
"veza-backend-api/internal/repositories"
|
||||
"veza-backend-api/internal/services"
|
||||
"veza-backend-api/internal/validators"
|
||||
)
|
||||
|
||||
// setupLogoutBlacklistTestRouter creates a test router with AuthMiddleware + TokenBlacklist
|
||||
func setupLogoutBlacklistTestRouter(t *testing.T) (*gin.Engine, *auth.AuthService, *services.TokenBlacklist, *gorm.DB, func()) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
logger := zaptest.NewLogger(t)
|
||||
|
||||
// Redis for TokenBlacklist
|
||||
redisAddr := os.Getenv("REDIS_ADDR")
|
||||
if redisAddr == "" {
|
||||
redisAddr = "localhost:6379"
|
||||
}
|
||||
redisClient := redis.NewClient(&redis.Options{Addr: redisAddr})
|
||||
ctx := context.Background()
|
||||
if err := redisClient.Ping(ctx).Err(); err != nil {
|
||||
t.Skipf("Skipping test: Redis not available at %s: %v", redisAddr, err)
|
||||
return nil, nil, nil, nil, func() {}
|
||||
}
|
||||
|
||||
tokenBlacklist := services.NewTokenBlacklist(redisClient)
|
||||
|
||||
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)
|
||||
auditService := services.NewAuditService(dbWrapper, logger)
|
||||
permissionService := services.NewPermissionService(db)
|
||||
|
||||
authMiddleware := middleware.NewAuthMiddleware(
|
||||
sessionService,
|
||||
auditService,
|
||||
permissionService,
|
||||
jwtService,
|
||||
userService,
|
||||
nil, // apiKeyService
|
||||
tokenBlacklist,
|
||||
logger,
|
||||
)
|
||||
|
||||
cfg := &config.Config{
|
||||
CookiePath: "/",
|
||||
CookieDomain: "",
|
||||
CookieHttpOnly: true,
|
||||
CookieSecure: false,
|
||||
CookieSameSite: "lax",
|
||||
JWTService: jwtService,
|
||||
TokenBlacklist: tokenBlacklist,
|
||||
}
|
||||
|
||||
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("/verify-email", handlers.VerifyEmail(authService))
|
||||
authGroup.GET("/check-username", handlers.CheckUsername(authService))
|
||||
|
||||
protected := authGroup.Group("")
|
||||
protected.Use(authMiddleware.RequireAuth())
|
||||
protected.POST("/logout", handlers.Logout(authService, sessionService, logger, cfg))
|
||||
protected.GET("/me", handlers.GetMe(userService))
|
||||
}
|
||||
|
||||
cleanup := func() {
|
||||
redisClient.FlushDB(ctx)
|
||||
redisClient.Close()
|
||||
}
|
||||
|
||||
return router, authService, tokenBlacklist, db, cleanup
|
||||
}
|
||||
|
||||
// TestLogoutBlacklist tests that after logout, the access token is blacklisted and returns 401
|
||||
func TestLogoutBlacklist(t *testing.T) {
|
||||
router, _, _, db, cleanup := setupLogoutBlacklistTestRouter(t)
|
||||
defer cleanup()
|
||||
|
||||
if router == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 1. Register
|
||||
registerBody, _ := json.Marshal(dto.RegisterRequest{
|
||||
Email: "blacklist@test.com",
|
||||
Username: "blacklisttest",
|
||||
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 = ?", "blacklist@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: "blacklist@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 tokens from response and cookies
|
||||
var loginResp handlers.APIResponse
|
||||
require.NoError(t, json.Unmarshal(loginW.Body.Bytes(), &loginResp))
|
||||
loginDataBytes, _ := json.Marshal(loginResp.Data)
|
||||
var loginData dto.LoginResponse
|
||||
require.NoError(t, json.Unmarshal(loginDataBytes, &loginData))
|
||||
accessToken := loginData.Token.AccessToken
|
||||
require.NotEmpty(t, accessToken)
|
||||
|
||||
var refreshCookie *http.Cookie
|
||||
for _, c := range loginW.Result().Cookies() {
|
||||
if c.Name == "refresh_token" {
|
||||
refreshCookie = c
|
||||
break
|
||||
}
|
||||
}
|
||||
require.NotNil(t, refreshCookie)
|
||||
|
||||
// 4. Access /me with access token -> 200
|
||||
meReq := httptest.NewRequest(http.MethodGet, "/auth/me", nil)
|
||||
meReq.Header.Set("Authorization", "Bearer "+accessToken)
|
||||
meW := httptest.NewRecorder()
|
||||
router.ServeHTTP(meW, meReq)
|
||||
assert.Equal(t, http.StatusOK, meW.Code)
|
||||
|
||||
// 5. Logout (with access token in Authorization and refresh in cookie)
|
||||
logoutReq := httptest.NewRequest(http.MethodPost, "/auth/logout", nil)
|
||||
logoutReq.Header.Set("Authorization", "Bearer "+accessToken)
|
||||
logoutReq.AddCookie(refreshCookie)
|
||||
logoutW := httptest.NewRecorder()
|
||||
router.ServeHTTP(logoutW, logoutReq)
|
||||
assert.Equal(t, http.StatusOK, logoutW.Code)
|
||||
|
||||
// 6. Access /me with SAME access token -> 401 (blacklisted)
|
||||
meReq2 := httptest.NewRequest(http.MethodGet, "/auth/me", nil)
|
||||
meReq2.Header.Set("Authorization", "Bearer "+accessToken)
|
||||
meW2 := httptest.NewRecorder()
|
||||
router.ServeHTTP(meW2, meReq2)
|
||||
assert.Equal(t, http.StatusUnauthorized, meW2.Code, "Blacklisted token should return 401")
|
||||
|
||||
// 7. Refresh with old refresh token -> 401 (invalidated)
|
||||
refreshReq := httptest.NewRequest(http.MethodPost, "/auth/refresh", nil)
|
||||
refreshReq.AddCookie(refreshCookie)
|
||||
refreshW := httptest.NewRecorder()
|
||||
router.ServeHTTP(refreshW, refreshReq)
|
||||
assert.Equal(t, http.StatusUnauthorized, refreshW.Code, "Revoked refresh token should return 401")
|
||||
}
|
||||
186
veza-backend-api/tests/integration/oauth_github_test.go
Normal file
186
veza-backend-api/tests/integration/oauth_github_test.go
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
//go:build integration
|
||||
// +build integration
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/zap/zaptest"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"veza-backend-api/internal/config"
|
||||
"veza-backend-api/internal/database"
|
||||
"veza-backend-api/internal/handlers"
|
||||
"veza-backend-api/internal/models"
|
||||
"veza-backend-api/internal/repositories"
|
||||
"veza-backend-api/internal/services"
|
||||
"veza-backend-api/internal/testutils"
|
||||
)
|
||||
|
||||
// setupOAuthGitHubTestRouter creates a test router with OAuth GitHub flow (mock provider)
|
||||
func setupOAuthGitHubTestRouter(t *testing.T) (*gin.Engine, *services.OAuthService, *services.JWTService, *gorm.DB, *httptest.Server, func()) {
|
||||
ctx := context.Background()
|
||||
gin.SetMode(gin.TestMode)
|
||||
logger := zaptest.NewLogger(t)
|
||||
|
||||
dsn, err := testutils.GetTestContainerDB(ctx)
|
||||
if err != nil {
|
||||
t.Skipf("Skipping test: PostgreSQL testcontainer not available: %v", err)
|
||||
return nil, nil, nil, nil, nil, func() {}
|
||||
}
|
||||
|
||||
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
|
||||
sqlDB, err := db.DB()
|
||||
require.NoError(t, err)
|
||||
|
||||
dbWrapper := &database.Database{
|
||||
DB: sqlDB,
|
||||
GormDB: db,
|
||||
Logger: logger,
|
||||
}
|
||||
|
||||
// Mock OAuth provider: token exchange + userinfo (GitHub format)
|
||||
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/token" && r.Method == http.MethodPost {
|
||||
// PKCE: code_verifier is sent in the token exchange (verified by oauth2 library)
|
||||
_ = r.ParseForm()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"access_token": "mock_github_access_token",
|
||||
"token_type": "Bearer",
|
||||
"expires_in": 3600,
|
||||
"refresh_token": "mock_github_refresh_token",
|
||||
})
|
||||
return
|
||||
}
|
||||
if r.URL.Path == "/userinfo" && r.Method == http.MethodGet {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"id": 12345,
|
||||
"login": "ghuser",
|
||||
"email": "oauth-github@test.com",
|
||||
"name": "GitHub OAuth User",
|
||||
})
|
||||
return
|
||||
}
|
||||
http.NotFound(w, r)
|
||||
}))
|
||||
|
||||
jwtService, err := services.NewJWTService("test-secret-key-must-be-32-chars-long", "test-issuer", "test-audience")
|
||||
require.NoError(t, err)
|
||||
|
||||
sessionService := services.NewSessionService(dbWrapper, logger)
|
||||
userRepo := repositories.NewGormUserRepository(db)
|
||||
userService := services.NewUserServiceWithDB(userRepo, db)
|
||||
|
||||
oauthCfg := &services.OAuthServiceConfig{
|
||||
TestTokenURL: mockServer.URL + "/token",
|
||||
TestUserInfoURL: mockServer.URL + "/userinfo",
|
||||
FrontendURL: "http://localhost:5173",
|
||||
AllowedDomains: []string{"*"},
|
||||
}
|
||||
|
||||
oauthService := services.NewOAuthService(dbWrapper, logger, jwtService, sessionService, userService, oauthCfg)
|
||||
oauthService.InitializeConfigs("", "", "test-client", "test-secret", "", "", "", "", "http://localhost:8080")
|
||||
|
||||
cfg := &config.Config{
|
||||
CookiePath: "/",
|
||||
CookieDomain: "",
|
||||
CookieHttpOnly: true,
|
||||
CookieSecure: false,
|
||||
CookieSameSite: "lax",
|
||||
JWTService: jwtService,
|
||||
}
|
||||
|
||||
oauthHandler := handlers.NewOAuthHandler(oauthService, logger, nil, "http://localhost:5173", cfg)
|
||||
|
||||
router := gin.New()
|
||||
authGroup := router.Group("/auth")
|
||||
oauthGroup := authGroup.Group("/oauth")
|
||||
{
|
||||
oauthGroup.GET("/:provider", oauthHandler.InitiateOAuth)
|
||||
oauthGroup.GET("/:provider/callback", oauthHandler.OAuthCallback)
|
||||
}
|
||||
|
||||
cleanup := func() {
|
||||
mockServer.Close()
|
||||
}
|
||||
|
||||
return router, oauthService, jwtService, db, mockServer, cleanup
|
||||
}
|
||||
|
||||
// TestOAuthGitHubFlow tests the full OAuth GitHub flow with mocked provider and PKCE
|
||||
func TestOAuthGitHubFlow(t *testing.T) {
|
||||
router, oauthService, jwtService, db, _, cleanup := setupOAuthGitHubTestRouter(t)
|
||||
defer cleanup()
|
||||
|
||||
if router == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Verify PKCE: GetAuthURL should include code_challenge in the URL
|
||||
authURL, err := oauthService.GetAuthURL("github")
|
||||
require.NoError(t, err)
|
||||
parsed, err := url.Parse(authURL)
|
||||
require.NoError(t, err)
|
||||
codeChallenge := parsed.Query().Get("code_challenge")
|
||||
codeChallengeMethod := parsed.Query().Get("code_challenge_method")
|
||||
assert.NotEmpty(t, codeChallenge, "code_challenge must be present in auth URL for PKCE")
|
||||
assert.Equal(t, "S256", codeChallengeMethod, "code_challenge_method should be S256")
|
||||
|
||||
// Generate state token (simulate InitiateOAuth - we need the state for callback)
|
||||
stateToken, codeVerifier, err := oauthService.GenerateStateToken("github", "http://localhost:5173")
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, stateToken)
|
||||
require.NotEmpty(t, codeVerifier)
|
||||
|
||||
// Simulate OAuth callback with mock code
|
||||
callbackURL := "/auth/oauth/github/callback?code=mock_github_code&state=" + stateToken
|
||||
req := httptest.NewRequest(http.MethodGet, callbackURL, nil)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusTemporaryRedirect, w.Code, "Expected redirect after OAuth callback")
|
||||
|
||||
// Verify cookies are set
|
||||
cookies := w.Result().Cookies()
|
||||
var accessTokenCookie, refreshTokenCookie *http.Cookie
|
||||
for _, c := range cookies {
|
||||
if c.Name == "access_token" {
|
||||
accessTokenCookie = c
|
||||
}
|
||||
if c.Name == "refresh_token" {
|
||||
refreshTokenCookie = c
|
||||
}
|
||||
}
|
||||
|
||||
require.NotNil(t, accessTokenCookie, "access_token cookie should be set")
|
||||
require.NotNil(t, refreshTokenCookie, "refresh_token cookie should be set")
|
||||
assert.NotEmpty(t, accessTokenCookie.Value)
|
||||
assert.True(t, accessTokenCookie.HttpOnly)
|
||||
|
||||
// Validate JWT
|
||||
claims, err := jwtService.ValidateToken(accessTokenCookie.Value)
|
||||
require.NoError(t, err)
|
||||
assert.NotEqual(t, claims.UserID, uuid.Nil)
|
||||
|
||||
// Verify user was created with GitHub data
|
||||
var user models.User
|
||||
err = db.Where("email = ?", "oauth-github@test.com").First(&user).Error
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "oauth-github@test.com", user.Email)
|
||||
}
|
||||
179
veza-backend-api/tests/integration/oauth_google_test.go
Normal file
179
veza-backend-api/tests/integration/oauth_google_test.go
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
//go:build integration
|
||||
// +build integration
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/zap/zaptest"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"veza-backend-api/internal/config"
|
||||
"veza-backend-api/internal/database"
|
||||
"veza-backend-api/internal/handlers"
|
||||
"veza-backend-api/internal/models"
|
||||
"veza-backend-api/internal/repositories"
|
||||
"veza-backend-api/internal/services"
|
||||
"veza-backend-api/internal/testutils"
|
||||
)
|
||||
|
||||
// setupOAuthGoogleTestRouter creates a test router with OAuth Google flow (mock provider)
|
||||
func setupOAuthGoogleTestRouter(t *testing.T) (*gin.Engine, *services.OAuthService, *services.JWTService, *gorm.DB, *httptest.Server, func()) {
|
||||
ctx := context.Background()
|
||||
gin.SetMode(gin.TestMode)
|
||||
logger := zaptest.NewLogger(t)
|
||||
|
||||
dsn, err := testutils.GetTestContainerDB(ctx)
|
||||
if err != nil {
|
||||
t.Skipf("Skipping test: PostgreSQL testcontainer not available: %v", err)
|
||||
return nil, nil, nil, nil, nil, func() {}
|
||||
}
|
||||
|
||||
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
|
||||
sqlDB, err := db.DB()
|
||||
require.NoError(t, err)
|
||||
|
||||
dbWrapper := &database.Database{
|
||||
DB: sqlDB,
|
||||
GormDB: db,
|
||||
Logger: logger,
|
||||
}
|
||||
|
||||
// Mock OAuth provider: token exchange + userinfo
|
||||
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/token" && r.Method == http.MethodPost {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"access_token": "mock_access_token_123",
|
||||
"token_type": "Bearer",
|
||||
"expires_in": 3600,
|
||||
"refresh_token": "mock_refresh_token",
|
||||
})
|
||||
return
|
||||
}
|
||||
if r.URL.Path == "/userinfo" && r.Method == http.MethodGet {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"id": "google123",
|
||||
"email": "oauth-google@test.com",
|
||||
"name": "OAuth Google User",
|
||||
})
|
||||
return
|
||||
}
|
||||
http.NotFound(w, r)
|
||||
}))
|
||||
|
||||
jwtService, err := services.NewJWTService("test-secret-key-must-be-32-chars-long", "test-issuer", "test-audience")
|
||||
require.NoError(t, err)
|
||||
|
||||
sessionService := services.NewSessionService(dbWrapper, logger)
|
||||
userRepo := repositories.NewGormUserRepository(db)
|
||||
userService := services.NewUserServiceWithDB(userRepo, db)
|
||||
|
||||
oauthCfg := &services.OAuthServiceConfig{
|
||||
TestTokenURL: mockServer.URL + "/token",
|
||||
TestUserInfoURL: mockServer.URL + "/userinfo",
|
||||
FrontendURL: "http://localhost:5173",
|
||||
AllowedDomains: []string{"*"},
|
||||
}
|
||||
|
||||
oauthService := services.NewOAuthService(dbWrapper, logger, jwtService, sessionService, userService, oauthCfg)
|
||||
oauthService.InitializeConfigs("test-client", "test-secret", "", "", "", "", "", "", "http://localhost:8080")
|
||||
|
||||
cfg := &config.Config{
|
||||
CookiePath: "/",
|
||||
CookieDomain: "",
|
||||
CookieHttpOnly: true,
|
||||
CookieSecure: false,
|
||||
CookieSameSite: "lax",
|
||||
JWTService: jwtService,
|
||||
}
|
||||
|
||||
oauthHandler := handlers.NewOAuthHandler(oauthService, logger, nil, "http://localhost:5173", cfg)
|
||||
|
||||
router := gin.New()
|
||||
authGroup := router.Group("/auth")
|
||||
oauthGroup := authGroup.Group("/oauth")
|
||||
{
|
||||
oauthGroup.GET("/:provider", oauthHandler.InitiateOAuth)
|
||||
oauthGroup.GET("/:provider/callback", oauthHandler.OAuthCallback)
|
||||
}
|
||||
|
||||
cleanup := func() {
|
||||
mockServer.Close()
|
||||
}
|
||||
|
||||
return router, oauthService, jwtService, db, mockServer, cleanup
|
||||
}
|
||||
|
||||
// TestOAuthGoogleFlow tests the full OAuth Google flow with mocked provider
|
||||
func TestOAuthGoogleFlow(t *testing.T) {
|
||||
router, oauthService, jwtService, db, _, cleanup := setupOAuthGoogleTestRouter(t)
|
||||
defer cleanup()
|
||||
|
||||
if router == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Generate state token and insert into DB (simulate InitiateOAuth step)
|
||||
stateToken, codeVerifier, err := oauthService.GenerateStateToken("google", "http://localhost:5173")
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, stateToken)
|
||||
require.NotEmpty(t, codeVerifier)
|
||||
|
||||
// Simulate OAuth callback with mock code
|
||||
callbackURL := "/auth/oauth/google/callback?code=mock_code&state=" + stateToken
|
||||
req := httptest.NewRequest(http.MethodGet, callbackURL, nil)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusTemporaryRedirect, w.Code, "Expected redirect after OAuth callback")
|
||||
|
||||
// Verify cookies are set
|
||||
cookies := w.Result().Cookies()
|
||||
var accessTokenCookie, refreshTokenCookie *http.Cookie
|
||||
for _, c := range cookies {
|
||||
if c.Name == "access_token" {
|
||||
accessTokenCookie = c
|
||||
}
|
||||
if c.Name == "refresh_token" {
|
||||
refreshTokenCookie = c
|
||||
}
|
||||
}
|
||||
|
||||
require.NotNil(t, accessTokenCookie, "access_token cookie should be set")
|
||||
require.NotNil(t, refreshTokenCookie, "refresh_token cookie should be set")
|
||||
assert.NotEmpty(t, accessTokenCookie.Value, "access_token should not be empty")
|
||||
assert.NotEmpty(t, refreshTokenCookie.Value, "refresh_token should not be empty")
|
||||
assert.True(t, accessTokenCookie.HttpOnly, "access_token should be httpOnly")
|
||||
assert.True(t, refreshTokenCookie.HttpOnly, "refresh_token should be httpOnly")
|
||||
|
||||
// Validate JWT with JWTService
|
||||
claims, err := jwtService.ValidateToken(accessTokenCookie.Value)
|
||||
require.NoError(t, err)
|
||||
assert.NotEqual(t, claims.UserID, uuid.Nil)
|
||||
|
||||
// Verify session exists in DB
|
||||
var sessionCount int64
|
||||
err = db.Table("sessions").Count(&sessionCount).Error
|
||||
require.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, sessionCount, int64(1), "Session should be created")
|
||||
|
||||
// Verify user was created
|
||||
var user models.User
|
||||
err = db.Where("email = ?", "oauth-google@test.com").First(&user).Error
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "oauth-google@test.com", user.Email)
|
||||
}
|
||||
|
|
@ -27,7 +27,7 @@ import (
|
|||
"veza-backend-api/internal/core/track"
|
||||
"veza-backend-api/internal/models"
|
||||
"veza-backend-api/internal/services"
|
||||
"veza-backend-api/tests/testutils"
|
||||
"veza-backend-api/internal/testutils"
|
||||
)
|
||||
|
||||
// TestResumeUploadEndpoint_ChunkedUploads tests the GET /api/v1/tracks/resume/:uploadId endpoint
|
||||
|
|
|
|||
233
veza-backend-api/tests/integration/token_refresh_test.go
Normal file
233
veza-backend-api/tests/integration/token_refresh_test.go
Normal file
|
|
@ -0,0 +1,233 @@
|
|||
//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)
|
||||
}
|
||||
|
|
@ -22,7 +22,7 @@ import (
|
|||
"veza-backend-api/internal/core/track"
|
||||
"veza-backend-api/internal/models"
|
||||
"veza-backend-api/internal/services"
|
||||
"veza-backend-api/tests/testutils"
|
||||
"veza-backend-api/internal/testutils"
|
||||
)
|
||||
|
||||
// TestTrackQuotaEndpoint_GetQuota tests the GET /api/v1/tracks/quota/:id endpoint
|
||||
|
|
@ -99,7 +99,7 @@ func TestTrackQuotaEndpoint_GetQuota(t *testing.T) {
|
|||
track1 := &models.Track{
|
||||
ID: uuid.New(),
|
||||
Title: "Test Track 1",
|
||||
CreatorID: userID,
|
||||
UserID: userID,
|
||||
FileSize: 5 * 1024 * 1024, // 5MB
|
||||
IsPublic: true,
|
||||
}
|
||||
|
|
@ -108,7 +108,7 @@ func TestTrackQuotaEndpoint_GetQuota(t *testing.T) {
|
|||
track2 := &models.Track{
|
||||
ID: uuid.New(),
|
||||
Title: "Test Track 2",
|
||||
CreatorID: userID,
|
||||
UserID: userID,
|
||||
FileSize: 10 * 1024 * 1024, // 10MB
|
||||
IsPublic: true,
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue