metis-desk/apps/server/src/mail.integration.ts
David 002781b56d feat: update asset status handling and improve UI labels
- Changed asset status label from "status" to "technicalStatus" in AssetForm component.
- Added help text for technical status in AssetForm.
- Updated test cases to reflect changes in asset status from "available" to "operational".
- Enhanced workstation form to utilize new asset status checks and display appropriate messages.
- Refactored asset context to replace "available" with "notRetired" across various application policies and repositories.
- Introduced new utility functions for asset status management, including technical status determination and assignment validation.
- Added integration tests for asset state verification and unlinking assets from workstations.
2026-10-13 11:35:31 +02:00

391 lines
13 KiB
TypeScript

import assert from "node:assert/strict";
import { randomUUID } from "node:crypto";
import { InventoryResponseSchema } from "@metis/assets/common";
import type {
MailConfigurationInput,
MailboxInput,
MailboxUseInput,
} from "@metis/companies/common";
import {
AccessResponseSchema,
CompanyResponseSchema,
MailConfigurationResponseSchema,
MailConfigurationsResponseSchema,
MailboxesResponseSchema,
WorkProfileResponseSchema,
} from "@metis/companies/common";
import type { Sequelize } from "sequelize";
import { QueryTypes } from "sequelize";
import { z } from "zod/v4";
import { mailSnapshot } from "../../../modules/companies/src/api/infrastructure/persistence/mail-remote-snapshots";
import { snapshotValues } from "../../../modules/companies/src/api/infrastructure/persistence/scalar-snapshot-reader";
import { writeSnapshotRow } from "../../../modules/companies/src/api/infrastructure/persistence/scalar-snapshot-writer";
import { prepareProfileHosts } from "./work-profile-hosts.fixture";
type Request = (path: string, body?: unknown, origin?: string, token?: string) => Promise<Response>;
export async function verifyMail(
request: Request,
companyId: string,
otherId: string,
database: Sequelize
) {
const path = `/mail-configurations/${companyId}`;
const boxPath = `/mailboxes/${companyId}`;
const company = CompanyResponseSchema.parse(
await (await request(`/companies/${companyId}`)).json()
).company;
const other = CompanyResponseSchema.parse(
await (await request(`/companies/${otherId}`)).json()
).company;
const assets = InventoryResponseSchema.parse(
await (await request(`/assets/${companyId}`)).json()
).assets;
const first = assets[0];
const second = assets[1];
const otherPerson = company.people.find((item) => item.active);
const foreignPerson = other.people[0];
assert.ok(first && second && otherPerson && foreignPerson);
for (const asset of [first, second]) {
if (asset.values.status === "retired") {
const response = await request(`/assets/${companyId}/${asset.id}/update`, {
values: { ...asset.values, status: "operational" },
});
assert.equal(response.status, 200, await response.clone().text());
}
}
await prepareProfileHosts(request, companyId, [first.id, second.id]);
const personFields = {
name: "Persona correo",
position: null,
email: null,
phone: null,
active: true,
isContact: false,
};
const peopleResponse = await request(`/companies/${companyId}/people`, personFields);
assert.equal(peopleResponse.status, 201);
const person = CompanyResponseSchema.parse(await peopleResponse.json()).company.people.at(-1);
assert.ok(person);
const today = new Date().toISOString().slice(0, 10);
const profile = async (personId: string, assetId: string) => {
const response = await request(`/work-profiles/${companyId}`, {
values: {
name: `Perfil correo ${assetId}`,
personId,
assetId,
accountId: null,
active: true,
notes: null,
},
});
assert.equal(response.status, 201, await response.clone().text());
return WorkProfileResponseSchema.parse(await response.json()).profile;
};
const primary = await profile(person.id, first.id);
const mobileProfile = await profile(person.id, second.id);
const sharedProfile = await profile(otherPerson.id, first.id);
const saveBox = async (values: MailboxInput, id?: string) => {
const response = await request(boxPath + (id ? `/${id}/update` : ""), { values });
assert.equal(response.status, id ? 200 : 201, await response.clone().text());
const data = MailboxesResponseSchema.parse(await response.json());
const box = data.mailboxes.find((item) => item.id === data.savedId);
assert.ok(box);
return box;
};
const saveUse = async (values: MailboxUseInput, id?: string) => {
const response = await request(`${boxPath}/uses${id ? `/${id}/update` : ""}`, { values });
assert.equal(response.status, id ? 200 : 201, await response.clone().text());
const data = MailboxesResponseSchema.parse(await response.json());
const use = data.uses.find((item) => item.id === data.savedId);
assert.ok(use);
return use;
};
const boxFields: MailboxInput = {
address: "ventas@example.test",
kind: "corporate",
context: null,
accountId: null,
active: true,
notes: null,
};
const box = await saveBox(boxFields);
const external = await saveBox({
...boxFields,
address: "ana@outside.test",
kind: "external",
context: "Colaboración",
});
const useFields = { mailboxId: box.id, personId: person.id, startDate: today, endDate: null };
const authorization = await saveUse(useFields);
await saveUse({ ...useFields, mailboxId: external.id });
await saveUse({ ...useFields, personId: otherPerson.id });
assert.equal(box.id[14], "7");
assert.equal(box.values.accountId, null);
assert.deepEqual(await saveBox(boxFields, box.id), box);
const duplicateBox = await request(boxPath, {
values: { ...boxFields, address: "VENTAS@EXAMPLE.TEST" },
});
assert.equal(duplicateBox.status, 409);
assert.equal((await request(`${boxPath}/uses`, { values: useFields })).status, 409);
assert.equal(
(await request(`${boxPath}/uses`, { values: { ...useFields, personId: foreignPerson.id } }))
.status,
409
);
assert.equal((await request(boxPath, { values: { ...boxFields, address: "bad" } })).status, 400);
assert.equal(
(
await request(boxPath, {
values: { ...boxFields, address: "other@example.test", accountId: foreignPerson.id },
})
).status,
409
);
const input: MailConfigurationInput = {
profileId: primary.id,
mailboxId: box.id,
method: "imap",
state: "configured",
notes: null,
};
const save = async (values: MailConfigurationInput, id?: string) => {
const response = await request(path + (id ? `/${id}/update` : ""), { values });
assert.equal(response.status, id ? 200 : 201, await response.clone().text());
return MailConfigurationResponseSchema.parse(await response.json()).configuration;
};
const error = async (values: unknown, code: string, id?: string, status = 409) => {
const response = await request(path + (id ? `/${id}/update` : ""), { values });
assert.equal(response.status, status, await response.clone().text());
assert.equal(z.object({ code: z.string() }).parse(await response.json()).code, code);
};
await error({ ...input, profileId: foreignPerson.id }, "MAIL_SCOPE_MISMATCH");
await error({ ...input, mailboxId: foreignPerson.id }, "MAIL_SCOPE_MISMATCH");
await error({ ...input, address: "invalid" }, "INVALID_REQUEST", undefined, 400);
await error({ ...input, actorId: person.id }, "INVALID_REQUEST", undefined, 400);
await error({ ...input, state: "removed" }, "INVALID_MAIL_TRANSITION");
assert.equal((await request(path, { values: input }, "https://other.example")).status, 403);
assert.equal((await request(path, { values: input }, undefined, "bad")).status, 403);
const [a, b] = await Promise.all([
request(path, { values: input }),
request(path, { values: input }),
]);
assert.deepEqual([a.status, b.status].sort(), [201, 409]);
const created = MailConfigurationResponseSchema.parse(
await (a.status === 201 ? a : b).json()
).configuration;
assert.equal(created.id[14], "7");
assert.equal(created.history[0]?.id[14], "7");
assert.ok(created.history[0]?.actorName);
assert.equal(created.needsReview, false);
assert.deepEqual(await save(input, created.id), created);
const personal = await save({ ...input, mailboxId: external.id, state: "pending" });
const mobile = await save({ ...input, profileId: mobileProfile.id, method: "exchange" });
const shared = await save({
...input,
profileId: sharedProfile.id,
state: "pending",
method: "pop",
});
assert.equal(new Set([created.id, personal.id, mobile.id, shared.id]).size, 4);
await error(
{ ...input, profileId: mobileProfile.id, state: "removed" },
"MAIL_TARGET_IMMUTABLE",
created.id
);
await error({ ...input, state: "removed" }, "INVALID_MAIL_TRANSITION", created.id);
assert.equal(
(await request(`/mail-configurations/${otherId}/${created.id}/update`, { values: input }))
.status,
404
);
await saveUse({ ...useFields, endDate: today }, authorization.id);
const reviewed = MailConfigurationsResponseSchema.parse(
await (await request(path)).json()
).configurations;
assert.equal(reviewed.find((item) => item.id === created.id)?.needsReview, true);
assert.equal(reviewed.find((item) => item.id === personal.id)?.needsReview, false);
await error({ ...input, method: "pop" }, "MAIL_NOT_AUTHORIZED", created.id);
const pending = await save({ ...input, state: "removal_pending" }, created.id);
const removed = await save({ ...input, state: "removed" }, created.id);
assert.equal(pending.history.length, 2);
assert.equal(removed.history.length, 3);
assert.equal(removed.history[0]?.values.state, "configured");
assert.equal(removed.needsReview, false);
await error(input, "INVALID_MAIL_TRANSITION", removed.id);
const deactivate = await request(`/companies/${companyId}/people/${person.id}/update`, {
...personFields,
active: false,
});
assert.equal(deactivate.status, 200);
const inactive = MailConfigurationsResponseSchema.parse(
await (await request(path)).json()
).configurations;
assert.equal(inactive.find((item) => item.id === mobile.id)?.needsReview, true);
const never = await save(
{ ...input, mailboxId: external.id, state: "never_configured" },
personal.id
);
assert.equal(never.history.length, 2);
// Simula un registro previo: no se reescriben sus UUID ni sus revisiones al vincularlo.
const active = await request(`/companies/${companyId}/people/${person.id}/update`, personFields);
assert.equal(active.status, 200);
await saveUse(
{ ...useFields, mailboxId: external.id, startDate: today, endDate: today },
undefined
);
const systemResponse = await request(`/access/${companyId}`, {
values: { kind: "system", name: "Correo legado", active: true },
});
assert.equal(systemResponse.status, 201);
const system = AccessResponseSchema.parse(await systemResponse.json()).items.at(-1);
assert.ok(system);
const accountResponse = await request(`/access/${companyId}`, {
values: {
kind: "account",
systemId: system.id,
environmentId: null,
usage: "shared",
login: "legado",
active: true,
expiresOn: null,
notes: null,
},
});
assert.equal(accountResponse.status, 201);
const account = AccessResponseSchema.parse(await accountResponse.json()).items.at(-1);
assert.ok(account);
const legacyId = randomUUID();
const revisionId = randomUUID();
const actorId = created.history[0]?.actorId;
assert.ok(actorId);
const legacyValues = {
personId: person.id,
accountId: account.id,
assetId: first.id,
address: "legacy@outside.test",
method: "imap",
state: "configured",
notes: "Conservar",
};
await database.transaction(async (transaction) => {
await writeSnapshotRow(
database,
"mail_configurations",
mailSnapshot,
{
id: legacyId,
companyId,
values: legacyValues,
currentKey: [person.id, account.id, first.id].join(":"),
},
transaction,
"insert"
);
await writeSnapshotRow(
database,
"mail_configuration_revisions",
mailSnapshot,
{
id: revisionId,
configurationId: legacyId,
position: 0,
actorId,
recordedAt: new Date().toISOString(),
values: legacyValues,
},
transaction,
"insert"
);
});
const legacy = MailConfigurationsResponseSchema.parse(
await (await request(path)).json()
).configurations.find((item) => item.id === legacyId);
assert.ok(legacy?.legacy);
assert.equal(legacy.needsReview, true);
await error({ ...input, mailboxId: external.id }, "MAIL_LEGACY_TARGET_MISMATCH", legacyId);
const legacyBox = await saveBox({
...boxFields,
address: legacyValues.address,
kind: "external",
});
await saveUse({ ...useFields, mailboxId: legacyBox.id });
const adopted = await save({ ...input, mailboxId: legacyBox.id, notes: "Conservar" }, legacyId);
assert.equal(adopted.id, legacyId);
assert.equal(adopted.legacy, false);
assert.equal(adopted.history[0]?.id, revisionId);
assert.equal(adopted.history.length, 2);
assert.deepEqual(adopted.history[0]?.values, legacyValues);
const persisted = await database.query<Record<string, unknown>>(
"SELECT * FROM mail_configuration_revisions WHERE id=:id",
{ replacements: { id: revisionId }, type: QueryTypes.SELECT }
);
const original = persisted[0];
assert.ok(original);
assert.deepEqual(snapshotValues(mailSnapshot, original), legacyValues);
await database.query("UPDATE mail_configurations SET currentKey='invalid' WHERE id=:id", {
replacements: { id: mobile.id },
});
try {
assert.equal((await request(path)).status, 500);
} finally {
await database.query("UPDATE mail_configurations SET currentKey=:key WHERE id=:id", {
replacements: { id: mobile.id, key: `${mobileProfile.id}:${box.id}` },
});
}
assert.equal(
MailConfigurationsResponseSchema.parse(
await (await request(`/mail-configurations/${otherId}`)).json()
).configurations.length,
0
);
}