2026-10-06 15:15:42 +00:00
|
|
|
import assert from "node:assert/strict";
|
|
|
|
|
import { randomUUID } from "node:crypto";
|
|
|
|
|
|
|
|
|
|
import { InventoryResponseSchema as AssetsSchema } from "@metis/assets/common";
|
2026-10-08 14:43:34 +00:00
|
|
|
import type { ApplicationConfigurationInput } from "@metis/companies/common";
|
2026-10-06 15:15:42 +00:00
|
|
|
import {
|
|
|
|
|
ApplicationConfigurationResponseSchema,
|
|
|
|
|
ApplicationConfigurationsResponseSchema,
|
|
|
|
|
CompanyResponseSchema,
|
|
|
|
|
WorkProfileResponseSchema,
|
|
|
|
|
} from "@metis/companies/common";
|
|
|
|
|
import { CatalogResponseSchema, ResourceResponseSchema } from "@metis/resources/common";
|
|
|
|
|
import type { Sequelize } from "sequelize";
|
|
|
|
|
import { z } from "zod/v4";
|
|
|
|
|
|
|
|
|
|
type Request = (path: string, body?: unknown, origin?: string, token?: string) => Promise<Response>;
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-06 15:15:42 +00:00
|
|
|
export async function verifyApplications(
|
|
|
|
|
request: Request,
|
|
|
|
|
companyId: string,
|
|
|
|
|
otherId: string,
|
|
|
|
|
database: Sequelize
|
|
|
|
|
) {
|
|
|
|
|
const personResponse = await request(`/companies/${companyId}/people`, {
|
|
|
|
|
name: "Persona aplicaciones",
|
|
|
|
|
position: null,
|
|
|
|
|
email: null,
|
|
|
|
|
phone: null,
|
|
|
|
|
active: true,
|
|
|
|
|
isContact: false,
|
|
|
|
|
});
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-06 15:15:42 +00:00
|
|
|
assert.equal(personResponse.status, 201);
|
|
|
|
|
const person = CompanyResponseSchema.parse(await personResponse.json()).company.people.at(-1);
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-06 15:15:42 +00:00
|
|
|
assert.ok(person);
|
|
|
|
|
const assets = AssetsSchema.parse(await (await request(`/assets/${companyId}`)).json());
|
|
|
|
|
const host = assets.assets.find((item) => item.values.status !== "retired");
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-06 15:15:42 +00:00
|
|
|
assert.ok(host);
|
|
|
|
|
const profileValues = {
|
|
|
|
|
name: "Perfil aplicaciones",
|
|
|
|
|
personId: person.id,
|
|
|
|
|
assetId: host.id,
|
|
|
|
|
accountId: null,
|
|
|
|
|
active: true,
|
|
|
|
|
notes: null,
|
|
|
|
|
};
|
|
|
|
|
const profileResponse = await request(`/work-profiles/${companyId}`, { values: profileValues });
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-06 15:15:42 +00:00
|
|
|
assert.equal(profileResponse.status, 201);
|
|
|
|
|
const profile = WorkProfileResponseSchema.parse(await profileResponse.json()).profile;
|
|
|
|
|
const catalog = CatalogResponseSchema.parse(
|
|
|
|
|
await (await request(`/resource-catalog/${companyId}`)).json()
|
|
|
|
|
);
|
|
|
|
|
const template = catalog.items.find((item) => item.builtinKey === "template:application");
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-06 15:15:42 +00:00
|
|
|
assert.ok(template);
|
|
|
|
|
assert.equal(template.values.kind, "template");
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-06 15:15:42 +00:00
|
|
|
const add = async (templateId: string, name: string) => {
|
|
|
|
|
const response = await request(`/resources/${companyId}`, {
|
|
|
|
|
values: { name, templateId, active: true, notes: "", characteristics: {} },
|
|
|
|
|
});
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-06 15:15:42 +00:00
|
|
|
assert.equal(response.status, 201);
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-06 15:15:42 +00:00
|
|
|
return ResourceResponseSchema.parse(await response.json()).resource;
|
|
|
|
|
};
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-06 15:15:42 +00:00
|
|
|
const app = await add(template.id, "Thunderbird prueba");
|
|
|
|
|
const certificateTemplate = catalog.items.find(
|
|
|
|
|
(item) => item.builtinKey === "template:certificate"
|
|
|
|
|
);
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-06 15:15:42 +00:00
|
|
|
assert.ok(certificateTemplate);
|
|
|
|
|
const certificate = await add(certificateTemplate.id, "Certificado prueba aplicaciones");
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-06 15:15:42 +00:00
|
|
|
assert.ok(template.values.kind === "template");
|
|
|
|
|
const customValues = {
|
|
|
|
|
...template.values,
|
|
|
|
|
name: "Software personalizado",
|
|
|
|
|
usage: "application" as const,
|
|
|
|
|
};
|
|
|
|
|
const customResponse = await request(`/resource-catalog/${companyId}`, { values: customValues });
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-06 15:15:42 +00:00
|
|
|
assert.equal(customResponse.status, 201);
|
|
|
|
|
const custom = CatalogResponseSchema.parse(
|
|
|
|
|
await (await request(`/resource-catalog/${companyId}`)).json()
|
|
|
|
|
).items.find((item) => item.values.name === customValues.name);
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-06 15:15:42 +00:00
|
|
|
assert.ok(custom);
|
|
|
|
|
const customApp = await add(custom.id, "ERP navegador prueba");
|
|
|
|
|
const immutable = await request(`/resource-catalog/${companyId}/${custom.id}/update`, {
|
|
|
|
|
values: { ...customValues, usage: "certificate" },
|
|
|
|
|
});
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-06 15:15:42 +00:00
|
|
|
assert.equal(immutable.status, 409);
|
|
|
|
|
const path = `/application-configurations/${companyId}`;
|
|
|
|
|
const input: ApplicationConfigurationInput = {
|
|
|
|
|
profileId: profile.id,
|
|
|
|
|
resourceId: app.id,
|
|
|
|
|
delivery: "installed",
|
|
|
|
|
version: null,
|
|
|
|
|
state: "pending",
|
|
|
|
|
notes: null,
|
|
|
|
|
};
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-06 15:15:42 +00:00
|
|
|
const save = async (values: ApplicationConfigurationInput, id?: string) => {
|
|
|
|
|
const response = await request(path + (id ? `/${id}/update` : ""), { values });
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-06 15:15:42 +00:00
|
|
|
assert.equal(response.status, id ? 200 : 201);
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-06 15:15:42 +00:00
|
|
|
return ApplicationConfigurationResponseSchema.parse(await response.json()).configuration;
|
|
|
|
|
};
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-06 15:15:42 +00:00
|
|
|
const error = async (values: unknown, code: string, id?: string, status = 409) => {
|
|
|
|
|
const response = await request(path + (id ? `/${id}/update` : ""), { values });
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-06 15:15:42 +00:00
|
|
|
assert.equal(response.status, status);
|
|
|
|
|
assert.equal(z.object({ code: z.string() }).parse(await response.json()).code, code);
|
|
|
|
|
};
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-06 15:15:42 +00:00
|
|
|
await error({ ...input, actorId: person.id }, "INVALID_REQUEST", undefined, 400);
|
|
|
|
|
await error(
|
|
|
|
|
{ ...input, resourceId: randomUUID() },
|
|
|
|
|
"APPLICATION_REFERENCE_NOT_FOUND",
|
|
|
|
|
undefined,
|
|
|
|
|
404
|
|
|
|
|
);
|
|
|
|
|
await error(
|
|
|
|
|
{ ...input, resourceId: certificate.id },
|
|
|
|
|
"APPLICATION_REFERENCE_NOT_FOUND",
|
|
|
|
|
undefined,
|
|
|
|
|
404
|
|
|
|
|
);
|
|
|
|
|
await error({ ...input, delivery: "web", version: "1" }, "INVALID_REQUEST", undefined, 400);
|
|
|
|
|
assert.equal((await request(path, { values: input }, "https://other.example")).status, 403);
|
|
|
|
|
assert.equal((await request(path, { values: input }, undefined, "a".repeat(64))).status, 403);
|
|
|
|
|
const attempts = await Promise.all([
|
|
|
|
|
request(path, { values: input }),
|
|
|
|
|
request(path, { values: input }),
|
|
|
|
|
]);
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-06 15:15:42 +00:00
|
|
|
assert.deepEqual(attempts.map((r) => r.status).sort(), [201, 409]);
|
|
|
|
|
const response = attempts.find((r) => r.status === 201);
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-06 15:15:42 +00:00
|
|
|
assert.ok(response);
|
|
|
|
|
const created = ApplicationConfigurationResponseSchema.parse(await response.json()).configuration;
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-06 15:15:42 +00:00
|
|
|
assert.equal(created.id[14], "7");
|
|
|
|
|
assert.equal(created.history[0]?.id[14], "7");
|
|
|
|
|
assert.deepEqual(await save(input, created.id), created);
|
|
|
|
|
const configured = await save(
|
|
|
|
|
{ ...input, state: "configured", version: "128", notes: "Solicitado por Ana" },
|
|
|
|
|
created.id
|
|
|
|
|
);
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-06 15:15:42 +00:00
|
|
|
assert.equal(configured.history.length, 2);
|
|
|
|
|
assert.equal(configured.history[0]?.values.version, null);
|
|
|
|
|
await save({ ...input, resourceId: customApp.id, delivery: "web", state: "configured" });
|
|
|
|
|
await error({ ...input, resourceId: customApp.id }, "APPLICATION_TARGET_IMMUTABLE", created.id);
|
|
|
|
|
await error({ ...input, state: "removed" }, "INVALID_APPLICATION_TRANSITION", created.id);
|
|
|
|
|
await database.query("UPDATE application_configurations SET currentKey=NULL WHERE id=:id", {
|
|
|
|
|
replacements: { id: created.id },
|
|
|
|
|
});
|
|
|
|
|
try {
|
|
|
|
|
assert.equal((await request(path)).status, 500);
|
|
|
|
|
} finally {
|
|
|
|
|
await database.query("UPDATE application_configurations SET currentKey=:key WHERE id=:id", {
|
|
|
|
|
replacements: { id: created.id, key: `${profile.id}:${app.id}` },
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
const deactivate = await request(`/resources/${companyId}/${app.id}/update`, {
|
|
|
|
|
values: { ...app.values, active: false },
|
|
|
|
|
});
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-06 15:15:42 +00:00
|
|
|
assert.equal(deactivate.status, 200);
|
|
|
|
|
const list = ApplicationConfigurationsResponseSchema.parse(
|
|
|
|
|
await (await request(path)).json()
|
|
|
|
|
).configurations;
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-06 15:15:42 +00:00
|
|
|
assert.equal(list.find((item) => item.id === created.id)?.needsReview, true);
|
|
|
|
|
await error(
|
|
|
|
|
{ ...configured.values, version: "129" },
|
|
|
|
|
"APPLICATION_REFERENCE_UNAVAILABLE",
|
|
|
|
|
created.id
|
|
|
|
|
);
|
|
|
|
|
const noted = await save({ ...configured.values, notes: "Retirar al salir" }, created.id);
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-06 15:15:42 +00:00
|
|
|
assert.equal(noted.history.length, 3);
|
|
|
|
|
const removal = await save({ ...noted.values, state: "removal_pending" }, created.id);
|
|
|
|
|
const removed = await save({ ...removal.values, state: "removed" }, created.id);
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-06 15:15:42 +00:00
|
|
|
assert.equal(removed.needsReview, false);
|
|
|
|
|
await error({ ...removed.values, notes: "Otra" }, "INVALID_APPLICATION_TRANSITION", created.id);
|
|
|
|
|
assert.equal(
|
|
|
|
|
(
|
|
|
|
|
await request(`/application-configurations/${otherId}/${created.id}/update`, {
|
|
|
|
|
values: input,
|
|
|
|
|
})
|
|
|
|
|
).status,
|
|
|
|
|
404
|
|
|
|
|
);
|
|
|
|
|
assert.equal(
|
|
|
|
|
ApplicationConfigurationsResponseSchema.parse(
|
|
|
|
|
await (await request(`/application-configurations/${otherId}`)).json()
|
|
|
|
|
).configurations.length,
|
|
|
|
|
0
|
|
|
|
|
);
|
|
|
|
|
// Un recurso compartido por perfiles mantiene configuraciones independientes.
|
|
|
|
|
const secondPersonResponse = await request(`/companies/${companyId}/people`, {
|
|
|
|
|
name: "Otra persona aplicaciones",
|
|
|
|
|
position: null,
|
|
|
|
|
email: null,
|
|
|
|
|
phone: null,
|
|
|
|
|
active: true,
|
|
|
|
|
isContact: false,
|
|
|
|
|
});
|
|
|
|
|
const secondPerson = CompanyResponseSchema.parse(
|
|
|
|
|
await secondPersonResponse.json()
|
|
|
|
|
).company.people.at(-1);
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-06 15:15:42 +00:00
|
|
|
assert.ok(secondPerson);
|
|
|
|
|
const secondProfile = await request(`/work-profiles/${companyId}`, {
|
|
|
|
|
values: { ...profileValues, name: "Otro perfil aplicaciones", personId: secondPerson.id },
|
|
|
|
|
});
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-06 15:15:42 +00:00
|
|
|
assert.equal(secondProfile.status, 201);
|
|
|
|
|
const secondId = WorkProfileResponseSchema.parse(await secondProfile.json()).profile.id;
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-06 15:15:42 +00:00
|
|
|
await save({ ...input, profileId: secondId, resourceId: customApp.id, delivery: "web" });
|
|
|
|
|
const archive = await request(`/work-profiles/${companyId}/${secondId}/update`, {
|
|
|
|
|
values: {
|
|
|
|
|
...profileValues,
|
|
|
|
|
name: "Otro perfil aplicaciones",
|
|
|
|
|
personId: secondPerson.id,
|
|
|
|
|
active: false,
|
|
|
|
|
},
|
|
|
|
|
});
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-06 15:15:42 +00:00
|
|
|
assert.equal(archive.status, 200);
|
|
|
|
|
const reviewed = ApplicationConfigurationsResponseSchema.parse(
|
|
|
|
|
await (await request(path)).json()
|
|
|
|
|
).configurations;
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-06 15:15:42 +00:00
|
|
|
assert.equal(reviewed.find((item) => item.values.profileId === secondId)?.needsReview, true);
|
|
|
|
|
}
|