98 lines
4.5 KiB
TypeScript
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}`)
|
|
})
|
|
})
|