From 74944c0387cc5745d7577de808117716bfcc9518 Mon Sep 17 00:00:00 2001 From: saad moumou Date: Thu, 2 Apr 2026 14:14:32 +0100 Subject: [PATCH 1/2] chore(indicators): remove MongoHealthIndicator spec --- jest.config.ts | 8 ++++---- src/controllers/health.controller.spec.ts | 4 ++-- src/services/health.service.spec.ts | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/jest.config.ts b/jest.config.ts index 2b69a86..a8f6751 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -27,10 +27,10 @@ const config: Config = { coverageDirectory: "coverage", coverageThreshold: { global: { - branches: 80, - functions: 80, - lines: 80, - statements: 80, + branches: 85, + functions: 85, + lines: 85, + statements: 85, }, }, moduleNameMapper: { diff --git a/src/controllers/health.controller.spec.ts b/src/controllers/health.controller.spec.ts index c9e8d2d..a6709e9 100644 --- a/src/controllers/health.controller.spec.ts +++ b/src/controllers/health.controller.spec.ts @@ -10,8 +10,8 @@ import { createHealthController } from "./health.controller"; const makeService = (liveness: "ok" | "error", readiness: "ok" | "error") => ({ - checkLiveness: jest.fn().mockResolvedValue({ status: liveness, indicators: [] }), - checkReadiness: jest.fn().mockResolvedValue({ status: readiness, indicators: [] }), + checkLiveness: jest.fn().mockResolvedValue({ status: liveness, results: [] }), + checkReadiness: jest.fn().mockResolvedValue({ status: readiness, results: [] }), }) as unknown as HealthService; interface HealthControllerInstance { diff --git a/src/services/health.service.spec.ts b/src/services/health.service.spec.ts index 6298eaa..abcff42 100644 --- a/src/services/health.service.spec.ts +++ b/src/services/health.service.spec.ts @@ -86,7 +86,7 @@ describe("HealthService", () => { const result = await service.checkReadiness(); expect(result.status).toBe("error"); - const failed = result.results.find((r: { status: string }) => r.status === "down"); + const failed = result.results.find((r) => r.status === "down"); expect(failed?.message).toBe("ECONNREFUSED"); }); From f3168941470940f59138f89ee88702b573115766 Mon Sep 17 00:00:00 2001 From: saad moumou Date: Thu, 2 Apr 2026 14:16:23 +0100 Subject: [PATCH 2/2] test(module): add HealthKitModule spec - register, registerAsync, path default --- src/health-kit.module.spec.ts | 187 ++++++++++++++++++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 src/health-kit.module.spec.ts diff --git a/src/health-kit.module.spec.ts b/src/health-kit.module.spec.ts new file mode 100644 index 0000000..1393049 --- /dev/null +++ b/src/health-kit.module.spec.ts @@ -0,0 +1,187 @@ +import "reflect-metadata"; + +import { HealthIndicator } from "@decorators/health-indicator.decorator"; +import { BaseHealthIndicator } from "@indicators/base.indicator"; +import { createIndicator } from "@indicators/create-indicator"; +import type { HealthIndicatorResult } from "@interfaces/health-indicator.interface"; +import { Injectable } from "@nestjs/common"; +import { Test } from "@nestjs/testing"; + +import { + HEALTH_LIVENESS_INDICATORS, + HEALTH_READINESS_INDICATORS, + HealthKitModule, +} from "./health-kit.module"; +import { HealthService } from "./services/health.service"; + +// ÔöÇÔöÇ Fixtures ÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇ + +@HealthIndicator("liveness") +@Injectable() +class LivenessIndicator extends BaseHealthIndicator { + readonly name = "custom-live"; + async check(): Promise { + return this.result("up"); + } +} + +@HealthIndicator("readiness") +@Injectable() +class ReadinessIndicator extends BaseHealthIndicator { + readonly name = "custom-ready"; + async check(): Promise { + return this.result("up"); + } +} + +@Injectable() +class UndecoratedIndicator extends BaseHealthIndicator { + readonly name = "no-scope"; + async check(): Promise { + return this.result("up"); + } +} + +// ÔöÇÔöÇ Helpers ÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇ + +async function compile(options: Parameters[0]) { + const module = await Test.createTestingModule({ + imports: [HealthKitModule.register(options)], + }).compile(); + return module; +} + +// ÔöÇÔöÇ Tests ÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇ + +describe("HealthKitModule.register()", () => { + it("returns a DynamicModule with the correct module reference", () => { + const dyn = HealthKitModule.register({ path: "health" }); + expect(dyn.module).toBe(HealthKitModule); + }); + + it("exports HealthService", () => { + const dyn = HealthKitModule.register({ path: "health" }); + expect(dyn.exports).toContain(HealthService); + }); + + it("compiles with no indicators", async () => { + const module = await compile({ path: "health" }); + const service = module.get(HealthService); + expect(service).toBeInstanceOf(HealthService); + }); + + it("liveness result is ok when all liveness indicators pass", async () => { + const indicator = createIndicator("ping", async () => true); + const module = await compile({ path: "health", liveness: [indicator] }); + + const service = module.get(HealthService); + const result = await service.checkLiveness(); + + expect(result.status).toBe("ok"); + expect(result.results[0]?.name).toBe("ping"); + }); + + it("readiness result is ok when all readiness indicators pass", async () => { + const indicator = createIndicator("db", async () => true); + const module = await compile({ path: "health", readiness: [indicator] }); + + const service = module.get(HealthService); + const result = await service.checkReadiness(); + + expect(result.status).toBe("ok"); + expect(result.results[0]?.name).toBe("db"); + }); + + it("auto-routes @HealthIndicator('liveness') class to liveness list", async () => { + const module = await compile({ + path: "health", + indicators: [LivenessIndicator], + }); + + const liveness = module.get(HEALTH_LIVENESS_INDICATORS); + expect(liveness).toHaveLength(1); + + const readiness = module.get(HEALTH_READINESS_INDICATORS); + expect(readiness).toHaveLength(0); + }); + + it("auto-routes @HealthIndicator('readiness') class to readiness list", async () => { + const module = await compile({ + path: "health", + indicators: [ReadinessIndicator], + }); + + const readiness = module.get(HEALTH_READINESS_INDICATORS); + expect(readiness).toHaveLength(1); + + const liveness = module.get(HEALTH_LIVENESS_INDICATORS); + expect(liveness).toHaveLength(0); + }); + + it("merges explicit instances with DI-based indicators", async () => { + const explicitReady = createIndicator("http", async () => true); + const module = await compile({ + path: "health", + readiness: [explicitReady], + indicators: [ReadinessIndicator], + }); + + const readiness = module.get(HEALTH_READINESS_INDICATORS); + expect(readiness).toHaveLength(2); + }); + + it("undecorated indicator class is not added to either list", async () => { + const module = await compile({ + path: "health", + indicators: [UndecoratedIndicator], + }); + + const liveness = module.get(HEALTH_LIVENESS_INDICATORS); + const readiness = module.get(HEALTH_READINESS_INDICATORS); + + expect(liveness).toHaveLength(0); + expect(readiness).toHaveLength(0); + }); + + it("handles both liveness and readiness DI indicators simultaneously", async () => { + const module = await compile({ + path: "health", + indicators: [LivenessIndicator, ReadinessIndicator], + }); + + const liveness = module.get(HEALTH_LIVENESS_INDICATORS); + const readiness = module.get(HEALTH_READINESS_INDICATORS); + + expect(liveness).toHaveLength(1); + expect(readiness).toHaveLength(1); + + const service = module.get(HealthService); + const liveResult = await service.checkLiveness(); + const readyResult = await service.checkReadiness(); + + expect(liveResult.status).toBe("ok"); + expect(readyResult.status).toBe("ok"); + }); + + it("path defaults to 'health' when not provided", () => { + const dyn = HealthKitModule.register({}); + expect(dyn.controllers).toBeDefined(); + expect(dyn.module).toBe(HealthKitModule); + }); + + it("registerAsync resolves options from factory", async () => { + const indicator = createIndicator("async-ping", async () => true); + const module = await Test.createTestingModule({ + imports: [ + HealthKitModule.registerAsync({ + useFactory: () => ({ liveness: [indicator] }), + }), + ], + }).compile(); + + const service = module.get(HealthService); + const result = await service.checkLiveness(); + expect(result.status).toBe("ok"); + expect(result.results[0]?.name).toBe("async-ping"); + }); +});