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

238 lines
8.6 KiB
TypeScript
Raw Normal View History

import assert from "node:assert/strict";
import {
AssetResponseSchema,
CatalogResponseSchema,
InventoryResponseSchema,
SaveDefinitionResponseSchema,
} from "@metis/assets/common";
import { CompanyResponseSchema } from "@metis/companies/common";
import type { Sequelize } from "sequelize";
import { z } from "zod/v4";
export async function verifyInventory(
request: (path: string, body?: unknown, origin?: string, token?: string) => Promise<Response>,
companyId: string,
otherId: string,
database: Sequelize
) {
const path = `/assets/${companyId}`;
async function save(url: string, values: unknown) {
const response = await request(url, { values });
assert.ok([200, 201].includes(response.status), await response.clone().text());
return AssetResponseSchema.parse(await response.json()).asset;
}
async function error(url: string, values: unknown, code: string, status = 409) {
const response = await request(url, { values });
assert.equal(response.status, status);
assert.equal(z.object({ code: z.string() }).parse(await response.json()).code, code);
}
async function read(url = path) {
const response = await request(url);
assert.equal(response.status, 200);
return InventoryResponseSchema.parse(await response.json());
}
const catalog = CatalogResponseSchema.parse(
await (await request(`/asset-catalog/${companyId}`)).json()
);
const monitor = catalog.items.find((item) => item.values.name === "Monitor");
assert.ok(monitor);
const laptop = catalog.items.find((item) => item.values.name === "Portátil");
assert.ok(laptop);
const inch = catalog.items.find((item) => item.values.name === "Pulgadas");
assert.ok(inch);
async function person(company: string) {
const response = await request(`/companies/${company}/people`, {
name: "Persona inventario",
position: null,
email: null,
phone: null,
active: true,
isContact: false,
});
assert.equal(response.status, 201);
const saved = CompanyResponseSchema.parse(await response.json()).company.people.at(-1);
assert.ok(saved);
return saved;
}
async function site(company: string) {
const response = await request(`/companies/${company}/sites`, {
name: "Sede inventario",
address: null,
});
assert.equal(response.status, 201);
const saved = CompanyResponseSchema.parse(await response.json()).company.sites.at(-1);
assert.ok(saved);
return saved;
}
const ownPerson = await person(companyId);
const foreignPerson = await person(otherId);
const ownSite = await site(companyId);
const foreignSite = await site(otherId);
const input = {
name: "MON-01",
templateId: monitor.id,
siteId: null,
personId: null,
status: "available",
notes: "",
characteristics: {},
};
assert.equal((await read()).assets.length, 0);
const created = await save(path, input);
assert.equal(created.history.length, 1);
assert.equal(created.id.charAt(14), "7");
assert.equal(created.history[0]?.id.charAt(14), "7");
assert.ok(created.history[0]?.actorName);
assert.ok(created.history[0]?.actorId);
assert.equal((await read(`/assets/${otherId}`)).assets.length, 0);
await error(`/assets/${otherId}/${created.id}/update`, input, "ASSET_NOT_FOUND", 404);
await error(path, { ...input, siteId: foreignSite.id }, "SITE_NOT_FOUND", 404);
await error(path, { ...input, personId: foreignPerson.id }, "PERSON_UNAVAILABLE");
await error(path, { ...input, characteristics: { [inch.id]: "24" } }, "INVALID_CHARACTERISTICS");
await error(
path,
{ ...input, characteristics: { [ownPerson.id]: 24 } },
"INVALID_CHARACTERISTICS"
);
await error(path, { ...input, actorId: ownPerson.id }, "INVALID_REQUEST", 400);
assert.equal((await request(path, { values: input }, "https://other.example")).status, 403);
assert.equal((await request(path, { values: input }, undefined, "a".repeat(64))).status, 403);
const assigned = await save(`${path}/${created.id}/update`, {
...input,
siteId: ownSite.id,
personId: ownPerson.id,
status: "in_use",
characteristics: { [inch.id]: 24 },
});
assert.equal(assigned.id, created.id);
assert.equal(assigned.history.length, 2);
assert.equal(assigned.history[0]?.values.personId, null);
const unchanged = await save(`${path}/${created.id}/update`, assigned.values);
assert.deepEqual(unchanged.history, assigned.history);
await error(
`${path}/${created.id}/update`,
{ ...assigned.values, templateId: laptop.id },
"TEMPLATE_FIXED"
);
await error(
`${path}/${created.id}/update`,
{ ...assigned.values, status: "retired" },
"RETIRED_ASSIGNED"
);
const deactivated = await request(`/companies/${companyId}/people/${ownPerson.id}/update`, {
name: ownPerson.name,
position: null,
email: null,
phone: null,
active: false,
isContact: false,
});
assert.equal(deactivated.status, 200);
const kept = await save(`${path}/${created.id}/update`, {
...assigned.values,
notes: "Persona inactiva: revisar entrega.",
});
assert.equal(kept.values.personId, ownPerson.id);
await error(path, { ...input, personId: ownPerson.id }, "PERSON_UNAVAILABLE");
const retired = await save(`${path}/${created.id}/update`, {
...kept.values,
personId: null,
status: "retired",
});
assert.equal(retired.history.length, 4);
assert.equal(retired.history[1]?.values.personId, ownPerson.id);
const templateResponse = await request(`/asset-catalog/${companyId}`, {
values: {
kind: "template",
name: "Monitor obligatorio inventario",
description: "",
fields: [{ fieldId: inch.id, required: true }],
},
});
assert.equal(templateResponse.status, 201);
const templateId = SaveDefinitionResponseSchema.parse(await templateResponse.json()).savedId;
await error(path, { ...input, templateId }, "REQUIRED_CHARACTERISTICS");
await error(`/assets/${otherId}`, { ...input, templateId }, "TEMPLATE_UNAVAILABLE");
const custom = await save(path, {
...input,
templateId,
name: "MON-02",
characteristics: { [inch.id]: 0 },
});
const revisionId = custom.history[0]?.id;
assert.equal(
(
await request(`/asset-catalog/${companyId}/${templateId}/update`, {
values: {
kind: "template",
name: "Plantilla renombrada",
description: "",
fields: [{ fieldId: inch.id, required: false }],
},
})
).status,
200
);
const current = (await read()).assets.find((asset) => asset.id === custom.id);
assert.ok(current);
assert.equal(current.specification.templateName, "Monitor obligatorio inventario");
assert.equal(current.specification.fields[0]?.required, true);
await error(
`${path}/${custom.id}/update`,
{ ...custom.values, characteristics: {} },
"REQUIRED_CHARACTERISTICS"
);
assert.equal(
(await request(`/asset-catalog/${companyId}/${templateId}/status`, { active: false })).status,
200
);
await error(path, { ...input, templateId }, "TEMPLATE_UNAVAILABLE");
const updated = await save(`${path}/${custom.id}/update`, {
...custom.values,
notes: "Plantilla conservada",
});
assert.equal(updated.history[0]?.id, revisionId);
const stored = (await read()).assets.find((asset) => asset.id === updated.id);
assert.deepEqual(stored, updated);
// Corrupción cruzada simulada solo en la base de pruebas y restaurada incluso ante fallo.
const corrupt = { ...updated.values, personId: foreignPerson.id };
const lastRevision = updated.history.at(-1);
assert.ok(lastRevision);
await database.query("UPDATE assets SET personId=:person, `values`=:values WHERE id=:id", {
replacements: { person: foreignPerson.id, values: JSON.stringify(corrupt), id: updated.id },
});
await database.query(
"UPDATE asset_revisions SET personId=:person, `values`=:values WHERE id=:id",
{
replacements: {
person: foreignPerson.id,
values: JSON.stringify(corrupt),
id: lastRevision.id,
},
}
);
try {
assert.equal((await request(path)).status, 500);
} finally {
await database.query(
"UPDATE asset_revisions SET personId=:person, `values`=:values WHERE id=:id",
{
replacements: {
person: updated.values.personId,
values: JSON.stringify(updated.values),
id: lastRevision.id,
},
}
);
await database.query("UPDATE assets SET personId=:person, `values`=:values WHERE id=:id", {
replacements: {
person: updated.values.personId,
values: JSON.stringify(updated.values),
id: updated.id,
},
});
}
assert.equal((await request("/assets/00000000-0000-4000-8000-000000000001")).status, 404);
}