文件
OmniCard/backend/test/modules/crm/lead-external-sync.test.ts
wikiboss 62e0ae4435 feat: 同步管理端、用户端与后端业务,并接通线上 Web 接口
补齐平台订阅、外部登录、CRM 同步与用户端页面;Nginx 转发用户端接口,登录页使用 Google 与 Apple 标志。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-23 18:01:47 +08:00

230 行
7.0 KiB
TypeScript

import { CoolCommException } from '@cool-midway/core';
import { AdminCrmLeadController } from '../../../src/modules/crm/controller/admin/lead';
import { CrmSyncService } from '../../../src/modules/crm/service/sync';
import { CrmLeadEntity } from '../../../src/modules/crm/entity/lead';
describe('AdminCrmLeadController.syncExternal 外部 CRM 真实同步', () => {
let controller: AdminCrmLeadController;
let syncService: CrmSyncService;
let mockLeadRepo: { findOne: jest.Mock };
let hubspotService: { createContact: jest.Mock };
let salesforceService: { createLead: jest.Mock };
const mockLead = {
id: 101,
name: 'John Doe',
email: 'john.doe@acme.com',
phone: '+1-555-0199',
company: 'Acme Global Corp',
status: 'new',
} as unknown as CrmLeadEntity;
const envKeys = [
'HUBSPOT_ACCESS_TOKEN',
'SALESFORCE_ACCESS_TOKEN',
'SALESFORCE_INSTANCE_URL',
] as const;
const savedEnv: Record<string, string | undefined> = {};
beforeEach(() => {
for (const key of envKeys) {
savedEnv[key] = process.env[key];
delete process.env[key];
}
syncService = new CrmSyncService();
mockLeadRepo = { findOne: jest.fn().mockResolvedValue(mockLead) };
hubspotService = { createContact: jest.fn() };
salesforceService = { createLead: jest.fn() };
controller = new AdminCrmLeadController();
(controller as unknown as { crmLeadEntity: unknown }).crmLeadEntity =
mockLeadRepo;
(controller as unknown as { crmSyncService: unknown }).crmSyncService =
syncService;
(controller as unknown as { crmHubSpotService: unknown }).crmHubSpotService =
hubspotService;
(
controller as unknown as { crmSalesforceService: unknown }
).crmSalesforceService = salesforceService;
});
afterEach(() => {
for (const key of envKeys) {
const prev = savedEnv[key];
if (prev === undefined) {
delete process.env[key];
} else {
process.env[key] = prev;
}
}
jest.restoreAllMocks();
});
describe('target=hubspot', () => {
it('无 HUBSPOT_ACCESS_TOKEN 时不调用客户端,返回 synced:false', async () => {
const res = await controller.syncExternal({
id: 101,
target: 'hubspot',
});
expect(hubspotService.createContact).not.toHaveBeenCalled();
expect(res.code).toBe(1000);
expect(res.data).toMatchObject({
target: 'hubspot',
leadId: 101,
synced: false,
reason: '未配置 HubSpot',
payload: {
properties: {
email: 'john.doe@acme.com',
firstname: 'John',
lastname: 'Doe',
phone: '+1-555-0199',
company: 'Acme Global Corp',
lifecyclestage: 'lead',
},
},
});
});
it('有 token 时应调用 createContact 并返回 synced:true 与 id', async () => {
process.env.HUBSPOT_ACCESS_TOKEN = 'pat-test-token';
hubspotService.createContact.mockResolvedValueOnce({
id: 'hs-42',
raw: { id: 'hs-42' },
});
const res = await controller.syncExternal({
id: 101,
target: 'hubspot',
});
expect(hubspotService.createContact).toHaveBeenCalledTimes(1);
expect(hubspotService.createContact).toHaveBeenCalledWith(
'pat-test-token',
{
properties: {
email: 'john.doe@acme.com',
firstname: 'John',
lastname: 'Doe',
phone: '+1-555-0199',
company: 'Acme Global Corp',
lifecyclestage: 'lead',
},
}
);
expect(res.code).toBe(1000);
expect(res.data).toMatchObject({
target: 'hubspot',
leadId: 101,
synced: true,
id: 'hs-42',
});
});
it('客户端抛错时应向上抛出,不返回 synced:true', async () => {
process.env.HUBSPOT_ACCESS_TOKEN = 'pat-test-token';
hubspotService.createContact.mockRejectedValueOnce(
new CoolCommException('HubSpot API 错误 400: bad', 400)
);
await expect(
controller.syncExternal({ id: 101, target: 'hubspot' })
).rejects.toThrow(CoolCommException);
expect(hubspotService.createContact).toHaveBeenCalledTimes(1);
});
});
describe('target=salesforce', () => {
it('缺凭证时不调用客户端,返回 synced:false', async () => {
const res = await controller.syncExternal({
id: 101,
target: 'salesforce',
});
expect(salesforceService.createLead).not.toHaveBeenCalled();
expect(res.code).toBe(1000);
expect(res.data).toMatchObject({
target: 'salesforce',
leadId: 101,
synced: false,
reason: '未配置 Salesforce',
payload: {
FirstName: 'John',
LastName: 'Doe',
Email: 'john.doe@acme.com',
Company: 'Acme Global Corp',
Phone: '+1-555-0199',
Status: 'Open - Not Contacted',
},
});
});
it('仅有 token 缺 instanceUrl 时也不调用客户端', async () => {
process.env.SALESFORCE_ACCESS_TOKEN = 'sf-token';
const res = await controller.syncExternal({
id: 101,
target: 'salesforce',
});
expect(salesforceService.createLead).not.toHaveBeenCalled();
expect(res.data).toMatchObject({
synced: false,
reason: '未配置 Salesforce',
});
});
it('凭证齐全时应调用 createLead 并返回 synced:true 与 id', async () => {
process.env.SALESFORCE_ACCESS_TOKEN = 'sf-token';
process.env.SALESFORCE_INSTANCE_URL = 'https://example.my.salesforce.com';
salesforceService.createLead.mockResolvedValueOnce({
id: '00QsfLead1',
raw: { id: '00QsfLead1', success: true },
});
const res = await controller.syncExternal({
id: 101,
target: 'salesforce',
});
expect(salesforceService.createLead).toHaveBeenCalledTimes(1);
expect(salesforceService.createLead).toHaveBeenCalledWith(
'sf-token',
'https://example.my.salesforce.com',
{
FirstName: 'John',
LastName: 'Doe',
Email: 'john.doe@acme.com',
Company: 'Acme Global Corp',
Phone: '+1-555-0199',
Status: 'Open - Not Contacted',
}
);
expect(res.code).toBe(1000);
expect(res.data).toMatchObject({
target: 'salesforce',
leadId: 101,
synced: true,
id: '00QsfLead1',
});
});
it('客户端抛错时应向上抛出,不返回 synced:true', async () => {
process.env.SALESFORCE_ACCESS_TOKEN = 'sf-token';
process.env.SALESFORCE_INSTANCE_URL = 'https://example.my.salesforce.com';
salesforceService.createLead.mockRejectedValueOnce(
new CoolCommException('Salesforce API 错误 401: unauthorized', 401)
);
await expect(
controller.syncExternal({ id: 101, target: 'salesforce' })
).rejects.toThrow(CoolCommException);
expect(salesforceService.createLead).toHaveBeenCalledTimes(1);
});
});
});