first commit
This commit is contained in:
652
app/accounts/controllers/user_test.go
Normal file
652
app/accounts/controllers/user_test.go
Normal file
@@ -0,0 +1,652 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user