refactor: [shitty claude AI first try] restructure server and user services, add new test cases, and improve error handling

This commit is contained in:
2025-08-10 21:40:15 +03:00
parent 588576b82f
commit f503e45be1
23 changed files with 2568 additions and 134 deletions

View File

@ -0,0 +1,146 @@
package domain
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestErrOtpNotFound_Error(t *testing.T) {
tests := []struct {
name string
uuid string
want string
}{
{
name: "returns formatted error message",
uuid: "123e4567-e89b-12d3-a456-426614174000",
want: "OTP request not found for UUID: 123e4567-e89b-12d3-a456-426614174000",
},
{
name: "handles empty uuid",
uuid: "",
want: "OTP request not found for UUID: ",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ErrOtpNotFound{Uuid: tt.uuid}
assert.Equal(t, tt.want, err.Error())
})
}
}
func TestErrUserNotFound_Error(t *testing.T) {
tests := []struct {
name string
phoneNumber string
want string
}{
{
name: "returns formatted error message",
phoneNumber: "+1234567890",
want: "User not found with phone number: +1234567890",
},
{
name: "handles empty phone number",
phoneNumber: "",
want: "User not found with phone number: ",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ErrUserNotFound{PhoneNumber: tt.phoneNumber}
assert.Equal(t, tt.want, err.Error())
})
}
}
func TestErrOtpInvalid_Error(t *testing.T) {
tests := []struct {
name string
code string
uuid string
want string
}{
{
name: "returns formatted error message",
code: "123456",
uuid: "123e4567-e89b-12d3-a456-426614174000",
want: "Invalid OTP code: 123456 for UUID: 123e4567-e89b-12d3-a456-426614174000",
},
{
name: "handles empty values",
code: "",
uuid: "",
want: "Invalid OTP code: for UUID: ",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ErrOtpInvalid{Code: tt.code, Uuid: tt.uuid}
assert.Equal(t, tt.want, err.Error())
})
}
}
func TestErrInvalidHydraAccept_Error(t *testing.T) {
tests := []struct {
name string
message string
uuid string
want string
}{
{
name: "returns formatted error message",
message: "Invalid response",
uuid: "123e4567-e89b-12d3-a456-426614174000",
want: "Invalid Hydra accept request: Invalid response for UUID: 123e4567-e89b-12d3-a456-426614174000",
},
{
name: "handles empty values",
message: "",
uuid: "",
want: "Invalid Hydra accept request: for UUID: ",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ErrInvalidHydraAccept{Message: tt.message, Uuid: tt.uuid}
assert.Equal(t, tt.want, err.Error())
})
}
}
func TestErrInvalidPhoneNumber_Error(t *testing.T) {
tests := []struct {
name string
phoneNumber string
err error
want string
}{
{
name: "returns formatted error message with nested error",
phoneNumber: "invalid",
err: assert.AnError,
want: "Invalid phone number: invalid, error: assert.AnError general error for testing",
},
{
name: "handles empty phone number",
phoneNumber: "",
err: assert.AnError,
want: "Invalid phone number: , error: assert.AnError general error for testing",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ErrInvalidPhoneNumber{PhoneNumber: tt.phoneNumber, Err: tt.err}
assert.Equal(t, tt.want, err.Error())
})
}
}

View File

