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

395 lines
14 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";
import { UniqueID } from "../../../packages/rdx-ddd/src/index";
2026-10-05 11:13:51 +00:00
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-07 11:26:59 +00:00
assert.equal(initial.items.length, 26);
assert.equal(initial.items.filter((item) => item.values.kind === "template").length, 6);
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")
)
);
2026-10-07 11:26:59 +00:00
const licenseTemplate = initial.items.find(
(item) => item.builtinKey === "template:software_license"
);
assert.ok(licenseTemplate && licenseTemplate.values.kind === "template");
const licenseFieldId = (key: string) => {
const field = initial.items.find((item) => item.builtinKey === `field:${key}`);
assert.ok(field && field.values.kind === "field");
return field.id;
};
const licenseInput = {
name: "Microsoft 365 Business Standard",
templateId: licenseTemplate.id,
active: true,
notes: "",
characteristics: {
[licenseFieldId("software_product")]: "Microsoft 365",
[licenseFieldId("software_edition")]: "Business Standard",
[licenseFieldId("license_model")]: "Suscripción",
[licenseFieldId("license_metric")]: "Por usuario",
[licenseFieldId("license_quantity")]: 25,
[licenseFieldId("expires")]: "2027-10-07",
[licenseFieldId("renews")]: "2027-09-30",
[licenseFieldId("auto_renew")]: false,
},
};
const license = await resource(inventory, licenseInput);
assert.equal(license.companyId, companyId);
assert.equal(license.specification.templateName, "Licencia de software");
assert.equal(license.values.characteristics[licenseFieldId("auto_renew")], false);
assert.equal(license.id[14], "7");
const licenseUpdated = await resource(`${inventory}/${license.id}/update`, {
...licenseInput,
characteristics: { ...licenseInput.characteristics, [licenseFieldId("license_quantity")]: 30 },
});
assert.equal(licenseUpdated.history.length, 2);
assert.equal(
licenseUpdated.history[0]?.values.characteristics[licenseFieldId("license_quantity")],
25
);
await error(
inventory,
{
values: {
...licenseInput,
name: "Fecha inválida",
characteristics: { [licenseFieldId("renews")]: "2027-02-30" },
},
},
"INVALID_CHARACTERISTICS"
);
await error(
inventory,
{
values: {
...licenseInput,
name: "Modalidad inválida",
characteristics: { [licenseFieldId("license_model")]: "Desconocida" },
},
},
"INVALID_CHARACTERISTICS"
);
const minimal = await resource(inventory, {
...licenseInput,
name: "ESET empresa",
characteristics: {},
});
assert.deepEqual(minimal.values.characteristics, {});
await error(
`/resources/${otherId}/${license.id}/update`,
{ values: licenseInput },
"RESOURCE_NOT_FOUND",
404
);
assert.equal(
(await request(`${company}/${licenseTemplate.id}/status`, { active: false })).status,
200
);
await error(
inventory,
{ values: { ...licenseInput, name: "No disponible" } },
"TEMPLATE_UNAVAILABLE"
);
await resource(`${inventory}/${license.id}/update`, {
...licenseUpdated.values,
notes: "Conservada",
});
assert.equal(
(await request(`${company}/${licenseTemplate.id}/status`, { active: null })).status,
200
);
2026-10-05 11:13:51 +00:00
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 section = {
id: UniqueID.generateNewID().toPrimitive(),
name: "Datos técnicos",
description: "",
};
2026-10-05 11:13:51 +00:00
const template = {
kind: "template",
name: "Recurso propio",
sections: [section],
2026-10-05 11:13:51 +00:00
description: "",
fields: [{ fieldId, required: true, sectionId: section.id }],
2026-10-05 11:13:51 +00:00
};
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.deepEqual(created.specification.sections, [section]);
assert.equal(created.specification.fields[0]?.sectionId, section.id);
2026-10-05 11:13:51 +00:00
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",
sections: [],
2026-10-05 11:13:51 +00:00
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.deepEqual(stored.specification.sections, [section]);
assert.equal(stored.specification.fields[0]?.sectionId, section.id);
2026-10-05 11:13:51 +00:00
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);
const globalSection = {
id: UniqueID.generateNewID().toPrimitive(),
name: "General",
description: "",
};
const groupedGlobal = await save(global, {
kind: "template",
name: "Plantilla global agrupada resources",
description: "",
sections: [globalSection],
fields: [{ fieldId: stock.id, required: false, sectionId: globalSection.id }],
});
const groupedStored = (await read(global)).items.find(
(item) => item.id === groupedGlobal.savedId
);
assert.ok(groupedStored?.values.kind === "template");
assert.deepEqual(groupedStored.values.sections, [globalSection]);
await error(
global,
{
values: {
kind: "template",
name: "Sección ajena resources",
description: "",
sections: [],
fields: [{ fieldId: stock.id, required: false, sectionId: globalSection.id }],
},
},
"INVALID_REQUEST",
400
);
2026-10-05 11:13:51 +00:00
}