metis-desk/apps/server/src/software-installations.integration.ts

208 lines
8.3 KiB
TypeScript

import assert from "node:assert/strict";
import { InventoryResponseSchema } from "@metis/assets/common";
import type { SoftwareInstallationInput } from "@metis/companies/common";
import {
SoftwareInstallationResponseSchema,
SoftwareInstallationsResponseSchema,
} from "@metis/companies/common";
import { CatalogResponseSchema, ResourceResponseSchema } from "@metis/resources/common";
import type { Sequelize } from "sequelize";
import { z } from "zod/v4";
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");
const licenseTemplate = catalog.items.find(
(item) => item.builtinKey === "template:software_license"
);
const certificate = catalog.items.find((item) => item.builtinKey === "template:certificate");
assert.ok(host && otherHost && template && application && certificate && licenseTemplate);
assert.ok(template.values.kind === "template" && template.values.usage === "os");
assert.ok(
licenseTemplate.values.kind === "template" && licenseTemplate.values.usage === "license"
);
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;
};
const license = await addResource(licenseTemplate.id, "Licencia equipo test");
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}`;
const values: SoftwareInstallationInput = {
assetId: host.id,
resourceId: os.id,
licenseResourceId: null,
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);
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
);
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");
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);
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
);
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);
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);
}