Skip to content
Closed
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
203 changes: 160 additions & 43 deletions backend/tests/health.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import request from 'supertest';
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import request from "supertest";

// Prisma mock — replaced per test to simulate DB up/down and indexer state.
// Defined via vi.hoisted so it exists when the hoisted vi.mock factory runs.
Expand All @@ -12,12 +12,12 @@ const { prismaMock } = vi.hoisted(() => ({
},
}));

vi.mock('../src/lib/prisma.js', () => ({
vi.mock("../src/lib/prisma.js", () => ({
prisma: prismaMock,
default: prismaMock,
}));

vi.mock('../src/workers/soroban-event-worker.js', () => ({
vi.mock("../src/workers/soroban-event-worker.js", () => ({
sorobanEventWorker: {
getEventCounters: vi.fn().mockReturnValue({
eventsProcessed: 0,
Expand All @@ -29,18 +29,18 @@ vi.mock('../src/workers/soroban-event-worker.js', () => ({
SorobanEventWorker: vi.fn(),
}));

import app from '../src/app.js';
import { sorobanEventWorker } from '../src/workers/soroban-event-worker.js';
import app from "../src/app.js";
import { sorobanEventWorker } from "../src/workers/soroban-event-worker.js";

function makeState(lagSeconds: number) {
const updatedAt = new Date(Date.now() - lagSeconds * 1000);
return { id: 'singleton', updatedAt };
return { id: "singleton", updatedAt };
}

describe('GET /health', () => {
describe("GET /health", () => {
beforeEach(() => {
vi.unstubAllEnvs();
prismaMock.$queryRaw.mockResolvedValue([{ '?column?': 1n }]);
prismaMock.$queryRaw.mockResolvedValue([{ "?column?": 1n }]);
prismaMock.indexerState.findUnique.mockResolvedValue(null);
vi.mocked(sorobanEventWorker.getEventCounters).mockReturnValue({
eventsProcessed: 0,
Expand All @@ -54,13 +54,13 @@ describe('GET /health', () => {
vi.restoreAllMocks();
});

it('returns 200 when DB is up and indexer is disabled (no STREAM_CONTRACT_ID)', async () => {
vi.stubEnv('STREAM_CONTRACT_ID', '');
it("returns 200 when DB is up and indexer is disabled (no STREAM_CONTRACT_ID)", async () => {
vi.stubEnv("STREAM_CONTRACT_ID", "");

const res = await request(app).get('/health');
const res = await request(app).get("/health");
expect(res.status).toBe(200);
expect(res.body.status).toBe('ok');
expect(res.body.db).toBe('connected');
expect(res.body.status).toBe("ok");
expect(res.body.db).toBe("connected");
expect(res.body.indexerEnabled).toBe(false);
expect(res.body.indexerLag).toBeNull();
expect(res.body.eventsProcessed).toBe(0);
Expand All @@ -69,75 +69,192 @@ describe('GET /health', () => {
expect(res.body.indexerDegraded).toBe(false);
});

it('returns 200 when DB is up and indexer is enabled but has no state row yet (cold start)', async () => {
vi.stubEnv('STREAM_CONTRACT_ID', 'CSOME_CONTRACT_ADDRESS');
it("returns 200 when DB is up and indexer is enabled but has no state row yet (cold start)", async () => {
vi.stubEnv("STREAM_CONTRACT_ID", "CSOME_CONTRACT_ADDRESS");
prismaMock.indexerState.findUnique.mockResolvedValue(null);

const res = await request(app).get('/health');
const res = await request(app).get("/health");
expect(res.status).toBe(200);
expect(res.body.status).toBe('ok');
expect(res.body.status).toBe("ok");
expect(res.body.indexerEnabled).toBe(true);
expect(res.body.indexerLag).toBeNull();
});

it('returns 200 when DB is up, indexer enabled, and lag is within threshold', async () => {
vi.stubEnv('STREAM_CONTRACT_ID', 'CSOME_CONTRACT_ADDRESS');
it("returns 200 when DB is up, indexer enabled, and lag is within threshold", async () => {
vi.stubEnv("STREAM_CONTRACT_ID", "CSOME_CONTRACT_ADDRESS");
prismaMock.indexerState.findUnique.mockResolvedValue(makeState(30));

const res = await request(app).get('/health');
const res = await request(app).get("/health");
expect(res.status).toBe(200);
expect(res.body.status).toBe('ok');
expect(res.body.status).toBe("ok");
expect(res.body.indexerLag).toBeGreaterThanOrEqual(0);
expect(res.body.indexerLag).toBeLessThanOrEqual(60);
});

it('returns 503 when DB is up, indexer enabled, and lag exceeds 60 s', async () => {
vi.stubEnv('STREAM_CONTRACT_ID', 'CSOME_CONTRACT_ADDRESS');
it("returns 503 when DB is up, indexer enabled, and lag exceeds 60 s", async () => {
vi.stubEnv("STREAM_CONTRACT_ID", "CSOME_CONTRACT_ADDRESS");
prismaMock.indexerState.findUnique.mockResolvedValue(makeState(120));

const res = await request(app).get('/health');
const res = await request(app).get("/health");
expect(res.status).toBe(503);
expect(res.body.status).toBe('degraded');
expect(res.body.status).toBe("degraded");
expect(res.body.indexerLag).toBeGreaterThan(60);
});

it('returns 503 when DB is down regardless of indexer state', async () => {
vi.stubEnv('STREAM_CONTRACT_ID', '');
prismaMock.$queryRaw.mockRejectedValue(new Error('DB connection refused'));
it("returns 503 when DB is down regardless of indexer state", async () => {
vi.stubEnv("STREAM_CONTRACT_ID", "");
prismaMock.$queryRaw.mockRejectedValue(new Error("DB connection refused"));

const res = await request(app).get('/health');
const res = await request(app).get("/health");
expect(res.status).toBe(503);
expect(res.body.status).toBe('degraded');
expect(res.body.db).toBe('disconnected');
expect(res.body.status).toBe("degraded");
expect(res.body.db).toBe("disconnected");
});

it('returns 200 with indexerLag in body for observability', async () => {
vi.stubEnv('STREAM_CONTRACT_ID', 'CSOME_CONTRACT_ADDRESS');
it("returns 200 with indexerLag in body for observability", async () => {
vi.stubEnv("STREAM_CONTRACT_ID", "CSOME_CONTRACT_ADDRESS");
prismaMock.indexerState.findUnique.mockResolvedValue(makeState(10));

const res = await request(app).get('/health');
const res = await request(app).get("/health");
expect(res.status).toBe(200);
expect(typeof res.body.indexerLag).toBe('number');
expect(typeof res.body.uptime).toBe('number');
expect(typeof res.body.indexerLag).toBe("number");
expect(typeof res.body.uptime).toBe("number");
});

it('returns 503 when indexer is enabled and event-processing failures spike (#844)', async () => {
vi.stubEnv('STREAM_CONTRACT_ID', 'CSOME_CONTRACT_ADDRESS');
it("returns 503 when indexer is enabled and event-processing failures spike (#844)", async () => {
vi.stubEnv("STREAM_CONTRACT_ID", "CSOME_CONTRACT_ADDRESS");
prismaMock.indexerState.findUnique.mockResolvedValue(makeState(5));
vi.mocked(sorobanEventWorker.getEventCounters).mockReturnValue({
eventsProcessed: 1,
eventsFailed: 10,
lastErrorAt: '2026-07-27T08:00:00.000Z',
lastErrorAt: "2026-07-27T08:00:00.000Z",
degraded: true,
});

const res = await request(app).get('/health');
const res = await request(app).get("/health");
expect(res.status).toBe(503);
expect(res.body.status).toBe('degraded');
expect(res.body.status).toBe("degraded");
expect(res.body.indexerLag).toBeLessThanOrEqual(60);
expect(res.body.eventsProcessed).toBe(1);
expect(res.body.eventsFailed).toBe(10);
expect(res.body.lastErrorAt).toBe('2026-07-27T08:00:00.000Z');
expect(res.body.lastErrorAt).toBe("2026-07-27T08:00:00.000Z");
expect(res.body.indexerDegraded).toBe(true);
});

describe("response shape contract assertions", () => {
const EXPECTED_HEALTH_KEYS = [
"db",
"eventsFailed",
"eventsProcessed",
"indexerDegraded",
"indexerEnabled",
"indexerLag",
"lastErrorAt",
"status",
"uptime",
];

it("matches exact response shape contract in healthy state (200 OK)", async () => {
vi.stubEnv("STREAM_CONTRACT_ID", "CSOME_CONTRACT_ADDRESS");
prismaMock.indexerState.findUnique.mockResolvedValue(makeState(15));

const res = await request(app).get("/health");
expect(res.status).toBe(200);

// Verify exact keys set
expect(Object.keys(res.body).sort()).toEqual(EXPECTED_HEALTH_KEYS);

// Verify type & contract values
expect(res.body).toEqual({
status: "ok",
db: "connected",
indexerEnabled: true,
indexerLag: expect.any(Number),
eventsProcessed: 0,
eventsFailed: 0,
lastErrorAt: null,
indexerDegraded: false,
uptime: expect.any(Number),
});
expect(res.body.indexerLag).toBe(15);
expect(res.body.uptime).toBeGreaterThanOrEqual(0);
});

it("matches exact response shape contract in degraded state (indexer lagging > 60s -> 503)", async () => {
vi.stubEnv("STREAM_CONTRACT_ID", "CSOME_CONTRACT_ADDRESS");
prismaMock.indexerState.findUnique.mockResolvedValue(makeState(120));

const res = await request(app).get("/health");
expect(res.status).toBe(503);

// Verify exact keys set
expect(Object.keys(res.body).sort()).toEqual(EXPECTED_HEALTH_KEYS);

// Verify type & contract values
expect(res.body).toEqual({
status: "degraded",
db: "connected",
indexerEnabled: true,
indexerLag: expect.any(Number),
eventsProcessed: 0,
eventsFailed: 0,
lastErrorAt: null,
indexerDegraded: false,
uptime: expect.any(Number),
});
expect(res.body.indexerLag).toBe(120);
expect(res.body.uptime).toBeGreaterThanOrEqual(0);
});

it("matches exact response shape contract in degraded state (DB disconnected -> 503)", async () => {
vi.stubEnv("STREAM_CONTRACT_ID", "CSOME_CONTRACT_ADDRESS");
prismaMock.$queryRaw.mockRejectedValue(new Error("DB Connection Error"));
prismaMock.indexerState.findUnique.mockResolvedValue(makeState(5));

const res = await request(app).get("/health");
expect(res.status).toBe(503);

// Verify exact keys set
expect(Object.keys(res.body).sort()).toEqual(EXPECTED_HEALTH_KEYS);

// Verify type & contract values
expect(res.body).toEqual({
status: "degraded",
db: "disconnected",
indexerEnabled: true,
indexerLag: 5,
eventsProcessed: 0,
eventsFailed: 0,
lastErrorAt: null,
indexerDegraded: false,
uptime: expect.any(Number),
});
expect(res.body.uptime).toBeGreaterThanOrEqual(0);
});

it("matches exact response shape contract in cold-start state (indexerLag is null -> 200 OK)", async () => {
vi.stubEnv("STREAM_CONTRACT_ID", "CSOME_CONTRACT_ADDRESS");
prismaMock.indexerState.findUnique.mockResolvedValue(null);

const res = await request(app).get("/health");
expect(res.status).toBe(200);

// Verify exact keys set
expect(Object.keys(res.body).sort()).toEqual(EXPECTED_HEALTH_KEYS);

// Verify type & contract values
expect(res.body).toEqual({
status: "ok",
db: "connected",
indexerEnabled: true,
indexerLag: null,
eventsProcessed: 0,
eventsFailed: 0,
lastErrorAt: null,
indexerDegraded: false,
uptime: expect.any(Number),
});
expect(res.body.uptime).toBeGreaterThanOrEqual(0);
});
});
});
Loading