import { beforeEach, describe, expect, it, vi } from 'vitest' import type { IPCResult } from '@d3ro/core/errors' import type { CheckoutSessionResult, SubscriptionStatusResult } from '@d3ro/core/types' import { CheckoutFlowController, type CheckoutFlowPorts, isTrustedStripeCheckoutUrl } from '../../src/renderer/components/payment/checkout-flow' type CreateResult = IPCResult type StatusResult = IPCResult const trustedUrl = 'https://checkout.stripe.com/c/pay/cs_test_renderer_safe#fragment' const checkoutSuccess: CreateResult = { success: true, data: { checkoutUrl: trustedUrl, provider: 'stripe', status: 'pending' } } const openSuccess: IPCResult = { success: true, data: undefined } const activeSubscription: StatusResult = { success: true, data: { tier: 'pro_plus', valid: true, expiresAt: Date.parse('2099-01-01T00:00:00.000Z') } } const ipcFailure = { success: false as const, error: { code: 999, message: 'private provider details' } } function deferred(): { promise: Promise resolve: (value: T) => void } { let resolve!: (value: T) => void const promise = new Promise((done) => { resolve = done }) return { promise, resolve } } describe('renderer checkout flow', () => { let createCheckoutSession: ReturnType let openExternal: ReturnType let getSubscriptionStatus: ReturnType let controller: CheckoutFlowController beforeEach(() => { createCheckoutSession = vi.fn().mockResolvedValue(checkoutSuccess) openExternal = vi.fn().mockResolvedValue(openSuccess) getSubscriptionStatus = vi.fn().mockResolvedValue(activeSubscription) const ports: CheckoutFlowPorts = { createCheckoutSession, openExternal, getSubscriptionStatus } controller = new CheckoutFlowController(ports) }) it('opens only the authenticated IPC checkout result and waits for server confirmation', async () => { const opened = await controller.start('pro') expect(opened).toBe(true) expect(createCheckoutSession).toHaveBeenCalledWith({ tier: 'pro', provider: 'stripe' }) expect(openExternal).toHaveBeenCalledWith({ url: trustedUrl }) expect(getSubscriptionStatus).not.toHaveBeenCalled() expect(controller.getState()).toEqual({ phase: 'awaiting', error: null, verifiedTier: null }) }) it('fails closed when checkout IPC returns a provider error', async () => { createCheckoutSession.mockResolvedValue(ipcFailure) await controller.start('pro') expect(controller.getState()).toMatchObject({ phase: 'idle', error: 'checkout-failed' }) expect(openExternal).not.toHaveBeenCalled() }) it('fails closed when checkout IPC throws a network error', async () => { createCheckoutSession.mockRejectedValue(new Error('private network details')) await controller.start('pro') expect(controller.getState()).toMatchObject({ phase: 'idle', error: 'network-failed' }) expect(openExternal).not.toHaveBeenCalled() }) it.each([ { checkoutUrl: 'http://checkout.stripe.com/c/pay/cs_test_bad', provider: 'stripe', status: 'pending' }, { checkoutUrl: 'https://evil.test/c/pay/cs_test_bad', provider: 'stripe', status: 'pending' }, { checkoutUrl: trustedUrl, provider: 'toss', status: 'pending' }, { checkoutUrl: trustedUrl, provider: 'stripe', status: 'completed' } ])('rejects an unsafe or forged checkout response %#', async (data) => { createCheckoutSession.mockResolvedValue({ success: true, data }) await controller.start('pro') expect(controller.getState()).toMatchObject({ phase: 'idle', error: 'unsafe-checkout-response' }) expect(openExternal).not.toHaveBeenCalled() }) it('fails closed when the OS cannot open the checkout page', async () => { openExternal.mockResolvedValue(ipcFailure) await controller.start('pro') expect(controller.getState()).toMatchObject({ phase: 'idle', error: 'open-failed' }) }) it('fails closed when opening the checkout page throws', async () => { openExternal.mockRejectedValue(new Error('private shell details')) await controller.start('pro') expect(controller.getState()).toMatchObject({ phase: 'idle', error: 'open-failed' }) }) it('coalesces duplicate checkout clicks into one IPC request', async () => { const pending = deferred() createCheckoutSession.mockReturnValue(pending.promise) const first = controller.start('pro') const duplicate = controller.start('pro_plus') expect(await duplicate).toBe(false) expect(createCheckoutSession).toHaveBeenCalledTimes(1) pending.resolve(checkoutSuccess) expect(await first).toBe(true) expect(openExternal).toHaveBeenCalledTimes(1) }) it('invalidates a pending checkout when the user closes the modal', async () => { const pending = deferred() createCheckoutSession.mockReturnValue(pending.promise) const start = controller.start('pro') controller.cancel() pending.resolve(checkoutSuccess) expect(await start).toBe(false) expect(openExternal).not.toHaveBeenCalled() expect(controller.getState()).toEqual({ phase: 'idle', error: null, verifiedTier: null }) }) it('does not treat an opened or cancelled provider page as payment success', async () => { await controller.start('pro') getSubscriptionStatus.mockResolvedValue({ success: true, data: { tier: 'free', valid: false, expiresAt: null } }) const tier = await controller.verify() expect(tier).toBeNull() expect(controller.getState()).toMatchObject({ phase: 'awaiting', error: 'not-confirmed' }) }) it('fails closed when subscription readback returns an IPC error', async () => { await controller.start('pro') getSubscriptionStatus.mockResolvedValue(ipcFailure) const tier = await controller.verify() expect(tier).toBeNull() expect(controller.getState()).toMatchObject({ phase: 'awaiting', error: 'verification-failed' }) }) it('fails closed when subscription readback throws a network error', async () => { await controller.start('pro') getSubscriptionStatus.mockRejectedValue(new Error('private subscription details')) const tier = await controller.verify() expect(tier).toBeNull() expect(controller.getState()).toMatchObject({ phase: 'awaiting', error: 'network-failed' }) }) it('rejects an arbitrary paid tier even when a forged response claims it is valid', async () => { await controller.start('pro') getSubscriptionStatus.mockResolvedValue({ success: true, data: { tier: 'enterprise', valid: true, expiresAt: null } }) const tier = await controller.verify() expect(tier).toBeNull() expect(controller.getState()).toMatchObject({ phase: 'awaiting', error: 'invalid-entitlement' }) }) it('returns and displays only the server-read subscription tier', async () => { await controller.start('pro') const tier = await controller.verify() expect(tier).toBe('pro_plus') expect(controller.getState()).toEqual({ phase: 'complete', error: null, verifiedTier: 'pro_plus' }) }) it('coalesces duplicate subscription verification clicks', async () => { await controller.start('pro') const pending = deferred() getSubscriptionStatus.mockReturnValue(pending.promise) const first = controller.verify() const duplicate = controller.verify() expect(await duplicate).toBeNull() expect(getSubscriptionStatus).toHaveBeenCalledTimes(1) pending.resolve(activeSubscription) expect(await first).toBe('pro_plus') }) it('ignores a successful subscription response after cancellation', async () => { await controller.start('pro') const pending = deferred() getSubscriptionStatus.mockReturnValue(pending.promise) const verification = controller.verify() controller.cancel() pending.resolve(activeSubscription) expect(await verification).toBeNull() expect(controller.getState()).toEqual({ phase: 'idle', error: null, verifiedTier: null }) }) it('accepts only the trusted Stripe HTTPS checkout origin and path', () => { expect(isTrustedStripeCheckoutUrl(trustedUrl)).toBe(true) expect(isTrustedStripeCheckoutUrl('https://checkout.stripe.com/portal/cs_test_bad')).toBe(false) expect( isTrustedStripeCheckoutUrl('https://user:pass@checkout.stripe.com/c/pay/cs_test_bad') ).toBe(false) }) })