388 lines
13 KiB
TypeScript
388 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-normalization";
|
|
import { writeSnapshotRow } from "../../../modules/companies/src/api/infrastructure/persistence/scalar-snapshot-writer";
|
|
|
|
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: "available" },
|
|
});
|
|
|
|
assert.equal(response.status, 200, await response.clone().text());
|
|
}
|
|
}
|
|
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
|
|
);
|
|
}
|