package keycloak import ( "context" "errors" "sync" ) // Mock is the test-friendly Adapter. Records every call; predictable IDs. // Use in unit tests + as the default adapter when KEYCLOAK_BASE_URL is empty // (dev convenience). type Mock struct { mu sync.Mutex Orgs map[string]string // tenantID → orgID Users map[string]string // email → userID Claims map[string]Claims // userID → last synced FailNext error // set to force the next call to fail } func NewMock() *Mock { return &Mock{ Orgs: map[string]string{}, Users: map[string]string{}, Claims: map[string]Claims{}, } } func (m *Mock) Health(_ context.Context) error { return nil } func (m *Mock) CreateOrgAndInvite(_ context.Context, in InviteInput) (*InviteResult, error) { m.mu.Lock() defer m.mu.Unlock() if m.FailNext != nil { err := m.FailNext m.FailNext = nil return nil, err } if _, taken := m.Orgs[in.TenantID]; taken { return nil, ErrOrgConflict } if _, taken := m.Users[in.AdminEmail]; taken { return nil, ErrUserConflict } orgID := "mock-org-" + in.Slug userID := "mock-user-" + in.AdminEmail m.Orgs[in.TenantID] = orgID m.Users[in.AdminEmail] = userID return &InviteResult{ OrganizationID: orgID, UserID: userID, InviteURL: "http://mock-keycloak/invite/" + userID, }, nil } func (m *Mock) SyncClaims(_ context.Context, userID string, c Claims) error { m.mu.Lock() defer m.mu.Unlock() if m.FailNext != nil { err := m.FailNext m.FailNext = nil return err } if userID == "" { return errors.New("mock: user_id required") } m.Claims[userID] = c return nil }