2026-10-05 11:13:51 +00:00
|
|
|
import assert from "node:assert/strict";
|
|
|
|
|
import { test } from "node:test";
|
|
|
|
|
|
|
|
|
|
import { UniqueID } from "@repo/rdx-ddd";
|
|
|
|
|
import { Maybe } from "@repo/rdx-utils";
|
|
|
|
|
|
|
|
|
|
import { ResourceCatalog } from "./resource-catalog.aggregate";
|
|
|
|
|
import { seedResourceCatalog } from "./resource-catalog-seed";
|
|
|
|
|
import type { ResourceDefinitionSpec } from "./resource-definition";
|
2026-10-10 20:22:28 +00:00
|
|
|
import { ResourceDefinition } from "./resource-definition.entity";
|
2026-10-09 08:38:19 +00:00
|
|
|
import { ResourceDefinitionValues } from "./value-objects/resource-definition-values.vo";
|
2026-10-05 11:13:51 +00:00
|
|
|
|
|
|
|
|
function values(input: ResourceDefinitionSpec) {
|
|
|
|
|
const result = ResourceDefinitionValues.create(input);
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-05 11:13:51 +00:00
|
|
|
assert.ok(result.isSuccess);
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-05 11:13:51 +00:00
|
|
|
return result.data;
|
|
|
|
|
}
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-05 11:13:51 +00:00
|
|
|
const field = {
|
|
|
|
|
kind: "field",
|
|
|
|
|
name: "Marca",
|
|
|
|
|
description: "",
|
|
|
|
|
type: "text",
|
|
|
|
|
unit: "",
|
|
|
|
|
choices: [],
|
|
|
|
|
} as const;
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-05 11:13:51 +00:00
|
|
|
test("catálogo de recursos: serie idempotente, UUID v7, características opcionales y protección", () => {
|
|
|
|
|
const catalog = seedResourceCatalog(ResourceCatalog.create());
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-12 18:31:21 +00:00
|
|
|
assert.equal(catalog.definitions.length, 28);
|
2026-10-05 11:13:51 +00:00
|
|
|
assert.equal(seedResourceCatalog(catalog), catalog);
|
|
|
|
|
assert.ok(catalog.definitions.every((item) => item.id.toPrimitive()[14] === "7"));
|
2026-10-10 20:22:28 +00:00
|
|
|
assert.ok(
|
|
|
|
|
catalog.definitions
|
|
|
|
|
.filter((item) => item.values.data.kind === "field")
|
|
|
|
|
.every((item) => item.values.data.description.includes("Ejemplo:"))
|
|
|
|
|
);
|
|
|
|
|
const previousDescription = ResourceDefinitionValues.create({
|
|
|
|
|
kind: "field",
|
|
|
|
|
name: "Dirección de correo",
|
|
|
|
|
description: "Indica la dirección completa del buzón.",
|
|
|
|
|
type: "text",
|
|
|
|
|
unit: "",
|
|
|
|
|
choices: [],
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
assert.ok(previousDescription.isSuccess);
|
|
|
|
|
const email = catalog.definitions.find(
|
|
|
|
|
(item) => item.stockKey.getOrUndefined() === "field:email"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
assert.ok(email);
|
|
|
|
|
const catalogWithoutHelp = ResourceCatalog.rehydrate(
|
|
|
|
|
{
|
|
|
|
|
definitions: catalog.definitions.map((item) =>
|
|
|
|
|
item.id.equals(email.id)
|
|
|
|
|
? ResourceDefinition.rehydrate(
|
|
|
|
|
{
|
|
|
|
|
companyId: item.companyId,
|
|
|
|
|
stockKey: item.stockKey,
|
|
|
|
|
values: previousDescription.data,
|
|
|
|
|
active: item.active,
|
|
|
|
|
bindings: item.bindings,
|
|
|
|
|
},
|
|
|
|
|
item.id
|
|
|
|
|
)
|
|
|
|
|
: item
|
|
|
|
|
),
|
|
|
|
|
availability: catalog.availability,
|
|
|
|
|
},
|
|
|
|
|
catalog.id
|
|
|
|
|
);
|
|
|
|
|
const enriched = seedResourceCatalog(catalogWithoutHelp);
|
|
|
|
|
|
|
|
|
|
assert.ok(
|
|
|
|
|
enriched.definitions
|
|
|
|
|
.find((item) => item.id.equals(email.id))
|
|
|
|
|
?.values.data.description.includes("Ejemplo:")
|
|
|
|
|
);
|
2026-10-05 11:13:51 +00:00
|
|
|
const template = catalog.definitions.find((item) => item.values.data.kind === "template");
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-05 11:13:51 +00:00
|
|
|
assert.ok(template && template.values.data.kind === "template");
|
|
|
|
|
assert.ok(template.values.data.fields.every((binding) => !binding.required));
|
|
|
|
|
assert.ok(template.bindings.every((binding) => binding.id.toPrimitive()[14] === "7"));
|
|
|
|
|
const changed = catalog.save(
|
|
|
|
|
Maybe.none(),
|
|
|
|
|
Maybe.some(template.id),
|
|
|
|
|
values({ ...template.values.data, name: "Cambio" })
|
|
|
|
|
);
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-10 20:00:45 +00:00
|
|
|
assert.ok(changed.isSuccess);
|
|
|
|
|
const edited = changed.data.definitions.find((item) => item.id.equals(template.id));
|
|
|
|
|
|
|
|
|
|
assert.equal(edited?.values.data.name, "Cambio");
|
|
|
|
|
assert.equal(seedResourceCatalog(changed.data), changed.data);
|
2026-10-05 11:13:51 +00:00
|
|
|
const disabled = catalog.setActive(Maybe.none(), template.id, false);
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-05 11:13:51 +00:00
|
|
|
assert.ok(disabled.isSuccess);
|
|
|
|
|
assert.equal(
|
|
|
|
|
seedResourceCatalog(disabled.data).definitions.find((item) => item.id.equals(template.id))
|
|
|
|
|
?.active,
|
|
|
|
|
false
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
test("ámbitos, tipos, listas, campos externos y disponibilidad por compañía", () => {
|
|
|
|
|
const company = UniqueID.generateNewID();
|
|
|
|
|
const other = UniqueID.generateNewID();
|
|
|
|
|
const saved = ResourceCatalog.create().save(
|
|
|
|
|
Maybe.some(company),
|
|
|
|
|
Maybe.none(),
|
|
|
|
|
values({ ...field, choices: [] })
|
|
|
|
|
);
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-05 11:13:51 +00:00
|
|
|
assert.ok(saved.isSuccess);
|
|
|
|
|
const local = saved.data.definitions[0];
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-05 11:13:51 +00:00
|
|
|
assert.ok(local);
|
|
|
|
|
assert.equal(saved.data.visible(Maybe.some(other)).length, 0);
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-05 11:13:51 +00:00
|
|
|
const template = (fieldId: string): ResourceDefinitionSpec => ({
|
|
|
|
|
kind: "template",
|
|
|
|
|
name: "Propia",
|
|
|
|
|
description: "",
|
|
|
|
|
fields: [{ fieldId, required: false }],
|
|
|
|
|
});
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-05 11:13:51 +00:00
|
|
|
for (const [scope, id] of [
|
|
|
|
|
[Maybe.none<UniqueID>(), local.id],
|
|
|
|
|
[Maybe.some(other), local.id],
|
|
|
|
|
[Maybe.some(company), UniqueID.generateNewID()],
|
|
|
|
|
] as const) {
|
|
|
|
|
const result = saved.data.save(scope, Maybe.none(), values(template(id.toPrimitive())));
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-05 11:13:51 +00:00
|
|
|
assert.ok(!result.isSuccess);
|
|
|
|
|
assert.equal(result.error.code, "FIELD_UNAVAILABLE");
|
|
|
|
|
}
|
|
|
|
|
const duplicate = saved.data.save(
|
|
|
|
|
Maybe.some(company),
|
|
|
|
|
Maybe.none(),
|
|
|
|
|
values({ ...field, choices: [], name: " marca " })
|
|
|
|
|
);
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-05 11:13:51 +00:00
|
|
|
assert.ok(!duplicate.isSuccess);
|
|
|
|
|
assert.equal(duplicate.error.code, "NAME_EXISTS");
|
|
|
|
|
const changeType = local.update(values({ ...field, choices: [], type: "number" }));
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-05 11:13:51 +00:00
|
|
|
assert.ok(!changeType.isSuccess);
|
|
|
|
|
assert.equal(changeType.error.code, "FIELD_TYPE_FIXED");
|
|
|
|
|
assert.equal(
|
|
|
|
|
ResourceDefinitionValues.create({ ...field, type: "choice", choices: ["A", " a "] }).isSuccess,
|
|
|
|
|
false
|
|
|
|
|
);
|
|
|
|
|
const choices = saved.data.save(
|
|
|
|
|
Maybe.none(),
|
|
|
|
|
Maybe.none(),
|
|
|
|
|
values({ ...field, name: "Lista", type: "choice", choices: ["A", "B"] })
|
|
|
|
|
);
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-05 11:13:51 +00:00
|
|
|
assert.ok(choices.isSuccess);
|
|
|
|
|
const list = choices.data.definitions.find((item) => item.values.data.name === "Lista");
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-05 11:13:51 +00:00
|
|
|
assert.ok(list);
|
|
|
|
|
assert.equal(
|
|
|
|
|
list.update(values({ ...field, name: "Lista", type: "choice", choices: ["A"] })).isSuccess,
|
|
|
|
|
false
|
|
|
|
|
);
|
|
|
|
|
const seeded = seedResourceCatalog(ResourceCatalog.create());
|
|
|
|
|
const stock = seeded.definitions[0];
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-05 11:13:51 +00:00
|
|
|
assert.ok(stock);
|
|
|
|
|
const override = seeded.setActive(Maybe.some(company), stock.id, false);
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-05 11:13:51 +00:00
|
|
|
assert.ok(override.isSuccess);
|
|
|
|
|
assert.equal(override.data.active(stock, Maybe.some(company)), false);
|
|
|
|
|
assert.equal(override.data.active(stock, Maybe.some(other)), true);
|
|
|
|
|
const reset = override.data.setActive(Maybe.some(company), stock.id, null);
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-05 11:13:51 +00:00
|
|
|
assert.ok(reset.isSuccess);
|
|
|
|
|
assert.equal(reset.data.active(stock, Maybe.some(company)), true);
|
|
|
|
|
});
|
2026-10-07 11:26:59 +00:00
|
|
|
|
|
|
|
|
test("licencias: ampliar un catálogo previo conserva identidades y disponibilidad", () => {
|
|
|
|
|
const seeded = seedResourceCatalog(ResourceCatalog.create());
|
|
|
|
|
const originalKeys = new Set([
|
|
|
|
|
"field:email",
|
|
|
|
|
"field:provider",
|
|
|
|
|
"field:context",
|
|
|
|
|
"field:unc",
|
|
|
|
|
"field:server",
|
|
|
|
|
"field:description",
|
|
|
|
|
"field:url",
|
|
|
|
|
"field:holder",
|
|
|
|
|
"field:issuer",
|
|
|
|
|
"field:expires",
|
|
|
|
|
"field:fingerprint",
|
|
|
|
|
"field:purpose",
|
|
|
|
|
"field:reference",
|
|
|
|
|
"template:mailbox",
|
|
|
|
|
"template:access",
|
|
|
|
|
"template:shared_folder",
|
|
|
|
|
"template:application",
|
|
|
|
|
"template:certificate",
|
|
|
|
|
]);
|
|
|
|
|
const previous = ResourceCatalog.rehydrate(
|
|
|
|
|
{
|
|
|
|
|
definitions: seeded.definitions.filter((item) =>
|
|
|
|
|
originalKeys.has(item.stockKey.getOrUndefined() ?? "")
|
|
|
|
|
),
|
|
|
|
|
availability: [],
|
|
|
|
|
},
|
|
|
|
|
seeded.id
|
|
|
|
|
);
|
|
|
|
|
const original = previous.definitions[0];
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-07 11:26:59 +00:00
|
|
|
assert.ok(original);
|
|
|
|
|
const disabled = previous.setActive(Maybe.none(), original.id, false);
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-07 11:26:59 +00:00
|
|
|
assert.ok(disabled.isSuccess);
|
|
|
|
|
const upgraded = seedResourceCatalog(disabled.data);
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-12 18:31:21 +00:00
|
|
|
assert.equal(upgraded.definitions.length, 28);
|
2026-10-07 11:26:59 +00:00
|
|
|
for (const existing of disabled.data.definitions) {
|
|
|
|
|
assert.equal(
|
|
|
|
|
upgraded.definitions.find((item) => item.id.equals(existing.id)),
|
|
|
|
|
existing
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
const license = upgraded.definitions.find(
|
|
|
|
|
(item) => item.stockKey.getOrUndefined() === "template:software_license"
|
|
|
|
|
);
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-07 11:26:59 +00:00
|
|
|
assert.ok(license && license.values.data.kind === "template");
|
|
|
|
|
assert.ok(license.values.data.fields.every((binding) => !binding.required));
|
2026-10-13 07:30:28 +00:00
|
|
|
assert.equal(license.values.data.usage, "license");
|
2026-10-10 20:00:45 +00:00
|
|
|
const editedTemplate = license.update(values({ ...license.values.data, name: "Otra" }));
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-10 20:00:45 +00:00
|
|
|
assert.ok(editedTemplate.isSuccess);
|
|
|
|
|
assert.ok(editedTemplate.data.id.equals(license.id));
|
2026-10-07 11:26:59 +00:00
|
|
|
const company = UniqueID.generateNewID();
|
|
|
|
|
const locallyDisabled = upgraded.setActive(Maybe.some(company), license.id, false);
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-07 11:26:59 +00:00
|
|
|
assert.ok(locallyDisabled.isSuccess);
|
|
|
|
|
assert.equal(locallyDisabled.data.active(license, Maybe.some(company)), false);
|
|
|
|
|
assert.equal(locallyDisabled.data.active(license, Maybe.none()), true);
|
|
|
|
|
assert.equal(seedResourceCatalog(locallyDisabled.data), locallyDisabled.data);
|
|
|
|
|
});
|
2026-10-10 20:00:45 +00:00
|
|
|
|
|
|
|
|
test("plantillas globales de serie editables: conserva identidad, asociaciones, disponibilidad y cambios al reiniciar", () => {
|
|
|
|
|
const initial = seedResourceCatalog(ResourceCatalog.create());
|
|
|
|
|
const template = initial.definitions.find((item) => item.values.data.kind === "template");
|
|
|
|
|
|
|
|
|
|
assert.ok(template?.values.data.kind === "template");
|
|
|
|
|
const original = template.values.data;
|
|
|
|
|
const input = values({
|
|
|
|
|
...original,
|
|
|
|
|
name: "Plantilla revisada",
|
|
|
|
|
description: "Personalizada",
|
|
|
|
|
sections: [],
|
|
|
|
|
fields: original.fields
|
|
|
|
|
.slice(0, 1)
|
|
|
|
|
.map((field) => ({ fieldId: field.fieldId, required: true })),
|
|
|
|
|
});
|
|
|
|
|
const result = initial.save(Maybe.none(), Maybe.some(template.id), input);
|
|
|
|
|
|
|
|
|
|
assert.ok(result.isSuccess);
|
|
|
|
|
const edited = result.data.definitions.find((item) => item.id.equals(template.id));
|
|
|
|
|
|
|
|
|
|
assert.ok(edited?.values.data.kind === "template");
|
|
|
|
|
assert.equal(edited.values.data.name, "Plantilla revisada");
|
|
|
|
|
assert.equal(edited.values.data.fields[0]?.required, true);
|
|
|
|
|
assert.ok(edited.bindings[0]?.id.equals(template.bindings[0]?.id));
|
|
|
|
|
assert.equal(edited.stockKey.getOrUndefined(), template.stockKey.getOrUndefined());
|
|
|
|
|
assert.equal(edited.active, template.active);
|
|
|
|
|
assert.equal(seedResourceCatalog(result.data), result.data);
|
|
|
|
|
assert.deepEqual(template.values.data, original);
|
|
|
|
|
const repeated = result.data.save(Maybe.none(), Maybe.some(template.id), input);
|
|
|
|
|
|
|
|
|
|
assert.ok(repeated.isSuccess);
|
|
|
|
|
assert.equal(repeated.data, result.data);
|
|
|
|
|
const crossScope = result.data.save(
|
|
|
|
|
Maybe.some(UniqueID.generateNewID()),
|
|
|
|
|
Maybe.some(template.id),
|
|
|
|
|
input
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
assert.ok(!crossScope.isSuccess);
|
|
|
|
|
const protectedField = initial.definitions.find((item) => item.values.data.kind === "field");
|
|
|
|
|
|
|
|
|
|
assert.ok(protectedField);
|
|
|
|
|
const fieldChange = initial.save(
|
|
|
|
|
Maybe.none(),
|
|
|
|
|
Maybe.some(protectedField.id),
|
|
|
|
|
values({ ...protectedField.values.data, name: "Campo revisado" })
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
assert.ok(fieldChange.isSuccess);
|
|
|
|
|
const fieldEdited = fieldChange.data.definitions.find((item) =>
|
|
|
|
|
item.id.equals(protectedField.id)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
assert.equal(fieldEdited?.values.data.name, "Campo revisado");
|
|
|
|
|
assert.equal(seedResourceCatalog(fieldChange.data), fieldChange.data);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("campos globales de serie editables: listas ampliables, tipo fijo y opciones existentes conservadas", () => {
|
|
|
|
|
const catalog = seedResourceCatalog(ResourceCatalog.create());
|
|
|
|
|
const field = catalog.definitions.find(
|
|
|
|
|
(item) => item.values.data.kind === "field" && item.values.data.type === "choice"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
assert.ok(field?.values.data.kind === "field");
|
|
|
|
|
const previous = field.values.data;
|
|
|
|
|
const added = values({
|
|
|
|
|
...previous,
|
|
|
|
|
name: "Lista revisada",
|
|
|
|
|
description: "Personalizada",
|
|
|
|
|
unit: "unidad",
|
|
|
|
|
choices: [...previous.choices, "Nueva opción"],
|
|
|
|
|
});
|
|
|
|
|
const result = catalog.save(Maybe.none(), Maybe.some(field.id), added);
|
|
|
|
|
|
|
|
|
|
assert.ok(result.isSuccess);
|
|
|
|
|
const edited = result.data.definitions.find((item) => item.id.equals(field.id));
|
|
|
|
|
|
|
|
|
|
assert.ok(edited);
|
|
|
|
|
assert.deepEqual(edited.values.data, added.data);
|
|
|
|
|
assert.equal(seedResourceCatalog(result.data), result.data);
|
|
|
|
|
assert.deepEqual(field.values.data, previous);
|
|
|
|
|
const differentType = result.data.save(
|
|
|
|
|
Maybe.none(),
|
|
|
|
|
Maybe.some(field.id),
|
|
|
|
|
values({ ...previous, type: "text", choices: [] })
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
assert.ok(!differentType.isSuccess);
|
|
|
|
|
assert.equal(differentType.error.code, "FIELD_TYPE_FIXED");
|
|
|
|
|
const removedOption = result.data.save(Maybe.none(), Maybe.some(field.id), values(previous));
|
|
|
|
|
|
|
|
|
|
assert.ok(!removedOption.isSuccess);
|
|
|
|
|
assert.equal(removedOption.error.code, "FIELD_TYPE_FIXED");
|
|
|
|
|
});
|
2026-10-12 18:31:21 +00:00
|
|
|
|
|
|
|
|
test("sistema operativo: plantilla con uso explícito, copia y ampliación sin sobrescribir definiciones", () => {
|
|
|
|
|
const catalog = seedResourceCatalog(ResourceCatalog.create());
|
|
|
|
|
const template = catalog.definitions.find(
|
|
|
|
|
(item) => item.stockKey.getOrUndefined() === "template:os"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
assert.ok(template && template.values.data.kind === "template");
|
|
|
|
|
assert.equal(template.values.data.usage, "os");
|
|
|
|
|
assert.equal(template.values.data.fields.length, 3);
|
|
|
|
|
assert.equal(seedResourceCatalog(catalog), catalog);
|
|
|
|
|
});
|