Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
4 changes: 2 additions & 2 deletions src/controllers/health.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
187 changes: 187 additions & 0 deletions src/health-kit.module.spec.ts
Original file line number Diff line number Diff line change
@@ -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 ÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇ

Comment on lines +17 to +18
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The section divider comments contain garbled characters ("ÔöÇ"), which looks like a bad encoding/transcoding artifact and hurts readability/searchability. Replace these with plain ASCII (e.g., // --- Fixtures ---) or remove them.

Copilot uses AI. Check for mistakes.
@HealthIndicator("liveness")
@Injectable()
class LivenessIndicator extends BaseHealthIndicator {
readonly name = "custom-live";
async check(): Promise<HealthIndicatorResult> {
return this.result("up");
}
}

@HealthIndicator("readiness")
@Injectable()
class ReadinessIndicator extends BaseHealthIndicator {
readonly name = "custom-ready";
async check(): Promise<HealthIndicatorResult> {
return this.result("up");
}
}

@Injectable()
class UndecoratedIndicator extends BaseHealthIndicator {
readonly name = "no-scope";
async check(): Promise<HealthIndicatorResult> {
return this.result("up");
}
}

// ÔöÇÔöÇ Helpers ÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇÔöÇ

async function compile(options: Parameters<typeof HealthKitModule.register>[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<HealthIndicatorResult[]>(HEALTH_LIVENESS_INDICATORS);
expect(liveness).toHaveLength(1);

const readiness = module.get<HealthIndicatorResult[]>(HEALTH_READINESS_INDICATORS);
expect(readiness).toHaveLength(0);
Comment on lines +101 to +105
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These DI tokens (HEALTH_LIVENESS_INDICATORS / HEALTH_READINESS_INDICATORS) provide arrays of indicator instances (IHealthIndicator[]/BaseHealthIndicator[]), but the test retrieves them as HealthIndicatorResult[]. Using the correct type here will prevent confusion and type-unsafe usage in future test edits.

Copilot uses AI. Check for mistakes.
});

it("auto-routes @HealthIndicator('readiness') class to readiness list", async () => {
const module = await compile({
path: "health",
indicators: [ReadinessIndicator],
});

const readiness = module.get<HealthIndicatorResult[]>(HEALTH_READINESS_INDICATORS);
expect(readiness).toHaveLength(1);

const liveness = module.get<HealthIndicatorResult[]>(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<HealthIndicatorResult[]>(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<HealthIndicatorResult[]>(HEALTH_LIVENESS_INDICATORS);
const readiness = module.get<HealthIndicatorResult[]>(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<HealthIndicatorResult[]>(HEALTH_LIVENESS_INDICATORS);
const readiness = module.get<HealthIndicatorResult[]>(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");
});
});
2 changes: 1 addition & 1 deletion src/services/health.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});

Expand Down
Loading