Files
template-app/modules/invoicing/__tests__/procedures.test.ts
T
2026-11-14 09:56:45 +00:00

98 lines
4.5 KiB
TypeScript

// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// Memory app + injected service port. Proves app membership/roles, never external partition isolation.
import { type Actor, buildRegistry, Container, createCaller } from "@conusai-kit/core"
import { createMemoryDataSource, DATA_SOURCE, entitiesOf, SYSTEM_DATA } from "@conusai-kit/data"
import { createTenancyHooks, TENANCY, tenancyModule, users } from "@conusai-kit/tenancy"
import { describe, expect, it, vi } from "vitest"
import { simulatedBusinessAdmission } from "../../../test/helpers/business-admission.js"
import { INVOICING_SERVICE, type InvoicingService } from "../core/ports.js"
import { invoicingModule } from "../module.js"
import { id, invoiceId, row, terms } from "./fixture.js"
import { invoicingIsolationFixtures } from "./isolation.js"
async function fixture() {
const registry = buildRegistry([tenancyModule, invoicingModule])
const data = createMemoryDataSource({ entities: entitiesOf(registry, "postgres") })
const root = new Container()
root.value(DATA_SOURCE, data)
root.value(SYSTEM_DATA, data)
const port: InvoicingService = {
list: vi.fn(async () => ({ rows: [row] })),
get: vi.fn(async () => row),
series: vi.fn(async () => ({ rows: [] })),
ubl: vi.fn(async () => ({ filename: "fixture.xml", xml: "<Invoice/>" })),
draft: vi.fn(async () => row),
update: vi.fn(async () => row),
transition: vi.fn(async () => row),
createSeries: vi.fn(async () => {
throw new Error("not used")
}),
}
root.value(INVOICING_SERVICE, port)
const caller = (actor: Actor) => {
const tenancy = createTenancyHooks({ data, registry })
const container = root.child()
container.value(TENANCY, tenancy)
return createCaller({ registry, container, tenancy, serviceReadiness: simulatedBusinessAdmission }, { actor })
}
const user = async (name: string) => {
const row = await data.global(users).insert({
issuer: "https://id.example.test",
subject: name,
email: `${name}@example.test`,
displayName: name,
locale: "en",
})
return { userId: row.id }
}
const owner = caller(await user("owner"))
await owner.callByName("tenancy.org.create", { slug: "alpha", name: "Synthetic Alpha" })
const foreign = caller(await user("foreign"))
await foreign.callByName("tenancy.org.create", { slug: "beta", name: "Synthetic Beta" })
const invite = (await owner.callByName("tenancy.member.invite", {
org: "alpha",
email: "viewer@example.test",
role: "viewer",
})) as { token: string }
const viewer = caller(await user("viewer"))
await viewer.callByName("tenancy.invitation.accept", { org: "alpha", token: invite.token })
return { owner, foreign, viewer, port }
}
describe("Invoicing app dispatch (simulated service)", () => {
it("denies every foreign-organization operation before reaching a service port", async () => {
const { owner, port } = await fixture()
for (const [name, fixture] of Object.entries(invoicingIsolationFixtures)) {
const input = fixture.input({ orgSlug: "beta" } as never)
await expect(owner.callByName(name, input)).rejects.toMatchObject({ code: "not_found" })
}
for (const method of Object.values(port)) expect(method).not.toHaveBeenCalled()
})
it("allows viewer reads but refuses fiscal writes and series administration", async () => {
const { viewer, port } = await fixture()
expect(await viewer.callByName("invoicing.invoice.list", { org: "alpha" })).toMatchObject({ rows: [{ invoiceId }] })
for (const name of [
"invoicing.invoice.draft",
"invoicing.invoice.update",
"invoicing.invoice.issue",
"invoicing.invoice.credit",
"invoicing.invoice.void",
"invoicing.series.create",
]) {
const fixture = invoicingIsolationFixtures[name]
if (!fixture) throw new Error("fixture missing")
await expect(viewer.callByName(name, fixture.input({ orgSlug: "alpha" } as never))).rejects.toMatchObject({
code: "not_found",
})
}
expect(port.draft).not.toHaveBeenCalled()
expect(port.transition).not.toHaveBeenCalled()
expect(port.createSeries).not.toHaveBeenCalled()
})
it("routes an authorized draft with exact strings and a stable operation key", async () => {
const { owner, port } = await fixture()
const result = await owner.callByName("invoicing.invoice.draft", { org: "alpha", data: terms, requestId: id })
expect(result).toMatchObject({ invoiceId, revision: row.revision })
expect(port.draft).toHaveBeenCalledWith(terms, `invoicing:draft:${id}`)
})
})