Skip to content
Open
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
6,220 changes: 1,892 additions & 4,328 deletions package-lock.json

Large diffs are not rendered by default.

35 changes: 8 additions & 27 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
"lint": "eslint src",
"test": "vitest --run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage"
"test:coverage": "vitest run --coverage",
"db:migrate": "tsx src/db/migrate-cli.ts",
"db:validate": "tsx src/db/validate-cli.ts",
Expand All @@ -19,11 +18,10 @@
"dependencies": {
"cors": "^2.8.5",
"express": "^4.18.2",
"zod": "^4.3.6"
"pg": "^8.11.3"
"zod": "^4.3.6",
"pg": "^8.11.3",
"swagger-ui-express": "^5.0.1",
"yaml": "^2.8.2"
"yaml": "^2.8.2",
"test:coverage": "vitest --coverage"
},
"vitest": {
Expand All @@ -41,40 +39,23 @@
]
}
},
"dependencies": {
"cors": "^2.8.5",
"express": "^4.18.2"
},
"devDependencies": {
"@types/cors": "^2.8.17",
"@types/express": "^4.17.21",
"@types/jest": "^30.0.0",
"@types/node": "^20.11.0",
"@types/pg": "^8.10.9",
"@types/supertest": "^7.2.0",
"@typescript-eslint/parser": "^7.8.0",
"eslint": "^8.57.0",
"jest": "^30.2.0",
"supertest": "^7.2.2",
"ts-jest": "^29.4.6",
"@vitest/coverage-v8": "^4.0.18",
"supertest": "^7.2.2",
"tsx": "^4.7.0",
"typescript": "~5.2.2",
"vitest": "^4.0.18"
"@types/supertest": "^6.0.3",
"@typescript-eslint/eslint-plugin": "^6.19.0",
"@typescript-eslint/parser": "^6.19.0",
"@vitest/coverage-v8": "^1.2.0",
"eslint": "^8.56.0",
"supertest": "^6.3.4",
"@types/pg": "^8.10.9",
"@types/supertest": "^7.2.0",
"vitest": "^4.0.18",
"@types/swagger-ui-express": "^4.1.8",
"@typescript-eslint/eslint-plugin": "^7.8.0",
"@typescript-eslint/parser": "^7.8.0",
"eslint": "^8.57.0",
"@vitest/coverage-v8": "^4.0.18",
"supertest": "^7.2.2",
"tsx": "^4.7.0",
"typescript": "~5.2.2",
"vitest": "^1.2.0"
"@vitest/coverage-v8": "^4.0.18"
}
}
}
222 changes: 222 additions & 0 deletions src/health/__tests__/healthCheckers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";

// ---------------------------------------------------------------------------
// Mock db/client so tests never need a real Postgres connection
// ---------------------------------------------------------------------------
vi.mock("../../db/client.js", () => ({
getConnection: vi.fn(),
}));

import { getConnection } from "../../db/client.js";
import { checkDatabase } from "../checks/database.js";

const mockGetConnection = vi.mocked(getConnection);

function makeMockClient({
connectError,
queryError,
endError,
}: {
connectError?: Error;
queryError?: Error;
endError?: Error;
} = {}) {
return {
connect: connectError
? vi.fn().mockRejectedValue(connectError)
: vi.fn().mockResolvedValue(undefined),
query: queryError
? vi.fn().mockRejectedValue(queryError)
: vi.fn().mockResolvedValue({ rows: [{ "?column?": 1 }] }),
end: endError
? vi.fn().mockRejectedValue(endError)
: vi.fn().mockResolvedValue(undefined),
};
}

