metis-desk/apps/server/src/application-coincidences.integration.test.ts
2026-10-25 01:19:17 +02:00

571 lines
20 KiB
TypeScript

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<string, string> = {};
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<Record<string, unknown>>(
"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<Record<string, unknown>>(
"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<string, Record<string, unknown>[]>();
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<Record<string, unknown>>(
`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<string, unknown>, 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<void>((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();
}
});