// @vitest-environment jsdom /** * 账户与钱包 typed façade 的边界。 * * 渲染层只提交结构化业务参数;平台 origin、Bearer、envelope 与错误分类由 Rust 持有。 * 这里验证 IPC 载荷、登录失效时「续期后重试一次」,以及业务失败不重放。 */ import { beforeEach, expect, test, vi } from 'vitest'; const invoke = vi.fn(); const platformSession = vi.hoisted(() => ({ generation: 1, refresh: vi.fn(), })); // 与仓库其它 App 内调用方同口径:通过 `window.__TAURI__.core.invoke` 解析原生桥。 window.__TAURI__ = { core: { invoke: (...args: unknown[]) => invoke(...args) }, } as never; vi.mock('../src/services/platformSession', () => ({ currentPlatformSessionGeneration: () => platformSession.generation, requestPlatformSessionRefresh: (...args: unknown[]) => platformSession.refresh(...args), })); import { confirmClientWechatProfileRechargeOrder, createClientProfileRechargeOrder, getClientProfileRechargeCenter, getClientProfileReferralInviteCenter, getClientProfileWalletLedger, redeemClientProfileRewardCode, } from '../src/services/accountHost'; beforeEach(() => { invoke.mockReset(); platformSession.generation = 1; platformSession.refresh.mockReset(); platformSession.refresh.mockResolvedValue({ status: 'refreshed', generation: 1, user: { id: 'user-1' }, }); }); test('账户读取只提交结构化参数,不携带 token 或远端地址', async () => { invoke.mockResolvedValue({ walletBalance: 1 }); await getClientProfileRechargeCenter(); await getClientProfileWalletLedger(); await getClientProfileReferralInviteCenter(); await createClientProfileRechargeOrder('product-1'); await confirmClientWechatProfileRechargeOrder('order-1'); await redeemClientProfileRewardCode('CODE-1'); expect(invoke.mock.calls).toEqual([ ['read_profile_recharge_center', undefined], ['read_profile_wallet_ledger', undefined], ['read_profile_referral_invite_center', undefined], ['create_profile_recharge_order', { productId: 'product-1' }], ['confirm_wechat_profile_recharge_order', { orderId: 'order-1' }], ['redeem_profile_reward_code', { code: 'CODE-1' }], ]); const serialized = JSON.stringify(invoke.mock.calls); expect(serialized).not.toContain('token'); expect(serialized).not.toContain('Authorization'); expect(serialized).not.toContain('http'); }); test('登录失效时续期一次并重放,其它业务失败原样抛出', async () => { invoke .mockRejectedValueOnce(new Error('authentication-required: 请先登录')) .mockResolvedValueOnce({ entries: [] }); await expect(getClientProfileWalletLedger()).resolves.toEqual({ entries: [], }); expect(platformSession.refresh).toHaveBeenCalledTimes(1); expect(invoke).toHaveBeenCalledTimes(2); invoke.mockReset(); platformSession.refresh.mockClear(); invoke.mockRejectedValueOnce(new Error('读取泥点账单失败:余额服务不可用')); await expect(getClientProfileWalletLedger()).rejects.toThrow( '读取泥点账单失败:余额服务不可用', ); expect(platformSession.refresh).not.toHaveBeenCalled(); expect(invoke).toHaveBeenCalledTimes(1); }); test('续期期间换号时不重放旧身份的付费请求', async () => { invoke.mockRejectedValueOnce( new Error('authentication-required: 登录已过期'), ); // 续期在飞期间另一个账号完成登录:代际已经推进,旧身份的续期结果不算数。 platformSession.refresh.mockImplementationOnce(async () => { platformSession.generation = 2; return { status: 'refreshed', generation: 1, user: { id: 'user-1' } }; }); await expect(createClientProfileRechargeOrder('product-1')).rejects.toThrow( '登录账号已变化,原请求已停止', ); expect(invoke).toHaveBeenCalledTimes(1); });