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 = {}; 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); }); }); });