package handler import ( "baron-sso-backend/internal/domain" "baron-sso-backend/internal/service" "bytes" "context" "encoding/json" "errors" "net/http" "net/http/httptest" "testing" "time" "github.com/gofiber/fiber/v2" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) // --- Async Test Mocks --- type AsyncMockIdpProvider struct { mock.Mock } func (m *AsyncMockIdpProvider) Name() string { return "mock-idp" } func (m *AsyncMockIdpProvider) GetMetadata() (*domain.IDPMetadata, error) { return &domain.IDPMetadata{}, nil } func (m *AsyncMockIdpProvider) UserExists(loginID string) (bool, error) { args := m.Called(loginID) return args.Bool(0), args.Error(1) } func (m *AsyncMockIdpProvider) CreateUser(user *domain.BrokerUser, password string) (string, error) { args := m.Called(user, password) return args.String(0), args.Error(1) } func (m *AsyncMockIdpProvider) SignIn(loginID, password string) (*domain.AuthInfo, error) { return nil, nil } func (m *AsyncMockIdpProvider) IssueSession(loginID string) (*domain.AuthInfo, error) { return nil, nil } func (m *AsyncMockIdpProvider) InitiateLinkLogin(loginID, returnTo string) (*domain.LinkLoginInit, error) { return nil, nil } func (m *AsyncMockIdpProvider) VerifyLoginCode(loginID, flowID, code string) (*domain.AuthInfo, error) { return nil, nil } func (m *AsyncMockIdpProvider) GetPasswordPolicy() (*domain.PasswordPolicy, error) { return &domain.PasswordPolicy{MinLength: 12}, nil } func (m *AsyncMockIdpProvider) InitiatePasswordReset(loginID, redirectUrl string) error { return nil } func (m *AsyncMockIdpProvider) VerifyPasswordResetToken(token string) (*domain.AuthInfo, error) { return nil, nil } func (m *AsyncMockIdpProvider) UpdateUserPassword(loginID, newPassword string, r *http.Request) error { return nil } type AsyncMockUserRepo struct { mock.Mock createCalled chan bool } func (m *AsyncMockUserRepo) Create(ctx context.Context, user *domain.User) error { // Simulate DB latency time.Sleep(50 * time.Millisecond) args := m.Called(ctx, user) if m.createCalled != nil { m.createCalled <- true } return args.Error(0) } func (m *AsyncMockUserRepo) Update(ctx context.Context, user *domain.User) error { return nil } func (m *AsyncMockUserRepo) Delete(ctx context.Context, id string) error { return nil } func (m *AsyncMockUserRepo) FindByEmail(ctx context.Context, email string) (*domain.User, error) { return nil, nil } func (m *AsyncMockUserRepo) FindByID(ctx context.Context, id string) (*domain.User, error) { return nil, nil } func (m *AsyncMockUserRepo) FindByIDs(ctx context.Context, ids []string) ([]domain.User, error) { return nil, nil } func (m *AsyncMockUserRepo) ListByTenant(ctx context.Context, tenantID string) ([]domain.User, error) { return nil, nil } func (m *AsyncMockUserRepo) List(ctx context.Context, offset, limit int, search string) ([]domain.User, int64, error) { return nil, 0, nil } type AsyncMockRedisRepo struct { mock.Mock } func (m *AsyncMockRedisRepo) Set(key string, value string, expiration time.Duration) error { args := m.Called(key, value, expiration) return args.Error(0) } func (m *AsyncMockRedisRepo) Get(key string) (string, error) { args := m.Called(key) return args.String(0), args.Error(1) } func (m *AsyncMockRedisRepo) Delete(key string) error { args := m.Called(key) return args.Error(0) } func (m *AsyncMockRedisRepo) StoreVerificationCode(phone, code string) error { return nil } func (m *AsyncMockRedisRepo) GetVerificationCode(phone string) (string, error) { return "", nil } func (m *AsyncMockRedisRepo) DeleteVerificationCode(phone string) error { return nil } type AsyncMockTenantService struct { mock.Mock } func (m *AsyncMockTenantService) RegisterTenant(ctx context.Context, name, slug, description string, domains []string, parentID *string) (*domain.Tenant, error) { return nil, nil } func (m *AsyncMockTenantService) RequestRegistration(ctx context.Context, name, slug, description string, domainName string, adminEmail string) (*domain.Tenant, error) { return nil, nil } func (m *AsyncMockTenantService) GetTenantByDomain(ctx context.Context, emailDomain string) (*domain.Tenant, error) { args := m.Called(ctx, emailDomain) if args.Get(0) == nil { return nil, args.Error(1) } return args.Get(0).(*domain.Tenant), args.Error(1) } func (m *AsyncMockTenantService) GetTenantBySlug(ctx context.Context, slug string) (*domain.Tenant, error) { return nil, nil } func (m *AsyncMockTenantService) GetTenant(ctx context.Context, id string) (*domain.Tenant, error) { return nil, nil } func (m *AsyncMockTenantService) ListManageableTenants(ctx context.Context, userID string) ([]domain.Tenant, error) { return nil, nil } func (m *AsyncMockTenantService) ApproveTenant(ctx context.Context, id string) error { return nil } func (m *AsyncMockTenantService) SetKetoService(keto service.KetoService) {} func (m *AsyncMockTenantService) AddTenantAdmin(ctx context.Context, tenantID, userID string) error { return nil } func (m *AsyncMockTenantService) RemoveTenantAdmin(ctx context.Context, tenantID, userID string) error { return nil } func (m *AsyncMockTenantService) ListTenantAdmins(ctx context.Context, tenantID string) ([]string, error) { return nil, nil } type AsyncMockKetoService struct { mock.Mock } func (m *AsyncMockKetoService) CreateRelation(ctx context.Context, namespace, object, relation, subject string) error { args := m.Called(ctx, namespace, object, relation, subject) return args.Error(0) } func (m *AsyncMockKetoService) DeleteRelation(ctx context.Context, namespace, object, relation, subject string) error { return nil } func (m *AsyncMockKetoService) CheckPermission(ctx context.Context, namespace, object, relation, subject string) (bool, error) { return false, nil } func (m *AsyncMockKetoService) ListObjects(ctx context.Context, namespace, relation, subject string) ([]string, error) { return nil, nil } func (m *AsyncMockKetoService) ListRelations(ctx context.Context, namespace, object, relation, subject string) ([]service.RelationTuple, error) { return nil, nil } // --- Tests --- func TestSignup_AsyncDB_Isolation(t *testing.T) { mockIdp := new(AsyncMockIdpProvider) mockUserRepo := new(AsyncMockUserRepo) mockRedis := new(AsyncMockRedisRepo) mockTenant := new(AsyncMockTenantService) mockKeto := new(AsyncMockKetoService) h := &AuthHandler{ IdpProvider: mockIdp, UserRepo: mockUserRepo, RedisService: mockRedis, TenantService: mockTenant, KetoService: mockKeto, } app := fiber.New() app.Post("/signup", h.Signup) t.Run("SoT_DB_Failure_Ignored_And_Async", func(t *testing.T) { email := "test@example.com" phone := "010-1234-5678" emailKey := "signup:email:" + email phoneKey := "signup:phone:" + "01012345678" // Redis Mocks mockRedis.On("Get", emailKey).Return(`{"verified": true, "expires_at": 9999999999}`, nil) mockRedis.On("Get", phoneKey).Return(`{"verified": true, "expires_at": 9999999999}`, nil) mockRedis.On("Delete", emailKey).Return(nil) mockRedis.On("Delete", phoneKey).Return(nil) // Tenant Mocks mockTenant.On("GetTenantByDomain", mock.Anything, "example.com").Return(nil, errors.New("not found")) // Kratos Mocks (Success) mockIdp.On("CreateUser", mock.Anything, "Password123!").Return("new-user-uuid", nil) // UserRepo Mocks (Async & Failure) mockUserRepo.createCalled = make(chan bool, 1) mockUserRepo.On("Create", mock.Anything, mock.MatchedBy(func(u *domain.User) bool { return u.Email == email })).Return(errors.New("db connection error")) // Keto Mocks (Optional, since it's also async) // We won't block on this either body, _ := json.Marshal(domain.SignupRequest{ Email: email, Password: "Password123!", Name: "Test User", Phone: phone, TermsAccepted: true, }) req := httptest.NewRequest("POST", "/signup", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") start := time.Now() resp, err := app.Test(req, 5000) elapsed := time.Since(start) if err != nil { t.Fatalf("Request failed: %v", err) } assert.Equal(t, 200, resp.StatusCode) // Ensure API responded faster than DB latency (50ms) assert.Less(t, int64(elapsed), int64(60*time.Millisecond), "API should return before DB timeout") // Wait for async execution select { case <-mockUserRepo.createCalled: // Pass case <-time.After(2 * time.Second): t.Fatal("UserRepo.Create was not called asynchronously") } mockRedis.AssertExpectations(t) mockIdp.AssertExpectations(t) mockUserRepo.AssertExpectations(t) }) }