diff --git a/backend-api/src/functions/adapters/aws/sqs/sendMessageToSqs.ts b/backend-api/src/functions/adapters/aws/sqs/sendMessageToSqs.ts index 83e53f2c4..4bc28a67a 100644 --- a/backend-api/src/functions/adapters/aws/sqs/sendMessageToSqs.ts +++ b/backend-api/src/functions/adapters/aws/sqs/sendMessageToSqs.ts @@ -7,9 +7,9 @@ import { NodeHttpHandler } from "@smithy/node-http-handler"; import { LogMessage } from "../../../common/logging/LogMessage"; import { logger } from "../../../common/logging/logger"; import { emptyFailure, Result, successResult } from "../../../utils/result"; -import { SQSMessageBody } from "./types"; +import { ISendMessageToSqs, SQSMessageBody } from "./types"; -export const sendMessageToSqs = async ( +export const sendMessageToSqs: ISendMessageToSqs = async ( sqsArn: string, messageBody: SQSMessageBody, ): Promise> => { diff --git a/backend-api/src/functions/adapters/aws/sqs/types.ts b/backend-api/src/functions/adapters/aws/sqs/types.ts index becc4f7f6..d111bf612 100644 --- a/backend-api/src/functions/adapters/aws/sqs/types.ts +++ b/backend-api/src/functions/adapters/aws/sqs/types.ts @@ -1,4 +1,15 @@ import { VcIssuedTxMAEvent } from "../../../asyncIssueBiometricCredential/getVcIssuedEvent"; +import { + AppStartEvent, + ClientCredentialsTokenIssuedEvent, + StartEvent, +} from "../../../common/audit/types"; +import { Result } from "../../../utils/result"; + +export type ISendMessageToSqs = ( + sqsArn: string, + messageBody: SQSMessageBody, +) => Promise>; export interface VendorProcessingMessage { biometricSessionId: string; @@ -22,6 +33,9 @@ export interface VerifiableCredentialMessage { export type SQSMessageBody = | VcIssuedTxMAEvent + | AppStartEvent + | ClientCredentialsTokenIssuedEvent + | StartEvent | VendorProcessingMessage | OutboundQueueErrorMessage | VerifiableCredentialMessage; diff --git a/backend-api/src/functions/asyncActiveSession/asyncActiveSessionHandler.test.ts b/backend-api/src/functions/asyncActiveSession/asyncActiveSessionHandler.test.ts index de8a1269c..874e38d85 100644 --- a/backend-api/src/functions/asyncActiveSession/asyncActiveSessionHandler.test.ts +++ b/backend-api/src/functions/asyncActiveSession/asyncActiveSessionHandler.test.ts @@ -2,7 +2,6 @@ import { expect } from "@jest/globals"; import { APIGatewayProxyResult, Context } from "aws-lambda"; import "../../../tests/testUtils/matchers"; import { logger } from "../common/logging/logger"; -import { MockEventWriterSuccess } from "../services/events/tests/mocks"; import { MockSessionServiceGetErrorResult, MockSessionServiceGetNullSuccessResult, @@ -13,12 +12,12 @@ import { MockJWTBuilder } from "../testUtils/mockJwtBuilder"; import { buildRequest } from "../testUtils/mockRequest"; import { mockGovukSigninJourneyId, - mockInertEventService, + mockSendMessageToSqsFailure, + mockSendMessageToSqsSuccess, mockSessionId, - mockSuccessfulEventService, - mockWriteGenericEventSuccessResult, + NOW_IN_MILLISECONDS, + NOW_IN_SECONDS, } from "../testUtils/unitTestData"; -import { errorResult } from "../utils/result"; import { lambdaHandlerConstructor } from "./asyncActiveSessionHandler"; import { IAsyncActiveSessionDependencies } from "./handlerDependencies"; import { @@ -31,6 +30,7 @@ import { MockTokenServiceServerError, MockTokenServiceSuccess, } from "./mocks"; +import { AppStartEvent } from "../common/audit/types"; const env = { ENCRYPTION_KEY_ARN: "mockEncryptionKeyArn", @@ -59,11 +59,17 @@ describe("Async Active Session", () => { jweDecrypter: () => new MockJweDecrypterSuccess(), tokenService: () => new MockTokenServiceSuccess(), sessionService: () => new MockSessionServiceGetSuccessResult(), - eventService: () => new MockEventWriterSuccess(), + sendMessageToSqs: mockSendMessageToSqsSuccess, }; context = buildLambdaContext(); consoleInfoSpy = jest.spyOn(console, "info"); consoleErrorSpy = jest.spyOn(console, "error"); + jest.useFakeTimers(); + jest.setSystemTime(NOW_IN_MILLISECONDS); + }); + + afterEach(() => { + jest.useRealTimers(); }); describe("On every invocation", () => { @@ -73,7 +79,14 @@ describe("Async Active Session", () => { beforeEach(async () => { result = await lambdaHandlerConstructor( dependencies, - { ...validRequest, ...{ headers: { "User-Agent": androidUserAgent } } }, + { + ...validRequest, + ...{ + headers: { + "User-Agent": androidUserAgent, + }, + }, + }, context, ); }); @@ -480,26 +493,20 @@ describe("Async Active Session", () => { describe("Given an active session is found", () => { beforeEach(async () => { const request = buildRequest({ - headers: { Authorization: `Bearer ${jwtBuilder.getEncodedJwt()}` }, + headers: { + Authorization: `Bearer ${jwtBuilder.getEncodedJwt()}`, + "Txma-Audit-Encoded": "mockTxmaAuditEncoded", + }, }); dependencies.sessionService = () => new MockSessionServiceGetSuccessResult(); - dependencies.eventService = () => mockSuccessfulEventService; result = await lambdaHandlerConstructor(dependencies, request, context); }); describe("Given DCMAW_ASYNC_CRI_APP_START event fails to write to TxMA", () => { beforeEach(async () => { - dependencies.eventService = () => ({ - ...mockInertEventService, - writeGenericEvent: jest.fn().mockResolvedValue( - errorResult({ - errorMessage: "mockError", - }), - ), - }); - + dependencies.sendMessageToSqs = mockSendMessageToSqsFailure; result = await lambdaHandlerConstructor( dependencies, validRequest, @@ -532,18 +539,29 @@ describe("Async Active Session", () => { }); it("Writes DCMAW_ASYNC_CRI_APP_START event to TxMA", () => { - expect(mockWriteGenericEventSuccessResult).toHaveBeenCalledWith({ - eventName: "DCMAW_ASYNC_CRI_APP_START", - sub: "mockSub", - sessionId: mockSessionId, - govukSigninJourneyId: mockGovukSigninJourneyId, - getNowInMilliseconds: Date.now, - componentId: "https://mockIssuer.com/", - ipAddress: "1.1.1.1", - txmaAuditEncoded: undefined, - redirect_uri: "https://mockUrl.com/redirect", - suspected_fraud_signal: undefined, - }); + const appStart: AppStartEvent = { + user: { + user_id: "mockSub", + session_id: mockSessionId, + govuk_signin_journey_id: mockGovukSigninJourneyId, + ip_address: "1.1.1.1", + }, + timestamp: NOW_IN_SECONDS, + event_timestamp_ms: NOW_IN_MILLISECONDS, + event_name: "DCMAW_ASYNC_CRI_APP_START", + component_id: "https://mockIssuer.com/", + extensions: { redirect_uri: "https://mockUrl.com/redirect" }, + restricted: { + device_information: { encoded: "mockTxmaAuditEncoded" }, + }, + }; + expect(mockSendMessageToSqsSuccess).toHaveBeenCalledNthWithSqsMessage( + 1, + { + sqsArn: "mockTxmaSqs", + expectedMessage: appStart, + }, + ); }); it("Logs COMPLETED with persistent identifiers", () => { diff --git a/backend-api/src/functions/asyncActiveSession/asyncActiveSessionHandler.ts b/backend-api/src/functions/asyncActiveSession/asyncActiveSessionHandler.ts index e3b43b929..92b5ea198 100644 --- a/backend-api/src/functions/asyncActiveSession/asyncActiveSessionHandler.ts +++ b/backend-api/src/functions/asyncActiveSession/asyncActiveSessionHandler.ts @@ -20,7 +20,8 @@ import { getAuditData, } from "../common/request/getAuditData/getAuditData"; import { Session } from "../services/session/types"; -import { IEventService } from "../services/events/types"; +import { ISendMessageToSqs } from "../adapters/aws/sqs/types"; +import { getAppStartEvent } from "./getAppStartEvent"; export async function lambdaHandlerConstructor( dependencies: IAsyncActiveSessionDependencies, @@ -35,7 +36,6 @@ export async function lambdaHandlerConstructor( return serverErrorResponse; } const config = configResult.value; - const eventService = dependencies.eventService(config.TXMA_SQS); const authorizationHeaderResult = new RequestService().getAuthorizationHeader( event.headers["Authorization"] ?? event.headers["authorization"], @@ -103,12 +103,16 @@ export async function lambdaHandlerConstructor( govukSigninJourneyId: session.govukSigninJourneyId, }); - return await handleOkResponse(eventService, { - session, - auditData: getAuditData(event), - sub, - issuer: config.ISSUER, - }); + return await handleOkResponse( + config.TXMA_SQS, + dependencies.sendMessageToSqs, + { + session, + auditData: getAuditData(event), + sub, + issuer: config.ISSUER, + }, + ); } export const lambdaHandler = lambdaHandlerConstructor.bind(null, dependencies); @@ -161,29 +165,25 @@ interface HandleOkResponseData { } async function handleOkResponse( - eventService: IEventService, + sqsArn: string, + sendMessageToSqs: ISendMessageToSqs, { session, auditData, sub, issuer }: HandleOkResponseData, ) { - const { ipAddress, txmaAuditEncoded } = auditData; const { govukSigninJourneyId, redirectUri, sessionId, state } = session; - const eventName = "DCMAW_ASYNC_CRI_APP_START"; - const writeEventResult = await eventService.writeGenericEvent({ - eventName, - sub, + const appStartEvent = getAppStartEvent({ sessionId, govukSigninJourneyId, - getNowInMilliseconds: Date.now, - componentId: issuer, - ipAddress, - txmaAuditEncoded, - redirect_uri: redirectUri, - suspected_fraud_signal: undefined, + redirectUri, + userId: sub, + issuer, + ...auditData, }); - if (writeEventResult.isError) { + const sendMessageToSqsResult = await sendMessageToSqs(sqsArn, appStartEvent); + if (sendMessageToSqsResult.isError) { logger.error(LogMessage.ERROR_WRITING_AUDIT_EVENT, { data: { - auditEventName: eventName, + auditEventName: appStartEvent.event_name, }, }); diff --git a/backend-api/src/functions/asyncActiveSession/getAppStartEvent.ts b/backend-api/src/functions/asyncActiveSession/getAppStartEvent.ts new file mode 100644 index 000000000..806f3669c --- /dev/null +++ b/backend-api/src/functions/asyncActiveSession/getAppStartEvent.ts @@ -0,0 +1,31 @@ +import { AppStartEvent } from "../common/audit/types"; + +export const getAppStartEvent = (inputs: { + sessionId: string; + userId: string; + issuer: string; + govukSigninJourneyId: string; + ipAddress: string; + redirectUri?: string; + txmaAuditEncoded?: string; +}): AppStartEvent => { + const timestampInMillis = Date.now(); + return { + event_name: "DCMAW_ASYNC_CRI_APP_START", + user: { + user_id: inputs.userId, + session_id: inputs.sessionId, + ip_address: inputs.ipAddress, + govuk_signin_journey_id: inputs.govukSigninJourneyId, + }, + event_timestamp_ms: timestampInMillis, + timestamp: Math.floor(timestampInMillis / 1000), + component_id: inputs.issuer, + ...(inputs.redirectUri && { + extensions: { redirect_uri: inputs.redirectUri }, + }), + ...(inputs.txmaAuditEncoded && { + restricted: { device_information: { encoded: inputs.txmaAuditEncoded } }, + }), + }; +}; diff --git a/backend-api/src/functions/asyncActiveSession/handlerDependencies.ts b/backend-api/src/functions/asyncActiveSession/handlerDependencies.ts index 7a33d1b0e..d9aaacdf7 100644 --- a/backend-api/src/functions/asyncActiveSession/handlerDependencies.ts +++ b/backend-api/src/functions/asyncActiveSession/handlerDependencies.ts @@ -5,15 +5,15 @@ import { import { TokenService } from "./TokenService/TokenService"; import { IDecryptJwe, JweDecrypter } from "./jwe/jweDecrypter"; import { ITokenService } from "./TokenService/types"; -import { IEventService } from "../services/events/types"; -import { EventService } from "../services/events/eventService"; +import { ISendMessageToSqs } from "../adapters/aws/sqs/types"; +import { sendMessageToSqs } from "../adapters/aws/sqs/sendMessageToSqs"; export interface IAsyncActiveSessionDependencies { env: NodeJS.ProcessEnv; jweDecrypter: (encryptionKeyId: string) => IDecryptJwe; tokenService: () => ITokenService; sessionService: (tableName: string) => ISessionService; - eventService: (sqsQueue: string) => IEventService; + sendMessageToSqs: ISendMessageToSqs; } export const dependencies: IAsyncActiveSessionDependencies = { @@ -21,5 +21,5 @@ export const dependencies: IAsyncActiveSessionDependencies = { jweDecrypter: (encryptionKeyId: string) => new JweDecrypter(encryptionKeyId), tokenService: () => new TokenService(), sessionService: (tableName: string) => new SessionService(tableName), - eventService: (sqsQueue) => new EventService(sqsQueue), + sendMessageToSqs, }; diff --git a/backend-api/src/functions/asyncCredential/asyncCredentialHandler.test.ts b/backend-api/src/functions/asyncCredential/asyncCredentialHandler.test.ts index 991f801e6..08e299be8 100644 --- a/backend-api/src/functions/asyncCredential/asyncCredentialHandler.test.ts +++ b/backend-api/src/functions/asyncCredential/asyncCredentialHandler.test.ts @@ -1,15 +1,12 @@ import { expect } from "@jest/globals"; import "../../../tests/testUtils/matchers"; import { APIGatewayProxyResult, Context } from "aws-lambda"; -import { - MockEventServiceFailToWrite, - MockEventWriterSuccess, -} from "../services/events/tests/mocks"; import { MockJWTBuilder } from "../testUtils/mockJwtBuilder"; import { buildRequest } from "../testUtils/mockRequest"; import { Result, successResult } from "../utils/result"; import { lambdaHandlerConstructor } from "./asyncCredentialHandler"; import { IAsyncCredentialDependencies } from "./handlerDependencies"; +import "aws-sdk-client-mock-jest"; import { IDecodedToken, IDecodeToken, @@ -34,7 +31,12 @@ import { buildLambdaContext } from "../testUtils/mockContext"; import { logger } from "../common/logging/logger"; import { mockGovukSigninJourneyId, + mockIssuer, + mockSendMessageToSqsFailure, + mockSendMessageToSqsSuccess, mockSessionId, + NOW_IN_MILLISECONDS, + NOW_IN_SECONDS, } from "../testUtils/unitTestData"; const env = { @@ -56,7 +58,7 @@ describe("Async Credential", () => { consoleInfoSpy = jest.spyOn(console, "info"); consoleErrorSpy = jest.spyOn(console, "error"); dependencies = { - eventService: () => new MockEventWriterSuccess(), + sendMessageToSqs: mockSendMessageToSqsSuccess, tokenService: () => new MockTokenServiceSuccess(), clientRegistryService: () => new MockClientRegistryServiceGetPartialClientSuccessResult(), @@ -64,6 +66,13 @@ describe("Async Credential", () => { env, }; context = buildLambdaContext(); + + jest.useFakeTimers(); + jest.setSystemTime(NOW_IN_MILLISECONDS); + }); + + afterEach(() => { + jest.useRealTimers(); }); describe("On every invocation", () => { @@ -873,11 +882,12 @@ describe("Async Credential", () => { govuk_signin_journey_id: "mockGovukSigninJourneyId", }), }); - dependencies.eventService = () => - new MockEventServiceFailToWrite("DCMAW_ASYNC_CRI_START"); + dependencies.sessionService = () => new MockSessionServiceCreateSuccessResult(); + dependencies.sendMessageToSqs = mockSendMessageToSqsFailure; + const result = await lambdaHandlerConstructor( dependencies, event, @@ -920,8 +930,7 @@ describe("Async Credential", () => { redirect_uri: "https://www.mockUrl.com", }), }); - const mockEventService = new MockEventWriterSuccess(); - dependencies.eventService = () => mockEventService; + dependencies.sessionService = () => new MockSessionServiceCreateSuccessResult(); @@ -931,20 +940,23 @@ describe("Async Credential", () => { context, ); - expect(mockEventService.eventConfig).toEqual( - expect.objectContaining({ - eventName: "DCMAW_ASYNC_CRI_START", - componentId: "mockIssuer", - getNowInMilliseconds: Date.now, - govukSigninJourneyId: "mockGovukSigninJourneyId", - sub: "mockSub", - sessionId: mockSessionId, - ipAddress: undefined, - redirect_uri: "https://www.mockUrl.com", - suspected_fraud_signal: undefined, - txmaAuditEncoded: undefined, - }), - ); + expect( + mockSendMessageToSqsSuccess, + ).toHaveBeenCalledNthWithSqsMessage(1, { + sqsArn: "mockSqsQueue", + expectedMessage: { + user: { + user_id: "mockSub", + session_id: mockSessionId, + govuk_signin_journey_id: mockGovukSigninJourneyId, + }, + timestamp: NOW_IN_SECONDS, + event_timestamp_ms: NOW_IN_MILLISECONDS, + event_name: "DCMAW_ASYNC_CRI_START", + component_id: mockIssuer, + extensions: { redirect_uri: "https://www.mockUrl.com" }, + }, + }); expect(consoleInfoSpy).toHaveBeenCalledWithLogFields({ messageCode: "MOBILE_ASYNC_CREDENTIAL_SESSION_CREATED", diff --git a/backend-api/src/functions/asyncCredential/asyncCredentialHandler.ts b/backend-api/src/functions/asyncCredential/asyncCredentialHandler.ts index 9868ca83a..3f7b54d5f 100644 --- a/backend-api/src/functions/asyncCredential/asyncCredentialHandler.ts +++ b/backend-api/src/functions/asyncCredential/asyncCredentialHandler.ts @@ -16,6 +16,7 @@ import { setupLogger } from "../common/logging/setupLogger"; import { getHeader } from "../common/request/getHeader/getHeader"; import { getCredentialConfig } from "./credentialConfig"; import { appendPersistentIdentifiersToLogger } from "../common/logging/helpers/appendPersistentIdentifiersToLogger"; +import { getCriStartEvent } from "./getCriStartEvent"; export async function lambdaHandlerConstructor( dependencies: IAsyncCredentialDependencies, @@ -173,25 +174,22 @@ export async function lambdaHandlerConstructor( } const sessionId = createSessionResult.value; - // Write audit event - const eventService = dependencies.eventService(config.TXMA_SQS); - const writeEventResult = await eventService.writeGenericEvent({ - eventName: "DCMAW_ASYNC_CRI_START", - sub: requestBody.sub, + const criStartEvent = getCriStartEvent({ sessionId, + userId: requestBody.sub, + issuer: configResult.value.ISSUER, govukSigninJourneyId: requestBody.govuk_signin_journey_id, - getNowInMilliseconds: Date.now, - componentId: config.ISSUER, - redirect_uri: requestBody.redirect_uri, - suspected_fraud_signal: undefined, - // ipAddress and txmaAuditEncoded values only required for lambdas that are triggered as a result of a direct user interaction to the ID Check service - ipAddress: undefined, - txmaAuditEncoded: undefined, + redirectUri: requestBody.redirect_uri, }); - if (writeEventResult.isError) { + const sendMessageToSqsResult = await dependencies.sendMessageToSqs( + config.TXMA_SQS, + criStartEvent, + ); + + if (sendMessageToSqsResult.isError) { logger.error(LogMessage.ERROR_WRITING_AUDIT_EVENT, { - data: { auditEventName: "DCMAW_ASYNC_CRI_START" }, + data: { auditEventName: criStartEvent.event_name }, }); return serverErrorResponse; } diff --git a/backend-api/src/functions/asyncCredential/getCriStartEvent.ts b/backend-api/src/functions/asyncCredential/getCriStartEvent.ts new file mode 100644 index 000000000..65df7dd40 --- /dev/null +++ b/backend-api/src/functions/asyncCredential/getCriStartEvent.ts @@ -0,0 +1,25 @@ +import { StartEvent } from "../common/audit/types"; + +export const getCriStartEvent = (inputs: { + sessionId: string; + userId: string; + issuer: string; + govukSigninJourneyId: string; + redirectUri?: string; +}): StartEvent => { + const timeInMillis = Date.now(); + return { + event_name: "DCMAW_ASYNC_CRI_START", + component_id: inputs.issuer, + user: { + user_id: inputs.userId, + govuk_signin_journey_id: inputs.govukSigninJourneyId, + session_id: inputs.sessionId, + }, + timestamp: Math.floor(timeInMillis / 1000), + event_timestamp_ms: timeInMillis, + ...(inputs.redirectUri && { + extensions: { redirect_uri: inputs.redirectUri }, + }), + }; +}; diff --git a/backend-api/src/functions/asyncCredential/handlerDependencies.ts b/backend-api/src/functions/asyncCredential/handlerDependencies.ts index 8d2ff0709..291edff1f 100644 --- a/backend-api/src/functions/asyncCredential/handlerDependencies.ts +++ b/backend-api/src/functions/asyncCredential/handlerDependencies.ts @@ -2,7 +2,6 @@ import { ClientRegistryService, IGetPartialRegisteredClientByClientId, } from "../services/clientRegistryService/clientRegistryService"; -import { EventService } from "../services/events/eventService"; import { IDecodeToken, IVerifyTokenSignature, @@ -12,23 +11,24 @@ import { ISessionService, SessionService, } from "../services/session/sessionService"; -import { IEventService } from "../services/events/types"; +import { ISendMessageToSqs } from "../adapters/aws/sqs/types"; +import { sendMessageToSqs } from "../adapters/aws/sqs/sendMessageToSqs"; export interface IAsyncCredentialDependencies { - eventService: (sqsQueue: string) => IEventService; tokenService: () => IDecodeToken & IVerifyTokenSignature; clientRegistryService: ( clientRegistryParameterName: string, ) => IGetPartialRegisteredClientByClientId; sessionService: (tableName: string) => ISessionService; env: NodeJS.ProcessEnv; + sendMessageToSqs: ISendMessageToSqs; } export const dependencies: IAsyncCredentialDependencies = { env: process.env, - eventService: (sqsQueue: string) => new EventService(sqsQueue), clientRegistryService: (clientRegistryParameterName: string) => new ClientRegistryService(clientRegistryParameterName), tokenService: () => new TokenService(), sessionService: (tableName: string) => new SessionService(tableName), + sendMessageToSqs, }; diff --git a/backend-api/src/functions/asyncToken/asyncTokenHandler.test.ts b/backend-api/src/functions/asyncToken/asyncTokenHandler.test.ts index 50f28636f..a27c99d32 100644 --- a/backend-api/src/functions/asyncToken/asyncTokenHandler.test.ts +++ b/backend-api/src/functions/asyncToken/asyncTokenHandler.test.ts @@ -1,10 +1,6 @@ import { expect } from "@jest/globals"; import "../../../tests/testUtils/matchers"; import { APIGatewayProxyEvent, Context } from "aws-lambda"; -import { - MockEventServiceFailToWrite, - MockEventWriterSuccess, -} from "../services/events/tests/mocks"; import { buildLambdaContext } from "../testUtils/mockContext"; import { buildRequest, @@ -23,6 +19,12 @@ import { } from "./tokenService/tests/mocks"; import { RequestService } from "./requestService/requestService"; import { logger } from "../common/logging/logger"; +import { + mockSendMessageToSqsFailure, + mockSendMessageToSqsSuccess, + NOW_IN_MILLISECONDS, + NOW_IN_SECONDS, +} from "../testUtils/unitTestData"; describe("Async Token", () => { let request: APIGatewayProxyEvent; @@ -52,11 +54,17 @@ describe("Async Token", () => { }); dependencies = { env, - eventService: () => new MockEventWriterSuccess(), clientRegistryService: () => new MockClientRegistryServiceSuccessResult(), tokenService: () => new MockTokenServiceSuccessResult(), requestService: () => new RequestService(), + sendMessageToSqs: mockSendMessageToSqsSuccess, }; + jest.useFakeTimers(); + jest.setSystemTime(NOW_IN_MILLISECONDS); + }); + + afterEach(() => { + jest.useRealTimers(); }); describe("On every invocation", () => { @@ -427,11 +435,7 @@ describe("Async Token", () => { describe("Given the request is valid", () => { describe("Given there is an error writing the audit event", () => { it("Logs and returns a 500 server error", async () => { - const mockFailingEventService = new MockEventServiceFailToWrite( - "DCMAW_ASYNC_CLIENT_CREDENTIALS_TOKEN_ISSUED", - ); - dependencies.eventService = () => mockFailingEventService; - + dependencies.sendMessageToSqs = mockSendMessageToSqsFailure; const result = await lambdaHandlerConstructor( dependencies, request, @@ -454,22 +458,29 @@ describe("Async Token", () => { describe("Given the event is written successfully", () => { it("Logs and returns with 200 response with an access token in the response body", async () => { - const mockEventWriter = new MockEventWriterSuccess(); - dependencies.eventService = () => mockEventWriter; const result = await lambdaHandlerConstructor( dependencies, request, buildLambdaContext(), ); + expect(mockSendMessageToSqsSuccess).toHaveBeenCalledNthWithSqsMessage( + 1, + { + sqsArn: "mockSQSQueue", + expectedMessage: { + event_name: "DCMAW_ASYNC_CLIENT_CREDENTIALS_TOKEN_ISSUED", + component_id: "mockIssuer", + timestamp: NOW_IN_SECONDS, + event_timestamp_ms: NOW_IN_MILLISECONDS, + }, + }, + ); + expect(consoleInfoSpy).toHaveBeenCalledWithLogFields({ messageCode: "MOBILE_ASYNC_TOKEN_COMPLETED", }); - expect(mockEventWriter.auditEvents[0]).toBe( - "DCMAW_ASYNC_CLIENT_CREDENTIALS_TOKEN_ISSUED", - ); - expect(result.statusCode); expect(result.body).toEqual( JSON.stringify({ diff --git a/backend-api/src/functions/asyncToken/asyncTokenHandler.ts b/backend-api/src/functions/asyncToken/asyncTokenHandler.ts index 347f28323..526365960 100644 --- a/backend-api/src/functions/asyncToken/asyncTokenHandler.ts +++ b/backend-api/src/functions/asyncToken/asyncTokenHandler.ts @@ -13,6 +13,7 @@ import { LogMessage } from "../common/logging/LogMessage"; import { setupLogger } from "../common/logging/setupLogger"; import { getHeader } from "../common/request/getHeader/getHeader"; import { ErrorCategory } from "../utils/result"; +import { getClientCredentialsTokenIssuedTxmaEvent } from "./getClientCredentialsTokenIssuedEvent"; export async function lambdaHandlerConstructor( dependencies: IAsyncTokenRequestDependencies, @@ -96,13 +97,12 @@ export async function lambdaHandlerConstructor( } const accessToken = mintTokenResult.value; - const eventWriter = dependencies.eventService(config.TXMA_SQS); - const writeEventResult = await eventWriter.writeCredentialTokenIssuedEvent({ - componentId: config.ISSUER, - getNowInMilliseconds: Date.now, - eventName: "DCMAW_ASYNC_CLIENT_CREDENTIALS_TOKEN_ISSUED", - }); - if (writeEventResult.isError) { + const sendMessageToSqsResult = await dependencies.sendMessageToSqs( + config.TXMA_SQS, + getClientCredentialsTokenIssuedTxmaEvent(config.ISSUER), + ); + + if (sendMessageToSqsResult.isError) { logger.error(LogMessage.ERROR_WRITING_AUDIT_EVENT, { errorMessage: "Unexpected error writing the DCMAW_ASYNC_CLIENT_CREDENTIALS_TOKEN_ISSUED event", diff --git a/backend-api/src/functions/asyncToken/getClientCredentialsTokenIssuedEvent.ts b/backend-api/src/functions/asyncToken/getClientCredentialsTokenIssuedEvent.ts new file mode 100644 index 000000000..42f689c4f --- /dev/null +++ b/backend-api/src/functions/asyncToken/getClientCredentialsTokenIssuedEvent.ts @@ -0,0 +1,13 @@ +import { ClientCredentialsTokenIssuedEvent } from "../common/audit/types"; + +export const getClientCredentialsTokenIssuedTxmaEvent = ( + componentId: string, +): ClientCredentialsTokenIssuedEvent => { + const timeInMillis = Date.now(); + return { + event_name: "DCMAW_ASYNC_CLIENT_CREDENTIALS_TOKEN_ISSUED", + component_id: componentId, + event_timestamp_ms: timeInMillis, + timestamp: Math.floor(timeInMillis / 1000), + }; +}; diff --git a/backend-api/src/functions/asyncToken/handlerDependencies.ts b/backend-api/src/functions/asyncToken/handlerDependencies.ts index 62194cb3a..c0bb46f9f 100644 --- a/backend-api/src/functions/asyncToken/handlerDependencies.ts +++ b/backend-api/src/functions/asyncToken/handlerDependencies.ts @@ -2,29 +2,29 @@ import { IGetRegisteredIssuerUsingClientSecrets, ClientRegistryService, } from "../services/clientRegistryService/clientRegistryService"; -import { EventService } from "../services/events/eventService"; import { IMintToken, TokenService } from "./tokenService/tokenService"; import { IRequestService, RequestService, } from "./requestService/requestService"; -import { IEventService } from "../services/events/types"; +import { ISendMessageToSqs } from "../adapters/aws/sqs/types"; +import { sendMessageToSqs } from "../adapters/aws/sqs/sendMessageToSqs"; export interface IAsyncTokenRequestDependencies { env: NodeJS.ProcessEnv; - eventService: (sqsQueue: string) => IEventService; clientRegistryService: ( clientRegistryParameterName: string, ) => IGetRegisteredIssuerUsingClientSecrets; tokenService: (signingKey: string) => IMintToken; requestService: () => IRequestService; + sendMessageToSqs: ISendMessageToSqs; } export const dependencies: IAsyncTokenRequestDependencies = { env: process.env, - eventService: (sqsQueue: string) => new EventService(sqsQueue), clientRegistryService: (clientRegistryParameterName: string) => new ClientRegistryService(clientRegistryParameterName), tokenService: (signingKey: string) => new TokenService(signingKey), requestService: () => new RequestService(), + sendMessageToSqs: sendMessageToSqs, }; diff --git a/backend-api/src/functions/common/audit/types.ts b/backend-api/src/functions/common/audit/types.ts new file mode 100644 index 000000000..0e76f6f19 --- /dev/null +++ b/backend-api/src/functions/common/audit/types.ts @@ -0,0 +1,46 @@ +type AsyncPrefix = "DCMAW_ASYNC"; +type EventNameShortHand = + | "CLIENT_CREDENTIALS_TOKEN_ISSUED" + | "CRI_START" + | "CRI_APP_START"; + +export type EventNames = `${AsyncPrefix}_${EventNameShortHand}`; + +interface BaseEvent { + timestamp: number; + event_timestamp_ms: number; + event_name: T; + component_id: string; +} + +interface User { + user_id: string; + session_id: string; + govuk_signin_journey_id: string; + ip_address?: string; +} + +type UserWithIpAddress = Required; + +interface DeviceInformation { + device_information: { + encoded: string; + }; +} + +interface RedirectUri { + redirect_uri: string; +} + +export type ClientCredentialsTokenIssuedEvent = + BaseEvent<"DCMAW_ASYNC_CLIENT_CREDENTIALS_TOKEN_ISSUED">; + +export type StartEvent = BaseEvent<"DCMAW_ASYNC_CRI_START"> & { user: User } & { + extensions?: RedirectUri; +}; + +export type AppStartEvent = BaseEvent<"DCMAW_ASYNC_CRI_APP_START"> & { + user: UserWithIpAddress; +} & { + extensions?: RedirectUri; +} & { restricted?: DeviceInformation }; diff --git a/backend-api/src/functions/services/events/eventService.ts b/backend-api/src/functions/services/events/eventService.ts index e38ecacf1..6689da7f7 100644 --- a/backend-api/src/functions/services/events/eventService.ts +++ b/backend-api/src/functions/services/events/eventService.ts @@ -4,8 +4,6 @@ import { sqsClient } from "./sqsClient"; import { BiometricTokenIssuedEvent, BiometricTokenIssuedEventConfig, - CredentialTokenIssuedEvent, - CredentialTokenIssuedEventConfig, GenericEventConfig, GenericTxmaEvent, IEventService, @@ -27,13 +25,6 @@ export class EventService implements IEventService { return await this.writeToSqs(txmaEvent); } - async writeCredentialTokenIssuedEvent( - eventConfig: CredentialTokenIssuedEventConfig, - ): Promise> { - const txmaEvent = this.buildCredentialTokenIssuedEvent(eventConfig); - return await this.writeToSqs(txmaEvent); - } - async writeBiometricTokenIssuedEvent( eventConfig: BiometricTokenIssuedEventConfig, ): Promise> { @@ -97,18 +88,6 @@ export class EventService implements IEventService { }; } - private buildCredentialTokenIssuedEvent = ( - eventConfig: CredentialTokenIssuedEventConfig, - ): CredentialTokenIssuedEvent => { - const timestampInMillis = eventConfig.getNowInMilliseconds(); - return { - event_name: eventConfig.eventName, - component_id: eventConfig.componentId, - timestamp: Math.floor(timestampInMillis / 1000), - event_timestamp_ms: timestampInMillis, - }; - }; - private readonly buildBiometricTokenEvent = ( eventConfig: BiometricTokenIssuedEventConfig, ): BiometricTokenIssuedEvent => { diff --git a/backend-api/src/functions/services/events/tests/eventService.test.ts b/backend-api/src/functions/services/events/tests/eventService.test.ts index 334773739..d9fd3e139 100644 --- a/backend-api/src/functions/services/events/tests/eventService.test.ts +++ b/backend-api/src/functions/services/events/tests/eventService.test.ts @@ -25,12 +25,10 @@ describe("Event Service", () => { }); describe.each([ - "DCMAW_ASYNC_CRI_START", "DCMAW_ASYNC_CRI_4XXERROR", "DCMAW_ASYNC_CRI_5XXERROR", "DCMAW_ASYNC_CRI_ERROR", "DCMAW_ASYNC_CRI_END", - "DCMAW_ASYNC_CRI_APP_START", "DCMAW_ASYNC_APP_END", "DCMAW_ASYNC_ABORT_APP", "DCMAW_ASYNC_HYBRID_BILLING_STARTED", @@ -188,74 +186,6 @@ describe("Event Service", () => { }); }); - describe("Writing credential token issued event to SQS", () => { - describe("Given writing DCMAW_ASYNC_CLIENT_CREDENTIALS_TOKEN_ISSUED event to SQS fails", () => { - beforeEach(async () => { - sqsMock.on(SendMessageCommand).rejects("Failed to write to SQS"); - - result = await eventWriter.writeCredentialTokenIssuedEvent({ - getNowInMilliseconds: () => 1609462861000, - componentId: "mockComponentId", - eventName: "DCMAW_ASYNC_CLIENT_CREDENTIALS_TOKEN_ISSUED", - }); - }); - - it("Attempts to send DCMAW_ASYNC_CLIENT_CREDENTIALS_TOKEN_ISSUED event to SQS", () => { - const expectedCommandInput = { - MessageBody: JSON.stringify({ - event_name: "DCMAW_ASYNC_CLIENT_CREDENTIALS_TOKEN_ISSUED", - component_id: "mockComponentId", - timestamp: 1609462861, - event_timestamp_ms: 1609462861000, - }), - QueueUrl: "mockSqsQueue", - }; - - expect(sqsMock).toHaveReceivedCommandWith( - SendMessageCommand, - expectedCommandInput, - ); - }); - - it("Returns an emptyFailure", () => { - expect(result).toEqual(emptyFailure()); - }); - }); - - describe("Given writing to SQS is successful", () => { - beforeEach(async () => { - sqsMock.on(SendMessageCommand).resolves({}); - - result = await eventWriter.writeCredentialTokenIssuedEvent({ - getNowInMilliseconds: () => 1609462861000, - componentId: "mockComponentId", - eventName: "DCMAW_ASYNC_CLIENT_CREDENTIALS_TOKEN_ISSUED", - }); - }); - - it("Attempts to send DCMAW_ASYNC_CLIENT_CREDENTIALS_TOKEN_ISSUED event to SQS", () => { - const expectedCommandInput = { - MessageBody: JSON.stringify({ - event_name: "DCMAW_ASYNC_CLIENT_CREDENTIALS_TOKEN_ISSUED", - component_id: "mockComponentId", - timestamp: 1609462861, - event_timestamp_ms: 1609462861000, - }), - QueueUrl: "mockSqsQueue", - }; - - expect(sqsMock).toHaveReceivedCommandWith( - SendMessageCommand, - expectedCommandInput, - ); - }); - - it("Returns an emptySuccess", () => { - expect(result).toEqual(emptySuccess()); - }); - }); - }); - describe("Writing biometric token issued event to SQS", () => { describe(`Given writing "DCMAW_ASYNC_BIOMETRIC_TOKEN_ISSUED" event to SQS fails`, () => { beforeEach(async () => { diff --git a/backend-api/src/functions/services/events/types.ts b/backend-api/src/functions/services/events/types.ts index 03a45fe46..a618481c4 100644 --- a/backend-api/src/functions/services/events/types.ts +++ b/backend-api/src/functions/services/events/types.ts @@ -45,12 +45,10 @@ export interface BaseUserTxmaEvent extends BaseTxmaEvent { } export type GenericEventNames = - | "DCMAW_ASYNC_CRI_START" | "DCMAW_ASYNC_CRI_4XXERROR" | "DCMAW_ASYNC_CRI_5XXERROR" | "DCMAW_ASYNC_CRI_ERROR" | "DCMAW_ASYNC_CRI_END" - | "DCMAW_ASYNC_CRI_APP_START" | "DCMAW_ASYNC_APP_END" | "DCMAW_ASYNC_ABORT_APP"; @@ -65,7 +63,6 @@ export type TxmaBillingEventName = (typeof txmaBillingEventNames)[number]; export type EventNames = | GenericEventNames | TxmaBillingEventName - | "DCMAW_ASYNC_CLIENT_CREDENTIALS_TOKEN_ISSUED" | "DCMAW_ASYNC_BIOMETRIC_TOKEN_ISSUED"; export interface GenericEventConfig extends BaseUserEventConfig { @@ -73,9 +70,6 @@ export interface GenericEventConfig extends BaseUserEventConfig { redirect_uri: string | undefined; suspected_fraud_signal: string | undefined; } -export interface CredentialTokenIssuedEventConfig extends BaseEventConfig { - eventName: "DCMAW_ASYNC_CLIENT_CREDENTIALS_TOKEN_ISSUED"; -} export interface BiometricTokenIssuedEventConfig extends BaseUserEventConfig { documentType: DocumentType; @@ -88,10 +82,6 @@ export interface GenericTxmaEvent extends BaseUserTxmaEvent { extensions: Extensions | undefined; } -export interface CredentialTokenIssuedEvent extends BaseTxmaEvent { - event_name: "DCMAW_ASYNC_CLIENT_CREDENTIALS_TOKEN_ISSUED"; -} - export interface BiometricTokenIssuedEvent extends BaseUserTxmaEvent { event_name: "DCMAW_ASYNC_BIOMETRIC_TOKEN_ISSUED"; extensions: { @@ -101,15 +91,9 @@ export interface BiometricTokenIssuedEvent extends BaseUserTxmaEvent { }; } -export type TxmaEvents = - | GenericTxmaEvent - | CredentialTokenIssuedEvent - | BiometricTokenIssuedEvent; +export type TxmaEvents = GenericTxmaEvent | BiometricTokenIssuedEvent; export interface IEventService { - writeCredentialTokenIssuedEvent: ( - eventConfig: CredentialTokenIssuedEventConfig, - ) => Promise>; writeGenericEvent: ( eventConfig: GenericEventConfig, ) => Promise>; diff --git a/backend-api/src/functions/testUtils/unitTestData.ts b/backend-api/src/functions/testUtils/unitTestData.ts index 8cd58ae38..db75ebdfe 100644 --- a/backend-api/src/functions/testUtils/unitTestData.ts +++ b/backend-api/src/functions/testUtils/unitTestData.ts @@ -254,9 +254,6 @@ export const mockInertEventService: IEventService = { writeGenericEvent: jest.fn(() => { throw new Error("Not implemented"); }), - writeCredentialTokenIssuedEvent: jest.fn(() => { - throw new Error("Not implemented"); - }), writeBiometricTokenIssuedEvent: jest.fn(() => { throw new Error("Not implemented"); }), diff --git a/backend-api/src/functions/tests/pact/dependencies/asyncCredentialDependencies.ts b/backend-api/src/functions/tests/pact/dependencies/asyncCredentialDependencies.ts index a6c7a8c09..464ece180 100644 --- a/backend-api/src/functions/tests/pact/dependencies/asyncCredentialDependencies.ts +++ b/backend-api/src/functions/tests/pact/dependencies/asyncCredentialDependencies.ts @@ -1,12 +1,13 @@ +import { IAsyncCredentialDependencies } from "../../../asyncCredential/handlerDependencies"; import { MockTokenServiceGetDecodedTokenErrorResult, MockTokenServiceSuccessIPV, } from "../../../asyncCredential/tokenService/tests/mocks"; import { MockClientRegistryServiceGetPartialClientSuccessResultIPV } from "../../../services/clientRegistryService/tests/mocks"; -import { MockEventWriterSuccess } from "../../../services/events/tests/mocks"; import { MockSessionServiceCreateSuccessResult } from "../../../services/session/tests/mocks"; +import { mockSendMessageToSqsSuccess } from "../../../testUtils/unitTestData"; -const defaultPassingDependencies = { +const defaultPassingDependencies: IAsyncCredentialDependencies = { env: { SIGNING_KEY_ID: "mockKid", ISSUER: "mockIssuer", @@ -16,11 +17,11 @@ const defaultPassingDependencies = { TXMA_SQS: "mockSqsQueue", CLIENT_REGISTRY_SECRET_NAME: "mockParmaterName", }, - eventService: () => new MockEventWriterSuccess(), clientRegistryService: () => new MockClientRegistryServiceGetPartialClientSuccessResultIPV(), tokenService: () => new MockTokenServiceSuccessIPV(), sessionService: () => new MockSessionServiceCreateSuccessResult(), + sendMessageToSqs: mockSendMessageToSqsSuccess, }; export class AsyncCredentialDependencies { diff --git a/backend-api/src/functions/tests/pact/dependencies/asyncTokenDependencies.ts b/backend-api/src/functions/tests/pact/dependencies/asyncTokenDependencies.ts index b4ee890e6..d26dc2654 100644 --- a/backend-api/src/functions/tests/pact/dependencies/asyncTokenDependencies.ts +++ b/backend-api/src/functions/tests/pact/dependencies/asyncTokenDependencies.ts @@ -1,22 +1,23 @@ -import { MockEventWriterSuccess } from "../../../services/events/tests/mocks"; import { MockTokenServiceSuccessResult } from "../../../asyncToken/tokenService/tests/mocks"; import { MockClientRegistryServiceSuccessResult, MockClientRegistryServiceBadRequestResult, } from "../../../services/clientRegistryService/tests/mocks"; import { MockRequestServiceSuccessResult } from "../../../asyncToken/requestService/tests/mocks"; +import { IAsyncTokenRequestDependencies } from "../../../asyncToken/handlerDependencies"; +import { mockSendMessageToSqsSuccess } from "../../../testUtils/unitTestData"; -const defaultPassingDependencies = { +const defaultPassingDependencies: IAsyncTokenRequestDependencies = { env: { SIGNING_KEY_ID: "mockSigningKeyId", ISSUER: "mockIssuer", TXMA_SQS: "mockSQSQueue", CLIENT_REGISTRY_SECRET_NAME: "mockRegistryParameterName", }, - eventService: () => new MockEventWriterSuccess(), clientRegistryService: () => new MockClientRegistryServiceSuccessResult(), tokenService: () => new MockTokenServiceSuccessResult(), requestService: () => new MockRequestServiceSuccessResult(), + sendMessageToSqs: mockSendMessageToSqsSuccess, }; export class AsyncTokenDependencies {