补齐平台订阅、外部登录、CRM 同步与用户端页面;Nginx 转发用户端接口,登录页使用 Google 与 Apple 标志。 Co-authored-by: Cursor <cursoragent@cursor.com>
230 行
7.0 KiB
TypeScript
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);
|
|
});
|
|
});
|
|
});
|