package tests import ( "context" "errors" "testing" "time" "github.com/gojuno/minimock/v3" "github.com/stretchr/testify/suite" "git.techease.ru/Smart-search/smart-search-back/internal/mocks" "git.techease.ru/Smart-search/smart-search-back/internal/model" "git.techease.ru/Smart-search/smart-search-back/internal/service" "git.techease.ru/Smart-search/smart-search-back/pkg/crypto" apperrors "git.techease.ru/Smart-search/smart-search-back/pkg/errors" "git.techease.ru/Smart-search/smart-search-back/pkg/jwt" ) const ( testJWTSecret = "test-jwt-secret-key" testCryptoSecret = "test-crypto-secret-key" ) type Suite struct { suite.Suite ctx context.Context authService service.AuthService userRepo *mocks.UserRepositoryMock sessionRepo *mocks.SessionRepositoryMock crypto *crypto.Crypto } func newSuite(ctx context.Context) *Suite { return &Suite{ctx: ctx} } func TestAuthService(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() suite.Run(t, newSuite(ctx)) } func (s *Suite) SetupSuite() { s.ctx = context.Background() } func (s *Suite) SetupTest() { ctrl := minimock.NewController(s.T()) s.userRepo = mocks.NewUserRepositoryMock(ctrl) s.sessionRepo = mocks.NewSessionRepositoryMock(ctrl) s.crypto = crypto.NewCrypto(testCryptoSecret) s.authService = service.NewAuthService(s.userRepo, s.sessionRepo, testJWTSecret, testCryptoSecret) } func createTestUser(password string) *model.User { c := crypto.NewCrypto(testCryptoSecret) return &model.User{ ID: 1, Email: "test@example.com", EmailHash: c.EmailHash("test@example.com"), PasswordHash: crypto.PasswordHash(password), CreatedAt: time.Now(), } } func createTestSession(userID int) *model.Session { return &model.Session{ ID: 1, UserID: userID, AccessToken: "test-access-token", RefreshToken: "test-refresh-token", IP: "127.0.0.1", UserAgent: "test-agent", CreatedAt: time.Now(), ExpiresAt: time.Now().Add(30 * 24 * time.Hour), } } func (s *Suite) TestAuthService_Login_Success() { password := "testpassword" user := createTestUser(password) s.userRepo.FindByEmailHashMock.Return(user, nil) s.sessionRepo.CreateMock.Return(nil) accessToken, refreshToken, err := s.authService.Login( s.ctx, "test@example.com", password, "127.0.0.1", "test-agent", ) s.NoError(err) s.NotEmpty(accessToken) s.NotEmpty(refreshToken) } func (s *Suite) TestAuthService_Login_UserNotFound() { err := apperrors.NewBusinessError(apperrors.UserNotFound, "user not found") s.userRepo.FindByEmailHashMock.Return(nil, err) accessToken, refreshToken, loginErr := s.authService.Login( s.ctx, "test@example.com", "password", "127.0.0.1", "test-agent", ) s.Error(loginErr) s.Empty(accessToken) s.Empty(refreshToken) var appErr *apperrors.AppError s.True(errors.As(loginErr, &appErr)) s.Equal(apperrors.UserNotFound, appErr.Code) } func (s *Suite) TestAuthService_Login_DatabaseError_OnFindUser() { dbErr := apperrors.NewInternalError(apperrors.DatabaseError, "failed to find user", nil) s.userRepo.FindByEmailHashMock.Return(nil, dbErr) accessToken, refreshToken, err := s.authService.Login( s.ctx, "test@example.com", "password", "127.0.0.1", "test-agent", ) s.Error(err) s.Empty(accessToken) s.Empty(refreshToken) var appErr *apperrors.AppError s.True(errors.As(err, &appErr)) s.Equal(apperrors.InternalErrorType, appErr.Type) } func (s *Suite) TestAuthService_Login_InvalidPassword() { password := "correctpassword" user := createTestUser(password) s.userRepo.FindByEmailHashMock.Return(user, nil) accessToken, refreshToken, err := s.authService.Login( s.ctx, "test@example.com", "wrongpassword", "127.0.0.1", "test-agent", ) s.Error(err) s.Empty(accessToken) s.Empty(refreshToken) var appErr *apperrors.AppError s.True(errors.As(err, &appErr)) s.Equal(apperrors.AuthInvalidCredentials, appErr.Code) s.Contains(appErr.Message, "Invalid email or password") } func (s *Suite) TestAuthService_Login_EmptyPassword() { user := createTestUser("") s.userRepo.FindByEmailHashMock.Return(user, nil) s.sessionRepo.CreateMock.Return(nil) accessToken, refreshToken, err := s.authService.Login( s.ctx, "test@example.com", "", "127.0.0.1", "test-agent", ) s.NoError(err) s.NotEmpty(accessToken) s.NotEmpty(refreshToken) } func (s *Suite) TestAuthService_Login_EmailWithSpacesAndCase() { password := "testpassword" normalizedEmail := "test@example.com" user := createTestUser(password) user.EmailHash = s.crypto.EmailHash(normalizedEmail) s.userRepo.FindByEmailHashMock.Return(user, nil) s.sessionRepo.CreateMock.Return(nil) accessToken, refreshToken, err := s.authService.Login( s.ctx, " TEST@EXAMPLE.COM ", password, "127.0.0.1", "test-agent", ) s.NoError(err) s.NotEmpty(accessToken) s.NotEmpty(refreshToken) } func (s *Suite) TestAuthService_Login_EmptyEmail() { err := apperrors.NewBusinessError(apperrors.UserNotFound, "user not found") s.userRepo.FindByEmailHashMock.Return(nil, err) accessToken, refreshToken, loginErr := s.authService.Login( s.ctx, "", "password", "127.0.0.1", "test-agent", ) s.Error(loginErr) s.Empty(accessToken) s.Empty(refreshToken) } func (s *Suite) TestAuthService_Login_SessionCreateError() { password := "testpassword" user := createTestUser(password) s.userRepo.FindByEmailHashMock.Return(user, nil) dbErr := apperrors.NewInternalError(apperrors.DatabaseError, "failed to create session", nil) s.sessionRepo.CreateMock.Return(dbErr) accessToken, refreshToken, err := s.authService.Login( s.ctx, "test@example.com", password, "127.0.0.1", "test-agent", ) s.Error(err) s.Empty(accessToken) s.Empty(refreshToken) } func (s *Suite) TestAuthService_Login_EmptyIPAndUserAgent() { password := "testpassword" user := createTestUser(password) s.userRepo.FindByEmailHashMock.Return(user, nil) s.sessionRepo.CreateMock.Return(nil) accessToken, refreshToken, err := s.authService.Login( s.ctx, "test@example.com", password, "", "", ) s.NoError(err) s.NotEmpty(accessToken) s.NotEmpty(refreshToken) } func (s *Suite) TestAuthService_Refresh_Success() { session := createTestSession(1) s.sessionRepo.FindByRefreshTokenMock.Return(session, nil) s.sessionRepo.UpdateAccessTokenMock.Return(nil) accessToken, err := s.authService.Refresh(s.ctx, "test-refresh-token") s.NoError(err) s.NotEmpty(accessToken) } func (s *Suite) TestAuthService_Refresh_RefreshInvalid() { err := apperrors.NewBusinessError(apperrors.RefreshInvalid, "refresh token is invalid or expired") s.sessionRepo.FindByRefreshTokenMock.Return(nil, err) accessToken, refreshErr := s.authService.Refresh(s.ctx, "invalid-token") s.Error(refreshErr) s.Empty(accessToken) var appErr *apperrors.AppError s.True(errors.As(refreshErr, &appErr)) s.Equal(apperrors.RefreshInvalid, appErr.Code) } func (s *Suite) TestAuthService_Refresh_DatabaseError_OnFindSession() { dbErr := apperrors.NewInternalError(apperrors.DatabaseError, "failed to find session", nil) s.sessionRepo.FindByRefreshTokenMock.Return(nil, dbErr) accessToken, err := s.authService.Refresh(s.ctx, "test-refresh-token") s.Error(err) s.Empty(accessToken) var appErr *apperrors.AppError s.True(errors.As(err, &appErr)) s.Equal(apperrors.InternalErrorType, appErr.Type) } func (s *Suite) TestAuthService_Refresh_EmptyToken() { err := apperrors.NewBusinessError(apperrors.RefreshInvalid, "refresh token is invalid or expired") s.sessionRepo.FindByRefreshTokenMock.Return(nil, err) accessToken, refreshErr := s.authService.Refresh(s.ctx, "") s.Error(refreshErr) s.Empty(accessToken) } func (s *Suite) TestAuthService_Refresh_UpdateAccessTokenError() { session := createTestSession(1) s.sessionRepo.FindByRefreshTokenMock.Return(session, nil) dbErr := apperrors.NewInternalError(apperrors.DatabaseError, "failed to update access token", nil) s.sessionRepo.UpdateAccessTokenMock.Return(dbErr) accessToken, err := s.authService.Refresh(s.ctx, "test-refresh-token") s.Error(err) s.Empty(accessToken) } func (s *Suite) TestAuthService_Refresh_UserIDZero() { session := createTestSession(0) s.sessionRepo.FindByRefreshTokenMock.Return(session, nil) s.sessionRepo.UpdateAccessTokenMock.Return(nil) accessToken, err := s.authService.Refresh(s.ctx, "test-refresh-token") s.NoError(err) s.NotEmpty(accessToken) } func (s *Suite) TestAuthService_Validate_Success() { userID := 1 accessToken, err := jwt.GenerateAccessToken(userID, testJWTSecret) s.NoError(err) s.sessionRepo.IsAccessTokenValidMock.Return(true, nil) validatedUserID, validateErr := s.authService.Validate(s.ctx, accessToken) s.NoError(validateErr) s.Equal(userID, validatedUserID) } func (s *Suite) TestAuthService_Validate_EmptyToken() { s.T().Parallel() userID, err := s.authService.Validate(s.ctx, "") s.Error(err) s.Equal(0, userID) var appErr *apperrors.AppError s.True(errors.As(err, &appErr)) s.Equal(apperrors.AuthInvalidToken, appErr.Code) } func (s *Suite) TestAuthService_Validate_InvalidTokenFormat() { s.T().Parallel() userID, err := s.authService.Validate(s.ctx, "invalid.token.format") s.Error(err) s.Equal(0, userID) var appErr *apperrors.AppError s.True(errors.As(err, &appErr)) s.Equal(apperrors.AuthInvalidToken, appErr.Code) } func (s *Suite) TestAuthService_Validate_RefreshTokenInsteadOfAccess() { s.T().Parallel() userID := 1 refreshToken, err := jwt.GenerateRefreshToken(userID, testJWTSecret) s.NoError(err) validatedUserID, validateErr := s.authService.Validate(s.ctx, refreshToken) s.Error(validateErr) s.Equal(0, validatedUserID) var appErr *apperrors.AppError s.True(errors.As(validateErr, &appErr)) s.Equal(apperrors.AuthInvalidToken, appErr.Code) s.Contains(appErr.Message, "not an access token") } func (s *Suite) TestAuthService_Validate_UserIDZero() { accessToken, err := jwt.GenerateAccessToken(0, testJWTSecret) s.NoError(err) s.sessionRepo.IsAccessTokenValidMock.Return(true, nil) validatedUserID, validateErr := s.authService.Validate(s.ctx, accessToken) s.NoError(validateErr) s.Equal(0, validatedUserID) } func (s *Suite) TestAuthService_Validate_InvalidSignature() { s.T().Parallel() invalidToken := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxIiwidHlwZSI6ImFjY2VzcyIsImlhdCI6MTYwOTQ1NjgwMCwiZXhwIjoxNjA5NDY1ODAwfQ.invalid-signature" userID, err := s.authService.Validate(s.ctx, invalidToken) s.Error(err) s.Equal(0, userID) var appErr *apperrors.AppError s.True(errors.As(err, &appErr)) s.Equal(apperrors.AuthInvalidToken, appErr.Code) } func (s *Suite) TestAuthService_Logout_Success() { s.sessionRepo.RevokeByAccessTokenMock.Return(nil) err := s.authService.Logout(s.ctx, "test-access-token") s.NoError(err) } func (s *Suite) TestAuthService_Logout_DatabaseError() { dbErr := apperrors.NewInternalError(apperrors.DatabaseError, "failed to revoke session", nil) s.sessionRepo.RevokeByAccessTokenMock.Return(dbErr) err := s.authService.Logout(s.ctx, "test-access-token") s.Error(err) var appErr *apperrors.AppError s.True(errors.As(err, &appErr)) s.Equal(apperrors.InternalErrorType, appErr.Type) } func (s *Suite) TestAuthService_Logout_EmptyToken() { s.sessionRepo.RevokeByAccessTokenMock.Return(nil) err := s.authService.Logout(s.ctx, "") s.NoError(err) } func (s *Suite) TestAuthService_Logout_NonExistentToken() { s.sessionRepo.RevokeByAccessTokenMock.Return(nil) err := s.authService.Logout(s.ctx, "non-existent-token") s.NoError(err) }