// The archive 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 typed state a page shows. import { describeBusinessRefusal } from "@conusai-kit/business/refusals" 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 { sdkArchiveService } from "../adapters/service.js" import { requestKey } from "../core/archive.js" import { archiveApp, type Tenant } from "./app.js" import { archiveIsolationFixtures } 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") } type App = ReturnType const retention = async (t: Tenant, code = "contracts", keepFor = "P10Y") => t.call.callByName("archive.retention.create", { ...t.slugs, code, keepFor, from: "created", requestId: crypto.randomUUID(), }) const register = async (app: App, t: Tenant, title = "Signed contract", retentionClass = "contracts") => (await t.call.callByName("archive.document.register", { ...t.slugs, title, retentionClass, fileId: await app.storedFile(t), requestId: crypto.randomUUID(), })) as { documentId: string; revision: string } describe("the archive procedures", () => { it("accepts incidental date-like digits in either UUID component without merging project keys", async () => { const app = archiveApp() const service = sdkArchiveService({ client: app.client, orgSlug: app.fake.orgSlug }) const incidental = "0195a3c4-e12a-7b3f-8a1b-39001010000f" const ordinary = "0195a3c4-e12a-7b3f-8a1b-aaaaaaaaaaaa" const first = { code: "first", keepFor: "P10Y", from: "created" as const } const key = requestKey({ projectId: incidental }, ordinary) expect(key).toBe(`app-project:${incidental}:${ordinary}`) await service.createRetentionClass({ ...first, key }) await service.createRetentionClass({ ...first, key }) await service.createRetentionClass({ code: "second", keepFor: "P10Y", from: "created", key: requestKey({ projectId: ordinary }, incidental), }) await service.createRetentionClass({ code: "third", keepFor: "P10Y", from: "created", key: requestKey({ projectId: ordinary }, ordinary), }) expect(app.fake.archive.retentionClasses.size).toBe(3) }) it("accepts a request UUID with incidental date-like digits and replays its receipt", async () => { const app = archiveApp() const t = await app.tenant("alpha") const request = { ...t.slugs, code: "contracts", keepFor: "P10Y", from: "created", requestId: "0195a3c4-e12a-7b3f-8a1b-39001010000f", } expect(await t.call.callByName("archive.retention.create", request)).toEqual({ code: "contracts" }) expect(await t.call.callByName("archive.retention.create", request)).toEqual({ code: "contracts" }) expect(app.fake.archive.retentionClasses.size).toBe(1) }) it("register a document once per request, list it, and read it with its versions", async () => { const app = archiveApp() const t = await app.tenant("alpha") await retention(t) const fileId = await app.storedFile(t) const request = { ...t.slugs, title: "Signed contract", kind: "380", retentionClass: "contracts", fileId, requestId: crypto.randomUUID(), } const receipt = (await t.call.callByName("archive.document.register", request)) as { documentId: string revision: string } expect(receipt.revision).toBe("1") expect(await t.call.callByName("archive.document.register", request)).toEqual(receipt) expect(app.fake.archive.documents.size).toBe(1) const listed = await t.call.callByName("archive.document.list", t.slugs) expect(listed).toMatchObject({ rows: [{ documentId: receipt.documentId, title: "Signed contract", kind: "380" }] }) const read = await t.call.callByName("archive.document.get", { ...t.slugs, documentId: receipt.documentId }) expect(read).toMatchObject({ document: { status: "active", retentionClass: "contracts", legalHold: false, revision: "1", disposed: false }, versions: [{ sequence: 1, sha256: "a".repeat(64), mediaType: "application/pdf", size: "1234" }], }) expect(JSON.parse(JSON.stringify(read))).toEqual(read) const [version] = [...app.fake.archive.versions.values()] expect(version?.blob_ref).toBe(`conusai://blob/files/${fileId}`) }) it("append a version over the revision it read, and refuse a stale one", async () => { const app = archiveApp() const t = await app.tenant("alpha") await retention(t) const { documentId } = await register(app, t) const added = await t.call.callByName("archive.version.add", { ...t.slugs, documentId, revision: "1", fileId: await app.storedFile(t, "b".repeat(64), 99), requestId: crypto.randomUUID(), }) expect(added).toEqual({ documentId, revision: "2" }) const stale = await refusal( t.call.callByName("archive.version.add", { ...t.slugs, documentId, revision: "1", fileId: await app.storedFile(t, "c".repeat(64), 7), requestId: crypto.randomUUID(), }), ) expect(stale.problem).toMatchObject({ status: 409, code: "business_conflict" }) const read = (await t.call.callByName("archive.document.get", { ...t.slugs, documentId })) as { versions: Array<{ sequence: number; size: string }> } expect(read.versions.map((version) => [version.sequence, version.size])).toEqual([ [1, "1234"], [2, "99"], ]) }) it("keep retention: a class, a hold that blocks disposal, the due list, and disposal when it has ended", async () => { let now = new Date("2026-11-02T10:00:00Z").getTime() const app = archiveApp({ clock: () => new Date(now) }) const t = await app.tenant("alpha") await retention(t, "short", "P1D") expect(await t.call.callByName("archive.retention.list", t.slugs)).toMatchObject({ rows: [{ code: "short", keepFor: "P1D", from: "created" }], }) const { documentId } = await register(app, t, "Delivery note", "short") const request = () => ({ ...t.slugs, documentId, requestId: crypto.randomUUID() }) const early = await refusal(t.call.callByName("archive.document.dispose", { ...request(), revision: "1" })) expect(early.problem).toMatchObject({ status: 409, code: "business_retention_active" }) expect(await t.call.callByName("archive.document.hold", { ...request(), revision: "1", legalHold: true })).toEqual({ documentId, revision: "2", }) now += 2 * 24 * 3600 * 1000 expect(await t.call.callByName("archive.disposal.list", t.slugs)).toEqual({ rows: [] }) const held = await refusal(t.call.callByName("archive.document.dispose", { ...request(), revision: "2" })) expect(held.problem).toMatchObject({ status: 409, code: "business_legal_hold" }) // The person reads the kit's sentence for the refusal, in their language. expect(describeBusinessRefusal(held, "lt")).toBe( "Šiam įrašui taikomas teisinis sulaikymas, todėl jo keisti ar ištrinti negalima.", ) await t.call.callByName("archive.document.hold", { ...request(), revision: "2", legalHold: false }) expect(await t.call.callByName("archive.disposal.list", t.slugs)).toMatchObject({ rows: [{ documentId }] }) await t.call.callByName("archive.document.dispose", { ...request(), revision: "3" }) const read = await t.call.callByName("archive.document.get", { ...t.slugs, documentId }) expect(read).toMatchObject({ document: { status: "disposed", disposed: true } }) }) it("show the service's refusals as the kit's codes", async () => { const app = archiveApp() const t = await app.tenant("alpha") const missing = await refusal(t.call.callByName("archive.document.get", { ...t.slugs, documentId: "nobody" })) expect(missing.problem).toMatchObject({ status: 404, code: "business_not_found" }) const noClass = await refusal(register(app, t, "Signed contract", "nothing")) expect(noClass.problem).toMatchObject({ status: 404, code: "business_not_found" }) const bad = await refusal( t.call.callByName("archive.retention.create", { ...t.slugs, code: "bad", keepFor: "ten years", from: "created", 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 = archiveApp({ archive: false }) const t = await app.tenant("alpha") const disabled = await refusal(t.call.callByName("archive.document.list", t.slugs)) expect(disabled.problem).toMatchObject({ status: 404, code: "service_not_enabled" }) app.fake.enable("archive") app.fake.revoke("archive.retention:write") const scoped = await refusal(retention(t)) expect(scoped.problem).toMatchObject({ status: 403, code: "service_permission_missing" }) expect(scoped.problem.detail).not.toContain("archive.retention:write") app.fake.refuseNext(503) const silent = await refusal(t.call.callByName("archive.document.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 = archiveApp() const t = await app.tenant("alpha") await retention(t) const { documentId } = await register(app, t) const viewer = await app.member(t, "viewer@alpha.test", "viewer") expect(await viewer.callByName("archive.document.list", t.slugs)).toMatchObject({ rows: [{ documentId }] }) 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). for (const [name, input] of [ [ "archive.document.register", { title: "No", retentionClass: "contracts", fileId: await app.storedFile(t), requestId: crypto.randomUUID() }, ], ["archive.document.hold", { documentId, revision: "1", legalHold: true, requestId: crypto.randomUUID() }], ["archive.document.dispose", { documentId, revision: "1", requestId: crypto.randomUUID() }], ["archive.retention.create", { code: "no", keepFor: "P1Y", from: "created", requestId: crypto.randomUUID() }], ] as const) { const refused = await refusal(viewer.callByName(name, { ...t.slugs, ...input })) expect(refused.problem.status, name).toBe(404) } expect(app.fake.calls.length).toBe(before) }) it("let an editor register and keep retention to an administrator", async () => { const app = archiveApp() const t = await app.tenant("alpha") await retention(t) const { documentId } = await register(app, t) const editor = await app.member(t, "editor@alpha.test", "member") const registered = await editor.callByName("archive.document.register", { ...t.slugs, title: "Editor's document", retentionClass: "contracts", fileId: await app.storedFile(t, "d".repeat(64)), requestId: crypto.randomUUID(), }) expect(registered).toMatchObject({ revision: "1" }) const refused = await refusal( editor.callByName("archive.document.hold", { ...t.slugs, documentId, revision: "1", legalHold: true, requestId: crypto.randomUUID(), }), ) expect(refused.problem.status).toBe(404) }) it("need no scope beyond the ones `kit module:on archive` asks for", async () => { let now = new Date("2026-11-02T10:00:00Z").getTime() const app = archiveApp({ clock: () => new Date(now), scopes: [ "archive.documents:read", "archive.documents:write", "archive.holds:write", "archive.retention:read", "archive.retention:write", "archive.disposal:read", "archive.disposal:write", ], }) const t = await app.tenant("alpha") await retention(t, "short", "P1D") const { documentId } = await register(app, t, "Delivery note", "short") await t.call.callByName("archive.version.add", { ...t.slugs, documentId, revision: "1", fileId: await app.storedFile(t, "b".repeat(64)), requestId: crypto.randomUUID(), }) await t.call.callByName("archive.document.list", t.slugs) await t.call.callByName("archive.document.get", { ...t.slugs, documentId }) await t.call.callByName("archive.retention.list", t.slugs) const request = () => ({ ...t.slugs, documentId, requestId: crypto.randomUUID() }) await t.call.callByName("archive.document.hold", { ...request(), revision: "2", legalHold: true }) await t.call.callByName("archive.document.hold", { ...request(), revision: "3", legalHold: false }) now += 2 * 24 * 3600 * 1000 await t.call.callByName("archive.disposal.list", t.slugs) await t.call.callByName("archive.document.dispose", { ...request(), revision: "4" }) }) it("refuse another organization's file: a document cannot be made of a record its project does not hold", async () => { const app = archiveApp() const alpha = await app.tenant("alpha") const bravo = await app.tenant("bravo") await retention(bravo) const theirs = await app.storedFile(alpha) const before = app.fake.calls.length const refused = await refusal( bravo.call.callByName("archive.document.register", { ...bravo.slugs, title: "Not mine", retentionClass: "contracts", fileId: theirs, requestId: crypto.randomUUID(), }), ) expect(refused.problem.status).toBe(404) expect(app.fake.calls.length).toBe(before) expect(app.fake.archive.documents.size).toBe(0) }) it("refuse a record of another platform organization: the token reaches only its own", async () => { const app = archiveApp() const t = await app.tenant("alpha") await retention(t) const { documentId } = await register(app, t) const elsewhere = sdkArchiveService({ client: app.client, orgSlug: "another-organization" }) await expect(elsewhere.getDocument(documentId)).rejects.toMatchObject({ problem: { status: 404 } }) }) }) describe("the archive module's tenant isolation", () => { it("refuses every procedure called with another organization's slugs, and never reaches the service", async () => { const app = archiveApp() 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(), ...archiveIsolationFixtures }, alsoCheck: [...TENANCY_ALSO_CHECK], exempt: tenancyIsolationExempt, }) expect(result.failures).toEqual([]) for (const name of Object.keys(archiveIsolationFixtures)) expect(result.passed, name).toContain(name) expect(app.fake.calls).toEqual([]) }) })