- 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
186 lines
5.8 KiB
Go
186 lines
5.8 KiB
Go
//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)
|
|
}
|