653 lines
20 KiB
Go
653 lines
20 KiB
Go
package controllers
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"golang.org/x/crypto/bcrypt"
|
|
"golang.org/x/oauth2"
|
|
"gorm.io/driver/sqlite"
|
|
"gorm.io/gorm"
|
|
|
|
"goaresv3/app/accounts/models"
|
|
"goaresv3/config"
|
|
jwtHelper "goaresv3/pkg/jwt"
|
|
)
|
|
|
|
func setupTestDB(t *testing.T) *gorm.DB {
|
|
t.Helper()
|
|
|
|
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
|
|
if err != nil {
|
|
t.Fatalf("failed to open sqlite db: %v", err)
|
|
}
|
|
if err := db.AutoMigrate(&models.User{}); err != nil {
|
|
t.Fatalf("failed to migrate user model: %v", err)
|
|
}
|
|
if err := db.AutoMigrate(&models.SocialAccount{}); err != nil {
|
|
t.Fatalf("failed to migrate social account model: %v", err)
|
|
}
|
|
config.DB = db
|
|
return db
|
|
}
|
|
|
|
func boolPtr(v bool) *bool {
|
|
return &v
|
|
}
|
|
|
|
func TestVerifyEmailSuccess(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
db := setupTestDB(t)
|
|
|
|
user := models.User{UserName: "u1", Email: "u1@example.com", EmailVerified: boolPtr(false), EmailVerifyToken: "tok-123"}
|
|
if err := db.Create(&user).Error; err != nil {
|
|
t.Fatalf("failed to create user: %v", err)
|
|
}
|
|
|
|
r := gin.New()
|
|
r.GET("/api/v1/auth/verify-email", VerifyEmail)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/auth/verify-email?token=tok-123", nil)
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", w.Code)
|
|
}
|
|
|
|
var updated models.User
|
|
if err := db.First(&updated, user.ID).Error; err != nil {
|
|
t.Fatalf("failed to fetch updated user: %v", err)
|
|
}
|
|
if !updated.IsEmailVerified() {
|
|
t.Fatal("expected email_verified=true")
|
|
}
|
|
if updated.EmailVerifyToken != "" {
|
|
t.Fatalf("expected email_verify_token cleared, got %q", updated.EmailVerifyToken)
|
|
}
|
|
}
|
|
|
|
func TestVerifyEmailMissingToken(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
setupTestDB(t)
|
|
|
|
r := gin.New()
|
|
r.GET("/api/v1/auth/verify-email", VerifyEmail)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/auth/verify-email", nil)
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected 400, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestVerifyEmailInvalidToken(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
setupTestDB(t)
|
|
|
|
r := gin.New()
|
|
r.GET("/api/v1/auth/verify-email", VerifyEmail)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/auth/verify-email?token=missing", nil)
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected 400, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestLoginRejectsUnverifiedUser(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
db := setupTestDB(t)
|
|
t.Setenv("JWT_SECRET", "test-secret-1234567890")
|
|
t.Setenv("JWT_REFRESH_SECRET", "test-refresh-secret-1234567890")
|
|
|
|
hashed, _ := bcrypt.GenerateFromPassword([]byte("password123"), bcrypt.DefaultCost)
|
|
user := models.User{UserName: "u2", Email: "u2@example.com", Password: string(hashed), EmailVerified: boolPtr(false)}
|
|
if err := db.Create(&user).Error; err != nil {
|
|
t.Fatalf("failed to create user: %v", err)
|
|
}
|
|
|
|
body, _ := json.Marshal(LoginRequest{Email: "u2@example.com", Password: "password123"})
|
|
r := gin.New()
|
|
r.POST("/api/v1/auth/login", Login)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/login", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusForbidden {
|
|
t.Fatalf("expected 403, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestLoginBadRequest(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
setupTestDB(t)
|
|
|
|
r := gin.New()
|
|
r.POST("/api/v1/auth/login", Login)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/login", bytes.NewBufferString(`{"email":"bad"}`))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected 400, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestLoginInvalidCredentials(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
db := setupTestDB(t)
|
|
t.Setenv("JWT_SECRET", "test-secret-1234567890")
|
|
t.Setenv("JWT_REFRESH_SECRET", "test-refresh-secret-1234567890")
|
|
|
|
hashed, _ := bcrypt.GenerateFromPassword([]byte("password123"), bcrypt.DefaultCost)
|
|
user := models.User{UserName: "u2x", Email: "u2x@example.com", Password: string(hashed), EmailVerified: boolPtr(true)}
|
|
if err := db.Create(&user).Error; err != nil {
|
|
t.Fatalf("failed to create user: %v", err)
|
|
}
|
|
|
|
body, _ := json.Marshal(LoginRequest{Email: "u2x@example.com", Password: "wrong-pass"})
|
|
r := gin.New()
|
|
r.POST("/api/v1/auth/login", Login)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/login", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusUnauthorized {
|
|
t.Fatalf("expected 401, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestLoginVerifiedUserSuccess(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
db := setupTestDB(t)
|
|
t.Setenv("JWT_SECRET", "test-secret-1234567890")
|
|
t.Setenv("JWT_REFRESH_SECRET", "test-refresh-secret-1234567890")
|
|
|
|
hashed, _ := bcrypt.GenerateFromPassword([]byte("password123"), bcrypt.DefaultCost)
|
|
user := models.User{UserName: "u3", Email: "u3@example.com", Password: string(hashed), EmailVerified: boolPtr(true)}
|
|
if err := db.Create(&user).Error; err != nil {
|
|
t.Fatalf("failed to create user: %v", err)
|
|
}
|
|
|
|
body, _ := json.Marshal(LoginRequest{Email: "u3@example.com", Password: "password123"})
|
|
r := gin.New()
|
|
r.POST("/api/v1/auth/login", Login)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/login", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestRefreshRejectsUnverifiedUser(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
db := setupTestDB(t)
|
|
t.Setenv("JWT_SECRET", "test-secret-1234567890")
|
|
t.Setenv("JWT_REFRESH_SECRET", "test-refresh-secret-1234567890")
|
|
|
|
user := models.User{UserName: "u4", Email: "u4@example.com", EmailVerified: boolPtr(false)}
|
|
if err := db.Create(&user).Error; err != nil {
|
|
t.Fatalf("failed to create user: %v", err)
|
|
}
|
|
|
|
rt, err := jwtHelper.GenerateRefreshToken(user.ID, user.Email, user.UserName)
|
|
if err != nil {
|
|
t.Fatalf("failed to generate refresh token: %v", err)
|
|
}
|
|
|
|
body, _ := json.Marshal(RefreshRequest{RefreshToken: rt})
|
|
r := gin.New()
|
|
r.POST("/api/v1/auth/refresh", RefreshToken)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/refresh", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusForbidden {
|
|
t.Fatalf("expected 403, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestRefreshBadRequest(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
setupTestDB(t)
|
|
|
|
r := gin.New()
|
|
r.POST("/api/v1/auth/refresh", RefreshToken)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/refresh", bytes.NewBufferString(`{"x":1}`))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected 400, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestRefreshInvalidToken(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
setupTestDB(t)
|
|
t.Setenv("JWT_REFRESH_SECRET", "test-refresh-secret-1234567890")
|
|
|
|
r := gin.New()
|
|
r.POST("/api/v1/auth/refresh", RefreshToken)
|
|
|
|
body := []byte(`{"refresh_token":"not-a-jwt"}`)
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/refresh", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusUnauthorized {
|
|
t.Fatalf("expected 401, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestRefreshInvalidUser(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
setupTestDB(t)
|
|
t.Setenv("JWT_REFRESH_SECRET", "test-refresh-secret-1234567890")
|
|
|
|
rt, err := jwtHelper.GenerateRefreshToken(9999, "ghost@example.com", "ghost")
|
|
if err != nil {
|
|
t.Fatalf("failed to generate refresh token: %v", err)
|
|
}
|
|
|
|
body, _ := json.Marshal(RefreshRequest{RefreshToken: rt})
|
|
r := gin.New()
|
|
r.POST("/api/v1/auth/refresh", RefreshToken)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/refresh", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusUnauthorized {
|
|
t.Fatalf("expected 401, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestRegisterPasswordMismatch(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
setupTestDB(t)
|
|
|
|
r := gin.New()
|
|
r.POST("/api/v1/auth/register", Register)
|
|
|
|
body := []byte(`{"username":"u","email":"u@example.com","password":"password123","confirm_password":"different123"}`)
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/register", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected 400, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestRegisterDuplicateEmail(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
db := setupTestDB(t)
|
|
|
|
verified := true
|
|
hashed, _ := bcrypt.GenerateFromPassword([]byte("password123"), bcrypt.DefaultCost)
|
|
seed := models.User{UserName: "seed", Email: "dup@example.com", Password: string(hashed), EmailVerified: &verified}
|
|
if err := db.Create(&seed).Error; err != nil {
|
|
t.Fatalf("failed to seed user: %v", err)
|
|
}
|
|
|
|
r := gin.New()
|
|
r.POST("/api/v1/auth/register", Register)
|
|
|
|
body := []byte(`{"username":"newuser","email":"dup@example.com","password":"password123","confirm_password":"password123"}`)
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/register", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusConflict {
|
|
t.Fatalf("expected 409, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestRegisterMailFailureRollsBackUser(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
db := setupTestDB(t)
|
|
|
|
t.Setenv("EMAIL_HOST", "")
|
|
t.Setenv("EMAIL_PORT", "")
|
|
t.Setenv("EMAIL_FROM", "")
|
|
|
|
r := gin.New()
|
|
r.POST("/api/v1/auth/register", Register)
|
|
|
|
body := []byte(`{"username":"rollback","email":"rollback@example.com","password":"password123","confirm_password":"password123"}`)
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/register", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusInternalServerError {
|
|
t.Fatalf("expected 500, got %d", w.Code)
|
|
}
|
|
|
|
var count int64
|
|
if err := db.Model(&models.User{}).Where("email = ?", "rollback@example.com").Count(&count).Error; err != nil {
|
|
t.Fatalf("failed to count users: %v", err)
|
|
}
|
|
if count != 0 {
|
|
t.Fatalf("expected rollback delete, user count=%d", count)
|
|
}
|
|
}
|
|
|
|
func TestMeIncludesUsernameFallbackFromDB(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
db := setupTestDB(t)
|
|
|
|
user := models.User{UserName: "fallback-user", Email: "u5@example.com", EmailVerified: boolPtr(true)}
|
|
if err := db.Create(&user).Error; err != nil {
|
|
t.Fatalf("failed to create user: %v", err)
|
|
}
|
|
|
|
w := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(w)
|
|
c.Request = httptest.NewRequest(http.MethodGet, "/api/v1/me", nil)
|
|
c.Set("user_id", user.ID)
|
|
c.Set("email", user.Email)
|
|
|
|
Me(c)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", w.Code)
|
|
}
|
|
|
|
var resp map[string]any
|
|
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
|
t.Fatalf("failed to decode response: %v", err)
|
|
}
|
|
if resp["username"] != "fallback-user" {
|
|
t.Fatalf("expected username=fallback-user, got %v", resp["username"])
|
|
}
|
|
}
|
|
|
|
func TestMeUsesContextUsernameWhenProvided(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
setupTestDB(t)
|
|
|
|
w := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(w)
|
|
c.Request = httptest.NewRequest(http.MethodGet, "/api/v1/me", nil)
|
|
c.Set("user_id", uint(99))
|
|
c.Set("email", "ctx@example.com")
|
|
c.Set("username", "ctx-user")
|
|
|
|
Me(c)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", w.Code)
|
|
}
|
|
|
|
var resp map[string]any
|
|
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
|
t.Fatalf("failed to decode response: %v", err)
|
|
}
|
|
if resp["username"] != "ctx-user" {
|
|
t.Fatalf("expected username=ctx-user, got %v", resp["username"])
|
|
}
|
|
}
|
|
|
|
func TestGoogleLoginMissingConfig(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
setupTestDB(t)
|
|
t.Setenv("SOCIAL_AUTH_GOOGLE_OAUTH2_KEY", "")
|
|
t.Setenv("SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET", "")
|
|
t.Setenv("SOCIAL_AUTH_GOOGLE_REDIRECT_URL", "")
|
|
|
|
r := gin.New()
|
|
r.GET("/api/v1/auth/google/login", GoogleLogin)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/auth/google/login", nil)
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusServiceUnavailable {
|
|
t.Fatalf("expected 503, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestGoogleCallbackInvalidState(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
setupTestDB(t)
|
|
t.Setenv("SOCIAL_AUTH_GOOGLE_OAUTH2_KEY", "client-id")
|
|
t.Setenv("SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET", "client-secret")
|
|
t.Setenv("SOCIAL_AUTH_GOOGLE_REDIRECT_URL", "http://localhost:8080/api/v1/auth/google/callback")
|
|
|
|
r := gin.New()
|
|
r.GET("/api/v1/auth/google/callback", GoogleCallback)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/auth/google/callback?state=state1&code=code1", nil)
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected 400, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestGoogleCallbackSuccessCreatesUserAndTokens(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
db := setupTestDB(t)
|
|
t.Setenv("SOCIAL_AUTH_GOOGLE_OAUTH2_KEY", "client-id")
|
|
t.Setenv("SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET", "client-secret")
|
|
t.Setenv("SOCIAL_AUTH_GOOGLE_REDIRECT_URL", "http://localhost:8080/api/v1/auth/google/callback")
|
|
t.Setenv("JWT_SECRET", "test-secret-1234567890")
|
|
t.Setenv("JWT_REFRESH_SECRET", "test-refresh-secret-1234567890")
|
|
|
|
originalExchange := exchangeGoogleCode
|
|
originalFetch := fetchGoogleUserInfo
|
|
t.Cleanup(func() {
|
|
exchangeGoogleCode = originalExchange
|
|
fetchGoogleUserInfo = originalFetch
|
|
})
|
|
|
|
exchangeGoogleCode = func(ctx context.Context, cfg *oauth2.Config, code string) (*oauth2.Token, error) {
|
|
return &oauth2.Token{AccessToken: "google-access", TokenType: "Bearer"}, nil
|
|
}
|
|
fetchGoogleUserInfo = func(ctx context.Context, cfg *oauth2.Config, token *oauth2.Token) (*googleUserInfo, error) {
|
|
return &googleUserInfo{
|
|
Sub: "google-sub-123",
|
|
Email: "google.user@example.com",
|
|
EmailVerified: true,
|
|
Name: "Google User",
|
|
Picture: "https://cdn.example.com/avatar.png",
|
|
GivenName: "Google",
|
|
FamilyName: "User",
|
|
}, nil
|
|
}
|
|
|
|
r := gin.New()
|
|
r.GET("/api/v1/auth/google/callback", GoogleCallback)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/auth/google/callback?state=state123&code=code123", nil)
|
|
req.AddCookie(&http.Cookie{Name: googleOAuthStateCookieName, Value: "state123"})
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d body=%s", w.Code, w.Body.String())
|
|
}
|
|
|
|
var resp map[string]any
|
|
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
|
t.Fatalf("failed to decode response: %v", err)
|
|
}
|
|
if resp["access_token"] == "" || resp["refresh_token"] == "" {
|
|
t.Fatal("expected access_token and refresh_token in response")
|
|
}
|
|
if resp["provider"] != "google" {
|
|
t.Fatalf("expected provider=google, got %v", resp["provider"])
|
|
}
|
|
|
|
var user models.User
|
|
if err := db.Where("email = ?", "google.user@example.com").First(&user).Error; err != nil {
|
|
t.Fatalf("expected user to be created, err=%v", err)
|
|
}
|
|
if !user.IsEmailVerified() {
|
|
t.Fatal("expected created google user to be verified")
|
|
}
|
|
|
|
var social models.SocialAccount
|
|
if err := db.Where("user_id = ? AND provider = ? AND provider_id = ?", user.ID, "google", "google-sub-123").First(&social).Error; err != nil {
|
|
t.Fatalf("expected social account to be linked, err=%v", err)
|
|
}
|
|
}
|
|
|
|
func TestGitHubLoginMissingConfig(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
setupTestDB(t)
|
|
t.Setenv("SOCIAL_AUTH_GITHUB_KEY", "")
|
|
t.Setenv("SOCIAL_AUTH_GITHUB_SECRET", "")
|
|
t.Setenv("SOCIAL_AUTH_GITHUB_REDIRECT_URL", "")
|
|
|
|
r := gin.New()
|
|
r.GET("/api/v1/auth/github/login", GitHubLogin)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/auth/github/login", nil)
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusServiceUnavailable {
|
|
t.Fatalf("expected 503, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestGitHubCallbackSuccessCreatesVerifiedUserAndTokens(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
db := setupTestDB(t)
|
|
t.Setenv("SOCIAL_AUTH_GITHUB_KEY", "gh-client-id")
|
|
t.Setenv("SOCIAL_AUTH_GITHUB_SECRET", "gh-client-secret")
|
|
t.Setenv("SOCIAL_AUTH_GITHUB_REDIRECT_URL", "http://localhost:8080/api/v1/auth/github/callback")
|
|
t.Setenv("JWT_SECRET", "test-secret-1234567890")
|
|
t.Setenv("JWT_REFRESH_SECRET", "test-refresh-secret-1234567890")
|
|
|
|
originalExchange := exchangeGitHubCode
|
|
originalFetch := fetchGitHubUserInfo
|
|
t.Cleanup(func() {
|
|
exchangeGitHubCode = originalExchange
|
|
fetchGitHubUserInfo = originalFetch
|
|
})
|
|
|
|
exchangeGitHubCode = func(ctx context.Context, cfg *oauth2.Config, code string) (*oauth2.Token, error) {
|
|
return &oauth2.Token{AccessToken: "github-access", TokenType: "Bearer"}, nil
|
|
}
|
|
fetchGitHubUserInfo = func(ctx context.Context, cfg *oauth2.Config, token *oauth2.Token) (*socialUserProfile, error) {
|
|
return &socialUserProfile{
|
|
ProviderID: "4242",
|
|
Email: "github.user@example.com",
|
|
Name: "GitHub User",
|
|
AvatarURL: "https://cdn.example.com/gh-avatar.png",
|
|
EmailVerified: true,
|
|
}, nil
|
|
}
|
|
|
|
r := gin.New()
|
|
r.GET("/api/v1/auth/github/callback", GitHubCallback)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/auth/github/callback?state=state-gh-123&code=code-gh-123", nil)
|
|
req.AddCookie(&http.Cookie{Name: githubOAuthStateCookieName, Value: "state-gh-123"})
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d body=%s", w.Code, w.Body.String())
|
|
}
|
|
|
|
var resp map[string]any
|
|
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
|
t.Fatalf("failed to decode response: %v", err)
|
|
}
|
|
if resp["access_token"] == "" || resp["refresh_token"] == "" {
|
|
t.Fatal("expected access_token and refresh_token in response")
|
|
}
|
|
if resp["provider"] != "github" {
|
|
t.Fatalf("expected provider=github, got %v", resp["provider"])
|
|
}
|
|
|
|
var user models.User
|
|
if err := db.Where("email = ?", "github.user@example.com").First(&user).Error; err != nil {
|
|
t.Fatalf("expected user to be created, err=%v", err)
|
|
}
|
|
if !user.IsEmailVerified() {
|
|
t.Fatal("expected created github user to be verified")
|
|
}
|
|
|
|
var social models.SocialAccount
|
|
if err := db.Where("user_id = ? AND provider = ? AND provider_id = ?", user.ID, "github", "4242").First(&social).Error; err != nil {
|
|
t.Fatalf("expected github social account to be linked, err=%v", err)
|
|
}
|
|
}
|
|
|
|
func TestGoogleOAuthConfigSupportsSocialAuthEnv(t *testing.T) {
|
|
t.Setenv("SOCIAL_AUTH_GOOGLE_OAUTH2_KEY", "'google-client-id-from-social'")
|
|
t.Setenv("SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET", "'google-client-secret-from-social'")
|
|
t.Setenv("SOCIAL_AUTH_GOOGLE_REDIRECT_URL", "")
|
|
t.Setenv("APP_BASE_URL", "http://localhost:8080")
|
|
|
|
cfg, err := getGoogleOAuthConfig()
|
|
if err != nil {
|
|
t.Fatalf("expected config, got error: %v", err)
|
|
}
|
|
if cfg.ClientID != "google-client-id-from-social" {
|
|
t.Fatalf("unexpected client id: %q", cfg.ClientID)
|
|
}
|
|
if cfg.ClientSecret != "google-client-secret-from-social" {
|
|
t.Fatalf("unexpected client secret: %q", cfg.ClientSecret)
|
|
}
|
|
if cfg.RedirectURL != "http://localhost:8080/api/v1/auth/google/callback" {
|
|
t.Fatalf("unexpected redirect url: %q", cfg.RedirectURL)
|
|
}
|
|
}
|
|
|
|
func TestGitHubOAuthConfigSupportsSocialAuthEnv(t *testing.T) {
|
|
t.Setenv("SOCIAL_AUTH_GITHUB_KEY", "'github-client-id-from-social'")
|
|
t.Setenv("SOCIAL_AUTH_GITHUB_SECRET", "'github-client-secret-from-social'")
|
|
t.Setenv("SOCIAL_AUTH_GITHUB_REDIRECT_URL", "")
|
|
t.Setenv("APP_BASE_URL", "http://localhost:8080")
|
|
|
|
cfg, err := getGitHubOAuthConfig()
|
|
if err != nil {
|
|
t.Fatalf("expected config, got error: %v", err)
|
|
}
|
|
if cfg.ClientID != "github-client-id-from-social" {
|
|
t.Fatalf("unexpected client id: %q", cfg.ClientID)
|
|
}
|
|
if cfg.ClientSecret != "github-client-secret-from-social" {
|
|
t.Fatalf("unexpected client secret: %q", cfg.ClientSecret)
|
|
}
|
|
if cfg.RedirectURL != "http://localhost:8080/api/v1/auth/github/callback" {
|
|
t.Fatalf("unexpected redirect url: %q", cfg.RedirectURL)
|
|
}
|
|
}
|