From 971d51e038be100abd743347d152eadfa3989aa7 Mon Sep 17 00:00:00 2001 From: "mike.collingwood" Date: Tue, 29 Oct 2024 11:34:11 +0000 Subject: [PATCH] PYIC-6243: Add CheckMobileAppVcReceiptHandler --- deploy/template.yaml | 87 +++++++++ .../check-mobile-app-vc-receipt/build.gradle | 49 +++++ .../CheckMobileAppVcReceiptHandler.java | 147 ++++++++++++++ .../dto/CheckMobileAppVcReceiptRequest.java | 21 ++ ...eckMobileAppVcReceiptRequestException.java | 14 ++ .../main/resources/IpvLambdaJsonLayout.json | 88 +++++++++ .../src/main/resources/log4j2.json | 22 +++ .../CheckMobileAppVcReceiptHandlerTest.java | 179 ++++++++++++++++++ .../helpers/ApiGatewayResponseGenerator.java | 8 + .../core/library/fixtures/TestFixtures.java | 23 --- .../library/service/CriResponseService.java | 1 + openAPI/core-back-internal.yaml | 18 ++ settings.gradle | 2 + 13 files changed, 636 insertions(+), 23 deletions(-) create mode 100644 lambdas/check-mobile-app-vc-receipt/build.gradle create mode 100644 lambdas/check-mobile-app-vc-receipt/src/main/java/uk/gov/di/ipv/core/checkmobileappvcreceipt/CheckMobileAppVcReceiptHandler.java create mode 100644 lambdas/check-mobile-app-vc-receipt/src/main/java/uk/gov/di/ipv/core/checkmobileappvcreceipt/dto/CheckMobileAppVcReceiptRequest.java create mode 100644 lambdas/check-mobile-app-vc-receipt/src/main/java/uk/gov/di/ipv/core/checkmobileappvcreceipt/exception/InvalidCheckMobileAppVcReceiptRequestException.java create mode 100644 lambdas/check-mobile-app-vc-receipt/src/main/resources/IpvLambdaJsonLayout.json create mode 100644 lambdas/check-mobile-app-vc-receipt/src/main/resources/log4j2.json create mode 100644 lambdas/check-mobile-app-vc-receipt/src/test/java/uk/gov/di/ipv/core/processmobileappcallback/CheckMobileAppVcReceiptHandlerTest.java diff --git a/deploy/template.yaml b/deploy/template.yaml index 1acea16b40..64b75621ab 100644 --- a/deploy/template.yaml +++ b/deploy/template.yaml @@ -1172,6 +1172,93 @@ Resources: FilterPattern: "" LogGroupName: !Ref ProcessMobileAppCallbackLogGroup + CheckMobileAppVcReceiptFunction: + Type: AWS::Serverless::Function + DependsOn: + - "CheckMobileAppVcReceiptLogGroup" + Properties: + # checkov:skip=CKV_AWS_115: We do not have enough data to allocate the concurrent execution allowance per function. + # checkov:skip=CKV_AWS_116: Lambdas invoked via API Gateway do not support Dead Letter Queues. + # checkov:skip=CKV_AWS_117: Lambdas will migrate to our own VPC in future work. + FunctionName: !Sub "check-mobile-app-vc-receipt-${Environment}" + Handler: uk.gov.di.ipv.core.checkmobileappvcreceipt.CheckMobileAppVcReceiptHandler::handleRequest + Runtime: java17 + PackageType: Zip + CodeUri: ../lambdas/check-mobile-app-vc-receipt + MemorySize: 2048 + Tracing: Active + Environment: + # checkov:skip=CKV_AWS_173: These environment variables do not require encryption. + Variables: + ENVIRONMENT: !Sub "${Environment}" + POWERTOOLS_SERVICE_NAME: !Sub check-mobile-app-vc-receipt-${Environment} + IPV_SESSIONS_TABLE_NAME: !Ref SessionsTable + CRI_RESPONSE_TABLE_NAME: !Ref CRIResponseTable + CLIENT_OAUTH_SESSIONS_TABLE_NAME: !Ref ClientOAuthSessionsTable + VpcConfig: + SubnetIds: + - Fn::ImportValue: !Sub ${VpcStackName}-ProtectedSubnetIdA + - Fn::ImportValue: !Sub ${VpcStackName}-ProtectedSubnetIdB + SecurityGroupIds: + - !GetAtt LambdaSecurityGroup.GroupId + Policies: + - VPCAccessPolicy: { } + - KMSDecryptPolicy: + KeyId: !Ref DynamoDBKmsKey + - DynamoDBReadPolicy: + TableName: !Ref SessionsTable + - DynamoDBReadPolicy: + TableName: !Ref ClientOAuthSessionsTable + - DynamoDBReadPolicy: + TableName: !Ref CRIResponseTable + - SSMParameterReadPolicy: + ParameterName: !Sub ${Environment}/core/* + - Statement: + - Sid: EnforceStayinSpecificVpc + Effect: Allow + Action: + - 'lambda:CreateFunction' + - 'lambda:UpdateFunctionConfiguration' + Resource: + - "*" + Condition: + StringEquals: + "lambda:VpcIds": + - Fn::ImportValue: !Sub ${VpcStackName}-VpcId + Events: + IPVCoreCheckMobileAppVcReceipt: + Type: Api + Properties: + RestApiId: !Ref IPVCorePrivateAPI + Path: /app/check-vc-receipt + Method: GET + AutoPublishAlias: live + + CheckMobileAppVcReceiptFunctionTestingApiPermission: + Type: AWS::Lambda::Permission + Condition: IsTestApiEnv + Properties: + Action: "lambda:InvokeFunction" + FunctionName: !Ref CheckMobileAppVcReceiptFunction.Alias + Principal: "apigateway.amazonaws.com" + SourceArn: !Sub "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${IPVCoreInternalTestingApi}/*/GET/app/check-vc-receipt" + + CheckMobileAppVcReceiptLogGroup: + Type: AWS::Logs::LogGroup + # checkov:skip=CKV_AWS_158: No need for customer managed keys for short lived logs + Properties: + RetentionInDays: 30 + LogGroupName: !Sub "/aws/lambda/check-mobile-app-vc-receipt-${Environment}" + + CheckMobileAppVcReceiptLogGroupSubscriptionFilter: + Type: AWS::Logs::SubscriptionFilter + Condition: IsSubscriptionEnviroment + Properties: + DestinationArn: "arn:aws:logs:eu-west-2:885513274347:destination:csls_cw_logs_destination_prodpython" + FilterPattern: "" + LogGroupName: !Ref CheckMobileAppVcReceiptLogGroup + + BuildUserIdentityFunction: Type: AWS::Serverless::Function DependsOn: diff --git a/lambdas/check-mobile-app-vc-receipt/build.gradle b/lambdas/check-mobile-app-vc-receipt/build.gradle new file mode 100644 index 0000000000..2431dda654 --- /dev/null +++ b/lambdas/check-mobile-app-vc-receipt/build.gradle @@ -0,0 +1,49 @@ +plugins { + id "java" + id "idea" + id "jacoco" + alias libs.plugins.postCompileWeaving +} + +dependencies { + + implementation libs.bundles.awsLambda, + project(":libs:common-services"), + project(":libs:cri-response-service"), + project(':libs:journey-uris'), + project(":libs:verifiable-credentials") + + aspect libs.powertoolsLogging, + libs.powertoolsTracing, + libs.aspectj + + compileOnly libs.lombok + annotationProcessor libs.lombok + + testImplementation libs.junitJupiter, + libs.mockitoJunit, + libs.wiremock, + project(path: ':libs:common-services', configuration: 'tests') + + testRuntimeOnly libs.junitPlatform +} + +java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} + +test { + // Configures environment variable to avoid initialization of AWS X-Ray segments for each tests + environment "LAMBDA_TASK_ROOT", "handler" + useJUnitPlatform () + finalizedBy jacocoTestReport + exclude 'uk/gov/di/ipv/core/checkmobileappvcreceipt/pact/**' +} + +jacocoTestReport { + dependsOn test + reports { + xml.required.set(true) + } +} diff --git a/lambdas/check-mobile-app-vc-receipt/src/main/java/uk/gov/di/ipv/core/checkmobileappvcreceipt/CheckMobileAppVcReceiptHandler.java b/lambdas/check-mobile-app-vc-receipt/src/main/java/uk/gov/di/ipv/core/checkmobileappvcreceipt/CheckMobileAppVcReceiptHandler.java new file mode 100644 index 0000000000..9250338856 --- /dev/null +++ b/lambdas/check-mobile-app-vc-receipt/src/main/java/uk/gov/di/ipv/core/checkmobileappvcreceipt/CheckMobileAppVcReceiptHandler.java @@ -0,0 +1,147 @@ +package uk.gov.di.ipv.core.checkmobileappvcreceipt; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; +import com.nimbusds.oauth2.sdk.util.StringUtils; +import org.apache.http.HttpStatus; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import software.amazon.lambda.powertools.logging.Logging; +import software.amazon.lambda.powertools.tracing.Tracing; +import uk.gov.di.ipv.core.checkmobileappvcreceipt.dto.CheckMobileAppVcReceiptRequest; +import uk.gov.di.ipv.core.checkmobileappvcreceipt.exception.InvalidCheckMobileAppVcReceiptRequestException; +import uk.gov.di.ipv.core.library.annotations.ExcludeFromGeneratedCoverageReport; +import uk.gov.di.ipv.core.library.domain.Cri; +import uk.gov.di.ipv.core.library.domain.ErrorResponse; +import uk.gov.di.ipv.core.library.exceptions.ClientOauthSessionNotFoundException; +import uk.gov.di.ipv.core.library.exceptions.CredentialParseException; +import uk.gov.di.ipv.core.library.exceptions.IpvSessionNotFoundException; +import uk.gov.di.ipv.core.library.helpers.ApiGatewayResponseGenerator; +import uk.gov.di.ipv.core.library.helpers.LogHelper; +import uk.gov.di.ipv.core.library.helpers.RequestHelper; +import uk.gov.di.ipv.core.library.service.ClientOAuthSessionDetailsService; +import uk.gov.di.ipv.core.library.service.ConfigService; +import uk.gov.di.ipv.core.library.service.CriResponseService; +import uk.gov.di.ipv.core.library.service.IpvSessionService; +import uk.gov.di.ipv.core.library.service.exception.InvalidCriResponseException; +import uk.gov.di.ipv.core.library.verifiablecredential.service.VerifiableCredentialService; + +import static uk.gov.di.ipv.core.library.helpers.LogHelper.buildErrorMessage; + +public class CheckMobileAppVcReceiptHandler + implements RequestHandler { + private static final Logger LOGGER = LogManager.getLogger(); + private final ConfigService configService; + private final IpvSessionService ipvSessionService; + private final ClientOAuthSessionDetailsService clientOAuthSessionDetailsService; + private final CriResponseService criResponseService; + private final VerifiableCredentialService verifiableCredentialService; + + public CheckMobileAppVcReceiptHandler( + ConfigService configService, + IpvSessionService ipvSessionService, + ClientOAuthSessionDetailsService clientOAuthSessionDetailsService, + CriResponseService criResponseService, + VerifiableCredentialService verifiableCredentialService) { + this.configService = configService; + this.ipvSessionService = ipvSessionService; + this.clientOAuthSessionDetailsService = clientOAuthSessionDetailsService; + this.criResponseService = criResponseService; + this.verifiableCredentialService = verifiableCredentialService; + } + + @ExcludeFromGeneratedCoverageReport + public CheckMobileAppVcReceiptHandler() { + configService = ConfigService.create(); + ipvSessionService = new IpvSessionService(configService); + clientOAuthSessionDetailsService = new ClientOAuthSessionDetailsService(configService); + criResponseService = new CriResponseService(configService); + this.verifiableCredentialService = new VerifiableCredentialService(configService); + } + + @Override + @Tracing + @Logging(clearState = true) + public APIGatewayProxyResponseEvent handleRequest( + APIGatewayProxyRequestEvent input, Context context) { + try { + var callbackRequest = parseCallbackRequest(input); + + var status = getStatus(callbackRequest); + + return ApiGatewayResponseGenerator.proxyResponse(status); + } catch (InvalidCheckMobileAppVcReceiptRequestException e) { + LOGGER.info(buildErrorMessage(e.getErrorResponse())); + return ApiGatewayResponseGenerator.proxyResponse(HttpStatus.SC_BAD_REQUEST); + } catch (IpvSessionNotFoundException e) { + LOGGER.info(buildErrorMessage(ErrorResponse.IPV_SESSION_NOT_FOUND)); + return ApiGatewayResponseGenerator.proxyResponse(HttpStatus.SC_BAD_REQUEST); + } catch (ClientOauthSessionNotFoundException e) { + LOGGER.info(buildErrorMessage(e.getErrorResponse())); + return ApiGatewayResponseGenerator.proxyResponse(HttpStatus.SC_BAD_REQUEST); + } catch (InvalidCriResponseException e) { + LOGGER.info(buildErrorMessage(e.getErrorResponse())); + return ApiGatewayResponseGenerator.proxyResponse(HttpStatus.SC_INTERNAL_SERVER_ERROR); + } catch (CredentialParseException e) { + LOGGER.info(buildErrorMessage(ErrorResponse.FAILED_TO_PARSE_ISSUED_CREDENTIALS)); + return ApiGatewayResponseGenerator.proxyResponse(HttpStatus.SC_INTERNAL_SERVER_ERROR); + } + } + + private CheckMobileAppVcReceiptRequest parseCallbackRequest(APIGatewayProxyRequestEvent input) { + return new CheckMobileAppVcReceiptRequest( + input.getHeaders().get("ipv-session-id"), + input.getHeaders().get("ip-address"), + input.getHeaders().get("txma-audit-encoded"), + RequestHelper.getFeatureSet(input.getHeaders())); + } + + private int getStatus(CheckMobileAppVcReceiptRequest callbackRequest) + throws InvalidCheckMobileAppVcReceiptRequestException, IpvSessionNotFoundException, + ClientOauthSessionNotFoundException, InvalidCriResponseException, + CredentialParseException { + // Validate callback sessions + validateSessionId(callbackRequest); + + // Get/ set session items/ config + var ipvSessionItem = ipvSessionService.getIpvSession(callbackRequest.getIpvSessionId()); + var clientOAuthSessionItem = + clientOAuthSessionDetailsService.getClientOAuthSession( + ipvSessionItem.getClientOAuthSessionId()); + var userId = clientOAuthSessionItem.getUserId(); + configService.setFeatureSet(callbackRequest.getFeatureSet()); + + // Attach variables to logs + LogHelper.attachGovukSigninJourneyIdToLogs( + clientOAuthSessionItem.getGovukSigninJourneyId()); + LogHelper.attachIpvSessionIdToLogs(callbackRequest.getIpvSessionId()); + LogHelper.attachFeatureSetToLogs(callbackRequest.getFeatureSet()); + LogHelper.attachComponentId(configService); + + // Retrieve and check cri response + var criResponse = criResponseService.getCriResponseItem(userId, Cri.DCMAW_ASYNC); + + if (criResponse == null) { + return HttpStatus.SC_INTERNAL_SERVER_ERROR; + } + + if (!CriResponseService.STATUS_PENDING.equals(criResponse.getStatus()) + || verifiableCredentialService.getVc(userId, Cri.DCMAW_ASYNC.toString()) != null) { + return HttpStatus.SC_OK; + } + + return HttpStatus.SC_NOT_FOUND; + } + + private void validateSessionId(CheckMobileAppVcReceiptRequest callbackRequest) + throws InvalidCheckMobileAppVcReceiptRequestException { + var ipvSessionId = callbackRequest.getIpvSessionId(); + + if (StringUtils.isBlank(ipvSessionId)) { + throw new InvalidCheckMobileAppVcReceiptRequestException( + ErrorResponse.MISSING_IPV_SESSION_ID); + } + } +} diff --git a/lambdas/check-mobile-app-vc-receipt/src/main/java/uk/gov/di/ipv/core/checkmobileappvcreceipt/dto/CheckMobileAppVcReceiptRequest.java b/lambdas/check-mobile-app-vc-receipt/src/main/java/uk/gov/di/ipv/core/checkmobileappvcreceipt/dto/CheckMobileAppVcReceiptRequest.java new file mode 100644 index 0000000000..e9020322e2 --- /dev/null +++ b/lambdas/check-mobile-app-vc-receipt/src/main/java/uk/gov/di/ipv/core/checkmobileappvcreceipt/dto/CheckMobileAppVcReceiptRequest.java @@ -0,0 +1,21 @@ +package uk.gov.di.ipv.core.checkmobileappvcreceipt.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import uk.gov.di.ipv.core.library.annotations.ExcludeFromGeneratedCoverageReport; + +import java.util.List; + +@ExcludeFromGeneratedCoverageReport +@NoArgsConstructor +@AllArgsConstructor +@Data +@Builder +public class CheckMobileAppVcReceiptRequest { + private String ipvSessionId; + private String ipAddress; + private String deviceInformation; + private List featureSet; +} diff --git a/lambdas/check-mobile-app-vc-receipt/src/main/java/uk/gov/di/ipv/core/checkmobileappvcreceipt/exception/InvalidCheckMobileAppVcReceiptRequestException.java b/lambdas/check-mobile-app-vc-receipt/src/main/java/uk/gov/di/ipv/core/checkmobileappvcreceipt/exception/InvalidCheckMobileAppVcReceiptRequestException.java new file mode 100644 index 0000000000..b2bd702d85 --- /dev/null +++ b/lambdas/check-mobile-app-vc-receipt/src/main/java/uk/gov/di/ipv/core/checkmobileappvcreceipt/exception/InvalidCheckMobileAppVcReceiptRequestException.java @@ -0,0 +1,14 @@ +package uk.gov.di.ipv.core.checkmobileappvcreceipt.exception; + +import lombok.Getter; +import uk.gov.di.ipv.core.library.domain.ErrorResponse; + +@Getter +public class InvalidCheckMobileAppVcReceiptRequestException extends Exception { + private final ErrorResponse errorResponse; + + public InvalidCheckMobileAppVcReceiptRequestException(ErrorResponse errorResponse) { + super(errorResponse.getMessage()); + this.errorResponse = errorResponse; + } +} diff --git a/lambdas/check-mobile-app-vc-receipt/src/main/resources/IpvLambdaJsonLayout.json b/lambdas/check-mobile-app-vc-receipt/src/main/resources/IpvLambdaJsonLayout.json new file mode 100644 index 0000000000..fb70c97dfe --- /dev/null +++ b/lambdas/check-mobile-app-vc-receipt/src/main/resources/IpvLambdaJsonLayout.json @@ -0,0 +1,88 @@ +{ + "timestamp": { + "$resolver": "timestamp" + }, + "instant": { + "epochSecond": { + "$resolver": "timestamp", + "epoch": { + "unit": "secs", + "rounded": true + } + }, + "nanoOfSecond": { + "$resolver": "timestamp", + "epoch": { + "unit": "secs.nanos" + } + } + }, + "thread": { + "$resolver": "thread", + "field": "name" + }, + "level": { + "$resolver": "level", + "field": "name" + }, + "loggerName": { + "$resolver": "logger", + "field": "name" + }, + "message": { + "$resolver": "message" + }, + "thrown": { + "message": { + "$resolver": "exception", + "field": "message" + }, + "name": { + "$resolver": "exception", + "field": "className" + }, + "extendedStackTrace": { + "$resolver": "exception", + "field": "stackTrace" + } + }, + "contextStack": { + "$resolver": "ndc" + }, + "endOfBatch": { + "$resolver": "endOfBatch" + }, + "loggerFqcn": { + "$resolver": "logger", + "field": "fqcn" + }, + "threadId": { + "$resolver": "thread", + "field": "id" + }, + "threadPriority": { + "$resolver": "thread", + "field": "priority" + }, + "source": { + "class": { + "$resolver": "source", + "field": "className" + }, + "method": { + "$resolver": "source", + "field": "methodName" + }, + "file": { + "$resolver": "source", + "field": "fileName" + }, + "line": { + "$resolver": "source", + "field": "lineNumber" + } + }, + "": { + "$resolver": "powertools" + } +} diff --git a/lambdas/check-mobile-app-vc-receipt/src/main/resources/log4j2.json b/lambdas/check-mobile-app-vc-receipt/src/main/resources/log4j2.json new file mode 100644 index 0000000000..7055612fe6 --- /dev/null +++ b/lambdas/check-mobile-app-vc-receipt/src/main/resources/log4j2.json @@ -0,0 +1,22 @@ +{ + "Configuration": { + "status": "warn", + "appenders": { + "Console": { + "name": "JsonAppender", + "target": "SYSTEM_OUT", + "JsonTemplateLayout": { + "eventTemplateUri": "classpath:IpvLambdaJsonLayout.json" + } + } + }, + "Loggers": { + "Root": { + "level": "info", + "AppenderRef": { + "ref": "JsonAppender" + } + } + } + } +} diff --git a/lambdas/check-mobile-app-vc-receipt/src/test/java/uk/gov/di/ipv/core/processmobileappcallback/CheckMobileAppVcReceiptHandlerTest.java b/lambdas/check-mobile-app-vc-receipt/src/test/java/uk/gov/di/ipv/core/processmobileappcallback/CheckMobileAppVcReceiptHandlerTest.java new file mode 100644 index 0000000000..6209d3426f --- /dev/null +++ b/lambdas/check-mobile-app-vc-receipt/src/test/java/uk/gov/di/ipv/core/processmobileappcallback/CheckMobileAppVcReceiptHandlerTest.java @@ -0,0 +1,179 @@ +package uk.gov.di.ipv.core.processmobileappcallback; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.SignedJWT; +import org.apache.http.HttpStatus; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import uk.gov.di.ipv.core.checkmobileappvcreceipt.CheckMobileAppVcReceiptHandler; +import uk.gov.di.ipv.core.library.domain.Cri; +import uk.gov.di.ipv.core.library.domain.VerifiableCredential; +import uk.gov.di.ipv.core.library.exceptions.IpvSessionNotFoundException; +import uk.gov.di.ipv.core.library.persistence.item.ClientOAuthSessionItem; +import uk.gov.di.ipv.core.library.persistence.item.CriResponseItem; +import uk.gov.di.ipv.core.library.persistence.item.IpvSessionItem; +import uk.gov.di.ipv.core.library.service.ClientOAuthSessionDetailsService; +import uk.gov.di.ipv.core.library.service.ConfigService; +import uk.gov.di.ipv.core.library.service.CriResponseService; +import uk.gov.di.ipv.core.library.service.IpvSessionService; +import uk.gov.di.ipv.core.library.verifiablecredential.service.VerifiableCredentialService; + +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class CheckMobileAppVcReceiptHandlerTest { + private static final String TEST_IPV_SESSION_ID = "test_ipv_session_id"; + private static final String TEST_CLIENT_OAUTH_SESSION_ID = "test_client_oauth_id"; + private static final String TEST_USER_ID = "test_user_id"; + @Mock private Context mockContext; + @Mock private SignedJWT mockSignedJwt; + @Mock private ConfigService configService; + @Mock private IpvSessionService ipvSessionService; + @Mock private ClientOAuthSessionDetailsService clientOAuthSessionDetailsService; + @Mock private CriResponseService criResponseService; + @Mock private VerifiableCredentialService verifiableCredentialService; + @InjectMocks private CheckMobileAppVcReceiptHandler checkMobileAppVcReceiptHandler; + + @Test + void shouldReturnErrorWhenCallbackRequestMissingIpvSessionId() { + // Arrange + var requestEvent = buildValidRequestEventWithState(); + requestEvent.setHeaders(Map.of()); + + // Act + var response = checkMobileAppVcReceiptHandler.handleRequest(requestEvent, mockContext); + + // Assert + assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); + } + + @Test + void shouldReturnErrorWhenIpvSessionNotFound() throws Exception { + // Arrange + var requestEvent = buildValidRequestEventWithState(); + when(ipvSessionService.getIpvSession(TEST_IPV_SESSION_ID)) + .thenThrow(new IpvSessionNotFoundException("")); + + // Act + var response = checkMobileAppVcReceiptHandler.handleRequest(requestEvent, mockContext); + + // Assert + assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); + } + + @Test + void shouldReturn500WhenCriResponseNotFound() throws Exception { + // Arrange + var requestEvent = buildValidRequestEventWithState(); + when(ipvSessionService.getIpvSession(TEST_IPV_SESSION_ID)) + .thenReturn(buildValidIpvSessionItem()); + when(clientOAuthSessionDetailsService.getClientOAuthSession(TEST_CLIENT_OAUTH_SESSION_ID)) + .thenReturn(buildValidClientOAuthSessionItem()); + when(criResponseService.getCriResponseItem(TEST_USER_ID, Cri.DCMAW_ASYNC)).thenReturn(null); + + // Act + var response = checkMobileAppVcReceiptHandler.handleRequest(requestEvent, mockContext); + + // Assert + assertEquals(HttpStatus.SC_INTERNAL_SERVER_ERROR, response.getStatusCode()); + } + + @Test + void shouldReturn200WhenStatusNotPending() throws Exception { + // Arrange + var requestEvent = buildValidRequestEventWithState(); + when(ipvSessionService.getIpvSession(TEST_IPV_SESSION_ID)) + .thenReturn(buildValidIpvSessionItem()); + when(clientOAuthSessionDetailsService.getClientOAuthSession(TEST_CLIENT_OAUTH_SESSION_ID)) + .thenReturn(buildValidClientOAuthSessionItem()); + when(criResponseService.getCriResponseItem(TEST_USER_ID, Cri.DCMAW_ASYNC)) + .thenReturn(buildValidCriResponseItem(CriResponseService.STATUS_RECEIVED)); + + // Act + var response = checkMobileAppVcReceiptHandler.handleRequest(requestEvent, mockContext); + + // Assert + assertEquals(HttpStatus.SC_OK, response.getStatusCode()); + } + + @Test + void shouldReturn200WhenCriResponseStatusPendingButVcExists() throws Exception { + // Arrange + var requestEvent = buildValidRequestEventWithState(); + when(ipvSessionService.getIpvSession(TEST_IPV_SESSION_ID)) + .thenReturn(buildValidIpvSessionItem()); + when(clientOAuthSessionDetailsService.getClientOAuthSession(TEST_CLIENT_OAUTH_SESSION_ID)) + .thenReturn(buildValidClientOAuthSessionItem()); + var criResponseItem = buildValidCriResponseItem(CriResponseService.STATUS_PENDING); + when(criResponseService.getCriResponseItem(TEST_USER_ID, Cri.DCMAW_ASYNC)) + .thenReturn(criResponseItem); + when(mockSignedJwt.getJWTClaimsSet()) + .thenReturn( + JWTClaimsSet.parse( + Map.of( + "vc", + Map.of("type", List.of("IdentityAssertionCredential"))))); + var vc = VerifiableCredential.fromValidJwt(TEST_USER_ID, Cri.DCMAW_ASYNC, mockSignedJwt); + when(verifiableCredentialService.getVc(TEST_USER_ID, Cri.DCMAW_ASYNC.toString())) + .thenReturn(vc); + + // Act + var response = checkMobileAppVcReceiptHandler.handleRequest(requestEvent, mockContext); + + // Assert + assertEquals(HttpStatus.SC_OK, response.getStatusCode()); + } + + @Test + void shouldReturn404WhenCriResponseStatusPendingAndVcNotFound() throws Exception { + // Arrange + var requestEvent = buildValidRequestEventWithState(); + when(ipvSessionService.getIpvSession(TEST_IPV_SESSION_ID)) + .thenReturn(buildValidIpvSessionItem()); + when(clientOAuthSessionDetailsService.getClientOAuthSession(TEST_CLIENT_OAUTH_SESSION_ID)) + .thenReturn(buildValidClientOAuthSessionItem()); + var criResponseItem = buildValidCriResponseItem(CriResponseService.STATUS_PENDING); + when(criResponseService.getCriResponseItem(TEST_USER_ID, Cri.DCMAW_ASYNC)) + .thenReturn(criResponseItem); + + // Act + var response = checkMobileAppVcReceiptHandler.handleRequest(requestEvent, mockContext); + + // Assert + assertEquals(HttpStatus.SC_NOT_FOUND, response.getStatusCode()); + } + + private APIGatewayProxyRequestEvent buildValidRequestEventWithState() { + var event = new APIGatewayProxyRequestEvent(); + event.setHeaders(Map.of("ipv-session-id", TEST_IPV_SESSION_ID)); + return event; + } + + private IpvSessionItem buildValidIpvSessionItem() { + var ipvSessionItem = new IpvSessionItem(); + ipvSessionItem.setIpvSessionId(TEST_IPV_SESSION_ID); + ipvSessionItem.setClientOAuthSessionId(TEST_CLIENT_OAUTH_SESSION_ID); + return ipvSessionItem; + } + + private ClientOAuthSessionItem buildValidClientOAuthSessionItem() { + return ClientOAuthSessionItem.builder().userId(TEST_USER_ID).build(); + } + + private CriResponseItem buildValidCriResponseItem(String status) { + return CriResponseItem.builder() + .userId(TEST_USER_ID) + .credentialIssuer(Cri.DCMAW_ASYNC.toString()) + .status(status) + .build(); + } +} diff --git a/libs/common-services/src/main/java/uk/gov/di/ipv/core/library/helpers/ApiGatewayResponseGenerator.java b/libs/common-services/src/main/java/uk/gov/di/ipv/core/library/helpers/ApiGatewayResponseGenerator.java index bff930910d..b6558c3d5e 100644 --- a/libs/common-services/src/main/java/uk/gov/di/ipv/core/library/helpers/ApiGatewayResponseGenerator.java +++ b/libs/common-services/src/main/java/uk/gov/di/ipv/core/library/helpers/ApiGatewayResponseGenerator.java @@ -33,6 +33,14 @@ public static APIGatewayProxyResponseEvent proxyJsonResponse(int statusCode, } } + public static APIGatewayProxyResponseEvent proxyResponse(int statusCode) { + APIGatewayProxyResponseEvent apiGatewayProxyResponseEvent = + new APIGatewayProxyResponseEvent(); + apiGatewayProxyResponseEvent.setStatusCode(statusCode); + + return apiGatewayProxyResponseEvent; + } + public static APIGatewayProxyResponseEvent proxyResponse( int statusCode, String body, Map headers) { APIGatewayProxyResponseEvent apiGatewayProxyResponseEvent = diff --git a/libs/common-services/src/test/java/uk/gov/di/ipv/core/library/fixtures/TestFixtures.java b/libs/common-services/src/test/java/uk/gov/di/ipv/core/library/fixtures/TestFixtures.java index 385cbd2fc1..f03918a0ee 100644 --- a/libs/common-services/src/test/java/uk/gov/di/ipv/core/library/fixtures/TestFixtures.java +++ b/libs/common-services/src/test/java/uk/gov/di/ipv/core/library/fixtures/TestFixtures.java @@ -9,12 +9,8 @@ import com.nimbusds.jose.crypto.RSAEncrypter; import com.nimbusds.jwt.SignedJWT; import uk.gov.di.ipv.core.library.domain.ErrorResponse; -import uk.gov.di.ipv.core.library.domain.VerifiableCredential; -import uk.gov.di.ipv.core.library.exceptions.CredentialParseException; import uk.gov.di.ipv.core.library.exceptions.HttpResponseExceptionWithErrorBody; -import uk.gov.di.ipv.core.library.persistence.item.VcStoreItem; -import java.time.Instant; import java.util.List; import java.util.Map; @@ -185,23 +181,4 @@ static JWEObject createJweObject(RSAEncrypter rsaEncrypter, SignedJWT signedJWT) throw new HttpResponseExceptionWithErrorBody(500, ErrorResponse.FAILED_TO_ENCRYPT_JWT); } } - - static VerifiableCredential createVerifiableCredential( - String userId, String credentialIssuer, String credential) - throws CredentialParseException { - return VerifiableCredential.fromVcStoreItem( - createVcStoreItem(userId, credentialIssuer, credential)); - } - - static VcStoreItem createVcStoreItem( - String userId, String credentialIssuer, String credential) { - VcStoreItem vcStoreItem = new VcStoreItem(); - vcStoreItem.setUserId(userId); - vcStoreItem.setCredentialIssuer(credentialIssuer); - vcStoreItem.setCredential(credential); - Instant dateCreated = Instant.now(); - vcStoreItem.setDateCreated(dateCreated); - vcStoreItem.setExpirationTime(dateCreated.plusSeconds(1000L)); - return vcStoreItem; - } } diff --git a/libs/cri-response-service/src/main/java/uk/gov/di/ipv/core/library/service/CriResponseService.java b/libs/cri-response-service/src/main/java/uk/gov/di/ipv/core/library/service/CriResponseService.java index e3794060cf..7022473f1b 100644 --- a/libs/cri-response-service/src/main/java/uk/gov/di/ipv/core/library/service/CriResponseService.java +++ b/libs/cri-response-service/src/main/java/uk/gov/di/ipv/core/library/service/CriResponseService.java @@ -15,6 +15,7 @@ public class CriResponseService { + public static final String STATUS_RECEIVED = "received"; public static final String STATUS_PENDING = "pending"; public static final String STATUS_ERROR = "error"; private final DataStore dataStore; diff --git a/openAPI/core-back-internal.yaml b/openAPI/core-back-internal.yaml index 77a753e251..a450a94ed3 100644 --- a/openAPI/core-back-internal.yaml +++ b/openAPI/core-back-internal.yaml @@ -60,6 +60,24 @@ paths: Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${ProcessMobileAppCallbackFunction.Arn}:live/invocations passthroughBehavior: "when_no_match" + /app/check-vc-receipt: + get: + description: | + Called each poll of the backend by the frontend when checking receipt of the verifiable credentials associated to a mobile app journey. This triggers CRI response and verifiable credential retrieval. + responses: + 100: + description: "Pending CRI response, wait then poll again" + 200: + description: "Verifiable credential response is received" + 500: + description: "Error finding CRI response" + x-amazon-apigateway-integration: + type: "aws_proxy" + httpMethod: "GET" + uri: + Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${CheckMobileAppVcReceiptFunction.Arn}:live/invocations + passthroughBehavior: "when_no_match" + /journey/{journeyStep+}: post: description: Called when the user selects a journey event. Triggers an express step function diff --git a/settings.gradle b/settings.gradle index 8338eaed78..af50eb1366 100644 --- a/settings.gradle +++ b/settings.gradle @@ -61,3 +61,5 @@ dependencyResolutionManagement { } } } +include 'lambdas:check-mobile-app-vc-receipt' +findProject(':lambdas:check-mobile-app-vc-receipt')?.name = 'check-mobile-app-vc-receipt'