242 lines
7.4 KiB
TypeScript
242 lines
7.4 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { test } from "node:test";
|
|
|
|
import { UniqueID } from "@repo/rdx-ddd";
|
|
import { normalizeSearchText } from "@repo/rdx-utils";
|
|
|
|
import {
|
|
CreateGlobalApplicationRequestSchema,
|
|
ListApplicationCandidatesRequestSchema,
|
|
ListApplicationCandidatesResponseSchema,
|
|
ResolveApplicationReferencesRequestSchema,
|
|
ResolveApplicationReferencesResponseSchema,
|
|
SoftwareReferenceSchema,
|
|
} from "../../common";
|
|
|
|
import { globalApplicationTemplateEligible } from "./global-application-template-policy";
|
|
import { ResourceGlobalGatesSchema, globalResourcePurposeEnabled } from "./resource-global-gates";
|
|
import { paginateResourceMetadata } from "./resource-reference-pagination";
|
|
|
|
const id = () => UniqueID.generateNewID().toPrimitive();
|
|
|
|
test("elegibilidad global exige serie exacta y campos globales; las copias no elevan ámbito", () => {
|
|
const template = {
|
|
kind: "template",
|
|
stockKey: "template:application",
|
|
companyId: null,
|
|
hasLocalFields: false,
|
|
};
|
|
|
|
assert.equal(globalApplicationTemplateEligible(template), true);
|
|
for (const change of [
|
|
{ stockKey: null },
|
|
{ stockKey: "template:os" },
|
|
{ stockKey: "template:software_license" },
|
|
{ stockKey: "template:APPLICATION" },
|
|
{ kind: "field" },
|
|
{ companyId: id() },
|
|
{ hasLocalFields: true },
|
|
]) {
|
|
assert.equal(globalApplicationTemplateEligible({ ...template, ...change }), false);
|
|
}
|
|
});
|
|
|
|
test("schemas discriminan owner, propósito, OS local y proyecciones privadas", () => {
|
|
const reference = {
|
|
id: id(),
|
|
templateId: id(),
|
|
name: "Aplicación",
|
|
active: true,
|
|
kind: "application",
|
|
scope: "global",
|
|
companyId: null,
|
|
};
|
|
|
|
assert.equal(SoftwareReferenceSchema.safeParse(reference).success, true);
|
|
for (const change of [
|
|
{ companyId: id() },
|
|
{ kind: "os" },
|
|
{ notes: "Privado" },
|
|
{ history: [] },
|
|
{ scope: "GLOBAL" },
|
|
]) {
|
|
assert.equal(SoftwareReferenceSchema.safeParse({ ...reference, ...change }).success, false);
|
|
}
|
|
const os = { ...reference, kind: "os", scope: "company", companyId: id() };
|
|
const payload = { items: [{ id: os.id, status: "resolved", reference: os }] };
|
|
|
|
assert.equal(
|
|
ResolveApplicationReferencesResponseSchema.safeParse({
|
|
...payload,
|
|
purpose: "software-installation",
|
|
}).success,
|
|
true
|
|
);
|
|
assert.equal(
|
|
ResolveApplicationReferencesResponseSchema.safeParse({
|
|
...payload,
|
|
purpose: "application-configuration",
|
|
}).success,
|
|
false
|
|
);
|
|
assert.equal(
|
|
ListApplicationCandidatesResponseSchema.safeParse({
|
|
purpose: "application-configuration",
|
|
items: [{ ...os, provider: null }],
|
|
nextCursor: null,
|
|
}).success,
|
|
false
|
|
);
|
|
});
|
|
|
|
test("requests acotan lotes, consulta, paginación y rechazan privilegios o integración", () => {
|
|
const ids = Array.from({ length: 101 }, id);
|
|
const query = { purpose: "software-installation" };
|
|
|
|
for (const input of [[], ids, [ids[0], ids[0]], ["invalid"]]) {
|
|
assert.equal(
|
|
ResolveApplicationReferencesRequestSchema.safeParse({ ...query, ids: input }).success,
|
|
false
|
|
);
|
|
}
|
|
assert.equal(
|
|
ResolveApplicationReferencesRequestSchema.safeParse({ ...query, ids: ids.slice(0, 100) })
|
|
.success,
|
|
true
|
|
);
|
|
for (const extra of [
|
|
{ pageSize: 0 },
|
|
{ pageSize: 101 },
|
|
{ search: "a".repeat(101) },
|
|
{ cursor: "a".repeat(2049) },
|
|
{ purpose: "remote" },
|
|
{ canManageGlobalResources: true },
|
|
]) {
|
|
assert.equal(
|
|
ListApplicationCandidatesRequestSchema.safeParse({ ...query, ...extra }).success,
|
|
false
|
|
);
|
|
}
|
|
const create = {
|
|
creationId: id(),
|
|
values: { name: "Producto", templateId: id(), active: true, notes: "", characteristics: {} },
|
|
};
|
|
|
|
assert.equal(CreateGlobalApplicationRequestSchema.safeParse(create).success, true);
|
|
for (const extra of [
|
|
{ scope: "global" },
|
|
{ companyId: null },
|
|
{ isInitialAdmin: true },
|
|
{ actorId: id() },
|
|
]) {
|
|
assert.equal(
|
|
CreateGlobalApplicationRequestSchema.safeParse({ ...create, ...extra }).success,
|
|
false
|
|
);
|
|
}
|
|
assert.equal(
|
|
CreateGlobalApplicationRequestSchema.safeParse({
|
|
...create,
|
|
values: { ...create.values, integration: {} },
|
|
}).success,
|
|
false
|
|
);
|
|
});
|
|
|
|
test("búsqueda literal conserva ñ, puntuación y espacios; pagina tras filtrar con desempate UUID", () => {
|
|
assert.equal(normalizeSearchText(" ÁÉÏõÜ Ñ %_ "), "aeiou ñ %_");
|
|
const rows = Array.from({ length: 51 }, (_, index) => ({
|
|
id: id(),
|
|
name: index < 25 ? "Aplicación Ágil %_" : "Aplicación Ágil",
|
|
provider: "Fábrica",
|
|
}));
|
|
const base = {
|
|
search: "%_",
|
|
pageSize: 7,
|
|
sort: "name",
|
|
direction: "asc",
|
|
context: "company-A/software",
|
|
} as const;
|
|
const seen = new Set<string>();
|
|
let cursor: string | undefined;
|
|
|
|
do {
|
|
const page = paginateResourceMetadata(rows, { ...base, cursor });
|
|
|
|
assert.ok(page.isSuccess);
|
|
for (const item of page.data.items) {
|
|
assert.equal(seen.has(item.id), false);
|
|
seen.add(item.id);
|
|
}
|
|
cursor = page.data.nextCursor ?? undefined;
|
|
if (cursor) {
|
|
assert.equal(
|
|
paginateResourceMetadata(rows, { ...base, cursor, context: "company-B/software" })
|
|
.isSuccess,
|
|
false
|
|
);
|
|
assert.equal(
|
|
paginateResourceMetadata(rows, { ...base, cursor, search: "otra" }).isSuccess,
|
|
false
|
|
);
|
|
}
|
|
} while (cursor);
|
|
assert.equal(seen.size, 25);
|
|
assert.equal(paginateResourceMetadata(rows, { ...base, cursor: "corrupto" }).isSuccess, false);
|
|
});
|
|
|
|
test("R2.5 admite gates configurables; los valores predeterminados siguen cerrados", () => {
|
|
const gates = {
|
|
catalog: false,
|
|
administration: false,
|
|
mixedSelection: false,
|
|
software: false,
|
|
profile: false,
|
|
};
|
|
|
|
assert.equal(ResourceGlobalGatesSchema.safeParse(gates).success, true);
|
|
assert.equal(
|
|
ResourceGlobalGatesSchema.safeParse({ ...gates, catalog: true, administration: true }).success,
|
|
true
|
|
);
|
|
assert.equal(ResourceGlobalGatesSchema.safeParse({ ...gates, profile: true }).success, true);
|
|
});
|
|
|
|
test("R2.5: software y perfil se habilitan por separado y requieren catálogo", () => {
|
|
const open = {
|
|
catalog: true,
|
|
administration: true,
|
|
mixedSelection: true,
|
|
software: true,
|
|
profile: false,
|
|
} as const;
|
|
|
|
assert.ok(ResourceGlobalGatesSchema.safeParse(open).success);
|
|
assert.equal(globalResourcePurposeEnabled("software-installation", open), true);
|
|
assert.equal(
|
|
globalResourcePurposeEnabled("software-installation", { ...open, catalog: false }),
|
|
false
|
|
);
|
|
assert.equal(globalResourcePurposeEnabled("application-configuration", open), false);
|
|
assert.ok(ResourceGlobalGatesSchema.safeParse({ ...open, mixedSelection: false }).success);
|
|
assert.ok(ResourceGlobalGatesSchema.safeParse({ ...open, software: false }).success);
|
|
assert.equal(ResourceGlobalGatesSchema.safeParse({ ...open, profile: true }).success, true);
|
|
});
|
|
|
|
test("R2.5: perfil no depende de software ni abre asociaciones con catálogo cerrado", () => {
|
|
const profileOnly = {
|
|
catalog: true,
|
|
administration: false,
|
|
mixedSelection: false,
|
|
software: false,
|
|
profile: true,
|
|
};
|
|
|
|
assert.equal(globalResourcePurposeEnabled("application-configuration", profileOnly), true);
|
|
assert.equal(globalResourcePurposeEnabled("software-installation", profileOnly), false);
|
|
assert.equal(
|
|
globalResourcePurposeEnabled("application-configuration", { ...profileOnly, catalog: false }),
|
|
false
|
|
);
|
|
});
|