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"; type Request = (path: string, body?: unknown, origin?: string, token?: string) => Promise; 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); 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); }