metis-desk/apps/server/src/resources.integration.ts

261 lines
10 KiB
TypeScript
Raw Normal View History

2026-10-05 11:13:51 +00:00
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<Response>,
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);
2026-10-05 15:01:32 +00:00
assert.equal(initial.items.length, 18);
assert.equal(initial.items.filter((item) => item.values.kind === "template").length, 5);
2026-10-05 11:13:51 +00:00
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: "",
2026-10-05 15:01:32 +00:00
characteristics: {
[initial.items.find((item) => item.builtinKey === "field:email")?.id ?? ""]:
"concurrent@example.test",
},
2026-10-05 11:13:51 +00:00
};
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);
}