metis-desk/modules/assets/src/api/domain/asset-catalog.test.ts

273 lines
9.7 KiB
TypeScript
Raw Normal View History

import assert from "node:assert/strict";
import { test } from "node:test";
import { UniqueID } from "@repo/rdx-ddd";
import { Maybe } from "@repo/rdx-utils";
import { AssetCatalog } from "./asset-catalog.aggregate";
import { seedAssetCatalog } from "./asset-catalog-seed";
import type { AssetDefinitionSpec } from "./asset-definition";
import { AssetDefinitionSchema } from "./asset-definition";
import { AssetDefinition } from "./asset-definition.entity";
2026-10-09 08:38:19 +00:00
import { AssetDefinitionValues } from "./value-objects/asset-definition-values.vo";
const global = Maybe.none<UniqueID>();
2026-10-10 10:01:58 +00:00
function values(spec: AssetDefinitionSpec) {
const result = AssetDefinitionValues.create(spec);
2026-10-10 10:01:58 +00:00
assert.ok(result.isSuccess);
2026-10-10 10:01:58 +00:00
return result.data;
}
2026-10-10 10:01:58 +00:00
function field(name: string) {
return values({ kind: "field", name, type: "text", unit: "", choices: [], description: "" });
}
2026-10-10 10:01:58 +00:00
function changed(result: ReturnType<AssetCatalog["save"]>) {
assert.ok(result.isSuccess);
2026-10-10 10:01:58 +00:00
return result.data;
}
2026-10-10 10:01:58 +00:00
test("el catálogo inicial es idempotente, con UUID propios y características opcionales", () => {
const catalog = seedAssetCatalog(AssetCatalog.create());
2026-10-10 10:01:58 +00:00
2026-10-04 19:04:14 +00:00
assert.equal(catalog.definitions.length, 47);
assert.equal(seedAssetCatalog(catalog), catalog);
const ids = [
catalog.id,
...catalog.definitions.map((item) => item.id),
...catalog.definitions.flatMap((item) => item.bindings.map((binding) => binding.id)),
].map((id) => id.toPrimitive());
2026-10-10 10:01:58 +00:00
assert.equal(new Set(ids).size, ids.length);
assert.ok(ids.every((id) => id.charAt(14) === "7"));
const templates = catalog.definitions.filter((item) => item.values.data.kind === "template");
2026-10-10 10:01:58 +00:00
assert.equal(templates.length, 12);
assert.ok(templates.every((item) => item.bindings.every((field) => !field.required)));
const rehydrated = AssetCatalog.rehydrate(
{ definitions: catalog.definitions, availability: catalog.availability },
catalog.id
);
2026-10-10 10:01:58 +00:00
assert.ok(rehydrated.id.equals(catalog.id));
assert.ok(rehydrated.definitions[0]?.id.equals(catalog.definitions[0]?.id));
const stock = catalog.definitions[0];
2026-10-10 10:01:58 +00:00
assert.ok(stock);
const protectedResult = catalog.save(global, Maybe.some(stock.id), field("Cambio"));
2026-10-10 10:01:58 +00:00
assert.ok(!protectedResult.isSuccess);
assert.equal(protectedResult.error.code, "BUILTIN_PROTECTED");
});
test("la disponibilidad por compañía conserva el global y los campos desactivados", () => {
let catalog = seedAssetCatalog(AssetCatalog.create());
const first = Maybe.some(UniqueID.generateNewID());
const second = Maybe.some(UniqueID.generateNewID());
const stock = catalog.definitions[0];
2026-10-10 10:01:58 +00:00
assert.ok(stock);
catalog = changed(catalog.setActive(first, stock.id, false));
assert.equal(catalog.active(stock, first), false);
assert.equal(catalog.active(stock, second), true);
assert.equal(stock.active, true);
2026-10-04 19:04:14 +00:00
assert.equal(catalog.definitions.length, 47);
const overrideId = catalog.availability[0]?.id;
2026-10-10 10:01:58 +00:00
assert.ok(overrideId);
assert.equal(changed(catalog.setActive(first, stock.id, false)), catalog);
catalog = changed(catalog.setActive(first, stock.id, null));
assert.equal(catalog.active(stock, first), true);
assert.ok(catalog.availability[0]?.id.equals(overrideId));
catalog = changed(catalog.setActive(global, stock.id, false));
assert.equal(seedAssetCatalog(catalog), catalog);
assert.equal(catalog.active(catalog.definitions[0] ?? stock, first), false);
});
test("las plantillas validan ámbito, campos activos e identidad estable al editar", () => {
let catalog = seedAssetCatalog(AssetCatalog.create());
const company = Maybe.some(UniqueID.generateNewID());
const other = Maybe.some(UniqueID.generateNewID());
2026-10-10 10:01:58 +00:00
catalog = changed(catalog.save(company, Maybe.none(), field("Campo local")));
const local = catalog.definitions.at(-1);
2026-10-10 10:01:58 +00:00
assert.ok(local);
const template = values({
kind: "template",
name: "Plantilla local",
description: "",
fields: [{ fieldId: local.id.toPrimitive(), required: false }],
});
2026-10-10 10:01:58 +00:00
assert.ok(!catalog.save(global, Maybe.none(), template).isSuccess);
assert.ok(!catalog.save(other, Maybe.none(), template).isSuccess);
catalog = changed(catalog.save(company, Maybe.none(), template));
const saved = catalog.definitions.at(-1);
2026-10-10 10:01:58 +00:00
assert.ok(saved);
assert.equal(changed(catalog.save(company, Maybe.some(saved.id), template)), catalog);
const updated = values({ ...template.data, name: "Otra denominación" });
2026-10-10 10:01:58 +00:00
catalog = changed(catalog.save(company, Maybe.some(saved.id), updated));
const edited = catalog.definitions.at(-1);
2026-10-10 10:01:58 +00:00
assert.ok(edited);
assert.ok(edited.id.equals(saved.id));
assert.ok(edited.bindings[0]?.id.equals(saved.bindings[0]?.id));
catalog = changed(catalog.setActive(company, local.id, false));
assert.ok(
!catalog.save(company, Maybe.none(), values({ ...template.data, name: "Nueva" })).isSuccess
);
assert.ok(catalog.save(company, Maybe.some(saved.id), updated).isSuccess);
assert.ok(!catalog.visible(other).some((item) => item.id.equals(local.id)));
});
test("los campos validan listas, igualdad semántica, duplicados y tipo fijo", () => {
const list: AssetDefinitionSpec = {
kind: "field",
name: "Batería",
description: "",
type: "choice",
unit: "",
choices: ["Buena", "Sustituir"],
};
2026-10-10 10:01:58 +00:00
assert.ok(!AssetDefinitionValues.create({ ...list, choices: [] }).isSuccess);
assert.ok(!AssetDefinitionValues.create({ ...list, choices: ["Buena", " buena "] }).isSuccess);
assert.ok(!AssetDefinitionValues.create({ ...list, type: "text" }).isSuccess);
const frozen = values(list);
2026-10-10 10:01:58 +00:00
assert.ok(Object.isFrozen(frozen.data));
if (frozen.data.kind === "field") {
assert.ok(Object.isFrozen(frozen.data.choices));
}
let catalog = AssetCatalog.create();
2026-10-10 10:01:58 +00:00
catalog = changed(catalog.save(global, Maybe.none(), frozen));
const saved = catalog.definitions[0];
2026-10-10 10:01:58 +00:00
assert.ok(saved);
assert.ok(!catalog.save(global, Maybe.none(), values({ ...list, name: " batería " })).isSuccess);
assert.ok(!catalog.save(global, Maybe.some(saved.id), field("Batería")).isSuccess);
assert.ok(
!catalog.save(global, Maybe.some(saved.id), values({ ...list, choices: ["Buena"] })).isSuccess
);
assert.ok(
catalog.save(
global,
Maybe.some(saved.id),
values({ ...list, choices: [...list.choices, "Desconocida"] })
).isSuccess
);
});
test("iconos opcionales: cambio y retirada conservan identidad y asociaciones", () => {
let catalog = seedAssetCatalog(AssetCatalog.create());
const fieldId = catalog.definitions[0]?.id.toPrimitive();
assert.ok(fieldId);
const base = {
kind: "template",
name: "Equipo con icono",
description: "",
fields: [{ fieldId, required: false }],
} satisfies AssetDefinitionSpec;
const company = Maybe.some(UniqueID.generateNewID());
for (const scope of [global, company]) {
const input = {
...base,
name: scope === global ? "Plantilla global con icono" : "Plantilla local con icono",
};
catalog = changed(catalog.save(scope, Maybe.none(), values(input)));
const original = catalog.definitions.at(-1);
assert.ok(original);
catalog = changed(
catalog.save(scope, Maybe.some(original.id), values({ ...input, icon: "laptop" }))
);
const updated = catalog.definitions.find((item) => item.id.equals(original.id));
assert.ok(updated?.values.data.kind === "template");
assert.equal(updated.values.data.icon, "laptop");
assert.ok(updated.bindings[0]?.id.equals(original.bindings[0]?.id));
assert.equal(changed(catalog.save(scope, Maybe.some(original.id), updated.values)), catalog);
catalog = changed(catalog.save(scope, Maybe.some(original.id), values(input)));
const cleared = catalog.definitions.find((item) => item.id.equals(original.id));
assert.ok(cleared?.values.data.kind === "template");
assert.equal(cleared.values.data.icon, undefined);
}
});
test("el arranque completa iconos de serie sin cambiar UUID, campos o disponibilidad", () => {
const initial = seedAssetCatalog(AssetCatalog.create());
const template = initial.definitions.find(
(item) => item.stockKey.getOrUndefined() === "template:laptop"
);
assert.ok(template?.values.data.kind === "template");
assert.equal(template.values.data.icon, "laptop");
const { icon, ...withoutIcon } = template.values.data;
assert.ok(icon);
const previous = AssetDefinition.rehydrate(
{
companyId: template.companyId,
stockKey: template.stockKey,
values: values(withoutIcon),
active: false,
bindings: template.bindings,
},
template.id
);
const upgraded = seedAssetCatalog(
AssetCatalog.rehydrate(
{
definitions: initial.definitions.map((item) =>
item.id.equals(template.id) ? previous : item
),
availability: initial.availability,
},
initial.id
)
);
const saved = upgraded.definitions.find((item) => item.id.equals(template.id));
assert.ok(saved?.values.data.kind === "template");
assert.equal(saved.values.data.icon, "laptop");
assert.equal(saved.active, false);
assert.deepEqual(saved.values.data.fields, template.values.data.fields);
assert.deepEqual(saved.bindings, template.bindings);
assert.equal(seedAssetCatalog(upgraded), upgraded);
});
test("los iconos desconocidos o asociados a campos se rechazan", () => {
const template = {
kind: "template",
name: "Equipo",
description: "",
fields: [{ fieldId: UniqueID.generateNewID().toPrimitive(), required: false }],
};
assert.equal(
AssetDefinitionSchema.safeParse({ ...template, icon: "unknown-icon" }).success,
false
);
assert.equal(
AssetDefinitionSchema.safeParse({
kind: "field",
name: "Campo",
description: "",
type: "text",
unit: "",
choices: [],
icon: "laptop",
}).success,
false
);
});