98 lines
3.2 KiB
TypeScript
98 lines
3.2 KiB
TypeScript
import { clearIdempotencyStoreForTests } from '@/lib/demo/idempotency';
|
|
import type { LeadDestinationAdapter } from '@/lib/demo/adapters';
|
|
import { submitDemoLead } from '@/lib/demo/service';
|
|
import type { DemoLead } from '@/lib/demo/schema';
|
|
import type { IntegrationEnvironment } from '@/lib/integrations/environment';
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
const environment: IntegrationEnvironment = {
|
|
NODE_ENV: 'test',
|
|
NEXT_PUBLIC_DEMO_ENABLED: true,
|
|
DEMO_SUBMISSION_MODE: 'local',
|
|
DEMO_REQUEST_TIMEOUT_MS: 250,
|
|
DEMO_IDEMPOTENCY_TTL_SECONDS: 900,
|
|
DEMO_TEST_SCENARIOS_ENABLED: true,
|
|
DEMO_LOCAL_TEST_MODE: false,
|
|
NEXT_PUBLIC_ANALYTICS_MODE: 'disabled',
|
|
};
|
|
|
|
const lead: DemoLead = {
|
|
fullName: 'Avery Example',
|
|
workEmail: 'avery@example.test',
|
|
company: 'Example Rental Lab',
|
|
fleetSize: '10-24',
|
|
preferredLanguage: 'en',
|
|
idempotencyKey: '6b4992d5-d88e-4e44-9b66-adc18fd8ee37',
|
|
source: 'hero',
|
|
};
|
|
|
|
beforeEach(clearIdempotencyStoreForTests);
|
|
|
|
describe('demo submission orchestration', () => {
|
|
it('confirms acceptance before returning success', async () => {
|
|
const adapter: LeadDestinationAdapter = {
|
|
name: 'test-success',
|
|
submitLead: vi.fn(async () => ({
|
|
ok: true as const,
|
|
externalId: 'local_test',
|
|
acceptedAt: '2026-06-25T00:00:00.000Z',
|
|
mode: 'local' as const,
|
|
})),
|
|
};
|
|
const result = await submitDemoLead(lead, { environment, adapter });
|
|
expect(result.ok).toBe(true);
|
|
expect(adapter.submitLead).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it('coalesces repeated transport with the same idempotency key', async () => {
|
|
const adapter: LeadDestinationAdapter = {
|
|
name: 'test-idempotent',
|
|
submitLead: vi.fn(async () => ({
|
|
ok: true as const,
|
|
externalId: 'local_once',
|
|
acceptedAt: '2026-06-25T00:00:00.000Z',
|
|
mode: 'local' as const,
|
|
})),
|
|
};
|
|
const [first, second] = await Promise.all([
|
|
submitDemoLead(lead, { environment, adapter }),
|
|
submitDemoLead(lead, { environment, adapter }),
|
|
]);
|
|
expect(first).toEqual(second);
|
|
expect(adapter.submitLead).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it('normalizes adapter failures without exposing vendor bodies', async () => {
|
|
const adapter: LeadDestinationAdapter = {
|
|
name: 'test-error',
|
|
submitLead: async () => ({
|
|
ok: false,
|
|
category: 'integration',
|
|
retryable: true,
|
|
code: 'normalized_failure',
|
|
}),
|
|
};
|
|
const result = await submitDemoLead(lead, { environment, adapter });
|
|
expect(result).toMatchObject({
|
|
ok: false,
|
|
category: 'integration',
|
|
formErrorCode: 'normalized_failure',
|
|
retryable: true,
|
|
});
|
|
});
|
|
|
|
it('bounds a hung integration with a timeout', async () => {
|
|
const adapter: LeadDestinationAdapter = {
|
|
name: 'test-timeout',
|
|
submitLead: (_lead, context) =>
|
|
new Promise((_resolve, reject) => {
|
|
context.signal.addEventListener('abort', () =>
|
|
reject(new DOMException('Aborted', 'AbortError')),
|
|
);
|
|
}),
|
|
};
|
|
const result = await submitDemoLead(lead, { environment, adapter });
|
|
expect(result).toMatchObject({ ok: false, category: 'timeout', retryable: true });
|
|
});
|
|
});
|