diff --git a/controllers/healthController.spec.ts b/controllers/healthController.spec.ts index 404fd0f..6726a8e 100644 --- a/controllers/healthController.spec.ts +++ b/controllers/healthController.spec.ts @@ -20,7 +20,7 @@ describe('checkDomainHealth', () => { }); test('returns error when domain is not configured', async () => { - const result = await checkDomainHealth('VIDSRC_DOMAIN'); + const result = await checkDomainHealth('VIDSRC_DOMAIN', undefined); expect(result).toEqual({ name: 'VIDSRC_DOMAIN', status: 'error', @@ -67,6 +67,12 @@ describe('checkDomainHealth', () => { describe('healthController', () => { const createRes = () => { const res: Partial = {}; + res.statusCode = 200; + res.status = jest.fn().mockImplementation((code: number) => { + res.statusCode = code; + return res; + }); + res.set = jest.fn().mockReturnValue(res); res.json = jest.fn().mockReturnValue(res); return res as Response; }; @@ -84,9 +90,16 @@ describe('healthController', () => { .mockResolvedValueOnce({ status: 503 } as any); const res = createRes(); - await healthController.getEmbedDomains({} as Request, res); - + await healthController.getEmbedDomains({ query: {} } as unknown as Request, res); + expect(res.set).toHaveBeenCalledWith({ + 'Cache-Control': 'no-store, no-cache, must-revalidate, proxy-revalidate', + Pragma: 'no-cache', + Expires: '0', + 'Surrogate-Control': 'no-store', + }); + expect(res.status).toHaveBeenCalledWith(503); expect(res.json).toHaveBeenCalledWith({ + status: 'error', domains: [ { name: 'VIDSRC_DOMAIN', @@ -109,6 +122,13 @@ describe('healthController', () => { mockedHttpClient.get.mockResolvedValue({ status: 301 } as any); const res = createRes(); await healthController.getAppUrl({} as Request, res); + expect(res.set).toHaveBeenCalledWith({ + 'Cache-Control': 'no-store, no-cache, must-revalidate, proxy-revalidate', + Pragma: 'no-cache', + Expires: '0', + 'Surrogate-Control': 'no-store', + }); + expect(res.status).toHaveBeenCalledWith(200); expect(res.json).toHaveBeenCalledWith({ name: 'APP_URL', domain: 'https://app.example', @@ -122,9 +142,11 @@ describe('healthController', () => { mockedHttpClient.get.mockResolvedValue({ status: 200 } as any); const res = createRes(); - await healthController.getEmbedDomains({} as Request, res); + await healthController.getEmbedDomains({ query: {} } as unknown as Request, res); + expect(res.status).toHaveBeenCalledWith(200); expect(res.json).toHaveBeenCalledWith({ + status: 'success', domains: [ { name: 'VIDSRC_DOMAIN', @@ -132,12 +154,70 @@ describe('healthController', () => { status: 'success', httpStatus: 200, }, + ], + }); + }); + + test('getEmbedDomains rejects multi target when MULTI_DOMAIN is not configured', async () => { + (appConfig as any).MULTI_DOMAIN = undefined; + const res = createRes(); + + await healthController.getEmbedDomains({ query: { target: 'multi' } } as unknown as Request, res); + + expect(res.status).toHaveBeenCalledWith(400); + expect(res.json).toHaveBeenCalledWith({ + status: 'error', + message: 'Target not configured', + domains: [], + }); + }); + + test('getEmbedDomains filters by target query and returns 200 when healthy', async () => { + mockedHttpClient.get.mockResolvedValue({ status: 200 } as any); + + const res = createRes(); + await healthController.getEmbedDomains({ query: { target: 'vidsrc' } } as unknown as Request, res); + + expect(res.status).toHaveBeenCalledWith(200); + expect(res.json).toHaveBeenCalledWith({ + status: 'success', + domains: [ { - name: 'MULTI_DOMAIN', - status: 'error', - message: 'Domain not configured', + name: 'VIDSRC_DOMAIN', + domain: 'vidsrc.example', + status: 'success', + httpStatus: 200, }, ], }); }); + + test('getEmbedDomains rejects invalid target', async () => { + const res = createRes(); + + await healthController.getEmbedDomains({ query: { target: 'unknown' } } as unknown as Request, res); + + expect(res.status).toHaveBeenCalledWith(400); + expect(res.json).toHaveBeenCalledWith({ + status: 'error', + message: 'Invalid target', + domains: [], + }); + }); + + test('getAppUrl returns 503 when app url is unhealthy', async () => { + mockedHttpClient.get.mockResolvedValue({ status: 500 } as any); + const res = createRes(); + + await healthController.getAppUrl({} as Request, res); + + expect(res.status).toHaveBeenCalledWith(503); + expect(res.json).toHaveBeenCalledWith({ + name: 'APP_URL', + domain: 'https://app.example', + status: 'error', + httpStatus: 500, + message: 'Received status code 500', + }); + }); }); diff --git a/controllers/healthController.ts b/controllers/healthController.ts index b1d6526..a744019 100644 --- a/controllers/healthController.ts +++ b/controllers/healthController.ts @@ -18,13 +18,13 @@ export interface DomainHealthResult { message?: string; } -const normalizeUrl = (domain: string): string => { +const normaliseUrl = (domain: string): string => { return /^https?:\/\//i.test(domain) ? domain : `https://${domain}`; }; export const checkDomainHealth = async ( name: string, - domain?: string + domain: string | undefined ): Promise => { if (!domain) { return { @@ -35,7 +35,7 @@ export const checkDomainHealth = async ( } try { - const response = await httpClient.get(normalizeUrl(domain), { + const response = await httpClient.get(normaliseUrl(domain), { maxRedirects: 0, /* c8 ignore next */ validateStatus: () => true, @@ -72,6 +72,19 @@ export const checkDomainHealth = async ( } }; +const isHealthy = (results: DomainHealthResult[]): boolean => { + return results.every((result) => result.status === 'success'); +}; + +const setNoCacheHeaders = (res: Response): void => { + res.set({ + 'Cache-Control': 'no-store, no-cache, must-revalidate, proxy-revalidate', + Pragma: 'no-cache', + Expires: '0', + 'Surrogate-Control': 'no-store', + }); +}; + const healthController = { /** * Checks the configured embed domains and returns their reachability status. @@ -79,13 +92,43 @@ const healthController = { * @param {Response} res - Express response object. * @returns {Promise} JSON response containing domain health information. */ - async getEmbedDomains(_req: Request, res: Response): Promise { - const domains = await Promise.all([ - checkDomainHealth('VIDSRC_DOMAIN', appConfig.VIDSRC_DOMAIN), - checkDomainHealth('MULTI_DOMAIN', appConfig.MULTI_DOMAIN), - ]); + async getEmbedDomains(req: Request, res: Response): Promise { + setNoCacheHeaders(res); + const target = typeof req.query.target === 'string' ? req.query.target.toLowerCase() : undefined; + + // Return 400 if there is no `MULTI_DOMAIN` configured + if (target === 'multi' && !appConfig.MULTI_DOMAIN) { + return res.status(400).json({ + status: 'error', + message: 'Target not configured', + domains: [], + }); + } + + const checks: Array> = []; + + if (!target || target === 'vidsrc') { + checks.push(checkDomainHealth('VIDSRC_DOMAIN', appConfig.VIDSRC_DOMAIN)); + } + + if (!target || target === 'multi') { + // This makes sure we make this optional as there will be scenarios where MULTI_DOMAIN isn't configured + if (appConfig.MULTI_DOMAIN) { + checks.push(checkDomainHealth('MULTI_DOMAIN', appConfig.MULTI_DOMAIN)); + } + } + + if (checks.length === 0) { + return res.status(400).json({ + status: 'error', + message: 'Invalid target', + domains: [], + }); + } - return res.json({ domains }); + const domains = await Promise.all(checks); + const healthy = isHealthy(domains); + return res.status(healthy ? 200 : 503).json({ domains, status: healthy ? 'success' : 'error' }); }, /** @@ -95,8 +138,10 @@ const healthController = { * @returns {Promise} JSON response containing APP_URL health information. */ async getAppUrl(_req: Request, res: Response): Promise { + setNoCacheHeaders(res); const domain = await checkDomainHealth('APP_URL', appConfig.APP_URL); - return res.json(domain); + const healthy = domain.status === 'success'; + return res.status(healthy ? 200 : 503).json(domain); }, };