diff --git a/api/src/category.router.test.ts b/api/src/category.router.test.ts index 87b418d..2886882 100644 --- a/api/src/category.router.test.ts +++ b/api/src/category.router.test.ts @@ -6,99 +6,99 @@ import { Request, Response } from 'express'; // 1. Generate a mock factory for the Express Response object function createMockResponse() { - const res: Partial = {}; - const data: { statusCode: number; jsonPayload: any } = { statusCode: 200, jsonPayload: null }; + const res: Partial = {}; + const data: { statusCode: number; jsonPayload: any } = { statusCode: 200, jsonPayload: null }; - res.status = function (code: number) { - data.statusCode = code; - return this as Response; - }; + res.status = function (code: number) { + data.statusCode = code; + return this as Response; + }; - res.json = function (payload: any) { - data.jsonPayload = payload; - return this as Response; - }; + res.json = function (payload: any) { + data.jsonPayload = payload; + return this as Response; + }; - return { mockRes: res as Response, data }; + return { mockRes: res as Response, data }; } // 2. Generate a mock factory for the repository layer function createMockRepository(behavior: 'success' | 'failure') { - return { - findAllDescendants: async (path: string) => { - if (behavior === 'failure') throw new Error('Database connection failed'); - // Return a valid single-root array format mimicking real ltree database outputs - return [ - { id: 1, name: 'Top', path: 'Top' }, - { id: 2, name: 'Science', path: 'Top.Science' } - ]; - } - } as unknown as CategoryRepository; + return { + findAllDescendants: async (path: string) => { + if (behavior === 'failure') throw new Error('Database connection failed'); + // Return a valid single-root array format mimicking real ltree database outputs + return [ + { id: 1, name: 'Top', path: 'Top' }, + { id: 2, name: 'Science', path: 'Top.Science' } + ]; + } + } as unknown as CategoryRepository; } // Helper utility to safely extract the Express handler bypassing strict undefined types function getRouteHandler(router: any): Function { - const routeLayer = router.stack.find((layer: any) => layer.route); - if (!routeLayer || !routeLayer.route || !routeLayer.route.stack) { - throw new Error('Could not find route handler in mock router stack'); - } - return routeLayer.route.stack[0].handle; // Target the primary callback handler array element + const routeLayer = router.stack.find((layer: any) => layer.route); + if (!routeLayer || !routeLayer.route || !routeLayer.route.stack) { + throw new Error('Could not find route handler in mock router stack'); + } + return routeLayer.route.stack[0].handle; // Target the primary callback handler array element } describe('Category Router Endpoints', () => { - - it('GET /tree{/:path} - should return 200 and structural JSON array data on valid requests', async () => { - const mockRepo = createMockRepository('success'); - const router = createCategoryRouter(mockRepo); - - const mockReq = { params: { path: 'Top' } } as unknown as Request; - const { mockRes, data } = createMockResponse(); - const routeHandler = getRouteHandler(router); - await routeHandler(mockReq, mockRes); + it('GET /tree{/:path} - should return 200 and structural JSON array data on valid requests', async () => { + const mockRepo = createMockRepository('success'); + const router = createCategoryRouter(mockRepo); - assert.equal(data.statusCode, 200); - assert.equal(data.jsonPayload.success, true); - - // Correct alignment: buildCategoryTree returns an ARRAY of root nodes - assert.equal(Array.isArray(data.jsonPayload.data), true); - assert.equal(data.jsonPayload.data[0].name, 'Top'); - }); + const mockReq = { params: { path: 'Top' } } as unknown as Request; + const { mockRes, data } = createMockResponse(); - it('GET /tree{/:path} - should return 400 Bad Request if ltree path contains malformed formatting', async () => { - const mockRepo = createMockRepository('success'); - const router = createCategoryRouter(mockRepo); - - const mockReq = { params: { path: 'Top.Bad Path!' } } as unknown as Request; - const { mockRes, data } = createMockResponse(); + const routeHandler = getRouteHandler(router); + await routeHandler(mockReq, mockRes); - const routeHandler = getRouteHandler(router); - await routeHandler(mockReq, mockRes); + assert.equal(data.statusCode, 200); + assert.equal(data.jsonPayload.success, true); - assert.equal(data.statusCode, 400); - assert.equal(data.jsonPayload.success, false); - assert.match(data.jsonPayload.error, /Invalid path format/); - }); + // Correct alignment: buildCategoryTree returns an ARRAY of root nodes + assert.equal(Array.isArray(data.jsonPayload.data), true); + assert.equal(data.jsonPayload.data[0].name, 'Top'); + }); - it('GET /tree{/:path} - should handle runtime database crashes gracefully with a 500 error status', async () => { - const mockRepo = createMockRepository('failure'); - const router = createCategoryRouter(mockRepo); - - const mockReq = { params: { path: 'Top' } } as unknown as Request; - const { mockRes, data } = createMockResponse(); + it('GET /tree{/:path} - should return 400 Bad Request if ltree path contains malformed formatting', async () => { + const mockRepo = createMockRepository('success'); + const router = createCategoryRouter(mockRepo); - // Prevent console.error from cluttering the test runner feedback during intentional failures - const originalConsoleError = console.error; - console.error = () => {}; + const mockReq = { params: { path: 'Top.Bad Path!' } } as unknown as Request; + const { mockRes, data } = createMockResponse(); - const routeHandler = getRouteHandler(router); - await routeHandler(mockReq, mockRes); + const routeHandler = getRouteHandler(router); + await routeHandler(mockReq, mockRes); - // Restore original logger tracking - console.error = originalConsoleError; + assert.equal(data.statusCode, 400); + assert.equal(data.jsonPayload.success, false); + assert.match(data.jsonPayload.error, /Invalid path format/); + }); - assert.equal(data.statusCode, 500); - assert.equal(data.jsonPayload.success, false); - assert.equal(data.jsonPayload.error, 'Internal Server Error'); - }); + it('GET /tree{/:path} - should handle runtime database crashes gracefully with a 500 error status', async () => { + const mockRepo = createMockRepository('failure'); + const router = createCategoryRouter(mockRepo); + + const mockReq = { params: { path: 'Top' } } as unknown as Request; + const { mockRes, data } = createMockResponse(); + + // Prevent console.error from cluttering the test runner feedback during intentional failures + const originalConsoleError = console.error; + console.error = () => { }; + + const routeHandler = getRouteHandler(router); + await routeHandler(mockReq, mockRes); + + // Restore original logger tracking + console.error = originalConsoleError; + + assert.equal(data.statusCode, 500); + assert.equal(data.jsonPayload.success, false); + assert.equal(data.jsonPayload.error, 'Internal Server Error'); + }); }); diff --git a/api/src/category.router.ts b/api/src/category.router.ts index d483426..eb5dd33 100644 --- a/api/src/category.router.ts +++ b/api/src/category.router.ts @@ -6,31 +6,31 @@ import { buildCategoryTree } from './tree.utility.ts'; const LTREE_REGEX = /^[A-Za-z0-9_]+(\.[A-Za-z0-9_]+)*$/; export function createCategoryRouter(repository: CategoryRepository): Router { - const router = Router(); + const router = Router(); - router.get('/tree{/:path}', async (req: Request, res: Response): Promise => { - try { - const rawPath = req.params.path; - const parentPath = Array.isArray(rawPath) ? rawPath[0] : (rawPath || 'Top'); + router.get('/tree{/:path}', async (req: Request, res: Response): Promise => { + try { + const rawPath = req.params.path; + const parentPath = Array.isArray(rawPath) ? rawPath[0] : (rawPath || 'Top'); - // 1. INPUT VALIDATION: Stop malicious or broken ltree strings early - if (!LTREE_REGEX.test(parentPath)) { - res.status(400).json({ - success: false, - error: 'Invalid path format. Path elements must be alphanumeric separated by dots.' - }); - return; // Break execution early - } + // 1. INPUT VALIDATION: Stop malicious or broken ltree strings early + if (!LTREE_REGEX.test(parentPath)) { + res.status(400).json({ + success: false, + error: 'Invalid path format. Path elements must be alphanumeric separated by dots.' + }); + return; // Break execution early + } - const flatRows = await repository.findAllDescendants(parentPath); - const nestedTree = buildCategoryTree(flatRows); - - res.json({ success: true, data: nestedTree }); - } catch (error) { - console.error('Failed to resolve category tree:', error); - res.status(500).json({ success: false, error: 'Internal Server Error' }); - } - }); + const flatRows = await repository.findAllDescendants(parentPath); + const nestedTree = buildCategoryTree(flatRows); - return router; + res.json({ success: true, data: nestedTree }); + } catch (error) { + console.error('Failed to resolve category tree:', error); + res.status(500).json({ success: false, error: 'Internal Server Error' }); + } + }); + + return router; }