- Add mailbox persistence mapper for domain object mapping. - Create Sequelize repository for mailbox data access. - Define common mailbox schemas and types for validation. - Implement mailbox API for frontend communication. - Develop mailbox form component for creating and editing mailboxes. - Add internationalization support for mailbox labels and messages. - Create mailbox use form for managing mailbox usage periods. - Build mailboxes page to display and manage mailboxes and their usage.
809 lines
29 KiB
TypeScript
809 lines
29 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { once } from "node:events";
|
|
import { test } from "node:test";
|
|
|
|
import { createAssetModule } from "@metis/assets/module";
|
|
import {
|
|
CompaniesResponseSchema,
|
|
CompanyErrorResponseSchema,
|
|
CompanyResponseSchema,
|
|
} from "@metis/companies/common";
|
|
import { createCompanyModule } from "@metis/companies/module";
|
|
import {
|
|
IdentityErrorResponseSchema,
|
|
SessionResponseSchema,
|
|
UserResponseSchema,
|
|
UsersResponseSchema,
|
|
} from "@metis/identity/common";
|
|
import { QueryTypes } from "sequelize";
|
|
|
|
import { verifyAccess } from "./access.integration";
|
|
import { createApp } from "./app";
|
|
import { verifyAssets } from "./assets.integration";
|
|
import { createDatabase, readDatabaseEnvironment } from "./config/database";
|
|
import { createIdentityRuntime } from "./infrastructure/http/identity-runtime";
|
|
import { verifyInventory } from "./inventory.integration";
|
|
import { verifyMail } from "./mail.integration";
|
|
import { verifyRelationships } from "./relationships.integration";
|
|
import { verifyWorkProfiles } from "./work-profiles.integration";
|
|
import { verifyWorkstations } from "./workstations.integration";
|
|
|
|
test("HTTP y MariaDB: configuración única, usuarios, sesiones y protección del administrador", async () => {
|
|
const environment = readDatabaseEnvironment(process.env);
|
|
// Este proceso solo puede limpiar la base de pruebas dedicada.
|
|
assert.equal(environment.DB_NAME, "metis_desk_test");
|
|
const database = createDatabase(environment);
|
|
const identity = await createIdentityRuntime(database, environment);
|
|
const server = createApp(() => undefined, identity).listen(0, "127.0.0.1");
|
|
try {
|
|
await once(server, "listening");
|
|
await database.query("UPDATE identity_state SET initialAdminId = NULL WHERE id = 1");
|
|
await database.query("DELETE FROM http_sessions");
|
|
await database.query("DELETE FROM mail_configuration_revisions");
|
|
await database.query("DELETE FROM mail_configurations");
|
|
await database.query("DELETE FROM mailbox_use_revisions");
|
|
await database.query("DELETE FROM mailbox_uses");
|
|
await database.query("DELETE FROM mailbox_revisions");
|
|
await database.query("DELETE FROM mailboxes");
|
|
await database.query("DELETE FROM work_profile_revisions");
|
|
await database.query("DELETE FROM work_profiles");
|
|
await database.query("DELETE FROM workstation_assets");
|
|
await database.query("DELETE FROM workstation_revisions");
|
|
await database.query("DELETE FROM workstations");
|
|
await database.query("DELETE FROM asset_relationships");
|
|
await database.query("DELETE FROM asset_revisions");
|
|
await database.query("DELETE FROM assets");
|
|
await database.query("DELETE FROM asset_availability");
|
|
await database.query("DELETE FROM asset_template_fields");
|
|
await database.query("DELETE FROM asset_definitions");
|
|
await database.query("DELETE FROM asset_catalogs");
|
|
await database.query("DELETE FROM access_person_references");
|
|
await database.query("DELETE FROM access_references");
|
|
await database.query("DELETE FROM access_revisions");
|
|
await database.query("DELETE FROM access_items");
|
|
await database.query("DELETE FROM access_workspaces WHERE companyId IS NOT NULL");
|
|
await database.query("DELETE FROM users");
|
|
await database.query("DELETE FROM department_assignment_revisions");
|
|
await database.query("DELETE FROM person_department_assignments");
|
|
await database.query("DELETE FROM company_contacts");
|
|
await database.query("DELETE FROM company_departments");
|
|
await database.query("DELETE FROM company_sites");
|
|
await database.query("DELETE FROM companies");
|
|
await createAssetModule(database, (_request, _response, next) => next());
|
|
// Simular la tabla de contactos de la versión anterior solo en la base de pruebas.
|
|
const legacyCompanyId = "48f13545-1529-4ba1-a353-a5f844141411";
|
|
const legacyPersonId = "4ec8f8c5-3ad9-4e22-abf3-b95370400898";
|
|
await database.query("INSERT INTO companies (id,name,active) VALUES (:id,'Legacy',1)", {
|
|
replacements: { id: legacyCompanyId },
|
|
});
|
|
await database.query(
|
|
"INSERT INTO company_contacts (id,companyId,name,position,email,phone,active,isContact) VALUES (:id,:companyId,'Contacto anterior','Compras','legacy@example.test','900123456',1,1)",
|
|
{ replacements: { id: legacyPersonId, companyId: legacyCompanyId } }
|
|
);
|
|
await database.query(
|
|
"ALTER TABLE company_contacts DROP COLUMN isContact, MODIFY position VARCHAR(100) NOT NULL, MODIFY email VARCHAR(254) NOT NULL, MODIFY phone VARCHAR(50) NOT NULL"
|
|
);
|
|
await createCompanyModule(database, (_request, _response, next) => next());
|
|
await createCompanyModule(database, (_request, _response, next) => next());
|
|
const legacy = await database.query<{ id: string; email: string; isContact: number }>(
|
|
"SELECT id,email,isContact FROM company_contacts WHERE id=:id",
|
|
{ replacements: { id: legacyPersonId }, type: QueryTypes.SELECT }
|
|
);
|
|
assert.equal(legacy[0]?.id, legacyPersonId);
|
|
assert.equal(legacy[0]?.email, "legacy@example.test");
|
|
assert.equal(Number(legacy[0]?.isContact), 1);
|
|
await database.query("DELETE FROM company_contacts WHERE id=:id", {
|
|
replacements: { id: legacyPersonId },
|
|
});
|
|
await database.query("DELETE FROM companies WHERE id=:id", {
|
|
replacements: { id: legacyCompanyId },
|
|
});
|
|
|
|
const address = server.address();
|
|
assert.ok(address && typeof address !== "string");
|
|
const baseUrl = `http://127.0.0.1:${address.port}/api`;
|
|
function browser() {
|
|
let cookie = "";
|
|
let csrf = "";
|
|
return {
|
|
async request(
|
|
path: string,
|
|
body?: unknown,
|
|
origin = environment.APP_ORIGIN,
|
|
token?: string
|
|
) {
|
|
const response = await fetch(`${baseUrl}${path}`, {
|
|
method: body === undefined ? "GET" : "POST",
|
|
headers: {
|
|
cookie,
|
|
origin,
|
|
"content-type": "application/json",
|
|
"x-csrf-token": token ?? csrf,
|
|
},
|
|
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
});
|
|
const newCookie = response.headers.get("set-cookie");
|
|
if (newCookie) {
|
|
cookie = newCookie.split(";")[0] ?? "";
|
|
}
|
|
return response;
|
|
},
|
|
async session() {
|
|
const response = await this.request("/auth/session");
|
|
assert.equal(response.status, 200);
|
|
const data = SessionResponseSchema.parse(await response.json());
|
|
csrf = data.csrfToken;
|
|
return data;
|
|
},
|
|
cookie: () => cookie,
|
|
};
|
|
}
|
|
async function expectError(response: Response, status: number, code: string) {
|
|
assert.equal(response.status, status);
|
|
assert.equal(IdentityErrorResponseSchema.parse(await response.json()).code, code);
|
|
}
|
|
const admin = browser();
|
|
const competitor = browser();
|
|
assert.equal((await admin.session()).setupRequired, true);
|
|
await competitor.session();
|
|
await expectError(await admin.request("/users"), 401, "UNAUTHENTICATED");
|
|
await expectError(await admin.request("/asset-catalog/global"), 401, "UNAUTHENTICATED");
|
|
const first = {
|
|
name: "Admin test",
|
|
email: "ADMIN@example.test",
|
|
password: "Admin123",
|
|
};
|
|
await expectError(
|
|
await admin.request("/auth/bootstrap", first, "https://other.example"),
|
|
403,
|
|
"FORBIDDEN"
|
|
);
|
|
await expectError(
|
|
await admin.request("/auth/bootstrap", first, environment.APP_ORIGIN, "á".repeat(64)),
|
|
403,
|
|
"FORBIDDEN"
|
|
);
|
|
await expectError(
|
|
await admin.request("/auth/bootstrap", { ...first, password: "1234567" }),
|
|
400,
|
|
"INVALID_REQUEST"
|
|
);
|
|
const beforeCookie = admin.cookie();
|
|
const attempts = await Promise.all([
|
|
admin.request("/auth/bootstrap", first),
|
|
competitor.request("/auth/bootstrap", { ...first, email: "other@example.test" }),
|
|
]);
|
|
assert.deepEqual(attempts.map((response) => response.status).sort(), [201, 409]);
|
|
const winnerIndex = attempts.findIndex((response) => response.status === 201);
|
|
const administrator = winnerIndex === 0 ? admin : competitor;
|
|
const winnerResponse = attempts[winnerIndex];
|
|
assert.ok(winnerResponse);
|
|
const initial = UserResponseSchema.parse(await winnerResponse.json()).user;
|
|
assert.equal(initial.isInitialAdmin, true);
|
|
assert.equal(initial.id.charAt(14), "7");
|
|
if (winnerIndex === 0) {
|
|
assert.notEqual(admin.cookie(), beforeCookie);
|
|
}
|
|
assert.equal((await administrator.session()).setupRequired, false);
|
|
await expectError(await administrator.request("/auth/bootstrap", first), 409, "SETUP_CLOSED");
|
|
await expectError(
|
|
await administrator.request(`/users/${initial.id}/update`, {
|
|
name: initial.name,
|
|
email: initial.email,
|
|
active: false,
|
|
}),
|
|
409,
|
|
"ADMIN_PROTECTED"
|
|
);
|
|
await expectError(
|
|
await administrator.request("/users", {
|
|
name: "Intruso",
|
|
email: "intruder@example.test",
|
|
password: first.password,
|
|
isInitialAdmin: true,
|
|
}),
|
|
400,
|
|
"INVALID_REQUEST"
|
|
);
|
|
await expectError(
|
|
await administrator.request(`/users/${initial.id.toUpperCase()}/reset-password`, {
|
|
password: "Admin123",
|
|
}),
|
|
409,
|
|
"ADMIN_PROTECTED"
|
|
);
|
|
const person = {
|
|
name: "Operador test",
|
|
email: "operator@example.test",
|
|
password: "User1234",
|
|
};
|
|
const created = await administrator.request("/users", person);
|
|
assert.equal(created.status, 201);
|
|
const user = UserResponseSchema.parse(await created.json()).user;
|
|
assert.equal(user.isInitialAdmin, false);
|
|
await expectError(
|
|
await administrator.request("/users", { ...person, email: "OPERATOR@example.test" }),
|
|
409,
|
|
"EMAIL_EXISTS"
|
|
);
|
|
const list = UsersResponseSchema.parse(await (await administrator.request("/users")).json());
|
|
assert.equal(list.users.length, 2);
|
|
assert.equal(JSON.stringify(list).includes("passwordHash"), false);
|
|
const operator = browser();
|
|
await operator.session();
|
|
await expectError(
|
|
await operator.request("/auth/login", { email: person.email, password: "incorrect" }),
|
|
401,
|
|
"INVALID_CREDENTIALS"
|
|
);
|
|
assert.equal(
|
|
(await operator.request("/auth/login", { email: person.email, password: person.password }))
|
|
.status,
|
|
200
|
|
);
|
|
await operator.session();
|
|
await expectError(await operator.request("/users"), 403, "FORBIDDEN");
|
|
await expectError(
|
|
await operator.request("/auth/change-password", {
|
|
currentPassword: "incorrect",
|
|
password: "Nuevo123",
|
|
}),
|
|
401,
|
|
"INVALID_CREDENTIALS"
|
|
);
|
|
const oldSession = browser();
|
|
await oldSession.session();
|
|
assert.equal(
|
|
(await oldSession.request("/auth/login", { email: person.email, password: person.password }))
|
|
.status,
|
|
200
|
|
);
|
|
await oldSession.session();
|
|
const changedPassword = "Nuevo123";
|
|
assert.equal(
|
|
(
|
|
await operator.request("/auth/change-password", {
|
|
currentPassword: person.password,
|
|
password: changedPassword,
|
|
})
|
|
).status,
|
|
200
|
|
);
|
|
assert.equal((await operator.session()).user?.id, user.id);
|
|
assert.equal((await oldSession.session()).user, null);
|
|
const resetPassword = "Reset123";
|
|
assert.equal(
|
|
(await administrator.request(`/users/${user.id}/reset-password`, { password: resetPassword }))
|
|
.status,
|
|
200
|
|
);
|
|
assert.equal((await operator.session()).user, null);
|
|
await expectError(
|
|
await operator.request("/auth/login", { email: person.email, password: changedPassword }),
|
|
401,
|
|
"INVALID_CREDENTIALS"
|
|
);
|
|
assert.equal(
|
|
(await operator.request("/auth/login", { email: person.email, password: resetPassword }))
|
|
.status,
|
|
200
|
|
);
|
|
await operator.session();
|
|
assert.equal(
|
|
(
|
|
await administrator.request(`/users/${user.id}/update`, {
|
|
name: "Operador editado",
|
|
email: person.email,
|
|
active: false,
|
|
})
|
|
).status,
|
|
200
|
|
);
|
|
assert.equal((await operator.session()).user, null);
|
|
await expectError(
|
|
await operator.request("/auth/login", { email: person.email, password: resetPassword }),
|
|
401,
|
|
"INVALID_CREDENTIALS"
|
|
);
|
|
assert.equal(
|
|
(
|
|
await administrator.request(`/users/${user.id}/update`, {
|
|
name: person.name,
|
|
email: person.email,
|
|
active: true,
|
|
})
|
|
).status,
|
|
200
|
|
);
|
|
assert.equal(
|
|
(await operator.request("/auth/login", { email: person.email, password: resetPassword }))
|
|
.status,
|
|
200
|
|
);
|
|
await operator.session();
|
|
const anonymousCompanyBrowser = browser();
|
|
await anonymousCompanyBrowser.session();
|
|
await expectError(await anonymousCompanyBrowser.request("/companies"), 401, "UNAUTHENTICATED");
|
|
const companyCreated = await operator.request("/companies", {
|
|
name: "Compañía de pruebas",
|
|
active: true,
|
|
});
|
|
assert.equal(companyCreated.status, 201);
|
|
const customer = CompanyResponseSchema.parse(await companyCreated.json()).company;
|
|
assert.equal(customer.id.charAt(14), "7");
|
|
const secondCreated = await operator.request("/companies", {
|
|
name: "Segunda compañía",
|
|
active: true,
|
|
});
|
|
const secondCompany = CompanyResponseSchema.parse(await secondCreated.json()).company;
|
|
const personContact = {
|
|
name: "Ana Test",
|
|
position: "Responsable IT",
|
|
email: "ANA@example.test",
|
|
phone: "+34 900 123 456",
|
|
active: true,
|
|
};
|
|
await expectError(
|
|
await operator.request(
|
|
`/companies/${customer.id}/contacts`,
|
|
personContact,
|
|
"https://other.example"
|
|
),
|
|
403,
|
|
"FORBIDDEN"
|
|
);
|
|
await expectError(
|
|
await operator.request(`/companies/${customer.id}/contacts`, {
|
|
...personContact,
|
|
phone: "invalid",
|
|
}),
|
|
400,
|
|
"INVALID_REQUEST"
|
|
);
|
|
const addedContact = await operator.request(
|
|
`/companies/${customer.id}/contacts`,
|
|
personContact
|
|
);
|
|
assert.equal(addedContact.status, 201);
|
|
const afterContact = CompanyResponseSchema.parse(await addedContact.json()).company;
|
|
const contact = afterContact.contacts[0];
|
|
assert.ok(contact);
|
|
assert.equal(contact.companyId, customer.id);
|
|
assert.equal(contact.id.charAt(14), "7");
|
|
assert.equal(contact.email, "ana@example.test");
|
|
const crossContact = await operator.request(
|
|
`/companies/${secondCompany.id}/contacts/${contact.id}/update`,
|
|
personContact
|
|
);
|
|
assert.equal(crossContact.status, 404);
|
|
assert.equal(
|
|
CompanyErrorResponseSchema.parse(await crossContact.json()).code,
|
|
"CONTACT_NOT_FOUND"
|
|
);
|
|
const changedContact = await operator.request(
|
|
`/companies/${customer.id}/contacts/${contact.id}/update`,
|
|
{ ...personContact, name: "Ana Actualizada", active: false }
|
|
);
|
|
assert.equal(changedContact.status, 200);
|
|
assert.equal(
|
|
CompanyResponseSchema.parse(await changedContact.json()).company.contacts[0]?.active,
|
|
false
|
|
);
|
|
const noAddressSite = await operator.request(`/companies/${customer.id}/sites`, {
|
|
name: "Almacén",
|
|
});
|
|
assert.equal(noAddressSite.status, 201);
|
|
const emptySite = CompanyResponseSchema.parse(await noAddressSite.json()).company.sites[0];
|
|
assert.ok(emptySite);
|
|
assert.equal(emptySite.address, null);
|
|
const siteAddress = "Calle Mayor, 10\n28001 Madrid";
|
|
const changedSite = await operator.request(
|
|
`/companies/${customer.id}/sites/${emptySite.id}/update`,
|
|
{ name: "Sede central", address: siteAddress }
|
|
);
|
|
assert.equal(changedSite.status, 200);
|
|
assert.equal(
|
|
CompanyResponseSchema.parse(await changedSite.json()).company.sites[0]?.address,
|
|
siteAddress
|
|
);
|
|
const crossSite = await operator.request(
|
|
`/companies/${secondCompany.id}/sites/${emptySite.id}/update`,
|
|
{ name: "Ajena", address: null }
|
|
);
|
|
assert.equal(crossSite.status, 404);
|
|
const largeAddress = "Á".repeat(10000);
|
|
assert.equal(
|
|
(
|
|
await operator.request(`/companies/${customer.id}/sites`, {
|
|
name: "Oficina",
|
|
address: largeAddress,
|
|
})
|
|
).status,
|
|
201
|
|
);
|
|
assert.equal(
|
|
(
|
|
await operator.request(`/companies/${customer.id}/sites`, {
|
|
name: "Demasiado",
|
|
address: "a".repeat(10001),
|
|
})
|
|
).status,
|
|
400
|
|
);
|
|
const editedCompany = await operator.request(`/companies/${customer.id}/update`, {
|
|
name: "Compañía actualizada",
|
|
active: false,
|
|
});
|
|
assert.equal(editedCompany.status, 200);
|
|
const details = CompanyResponseSchema.parse(
|
|
await (await operator.request(`/companies/${customer.id}`)).json()
|
|
).company;
|
|
assert.equal(details.id, customer.id);
|
|
assert.equal(details.active, false);
|
|
assert.equal(details.contacts[0]?.id, contact.id);
|
|
assert.equal(details.sites.length, 2);
|
|
const companiesList = CompaniesResponseSchema.parse(
|
|
await (await operator.request("/companies")).json()
|
|
);
|
|
assert.equal(companiesList.companies.length, 2);
|
|
await assert.rejects(
|
|
database.query("DELETE FROM companies WHERE id = :id", { replacements: { id: customer.id } })
|
|
);
|
|
const clearedSite = await operator.request(
|
|
`/companies/${customer.id}/sites/${emptySite.id}/update`,
|
|
{ name: "Sede central", address: null }
|
|
);
|
|
assert.equal(clearedSite.status, 200);
|
|
// Personas y departamentos: mismas fronteras de sesión, UUID y compañía.
|
|
const peoplePath = `/companies/${customer.id}`;
|
|
async function companyData(response: Response) {
|
|
assert.ok(response.status === 200 || response.status === 201);
|
|
return CompanyResponseSchema.parse(await response.json()).company;
|
|
}
|
|
async function companyError(response: Response, status: number, code: string) {
|
|
assert.equal(response.status, status);
|
|
assert.equal(CompanyErrorResponseSchema.parse(await response.json()).code, code);
|
|
}
|
|
const personBody = {
|
|
name: "Persona sin correo",
|
|
position: null,
|
|
email: null,
|
|
phone: null,
|
|
active: true,
|
|
isContact: false,
|
|
};
|
|
const personResponse = await companyData(
|
|
await operator.request(`${peoplePath}/people`, personBody)
|
|
);
|
|
const companyPerson = personResponse.people.find((item) => item.name === personBody.name);
|
|
assert.ok(companyPerson);
|
|
assert.equal(companyPerson.email, null);
|
|
assert.equal(companyPerson.id.charAt(14), "7");
|
|
assert.equal(
|
|
personResponse.contacts.some((item) => item.id === companyPerson.id),
|
|
false
|
|
);
|
|
await companyError(
|
|
await operator.request(`${peoplePath}/people`, { ...personBody, isInitialAdmin: true }),
|
|
400,
|
|
"INVALID_REQUEST"
|
|
);
|
|
await companyError(
|
|
await operator.request(
|
|
`${peoplePath}/departments`,
|
|
{ name: "Forbidden", active: true },
|
|
"https://other.example"
|
|
),
|
|
403,
|
|
"FORBIDDEN"
|
|
);
|
|
const departmentResponse = await companyData(
|
|
await operator.request(`${peoplePath}/departments`, { name: " Compras ", active: true })
|
|
);
|
|
const department = departmentResponse.departments.find((item) => item.name === "Compras");
|
|
assert.ok(department);
|
|
await companyError(
|
|
await operator.request(`${peoplePath}/departments`, { name: "COMPRAS", active: true }),
|
|
409,
|
|
"DEPARTMENT_NAME_EXISTS"
|
|
);
|
|
const otherDepartmentResponse = await companyData(
|
|
await operator.request(`/companies/${secondCompany.id}/departments`, {
|
|
name: "Compras",
|
|
active: true,
|
|
})
|
|
);
|
|
const foreignDepartment = otherDepartmentResponse.departments[0];
|
|
assert.ok(foreignDepartment);
|
|
const assignmentBody = {
|
|
departmentId: department.id,
|
|
position: "Responsable",
|
|
startDate: "2026-01-01",
|
|
endDate: null,
|
|
isPrimary: true,
|
|
};
|
|
await companyError(
|
|
await operator.request(`${peoplePath}/people/${companyPerson.id}/assignments`, {
|
|
...assignmentBody,
|
|
departmentId: foreignDepartment.id,
|
|
}),
|
|
404,
|
|
"DEPARTMENT_NOT_FOUND"
|
|
);
|
|
const assignmentResponse = await companyData(
|
|
await operator.request(`${peoplePath}/people/${companyPerson.id}/assignments`, assignmentBody)
|
|
);
|
|
const membership = assignmentResponse.assignments.find(
|
|
(item) => item.personId === companyPerson.id
|
|
);
|
|
assert.ok(membership);
|
|
assert.equal(membership.id.charAt(14), "7");
|
|
assert.equal(membership.history.length, 1);
|
|
await companyError(
|
|
await operator.request(
|
|
`${peoplePath}/people/${companyPerson.id}/assignments`,
|
|
assignmentBody
|
|
),
|
|
409,
|
|
"ASSIGNMENT_CONFLICT"
|
|
);
|
|
const secondResponse = await companyData(
|
|
await operator.request(`${peoplePath}/departments`, { name: "Administración", active: true })
|
|
);
|
|
const secondDepartment = secondResponse.departments.find(
|
|
(item) => item.name === "Administración"
|
|
);
|
|
assert.ok(secondDepartment);
|
|
await companyError(
|
|
await operator.request(`${peoplePath}/people/${companyPerson.id}/assignments`, {
|
|
...assignmentBody,
|
|
departmentId: secondDepartment.id,
|
|
}),
|
|
409,
|
|
"ASSIGNMENT_CONFLICT"
|
|
);
|
|
await companyData(
|
|
await operator.request(`${peoplePath}/people/${companyPerson.id}/assignments`, {
|
|
...assignmentBody,
|
|
departmentId: secondDepartment.id,
|
|
isPrimary: false,
|
|
})
|
|
);
|
|
const editedAssignment = {
|
|
position: "Supervisor",
|
|
startDate: "2026-01-01",
|
|
endDate: null,
|
|
isPrimary: true,
|
|
};
|
|
const editedMembership = await companyData(
|
|
await operator.request(
|
|
`${peoplePath}/people/${companyPerson.id}/assignments/${membership.id}/update`,
|
|
editedAssignment
|
|
)
|
|
);
|
|
const history = editedMembership.assignments.find((item) => item.id === membership.id)?.history;
|
|
assert.equal(history?.length, 2);
|
|
assert.equal(history?.[0]?.position, "Responsable");
|
|
const repeatedMembership = await companyData(
|
|
await operator.request(
|
|
`${peoplePath}/people/${companyPerson.id}/assignments/${membership.id}/update`,
|
|
editedAssignment
|
|
)
|
|
);
|
|
assert.equal(
|
|
repeatedMembership.assignments.find((item) => item.id === membership.id)?.history.length,
|
|
2
|
|
);
|
|
await companyError(
|
|
await operator.request(
|
|
`${peoplePath}/people/${contact.id}/assignments/${membership.id}/update`,
|
|
editedAssignment
|
|
),
|
|
404,
|
|
"ASSIGNMENT_NOT_FOUND"
|
|
);
|
|
await companyError(
|
|
await operator.request(
|
|
`${peoplePath}/people/${companyPerson.id}/assignments/${membership.id}/update`,
|
|
{ ...editedAssignment, endDate: "2025-12-31", isPrimary: false }
|
|
),
|
|
400,
|
|
"INVALID_REQUEST"
|
|
);
|
|
await companyData(
|
|
await operator.request(`${peoplePath}/departments/${secondDepartment.id}/update`, {
|
|
name: secondDepartment.name,
|
|
active: false,
|
|
})
|
|
);
|
|
await companyError(
|
|
await operator.request(`${peoplePath}/people/${contact.id}/assignments`, {
|
|
...assignmentBody,
|
|
departmentId: secondDepartment.id,
|
|
isPrimary: false,
|
|
}),
|
|
409,
|
|
"INACTIVE_ASSIGNMENT_TARGET"
|
|
);
|
|
await companyData(
|
|
await operator.request(`${peoplePath}/people/${companyPerson.id}/update`, {
|
|
...personBody,
|
|
active: false,
|
|
})
|
|
);
|
|
await companyError(
|
|
await operator.request(`${peoplePath}/people/${companyPerson.id}/assignments`, {
|
|
...assignmentBody,
|
|
startDate: "2025-01-01",
|
|
endDate: "2025-02-01",
|
|
isPrimary: false,
|
|
}),
|
|
409,
|
|
"INACTIVE_ASSIGNMENT_TARGET"
|
|
);
|
|
const closed = await companyData(
|
|
await operator.request(
|
|
`${peoplePath}/people/${companyPerson.id}/assignments/${membership.id}/update`,
|
|
{ ...editedAssignment, endDate: "2026-03-01", isPrimary: false }
|
|
)
|
|
);
|
|
assert.equal(closed.assignments.find((item) => item.id === membership.id)?.history.length, 3);
|
|
await companyData(
|
|
await operator.request(`${peoplePath}/people/${contact.id}/update`, {
|
|
name: contact.name,
|
|
position: contact.position,
|
|
email: contact.email,
|
|
phone: contact.phone,
|
|
active: true,
|
|
isContact: true,
|
|
})
|
|
);
|
|
// Concurrencia: el bloqueo de compañía evita dos asignaciones abiertas al mismo departamento.
|
|
const concurrent = await Promise.all([
|
|
operator.request(`${peoplePath}/people/${contact.id}/assignments`, {
|
|
...assignmentBody,
|
|
isPrimary: false,
|
|
}),
|
|
operator.request(`${peoplePath}/people/${contact.id}/assignments`, {
|
|
...assignmentBody,
|
|
isPrimary: false,
|
|
}),
|
|
]);
|
|
assert.deepEqual(concurrent.map((response) => response.status).sort(), [201, 409]);
|
|
// La FK compuesta impide pertenencias cruzadas aunque se intente escribir SQL directamente.
|
|
await assert.rejects(
|
|
database.query(
|
|
"INSERT INTO person_department_assignments (id, companyId, personId, departmentId, position, startDate, endDate, isPrimary) VALUES (:id, :companyId, :personId, :departmentId, NULL, '2026-01-01', NULL, 0)",
|
|
{
|
|
replacements: {
|
|
id: "a2349bc1-8c29-4d8b-b53a-5ec40e37a002",
|
|
companyId: customer.id,
|
|
personId: companyPerson.id,
|
|
departmentId: foreignDepartment.id,
|
|
},
|
|
}
|
|
)
|
|
);
|
|
const userCount = await database.query<{ total: number }>(
|
|
"SELECT COUNT(*) AS total FROM users",
|
|
{ type: QueryTypes.SELECT }
|
|
);
|
|
assert.equal(Number(userCount[0]?.total), 2);
|
|
|
|
const accessCompany = CompanyResponseSchema.parse(
|
|
await (await operator.request(peoplePath)).json()
|
|
).company;
|
|
const accessOther = CompanyResponseSchema.parse(
|
|
await (await operator.request(`/companies/${secondCompany.id}`)).json()
|
|
).company;
|
|
await verifyAccess((...args) => operator.request(...args), accessCompany, accessOther);
|
|
await verifyAssets(
|
|
(...args) => operator.request(...args),
|
|
customer.id,
|
|
secondCompany.id,
|
|
database
|
|
);
|
|
await verifyInventory(
|
|
(...args) => operator.request(...args),
|
|
customer.id,
|
|
secondCompany.id,
|
|
database
|
|
);
|
|
await verifyRelationships(
|
|
(...args) => operator.request(...args),
|
|
customer.id,
|
|
secondCompany.id,
|
|
database
|
|
);
|
|
await verifyWorkstations(
|
|
(...args) => operator.request(...args),
|
|
customer.id,
|
|
secondCompany.id,
|
|
database
|
|
);
|
|
await verifyMail(
|
|
(...args) => operator.request(...args),
|
|
customer.id,
|
|
secondCompany.id,
|
|
database
|
|
);
|
|
await verifyWorkProfiles(
|
|
(...args) => operator.request(...args),
|
|
customer.id,
|
|
secondCompany.id,
|
|
database
|
|
);
|
|
const countAfterAccess = await database.query<{ total: number }>(
|
|
"SELECT COUNT(*) AS total FROM users",
|
|
{ type: QueryTypes.SELECT }
|
|
);
|
|
assert.equal(Number(countAfterAccess[0]?.total), 2);
|
|
assert.equal((await operator.request("/auth/logout", {})).status, 200);
|
|
await expectError(await operator.request("/access/global"), 401, "UNAUTHENTICATED");
|
|
await expectError(
|
|
await operator.request(`/work-profiles/${customer.id}`),
|
|
401,
|
|
"UNAUTHENTICATED"
|
|
);
|
|
await expectError(await operator.request(`/assets/${customer.id}`), 401, "UNAUTHENTICATED");
|
|
assert.equal((await operator.session()).user, null);
|
|
await expectError(
|
|
await operator.request(`/assets/${customer.id}/relationships`, {}),
|
|
401,
|
|
"UNAUTHENTICATED"
|
|
);
|
|
await expectError(
|
|
await operator.request(`/assets/${customer.id}/workstations`, {}),
|
|
401,
|
|
"UNAUTHENTICATED"
|
|
);
|
|
await expectError(
|
|
await operator.request(`/mail-configurations/${customer.id}`),
|
|
401,
|
|
"UNAUTHENTICATED"
|
|
);
|
|
await expectError(
|
|
await operator.request(`/mail-configurations/${customer.id}`, {}),
|
|
401,
|
|
"UNAUTHENTICATED"
|
|
);
|
|
await expectError(await operator.request("/users"), 401, "UNAUTHENTICATED");
|
|
await expectError(await operator.request("/companies"), 401, "UNAUTHENTICATED");
|
|
await assert.rejects(
|
|
database.query("UPDATE users SET active = 0 WHERE id = :id", {
|
|
replacements: { id: initial.id },
|
|
})
|
|
);
|
|
await assert.rejects(
|
|
database.query("DELETE FROM users WHERE id = :id", { replacements: { id: initial.id } })
|
|
);
|
|
const stored = await database.query<{ passwordHash: string }>(
|
|
"SELECT passwordHash FROM users WHERE id = :id",
|
|
{ replacements: { id: user.id }, type: QueryTypes.SELECT }
|
|
);
|
|
assert.ok(stored[0]?.passwordHash.startsWith("scrypt-v1$"));
|
|
assert.equal(JSON.stringify(stored).includes(resetPassword), false);
|
|
// Al acumular fallos se rechazan los siguientes intentos.
|
|
let limited = false;
|
|
for (let index = 0; index < 12; index += 1) {
|
|
const response = await operator.request("/auth/login", {
|
|
email: "missing@example.test",
|
|
password: "wrong",
|
|
});
|
|
if (response.status === 429) {
|
|
limited = true;
|
|
break;
|
|
}
|
|
assert.equal(response.status, 401);
|
|
}
|
|
assert.equal(limited, true);
|
|
} finally {
|
|
identity.stop();
|
|
await new Promise<void>((resolve, reject) => {
|
|
server.close((error) => (error ? reject(error) : resolve()));
|
|
server.closeAllConnections();
|
|
});
|
|
await database.close();
|
|
}
|
|
});
|