- 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.
873 lines
30 KiB
TypeScript
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();
|
|
}
|
|
});
|