450 lines
11 KiB
Go
450 lines
11 KiB
Go
package tests
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/gojuno/minimock/v3"
|
|
"github.com/stretchr/testify/suite"
|
|
|
|
"smart-search-back/internal/mocks"
|
|
"smart-search-back/internal/model"
|
|
"smart-search-back/internal/service"
|
|
"smart-search-back/pkg/crypto"
|
|
apperrors "smart-search-back/pkg/errors"
|
|
"smart-search-back/pkg/jwt"
|
|
)
|
|
|
|
type Suite struct {
|
|
suite.Suite
|
|
|
|
ctx context.Context
|
|
authService service.AuthService
|
|
userRepo *mocks.UserRepositoryMock
|
|
sessionRepo *mocks.SessionRepositoryMock
|
|
}
|
|
|
|
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.authService = service.NewAuthService(s.userRepo, s.sessionRepo)
|
|
}
|
|
|
|
func createTestUser(password string) *model.User {
|
|
return &model.User{
|
|
ID: 1,
|
|
Email: "test@example.com",
|
|
EmailHash: crypto.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 = 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() {
|
|
s.T().Parallel()
|
|
|
|
userID := 1
|
|
accessToken, err := jwt.GenerateAccessToken(userID)
|
|
s.NoError(err)
|
|
|
|
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)
|
|
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() {
|
|
s.T().Parallel()
|
|
|
|
accessToken, err := jwt.GenerateAccessToken(0)
|
|
s.NoError(err)
|
|
|
|
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.RevokeMock.Return(nil)
|
|
|
|
err := s.authService.Logout(s.ctx, "test-refresh-token")
|
|
|
|
s.NoError(err)
|
|
}
|
|
|
|
func (s *Suite) TestAuthService_Logout_DatabaseError() {
|
|
dbErr := apperrors.NewInternalError(apperrors.DatabaseError, "failed to revoke session", nil)
|
|
s.sessionRepo.RevokeMock.Return(dbErr)
|
|
|
|
err := s.authService.Logout(s.ctx, "test-refresh-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.RevokeMock.Return(nil)
|
|
|
|
err := s.authService.Logout(s.ctx, "")
|
|
|
|
s.NoError(err)
|
|
}
|
|
|
|
func (s *Suite) TestAuthService_Logout_NonExistentToken() {
|
|
s.sessionRepo.RevokeMock.Return(nil)
|
|
|
|
err := s.authService.Logout(s.ctx, "non-existent-token")
|
|
|
|
s.NoError(err)
|
|
}
|