Skip to content

Commit b53ae81

Browse files
committed
CCM-10893 Utils and unit tests
1 parent a3fb540 commit b53ae81

File tree

4 files changed

+219
-56
lines changed

4 files changed

+219
-56
lines changed

frontend/src/__tests__/utils/amplify-utils.test.ts

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,13 @@ import {
1010
} from '../../utils/amplify-utils';
1111

1212
jest.mock('aws-amplify/auth/server');
13-
jest.mock('@aws-amplify/adapter-nextjs/api');
13+
14+
jest.mock('@aws-amplify/adapter-nextjs', () => ({
15+
createServerRunner: () => ({
16+
runWithAmplifyServerContext: async ({ operation }: any) => operation({}),
17+
}),
18+
}));
19+
1420
jest.mock('next/headers', () => ({
1521
cookies: () => ({
1622
getAll: jest.fn(),
@@ -27,7 +33,7 @@ describe('amplify-utils', () => {
2733
jest.resetAllMocks();
2834
});
2935

30-
test('getSessionServer - should return the auth token and clientID', async () => {
36+
test('getSessionServer - should return the auth tokens and clientID', async () => {
3137
const mockAccessToken = {
3238
toString: () =>
3339
sign(
@@ -38,16 +44,24 @@ describe('amplify-utils', () => {
3844
),
3945
payload: {},
4046
};
47+
const mockIdToken = {
48+
toString: () =>
49+
sign({ ['nhs-notify:client-name']: 'client name' }, 'mockToken'),
50+
payload: {},
51+
};
52+
4153
fetchAuthSessionMock.mockResolvedValueOnce({
4254
tokens: {
4355
accessToken: mockAccessToken,
56+
idToken: mockIdToken,
4457
},
4558
});
4659

4760
const result = await getSessionServer();
4861

4962
expect(result).toEqual({
5063
accessToken: mockAccessToken.toString(),
64+
idToken: mockIdToken.toString(),
5165
clientId: 'client1',
5266
});
5367
});
@@ -57,9 +71,16 @@ describe('amplify-utils', () => {
5771
toString: () => sign({}, 'mockToken'),
5872
payload: {},
5973
};
74+
const mockIdToken = {
75+
toString: () =>
76+
sign({ ['nhs-notify:client-name']: 'client name' }, 'mockToken'),
77+
payload: {},
78+
};
79+
6080
fetchAuthSessionMock.mockResolvedValueOnce({
6181
tokens: {
6282
accessToken: mockAccessToken,
83+
idToken: mockIdToken,
6384
},
6485
});
6586

@@ -73,7 +94,11 @@ describe('amplify-utils', () => {
7394

7495
const result = await getSessionServer();
7596

76-
expect(result).toEqual({ accessToken: undefined });
97+
expect(result).toEqual({
98+
accessToken: undefined,
99+
idToken: undefined,
100+
clientId: undefined,
101+
});
77102
});
78103

79104
test('getSessionServer - should return undefined properties if an error occurs', async () => {
@@ -83,7 +108,11 @@ describe('amplify-utils', () => {
83108

84109
const result = await getSessionServer();
85110

86-
expect(result).toEqual({ accessToken: undefined });
111+
expect(result).toEqual({
112+
accessToken: undefined,
113+
idToken: undefined,
114+
clientId: undefined,
115+
});
87116
});
88117

89118
describe('getSessionId', () => {
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
/**
2+
* @jest-environment node
3+
*/
4+
import { sign } from 'jsonwebtoken';
5+
import { decodeJwt, getClaim, getIdTokenClaims } from '@utils/token-utils';
6+
import { JWT } from 'aws-amplify/auth';
7+
8+
describe('token-utils', () => {
9+
describe('decodeJwt', () => {
10+
it('decodes a valid JWT payload', () => {
11+
const token = sign({ testKey: 'value', testNum: 1 }, 'secret');
12+
const claims = decodeJwt(token);
13+
14+
expect(claims.testKey).toBe('value');
15+
expect(claims.testNum).toBe(1);
16+
});
17+
});
18+
19+
describe('getClaim', () => {
20+
it('returns a string when the claim exists (string)', () => {
21+
const claims = { testKey: 'value' } as unknown as JWT['payload'];
22+
23+
expect(getClaim(claims, 'testKey')).toBe('value');
24+
});
25+
26+
it('returns stringified value for non-strings', () => {
27+
const claims = { num: 123, bool: true } as unknown as JWT['payload'];
28+
29+
expect(getClaim(claims, 'num')).toBe('123');
30+
expect(getClaim(claims, 'bool')).toBe('true');
31+
});
32+
33+
it('returns undefined when the claim is missing, null or undefined', () => {
34+
const claims = { a: null, b: undefined } as unknown as JWT['payload'];
35+
36+
expect(getClaim(claims, 'missing')).toBeUndefined();
37+
expect(getClaim(claims, 'a')).toBeUndefined();
38+
expect(getClaim(claims, 'b')).toBeUndefined();
39+
});
40+
});
41+
42+
describe('getIdTokenClaims', () => {
43+
it('includes clientName when present', () => {
44+
const token = sign(
45+
{
46+
'nhs-notify:client-name': 'Test client',
47+
},
48+
'secret'
49+
);
50+
51+
const claims = getIdTokenClaims(token);
52+
53+
expect(claims.clientName).toBe('Test client');
54+
});
55+
56+
it('returns undefined clientName when no suitable claim exists', () => {
57+
const token = sign({ displayName: 'Test name' }, 'secret');
58+
59+
const claims = getIdTokenClaims(token);
60+
61+
expect(claims.clientName).toBeUndefined();
62+
});
63+
64+
it('prefers preferred_username as display name when present', () => {
65+
const token = sign(
66+
{
67+
'nhs-notify:client-name': 'Test client',
68+
preferred_username: 'Preferred Name',
69+
display_name: 'Display Name',
70+
given_name: 'Given',
71+
family_name: 'Family',
72+
73+
},
74+
'secret'
75+
);
76+
77+
const claims = getIdTokenClaims(token);
78+
79+
expect(claims.displayName).toBe('Preferred Name');
80+
});
81+
82+
it('falls back to display_name when preferred_username is missing', () => {
83+
const token = sign(
84+
{
85+
'nhs-notify:client-name': 'Test client',
86+
display_name: 'Display Name',
87+
given_name: 'Given',
88+
family_name: 'Family',
89+
90+
},
91+
'secret'
92+
);
93+
94+
const claims = getIdTokenClaims(token);
95+
96+
expect(claims.displayName).toBe('Display Name');
97+
});
98+
99+
it('falls back to given_name + family_name when no preferred/display name', () => {
100+
const token = sign(
101+
{
102+
given_name: 'Given',
103+
family_name: 'Family',
104+
105+
},
106+
'secret'
107+
);
108+
109+
const claims = getIdTokenClaims(token);
110+
111+
expect(claims.clientName).toBeUndefined();
112+
expect(claims.displayName).toBe('Given Family');
113+
});
114+
115+
it('falls back to email when no preferred/display/full name', () => {
116+
const token = sign(
117+
{
118+
119+
},
120+
'secret'
121+
);
122+
123+
const claims = getIdTokenClaims(token);
124+
125+
expect(claims.clientName).toBeUndefined();
126+
expect(claims.displayName).toBe('[email protected]');
127+
});
128+
129+
it('returns undefined displayName when no suitable claim exists', () => {
130+
const token = sign({ clientName: 'client' }, 'secret');
131+
132+
const claims = getIdTokenClaims(token);
133+
134+
expect(claims.displayName).toBeUndefined();
135+
});
136+
});
137+
});

frontend/src/utils/amplify-utils.ts

Lines changed: 6 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
import { cookies } from 'next/headers';
66
import { createServerRunner } from '@aws-amplify/adapter-nextjs';
77
import { fetchAuthSession } from 'aws-amplify/auth/server';
8-
import { FetchAuthSessionOptions, JWT } from 'aws-amplify/auth';
9-
import { jwtDecode } from 'jwt-decode';
8+
import { FetchAuthSessionOptions } from 'aws-amplify/auth';
9+
import { decodeJwt, getClaim } from './token-utils';
1010

1111
const config = require('@/amplify_outputs.json');
1212

@@ -20,8 +20,6 @@ export async function getSessionServer(
2020
accessToken?: string;
2121
idToken?: string;
2222
clientId?: string;
23-
clientName?: string;
24-
displayName?: string;
2523
}> {
2624
const session = await runWithAmplifyServerContext({
2725
nextServerContext: { cookies },
@@ -34,25 +32,14 @@ export async function getSessionServer(
3432
const clientId = accessToken && getClientId(accessToken);
3533

3634
const idToken = session?.tokens?.idToken?.toString();
37-
const idClaims = idToken ? getIdTokenClaims(idToken) : undefined;
3835

3936
return {
4037
accessToken,
4138
idToken,
4239
clientId,
43-
clientName: idClaims?.clientName,
44-
displayName: idClaims?.displayName,
4540
};
4641
}
4742

48-
export const getSessionId = async () => {
49-
return getAccessTokenParam('origin_jti');
50-
};
51-
52-
export const getClientId = (accessToken: string) => {
53-
return getClaim(decodeJwt(accessToken), 'nhs-notify:client-id');
54-
};
55-
5643
const getAccessTokenParam = async (key: string) => {
5744
const authSession = await getSessionServer();
5845
const accessToken = authSession.accessToken;
@@ -62,43 +49,10 @@ const getAccessTokenParam = async (key: string) => {
6249
return getClaim(decodeJwt(accessToken), key);
6350
};
6451

65-
const decodeJwt = (token: string): JWT['payload'] =>
66-
jwtDecode<JWT['payload']>(token);
67-
68-
const getClaim = (claims: JWT['payload'], key: string): string | undefined => {
69-
const value = claims[key];
70-
return value != null ? String(value) : undefined;
52+
export const getSessionId = async () => {
53+
return getAccessTokenParam('origin_jti');
7154
};
7255

73-
const getIdTokenClaims = (
74-
idToken: string
75-
): {
76-
clientName?: string;
77-
displayName?: string;
78-
} => {
79-
const claims = decodeJwt(idToken);
80-
81-
const clientName = getClaim(claims, 'nhs-notify:client-name');
82-
83-
let displayName;
84-
85-
const preferredUsername =
86-
getClaim(claims, 'preferred_username') || getClaim(claims, 'display_name');
87-
88-
if (preferredUsername) displayName = preferredUsername;
89-
else {
90-
const givenName = getClaim(claims, 'given_name');
91-
const familyName = getClaim(claims, 'family_name');
92-
93-
if (givenName && familyName) displayName = `${givenName} ${familyName}`;
94-
else {
95-
const email = getClaim(claims, 'email');
96-
if (email) displayName = email;
97-
}
98-
}
99-
100-
return {
101-
clientName,
102-
displayName,
103-
};
56+
export const getClientId = (accessToken: string) => {
57+
return getClaim(decodeJwt(accessToken), 'nhs-notify:client-id');
10458
};

frontend/src/utils/token-utils.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { jwtDecode } from "jwt-decode";
2+
import { JWT } from "aws-amplify/auth";
3+
4+
export const decodeJwt = (token: string): JWT['payload'] =>
5+
jwtDecode<JWT['payload']>(token);
6+
7+
export const getClaim = (claims: JWT['payload'], key: string): string | undefined => {
8+
const value = claims[key];
9+
return value != null ? String(value) : undefined;
10+
};
11+
12+
export const getIdTokenClaims = (
13+
idToken: string
14+
): {
15+
clientName?: string;
16+
displayName?: string;
17+
} => {
18+
const claims = decodeJwt(idToken);
19+
20+
const clientName = getClaim(claims, 'nhs-notify:client-name');
21+
22+
let displayName;
23+
24+
const preferredUsername =
25+
getClaim(claims, 'preferred_username') || getClaim(claims, 'display_name');
26+
27+
if (preferredUsername) displayName = preferredUsername;
28+
else {
29+
const givenName = getClaim(claims, 'given_name');
30+
const familyName = getClaim(claims, 'family_name');
31+
32+
if (givenName && familyName) displayName = `${givenName} ${familyName}`;
33+
else {
34+
const email = getClaim(claims, 'email');
35+
if (email) displayName = email;
36+
}
37+
}
38+
39+
return {
40+
clientName,
41+
displayName,
42+
};
43+
};

0 commit comments

Comments
 (0)