// The catalog 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 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 { sdkCatalogService } from "../adapters/service.js" import { catalogApp } from "./app.js" import { catalogIsolationFixtures } 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 HASH = "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08" const bolt = { code: "BOLT-M8", name: "Bolt M8", baseUnit: "C62" } const nut = { code: "NUT-M8", name: "Nut M8", baseUnit: "C62", description: "A nut" } const importOf = (rows: unknown[], over: Record = {}) => ({ fileHash: HASH, sourceName: "products.csv", format: "csv", chunk: 0, finalChunk: true, rows, requestId: crypto.randomUUID(), ...over, }) async function seeded(options: Parameters[0] = {}) { const app = catalogApp(options) const t = await app.tenant("alpha") await t.call.callByName("catalog.import.apply", { ...t.slugs, ...importOf([ { sourceRow: 2, product: bolt }, { sourceRow: 3, product: nut }, ]), }) return { app, t } } describe("the catalog procedures", () => { it("apply an import, and read its products, with every int64 a plain value", async () => { const app = catalogApp() const t = await app.tenant("alpha") const requestId = crypto.randomUUID() const applied = await t.call.callByName("catalog.import.apply", { ...t.slugs, ...importOf([{ sourceRow: 2, product: bolt }], { requestId }), }) expect(applied).toMatchObject({ import: { sourceName: "products.csv", status: "applied", revision: "1", created: 1, refused: 0 }, rows: [{ sourceRow: 2, outcome: "created", reason: null }], }) expect(JSON.parse(JSON.stringify(applied))).toEqual(applied) // A retried request applies once. expect( await t.call.callByName("catalog.import.apply", { ...t.slugs, ...importOf([{ sourceRow: 2, product: bolt }], { requestId }), }), ).toEqual(applied) expect(app.fake.catalog.products.size).toBe(1) const listed = (await t.call.callByName("catalog.product.list", t.slugs)) as { rows: Array<{ productId: string }> } expect(listed.rows).toEqual([ expect.objectContaining({ code: "BOLT-M8", name: "Bolt M8", baseUnit: "C62", status: "active", revision: "1" }), ]) const productId = listed.rows[0]?.productId as string expect(await t.call.callByName("catalog.product.get", { ...t.slugs, productId })).toEqual(listed.rows[0]) expect(await t.call.callByName("catalog.import.list", t.slugs)).toMatchObject({ rows: [{ sourceName: "products.csv", created: 1 }], }) }) it("search by name, by code and by identifier", async () => { const { t } = await seeded() const search = async (q: string, by?: string) => ( (await t.call.callByName("catalog.product.search", { ...t.slugs, q, ...(by ? { by } : {}) })) as { rows: Array<{ code: string }> } ).rows.map((row) => row.code) expect(await search(" nut ")).toEqual(["NUT-M8"]) expect(await search("BOLT", "code")).toEqual(["BOLT-M8"]) expect(await search("nothing")).toEqual([]) expect(await search("4006381333931", "gtin")).toEqual([]) }) it("carry a chunk's revision forward, and report a refused row without losing the others", async () => { const app = catalogApp() const t = await app.tenant("alpha") const first = (await t.call.callByName("catalog.import.apply", { ...t.slugs, ...importOf([{ sourceRow: 2, product: bolt }], { finalChunk: false }), })) as { import: { revision: string; status: string } } expect(first.import.status).toBe("applying") const second = await t.call.callByName("catalog.import.apply", { ...t.slugs, ...importOf( [ { sourceRow: 3, product: nut }, { sourceRow: 4, product: { code: "BAD CODE", name: "Bad", baseUnit: "C62" } }, ], { chunk: 1, revision: first.import.revision }, ), }) expect(second).toMatchObject({ import: { status: "applying", revision: "2", created: 2, refused: 1 }, rows: [ { sourceRow: 3, outcome: "created" }, { sourceRow: 4, outcome: "refused", productId: null, reason: "invalid_fields" }, ], }) }) it("show the service's refusals in the service's words", async () => { const app = catalogApp() const t = await app.tenant("alpha") const missing = await refusal(t.call.callByName("catalog.product.get", { ...t.slugs, productId: "nobody" })) expect(missing.problem).toMatchObject({ status: 404, code: "business_not_found" }) const stale = await refusal( t.call.callByName("catalog.import.apply", { ...t.slugs, ...importOf([{ sourceRow: 2, product: bolt }], { chunk: 1 }), }), ) expect(stale.problem).toMatchObject({ status: 409, code: "business_conflict" }) }) it("answer a disabled service, a missing scope and a silent service with the platform's own codes", async () => { const app = catalogApp({ catalog: false }) const t = await app.tenant("alpha") const disabled = await refusal(t.call.callByName("catalog.product.list", t.slugs)) expect(disabled.problem).toMatchObject({ status: 404, code: "service_not_enabled" }) app.fake.enable("catalog") app.fake.revoke("catalog.imports:write") const scoped = await refusal( t.call.callByName("catalog.import.apply", { ...t.slugs, ...importOf([{ sourceRow: 2, product: bolt }]) }), ) expect(scoped.problem).toMatchObject({ status: 403, code: "service_permission_missing" }) expect(scoped.problem.detail).not.toContain("catalog.imports:write") app.fake.refuseNext(503) const silent = await refusal(t.call.callByName("catalog.product.list", t.slugs)) expect(silent.problem).toMatchObject({ status: 503, code: "business_unavailable" }) }) it("let a viewer read and refuse their import before the service is called", async () => { const { app, t } = await seeded() const viewer = await app.member(t, "viewer@alpha.test", "viewer") const listed = (await viewer.callByName("catalog.product.list", t.slugs)) as { rows: unknown[] } expect(listed.rows).toHaveLength(2) 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("catalog.import.apply", { ...t.slugs, ...importOf([{ sourceRow: 9, product: bolt }]) }), ) expect(refused.problem.status).toBe(404) expect(app.fake.calls.length).toBe(before) }) it("need no scope beyond the ones `kit module:on catalog` asks for", async () => { const { t } = await seeded({ scopes: ["catalog.products:read", "catalog.imports:read", "catalog.imports:write"] }) const { rows } = (await t.call.callByName("catalog.product.list", t.slugs)) as { rows: Array<{ productId: string }> } expect(rows).toHaveLength(2) await t.call.callByName("catalog.product.search", { ...t.slugs, q: "nut" }) await t.call.callByName("catalog.product.get", { ...t.slugs, productId: rows[0]?.productId }) await t.call.callByName("catalog.import.list", t.slugs) }) it("refuses a record of another platform organization: the token reaches only its own", async () => { const { app } = await seeded() const [product] = [...app.fake.catalog.products.values()] const elsewhere = sdkCatalogService({ client: app.client, orgSlug: "another-organization" }) await expect(elsewhere.getProduct(product?.id as string)).rejects.toMatchObject({ problem: { status: 404 } }) }) }) describe("the catalog module's tenant isolation", () => { it("refuses every procedure called with another organization's slugs, and never reaches the service", async () => { const app = catalogApp() 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(), ...catalogIsolationFixtures }, alsoCheck: [...TENANCY_ALSO_CHECK], exempt: tenancyIsolationExempt, }) expect(result.failures).toEqual([]) for (const name of Object.keys(catalogIsolationFixtures)) expect(result.passed, name).toContain(name) expect(app.fake.calls).toEqual([]) }) })