package handler import ( "baron-sso-backend/internal/domain" "bytes" "context" "encoding/json" "io" "net/http" "slices" "time" ) // --- Mock IDP Provider --- type mockIdpProvider struct { userExists bool name string signInInfo *domain.AuthInfo issueSession *domain.AuthInfo verifyCodeInfo *domain.AuthInfo err error initiateLinkErr error updateCalled bool updateCallCount int updatedLoginID string updatedPassword string } func (m *mockIdpProvider) Name() string { if m.name != "" { return m.name } return "mock-idp" } func (m *mockIdpProvider) GetMetadata() (*domain.IDPMetadata, error) { return nil, m.err } func (m *mockIdpProvider) CreateUser(user *domain.BrokerUser, password string) (string, error) { return "mock-user-id", m.err } func (m *mockIdpProvider) SignIn(loginID, password string) (*domain.AuthInfo, error) { return m.signInInfo, m.err } func (m *mockIdpProvider) UserExists(loginID string) (bool, error) { return m.userExists, m.err } func (m *mockIdpProvider) IssueSession(loginID string) (*domain.AuthInfo, error) { if m.issueSession != nil { return m.issueSession, m.err } return &domain.AuthInfo{ SessionToken: &domain.Token{JWT: "valid-jwt", SessionID: "valid-sid"}, }, m.err } func (m *mockIdpProvider) InitiateLinkLogin(loginID, returnTo string) (*domain.LinkLoginInit, error) { if m.initiateLinkErr != nil { return nil, m.initiateLinkErr } return &domain.LinkLoginInit{FlowID: "mock-flow-id", Mode: "code"}, m.err } func (m *mockIdpProvider) VerifyLoginCode(loginID, flowID, code string) (*domain.AuthInfo, error) { return m.verifyCodeInfo, m.err } func (m *mockIdpProvider) GetPasswordPolicy() (*domain.PasswordPolicy, error) { return nil, m.err } func (m *mockIdpProvider) InitiatePasswordReset(loginID, redirectUrl string) error { return m.err } func (m *mockIdpProvider) VerifyPasswordResetToken(token string) (*domain.AuthInfo, error) { return nil, m.err } func (m *mockIdpProvider) UpdateUserPassword(loginID, newPassword string, r *http.Request) error { m.updateCalled = true m.updateCallCount++ m.updatedLoginID = loginID m.updatedPassword = newPassword return m.err } // --- Mock Audit Repository --- type mockAuditRepo struct { logs []domain.AuditLog } func (m *mockAuditRepo) Create(log *domain.AuditLog) error { m.logs = append(m.logs, *log) return nil } func (m *mockAuditRepo) FindPage(ctx context.Context, limit int, cursor *domain.AuditCursor, tenantID string) ([]domain.AuditLog, error) { return m.logs, nil } func (m *mockAuditRepo) FindByUserAndEvents(ctx context.Context, userID string, eventTypes []string, limit int) ([]domain.AuditLog, error) { var results []domain.AuditLog for _, log := range m.logs { if log.UserID == userID { if slices.Contains(eventTypes, log.EventType) { results = append(results, log) } } } return results, nil } func (m *mockAuditRepo) CountFailuresSince(ctx context.Context, since time.Time, tenantID string) (int64, error) { return 0, nil } func (m *mockAuditRepo) CountEventsSince(ctx context.Context, since time.Time) (int64, error) { return 0, nil } func (m *mockAuditRepo) CountActiveSessionsSince(ctx context.Context, since time.Time, tenantID string) (int64, error) { return 0, nil } func (m *mockAuditRepo) Ping(ctx context.Context) error { return nil } type mockRPUsageEventSink struct { events []domain.RPUsageEvent err error } func (m *mockRPUsageEventSink) EmitRPUsageEvent(ctx context.Context, event domain.RPUsageEvent) error { if m.err != nil { return m.err } m.events = append(m.events, event) return nil } type mockOathkeeperRepo struct { logs []domain.OathkeeperAccessLog } func (m *mockOathkeeperRepo) FindPageBySubject(ctx context.Context, subject string, limit int, cursor *domain.AuditCursor) ([]domain.OathkeeperAccessLog, error) { if subject == "" { return m.logs, nil } results := make([]domain.OathkeeperAccessLog, 0, len(m.logs)) for _, log := range m.logs { if log.Subject == subject { results = append(results, log) } } return results, nil } func (m *mockOathkeeperRepo) Ping(ctx context.Context) error { return nil } // --- Mock Consent Repository --- type mockConsentRepo struct { consents []domain.ClientConsent } func (m *mockConsentRepo) Upsert(ctx context.Context, consent *domain.ClientConsent) error { m.consents = append(m.consents, *consent) return nil } func (m *mockConsentRepo) ListBySubject(ctx context.Context, subject string) ([]domain.ClientConsent, error) { var results []domain.ClientConsent for _, c := range m.consents { if c.Subject == subject { results = append(results, c) } } return results, nil } func (m *mockConsentRepo) ListSubjectsByClient(ctx context.Context, clientID string) ([]string, error) { seen := map[string]struct{}{} subjects := make([]string, 0, len(m.consents)) for _, consent := range m.consents { if consent.ClientID != clientID { continue } if _, ok := seen[consent.Subject]; ok { continue } seen[consent.Subject] = struct{}{} subjects = append(subjects, consent.Subject) } return subjects, nil } func (m *mockConsentRepo) Find(ctx context.Context, clientID, subject string) (*domain.ClientConsent, error) { for _, consent := range m.consents { if consent.ClientID == clientID && consent.Subject == subject { found := consent return &found, nil } } return nil, nil } func (m *mockConsentRepo) Delete(ctx context.Context, subject, clientID string) error { filtered := m.consents[:0] for _, consent := range m.consents { if consent.Subject == subject && (clientID == "" || consent.ClientID == clientID) { continue } filtered = append(filtered, consent) } m.consents = filtered return nil } func (m *mockConsentRepo) DeleteByClient(ctx context.Context, clientID string) error { filtered := m.consents[:0] for _, consent := range m.consents { if consent.ClientID != clientID { filtered = append(filtered, consent) } } m.consents = filtered return nil } func (m *mockConsentRepo) List(ctx context.Context, clientID string, limit, offset int) ([]domain.ClientConsentWithTenantInfo, int64, error) { results := make([]domain.ClientConsentWithTenantInfo, 0, len(m.consents)) for _, consent := range m.consents { if consent.ClientID == clientID { results = append(results, domain.ClientConsentWithTenantInfo{ClientConsent: consent}) } } return results, int64(len(results)), nil } func (m *mockConsentRepo) ListByTenant(ctx context.Context, clientID, tenantID string, limit, offset int) ([]domain.ClientConsentWithTenantInfo, int64, error) { results := make([]domain.ClientConsentWithTenantInfo, 0, len(m.consents)) for _, consent := range m.consents { if consent.ClientID == clientID { results = append(results, domain.ClientConsentWithTenantInfo{ ClientConsent: consent, TenantID: tenantID, }) } } return results, int64(len(results)), nil } // --- Mock Secret Repository --- type mockSecretRepo struct { secrets map[string]string } func (m *mockSecretRepo) Upsert(ctx context.Context, clientID, secret string) error { if m.secrets == nil { m.secrets = make(map[string]string) } m.secrets[clientID] = secret return nil } func (m *mockSecretRepo) GetByID(ctx context.Context, clientID string) (string, error) { return m.secrets[clientID], nil } func (m *mockSecretRepo) Delete(ctx context.Context, clientID string) error { delete(m.secrets, clientID) return nil } // --- HTTP Mock Helpers --- type roundTripFunc func(req *http.Request) (*http.Response, error) func (f roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) { return f(req) } func setDefaultHTTPClientForTest(t interface{ Cleanup(func()) }, transport http.RoundTripper) { origDefault := http.DefaultClient http.DefaultClient = &http.Client{Transport: transport} t.Cleanup(func() { http.DefaultClient = origDefault }) } func httpResponse(r *http.Request, code int, body string) *http.Response { return &http.Response{ StatusCode: code, Header: make(http.Header), Body: io.NopCloser(bytes.NewBufferString(body)), Request: r, } } func httpJSONAny(r *http.Request, code int, data any) *http.Response { body, _ := json.Marshal(data) return &http.Response{ StatusCode: code, Header: http.Header{ "Content-Type": []string{"application/json"}, }, Body: io.NopCloser(bytes.NewBuffer(body)), Request: r, } }