Skip to content

Commit d0273f7

Browse files
ilteooodEomm
andauthored
chore: increase coverage (#1358)
* chore: test decorators * chore: passport files * fix: integration * chore: base strategy tests * chore: authenticator * chore: authentication route * chore: decorators * chore: decorators * chore: authentication route constructor * chore: session strategy * chore: audit * chore: passport * chore: session strategy * chore: prefer use json * chore: add coverage checker * Update package.json * Update package.json * Apply suggestions from code review Co-authored-by: Manuel Spigolon <[email protected]> Signed-off-by: Matteo Pietro Dazzi <[email protected]> --------- Signed-off-by: Matteo Pietro Dazzi <[email protected]> Co-authored-by: Manuel Spigolon <[email protected]>
1 parent 8a81c40 commit d0273f7

13 files changed

+741
-230
lines changed

package-lock.json

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

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
"lint:fix": "eslint --fix",
1313
"prepublishOnly": "npm run build",
1414
"test": "npm run build:test && npm run test:unit",
15-
"test:unit": "borp --coverage"
15+
"test:unit": "borp --coverage --lines 100"
1616
},
1717
"repository": {
1818
"type": "git",
@@ -50,6 +50,9 @@
5050
"@tsconfig/node20": "^20.1.6",
5151
"@types/node": "^24.0.8",
5252
"@types/passport": "^1.0.17",
53+
"@types/passport-facebook": "^3.0.4",
54+
"@types/passport-github2": "^1.2.9",
55+
"@types/passport-google-oauth": "^1.0.45",
5356
"@types/set-cookie-parser": "^2.4.10",
5457
"borp": "^0.21.0",
5558
"eslint": "^9.17.0",

test/authentication-route.test.ts

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
import assert from 'node:assert'
2+
import { describe, test } from 'node:test'
3+
import { getConfiguredTestServer, TestStrategy } from './helpers'
4+
5+
describe('AuthenticationRoute edge cases', () => {
6+
test('should use failWithError option to throw error on authentication failure', async () => {
7+
const { server, fastifyPassport } = getConfiguredTestServer()
8+
9+
server.post(
10+
'/login',
11+
{ preValidation: fastifyPassport.authenticate('test', { failWithError: true }) },
12+
async () => assert.fail('should not reach here')
13+
)
14+
15+
const response = await server.inject({
16+
method: 'POST',
17+
payload: { login: 'wrong', password: 'wrong' },
18+
url: '/login'
19+
})
20+
21+
assert.strictEqual(response.statusCode, 401)
22+
})
23+
24+
test('should set WWW-Authenticate header on 401 when challenge is provided', async () => {
25+
class ChallengeStrategy extends TestStrategy {
26+
authenticate () {
27+
this.fail('Bearer realm="Users"', 401)
28+
}
29+
}
30+
31+
const { server, fastifyPassport } = getConfiguredTestServer('challenge', new ChallengeStrategy('challenge'))
32+
33+
server.post(
34+
'/login',
35+
{ preValidation: fastifyPassport.authenticate('challenge') },
36+
async () => assert.fail('should not reach here')
37+
)
38+
39+
const response = await server.inject({
40+
method: 'POST',
41+
url: '/login'
42+
})
43+
44+
assert.strictEqual(response.statusCode, 401)
45+
assert.ok(response.headers['www-authenticate'])
46+
// WWW-Authenticate can be an array or string
47+
const authHeader = response.headers['www-authenticate']
48+
const authValue = Array.isArray(authHeader) ? authHeader[0] : authHeader
49+
assert.strictEqual(authValue, 'Bearer realm="Users"')
50+
})
51+
52+
test('should handle multiple challenges in WWW-Authenticate header', async () => {
53+
class FirstChallengeStrategy extends TestStrategy {
54+
authenticate () {
55+
this.fail('Basic realm="Users"')
56+
}
57+
}
58+
59+
class SecondChallengeStrategy extends TestStrategy {
60+
authenticate () {
61+
this.fail('Bearer realm="API"')
62+
}
63+
}
64+
65+
const { server, fastifyPassport } = getConfiguredTestServer()
66+
fastifyPassport.use('first', new FirstChallengeStrategy('first'))
67+
fastifyPassport.use('second', new SecondChallengeStrategy('second'))
68+
69+
server.post(
70+
'/login',
71+
{ preValidation: fastifyPassport.authenticate(['first', 'second']) },
72+
async () => 'should not reach here'
73+
)
74+
75+
const response = await server.inject({
76+
method: 'POST',
77+
url: '/login'
78+
})
79+
80+
assert.strictEqual(response.statusCode, 401)
81+
assert.ok(response.headers['www-authenticate'])
82+
})
83+
84+
test('should handle strategy error with callback', async () => {
85+
class ErrorStrategy extends TestStrategy {
86+
authenticate () {
87+
this.error(new Error('Strategy error'))
88+
}
89+
}
90+
91+
const { server, fastifyPassport } = getConfiguredTestServer('error', new ErrorStrategy('error'))
92+
93+
server.post('/login', async (request: any, reply) => {
94+
const handler = fastifyPassport.authenticate(
95+
'error',
96+
async (req: any, rep: any, err: any, user: any) => {
97+
if (err) {
98+
return rep.status(500).send({ error: err.message })
99+
}
100+
rep.send({ user })
101+
}
102+
)
103+
return handler.call(server, request, reply)
104+
})
105+
106+
const response = await server.inject({
107+
method: 'POST',
108+
url: '/login'
109+
})
110+
111+
assert.strictEqual(response.statusCode, 500)
112+
assert.strictEqual(response.json().error, 'Strategy error')
113+
})
114+
115+
test('should throw error for unknown strategy', async () => {
116+
const { server, fastifyPassport } = getConfiguredTestServer()
117+
118+
server.post(
119+
'/login',
120+
{ preValidation: fastifyPassport.authenticate('nonexistent') },
121+
async () => assert.fail('should not reach here')
122+
)
123+
124+
const response = await server.inject({
125+
method: 'POST',
126+
url: '/login'
127+
})
128+
129+
assert.strictEqual(response.statusCode, 500)
130+
})
131+
132+
test('should handle strategy instance with constructor name fallback', async () => {
133+
class CustomNameStrategy extends TestStrategy {
134+
constructor () {
135+
super('custom')
136+
}
137+
}
138+
139+
const strategy = new CustomNameStrategy()
140+
const { server, fastifyPassport } = getConfiguredTestServer()
141+
fastifyPassport.use(strategy)
142+
143+
server.post(
144+
'/login',
145+
{ preValidation: fastifyPassport.authenticate(strategy) },
146+
async (request: any) => (request.user as any).name
147+
)
148+
149+
const response = await server.inject({
150+
method: 'POST',
151+
payload: { login: 'test', password: 'test' },
152+
url: '/login'
153+
})
154+
155+
assert.strictEqual(response.statusCode, 200)
156+
})
157+
158+
test('should handle failure with custom status code', async () => {
159+
class CustomStatusStrategy extends TestStrategy {
160+
authenticate () {
161+
this.fail('Custom error', 403)
162+
}
163+
}
164+
165+
const { server, fastifyPassport } = getConfiguredTestServer('custom', new CustomStatusStrategy('custom'))
166+
167+
server.post(
168+
'/login',
169+
{ preValidation: fastifyPassport.authenticate('custom') },
170+
async () => 'should not reach here'
171+
)
172+
173+
const response = await server.inject({
174+
method: 'POST',
175+
url: '/login'
176+
})
177+
178+
assert.strictEqual(response.statusCode, 403)
179+
})
180+
181+
test('should handle object challenge in failure', async () => {
182+
class ObjectChallengeStrategy extends TestStrategy {
183+
authenticate () {
184+
this.fail({ type: 'error', message: 'Invalid credentials' }, 401)
185+
}
186+
}
187+
188+
const { server, fastifyPassport } = getConfiguredTestServer('object', new ObjectChallengeStrategy('object'))
189+
190+
server.post(
191+
'/login',
192+
{ preValidation: fastifyPassport.authenticate('object') },
193+
async () => assert.fail('should not reach here')
194+
)
195+
196+
const response = await server.inject({
197+
method: 'POST',
198+
url: '/login'
199+
})
200+
201+
assert.strictEqual(response.statusCode, 401)
202+
})
203+
204+
test('should use constructor.name when strategy instance name property is empty', async () => {
205+
class CustomNamedStrategy extends TestStrategy {
206+
constructor () {
207+
super('test')
208+
Object.defineProperty(this, 'name', {
209+
value: '',
210+
writable: false,
211+
configurable: true
212+
})
213+
}
214+
}
215+
216+
const strategy = new CustomNamedStrategy()
217+
const { server, fastifyPassport } = getConfiguredTestServer()
218+
219+
fastifyPassport.use('CustomNamedStrategy', strategy)
220+
221+
server.post(
222+
'/login',
223+
{ preValidation: fastifyPassport.authenticate(strategy) },
224+
async (request: any) => (request.user as any).name
225+
)
226+
227+
const response = await server.inject({
228+
method: 'POST',
229+
payload: { login: 'test', password: 'test' },
230+
url: '/login'
231+
})
232+
233+
assert.strictEqual(response.statusCode, 200)
234+
})
235+
})

test/authenticator.test.ts

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import assert from 'node:assert'
2+
import { describe, test } from 'node:test'
3+
import Authenticator from '../src/Authenticator'
4+
import { getConfiguredTestServer } from './helpers'
5+
6+
describe('Authenticator edge cases', () => {
7+
test('should throw error when no serializer succeeds', async () => {
8+
const fastifyPassport = new Authenticator()
9+
10+
fastifyPassport.registerUserSerializer(async () => {
11+
throw 'pass' // eslint-disable-line no-throw-literal
12+
})
13+
fastifyPassport.registerUserSerializer(async () => {
14+
throw 'pass' // eslint-disable-line no-throw-literal
15+
})
16+
17+
const { server } = getConfiguredTestServer()
18+
19+
try {
20+
await fastifyPassport.serializeUser({ name: 'test' }, server.inject as any)
21+
assert.fail('Should have thrown an error')
22+
} catch (error: any) {
23+
assert.ok(error.message.includes('Failed to serialize user into session'))
24+
assert.ok(error.message.includes('Tried 2 serializers'))
25+
}
26+
})
27+
28+
test('should use options.assignProperty instead of default user property in authorize', async () => {
29+
const { server, fastifyPassport } = getConfiguredTestServer()
30+
31+
server.post(
32+
'/authorize',
33+
{ preValidation: fastifyPassport.authorize('test', { assignProperty: 'account' }) },
34+
async (request: any, reply) => {
35+
reply.send({ account: request.account })
36+
}
37+
)
38+
39+
const response = await server.inject({
40+
method: 'POST',
41+
payload: { login: 'test', password: 'test' },
42+
url: '/authorize'
43+
})
44+
45+
assert.strictEqual(response.statusCode, 200)
46+
const body = response.json()
47+
assert.ok(body.account)
48+
assert.strictEqual(body.account.name, 'test')
49+
})
50+
51+
test('should handle authorize with callback function as second parameter', async () => {
52+
const { server, fastifyPassport } = getConfiguredTestServer()
53+
54+
server.post('/authorize', async (request: any, reply) => {
55+
const handler = fastifyPassport.authorize(
56+
'test',
57+
async (req: any, rep: any, err: any, user: any) => {
58+
if (err) {
59+
return rep.status(500).send({ error: err.message })
60+
}
61+
rep.send({ authorizedUser: user })
62+
}
63+
)
64+
return handler.call(server, request, reply)
65+
})
66+
67+
const response = await server.inject({
68+
method: 'POST',
69+
payload: { login: 'test', password: 'test' },
70+
url: '/authorize'
71+
})
72+
73+
assert.strictEqual(response.statusCode, 200)
74+
const body = response.json()
75+
assert.ok(body.authorizedUser)
76+
})
77+
78+
test('should use default authInfo transformer when no transformers are registered', async () => {
79+
const fastifyPassport = new Authenticator()
80+
const { server } = getConfiguredTestServer()
81+
82+
const info = { message: 'test info' }
83+
const result = await fastifyPassport.transformAuthInfo(info, server.inject as any)
84+
85+
assert.deepStrictEqual(result, info)
86+
})
87+
88+
test('should transform authInfo with registered transformer', async () => {
89+
const fastifyPassport = new Authenticator()
90+
91+
fastifyPassport.registerAuthInfoTransformer(async (info) => {
92+
return { ...info, transformed: true }
93+
})
94+
95+
const { server } = getConfiguredTestServer()
96+
const info = { message: 'test info' }
97+
const result = await fastifyPassport.transformAuthInfo(info, server.inject as any)
98+
99+
assert.strictEqual(result.message, 'test info')
100+
assert.strictEqual(result.transformed, true)
101+
})
102+
103+
test('should skip infoTransformers that throw "pass"', async () => {
104+
const fastifyPassport = new Authenticator()
105+
106+
fastifyPassport.registerAuthInfoTransformer(async () => {
107+
throw 'pass' // eslint-disable-line no-throw-literal
108+
})
109+
110+
fastifyPassport.registerAuthInfoTransformer(async (info) => {
111+
return { ...info, transformed: true }
112+
})
113+
114+
const { server } = getConfiguredTestServer()
115+
const info = { message: 'test info' }
116+
const result = await fastifyPassport.transformAuthInfo(info, server.inject as any)
117+
118+
assert.strictEqual(result.message, 'test info')
119+
assert.strictEqual(result.transformed, true)
120+
})
121+
122+
test('should throw error from transformer if not "pass"', async () => {
123+
const fastifyPassport = new Authenticator()
124+
125+
fastifyPassport.registerAuthInfoTransformer(async () => {
126+
throw new Error('Transformer error')
127+
})
128+
129+
const { server } = getConfiguredTestServer()
130+
const info = { message: 'test info' }
131+
132+
try {
133+
await fastifyPassport.transformAuthInfo(info, server.inject as any)
134+
assert.fail('Should have thrown an error')
135+
} catch (error: any) {
136+
assert.strictEqual(error.message, 'Transformer error')
137+
}
138+
})
139+
})

0 commit comments

Comments
 (0)