diff --git a/edc-controlplane/edc-controlplane-base/build.gradle.kts b/edc-controlplane/edc-controlplane-base/build.gradle.kts index 9eab6832c..1d169697e 100644 --- a/edc-controlplane/edc-controlplane-base/build.gradle.kts +++ b/edc-controlplane/edc-controlplane-base/build.gradle.kts @@ -30,6 +30,8 @@ dependencies { runtimeOnly(project(":edc-extensions:edr:edr-api-v2")) runtimeOnly(project(":edc-extensions:edr:edr-callback")) runtimeOnly(project(":edc-extensions:tokenrefresh-handler")) + runtimeOnly(project(":edc-extensions:agreements")) + runtimeOnly(libs.edc.core.edrstore) runtimeOnly(libs.edc.edr.store.receiver) runtimeOnly(libs.edc.dpf.transfer.signaling) diff --git a/edc-extensions/agreements/README.md b/edc-extensions/agreements/README.md new file mode 100644 index 000000000..96cdd59f5 --- /dev/null +++ b/edc-extensions/agreements/README.md @@ -0,0 +1,38 @@ +# Agreements Retirement Extension + +This extension is introduced to allow a dataspace dataset provider to _prematurely_ retire an active contract agreement. + +Contract agreements are immutable entities by design. +The word prematurely is used here since contract agreements should only expire once the contractual terms agreed upon between participants no longer holds. + +Even though the previous statements are valid, the need to prematurely retire an active contract agreement exists if, for example, +the contract agreement is a digital representation of a physical agreement which might have changed via legal +mechanisms, hence resulting in a new contract. The digital representation is no longer valid and shouldn't allow any data transfers. + +## Technical approach + +Since contract agreements are immutable, the following approach was used to represent a contract agreement retirement. + +A policy pre validator was introduced that checks if the attached contract agreement exists in the `AgreementRetirementStore`. +If it exists, the validation is considered failed. + +This policy pre validator is registered both in the `policy-monitor` and `transfer` scopes. In both cases, a failed pre validation +leads to transfer process termination. + +An API was created to enable dataset providers to manage `AgreementRetirementEntry` entities in the `AgreementRetirementStore` via an endpoint. + +## AgreementRetirementEntry schema + +An `AgreementRetirementEntry` is composed of: +- A contract agreement id. +- A retirement reason. +- An agreement retirement timestamp. + +`AgreementRetirementEntry` entities can be managed via the `/retireagreements` endpoint of the data management API. +Please refer to the swagger API spec for detailed examples on how to manage these entities. + +## Impact on active or new transfer processes + +Once a contract agreement is retired, all active transfer processes related with that agreement +will be terminated. New transfer process requests made from the consumer using the retired agreement will also fail with an +agreement is invalid message. \ No newline at end of file diff --git a/edc-extensions/agreements/build.gradle.kts b/edc-extensions/agreements/build.gradle.kts new file mode 100644 index 000000000..ea6a46d2f --- /dev/null +++ b/edc-extensions/agreements/build.gradle.kts @@ -0,0 +1,29 @@ +/******************************************************************************** + * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +plugins { + `java-library` + `maven-publish` +} + +dependencies { + api(project(":edc-extensions:agreements:retirement-evaluation-core")) + api(project(":edc-extensions:agreements:retirement-evaluation-api")) + api(project(":edc-extensions:agreements:retirement-evaluation-spi")) +} diff --git a/edc-extensions/agreements/retirement-evaluation-api/build.gradle.kts b/edc-extensions/agreements/retirement-evaluation-api/build.gradle.kts new file mode 100644 index 000000000..cdb7bac44 --- /dev/null +++ b/edc-extensions/agreements/retirement-evaluation-api/build.gradle.kts @@ -0,0 +1,43 @@ +/******************************************************************************** + * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +plugins { + `java-library` + `maven-publish` + id(libs.plugins.swagger.get().pluginId) +} +dependencies { + + implementation(project(":edc-extensions:agreements:retirement-evaluation-spi")) + implementation(libs.edc.api.management.config) + + implementation(libs.jakarta.rsApi) + + testImplementation(testFixtures(libs.edc.core.jersey)) + testImplementation(libs.edc.spi.core) + testImplementation(libs.edc.junit) + testImplementation(libs.restAssured) + testImplementation(project(":edc-extensions:agreements:retirement-evaluation-spi")) +} + +edcBuild { + swagger { + apiGroup.set("control-plane") + } +} \ No newline at end of file diff --git a/edc-extensions/agreements/retirement-evaluation-api/src/main/java/org/eclipse/tractusx/edc/agreements/retirement/api/AgreementsRetirementApiExtension.java b/edc-extensions/agreements/retirement-evaluation-api/src/main/java/org/eclipse/tractusx/edc/agreements/retirement/api/AgreementsRetirementApiExtension.java new file mode 100644 index 000000000..39c9a3bad --- /dev/null +++ b/edc-extensions/agreements/retirement-evaluation-api/src/main/java/org/eclipse/tractusx/edc/agreements/retirement/api/AgreementsRetirementApiExtension.java @@ -0,0 +1,74 @@ +/******************************************************************************** + * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +package org.eclipse.tractusx.edc.agreements.retirement.api; + +import jakarta.json.Json; +import org.eclipse.edc.runtime.metamodel.annotation.Extension; +import org.eclipse.edc.runtime.metamodel.annotation.Inject; +import org.eclipse.edc.spi.monitor.Monitor; +import org.eclipse.edc.spi.system.ServiceExtension; +import org.eclipse.edc.spi.system.ServiceExtensionContext; +import org.eclipse.edc.transform.spi.TypeTransformerRegistry; +import org.eclipse.edc.validator.spi.JsonObjectValidatorRegistry; +import org.eclipse.edc.web.spi.WebService; +import org.eclipse.edc.web.spi.configuration.ApiContext; +import org.eclipse.tractusx.edc.agreements.retirement.api.transform.JsonObjectFromAgreementRetirementTransformer; +import org.eclipse.tractusx.edc.agreements.retirement.api.transform.JsonObjectToAgreementsRetirementEntryTransformer; +import org.eclipse.tractusx.edc.agreements.retirement.api.v3.AgreementsRetirementApiV3Controller; +import org.eclipse.tractusx.edc.agreements.retirement.spi.service.AgreementsRetirementService; + +import java.util.Map; + +import static org.eclipse.tractusx.edc.agreements.retirement.api.AgreementsRetirementApiExtension.NAME; + + +@Extension(value = NAME) +public class AgreementsRetirementApiExtension implements ServiceExtension { + + public static final String NAME = "Contract Agreement Retirement API "; + + @Override + public String name() { + return NAME; + } + + @Inject + private WebService webService; + @Inject + private TypeTransformerRegistry transformerRegistry; + @Inject + private JsonObjectValidatorRegistry validator; + @Inject + private AgreementsRetirementService agreementsRetirementService; + @Inject + private Monitor monitor; + + @Override + public void initialize(ServiceExtensionContext context) { + var jsonFactory = Json.createBuilderFactory(Map.of()); + var managementTypeTransformerRegistry = transformerRegistry.forContext("management-api"); + + managementTypeTransformerRegistry.register(new JsonObjectFromAgreementRetirementTransformer(jsonFactory)); + managementTypeTransformerRegistry.register(new JsonObjectToAgreementsRetirementEntryTransformer()); + + webService.registerResource(ApiContext.MANAGEMENT, new AgreementsRetirementApiV3Controller(agreementsRetirementService, managementTypeTransformerRegistry, validator, monitor)); + } + +} diff --git a/edc-extensions/agreements/retirement-evaluation-api/src/main/java/org/eclipse/tractusx/edc/agreements/retirement/api/transform/JsonObjectFromAgreementRetirementTransformer.java b/edc-extensions/agreements/retirement-evaluation-api/src/main/java/org/eclipse/tractusx/edc/agreements/retirement/api/transform/JsonObjectFromAgreementRetirementTransformer.java new file mode 100644 index 000000000..d2d97363e --- /dev/null +++ b/edc-extensions/agreements/retirement-evaluation-api/src/main/java/org/eclipse/tractusx/edc/agreements/retirement/api/transform/JsonObjectFromAgreementRetirementTransformer.java @@ -0,0 +1,53 @@ +/******************************************************************************** + * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +package org.eclipse.tractusx.edc.agreements.retirement.api.transform; + +import jakarta.json.JsonBuilderFactory; +import jakarta.json.JsonObject; +import org.eclipse.edc.jsonld.spi.transformer.AbstractJsonLdTransformer; +import org.eclipse.edc.transform.spi.TransformerContext; +import org.eclipse.tractusx.edc.agreements.retirement.spi.types.AgreementsRetirementEntry; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import static org.eclipse.tractusx.edc.agreements.retirement.spi.types.AgreementsRetirementEntry.AR_ENTRY_AGREEMENT_ID; +import static org.eclipse.tractusx.edc.agreements.retirement.spi.types.AgreementsRetirementEntry.AR_ENTRY_REASON; +import static org.eclipse.tractusx.edc.agreements.retirement.spi.types.AgreementsRetirementEntry.AR_ENTRY_RETIREMENT_DATE; + +public class JsonObjectFromAgreementRetirementTransformer extends AbstractJsonLdTransformer { + + private final JsonBuilderFactory jsonFactory; + + public JsonObjectFromAgreementRetirementTransformer(JsonBuilderFactory jsonFactory) { + super(AgreementsRetirementEntry.class, JsonObject.class); + this.jsonFactory = jsonFactory; + } + + + @Override + public @Nullable JsonObject transform(@NotNull AgreementsRetirementEntry entry, @NotNull TransformerContext transformerContext) { + return jsonFactory.createObjectBuilder() + .add(AR_ENTRY_AGREEMENT_ID, entry.getAgreementId()) + .add(AR_ENTRY_REASON, entry.getReason()) + .add(AR_ENTRY_RETIREMENT_DATE, entry.getAgreementRetirementDate()) + .build(); + + } +} diff --git a/edc-extensions/agreements/retirement-evaluation-api/src/main/java/org/eclipse/tractusx/edc/agreements/retirement/api/transform/JsonObjectToAgreementsRetirementEntryTransformer.java b/edc-extensions/agreements/retirement-evaluation-api/src/main/java/org/eclipse/tractusx/edc/agreements/retirement/api/transform/JsonObjectToAgreementsRetirementEntryTransformer.java new file mode 100644 index 000000000..368f0f0da --- /dev/null +++ b/edc-extensions/agreements/retirement-evaluation-api/src/main/java/org/eclipse/tractusx/edc/agreements/retirement/api/transform/JsonObjectToAgreementsRetirementEntryTransformer.java @@ -0,0 +1,45 @@ +/******************************************************************************** + * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +package org.eclipse.tractusx.edc.agreements.retirement.api.transform; + +import jakarta.json.JsonObject; +import org.eclipse.edc.jsonld.spi.transformer.AbstractJsonLdTransformer; +import org.eclipse.edc.transform.spi.TransformerContext; +import org.eclipse.tractusx.edc.agreements.retirement.spi.types.AgreementsRetirementEntry; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import static org.eclipse.tractusx.edc.agreements.retirement.spi.types.AgreementsRetirementEntry.AR_ENTRY_AGREEMENT_ID; +import static org.eclipse.tractusx.edc.agreements.retirement.spi.types.AgreementsRetirementEntry.AR_ENTRY_REASON; + +public class JsonObjectToAgreementsRetirementEntryTransformer extends AbstractJsonLdTransformer { + + public JsonObjectToAgreementsRetirementEntryTransformer() { + super(JsonObject.class, AgreementsRetirementEntry.class); + } + + @Override + public @Nullable AgreementsRetirementEntry transform(@NotNull JsonObject jsonObject, @NotNull TransformerContext context) { + var entryBuilder = AgreementsRetirementEntry.Builder.newInstance(); + entryBuilder.withAgreementId(transformString(jsonObject.get(AR_ENTRY_AGREEMENT_ID), context)); + entryBuilder.withReason(transformString(jsonObject.get(AR_ENTRY_REASON), context)); + return entryBuilder.build(); + } +} diff --git a/edc-extensions/agreements/retirement-evaluation-api/src/main/java/org/eclipse/tractusx/edc/agreements/retirement/api/v3/AgreementsRetirementApiV3.java b/edc-extensions/agreements/retirement-evaluation-api/src/main/java/org/eclipse/tractusx/edc/agreements/retirement/api/v3/AgreementsRetirementApiV3.java new file mode 100644 index 000000000..fee7dacee --- /dev/null +++ b/edc-extensions/agreements/retirement-evaluation-api/src/main/java/org/eclipse/tractusx/edc/agreements/retirement/api/v3/AgreementsRetirementApiV3.java @@ -0,0 +1,89 @@ +/******************************************************************************** + * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +package org.eclipse.tractusx.edc.agreements.retirement.api.v3; + +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +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.web.spi.ApiErrorDetail; + +import static org.eclipse.edc.jsonld.spi.JsonLdKeywords.ID; + +@OpenAPIDefinition(info = @Info(description = "With this API clients can retire an active Contract Agreement. Clients can also list all retired agreements.", title = "Agreements Retirement API")) +@Tag(name = "Agreements Retirement") +public interface AgreementsRetirementApiV3 { + + + @Operation(description = "Get all retired contract agreements.", + responses = { + @ApiResponse(responseCode = "200", description = "A list of retired contract agreements"), + @ApiResponse(responseCode = "400", description = "Request body was malformed", + content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)))) + }) + JsonArray getAllRetiredV3(JsonObject querySpecJson); + + @Operation(description = "Removes a contract agreement from the retired list, reactivating it.", + responses = { + @ApiResponse(responseCode = "204", description = "The contract agreement is reactivated"), + @ApiResponse(responseCode = "404", description = "No entry for the given agreementId was found"), + @ApiResponse(responseCode = "400", description = "Request body was malformed", + content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)))) + }) + void reactivateRetiredV3(@Parameter(name = "agreementId", description = "The contract agreement id") String agreementId); + + @Operation(description = "Retires an active contract agreement.", + requestBody = @RequestBody(content = @Content(schema = @Schema(implementation = RetirementSchema.class))), + + responses = { + @ApiResponse(responseCode = "204", description = "The contract agreement was successfully retired"), + @ApiResponse(responseCode = "409", description = "The contract agreement is already retired"), + @ApiResponse(responseCode = "400", description = "Request body was malformed", + content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)))) + }) + void retireAgreementV3(JsonObject entry); + + + @Schema(name = "Retirement Example", example = RetirementSchema.EXAMPLE) + record RetirementSchema( + @Schema(name = ID) String id, + String reason + ) { + public static final String EXAMPLE = """ + { + "@context": { + "tx": "https://w3id.org/tractusx/v0.0.1/ns/", + "edc": "https://w3id.org/edc/v0.0.1/ns/" + }, + "edc:agreementId": "contract-agreement-id", + "tx:reason": "This contract agreement was retired since the physical counterpart is no longer valid." + } + """; + } + +} diff --git a/edc-extensions/agreements/retirement-evaluation-api/src/main/java/org/eclipse/tractusx/edc/agreements/retirement/api/v3/AgreementsRetirementApiV3Controller.java b/edc-extensions/agreements/retirement-evaluation-api/src/main/java/org/eclipse/tractusx/edc/agreements/retirement/api/v3/AgreementsRetirementApiV3Controller.java new file mode 100644 index 000000000..aed053972 --- /dev/null +++ b/edc-extensions/agreements/retirement-evaluation-api/src/main/java/org/eclipse/tractusx/edc/agreements/retirement/api/v3/AgreementsRetirementApiV3Controller.java @@ -0,0 +1,110 @@ +/******************************************************************************** + * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +package org.eclipse.tractusx.edc.agreements.retirement.api.v3; + +import io.swagger.v3.oas.annotations.parameters.RequestBody; +import jakarta.json.JsonArray; +import jakarta.json.JsonObject; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +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.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.ValidationFailureException; +import org.eclipse.tractusx.edc.agreements.retirement.spi.service.AgreementsRetirementService; +import org.eclipse.tractusx.edc.agreements.retirement.spi.types.AgreementsRetirementEntry; + +import static jakarta.json.stream.JsonCollectors.toJsonArray; +import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; +import static org.eclipse.edc.spi.query.QuerySpec.EDC_QUERY_SPEC_TYPE; +import static org.eclipse.edc.web.spi.exception.ServiceResultHandler.exceptionMapper; +import static org.eclipse.tractusx.edc.agreements.retirement.spi.types.AgreementsRetirementEntry.AR_ENTRY_TYPE; + +@Consumes(APPLICATION_JSON) +@Produces(APPLICATION_JSON) +@Path("/v3.1alpha/retireagreements") +public class AgreementsRetirementApiV3Controller implements AgreementsRetirementApiV3 { + + private final AgreementsRetirementService service; + private final TypeTransformerRegistry transformerRegistry; + private final JsonObjectValidatorRegistry validator; + private final Monitor monitor; + + + public AgreementsRetirementApiV3Controller(AgreementsRetirementService service, TypeTransformerRegistry transformerRegistry, JsonObjectValidatorRegistry validator, Monitor monitor) { + this.service = service; + this.transformerRegistry = transformerRegistry; + this.validator = validator; + this.monitor = monitor; + } + + @POST + @Path("/request") + @Override + public JsonArray getAllRetiredV3(@RequestBody JsonObject querySpecJson) { + + QuerySpec querySpec; + if (querySpecJson == null) { + querySpec = QuerySpec.max(); + } else { + validator.validate(EDC_QUERY_SPEC_TYPE, querySpecJson).orElseThrow(ValidationFailureException::new); + + querySpec = transformerRegistry.transform(querySpecJson, QuerySpec.class) + .orElseThrow(InvalidRequestException::new); + } + + return service.findAll(querySpec) + .orElseThrow(exceptionMapper(QuerySpec.class, null)).stream() + .map(it -> transformerRegistry.transform(it, JsonObject.class)) + .peek(r -> r.onFailure(f -> monitor.warning(f.getFailureDetail()))) + .filter(Result::succeeded) + .map(Result::getContent) + .collect(toJsonArray()); + } + + @DELETE + @Path("/{agreementId}") + @Override + public void reactivateRetiredV3(@PathParam("agreementId") String agreementId) { + service.reactivate(agreementId) + .orElseThrow(exceptionMapper(AgreementsRetirementEntry.class, agreementId)); + } + + @POST + @Override + public void retireAgreementV3(@RequestBody JsonObject entry) { + validator.validate(AR_ENTRY_TYPE, entry).orElseThrow(ValidationFailureException::new); + + var retirementEntry = transformerRegistry.transform(entry, AgreementsRetirementEntry.class) + .orElseThrow(InvalidRequestException::new); + + service.retireAgreement(retirementEntry) + .orElseThrow(exceptionMapper(AgreementsRetirementEntry.class, retirementEntry.getAgreementId())); + + + } +} diff --git a/edc-extensions/agreements/retirement-evaluation-api/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension b/edc-extensions/agreements/retirement-evaluation-api/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension new file mode 100644 index 000000000..124039628 --- /dev/null +++ b/edc-extensions/agreements/retirement-evaluation-api/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension @@ -0,0 +1,20 @@ +################################################################################# +# Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License, Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# SPDX-License-Identifier: Apache-2.0 +################################################################################# + +org.eclipse.tractusx.edc.agreements.retirement.api.AgreementsRetirementApiExtension diff --git a/edc-extensions/agreements/retirement-evaluation-api/src/test/java/org/eclipse/tractusx/edc/agreements/retirement/api/transform/JsonObjectFromAgreementRetirementTransformerTest.java b/edc-extensions/agreements/retirement-evaluation-api/src/test/java/org/eclipse/tractusx/edc/agreements/retirement/api/transform/JsonObjectFromAgreementRetirementTransformerTest.java new file mode 100644 index 000000000..799d286fb --- /dev/null +++ b/edc-extensions/agreements/retirement-evaluation-api/src/test/java/org/eclipse/tractusx/edc/agreements/retirement/api/transform/JsonObjectFromAgreementRetirementTransformerTest.java @@ -0,0 +1,64 @@ +/******************************************************************************** + * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +package org.eclipse.tractusx.edc.agreements.retirement.api.transform; + +import jakarta.json.Json; +import jakarta.json.JsonBuilderFactory; +import org.eclipse.edc.transform.spi.TransformerContext; +import org.eclipse.tractusx.edc.agreements.retirement.spi.types.AgreementsRetirementEntry; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.eclipse.tractusx.edc.agreements.retirement.spi.types.AgreementsRetirementEntry.AR_ENTRY_AGREEMENT_ID; +import static org.eclipse.tractusx.edc.agreements.retirement.spi.types.AgreementsRetirementEntry.AR_ENTRY_REASON; +import static org.eclipse.tractusx.edc.agreements.retirement.spi.types.AgreementsRetirementEntry.AR_ENTRY_RETIREMENT_DATE; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +class JsonObjectFromAgreementRetirementTransformerTest { + + private final JsonBuilderFactory factory = Json.createBuilderFactory(Map.of()); + + private final JsonObjectFromAgreementRetirementTransformer transformer = new JsonObjectFromAgreementRetirementTransformer(factory); + + @Test + void transform() { + + var context = mock(TransformerContext.class); + + var entry = AgreementsRetirementEntry.Builder.newInstance() + .withAgreementId("agreementId") + .withReason("long-reason") + .build(); + + var result = transformer.transform(entry, context); + + assertThat(result).isNotNull(); + assertThat(result.getString(AR_ENTRY_AGREEMENT_ID)).isEqualTo("agreementId"); + assertThat(result.getString(AR_ENTRY_REASON)).isEqualTo("long-reason"); + assertThat(result.getJsonNumber(AR_ENTRY_RETIREMENT_DATE)).isNotNull(); + verify(context, never()).reportProblem(anyString()); + } + +} \ No newline at end of file diff --git a/edc-extensions/agreements/retirement-evaluation-api/src/test/java/org/eclipse/tractusx/edc/agreements/retirement/api/transform/JsonObjectToAgreementsRetirementEntryTransformerTest.java b/edc-extensions/agreements/retirement-evaluation-api/src/test/java/org/eclipse/tractusx/edc/agreements/retirement/api/transform/JsonObjectToAgreementsRetirementEntryTransformerTest.java new file mode 100644 index 000000000..d4b972103 --- /dev/null +++ b/edc-extensions/agreements/retirement-evaluation-api/src/test/java/org/eclipse/tractusx/edc/agreements/retirement/api/transform/JsonObjectToAgreementsRetirementEntryTransformerTest.java @@ -0,0 +1,52 @@ +/******************************************************************************** + * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +package org.eclipse.tractusx.edc.agreements.retirement.api.transform; + +import jakarta.json.Json; +import org.eclipse.edc.transform.spi.TransformerContext; +import org.eclipse.tractusx.edc.agreements.retirement.spi.types.AgreementsRetirementEntry; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.mockito.Mockito.mock; + +class JsonObjectToAgreementsRetirementEntryTransformerTest { + + private final JsonObjectToAgreementsRetirementEntryTransformer transformer = new JsonObjectToAgreementsRetirementEntryTransformer(); + + @Test + void transform() { + + var context = mock(TransformerContext.class); + var jsonEntry = Json.createObjectBuilder() + .add(AgreementsRetirementEntry.AR_ENTRY_AGREEMENT_ID, "agreementId") + .add(AgreementsRetirementEntry.AR_ENTRY_REASON, "reason") + .build(); + + var result = transformer.transform(jsonEntry, context); + + assertThat(result).isNotNull(); + assertThat(result).isInstanceOf(AgreementsRetirementEntry.class); + assertThat(result.getAgreementId()).isEqualTo("agreementId"); + assertThat(result.getReason()).isEqualTo("reason"); + assertThat(result.getAgreementRetirementDate()).isNotNull(); + } + +} \ No newline at end of file diff --git a/edc-extensions/agreements/retirement-evaluation-api/src/test/java/org/eclipse/tractusx/edc/agreements/retirement/api/v3/AgreementsRetirementApiV3ControllerTest.java b/edc-extensions/agreements/retirement-evaluation-api/src/test/java/org/eclipse/tractusx/edc/agreements/retirement/api/v3/AgreementsRetirementApiV3ControllerTest.java new file mode 100644 index 000000000..bcc2b85c6 --- /dev/null +++ b/edc-extensions/agreements/retirement-evaluation-api/src/test/java/org/eclipse/tractusx/edc/agreements/retirement/api/v3/AgreementsRetirementApiV3ControllerTest.java @@ -0,0 +1,232 @@ +/******************************************************************************** + * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +package org.eclipse.tractusx.edc.agreements.retirement.api.v3; + +import io.restassured.http.ContentType; +import io.restassured.specification.RequestSpecification; +import jakarta.json.Json; +import jakarta.json.JsonObject; +import org.eclipse.edc.spi.monitor.Monitor; +import org.eclipse.edc.spi.query.Criterion; +import org.eclipse.edc.spi.query.QuerySpec; +import org.eclipse.edc.spi.result.Result; +import org.eclipse.edc.spi.result.ServiceResult; +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.eclipse.tractusx.edc.agreements.retirement.spi.service.AgreementsRetirementService; +import org.eclipse.tractusx.edc.agreements.retirement.spi.types.AgreementsRetirementEntry; +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.util.List; + +import static io.restassured.RestAssured.given; +import static org.eclipse.edc.jsonld.spi.JsonLdKeywords.TYPE; +import static org.eclipse.edc.spi.query.Criterion.CRITERION_OPERAND_LEFT; +import static org.eclipse.edc.spi.query.Criterion.CRITERION_OPERAND_RIGHT; +import static org.eclipse.edc.spi.query.Criterion.CRITERION_OPERATOR; +import static org.eclipse.edc.spi.query.QuerySpec.EDC_QUERY_SPEC_FILTER_EXPRESSION; +import static org.eclipse.edc.spi.query.QuerySpec.EDC_QUERY_SPEC_TYPE; +import static org.eclipse.tractusx.edc.agreements.retirement.spi.store.AgreementsRetirementStore.ALREADY_EXISTS_TEMPLATE; +import static org.eclipse.tractusx.edc.agreements.retirement.spi.store.AgreementsRetirementStore.NOT_FOUND_TEMPLATE; +import static org.eclipse.tractusx.edc.agreements.retirement.spi.types.AgreementsRetirementEntry.AR_ENTRY_AGREEMENT_ID; +import static org.eclipse.tractusx.edc.agreements.retirement.spi.types.AgreementsRetirementEntry.AR_ENTRY_REASON; +import static org.eclipse.tractusx.edc.agreements.retirement.spi.types.AgreementsRetirementEntry.AR_ENTRY_RETIREMENT_DATE; +import static org.eclipse.tractusx.edc.agreements.retirement.spi.types.AgreementsRetirementEntry.AR_ENTRY_TYPE; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +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.when; + +class AgreementsRetirementApiV3ControllerTest extends RestControllerTestBase { + + private final AgreementsRetirementService service = mock(); + private final TypeTransformerRegistry transformer = mock(); + private final JsonObjectValidatorRegistry validator = mock(); + private final Monitor monitor = mock(); + + @Test + void should_getAllWithoutQuerySpec() { + var agreement1 = "test-agreement-id"; + var agreement2 = "test-agreement-id2"; + var entry1 = createRetirementEntry(agreement1); + var entry2 = createRetirementEntry(agreement2); + + when(validator.validate(any(), any())) + .thenReturn(ValidationResult.success()); + when(transformer.transform(isA(JsonObject.class), eq(QuerySpec.class))) + .thenReturn(Result.success(QuerySpec.Builder.newInstance().build())); + when(transformer.transform(isA(AgreementsRetirementEntry.class), eq(JsonObject.class))) + .thenReturn(Result.success(createJsonRetirementEntry(agreement1))) + .thenReturn(Result.success(createJsonRetirementEntry(agreement2))); + when(service.findAll(any())).thenReturn(ServiceResult.success(List.of(entry1, entry2))); + + baseRequest() + .contentType(ContentType.JSON) + .body("{}") + .post("/request") + .then() + .log().ifError() + .statusCode(200) + .body(notNullValue()) + .body("size()", is(2)); + } + + @Test + void should_getAgreementUsingFilteredQuerySpec() { + var agreement1 = "test-agreement-id"; + var entry1 = createRetirementEntry(agreement1); + var querySpec = QuerySpec.Builder.newInstance().filter(createFilterCriteria(agreement1)).build(); + + when(validator.validate(any(), any())) + .thenReturn(ValidationResult.success()); + when(transformer.transform(isA(JsonObject.class), eq(QuerySpec.class))) + .thenReturn(Result.success(querySpec)); + when(transformer.transform(isA(AgreementsRetirementEntry.class), eq(JsonObject.class))) + .thenReturn(Result.success(createJsonRetirementEntry(agreement1))); + when(service.findAll(any())) + .thenReturn(ServiceResult.success(List.of(entry1))); + + baseRequest() + .contentType(ContentType.JSON) + .body(createFilterQuerySpecJson(agreement1)) + .post("/request") + .then() + .statusCode(200) + .body(notNullValue()) + .body("size()", is(1)); + } + + @Test + void should_removeAgreement() { + var agreementId = "test-agreement-id"; + when(service.reactivate(agreementId)).thenReturn(ServiceResult.success()); + baseRequest() + .delete("/{id}", agreementId) + .then() + .statusCode(204); + } + + @Test + void shouldNot_removeAgreementWhenNotExists() { + var agreementId = "test-agreement-id"; + when(service.reactivate(agreementId)) + .thenReturn(ServiceResult.notFound(String.format(NOT_FOUND_TEMPLATE, agreementId))); + + baseRequest() + .delete("/{id}", agreementId) + .then() + .log().ifError() + .statusCode(404); + } + + @Test + void should_saveAgreementRetirement() { + var agreementId = "test-agreement-id"; + when(validator.validate(any(), any())) + .thenReturn(ValidationResult.success()); + when(transformer.transform(isA(JsonObject.class), eq(AgreementsRetirementEntry.class))) + .thenReturn(Result.success(createRetirementEntry(agreementId))); + when(service.retireAgreement(isA(AgreementsRetirementEntry.class))) + .thenReturn(ServiceResult.success()); + + baseRequest() + .contentType(ContentType.JSON) + .body(createJsonRetirementEntry(agreementId)) + .post() + .then() + .log().ifError() + .statusCode(204); + } + + @Test + void shouldNot_saveAgreementRetirementWhenExists() { + var agreementId = "test-agreement-id"; + when(validator.validate(any(), any())) + .thenReturn(ValidationResult.success()); + when(transformer.transform(isA(JsonObject.class), eq(AgreementsRetirementEntry.class))) + .thenReturn(Result.success(createRetirementEntry(agreementId))); + when(service.retireAgreement(isA(AgreementsRetirementEntry.class))) + .thenReturn(ServiceResult.conflict(String.format(ALREADY_EXISTS_TEMPLATE, agreementId))); + + baseRequest() + .contentType(ContentType.JSON) + .body(createJsonRetirementEntry(agreementId)) + .post() + .then() + .log().ifError() + .statusCode(409); + } + + @Override + protected Object controller() { + return new AgreementsRetirementApiV3Controller(service, transformer, validator, monitor); + } + + private JsonObject createFilterQuerySpecJson(String agreement1) { + return Json.createObjectBuilder() + .add(TYPE, EDC_QUERY_SPEC_TYPE) + .add(EDC_QUERY_SPEC_FILTER_EXPRESSION, + Json.createObjectBuilder() + .add(CRITERION_OPERAND_LEFT, AR_ENTRY_AGREEMENT_ID) + .add(CRITERION_OPERATOR, "=") + .add(CRITERION_OPERAND_RIGHT, agreement1) + .build()) + .build(); + } + + private Criterion createFilterCriteria(String agreementId) { + return Criterion.Builder.newInstance() + .operandLeft(AR_ENTRY_AGREEMENT_ID) + .operator("=") + .operandRight(agreementId) + .build(); + } + + private AgreementsRetirementEntry createRetirementEntry(String agreementId) { + return AgreementsRetirementEntry.Builder.newInstance() + .withAgreementId(agreementId) + .withReason("long-reason") + .build(); + } + + private JsonObject createJsonRetirementEntry(String agreementId) { + return Json.createObjectBuilder() + .add(TYPE, AR_ENTRY_TYPE) + .add(AR_ENTRY_AGREEMENT_ID, agreementId) + .add(AR_ENTRY_REASON, "long-reason") + .add(AR_ENTRY_RETIREMENT_DATE, Instant.now().toString()) + .build(); + } + + private RequestSpecification baseRequest() { + return given() + .baseUri("http://localhost:" + port) + .basePath("/v3.1alpha/retireagreements") + .when(); + } + + +} \ No newline at end of file diff --git a/edc-extensions/agreements/retirement-evaluation-core/build.gradle.kts b/edc-extensions/agreements/retirement-evaluation-core/build.gradle.kts new file mode 100644 index 000000000..e7635586a --- /dev/null +++ b/edc-extensions/agreements/retirement-evaluation-core/build.gradle.kts @@ -0,0 +1,42 @@ +/******************************************************************************** + * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +plugins { + `java-library` + `maven-publish` + `java-test-fixtures` +} + +dependencies { + + implementation(libs.edc.runtime.metamodel) + implementation(libs.edc.spi.boot) + implementation(libs.edc.spi.policyengine) + implementation(libs.edc.spi.contract) + implementation(libs.edc.spi.transactionspi) + implementation(libs.edc.core.policy.monitor) + implementation(libs.edc.lib.store) + implementation(libs.edc.lib.query) + api(project(":edc-extensions:agreements:retirement-evaluation-spi")) + + testImplementation(libs.edc.junit) + testFixturesImplementation(libs.edc.junit) + testFixturesImplementation(libs.junit.jupiter.api) + testFixturesImplementation(libs.assertj) +} diff --git a/edc-extensions/agreements/retirement-evaluation-core/src/main/java/org/eclipse/tractusx/edc/agreements/retirement/AgreementRetirementValidator.java b/edc-extensions/agreements/retirement-evaluation-core/src/main/java/org/eclipse/tractusx/edc/agreements/retirement/AgreementRetirementValidator.java new file mode 100644 index 000000000..e904b4c61 --- /dev/null +++ b/edc-extensions/agreements/retirement-evaluation-core/src/main/java/org/eclipse/tractusx/edc/agreements/retirement/AgreementRetirementValidator.java @@ -0,0 +1,42 @@ +/******************************************************************************** + * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +package org.eclipse.tractusx.edc.agreements.retirement; + +import org.eclipse.edc.connector.controlplane.contract.spi.types.agreement.ContractAgreement; +import org.eclipse.edc.policy.engine.spi.PolicyContext; +import org.eclipse.edc.policy.engine.spi.PolicyValidatorFunction; +import org.eclipse.edc.policy.model.Policy; +import org.eclipse.tractusx.edc.agreements.retirement.spi.service.AgreementsRetirementService; + + +public record AgreementRetirementValidator(AgreementsRetirementService agreementsRetirementService) implements PolicyValidatorFunction { + + @Override + public Boolean apply(Policy policy, PolicyContext policyContext) { + var agreement = policyContext.getContextData(ContractAgreement.class); + if (agreement != null) { + if (agreementsRetirementService.isRetired(agreement.getId())) { + policyContext.reportProblem(String.format("Contract Agreement with ID=%s has been retired", agreement.getId())); + return false; + } + } + return true; + } +} diff --git a/edc-extensions/agreements/retirement-evaluation-core/src/main/java/org/eclipse/tractusx/edc/agreements/retirement/AgreementsRetirementPreValidatorRegisterExtension.java b/edc-extensions/agreements/retirement-evaluation-core/src/main/java/org/eclipse/tractusx/edc/agreements/retirement/AgreementsRetirementPreValidatorRegisterExtension.java new file mode 100644 index 000000000..1340a6f63 --- /dev/null +++ b/edc-extensions/agreements/retirement-evaluation-core/src/main/java/org/eclipse/tractusx/edc/agreements/retirement/AgreementsRetirementPreValidatorRegisterExtension.java @@ -0,0 +1,51 @@ +/******************************************************************************** + * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +package org.eclipse.tractusx.edc.agreements.retirement; + +import org.eclipse.edc.policy.engine.spi.PolicyEngine; +import org.eclipse.edc.runtime.metamodel.annotation.Extension; +import org.eclipse.edc.runtime.metamodel.annotation.Inject; +import org.eclipse.edc.spi.system.ServiceExtension; +import org.eclipse.edc.spi.system.ServiceExtensionContext; +import org.eclipse.tractusx.edc.agreements.retirement.spi.service.AgreementsRetirementService; + +import static org.eclipse.edc.connector.controlplane.contract.spi.validation.ContractValidationService.TRANSFER_SCOPE; +import static org.eclipse.edc.connector.policy.monitor.PolicyMonitorExtension.POLICY_MONITOR_SCOPE; +import static org.eclipse.tractusx.edc.agreements.retirement.AgreementsRetirementPreValidatorRegisterExtension.NAME; + + +@Extension(value = NAME) +public class AgreementsRetirementPreValidatorRegisterExtension implements ServiceExtension { + + public static final String NAME = "Agreements Retirement Policy Function Extension"; + + @Inject + private AgreementsRetirementService service; + + @Inject + private PolicyEngine policyEngine; + + @Override + public void initialize(ServiceExtensionContext context) { + var agreementRetirementValidator = new AgreementRetirementValidator(service); + policyEngine.registerPreValidator(TRANSFER_SCOPE, agreementRetirementValidator); + policyEngine.registerPreValidator(POLICY_MONITOR_SCOPE, agreementRetirementValidator); + } +} diff --git a/edc-extensions/agreements/retirement-evaluation-core/src/main/java/org/eclipse/tractusx/edc/agreements/retirement/defaults/DefaultAgreementRetirementStoreProviderExtension.java b/edc-extensions/agreements/retirement-evaluation-core/src/main/java/org/eclipse/tractusx/edc/agreements/retirement/defaults/DefaultAgreementRetirementStoreProviderExtension.java new file mode 100644 index 000000000..a2790a728 --- /dev/null +++ b/edc-extensions/agreements/retirement-evaluation-core/src/main/java/org/eclipse/tractusx/edc/agreements/retirement/defaults/DefaultAgreementRetirementStoreProviderExtension.java @@ -0,0 +1,45 @@ +/******************************************************************************** + * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +package org.eclipse.tractusx.edc.agreements.retirement.defaults; + +import org.eclipse.edc.runtime.metamodel.annotation.Extension; +import org.eclipse.edc.runtime.metamodel.annotation.Inject; +import org.eclipse.edc.runtime.metamodel.annotation.Provider; +import org.eclipse.edc.spi.query.CriterionOperatorRegistry; +import org.eclipse.edc.spi.system.ServiceExtension; +import org.eclipse.tractusx.edc.agreements.retirement.spi.store.AgreementsRetirementStore; + +import static org.eclipse.tractusx.edc.agreements.retirement.AgreementsRetirementPreValidatorRegisterExtension.NAME; + + +@Extension(NAME) +public class DefaultAgreementRetirementStoreProviderExtension implements ServiceExtension { + + private static final String NAME = "Default Agreements Store Provider Extension"; + + @Inject + CriterionOperatorRegistry criterionOperatorRegistry; + + @Provider(isDefault = true) + public AgreementsRetirementStore createInMemStore() { + return new InMemoryAgreementsRetirementStore(criterionOperatorRegistry); + } + +} diff --git a/edc-extensions/agreements/retirement-evaluation-core/src/main/java/org/eclipse/tractusx/edc/agreements/retirement/defaults/InMemoryAgreementsRetirementStore.java b/edc-extensions/agreements/retirement-evaluation-core/src/main/java/org/eclipse/tractusx/edc/agreements/retirement/defaults/InMemoryAgreementsRetirementStore.java new file mode 100644 index 000000000..f4b1df2c6 --- /dev/null +++ b/edc-extensions/agreements/retirement-evaluation-core/src/main/java/org/eclipse/tractusx/edc/agreements/retirement/defaults/InMemoryAgreementsRetirementStore.java @@ -0,0 +1,66 @@ +/******************************************************************************** + * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +package org.eclipse.tractusx.edc.agreements.retirement.defaults; + +import org.eclipse.edc.spi.query.CriterionOperatorRegistry; +import org.eclipse.edc.spi.query.QueryResolver; +import org.eclipse.edc.spi.query.QuerySpec; +import org.eclipse.edc.spi.result.StoreResult; +import org.eclipse.edc.store.ReflectionBasedQueryResolver; +import org.eclipse.tractusx.edc.agreements.retirement.spi.store.AgreementsRetirementStore; +import org.eclipse.tractusx.edc.agreements.retirement.spi.types.AgreementsRetirementEntry; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Stream; + +/** + * In Memory implementation of a {@link AgreementsRetirementStore}. + */ +public class InMemoryAgreementsRetirementStore implements AgreementsRetirementStore { + + private final QueryResolver queryResolver; + private final Map cache = new ConcurrentHashMap<>(); + + public InMemoryAgreementsRetirementStore(CriterionOperatorRegistry criterionOperatorRegistry) { + queryResolver = new ReflectionBasedQueryResolver<>(AgreementsRetirementEntry.class, criterionOperatorRegistry); + } + + @Override + public StoreResult save(AgreementsRetirementEntry entry) { + if (cache.containsKey(entry.getAgreementId())) { + return StoreResult.alreadyExists(ALREADY_EXISTS_TEMPLATE.formatted(entry.getAgreementId())); + } + cache.put(entry.getAgreementId(), entry); + return StoreResult.success(); + } + + @Override + public StoreResult delete(String contractAgreementId) { + return cache.remove(contractAgreementId) == null ? + StoreResult.notFound(NOT_FOUND_TEMPLATE.formatted(contractAgreementId)) : + StoreResult.success(); + } + + @Override + public Stream findRetiredAgreements(QuerySpec querySpec) { + return queryResolver.query(cache.values().stream(), querySpec); + } +} diff --git a/edc-extensions/agreements/retirement-evaluation-core/src/main/java/org/eclipse/tractusx/edc/agreements/retirement/service/AgreementRetirementServiceExtension.java b/edc-extensions/agreements/retirement-evaluation-core/src/main/java/org/eclipse/tractusx/edc/agreements/retirement/service/AgreementRetirementServiceExtension.java new file mode 100644 index 000000000..5d04aad90 --- /dev/null +++ b/edc-extensions/agreements/retirement-evaluation-core/src/main/java/org/eclipse/tractusx/edc/agreements/retirement/service/AgreementRetirementServiceExtension.java @@ -0,0 +1,52 @@ +/******************************************************************************** + * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +package org.eclipse.tractusx.edc.agreements.retirement.service; + +import org.eclipse.edc.runtime.metamodel.annotation.Extension; +import org.eclipse.edc.runtime.metamodel.annotation.Inject; +import org.eclipse.edc.runtime.metamodel.annotation.Provider; +import org.eclipse.edc.spi.system.ServiceExtension; +import org.eclipse.edc.transaction.spi.TransactionContext; +import org.eclipse.tractusx.edc.agreements.retirement.spi.service.AgreementsRetirementService; +import org.eclipse.tractusx.edc.agreements.retirement.spi.store.AgreementsRetirementStore; + +import static org.eclipse.tractusx.edc.agreements.retirement.AgreementsRetirementPreValidatorRegisterExtension.NAME; + +@Extension(NAME) +public class AgreementRetirementServiceExtension implements ServiceExtension { + + private static final String NAME = "Agreement Retirement Service Extension"; + + @Override + public String name() { + return NAME; + } + + @Inject + AgreementsRetirementStore store; + + @Inject + TransactionContext transactionContext; + + @Provider() + public AgreementsRetirementService createInMemAgreementRetirementService() { + return new AgreementsRetirementServiceImpl(store, transactionContext); + } +} diff --git a/edc-extensions/agreements/retirement-evaluation-core/src/main/java/org/eclipse/tractusx/edc/agreements/retirement/service/AgreementsRetirementServiceImpl.java b/edc-extensions/agreements/retirement-evaluation-core/src/main/java/org/eclipse/tractusx/edc/agreements/retirement/service/AgreementsRetirementServiceImpl.java new file mode 100644 index 000000000..6483a02bb --- /dev/null +++ b/edc-extensions/agreements/retirement-evaluation-core/src/main/java/org/eclipse/tractusx/edc/agreements/retirement/service/AgreementsRetirementServiceImpl.java @@ -0,0 +1,78 @@ +/******************************************************************************** + * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +package org.eclipse.tractusx.edc.agreements.retirement.service; + +import org.eclipse.edc.spi.query.Criterion; +import org.eclipse.edc.spi.query.QuerySpec; +import org.eclipse.edc.spi.result.ServiceResult; +import org.eclipse.edc.transaction.spi.TransactionContext; +import org.eclipse.tractusx.edc.agreements.retirement.spi.service.AgreementsRetirementService; +import org.eclipse.tractusx.edc.agreements.retirement.spi.store.AgreementsRetirementStore; +import org.eclipse.tractusx.edc.agreements.retirement.spi.types.AgreementsRetirementEntry; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * Implementation for the {@link AgreementsRetirementService}. + */ +public class AgreementsRetirementServiceImpl implements AgreementsRetirementService { + + private final AgreementsRetirementStore store; + private final TransactionContext transactionContext; + + public AgreementsRetirementServiceImpl(AgreementsRetirementStore store, TransactionContext transactionContext) { + this.store = store; + this.transactionContext = transactionContext; + } + + @Override + public boolean isRetired(String agreementId) { + return transactionContext.execute(() -> store.findRetiredAgreements(createFilterQueryByAgreementId(agreementId)) + .findAny() + .isPresent()); + } + + @Override + public ServiceResult> findAll(QuerySpec querySpec) { + return transactionContext.execute(() -> ServiceResult.success(store.findRetiredAgreements(querySpec).collect(Collectors.toList()))); + } + + @Override + public ServiceResult retireAgreement(AgreementsRetirementEntry entry) { + return transactionContext.execute(() -> ServiceResult.from(store.save(entry))); + } + + @Override + public ServiceResult reactivate(String contractAgreementId) { + return transactionContext.execute(() -> ServiceResult.from(store.delete(contractAgreementId))); + } + + private QuerySpec createFilterQueryByAgreementId(String agreementId) { + return QuerySpec.Builder.newInstance() + .filter( + Criterion.Builder.newInstance() + .operandLeft("agreementId") + .operator("=") + .operandRight(agreementId) + .build() + ).build(); + } +} diff --git a/edc-extensions/agreements/retirement-evaluation-core/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension b/edc-extensions/agreements/retirement-evaluation-core/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension new file mode 100644 index 000000000..0b11915e3 --- /dev/null +++ b/edc-extensions/agreements/retirement-evaluation-core/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension @@ -0,0 +1,22 @@ +################################################################################# +# Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License, Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# SPDX-License-Identifier: Apache-2.0 +################################################################################# + +org.eclipse.tractusx.edc.agreements.retirement.AgreementsRetirementPreValidatorRegisterExtension +org.eclipse.tractusx.edc.agreements.retirement.defaults.DefaultAgreementRetirementStoreProviderExtension +org.eclipse.tractusx.edc.agreements.retirement.service.AgreementRetirementServiceExtension diff --git a/edc-extensions/agreements/retirement-evaluation-core/src/test/java/org/eclipse/tractusx/edc/agreements/retirement/AgreementRetirementValidatorTest.java b/edc-extensions/agreements/retirement-evaluation-core/src/test/java/org/eclipse/tractusx/edc/agreements/retirement/AgreementRetirementValidatorTest.java new file mode 100644 index 000000000..31c528fe1 --- /dev/null +++ b/edc-extensions/agreements/retirement-evaluation-core/src/test/java/org/eclipse/tractusx/edc/agreements/retirement/AgreementRetirementValidatorTest.java @@ -0,0 +1,103 @@ +/******************************************************************************** + * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +package org.eclipse.tractusx.edc.agreements.retirement; + + +import org.eclipse.edc.connector.controlplane.contract.spi.types.agreement.ContractAgreement; +import org.eclipse.edc.policy.engine.spi.PolicyContext; +import org.eclipse.edc.policy.model.Policy; +import org.eclipse.tractusx.edc.agreements.retirement.spi.service.AgreementsRetirementService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class AgreementRetirementValidatorTest { + + private AgreementRetirementValidator validator; + private final AgreementsRetirementService service = mock(); + private final Policy policy = mock(); + private final PolicyContext context = mock(); + + @BeforeEach + public void setup() { + validator = new AgreementRetirementValidator(service); + } + + @Test + @DisplayName("Verify validator returns true if no agreement is found in policyContext") + public void verify_agreementExistsInPolicyContext() { + + when(context.getContextData(ContractAgreement.class)) + .thenReturn(null); + assertThat(validator.apply(policy, context)).isTrue(); + + } + + @Test + public void verify_returnFalseWhenRetired() { + var agreementId = "test-agreement"; + var agreement = buildAgreement(agreementId); + + when(context.getContextData(ContractAgreement.class)) + .thenReturn(agreement); + when(service.isRetired(agreementId)) + .thenReturn(true); + + var result = validator.apply(policy, context); + + assertThat(result).isFalse(); + verify(context, times(1)).reportProblem(anyString()); + } + + @Test + public void verify_returnFalseWhenNotRetired() { + var agreementId = "test-agreement"; + var agreement = buildAgreement(agreementId); + + when(context.getContextData(ContractAgreement.class)) + .thenReturn(agreement); + when(service.isRetired(agreementId)) + .thenReturn(false); + + var result = validator.apply(policy, context); + + assertThat(result).isTrue(); + verify(context, never()).reportProblem(anyString()); + } + + private ContractAgreement buildAgreement(String agreementId) { + return ContractAgreement.Builder.newInstance() + .id(agreementId) + .assetId("fake") + .consumerId("fake") + .providerId("fake") + .policy(mock(Policy.class)) + .build(); + } + +} \ No newline at end of file diff --git a/edc-extensions/agreements/retirement-evaluation-core/src/test/java/org/eclipse/tractusx/edc/agreements/retirement/AgreementsRetirementPreValidatorRegisterExtensionTest.java b/edc-extensions/agreements/retirement-evaluation-core/src/test/java/org/eclipse/tractusx/edc/agreements/retirement/AgreementsRetirementPreValidatorRegisterExtensionTest.java new file mode 100644 index 000000000..003a7d5e3 --- /dev/null +++ b/edc-extensions/agreements/retirement-evaluation-core/src/test/java/org/eclipse/tractusx/edc/agreements/retirement/AgreementsRetirementPreValidatorRegisterExtensionTest.java @@ -0,0 +1,57 @@ +/******************************************************************************** + * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +package org.eclipse.tractusx.edc.agreements.retirement; + + +import org.eclipse.edc.junit.extensions.DependencyInjectionExtension; +import org.eclipse.edc.policy.engine.spi.PolicyEngine; +import org.eclipse.edc.spi.system.ServiceExtensionContext; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import static org.eclipse.edc.connector.controlplane.contract.spi.validation.ContractValidationService.TRANSFER_SCOPE; +import static org.eclipse.edc.connector.policy.monitor.PolicyMonitorExtension.POLICY_MONITOR_SCOPE; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +@ExtendWith(DependencyInjectionExtension.class) +class AgreementsRetirementPreValidatorRegisterExtensionTest { + + private final PolicyEngine policyEngine = mock(); + + @BeforeEach + void setup(ServiceExtensionContext context) { + context.registerService(PolicyEngine.class, policyEngine); + } + + @Test + public void verify_functionIsRegisteredOnInitialization(ServiceExtensionContext context, AgreementsRetirementPreValidatorRegisterExtension extension) { + extension.initialize(context); + + verify(policyEngine, times(1)) + .registerPreValidator(eq(TRANSFER_SCOPE), any()); + verify(policyEngine, times(1)) + .registerPreValidator(eq(POLICY_MONITOR_SCOPE), any()); + } +} \ No newline at end of file diff --git a/edc-extensions/agreements/retirement-evaluation-core/src/test/java/org/eclipse/tractusx/edc/agreements/retirement/defaults/InMemoryAgreementsRetirementStoreTest.java b/edc-extensions/agreements/retirement-evaluation-core/src/test/java/org/eclipse/tractusx/edc/agreements/retirement/defaults/InMemoryAgreementsRetirementStoreTest.java new file mode 100644 index 000000000..7a6fcf5c1 --- /dev/null +++ b/edc-extensions/agreements/retirement-evaluation-core/src/test/java/org/eclipse/tractusx/edc/agreements/retirement/defaults/InMemoryAgreementsRetirementStoreTest.java @@ -0,0 +1,35 @@ +/******************************************************************************** + * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +package org.eclipse.tractusx.edc.agreements.retirement.defaults; + +import org.eclipse.edc.query.CriterionOperatorRegistryImpl; +import org.eclipse.tractusx.edc.agreements.retirement.spi.store.AgreementsRetirementStore; +import org.eclipse.tractusx.edc.agreements.retirement.store.AgreementsRetirementStoreTestBase; + + +public class InMemoryAgreementsRetirementStoreTest extends AgreementsRetirementStoreTestBase { + + private final InMemoryAgreementsRetirementStore store = new InMemoryAgreementsRetirementStore(CriterionOperatorRegistryImpl.ofDefaults()); + + @Override + protected AgreementsRetirementStore getStore() { + return store; + } +} \ No newline at end of file diff --git a/edc-extensions/agreements/retirement-evaluation-core/src/test/java/org/eclipse/tractusx/edc/agreements/retirement/service/AgreementsRetirementServiceImplTest.java b/edc-extensions/agreements/retirement-evaluation-core/src/test/java/org/eclipse/tractusx/edc/agreements/retirement/service/AgreementsRetirementServiceImplTest.java new file mode 100644 index 000000000..5cc87521f --- /dev/null +++ b/edc-extensions/agreements/retirement-evaluation-core/src/test/java/org/eclipse/tractusx/edc/agreements/retirement/service/AgreementsRetirementServiceImplTest.java @@ -0,0 +1,132 @@ +/******************************************************************************** + * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +package org.eclipse.tractusx.edc.agreements.retirement.service; + +import org.eclipse.edc.spi.query.Criterion; +import org.eclipse.edc.spi.query.QuerySpec; +import org.eclipse.edc.spi.result.StoreResult; +import org.eclipse.edc.transaction.spi.NoopTransactionContext; +import org.eclipse.edc.transaction.spi.TransactionContext; +import org.eclipse.tractusx.edc.agreements.retirement.spi.service.AgreementsRetirementService; +import org.eclipse.tractusx.edc.agreements.retirement.spi.store.AgreementsRetirementStore; +import org.eclipse.tractusx.edc.agreements.retirement.spi.types.AgreementsRetirementEntry; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.edc.junit.assertions.AbstractResultAssert.assertThat; +import static org.eclipse.edc.spi.result.ServiceFailure.Reason.CONFLICT; +import static org.eclipse.edc.spi.result.ServiceFailure.Reason.NOT_FOUND; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class AgreementsRetirementServiceImplTest { + + private AgreementsRetirementService service; + private final AgreementsRetirementStore store = mock(); + private final TransactionContext transactionContext = new NoopTransactionContext(); + + @BeforeEach + void setUp() { + service = new AgreementsRetirementServiceImpl(store, transactionContext); + } + + @Test + @DisplayName("Returns true if agreement is retired") + void returnsTrue_ifAgreementIsRetired() { + + var agreementId = "test-agreement-id"; + + var entry = AgreementsRetirementEntry.Builder.newInstance() + .withAgreementId(agreementId) + .withReason("mock-reason") + .build(); + + when(store.findRetiredAgreements(createFilterQueryByAgreementId(agreementId))) + .thenReturn(Stream.of(entry)); + + var result = service.isRetired(agreementId); + + assertThat(result).isTrue(); + } + + @Test + @DisplayName("Returns false if agreement is not retired") + void returnsFalse_ifAgreementIsNotRetired() { + + when(store.findRetiredAgreements(any(QuerySpec.class))) + .thenReturn(Stream.of()); + + var result = service.isRetired(anyString()); + + assertThat(result).isFalse(); + } + + @Test + @DisplayName("Verify find all response") + void verify_findAll() { + var query = QuerySpec.Builder.newInstance().build(); + when(store.findRetiredAgreements(query)) + .thenReturn(Stream.of()); + + var result = service.findAll(query); + assertThat(result).isSucceeded(); + assertThat(result.getContent()).hasSize(0); + } + + @Test + @DisplayName("Verify reactivate response on failure") + void verify_reactivateResponseOnFailure() { + when(store.delete(any())) + .thenReturn(StoreResult.notFound("test")); + + var result = service.reactivate(anyString()); + assertThat(result).isFailed(); + assertThat(result.reason()).isEqualTo(NOT_FOUND); + } + + @Test + @DisplayName("Verify retire response on failure") + void verify_retireResponseOnFailure() { + when(store.save(any())) + .thenReturn(StoreResult.alreadyExists("test")); + + var result = service.retireAgreement(any()); + assertThat(result).isFailed(); + assertThat(result.reason()).isEqualTo(CONFLICT); + } + + private QuerySpec createFilterQueryByAgreementId(String agreementId) { + return QuerySpec.Builder.newInstance() + .filter( + Criterion.Builder.newInstance() + .operandLeft("agreementId") + .operator("=") + .operandRight(agreementId) + .build() + ).build(); + } + +} \ No newline at end of file diff --git a/edc-extensions/agreements/retirement-evaluation-core/src/testFixtures/java/org/eclipse/tractusx/edc/agreements/retirement/store/AgreementsRetirementStoreTestBase.java b/edc-extensions/agreements/retirement-evaluation-core/src/testFixtures/java/org/eclipse/tractusx/edc/agreements/retirement/store/AgreementsRetirementStoreTestBase.java new file mode 100644 index 000000000..075adb2db --- /dev/null +++ b/edc-extensions/agreements/retirement-evaluation-core/src/testFixtures/java/org/eclipse/tractusx/edc/agreements/retirement/store/AgreementsRetirementStoreTestBase.java @@ -0,0 +1,120 @@ +/******************************************************************************** + * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +package org.eclipse.tractusx.edc.agreements.retirement.store; + +import org.eclipse.edc.spi.query.Criterion; +import org.eclipse.edc.spi.query.QuerySpec; +import org.eclipse.tractusx.edc.agreements.retirement.spi.store.AgreementsRetirementStore; +import org.eclipse.tractusx.edc.agreements.retirement.spi.types.AgreementsRetirementEntry; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.edc.junit.assertions.AbstractResultAssert.assertThat; +import static org.eclipse.tractusx.edc.agreements.retirement.spi.store.AgreementsRetirementStore.ALREADY_EXISTS_TEMPLATE; +import static org.eclipse.tractusx.edc.agreements.retirement.spi.store.AgreementsRetirementStore.NOT_FOUND_TEMPLATE; + +public abstract class AgreementsRetirementStoreTestBase { + + private AgreementsRetirementStore store; + + @BeforeEach + void setup() { + store = getStore(); + } + + @Test + void findRetiredAgreement() { + var agreementId = "test-agreement-id"; + var entry = createRetiredAgreementEntry(agreementId, "mock-reason"); + store.save(entry); + + var query = createFilterQueryByAgreementId(agreementId); + var retiredAgreements = store.findRetiredAgreements(query).collect(Collectors.toList()); + assertThat(retiredAgreements) + .isNotNull() + .hasSize(1) + .first() + .extracting(AgreementsRetirementEntry::getAgreementId) + .isEqualTo(agreementId); + } + + @Test + void findRetiredAgreement_notExists() { + var agreementId = "test-agreement-not-exists"; + var query = createFilterQueryByAgreementId(agreementId); + var result = store.findRetiredAgreements(query).collect(Collectors.toList()); + assertThat(result).isEmpty(); + } + + @Test + void save_whenExists() { + var agreementId = "test-agreement-id"; + var entry = createRetiredAgreementEntry(agreementId, "mock-reason"); + store.save(entry); + var result = store.save(entry); + assertThat(result).isFailed() + .detail().isEqualTo(ALREADY_EXISTS_TEMPLATE.formatted(agreementId)); + } + + @Test + void delete() { + var agreementId = "test-agreement-id"; + var entry = createRetiredAgreementEntry(agreementId, "mock-reason"); + store.save(entry); + var delete = store.delete(agreementId); + + assertThat(delete).isSucceeded(); + + } + + @Test + void delete_notExist() { + var agreementId = "test-agreement-id"; + var delete = store.delete(agreementId); + + assertThat(delete).isFailed() + .detail().isEqualTo(NOT_FOUND_TEMPLATE.formatted(agreementId)); + + } + + private AgreementsRetirementEntry createRetiredAgreementEntry(String agreementId, String reason) { + return AgreementsRetirementEntry.Builder.newInstance() + .withAgreementId(agreementId) + .withReason(reason) + .build(); + } + + private QuerySpec createFilterQueryByAgreementId(String agreementId) { + return QuerySpec.Builder.newInstance() + .filter( + Criterion.Builder.newInstance() + .operandLeft("agreementId") + .operator("=") + .operandRight(agreementId) + .build() + ).build(); + } + + protected abstract AgreementsRetirementStore getStore(); + +} diff --git a/edc-extensions/agreements/retirement-evaluation-spi/build.gradle.kts b/edc-extensions/agreements/retirement-evaluation-spi/build.gradle.kts new file mode 100644 index 000000000..68565ab80 --- /dev/null +++ b/edc-extensions/agreements/retirement-evaluation-spi/build.gradle.kts @@ -0,0 +1,31 @@ +/******************************************************************************** + * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +plugins { + `java-library` + `maven-publish` +} + +dependencies { + api(libs.edc.spi.policyengine) + implementation(libs.edc.spi.core) + implementation(project(":spi:core-spi")) + + testImplementation(libs.edc.junit) +} diff --git a/edc-extensions/agreements/retirement-evaluation-spi/src/main/java/org/eclipse/tractusx/edc/agreements/retirement/spi/service/AgreementsRetirementService.java b/edc-extensions/agreements/retirement-evaluation-spi/src/main/java/org/eclipse/tractusx/edc/agreements/retirement/spi/service/AgreementsRetirementService.java new file mode 100644 index 000000000..1fe007a51 --- /dev/null +++ b/edc-extensions/agreements/retirement-evaluation-spi/src/main/java/org/eclipse/tractusx/edc/agreements/retirement/spi/service/AgreementsRetirementService.java @@ -0,0 +1,64 @@ +/******************************************************************************** + * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +package org.eclipse.tractusx.edc.agreements.retirement.spi.service; + +import org.eclipse.edc.spi.query.QuerySpec; +import org.eclipse.edc.spi.result.ServiceResult; +import org.eclipse.tractusx.edc.agreements.retirement.spi.types.AgreementsRetirementEntry; + +import java.util.List; + +/** + * Service interface that offers the necessary functionality for the Contract Agreement Retirement feature. + */ +public interface AgreementsRetirementService { + + /** + * Given a contract agreement id, verifies if a corresponding {@link AgreementsRetirementEntry} exists in the {@link org.eclipse.tractusx.edc.agreements.retirement.spi.store.AgreementsRetirementStore}. + * + * @param agreementId the contract agreement id to verify + * @return true if it exists, false otherwise. + */ + boolean isRetired(String agreementId); + + /** + * Returns a list of {@link AgreementsRetirementEntry} entries matching a valid {@link QuerySpec} + * + * @param querySpec a valid {@link QuerySpec} + * @return a list of {@link AgreementsRetirementEntry} + */ + ServiceResult> findAll(QuerySpec querySpec); + + /** + * Saves an {@link AgreementsRetirementEntry} in the {@link org.eclipse.tractusx.edc.agreements.retirement.spi.store.AgreementsRetirementStore}. + * + * @param entry a valid {@link AgreementsRetirementEntry} + * @return ServiceResult successs, or a conflict failure if it already exists. + */ + ServiceResult retireAgreement(AgreementsRetirementEntry entry); + + /** + * Given a contract agreement id, removes its matching {@link AgreementsRetirementEntry} from the {@link org.eclipse.tractusx.edc.agreements.retirement.spi.store.AgreementsRetirementStore}. + * + * @param contractAgreementId the contract agreement id of the AgreementRetirementEntry to delete + * @return StoreResult success, not found failure if entry not found. + */ + ServiceResult reactivate(String contractAgreementId); +} diff --git a/edc-extensions/agreements/retirement-evaluation-spi/src/main/java/org/eclipse/tractusx/edc/agreements/retirement/spi/store/AgreementsRetirementStore.java b/edc-extensions/agreements/retirement-evaluation-spi/src/main/java/org/eclipse/tractusx/edc/agreements/retirement/spi/store/AgreementsRetirementStore.java new file mode 100644 index 000000000..51245d1c6 --- /dev/null +++ b/edc-extensions/agreements/retirement-evaluation-spi/src/main/java/org/eclipse/tractusx/edc/agreements/retirement/spi/store/AgreementsRetirementStore.java @@ -0,0 +1,61 @@ +/******************************************************************************** + * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +package org.eclipse.tractusx.edc.agreements.retirement.spi.store; + +import org.eclipse.edc.runtime.metamodel.annotation.ExtensionPoint; +import org.eclipse.edc.spi.query.QuerySpec; +import org.eclipse.edc.spi.result.StoreResult; +import org.eclipse.tractusx.edc.agreements.retirement.spi.types.AgreementsRetirementEntry; + +import java.util.stream.Stream; + +/** + * Interface for managing the storage point of {@link AgreementsRetirementEntry}. + */ +@ExtensionPoint +public interface AgreementsRetirementStore { + String NOT_FOUND_TEMPLATE = "Contract Agreement with %s was not found on retirement list."; + String ALREADY_EXISTS_TEMPLATE = "Contract Agreement %s is already retired."; + + /** + * Saves an AgreementsRetirementEntry in the store. + * + * @param entry {@link AgreementsRetirementEntry} + * @return StoreResult success, already exists failure if already exists + */ + StoreResult save(AgreementsRetirementEntry entry); + + /** + * Deletes an AgreementsRetirementEntry from the store, given a contract agreement id. + * + * @param contractAgreementId the contract agreement id of the AgreementRetirementEntry to delete + * @return StoreResult success, not found failure if entry not found. + */ + StoreResult delete(String contractAgreementId); + + /** + * Returns a list of AgreementRetirementEntry matching a query spec. + * + * @param querySpec a valid {@link QuerySpec} + * @return a list of AgreementRetirementEntry entries. + */ + Stream findRetiredAgreements(QuerySpec querySpec); + +} diff --git a/edc-extensions/agreements/retirement-evaluation-spi/src/main/java/org/eclipse/tractusx/edc/agreements/retirement/spi/types/AgreementsRetirementEntry.java b/edc-extensions/agreements/retirement-evaluation-spi/src/main/java/org/eclipse/tractusx/edc/agreements/retirement/spi/types/AgreementsRetirementEntry.java new file mode 100644 index 000000000..d88c1a428 --- /dev/null +++ b/edc-extensions/agreements/retirement-evaluation-spi/src/main/java/org/eclipse/tractusx/edc/agreements/retirement/spi/types/AgreementsRetirementEntry.java @@ -0,0 +1,102 @@ +/******************************************************************************** + * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +package org.eclipse.tractusx.edc.agreements.retirement.spi.types; + +import com.fasterxml.jackson.annotation.JsonCreator; +import org.eclipse.edc.spi.entity.Entity; + +import static java.util.Objects.requireNonNull; +import static org.eclipse.edc.spi.constants.CoreConstants.EDC_NAMESPACE; +import static org.eclipse.tractusx.edc.edr.spi.CoreConstants.TX_NAMESPACE; + +/** + * Representation of a Contract Agreement Retirement entry, to be stored in the {@link org.eclipse.tractusx.edc.agreements.retirement.spi.store.AgreementsRetirementStore}. + */ +public class AgreementsRetirementEntry extends Entity { + + public static final String AR_ENTRY_TYPE = EDC_NAMESPACE + "AgreementsRetirementEntry"; + + public static final String AR_ENTRY_AGREEMENT_ID = EDC_NAMESPACE + "agreementId"; + public static final String AR_ENTRY_REASON = TX_NAMESPACE + "reason"; + public static final String AR_ENTRY_RETIREMENT_DATE = TX_NAMESPACE + "agreementRetirementDate"; + + private String agreementId; + private String reason; + private long agreementRetirementDate = 0L; + + public AgreementsRetirementEntry() {} + + public String getAgreementId() { + return agreementId; + } + + public String getReason() { + return reason; + } + + public long getAgreementRetirementDate() { + return agreementRetirementDate; + } + + public static class Builder extends Entity.Builder { + + private Builder() { + super(new AgreementsRetirementEntry()); + } + + @JsonCreator + public static Builder newInstance() { + return new Builder(); + } + + public Builder withAgreementId(String agreementId) { + this.entity.agreementId = agreementId; + return this; + } + + public Builder withReason(String reason) { + this.entity.reason = reason; + return this; + } + + public Builder withAgreementRetirementDate(long agreementRetirementDate) { + this.entity.agreementRetirementDate = agreementRetirementDate; + return this; + } + + @Override + public Builder self() { + return this; + } + + @Override + public AgreementsRetirementEntry build() { + super.build(); + requireNonNull(entity.agreementId, AR_ENTRY_AGREEMENT_ID); + requireNonNull(entity.reason, AR_ENTRY_REASON); + + if (entity.agreementRetirementDate == 0L) { + entity.agreementRetirementDate = this.entity.clock.instant().getEpochSecond(); + } + + return entity; + } + } +} diff --git a/edc-tests/edc-controlplane/agreement-retirement-tests/build.gradle.kts b/edc-tests/edc-controlplane/agreement-retirement-tests/build.gradle.kts new file mode 100644 index 000000000..a577afd5b --- /dev/null +++ b/edc-tests/edc-controlplane/agreement-retirement-tests/build.gradle.kts @@ -0,0 +1,39 @@ +/******************************************************************************** + * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +plugins { + `java-library` + `java-test-fixtures` +} + +dependencies { + testImplementation(testFixtures(project(":edc-tests:edc-controlplane:fixtures"))) + + testImplementation(libs.netty.mockserver) + testImplementation(libs.edc.junit) + testImplementation(libs.restAssured) + testImplementation(libs.awaitility) + testRuntimeOnly(libs.edc.transaction.local) + testImplementation(project(":edc-extensions:agreements:retirement-evaluation-spi")) +} + +// do not publish +edcBuild { + publish.set(false) +} diff --git a/edc-tests/edc-controlplane/agreement-retirement-tests/src/test/java/org/eclipse/tractusx/edc/tests/agreement/retirement/RetireAgreementTest.java b/edc-tests/edc-controlplane/agreement-retirement-tests/src/test/java/org/eclipse/tractusx/edc/tests/agreement/retirement/RetireAgreementTest.java new file mode 100644 index 000000000..7d4ae42b6 --- /dev/null +++ b/edc-tests/edc-controlplane/agreement-retirement-tests/src/test/java/org/eclipse/tractusx/edc/tests/agreement/retirement/RetireAgreementTest.java @@ -0,0 +1,168 @@ +/******************************************************************************** + * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +package org.eclipse.tractusx.edc.tests.agreement.retirement; + +import jakarta.json.Json; +import org.eclipse.edc.connector.controlplane.transfer.spi.types.TransferProcessStates; +import org.eclipse.edc.junit.annotations.EndToEndTest; +import org.eclipse.edc.junit.annotations.PostgresqlIntegrationTest; +import org.eclipse.edc.junit.extensions.RuntimeExtension; +import org.eclipse.edc.policy.model.Operator; +import org.eclipse.tractusx.edc.tests.helpers.PolicyHelperFunctions; +import org.eclipse.tractusx.edc.tests.participant.TransferParticipant; +import org.eclipse.tractusx.edc.tests.runtimes.PostgresExtension; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mockserver.integration.ClientAndServer; + +import java.io.IOException; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; +import static org.eclipse.edc.util.io.Ports.getFreePort; +import static org.eclipse.tractusx.edc.tests.TestRuntimeConfiguration.CONSUMER_BPN; +import static org.eclipse.tractusx.edc.tests.TestRuntimeConfiguration.CONSUMER_NAME; +import static org.eclipse.tractusx.edc.tests.TestRuntimeConfiguration.PROVIDER_BPN; +import static org.eclipse.tractusx.edc.tests.TestRuntimeConfiguration.PROVIDER_NAME; +import static org.eclipse.tractusx.edc.tests.participant.TractusxParticipantBase.ASYNC_POLL_INTERVAL; +import static org.eclipse.tractusx.edc.tests.participant.TractusxParticipantBase.ASYNC_TIMEOUT; +import static org.eclipse.tractusx.edc.tests.runtimes.Runtimes.memoryRuntime; +import static org.eclipse.tractusx.edc.tests.runtimes.Runtimes.pgRuntime; + +public class RetireAgreementTest { + + protected static final TransferParticipant CONSUMER = TransferParticipant.Builder.newInstance() + .name(CONSUMER_NAME) + .id(CONSUMER_BPN) + .build(); + + protected static final TransferParticipant PROVIDER = TransferParticipant.Builder.newInstance() + .name(PROVIDER_NAME) + .id(PROVIDER_BPN) + .build(); + + + abstract static class Tests { + + ClientAndServer server; + + @BeforeEach + void setup() { + server = ClientAndServer.startClientAndServer("localhost", getFreePort()); + } + + @Test + @DisplayName("Verify all existing TPs related to an agreement are terminated upon its retirement") + void retireAgreement_shouldCloseTransferProcesses() { + + var assetId = "api-asset-1"; + + Map dataAddress = Map.of( + "name", "transfer-test", + "baseUrl", "https://mock-url.com", + "type", "HttpData", + "contentType", "application/json" + ); + + PROVIDER.createAsset(assetId, Map.of(), dataAddress); + + PROVIDER.storeBusinessPartner(CONSUMER.getBpn(), "test-group1"); + var accessPolicy = PROVIDER.createPolicyDefinition(PolicyHelperFunctions.bpnGroupPolicy(Operator.EQ, "test-group1")); + var policy = PolicyHelperFunctions.frameworkPolicy(Map.of()); + var contractPolicy = PROVIDER.createPolicyDefinition(policy); + PROVIDER.createContractDefinition(assetId, "def-1", accessPolicy, contractPolicy); + + var edrsApi = CONSUMER.edrs(); + + edrsApi.negotiateEdr(PROVIDER, assetId, Json.createArrayBuilder().build()); + + await().pollInterval(ASYNC_POLL_INTERVAL) + .atMost(ASYNC_TIMEOUT) + .untilAsserted(() -> { + var edrCaches = CONSUMER.edrs().getEdrEntriesByAssetId(assetId); + assertThat(edrCaches).hasSize(1); + }); + + var edrCaches = CONSUMER.edrs().getEdrEntriesByAssetId(assetId); + + var agreementId = edrCaches.get(0).asJsonObject().getString("agreementId"); + + var transferProcessId = edrCaches.get(0).asJsonObject().getString("transferProcessId"); + + PROVIDER.retireProviderAgreement(agreementId); + + // verify existing TP on consumer retires + + CONSUMER.waitForTransferProcess(transferProcessId, TransferProcessStates.TERMINATED); + + // verify no new TP can start for same contract agreement + + var privateProperties = Json.createObjectBuilder().build(); + var dataDestination = Json.createObjectBuilder().add("type", "HttpData").build(); + + var failedTransferId = CONSUMER.initiateTransfer(PROVIDER, agreementId, privateProperties, dataDestination, "HttpData-PULL"); + + CONSUMER.waitForTransferProcess(failedTransferId, TransferProcessStates.TERMINATED); + + + } + + @AfterEach + void teardown() throws IOException { + server.stop(); + } + } + + @Nested + @EndToEndTest + class InMemory extends Tests { + + @RegisterExtension + protected static final RuntimeExtension CONSUMER_RUNTIME = memoryRuntime(CONSUMER.getName(), CONSUMER.getBpn(), CONSUMER.getConfiguration()); + + @RegisterExtension + protected static final RuntimeExtension PROVIDER_RUNTIME = memoryRuntime(PROVIDER.getName(), PROVIDER.getBpn(), PROVIDER.getConfiguration()); + + } + + @Nested + @PostgresqlIntegrationTest + @Disabled + class Postgres extends Tests { + + @RegisterExtension + @Order(0) + private static final PostgresExtension POSTGRES = new PostgresExtension(CONSUMER.getName(), PROVIDER.getName()); + + @RegisterExtension + protected static final RuntimeExtension CONSUMER_RUNTIME = pgRuntime(CONSUMER, POSTGRES); + + @RegisterExtension + protected static final RuntimeExtension PROVIDER_RUNTIME = pgRuntime(PROVIDER, POSTGRES); + + } +} diff --git a/edc-tests/edc-controlplane/fixtures/build.gradle.kts b/edc-tests/edc-controlplane/fixtures/build.gradle.kts index b40ec289a..a8ce8a66d 100644 --- a/edc-tests/edc-controlplane/fixtures/build.gradle.kts +++ b/edc-tests/edc-controlplane/fixtures/build.gradle.kts @@ -52,6 +52,7 @@ dependencies { testFixturesImplementation(libs.assertj) testFixturesImplementation(libs.junit.jupiter.api) testFixturesImplementation(project(":edc-extensions:bpn-validation:bpn-validation-spi")) + testFixturesImplementation(project(":edc-extensions:agreements:retirement-evaluation-spi")) testCompileOnly(project(":edc-tests:runtime:runtime-memory")) testCompileOnly(project(":edc-tests:runtime:runtime-postgresql")) diff --git a/edc-tests/edc-controlplane/fixtures/src/testFixtures/java/org/eclipse/tractusx/edc/tests/participant/TractusxParticipantBase.java b/edc-tests/edc-controlplane/fixtures/src/testFixtures/java/org/eclipse/tractusx/edc/tests/participant/TractusxParticipantBase.java index 9abaaf095..1fadd7e34 100644 --- a/edc-tests/edc-controlplane/fixtures/src/testFixtures/java/org/eclipse/tractusx/edc/tests/participant/TractusxParticipantBase.java +++ b/edc-tests/edc-controlplane/fixtures/src/testFixtures/java/org/eclipse/tractusx/edc/tests/participant/TractusxParticipantBase.java @@ -47,8 +47,12 @@ import static org.eclipse.edc.jsonld.spi.JsonLdKeywords.VOCAB; import static org.eclipse.edc.spi.constants.CoreConstants.EDC_NAMESPACE; import static org.eclipse.edc.util.io.Ports.getFreePort; +import static org.eclipse.tractusx.edc.agreements.retirement.spi.types.AgreementsRetirementEntry.AR_ENTRY_AGREEMENT_ID; +import static org.eclipse.tractusx.edc.agreements.retirement.spi.types.AgreementsRetirementEntry.AR_ENTRY_REASON; +import static org.eclipse.tractusx.edc.agreements.retirement.spi.types.AgreementsRetirementEntry.AR_ENTRY_TYPE; import static org.eclipse.tractusx.edc.edr.spi.CoreConstants.TX_NAMESPACE; + /** * Base class for doing E2E tests with participants. */ @@ -164,6 +168,21 @@ public void storeBusinessPartner(String bpn, String... groups) { .statusCode(204); } + public void retireProviderAgreement(String agreementId) { + var body = createObjectBuilder() + .add(TYPE, AR_ENTRY_TYPE) + .add(AR_ENTRY_AGREEMENT_ID, agreementId) + .add(AR_ENTRY_REASON, "long-reason") + .build(); + managementEndpoint.baseRequest() + .contentType(JSON) + .body(body) + .when() + .post("/v3.1alpha/retireagreements") + .then() + .statusCode(204); + } + /** * waits for the configured timeout until the transfer process reaches the provided state * diff --git a/settings.gradle.kts b/settings.gradle.kts index 2833708c6..a3fe63851 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -52,6 +52,10 @@ include(":edc-extensions:dcp:tx-dcp") include(":edc-extensions:dcp:tx-dcp-sts-dim") include(":edc-extensions:data-flow-properties-provider") +include(":edc-extensions:agreements") +include(":edc-extensions:agreements:retirement-evaluation-core") +include(":edc-extensions:agreements:retirement-evaluation-api") +include(":edc-extensions:agreements:retirement-evaluation-spi") // extensions - data plane include(":edc-extensions:dataplane:dataplane-proxy:edc-dataplane-proxy-consumer-api") @@ -65,6 +69,7 @@ include(":edc-tests:edc-controlplane:catalog-tests") include(":edc-tests:edc-controlplane:transfer-tests") include(":edc-tests:edc-controlplane:iatp-tests") include(":edc-tests:edc-controlplane:policy-tests") +include(":edc-tests:edc-controlplane:agreement-retirement-tests") include(":edc-tests:edc-controlplane:fixtures") include(":edc-tests:runtime:extensions") include(":edc-tests:runtime:runtime-memory")