import { ComplianceGdprService, maskIpAddress, anonymizePiiData, GdprUserDataBundle, } from '../../../src/modules/crm/service/compliance'; describe('Point 53: 国际合规 GDPR / CCPA 访客数据匿名化脱敏与被遗忘权导出', () => { let service: ComplianceGdprService; beforeEach(() => { service = new ComplianceGdprService(); }); describe('1. maskIpAddress (IP 地址合规脱敏掩码规范)', () => { it('IPv4 地址末尾一段应被严格掩码为 xxx', () => { expect(maskIpAddress('192.168.1.100')).toBe('192.168.1.xxx'); expect(maskIpAddress('8.8.8.8')).toBe('8.8.8.xxx'); }); it('IPv6 地址后半段应被规范掩码', () => { expect(maskIpAddress('2001:0db8:85a3:0000:0000:8a2e:0370:7334')).toBe( '2001:0db8:85a3:xxxx:xxxx:xxxx:xxxx:xxxx' ); }); it('非法或空 IP 应安全返回未知掩码', () => { expect(maskIpAddress('')).toBe('unknown'); expect(maskIpAddress(undefined)).toBe('unknown'); }); }); describe('2. anonymizePiiData (可识别个人隐私数据匿名化清洗)', () => { it('应将姓名、手机号、邮箱与备注彻底清除并标记为已脱敏', () => { const rawLead = { id: 101, name: 'Hans Zimmer', email: 'hans@berlin-music.de', phone: '+49 170 1234567', company: 'Berlin Music AG', notes: 'Interested in 500 VIP NFC cards', }; const cleaned = anonymizePiiData(rawLead); expect(cleaned.id).toBe(101); expect(cleaned.name).toBe('[GDPR_DELETED]'); expect(cleaned.email).toBe('deleted-101@privacy.anonymized'); expect(cleaned.phone).toBe(''); expect(cleaned.notes).toBe(''); expect(cleaned.company).toBe('Berlin Music AG'); // 保留无识别性企业属性用于统计 }); }); describe('3. ComplianceGdprService 数据导出包与被遗忘权销毁', () => { it('数据主体请求导出时应打包符合 GDPR Article 20 的 JSON 便携数据包', async () => { const bundle = await service.exportPersonalDataBundle('visitor-de-001', { profile: { name: 'Klaus Mueller', email: 'klaus@munich.de' }, visitLogs: [{ page: '/card/alex', staySeconds: 45 }], }); expect(bundle.subjectId).toBe('visitor-de-001'); expect(bundle.exportedAt).toBeDefined(); expect(bundle.complianceStandard).toBe('GDPR_ARTICLE_20'); expect(bundle.dataPayload.profile.name).toBe('Klaus Mueller'); }); it('执行被遗忘权销毁应返回销毁证书与合规哈希凭据', async () => { const receipt = await service.executeRightToBeForgotten('visitor-de-001'); expect(receipt.subjectId).toBe('visitor-de-001'); expect(receipt.action).toBe('PURGED_AND_ANONYMIZED'); expect(receipt.certificateHash).toHaveLength(64); }); }); describe('4. purgeVisitorPersonalData 数据库级合规擦除执行', () => { it('根据 email 或 leadId 执行级联清洗,将线索脱敏且掩码历史雷达 IP', async () => { const mockLead = { id: 105, name: 'Pierre Dupont', email: 'pierre@paris.fr', phone: '+33 6 12345678', company: 'Paris Luxury', notes: 'VIP customer', }; const mockTracks = [ { id: 1, visitorId: 'vis_fr_99', ip: '193.252.122.103', stayDurationMs: 5000 }, { id: 2, visitorId: 'vis_fr_99', ip: '82.120.45.67', stayDurationMs: 8000 }, ]; const savedLeads: any[] = []; const savedTracks: any[] = []; service.crmLeadEntity = { findOne: jest.fn().mockResolvedValue(mockLead), save: jest.fn().mockImplementation((rec) => { savedLeads.push(rec); return Promise.resolve(rec); }), } as any; service.crmTrackEntity = { find: jest.fn().mockResolvedValue(mockTracks), save: jest.fn().mockImplementation((tracks) => { savedTracks.push(...(Array.isArray(tracks) ? tracks : [tracks])); return Promise.resolve(tracks); }), } as any; const receipt = await service.purgeVisitorPersonalData({ leadId: 105, email: 'pierre@paris.fr', visitorId: 'vis_fr_99', }); expect(receipt.subjectId).toBe('pierre@paris.fr'); expect(receipt.certificateHash).toHaveLength(64); // 验证线索已被彻底清洗 expect(savedLeads[0].name).toBe('[GDPR_DELETED]'); expect(savedLeads[0].email).toBe('deleted-105@privacy.anonymized'); expect(savedLeads[0].phone).toBe(''); // 验证足迹 IP 已被掩码 expect(savedTracks[0].ip).toBe('193.252.122.xxx'); expect(savedTracks[1].ip).toBe('82.120.45.xxx'); }); }); describe('5. AdminCrmComplianceController 控制器接口契约', () => { let controller: any; beforeEach(() => { controller = new (require('../../../src/modules/crm/controller/admin/compliance').AdminCrmComplianceController)(); controller.complianceGdprService = service; controller.ok = (data: any) => ({ code: 1000, message: 'success', data }); }); it('POST /erasure 缺少主体参数应抛出 CoolCommException', async () => { await expect(controller.erasure({})).rejects.toThrow(); }); it('POST /erasure 传入合法主体参数应成功返回销毁收据', async () => { service.purgeVisitorPersonalData = jest.fn().mockResolvedValue({ subjectId: 'vis_test', action: 'PURGED_AND_ANONYMIZED', executedAt: new Date().toISOString(), certificateHash: 'hash_abc_123', }); const res = await controller.erasure({ visitorId: 'vis_test' }); expect(res.code).toBe(1000); expect(res.data.certificateHash).toBe('hash_abc_123'); }); }); });