metis-desk/apps/server/src/identity.test.ts

432 lines
15 KiB
TypeScript
Raw Normal View History

import assert from "node:assert/strict";
import { once } from "node:events";
import { test } from "node:test";
import {
CompaniesResponseSchema,
CompanyErrorResponseSchema,
CompanyResponseSchema,
} from "@metis/companies/common";
import {
IdentityErrorResponseSchema,
SessionResponseSchema,
UserResponseSchema,
UsersResponseSchema,
} from "@metis/identity/common";
import { QueryTypes } from "sequelize";
import { createApp } from "./app";
import { createDatabase, readDatabaseEnvironment } from "./config/database";
import { createIdentityRuntime } from "./infrastructure/http/identity-runtime";
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 users");
await database.query("DELETE FROM company_contacts");
await database.query("DELETE FROM company_sites");
await database.query("DELETE FROM companies");
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");
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);
assert.equal((await operator.request("/auth/logout", {})).status, 200);
assert.equal((await operator.session()).user, null);
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();
}
});