tests/bento-first-org-lifecycle.unit.test.ts
Rama main · réplica de lectura del repositorio original.
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<string, unknown>[] }>>(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<string, unknown> | null, user_metadata: Record<string, unknown> | 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<boolean | undefined>>(async () => true),
syncUserPreferenceTagsMock: vi.fn(async () => undefined),
trackBentoEventMock: vi.fn<(
c: unknown,
email: string,
data: Record<string, unknown>,
event: string,
) => Promise<boolean | undefined>>(async () => true),
unsubscribeBentoMock: vi.fn<(
c: unknown,
email: string,
signal?: AbortSignal,
) => Promise<boolean | undefined>>(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<void>) => 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<boolean | undefined>
syncBentoFirstOrgOnRoleBindingWrite: (c: never, roleBindingId: string) => Promise<void>
}
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<string, unknown> = {}) {
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<string, unknown> = {}) {
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<string, unknown> | null, app_metadata: Record<string, unknown> = {}) {
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<string>()
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()
})
})