Files
system_update/server/services/dockerPull.test.ts
T
gilles b1c81ba518 feat(docker): pull-check + comparaison déterministe par stack (tâche 2 SJ-5)
- template docker/pull-check.sh.tpl (pull sans up, inspect before/after)
- dockerPull: parseDockerPullCheck + buildDockerPullResult (TDD) — compare
  image id/digest/label OCI → services up_to_date|updates_available|error,
  changes operation=pulled ; erreurs registry nettoyées (URL/token/password)
- dockerDedupKey (digests prioritaires, fallback image ids) + DockerImageChange.dedupKey
- pullCheckStack: SSH + upsert docker_stack_services, refuse stack non enabled,
  refresh Docker séparé (hors refreshMachine, pas de pull auto)
- execute: runAction(opts.stackId), branche docker_pull_check, injection stackDir
  (corrige docker_inspect_current) ; route: allowlist Docker passifs + pull_check,
  destructives toujours hors API jusqu'à action_requests (SJ-6)

Pas de migration (schéma SJ-4 suffisant). tsc 0 erreur · 85 tests · build OK.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 21:02:38 +02:00

106 lines
4.0 KiB
TypeScript

import { describe, it, expect } from "vitest";
import { parseDockerPullCheck, buildDockerPullResult, dockerDedupKey } from "./dockerPull.js";
// Stack à 3 images : nginx inchangé, app mis à jour, redis en échec d'auth registry.
const RAW = [
"===SU:DOCKER_INSPECT_BEFORE===",
"BEFORE\tnginx:latest\tsha256:aaa\tnginx@sha256:d1",
"BEFORE\tmyorg/app:latest\tsha256:bbb\tmyorg/app@sha256:d2",
"BEFORE\tredis:7\tsha256:ccc\tredis@sha256:d3",
"===SU:DOCKER_PULL===",
"nginx Pulling ",
"nginx Pull complete ",
"myorg/app Pulling ",
"myorg/app Downloaded newer image ",
"redis Pulling ",
'redis Error response from daemon: Head "https://registry-1.docker.io/v2/library/redis/manifests/7": unauthorized: incorrect username or password for token=AbCdEf123',
"===SU:DOCKER_INSPECT_AFTER===",
"AFTER\tnginx:latest\tsha256:aaa\tnginx@sha256:d1\t1.25.3",
"AFTER\tmyorg/app:latest\tsha256:zzz\tmyorg/app@sha256:d9\t2.4.0",
"AFTER\tredis:7\tsha256:ccc\tredis@sha256:d3\t7.2.0",
"===SU:EXIT=18===",
].join("\n");
describe("parseDockerPullCheck", () => {
it("lit les sections BEFORE / AFTER et le code de sortie", () => {
const p = parseDockerPullCheck(RAW);
expect(p.exitCode).toBe(18);
expect(p.before["myorg/app:latest"]).toEqual({ id: "sha256:bbb", digest: "myorg/app@sha256:d2" });
expect(p.after["myorg/app:latest"]).toEqual({
id: "sha256:zzz",
digest: "myorg/app@sha256:d9",
version: "2.4.0",
});
});
it("détecte l'erreur d'authentification registry et la nettoie (pas d'URL ni de token)", () => {
const p = parseDockerPullCheck(RAW);
expect(p.pullErrors.length).toBeGreaterThan(0);
const auth = p.pullErrors.find((e) => e.kind === "registry_auth_failed");
expect(auth).toBeTruthy();
expect(auth!.message).not.toContain("registry-1.docker.io");
expect(auth!.message).not.toContain("AbCdEf123");
expect(auth!.message.toLowerCase()).toContain("unauthorized");
});
});
describe("buildDockerPullResult", () => {
it("classe up_to_date / updates_available par image et n'émet de change que pour les modifiées", () => {
const r = buildDockerPullResult("media", RAW);
const byImage = new Map(r.services.map((s) => [s.image, s]));
expect(byImage.get("nginx:latest")!.status).toBe("up_to_date");
const app = byImage.get("myorg/app:latest")!;
expect(app.status).toBe("updates_available");
expect(app.currentImageId).toBe("sha256:bbb");
expect(app.candidateImageId).toBe("sha256:zzz");
expect(app.candidateVersion).toBe("2.4.0");
// Un seul change "pulled" (app). nginx inchangé, redis id inchangé.
expect(r.changes).toHaveLength(1);
expect(r.changes[0]).toMatchObject({
stack: "media",
imageRef: "myorg/app:latest",
fromImageId: "sha256:bbb",
toImageId: "sha256:zzz",
operation: "pulled",
});
});
it("status global = error quand le pull renvoie un code non nul / une erreur", () => {
const r = buildDockerPullResult("media", RAW);
expect(r.status).toBe("error");
expect(r.errors.some((e) => e.source === "docker")).toBe(true);
});
it("status = updates_available sans erreur quand tout réussit", () => {
const ok = [
"===SU:DOCKER_INSPECT_BEFORE===",
"BEFORE\tapp:latest\tsha256:old\tapp@sha256:o",
"===SU:DOCKER_PULL===",
"app Pulling ",
"app Downloaded newer image ",
"===SU:DOCKER_INSPECT_AFTER===",
"AFTER\tapp:latest\tsha256:new\tapp@sha256:n\t3.0",
"===SU:EXIT=0===",
].join("\n");
const r = buildDockerPullResult("s", ok);
expect(r.status).toBe("updates_available");
expect(r.errors).toHaveLength(0);
});
});
describe("dockerDedupKey", () => {
it("utilise les digests en priorité", () => {
expect(dockerDedupKey("app:latest", "app@sha256:d2", "app@sha256:d9")).toBe(
"app:latest|app@sha256:d2|app@sha256:d9",
);
});
it("retombe sur les image IDs quand les digests manquent", () => {
expect(dockerDedupKey("app:latest", null, null, "sha256:bbb", "sha256:zzz")).toBe(
"app:latest|sha256:bbb|sha256:zzz",
);
});
});