From 9979f3418aed07a4daf248fca185765d550416db Mon Sep 17 00:00:00 2001 From: PunGrumpy <108584943+PunGrumpy@users.noreply.github.com> Date: Sun, 12 Jul 2026 21:01:01 +0700 Subject: [PATCH 1/4] test(core): import untested rules into rules.test.ts --- packages/core/test/rules.test.ts | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/packages/core/test/rules.test.ts b/packages/core/test/rules.test.ts index d73c21c..09f770d 100644 --- a/packages/core/test/rules.test.ts +++ b/packages/core/test/rules.test.ts @@ -8,14 +8,33 @@ import { avoidRunCd, sortMultilineArgs, useraddNoLogInit, + requireHealthcheck, + preferCopyOverAdd, + useExecForm, + requireLabels, } from "../src/rules/best-practices.js"; -import { noVersionKey, requireResourceLimits } from "../src/rules/compose.js"; -import { preferSlimBase, cleanPackageCache } from "../src/rules/image-size.js"; -import { orderLayers } from "../src/rules/performance.js"; +import { + noVersionKey, + requireResourceLimits, + requireRestartPolicy, + useDependsOnCondition, +} from "../src/rules/compose.js"; +import { + preferSlimBase, + cleanPackageCache, + avoidDevDependencies, +} from "../src/rules/image-size.js"; +import { + orderLayers, + useMultiStage, + minimizeLayers, + useDockerignore, +} from "../src/rules/performance.js"; import { noRootUser, pinImageVersion, noSecretsInEnv, + noAddRemote, } from "../src/rules/security.js"; describe("Security Rules", () => { From 9bc48679b41f5f300e78a19b6cf951db386e02d5 Mon Sep 17 00:00:00 2001 From: PunGrumpy <108584943+PunGrumpy@users.noreply.github.com> Date: Sun, 12 Jul 2026 21:02:02 +0700 Subject: [PATCH 2/4] test(core): add unit tests for Best Practices rules --- packages/core/test/rules.test.ts | 58 ++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/packages/core/test/rules.test.ts b/packages/core/test/rules.test.ts index 09f770d..8b30ad7 100644 --- a/packages/core/test/rules.test.ts +++ b/packages/core/test/rules.test.ts @@ -265,4 +265,62 @@ describe("Best Practices Rules", () => { const diags2 = useraddNoLogInit.check(withFlag, "Dockerfile"); expect(diags2).toHaveLength(0); }); + + test("require-healthcheck", () => { + const withoutHealth = parseDockerfile(` + FROM node:22-alpine + EXPOSE 3000 + `); + const diags1 = requireHealthcheck.check(withoutHealth, "Dockerfile"); + expect(diags1).toHaveLength(1); + + const withHealth = parseDockerfile(` + FROM node:22-alpine + HEALTHCHECK --interval=30s --timeout=3s CMD curl -f http://localhost/ || exit 1 + `); + const diags2 = requireHealthcheck.check(withHealth, "Dockerfile"); + expect(diags2).toHaveLength(0); + }); + + test("prefer-copy-over-add", () => { + const addFile = parseDockerfile(` + ADD file.txt /app/file.txt + `); + const diags1 = preferCopyOverAdd.check(addFile, "Dockerfile"); + expect(diags1).toHaveLength(1); + + const addArchive = parseDockerfile(` + ADD archive.tar.gz /app/ + `); + const diags2 = preferCopyOverAdd.check(addArchive, "Dockerfile"); + expect(diags2).toHaveLength(0); + }); + + test("use-exec-form", () => { + const shellForm = parseDockerfile(` + CMD node index.js + `); + const diags1 = useExecForm.check(shellForm, "Dockerfile"); + expect(diags1).toHaveLength(1); + + const execForm = parseDockerfile(` + CMD ["node", "index.js"] + `); + const diags2 = useExecForm.check(execForm, "Dockerfile"); + expect(diags2).toHaveLength(0); + }); + + test("require-labels", () => { + const withoutLabels = parseDockerfile(` + FROM node:22-alpine + `); + const diags1 = requireLabels.check(withoutLabels, "Dockerfile"); + expect(diags1).toHaveLength(1); + + const withLabels = parseDockerfile(` + LABEL maintainer="me" + `); + const diags2 = requireLabels.check(withLabels, "Dockerfile"); + expect(diags2).toHaveLength(0); + }); }); From 2d8c9ed289f8f88af6e72b9b857b0272294cf824 Mon Sep 17 00:00:00 2001 From: PunGrumpy <108584943+PunGrumpy@users.noreply.github.com> Date: Sun, 12 Jul 2026 21:04:19 +0700 Subject: [PATCH 3/4] test(core): add unit tests for Compose rules --- packages/core/test/rules.test.ts | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/packages/core/test/rules.test.ts b/packages/core/test/rules.test.ts index 8b30ad7..c11398b 100644 --- a/packages/core/test/rules.test.ts +++ b/packages/core/test/rules.test.ts @@ -118,6 +118,34 @@ describe("Compose Rules", () => { const diags = requireResourceLimits.check(composeContent, "compose.yml"); expect(diags).toHaveLength(1); }); + + test("require-restart-policy", () => { + const withoutRestart = { + services: { + web: { image: "node:22" }, + }, + }; + const diags1 = requireRestartPolicy.check(withoutRestart, "compose.yml"); + expect(diags1).toHaveLength(1); + + const withRestart = { + services: { + web: { image: "node:22", restart: "always" }, + }, + }; + const diags2 = requireRestartPolicy.check(withRestart, "compose.yml"); + expect(diags2).toHaveLength(0); + }); + + test("use-depends-on-condition", () => { + const shortForm = { + services: { + web: { image: "node:22", depends_on: ["db"] }, + }, + }; + const diags1 = useDependsOnCondition.check(shortForm, "compose.yml"); + expect(diags1).toHaveLength(1); + }); }); describe("Image Size Rules", () => { From ca71b398032786916109470dd011c524ac448a7d Mon Sep 17 00:00:00 2001 From: PunGrumpy <108584943+PunGrumpy@users.noreply.github.com> Date: Sun, 12 Jul 2026 21:05:04 +0700 Subject: [PATCH 4/4] test(core): add unit tests for Security, Image Size, and Performance rules --- packages/core/test/rules.test.ts | 63 +++++++++++++++++++++++++++++++- 1 file changed, 62 insertions(+), 1 deletion(-) diff --git a/packages/core/test/rules.test.ts b/packages/core/test/rules.test.ts index c11398b..beec324 100644 --- a/packages/core/test/rules.test.ts +++ b/packages/core/test/rules.test.ts @@ -82,6 +82,14 @@ describe("Security Rules", () => { const diags = noSecretsInEnv.check(withSecret, "Dockerfile"); expect(diags).toHaveLength(1); }); + + test("no-add-remote", () => { + const remoteAdd = parseDockerfile(` + ADD https://example.com/file.txt /app/ + `); + const diags1 = noAddRemote.check(remoteAdd, "Dockerfile"); + expect(diags1).toHaveLength(1); + }); }); describe("Performance Rules", () => { @@ -95,6 +103,41 @@ describe("Performance Rules", () => { expect(diags).toHaveLength(1); expect(diags[0].rule).toBe("docker-doctor/order-layers"); }); + + test("use-multi-stage", () => { + const singleStage = parseDockerfile(` + FROM node:22 + RUN npm run build + `); + const diags1 = useMultiStage.check(singleStage, "Dockerfile"); + expect(diags1).toHaveLength(1); + }); + + test("minimize-layers", () => { + const consecutive = parseDockerfile(` + RUN step1 + RUN step2 + RUN step3 + `); + const diags1 = minimizeLayers.check(consecutive, "Dockerfile"); + expect(diags1).toHaveLength(1); + }); + + test("use-dockerignore", () => { + const copyAll = parseDockerfile(` + FROM node:22-alpine + COPY . . + `); + const diags1 = useDockerignore.check(copyAll, "Dockerfile", { + projectFiles: ["Dockerfile"], + }); + expect(diags1).toHaveLength(1); + + const diags2 = useDockerignore.check(copyAll, "Dockerfile", { + projectFiles: ["Dockerfile", ".dockerignore"], + }); + expect(diags2).toHaveLength(0); + }); }); describe("Compose Rules", () => { @@ -140,7 +183,7 @@ describe("Compose Rules", () => { test("use-depends-on-condition", () => { const shortForm = { services: { - web: { image: "node:22", depends_on: ["db"] }, + web: { depends_on: ["db"], image: "node:22" }, }, }; const diags1 = useDependsOnCondition.check(shortForm, "compose.yml"); @@ -176,6 +219,24 @@ describe("Image Size Rules", () => { const diags2 = cleanPackageCache.check(withCleanup, "Dockerfile"); expect(diags2).toHaveLength(0); }); + + test("avoid-dev-dependencies", () => { + const withDev = parseDockerfile(` + FROM node:22 AS builder + FROM node:22 AS runner + RUN npm install + `); + const diags1 = avoidDevDependencies.check(withDev, "Dockerfile"); + expect(diags1).toHaveLength(1); + + const withoutDev = parseDockerfile(` + FROM node:22 AS builder + FROM node:22 AS runner + RUN npm ci --omit=dev + `); + const diags2 = avoidDevDependencies.check(withoutDev, "Dockerfile"); + expect(diags2).toHaveLength(0); + }); }); describe("Best Practices Rules", () => {