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", ); }); });