@@ -0,0 +1,183 @@
|
||||
// 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<unknown>): Promise<AppError> => {
|
||||
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<IsolationTenant> => {
|
||||
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([])
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user