package auth import ( "bytes" "context" "encoding/json" "net/http" "net/http/httptest" "testing" "veza-backend-api/internal/dto" "veza-backend-api/internal/models" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "go.uber.org/zap/zaptest" "gorm.io/gorm" ) func setupTestAuthHandler(t *testing.T) (*AuthHandler, *gin.Engine, *TestMocks, *gorm.DB, func()) { service, db, mocks, cleanupService := setupTestAuthService(t) gin.SetMode(gin.TestMode) router := gin.New() handler := NewAuthHandler( service, nil, // sessionService zaptest.NewLogger(t), ) return handler, router, mocks, db, func() { cleanupService() } } // expectRegister wires the mocks Register touches. v1.0.9 item 1.4 removed // JWT issuance from Register, so the JWT/RefreshToken expectations live on // the Login or Refresh setup of the calling test instead. func expectRegister(mocks *TestMocks) { mocks.EmailVerification.On("GenerateToken").Return("verification-token", nil).Maybe() mocks.EmailVerification.On("StoreToken", mock.Anything, mock.Anything, "verification-token").Return(nil).Maybe() mocks.Email.On("SendVerificationEmail", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(nil).Maybe() } // registerVerifyLogin is the canonical "I need a logged-in user with a // real refresh token" recipe for tests that exercise post-login flows // (refresh, logout). It replaces the v1.0.6-era pattern where Register // returned a tokenPair directly. func registerVerifyLogin(t *testing.T, service *AuthService, db *gorm.DB, mocks *TestMocks, email, username, password string) (*models.User, *models.TokenPair) { t.Helper() ctx := context.Background() user, err := service.Register(ctx, email, username, password) require.NoError(t, err) require.NoError(t, db.Model(&models.User{}).Where("id = ?", user.ID).Update("is_verified", true).Error) mocks.JWT.On("GenerateAccessToken", mock.AnythingOfType("*models.User")).Return("access-token", nil).Once() mocks.JWT.On("GenerateRefreshToken", mock.AnythingOfType("*models.User")).Return("refresh-token", nil).Once() mocks.RefreshToken.On("Store", mock.Anything, "refresh-token", mock.Anything).Return(nil).Once() loggedInUser, tokens, err := service.Login(ctx, email, password, false) require.NoError(t, err) require.NotNil(t, tokens) return loggedInUser, tokens } func TestAuthHandler_Register_Success(t *testing.T) { handler, router, mocks, _, cleanup := setupTestAuthHandler(t) defer cleanup() router.POST("/register", handler.Register) reqBody := dto.RegisterRequest{ Email: "handler@example.com", Username: "handleruser", Password: "StrongPassword123!", PasswordConfirm: "StrongPassword123!", } body, _ := json.Marshal(reqBody) expectRegister(mocks) req, _ := http.NewRequest("POST", "/register", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusCreated, w.Code) var resp dto.RegisterResponse err := json.Unmarshal(w.Body.Bytes(), &resp) require.NoError(t, err) assert.Equal(t, reqBody.Email, resp.User.Email) assert.Equal(t, reqBody.Username, resp.User.Username) // v1.0.9 item 1.4 — Register no longer issues tokens; the response // signals the frontend to route the user to "check your email". assert.True(t, resp.VerificationRequired) assert.NotEmpty(t, resp.Message) } func TestAuthHandler_Login_Success(t *testing.T) { handler, router, mocks, db, cleanup := setupTestAuthHandler(t) defer cleanup() router.POST("/login", handler.Login) // Pre-register user directly via service ctx := context.Background() expectRegister(mocks) registeredUser, err := handler.authService.Register(ctx, "login_h@example.com", "login_h", "StrongPassword123!") require.NoError(t, err) // Simulate the user clicking the verification link — Register leaves // is_verified=false and Login refuses unverified users. require.NoError(t, db.Model(&models.User{}).Where("id = ?", registeredUser.ID).Update("is_verified", true).Error) reqBody := dto.LoginRequest{ Email: "login_h@example.com", Password: "StrongPassword123!", } body, _ := json.Marshal(reqBody) req, _ := http.NewRequest("POST", "/login", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() // Login expectations mocks.JWT.On("GenerateAccessToken", mock.AnythingOfType("*models.User")).Return("new-access-token", nil).Once() mocks.JWT.On("GenerateRefreshToken", mock.AnythingOfType("*models.User")).Return("new-refresh-token", nil).Once() mocks.RefreshToken.On("Store", mock.Anything, "new-refresh-token", mock.Anything).Return(nil).Once() router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) var resp dto.LoginResponse err = json.Unmarshal(w.Body.Bytes(), &resp) require.NoError(t, err) assert.Equal(t, reqBody.Email, resp.User.Email) assert.NotEmpty(t, resp.Token.AccessToken) } func TestAuthHandler_Login_InvalidCredentials(t *testing.T) { handler, router, _, _, cleanup := setupTestAuthHandler(t) defer cleanup() router.POST("/login", handler.Login) reqBody := dto.LoginRequest{ Email: "nonexistent@example.com", Password: "StrongPassword123!", } body, _ := json.Marshal(reqBody) req, _ := http.NewRequest("POST", "/login", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusUnauthorized, w.Code) } func TestAuthHandler_Refresh_Success(t *testing.T) { handler, _, mocks, db, cleanup := setupTestAuthHandler(t) defer cleanup() expectRegister(mocks) // v1.0.9 item 1.4 — Register no longer issues tokens. We must verify // the user and call Login to obtain a refresh token to test refresh. user, tokenPair := registerVerifyLogin(t, handler.authService, db, mocks, "refresh_h@example.com", "refresh_h", "StrongPassword123!") reqBody := dto.RefreshRequest{ RefreshToken: tokenPair.RefreshToken, } body, _ := json.Marshal(reqBody) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request, _ = http.NewRequest("POST", "/refresh", bytes.NewBuffer(body)) c.Request.Header.Set("Content-Type", "application/json") // Refresh expectations claims := &models.CustomClaims{UserID: user.ID, IsRefresh: true} mocks.JWT.On("ValidateToken", tokenPair.RefreshToken).Return(claims, nil) mocks.RefreshToken.On("Validate", user.ID, tokenPair.RefreshToken).Return(nil) mocks.JWT.On("GenerateAccessToken", mock.AnythingOfType("*models.User")).Return("refreshed-access-token", nil) mocks.JWT.On("GenerateRefreshToken", mock.AnythingOfType("*models.User")).Return("refreshed-refresh-token", nil) mocks.RefreshToken.On("Rotate", user.ID, tokenPair.RefreshToken, "refreshed-refresh-token", mock.Anything).Return(nil) handler.Refresh(c) assert.Equal(t, http.StatusOK, w.Code) var resp dto.TokenResponse err := json.Unmarshal(w.Body.Bytes(), &resp) require.NoError(t, err) assert.NotEmpty(t, resp.AccessToken) } func TestAuthHandler_CheckUsername_Available(t *testing.T) { handler, _, _, _, cleanup := setupTestAuthHandler(t) defer cleanup() w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request, _ = http.NewRequest("GET", "/check-username?username=newuser_check", nil) handler.CheckUsername(c) assert.Equal(t, http.StatusOK, w.Code) var resp map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &resp) require.NoError(t, err) val, ok := resp["available"] assert.True(t, ok) assert.Equal(t, true, val) } func TestAuthHandler_GetMe_Success(t *testing.T) { handler, _, mocks, _, cleanup := setupTestAuthHandler(t) defer cleanup() ctx := context.Background() expectRegister(mocks) user, err := handler.authService.Register(ctx, "me@example.com", "meuser", "StrongPassword123!") require.NoError(t, err) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request, _ = http.NewRequest("GET", "/me", nil) c.Set("user_id", user.ID) c.Set("email", user.Email) c.Set("role", user.Role) handler.GetMe(c) assert.Equal(t, http.StatusOK, w.Code) var resp map[string]interface{} err = json.Unmarshal(w.Body.Bytes(), &resp) require.NoError(t, err) assert.Equal(t, user.Email, resp["email"]) } func TestAuthHandler_Logout_Success(t *testing.T) { handler, _, mocks, db, cleanup := setupTestAuthHandler(t) defer cleanup() expectRegister(mocks) // v1.0.9 item 1.4 — Register no longer issues tokens; we acquire a // refresh token via the post-verification Login path. user, tokenPair := registerVerifyLogin(t, handler.authService, db, mocks, "logout_h@example.com", "logout_h", "StrongPassword123!") reqBody := struct { RefreshToken string `json:"refresh_token"` }{ RefreshToken: tokenPair.RefreshToken, } body, _ := json.Marshal(reqBody) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request, _ = http.NewRequest("POST", "/logout", bytes.NewBuffer(body)) c.Request.Header.Set("Content-Type", "application/json") c.Set("user_id", user.ID) // Logout expectations claims := &models.CustomClaims{UserID: user.ID} mocks.JWT.On("ValidateToken", tokenPair.RefreshToken).Return(claims, nil) mocks.RefreshToken.On("Revoke", user.ID, tokenPair.RefreshToken).Return(nil) handler.Logout(c) assert.Equal(t, http.StatusOK, w.Code) }