文件
OmniCard/backend/test/mpuniapp/nfc-security.test.ts
wikiboss eec259a3a7 feat: 完成全系统30个功能点开发与E2E全链路验收 (8轮调度32个子代理闭环)
- 模块一: 出海案例作品集、博客动态发布、人名发音语音条、RTL阿语多语言切换
- 模块二: AI顾问人设配置、行业术语库润色、RAG切片沙盒调试、流式会话与意向留资卡片
- 模块三: 全球访客雷达大屏、多维加权意向算分、外部CRM对接推送、团队公海池流转
- 模块四: 移动端NFC安全挂失、NFC动态路由规则配置、动态防伪验签、展会制卡Zip批量导出
- 模块五: 组织架构与名片分配、移动端个人中心设置、离职交接与历史线索迁移、安全审计日志
- 模块六: Stripe国际信用卡订阅Webhook、独立二级域名与SSL检测、坐席配额预警、多币种发票
- 模块七: 纸质名片OCR解析入库、国际商务日程RFC 5545 ICS日历、微官网离线SWR预加载、展会实时排行榜大屏
- 模块八: 全球出海SEO OpenGraph元标签注入与社交爬虫探测、30点端到端全链路业务流集成测试套件
2026-09-20 23:47:47 +08:00

297 行
11 KiB
TypeScript

此文件含有模棱两可的 Unicode 字符

此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

