ConusAI vendor v0.1.0-beta.114
ci / gate (push) Waiting to run

This commit is contained in:
ConusAI vendor v0.1.0-beta.114
2026-11-14 09:56:45 +00:00
commit 1ce8d23c35
867 changed files with 122104 additions and 0 deletions
+103
View File
@@ -0,0 +1,103 @@
import { simulatedBusinessAdmission } from "../../../test/helpers/business-admission.js"
// Simulated adapter dispatch; these fixtures do not prove gateway or partition isolation.
// The module on its own, wired the way apps/web wires a request -- tenancy on the in-memory data
// source, the catalog module, and the Catalog port bound to the real generated client over the
// business-services fake of @conusai-kit/testing -- so its procedures are tested whether or not this
// app turned the module on.
import { businessClient, type Client } from "@conusai-kit/business"
import { type Actor, buildRegistry, Container, createCaller, type TransportCaller } 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 { type FakeBusinessServices, fakeBusinessServices } from "@conusai-kit/testing"
import { createClient } from "@conusai-sdk/catalog/client"
import { sdkCatalogService } from "../adapters/service.js"
import { CATALOG_SERVICE } from "../core/ports.js"
import { catalogModule } from "../module.js"
export interface Tenant {
readonly owner: Actor
readonly call: TransportCaller
readonly orgSlug: string
readonly orgId: string
readonly workspaceSlug: string
readonly workspaceId: string
readonly projectSlug: string
readonly projectId: string
readonly slugs: { org: string; workspace: string; project: string }
}
export function catalogApp(options: Parameters<typeof fakeBusinessServices>[0] = {}) {
const fake: FakeBusinessServices = fakeBusinessServices(options)
const client: Client = businessClient({ apiUrl: fake.apiUrl, token: fake.token, orgSlug: fake.orgSlug }, createClient)
client.setConfig({ fetch: fake.fetch as typeof fetch })
const registry = buildRegistry([tenancyModule, catalogModule])
const entities = entitiesOf(registry, "postgres")
const data = createMemoryDataSource({ entities })
const root = new Container()
root.value(DATA_SOURCE, data)
root.value(SYSTEM_DATA, data)
root.value(CATALOG_SERVICE, sdkCatalogService({ client, orgSlug: fake.orgSlug }))
const caller = (actor: Actor): TransportCaller => {
const tenancy = createTenancyHooks({ data, registry })
const container = root.child()
container.value(TENANCY, tenancy)
return createCaller({ registry, container, tenancy, serviceReadiness: simulatedBusinessAdmission }, { actor })
}
const signUp = async (email: string): Promise<Actor> => {
const user = await data.global(users).insert({
issuer: "https://id.example.test",
subject: email,
email,
displayName: email.split("@")[0] ?? email,
locale: "en",
})
return { userId: user.id }
}
/** An organization with one workspace and one project, owned by a new user. */
const tenant = async (slug: string): Promise<Tenant> => {
const owner = await signUp(`owner@${slug}.test`)
const call = caller(owner)
const org = (await call.callByName("tenancy.org.create", { slug, name: slug })) as { orgId: string; slug: string }
const workspace = (await call.callByName("tenancy.workspace.create", {
org: slug,
slug: `ws-${slug}`,
name: `Workspace ${slug}`,
visibility: "org",
})) as { workspaceId: string; slug: string }
const project = (await call.callByName("tenancy.project.create", {
org: slug,
workspace: workspace.slug,
slug: `p-${slug}`,
name: `Project ${slug}`,
moduleKey: "catalog",
})) as { projectId: string; slug: string }
return {
owner,
call,
orgSlug: org.slug,
orgId: org.orgId,
workspaceSlug: workspace.slug,
workspaceId: workspace.workspaceId,
projectSlug: project.slug,
projectId: project.projectId,
slugs: { org: org.slug, workspace: workspace.slug, project: project.slug },
}
}
/** A second user who joins `t`'s organization with the org role `role`. */
const member = async (t: Tenant, email: string, role: "viewer" | "member"): Promise<TransportCaller> => {
const invite = (await t.call.callByName("tenancy.member.invite", { org: t.orgSlug, email, role })) as {
token: string
}
const actor = await signUp(email)
const call = caller(actor)
await call.callByName("tenancy.invitation.accept", { org: t.orgSlug, token: invite.token })
return call
}
return { fake, client, registry, data, caller, signUp, tenant, member }
}
+112
View File
@@ -0,0 +1,112 @@
// The catalog use cases over a recording port: what each asks the service, with no client and no network.
import type { ProjectScope } from "@conusai-kit/core"
import { describe, expect, it } from "vitest"
import type { ImportOutcomeView } from "../contract/index.js"
import { applyImport, listProducts, requestKey, searchProducts } from "../core/catalog.js"
import { type CatalogService, catalogReadsOf } from "../core/ports.js"
const scope: ProjectScope = {
level: "project",
actor: { userId: "u-1" },
orgId: "o1",
workspaceId: "w1",
projectId: "p1",
}
const outcome: ImportOutcomeView = {
import: {
importId: "cim_1",
sourceName: "products.csv",
format: "csv",
status: "applied",
revision: "1",
created: 1,
updated: 0,
unchanged: 0,
refused: 0,
},
rows: [],
}
function recording() {
const asked: Array<[string, unknown]> = []
const service: CatalogService = {
listProducts: async (query) => {
asked.push(["listProducts", query])
return { rows: [] }
},
searchProducts: async (query) => {
asked.push(["searchProducts", query])
return { rows: [] }
},
getProduct: async () => {
throw new Error("not asked")
},
listImports: async () => ({ rows: [] }),
applyImport: async (input) => {
asked.push(["applyImport", input])
return outcome
},
}
return { service, asked }
}
const row = { sourceRow: 2, product: { code: "BOLT-M8", name: "Bolt M8", baseUnit: "C62" } }
const chunk = {
fileHash: "a".repeat(64),
sourceName: " products.csv ",
format: "csv" as const,
chunk: 0,
finalChunk: true,
rows: [row],
requestId: "r1",
}
describe("the catalog use cases", () => {
it("list products with the module's page size", async () => {
const { service, asked } = recording()
await listProducts(service, {})
expect(asked).toEqual([["listProducts", { limit: 50 }]])
})
it("search by name unless told otherwise, with the text trimmed", async () => {
const { service, asked } = recording()
await searchProducts(service, { q: " bolt " })
await searchProducts(service, { q: "BOLT", by: "code", after: "x", limit: 10 })
await searchProducts(service, { q: "4006381333931", by: "gtin" })
expect(asked).toEqual([
["searchProducts", { q: "bolt", limit: 50 }],
["listProducts", { search: { by: "code", q: "BOLT" }, after: "x", limit: 10 }],
["listProducts", { search: { by: "gtin", q: "4006381333931" }, limit: 50 }],
])
})
it("apply an import chunk with the project's request key and the source's trimmed name", async () => {
const { service, asked } = recording()
await applyImport(service, scope, chunk)
expect(asked).toEqual([
[
"applyImport",
{
fileHash: "a".repeat(64),
sourceName: "products.csv",
format: "csv",
chunk: 0,
finalChunk: true,
rows: [row],
key: requestKey(scope, "r1"),
},
],
])
})
it("hand a read handler a view without a single write", () => {
expect(Object.keys(catalogReadsOf(recording().service)).sort()).toEqual([
"getProduct",
"listImports",
"listProducts",
"searchProducts",
])
})
})
+94
View File
@@ -0,0 +1,94 @@
// Every string a page of the module shows is a message in English and in Lithuanian, and no component of
// the module carries a string of its own.
import { readdirSync, readFileSync } from "node:fs"
import { join } from "node:path"
import { buildRegistry } from "@conusai-kit/core"
import { createI18n, lintUiStrings } from "@conusai-kit/i18n"
import { tenancyModule } from "@conusai-kit/tenancy"
import { describe, expect, it } from "vitest"
import { catalogModule } from "../module.js"
import { importFormLabels, importListLabels, productListLabels } from "../ui/labels.js"
// The messages as an app that turned the module on has them: from the manifest, flattened by the kit.
const catalogues = buildRegistry([tenancyModule, catalogModule]).catalogues
const en = catalogues.en?.catalog ?? {}
const lt = catalogues.lt?.catalog ?? {}
function keysUsed(): string[] {
const asked: string[] = []
const t = (key: string) => {
asked.push(key)
return key
}
productListLabels(t, "failed", {})
importListLabels(t, 2)
importFormLabels(t, "failed", {})
return asked
}
describe("the catalog catalogues", () => {
it("hold the same keys in English and in Lithuanian, none empty", () => {
expect(Object.keys(lt).sort()).toEqual(Object.keys(en).sort())
for (const [key, text] of [...Object.entries(en), ...Object.entries(lt)]) expect(text.trim(), key).not.toBe("")
})
it("hold every message the labels ask for, and none that nothing asks for", () => {
const asked = keysUsed()
for (const key of asked) {
expect(en[key as keyof typeof en], `en is missing catalog.${key}`).toBeTruthy()
expect(lt[key as keyof typeof lt], `lt is missing catalog.${key}`).toBeTruthy()
}
// The pages and the navigation ask for these directly.
const direct = new Set([
"title",
"intro",
"nav.products",
"nav.imports",
"product.back",
"product.code",
"product.unit",
"product.status",
"product.group",
"product.description",
"product.revision",
"product.identifiers",
"product.none",
"statuses.active",
"statuses.archived",
"statuses.deleted",
"imports.title",
"imports.intro",
])
expect(Object.keys(en).filter((key) => !asked.includes(key) && !direct.has(key))).toEqual([])
})
it("answer the navigation label the manifest names", () => {
expect(catalogModule.ui?.nav?.map((item) => item.label)).toEqual(["catalog.title"])
})
it("count imports with the Lithuanian one, few and other forms", () => {
const count = (locale: "en" | "lt", n: number) =>
createI18n(locale, { en: { catalog: en }, lt: { catalog: lt } }).message("catalog.imports.count", { count: n })
expect(count("lt", 1)).toBe("1 importas")
expect(count("lt", 2)).toBe("2 importai")
expect(count("lt", 10)).toBe("10 importų")
expect(count("lt", 21)).toBe("21 importas")
expect(count("en", 2)).toBe("2 imports")
})
})
describe("the components", () => {
const dir = new URL("../ui/", import.meta.url).pathname
const sources = readdirSync(dir)
.filter((name) => name.endsWith(".tsx"))
.map((name) => ({ name, text: readFileSync(join(dir, name), "utf8") }))
it("carry no hard-coded user-facing string and no colour literal", () => {
expect(sources.length).toBeGreaterThanOrEqual(3)
for (const { name, text } of sources) {
expect(lintUiStrings(text, name), name).toEqual([])
expect(text, name).not.toMatch(/#[0-9a-fA-F]{3,8}\b|\brgb\(|\bhsl\(|\boklch\(/)
}
})
})
@@ -0,0 +1,44 @@
// What the import form reads out of a CSV file: rows as the service takes them, each with the line it came
// from, chunks the service accepts, and a file it cannot read named as such.
import { describe, expect, it } from "vitest"
import { chunksOf, csvRows, parseProductCsv } from "../ui/import-rows.js"
describe("a product CSV", () => {
it("reads the required columns by name, in any order, with the line number a person sees", () => {
const text = "name,base_unit,code,description\nBolt M8,C62,BOLT-M8,\n\nNut M8,C62,NUT-M8,A nut\n"
expect(parseProductCsv(text)).toEqual({
kind: "rows",
rows: [
{ sourceRow: 2, product: { code: "BOLT-M8", name: "Bolt M8", baseUnit: "C62" } },
{ sourceRow: 4, product: { code: "NUT-M8", name: "Nut M8", baseUnit: "C62", description: "A nut" } },
],
})
})
it("keeps a quoted comma, a doubled quote and a CRLF ending", () => {
expect(csvRows('a,"b,c","d""e"\r\nf,,g\r\n')).toEqual([
["a", "b,c", 'd"e'],
["f", "", "g"],
])
})
it("carries a row's revision when the file has one", () => {
expect(parseProductCsv("code,name,base_unit,revision\nBOLT-M8,Bolt M8,C62,4\n")).toEqual({
kind: "rows",
rows: [{ sourceRow: 2, revision: "4", product: { code: "BOLT-M8", name: "Bolt M8", baseUnit: "C62" } }],
})
})
it("names what is wrong with a file it cannot read, and reads an empty one as nothing", () => {
expect(parseProductCsv("code,name\nBOLT-M8,Bolt M8\n")).toEqual({ kind: "unreadable", missing: ["base_unit"] })
expect(parseProductCsv("")).toEqual({ kind: "unreadable", missing: ["code", "name", "base_unit"] })
expect(parseProductCsv("code,name,base_unit\n")).toEqual({ kind: "rows", rows: [] })
})
it("splits rows into chunks of the service's size", () => {
const rows = Array.from({ length: 401 }, (_, at) => at)
expect(chunksOf(rows).map((chunk) => chunk.length)).toEqual([200, 200, 1])
expect(chunksOf([])).toEqual([])
})
})
+48
View File
@@ -0,0 +1,48 @@
// What the generated tenant-isolation suite (test/isolation.test.ts) needs of this module, from this
// folder: one fixture per tenant-scoped procedure, each called as a member of one organization with
// another's slugs, which the resolver must refuse before the Catalog service is reached.
//
// The module keeps no row of its own, so it seeds nothing: what a fixture names is the other tenant's
// project, and an id is any well-formed one (the service is never asked). The suite reads this file by
// its place whether or not the module is on, and checks only the procedures the app registers, so
// while the module is off (the template's default) it brings no fixture; `procedures.test.ts` runs the
// same fixtures against the module on its own, on or off.
import type { TransportCaller } from "@conusai-kit/core"
import type { IsolationFixture, IsolationTenant } from "@conusai-kit/testing"
import { businessModulesOn } from "../../index.js"
const SOMEWHERE = "00000000-0000-4000-8000-000000000001"
const HASH = "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
const inProjectOf = (t: IsolationTenant) => ({ org: t.orgSlug, workspace: t.workspaceSlug, project: t.projectSlug })
export const catalogIsolationFixtures: Record<string, IsolationFixture> = {
"catalog.product.list": { input: (t) => inProjectOf(t) },
"catalog.product.search": { input: (t) => ({ ...inProjectOf(t), q: "anything" }) },
"catalog.product.get": { input: (t) => ({ ...inProjectOf(t), productId: SOMEWHERE }) },
"catalog.import.list": { input: (t) => inProjectOf(t) },
"catalog.import.apply": {
input: (t) => ({
...inProjectOf(t),
fileHash: HASH,
sourceName: "intruder.csv",
format: "csv",
chunk: 0,
finalChunk: true,
rows: [{ sourceRow: 2, product: { code: "INTRUDER", name: "Intruder", baseUnit: "C62" } }],
requestId: SOMEWHERE,
}),
},
}
/** The shape the suite reads (`modules/notes/__tests__/isolation.ts` names it), so no other module is imported. */
export const isolation: {
seed(
call: TransportCaller,
inProject: { org: string; workspace: string; project: string },
): Promise<Record<string, string>>
fixtures: Record<string, IsolationFixture>
} = {
seed: async () => ({}),
fixtures: businessModulesOn.includes("catalog") ? catalogIsolationFixtures : {},
}
@@ -0,0 +1,224 @@
// 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<unknown>): Promise<AppError> => {
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<string, unknown> = {}) => ({
fileHash: HASH,
sourceName: "products.csv",
format: "csv",
chunk: 0,
finalChunk: true,
rows,
requestId: crypto.randomUUID(),
...over,
})
async function seeded(options: Parameters<typeof catalogApp>[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<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(), ...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([])
})
})
+189
View File
@@ -0,0 +1,189 @@
// The pages and components, rendered the way the shell renders them, from the loader's data: the platform's
// refusals are the page's own states, a control is offered to whoever may use it, every table has a
// caption and scoped headers.
import { AppError, buildRegistry, Container, type TransportCaller } from "@conusai-kit/core"
import { builtInCatalogues, createI18n, uiLabels } from "@conusai-kit/i18n"
import { tenancyModule } from "@conusai-kit/tenancy"
import { createDataClient, DataProvider, type PermissionKey, PermissionsProvider } from "@conusai-kit/ui/data"
import { UiLabelsProvider } from "@conusai-kit/ui/primitives"
import type { ReactNode } from "react"
import { renderToStaticMarkup as renderMarkup } from "react-dom/server"
import { describe, expect, it, vi } from "vitest"
import { modulePages } from "../../pages.js"
import { catalogModule } from "../module.js"
import { catalogPage } from "../ui/pages.js"
const catalogues = buildRegistry([tenancyModule, catalogModule]).catalogues
const i18n = createI18n("en", {
en: { catalog: catalogues.en?.catalog ?? {}, app: { failed: "That did not work: {detail}" } },
})
const lithuanian = createI18n("lt", {
lt: { catalog: catalogues.lt?.catalog ?? {}, app: { failed: "Nepavyko: {detail}" } },
})
const slugs = { org: "acme", workspace: "ws", project: "p" }
const EVERYTHING: readonly PermissionKey[] = ["catalog.product.read", "catalog.product.write"]
const kitStrings = uiLabels(createI18n("en", builtInCatalogues))
const renderToStaticMarkup = (node: ReactNode) =>
renderMarkup(<UiLabelsProvider labels={kitStrings}>{node}</UiLabelsProvider>)
const inProvider = (node: ReactNode, permissions: readonly PermissionKey[] = EVERYTHING) =>
renderToStaticMarkup(
<DataProvider client={createDataClient({ fetch: async () => new Response(null, { status: 501 }) })}>
<PermissionsProvider scope={slugs} seed={{ scope: slugs, permissions, revision: "1" }}>
{node}
</PermissionsProvider>
</DataProvider>,
)
const callerOf = (answers: Record<string, unknown>): TransportCaller =>
({
call: async (contract: { name: string }) => {
const answer = answers[contract.name]
if (answer instanceof Error) throw answer
return answer
},
}) as unknown as TransportCaller
/** The page as the shell renders it: the loader's data through JSON, as the browser receives it, then the view. */
const render = async (segments: string[], answers: Record<string, unknown>, permissions?: readonly PermissionKey[]) => {
const data = await catalogPage.load({
slugs,
segments,
search: {},
caller: callerOf(answers),
i18n,
container: new Container(),
})
expect(JSON.parse(JSON.stringify(data))).toEqual(data)
const { View } = catalogPage
return inProvider(
<View slugs={slugs} segments={segments} locale="en" data={JSON.parse(JSON.stringify(data))} />,
permissions,
)
}
const product = {
productId: "prd_1",
code: "BOLT-M8",
name: "Bolt M8",
description: "A bolt",
baseUnit: "C62",
status: "active",
groupCode: null,
identifiers: [{ kind: "gtin", value: "4006381333931" }],
revision: "3",
}
const escaped = (text: string): string => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
/** Every label of a form names a control the page holds, and every hint is in its control's description. */
const expectLabelled = (html: string, labels: number): void => {
const targets = [...html.matchAll(/<label[^>]* for="([^"]+)"/g)].map((match) => match[1] ?? "")
expect(targets).toHaveLength(labels)
for (const id of targets) expect(html).toMatch(new RegExp(`<(?:input|select|button)[^>]* id="${escaped(id)}"`))
for (const [, id = ""] of html.matchAll(/<p id="([^"]+)" data-slot="field-description"/g))
expect(html).toMatch(new RegExp(`aria-describedby="(?:[^"]* )?${escaped(id)}[ "]`))
}
const applied = {
importId: "cim_1",
sourceName: "products.csv",
format: "csv",
status: "applied",
revision: "1",
created: 2,
updated: 1,
unchanged: 0,
refused: 0,
}
describe("the catalog page", () => {
it("lists the products, with the way to the imports", async () => {
const html = await render([], { "catalog.product.list": { rows: [product] } })
expect(html).toMatch(/<h1\b[^>]*>Products<\/h1>/)
expect(html).toMatch(
/<a\b(?=[^>]*href="\/acme\/ws\/p\/catalog\/prd_1")(?=[^>]*aria-label="Open Bolt M8")[^>]*>Bolt M8<\/a>/,
)
expect(html).toMatch(/<table\b[^>]*aria-label="Products"/)
expect(html).toMatch(/<th\b[^>]*role="columnheader"[^>]*>Code<\/th>/)
expect(html).toContain('href="/acme/ws/p/catalog/imports"')
})
it("shows one product with its identifiers and revision", async () => {
const html = await render(["prd_1"], { "catalog.product.get": product })
expect(html).toContain("<h2>Bolt M8</h2>")
expect(html).toContain("<dd>BOLT-M8</dd>")
expect(html).toContain("<dd>3</dd>")
expect(html).toContain("4006381333931")
expect(html).toContain('href="/acme/ws/p/catalog"')
})
it("lists the imports, with the form for whoever may apply one", async () => {
const answers = { "catalog.import.list": { rows: [applied] } }
const html = await render(["imports"], answers)
expect(html).toMatch(/<h1\b[^>]*>Imports<\/h1>/)
expect(html).toMatch(/<th\b[^>]*scope="row"[^>]*>products\.csv<\/th>/)
expect(html).toMatch(/<caption\b[^>]*>Imports<\/caption>/)
expect(html).toContain('type="file"')
expectLabelled(html, 1)
expect(html.match(/data-slot="field-description"/g)).toHaveLength(1)
expect(await render(["imports"], answers, ["catalog.product.read"])).not.toContain('type="file"')
})
it.each([
["service_not_enabled", "This service is not enabled for your organization."],
["service_permission_missing", "This app is not allowed to use this service. Ask an administrator to grant it."],
["business_unavailable", "The service did not answer. Try again later."],
])("shows the kit's state %s", async (code, text) => {
const refused = new AppError({ type: `/problems/${code}`, title: "Refused", status: 503, code, detail: "why" })
const html = await render([], { "catalog.product.list": refused })
expect(html).toContain(`data-state="${code}"`)
expect(html).toContain('role="status"')
expect(html).toContain(text)
expect(html).not.toContain("<table")
})
it("shows the same state in Lithuanian", async () => {
const code = "service_not_enabled"
const refused = new AppError({ type: `/problems/${code}`, title: "Refused", status: 404, code })
const data = await catalogPage.load({
slugs,
segments: [],
search: {},
caller: callerOf({ "catalog.product.list": refused }),
i18n: lithuanian,
container: new Container(),
})
expect(data).toMatchObject({
kind: "state",
notice: { code, text: "Ši paslauga jūsų organizacijai neįjungta." },
})
})
it("leaves a product that is not there, and a path it does not have, to the shell", async () => {
const missing = new AppError({
type: "/problems/business_not_found",
title: "Not found",
status: 404,
code: "business_not_found",
})
await expect(render(["prd_2"], { "catalog.product.get": missing })).rejects.toMatchObject({
problem: { code: "business_not_found" },
})
await expect(render(["prd_1", "x"], {})).rejects.toMatchObject({ problem: { status: 404 } })
await expect(render(["imports", "x"], {})).rejects.toMatchObject({ problem: { status: 404 } })
await expect(render(["%2Fetc"], {})).rejects.toMatchObject({ problem: { status: 404 } })
await expect(render(["%E0%A4%A"], {})).rejects.toMatchObject({ problem: { status: 404 } })
})
it("is listed for the shell", async () => {
await expect(modulePages.catalog?.()).resolves.toBe(catalogPage)
})
})
// Explicit synthetic admission: module-local fixtures exercise the selected module.
vi.mock("../../inclusion.json", () => ({
default: { schemaVersion: 1, selected: ["catalog"], everAdopted: ["catalog"] },
}))