@ -0,0 +1,293 @@
package handler
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
// MockAuthService implements service.AuthService
type MockAuthService struct {
mock.Mock
}
func (m *MockAuthService) OtpRequest(ctx context.Context, phoneNumber string) error {
args := m.Called(ctx, phoneNumber)
return args.Error(0)
}
func (m *MockAuthService) OtpVerify(ctx context.Context, phoneNumber, code string, loginChallenge string) (string, error) {
args := m.Called(ctx, phoneNumber, code, loginChallenge)
return args.String(0), args.Error(1)
}
func (m *MockAuthService) AcceptConsent(ctx context.Context, phoneNumber string, challenge string) (string, error) {
args := m.Called(ctx, phoneNumber, challenge)
return args.String(0), args.Error(1)
}
func TestNewAuthHandler(t *testing.T) {
mockService := &MockAuthService{}
handler := NewAuthHandler(mockService)
assert.NotNil(t, handler)
assert.Equal(t, mockService, handler.service)
assert.Implements(t, (*StrictServerInterface)(nil), handler)
}
func TestAuthHandler_PostAuthOtpRequest(t *testing.T) {
tests := []struct {
name string
phoneNumber string
setupMock func(*MockAuthService)
expectedStatus int
expectedOk bool
expectedMsg string
}{
{
name: "successful otp request",
phoneNumber: "+79161234567",
setupMock: func(m *MockAuthService) {
m.On("OtpRequest", mock.Anything, "+79161234567").Return(nil).Once()
},
expectedStatus: 200,
expectedOk: true,
expectedMsg: "OTP request successful",
},
{
name: "service error",
phoneNumber: "invalid",
setupMock: func(m *MockAuthService) {
m.On("OtpRequest", mock.Anything, "invalid").Return(assert.AnError).Once()
},
expectedStatus: 400,
expectedOk: false,
expectedMsg: assert.AnError.Error(),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockService := &MockAuthService{}
handler := &AuthHandler{service: mockService}
ctx := context.Background()
tt.setupMock(mockService)
request := PostAuthOtpRequestRequestObject{
Body: &PostAuthOtpRequestJSONRequestBody{
PhoneNumber: tt.phoneNumber,
},
}
result, err := handler.PostAuthOtpRequest(ctx, request)
assert.NoError(t, err) // Handler should not return errors, only response objects
if tt.expectedStatus == 200 {
response, ok := result.(PostAuthOtpRequest200JSONResponse)
assert.True(t, ok)
assert.Equal(t, tt.expectedOk, response.Ok)
assert.Equal(t, tt.expectedMsg, response.Message)
} else {
response, ok := result.(PostAuthOtpRequest400JSONResponse)
assert.True(t, ok)
assert.Equal(t, tt.expectedOk, response.Ok)
assert.Equal(t, tt.expectedMsg, response.Message)
}
mockService.AssertExpectations(t)
})
}
}
func TestAuthHandler_PostAuthOtpVerify(t *testing.T) {
tests := []struct {
name string
phoneNumber string
otp string
loginChallenge string
setupMock func(*MockAuthService)
expectedStatus int
expectedOk bool
expectedMsg string
expectedRedirect string
}{
{
name: "successful otp verification",
phoneNumber: "+79161234567",
otp: "123456",
loginChallenge: "challenge123",
setupMock: func(m *MockAuthService) {
m.On("OtpVerify", mock.Anything, "+79161234567", "123456", "challenge123").
Return("https://example.com/callback", nil).Once()
},
expectedStatus: 200,
expectedOk: true,
expectedMsg: "OTP verification successful",
expectedRedirect: "https://example.com/callback",
},
{
name: "service error",
phoneNumber: "+79161234567",
otp: "wrong",
loginChallenge: "challenge123",
setupMock: func(m *MockAuthService) {
m.On("OtpVerify", mock.Anything, "+79161234567", "wrong", "challenge123").
Return("", assert.AnError).Once()
},
expectedStatus: 400,
expectedOk: false,
expectedMsg: assert.AnError.Error(),
expectedRedirect: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockService := &MockAuthService{}
handler := &AuthHandler{service: mockService}
ctx := context.Background()
tt.setupMock(mockService)
request := PostAuthOtpVerifyRequestObject{
Body: &PostAuthOtpVerifyJSONRequestBody{
PhoneNumber: tt.phoneNumber,
Otp: tt.otp,
LoginChallenge: tt.loginChallenge,
},
}
result, err := handler.PostAuthOtpVerify(ctx, request)
assert.NoError(t, err)
if tt.expectedStatus == 200 {
response, ok := result.(PostAuthOtpVerify200JSONResponse)
assert.True(t, ok)
assert.Equal(t, tt.expectedOk, response.Ok)
assert.Equal(t, tt.expectedMsg, response.Message)
assert.Equal(t, tt.expectedRedirect, response.RedirectUrl)
} else {
response, ok := result.(PostAuthOtpVerify400JSONResponse)
assert.True(t, ok)
assert.Equal(t, tt.expectedOk, response.Ok)
assert.Equal(t, tt.expectedMsg, response.Message)
assert.Equal(t, tt.expectedRedirect, response.RedirectUrl)
}
mockService.AssertExpectations(t)
})
}
}
func TestAuthHandler_PostAuthConsentAccept(t *testing.T) {
tests := []struct {
name string
phoneNumber string
consentChallenge string
setupMock func(*MockAuthService)
expectedStatus int
expectedOk bool
expectedMsg string
expectedRedirect string
}{
{
name: "successful consent accept",
phoneNumber: "+79161234567",
consentChallenge: "consent123",
setupMock: func(m *MockAuthService) {
m.On("AcceptConsent", mock.Anything, "+79161234567", "consent123").
Return("https://example.com/callback", nil).Once()
},
expectedStatus: 200,
expectedOk: true,
expectedMsg: "Consent accepted successfully",
expectedRedirect: "https://example.com/callback",
},
{
name: "service error",
phoneNumber: "+79161234567",
consentChallenge: "invalid",
setupMock: func(m *MockAuthService) {
m.On("AcceptConsent", mock.Anything, "+79161234567", "invalid").
Return("", assert.AnError).Once()
},
expectedStatus: 400,
expectedOk: false,
expectedMsg: assert.AnError.Error(),
expectedRedirect: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockService := &MockAuthService{}
handler := &AuthHandler{service: mockService}
ctx := context.Background()
tt.setupMock(mockService)
request := PostAuthConsentAcceptRequestObject{
Body: &PostAuthConsentAcceptJSONRequestBody{
PhoneNumber: tt.phoneNumber,
ConsentChallenge: tt.consentChallenge,
},
}
result, err := handler.PostAuthConsentAccept(ctx, request)
assert.NoError(t, err)
if tt.expectedStatus == 200 {
response, ok := result.(PostAuthConsentAccept200JSONResponse)
assert.True(t, ok)
assert.Equal(t, tt.expectedOk, response.Ok)
assert.Equal(t, tt.expectedMsg, response.Message)
assert.Equal(t, tt.expectedRedirect, response.RedirectUrl)
} else {
response, ok := result.(PostAuthConsentAccept400JSONResponse)
assert.True(t, ok)
assert.Equal(t, tt.expectedOk, response.Ok)
assert.Equal(t, tt.expectedMsg, response.Message)
assert.Equal(t, tt.expectedRedirect, response.RedirectUrl)
}
mockService.AssertExpectations(t)
})
}
}
func TestAuthHandler_EdgeCases(t *testing.T) {
t.Run("nil request body should not panic", func(t *testing.T) {
mockService := &MockAuthService{}
handler := &AuthHandler{service: mockService}
ctx := context.Background()
// Test with nil body - this should be handled by the generated code
// but we test that our handler doesn't panic
defer func() {
if r := recover(); r != nil {
t.Errorf("Handler panicked with nil body: %v", r)
}
}()
request := PostAuthOtpRequestRequestObject{
Body: &PostAuthOtpRequestJSONRequestBody{
PhoneNumber: "",
},
}
mockService.On("OtpRequest", mock.Anything, "").Return(assert.AnError).Once()
result, err := handler.PostAuthOtpRequest(ctx, request)
assert.NoError(t, err)
assert.NotNil(t, result)
mockService.AssertExpectations(t)
})
}

