Skip to content

Commit e6fe28e

Browse files
authored
fix: replace koa-helmet (#379)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Chores** * Updated security middleware dependency to version 8.1.0. * Removed legacy dependency. * **Refactor** * Restructured middleware configuration for improved framework compatibility. * **Tests** * Updated test suite to reflect middleware changes. <sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub> <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent 5b0a459 commit e6fe28e

File tree

9 files changed

+130
-36
lines changed

9 files changed

+130
-36
lines changed

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,12 @@
2020
"//": "Dependencies required at runtime",
2121
"dependencies": {
2222
"@graphile-contrib/pg-simplify-inflector": "6.1.0",
23-
"@koa/router": "15.0.0",
2423
"@koa/bodyparser": "6.0.0",
24+
"@koa/router": "15.0.0",
2525
"dotenv": "17.2.3",
26+
"helmet": "8.1.0",
2627
"koa": "3.1.1",
2728
"koa-compress": "5.1.1",
28-
"koa-helmet": "8.0.1",
2929
"pg": "8.16.3",
3030
"postgraphile": "4.14.1"
3131
},

pnpm-lock.yaml

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

src/index.test.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import config from './config.js'
44
import {
55
bodyParser,
66
compress,
7-
helmet,
7+
koaHelmet,
88
postGraphile,
99
} from './middleware/index.js'
1010
import { healthRouter } from './router/index.js'
@@ -27,7 +27,10 @@ vi.mock('./config.js', () => ({
2727
vi.mock('./middleware/index.js', () => ({
2828
bodyParser: vi.fn().mockName('bodyParser'),
2929
compress: vi.fn().mockName('compress'),
30-
helmet: vi.fn().mockName('helmet'),
30+
koaHelmet: vi
31+
.fn()
32+
.mockName('koaHelmet')
33+
.mockReturnValue(vi.fn().mockName('helmet-middleware')),
3134
postGraphile: vi.fn().mockName('postGraphile'),
3235
}))
3336
vi.mock('./router/index.js', () => ({
@@ -43,7 +46,7 @@ describe('index', () => {
4346
})
4447

4548
it('should be tested', async () => {
46-
expect.assertions(12)
49+
expect.assertions(13)
4750

4851
await import('./index.js')
4952

@@ -53,7 +56,8 @@ describe('index', () => {
5356
expect.soft(mockKoaInstance.use).toHaveBeenCalledTimes(6)
5457
expect.soft(mockKoaInstance.use).toHaveBeenCalledWith(bodyParser)
5558
expect.soft(mockKoaInstance.use).toHaveBeenCalledWith(compress)
56-
expect.soft(mockKoaInstance.use).toHaveBeenCalledWith(helmet)
59+
expect.soft(koaHelmet).toHaveBeenCalledWith()
60+
expect.soft(mockKoaInstance.use).toHaveBeenCalledWith(koaHelmet())
5761
expect.soft(healthRouter.routes).toHaveBeenCalledOnce()
5862
expect.soft(mockKoaInstance.use).toHaveBeenCalledWith(healthRouter.routes())
5963
expect.soft(healthRouter.allowedMethods).toHaveBeenCalledOnce()

src/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import config from './config.js'
33
import {
44
bodyParser,
55
compress,
6-
helmet,
6+
koaHelmet,
77
postGraphile,
88
} from './middleware/index.js'
99
import { healthRouter } from './router/index.js'
@@ -14,7 +14,7 @@ app
1414
// register common middleware
1515
.use(bodyParser)
1616
.use(compress)
17-
.use(helmet)
17+
.use(koaHelmet())
1818
// register health router
1919
.use(healthRouter.routes())
2020
.use(healthRouter.allowedMethods())

src/middleware/helmet.test.ts

Lines changed: 89 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,97 @@
1-
import helmet from 'koa-helmet'
2-
import { describe, expect, it, vi } from 'vitest'
1+
import helmet from 'helmet'
2+
import type { Context } from 'koa'
3+
import { beforeEach, describe, expect, it, vi } from 'vitest'
34

4-
vi.mock('koa-helmet')
5+
vi.mock('helmet')
56

67
describe('helmet', () => {
7-
it('should export helmet', async () => {
8+
beforeEach(() => {
9+
vi.clearAllMocks()
10+
})
11+
12+
it('should export a factory that calls helmet and returns Koa middleware', async () => {
13+
expect.assertions(3)
14+
15+
const mockExpressHelmet = vi.fn()
16+
const mockHelmet = vi.mocked(helmet)
17+
mockHelmet.mockReturnValue(mockExpressHelmet)
18+
19+
const { koaHelmet } = await import('./helmet.js')
20+
21+
const middleware = koaHelmet()
22+
23+
expect(mockHelmet).toHaveBeenCalledOnce()
24+
expect(mockHelmet).toHaveBeenCalledWith(undefined)
25+
expect(middleware).toBeTypeOf('function')
26+
})
27+
28+
it('should call helmet with options when provided', async () => {
29+
expect.assertions(3)
30+
31+
const mockExpressHelmet = vi.fn()
32+
const mockHelmet = vi.mocked(helmet)
33+
mockHelmet.mockReturnValue(mockExpressHelmet)
34+
35+
const { koaHelmet } = await import('./helmet.js')
36+
37+
const options = { contentSecurityPolicy: false }
38+
const middleware = koaHelmet(options)
39+
40+
expect(mockHelmet).toHaveBeenCalledOnce()
41+
expect(mockHelmet).toHaveBeenCalledWith(options)
42+
expect(middleware).toBeTypeOf('function')
43+
})
44+
45+
it('should call the express helmet middleware and continue to next', async () => {
846
expect.assertions(2)
947

10-
const { default: actual } = await import('./helmet.js')
48+
const mockExpressHelmet = vi.fn((_req, _res, next) => {
49+
next()
50+
})
51+
const mockHelmet = vi.mocked(helmet)
52+
mockHelmet.mockReturnValue(mockExpressHelmet)
53+
54+
const { koaHelmet } = await import('./helmet.js')
55+
56+
const middleware = koaHelmet()
57+
58+
const ctx = {
59+
req: {},
60+
res: {},
61+
} as Context
62+
const next = vi.fn().mockResolvedValue(undefined)
63+
64+
await middleware(ctx, next)
65+
66+
expect(mockExpressHelmet).toHaveBeenCalledWith(
67+
ctx.req,
68+
ctx.res,
69+
expect.any(Function),
70+
)
71+
expect(next).toHaveBeenCalledOnce()
72+
})
73+
74+
it('should reject promise when express helmet middleware returns error', async () => {
75+
expect.assertions(2)
76+
77+
const error = new Error('Helmet error')
78+
const mockExpressHelmet = vi.fn((_req, _res, callback) => {
79+
callback(error)
80+
})
81+
const mockHelmet = vi.mocked(helmet)
82+
mockHelmet.mockReturnValue(mockExpressHelmet)
83+
84+
const { koaHelmet } = await import('./helmet.js')
85+
86+
const middleware = koaHelmet()
87+
88+
const ctx = {
89+
req: {},
90+
res: {},
91+
} as Context
92+
const next = vi.fn()
1193

12-
expect(helmet.default).toHaveBeenCalledOnce()
13-
expect(actual).toStrictEqual(helmet.default())
94+
await expect(middleware(ctx, next)).rejects.toThrow('Helmet error')
95+
expect(next).not.toHaveBeenCalled()
1496
})
1597
})

src/middleware/helmet.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,20 @@
1-
import helmet from 'koa-helmet'
1+
import helmet, { type HelmetOptions } from 'helmet'
2+
import type { Middleware } from 'koa'
23

3-
export default helmet.default()
4+
/**
5+
* Koa-compatible Helmet middleware.
6+
*
7+
* Accepts the same options helmet() does.
8+
*/
9+
export function koaHelmet(options?: HelmetOptions): Middleware {
10+
const expressHelmet = helmet(options)
11+
return async (ctx, next) => {
12+
await new Promise<void>((resolve, reject) => {
13+
expressHelmet(ctx.req, ctx.res, (err) => {
14+
if (err) return reject(err)
15+
resolve()
16+
})
17+
})
18+
return next()
19+
}
20+
}

src/middleware/index.test.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { describe, expect, it, vi } from 'vitest'
22
import bodyParser from './bodyparser.js'
33
import compress from './compress.js'
4-
import helmet from './helmet.js'
4+
import { koaHelmet } from './helmet.js'
55
import postGraphile from './postgraphile.js'
66

77
vi.mock('./bodyparser', () => ({
@@ -11,7 +11,7 @@ vi.mock('./compress', () => ({
1111
default: vi.fn().mockName('compress'),
1212
}))
1313
vi.mock('./helmet', () => ({
14-
default: vi.fn().mockName('helmet'),
14+
koaHelmet: vi.fn().mockName('helmet'),
1515
}))
1616
vi.mock('./postgraphile', () => ({
1717
default: vi.fn().mockName('postgraphile'),
@@ -26,10 +26,12 @@ describe('index', () => {
2626
const expected = {
2727
bodyParser,
2828
compress,
29-
helmet,
29+
koaHelmet,
3030
postGraphile,
3131
}
3232
expect.soft(actual).toMatchObject(expected)
33-
expect.soft(Object.keys(actual)).toEqual(Object.keys(expected))
33+
expect
34+
.soft(Object.keys(actual).sort())
35+
.toEqual(Object.keys(expected).sort())
3436
})
3537
})

src/middleware/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
export { default as bodyParser } from './bodyparser.js'
22
export { default as compress } from './compress.js'
3-
export { default as helmet } from './helmet.js'
3+
export * from './helmet.js'
44
export { default as postGraphile } from './postgraphile.js'

src/router/health.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ router.get('/health', async (ctx) => {
1717
}
1818
ctx.body = 'ERROR: no views found in database'
1919
ctx.status = 500
20-
return
2120
})
2221
.catch(() => {
2322
ctx.body = 'ERROR: cannot connect to database'

0 commit comments

Comments
 (0)