/**
* OmniCard AI (CRMmp) - 移动端 NFC 安全与远程挂失测试套件 (TDD Red -> Green)
*
* 严格按照 AGENTS.md 规范与 Point 13 需求编写:
* 1. 管理 NFC 硬件列表与在线/挂失冻结状态 ('active' / 'frozen')
* 2. freezeDevice(id) 与 unfreezeDevice(id) 状态流转与异常保护
* 3. 防伪 UID 校验与安全哈希查验
* 4. 极简瑞士国际主义风格规范(零 emoji、单一主 CTA、触控区 >= 44x44)
*/
import * as fs from 'fs';
import * as path from 'path';
const SECURITY_PAGE = path.resolve(
__dirname,
'../../../mpuniapp/pages/owner/nfc/security.uvue'
);
const SECURITY_UTS = path.resolve(
__dirname,
'../../../mpuniapp/pages/owner/nfc/useSecurity.uts'
);
const PAGES_JSON = path.resolve(__dirname, '../../../mpuniapp/pages.json');
export type SecurityDeviceStatus = 'active' | 'frozen';
export interface NfcSecurityDevice {
id: number;
uid: string;
cardId?: number | null;
status: SecurityDeviceStatus;
securityHash?: string;
batchNo?: string;
material?: string;
lastScannedAt?: string | null;
}
export interface SecurityState {
devices: NfcSecurityDevice[];
selectedId: number | null;
submitting: boolean;
errorMessage: string;
}
export interface SecurityApi {
freeze: (params: { id: number; uid?: string }) => Promise<unknown>;
unfreeze: (params: { id: number; uid?: string }) => Promise<unknown>;
}
export interface UseSecurityModule {
normalizeUid: (raw: string | null | undefined) => string;
validateNfcUid: (raw: string | null | undefined) => {
valid: boolean;
normalizedUid: string;
error?: string;
};
computeSecurityHash: (uid: string, salt?: string) => string;
verifySecurityHash: (uid: string, hash: string, salt?: string) => boolean;
createSecurityState: (initialDevices?: NfcSecurityDevice[]) => SecurityState;
selectDevice: (state: SecurityState, id: number) => void;
freezeDevice: (
state: SecurityState,
id: number,
api?: SecurityApi
) => Promise<NfcSecurityDevice>;
unfreezeDevice: (
state: SecurityState,
id: number,
api?: SecurityApi
) => Promise<NfcSecurityDevice>;
}
function loadUseSecurity(): UseSecurityModule {
return require(SECURITY_UTS) as UseSecurityModule;
}
const EMOJI_REGEX = /[\u2600-\u27BF\uFE0F]|[\uD83C-\uDBFF][\uDC00-\uDFFF]/;
describe('Point 13: 移动端 NFC 安全与远程挂失测试套件', () => {
beforeEach(() => {
jest.resetModules();
(global as { uni?: Record<string, unknown> }).uni = {
getLocale: () => 'zh-CN',
showToast: jest.fn(),
};
});
afterEach(() => {
delete (global as { uni?: Record<string, unknown> }).uni;
});
describe('1. 防伪 UID 校验 (validateNfcUid & normalizeUid)', () => {
it('1.1 规范化 UID,过滤冒号、空格、连字符并转大写', () => {
const { normalizeUid } = loadUseSecurity();
expect(normalizeUid('04:a3:2b:1c:8e:5f:90')).toBe('04A32B1C8E5F90');
expect(normalizeUid(' 04-a3-2b-1c-8e-5f-90 ')).toBe('04A32B1C8E5F90');
expect(normalizeUid('e1 2a 3b 4c')).toBe('E12A3B4C');
expect(normalizeUid('')).toBe('');
expect(normalizeUid(null)).toBe('');
});
it('1.2 正确识别合法的 7 字节 (14 字符) 与 4 字节 (8 字符) 标准 NFC UID', () => {
const { validateNfcUid } = loadUseSecurity();
const res7Byte = validateNfcUid('04:A3:2B:1C:8E:5F:90');
expect(res7Byte.valid).toBe(true);
expect(res7Byte.normalizedUid).toBe('04A32B1C8E5F90');
const res4Byte = validateNfcUid('E12A3B4C');
expect(res4Byte.valid).toBe(true);
expect(res4Byte.normalizedUid).toBe('E12A3B4C');
});
it('1.3 拒绝非法长度、非 Hex 字符或空值', () => {
const { validateNfcUid } = loadUseSecurity();
expect(validateNfcUid('').valid).toBe(false);
expect(validateNfcUid(' ').valid).toBe(false);
expect(validateNfcUid(null).valid).toBe(false);
// 包含非 Hex 字符
expect(validateNfcUid('04A32B1C8E5F9Z').valid).toBe(false);
// 长度不合规 (例如 10 位)
expect(validateNfcUid('04A32B1C8E').valid).toBe(false);
// 奇数长度
expect(validateNfcUid('04A32B1C8E5F9').valid).toBe(false);
});
});
describe('2. 防伪安全哈希查验 (computeSecurityHash & verifySecurityHash)', () => {
it('2.1 能为合法的 UID 计算防伪安全哈希', () => {
const { computeSecurityHash } = loadUseSecurity();
const hash1 = computeSecurityHash('04A32B1C8E5F90');
const hash2 = computeSecurityHash('04A32B1C8E5F90');
expect(typeof hash1).toBe('string');
expect(hash1.length).toBeGreaterThanOrEqual(16);
expect(hash1).toBe(hash2); // 确定性
});
it('2.2 正确校验防伪哈希,阻断篡改与伪造', () => {
const { computeSecurityHash, verifySecurityHash } = loadUseSecurity();
const uid = '04A32B1C8E5F90';
const hash = computeSecurityHash(uid, 'OMNICARD_SALT');
// 验签通过
expect(verifySecurityHash(uid, hash, 'OMNICARD_SALT')).toBe(true);
// 篡改 UID 校验失败
expect(verifySecurityHash('04A32B1C8E5F91', hash, 'OMNICARD_SALT')).toBe(false);
// 篡改 hash 校验失败
expect(verifySecurityHash(uid, hash + 'X', 'OMNICARD_SALT')).toBe(false);
// 伪造 salt 校验失败
expect(verifySecurityHash(uid, hash, 'WRONG_SALT')).toBe(false);
});
it('2.3 空 UID 或空 Hash 拒绝校验', () => {
const { verifySecurityHash } = loadUseSecurity();
expect(verifySecurityHash('', 'anyhash')).toBe(false);
expect(verifySecurityHash('04A32B1C8E5F90', '')).toBe(false);
});
});
describe('3. 设备列表与挂失/解挂状态流转 (freezeDevice & unfreezeDevice)', () => {
function getMockDevices(): NfcSecurityDevice[] {
return [
{
id: 101,
uid: '04A32B1C8E5F90',
cardId: 1,
status: 'active',
batchNo: 'B2026-01',
},
{
id: 102,
uid: '04B32B1C8E5F91',
cardId: 2,
status: 'frozen',
batchNo: 'B2026-02',
},
];
}
it('3.1 初始化状态与设备切换', () => {
const { createSecurityState, selectDevice } = loadUseSecurity();
const state = createSecurityState(getMockDevices());
expect(state.devices).toHaveLength(2);
expect(state.selectedId).toBe(101); // 默认选中首个设备
selectDevice(state, 102);
expect(state.selectedId).toBe(102);
});
it('3.2 freezeDevice 正常冻结状态流转并调用 API', async () => {
const { createSecurityState, freezeDevice } = loadUseSecurity();
const state = createSecurityState(getMockDevices());
const freezeApiMock = jest.fn().mockResolvedValue({ success: true });
const api: SecurityApi = {
freeze: freezeApiMock,
unfreeze: jest.fn(),
};
const updated = await freezeDevice(state, 101, api);
expect(freezeApiMock).toHaveBeenCalledWith({ id: 101, uid: '04A32B1C8E5F90' });
expect(updated.status).toBe('frozen');
expect(state.devices.find((d) => d.id === 101)?.status).toBe('frozen');
});
it('3.3 freezeDevice 重复冻结应防护抛出异常', async () => {
const { createSecurityState, freezeDevice } = loadUseSecurity();
const state = createSecurityState(getMockDevices());
// 设备 102 初始即为 frozen
await expect(freezeDevice(state, 102)).rejects.toThrow(/已处于挂失冻结状态/);
});
it('3.4 unfreezeDevice 正常解冻恢复状态流转并调用 API', async () => {
const { createSecurityState, unfreezeDevice } = loadUseSecurity();
const state = createSecurityState(getMockDevices());
const unfreezeApiMock = jest.fn().mockResolvedValue({ success: true });
const api: SecurityApi = {
freeze: jest.fn(),
unfreeze: unfreezeApiMock,
};
const updated = await unfreezeDevice(state, 102, api);
expect(unfreezeApiMock).toHaveBeenCalledWith({ id: 102, uid: '04B32B1C8E5F91' });
expect(updated.status).toBe('active');
expect(state.devices.find((d) => d.id === 102)?.status).toBe('active');
});
it('3.5 unfreezeDevice 对非冻结设备解冻应抛出异常', async () => {
const { createSecurityState, unfreezeDevice } = loadUseSecurity();
const state = createSecurityState(getMockDevices());
// 设备 101 初始为 active
await expect(unfreezeDevice(state, 101)).rejects.toThrow(/未处于挂失冻结状态/);
});
it('3.6 操作不存在的设备应抛出异常', async () => {
const { createSecurityState, freezeDevice, unfreezeDevice } = loadUseSecurity();
const state = createSecurityState(getMockDevices());
await expect(freezeDevice(state, 9999)).rejects.toThrow(/设备不存在/);
await expect(unfreezeDevice(state, 9999)).rejects.toThrow(/设备不存在/);
});
it('3.7 API 调用失败时应捕获错误并保留原始状态', async () => {
const { createSecurityState, freezeDevice } = loadUseSecurity();
const state = createSecurityState(getMockDevices());
const failingApi: SecurityApi = {
freeze: jest.fn().mockRejectedValue(new Error('网络超时')),
unfreeze: jest.fn(),
};
await expect(freezeDevice(state, 101, failingApi)).rejects.toThrow(/网络超时/);
// 状态未被篡改成 frozen
expect(state.devices.find((d) => d.id === 101)?.status).toBe('active');
});
});
describe('4. 界面与设计规范 (security.uvue & pages.json)', () => {
it('4.1 页面文件 security.uvue 存在且符合极简规范', () => {
expect(fs.existsSync(SECURITY_PAGE)).toBe(true);
const content = fs.readFileSync(SECURITY_PAGE, 'utf-8');
// 严格零 emoji 检查
expect(EMOJI_REGEX.test(content)).toBe(false);
// 单一主 CTA 检查 (单屏只允许 1 个主 CTA 按钮)
const primaryBtnCount = (content.match(/<button[^>]*class=["'][^"']*primary-cta-btn/g) || []).length;
expect(primaryBtnCount).toBe(1);
// 设计 Token 引用检查
expect(content).toContain('--oc-primary');
expect(content).toContain('--oc-border');
});
it('4.2 pages.json 中已正确注册 pages/owner/nfc/security 页面', () => {
expect(fs.existsSync(PAGES_JSON)).toBe(true);
const pagesContent = fs.readFileSync(PAGES_JSON, 'utf-8');
const cleanJson = pagesContent.replace(/\/\/[^\n\r]*/g, '');
const pagesConfig = JSON.parse(cleanJson);
const registered = pagesConfig.pages.some(
(p: { path: string }) => p.path === 'pages/owner/nfc/security'
);
expect(registered).toBe(true);
});
});
});