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/); });