Skip to content
Merged
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
74 changes: 74 additions & 0 deletions apps/public-api/src/__tests__/health.route.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
'use strict';

const mockRedis = {
status: 'ready',
ping: jest.fn(),
};

jest.mock('mongoose', () => ({
connection: {
readyState: 1,
},
}));

jest.mock('@urbackend/common', () => ({
redis: mockRedis,
}));

const mongoose = require('mongoose');
const express = require('express');
const request = require('supertest');
const healthRoute = require('../routes/health');

describe('health route', () => {
let app;

beforeEach(() => {
jest.clearAllMocks();
mongoose.connection.readyState = 1;
mockRedis.status = 'ready';
mockRedis.ping.mockResolvedValue('PONG');

app = express();
app.use('/api/health', healthRoute);
});

test('returns ok when mongodb and redis are connected', async () => {
const res = await request(app).get('/api/health');

expect(res.status).toBe(200);
expect(res.body.status).toBe('ok');
expect(res.body.dependencies).toEqual({
mongodb: 'connected',
redis: 'connected',
});
expect(typeof res.body.timestamp).toBe('string');
expect(mockRedis.ping).toHaveBeenCalledTimes(1);
});

test('returns error when mongodb is disconnected', async () => {
mongoose.connection.readyState = 0;

const res = await request(app).get('/api/health');

expect(res.status).toBe(503);
expect(res.body.status).toBe('error');
expect(res.body.dependencies).toEqual({
mongodb: 'disconnected',
redis: 'connected',
});
});

test('returns error when redis is not responsive', async () => {
mockRedis.ping.mockRejectedValue(new Error('redis unavailable'));

const res = await request(app).get('/api/health');

expect(res.status).toBe(503);
expect(res.body.status).toBe('error');
expect(res.body.dependencies).toEqual({
mongodb: 'connected',
redis: 'disconnected',
});
});
});
Comment on lines +62 to +74

Copilot AI Apr 9, 2026

Copy link

Choose a reason for hiding this comment

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

The controller introduces a Redis ping timeout guard, but the tests only cover the case where ping() rejects. This doesn't verify the intended behavior when ping() hangs (the case the timeout is meant to protect against). Add a test where mockRedis.ping returns a never-resolving promise and assert the endpoint still responds (e.g., using Jest fake timers / advancing time) and marks Redis as disconnected.

Copilot uses AI. Check for mistakes.
2 changes: 2 additions & 0 deletions apps/public-api/src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ const userAuthRoute = require('./routes/userAuth');
const storageRoute = require('./routes/storage');
const schemaRoute = require('./routes/schemas');
const mailRoute = require('./routes/mail');
const healthRoute = require('./routes/health');

// ROUTES SETUP
app.use('/api/userAuth', limiter, logger, userAuthRoute);
Expand All @@ -78,6 +79,7 @@ app.use('/api/data', projectCorsPreflight, limiter, logger, dataRoute);
app.use('/api/schemas', projectCorsPreflight, limiter, logger, schemaRoute);
app.use('/api/storage', projectCorsPreflight, limiter, logger, storageRoute);
app.use('/api/mail', projectCorsPreflight, limiter, logger, mailRoute);
app.use('/api/health', limiter, logger, healthRoute);

app.get('/api/server-ip', async (req, res) => {
const ip = await getPublicIp();
Expand Down
35 changes: 35 additions & 0 deletions apps/public-api/src/controllers/health.controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
const mongoose = require('mongoose');
const { redis } = require('@urbackend/common');
const REDIS_PING_TIMEOUT_MS = 500;

const getHealth = async (req, res) => {
const isMongoConnected = mongoose.connection.readyState === 1;

let isRedisConnected = false;
if (redis?.status === 'ready' && typeof redis.ping === 'function') {
try {
const pingResponse = await Promise.race([
redis.ping(),
new Promise((resolve) => setTimeout(() => resolve('TIMEOUT'), REDIS_PING_TIMEOUT_MS)),
]);
isRedisConnected = pingResponse === 'PONG';
Comment on lines +11 to +15

Copilot AI Apr 9, 2026

Copy link

Choose a reason for hiding this comment

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

The Redis ping timeout is implemented with Promise.race + setTimeout, but the timeout timer is never cleared when redis.ping() resolves first. Under frequent health checks this leaves many unnecessary timers pending (and the ping promise can also continue running after the timeout wins), which can add avoidable event-loop overhead and resource usage. Consider creating an explicit timeout handle, unref() it, and clearTimeout() it in a finally block (and, if possible with the Redis client, use a cancellable/command-timeout mechanism instead of leaving in-flight pings running).

Copilot uses AI. Check for mistakes.
} catch (_error) {
isRedisConnected = false;
}
}

const status = isMongoConnected && isRedisConnected ? 'ok' : 'error';

return res.status(status === 'ok' ? 200 : 503).json({
status,
timestamp: new Date().toISOString(),
dependencies: {
mongodb: isMongoConnected ? 'connected' : 'disconnected',
redis: isRedisConnected ? 'connected' : 'disconnected',
},
});
};

module.exports = {
getHealth,
};
8 changes: 8 additions & 0 deletions apps/public-api/src/routes/health.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
const express = require('express');
const { getHealth } = require('../controllers/health.controller');

const router = express.Router();

router.get('/', getHealth);

module.exports = router;
Loading