import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { ipcMain } from 'electron' import { IPC_CHANNELS } from '@d3ro/core/ipc-channels' import type { IPCResult } from '@d3ro/core/errors' import type { CheckoutSessionParams, CheckoutSessionResult } from '@d3ro/core/types' const mocks = vi.hoisted(() => { const invokeFunction = vi.fn() const activate = vi.fn() const getLicenseService = vi.fn(() => ({ activate })) const cloud = { isAuthenticated: vi.fn(() => true), getUser: vi.fn(() => ({ id: 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa' })), invokeFunction } return { activate, cloud, getLicenseService, invokeFunction } }) vi.mock('../../src/main/services/CloudSyncService', () => ({ getCloudSyncService: () => mocks.cloud })) vi.mock('../../src/main/services/LicenseService', () => ({ getLicenseService: mocks.getLicenseService })) import { PAYMENT_REQUEST_TIMEOUT_MS, registerPaymentHandlers } from '../../src/main/ipc/payment-handlers' type CapturedHandler = (...args: unknown[]) => unknown const handlers = new Map() const validParams: CheckoutSessionParams = { tier: 'pro', provider: 'stripe' } async function invokeIpc(channel: string, ...args: unknown[]): Promise> { const handler = handlers.get(channel) if (!handler) throw new Error(`Missing IPC handler: ${channel}`) return (await handler({}, ...args)) as IPCResult } function checkoutResponse(url = 'https://checkout.stripe.com/c/pay/cs_test_safe#fragment') { return { data: { url }, error: null } } beforeEach(() => { vi.useRealTimers() vi.clearAllMocks() handlers.clear() mocks.cloud.isAuthenticated.mockReturnValue(true) mocks.cloud.getUser.mockReturnValue({ id: 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa' }) vi.mocked(ipcMain.handle).mockImplementation((channel: string, handler: CapturedHandler) => { handlers.set(channel, handler) }) registerPaymentHandlers() }) afterEach(() => { vi.useRealTimers() }) describe('desktop payment IPC security boundary', () => { it('uses the authenticated Stripe Edge checkout contract and returns only its trusted URL', async () => { mocks.invokeFunction.mockResolvedValue(checkoutResponse()) const result = await invokeIpc( IPC_CHANNELS.PAYMENT.CREATE_CHECKOUT_SESSION, validParams ) expect(result).toEqual({ success: true, data: { checkoutUrl: 'https://checkout.stripe.com/c/pay/cs_test_safe#fragment', provider: 'stripe', status: 'pending' } }) expect(mocks.invokeFunction).toHaveBeenCalledTimes(1) const [name, body, options] = mocks.invokeFunction.mock.calls[0] expect(name).toBe('stripe-checkout') expect(body).toEqual({ tier: 'pro', success_url: 'https://d3ro.chanpaca.net/billing?desktop_checkout=success', cancel_url: 'https://d3ro.chanpaca.net/billing?desktop_checkout=cancelled', idempotency_key: expect.stringMatching(/^desktop:stripe-checkout:[0-9a-f-]{36}$/) }) expect(options).toMatchObject({ timeoutMs: PAYMENT_REQUEST_TIMEOUT_MS }) expect(options.signal).toBeInstanceOf(AbortSignal) }) it.each(['toss', 'portone', 'arbitrary-provider'])( 'rejects unsupported provider %s before network I/O', async (provider) => { const result = await invokeIpc(IPC_CHANNELS.PAYMENT.CREATE_CHECKOUT_SESSION, { ...validParams, provider }) expect(result.success).toBe(false) expect(mocks.invokeFunction).not.toHaveBeenCalled() } ) it.each(['free', 'team', 'enterprise', 'arbitrary-tier'])( 'rejects renderer-selected tier %s before network I/O', async (tier) => { const result = await invokeIpc(IPC_CHANNELS.PAYMENT.CREATE_CHECKOUT_SESSION, { ...validParams, tier }) expect(result.success).toBe(false) expect(mocks.invokeFunction).not.toHaveBeenCalled() } ) it.each([ 'http://checkout.stripe.com/c/pay/cs_test_unsafe', 'https://checkout.stripe.com.evil.test/c/pay/cs_test_unsafe', 'https://checkout.stripe.com@evil.test/c/pay/cs_test_unsafe', 'https://user:pass@checkout.stripe.com/c/pay/cs_test_unsafe', 'https://checkout.stripe.com/portal/cs_test_unsafe' ])('rejects an untrusted checkout URL: %s', async (url) => { mocks.invokeFunction.mockResolvedValue(checkoutResponse(url)) const result = await invokeIpc(IPC_CHANNELS.PAYMENT.CREATE_CHECKOUT_SESSION, validParams) expect(result.success).toBe(false) }) it('fails closed on provider errors without exposing provider details', async () => { mocks.invokeFunction.mockResolvedValue({ data: null, error: { message: 'sensitive Stripe provider details' } }) const result = await invokeIpc(IPC_CHANNELS.PAYMENT.CREATE_CHECKOUT_SESSION, validParams) expect(result).toMatchObject({ success: false, error: { message: 'Checkout service rejected the request' } }) expect(JSON.stringify(result)).not.toContain('sensitive Stripe provider details') }) it('fails closed on network errors without converting them into checkout success', async () => { mocks.invokeFunction.mockRejectedValue(new Error('sensitive network details')) const result = await invokeIpc(IPC_CHANNELS.PAYMENT.CREATE_CHECKOUT_SESSION, validParams) expect(result).toMatchObject({ success: false, error: { message: 'Checkout service is unavailable' } }) expect(JSON.stringify(result)).not.toContain('sensitive network details') }) it('coalesces concurrent and sequential duplicate checkout requests', async () => { let resolveProvider: ((value: ReturnType) => void) | undefined mocks.invokeFunction.mockReturnValue( new Promise((resolve) => { resolveProvider = resolve }) ) const first = invokeIpc( IPC_CHANNELS.PAYMENT.CREATE_CHECKOUT_SESSION, validParams ) const concurrent = invokeIpc( IPC_CHANNELS.PAYMENT.CREATE_CHECKOUT_SESSION, validParams ) expect(mocks.invokeFunction).toHaveBeenCalledTimes(1) resolveProvider?.(checkoutResponse()) const [firstResult, concurrentResult] = await Promise.all([first, concurrent]) const sequentialResult = await invokeIpc( IPC_CHANNELS.PAYMENT.CREATE_CHECKOUT_SESSION, validParams ) expect(firstResult).toEqual(concurrentResult) expect(sequentialResult).toEqual(firstResult) expect(mocks.invokeFunction).toHaveBeenCalledTimes(1) }) it('reuses the server idempotency key when a failed checkout is retried', async () => { mocks.invokeFunction.mockResolvedValue({ data: null, error: { message: 'provider unavailable' } }) const first = await invokeIpc(IPC_CHANNELS.PAYMENT.CREATE_CHECKOUT_SESSION, validParams) const retry = await invokeIpc(IPC_CHANNELS.PAYMENT.CREATE_CHECKOUT_SESSION, validParams) expect(first.success).toBe(false) expect(retry.success).toBe(false) expect(mocks.invokeFunction).toHaveBeenCalledTimes(2) expect(mocks.invokeFunction.mock.calls[0][1].idempotency_key).toBe( mocks.invokeFunction.mock.calls[1][1].idempotency_key ) }) it('aborts and fails closed when checkout exceeds the deadline', async () => { vi.useFakeTimers() mocks.invokeFunction.mockReturnValue(new Promise(() => undefined)) const pending = invokeIpc(IPC_CHANNELS.PAYMENT.CREATE_CHECKOUT_SESSION, validParams) await vi.advanceTimersByTimeAsync(PAYMENT_REQUEST_TIMEOUT_MS) const result = await pending expect(result).toMatchObject({ success: false, error: { message: 'Payment server request timed out' } }) const options = mocks.invokeFunction.mock.calls[0][2] expect(options.signal.aborted).toBe(true) }) it('uses server subscription readback and never mutates the local license', async () => { mocks.invokeFunction.mockResolvedValue({ data: { tier: 'pro_plus', status: 'active', current_period_end: '2099-01-01T00:00:00.000Z' }, error: null }) const result = await invokeIpc<{ success: boolean; activeTier: string }>( IPC_CHANNELS.PAYMENT.VERIFY_PAYMENT, { tier: 'enterprise' } ) expect(result).toEqual({ success: true, data: { success: true, activeTier: 'pro_plus' } }) expect(mocks.invokeFunction).toHaveBeenCalledWith( 'payple-manage', { action: 'info' }, expect.objectContaining({ timeoutMs: PAYMENT_REQUEST_TIMEOUT_MS }) ) expect(mocks.getLicenseService).not.toHaveBeenCalled() expect(mocks.activate).not.toHaveBeenCalled() }) it('does not grant an arbitrary renderer tier when the server says free', async () => { mocks.invokeFunction.mockResolvedValue({ data: { tier: 'free', status: 'active', current_period_end: null }, error: null }) const result = await invokeIpc<{ success: boolean; activeTier: string }>( IPC_CHANNELS.PAYMENT.VERIFY_PAYMENT, { tier: 'enterprise' } ) expect(result).toEqual({ success: true, data: { success: false, activeTier: 'free' } }) expect(mocks.activate).not.toHaveBeenCalled() }) it('preserves prepaid cancellation until expiry and fails closed after expiry', async () => { mocks.invokeFunction .mockResolvedValueOnce({ data: { tier: 'pro', status: 'canceled', current_period_end: '2099-01-01T00:00:00.000Z' }, error: null }) .mockResolvedValueOnce({ data: { tier: 'pro', status: 'canceled', current_period_end: '2000-01-01T00:00:00.000Z' }, error: null }) const current = await invokeIpc<{ tier: string valid: boolean expiresAt: number | null }>(IPC_CHANNELS.PAYMENT.GET_SUBSCRIPTION_STATUS) const expired = await invokeIpc<{ tier: string valid: boolean expiresAt: number | null }>(IPC_CHANNELS.PAYMENT.GET_SUBSCRIPTION_STATUS) expect(current).toMatchObject({ success: true, data: { tier: 'pro', valid: true } }) expect(expired).toMatchObject({ success: true, data: { tier: 'free', valid: false } }) }) it('rejects malformed or arbitrary server entitlement data', async () => { mocks.invokeFunction.mockResolvedValue({ data: { tier: 'enterprise', status: 'active', current_period_end: '2099-01-01T00:00:00.000Z' }, error: null }) const result = await invokeIpc(IPC_CHANNELS.PAYMENT.GET_SUBSCRIPTION_STATUS) expect(result).toMatchObject({ success: false, error: { message: 'Subscription status is unavailable' } }) }) it('requires an authenticated user before invoking payment functions', async () => { mocks.cloud.isAuthenticated.mockReturnValue(false) const result = await invokeIpc(IPC_CHANNELS.PAYMENT.CREATE_CHECKOUT_SESSION, validParams) expect(result.success).toBe(false) expect(mocks.invokeFunction).not.toHaveBeenCalled() }) })