Skip to content

Commit 96ccf85

Browse files
VIA-537 SB Use lazy config in middleware.
VIA-537 SB Add lazy config builder for tests. VIA-537 SB Extend use of lazy config to everything called by middleware. VIA-537 SB Extend use of lazy config to EliD & content services.
1 parent eef6c26 commit 96ccf85

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)