e6f4ae470b
- templates custom/bootstrap-root + identity-network (sortie structurée parsable, sauvegardes, échec contrôlé, jamais de coupure réseau sans reconnexion) - postInstall: registre de manifestes (champs typés + defaults/defaultFrom), validateProfileValues + maskSecretValues + buildPostInstallResult (TDD), renderProfile/previewProfile (masquage secrets), runPostInstall (SSH) - execute: RunActionOpts.profileId/values + branche post_install (bloc postInstall) - action_requests: post_install accepté, payload profileId/values transmis à approve - routes: GET /profiles, POST .../preview (script masqué + validation), POST .../run (action_request si requiresConfirmation, sinon direct) Champs = formulaire (pas de question SSH interactive) ; secrets jamais sérialisés ; identity_network exige confirmation. tsc 0 · 101 tests · build OK · boot OK. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
110 lines
3.5 KiB
TypeScript
110 lines
3.5 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import {
|
|
PROFILES,
|
|
validateProfileValues,
|
|
maskSecretValues,
|
|
buildPostInstallResult,
|
|
type ProfileManifest,
|
|
} from "./postInstall.js";
|
|
|
|
describe("validateProfileValues", () => {
|
|
const identity = PROFILES.identity_network!;
|
|
|
|
it("échoue si un champ requis manque", () => {
|
|
const r = validateProfileValues(identity, { newHostname: "srv1" });
|
|
expect(r.ok).toBe(false);
|
|
expect(r.errors.some((e) => e.field === "interfaceName")).toBe(true);
|
|
});
|
|
|
|
it("échoue sur une IP/CIDR invalide", () => {
|
|
const r = validateProfileValues(identity, {
|
|
newHostname: "srv1",
|
|
domain: "home",
|
|
interfaceName: "eth0",
|
|
staticAddress: "999.1.1.1/24",
|
|
gateway: "10.0.0.1",
|
|
dnsNameservers: "10.0.0.1",
|
|
reconnectHost: "10.0.0.50",
|
|
});
|
|
expect(r.ok).toBe(false);
|
|
expect(r.errors.some((e) => e.field === "staticAddress")).toBe(true);
|
|
});
|
|
|
|
it("passe avec des valeurs valides", () => {
|
|
const r = validateProfileValues(identity, {
|
|
newHostname: "srv1",
|
|
domain: "home",
|
|
interfaceName: "eth0",
|
|
staticAddress: "10.0.0.50/22",
|
|
gateway: "10.0.0.1",
|
|
dnsNameservers: "10.0.0.1 10.0.0.10",
|
|
reconnectHost: "10.0.0.50",
|
|
});
|
|
expect(r.ok).toBe(true);
|
|
expect(r.errors).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
describe("maskSecretValues", () => {
|
|
const manifest: ProfileManifest = {
|
|
id: "x",
|
|
label: "x",
|
|
description: "",
|
|
risk: "low",
|
|
requiresConfirmation: false,
|
|
template: "custom/bootstrap-root.sh.tpl",
|
|
fields: [
|
|
{ name: "user", type: "string", required: true },
|
|
{ name: "token", type: "secret", required: true },
|
|
],
|
|
};
|
|
|
|
it("masque les champs secret et conserve les autres", () => {
|
|
const masked = maskSecretValues(manifest, { user: "gilles", token: "s3cr3t-ABC" });
|
|
expect(masked.user).toBe("gilles");
|
|
expect(masked.token).toBe("********");
|
|
expect(JSON.stringify(masked)).not.toContain("s3cr3t");
|
|
});
|
|
});
|
|
|
|
describe("buildPostInstallResult", () => {
|
|
const raw = [
|
|
"===SU:CUSTOM_IDENTITY===",
|
|
"FILE_MODIFIED=/etc/hosts",
|
|
"FILE_MODIFIED=/etc/network/interfaces",
|
|
"OLD_ENDPOINT=10.0.0.99",
|
|
"HOSTNAME_SET=srv1",
|
|
"ERR=interface_not_found",
|
|
"NEW_ENDPOINT=10.0.0.50",
|
|
"RECONNECT_REQUIRED=1",
|
|
"REBOOT_REQUESTED=1",
|
|
"===SU:EXIT=0===",
|
|
].join("\n");
|
|
|
|
it("extrait fichiers modifiés, reboot, changement réseau et erreurs", () => {
|
|
const r = buildPostInstallResult(raw, ["identity_network"], { newHostname: "srv1" });
|
|
expect(r.profilesRun).toEqual(["identity_network"]);
|
|
expect(r.filesModified).toContain("/etc/hosts");
|
|
expect(r.filesModified).toContain("/etc/network/interfaces");
|
|
expect(r.rebootsRequested).toBe(true);
|
|
expect(r.networkChange).toEqual({ oldEndpoint: "10.0.0.99", newEndpoint: "10.0.0.50", reconnectHost: "10.0.0.50" });
|
|
expect(r.errors?.some((e) => e.kind === "interface_not_found")).toBe(true);
|
|
expect(r.variablesUsed.newHostname).toBe("srv1");
|
|
});
|
|
|
|
it("parse les paquets installés du bootstrap", () => {
|
|
const boot = [
|
|
"===SU:CUSTOM_BOOTSTRAP===",
|
|
"PKG_INSTALLED=sudo",
|
|
"PKG_INSTALLED=curl",
|
|
"GROUP_ADDED=sudo:gilles",
|
|
"SUDO_OK=1",
|
|
"===SU:EXIT=0===",
|
|
].join("\n");
|
|
const r = buildPostInstallResult(boot, ["bootstrap_root"], { operatorUser: "gilles" });
|
|
expect(r.packagesInstalled).toEqual(["sudo", "curl"]);
|
|
expect(r.rebootsRequested).toBe(false);
|
|
expect(r.errors ?? []).toHaveLength(0);
|
|
});
|
|
});
|