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);
|
|
|
|
|
}
|