import assert from "node:assert/strict"; import { randomUUID } from "node:crypto"; import { InventoryResponseSchema as AssetsSchema } from "@metis/assets/common"; import type { ProcessTemplateInput, UpdateProcessInput } from "@metis/companies/common"; import { ApplicationConfigurationResponseSchema, CompanyResponseSchema, ProcessResponseSchema, ProcessReviewResponseSchema, ProcessTemplateResponseSchema, ProcessTemplatesResponseSchema, ProcessesResponseSchema, RemoteConnectionResponseSchema, WorkProfileResponseSchema, } from "@metis/companies/common"; import { InventoryResponseSchema as ResourcesSchema } 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 verifyProcesses( request: Request, companyId: string, otherId: string, database: Sequelize ) { const path = `/processes/${companyId}`; const templates = ProcessTemplatesResponseSchema.parse( await (await request("/processes/templates")).json() ).templates; for (const kind of ["onboarding", "change", "offboarding"]) { assert.ok(templates.some((t) => t.values.kind === kind && t.values.active)); } const templateValues: ProcessTemplateInput = { name: "Proceso test salida", kind: "offboarding", active: true, notes: null, instructions: ["Revisar y documentar"], }; const templateResponse = await request("/processes/templates", { values: templateValues }); assert.equal(templateResponse.status, 201); const template = ProcessTemplateResponseSchema.parse(await templateResponse.json()).template; assert.equal(template.id[14], "7"); const personValues = { name: "Persona proceso", position: null, email: null, phone: null, active: true, isContact: false, }; const personResponse = await request(`/companies/${companyId}/people`, personValues); const person = CompanyResponseSchema.parse(await personResponse.json()).company.people.at(-1); assert.ok(person); const host = AssetsSchema.parse(await (await request(`/assets/${companyId}`)).json()).assets.find( (a) => a.values.status !== "retired" ); assert.ok(host); assert.equal( ( await request(`/assets/${companyId}/${host.id}/update`, { values: { ...host.values, personId: person.id }, }) ).status, 200 ); const profileResponse = await request(`/work-profiles/${companyId}`, { values: { personId: person.id, name: "Perfil proceso", assetId: host.id, accountId: null, active: true, notes: null, }, }); assert.equal(profileResponse.status, 201); const profile = WorkProfileResponseSchema.parse(await profileResponse.json()).profile; const resource = ResourcesSchema.parse( await (await request(`/resources/${companyId}`)).json() ).resources.find((r) => r.values.active && r.specification.templateName === "Aplicación"); assert.ok(resource); const appValues = { profileId: profile.id, resourceId: resource.id, delivery: "installed", version: null, state: "configured", notes: null, }; const appResponse = await request(`/application-configurations/${companyId}`, { values: appValues, }); assert.equal(appResponse.status, 201); const app = ApplicationConfigurationResponseSchema.parse(await appResponse.json()).configuration; const remoteValues = { personId: person.id, sourceKind: "personal_mobile", sourceAssetId: null, targetKind: "network", targetId: null, siteId: null, method: "VPN móvil proceso", requiresVpn: false, state: "configured", notes: null, }; const remoteResponse = await request(`/remote-connections/${companyId}`, { values: remoteValues, }); assert.equal(remoteResponse.status, 201); const remote = RemoteConnectionResponseSchema.parse(await remoteResponse.json()).connection; const currentReview = ProcessReviewResponseSchema.parse( await (await request(`${path}/people/${person.id}/review`)).json() ).review; assert.ok( currentReview.some( (r) => r.kind === "remote" && r.id === remote.id && r.sourceKind === "personal_mobile" ) ); assert.ok(currentReview.some((r) => r.kind === "application" && r.id === app.id)); assert.ok(currentReview.some((r) => r.kind === "asset" && r.id === host.id)); assert.equal((await request(`/processes/${otherId}/people/${person.id}/review`)).status, 404); const input = { personId: person.id, templateId: template.id, name: "Salida persona proceso", notes: null, }; const error = async (body: unknown, code: string, id?: string, status = 409) => { const response = await request(path + (id ? `/${id}/update` : ""), { values: body }); 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, personId: randomUUID() }, "PROCESS_PERSON_NOT_FOUND", undefined, 404); assert.equal((await request(path, { values: input }, "https://other.example")).status, 403); assert.equal((await request(path, { values: input }, undefined, "b".repeat(64))).status, 403); const concurrent = await Promise.all([ request(path, { values: input }), request(path, { values: input }), ]); assert.deepEqual(concurrent.map((r) => r.status).sort(), [201, 409]); const createdResponse = concurrent.find((r) => r.status === 201); assert.ok(createdResponse); const process = ProcessResponseSchema.parse(await createdResponse.json()).process; assert.equal(process.id[14], "7"); assert.equal(process.history[0]?.id[14], "7"); assert.ok(process.values.tasks.every((t) => t.id[14] === "7")); assert.equal(process.values.tasks.length, currentReview.length + 1); const revision = process.history.at(-1); assert.ok(revision); const edit: UpdateProcessInput = { name: process.values.name, notes: null, state: "open", revisionId: revision.id, tasks: process.values.tasks.map((t) => ({ id: t.id, status: t.status, notes: t.notes })), }; const save = async (values: UpdateProcessInput) => { const response = await request(`${path}/${process.id}/update`, { values }); assert.equal(response.status, 200); return ProcessResponseSchema.parse(await response.json()).process; }; const unchanged = await save(edit); assert.deepEqual(unchanged, process); await error({ ...edit, state: "completed" }, "PROCESS_PENDING_TASKS", process.id); await error({ ...edit, tasks: edit.tasks.slice(1) }, "PROCESS_TASKS_IMMUTABLE", process.id); assert.equal( (await request(`/processes/${otherId}/${process.id}/update`, { values: edit })).status, 404 ); assert.equal( ProcessesResponseSchema.parse(await (await request(`/processes/${otherId}`)).json()).processes .length, 0 ); const templateUpdate = await request(`/processes/templates/${template.id}/update`, { values: { ...templateValues, active: false, instructions: ["Otra plantilla"] }, }); assert.equal(templateUpdate.status, 200); const resolved = await save({ ...edit, tasks: edit.tasks.map((t) => ({ ...t, status: "done", notes: "Revisado; conserva acceso hasta entrega", })), }); assert.equal(resolved.values.tasks[0]?.title, "Revisar y documentar"); assert.equal(resolved.history.length, 2); await error({ ...edit, notes: "Cambio perdido" }, "PROCESS_CONFLICT", process.id); const inactivePerson = await request(`/companies/${companyId}/people/${person.id}/update`, { ...personValues, active: false, }); assert.equal(inactivePerson.status, 200); const newRemote = await request(`/remote-connections/${companyId}`, { values: { ...remoteValues, method: "Herramienta nueva proceso" }, }); assert.equal(newRemote.status, 201); const refreshedResponse = await request(`${path}/${process.id}/refresh`, { revisionId: resolved.history.at(-1)?.id, }); assert.equal(refreshedResponse.status, 200); const refreshed = ProcessResponseSchema.parse(await refreshedResponse.json()).process; assert.equal(refreshed.values.tasks.length, resolved.values.tasks.length + 1); assert.equal(refreshed.history.length, 3); assert.ok(refreshed.values.tasks.slice(0, -1).every((t) => t.status === "done")); assert.equal( (await request(`${path}/${process.id}/refresh`, { revisionId: resolved.history.at(-1)?.id })) .status, 409 ); const freshTasks = refreshed.values.tasks.map((t) => ({ id: t.id, status: "not_applicable" as const, notes: "Transferido o comprobado", })); const finished = await save({ ...edit, revisionId: refreshed.history.at(-1)?.id ?? "", state: "completed", tasks: freshTasks, }); assert.equal(finished.values.state, "completed"); assert.equal(finished.history.length, 4); assert.ok(finished.review.some((r) => r.kind === "remote")); // Completar seguimiento no revoca una conexión real. await error( { ...edit, revisionId: finished.history.at(-1)?.id, notes: "Cambiar cerrado", tasks: freshTasks, }, "PROCESS_CLOSED", process.id ); assert.equal( (await request(`${path}/${process.id}/refresh`, { revisionId: finished.history.at(-1)?.id })) .status, 409 ); await error(input, "PROCESS_TEMPLATE_INACTIVE"); assert.equal( (await request(`/processes/templates/${template.id}/update`, { values: templateValues })) .status, 200 ); const restarted = await request(path, { values: input }); assert.equal(restarted.status, 201); const next = ProcessResponseSchema.parse(await restarted.json()).process; assert.notEqual(next.id, process.id); assert.equal((await request("/processes/templates", { values: templateValues })).status, 409); const onboarding = templates.find((t) => t.values.kind === "onboarding"); assert.ok(onboarding); const onboardResponse = await request(path, { values: { ...input, templateId: onboarding.id, name: "Alta test" }, }); assert.equal(onboardResponse.status, 201); const onboard = ProcessResponseSchema.parse(await onboardResponse.json()).process; assert.ok(onboard.values.tasks.every((t) => !t.reference)); await database.query("UPDATE person_processes SET currentKey=:key WHERE id=:id", { replacements: { key: "bad", id: next.id }, }); try { const corrupt = await request(path); assert.equal(corrupt.status, 500); assert.ok(!(await corrupt.text()).includes("SELECT")); } finally { await database.query("UPDATE person_processes SET currentKey=:key WHERE id=:id", { replacements: { key: `${companyId}:${person.id}:offboarding`, id: next.id }, }); } assert.equal((await request(path)).status, 200); assert.equal( (await request(`/assets/${companyId}/${host.id}/update`, { values: host.values })).status, 200 ); }