// The crm procedures end to end: the caller, the tenancy resolver, the module and the real generated // client against the business-services fake -- what a member sees, what the service gets, and each // refusal as the kit's app error (`businessAppError`) a page or a form shows. import { actorRef } from "@conusai-kit/business" import type { AppError } from "@conusai-kit/core" import { entitiesOf } from "@conusai-kit/data" import { TENANCY_ALSO_CHECK, tenancyIsolationExempt, tenancyIsolationFixtures } from "@conusai-kit/tenancy/testing" import { type IsolationTenant, runIsolation, snapshotOf } from "@conusai-kit/testing" import { describe, expect, it } from "vitest" import { SUBJECT_SCHEME } from "../contract/index.js" import { BINDING_ID, crmApp } from "./app.js" import { crmIsolationFixtures } from "./isolation.js" const refusal = async (promise: Promise): Promise => { try { await promise } catch (error) { return error as AppError } throw new Error("the call was not refused") } const acme = { kind: "organization", displayName: "Acme Supplies", country: "LT" } describe("the crm procedures", () => { it("create a party once per request, list it, and read it with its contact points", async () => { const app = crmApp() const t = await app.tenant("alpha") const requestId = crypto.randomUUID() const created = (await t.call.callByName("crm.party.create", { ...t.slugs, ...acme, requestId })) as { partyId: string } expect(created).toMatchObject({ displayName: "Acme Supplies", kind: "organization", country: "LT", status: "active", linked: false, }) expect(await t.call.callByName("crm.party.create", { ...t.slugs, ...acme, requestId })).toEqual(created) expect(app.fake.crm.parties.size).toBe(1) const listed = (await t.call.callByName("crm.party.list", t.slugs)) as { rows: unknown[] } expect(listed.rows).toEqual([created]) const contact = await t.call.callByName("crm.contact.add", { ...t.slugs, partyId: created.partyId, kind: "email", value: " orders@acme.example ", purposes: ["invoice_email", "invoice_email"], isPrimary: true, requestId: crypto.randomUUID(), }) expect(contact).toMatchObject({ kind: "email", value: "orders@acme.example", purposes: ["invoice_email"], isPrimary: true, erased: false, }) const read = (await t.call.callByName("crm.party.get", { ...t.slugs, partyId: created.partyId })) as { party: unknown contacts: unknown[] } expect(read).toEqual({ party: created, contacts: [contact] }) const [stored] = [...app.fake.crm.contacts.values()] if (!stored?.fields) throw new Error("the fake holds no contact point") expect((stored.fields as { effective: { from: string } }).effective.from).toMatch(/^\d{4}-\d{2}-\d{2}$/) }) it("link a person to the member who creates it with the member's subject reference", async () => { const app = crmApp() const t = await app.tenant("alpha") const created = (await t.call.callByName("crm.party.create", { ...t.slugs, kind: "person", displayName: "Ada", country: "LT", linkSelf: true, requestId: crypto.randomUUID(), })) as { partyId: string; linked: boolean } expect(created.linked).toBe(true) expect(app.fake.crm.parties.get(created.partyId)?.identifiers).toEqual([ { scheme: SUBJECT_SCHEME, value: actorRef(BINDING_ID, t.owner.userId) }, ]) const organization = await refusal( t.call.callByName("crm.party.create", { ...t.slugs, ...acme, linkSelf: true, requestId: crypto.randomUUID() }), ) expect(organization.problem.status).toBe(403) }) it("show the service's refusals in the service's words", async () => { const app = crmApp() const t = await app.tenant("alpha") const missing = await refusal(t.call.callByName("crm.party.get", { ...t.slugs, partyId: "nobody" })) expect(missing.problem).toMatchObject({ status: 404, code: "business_not_found" }) const { partyId } = (await t.call.callByName("crm.party.create", { ...t.slugs, ...acme, requestId: crypto.randomUUID(), })) as { partyId: string } const bad = await refusal( t.call.callByName("crm.contact.add", { ...t.slugs, partyId, kind: "email", value: "not an address", purposes: [], isPrimary: false, requestId: crypto.randomUUID(), }), ) expect(bad.problem).toMatchObject({ status: 400, code: "business_invalid" }) }) it("answer a disabled service, a missing scope and a silent service with the platform's own codes", async () => { const app = crmApp({ crm: false }) const t = await app.tenant("alpha") const disabled = await refusal(t.call.callByName("crm.party.list", t.slugs)) expect(disabled.problem).toMatchObject({ status: 404, code: "service_not_enabled" }) app.fake.enable("crm") app.fake.revoke("crm.parties:write") const scoped = await refusal( t.call.callByName("crm.party.create", { ...t.slugs, ...acme, requestId: crypto.randomUUID() }), ) expect(scoped.problem).toMatchObject({ status: 403, code: "service_permission_missing" }) expect(scoped.problem.detail).not.toContain("crm.parties:write") app.fake.refuseNext(503) const silent = await refusal(t.call.callByName("crm.party.list", t.slugs)) expect(silent.problem).toMatchObject({ status: 503, code: "business_unavailable" }) }) it("let a viewer read and refuse their writes before the service is called", async () => { const app = crmApp() const t = await app.tenant("alpha") const viewer = await app.member(t, "viewer@alpha.test", "viewer") expect(await viewer.callByName("crm.party.list", t.slugs)).toEqual({ rows: [] }) const before = app.fake.calls.length // A member without the permission is answered as a non-member is (kit-core's caller, spec ยง3.3). const refused = await refusal( viewer.callByName("crm.party.create", { ...t.slugs, ...acme, requestId: crypto.randomUUID() }), ) expect(refused.problem.status).toBe(404) expect(app.fake.calls.length).toBe(before) }) }) describe("the crm module's tenant isolation", () => { it("refuses every procedure called with another organization's slugs, and never reaches the service", async () => { const app = crmApp() const tenant = async (slug: string): Promise => { const t = await app.tenant(slug) return { orgSlug: t.orgSlug, workspaceSlug: t.workspaceSlug, projectSlug: t.projectSlug, orgId: t.orgId, workspaceId: t.workspaceId, projectId: t.projectId, userId: t.owner.userId, email: `owner@${slug}.test`, extra: {}, } } const a = await tenant("alpha") const b = await tenant("bravo") const result = await runIsolation({ registry: app.registry, seed: async () => ({ a, b }), caller: (actor) => app.caller(actor), snapshot: () => snapshotOf(app.data, entitiesOf(app.registry, "postgres"), { outbox: app.data.outbox }), fixtures: { ...tenancyIsolationFixtures(), ...crmIsolationFixtures }, alsoCheck: [...TENANCY_ALSO_CHECK], exempt: tenancyIsolationExempt, }) expect(result.failures).toEqual([]) for (const name of Object.keys(crmIsolationFixtures)) expect(result.passed, name).toContain(name) expect(app.fake.calls).toEqual([]) }) })