Skip to content

Commit 5220c55

Browse files
VIA-630 Move age calculation from getSession to updateToken on login
Placing the age calculation on getSession had the unintended side effect of being re-evaluated every time NextAuth loaded the session (which it does often; twice every minute), and any errors with the dateOfBirth resulted in a single user outputing many errors in the logs. Instead move the age calculation to the point when a user logs in and user details are being persisted to the token; store age group on the token for use by the session.
1 parent 9829c5c commit 5220c55

File tree

8 files changed

+143
-60
lines changed

8 files changed

+143
-60
lines changed

src/app/_components/hub/AgeBasedHubCards.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ type AgeBasedHubProps = {
1010
const AgeBasedHubCards = ({ ageGroup }: AgeBasedHubProps): JSX.Element => {
1111
const hubInfoForAgeGroup: AgeBasedHubDetails | undefined = AgeBasedHubInfo[ageGroup];
1212

13+
// TODO VIA-161 Age-Based Hub epic: hubInfo info will be undefined for the age ranges we have not implemented yet
14+
// This is expected for now; fail gracefully and do not render anything
15+
// This boolean check can likely be removed once all ages are implemented
1316
if (hubInfoForAgeGroup) {
1417
return (
1518
<>
Lines changed: 50 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,57 @@
1-
import { getAgeGroup } from "@src/app/_components/hub/ageGroupHelper";
1+
import { getAgeGroup, getAgeGroupOfUser } from "@src/app/_components/hub/ageGroupHelper";
22
import { AgeGroup } from "@src/models/ageBasedHub";
3+
import { calculateAge } from "@src/utils/date";
4+
5+
jest.mock("sanitize-data", () => ({ sanitize: jest.fn() }));
6+
jest.mock("@src/utils/date", () => ({ calculateAge: jest.fn() }));
37

48
describe("Age group helper", () => {
5-
const expectedGroupsForEachAge = [
6-
{ age: 12, expectedAgeGroup: AgeGroup.AGE_12_to_16 },
7-
{ age: 16, expectedAgeGroup: AgeGroup.AGE_12_to_16 },
8-
{ age: 17, expectedAgeGroup: AgeGroup.AGE_17_to_24 },
9-
{ age: 24, expectedAgeGroup: AgeGroup.AGE_17_to_24 },
10-
{ age: 25, expectedAgeGroup: AgeGroup.AGE_25_to_64 },
11-
{ age: 64, expectedAgeGroup: AgeGroup.AGE_25_to_64 },
12-
{ age: 65, expectedAgeGroup: AgeGroup.AGE_65_to_74 },
13-
{ age: 74, expectedAgeGroup: AgeGroup.AGE_65_to_74 },
14-
{ age: 75, expectedAgeGroup: AgeGroup.AGE_75_to_80 },
15-
];
16-
17-
it.each(expectedGroupsForEachAge)(`returns $expectedAgeGroup for user age $age`, ({ age, expectedAgeGroup }) => {
18-
expect(getAgeGroup(age)).toEqual(expectedAgeGroup);
9+
describe("getAgeGroup", () => {
10+
const expectedGroupsForEachAge = [
11+
{ age: 12, expectedAgeGroup: AgeGroup.AGE_12_to_16 },
12+
{ age: 16, expectedAgeGroup: AgeGroup.AGE_12_to_16 },
13+
{ age: 17, expectedAgeGroup: AgeGroup.AGE_17_to_24 },
14+
{ age: 24, expectedAgeGroup: AgeGroup.AGE_17_to_24 },
15+
{ age: 25, expectedAgeGroup: AgeGroup.AGE_25_to_64 },
16+
{ age: 64, expectedAgeGroup: AgeGroup.AGE_25_to_64 },
17+
{ age: 65, expectedAgeGroup: AgeGroup.AGE_65_to_74 },
18+
{ age: 74, expectedAgeGroup: AgeGroup.AGE_65_to_74 },
19+
{ age: 75, expectedAgeGroup: AgeGroup.AGE_75_to_80 },
20+
];
21+
22+
it.each(expectedGroupsForEachAge)(`returns $expectedAgeGroup for user age $age`, ({ age, expectedAgeGroup }) => {
23+
expect(getAgeGroup(age)).toEqual(expectedAgeGroup);
24+
});
25+
26+
it("returns unknown age group when age not in defined ranges", () => {
27+
expect(getAgeGroup(6)).toBe(AgeGroup.UNKNOWN_AGE_GROUP);
28+
});
1929
});
2030

21-
it("returns unknown age group when age not in defined ranges", () => {
22-
expect(getAgeGroup(6)).toBe(AgeGroup.UNKNOWN_AGE_GROUP);
31+
describe("getAgeGroupForUser", () => {
32+
const mockDateOfBirth = "mock-dob";
33+
const mockCalculatedAge = 30;
34+
35+
beforeEach(() => {
36+
(calculateAge as jest.Mock).mockImplementation(() => mockCalculatedAge);
37+
});
38+
39+
it("should return age group for given dob", () => {
40+
const ageGroupForUser = getAgeGroupOfUser(mockDateOfBirth);
41+
42+
expect(ageGroupForUser).toBe(AgeGroup.AGE_25_to_64);
43+
});
44+
45+
it("should return unknown age group if calculateAge throws", () => {
46+
const calculateAgeError = new Error('[{ "code": "custom","message": "Invalid date value","path": []}]');
47+
48+
(calculateAge as jest.Mock).mockImplementation(() => {
49+
throw calculateAgeError;
50+
});
51+
52+
const ageGroupForUser = getAgeGroupOfUser(mockDateOfBirth);
53+
54+
expect(ageGroupForUser).toBe(AgeGroup.UNKNOWN_AGE_GROUP);
55+
});
2356
});
2457
});

src/app/_components/hub/ageGroupHelper.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
11
import { AgeGroup } from "@src/models/ageBasedHub";
2+
import { calculateAge } from "@src/utils/date";
3+
import { logger } from "@src/utils/logger";
4+
import { Logger } from "pino";
5+
6+
const log: Logger = logger.child({
7+
module: "age-group-helper",
8+
});
29

310
const getAgeGroup = (age: number): AgeGroup => {
411
if (12 <= age && age <= 16) {
@@ -21,4 +28,22 @@ const getAgeGroup = (age: number): AgeGroup => {
2128
} else return AgeGroup.UNKNOWN_AGE_GROUP;
2229
};
2330

24-
export { getAgeGroup };
31+
const getAgeGroupOfUser = (dateOfBirth: string): AgeGroup => {
32+
let ageGroupOfUser;
33+
34+
try {
35+
const age = calculateAge(dateOfBirth);
36+
ageGroupOfUser = getAgeGroup(age);
37+
} catch (error) {
38+
ageGroupOfUser = AgeGroup.UNKNOWN_AGE_GROUP;
39+
const errorMessage = error instanceof Error && error?.message != undefined ? error.message : "unknown error";
40+
const errorCause = error instanceof Error ? error.cause : "";
41+
log.error(
42+
{ error: { message: errorMessage, cause: errorCause } },
43+
"User data error; unable to determine age of user",
44+
);
45+
}
46+
return ageGroupOfUser;
47+
};
48+
49+
export { getAgeGroup, getAgeGroupOfUser };

src/utils/auth/callbacks/get-token.test.ts

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { AgeGroup } from "@src/models/ageBasedHub";
12
import { ApimHttpError } from "@src/utils/auth/apim/exceptions";
23
import { getOrRefreshApimCredentials } from "@src/utils/auth/apim/get-or-refresh-apim-credentials";
34
import { getToken } from "@src/utils/auth/callbacks/get-token";
@@ -52,6 +53,7 @@ describe("getToken", () => {
5253
nhs_number: "test_nhs_number",
5354
birthdate: "1994-08-04",
5455
};
56+
const expectedAgeGroupForBirthdate = AgeGroup.AGE_25_to_64;
5557

5658
const account = {
5759
expires_at: nowInSeconds + 1000,
@@ -103,6 +105,7 @@ describe("getToken", () => {
103105
result,
104106
profile.nhs_number,
105107
profile.birthdate,
108+
expectedAgeGroupForBirthdate,
106109
"newIdToken",
107110
"new-apim-access-token",
108111
nowInSeconds + 1111,
@@ -119,7 +122,7 @@ describe("getToken", () => {
119122

120123
const result = await getToken(undefinedToken, undefinedAccount, undefinedProfile, maxAgeInSeconds);
121124

122-
expectResultToMatchTokenWith(result, "", "", "", "", 0, maxAgeInSeconds);
125+
expectResultToMatchTokenWith(result, "", "", AgeGroup.UNKNOWN_AGE_GROUP, "", "", 0, maxAgeInSeconds);
123126
});
124127

125128
it("should fill in missing values in token with default empty string", async () => {
@@ -137,6 +140,7 @@ describe("getToken", () => {
137140
user: {
138141
nhs_number: "",
139142
birthdate: "",
143+
age_group: AgeGroup.UNKNOWN_AGE_GROUP,
140144
},
141145
nhs_login: {
142146
id_token: "",
@@ -168,7 +172,16 @@ describe("getToken", () => {
168172

169173
const result = await getToken(token, account, profile, maxAgeInSeconds);
170174

171-
expectResultToMatchTokenWith(result, profile.nhs_number, profile.birthdate, "newIdToken", "", 0, maxAgeInSeconds);
175+
expectResultToMatchTokenWith(
176+
result,
177+
profile.nhs_number,
178+
profile.birthdate,
179+
expectedAgeGroupForBirthdate,
180+
"newIdToken",
181+
"",
182+
0,
183+
maxAgeInSeconds,
184+
);
172185
});
173186
});
174187

@@ -184,14 +197,24 @@ describe("getToken", () => {
184197

185198
const result = await getToken(token, account, profile, maxAgeInSeconds);
186199

187-
expectResultToMatchTokenWith(result, profile.nhs_number, profile.birthdate, "newIdToken", "", 0, maxAgeInSeconds);
200+
expectResultToMatchTokenWith(
201+
result,
202+
profile.nhs_number,
203+
profile.birthdate,
204+
expectedAgeGroupForBirthdate,
205+
"newIdToken",
206+
"",
207+
0,
208+
maxAgeInSeconds,
209+
);
188210
});
189211
});
190212

191213
const expectResultToMatchTokenWith = (
192214
result: JWT | null,
193215
nhsNumber: string,
194216
birthdate: string | null | undefined,
217+
ageGroup: AgeGroup,
195218
idToken: string,
196219
apimToken: string,
197220
apimExpiresAt: number,
@@ -202,6 +225,7 @@ describe("getToken", () => {
202225
user: {
203226
nhs_number: nhsNumber,
204227
birthdate: birthdate,
228+
age_group: ageGroup,
205229
},
206230
nhs_login: {
207231
id_token: idToken,

src/utils/auth/callbacks/get-token.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { getAgeGroupOfUser } from "@src/app/_components/hub/ageGroupHelper";
2+
import { AgeGroup } from "@src/models/ageBasedHub";
13
import { NhsNumber } from "@src/models/vaccine";
24
import { getOrRefreshApimCredentials } from "@src/utils/auth/apim/get-or-refresh-apim-credentials";
35
import { ApimAccessCredentials } from "@src/utils/auth/apim/types";
@@ -88,6 +90,7 @@ const fillMissingFieldsInTokenWithDefaultValues = (token: JWT, apimAccessCredent
8890
user: {
8991
nhs_number: token.user?.nhs_number ?? ("" as NhsNumber),
9092
birthdate: token.user?.birthdate ?? ("" as BirthDate),
93+
age_group: token.user?.age_group ?? AgeGroup.UNKNOWN_AGE_GROUP,
9194
},
9295
nhs_login: {
9396
id_token: token.nhs_login?.id_token ?? ("" as IdToken),
@@ -110,11 +113,14 @@ const updateTokenWithValuesFromAccountAndProfile = (
110113
nowInSeconds: NowInSeconds,
111114
maxAgeInSeconds: MaxAgeInSeconds,
112115
): JWT => {
116+
const ageGroupOfUser = getAgeGroupOfUser(profile.birthdate ?? "");
117+
113118
const updatedToken: JWT = {
114119
...token,
115120
user: {
116121
nhs_number: (profile.nhs_number ?? "") as NhsNumber,
117122
birthdate: (profile.birthdate ?? "") as BirthDate,
123+
age_group: ageGroupOfUser,
118124
},
119125
nhs_login: {
120126
id_token: (account.id_token ?? "") as IdToken,

src/utils/auth/callbacks/get-updated-session.test.ts

Lines changed: 29 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,42 @@
1-
import { getAgeGroup } from "@src/app/_components/hub/ageGroupHelper";
21
import { AgeGroup } from "@src/models/ageBasedHub";
32
import { NhsNumber } from "@src/models/vaccine";
43
import { getUpdatedSession } from "@src/utils/auth/callbacks/get-updated-session";
54
import { AccessToken, BirthDate, ExpiresAt, IdToken } from "@src/utils/auth/types";
6-
import { calculateAge } from "@src/utils/date";
75
import { Session } from "next-auth";
86
import { JWT } from "next-auth/jwt";
97

108
jest.mock("sanitize-data", () => ({ sanitize: jest.fn() }));
11-
jest.mock("@src/utils/date", () => ({ calculateAge: jest.fn() }));
12-
jest.mock("@src/app/_components/hub/ageGroupHelper.ts", () => ({ getAgeGroup: jest.fn() }));
139

1410
const mockBirthdate = "1995-01-01";
15-
const mockCalculatedAge = 30;
16-
const mockAgeGroup = AgeGroup.AGE_75_to_80;
11+
const mockAgeGroup = AgeGroup.AGE_17_to_24;
1712

18-
describe("getUpdatedSession", () => {
19-
beforeEach(() => {
20-
(calculateAge as jest.Mock).mockImplementation(() => mockCalculatedAge);
21-
(getAgeGroup as jest.Mock).mockImplementation(() => mockAgeGroup);
22-
});
13+
const session: Session = {
14+
user: {
15+
nhs_number: "" as NhsNumber,
16+
age_group: undefined,
17+
},
18+
expires: "some-date",
19+
};
2320

24-
it("updates session with user fields and age information from token", () => {
25-
const session: Session = {
26-
user: {
27-
nhs_number: "" as NhsNumber,
28-
age_group: undefined,
29-
},
30-
expires: "some-date",
31-
};
21+
const token = {
22+
user: {
23+
nhs_number: "test-nhs-number" as NhsNumber,
24+
birthdate: mockBirthdate as BirthDate,
25+
age_group: mockAgeGroup,
26+
},
27+
nhs_login: {
28+
id_token: "test-id-token" as IdToken,
29+
},
30+
apim: {
31+
access_token: "test-access-token" as AccessToken,
32+
expires_at: 0 as ExpiresAt,
33+
},
34+
} as JWT;
3235

33-
const token = {
34-
user: {
35-
nhs_number: "test-nhs-number" as NhsNumber,
36-
birthdate: mockBirthdate as BirthDate,
37-
},
38-
nhs_login: {
39-
id_token: "test-id-token" as IdToken,
40-
},
41-
apim: {
42-
access_token: "test-access-token" as AccessToken,
43-
expires_at: 0 as ExpiresAt,
44-
},
45-
} as JWT;
36+
describe("getUpdatedSession", () => {
37+
beforeEach(() => {});
4638

39+
it("updates session with user fields and age information from token", () => {
4740
const result: Session = getUpdatedSession(session, token);
4841

4942
expect(result.user.nhs_number).toBe("test-nhs-number");
@@ -59,16 +52,16 @@ describe("getUpdatedSession", () => {
5952
expires: "some-date",
6053
};
6154

62-
const token = {} as JWT;
55+
const emptyToken = {} as JWT;
6356

64-
const result: Session = getUpdatedSession(session, token);
57+
const result: Session = getUpdatedSession(session, emptyToken);
6558

6659
expect(result.user.nhs_number).toBe("old-nhs-number");
6760
expect(result.user.age_group).toBe(AgeGroup.AGE_25_to_64);
6861
});
6962

7063
it("does not update session if session.user is missing", () => {
71-
const session = {
64+
const sessionWithoutUser = {
7265
expires: "some-date",
7366
} as Session;
7467

@@ -79,7 +72,7 @@ describe("getUpdatedSession", () => {
7972
},
8073
} as JWT;
8174

82-
const result: Session = getUpdatedSession(session, token);
75+
const result: Session = getUpdatedSession(sessionWithoutUser, token);
8376

8477
expect(result.user).toBeUndefined();
8578
});

src/utils/auth/callbacks/get-updated-session.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import { getAgeGroup } from "@src/app/_components/hub/ageGroupHelper";
2-
import { calculateAge } from "@src/utils/date";
31
import { logger } from "@src/utils/logger";
42
import { Session } from "next-auth";
53
import { JWT } from "next-auth/jwt";
@@ -12,7 +10,7 @@ const log: Logger = logger.child({
1210
const getUpdatedSession = (session: Session, token: JWT): Session => {
1311
if (token?.user && session.user) {
1412
session.user.nhs_number = token.user.nhs_number;
15-
session.user.age_group = getAgeGroup(calculateAge(token.user.birthdate));
13+
session.user.age_group = token.user.age_group;
1614
} else {
1715
log.info(
1816
{

src/utils/auth/types.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ declare module "next-auth/jwt" {
6161
user: {
6262
nhs_number: NhsNumber;
6363
birthdate: BirthDate;
64+
age_group: AgeGroup;
6465
};
6566
nhs_login: {
6667
id_token: IdToken;

0 commit comments

Comments
 (0)