Skip to content

Commit 7a74758

Browse files
renovate[bot]kyle1morel
authored andcommitted
fix(deps): update dependency js-yaml to v5
1 parent 92b6bb9 commit 7a74758

5 files changed

Lines changed: 147 additions & 79 deletions

File tree

app/package-lock.json

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@
7777
"express-winston": "4.2.0",
7878
"helmet": "8.2.0",
7979
"joi": "18.2.1",
80-
"js-yaml": "4.2.0",
80+
"js-yaml": "5.2.1",
8181
"jsonwebtoken": "9.0.3",
8282
"jwks-rsa": "4.0.1",
8383
"knex": "3.2.10",

app/src/docs/docs.ts

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,28 @@
1-
const docs = {
2-
getDocHTML: (version: string) => `<!DOCTYPE html>
1+
import { load } from 'js-yaml';
2+
import { readFileSync } from 'node:fs';
3+
import { join } from 'node:path';
4+
5+
/** A partial OpenAPI specification schema structure */
6+
interface OpenAPISpec {
7+
security?: [];
8+
servers: { url: string }[];
9+
components: {
10+
securitySchemes?: {
11+
OpenID: {
12+
openIdConnectUrl?: string;
13+
};
14+
};
15+
};
16+
}
17+
18+
/**
19+
* Generates a ReDocs HTML string for the documentation page of the NR
20+
* Permitting Exchange, Aggregation and Collection Hub (PEACH) API.
21+
* @param version - The version of the API documentation to display. Defaults to 'v1'.
22+
* @returns The HTML string for the documentation page.
23+
*/
24+
export function getDocHTML(version = 'v1'): string {
25+
return `<!DOCTYPE html>
326
<html>
427
<head>
528
<title>NR PermitConnect Navigator Service (PCNS) API - Documentation ${version}</title>
@@ -17,7 +40,16 @@ const docs = {
1740
<redoc spec-url='/api/${version}/docs/api-spec.yaml' />
1841
<script src="https://cdn.redoc.ly/redoc/latest/bundles/redoc.standalone.js"></script>
1942
</body>
20-
</html>`
21-
};
43+
</html>`;
44+
}
2245

23-
export default docs;
46+
/**
47+
* Gets the OpenAPI specification
48+
* @returns The OpenAPI spec
49+
*/
50+
export function getSpec(): OpenAPISpec | undefined {
51+
const rawSpec = readFileSync(join(__dirname, '../../docs/v1.api-spec.yaml'), 'utf8');
52+
const spec = load(rawSpec) as OpenAPISpec;
53+
spec.servers[0].url = '/api/v1';
54+
return spec;
55+
}

app/src/routes/v1/docs.ts

Lines changed: 31 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,83 +1,55 @@
11
import { Router } from 'express';
2-
import { readFileSync } from 'node:fs';
32
import helmet from 'helmet';
4-
import yaml from 'js-yaml';
5-
import { join } from 'node:path';
3+
import { dump } from 'js-yaml';
64

7-
import docs from '../../docs/docs.ts';
5+
import { getDocHTML, getSpec } from '../../docs/docs';
86

97
import type { Request, Response } from 'express';
108

119
const router = Router();
1210

13-
interface OpenAPISpec {
14-
servers: { url: string }[];
15-
components: {
16-
securitySchemes: {
17-
OpenID: {
18-
openIdConnectUrl?: string;
19-
};
20-
};
21-
};
22-
}
23-
24-
/**
25-
* Gets the OpenAPI specification
26-
* @returns The OpenAPI spec
27-
*/
28-
function getSpec(): OpenAPISpec | undefined {
29-
const rawSpec = readFileSync(join(__dirname, '../../docs/v1.api-spec.yaml'), 'utf8');
30-
const spec = yaml.load(rawSpec) as OpenAPISpec;
31-
spec.servers[0].url = '/api/v1';
32-
return spec;
33-
}
34-
3511
/** OpenAPI Docs */
3612
router.get(
3713
'/',
38-
helmet({
39-
contentSecurityPolicy: {
40-
directives: {
41-
'connect-src': [
42-
"'self'", // eslint-disable-line quotes
43-
'https://raw.githubusercontent.com'
44-
],
45-
// @ts-expect-error ts(2322)
46-
'img-src': [
47-
"'self'", // eslint-disable-line quotes
48-
'data:',
49-
(_req: Request, res: Response): string => `'nonce-${res.locals.cspNonce}'`,
50-
'https://cdn.redoc.ly'
51-
],
52-
// @ts-expect-error ts(2322)
53-
'media-src': [
54-
"'self'", // eslint-disable-line quotes
55-
'data:',
56-
(_req: Request, res: Response): string => `'nonce-${res.locals.cspNonce}'`
57-
],
58-
'script-src': [
59-
'blob:',
60-
"'unsafe-eval'" // eslint-disable-line quotes
61-
],
62-
'script-src-elem': [
63-
'https://cdn.redoc.ly',
64-
"'unsafe-inline'" // eslint-disable-line quotes
65-
]
66-
}
14+
helmet.contentSecurityPolicy({
15+
directives: {
16+
'connect-src': [
17+
"'self'", // eslint-disable-line quotes
18+
'https://raw.githubusercontent.com'
19+
],
20+
'img-src': [
21+
"'self'", // eslint-disable-line quotes
22+
'data:',
23+
(_req, res): string => `'nonce-${(res as Response).locals.cspNonce}'`,
24+
'https://cdn.redoc.ly'
25+
],
26+
'media-src': [
27+
"'self'", // eslint-disable-line quotes
28+
'data:',
29+
(_req, res): string => `'nonce-${(res as Response).locals.cspNonce}'`
30+
],
31+
'script-src': [
32+
'blob:',
33+
"'unsafe-eval'" // eslint-disable-line quotes
34+
],
35+
'script-src-elem': [
36+
'https://cdn.redoc.ly',
37+
"'unsafe-inline'" // eslint-disable-line quotes
38+
]
6739
}
6840
}),
6941
(_req: Request, res: Response): void => {
70-
res.send(docs.getDocHTML('v1'));
42+
res.send(getDocHTML());
7143
}
7244
);
7345

7446
/** OpenAPI YAML Spec */
75-
router.get('/api-spec.yaml', (_req: Request, res: Response) => {
76-
res.status(200).type('application/yaml').send(yaml.dump(getSpec()));
47+
router.get('/api-spec.yaml', (_req: Request, res: Response): void => {
48+
res.status(200).type('application/yaml').send(dump(getSpec()));
7749
});
7850

7951
/** OpenAPI JSON Spec */
80-
router.get('/api-spec.json', (_req: Request, res: Response) => {
52+
router.get('/api-spec.json', (_req: Request, res: Response): void => {
8153
res.status(200).json(getSpec());
8254
});
8355

Lines changed: 73 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,99 @@
11
import express from 'express';
2+
import helmet from 'helmet';
23
import request from 'supertest';
4+
import { dump } from 'js-yaml';
35

4-
import router from '../../../../src/routes/v1/docs.ts';
6+
import router from '../../../../src/routes/v1/docs';
7+
import { getDocHTML, getSpec } from '../../../../src/docs/docs';
8+
9+
// Mock dependencies
10+
vi.mock('helmet', () => ({
11+
default: {
12+
contentSecurityPolicy: vi.fn(() => (_req: unknown, _res: unknown, next: () => void) => next())
13+
}
14+
}));
15+
16+
vi.mock('js-yaml', () => ({
17+
dump: vi.fn()
18+
}));
19+
20+
vi.mock('../../../../src/docs/docs', () => ({
21+
getDocHTML: vi.fn(),
22+
getSpec: vi.fn()
23+
}));
524

625
const app = express();
726
app.use(router);
827

928
describe('Docs Router', () => {
29+
beforeEach(() => {
30+
vi.clearAllMocks();
31+
});
32+
1033
describe('GET /', () => {
11-
it('should return OpenAPI documentation HTML', async () => {
34+
it('should return HTML documentation', async () => {
35+
const mockHtml = '<html>Mock Docs</html>';
36+
vi.mocked(getDocHTML).mockReturnValue(mockHtml);
37+
1238
const response = await request(app).get('/');
39+
1340
expect(response.status).toBe(200);
14-
expect(response.text).toContain('<!DOCTYPE html>');
41+
expect(response.text).toBe(mockHtml);
42+
expect(getDocHTML).toHaveBeenCalled();
43+
});
44+
45+
it('should configure CSP with dynamic nonce for img-src and media-src', async () => {
46+
vi.resetModules();
47+
await import('../../../../src/routes/v1/docs.ts');
48+
49+
expect(helmet.contentSecurityPolicy).toHaveBeenCalled();
50+
51+
const config = vi.mocked(helmet.contentSecurityPolicy).mock.calls[0]?.[0] ?? {};
52+
const { 'img-src': imgSrc, 'media-src': mediaSrc } = config?.directives as Record<string, unknown[]>;
53+
const mockRes = { locals: { cspNonce: 'test-nonce' } } as unknown as express.Response;
54+
55+
const imgNonceFn = imgSrc?.find((fn: unknown) => typeof fn === 'function') as (
56+
req: unknown,
57+
res: express.Response
58+
) => string;
59+
const mediaNonceFn = mediaSrc?.find((fn: unknown) => typeof fn === 'function') as (
60+
req: unknown,
61+
res: express.Response
62+
) => string;
63+
64+
expect(imgNonceFn(null, mockRes)).toBe("'nonce-test-nonce'"); // eslint-disable-line quotes
65+
expect(mediaNonceFn(null, mockRes)).toBe("'nonce-test-nonce'"); // eslint-disable-line quotes
1566
});
1667
});
1768

1869
describe('GET /api-spec.yaml', () => {
19-
it('should return OpenAPI YAML spec', async () => {
70+
it('should return YAML specification', async () => {
71+
const mockSpec = { openapi: '3.0.0', servers: [], components: {} };
72+
const mockYaml = 'openapi: 3.0.0';
73+
vi.mocked(getSpec).mockReturnValue(mockSpec);
74+
vi.mocked(dump).mockReturnValue(mockYaml);
75+
2076
const response = await request(app).get('/api-spec.yaml');
77+
2178
expect(response.status).toBe(200);
22-
expect(response.type).toBe('application/yaml');
23-
expect(response.text).toContain('openapi:');
79+
expect(response.headers['content-type']).toContain('application/yaml');
80+
expect(response.text).toBe(mockYaml);
81+
expect(getSpec).toHaveBeenCalled();
82+
expect(dump).toHaveBeenCalledWith(mockSpec);
2483
});
2584
});
2685

2786
describe('GET /api-spec.json', () => {
28-
it('should return OpenAPI JSON spec', async () => {
87+
it('should return JSON specification', async () => {
88+
const mockSpec = { openapi: '3.0.0', servers: [], components: {} };
89+
vi.mocked(getSpec).mockReturnValue(mockSpec);
90+
2991
const response = await request(app).get('/api-spec.json');
92+
3093
expect(response.status).toBe(200);
31-
expect(response.type).toBe('application/json');
32-
expect(response.body).toHaveProperty('openapi');
94+
expect(response.headers['content-type']).toContain('application/json');
95+
expect(response.body).toEqual(mockSpec);
96+
expect(getSpec).toHaveBeenCalled();
3397
});
3498
});
3599
});

0 commit comments

Comments
 (0)