import assert from "node:assert/strict"; import { createHash, randomBytes } from "node:crypto"; import { once } from "node:events"; import { test } from "node:test"; import type { ApplicationConfigurationInput } from "@metis/companies/common"; import { ApplicationConfigurationResponseSchema, ApplicationConfigurationsResponseSchema, CompanyResponseSchema, WorkProfileResponseSchema, } from "@metis/companies/common"; import { ApplicationCoincidencesResponseSchema, CatalogResponseSchema, GetGlobalApplicationAdminResponseSchema, GlobalApplicationTemplateResponseSchema, InventoryResponseSchema, ListApplicationCandidatesResponseSchema, ResourceResponseSchema, } from "@metis/resources/common"; import { migrateResourceScope } from "@metis/resources/module"; import { QueryTypes } from "sequelize"; import { UniqueID } from "../../../packages/rdx-ddd/src/index"; import { createApp } from "./app"; import { prepareMembershipFixture } from "./asset-directory-memberships.fixture"; import { createDatabase, readDatabaseEnvironment } from "./config/database"; import { createIdentityRuntime } from "./infrastructure/http/identity-runtime"; import { disposableScopeEnvironment, writeScopeEvidence } from "./resource-scope.preflight"; import { scopeApiClient } from "./resource-scope-api.client"; import { scopeHttpFixture, seedScopeVolume } from "./resource-scope-http.fixture"; test("R2.6: coincidencias contextuales acotadas sin mutaciones", async (t) => { const database = createDatabase(disposableScopeEnvironment()); const owners = Array.from({ length: 15 }, () => UniqueID.generateNewID().toPrimitive()); const company = owners[0]; const other = owners[1]; assert.ok(company && other); const actor = UniqueID.generateNewID().toPrimitive(); const fixture = await prepareMembershipFixture(database, "http://localhost:5173", actor, owners); await migrateResourceScope(database); const seedHttp = await scopeHttpFixture(database, fixture); const volume = await seedScopeVolume(fixture, seedHttp, company); await seedHttp.close(); const a = volume.profileIds[0]; const a2 = volume.profileIds[1]; const host = volume.assetIds[0]; assert.ok(a && a2 && host); const foreignHost = await fixture.asset(other, "laptop"); let runtime = await createIdentityRuntime( database, readDatabaseEnvironment({ ...process.env, RESOURCE_GLOBAL_CATALOG_ENABLED: "true", RESOURCE_GLOBAL_SOFTWARE_ENABLED: "false", RESOURCE_GLOBAL_SOFTWARE_CANDIDATES_ENABLED: "false", RESOURCE_GLOBAL_PROFILE_ENABLED: "false", }) ); let server = createApp(() => undefined, runtime).listen(0, "127.0.0.1"); await once(server, "listening"); const address = server.address(); assert.ok(address && typeof address !== "string"); const base = `http://127.0.0.1:${address.port}`; const admin = await scopeApiClient(base); const ordinary = await scopeApiClient(base); const password = randomBytes(24).toString("hex"); try { assert.equal( ( await admin.request("/auth/bootstrap", { name: "Preparación R2.6", email: "r25-admin@example.invalid", password, }) ).status, 201 ); await admin.refresh(); assert.equal( ( await admin.request("/users", { name: "Operador R2.6", email: "r25-user@example.invalid", password, }) ).status, 201 ); assert.equal( (await ordinary.request("/auth/login", { email: "r25-user@example.invalid", password })) .status, 200 ); await ordinary.refresh(); const specification = GlobalApplicationTemplateResponseSchema.parse( await (await admin.request("/resource-global-applications/template")).json() ).specification; const product = async (name: string, active = true) => { const response = await admin.request("/resource-global-applications", { creationId: UniqueID.generateNewID().toPrimitive(), values: { name, templateId: specification.templateId, active, notes: "Gestión privada R2.6", characteristics: {}, }, }); assert.equal(response.status, 201, await response.clone().text()); return GetGlobalApplicationAdminResponseSchema.parse(await response.json()).resource; }; const global = await product("Ágil compartida R2.6"); const _inactive = await product("Inactiva R2.6", false); const catalog = CatalogResponseSchema.parse( await (await ordinary.request(`/resource-catalog/${company}`)).json() ); const template = (key: string) => { const found = catalog.items.find((item) => item.builtinKey === key); assert.ok(found); return found; }; const local = async (owner: string, templateId: string, name: string) => { const response = await ordinary.request(`/resources/${owner}`, { values: { name, templateId, active: true, notes: "", characteristics: {} }, }); assert.equal(response.status, 201); return ResourceResponseSchema.parse(await response.json()).resource.id; }; const own = await local(company, specification.templateId, "Ágil particular"); const foreign = await local(other, specification.templateId, "Ágil ajena"); const os = await local(company, template("template:os").id, "SO no configurable"); const license = await local( company, template("template:software_license").id, "Licencia no configurable" ); const certificate = await local( company, template("template:certificate").id, "Certificado no configurable" ); const locals: string[] = []; for (let index = 0; index < 50; index++) { locals.push(await local(company, specification.templateId, `Ágil volumen ${index + 1}`)); } const personResult = await ordinary.request(`/companies/${other}/people`, { name: "Persona de B", active: true, isContact: false, position: null, email: null, phone: null, }); const person = CompanyResponseSchema.parse(await personResult.json()).company.people.at(-1); assert.ok(person); const otherWorkstation = await ordinary.request(`/assets/${other}/workstations`, { values: { name: "Puesto B R2.6", active: true, siteId: null, personId: person.id, computerAssetId: foreignHost.id.toPrimitive(), assetIds: [foreignHost.id.toPrimitive()], }, }); assert.equal(otherWorkstation.status, 201); const profileResult = await ordinary.request(`/work-profiles/${other}`, { values: { name: "Perfil de B", personId: person.id, assetId: foreignHost.id.toPrimitive(), accountId: null, active: true, notes: null, }, }); assert.equal(profileResult.status, 201, await profileResult.clone().text()); const _b = WorkProfileResponseSchema.parse(await profileResult.json()).profile.id; const path = `/application-configurations/${company}`; const input: ApplicationConfigurationInput = { profileId: a, resourceId: global.id, delivery: "installed", version: "123.4", state: "pending", notes: "Configuración A", }; const save = async (values: ApplicationConfigurationInput, owner = company) => { const response = await ordinary.request(`/application-configurations/${owner}`, { values }); assert.equal(response.status, 201, await response.clone().text()); return ApplicationConfigurationResponseSchema.parse(await response.json()).configuration; }; const first = await save({ ...input, resourceId: own }); const installed = await ordinary.request(`/software-installations/${company}`, { values: { assetId: host, resourceId: own, licenseResourceId: license, version: "LOCAL-123", state: "configured", notes: "No migrar", }, }); assert.equal(installed.status, 201); const endpoint = `/resource-application-coincidences/${company}/${own}`; const measurements: unknown[] = []; const snapshot = async () => { const result: Record = {}; for (const table of [ "resources", "resource_revisions", "resource_characteristics", "software_installations", "software_installation_revisions", "application_configurations", "application_configuration_revisions", ]) { const rows = await database.query( `SELECT * FROM ${table} ORDER BY ${table === "resource_characteristics" ? "resourceId,fieldId" : "id"}`, { type: QueryTypes.SELECT } ); result[table] = createHash("sha256").update(JSON.stringify(rows)).digest("hex"); } return result; }; await t.test("volumen 10/100/1000/10000: recorrido acotado y consultas medidas", async () => { const rootRows = await database.query>( "SELECT * FROM resources WHERE id=:id", { replacements: { id: global.id }, type: QueryTypes.SELECT } ); const root = rootRows[0]; assert.ok(root); const revisionRows = await database.query>( "SELECT * FROM resource_revisions WHERE resourceId=:id", { replacements: { id: global.id }, type: QueryTypes.SELECT } ); const revision = revisionRows[0]; assert.ok(revision && typeof revision.id === "string"); const related = await database.query<{ tableName: string; columnName: string }>( "SELECT DISTINCT TABLE_NAME AS tableName,COLUMN_NAME AS columnName FROM information_schema.KEY_COLUMN_USAGE WHERE TABLE_SCHEMA=DATABASE() AND REFERENCED_TABLE_NAME IN ('resources','resource_revisions')", { type: QueryTypes.SELECT } ); const children = new Map[]>(); for (const table of new Set(related.map((row) => row.tableName))) { if (table === "resource_revisions") { continue; } assert.match(table, /^[a-z_]+$/u); const fields = related .filter((row) => row.tableName === table) .map((row) => row.columnName); for (const field of fields) { assert.match(field, /^[A-Za-z_]+$/u); } const rows = await database.query>( `SELECT * FROM ${table} WHERE ${fields.map((field) => `\`${field}\` IN (:parents)`).join(" OR ")}`, { replacements: { parents: [global.id, revision.id] }, type: QueryTypes.SELECT } ); if (rows.length) { children.set(table, rows); } } let total = 2; for (const size of [10, 100, 1000, 10000]) { await database.transaction(async (transaction) => { while (total < size) { const ids = Array.from({ length: Math.min(100, size - total) }, () => ({ root: UniqueID.generateNewID().toPrimitive(), revision: UniqueID.generateNewID().toPrimitive(), })); const copy = (row: Record, pair: { root: string; revision: string }) => Object.fromEntries( Object.entries(row).map(([key, value]) => [ key, value === global.id ? pair.root : value === revision.id ? pair.revision : value, ]) ); await database.getQueryInterface().bulkInsert( "resources", ids.map((pair) => copy(root, pair)), { transaction } ); await database.getQueryInterface().bulkInsert( "resource_revisions", ids.map((pair) => copy(revision, pair)), { transaction } ); for (const [table, rows] of children) { await database.getQueryInterface().bulkInsert( table, ids.flatMap((pair) => rows.map((row) => copy(row, pair))), { transaction } ); } total += ids.length; } }); const warm = await ordinary.request(endpoint); assert.equal(warm.status, 200); let queries = 0; database.addHook("afterQuery", "r26-count", () => { queries++; }); try { const started = performance.now(); const response = await ordinary.request(endpoint); const elapsed = performance.now() - started; assert.equal(response.status, 200); const data = ApplicationCoincidencesResponseSchema.parse(await response.json()); assert.ok(data.scanned <= 200); measurements.push({ globals: size, elapsedMs: elapsed, queries, scanned: data.scanned, complete: data.complete, }); } finally { database.removeHook("afterQuery", "r26-count"); } } }); const matching: { id: string; values: { active: boolean } }[] = []; for (let index = 0; index < 24; index++) { matching.push(await product("Ágil particular", index % 4 !== 0)); } await product("Ágil particular Pro"); await product("Agil particular"); const provider = catalog.items.find((item) => item.builtinKey === "field:provider"); assert.ok(provider); const ownValues = InventoryResponseSchema.parse( await (await ordinary.request(`/resources/${company}`)).json() ).resources.find((item) => item.id === own)?.values; assert.ok(ownValues); assert.equal( ( await ordinary.request(`/resources/${company}/${own}/update`, { values: { ...ownValues, characteristics: { [provider.id]: "Fábrica" } }, }) ).status, 200 ); const addProvider = async (value: string) => { const response = await admin.request("/resource-global-applications", { creationId: UniqueID.generateNewID().toPrimitive(), values: { ...global.values, name: "Ágil particular", characteristics: { [provider.id]: value }, }, }); assert.equal(response.status, 201); return GetGlobalApplicationAdminResponseSchema.parse(await response.json()).resource.id; }; const strongId = await addProvider("Fábrica"); await addProvider("Otro proveedor"); const before = await snapshot(); await t.test( "paginación determinista, inactivos y proyección mínima sin escribir", async () => { let cursor: string | null = null; const ids: string[] = []; let pages = 0; do { const response = await ordinary.request( `${endpoint}?pageSize=7${cursor ? `&cursor=${encodeURIComponent(cursor)}` : ""}` ); assert.equal(response.status, 200); const data = ApplicationCoincidencesResponseSchema.parse(await response.json()); assert.equal(data.source.id, own); assert.equal(data.source.companyId, company); assert.ok(data.scanned <= 200); for (const item of data.items) { assert.equal( item.classification, item.candidate.id === strongId ? "name-provider" : "name-only" ); assert.ok(item.reasons.includes("same-normalized-name")); assert.equal(Object.hasOwn(item.candidate, "notes"), false); assert.equal(Object.hasOwn(item.candidate, "history"), false); ids.push(item.candidate.id); } cursor = data.nextCursor; pages++; assert.ok(pages < 80); } while (cursor); assert.equal(new Set(ids).size, 25); assert.deepEqual(ids, [...ids].sort()); assert.equal( ids.filter((id) => matching.some((item) => item.id === id && !item.values.active)).length, 6 ); assert.deepEqual(await snapshot(), before); } ); await t.test("contexto, elegibilidad, validación, autenticación y cursores", async () => { for (const resourceId of [ foreign, os, license, certificate, global.id, UniqueID.generateNewID().toPrimitive(), ]) { assert.equal( (await admin.request(`/resource-application-coincidences/${company}/${resourceId}`)) .status, 404 ); } assert.equal((await ordinary.request(`${endpoint}?pageSize=0`)).status, 400); assert.equal((await ordinary.request(`${endpoint}?cursor=invalid`)).status, 400); assert.equal((await ordinary.request(`${endpoint}?scope=global`)).status, 400); const anonymous = await scopeApiClient(base); assert.equal((await anonymous.request(endpoint)).status, 401); const initial = ApplicationCoincidencesResponseSchema.parse( await (await ordinary.request(endpoint)).json() ); assert.ok(initial.nextCursor); assert.equal( ( await ordinary.request( `${endpoint}?search=otro&cursor=${encodeURIComponent(initial.nextCursor)}` ) ).status, 400 ); assert.equal( ( await ordinary.request( `/resource-application-coincidences/${other}/${foreign}?cursor=${encodeURIComponent(initial.nextCursor)}` ) ).status, 400 ); const response = await ordinary.request(`${endpoint}?search=Pro`); assert.equal(response.status, 200); assert.equal( ApplicationCoincidencesResponseSchema.parse(await response.json()).items.length, 0 ); assert.deepEqual(await snapshot(), before); }); await t.test( "uso local conserva producto, licencia, versión, configuración e historial", async () => { const read = ApplicationConfigurationsResponseSchema.parse( await (await ordinary.request(path)).json() ).configurations.find((item) => item.id === first.id); assert.ok(read); assert.equal(read.values.resourceId, own); assert.equal(read.history.length, 1); const software = await ( await ordinary.request(`/software-installations/${company}`) ).json(); assert.match(JSON.stringify(software), /LOCAL-123/); assert.match(JSON.stringify(software), new RegExp(own)); const page = ListApplicationCandidatesResponseSchema.parse( await ( await ordinary.request( `/resource-application-candidates/${company}?purpose=application-configuration&search=particular` ) ).json() ); assert.ok(page.items.some((item) => item.id === own)); assert.ok(page.items.every((item) => item.scope === "company")); assert.deepEqual(await snapshot(), before); } ); await t.test( "catálogo cerrado no permite detección ni altera referencias existentes", async () => { await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())) ); runtime.stop(); runtime = await createIdentityRuntime( database, readDatabaseEnvironment({ ...process.env, RESOURCE_GLOBAL_CATALOG_ENABLED: "false", RESOURCE_GLOBAL_SOFTWARE_ENABLED: "false", RESOURCE_GLOBAL_PROFILE_ENABLED: "false", }) ); server = createApp(() => undefined, runtime).listen(address.port, "127.0.0.1"); await once(server, "listening"); assert.equal((await ordinary.request(endpoint)).status, 409); assert.deepEqual(await snapshot(), before); } ); await writeScopeEvidence("coincidence-results.json", { companies: 15, globalVolume: 10028, measurements, mutationFree: true, sourceLocalResource: own, }); } finally { server.close(); await once(server, "close"); runtime.stop(); await database.close(); } });