238 lines
8.6 KiB
TypeScript
238 lines
8.6 KiB
TypeScript
|
|
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);
|
||
|
|
}
|