leads.test.ts
Language TypeScript3.4 KB81 lines
import { describe, expect, it } from 'vitest'import { isLikelyNoise, normalizeLead, type RawLead } from '../src/leads.js'import { BitrixClient, BitrixOperationTimeError, BitrixRateLimitError } from '../src/http.js'describe('lead noise detection', () => {// Every one of these has the shape of a row in the live open-lead list.it.each([['Security alert: new sign-in to your account', 'automated security notification'],['15550100123 - Incoming call (unknown)', 'call log entry'],['Complete CRM form "Website Chat"', 'unattended form submission'],['Blue guest #19 - Meridian Display Support', 'support chat guest record'],])('flags %s', (title, reason) => {const r = isLikelyNoise(title)expect(r.noise).toBe(true)expect(r.reason).toBe(reason)})it.each(['Re: Event kit order for Lakeside Robotics Club','Re: Custom flag order for Pinecrest Community Center','RE: [EXTERNAL]Meridian Display | Setup instructions','MD2641873',])('does not flag the real lead %s', (title) => {expect(isLikelyNoise(title).noise).toBe(false)})it('flags rather than drops, so the count stays honest', () => {const l = normalizeLead({ ID: '1', TITLE: 'Security alert: something' } as RawLead)expect(l.noise).toBe(true)expect(l.title).toBe('Security alert: something') // still present})})describe('lead normalization', () => {it('falls back to name fields when TITLE is absent', () => {expect(normalizeLead({ ID: '1', NAME: 'Casey', LAST_NAME: 'Morgan' } as RawLead).title).toBe('Casey Morgan')})it('coerces OPPORTUNITY, which arrives as a string', () => {expect(normalizeLead({ ID: '1', TITLE: 'x', OPPORTUNITY: '1250.00' } as RawLead).amount).toBe(1250)expect(normalizeLead({ ID: '1', TITLE: 'x' } as RawLead).amount).toBe(0)})})describe('the two Bitrix limits are handled differently', () => {const client = (status: number, body: unknown) =>new BitrixClient({webhookUrl: 'https://x.bitrix24.com/rest/1/k/',maxRetries: 1,sleep: async () => {},rps: 1000,fetchImpl: (async () =>new Response(JSON.stringify(body), {status,headers: { 'Content-Type': 'application/json' },})) as unknown as typeof fetch,})it('treats 503 QUERY_LIMIT_EXCEEDED as the rate limit', async () => {await expect(client(503, { error: 'QUERY_LIMIT_EXCEEDED' }).call('crm.lead.list')).rejects.toBeInstanceOf(BitrixRateLimitError,)})it('treats 429 as the per-method execution-time limit, NOT the rate limit', async () => {// Inverted from most APIs, and from @meridian/ninety-client. Retrying now cannot help.const c = client(429, { error: 'OPERATION_TIME_LIMIT', time: { operating_reset_at: 1786240000 } })await expect(c.call('crm.lead.list')).rejects.toBeInstanceOf(BitrixOperationTimeError)})it('suspends only the offending method', async () => {const c = client(429, { error: 'OPERATION_TIME_LIMIT', time: { operating_reset_at: 9999999999 } })await expect(c.call('crm.lead.list')).rejects.toThrow(/suspended/)// A second call to the same method fails fast without hitting the network.await expect(c.call('crm.lead.list')).rejects.toBeInstanceOf(BitrixOperationTimeError)})it('refuses to construct without a webhook URL', () => {expect(() => new BitrixClient({ webhookUrl: '' })).toThrow(/BITRIX_WEBHOOK_URL/)})})