Skip to content

Commit aa79413

Browse files
SPIKE SB Use lazy config in middleware.
SPIKE SB Add lazy config builder for tests. SPIKE SB Extend use of lazy config to everything called by middleware. SPIKE SB Extend use of lazy config to EliD & content services.
1 parent 063521c commit aa79413

19 files changed

+255
-137
lines changed

auth.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { getToken } from "@src/utils/auth/callbacks/get-token";
55
import { getUpdatedSession } from "@src/utils/auth/callbacks/get-updated-session";
66
import { isValidSignIn } from "@src/utils/auth/callbacks/is-valid-signin";
77
import { MaxAgeInSeconds } from "@src/utils/auth/types";
8-
import { AppConfig, configProvider } from "@src/utils/config";
8+
import lazyConfig from "@src/utils/lazy-config";
99
import { logger } from "@src/utils/logger";
1010
import { profilePerformanceEnd, profilePerformanceStart } from "@src/utils/performance";
1111
import { RequestContext, asyncLocalStorage } from "@src/utils/requestContext";
@@ -21,16 +21,15 @@ const AuthJWTPerformanceMarker = "auth-jwt-callback";
2121
const AuthSessionPerformanceMarker = "auth-session-callback";
2222

2323
export const { handlers, signIn, signOut, auth } = NextAuth(async () => {
24-
const config: AppConfig = await configProvider();
25-
const MAX_SESSION_AGE_SECONDS: number = config.MAX_SESSION_AGE_MINUTES * 60;
24+
const MAX_SESSION_AGE_SECONDS: number = ((await lazyConfig.MAX_SESSION_AGE_MINUTES) as number) * 60;
2625
const headerValues = await headers();
2726

2827
const requestContext: RequestContext = extractRequestContextFromHeaders(headerValues);
2928

3029
return await asyncLocalStorage.run(requestContext, async () => {
3130
return {
3231
providers: [await NHSLoginAuthProvider()],
33-
secret: config.AUTH_SECRET,
32+
secret: (await lazyConfig.AUTH_SECRET) as string,
3433
pages: {
3534
signIn: SSO_FAILURE_ROUTE,
3635
signOut: SESSION_LOGOUT_ROUTE,
@@ -49,7 +48,7 @@ export const { handlers, signIn, signOut, auth } = NextAuth(async () => {
4948
let response: boolean;
5049
try {
5150
profilePerformanceStart(AuthSignInPerformanceMarker);
52-
response = isValidSignIn(account, config);
51+
response = await isValidSignIn(account);
5352
profilePerformanceEnd(AuthSignInPerformanceMarker);
5453
} catch (error) {
5554
log.error({ error: error }, "signIn() callback error");
@@ -64,7 +63,7 @@ export const { handlers, signIn, signOut, auth } = NextAuth(async () => {
6463
let response;
6564
try {
6665
profilePerformanceStart(AuthJWTPerformanceMarker);
67-
response = getToken(token, account, profile, config, MAX_SESSION_AGE_SECONDS as MaxAgeInSeconds);
66+
response = getToken(token, account, profile, MAX_SESSION_AGE_SECONDS as MaxAgeInSeconds);
6867
profilePerformanceEnd(AuthJWTPerformanceMarker);
6968
} catch (error) {
7069
log.error({ error: error }, "jwt() callback error");

contract/fetch-eligibility-content.contract.ts

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,13 @@
11
import { NhsNumber } from "@src/models/vaccine";
22
import { EligibilityApiResponse } from "@src/services/eligibility-api/api-types";
33
import { fetchEligibilityContent } from "@src/services/eligibility-api/gateway/fetch-eligibility-content";
4-
import { AppConfig } from "@src/utils/config";
4+
import lazyConfig from "@src/utils/lazy-config";
55
import { asyncLocalStorage } from "@src/utils/requestContext";
6-
import { appConfigBuilder } from "@test-data/config/builders";
6+
import { AsyncConfigMock, lazyConfigBuilder } from "@test-data/config/builders";
77
import { readFileSync } from "fs";
88
import { pactWith } from "jest-pact";
99

10-
jest.mock("@src/utils/config", () => ({
11-
configProvider: jest.fn((): Promise<AppConfig> => {
12-
const value: AppConfig = appConfigBuilder()
13-
.withELIGIBILITY_API_ENDPOINT(new URL("http://localhost:1234/"))
14-
.andELIGIBILITY_API_KEY("test-api-key")
15-
.andIS_APIM_AUTH_ENABLED(false)
16-
.build();
17-
return Promise.resolve(value);
18-
}),
19-
}));
10+
jest.mock("@src/utils/lazy-config");
2011
jest.mock("next-auth/jwt", () => ({
2112
getToken: jest.fn(),
2213
}));
@@ -49,6 +40,17 @@ const successfulResponse: EligibilityApiResponse = {
4940
};
5041

5142
pactWith({ consumer: "VitA", provider: "EliD", port: 1234, logLevel: "warn" }, (provider) => {
43+
const mockedConfig = lazyConfig as AsyncConfigMock;
44+
45+
beforeEach(() => {
46+
const defaultConfig = lazyConfigBuilder()
47+
.withEligibilityApiEndpoint(new URL("http://localhost:1234/"))
48+
.andEligibilityApiKey("test-api-key")
49+
.andIsApimAuthEnabled(false)
50+
.build();
51+
Object.assign(mockedConfig, defaultConfig);
52+
});
53+
5254
describe("EliD returns expected fields", () => {
5355
const mockNhsNumber = "9450114080" as NhsNumber;
5456
const vitaTraceId = "mock-trace-id";

src/middleware.test.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,16 @@
44
import { auth } from "@project/auth";
55
import { unprotectedUrlPaths } from "@src/app/_components/inactivity/constants";
66
import { config, middleware } from "@src/middleware";
7-
import { AppConfig, configProvider } from "@src/utils/config";
7+
import lazyConfig from "@src/utils/lazy-config";
8+
import { AsyncConfigMock, lazyConfigBuilder } from "@test-data/config/builders";
89
import { NextRequest } from "next/server";
910

1011
jest.mock("@project/auth", () => ({
1112
auth: jest.fn(),
1213
signIn: jest.fn(),
1314
}));
1415
jest.mock("sanitize-data", () => ({ sanitize: jest.fn() }));
15-
16-
jest.mock("@src/utils/config");
16+
jest.mock("@src/utils/lazy-config");
1717

1818
const middlewareRegex = new RegExp(config.matcher[0]);
1919
const otherExcludedPaths = ["/favicon.ico", "/assets", "/js", "/css", "/_next"];
@@ -34,12 +34,14 @@ function getMockRequest(testUrl: string) {
3434
}
3535

3636
describe("middleware", () => {
37+
const mockedConfig = lazyConfig as AsyncConfigMock;
38+
3739
beforeEach(() => {
38-
(configProvider as jest.Mock).mockImplementation(
39-
(): Partial<AppConfig> => ({
40-
NHS_APP_REDIRECT_LOGIN_URL: "https://nhs-app-redirect-login-url",
41-
}),
42-
);
40+
const defaultConfig = lazyConfigBuilder()
41+
.withNhsAppRedirectLoginUrl(new URL("https://nhs-app-redirect-login-url"))
42+
.build();
43+
Object.assign(mockedConfig, defaultConfig);
44+
4345
jest.clearAllMocks();
4446
});
4547

src/middleware.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { auth } from "@project/auth";
2-
import { AppConfig, configProvider } from "@src/utils/config";
2+
import lazyConfig from "@src/utils/lazy-config";
33
import { logger } from "@src/utils/logger";
44
import { profilePerformanceEnd, profilePerformanceStart } from "@src/utils/performance";
55
import { RequestContext, asyncLocalStorage } from "@src/utils/requestContext";
@@ -25,13 +25,11 @@ const middlewareWrapper = async (request: NextRequest) => {
2525
const headers = new Headers(request.headers);
2626
headers.set("nextUrl", request.nextUrl.href);
2727

28-
const config: AppConfig = await configProvider();
29-
3028
let response: NextResponse;
3129
const session: Session | null = await auth();
3230
if (!session?.user) {
3331
log.info({ context: { nextUrl: request.nextUrl.href } }, "Missing user session, redirecting to login");
34-
response = NextResponse.redirect(new URL(config.NHS_APP_REDIRECT_LOGIN_URL));
32+
response = NextResponse.redirect((await lazyConfig.NHS_APP_REDIRECT_LOGIN_URL) as URL);
3533
response.headers.set("Cache-Control", "no-cache, no-store, max-age=0, must-revalidate");
3634
} else {
3735
response = NextResponse.next({

src/services/content-api/content-api.integration.test.tsx

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ import mockRsvVaccineJson from "@project/wiremock/__files/rsv-vaccine.json";
66
import { VaccineType } from "@src/models/vaccine";
77
import { getContentForVaccine } from "@src/services/content-api/content-service";
88
import { GetContentForVaccineResponse } from "@src/services/content-api/types";
9-
import { configProvider } from "@src/utils/config";
9+
import lazyConfig from "@src/utils/lazy-config";
10+
import { AsyncConfigMock, lazyConfigBuilder } from "@test-data/config/builders";
1011
import { Readable } from "stream";
1112

1213
jest.mock("@src/utils/config");
@@ -23,6 +24,8 @@ const mockRsvResponse = {
2324
};
2425

2526
describe("Content API Read Integration Test ", () => {
27+
const mockedConfig = lazyConfig as AsyncConfigMock;
28+
2629
afterEach(async () => {
2730
const { styledVaccineContent, contentError }: GetContentForVaccineResponse = await getContentForVaccine(
2831
VaccineType.RSV,
@@ -38,15 +41,14 @@ describe("Content API Read Integration Test ", () => {
3841
});
3942

4043
it("should return processed data from local cache", async () => {
41-
(configProvider as jest.Mock).mockImplementation(() => ({
42-
CONTENT_CACHE_PATH: "wiremock/__files/",
43-
}));
44+
const defaultConfig = lazyConfigBuilder().withContentCachePath("wiremock/__files/").build();
45+
Object.assign(mockedConfig, defaultConfig);
4446
});
4547

4648
it("should return processed data from external cache", async () => {
47-
(configProvider as jest.Mock).mockImplementation(() => ({
48-
CONTENT_CACHE_PATH: "s3://test-bucket",
49-
}));
49+
const defaultConfig = lazyConfigBuilder().withContentCachePath("s3://test-bucket").build();
50+
Object.assign(mockedConfig, defaultConfig);
51+
5052
(S3Client as jest.Mock).mockImplementation(() => ({
5153
send: () => mockRsvResponse,
5254
}));

src/services/content-api/content-service.test.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,20 @@ import { getContentForVaccine } from "@src/services/content-api/content-service"
44
import { readContentFromCache } from "@src/services/content-api/gateway/content-reader-service";
55
import { InvalidatedCacheError, S3NoSuchKeyError } from "@src/services/content-api/gateway/exceptions";
66
import { ContentErrorTypes, GetContentForVaccineResponse } from "@src/services/content-api/types";
7-
import { configProvider } from "@src/utils/config";
7+
import lazyConfig from "@src/utils/lazy-config";
8+
import { AsyncConfigMock, lazyConfigBuilder } from "@test-data/config/builders";
89

910
jest.mock("@src/services/content-api/gateway/content-reader-service");
10-
jest.mock("@src/utils/config");
11+
jest.mock("@src/utils/lazy-config");
1112
jest.mock("sanitize-data", () => ({ sanitize: jest.fn() }));
1213

1314
describe("getContentForVaccine()", () => {
15+
const mockedConfig = lazyConfig as AsyncConfigMock;
16+
1417
describe("when readContent succeeds", () => {
1518
beforeEach(() => {
16-
(configProvider as jest.Mock).mockImplementation(() => ({
17-
CONTENT_CACHE_PATH: "wiremock/__files/",
18-
}));
19+
const defaultConfig = lazyConfigBuilder().withContentCachePath("wiremock/__files/").build();
20+
Object.assign(mockedConfig, defaultConfig);
1921
});
2022

2123
it("should return response for rsv vaccine from content cache", async () => {

src/services/content-api/content-service.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
StyledVaccineContent,
1010
VaccinePageContent,
1111
} from "@src/services/content-api/types";
12-
import { AppConfig, configProvider } from "@src/utils/config";
12+
import lazyConfig from "@src/utils/lazy-config";
1313
import { logger } from "@src/utils/logger";
1414
import { profilePerformanceEnd, profilePerformanceStart } from "@src/utils/performance";
1515
import { Logger } from "pino";
@@ -22,11 +22,11 @@ const getContentForVaccine = async (vaccineType: VaccineType): Promise<GetConten
2222
try {
2323
profilePerformanceStart(GetVaccineContentPerformanceMarker);
2424

25-
const config: AppConfig = await configProvider();
25+
const cachePath = (await lazyConfig.CONTENT_CACHE_PATH) as string;
2626
const vaccineCacheFilename = VaccineInfo[vaccineType].cacheFilename;
2727

2828
// fetch content from api
29-
const vaccineContent = await readContentFromCache(config.CONTENT_CACHE_PATH, vaccineCacheFilename, vaccineType);
29+
const vaccineContent = await readContentFromCache(cachePath, vaccineCacheFilename, vaccineType);
3030

3131
// filter and style content
3232
const filteredContent: VaccinePageContent = getFilteredContentForVaccine(vaccineType, vaccineContent);

src/services/eligibility-api/gateway/fetch-eligibility-content.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
} from "@src/services/eligibility-api/gateway/exceptions";
1111
import { Cohort, Heading } from "@src/services/eligibility-api/types";
1212
import { getApimAccessToken } from "@src/utils/auth/apim/get-apim-access-token";
13-
import { AppConfig, configProvider } from "@src/utils/config";
13+
import lazyConfig from "@src/utils/lazy-config";
1414
import { logger } from "@src/utils/logger";
1515
import { asyncLocalStorage } from "@src/utils/requestContext";
1616
import axios, { AxiosError, AxiosResponse, HttpStatusCode } from "axios";
@@ -27,10 +27,8 @@ const log = logger.child({ module: "fetch-eligibility-content" });
2727
const ELIGIBILITY_API_PATH_SUFFIX = "eligibility-signposting-api/patient-check/";
2828

2929
export const fetchEligibilityContent = async (nhsNumber: NhsNumber): Promise<EligibilityApiResponse> => {
30-
const config: AppConfig = await configProvider();
31-
32-
const apiEndpoint: URL = config.ELIGIBILITY_API_ENDPOINT;
33-
const apiKey: string = config.ELIGIBILITY_API_KEY;
30+
const apiEndpoint: URL = (await lazyConfig.ELIGIBILITY_API_ENDPOINT) as URL;
31+
const apiKey: string = (await lazyConfig.ELIGIBILITY_API_KEY) as string;
3432
const vitaTraceId: string | undefined = asyncLocalStorage?.getStore()?.traceId;
3533

3634
const elidUri: string = `${apiEndpoint}${ELIGIBILITY_API_PATH_SUFFIX}${nhsNumber}`;
@@ -40,8 +38,8 @@ export const fetchEligibilityContent = async (nhsNumber: NhsNumber): Promise<Eli
4038
"X-Correlation-ID": vitaTraceId,
4139
};
4240

43-
if (config.IS_APIM_AUTH_ENABLED) {
44-
const apimAccessToken = await getApimAccessToken(config);
41+
if (await lazyConfig.IS_APIM_AUTH_ENABLED) {
42+
const apimAccessToken = await getApimAccessToken();
4543
headers = { ...headers, Authorization: `Bearer ${apimAccessToken}` };
4644
}
4745

src/utils/auth/apim/get-apim-access-token.test.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@ import { getApimAccessToken, retrieveApimCredentials } from "@src/utils/auth/api
33
import { getOrRefreshApimCredentials } from "@src/utils/auth/apim/get-or-refresh-apim-credentials";
44
import { getJwtToken } from "@src/utils/auth/get-jwt-token";
55
import { AccessToken, IdToken } from "@src/utils/auth/types";
6-
import { AppConfig } from "@src/utils/config";
7-
import { appConfigBuilder } from "@test-data/config/builders";
86

97
jest.mock("@src/utils/auth/get-jwt-token", () => ({
108
getJwtToken: jest.fn(),
@@ -19,7 +17,6 @@ jest.mock("@src/utils/auth/apim/get-or-refresh-apim-credentials", () => ({
1917
getOrRefreshApimCredentials: jest.fn(),
2018
}));
2119

22-
const mockConfig: AppConfig = appConfigBuilder().build();
2320
const nowInSeconds = 1000;
2421

2522
const apimAccessTokenFromJwt = "test-access-token" as AccessToken;
@@ -50,24 +47,24 @@ describe("getApimAccessToken", () => {
5047

5148
(getJwtToken as jest.Mock).mockResolvedValue(mockJwtToken);
5249

53-
const apimAccessToken = await getApimAccessToken(mockConfig);
50+
const apimAccessToken = await getApimAccessToken();
5451

55-
expect(getOrRefreshApimCredentials).toHaveBeenCalledWith(mockConfig, mockJwtToken, nowInSeconds);
52+
expect(getOrRefreshApimCredentials).toHaveBeenCalledWith(mockJwtToken, nowInSeconds);
5653
expect(apimAccessToken).toEqual(getOrRefreshedApimAccessToken as AccessToken);
5754
});
5855

5956
it("should throw error if APIM access token not available in JWT token", async () => {
6057
(getJwtToken as jest.Mock).mockResolvedValue({ apim: {} });
6158

62-
await expect(getApimAccessToken(mockConfig)).rejects.toThrow("APIM access token is not present on JWT token");
59+
await expect(getApimAccessToken()).rejects.toThrow("APIM access token is not present on JWT token");
6360
});
6461

6562
it("should throw error if _getOrRefresh APIM access token returns undefined", async () => {
6663
(getOrRefreshApimCredentials as jest.Mock).mockResolvedValue(undefined);
6764

6865
(getJwtToken as jest.Mock).mockResolvedValue(mockJwtToken);
6966

70-
await expect(getApimAccessToken(mockConfig)).rejects.toThrow("getOrRefreshApimCredentials returned undefined");
67+
await expect(getApimAccessToken()).rejects.toThrow("getOrRefreshApimCredentials returned undefined");
7168
});
7269
});
7370

src/utils/auth/apim/get-apim-access-token.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,11 @@ import { getOrRefreshApimCredentials } from "@src/utils/auth/apim/get-or-refresh
44
import { ApimAccessCredentials, ApimTokenResponse } from "@src/utils/auth/apim/types";
55
import { getJwtToken } from "@src/utils/auth/get-jwt-token";
66
import { AccessToken, ExpiresAt, IdToken } from "@src/utils/auth/types";
7-
import { AppConfig } from "@src/utils/config";
87
import { logger } from "@src/utils/logger";
98

109
const log = logger.child({ module: "utils-auth-apim-get-apim-access-token" });
1110

12-
const getApimAccessToken = async (config: AppConfig): Promise<AccessToken> => {
11+
const getApimAccessToken = async (): Promise<AccessToken> => {
1312
/**
1413
* Gets the APIM access token from the JWT session cookie.
1514
*
@@ -26,7 +25,7 @@ const getApimAccessToken = async (config: AppConfig): Promise<AccessToken> => {
2625
}
2726

2827
const nowInSeconds = Math.floor(Date.now() / 1000);
29-
const apimAccessCredentials = await getOrRefreshApimCredentials(config, token, nowInSeconds);
28+
const apimAccessCredentials = await getOrRefreshApimCredentials(token, nowInSeconds);
3029

3130
if (!apimAccessCredentials) {
3231
log.error("Error: getOrRefreshApimCredentials returned undefined");

0 commit comments

Comments
 (0)