/** * OmniCard AI (CRMmp) - mpuniapp 网络拦截器与 API 客户端基础契约测试 (TDD Red -> Green) * * 严格按照 AGENTS.md 3.3、4.3 规范与 PRD 设计编写。 */ describe('OmniCard AI 用户端 mpuniapp 网络拦截器与 API 契约测试', () => { let storageMap: Map; let toastCalls: Array<{ title: string; icon?: string }>; let reLaunchCalls: Array<{ url: string }>; let lastRequestOptions: any; let mockRequestResponse: { statusCode?: number; data?: any; failError?: any; }; beforeEach(() => { storageMap = new Map(); toastCalls = []; reLaunchCalls = []; lastRequestOptions = null; mockRequestResponse = { statusCode: 200, data: { code: 1000, message: 'success', data: { ok: true } }, }; // 构造全局 uni 运行时 Mock 环境 (global as any).uni = { getStorageSync: (key: string) => storageMap.get(key) || '', setStorageSync: (key: string, val: string) => storageMap.set(key, val), removeStorageSync: (key: string) => storageMap.delete(key), showToast: (options: { title: string; icon?: string }) => { toastCalls.push(options); }, reLaunch: (options: { url: string }) => { reLaunchCalls.push(options); }, request: (options: any) => { lastRequestOptions = options; setTimeout(() => { if (mockRequestResponse.failError) { options.fail?.(mockRequestResponse.failError); } else { options.success?.({ statusCode: mockRequestResponse.statusCode ?? 200, data: mockRequestResponse.data, header: {}, }); } options.complete?.(); }, 0); return { abort: jest.fn(), }; }, }; }); afterEach(() => { delete (global as any).uni; }); describe('1. 基础配置与请求拦截器 (request.uts)', () => { it('1.1 默认配置、baseUrl 拼接与超时时长 (10000ms)', async () => { // @ts-ignore const { request, getBaseUrl, setBaseUrl, getRequestTimeout, setRequestTimeout } = require('../../../mpuniapp/common/request.uts'); expect(getRequestTimeout()).toBe(10000); expect(getBaseUrl()).toBeDefined(); setBaseUrl('https://api.omnicard.ai/api'); setRequestTimeout(8000); expect(getBaseUrl()).toBe('https://api.omnicard.ai/api'); expect(getRequestTimeout()).toBe(8000); // 发起相对路径请求 await request({ url: '/test/endpoint', method: 'GET', }); expect(lastRequestOptions).toBeDefined(); expect(lastRequestOptions.url).toBe('https://api.omnicard.ai/api/test/endpoint'); expect(lastRequestOptions.timeout).toBe(8000); }); it('1.2 自动注入 Authorization (Token) 与 x-tenant-id 请求头', async () => { // @ts-ignore const { request } = require('../../../mpuniapp/common/request.uts'); storageMap.set('token', 'mock_jwt_token_999'); storageMap.set('tenant_id', 'tenant_omni_88'); await request({ url: '/card/info', method: 'GET', }); expect(lastRequestOptions.header).toBeDefined(); expect(lastRequestOptions.header['Authorization']).toBe('Bearer mock_jwt_token_999'); expect(lastRequestOptions.header['x-tenant-id']).toBe('tenant_omni_88'); }); it('1.3 允许显式自定义 Header 覆盖或扩展', async () => { // @ts-ignore const { request } = require('../../../mpuniapp/common/request.uts'); storageMap.set('token', 'default_token'); await request({ url: '/custom/header', method: 'POST', header: { Authorization: 'CustomToken override', 'x-custom-trace': 'trace_123', }, }); expect(lastRequestOptions.header['Authorization']).toBe('CustomToken override'); expect(lastRequestOptions.header['x-custom-trace']).toBe('trace_123'); }); }); describe('2. 统一响应解包与错误拦截', () => { it('2.1 成功响应 (code === 1000) 统一自动解包返回 data 核心数据', async () => { // @ts-ignore const { request } = require('../../../mpuniapp/common/request.uts'); mockRequestResponse.data = { code: 1000, message: 'success', data: { cardId: 'card_1001', name: 'Alex Rivera', }, }; const res = await request({ url: '/card/info', method: 'GET', }); expect(res).toEqual({ cardId: 'card_1001', name: 'Alex Rivera', }); expect(toastCalls).toHaveLength(0); }); it('2.2 业务失败 (code !== 1000) 自动弹出 uni.showToast 且 Promise reject', async () => { // @ts-ignore const { request } = require('../../../mpuniapp/common/request.uts'); mockRequestResponse.data = { code: 1002, message: '指定微官网名片不存在或已下架', data: null, }; await expect( request({ url: '/card/portal/info', method: 'GET', }) ).rejects.toThrow('指定微官网名片不存在或已下架'); expect(toastCalls).toHaveLength(1); expect(toastCalls[0].title).toBe('指定微官网名片不存在或已下架'); expect(toastCalls[0].icon).toBe('none'); }); it('2.3 认证失效 (code === 401 或 HTTP 401) 自动跳转登录页、清除 Token 并弹出提示', async () => { // @ts-ignore const { request } = require('../../../mpuniapp/common/request.uts'); storageMap.set('token', 'expired_token'); mockRequestResponse.data = { code: 401, message: '登录状态已失效,请重新登录', }; await expect( request({ url: '/card/my-cards', method: 'GET', }) ).rejects.toThrow('登录状态已失效,请重新登录'); expect(storageMap.has('token')).toBe(false); expect(reLaunchCalls).toHaveLength(1); expect(reLaunchCalls[0].url).toBe('/pages/common/login'); expect(toastCalls).toHaveLength(1); expect(toastCalls[0].title).toBe('登录状态已失效,请重新登录'); }); it('2.4 网络请求异常 (fail 回调) 提示网络异常并 Promise reject', async () => { // @ts-ignore const { request } = require('../../../mpuniapp/common/request.uts'); mockRequestResponse.failError = { errMsg: 'request:fail timeout', }; await expect( request({ url: '/timeout/test', method: 'GET', }) ).rejects.toThrow(); expect(toastCalls).toHaveLength(1); expect(toastCalls[0].title).toContain('网络连接异常'); }); }); describe('3. 核心 API 方法契约 (api.uts)', () => { it('3.1 fetchPortalCard 能够按 ID 或 Slug 正确请求微官网聚合详情', async () => { // @ts-ignore const { fetchPortalCard } = require('../../../mpuniapp/common/api.uts'); mockRequestResponse.data = { code: 1000, message: 'success', data: { id: 1001, slug: 'alex-rivera', name: 'Alex Rivera', title: 'VP of Global Business', company: 'OmniCard AI Global', bentoBlocks: [], }, }; const result = await fetchPortalCard('alex-rivera'); expect(lastRequestOptions.method).toBe('GET'); expect(lastRequestOptions.url).toContain('/open/card/portal/detail'); expect(lastRequestOptions.data).toEqual({ slugOrId: 'alex-rivera', slug: 'alex-rivera', }); expect(result.name).toBe('Alex Rivera'); expect(result.slug).toBe('alex-rivera'); }); it('3.1b fetchPortalCard 数字标识同时带 slugOrId 与 id', async () => { // @ts-ignore const { fetchPortalCard } = require('../../../mpuniapp/common/api.uts'); mockRequestResponse.data = { code: 1000, message: 'success', data: { id: 1001, slug: 'alex-rivera', name: 'Alex Rivera', title: 'VP of Global Business', company: 'OmniCard AI Global', bentoBlocks: [], }, }; await fetchPortalCard('1001'); expect(lastRequestOptions.method).toBe('GET'); expect(lastRequestOptions.url).toContain('/open/card/portal/detail'); expect(lastRequestOptions.data).toEqual({ slugOrId: '1001', id: '1001', }); }); it('3.2 submitLead 能够提交留资/互换名片数据并返回潜客模型', async () => { // @ts-ignore const { submitLead } = require('../../../mpuniapp/common/api.uts'); const leadPayload = { cardId: 1001, name: 'Tariq Mansour', company: 'Dubai Energy Holding', phone: '+971-50-1234567', email: 'tariq@dubaienergy.ae', source: 'nfc', notes: 'Interested in enterprise solution', }; mockRequestResponse.data = { code: 1000, message: 'success', data: { id: 'lead_9921', ...leadPayload, status: 'new', intentScore: 85, }, }; const lead = await submitLead(leadPayload); expect(lastRequestOptions.method).toBe('POST'); expect(lastRequestOptions.url).toContain('/open/crm/lead/exchange'); expect(lastRequestOptions.data).toEqual(leadPayload); expect(lead.id).toBe('lead_9921'); expect(lead.status).toBe('new'); }); it('3.3 recordRadarTrack 能够发送访客轨迹数据并记录', async () => { // @ts-ignore const { recordRadarTrack } = require('../../../mpuniapp/common/api.uts'); const trackPayload = { cardId: 1001, visitorId: 'vis_de_4432', touchType: 'qr', stayDurationMs: 65000, scrollDepth: 80, viewedBlocks: ['hero', 'case_1'], }; mockRequestResponse.data = { code: 1000, message: 'success', data: { id: 'track_881', ...trackPayload, country: 'Germany', city: 'Frankfurt', }, }; const track = await recordRadarTrack(trackPayload); expect(lastRequestOptions.method).toBe('POST'); expect(lastRequestOptions.url).toContain('/open/crm/radar/track'); expect(lastRequestOptions.data).toEqual(trackPayload); expect(track.id).toBe('track_881'); expect(track.country).toBe('Germany'); }); it('3.4 getRecentTracks 按 cardId 查询最近足迹流水', async () => { // @ts-ignore const { getRecentTracks } = require('../../../mpuniapp/common/api.uts'); mockRequestResponse.data = { code: 1000, message: 'success', data: [ { id: 'track_881', cardId: 1001, visitorId: 'vis_de_4432', touchType: 'qr', stayDurationMs: 65000, scrollDepth: 80, viewedBlocks: ['hero'], }, ], }; const tracks = await getRecentTracks(1001, 10); expect(lastRequestOptions.method).toBe('GET'); expect(lastRequestOptions.url).toContain('/open/crm/radar/recent'); expect(lastRequestOptions.data).toEqual({ cardId: 1001, limit: 10 }); expect(tracks).toHaveLength(1); expect(tracks[0].id).toBe('track_881'); }); }); });