From 86e62ee63fdfcc8ebbbe6f925a3d3102d331d74a Mon Sep 17 00:00:00 2001 From: ndr_brt Date: Thu, 29 Jun 2023 09:34:24 +0200 Subject: [PATCH] refactor: put DataAddress as Asset field --- .../DatasetResolverImplIntegrationTest.java | 25 +- .../DatasetResolverImplPerformanceTest.java | 15 +- .../ContractNegotiationIntegrationTest.java | 3 +- ...tractOfferResolverImplIntegrationTest.java | 25 +- .../service/asset/AssetServiceImpl.java | 9 +- .../service/asset/AssetServiceImplTest.java | 64 ++- .../ContractNegotiationEventDispatchTest.java | 2 +- .../assetindex/InMemoryAssetIndex.java | 72 +-- .../assetindex/InMemoryAssetIndexTest.java | 164 +------ .../InMemoryDataAddressResolverTest.java | 26 +- .../from/JsonObjectFromAssetTransformer.java | 15 +- .../to/JsonObjectToAssetTransformer.java | 4 + .../JsonObjectFromAssetTransformerTest.java | 36 +- .../to/JsonObjectToAssetTransformerTest.java | 42 +- .../management/asset/AssetApiExtension.java | 7 +- .../management/asset/{ => v2}/AssetApi.java | 3 +- .../asset/{ => v2}/AssetApiController.java | 7 +- .../api/management/asset/v3/AssetApi.java | 99 ++++ .../asset/v3/AssetApiController.java | 141 ++++++ .../validation/AssetEntryDtoValidator.java | 3 + .../asset/validation/AssetValidator.java | 61 +++ .../asset/AssetApiExtensionTest.java | 13 + .../{ => v2}/AssetApiControllerTest.java | 5 +- .../asset/v3/AssetApiControllerTest.java | 455 ++++++++++++++++++ .../asset/validation/AssetValidatorTest.java | 123 +++++ .../HttpProvisionerExtensionEndToEndTest.java | 10 +- .../store/sql/assetindex/SqlAssetIndex.java | 27 +- .../schema/BaseSqlDialectStatements.java | 4 +- .../assetindex/PostgresAssetIndexTest.java | 123 ----- resources/openapi/openapi.yaml | 193 ++++++++ .../yaml/management-api/asset-api.yaml | 189 ++++++++ spi/common/core-spi/build.gradle.kts | 3 +- .../org/eclipse/edc/spi/asset/AssetIndex.java | 31 +- .../edc/spi/asset/DataAddressResolver.java | 3 +- .../edc/spi/types/domain/asset/Asset.java | 72 ++- .../edc/spi/types/domain/asset/AssetTest.java | 5 +- .../asset/AssetIndexTestBase.java | 383 +++++++-------- .../edc/connector/spi/asset/AssetService.java | 10 + .../org/eclipse/edc/test/e2e/Participant.java | 9 +- .../AssetApiDeprecatedEndToEndTest.java | 398 +++++++++++++++ .../managementapi/AssetApiEndToEndTest.java | 93 +--- .../managementapi/CatalogApiEndToEndTest.java | 9 +- 42 files changed, 2231 insertions(+), 750 deletions(-) rename extensions/control-plane/api/management-api/asset-api/src/main/java/org/eclipse/edc/connector/api/management/asset/{ => v2}/AssetApi.java (99%) rename extensions/control-plane/api/management-api/asset-api/src/main/java/org/eclipse/edc/connector/api/management/asset/{ => v2}/AssetApiController.java (96%) create mode 100644 extensions/control-plane/api/management-api/asset-api/src/main/java/org/eclipse/edc/connector/api/management/asset/v3/AssetApi.java create mode 100644 extensions/control-plane/api/management-api/asset-api/src/main/java/org/eclipse/edc/connector/api/management/asset/v3/AssetApiController.java create mode 100644 extensions/control-plane/api/management-api/asset-api/src/main/java/org/eclipse/edc/connector/api/management/asset/validation/AssetValidator.java rename extensions/control-plane/api/management-api/asset-api/src/test/java/org/eclipse/edc/connector/api/management/asset/{ => v2}/AssetApiControllerTest.java (99%) create mode 100644 extensions/control-plane/api/management-api/asset-api/src/test/java/org/eclipse/edc/connector/api/management/asset/v3/AssetApiControllerTest.java create mode 100644 extensions/control-plane/api/management-api/asset-api/src/test/java/org/eclipse/edc/connector/api/management/asset/validation/AssetValidatorTest.java create mode 100644 system-tests/management-api/management-api-test-runner/src/test/java/org/eclipse/edc/test/e2e/managementapi/AssetApiDeprecatedEndToEndTest.java diff --git a/core/control-plane/catalog-core/src/test/java/org/eclipse/edc/connector/catalog/DatasetResolverImplIntegrationTest.java b/core/control-plane/catalog-core/src/test/java/org/eclipse/edc/connector/catalog/DatasetResolverImplIntegrationTest.java index 8eb600db04f..a21c1e39640 100644 --- a/core/control-plane/catalog-core/src/test/java/org/eclipse/edc/connector/catalog/DatasetResolverImplIntegrationTest.java +++ b/core/control-plane/catalog-core/src/test/java/org/eclipse/edc/connector/catalog/DatasetResolverImplIntegrationTest.java @@ -29,7 +29,6 @@ import org.eclipse.edc.spi.query.QuerySpec; import org.eclipse.edc.spi.types.domain.DataAddress; import org.eclipse.edc.spi.types.domain.asset.Asset; -import org.eclipse.edc.spi.types.domain.asset.AssetEntry; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -77,9 +76,9 @@ void shouldLimitResult_withHeterogenousChunks() { var assets2 = range(24, 113).mapToObj(i -> createAsset("asset" + i).build()).collect(Collectors.toList()); var assets3 = range(113, 178).mapToObj(i -> createAsset("asset" + i).build()).collect(Collectors.toList()); - store(assets1); - store(assets2); - store(assets3); + assets1.forEach(assetIndex::create); + assets2.forEach(assetIndex::create); + assets3.forEach(assetIndex::create); var def1 = getContractDefBuilder("def1").assetsSelector(selectorFrom(assets1)).build(); var def2 = getContractDefBuilder("def2").assetsSelector(selectorFrom(assets2)).build(); @@ -104,8 +103,8 @@ void should_return_offers_subset_when_across_multiple_contract_definitions(int f var maximumRange = max(0, (assets1.size() + assets2.size()) - from); var requestedRange = to - from; - store(assets1); - store(assets2); + assets1.forEach(assetIndex::create); + assets2.forEach(assetIndex::create); var contractDefinition1 = getContractDefBuilder("contract-definition-") .assetsSelector(selectorFrom(assets1)).build(); @@ -125,8 +124,8 @@ void shouldLimitResult_insufficientAssets() { var assets1 = range(0, 12).mapToObj(i -> createAsset("asset" + i).build()).collect(Collectors.toList()); var assets2 = range(12, 18).mapToObj(i -> createAsset("asset" + i).build()).collect(Collectors.toList()); - store(assets1); - store(assets2); + assets1.forEach(assetIndex::create); + assets2.forEach(assetIndex::create); var def1 = getContractDefBuilder("def1").assetsSelector(selectorFrom(assets1)).build(); var def2 = getContractDefBuilder("def2").assetsSelector(selectorFrom(assets2)).build(); @@ -162,11 +161,6 @@ private ParticipantAgent createAgent() { return new ParticipantAgent(emptyMap(), emptyMap()); } - private void store(Collection assets) { - assets.stream().map(a -> new AssetEntry(a, DataAddress.Builder.newInstance().type("test-type").build())) - .forEach(assetIndex::create); - } - private List selectorFrom(Collection assets1) { var ids = assets1.stream().map(Asset::getId).collect(Collectors.toList()); return List.of(new Criterion(Asset.PROPERTY_ID, "in", ids)); @@ -181,7 +175,10 @@ private ContractDefinition.Builder getContractDefBuilder(String id) { } private Asset.Builder createAsset(String id) { - return Asset.Builder.newInstance().id(id).name("test asset " + id); + return Asset.Builder.newInstance() + .id(id) + .name("test asset " + id) + .dataAddress(DataAddress.Builder.newInstance().type("test-type").build()); } static class RangeProvider implements ArgumentsProvider { diff --git a/core/control-plane/catalog-core/src/test/java/org/eclipse/edc/connector/catalog/DatasetResolverImplPerformanceTest.java b/core/control-plane/catalog-core/src/test/java/org/eclipse/edc/connector/catalog/DatasetResolverImplPerformanceTest.java index 45dcb693d9a..5986b8b5b5f 100644 --- a/core/control-plane/catalog-core/src/test/java/org/eclipse/edc/connector/catalog/DatasetResolverImplPerformanceTest.java +++ b/core/control-plane/catalog-core/src/test/java/org/eclipse/edc/connector/catalog/DatasetResolverImplPerformanceTest.java @@ -29,7 +29,6 @@ import org.eclipse.edc.spi.query.QuerySpec; import org.eclipse.edc.spi.types.domain.DataAddress; import org.eclipse.edc.spi.types.domain.asset.Asset; -import org.eclipse.edc.spi.types.domain.asset.AssetEntry; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -70,7 +69,7 @@ void oneAssetPerDefinition(DatasetResolver datasetResolver, ContractDefinitionSt .contractPolicyId("policy") .assetsSelectorCriterion(criterion(Asset.PROPERTY_ID, "=", String.valueOf(i))).build() ).forEach(contractDefinitionStore::save); - range(0, 10000).mapToObj(i -> createAsset(String.valueOf(i)).build()).map(this::createAssetEntry).forEach(assetIndex::create); + range(0, 10000).mapToObj(i -> createAsset(String.valueOf(i)).build()).forEach(assetIndex::create); var firstPageQuery = QuerySpec.Builder.newInstance().offset(0).limit(100).build(); var firstPageDatasets = queryDatasetsIn(datasetResolver, firstPageQuery, ofSeconds(1)); @@ -87,7 +86,7 @@ void oneAssetPerDefinition(DatasetResolver datasetResolver, ContractDefinitionSt void fewDefinitionsSelectAllAssets(DatasetResolver datasetResolver, ContractDefinitionStore contractDefinitionStore, AssetIndex assetIndex, PolicyDefinitionStore policyDefinitionStore) { policyDefinitionStore.create(createPolicyDefinition("policy").build()); range(0, 10).mapToObj(i -> createContractDefinition(String.valueOf(i)).accessPolicyId("policy").contractPolicyId("policy").build()).forEach(contractDefinitionStore::save); - range(0, 10000).mapToObj(i -> createAsset(String.valueOf(i)).build()).map(this::createAssetEntry).forEach(assetIndex::create); + range(0, 10000).mapToObj(i -> createAsset(String.valueOf(i)).build()).forEach(assetIndex::create); var firstPageQuery = QuerySpec.Builder.newInstance().offset(0).limit(100).build(); var firstPageDatasets = queryDatasetsIn(datasetResolver, firstPageQuery, ofSeconds(1)); @@ -116,12 +115,10 @@ private ContractDefinition.Builder createContractDefinition(String id) { .contractPolicyId("contract"); } - @NotNull - private AssetEntry createAssetEntry(Asset it) { - return new AssetEntry(it, DataAddress.Builder.newInstance().type("type").build()); - } - private Asset.Builder createAsset(String id) { - return Asset.Builder.newInstance().id(id).name("test asset " + id); + return Asset.Builder.newInstance() + .id(id) + .name("test asset " + id) + .dataAddress(DataAddress.Builder.newInstance().type("type").build()); } } diff --git a/core/control-plane/contract-core/src/test/java/org/eclipse/edc/connector/contract/negotiation/ContractNegotiationIntegrationTest.java b/core/control-plane/contract-core/src/test/java/org/eclipse/edc/connector/contract/negotiation/ContractNegotiationIntegrationTest.java index 0d2aaa0142c..44aa1bcc44c 100644 --- a/core/control-plane/contract-core/src/test/java/org/eclipse/edc/connector/contract/negotiation/ContractNegotiationIntegrationTest.java +++ b/core/control-plane/contract-core/src/test/java/org/eclipse/edc/connector/contract/negotiation/ContractNegotiationIntegrationTest.java @@ -35,6 +35,7 @@ import org.eclipse.edc.connector.policy.spi.store.PolicyDefinitionStore; import org.eclipse.edc.connector.service.contractnegotiation.ContractNegotiationProtocolServiceImpl; import org.eclipse.edc.connector.spi.contractnegotiation.ContractNegotiationProtocolService; +import org.eclipse.edc.junit.annotations.ComponentTest; import org.eclipse.edc.policy.model.Action; import org.eclipse.edc.policy.model.Duty; import org.eclipse.edc.policy.model.Policy; @@ -76,7 +77,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -//@ComponentTest +@ComponentTest class ContractNegotiationIntegrationTest { private static final String CONSUMER_ID = "consumer"; private static final String PROVIDER_ID = "provider"; diff --git a/core/control-plane/contract-core/src/test/java/org/eclipse/edc/connector/contract/offer/ContractOfferResolverImplIntegrationTest.java b/core/control-plane/contract-core/src/test/java/org/eclipse/edc/connector/contract/offer/ContractOfferResolverImplIntegrationTest.java index e881cc1fa39..dcc5bb0e453 100644 --- a/core/control-plane/contract-core/src/test/java/org/eclipse/edc/connector/contract/offer/ContractOfferResolverImplIntegrationTest.java +++ b/core/control-plane/contract-core/src/test/java/org/eclipse/edc/connector/contract/offer/ContractOfferResolverImplIntegrationTest.java @@ -32,7 +32,6 @@ import org.eclipse.edc.spi.query.Criterion; import org.eclipse.edc.spi.types.domain.DataAddress; import org.eclipse.edc.spi.types.domain.asset.Asset; -import org.eclipse.edc.spi.types.domain.asset.AssetEntry; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtensionContext; @@ -90,9 +89,9 @@ void shouldLimitResult_withHeterogenousChunks() { var assets2 = range(24, 113).mapToObj(i -> createAsset("asset" + i).build()).collect(Collectors.toList()); var assets3 = range(113, 178).mapToObj(i -> createAsset("asset" + i).build()).collect(Collectors.toList()); - store(assets1); - store(assets2); - store(assets3); + assets1.forEach(assetIndex::create); + assets2.forEach(assetIndex::create); + assets3.forEach(assetIndex::create); var def1 = getContractDefBuilder("def1").assetsSelector(selectorFrom(assets1)).build(); var def2 = getContractDefBuilder("def2").assetsSelector(selectorFrom(assets2)).build(); @@ -123,8 +122,8 @@ void should_return_offers_subset_when_across_multiple_contract_definitions(int f var maximumRange = max(0, (assets1.size() + assets2.size()) - from); var requestedRange = to - from; - store(assets1); - store(assets2); + assets1.forEach(assetIndex::create); + assets2.forEach(assetIndex::create); var contractDefinition1 = getContractDefBuilder("contract-definition-") .assetsSelector(selectorFrom(assets1)).build(); @@ -144,8 +143,8 @@ void shouldLimitResult_insufficientAssets() { var assets1 = range(0, 12).mapToObj(i -> createAsset("asset" + i).build()).collect(Collectors.toList()); var assets2 = range(12, 18).mapToObj(i -> createAsset("asset" + i).build()).collect(Collectors.toList()); - store(assets1); - store(assets2); + assets1.forEach(assetIndex::create); + assets2.forEach(assetIndex::create); var def1 = getContractDefBuilder("def1").assetsSelector(selectorFrom(assets1)).build(); var def2 = getContractDefBuilder("def2").assetsSelector(selectorFrom(assets2)).build(); @@ -186,11 +185,6 @@ void shouldLimitResult_pageOffsetLargerThanNumAssets() { verify(policyStore, never()).findById("contract"); } - private void store(Collection assets) { - assets.stream().map(a -> new AssetEntry(a, DataAddress.Builder.newInstance().type("test-type").build())) - .forEach(assetIndex::create); - } - private List selectorFrom(Collection assets1) { var ids = assets1.stream().map(Asset::getId).toList(); return List.of(new Criterion(Asset.PROPERTY_ID, "in", ids)); @@ -204,7 +198,10 @@ private ContractDefinition.Builder getContractDefBuilder(String id) { } private Asset.Builder createAsset(String id) { - return Asset.Builder.newInstance().id(id).name("test asset " + id); + return Asset.Builder.newInstance() + .id(id) + .name("test asset " + id) + .dataAddress(DataAddress.Builder.newInstance().type("test-type").build()); } static class RangeProvider implements ArgumentsProvider { diff --git a/core/control-plane/control-plane-aggregate-services/src/main/java/org/eclipse/edc/connector/service/asset/AssetServiceImpl.java b/core/control-plane/control-plane-aggregate-services/src/main/java/org/eclipse/edc/connector/service/asset/AssetServiceImpl.java index 32c7ee66272..ccbfa6cc12f 100644 --- a/core/control-plane/control-plane-aggregate-services/src/main/java/org/eclipse/edc/connector/service/asset/AssetServiceImpl.java +++ b/core/control-plane/control-plane-aggregate-services/src/main/java/org/eclipse/edc/connector/service/asset/AssetServiceImpl.java @@ -70,13 +70,18 @@ public ServiceResult> query(QuerySpec query) { @Override public ServiceResult create(Asset asset, DataAddress dataAddress) { - var validDataAddress = dataAddressValidator.validate(dataAddress); + return create(asset.toBuilder().dataAddress(dataAddress).build()); + } + + @Override + public ServiceResult create(Asset asset) { + var validDataAddress = dataAddressValidator.validate(asset.getDataAddress()); if (validDataAddress.failed()) { return ServiceResult.badRequest(validDataAddress.getFailureMessages()); } return transactionContext.execute(() -> { - var createResult = index.create(asset, dataAddress); + var createResult = index.create(asset); if (createResult.succeeded()) { observable.invokeForEach(l -> l.created(asset)); return ServiceResult.success(asset); diff --git a/core/control-plane/control-plane-aggregate-services/src/test/java/org/eclipse/edc/connector/service/asset/AssetServiceImplTest.java b/core/control-plane/control-plane-aggregate-services/src/test/java/org/eclipse/edc/connector/service/asset/AssetServiceImplTest.java index 205b707965d..cd53ce71bc6 100644 --- a/core/control-plane/control-plane-aggregate-services/src/test/java/org/eclipse/edc/connector/service/asset/AssetServiceImplTest.java +++ b/core/control-plane/control-plane-aggregate-services/src/test/java/org/eclipse/edc/connector/service/asset/AssetServiceImplTest.java @@ -19,7 +19,9 @@ import org.eclipse.edc.connector.contract.spi.negotiation.store.ContractNegotiationStore; import org.eclipse.edc.connector.contract.spi.types.agreement.ContractAgreement; import org.eclipse.edc.connector.contract.spi.types.negotiation.ContractNegotiation; +import org.eclipse.edc.connector.spi.asset.AssetService; import org.eclipse.edc.policy.model.Policy; +import org.eclipse.edc.service.spi.result.ServiceFailure; import org.eclipse.edc.service.spi.result.ServiceResult; import org.eclipse.edc.spi.asset.AssetIndex; import org.eclipse.edc.spi.dataaddress.DataAddressValidator; @@ -53,9 +55,11 @@ import static org.eclipse.edc.service.spi.result.ServiceFailure.Reason.NOT_FOUND; import static org.eclipse.edc.spi.query.Criterion.criterion; import static org.junit.jupiter.params.provider.Arguments.arguments; +import static org.mockito.AdditionalMatchers.and; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isA; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; @@ -72,7 +76,7 @@ class AssetServiceImplTest { private final AssetObservable observable = mock(AssetObservable.class); private final DataAddressValidator dataAddressValidator = mock(DataAddressValidator.class); - private final AssetServiceImpl service = new AssetServiceImpl(index, contractNegotiationStore, dummyTransactionContext, + private final AssetService service = new AssetServiceImpl(index, contractNegotiationStore, dummyTransactionContext, observable, dataAddressValidator); @Test @@ -129,37 +133,77 @@ void query_invalidFilter(Criterion filter) { @Test void createAsset_shouldCreateAssetIfItDoesNotAlreadyExist() { + when(dataAddressValidator.validate(any())).thenReturn(Result.success()); + var assetId = "assetId"; + var asset = createAsset(assetId); + when(index.create(asset)).thenReturn(StoreResult.success()); + + var inserted = service.create(asset); + + assertThat(inserted.succeeded()).isTrue(); + assertThat(inserted.getContent()).matches(hasId(assetId)); + verify(index).create(and(isA(Asset.class), argThat(it -> assetId.equals(it.getId())))); + verifyNoMoreInteractions(index); + verify(observable).invokeForEach(any()); + } + + @Test + void createAsset_shouldNotCreateAssetIfItAlreadyExists() { + when(dataAddressValidator.validate(any())).thenReturn(Result.success()); + var asset = createAsset("assetId"); + when(index.create(asset)).thenReturn(StoreResult.alreadyExists("test")); + + var inserted = service.create(asset); + + assertThat(inserted).isFailed().extracting(ServiceFailure::getReason).isEqualTo(CONFLICT); + } + + @Test + void createAsset_shouldNotCreateAssetIfDataAddressInvalid() { + var asset = createAsset("assetId"); + when(dataAddressValidator.validate(any())).thenReturn(Result.failure("Data address is invalid")); + + var result = service.create(asset); + + Assertions.assertThat(result).satisfies(ServiceResult::failed) + .extracting(ServiceResult::reason) + .isEqualTo(BAD_REQUEST); + verifyNoInteractions(index); + } + + @Test + @Deprecated(since = "0.1.2") + void createAssetDeprecated_shouldCreateAssetIfItDoesNotAlreadyExist() { when(dataAddressValidator.validate(any())).thenReturn(Result.success()); var assetId = "assetId"; var asset = createAsset(assetId); var addressType = "addressType"; var dataAddress = DataAddress.Builder.newInstance().type(addressType).build(); - when(index.create(asset, dataAddress)).thenReturn(StoreResult.success()); + when(index.create(isA(Asset.class))).thenReturn(StoreResult.success()); var inserted = service.create(asset, dataAddress); assertThat(inserted.succeeded()).isTrue(); assertThat(inserted.getContent()).matches(hasId(assetId)); - verify(index).create(argThat(it -> assetId.equals(it.getId())), argThat(it -> addressType.equals(it.getType()))); - verifyNoMoreInteractions(index); verify(observable).invokeForEach(any()); } @Test - void createAsset_shouldNotCreateAssetIfItAlreadyExists() { + @Deprecated(since = "0.1.2") + void createAssetDeprecated_shouldNotCreateAssetIfItAlreadyExists() { when(dataAddressValidator.validate(any())).thenReturn(Result.success()); var asset = createAsset("assetId"); var dataAddress = DataAddress.Builder.newInstance().type("addressType").build(); - when(index.create(asset, dataAddress)).thenReturn(StoreResult.alreadyExists("test")); + when(index.create(isA(Asset.class))).thenReturn(StoreResult.alreadyExists("test")); var inserted = service.create(asset, dataAddress); - assertThat(inserted.succeeded()).isFalse(); - assertThat(inserted.reason()).isEqualTo(CONFLICT); + assertThat(inserted).isFailed().extracting(ServiceFailure::getReason).isEqualTo(CONFLICT); } @Test - void createAsset_shouldNotCreateAssetIfDataAddressInvalid() { + @Deprecated(since = "0.1.2") + void createAssetDeprecated_shouldNotCreateAssetIfDataAddressInvalid() { var asset = createAsset("assetId"); var dataAddress = DataAddress.Builder.newInstance().type("addressType").build(); when(dataAddressValidator.validate(any())).thenReturn(Result.failure("Data address is invalid")); @@ -307,6 +351,6 @@ private Predicate hasId(String assetId) { } private Asset createAsset(String assetId) { - return Asset.Builder.newInstance().id(assetId).build(); + return Asset.Builder.newInstance().id(assetId).dataAddress(DataAddress.Builder.newInstance().type("any").build()).build(); } } diff --git a/core/control-plane/control-plane-aggregate-services/src/test/java/org/eclipse/edc/connector/service/contractnegotiation/ContractNegotiationEventDispatchTest.java b/core/control-plane/control-plane-aggregate-services/src/test/java/org/eclipse/edc/connector/service/contractnegotiation/ContractNegotiationEventDispatchTest.java index 485d60bbac8..76bee338f7a 100644 --- a/core/control-plane/control-plane-aggregate-services/src/test/java/org/eclipse/edc/connector/service/contractnegotiation/ContractNegotiationEventDispatchTest.java +++ b/core/control-plane/control-plane-aggregate-services/src/test/java/org/eclipse/edc/connector/service/contractnegotiation/ContractNegotiationEventDispatchTest.java @@ -98,7 +98,7 @@ void shouldDispatchEventsOnProviderContractNegotiationStateChanges(EventRouter e .build(); contractDefinitionStore.save(contractDefinition); policyDefinitionStore.create(PolicyDefinition.Builder.newInstance().id("policyId").policy(policy).build()); - assetIndex.create(Asset.Builder.newInstance().id("assetId").build(), DataAddress.Builder.newInstance().type("any").build()); + assetIndex.create(Asset.Builder.newInstance().id("assetId").dataAddress(DataAddress.Builder.newInstance().type("any").build()).build()); service.notifyRequested(createContractOfferRequest(policy, "assetId"), token); diff --git a/core/control-plane/control-plane-core/src/main/java/org/eclipse/edc/connector/defaults/storage/assetindex/InMemoryAssetIndex.java b/core/control-plane/control-plane-core/src/main/java/org/eclipse/edc/connector/defaults/storage/assetindex/InMemoryAssetIndex.java index 3721abe073d..b77fab5b1cb 100644 --- a/core/control-plane/control-plane-core/src/main/java/org/eclipse/edc/connector/defaults/storage/assetindex/InMemoryAssetIndex.java +++ b/core/control-plane/control-plane-core/src/main/java/org/eclipse/edc/connector/defaults/storage/assetindex/InMemoryAssetIndex.java @@ -22,9 +22,9 @@ import org.eclipse.edc.spi.result.StoreResult; import org.eclipse.edc.spi.types.domain.DataAddress; import org.eclipse.edc.spi.types.domain.asset.Asset; -import org.eclipse.edc.spi.types.domain.asset.AssetEntry; import org.jetbrains.annotations.Nullable; +import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Objects; @@ -54,31 +54,14 @@ public InMemoryAssetIndex() { public Stream queryAssets(QuerySpec querySpec) { lock.readLock().lock(); try { - // filter - var result = filterBy(querySpec.getFilterExpression()); - - // ... then sort - var sortField = querySpec.getSortField(); - if (sortField != null) { - result = result.sorted((asset1, asset2) -> { - var f1 = asComparable(asset1.getProperty(sortField)); - var f2 = asComparable(asset2.getProperty(sortField)); - - // try for private properties next - if (f1 == null && f2 == null) { - f1 = asComparable(asset1.getPrivateProperty(sortField)); - f2 = asComparable(asset2.getPrivateProperty(sortField)); - } - - if (f1 == null || f2 == null) { - throw new IllegalArgumentException(format("Cannot sort by field %s, it does not exist on one or more Assets", sortField)); - } - return querySpec.getSortOrder() == SortOrder.ASC ? f1.compareTo(f2) : f2.compareTo(f1); - }); - } + var comparator = querySpec.getSortField() == null + ? (Comparator) (o1, o2) -> 0 + : new AssetComparator(querySpec.getSortField(), querySpec.getSortOrder()); + + return filterBy(querySpec.getFilterExpression()) + .sorted(comparator) + .skip(querySpec.getOffset()).limit(querySpec.getLimit()); - // ... then limit - return result.skip(querySpec.getOffset()).limit(querySpec.getLimit()); } finally { lock.readLock().unlock(); } @@ -98,14 +81,19 @@ public Asset findById(String assetId) { } @Override - public StoreResult create(AssetEntry item) { + public StoreResult create(Asset asset) { lock.writeLock().lock(); try { - var id = item.getAsset().getId(); + if (asset.hasDuplicatePropertyKeys()) { + var msg = format(DUPLICATE_PROPERTY_KEYS_TEMPLATE); + return StoreResult.duplicateKeys(msg); + } + + var id = asset.getId(); if (cache.containsKey(id)) { return StoreResult.alreadyExists(format(ASSET_EXISTS_TEMPLATE, id)); } - add(item.getAsset(), item.getDataAddress()); + add(asset, asset.getDataAddress()); } finally { lock.writeLock().unlock(); } @@ -133,7 +121,7 @@ public long countAssets(List criteria) { public StoreResult updateAsset(Asset asset) { lock.writeLock().lock(); try { - String id = asset.getId(); + var id = asset.getId(); Objects.requireNonNull(asset, "asset"); Objects.requireNonNull(id, "assetId"); if (cache.containsKey(id)) { @@ -156,7 +144,7 @@ public StoreResult updateDataAddress(String assetId, DataAddress da dataAddresses.put(assetId, dataAddress); return StoreResult.success(dataAddress); } - return StoreResult.notFound(format(DATAADDRESS_NOT_FOUND_TEMPLATE, assetId)); + return StoreResult.notFound(format(DATA_ADDRESS_NOT_FOUND_TEMPLATE, assetId)); } finally { lock.writeLock().unlock(); } @@ -182,10 +170,6 @@ private Stream filterBy(List criteria) { .filter(predicate); } - private @Nullable Comparable asComparable(Object property) { - return property instanceof Comparable ? (Comparable) property : null; - } - private Asset delete(String assetId) { dataAddresses.remove(assetId); return cache.remove(assetId); @@ -195,10 +179,28 @@ private Asset delete(String assetId) { * this method is NOT secured with locks, any guarding must take place in the calling method! */ private void add(Asset asset, DataAddress address) { - String id = asset.getId(); + var id = asset.getId(); Objects.requireNonNull(asset, "asset"); Objects.requireNonNull(id, "asset.getId()"); cache.put(id, asset); dataAddresses.put(id, address); } + + private record AssetComparator(String sortField, SortOrder sortOrder) implements Comparator { + + @Override + public int compare(Asset asset1, Asset asset2) { + var f1 = asComparable(asset1.getPropertyOrPrivate(sortField)); + var f2 = asComparable(asset2.getPropertyOrPrivate(sortField)); + + if (f1 == null || f2 == null) { + throw new IllegalArgumentException(format("Cannot sort by field %s, it does not exist on one or more Assets", sortField)); + } + return sortOrder == SortOrder.ASC ? f1.compareTo(f2) : f2.compareTo(f1); + } + + private @Nullable Comparable asComparable(Object property) { + return property instanceof Comparable ? (Comparable) property : null; + } + } } diff --git a/core/control-plane/control-plane-core/src/test/java/org/eclipse/edc/connector/defaults/storage/assetindex/InMemoryAssetIndexTest.java b/core/control-plane/control-plane-core/src/test/java/org/eclipse/edc/connector/defaults/storage/assetindex/InMemoryAssetIndexTest.java index af72e40f89c..a7bcd8c1265 100644 --- a/core/control-plane/control-plane-core/src/test/java/org/eclipse/edc/connector/defaults/storage/assetindex/InMemoryAssetIndexTest.java +++ b/core/control-plane/control-plane-core/src/test/java/org/eclipse/edc/connector/defaults/storage/assetindex/InMemoryAssetIndexTest.java @@ -16,25 +16,14 @@ import org.eclipse.edc.spi.asset.AssetIndex; -import org.eclipse.edc.spi.query.QuerySpec; -import org.eclipse.edc.spi.query.SortOrder; -import org.eclipse.edc.spi.result.StoreResult; import org.eclipse.edc.spi.testfixtures.asset.AssetIndexTestBase; -import org.eclipse.edc.spi.types.domain.asset.Asset; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; import java.util.Collection; import java.util.List; -import java.util.UUID; -import java.util.stream.Collectors; -import java.util.stream.IntStream; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.eclipse.edc.spi.query.Criterion.criterion; -import static org.eclipse.edc.spi.result.StoreFailure.Reason.NOT_FOUND; class InMemoryAssetIndexTest extends AssetIndexTestBase { + private InMemoryAssetIndex index; @BeforeEach @@ -42,156 +31,6 @@ void setUp() { index = new InMemoryAssetIndex(); } - @Test - void findById() { - String id = UUID.randomUUID().toString(); - var testAsset = createAsset("barbaz", id); - index.create(testAsset, createDataAddress(testAsset)); - - var result = index.findById(id); - - assertThat(result).isNotNull().isEqualTo(testAsset); - } - - @Test - void findById_notfound() { - String id = UUID.randomUUID().toString(); - var testAsset = createAsset("foobar", id); - index.create(testAsset, createDataAddress(testAsset)); - - var result = index.findById("not-exist"); - - assertThat(result).isNull(); - } - - @Test - void findAll_noQuerySpec() { - var assets = IntStream.range(0, 10).mapToObj(i -> createAsset("test-asset", "id" + i)) - .peek(a -> index.create(a, createDataAddress(a))).collect(Collectors.toList()); - - assertThat(index.queryAssets(QuerySpec.Builder.newInstance().build())).containsAll(assets); - } - - @Test - void findAll_withPaging_noSortOrderDesc() { - IntStream.range(0, 10) - .mapToObj(i -> createAsset("test-asset", "id" + i)) - .forEach(a -> index.create(a, createDataAddress(a))); - - var spec = QuerySpec.Builder.newInstance().sortOrder(SortOrder.DESC).offset(5).limit(2).build(); - - var all = index.queryAssets(spec); - assertThat(all).hasSize(2); - } - - @Test - void findAll_withPaging_noSortOrderAsc() { - IntStream.range(0, 10) - .mapToObj(i -> createAsset("test-asset", "id" + i)) - .forEach(a -> index.create(a, createDataAddress(a))); - - var spec = QuerySpec.Builder.newInstance().sortOrder(SortOrder.ASC).offset(3).limit(3).build(); - - var all = index.queryAssets(spec); - assertThat(all).hasSize(3); - } - - @Test - void findAll_withFiltering() { - var assets = IntStream.range(0, 10) - .mapToObj(i -> createAsset("test-asset", "id" + i)) - .peek(a -> index.create(a, createDataAddress(a))) - .collect(Collectors.toList()); - - var spec = QuerySpec.Builder.newInstance().filter(criterion(Asset.PROPERTY_ID, "=", "id1")).build(); - assertThat(index.queryAssets(spec)).hasSize(1).containsExactly(assets.get(1)); - } - - @Test - void findAll_withFiltering_limitExceedsResultSize() { - IntStream.range(0, 10) - .mapToObj(i -> createAsset("test-asset" + i)) - .forEach(a -> index.create(a, createDataAddress(a))); - - var spec = QuerySpec.Builder.newInstance() - .sortOrder(SortOrder.ASC) - .offset(15) - .limit(10) - .build(); - assertThat(index.queryAssets(spec)).isEmpty(); - } - - @Test - void findAll_withSorting() { - var assets = IntStream.range(0, 10) - .mapToObj(i -> createAsset("test-asset", "id" + i)) - .peek(a -> index.create(a, createDataAddress(a))) - .collect(Collectors.toList()); - - var spec = QuerySpec.Builder.newInstance().sortField(Asset.PROPERTY_ID).sortOrder(SortOrder.ASC).build(); - assertThat(index.queryAssets(spec)).containsAll(assets); - } - - @Test - void findAll_withPrivateSorting() { - var assets = IntStream.range(0, 10) - .mapToObj(i -> createAssetBuilder("" + i).privateProperty("pKey", "pValue").build()) - .peek(a -> index.create(a, createDataAddress(a))) - .collect(Collectors.toList()); - - var spec = QuerySpec.Builder.newInstance().sortField("pKey").sortOrder(SortOrder.ASC).build(); - assertThat(index.queryAssets(spec)).containsAll(assets); - } - - @Test - void deleteById_whenMissing_returnsNull() { - assertThat(index.deleteById("not-exists")).isNotNull().extracting(StoreResult::reason).isEqualTo(NOT_FOUND); - } - - @Test - void updateAsset_whenNotExists_returnsFailure() { - var id = UUID.randomUUID().toString(); - var asset = createAsset("test-asset", id); - var result = index.updateAsset(asset); - assertThat(result.succeeded()).isFalse(); - } - - @Test - void updateAsset_whenExists_returnsUpdatedAsset() { - var id = UUID.randomUUID().toString(); - var asset = createAsset("test-asset", id); - var dataAddress = createDataAddress(asset); - - index.create(asset, dataAddress); - - var newAsset = createAsset("new-name", id); - var result = index.updateAsset(newAsset); - assertThat(result).isNotNull().extracting(StoreResult::getContent).usingRecursiveComparison().isEqualTo(newAsset); - } - - @Test - void updateDataAddress_whenNotExists_returnsFailure() { - var id = UUID.randomUUID().toString(); - var address = createDataAddress(createAsset("test-asset", id)); - var result = index.updateDataAddress(id, address); - assertThat(result.succeeded()).isFalse(); - } - - @Test - void updateDataAddress_whenExists_returnsUpdatedDataAddress() { - var id = UUID.randomUUID().toString(); - var asset = createAsset("test-asset", id); - var dataAddress = createDataAddress(asset); - - index.create(asset, dataAddress); - - dataAddress.getProperties().put("new", "value"); - var result = index.updateDataAddress(id, dataAddress); - assertThat(result.succeeded()).isTrue(); - assertThat(result.getContent()).isNotNull().usingRecursiveComparison().isEqualTo(dataAddress); - assertThat(result.getContent().getProperties().get("new")).isEqualTo("value"); - } - @Override protected Collection getSupportedOperators() { return List.of("=", "in"); @@ -202,5 +41,4 @@ protected AssetIndex getAssetIndex() { return index; } - } diff --git a/core/control-plane/control-plane-core/src/test/java/org/eclipse/edc/connector/defaults/storage/assetindex/InMemoryDataAddressResolverTest.java b/core/control-plane/control-plane-core/src/test/java/org/eclipse/edc/connector/defaults/storage/assetindex/InMemoryDataAddressResolverTest.java index f06757d6934..fda8f1b93e4 100644 --- a/core/control-plane/control-plane-core/src/test/java/org/eclipse/edc/connector/defaults/storage/assetindex/InMemoryDataAddressResolverTest.java +++ b/core/control-plane/control-plane-core/src/test/java/org/eclipse/edc/connector/defaults/storage/assetindex/InMemoryDataAddressResolverTest.java @@ -35,9 +35,9 @@ void setUp() { @Test void resolveForAsset() { var id = UUID.randomUUID().toString(); - var testAsset = createAsset("foobar", id); - var address = createDataAddress(testAsset); - resolver.create(testAsset, address); + var address = createDataAddress(); + var testAsset = createAssetBuilder("foobar", id).dataAddress(address).build(); + resolver.create(testAsset); assertThat(resolver.resolveForAsset(testAsset.getId())).isEqualTo(address); } @@ -45,31 +45,31 @@ void resolveForAsset() { @Test void resolveForAsset_assetNull_raisesException() { var id = UUID.randomUUID().toString(); - var testAsset = createAsset("foobar", id); - var address = createDataAddress(testAsset); - resolver.create(testAsset, address); + var address = createDataAddress(); + var testAsset = createAssetBuilder("foobar", id).dataAddress(address).build(); + resolver.create(testAsset); assertThatThrownBy(() -> resolver.resolveForAsset(null)).isInstanceOf(NullPointerException.class); } @Test void resolveForAsset_whenAssetDeleted_raisesException() { - var testAsset = createAsset("foobar", UUID.randomUUID().toString()); - var address = createDataAddress(testAsset); - resolver.create(testAsset, address); + var address = createDataAddress(); + var testAsset = createAssetBuilder("foobar", UUID.randomUUID().toString()).dataAddress(address).build(); + resolver.create(testAsset); resolver.deleteById(testAsset.getId()); assertThat(resolver.resolveForAsset(testAsset.getId())).isNull(); } - private Asset createAsset(String name, String id) { - return Asset.Builder.newInstance().id(id).name(name).version("1").contentType("type").build(); + private static Asset.Builder createAssetBuilder(String name, String id) { + return Asset.Builder.newInstance().id(id).name(name).version("1").contentType("type"); } - private DataAddress createDataAddress(Asset asset) { + private DataAddress createDataAddress() { return DataAddress.Builder.newInstance() .keyName("test-keyname") - .type(asset.getContentType()) + .type("type") .build(); } } diff --git a/extensions/common/json-ld/src/main/java/org/eclipse/edc/jsonld/transformer/from/JsonObjectFromAssetTransformer.java b/extensions/common/json-ld/src/main/java/org/eclipse/edc/jsonld/transformer/from/JsonObjectFromAssetTransformer.java index ee08eb5858c..b908f16f16e 100644 --- a/extensions/common/json-ld/src/main/java/org/eclipse/edc/jsonld/transformer/from/JsonObjectFromAssetTransformer.java +++ b/extensions/common/json-ld/src/main/java/org/eclipse/edc/jsonld/transformer/from/JsonObjectFromAssetTransformer.java @@ -38,21 +38,24 @@ public JsonObjectFromAssetTransformer(JsonBuilderFactory jsonFactory, ObjectMapp @Override public @Nullable JsonObject transform(@NotNull Asset asset, @NotNull TransformerContext context) { - var builder = jsonFactory.createObjectBuilder(); - builder.add(ID, asset.getId()); - builder.add(TYPE, Asset.EDC_ASSET_TYPE); - //transform public properties + var builder = jsonFactory.createObjectBuilder() + .add(ID, asset.getId()) + .add(TYPE, Asset.EDC_ASSET_TYPE); + var propBuilder = jsonFactory.createObjectBuilder(); transformProperties(asset.getProperties(), propBuilder, mapper, context); builder.add(Asset.EDC_ASSET_PROPERTIES, propBuilder); - - //transform private properties if (asset.getPrivateProperties() != null && !asset.getPrivateProperties().isEmpty()) { var privatePropBuilder = jsonFactory.createObjectBuilder(); transformProperties(asset.getPrivateProperties(), privatePropBuilder, mapper, context); builder.add(Asset.EDC_ASSET_PRIVATE_PROPERTIES, privatePropBuilder); } + + if (asset.getDataAddress() != null) { + builder.add(Asset.EDC_ASSET_DATA_ADDRESS, context.transform(asset.getDataAddress(), JsonObject.class)); + } + return builder.build(); } } diff --git a/extensions/common/json-ld/src/main/java/org/eclipse/edc/jsonld/transformer/to/JsonObjectToAssetTransformer.java b/extensions/common/json-ld/src/main/java/org/eclipse/edc/jsonld/transformer/to/JsonObjectToAssetTransformer.java index 0055ebdb6ca..efe8ba36b90 100644 --- a/extensions/common/json-ld/src/main/java/org/eclipse/edc/jsonld/transformer/to/JsonObjectToAssetTransformer.java +++ b/extensions/common/json-ld/src/main/java/org/eclipse/edc/jsonld/transformer/to/JsonObjectToAssetTransformer.java @@ -18,11 +18,13 @@ import jakarta.json.JsonObject; import jakarta.json.JsonValue; import org.eclipse.edc.jsonld.spi.transformer.AbstractJsonLdTransformer; +import org.eclipse.edc.spi.types.domain.DataAddress; import org.eclipse.edc.spi.types.domain.asset.Asset; import org.eclipse.edc.transform.spi.TransformerContext; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import static org.eclipse.edc.spi.types.domain.asset.Asset.EDC_ASSET_DATA_ADDRESS; import static org.eclipse.edc.spi.types.domain.asset.Asset.EDC_ASSET_PRIVATE_PROPERTIES; /** @@ -49,6 +51,8 @@ private void transformProperties(String key, JsonValue jsonValue, Asset.Builder } else if (EDC_ASSET_PRIVATE_PROPERTIES.equals(key) && jsonValue instanceof JsonArray) { var props = jsonValue.asJsonArray().getJsonObject(0); visitProperties(props, (k, val) -> transformProperties(k, val, builder, context, true)); + } else if (EDC_ASSET_DATA_ADDRESS.equals(key) && jsonValue instanceof JsonArray) { + builder.dataAddress(transformObject(jsonValue, DataAddress.class, context)); } else { if (isPrivate) { builder.privateProperty(key, transformGenericProperty(jsonValue, context)); diff --git a/extensions/common/json-ld/src/test/java/org/eclipse/edc/jsonld/transformer/from/JsonObjectFromAssetTransformerTest.java b/extensions/common/json-ld/src/test/java/org/eclipse/edc/jsonld/transformer/from/JsonObjectFromAssetTransformerTest.java index 354de119d40..c09b3bd9339 100644 --- a/extensions/common/json-ld/src/test/java/org/eclipse/edc/jsonld/transformer/from/JsonObjectFromAssetTransformerTest.java +++ b/extensions/common/json-ld/src/test/java/org/eclipse/edc/jsonld/transformer/from/JsonObjectFromAssetTransformerTest.java @@ -15,8 +15,10 @@ package org.eclipse.edc.jsonld.transformer.from; import jakarta.json.Json; +import jakarta.json.JsonArrayBuilder; import jakarta.json.JsonObject; import org.eclipse.edc.jsonld.transformer.Payload; +import org.eclipse.edc.spi.types.domain.DataAddress; import org.eclipse.edc.spi.types.domain.asset.Asset; import org.eclipse.edc.transform.spi.TransformerContext; import org.junit.jupiter.api.BeforeEach; @@ -24,14 +26,23 @@ import java.util.Map; +import static jakarta.json.Json.createArrayBuilder; +import static jakarta.json.Json.createObjectBuilder; import static org.assertj.core.api.Assertions.assertThat; import static org.eclipse.edc.jsonld.spi.JsonLdKeywords.ID; import static org.eclipse.edc.jsonld.spi.JsonLdKeywords.TYPE; +import static org.eclipse.edc.jsonld.spi.JsonLdKeywords.VALUE; import static org.eclipse.edc.jsonld.util.JacksonJsonLd.createObjectMapper; import static org.eclipse.edc.spi.CoreConstants.EDC_NAMESPACE; +import static org.eclipse.edc.spi.types.domain.DataAddress.EDC_DATA_ADDRESS_TYPE_PROPERTY; +import static org.eclipse.edc.spi.types.domain.asset.Asset.EDC_ASSET_DATA_ADDRESS; import static org.eclipse.edc.spi.types.domain.asset.Asset.EDC_ASSET_PRIVATE_PROPERTIES; import static org.eclipse.edc.spi.types.domain.asset.Asset.EDC_ASSET_PROPERTIES; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isA; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; class JsonObjectFromAssetTransformerTest { @@ -40,6 +51,7 @@ class JsonObjectFromAssetTransformerTest { private static final String TEST_DESCRIPTION = "test-description"; private static final String TEST_VERSION = "0.6.9"; private static final String TEST_ASSET_NAME = "test-asset"; + private final TransformerContext context = mock(TransformerContext.class); private JsonObjectFromAssetTransformer transformer; @BeforeEach @@ -49,15 +61,18 @@ void setUp() { @Test void transform_noCustomProperties() { + when(context.transform(isA(DataAddress.class), eq(JsonObject.class))) + .thenReturn(createObjectBuilder().add(EDC_DATA_ADDRESS_TYPE_PROPERTY, value("address-type")).build()); + var dataAddress = DataAddress.Builder.newInstance().type("address-type").build(); var asset = createAssetBuilder() + .dataAddress(dataAddress) .build(); - var jsonObject = transformer.transform(asset, mock(TransformerContext.class)); + var jsonObject = transformer.transform(asset, context); assertThat(jsonObject).isNotNull(); var propsJson = jsonObject.getJsonObject(EDC_ASSET_PROPERTIES); - assertThat(propsJson).hasSize(5); assertThat(jsonObject.getJsonString(ID).getString()).isEqualTo(TEST_ASSET_ID); assertThat(jsonObject.getJsonString(TYPE).getString()).isEqualTo(Asset.EDC_ASSET_TYPE); assertThat(propsJson.getJsonString(EDC_NAMESPACE + "id").getString()).isEqualTo(TEST_ASSET_ID); @@ -65,6 +80,8 @@ void transform_noCustomProperties() { assertThat(propsJson.getJsonString(EDC_NAMESPACE + "description").getString()).isEqualTo(TEST_DESCRIPTION); assertThat(propsJson.getJsonString(EDC_NAMESPACE + "name").getString()).isEqualTo(TEST_ASSET_NAME); assertThat(propsJson.getJsonString(EDC_NAMESPACE + "version").getString()).isEqualTo(TEST_VERSION); + assertThat(jsonObject.getJsonObject(EDC_ASSET_DATA_ADDRESS).getJsonArray(EDC_DATA_ADDRESS_TYPE_PROPERTY).get(0).asJsonObject().getString(VALUE)).isEqualTo("address-type"); + verify(context).transform(dataAddress, JsonObject.class); } @Test @@ -73,7 +90,7 @@ void transform_customProperties_simpleTypes() { .property("some-key", "some-value") .build(); - var jsonObject = transformer.transform(asset, mock(TransformerContext.class)); + var jsonObject = transformer.transform(asset, context); assertThat(jsonObject).isNotNull(); assertThat(jsonObject.getJsonObject(EDC_ASSET_PROPERTIES).getJsonString("some-key").getString()).isEqualTo("some-value"); @@ -85,7 +102,7 @@ void transform_withPrivateProperties_simpleTypes() { .privateProperty("some-key", "some-value") .build(); - var jsonObject = transformer.transform(asset, mock(TransformerContext.class)); + var jsonObject = transformer.transform(asset, context); assertThat(jsonObject).isNotNull(); assertThat(jsonObject.getJsonObject(EDC_ASSET_PRIVATE_PROPERTIES).getJsonString("some-key").getString()).isEqualTo("some-value"); @@ -97,7 +114,7 @@ void transform_customProperties_withExpandedNamespace() { .property("https://foo.bar.org/schema/some-key", "some-value") .build(); - var jsonObject = transformer.transform(asset, mock(TransformerContext.class)); + var jsonObject = transformer.transform(asset, context); assertThat(jsonObject).isNotNull(); assertThat(jsonObject.getJsonObject(EDC_ASSET_PROPERTIES).getJsonString("https://foo.bar.org/schema/some-key").getString()).isEqualTo("some-value"); @@ -109,8 +126,7 @@ void transform_customProperties_withCustomObject() { .property("https://foo.bar.org/schema/payload", new Payload("foo-bar", 42)) .build(); - var mock = mock(TransformerContext.class); - var jsonObject = transformer.transform(asset, mock); + var jsonObject = transformer.transform(asset, context); assertThat(jsonObject).isNotNull(); assertThat(jsonObject.getJsonObject(EDC_ASSET_PROPERTIES).getJsonObject("https://foo.bar.org/schema/payload")).isInstanceOf(JsonObject.class); @@ -124,4 +140,8 @@ private Asset.Builder createAssetBuilder() { .description(TEST_DESCRIPTION) .name(TEST_ASSET_NAME); } -} \ No newline at end of file + + private JsonArrayBuilder value(String value) { + return createArrayBuilder().add(createObjectBuilder().add(VALUE, value)); + } +} diff --git a/extensions/common/json-ld/src/test/java/org/eclipse/edc/jsonld/transformer/to/JsonObjectToAssetTransformerTest.java b/extensions/common/json-ld/src/test/java/org/eclipse/edc/jsonld/transformer/to/JsonObjectToAssetTransformerTest.java index 717ec9c9654..2b36710b32b 100644 --- a/extensions/common/json-ld/src/test/java/org/eclipse/edc/jsonld/transformer/to/JsonObjectToAssetTransformerTest.java +++ b/extensions/common/json-ld/src/test/java/org/eclipse/edc/jsonld/transformer/to/JsonObjectToAssetTransformerTest.java @@ -15,7 +15,6 @@ package org.eclipse.edc.jsonld.transformer.to; import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.json.Json; import jakarta.json.JsonBuilderFactory; import jakarta.json.JsonObject; @@ -24,8 +23,8 @@ import org.eclipse.edc.jsonld.TitaniumJsonLd; import org.eclipse.edc.jsonld.transformer.Payload; import org.eclipse.edc.jsonld.transformer.PayloadTransformer; -import org.eclipse.edc.junit.assertions.AbstractResultAssert; import org.eclipse.edc.spi.monitor.Monitor; +import org.eclipse.edc.spi.types.domain.DataAddress; import org.eclipse.edc.spi.types.domain.asset.Asset; import org.eclipse.edc.transform.spi.TypeTransformerRegistry; import org.junit.jupiter.api.BeforeEach; @@ -40,6 +39,7 @@ import static org.eclipse.edc.jsonld.spi.JsonLdKeywords.VOCAB; import static org.eclipse.edc.jsonld.transformer.to.TestInput.getExpanded; import static org.eclipse.edc.jsonld.util.JacksonJsonLd.createObjectMapper; +import static org.eclipse.edc.junit.assertions.AbstractResultAssert.assertThat; import static org.eclipse.edc.spi.CoreConstants.EDC_NAMESPACE; import static org.eclipse.edc.spi.CoreConstants.EDC_PREFIX; import static org.eclipse.edc.spi.types.domain.asset.Asset.EDC_ASSET_PRIVATE_PROPERTIES; @@ -62,17 +62,16 @@ class JsonObjectToAssetTransformerTest { private static final String CUSTOM_PAYLOAD_NAME = "max"; private final JsonBuilderFactory jsonFactory = Json.createBuilderFactory(Map.of()); private final TitaniumJsonLd jsonLd = new TitaniumJsonLd(mock(Monitor.class)); - private ObjectMapper jsonPmapper; private TypeTransformerRegistry typeTransformerRegistry; @BeforeEach void setUp() throws JsonProcessingException { - - jsonPmapper = createObjectMapper(); + var objectMapper = createObjectMapper(); var transformer = new JsonObjectToAssetTransformer(); typeTransformerRegistry = new TypeTransformerRegistryImpl(); - typeTransformerRegistry.register(new JsonValueToGenericTypeTransformer(jsonPmapper)); + typeTransformerRegistry.register(new JsonValueToGenericTypeTransformer(objectMapper)); typeTransformerRegistry.register(transformer); + typeTransformerRegistry.register(new JsonObjectToDataAddressTransformer()); typeTransformerRegistry.register(new PayloadTransformer()); typeTransformerRegistry.registerTypeAlias(EDC_NAMESPACE + "customPayload", Payload.class); } @@ -84,19 +83,24 @@ void transform_onlyKnownProperties() { .add(TYPE, EDC_ASSET_TYPE) .add(ID, TEST_ASSET_ID) .add("properties", createPropertiesBuilder().build()) + .add("dataAddress", jsonFactory.createObjectBuilder().add("type", "address-type")) .build(); jsonObj = expand(jsonObj); - var asset = typeTransformerRegistry.transform(getExpanded(jsonObj), Asset.class); - AbstractResultAssert.assertThat(asset).withFailMessage(asset::getFailureDetail).isSucceeded(); - assertThat(asset.getContent().getProperties()) - .hasSize(5) - .containsEntry(PROPERTY_ID, TEST_ASSET_ID) - .containsEntry(PROPERTY_ID, asset.getContent().getId()) - .containsEntry(PROPERTY_NAME, TEST_ASSET_NAME) - .containsEntry(PROPERTY_DESCRIPTION, TEST_ASSET_DESCRIPTION) - .containsEntry(PROPERTY_CONTENT_TYPE, TEST_ASSET_CONTENTTYPE) - .containsEntry(PROPERTY_VERSION, TEST_ASSET_VERSION); + var result = typeTransformerRegistry.transform(getExpanded(jsonObj), Asset.class); + + assertThat(result).isSucceeded().satisfies(asset -> { + assertThat(asset.getProperties()) + .hasSize(5) + .containsEntry(PROPERTY_ID, TEST_ASSET_ID) + .containsEntry(PROPERTY_ID, result.getContent().getId()) + .containsEntry(PROPERTY_NAME, TEST_ASSET_NAME) + .containsEntry(PROPERTY_DESCRIPTION, TEST_ASSET_DESCRIPTION) + .containsEntry(PROPERTY_CONTENT_TYPE, TEST_ASSET_CONTENTTYPE) + .containsEntry(PROPERTY_VERSION, TEST_ASSET_VERSION); + assertThat(asset.getDataAddress()).isNotNull().extracting(DataAddress::getType).isEqualTo("address-type"); + }); + } @Test @@ -111,7 +115,7 @@ void transform_withPrivateProperties() { jsonObj = expand(jsonObj); var asset = typeTransformerRegistry.transform(jsonObj, Asset.class); - AbstractResultAssert.assertThat(asset).withFailMessage(asset::getFailureDetail).isSucceeded(); + assertThat(asset).withFailMessage(asset::getFailureDetail).isSucceeded(); assertThat(asset.getContent().getProperties()) .hasSize(5) .containsEntry(PROPERTY_ID, TEST_ASSET_ID) @@ -137,7 +141,7 @@ void transform_withCustomProperty() { .build(); jsonObj = expand(jsonObj); var asset = typeTransformerRegistry.transform(getExpanded(jsonObj), Asset.class); - AbstractResultAssert.assertThat(asset).withFailMessage(asset::getFailureDetail).isSucceeded(); + assertThat(asset).withFailMessage(asset::getFailureDetail).isSucceeded(); assertThat(asset.getContent().getProperties()) .hasSize(6) .hasEntrySatisfying(EDC_NAMESPACE + "payload", o -> assertThat(o).isInstanceOf(Payload.class) @@ -176,7 +180,7 @@ void transform_noEdcContextDecl_shouldUseRawPrefix() { jsonObj = expand(jsonObj); var asset = typeTransformerRegistry.transform(getExpanded(jsonObj), Asset.class); - AbstractResultAssert.assertThat(asset).withFailMessage(asset::getFailureDetail).isSucceeded(); + assertThat(asset).withFailMessage(asset::getFailureDetail).isSucceeded(); assertThat(asset.getContent().getVersion()).isNull(); assertThat(asset.getContent().getProperties()) .containsEntry("edc:version", TEST_ASSET_VERSION); diff --git a/extensions/control-plane/api/management-api/asset-api/src/main/java/org/eclipse/edc/connector/api/management/asset/AssetApiExtension.java b/extensions/control-plane/api/management-api/asset-api/src/main/java/org/eclipse/edc/connector/api/management/asset/AssetApiExtension.java index f2f16c687f2..e0eb57183c5 100644 --- a/extensions/control-plane/api/management-api/asset-api/src/main/java/org/eclipse/edc/connector/api/management/asset/AssetApiExtension.java +++ b/extensions/control-plane/api/management-api/asset-api/src/main/java/org/eclipse/edc/connector/api/management/asset/AssetApiExtension.java @@ -21,7 +21,9 @@ import org.eclipse.edc.connector.api.management.asset.transform.AssetToAssetResponseDtoTransformer; import org.eclipse.edc.connector.api.management.asset.transform.AssetUpdateRequestWrapperDtoToAssetTransformer; import org.eclipse.edc.connector.api.management.asset.transform.JsonObjectToAssetEntryNewDtoTransformer; +import org.eclipse.edc.connector.api.management.asset.v3.AssetApiController; import org.eclipse.edc.connector.api.management.asset.validation.AssetEntryDtoValidator; +import org.eclipse.edc.connector.api.management.asset.validation.AssetValidator; import org.eclipse.edc.connector.api.management.configuration.ManagementApiConfiguration; import org.eclipse.edc.connector.spi.asset.AssetService; import org.eclipse.edc.runtime.metamodel.annotation.Extension; @@ -75,9 +77,10 @@ public void initialize(ServiceExtensionContext context) { transformerRegistry.register(new JsonObjectToAssetEntryNewDtoTransformer()); validator.register(EDC_ASSET_ENTRY_DTO_TYPE, AssetEntryDtoValidator.assetEntryValidator()); - validator.register(EDC_ASSET_TYPE, AssetEntryDtoValidator.assetValidator()); + validator.register(EDC_ASSET_TYPE, AssetValidator.instance()); validator.register(EDC_DATA_ADDRESS_TYPE, DataAddressDtoValidator.instance()); - webService.registerResource(config.getContextAlias(), new AssetApiController(assetService, dataAddressResolver, transformerRegistry, monitor, validator)); + webService.registerResource(config.getContextAlias(), new org.eclipse.edc.connector.api.management.asset.v2.AssetApiController(assetService, dataAddressResolver, transformerRegistry, monitor, validator)); + webService.registerResource(config.getContextAlias(), new AssetApiController(assetService, transformerRegistry, monitor, validator)); } } diff --git a/extensions/control-plane/api/management-api/asset-api/src/main/java/org/eclipse/edc/connector/api/management/asset/AssetApi.java b/extensions/control-plane/api/management-api/asset-api/src/main/java/org/eclipse/edc/connector/api/management/asset/v2/AssetApi.java similarity index 99% rename from extensions/control-plane/api/management-api/asset-api/src/main/java/org/eclipse/edc/connector/api/management/asset/AssetApi.java rename to extensions/control-plane/api/management-api/asset-api/src/main/java/org/eclipse/edc/connector/api/management/asset/v2/AssetApi.java index 04c6fc36e2e..ee95952969f 100644 --- a/extensions/control-plane/api/management-api/asset-api/src/main/java/org/eclipse/edc/connector/api/management/asset/AssetApi.java +++ b/extensions/control-plane/api/management-api/asset-api/src/main/java/org/eclipse/edc/connector/api/management/asset/v2/AssetApi.java @@ -12,7 +12,7 @@ * */ -package org.eclipse.edc.connector.api.management.asset; +package org.eclipse.edc.connector.api.management.asset.v2; import io.swagger.v3.oas.annotations.OpenAPIDefinition; import io.swagger.v3.oas.annotations.Operation; @@ -36,6 +36,7 @@ @OpenAPIDefinition(info = @Info(description = "This contains both the current and the new Asset API, which accepts JSON-LD and will become the standard API once the Dataspace Protocol is stable. " + "The new Asset API is prefixed with /v2, and the old endpoints have been deprecated. At that time of switching, the old API will be removed, and this API will be available without the /v2 prefix.", title = "Asset API")) @Tag(name = "Asset") +@Deprecated public interface AssetApi { @Operation(description = "Creates a new asset together with a data address", diff --git a/extensions/control-plane/api/management-api/asset-api/src/main/java/org/eclipse/edc/connector/api/management/asset/AssetApiController.java b/extensions/control-plane/api/management-api/asset-api/src/main/java/org/eclipse/edc/connector/api/management/asset/v2/AssetApiController.java similarity index 96% rename from extensions/control-plane/api/management-api/asset-api/src/main/java/org/eclipse/edc/connector/api/management/asset/AssetApiController.java rename to extensions/control-plane/api/management-api/asset-api/src/main/java/org/eclipse/edc/connector/api/management/asset/v2/AssetApiController.java index 524dc7a4c49..9a9ef209c8c 100644 --- a/extensions/control-plane/api/management-api/asset-api/src/main/java/org/eclipse/edc/connector/api/management/asset/AssetApiController.java +++ b/extensions/control-plane/api/management-api/asset-api/src/main/java/org/eclipse/edc/connector/api/management/asset/v2/AssetApiController.java @@ -12,7 +12,7 @@ * */ -package org.eclipse.edc.connector.api.management.asset; +package org.eclipse.edc.connector.api.management.asset.v2; import jakarta.json.JsonArray; import jakarta.json.JsonObject; @@ -47,12 +47,12 @@ import static org.eclipse.edc.api.model.QuerySpecDto.EDC_QUERY_SPEC_TYPE; import static org.eclipse.edc.connector.api.management.asset.model.AssetEntryNewDto.EDC_ASSET_ENTRY_DTO_TYPE; import static org.eclipse.edc.spi.types.domain.DataAddress.EDC_DATA_ADDRESS_TYPE; -import static org.eclipse.edc.spi.types.domain.asset.Asset.EDC_ASSET_TYPE; import static org.eclipse.edc.web.spi.exception.ServiceResultHandler.exceptionMapper; @Consumes(APPLICATION_JSON) @Produces(APPLICATION_JSON) @Path("/v2/assets") +@Deprecated public class AssetApiController implements AssetApi { private final TypeTransformerRegistry transformerRegistry; private final AssetService service; @@ -136,7 +136,8 @@ public void removeAsset(@PathParam("id") String id) { @PUT @Override public void updateAsset(JsonObject assetJsonObject) { - validator.validate(EDC_ASSET_TYPE, assetJsonObject).orElseThrow(ValidationFailureException::new); + // validation removed because now the asset validation requires the dataAddress field + // validator.validate(EDC_ASSET_TYPE, assetJsonObject).orElseThrow(ValidationFailureException::new); var assetResult = transformerRegistry.transform(assetJsonObject, Asset.class) .orElseThrow(InvalidRequestException::new); diff --git a/extensions/control-plane/api/management-api/asset-api/src/main/java/org/eclipse/edc/connector/api/management/asset/v3/AssetApi.java b/extensions/control-plane/api/management-api/asset-api/src/main/java/org/eclipse/edc/connector/api/management/asset/v3/AssetApi.java new file mode 100644 index 00000000000..f832122d326 --- /dev/null +++ b/extensions/control-plane/api/management-api/asset-api/src/main/java/org/eclipse/edc/connector/api/management/asset/v3/AssetApi.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * 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 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.edc.connector.api.management.asset.v3; + +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.info.Info; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.parameters.RequestBody; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.json.JsonArray; +import jakarta.json.JsonObject; +import org.eclipse.edc.api.model.IdResponseDto; +import org.eclipse.edc.api.model.QuerySpecDto; +import org.eclipse.edc.connector.api.management.asset.model.AssetResponseDto; +import org.eclipse.edc.connector.api.management.asset.model.AssetUpdateRequestDto; +import org.eclipse.edc.spi.types.domain.asset.Asset; +import org.eclipse.edc.web.spi.ApiErrorDetail; + +@OpenAPIDefinition(info = @Info(description = "This contains both the current and the new Asset API, which accepts JSON-LD and will become the standard API once the Dataspace Protocol is stable. " + + "The new Asset API is prefixed with /v2, and the old endpoints have been deprecated. At that time of switching, the old API will be removed, and this API will be available without the /v2 prefix.", title = "Asset API")) +@Tag(name = "Asset") +public interface AssetApi { + + @Operation(description = "Creates a new asset together with a data address", + requestBody = @RequestBody(content = @Content(schema = @Schema(implementation = Asset.class))), + responses = { + @ApiResponse(responseCode = "200", description = "Asset was created successfully. Returns the asset Id and created timestamp", + content = @Content(schema = @Schema(implementation = IdResponseDto.class))), + @ApiResponse(responseCode = "400", description = "Request body was malformed", + content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)))), + @ApiResponse(responseCode = "409", description = "Could not create asset, because an asset with that ID already exists", + content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)))) } + ) + JsonObject createAsset(JsonObject asset); + + @Operation(description = " all assets according to a particular query", + requestBody = @RequestBody(content = @Content(schema = @Schema(implementation = QuerySpecDto.class))), + responses = { + @ApiResponse(responseCode = "200", description = "The assets matching the query", + content = @Content(array = @ArraySchema(schema = @Schema(implementation = AssetResponseDto.class)))), + @ApiResponse(responseCode = "400", description = "Request body was malformed", + content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)))) + }) + JsonArray requestAssets(JsonObject querySpecDto); + + @Operation(description = "Gets an asset with the given ID", + responses = { + @ApiResponse(responseCode = "200", description = "The asset", + content = @Content(schema = @Schema(implementation = Asset.class))), + @ApiResponse(responseCode = "400", description = "Request was malformed, e.g. id was null", + content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)))), + @ApiResponse(responseCode = "404", description = "An asset with the given ID does not exist", + content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)))) + } + ) + JsonObject getAsset(String id); + + @Operation(description = "Removes an asset with the given ID if possible. Deleting an asset is only possible if that asset is not yet referenced " + + "by a contract agreement, in which case an error is returned. " + + "DANGER ZONE: Note that deleting assets can have unexpected results, especially for contract offers that have been sent out or ongoing or contract negotiations.", + responses = { + @ApiResponse(responseCode = "200", description = "Asset was deleted successfully"), + @ApiResponse(responseCode = "400", description = "Request was malformed, e.g. id was null", + content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)))), + @ApiResponse(responseCode = "404", description = "An asset with the given ID does not exist", + content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)))), + @ApiResponse(responseCode = "409", description = "The asset cannot be deleted, because it is referenced by a contract agreement", + content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)))) + }) + void removeAsset(String id); + + @Operation(description = "Updates an asset with the given ID if it exists. If the asset is not found, no further action is taken. " + + "DANGER ZONE: Note that updating assets can have unexpected results, especially for contract offers that have been sent out or are ongoing in contract negotiations.", + requestBody = @RequestBody(content = @Content(schema = @Schema(implementation = AssetUpdateRequestDto.class))), + responses = { + @ApiResponse(responseCode = "200", description = "Asset was updated successfully"), + @ApiResponse(responseCode = "404", description = "Asset could not be updated, because it does not exist."), + @ApiResponse(responseCode = "400", description = "Request was malformed, e.g. id was null", + content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)))), + }) + void updateAsset(JsonObject asset); + +} diff --git a/extensions/control-plane/api/management-api/asset-api/src/main/java/org/eclipse/edc/connector/api/management/asset/v3/AssetApiController.java b/extensions/control-plane/api/management-api/asset-api/src/main/java/org/eclipse/edc/connector/api/management/asset/v3/AssetApiController.java new file mode 100644 index 00000000000..d678d7efa41 --- /dev/null +++ b/extensions/control-plane/api/management-api/asset-api/src/main/java/org/eclipse/edc/connector/api/management/asset/v3/AssetApiController.java @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * 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 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.edc.connector.api.management.asset.v3; + +import jakarta.json.JsonArray; +import jakarta.json.JsonObject; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import org.eclipse.edc.api.model.IdResponseDto; +import org.eclipse.edc.api.model.QuerySpecDto; +import org.eclipse.edc.connector.spi.asset.AssetService; +import org.eclipse.edc.spi.EdcException; +import org.eclipse.edc.spi.monitor.Monitor; +import org.eclipse.edc.spi.query.QuerySpec; +import org.eclipse.edc.spi.result.Result; +import org.eclipse.edc.spi.types.domain.asset.Asset; +import org.eclipse.edc.transform.spi.TypeTransformerRegistry; +import org.eclipse.edc.validator.spi.JsonObjectValidatorRegistry; +import org.eclipse.edc.web.spi.exception.InvalidRequestException; +import org.eclipse.edc.web.spi.exception.ObjectNotFoundException; +import org.eclipse.edc.web.spi.exception.ValidationFailureException; + +import static jakarta.json.stream.JsonCollectors.toJsonArray; +import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; +import static java.util.Optional.of; +import static org.eclipse.edc.api.model.QuerySpecDto.EDC_QUERY_SPEC_TYPE; +import static org.eclipse.edc.spi.types.domain.asset.Asset.EDC_ASSET_TYPE; +import static org.eclipse.edc.web.spi.exception.ServiceResultHandler.exceptionMapper; + +@Consumes(APPLICATION_JSON) +@Produces(APPLICATION_JSON) +@Path("/v3/assets") +public class AssetApiController implements AssetApi { + private final TypeTransformerRegistry transformerRegistry; + private final AssetService service; + private final Monitor monitor; + private final JsonObjectValidatorRegistry validator; + + public AssetApiController(AssetService service, TypeTransformerRegistry transformerRegistry, + Monitor monitor, JsonObjectValidatorRegistry validator) { + this.transformerRegistry = transformerRegistry; + this.service = service; + this.monitor = monitor; + this.validator = validator; + } + + @POST + @Override + public JsonObject createAsset(JsonObject assetJson) { + validator.validate(EDC_ASSET_TYPE, assetJson).orElseThrow(ValidationFailureException::new); + + var asset = transformerRegistry.transform(assetJson, Asset.class) + .orElseThrow(InvalidRequestException::new); + + var dto = service.create(asset) + .map(a -> IdResponseDto.Builder.newInstance() + .id(a.getId()) + .createdAt(a.getCreatedAt()) + .build()) + .orElseThrow(exceptionMapper(Asset.class, asset.getId())); + + return transformerRegistry.transform(dto, JsonObject.class) + .orElseThrow(f -> new EdcException(f.getFailureDetail())); + } + + @POST + @Path("/request") + @Override + public JsonArray requestAssets(JsonObject querySpecDto) { + QuerySpec querySpec; + if (querySpecDto == null) { + querySpec = QuerySpec.Builder.newInstance().build(); + } else { + validator.validate(EDC_QUERY_SPEC_TYPE, querySpecDto).orElseThrow(ValidationFailureException::new); + + querySpec = transformerRegistry.transform(querySpecDto, QuerySpecDto.class) + .compose(dto -> transformerRegistry.transform(dto, QuerySpec.class)) + .orElseThrow(InvalidRequestException::new); + } + + try (var assets = service.query(querySpec).orElseThrow(exceptionMapper(QuerySpec.class, null))) { + return assets + .map(it -> transformerRegistry.transform(it, JsonObject.class)) + .peek(r -> r.onFailure(f -> monitor.warning(f.getFailureDetail()))) + .filter(Result::succeeded) + .map(Result::getContent) + .collect(toJsonArray()); + } + } + + @GET + @Path("{id}") + @Override + public JsonObject getAsset(@PathParam("id") String id) { + var asset = of(id) + .map(it -> service.findById(id)) + .orElseThrow(() -> new ObjectNotFoundException(Asset.class, id)); + + return transformerRegistry.transform(asset, JsonObject.class) + .orElseThrow(f -> new EdcException(f.getFailureDetail())); + + } + + @DELETE + @Path("{id}") + @Override + public void removeAsset(@PathParam("id") String id) { + service.delete(id).orElseThrow(exceptionMapper(Asset.class, id)); + } + + @PUT + @Override + public void updateAsset(JsonObject assetJson) { + validator.validate(EDC_ASSET_TYPE, assetJson).orElseThrow(ValidationFailureException::new); + + var assetResult = transformerRegistry.transform(assetJson, Asset.class) + .orElseThrow(InvalidRequestException::new); + + service.update(assetResult) + .orElseThrow(exceptionMapper(Asset.class, assetResult.getId())); + } + +} diff --git a/extensions/control-plane/api/management-api/asset-api/src/main/java/org/eclipse/edc/connector/api/management/asset/validation/AssetEntryDtoValidator.java b/extensions/control-plane/api/management-api/asset-api/src/main/java/org/eclipse/edc/connector/api/management/asset/validation/AssetEntryDtoValidator.java index b07554e76c2..dacdef6d8b5 100644 --- a/extensions/control-plane/api/management-api/asset-api/src/main/java/org/eclipse/edc/connector/api/management/asset/validation/AssetEntryDtoValidator.java +++ b/extensions/control-plane/api/management-api/asset-api/src/main/java/org/eclipse/edc/connector/api/management/asset/validation/AssetEntryDtoValidator.java @@ -30,7 +30,10 @@ /** * Contains the AssetEntryDto validator definition + * + * @deprecated this will supersede by {@link AssetValidator} */ +@Deprecated(since = "0.1.2", forRemoval = true) public class AssetEntryDtoValidator { public static Validator assetEntryValidator() { diff --git a/extensions/control-plane/api/management-api/asset-api/src/main/java/org/eclipse/edc/connector/api/management/asset/validation/AssetValidator.java b/extensions/control-plane/api/management-api/asset-api/src/main/java/org/eclipse/edc/connector/api/management/asset/validation/AssetValidator.java new file mode 100644 index 00000000000..dd6a717137b --- /dev/null +++ b/extensions/control-plane/api/management-api/asset-api/src/main/java/org/eclipse/edc/connector/api/management/asset/validation/AssetValidator.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * 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 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.edc.connector.api.management.asset.validation; + +import jakarta.json.JsonObject; +import org.eclipse.edc.api.validation.DataAddressDtoValidator; +import org.eclipse.edc.validator.jsonobject.JsonObjectValidator; +import org.eclipse.edc.validator.jsonobject.validators.MandatoryObject; +import org.eclipse.edc.validator.jsonobject.validators.OptionalIdNotBlank; +import org.eclipse.edc.validator.spi.ValidationResult; +import org.eclipse.edc.validator.spi.Validator; + +import static org.eclipse.edc.spi.types.domain.asset.Asset.EDC_ASSET_DATA_ADDRESS; +import static org.eclipse.edc.spi.types.domain.asset.Asset.EDC_ASSET_PRIVATE_PROPERTIES; +import static org.eclipse.edc.spi.types.domain.asset.Asset.EDC_ASSET_PROPERTIES; +import static org.eclipse.edc.validator.spi.Violation.violation; + +/** + * Contains the AssetEntryDto validator definition + */ +public class AssetValidator { + + public static Validator instance() { + return JsonObjectValidator.newValidator() + .verifyId(OptionalIdNotBlank::new) + .verify(EDC_ASSET_PROPERTIES, MandatoryObject::new) + .verify(EDC_ASSET_DATA_ADDRESS, MandatoryObject::new) + .verify(path -> new AssetPropertiesUniqueness()) + .verifyObject(EDC_ASSET_DATA_ADDRESS, DataAddressDtoValidator::instance) + .build(); + } + + private static class AssetPropertiesUniqueness implements Validator { + @Override + public ValidationResult validate(JsonObject input) { + if (!input.containsKey(EDC_ASSET_PROPERTIES) || !input.containsKey(EDC_ASSET_PRIVATE_PROPERTIES)) { + return ValidationResult.success(); + } + var properties = input.getJsonArray(EDC_ASSET_PROPERTIES).getJsonObject(0); + var privateProperties = input.getJsonArray(EDC_ASSET_PRIVATE_PROPERTIES).getJsonObject(0); + + if (properties.keySet().stream().anyMatch(privateProperties::containsKey)) { + return ValidationResult.failure(violation("cannot exists duplicated keys between 'properties' and 'privateProperties'", EDC_ASSET_PROPERTIES)); + } + return ValidationResult.success(); + } + } + +} diff --git a/extensions/control-plane/api/management-api/asset-api/src/test/java/org/eclipse/edc/connector/api/management/asset/AssetApiExtensionTest.java b/extensions/control-plane/api/management-api/asset-api/src/test/java/org/eclipse/edc/connector/api/management/asset/AssetApiExtensionTest.java index c8d883ef359..3c2251abfd8 100644 --- a/extensions/control-plane/api/management-api/asset-api/src/test/java/org/eclipse/edc/connector/api/management/asset/AssetApiExtensionTest.java +++ b/extensions/control-plane/api/management-api/asset-api/src/test/java/org/eclipse/edc/connector/api/management/asset/AssetApiExtensionTest.java @@ -14,9 +14,11 @@ package org.eclipse.edc.connector.api.management.asset; +import org.eclipse.edc.connector.api.management.asset.v3.AssetApiController; import org.eclipse.edc.junit.extensions.DependencyInjectionExtension; import org.eclipse.edc.spi.system.ServiceExtensionContext; import org.eclipse.edc.validator.spi.JsonObjectValidatorRegistry; +import org.eclipse.edc.web.spi.WebService; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -26,6 +28,7 @@ import static org.eclipse.edc.spi.types.domain.asset.Asset.EDC_ASSET_TYPE; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isA; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -33,10 +36,20 @@ class AssetApiExtensionTest { private final JsonObjectValidatorRegistry validatorRegistry = mock(); + private final WebService webService = mock(); @BeforeEach void setUp(ServiceExtensionContext context) { context.registerService(JsonObjectValidatorRegistry.class, validatorRegistry); + context.registerService(WebService.class, webService); + } + + @Test + void initialize_shouldRegisterControllers(AssetApiExtension extension, ServiceExtensionContext context) { + extension.initialize(context); + + verify(webService).registerResource(any(), isA(org.eclipse.edc.connector.api.management.asset.v2.AssetApiController.class)); + verify(webService).registerResource(any(), isA(AssetApiController.class)); } @Test diff --git a/extensions/control-plane/api/management-api/asset-api/src/test/java/org/eclipse/edc/connector/api/management/asset/AssetApiControllerTest.java b/extensions/control-plane/api/management-api/asset-api/src/test/java/org/eclipse/edc/connector/api/management/asset/v2/AssetApiControllerTest.java similarity index 99% rename from extensions/control-plane/api/management-api/asset-api/src/test/java/org/eclipse/edc/connector/api/management/asset/AssetApiControllerTest.java rename to extensions/control-plane/api/management-api/asset-api/src/test/java/org/eclipse/edc/connector/api/management/asset/v2/AssetApiControllerTest.java index fc1c1110c85..6e268437ab3 100644 --- a/extensions/control-plane/api/management-api/asset-api/src/test/java/org/eclipse/edc/connector/api/management/asset/AssetApiControllerTest.java +++ b/extensions/control-plane/api/management-api/asset-api/src/test/java/org/eclipse/edc/connector/api/management/asset/v2/AssetApiControllerTest.java @@ -12,7 +12,7 @@ * */ -package org.eclipse.edc.connector.api.management.asset; +package org.eclipse.edc.connector.api.management.asset.v2; import io.restassured.specification.RequestSpecification; import jakarta.json.JsonObject; @@ -34,6 +34,7 @@ import org.eclipse.edc.validator.spi.ValidationResult; import org.eclipse.edc.web.jersey.testfixtures.RestControllerTestBase; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import java.util.Map; @@ -70,6 +71,7 @@ import static org.mockito.Mockito.when; @ApiTest +@Deprecated class AssetApiControllerTest extends RestControllerTestBase { private static final String TEST_ASSET_ID = "test-asset-id"; @@ -437,6 +439,7 @@ void updateAsset_shouldReturnBadRequest_whenTransformFails() { } @Test + @Disabled void updateAsset_shouldReturnBadRequest_whenValidationFails() { when(transformerRegistry.transform(isA(JsonObject.class), eq(Asset.class))).thenReturn(Result.failure("error")); when(validator.validate(any(), any())).thenReturn(ValidationResult.failure(violation("validation failure", "path"))); diff --git a/extensions/control-plane/api/management-api/asset-api/src/test/java/org/eclipse/edc/connector/api/management/asset/v3/AssetApiControllerTest.java b/extensions/control-plane/api/management-api/asset-api/src/test/java/org/eclipse/edc/connector/api/management/asset/v3/AssetApiControllerTest.java new file mode 100644 index 00000000000..1e9738e48b9 --- /dev/null +++ b/extensions/control-plane/api/management-api/asset-api/src/test/java/org/eclipse/edc/connector/api/management/asset/v3/AssetApiControllerTest.java @@ -0,0 +1,455 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * 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 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.edc.connector.api.management.asset.v3; + +import io.restassured.specification.RequestSpecification; +import jakarta.json.JsonObject; +import jakarta.json.JsonObjectBuilder; +import org.eclipse.edc.api.model.IdResponseDto; +import org.eclipse.edc.api.model.QuerySpecDto; +import org.eclipse.edc.connector.spi.asset.AssetService; +import org.eclipse.edc.junit.annotations.ApiTest; +import org.eclipse.edc.service.spi.result.ServiceResult; +import org.eclipse.edc.spi.asset.DataAddressResolver; +import org.eclipse.edc.spi.query.QuerySpec; +import org.eclipse.edc.spi.result.Result; +import org.eclipse.edc.spi.types.domain.DataAddress; +import org.eclipse.edc.spi.types.domain.asset.Asset; +import org.eclipse.edc.transform.spi.TypeTransformerRegistry; +import org.eclipse.edc.validator.spi.JsonObjectValidatorRegistry; +import org.eclipse.edc.validator.spi.ValidationResult; +import org.eclipse.edc.web.jersey.testfixtures.RestControllerTestBase; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Map; +import java.util.stream.Stream; + +import static io.restassured.RestAssured.given; +import static io.restassured.http.ContentType.JSON; +import static jakarta.json.Json.createObjectBuilder; +import static org.eclipse.edc.api.model.IdResponseDto.EDC_ID_RESPONSE_DTO_CREATED_AT; +import static org.eclipse.edc.api.model.IdResponseDto.EDC_ID_RESPONSE_DTO_TYPE; +import static org.eclipse.edc.jsonld.spi.JsonLdKeywords.CONTEXT; +import static org.eclipse.edc.jsonld.spi.JsonLdKeywords.ID; +import static org.eclipse.edc.jsonld.spi.JsonLdKeywords.TYPE; +import static org.eclipse.edc.jsonld.spi.JsonLdKeywords.VOCAB; +import static org.eclipse.edc.spi.CoreConstants.EDC_NAMESPACE; +import static org.eclipse.edc.spi.CoreConstants.EDC_PREFIX; +import static org.eclipse.edc.spi.types.domain.asset.Asset.EDC_ASSET_TYPE; +import static org.eclipse.edc.validator.spi.Violation.violation; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.is; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isA; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +@ApiTest +class AssetApiControllerTest extends RestControllerTestBase { + + private static final String TEST_ASSET_ID = "test-asset-id"; + private static final String TEST_ASSET_CONTENTTYPE = "application/json"; + private static final String TEST_ASSET_DESCRIPTION = "test description"; + private static final String TEST_ASSET_VERSION = "0.4.2"; + private static final String TEST_ASSET_NAME = "test-asset"; + private final AssetService service = mock(AssetService.class); + private final DataAddressResolver dataAddressResolver = mock(DataAddressResolver.class); + private final TypeTransformerRegistry transformerRegistry = mock(TypeTransformerRegistry.class); + private final JsonObjectValidatorRegistry validator = mock(JsonObjectValidatorRegistry.class); + + @BeforeEach + void setup() { + when(transformerRegistry.transform(isA(JsonObject.class), eq(DataAddress.class))).thenReturn(Result.success(DataAddress.Builder.newInstance().type("test-type").build())); + when(transformerRegistry.transform(isA(IdResponseDto.class), eq(JsonObject.class))).thenAnswer(a -> { + var dto = (IdResponseDto) a.getArgument(0); + return Result.success(createObjectBuilder() + .add(TYPE, EDC_ID_RESPONSE_DTO_TYPE) + .add(ID, dto.getId()) + .add(EDC_ID_RESPONSE_DTO_CREATED_AT, dto.getCreatedAt()) + .build() + ); + }); + } + + @Test + void requestAsset() { + when(service.query(any())) + .thenReturn(ServiceResult.success(Stream.of(Asset.Builder.newInstance().build()))); + when(transformerRegistry.transform(isA(Asset.class), eq(JsonObject.class))) + .thenReturn(Result.success(createAssetJson().build())); + when(transformerRegistry.transform(isA(JsonObject.class), eq(QuerySpecDto.class))) + .thenReturn(Result.success(QuerySpecDto.Builder.newInstance().offset(10).build())); + when(transformerRegistry.transform(isA(QuerySpecDto.class), eq(QuerySpec.class))) + .thenReturn(Result.success(QuerySpec.Builder.newInstance().offset(10).build())); + when(validator.validate(any(), any())).thenReturn(ValidationResult.success()); + + baseRequest() + .contentType(JSON) + .body("{}") + .post("/assets/request") + .then() + .log().ifError() + .statusCode(200) + .contentType(JSON) + .body("size()", is(1)); + verify(service).query(argThat(s -> s.getOffset() == 10)); + verify(transformerRegistry).transform(isA(Asset.class), eq(JsonObject.class)); + verify(transformerRegistry).transform(isA(QuerySpecDto.class), eq(QuerySpec.class)); + } + + @Test + void requestAsset_filtersOutFailedTransforms() { + when(service.query(any())) + .thenReturn(ServiceResult.success(Stream.of(Asset.Builder.newInstance().build()))); + when(transformerRegistry.transform(isA(QuerySpecDto.class), eq(QuerySpec.class))) + .thenReturn(Result.success(QuerySpec.Builder.newInstance().offset(10).build())); + when(transformerRegistry.transform(isA(Asset.class), eq(JsonObject.class))) + .thenReturn(Result.failure("failed to transform")); + when(validator.validate(any(), any())).thenReturn(ValidationResult.success()); + + baseRequest() + .contentType(JSON) + .post("/assets/request") + .then() + .statusCode(200) + .contentType(JSON) + .body("size()", is(0)); + } + + @Test + void requestAsset_shouldReturnBadRequest_whenQueryIsInvalid() { + when(transformerRegistry.transform(any(JsonObject.class), eq(QuerySpecDto.class))).thenReturn(Result.success(QuerySpecDto.Builder.newInstance().build())); + when(transformerRegistry.transform(any(QuerySpecDto.class), eq(QuerySpec.class))).thenReturn(Result.success(QuerySpec.Builder.newInstance().build())); + when(service.query(any())).thenReturn(ServiceResult.badRequest("test-message")); + when(validator.validate(any(), any())).thenReturn(ValidationResult.success()); + + baseRequest() + .body(Map.of("offset", -1)) + .contentType(JSON) + .post("/assets/request") + .then() + .statusCode(400); + } + + @Test + void requestAsset_shouldReturnBadRequest_whenQueryTransformFails() { + when(transformerRegistry.transform(isA(JsonObject.class), eq(QuerySpecDto.class))) + .thenReturn(Result.success(QuerySpecDto.Builder.newInstance().build())); + when(transformerRegistry.transform(isA(QuerySpecDto.class), eq(QuerySpec.class))) + .thenReturn(Result.failure("error")); + when(service.query(any())).thenReturn(ServiceResult.success()); + when(validator.validate(any(), any())).thenReturn(ValidationResult.success()); + + baseRequest() + .contentType(JSON) + .body("{}") + .post("/assets/request") + .then() + .statusCode(400); + } + + @Test + void requestAsset_shouldReturnBadRequest_whenServiceReturnsBadRequest() { + when(transformerRegistry.transform(isA(QuerySpecDto.class), eq(QuerySpec.class))) + .thenReturn(Result.success(QuerySpec.Builder.newInstance().build())); + when(service.query(any())).thenReturn(ServiceResult.badRequest()); + when(validator.validate(any(), any())).thenReturn(ValidationResult.success()); + + baseRequest() + .contentType(JSON) + .post("/assets/request") + .then() + .statusCode(400); + } + + @Test + void requestAsset_shouldReturnBadRequest_whenValidationFails() { + when(transformerRegistry.transform(isA(QuerySpecDto.class), eq(QuerySpec.class))) + .thenReturn(Result.success(QuerySpec.Builder.newInstance().build())); + when(validator.validate(any(), any())).thenReturn(ValidationResult.failure(violation("validation failure", "a path"))); + + baseRequest() + .contentType(JSON) + .body("{}") + .post("/assets/request") + .then() + .statusCode(400); + verify(validator).validate(eq(QuerySpecDto.EDC_QUERY_SPEC_TYPE), isA(JsonObject.class)); + verifyNoInteractions(service); + } + + @Test + void getSingleAsset() { + var asset = Asset.Builder.newInstance().property("key", "value").build(); + when(service.findById("id")).thenReturn(asset); + var assetJson = createAssetJson().build(); + when(transformerRegistry.transform(isA(Asset.class), eq(JsonObject.class))).thenReturn(Result.success(assetJson)); + + baseRequest() + .get("/assets/id") + .then() + .statusCode(200) + .contentType(JSON) + .body(ID, equalTo(TEST_ASSET_ID)); + + verify(transformerRegistry).transform(isA(Asset.class), eq(JsonObject.class)); + verifyNoMoreInteractions(transformerRegistry); + } + + @Test + void getSingleAsset_notFound() { + when(service.findById(any())).thenReturn(null); + + baseRequest() + .get("/assets/not-existent-id") + .then() + .statusCode(404); + } + + @Test + void getAssetById_shouldReturnNotFound_whenTransformFails() { + when(service.findById("id")).thenReturn(Asset.Builder.newInstance().build()); + when(transformerRegistry.transform(isA(Asset.class), eq(JsonObject.class))).thenReturn(Result.failure("failure")); + + baseRequest() + .get("/assets/id") + .then() + .statusCode(500); + } + + @Test + void createAsset() { + var asset = createAssetBuilder().dataAddress(DataAddress.Builder.newInstance().type("any").build()).build(); + when(transformerRegistry.transform(any(JsonObject.class), eq(Asset.class))).thenReturn(Result.success(asset)); + when(service.create(isA(Asset.class))).thenReturn(ServiceResult.success(asset)); + when(validator.validate(any(), any())).thenReturn(ValidationResult.success()); + + baseRequest() + .contentType(JSON) + .body(createAssetJson().build()) + .post("/assets") + .then() + .statusCode(200) + .contentType(JSON) + .body(ID, is(TEST_ASSET_ID)) + .body("'" + EDC_NAMESPACE + "createdAt'", greaterThan(0L)); + + verify(transformerRegistry).transform(any(), eq(Asset.class)); + verify(transformerRegistry).transform(isA(IdResponseDto.class), eq(JsonObject.class)); + verify(service).create(isA(Asset.class)); + verifyNoMoreInteractions(service, transformerRegistry); + } + + @Test + void createAsset_shouldReturnBadRequest_whenValidationFails() { + when(validator.validate(any(), any())).thenReturn(ValidationResult.failure(violation("a failure", "a path"))); + + baseRequest() + .contentType(JSON) + .body(createAssetJson().build()) + .post("/assets") + .then() + .statusCode(400); + + verify(validator).validate(eq(EDC_ASSET_TYPE), isA(JsonObject.class)); + verifyNoInteractions(service, transformerRegistry); + } + + @Test + void createAsset_shouldReturnBadRequest_whenTransformFails() { + when(transformerRegistry.transform(isA(JsonObject.class), eq(Asset.class))).thenReturn(Result.failure("failed")); + when(validator.validate(any(), any())).thenReturn(ValidationResult.success()); + + baseRequest() + .body(createAssetJson().build()) + .contentType(JSON) + .post("/assets") + .then() + .statusCode(400); + verifyNoInteractions(service); + } + + @Test + void createAsset_alreadyExists() { + var asset = createAssetBuilder().dataAddress(DataAddress.Builder.newInstance().type("any").build()).build(); + when(transformerRegistry.transform(any(JsonObject.class), eq(Asset.class))).thenReturn(Result.success(asset)); + when(service.create(isA(Asset.class))).thenReturn(ServiceResult.conflict("already exists")); + when(validator.validate(any(), any())).thenReturn(ValidationResult.success()); + + baseRequest() + .body(createAssetJson().build()) + .contentType(JSON) + .post("/assets") + .then() + .statusCode(409); + } + + @Test + void createAsset_emptyAttributes() { + when(transformerRegistry.transform(isA(JsonObject.class), any())).thenReturn(Result.failure("Cannot be transformed")); + when(validator.validate(any(), any())).thenReturn(ValidationResult.success()); + + baseRequest() + .body(createAssetJson().build()) + .contentType(JSON) + .post("/assets") + .then() + .statusCode(400); + } + + @Test + void deleteAsset() { + when(service.delete("assetId")) + .thenReturn(ServiceResult.success(createAssetBuilder().build())); + + baseRequest() + .contentType(JSON) + .delete("/assets/assetId") + .then() + .statusCode(204); + verify(service).delete("assetId"); + } + + @Test + void deleteAsset_notExists() { + when(service.delete(any())).thenReturn(ServiceResult.notFound("not found")); + + baseRequest() + .contentType(JSON) + .delete("/assets/not-existent-id") + .then() + .statusCode(404); + } + + @Test + void deleteAsset_conflicts() { + when(service.delete(any())).thenReturn(ServiceResult.conflict("conflict")); + + baseRequest() + .contentType(JSON) + .delete("/assets/id") + .then() + .statusCode(409); + } + + @Test + void updateAsset_whenExists() { + var asset = Asset.Builder.newInstance().property("key1", "value1").build(); + when(transformerRegistry.transform(isA(JsonObject.class), eq(Asset.class))).thenReturn(Result.success(asset)); + when(service.update(any(Asset.class))).thenReturn(ServiceResult.success()); + when(validator.validate(any(), any())).thenReturn(ValidationResult.success()); + + baseRequest() + .body(createAssetJson().build()) + .contentType(JSON) + .put("/assets") + .then() + .statusCode(204); + verify(service).update(eq(asset)); + } + + @Test + void updateAsset_shouldReturnNotFound_whenItDoesNotExists() { + var asset = Asset.Builder.newInstance().property("key1", "value1").build(); + when(transformerRegistry.transform(isA(JsonObject.class), eq(Asset.class))).thenReturn(Result.success(asset)); + when(service.update(any(Asset.class))).thenReturn(ServiceResult.notFound("not found")); + when(validator.validate(any(), any())).thenReturn(ValidationResult.success()); + + baseRequest() + .body(createAssetJson().build()) + .contentType(JSON) + .put("/assets") + .then() + .statusCode(404); + } + + @Test + void updateAsset_shouldReturnBadRequest_whenTransformFails() { + when(transformerRegistry.transform(isA(JsonObject.class), eq(Asset.class))).thenReturn(Result.failure("error")); + when(validator.validate(any(), any())).thenReturn(ValidationResult.success()); + + baseRequest() + .body(createAssetJson().build()) + .contentType(JSON) + .put("/assets") + .then() + .statusCode(400); + verifyNoInteractions(service); + } + + @Test + void updateAsset_shouldReturnBadRequest_whenValidationFails() { + when(transformerRegistry.transform(isA(JsonObject.class), eq(Asset.class))).thenReturn(Result.failure("error")); + when(validator.validate(any(), any())).thenReturn(ValidationResult.failure(violation("validation failure", "path"))); + + baseRequest() + .body(createAssetJson().build()) + .contentType(JSON) + .put("/assets") + .then() + .statusCode(400); + verify(validator).validate(eq(EDC_ASSET_TYPE), isA(JsonObject.class)); + verifyNoInteractions(service, transformerRegistry); + } + + @Override + protected Object controller() { + return new AssetApiController(service, transformerRegistry, monitor, validator); + } + + private JsonObjectBuilder createAssetJson() { + return createObjectBuilder() + .add(CONTEXT, createContextBuilder().build()) + .add(TYPE, EDC_ASSET_TYPE) + .add(ID, TEST_ASSET_ID) + .add("properties", createPropertiesBuilder().build()); + } + + private JsonObjectBuilder createPropertiesBuilder() { + return createObjectBuilder() + .add("name", TEST_ASSET_NAME) + .add("description", TEST_ASSET_DESCRIPTION) + .add("edc:version", TEST_ASSET_VERSION) + .add("contenttype", TEST_ASSET_CONTENTTYPE); + } + + private JsonObjectBuilder createContextBuilder() { + return createObjectBuilder() + .add(VOCAB, EDC_NAMESPACE) + .add(EDC_PREFIX, EDC_NAMESPACE); + } + + private RequestSpecification baseRequest() { + return given() + .baseUri("http://localhost:" + port + "/v3") + .when(); + } + + private Asset.Builder createAssetBuilder() { + return Asset.Builder.newInstance() + .name(TEST_ASSET_NAME) + .id(TEST_ASSET_ID) + .contentType(TEST_ASSET_CONTENTTYPE) + .description(TEST_ASSET_DESCRIPTION) + .version(TEST_ASSET_VERSION); + } +} diff --git a/extensions/control-plane/api/management-api/asset-api/src/test/java/org/eclipse/edc/connector/api/management/asset/validation/AssetValidatorTest.java b/extensions/control-plane/api/management-api/asset-api/src/test/java/org/eclipse/edc/connector/api/management/asset/validation/AssetValidatorTest.java new file mode 100644 index 00000000000..c6ee14bfff8 --- /dev/null +++ b/extensions/control-plane/api/management-api/asset-api/src/test/java/org/eclipse/edc/connector/api/management/asset/validation/AssetValidatorTest.java @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * 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 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.edc.connector.api.management.asset.validation; + +import jakarta.json.JsonArrayBuilder; +import jakarta.json.JsonObject; +import org.eclipse.edc.validator.spi.Validator; +import org.junit.jupiter.api.Test; + +import static jakarta.json.Json.createArrayBuilder; +import static jakarta.json.Json.createObjectBuilder; +import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.edc.jsonld.spi.JsonLdKeywords.ID; +import static org.eclipse.edc.jsonld.spi.JsonLdKeywords.VALUE; +import static org.eclipse.edc.junit.assertions.AbstractResultAssert.assertThat; +import static org.eclipse.edc.spi.types.domain.DataAddress.EDC_DATA_ADDRESS_TYPE_PROPERTY; +import static org.eclipse.edc.spi.types.domain.asset.Asset.EDC_ASSET_DATA_ADDRESS; +import static org.eclipse.edc.spi.types.domain.asset.Asset.EDC_ASSET_PRIVATE_PROPERTIES; +import static org.eclipse.edc.spi.types.domain.asset.Asset.EDC_ASSET_PROPERTIES; + +class AssetValidatorTest { + + private final Validator validator = AssetValidator.instance(); + + @Test + void shouldSucceed_whenValidInput() { + var input = createObjectBuilder() + .add(EDC_ASSET_PROPERTIES, createArrayBuilder().add(createObjectBuilder())) + .add(EDC_ASSET_DATA_ADDRESS, validDataAddress()) + .build(); + + var result = validator.validate(input); + + assertThat(result).isSucceeded(); + } + + @Test + void shouldFail_whenIdIsBlank() { + var input = createObjectBuilder() + .add(ID, " ") + .add(EDC_ASSET_PROPERTIES, createArrayBuilder().add(createObjectBuilder())) + .add(EDC_ASSET_DATA_ADDRESS, validDataAddress()) + .build(); + + var result = validator.validate(input); + + assertThat(result).isFailed().satisfies(failure -> { + assertThat(failure.getViolations()).hasSize(1).anySatisfy(v -> { + assertThat(v.path()).isEqualTo(ID); + }); + }); + } + + @Test + void shouldFail_whenPropertiesAreMissing() { + var input = createObjectBuilder() + .add(EDC_ASSET_DATA_ADDRESS, validDataAddress()) + .build(); + + var result = validator.validate(input); + + assertThat(result).isFailed().satisfies(failure -> { + assertThat(failure.getViolations()).hasSize(1).anySatisfy(v -> + assertThat(v.path()).isEqualTo(EDC_ASSET_PROPERTIES)); + }); + } + + @Test + void shouldFail_whenPropertiesAndPrivatePropertiesHaveDuplicatedKeys() { + var input = createObjectBuilder() + .add(EDC_ASSET_PROPERTIES, createArrayBuilder().add(createObjectBuilder().add("key", createArrayBuilder()))) + .add(EDC_ASSET_PRIVATE_PROPERTIES, createArrayBuilder().add(createObjectBuilder().add("key", createArrayBuilder()))) + .add(EDC_ASSET_DATA_ADDRESS, validDataAddress()) + .build(); + + var result = validator.validate(input); + + assertThat(result).isFailed().satisfies(failure -> { + assertThat(failure.getViolations()).hasSize(1).anySatisfy(v -> + assertThat(v.path()).isEqualTo(EDC_ASSET_PROPERTIES)); + }); + } + + @Test + void shouldFail_whenDataAddressHasNoType() { + var input = createObjectBuilder() + .add(EDC_ASSET_PROPERTIES, createArrayBuilder().add(createObjectBuilder())) + .add(EDC_ASSET_DATA_ADDRESS, createArrayBuilder().add(createObjectBuilder())) + .build(); + + var result = validator.validate(input); + + assertThat(result).isFailed().satisfies(failure -> { + assertThat(failure.getViolations()).hasSize(1).anySatisfy(v -> + assertThat(v.path()).isEqualTo(EDC_ASSET_DATA_ADDRESS + "/" + EDC_DATA_ADDRESS_TYPE_PROPERTY)); + }); + } + + private JsonArrayBuilder validDataAddress() { + return createArrayBuilder().add(createObjectBuilder() + .add(EDC_DATA_ADDRESS_TYPE_PROPERTY, createArrayBuilder().add(createObjectBuilder().add(VALUE, "AddressType"))) + ); + } + + private JsonArrayBuilder validAsset() { + return createArrayBuilder() + .add(createObjectBuilder() + .add(EDC_ASSET_PROPERTIES, createArrayBuilder().add(createObjectBuilder())) + ); + } +} diff --git a/extensions/control-plane/provision/provision-http/src/test/java/org/eclipse/edc/connector/provision/http/impl/HttpProvisionerExtensionEndToEndTest.java b/extensions/control-plane/provision/provision-http/src/test/java/org/eclipse/edc/connector/provision/http/impl/HttpProvisionerExtensionEndToEndTest.java index 4b0ad72ed6e..ffb20797a2d 100644 --- a/extensions/control-plane/provision/provision-http/src/test/java/org/eclipse/edc/connector/provision/http/impl/HttpProvisionerExtensionEndToEndTest.java +++ b/extensions/control-plane/provision/provision-http/src/test/java/org/eclipse/edc/connector/provision/http/impl/HttpProvisionerExtensionEndToEndTest.java @@ -43,7 +43,6 @@ import org.eclipse.edc.spi.system.ServiceExtensionContext; import org.eclipse.edc.spi.types.domain.DataAddress; import org.eclipse.edc.spi.types.domain.asset.Asset; -import org.eclipse.edc.spi.types.domain.asset.AssetEntry; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -156,10 +155,11 @@ private ContractNegotiation createContractNegotiation() { } @NotNull - private AssetEntry createAssetEntry() { - var asset = Asset.Builder.newInstance().id(ASSET_ID).build(); - var dataAddress = DataAddress.Builder.newInstance().type(TEST_DATA_TYPE).build(); - return new AssetEntry(asset, dataAddress); + private Asset createAssetEntry() { + return Asset.Builder.newInstance() + .id(ASSET_ID) + .dataAddress(DataAddress.Builder.newInstance().type(TEST_DATA_TYPE).build()) + .build(); } private TransferRequestMessage createTransferRequestMessage() { diff --git a/extensions/control-plane/store/sql/asset-index-sql/src/main/java/org/eclipse/edc/connector/store/sql/assetindex/SqlAssetIndex.java b/extensions/control-plane/store/sql/asset-index-sql/src/main/java/org/eclipse/edc/connector/store/sql/assetindex/SqlAssetIndex.java index 60f87dead97..90b21273234 100644 --- a/extensions/control-plane/store/sql/asset-index-sql/src/main/java/org/eclipse/edc/connector/store/sql/assetindex/SqlAssetIndex.java +++ b/extensions/control-plane/store/sql/asset-index-sql/src/main/java/org/eclipse/edc/connector/store/sql/assetindex/SqlAssetIndex.java @@ -26,7 +26,6 @@ import org.eclipse.edc.spi.result.StoreResult; import org.eclipse.edc.spi.types.domain.DataAddress; import org.eclipse.edc.spi.types.domain.asset.Asset; -import org.eclipse.edc.spi.types.domain.asset.AssetEntry; import org.eclipse.edc.sql.QueryExecutor; import org.eclipse.edc.sql.store.AbstractSqlStore; import org.eclipse.edc.transaction.datasource.spi.DataSourceRegistry; @@ -38,7 +37,6 @@ import java.sql.SQLException; import java.util.AbstractMap; import java.util.List; -import java.util.Map; import java.util.Objects; import java.util.stream.Stream; @@ -90,14 +88,16 @@ public Stream queryAssets(QuerySpec querySpec) { var allPropertiesStream = queryExecutor.query(connection, false, this::mapPropertyResultSet, findPropertyByIdSql, assetId) ) { var createdAt = createdAtStream.findFirst().orElse(0L); - Map> groupedProperties = allPropertiesStream.collect(partitioningBy(SqlPropertyWrapper::isPrivate)); + var groupedProperties = allPropertiesStream.collect(partitioningBy(SqlPropertyWrapper::isPrivate)); var assetProperties = groupedProperties.get(false).stream().collect(toMap(SqlPropertyWrapper::getPropertyKey, SqlPropertyWrapper::getPropertyValue)); var assetPrivateProperties = groupedProperties.get(true).stream().collect(toMap(SqlPropertyWrapper::getPropertyKey, SqlPropertyWrapper::getPropertyValue)); + var dataAddress = resolveForAsset(assetId); return Asset.Builder.newInstance() .id(assetId) .properties(assetProperties) .privateProperties(assetPrivateProperties) .createdAt(createdAt) + .dataAddress(dataAddress) .build(); } }); @@ -112,12 +112,10 @@ public Stream queryAssets(QuerySpec querySpec) { } @Override - public StoreResult create(AssetEntry item) { - Objects.requireNonNull(item); - var asset = item.getAsset(); - var dataAddress = item.getDataAddress(); - + public StoreResult create(Asset asset) { Objects.requireNonNull(asset); + var dataAddress = asset.getDataAddress(); + Objects.requireNonNull(dataAddress); var assetId = asset.getId(); @@ -128,7 +126,7 @@ public StoreResult create(AssetEntry item) { return StoreResult.alreadyExists(msg); } - if (checkDuplicatePropertyKeys(asset)) { + if (asset.hasDuplicatePropertyKeys()) { var msg = format(DUPLICATE_PROPERTY_KEYS_TEMPLATE); return StoreResult.duplicateKeys(msg); } @@ -183,7 +181,7 @@ public long countAssets(List criteria) { public StoreResult updateAsset(Asset asset) { return transactionContext.execute(() -> { try (var connection = getConnection()) { - if (checkDuplicatePropertyKeys(asset)) { + if (asset.hasDuplicatePropertyKeys()) { var msg = format(DUPLICATE_PROPERTY_KEYS_TEMPLATE); return StoreResult.duplicateKeys(msg); } @@ -304,15 +302,6 @@ private void insertProperties(Asset asset, String assetId, Connection connection } } - private boolean checkDuplicatePropertyKeys(Asset asset) { - var properties = asset.getProperties(); - var privateProperties = asset.getPrivateProperties(); - if (privateProperties != null && properties != null) { - return privateProperties.keySet().stream().distinct().anyMatch(properties::containsKey); - } - return true; - } - private static class SqlPropertyWrapper { private final boolean isPrivate; private final AbstractMap.SimpleImmutableEntry property; diff --git a/extensions/control-plane/store/sql/asset-index-sql/src/main/java/org/eclipse/edc/connector/store/sql/assetindex/schema/BaseSqlDialectStatements.java b/extensions/control-plane/store/sql/asset-index-sql/src/main/java/org/eclipse/edc/connector/store/sql/assetindex/schema/BaseSqlDialectStatements.java index 264b37f2dce..4711a420a3d 100644 --- a/extensions/control-plane/store/sql/asset-index-sql/src/main/java/org/eclipse/edc/connector/store/sql/assetindex/schema/BaseSqlDialectStatements.java +++ b/extensions/control-plane/store/sql/asset-index-sql/src/main/java/org/eclipse/edc/connector/store/sql/assetindex/schema/BaseSqlDialectStatements.java @@ -123,8 +123,8 @@ public String getFormatAsJsonOperator() { @Override public SqlQueryStatement createQuery(QuerySpec querySpec) { var criteria = querySpec.getFilterExpression(); - var conditions = criteria.stream().map(SqlConditionExpression::new).collect(Collectors.toList()); - var results = conditions.stream().map(SqlConditionExpression::isValidExpression).collect(Collectors.toList()); + var conditions = criteria.stream().map(SqlConditionExpression::new).toList(); + var results = conditions.stream().map(SqlConditionExpression::isValidExpression).toList(); if (results.stream().anyMatch(Result::failed)) { var message = results.stream().flatMap(r -> r.getFailureMessages().stream()).collect(Collectors.joining(", ")); diff --git a/extensions/control-plane/store/sql/asset-index-sql/src/test/java/org/eclipse/edc/connector/store/sql/assetindex/PostgresAssetIndexTest.java b/extensions/control-plane/store/sql/asset-index-sql/src/test/java/org/eclipse/edc/connector/store/sql/assetindex/PostgresAssetIndexTest.java index 387c1ecc826..de80fa44bde 100644 --- a/extensions/control-plane/store/sql/asset-index-sql/src/test/java/org/eclipse/edc/connector/store/sql/assetindex/PostgresAssetIndexTest.java +++ b/extensions/control-plane/store/sql/asset-index-sql/src/test/java/org/eclipse/edc/connector/store/sql/assetindex/PostgresAssetIndexTest.java @@ -20,31 +20,17 @@ import org.eclipse.edc.connector.store.sql.assetindex.schema.postgres.PostgresDialectStatements; import org.eclipse.edc.junit.annotations.PostgresqlDbIntegrationTest; import org.eclipse.edc.policy.model.PolicyRegistrationTypes; -import org.eclipse.edc.spi.query.QuerySpec; -import org.eclipse.edc.spi.result.StoreResult; import org.eclipse.edc.spi.testfixtures.asset.AssetIndexTestBase; -import org.eclipse.edc.spi.testfixtures.asset.TestObject; import org.eclipse.edc.spi.types.TypeManager; -import org.eclipse.edc.spi.types.domain.asset.Asset; import org.eclipse.edc.sql.QueryExecutor; import org.eclipse.edc.sql.testfixtures.PostgresqlStoreSetupExtension; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Paths; -import java.util.List; -import java.util.stream.Collectors; -import java.util.stream.IntStream; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.eclipse.edc.spi.query.Criterion.criterion; -import static org.eclipse.edc.spi.result.StoreFailure.Reason.DUPLICATE_KEYS; @PostgresqlDbIntegrationTest @ExtendWith(PostgresqlStoreSetupExtension.class) @@ -73,118 +59,9 @@ void tearDown(PostgresqlStoreSetupExtension setupExtension) { setupExtension.runQuery("DROP TABLE " + sqlStatements.getAssetPropertyTable() + " CASCADE"); } - - @Test - @DisplayName("Verify an asset query based on an Asset property") - void query_byAssetProperty() { - List allAssets = createAssets(5); - var query = QuerySpec.Builder.newInstance().filter(criterion("test-key", "=", "test-value1")).build(); - - assertThat(sqlAssetIndex.queryAssets(query)).usingRecursiveFieldByFieldElementComparator().containsOnly(allAssets.get(1)); - - } - - @Test - @DisplayName("Verify an asset query based on an Asset property") - void query_byAssetPrivateProperty() { - List allAssets = createPrivateAssets(5); - var query = QuerySpec.Builder.newInstance().filter(criterion("test-pKey", "=", "test-pValue1")).build(); - - assertThat(sqlAssetIndex.queryAssets(query)).usingRecursiveFieldByFieldElementComparator().containsOnly(allAssets.get(1)); - - } - - @Test - @DisplayName("Verify an asset query based on an Asset property, when the left operand does not exist") - void query_byAssetProperty_leftOperandNotExist() { - createAssets(5); - var query = QuerySpec.Builder.newInstance().filter(criterion("notexist-key", "=", "test-value1")).build(); - - assertThat(sqlAssetIndex.queryAssets(query)).isEmpty(); - } - - @Test - @DisplayName("Verify that the correct Postgres JSON operator is used") - void verifyCorrectJsonOperator() { - assertThat(sqlStatements.getFormatAsJsonOperator()).isEqualTo("::json"); - } - - @Test - @DisplayName("Verify an asset query based on an Asset property, where the property value is actually a complex object") - void query_assetPropertyAsObject() { - var asset = TestFunctions.createAsset("id1"); - asset.getProperties().put("testobj", new TestObject("test123", 42, false)); - sqlAssetIndex.create(asset, TestFunctions.createDataAddress("test-type")); - - var assetsFound = sqlAssetIndex.queryAssets(QuerySpec.Builder.newInstance() - .filter(criterion("testobj", "like", "%test1%")) - .build()); - - assertThat(assetsFound).usingRecursiveFieldByFieldElementComparator().containsExactly(asset); - assertThat(asset.getProperty("testobj")).isInstanceOf(TestObject.class); - } - - @Test - @DisplayName("Verify an asset query based on an Asset property, where the right operand does not exist") - void query_byAssetProperty_rightOperandNotExist() { - createAssets(5); - var query = QuerySpec.Builder.newInstance().filter(criterion("test-key", "=", "notexist")).build(); - - assertThat(sqlAssetIndex.queryAssets(query)).isEmpty(); - } - - @Test - @DisplayName("Verify an asset query where the operator is invalid (=not supported)") - void queryAgreements_withQuerySpec_invalidOperator() { - var asset = TestFunctions.createAssetBuilder("id1").property("testproperty", "testvalue").build(); - sqlAssetIndex.create(asset, TestFunctions.createDataAddress("test-type")); - - var query = QuerySpec.Builder.newInstance().filter(criterion("testproperty", "<>", "foobar")).build(); - assertThatThrownBy(() -> sqlAssetIndex.queryAssets(query)).isInstanceOf(IllegalArgumentException.class); - } - - @Test - @DisplayName("Verify that creating an asset that contains duplicate keys in properties and private properties fails") - void createAsset_withDuplicatePropertyKeys() { - var asset = TestFunctions.createAssetBuilder("id1") - .property("testproperty", "testvalue") - .privateProperty("testproperty", "testvalue") - .build(); - - var result = sqlAssetIndex.create(asset, TestFunctions.createDataAddress("test-type")); - assertThat(result).isNotNull().extracting(StoreResult::reason).isEqualTo(DUPLICATE_KEYS); - } - @Override protected SqlAssetIndex getAssetIndex() { return sqlAssetIndex; } - /** - * creates a configurable amount of assets with one property ("test-key" = "test-valueN") and a data address of type - * "test-type" - */ - private List createAssets(int amount) { - return IntStream.range(0, amount).mapToObj(i -> { - var asset = TestFunctions.createAssetBuilder("test-asset" + i) - .property("test-key", "test-value" + i) - .build(); - var dataAddress = TestFunctions.createDataAddress("test-type"); - sqlAssetIndex.create(asset, dataAddress); - return asset; - }).collect(Collectors.toList()); - } - - private List createPrivateAssets(int amount) { - return IntStream.range(0, amount).mapToObj(i -> { - var asset = TestFunctions.createAssetBuilder("test-asset" + i) - .property("test-key", "test-value" + i) - .privateProperty("test-pKey", "test-pValue" + i) - .build(); - var dataAddress = TestFunctions.createDataAddress("test-type"); - sqlAssetIndex.create(asset, dataAddress); - return asset; - }).collect(Collectors.toList()); - } - } diff --git a/resources/openapi/openapi.yaml b/resources/openapi/openapi.yaml index 43b93b519dd..ff690a42247 100644 --- a/resources/openapi/openapi.yaml +++ b/resources/openapi/openapi.yaml @@ -330,6 +330,7 @@ paths: $ref: '#/components/schemas/ApiErrorDetail' "404": description: "Asset could not be updated, because it does not exist." + deprecated: true post: tags: - Asset @@ -367,6 +368,7 @@ paths: example: null items: $ref: '#/components/schemas/ApiErrorDetail' + deprecated: true /v2/assets/request: post: tags: @@ -397,6 +399,7 @@ paths: example: null items: $ref: '#/components/schemas/ApiErrorDetail' + deprecated: true /v2/assets/{assetId}/dataaddress: put: tags: @@ -438,6 +441,7 @@ paths: example: null items: $ref: '#/components/schemas/ApiErrorDetail' + deprecated: true /v2/assets/{id}: get: tags: @@ -478,6 +482,7 @@ paths: example: null items: $ref: '#/components/schemas/ApiErrorDetail' + deprecated: true delete: tags: - Asset @@ -527,6 +532,7 @@ paths: example: null items: $ref: '#/components/schemas/ApiErrorDetail' + deprecated: true /v2/assets/{id}/dataaddress: get: tags: @@ -567,6 +573,7 @@ paths: example: null items: $ref: '#/components/schemas/ApiErrorDetail' + deprecated: true /v2/catalog/request: post: tags: @@ -1549,6 +1556,190 @@ paths: example: null items: $ref: '#/components/schemas/ApiErrorDetail' + /v3/assets: + put: + tags: + - Asset + description: "Updates an asset with the given ID if it exists. If the asset\ + \ is not found, no further action is taken. DANGER ZONE: Note that updating\ + \ assets can have unexpected results, especially for contract offers that\ + \ have been sent out or are ongoing in contract negotiations." + operationId: updateAsset_1 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/AssetUpdateRequestDto' + responses: + "200": + description: Asset was updated successfully + "400": + description: "Request was malformed, e.g. id was null" + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + "404": + description: "Asset could not be updated, because it does not exist." + post: + tags: + - Asset + description: Creates a new asset together with a data address + operationId: createAsset_1 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Asset' + responses: + "200": + description: Asset was created successfully. Returns the asset Id and created + timestamp + content: + application/json: + schema: + $ref: '#/components/schemas/IdResponseDto' + "400": + description: Request body was malformed + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + "409": + description: "Could not create asset, because an asset with that ID already\ + \ exists" + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + /v3/assets/request: + post: + tags: + - Asset + description: ' all assets according to a particular query' + operationId: requestAssets_1 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/QuerySpecDto' + responses: + "200": + description: The assets matching the query + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/AssetResponseDto' + "400": + description: Request body was malformed + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + /v3/assets/{id}: + get: + tags: + - Asset + description: Gets an asset with the given ID + operationId: getAsset_1 + parameters: + - name: id + in: path + required: true + style: simple + explode: false + schema: + type: string + example: null + responses: + "200": + description: The asset + content: + application/json: + schema: + $ref: '#/components/schemas/Asset' + "400": + description: "Request was malformed, e.g. id was null" + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + "404": + description: An asset with the given ID does not exist + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + delete: + tags: + - Asset + description: "Removes an asset with the given ID if possible. Deleting an asset\ + \ is only possible if that asset is not yet referenced by a contract agreement,\ + \ in which case an error is returned. DANGER ZONE: Note that deleting assets\ + \ can have unexpected results, especially for contract offers that have been\ + \ sent out or ongoing or contract negotiations." + operationId: removeAsset_1 + parameters: + - name: id + in: path + required: true + style: simple + explode: false + schema: + type: string + example: null + responses: + "200": + description: Asset was deleted successfully + "400": + description: "Request was malformed, e.g. id was null" + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + "404": + description: An asset with the given ID does not exist + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + "409": + description: "The asset cannot be deleted, because it is referenced by a\ + \ contract agreement" + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' /{any}: get: tags: @@ -1647,6 +1838,8 @@ components: type: integer format: int64 example: null + dataAddress: + $ref: '#/components/schemas/DataAddress' id: type: string example: null diff --git a/resources/openapi/yaml/management-api/asset-api.yaml b/resources/openapi/yaml/management-api/asset-api.yaml index 910ea359cb4..7c84aada481 100644 --- a/resources/openapi/yaml/management-api/asset-api.yaml +++ b/resources/openapi/yaml/management-api/asset-api.yaml @@ -9,6 +9,7 @@ info: paths: /v2/assets: post: + deprecated: true description: Creates a new asset together with a data address operationId: createAsset requestBody: @@ -46,6 +47,7 @@ paths: tags: - Asset put: + deprecated: true description: "Updates an asset with the given ID if it exists. If the asset\ \ is not found, no further action is taken. DANGER ZONE: Note that updating\ \ assets can have unexpected results, especially for contract offers that\ @@ -74,6 +76,7 @@ paths: - Asset /v2/assets/request: post: + deprecated: true description: ' all assets according to a particular query' operationId: requestAssets requestBody: @@ -104,6 +107,7 @@ paths: - Asset /v2/assets/{assetId}/dataaddress: put: + deprecated: true description: Updates a DataAddress for an asset with the given ID. operationId: updateDataAddress parameters: @@ -143,6 +147,7 @@ paths: - Asset /v2/assets/{id}: delete: + deprecated: true description: "Removes an asset with the given ID if possible. Deleting an asset\ \ is only possible if that asset is not yet referenced by a contract agreement,\ \ in which case an error is returned. DANGER ZONE: Note that deleting assets\ @@ -190,6 +195,7 @@ paths: tags: - Asset get: + deprecated: true description: Gets an asset with the given ID operationId: getAsset parameters: @@ -228,6 +234,7 @@ paths: - Asset /v2/assets/{id}/dataaddress: get: + deprecated: true description: Gets a data address of an asset with the given ID operationId: getAssetDataAddress parameters: @@ -264,6 +271,186 @@ paths: description: An asset with the given ID does not exist tags: - Asset + /v3/assets: + post: + description: Creates a new asset together with a data address + operationId: createAsset_1 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Asset' + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/IdResponseDto' + description: Asset was created successfully. Returns the asset Id and created + timestamp + "400": + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + description: Request body was malformed + "409": + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + description: "Could not create asset, because an asset with that ID already\ + \ exists" + tags: + - Asset + put: + description: "Updates an asset with the given ID if it exists. If the asset\ + \ is not found, no further action is taken. DANGER ZONE: Note that updating\ + \ assets can have unexpected results, especially for contract offers that\ + \ have been sent out or are ongoing in contract negotiations." + operationId: updateAsset_1 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/AssetUpdateRequestDto' + responses: + "200": + description: Asset was updated successfully + "400": + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + description: "Request was malformed, e.g. id was null" + "404": + description: "Asset could not be updated, because it does not exist." + tags: + - Asset + /v3/assets/request: + post: + description: ' all assets according to a particular query' + operationId: requestAssets_1 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/QuerySpecDto' + responses: + "200": + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/AssetResponseDto' + description: The assets matching the query + "400": + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + description: Request body was malformed + tags: + - Asset + /v3/assets/{id}: + delete: + description: "Removes an asset with the given ID if possible. Deleting an asset\ + \ is only possible if that asset is not yet referenced by a contract agreement,\ + \ in which case an error is returned. DANGER ZONE: Note that deleting assets\ + \ can have unexpected results, especially for contract offers that have been\ + \ sent out or ongoing or contract negotiations." + operationId: removeAsset_1 + parameters: + - in: path + name: id + required: true + schema: + type: string + example: null + responses: + "200": + description: Asset was deleted successfully + "400": + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + description: "Request was malformed, e.g. id was null" + "404": + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + description: An asset with the given ID does not exist + "409": + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + description: "The asset cannot be deleted, because it is referenced by a\ + \ contract agreement" + tags: + - Asset + get: + description: Gets an asset with the given ID + operationId: getAsset_1 + parameters: + - in: path + name: id + required: true + schema: + type: string + example: null + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Asset' + description: The asset + "400": + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + description: "Request was malformed, e.g. id was null" + "404": + content: + application/json: + schema: + type: array + example: null + items: + $ref: '#/components/schemas/ApiErrorDetail' + description: An asset with the given ID does not exist + tags: + - Asset components: schemas: ApiErrorDetail: @@ -290,6 +477,8 @@ components: type: integer format: int64 example: null + dataAddress: + $ref: '#/components/schemas/DataAddress' id: type: string example: null diff --git a/spi/common/core-spi/build.gradle.kts b/spi/common/core-spi/build.gradle.kts index f8fedd7e554..c306a16b659 100644 --- a/spi/common/core-spi/build.gradle.kts +++ b/spi/common/core-spi/build.gradle.kts @@ -29,10 +29,11 @@ dependencies { testImplementation(project(":core:common:junit")) // needed by the abstract test spec located in testFixtures + testFixturesImplementation(project(":core:common:junit")) testFixturesImplementation(libs.bundles.jupiter) - testFixturesRuntimeOnly(libs.junit.jupiter.engine) testFixturesImplementation(libs.mockito.core) testFixturesImplementation(libs.assertj) + testFixturesRuntimeOnly(libs.junit.jupiter.engine) } diff --git a/spi/common/core-spi/src/main/java/org/eclipse/edc/spi/asset/AssetIndex.java b/spi/common/core-spi/src/main/java/org/eclipse/edc/spi/asset/AssetIndex.java index 5b039375cb9..06bb94d2df9 100644 --- a/spi/common/core-spi/src/main/java/org/eclipse/edc/spi/asset/AssetIndex.java +++ b/spi/common/core-spi/src/main/java/org/eclipse/edc/spi/asset/AssetIndex.java @@ -39,7 +39,7 @@ public interface AssetIndex extends DataAddressResolver { String ASSET_EXISTS_TEMPLATE = "Asset with ID %s already exists"; String ASSET_NOT_FOUND_TEMPLATE = "Asset with ID %s not found"; - String DATAADDRESS_NOT_FOUND_TEMPLATE = "DataAddress with ID %s not found"; + String DATA_ADDRESS_NOT_FOUND_TEMPLATE = "DataAddress with ID %s not found"; String DUPLICATE_PROPERTY_KEYS_TEMPLATE = "Duplicate keys in properties and private properties are not allowed"; /** @@ -73,12 +73,39 @@ public interface AssetIndex extends DataAddressResolver { * @param asset The {@link Asset} to store * @param dataAddress The {@link DataAddress} to store * @return {@link StoreResult#success()} if the objects were stored, {@link StoreResult#alreadyExists(String)} when an object with the same ID already exists. + * @deprecated please use {@link #create(Asset)} */ + @Deprecated(since = "0.1.2", forRemoval = true) default StoreResult create(Asset asset, DataAddress dataAddress) { return create(new AssetEntry(asset, dataAddress)); } - StoreResult create(AssetEntry item); + /** + * This method will be removed in favor of {@link #create(Asset)} + * + * @param item the asset entry. + * @return the result. + * @deprecated please use and override {@link #create(Asset)} + */ + @Deprecated(since = "0.1.2") + default StoreResult create(AssetEntry item) { + var asset = item.getAsset(); + var assetWithDataAddress = asset.toBuilder() + .dataAddress(item.getDataAddress()) + .build(); + return create(assetWithDataAddress); + } + + /** + * Stores a {@link Asset} in the asset index, if no asset with the same ID already exists. + * Implementors must ensure that it's stored in a transactional way. + * + * @param asset The {@link Asset} to store + * @return {@link StoreResult#success()} if the objects were stored, {@link StoreResult#alreadyExists(String)} when an object with the same ID already exists. + */ + default StoreResult create(Asset asset) { + return create(new AssetEntry(asset, asset.getDataAddress())); + } /** * Deletes an asset if it exists. diff --git a/spi/common/core-spi/src/main/java/org/eclipse/edc/spi/asset/DataAddressResolver.java b/spi/common/core-spi/src/main/java/org/eclipse/edc/spi/asset/DataAddressResolver.java index a3ee0eafd78..b01c7609eba 100644 --- a/spi/common/core-spi/src/main/java/org/eclipse/edc/spi/asset/DataAddressResolver.java +++ b/spi/common/core-spi/src/main/java/org/eclipse/edc/spi/asset/DataAddressResolver.java @@ -28,8 +28,7 @@ public interface DataAddressResolver { * a storage system like a database or a document store. * * @param assetId The {@code assetId} for which the data pointer should be fetched. - * @return A DataAddress - * @throws IllegalArgumentException if no corresponding {@code DataAddress} was found for a certain asset + * @return A DataAddress, null if not found */ DataAddress resolveForAsset(String assetId); } diff --git a/spi/common/core-spi/src/main/java/org/eclipse/edc/spi/types/domain/asset/Asset.java b/spi/common/core-spi/src/main/java/org/eclipse/edc/spi/types/domain/asset/Asset.java index 0e9f6259765..c9f84a284fb 100644 --- a/spi/common/core-spi/src/main/java/org/eclipse/edc/spi/types/domain/asset/Asset.java +++ b/spi/common/core-spi/src/main/java/org/eclipse/edc/spi/types/domain/asset/Asset.java @@ -19,6 +19,7 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; import org.eclipse.edc.spi.entity.Entity; +import org.eclipse.edc.spi.types.domain.DataAddress; import java.util.HashMap; import java.util.Map; @@ -43,10 +44,9 @@ public class Asset extends Entity { public static final String EDC_ASSET_TYPE = EDC_NAMESPACE + "Asset"; public static final String EDC_ASSET_PROPERTIES = EDC_NAMESPACE + "properties"; public static final String EDC_ASSET_PRIVATE_PROPERTIES = EDC_NAMESPACE + "privateProperties"; + public static final String EDC_ASSET_DATA_ADDRESS = EDC_NAMESPACE + "dataAddress"; - @Deprecated(since = "milestone9") - private static final String DEPRECATED_PROPERTY_PREFIX = "asset:prop:"; - + private DataAddress dataAddress; private final Map properties; private final Map privateProperties; @@ -57,28 +57,28 @@ protected Asset() { @Override public String getId() { - return id == null ? ofNullable(getPropertyAsString(PROPERTY_ID)).orElse(getPropertyAsString(DEPRECATED_PROPERTY_PREFIX + id)) : id; + return id == null ? ofNullable(getPropertyAsString(PROPERTY_ID)).orElse(getPropertyAsString(id)) : id; } @JsonIgnore public String getName() { return ofNullable(getPropertyAsString(PROPERTY_NAME)) - .orElse(getPropertyAsString(DEPRECATED_PROPERTY_PREFIX + "name")); + .orElse(getPropertyAsString("name")); } @JsonIgnore public String getDescription() { - return ofNullable(getPropertyAsString(PROPERTY_DESCRIPTION)).orElse(getPropertyAsString(DEPRECATED_PROPERTY_PREFIX + "description")); + return ofNullable(getPropertyAsString(PROPERTY_DESCRIPTION)).orElse(getPropertyAsString("description")); } @JsonIgnore public String getVersion() { - return ofNullable(getPropertyAsString(PROPERTY_VERSION)).orElse(getPropertyAsString(DEPRECATED_PROPERTY_PREFIX + "version")); + return ofNullable(getPropertyAsString(PROPERTY_VERSION)).orElse(getPropertyAsString("version")); } @JsonIgnore public String getContentType() { - return ofNullable(getPropertyAsString(PROPERTY_CONTENT_TYPE)).orElse(getPropertyAsString(DEPRECATED_PROPERTY_PREFIX + "contenttype")); + return ofNullable(getPropertyAsString(PROPERTY_CONTENT_TYPE)).orElse(getPropertyAsString("contenttype")); } public Map getProperties() { @@ -90,6 +90,11 @@ public Object getProperty(String key) { return properties.get(key); } + @JsonIgnore + public Object getPropertyOrPrivate(String key) { + return properties.getOrDefault(key, privateProperties.get(key)); + } + public Map getPrivateProperties() { return privateProperties; } @@ -103,9 +108,27 @@ private String getPropertyAsString(String key) { return val != null ? val.toString() : null; } - private String getPrivatePropertyAsString(String key) { - var val = getPrivateProperty(key); - return val != null ? val.toString() : null; + public DataAddress getDataAddress() { + return dataAddress; + } + + public Builder toBuilder() { + return Asset.Builder.newInstance() + .id(id) + .properties(properties) + .privateProperties(privateProperties) + .dataAddress(dataAddress) + .createdAt(createdAt); + } + + @JsonIgnore + public boolean hasDuplicatePropertyKeys() { + var properties = getProperties(); + var privateProperties = getPrivateProperties(); + if (privateProperties != null && properties != null) { + return privateProperties.keySet().stream().distinct().anyMatch(properties::containsKey); + } + return true; } @JsonPOJOBuilder(withPrefix = "") @@ -122,9 +145,8 @@ public static Builder newInstance() { @Override public Builder id(String id) { - // todo: remove storing the ID in the properties map in future versions - entity.properties.put(PROPERTY_ID, id); entity.id = id; + entity.properties.put(PROPERTY_ID, id); return self(); } @@ -139,14 +161,6 @@ public Builder self() { return this; } - @Override - public Asset build() { - if (entity.getId() == null) { - id(UUID.randomUUID().toString()); - } - return super.build(); - } - public Builder name(String title) { entity.properties.put(PROPERTY_NAME, title); return self(); @@ -178,6 +192,11 @@ public Builder property(String key, Object value) { return self(); } + public Builder dataAddress(DataAddress dataAddress) { + entity.dataAddress = dataAddress; + return self(); + } + public Builder privateProperties(Map privateProperties) { Objects.requireNonNull(privateProperties); entity.privateProperties.putAll(privateProperties); @@ -188,6 +207,17 @@ public Builder privateProperty(String key, Object value) { entity.privateProperties.put(key, value); return self(); } + + @Override + public Asset build() { + super.build(); + + if (entity.getId() == null) { + id(UUID.randomUUID().toString()); + } + + return entity; + } } } diff --git a/spi/common/core-spi/src/test/java/org/eclipse/edc/spi/types/domain/asset/AssetTest.java b/spi/common/core-spi/src/test/java/org/eclipse/edc/spi/types/domain/asset/AssetTest.java index 0f86d4d5d07..f2bb53181d0 100644 --- a/spi/common/core-spi/src/test/java/org/eclipse/edc/spi/types/domain/asset/AssetTest.java +++ b/spi/common/core-spi/src/test/java/org/eclipse/edc/spi/types/domain/asset/AssetTest.java @@ -40,9 +40,8 @@ void verifySerialization() { .build(); var json = typeManager.writeValueAsString(asset); - assertThat(json).isNotNull(); - assertThat(json).contains("abcd123") + assertThat(json).isNotNull().contains("abcd123") .contains("application/json") .contains("testasset") .contains("some-critical.value") @@ -79,4 +78,4 @@ void getNamedProperty_whenNotPresent_shouldReturnNull() { assertThat(asset.getName()).isNull(); assertThat(asset.getVersion()).isNull(); } -} \ No newline at end of file +} diff --git a/spi/common/core-spi/src/testFixtures/java/org/eclipse/edc/spi/testfixtures/asset/AssetIndexTestBase.java b/spi/common/core-spi/src/testFixtures/java/org/eclipse/edc/spi/testfixtures/asset/AssetIndexTestBase.java index eae806e1452..108b0091bd4 100644 --- a/spi/common/core-spi/src/testFixtures/java/org/eclipse/edc/spi/testfixtures/asset/AssetIndexTestBase.java +++ b/spi/common/core-spi/src/testFixtures/java/org/eclipse/edc/spi/testfixtures/asset/AssetIndexTestBase.java @@ -16,13 +16,15 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import org.assertj.core.api.Assertions; import org.eclipse.edc.spi.asset.AssetIndex; import org.eclipse.edc.spi.query.Criterion; import org.eclipse.edc.spi.query.QuerySpec; +import org.eclipse.edc.spi.query.SortOrder; +import org.eclipse.edc.spi.result.StoreFailure; import org.eclipse.edc.spi.result.StoreResult; import org.eclipse.edc.spi.types.domain.DataAddress; import org.eclipse.edc.spi.types.domain.asset.Asset; -import org.eclipse.edc.spi.types.domain.asset.AssetEntry; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -35,15 +37,18 @@ import java.util.List; import java.util.Map; import java.util.UUID; +import java.util.stream.Collectors; +import java.util.stream.IntStream; import java.util.stream.Stream; import static java.util.Collections.emptyList; -import static java.util.stream.Collectors.toList; import static java.util.stream.IntStream.range; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.assertj.core.api.Assertions.catchException; +import static org.eclipse.edc.junit.assertions.AbstractResultAssert.assertThat; +import static org.eclipse.edc.spi.query.Criterion.criterion; import static org.eclipse.edc.spi.result.StoreFailure.Reason.ALREADY_EXISTS; +import static org.eclipse.edc.spi.result.StoreFailure.Reason.DUPLICATE_KEYS; import static org.eclipse.edc.spi.result.StoreFailure.Reason.NOT_FOUND; /** @@ -55,8 +60,8 @@ public abstract class AssetIndexTestBase { public AssetIndexTestBase() { var supportedOperators = getSupportedOperators(); - boolean hasLikeOperator = true; - boolean hasInOperator = true; + var hasLikeOperator = true; + var hasInOperator = true; if (!supportedOperators.isEmpty()) { hasLikeOperator = supportedOperators.contains("like"); hasInOperator = supportedOperators.contains("in"); @@ -66,10 +71,9 @@ public AssetIndexTestBase() { } @Test - @DisplayName("Accept an asset and a data address that don't exist yet") - void acceptAssetAndDataAddress_doesNotExist() { + void create_shouldStoreAsset() { var assetExpected = getAsset("id1"); - getAssetIndex().create(assetExpected, getDataAddress()); + getAssetIndex().create(assetExpected); var assetFound = getAssetIndex().findById("id1"); @@ -77,31 +81,14 @@ void acceptAssetAndDataAddress_doesNotExist() { assertThat(assetFound).usingRecursiveComparison().isEqualTo(assetExpected); } - @Test - @DisplayName("Verify that an asset can be stored") - void accept() { - var asset = createAsset("test-asset", UUID.randomUUID().toString()); - var dataAddress = createDataAddress(asset); - var assetIndex = getAssetIndex(); - var result = assetIndex.create(asset, dataAddress); - assertThat(result.succeeded()).isTrue(); - - assertThat(assetIndex.queryAssets(QuerySpec.none())).hasSize(1) - .usingRecursiveFieldByFieldElementComparator() - .contains(asset); - assertThat(assetIndex.resolveForAsset(asset.getId())).usingRecursiveComparison().isEqualTo(dataAddress); - } - @Test @DisplayName("Verify that storing an asset fails if it already exists") - void accept_exists() { + void create_exists() { var asset = createAsset("test-asset", UUID.randomUUID().toString()); - var dataAddress = createDataAddress(asset); var assetIndex = getAssetIndex(); - assetIndex.create(asset, dataAddress); + assetIndex.create(asset); - DataAddress dataAddress1 = createDataAddress(asset); - var result = assetIndex.create(asset, dataAddress1); + var result = assetIndex.create(asset); assertThat(result.succeeded()).isFalse(); assertThat(result.reason()).isEqualTo(ALREADY_EXISTS); @@ -109,51 +96,29 @@ void accept_exists() { assertThat(getAssetIndex().queryAssets(QuerySpec.none())).hasSize(1) .usingRecursiveFieldByFieldElementComparator() .contains(asset); - } @Test @DisplayName("Verify that multiple assets can be stored") - void acceptAll() { - var asset1 = createAsset("asset1", "id1"); - var asset2 = createAsset("asset2", "id2"); - - var address1 = createDataAddress(asset1); - var address2 = createDataAddress(asset2); + void create_shouldAddMultipleAssets() { + var asset1 = createAssetBuilder("id1").name("asset1").dataAddress(createDataAddress()).build(); + var asset2 = createAssetBuilder("id2").name("asset2").dataAddress(createDataAddress()).build(); var assetIndex = getAssetIndex(); - var results = Stream.of(new AssetEntry(asset1, address1), new AssetEntry(asset2, address2)).map(assetIndex::create); + var results = Stream.of(asset1, asset2).map(assetIndex::create); assertThat(results).allSatisfy(sr -> assertThat(sr.succeeded()).isTrue()); + assertThat(assetIndex.queryAssets(QuerySpec.none())).hasSize(2) .usingRecursiveFieldByFieldElementComparator() .containsExactlyInAnyOrder(asset1, asset2); } - @Test - @DisplayName("Verify that the correct results are returned for a series of assets, when one fails") - void acceptMany_oneExists_shouldReturnFailure() { - var asset1 = createAsset("asset1", "id1"); - - var address1 = createDataAddress(asset1); - var address2 = createDataAddress(asset1); - - var results = List.of(new AssetEntry(asset1, address1), new AssetEntry(asset1, address2)) - .stream().map(entry -> getAssetIndex().create(entry)); - - assertThat(results).extracting(StoreResult::succeeded).contains(true, false); - // only one address/asset combo should exist - assertThat(getAssetIndex().queryAssets(QuerySpec.none())).hasSize(1) - .usingRecursiveFieldByFieldElementComparator() - .contains(asset1); - - } - @Test @DisplayName("Verify that the object was stored with the correct timestamp") - void accept_verifyTimestamp() { + void create_verifyTimestamp() { var asset = getAsset("test-asset"); - getAssetIndex().create(asset, getDataAddress()); + getAssetIndex().create(asset); var allAssets = getAssetIndex().queryAssets(QuerySpec.none()); @@ -162,64 +127,34 @@ void accept_verifyTimestamp() { } @Test - @DisplayName("Accept an asset and a data address that already exist") - void acceptAssetAndDataAddress_exists() { - var asset = getAsset("id1"); - getAssetIndex().create(asset, getDataAddress()); - getAssetIndex().create(asset, getDataAddress()); - - var assets = getAssetIndex().queryAssets(QuerySpec.none()); - - assertThat(assets).hasSize(1) - .usingRecursiveFieldByFieldElementComparator() - .containsOnly(asset); - } - - @Test - @DisplayName("Accept an asset entry that doesn't exist yet") - void acceptAssetEntry_doesNotExist() { - var assetExpected = getAsset("id1"); - getAssetIndex().create(new AssetEntry(assetExpected, getDataAddress())); - - - var assetFound = getAssetIndex().findById("id1"); - - assertThat(assetFound).isNotNull(); - assertThat(assetFound).usingRecursiveComparison().isEqualTo(assetExpected); - - } - - @Test - @DisplayName("Accept an asset entry that already exists") - void acceptEntry_exists() { - var asset = getAsset("id1"); - getAssetIndex().create(new AssetEntry(asset, getDataAddress())); - getAssetIndex().create(asset, getDataAddress()); - - var assets = getAssetIndex().queryAssets(QuerySpec.none()); + @DisplayName("Verify that creating an asset that contains duplicate keys in properties and private properties fails") + void createAsset_withDuplicatePropertyKeys() { + var asset = createAssetBuilder("id1") + .property("testproperty", "testvalue") + .privateProperty("testproperty", "testvalue") + .build(); - assertThat(assets).hasSize(1) - .usingRecursiveFieldByFieldElementComparator() - .containsOnly(asset); + var result = getAssetIndex().create(asset, createDataAddress()); + assertThat(result).isFailed().extracting(StoreFailure::getReason).isEqualTo(DUPLICATE_KEYS); } @Test @DisplayName("Delete an asset that doesn't exist") - void deleteAsset_doesNotExist() { + void deleteById_doesNotExist() { var assetDeleted = getAssetIndex().deleteById("id1"); - assertThat(assetDeleted).isNotNull().extracting(StoreResult::reason).isEqualTo(NOT_FOUND); + Assertions.assertThat(assetDeleted).isNotNull().extracting(StoreResult::reason).isEqualTo(NOT_FOUND); } @Test @DisplayName("Delete an asset that exists") - void deleteAsset_exists() { + void deleteById_exists() { var asset = getAsset("id1"); - getAssetIndex().create(asset, getDataAddress()); + getAssetIndex().create(asset); var assetDeleted = getAssetIndex().deleteById("id1"); - assertThat(assetDeleted).isNotNull().extracting(StoreResult::succeeded).isEqualTo(true); + Assertions.assertThat(assetDeleted).isNotNull().extracting(StoreResult::succeeded).isEqualTo(true); assertThat(assetDeleted.getContent()).usingRecursiveComparison().isEqualTo(asset); assertThat(getAssetIndex().queryAssets(QuerySpec.none())).isEmpty(); @@ -228,7 +163,7 @@ void deleteAsset_exists() { @Test void count_withResults() { var assets = range(0, 5).mapToObj(i -> getAsset("id" + i)); - assets.forEach(a -> getAssetIndex().create(a, getDataAddress())); + assets.forEach(a -> getAssetIndex().create(a)); var criteria = Collections.emptyList(); var count = getAssetIndex().countAssets(criteria); @@ -245,12 +180,24 @@ void count_withNoResults() { assertThat(count).isEqualTo(0); } + @Test + void queryAssets_shouldReturnAllTheAssets_whenQuerySpecIsEmpty() { + var assets = IntStream.range(0, 5) + .mapToObj(i -> createAsset("test-asset", "id" + i)) + .peek(a -> getAssetIndex().create(a)).toList(); + + var result = getAssetIndex().queryAssets(QuerySpec.none()); + + var result1 = result.toList(); + assertThat(result1).hasSize(5).usingRecursiveFieldByFieldElementComparator().containsAll(assets); + } + @Test @DisplayName("Query assets with query spec") - void queryAsset_limit() { + void queryAssets_limit() { for (var i = 1; i <= 10; i++) { var asset = getAsset("id" + i); - getAssetIndex().create(asset, getDataAddress()); + getAssetIndex().create(asset); } var querySpec = QuerySpec.Builder.newInstance().limit(3).offset(2).build(); @@ -261,11 +208,8 @@ void queryAsset_limit() { @Test @DisplayName("Query assets with query spec and short asset count") - void queryAsset_shortCount() { - range(1, 5).forEach((item) -> { - var asset = getAsset("id" + item); - getAssetIndex().create(asset, getDataAddress()); - }); + void queryAssets_shortCount() { + range(1, 5).mapToObj(it -> getAsset("id" + it)).forEach(asset -> getAssetIndex().create(asset)); var querySpec = QuerySpec.Builder.newInstance() .limit(3) .offset(2) @@ -277,10 +221,22 @@ void queryAsset_shortCount() { } @Test - @DisplayName("Query assets with query spec where the property (=leftOperand) does not exist") - void queryAsset_shouldThrowException_whenUnsupportedOperator() { + void queryAssets_shouldReturnNoAssets_whenOffsetIsOutOfBounds() { + range(1, 5).mapToObj(it -> getAsset("id" + it)).forEach(asset -> getAssetIndex().create(asset)); + var querySpec = QuerySpec.Builder.newInstance() + .limit(3) + .offset(5) + .build(); + + var assetsFound = getAssetIndex().queryAssets(querySpec); + + assertThat(assetsFound).isEmpty(); + } + + @Test + void queryAssets_shouldThrowException_whenUnsupportedOperator() { var asset = getAsset("id1"); - getAssetIndex().create(asset, getDataAddress()); + getAssetIndex().create(asset); var unsupportedOperator = new Criterion(Asset.PROPERTY_ID, "unsupported", "42"); assertThatThrownBy(() -> getAssetIndex().queryAssets(filter(unsupportedOperator))) @@ -288,10 +244,9 @@ void queryAsset_shouldThrowException_whenUnsupportedOperator() { } @Test - @DisplayName("Query assets with query spec where the property (=leftOperand) does not exist") - void queryAsset_nonExistProperty() { + void queryAssets_shouldReturnEmptyStream_whenLeftOperandDoesNotExist() { var asset = getAsset("id1"); - getAssetIndex().create(asset, getDataAddress()); + getAssetIndex().create(asset); var notExistingProperty = new Criterion("noexist", "=", "42"); var assets = getAssetIndex().queryAssets(filter(notExistingProperty)); @@ -301,10 +256,10 @@ void queryAsset_nonExistProperty() { @Test @DisplayName("Query assets with query spec where the value (=rightOperand) does not exist") - void queryAsset_nonExistValue() { + void queryAssets_nonExistValue() { var asset = getAsset("id1"); asset.getProperties().put("someprop", "someval"); - getAssetIndex().create(asset, getDataAddress()); + getAssetIndex().create(asset); var notExistingValue = new Criterion("someprop", "=", "some-other-val"); var assets = getAssetIndex().queryAssets(filter(notExistingValue)); @@ -314,13 +269,13 @@ void queryAsset_nonExistValue() { @Test @DisplayName("Verifies an asset query, that contains a filter expression") - void queryAsset_withFilterExpression() { + void queryAssets_withFilterExpression() { var expected = createAssetBuilder("id1").property("version", "2.0").property("contenttype", "whatever").build(); var differentVersion = createAssetBuilder("id2").property("version", "2.1").property("contenttype", "whatever").build(); var differentContentType = createAssetBuilder("id3").property("version", "2.0").property("contenttype", "different").build(); - getAssetIndex().create(expected, getDataAddress()); - getAssetIndex().create(differentVersion, getDataAddress()); - getAssetIndex().create(differentContentType, getDataAddress()); + getAssetIndex().create(expected); + getAssetIndex().create(differentVersion); + getAssetIndex().create(differentContentType); var filter = filter( new Criterion("version", "=", "2.0"), new Criterion("contenttype", "=", "whatever") @@ -328,7 +283,24 @@ void queryAsset_withFilterExpression() { var assets = getAssetIndex().queryAssets(filter); - assertThat(assets).usingRecursiveFieldByFieldElementComparator().containsOnly(expected); + assertThat(assets).hasSize(1).usingRecursiveFieldByFieldElementComparator().containsOnly(expected); + } + + @Test + @DisplayName("Verify an asset query based on an Asset property, where the property value is actually a complex object") + @EnabledIfSystemProperty(named = "assetindex.supports.operator.like", matches = "true", disabledReason = "This test only runs if the LIKE operator is supported") + void query_assetPropertyAsObject() { + var dataAddress = createDataAddress(); + var asset = createAssetBuilder("id1").dataAddress(dataAddress).build(); + asset.getProperties().put("testobj", new TestObject("test123", 42, false)); + getAssetIndex().create(asset); + + var assetsFound = getAssetIndex().queryAssets(QuerySpec.Builder.newInstance() + .filter(criterion("testobj", "like", "%test1%")) + .build()); + + assertThat(assetsFound).hasSize(1).first().usingRecursiveComparison().isEqualTo(asset); + assertThat(asset.getProperty("testobj")).isInstanceOf(TestObject.class); } @Test @@ -336,9 +308,9 @@ void queryAssets_multipleFound() { var testAsset1 = createAsset("foobar"); var testAsset2 = createAsset("barbaz"); var testAsset3 = createAsset("barbaz"); - getAssetIndex().create(testAsset1, createDataAddress(testAsset1)); - getAssetIndex().create(testAsset2, createDataAddress(testAsset2)); - getAssetIndex().create(testAsset3, createDataAddress(testAsset3)); + getAssetIndex().create(testAsset1); + getAssetIndex().create(testAsset2); + getAssetIndex().create(testAsset3); var criterion = new Criterion(Asset.PROPERTY_NAME, "=", "barbaz"); var assets = getAssetIndex().queryAssets(filter(criterion)); @@ -348,11 +320,9 @@ void queryAssets_multipleFound() { @Test @DisplayName("Query assets using the IN operator") - void queryAsset_in() { - var asset1 = getAsset("id1"); - getAssetIndex().create(asset1, getDataAddress()); - var asset2 = getAsset("id2"); - getAssetIndex().create(asset2, getDataAddress()); + void queryAssets_in() { + getAssetIndex().create(getAsset("id1")); + getAssetIndex().create(getAsset("id2")); var criterion = new Criterion(Asset.PROPERTY_ID, "in", List.of("id1", "id2")); var assetsFound = getAssetIndex().queryAssets(filter(criterion)); @@ -362,28 +332,55 @@ void queryAsset_in() { @Test @DisplayName("Query assets using the IN operator, invalid righ-operand") - void queryAsset_in_shouldThrowException_whenInvalidRightOperand() { + void queryAssets_in_shouldThrowException_whenInvalidRightOperand() { var asset1 = getAsset("id1"); - getAssetIndex().create(asset1, getDataAddress()); + getAssetIndex().create(asset1); var asset2 = getAsset("id2"); - getAssetIndex().create(asset2, getDataAddress()); + getAssetIndex().create(asset2); var invalidRightOperand = new Criterion(Asset.PROPERTY_ID, "in", "(id1, id2)"); - var exception = catchException(() -> getAssetIndex() - .queryAssets(filter(invalidRightOperand)) - .collect(toList())); // must collect, otherwise the stream may not get materialized + assertThatThrownBy(() -> getAssetIndex().queryAssets(filter(invalidRightOperand)).toList()) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void queryAssets_withSorting() { + var assets = IntStream.range(9, 12) + .mapToObj(i -> createAsset("test-asset", "id" + i)) + .peek(a -> getAssetIndex().create(a)) + .toList(); + var spec = QuerySpec.Builder.newInstance() + .sortField(Asset.PROPERTY_ID) + .sortOrder(SortOrder.ASC) + .build(); + + var result = getAssetIndex().queryAssets(spec); + + assertThat(result).usingRecursiveFieldByFieldElementComparator().containsAll(assets); + } + + @Test + void queryAssets_withPrivateSorting() { + var assets = IntStream.range(0, 10) + .mapToObj(i -> createAssetBuilder(String.valueOf(i)).privateProperty("pKey", "pValue").build()) + .peek(a -> getAssetIndex().create(a)) + .collect(Collectors.toList()); + + var spec = QuerySpec.Builder.newInstance().sortField("pKey").sortOrder(SortOrder.ASC).build(); + + var result = getAssetIndex().queryAssets(spec); - assertThat(exception).isInstanceOf(IllegalArgumentException.class); + assertThat(result).usingRecursiveFieldByFieldElementComparator().containsAll(assets); } @Test @DisplayName("Query assets using the LIKE operator") @EnabledIfSystemProperty(named = "assetindex.supports.operator.like", matches = "true", disabledReason = "This test only runs if the LIKE operator is supported") - void queryAsset_like() { + void queryAssets_like() { var asset1 = getAsset("id1"); - getAssetIndex().create(asset1, getDataAddress()); + getAssetIndex().create(asset1); var asset2 = getAsset("id2"); - getAssetIndex().create(asset2, getDataAddress()); + getAssetIndex().create(asset2); var criterion = new Criterion(Asset.PROPERTY_ID, "LIKE", "id%"); var assetsFound = getAssetIndex().queryAssets(filter(criterion)); @@ -394,10 +391,10 @@ void queryAsset_like() { @Test @DisplayName("Query assets using the LIKE operator on a json value") @EnabledIfSystemProperty(named = "assetindex.supports.operator.like", matches = "true", disabledReason = "This test only runs if the LIKE operator is supported") - void queryAsset_likeJson() throws JsonProcessingException { + void queryAssets_likeJson() throws JsonProcessingException { var asset = getAsset("id1"); asset.getProperties().put("myjson", new ObjectMapper().writeValueAsString(new TestObject("test123", 42, false))); - getAssetIndex().create(asset, getDataAddress()); + getAssetIndex().create(asset); var criterion = new Criterion("myjson", "LIKE", "%test123%"); var assetsFound = getAssetIndex().queryAssets(filter(criterion)); @@ -408,11 +405,11 @@ void queryAsset_likeJson() throws JsonProcessingException { @Test @DisplayName("Query assets using two criteria, each with the LIKE operator on a nested json value") @EnabledIfSystemProperty(named = "assetindex.supports.operator.like", matches = "true", disabledReason = "This test only runs if the LIKE operator is supported") - void queryAsset_likeJson_withComplexObject() throws JsonProcessingException { + void queryAssets_likeJson_withComplexObject() throws JsonProcessingException { var asset = getAsset("id1"); var jsonObject = Map.of("root", Map.of("key1", "value1", "nested1", Map.of("key2", "value2", "key3", Map.of("theKey", "theValue, this is what we're looking for")))); asset.getProperties().put("myProp", new ObjectMapper().writeValueAsString(jsonObject)); - getAssetIndex().create(asset, getDataAddress()); + getAssetIndex().create(asset); var criterion1 = new Criterion("myProp", "LIKE", "%is%what%"); var criterion2 = new Criterion("myProp", "LIKE", "%we're%looking%"); @@ -422,23 +419,24 @@ void queryAsset_likeJson_withComplexObject() throws JsonProcessingException { } @Test - @DisplayName("Find an asset that doesn't exist") - void findAsset_doesNotExist() { - assertThat(getAssetIndex().findById("id1")).isNull(); - } - - @Test - @DisplayName("Find an asset that exists") - void findAsset_exists() { - var asset = getAsset("id1"); - getAssetIndex().create(asset, getDataAddress()); + void findById_shouldReturnAsset() { + var id = UUID.randomUUID().toString(); + var asset = getAsset(id); + getAssetIndex().create(asset); - var assetFound = getAssetIndex().findById("id1"); + var assetFound = getAssetIndex().findById(id); assertThat(assetFound).isNotNull(); assertThat(assetFound).usingRecursiveComparison().isEqualTo(asset); } + @Test + void findById_shouldReturnNull_whenAssetDoesNotExist() { + var result = getAssetIndex().findById("unexistent"); + + assertThat(result).isNull(); + } + @Test @DisplayName("Find a data address that doesn't exist") void resolveDataAddress_doesNotExist() { @@ -450,7 +448,7 @@ void resolveDataAddress_doesNotExist() { void resolveDataAddress_exists() { var asset = getAsset("id1"); var dataAddress = getDataAddress(); - getAssetIndex().create(asset, dataAddress); + getAssetIndex().create(asset); var dataAddressFound = getAssetIndex().resolveForAsset("id1"); @@ -466,7 +464,7 @@ void updateAsset_doesNotExist() { var assetIndex = getAssetIndex(); var updated = assetIndex.updateAsset(assetExpected); - assertThat(updated).isNotNull().extracting(StoreResult::succeeded).isEqualTo(false); + Assertions.assertThat(updated).isNotNull().extracting(StoreResult::succeeded).isEqualTo(false); } @Test @@ -475,20 +473,19 @@ void updateAsset_exists_addsProperty() { var id = "id1"; var asset = getAsset(id); var assetIndex = getAssetIndex(); - assetIndex.create(asset, getDataAddress()); + assetIndex.create(asset); assertThat(assetIndex.countAssets(List.of())).isEqualTo(1); - var updatedAsset = asset; - updatedAsset.getProperties().put("newKey", "newValue"); - var updated = assetIndex.updateAsset(updatedAsset); + asset.getProperties().put("newKey", "newValue"); + var updated = assetIndex.updateAsset(asset); - assertThat(updated).isNotNull(); + Assertions.assertThat(updated).isNotNull(); var assetFound = getAssetIndex().findById("id1"); assertThat(assetFound).isNotNull(); - assertThat(assetFound).usingRecursiveComparison().isEqualTo(updatedAsset); + assertThat(assetFound).usingRecursiveComparison().isEqualTo(asset); } @Test @@ -498,20 +495,19 @@ void updateAsset_exists_removesProperty() { var asset = getAsset(id); asset.getProperties().put("newKey", "newValue"); var assetIndex = getAssetIndex(); - assetIndex.create(asset, getDataAddress()); + assetIndex.create(asset); assertThat(assetIndex.countAssets(List.of())).isEqualTo(1); - var updatedAsset = asset; - updatedAsset.getProperties().remove("newKey"); - var updated = assetIndex.updateAsset(updatedAsset); + asset.getProperties().remove("newKey"); + var updated = assetIndex.updateAsset(asset); - assertThat(updated).isNotNull(); + Assertions.assertThat(updated).isNotNull(); var assetFound = getAssetIndex().findById("id1"); assertThat(assetFound).isNotNull(); - assertThat(assetFound).usingRecursiveComparison().isEqualTo(updatedAsset); + assertThat(assetFound).usingRecursiveComparison().isEqualTo(asset); assertThat(assetFound.getProperties().keySet()).doesNotContain("newKey"); } @@ -522,20 +518,19 @@ void updateAsset_exists_replacingProperty() { var asset = getAsset(id); asset.getProperties().put("newKey", "originalValue"); var assetIndex = getAssetIndex(); - assetIndex.create(asset, getDataAddress()); + assetIndex.create(asset); assertThat(assetIndex.countAssets(List.of())).isEqualTo(1); - var updatedAsset = asset; - updatedAsset.getProperties().put("newKey", "newValue"); - var updated = assetIndex.updateAsset(updatedAsset); + asset.getProperties().put("newKey", "newValue"); + var updated = assetIndex.updateAsset(asset); - assertThat(updated).isNotNull(); + Assertions.assertThat(updated).isNotNull(); var assetFound = getAssetIndex().findById("id1"); assertThat(assetFound).isNotNull(); - assertThat(assetFound).usingRecursiveComparison().isEqualTo(updatedAsset); + assertThat(assetFound).usingRecursiveComparison().isEqualTo(asset); assertThat(assetFound.getProperties()).containsEntry("newKey", "newValue"); } @@ -547,7 +542,7 @@ void updateDataAddress_doesNotExist() { var assetIndex = getAssetIndex(); var updated = assetIndex.updateDataAddress(id, assetExpected); - assertThat(updated).isNotNull().extracting(StoreResult::reason).isEqualTo(NOT_FOUND); + Assertions.assertThat(updated).isNotNull().extracting(StoreResult::reason).isEqualTo(NOT_FOUND); } @Test @@ -556,14 +551,13 @@ void updateDataAddress_exists_addsProperty() { var id = "id1"; var asset = getAsset(id); var assetIndex = getAssetIndex(); - var dataAddress = getDataAddress(); - assetIndex.create(asset, dataAddress); + assetIndex.create(asset); var updatedDataAddress = getDataAddress(); updatedDataAddress.getProperties().put("newKey", "newValue"); var updated = assetIndex.updateDataAddress(id, updatedDataAddress); - assertThat(updated).isNotNull(); + Assertions.assertThat(updated).isNotNull(); var addressFound = getAssetIndex().resolveForAsset("id1"); @@ -579,13 +573,13 @@ void updateDataAddress_exists_removesProperty() { var assetIndex = getAssetIndex(); var dataAddress = getDataAddress(); dataAddress.getProperties().put("newKey", "newValue"); - assetIndex.create(asset, dataAddress); + assetIndex.create(asset); var updatedDataAddress = dataAddress; updatedDataAddress.getProperties().remove("newKey"); var updated = assetIndex.updateDataAddress(id, updatedDataAddress); - assertThat(updated).isNotNull(); + Assertions.assertThat(updated).isNotNull(); var addressFound = getAssetIndex().resolveForAsset("id1"); @@ -602,18 +596,17 @@ void updateDataAddress_exists_replacesProperty() { var assetIndex = getAssetIndex(); var dataAddress = getDataAddress(); dataAddress.getProperties().put("newKey", "originalValue"); - assetIndex.create(asset, dataAddress); + assetIndex.create(asset); - var updatedDataAddress = dataAddress; - updatedDataAddress.getProperties().put("newKey", "newValue"); - var updated = assetIndex.updateDataAddress(id, updatedDataAddress); + dataAddress.getProperties().put("newKey", "newValue"); + var updated = assetIndex.updateDataAddress(id, dataAddress); - assertThat(updated).isNotNull(); + Assertions.assertThat(updated).isNotNull(); var addressFound = getAssetIndex().resolveForAsset("id1"); assertThat(addressFound).isNotNull(); - assertThat(addressFound).usingRecursiveComparison().isEqualTo(updatedDataAddress); + assertThat(addressFound).usingRecursiveComparison().isEqualTo(dataAddress); assertThat(addressFound.getProperties()).containsEntry("newKey", "newValue"); } @@ -629,7 +622,16 @@ protected Asset createAsset(String name, String id) { @NotNull protected Asset createAsset(String name, String id, String contentType) { - return Asset.Builder.newInstance().id(id).name(name).version("1").contentType(contentType).build(); + return Asset.Builder.newInstance() + .id(id) + .name(name) + .version("1") + .contentType(contentType) + .dataAddress(DataAddress.Builder.newInstance() + .keyName("test-keyname") + .type(contentType) + .build()) + .build(); } /** @@ -646,10 +648,10 @@ protected Collection getSupportedOperators() { */ protected abstract AssetIndex getAssetIndex(); - protected DataAddress createDataAddress(Asset asset) { + protected DataAddress createDataAddress() { return DataAddress.Builder.newInstance() .keyName("test-keyname") - .type(asset.getContentType()) + .type("type") .build(); } @@ -667,7 +669,8 @@ protected Asset.Builder createAssetBuilder(String id) { .id(id) .createdAt(Clock.systemUTC().millis()) .property("key" + id, "value" + id) - .contentType("type"); + .contentType("type") + .dataAddress(getDataAddress()); } private DataAddress getDataAddress() { diff --git a/spi/control-plane/control-plane-spi/src/main/java/org/eclipse/edc/connector/spi/asset/AssetService.java b/spi/control-plane/control-plane-spi/src/main/java/org/eclipse/edc/connector/spi/asset/AssetService.java index 9c4fd733ab1..839d381b522 100644 --- a/spi/control-plane/control-plane-spi/src/main/java/org/eclipse/edc/connector/spi/asset/AssetService.java +++ b/spi/control-plane/control-plane-spi/src/main/java/org/eclipse/edc/connector/spi/asset/AssetService.java @@ -45,9 +45,19 @@ public interface AssetService { * @param asset the asset * @param dataAddress the address of the asset * @return successful result if the asset is created correctly, failure otherwise + * @deprecated please use {@link #create(Asset)} */ + @Deprecated(since = "0.1.2") ServiceResult create(Asset asset, DataAddress dataAddress); + /** + * Create an asset + * + * @param asset the asset + * @return successful result if the asset is created correctly, failure otherwise + */ + ServiceResult create(Asset asset); + /** * Delete an asset * diff --git a/system-tests/e2e-transfer-test/runner/src/test/java/org/eclipse/edc/test/e2e/Participant.java b/system-tests/e2e-transfer-test/runner/src/test/java/org/eclipse/edc/test/e2e/Participant.java index 440f0149eb5..3bd559361c9 100644 --- a/system-tests/e2e-transfer-test/runner/src/test/java/org/eclipse/edc/test/e2e/Participant.java +++ b/system-tests/e2e-transfer-test/runner/src/test/java/org/eclipse/edc/test/e2e/Participant.java @@ -87,10 +87,9 @@ public Participant(String name, String participantId) { public void createAsset(String assetId, Map dataAddressProperties) { var requestBody = createObjectBuilder() .add(CONTEXT, createObjectBuilder().add(EDC_PREFIX, EDC_NAMESPACE)) - .add("asset", createObjectBuilder() - .add(ID, assetId) - .add("properties", createObjectBuilder() - .add("description", "description"))) + .add(ID, assetId) + .add("properties", createObjectBuilder() + .add("description", "description")) .add("dataAddress", createObjectBuilder(dataAddressProperties)) .build(); @@ -99,7 +98,7 @@ public void createAsset(String assetId, Map dataAddressPropertie .contentType(JSON) .body(requestBody) .when() - .post("/v2/assets") + .post("/v3/assets") .then() .statusCode(200) .contentType(JSON); diff --git a/system-tests/management-api/management-api-test-runner/src/test/java/org/eclipse/edc/test/e2e/managementapi/AssetApiDeprecatedEndToEndTest.java b/system-tests/management-api/management-api-test-runner/src/test/java/org/eclipse/edc/test/e2e/managementapi/AssetApiDeprecatedEndToEndTest.java new file mode 100644 index 00000000000..5846c63edb8 --- /dev/null +++ b/system-tests/management-api/management-api-test-runner/src/test/java/org/eclipse/edc/test/e2e/managementapi/AssetApiDeprecatedEndToEndTest.java @@ -0,0 +1,398 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * 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 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.edc.test.e2e.managementapi; + +import io.restassured.http.ContentType; +import io.restassured.specification.RequestSpecification; +import jakarta.json.JsonObject; +import jakarta.json.JsonObjectBuilder; +import org.eclipse.edc.junit.annotations.EndToEndTest; +import org.eclipse.edc.spi.asset.AssetIndex; +import org.eclipse.edc.spi.types.domain.DataAddress; +import org.eclipse.edc.spi.types.domain.asset.Asset; +import org.eclipse.edc.spi.types.domain.asset.AssetEntry; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static io.restassured.RestAssured.given; +import static jakarta.json.Json.createArrayBuilder; +import static jakarta.json.Json.createObjectBuilder; +import static java.util.Collections.emptyList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.edc.jsonld.spi.JsonLdKeywords.CONTEXT; +import static org.eclipse.edc.jsonld.spi.JsonLdKeywords.ID; +import static org.eclipse.edc.jsonld.spi.JsonLdKeywords.TYPE; +import static org.eclipse.edc.spi.CoreConstants.EDC_NAMESPACE; +import static org.eclipse.edc.spi.CoreConstants.EDC_PREFIX; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; + +/** + * V2 end-to-end tests + * + * @deprecated can be removed when Asset v2 endpoints will be removed. + */ +@EndToEndTest +@Deprecated(since = "0.1.2") +public class AssetApiDeprecatedEndToEndTest extends BaseManagementApiEndToEndTest { + + private static final String BASE_PATH = "/management/v2/assets"; + + private static final String TEST_ASSET_ID = "test-asset-id"; + private static final String TEST_ASSET_CONTENTTYPE = "application/json"; + private static final String TEST_ASSET_DESCRIPTION = "test description"; + private static final String TEST_ASSET_VERSION = "0.4.2"; + private static final String TEST_ASSET_NAME = "test-asset"; + + @Test + void getAssetById() { + //insert one asset into the index + controlPlane.getContext().getService(AssetIndex.class) + .create(new AssetEntry(createAsset().build(), + createDataAddress().build())); + + var body = baseRequest() + .get("/" + TEST_ASSET_ID) + .then() + .statusCode(200) + .extract().body().jsonPath(); + + assertThat(body).isNotNull(); + assertThat(body.getString(ID)).isEqualTo(TEST_ASSET_ID); + assertThat(body.getMap("edc:properties")) + .hasSize(5) + .containsEntry("edc:name", TEST_ASSET_NAME) + .containsEntry("edc:description", TEST_ASSET_DESCRIPTION) + .containsEntry("edc:contenttype", TEST_ASSET_CONTENTTYPE) + .containsEntry("edc:version", TEST_ASSET_VERSION); + } + + @Test + void createAsset_shouldBeStored() { + + var assetJson = createObjectBuilder() + .add(CONTEXT, createObjectBuilder().add(EDC_PREFIX, EDC_NAMESPACE)) + .add(TYPE, "Asset") + .add(ID, TEST_ASSET_ID) + .add("properties", createPropertiesBuilder().build()) + .build(); + + var dataAddressJson = createObjectBuilder() + .add(CONTEXT, createObjectBuilder().add(EDC_PREFIX, EDC_NAMESPACE)) + .add(TYPE, "DataAddress") + .add("type", "test-type").build(); + + var assetNewJson = createObjectBuilder() + .add(CONTEXT, createObjectBuilder().add(EDC_PREFIX, EDC_NAMESPACE)) + .add(TYPE, "AssetEntryDto") + .add("asset", assetJson) + .add("dataAddress", dataAddressJson) + .build(); + + baseRequest() + .contentType(ContentType.JSON) + .body(assetNewJson) + .post() + .then() + .log().ifError() + .statusCode(200) + .body(ID, is("test-asset-id")); + var assetIndex = controlPlane.getContext().getService(AssetIndex.class); + + assertThat(assetIndex.countAssets(List.of())).isEqualTo(1); + assertThat(assetIndex.findById("test-asset-id")).isNotNull(); + } + + @Test + void createAsset_shouldFail_whenBodyIsNotValid() { + var assetJson = createObjectBuilder() + .add(CONTEXT, createObjectBuilder().add(EDC_PREFIX, EDC_NAMESPACE)) + .add(TYPE, "Asset") + .add(ID, " ") + .add("properties", createPropertiesBuilder().build()) + .build(); + + var assetNewJson = createObjectBuilder() + .add(CONTEXT, createObjectBuilder().add(EDC_PREFIX, EDC_NAMESPACE)) + .add(TYPE, "AssetEntryDto") + .add("asset", assetJson) + .build(); + + baseRequest() + .contentType(ContentType.JSON) + .body(assetNewJson) + .post() + .then() + .log().ifError() + .statusCode(400); + + var assetIndex = controlPlane.getContext().getService(AssetIndex.class); + + assertThat(assetIndex.countAssets(emptyList())).isEqualTo(0); + } + + @Test + void createAsset_withoutPrefix_shouldAddEdcNamespace() { + var assetJson = createObjectBuilder() + .add(CONTEXT, createObjectBuilder().add(EDC_PREFIX, EDC_NAMESPACE)) + .add(TYPE, "Asset") + .add(ID, TEST_ASSET_ID) + .add("properties", createPropertiesBuilder() + .add("unprefixed-key", "test-value").build()) + .build(); + + var dataAddressJson = createObjectBuilder() + .add(CONTEXT, createObjectBuilder().add(EDC_PREFIX, EDC_NAMESPACE)) + .add(TYPE, "DataAddress") + .add("type", "test-type") + .add("unprefixed-key", "test-value").build(); + + var assetNewJson = createObjectBuilder() + .add(CONTEXT, createObjectBuilder().add(EDC_PREFIX, EDC_NAMESPACE)) + .add(TYPE, "AssetEntryDto") + .add("asset", assetJson) + .add("dataAddress", dataAddressJson) + .build(); + + baseRequest() + .contentType(ContentType.JSON) + .body(assetNewJson) + .post() + .then() + .log().ifError() + .statusCode(200) + .body(ID, is("test-asset-id")); + var assetIndex = controlPlane.getContext().getService(AssetIndex.class); + + assertThat(assetIndex.countAssets(List.of())).isEqualTo(1); + var asset = assetIndex.findById("test-asset-id"); + assertThat(asset).isNotNull(); + //make sure unprefixed keys are caught and prefixed with the EDC_NAMESPACE ns. + assertThat(asset.getProperties().keySet()) + .hasSize(6) + .allMatch(key -> key.startsWith(EDC_NAMESPACE)); + + var dataAddress = assetIndex.resolveForAsset(asset.getId()); + assertThat(dataAddress).isNotNull(); + assertThat(dataAddress.getProperties().keySet()) + .hasSize(2) + .allMatch(key -> key.startsWith(EDC_NAMESPACE)); + + } + + @Test + void queryAsset_byContentType() { + //insert one asset into the index + controlPlane.getContext().getService(AssetIndex.class) + .create(new AssetEntry(Asset.Builder.newInstance().id("test-asset").contentType("application/octet-stream").build(), + createDataAddress().build())); + + var query = createObjectBuilder() + .add(CONTEXT, createObjectBuilder().add(EDC_PREFIX, EDC_NAMESPACE)) + .add("filterExpression", createArrayBuilder() + .add(createObjectBuilder() + .add("operandLeft", EDC_NAMESPACE + "contenttype") + .add("operator", "=") + .add("operandRight", "application/octet-stream")) + ).build(); + + baseRequest() + .contentType(ContentType.JSON) + .body(query) + .post("/request") + .then() + .log().ifError() + .statusCode(200) + .body("size()", is(1)); + } + + @Test + void queryAsset_byCustomStringProperty() { + //insert one asset into the index + var assetIndex = controlPlane.getContext().getService(AssetIndex.class); + assetIndex.create(new AssetEntry(Asset.Builder.newInstance() + .id("test-asset") + .contentType("application/octet-stream") + .property("myProp", "myVal") + .build(), + createDataAddress().build())); + + var query = createSingleFilterQuery("myProp", "=", "myVal"); + + baseRequest() + .contentType(ContentType.JSON) + .body(query) + .post("/request") + .then() + .log().ifError() + .statusCode(200) + .body("size()", is(1)); + } + + @Test + void queryAsset_byCustomComplexProperty_whenJsonPathQuery_expectNoResult() { + //insert one asset into the index + var assetIndex = controlPlane.getContext().getService(AssetIndex.class); + assetIndex.create(new AssetEntry(Asset.Builder.newInstance() + .id("test-asset") + .contentType("application/octet-stream") + // use a custom, complex object type + .property("myProp", new TestObject("test desc", 42)) + .build(), + createDataAddress().build())); + + var query = createSingleFilterQuery("myProp.description", "=", "test desc"); + + // querying custom complex types in "json-path" style is expected not to work. + baseRequest() + .contentType(ContentType.JSON) + .body(query) + .post("/request") + .then() + .log().ifError() + .statusCode(200) + .body("size()", is(0)); + } + + @Test + void queryAsset_byCustomComplexProperty_whenLikeOperator_expectException() { + //insert one asset into the index + var assetIndex = controlPlane.getContext().getService(AssetIndex.class); + assetIndex.create(new AssetEntry(Asset.Builder.newInstance() + .id("test-asset") + .contentType("application/octet-stream") + // use a custom, complex object type + .property("myProp", new TestObject("test desc", 42)) + .build(), + createDataAddress().build())); + + var query = createSingleFilterQuery("myProp", "LIKE", "test desc"); + + // querying custom complex types in "json-path" style is expected not to work. + baseRequest() + .contentType(ContentType.JSON) + .body(query) + .post("/request") + .then() + .log().ifError() + .statusCode(500); + } + + @Test + void updateAsset() { + var asset = createAsset(); + var assetIndex = controlPlane.getContext().getService(AssetIndex.class); + assetIndex.create(new AssetEntry(asset.build(), createDataAddress().build())); + + var assetJson = createObjectBuilder() + .add(CONTEXT, createObjectBuilder().add(EDC_PREFIX, EDC_NAMESPACE)) + .add(TYPE, "Asset") + .add(ID, TEST_ASSET_ID) + .add("properties", createPropertiesBuilder() + .add("some-new-property", "some-new-value").build()) + .build(); + + baseRequest() + .contentType(ContentType.JSON) + .body(assetJson) + .put() + .then() + .log().all() + .statusCode(204) + .body(notNullValue()); + + var dbAsset = assetIndex.findById(TEST_ASSET_ID); + assertThat(dbAsset).isNotNull(); + assertThat(dbAsset.getProperties()).containsEntry(EDC_NAMESPACE + "some-new-property", "some-new-value"); + } + + @Test + void getDataAddress() { + controlPlane.getContext().getService(AssetIndex.class) + .create(new AssetEntry(Asset.Builder.newInstance().id("test-asset").build(), + DataAddress.Builder.newInstance().type("test-type").property(EDC_NAMESPACE + "another-key", "another-value").build())); + + baseRequest() + .get("/test-asset/dataaddress") + .then() + .statusCode(200) + .body("'edc:type'", equalTo("test-type")) + .body("'edc:another-key'", equalTo("another-value")); + } + + private DataAddress.Builder createDataAddress() { + return DataAddress.Builder.newInstance().type("test-type"); + } + + private Asset.Builder createAsset() { + return Asset.Builder.newInstance() + .id(TEST_ASSET_ID) + .name(TEST_ASSET_NAME) + .description(TEST_ASSET_DESCRIPTION) + .contentType(TEST_ASSET_CONTENTTYPE) + .version(TEST_ASSET_VERSION); + } + + private JsonObjectBuilder createPropertiesBuilder() { + return createObjectBuilder() + .add("name", TEST_ASSET_NAME) + .add("description", TEST_ASSET_DESCRIPTION) + .add("version", TEST_ASSET_VERSION) + .add("contentType", TEST_ASSET_CONTENTTYPE); + } + + private JsonObject createSingleFilterQuery(String leftOperand, String operator, String rightOperand) { + var criteria = createArrayBuilder() + .add(createObjectBuilder() + .add(TYPE, "CriterionDto") + .add("operandLeft", leftOperand) + .add("operator", operator) + .add("operandRight", rightOperand) + ); + + return createObjectBuilder() + .add(CONTEXT, createObjectBuilder().add(EDC_PREFIX, EDC_NAMESPACE)) + .add(TYPE, "QuerySpecDto") + .add("filterExpression", criteria) + .build(); + } + + private RequestSpecification baseRequest() { + return given() + .port(PORT) + .basePath(BASE_PATH) + .when(); + } + + private static class TestObject { + private final String description; + private final int number; + + TestObject(String description, int number) { + this.description = description; + this.number = number; + } + + public String getDescription() { + return description; + } + + public int getNumber() { + return number; + } + } +} diff --git a/system-tests/management-api/management-api-test-runner/src/test/java/org/eclipse/edc/test/e2e/managementapi/AssetApiEndToEndTest.java b/system-tests/management-api/management-api-test-runner/src/test/java/org/eclipse/edc/test/e2e/managementapi/AssetApiEndToEndTest.java index b904594262e..b904736a94e 100644 --- a/system-tests/management-api/management-api-test-runner/src/test/java/org/eclipse/edc/test/e2e/managementapi/AssetApiEndToEndTest.java +++ b/system-tests/management-api/management-api-test-runner/src/test/java/org/eclipse/edc/test/e2e/managementapi/AssetApiEndToEndTest.java @@ -37,14 +37,16 @@ import static org.eclipse.edc.jsonld.spi.JsonLdKeywords.TYPE; import static org.eclipse.edc.spi.CoreConstants.EDC_NAMESPACE; import static org.eclipse.edc.spi.CoreConstants.EDC_PREFIX; -import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; +/** + * Asset V3 endpoints end-to-end tests + */ @EndToEndTest public class AssetApiEndToEndTest extends BaseManagementApiEndToEndTest { - private static final String BASE_PATH = "/management/v2/assets"; + private static final String BASE_PATH = "/management/v3/assets"; private static final String TEST_ASSET_ID = "test-asset-id"; private static final String TEST_ASSET_CONTENTTYPE = "application/json"; @@ -56,8 +58,7 @@ public class AssetApiEndToEndTest extends BaseManagementApiEndToEndTest { void getAssetById() { //insert one asset into the index controlPlane.getContext().getService(AssetIndex.class) - .create(new AssetEntry(createAsset().build(), - createDataAddress().build())); + .create(createAsset().dataAddress(createDataAddress().type("addressType").build()).build()); var body = baseRequest() .get("/" + TEST_ASSET_ID) @@ -73,33 +74,26 @@ void getAssetById() { .containsEntry("edc:description", TEST_ASSET_DESCRIPTION) .containsEntry("edc:contenttype", TEST_ASSET_CONTENTTYPE) .containsEntry("edc:version", TEST_ASSET_VERSION); + assertThat(body.getMap("'edc:dataAddress'")) + .containsEntry("edc:type", "addressType"); } @Test void createAsset_shouldBeStored() { - var assetJson = createObjectBuilder() .add(CONTEXT, createObjectBuilder().add(EDC_PREFIX, EDC_NAMESPACE)) .add(TYPE, "Asset") .add(ID, TEST_ASSET_ID) .add("properties", createPropertiesBuilder().build()) - .build(); - - var dataAddressJson = createObjectBuilder() - .add(CONTEXT, createObjectBuilder().add(EDC_PREFIX, EDC_NAMESPACE)) - .add(TYPE, "DataAddress") - .add("type", "test-type").build(); - - var assetNewJson = createObjectBuilder() - .add(CONTEXT, createObjectBuilder().add(EDC_PREFIX, EDC_NAMESPACE)) - .add(TYPE, "AssetEntryDto") - .add("asset", assetJson) - .add("dataAddress", dataAddressJson) + .add("dataAddress", createObjectBuilder() + .add(TYPE, "DataAddress") + .add("type", "test-type") + .build()) .build(); baseRequest() .contentType(ContentType.JSON) - .body(assetNewJson) + .body(assetJson) .post() .then() .log().ifError() @@ -120,15 +114,9 @@ void createAsset_shouldFail_whenBodyIsNotValid() { .add("properties", createPropertiesBuilder().build()) .build(); - var assetNewJson = createObjectBuilder() - .add(CONTEXT, createObjectBuilder().add(EDC_PREFIX, EDC_NAMESPACE)) - .add(TYPE, "AssetEntryDto") - .add("asset", assetJson) - .build(); - baseRequest() .contentType(ContentType.JSON) - .body(assetNewJson) + .body(assetJson) .post() .then() .log().ifError() @@ -147,24 +135,16 @@ void createAsset_withoutPrefix_shouldAddEdcNamespace() { .add(ID, TEST_ASSET_ID) .add("properties", createPropertiesBuilder() .add("unprefixed-key", "test-value").build()) - .build(); - - var dataAddressJson = createObjectBuilder() - .add(CONTEXT, createObjectBuilder().add(EDC_PREFIX, EDC_NAMESPACE)) - .add(TYPE, "DataAddress") - .add("type", "test-type") - .add("unprefixed-key", "test-value").build(); - - var assetNewJson = createObjectBuilder() - .add(CONTEXT, createObjectBuilder().add(EDC_PREFIX, EDC_NAMESPACE)) - .add(TYPE, "AssetEntryDto") - .add("asset", assetJson) - .add("dataAddress", dataAddressJson) + .add("dataAddress", createObjectBuilder() + .add(TYPE, "DataAddress") + .add("type", "test-type") + .add("unprefixed-key", "test-value") + .build()) .build(); baseRequest() .contentType(ContentType.JSON) - .body(assetNewJson) + .body(assetJson) .post() .then() .log().ifError() @@ -298,6 +278,8 @@ void updateAsset() { .add(ID, TEST_ASSET_ID) .add("properties", createPropertiesBuilder() .add("some-new-property", "some-new-value").build()) + .add("dataAddress", createObjectBuilder() + .add("type", "addressType")) .build(); baseRequest() @@ -312,20 +294,7 @@ void updateAsset() { var dbAsset = assetIndex.findById(TEST_ASSET_ID); assertThat(dbAsset).isNotNull(); assertThat(dbAsset.getProperties()).containsEntry(EDC_NAMESPACE + "some-new-property", "some-new-value"); - } - - @Test - void getDataAddress() { - controlPlane.getContext().getService(AssetIndex.class) - .create(new AssetEntry(Asset.Builder.newInstance().id("test-asset").build(), - DataAddress.Builder.newInstance().type("test-type").property(EDC_NAMESPACE + "another-key", "another-value").build())); - - baseRequest() - .get("/test-asset/dataaddress") - .then() - .statusCode(200) - .body("'edc:type'", equalTo("test-type")) - .body("'edc:another-key'", equalTo("another-value")); + assertThat(dbAsset.getDataAddress().getType()).isEqualTo("addressType"); } private DataAddress.Builder createDataAddress() { @@ -372,21 +341,5 @@ private RequestSpecification baseRequest() { .when(); } - private static class TestObject { - private final String description; - private final int number; - - TestObject(String description, int number) { - this.description = description; - this.number = number; - } - - public String getDescription() { - return description; - } - - public int getNumber() { - return number; - } - } + private record TestObject(String description, int number) { } } diff --git a/system-tests/management-api/management-api-test-runner/src/test/java/org/eclipse/edc/test/e2e/managementapi/CatalogApiEndToEndTest.java b/system-tests/management-api/management-api-test-runner/src/test/java/org/eclipse/edc/test/e2e/managementapi/CatalogApiEndToEndTest.java index e93d3e2ea0e..ef008192045 100644 --- a/system-tests/management-api/management-api-test-runner/src/test/java/org/eclipse/edc/test/e2e/managementapi/CatalogApiEndToEndTest.java +++ b/system-tests/management-api/management-api-test-runner/src/test/java/org/eclipse/edc/test/e2e/managementapi/CatalogApiEndToEndTest.java @@ -23,7 +23,6 @@ import org.eclipse.edc.spi.asset.AssetIndex; import org.eclipse.edc.spi.types.domain.DataAddress; import org.eclipse.edc.spi.types.domain.asset.Asset; -import org.eclipse.edc.spi.types.domain.asset.AssetEntry; import org.junit.jupiter.api.Test; import java.util.UUID; @@ -67,8 +66,8 @@ void shouldReturnCatalog_withoutQuerySpec() { @Test void shouldReturnCatalog_withQuerySpec() { - var asset = createAsset("id-1"); - var asset1 = createAsset("id-2"); + var asset = createAsset("id-1").dataAddress(createDataAddress().build()); + var asset1 = createAsset("id-2").dataAddress(createDataAddress().build()); var assetIndex = controlPlane.getContext().getService(AssetIndex.class); var policyDefinitionStore = controlPlane.getContext().getService(PolicyDefinitionStore.class); @@ -88,8 +87,8 @@ void shouldReturnCatalog_withQuerySpec() { policyDefinitionStore.create(PolicyDefinition.Builder.newInstance().id(policyId).policy(policy).build()); contractDefinitionStore.save(cd); - assetIndex.create(new AssetEntry(asset.build(), createDataAddress().build())); - assetIndex.create(new AssetEntry(asset1.build(), createDataAddress().build())); + assetIndex.create(asset.build()); + assetIndex.create(asset1.build()); var criteria = createArrayBuilder() .add(createObjectBuilder()