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; 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>( "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 ); }