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
20 changes: 8 additions & 12 deletions backend/src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,8 @@ import x402Router from "./routes/x402.js";
import authRouter from "./routes/auth.js";

import { requireApiKeyAuth } from "./lib/auth.js";
import { isHorizonReachable } from "./lib/stellar.js";
import { supabase } from "./lib/supabase.js";
import { pool } from "./lib/db.js";
import { probeHealth } from "./lib/health.js";
import { x402Middleware } from "./middleware/x402.js";

import { idempotencyMiddleware } from "./lib/idempotency.js";
Expand Down Expand Up @@ -203,18 +202,15 @@ export async function createApp({ redisClient }) {
* type: string
* horizon_reachable:
* type: boolean
*/
*/
app.get("/health", async (req, res) => {
const [dbResult, horizonReachable] = await Promise.allSettled([
supabase.from("merchants").select("id").limit(1),
isHorizonReachable(),
]);

const dbOk = dbResult.status === "fulfilled" && !dbResult.value?.error;
if (!dbOk) {
console.error("Health DB error:", JSON.stringify(dbResult.reason || dbResult.value?.error));
const { database, horizon } = await probeHealth();

if (!database.ok) {
console.error("Health DB error:", database.error);
}
const horizonOk = horizonReachable.status === "fulfilled" && horizonReachable.value === true;
const dbOk = database.ok;
const horizonOk = horizon.ok;

const status = dbOk && horizonOk ? 200 : 503;

Expand Down
53 changes: 20 additions & 33 deletions backend/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,11 @@ import metricsRouter from "./routes/metrics.js";
import authRouter from "./routes/auth.js";
import auditRouter from "./routes/audit.js";
import { requireApiKeyAuth } from "./lib/auth.js";
import { isHorizonReachable } from "./lib/stellar.js";
import { supabase } from "./lib/supabase.js";
import { pool, closePool } from "./lib/db.js";
import { validateEnvironmentVariables } from "./lib/env-validation.js";
import { formatZodError } from "./lib/request-schemas.js";
import { idempotencyMiddleware } from "./lib/idempotency.js";
import { probeHealth } from "./lib/health.js";
import { closeRedisClient, connectRedisClient } from "./lib/redis.js";
import {
createRedisRateLimitStore,
Expand Down Expand Up @@ -82,45 +81,33 @@ app.use(express.json({ limit: "1mb" }));
app.use(morgan(":request-id :method :url :status :response-time ms"));

app.get("/health", async (req, res) => {
try {
const [dbResult, horizonReachable] = await Promise.all([
supabase.from("merchants").select("id").limit(1),
isHorizonReachable(),
]);

const { error } = dbResult;

if (error) {
return res.status(503).json({
ok: false,
service: "stellar-payment-api",
error: "Database unavailable",
horizon_reachable: horizonReachable,
});
}

if (!horizonReachable) {
return res.status(503).json({
ok: false,
service: "stellar-payment-api",
error: "Horizon unavailable",
horizon_reachable: false,
});
}

res.json({
ok: true,
const { database, horizon } = await probeHealth();

if (!database.ok) {
res.status(503).json({
ok: false,
service: "stellar-payment-api",
horizon_reachable: true,
error: "Database unavailable",
horizon_reachable: horizon.ok,
});
} catch {
return;
}

if (!horizon.ok) {
res.status(503).json({
ok: false,
service: "stellar-payment-api",
error: "Health check failed",
error: "Horizon unavailable",
horizon_reachable: false,
});
return;
}

res.json({
ok: true,
service: "stellar-payment-api",
horizon_reachable: true,
});
});

app.use("/api/create-payment", requireApiKeyAuth());
Expand Down
20 changes: 20 additions & 0 deletions backend/src/lib/health.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { pool } from "./db.js";
import { isHorizonReachable } from "./stellar.js";

export async function probeHealth({ db = pool, horizonProbe = isHorizonReachable } = {}) {
const [dbResult, horizonResult] = await Promise.allSettled([
db.query("SELECT 1"),
horizonProbe(),
]);

return {
database: {
ok: dbResult.status === "fulfilled",
error: dbResult.status === "rejected" ? dbResult.reason : null,
},
horizon: {
ok: horizonResult.status === "fulfilled" && horizonResult.value === true,
error: horizonResult.status === "rejected" ? horizonResult.reason : null,
},
};
}
45 changes: 45 additions & 0 deletions backend/src/lib/health.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { beforeEach, describe, expect, it, vi } from "vitest";

const query = vi.fn();
const isHorizonReachable = vi.fn();

vi.mock("./db.js", () => ({
pool: { query },
}));

vi.mock("./stellar.js", () => ({
isHorizonReachable,
}));

describe("probeHealth", () => {
beforeEach(() => {
vi.clearAllMocks();
});

it('runs a "SELECT 1" database probe and reports both services healthy', async () => {
query.mockResolvedValue({ rows: [{ "?column?": 1 }] });
isHorizonReachable.mockResolvedValue(true);

const { probeHealth } = await import("./health.js");
const result = await probeHealth();

expect(query).toHaveBeenCalledWith("SELECT 1");
expect(isHorizonReachable).toHaveBeenCalledTimes(1);
expect(result).toEqual({
database: { ok: true, error: null },
horizon: { ok: true, error: null },
});
});

it("reports the database as unavailable when the SELECT 1 query fails", async () => {
const dbError = new Error("db down");
query.mockRejectedValue(dbError);
isHorizonReachable.mockResolvedValue(true);

const { probeHealth } = await import("./health.js");
const result = await probeHealth();

expect(result.database).toEqual({ ok: false, error: dbError });
expect(result.horizon).toEqual({ ok: true, error: null });
});
});
7 changes: 7 additions & 0 deletions backend/tests/integration/health.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ vi.mock("../../src/lib/stellar.js", () => ({
isHorizonReachable: vi.fn(async () => true),
}));

vi.mock("../../src/lib/health.js", () => ({
probeHealth: vi.fn(async () => ({
database: { ok: true, error: null },
horizon: { ok: true, error: null },
})),
}));

vi.mock("../../src/lib/supabase.js", () => {
return {
supabase: {
Expand Down
Loading