-
Notifications
You must be signed in to change notification settings - Fork 167
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Unit test fixes for openid-client esm import (#3418)
* Remove NODE_OPTIONS and mock utils module * Fixes * Update node version * Fix mocking of userInfo call inside utils * Remove console.log * Revert node version change for CI * Upgrade to node LTS v22 * Revert accidental local changes * Cleanup
- Loading branch information
Showing
6 changed files
with
122 additions
and
105 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,33 +1,77 @@ | ||
import tap from 'tap'; | ||
import * as client from 'openid-client'; | ||
import { IConnectionAPIController, IOAuthController, OAuthReq } from '../../src/typings'; | ||
import * as utils from '../../src/controller/utils'; | ||
import { IConnectionAPIController, IOAuthController, OAuthReq, Profile } from '../../src/typings'; | ||
import { authz_request_oidc_provider, oidc_response, oidc_response_with_error } from './fixture'; | ||
import { JacksonError } from '../../src/controller/error'; | ||
import { addSSOConnections, jacksonOptions } from '../utils'; | ||
import path from 'path'; | ||
import type { Configuration } from 'openid-client'; | ||
|
||
let connectionAPIController: IConnectionAPIController; | ||
let oauthController: IOAuthController; | ||
|
||
const metadataPath = path.join(__dirname, '/data/metadata'); | ||
|
||
const code_verifier: string = client.randomPKCECodeVerifier(); | ||
let code_verifier: string; | ||
let code_challenge: string; | ||
|
||
const openIdClientMock = tap.createMock(client, { | ||
...client, | ||
randomPKCECodeVerifier: () => { | ||
return code_verifier; | ||
}, | ||
calculatePKCECodeChallenge: async () => { | ||
code_challenge = await client.calculatePKCECodeChallenge(code_verifier); | ||
return code_challenge; | ||
}, | ||
}); | ||
let openIdClientMock: typeof import('openid-client'); | ||
let utilsMock: any; | ||
|
||
tap.before(async () => { | ||
const client = await import('openid-client'); | ||
code_verifier = client.randomPKCECodeVerifier(); | ||
code_challenge = await client.calculatePKCECodeChallenge(code_verifier); | ||
openIdClientMock = { | ||
...client, | ||
randomPKCECodeVerifier: () => { | ||
return code_verifier; | ||
}, | ||
calculatePKCECodeChallenge: async () => { | ||
return code_challenge; | ||
}, | ||
}; | ||
utilsMock = tap.createMock(utils, { | ||
...utils, | ||
dynamicImport: async (packageName) => { | ||
if (packageName === 'openid-client') { | ||
return openIdClientMock; | ||
} | ||
// fallback to original impl for other packages | ||
return utils.dynamicImport(packageName); | ||
}, | ||
extractOIDCUserProfile: async (tokens: utils.AuthorizationCodeGrantResult, oidcConfig: Configuration) => { | ||
const idTokenClaims = tokens.claims()!; | ||
const client = openIdClientMock as typeof import('openid-client'); | ||
openIdClientMock.fetchUserInfo = async () => { | ||
return { | ||
sub: 'USER_IDENTIFIER', | ||
email: '[email protected]', | ||
given_name: 'jackson', | ||
family_name: 'samuel', | ||
picture: 'https://jackson.cloud.png', | ||
email_verified: true, | ||
}; | ||
}; | ||
const userinfo = await client.fetchUserInfo(oidcConfig, tokens.access_token, idTokenClaims.sub); | ||
|
||
const profile: { claims: Partial<Profile & { raw: Record<string, unknown> }> } = { claims: {} }; | ||
|
||
profile.claims.id = idTokenClaims.sub; | ||
profile.claims.email = typeof idTokenClaims.email === 'string' ? idTokenClaims.email : userinfo.email; | ||
profile.claims.firstName = | ||
typeof idTokenClaims.given_name === 'string' ? idTokenClaims.given_name : userinfo.given_name; | ||
profile.claims.lastName = | ||
typeof idTokenClaims.family_name === 'string' ? idTokenClaims.family_name : userinfo.family_name; | ||
profile.claims.roles = idTokenClaims.roles ?? (userinfo.roles as any); | ||
profile.claims.groups = idTokenClaims.groups ?? (userinfo.groups as any); | ||
profile.claims.raw = { ...idTokenClaims, ...userinfo }; | ||
|
||
return profile; | ||
}, | ||
}); | ||
|
||
const indexModule = tap.mockRequire('../../src/index', { | ||
'openid-client': openIdClientMock, | ||
'../../src/controller/utils': utilsMock, | ||
}); | ||
const controller = await indexModule.default(jacksonOptions); | ||
|
||
|
@@ -43,24 +87,20 @@ tap.teardown(async () => { | |
tap.test('[OIDCProvider]', async (t) => { | ||
const context: Record<string, any> = {}; | ||
|
||
t.test( | ||
'[authorize] Should return the IdP SSO URL', | ||
{ todo: 'fix mocking of openid-client which is dynamically imported' }, | ||
async (t) => { | ||
// will be matched in happy path test | ||
context.codeVerifier = code_verifier; | ||
t.test('[authorize] Should return the IdP SSO URL', async (t) => { | ||
// will be matched in happy path test | ||
context.codeVerifier = code_verifier; | ||
|
||
const response = (await oauthController.authorize(<OAuthReq>authz_request_oidc_provider)) as { | ||
redirect_url: string; | ||
}; | ||
const params = new URLSearchParams(new URL(response.redirect_url!).search); | ||
t.ok('redirect_url' in response, 'got the Idp authorize URL'); | ||
t.ok(params.has('state'), 'state present'); | ||
t.match(params.get('scope'), 'openid email profile', 'openid scopes present'); | ||
t.match(params.get('code_challenge'), code_challenge, 'codeChallenge present'); | ||
context.state = params.get('state'); | ||
} | ||
); | ||
const response = (await oauthController.authorize(<OAuthReq>authz_request_oidc_provider)) as { | ||
redirect_url: string; | ||
}; | ||
const params = new URLSearchParams(new URL(response.redirect_url!).search); | ||
t.ok('redirect_url' in response, 'got the Idp authorize URL'); | ||
t.ok(params.has('state'), 'state present'); | ||
t.match(params.get('scope'), 'openid email profile', 'openid scopes present'); | ||
t.match(params.get('code_challenge'), code_challenge, 'codeChallenge present'); | ||
context.state = params.get('state'); | ||
}); | ||
|
||
t.test('[authorize] Should omit profile scope if openid.requestProfileScope is set to false', async (t) => { | ||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment | ||
|
@@ -155,21 +195,17 @@ tap.test('[OIDCProvider]', async (t) => { | |
oauthController.opts.oidcPath = jacksonOptions.oidcPath; | ||
}); | ||
|
||
t.test( | ||
'[oidcAuthzResponse] Should throw an error if `state` is missing', | ||
{ todo: 'fix mocking of openid-client which is dynamically imported' }, | ||
async (t) => { | ||
try { | ||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment | ||
//@ts-ignore | ||
await oauthController.oidcAuthzResponse(oidc_response); | ||
} catch (err) { | ||
const { message, statusCode } = err as JacksonError; | ||
t.equal(message, 'State from original request is missing.', 'got expected error message'); | ||
t.equal(statusCode, 403, 'got expected status code'); | ||
} | ||
t.test('[oidcAuthzResponse] Should throw an error if `state` is missing', async (t) => { | ||
try { | ||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment | ||
//@ts-ignore | ||
await oauthController.oidcAuthzResponse(oidc_response); | ||
} catch (err) { | ||
const { message, statusCode } = err as JacksonError; | ||
t.equal(message, 'State from original request is missing.', 'got expected error message'); | ||
t.equal(statusCode, 403, 'got expected status code'); | ||
} | ||
); | ||
}); | ||
|
||
t.test('[oidcAuthzResponse] Should throw an error if `state` is invalid', async (t) => { | ||
try { | ||
|
@@ -181,50 +217,35 @@ tap.test('[OIDCProvider]', async (t) => { | |
} | ||
}); | ||
|
||
t.test( | ||
'[oidcAuthzResponse] Should forward any provider errors to redirect_uri', | ||
{ todo: 'fix mocking of openid-client which is dynamically imported' }, | ||
async (t) => { | ||
const { redirect_url } = await oauthController.oidcAuthzResponse({ | ||
...oidc_response_with_error, | ||
state: context.state, | ||
}); | ||
const response_params = new URLSearchParams(new URL(redirect_url!).search); | ||
t.test('[oidcAuthzResponse] Should forward any provider errors to redirect_uri', async (t) => { | ||
const { redirect_url } = await oauthController.oidcAuthzResponse({ | ||
...oidc_response_with_error, | ||
state: context.state, | ||
}); | ||
const response_params = new URLSearchParams(new URL(redirect_url!).search); | ||
|
||
t.match( | ||
response_params.get('error'), | ||
oidc_response_with_error.error, | ||
'mismatch in forwarded oidc provider error' | ||
); | ||
t.match( | ||
response_params.get('error_description'), | ||
oidc_response_with_error.error_description, | ||
'mismatch in forwaded oidc error_description' | ||
); | ||
t.match( | ||
response_params.get('state'), | ||
authz_request_oidc_provider.state, | ||
'state mismatch in error response' | ||
); | ||
} | ||
); | ||
t.match( | ||
response_params.get('error'), | ||
oidc_response_with_error.error, | ||
'mismatch in forwarded oidc provider error' | ||
); | ||
t.match( | ||
response_params.get('error_description'), | ||
oidc_response_with_error.error_description, | ||
'mismatch in forwaded oidc error_description' | ||
); | ||
t.match( | ||
response_params.get('state'), | ||
authz_request_oidc_provider.state, | ||
'state mismatch in error response' | ||
); | ||
}); | ||
|
||
t.test( | ||
'[oidcAuthzResponse] Should return the client redirect url with code and original state attached', | ||
{ todo: 'fix mocking of openid-client which is dynamically imported' }, | ||
async (t) => { | ||
// let capturedArgs: any; | ||
openIdClientMock.fetchUserInfo = async () => { | ||
return { | ||
sub: 'USER_IDENTIFIER', | ||
email: '[email protected]', | ||
given_name: 'jackson', | ||
family_name: 'samuel', | ||
picture: 'https://jackson.cloud.png', | ||
email_verified: true, | ||
}; | ||
}; | ||
const mockAuthorizationCodeGrant = async () => { | ||
openIdClientMock.authorizationCodeGrant = async () => { | ||
return { | ||
access_token: 'ACCESS_TOKEN', | ||
id_token: 'ID_TOKEN', | ||
|
@@ -241,7 +262,6 @@ tap.test('[OIDCProvider]', async (t) => { | |
}), | ||
} as any; | ||
}; | ||
openIdClientMock.authorizationCodeGrant = mockAuthorizationCodeGrant; | ||
|
||
const { redirect_url } = await oauthController.oidcAuthzResponse({ | ||
...oidc_response, | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters