metis-desk/apps/server/src/remote.integration.ts
David 002781b56d feat: update asset status handling and improve UI labels
- Changed asset status label from "status" to "technicalStatus" in AssetForm component.
- Added help text for technical status in AssetForm.
- Updated test cases to reflect changes in asset status from "available" to "operational".
- Enhanced workstation form to utilize new asset status checks and display appropriate messages.
- Refactored asset context to replace "available" with "notRetired" across various application policies and repositories.
- Introduced new utility functions for asset status management, including technical status determination and assignment validation.
- Added integration tests for asset state verification and unlinking assets from workstations.
2026-10-13 11:35:31 +02:00

316 lines
10 KiB
TypeScript

import assert from "node:assert/strict";
import { randomUUID } from "node:crypto";
import { InventoryResponseSchema as AssetsSchema } from "@metis/assets/common";
import type { RemoteConnectionInput } from "@metis/companies/common";
import {
CompanyResponseSchema,
RemoteConnectionResponseSchema,
RemoteConnectionsResponseSchema,
TeleworkAuthorizationResponseSchema,
} from "@metis/companies/common";
import { CatalogResponseSchema, ResourceResponseSchema } from "@metis/resources/common";
import type { Sequelize } from "sequelize";
import { z } from "zod/v4";
import { unlinkAssetFromWorkstations } from "./unlink-asset.integration-helper";
type Request = (path: string, body?: unknown, origin?: string, token?: string) => Promise<Response>;
export async function verifyRemoteConnections(
request: Request,
companyId: string,
otherId: string,
database: Sequelize
) {
const personValues = {
name: "Persona acceso remoto",
position: null,
email: null,
phone: null,
active: true,
isContact: false,
};
const personResponse = await request(`/companies/${companyId}/people`, personValues);
assert.equal(personResponse.status, 201);
const person = CompanyResponseSchema.parse(await personResponse.json()).company.people.at(-1);
assert.ok(person);
const assets = AssetsSchema.parse(await (await request(`/assets/${companyId}`)).json()).assets;
const host = assets.find((a) => a.values.status !== "retired");
assert.ok(host);
const catalog = CatalogResponseSchema.parse(
await (await request(`/resource-catalog/${companyId}`)).json()
);
const template = catalog.items.find((item) => item.builtinKey === "template:application");
assert.ok(template);
const resourceValues = {
name: "ERP acceso remoto",
templateId: template.id,
active: true,
notes: "",
characteristics: {},
};
const resourceResponse = await request(`/resources/${companyId}`, { values: resourceValues });
assert.equal(resourceResponse.status, 201);
const resource = ResourceResponseSchema.parse(await resourceResponse.json()).resource;
const path = `/remote-connections/${companyId}`;
const input: RemoteConnectionInput = {
personId: person.id,
sourceKind: "personal_computer",
sourceAssetId: null,
targetKind: "network",
targetId: null,
siteId: null,
method: "VPN",
requiresVpn: false,
state: "configured",
notes: null,
};
const list = async (scope = companyId) => {
const response = await request(`/remote-connections/${scope}`);
assert.equal(response.status, 200);
return RemoteConnectionsResponseSchema.parse(await response.json()).connections;
};
const save = async (values: RemoteConnectionInput, id?: string) => {
const response = await request(path + (id ? `/${id}/update` : ""), { values });
assert.equal(response.status, id ? 200 : 201);
return RemoteConnectionResponseSchema.parse(await response.json()).connection;
};
const error = async (values: unknown, code: string, id?: string, status = 409) => {
const response = await request(path + (id ? `/${id}/update` : ""), { values });
assert.equal(response.status, status);
assert.equal(z.object({ code: z.string() }).parse(await response.json()).code, code);
};
await error({ ...input, actorId: person.id }, "INVALID_REQUEST", undefined, 400);
await error({ ...input, sourceAssetId: host.id }, "INVALID_REQUEST", undefined, 400);
await error({ ...input, targetKind: "asset" }, "INVALID_REQUEST", undefined, 400);
await error(
{
...input,
sourceKind: "company_asset",
sourceAssetId: host.id,
targetKind: "asset",
targetId: host.id,
},
"INVALID_REQUEST",
undefined,
400
);
await error({ ...input, personId: randomUUID() }, "REMOTE_REFERENCE_NOT_FOUND", undefined, 404);
await error(
{ ...input, targetKind: "asset", targetId: randomUUID() },
"REMOTE_REFERENCE_NOT_FOUND",
undefined,
404
);
assert.equal((await request(path, { values: input }, "https://other.example")).status, 403);
assert.equal((await request(path, { values: input }, undefined, "a".repeat(64))).status, 403);
const concurrent = await Promise.all([
request(path, { values: input }),
request(path, { values: { ...input, method: "vpn" } }),
]);
assert.deepEqual(concurrent.map((r) => r.status).sort(), [201, 409]);
const createdResponse = concurrent.find((r) => r.status === 201);
assert.ok(createdResponse);
const created = RemoteConnectionResponseSchema.parse(await createdResponse.json()).connection;
assert.equal(created.id[14], "7");
assert.equal(created.history[0]?.id[14], "7");
assert.ok(created.needsReview && !created.authorizationEffective);
assert.deepEqual(await save(created.values, created.id), created);
await error({ ...created.values, personId: randomUUID() }, "REMOTE_TARGET_IMMUTABLE", created.id);
await error(
{ ...created.values, sourceKind: "personal_mobile" },
"REMOTE_TARGET_IMMUTABLE",
created.id
);
await error(
{ ...created.values, targetKind: "asset", targetId: host.id },
"REMOTE_TARGET_IMMUTABLE",
created.id
);
await error({ ...created.values, state: "removed" }, "INVALID_REMOTE_TRANSITION", created.id);
assert.equal((await request(`/remote-connections/${otherId}`, { values: input })).status, 404);
assert.equal((await list(otherId)).length, 0);
assert.equal(
(
await request(`/remote-connections/${otherId}/${created.id}/update`, {
values: created.values,
})
).status,
404
);
const rdp = await save({
...input,
targetKind: "asset",
targetId: host.id,
method: "RDP",
requiresVpn: true,
});
const mobile = await save({
...input,
sourceKind: "personal_mobile",
targetKind: "resource",
targetId: resource.id,
method: "VPN móvil",
});
const corporate = await save({ ...input, sourceKind: "company_asset", sourceAssetId: host.id });
assert.ok(rdp.values.requiresVpn && corporate.values.sourceAssetId === host.id);
const alternative = await save({ ...input, method: "Herramienta propia" });
await error(
{ ...alternative.values, method: created.values.method },
"REMOTE_ALREADY_OPEN",
alternative.id
);
const teleworkValues = {
personId: person.id,
active: true,
startsOn: null,
expiresOn: null,
notes: null,
};
const authResponse = await request(`/telework-authorizations/${companyId}`, {
values: teleworkValues,
});
assert.equal(authResponse.status, 201);
const auth = TeleworkAuthorizationResponseSchema.parse(await authResponse.json()).authorization;
const current = async (id: string) => {
const item = (await list()).find((item) => item.id === id);
assert.ok(item);
return item;
};
assert.equal((await current(created.id)).needsReview, false);
assert.equal((await current(mobile.id)).needsReview, false);
assert.equal(
(
await request(`/resources/${companyId}/${resource.id}/update`, {
values: { ...resourceValues, active: false },
})
).status,
200
);
assert.equal((await current(mobile.id)).needsReview, true);
assert.equal((await current(created.id)).needsReview, false);
await unlinkAssetFromWorkstations(request, companyId, host.id);
assert.equal(
(
await request(`/assets/${companyId}/${host.id}/update`, {
values: { ...host.values, status: "retired" },
})
).status,
200
);
assert.equal((await current(corporate.id)).needsReview, true);
assert.equal((await current(rdp.id)).needsReview, true);
assert.equal(
(await request(`/assets/${companyId}/${host.id}/update`, { values: host.values })).status,
200
);
assert.equal((await current(corporate.id)).needsReview, false);
assert.equal(
(
await request(`/telework-authorizations/${companyId}/${auth.id}/update`, {
values: { ...teleworkValues, expiresOn: "2000-01-01" },
})
).status,
200
);
assert.equal((await current(created.id)).needsReview, true);
assert.equal(
(
await request(`/telework-authorizations/${companyId}/${auth.id}/update`, {
values: teleworkValues,
})
).status,
200
);
assert.equal(
(
await request(`/companies/${companyId}/people/${person.id}/update`, {
...personValues,
active: false,
})
).status,
200
);
assert.equal((await current(created.id)).authorizationEffective, false);
assert.equal((await current(created.id)).history.length, 1);
assert.equal(
(
await request(`/telework-authorizations/${companyId}/${auth.id}/update`, {
values: { ...teleworkValues, active: false },
})
).status,
200
);
assert.equal(
(await request(`/companies/${companyId}/people/${person.id}/update`, personValues)).status,
200
);
assert.equal((await current(created.id)).needsReview, true);
const retiring = await save({ ...created.values, state: "removal_pending" }, created.id);
const removed = await save({ ...retiring.values, state: "removed" }, created.id);
assert.equal(removed.needsReview, false);
assert.equal(removed.history.length, 3);
assert.ok(removed.history.every((r) => r.actorId !== person.id));
assert.deepEqual(await save(removed.values, removed.id), removed);
await error(
{ ...removed.values, notes: "Cambiar cerrada" },
"INVALID_REMOTE_TRANSITION",
removed.id
);
const replacement = await save(created.values);
assert.notEqual(replacement.id, created.id);
await database.query("UPDATE remote_connections SET currentKey=:key WHERE id=:id", {
replacements: { id: replacement.id, key: "invalid" },
});
try {
const corrupt = await request(path);
assert.equal(corrupt.status, 500);
assert.ok(!(await corrupt.text()).includes("SELECT"));
} finally {
// El cierre conserva historial y libera la clave; este fixture se limpia al reiniciar la suite.
await database.query("UPDATE remote_connections SET currentKey=SHA2(:key,256) WHERE id=:id", {
replacements: {
id: replacement.id,
key: [
person.id,
input.sourceKind,
"",
input.targetKind,
"",
"",
replacement.values.method.toLowerCase(),
].join("|"),
},
});
}
assert.equal((await current(replacement.id)).history.length, 1);
}