Files
stack/apps/orchestrator/src/api/health/health.controller.spec.ts
Jason Woltje 89bb24493a fix(SEC-ORCH-16): Implement real health and readiness checks
- Add ping() method to ValkeyClient and ValkeyService for health checks
- Update HealthService to check Valkey connectivity before reporting ready
- /health/ready now returns 503 if dependencies are unhealthy
- Add detailed checks object showing individual dependency status
- Update tests with ValkeyService mock

Refs #339

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 19:20:07 -06:00

137 lines
4.4 KiB
TypeScript

import { describe, it, expect, beforeEach, vi } from "vitest";
import { HttpException, HttpStatus } from "@nestjs/common";
import { HealthController } from "./health.controller";
import { HealthService } from "./health.service";
import { ValkeyService } from "../../valkey/valkey.service";
// Mock ValkeyService
const mockValkeyService = {
ping: vi.fn(),
} as unknown as ValkeyService;
describe("HealthController", () => {
let controller: HealthController;
let service: HealthService;
beforeEach(() => {
vi.clearAllMocks();
service = new HealthService(mockValkeyService);
controller = new HealthController(service);
});
describe("GET /health", () => {
it("should return 200 OK with correct format", () => {
const result = controller.check();
expect(result).toBeDefined();
expect(result).toHaveProperty("status");
expect(result).toHaveProperty("uptime");
expect(result).toHaveProperty("timestamp");
});
it('should return status as "healthy"', () => {
const result = controller.check();
expect(result.status).toBe("healthy");
});
it("should return uptime as a positive number", () => {
const result = controller.check();
expect(typeof result.uptime).toBe("number");
expect(result.uptime).toBeGreaterThanOrEqual(0);
});
it("should return timestamp as valid ISO 8601 string", () => {
const result = controller.check();
expect(typeof result.timestamp).toBe("string");
expect(() => new Date(result.timestamp)).not.toThrow();
// Verify it's a valid ISO 8601 format
const date = new Date(result.timestamp);
expect(date.toISOString()).toBe(result.timestamp);
});
it("should return only required fields (status, uptime, timestamp)", () => {
const result = controller.check();
const keys = Object.keys(result);
expect(keys).toHaveLength(3);
expect(keys).toContain("status");
expect(keys).toContain("uptime");
expect(keys).toContain("timestamp");
});
it("should increment uptime over time", async () => {
const result1 = controller.check();
const uptime1 = result1.uptime;
// Wait 1100ms to ensure at least 1 second has passed
await new Promise((resolve) => setTimeout(resolve, 1100));
const result2 = controller.check();
const uptime2 = result2.uptime;
// Uptime should be at least 1 second higher
expect(uptime2).toBeGreaterThanOrEqual(uptime1 + 1);
});
it("should return current timestamp", () => {
const before = Date.now();
const result = controller.check();
const after = Date.now();
const resultTime = new Date(result.timestamp).getTime();
// Timestamp should be between before and after (within test execution time)
expect(resultTime).toBeGreaterThanOrEqual(before);
expect(resultTime).toBeLessThanOrEqual(after);
});
});
describe("GET /health/ready", () => {
it("should return ready status with checks when all dependencies are healthy", async () => {
vi.mocked(mockValkeyService.ping).mockResolvedValue(true);
const result = await controller.ready();
expect(result).toBeDefined();
expect(result).toHaveProperty("ready");
expect(result).toHaveProperty("checks");
expect(result.ready).toBe(true);
expect(result.checks.valkey).toBe(true);
});
it("should throw 503 when Valkey is unhealthy", async () => {
vi.mocked(mockValkeyService.ping).mockResolvedValue(false);
await expect(controller.ready()).rejects.toThrow(HttpException);
try {
await controller.ready();
} catch (error) {
expect(error).toBeInstanceOf(HttpException);
expect((error as HttpException).getStatus()).toBe(HttpStatus.SERVICE_UNAVAILABLE);
const response = (error as HttpException).getResponse() as { ready: boolean };
expect(response.ready).toBe(false);
}
});
it("should return checks object with individual dependency status", async () => {
vi.mocked(mockValkeyService.ping).mockResolvedValue(true);
const result = await controller.ready();
expect(result.checks).toBeDefined();
expect(typeof result.checks.valkey).toBe("boolean");
});
it("should handle Valkey ping errors gracefully", async () => {
vi.mocked(mockValkeyService.ping).mockRejectedValue(new Error("Connection refused"));
await expect(controller.ready()).rejects.toThrow(HttpException);
});
});
});