metis-desk/modules/resources/src/api/infrastructure/persistence/resource-scope-reader.test.ts
2026-10-25 01:19:17 +02:00

144 lines
4.9 KiB
TypeScript

import assert from "node:assert/strict";
import { test } from "node:test";
import { Sequelize, Transaction } from "sequelize";
import { buildResourceResponse } from "../../application/resource-response.builder";
import { scopeFixture } from "../../domain/resource-scope.fixture";
import { ResourceValues } from "../../domain/value-objects/resource-values.vo";
import { projectDirectoryDomain } from "./directory-domain-projection";
import { projectNetworkShare } from "./network-share-projection";
import { projectResource } from "./resource-compatibility";
import { ResourcePersistenceMapper } from "./resource-persistence.mapper";
test("lector legacy: roundtrip conserva UUID, revisiones, valores, actor, fecha y DTO sin columna scope", () => {
const { local, actor } = scopeFixture();
const values = ResourceValues.create(
{ ...local.values.toPrimitive(), active: false, notes: "Cambio auditado" },
local.specification
);
assert.ok(values.isSuccess);
const changed = local.update(values.data, actor, "2026-10-24T12:00:00.000Z");
assert.ok(changed.isSuccess);
const mapper = new ResourcePersistenceMapper();
const stored = mapper.toPersistence(changed.data);
const read = mapper.toDomain(stored.resource, stored.revisions);
assert.equal(read.scope, "company");
assert.deepEqual(mapper.toPersistence(read), stored);
assert.equal(Object.hasOwn(stored.resource, "scope"), false);
assert.deepEqual(
buildResourceResponse(read, new Map()),
buildResourceResponse(changed.data, new Map())
);
assert.deepEqual(
mapper.toPersistence(
mapper.toScopedDomain({ ...stored.resource, scope: "company" }, stored.revisions)
),
stored
);
});
test("lector: solo el adaptador legacy conocido interpreta ausencia; ningún ámbito inválido usa fallback", () => {
const { local } = scopeFixture();
const mapper = new ResourcePersistenceMapper();
const stored = mapper.toPersistence(local);
for (const patch of [
{ scope: "global", companyId: stored.resource.companyId },
{ scope: "company", companyId: null },
{ scope: "other" },
{ scope: undefined },
{ companyId: null },
{ companyId: undefined },
{ companyId: "invalid" },
]) {
assert.throws(
() =>
Reflect.apply(mapper.toDomain, mapper, [
{ ...stored.resource, ...patch },
stored.revisions,
]),
/INVALID_RESOURCE_SCOPE/
);
assert.throws(
() =>
Reflect.apply(mapper.toScopedDomain, mapper, [
{ ...stored.resource, ...patch },
stored.revisions,
]),
/INVALID_RESOURCE_SCOPE/
);
}
assert.throws(
() => Reflect.apply(mapper.toScopedDomain, mapper, [stored.resource, stored.revisions]),
/INVALID_RESOURCE_SCOPE/
);
const global = mapper.toScopedDomain(
{ ...stored.resource, scope: "global", companyId: null },
stored.revisions
);
assert.equal(global.companyId, null);
assert.equal(global.scope, "global");
assert.deepEqual(global.history, local.history);
assert.throws(() => mapper.toPersistence(global), /RESOURCE_SCOPE_UNSUPPORTED/);
});
test("proyecciones operativas: global sintético se rechaza antes de SQL, el recurso local sin integración no crea proyección", async () => {
const { local, global, catalog } = scopeFixture();
const database = new Sequelize({
dialect: "mariadb",
database: "metis_desk_test",
logging: false,
});
const transaction = new Transaction(database, {});
// Una conexión cerrada hace fallar cualquier acceso accidental a persistencia.
await database.close();
await assert.rejects(
() => projectResource(database, global, transaction),
/RESOURCE_SCOPE_UNSUPPORTED/
);
await assert.rejects(
() => projectDirectoryDomain(database, global, catalog, undefined, undefined, transaction),
/RESOURCE_SCOPE_UNSUPPORTED/
);
await assert.rejects(
() => projectNetworkShare(database, global, catalog, undefined, false, transaction),
/RESOURCE_SCOPE_UNSUPPORTED/
);
assert.equal(await projectResource(database, local, transaction), undefined);
const directory = await projectDirectoryDomain(
database,
local,
catalog,
undefined,
undefined,
transaction
);
assert.ok(directory.isSuccess);
assert.equal(directory.data, undefined);
assert.equal(
await projectNetworkShare(database, local, catalog, undefined, false, transaction),
undefined
);
});
test("escritor scoped: envía company explícito, preserva la salida legacy y rechaza global", () => {
const { local, global } = scopeFixture();
const mapper = new ResourcePersistenceMapper();
const legacy = mapper.toPersistence(local);
const scoped = mapper.toScopedPersistence(local);
const { scope, ...row } = scoped.resource;
assert.equal(scope, "company");
assert.deepEqual(row, legacy.resource);
assert.deepEqual(scoped.revisions, legacy.revisions);
assert.throws(() => mapper.toScopedPersistence(global), /RESOURCE_SCOPE_UNSUPPORTED/);
});