From 737e370e6f1496565a70a1dbafa50b847a50372d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 9 Apr 2026 09:04:00 +0000 Subject: [PATCH 1/3] Initial plan From 3a9e24f7ad949d7d3fb151443aa25e33a80eeb50 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 9 Apr 2026 09:09:59 +0000 Subject: [PATCH 2/3] feat(public-api): add /api/health endpoint with dependency checks Agent-Logs-Url: https://github.com/geturbackend/urBackend/sessions/de8db7a6-e16f-4427-8b4e-7737877312b3 Co-authored-by: Shivkumardhakad <202952715+Shivkumardhakad@users.noreply.github.com> --- .../src/__tests__/health.route.test.js | 74 +++++++++++++++++++ apps/public-api/src/app.js | 2 + .../src/controllers/health.controller.js | 31 ++++++++ apps/public-api/src/routes/health.js | 8 ++ 4 files changed, 115 insertions(+) create mode 100644 apps/public-api/src/__tests__/health.route.test.js create mode 100644 apps/public-api/src/controllers/health.controller.js create mode 100644 apps/public-api/src/routes/health.js diff --git a/apps/public-api/src/__tests__/health.route.test.js b/apps/public-api/src/__tests__/health.route.test.js new file mode 100644 index 000000000..c846204dc --- /dev/null +++ b/apps/public-api/src/__tests__/health.route.test.js @@ -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', + }); + }); +}); diff --git a/apps/public-api/src/app.js b/apps/public-api/src/app.js index b4c5108d9..0bd36d8f0 100644 --- a/apps/public-api/src/app.js +++ b/apps/public-api/src/app.js @@ -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); @@ -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', healthRoute); app.get('/api/server-ip', async (req, res) => { const ip = await getPublicIp(); diff --git a/apps/public-api/src/controllers/health.controller.js b/apps/public-api/src/controllers/health.controller.js new file mode 100644 index 000000000..0ca43a03e --- /dev/null +++ b/apps/public-api/src/controllers/health.controller.js @@ -0,0 +1,31 @@ +const mongoose = require('mongoose'); +const { redis } = require('@urbackend/common'); + +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 redis.ping(); + isRedisConnected = pingResponse === 'PONG'; + } 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, +}; diff --git a/apps/public-api/src/routes/health.js b/apps/public-api/src/routes/health.js new file mode 100644 index 000000000..154fca7f3 --- /dev/null +++ b/apps/public-api/src/routes/health.js @@ -0,0 +1,8 @@ +const express = require('express'); +const { getHealth } = require('../controllers/health.controller'); + +const router = express.Router(); + +router.get('/', getHealth); + +module.exports = router; From 4a3b6852c3ab1968e6765ce5217d68f1f5182398 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 9 Apr 2026 09:12:23 +0000 Subject: [PATCH 3/3] fix(public-api): harden health endpoint checks and route protection Agent-Logs-Url: https://github.com/geturbackend/urBackend/sessions/de8db7a6-e16f-4427-8b4e-7737877312b3 Co-authored-by: Shivkumardhakad <202952715+Shivkumardhakad@users.noreply.github.com> --- apps/public-api/src/app.js | 2 +- apps/public-api/src/controllers/health.controller.js | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/public-api/src/app.js b/apps/public-api/src/app.js index 0bd36d8f0..db5cbc362 100644 --- a/apps/public-api/src/app.js +++ b/apps/public-api/src/app.js @@ -79,7 +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', healthRoute); +app.use('/api/health', limiter, logger, healthRoute); app.get('/api/server-ip', async (req, res) => { const ip = await getPublicIp(); diff --git a/apps/public-api/src/controllers/health.controller.js b/apps/public-api/src/controllers/health.controller.js index 0ca43a03e..750711f6c 100644 --- a/apps/public-api/src/controllers/health.controller.js +++ b/apps/public-api/src/controllers/health.controller.js @@ -1,5 +1,6 @@ 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; @@ -7,7 +8,10 @@ const getHealth = async (req, res) => { let isRedisConnected = false; if (redis?.status === 'ready' && typeof redis.ping === 'function') { try { - const pingResponse = await redis.ping(); + const pingResponse = await Promise.race([ + redis.ping(), + new Promise((resolve) => setTimeout(() => resolve('TIMEOUT'), REDIS_PING_TIMEOUT_MS)), + ]); isRedisConnected = pingResponse === 'PONG'; } catch (_error) { isRedisConnected = false;