describe("checkDatabase()", () => {
const originalEnv = process.env;

beforeEach(() => {
process.env = { ...originalEnv };
});

afterEach(() => {
process.env = originalEnv;
vi.clearAllMocks();
});

it("returns down when DATABASE_URL is not set", async () => {
delete process.env["DATABASE_URL"];
const result = await checkDatabase();
expect(result.status).toBe("down");
expect(result.error).toContain("DATABASE_URL");
});

it("returns ok on a successful ping", async () => {
process.env["DATABASE_URL"] = "postgres://localhost/creditra_test";
mockGetConnection.mockReturnValue(makeMockClient());
const result = await checkDatabase();
expect(result.status).toBe("ok");
});

it("returns down when connect() throws", async () => {
process.env["DATABASE_URL"] = "postgres://localhost/creditra_test";
mockGetConnection.mockReturnValue(
makeMockClient({ connectError: new Error("ECONNREFUSED") }),
);
const result = await checkDatabase();
expect(result.status).toBe("down");
expect(result.error).toContain("ECONNREFUSED");
});

it("returns down when query() throws", async () => {
process.env["DATABASE_URL"] = "postgres://localhost/creditra_test";
mockGetConnection.mockReturnValue(
makeMockClient({ queryError: new Error("query failed") }),
);
const result = await checkDatabase();
expect(result.status).toBe("down");
expect(result.error).toContain("query failed");
});

it("still returns ok when end() throws (cleanup errors ignored)", async () => {
process.env["DATABASE_URL"] = "postgres://localhost/creditra_test";
mockGetConnection.mockReturnValue(
makeMockClient({ endError: new Error("end failed") }),
);
const result = await checkDatabase();
expect(result.status).toBe("ok");
});

it("wraps non-Error thrown values gracefully", async () => {
process.env["DATABASE_URL"] = "postgres://localhost/creditra_test";
const client = makeMockClient();
client.query = vi.fn().mockRejectedValue("plain string error");
mockGetConnection.mockReturnValue(client);
const result = await checkDatabase();
expect(result.status).toBe("down");
expect(result.error).toBe("Unknown database error");
});

it("works when client has no connect() method (optional connect)", async () => {
process.env["DATABASE_URL"] = "postgres://localhost/creditra_test";
const clientWithoutConnect = {
query: vi.fn().mockResolvedValue({ rows: [] }),
end: vi.fn().mockResolvedValue(undefined),
};
// Type assertion needed because connect is optional
mockGetConnection.mockReturnValue(
clientWithoutConnect as ReturnType<typeof getConnection>,
);
const result = await checkDatabase();
expect(result.status).toBe("ok");
});
});

// ---------------------------------------------------------------------------
// checkHorizon
// ---------------------------------------------------------------------------
vi.mock("../../services/horizonListener.js", () => ({
isRunning: vi.fn(),
getConfig: vi.fn(),
}));

import { isRunning, getConfig } from "../../services/horizonListener.js";
import { checkHorizon } from "../checks/horizon.js";

const mockIsRunning = vi.mocked(isRunning);
const mockGetConfig = vi.mocked(getConfig);

describe("checkHorizon()", () => {
afterEach(() => vi.clearAllMocks());

it("returns ok when listener is running", () => {
mockIsRunning.mockReturnValue(true);
const result = checkHorizon();
expect(result.status).toBe("ok");
});

it("returns degraded when not running but config exists", () => {
mockIsRunning.mockReturnValue(false);
mockGetConfig.mockReturnValue({
horizonUrl: "https://horizon-testnet.stellar.org",
contractIds: [],
pollIntervalMs: 5000,
startLedger: "latest",
});
const result = checkHorizon();
expect(result.status).toBe("degraded");
expect(result.error).toBeDefined();
});

it("returns down when not running and no config", () => {
mockIsRunning.mockReturnValue(false);
mockGetConfig.mockReturnValue(null);
const result = checkHorizon();
expect(result.status).toBe("down");
expect(result.error).toBeDefined();
});
});

// ---------------------------------------------------------------------------
// checkRedis
// ---------------------------------------------------------------------------
import { checkRedis } from "../checks/redis.js";

describe("checkRedis()", () => {
const originalEnv = process.env;

beforeEach(() => {
process.env = { ...originalEnv };
});

afterEach(() => {
process.env = originalEnv;
});

it("returns ok with a note when REDIS_URL is not set", async () => {
delete process.env["REDIS_URL"];
const result = await checkRedis();
expect(result.status).toBe("ok");
expect(result.note).toBeDefined();
});

it("returns ok with a note when REDIS_URL is set (stub)", async () => {
process.env["REDIS_URL"] = "redis://localhost:6379";
const result = await checkRedis();
expect(result.status).toBe("ok");
expect(result.note).toBeDefined();
});
});

// ---------------------------------------------------------------------------
// checkRiskEngine
// ---------------------------------------------------------------------------
import { checkRiskEngine } from "../checks/riskEngine.js";

describe("checkRiskEngine()", () => {
const originalEnv = process.env;

beforeEach(() => {
process.env = { ...originalEnv };
});

afterEach(() => {
process.env = originalEnv;
});

it("returns ok with a note when RISK_ENGINE_URL is not set", async () => {
delete process.env["RISK_ENGINE_URL"];
const result = await checkRiskEngine();
expect(result.status).toBe("ok");
expect(result.note).toBeDefined();
});

it("returns ok with a note when RISK_ENGINE_URL is set (stub)", async () => {
process.env["RISK_ENGINE_URL"] = "http://risk-engine.local";
const result = await checkRiskEngine();
expect(result.status).toBe("ok");
expect(result.note).toBeDefined();
});
});
Loading