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