diff --git a/backend-api/src/functions/asyncBiometricToken/getBiometricToken/getBiometricToken.test.ts b/backend-api/src/functions/asyncBiometricToken/getBiometricToken/getBiometricToken.test.ts index 8a28bc5f..0bbbbeed 100644 --- a/backend-api/src/functions/asyncBiometricToken/getBiometricToken/getBiometricToken.test.ts +++ b/backend-api/src/functions/asyncBiometricToken/getBiometricToken/getBiometricToken.test.ts @@ -1,10 +1,170 @@ -import { successResult } from "../../utils/result"; +import { emptyFailure, Result, successResult } from "../../utils/result"; import { getBiometricToken } from "./getBiometricToken"; +import { expect } from "@jest/globals"; +import "../../testUtils/matchers"; +import { ISendHttpRequest } from "../../services/http/sendHttpRequest"; describe("getBiometricToken", () => { - it("Returns successResult containing a string", async () => { - const result = await getBiometricToken("mockUrl", "mockSubmitterKey"); + let result: Result; + let consoleDebugSpy: jest.SpyInstance; + let consoleErrorSpy: jest.SpyInstance; + let mockSendHttpRequest: ISendHttpRequest; + const expectedArguments = { + headers: { + "Content-Type": "application/x-www-form-urlencoded", + "X-Innovalor-Authorization": "mockSubmitterKey", + }, + method: "POST", + url: "https://mockUrl.com/oauth/token?grant_type=client_credentials", + }; - expect(result).toEqual(successResult("mockBiometricToken")); + beforeEach(() => { + consoleDebugSpy = jest.spyOn(console, "debug"); + consoleErrorSpy = jest.spyOn(console, "error"); + }); + + describe("On every call", () => { + beforeEach(async () => { + mockSendHttpRequest = jest.fn().mockResolvedValue({ + statusCode: 200, + body: "mockBody", + headers: { + mockHeaderKey: "mockHeaderValue", + }, + }); + + result = await getBiometricToken( + "https://mockUrl.com", + "mockSubmitterKey", + mockSendHttpRequest, + ); + }); + + it("Logs network call attempt at debug level", () => { + expect(consoleDebugSpy).toHaveBeenCalledWithLogFields({ + messageCode: + "MOBILE_ASYNC_BIOMETRIC_TOKEN_GET_BIOMETRIC_TOKEN_FROM_READID_ATTEMPT", + }); + }); + }); + + describe("Given an error is caught when requesting token", () => { + beforeEach(async () => { + mockSendHttpRequest = jest.fn().mockRejectedValue(new Error("mockError")); + + result = await getBiometricToken( + "https://mockUrl.com", + "mockSubmitterKey", + mockSendHttpRequest, + ); + }); + + it("Logs error", () => { + expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({ + messageCode: + "MOBILE_ASYNC_BIOMETRIC_TOKEN_GET_BIOMETRIC_TOKEN_FROM_READID_FAILURE", + }); + }); + + it("Returns an empty failure", () => { + expect(result).toEqual(emptyFailure()); + expect(mockSendHttpRequest).toBeCalledWith(expectedArguments); + }); + }); + + describe("Given the response is invalid", () => { + describe("Given response body is undefined", () => { + beforeEach(async () => { + mockSendHttpRequest = jest.fn().mockResolvedValue({ + statusCode: 200, + body: undefined, + headers: { + mockHeaderKey: "mockHeaderValue", + }, + }); + + result = await getBiometricToken( + "https://mockUrl.com", + "mockSubmitterKey", + mockSendHttpRequest, + ); + }); + + it("Logs error", () => { + expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({ + messageCode: + "MOBILE_ASYNC_BIOMETRIC_TOKEN_GET_BIOMETRIC_TOKEN_FROM_READID_FAILURE", + }); + }); + + it("Returns an empty failure", () => { + expect(result).toEqual(emptyFailure()); + expect(mockSendHttpRequest).toBeCalledWith(expectedArguments); + }); + }); + + describe("Given response body cannot be parsed", () => { + beforeEach(async () => { + mockSendHttpRequest = jest.fn().mockResolvedValue({ + statusCode: 200, + body: "Invalid JSON", + headers: { + mockHeaderKey: "mockHeaderValue", + }, + }); + + result = await getBiometricToken( + "https://mockUrl.com", + "mockSubmitterKey", + mockSendHttpRequest, + ); + }); + + it("Logs error", () => { + expect(consoleErrorSpy).toHaveBeenCalledWithLogFields({ + messageCode: + "MOBILE_ASYNC_BIOMETRIC_TOKEN_GET_BIOMETRIC_TOKEN_FROM_READID_FAILURE", + }); + }); + + it("Returns an empty failure", () => { + expect(result).toEqual(emptyFailure()); + expect(mockSendHttpRequest).toBeCalledWith(expectedArguments); + }); + }); + }); + + describe("Given valid request is made", () => { + beforeEach(async () => { + mockSendHttpRequest = jest.fn().mockResolvedValue({ + statusCode: 200, + body: JSON.stringify({ + access_token: "mockBiometricToken", + expires_in: 3600, + token_type: "Bearer", + }), + headers: { + mockHeaderKey: "mockHeaderValue", + }, + }); + + result = await getBiometricToken( + "https://mockUrl.com", + "mockSubmitterKey", + mockSendHttpRequest, + ); + }); + + it("Logs success at debug level", () => { + expect(consoleDebugSpy).toHaveBeenCalledWithLogFields({ + messageCode: + "MOBILE_ASYNC_BIOMETRIC_TOKEN_GET_BIOMETRIC_TOKEN_FROM_READID_SUCCESS", + }); + }); + + it("Returns successResult containing biometric token", () => { + expect(result).toEqual(successResult("mockBiometricToken")); + expect(mockSendHttpRequest).toBeCalledWith(expectedArguments); + }); }); }); diff --git a/backend-api/src/functions/asyncBiometricToken/getBiometricToken/getBiometricToken.ts b/backend-api/src/functions/asyncBiometricToken/getBiometricToken/getBiometricToken.ts index 5e29dead..5d90cb3a 100644 --- a/backend-api/src/functions/asyncBiometricToken/getBiometricToken/getBiometricToken.ts +++ b/backend-api/src/functions/asyncBiometricToken/getBiometricToken/getBiometricToken.ts @@ -1,10 +1,93 @@ -import { Result, successResult } from "../../utils/result"; +import { logger } from "../../common/logging/logger"; +import { LogMessage } from "../../common/logging/LogMessage"; +import { + ISendHttpRequest, + sendHttpRequest as sendHttpRequestDefault, +} from "../../services/http/sendHttpRequest"; +import { emptyFailure, Result, successResult } from "../../utils/result"; export type GetBiometricToken = ( url: string, submitterKey: string, + sendHttpRequest?: ISendHttpRequest, ) => Promise>; -export const getBiometricToken: GetBiometricToken = async () => { - return successResult("mockBiometricToken"); +export const getBiometricToken: GetBiometricToken = async ( + url: string, + submitterKey: string, + sendHttpRequest: ISendHttpRequest = sendHttpRequestDefault, +) => { + const httpRequest = { + url: `${url}/oauth/token?grant_type=client_credentials`, + method: "POST" as const, + headers: { + "X-Innovalor-Authorization": submitterKey, + "Content-Type": "application/x-www-form-urlencoded", + }, + }; + const httpRequestLogData = { + ...httpRequest, + headers: { + ...httpRequest.headers, + "X-Innovalor-Authorization": "Secret value and cannot be logged", + }, + }; + + let response; + try { + logger.debug( + LogMessage.BIOMETRIC_TOKEN_GET_BIOMETRIC_TOKEN_FROM_READID_ATTEMPT, + { + data: { + httpRequest: httpRequestLogData, + }, + }, + ); + response = await sendHttpRequest(httpRequest); + } catch (error) { + logger.error( + LogMessage.BIOMETRIC_TOKEN_GET_BIOMETRIC_TOKEN_FROM_READID_FAILURE, + { + data: { + error, + httpRequest: httpRequestLogData, + }, + }, + ); + return emptyFailure(); + } + + if (response?.body == null) { + logger.error( + LogMessage.BIOMETRIC_TOKEN_GET_BIOMETRIC_TOKEN_FROM_READID_FAILURE, + { + data: { + response, + httpRequest: httpRequestLogData, + }, + }, + ); + return emptyFailure(); + } + + let parsedBody; + try { + parsedBody = JSON.parse(response.body); + } catch (error) { + logger.error( + LogMessage.BIOMETRIC_TOKEN_GET_BIOMETRIC_TOKEN_FROM_READID_FAILURE, + { + data: { + error, + httpRequest: httpRequestLogData, + }, + }, + ); + return emptyFailure(); + } + + logger.debug( + LogMessage.BIOMETRIC_TOKEN_GET_BIOMETRIC_TOKEN_FROM_READID_SUCCESS, + ); + return successResult(parsedBody.access_token); }; diff --git a/backend-api/src/functions/common/logging/LogMessage.ts b/backend-api/src/functions/common/logging/LogMessage.ts index f4f144be..3bfcd1e1 100644 --- a/backend-api/src/functions/common/logging/LogMessage.ts +++ b/backend-api/src/functions/common/logging/LogMessage.ts @@ -34,6 +34,21 @@ export class LogMessage implements LogAttributes { "MOBILE_ASYNC_BIOMETRIC_TOKEN_REQUEST_BODY_INVALID", "The incoming request body was missing or invalid.", ); + static readonly BIOMETRIC_TOKEN_GET_BIOMETRIC_TOKEN_FROM_READID_ATTEMPT = + new LogMessage( + "MOBILE_ASYNC_BIOMETRIC_TOKEN_GET_BIOMETRIC_TOKEN_FROM_READID_ATTEMPT", + "Attempting to retrieve biometric access token from ReadID", + ); + static readonly BIOMETRIC_TOKEN_GET_BIOMETRIC_TOKEN_FROM_READID_FAILURE = + new LogMessage( + "MOBILE_ASYNC_BIOMETRIC_TOKEN_GET_BIOMETRIC_TOKEN_FROM_READID_FAILURE", + "Failed to retrieve biometric access token from ReadID", + ); + static readonly BIOMETRIC_TOKEN_GET_BIOMETRIC_TOKEN_FROM_READID_SUCCESS = + new LogMessage( + "MOBILE_ASYNC_BIOMETRIC_TOKEN_GET_BIOMETRIC_TOKEN_FROM_READID_SUCCESS", + "Successfully retrieved biometric access token from ReadID", + ); private constructor( public readonly messageCode: string, diff --git a/backend-api/src/functions/services/http/sendHttpRequest.ts b/backend-api/src/functions/services/http/sendHttpRequest.ts index 93edb2dd..b1b68f5a 100644 --- a/backend-api/src/functions/services/http/sendHttpRequest.ts +++ b/backend-api/src/functions/services/http/sendHttpRequest.ts @@ -63,7 +63,7 @@ export type RetryConfig = { delayInMillis?: number; }; -export type HttpMethod = "GET"; +export type HttpMethod = "GET" | "POST"; export type HttpHeaders = { [key: string]: string; diff --git a/backend-api/template.yaml b/backend-api/template.yaml index 157c93e3..96f7b36f 100644 --- a/backend-api/template.yaml +++ b/backend-api/template.yaml @@ -160,7 +160,7 @@ Mappings: EnvironmentVariables: dev: STSBASEURL: 'https://mob-sts-mock.review-b-async.dev.account.gov.uk' - ReadIdBaseUrl: 'https://api-mob-readid-mock.review-b-async.dev.account.gov.uk' + ReadIdBaseUrl: 'https://api-mob-readid-mock.review-b-async.dev.account.gov.uk/v2' ClientRegistrySecretPath: 'dev/clientRegistry' BiometricSubmitterKeySecretPathPassport: '/dev/BIOMETRIC_SUBMITTER_ACCESS_KEY_NFC_PASSPORT' BiometricSubmitterKeySecretPathBrp: '/dev/BIOMETRIC_SUBMITTER_ACCESS_KEY_NFC_BRP' @@ -169,7 +169,7 @@ Mappings: build: STSBASEURL: 'https://mob-sts-mock.review-b-async.build.account.gov.uk' - ReadIdBaseUrl: 'https://api-mob-readid-mock.review-b-async.build.account.gov.uk' + ReadIdBaseUrl: 'https://api-mob-readid-mock.review-b-async.build.account.gov.uk/v2' ClientRegistrySecretPath: 'build/clientRegistry' BiometricSubmitterKeySecretPathPassport: '/build/BIOMETRIC_SUBMITTER_ACCESS_KEY_NFC_PASSPORT' BiometricSubmitterKeySecretPathBrp: '/build/BIOMETRIC_SUBMITTER_ACCESS_KEY_NFC_BRP'