diff --git a/irs-api/src/main/java/org/eclipse/tractusx/irs/aaswrapper/job/delegate/AbstractDelegate.java b/irs-api/src/main/java/org/eclipse/tractusx/irs/aaswrapper/job/delegate/AbstractDelegate.java index e78a394b29..8f8b4cb608 100644 --- a/irs-api/src/main/java/org/eclipse/tractusx/irs/aaswrapper/job/delegate/AbstractDelegate.java +++ b/irs-api/src/main/java/org/eclipse/tractusx/irs/aaswrapper/job/delegate/AbstractDelegate.java @@ -109,6 +109,7 @@ protected SubmodelDescriptor requestSubmodel(final EdcSubmodelFacade submodelFac private SubmodelDescriptor getSubmodel(final EdcSubmodelFacade submodelFacade, final Endpoint digitalTwinRegistryEndpoint, final List connectorEndpoints, final String bpn) throws EdcClientException { + for (final String connectorEndpoint : connectorEndpoints) { try { return submodelFacade.getSubmodelPayload(connectorEndpoint, @@ -118,6 +119,7 @@ private SubmodelDescriptor getSubmodel(final EdcSubmodelFacade submodelFacade, log.info("EdcClientException while accessing digitalTwinRegistryEndpoint '{}'", connectorEndpoint, e); } } + throw new EdcClientException( String.format("Called %s connectorEndpoints but did not get any submodels. Connectors: '%s'", connectorEndpoints.size(), String.join(", ", connectorEndpoints))); diff --git a/irs-api/src/main/java/org/eclipse/tractusx/irs/aaswrapper/job/delegate/DigitalTwinDelegate.java b/irs-api/src/main/java/org/eclipse/tractusx/irs/aaswrapper/job/delegate/DigitalTwinDelegate.java index 55f5fc4132..97a4ea6ddd 100644 --- a/irs-api/src/main/java/org/eclipse/tractusx/irs/aaswrapper/job/delegate/DigitalTwinDelegate.java +++ b/irs-api/src/main/java/org/eclipse/tractusx/irs/aaswrapper/job/delegate/DigitalTwinDelegate.java @@ -42,6 +42,7 @@ import org.eclipse.tractusx.irs.registryclient.DigitalTwinRegistryKey; import org.eclipse.tractusx.irs.registryclient.DigitalTwinRegistryService; import org.eclipse.tractusx.irs.registryclient.exceptions.RegistryServiceException; +import org.eclipse.tractusx.irs.registryclient.exceptions.ShellNotFoundException; /** * Retrieves AAShell from Digital Twin Registry service and storing it inside {@link ItemContainer}. @@ -72,22 +73,20 @@ public ItemContainer process(final ItemContainer.ItemContainerBuilder itemContai final var dtrKeys = List.of(new DigitalTwinRegistryKey(itemId.getGlobalAssetId(), itemId.getBpn())); final var shells = digitalTwinRegistryService.fetchShells(dtrKeys); final var shell = shells.stream() - // we use findFirst here, because we query only for one - // DigitalTwinRegistryKey here - .map(Either::getOrNull) - .filter(Objects::nonNull) - .findFirst() - .orElseThrow(() -> shellNotFound(shells)); + // we use findFirst here, because we query only for one + // DigitalTwinRegistryKey here + .map(Either::getOrNull) + .filter(Objects::nonNull) + .findFirst() + .orElseThrow(() -> shellNotFound(shells)); itemContainerBuilder.shell( jobData.isAuditContractNegotiation() ? shell : shell.withoutContractAgreementId()); + } catch (final RegistryServiceException | RuntimeException e) { // catching generic exception is intended here, // otherwise Jobs stay in state RUNNING forever - log.info("Shell Endpoint could not be retrieved for Item: {}. Creating Tombstone.", itemId); - itemContainerBuilder.tombstone( - Tombstone.from(itemId.getGlobalAssetId(), null, e, e.getSuppressed(), retryCount, - ProcessStep.DIGITAL_TWIN_REQUEST)); + createShellEndpointCouldNotBeRetrievedTombstone(itemContainerBuilder, itemId, e); } if (expectedDepthOfTreeIsNotReached(jobData.getDepth(), aasTransferProcess.getDepth())) { @@ -98,7 +97,38 @@ public ItemContainer process(final ItemContainer.ItemContainerBuilder itemContai return itemContainerBuilder.build(); } - private Tombstone createNoBpnProvidedTombstone(final JobParameter jobData, final PartChainIdentificationKey itemId) { + private void createShellEndpointCouldNotBeRetrievedTombstone( + final ItemContainer.ItemContainerBuilder itemContainerBuilder, final PartChainIdentificationKey itemId, + final Exception exception) { + + // TODO (mfischer) is this log message and method name correct? + log.info("Shell Endpoint could not be retrieved for Item: {}. Creating Tombstone.", itemId); + log.debug(exception.getMessage(), exception); + + final List rootErrorMessages = Tombstone.getRootErrorMessages(exception.getSuppressed()); + final ProcessingError error = ProcessingError.builder() + .withProcessStep(ProcessStep.DIGITAL_TWIN_REQUEST) + .withRetryCounterAndLastAttemptNow(retryCount) + .withErrorDetail(exception.getMessage()) + .withRootCauses(rootErrorMessages) + .build(); + + String endpointURL = null; + if (exception instanceof ShellNotFoundException) { + endpointURL = String.join("; ", ((ShellNotFoundException) exception).getCalledEndpoints()); + } + + final Tombstone tombstone = Tombstone.builder() + .endpointURL(endpointURL) + .catenaXId(itemId.getGlobalAssetId()) + .processingError(error) + .businessPartnerNumber(itemId.getBpn()) + .build(); + itemContainerBuilder.tombstone(tombstone); + } + + private Tombstone createNoBpnProvidedTombstone(final JobParameter jobData, + final PartChainIdentificationKey itemId) { log.warn("Could not process item with id {} because no BPN was provided. Creating Tombstone.", itemId.getGlobalAssetId()); diff --git a/irs-api/src/main/java/org/eclipse/tractusx/irs/aaswrapper/job/delegate/RelationshipDelegate.java b/irs-api/src/main/java/org/eclipse/tractusx/irs/aaswrapper/job/delegate/RelationshipDelegate.java index f45dca48e2..1516a3c571 100644 --- a/irs-api/src/main/java/org/eclipse/tractusx/irs/aaswrapper/job/delegate/RelationshipDelegate.java +++ b/irs-api/src/main/java/org/eclipse/tractusx/irs/aaswrapper/job/delegate/RelationshipDelegate.java @@ -24,6 +24,7 @@ package org.eclipse.tractusx.irs.aaswrapper.job.delegate; import java.util.List; +import java.util.Map; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; @@ -32,6 +33,7 @@ import org.eclipse.tractusx.irs.component.Bpn; import org.eclipse.tractusx.irs.component.JobParameter; import org.eclipse.tractusx.irs.component.PartChainIdentificationKey; +import org.eclipse.tractusx.irs.component.ProcessingError; import org.eclipse.tractusx.irs.component.Relationship; import org.eclipse.tractusx.irs.component.Tombstone; import org.eclipse.tractusx.irs.component.assetadministrationshell.Endpoint; @@ -41,6 +43,7 @@ import org.eclipse.tractusx.irs.data.JsonParseException; import org.eclipse.tractusx.irs.edc.client.EdcSubmodelFacade; import org.eclipse.tractusx.irs.edc.client.exceptions.EdcClientException; +import org.eclipse.tractusx.irs.edc.client.exceptions.PolicyException; import org.eclipse.tractusx.irs.edc.client.exceptions.UsagePolicyExpiredException; import org.eclipse.tractusx.irs.edc.client.exceptions.UsagePolicyPermissionException; import org.eclipse.tractusx.irs.edc.client.relationships.RelationshipAspect; @@ -94,9 +97,7 @@ private void processEndpoint(final Endpoint endpoint, final RelationshipAspect r if (StringUtils.isBlank(itemId.getBpn())) { log.warn("Could not process item with id {} because no BPN was provided. Creating Tombstone.", itemId.getGlobalAssetId()); - itemContainerBuilder.tombstone( - Tombstone.from(itemId.getGlobalAssetId(), endpoint.getProtocolInformation().getHref(), - "Can't get relationship without a BPN", retryCount, ProcessStep.SUBMODEL_REQUEST)); + itemContainerBuilder.tombstone(createNoBpnProvidedTombstone(endpoint, itemId)); return; } @@ -111,30 +112,77 @@ private void processEndpoint(final Endpoint endpoint, final RelationshipAspect r relationshipAspect.getDirection()); log.info("Processing Relationships with {} items", idsToProcess.size()); - aasTransferProcess.addIdsToProcess(idsToProcess); itemContainerBuilder.relationships(relationships); itemContainerBuilder.bpns(getBpnsFrom(relationships)); + } catch (final UsagePolicyPermissionException | UsagePolicyExpiredException e) { log.info("Encountered usage policy exception: {}. Creating Tombstone.", e.getMessage()); - itemContainerBuilder.tombstone( - Tombstone.from(itemId.getGlobalAssetId(), endpoint.getProtocolInformation().getHref(), e, 0, - ProcessStep.USAGE_POLICY_VALIDATION, e.getBusinessPartnerNumber(), - jsonUtil.asMap(e.getPolicy()))); + final Tombstone tombstone = createPolicyTombstone(endpoint, itemId, e); + itemContainerBuilder.tombstone(tombstone); + } catch (final EdcClientException e) { log.info("Submodel Endpoint could not be retrieved for Endpoint: {}. Creating Tombstone.", endpoint.getProtocolInformation().getHref()); - itemContainerBuilder.tombstone( - Tombstone.from(itemId.getGlobalAssetId(), endpoint.getProtocolInformation().getHref(), e, 0, - ProcessStep.SUBMODEL_REQUEST)); + final Tombstone tombstone = createEdcClientExceptionTombstone(endpoint, itemId, e); + itemContainerBuilder.tombstone(tombstone); + } catch (final JsonParseException e) { log.info("Submodel payload did not match the expected AspectType. Creating Tombstone."); - itemContainerBuilder.tombstone( - Tombstone.from(itemId.getGlobalAssetId(), endpoint.getProtocolInformation().getHref(), e, 0, - ProcessStep.SUBMODEL_REQUEST)); + final Tombstone tombstone = createJsonParseSubmodelPayloadTombstone(endpoint, itemId, e); + itemContainerBuilder.tombstone(tombstone); } } + private Tombstone createNoBpnProvidedTombstone(final Endpoint endpoint, final PartChainIdentificationKey itemId) { + final ProcessingError error = createProcessingError(ProcessStep.SUBMODEL_REQUEST, retryCount, + "Can't get relationship without a BPN"); + return createTombstone(endpoint.getProtocolInformation().getHref(), itemId.getGlobalAssetId(), error, + itemId.getBpn()); + } + + private Tombstone createPolicyTombstone(final Endpoint endpoint, final PartChainIdentificationKey itemId, + final PolicyException exception) { + final Map policy = jsonUtil.asMap(exception.getPolicy()); + final ProcessingError error = createProcessingError(ProcessStep.USAGE_POLICY_VALIDATION, 0, + exception.getMessage()); + return createTombstone(endpoint.getProtocolInformation().getHref(), itemId.getGlobalAssetId(), error, + exception.getBusinessPartnerNumber()).toBuilder().policy(policy).build(); + } + + private Tombstone createEdcClientExceptionTombstone(final Endpoint endpoint, + final PartChainIdentificationKey itemId, final EdcClientException exception) { + final ProcessingError error = createProcessingError(ProcessStep.SUBMODEL_REQUEST, 0, exception.getMessage()); + return createTombstone(endpoint.getProtocolInformation().getHref(), itemId.getGlobalAssetId(), error, + itemId.getBpn()); + } + + private Tombstone createJsonParseSubmodelPayloadTombstone(final Endpoint endpoint, + final PartChainIdentificationKey itemId, final JsonParseException exception) { + final ProcessingError error = createProcessingError(ProcessStep.SUBMODEL_REQUEST, 0, exception.getMessage()); + return createTombstone(endpoint.getProtocolInformation().getHref(), itemId.getGlobalAssetId(), error, + itemId.getBpn()); + } + + private ProcessingError createProcessingError(final ProcessStep processStep, final int retryCount, + final String exception) { + return ProcessingError.builder() + .withProcessStep(processStep) + .withRetryCounterAndLastAttemptNow(retryCount) + .withErrorDetail(exception) + .build(); + } + + private Tombstone createTombstone(final String endpointURL, final String globalAssetId, final ProcessingError error, + final String bpn) { + return Tombstone.builder() + .endpointURL(endpointURL) + .catenaXId(globalAssetId) + .processingError(error) + .businessPartnerNumber(bpn) + .build(); + } + private static List getBpnsFrom(final List relationships) { return relationships.stream() .map(Relationship::getBpn) diff --git a/irs-api/src/main/java/org/eclipse/tractusx/irs/aaswrapper/job/delegate/SubmodelDelegate.java b/irs-api/src/main/java/org/eclipse/tractusx/irs/aaswrapper/job/delegate/SubmodelDelegate.java index ca92b2f799..8a89e63c78 100644 --- a/irs-api/src/main/java/org/eclipse/tractusx/irs/aaswrapper/job/delegate/SubmodelDelegate.java +++ b/irs-api/src/main/java/org/eclipse/tractusx/irs/aaswrapper/job/delegate/SubmodelDelegate.java @@ -27,13 +27,13 @@ import java.util.List; import java.util.Map; -import io.github.resilience4j.retry.RetryRegistry; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.eclipse.tractusx.irs.aaswrapper.job.AASTransferProcess; import org.eclipse.tractusx.irs.aaswrapper.job.ItemContainer; import org.eclipse.tractusx.irs.component.JobParameter; import org.eclipse.tractusx.irs.component.PartChainIdentificationKey; +import org.eclipse.tractusx.irs.component.ProcessingError; import org.eclipse.tractusx.irs.component.Submodel; import org.eclipse.tractusx.irs.component.Tombstone; import org.eclipse.tractusx.irs.component.assetadministrationshell.SubmodelDescriptor; @@ -107,17 +107,21 @@ public ItemContainer process(final ItemContainer.ItemContainerBuilder itemContai private List getSubmodels(final SubmodelDescriptor submodelDescriptor, final ItemContainer.ItemContainerBuilder itemContainerBuilder, final String itemId, final String bpn, final boolean auditContractNegotiation) { + final List submodels = new ArrayList<>(); submodelDescriptor.getEndpoints().forEach(endpoint -> { + final String endpointURL = endpoint.getProtocolInformation().getHref(); if (StringUtils.isBlank(bpn)) { log.warn("Could not process item with id {} because no BPN was provided. Creating Tombstone.", itemId); - itemContainerBuilder.tombstone(Tombstone.from(itemId, endpoint.getProtocolInformation().getHref(), - "Can't get submodel without a BPN", retryCount, ProcessStep.SUBMODEL_REQUEST)); + final ProcessingError error = createProcessingError(ProcessStep.SUBMODEL_REQUEST, retryCount, + "Can't get submodel without a BPN"); + itemContainerBuilder.tombstone(createTombstone(itemId, null, endpointURL, error)); return; } try { + final String jsonSchema = semanticsHubFacade.getModelJsonSchema(submodelDescriptor.getAspectType()); final org.eclipse.tractusx.irs.edc.client.model.SubmodelDescriptor submodel = requestSubmodel( submodelFacade, connectorEndpointsService, endpoint, bpn); @@ -129,34 +133,71 @@ private List getSubmodels(final SubmodelDescriptor submodelDescriptor, if (validationResult.isValid()) { submodels.add(Submodel.from(submodelDescriptor.getId(), submodelDescriptor.getAspectType(), contractAgreementId, jsonUtil.fromString(submodelRawPayload, Map.class))); + } else { - final String errors = String.join(", ", validationResult.getValidationErrors()); - itemContainerBuilder.tombstone(Tombstone.from(itemId, endpoint.getProtocolInformation().getHref(), - new IllegalArgumentException("Submodel payload validation failed. " + errors), 0, - ProcessStep.SCHEMA_VALIDATION)); + final String errorDetail = "Submodel payload validation failed. %s".formatted( + String.join(", ", validationResult.getValidationErrors())); + final ProcessingError error = createProcessingError(ProcessStep.SCHEMA_VALIDATION, 0, errorDetail); + final Tombstone tombstone = createTombstone(itemId, bpn, endpointURL, error); + itemContainerBuilder.tombstone(tombstone); } + } catch (final JsonParseException e) { - itemContainerBuilder.tombstone(Tombstone.from(itemId, endpoint.getProtocolInformation().getHref(), e, - RetryRegistry.ofDefaults().getDefaultConfig().getMaxAttempts(), ProcessStep.SCHEMA_VALIDATION)); log.info("Submodel payload did not match the expected AspectType. Creating Tombstone."); + final ProcessingError error = createProcessingError(ProcessStep.SCHEMA_VALIDATION, retryCount, + e.getMessage()); + final Tombstone tombstone = createTombstone(itemId, bpn, endpointURL, error); + itemContainerBuilder.tombstone(tombstone); + } catch (final SchemaNotFoundException | InvalidSchemaException | RestClientException e) { - itemContainerBuilder.tombstone(Tombstone.from(itemId, endpoint.getProtocolInformation().getHref(), e, 0, - ProcessStep.SCHEMA_REQUEST)); log.info("Cannot load JSON schema for validation. Creating Tombstone."); + final ProcessingError error = createProcessingError(ProcessStep.SCHEMA_REQUEST, 0, e.getMessage()); + itemContainerBuilder.tombstone(createTombstone(itemId, bpn, endpointURL, error)); + } catch (final UsagePolicyPermissionException | UsagePolicyExpiredException e) { log.info("Encountered usage policy permission exception: {}. Creating Tombstone.", e.getMessage()); - itemContainerBuilder.tombstone(Tombstone.from(itemId, endpoint.getProtocolInformation().getHref(), e, 0, - ProcessStep.USAGE_POLICY_VALIDATION, e.getBusinessPartnerNumber(), - jsonUtil.asMap(e.getPolicy()))); + final Map policy = jsonUtil.asMap(e.getPolicy()); + final ProcessingError error = createProcessingError(ProcessStep.USAGE_POLICY_VALIDATION, 0, + e.getMessage()); + final Tombstone tombstone = Tombstone.builder() + .endpointURL(endpointURL) + .catenaXId(itemId) + .processingError(error) + .businessPartnerNumber(e.getBusinessPartnerNumber()) + .policy(policy) + .build(); + itemContainerBuilder.tombstone(tombstone); + } catch (final EdcClientException e) { log.info("Submodel Endpoint could not be retrieved for Item: {}. Creating Tombstone.", itemId); - itemContainerBuilder.tombstone(Tombstone.from(itemId, endpoint.getProtocolInformation().getHref(), e, 0, - ProcessStep.SUBMODEL_REQUEST)); + final ProcessingError error = createProcessingError(ProcessStep.SUBMODEL_REQUEST, 0, e.getMessage()); + final Tombstone tombstone = createTombstone(itemId, bpn, endpointURL, error); + itemContainerBuilder.tombstone(tombstone); } }); + return submodels; } + private Tombstone createTombstone(final String itemId, final String bpn, final String endpointURL, + final ProcessingError error) { + return Tombstone.builder() + .endpointURL(endpointURL) + .catenaXId(itemId) + .processingError(error) + .businessPartnerNumber(bpn) + .build(); + } + + private ProcessingError createProcessingError(final ProcessStep processStep, final int retryCount, + final String errorDetail) { + return ProcessingError.builder() + .withProcessStep(processStep) + .withRetryCounterAndLastAttemptNow(retryCount) + .withErrorDetail(errorDetail) + .build(); + } + @Nullable private String getContractAgreementId(final boolean auditContractNegotiation, final org.eclipse.tractusx.irs.edc.client.model.SubmodelDescriptor submodel) { diff --git a/irs-api/src/main/java/org/eclipse/tractusx/irs/configuration/RegistryConfiguration.java b/irs-api/src/main/java/org/eclipse/tractusx/irs/configuration/RegistryConfiguration.java index 032758c0ab..ec10c41d4a 100644 --- a/irs-api/src/main/java/org/eclipse/tractusx/irs/configuration/RegistryConfiguration.java +++ b/irs-api/src/main/java/org/eclipse/tractusx/irs/configuration/RegistryConfiguration.java @@ -80,7 +80,7 @@ public DecentralDigitalTwinRegistryService decentralDigitalTwinRegistryService( try { return facade.getEndpointReferencesForRegistryAsset(edcConnectorEndpoint, bpn); } catch (EdcClientException e) { - throw new EdcRetrieverException(e); + throw new EdcRetrieverException.Builder(e).withEdcUrl(edcConnectorEndpoint).withBpn(bpn).build(); } }; diff --git a/irs-api/src/main/java/org/eclipse/tractusx/irs/ess/bpn/validation/IncidentValidation.java b/irs-api/src/main/java/org/eclipse/tractusx/irs/ess/bpn/validation/IncidentValidation.java index a8d30d1af6..9842f7b7fc 100644 --- a/irs-api/src/main/java/org/eclipse/tractusx/irs/ess/bpn/validation/IncidentValidation.java +++ b/irs-api/src/main/java/org/eclipse/tractusx/irs/ess/bpn/validation/IncidentValidation.java @@ -23,11 +23,16 @@ ********************************************************************************/ package org.eclipse.tractusx.irs.ess.bpn.validation; +import java.util.Optional; import java.util.UUID; import lombok.extern.slf4j.Slf4j; +import org.eclipse.tractusx.irs.component.Job; +import org.eclipse.tractusx.irs.component.JobParameter; import org.eclipse.tractusx.irs.component.Jobs; +import org.eclipse.tractusx.irs.component.ProcessingError; import org.eclipse.tractusx.irs.component.Tombstone; +import org.eclipse.tractusx.irs.component.Tombstone.TombstoneBuilder; import org.eclipse.tractusx.irs.component.enums.AspectType; import org.eclipse.tractusx.irs.component.enums.ProcessStep; import org.eclipse.tractusx.irs.component.partasplanned.PartAsPlanned; @@ -55,12 +60,14 @@ private IncidentValidation() { */ public static InvestigationResult getResult(final BpnInvestigationJob investigationJob, final Jobs job, final UUID completedJobId) { + SupplyChainImpacted partAsPlannedValidity; Jobs completedJob = job; try { partAsPlannedValidity = validatePartAsPlanned(completedJob); } catch (final AspectTypeNotFoundException e) { - completedJob = createTombstone(e, completedJob); + final Tombstone tombstone = createValidationTombstone(e, completedJob); + completedJob = completedJob.toBuilder().tombstone(tombstone).build(); partAsPlannedValidity = SupplyChainImpacted.UNKNOWN; } log.info("Local validation of PartAsPlanned Validity was done for job {}. with result {}.", completedJobId, @@ -70,13 +77,16 @@ public static InvestigationResult getResult(final BpnInvestigationJob investigat try { partSiteInformationAsPlannedValidity = validatePartSiteInformationAsPlanned(investigationJob, completedJob); } catch (final ValidationException e) { - completedJob = createTombstone(e, completedJob); + final Tombstone tombstone = createValidationTombstone(e, completedJob); + completedJob = completedJob.toBuilder().tombstone(tombstone).build(); partSiteInformationAsPlannedValidity = SupplyChainImpacted.UNKNOWN; } + log.info("Local validation of PartSiteInformationAsPlanned Validity was done for job {}. with result {}.", completedJobId, partSiteInformationAsPlannedValidity); final SupplyChainImpacted supplyChainImpacted = partAsPlannedValidity.or(partSiteInformationAsPlannedValidity); + log.debug("Supply Chain Validity result of {} and {} resulted in {}", partAsPlannedValidity, partSiteInformationAsPlannedValidity, supplyChainImpacted); return new InvestigationResult(completedJob, supplyChainImpacted); @@ -118,10 +128,27 @@ private static String getAspectTypeFromJob(final Jobs job, final AspectType aspe .getPayload()); } - private static Jobs createTombstone(final ValidationException exception, final Jobs completedJob) { + private static Tombstone createValidationTombstone(final ValidationException exception, final Jobs completedJob) { log.warn("Validation failed. {}", exception.getMessage()); - final Tombstone tombstone = Tombstone.from(completedJob.getJob().getGlobalAssetId().getGlobalAssetId(), null, exception, - 0, ProcessStep.ESS_VALIDATION); - return completedJob.toBuilder().tombstone(tombstone).build(); + + final TombstoneBuilder tombstoneBuilder = Tombstone.builder(); + + tombstoneBuilder.catenaXId(completedJob.getJob().getGlobalAssetId().getGlobalAssetId()); + + // null because failure is before endpoint url is known + tombstoneBuilder.endpointURL(null); + + tombstoneBuilder.processingError(ProcessingError.builder() + .withErrorDetail(exception.getMessage()) + .withRetryCounterAndLastAttemptNow(0) + .withProcessStep(ProcessStep.ESS_VALIDATION) + .build()); + Optional.of(completedJob) + .map(Jobs::getJob) + .map(Job::getParameter) + .map(JobParameter::getBpn) + .ifPresent(tombstoneBuilder::businessPartnerNumber); + + return tombstoneBuilder.build(); } } diff --git a/irs-api/src/test/java/org/eclipse/tractusx/irs/IrsWireMockIntegrationTest.java b/irs-api/src/test/java/org/eclipse/tractusx/irs/IrsWireMockIntegrationTest.java index ee2cf697c4..367491f1e2 100644 --- a/irs-api/src/test/java/org/eclipse/tractusx/irs/IrsWireMockIntegrationTest.java +++ b/irs-api/src/test/java/org/eclipse/tractusx/irs/IrsWireMockIntegrationTest.java @@ -32,6 +32,7 @@ import static org.eclipse.tractusx.irs.WiremockSupport.randomUUID; import static org.eclipse.tractusx.irs.component.enums.AspectType.AspectTypesConstants.BATCH; import static org.eclipse.tractusx.irs.component.enums.AspectType.AspectTypesConstants.SINGLE_LEVEL_BOM_AS_BUILT; +import static org.eclipse.tractusx.irs.testing.wiremock.DiscoveryServiceWiremockSupport.CONTROLPLANE_PUBLIC_URL; import static org.eclipse.tractusx.irs.testing.wiremock.DiscoveryServiceWiremockSupport.DISCOVERY_FINDER_PATH; import static org.eclipse.tractusx.irs.testing.wiremock.DiscoveryServiceWiremockSupport.DISCOVERY_FINDER_URL; import static org.eclipse.tractusx.irs.testing.wiremock.DiscoveryServiceWiremockSupport.EDC_DISCOVERY_PATH; @@ -92,19 +93,24 @@ @ContextConfiguration(initializers = IrsWireMockIntegrationTest.MinioConfigInitializer.class) @ActiveProfiles("integrationtest") class IrsWireMockIntegrationTest { + public static final String SEMANTIC_HUB_URL = "http://semantic.hub/models"; public static final String EDC_URL = "http://edc.test"; + private static final String ACCESS_KEY = "accessKey"; private static final String SECRET_KEY = "secretKey"; private static final MinioContainer minioContainer = new MinioContainer( new MinioContainer.CredentialsProvider(ACCESS_KEY, SECRET_KEY)).withReuse(true); + @Autowired private IrsItemGraphQueryService irsService; @Autowired private SemanticHubService semanticHubService; + @Autowired private EndpointDataReferenceStorage endpointDataReferenceStorage; + @Autowired private CacheManager cacheManager; @@ -249,9 +255,15 @@ void shouldCreateTombstoneWhenDiscoveryServiceNotAvailable() { verify(0, postRequestedFor(urlPathEqualTo(EDC_DISCOVERY_PATH))); assertThat(jobForJobId.getJob().getState()).isEqualTo(JobState.COMPLETED); + assertThat(jobForJobId.getShells()).isEmpty(); assertThat(jobForJobId.getRelationships()).isEmpty(); - assertThat(jobForJobId.getTombstones()).hasSize(1); + + final List tombstones = jobForJobId.getTombstones(); + assertThat(tombstones).hasSize(1); + assertThat(tombstones.get(0).getBusinessPartnerNumber()).isEqualTo(TEST_BPN); + assertThat(tombstones.get(0).getEndpointURL()).describedAs( + "Endpoint URL should be empty because discovery not successful").isEmpty(); } @Test @@ -274,9 +286,15 @@ void shouldCreateTombstoneWhenEdcDiscoveryIsEmpty() { WiremockSupport.verifyDiscoveryCalls(1); assertThat(jobForJobId.getJob().getState()).isEqualTo(JobState.COMPLETED); + assertThat(jobForJobId.getShells()).isEmpty(); assertThat(jobForJobId.getRelationships()).isEmpty(); - assertThat(jobForJobId.getTombstones()).hasSize(1); + + final List tombstones = jobForJobId.getTombstones(); + assertThat(tombstones).hasSize(1); + assertThat(tombstones.get(0).getBusinessPartnerNumber()).isEqualTo(TEST_BPN); + assertThat(tombstones.get(0).getEndpointURL()).describedAs( + "Endpoint URL should be empty because discovery not successful").isEmpty(); } @Test @@ -318,7 +336,7 @@ void shouldStartRecursiveProcesses() { } @Test - void shouldCreateDetailedTombstoneForMissmatchPolicy() { + void shouldCreateDetailedTombstoneForMismatchPolicy() { // Arrange final String globalAssetId = "urn:uuid:334cce52-1f52-4bc9-9dd1-410bbe497bbc"; @@ -339,14 +357,22 @@ void shouldCreateDetailedTombstoneForMissmatchPolicy() { final Jobs jobForJobId = irsService.getJobForJobId(jobHandle.getId(), false); assertThat(jobForJobId.getJob().getState()).isEqualTo(JobState.COMPLETED); + assertThat(jobForJobId.getShells()).isEmpty(); assertThat(jobForJobId.getRelationships()).isEmpty(); assertThat(jobForJobId.getSubmodels()).isEmpty(); + assertThat(jobForJobId.getTombstones()).hasSize(1); + final Tombstone actualTombstone = jobForJobId.getTombstones().get(0); - assertThat(actualTombstone.getProcessingError().getRootCauses()).hasSize(1); - assertThat(actualTombstone.getProcessingError().getRootCauses().get(0)).contains( - "UsagePolicyPermissionException: Policies [default-policy] did not match with policy from BPNL00000000TEST."); + assertThat(actualTombstone.getBusinessPartnerNumber()).isEqualTo(TEST_BPN); + assertThat(actualTombstone.getEndpointURL()).isEqualTo(CONTROLPLANE_PUBLIC_URL); + + final List rootCauses = actualTombstone.getProcessingError().getRootCauses(); + assertThat(rootCauses).hasSize(1); + assertThat(rootCauses.get(0)).contains( + "UsagePolicyPermissionException: Policies [default-policy] did not match with policy from %s.".formatted( + TEST_BPN)); } @Test @@ -371,13 +397,61 @@ void shouldCreateDetailedTombstoneForEdcErrors() { final Jobs jobForJobId = irsService.getJobForJobId(jobHandle.getId(), false); assertThat(jobForJobId.getJob().getState()).isEqualTo(JobState.COMPLETED); + assertThat(jobForJobId.getShells()).isEmpty(); assertThat(jobForJobId.getRelationships()).isEmpty(); assertThat(jobForJobId.getSubmodels()).isEmpty(); - assertThat(jobForJobId.getTombstones()).hasSize(1); - final Tombstone actualTombstone = jobForJobId.getTombstones().get(0); - assertThat(actualTombstone.getProcessingError().getRootCauses()).hasSize(1); - assertThat(actualTombstone.getProcessingError().getRootCauses().get(0)).contains("502 Bad Gateway"); + + final List tombstones = jobForJobId.getTombstones(); + assertThat(tombstones).hasSize(1); + + final Tombstone actualTombstone = tombstones.get(0); + assertThat(actualTombstone.getBusinessPartnerNumber()).isEqualTo(TEST_BPN); + assertThat(actualTombstone.getEndpointURL()).isEqualTo(CONTROLPLANE_PUBLIC_URL); + + final List rootCauses = actualTombstone.getProcessingError().getRootCauses(); + assertThat(rootCauses).hasSize(1); + assertThat(rootCauses.get(0)).contains("502 Bad Gateway"); + } + + @Test + void whenEmptyCatalogIsReturnedFromAllEndpoints() { + // Arrange + final String globalAssetId = "urn:uuid:334cce52-1f52-4bc9-9dd1-410bbe497bbc"; + final List edcUrls = List.of("https://test.edc1.io", "https://test.edc2.io"); + + WiremockSupport.successfulSemanticModelRequest(); + WiremockSupport.successfulSemanticHubRequests(); + WiremockSupport.successfulDiscovery(edcUrls); + + edcUrls.forEach(edcUrl -> emptyCatalog(TEST_BPN, edcUrl)); + + // Act + final RegisterJob request = WiremockSupport.jobRequest(globalAssetId, TEST_BPN, 4); + final JobHandle jobHandle = irsService.registerItemJob(request); + assertThat(jobHandle.getId()).isNotNull(); + waitForCompletion(jobHandle); + final Jobs jobForJobId = irsService.getJobForJobId(jobHandle.getId(), false); + + // Assert + + assertThat(jobForJobId.getJob().getState()).isEqualTo(JobState.COMPLETED); + + assertThat(jobForJobId.getShells()).isEmpty(); + assertThat(jobForJobId.getRelationships()).isEmpty(); + assertThat(jobForJobId.getSubmodels()).isEmpty(); + + final List tombstones = jobForJobId.getTombstones(); + assertThat(tombstones).hasSize(1); + + final Tombstone actualTombstone = tombstones.get(0); + assertThat(actualTombstone.getBusinessPartnerNumber()).isEqualTo(TEST_BPN); + assertThat(actualTombstone.getEndpointURL()).describedAs("Tombstone should contain all EDC URLs") + .isEqualTo(String.join("; ", edcUrls)); + + final List rootCauses = actualTombstone.getProcessingError().getRootCauses(); + assertThat(rootCauses).hasSize(edcUrls.size()); + edcUrls.forEach(edcUrl -> assertThat(rootCauses).anyMatch(rootCause -> rootCause.contains(edcUrl))); } @Test @@ -402,14 +476,21 @@ void shouldCreateDetailedTombstoneForDiscoveryErrors() { final Jobs jobForJobId = irsService.getJobForJobId(jobHandle.getId(), false); assertThat(jobForJobId.getJob().getState()).isEqualTo(JobState.COMPLETED); + assertThat(jobForJobId.getShells()).isEmpty(); assertThat(jobForJobId.getRelationships()).isEmpty(); assertThat(jobForJobId.getSubmodels()).isEmpty(); + assertThat(jobForJobId.getTombstones()).hasSize(1); final Tombstone actualTombstone = jobForJobId.getTombstones().get(0); - assertThat(actualTombstone.getProcessingError().getRootCauses()).hasSize(1); - assertThat(actualTombstone.getProcessingError().getRootCauses().get(0)).contains( - "No EDC Endpoints could be discovered for BPN '%s'".formatted(TEST_BPN)); + + assertThat(actualTombstone.getBusinessPartnerNumber()).isEqualTo(TEST_BPN); + assertThat(actualTombstone.getEndpointURL()).describedAs( + "Endpoint url empty because it could not be discovered").isEmpty(); + + final List rootCauses = actualTombstone.getProcessingError().getRootCauses(); + assertThat(rootCauses).hasSize(1); + assertThat(rootCauses.get(0)).contains("No EDC Endpoints could be discovered for BPN '%s'".formatted(TEST_BPN)); } private void successfulRegistryAndDataRequest(final String globalAssetId, final String idShort, final String bpn, @@ -460,6 +541,10 @@ private void failedNegotiation() { SubmodelFacadeWiremockSupport.prepareFailingCatalog(); } + private void emptyCatalog(final String bpn, final String edcUrl) { + SubmodelFacadeWiremockSupport.prepareEmptyCatalog(bpn, edcUrl); + } + private void waitForCompletion(final JobHandle jobHandle) { Awaitility.await() .timeout(Duration.ofSeconds(35)) diff --git a/irs-api/src/test/java/org/eclipse/tractusx/irs/WiremockSupport.java b/irs-api/src/test/java/org/eclipse/tractusx/irs/WiremockSupport.java index 0192d77d33..addaad76a6 100644 --- a/irs-api/src/test/java/org/eclipse/tractusx/irs/WiremockSupport.java +++ b/irs-api/src/test/java/org/eclipse/tractusx/irs/WiremockSupport.java @@ -116,6 +116,11 @@ static void successfulDiscovery() { stubFor(DiscoveryServiceWiremockSupport.postEdcDiscovery200()); } + static void successfulDiscovery(final List edcUrls) { + stubFor(DiscoveryServiceWiremockSupport.postDiscoveryFinder200()); + stubFor(DiscoveryServiceWiremockSupport.postEdcDiscovery200(edcUrls)); + } + static void failedEdcDiscovery() { stubFor(DiscoveryServiceWiremockSupport.postDiscoveryFinder200()); stubFor(DiscoveryServiceWiremockSupport.postEdcDiscovery200Empty()); diff --git a/irs-api/src/test/java/org/eclipse/tractusx/irs/aaswrapper/job/delegate/RelationshipDelegateTest.java b/irs-api/src/test/java/org/eclipse/tractusx/irs/aaswrapper/job/delegate/RelationshipDelegateTest.java index bc9a42531c..540335fa15 100644 --- a/irs-api/src/test/java/org/eclipse/tractusx/irs/aaswrapper/job/delegate/RelationshipDelegateTest.java +++ b/irs-api/src/test/java/org/eclipse/tractusx/irs/aaswrapper/job/delegate/RelationshipDelegateTest.java @@ -57,9 +57,11 @@ import org.eclipse.tractusx.irs.aaswrapper.job.AASTransferProcess; import org.eclipse.tractusx.irs.aaswrapper.job.ItemContainer; +import org.eclipse.tractusx.irs.aaswrapper.job.ItemContainer.ItemContainerBuilder; import org.eclipse.tractusx.irs.component.JobParameter; import org.eclipse.tractusx.irs.component.PartChainIdentificationKey; import org.eclipse.tractusx.irs.component.Quantity; +import org.eclipse.tractusx.irs.component.Tombstone; import org.eclipse.tractusx.irs.component.enums.ProcessStep; import org.eclipse.tractusx.irs.edc.client.EdcSubmodelFacade; import org.eclipse.tractusx.irs.edc.client.exceptions.EdcClientException; @@ -90,11 +92,11 @@ void shouldFillItemContainerWithRelationshipAndAddChildIdsToProcess() new SubmodelDescriptor("cid", payload)); when(connectorEndpointsService.fetchConnectorEndpoints(any())).thenReturn(List.of("http://localhost")); - final ItemContainer.ItemContainerBuilder itemContainerWithShell = ItemContainer.builder() - .shell(shell("", shellDescriptor( - List.of(submodelDescriptorWithDspEndpoint( - SINGLE_LEVEL_BOM_AS_BUILT_3_0_0, - "address"))))); + final ItemContainerBuilder itemContainerWithShell = ItemContainer.builder() + .shell(shell("", shellDescriptor( + List.of(submodelDescriptorWithDspEndpoint( + SINGLE_LEVEL_BOM_AS_BUILT_3_0_0, + "address"))))); final AASTransferProcess aasTransferProcess = new AASTransferProcess(); // when @@ -118,11 +120,11 @@ void shouldFillItemContainerWithUpwardRelationshipAndAddChildIdsToProcess() new SubmodelDescriptor("cid", payload)); when(connectorEndpointsService.fetchConnectorEndpoints(any())).thenReturn(List.of("http://localhost")); - final ItemContainer.ItemContainerBuilder itemContainerWithShell = ItemContainer.builder() - .shell(shell("", shellDescriptor( - List.of(submodelDescriptorWithDspEndpoint( - SINGLE_LEVEL_USAGE_AS_BUILT_2_0_0, - "address"))))); + final ItemContainerBuilder itemContainerWithShell = ItemContainer.builder() + .shell(shell("", shellDescriptor( + List.of(submodelDescriptorWithDspEndpoint( + SINGLE_LEVEL_USAGE_AS_BUILT_2_0_0, + "address"))))); final AASTransferProcess aasTransferProcess = new AASTransferProcess(); // when @@ -131,12 +133,15 @@ void shouldFillItemContainerWithUpwardRelationshipAndAddChildIdsToProcess() // then assertThat(result).isNotNull(); + final Quantity quantity = result.getRelationships().get(0).getLinkedItem().getQuantity(); assertThat(quantity.getQuantityNumber()).isEqualTo(20.0); assertThat(quantity.getMeasurementUnit().getLexicalValue()).isEqualTo("unit:piece"); - assertThat(aasTransferProcess.getIdsToProcess()).isNotEmpty(); - assertThat(aasTransferProcess.getIdsToProcess().get(0).getGlobalAssetId()).isNotEmpty(); - assertThat(aasTransferProcess.getIdsToProcess().get(0).getBpn()).isNotEmpty(); + + final List idsToProcess = aasTransferProcess.getIdsToProcess(); + assertThat(idsToProcess).isNotEmpty(); + assertThat(idsToProcess.get(0).getGlobalAssetId()).isNotEmpty(); + assertThat(idsToProcess.get(0).getBpn()).isNotEmpty(); } @Test @@ -150,11 +155,11 @@ void shouldFillItemContainerWithUpwardAsPlannedRelationshipAndAddChildIdsToProce new SubmodelDescriptor("cid", payload)); when(connectorEndpointsService.fetchConnectorEndpoints(any())).thenReturn(List.of("http://localhost")); - final ItemContainer.ItemContainerBuilder itemContainerWithShell = ItemContainer.builder() - .shell(shell("", shellDescriptor( - List.of(submodelDescriptorWithDspEndpoint( - SINGLE_LEVEL_USAGE_AS_PLANNED_2_0_0, - "address"))))); + final ItemContainerBuilder itemContainerWithShell = ItemContainer.builder() + .shell(shell("", shellDescriptor( + List.of(submodelDescriptorWithDspEndpoint( + SINGLE_LEVEL_USAGE_AS_PLANNED_2_0_0, + "address"))))); final AASTransferProcess aasTransferProcess = new AASTransferProcess(); // when @@ -163,13 +168,15 @@ void shouldFillItemContainerWithUpwardAsPlannedRelationshipAndAddChildIdsToProce // then assertThat(result).isNotNull(); + final Quantity quantity = result.getRelationships().get(0).getLinkedItem().getQuantity(); assertThat(quantity.getQuantityNumber()).isEqualTo(20.0); assertThat(quantity.getMeasurementUnit().getLexicalValue()).isEqualTo("unit:piece"); - assertThat(aasTransferProcess.getIdsToProcess()).isNotEmpty(); - assertThat(aasTransferProcess.getIdsToProcess().get(0).getGlobalAssetId()).isEqualTo( - "urn:uuid:56319907-28dc-440e-afcc-72d67ad343e7"); - assertThat(aasTransferProcess.getIdsToProcess().get(0).getBpn()).isEqualTo("BPNL50096894aNXY"); + + final List idsToProcess = aasTransferProcess.getIdsToProcess(); + assertThat(idsToProcess).isNotEmpty(); + assertThat(idsToProcess.get(0).getGlobalAssetId()).isEqualTo("urn:uuid:56319907-28dc-440e-afcc-72d67ad343e7"); + assertThat(idsToProcess.get(0).getBpn()).isEqualTo("BPNL50096894aNXY"); } @ParameterizedTest @@ -183,11 +190,10 @@ void shouldFillItemContainerWithSupportedRelationshipAndAddChildIdsToProcess(fin new SubmodelDescriptor("cid", payload)); when(connectorEndpointsService.fetchConnectorEndpoints(any())).thenReturn(List.of("http://localhost")); - final ItemContainer.ItemContainerBuilder itemContainerWithShell = ItemContainer.builder() - .shell(shell("", shellDescriptor( - List.of(submodelDescriptorWithDspEndpoint( - aspectName, - "address"))))); + final ItemContainerBuilder itemContainerWithShell = ItemContainer.builder() + .shell(shell("", shellDescriptor( + List.of(submodelDescriptorWithDspEndpoint( + aspectName, "address"))))); final AASTransferProcess aasTransferProcess = new AASTransferProcess(); // when @@ -197,12 +203,15 @@ void shouldFillItemContainerWithSupportedRelationshipAndAddChildIdsToProcess(fin // then assertThat(result).isNotNull(); assertThat(result.getRelationships()).hasSize(1); + final Quantity quantity = result.getRelationships().get(0).getLinkedItem().getQuantity(); assertThat(quantity.getQuantityNumber()).isEqualTo(20.0); assertThat(quantity.getMeasurementUnit().getLexicalValue()).isEqualTo("unit:piece"); - assertThat(aasTransferProcess.getIdsToProcess()).isNotEmpty(); - assertThat(aasTransferProcess.getIdsToProcess().get(0).getGlobalAssetId()).isNotEmpty(); - assertThat(aasTransferProcess.getIdsToProcess().get(0).getBpn()).isNotEmpty(); + + final List idsToProcess = aasTransferProcess.getIdsToProcess(); + assertThat(idsToProcess).isNotEmpty(); + assertThat(idsToProcess.get(0).getGlobalAssetId()).isNotEmpty(); + assertThat(idsToProcess.get(0).getBpn()).isNotEmpty(); } private static Stream relationshipParameters() { @@ -233,11 +242,10 @@ void shouldFillItemContainerWithPotentialFutureMinorVersions(final String relati new SubmodelDescriptor("cid", payload)); when(connectorEndpointsService.fetchConnectorEndpoints(any())).thenReturn(List.of("http://localhost")); - final ItemContainer.ItemContainerBuilder itemContainerWithShell = ItemContainer.builder() - .shell(shell("", shellDescriptor( - List.of(submodelDescriptorWithDspEndpoint( - aspectName, - "address"))))); + final ItemContainerBuilder itemContainerWithShell = ItemContainer.builder() + .shell(shell("", shellDescriptor( + List.of(submodelDescriptorWithDspEndpoint( + aspectName, "address"))))); final AASTransferProcess aasTransferProcess = new AASTransferProcess(); // when @@ -247,12 +255,15 @@ void shouldFillItemContainerWithPotentialFutureMinorVersions(final String relati // then assertThat(result).isNotNull(); assertThat(result.getRelationships()).hasSize(1); + final Quantity quantity = result.getRelationships().get(0).getLinkedItem().getQuantity(); assertThat(quantity.getQuantityNumber()).isEqualTo(20.0); assertThat(quantity.getMeasurementUnit().getLexicalValue()).isEqualTo("unit:piece"); - assertThat(aasTransferProcess.getIdsToProcess()).isNotEmpty(); - assertThat(aasTransferProcess.getIdsToProcess().get(0).getGlobalAssetId()).isNotEmpty(); - assertThat(aasTransferProcess.getIdsToProcess().get(0).getBpn()).isNotEmpty(); + + final List idsToProcess = aasTransferProcess.getIdsToProcess(); + assertThat(idsToProcess).isNotEmpty(); + assertThat(idsToProcess.get(0).getGlobalAssetId()).isNotEmpty(); + assertThat(idsToProcess.get(0).getBpn()).isNotEmpty(); } public static Stream relationshipParametersFutureVersions() { @@ -282,11 +293,10 @@ void shouldFillItemContainerWithPreviousVersions(final String relationshipFile, new SubmodelDescriptor("cid", payload)); when(connectorEndpointsService.fetchConnectorEndpoints(any())).thenReturn(List.of("http://localhost")); - final ItemContainer.ItemContainerBuilder itemContainerWithShell = ItemContainer.builder() - .shell(shell("", shellDescriptor( - List.of(submodelDescriptorWithDspEndpoint( - aspectName, - "address"))))); + final ItemContainerBuilder itemContainerWithShell = ItemContainer.builder() + .shell(shell("", shellDescriptor( + List.of(submodelDescriptorWithDspEndpoint( + aspectName, "address"))))); final AASTransferProcess aasTransferProcess = new AASTransferProcess(); // when @@ -296,12 +306,15 @@ void shouldFillItemContainerWithPreviousVersions(final String relationshipFile, // then assertThat(result).isNotNull(); assertThat(result.getRelationships()).hasSize(1); + final Quantity quantity = result.getRelationships().get(0).getLinkedItem().getQuantity(); assertThat(quantity.getQuantityNumber()).isEqualTo(2.5); assertThat(quantity.getMeasurementUnit().getLexicalValue()).isEqualTo("unit:litre"); - assertThat(aasTransferProcess.getIdsToProcess()).isNotEmpty(); - assertThat(aasTransferProcess.getIdsToProcess().get(0).getGlobalAssetId()).isNotEmpty(); - assertThat(aasTransferProcess.getIdsToProcess().get(0).getBpn()).isNotEmpty(); + + final List idsToProcess = aasTransferProcess.getIdsToProcess(); + assertThat(idsToProcess).isNotEmpty(); + assertThat(idsToProcess.get(0).getGlobalAssetId()).isNotEmpty(); + assertThat(idsToProcess.get(0).getBpn()).isNotEmpty(); } public static Stream relationshipParametersPreviousVersions() { @@ -313,21 +326,26 @@ public static Stream relationshipParametersPreviousVersions() { @Test void shouldPutTombstoneForMissingBpn() { - final ItemContainer.ItemContainerBuilder itemContainerWithShell = ItemContainer.builder() - .shell(shell("", shellDescriptor( - List.of(submodelDescriptorWithDspEndpoint( - SINGLE_LEVEL_BOM_AS_BUILT_3_0_0, - "address"))))); + final ItemContainerBuilder itemContainerWithShell = ItemContainer.builder() + .shell(shell("", shellDescriptor( + List.of(submodelDescriptorWithDspEndpoint( + SINGLE_LEVEL_BOM_AS_BUILT_3_0_0, + "address"))))); // when final ItemContainer result = relationshipDelegate.process(itemContainerWithShell, jobParameter(), new AASTransferProcess(), PartChainIdentificationKey.builder().globalAssetId("testId").build()); // then assertThat(result).isNotNull(); - assertThat(result.getTombstones()).hasSize(1); - assertThat(result.getTombstones().get(0).getCatenaXId()).isEqualTo("testId"); - assertThat(result.getTombstones().get(0).getProcessingError().getProcessStep()).isEqualTo( - ProcessStep.SUBMODEL_REQUEST); + + final List tombstones = result.getTombstones(); + assertThat(tombstones).hasSize(1); + + assertThat(tombstones.get(0).getEndpointURL()).isEqualTo("address"); + assertThat(tombstones.get(0).getBusinessPartnerNumber()).isNull(); + + assertThat(tombstones.get(0).getCatenaXId()).isEqualTo("testId"); + assertThat(tombstones.get(0).getProcessingError().getProcessStep()).isEqualTo(ProcessStep.SUBMODEL_REQUEST); } @Test @@ -337,22 +355,28 @@ void shouldCatchRestClientExceptionAndPutTombstone() throws EdcClientException { new EdcClientException("Unable to call endpoint")); when(connectorEndpointsService.fetchConnectorEndpoints(any())).thenReturn(List.of("http://localhost")); - final ItemContainer.ItemContainerBuilder itemContainerWithShell = ItemContainer.builder() - .shell(shell("", shellDescriptor( - List.of(submodelDescriptorWithDspEndpoint( - SINGLE_LEVEL_BOM_AS_BUILT_3_0_0, - "address"))))); + final ItemContainerBuilder itemContainerWithShell = ItemContainer.builder() + .shell(shell("", shellDescriptor( + List.of(submodelDescriptorWithDspEndpoint( + SINGLE_LEVEL_BOM_AS_BUILT_3_0_0, + "address"))))); // when + final PartChainIdentificationKey partChainIdentificationKey = createKey(); final ItemContainer result = relationshipDelegate.process(itemContainerWithShell, jobParameter(), - new AASTransferProcess(), createKey()); + new AASTransferProcess(), partChainIdentificationKey); // then assertThat(result).isNotNull(); assertThat(result.getTombstones()).hasSize(1); - assertThat(result.getTombstones().get(0).getCatenaXId()).isEqualTo("itemId"); - assertThat(result.getTombstones().get(0).getProcessingError().getProcessStep()).isEqualTo( - ProcessStep.SUBMODEL_REQUEST); + + final Tombstone tombstone = result.getTombstones().get(0); + + assertThat(tombstone.getBusinessPartnerNumber()).isEqualTo(partChainIdentificationKey.getBpn()); + assertThat(tombstone.getEndpointURL()).isEqualTo("address"); + + assertThat(tombstone.getCatenaXId()).isEqualTo("itemId"); + assertThat(tombstone.getProcessingError().getProcessStep()).isEqualTo(ProcessStep.SUBMODEL_REQUEST); } @Test @@ -361,19 +385,25 @@ void shouldCatchJsonParseExceptionAndPutTombstone() throws EdcClientException { when(submodelFacade.getSubmodelPayload(anyString(), anyString(), anyString(), any())).thenThrow( new EdcClientException(new Exception("Payload did not match expected submodel"))); when(connectorEndpointsService.fetchConnectorEndpoints(any())).thenReturn(List.of("http://localhost")); - final ItemContainer.ItemContainerBuilder itemContainerWithShell = ItemContainer.builder() - .shell(shell("", shellDescriptor( - List.of(submodelDescriptorWithDspEndpoint( - SINGLE_LEVEL_BOM_AS_BUILT_3_0_0, - "address"))))); + final ItemContainerBuilder itemContainerWithShell = ItemContainer.builder() + .shell(shell("", shellDescriptor( + List.of(submodelDescriptorWithDspEndpoint( + SINGLE_LEVEL_BOM_AS_BUILT_3_0_0, + "address"))))); // when + final PartChainIdentificationKey partChainIdentificationKey = createKey(); final ItemContainer result = relationshipDelegate.process(itemContainerWithShell, jobParameter(), - new AASTransferProcess(), createKey()); + new AASTransferProcess(), partChainIdentificationKey); // then assertThat(result).isNotNull(); assertThat(result.getTombstones()).hasSize(1); + + assertThat(result.getTombstones().get(0).getBusinessPartnerNumber()).isEqualTo( + partChainIdentificationKey.getBpn()); + assertThat(result.getTombstones().get(0).getEndpointURL()).isEqualTo("address"); + assertThat(result.getTombstones().get(0).getCatenaXId()).isEqualTo("itemId"); assertThat(result.getTombstones().get(0).getProcessingError().getProcessStep()).isEqualTo( ProcessStep.SUBMODEL_REQUEST); @@ -382,27 +412,34 @@ void shouldCatchJsonParseExceptionAndPutTombstone() throws EdcClientException { @Test void shouldCatchUsagePolicyExceptionAndPutTombstone() throws EdcClientException { // given - final String businessPartnerNumber = "BPNL000000011111"; - final ItemContainer.ItemContainerBuilder itemContainerWithShell = ItemContainer.builder() - .shell(shell("", shellDescriptor( - List.of(submodelDescriptorWithDspEndpoint( - SINGLE_LEVEL_BOM_AS_BUILT_3_0_0, - "address"))))); + + final PartChainIdentificationKey partChainIdentificationKey = createKey(); + final ItemContainerBuilder itemContainerWithShell = ItemContainer.builder() + .shell(shell("", shellDescriptor( + List.of(submodelDescriptorWithDspEndpoint( + SINGLE_LEVEL_BOM_AS_BUILT_3_0_0, + "address"))))); // when when(submodelFacade.getSubmodelPayload(any(), any(), any(), any())).thenThrow( - new UsagePolicyPermissionException(List.of(), null, businessPartnerNumber)); + new UsagePolicyPermissionException(List.of(), null, partChainIdentificationKey.getBpn())); when(connectorEndpointsService.fetchConnectorEndpoints(any())).thenReturn(List.of("connector.endpoint.nl")); final ItemContainer result = relationshipDelegate.process(itemContainerWithShell, jobParameter(), - new AASTransferProcess(), createKey()); + new AASTransferProcess(), partChainIdentificationKey); // then assertThat(result).isNotNull(); - assertThat(result.getTombstones()).hasSize(1); - assertThat(result.getTombstones().get(0).getCatenaXId()).isEqualTo("itemId"); - assertThat(result.getTombstones().get(0).getBusinessPartnerNumber()).isEqualTo(businessPartnerNumber); - assertThat(result.getTombstones().get(0).getProcessingError().getProcessStep()).isEqualTo( - ProcessStep.USAGE_POLICY_VALIDATION); + + final List tombstones = result.getTombstones(); + assertThat(tombstones).hasSize(1); + + final Tombstone tombstone = tombstones.get(0); + assertThat(tombstone.getBusinessPartnerNumber()).isEqualTo(partChainIdentificationKey.getBpn()); + assertThat(tombstone.getEndpointURL()).isEqualTo("address"); + + assertThat(tombstone.getCatenaXId()).isEqualTo("itemId"); + assertThat(tombstone.getBusinessPartnerNumber()).isEqualTo(partChainIdentificationKey.getBpn()); + assertThat(tombstone.getProcessingError().getProcessStep()).isEqualTo(ProcessStep.USAGE_POLICY_VALIDATION); } private static PartChainIdentificationKey createKey() { diff --git a/irs-api/src/test/java/org/eclipse/tractusx/irs/component/TombstoneTest.java b/irs-api/src/test/java/org/eclipse/tractusx/irs/component/TombstoneTest.java index c62e5d8b54..830c548375 100644 --- a/irs-api/src/test/java/org/eclipse/tractusx/irs/component/TombstoneTest.java +++ b/irs-api/src/test/java/org/eclipse/tractusx/irs/component/TombstoneTest.java @@ -31,11 +31,10 @@ class TombstoneTest { @Test - void fromTombstoneTest() { + void buildTombstoneTest() { // arrange final String catenaXId = "5e3e9060-ba73-4d5d-a6c8-dfd5123f4d99"; - final IllegalArgumentException illegalArgumentException = new IllegalArgumentException( - "Some funny error occur"); + final IllegalArgumentException exception = new IllegalArgumentException("Some funny error occur"); final String endPointUrl = "http://localhost/dummy/interfaceinformation/urn:uuid:8a61c8db-561e-4db0-84ec-a693fc5ffdf6"; final ProcessingError processingError = ProcessingError.builder() @@ -53,9 +52,17 @@ void fromTombstoneTest() { .processingError(processingError) .build(); - //act - final Tombstone tombstone = Tombstone.from(catenaXId, endPointUrl, illegalArgumentException, - RetryRegistry.ofDefaults().getDefaultConfig().getMaxAttempts(), ProcessStep.SUBMODEL_REQUEST); + // act + final int retryCount = RetryRegistry.ofDefaults().getDefaultConfig().getMaxAttempts(); + final ProcessingError error = ProcessingError.builder() + .withProcessStep(ProcessStep.SUBMODEL_REQUEST) + .withRetryCounterAndLastAttemptNow(retryCount) + .withErrorDetail(exception.getMessage()) + .build(); + final Tombstone tombstone = Tombstone.builder().endpointURL(endPointUrl) + .catenaXId(catenaXId) + .processingError(error) + .build(); // assert assertThat(tombstone).isNotNull(); @@ -79,12 +86,22 @@ void shouldUseSuppressedExceptionWhenPresent() { final Throwable[] suppressed = exception.getSuppressed(); // act - final Tombstone from = Tombstone.from("testId", "testUrl", exception, suppressed, 1, - ProcessStep.DIGITAL_TWIN_REQUEST); + + final ProcessingError error = ProcessingError.builder() + .withProcessStep(ProcessStep.DIGITAL_TWIN_REQUEST) + .withRetryCounterAndLastAttemptNow(1) + .withErrorDetail(exception.getMessage()) + .withRootCauses(Tombstone.getRootErrorMessages(suppressed)) + .build(); + final Tombstone tombstone = Tombstone.builder() + .endpointURL("testUrl") + .catenaXId("testId") + .processingError(error) + .build(); // assert - assertThat(from.getProcessingError().getErrorDetail()).isEqualTo(exception.getMessage()); - assertThat(from.getProcessingError().getRootCauses()).contains("Exception: " + suppressedExceptionMessage); + assertThat(tombstone.getProcessingError().getErrorDetail()).isEqualTo(exception.getMessage()); + assertThat(tombstone.getProcessingError().getRootCauses()).contains("Exception: " + suppressedExceptionMessage); } @Test @@ -103,12 +120,21 @@ void shouldUseDeepSuppressedExceptionWhenPresent() { final Throwable[] suppressed = exception.getSuppressed(); // act - final Tombstone from = Tombstone.from("testId", "testUrl", exception, suppressed, 1, - ProcessStep.DIGITAL_TWIN_REQUEST); + final ProcessingError error = ProcessingError.builder() + .withProcessStep(ProcessStep.DIGITAL_TWIN_REQUEST) + .withRetryCounterAndLastAttemptNow(1) + .withErrorDetail(exception.getMessage()) + .withRootCauses(Tombstone.getRootErrorMessages(suppressed)) + .build(); + final Tombstone tombstone = Tombstone.builder() + .endpointURL("testUrl") + .catenaXId("testId") + .processingError(error) + .build(); // assert - assertThat(from.getProcessingError().getErrorDetail()).isEqualTo(exception.getMessage()); - assertThat(from.getProcessingError().getRootCauses()).contains("Exception: " + suppressedRootCause); + assertThat(tombstone.getProcessingError().getErrorDetail()).isEqualTo(exception.getMessage()); + assertThat(tombstone.getProcessingError().getRootCauses()).contains("Exception: " + suppressedRootCause); } @Test @@ -119,12 +145,22 @@ void shouldUseExceptionMessageWhenSuppressedExceptionNotPresent() { final Throwable[] suppressed = exception.getSuppressed(); // act - final Tombstone from = Tombstone.from("testId", "testUrl", exception, suppressed, 1, - ProcessStep.DIGITAL_TWIN_REQUEST); + + final ProcessingError error = ProcessingError.builder() + .withProcessStep(ProcessStep.DIGITAL_TWIN_REQUEST) + .withRetryCounterAndLastAttemptNow(1) + .withErrorDetail(exception.getMessage()) + .withRootCauses(Tombstone.getRootErrorMessages(suppressed)) + .build(); + final Tombstone tombstone = Tombstone.builder() + .endpointURL("testUrl") + .catenaXId("testId") + .processingError(error) + .build(); // assert - assertThat(from.getProcessingError().getErrorDetail()).isEqualTo(exception.getMessage()); - assertThat(from.getProcessingError().getRootCauses()).isEmpty(); + assertThat(tombstone.getProcessingError().getErrorDetail()).isEqualTo(exception.getMessage()); + assertThat(tombstone.getProcessingError().getRootCauses()).isEmpty(); } private String zonedDateTimeExcerpt(ZonedDateTime dateTime) { diff --git a/irs-models/src/main/java/org/eclipse/tractusx/irs/component/Tombstone.java b/irs-models/src/main/java/org/eclipse/tractusx/irs/component/Tombstone.java index 01a464eabb..6aaa3372aa 100644 --- a/irs-models/src/main/java/org/eclipse/tractusx/irs/component/Tombstone.java +++ b/irs-models/src/main/java/org/eclipse/tractusx/irs/component/Tombstone.java @@ -23,11 +23,12 @@ ********************************************************************************/ package org.eclipse.tractusx.irs.component; -import java.time.ZoneOffset; -import java.time.ZonedDateTime; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Builder; @@ -35,7 +36,6 @@ import lombok.extern.jackson.Jacksonized; import org.apache.commons.lang3.exception.ExceptionUtils; import org.eclipse.tractusx.irs.component.enums.NodeType; -import org.eclipse.tractusx.irs.component.enums.ProcessStep; /** * Tombstone with information about request failure @@ -60,87 +60,36 @@ public class Tombstone { private final ProcessingError processingError; private final Map policy; - public static Tombstone from(final String catenaXId, final String endpointURL, final Exception exception, - final int retryCount, final ProcessStep processStep) { - return from(catenaXId, endpointURL, exception.getMessage(), retryCount, processStep); - } - - public static Tombstone from(final String catenaXId, final String endpointURL, final Exception exception, - final int retryCount, final ProcessStep processStep, final String businessPartnerNumber, - final Map policy) { - - return Tombstone.builder() - .endpointURL(endpointURL) - .catenaXId(catenaXId) - .processingError(withProcessingError(processStep, retryCount, exception.getMessage())) - .businessPartnerNumber(businessPartnerNumber) - .policy(policy) - .build(); - } - - public static Tombstone from(final String catenaXId, final String endpointURL, final String errorDetails, - final int retryCount, final ProcessStep processStep) { - - return Tombstone.builder() - .endpointURL(endpointURL) - .catenaXId(catenaXId) - .processingError(withProcessingError(processStep, retryCount, errorDetails)) - .build(); - } - - public static Tombstone from(final String globalAssetId, final String endpointURL, final Throwable exception, - final Throwable[] suppressed, final int retryCount, final ProcessStep processStep) { - return Tombstone.builder() - .endpointURL(endpointURL) - .catenaXId(globalAssetId) - .processingError(ProcessingError.builder() - .withProcessStep(processStep) - .withRetryCounterAndLastAttemptNow(retryCount) - .withErrorDetail(exception.getMessage()) - .withRootCauses(getRootErrorMessages(suppressed)) - .build()) - .build(); - } - - private static List getRootErrorMessages(final Throwable... throwables) { - return Arrays.stream(throwables).map(Tombstone::getRootErrorMessages).toList(); + public static List getRootErrorMessages(final Throwable... throwables) { + return Arrays.stream(throwables).flatMap(throwable -> getRootErrorMessages(throwable).stream()).toList(); } /** - * Search for the root cause or suppressed exception as long as there is a cause or suppressed exception. + * Search for the root causes or suppressed exception as long as there is a cause or suppressed exception. * Stop after a depth of 10 to prevent endless loop. * * @param throwable the exception with a nested or suppressed exception - * @return the root cause, eiter suppressed or nested + * @return the root causes, eiter suppressed or nested */ - private static String getRootErrorMessages(final Throwable throwable) { + private static Set getRootErrorMessages(final Throwable throwable) { final Throwable cause = throwable.getCause(); if (cause != null) { - Throwable rootCause = cause; + final List causes = new ArrayList<>(); int depth = 0; final int maxDepth = 10; - while ((rootCause.getCause() != null || hasSuppressedExceptions(rootCause)) && depth < maxDepth) { - if (hasSuppressedExceptions(rootCause)) { - rootCause = rootCause.getSuppressed()[0]; + while ((cause.getCause() != null || hasSuppressedExceptions(cause)) && depth < maxDepth) { + if (hasSuppressedExceptions(cause)) { + causes.addAll(Arrays.asList(cause.getSuppressed())); } else { - rootCause = rootCause.getCause(); + causes.add(cause.getCause()); } depth++; } - return ExceptionUtils.getRootCauseMessage(rootCause); + return causes.stream().map(ExceptionUtils::getRootCauseMessage).collect(Collectors.toSet()); } - return ExceptionUtils.getRootCauseMessage(throwable); - } - private static ProcessingError withProcessingError(final ProcessStep processStep, final int retryCount, - final String exception) { - return ProcessingError.builder() - .withProcessStep(processStep) - .withRetryCounter(retryCount) - .withLastAttempt(ZonedDateTime.now(ZoneOffset.UTC)) - .withErrorDetail(exception) - .build(); + return Set.of(ExceptionUtils.getRootCauseMessage(throwable)); } private static boolean hasSuppressedExceptions(final Throwable exception) { diff --git a/irs-registry-client/src/main/java/org/eclipse/tractusx/irs/registryclient/DefaultConfiguration.java b/irs-registry-client/src/main/java/org/eclipse/tractusx/irs/registryclient/DefaultConfiguration.java index a15eeb7221..230c66d7ee 100644 --- a/irs-registry-client/src/main/java/org/eclipse/tractusx/irs/registryclient/DefaultConfiguration.java +++ b/irs-registry-client/src/main/java/org/eclipse/tractusx/irs/registryclient/DefaultConfiguration.java @@ -122,7 +122,7 @@ public EndpointDataForConnectorsService endpointDataForConnectorsService(final E try { return facade.getEndpointReferencesForRegistryAsset(edcConnectorEndpoint, bpn); } catch (EdcClientException e) { - throw new EdcRetrieverException(e); + throw new EdcRetrieverException.Builder(e).withEdcUrl(edcConnectorEndpoint).withBpn(bpn).build(); } }; diff --git a/irs-registry-client/src/main/java/org/eclipse/tractusx/irs/registryclient/decentral/EdcRetrieverException.java b/irs-registry-client/src/main/java/org/eclipse/tractusx/irs/registryclient/decentral/EdcRetrieverException.java index 71647bcf37..64efc443ff 100644 --- a/irs-registry-client/src/main/java/org/eclipse/tractusx/irs/registryclient/decentral/EdcRetrieverException.java +++ b/irs-registry-client/src/main/java/org/eclipse/tractusx/irs/registryclient/decentral/EdcRetrieverException.java @@ -23,11 +23,49 @@ ********************************************************************************/ package org.eclipse.tractusx.irs.registryclient.decentral; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.Setter; + /** * Thrown in case of error in EDC communication */ -public class EdcRetrieverException extends Exception { - public EdcRetrieverException(final Throwable cause) { +@Getter +@Setter(AccessLevel.PRIVATE) +public final class EdcRetrieverException extends Exception { + + private String bpn; + + private String edcUrl; + + private EdcRetrieverException(final Throwable cause) { super(cause); } + + /** + * Builder for {@link EdcRetrieverException} + */ + public static class Builder { + + private final EdcRetrieverException exception; + + public Builder(final Throwable cause) { + this.exception = new EdcRetrieverException(cause); + } + + public Builder withBpn(final String bpn) { + this.exception.setBpn(bpn); + return this; + } + + public Builder withEdcUrl(final String edcUrl) { + this.exception.setEdcUrl(edcUrl); + return this; + } + + public EdcRetrieverException build() { + return this.exception; + } + } + } diff --git a/irs-registry-client/src/main/java/org/eclipse/tractusx/irs/registryclient/decentral/EndpointDataForConnectorsService.java b/irs-registry-client/src/main/java/org/eclipse/tractusx/irs/registryclient/decentral/EndpointDataForConnectorsService.java index a71abc4d6e..6e6d4b3ed5 100644 --- a/irs-registry-client/src/main/java/org/eclipse/tractusx/irs/registryclient/decentral/EndpointDataForConnectorsService.java +++ b/irs-registry-client/src/main/java/org/eclipse/tractusx/irs/registryclient/decentral/EndpointDataForConnectorsService.java @@ -78,7 +78,8 @@ private List> createGetEndpointReferenc return edcSubmodelFacade.getEndpointReferencesForAsset(edcUrl, bpn); } catch (EdcRetrieverException e) { log.warn("Exception occurred when retrieving EndpointDataReference from connector '{}'", edcUrl, e); - return List.of(CompletableFuture.failedFuture(e)); + return List.of(CompletableFuture.failedFuture( + new EdcRetrieverException.Builder(e).withBpn(bpn).withEdcUrl(edcUrl).build())); } finally { watch.stop(); log.info(TOOK_MS, watch.getLastTaskName(), watch.getLastTaskTimeMillis()); diff --git a/irs-registry-client/src/test/java/org/eclipse/tractusx/irs/registryclient/decentral/DecentralDigitalTwinRegistryServiceWiremockTest.java b/irs-registry-client/src/test/java/org/eclipse/tractusx/irs/registryclient/decentral/DecentralDigitalTwinRegistryServiceWiremockTest.java index 915a384f8b..f34da42154 100644 --- a/irs-registry-client/src/test/java/org/eclipse/tractusx/irs/registryclient/decentral/DecentralDigitalTwinRegistryServiceWiremockTest.java +++ b/irs-registry-client/src/test/java/org/eclipse/tractusx/irs/registryclient/decentral/DecentralDigitalTwinRegistryServiceWiremockTest.java @@ -113,7 +113,8 @@ void shouldDiscoverEDCAndRequestRegistry() throws RegistryServiceException, EdcR givenThat(getShellDescriptor200()); final var endpointDataReference = endpointDataReference("assetId"); - when(edcEndpointReferenceRetrieverMock.getEndpointReferencesForAsset(any(), any())).thenReturn(List.of(CompletableFuture.completedFuture(endpointDataReference))); + when(edcEndpointReferenceRetrieverMock.getEndpointReferencesForAsset(any(), any())).thenReturn( + List.of(CompletableFuture.completedFuture(endpointDataReference))); // Act final Collection> shells = decentralDigitalTwinRegistryService.fetchShells( @@ -161,7 +162,8 @@ void shouldThrowInCaseOfLookupShellsError() throws EdcRetrieverException { givenThat(postEdcDiscovery200()); final var endpointDataReference = endpointDataReference("assetId"); - when(edcEndpointReferenceRetrieverMock.getEndpointReferencesForAsset(any(), any())).thenReturn(List.of(CompletableFuture.completedFuture(endpointDataReference))); + when(edcEndpointReferenceRetrieverMock.getEndpointReferencesForAsset(any(), any())).thenReturn( + List.of(CompletableFuture.completedFuture(endpointDataReference))); givenThat(getLookupShells404()); final List testId = List.of(new DigitalTwinRegistryKey("testId", TEST_BPN)); @@ -181,7 +183,8 @@ void shouldThrowInCaseOfShellDescriptorsError() throws EdcRetrieverException { givenThat(postEdcDiscovery200()); final var endpointDataReference = endpointDataReference("assetId"); - when(edcEndpointReferenceRetrieverMock.getEndpointReferencesForAsset(any(), any())).thenReturn(List.of(CompletableFuture.completedFuture(endpointDataReference))); + when(edcEndpointReferenceRetrieverMock.getEndpointReferencesForAsset(any(), any())).thenReturn( + List.of(CompletableFuture.completedFuture(endpointDataReference))); givenThat(getLookupShells200()); givenThat(getShellDescriptor404()); @@ -203,7 +206,8 @@ void shouldThrowExceptionOnEmptyShells() throws EdcRetrieverException { givenThat(postEdcDiscovery200()); final var endpointDataReference = endpointDataReference("assetId"); - when(edcEndpointReferenceRetrieverMock.getEndpointReferencesForAsset(any(), any())).thenReturn(List.of(CompletableFuture.completedFuture(endpointDataReference))); + when(edcEndpointReferenceRetrieverMock.getEndpointReferencesForAsset(any(), any())).thenReturn( + List.of(CompletableFuture.completedFuture(endpointDataReference))); givenThat(getLookupShells200Empty()); givenThat(getShellDescriptor404()); @@ -232,7 +236,8 @@ void lookupShellIdentifiers_oneEDC_oneDTR() throws RegistryServiceException, Edc // simulate endpoint data reference final var endpointDataReference = endpointDataReference("assetId"); - when(edcEndpointReferenceRetrieverMock.getEndpointReferencesForAsset(any(), any())).thenReturn(List.of(CompletableFuture.completedFuture(endpointDataReference))); + when(edcEndpointReferenceRetrieverMock.getEndpointReferencesForAsset(any(), any())).thenReturn( + List.of(CompletableFuture.completedFuture(endpointDataReference))); // Act final Collection digitalTwinRegistryKeys = decentralDigitalTwinRegistryService.lookupShellIdentifiers( @@ -262,8 +267,10 @@ void lookupShellIdentifiers_multipleEDCs_oneDTR(String title, // simulate endpoint data reference final var endpointDataReference = endpointDataReference("assetId"); - when(edcEndpointReferenceRetrieverMock.getEndpointReferencesForAsset(eq(edc1Url), any())).thenReturn(List.of(CompletableFuture.completedFuture(endpointDataReference))); - when(edcEndpointReferenceRetrieverMock.getEndpointReferencesForAsset(eq(edc2Url), any())).thenReturn(endpointDataReferenceForAssetFutures); + when(edcEndpointReferenceRetrieverMock.getEndpointReferencesForAsset(eq(edc1Url), any())).thenReturn( + List.of(CompletableFuture.completedFuture(endpointDataReference))); + when(edcEndpointReferenceRetrieverMock.getEndpointReferencesForAsset(eq(edc2Url), any())).thenReturn( + endpointDataReferenceForAssetFutures); // Act final Collection digitalTwinRegistryKeys = decentralDigitalTwinRegistryService.lookupShellIdentifiers( @@ -285,9 +292,10 @@ public Stream provideArguments(final ExtensionContext exten return Stream.of( // failed future Arguments.of("given failed future", List.of(CompletableFuture.failedFuture( - new EdcRetrieverException(new EdcClientException(new RuntimeException("test")))))), - // no result - Arguments.of("given no result", Collections.emptyList())); + new EdcRetrieverException.Builder( + new EdcClientException(new RuntimeException("test"))).build())), + // no result + Arguments.of("given no result", Collections.emptyList()))); } } @@ -305,8 +313,10 @@ void lookupShellIdentifiers_multipleEDCs_multipleDTRs() throws RegistryServiceEx // simulate endpoint data reference final var endpointDataReference1 = endpointDataReference("dtr1-assetId"); final var endpointDataReference2 = endpointDataReference("dtr2-assetId"); - when(edcEndpointReferenceRetrieverMock.getEndpointReferencesForAsset(eq(edc1Url), any())).thenReturn(List.of(CompletableFuture.completedFuture(endpointDataReference1))); - when(edcEndpointReferenceRetrieverMock.getEndpointReferencesForAsset(eq(edc2Url), any())).thenReturn(List.of(CompletableFuture.completedFuture(endpointDataReference2))); + when(edcEndpointReferenceRetrieverMock.getEndpointReferencesForAsset(eq(edc1Url), any())).thenReturn( + List.of(CompletableFuture.completedFuture(endpointDataReference1))); + when(edcEndpointReferenceRetrieverMock.getEndpointReferencesForAsset(eq(edc2Url), any())).thenReturn( + List.of(CompletableFuture.completedFuture(endpointDataReference2))); // Act & Assert final Collection digitalTwinRegistryKeys = decentralDigitalTwinRegistryService.lookupShellIdentifiers( diff --git a/irs-registry-client/src/test/java/org/eclipse/tractusx/irs/registryclient/decentral/EdcRetrieverExceptionTest.java b/irs-registry-client/src/test/java/org/eclipse/tractusx/irs/registryclient/decentral/EdcRetrieverExceptionTest.java new file mode 100644 index 0000000000..2a9c9cef50 --- /dev/null +++ b/irs-registry-client/src/test/java/org/eclipse/tractusx/irs/registryclient/decentral/EdcRetrieverExceptionTest.java @@ -0,0 +1,43 @@ +/******************************************************************************** + * Copyright (c) 2022,2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +package org.eclipse.tractusx.irs.registryclient.decentral; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +class EdcRetrieverExceptionTest { + + @Test + void test() throws EdcRetrieverException { + + final EdcRetrieverException build = new EdcRetrieverException.Builder( + new IllegalArgumentException("my illegal arg")).withEdcUrl("my url").withBpn("my bpn").build(); + + assertThat(build.getBpn()).isEqualTo("my bpn"); + assertThat(build.getEdcUrl()).isEqualTo("my url"); + + Assertions.assertThatThrownBy(() -> { + throw build; + }).hasMessageContaining("my illegal arg"); + + } +} \ No newline at end of file diff --git a/irs-registry-client/src/test/java/org/eclipse/tractusx/irs/registryclient/decentral/EndpointDataForConnectorsServiceTest.java b/irs-registry-client/src/test/java/org/eclipse/tractusx/irs/registryclient/decentral/EndpointDataForConnectorsServiceTest.java index 30163c000f..865fc43740 100644 --- a/irs-registry-client/src/test/java/org/eclipse/tractusx/irs/registryclient/decentral/EndpointDataForConnectorsServiceTest.java +++ b/irs-registry-client/src/test/java/org/eclipse/tractusx/irs/registryclient/decentral/EndpointDataForConnectorsServiceTest.java @@ -24,6 +24,7 @@ package org.eclipse.tractusx.irs.registryclient.decentral; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.tuple; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; @@ -59,7 +60,8 @@ class EndpointDataForConnectorsServiceTest { .build(); private static final EndpointDataReference CONNECTION_TWO_DATA_REF = // - EndpointDataReference.Builder.newInstance().endpoint(CONNECTION_TWO_ADDRESS) + EndpointDataReference.Builder.newInstance() + .endpoint(CONNECTION_TWO_ADDRESS) .contractId(CONNECTION_TWO_CONTRACT_ID) .id("test1") .authKey(HttpHeaders.AUTHORIZATION) @@ -96,7 +98,7 @@ void shouldReturnExpectedEndpointDataReferenceFromSecondConnectionEndpoint() thr // a first endpoint failing (1) when(edcSubmodelFacade.getEndpointReferencesForAsset(CONNECTION_ONE_ADDRESS, BPN)).thenThrow( - new EdcRetrieverException(new EdcClientException("EdcClientException"))); + new EdcRetrieverException.Builder(new EdcClientException("EdcClientException")).build()); // and a second endpoint returning successfully (2) when(edcSubmodelFacade.getEndpointReferencesForAsset(CONNECTION_TWO_ADDRESS, BPN)).thenReturn( @@ -135,12 +137,10 @@ void shouldThrowExceptionWhenConnectorEndpointsNotReachable() throws EdcRetrieve // GIVEN when(edcSubmodelFacade.getEndpointReferencesForAsset(anyString(), eq(BPN))).thenThrow( - new EdcRetrieverException(new EdcClientException("EdcClientException"))); + new EdcRetrieverException.Builder(new EdcClientException("EdcClientException")).build()); // WHEN - final var exceptions = new ArrayList<>(); - - // THEN + final List exceptions = new ArrayList<>(); final List connectorEndpoints = List.of(CONNECTION_ONE_ADDRESS, CONNECTION_TWO_ADDRESS); sut.createFindEndpointDataForConnectorsFutures(connectorEndpoints, BPN) // .forEach(future -> { @@ -151,7 +151,14 @@ void shouldThrowExceptionWhenConnectorEndpointsNotReachable() throws EdcRetrieve } }); - assertThat(exceptions).hasSize(connectorEndpoints.size()); + // THEN + assertThat(exceptions).hasSize(connectorEndpoints.size()) + .extracting(Exception::getCause) + .allMatch(exception -> exception instanceof EdcRetrieverException) + .extracting("bpn", "edcUrl") + .containsExactlyInAnyOrder(tuple(BPN, CONNECTION_ONE_ADDRESS), + tuple(BPN, CONNECTION_TWO_ADDRESS)); + } } diff --git a/irs-testing/src/main/java/org/eclipse/tractusx/irs/testing/wiremock/DiscoveryServiceWiremockSupport.java b/irs-testing/src/main/java/org/eclipse/tractusx/irs/testing/wiremock/DiscoveryServiceWiremockSupport.java index fdfa23ff5c..41f6dcc03f 100644 --- a/irs-testing/src/main/java/org/eclipse/tractusx/irs/testing/wiremock/DiscoveryServiceWiremockSupport.java +++ b/irs-testing/src/main/java/org/eclipse/tractusx/irs/testing/wiremock/DiscoveryServiceWiremockSupport.java @@ -23,13 +23,17 @@ import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; import static org.eclipse.tractusx.irs.testing.wiremock.WireMockConfig.responseWithStatus; +import java.util.Arrays; import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; import com.github.tomakehurst.wiremock.client.MappingBuilder; /** * WireMock configurations and requests used for testing the Discovery Service flow. */ +@SuppressWarnings("PMD.TooManyMethods") public final class DiscoveryServiceWiremockSupport { public static final String CONTROLPLANE_PUBLIC_URL = "https://test.edc.io"; public static final String EDC_DISCOVERY_PATH = "/edcDiscovery"; @@ -45,7 +49,15 @@ private DiscoveryServiceWiremockSupport() { } public static MappingBuilder postEdcDiscovery200() { - return postEdcDiscovery200(TEST_BPN, List.of(CONTROLPLANE_PUBLIC_URL)); + return postEdcDiscovery200(List.of(CONTROLPLANE_PUBLIC_URL)); + } + + public static MappingBuilder postEdcDiscovery200(final String... edcUrls) { + return postEdcDiscovery200(Arrays.asList(edcUrls)); + } + + public static MappingBuilder postEdcDiscovery200(final List edcUrls) { + return postEdcDiscovery200(TEST_BPN, edcUrls); } public static MappingBuilder postEdcDiscovery200Empty() { @@ -79,20 +91,35 @@ public static MappingBuilder postDiscoveryFinder200() { responseWithStatus(STATUS_CODE_OK).withBody(discoveryFinderResponse(EDC_DISCOVERY_URL))); } - public static String discoveryFinderResponse(final String discoveryFinderUrl) { + public static MappingBuilder postDiscoveryFinder200(final String... edcUrls) { + return post(urlPathEqualTo(DISCOVERY_FINDER_PATH)).willReturn( + responseWithStatus(STATUS_CODE_OK).withBody(discoveryFinderResponse(edcUrls))); + } + + public static String discoveryFinderResponse(final String... discoveryFinderUrls) { + + final String endpoints = Arrays.stream(discoveryFinderUrls) + .map(endpointAddress -> { + final String resourceId = UUID.randomUUID().toString(); + return """ + { + "type": "bpn", + "description": "Service to discover EDC to a particular BPN", + "endpointAddress": "%s", + "documentation": "http://.../swagger/index.html", + "resourceId": "%s" + } + """.formatted(endpointAddress, resourceId); + }) + .collect(Collectors.joining(",")); + return """ { "endpoints": [ - { - "type": "bpn", - "description": "Service to discover EDC to a particular BPN", - "endpointAddress": "%s", - "documentation": "http://.../swagger/index.html", - "resourceId": "316417cd-0fb5-4daf-8dfa-8f68125923f1" - } + %s ] } - """.formatted(discoveryFinderUrl); + """.formatted(endpoints); } public static MappingBuilder postDiscoveryFinder404() { diff --git a/irs-testing/src/main/java/org/eclipse/tractusx/irs/testing/wiremock/SubmodelFacadeWiremockSupport.java b/irs-testing/src/main/java/org/eclipse/tractusx/irs/testing/wiremock/SubmodelFacadeWiremockSupport.java index ffbd23bd3b..86abd67420 100644 --- a/irs-testing/src/main/java/org/eclipse/tractusx/irs/testing/wiremock/SubmodelFacadeWiremockSupport.java +++ b/irs-testing/src/main/java/org/eclipse/tractusx/irs/testing/wiremock/SubmodelFacadeWiremockSupport.java @@ -29,6 +29,7 @@ /** * WireMock configurations and requests used for testing the EDC Flow. */ +@SuppressWarnings("PMD.TooManyMethods") public final class SubmodelFacadeWiremockSupport { public static final String PATH_CATALOG = "/catalog/request"; public static final String PATH_NEGOTIATE = "/contractnegotiations"; @@ -121,6 +122,38 @@ public static void prepareFailingCatalog() { WireMockConfig.responseWithStatus(STATUS_CODE_BAD_GATEWAY).withBody(""))); } + public static void prepareEmptyCatalog(final String bpn, final String edcUrl) { + stubFor(post(urlPathEqualTo(PATH_CATALOG)).willReturn( + WireMockConfig.responseWithStatus(STATUS_CODE_OK).withBody(""" + { + "@id": "6af0d267-aaed-4d2e-86bb-adf391597fbe", + "@type": "dcat:Catalog", + "dspace:participantId": "%s", + "dcat:dataset": [], + "dcat:service": { + "@id": "75b09a2c-e7f9-4d15-bd67-334c50f35c48", + "@type": "dcat:DataService", + "dcat:endpointDescription": "dspace:connector", + "dcat:endpointUrl": "%s", + "dct:terms": "dspace:connector", + "dct:endpointUrl": "%s" + }, + "participantId": "%s", + "@context": { + "@vocab": "https://w3id.org/edc/v0.0.1/ns/", + "edc": "https://w3id.org/edc/v0.0.1/ns/", + "tx": "https://w3id.org/tractusx/v0.0.1/ns/", + "tx-auth": "https://w3id.org/tractusx/auth/", + "cx-policy": "https://w3id.org/catenax/policy/", + "dcat": "http://www.w3.org/ns/dcat#", + "dct": "http://purl.org/dc/terms/", + "odrl": "http://www.w3.org/ns/odrl/2/", + "dspace": "https://w3id.org/dspace/v0.8/" + } + } + """.formatted(bpn, edcUrl, edcUrl, bpn)))); + } + private static String startTransferProcessResponse(final String transferProcessId) { return startNegotiationResponse(transferProcessId); }