272 lines
12 KiB
TypeScript
272 lines
12 KiB
TypeScript
/**
|
|
* @file external-integrations.test.ts
|
|
* @description Contract/mock tests for all external integrations.
|
|
* These tests validate the SHAPE and BEHAVIOUR of each mock so that
|
|
* when real APIs are wired, only the mock needs to be swapped.
|
|
*
|
|
* Integrations covered:
|
|
* 1. SAP OData — Dealer Code generation (mockGenerateSapCodes, mockSyncDealerStatusToSap)
|
|
* 2. Google Calendar — Interview invite scheduling (mockScheduleMeeting)
|
|
* 3. WhatsApp — Notification delivery (mockSendWhatsApp)
|
|
* 4. Gemini AI — Panel evaluation summary (mockGenerateAiSummary)
|
|
*
|
|
* SRS Coverage:
|
|
* §6.17.3.1 — SAP OData API for Sales/Service/GMA/Gear codes
|
|
* §6.9.2 — Google Calendar invites for all participants
|
|
* §1.1.1 — WhatsApp as supported notification channel
|
|
* §6.10.4 — AI-assisted recommendation via Gemini API
|
|
*/
|
|
|
|
import { ExternalMocksService } from '../common/utils/externalMocks.service.js';
|
|
|
|
// ─── 1. SAP Dealer Code Generation ───────────────────────────────────────────
|
|
|
|
describe('SAP Mock — Dealer Code Generation (SRS §6.17.3.1)', () => {
|
|
it('TC-SAP-01: returns success=true', async () => {
|
|
const result = await ExternalMocksService.mockGenerateSapCodes('app-001');
|
|
expect(result.success).toBe(true);
|
|
});
|
|
|
|
it('TC-SAP-02: returns salesCode in SLS-XXXX format', async () => {
|
|
const { data } = await ExternalMocksService.mockGenerateSapCodes('app-001');
|
|
expect(data.salesCode).toMatch(/^SLS-\d{4}$/);
|
|
});
|
|
|
|
it('TC-SAP-03: returns serviceCode in SRV-XXXX format', async () => {
|
|
const { data } = await ExternalMocksService.mockGenerateSapCodes('app-001');
|
|
expect(data.serviceCode).toMatch(/^SRV-\d{4}$/);
|
|
});
|
|
|
|
it('TC-SAP-04: returns gmaCode in GMA-XXXX format (Genuine Motorcycle Accessories)', async () => {
|
|
const { data } = await ExternalMocksService.mockGenerateSapCodes('app-001');
|
|
expect(data.gmaCode).toMatch(/^GMA-\d{4}$/);
|
|
});
|
|
|
|
it('TC-SAP-05: returns gearCode in GER-XXXX format', async () => {
|
|
const { data } = await ExternalMocksService.mockGenerateSapCodes('app-001');
|
|
expect(data.gearCode).toMatch(/^GER-\d{4}$/);
|
|
});
|
|
|
|
it('TC-SAP-06: returns a non-empty sapMasterId', async () => {
|
|
const { data } = await ExternalMocksService.mockGenerateSapCodes('app-001');
|
|
expect(data.sapMasterId).toBeTruthy();
|
|
expect(typeof data.sapMasterId).toBe('string');
|
|
});
|
|
|
|
it('TC-SAP-07: each call generates unique codes (no collisions)', async () => {
|
|
const r1 = await ExternalMocksService.mockGenerateSapCodes('app-001');
|
|
const r2 = await ExternalMocksService.mockGenerateSapCodes('app-002');
|
|
// Codes are random; while collision is statistically possible, sapMasterIds must differ
|
|
expect(r1.data.sapMasterId).not.toBe(r2.data.sapMasterId);
|
|
});
|
|
|
|
it('TC-SAP-08: returns all 4 code types in a single call (no missing fields)', async () => {
|
|
const { data } = await ExternalMocksService.mockGenerateSapCodes('app-001');
|
|
expect(data).toHaveProperty('salesCode');
|
|
expect(data).toHaveProperty('serviceCode');
|
|
expect(data).toHaveProperty('gmaCode');
|
|
expect(data).toHaveProperty('gearCode');
|
|
expect(data).toHaveProperty('sapMasterId');
|
|
});
|
|
});
|
|
|
|
// ─── SAP Status Sync ─────────────────────────────────────────────────────────
|
|
|
|
describe('SAP Mock — Dealer Status Synchronization', () => {
|
|
it('TC-SAP-09: mockSyncDealerStatusToSap returns success=true', async () => {
|
|
const result = await ExternalMocksService.mockSyncDealerStatusToSap('SLS-1234', 'Active');
|
|
expect(result.success).toBe(true);
|
|
});
|
|
|
|
it('TC-SAP-10: sync result includes a sapTransactionId and ISO timestamp', async () => {
|
|
const result = await ExternalMocksService.mockSyncDealerStatusToSap('SLS-1234', 'Terminated');
|
|
expect(result.sapTransactionId).toMatch(/^SAP-TX-/);
|
|
expect(new Date(result.timestamp).toISOString()).toBe(result.timestamp);
|
|
});
|
|
});
|
|
|
|
// ─── SAP Financial Dues ───────────────────────────────────────────────────────
|
|
|
|
describe('SAP Mock — Financial Dues (F&F Context)', () => {
|
|
it('TC-SAP-11: mockGetFinancialDuesFromSap returns financial data for a dealer', async () => {
|
|
const result = await ExternalMocksService.mockGetFinancialDuesFromSap('SLS-5678');
|
|
expect(result.success).toBe(true);
|
|
expect(result.data).toHaveProperty('outstandingInvoices');
|
|
expect(result.data).toHaveProperty('securityDeposit');
|
|
expect(result.data).toHaveProperty('creditLimit');
|
|
expect(result.data).toHaveProperty('pendingClaims');
|
|
});
|
|
|
|
it('TC-SAP-12: returned financial amounts are positive numbers', async () => {
|
|
const { data } = await ExternalMocksService.mockGetFinancialDuesFromSap('SLS-5678');
|
|
expect(data.securityDeposit).toBeGreaterThan(0);
|
|
expect(data.creditLimit).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
|
|
// ─── 2. Google Calendar Mock ──────────────────────────────────────────────────
|
|
|
|
describe('Google Calendar Mock — Interview Scheduling (SRS §6.9.2)', () => {
|
|
const interviewPayload = {
|
|
type: 'Level 1 Interview',
|
|
scheduledAt: '2026-05-15T10:00:00Z',
|
|
participants: ['zm@re.com', 'rbm@re.com', 'applicant@gmail.com'],
|
|
mode: 'Virtual',
|
|
applicationId: 'app-uuid-001',
|
|
};
|
|
|
|
it('TC-CAL-01: mockScheduleMeeting returns success=true', async () => {
|
|
const result = await ExternalMocksService.mockScheduleMeeting(interviewPayload);
|
|
expect(result.success).toBe(true);
|
|
});
|
|
|
|
it('TC-CAL-02: returns a Google Meet link URL', async () => {
|
|
const result = await ExternalMocksService.mockScheduleMeeting(interviewPayload);
|
|
expect(result.meetLink).toContain('meet.google.com');
|
|
});
|
|
|
|
it('TC-CAL-03: returns a non-empty calendarEventId (UUID format)', async () => {
|
|
const result = await ExternalMocksService.mockScheduleMeeting(interviewPayload);
|
|
expect(result.calendarEventId).toMatch(
|
|
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
|
|
);
|
|
});
|
|
|
|
it('TC-CAL-04: each scheduling call returns a unique meetLink (no duplicate links)', async () => {
|
|
const r1 = await ExternalMocksService.mockScheduleMeeting(interviewPayload);
|
|
const r2 = await ExternalMocksService.mockScheduleMeeting({ ...interviewPayload, type: 'Level 2 Interview' });
|
|
expect(r1.meetLink).not.toBe(r2.meetLink);
|
|
});
|
|
|
|
it('TC-CAL-05: mock works for Level 1, Level 2, and Level 3 interview types', async () => {
|
|
for (const type of ['Level 1 Interview', 'Level 2 Interview', 'Level 3 Interview']) {
|
|
const result = await ExternalMocksService.mockScheduleMeeting({ ...interviewPayload, type });
|
|
expect(result.success).toBe(true);
|
|
}
|
|
});
|
|
|
|
it('TC-CAL-06: mock works for both Virtual and Physical interview modes', async () => {
|
|
const virtualResult = await ExternalMocksService.mockScheduleMeeting({ ...interviewPayload, mode: 'Virtual' });
|
|
const physicalResult = await ExternalMocksService.mockScheduleMeeting({ ...interviewPayload, mode: 'Physical' });
|
|
expect(virtualResult.success).toBe(true);
|
|
expect(physicalResult.success).toBe(true);
|
|
});
|
|
});
|
|
|
|
// ─── 3. WhatsApp Mock ─────────────────────────────────────────────────────────
|
|
|
|
describe('WhatsApp Mock — Notification Delivery (SRS §1.1.1)', () => {
|
|
it('TC-WA-01: mockSendWhatsApp resolves successfully', async () => {
|
|
const result = await ExternalMocksService.mockSendWhatsApp(
|
|
'+919876543210',
|
|
'Questionnaire reminder for your dealership application.'
|
|
);
|
|
expect(result.success).toBe(true);
|
|
});
|
|
|
|
it('TC-WA-02: returned messageId starts with WA- prefix', async () => {
|
|
const result = await ExternalMocksService.mockSendWhatsApp(
|
|
'+919876543210',
|
|
'Your resignation request has been received.'
|
|
);
|
|
expect(result.messageId).toMatch(/^WA-/);
|
|
});
|
|
|
|
it('TC-WA-03: LOI-related messages must NOT be sent via WhatsApp (SRS §1.1.2)', () => {
|
|
// Contract test: LOI channel must not include WhatsApp
|
|
const loiChannels = ['email', 'system']; // per SRS §1.1.2
|
|
expect(loiChannels).not.toContain('whatsapp');
|
|
});
|
|
|
|
it('TC-WA-04: questionnaire reminders MUST include WhatsApp channel (SRS §1.1.1)', () => {
|
|
const questionnaireChannels = ['email', 'whatsapp']; // per SRS §1.1.1
|
|
expect(questionnaireChannels).toContain('whatsapp');
|
|
});
|
|
|
|
it('TC-WA-05: resignation submission acknowledgement includes WhatsApp channel (SRS §1.1.5)', () => {
|
|
const resignationChannels = ['email', 'whatsapp'];
|
|
expect(resignationChannels).toContain('whatsapp');
|
|
});
|
|
|
|
it('TC-WA-06: each WhatsApp call generates a unique messageId', async () => {
|
|
const r1 = await ExternalMocksService.mockSendWhatsApp('+91111', 'Msg 1');
|
|
const r2 = await ExternalMocksService.mockSendWhatsApp('+91222', 'Msg 2');
|
|
expect(r1.messageId).not.toBe(r2.messageId);
|
|
});
|
|
});
|
|
|
|
// ─── 4. Gemini AI Mock ────────────────────────────────────────────────────────
|
|
|
|
describe('Gemini AI Mock — Panel Evaluation Summary (SRS §6.10.4)', () => {
|
|
const allApprovedFeedback = [
|
|
{ recommendation: 'Approve', score: 85 },
|
|
{ recommendation: 'Approve', score: 90 },
|
|
{ recommendation: 'Approve', score: 88 },
|
|
];
|
|
|
|
const mixedFeedback = [
|
|
{ recommendation: 'Approve', score: 80 },
|
|
{ recommendation: 'Approve', score: 75 },
|
|
{ recommendation: 'Reject', score: 40 },
|
|
];
|
|
|
|
const allRejectFeedback = [
|
|
{ recommendation: 'Reject', score: 30 },
|
|
{ recommendation: 'Reject', score: 25 },
|
|
];
|
|
|
|
it('TC-AI-01: mockGenerateAiSummary returns success=true', async () => {
|
|
const result = await ExternalMocksService.mockGenerateAiSummary('app-001', allApprovedFeedback);
|
|
expect(result.success).toBe(true);
|
|
});
|
|
|
|
it('TC-AI-02: returns a non-empty summary string', async () => {
|
|
const result = await ExternalMocksService.mockGenerateAiSummary('app-001', allApprovedFeedback);
|
|
expect(typeof result.summary).toBe('string');
|
|
expect(result.summary.length).toBeGreaterThan(10);
|
|
});
|
|
|
|
it('TC-AI-03: unanimous approval panel produces positive consensus summary', async () => {
|
|
const result = await ExternalMocksService.mockGenerateAiSummary('app-001', allApprovedFeedback);
|
|
// Unanimous approval → strong consensus message
|
|
expect(result.summary).toMatch(/strong consensus|strong candidate|exceptional/i);
|
|
});
|
|
|
|
it('TC-AI-04: majority approval produces cautiously positive summary', async () => {
|
|
const result = await ExternalMocksService.mockGenerateAiSummary('app-001', mixedFeedback);
|
|
// Majority but not all → cautious message
|
|
expect(result.summary).toMatch(/majority|recommend approval|monitored/i);
|
|
});
|
|
|
|
it('TC-AI-05: rejection-leaning panel produces concern-focused summary', async () => {
|
|
const result = await ExternalMocksService.mockGenerateAiSummary('app-001', allRejectFeedback);
|
|
expect(result.summary).toMatch(/divided|rejection|concern/i);
|
|
});
|
|
|
|
it('TC-AI-06: empty feedback list produces a valid (non-crashing) summary', async () => {
|
|
const result = await ExternalMocksService.mockGenerateAiSummary('app-001', []);
|
|
expect(result.success).toBe(true);
|
|
expect(result.summary).toBeDefined();
|
|
});
|
|
|
|
it('TC-AI-07: summary is presentable to NBH — 2 to 3 sentences', async () => {
|
|
const result = await ExternalMocksService.mockGenerateAiSummary('app-001', allApprovedFeedback);
|
|
// SRS §6.10.4: "two- to three-line summarized recommendation"
|
|
const sentenceCount = result.summary.split(/[.!?]/).filter(Boolean).length;
|
|
expect(sentenceCount).toBeGreaterThanOrEqual(1);
|
|
expect(sentenceCount).toBeLessThanOrEqual(5);
|
|
});
|
|
|
|
it('TC-AI-08: mixed feedback with exactly half approvals falls into majority branch', async () => {
|
|
const halfHalf = [
|
|
{ recommendation: 'Approve', score: 75 },
|
|
{ recommendation: 'Reject', score: 40 },
|
|
];
|
|
// 1 approve out of 2 = not > total/2, so should fall into rejection branch
|
|
const result = await ExternalMocksService.mockGenerateAiSummary('app-001', halfHalf);
|
|
expect(result.success).toBe(true);
|
|
// The summary should NOT be the "strong consensus" one
|
|
expect(result.summary).not.toMatch(/strong consensus|exceptional/i);
|
|
});
|
|
});
|