import axios from 'axios'; import { CoolCommException } from '@cool-midway/core'; import { CrmSalesforceService } from '../../../src/modules/crm/service/salesforce'; describe('CrmSalesforceService.createLead', () => { let service: CrmSalesforceService; const validPayload = { FirstName: 'Jane', LastName: 'Doe', Email: 'jane@acme.com', Company: 'Acme', Phone: '+15551234567', Status: 'Open - Not Contacted', }; const validInstanceUrl = 'https://myorg.my.salesforce.com'; beforeEach(() => { service = new CrmSalesforceService(); jest.clearAllMocks(); }); afterEach(() => { jest.restoreAllMocks(); }); it('accessToken 为空时应抛出 CoolCommException,说明未配置 Salesforce', async () => { const postSpy = jest.spyOn(axios, 'post'); await expect( service.createLead('', validInstanceUrl, validPayload) ).rejects.toThrow(CoolCommException); await expect( service.createLead(' ', validInstanceUrl, validPayload) ).rejects.toThrow(/Salesforce/); expect(postSpy).not.toHaveBeenCalled(); }); it('instanceUrl 为空时应抛出 CoolCommException,说明未配置 Salesforce', async () => { const postSpy = jest.spyOn(axios, 'post'); await expect( service.createLead('sf-token', '', validPayload) ).rejects.toThrow(CoolCommException); await expect( service.createLead('sf-token', ' ', validPayload) ).rejects.toThrow(/Salesforce/); expect(postSpy).not.toHaveBeenCalled(); }); it('非法 instanceUrl(非 https 或主机名不符)应抛错且不发请求', async () => { const postSpy = jest.spyOn(axios, 'post'); await expect( service.createLead('sf-token', 'http://myorg.my.salesforce.com', validPayload) ).rejects.toThrow(CoolCommException); await expect( service.createLead('sf-token', 'https://evil.example.com', validPayload) ).rejects.toThrow(CoolCommException); await expect( service.createLead('sf-token', 'https://127.0.0.1', validPayload) ).rejects.toThrow(CoolCommException); expect(postSpy).not.toHaveBeenCalled(); }); it('有效时应向 Salesforce Lead API 发起 Bearer 请求并返回 id 与原始响应', async () => { const token = 'sf-access-token-xyz'; const raw = { id: '00Qxx000001Lead', success: true, errors: [] }; const postSpy = jest.spyOn(axios, 'post').mockResolvedValueOnce({ status: 201, data: raw, }); const result = await service.createLead(token, validInstanceUrl, validPayload); expect(postSpy).toHaveBeenCalledTimes(1); const [url, body, config] = postSpy.mock.calls[0]; expect(url).toBe( `${validInstanceUrl}/services/data/v59.0/sobjects/Lead` ); expect(body).toEqual(validPayload); expect(config?.headers?.Authorization).toBe(`Bearer ${token}`); expect(config?.timeout).toBe(8000); expect(result.id).toBe('00Qxx000001Lead'); expect(result.raw).toEqual(raw); }); it('force.com 主机名应被允许', async () => { const instanceUrl = 'https://na1.salesforce.com'; const forceUrl = 'https://custom.force.com'; const token = 'sf-token'; const raw = { id: '00Qxx000002', success: true }; const postSpy = jest .spyOn(axios, 'post') .mockResolvedValueOnce({ status: 201, data: raw }) .mockResolvedValueOnce({ status: 201, data: raw }); await service.createLead(token, instanceUrl, validPayload); await service.createLead(token, forceUrl, validPayload); expect(postSpy).toHaveBeenCalledTimes(2); expect(postSpy.mock.calls[0][0]).toBe( `${instanceUrl}/services/data/v59.0/sobjects/Lead` ); expect(postSpy.mock.calls[1][0]).toBe( `${forceUrl}/services/data/v59.0/sobjects/Lead` ); }); it('对方返回 4xx 时应抛出包含状态码与错误信息的异常,不返回 success:true', async () => { const axiosError = Object.assign(new Error('Request failed with status code 400'), { isAxiosError: true, response: { status: 400, data: [ { message: 'Required fields are missing: [LastName]', errorCode: 'REQUIRED_FIELD_MISSING', }, ], }, }); jest.spyOn(axios, 'post').mockRejectedValueOnce(axiosError); await expect( service.createLead('sf-valid', validInstanceUrl, validPayload) ).rejects.toThrow(/400|Required fields are missing/); }); });