Skip to content

Commit cdf0e7a

Browse files
Fix Codacy issues and improve test coverage across backend and frontend
Suppress false-positive Codacy rules via .codacy.yml. Apply mechanical fixes across all backend route files: extract shared error handling into route-helpers.ts (asyncHandler, handleValidationError, validateBody), replace any types with proper TypeScript interfaces, use import type for type-only imports, and add node: protocol prefixes. Decompose complex functions in app.ts and seed-demo.ts into smaller helpers. Add backend tests for security middleware, logger, and route-helpers (844 total backend tests passing). Add frontend component tests for PageHeader, HelpTip, SearchSelect, SideBar, and TagInput (315 total frontend tests passing). Fix Teleport stub, vue-router mock, cookie leak, and jsdom focus issues in frontend test infrastructure. Signed-off-by: Steve Springett <steve@springett.us>
1 parent 38a62e1 commit cdf0e7a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

64 files changed

+9128
-6113
lines changed

.codacy.yml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
---
2+
exclude_paths:
3+
- node_modules
4+
- dist
5+
- coverage
6+
- backend/package-lock.json
7+
- frontend/package-lock.json
8+
- "**/*.test.ts"
9+
- "**/*.test.tsx"
10+
- "**/*.spec.ts"
11+
- "**/*.spec.tsx"
12+
- "**/tests/**"
13+
- "**/test/**"
14+
15+
engines:
16+
biome:
17+
enabled: true
18+
exclude_paths:
19+
- node_modules
20+
- dist
21+
- coverage
22+
disable_rules:
23+
- Biome_lint_correctness_useQwikValidLexicalScope
24+
- Biome_lint_correctness_useHookAtTopLevel
25+
26+
securitypatterns:
27+
enabled: true
28+
exclude_paths:
29+
- node_modules
30+
- dist
31+
- coverage
32+
- "**/*.test.ts"
33+
- "**/*.test.tsx"
34+
- "**/*.spec.ts"
35+
- "**/*.spec.tsx"
36+
- "**/tests/**"
37+
- "**/test/**"
38+
disable_rules:
39+
- detect-object-injection

backend/src/__tests__/http/notification-rules.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -332,7 +332,7 @@ describe('Notification Rules API', () => {
332332
});
333333

334334
expect(res.status).toBe(400);
335-
expect(res.body.error).toContain('Validation failed');
335+
expect(res.body.error).toContain('Invalid input');
336336
});
337337

