import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' const { closeClientMock, createApiKeyMock, getUserByIdMock, getPgClientMock, pgConnectMock, pgQueryMock, pgReleaseMock, sendEventToTrackingMock, syncNewUserABTestsMock, syncBentoSubscriberTagsMock, syncUserPreferenceTagsMock, trackBentoEventMock, unsubscribeBentoMock, } = vi.hoisted(() => { const pgQueryMock = vi.fn<(query: string, params?: unknown[]) => Promise<{ rows: Record[] }>>(async () => ({ rows: [] })) const pgReleaseMock = vi.fn<(destroy?: Error | boolean) => void>(() => undefined) const pgConnectMock = vi.fn(async () => ({ query: pgQueryMock, release: pgReleaseMock })) const getUserByIdMock = vi.fn<(userId: string) => Promise<{ data: { user: { app_metadata?: Record | null, user_metadata: Record | null } | null } error: Error | null }>>(async () => ({ data: { user: { app_metadata: {}, user_metadata: {} } }, error: null })) return { closeClientMock: vi.fn(async () => undefined), createApiKeyMock: vi.fn(async () => undefined), getUserByIdMock, getPgClientMock: vi.fn(() => ({ connect: pgConnectMock, query: pgQueryMock })), pgConnectMock, pgQueryMock, pgReleaseMock, sendEventToTrackingMock: vi.fn(async () => undefined), syncNewUserABTestsMock: vi.fn(async () => undefined), syncBentoSubscriberTagsMock: vi.fn<( c: unknown, update: { deleteSegments: string[], email: string, segments: string[] } | Array<{ deleteSegments: string[], email: string, segments: string[] }>, signal?: AbortSignal, ) => Promise>(async () => true), syncUserPreferenceTagsMock: vi.fn(async () => undefined), trackBentoEventMock: vi.fn<( c: unknown, email: string, data: Record, event: string, ) => Promise>(async () => true), unsubscribeBentoMock: vi.fn<( c: unknown, email: string, signal?: AbortSignal, ) => Promise>(async () => true), } }) vi.mock('../supabase/functions/_backend/utils/ab_tests.ts', () => ({ syncNewUserABTests: syncNewUserABTestsMock, })) const bentoState = vi.hoisted(() => ({ configured: true, plansLookups: 0 })) vi.mock('../supabase/functions/_backend/utils/bento.ts', () => ({ isBentoConfigured: () => bentoState.configured, syncBentoSubscriberTags: syncBentoSubscriberTagsMock, trackBentoEvent: trackBentoEventMock, unsubscribeBento: unsubscribeBentoMock, })) vi.mock('../supabase/functions/_backend/utils/hono.ts', async () => { const actual = await vi.importActual('../supabase/functions/_backend/utils/hono.ts') return { ...actual, middlewareAPISecret: async (_c: unknown, next: () => Promise) => await next(), } }) vi.mock('../supabase/functions/_backend/utils/pg.ts', async () => ({ ...(await import('./helpers/pg-checkout-release-mocks.ts')), closeClient: closeClientMock, // on_user_create reads trial plan names with its own client; keep that out of the lifecycle query mocks. getPgClient: (...args: unknown[]) => { const client = getPgClientMock(...(args as [])) const query = client.query client.query = vi.fn(async (sql: string, params?: unknown[]) => { if (sql === 'SELECT name FROM public.plans') { bentoState.plansLookups++ return { rows: [{ name: 'Solo' }, { name: 'Team' }] } } return query(sql, params) }) return client }, })) vi.mock('../supabase/functions/_backend/utils/supabase.ts', () => ({ createApiKey: createApiKeyMock, supabaseAdmin: vi.fn(() => ({ auth: { admin: { getUserById: getUserByIdMock } }, })), })) vi.mock('../supabase/functions/_backend/utils/tracking.ts', () => ({ sendEventToTracking: sendEventToTrackingMock, })) vi.mock('../supabase/functions/_backend/utils/user_preferences.ts', () => ({ syncUserPreferenceTags: syncUserPreferenceTagsMock, })) // Model the Edge Runtime binding while executing trigger handlers under Node. beforeEach(() => { vi.stubGlobal('EdgeRuntime', undefined) bentoState.configured = true bentoState.plansLookups = 0 }) afterEach(() => vi.unstubAllGlobals()) const { app: onUserCreateApp } = await import('../supabase/functions/_backend/triggers/on_user_create.ts') const bentoFirstOrgLifecycle = await import('../supabase/functions/_backend/utils/bento_first_org.ts') const USER_ID = '11111111-1111-4111-8111-111111111111' const REGISTERED_AT = '2026-08-03T08:30:00.000Z' const JOINED_AT = '2026-08-03T09:15:00.000Z' const LIFECYCLE_TAG = 'onboarding:awaiting_first_org' const SUPPRESSION_TAG = 'onboarding:first_org_recovery_suppressed' const ORG_ID = '22222222-2222-4222-8222-222222222222' const ROLE_BINDING_ID = '33333333-3333-4333-8333-333333333333' type ExpectedLifecycleModule = typeof bentoFirstOrgLifecycle & { syncBentoFirstOrgOnEmailChange: (c: never, oldEmail: string, newEmail: string) => Promise syncBentoFirstOrgOnRoleBindingWrite: (c: never, roleBindingId: string) => Promise } async function suppressEmailAliases(oldEmail: string, newEmail: string) { return await (bentoFirstOrgLifecycle as ExpectedLifecycleModule) .syncBentoFirstOrgOnEmailChange({ get: vi.fn(() => 'request-id') } as never, oldEmail, newEmail) } async function syncRoleBinding(roleBindingId = ROLE_BINDING_ID) { return await (bentoFirstOrgLifecycle as ExpectedLifecycleModule) .syncBentoFirstOrgOnRoleBindingWrite({ get: vi.fn(() => 'request-id') } as never, roleBindingId) } function activeBinding(overrides: Record = {}) { return { email: ' Current.User@Example.COM ', granted_at: new Date(JOINED_AT), id: ROLE_BINDING_ID, is_active: true, is_direct: true, org_id: ORG_ID, principal_id: USER_ID, principal_type: 'user', scope_type: 'org', ...overrides, } } function firstOrgDatabaseState(hasActiveDirectOrgAccess = false, userIsRecoveryEligible = true) { return { has_active_direct_org_access: hasActiveDirectOrgAccess, user_is_recovery_eligible: userIsRecoveryEligible, } } function userRecord(overrides: Record = {}) { return { ban_time: null, country: null, created_at: REGISTERED_AT, created_via_invite: false, discord_username: null, email: ' New.User@Example.COM ', email_preferences: {}, enable_notifications: false, first_name: 'New', format_locale: null, github_id: null, github_username: null, id: USER_ID, image_url: null, last_name: 'User', onboarding: {}, opt_for_newsletters: false, updated_at: REGISTERED_AT, ...overrides, } } function mockRegistrationMetadata(user_metadata: Record | null, app_metadata: Record = {}) { getUserByIdMock.mockResolvedValue({ data: { user: { app_metadata, user_metadata } }, error: null }) } async function postUser(record = userRecord()) { return await onUserCreateApp.request('http://local/', { body: JSON.stringify({ old_record: null, record, schema: 'public', table: 'users', type: 'INSERT', }), headers: { 'content-type': 'application/json' }, method: 'POST', }) } describe('first-organization lifecycle on user registration', () => { beforeEach(() => { vi.clearAllMocks() pgConnectMock.mockImplementation(async () => ({ query: pgQueryMock, release: pgReleaseMock })) pgReleaseMock.mockImplementation(() => undefined) closeClientMock.mockResolvedValue(undefined) mockRegistrationMetadata({}) getPgClientMock.mockImplementation(() => ({ connect: pgConnectMock, query: pgQueryMock })) pgQueryMock.mockResolvedValue({ rows: [firstOrgDatabaseState()] }) syncBentoSubscriberTagsMock.mockResolvedValue(true) trackBentoEventMock.mockResolvedValue(true) unsubscribeBentoMock.mockResolvedValue(true) }) afterEach(() => { vi.restoreAllMocks() }) it('adds the lifecycle tag and emits the entry event when no active org access exists', async () => { const response = await postUser() expect(response.status).toBe(200) expect(syncBentoSubscriberTagsMock).toHaveBeenCalledWith(expect.anything(), { deleteSegments: [], email: 'new.user@example.com', segments: [LIFECYCLE_TAG], }) expect(syncUserPreferenceTagsMock).toHaveBeenCalledWith( expect.anything(), 'new.user@example.com', expect.objectContaining({ id: USER_ID }), ) expect(syncNewUserABTestsMock).toHaveBeenCalledWith( expect.anything(), 'new.user@example.com', expect.objectContaining({ id: USER_ID }), ) expect(createApiKeyMock.mock.invocationCallOrder[0]) .toBeLessThan(syncNewUserABTestsMock.mock.invocationCallOrder[0]!) expect(trackBentoEventMock).toHaveBeenCalledWith( expect.anything(), 'new.user@example.com', { created_via_invite: false, registered_at: REGISTERED_AT, user_id: USER_ID, }, 'user:registered_without_org', ) expect(pgQueryMock).toHaveBeenCalledTimes(4) }) it.each([ ['mobile', true], ['tablet', true], ['desktop', false], ['unknown', false], [undefined, false], ])('emits the mobile registration event for %s device metadata', async (deviceType, shouldEmit) => { mockRegistrationMetadata(deviceType === undefined ? null : { registration_browser: 'Mobile Safari', registration_device_type: deviceType, registration_os: 'iOS', }) const response = await postUser() const mobileEvents = trackBentoEventMock.mock.calls.filter(call => call[3] === 'user:registered_from_mobile') expect(response.status).toBe(200) expect(mobileEvents).toHaveLength(shouldEmit ? 1 : 0) if (shouldEmit) { expect(mobileEvents[0]).toEqual([expect.anything(), 'new.user@example.com', { registered_at: REGISTERED_AT, registration_browser: 'Mobile Safari', registration_device_type: deviceType, registration_os: 'iOS', user_id: USER_ID, }, 'user:registered_from_mobile']) } }) it('tags direct-offer signups offer=direct in Bento and removes trial tags', async () => { mockRegistrationMetadata({}, { offer: 'direct' }) const response = await postUser() expect(response.status).toBe(200) expect(syncBentoSubscriberTagsMock).toHaveBeenCalledWith(expect.anything(), { deleteSegments: ['trial', 'trial7', 'trial1', 'trial0', 'trial-card-on-file', 'trial-plan:Solo', 'trial-plan:Team'], email: 'new.user@example.com', segments: ['offer=direct'], }) }) it('skips the plans lookup for direct-offer signups when Bento is disabled', async () => { bentoState.configured = false mockRegistrationMetadata({}, { offer: 'direct' }) const response = await postUser() expect(response.status).toBe(200) expect(bentoState.plansLookups).toBe(0) }) it('ignores a user-writable offer in user metadata', async () => { mockRegistrationMetadata({ offer: 'direct' }) const response = await postUser() const sentSegments = syncBentoSubscriberTagsMock.mock.calls.flatMap(call => Array.isArray(call[1]) ? call[1].flatMap(item => item.segments) : call[1].segments) expect(response.status).toBe(200) expect(sentSegments).not.toContain('offer=direct') }) it('does not add the offer tag for standard signups', async () => { mockRegistrationMetadata({}, { offer: 'Direct' }) const response = await postUser() const sentSegments = syncBentoSubscriberTagsMock.mock.calls.flatMap(call => Array.isArray(call[1]) ? call[1].flatMap(item => item.segments) : call[1].segments) expect(response.status).toBe(200) expect(sentSegments).not.toContain('offer=direct') }) it('fails for retry when Bento rejects the mobile registration event', async () => { mockRegistrationMetadata({ registration_device_type: 'mobile' }) trackBentoEventMock.mockResolvedValueOnce(true).mockResolvedValueOnce(false) const response = await postUser() trackBentoEventMock.mockReset() trackBentoEventMock.mockResolvedValue(true) expect(response.status).toBe(500) }) it('fails before provisioning when the Auth metadata lookup fails', async () => { getUserByIdMock.mockResolvedValueOnce({ data: { user: null }, error: new Error('Auth unavailable') }) const response = await postUser() expect(response.status).toBe(500) expect(createApiKeyMock).not.toHaveBeenCalled() }) it('returns a retryable failure when default API-key provisioning times out on a lock', async () => { createApiKeyMock.mockRejectedValueOnce(Object.assign(new Error('lock timeout'), { code: '55P03' })) const response = await postUser() expect(response.status).toBe(500) expect(createApiKeyMock).toHaveBeenCalledOnce() expect(syncUserPreferenceTagsMock).not.toHaveBeenCalled() expect(syncBentoSubscriberTagsMock).not.toHaveBeenCalled() expect(trackBentoEventMock).not.toHaveBeenCalled() }) it('destroys each checked-out Workerd client before the following Bento request', async () => { const lifecycleTrace: string[] = [] let queryNumber = 0 pgQueryMock.mockImplementation(async () => { lifecycleTrace.push(`query:${++queryNumber}`) return { rows: [firstOrgDatabaseState()] } }) // Workerd closeClient is intentionally a no-op. This test must prove the // checked-out clients are released without relying on pool shutdown. closeClientMock.mockImplementation(async () => undefined) pgReleaseMock.mockImplementation((destroy) => { lifecycleTrace.push(`client:released:${destroy}`) }) syncBentoSubscriberTagsMock.mockImplementation(async () => { lifecycleTrace.push('tag:add') return true }) trackBentoEventMock.mockImplementation(async () => { lifecycleTrace.push('event:entry') return true }) const response = await postUser() expect(response.status).toBe(200) expect(lifecycleTrace).toEqual([ 'query:1', 'client:released:true', 'query:2', 'client:released:true', 'tag:add', 'query:3', 'client:released:true', 'event:entry', 'query:4', 'client:released:true', ]) expect(getPgClientMock).toHaveBeenCalledTimes(2) expect(pgConnectMock).toHaveBeenCalledTimes(4) expect(pgReleaseMock).toHaveBeenCalledTimes(4) expect(pgReleaseMock).toHaveBeenNthCalledWith(1, true) expect(pgReleaseMock).toHaveBeenNthCalledWith(2, true) expect(pgReleaseMock).toHaveBeenNthCalledWith(3, true) expect(pgReleaseMock).toHaveBeenNthCalledWith(4, true) expect(closeClientMock).toHaveBeenCalledTimes(2) }) it('skips entry and permanently suppresses invite-created profiles', async () => { const response = await postUser(userRecord({ created_via_invite: true })) expect(response.status).toBe(200) expect(syncBentoSubscriberTagsMock).toHaveBeenCalledWith(expect.anything(), { deleteSegments: [LIFECYCLE_TAG], email: 'new.user@example.com', segments: [SUPPRESSION_TAG], }) expect(trackBentoEventMock).not.toHaveBeenCalled() expect(pgQueryMock).toHaveBeenCalledTimes(3) }) it.each([ ['invite-created profile', userRecord({ created_via_invite: true }), firstOrgDatabaseState()], ['existing organization member', userRecord(), firstOrgDatabaseState(true)], ])('suppresses a %s when deletion appears after terminal tag removal', async (_label, record, branchState) => { pgQueryMock .mockResolvedValueOnce({ rows: [firstOrgDatabaseState()] }) .mockResolvedValueOnce({ rows: [branchState] }) .mockResolvedValueOnce({ rows: [firstOrgDatabaseState(false, false)] }) const response = await postUser(record) expect(response.status).toBe(200) expect(syncBentoSubscriberTagsMock).toHaveBeenNthCalledWith(1, expect.anything(), { deleteSegments: [LIFECYCLE_TAG], email: 'new.user@example.com', segments: [SUPPRESSION_TAG], }) expect(syncBentoSubscriberTagsMock).toHaveBeenNthCalledWith(2, expect.anything(), [{ deleteSegments: [LIFECYCLE_TAG], email: 'new.user@example.com', segments: [SUPPRESSION_TAG], }], expect.any(AbortSignal)) expect(trackBentoEventMock).not.toHaveBeenCalled() expect(unsubscribeBentoMock).toHaveBeenCalledWith(expect.anything(), 'new.user@example.com', expect.any(AbortSignal)) }) it('suppresses an invite-created profile when deletion is scheduled during provisioning', async () => { pgQueryMock .mockResolvedValueOnce({ rows: [firstOrgDatabaseState()] }) .mockResolvedValueOnce({ rows: [firstOrgDatabaseState(false, false)] }) const response = await postUser(userRecord({ created_via_invite: true })) expect(response.status).toBe(200) expect(createApiKeyMock).toHaveBeenCalledOnce() expect(syncUserPreferenceTagsMock).toHaveBeenCalledOnce() expect(syncBentoSubscriberTagsMock).toHaveBeenCalledOnce() expect(syncBentoSubscriberTagsMock).toHaveBeenCalledWith(expect.anything(), [{ deleteSegments: [LIFECYCLE_TAG], email: 'new.user@example.com', segments: [SUPPRESSION_TAG], }], expect.any(AbortSignal)) expect(unsubscribeBentoMock).toHaveBeenCalledWith(expect.anything(), 'new.user@example.com', expect.any(AbortSignal)) expect(trackBentoEventMock).not.toHaveBeenCalled() }) it('permanently suppresses recovery when a delayed create message finds deletion scheduled', async () => { pgQueryMock.mockResolvedValue({ rows: [firstOrgDatabaseState(false, false)] }) const response = await postUser() expect(response.status).toBe(200) expect(syncBentoSubscriberTagsMock).toHaveBeenCalledWith(expect.anything(), [{ deleteSegments: [LIFECYCLE_TAG], email: 'new.user@example.com', segments: [SUPPRESSION_TAG], }], expect.any(AbortSignal)) expect(trackBentoEventMock).not.toHaveBeenCalled() expect(pgQueryMock).toHaveBeenCalledOnce() expect(createApiKeyMock).not.toHaveBeenCalled() expect(syncNewUserABTestsMock).not.toHaveBeenCalled() expect(syncUserPreferenceTagsMock).not.toHaveBeenCalled() expect(unsubscribeBentoMock).toHaveBeenCalledWith(expect.anything(), 'new.user@example.com', expect.any(AbortSignal)) expect(syncBentoSubscriberTagsMock.mock.invocationCallOrder[0]) .toBeLessThan(unsubscribeBentoMock.mock.invocationCallOrder[0]) }) it('still attempts the final unsubscribe when deletion suppression throws', async () => { pgQueryMock.mockResolvedValue({ rows: [firstOrgDatabaseState(false, false)] }) syncBentoSubscriberTagsMock.mockRejectedValueOnce(new Error('Bento unavailable')) const response = await postUser() expect(response.status).toBe(500) expect(unsubscribeBentoMock).toHaveBeenCalledWith(expect.anything(), 'new.user@example.com', expect.any(AbortSignal)) expect(createApiKeyMock).not.toHaveBeenCalled() expect(syncUserPreferenceTagsMock).not.toHaveBeenCalled() }) it('skips entry and permanently suppresses a user with active direct org access', async () => { pgQueryMock.mockResolvedValue({ rows: [firstOrgDatabaseState(true)] }) const response = await postUser() expect(response.status).toBe(200) expect(syncBentoSubscriberTagsMock).toHaveBeenCalledWith(expect.anything(), { deleteSegments: [LIFECYCLE_TAG], email: 'new.user@example.com', segments: [SUPPRESSION_TAG], }) expect(trackBentoEventMock).not.toHaveBeenCalled() expect(pgQueryMock).toHaveBeenCalledTimes(3) }) it('suppresses an existing organization member when deletion is scheduled during provisioning', async () => { pgQueryMock .mockResolvedValueOnce({ rows: [firstOrgDatabaseState(true)] }) .mockResolvedValueOnce({ rows: [firstOrgDatabaseState(false, false)] }) const response = await postUser() expect(response.status).toBe(200) expect(createApiKeyMock).toHaveBeenCalledOnce() expect(syncUserPreferenceTagsMock).toHaveBeenCalledOnce() expect(syncBentoSubscriberTagsMock).toHaveBeenCalledOnce() expect(syncBentoSubscriberTagsMock).toHaveBeenCalledWith(expect.anything(), [{ deleteSegments: [LIFECYCLE_TAG], email: 'new.user@example.com', segments: [SUPPRESSION_TAG], }], expect.any(AbortSignal)) expect(unsubscribeBentoMock).toHaveBeenCalledWith(expect.anything(), 'new.user@example.com', expect.any(AbortSignal)) expect(trackBentoEventMock).not.toHaveBeenCalled() }) it('restricts the active-access check to active direct user-to-org bindings', async () => { const response = await postUser() expect(response.status).toBe(200) expect(syncBentoSubscriberTagsMock).toHaveBeenCalledWith(expect.anything(), { deleteSegments: [], email: 'new.user@example.com', segments: [LIFECYCLE_TAG], }) expect(trackBentoEventMock).toHaveBeenCalledTimes(1) const normalizedQuery = String(pgQueryMock.mock.calls[0]?.[0]).replace(/\s+/g, ' ') expect(normalizedQuery).toContain('principal_type = \'user\'') expect(normalizedQuery).toContain('FROM public.users AS users') expect(normalizedQuery).toContain('FROM public.to_delete_accounts AS deleted') expect(normalizedQuery).toContain('scope_type = \'org\'') expect(normalizedQuery).toContain('is_direct IS TRUE') expect(normalizedQuery).toContain('org_id IS NOT NULL') expect(normalizedQuery).toContain('(expires_at IS NULL OR expires_at > pg_catalog.now())') expect(pgQueryMock.mock.calls[0]?.[1]).toEqual([USER_ID]) }) it('permanently suppresses recovery when org access appears after the tag write', async () => { const lifecycleTrace: string[] = [] pgQueryMock .mockImplementationOnce(async () => { lifecycleTrace.push('query:preflight-no-access') return { rows: [firstOrgDatabaseState()] } }) .mockImplementationOnce(async () => { lifecycleTrace.push('query:post-provision-no-access') return { rows: [firstOrgDatabaseState()] } }) .mockImplementationOnce(async () => { lifecycleTrace.push('query:access-found') return { rows: [firstOrgDatabaseState(true)] } }) .mockImplementationOnce(async () => { lifecycleTrace.push('query:post-remove') return { rows: [firstOrgDatabaseState(true)] } }) syncBentoSubscriberTagsMock.mockImplementation(async (_c, update) => { await Promise.resolve() const updates = Array.isArray(update) ? update : [update] if (updates.some(item => item.segments.includes(LIFECYCLE_TAG))) lifecycleTrace.push('tag:add') if (updates.some(item => item.deleteSegments.includes(LIFECYCLE_TAG))) lifecycleTrace.push('tag:suppress') return true }) // Model Workerd: pool close does nothing, so only explicit client release // can appear before the external Bento operations. closeClientMock.mockImplementation(async () => undefined) pgReleaseMock.mockImplementation((destroy) => { lifecycleTrace.push(`client:released:${destroy}`) }) const response = await postUser() expect(response.status).toBe(200) expect(lifecycleTrace).toEqual([ 'query:preflight-no-access', 'client:released:true', 'query:post-provision-no-access', 'client:released:true', 'tag:add', 'query:access-found', 'client:released:true', 'tag:suppress', 'query:post-remove', 'client:released:true', ]) expect(getPgClientMock).toHaveBeenCalledTimes(2) expect(pgConnectMock).toHaveBeenCalledTimes(4) expect(pgReleaseMock).toHaveBeenNthCalledWith(1, true) expect(pgReleaseMock).toHaveBeenNthCalledWith(2, true) expect(pgReleaseMock).toHaveBeenNthCalledWith(3, true) expect(pgReleaseMock).toHaveBeenNthCalledWith(4, true) expect(closeClientMock).toHaveBeenCalledTimes(2) expect(pgQueryMock).toHaveBeenCalledTimes(4) expect(syncBentoSubscriberTagsMock).toHaveBeenCalledTimes(2) expect(syncBentoSubscriberTagsMock).toHaveBeenNthCalledWith(1, expect.anything(), { deleteSegments: [], email: 'new.user@example.com', segments: [LIFECYCLE_TAG], }) expect(syncBentoSubscriberTagsMock).toHaveBeenNthCalledWith(2, expect.anything(), { deleteSegments: [LIFECYCLE_TAG], email: 'new.user@example.com', segments: [SUPPRESSION_TAG], }) expect(trackBentoEventMock).not.toHaveBeenCalled() }) it('suppresses recovery when deletion is scheduled after the tag write', async () => { pgQueryMock .mockResolvedValueOnce({ rows: [firstOrgDatabaseState()] }) .mockResolvedValueOnce({ rows: [firstOrgDatabaseState()] }) .mockResolvedValueOnce({ rows: [firstOrgDatabaseState(false, false)] }) const response = await postUser() expect(response.status).toBe(200) expect(syncBentoSubscriberTagsMock).toHaveBeenNthCalledWith(1, expect.anything(), { deleteSegments: [], email: 'new.user@example.com', segments: [LIFECYCLE_TAG], }) expect(syncBentoSubscriberTagsMock).toHaveBeenNthCalledWith(2, expect.anything(), [{ deleteSegments: [LIFECYCLE_TAG], email: 'new.user@example.com', segments: [SUPPRESSION_TAG], }], expect.any(AbortSignal)) expect(trackBentoEventMock).not.toHaveBeenCalled() expect(createApiKeyMock).toHaveBeenCalledOnce() expect(syncUserPreferenceTagsMock).toHaveBeenCalledOnce() expect(unsubscribeBentoMock).toHaveBeenCalledWith(expect.anything(), 'new.user@example.com', expect.any(AbortSignal)) }) it('suppresses and unsubscribes when deletion is scheduled after the entry event', async () => { pgQueryMock .mockResolvedValueOnce({ rows: [firstOrgDatabaseState()] }) .mockResolvedValueOnce({ rows: [firstOrgDatabaseState()] }) .mockResolvedValueOnce({ rows: [firstOrgDatabaseState()] }) .mockResolvedValueOnce({ rows: [firstOrgDatabaseState(false, false)] }) const response = await postUser() expect(response.status).toBe(200) expect(trackBentoEventMock).toHaveBeenCalledOnce() expect(syncBentoSubscriberTagsMock).toHaveBeenNthCalledWith(2, expect.anything(), [{ deleteSegments: [LIFECYCLE_TAG], email: 'new.user@example.com', segments: [SUPPRESSION_TAG], }], expect.any(AbortSignal)) expect(trackBentoEventMock.mock.invocationCallOrder[0]) .toBeLessThan(syncBentoSubscriberTagsMock.mock.invocationCallOrder[1]!) expect(unsubscribeBentoMock).toHaveBeenCalledWith(expect.anything(), 'new.user@example.com', expect.any(AbortSignal)) }) it('permanently suppresses recovery when org access appears after the entry event', async () => { pgQueryMock .mockResolvedValueOnce({ rows: [firstOrgDatabaseState()] }) .mockResolvedValueOnce({ rows: [firstOrgDatabaseState()] }) .mockResolvedValueOnce({ rows: [firstOrgDatabaseState()] }) .mockResolvedValueOnce({ rows: [firstOrgDatabaseState(true)] }) .mockResolvedValueOnce({ rows: [firstOrgDatabaseState(true)] }) const response = await postUser() expect(response.status).toBe(200) expect(trackBentoEventMock).toHaveBeenCalledOnce() expect(syncBentoSubscriberTagsMock).toHaveBeenNthCalledWith(2, expect.anything(), { deleteSegments: [LIFECYCLE_TAG], email: 'new.user@example.com', segments: [SUPPRESSION_TAG], }) expect(unsubscribeBentoMock).not.toHaveBeenCalled() }) it.each([ ['returns false', async () => false], ['throws', async () => { throw new Error('Bento unavailable') }], ])('fails the handler for retry when configured Bento %s', async (_label, failure) => { pgQueryMock .mockResolvedValueOnce({ rows: [firstOrgDatabaseState()] }) .mockResolvedValueOnce({ rows: [firstOrgDatabaseState()] }) .mockResolvedValueOnce({ rows: [firstOrgDatabaseState(false, false)] }) syncBentoSubscriberTagsMock.mockImplementationOnce(failure) const response = await postUser() expect(response.status).toBe(500) expect(trackBentoEventMock).not.toHaveBeenCalled() expect(syncBentoSubscriberTagsMock).toHaveBeenNthCalledWith(2, expect.anything(), [{ deleteSegments: [LIFECYCLE_TAG], email: 'new.user@example.com', segments: [SUPPRESSION_TAG], }], expect.any(AbortSignal)) expect(unsubscribeBentoMock).toHaveBeenCalledWith(expect.anything(), 'new.user@example.com', expect.any(AbortSignal)) }) it('fails the handler for retry when the entry event returns false', async () => { pgQueryMock .mockResolvedValueOnce({ rows: [firstOrgDatabaseState()] }) .mockResolvedValueOnce({ rows: [firstOrgDatabaseState()] }) .mockResolvedValueOnce({ rows: [firstOrgDatabaseState()] }) .mockResolvedValueOnce({ rows: [firstOrgDatabaseState(false, false)] }) trackBentoEventMock.mockResolvedValue(false) const response = await postUser() expect(response.status).toBe(500) expect(syncBentoSubscriberTagsMock).toHaveBeenNthCalledWith(2, expect.anything(), [{ deleteSegments: [LIFECYCLE_TAG], email: 'new.user@example.com', segments: [SUPPRESSION_TAG], }], expect.any(AbortSignal)) expect(unsubscribeBentoMock).toHaveBeenCalledWith(expect.anything(), 'new.user@example.com', expect.any(AbortSignal)) }) it('succeeds as a no-op when Bento is not configured', async () => { syncBentoSubscriberTagsMock.mockResolvedValue(undefined) trackBentoEventMock.mockResolvedValue(undefined) const response = await postUser() expect(response.status).toBe(200) expect(trackBentoEventMock).toHaveBeenCalledTimes(1) }) }) describe('first-organization recovery suppression on email changes', () => { beforeEach(() => { vi.clearAllMocks() pgConnectMock.mockImplementation(async () => ({ query: pgQueryMock, release: pgReleaseMock })) pgReleaseMock.mockImplementation(() => undefined) closeClientMock.mockResolvedValue(undefined) getPgClientMock.mockImplementation(() => ({ connect: pgConnectMock, query: pgQueryMock })) pgQueryMock.mockResolvedValue({ rows: [firstOrgDatabaseState()] }) syncBentoSubscriberTagsMock.mockResolvedValue(true) trackBentoEventMock.mockResolvedValue(true) }) afterEach(() => { vi.restoreAllMocks() }) it('suppresses both unique normalized aliases and clears awaiting state in one batch', async () => { await expect(suppressEmailAliases(' Old.User@Example.COM ', ' New.User@Example.COM ')).resolves.toBe(true) expect(syncBentoSubscriberTagsMock).toHaveBeenCalledOnce() expect(syncBentoSubscriberTagsMock).toHaveBeenCalledWith(expect.anything(), [ { deleteSegments: [LIFECYCLE_TAG], email: 'old.user@example.com', segments: [SUPPRESSION_TAG], }, { deleteSegments: [LIFECYCLE_TAG], email: 'new.user@example.com', segments: [SUPPRESSION_TAG], }, ]) }) it('deduplicates the same normalized alias within a batch', async () => { await expect(suppressEmailAliases(' Same.User@Example.COM ', 'same.user@example.com')).resolves.toBe(true) expect(syncBentoSubscriberTagsMock).toHaveBeenCalledWith(expect.anything(), [{ deleteSegments: [LIFECYCLE_TAG], email: 'same.user@example.com', segments: [SUPPRESSION_TAG], }]) }) it('returns false for configured delivery failure and undefined when unconfigured', async () => { syncBentoSubscriberTagsMock.mockResolvedValueOnce(false) await expect(suppressEmailAliases('a@example.com', 'b@example.com')).resolves.toBe(false) syncBentoSubscriberTagsMock.mockResolvedValueOnce(undefined) await expect(suppressEmailAliases('a@example.com', 'b@example.com')).resolves.toBeUndefined() }) it('keeps suppression monotonic across reversed, repeated, and chained alias changes', async () => { await suppressEmailAliases('A@Example.com', 'b@example.com') await suppressEmailAliases('b@example.com', 'A@example.com') await suppressEmailAliases('a@example.com', 'b@example.com') await suppressEmailAliases('b@example.com', 'c@example.com') await suppressEmailAliases('C@example.com', 'c@example.com') expect(syncBentoSubscriberTagsMock).toHaveBeenCalledTimes(5) const expectedAliasBatches = [ ['a@example.com', 'b@example.com'], ['b@example.com', 'a@example.com'], ['a@example.com', 'b@example.com'], ['b@example.com', 'c@example.com'], ['c@example.com'], ] const requestedAliases = new Set() for (const [index, [, rawUpdate]] of syncBentoSubscriberTagsMock.mock.calls.entries()) { const updates = Array.isArray(rawUpdate) ? rawUpdate : [rawUpdate] expect(updates.map(update => update.email)).toEqual(expectedAliasBatches[index]) expect(new Set(updates.map(update => update.email)).size).toBe(updates.length) for (const update of updates) { requestedAliases.add(update.email) expect(update.segments).toEqual([SUPPRESSION_TAG]) expect(update.deleteSegments).toEqual([LIFECYCLE_TAG]) expect(update.deleteSegments).not.toContain(SUPPRESSION_TAG) } } expect(requestedAliases).toEqual(new Set(['a@example.com', 'b@example.com', 'c@example.com'])) }) it('never removes permanent suppression when signup later adds awaiting state', async () => { await suppressEmailAliases('old@example.com', 'new@example.com') const response = await postUser(userRecord({ email: 'new@example.com' })) expect(response.status).toBe(200) expect(syncBentoSubscriberTagsMock).toHaveBeenCalledTimes(2) expect(syncBentoSubscriberTagsMock).toHaveBeenNthCalledWith(2, expect.anything(), { deleteSegments: [], email: 'new@example.com', segments: [LIFECYCLE_TAG], }) for (const [, rawUpdate] of syncBentoSubscriberTagsMock.mock.calls) { const updates = Array.isArray(rawUpdate) ? rawUpdate : [rawUpdate] for (const update of updates) expect(update.deleteSegments).not.toContain(SUPPRESSION_TAG) } }) }) describe('first-organization lifecycle on direct org access', () => { beforeEach(() => { vi.clearAllMocks() pgConnectMock.mockImplementation(async () => ({ query: pgQueryMock, release: pgReleaseMock })) pgReleaseMock.mockImplementation(() => undefined) closeClientMock.mockResolvedValue(undefined) getPgClientMock.mockImplementation(() => ({ connect: pgConnectMock, query: pgQueryMock })) pgQueryMock.mockImplementation(async query => ({ rows: String(query).includes('FROM public.role_bindings AS binding') ? [activeBinding()] : [firstOrgDatabaseState()], })) syncBentoSubscriberTagsMock.mockResolvedValue(true) trackBentoEventMock.mockResolvedValue(true) }) afterEach(() => { vi.restoreAllMocks() }) it('atomically suppresses recovery and emits a joined-org fact for a qualifying binding', async () => { await syncRoleBinding() expect(pgQueryMock).toHaveBeenCalledWith(expect.stringContaining('FROM public.role_bindings'), [ROLE_BINDING_ID]) expect(syncBentoSubscriberTagsMock).toHaveBeenCalledWith(expect.anything(), { deleteSegments: [LIFECYCLE_TAG], email: 'current.user@example.com', segments: [SUPPRESSION_TAG], }) expect(trackBentoEventMock).toHaveBeenCalledWith( expect.anything(), 'current.user@example.com', { joined_at: JOINED_AT, org_id: ORG_ID, role_binding_id: ROLE_BINDING_ID, user_id: USER_ID, }, 'user:joined_org', ) }) it('suppresses and unsubscribes when deletion is scheduled during joined-org delivery', async () => { pgQueryMock .mockResolvedValueOnce({ rows: [activeBinding()] }) .mockResolvedValueOnce({ rows: [firstOrgDatabaseState(true)] }) .mockResolvedValueOnce({ rows: [firstOrgDatabaseState(false, false)] }) await syncRoleBinding() expect(trackBentoEventMock).toHaveBeenCalledOnce() expect(syncBentoSubscriberTagsMock).toHaveBeenNthCalledWith(2, expect.anything(), [{ deleteSegments: [LIFECYCLE_TAG], email: 'current.user@example.com', segments: [SUPPRESSION_TAG], }], expect.any(AbortSignal)) expect(trackBentoEventMock.mock.invocationCallOrder[0]) .toBeLessThan(syncBentoSubscriberTagsMock.mock.invocationCallOrder[1]!) expect(unsubscribeBentoMock).toHaveBeenCalledWith(expect.anything(), 'current.user@example.com', expect.any(AbortSignal)) }) it('suppresses without emitting joined-org state when deletion is already scheduled', async () => { pgQueryMock .mockResolvedValueOnce({ rows: [activeBinding()] }) .mockResolvedValueOnce({ rows: [firstOrgDatabaseState(false, false)] }) await syncRoleBinding() expect(syncBentoSubscriberTagsMock).toHaveBeenCalledWith(expect.anything(), [{ deleteSegments: [LIFECYCLE_TAG], email: 'current.user@example.com', segments: [SUPPRESSION_TAG], }], expect.any(AbortSignal)) expect(trackBentoEventMock).not.toHaveBeenCalled() expect(unsubscribeBentoMock).toHaveBeenCalledWith(expect.anything(), 'current.user@example.com', expect.any(AbortSignal)) }) it('destroys the checked-out Workerd client before joined-org Bento requests', async () => { const lifecycleTrace: string[] = [] let stateRead = 0 pgQueryMock.mockImplementation(async (query) => { if (String(query).includes('FROM public.role_bindings AS binding')) { lifecycleTrace.push('binding:queried') return { rows: [activeBinding()] } } lifecycleTrace.push(stateRead++ === 0 ? 'state:preflight' : 'state:reconciled') return { rows: [firstOrgDatabaseState()] } }) // Workerd closeClient is intentionally a no-op. The checked-out client // must be destroyed explicitly before any external Bento request starts. closeClientMock.mockImplementation(async () => undefined) pgReleaseMock.mockImplementation((destroy) => { lifecycleTrace.push(`client:released:${destroy}`) }) syncBentoSubscriberTagsMock.mockImplementation(async () => { lifecycleTrace.push('tag:suppress') return true }) trackBentoEventMock.mockImplementation(async () => { lifecycleTrace.push('event:joined') return true }) await syncRoleBinding() expect(lifecycleTrace).toEqual([ 'binding:queried', 'client:released:true', 'state:preflight', 'client:released:true', 'tag:suppress', 'event:joined', 'state:reconciled', 'client:released:true', ]) expect(getPgClientMock).toHaveBeenCalledOnce() expect(pgConnectMock).toHaveBeenCalledTimes(3) expect(pgReleaseMock).toHaveBeenCalledTimes(3) expect(pgReleaseMock).toHaveBeenNthCalledWith(1, true) expect(pgReleaseMock).toHaveBeenNthCalledWith(2, true) expect(pgReleaseMock).toHaveBeenNthCalledWith(3, true) expect(closeClientMock).toHaveBeenCalledOnce() expect(closeClientMock).toHaveBeenCalledWith(expect.anything(), getPgClientMock.mock.results[0]?.value) expect(trackBentoEventMock.mock.invocationCallOrder[0]) .toBeLessThan(closeClientMock.mock.invocationCallOrder[0]!) }) it.each([ ['deleted binding', null], ['expired binding', activeBinding({ is_active: false })], ['API-key binding', activeBinding({ principal_type: 'apikey' })], ['non-org binding', activeBinding({ scope_type: 'app' })], ['null-org binding', activeBinding({ org_id: null })], ['inherited binding', activeBinding({ is_direct: false })], ['binding without a current user email', activeBinding({ email: null })], ])('is a no-op for a %s', async (_label, binding) => { pgQueryMock.mockResolvedValue({ rows: binding ? [binding] : [] }) await syncRoleBinding() expect(syncBentoSubscriberTagsMock).not.toHaveBeenCalled() expect(trackBentoEventMock).not.toHaveBeenCalled() }) it('repeats the fact event while keeping permanent suppression idempotent for duplicate messages', async () => { await syncRoleBinding() await syncRoleBinding() expect(syncBentoSubscriberTagsMock).toHaveBeenCalledTimes(2) expect(syncBentoSubscriberTagsMock).toHaveBeenNthCalledWith(1, expect.anything(), { deleteSegments: [LIFECYCLE_TAG], email: 'current.user@example.com', segments: [SUPPRESSION_TAG], }) expect(syncBentoSubscriberTagsMock).toHaveBeenNthCalledWith(2, expect.anything(), { deleteSegments: [LIFECYCLE_TAG], email: 'current.user@example.com', segments: [SUPPRESSION_TAG], }) expect(trackBentoEventMock).toHaveBeenCalledTimes(2) const factData = { joined_at: JOINED_AT, org_id: ORG_ID, role_binding_id: ROLE_BINDING_ID, user_id: USER_ID, } expect(trackBentoEventMock).toHaveBeenNthCalledWith( 1, expect.anything(), 'current.user@example.com', factData, 'user:joined_org', ) expect(trackBentoEventMock).toHaveBeenNthCalledWith( 2, expect.anything(), 'current.user@example.com', factData, 'user:joined_org', ) }) it.each([ ['tag delivery returns false', () => syncBentoSubscriberTagsMock.mockResolvedValueOnce(false)], ['tag delivery throws', () => syncBentoSubscriberTagsMock.mockRejectedValueOnce(new Error('Bento unavailable'))], ['fact event returns false', () => trackBentoEventMock.mockResolvedValueOnce(false)], ])('fails for retry when configured Bento %s', async (_label, configureFailure) => { configureFailure() await expect(syncRoleBinding()).rejects.toThrow() }) it.each([ ['tag delivery returns false', () => syncBentoSubscriberTagsMock.mockResolvedValueOnce(false), false], ['tag delivery throws', () => syncBentoSubscriberTagsMock.mockRejectedValueOnce(new Error('Bento unavailable')), false], ['fact event returns false', () => trackBentoEventMock.mockResolvedValueOnce(false), true], ['fact event throws', () => trackBentoEventMock.mockRejectedValueOnce(new Error('Bento unavailable')), true], ])('reconciles deletion after ambiguous joined-org %s', async (_label, configureFailure, eventAttempted) => { pgQueryMock .mockResolvedValueOnce({ rows: [activeBinding()] }) .mockResolvedValueOnce({ rows: [firstOrgDatabaseState(true)] }) .mockResolvedValueOnce({ rows: [firstOrgDatabaseState(false, false)] }) configureFailure() await expect(syncRoleBinding()).rejects.toThrow() expect(trackBentoEventMock).toHaveBeenCalledTimes(eventAttempted ? 1 : 0) expect(syncBentoSubscriberTagsMock).toHaveBeenLastCalledWith(expect.anything(), [{ deleteSegments: [LIFECYCLE_TAG], email: 'current.user@example.com', segments: [SUPPRESSION_TAG], }], expect.any(AbortSignal)) expect(unsubscribeBentoMock).toHaveBeenCalledWith(expect.anything(), 'current.user@example.com', expect.any(AbortSignal)) }) it('preserves both errors when a Bento mutation and deletion reconciliation fail', async () => { const mutationError = new Error('Bento mutation unavailable') const reconciliationError = new Error('Bento reconciliation unavailable') pgQueryMock .mockResolvedValueOnce({ rows: [activeBinding()] }) .mockResolvedValueOnce({ rows: [firstOrgDatabaseState(true)] }) .mockResolvedValueOnce({ rows: [firstOrgDatabaseState(false, false)] }) syncBentoSubscriberTagsMock .mockRejectedValueOnce(mutationError) .mockRejectedValueOnce(reconciliationError) const result = syncRoleBinding() await expect(result).rejects.toMatchObject({ errors: [mutationError, reconciliationError], message: 'Bento mutation and first-organization reconciliation failed', }) expect(unsubscribeBentoMock).toHaveBeenCalledWith( expect.anything(), 'current.user@example.com', expect.any(AbortSignal), ) }) it('succeeds as a no-op when Bento is not configured', async () => { syncBentoSubscriberTagsMock.mockResolvedValue(undefined) trackBentoEventMock.mockResolvedValue(undefined) await expect(syncRoleBinding()).resolves.toBeUndefined() }) })