- Changed asset status label from "status" to "technicalStatus" in AssetForm component. - Added help text for technical status in AssetForm. - Updated test cases to reflect changes in asset status from "available" to "operational". - Enhanced workstation form to utilize new asset status checks and display appropriate messages. - Refactored asset context to replace "available" with "notRetired" across various application policies and repositories. - Introduced new utility functions for asset status management, including technical status determination and assignment validation. - Added integration tests for asset state verification and unlinking assets from workstations.
254 lines
9.5 KiB
TypeScript
254 lines
9.5 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { randomUUID } from "node:crypto";
|
|
|
|
import {
|
|
AssetResponseSchema,
|
|
BrandResponseSchema,
|
|
BrandsResponseSchema,
|
|
CatalogResponseSchema,
|
|
} from "@metis/assets/common";
|
|
import type { Sequelize } from "sequelize";
|
|
import { QueryTypes } from "sequelize";
|
|
import { z } from "zod/v4";
|
|
|
|
import { migrateBrands } from "../../../modules/assets/src/api/infrastructure/persistence/brand-migration";
|
|
|
|
type Request = (path: string, body?: unknown, origin?: string, token?: string) => Promise<Response>;
|
|
|
|
export async function verifyBrands(
|
|
request: Request,
|
|
companyId: string,
|
|
otherId: string,
|
|
database: Sequelize
|
|
) {
|
|
const path = "/asset-brands";
|
|
|
|
async function list() {
|
|
const response = await request(path);
|
|
|
|
assert.equal(response.status, 200);
|
|
|
|
return BrandsResponseSchema.parse(await response.json()).brands;
|
|
}
|
|
|
|
async function save(values: unknown, id?: string) {
|
|
const response = await request(path + (id ? `/${id}/update` : ""), { values });
|
|
|
|
assert.equal(response.status, id ? 200 : 201, await response.clone().text());
|
|
|
|
return BrandResponseSchema.parse(await response.json()).brand;
|
|
}
|
|
|
|
async function error(url: string, body: unknown, status: number, code: string) {
|
|
const response = await request(url, body);
|
|
|
|
assert.equal(response.status, status, await response.clone().text());
|
|
assert.equal(z.object({ code: z.string() }).parse(await response.json()).code, code);
|
|
}
|
|
|
|
const initial = await list();
|
|
|
|
for (const name of ["Microsoft", "HP", "LG", "Kyocera", "Zebra", "Logitech", "Clónico"]) {
|
|
assert.ok(initial.some((brand) => brand.name === name && brand.builtin));
|
|
}
|
|
const builtin = initial.find((brand) => brand.name === "LG");
|
|
|
|
assert.ok(builtin);
|
|
assert.ok(
|
|
initial.every((brand) => !brand.builtin || brand.name === "Clónico" || brand.logoSvg !== null)
|
|
);
|
|
assert.equal(initial.find((brand) => brand.name === "Clónico")?.logoSvg, null);
|
|
const logoSvg =
|
|
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M1 1h20v20H1z"/></svg>';
|
|
|
|
await error(
|
|
path,
|
|
{
|
|
values: {
|
|
name: "SVG inválido",
|
|
active: true,
|
|
logoSvg: "<svg><script>alert(1)</script></svg>",
|
|
},
|
|
},
|
|
400,
|
|
"INVALID_REQUEST"
|
|
);
|
|
await error(
|
|
`${path}/${builtin.id}/update`,
|
|
{ values: { name: "Otro LG", active: true } },
|
|
409,
|
|
"BUILTIN_PROTECTED"
|
|
);
|
|
await error(path, { values: { name: " lg ", active: true } }, 409, "NAME_EXISTS");
|
|
await error(path, { values: { name: " ", active: true } }, 400, "INVALID_REQUEST");
|
|
await error(
|
|
`${path}/invalid/update`,
|
|
{ values: { name: "Válida", active: true } },
|
|
400,
|
|
"INVALID_REQUEST"
|
|
);
|
|
await error(
|
|
`${path}/${randomUUID()}/update`,
|
|
{ values: { name: "Válida", active: true } },
|
|
404,
|
|
"BRAND_NOT_FOUND"
|
|
);
|
|
assert.equal(
|
|
(await request(path, { values: { name: "Sin CSRF", active: true } }, undefined, "invalid"))
|
|
.status,
|
|
403
|
|
);
|
|
await error(`${path}/${builtin.id}/delete`, {}, 409, "BUILTIN_PROTECTED");
|
|
await error(`${path}/invalid/delete`, {}, 400, "INVALID_REQUEST");
|
|
await error(`${path}/${randomUUID()}/delete`, {}, 404, "BRAND_NOT_FOUND");
|
|
for (const active of [true, false]) {
|
|
const unused = await save({ name: `Eliminar ${randomUUID()}`, active, logoSvg });
|
|
const url = `${path}/${unused.id}/delete`;
|
|
|
|
await error(url, { unexpected: true }, 400, "INVALID_REQUEST");
|
|
await error(`${url}?unexpected=true`, {}, 400, "INVALID_REQUEST");
|
|
assert.equal((await request(url, {}, undefined, "invalid")).status, 403);
|
|
assert.ok((await list()).some((brand) => brand.id === unused.id));
|
|
const response = await request(url, {});
|
|
|
|
assert.equal(response.status, 200, await response.clone().text());
|
|
assert.deepEqual(BrandResponseSchema.parse(await response.json()).brand, unused);
|
|
await error(url, {}, 404, "BRAND_NOT_FOUND");
|
|
await migrateBrands(database);
|
|
assert.equal(
|
|
(await list()).some((brand) => brand.id === unused.id),
|
|
false
|
|
);
|
|
const recreated = await save({ name: unused.name, active });
|
|
|
|
assert.notEqual(recreated.id, unused.id);
|
|
assert.equal((await request(`${path}/${recreated.id}/delete`, {})).status, 200);
|
|
}
|
|
const name = `Marca prueba ${randomUUID()}`;
|
|
const created = await save({ name: ` ${name} `, active: true, logoSvg });
|
|
|
|
assert.equal(created.name, name);
|
|
assert.equal(created.id[14], "7");
|
|
assert.equal(created.builtin, false);
|
|
assert.equal(created.logoSvg, logoSvg);
|
|
assert.equal((await list()).find((brand) => brand.id === created.id)?.logoSvg, logoSvg);
|
|
const concurrentName = `Concurrente ${randomUUID()}`;
|
|
const concurrent = await Promise.all([
|
|
request(path, { values: { name: concurrentName, active: true } }),
|
|
request(path, { values: { name: concurrentName.toUpperCase(), active: true } }),
|
|
]);
|
|
|
|
assert.deepEqual(concurrent.map((response) => response.status).sort(), [201, 409]);
|
|
const catalog = CatalogResponseSchema.parse(
|
|
await (await request(`/asset-catalog/${companyId}`)).json()
|
|
);
|
|
const template = catalog.items.find((item) => item.values.name === "Portátil");
|
|
|
|
assert.ok(template);
|
|
const field = catalog.items.find((item) => item.referenceCatalog === "brands");
|
|
|
|
assert.ok(field);
|
|
const values = {
|
|
name: "PORT-MARCA",
|
|
templateId: template.id,
|
|
siteId: null,
|
|
status: "operational",
|
|
notes: "",
|
|
brandId: created.id,
|
|
characteristics: { [field.id]: "Nombre manipulado" },
|
|
};
|
|
|
|
async function asset(company: string, input: unknown, id?: string) {
|
|
const response = await request(`/assets/${company}${id ? `/${id}/update` : ""}`, {
|
|
values: input,
|
|
});
|
|
|
|
assert.equal(response.status, id ? 200 : 201, await response.clone().text());
|
|
|
|
return AssetResponseSchema.parse(await response.json()).asset;
|
|
}
|
|
|
|
const first = await asset(companyId, values);
|
|
|
|
assert.equal(first.values.brandId, created.id);
|
|
assert.equal(first.values.characteristics[field.id], name);
|
|
const same = await asset(companyId, { ...first.values }, first.id);
|
|
|
|
assert.equal(same.history.length, 1);
|
|
const foreignCatalog = CatalogResponseSchema.parse(
|
|
await (await request(`/asset-catalog/${otherId}`)).json()
|
|
);
|
|
const foreignTemplate = foreignCatalog.items.find((item) => item.values.name === "Portátil");
|
|
|
|
assert.ok(foreignTemplate);
|
|
const second = await asset(otherId, { ...values, templateId: foreignTemplate.id });
|
|
|
|
assert.equal(second.values.brandId, created.id);
|
|
await error(`${path}/${created.id}/delete`, {}, 409, "BRAND_IN_USE");
|
|
assert.ok((await list()).some((brand) => brand.id === created.id));
|
|
await save({ name, active: false }, created.id);
|
|
assert.equal((await list()).find((brand) => brand.id === created.id)?.logoSvg, logoSvg);
|
|
await error(`/assets/${companyId}`, { values }, 409, "BRAND_UNAVAILABLE");
|
|
const retained = await asset(
|
|
companyId,
|
|
{ ...first.values, notes: "Conserva marca inactiva" },
|
|
first.id
|
|
);
|
|
|
|
assert.equal(retained.values.brandId, created.id);
|
|
await save({ name: `${name} renombrada`, active: true }, created.id);
|
|
const renamed = await asset(companyId, retained.values, first.id);
|
|
|
|
assert.equal(renamed.values.characteristics[field.id], `${name} renombrada`);
|
|
assert.equal(renamed.history[0]?.values.characteristics[field.id], name);
|
|
await error(
|
|
`/assets/${companyId}`,
|
|
{ values: { ...values, brandId: randomUUID() } },
|
|
409,
|
|
"BRAND_UNAVAILABLE"
|
|
);
|
|
const snapshots = await database.query<{ brandId: string; values: unknown }>(
|
|
"SELECT brandId FROM asset_revisions WHERE assetId=:id ORDER BY position",
|
|
{ replacements: { id: first.id }, type: QueryTypes.SELECT }
|
|
);
|
|
|
|
assert.ok(snapshots.every((row) => row.brandId === created.id));
|
|
await save({ name: builtin.name, active: false }, builtin.id);
|
|
const changed = await save({ name: builtin.name, active: false, logoSvg }, builtin.id);
|
|
|
|
assert.equal(changed.logoSvg, logoSvg);
|
|
await migrateBrands(database);
|
|
const after = (await list()).find((brand) => brand.id === builtin.id);
|
|
|
|
assert.ok(after);
|
|
assert.equal(after.active, false);
|
|
assert.equal(after.logoSvg, logoSvg);
|
|
await save({ name: builtin.name, active: false, logoSvg: null }, builtin.id);
|
|
await migrateBrands(database);
|
|
assert.equal((await list()).find((brand) => brand.id === builtin.id)?.logoSvg, null);
|
|
assert.equal((await list()).length, new Set((await list()).map((brand) => brand.id)).size);
|
|
await save({ name: builtin.name, active: true, logoSvg: builtin.logoSvg }, builtin.id);
|
|
// El historial sigue protegiendo la marca aunque ningún activo la use actualmente.
|
|
await asset(companyId, { ...renamed.values, brandId: null }, first.id);
|
|
await asset(otherId, { ...second.values, brandId: null }, second.id);
|
|
await error(`${path}/${created.id}/delete`, {}, 409, "BRAND_IN_USE");
|
|
assert.ok((await list()).some((brand) => brand.id === created.id));
|
|
const racing = await save({ name: `Concurrente eliminación ${randomUUID()}`, active: true });
|
|
const [association, deletion] = await Promise.all([
|
|
request(`/assets/${companyId}`, { values: { ...values, brandId: racing.id } }),
|
|
request(`${path}/${racing.id}/delete`, {}),
|
|
]);
|
|
|
|
if (deletion.status === 200) {
|
|
assert.equal(association.status, 409, await association.clone().text());
|
|
assert.equal(
|
|
(await list()).some((brand) => brand.id === racing.id),
|
|
false
|
|
);
|
|
} else {
|
|
assert.equal(association.status, 201, await association.clone().text());
|
|
assert.equal(deletion.status, 409, await deletion.clone().text());
|
|
assert.equal(z.object({ code: z.string() }).parse(await deletion.json()).code, "BRAND_IN_USE");
|
|
}
|
|
}
|