metis-desk/apps/server/src/record-deletion.integration.ts
David b45925f3e2 feat(assets): implement asset picker dialog and selection components
- Added `useAssetPickerController` for managing asset selection logic.
- Created `AssetSelectProps` type for defining asset selection properties.
- Implemented `AssetPickerDialog` for displaying the asset selection interface.
- Developed `AssetPickerList` for rendering the list of selectable assets.
- Introduced `AssetSelect` component for integrating the asset picker into forms.
- Created `AssetList` component for shared asset listing functionality.
- Added tests for workstation creation logic to ensure proper validation.
- Implemented workstation asset fields and association fields for managing workstation assets.
- Developed workstation lifecycle dialog for managing work profile states.
- Added form progress component for visualizing multi-step forms.
2026-10-14 19:42:59 +02:00

177 lines
5.5 KiB
TypeScript

import assert from "node:assert/strict";
import { randomUUID } from "node:crypto";
import type { Sequelize } from "sequelize";
import { QueryTypes } from "sequelize";
import { z } from "zod/v4";
type Request = (path: string, body?: unknown) => Promise<Response>;
async function verifyProfileDeletionBlocked(
request: Request,
row: { companyId: string; profileId?: string },
table: string
) {
if (!row.profileId) {
return;
}
const path = `/work-profiles/${row.companyId}/${row.profileId}`;
assert.equal((await request(`${path}/deactivate`, {})).status, 200);
const blocked = await request(`${path}/delete`, {});
assert.equal(blocked.status, 409);
assert.equal(
z.object({ code: z.string() }).parse(await blocked.json()).code,
"PROFILE_IN_USE",
`La referencia de ${table} debe impedir eliminar el perfil`
);
}
export async function verifyRecordDeletion(request: Request, database: Sequelize) {
const counts = async () => {
const result: number[] = [];
for (const table of [
"company_contacts",
"assets",
"resources",
"work_profiles",
"access_items",
"mailbox_uses",
]) {
const rows = await database.query<{ count: number }>(
`SELECT COUNT(*) AS count FROM ${table}`,
{ type: QueryTypes.SELECT }
);
result.push(Number(rows[0]?.count));
}
return result;
};
const folderSubjects = await database.query<{
profileId: string;
companyId: string;
needId: string;
actorId: string;
}>(
"SELECT p.id AS profileId,p.companyId,i.id AS needId,u.id AS actorId FROM work_profiles p JOIN access_workspaces w ON w.companyId=p.companyId JOIN access_items i ON i.workspaceId=w.id CROSS JOIN users u WHERE i.kind='need' LIMIT 1",
{ type: QueryTypes.SELECT }
);
const subject = folderSubjects[0];
assert.ok(subject, "Debe existir un perfil y una necesidad de recurso para la prueba de carpeta");
const mappingId = randomUUID();
const values = {
profileId: subject.profileId,
needId: subject.needId,
path: "\\\\SERVER\\Test",
driveLetter: null,
state: "pending",
notes: null,
};
await database.query(
"INSERT INTO folder_mappings (id,companyId,profileId,needId,path,driveLetter,state,notes,currentKey,driveKey) VALUES (:id,:companyId,:profileId,:needId,:path,:driveLetter,:state,:notes,:currentKey,NULL)",
{
replacements: {
...subject,
id: mappingId,
...values,
currentKey: `${subject.profileId}:${subject.needId}`,
},
}
);
await database.query(
"INSERT INTO folder_mapping_revisions (id,mappingId,position,profileId,needId,path,driveLetter,state,notes,actorId,recordedAt) VALUES (:id,:mappingId,0,:profileId,:needId,:path,:driveLetter,:state,:notes,:actorId,:recordedAt)",
{
replacements: {
id: randomUUID(),
mappingId,
...values,
actorId: subject.actorId,
recordedAt: new Date().toISOString(),
},
}
);
const before = await counts();
const specs = [
[
"application_configurations",
"application_configuration_revisions",
"configurationId",
"application-configurations",
],
[
"certificate_configurations",
"certificate_configuration_revisions",
"configurationId",
"certificate-configurations",
],
[
"printer_configurations",
"printer_configuration_revisions",
"configurationId",
"printer-configurations",
],
[
"mail_configurations",
"mail_configuration_revisions",
"configurationId",
"mail-configurations",
],
["folder_mappings", "folder_mapping_revisions", "mappingId", "folder-mappings"],
["asset_support", "asset_support_revisions", "connectionId", "asset-support"],
["asset_relationships", null, null, "assets"],
] as const;
for (const [table, history, foreignKey, endpoint] of specs) {
const rows = await database.query<{
id: string;
companyId: string;
assetId?: string;
profileId?: string;
}>(`SELECT * FROM ${table}`, { type: QueryTypes.SELECT });
assert.ok(rows.length > 0, `No existen datos para probar ${table}`);
// Todos los estados, incluidos los cerrados, admiten corregir un alta equivocada.
for (const row of rows) {
const suffix =
endpoint === "asset-support"
? `${row.assetId}/${row.id}`
: endpoint === "assets"
? `relationships/${row.id}`
: row.id;
const path = `/${endpoint}/${row.companyId}/${suffix}/delete`;
await verifyProfileDeletionBlocked(request, row, table);
assert.equal(
(await request(`/${endpoint}/${randomUUID()}/${suffix}/delete`, {})).status,
404
);
assert.equal((await request(path.replace(row.companyId, "invalid"), {})).status, 400);
assert.equal((await request(path, { unexpected: true })).status, 400);
assert.equal((await request(path, {})).status, 200);
assert.equal((await request(path, {})).status, 404);
const remaining = await database.query(`SELECT id FROM ${table} WHERE id=:id`, {
replacements: { id: row.id },
type: QueryTypes.SELECT,
});
assert.equal(remaining.length, 0);
if (history && foreignKey) {
const revisions = await database.query(
`SELECT id FROM ${history} WHERE ${foreignKey}=:id`,
{ replacements: { id: row.id }, type: QueryTypes.SELECT }
);
assert.equal(revisions.length, 0);
}
}
}
assert.deepEqual(await counts(), before, "La eliminación debe conservar las entidades de origen");
}