import assert from "node:assert/strict"; import { CatalogResponseSchema as AssetCatalogResponseSchema } from "@metis/assets/common"; import { CatalogResponseSchema, InventoryResponseSchema, ResourceResponseSchema, SaveDefinitionResponseSchema, } from "@metis/resources/common"; import { createResourceModule } from "@metis/resources/module"; import type { Sequelize } from "sequelize"; import { QueryTypes } from "sequelize"; import { z } from "zod/v4"; export async function verifyResources( request: (path: string, body?: unknown, origin?: string, token?: string) => Promise, companyId: string, otherId: string, database: Sequelize ) { const global = "/resource-catalog/global"; const company = `/resource-catalog/${companyId}`; const other = `/resource-catalog/${otherId}`; const inventory = `/resources/${companyId}`; async function read(path: string) { const response = await request(path); assert.equal(response.status, 200); return CatalogResponseSchema.parse(await response.json()); } async function save(path: string, values: unknown) { const response = await request(path, { values }); assert.ok([200, 201].includes(response.status), await response.clone().text()); return SaveDefinitionResponseSchema.parse(await response.json()); } async function error(path: string, body: unknown, code: string, status = 409) { const response = await request(path, body); assert.equal(response.status, status); assert.equal(z.object({ code: z.string() }).parse(await response.json()).code, code); } async function resource(path: string, values: unknown) { const response = await request(path, { values }); assert.ok([200, 201].includes(response.status), await response.clone().text()); return ResourceResponseSchema.parse(await response.json()).resource; } const initial = await read(global); assert.equal(initial.items.length, 18); assert.equal(initial.items.filter((item) => item.values.kind === "template").length, 5); assert.ok(initial.items.every((item) => item.builtin && item.id[14] === "7")); assert.ok( initial.items.every((item) => item.bindings.every((binding) => !binding.required && binding.id[14] === "7") ) ); const assetsResponse = await request("/asset-catalog/global"); const assets = AssetCatalogResponseSchema.parse(await assetsResponse.json()); assert.ok(initial.items.every((item) => !assets.items.some((asset) => asset.id === item.id))); const stock = initial.items.find((item) => item.values.name === "Dirección de correo"); const stockTemplate = initial.items.find((item) => item.values.name === "Buzón de correo"); assert.ok(stock && stockTemplate && stockTemplate.values.kind === "template"); await error( `${global}/${stock.id}/update`, { values: { ...stock.values, name: "Cambio" } }, "BUILTIN_PROTECTED" ); assert.equal( (await request(`${global}/${stock.id}/status`, { active: false }, "https://other.example")) .status, 403 ); assert.equal( (await request(`${global}/${stock.id}/status`, { active: false }, undefined, "a".repeat(64))) .status, 403 ); assert.equal((await request(`${company}/${stock.id}/status`, { active: false })).status, 200); assert.equal( (await read(other)).items.find((item) => item.id === stock.id)?.effectiveActive, true ); await createResourceModule(database, (_request, _response, next) => next()); const restarted = await read(global); assert.deepEqual( restarted.items.map((item) => item.id).sort(), initial.items.map((item) => item.id).sort() ); assert.equal( (await read(company)).items.find((item) => item.id === stock.id)?.effectiveActive, false ); assert.equal((await request(`${company}/${stock.id}/status`, { active: null })).status, 200); const definition = { kind: "field", name: "Marca", type: "text", unit: "", description: "", choices: [], }; const local = await save(company, definition); const fieldId = local.savedId; assert.ok(!(await read(other)).items.some((item) => item.id === fieldId)); const assetField = assets.items.find((item) => item.values.kind === "field"); const assetTemplate = assets.items.find((item) => item.values.kind === "template"); assert.ok(assetField && assetTemplate); const template = { kind: "template", name: "Recurso propio", description: "", fields: [{ fieldId, required: true }], }; await error( company, { values: { ...template, fields: [{ fieldId: assetField.id, required: false }] } }, "FIELD_UNAVAILABLE" ); await error( `/asset-catalog/${companyId}`, { values: { ...template, fields: [{ fieldId: stock.id, required: false }] } }, "FIELD_UNAVAILABLE" ); await error(other, { values: template }, "FIELD_UNAVAILABLE"); await error(global, { values: template }, "FIELD_UNAVAILABLE"); const createdTemplate = await save(company, template); await error( `${other}/${createdTemplate.savedId}/update`, { values: template }, "DEFINITION_NOT_FOUND", 404 ); const first = { name: "Zoom corporativo", templateId: createdTemplate.savedId, active: true, notes: "", characteristics: { [fieldId]: "Zoom" }, }; await error( inventory, { values: { ...first, templateId: assetTemplate.id } }, "TEMPLATE_UNAVAILABLE" ); await error(`/resources/${otherId}`, { values: first }, "TEMPLATE_UNAVAILABLE"); await error(inventory, { values: { ...first, characteristics: {} } }, "REQUIRED_CHARACTERISTICS"); await error( inventory, { values: { ...first, characteristics: { [assetField.id]: "Ajeno" } } }, "INVALID_CHARACTERISTICS" ); await error(inventory, { values: { ...first, personId: companyId } }, "INVALID_REQUEST", 400); assert.equal((await request(inventory, { values: first }, "https://other.example")).status, 403); assert.equal( (await request(inventory, { values: first }, undefined, "a".repeat(64))).status, 403 ); const created = await resource(inventory, first); assert.equal(created.id[14], "7"); assert.equal(created.history[0]?.id[14], "7"); const repeated = await resource(`${inventory}/${created.id}/update`, first); assert.equal(repeated.history.length, 1); assert.equal(repeated.id, created.id); await error( `/resources/${otherId}/${created.id}/update`, { values: first }, "RESOURCE_NOT_FOUND", 404 ); const altered = await resource(`${inventory}/${created.id}/update`, { ...first, active: false, notes: "Fin de uso", }); assert.equal(altered.id, created.id); assert.equal(altered.history.length, 2); assert.equal(altered.history[0]?.values.active, true); assert.equal(altered.values.active, false); await error(inventory, { values: { ...first, name: " zoom CORPORATIVO " } }, "NAME_EXISTS"); await error( `${inventory}/${created.id}/update`, { values: { ...first, templateId: stockTemplate.id } }, "TEMPLATE_FIXED" ); // Cambiar una plantilla no altera las características ni la ficha ya registrada. await save(`${company}/${createdTemplate.savedId}/update`, { ...template, name: "Plantilla revisada", fields: [{ fieldId: stock.id, required: false }], }); const listResponse = await request(inventory); const stored = InventoryResponseSchema.parse(await listResponse.json()).resources.find( (item) => item.id === created.id ); assert.ok(stored); assert.equal(stored.specification.templateName, "Recurso propio"); assert.equal(stored.specification.fields[0]?.fieldId, fieldId); await resource(`${inventory}/${created.id}/update`, { ...first, active: false, notes: "Notas posteriores", }); // Una revisión fuera de secuencia se rechaza sin exponer detalles de persistencia. const audit = await database.query<{ id: string; position: number }>( "SELECT id, position FROM resource_revisions WHERE resourceId=:id ORDER BY position DESC LIMIT 1", { replacements: { id: created.id }, type: QueryTypes.SELECT } ); const latest = audit[0]; assert.ok(latest); try { await database.query("UPDATE resource_revisions SET position=999 WHERE id=:id", { replacements: { id: latest.id }, }); const inconsistent = await request(inventory); assert.equal(inconsistent.status, 500); const failure = z .strictObject({ code: z.string(), requestId: z.string() }) .parse(await inconsistent.json()); assert.equal(failure.code, "INTERNAL_ERROR"); } finally { await database.query("UPDATE resource_revisions SET position=:position WHERE id=:id", { replacements: { id: latest.id, position: latest.position }, }); } assert.equal((await request(inventory)).status, 200); // Una plantilla desactivada impide altas, pero permite conservar y editar registros existentes. assert.equal( (await request(`${company}/${createdTemplate.savedId}/status`, { active: false })).status, 200 ); await error(inventory, { values: { ...first, name: "Otro" } }, "TEMPLATE_UNAVAILABLE"); await resource(`${inventory}/${created.id}/update`, { ...first, active: false, notes: "Plantilla desactivada", }); assert.equal( (await request(`${company}/${createdTemplate.savedId}/status`, { active: true })).status, 200 ); const concurrentInput = { name: "Concurrente", templateId: stockTemplate.id, active: true, notes: "", characteristics: { [initial.items.find((item) => item.builtinKey === "field:email")?.id ?? ""]: "concurrent@example.test", }, }; const concurrent = await Promise.all([ request(inventory, { values: concurrentInput }), request(inventory, { values: concurrentInput }), ]); assert.deepEqual(concurrent.map((response) => response.status).sort(), [201, 409]); // Los buzones anteriores y los recursos básicos continúan en sus superficies originales. assert.equal((await request(`/mailboxes/${companyId}`)).status, 200); assert.equal((await request(`/access/${companyId}`)).status, 200); assert.equal((await request(`/resources/${otherId}`)).status, 200); assert.equal((await request(`${inventory}?unexpected=1`)).status, 400); assert.equal((await request("/resources/not-a-uuid")).status, 400); assert.equal((await request("/resources/82be74bb-894f-4cca-ac7b-dedc96c15689")).status, 404); }