338338
it('should allow wildcard event type', async () => {
Lines changed: 323 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,323 @@
1+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2+
import type { Request, Response, NextFunction } from 'express';
3+
import { requestIdMiddleware, type SecureRequest } from '../../middleware/security.js';
4+
5+
// Mock uuid
6+
vi.mock('uuid', () => ({
7+
v4: vi.fn(() => 'mocked-uuid-1234-5678'),
8+
}));
9+
10+
describe('Security Middleware', () => {
11+
let mockReq: Partial<SecureRequest>;
12+
let mockRes: Partial<Response>;
13+
let mockNext: NextFunction;
14+
15+
beforeEach(() => {
16+
mockReq = {
17+
headers: {},
18+
requestId: undefined,
19+
} as any;
20+
21+
mockRes = {
22+
setHeader: vi.fn(),
23+
} as any;
24+
25+
mockNext = vi.fn();
26+
});
27+
28+
afterEach(() => {
29+
vi.clearAllMocks();
30+
});
31+
32+
describe('requestIdMiddleware', () => {
33+
it('should add requestId to request', () => {
34+
requestIdMiddleware(mockReq as SecureRequest, mockRes as Response, mockNext);
35+
36+
expect(mockReq.requestId).toBeDefined();
37+
expect(typeof mockReq.requestId).toBe('string');
38+
});
39+
40+
it('should generate a requestId', () => {
41+
requestIdMiddleware(mockReq as SecureRequest, mockRes as Response, mockNext);
42+
43+
expect(mockReq.requestId).toBe('mocked-uuid-1234-5678');
44+
});
45+
46+
it('should set X-Request-ID header', () => {
47+
requestIdMiddleware(mockReq as SecureRequest, mockRes as Response, mockNext);
48+
49+
expect(mockRes.setHeader).toHaveBeenCalledWith(
50+
'X-Request-ID',
51+
'mocked-uuid-1234-5678'
52+
);
53+
});
54+
55+
it('should call next function', () => {
56+
requestIdMiddleware(mockReq as SecureRequest, mockRes as Response, mockNext);
57+
58+
expect(mockNext).toHaveBeenCalled();
59+
expect(mockNext).toHaveBeenCalledTimes(1);
60+
});
61+
62+
it('should call next even if request already has requestId', () => {
63+
(mockReq as any).requestId = 'existing-id';
64+
65+
requestIdMiddleware(mockReq as SecureRequest, mockRes as Response, mockNext);
66+
67+
expect(mockNext).toHaveBeenCalled();
68+
// requestId should be overwritten with new one
69+
expect(mockReq.requestId).toBe('mocked-uuid-1234-5678');
70+
});
71+
72+
it('should generate unique requestIds for multiple calls', async () => {
73+
const { v4 } = await import('uuid');
74+
const uuidMock = v4 as any;
75+
76+
// Setup mock to return different values
77+
let callCount = 0;
78+
uuidMock.mockImplementation(() => {
79+
callCount++;
80+
return `uuid-${callCount}`;
81+
});
82+
83+
const req1 = { headers: {} } as SecureRequest;
84+
const res1 = { setHeader: vi.fn() } as any as Response;
85+
const next1 = vi.fn();
86+
87+
const req2 = { headers: {} } as SecureRequest;
88+
const res2 = { setHeader: vi.fn() } as any as Response;
89+
const next2 = vi.fn();
90+
91+
requestIdMiddleware(req1, res1, next1);
92+
requestIdMiddleware(req2, res2, next2);
93+
94+
expect(req1.requestId).toBe('uuid-1');
95+
expect(req2.requestId).toBe('uuid-2');
96+
});
97+
98+
it('should set header as string or array of strings', () => {
99+
requestIdMiddleware(mockReq as SecureRequest, mockRes as Response, mockNext);
100+
101+
const headerCall = (mockRes.setHeader as any).mock.calls[0];
102+
expect(headerCall[0]).toBe('X-Request-ID');
103+
// The second argument should be a string or array of strings
104+
expect(headerCall[1]).toBeDefined();
105+
});
106+
107+
it('should work with Express-like request object', () => {
108+
const expressLikeReq: SecureRequest = {
109+
headers: {},
110+
method: 'GET',
111+
url: '/api/test',
112+
requestId: undefined,
113+
} as any;
114+
115+
const expressLikeRes: Response = {
116+
setHeader: vi.fn(),
117+
} as any;
118+
119+
requestIdMiddleware(expressLikeReq, expressLikeRes, mockNext);
120+
121+
expect(expressLikeReq.requestId).toBeDefined();
122+
expect(mockNext).toHaveBeenCalled();
123+
});
124+
125+
it('should not throw on missing headers object', () => {
126+
const reqWithoutHeaders = { requestId: undefined } as SecureRequest;
127+
128+
expect(() => {
129+
requestIdMiddleware(reqWithoutHeaders, mockRes as Response, mockNext);
130+
}).not.toThrow();
131+
132+
expect(mockNext).toHaveBeenCalled();
133+
});
134+
135+
it('should preserve existing request properties', () => {
136+
const enrichedReq: SecureRequest = {
137+
headers: { 'user-agent': 'test-agent' },
138+
method: 'POST',
139+
url: '/api/users',
140+
requestId: undefined,
141+
} as any;
142+
143+
requestIdMiddleware(enrichedReq, mockRes as Response, mockNext);
144+
145+
expect(enrichedReq.headers?.['user-agent']).toBe('test-agent');
146+
expect(enrichedReq.method).toBe('POST');
147+
expect(enrichedReq.url).toBe('/api/users');
148+
expect(enrichedReq.requestId).toBeDefined();
149+
});
150+
151+
it('should handle multiple header operations', () => {
152+
const multiHeaderRes: Response = {
153+
setHeader: vi.fn(),
154+
} as any;
155+
156+
requestIdMiddleware(mockReq as SecureRequest, multiHeaderRes, mockNext);
157+
158+
expect(multiHeaderRes.setHeader).toHaveBeenCalledTimes(1);
159+
expect(multiHeaderRes.setHeader).toHaveBeenCalledWith(
160+
'X-Request-ID',
161+
expect.any(String)
162+
);
163+
});
164+
165+
it('should maintain requestId across middleware chain', () => {
166+
requestIdMiddleware(mockReq as SecureRequest, mockRes as Response, mockNext);
167+
168+
const requestId1 = mockReq.requestId;
169+
170+
// Simulate calling middleware again (which shouldn't happen, but test the behavior)
171+
const nextMockReq: SecureRequest = {
172+
...mockReq,
173+
requestId: requestId1,
174+
} as any;
175+
176+
requestIdMiddleware(nextMockReq, mockRes as Response, mockNext);
177+
178+
// RequestId should be overwritten with a new UUID
179+
expect(nextMockReq.requestId).toBeDefined();
180+
expect(typeof nextMockReq.requestId).toBe('string');
181+
expect(nextMockReq.requestId!.length).toBeGreaterThan(0);
182+
});
183+
});
184+
185+
describe('SecureRequest interface', () => {
186+
it('should extend Request with requestId', () => {
187+
const secureReq: SecureRequest = {
188+
headers: {},
189+
requestId: 'test-id',
190+
} as any;
191+
192+
expect(secureReq.requestId).toBe('test-id');
193+
});
194+
195+
it('should allow optional requestId', () => {
196+
const secureReq: SecureRequest = {
197+
headers: {},
198+
} as any;
199+
200+
expect(secureReq.requestId).toBeUndefined();
201+
});
202+
203+
it('should maintain Request properties', () => {
204+
const secureReq: SecureRequest = {
205+
headers: { 'content-type': 'application/json' },
206+
method: 'POST',
207+
url: '/api/data',
208+
requestId: 'req-123',
209+
} as any;
210+
211+
expect(secureReq.headers?.['content-type']).toBe('application/json');
212+
expect(secureReq.method).toBe('POST');
213+
expect(secureReq.url).toBe('/api/data');
214+
expect(secureReq.requestId).toBe('req-123');
215+
});
216+
});
217+
218+
describe('Middleware integration scenarios', () => {
219+
it('should work as first middleware in chain', () => {
220+
const req: SecureRequest = { headers: {} } as any;
221+
const res = { setHeader: vi.fn() } as any;
222+
const next = vi.fn();
223+
224+
requestIdMiddleware(req, res, next);
225+
226+
expect(req.requestId).toBeDefined();
227+
expect(next).toHaveBeenCalled();
228+
});
229+
230+
it('should work in middleware chain with other middlewares', () => {
231+
const req: SecureRequest = { headers: {} } as any;
232+
const res = { setHeader: vi.fn() } as any;
233+
234+
const middlewares = [
235+
(r: SecureRequest, rs: Response, n: NextFunction) => {
236+
r.headers = { ...r.headers, 'x-custom': 'value' };
237+
n();
238+
},
239+
requestIdMiddleware,
240+
];
241+
242+
const next = vi.fn();
243+
let chainCalled = false;
244+
245+
middlewares[0](req, res, () => {
246+
middlewares[1](req, res, () => {
247+
chainCalled = true;
248+
});
249+
});
250+
251+
expect(chainCalled).toBe(true);
252+
expect(req.requestId).toBeDefined();
253+
expect(req.headers?.['x-custom']).toBe('value');
254+
});
255+
256+
it('should handle async operations after requestId is set', async () => {
257+
const req: SecureRequest = { headers: {} } as any;
258+
const res = { setHeader: vi.fn() } as any;
259+
260+
requestIdMiddleware(req, res, async () => {
261+
// Simulate async operation
262+
await new Promise((resolve) => setTimeout(resolve, 10));
263+
expect(req.requestId).toBeDefined();
264+
});
265+
266+
expect(req.requestId).toBeDefined();
267+
});
268+
269+
it('should allow downstream handlers to access requestId', () => {
270+
const req: SecureRequest = { headers: {} } as any;
271+
const res = { setHeader: vi.fn() } as any;
272+
273+
let downstreamRequestId: string | undefined;
274+
275+
requestIdMiddleware(req, res, () => {
276+
downstreamRequestId = req.requestId;
277+
});
278+
279+
expect(downstreamRequestId).toBeDefined();
280+
expect(downstreamRequestId).toBe(req.requestId);
281+
});
282+
283+
it('should set header correctly for client to receive requestId', () => {
284+
const req: SecureRequest = { headers: {} } as any;
285+
const res = { setHeader: vi.fn() } as any;
286+
287+
requestIdMiddleware(req, res, vi.fn());
288+
289+
const setHeaderCall = (res.setHeader as any).mock.calls[0];
290+
expect(setHeaderCall[0]).toBe('X-Request-ID');
291+
// Header value should match the requestId assigned to the request
292+
expect(setHeaderCall[1]).toBe(req.requestId);
293+
});
294+
});
295+
296+
describe('UUID generation behavior', () => {
297+
it('should call uuid.v4 to generate requestId', async () => {
298+
const { v4 } = await import('uuid');
299+
const uuidMock = v4 as any;
300+
301+
uuidMock.mockClear();
302+
303+
requestIdMiddleware(mockReq as SecureRequest, mockRes as Response, mockNext);
304+
305+
expect(uuidMock).toHaveBeenCalled();
306+
});
307+
308+
it('should use uuid.v4 result as requestId', async () => {
309+
const { v4 } = await import('uuid');
310+
const uuidMock = v4 as any;
311+
312+
const testUuid = 'test-uuid-12345';
313+
uuidMock.mockReturnValue(testUuid);
314+
315+
const req: SecureRequest = { headers: {} } as any;
316+
const res = { setHeader: vi.fn() } as any;
317+
318+
requestIdMiddleware(req, res, vi.fn());
319+
320+
expect(req.requestId).toBe(testUuid);
321+
});
322+
});
323+
});

0 commit comments

Comments
 (0)