diff --git a/lambdas/call-ticf-cri/build.gradle b/lambdas/call-ticf-cri/build.gradle index b049775e5a..90b3661905 100644 --- a/lambdas/call-ticf-cri/build.gradle +++ b/lambdas/call-ticf-cri/build.gradle @@ -12,12 +12,12 @@ dependencies { project(":libs:common-services"), project(":libs:cri-storing-service"), project(":libs:user-identity-service"), - project(":libs:verifiable-credentials") + project(":libs:verifiable-credentials"), + project(":libs:ticf-cri-service") testImplementation libs.hamcrest, libs.junitJupiter, libs.mockitoJunit, - libs.pactConsumerJunit, project(path: ':libs:common-services', configuration: 'tests'), project(path: ':libs:test-helpers') @@ -37,12 +37,6 @@ test { exclude 'uk/gov/di/ipv/core/callticfcri/pact/**' } -task pactConsumerTests (type: Test) { - useJUnitPlatform() - include 'uk/gov/di/ipv/core/callticfcri/pact/**' - systemProperties['pact.rootDir'] = "$rootDir/build/pacts" -} - jacocoTestReport { dependsOn test reports { diff --git a/lambdas/call-ticf-cri/src/main/java/uk/gov/di/ipv/core/callticfcri/CallTicfCriHandler.java b/lambdas/call-ticf-cri/src/main/java/uk/gov/di/ipv/core/callticfcri/CallTicfCriHandler.java index 969ae5a887..46bbe530a0 100644 --- a/lambdas/call-ticf-cri/src/main/java/uk/gov/di/ipv/core/callticfcri/CallTicfCriHandler.java +++ b/lambdas/call-ticf-cri/src/main/java/uk/gov/di/ipv/core/callticfcri/CallTicfCriHandler.java @@ -8,8 +8,6 @@ import software.amazon.lambda.powertools.logging.Logging; import software.amazon.lambda.powertools.metrics.Metrics; import software.amazon.lambda.powertools.tracing.Tracing; -import uk.gov.di.ipv.core.callticfcri.exception.TicfCriServiceException; -import uk.gov.di.ipv.core.callticfcri.service.TicfCriService; import uk.gov.di.ipv.core.library.annotations.ExcludeFromGeneratedCoverageReport; import uk.gov.di.ipv.core.library.cimit.exception.CiPostMitigationsException; import uk.gov.di.ipv.core.library.cimit.exception.CiPutException; @@ -33,6 +31,8 @@ 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.IpvSessionService; +import uk.gov.di.ipv.core.library.ticf.TicfCriService; +import uk.gov.di.ipv.core.library.ticf.exception.TicfCriServiceException; import uk.gov.di.ipv.core.library.verifiablecredential.service.SessionCredentialsService; import java.util.List; diff --git a/lambdas/call-ticf-cri/src/test/java/uk/gov/di/ipv/core/callticfcri/CallTicfCriHandlerTest.java b/lambdas/call-ticf-cri/src/test/java/uk/gov/di/ipv/core/callticfcri/CallTicfCriHandlerTest.java index 14b08a3d0c..0d07063804 100644 --- a/lambdas/call-ticf-cri/src/test/java/uk/gov/di/ipv/core/callticfcri/CallTicfCriHandlerTest.java +++ b/lambdas/call-ticf-cri/src/test/java/uk/gov/di/ipv/core/callticfcri/CallTicfCriHandlerTest.java @@ -13,8 +13,6 @@ import org.mockito.Mock; import org.mockito.Spy; import org.mockito.junit.jupiter.MockitoExtension; -import uk.gov.di.ipv.core.callticfcri.exception.TicfCriServiceException; -import uk.gov.di.ipv.core.callticfcri.service.TicfCriService; import uk.gov.di.ipv.core.library.cimit.exception.CiPostMitigationsException; import uk.gov.di.ipv.core.library.cimit.exception.CiPutException; import uk.gov.di.ipv.core.library.cimit.exception.CiRetrievalException; @@ -33,6 +31,8 @@ import uk.gov.di.ipv.core.library.service.ConfigService; import uk.gov.di.ipv.core.library.service.IpvSessionService; import uk.gov.di.ipv.core.library.testhelpers.unit.LogCollector; +import uk.gov.di.ipv.core.library.ticf.TicfCriService; +import uk.gov.di.ipv.core.library.ticf.exception.TicfCriServiceException; import java.util.List; import java.util.Map; diff --git a/lambdas/process-candidate-identity/build.gradle b/lambdas/process-candidate-identity/build.gradle new file mode 100644 index 0000000000..23ae75968e --- /dev/null +++ b/lambdas/process-candidate-identity/build.gradle @@ -0,0 +1,46 @@ +plugins { + id "java" + id "idea" + id "jacoco" + alias libs.plugins.postCompileWeaving +} + +dependencies { + implementation libs.bundles.awsLambda, + project(":libs:common-services"), + project(":libs:audit-service"), + project(":libs:gpg45-evaluator"), + project(":libs:verifiable-credentials"), + project(":libs:user-identity-service"), + project(":libs:cimit-service"), + project(":libs:cri-storing-service"), + project(":libs:evcs-service"), + project(":libs:ticf-cri-service") + + testImplementation libs.hamcrest, + libs.junitJupiter, + libs.mockitoJunit, + project(path: ':libs:common-services', configuration: 'tests'), + project(path: ':libs:test-helpers') + + 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 +} + +jacocoTestReport { + dependsOn test + reports { + xml.required.set(true) + } +} diff --git a/lambdas/process-candidate-identity/src/main/java/uk/gov/di/ipv/core/processcandidateidentity/ProcessCandidateIdentityHandler.java b/lambdas/process-candidate-identity/src/main/java/uk/gov/di/ipv/core/processcandidateidentity/ProcessCandidateIdentityHandler.java new file mode 100644 index 0000000000..c002dda9e1 --- /dev/null +++ b/lambdas/process-candidate-identity/src/main/java/uk/gov/di/ipv/core/processcandidateidentity/ProcessCandidateIdentityHandler.java @@ -0,0 +1,487 @@ +package uk.gov.di.ipv.core.processcandidateidentity; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +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.metrics.Metrics; +import software.amazon.lambda.powertools.tracing.Tracing; +import uk.gov.di.ipv.core.library.annotations.ExcludeFromGeneratedCoverageReport; +import uk.gov.di.ipv.core.library.auditing.AuditEvent; +import uk.gov.di.ipv.core.library.auditing.AuditEventTypes; +import uk.gov.di.ipv.core.library.auditing.AuditEventUser; +import uk.gov.di.ipv.core.library.auditing.extension.AuditExtensionGpg45ProfileMatched; +import uk.gov.di.ipv.core.library.auditing.restricted.AuditRestrictedDeviceInformation; +import uk.gov.di.ipv.core.library.cimit.exception.CiPostMitigationsException; +import uk.gov.di.ipv.core.library.cimit.exception.CiPutException; +import uk.gov.di.ipv.core.library.cimit.exception.CiRetrievalException; +import uk.gov.di.ipv.core.library.config.ConfigurationVariable; +import uk.gov.di.ipv.core.library.cristoringservice.CriStoringService; +import uk.gov.di.ipv.core.library.domain.Cri; +import uk.gov.di.ipv.core.library.domain.JourneyErrorResponse; +import uk.gov.di.ipv.core.library.domain.JourneyResponse; +import uk.gov.di.ipv.core.library.domain.ProcessRequest; +import uk.gov.di.ipv.core.library.domain.ProfileType; +import uk.gov.di.ipv.core.library.domain.VerifiableCredential; +import uk.gov.di.ipv.core.library.enums.CandidateIdentityType; +import uk.gov.di.ipv.core.library.enums.CoiCheckType; +import uk.gov.di.ipv.core.library.enums.Vot; +import uk.gov.di.ipv.core.library.exception.EvcsServiceException; +import uk.gov.di.ipv.core.library.exceptions.ConfigException; +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.exceptions.IpvSessionNotFoundException; +import uk.gov.di.ipv.core.library.exceptions.UnknownProcessIdentityTypeException; +import uk.gov.di.ipv.core.library.exceptions.UnrecognisedVotException; +import uk.gov.di.ipv.core.library.exceptions.VerifiableCredentialException; +import uk.gov.di.ipv.core.library.gpg45.Gpg45ProfileEvaluator; +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.journeys.JourneyUris; +import uk.gov.di.ipv.core.library.persistence.item.ClientOAuthSessionItem; +import uk.gov.di.ipv.core.library.persistence.item.IpvSessionItem; +import uk.gov.di.ipv.core.library.service.AuditService; +import uk.gov.di.ipv.core.library.service.CimitService; +import uk.gov.di.ipv.core.library.service.CimitUtilityService; +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.IpvSessionService; +import uk.gov.di.ipv.core.library.service.UserIdentityService; +import uk.gov.di.ipv.core.library.service.VotMatcher; +import uk.gov.di.ipv.core.library.service.VotMatchingResult; +import uk.gov.di.ipv.core.library.ticf.TicfCriService; +import uk.gov.di.ipv.core.library.ticf.exception.TicfCriServiceException; +import uk.gov.di.ipv.core.library.verifiablecredential.helpers.VcHelper; +import uk.gov.di.ipv.core.library.verifiablecredential.service.SessionCredentialsService; +import uk.gov.di.ipv.core.processcandidateidentity.service.CheckCoiService; +import uk.gov.di.ipv.core.processcandidateidentity.service.StoreIdentityService; +import uk.gov.di.model.ContraIndicator; + +import java.text.ParseException; +import java.util.EnumSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static java.lang.Boolean.TRUE; +import static org.apache.http.HttpStatus.SC_INTERNAL_SERVER_ERROR; +import static uk.gov.di.ipv.core.library.config.ConfigurationVariable.CREDENTIAL_ISSUER_ENABLED; +import static uk.gov.di.ipv.core.library.domain.Cri.TICF; +import static uk.gov.di.ipv.core.library.domain.ErrorResponse.ERROR_PROCESSING_TICF_CRI_RESPONSE; +import static uk.gov.di.ipv.core.library.domain.ErrorResponse.FAILED_TO_GET_STORED_CIS; +import static uk.gov.di.ipv.core.library.domain.ErrorResponse.FAILED_TO_PARSE_ISSUED_CREDENTIALS; +import static uk.gov.di.ipv.core.library.domain.ErrorResponse.IPV_SESSION_NOT_FOUND; +import static uk.gov.di.ipv.core.library.domain.ErrorResponse.UNEXPECTED_PROCESS_IDENTITY_TYPE; +import static uk.gov.di.ipv.core.library.enums.CandidateIdentityType.NEW; +import static uk.gov.di.ipv.core.library.enums.CandidateIdentityType.PENDING; +import static uk.gov.di.ipv.core.library.enums.CandidateIdentityType.REVERIFICATION; +import static uk.gov.di.ipv.core.library.enums.CandidateIdentityType.UPDATE; +import static uk.gov.di.ipv.core.library.journeys.JourneyUris.JOURNEY_COI_CHECK_FAILED_PATH; +import static uk.gov.di.ipv.core.library.journeys.JourneyUris.JOURNEY_ERROR_PATH; +import static uk.gov.di.ipv.core.library.journeys.JourneyUris.JOURNEY_GPG45_UNMET_PATH; +import static uk.gov.di.ipv.core.library.journeys.JourneyUris.JOURNEY_NEXT_PATH; + +public class ProcessCandidateIdentityHandler + implements RequestHandler> { + private static final Logger LOGGER = LogManager.getLogger(); + private static final JourneyResponse JOURNEY_NEXT = new JourneyResponse(JOURNEY_NEXT_PATH); + private static final JourneyResponse JOURNEY_GPG45_UNMET = + new JourneyResponse(JOURNEY_GPG45_UNMET_PATH); + private static final JourneyResponse JOURNEY_VCS_NOT_CORRELATED = + new JourneyResponse(JourneyUris.JOURNEY_VCS_NOT_CORRELATED); + private static final Map JOURNEY_COI_CHECK_FAILED = + new JourneyResponse(JOURNEY_COI_CHECK_FAILED_PATH).toObjectMap(); + + private final ConfigService configService; + private final ClientOAuthSessionDetailsService clientOAuthSessionDetailsService; + private final IpvSessionService ipvSessionService; + private final SessionCredentialsService sessionCredentialsService; + private final AuditService auditService; + private final CimitService cimitService; + private final CheckCoiService checkCoiService; + private final CriStoringService criStoringService; + private final UserIdentityService userIdentityService; + private final StoreIdentityService storeIdentityService; + private final VotMatcher votMatcher; + private final TicfCriService ticfCriService; + private final CimitUtilityService cimitUtilityService; + + // Candidate identities that should be subject to a COI check + private static final Set COI_CHECK_TYPES = + EnumSet.of(NEW, PENDING, REVERIFICATION, UPDATE); + + // Candidate identities that should store the given identity (if successful) + private static final Set STORE_IDENTITY_TYPES = + EnumSet.of(NEW, PENDING, UPDATE); + + // Candidate identities that should match a profile + private static final Set PROFILE_MATCHING_TYPES = + EnumSet.of(NEW, UPDATE); + + @ExcludeFromGeneratedCoverageReport + public ProcessCandidateIdentityHandler() { + this(ConfigService.create()); + } + + @ExcludeFromGeneratedCoverageReport + public ProcessCandidateIdentityHandler(ConfigService configService) { + this.auditService = AuditService.create(configService); + this.cimitService = new CimitService(configService); + this.configService = configService; + this.clientOAuthSessionDetailsService = new ClientOAuthSessionDetailsService(configService); + this.ipvSessionService = new IpvSessionService(configService); + this.sessionCredentialsService = new SessionCredentialsService(configService); + this.checkCoiService = new CheckCoiService(configService, auditService); + this.userIdentityService = new UserIdentityService(configService); + this.storeIdentityService = new StoreIdentityService(configService, auditService); + this.ticfCriService = new TicfCriService(configService); + this.cimitUtilityService = new CimitUtilityService(configService); + this.votMatcher = new VotMatcher(userIdentityService, new Gpg45ProfileEvaluator()); + this.criStoringService = + new CriStoringService( + configService, auditService, null, sessionCredentialsService, cimitService); + } + + @SuppressWarnings("java:S107") // Methods should not have too many parameters + @ExcludeFromGeneratedCoverageReport + public ProcessCandidateIdentityHandler( + ConfigService configService, + ClientOAuthSessionDetailsService clientOAuthSessionDetailsService, + IpvSessionService ipvSessionService, + CheckCoiService checkCoiService, + SessionCredentialsService sessionCredentialsService, + CriStoringService criStoringService, + AuditService auditService, + CimitService cimitService, + UserIdentityService userIdentityService, + StoreIdentityService storeIdentityService, + VotMatcher votMatcher, + CimitUtilityService cimitUtilityService, + TicfCriService ticfCriService) { + this.configService = configService; + this.clientOAuthSessionDetailsService = clientOAuthSessionDetailsService; + this.ipvSessionService = ipvSessionService; + this.sessionCredentialsService = sessionCredentialsService; + this.checkCoiService = checkCoiService; + this.cimitService = cimitService; + this.userIdentityService = userIdentityService; + this.auditService = auditService; + this.criStoringService = criStoringService; + this.votMatcher = votMatcher; + this.storeIdentityService = storeIdentityService; + this.ticfCriService = ticfCriService; + this.cimitUtilityService = cimitUtilityService; + } + + @Override + @Tracing + @Logging(clearState = true) + @Metrics(captureColdStart = true) + public Map handleRequest(ProcessRequest request, Context context) { + LogHelper.attachComponentId(configService); + configService.setFeatureSet(RequestHelper.getFeatureSet(request)); + + IpvSessionItem ipvSessionItem = null; + try { + var ipvSessionId = RequestHelper.getIpvSessionId(request); + var ipAddress = RequestHelper.getIpAddress(request); + var deviceInformation = request.getDeviceInformation(); + var processIdentityType = RequestHelper.getProcessIdentityType(request); + + ipvSessionItem = ipvSessionService.getIpvSession(ipvSessionId); + ClientOAuthSessionItem clientOAuthSessionItem = + clientOAuthSessionDetailsService.getClientOAuthSession( + ipvSessionItem.getClientOAuthSessionId()); + + String govukSigninJourneyId = clientOAuthSessionItem.getGovukSigninJourneyId(); + LogHelper.attachGovukSigninJourneyIdToLogs(govukSigninJourneyId); + + String userId = clientOAuthSessionItem.getUserId(); + var sessionVcs = + sessionCredentialsService.getCredentials( + ipvSessionItem.getIpvSessionId(), userId); + + var auditEventUser = + new AuditEventUser(userId, ipvSessionId, govukSigninJourneyId, ipAddress); + + return processCandidateThroughJourney( + processIdentityType, + ipvSessionItem, + clientOAuthSessionItem, + deviceInformation, + ipAddress, + sessionVcs, + auditEventUser); + + } catch (HttpResponseExceptionWithErrorBody e) { + LOGGER.error(LogHelper.buildErrorMessage("Failed to process identity", e)); + return new JourneyErrorResponse( + JOURNEY_ERROR_PATH, e.getResponseCode(), e.getErrorResponse()) + .toObjectMap(); + } catch (UnknownProcessIdentityTypeException e) { + LOGGER.error(LogHelper.buildErrorMessage("Unknown process identity type", e)); + return new JourneyErrorResponse( + JOURNEY_ERROR_PATH, + HttpStatus.SC_BAD_REQUEST, + UNEXPECTED_PROCESS_IDENTITY_TYPE) + .toObjectMap(); + } catch (IpvSessionNotFoundException e) { + LOGGER.error(LogHelper.buildErrorMessage("Failed to find ipv session", e)); + return new JourneyErrorResponse( + JOURNEY_ERROR_PATH, + HttpStatus.SC_INTERNAL_SERVER_ERROR, + IPV_SESSION_NOT_FOUND) + .toObjectMap(); + } catch (VerifiableCredentialException | EvcsServiceException e) { + LOGGER.error(LogHelper.buildErrorMessage("Failed to store identity", e)); + return new JourneyErrorResponse( + JOURNEY_ERROR_PATH, e.getResponseCode(), e.getErrorResponse()) + .toObjectMap(); + } catch (CredentialParseException e) { + LOGGER.error(LogHelper.buildErrorMessage("Unable to parse existing credentials", e)); + return new JourneyErrorResponse( + JOURNEY_ERROR_PATH, + SC_INTERNAL_SERVER_ERROR, + FAILED_TO_PARSE_ISSUED_CREDENTIALS) + .toObjectMap(); + } catch (CiRetrievalException e) { + LOGGER.error(LogHelper.buildErrorMessage(FAILED_TO_GET_STORED_CIS.getMessage(), e)); + return new JourneyErrorResponse( + JOURNEY_ERROR_PATH, + HttpStatus.SC_INTERNAL_SERVER_ERROR, + FAILED_TO_GET_STORED_CIS) + .toObjectMap(); + } catch (ParseException e) { + LOGGER.error(LogHelper.buildErrorMessage("Failed to get VOT from operational VC", e)); + return new JourneyErrorResponse( + JOURNEY_ERROR_PATH, + HttpStatus.SC_INTERNAL_SERVER_ERROR, + FAILED_TO_PARSE_ISSUED_CREDENTIALS) + .toObjectMap(); + } catch (Exception e) { + LOGGER.error(LogHelper.buildErrorMessage("Unhandled lambda exception", e)); + throw e; + } finally { + auditService.awaitAuditEvents(); + } + } + + private CoiCheckType getCoiCheckType( + CandidateIdentityType identityType, ClientOAuthSessionItem clientOAuthSessionItem) { + if (REVERIFICATION.equals(identityType)) { + return CoiCheckType.REVERIFICATION; + } + + if (TRUE.equals(clientOAuthSessionItem.getReproveIdentity())) { + return CoiCheckType.ACCOUNT_INTERVENTION; + } + + return CoiCheckType.STANDARD; + } + + private Map processCandidateThroughJourney( + CandidateIdentityType processIdentityType, + IpvSessionItem ipvSessionItem, + ClientOAuthSessionItem clientOAuthSessionItem, + String deviceInformation, + String ipAddress, + List sessionVcs, + AuditEventUser auditEventUser) + throws EvcsServiceException, HttpResponseExceptionWithErrorBody, CiRetrievalException, + CredentialParseException, ParseException { + if (COI_CHECK_TYPES.contains(processIdentityType)) { + var coiCheckType = getCoiCheckType(processIdentityType, clientOAuthSessionItem); + var isCoiCheckSuccessful = + checkCoiService.isCoiCheckSuccessful( + ipvSessionItem, + clientOAuthSessionItem, + coiCheckType, + deviceInformation, + sessionVcs, + auditEventUser); + + if (!isCoiCheckSuccessful) { + return JOURNEY_COI_CHECK_FAILED; + } + } + + if (PROFILE_MATCHING_TYPES.contains(processIdentityType)) { + var journey = + getJourneyResponseForProfileMatching( + ipvSessionItem, + clientOAuthSessionItem, + deviceInformation, + ipAddress, + sessionVcs, + auditEventUser); + + if (journey != null) { + return journey.toObjectMap(); + } + } + + if (configService.getBooleanParameter(CREDENTIAL_ISSUER_ENABLED, Cri.TICF.getId())) { + var journey = + getJourneyResponseFromTicfCall( + ipvSessionItem, + clientOAuthSessionItem, + deviceInformation, + ipAddress, + auditEventUser); + + if (journey != null) { + return journey.toObjectMap(); + } + ipvSessionService.updateIpvSession(ipvSessionItem); + } + + if (STORE_IDENTITY_TYPES.contains(processIdentityType)) { + storeIdentityService.storeIdentity( + ipvSessionItem, + clientOAuthSessionItem, + processIdentityType, + deviceInformation, + sessionVcs, + auditEventUser); + } + + return JOURNEY_NEXT.toObjectMap(); + } + + private JourneyResponse getJourneyResponseForProfileMatching( + IpvSessionItem ipvSessionItem, + ClientOAuthSessionItem clientOAuthSessionItem, + String deviceInformation, + String ipAddress, + List sessionVcs, + AuditEventUser auditEventUser) + throws HttpResponseExceptionWithErrorBody, CiRetrievalException, ParseException { + + var areVcsCorrelated = userIdentityService.areVcsCorrelated(sessionVcs); + + if (!areVcsCorrelated) { + return JOURNEY_VCS_NOT_CORRELATED; + } + + // This is a performance optimisation to save a call to CIMIT if it is not required + // If the VTR only contains one entry then it is impossible for a user to reach here + // with a breaching CI so we don't have to check. + var contraIndicators = + clientOAuthSessionItem.getVtr().size() == 1 + ? List.of() + : cimitService.getContraIndicators( + clientOAuthSessionItem.getUserId(), + clientOAuthSessionItem.getGovukSigninJourneyId(), + ipAddress); + + var votResult = + votMatcher.matchFirstVot( + clientOAuthSessionItem + .getParsedVtr() + .getRequestedVotsByStrengthDescending(), + sessionVcs, + contraIndicators, + areVcsCorrelated); + + if (votResult.isEmpty()) { + LOGGER.info(LogHelper.buildLogMessage("No GPG45 profiles have been met")); + return JOURNEY_GPG45_UNMET; + } + + ipvSessionItem.setVot(votResult.get().vot()); + ipvSessionService.updateIpvSession(ipvSessionItem); + + if (votResult.get().vot().getProfileType() == ProfileType.GPG45) { + LOGGER.info(LogHelper.buildLogMessage("A GPG45 profile has been met")); + sendProfileMatchedAuditEvent( + votResult.get(), + VcHelper.filterVCBasedOnProfileType(sessionVcs, ProfileType.GPG45), + auditEventUser, + deviceInformation); + } + + return null; + } + + public JourneyResponse getJourneyResponseFromTicfCall( + IpvSessionItem ipvSessionItem, + ClientOAuthSessionItem clientOAuthSessionItem, + String deviceInformation, + String ipAddress, + AuditEventUser auditEventUser) { + try { + var ticfVcs = ticfCriService.getTicfVc(clientOAuthSessionItem, ipvSessionItem); + + if (ticfVcs.isEmpty()) { + LOGGER.warn(LogHelper.buildLogMessage("No TICF VC to process - returning next")); + return null; + } + + criStoringService.storeVcs( + TICF, + ipAddress, + deviceInformation, + ticfVcs, + clientOAuthSessionItem, + ipvSessionItem, + List.of(), + auditEventUser); + + var cis = + cimitService.getContraIndicators( + clientOAuthSessionItem.getUserId(), + clientOAuthSessionItem.getGovukSigninJourneyId(), + ipAddress); + + var thresholdVot = ipvSessionItem.getThresholdVot(); + + var journeyResponse = + cimitUtilityService.getMitigationJourneyIfBreaching(cis, thresholdVot); + if (journeyResponse.isPresent()) { + LOGGER.info( + LogHelper.buildLogMessage( + "CI score is breaching threshold - setting VOT to P0")); + ipvSessionItem.setVot(Vot.P0); + ipvSessionService.updateIpvSession(ipvSessionItem); + + return journeyResponse.get(); + } + + LOGGER.info(LogHelper.buildLogMessage("CI score not breaching threshold")); + + return null; + } catch (TicfCriServiceException + | VerifiableCredentialException + | CiPostMitigationsException + | CiPutException + | CiRetrievalException + | ConfigException + | UnrecognisedVotException e) { + LOGGER.error(LogHelper.buildErrorMessage("Error processing response from TICF CRI", e)); + return new JourneyErrorResponse( + JOURNEY_ERROR_PATH, + HttpStatus.SC_INTERNAL_SERVER_ERROR, + ERROR_PROCESSING_TICF_CRI_RESPONSE); + } + } + + private void sendProfileMatchedAuditEvent( + VotMatchingResult votMatchingResult, + List vcs, + AuditEventUser auditEventUser, + String deviceInformation) { + var auditEvent = + AuditEvent.createWithDeviceInformation( + AuditEventTypes.IPV_GPG45_PROFILE_MATCHED, + configService.getParameter(ConfigurationVariable.COMPONENT_ID), + auditEventUser, + new AuditExtensionGpg45ProfileMatched( + votMatchingResult.gpg45Profile(), + votMatchingResult.gpg45Scores(), + VcHelper.extractTxnIdsFromCredentials(vcs)), + new AuditRestrictedDeviceInformation(deviceInformation)); + auditService.sendAuditEvent(auditEvent); + } +} diff --git a/lambdas/process-candidate-identity/src/main/java/uk/gov/di/ipv/core/processcandidateidentity/service/CheckCoiService.java b/lambdas/process-candidate-identity/src/main/java/uk/gov/di/ipv/core/processcandidateidentity/service/CheckCoiService.java new file mode 100644 index 0000000000..f81f545994 --- /dev/null +++ b/lambdas/process-candidate-identity/src/main/java/uk/gov/di/ipv/core/processcandidateidentity/service/CheckCoiService.java @@ -0,0 +1,194 @@ +package uk.gov.di.ipv.core.processcandidateidentity.service; + +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.library.annotations.ExcludeFromGeneratedCoverageReport; +import uk.gov.di.ipv.core.library.auditing.AuditEvent; +import uk.gov.di.ipv.core.library.auditing.AuditEventTypes; +import uk.gov.di.ipv.core.library.auditing.AuditEventUser; +import uk.gov.di.ipv.core.library.auditing.extension.AuditExtensionCoiCheck; +import uk.gov.di.ipv.core.library.auditing.restricted.AuditRestrictedCheckCoi; +import uk.gov.di.ipv.core.library.auditing.restricted.AuditRestrictedDeviceInformation; +import uk.gov.di.ipv.core.library.auditing.restricted.DeviceInformation; +import uk.gov.di.ipv.core.library.config.ConfigurationVariable; +import uk.gov.di.ipv.core.library.domain.IdentityClaim; +import uk.gov.di.ipv.core.library.domain.ReverificationFailureCode; +import uk.gov.di.ipv.core.library.domain.ReverificationStatus; +import uk.gov.di.ipv.core.library.domain.ScopeConstants; +import uk.gov.di.ipv.core.library.domain.VerifiableCredential; +import uk.gov.di.ipv.core.library.enums.CoiCheckType; +import uk.gov.di.ipv.core.library.exception.EvcsServiceException; +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.helpers.LogHelper; +import uk.gov.di.ipv.core.library.persistence.item.ClientOAuthSessionItem; +import uk.gov.di.ipv.core.library.persistence.item.IpvSessionItem; +import uk.gov.di.ipv.core.library.service.AuditService; +import uk.gov.di.ipv.core.library.service.ConfigService; +import uk.gov.di.ipv.core.library.service.EvcsService; +import uk.gov.di.ipv.core.library.service.IpvSessionService; +import uk.gov.di.ipv.core.library.service.UserIdentityService; + +import java.util.List; +import java.util.stream.Stream; + +import static java.lang.Boolean.TRUE; +import static uk.gov.di.ipv.core.library.enums.CoiCheckType.ACCOUNT_INTERVENTION; +import static uk.gov.di.ipv.core.library.enums.EvcsVCState.CURRENT; +import static uk.gov.di.ipv.core.library.helpers.LogHelper.LogField.LOG_CHECK_TYPE; + +public class CheckCoiService { + private static final Logger LOGGER = LogManager.getLogger(); + + private final ConfigService configService; + private final AuditService auditService; + private final IpvSessionService ipvSessionService; + private final UserIdentityService userIdentityService; + private final EvcsService evcsService; + + @ExcludeFromGeneratedCoverageReport + public CheckCoiService(ConfigService configService, AuditService auditService) { + this.configService = configService; + this.auditService = auditService; + this.ipvSessionService = new IpvSessionService(configService); + this.userIdentityService = new UserIdentityService(configService); + this.evcsService = new EvcsService(configService); + } + + @ExcludeFromGeneratedCoverageReport + public CheckCoiService( + ConfigService configService, + AuditService auditService, + IpvSessionService ipvSessionService, + UserIdentityService userIdentityService, + EvcsService evcsService) { + this.configService = configService; + this.auditService = auditService; + this.ipvSessionService = ipvSessionService; + this.userIdentityService = userIdentityService; + this.evcsService = evcsService; + } + + @Tracing + @Logging(clearState = true) + public boolean isCoiCheckSuccessful( + IpvSessionItem ipvSessionItem, + ClientOAuthSessionItem clientOAuthSession, + CoiCheckType checkType, + String deviceInformation, + List sessionVcs, + AuditEventUser auditEventUser) + throws HttpResponseExceptionWithErrorBody, CredentialParseException, + EvcsServiceException { + + var userId = clientOAuthSession.getUserId(); + + if (TRUE.equals(clientOAuthSession.getReproveIdentity())) { + LOGGER.info( + LogHelper.buildLogMessage( + "Reprove identity flag set - checking full name and DOB")); + checkType = ACCOUNT_INTERVENTION; + } + + sendAuditEvent( + AuditEventTypes.IPV_CONTINUITY_OF_IDENTITY_CHECK_START, + checkType, + null, + auditEventUser, + null, + null, + deviceInformation); + + var oldVcs = + evcsService.getVerifiableCredentials( + userId, clientOAuthSession.getEvcsAccessToken(), CURRENT); + var combinedCredentials = Stream.concat(oldVcs.stream(), sessionVcs.stream()).toList(); + var successfulCheck = + switch (checkType) { + case STANDARD -> userIdentityService.areNamesAndDobCorrelated( + combinedCredentials); + case REVERIFICATION -> userIdentityService + .areNamesAndDobCorrelatedForReverification(combinedCredentials); + case ACCOUNT_INTERVENTION -> userIdentityService.areVcsCorrelated( + combinedCredentials); + }; + + var scopeClaims = clientOAuthSession.getScopeClaims(); + var isReverification = scopeClaims.contains(ScopeConstants.REVERIFICATION); + + sendAuditEvent( + AuditEventTypes.IPV_CONTINUITY_OF_IDENTITY_CHECK_END, + checkType, + successfulCheck, + auditEventUser, + oldVcs, + sessionVcs, + deviceInformation); + + if (isReverification) { + ipvSessionItem.setReverificationStatus( + successfulCheck ? ReverificationStatus.SUCCESS : ReverificationStatus.FAILED); + + if (!successfulCheck) { + ipvSessionItem.setFailureCode(ReverificationFailureCode.IDENTITY_DID_NOT_MATCH); + } + + ipvSessionService.updateIpvSession(ipvSessionItem); + } + + if (!successfulCheck) { + LOGGER.info( + LogHelper.buildLogMessage("Failed COI check") + .with(LOG_CHECK_TYPE.getFieldName(), checkType)); + return false; + } + + LOGGER.info( + LogHelper.buildLogMessage("Successful COI check") + .with(LOG_CHECK_TYPE.getFieldName(), checkType)); + + return true; + } + + private void sendAuditEvent( + AuditEventTypes auditEventType, + CoiCheckType coiCheckType, + Boolean coiCheckSuccess, + AuditEventUser auditEventUser, + List oldVcs, + List sessionsVcs, + String deviceInformation) + throws HttpResponseExceptionWithErrorBody, CredentialParseException { + + var restrictedData = + auditEventType == AuditEventTypes.IPV_CONTINUITY_OF_IDENTITY_CHECK_END + ? getRestrictedCheckCoiAuditData(oldVcs, sessionsVcs, deviceInformation) + : new AuditRestrictedDeviceInformation(deviceInformation); + + auditService.sendAuditEvent( + AuditEvent.createWithDeviceInformation( + auditEventType, + configService.getParameter(ConfigurationVariable.COMPONENT_ID), + auditEventUser, + new AuditExtensionCoiCheck(coiCheckType, coiCheckSuccess), + restrictedData)); + } + + private AuditRestrictedCheckCoi getRestrictedCheckCoiAuditData( + List oldVcs, + List sessionVcs, + String deviceInformation) + throws HttpResponseExceptionWithErrorBody, CredentialParseException { + var oldIdentityClaim = userIdentityService.findIdentityClaim(oldVcs); + var sessionIdentityClaim = userIdentityService.findIdentityClaim(sessionVcs); + + return new AuditRestrictedCheckCoi( + oldIdentityClaim.map(IdentityClaim::getName).orElse(null), + sessionIdentityClaim.map(IdentityClaim::getName).orElse(null), + oldIdentityClaim.map(IdentityClaim::getBirthDate).orElse(null), + sessionIdentityClaim.map(IdentityClaim::getBirthDate).orElse(null), + new DeviceInformation(deviceInformation)); + } +} diff --git a/lambdas/process-candidate-identity/src/main/java/uk/gov/di/ipv/core/processcandidateidentity/service/StoreIdentityService.java b/lambdas/process-candidate-identity/src/main/java/uk/gov/di/ipv/core/processcandidateidentity/service/StoreIdentityService.java new file mode 100644 index 0000000000..75b0ab8398 --- /dev/null +++ b/lambdas/process-candidate-identity/src/main/java/uk/gov/di/ipv/core/processcandidateidentity/service/StoreIdentityService.java @@ -0,0 +1,86 @@ +package uk.gov.di.ipv.core.processcandidateidentity.service; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import uk.gov.di.ipv.core.library.annotations.ExcludeFromGeneratedCoverageReport; +import uk.gov.di.ipv.core.library.auditing.AuditEvent; +import uk.gov.di.ipv.core.library.auditing.AuditEventUser; +import uk.gov.di.ipv.core.library.auditing.extension.AuditExtensionCandidateIdentityType; +import uk.gov.di.ipv.core.library.auditing.restricted.AuditRestrictedDeviceInformation; +import uk.gov.di.ipv.core.library.config.ConfigurationVariable; +import uk.gov.di.ipv.core.library.domain.VerifiableCredential; +import uk.gov.di.ipv.core.library.enums.CandidateIdentityType; +import uk.gov.di.ipv.core.library.enums.Vot; +import uk.gov.di.ipv.core.library.exception.EvcsServiceException; +import uk.gov.di.ipv.core.library.helpers.LogHelper; +import uk.gov.di.ipv.core.library.persistence.item.ClientOAuthSessionItem; +import uk.gov.di.ipv.core.library.persistence.item.IpvSessionItem; +import uk.gov.di.ipv.core.library.service.AuditService; +import uk.gov.di.ipv.core.library.service.ConfigService; +import uk.gov.di.ipv.core.library.service.EvcsService; + +import java.util.List; + +import static uk.gov.di.ipv.core.library.auditing.AuditEventTypes.IPV_IDENTITY_STORED; +import static uk.gov.di.ipv.core.library.enums.Vot.P0; + +public class StoreIdentityService { + private static final Logger LOGGER = LogManager.getLogger(); + private final ConfigService configService; + private final AuditService auditService; + private final EvcsService evcsService; + + @ExcludeFromGeneratedCoverageReport + public StoreIdentityService(ConfigService configService, AuditService auditService) { + this.configService = configService; + this.auditService = auditService; + this.evcsService = new EvcsService(configService); + } + + @ExcludeFromGeneratedCoverageReport + public StoreIdentityService( + ConfigService configService, AuditService auditService, EvcsService evcsService) { + this.configService = configService; + this.auditService = auditService; + this.evcsService = evcsService; + } + + public void storeIdentity( + IpvSessionItem ipvSessionItem, + ClientOAuthSessionItem clientOAuthSessionItem, + CandidateIdentityType identityType, + String deviceInformation, + List credentials, + AuditEventUser auditEventUser) + throws EvcsServiceException { + String userId = clientOAuthSessionItem.getUserId(); + + if (identityType == CandidateIdentityType.PENDING) { + evcsService.storePendingIdentity( + userId, credentials, clientOAuthSessionItem.getEvcsAccessToken()); + } else { + evcsService.storeCompletedIdentity( + userId, credentials, clientOAuthSessionItem.getEvcsAccessToken()); + } + + LOGGER.info(LogHelper.buildLogMessage("Identity successfully stored")); + + sendIdentityStoredEvent(ipvSessionItem, identityType, deviceInformation, auditEventUser); + } + + private void sendIdentityStoredEvent( + IpvSessionItem ipvSessionItem, + CandidateIdentityType identityType, + String deviceInformation, + AuditEventUser auditEventUser) { + Vot vot = ipvSessionItem.getVot(); + auditService.sendAuditEvent( + AuditEvent.createWithDeviceInformation( + IPV_IDENTITY_STORED, + configService.getParameter(ConfigurationVariable.COMPONENT_ID), + auditEventUser, + new AuditExtensionCandidateIdentityType( + identityType, vot.equals(P0) ? null : vot), + new AuditRestrictedDeviceInformation(deviceInformation))); + } +} diff --git a/lambdas/process-candidate-identity/src/test/java/uk/gov/di/ipv/core/processcandidateidentity/ProcessCandidateIdentityHandlerTest.java b/lambdas/process-candidate-identity/src/test/java/uk/gov/di/ipv/core/processcandidateidentity/ProcessCandidateIdentityHandlerTest.java new file mode 100644 index 0000000000..9aee1be3ff --- /dev/null +++ b/lambdas/process-candidate-identity/src/test/java/uk/gov/di/ipv/core/processcandidateidentity/ProcessCandidateIdentityHandlerTest.java @@ -0,0 +1,725 @@ +package uk.gov.di.ipv.core.processcandidateidentity; + +import com.amazonaws.services.lambda.runtime.Context; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; +import uk.gov.di.ipv.core.library.auditing.AuditEventUser; +import uk.gov.di.ipv.core.library.cristoringservice.CriStoringService; +import uk.gov.di.ipv.core.library.domain.Cri; +import uk.gov.di.ipv.core.library.domain.JourneyResponse; +import uk.gov.di.ipv.core.library.domain.ProcessRequest; +import uk.gov.di.ipv.core.library.enums.CandidateIdentityType; +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.IpvSessionItem; +import uk.gov.di.ipv.core.library.service.AuditService; +import uk.gov.di.ipv.core.library.service.CimitService; +import uk.gov.di.ipv.core.library.service.CimitUtilityService; +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.IpvSessionService; +import uk.gov.di.ipv.core.library.service.UserIdentityService; +import uk.gov.di.ipv.core.library.service.VotMatcher; +import uk.gov.di.ipv.core.library.service.VotMatchingResult; +import uk.gov.di.ipv.core.library.testhelpers.unit.LogCollector; +import uk.gov.di.ipv.core.library.ticf.TicfCriService; +import uk.gov.di.ipv.core.library.verifiablecredential.service.SessionCredentialsService; +import uk.gov.di.ipv.core.processcandidateidentity.service.CheckCoiService; +import uk.gov.di.ipv.core.processcandidateidentity.service.StoreIdentityService; +import uk.gov.di.model.ContraIndicator; + +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.StringContains.containsString; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static uk.gov.di.ipv.core.library.config.ConfigurationVariable.CREDENTIAL_ISSUER_ENABLED; +import static uk.gov.di.ipv.core.library.domain.ErrorResponse.IPV_SESSION_NOT_FOUND; +import static uk.gov.di.ipv.core.library.domain.ErrorResponse.MISSING_PROCESS_IDENTITY_TYPE; +import static uk.gov.di.ipv.core.library.domain.ErrorResponse.UNEXPECTED_PROCESS_IDENTITY_TYPE; +import static uk.gov.di.ipv.core.library.enums.CoiCheckType.ACCOUNT_INTERVENTION; +import static uk.gov.di.ipv.core.library.enums.CoiCheckType.REVERIFICATION; +import static uk.gov.di.ipv.core.library.enums.CoiCheckType.STANDARD; +import static uk.gov.di.ipv.core.library.enums.Vot.P2; +import static uk.gov.di.ipv.core.library.fixtures.VcFixtures.vcTicf; +import static uk.gov.di.ipv.core.library.fixtures.VcFixtures.vcTicfWithCi; +import static uk.gov.di.ipv.core.library.gpg45.enums.Gpg45Profile.M1A; +import static uk.gov.di.ipv.core.library.journeys.JourneyUris.JOURNEY_COI_CHECK_FAILED_PATH; +import static uk.gov.di.ipv.core.library.journeys.JourneyUris.JOURNEY_ERROR_PATH; +import static uk.gov.di.ipv.core.library.journeys.JourneyUris.JOURNEY_FAIL_WITH_CI_PATH; +import static uk.gov.di.ipv.core.library.journeys.JourneyUris.JOURNEY_GPG45_UNMET_PATH; +import static uk.gov.di.ipv.core.library.journeys.JourneyUris.JOURNEY_NEXT_PATH; +import static uk.gov.di.ipv.core.library.journeys.JourneyUris.JOURNEY_VCS_NOT_CORRELATED; + +@ExtendWith(MockitoExtension.class) +class ProcessCandidateIdentityHandlerTest { + private static ProcessRequest.ProcessRequestBuilder requestBuilder; + + private static final String SESSION_ID = "session-id"; + private static final String IP_ADDRESS = "ip-address"; + private static final String DEVICE_INFORMATION = "device_information"; + private static final String SIGNIN_JOURNEY_ID = "journey-id"; + private static final String USER_ID = "user-id"; + + private static final JourneyResponse JOURNEY_NEXT = new JourneyResponse(JOURNEY_NEXT_PATH); + + @Spy private IpvSessionItem ipvSessionItem; + private ClientOAuthSessionItem clientOAuthSessionItem; + private ClientOAuthSessionItem.ClientOAuthSessionItemBuilder clientOAuthSessionItemBuilder; + + @Mock private Context context; + @Mock private ConfigService configService; + @Mock private IpvSessionService ipvSessionService; + @Mock private ClientOAuthSessionDetailsService clientOAuthSessionDetailsService; + @Mock private AuditService auditService; + @Mock private SessionCredentialsService sessionCredentialsService; + @Mock private CheckCoiService checkCoiService; + @Mock private VotMatcher votMatcher; + @Mock private StoreIdentityService storeIdentityService; + @Mock private UserIdentityService userIdentityService; + @Mock private TicfCriService ticfCriService; + @Mock private CriStoringService criStoringService; + @Mock private CimitUtilityService cimitUtilityService; + @Mock private CimitService cimitService; + @InjectMocks ProcessCandidateIdentityHandler processCandidateIdentityHandler; + + @BeforeEach + void setUp() { + requestBuilder = + ProcessRequest.processRequestBuilder() + .ipvSessionId(SESSION_ID) + .ipAddress(IP_ADDRESS) + .deviceInformation(DEVICE_INFORMATION); + + clientOAuthSessionItemBuilder = + ClientOAuthSessionItem.builder() + .userId(USER_ID) + .govukSigninJourneyId(SIGNIN_JOURNEY_ID) + .reproveIdentity(false); + + ipvSessionItem.setIpvSessionId(SESSION_ID); + ipvSessionItem.setVot(P2); + } + + @Nested + class processIdentity { + @BeforeEach + void setUp() throws Exception { + clientOAuthSessionItem = clientOAuthSessionItemBuilder.vtr(List.of("P2")).build(); + when(ipvSessionService.getIpvSession(SESSION_ID)).thenReturn(ipvSessionItem); + when(clientOAuthSessionDetailsService.getClientOAuthSession(any())) + .thenReturn(clientOAuthSessionItem); + } + + @Test + void shouldHandleCandidateIdentityTypeNewAndReturnJourneyNext() throws Exception { + // Arrange + var ticfVcs = List.of(vcTicf()); + when(checkCoiService.isCoiCheckSuccessful( + eq(ipvSessionItem), + eq(clientOAuthSessionItem), + eq(STANDARD), + eq(DEVICE_INFORMATION), + eq(List.of()), + any(AuditEventUser.class))) + .thenReturn(true); + when(votMatcher.matchFirstVot(anyList(), eq(List.of()), eq(List.of()), eq(true))) + .thenReturn(Optional.of(new VotMatchingResult(P2, M1A, M1A.getScores()))); + when(userIdentityService.areVcsCorrelated(List.of())).thenReturn(true); + when(configService.getBooleanParameter(CREDENTIAL_ISSUER_ENABLED, Cri.TICF.getId())) + .thenReturn(true); + when(ticfCriService.getTicfVc(clientOAuthSessionItem, ipvSessionItem)) + .thenReturn(ticfVcs); + when(cimitService.getContraIndicators(USER_ID, SIGNIN_JOURNEY_ID, IP_ADDRESS)) + .thenReturn(List.of()); + when(cimitUtilityService.getMitigationJourneyIfBreaching( + List.of(), ipvSessionItem.getThresholdVot())) + .thenReturn(Optional.empty()); + + var request = + requestBuilder + .lambdaInput( + Map.of("processIdentityType", CandidateIdentityType.NEW.name())) + .build(); + + // Act + var response = processCandidateIdentityHandler.handleRequest(request, context); + + // Assert + assertEquals(JOURNEY_NEXT.getJourney(), response.get("journey")); + + verify(storeIdentityService, times(1)) + .storeIdentity( + eq(ipvSessionItem), + eq(clientOAuthSessionItem), + eq(CandidateIdentityType.NEW), + eq(DEVICE_INFORMATION), + eq(List.of()), + any(AuditEventUser.class)); + verify(criStoringService, times(1)) + .storeVcs( + eq(Cri.TICF), + eq(IP_ADDRESS), + eq(DEVICE_INFORMATION), + eq(ticfVcs), + eq(clientOAuthSessionItem), + eq(ipvSessionItem), + eq(List.of()), + any(AuditEventUser.class)); + } + + @Test + void shouldHandleCandidateIdentityTypePendingAndReturnJourneyNext() throws Exception { + // Arrange + var ticfVcs = List.of(vcTicf()); + when(checkCoiService.isCoiCheckSuccessful( + eq(ipvSessionItem), + eq(clientOAuthSessionItem), + eq(STANDARD), + eq(DEVICE_INFORMATION), + eq(List.of()), + any(AuditEventUser.class))) + .thenReturn(true); + when(configService.getBooleanParameter(CREDENTIAL_ISSUER_ENABLED, Cri.TICF.getId())) + .thenReturn(true); + when(ticfCriService.getTicfVc(clientOAuthSessionItem, ipvSessionItem)) + .thenReturn(ticfVcs); + when(cimitService.getContraIndicators(USER_ID, SIGNIN_JOURNEY_ID, IP_ADDRESS)) + .thenReturn(List.of()); + when(cimitUtilityService.getMitigationJourneyIfBreaching( + List.of(), ipvSessionItem.getThresholdVot())) + .thenReturn(Optional.empty()); + + var request = + requestBuilder + .lambdaInput( + Map.of( + "processIdentityType", + CandidateIdentityType.PENDING.name())) + .build(); + + // Act + var response = processCandidateIdentityHandler.handleRequest(request, context); + + // Assert + assertEquals(JOURNEY_NEXT.getJourney(), response.get("journey")); + + verify(votMatcher, times(0)).matchFirstVot(any(), any(), any(), anyBoolean()); + verify(storeIdentityService, times(1)) + .storeIdentity( + eq(ipvSessionItem), + eq(clientOAuthSessionItem), + eq(CandidateIdentityType.PENDING), + eq(DEVICE_INFORMATION), + eq(List.of()), + any(AuditEventUser.class)); + verify(criStoringService, times(1)) + .storeVcs( + eq(Cri.TICF), + eq(IP_ADDRESS), + eq(DEVICE_INFORMATION), + eq(ticfVcs), + eq(clientOAuthSessionItem), + eq(ipvSessionItem), + eq(List.of()), + any(AuditEventUser.class)); + } + + @Test + void shouldHandleCandidateIdentityTypeReverificationAndReturnJourneyNext() + throws Exception { + // Arrange + var ticfVcs = List.of(vcTicf()); + when(checkCoiService.isCoiCheckSuccessful( + eq(ipvSessionItem), + eq(clientOAuthSessionItem), + eq(REVERIFICATION), + eq(DEVICE_INFORMATION), + eq(List.of()), + any(AuditEventUser.class))) + .thenReturn(true); + when(configService.getBooleanParameter(CREDENTIAL_ISSUER_ENABLED, Cri.TICF.getId())) + .thenReturn(true); + when(ticfCriService.getTicfVc(clientOAuthSessionItem, ipvSessionItem)) + .thenReturn(ticfVcs); + when(cimitService.getContraIndicators(USER_ID, SIGNIN_JOURNEY_ID, IP_ADDRESS)) + .thenReturn(List.of()); + when(cimitUtilityService.getMitigationJourneyIfBreaching( + List.of(), ipvSessionItem.getThresholdVot())) + .thenReturn(Optional.empty()); + + var request = + requestBuilder + .lambdaInput( + Map.of( + "processIdentityType", + CandidateIdentityType.REVERIFICATION.name())) + .build(); + + // Act + var response = processCandidateIdentityHandler.handleRequest(request, context); + + // Assert + assertEquals(JOURNEY_NEXT.getJourney(), response.get("journey")); + + verify(votMatcher, times(0)).matchFirstVot(any(), any(), any(), anyBoolean()); + verify(storeIdentityService, times(0)) + .storeIdentity(any(), any(), any(), any(), any(), any()); + verify(criStoringService, times(1)) + .storeVcs( + eq(Cri.TICF), + eq(IP_ADDRESS), + eq(DEVICE_INFORMATION), + eq(ticfVcs), + eq(clientOAuthSessionItem), + eq(ipvSessionItem), + eq(List.of()), + any(AuditEventUser.class)); + } + + @Test + void shouldHandleCandidateIdentityTypeExistingAndReturnJourneyNext() throws Exception { + // Arrange + var ticfVcs = List.of(vcTicf()); + when(configService.getBooleanParameter(CREDENTIAL_ISSUER_ENABLED, Cri.TICF.getId())) + .thenReturn(true); + when(ticfCriService.getTicfVc(clientOAuthSessionItem, ipvSessionItem)) + .thenReturn(ticfVcs); + when(cimitService.getContraIndicators(USER_ID, SIGNIN_JOURNEY_ID, IP_ADDRESS)) + .thenReturn(List.of()); + when(cimitUtilityService.getMitigationJourneyIfBreaching( + List.of(), ipvSessionItem.getThresholdVot())) + .thenReturn(Optional.empty()); + + var request = + requestBuilder + .lambdaInput( + Map.of( + "processIdentityType", + CandidateIdentityType.EXISTING.name())) + .build(); + + // Act + var response = processCandidateIdentityHandler.handleRequest(request, context); + + // Assert + assertEquals(JOURNEY_NEXT.getJourney(), response.get("journey")); + + verify(votMatcher, times(0)).matchFirstVot(any(), any(), any(), anyBoolean()); + verify(storeIdentityService, times(0)) + .storeIdentity(any(), any(), any(), any(), any(), any()); + verify(checkCoiService, times(0)) + .isCoiCheckSuccessful(any(), any(), any(), any(), any(), any()); + } + + @Test + void shouldHandleCandidateIdentityTypeIncompleteAndReturnJourneyNext() throws Exception { + // Arrange + var ticfVcs = List.of(vcTicf()); + when(configService.getBooleanParameter(CREDENTIAL_ISSUER_ENABLED, Cri.TICF.getId())) + .thenReturn(true); + when(ticfCriService.getTicfVc(clientOAuthSessionItem, ipvSessionItem)) + .thenReturn(ticfVcs); + when(cimitService.getContraIndicators(USER_ID, SIGNIN_JOURNEY_ID, IP_ADDRESS)) + .thenReturn(List.of()); + when(cimitUtilityService.getMitigationJourneyIfBreaching( + List.of(), ipvSessionItem.getThresholdVot())) + .thenReturn(Optional.empty()); + + var request = + requestBuilder + .lambdaInput( + Map.of( + "processIdentityType", + CandidateIdentityType.INCOMPLETE.name())) + .build(); + + // Act + var response = processCandidateIdentityHandler.handleRequest(request, context); + + // Assert + assertEquals(JOURNEY_NEXT.getJourney(), response.get("journey")); + + verify(votMatcher, times(0)).matchFirstVot(any(), any(), any(), anyBoolean()); + verify(storeIdentityService, times(0)) + .storeIdentity(any(), any(), any(), any(), any(), any()); + verify(checkCoiService, times(0)) + .isCoiCheckSuccessful(any(), any(), any(), any(), any(), any()); + } + + @Test + void shouldHandleCandidateIdentityTypeUpdateAndReturnJourneyNext() throws Exception { + // Arrange + var ticfVcs = List.of(vcTicf()); + when(checkCoiService.isCoiCheckSuccessful( + eq(ipvSessionItem), + eq(clientOAuthSessionItem), + eq(STANDARD), + eq(DEVICE_INFORMATION), + eq(List.of()), + any(AuditEventUser.class))) + .thenReturn(true); + when(votMatcher.matchFirstVot(anyList(), eq(List.of()), eq(List.of()), eq(true))) + .thenReturn(Optional.of(new VotMatchingResult(P2, M1A, M1A.getScores()))); + when(userIdentityService.areVcsCorrelated(List.of())).thenReturn(true); + when(configService.getBooleanParameter(CREDENTIAL_ISSUER_ENABLED, Cri.TICF.getId())) + .thenReturn(true); + when(ticfCriService.getTicfVc(clientOAuthSessionItem, ipvSessionItem)) + .thenReturn(ticfVcs); + when(cimitService.getContraIndicators(USER_ID, SIGNIN_JOURNEY_ID, IP_ADDRESS)) + .thenReturn(List.of()); + when(cimitUtilityService.getMitigationJourneyIfBreaching( + List.of(), ipvSessionItem.getThresholdVot())) + .thenReturn(Optional.empty()); + + var request = + requestBuilder + .lambdaInput( + Map.of( + "processIdentityType", + CandidateIdentityType.UPDATE.name())) + .build(); + + // Act + var response = processCandidateIdentityHandler.handleRequest(request, context); + + // Assert + assertEquals(JOURNEY_NEXT.getJourney(), response.get("journey")); + + verify(storeIdentityService, times(1)) + .storeIdentity( + eq(ipvSessionItem), + eq(clientOAuthSessionItem), + eq(CandidateIdentityType.UPDATE), + eq(DEVICE_INFORMATION), + eq(List.of()), + any(AuditEventUser.class)); + verify(criStoringService, times(1)) + .storeVcs( + eq(Cri.TICF), + eq(IP_ADDRESS), + eq(DEVICE_INFORMATION), + eq(ticfVcs), + eq(clientOAuthSessionItem), + eq(ipvSessionItem), + eq(List.of()), + any(AuditEventUser.class)); + } + + @Test + void shouldNotCallTicfIfDisabled() throws Exception { + // Arrange + when(configService.getBooleanParameter(CREDENTIAL_ISSUER_ENABLED, Cri.TICF.getId())) + .thenReturn(false); + + var request = + requestBuilder + .lambdaInput( + Map.of( + "processIdentityType", + CandidateIdentityType.INCOMPLETE.name())) + .build(); + + // Act + var response = processCandidateIdentityHandler.handleRequest(request, context); + + // Assert + assertEquals(JOURNEY_NEXT.getJourney(), response.get("journey")); + + verify(votMatcher, times(0)).matchFirstVot(any(), any(), any(), anyBoolean()); + verify(storeIdentityService, times(0)) + .storeIdentity(any(), any(), any(), any(), any(), any()); + verify(checkCoiService, times(0)) + .isCoiCheckSuccessful(any(), any(), any(), any(), any(), any()); + verify(ticfCriService, times(0)).getTicfVc(any(), any()); + } + + @Test + void shouldHandleCoiFailure() throws Exception { + // Arrange + when(checkCoiService.isCoiCheckSuccessful( + eq(ipvSessionItem), + eq(clientOAuthSessionItem), + eq(STANDARD), + eq(DEVICE_INFORMATION), + eq(List.of()), + any(AuditEventUser.class))) + .thenReturn(false); + + var request = + requestBuilder + .lambdaInput( + Map.of("processIdentityType", CandidateIdentityType.NEW.name())) + .build(); + + // Act + var response = processCandidateIdentityHandler.handleRequest(request, context); + + // Assert + assertEquals(JOURNEY_COI_CHECK_FAILED_PATH, response.get("journey")); + + verify(storeIdentityService, times(0)) + .storeIdentity(any(), any(), any(), any(), anyList(), any()); + } + + @Test + void shouldHandleCorrelationFailure() throws Exception { + // Arrange + when(checkCoiService.isCoiCheckSuccessful( + eq(ipvSessionItem), + eq(clientOAuthSessionItem), + eq(STANDARD), + eq(DEVICE_INFORMATION), + eq(List.of()), + any(AuditEventUser.class))) + .thenReturn(true); + when(userIdentityService.areVcsCorrelated(List.of())).thenReturn(false); + + var request = + requestBuilder + .lambdaInput( + Map.of("processIdentityType", CandidateIdentityType.NEW.name())) + .build(); + + // Act + var response = processCandidateIdentityHandler.handleRequest(request, context); + + // Assert + assertEquals(JOURNEY_VCS_NOT_CORRELATED, response.get("journey")); + + verify(storeIdentityService, times(0)) + .storeIdentity(any(), any(), any(), any(), anyList(), any()); + } + + @Test + void shouldHandleNoProfileMatch() throws Exception { + // Arrange + when(checkCoiService.isCoiCheckSuccessful( + eq(ipvSessionItem), + eq(clientOAuthSessionItem), + eq(STANDARD), + eq(DEVICE_INFORMATION), + eq(List.of()), + any(AuditEventUser.class))) + .thenReturn(true); + when(votMatcher.matchFirstVot(anyList(), eq(List.of()), eq(List.of()), eq(true))) + .thenReturn(Optional.empty()); + when(userIdentityService.areVcsCorrelated(List.of())).thenReturn(true); + + var request = + requestBuilder + .lambdaInput( + Map.of("processIdentityType", CandidateIdentityType.NEW.name())) + .build(); + + // Act + var response = processCandidateIdentityHandler.handleRequest(request, context); + + // Assert + assertEquals(JOURNEY_GPG45_UNMET_PATH, response.get("journey")); + + verify(storeIdentityService, times(0)) + .storeIdentity(any(), any(), any(), any(), anyList(), any()); + } + + @Test + void shouldHandleTicfBreachingContraindicator() throws Exception { + // Arrange + var ticfVcs = List.of(vcTicfWithCi()); + var ticfCis = List.of(new ContraIndicator()); + when(checkCoiService.isCoiCheckSuccessful( + eq(ipvSessionItem), + eq(clientOAuthSessionItem), + eq(STANDARD), + eq(DEVICE_INFORMATION), + eq(List.of()), + any(AuditEventUser.class))) + .thenReturn(true); + when(votMatcher.matchFirstVot(anyList(), eq(List.of()), eq(List.of()), eq(true))) + .thenReturn(Optional.of(new VotMatchingResult(P2, M1A, M1A.getScores()))); + when(userIdentityService.areVcsCorrelated(List.of())).thenReturn(true); + when(configService.getBooleanParameter(CREDENTIAL_ISSUER_ENABLED, Cri.TICF.getId())) + .thenReturn(true); + when(ticfCriService.getTicfVc(clientOAuthSessionItem, ipvSessionItem)) + .thenReturn(ticfVcs); + when(cimitService.getContraIndicators(USER_ID, SIGNIN_JOURNEY_ID, IP_ADDRESS)) + .thenReturn(ticfCis); + when(cimitUtilityService.getMitigationJourneyIfBreaching( + ticfCis, ipvSessionItem.getThresholdVot())) + .thenReturn(Optional.of(new JourneyResponse(JOURNEY_FAIL_WITH_CI_PATH))); + + var request = + requestBuilder + .lambdaInput( + Map.of("processIdentityType", CandidateIdentityType.NEW.name())) + .build(); + + // Act + var response = processCandidateIdentityHandler.handleRequest(request, context); + + // Assert + assertEquals(JOURNEY_FAIL_WITH_CI_PATH, response.get("journey")); + + verify(storeIdentityService, times(0)) + .storeIdentity(any(), any(), any(), any(), anyList(), any()); + } + + @Test + void shouldHandleReproveIdentityAndReturnJourneyNext() throws Exception { + // Arrange + var reproveIdentityClientOAuthSessionItem = + clientOAuthSessionItemBuilder.reproveIdentity(true).vtr(List.of("P2")).build(); + when(clientOAuthSessionDetailsService.getClientOAuthSession(any())) + .thenReturn(reproveIdentityClientOAuthSessionItem); + + var ticfVcs = List.of(vcTicf()); + when(checkCoiService.isCoiCheckSuccessful( + eq(ipvSessionItem), + eq(reproveIdentityClientOAuthSessionItem), + eq(ACCOUNT_INTERVENTION), + eq(DEVICE_INFORMATION), + eq(List.of()), + any(AuditEventUser.class))) + .thenReturn(true); + when(votMatcher.matchFirstVot(anyList(), eq(List.of()), eq(List.of()), eq(true))) + .thenReturn(Optional.of(new VotMatchingResult(P2, M1A, M1A.getScores()))); + when(userIdentityService.areVcsCorrelated(List.of())).thenReturn(true); + when(configService.getBooleanParameter(CREDENTIAL_ISSUER_ENABLED, Cri.TICF.getId())) + .thenReturn(true); + when(ticfCriService.getTicfVc(reproveIdentityClientOAuthSessionItem, ipvSessionItem)) + .thenReturn(ticfVcs); + when(cimitService.getContraIndicators(USER_ID, SIGNIN_JOURNEY_ID, IP_ADDRESS)) + .thenReturn(List.of()); + when(cimitUtilityService.getMitigationJourneyIfBreaching( + List.of(), ipvSessionItem.getThresholdVot())) + .thenReturn(Optional.empty()); + + var request = + requestBuilder + .lambdaInput( + Map.of("processIdentityType", CandidateIdentityType.NEW.name())) + .build(); + + // Act + var response = processCandidateIdentityHandler.handleRequest(request, context); + + // Assert + assertEquals(JOURNEY_NEXT.getJourney(), response.get("journey")); + + verify(storeIdentityService, times(1)) + .storeIdentity( + eq(ipvSessionItem), + eq(reproveIdentityClientOAuthSessionItem), + eq(CandidateIdentityType.NEW), + eq(DEVICE_INFORMATION), + eq(List.of()), + any(AuditEventUser.class)); + verify(criStoringService, times(1)) + .storeVcs( + eq(Cri.TICF), + eq(IP_ADDRESS), + eq(DEVICE_INFORMATION), + eq(ticfVcs), + eq(reproveIdentityClientOAuthSessionItem), + eq(ipvSessionItem), + eq(List.of()), + any(AuditEventUser.class)); + } + } + + @Test + void shouldReturnJourneyErrorIfIdentityTypeIsNotProvided() { + // Arrange + var request = requestBuilder.lambdaInput(Map.of()).build(); + + // Act + var response = processCandidateIdentityHandler.handleRequest(request, context); + + // Assert + assertEquals(JOURNEY_ERROR_PATH, response.get("journey")); + assertEquals(400, response.get("statusCode")); + assertEquals(MISSING_PROCESS_IDENTITY_TYPE.getMessage(), response.get("message")); + } + + @Test + void shouldReturnJourneyErrorIfIdentityTypeIsInvalid() { + // Arrange + var request = + requestBuilder.lambdaInput(Map.of("processIdentityType", "invalid-type")).build(); + + // Act + var response = processCandidateIdentityHandler.handleRequest(request, context); + + // Assert + assertEquals(JOURNEY_ERROR_PATH, response.get("journey")); + assertEquals(400, response.get("statusCode")); + assertEquals(UNEXPECTED_PROCESS_IDENTITY_TYPE.getMessage(), response.get("message")); + } + + @Test + void shouldReturnJourneyErrorIfIpvSessionMissing() throws Exception { + // Arrange + when(ipvSessionService.getIpvSession(SESSION_ID)) + .thenThrow(new IpvSessionNotFoundException("Oh no")); + var request = + requestBuilder + .lambdaInput( + Map.of( + "processIdentityType", + CandidateIdentityType.INCOMPLETE.name())) + .build(); + + // Act + var response = processCandidateIdentityHandler.handleRequest(request, context); + + // Assert + assertEquals(JOURNEY_ERROR_PATH, response.get("journey")); + assertEquals(500, response.get("statusCode")); + assertEquals(IPV_SESSION_NOT_FOUND.getMessage(), response.get("message")); + } + + @Test + void shouldLogRuntimeExceptionsAndRethrow() throws Exception { + // Arrange + when(ipvSessionService.getIpvSession(anyString())) + .thenThrow(new RuntimeException("Test error")); + + var logCollector = LogCollector.getLogCollectorFor(ProcessCandidateIdentityHandler.class); + + var request = + requestBuilder + .lambdaInput( + Map.of("processIdentityType", CandidateIdentityType.NEW.name())) + .build(); + + // Act + var thrown = + assertThrows( + Exception.class, + () -> processCandidateIdentityHandler.handleRequest(request, context), + "Expected handleRequest() to throw, but it didn't"); + + // Assert + assertEquals("Test error", thrown.getMessage()); + var logMessage = logCollector.getLogMessages().get(0); + assertThat(logMessage, containsString("Unhandled lambda exception")); + assertThat(logMessage, containsString("Test error")); + } +} diff --git a/lambdas/process-candidate-identity/src/test/java/uk/gov/di/ipv/core/processcandidateidentity/service/CheckCoiServiceTest.java b/lambdas/process-candidate-identity/src/test/java/uk/gov/di/ipv/core/processcandidateidentity/service/CheckCoiServiceTest.java new file mode 100644 index 0000000000..389972faf3 --- /dev/null +++ b/lambdas/process-candidate-identity/src/test/java/uk/gov/di/ipv/core/processcandidateidentity/service/CheckCoiServiceTest.java @@ -0,0 +1,430 @@ +package uk.gov.di.ipv.core.processcandidateidentity.service; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import uk.gov.di.ipv.core.library.auditing.AuditEvent; +import uk.gov.di.ipv.core.library.auditing.AuditEventTypes; +import uk.gov.di.ipv.core.library.auditing.AuditEventUser; +import uk.gov.di.ipv.core.library.auditing.extension.AuditExtensionCoiCheck; +import uk.gov.di.ipv.core.library.config.ConfigurationVariable; +import uk.gov.di.ipv.core.library.domain.IdentityClaim; +import uk.gov.di.ipv.core.library.domain.ReverificationStatus; +import uk.gov.di.ipv.core.library.enums.EvcsVCState; +import uk.gov.di.ipv.core.library.helpers.vocab.BirthDateGenerator; +import uk.gov.di.ipv.core.library.persistence.item.ClientOAuthSessionItem; +import uk.gov.di.ipv.core.library.persistence.item.IpvSessionItem; +import uk.gov.di.ipv.core.library.service.AuditService; +import uk.gov.di.ipv.core.library.service.ConfigService; +import uk.gov.di.ipv.core.library.service.EvcsService; +import uk.gov.di.ipv.core.library.service.IpvSessionService; +import uk.gov.di.ipv.core.library.service.UserIdentityService; +import uk.gov.di.ipv.core.library.verifiablecredential.service.SessionCredentialsService; +import uk.gov.di.model.NamePart; + +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static uk.gov.di.ipv.core.library.enums.CoiCheckType.ACCOUNT_INTERVENTION; +import static uk.gov.di.ipv.core.library.enums.CoiCheckType.STANDARD; +import static uk.gov.di.ipv.core.library.fixtures.VcFixtures.M1A_ADDRESS_VC; +import static uk.gov.di.ipv.core.library.fixtures.VcFixtures.M1A_EXPERIAN_FRAUD_VC; +import static uk.gov.di.ipv.core.library.helpers.vocab.NameGenerator.NamePartGenerator.createNamePart; +import static uk.gov.di.ipv.core.library.helpers.vocab.NameGenerator.createName; + +@ExtendWith(MockitoExtension.class) +class CheckCoiServiceTest { + private static final String EVCS_ACCESS_TOKEN = "evcs-access-token"; + private static final String USER_ID = "user-id"; + private static final String IPV_SESSION_ID = "ipv-session-id"; + private static final String OPENID_SCOPE = "openid"; + private static final String REVERIFICATION_SCOPE = "reverification"; + private AuditEventUser testAuditEventUser; + + @Mock private ConfigService mockConfigService; + @Mock private AuditService mockAuditService; + @Mock private SessionCredentialsService mockSessionCredentialsService; + @Mock private UserIdentityService mockUserIdentityService; + @Mock private EvcsService mockEvcsService; + @Mock private IpvSessionService mockIpvSessionService; + @InjectMocks private CheckCoiService checkCoiService; + @Captor private ArgumentCaptor auditEventCaptor; + + @BeforeEach + void setup() throws Exception { + testAuditEventUser = + new AuditEventUser(USER_ID, IPV_SESSION_ID, "govuk-signin_journeyid", "ip-address"); + when(mockEvcsService.getVerifiableCredentials( + USER_ID, EVCS_ACCESS_TOKEN, EvcsVCState.CURRENT)) + .thenReturn(List.of(M1A_ADDRESS_VC)); + when(mockConfigService.getParameter(ConfigurationVariable.COMPONENT_ID)) + .thenReturn("some-component-id"); + + when(mockUserIdentityService.findIdentityClaim(any())).thenReturn(getMockIdentityClaim()); + } + + @Test + void shouldReturnTrueForSuccessfulNamesAndDobCheck() throws Exception { + // Arrange + when(mockUserIdentityService.areNamesAndDobCorrelated(any())).thenReturn(true); + + var ipvSessionItem = new IpvSessionItem(); + ipvSessionItem.setIpvSessionId(IPV_SESSION_ID); + + var clientOAuthSessionItem = + ClientOAuthSessionItem.builder() + .scope(OPENID_SCOPE) + .userId(USER_ID) + .evcsAccessToken(EVCS_ACCESS_TOKEN) + .build(); + + // Act + var res = + checkCoiService.isCoiCheckSuccessful( + ipvSessionItem, + clientOAuthSessionItem, + STANDARD, + "device-information", + List.of(), + testAuditEventUser); + + // Assert + assertTrue(res); + + verify(mockAuditService, times(2)).sendAuditEvent(auditEventCaptor.capture()); + var auditEventsCaptured = auditEventCaptor.getAllValues(); + + assertEquals( + AuditEventTypes.IPV_CONTINUITY_OF_IDENTITY_CHECK_START, + auditEventsCaptured.get(0).getEventName()); + assertEquals( + new AuditExtensionCoiCheck(STANDARD, null), + auditEventsCaptured.get(0).getExtensions()); + assertEquals( + AuditEventTypes.IPV_CONTINUITY_OF_IDENTITY_CHECK_END, + auditEventsCaptured.get(1).getEventName()); + assertEquals( + new AuditExtensionCoiCheck(STANDARD, true), + auditEventsCaptured.get(1).getExtensions()); + + var restrictedAuditData = getRestrictedAuditDataNodeFromEvent(auditEventsCaptured.get(1)); + assertTrue(restrictedAuditData.has("newName")); + assertTrue(restrictedAuditData.has("oldName")); + assertTrue(restrictedAuditData.has("newBirthDate")); + assertTrue(restrictedAuditData.has("oldBirthDate")); + assertTrue(restrictedAuditData.has("device_information")); + } + + @Test + void shouldReturnTrueForSuccessfulFullNameAndDobCheck() throws Exception { + // Arrange + when(mockUserIdentityService.areVcsCorrelated(any())).thenReturn(true); + + var ipvSessionItem = new IpvSessionItem(); + ipvSessionItem.setIpvSessionId(IPV_SESSION_ID); + + var clientOAuthSessionItem = + ClientOAuthSessionItem.builder() + .scope(OPENID_SCOPE) + .userId(USER_ID) + .evcsAccessToken(EVCS_ACCESS_TOKEN) + .build(); + + // Act + var res = + checkCoiService.isCoiCheckSuccessful( + ipvSessionItem, + clientOAuthSessionItem, + ACCOUNT_INTERVENTION, + "device-information", + List.of(), + testAuditEventUser); + + // Assert + assertTrue(res); + verify(mockAuditService, times(2)).sendAuditEvent(auditEventCaptor.capture()); + } + + @Test + void shouldDoFullCheckIfReproveIdentityJourney() throws Exception { + when(mockUserIdentityService.areVcsCorrelated( + List.of(M1A_ADDRESS_VC, M1A_EXPERIAN_FRAUD_VC))) + .thenReturn(true); + var ipvSessionItem = new IpvSessionItem(); + ipvSessionItem.setIpvSessionId(IPV_SESSION_ID); + + var clientOAuthSessionItem = + ClientOAuthSessionItem.builder() + .scope(OPENID_SCOPE) + .userId(USER_ID) + .evcsAccessToken(EVCS_ACCESS_TOKEN) + .reproveIdentity(true) + .build(); + + // Act + var res = + checkCoiService.isCoiCheckSuccessful( + ipvSessionItem, + clientOAuthSessionItem, + STANDARD, + "device-information", + List.of(M1A_EXPERIAN_FRAUD_VC), + testAuditEventUser); + + // Assert + assertTrue(res); + verify(mockAuditService, times(2)).sendAuditEvent(auditEventCaptor.capture()); + } + + @Test + void shouldReturnPassedForSuccessfulReverificationCheckAndSetReverificationStatusToSuccess() + throws Exception { + when(mockUserIdentityService.areVcsCorrelated(any())).thenReturn(true); + + var ipvSessionItem = new IpvSessionItem(); + ipvSessionItem.setIpvSessionId(IPV_SESSION_ID); + + var clientOAuthSessionItem = + ClientOAuthSessionItem.builder() + .scope(REVERIFICATION_SCOPE) + .userId(USER_ID) + .evcsAccessToken(EVCS_ACCESS_TOKEN) + .build(); + + // Act + var res = + checkCoiService.isCoiCheckSuccessful( + ipvSessionItem, + clientOAuthSessionItem, + ACCOUNT_INTERVENTION, + "device-information", + List.of(), + testAuditEventUser); + + // Assert + assertTrue(res); + assertEquals(ReverificationStatus.SUCCESS, ipvSessionItem.getReverificationStatus()); + + verify(mockAuditService, times(2)).sendAuditEvent(auditEventCaptor.capture()); + verify(mockIpvSessionService, times(1)).updateIpvSession(ipvSessionItem); + } + + @Test + void shouldSendOnlyDeviceInformationInRestrictedDataIfNoIdentityClaimsFound() throws Exception { + when(mockUserIdentityService.areNamesAndDobCorrelated(any())).thenReturn(true); + when(mockUserIdentityService.findIdentityClaim(any())).thenReturn(Optional.empty()); + + var ipvSessionItem = new IpvSessionItem(); + ipvSessionItem.setIpvSessionId(IPV_SESSION_ID); + + var clientOAuthSessionItem = + ClientOAuthSessionItem.builder() + .scope(OPENID_SCOPE) + .userId(USER_ID) + .evcsAccessToken(EVCS_ACCESS_TOKEN) + .build(); + + // Act + var res = + checkCoiService.isCoiCheckSuccessful( + ipvSessionItem, + clientOAuthSessionItem, + STANDARD, + "device-information", + List.of(), + testAuditEventUser); + + // Assert + assertTrue(res); + + verify(mockAuditService, times(2)).sendAuditEvent(auditEventCaptor.capture()); + var auditEventsCaptured = auditEventCaptor.getAllValues(); + + assertEquals( + AuditEventTypes.IPV_CONTINUITY_OF_IDENTITY_CHECK_START, + auditEventsCaptured.get(0).getEventName()); + assertEquals( + new AuditExtensionCoiCheck(STANDARD, null), + auditEventsCaptured.get(0).getExtensions()); + assertEquals( + AuditEventTypes.IPV_CONTINUITY_OF_IDENTITY_CHECK_END, + auditEventsCaptured.get(1).getEventName()); + assertEquals( + new AuditExtensionCoiCheck(STANDARD, true), + auditEventsCaptured.get(1).getExtensions()); + + var restrictedAuditData = getRestrictedAuditDataNodeFromEvent(auditEventsCaptured.get(1)); + assertFalse(restrictedAuditData.has("newName")); + assertFalse(restrictedAuditData.has("oldName")); + assertFalse(restrictedAuditData.has("newBirthDate")); + assertFalse(restrictedAuditData.has("oldBirthDate")); + assertTrue(restrictedAuditData.has("device_information")); + } + + @Test + void shouldReturnFalseForFailedGivenNamesAndDobCheck() throws Exception { + // Arrange + when(mockUserIdentityService.areNamesAndDobCorrelated(any())).thenReturn(false); + + var ipvSessionItem = new IpvSessionItem(); + ipvSessionItem.setIpvSessionId(IPV_SESSION_ID); + + var clientOAuthSessionItem = + ClientOAuthSessionItem.builder() + .scope(OPENID_SCOPE) + .userId(USER_ID) + .evcsAccessToken(EVCS_ACCESS_TOKEN) + .build(); + + // Act + var res = + checkCoiService.isCoiCheckSuccessful( + ipvSessionItem, + clientOAuthSessionItem, + STANDARD, + "device-information", + List.of(), + testAuditEventUser); + + // Assert + assertFalse(res); + + verify(mockAuditService, times(2)).sendAuditEvent(auditEventCaptor.capture()); + var auditEventsCaptured = auditEventCaptor.getAllValues(); + + assertEquals( + AuditEventTypes.IPV_CONTINUITY_OF_IDENTITY_CHECK_START, + auditEventsCaptured.get(0).getEventName()); + assertEquals( + new AuditExtensionCoiCheck(STANDARD, null), + auditEventsCaptured.get(0).getExtensions()); + assertEquals( + AuditEventTypes.IPV_CONTINUITY_OF_IDENTITY_CHECK_END, + auditEventsCaptured.get(1).getEventName()); + assertEquals( + new AuditExtensionCoiCheck(STANDARD, false), + auditEventsCaptured.get(1).getExtensions()); + + var restrictedAuditData = getRestrictedAuditDataNodeFromEvent(auditEventsCaptured.get(1)); + assertTrue(restrictedAuditData.has("newName")); + assertTrue(restrictedAuditData.has("oldName")); + assertTrue(restrictedAuditData.has("newBirthDate")); + assertTrue(restrictedAuditData.has("oldBirthDate")); + assertTrue(restrictedAuditData.has("device_information")); + } + + @Test + void shouldReturnFalseForFailedFullNameAndDobCheck() throws Exception { + // Arrange + when(mockUserIdentityService.areVcsCorrelated(any())).thenReturn(false); + + var ipvSessionItem = new IpvSessionItem(); + ipvSessionItem.setIpvSessionId(IPV_SESSION_ID); + + var clientOAuthSessionItem = + ClientOAuthSessionItem.builder() + .scope(OPENID_SCOPE) + .userId(USER_ID) + .evcsAccessToken(EVCS_ACCESS_TOKEN) + .build(); + + // Act + var res = + checkCoiService.isCoiCheckSuccessful( + ipvSessionItem, + clientOAuthSessionItem, + ACCOUNT_INTERVENTION, + "device-information", + List.of(), + testAuditEventUser); + + // Assert + assertFalse(res); + + verify(mockAuditService, times(2)).sendAuditEvent(auditEventCaptor.capture()); + var auditEventsCaptured = auditEventCaptor.getAllValues(); + + assertEquals( + AuditEventTypes.IPV_CONTINUITY_OF_IDENTITY_CHECK_END, + auditEventsCaptured.get(1).getEventName()); + assertEquals( + new AuditExtensionCoiCheck(ACCOUNT_INTERVENTION, false), + auditEventsCaptured.get(1).getExtensions()); + } + + @Test + void shouldReturnFalseForFailedReverificationCheckAndReverificationStatusSetToFailed() + throws Exception { + // Arrange + when(mockUserIdentityService.areVcsCorrelated(any())).thenReturn(false); + + var ipvSessionItem = new IpvSessionItem(); + ipvSessionItem.setIpvSessionId(IPV_SESSION_ID); + + var clientOAuthSessionItem = + ClientOAuthSessionItem.builder() + .scope(REVERIFICATION_SCOPE) + .userId(USER_ID) + .evcsAccessToken(EVCS_ACCESS_TOKEN) + .build(); + + // Act + var res = + checkCoiService.isCoiCheckSuccessful( + ipvSessionItem, + clientOAuthSessionItem, + ACCOUNT_INTERVENTION, + "device-information", + List.of(), + testAuditEventUser); + + // Assert + assertFalse(res); + + verify(mockAuditService, times(2)).sendAuditEvent(auditEventCaptor.capture()); + var auditEventsCaptured = auditEventCaptor.getAllValues(); + + assertEquals( + AuditEventTypes.IPV_CONTINUITY_OF_IDENTITY_CHECK_END, + auditEventsCaptured.get(1).getEventName()); + assertEquals( + new AuditExtensionCoiCheck(ACCOUNT_INTERVENTION, false), + auditEventsCaptured.get(1).getExtensions()); + } + + private Optional getMockIdentityClaim() { + var mockNameParts = + createNamePart("Kenneth Decerqueira", NamePart.NamePartType.FAMILY_NAME); + var mockBirthDate = BirthDateGenerator.createBirthDate("1965-07-08"); + return Optional.of( + new IdentityClaim( + List.of(createName(List.of(mockNameParts))), List.of(mockBirthDate))); + } + + private JsonNode getRestrictedAuditDataNodeFromEvent(AuditEvent auditEvent) throws Exception { + var coiCheckEndAuditEvent = getJsonNodeForAuditEvent(auditEvent); + return coiCheckEndAuditEvent.get("restricted"); + } + + private JsonNode getJsonNodeForAuditEvent(AuditEvent object) throws Exception { + ObjectMapper mapper = new ObjectMapper(); + String json = mapper.writeValueAsString(object); + JsonParser parser = mapper.createParser(json); + return mapper.readTree(parser); + } +} diff --git a/lambdas/process-candidate-identity/src/test/java/uk/gov/di/ipv/core/processcandidateidentity/service/StoreIdentityServiceTest.java b/lambdas/process-candidate-identity/src/test/java/uk/gov/di/ipv/core/processcandidateidentity/service/StoreIdentityServiceTest.java new file mode 100644 index 0000000000..180d1beb67 --- /dev/null +++ b/lambdas/process-candidate-identity/src/test/java/uk/gov/di/ipv/core/processcandidateidentity/service/StoreIdentityServiceTest.java @@ -0,0 +1,271 @@ +package uk.gov.di.ipv.core.processcandidateidentity.service; + +import com.nimbusds.oauth2.sdk.http.HTTPResponse; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; +import uk.gov.di.ipv.core.library.auditing.AuditEvent; +import uk.gov.di.ipv.core.library.auditing.AuditEventUser; +import uk.gov.di.ipv.core.library.auditing.extension.AuditExtensionCandidateIdentityType; +import uk.gov.di.ipv.core.library.config.ConfigurationVariable; +import uk.gov.di.ipv.core.library.domain.VerifiableCredential; +import uk.gov.di.ipv.core.library.enums.CandidateIdentityType; +import uk.gov.di.ipv.core.library.exception.EvcsServiceException; +import uk.gov.di.ipv.core.library.persistence.item.ClientOAuthSessionItem; +import uk.gov.di.ipv.core.library.persistence.item.IpvSessionItem; +import uk.gov.di.ipv.core.library.service.AuditService; +import uk.gov.di.ipv.core.library.service.ConfigService; +import uk.gov.di.ipv.core.library.service.EvcsService; +import uk.gov.di.ipv.core.library.verifiablecredential.service.SessionCredentialsService; +import uk.gov.di.ipv.core.library.verifiablecredential.service.VerifiableCredentialService; + +import java.time.Instant; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static uk.gov.di.ipv.core.library.auditing.AuditEventTypes.IPV_IDENTITY_STORED; +import static uk.gov.di.ipv.core.library.domain.Cri.EXPERIAN_FRAUD; +import static uk.gov.di.ipv.core.library.domain.ErrorResponse.FAILED_AT_EVCS_HTTP_REQUEST_SEND; +import static uk.gov.di.ipv.core.library.enums.Vot.P0; +import static uk.gov.di.ipv.core.library.enums.Vot.P2; +import static uk.gov.di.ipv.core.library.fixtures.VcFixtures.EXPIRED_M1A_EXPERIAN_FRAUD_VC; +import static uk.gov.di.ipv.core.library.fixtures.VcFixtures.M1A_ADDRESS_VC; +import static uk.gov.di.ipv.core.library.fixtures.VcFixtures.PASSPORT_NON_DCMAW_SUCCESSFUL_VC; + +@ExtendWith(MockitoExtension.class) +class StoreIdentityServiceTest { + private static final String CLIENT_SESSION_ID = "client-session-id"; + private static final String COMPONENT_ID = "https://component-id.example"; + private static final String GOVUK_JOURNEY_ID = "govuk-journey-id"; + private static final String IP_ADDRESS = "1.2.3.4"; + private static final String SESSION_ID = "session-id"; + private static final String USER_ID = "user-id"; + private static final String DEVICE_INFORMATION = "device-information"; + private static final String EVCS_ACCESS_TOKEN = "evcs-access-token"; + private static final List VCS = + List.of( + PASSPORT_NON_DCMAW_SUCCESSFUL_VC, + EXPIRED_M1A_EXPERIAN_FRAUD_VC, + M1A_ADDRESS_VC); + @Spy private static IpvSessionItem ipvSessionItem; + private static ClientOAuthSessionItem clientOAuthSessionItem; + private AuditEventUser testAuditEventUser; + + @Mock ConfigService configService; + @Mock SessionCredentialsService sessionCredentialsService; + @Mock VerifiableCredentialService verifiableCredentialService; + @Mock AuditService auditService; + @Mock EvcsService evcsService; + @InjectMocks StoreIdentityService storeIdentityService; + @Captor private ArgumentCaptor auditEventCaptor; + + @BeforeEach + void setUpEach() { + testAuditEventUser = new AuditEventUser(USER_ID, SESSION_ID, GOVUK_JOURNEY_ID, IP_ADDRESS); + ipvSessionItem.setIpvSessionId(SESSION_ID); + ipvSessionItem.setClientOAuthSessionId(CLIENT_SESSION_ID); + ipvSessionItem.setVot(P2); + + clientOAuthSessionItem = + ClientOAuthSessionItem.builder() + .userId(USER_ID) + .govukSigninJourneyId(GOVUK_JOURNEY_ID) + .evcsAccessToken(EVCS_ACCESS_TOKEN) + .build(); + } + + @Nested + class StoreIdentityServiceSuccessfulStoreTest { + @BeforeEach + void setUpEach() { + when(configService.getParameter(ConfigurationVariable.COMPONENT_ID)) + .thenReturn(COMPONENT_ID); + } + + @Test + void shouldSuccessfullyStoreIdentityWhenEvcsWriteEnabled() throws Exception { + // Arrange + VCS.forEach( + credential -> { + if (credential.getCri().equals(EXPERIAN_FRAUD)) { + credential.setMigrated(null); + } else { + credential.setMigrated(Instant.now()); + } + }); + + // Act + storeIdentityService.storeIdentity( + ipvSessionItem, + clientOAuthSessionItem, + CandidateIdentityType.NEW, + DEVICE_INFORMATION, + VCS, + testAuditEventUser); + + // Assert + verify(evcsService, times(1)).storeCompletedIdentity(anyString(), any(), any()); + } + + @Test + void shouldSendAuditEventWithVotExtensionWhenIdentityAchieved() throws Exception { + // Arrange + VCS.stream() + .map( + credential -> { + if (credential.getCri().equals(EXPERIAN_FRAUD)) { + credential.setMigrated(null); + } else { + credential.setMigrated(Instant.now()); + } + return credential; + }) + .toList(); + + // Act + storeIdentityService.storeIdentity( + ipvSessionItem, + clientOAuthSessionItem, + CandidateIdentityType.NEW, + DEVICE_INFORMATION, + VCS, + testAuditEventUser); + + // Assert + verify(auditService).sendAuditEvent(auditEventCaptor.capture()); + var auditEvent = auditEventCaptor.getValue(); + + assertEquals(IPV_IDENTITY_STORED, auditEvent.getEventName()); + assertEquals( + P2, ((AuditExtensionCandidateIdentityType) auditEvent.getExtensions()).vot()); + assertEquals( + CandidateIdentityType.NEW, + ((AuditExtensionCandidateIdentityType) auditEvent.getExtensions()) + .identityType()); + assertEquals(COMPONENT_ID, auditEvent.getComponentId()); + assertEquals(testAuditEventUser, auditEvent.getUser()); + verify(evcsService, times(1)).storeCompletedIdentity(anyString(), any(), any()); + } + + @Test + void shouldSendAuditEventWithVotAndIdentityTypeExtensionWhenIdentityUpdated() + throws Exception { + // Act + storeIdentityService.storeIdentity( + ipvSessionItem, + clientOAuthSessionItem, + CandidateIdentityType.UPDATE, + DEVICE_INFORMATION, + VCS, + testAuditEventUser); + + // Assert + verify(auditService).sendAuditEvent(auditEventCaptor.capture()); + var auditEvent = auditEventCaptor.getValue(); + + assertEquals(IPV_IDENTITY_STORED, auditEvent.getEventName()); + assertEquals( + P2, ((AuditExtensionCandidateIdentityType) auditEvent.getExtensions()).vot()); + assertEquals( + CandidateIdentityType.UPDATE, + ((AuditExtensionCandidateIdentityType) auditEvent.getExtensions()) + .identityType()); + assertEquals(COMPONENT_ID, auditEvent.getComponentId()); + assertEquals(testAuditEventUser, auditEvent.getUser()); + verify(evcsService, times(1)).storeCompletedIdentity(anyString(), any(), any()); + } + + @Test + void shouldSendAuditEventWithVotAndIdentityTypeExtensionWhenIdentityIncomplete() + throws Exception { + // Arrange + ipvSessionItem.setVot(P0); + + // Act + storeIdentityService.storeIdentity( + ipvSessionItem, + clientOAuthSessionItem, + CandidateIdentityType.NEW, + DEVICE_INFORMATION, + VCS, + testAuditEventUser); + + // Assert + verify(auditService).sendAuditEvent(auditEventCaptor.capture()); + var auditEvent = auditEventCaptor.getValue(); + + assertEquals(IPV_IDENTITY_STORED, auditEvent.getEventName()); + assertNull(((AuditExtensionCandidateIdentityType) auditEvent.getExtensions()).vot()); + assertEquals( + CandidateIdentityType.NEW, + ((AuditExtensionCandidateIdentityType) auditEvent.getExtensions()) + .identityType()); + assertEquals(COMPONENT_ID, auditEvent.getComponentId()); + assertEquals(testAuditEventUser, auditEvent.getUser()); + } + + @Test + void shouldStoreIdentityInEvcsAndSendAuditEventForPendingVc() throws Exception { + // Act + storeIdentityService.storeIdentity( + ipvSessionItem, + clientOAuthSessionItem, + CandidateIdentityType.PENDING, + DEVICE_INFORMATION, + VCS, + testAuditEventUser); + + // Assert + verify(auditService).sendAuditEvent(auditEventCaptor.capture()); + var auditEvent = auditEventCaptor.getValue(); + assertEquals(IPV_IDENTITY_STORED, auditEvent.getEventName()); + assertEquals( + P2, ((AuditExtensionCandidateIdentityType) auditEvent.getExtensions()).vot()); + assertEquals( + CandidateIdentityType.PENDING, + ((AuditExtensionCandidateIdentityType) auditEvent.getExtensions()) + .identityType()); + assertEquals(COMPONENT_ID, auditEvent.getComponentId()); + assertEquals(testAuditEventUser, auditEvent.getUser()); + + verify(evcsService, times(1)) + .storePendingIdentity( + USER_ID, VCS, clientOAuthSessionItem.getEvcsAccessToken()); + } + } + + @Test + void shouldNotReturnAnErrorJourneyIfFailedAtEvcsIdentityStore_forPendingF2f() throws Exception { + // Arrange + doThrow( + new EvcsServiceException( + HTTPResponse.SC_SERVER_ERROR, FAILED_AT_EVCS_HTTP_REQUEST_SEND)) + .when(evcsService) + .storePendingIdentity(USER_ID, VCS, clientOAuthSessionItem.getEvcsAccessToken()); + + // Assert + assertThrows( + EvcsServiceException.class, + () -> + storeIdentityService.storeIdentity( + ipvSessionItem, + clientOAuthSessionItem, + CandidateIdentityType.PENDING, + DEVICE_INFORMATION, + VCS, + testAuditEventUser)); + } +} diff --git a/libs/audit-service/src/main/java/uk/gov/di/ipv/core/library/auditing/extension/AuditExtensionCandidateIdentityType.java b/libs/audit-service/src/main/java/uk/gov/di/ipv/core/library/auditing/extension/AuditExtensionCandidateIdentityType.java new file mode 100644 index 0000000000..6f061d536d --- /dev/null +++ b/libs/audit-service/src/main/java/uk/gov/di/ipv/core/library/auditing/extension/AuditExtensionCandidateIdentityType.java @@ -0,0 +1,12 @@ +package uk.gov.di.ipv.core.library.auditing.extension; + +import com.fasterxml.jackson.annotation.JsonProperty; +import uk.gov.di.ipv.core.library.annotations.ExcludeFromGeneratedCoverageReport; +import uk.gov.di.ipv.core.library.enums.CandidateIdentityType; +import uk.gov.di.ipv.core.library.enums.Vot; + +@ExcludeFromGeneratedCoverageReport +public record AuditExtensionCandidateIdentityType( + @JsonProperty(value = "identity_type", required = true) CandidateIdentityType identityType, + @JsonProperty(required = false) Vot vot) + implements AuditExtensions {} diff --git a/libs/common-services/src/main/java/uk/gov/di/ipv/core/library/domain/ErrorResponse.java b/libs/common-services/src/main/java/uk/gov/di/ipv/core/library/domain/ErrorResponse.java index 11a033c6f6..8949e04283 100644 --- a/libs/common-services/src/main/java/uk/gov/di/ipv/core/library/domain/ErrorResponse.java +++ b/libs/common-services/src/main/java/uk/gov/di/ipv/core/library/domain/ErrorResponse.java @@ -114,7 +114,9 @@ public enum ErrorResponse { 1099, "Failed to parse mobile app callback request body"), CRI_RESPONSE_ITEM_NOT_FOUND(1100, "CRI response item cannot be found"), MISSING_TARGET_VOT(1101, "Target VOT missing from session"), - INVALID_VTR_CLAIM(1102, "Invalid VTR claim"); + INVALID_VTR_CLAIM(1102, "Invalid VTR claim"), + MISSING_PROCESS_IDENTITY_TYPE(1103, "Process identity type missing"), + UNEXPECTED_PROCESS_IDENTITY_TYPE(1104, "Unexpected process identity type"); private static final String ERROR = "error"; private static final String ERROR_DESCRIPTION = "error_description"; diff --git a/libs/common-services/src/main/java/uk/gov/di/ipv/core/library/enums/CandidateIdentityType.java b/libs/common-services/src/main/java/uk/gov/di/ipv/core/library/enums/CandidateIdentityType.java new file mode 100644 index 0000000000..c8883fa074 --- /dev/null +++ b/libs/common-services/src/main/java/uk/gov/di/ipv/core/library/enums/CandidateIdentityType.java @@ -0,0 +1,13 @@ +package uk.gov.di.ipv.core.library.enums; + +import uk.gov.di.ipv.core.library.annotations.ExcludeFromGeneratedCoverageReport; + +@ExcludeFromGeneratedCoverageReport +public enum CandidateIdentityType { + EXISTING, + NEW, + PENDING, + REVERIFICATION, + INCOMPLETE, + UPDATE +} diff --git a/libs/common-services/src/main/java/uk/gov/di/ipv/core/library/exceptions/UnknownProcessIdentityTypeException.java b/libs/common-services/src/main/java/uk/gov/di/ipv/core/library/exceptions/UnknownProcessIdentityTypeException.java new file mode 100644 index 0000000000..0974373f40 --- /dev/null +++ b/libs/common-services/src/main/java/uk/gov/di/ipv/core/library/exceptions/UnknownProcessIdentityTypeException.java @@ -0,0 +1,12 @@ +package uk.gov.di.ipv.core.library.exceptions; + +import lombok.Getter; + +@Getter +public class UnknownProcessIdentityTypeException extends Exception { + private final String processIdentityType; + + public UnknownProcessIdentityTypeException(String checkType) { + this.processIdentityType = checkType; + } +} diff --git a/libs/common-services/src/main/java/uk/gov/di/ipv/core/library/helpers/RequestHelper.java b/libs/common-services/src/main/java/uk/gov/di/ipv/core/library/helpers/RequestHelper.java index 8e61fcb9df..0dc0edc1c8 100644 --- a/libs/common-services/src/main/java/uk/gov/di/ipv/core/library/helpers/RequestHelper.java +++ b/libs/common-services/src/main/java/uk/gov/di/ipv/core/library/helpers/RequestHelper.java @@ -8,12 +8,14 @@ import uk.gov.di.ipv.core.library.domain.ErrorResponse; import uk.gov.di.ipv.core.library.domain.JourneyRequest; import uk.gov.di.ipv.core.library.domain.ProcessRequest; +import uk.gov.di.ipv.core.library.enums.CandidateIdentityType; import uk.gov.di.ipv.core.library.enums.CoiCheckType; import uk.gov.di.ipv.core.library.enums.IdentityType; import uk.gov.di.ipv.core.library.enums.MobileAppJourneyType; import uk.gov.di.ipv.core.library.enums.SessionCredentialsResetType; import uk.gov.di.ipv.core.library.exceptions.HttpResponseExceptionWithErrorBody; import uk.gov.di.ipv.core.library.exceptions.UnknownCoiCheckTypeException; +import uk.gov.di.ipv.core.library.exceptions.UnknownProcessIdentityTypeException; import uk.gov.di.ipv.core.library.exceptions.UnknownResetTypeException; import java.util.Arrays; @@ -26,6 +28,7 @@ import static com.nimbusds.oauth2.sdk.http.HTTPResponse.SC_BAD_REQUEST; import static software.amazon.awssdk.utils.StringUtils.isBlank; import static uk.gov.di.ipv.core.library.domain.ErrorResponse.MISSING_CHECK_TYPE; +import static uk.gov.di.ipv.core.library.domain.ErrorResponse.MISSING_PROCESS_IDENTITY_TYPE; import static uk.gov.di.ipv.core.library.domain.ErrorResponse.MISSING_RESET_TYPE; public class RequestHelper { @@ -188,6 +191,18 @@ public static CoiCheckType getCoiCheckType(ProcessRequest request) } } + public static CandidateIdentityType getProcessIdentityType(ProcessRequest request) + throws HttpResponseExceptionWithErrorBody, UnknownProcessIdentityTypeException { + String checkType = + extractValueFromLambdaInput( + request, "processIdentityType", MISSING_PROCESS_IDENTITY_TYPE); + try { + return CandidateIdentityType.valueOf(checkType); + } catch (IllegalArgumentException e) { + throw new UnknownProcessIdentityTypeException(checkType); + } + } + public static SessionCredentialsResetType getSessionCredentialsResetType(ProcessRequest request) throws HttpResponseExceptionWithErrorBody, UnknownResetTypeException { String resetType = extractValueFromLambdaInput(request, "resetType", MISSING_RESET_TYPE); diff --git a/libs/common-services/src/main/java/uk/gov/di/ipv/core/library/journeys/JourneyUris.java b/libs/common-services/src/main/java/uk/gov/di/ipv/core/library/journeys/JourneyUris.java index 3a0c59c52a..807c873663 100644 --- a/libs/common-services/src/main/java/uk/gov/di/ipv/core/library/journeys/JourneyUris.java +++ b/libs/common-services/src/main/java/uk/gov/di/ipv/core/library/journeys/JourneyUris.java @@ -59,10 +59,13 @@ private JourneyUris() { public static final String JOURNEY_TEMPORARILY_UNAVAILABLE_PATH = "/journey/temporarily-unavailable"; public static final String JOURNEY_UNMET_PATH = "/journey/unmet"; + public static final String JOURNEY_GPG45_UNMET_PATH = "/journey/gpg45-unmet"; public static final String JOURNEY_VCS_NOT_CORRELATED = "/journey/vcs-not-correlated"; public static final String JOURNEY_ABANDON_PATH = "/journey/abandon"; public static final String JOURNEY_DCMAW_ASYNC_VC_RECEIVED_LOW_PATH = "/journey/dcmaw-async-vc-received-low"; public static final String JOURNEY_DCMAW_ASYNC_VC_RECEIVED_MEDIUM_PATH = "/journey/dcmaw-async-vc-received-medium"; + public static final String JOURNEY_PROCESS_CANDIDATE_IDENTITY = + "/journey/process-candidate-identity"; } diff --git a/libs/common-services/src/test/java/uk/gov/di/ipv/core/library/helpers/RequestHelperTest.java b/libs/common-services/src/test/java/uk/gov/di/ipv/core/library/helpers/RequestHelperTest.java index 0d3bfd9f57..7fa0941f89 100644 --- a/libs/common-services/src/test/java/uk/gov/di/ipv/core/library/helpers/RequestHelperTest.java +++ b/libs/common-services/src/test/java/uk/gov/di/ipv/core/library/helpers/RequestHelperTest.java @@ -9,9 +9,11 @@ import uk.gov.di.ipv.core.library.domain.ErrorResponse; import uk.gov.di.ipv.core.library.domain.JourneyRequest; import uk.gov.di.ipv.core.library.domain.ProcessRequest; +import uk.gov.di.ipv.core.library.enums.CandidateIdentityType; import uk.gov.di.ipv.core.library.enums.IdentityType; import uk.gov.di.ipv.core.library.exceptions.HttpResponseExceptionWithErrorBody; import uk.gov.di.ipv.core.library.exceptions.UnknownCoiCheckTypeException; +import uk.gov.di.ipv.core.library.exceptions.UnknownProcessIdentityTypeException; import uk.gov.di.ipv.core.library.exceptions.UnknownResetTypeException; import java.util.Arrays; @@ -29,6 +31,7 @@ import static uk.gov.di.ipv.core.library.domain.Cri.CLAIMED_IDENTITY; import static uk.gov.di.ipv.core.library.domain.Cri.DCMAW; import static uk.gov.di.ipv.core.library.domain.ErrorResponse.MISSING_CHECK_TYPE; +import static uk.gov.di.ipv.core.library.domain.ErrorResponse.MISSING_PROCESS_IDENTITY_TYPE; import static uk.gov.di.ipv.core.library.domain.ErrorResponse.MISSING_RESET_TYPE; import static uk.gov.di.ipv.core.library.enums.CoiCheckType.STANDARD; import static uk.gov.di.ipv.core.library.enums.SessionCredentialsResetType.NAME_ONLY_CHANGE; @@ -576,4 +579,43 @@ void getSessionCredentialsResetTypeShouldThrowIfUnknownResetType() { () -> RequestHelper.getSessionCredentialsResetType(request)); assertEquals("nooo", thrown.getResetType()); } + + @Test + void getProcessIdentityTypeShouldReturnCandidateIdentityIfValid() throws Exception { + ProcessRequest request = + ProcessRequest.processRequestBuilder() + .lambdaInput( + Map.of("processIdentityType", CandidateIdentityType.NEW.name())) + .build(); + + assertEquals(CandidateIdentityType.NEW, RequestHelper.getProcessIdentityType(request)); + } + + @Test + void getProcessIdentityTypeThrowsIfMissingProcessIdentityType() { + ProcessRequest request = + ProcessRequest.processRequestBuilder() + .lambdaInput(Map.of("anotherTypeField", "something-else")) + .build(); + + var thrown = + assertThrows( + HttpResponseExceptionWithErrorBody.class, + () -> RequestHelper.getProcessIdentityType(request)); + assertEquals(MISSING_PROCESS_IDENTITY_TYPE, thrown.getErrorResponse()); + } + + @Test + void getProcessIdentityTypeShouldThrowIfUnknownResetType() { + ProcessRequest request = + ProcessRequest.processRequestBuilder() + .lambdaInput(Map.of("processIdentityType", "some-nonsense")) + .build(); + + var thrown = + assertThrows( + UnknownProcessIdentityTypeException.class, + () -> RequestHelper.getProcessIdentityType(request)); + assertEquals("some-nonsense", thrown.getProcessIdentityType()); + } } diff --git a/libs/cri-storing-service/src/main/java/uk/gov/di/ipv/core/library/cristoringservice/CriStoringService.java b/libs/cri-storing-service/src/main/java/uk/gov/di/ipv/core/library/cristoringservice/CriStoringService.java index 194372a092..6d7a26c01b 100644 --- a/libs/cri-storing-service/src/main/java/uk/gov/di/ipv/core/library/cristoringservice/CriStoringService.java +++ b/libs/cri-storing-service/src/main/java/uk/gov/di/ipv/core/library/cristoringservice/CriStoringService.java @@ -142,7 +142,7 @@ public void storeVcs( ClientOAuthSessionItem clientOAuthSessionItem, IpvSessionItem ipvSessionItem, List sessionVcs) - throws CiPutException, CiPostMitigationsException, VerifiableCredentialException, + throws VerifiableCredentialException, CiPostMitigationsException, CiPutException, UnrecognisedVotException { var userId = clientOAuthSessionItem.getUserId(); var govukSigninJourneyId = clientOAuthSessionItem.getGovukSigninJourneyId(); @@ -151,6 +151,31 @@ public void storeVcs( new AuditEventUser( userId, ipvSessionItem.getIpvSessionId(), govukSigninJourneyId, ipAddress); + storeVcs( + cri, + ipAddress, + deviceInformation, + vcs, + clientOAuthSessionItem, + ipvSessionItem, + sessionVcs, + auditEventUser); + } + + @SuppressWarnings("java:S107") // Methods should not have too many parameters + public void storeVcs( + Cri cri, + String ipAddress, + String deviceInformation, + List vcs, + ClientOAuthSessionItem clientOAuthSessionItem, + IpvSessionItem ipvSessionItem, + List sessionVcs, + AuditEventUser auditEventUser) + throws CiPutException, CiPostMitigationsException, VerifiableCredentialException, + UnrecognisedVotException { + var govukSigninJourneyId = clientOAuthSessionItem.getGovukSigninJourneyId(); + List mitigationVcList = new ArrayList<>(sessionVcs); var scopeClaims = clientOAuthSessionItem.getScopeClaims(); diff --git a/libs/ticf-cri-service/build.gradle b/libs/ticf-cri-service/build.gradle new file mode 100644 index 0000000000..7bac431979 --- /dev/null +++ b/libs/ticf-cri-service/build.gradle @@ -0,0 +1,52 @@ +plugins { + id "java" + id "idea" + id "jacoco" + alias libs.plugins.postCompileWeaving +} + +dependencies { + + implementation libs.bundles.awsLambda, + project(":libs:common-services"), + project(":libs:audit-service"), + project(":libs:verifiable-credentials") + + aspect libs.powertoolsLogging, + libs.powertoolsTracing, + libs.aspectj + + testImplementation libs.hamcrest, + libs.junitJupiter, + libs.mockitoJunit, + libs.pactConsumerJunit, + project(path: ':libs:common-services', configuration: 'tests'), + project(path: ':libs:test-helpers') + + 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 +} + +task pactConsumerTests (type: Test) { + useJUnitPlatform() + include 'uk/gov/di/ipv/core/library/ticfcriservice/pact/**' + systemProperties['pact.rootDir'] = "$rootDir/build/pacts" +} + +jacocoTestReport { + dependsOn test + reports { + xml.required.set(true) + } +} diff --git a/lambdas/call-ticf-cri/src/main/java/uk/gov/di/ipv/core/callticfcri/service/TicfCriService.java b/libs/ticf-cri-service/src/main/java/uk/gov/di/ipv/core/library/ticf/TicfCriService.java similarity index 95% rename from lambdas/call-ticf-cri/src/main/java/uk/gov/di/ipv/core/callticfcri/service/TicfCriService.java rename to libs/ticf-cri-service/src/main/java/uk/gov/di/ipv/core/library/ticf/TicfCriService.java index 156181dff7..a896538f78 100644 --- a/lambdas/call-ticf-cri/src/main/java/uk/gov/di/ipv/core/callticfcri/service/TicfCriService.java +++ b/libs/ticf-cri-service/src/main/java/uk/gov/di/ipv/core/library/ticf/TicfCriService.java @@ -1,13 +1,10 @@ -package uk.gov.di.ipv.core.callticfcri.service; +package uk.gov.di.ipv.core.library.ticf; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import software.amazon.lambda.powertools.tracing.Tracing; -import uk.gov.di.ipv.core.callticfcri.dto.TicfCriDto; -import uk.gov.di.ipv.core.callticfcri.exception.TicfCriHttpResponseException; -import uk.gov.di.ipv.core.callticfcri.exception.TicfCriServiceException; import uk.gov.di.ipv.core.library.annotations.ExcludeFromGeneratedCoverageReport; import uk.gov.di.ipv.core.library.config.ConfigurationVariable; import uk.gov.di.ipv.core.library.domain.ErrorResponse; @@ -17,6 +14,9 @@ import uk.gov.di.ipv.core.library.persistence.item.ClientOAuthSessionItem; import uk.gov.di.ipv.core.library.persistence.item.IpvSessionItem; import uk.gov.di.ipv.core.library.service.ConfigService; +import uk.gov.di.ipv.core.library.ticf.dto.TicfCriDto; +import uk.gov.di.ipv.core.library.ticf.exception.TicfCriHttpResponseException; +import uk.gov.di.ipv.core.library.ticf.exception.TicfCriServiceException; import uk.gov.di.ipv.core.library.tracing.TracingHttpClient; import uk.gov.di.ipv.core.library.verifiablecredential.service.SessionCredentialsService; import uk.gov.di.ipv.core.library.verifiablecredential.validator.VerifiableCredentialValidator; @@ -32,6 +32,7 @@ import static uk.gov.di.ipv.core.library.domain.Cri.TICF; import static uk.gov.di.ipv.core.library.helpers.LogHelper.LogField.LOG_STATUS_CODE; +@SuppressWarnings("java:S107") // Should allow duplicate code for now public class TicfCriService { private static final Logger LOGGER = LogManager.getLogger(); private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); @@ -52,6 +53,7 @@ public TicfCriService(ConfigService configService) { } // Used by contract tests + @ExcludeFromGeneratedCoverageReport public TicfCriService( ConfigService configService, VerifiableCredentialValidator jwtValidator, diff --git a/lambdas/call-ticf-cri/src/main/java/uk/gov/di/ipv/core/callticfcri/dto/TicfCriDto.java b/libs/ticf-cri-service/src/main/java/uk/gov/di/ipv/core/library/ticf/dto/TicfCriDto.java similarity index 90% rename from lambdas/call-ticf-cri/src/main/java/uk/gov/di/ipv/core/callticfcri/dto/TicfCriDto.java rename to libs/ticf-cri-service/src/main/java/uk/gov/di/ipv/core/library/ticf/dto/TicfCriDto.java index 4afac1a289..c2f37eefd6 100644 --- a/lambdas/call-ticf-cri/src/main/java/uk/gov/di/ipv/core/callticfcri/dto/TicfCriDto.java +++ b/libs/ticf-cri-service/src/main/java/uk/gov/di/ipv/core/library/ticf/dto/TicfCriDto.java @@ -1,4 +1,4 @@ -package uk.gov.di.ipv.core.callticfcri.dto; +package uk.gov.di.ipv.core.library.ticf.dto; import com.fasterxml.jackson.annotation.JsonProperty; import uk.gov.di.ipv.core.library.enums.Vot; diff --git a/lambdas/call-ticf-cri/src/main/java/uk/gov/di/ipv/core/callticfcri/exception/TicfCriHttpResponseException.java b/libs/ticf-cri-service/src/main/java/uk/gov/di/ipv/core/library/ticf/exception/TicfCriHttpResponseException.java similarity index 75% rename from lambdas/call-ticf-cri/src/main/java/uk/gov/di/ipv/core/callticfcri/exception/TicfCriHttpResponseException.java rename to libs/ticf-cri-service/src/main/java/uk/gov/di/ipv/core/library/ticf/exception/TicfCriHttpResponseException.java index 6c732d6253..eca2562c32 100644 --- a/lambdas/call-ticf-cri/src/main/java/uk/gov/di/ipv/core/callticfcri/exception/TicfCriHttpResponseException.java +++ b/libs/ticf-cri-service/src/main/java/uk/gov/di/ipv/core/library/ticf/exception/TicfCriHttpResponseException.java @@ -1,4 +1,4 @@ -package uk.gov.di.ipv.core.callticfcri.exception; +package uk.gov.di.ipv.core.library.ticf.exception; public class TicfCriHttpResponseException extends Exception { public TicfCriHttpResponseException(String message) { diff --git a/lambdas/call-ticf-cri/src/main/java/uk/gov/di/ipv/core/callticfcri/exception/TicfCriServiceException.java b/libs/ticf-cri-service/src/main/java/uk/gov/di/ipv/core/library/ticf/exception/TicfCriServiceException.java similarity index 81% rename from lambdas/call-ticf-cri/src/main/java/uk/gov/di/ipv/core/callticfcri/exception/TicfCriServiceException.java rename to libs/ticf-cri-service/src/main/java/uk/gov/di/ipv/core/library/ticf/exception/TicfCriServiceException.java index e9599e63c1..c622717646 100644 --- a/lambdas/call-ticf-cri/src/main/java/uk/gov/di/ipv/core/callticfcri/exception/TicfCriServiceException.java +++ b/libs/ticf-cri-service/src/main/java/uk/gov/di/ipv/core/library/ticf/exception/TicfCriServiceException.java @@ -1,4 +1,4 @@ -package uk.gov.di.ipv.core.callticfcri.exception; +package uk.gov.di.ipv.core.library.ticf.exception; public class TicfCriServiceException extends Exception { public TicfCriServiceException(String message) { diff --git a/lambdas/call-ticf-cri/src/test/java/uk/gov/di/ipv/core/callticfcri/service/TicfCriServiceTest.java b/libs/ticf-cri-service/src/test/java/uk/gov/di/ipv/core/library/ticf/TicfCriServiceTest.java similarity index 97% rename from lambdas/call-ticf-cri/src/test/java/uk/gov/di/ipv/core/callticfcri/service/TicfCriServiceTest.java rename to libs/ticf-cri-service/src/test/java/uk/gov/di/ipv/core/library/ticf/TicfCriServiceTest.java index 60daf4c92b..6d750189bb 100644 --- a/lambdas/call-ticf-cri/src/test/java/uk/gov/di/ipv/core/callticfcri/service/TicfCriServiceTest.java +++ b/libs/ticf-cri-service/src/test/java/uk/gov/di/ipv/core/library/ticf/TicfCriServiceTest.java @@ -1,4 +1,4 @@ -package uk.gov.di.ipv.core.callticfcri.service; +package uk.gov.di.ipv.core.library.ticf; import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.http.HttpStatus; @@ -13,8 +13,6 @@ import org.mockito.Mock; import org.mockito.MockedStatic; import org.mockito.junit.jupiter.MockitoExtension; -import uk.gov.di.ipv.core.callticfcri.dto.TicfCriDto; -import uk.gov.di.ipv.core.callticfcri.exception.TicfCriServiceException; import uk.gov.di.ipv.core.library.domain.ErrorResponse; import uk.gov.di.ipv.core.library.dto.RestCriConfig; import uk.gov.di.ipv.core.library.enums.Vot; @@ -22,6 +20,8 @@ import uk.gov.di.ipv.core.library.persistence.item.ClientOAuthSessionItem; import uk.gov.di.ipv.core.library.persistence.item.IpvSessionItem; import uk.gov.di.ipv.core.library.service.ConfigService; +import uk.gov.di.ipv.core.library.ticf.dto.TicfCriDto; +import uk.gov.di.ipv.core.library.ticf.exception.TicfCriServiceException; import uk.gov.di.ipv.core.library.verifiablecredential.service.SessionCredentialsService; import uk.gov.di.ipv.core.library.verifiablecredential.validator.VerifiableCredentialValidator; @@ -45,14 +45,14 @@ import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import static uk.gov.di.ipv.core.callticfcri.service.TicfCriService.TRUSTMARK; -import static uk.gov.di.ipv.core.callticfcri.service.TicfCriService.X_API_KEY_HEADER; import static uk.gov.di.ipv.core.library.config.ConfigurationVariable.CREDENTIAL_ISSUER_API_KEY; import static uk.gov.di.ipv.core.library.domain.Cri.TICF; import static uk.gov.di.ipv.core.library.domain.ErrorResponse.FAILED_TO_GET_CREDENTIAL; import static uk.gov.di.ipv.core.library.fixtures.TestFixtures.TEST_EC_PUBLIC_JWK; import static uk.gov.di.ipv.core.library.fixtures.VcFixtures.M1B_DCMAW_VC; import static uk.gov.di.ipv.core.library.fixtures.VcFixtures.VC_ADDRESS; +import static uk.gov.di.ipv.core.library.ticf.TicfCriService.TRUSTMARK; +import static uk.gov.di.ipv.core.library.ticf.TicfCriService.X_API_KEY_HEADER; @ExtendWith(MockitoExtension.class) class TicfCriServiceTest { diff --git a/lambdas/call-ticf-cri/src/test/java/uk/gov/di/ipv/core/callticfcri/pact/ticfCri/ContractTest.java b/libs/ticf-cri-service/src/test/java/uk/gov/di/ipv/core/library/ticf/pact/ticfCri/ContractTest.java similarity index 99% rename from lambdas/call-ticf-cri/src/test/java/uk/gov/di/ipv/core/callticfcri/pact/ticfCri/ContractTest.java rename to libs/ticf-cri-service/src/test/java/uk/gov/di/ipv/core/library/ticf/pact/ticfCri/ContractTest.java index 8c811c9bb5..b21b03ae96 100644 --- a/lambdas/call-ticf-cri/src/test/java/uk/gov/di/ipv/core/callticfcri/pact/ticfCri/ContractTest.java +++ b/libs/ticf-cri-service/src/test/java/uk/gov/di/ipv/core/library/ticf/pact/ticfCri/ContractTest.java @@ -1,4 +1,4 @@ -package uk.gov.di.ipv.core.callticfcri.pact.ticfCri; +package uk.gov.di.ipv.core.library.ticf.pact.ticfCri; import au.com.dius.pact.consumer.MockServer; import au.com.dius.pact.consumer.dsl.DslPart; @@ -16,7 +16,6 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import uk.gov.di.ipv.core.callticfcri.service.TicfCriService; import uk.gov.di.ipv.core.library.domain.ContraIndicatorConfig; import uk.gov.di.ipv.core.library.domain.VerifiableCredential; import uk.gov.di.ipv.core.library.dto.RestCriConfig; @@ -26,6 +25,7 @@ import uk.gov.di.ipv.core.library.persistence.item.IpvSessionItem; import uk.gov.di.ipv.core.library.service.ConfigService; import uk.gov.di.ipv.core.library.testhelpers.pact.PactJwtBuilder; +import uk.gov.di.ipv.core.library.ticf.TicfCriService; import uk.gov.di.ipv.core.library.verifiablecredential.service.SessionCredentialsService; import uk.gov.di.ipv.core.library.verifiablecredential.validator.VerifiableCredentialValidator; diff --git a/lambdas/call-ticf-cri/src/test/resources/dvlaVc/body.json b/libs/ticf-cri-service/src/test/resources/dvlaVc/body.json similarity index 100% rename from lambdas/call-ticf-cri/src/test/resources/dvlaVc/body.json rename to libs/ticf-cri-service/src/test/resources/dvlaVc/body.json diff --git a/lambdas/call-ticf-cri/src/test/resources/dvlaVc/header.json b/libs/ticf-cri-service/src/test/resources/dvlaVc/header.json similarity index 100% rename from lambdas/call-ticf-cri/src/test/resources/dvlaVc/header.json rename to libs/ticf-cri-service/src/test/resources/dvlaVc/header.json diff --git a/lambdas/call-ticf-cri/src/test/resources/dvlaVc/signature b/libs/ticf-cri-service/src/test/resources/dvlaVc/signature similarity index 100% rename from lambdas/call-ticf-cri/src/test/resources/dvlaVc/signature rename to libs/ticf-cri-service/src/test/resources/dvlaVc/signature diff --git a/lambdas/call-ticf-cri/src/test/resources/dvlaWithCiVc/body.json b/libs/ticf-cri-service/src/test/resources/dvlaWithCiVc/body.json similarity index 100% rename from lambdas/call-ticf-cri/src/test/resources/dvlaWithCiVc/body.json rename to libs/ticf-cri-service/src/test/resources/dvlaWithCiVc/body.json diff --git a/lambdas/call-ticf-cri/src/test/resources/dvlaWithCiVc/header.json b/libs/ticf-cri-service/src/test/resources/dvlaWithCiVc/header.json similarity index 100% rename from lambdas/call-ticf-cri/src/test/resources/dvlaWithCiVc/header.json rename to libs/ticf-cri-service/src/test/resources/dvlaWithCiVc/header.json diff --git a/lambdas/call-ticf-cri/src/test/resources/dvlaWithCiVc/signature b/libs/ticf-cri-service/src/test/resources/dvlaWithCiVc/signature similarity index 100% rename from lambdas/call-ticf-cri/src/test/resources/dvlaWithCiVc/signature rename to libs/ticf-cri-service/src/test/resources/dvlaWithCiVc/signature diff --git a/lambdas/call-ticf-cri/src/test/resources/passportVc/body.json b/libs/ticf-cri-service/src/test/resources/passportVc/body.json similarity index 100% rename from lambdas/call-ticf-cri/src/test/resources/passportVc/body.json rename to libs/ticf-cri-service/src/test/resources/passportVc/body.json diff --git a/lambdas/call-ticf-cri/src/test/resources/passportVc/header.json b/libs/ticf-cri-service/src/test/resources/passportVc/header.json similarity index 100% rename from lambdas/call-ticf-cri/src/test/resources/passportVc/header.json rename to libs/ticf-cri-service/src/test/resources/passportVc/header.json diff --git a/lambdas/call-ticf-cri/src/test/resources/passportVc/signature b/libs/ticf-cri-service/src/test/resources/passportVc/signature similarity index 100% rename from lambdas/call-ticf-cri/src/test/resources/passportVc/signature rename to libs/ticf-cri-service/src/test/resources/passportVc/signature diff --git a/lambdas/call-ticf-cri/src/test/resources/ticfVc/empty/body.json b/libs/ticf-cri-service/src/test/resources/ticfVc/empty/body.json similarity index 100% rename from lambdas/call-ticf-cri/src/test/resources/ticfVc/empty/body.json rename to libs/ticf-cri-service/src/test/resources/ticfVc/empty/body.json diff --git a/lambdas/call-ticf-cri/src/test/resources/ticfVc/empty/header.json b/libs/ticf-cri-service/src/test/resources/ticfVc/empty/header.json similarity index 100% rename from lambdas/call-ticf-cri/src/test/resources/ticfVc/empty/header.json rename to libs/ticf-cri-service/src/test/resources/ticfVc/empty/header.json diff --git a/lambdas/call-ticf-cri/src/test/resources/ticfVc/empty/signature b/libs/ticf-cri-service/src/test/resources/ticfVc/empty/signature similarity index 100% rename from lambdas/call-ticf-cri/src/test/resources/ticfVc/empty/signature rename to libs/ticf-cri-service/src/test/resources/ticfVc/empty/signature diff --git a/lambdas/call-ticf-cri/src/test/resources/ticfVc/intervention/body.json b/libs/ticf-cri-service/src/test/resources/ticfVc/intervention/body.json similarity index 100% rename from lambdas/call-ticf-cri/src/test/resources/ticfVc/intervention/body.json rename to libs/ticf-cri-service/src/test/resources/ticfVc/intervention/body.json diff --git a/lambdas/call-ticf-cri/src/test/resources/ticfVc/intervention/header.json b/libs/ticf-cri-service/src/test/resources/ticfVc/intervention/header.json similarity index 100% rename from lambdas/call-ticf-cri/src/test/resources/ticfVc/intervention/header.json rename to libs/ticf-cri-service/src/test/resources/ticfVc/intervention/header.json diff --git a/lambdas/call-ticf-cri/src/test/resources/ticfVc/intervention/signature b/libs/ticf-cri-service/src/test/resources/ticfVc/intervention/signature similarity index 100% rename from lambdas/call-ticf-cri/src/test/resources/ticfVc/intervention/signature rename to libs/ticf-cri-service/src/test/resources/ticfVc/intervention/signature diff --git a/lambdas/call-ticf-cri/src/test/resources/ticfVc/noIntervention/body.json b/libs/ticf-cri-service/src/test/resources/ticfVc/noIntervention/body.json similarity index 100% rename from lambdas/call-ticf-cri/src/test/resources/ticfVc/noIntervention/body.json rename to libs/ticf-cri-service/src/test/resources/ticfVc/noIntervention/body.json diff --git a/lambdas/call-ticf-cri/src/test/resources/ticfVc/noIntervention/header.json b/libs/ticf-cri-service/src/test/resources/ticfVc/noIntervention/header.json similarity index 100% rename from lambdas/call-ticf-cri/src/test/resources/ticfVc/noIntervention/header.json rename to libs/ticf-cri-service/src/test/resources/ticfVc/noIntervention/header.json diff --git a/lambdas/call-ticf-cri/src/test/resources/ticfVc/noIntervention/signature b/libs/ticf-cri-service/src/test/resources/ticfVc/noIntervention/signature similarity index 100% rename from lambdas/call-ticf-cri/src/test/resources/ticfVc/noIntervention/signature rename to libs/ticf-cri-service/src/test/resources/ticfVc/noIntervention/signature diff --git a/lambdas/call-ticf-cri/src/test/resources/ticfVc/noInterventionWithWarnings/body.json b/libs/ticf-cri-service/src/test/resources/ticfVc/noInterventionWithWarnings/body.json similarity index 100% rename from lambdas/call-ticf-cri/src/test/resources/ticfVc/noInterventionWithWarnings/body.json rename to libs/ticf-cri-service/src/test/resources/ticfVc/noInterventionWithWarnings/body.json diff --git a/lambdas/call-ticf-cri/src/test/resources/ticfVc/noInterventionWithWarnings/header.json b/libs/ticf-cri-service/src/test/resources/ticfVc/noInterventionWithWarnings/header.json similarity index 100% rename from lambdas/call-ticf-cri/src/test/resources/ticfVc/noInterventionWithWarnings/header.json rename to libs/ticf-cri-service/src/test/resources/ticfVc/noInterventionWithWarnings/header.json diff --git a/lambdas/call-ticf-cri/src/test/resources/ticfVc/noInterventionWithWarnings/signature b/libs/ticf-cri-service/src/test/resources/ticfVc/noInterventionWithWarnings/signature similarity index 100% rename from lambdas/call-ticf-cri/src/test/resources/ticfVc/noInterventionWithWarnings/signature rename to libs/ticf-cri-service/src/test/resources/ticfVc/noInterventionWithWarnings/signature diff --git a/local-running/build.gradle b/local-running/build.gradle index 0a34c2b2e4..61db1b4e0f 100644 --- a/local-running/build.gradle +++ b/local-running/build.gradle @@ -28,6 +28,7 @@ dependencies { project(":lambdas:initialise-ipv-session"), project(":lambdas:issue-client-access-token"), project(":lambdas:process-async-cri-credential"), + project(":lambdas:process-candidate-identity"), project(":lambdas:process-cri-callback"), project(":lambdas:process-journey-event"), project(":lambdas:process-mobile-app-callback"), @@ -41,7 +42,8 @@ dependencies { project(":libs:gpg45-evaluator"), project(":libs:user-identity-service"), project(":libs:verifiable-credentials"), - project(":libs:oauth-key-service") + project(":libs:oauth-key-service"), + project(":libs:ticf-cri-service") } java { diff --git a/local-running/core.local.params.yaml b/local-running/core.local.params.yaml index 3dd05a8516..327e8f02db 100644 --- a/local-running/core.local.params.yaml +++ b/local-running/core.local.params.yaml @@ -389,7 +389,11 @@ core: sqsAsync: true kidJarHeaderEnabled: true drivingLicenceAuthCheck: false + processCandidateIdentity: false features: + processCandidateIdentity: + featureFlags: + processCandidateIdentity: true internationalAddress: featureFlags: internationalAddressEnabled: true diff --git a/local-running/src/main/java/uk/gov/di/ipv/coreback/handlers/JourneyEngineHandler.java b/local-running/src/main/java/uk/gov/di/ipv/coreback/handlers/JourneyEngineHandler.java index 4a7c5a1d63..4d6bd900ae 100644 --- a/local-running/src/main/java/uk/gov/di/ipv/coreback/handlers/JourneyEngineHandler.java +++ b/local-running/src/main/java/uk/gov/di/ipv/coreback/handlers/JourneyEngineHandler.java @@ -14,6 +14,7 @@ import uk.gov.di.ipv.core.library.domain.JourneyRequest; import uk.gov.di.ipv.core.library.domain.ProcessRequest; import uk.gov.di.ipv.core.library.service.YamlConfigService; +import uk.gov.di.ipv.core.processcandidateidentity.ProcessCandidateIdentityHandler; import uk.gov.di.ipv.core.processjourneyevent.ProcessJourneyEventHandler; import uk.gov.di.ipv.core.resetsessionidentity.ResetSessionIdentityHandler; import uk.gov.di.ipv.core.storeidentity.StoreIdentityHandler; @@ -31,6 +32,7 @@ import static uk.gov.di.ipv.core.library.journeys.JourneyUris.JOURNEY_CHECK_GPG45_SCORE_PATH; import static uk.gov.di.ipv.core.library.journeys.JourneyUris.JOURNEY_CHECK_REVERIFICATION_IDENTITY_PATH; import static uk.gov.di.ipv.core.library.journeys.JourneyUris.JOURNEY_EVALUATE_GPG45_SCORES_PATH; +import static uk.gov.di.ipv.core.library.journeys.JourneyUris.JOURNEY_PROCESS_CANDIDATE_IDENTITY; import static uk.gov.di.ipv.core.library.journeys.JourneyUris.JOURNEY_RESET_SESSION_IDENTITY_PATH; import static uk.gov.di.ipv.core.library.journeys.JourneyUris.JOURNEY_STORE_IDENTITY_PATH; @@ -58,6 +60,7 @@ public class JourneyEngineHandler { private final StoreIdentityHandler storeIdentityHandler; private final CheckCoiHandler checkCoiHandler; private final CheckReverificationIdentityHandler checkReverificationIdentityHandler; + private final ProcessCandidateIdentityHandler processCandidateIdentityHandler; public JourneyEngineHandler() throws IOException { this.configService = new YamlConfigService(); @@ -74,6 +77,7 @@ public JourneyEngineHandler() throws IOException { this.checkCoiHandler = new CheckCoiHandler(configService); this.checkReverificationIdentityHandler = new CheckReverificationIdentityHandler(configService); + this.processCandidateIdentityHandler = new ProcessCandidateIdentityHandler(configService); } public void journeyEngine(Context ctx) { @@ -133,6 +137,9 @@ private Map processJourneyStep( case JOURNEY_CHECK_REVERIFICATION_IDENTITY_PATH -> checkReverificationIdentityHandler .handleRequest( buildProcessRequest(ctx, processJourneyEventOutput), EMPTY_CONTEXT); + case JOURNEY_PROCESS_CANDIDATE_IDENTITY -> processCandidateIdentityHandler + .handleRequest( + buildProcessRequest(ctx, processJourneyEventOutput), EMPTY_CONTEXT); default -> { if (journeyStep.matches("/journey/cri/build-oauth-request/.*")) { yield buildCriOauthRequestHandler.handleRequest( diff --git a/settings.gradle b/settings.gradle index 1b7d8b7170..74f145dc2e 100644 --- a/settings.gradle +++ b/settings.gradle @@ -23,6 +23,7 @@ include "lambdas", "lambdas:initialise-ipv-session", "lambdas:issue-client-access-token", "lambdas:process-async-cri-credential", + "lambdas:process-candidate-identity", "lambdas:process-cri-callback", "lambdas:process-journey-event", "lambdas:reset-session-identity", @@ -43,6 +44,7 @@ include "lambdas", "libs:user-identity-service", "libs:verifiable-credentials", "libs:oauth-key-service", + "libs:ticf-cri-service", "local-running" dependencyResolutionManagement {