View File

@ -0,0 +1,34 @@
package repo
import (
"testing"
"git.logidex.ru/fakz9/logidex-id/internal/api/auth/domain"
"github.com/stretchr/testify/assert"
)
func TestNewAuthRepo(t *testing.T) {
// Test with nil client for interface testing
// In real tests, you would use a mock or test container
repo := NewAuthRepo(nil)
assert.NotNil(t, repo)
assert.Implements(t, (*domain.AuthRepository)(nil), repo)
}
func TestAuthRepo_Interface(t *testing.T) {
// Test that our auth repo implements the domain interface
var _ domain.AuthRepository = (*authRepo)(nil)
// Test constructor returns correct interface
repo := NewAuthRepo(nil)
// Verify interface compliance
assert.Implements(t, (*domain.AuthRepository)(nil), repo)
}
// Note: Actual Redis operations testing requires either:
// 1. Test containers with real Redis
// 2. A simpler interface wrapper around Redis
// 3. A Redis mock library specifically designed for rueidis
// For now, we focus on interface compliance and constructor behavior

View File

@ -2,10 +2,12 @@ package service
import (
"context"
"net/http"
"strconv"
"git.logidex.ru/fakz9/logidex-id/internal/api/auth/domain"
userDomain "git.logidex.ru/fakz9/logidex-id/internal/api/user/domain"
userService "git.logidex.ru/fakz9/logidex-id/internal/api/user/service"
"git.logidex.ru/fakz9/logidex-id/internal/phoneutil"
hydraApi "github.com/ory/hydra-client-go"
)
@ -18,24 +20,75 @@ type AuthService interface {
type authService struct {
repo domain.AuthRepository
userRepo userDomain.UserRepository
userService userService.UserService
hydraClient *hydraApi.APIClient
}
func (a authService) AcceptConsent(ctx context.Context, phoneNumber string, challenge string) (string, error) {
phoneNumber, err := phoneutil.ParseAndFormatPhoneNumber(phoneNumber)
func (a authService) validateAndFormatPhoneNumber(phoneNumber string) (string, error) {
formattedPhone, err := phoneutil.ParseAndFormatPhoneNumber(phoneNumber)
if err != nil {
return "", domain.ErrInvalidPhoneNumber{
PhoneNumber: phoneNumber,
Err: err,
}
}
user, err := a.userRepo.GetUserByPhoneNumber(ctx, phoneNumber)
return formattedPhone, nil
}
func (a authService) getUserByPhoneNumber(ctx context.Context, phoneNumber string) (*userDomain.User, error) {
user, err := a.userService.GetUserByPhoneNumber(ctx, phoneNumber)
if err != nil {
return nil, err
}
if user == nil {
return nil, domain.ErrUserNotFound{PhoneNumber: phoneNumber}
}
return user, nil
}
func (a authService) getOrCreateUser(ctx context.Context, phoneNumber string) (*userDomain.User, error) {
user, err := a.userService.GetUserByPhoneNumber(ctx, phoneNumber)
if err != nil {
return nil, err
}
if user == nil {
user, err = a.userService.CreateUser(ctx, phoneNumber)
if err != nil {
return nil, err
}
}
return user, nil
}
func (a authService) validateHydraResponse(rawRsp *http.Response, userUuid string) error {
if rawRsp.StatusCode != 200 {
return domain.ErrInvalidHydraAccept{
Message: "Hydra response status: " + strconv.Itoa(rawRsp.StatusCode),
Uuid: userUuid,
}
}
return nil
}
func (a authService) extractRedirectUrl(rsp interface{ GetRedirectToOk() (*string, bool) }, userUuid string) (string, error) {
redirectTo, ok := rsp.GetRedirectToOk()
if !ok || redirectTo == nil {
return "", domain.ErrInvalidHydraAccept{
Message: "Hydra redirectTo is nil",
Uuid: userUuid,
}
}
return *redirectTo, nil
}
func (a authService) AcceptConsent(ctx context.Context, phoneNumber string, challenge string) (string, error) {
phoneNumber, err := a.validateAndFormatPhoneNumber(phoneNumber)
if err != nil {
return "", err
}
if user == nil {
return "", domain.ErrUserNotFound{PhoneNumber: phoneNumber}
user, err := a.getUserByPhoneNumber(ctx, phoneNumber)
if err != nil {
return "", err
}
request := hydraApi.AcceptConsentRequest{}
request.SetGrantScope([]string{"openid"})
@ -50,86 +103,65 @@ func (a authService) AcceptConsent(ctx context.Context, phoneNumber string, chal
if err != nil {
return "", err
}
if rawRsp.StatusCode != 200 {
return "", domain.ErrInvalidHydraAccept{
Message: "Hydra response is nil: " + strconv.Itoa(rawRsp.StatusCode),
Uuid: "",
}
if err = a.validateHydraResponse(rawRsp, user.Uuid); err != nil {
return "", err
}
redirectTo, ok := rsp.GetRedirectToOk()
if !ok || redirectTo == nil {
return "", domain.ErrInvalidHydraAccept{
Message: "Hydra redirectTo is nil",
Uuid: "",
}
}
// TODO: Verify user in the database
_, err = a.userRepo.VerifyUser(ctx, user.Uuid.String())
redirectUrl, err := a.extractRedirectUrl(rsp, user.Uuid)
if err != nil {
return "", err
}
return *redirectTo, nil
// TODO: Verify user in the database
_, err = a.userService.VerifyUser(ctx, user.Uuid)
if err != nil {
return "", err
}
return redirectUrl, nil
}
func (a authService) OtpRequest(ctx context.Context, phoneNumber string) error {
phoneNumber, err := phoneutil.ParseAndFormatPhoneNumber(phoneNumber)
phoneNumber, err := a.validateAndFormatPhoneNumber(phoneNumber)
if err != nil {
return domain.ErrInvalidPhoneNumber{
PhoneNumber: phoneNumber,
Err: err,
}
return err
}
user, err := a.userRepo.GetUserByPhoneNumber(ctx, phoneNumber)
//if err != nil {
// return err
//}
if user == nil {
// Create a new user if it does not exist
user, err = a.userRepo.CreateUser(ctx, phoneNumber)
if err != nil {
return err
}
user, err := a.getOrCreateUser(ctx, phoneNumber)
if err != nil {
return err
}
code := "123456"
err = a.repo.SaveOtpRequest(ctx, user.Uuid.String(), code)
err = a.repo.SaveOtpRequest(ctx, user.Uuid, code)
if err != nil {
return err
}
// TODO implement sending OTP code via SMS
return nil
}
func (a authService) OtpVerify(ctx context.Context, phoneNumber string, code string, loggingChallenge string) (string, error) {
phoneNumber, err := phoneutil.ParseAndFormatPhoneNumber(phoneNumber)
if err != nil {
return "", domain.ErrInvalidPhoneNumber{
PhoneNumber: phoneNumber,
Err: err,
}
}
user, err := a.userRepo.GetUserByPhoneNumber(ctx, phoneNumber)
phoneNumber, err := a.validateAndFormatPhoneNumber(phoneNumber)
if err != nil {
return "", err
}
if user == nil {
return "", domain.ErrUserNotFound{PhoneNumber: phoneNumber}
user, err := a.getUserByPhoneNumber(ctx, phoneNumber)
if err != nil {
return "", err
}
otp, err := a.repo.GetOtpRequest(ctx, user.Uuid.String())
otp, err := a.repo.GetOtpRequest(ctx, user.Uuid)
if err != nil {
return "", err
}
if otp == nil {
return "", domain.ErrOtpNotFound{Uuid: user.Uuid.String()}
return "", domain.ErrOtpNotFound{Uuid: user.Uuid}
}
if *otp != code {
return "", domain.ErrOtpInvalid{Uuid: user.Uuid.String(), Code: code}
return "", domain.ErrOtpInvalid{Uuid: user.Uuid, Code: code}
}
request := hydraApi.AcceptLoginRequest{}
request.SetSubject(user.Uuid.String())
request.SetSubject(user.Uuid)
request.SetRemember(true)
request.SetRememberFor(3600) // 1 hour
@ -141,27 +173,22 @@ func (a authService) OtpVerify(ctx context.Context, phoneNumber string, code str
if err != nil {
return "", err
}
if rawRsp.StatusCode != 200 {
return "", domain.ErrInvalidHydraAccept{
Message: "Hydra response is nil: " + strconv.Itoa(rawRsp.StatusCode),
Uuid: user.Uuid.String(),
}
if err = a.validateHydraResponse(rawRsp, user.Uuid); err != nil {
return "", err
}
redirectTo, ok := rsp.GetRedirectToOk()
if !ok || redirectTo == nil {
return "", domain.ErrInvalidHydraAccept{
Message: "Hydra redirectTo is nil",
Uuid: user.Uuid.String(),
}
redirectUrl, err := a.extractRedirectUrl(rsp, user.Uuid)
if err != nil {
return "", err
}
return *redirectTo, nil
return redirectUrl, nil
}
func NewAuthService(repo domain.AuthRepository, userRepo userDomain.UserRepository, hydraClient *hydraApi.APIClient) AuthService {
func NewAuthService(repo domain.AuthRepository, userService userService.UserService, hydraClient *hydraApi.APIClient) AuthService {
return &authService{
repo: repo,
userRepo: userRepo,
userService: userService,
hydraClient: hydraClient,
}
}

View File

@ -0,0 +1,429 @@
package service
import (
"context"
"net/http"
"testing"
"git.logidex.ru/fakz9/logidex-id/internal/api/auth/domain"
userDomain "git.logidex.ru/fakz9/logidex-id/internal/api/user/domain"
hydraApi "github.com/ory/hydra-client-go"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
// MockAuthRepository implements domain.AuthRepository
type MockAuthRepository struct {
mock.Mock
}
func (m *MockAuthRepository) SaveOtpRequest(ctx context.Context, uuid string, code string) error {
args := m.Called(ctx, uuid, code)
return args.Error(0)
}
func (m *MockAuthRepository) GetOtpRequest(ctx context.Context, uuid string) (*string, error) {
args := m.Called(ctx, uuid)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*string), args.Error(1)
}
// MockUserService implements userService.UserService
type MockUserService struct {
mock.Mock
}
func (m *MockUserService) GetUserByPhoneNumber(ctx context.Context, phoneNumber string) (*userDomain.User, error) {
args := m.Called(ctx, phoneNumber)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*userDomain.User), args.Error(1)
}
func (m *MockUserService) GetUserByUuid(ctx context.Context, uuid string) (*userDomain.User, error) {
args := m.Called(ctx, uuid)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*userDomain.User), args.Error(1)
}
func (m *MockUserService) CreateUser(ctx context.Context, phoneNumber string) (*userDomain.User, error) {
args := m.Called(ctx, phoneNumber)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*userDomain.User), args.Error(1)
}
func (m *MockUserService) VerifyUser(ctx context.Context, uuid string) (*userDomain.User, error) {
args := m.Called(ctx, uuid)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*userDomain.User), args.Error(1)
}
// MockHydraResponse implements hydra response interface
type MockHydraResponse struct {
redirectTo *string
}
func (m *MockHydraResponse) GetRedirectToOk() (*string, bool) {
if m.redirectTo == nil {
return nil, false
}
return m.redirectTo, true
}
func TestNewAuthService(t *testing.T) {
mockRepo := &MockAuthRepository{}
mockUserService := &MockUserService{}
mockHydraClient := &hydraApi.APIClient{}
service := NewAuthService(mockRepo, mockUserService, mockHydraClient)
assert.NotNil(t, service)
assert.Implements(t, (*AuthService)(nil), service)
}
func TestAuthService_getUserByPhoneNumber(t *testing.T) {
mockUserService := &MockUserService{}
service := &authService{userService: mockUserService}
ctx := context.Background()
phoneNumber := "+79161234567"
tests := []struct {
name string
setupMock func()
expectedUser *userDomain.User
wantErr bool
errType interface{}
}{
{
name: "user found",
setupMock: func() {
user := &userDomain.User{
Uuid: "test-uuid",
PhoneNumber: phoneNumber,
}
mockUserService.On("GetUserByPhoneNumber", ctx, phoneNumber).Return(user, nil).Once()
},
expectedUser: &userDomain.User{
Uuid: "test-uuid",
PhoneNumber: phoneNumber,
},
wantErr: false,
},
{
name: "user not found",
setupMock: func() {
mockUserService.On("GetUserByPhoneNumber", ctx, phoneNumber).Return(nil, nil).Once()
},
wantErr: true,
errType: domain.ErrUserNotFound{},
},
{
name: "service error",
setupMock: func() {
mockUserService.On("GetUserByPhoneNumber", ctx, phoneNumber).Return(nil, assert.AnError).Once()
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.setupMock()
result, err := service.getUserByPhoneNumber(ctx, phoneNumber)
if tt.wantErr {
assert.Error(t, err)
assert.Nil(t, result)
if tt.errType != nil {
assert.IsType(t, tt.errType, err)
}
} else {
assert.NoError(t, err)
assert.Equal(t, tt.expectedUser, result)
}
mockUserService.AssertExpectations(t)
})
}
}
func TestAuthService_getOrCreateUser(t *testing.T) {
mockUserService := &MockUserService{}
service := &authService{userService: mockUserService}
ctx := context.Background()
phoneNumber := "+79161234567"
tests := []struct {
name string
setupMock func()
expectedUser *userDomain.User
wantErr bool
}{
{
name: "existing user found",
setupMock: func() {
user := &userDomain.User{
Uuid: "test-uuid",
PhoneNumber: phoneNumber,
}
mockUserService.On("GetUserByPhoneNumber", ctx, phoneNumber).Return(user, nil).Once()
},
expectedUser: &userDomain.User{
Uuid: "test-uuid",
PhoneNumber: phoneNumber,
},
wantErr: false,
},
{
name: "user not found, create new",
setupMock: func() {
newUser := &userDomain.User{
Uuid: "new-uuid",
PhoneNumber: phoneNumber,
}
mockUserService.On("GetUserByPhoneNumber", ctx, phoneNumber).Return(nil, nil).Once()
mockUserService.On("CreateUser", ctx, phoneNumber).Return(newUser, nil).Once()
},
expectedUser: &userDomain.User{
Uuid: "new-uuid",
PhoneNumber: phoneNumber,
},
wantErr: false,
},
{
name: "get user service error",
setupMock: func() {
mockUserService.On("GetUserByPhoneNumber", ctx, phoneNumber).Return(nil, assert.AnError).Once()
},
wantErr: true,
},
{
name: "create user service error",
setupMock: func() {
mockUserService.On("GetUserByPhoneNumber", ctx, phoneNumber).Return(nil, nil).Once()
mockUserService.On("CreateUser", ctx, phoneNumber).Return(nil, assert.AnError).Once()
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.setupMock()
result, err := service.getOrCreateUser(ctx, phoneNumber)
if tt.wantErr {
assert.Error(t, err)
assert.Nil(t, result)
} else {
assert.NoError(t, err)
assert.Equal(t, tt.expectedUser, result)
}
mockUserService.AssertExpectations(t)
})
}
}
func TestAuthService_validateHydraResponse(t *testing.T) {
service := &authService{}
userUuid := "test-uuid"
tests := []struct {
name string
statusCode int
wantErr bool
errType interface{}
}{
{
name: "success status",
statusCode: 200,
wantErr: false,
},
{
name: "bad request status",
statusCode: 400,
wantErr: true,
errType: domain.ErrInvalidHydraAccept{},
},
{
name: "internal server error status",
statusCode: 500,
wantErr: true,
errType: domain.ErrInvalidHydraAccept{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
resp := &http.Response{StatusCode: tt.statusCode}
err := service.validateHydraResponse(resp, userUuid)
if tt.wantErr {
assert.Error(t, err)
if tt.errType != nil {
assert.IsType(t, tt.errType, err)
}
} else {
assert.NoError(t, err)
}
})
}
}
func TestAuthService_extractRedirectUrl(t *testing.T) {
service := &authService{}
userUuid := "test-uuid"
tests := []struct {
name string
response *MockHydraResponse
expectedUrl string
wantErr bool
errType interface{}
}{
{
name: "valid redirect url",
response: &MockHydraResponse{
redirectTo: stringPtr("https://example.com/callback"),
},
expectedUrl: "https://example.com/callback",
wantErr: false,
},
{
name: "nil redirect url",
response: &MockHydraResponse{
redirectTo: nil,
},
wantErr: true,
errType: domain.ErrInvalidHydraAccept{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := service.extractRedirectUrl(tt.response, userUuid)
if tt.wantErr {
assert.Error(t, err)
assert.Empty(t, result)
if tt.errType != nil {
assert.IsType(t, tt.errType, err)
}
} else {
assert.NoError(t, err)
assert.Equal(t, tt.expectedUrl, result)
}
})
}
}
func TestAuthService_OtpRequest(t *testing.T) {
mockRepo := &MockAuthRepository{}
mockUserService := &MockUserService{}
service := &authService{
repo: mockRepo,
userService: mockUserService,
}
ctx := context.Background()
phoneNumber := "89161234567" // will be formatted to +79161234567
tests := []struct {
name string
setupMock func()
wantErr bool
errType interface{}
}{
{
name: "success - existing user",
setupMock: func() {
user := &userDomain.User{
Uuid: "test-uuid",
PhoneNumber: "+79161234567",
}
mockUserService.On("GetUserByPhoneNumber", ctx, "+79161234567").Return(user, nil).Once()
mockRepo.On("SaveOtpRequest", ctx, "test-uuid", "123456").Return(nil).Once()
},
wantErr: false,
},
{
name: "success - create new user",
setupMock: func() {
newUser := &userDomain.User{
Uuid: "new-uuid",
PhoneNumber: "+79161234567",
}
mockUserService.On("GetUserByPhoneNumber", ctx, "+79161234567").Return(nil, nil).Once()
mockUserService.On("CreateUser", ctx, "+79161234567").Return(newUser, nil).Once()
mockRepo.On("SaveOtpRequest", ctx, "new-uuid", "123456").Return(nil).Once()
},
wantErr: false,
},
{
name: "invalid phone number",
setupMock: func() {
// No mock setup needed for invalid phone
},
wantErr: true,
errType: domain.ErrInvalidPhoneNumber{},
},
{
name: "repo save error",
setupMock: func() {
user := &userDomain.User{
Uuid: "test-uuid",
PhoneNumber: "+79161234567",
}
mockUserService.On("GetUserByPhoneNumber", ctx, "+79161234567").Return(user, nil).Once()
mockRepo.On("SaveOtpRequest", ctx, "test-uuid", "123456").Return(assert.AnError).Once()
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Reset mocks
mockRepo.ExpectedCalls = nil
mockUserService.ExpectedCalls = nil
tt.setupMock()
testPhone := phoneNumber
if tt.name == "invalid phone number" {
testPhone = "invalid"
}
err := service.OtpRequest(ctx, testPhone)
if tt.wantErr {
assert.Error(t, err)
if tt.errType != nil {
assert.IsType(t, tt.errType, err)
}
} else {
assert.NoError(t, err)
}
mockRepo.AssertExpectations(t)
mockUserService.AssertExpectations(t)
})
}
}
// Helper function to create string pointer
func stringPtr(s string) *string {
return &s
}