import { describe, it, expect, vi, beforeEach } from 'vitest' import { eventBus } from '@/lib/events/bus' // Mock dependencies: factory must not reference outer variables const mockExchangeCodeForAccount = vi.fn() const mockFetchAccountDisplayName = vi.fn() vi.mock('@/extensions/general/stripe/lib/connect', () => ({ exchangeCodeForAccount: (...args: unknown[]) => mockExchangeCodeForAccount(...args), fetchAccountDisplayName: (...args: unknown[]) => mockFetchAccountDisplayName(...args), })) const { mockFrom } = vi.hoisted(() => ({ mockFrom: vi.fn() })) vi.mock('@/lib/supabase/server', () => ({ createServiceClient: vi.fn().mockResolvedValue({ from: mockFrom }), })) vi.mock('@/lib/init', () => ({ ensureInitialized: vi.fn() })) vi.stubEnv('NEXT_PUBLIC_APP_URL', 'http://localhost:3000') import { GET } from '../route' const CONNECTION_ID = 'connection-1' const OAUTH_STATE = 'state-token-1' function makeRequest(params: Record) { const url = new URL('http://localhost:3000/api/extensions/stripe/callback') for (const [k, v] of Object.entries(params)) { url.searchParams.set(k, v) } return new Request(url.toString()) } function mockChain(result: { data?: unknown; error?: unknown }) { const chain: Record = {} for (const m of ['select', 'eq', 'update', 'insert']) { chain[m] = vi.fn().mockReturnValue(chain) } chain.single = vi .fn() .mockResolvedValue({ data: result.data ?? null, error: result.error ?? null }) // For chains ending without .single() (the insert and the error-path updates) chain.then = (resolve: (v: unknown) => void) => resolve({ data: result.data ?? null, error: result.error ?? null }) return chain } describe('GET /api/extensions/stripe/callback', () => { beforeEach(() => { vi.clearAllMocks() eventBus.clear() mockExchangeCodeForAccount.mockResolvedValue({ stripeAccountId: 'acct_123', livemode: false, }) mockFetchAccountDisplayName.mockResolvedValue('Test Shop') }) it('activates the connection and turns the transaction feed on by default', async () => { const findChain = mockChain({ data: { id: CONNECTION_ID, user_id: 'user-1', company_id: 'company-1' }, }) const replayChain = mockChain({ error: null }) const activateChain = mockChain({ data: { id: CONNECTION_ID, company_id: 'company-1', user_id: 'user-1', stripe_account_id: 'acct_123', livemode: false, }, }) mockFrom .mockReturnValueOnce(findChain) .mockReturnValueOnce(replayChain) .mockReturnValueOnce(activateChain) const response = await GET(makeRequest({ code: 'ac_123', state: OAUTH_STATE })) expect(response.status).toBe(307) expect(response.headers.get('location')).toBe( 'http://localhost:3000/import?mode=stripe&stripe_connected=true', ) // Feed-only product: a completed OAuth must leave the nightly sync armed, // otherwise a connected account silently ingests nothing. const activatePayload = (activateChain.update as ReturnType).mock.calls[0][0] expect(activatePayload).toMatchObject({ stripe_account_id: 'acct_123', livemode: false, display_name: 'Test Shop', status: 'active', oauth_state: null, transaction_sync_enabled: true, }) }) it('redirects with an error and never activates when the state is unknown', async () => { mockFrom.mockReturnValueOnce(mockChain({ data: null, error: { code: 'PGRST116' } })) const response = await GET(makeRequest({ code: 'ac_123', state: 'unknown-state' })) expect(response.headers.get('location')).toBe( 'http://localhost:3000/import?mode=stripe&stripe_error=invalid_state', ) expect(mockExchangeCodeForAccount).not.toHaveBeenCalled() }) it('redirects with an error when the authorization code was already used', async () => { mockFrom .mockReturnValueOnce( mockChain({ data: { id: CONNECTION_ID, user_id: 'user-1', company_id: 'company-1' } }), ) .mockReturnValueOnce(mockChain({ error: { code: '23505' } })) const response = await GET(makeRequest({ code: 'ac_123', state: OAUTH_STATE })) expect(response.headers.get('location')).toBe( 'http://localhost:3000/import?mode=stripe&stripe_error=invalid_state', ) expect(mockExchangeCodeForAccount).not.toHaveBeenCalled() }) it('reports the conflict when the Stripe account is already connected', async () => { mockFrom .mockReturnValueOnce( mockChain({ data: { id: CONNECTION_ID, user_id: 'user-1', company_id: 'company-1' } }), ) .mockReturnValueOnce(mockChain({ error: null })) .mockReturnValueOnce(mockChain({ data: null, error: { code: '23505', message: 'dup' } })) .mockReturnValueOnce(mockChain({ error: null })) const response = await GET(makeRequest({ code: 'ac_123', state: OAUTH_STATE })) expect(response.headers.get('location')).toBe( 'http://localhost:3000/import?mode=stripe&stripe_error=account_already_connected', ) }) it('redirects without touching Stripe when parameters are missing', async () => { const response = await GET(makeRequest({ state: OAUTH_STATE })) expect(response.headers.get('location')).toBe( 'http://localhost:3000/import?mode=stripe&stripe_error=missing_parameters', ) expect(mockFrom).not.toHaveBeenCalled() }) })