|
| 1 | +import { afterEach, expect, test } from "bun:test" |
| 2 | +import { syncConsoleAccount, type SalesforceRequest } from "./salesforce" |
| 3 | +import { handleConsoleSignup } from "../routes/api/console-signup" |
| 4 | +import { z } from "zod" |
| 5 | + |
| 6 | +const account = { |
| 7 | + id: "user_console_test", |
| 8 | + email: "person@example.test", |
| 9 | + name: "Test Person", |
| 10 | + createdAt: "2026-10-01T10:00:00.000Z", |
| 11 | +} |
| 12 | +const servers = new Set<ReturnType<typeof Bun.serve>>() |
| 13 | +afterEach(async () => { |
| 14 | + await Promise.all([...servers].map((server) => server.stop(true))) |
| 15 | + servers.clear() |
| 16 | +}) |
| 17 | + |
| 18 | +function fixture( |
| 19 | + initial: { object: "Lead" | "Contact"; Id: string; LeadSource: string; Company?: string; AccountId?: string }[] = [], |
| 20 | + email = account.email, |
| 21 | +) { |
| 22 | + const people: Record<string, unknown>[] = initial.map((person) => ({ ...person })) |
| 23 | + const writes: { method: string; path: string; payload: Record<string, unknown> }[] = [] |
| 24 | + const server = Bun.serve({ |
| 25 | + hostname: "127.0.0.1", |
| 26 | + port: 0, |
| 27 | + async fetch(request) { |
| 28 | + const url = new URL(request.url) |
| 29 | + if (url.pathname.endsWith("/query")) { |
| 30 | + const query = url.searchParams.get("q") ?? "" |
| 31 | + expect(query).toContain("Console_Account_ID__c") |
| 32 | + expect(query).toContain(`Email = '${email.toLowerCase()}'`) |
| 33 | + const object = query.includes("FROM Contact") ? "Contact" : "Lead" |
| 34 | + if (object === "Lead") expect(query).toContain("IsConverted = false") |
| 35 | + return Response.json({ |
| 36 | + done: true, |
| 37 | + records: people.filter((person) => person.object === object).map((person) => ({ Id: person.Id })), |
| 38 | + }) |
| 39 | + } |
| 40 | + const payload = z.record(z.string(), z.unknown()).parse(await request.json()) |
| 41 | + writes.push({ method: request.method, path: url.pathname, payload }) |
| 42 | + if (request.method === "PATCH") { |
| 43 | + const person = people.find((person) => person.Id === url.pathname.split("/").at(-1)) |
| 44 | + if (!person) return Response.json({ error: "Not found" }, { status: 404 }) |
| 45 | + Object.assign(person, payload) |
| 46 | + return new Response(null, { status: 204 }) |
| 47 | + } |
| 48 | + const id = "00Q000000000001AAA" |
| 49 | + people.push({ object: "Lead", Id: id, ...payload }) |
| 50 | + return Response.json({ success: true, id }, { status: 201 }) |
| 51 | + }, |
| 52 | + }) |
| 53 | + servers.add(server) |
| 54 | + const request: SalesforceRequest = async (path, method = "GET", payload) => { |
| 55 | + const response = await fetch(new URL(path, server.url), { |
| 56 | + method, |
| 57 | + ...(payload ? { body: JSON.stringify(payload), headers: { "Content-Type": "application/json" } } : {}), |
| 58 | + }) |
| 59 | + if (!response.ok) throw new Error("CRM request failed") |
| 60 | + return response.status === 204 ? null : response.json() |
| 61 | + } |
| 62 | + return { people, writes, request } |
| 63 | +} |
| 64 | + |
| 65 | +test("a new signup creates one Console lead and an exact retry updates it", async () => { |
| 66 | + const crm = fixture() |
| 67 | + await syncConsoleAccount(account, crm.request) |
| 68 | + await syncConsoleAccount(account, crm.request) |
| 69 | + expect(crm.people).toHaveLength(1) |
| 70 | + expect(crm.writes.filter((write) => write.method === "POST")).toHaveLength(1) |
| 71 | + expect(crm.people[0]).toMatchObject({ |
| 72 | + Company: "example.test", |
| 73 | + LeadSource: "console signup", |
| 74 | + Has_Console_Account__c: true, |
| 75 | + Console_Account_ID__c: account.id, |
| 76 | + Console_Signup_Date__c: account.createdAt, |
| 77 | + }) |
| 78 | + expect(crm.writes[1].payload).not.toHaveProperty("LeadSource") |
| 79 | + expect(crm.writes[1].payload).not.toHaveProperty("Company") |
| 80 | +}) |
| 81 | + |
| 82 | +test.each([ |
| 83 | + ["hello@acme.com", "acme.com"], |
| 84 | + ["jane@company.co.uk", "company.co.uk"], |
| 85 | + ["HELLO@ACME.COM", "acme.com"], |
| 86 | + ["person@gmail.com", "Individual"], |
| 87 | + ["person+signup@GOOGLEMAIL.COM", "Individual"], |
| 88 | + ["person@outlook.com", "Individual"], |
| 89 | + ["person@hotmail.co.uk", "Individual"], |
| 90 | + ["person@yahoo.com", "Individual"], |
| 91 | + ["person@icloud.com", "Individual"], |
| 92 | + ["person@proton.me", "Individual"], |
| 93 | + ["person@protonmail.com", "Individual"], |
| 94 | + ["person@pm.me", "Individual"], |
| 95 | + ["person@aol.com", "Individual"], |
| 96 | + ["person@gmail.com.example.test", "gmail.com.example.test"], |
| 97 | +])("sets Company for a new signup with %s to %s", async (email, company) => { |
| 98 | + const crm = fixture([], email) |
| 99 | + await syncConsoleAccount({ ...account, email }, crm.request) |
| 100 | + expect(crm.people).toHaveLength(1) |
| 101 | + expect(crm.people[0]).toMatchObject({ Company: company, Email: email.toLowerCase(), LeadSource: "console signup" }) |
| 102 | +}) |
| 103 | + |
| 104 | +test("matching Leads and Contacts keep their source and receive the Console fields", async () => { |
| 105 | + const crm = fixture([ |
| 106 | + { object: "Contact", Id: "003000000000001AAA", LeadSource: "Partner", AccountId: "001000000000001AAA" }, |
| 107 | + { object: "Lead", Id: "00Q000000000002AAA", LeadSource: "enterprise website form", Company: "Visitor's Company" }, |
| 108 | + ]) |
| 109 | + await syncConsoleAccount(account, crm.request) |
| 110 | + expect(crm.writes.every((write) => write.method === "PATCH")).toBe(true) |
| 111 | + expect(crm.people.map((person) => person.LeadSource)).toEqual(["Partner", "enterprise website form"]) |
| 112 | + expect(crm.people[0].AccountId).toBe("001000000000001AAA") |
| 113 | + expect(crm.people[1].Company).toBe("Visitor's Company") |
| 114 | + for (const write of crm.writes) { |
| 115 | + expect(write.payload).not.toHaveProperty("Company") |
| 116 | + expect(write.payload).not.toHaveProperty("AccountId") |
| 117 | + } |
| 118 | + for (const person of crm.people) |
| 119 | + expect(person).toMatchObject({ |
| 120 | + Has_Console_Account__c: true, |
| 121 | + Console_Account_ID__c: account.id, |
| 122 | + Console_Signup_Date__c: account.createdAt, |
| 123 | + }) |
| 124 | +}) |
| 125 | + |
| 126 | +test("a converted person's Contact is updated without creating another Lead", async () => { |
| 127 | + const crm = fixture([{ object: "Contact", Id: "003000000000001AAA", LeadSource: "Website" }]) |
| 128 | + await syncConsoleAccount(account, crm.request) |
| 129 | + expect(crm.people).toHaveLength(1) |
| 130 | + expect(crm.writes[0].path).toContain("/Contact/") |
| 131 | + expect(crm.people[0].LeadSource).toBe("Website") |
| 132 | +}) |
| 133 | + |
| 134 | +test("the signup endpoint requires its server credential before touching Salesforce", async () => { |
| 135 | + const crm = fixture() |
| 136 | + const response = await handleConsoleSignup( |
| 137 | + new Request("http://localhost/api/console-signup", { method: "POST", body: JSON.stringify(account) }), |
| 138 | + "test-shared-token", |
| 139 | + crm.request, |
| 140 | + ) |
| 141 | + expect(response.status).toBe(401) |
| 142 | + expect(crm.writes).toHaveLength(0) |
| 143 | +}) |
| 144 | + |
| 145 | +test("the authenticated signup endpoint reports confirmed delivery", async () => { |
| 146 | + const crm = fixture() |
| 147 | + const response = await handleConsoleSignup( |
| 148 | + new Request("http://localhost/api/console-signup", { |
| 149 | + method: "POST", |
| 150 | + headers: { Authorization: "Bearer test-shared-token" }, |
| 151 | + body: JSON.stringify(account), |
| 152 | + }), |
| 153 | + "test-shared-token", |
| 154 | + crm.request, |
| 155 | + ) |
| 156 | + expect(response.status).toBe(200) |
| 157 | + expect(await response.json()).toMatchObject({ success: true }) |
| 158 | + expect(crm.people).toHaveLength(1) |
| 159 | +}) |
0 commit comments