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 }