From 58ed81cbae67426dbd5c02a0cb7196a65f81f2d5 Mon Sep 17 00:00:00 2001 From: Samikshaaggarwal <123529409+Samikshaaggarwal@users.noreply.github.com> Date: Thu, 4 Jun 2026 10:09:13 +0530 Subject: [PATCH 1/4] refactor/issue-25 Implement lock/unlock user admin action --- internal/handler/admin_handler.go | 44 ++++++- internal/handler/admin_handler_test.go | 170 +++++++++++++++++++++++++ internal/service/auth_service.go | 59 +++++++++ internal/service/auth_service_test.go | 125 ++++++++++++++++++ 4 files changed, 395 insertions(+), 3 deletions(-) create mode 100644 internal/handler/admin_handler_test.go create mode 100644 internal/service/auth_service_test.go diff --git a/internal/handler/admin_handler.go b/internal/handler/admin_handler.go index 80f1cb4..31b338e 100644 --- a/internal/handler/admin_handler.go +++ b/internal/handler/admin_handler.go @@ -39,21 +39,59 @@ func (h *AdminHandler) GetUsers(c *gin.Context) { // @Success 200 {object} utils.Response // @Router /api/admin/users/{id}/lock [post] func (h *AdminHandler) LockUser(c *gin.Context) { + adminId := c.GetString("userID") userID := c.Param("id") - // TODO: Implement LockUser in AuthService + ipAddress := c.ClientIP() + userAgent := c.GetHeader("User-Agent") + if err := h.authService.LockUser(userID,adminId, ipAddress, userAgent); err != nil { + switch err { + case service.ErrUserNotFound: + c.JSON(http.StatusNotFound, + utils.ErrorResponse("User not found", err)) + case service.ErrSelfLock: + c.JSON(http.StatusBadRequest, + utils.ErrorResponse("Cannot lock your own account", err)) + case service.ErrAdminLock: + c.JSON(http.StatusForbidden, + utils.ErrorResponse("Admin accounts cannot be locked", err)) + case service.ErrAlreadyLocked: + c.JSON(http.StatusConflict, + utils.ErrorResponse("Account is already locked", err)) + default: + c.JSON(http.StatusInternalServerError, + utils.ErrorResponse("Failed to lock user", err)) + } + return + } c.JSON(http.StatusOK, utils.SuccessResponse("User locked successfully", map[string]string{"userID": userID})) } // UnlockUser unlocks a user account -// @Summary Unlock user +// @Summary Unlock user // @Tags admin // @Security BearerAuth // @Param id path string true "User ID" // @Success 200 {object} utils.Response // @Router /api/admin/users/{id}/unlock [post] func (h *AdminHandler) UnlockUser(c *gin.Context) { + adminId := c.GetString("userID") userID := c.Param("id") - // TODO: Implement UnlockUser in AuthService + ipAddress := c.ClientIP() + userAgent := c.GetHeader("User-Agent") + if err := h.authService.UnlockUser(userID, adminId, ipAddress, userAgent); err != nil { + switch err { + case service.ErrUserNotFound: + c.JSON(http.StatusNotFound, + utils.ErrorResponse("User not found", err)) + case service.ErrNotLocked: + c.JSON(http.StatusBadRequest, + utils.ErrorResponse("Account is not locked", err)) + default: + c.JSON(http.StatusInternalServerError, + utils.ErrorResponse("Failed to unlock user", err)) + } + return + } c.JSON(http.StatusOK, utils.SuccessResponse("User unlocked successfully", map[string]string{"userID": userID})) } diff --git a/internal/handler/admin_handler_test.go b/internal/handler/admin_handler_test.go new file mode 100644 index 0000000..73c972b --- /dev/null +++ b/internal/handler/admin_handler_test.go @@ -0,0 +1,170 @@ +package handler + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/gin-gonic/gin" + + "github.com/roshankumar0036singh/auth-server/internal/config" + "github.com/roshankumar0036singh/auth-server/internal/dto" + "github.com/roshankumar0036singh/auth-server/internal/middleware" + "github.com/roshankumar0036singh/auth-server/internal/repository" + "github.com/roshankumar0036singh/auth-server/internal/service" + "github.com/roshankumar0036singh/auth-server/internal/testutils" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func setupAdminLockRouter(t *testing.T) ( + *gin.Engine, + *service.AuthService, + *service.TokenService, + *repository.UserRepository, +) { + t.Helper() + + authSvc, db, mr := testutils.SetupIntegrationTest(t) + + t.Cleanup(func() { + mr.Close() + }) + + userRepo := repository.NewUserRepository(db) + + cfg := &config.Config{ + JWT: config.JWTConfig{ + AccessSecret: "test-access-secret", + RefreshSecret: "test-refresh-secret", + }, + } + + tokenSvc := service.NewTokenService(cfg) + + adminHandler := NewAdminHandler(authSvc) + + gin.SetMode(gin.TestMode) + + r := gin.New() + + admin := r.Group("/api/admin") + admin.Use(middleware.AuthMiddleware(tokenSvc)) + admin.Use(middleware.RequireRole("admin")) + + admin.POST("/users/:id/lock", adminHandler.LockUser) + admin.POST("/users/:id/unlock", adminHandler.UnlockUser) + + return r, authSvc, tokenSvc, userRepo +} + +func TestAdminHandler_LockUser_Errors(t *testing.T) { + r, authSvc, tokenSvc, userRepo := setupAdminLockRouter(t) + + admin, err := authSvc.Register(&dto.RegisterRequest{ + Email: "lock-admin@test.com", + Password: "Password123!", + }) + require.NoError(t, err) + + require.NoError(t, + userRepo.Update(admin.ID, map[string]interface{}{"role": "admin"})) + admin.Role = "admin" + + user, err := authSvc.Register(&dto.RegisterRequest{ + Email: "lock-user@test.com", + Password: "Password123!", + }) + require.NoError(t, err) + + otherAdmin, err := authSvc.Register(&dto.RegisterRequest{ + Email: "lock-other-admin@test.com", + Password: "Password123!", + }) + require.NoError(t, err) + + require.NoError(t, + userRepo.Update(otherAdmin.ID, map[string]interface{}{"role": "admin"})) + + require.NoError(t, + authSvc.LockUser(user.ID, admin.ID, "", "")) + + token, err := tokenSvc.GenerateAccessToken(admin) + require.NoError(t, err) + + tests := []struct { + name string + userID string + status int + }{ + {"user not found", "00000000-0000-0000-0000-000000000000", http.StatusNotFound}, + {"self lock", admin.ID, http.StatusBadRequest}, + {"admin account", otherAdmin.ID, http.StatusForbidden}, + {"already locked", user.ID, http.StatusConflict}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req, _ := http.NewRequest( + http.MethodPost, + "/api/admin/users/"+tt.userID+"/lock", + nil, + ) + req.Header.Set("Authorization", "Bearer "+token) + + w := httptest.NewRecorder() + r.ServeHTTP(w, req) + + assert.Equal(t, tt.status, w.Code) + }) + } +} + +func TestAdminHandler_UnlockUser_Errors(t *testing.T) { + r, authSvc, tokenSvc, userRepo := setupAdminLockRouter(t) + + admin, err := authSvc.Register(&dto.RegisterRequest{ + Email: "unlock-admin@test.com", + Password: "Password123!", + }) + require.NoError(t, err) + + require.NoError(t, + userRepo.Update(admin.ID, map[string]interface{}{"role": "admin"})) + admin.Role = "admin" + + user, err := authSvc.Register(&dto.RegisterRequest{ + Email: "unlock-user@test.com", + Password: "Password123!", + }) + require.NoError(t, err) + + token, err := tokenSvc.GenerateAccessToken(admin) + require.NoError(t, err) + + tests := []struct { + name string + userID string + status int + }{ + {"user not found", "00000000-0000-0000-0000-000000000000", http.StatusNotFound}, + {"not locked", user.ID, http.StatusBadRequest}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req, _ := http.NewRequest( + http.MethodPost, + "/api/admin/users/"+tt.userID+"/unlock", + nil, + ) + req.Header.Set("Authorization", "Bearer "+token) + + w := httptest.NewRecorder() + r.ServeHTTP(w, req) + + assert.Equal(t, tt.status, w.Code) + }) + } +} \ No newline at end of file diff --git a/internal/service/auth_service.go b/internal/service/auth_service.go index 195e5c3..c8cdb66 100644 --- a/internal/service/auth_service.go +++ b/internal/service/auth_service.go @@ -15,6 +15,14 @@ import ( "github.com/roshankumar0036singh/auth-server/internal/utils" ) +var ( + ErrUserNotFound = errors.New("user not found") + ErrSelfLock = errors.New("admin cannot lock their own account") + ErrAdminLock = errors.New("admin accounts cannot be locked") + ErrAlreadyLocked = errors.New("account is already locked") + ErrNotLocked = errors.New("account is not locked") +) + type AuthService struct { userRepo *repository.UserRepository tokenRepo *repository.TokenRepository @@ -774,3 +782,54 @@ func (s *AuthService) RevokeSession(userID, tokenID string) error { return nil } + +func(s *AuthService) LockUser(userId , adminId, ipAddress, userAgent string) error { + if userId == adminId { + return ErrSelfLock + } + + user, err := s.userRepo.FindByID(userId) + if err != nil { + return ErrUserNotFound + } + + if user.Role == "admin" { + return ErrAdminLock + } + + if user.LockedUntil != nil && time.Now().Before(*user.LockedUntil) { + return ErrAlreadyLocked + } + + // effectively until manually unlocked + lockedUntil := time.Now().AddDate(100, 0, 0) + + if err := s.userRepo.Update(userId,map[string]interface{}{"locked_until": lockedUntil}); err != nil{ + return errors.New("failed to lock user") + } + + if err := s.tokenRepo.RevokeAllUserTokens(userId); err != nil { + return err + } + s.auditService.LogEvent(&userId, "USER_LOCKED", "USER", userId, ipAddress, userAgent, map[string]interface{}{"locked_until": lockedUntil}) + return nil +} + +func (s *AuthService) UnlockUser(userId, adminId, ipAddress, userAgent string) error { + user,err := s.userRepo.FindByID(userId) + if err != nil { + return ErrUserNotFound + } + + if user.LockedUntil == nil || !time.Now().Before(*user.LockedUntil){ + return ErrNotLocked + } + + if err := s.userRepo.Update(userId,map[string]interface{}{"locked_until": nil, "failed_login_attempts": 0}); err != nil{ + return errors.New("failed to unlock user") + } + + s.auditService.LogEvent(&userId, "USER_UNLOCKED", "USER", userId, ipAddress, userAgent, map[string]interface{}{"locked_until": nil}) + + return nil +} \ No newline at end of file diff --git a/internal/service/auth_service_test.go b/internal/service/auth_service_test.go new file mode 100644 index 0000000..5630bee --- /dev/null +++ b/internal/service/auth_service_test.go @@ -0,0 +1,125 @@ +package service + +import ( + "testing" + + "github.com/glebarez/sqlite" + "github.com/roshankumar0036singh/auth-server/internal/config" + "github.com/roshankumar0036singh/auth-server/internal/dto" + "github.com/roshankumar0036singh/auth-server/internal/models" + "github.com/roshankumar0036singh/auth-server/internal/repository" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gorm.io/gorm" +) + +type mockEmailSender struct{} + +func (m *mockEmailSender) SendVerificationEmail(email, token, appURL string) error { + return nil +} + +func (m *mockEmailSender) SendPasswordResetEmail(email, token, appURL string) error { + return nil +} + +func setupAuthService(t *testing.T) (*AuthService, *gorm.DB) { + db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{}) + require.NoError(t, err) + + require.NoError(t, db.AutoMigrate( + &models.User{}, + &models.RefreshToken{}, + &models.VerificationToken{}, + &models.PasswordResetToken{}, + &models.AuditLog{}, + )) + + userRepo := repository.NewUserRepository(db) + + cfg := &config.Config{ + JWT: config.JWTConfig{ + AccessSecret: "secret", + RefreshSecret: "refresh", + }, + } + + authService := NewAuthService( + userRepo, + repository.NewTokenRepository(db), + repository.NewVerificationRepository(db), + repository.NewPasswordResetRepository(db), + NewTokenService(cfg), + nil, + &mockEmailSender{}, + NewAuditService(repository.NewAuditRepository(db)), + nil, + cfg, + ) + + return authService, db +} + +func registerUser(t *testing.T, authSvc *AuthService, email string) *models.User { + t.Helper() + + user, err := authSvc.Register(&dto.RegisterRequest{ + Email: email, + Password: "Password123!", + FirstName: "Test", + LastName: "User", + }) + require.NoError(t, err) + + return user +} + +func promoteAdmin(t *testing.T, db *gorm.DB, userID string) { + t.Helper() + + require.NoError(t, + db.Model(&models.User{}). + Where("id = ?", userID). + Update("role", "admin").Error, + ) +} + +func TestAuthService_LockUser(t *testing.T) { + authSvc, db := setupAuthService(t) + + admin := registerUser(t, authSvc, "admin@example.com") + user := registerUser(t, authSvc, "user@example.com") + + promoteAdmin(t, db, admin.ID) + + require.NoError(t, + authSvc.LockUser(user.ID, admin.ID, "", ""), + ) + + updatedUser, err := repository.NewUserRepository(db).FindByID(user.ID) + require.NoError(t, err) + + assert.NotNil(t, updatedUser.LockedUntil) +} + +func TestAuthService_UnlockUser(t *testing.T) { + authSvc, db := setupAuthService(t) + + admin := registerUser(t, authSvc, "admin1@example.com") + user := registerUser(t, authSvc, "user1@example.com") + + promoteAdmin(t, db, admin.ID) + + require.NoError(t, + authSvc.LockUser(user.ID, admin.ID, "", ""), + ) + + require.NoError(t, + authSvc.UnlockUser(user.ID, admin.ID, "", ""), + ) + + updatedUser, err := repository.NewUserRepository(db).FindByID(user.ID) + require.NoError(t, err) + + assert.Nil(t, updatedUser.LockedUntil) +} \ No newline at end of file From 01fe147d56de1a3d62128a0f6bc4c0e716650c5f Mon Sep 17 00:00:00 2001 From: Samikshaaggarwal <123529409+Samikshaaggarwal@users.noreply.github.com> Date: Thu, 4 Jun 2026 19:04:57 +0530 Subject: [PATCH 2/4] refactor/issue-25 | Addressed review comments --- internal/handler/admin_handler.go | 45 +++--- internal/handler/admin_handler_test.go | 28 +++- internal/models/user.go | 35 +++-- internal/repository/user_repository.go | 16 +++ internal/service/auth_service.go | 81 ++++++----- .../service/auth_service_integration_test.go | 129 ++++++++++++++++++ internal/service/auth_service_test.go | 125 ----------------- 7 files changed, 253 insertions(+), 206 deletions(-) delete mode 100644 internal/service/auth_service_test.go diff --git a/internal/handler/admin_handler.go b/internal/handler/admin_handler.go index 31b338e..b85cfce 100644 --- a/internal/handler/admin_handler.go +++ b/internal/handler/admin_handler.go @@ -1,6 +1,7 @@ package handler import ( + "errors" "net/http" "github.com/gin-gonic/gin" @@ -39,22 +40,22 @@ func (h *AdminHandler) GetUsers(c *gin.Context) { // @Success 200 {object} utils.Response // @Router /api/admin/users/{id}/lock [post] func (h *AdminHandler) LockUser(c *gin.Context) { - adminId := c.GetString("userID") + adminID := c.GetString("userID") userID := c.Param("id") ipAddress := c.ClientIP() userAgent := c.GetHeader("User-Agent") - if err := h.authService.LockUser(userID,adminId, ipAddress, userAgent); err != nil { - switch err { - case service.ErrUserNotFound: + if err := h.authService.LockUser(userID, adminID, ipAddress, userAgent); err != nil { + switch { + case errors.Is(err, service.ErrUserNotFound): c.JSON(http.StatusNotFound, utils.ErrorResponse("User not found", err)) - case service.ErrSelfLock: + case errors.Is(err, service.ErrSelfLock): c.JSON(http.StatusBadRequest, utils.ErrorResponse("Cannot lock your own account", err)) - case service.ErrAdminLock: + case errors.Is(err, service.ErrAdminLock): c.JSON(http.StatusForbidden, utils.ErrorResponse("Admin accounts cannot be locked", err)) - case service.ErrAlreadyLocked: + case errors.Is(err, service.ErrAlreadyLocked): c.JSON(http.StatusConflict, utils.ErrorResponse("Account is already locked", err)) default: @@ -67,30 +68,30 @@ func (h *AdminHandler) LockUser(c *gin.Context) { } // UnlockUser unlocks a user account -// @Summary Unlock user +// @Summary Unlock user // @Tags admin // @Security BearerAuth // @Param id path string true "User ID" // @Success 200 {object} utils.Response // @Router /api/admin/users/{id}/unlock [post] func (h *AdminHandler) UnlockUser(c *gin.Context) { - adminId := c.GetString("userID") + adminID := c.GetString("userID") userID := c.Param("id") ipAddress := c.ClientIP() userAgent := c.GetHeader("User-Agent") - if err := h.authService.UnlockUser(userID, adminId, ipAddress, userAgent); err != nil { - switch err { - case service.ErrUserNotFound: - c.JSON(http.StatusNotFound, - utils.ErrorResponse("User not found", err)) - case service.ErrNotLocked: - c.JSON(http.StatusBadRequest, - utils.ErrorResponse("Account is not locked", err)) - default: - c.JSON(http.StatusInternalServerError, - utils.ErrorResponse("Failed to unlock user", err)) - } - return + if err := h.authService.UnlockUser(userID, adminID, ipAddress, userAgent); err != nil { + switch { + case errors.Is(err, service.ErrUserNotFound): + c.JSON(http.StatusNotFound, + utils.ErrorResponse("User not found", err)) + case errors.Is(err, service.ErrNotLocked): + c.JSON(http.StatusBadRequest, + utils.ErrorResponse("Account is not locked", err)) + default: + c.JSON(http.StatusInternalServerError, + utils.ErrorResponse("Failed to unlock user", err)) + } + return } c.JSON(http.StatusOK, utils.SuccessResponse("User unlocked successfully", map[string]string{"userID": userID})) } diff --git a/internal/handler/admin_handler_test.go b/internal/handler/admin_handler_test.go index 73c972b..9273493 100644 --- a/internal/handler/admin_handler_test.go +++ b/internal/handler/admin_handler_test.go @@ -1,4 +1,4 @@ -package handler +package handler_test import ( "net/http" @@ -6,6 +6,7 @@ import ( "testing" "github.com/gin-gonic/gin" + "github.com/roshankumar0036singh/auth-server/internal/handler" "github.com/roshankumar0036singh/auth-server/internal/config" "github.com/roshankumar0036singh/auth-server/internal/dto" @@ -43,7 +44,7 @@ func setupAdminLockRouter(t *testing.T) ( tokenSvc := service.NewTokenService(cfg) - adminHandler := NewAdminHandler(authSvc) + adminHandler := handler.NewAdminHandler(authSvc) gin.SetMode(gin.TestMode) @@ -87,6 +88,7 @@ func TestAdminHandler_LockUser_Errors(t *testing.T) { require.NoError(t, userRepo.Update(otherAdmin.ID, map[string]interface{}{"role": "admin"})) + // ensure already locked case is valid require.NoError(t, authSvc.LockUser(user.ID, admin.ID, "", "")) @@ -106,7 +108,7 @@ func TestAdminHandler_LockUser_Errors(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - req, _ := http.NewRequest( + req := httptest.NewRequest( http.MethodPost, "/api/admin/users/"+tt.userID+"/lock", nil, @@ -134,12 +136,24 @@ func TestAdminHandler_UnlockUser_Errors(t *testing.T) { userRepo.Update(admin.ID, map[string]interface{}{"role": "admin"})) admin.Role = "admin" - user, err := authSvc.Register(&dto.RegisterRequest{ + // user 1: will be NOT locked + unlockedUser, err := authSvc.Register(&dto.RegisterRequest{ Email: "unlock-user@test.com", Password: "Password123!", }) require.NoError(t, err) + // user 2: will be locked + lockedUser, err := authSvc.Register(&dto.RegisterRequest{ + Email: "unlock-user-locked@test.com", + Password: "Password123!", + }) + require.NoError(t, err) + + require.NoError(t, + authSvc.LockUser(lockedUser.ID, admin.ID, "", ""), + ) + token, err := tokenSvc.GenerateAccessToken(admin) require.NoError(t, err) @@ -149,12 +163,12 @@ func TestAdminHandler_UnlockUser_Errors(t *testing.T) { status int }{ {"user not found", "00000000-0000-0000-0000-000000000000", http.StatusNotFound}, - {"not locked", user.ID, http.StatusBadRequest}, + {"not locked", unlockedUser.ID, http.StatusBadRequest}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - req, _ := http.NewRequest( + req := httptest.NewRequest( http.MethodPost, "/api/admin/users/"+tt.userID+"/unlock", nil, @@ -167,4 +181,4 @@ func TestAdminHandler_UnlockUser_Errors(t *testing.T) { assert.Equal(t, tt.status, w.Code) }) } -} \ No newline at end of file +} diff --git a/internal/models/user.go b/internal/models/user.go index adc76ef..ddb040d 100644 --- a/internal/models/user.go +++ b/internal/models/user.go @@ -8,21 +8,21 @@ import ( ) type User struct { - ID string `gorm:"type:uuid;primary_key" json:"id"` - Email string `gorm:"uniqueIndex;not null;size:255" json:"email"` - PasswordHash string `gorm:"not null;size:255" json:"-"` // Never expose in JSON - FirstName string `gorm:"size:100" json:"firstName,omitempty"` - LastName string `gorm:"size:100" json:"lastName,omitempty"` - Phone string `gorm:"size:20" json:"phone,omitempty"` - PhoneVerified bool `gorm:"default:false" json:"phoneVerified"` - EmailVerified bool `gorm:"default:false" json:"emailVerified"` - IsActive bool `gorm:"default:true" json:"isActive"` - ProfileImage string `json:"profileImage,omitempty"` - OAuthProvider string `gorm:"size:50" json:"oauthProvider,omitempty"` // 'google', 'github', 'local' - OAuthID string `gorm:"size:255" json:"-"` - MFAEnabled bool `gorm:"default:false" json:"mfaEnabled"` - MFASecret string `gorm:"size:255" json:"-"` - Role string `gorm:"default:'user';size:50" json:"role"` // 'user', 'admin' + ID string `gorm:"type:uuid;primary_key" json:"id"` + Email string `gorm:"uniqueIndex;not null;size:255" json:"email"` + PasswordHash string `gorm:"not null;size:255" json:"-"` // Never expose in JSON + FirstName string `gorm:"size:100" json:"firstName,omitempty"` + LastName string `gorm:"size:100" json:"lastName,omitempty"` + Phone string `gorm:"size:20" json:"phone,omitempty"` + PhoneVerified bool `gorm:"default:false" json:"phoneVerified"` + EmailVerified bool `gorm:"default:false" json:"emailVerified"` + IsActive bool `gorm:"default:true" json:"isActive"` + ProfileImage string `json:"profileImage,omitempty"` + OAuthProvider string `gorm:"size:50" json:"oauthProvider,omitempty"` // 'google', 'github', 'local' + OAuthID string `gorm:"size:255" json:"-"` + MFAEnabled bool `gorm:"default:false" json:"mfaEnabled"` + MFASecret string `gorm:"size:255" json:"-"` + Role string `gorm:"default:'user';size:50" json:"role"` // 'user', 'admin' FailedLoginAttempts int `gorm:"default:0" json:"-"` LockedUntil *time.Time `json:"lockedUntil,omitempty"` CreatedAt time.Time `json:"createdAt"` @@ -77,3 +77,8 @@ func (u *User) ToPublic() *PublicUser { LastLoginAt: u.LastLoginAt, } } + +func (u *User) IsLocked() bool { + return u.LockedUntil != nil && + time.Now().Before(*u.LockedUntil) +} diff --git a/internal/repository/user_repository.go b/internal/repository/user_repository.go index 53d45b9..6e609a9 100644 --- a/internal/repository/user_repository.go +++ b/internal/repository/user_repository.go @@ -2,6 +2,7 @@ package repository import ( "errors" + "time" "github.com/roshankumar0036singh/auth-server/internal/models" "gorm.io/gorm" @@ -76,3 +77,18 @@ func (r *UserRepository) EmailExists(email string) (bool, error) { err := r.db.Model(&models.User{}).Where("email = ?", email).Count(&count).Error return count > 0, err } + +func (r *UserRepository) LockUser(userID string, lockedUntil time.Time) error { + return r.db.Model(&models.User{}). + Where("id = ?", userID). + Update("locked_until", lockedUntil).Error +} + +func (r *UserRepository) UnlockUser(userID string) error { + return r.db.Model(&models.User{}). + Where("id = ?", userID). + Updates(map[string]interface{}{ + "locked_until": nil, + "failed_login_attempts": 0, + }).Error +} diff --git a/internal/service/auth_service.go b/internal/service/auth_service.go index c8cdb66..6c0ec0e 100644 --- a/internal/service/auth_service.go +++ b/internal/service/auth_service.go @@ -7,20 +7,20 @@ import ( "log" "time" - "golang.org/x/crypto/bcrypt" "github.com/roshankumar0036singh/auth-server/internal/config" "github.com/roshankumar0036singh/auth-server/internal/dto" "github.com/roshankumar0036singh/auth-server/internal/models" "github.com/roshankumar0036singh/auth-server/internal/repository" "github.com/roshankumar0036singh/auth-server/internal/utils" + "golang.org/x/crypto/bcrypt" ) var ( - ErrUserNotFound = errors.New("user not found") - ErrSelfLock = errors.New("admin cannot lock their own account") - ErrAdminLock = errors.New("admin accounts cannot be locked") - ErrAlreadyLocked = errors.New("account is already locked") - ErrNotLocked = errors.New("account is not locked") + ErrUserNotFound = errors.New("user not found") + ErrSelfLock = errors.New("admin cannot lock their own account") + ErrAdminLock = errors.New("admin accounts cannot be locked") + ErrAlreadyLocked = errors.New("account is already locked") + ErrNotLocked = errors.New("account is not locked") ) type AuthService struct { @@ -79,7 +79,7 @@ func (s *AuthService) ForgotPassword(email string) error { token := &models.PasswordResetToken{ UserID: user.ID, Token: s.tokenService.GenerateRandomString(32), - ExpiresAt: time.Now().Add(1 * time.Hour), + ExpiresAt: time.Now().Add(1 * time.Hour), } if err := s.passwordResetRepo.Create(token); err != nil { @@ -143,7 +143,7 @@ func (s *AuthService) ResetPassword(tokenString, newPassword string) error { // UpdateProfile updates user profile information func (s *AuthService) UpdateProfile(userID string, req *dto.UpdateProfileRequest) (*models.User, error) { updates := make(map[string]interface{}) - + if req.FirstName != "" { updates["first_name"] = req.FirstName } @@ -209,7 +209,7 @@ func (s *AuthService) ChangePassword(userID string, req *dto.ChangePasswordReque // Revoke all other sessions? Maybe optional, but good practice for security. // For now, let's keep current session active. - + // Audit Log s.auditService.LogEvent(&userID, "PASSWORD_CHANGED", "USER", userID, "", "", nil) @@ -322,7 +322,7 @@ func (s *AuthService) VerifyLoginMFA(email, code, ipAddress, userAgent string) ( refreshToken := &models.RefreshToken{ UserID: user.ID, Token: refreshTokenString, - ExpiresAt: time.Now().Add(7 * 24 * time.Hour), + ExpiresAt: time.Now().Add(7 * 24 * time.Hour), IPAddress: ipAddress, UserAgent: userAgent, } @@ -369,7 +369,7 @@ func (s *AuthService) Register(req *dto.RegisterRequest) (*models.User, error) { FirstName: req.FirstName, LastName: req.LastName, OAuthProvider: "local", - IsActive: true, // Can allow login but restrict features, or set false + IsActive: true, // Can allow login but restrict features, or set false EmailVerified: false, } @@ -449,7 +449,6 @@ func (s *AuthService) ResendVerification(email string) error { return s.sendVerificationEmail(user) } - // Login authenticates a user and returns tokens with device tracking func (s *AuthService) Login(req *dto.LoginRequest, ipAddress, userAgent string) (*dto.LoginResponse, error) { ctx := context.Background() @@ -545,7 +544,7 @@ func (s *AuthService) LoginWithOAuth(email, oauthID, firstName, lastName, provid // User does not exist, create new one password := s.tokenService.GenerateRandomString(32) hashedPassword, _ := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) - + user = &models.User{ Email: email, PasswordHash: string(hashedPassword), @@ -556,11 +555,11 @@ func (s *AuthService) LoginWithOAuth(email, oauthID, firstName, lastName, provid IsActive: true, EmailVerified: true, // Trusted from OAuth } - + if err := s.userRepo.Create(user); err != nil { return nil, errors.New("failed to create user") } - + s.auditService.LogEvent(&user.ID, "USER_REGISTERED_OAUTH", "USER", user.ID, "", "", map[string]interface{}{"provider": provider}) } else { // User exists, link account if not generic local @@ -592,7 +591,7 @@ func (s *AuthService) LoginWithOAuth(email, oauthID, firstName, lastName, provid refreshToken := &models.RefreshToken{ UserID: user.ID, Token: refreshTokenString, - ExpiresAt: time.Now().Add(7 * 24 * time.Hour), + ExpiresAt: time.Now().Add(7 * 24 * time.Hour), IPAddress: ipAddress, UserAgent: userAgent, } @@ -629,7 +628,7 @@ func (s *AuthService) handleFailedLogin(user *models.User, email string, ctx con } s.userRepo.Update(user.ID, updates) - + // Audit Log Failed Login s.auditService.LogEvent(&user.ID, "USER_LOGIN_FAILED", "USER", user.ID, "", "", map[string]interface{}{"email": email}) } @@ -783,53 +782,61 @@ func (s *AuthService) RevokeSession(userID, tokenID string) error { return nil } -func(s *AuthService) LockUser(userId , adminId, ipAddress, userAgent string) error { - if userId == adminId { +func (s *AuthService) LockUser(userID, adminID, ipAddress, userAgent string) error { + if userID == adminID { return ErrSelfLock } - user, err := s.userRepo.FindByID(userId) + user, err := s.userRepo.FindByID(userID) if err != nil { - return ErrUserNotFound + if errors.Is(err, repository.ErrUserNotFound) { + return ErrUserNotFound + } + return err } if user.Role == "admin" { return ErrAdminLock } - if user.LockedUntil != nil && time.Now().Before(*user.LockedUntil) { + if user.IsLocked() { return ErrAlreadyLocked } - // effectively until manually unlocked + // effective until manually unlocked lockedUntil := time.Now().AddDate(100, 0, 0) - if err := s.userRepo.Update(userId,map[string]interface{}{"locked_until": lockedUntil}); err != nil{ - return errors.New("failed to lock user") + if err := s.userRepo.LockUser(userID, lockedUntil); err != nil { + return fmt.Errorf("lock user: %w", err) } - if err := s.tokenRepo.RevokeAllUserTokens(userId); err != nil { - return err + if err := s.tokenRepo.RevokeAllUserTokens(userID); err != nil { + return fmt.Errorf("revoke user tokens: %w", err) } - s.auditService.LogEvent(&userId, "USER_LOCKED", "USER", userId, ipAddress, userAgent, map[string]interface{}{"locked_until": lockedUntil}) + + s.auditService.LogEvent(&adminID, "USER_LOCKED", "USER", userID, ipAddress, userAgent, map[string]interface{}{"locked_until": lockedUntil}) + return nil } -func (s *AuthService) UnlockUser(userId, adminId, ipAddress, userAgent string) error { - user,err := s.userRepo.FindByID(userId) +func (s *AuthService) UnlockUser(userID, adminID, ipAddress, userAgent string) error { + user, err := s.userRepo.FindByID(userID) if err != nil { - return ErrUserNotFound + if errors.Is(err, repository.ErrUserNotFound) { + return ErrUserNotFound + } + return err } - if user.LockedUntil == nil || !time.Now().Before(*user.LockedUntil){ - return ErrNotLocked + if !user.IsLocked() { + return ErrNotLocked } - if err := s.userRepo.Update(userId,map[string]interface{}{"locked_until": nil, "failed_login_attempts": 0}); err != nil{ - return errors.New("failed to unlock user") + if err := s.userRepo.UnlockUser(userID); err != nil { + return fmt.Errorf("unlock user: %w", err) } - s.auditService.LogEvent(&userId, "USER_UNLOCKED", "USER", userId, ipAddress, userAgent, map[string]interface{}{"locked_until": nil}) + s.auditService.LogEvent(&adminID, "USER_UNLOCKED", "USER", userID, ipAddress, userAgent, map[string]interface{}{"locked_until": nil}) return nil -} \ No newline at end of file +} diff --git a/internal/service/auth_service_integration_test.go b/internal/service/auth_service_integration_test.go index c265a50..5407d25 100644 --- a/internal/service/auth_service_integration_test.go +++ b/internal/service/auth_service_integration_test.go @@ -4,8 +4,13 @@ import ( "testing" "github.com/roshankumar0036singh/auth-server/internal/dto" + "github.com/roshankumar0036singh/auth-server/internal/models" + "github.com/roshankumar0036singh/auth-server/internal/repository" + "github.com/roshankumar0036singh/auth-server/internal/service" "github.com/roshankumar0036singh/auth-server/internal/testutils" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gorm.io/gorm" ) func TestAuthService_Register_Integration(t *testing.T) { @@ -58,3 +63,127 @@ func TestAuthService_Login_Integration(t *testing.T) { _, err = service.Login(loginReqFail, "127.0.0.1", "UserAgent") assert.Error(t, err) } + +func registerUser(t *testing.T, authSvc *service.AuthService, email string) *models.User { + t.Helper() + + user, err := authSvc.Register(&dto.RegisterRequest{ + Email: email, + Password: "Password123!", + FirstName: "Test", + LastName: "User", + }) + require.NoError(t, err) + + return user +} + +func promoteAdmin(t *testing.T, db *gorm.DB, userID string) { + t.Helper() + + require.NoError(t, + db.Model(&models.User{}). + Where("id = ?", userID). + Update("role", "admin").Error, + ) +} + +func TestAuthService_LockUser(t *testing.T) { + authSvc, db, _ := testutils.SetupIntegrationTest(t) + + admin := registerUser(t, authSvc, "admin@example.com") + user := registerUser(t, authSvc, "user@example.com") + + promoteAdmin(t, db, admin.ID) + + require.NoError(t, + authSvc.LockUser(user.ID, admin.ID, "127.0.0.1", "test-agent"), + ) + + updatedUser, err := repository.NewUserRepository(db).FindByID(user.ID) + require.NoError(t, err) + + assert.NotNil(t, updatedUser.LockedUntil) + assert.True(t, updatedUser.IsLocked()) + + var tokenCount int64 + db.Model(&models.RefreshToken{}). + Where("user_id = ?", user.ID). + Count(&tokenCount) + + assert.Equal(t, int64(0), tokenCount) +} + +func TestAuthService_LockUser_TokenRevocation(t *testing.T) { + authSvc, db, _ := testutils.SetupIntegrationTest(t) + + admin := registerUser(t, authSvc, "admin2@example.com") + user := registerUser(t, authSvc, "user2@example.com") + + promoteAdmin(t, db, admin.ID) + + require.NoError(t, + authSvc.LockUser(user.ID, admin.ID, "", ""), + ) + + updatedUser, err := repository.NewUserRepository(db).FindByID(user.ID) + require.NoError(t, err) + + assert.NotNil(t, updatedUser.LockedUntil) +} + +func TestAuthService_LockUser_SelfLock(t *testing.T) { + authSvc, db, _ := testutils.SetupIntegrationTest(t) + + user := registerUser(t, authSvc, "self@example.com") + promoteAdmin(t, db, user.ID) + + err := authSvc.LockUser(user.ID, user.ID, "", "") + assert.ErrorIs(t, err, service.ErrSelfLock) +} + +func TestAuthService_LockUser_AdminLock(t *testing.T) { + authSvc, db, _ := testutils.SetupIntegrationTest(t) + + admin := registerUser(t, authSvc, "admin5@example.com") + user := registerUser(t, authSvc, "user5@example.com") + + promoteAdmin(t, db, admin.ID) + + err := authSvc.LockUser(admin.ID, user.ID, "", "") + assert.ErrorIs(t, err, service.ErrAdminLock) +} + +func TestAuthService_UnlockUser(t *testing.T) { + authSvc, db, _ := testutils.SetupIntegrationTest(t) + + admin := registerUser(t, authSvc, "admin3@example.com") + user := registerUser(t, authSvc, "user3@example.com") + + promoteAdmin(t, db, admin.ID) + + require.NoError(t, + authSvc.LockUser(user.ID, admin.ID, "", ""), + ) + + require.NoError(t, + authSvc.UnlockUser(user.ID, admin.ID, "", ""), + ) + + updatedUser, err := repository.NewUserRepository(db).FindByID(user.ID) + require.NoError(t, err) + + assert.Nil(t, updatedUser.LockedUntil) +} + +func TestAuthService_UnlockUser_WhenNotLocked(t *testing.T) { + authSvc, db, _ := testutils.SetupIntegrationTest(t) + + admin := registerUser(t, authSvc, "admin4@example.com") + user := registerUser(t, authSvc, "user4@example.com") + + promoteAdmin(t, db, admin.ID) + + err := authSvc.UnlockUser(user.ID, admin.ID, "", "") + assert.ErrorIs(t, err, service.ErrNotLocked) +} diff --git a/internal/service/auth_service_test.go b/internal/service/auth_service_test.go deleted file mode 100644 index 5630bee..0000000 --- a/internal/service/auth_service_test.go +++ /dev/null @@ -1,125 +0,0 @@ -package service - -import ( - "testing" - - "github.com/glebarez/sqlite" - "github.com/roshankumar0036singh/auth-server/internal/config" - "github.com/roshankumar0036singh/auth-server/internal/dto" - "github.com/roshankumar0036singh/auth-server/internal/models" - "github.com/roshankumar0036singh/auth-server/internal/repository" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "gorm.io/gorm" -) - -type mockEmailSender struct{} - -func (m *mockEmailSender) SendVerificationEmail(email, token, appURL string) error { - return nil -} - -func (m *mockEmailSender) SendPasswordResetEmail(email, token, appURL string) error { - return nil -} - -func setupAuthService(t *testing.T) (*AuthService, *gorm.DB) { - db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{}) - require.NoError(t, err) - - require.NoError(t, db.AutoMigrate( - &models.User{}, - &models.RefreshToken{}, - &models.VerificationToken{}, - &models.PasswordResetToken{}, - &models.AuditLog{}, - )) - - userRepo := repository.NewUserRepository(db) - - cfg := &config.Config{ - JWT: config.JWTConfig{ - AccessSecret: "secret", - RefreshSecret: "refresh", - }, - } - - authService := NewAuthService( - userRepo, - repository.NewTokenRepository(db), - repository.NewVerificationRepository(db), - repository.NewPasswordResetRepository(db), - NewTokenService(cfg), - nil, - &mockEmailSender{}, - NewAuditService(repository.NewAuditRepository(db)), - nil, - cfg, - ) - - return authService, db -} - -func registerUser(t *testing.T, authSvc *AuthService, email string) *models.User { - t.Helper() - - user, err := authSvc.Register(&dto.RegisterRequest{ - Email: email, - Password: "Password123!", - FirstName: "Test", - LastName: "User", - }) - require.NoError(t, err) - - return user -} - -func promoteAdmin(t *testing.T, db *gorm.DB, userID string) { - t.Helper() - - require.NoError(t, - db.Model(&models.User{}). - Where("id = ?", userID). - Update("role", "admin").Error, - ) -} - -func TestAuthService_LockUser(t *testing.T) { - authSvc, db := setupAuthService(t) - - admin := registerUser(t, authSvc, "admin@example.com") - user := registerUser(t, authSvc, "user@example.com") - - promoteAdmin(t, db, admin.ID) - - require.NoError(t, - authSvc.LockUser(user.ID, admin.ID, "", ""), - ) - - updatedUser, err := repository.NewUserRepository(db).FindByID(user.ID) - require.NoError(t, err) - - assert.NotNil(t, updatedUser.LockedUntil) -} - -func TestAuthService_UnlockUser(t *testing.T) { - authSvc, db := setupAuthService(t) - - admin := registerUser(t, authSvc, "admin1@example.com") - user := registerUser(t, authSvc, "user1@example.com") - - promoteAdmin(t, db, admin.ID) - - require.NoError(t, - authSvc.LockUser(user.ID, admin.ID, "", ""), - ) - - require.NoError(t, - authSvc.UnlockUser(user.ID, admin.ID, "", ""), - ) - - updatedUser, err := repository.NewUserRepository(db).FindByID(user.ID) - require.NoError(t, err) - - assert.Nil(t, updatedUser.LockedUntil) -} \ No newline at end of file From 1ebd7d48182b156580a33ff5b5fdb8457689ef65 Mon Sep 17 00:00:00 2001 From: Samikshaaggarwal <123529409+Samikshaaggarwal@users.noreply.github.com> Date: Thu, 4 Jun 2026 19:47:02 +0530 Subject: [PATCH 3/4] resolved merge conflicts --- internal/repository/user_repository.go | 28 ++++++- internal/service/auth_service.go | 81 ++++++++++--------- .../service/auth_service_integration_test.go | 44 ++++++++-- internal/testutils/setup.go | 1 + 4 files changed, 109 insertions(+), 45 deletions(-) diff --git a/internal/repository/user_repository.go b/internal/repository/user_repository.go index 6e609a9..7f8416f 100644 --- a/internal/repository/user_repository.go +++ b/internal/repository/user_repository.go @@ -79,16 +79,36 @@ func (r *UserRepository) EmailExists(email string) (bool, error) { } func (r *UserRepository) LockUser(userID string, lockedUntil time.Time) error { - return r.db.Model(&models.User{}). + result := r.db.Model(&models.User{}). Where("id = ?", userID). - Update("locked_until", lockedUntil).Error + Update("locked_until", lockedUntil) + + if result.Error != nil { + return result.Error + } + + if result.RowsAffected == 0 { + return ErrUserNotFound + } + + return nil } func (r *UserRepository) UnlockUser(userID string) error { - return r.db.Model(&models.User{}). + result := r.db.Model(&models.User{}). Where("id = ?", userID). Updates(map[string]interface{}{ "locked_until": nil, "failed_login_attempts": 0, - }).Error + }) + + if result.Error != nil { + return result.Error + } + + if result.RowsAffected == 0 { + return ErrUserNotFound + } + + return nil } diff --git a/internal/service/auth_service.go b/internal/service/auth_service.go index f5826b0..2ae8996 100644 --- a/internal/service/auth_service.go +++ b/internal/service/auth_service.go @@ -13,6 +13,7 @@ import ( "github.com/roshankumar0036singh/auth-server/internal/repository" "github.com/roshankumar0036singh/auth-server/internal/utils" "golang.org/x/crypto/bcrypt" + "gorm.io/gorm" ) var ( @@ -42,6 +43,7 @@ type AuthService struct { auditService *AuditService mfaService *MFAService config *config.Config + db *gorm.DB } func NewAuthService( @@ -55,6 +57,7 @@ func NewAuthService( auditService *AuditService, mfaService *MFAService, cfg *config.Config, + db *gorm.DB, ) *AuthService { return &AuthService{ userRepo: userRepo, @@ -67,6 +70,7 @@ func NewAuthService( auditService: auditService, mfaService: mfaService, config: cfg, + db: db, } } @@ -795,56 +799,61 @@ func (s *AuthService) LockUser(userID, adminID, ipAddress, userAgent string) err return ErrSelfLock } - user, err := s.userRepo.FindByID(userID) - if err != nil { - if errors.Is(err, repository.ErrUserNotFound) { - return ErrUserNotFound + return s.db.Transaction(func(tx *gorm.DB) error { + + user, err := s.userRepo.FindByID(userID) + if err != nil { + if errors.Is(err, repository.ErrUserNotFound) { + return ErrUserNotFound + } + return err } - return err - } - if user.Role == "admin" { - return ErrAdminLock - } + if user.Role == "admin" { + return ErrAdminLock + } - if user.IsLocked() { - return ErrAlreadyLocked - } + if user.IsLocked() { + return ErrAlreadyLocked + } - // effective until manually unlocked - lockedUntil := time.Now().AddDate(100, 0, 0) + // effective until manually unlocked + lockedUntil := time.Now().AddDate(100, 0, 0) - if err := s.userRepo.LockUser(userID, lockedUntil); err != nil { - return fmt.Errorf("lock user: %w", err) - } + if err := s.userRepo.LockUser(userID, lockedUntil); err != nil { + return fmt.Errorf("lock user: %w", err) + } - if err := s.tokenRepo.RevokeAllUserTokens(userID); err != nil { - return fmt.Errorf("revoke user tokens: %w", err) - } + if err := s.tokenRepo.RevokeAllUserTokens(userID); err != nil { + return fmt.Errorf("revoke user tokens: %w", err) + } - s.auditService.LogEvent(&adminID, "USER_LOCKED", "USER", userID, ipAddress, userAgent, map[string]interface{}{"locked_until": lockedUntil}) + s.auditService.LogEvent(&adminID, "USER_LOCKED", "USER", userID, ipAddress, userAgent, map[string]interface{}{"locked_until": lockedUntil}) - return nil + return nil + }) } func (s *AuthService) UnlockUser(userID, adminID, ipAddress, userAgent string) error { - user, err := s.userRepo.FindByID(userID) - if err != nil { - if errors.Is(err, repository.ErrUserNotFound) { - return ErrUserNotFound + return s.db.Transaction(func(tx *gorm.DB) error { + user, err := s.userRepo.FindByID(userID) + if err != nil { + if errors.Is(err, repository.ErrUserNotFound) { + return ErrUserNotFound + } + return err } - return err - } - if !user.IsLocked() { - return ErrNotLocked - } + if !user.IsLocked() { + return ErrNotLocked + } - if err := s.userRepo.UnlockUser(userID); err != nil { - return fmt.Errorf("unlock user: %w", err) - } + if err := s.userRepo.UnlockUser(userID); err != nil { + return fmt.Errorf("unlock user: %w", err) + } - s.auditService.LogEvent(&adminID, "USER_UNLOCKED", "USER", userID, ipAddress, userAgent, map[string]interface{}{"locked_until": nil}) + s.auditService.LogEvent(&adminID, "USER_UNLOCKED", "USER", userID, ipAddress, userAgent, map[string]interface{}{"locked_until": nil}) - return nil + return nil + }) } diff --git a/internal/service/auth_service_integration_test.go b/internal/service/auth_service_integration_test.go index 5407d25..2752d54 100644 --- a/internal/service/auth_service_integration_test.go +++ b/internal/service/auth_service_integration_test.go @@ -96,6 +96,12 @@ func TestAuthService_LockUser(t *testing.T) { promoteAdmin(t, db, admin.ID) + _, err := authSvc.Login(&dto.LoginRequest{ + Email: "user@example.com", + Password: "Password123!", + }, "127.0.0.1", "test-agent") + require.NoError(t, err) + require.NoError(t, authSvc.LockUser(user.ID, admin.ID, "127.0.0.1", "test-agent"), ) @@ -103,13 +109,16 @@ func TestAuthService_LockUser(t *testing.T) { updatedUser, err := repository.NewUserRepository(db).FindByID(user.ID) require.NoError(t, err) - assert.NotNil(t, updatedUser.LockedUntil) - assert.True(t, updatedUser.IsLocked()) + require.NotNil(t, updatedUser.LockedUntil) + require.True(t, updatedUser.IsLocked()) var tokenCount int64 - db.Model(&models.RefreshToken{}). - Where("user_id = ?", user.ID). - Count(&tokenCount) + + require.NoError(t, + db.Model(&models.RefreshToken{}). + Where("user_id = ? AND is_revoked = ?", user.ID, false). + Count(&tokenCount).Error, + ) assert.Equal(t, int64(0), tokenCount) } @@ -122,6 +131,21 @@ func TestAuthService_LockUser_TokenRevocation(t *testing.T) { promoteAdmin(t, db, admin.ID) + loginResp, err := authSvc.Login(&dto.LoginRequest{ + Email: "user2@example.com", + Password: "Password123!", + }, "127.0.0.1", "test-agent") + require.NoError(t, err) + require.NotEmpty(t, loginResp.AccessToken) + + var before int64 + require.NoError(t, + db.Model(&models.RefreshToken{}). + Where("user_id = ?", user.ID). + Count(&before).Error, + ) + require.Greater(t, before, int64(0)) + require.NoError(t, authSvc.LockUser(user.ID, admin.ID, "", ""), ) @@ -130,6 +154,16 @@ func TestAuthService_LockUser_TokenRevocation(t *testing.T) { require.NoError(t, err) assert.NotNil(t, updatedUser.LockedUntil) + + var after int64 + + require.NoError(t, + db.Model(&models.RefreshToken{}). + Where("user_id = ? AND is_revoked = ?", user.ID, false). + Count(&after).Error, + ) + + assert.Equal(t, int64(0), after) } func TestAuthService_LockUser_SelfLock(t *testing.T) { diff --git a/internal/testutils/setup.go b/internal/testutils/setup.go index e89ba30..382d318 100644 --- a/internal/testutils/setup.go +++ b/internal/testutils/setup.go @@ -90,6 +90,7 @@ func SetupIntegrationTest(t *testing.T) (*service.AuthService, *gorm.DB, *minire auditService, mfaService, cfg, + db, ) return authService, db, mr From 5ecb1834c1002b0e72feddf27e2a80c5033de130 Mon Sep 17 00:00:00 2001 From: Samikshaaggarwal <123529409+Samikshaaggarwal@users.noreply.github.com> Date: Thu, 4 Jun 2026 19:51:19 +0530 Subject: [PATCH 4/4] fixed test cases --- internal/routes/routes.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/routes/routes.go b/internal/routes/routes.go index 95791a0..38e57c8 100644 --- a/internal/routes/routes.go +++ b/internal/routes/routes.go @@ -48,6 +48,7 @@ func SetupRoutes(router *gin.Engine, db *gorm.DB, redisClient *redis.Client, cfg auditService, mfaService, cfg, + db, ) // OAuth Provider service