2026-10-07 07:34:18 +00:00
|
|
|
import assert from "node:assert/strict";
|
|
|
|
|
import { randomUUID } from "node:crypto";
|
|
|
|
|
|
|
|
|
|
import { InventoryResponseSchema as AssetsSchema } from "@metis/assets/common";
|
2026-10-08 14:43:34 +00:00
|
|
|
import type { ProcessTemplateInput, UpdateProcessInput } from "@metis/companies/common";
|
2026-10-07 07:34:18 +00:00
|
|
|
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<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);
|
|
|
|
|
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;
|
2026-10-09 23:37:10 +00:00
|
|
|
const workstationResponse = await request(`/assets/${companyId}/workstations`, {
|
|
|
|
|
values: {
|
|
|
|
|
name: "Puesto proceso",
|
|
|
|
|
active: true,
|
|
|
|
|
siteId: null,
|
|
|
|
|
personId: person.id,
|
|
|
|
|
assetIds: [host.id],
|
|
|
|
|
sharedAssetIds: [],
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
assert.equal(workstationResponse.status, 201);
|
2026-10-07 07:34:18 +00:00
|
|
|
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
|
|
|
|
|
);
|
|
|
|
|
}
|