metis-desk/apps/server/src/global-applications.integration.test.ts
David 91f1d07e97 Add tests for current operating systems and workplace asset management
- Implement `current-operating-systems-ui.test.py` to validate the functionality of current operating systems in the UI, including tests for source and states, computer changes, responsive design, error handling, and version saving.
- Create `workplace-add-asset.test.py` to test the addition of assets to workstations, covering scenarios such as conflict resolution, responsive design on mobile, error handling during asset creation, and HTTP guards for asset management.
2026-10-25 13:46:06 +01:00

873 lines
30 KiB
TypeScript

import assert from "node:assert/strict";
import { randomBytes } from "node:crypto";
import { once } from "node:events";
import { test } from "node:test";
import {
GetGlobalApplicationAdminResponseSchema,
GetGlobalApplicationResponseSchema,
GlobalApplicationCapabilitiesSchema,
GlobalApplicationTemplateResponseSchema,
InventoryResponseSchema,
ListCompanyApplicationsResponseSchema,
ListGlobalApplicationHistoryResponseSchema,
ListGlobalApplicationsResponseSchema,
ResolveApplicationReferencesResponseSchema,
ResourceResponseSchema,
} from "@metis/resources/common";
import { createResourceModule, migrateResourceScope } from "@metis/resources/module";
import { QueryTypes } from "sequelize";
import { z } from "zod/v4";
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 as apiClient } from "./resource-scope-api.client";
import { scopeHttpFixture, seedScopeVolume } from "./resource-scope-http.fixture";
test("R2.3: catálogo global autorizado, historia, idempotencia, privacidad y consumidores cerrados", async (t) => {
const environment = disposableScopeEnvironment();
const database = createDatabase(environment);
const companies = Array.from({ length: 15 }, () => UniqueID.generateNewID().toPrimitive());
const companyId = companies[0];
const other = companies[1];
const actor = UniqueID.generateNewID().toPrimitive();
assert.ok(companyId && other);
const fixture = await prepareMembershipFixture(
database,
environment.APP_ORIGIN,
actor,
companies
);
const closedOptions = { catalogEnabled: true, canManage: async () => false };
await assert.rejects(
() =>
createResourceModule(
database,
fixture.authenticate,
undefined,
undefined,
fixture.identity.requireGlobalResourceManager,
closedOptions
),
/R2.1b/
);
await migrateResourceScope(database);
const volumeHttp = await scopeHttpFixture(database, fixture);
const volume = await seedScopeVolume(fixture, volumeHttp, companyId);
const inventory = await fixture.resourceUseCases.get(companyId);
assert.ok(inventory.isSuccess);
const application = inventory.data.catalog.definitions.find(
(item) => item.stockKey.getOrUndefined() === "template:application"
);
const os = inventory.data.catalog.definitions.find(
(item) => item.stockKey.getOrUndefined() === "template:os"
);
const provider = inventory.data.catalog.definitions.find(
(item) => item.stockKey.getOrUndefined() === "field:provider"
);
assert.ok(application && os && provider);
const locals: string[] = [];
for (const owner of companies) {
for (let index = 0; index < (owner === companyId ? 50 : 1); index += 1) {
const saved = await fixture.resourceUseCases.save(
owner,
undefined,
{
name: `Aplicación local ${index + 1}`,
templateId: application.id.toPrimitive(),
active: true,
notes: "Local",
characteristics: {},
},
actor
);
assert.ok(saved.isSuccess);
if (owner === companyId) {
locals.push(saved.data.resource.id.toPrimitive());
}
}
}
await volumeHttp.close();
let runtime = await createIdentityRuntime(
database,
readDatabaseEnvironment({ ...process.env, RESOURCE_GLOBAL_CATALOG_ENABLED: "true" })
);
const diagnostics: unknown[] = [];
const app = createApp((event) => {
if (event.event === "http_error") {
diagnostics.push(event);
}
}, runtime);
let server = app.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}`;
try {
const admin = await apiClient(base);
const ordinary = await apiClient(base);
const anonymous = await apiClient(base);
const password = randomBytes(24).toString("hex");
assert.equal(
(
await admin.request("/auth/bootstrap", {
name: "Administrador de catálogo",
email: "catalog-admin@example.invalid",
password,
})
).status,
201
);
await admin.refresh();
assert.equal(
(
await admin.request("/users", {
name: "Operador local",
email: "catalog-user@example.invalid",
password,
})
).status,
201
);
assert.equal(
(await ordinary.request("/auth/login", { email: "catalog-user@example.invalid", password }))
.status,
200
);
await ordinary.refresh();
const template = GlobalApplicationTemplateResponseSchema.parse(
await (await admin.request("/resource-global-applications/template")).json()
).specification;
const values = {
name: "Aplicación global Ágil %_",
templateId: template.templateId,
active: true,
notes: "Nota administrativa privada",
characteristics: { [provider.id.toPrimitive()]: "Fábrica sintética" },
};
const creationId = UniqueID.generateNewID().toPrimitive();
const body = { creationId, values };
assert.equal(
ListGlobalApplicationsResponseSchema.parse(
await (await ordinary.request("/resource-global-applications")).json()
).items.length,
0
);
const created = await admin.request("/resource-global-applications", body);
assert.equal(created.status, 201, await created.clone().text());
let product = GetGlobalApplicationAdminResponseSchema.parse(await created.json()).resource;
const originalRevisionId = product.revisionId;
await t.test(
"catálogo de compañía combina ámbitos, pagina y permite altas locales aisladas",
async () => {
const path = `/resource-application-candidates/${companyId}/catalog`;
const firstResponse = await ordinary.request(`${path}?pageSize=20`);
assert.equal(firstResponse.status, 200);
const first = ListCompanyApplicationsResponseSchema.parse(await firstResponse.json());
const ids = first.items.map((item) => item.id);
let cursor = first.nextCursor;
while (cursor) {
const response = await ordinary.request(
`${path}?pageSize=20&cursor=${encodeURIComponent(cursor)}`
);
assert.equal(response.status, 200);
const page = ListCompanyApplicationsResponseSchema.parse(await response.json());
ids.push(...page.items.map((item) => item.id));
cursor = page.nextCursor;
}
assert.equal(ids.length, 51);
assert.equal(new Set(ids).size, 51);
assert.ok(ids.includes(creationId));
assert.ok(locals.every((id) => ids.includes(id)));
const foreign = ListCompanyApplicationsResponseSchema.parse(
await (
await ordinary.request(`/resource-application-candidates/${other}/catalog?pageSize=100`)
).json()
);
assert.equal(foreign.items.length, 2);
assert.ok(foreign.items.some((item) => item.id === creationId && item.scope === "global"));
assert.ok(
foreign.items.every((item) => item.scope === "global" || item.companyId === other)
);
assert.equal((await anonymous.request(path)).status, 401);
assert.equal(
(
await ordinary.request(
`/resource-application-candidates/${UniqueID.generateNewID().toPrimitive()}/catalog`
)
).status,
404
);
assert.equal((await ordinary.request(`${path}?unknown=value`)).status, 400);
assert.equal(
(
await ordinary.request(
`/resource-application-candidates/${other}/catalog?pageSize=20&cursor=${encodeURIComponent(first.nextCursor ?? "invalid")}`
)
).status,
400
);
for (const item of first.items) {
assert.deepEqual(
Object.keys(item).sort(),
["id", "templateId", "name", "active", "kind", "scope", "companyId", "provider"].sort()
);
}
const createdLocal = await ordinary.request(`/resources/${companyId}`, {
values: {
name: "Aplicación exclusiva Ñ",
templateId: template.templateId,
active: true,
notes: "Privada",
characteristics: {},
},
});
assert.equal(createdLocal.status, 201);
const local = ResourceResponseSchema.parse(await createdLocal.json()).resource;
const found = ListCompanyApplicationsResponseSchema.parse(
await (await ordinary.request(`${path}?search=exclusiva`)).json()
);
assert.equal(found.items.length, 1);
assert.equal(found.items[0]?.scope, "company");
assert.equal(found.items[0]?.companyId, companyId);
const absent = ListCompanyApplicationsResponseSchema.parse(
await (
await ordinary.request(
`/resource-application-candidates/${other}/catalog?search=exclusiva`
)
).json()
);
assert.equal(absent.items.length, 0);
assert.equal(
(await ordinary.request(`/resources/${companyId}/${local.id}/status`, { active: false }))
.status,
200
);
const inactive = ListCompanyApplicationsResponseSchema.parse(
await (await ordinary.request(`${path}?active=false`)).json()
);
assert.deepEqual(
inactive.items.map((item) => item.id),
[local.id]
);
const active = ListCompanyApplicationsResponseSchema.parse(
await (await ordinary.request(`${path}?active=true&search=exclusiva`)).json()
);
assert.equal(active.items.length, 0);
assert.equal(
(await ordinary.request(`/resources/${companyId}/${local.id}/delete`, {})).status,
200
);
}
);
await t.test(
"scope y plantilla persistidos, autorización real y proyección mínima",
async () => {
assert.equal(product.id, creationId);
assert.equal(product.scope, "global");
assert.equal(product.companyId, null);
const persisted = await database.query<{
scope: string;
companyId: null;
templateId: string;
}>("SELECT scope,companyId,templateId FROM resources WHERE id=:id", {
replacements: { id: creationId },
type: QueryTypes.SELECT,
});
assert.deepEqual(persisted, [
{ scope: "global", companyId: null, templateId: template.templateId },
]);
assert.equal((await anonymous.request("/resource-global-applications", body)).status, 401);
assert.equal((await ordinary.request("/resource-global-applications", body)).status, 403);
assert.equal(
(await ordinary.request(`/resource-global-applications/${creationId}/update`, { values }))
.status,
403
);
assert.equal(
(
await ordinary.request(`/resource-global-applications/${creationId}/status`, {
active: false,
})
).status,
403
);
assert.equal(
(await ordinary.request(`/resource-global-applications/${creationId}/admin`)).status,
403
);
assert.equal(
(await ordinary.request(`/resource-global-applications/${creationId}/history`)).status,
403
);
const minimal = GetGlobalApplicationResponseSchema.parse(
await (await ordinary.request(`/resource-global-applications/${creationId}`)).json()
).resource;
assert.equal(minimal.provider, "Fábrica sintética");
for (const key of [
"notes",
"specification",
"history",
"actorId",
"companies",
"installations",
]) {
assert.equal(Object.hasOwn(minimal, key), false);
}
assert.equal(
GlobalApplicationCapabilitiesSchema.parse(
await (await ordinary.request("/resource-global-applications/capabilities")).json()
).canManage,
false
);
assert.equal(
GlobalApplicationCapabilitiesSchema.parse(
await (await admin.request("/resource-global-applications/capabilities")).json()
).creationEnabled,
true
);
assert.equal(
(
await admin.request(
`/resource-global-applications/${creationId}/status`,
{ active: false },
{ token: "" }
)
).status,
403
);
assert.equal(
(
await admin.request(
`/resource-global-applications/${creationId}/status`,
{ active: false },
{ origin: "https://invalid.example" }
)
).status,
403
);
}
);
await t.test("idempotencia de creationId y concurrencia sin duplicar historia", async () => {
const replay = await admin.request("/resource-global-applications", body);
assert.equal(replay.status, 200);
assert.equal(
GetGlobalApplicationAdminResponseSchema.parse(await replay.json()).resource.revisionId,
originalRevisionId
);
assert.equal(
(
await admin.request("/resource-global-applications", {
...body,
values: { ...values, name: "Otro producto" },
})
).status,
409
);
assert.equal(
(await admin.request("/resource-global-applications", { ...body, creationId: locals[0] }))
.status,
409
);
const concurrentId = UniqueID.generateNewID().toPrimitive();
const responses = await Promise.all([
admin.request("/resource-global-applications", { ...body, creationId: concurrentId }),
admin.request("/resource-global-applications", { ...body, creationId: concurrentId }),
]);
assert.deepEqual(responses.map((response) => response.status).sort(), [200, 201]);
const history = ListGlobalApplicationHistoryResponseSchema.parse(
await (await admin.request(`/resource-global-applications/${concurrentId}/history`)).json()
);
assert.equal(history.items.length, 1);
});
await t.test("límites de plantilla, propietario y UUID; sin borrado global", async () => {
for (const extra of [
{ scope: "global" },
{ companyId },
{ actorId: actor },
{ isInitialAdmin: true },
]) {
assert.equal(
(await admin.request("/resource-global-applications", { ...body, ...extra })).status,
400
);
}
assert.equal(
(
await admin.request("/resource-global-applications", {
...body,
creationId: UniqueID.generateNewID().toPrimitive(),
values: { ...values, templateId: os.id.toPrimitive() },
})
).status,
409
);
assert.equal(
(
await admin.request(`/resource-global-applications/${creationId}/update`, {
values: { ...values, templateId: os.id.toPrimitive() },
})
).status,
409
);
assert.equal(
(await admin.request(`/resource-global-applications/${locals[0]}/update`, { values }))
.status,
404
);
assert.equal(
(await admin.request(`/resource-global-applications/${creationId}/delete`, {})).status,
404
);
assert.equal(
(
await ordinary.request(`/resource-catalog/%67lobal/${template.templateId}/update`, {
values: application.values.data,
})
).status,
403
);
});
await t.test(
"edición y optimistic concurrency, snapshot original e idempotencia semántica",
async () => {
const responses = await Promise.all([
admin.request(`/resource-global-applications/${creationId}/update`, {
values: { ...values, name: "Nombre actualizado A" },
expectedRevisionId: originalRevisionId,
}),
admin.request(`/resource-global-applications/${creationId}/update`, {
values: { ...values, name: "Nombre actualizado B" },
expectedRevisionId: originalRevisionId,
}),
]);
assert.deepEqual(responses.map((response) => response.status).sort(), [200, 409]);
product = GetGlobalApplicationAdminResponseSchema.parse(
await (await admin.request(`/resource-global-applications/${creationId}/admin`)).json()
).resource;
const unchanged = await admin.request(
`/resource-global-applications/${creationId}/update`,
{ values: product.values, expectedRevisionId: product.revisionId }
);
assert.equal(
GetGlobalApplicationAdminResponseSchema.parse(await unchanged.json()).resource.revisionId,
product.revisionId
);
const replay = await admin.request("/resource-global-applications", body);
assert.equal(replay.status, 200);
assert.equal(
GetGlobalApplicationAdminResponseSchema.parse(await replay.json()).resource.values.name,
product.values.name
);
const page = ListGlobalApplicationHistoryResponseSchema.parse(
await (
await admin.request(`/resource-global-applications/${creationId}/history?pageSize=1`)
).json()
);
assert.equal(page.items.length, 1);
assert.ok(page.nextCursor);
const next = ListGlobalApplicationHistoryResponseSchema.parse(
await (
await admin.request(
`/resource-global-applications/${creationId}/history?pageSize=1&cursor=${encodeURIComponent(page.nextCursor)}`
)
).json()
);
assert.equal(next.items[0]?.id, originalRevisionId);
assert.equal(next.items[0]?.values.name, values.name);
}
);
await t.test("desactivar/reactivar conserva UUID e historia y excluye candidatos", async () => {
const disabled = await admin.request(`/resource-global-applications/${creationId}/status`, {
active: false,
expectedRevisionId: product.revisionId,
});
product = GetGlobalApplicationAdminResponseSchema.parse(await disabled.json()).resource;
assert.equal(product.id, creationId);
assert.equal(product.values.active, false);
const repeated = await admin.request(`/resource-global-applications/${creationId}/status`, {
active: false,
});
assert.equal(
GetGlobalApplicationAdminResponseSchema.parse(await repeated.json()).resource.revisionId,
product.revisionId
);
const resolve = await ordinary.request(
`/resource-application-references/${companyId}/resolve`,
{ purpose: "software-installation", ids: [creationId] }
);
assert.equal(
ResolveApplicationReferencesResponseSchema.parse(await resolve.json()).items[0]?.status,
"resolved"
);
const reactivated = await admin.request(
`/resource-global-applications/${creationId}/status`,
{ active: true }
);
product = GetGlobalApplicationAdminResponseSchema.parse(await reactivated.json()).resource;
assert.equal(product.values.active, true);
});
await t.test(
"colección paginada, homónimos y búsqueda literal sin N+1 de cliente",
async () => {
for (let index = 0; index < 28; index += 1) {
assert.equal(
(
await admin.request("/resource-global-applications", {
creationId: UniqueID.generateNewID().toPrimitive(),
values: {
...values,
name: `Producto Ágil %_ ${index % 7}`,
active: index % 3 !== 0,
},
})
).status,
201
);
}
const seen = new Set<string>();
let cursor: string | null = null;
do {
const response = await ordinary.request(
`/resource-global-applications?search=${encodeURIComponent("ágil %_")}&pageSize=7${cursor ? `&cursor=${encodeURIComponent(cursor)}` : ""}`
);
const page = ListGlobalApplicationsResponseSchema.parse(await response.json());
for (const item of page.items) {
assert.equal(seen.has(item.id), false);
seen.add(item.id);
}
cursor = page.nextCursor;
} while (cursor);
assert.equal(seen.size, 29);
assert.ok(
ListGlobalApplicationsResponseSchema.parse(
await (await ordinary.request("/resource-global-applications?active=false")).json()
).items.length > 0
);
}
);
await t.test(
"rollback por error de infraestructura y recuperación por el mismo creationId",
async () => {
let inject = true;
const failedId = UniqueID.generateNewID().toPrimitive();
database.addHook("afterQuery", "r23-infrastructure-failure", (_options, query) => {
const sql: unknown = Reflect.get(query, "sql");
if (inject && typeof sql === "string" && sql.startsWith("INSERT INTO `resources`")) {
inject = false;
throw new Error("Fallo sintético después de insertar raíz.");
}
});
try {
const response = await admin.request("/resource-global-applications", {
creationId: failedId,
values,
});
assert.equal(response.status, 500);
const data = await response.text();
assert.match(data, /INTERNAL_ERROR/);
assert.equal(data.includes("INSERT"), false);
} finally {
database.removeHook("afterQuery", "r23-infrastructure-failure");
}
assert.equal(
(await admin.request(`/resource-global-applications/${failedId}/admin`)).status,
404
);
assert.equal(
(await admin.request("/resource-global-applications", { creationId: failedId, values }))
.status,
201
);
}
);
await t.test(
"propagación autorizada global/local y campos nuevos sin reescribir snapshots",
async () => {
const response = await admin.request("/resource-catalog/global", {
values: {
kind: "field",
name: "Código adicional de producto",
description: "Campo sintético",
type: "text",
unit: "",
choices: [],
},
});
const parsed = await response.json();
const fieldId = z.object({ savedId: z.uuid() }).parse(parsed).savedId;
assert.equal(response.status, 201);
assert.equal(application.values.data.kind, "template");
if (application.values.data.kind !== "template") {
throw new Error("Plantilla inesperada.");
}
const changed = await admin.request(
`/resource-catalog/global/${template.templateId}/update`,
{
values: {
...application.values.data,
fields: [...application.values.data.fields, { fieldId, required: true }],
},
}
);
assert.equal(changed.status, 200, await changed.clone().text());
product = GetGlobalApplicationAdminResponseSchema.parse(
await (await admin.request(`/resource-global-applications/${creationId}/admin`)).json()
).resource;
assert.ok(product.specification.fields.some((field) => field.fieldId === fieldId));
assert.equal(
(
await admin.request(`/resource-global-applications/${creationId}/update`, {
values: product.values,
})
).status,
409
);
assert.equal(
(
await admin.request(`/resource-global-applications/${creationId}/status`, {
active: false,
})
).status,
200
);
assert.equal(
(
await admin.request(`/resource-global-applications/${creationId}/status`, {
active: true,
})
).status,
409
);
const historical = ListGlobalApplicationHistoryResponseSchema.parse(
await (
await admin.request(`/resource-global-applications/${creationId}/history?pageSize=100`)
).json()
);
assert.ok(
historical.items.some(
(item) =>
item.id === originalRevisionId &&
!item.specification?.fields.some((field) => field.fieldId === fieldId)
)
);
}
);
await t.test("CRUD local y consumidores continúan locales tras existir catálogo", async () => {
const inventory = InventoryResponseSchema.parse(
await (await ordinary.request(`/resources/${companyId}`)).json()
);
assert.equal(inventory.resources.length, 50);
assert.ok(
inventory.resources.every(
(item) => item.companyId === companyId && !Object.hasOwn(item, "scope")
)
);
assert.equal(
(
await admin.request(`/software-installations/${companyId}`, {
values: {
assetId: volume.assetIds[0],
resourceId: creationId,
licenseResourceId: null,
version: null,
state: "configured",
notes: null,
},
})
).status,
409
);
assert.equal(
(
await admin.request(`/application-configurations/${companyId}`, {
values: {
profileId: volume.profileIds[0],
resourceId: creationId,
delivery: "installed",
version: null,
state: "configured",
notes: null,
},
})
).status,
409
);
});
// La plantilla compartida vuelve a su contrato inicial antes de otras suites del contenedor.
assert.equal(
(
await admin.request(`/resource-catalog/global/${template.templateId}/update`, {
values: application.values.data,
})
).status,
200
);
await t.test(
"reinicio HTTP: replay persistido y cierre de altas/edición/reactivación con lectura conservada",
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" })
);
server = createApp(() => undefined, runtime).listen(0, "127.0.0.1");
await once(server, "listening");
const restartedAddress = server.address();
assert.ok(restartedAddress && typeof restartedAddress !== "string");
const restarted = await apiClient(`http://127.0.0.1:${restartedAddress.port}`);
assert.equal(
(
await restarted.request("/auth/login", {
email: "catalog-admin@example.invalid",
password,
})
).status,
200
);
await restarted.refresh();
assert.equal((await restarted.request("/resource-global-applications", body)).status, 200);
assert.equal(
(
await restarted.request("/resource-global-applications", {
...body,
creationId: UniqueID.generateNewID().toPrimitive(),
})
).status,
409
);
assert.equal(
(await restarted.request(`/resource-global-applications/${creationId}/admin`)).status,
200
);
assert.equal(
(
await restarted.request(`/resource-global-applications/${creationId}/update`, {
values,
})
).status,
409
);
assert.equal(
(
await restarted.request(`/resource-global-applications/${creationId}/status`, {
active: false,
})
).status,
200
);
assert.equal(
(
await restarted.request(`/resource-global-applications/${creationId}/status`, {
active: true,
})
).status,
409
);
const history = ListGlobalApplicationHistoryResponseSchema.parse(
await (
await restarted.request(
`/resource-global-applications/${creationId}/history?pageSize=1`
)
).json()
);
assert.equal(history.items.length, 1);
const lastCursor = Buffer.from(
JSON.stringify({ version: 1, resourceId: creationId, position: 0 })
).toString("base64url");
const empty = await restarted.request(
`/resource-global-applications/${creationId}/history?cursor=${lastCursor}`
);
assert.equal(empty.status, 200);
assert.equal(
ListGlobalApplicationHistoryResponseSchema.parse(await empty.json()).items.length,
0
);
}
);
await writeScopeEvidence("catalog-results.json", {
companies: 15,
principal: { resources: 50, people: 50, assets: 50, profiles: 50, workstations: 50 },
publicGlobalWrites: "authorized initial administrator",
globalConsumers: false,
originalId: creationId,
originalRevisionId,
});
} finally {
await writeScopeEvidence("http-error-diagnostics.json", diagnostics);
await new Promise<void>((resolve, reject) =>
server.close((error) => (error ? reject(error) : resolve()))
);
runtime.stop();
await database.close();
}
});