313 行
9.5 KiB
TypeScript
313 行
9.5 KiB
TypeScript
/**
|
|
* OmniCard AI (CRMmp) - mpuniapp 网络拦截器与 API 客户端基础契约测试 (TDD Red -> Green)
|
|
*
|
|
* 严格按照 AGENTS.md 3.3、4.3 规范与 PRD 设计编写。
|
|
*/
|
|
|
|
describe('OmniCard AI 用户端 mpuniapp 网络拦截器与 API 契约测试', () => {
|
|
let storageMap: Map<string, string>;
|
|
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('/card/portal/info');
|
|
expect(lastRequestOptions.data).toEqual({ id: 'alex-rivera' });
|
|
expect(result.name).toBe('Alex Rivera');
|
|
expect(result.slug).toBe('alex-rivera');
|
|
});
|
|
|
|
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('/crm/lead/submit');
|
|
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('/crm/radar/track');
|
|
expect(lastRequestOptions.data).toEqual(trackPayload);
|
|
expect(track.id).toBe('track_881');
|
|
expect(track.country).toBe('Germany');
|
|
});
|
|
});
|
|
});
|