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

272 lines
9.8 KiB
TypeScript
Raw Normal View History

2026-10-04 19:04:14 +00:00
import assert from "node:assert/strict";
import { InventoryResponseSchema, WorkstationResponseSchema } from "@metis/assets/common";
import { CompanyResponseSchema } from "@metis/companies/common";
import type { Sequelize } from "sequelize";
import { z } from "zod/v4";
import { removeSharedWorkstationAssets } from "../../../modules/assets/src/api/infrastructure/persistence/remove-shared-workstation-assets";
2026-10-10 15:57:05 +00:00
import { initializeWorkstationMembers } from "../../../modules/assets/src/api/infrastructure/persistence/workstation-relational";
2026-10-10 11:32:40 +00:00
2026-10-04 19:04:14 +00:00
type Request = (path: string, body?: unknown, origin?: string, token?: string) => Promise<Response>;
2026-10-10 10:01:58 +00:00
2026-10-04 19:04:14 +00:00
export async function verifyWorkstations(
request: Request,
companyId: string,
otherId: string,
database: Sequelize
) {
const path = `/assets/${companyId}/workstations`;
const own = CompanyResponseSchema.parse(
await (await request(`/companies/${companyId}`)).json()
).company;
const other = CompanyResponseSchema.parse(
await (await request(`/companies/${otherId}`)).json()
).company;
const inventory = InventoryResponseSchema.parse(
await (await request(`/assets/${companyId}`)).json()
);
for (const asset of inventory.assets.filter((item) => item.values.status !== "operational")) {
assert.equal(
(
await request(`/assets/${companyId}/${asset.id}/update`, {
values: { ...asset.values, status: "operational" },
})
).status,
200
);
}
2026-10-04 19:04:14 +00:00
const foreign = InventoryResponseSchema.parse(await (await request(`/assets/${otherId}`)).json())
.assets[0];
const first = inventory.assets[0];
const second = inventory.assets[1];
2026-10-10 10:01:58 +00:00
2026-10-04 19:04:14 +00:00
assert.ok(first && second && foreign);
const personFields = {
name: "Persona puesto",
position: null,
email: null,
phone: null,
active: true,
isContact: false,
};
const personResponse = await request(`/companies/${companyId}/people`, personFields);
2026-10-10 10:01:58 +00:00
2026-10-04 19:04:14 +00:00
assert.equal(personResponse.status, 201);
const person = CompanyResponseSchema.parse(await personResponse.json()).company.people.at(-1);
2026-10-10 10:01:58 +00:00
2026-10-04 19:04:14 +00:00
assert.ok(person);
const otherPersonResponse = await request(`/companies/${otherId}/people`, personFields);
2026-10-10 10:01:58 +00:00
assert.equal(otherPersonResponse.status, 201);
const otherPerson = CompanyResponseSchema.parse(
await otherPersonResponse.json()
).company.people.at(-1);
2026-10-10 10:01:58 +00:00
assert.ok(otherPerson);
2026-10-04 19:04:14 +00:00
const input = {
name: "Puesto Ana",
active: true,
siteId: own.sites[0]?.id ?? null,
personId: person.id,
assetIds: [first.id, second.id],
};
2026-10-10 10:01:58 +00:00
2026-10-04 19:04:14 +00:00
const save = async (values: unknown, id?: string) => {
const response = await request(id ? `${path}/${id}/update` : path, { values });
2026-10-10 10:01:58 +00:00
2026-10-04 19:04:14 +00:00
assert.equal(response.status, id ? 200 : 201, await response.clone().text());
2026-10-10 10:01:58 +00:00
2026-10-04 19:04:14 +00:00
return WorkstationResponseSchema.parse(await response.json()).workstation;
};
2026-10-10 10:01:58 +00:00
2026-10-04 19:04:14 +00:00
const error = async (values: unknown, code: string, id?: string, status = 409) => {
const response = await request(id ? `${path}/${id}/update` : path, { values });
2026-10-10 10:01:58 +00:00
2026-10-04 19:04:14 +00:00
assert.equal(response.status, status);
assert.equal(z.object({ code: z.string() }).parse(await response.json()).code, code);
};
2026-10-10 10:01:58 +00:00
2026-10-04 19:04:14 +00:00
await error({ ...input, assetIds: [foreign.id] }, "ASSET_NOT_FOUND", undefined, 404);
await error({ ...input, siteId: other.sites[0]?.id }, "SITE_NOT_FOUND", undefined, 404);
await error({ ...input, personId: otherPerson.id }, "PERSON_UNAVAILABLE");
2026-10-04 19:04:14 +00:00
await error({ ...input, assetIds: [first.id, first.id] }, "INVALID_MEMBERS");
await error({ ...input, actorId: first.id }, "INVALID_REQUEST", undefined, 400);
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 [a, b] = await Promise.all([
request(path, { values: input }),
request(path, { values: { ...input, name: "Puesto conflictivo" } }),
]);
2026-10-10 10:01:58 +00:00
2026-10-04 19:04:14 +00:00
assert.deepEqual([a.status, b.status].sort(), [201, 409]);
const success = a.status === 201 ? a : b;
const created = WorkstationResponseSchema.parse(await success.json()).workstation;
2026-10-10 10:01:58 +00:00
2026-10-04 19:04:14 +00:00
assert.equal(created.id.charAt(14), "7");
assert.equal(created.history[0]?.id.charAt(14), "7");
assert.ok(created.history[0]?.actorName);
const unchanged = await save(
{ ...created.values, assetIds: [...created.values.assetIds].reverse() },
created.id
);
2026-10-10 10:01:58 +00:00
2026-10-04 19:04:14 +00:00
assert.deepEqual(unchanged, created);
const empty = await save({ ...input, name: "Puesto vacío", siteId: null, assetIds: [] });
2026-10-10 10:01:58 +00:00
2026-10-04 19:04:14 +00:00
assert.equal(
(
await request(`/companies/${companyId}/people/${person.id}/update`, {
...personFields,
active: false,
})
).status,
200
);
await error({ ...empty.values, name: "Nueva asignación inactiva" }, "PERSON_UNAVAILABLE");
assert.deepEqual(await save(created.values, created.id), created);
await error({ ...empty.values, assetIds: [first.id] }, "ASSET_IN_WORKSTATION", empty.id);
await error({ ...empty.values, assetIds: [created.id] }, "ASSET_NOT_FOUND", empty.id, 404);
const inactive = await save({ ...created.values, active: false }, created.id);
2026-10-10 10:01:58 +00:00
2026-10-04 19:04:14 +00:00
assert.equal(inactive.history.length, 2);
const receiver = await save({ ...empty.values, assetIds: [first.id] }, empty.id);
2026-10-10 10:01:58 +00:00
2026-10-04 19:04:14 +00:00
await error({ ...created.values, active: true }, "ASSET_IN_WORKSTATION", created.id);
const removed = await save({ ...receiver.values, assetIds: [] }, receiver.id);
2026-10-10 10:01:58 +00:00
2026-10-04 19:04:14 +00:00
assert.equal(removed.history.length, 3);
const restored = await save(created.values, created.id);
2026-10-10 10:01:58 +00:00
2026-10-04 19:04:14 +00:00
assert.equal(restored.history.length, 3);
assert.deepEqual(restored.history[1]?.values.assetIds, created.values.assetIds);
const foreignUpdate = await request(`/assets/${otherId}/workstations/${created.id}/update`, {
values: created.values,
});
2026-10-10 10:01:58 +00:00
2026-10-04 19:04:14 +00:00
assert.equal(foreignUpdate.status, 404);
await assert.rejects(
database.query("UPDATE workstation_assets SET assetId=:asset WHERE workstationId=:id", {
replacements: { asset: foreign.id, id: created.id },
})
);
2026-10-10 11:32:40 +00:00
await database.query("UPDATE workstation_members SET kind='invalid' WHERE workstationId=:id", {
replacements: { id: restored.id },
2026-10-04 19:04:14 +00:00
});
try {
assert.equal((await request(`/assets/${companyId}`)).status, 500);
} finally {
2026-10-10 11:32:40 +00:00
await database.query(
"UPDATE workstation_members SET kind='exclusive' WHERE workstationId=:id",
{
replacements: { id: restored.id },
}
);
2026-10-04 19:04:14 +00:00
}
const current = InventoryResponseSchema.parse(
await (await request(`/assets/${companyId}`)).json()
);
2026-10-10 10:01:58 +00:00
2026-10-04 19:04:14 +00:00
assert.deepEqual(
current.workstations.find((item) => item.id === restored.id),
restored
);
assert.equal(
InventoryResponseSchema.parse(await (await request(`/assets/${otherId}`)).json()).workstations
.length,
0
);
2026-10-10 15:57:05 +00:00
// Repetir la preparación del esquema conserva composición e historial.
const beforeInitialization = InventoryResponseSchema.parse(
2026-10-10 11:32:40 +00:00
await (await request(`/assets/${companyId}`)).json()
2026-10-05 07:53:29 +00:00
);
2026-10-10 11:32:40 +00:00
2026-10-10 15:57:05 +00:00
await initializeWorkstationMembers(database);
await initializeWorkstationMembers(database);
2026-10-10 11:32:40 +00:00
assert.deepEqual(
InventoryResponseSchema.parse(await (await request(`/assets/${companyId}`)).json()),
2026-10-10 15:57:05 +00:00
beforeInitialization
2026-10-10 11:32:40 +00:00
);
await error({ ...input, sharedAssetIds: [] }, "INVALID_REQUEST", undefined, 400);
2026-10-05 07:53:29 +00:00
await save({ ...restored.values, active: false }, restored.id);
const exclusive = await save({ ...input, personId: null, assetIds: [first.id] });
2026-10-10 10:01:58 +00:00
await error({ ...exclusive.values }, "ASSET_IN_WORKSTATION");
2026-10-05 07:53:29 +00:00
await save({ ...exclusive.values, active: false }, exclusive.id);
const designated = await save(
{ ...exclusive.values, active: false, computerAssetId: first.id },
exclusive.id
);
const reread = InventoryResponseSchema.parse(
await (await request(`/assets/${companyId}`)).json()
).workstations.find((item) => item.id === exclusive.id);
assert.equal(reread?.values.computerAssetId, first.id);
assert.equal(reread?.history.at(-1)?.values.computerAssetId, first.id);
const repeated = await save(designated.values, designated.id);
assert.equal(repeated.history.length, designated.history.length);
await error(
{ ...designated.values, computerAssetId: second.id },
"INVALID_MEMBERS",
designated.id
);
// Simula las asociaciones compartidas del esquema anterior, también en su última revisión.
const latest = designated.history.at(-1);
assert.ok(latest);
await database.transaction(async (transaction) => {
const replacements = {
id: designated.id,
revision: latest.id,
asset: second.id,
company: companyId,
};
await database.query(
"INSERT INTO workstation_members (workstationId,assetId,kind,position) VALUES (:id,:asset,'shared',1)",
{ replacements, transaction }
);
await database.query(
"INSERT INTO workstation_revision_members (revisionId,assetId,kind,position) VALUES (:revision,:asset,'shared',1)",
{ replacements, transaction }
);
await database.query(
"INSERT INTO workstation_assets (workstationId,companyId,assetId,activeKey) VALUES (:id,:company,:asset,NULL)",
{ replacements, transaction }
);
});
await removeSharedWorkstationAssets(database);
await removeSharedWorkstationAssets(database);
const migrated = InventoryResponseSchema.parse(
await (await request(`/assets/${companyId}`)).json()
);
assert.deepEqual(
migrated.workstations.find((item) => item.id === designated.id),
designated
);
assert.ok(migrated.assets.some((asset) => asset.id === second.id));
2026-10-05 07:53:29 +00:00
const race = await Promise.all([
request(path, {
values: {
...input,
personId: null,
2026-10-05 07:53:29 +00:00
name: "Reserva concurrente",
assetIds: [second.id],
},
}),
request(path, {
values: {
...input,
personId: null,
name: "Segunda reserva concurrente",
assetIds: [second.id],
},
2026-10-05 07:53:29 +00:00
}),
]);
2026-10-10 10:01:58 +00:00
2026-10-05 07:53:29 +00:00
assert.deepEqual(race.map((response) => response.status).sort(), [201, 409]);
2026-10-04 19:04:14 +00:00
}