1192 lines
42 KiB
TypeScript
1192 lines
42 KiB
TypeScript
|
|
import assert from "node:assert/strict";
|
||
|
|
import { randomBytes } from "node:crypto";
|
||
|
|
import { once } from "node:events";
|
||
|
|
import { test } from "node:test";
|
||
|
|
|
||
|
|
import {
|
||
|
|
SoftwareInstallationResponseSchema,
|
||
|
|
SoftwareInstallationsResponseSchema,
|
||
|
|
} from "@metis/companies/common";
|
||
|
|
import type { OperatingSystemIdentity } from "@metis/resources/common";
|
||
|
|
import {
|
||
|
|
ApplicationReferenceCapabilitiesSchema,
|
||
|
|
CatalogResponseSchema,
|
||
|
|
GetGlobalApplicationAdminResponseSchema,
|
||
|
|
GetGlobalOperatingSystemResponseSchema,
|
||
|
|
GlobalApplicationTemplateResponseSchema,
|
||
|
|
InventoryResponseSchema,
|
||
|
|
ListApplicationCandidatesResponseSchema,
|
||
|
|
ListGlobalApplicationHistoryResponseSchema,
|
||
|
|
ListGlobalApplicationsResponseSchema,
|
||
|
|
ListGlobalOperatingSystemsResponseSchema,
|
||
|
|
ResolveApplicationReferencesResponseSchema,
|
||
|
|
ResourceResponseSchema,
|
||
|
|
SaveDefinitionResponseSchema,
|
||
|
|
} from "@metis/resources/common";
|
||
|
|
import { migrateResourceScope } from "@metis/resources/module";
|
||
|
|
import { QueryTypes } from "sequelize";
|
||
|
|
import { z } from "zod/v4";
|
||
|
|
|
||
|
|
import { inspectResourceScopeSchema } from "../../../modules/resources/src/api/infrastructure/persistence/resource-scope-schema";
|
||
|
|
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 {
|
||
|
|
resourceSqlSnapshot,
|
||
|
|
verifyLegacyOperatingSystemEvolution,
|
||
|
|
} from "./global-operating-systems-legacy.fixture";
|
||
|
|
import { createIdentityRuntime } from "./infrastructure/http/identity-runtime";
|
||
|
|
import {
|
||
|
|
disposableScopeEnvironment,
|
||
|
|
scopeSchemaEvidence,
|
||
|
|
writeScopeEvidence,
|
||
|
|
} from "./resource-scope.preflight";
|
||
|
|
import { scopeApiClient } from "./resource-scope-api.client";
|
||
|
|
import { latch, waitBarrier, waitCatalogLock } from "./resource-scope-concurrency.fixture";
|
||
|
|
import { scopeHttpFixture, seedScopeVolume } from "./resource-scope-http.fixture";
|
||
|
|
|
||
|
|
const uuid = () => UniqueID.generateNewID().toPrimitive();
|
||
|
|
|
||
|
|
const v3 = { contract: "3" };
|
||
|
|
const ErrorSchema = z.object({ code: z.string() });
|
||
|
|
|
||
|
|
test("Cierre SO: matriz DB-SO, evolución, integridad y concurrencia real", async (t) => {
|
||
|
|
const environment = disposableScopeEnvironment();
|
||
|
|
const database = createDatabase(environment);
|
||
|
|
const companies = Array.from({ length: 15 }, uuid);
|
||
|
|
const company = companies[0];
|
||
|
|
const other = companies[1];
|
||
|
|
const actor = uuid();
|
||
|
|
|
||
|
|
assert.ok(company && other);
|
||
|
|
const fixture = await prepareMembershipFixture(
|
||
|
|
database,
|
||
|
|
environment.APP_ORIGIN,
|
||
|
|
actor,
|
||
|
|
companies
|
||
|
|
);
|
||
|
|
const migration = await migrateResourceScope(database);
|
||
|
|
const http = await scopeHttpFixture(database, fixture);
|
||
|
|
const volume = await seedScopeVolume(fixture, http, company);
|
||
|
|
|
||
|
|
await http.close();
|
||
|
|
const assetA = volume.assetIds[0];
|
||
|
|
const assetA2 = volume.assetIds[1];
|
||
|
|
const raceAsset = volume.assetIds[2];
|
||
|
|
const assetB = await fixture.asset(other, "laptop");
|
||
|
|
|
||
|
|
assert.ok(assetA && assetA2 && raceAsset);
|
||
|
|
const legacy = await verifyLegacyOperatingSystemEvolution(
|
||
|
|
database,
|
||
|
|
fixture.resourceUseCases,
|
||
|
|
company,
|
||
|
|
actor
|
||
|
|
);
|
||
|
|
|
||
|
|
await writeScopeEvidence("legacy-evolution.json", legacy);
|
||
|
|
const schema = await scopeSchemaEvidence(database);
|
||
|
|
const settings = z
|
||
|
|
.array(z.object({ version: z.string(), checkConstraints: z.number(), foreignKeys: z.number() }))
|
||
|
|
.parse(schema.version);
|
||
|
|
|
||
|
|
const currentSettings = settings[0];
|
||
|
|
|
||
|
|
assert.ok(currentSettings);
|
||
|
|
assert.ok(currentSettings.version.includes("MariaDB"));
|
||
|
|
assert.equal(currentSettings.checkConstraints, 1);
|
||
|
|
assert.equal(currentSettings.foreignKeys, 1);
|
||
|
|
assert.equal((await inspectResourceScopeSchema(database)).mode, "scoped");
|
||
|
|
assert.equal((await migrateResourceScope(database)).completed.length, 0);
|
||
|
|
await writeScopeEvidence("schema.json", {
|
||
|
|
...schema,
|
||
|
|
migration,
|
||
|
|
repeatedMigration: "no changes",
|
||
|
|
connection: { database: environment.DB_NAME, host: environment.DB_HOST, disposable: true },
|
||
|
|
engines: await database.query(
|
||
|
|
"SELECT TABLE_NAME,ENGINE FROM information_schema.TABLES WHERE TABLE_SCHEMA=DATABASE() ORDER BY TABLE_NAME",
|
||
|
|
{ type: QueryTypes.SELECT }
|
||
|
|
),
|
||
|
|
});
|
||
|
|
|
||
|
|
const openEnvironment = readDatabaseEnvironment({
|
||
|
|
...process.env,
|
||
|
|
RESOURCE_GLOBAL_CATALOG_ENABLED: "true",
|
||
|
|
RESOURCE_GLOBAL_SOFTWARE_ENABLED: "true",
|
||
|
|
RESOURCE_GLOBAL_SOFTWARE_CANDIDATES_ENABLED: "true",
|
||
|
|
RESOURCE_GLOBAL_PROFILE_ENABLED: "true",
|
||
|
|
RESOURCE_GLOBAL_OS_CATALOG_ENABLED: "true",
|
||
|
|
RESOURCE_GLOBAL_OS_SOFTWARE_ENABLED: "true",
|
||
|
|
});
|
||
|
|
let runtime = await createIdentityRuntime(database, openEnvironment);
|
||
|
|
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");
|
||
|
|
const root = "/resource-global-operating-systems";
|
||
|
|
const productId = uuid();
|
||
|
|
const applicationId = uuid();
|
||
|
|
const results: { id: string; result: string; http?: number[] }[] = [];
|
||
|
|
|
||
|
|
try {
|
||
|
|
assert.equal(
|
||
|
|
(
|
||
|
|
await admin.request("/auth/bootstrap", {
|
||
|
|
name: "Cierre SO",
|
||
|
|
email: "closure-admin@example.invalid",
|
||
|
|
password,
|
||
|
|
})
|
||
|
|
).status,
|
||
|
|
201
|
||
|
|
);
|
||
|
|
await admin.refresh();
|
||
|
|
assert.equal(
|
||
|
|
(
|
||
|
|
await admin.request("/users", {
|
||
|
|
name: "Operador de cierre",
|
||
|
|
email: "closure-user@example.invalid",
|
||
|
|
password,
|
||
|
|
})
|
||
|
|
).status,
|
||
|
|
201
|
||
|
|
);
|
||
|
|
assert.equal(
|
||
|
|
(await ordinary.request("/auth/login", { email: "closure-user@example.invalid", password }))
|
||
|
|
.status,
|
||
|
|
200
|
||
|
|
);
|
||
|
|
await ordinary.refresh();
|
||
|
|
const catalog = CatalogResponseSchema.parse(
|
||
|
|
await (await admin.request("/resource-catalog/global")).json()
|
||
|
|
);
|
||
|
|
|
||
|
|
const template = (key: string) => {
|
||
|
|
const item = catalog.items.find((item) => item.builtinKey === `template:${key}`);
|
||
|
|
|
||
|
|
assert.ok(item);
|
||
|
|
|
||
|
|
return item;
|
||
|
|
};
|
||
|
|
|
||
|
|
const field = (key: string) => {
|
||
|
|
const item = catalog.items.find((item) => item.builtinKey === `field:${key}`);
|
||
|
|
|
||
|
|
assert.ok(item);
|
||
|
|
|
||
|
|
return item.id;
|
||
|
|
};
|
||
|
|
|
||
|
|
const specification = GlobalApplicationTemplateResponseSchema.parse(
|
||
|
|
await (await admin.request(`${root}/template`)).json()
|
||
|
|
).specification;
|
||
|
|
const identity: OperatingSystemIdentity = {
|
||
|
|
provider: "Microsoft",
|
||
|
|
family: "Windows",
|
||
|
|
productVersion: "11",
|
||
|
|
editionState: "known",
|
||
|
|
edition: "Pro",
|
||
|
|
};
|
||
|
|
|
||
|
|
const values = (item: OperatingSystemIdentity) => ({
|
||
|
|
name: `${item.family} ${item.productVersion} ${item.edition}`.trim(),
|
||
|
|
templateId: specification.templateId,
|
||
|
|
active: true,
|
||
|
|
notes: "Datos sintéticos de cierre",
|
||
|
|
characteristics: {
|
||
|
|
[field("provider")]: item.provider,
|
||
|
|
[field("os_family")]: item.family,
|
||
|
|
[field("os_product_version")]: item.productVersion,
|
||
|
|
[field("os_edition_state")]: item.editionState,
|
||
|
|
[field("software_edition")]: item.edition,
|
||
|
|
},
|
||
|
|
});
|
||
|
|
|
||
|
|
const createProduct = async (version: string) => {
|
||
|
|
const item = { ...identity, productVersion: version };
|
||
|
|
const response = await admin.request(root, {
|
||
|
|
creationId: uuid(),
|
||
|
|
values: values(item),
|
||
|
|
reviewedIdentity: item,
|
||
|
|
});
|
||
|
|
|
||
|
|
assert.equal(response.status, 201);
|
||
|
|
|
||
|
|
return GetGlobalApplicationAdminResponseSchema.parse(await response.json()).resource;
|
||
|
|
};
|
||
|
|
|
||
|
|
const adminProduct = async () =>
|
||
|
|
GetGlobalApplicationAdminResponseSchema.parse(
|
||
|
|
await (await admin.request(`${root}/${productId}/admin`)).json()
|
||
|
|
).resource;
|
||
|
|
|
||
|
|
const local = async (owner: string, key: string, name: string) => {
|
||
|
|
const response = await ordinary.request(`/resources/${owner}`, {
|
||
|
|
values: {
|
||
|
|
name,
|
||
|
|
templateId: template(key).id,
|
||
|
|
active: true,
|
||
|
|
notes: "",
|
||
|
|
characteristics: {},
|
||
|
|
},
|
||
|
|
});
|
||
|
|
|
||
|
|
assert.equal(response.status, 201);
|
||
|
|
|
||
|
|
return ResourceResponseSchema.parse(await response.json()).resource;
|
||
|
|
};
|
||
|
|
|
||
|
|
for (let index = 0; index < 50; index += 1) {
|
||
|
|
await local(company, "application", `Cierre sintético Álvarez ${index + 1}`);
|
||
|
|
}
|
||
|
|
const licenseA = await local(company, "software_license", "Licencia A");
|
||
|
|
const licenseB = await local(other, "software_license", "Licencia B");
|
||
|
|
const localOs = await local(company, "os", "Windows 11 Pro");
|
||
|
|
const input = {
|
||
|
|
assetId: assetA,
|
||
|
|
resourceId: productId,
|
||
|
|
licenseResourceId: licenseA.id,
|
||
|
|
version: "23H2",
|
||
|
|
state: "configured",
|
||
|
|
notes: null,
|
||
|
|
};
|
||
|
|
const path = `/software-installations/${company}`;
|
||
|
|
|
||
|
|
const readInstallation = async (owner: string, assetId: string, resourceId: string) => {
|
||
|
|
const payload = SoftwareInstallationsResponseSchema.parse(
|
||
|
|
await (await ordinary.request(`/software-installations/${owner}`, undefined, v3)).json()
|
||
|
|
);
|
||
|
|
const item = payload.configurations.find(
|
||
|
|
(entry) => entry.values.assetId === assetId && entry.values.resourceId === resourceId
|
||
|
|
);
|
||
|
|
|
||
|
|
assert.ok(item);
|
||
|
|
|
||
|
|
return item;
|
||
|
|
};
|
||
|
|
|
||
|
|
const rootRows = (id: string) =>
|
||
|
|
database.query(
|
||
|
|
"SELECT id,companyId,scope,templateId,name,active FROM resources WHERE id=:id",
|
||
|
|
{ replacements: { id }, type: QueryTypes.SELECT }
|
||
|
|
);
|
||
|
|
|
||
|
|
const installationRows = (assetId: string, resourceId: string) =>
|
||
|
|
database.query(
|
||
|
|
"SELECT * FROM software_installations WHERE assetId=:assetId AND resourceId=:resourceId",
|
||
|
|
{ replacements: { assetId, resourceId }, type: QueryTypes.SELECT }
|
||
|
|
);
|
||
|
|
|
||
|
|
await t.test(
|
||
|
|
"B3 — evolución SQL doble conserva campos, valores, personalizaciones y revisiones",
|
||
|
|
() => {
|
||
|
|
assert.equal(legacy.evolutionRuns, 2);
|
||
|
|
assert.equal(legacy.originalResourcesUnchanged, true);
|
||
|
|
assert.equal(legacy.revisionsPreserved, 2);
|
||
|
|
results.push({
|
||
|
|
id: "B3",
|
||
|
|
result: "pass: two persisted evolutions; legacy root and two revisions unchanged",
|
||
|
|
});
|
||
|
|
}
|
||
|
|
);
|
||
|
|
await t.test("DB-SO-01 crear SO global", async () => {
|
||
|
|
const response = await admin.request(root, {
|
||
|
|
creationId: productId,
|
||
|
|
values: values(identity),
|
||
|
|
reviewedIdentity: identity,
|
||
|
|
});
|
||
|
|
|
||
|
|
assert.equal(response.status, 201);
|
||
|
|
const item = GetGlobalApplicationAdminResponseSchema.parse(await response.json()).resource;
|
||
|
|
|
||
|
|
assert.equal(item.id, productId);
|
||
|
|
assert.equal(item.specification.templateId, specification.templateId);
|
||
|
|
assert.deepEqual(await rootRows(productId), [
|
||
|
|
{
|
||
|
|
id: productId,
|
||
|
|
companyId: null,
|
||
|
|
scope: "global",
|
||
|
|
templateId: specification.templateId,
|
||
|
|
name: "Windows 11 Pro",
|
||
|
|
active: 1,
|
||
|
|
},
|
||
|
|
]);
|
||
|
|
results.push({ id: "DB-SO-01", result: "pass", http: [201] });
|
||
|
|
});
|
||
|
|
await t.test("DB-SO-02 crear aplicación global mantiene su tipo", async () => {
|
||
|
|
const response = await admin.request("/resource-global-applications", {
|
||
|
|
creationId: applicationId,
|
||
|
|
values: {
|
||
|
|
name: "Editor global",
|
||
|
|
templateId: template("application").id,
|
||
|
|
active: true,
|
||
|
|
notes: "",
|
||
|
|
characteristics: {},
|
||
|
|
},
|
||
|
|
});
|
||
|
|
|
||
|
|
assert.equal(response.status, 201);
|
||
|
|
assert.equal((await rootRows(applicationId)).length, 1);
|
||
|
|
const list = ListGlobalApplicationsResponseSchema.parse(
|
||
|
|
await (await ordinary.request("/resource-global-applications")).json()
|
||
|
|
);
|
||
|
|
|
||
|
|
assert.ok(list.items.some((item) => item.id === applicationId));
|
||
|
|
assert.ok(list.items.every((item) => item.kind === "application" && item.id !== productId));
|
||
|
|
results.push({ id: "DB-SO-02", result: "pass", http: [201] });
|
||
|
|
});
|
||
|
|
await t.test("DB-SO-03 licencia global rechazada sin raíz persistida", async () => {
|
||
|
|
const id = uuid();
|
||
|
|
|
||
|
|
assert.equal(
|
||
|
|
(
|
||
|
|
await admin.request(root, {
|
||
|
|
creationId: id,
|
||
|
|
values: { ...values(identity), templateId: template("software_license").id },
|
||
|
|
reviewedIdentity: identity,
|
||
|
|
})
|
||
|
|
).status,
|
||
|
|
409
|
||
|
|
);
|
||
|
|
assert.equal((await rootRows(id)).length, 0);
|
||
|
|
results.push({ id: "DB-SO-03", result: "pass", http: [409] });
|
||
|
|
});
|
||
|
|
await t.test("DB-SO-04 instalación válida desde A", async () => {
|
||
|
|
const response = await ordinary.request(path, { values: input }, v3);
|
||
|
|
|
||
|
|
assert.equal(response.status, 201);
|
||
|
|
const item = SoftwareInstallationResponseSchema.parse(await response.json()).configuration;
|
||
|
|
|
||
|
|
assert.equal(item.values.resourceId, productId);
|
||
|
|
assert.equal((await installationRows(assetA, productId)).length, 1);
|
||
|
|
results.push({ id: "DB-SO-04", result: "pass", http: [201] });
|
||
|
|
});
|
||
|
|
await t.test(
|
||
|
|
"DB-SO-05 mismo producto desde B, instalación y licencia independientes",
|
||
|
|
async () => {
|
||
|
|
const response = await ordinary.request(
|
||
|
|
`/software-installations/${other}`,
|
||
|
|
{
|
||
|
|
values: {
|
||
|
|
...input,
|
||
|
|
assetId: assetB.id.toPrimitive(),
|
||
|
|
licenseResourceId: licenseB.id,
|
||
|
|
version: "24H2",
|
||
|
|
},
|
||
|
|
},
|
||
|
|
v3
|
||
|
|
);
|
||
|
|
|
||
|
|
assert.equal(response.status, 201);
|
||
|
|
const a = await readInstallation(company, assetA, productId);
|
||
|
|
const b = SoftwareInstallationResponseSchema.parse(await response.json()).configuration;
|
||
|
|
|
||
|
|
assert.notEqual(a.id, b.id);
|
||
|
|
assert.equal(b.companyId, other);
|
||
|
|
assert.equal(b.values.version, "24H2");
|
||
|
|
assert.equal((await installationRows(assetB.id.toPrimitive(), productId)).length, 1);
|
||
|
|
results.push({ id: "DB-SO-05", result: "pass", http: [201] });
|
||
|
|
}
|
||
|
|
);
|
||
|
|
await t.test("DB-SO-06 dos activos de A usan el mismo SO global", async () => {
|
||
|
|
assert.equal(
|
||
|
|
(await ordinary.request(path, { values: { ...input, assetId: assetA2 } }, v3)).status,
|
||
|
|
201
|
||
|
|
);
|
||
|
|
assert.notEqual(
|
||
|
|
(await readInstallation(company, assetA, productId)).id,
|
||
|
|
(await readInstallation(company, assetA2, productId)).id
|
||
|
|
);
|
||
|
|
assert.equal(
|
||
|
|
(
|
||
|
|
await database.query(
|
||
|
|
"SELECT id FROM software_installations WHERE companyId=:company AND resourceId=:product",
|
||
|
|
{ replacements: { company, product: productId }, type: QueryTypes.SELECT }
|
||
|
|
)
|
||
|
|
).length,
|
||
|
|
2
|
||
|
|
);
|
||
|
|
results.push({ id: "DB-SO-06", result: "pass", http: [201] });
|
||
|
|
});
|
||
|
|
await t.test(
|
||
|
|
"DB-SO-07 particulares y globales equivalentes conservan UUID e instalaciones",
|
||
|
|
async () => {
|
||
|
|
assert.notEqual(localOs.id, productId);
|
||
|
|
assert.equal(
|
||
|
|
(await ordinary.request(path, { values: { ...input, resourceId: localOs.id } }, v3))
|
||
|
|
.status,
|
||
|
|
201
|
||
|
|
);
|
||
|
|
assert.equal((await installationRows(assetA, localOs.id)).length, 1);
|
||
|
|
assert.equal((await installationRows(assetA, productId)).length, 1);
|
||
|
|
const list = ListApplicationCandidatesResponseSchema.parse(
|
||
|
|
await (
|
||
|
|
await ordinary.request(
|
||
|
|
`/resource-application-candidates/${company}?purpose=software-installation&search=Windows%2011`,
|
||
|
|
undefined,
|
||
|
|
v3
|
||
|
|
)
|
||
|
|
).json()
|
||
|
|
);
|
||
|
|
|
||
|
|
assert.ok(list.items.some((item) => item.id === productId && item.scope === "global"));
|
||
|
|
assert.ok(list.items.some((item) => item.id === localOs.id && item.scope === "company"));
|
||
|
|
results.push({ id: "DB-SO-07", result: "pass", http: [201, 200] });
|
||
|
|
}
|
||
|
|
);
|
||
|
|
await t.test("DB-SO-08 licencia de A válida y particular", async () => {
|
||
|
|
const item = await readInstallation(company, assetA, productId);
|
||
|
|
|
||
|
|
assert.equal(item.values.licenseResourceId, licenseA.id);
|
||
|
|
const rows = await database.query(
|
||
|
|
"SELECT r.scope,r.companyId FROM resources r JOIN software_installations s ON s.licenseResourceId=r.id WHERE s.id=:id",
|
||
|
|
{ replacements: { id: item.id }, type: QueryTypes.SELECT }
|
||
|
|
);
|
||
|
|
|
||
|
|
assert.deepEqual(rows, [{ scope: "company", companyId: company }]);
|
||
|
|
results.push({ id: "DB-SO-08", result: "pass", http: [200] });
|
||
|
|
});
|
||
|
|
await t.test("DB-SO-09 licencia de A no puede vincularse desde B", async () => {
|
||
|
|
const b = await readInstallation(other, assetB.id.toPrimitive(), productId);
|
||
|
|
|
||
|
|
assert.equal(
|
||
|
|
(
|
||
|
|
await ordinary.request(
|
||
|
|
`/software-installations/${other}/${b.id}/update`,
|
||
|
|
{ values: { ...b.values, licenseResourceId: licenseA.id } },
|
||
|
|
v3
|
||
|
|
)
|
||
|
|
).status,
|
||
|
|
404
|
||
|
|
);
|
||
|
|
assert.equal(
|
||
|
|
(await readInstallation(other, assetB.id.toPrimitive(), productId)).values
|
||
|
|
.licenseResourceId,
|
||
|
|
licenseB.id
|
||
|
|
);
|
||
|
|
results.push({ id: "DB-SO-09", result: "pass", http: [404] });
|
||
|
|
});
|
||
|
|
await t.test("DB-SO-10 SO excluido de ApplicationConfiguration", async () => {
|
||
|
|
const response = await ordinary.request(
|
||
|
|
`/application-configurations/${company}`,
|
||
|
|
{
|
||
|
|
values: {
|
||
|
|
profileId: volume.profileIds[0],
|
||
|
|
resourceId: productId,
|
||
|
|
delivery: "installed",
|
||
|
|
version: null,
|
||
|
|
state: "configured",
|
||
|
|
notes: null,
|
||
|
|
},
|
||
|
|
},
|
||
|
|
v3
|
||
|
|
);
|
||
|
|
|
||
|
|
assert.equal(response.status, 404);
|
||
|
|
assert.equal(
|
||
|
|
(
|
||
|
|
await database.query("SELECT id FROM application_configurations WHERE resourceId=:id", {
|
||
|
|
replacements: { id: productId },
|
||
|
|
type: QueryTypes.SELECT,
|
||
|
|
})
|
||
|
|
).length,
|
||
|
|
0
|
||
|
|
);
|
||
|
|
results.push({ id: "DB-SO-10", result: "pass", http: [404] });
|
||
|
|
});
|
||
|
|
await t.test("DB-SO-11 desactivar conserva instalaciones usadas", async () => {
|
||
|
|
const before = await database.query(
|
||
|
|
"SELECT * FROM software_installations WHERE resourceId=:id ORDER BY id",
|
||
|
|
{ replacements: { id: productId }, type: QueryTypes.SELECT }
|
||
|
|
);
|
||
|
|
|
||
|
|
assert.equal(
|
||
|
|
(await admin.request(`${root}/${productId}/status`, { active: false })).status,
|
||
|
|
200
|
||
|
|
);
|
||
|
|
assert.deepEqual(
|
||
|
|
await database.query(
|
||
|
|
"SELECT * FROM software_installations WHERE resourceId=:id ORDER BY id",
|
||
|
|
{ replacements: { id: productId }, type: QueryTypes.SELECT }
|
||
|
|
),
|
||
|
|
before
|
||
|
|
);
|
||
|
|
results.push({ id: "DB-SO-11", result: "pass", http: [200] });
|
||
|
|
});
|
||
|
|
await t.test("DB-SO-12 inactivo bloquea nueva instalación", async () => {
|
||
|
|
assert.equal(
|
||
|
|
(await ordinary.request(path, { values: { ...input, assetId: raceAsset } }, v3)).status,
|
||
|
|
409
|
||
|
|
);
|
||
|
|
assert.equal((await installationRows(raceAsset, productId)).length, 0);
|
||
|
|
results.push({ id: "DB-SO-12", result: "pass", http: [409] });
|
||
|
|
});
|
||
|
|
await t.test("DB-SO-13 historial y resolver conservan SO inactivo", async () => {
|
||
|
|
const history = ListGlobalApplicationHistoryResponseSchema.parse(
|
||
|
|
await (await admin.request(`${root}/${productId}/history`)).json()
|
||
|
|
);
|
||
|
|
|
||
|
|
assert.equal(history.items.length, 2);
|
||
|
|
const resolved = ResolveApplicationReferencesResponseSchema.parse(
|
||
|
|
await (
|
||
|
|
await ordinary.request(
|
||
|
|
`/resource-application-references/${company}/resolve`,
|
||
|
|
{ purpose: "software-installation", ids: [productId, localOs.id] },
|
||
|
|
v3
|
||
|
|
)
|
||
|
|
).json()
|
||
|
|
);
|
||
|
|
|
||
|
|
assert.ok(resolved.items[0]?.status === "resolved" && !resolved.items[0].reference.active);
|
||
|
|
assert.equal(
|
||
|
|
(
|
||
|
|
await database.query("SELECT id FROM resource_revisions WHERE resourceId=:id", {
|
||
|
|
replacements: { id: productId },
|
||
|
|
type: QueryTypes.SELECT,
|
||
|
|
})
|
||
|
|
).length,
|
||
|
|
2
|
||
|
|
);
|
||
|
|
results.push({ id: "DB-SO-13", result: "pass", http: [200, 200] });
|
||
|
|
});
|
||
|
|
await t.test("DB-SO-14 versión instalada genera revisión sin cambiar producto", async () => {
|
||
|
|
assert.equal(
|
||
|
|
(
|
||
|
|
await admin.request(`${root}/${productId}/status`, {
|
||
|
|
active: true,
|
||
|
|
reviewedIdentity: identity,
|
||
|
|
})
|
||
|
|
).status,
|
||
|
|
200
|
||
|
|
);
|
||
|
|
const current = await readInstallation(company, assetA, productId);
|
||
|
|
|
||
|
|
assert.equal(
|
||
|
|
(
|
||
|
|
await ordinary.request(
|
||
|
|
`${path}/${current.id}/update`,
|
||
|
|
{ values: { ...current.values, version: "24H2" } },
|
||
|
|
v3
|
||
|
|
)
|
||
|
|
).status,
|
||
|
|
200
|
||
|
|
);
|
||
|
|
const changed = await readInstallation(company, assetA, productId);
|
||
|
|
|
||
|
|
assert.equal(changed.id, current.id);
|
||
|
|
assert.equal(changed.values.resourceId, productId);
|
||
|
|
assert.equal(changed.history.length, current.history.length + 1);
|
||
|
|
assert.equal(
|
||
|
|
(
|
||
|
|
await database.query(
|
||
|
|
"SELECT id FROM software_installation_revisions WHERE configurationId=:id",
|
||
|
|
{ replacements: { id: current.id }, type: QueryTypes.SELECT }
|
||
|
|
)
|
||
|
|
).length,
|
||
|
|
changed.history.length
|
||
|
|
);
|
||
|
|
results.push({ id: "DB-SO-14", result: "pass", http: [200, 200] });
|
||
|
|
});
|
||
|
|
await t.test("DB-SO-15 destino de instalación inmutable", async () => {
|
||
|
|
const current = await readInstallation(company, assetA, productId);
|
||
|
|
const before = await database.query(
|
||
|
|
"SELECT * FROM software_installation_revisions WHERE configurationId=:id ORDER BY position",
|
||
|
|
{ replacements: { id: current.id }, type: QueryTypes.SELECT }
|
||
|
|
);
|
||
|
|
const response = await ordinary.request(
|
||
|
|
`${path}/${current.id}/update`,
|
||
|
|
{ values: { ...current.values, resourceId: localOs.id } },
|
||
|
|
v3
|
||
|
|
);
|
||
|
|
|
||
|
|
assert.equal(response.status, 409);
|
||
|
|
assert.equal(ErrorSchema.parse(await response.json()).code, "SOFTWARE_TARGET_IMMUTABLE");
|
||
|
|
assert.deepEqual(
|
||
|
|
await database.query(
|
||
|
|
"SELECT * FROM software_installation_revisions WHERE configurationId=:id ORDER BY position",
|
||
|
|
{ replacements: { id: current.id }, type: QueryTypes.SELECT }
|
||
|
|
),
|
||
|
|
before
|
||
|
|
);
|
||
|
|
results.push({ id: "DB-SO-15", result: "pass", http: [409] });
|
||
|
|
});
|
||
|
|
await t.test("DB-SO-16 retirada de instalación de producto inactivo", async () => {
|
||
|
|
assert.equal(
|
||
|
|
(await admin.request(`${root}/${productId}/status`, { active: false })).status,
|
||
|
|
200
|
||
|
|
);
|
||
|
|
const current = await readInstallation(company, assetA, productId);
|
||
|
|
|
||
|
|
for (const state of ["removal_pending", "removed"]) {
|
||
|
|
assert.equal(
|
||
|
|
(
|
||
|
|
await ordinary.request(
|
||
|
|
`${path}/${current.id}/update`,
|
||
|
|
{ values: { ...current.values, state } },
|
||
|
|
v3
|
||
|
|
)
|
||
|
|
).status,
|
||
|
|
200
|
||
|
|
);
|
||
|
|
}
|
||
|
|
const removed = await readInstallation(company, assetA, productId);
|
||
|
|
|
||
|
|
assert.equal(removed.id, current.id);
|
||
|
|
assert.equal(removed.values.state, "removed");
|
||
|
|
assert.equal(removed.history.length, current.history.length + 2);
|
||
|
|
results.push({ id: "DB-SO-16", result: "pass", http: [200, 200] });
|
||
|
|
});
|
||
|
|
await t.test("DB-SO-17 replay de creación no duplica ni reactiva", async () => {
|
||
|
|
const before = await resourceSqlSnapshot(database, productId);
|
||
|
|
|
||
|
|
assert.equal(
|
||
|
|
(
|
||
|
|
await admin.request(root, {
|
||
|
|
creationId: productId,
|
||
|
|
values: values(identity),
|
||
|
|
reviewedIdentity: identity,
|
||
|
|
})
|
||
|
|
).status,
|
||
|
|
200
|
||
|
|
);
|
||
|
|
assert.deepEqual(await resourceSqlSnapshot(database, productId), before);
|
||
|
|
results.push({ id: "DB-SO-17", result: "pass", http: [200] });
|
||
|
|
});
|
||
|
|
await t.test("DB-SO-18 activar/desactivar preserva UUID, historial y referencias", async () => {
|
||
|
|
const before = await adminProduct();
|
||
|
|
|
||
|
|
assert.equal(
|
||
|
|
(
|
||
|
|
await admin.request(`${root}/${productId}/status`, {
|
||
|
|
active: true,
|
||
|
|
reviewedIdentity: identity,
|
||
|
|
})
|
||
|
|
).status,
|
||
|
|
200
|
||
|
|
);
|
||
|
|
assert.equal(
|
||
|
|
(await admin.request(`${root}/${productId}/status`, { active: false })).status,
|
||
|
|
200
|
||
|
|
);
|
||
|
|
const after = await adminProduct();
|
||
|
|
|
||
|
|
assert.equal(after.id, before.id);
|
||
|
|
const history = ListGlobalApplicationHistoryResponseSchema.parse(
|
||
|
|
await (await admin.request(`${root}/${productId}/history`)).json()
|
||
|
|
);
|
||
|
|
|
||
|
|
assert.equal(history.items.length, 6);
|
||
|
|
assert.equal((await installationRows(assetA2, productId)).length, 1);
|
||
|
|
results.push({ id: "DB-SO-18", result: "pass", http: [200, 200] });
|
||
|
|
});
|
||
|
|
await t.test("DB-SO-19 solo stock: copia y otros tipos no adquieren globalidad", async () => {
|
||
|
|
const os = template("os");
|
||
|
|
|
||
|
|
assert.ok(os.values.kind === "template");
|
||
|
|
const response = await admin.request("/resource-catalog/global", {
|
||
|
|
values: { ...os.values, name: "Copia de SO para cierre" },
|
||
|
|
});
|
||
|
|
|
||
|
|
assert.equal(response.status, 201);
|
||
|
|
const copy = SaveDefinitionResponseSchema.parse(await response.json());
|
||
|
|
const ids = [
|
||
|
|
copy.savedId,
|
||
|
|
...["software_license", "support", "shared_folder", "access", "certificate"].map(
|
||
|
|
(key) => template(key).id
|
||
|
|
),
|
||
|
|
];
|
||
|
|
|
||
|
|
for (const templateId of ids) {
|
||
|
|
for (const endpoint of [root, "/resource-global-applications"]) {
|
||
|
|
const id = uuid();
|
||
|
|
|
||
|
|
assert.equal(
|
||
|
|
(
|
||
|
|
await admin.request(endpoint, {
|
||
|
|
creationId: id,
|
||
|
|
values: {
|
||
|
|
name: "Global no autorizado",
|
||
|
|
templateId,
|
||
|
|
active: false,
|
||
|
|
notes: "",
|
||
|
|
characteristics: {},
|
||
|
|
},
|
||
|
|
})
|
||
|
|
).status,
|
||
|
|
409
|
||
|
|
);
|
||
|
|
assert.equal((await rootRows(id)).length, 0);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
results.push({
|
||
|
|
id: "DB-SO-19",
|
||
|
|
result: "pass: stock accepted; copied/support/SMB/access/certificate/license rejected",
|
||
|
|
http: [201, 409],
|
||
|
|
});
|
||
|
|
});
|
||
|
|
await t.test(
|
||
|
|
"DB-SO-20 particular anterior incompleto sigue siendo editable y local",
|
||
|
|
async () => {
|
||
|
|
const inventory = InventoryResponseSchema.parse(
|
||
|
|
await (await ordinary.request(`/resources/${company}`)).json()
|
||
|
|
);
|
||
|
|
const current = inventory.resources.find((item) => item.id === legacy.resourceId);
|
||
|
|
|
||
|
|
assert.ok(current);
|
||
|
|
assert.equal(
|
||
|
|
(
|
||
|
|
await ordinary.request(`/resources/${company}/${current.id}/update`, {
|
||
|
|
values: { ...current.values, notes: "Corrección posterior legítima" },
|
||
|
|
})
|
||
|
|
).status,
|
||
|
|
200
|
||
|
|
);
|
||
|
|
const after = InventoryResponseSchema.parse(
|
||
|
|
await (await ordinary.request(`/resources/${company}`)).json()
|
||
|
|
).resources.find((item) => item.id === current.id);
|
||
|
|
|
||
|
|
assert.ok(after);
|
||
|
|
assert.deepEqual(after.values.characteristics, current.values.characteristics);
|
||
|
|
assert.equal(after.history.length, 3);
|
||
|
|
assert.equal(after.companyId, company);
|
||
|
|
results.push({ id: "DB-SO-20", result: "pass", http: [200] });
|
||
|
|
}
|
||
|
|
);
|
||
|
|
|
||
|
|
await t.test(
|
||
|
|
"C1 — equivalentes concurrentes esperan el lock de catálogo y solo uno persiste",
|
||
|
|
async () => {
|
||
|
|
const item = { ...identity, productVersion: "30" };
|
||
|
|
const firstId = uuid();
|
||
|
|
const secondId = uuid();
|
||
|
|
const arrived = latch();
|
||
|
|
const release = latch();
|
||
|
|
|
||
|
|
database.addHook("afterQuery", "closure-duplicate", async (_options, query) => {
|
||
|
|
const sql: unknown = Reflect.get(query, "sql");
|
||
|
|
|
||
|
|
if (
|
||
|
|
typeof sql === "string" &&
|
||
|
|
sql.startsWith("INSERT INTO `resources`") &&
|
||
|
|
sql.includes(firstId)
|
||
|
|
) {
|
||
|
|
arrived.release();
|
||
|
|
await release.promise;
|
||
|
|
}
|
||
|
|
});
|
||
|
|
const first = admin.request(root, {
|
||
|
|
creationId: firstId,
|
||
|
|
values: values(item),
|
||
|
|
reviewedIdentity: item,
|
||
|
|
});
|
||
|
|
let second: Promise<Response> | undefined;
|
||
|
|
|
||
|
|
try {
|
||
|
|
await waitBarrier(arrived.promise);
|
||
|
|
second = admin.request(root, {
|
||
|
|
creationId: secondId,
|
||
|
|
values: values(item),
|
||
|
|
reviewedIdentity: item,
|
||
|
|
});
|
||
|
|
await waitCatalogLock(process.env.METIS_RESOURCE_SCOPE_DISPOSABLE ?? "");
|
||
|
|
release.release();
|
||
|
|
assert.equal((await first).status, 201);
|
||
|
|
assert.equal((await second).status, 409);
|
||
|
|
assert.equal((await rootRows(firstId)).length, 1);
|
||
|
|
assert.equal((await rootRows(secondId)).length, 0);
|
||
|
|
results.push({
|
||
|
|
id: "C1",
|
||
|
|
result: "pass: observed InnoDB catalog wait; one equivalent persisted",
|
||
|
|
http: [201, 409],
|
||
|
|
});
|
||
|
|
} finally {
|
||
|
|
release.release();
|
||
|
|
database.removeHook("afterQuery", "closure-duplicate");
|
||
|
|
await Promise.allSettled([first, ...(second ? [second] : [])]);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
);
|
||
|
|
await t.test(
|
||
|
|
"C2 — desactivación confirma primero y asociación revalida después de esperar",
|
||
|
|
async () => {
|
||
|
|
const target = await createProduct("31");
|
||
|
|
const arrived = latch();
|
||
|
|
const release = latch();
|
||
|
|
|
||
|
|
database.addHook("afterQuery", "closure-disable-first", async (_options, query) => {
|
||
|
|
const sql: unknown = Reflect.get(query, "sql");
|
||
|
|
|
||
|
|
if (
|
||
|
|
typeof sql === "string" &&
|
||
|
|
sql.startsWith("INSERT INTO `resources`") &&
|
||
|
|
sql.includes(target.id)
|
||
|
|
) {
|
||
|
|
arrived.release();
|
||
|
|
await release.promise;
|
||
|
|
}
|
||
|
|
});
|
||
|
|
const disabling = admin.request(`${root}/${target.id}/status`, { active: false });
|
||
|
|
let installing: Promise<Response> | undefined;
|
||
|
|
|
||
|
|
try {
|
||
|
|
await waitBarrier(arrived.promise);
|
||
|
|
installing = ordinary.request(
|
||
|
|
path,
|
||
|
|
{ values: { ...input, assetId: raceAsset, resourceId: target.id } },
|
||
|
|
v3
|
||
|
|
);
|
||
|
|
await waitCatalogLock(process.env.METIS_RESOURCE_SCOPE_DISPOSABLE ?? "");
|
||
|
|
release.release();
|
||
|
|
assert.equal((await disabling).status, 200);
|
||
|
|
assert.equal((await installing).status, 409);
|
||
|
|
assert.equal((await installationRows(raceAsset, target.id)).length, 0);
|
||
|
|
results.push({
|
||
|
|
id: "C2",
|
||
|
|
result: "pass: deactivation first, waiting installation rejected",
|
||
|
|
http: [200, 409],
|
||
|
|
});
|
||
|
|
} finally {
|
||
|
|
release.release();
|
||
|
|
database.removeHook("afterQuery", "closure-disable-first");
|
||
|
|
await Promise.allSettled([disabling, ...(installing ? [installing] : [])]);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
);
|
||
|
|
await t.test(
|
||
|
|
"C3 — identidad, instalación, metadatos y desactivación concurrentes",
|
||
|
|
async () => {
|
||
|
|
assert.equal(
|
||
|
|
(
|
||
|
|
await admin.request(`${root}/${productId}/status`, {
|
||
|
|
active: true,
|
||
|
|
reviewedIdentity: identity,
|
||
|
|
})
|
||
|
|
).status,
|
||
|
|
200
|
||
|
|
);
|
||
|
|
const current = await adminProduct();
|
||
|
|
const beforeHistory = await database.query(
|
||
|
|
"SELECT id FROM resource_revisions WHERE resourceId=:id",
|
||
|
|
{ replacements: { id: productId }, type: QueryTypes.SELECT }
|
||
|
|
);
|
||
|
|
const changed = { ...identity, productVersion: "99" };
|
||
|
|
const responses = await Promise.all([
|
||
|
|
admin.request(`${root}/${productId}/update`, {
|
||
|
|
values: values(changed),
|
||
|
|
reviewedIdentity: changed,
|
||
|
|
}),
|
||
|
|
ordinary.request(path, { values: { ...input, assetId: raceAsset } }, v3),
|
||
|
|
admin.request(`${root}/${productId}/update`, {
|
||
|
|
values: { ...current.values, notes: "Metadatos concurrentes" },
|
||
|
|
reviewedIdentity: identity,
|
||
|
|
expectedRevisionId: current.revisionId,
|
||
|
|
}),
|
||
|
|
admin.request(`${root}/${productId}/status`, {
|
||
|
|
active: false,
|
||
|
|
expectedRevisionId: current.revisionId,
|
||
|
|
}),
|
||
|
|
]);
|
||
|
|
const identityResult = responses[0];
|
||
|
|
const installationResult = responses[1];
|
||
|
|
const metadataResult = responses[2];
|
||
|
|
const statusResult = responses[3];
|
||
|
|
|
||
|
|
assert.ok(identityResult && installationResult && metadataResult && statusResult);
|
||
|
|
assert.equal(identityResult.status, 409);
|
||
|
|
assert.equal(ErrorSchema.parse(await identityResult.json()).code, "OS_IDENTITY_FIXED");
|
||
|
|
assert.ok([201, 409].includes(installationResult.status));
|
||
|
|
assert.deepEqual([metadataResult.status, statusResult.status].sort(), [200, 409]);
|
||
|
|
const publicProduct = GetGlobalOperatingSystemResponseSchema.parse(
|
||
|
|
await (await ordinary.request(`${root}/${productId}`)).json()
|
||
|
|
).resource;
|
||
|
|
|
||
|
|
assert.equal(publicProduct.productVersion, "11");
|
||
|
|
assert.equal(publicProduct.name, "Windows 11 Pro");
|
||
|
|
assert.equal(
|
||
|
|
(
|
||
|
|
await database.query("SELECT id FROM resource_revisions WHERE resourceId=:id", {
|
||
|
|
replacements: { id: productId },
|
||
|
|
type: QueryTypes.SELECT,
|
||
|
|
})
|
||
|
|
).length,
|
||
|
|
beforeHistory.length + 1
|
||
|
|
);
|
||
|
|
assert.equal(
|
||
|
|
(await installationRows(raceAsset, productId)).length,
|
||
|
|
installationResult.status === 201 ? 1 : 0
|
||
|
|
);
|
||
|
|
results.push({
|
||
|
|
id: "C3",
|
||
|
|
result: "pass: commercial identity fixed; exactly one optimistic admin update",
|
||
|
|
http: responses.map((response) => response.status),
|
||
|
|
});
|
||
|
|
}
|
||
|
|
);
|
||
|
|
await t.test(
|
||
|
|
"C4a — fallo tras escribir raíz global revierte raíz, características y revisiones",
|
||
|
|
async () => {
|
||
|
|
const id = uuid();
|
||
|
|
const item = { ...identity, productVersion: "40" };
|
||
|
|
let faultTriggered = false;
|
||
|
|
|
||
|
|
database.addHook("afterQuery", "closure-fail-resource", async (_options, query) => {
|
||
|
|
const sql: unknown = Reflect.get(query, "sql");
|
||
|
|
|
||
|
|
if (
|
||
|
|
typeof sql === "string" &&
|
||
|
|
sql.startsWith("INSERT INTO `resource_revisions`") &&
|
||
|
|
sql.includes(id)
|
||
|
|
) {
|
||
|
|
faultTriggered = true;
|
||
|
|
throw new Error("CONTROLLED_CLOSURE_RESOURCE_FAILURE");
|
||
|
|
}
|
||
|
|
});
|
||
|
|
try {
|
||
|
|
const response = await admin.request(root, {
|
||
|
|
creationId: id,
|
||
|
|
values: values(item),
|
||
|
|
reviewedIdentity: item,
|
||
|
|
});
|
||
|
|
|
||
|
|
assert.equal(response.status, 500);
|
||
|
|
assert.equal(faultTriggered, true);
|
||
|
|
const body = await response.text();
|
||
|
|
|
||
|
|
assert.ok(
|
||
|
|
!(body.includes("CONTROLLED_") || body.includes("INSERT") || body.includes("stack"))
|
||
|
|
);
|
||
|
|
const snapshot = await resourceSqlSnapshot(database, id);
|
||
|
|
|
||
|
|
assert.ok(
|
||
|
|
Object.values(snapshot).every((rows) => Array.isArray(rows) && rows.length === 0)
|
||
|
|
);
|
||
|
|
} finally {
|
||
|
|
database.removeHook("afterQuery", "closure-fail-resource");
|
||
|
|
}
|
||
|
|
assert.equal(
|
||
|
|
(
|
||
|
|
await admin.request(root, {
|
||
|
|
creationId: id,
|
||
|
|
values: values(item),
|
||
|
|
reviewedIdentity: item,
|
||
|
|
})
|
||
|
|
).status,
|
||
|
|
201
|
||
|
|
);
|
||
|
|
results.push({
|
||
|
|
id: "C4a",
|
||
|
|
result: "pass: rollback of root/values/revisions, same creation UUID retry succeeds",
|
||
|
|
http: [500, 201],
|
||
|
|
});
|
||
|
|
}
|
||
|
|
);
|
||
|
|
await t.test(
|
||
|
|
"C4b — fallo en revisión de instalación revierte instalación completa",
|
||
|
|
async () => {
|
||
|
|
const target = await createProduct("41");
|
||
|
|
const previousProduct = await resourceSqlSnapshot(database, target.id);
|
||
|
|
let faultTriggered = false;
|
||
|
|
|
||
|
|
database.addHook("afterQuery", "closure-fail-installation", async (_options, query) => {
|
||
|
|
const sql: unknown = Reflect.get(query, "sql");
|
||
|
|
|
||
|
|
if (
|
||
|
|
typeof sql === "string" &&
|
||
|
|
sql.startsWith("INSERT INTO software_installation_revisions") &&
|
||
|
|
sql.includes(target.id)
|
||
|
|
) {
|
||
|
|
faultTriggered = true;
|
||
|
|
throw new Error("CONTROLLED_CLOSURE_INSTALLATION_FAILURE");
|
||
|
|
}
|
||
|
|
});
|
||
|
|
try {
|
||
|
|
assert.equal(
|
||
|
|
(
|
||
|
|
await ordinary.request(
|
||
|
|
path,
|
||
|
|
{ values: { ...input, assetId: raceAsset, resourceId: target.id } },
|
||
|
|
v3
|
||
|
|
)
|
||
|
|
).status,
|
||
|
|
500
|
||
|
|
);
|
||
|
|
assert.equal((await installationRows(raceAsset, target.id)).length, 0);
|
||
|
|
assert.equal(faultTriggered, true);
|
||
|
|
assert.equal(
|
||
|
|
(
|
||
|
|
await database.query(
|
||
|
|
"SELECT id FROM software_installation_revisions WHERE resourceId=:id",
|
||
|
|
{ replacements: { id: target.id }, type: QueryTypes.SELECT }
|
||
|
|
)
|
||
|
|
).length,
|
||
|
|
0
|
||
|
|
);
|
||
|
|
assert.deepEqual(await resourceSqlSnapshot(database, target.id), previousProduct);
|
||
|
|
} finally {
|
||
|
|
database.removeHook("afterQuery", "closure-fail-installation");
|
||
|
|
}
|
||
|
|
results.push({
|
||
|
|
id: "C4b",
|
||
|
|
result: "pass: no partial installation or orphan revision; Resource unchanged",
|
||
|
|
http: [500],
|
||
|
|
});
|
||
|
|
}
|
||
|
|
);
|
||
|
|
await t.test("D — contratos v2/v3, propósito, capabilities y autorización", async () => {
|
||
|
|
const capabilities2 = await (
|
||
|
|
await ordinary.request(`/resource-application-candidates/${company}/capabilities`)
|
||
|
|
).json();
|
||
|
|
|
||
|
|
assert.deepEqual(Object.keys(z.record(z.string(), z.unknown()).parse(capabilities2)).sort(), [
|
||
|
|
"profileAssociation",
|
||
|
|
"profileSelection",
|
||
|
|
"softwareAssociation",
|
||
|
|
"softwareSelection",
|
||
|
|
]);
|
||
|
|
const capabilities3 = ApplicationReferenceCapabilitiesSchema.parse(
|
||
|
|
await (
|
||
|
|
await ordinary.request(
|
||
|
|
`/resource-application-candidates/${company}/capabilities`,
|
||
|
|
undefined,
|
||
|
|
v3
|
||
|
|
)
|
||
|
|
).json()
|
||
|
|
);
|
||
|
|
|
||
|
|
assert.equal(capabilities3.operatingSystemAssociation, true);
|
||
|
|
assert.equal((await ordinary.request(path)).status, 409);
|
||
|
|
assert.equal((await ordinary.request(path, undefined, { contract: null })).status, 409);
|
||
|
|
assert.equal((await ordinary.request(path, undefined, { contract: "4" })).status, 400);
|
||
|
|
assert.equal((await ordinary.request(`${root}/${productId}/admin`)).status, 403);
|
||
|
|
assert.equal(
|
||
|
|
(
|
||
|
|
await ordinary.request(root, {
|
||
|
|
creationId: uuid(),
|
||
|
|
values: values(identity),
|
||
|
|
reviewedIdentity: identity,
|
||
|
|
})
|
||
|
|
).status,
|
||
|
|
403
|
||
|
|
);
|
||
|
|
assert.equal(
|
||
|
|
(
|
||
|
|
await admin.request(
|
||
|
|
`${root}/${productId}/status`,
|
||
|
|
{ active: false },
|
||
|
|
{ token: "invalid" }
|
||
|
|
)
|
||
|
|
).status,
|
||
|
|
403
|
||
|
|
);
|
||
|
|
const osList = ListGlobalOperatingSystemsResponseSchema.parse(
|
||
|
|
await (await ordinary.request(root)).json()
|
||
|
|
);
|
||
|
|
|
||
|
|
assert.ok(osList.items.every((item) => item.kind === "os" && item.id !== applicationId));
|
||
|
|
results.push({
|
||
|
|
id: "D",
|
||
|
|
result:
|
||
|
|
"pass: exact capabilities v2, v3 OS, incompatible reads and unauthorized writes rejected",
|
||
|
|
http: [200, 409, 400, 403],
|
||
|
|
});
|
||
|
|
});
|
||
|
|
await t.test("F — cerrar gates conserva lectura, historial y retirada legítima", async () => {
|
||
|
|
const current = await readInstallation(company, assetA2, productId);
|
||
|
|
|
||
|
|
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_SOFTWARE_CANDIDATES_ENABLED: "false",
|
||
|
|
RESOURCE_GLOBAL_PROFILE_ENABLED: "false",
|
||
|
|
RESOURCE_GLOBAL_OS_CATALOG_ENABLED: "false",
|
||
|
|
RESOURCE_GLOBAL_OS_SOFTWARE_ENABLED: "false",
|
||
|
|
})
|
||
|
|
);
|
||
|
|
server = createApp(() => undefined, runtime).listen(0, "127.0.0.1");
|
||
|
|
await once(server, "listening");
|
||
|
|
const next = server.address();
|
||
|
|
|
||
|
|
assert.ok(next && typeof next !== "string");
|
||
|
|
const manager = await scopeApiClient(`http://127.0.0.1:${next.port}`);
|
||
|
|
const client = await scopeApiClient(`http://127.0.0.1:${next.port}`);
|
||
|
|
|
||
|
|
assert.equal(
|
||
|
|
(await manager.request("/auth/login", { email: "closure-admin@example.invalid", password }))
|
||
|
|
.status,
|
||
|
|
200
|
||
|
|
);
|
||
|
|
await manager.refresh();
|
||
|
|
assert.equal(
|
||
|
|
(await client.request("/auth/login", { email: "closure-user@example.invalid", password }))
|
||
|
|
.status,
|
||
|
|
200
|
||
|
|
);
|
||
|
|
await client.refresh();
|
||
|
|
assert.equal((await client.request(`${root}/${productId}`)).status, 200);
|
||
|
|
assert.equal((await manager.request(`${root}/${productId}/history`)).status, 200);
|
||
|
|
assert.equal((await client.request(path, undefined, v3)).status, 200);
|
||
|
|
assert.equal(
|
||
|
|
(
|
||
|
|
await manager.request(root, {
|
||
|
|
creationId: uuid(),
|
||
|
|
values: values({ ...identity, productVersion: "50" }),
|
||
|
|
reviewedIdentity: { ...identity, productVersion: "50" },
|
||
|
|
})
|
||
|
|
).status,
|
||
|
|
409
|
||
|
|
);
|
||
|
|
for (const state of ["removal_pending", "removed"]) {
|
||
|
|
assert.equal(
|
||
|
|
(
|
||
|
|
await client.request(
|
||
|
|
`${path}/${current.id}/update`,
|
||
|
|
{ values: { ...current.values, state } },
|
||
|
|
v3
|
||
|
|
)
|
||
|
|
).status,
|
||
|
|
200
|
||
|
|
);
|
||
|
|
}
|
||
|
|
results.push({
|
||
|
|
id: "F",
|
||
|
|
result: "pass: all six gates closed; read/history/removal preserved",
|
||
|
|
http: [200, 409],
|
||
|
|
});
|
||
|
|
});
|
||
|
|
const orphans = await database.query(
|
||
|
|
"SELECT r.id FROM software_installation_revisions r LEFT JOIN software_installations s ON s.id=r.configurationId WHERE s.id IS NULL UNION ALL SELECT r.id FROM resource_revisions r LEFT JOIN resources s ON s.id=r.resourceId WHERE s.id IS NULL",
|
||
|
|
{ type: QueryTypes.SELECT }
|
||
|
|
);
|
||
|
|
|
||
|
|
assert.equal(orphans.length, 0);
|
||
|
|
await writeScopeEvidence("closure-results.json", {
|
||
|
|
results,
|
||
|
|
databaseCases: results.filter((item) => item.id.startsWith("DB-SO-")).length,
|
||
|
|
companies: 15,
|
||
|
|
principal: { people: 50, assets: 50, workstations: 50, profiles: 50, resourcesAtLeast: 53 },
|
||
|
|
orphanRevisions: orphans.length,
|
||
|
|
semanticSqlUnique: false,
|
||
|
|
productionActivated: false,
|
||
|
|
});
|
||
|
|
} finally {
|
||
|
|
await new Promise<void>((resolve, reject) =>
|
||
|
|
server.close((error) => (error ? reject(error) : resolve()))
|
||
|
|
);
|
||
|
|
runtime.stop();
|
||
|
|
await database.close();
|
||
|
|
}
|
||
|
|
});
|