- Add softwareReferenceHref helper function for generating resource links. - Create useGlobalResourceEditor hook for managing global application resources. - Implement useGlobalResourceForm hook for handling global resource forms. - Introduce GlobalSoftwareCatalogProvider for context management of global applications. - Develop GlobalOperatingSystemsPage to render global operating systems using the existing GlobalApplicationsPage. - Add operating system form identity helpers for managing OS-specific fields. - Create operating system specification labels for better localization. - Implement OperatingSystemIdentityReview component for reviewing OS identities. - Add OperatingSystemProductFields component for displaying OS product details. - Write tests for resource characteristic control to ensure proper translation and value handling. - Create a Python script for auditing global operating systems UI interactions. - Add tests for expanded workstation profiles to ensure proper functionality and responsiveness.
851 lines
30 KiB
TypeScript
851 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 type { ApplicationConfigurationInput } from "@metis/companies/common";
|
|
import {
|
|
ApplicationConfigurationResponseSchema,
|
|
ApplicationConfigurationsResponseSchema,
|
|
CompanyResponseSchema,
|
|
WorkProfileResponseSchema,
|
|
} from "@metis/companies/common";
|
|
import {
|
|
ApplicationReferenceCapabilitiesSchema,
|
|
CatalogResponseSchema,
|
|
GetGlobalApplicationAdminResponseSchema,
|
|
GlobalApplicationTemplateResponseSchema,
|
|
InventoryResponseSchema,
|
|
ListApplicationCandidatesResponseSchema,
|
|
ResourceResponseSchema,
|
|
} from "@metis/resources/common";
|
|
import { migrateResourceScope } from "@metis/resources/module";
|
|
import { QueryTypes } from "sequelize";
|
|
|
|
import { UniqueID } from "../../../packages/rdx-ddd/src/index";
|
|
|
|
import { createApp } from "./app";
|
|
import { prepareMembershipFixture } from "./asset-directory-memberships.fixture";
|
|
import { createDatabase, readDatabaseEnvironment } from "./config/database";
|
|
import { createIdentityRuntime } from "./infrastructure/http/identity-runtime";
|
|
import { disposableScopeEnvironment, writeScopeEvidence } from "./resource-scope.preflight";
|
|
import { scopeApiClient } from "./resource-scope-api.client";
|
|
import { latch, waitBarrier, waitCatalogLock } from "./resource-scope-concurrency.fixture";
|
|
import { scopeHttpFixture, seedScopeVolume } from "./resource-scope-http.fixture";
|
|
|
|
test("R2.5: configuraciones globales locales al perfil, HTTP y concurrencia MariaDB", async (t) => {
|
|
const database = createDatabase(disposableScopeEnvironment());
|
|
const owners = Array.from({ length: 15 }, () => UniqueID.generateNewID().toPrimitive());
|
|
const company = owners[0];
|
|
const other = owners[1];
|
|
|
|
assert.ok(company && other);
|
|
const actor = UniqueID.generateNewID().toPrimitive();
|
|
const fixture = await prepareMembershipFixture(database, "http://localhost:5173", actor, owners);
|
|
|
|
await migrateResourceScope(database);
|
|
const seedHttp = await scopeHttpFixture(database, fixture);
|
|
const volume = await seedScopeVolume(fixture, seedHttp, company);
|
|
|
|
await seedHttp.close();
|
|
const a = volume.profileIds[0];
|
|
const a2 = volume.profileIds[1];
|
|
const host = volume.assetIds[0];
|
|
|
|
assert.ok(a && a2 && host);
|
|
const foreignHost = await fixture.asset(other, "laptop");
|
|
let runtime = await createIdentityRuntime(
|
|
database,
|
|
readDatabaseEnvironment({
|
|
...process.env,
|
|
RESOURCE_GLOBAL_CATALOG_ENABLED: "true",
|
|
RESOURCE_GLOBAL_SOFTWARE_ENABLED: "false",
|
|
RESOURCE_GLOBAL_SOFTWARE_CANDIDATES_ENABLED: "false",
|
|
RESOURCE_GLOBAL_PROFILE_ENABLED: "true",
|
|
})
|
|
);
|
|
let server = createApp(() => undefined, runtime).listen(0, "127.0.0.1");
|
|
|
|
await once(server, "listening");
|
|
const address = server.address();
|
|
|
|
assert.ok(address && typeof address !== "string");
|
|
const base = `http://127.0.0.1:${address.port}`;
|
|
const admin = await scopeApiClient(base);
|
|
const ordinary = await scopeApiClient(base);
|
|
const password = randomBytes(24).toString("hex");
|
|
|
|
try {
|
|
assert.equal(
|
|
(
|
|
await admin.request("/auth/bootstrap", {
|
|
name: "Preparación R2.5",
|
|
email: "r25-admin@example.invalid",
|
|
password,
|
|
})
|
|
).status,
|
|
201
|
|
);
|
|
await admin.refresh();
|
|
assert.equal(
|
|
(
|
|
await admin.request("/users", {
|
|
name: "Operador R2.5",
|
|
email: "r25-user@example.invalid",
|
|
password,
|
|
})
|
|
).status,
|
|
201
|
|
);
|
|
assert.equal(
|
|
(await ordinary.request("/auth/login", { email: "r25-user@example.invalid", password }))
|
|
.status,
|
|
200
|
|
);
|
|
await ordinary.refresh();
|
|
const specification = GlobalApplicationTemplateResponseSchema.parse(
|
|
await (await admin.request("/resource-global-applications/template")).json()
|
|
).specification;
|
|
|
|
const product = async (name: string, active = true) => {
|
|
const response = await admin.request("/resource-global-applications", {
|
|
creationId: UniqueID.generateNewID().toPrimitive(),
|
|
values: {
|
|
name,
|
|
templateId: specification.templateId,
|
|
active,
|
|
notes: "Gestión privada R2.5",
|
|
characteristics: {},
|
|
},
|
|
});
|
|
|
|
assert.equal(response.status, 201, await response.clone().text());
|
|
|
|
return GetGlobalApplicationAdminResponseSchema.parse(await response.json()).resource;
|
|
};
|
|
|
|
const global = await product("Ágil compartida R2.5");
|
|
const inactive = await product("Inactiva R2.5", false);
|
|
const catalog = CatalogResponseSchema.parse(
|
|
await (await ordinary.request(`/resource-catalog/${company}`)).json()
|
|
);
|
|
|
|
const template = (key: string) => {
|
|
const found = catalog.items.find((item) => item.builtinKey === key);
|
|
|
|
assert.ok(found);
|
|
|
|
return found;
|
|
};
|
|
|
|
const local = async (owner: string, templateId: string, name: string) => {
|
|
const response = await ordinary.request(`/resources/${owner}`, {
|
|
values: { name, templateId, active: true, notes: "", characteristics: {} },
|
|
});
|
|
|
|
assert.equal(response.status, 201);
|
|
|
|
return ResourceResponseSchema.parse(await response.json()).resource.id;
|
|
};
|
|
|
|
const own = await local(company, specification.templateId, "Ágil particular");
|
|
const foreign = await local(other, specification.templateId, "Ágil ajena");
|
|
const os = await local(company, template("template:os").id, "SO no configurable");
|
|
const license = await local(
|
|
company,
|
|
template("template:software_license").id,
|
|
"Licencia no configurable"
|
|
);
|
|
const certificate = await local(
|
|
company,
|
|
template("template:certificate").id,
|
|
"Certificado no configurable"
|
|
);
|
|
const locals: string[] = [];
|
|
|
|
for (let index = 0; index < 50; index++) {
|
|
locals.push(await local(company, specification.templateId, `Ágil volumen ${index + 1}`));
|
|
}
|
|
const personResult = await ordinary.request(`/companies/${other}/people`, {
|
|
name: "Persona de B",
|
|
active: true,
|
|
isContact: false,
|
|
position: null,
|
|
email: null,
|
|
phone: null,
|
|
});
|
|
const person = CompanyResponseSchema.parse(await personResult.json()).company.people.at(-1);
|
|
|
|
assert.ok(person);
|
|
const otherWorkstation = await ordinary.request(`/assets/${other}/workstations`, {
|
|
values: {
|
|
name: "Puesto B R2.5",
|
|
active: true,
|
|
siteId: null,
|
|
personId: person.id,
|
|
computerAssetId: foreignHost.id.toPrimitive(),
|
|
assetIds: [foreignHost.id.toPrimitive()],
|
|
},
|
|
});
|
|
|
|
assert.equal(otherWorkstation.status, 201);
|
|
const profileResult = await ordinary.request(`/work-profiles/${other}`, {
|
|
values: {
|
|
name: "Perfil de B",
|
|
personId: person.id,
|
|
assetId: foreignHost.id.toPrimitive(),
|
|
accountId: null,
|
|
active: true,
|
|
notes: null,
|
|
},
|
|
});
|
|
|
|
assert.equal(profileResult.status, 201, await profileResult.clone().text());
|
|
const b = WorkProfileResponseSchema.parse(await profileResult.json()).profile.id;
|
|
const path = `/application-configurations/${company}`;
|
|
const input: ApplicationConfigurationInput = {
|
|
profileId: a,
|
|
resourceId: global.id,
|
|
delivery: "installed",
|
|
version: "123.4",
|
|
state: "pending",
|
|
notes: "Configuración A",
|
|
};
|
|
|
|
const save = async (values: ApplicationConfigurationInput, owner = company) => {
|
|
const response = await ordinary.request(`/application-configurations/${owner}`, { values });
|
|
|
|
assert.equal(response.status, 201, await response.clone().text());
|
|
|
|
return ApplicationConfigurationResponseSchema.parse(await response.json()).configuration;
|
|
};
|
|
|
|
let first = await save(input);
|
|
const races: string[] = [];
|
|
|
|
await t.test(
|
|
"mismo UUID en varios perfiles y compañías, delivery/versión/estado propios, sin instalaciones",
|
|
async () => {
|
|
const second = await save({
|
|
...input,
|
|
profileId: a2,
|
|
delivery: "web",
|
|
version: null,
|
|
state: "configured",
|
|
notes: "Perfil A2",
|
|
});
|
|
const third = await save(
|
|
{ ...input, profileId: b, version: "B-9", state: "configured", notes: "Compañía B" },
|
|
other
|
|
);
|
|
|
|
assert.notEqual(first.id, second.id);
|
|
assert.notEqual(second.id, third.id);
|
|
assert.equal(second.values.version, null);
|
|
assert.equal(second.values.delivery, "web");
|
|
assert.equal(third.companyId, other);
|
|
assert.equal(third.values.version, "B-9");
|
|
assert.equal(first.values.version, "123.4");
|
|
const rows = await database.query("SELECT id FROM resources WHERE id=:id", {
|
|
replacements: { id: global.id },
|
|
type: QueryTypes.SELECT,
|
|
});
|
|
|
|
assert.equal(rows.length, 1);
|
|
assert.equal(
|
|
(
|
|
await database.query("SELECT id FROM software_installations", {
|
|
type: QueryTypes.SELECT,
|
|
})
|
|
).length,
|
|
0
|
|
);
|
|
const inventory = InventoryResponseSchema.parse(
|
|
await (await ordinary.request(`/resources/${company}`)).json()
|
|
);
|
|
|
|
assert.ok(inventory.resources.every((item) => item.id !== global.id));
|
|
}
|
|
);
|
|
await t.test(
|
|
"matriz negativa, recurso/perfil inmutables, autorización, CSRF y contrato estricto",
|
|
async () => {
|
|
for (const [profileId, resourceId, status] of [
|
|
[a, foreign, 404],
|
|
[b, own, 404],
|
|
[a, inactive.id, 409],
|
|
[a, os, 404],
|
|
[a, license, 404],
|
|
[a, certificate, 404],
|
|
[a, UniqueID.generateNewID().toPrimitive(), 404],
|
|
] as const) {
|
|
assert.equal(
|
|
(await admin.request(path, { values: { ...input, profileId, resourceId } })).status,
|
|
status
|
|
);
|
|
}
|
|
for (const values of [
|
|
{ ...input, profileId: a2 },
|
|
{ ...input, resourceId: own },
|
|
]) {
|
|
assert.equal(
|
|
(await ordinary.request(`${path}/${first.id}/update`, { values })).status,
|
|
409
|
|
);
|
|
}
|
|
assert.equal(
|
|
(
|
|
await ordinary.request(
|
|
path,
|
|
{ values: { ...input, resourceId: own } },
|
|
{ token: "invalid" }
|
|
)
|
|
).status,
|
|
403
|
|
);
|
|
assert.equal(
|
|
(
|
|
await ordinary.request(
|
|
path,
|
|
{ values: { ...input, resourceId: own } },
|
|
{ origin: "https://invalid.example" }
|
|
)
|
|
).status,
|
|
403
|
|
);
|
|
const anonymous = await scopeApiClient(base);
|
|
|
|
assert.equal((await anonymous.request(path)).status, 401);
|
|
assert.equal((await anonymous.request(path, { values: input })).status, 401);
|
|
assert.equal(
|
|
(await ordinary.request(path, { values: { ...input, scope: "global" } })).status,
|
|
400
|
|
);
|
|
assert.equal(
|
|
(
|
|
await ordinary.request(path, {
|
|
values: { ...input, delivery: "web", version: "no admitida" },
|
|
})
|
|
).status,
|
|
400
|
|
);
|
|
assert.equal(
|
|
(
|
|
await ordinary.request(`/work-profiles/${company}`, {
|
|
values: {
|
|
name: "Activo ajeno",
|
|
personId: person.id,
|
|
assetId: foreignHost.id.toPrimitive(),
|
|
accountId: null,
|
|
active: true,
|
|
notes: null,
|
|
},
|
|
})
|
|
).status,
|
|
404
|
|
);
|
|
}
|
|
);
|
|
await t.test(
|
|
"locales de serie y plantilla propia application, legacy y negociación global v2",
|
|
async () => {
|
|
const created = await ordinary.request(`/resource-catalog/${company}`, {
|
|
values: {
|
|
...template("template:application").values,
|
|
name: "Aplicación propia R2.5",
|
|
usage: "application",
|
|
},
|
|
});
|
|
|
|
assert.equal(created.status, 201);
|
|
const custom = CatalogResponseSchema.parse(
|
|
await (await ordinary.request(`/resource-catalog/${company}`)).json()
|
|
).items.find((item) => item.values.name === "Aplicación propia R2.5");
|
|
|
|
assert.ok(custom);
|
|
const customId = await local(company, custom.id, "Aplicación propia elegible");
|
|
|
|
assert.equal(
|
|
(
|
|
await ordinary.request(
|
|
path,
|
|
{ values: { ...input, resourceId: own } },
|
|
{ contract: null }
|
|
)
|
|
).status,
|
|
201
|
|
);
|
|
assert.equal(
|
|
(
|
|
await ordinary.request(
|
|
path,
|
|
{ values: { ...input, resourceId: customId } },
|
|
{ contract: null }
|
|
)
|
|
).status,
|
|
201
|
|
);
|
|
assert.equal((await ordinary.request(path, undefined, { contract: null })).status, 409);
|
|
assert.equal((await ordinary.request(path, undefined, { contract: "3" })).status, 200);
|
|
assert.equal((await ordinary.request(path, undefined, { contract: "4" })).status, 400);
|
|
assert.equal(
|
|
(
|
|
await ordinary.request(
|
|
`${path}/${first.id}/update`,
|
|
{ values: first.values },
|
|
{ contract: null }
|
|
)
|
|
).status,
|
|
409
|
|
);
|
|
assert.equal(
|
|
(await ordinary.request(`${path}/${first.id}/delete`, {}, { contract: null })).status,
|
|
409
|
|
);
|
|
assert.equal(
|
|
(
|
|
await ordinary.request(
|
|
path,
|
|
{
|
|
values: {
|
|
...input,
|
|
profileId: volume.profileIds[2],
|
|
resourceId: global.id.toUpperCase(),
|
|
},
|
|
},
|
|
{ contract: null }
|
|
)
|
|
).status,
|
|
409
|
|
);
|
|
assert.equal(
|
|
(
|
|
await ordinary.request(`${path}/${first.id.toUpperCase()}/update`, {
|
|
values: { ...first.values, resourceId: global.id.toUpperCase() },
|
|
})
|
|
).status,
|
|
200
|
|
);
|
|
}
|
|
);
|
|
await t.test(
|
|
"candidatos de perfil mixtos sin SO/licencias, proyección privada y cursor por propósito",
|
|
async () => {
|
|
for (let index = 0; index < 25; index++) {
|
|
await product(`Ágil global de perfil ${index}`, index % 5 !== 0);
|
|
}
|
|
let cursor: string | null = null;
|
|
const ids = new Set<string>();
|
|
const scopes = new Set<string>();
|
|
|
|
do {
|
|
const response = await ordinary.request(
|
|
`/resource-application-candidates/${company}?purpose=application-configuration&search=agil&pageSize=7${cursor ? `&cursor=${encodeURIComponent(cursor)}` : ""}`
|
|
);
|
|
|
|
assert.equal(response.status, 200);
|
|
const page = ListApplicationCandidatesResponseSchema.parse(await response.json());
|
|
|
|
assert.equal(page.purpose, "application-configuration");
|
|
for (const item of page.items) {
|
|
assert.equal(item.kind, "application");
|
|
assert.equal(item.active, true);
|
|
assert.equal(ids.has(item.id), false);
|
|
ids.add(item.id);
|
|
scopes.add(item.scope);
|
|
for (const key of [
|
|
"notes",
|
|
"history",
|
|
"actorId",
|
|
"profiles",
|
|
"installations",
|
|
"characteristics",
|
|
]) {
|
|
assert.equal(Object.hasOwn(item, key), false);
|
|
}
|
|
}
|
|
cursor = page.nextCursor;
|
|
} while (cursor);
|
|
assert.deepEqual([...scopes].sort(), ["company", "global"]);
|
|
assert.equal(ids.size, 72);
|
|
const caps = ApplicationReferenceCapabilitiesSchema.parse(
|
|
await (
|
|
await ordinary.request(`/resource-application-candidates/${company}/capabilities`)
|
|
).json()
|
|
);
|
|
|
|
assert.equal(caps.profileAssociation, true);
|
|
assert.equal(caps.softwareAssociation, false);
|
|
assert.equal(caps.softwareSelection, false);
|
|
assert.equal(
|
|
(
|
|
await ordinary.request(`/software-installations/${company}`, {
|
|
values: {
|
|
assetId: host,
|
|
resourceId: global.id,
|
|
licenseResourceId: null,
|
|
version: null,
|
|
state: "pending",
|
|
notes: null,
|
|
},
|
|
})
|
|
).status,
|
|
409
|
|
);
|
|
}
|
|
);
|
|
await t.test(
|
|
"edición semántica, historial y UUID preservados tras renombrar/desactivar; notas y retirada",
|
|
async () => {
|
|
const snapshot = first.history;
|
|
const unchanged = ApplicationConfigurationResponseSchema.parse(
|
|
await (
|
|
await ordinary.request(`${path}/${first.id}/update`, { values: first.values })
|
|
).json()
|
|
).configuration;
|
|
|
|
assert.deepEqual(unchanged.history, first.history);
|
|
const updated = await ordinary.request(`${path}/${first.id}/update`, {
|
|
values: { ...input, version: "124", state: "configured" },
|
|
});
|
|
|
|
assert.equal(updated.status, 200);
|
|
first = ApplicationConfigurationResponseSchema.parse(await updated.json()).configuration;
|
|
assert.equal(first.history.length, 2);
|
|
assert.deepEqual(first.history.slice(0, 1), snapshot);
|
|
assert.equal(
|
|
(
|
|
await admin.request(`/resource-global-applications/${global.id}/update`, {
|
|
values: { ...global.values, name: "Ágil renombrada R2.5" },
|
|
})
|
|
).status,
|
|
200
|
|
);
|
|
assert.equal(
|
|
(
|
|
await admin.request(`/resource-global-applications/${global.id}/status`, {
|
|
active: false,
|
|
})
|
|
).status,
|
|
200
|
|
);
|
|
const before = ApplicationConfigurationsResponseSchema.parse(
|
|
await (await ordinary.request(path)).json()
|
|
).configurations.find((item) => item.id === first.id);
|
|
|
|
assert.ok(before);
|
|
assert.equal(before.needsReview, true);
|
|
assert.deepEqual(before.history, first.history);
|
|
assert.equal(
|
|
(
|
|
await ordinary.request(`${path}/${first.id}/update`, {
|
|
values: { ...first.values, version: "125" },
|
|
})
|
|
).status,
|
|
409
|
|
);
|
|
const notes = await ordinary.request(`${path}/${first.id}/update`, {
|
|
values: { ...first.values, notes: "Revisión documentada" },
|
|
});
|
|
|
|
assert.equal(notes.status, 200);
|
|
first = ApplicationConfigurationResponseSchema.parse(await notes.json()).configuration;
|
|
const retirement = await ordinary.request(`${path}/${first.id}/update`, {
|
|
values: { ...first.values, state: "removal_pending" },
|
|
});
|
|
|
|
assert.equal(retirement.status, 200);
|
|
first = ApplicationConfigurationResponseSchema.parse(await retirement.json()).configuration;
|
|
assert.equal(first.values.resourceId, global.id);
|
|
assert.equal(first.values.version, "124");
|
|
const otherConfigurations = ApplicationConfigurationsResponseSchema.parse(
|
|
await (await ordinary.request(`/application-configurations/${other}`)).json()
|
|
);
|
|
|
|
assert.equal(otherConfigurations.configurations[0]?.history.length, 1);
|
|
const resolver = await ordinary.request(
|
|
`/resource-application-references/${company}/resolve`,
|
|
{ purpose: "application-configuration", ids: [global.id] }
|
|
);
|
|
|
|
assert.equal(resolver.status, 200);
|
|
assert.match(await resolver.text(), /Ágil renombrada/);
|
|
}
|
|
);
|
|
await t.test("carrera: configuración primero, desactivación espera el commit", async () => {
|
|
const target = await product("Carrera perfil primero");
|
|
const arrived = latch();
|
|
const release = latch();
|
|
|
|
database.addHook("afterQuery", "r25-create", async (_options, query) => {
|
|
const sql: unknown = Reflect.get(query, "sql");
|
|
|
|
if (typeof sql === "string" && sql.startsWith("INSERT INTO application_configurations")) {
|
|
arrived.release();
|
|
await release.promise;
|
|
}
|
|
});
|
|
const configuring = ordinary.request(path, { values: { ...input, resourceId: target.id } });
|
|
let disabling: Promise<Response> | undefined;
|
|
|
|
try {
|
|
await waitBarrier(arrived.promise);
|
|
disabling = admin.request(`/resource-global-applications/${target.id}/status`, {
|
|
active: false,
|
|
});
|
|
await waitCatalogLock(process.env.METIS_RESOURCE_SCOPE_DISPOSABLE ?? "");
|
|
release.release();
|
|
assert.equal((await configuring).status, 201);
|
|
assert.equal((await disabling).status, 200);
|
|
const read = ApplicationConfigurationsResponseSchema.parse(
|
|
await (await ordinary.request(path)).json()
|
|
).configurations.find((item) => item.values.resourceId === target.id);
|
|
|
|
assert.ok(read);
|
|
assert.equal(read.needsReview, true);
|
|
races.push("configuration-before-deactivation");
|
|
} finally {
|
|
release.release();
|
|
database.removeHook("afterQuery", "r25-create");
|
|
await Promise.allSettled([configuring, ...(disabling ? [disabling] : [])]);
|
|
}
|
|
});
|
|
await t.test(
|
|
"carrera: desactivación primero, revalidación posterior rechaza sin crear revisión",
|
|
async () => {
|
|
const target = await product("Carrera desactivación perfil");
|
|
const arrived = latch();
|
|
const release = latch();
|
|
|
|
database.addHook("afterQuery", "r25-status", 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(`/resource-global-applications/${target.id}/status`, {
|
|
active: false,
|
|
});
|
|
let configuring: Promise<Response> | undefined;
|
|
|
|
try {
|
|
await waitBarrier(arrived.promise);
|
|
configuring = ordinary.request(path, { values: { ...input, resourceId: target.id } });
|
|
await waitCatalogLock(process.env.METIS_RESOURCE_SCOPE_DISPOSABLE ?? "");
|
|
release.release();
|
|
assert.equal((await disabling).status, 200);
|
|
assert.equal((await configuring).status, 409);
|
|
assert.equal(
|
|
(
|
|
await database.query(
|
|
"SELECT id FROM application_configurations WHERE resourceId=:id",
|
|
{ replacements: { id: target.id }, type: QueryTypes.SELECT }
|
|
)
|
|
).length,
|
|
0
|
|
);
|
|
assert.equal(
|
|
(
|
|
await database.query(
|
|
"SELECT id FROM application_configuration_revisions WHERE resourceId=:id",
|
|
{ replacements: { id: target.id }, type: QueryTypes.SELECT }
|
|
)
|
|
).length,
|
|
0
|
|
);
|
|
races.push("deactivation-before-configuration");
|
|
} finally {
|
|
release.release();
|
|
database.removeHook("afterQuery", "r25-status");
|
|
await Promise.allSettled([disabling, ...(configuring ? [configuring] : [])]);
|
|
}
|
|
}
|
|
);
|
|
await t.test(
|
|
"concurrencia de la pareja: un único registro abierto y revisiones propias",
|
|
async () => {
|
|
const target = await product("Concurrencia pareja de perfil");
|
|
const results = await Promise.all([
|
|
ordinary.request(path, { values: { ...input, resourceId: target.id } }),
|
|
ordinary.request(path, { values: { ...input, resourceId: target.id } }),
|
|
]);
|
|
|
|
assert.deepEqual(results.map((result) => result.status).sort(), [201, 409]);
|
|
assert.equal(
|
|
(
|
|
await database.query(
|
|
"SELECT id FROM application_configurations WHERE profileId=:profileId AND resourceId=:resourceId",
|
|
{ replacements: { profileId: a, resourceId: target.id }, type: QueryTypes.SELECT }
|
|
)
|
|
).length,
|
|
1
|
|
);
|
|
}
|
|
);
|
|
await t.test(
|
|
"volumen de cincuenta configuraciones preserva aislamiento por perfil",
|
|
async () => {
|
|
for (let index = 0; index < 50; index++) {
|
|
const id = volume.profileIds[index];
|
|
const resourceId = locals[index];
|
|
|
|
assert.ok(id && resourceId);
|
|
await save({ ...input, profileId: id, resourceId, version: `Perfil-${index + 1}` });
|
|
}
|
|
const listed = ApplicationConfigurationsResponseSchema.parse(
|
|
await (await ordinary.request(path)).json()
|
|
);
|
|
|
|
assert.ok(listed.configurations.length >= 50);
|
|
assert.ok(listed.configurations.every((item) => item.companyId === company));
|
|
}
|
|
);
|
|
await t.test(
|
|
"gates independientes y apagado: lectores, notas, retirada y locales siguen funcionando",
|
|
async () => {
|
|
server.close();
|
|
await once(server, "close");
|
|
runtime.stop();
|
|
runtime = await createIdentityRuntime(
|
|
database,
|
|
readDatabaseEnvironment({
|
|
...process.env,
|
|
RESOURCE_GLOBAL_CATALOG_ENABLED: "true",
|
|
RESOURCE_GLOBAL_SOFTWARE_ENABLED: "true",
|
|
RESOURCE_GLOBAL_SOFTWARE_CANDIDATES_ENABLED: "true",
|
|
RESOURCE_GLOBAL_PROFILE_ENABLED: "false",
|
|
})
|
|
);
|
|
server = createApp(() => undefined, runtime).listen(address.port, "127.0.0.1");
|
|
await once(server, "listening");
|
|
const caps = ApplicationReferenceCapabilitiesSchema.parse(
|
|
await (
|
|
await ordinary.request(`/resource-application-candidates/${company}/capabilities`)
|
|
).json()
|
|
);
|
|
|
|
assert.equal(caps.profileAssociation, false);
|
|
assert.equal(caps.softwareAssociation, true);
|
|
const target = await product("Gates independientes R2.5");
|
|
|
|
assert.equal(
|
|
(await ordinary.request(path, { values: { ...input, resourceId: target.id } })).status,
|
|
409
|
|
);
|
|
assert.equal(
|
|
(
|
|
await ordinary.request(`/software-installations/${company}`, {
|
|
values: {
|
|
assetId: host,
|
|
resourceId: target.id,
|
|
licenseResourceId: null,
|
|
version: "R2.4",
|
|
state: "configured",
|
|
notes: null,
|
|
},
|
|
})
|
|
).status,
|
|
201
|
|
);
|
|
const read = ApplicationConfigurationsResponseSchema.parse(
|
|
await (await ordinary.request(path)).json()
|
|
).configurations.find((item) => item.id === first.id);
|
|
|
|
assert.ok(read);
|
|
assert.equal(read.values.resourceId, global.id);
|
|
assert.equal(
|
|
(
|
|
await ordinary.request(`${path}/${first.id}/update`, {
|
|
values: { ...read.values, notes: "Notas tras apagado" },
|
|
})
|
|
).status,
|
|
200
|
|
);
|
|
assert.equal(
|
|
(
|
|
await ordinary.request(`${path}/${first.id}/update`, {
|
|
values: { ...read.values, state: "removed" },
|
|
})
|
|
).status,
|
|
200
|
|
);
|
|
const candidates = ListApplicationCandidatesResponseSchema.parse(
|
|
await (
|
|
await ordinary.request(
|
|
`/resource-application-candidates/${company}?purpose=application-configuration`
|
|
)
|
|
).json()
|
|
);
|
|
|
|
assert.ok(candidates.items.every((item) => item.scope === "company"));
|
|
assert.equal(
|
|
(
|
|
await ordinary.request(
|
|
path,
|
|
{ values: { ...input, profileId: a2, resourceId: own } },
|
|
{ contract: null }
|
|
)
|
|
).status,
|
|
201
|
|
);
|
|
assert.equal((await ordinary.request(`${path}/${first.id}/delete`, {})).status, 200);
|
|
server.close();
|
|
await once(server, "close");
|
|
runtime.stop();
|
|
runtime = await createIdentityRuntime(
|
|
database,
|
|
readDatabaseEnvironment({
|
|
...process.env,
|
|
RESOURCE_GLOBAL_CATALOG_ENABLED: "false",
|
|
RESOURCE_GLOBAL_SOFTWARE_ENABLED: "true",
|
|
RESOURCE_GLOBAL_PROFILE_ENABLED: "true",
|
|
})
|
|
);
|
|
server = createApp(() => undefined, runtime).listen(address.port, "127.0.0.1");
|
|
await once(server, "listening");
|
|
const closed = ApplicationReferenceCapabilitiesSchema.parse(
|
|
await (
|
|
await ordinary.request(`/resource-application-candidates/${company}/capabilities`)
|
|
).json()
|
|
);
|
|
|
|
assert.equal(closed.profileAssociation, false);
|
|
assert.equal(closed.softwareAssociation, false);
|
|
assert.equal((await ordinary.request(path)).status, 200);
|
|
}
|
|
);
|
|
await writeScopeEvidence("profile-results.json", {
|
|
companies: 15,
|
|
principal: {
|
|
people: 50,
|
|
assets: 50,
|
|
profiles: 50,
|
|
workstations: 50,
|
|
localResourcesAtLeast: 50,
|
|
configurationsAtLeast: 50,
|
|
},
|
|
races,
|
|
softwareAndProfileIndependent: true,
|
|
migrationsAdded: false,
|
|
});
|
|
await writeScopeEvidence(
|
|
"profile-foreign-keys.json",
|
|
await database.query(
|
|
"SELECT TABLE_NAME,COLUMN_NAME,REFERENCED_TABLE_NAME,REFERENCED_COLUMN_NAME FROM information_schema.KEY_COLUMN_USAGE WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME IN ('application_configurations','application_configuration_revisions') AND REFERENCED_TABLE_NAME IS NOT NULL ORDER BY TABLE_NAME,COLUMN_NAME",
|
|
{ type: QueryTypes.SELECT }
|
|
)
|
|
);
|
|
} finally {
|
|
server.close();
|
|
await once(server, "close");
|
|
runtime.stop();
|
|
await database.close();
|
|
}
|
|
});
|