2026-10-12 18:31:21 +00:00
|
|
|
import assert from "node:assert/strict";
|
|
|
|
|
import { test } from "node:test";
|
|
|
|
|
|
|
|
|
|
import { UniqueID } from "@repo/rdx-ddd";
|
|
|
|
|
|
|
|
|
|
import type { SoftwareContext } from "../application/software-installation.repository";
|
|
|
|
|
import { mapSoftwareInput } from "../application/software-installation-input.mapper";
|
|
|
|
|
import { buildSoftwareResponse } from "../application/software-installation-response.builder";
|
|
|
|
|
import { SoftwareInstallationPersistenceMapper } from "../infrastructure/persistence/software-installation-persistence.mapper";
|
|
|
|
|
|
|
|
|
|
import { SoftwareInstallation } from "./software-installation.aggregate";
|
|
|
|
|
import type { SoftwareInstallationInput } from "./software-installation-values";
|
|
|
|
|
import { SoftwareInstallationInputSchema } from "./software-installation-values";
|
|
|
|
|
import { SoftwareInstallationValues } from "./value-objects/software-installation-values.vo";
|
|
|
|
|
|
|
|
|
|
const company = UniqueID.generateNewID();
|
|
|
|
|
const asset = UniqueID.generateNewID();
|
|
|
|
|
const resource = UniqueID.generateNewID();
|
|
|
|
|
const actor = UniqueID.generateNewID();
|
|
|
|
|
const date = "2026-10-12T10:00:00.000Z";
|
|
|
|
|
const input: SoftwareInstallationInput = {
|
|
|
|
|
assetId: asset.toPrimitive(),
|
|
|
|
|
resourceId: resource.toPrimitive(),
|
2026-10-13 07:30:28 +00:00
|
|
|
licenseResourceId: null,
|
2026-10-12 18:31:21 +00:00
|
|
|
version: "24H2",
|
|
|
|
|
state: "pending",
|
|
|
|
|
notes: null,
|
|
|
|
|
};
|
|
|
|
|
const context: SoftwareContext = {
|
|
|
|
|
configurations: [],
|
|
|
|
|
assets: [{ id: asset, available: true }],
|
2026-10-13 07:30:28 +00:00
|
|
|
resources: [{ id: resource, active: true, expiresOn: null, usage: "os" }],
|
2026-10-12 18:31:21 +00:00
|
|
|
actors: new Map([[actor.toPrimitive(), "David"]]),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
function values(data = input) {
|
|
|
|
|
const result = SoftwareInstallationValues.create(data);
|
|
|
|
|
|
|
|
|
|
assert.ok(result.isSuccess);
|
|
|
|
|
|
|
|
|
|
return result.data;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function installation() {
|
|
|
|
|
const result = SoftwareInstallation.create(company, values(), actor, date);
|
|
|
|
|
|
|
|
|
|
assert.ok(result.isSuccess);
|
|
|
|
|
|
|
|
|
|
return result.data;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
test("instalaciones: pertenecen al equipo, admiten SO/aplicaciones y rechazan recursos ajenos y destinos variables", () => {
|
|
|
|
|
assert.ok(mapSoftwareInput(input, context).isSuccess);
|
|
|
|
|
assert.ok(
|
|
|
|
|
mapSoftwareInput(input, {
|
|
|
|
|
...context,
|
2026-10-13 07:30:28 +00:00
|
|
|
resources: [{ id: resource, active: true, expiresOn: null, usage: "application" }],
|
2026-10-12 18:31:21 +00:00
|
|
|
}).isSuccess
|
|
|
|
|
);
|
|
|
|
|
assert.ok(!mapSoftwareInput(input, { ...context, assets: [] }).isSuccess);
|
|
|
|
|
assert.ok(
|
|
|
|
|
!mapSoftwareInput(input, {
|
|
|
|
|
...context,
|
2026-10-13 07:30:28 +00:00
|
|
|
resources: [{ id: resource, active: true, expiresOn: null, usage: "certificate" }],
|
2026-10-12 18:31:21 +00:00
|
|
|
}).isSuccess
|
|
|
|
|
);
|
|
|
|
|
assert.equal(
|
|
|
|
|
SoftwareInstallationInputSchema.safeParse({ ...input, profileId: actor.toPrimitive() }).success,
|
|
|
|
|
false
|
|
|
|
|
);
|
|
|
|
|
const previous = installation();
|
|
|
|
|
|
|
|
|
|
assert.ok(!mapSoftwareInput(input, { ...context, configurations: [previous] }).isSuccess);
|
|
|
|
|
assert.ok(
|
|
|
|
|
mapSoftwareInput(input, { ...context, configurations: [previous] }, previous).isSuccess
|
|
|
|
|
);
|
|
|
|
|
assert.ok(
|
|
|
|
|
!previous.update(values({ ...input, assetId: actor.toPrimitive() }), actor, date).isSuccess
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("instalaciones: versiones, idempotencia, retirada, revisión y persistencia de historial", () => {
|
|
|
|
|
const previous = installation();
|
|
|
|
|
|
|
|
|
|
assert.ok(previous.id.toPrimitive().charAt(14) === "7");
|
|
|
|
|
const same = previous.update(values(), actor, date);
|
|
|
|
|
|
|
|
|
|
assert.ok(same.isSuccess);
|
|
|
|
|
assert.equal(same.data, previous);
|
|
|
|
|
const configured = previous.update(
|
|
|
|
|
values({ ...input, state: "configured", version: "25H2" }),
|
|
|
|
|
actor,
|
|
|
|
|
date
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
assert.ok(configured.isSuccess);
|
|
|
|
|
const unavailable = { ...context, assets: [{ id: asset, available: false }] };
|
|
|
|
|
|
|
|
|
|
assert.ok(buildSoftwareResponse(configured.data, unavailable).needsReview);
|
|
|
|
|
assert.ok(
|
|
|
|
|
mapSoftwareInput(
|
|
|
|
|
{ ...configured.data.values.toPrimitive(), notes: "Revisar equipo" },
|
|
|
|
|
unavailable,
|
|
|
|
|
configured.data
|
|
|
|
|
).isSuccess
|
|
|
|
|
);
|
|
|
|
|
assert.ok(!mapSoftwareInput(input, unavailable).isSuccess);
|
|
|
|
|
assert.ok(!configured.data.update(values({ ...input, state: "removed" }), actor, date).isSuccess);
|
|
|
|
|
const retiring = configured.data.update(
|
|
|
|
|
values({ ...input, state: "removal_pending" }),
|
|
|
|
|
actor,
|
|
|
|
|
date
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
assert.ok(retiring.isSuccess);
|
|
|
|
|
const removed = retiring.data.update(values({ ...input, state: "removed" }), actor, date);
|
|
|
|
|
|
|
|
|
|
assert.ok(removed.isSuccess);
|
|
|
|
|
assert.ok(!removed.data.update(values(), actor, date).isSuccess);
|
|
|
|
|
assert.ok(mapSoftwareInput(input, { ...context, configurations: [removed.data] }).isSuccess);
|
|
|
|
|
const mapper = new SoftwareInstallationPersistenceMapper();
|
|
|
|
|
const rows = mapper.toPersistence(removed.data);
|
|
|
|
|
const restored = mapper.toDomain(rows.root, rows.revisions);
|
|
|
|
|
|
|
|
|
|
assert.equal(restored.history.length, 4);
|
|
|
|
|
assert.ok(restored.values.equals(removed.data.values));
|
|
|
|
|
assert.throws(() => mapper.toDomain({ ...rows.root, currentKey: "incorrect" }, rows.revisions));
|
|
|
|
|
assert.throws(() =>
|
|
|
|
|
mapper.toDomain(
|
|
|
|
|
rows.root,
|
|
|
|
|
rows.revisions.map((row) => ({ ...row, position: 9 }))
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
});
|
2026-10-13 07:30:28 +00:00
|
|
|
|
|
|
|
|
test("licencia del software: opcional, misma compañía y tipo, vigencia, desvinculación e historial idempotente", () => {
|
|
|
|
|
const license = UniqueID.generateNewID();
|
|
|
|
|
const licensedInput = { ...input, licenseResourceId: license.toPrimitive() };
|
|
|
|
|
const licenseItem = { id: license, active: true, usage: "license" as const, expiresOn: null };
|
|
|
|
|
const licensedContext = { ...context, resources: [...context.resources, licenseItem] };
|
|
|
|
|
|
|
|
|
|
assert.ok(mapSoftwareInput(licensedInput, licensedContext).isSuccess);
|
|
|
|
|
assert.ok(!mapSoftwareInput(licensedInput, context).isSuccess);
|
|
|
|
|
assert.ok(
|
|
|
|
|
!mapSoftwareInput({ ...input, licenseResourceId: resource.toPrimitive() }, context).isSuccess
|
|
|
|
|
);
|
|
|
|
|
assert.equal(
|
|
|
|
|
SoftwareInstallationInputSchema.safeParse({ ...input, licenseResourceId: "invalid" }).success,
|
|
|
|
|
false
|
|
|
|
|
);
|
|
|
|
|
const previous = installation();
|
|
|
|
|
const linked = previous.update(values(licensedInput), actor, date);
|
|
|
|
|
|
|
|
|
|
assert.ok(linked.isSuccess);
|
|
|
|
|
assert.equal(linked.data.history.length, 2);
|
|
|
|
|
assert.equal(linked.data.history[0]?.values.toPrimitive().licenseResourceId, null);
|
|
|
|
|
assert.equal(
|
|
|
|
|
linked.data.history[1]?.values.toPrimitive().licenseResourceId,
|
|
|
|
|
license.toPrimitive()
|
|
|
|
|
);
|
|
|
|
|
const same = linked.data.update(values(licensedInput), actor, date);
|
|
|
|
|
|
|
|
|
|
assert.ok(same.isSuccess);
|
|
|
|
|
assert.equal(same.data, linked.data);
|
|
|
|
|
for (const unavailable of [
|
|
|
|
|
{ ...licenseItem, active: false },
|
|
|
|
|
{ ...licenseItem, expiresOn: "2000-01-01" },
|
|
|
|
|
]) {
|
|
|
|
|
const unavailableContext = { ...context, resources: [...context.resources, unavailable] };
|
|
|
|
|
|
|
|
|
|
assert.ok(!mapSoftwareInput(licensedInput, unavailableContext).isSuccess);
|
|
|
|
|
assert.ok(buildSoftwareResponse(linked.data, unavailableContext).needsReview);
|
|
|
|
|
assert.ok(
|
|
|
|
|
mapSoftwareInput(
|
|
|
|
|
{ ...licensedInput, notes: "Revisar contrato" },
|
|
|
|
|
unavailableContext,
|
|
|
|
|
linked.data
|
|
|
|
|
).isSuccess
|
|
|
|
|
);
|
|
|
|
|
assert.ok(mapSoftwareInput(input, unavailableContext, linked.data).isSuccess);
|
|
|
|
|
assert.ok(
|
|
|
|
|
mapSoftwareInput(
|
|
|
|
|
input,
|
|
|
|
|
{ ...unavailableContext, assets: [{ id: asset, available: false }] },
|
|
|
|
|
linked.data
|
|
|
|
|
).isSuccess
|
|
|
|
|
);
|
|
|
|
|
assert.ok(
|
|
|
|
|
!mapSoftwareInput(
|
|
|
|
|
{ ...licensedInput, state: "removal_pending" },
|
|
|
|
|
unavailableContext,
|
|
|
|
|
previous
|
|
|
|
|
).isSuccess
|
|
|
|
|
);
|
|
|
|
|
assert.ok(
|
|
|
|
|
mapSoftwareInput(
|
|
|
|
|
{ ...licensedInput, state: "never_configured" },
|
|
|
|
|
unavailableContext,
|
|
|
|
|
linked.data
|
|
|
|
|
).isSuccess
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
const detached = linked.data.update(values(input), actor, date);
|
|
|
|
|
|
|
|
|
|
assert.ok(detached.isSuccess);
|
|
|
|
|
assert.equal(detached.data.history.length, 3);
|
|
|
|
|
const mapper = new SoftwareInstallationPersistenceMapper();
|
|
|
|
|
const stored = mapper.toPersistence(detached.data);
|
|
|
|
|
const restored = mapper.toDomain(stored.root, stored.revisions);
|
|
|
|
|
|
|
|
|
|
assert.equal(restored.values.toPrimitive().licenseResourceId, null);
|
|
|
|
|
assert.equal(restored.history[1]?.values.toPrimitive().licenseResourceId, license.toPrimitive());
|
|
|
|
|
assert.throws(() =>
|
|
|
|
|
mapper.toDomain({ ...stored.root, licenseResourceId: license.toPrimitive() }, stored.revisions)
|
|
|
|
|
);
|
|
|
|
|
});
|