metis-desk/apps/server/src/processes.integration.ts

284 lines
11 KiB
TypeScript
Raw Normal View History

import assert from "node:assert/strict";
import { randomUUID } from "node:crypto";
import { InventoryResponseSchema as AssetsSchema } from "@metis/assets/common";
import {
ApplicationConfigurationResponseSchema,
CompanyResponseSchema,
ProcessResponseSchema,
ProcessReviewResponseSchema,
type ProcessTemplateInput,
ProcessTemplateResponseSchema,
ProcessTemplatesResponseSchema,
ProcessesResponseSchema,
RemoteConnectionResponseSchema,
type UpdateProcessInput,
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<Response>;
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
);
}