Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
5 changes: 5 additions & 0 deletions api/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const mockApp = () => ({

const expressMock: any = jest.fn(() => mockApp());
expressMock.static = jest.fn(() => 'static');
expressMock.Router = jest.fn(() => ({ get: jest.fn() }));

jest.mock('express', () => expressMock);

Expand All @@ -18,10 +19,12 @@ jest.mock('../config/db', () => connectMock);
const appRouter = 'appRouter';
const authRouter = 'authRouter';
const watchlistRouter = 'watchlistRouter';
const healthRouter = 'healthRouter';

jest.mock('../routes/app', () => appRouter);
jest.mock('../routes/auth', () => authRouter);
jest.mock('../routes/watchlist', () => watchlistRouter);
jest.mock('../routes/health', () => healthRouter);

const baseConfig = {
APP_NAME: 'name',
Expand Down Expand Up @@ -49,6 +52,7 @@ describe('api/index', () => {
expect(res.locals.APP_NAME).toBe('name');
expect(res.locals.CARD_TYPE).toBe('card-add');
expect(connectMock).toHaveBeenCalled();
expect(app.use).toHaveBeenCalledWith('/health', healthRouter);
expect(app.use).toHaveBeenCalledWith('/', appRouter);
expect(app.use).toHaveBeenCalledWith('/user', authRouter);
expect(app.use).toHaveBeenCalledWith('/watchlist', watchlistRouter);
Expand All @@ -64,6 +68,7 @@ describe('api/index', () => {
localsMiddleware({}, res, () => {});
expect(res.locals.CARD_TYPE).toBe('card');
expect(connectMock).not.toHaveBeenCalled();
expect(app.use).toHaveBeenCalledWith('/health', healthRouter);
expect(app.use).toHaveBeenCalledWith('/', appRouter);
expect(app.use).not.toHaveBeenCalledWith('/user', authRouter);
});
Expand Down
16 changes: 6 additions & 10 deletions api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* @license MIT
*/

import express, { Request, Response, NextFunction } from 'express';
import express from 'express';
import path from 'path';
import { inject } from '@vercel/analytics';

Expand All @@ -16,6 +16,8 @@ import connectDB from '../config/db';
import { useAuth } from '../helpers/appHelper';
import appRouter from '../routes/app';
import authRouter from '../routes/auth';
import healthRouter from '../routes/health';
import appLocals from '../middleware/appLocals';
import watchlistRouter from '../routes/watchlist';

/**
Expand All @@ -35,15 +37,7 @@ inject({
* @param {Response} res - The response object containing the HTTP response details.
* @param {NextFunction} next - The next middleware function in the chain.
*/
app.use((req: Request, res: Response, next: NextFunction) => {
res.locals.APP_NAME = appConfig.APP_NAME;
res.locals.APP_SUBTITLE = appConfig.APP_SUBTITLE;
res.locals.APP_DESCRIPTION = appConfig.APP_DESCRIPTION;
res.locals.APP_URL = appConfig.APP_URL;
// We have different card types based on whether the app uses MongoDB or not.
res.locals.CARD_TYPE = useAuth ? 'card-add' : 'card';
next();
});
app.use(appLocals);

app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, '../views'));
Expand All @@ -58,6 +52,7 @@ app.use(express.static('public'));
* Load standard routes and conditionally use additional routes based on the value of useAuth boolean.
* The method checks if MONGO_DB_URI is true then connects to MongoDB and uses additional middleware.
*/
app.use('/health', healthRouter);
app.use('/', appRouter);
// Test if MONGO_DB_URI is set.
if (useAuth) {
Expand All @@ -75,6 +70,7 @@ if (useAuth) {
* @returns {void} - No return value.
*/
app.listen(appConfig.API_PORT, appConfig.API_HOST, () => {
/* c8 ignore next */
console.log(`Server is running on http://${appConfig.API_HOST}:${appConfig.API_PORT}`);
});

Expand Down
202 changes: 202 additions & 0 deletions controllers/healthController.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
import { Request, Response } from 'express';

import appConfig from '../config/app';
import httpClient from '../helpers/httpClient';
import healthController, { checkDomainHealth } from './healthController';

jest.mock('../helpers/httpClient', () => ({
__esModule: true,
default: {
get: jest.fn(),
},
}));

type MockedHttpClient = jest.Mocked<typeof httpClient>;
const mockedHttpClient = httpClient as MockedHttpClient;

describe('checkDomainHealth', () => {
beforeEach(() => {
jest.clearAllMocks();
});

test('returns error when domain is not configured', async () => {
const result = await checkDomainHealth('VIDSRC_DOMAIN', undefined);
expect(result).toEqual({
name: 'VIDSRC_DOMAIN',
status: 'error',
message: 'Domain not configured',
});
});

test('returns success when endpoint responds with < 400 status', async () => {
mockedHttpClient.get.mockResolvedValue({ status: 200 } as any);
const result = await checkDomainHealth('VIDSRC_DOMAIN', 'vidsrc.example');
expect(result).toEqual({
name: 'VIDSRC_DOMAIN',
domain: 'vidsrc.example',
status: 'success',
httpStatus: 200,
});
expect(mockedHttpClient.get).toHaveBeenCalledWith('https://vidsrc.example', expect.any(Object));
});

test('returns error when endpoint responds with >= 400 status', async () => {
mockedHttpClient.get.mockResolvedValue({ status: 404 } as any);
const result = await checkDomainHealth('VIDSRC_DOMAIN', 'vidsrc.example');
expect(result).toEqual({
name: 'VIDSRC_DOMAIN',
domain: 'vidsrc.example',
status: 'error',
httpStatus: 404,
message: 'Received status code 404',
});
});

test('returns error when request throws', async () => {
mockedHttpClient.get.mockRejectedValue(new Error('boom'));
const result = await checkDomainHealth('VIDSRC_DOMAIN', 'vidsrc.example');
expect(result).toEqual({
name: 'VIDSRC_DOMAIN',
domain: 'vidsrc.example',
status: 'error',
message: 'boom',
});
});
});

describe('healthController', () => {
const createRes = () => {
const res: Partial<Response> = {};
res.statusCode = 200;
res.status = jest.fn().mockImplementation((code: number) => {
res.statusCode = code;
return res;
});
res.json = jest.fn().mockReturnValue(res);
return res as Response;
};

beforeEach(() => {
jest.clearAllMocks();
(appConfig as any).VIDSRC_DOMAIN = 'vidsrc.example';
(appConfig as any).MULTI_DOMAIN = 'multi.example';
(appConfig as any).APP_URL = 'https://app.example';
});

test('getEmbedDomains returns results for both domains', async () => {
mockedHttpClient.get
.mockResolvedValueOnce({ status: 200 } as any)
.mockResolvedValueOnce({ status: 503 } as any);

const res = createRes();
await healthController.getEmbedDomains({ query: {} } as unknown as Request, res);

expect(res.status).toHaveBeenCalledWith(503);
expect(res.json).toHaveBeenCalledWith({
status: 'error',
domains: [
{
name: 'VIDSRC_DOMAIN',
domain: 'vidsrc.example',
status: 'success',
httpStatus: 200,
},
{
name: 'MULTI_DOMAIN',
domain: 'multi.example',
status: 'error',
httpStatus: 503,
message: 'Received status code 503',
},
],
});
});

test('getAppUrl returns single domain result', async () => {
mockedHttpClient.get.mockResolvedValue({ status: 301 } as any);
const res = createRes();
await healthController.getAppUrl({} as Request, res);
expect(res.status).toHaveBeenCalledWith(200);
expect(res.json).toHaveBeenCalledWith({
name: 'APP_URL',
domain: 'https://app.example',
status: 'success',
httpStatus: 301,
});
});

test('getEmbedDomains handles missing MULTI_DOMAIN', async () => {
(appConfig as any).MULTI_DOMAIN = undefined;
mockedHttpClient.get.mockResolvedValue({ status: 200 } as any);

const res = createRes();
await healthController.getEmbedDomains({ query: {} } as unknown as Request, res);

expect(res.status).toHaveBeenCalledWith(503);
expect(res.json).toHaveBeenCalledWith({
status: 'error',
domains: [
{
name: 'VIDSRC_DOMAIN',
domain: 'vidsrc.example',
status: 'success',
httpStatus: 200,
},
{
name: 'MULTI_DOMAIN',
status: 'error',
message: 'Domain not configured',
},
],
});
});

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: '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',
});
});
});
Loading