2026-10-12 18:31:21 +00:00
|
|
|
import assert from "node:assert/strict";
|
|
|
|
|
|
|
|
|
|
import { InventoryResponseSchema } from "@metis/assets/common";
|
2026-10-13 07:30:28 +00:00
|
|
|
import type { SoftwareInstallationInput } from "@metis/companies/common";
|
2026-10-12 18:31:21 +00:00
|
|
|
import {
|
|
|
|
|
SoftwareInstallationResponseSchema,
|
|
|
|
|
SoftwareInstallationsResponseSchema,
|
|
|
|
|
} from "@metis/companies/common";
|
|
|
|
|
import { CatalogResponseSchema, ResourceResponseSchema } from "@metis/resources/common";
|
|
|
|
|
import type { Sequelize } from "sequelize";
|
2026-10-13 07:30:28 +00:00
|
|
|
import { z } from "zod/v4";
|
2026-10-12 18:31:21 +00:00
|
|
|
|
|
|
|
|
import { migrateSoftwareInstallations } from "../../../modules/companies/src/api/infrastructure/persistence/software-installation-migration";
|
|
|
|
|
|
|
|
|
|
type Request = (path: string, body?: unknown, origin?: string, token?: string) => Promise<Response>;
|
|
|
|
|
|
|
|
|
|
export async function verifySoftwareInstallations(
|
|
|
|
|
request: Request,
|
|
|
|
|
companyId: string,
|
|
|
|
|
otherId: string,
|
|
|
|
|
database: Sequelize
|
|
|
|
|
) {
|
|
|
|
|
await migrateSoftwareInstallations(database);
|
|
|
|
|
const inventory = InventoryResponseSchema.parse(
|
|
|
|
|
await (await request(`/assets/${companyId}`)).json()
|
|
|
|
|
);
|
|
|
|
|
const host = inventory.assets.find((asset) => asset.values.status !== "retired");
|
|
|
|
|
const otherHost = InventoryResponseSchema.parse(
|
|
|
|
|
await (await request(`/assets/${otherId}`)).json()
|
|
|
|
|
).assets[0];
|
|
|
|
|
const catalog = CatalogResponseSchema.parse(
|
|
|
|
|
await (await request(`/resource-catalog/${companyId}`)).json()
|
|
|
|
|
);
|
|
|
|
|
const template = catalog.items.find((item) => item.builtinKey === "template:os");
|
|
|
|
|
const application = catalog.items.find((item) => item.builtinKey === "template:application");
|
2026-10-13 07:30:28 +00:00
|
|
|
const licenseTemplate = catalog.items.find(
|
|
|
|
|
(item) => item.builtinKey === "template:software_license"
|
|
|
|
|
);
|
2026-10-12 18:31:21 +00:00
|
|
|
const certificate = catalog.items.find((item) => item.builtinKey === "template:certificate");
|
|
|
|
|
|
2026-10-13 07:30:28 +00:00
|
|
|
assert.ok(host && otherHost && template && application && certificate && licenseTemplate);
|
2026-10-12 18:31:21 +00:00
|
|
|
assert.ok(template.values.kind === "template" && template.values.usage === "os");
|
2026-10-13 07:30:28 +00:00
|
|
|
assert.ok(
|
|
|
|
|
licenseTemplate.values.kind === "template" && licenseTemplate.values.usage === "license"
|
|
|
|
|
);
|
2026-10-12 18:31:21 +00:00
|
|
|
|
|
|
|
|
const addResource = async (templateId: string, name: string) => {
|
|
|
|
|
const response = await request(`/resources/${companyId}`, {
|
|
|
|
|
values: { name, templateId, active: true, notes: "", characteristics: {} },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
assert.equal(response.status, 201, await response.clone().text());
|
|
|
|
|
|
|
|
|
|
return ResourceResponseSchema.parse(await response.json()).resource;
|
|
|
|
|
};
|
|
|
|
|
|
2026-10-13 07:30:28 +00:00
|
|
|
const license = await addResource(licenseTemplate.id, "Licencia equipo test");
|
2026-10-12 18:31:21 +00:00
|
|
|
const os = await addResource(template.id, "Windows 11 Pro test");
|
|
|
|
|
const app = await addResource(application.id, "Office equipo test");
|
|
|
|
|
const cert = await addResource(certificate.id, "Certificado no instalable como software");
|
|
|
|
|
const path = `/software-installations/${companyId}`;
|
2026-10-13 07:30:28 +00:00
|
|
|
const values: SoftwareInstallationInput = {
|
2026-10-12 18:31:21 +00:00
|
|
|
assetId: host.id,
|
|
|
|
|
resourceId: os.id,
|
2026-10-13 07:30:28 +00:00
|
|
|
licenseResourceId: null,
|
2026-10-12 18:31:21 +00:00
|
|
|
version: "24H2",
|
|
|
|
|
state: "configured",
|
|
|
|
|
notes: null,
|
|
|
|
|
};
|
|
|
|
|
const foreign = await request(path, { values: { ...values, assetId: otherHost.id } });
|
|
|
|
|
|
|
|
|
|
assert.equal(foreign.status, 404);
|
|
|
|
|
assert.equal((await request(path, { values: { ...values, resourceId: cert.id } })).status, 404);
|
2026-10-13 07:30:28 +00:00
|
|
|
assert.equal(
|
|
|
|
|
(await request(path, { values: { ...values, licenseResourceId: cert.id } })).status,
|
|
|
|
|
404
|
|
|
|
|
);
|
|
|
|
|
const foreignLicenseResponse = await request(`/resources/${otherId}`, {
|
|
|
|
|
values: {
|
|
|
|
|
name: "Licencia ajena",
|
|
|
|
|
templateId: licenseTemplate.id,
|
|
|
|
|
active: true,
|
|
|
|
|
notes: "",
|
|
|
|
|
characteristics: {},
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
assert.equal(foreignLicenseResponse.status, 201);
|
|
|
|
|
const foreignLicense = ResourceResponseSchema.parse(await foreignLicenseResponse.json()).resource;
|
|
|
|
|
|
|
|
|
|
assert.equal(
|
|
|
|
|
(await request(path, { values: { ...values, licenseResourceId: foreignLicense.id } })).status,
|
|
|
|
|
404
|
|
|
|
|
);
|
2026-10-12 18:31:21 +00:00
|
|
|
assert.equal((await request(path, { values: { ...values, profileId: host.id } })).status, 400);
|
|
|
|
|
assert.equal((await request(path, { values }, "https://other.example")).status, 403);
|
|
|
|
|
assert.equal((await request(path, { values }, undefined, "a".repeat(64))).status, 403);
|
|
|
|
|
const responses = await Promise.all([request(path, { values }), request(path, { values })]);
|
|
|
|
|
|
|
|
|
|
assert.deepEqual(responses.map((response) => response.status).sort(), [201, 409]);
|
|
|
|
|
const success = responses.find((response) => response.status === 201);
|
|
|
|
|
|
|
|
|
|
assert.ok(success);
|
|
|
|
|
const created = SoftwareInstallationResponseSchema.parse(await success.json()).configuration;
|
|
|
|
|
|
|
|
|
|
const save = async (data: typeof values) => {
|
|
|
|
|
const response = await request(`${path}/${created.id}/update`, { values: data });
|
|
|
|
|
|
|
|
|
|
assert.equal(response.status, 200, await response.clone().text());
|
|
|
|
|
|
|
|
|
|
return SoftwareInstallationResponseSchema.parse(await response.json()).configuration;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
assert.deepEqual(await save(values), created);
|
|
|
|
|
const updated = await save({ ...values, version: "25H2" });
|
|
|
|
|
|
|
|
|
|
assert.equal(updated.history.length, 2);
|
|
|
|
|
assert.equal(updated.history[0]?.values.version, "24H2");
|
|
|
|
|
assert.equal(updated.history[1]?.values.version, "25H2");
|
2026-10-13 07:30:28 +00:00
|
|
|
const linkedValues = { ...values, version: "25H2", licenseResourceId: license.id };
|
|
|
|
|
const linked = await save(linkedValues);
|
|
|
|
|
|
|
|
|
|
assert.equal(linked.history.length, 3);
|
|
|
|
|
assert.equal(linked.history[0]?.values.licenseResourceId, null);
|
|
|
|
|
assert.equal(linked.history[2]?.values.licenseResourceId, license.id);
|
|
|
|
|
assert.deepEqual(await save(linkedValues), linked);
|
|
|
|
|
const deactivateLicense = await request(`/resources/${companyId}/${license.id}/update`, {
|
|
|
|
|
values: { ...license.values, active: false },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
assert.equal(deactivateLicense.status, 200);
|
|
|
|
|
const licenseReview = SoftwareInstallationsResponseSchema.parse(
|
|
|
|
|
await (await request(path)).json()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
assert.equal(
|
|
|
|
|
licenseReview.configurations.find((item) => item.id === created.id)?.needsReview,
|
|
|
|
|
true
|
|
|
|
|
);
|
|
|
|
|
assert.equal(
|
|
|
|
|
(
|
|
|
|
|
await request(`${path}/${created.id}/update`, {
|
|
|
|
|
values: { ...linkedValues, version: "26H2" },
|
|
|
|
|
})
|
|
|
|
|
).status,
|
|
|
|
|
409
|
|
|
|
|
);
|
|
|
|
|
await save({ ...linkedValues, notes: "Revisar licencia" });
|
|
|
|
|
const detached = await save({ ...values, version: "25H2" });
|
|
|
|
|
|
|
|
|
|
assert.equal(detached.needsReview, false);
|
|
|
|
|
assert.equal(detached.values.licenseResourceId, null);
|
|
|
|
|
assert.equal(detached.history[2]?.values.licenseResourceId, license.id);
|
2026-10-12 18:31:21 +00:00
|
|
|
assert.equal((await request(path, { values: { ...values, resourceId: app.id } })).status, 201);
|
|
|
|
|
assert.equal(
|
|
|
|
|
(await request(`/software-installations/${otherId}/${created.id}/update`, { values })).status,
|
|
|
|
|
404
|
|
|
|
|
);
|
|
|
|
|
assert.equal(
|
|
|
|
|
SoftwareInstallationsResponseSchema.parse(
|
|
|
|
|
await (await request(`/software-installations/${otherId}`)).json()
|
|
|
|
|
).configurations.length,
|
|
|
|
|
0
|
|
|
|
|
);
|
2026-10-13 07:30:28 +00:00
|
|
|
const expiryField = catalog.items.find((item) => item.builtinKey === "field:expires");
|
|
|
|
|
|
|
|
|
|
assert.ok(expiryField);
|
|
|
|
|
const expiredResponse = await request(`/resources/${companyId}`, {
|
|
|
|
|
values: {
|
|
|
|
|
name: "Licencia caducada",
|
|
|
|
|
templateId: licenseTemplate.id,
|
|
|
|
|
active: true,
|
|
|
|
|
notes: "",
|
|
|
|
|
characteristics: { [expiryField.id]: "2000-01-01" },
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
assert.equal(expiredResponse.status, 201);
|
|
|
|
|
const expired = ResourceResponseSchema.parse(await expiredResponse.json()).resource;
|
|
|
|
|
const rejectedExpiry = await request(`${path}/${created.id}/update`, {
|
|
|
|
|
values: { ...values, version: "25H2", licenseResourceId: expired.id },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
assert.equal(rejectedExpiry.status, 409);
|
|
|
|
|
assert.equal(
|
|
|
|
|
z.object({ code: z.string() }).parse(await rejectedExpiry.json()).code,
|
|
|
|
|
"SOFTWARE_REFERENCE_UNAVAILABLE"
|
|
|
|
|
);
|
|
|
|
|
await migrateSoftwareInstallations(database);
|
|
|
|
|
const persisted = SoftwareInstallationsResponseSchema.parse(
|
|
|
|
|
await (await request(path)).json()
|
|
|
|
|
).configurations.find((item) => item.id === created.id);
|
|
|
|
|
|
|
|
|
|
assert.deepEqual(persisted, detached);
|
2026-10-12 18:31:21 +00:00
|
|
|
const deactivate = await request(`/resources/${companyId}/${os.id}/update`, {
|
|
|
|
|
values: { ...os.values, active: false },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
assert.equal(deactivate.status, 200);
|
|
|
|
|
const listed = SoftwareInstallationsResponseSchema.parse(await (await request(path)).json());
|
|
|
|
|
|
|
|
|
|
assert.equal(listed.configurations.find((item) => item.id === created.id)?.needsReview, true);
|
|
|
|
|
await save({ ...values, version: "25H2", state: "removal_pending" });
|
|
|
|
|
await save({ ...values, version: "25H2", state: "removed" });
|
|
|
|
|
assert.equal((await request(`${path}/${created.id}/delete`, {})).status, 200);
|
|
|
|
|
assert.equal((await request(`${path}/${created.id}/delete`, {})).status, 404);
|
|
|
|
|
}
|