221 lines
9.2 KiB
TypeScript
221 lines
9.2 KiB
TypeScript
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";
|
|
|
|
type Request = (path: string, body?: unknown, origin?: string, token?: string) => Promise<Response>;
|
|
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()
|
|
);
|
|
const foreign = InventoryResponseSchema.parse(await (await request(`/assets/${otherId}`)).json())
|
|
.assets[0];
|
|
const first = inventory.assets[0];
|
|
const second = inventory.assets[1];
|
|
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);
|
|
assert.equal(personResponse.status, 201);
|
|
const person = CompanyResponseSchema.parse(await personResponse.json()).company.people.at(-1);
|
|
assert.ok(person);
|
|
const otherPersonResponse = await request(`/companies/${otherId}/people`, personFields);
|
|
assert.equal(otherPersonResponse.status, 201);
|
|
const otherPerson = CompanyResponseSchema.parse(
|
|
await otherPersonResponse.json()
|
|
).company.people.at(-1);
|
|
assert.ok(otherPerson);
|
|
const input = {
|
|
name: "Puesto Ana",
|
|
active: true,
|
|
siteId: own.sites[0]?.id ?? null,
|
|
personId: person.id,
|
|
assetIds: [first.id, second.id],
|
|
sharedAssetIds: [],
|
|
};
|
|
const save = async (values: unknown, id?: string) => {
|
|
const response = await request(id ? `${path}/${id}/update` : path, { values });
|
|
assert.equal(response.status, id ? 200 : 201, await response.clone().text());
|
|
return WorkstationResponseSchema.parse(await response.json()).workstation;
|
|
};
|
|
const error = async (values: unknown, code: string, id?: string, status = 409) => {
|
|
const response = await request(id ? `${path}/${id}/update` : path, { values });
|
|
assert.equal(response.status, status);
|
|
assert.equal(z.object({ code: z.string() }).parse(await response.json()).code, code);
|
|
};
|
|
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");
|
|
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" } }),
|
|
]);
|
|
assert.deepEqual([a.status, b.status].sort(), [201, 409]);
|
|
const success = a.status === 201 ? a : b;
|
|
const created = WorkstationResponseSchema.parse(await success.json()).workstation;
|
|
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
|
|
);
|
|
assert.deepEqual(unchanged, created);
|
|
const empty = await save({ ...input, name: "Puesto vacío", siteId: null, assetIds: [] });
|
|
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);
|
|
assert.equal(inactive.history.length, 2);
|
|
const receiver = await save({ ...empty.values, assetIds: [first.id] }, empty.id);
|
|
await error({ ...created.values, active: true }, "ASSET_IN_WORKSTATION", created.id);
|
|
const removed = await save({ ...receiver.values, assetIds: [] }, receiver.id);
|
|
assert.equal(removed.history.length, 3);
|
|
const restored = await save(created.values, created.id);
|
|
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,
|
|
});
|
|
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 },
|
|
})
|
|
);
|
|
const corruption = { ...restored.values, assetIds: [foreign.id] };
|
|
await database.query("UPDATE workstations SET `values`=:values WHERE id=:id", {
|
|
replacements: { values: JSON.stringify(corruption), id: restored.id },
|
|
});
|
|
try {
|
|
assert.equal((await request(`/assets/${companyId}`)).status, 500);
|
|
} finally {
|
|
await database.query("UPDATE workstations SET `values`=:values WHERE id=:id", {
|
|
replacements: { values: JSON.stringify(restored.values), id: restored.id },
|
|
});
|
|
}
|
|
const current = InventoryResponseSchema.parse(
|
|
await (await request(`/assets/${companyId}`)).json()
|
|
);
|
|
assert.deepEqual(
|
|
current.workstations.find((item) => item.id === restored.id),
|
|
restored
|
|
);
|
|
assert.equal(
|
|
InventoryResponseSchema.parse(await (await request(`/assets/${otherId}`)).json()).workstations
|
|
.length,
|
|
0
|
|
);
|
|
// Las instantáneas anteriores se leen sin reescribir identidades ni historia.
|
|
const legacyRoot = { ...restored.values };
|
|
const { sharedAssetIds: _legacyShared, ...legacyValues } = legacyRoot;
|
|
await database.query(
|
|
"UPDATE workstations SET " +
|
|
String.fromCharCode(96) +
|
|
"values" +
|
|
String.fromCharCode(96) +
|
|
"=:values WHERE id=:id",
|
|
{
|
|
replacements: { values: JSON.stringify(legacyValues), id: restored.id },
|
|
}
|
|
);
|
|
await database.query(
|
|
"UPDATE workstation_revisions SET `values`=JSON_REMOVE(`values`, '$.sharedAssetIds') WHERE workstationId=:id",
|
|
{ replacements: { id: restored.id } }
|
|
);
|
|
try {
|
|
assert.deepEqual(
|
|
InventoryResponseSchema.parse(
|
|
await (await request(`/assets/${companyId}`)).json()
|
|
).workstations.find((item) => item.id === restored.id),
|
|
restored
|
|
);
|
|
} finally {
|
|
for (const revision of restored.history) {
|
|
await database.query("UPDATE workstation_revisions SET `values`=:values WHERE id=:id", {
|
|
replacements: { id: revision.id, values: JSON.stringify(revision.values) },
|
|
});
|
|
}
|
|
await database.query(
|
|
"UPDATE workstations SET " +
|
|
String.fromCharCode(96) +
|
|
"values" +
|
|
String.fromCharCode(96) +
|
|
"=:values WHERE id=:id",
|
|
{
|
|
replacements: { values: JSON.stringify(restored.values), id: restored.id },
|
|
}
|
|
);
|
|
}
|
|
await error(
|
|
{ ...input, personId: null, assetIds: [], sharedAssetIds: [first.id] },
|
|
"ASSET_IN_WORKSTATION"
|
|
);
|
|
await error(
|
|
{ ...input, personId: null, assetIds: [], sharedAssetIds: [foreign.id] },
|
|
"ASSET_NOT_FOUND",
|
|
undefined,
|
|
404
|
|
);
|
|
await error({ ...input, sharedAssetIds: [first.id] }, "INVALID_MEMBERS");
|
|
await save({ ...restored.values, active: false }, restored.id);
|
|
const sharedInput = { ...input, personId: null, assetIds: [], sharedAssetIds: [first.id] };
|
|
const sharedOne = await save({ ...sharedInput, name: "Compartido uno" });
|
|
const sharedTwo = await save({ ...sharedInput, name: "Compartido dos" });
|
|
await error({ ...sharedInput, assetIds: [first.id], sharedAssetIds: [] }, "ASSET_IN_WORKSTATION");
|
|
await save({ ...sharedOne.values, sharedAssetIds: [] }, sharedOne.id);
|
|
await error({ ...sharedInput, assetIds: [first.id], sharedAssetIds: [] }, "ASSET_IN_WORKSTATION");
|
|
await save({ ...sharedTwo.values, active: false }, sharedTwo.id);
|
|
const exclusive = await save({ ...sharedInput, assetIds: [first.id], sharedAssetIds: [] });
|
|
await error({ ...sharedTwo.values, active: true }, "ASSET_IN_WORKSTATION", sharedTwo.id);
|
|
await save({ ...exclusive.values, active: false }, exclusive.id);
|
|
const race = await Promise.all([
|
|
request(path, {
|
|
values: {
|
|
...sharedInput,
|
|
name: "Reserva concurrente",
|
|
assetIds: [second.id],
|
|
sharedAssetIds: [],
|
|
},
|
|
}),
|
|
request(path, {
|
|
values: { ...sharedInput, name: "Uso compartido concurrente", sharedAssetIds: [second.id] },
|
|
}),
|
|
]);
|
|
assert.deepEqual(race.map((response) => response.status).sort(), [201, 409]);
|
|
}
|