diff --git a/.github/workflows/publish-docker.yaml b/.github/workflows/publish-docker.yaml index 89e98b1ea..bf5ac3b01 100644 --- a/.github/workflows/publish-docker.yaml +++ b/.github/workflows/publish-docker.yaml @@ -49,7 +49,8 @@ jobs: { dir: edc-controlplane, img: edc-controlplane-postgresql-hashicorp-vault }, { dir: edc-controlplane, img: edc-controlplane-postgresql-azure-vault }, { dir: edc-dataplane, img: edc-dataplane-azure-vault }, - { dir: edc-dataplane, img: edc-dataplane-hashicorp-vault } ] + { dir: edc-dataplane, img: edc-dataplane-hashicorp-vault }, + { dir: edc-tests/runtime, img: mock-connector }] permissions: contents: write packages: write diff --git a/.github/workflows/publish-new-release.yml b/.github/workflows/publish-new-release.yml index 1c50db5ec..f87be958e 100644 --- a/.github/workflows/publish-new-release.yml +++ b/.github/workflows/publish-new-release.yml @@ -104,8 +104,8 @@ jobs: { dir: edc-controlplane, img: edc-controlplane-postgresql-hashicorp-vault }, { dir: edc-controlplane, img: edc-controlplane-postgresql-azure-vault }, { dir: edc-dataplane, img: edc-dataplane-azure-vault }, - { dir: edc-dataplane, img: edc-dataplane-hashicorp-vault } ] - + { dir: edc-dataplane, img: edc-dataplane-hashicorp-vault }, + { dir: edc-tests/runtime, img: mock-connector }] steps: - uses: actions/checkout@v4 - name: Export RELEASE_VERSION env diff --git a/.github/workflows/verify.yaml b/.github/workflows/verify.yaml index b4dd1bf4a..88bf8d850 100644 --- a/.github/workflows/verify.yaml +++ b/.github/workflows/verify.yaml @@ -95,7 +95,9 @@ jobs: - uses: ./.github/actions/setup-java - name: Run Integration tests - run: ./gradlew test -DincludeTags="ComponentTest" + run: | + ./gradlew :edc-tests:runtime:mock-connector:dockerize + ./gradlew test -DincludeTags="ComponentTest" api-tests: runs-on: ubuntu-latest diff --git a/docs/development/mock-edc.md b/docs/development/mock-edc.md new file mode 100644 index 000000000..2efdaf995 --- /dev/null +++ b/docs/development/mock-edc.md @@ -0,0 +1,220 @@ +# Using the Mock-Connector for contract-based testing + +Modern testing methodologies are based on small, independent units of code that have a defined behaviour. +Implementations as well as testing should be fast, repeatable, continuous and easily maintainable. In the context of EDC +that means, that downstream projects that are based on EDC should not need to run a fully-fledged connector runtime to +test their workflows. While the Tractus-X EDC project did provide a pure in-memory runtime for testing, that still +requires all the configuration and a complex runtime environment to work, which may be a high barrier of entry. + +For this reason, and to developers who primarily interact with the Management API of a connector, the Tractus-X EDC +project provides a testing framework with an even smaller footprint called the "Mock-Connector". It is a Docker image, that +contains just the Management API plus an instrumentation interface to enable developers to use this in their +unit/component testing and in continuous integration. + +We call this "contract-based testing", as it defines the specified behaviour of an application (here: the connector). +The Mock-Connector's Management API is guaranteed to behave exactly the same, in fact, it even runs the +same code as a "real" EDC. + +## 1. The contract + +The [Management API spec](https://eclipse-edc.github.io/Connector/openapi/management-api/). + +### 1.1 Definition of terms + +- connector: runnable Java application that contains Tractus-X modules. Also referred to as: EDC, runtime, tx-edc +- mock: a replacement for a collaborator object (class, component, application), where the behaviour can be + controlled +- stub: very similar to a mock, but while a mock oftentimes is a drop-in _replacement_, a stub would be a + re-implementation with a fixed behaviour. Also referred to as: dummy +- instrumentation: the process of setting up a mock to behave a certain way. Also referred to as priming the mock. + +## 2. Intended audience + +Developers who build their applications and systems based on EDC, and interact with EDC through the Management API can +use the Mock-Connector to decrease friction by not having to spin up and configure a fully-fledged connector runtime. + +Developers who plan to work with (Tractus-X) EDC in another way, like directly using its Maven artifacts, or even by +implementing a DSP protocol head are kindly redirected to +the [additional references section](#5-references-and-further-reading). + +## 3. Use with TestContainers + +Mock-Connector should be used as Docker image, we publish it as `tractusx/edc-mock`. + +Using the Mock-Connector is very easy, we recommend usage via Testcontainers. For example, setting up a JUnit test for a +client application using Testcontainers could be done as follows: + +```java + +@Testcontainers +@ComponentTest +public class UseMockedEdcSampleTest { + @Container + protected static GenericContainer edcContainer = new GenericContainer<>("tractusx/edc-mock:latest") + .withEnv("WEB_HTTP_PORT", "8080") + .withEnv("WEB_HTTP_PATH", "/api") + .withEnv("WEB_HTTP_MANAGEMENT_PORT", "8081") + .withEnv("WEB_HTTP_MANAGEMENT_PATH", "/api/management") + .withExposedPorts(8080, 8081); + private int managementPort; + private int defaultPort; + + @BeforeEach + void setup() { + managementPort = edcContainer.getMappedPort(8081); + defaultPort = edcContainer.getMappedPort(8080); + } +} +``` + +This downloads and runs the Docker image for the Mock-Connector and supplies it with minimal configuration. Specifically, it +exposes the Management API and the default context, because that is needed to set up the mock. + +> Please note that in +> the [example](../../samples/testing-with-mocked-edc/src/test/java/org/eclipse/tractusx/edc/samples/mockedc/UseMockedEdcSampleTest.java), +> the image name is `mock-edc` - that is because in our CI testing we build the image and then run the tests, so we +> can't use the official image. + +### 3.1 Running a simple positive test + +Executing a simple request against the Management API of EDC can be done like this: + +```java + +@Test +void test_getAsset() { + //prime the mock - post a RecordedRequest + setupNextResponse("asset.request.json"); + + // perform the actual Asset API request. In a real test scenario, this would be the client code we're testing, i.e. the + // System-under-Test (SuT). + var assetArray = mgmtRequest() + .contentType(ContentType.JSON) + .body(""" + { + "@context": { + "@vocab": "https://w3id.org/edc/v0.0.1/ns/" + }, + "@type": "QuerySpec" + } + """) + .post("/v3/assets/request") + .then() + .log().ifError() + .statusCode(200) + .extract().body().as(JsonArray.class); + + // assert the response + assertThat(assetArray).hasSize(1); + assertThat(assetArray.get(0).asJsonObject().get("properties")) + .hasFieldOrProperty("prop1") + .hasFieldOrProperty("id") + .hasFieldOrProperty("contenttype"); +} +``` + +### 3.2 Running a test expecting a failure + +```java + +@Test +void test_apiNotAuthenticated_expect400() { + //prime the mock - post a RecordedRequest + setupNextResponse("asset.creation.failure.json"); + + // perform the actual Asset API request. In a real test scenario, this would be the client code we're testing, i.e. the + // System-under-Test (SuT). + var assetArray = mgmtRequest() + .contentType(ContentType.JSON) + .body(""" + { + "@context": { + "@vocab": "https://w3id.org/edc/v0.0.1/ns/" + }, + "@type": "QuerySpec" + } + """) + .post("/v3/assets/request") + .then() + .log().ifError() + .statusCode(400) + .extract().body().as(JsonArray.class); + + // assert the response contains error information + assertThat(assetArray).hasSize(1); + var errorObject = assetArray.get(0).asJsonObject(); + assertThat(errorObject.get("message").toString()).contains("This user is not authorized, This is just a second error message"); +} +``` + +Note that the difference here is that we prime the mock with a different JSON file (more on that later), we expect a +different HTTP response code, i.e. 400, and the response body contains an error object instead of an array of Assets. + +## 4. Request pipeline and the instrumentation API + +The Mock-Connector internally contains a pipeline of "recorded requests", much like mocked HTTP webservers, like Netty +Mockserver or OkHttp MockWebServer. Out-of-the-box, that pipeline is empty, which means the Management API would always +respond with an error like the following: + +```json +[ + { + "message": "Failure: no recorded request left in queue.", + "type": "InvalidRequest", + "path": null, + "invalidValue": null + } +] +``` + +To get beyond that, we need to _prime_ the mock. That means, we need to tell it how to respond to the next request by +inserting a "recorded request" into its request pipeline. In previous code examples, this was done using +the `setupNextResponse()` method. Mock-Connector offers an instrumentation API which can be used to insert recorded requests, +to clear the queue and to get a count. + +### 4.1 Recorded requests + +A `RecordedRequest` is a POJO, that tells the Mock-Connector how to respond to the _next_ Management API request. To that end, +it contains the input parameter type, the data associated with it, plus the return value type plus - most importantly - +the data that is supposed to be returned. + +Recall the [previous example](#31-running-a-simple-positive-test), which tests an Asset request. Thus, we have to prime +the mock such that it responds with a list of `Asset` objects. The semantic being: "on the next request, respond +with ...". + +The contents of the [asset.request.json](../../samples/testing-with-mocked-edc/src/test/resources/asset.request.json) +contains a section that defines the `input`, which in this case is a `QuerySpec`, and the `output` is a list of `Asset` +objects. The `data` section must then contain serialized JSON that matches the `class` property. For instance, +the `data` section of the `input` must contain JSON that can be deserialized into an `Asset`. + +> _Note that the information about input and output datatypes must currently be obtained from the aggregate services. +Here, that would be the `AssetService` interface. In future iterations there will be a more convenient way to obtain +that information._ + +> _Note that input argument type matching is currently not supported, it will come in future releases._ + +### 4.2 Instrumentation API + +The instrumentation is done via a simple REST API: + +```shell +GET /api/instrumentation/count -> returns the number of requests in the queue +GET /api/instrumentation -> returns the list of queued requests +DELETE /api/instrumentation -> clears the queue +POST /api/instrumentation -> adds a new RecordedRequest, JSON must be in the request body +``` + +## 5. References and further reading + +- A complete sample how to run a test using the Mock-Connector in a Testcontainer can be + found [here](../../samples/testing-with-mocked-edc) +- To test compliance with DSP, use the [TCK](https://github.com/eclipse-dataspacetck/cvf) +- A Mock-IATP runtime is planned for future releases. + +## 6. Future improvements + +- matching requests to endpoints to allow for a "from-now-on" semantic +- introducing placeholders for domain objects to increase refactoring robustness +- abstract description of the endpoint's inputs and outputs, so developers don't need to know about service signatures + anymore +- request input matching \ No newline at end of file diff --git a/edc-tests/runtime/mock-connector/build.gradle.kts b/edc-tests/runtime/mock-connector/build.gradle.kts new file mode 100644 index 000000000..267e440d2 --- /dev/null +++ b/edc-tests/runtime/mock-connector/build.gradle.kts @@ -0,0 +1,69 @@ +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar + +/******************************************************************************** + * Copyright (c) 2023 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` + id("application") + id("com.github.johnrengelman.shadow") version "8.1.1" + id("io.swagger.core.v3.swagger-gradle-plugin") +} + + +dependencies { + // compile-time dependencies + implementation(libs.edc.spi.boot) + implementation(libs.edc.spi.controlplane) + implementation(libs.edc.lib.util) + + // runtime dependencies + runtimeOnly(libs.edc.core.connector) + runtimeOnly(libs.edc.boot) + runtimeOnly(libs.edc.api.management) + runtimeOnly(libs.edc.api.management.config) + + runtimeOnly(libs.edc.ext.http) + runtimeOnly(libs.bundles.edc.monitoring) + + // edc libs + runtimeOnly(libs.edc.ext.jsonld) + + testImplementation(libs.edc.junit) + testImplementation(libs.assertj) +} + +application { + mainClass.set("org.eclipse.edc.boot.system.runtime.BaseRuntime") +} + +edcBuild { + publish.set(false) +} + +tasks.withType { + exclude("**/pom.properties", "**/pom.xm") + mergeServiceFiles() + archiveFileName.set("${project.name}.jar") +} + + +application { + mainClass.set("org.eclipse.edc.boot.system.runtime.BaseRuntime") +} diff --git a/edc-tests/runtime/mock-connector/src/main/docker/Dockerfile b/edc-tests/runtime/mock-connector/src/main/docker/Dockerfile new file mode 100644 index 000000000..999103615 --- /dev/null +++ b/edc-tests/runtime/mock-connector/src/main/docker/Dockerfile @@ -0,0 +1,51 @@ +################################################################################# +# Copyright (c) 2024 Bayerische Motoren Werk 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 +################################################################################# + + +FROM eclipse-temurin:22_36-jre-alpine +ARG JAR +ARG OTEL_JAR +ARG ADDITIONAL_FILES + +ARG APP_USER=docker +ARG APP_UID=10100 + +RUN addgroup --system "$APP_USER" + +RUN adduser \ + --shell /sbin/nologin \ + --disabled-password \ + --gecos "" \ + --ingroup "$APP_USER" \ + --no-create-home \ + --uid "$APP_UID" \ + "$APP_USER" + +USER "$APP_USER" +WORKDIR /app + +COPY ${JAR} edc-mock.jar +COPY ${ADDITIONAL_FILES} ./ + +HEALTHCHECK NONE + +CMD ["java", \ + "-Djava.security.egd=file:/dev/urandom", \ + "-jar", \ + "edc-mock.jar"] diff --git a/edc-tests/runtime/mock-connector/src/main/java/org/eclipse/tractusx/edc/mock/MatchType.java b/edc-tests/runtime/mock-connector/src/main/java/org/eclipse/tractusx/edc/mock/MatchType.java new file mode 100644 index 000000000..fcdcacbce --- /dev/null +++ b/edc-tests/runtime/mock-connector/src/main/java/org/eclipse/tractusx/edc/mock/MatchType.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft + * + * 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.mock; + +/** + * Represents how arguments are matched. + * + */ +public enum MatchType { + CLASS, PARTIAL, EXACT +} diff --git a/edc-tests/runtime/mock-connector/src/main/java/org/eclipse/tractusx/edc/mock/MockServiceExtension.java b/edc-tests/runtime/mock-connector/src/main/java/org/eclipse/tractusx/edc/mock/MockServiceExtension.java new file mode 100644 index 000000000..7176f87dd --- /dev/null +++ b/edc-tests/runtime/mock-connector/src/main/java/org/eclipse/tractusx/edc/mock/MockServiceExtension.java @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft + * + * 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.mock; + +import com.fasterxml.jackson.databind.module.SimpleModule; +import org.eclipse.edc.connector.controlplane.services.spi.asset.AssetService; +import org.eclipse.edc.connector.controlplane.services.spi.catalog.CatalogService; +import org.eclipse.edc.connector.controlplane.services.spi.contractagreement.ContractAgreementService; +import org.eclipse.edc.connector.controlplane.services.spi.contractdefinition.ContractDefinitionService; +import org.eclipse.edc.connector.controlplane.services.spi.contractnegotiation.ContractNegotiationService; +import org.eclipse.edc.connector.controlplane.services.spi.policydefinition.PolicyDefinitionService; +import org.eclipse.edc.connector.controlplane.services.spi.transferprocess.TransferProcessService; +import org.eclipse.edc.runtime.metamodel.annotation.Inject; +import org.eclipse.edc.runtime.metamodel.annotation.Provider; +import org.eclipse.edc.spi.monitor.Monitor; +import org.eclipse.edc.spi.query.QuerySpec; +import org.eclipse.edc.spi.response.StatusResult; +import org.eclipse.edc.spi.result.ServiceFailure; +import org.eclipse.edc.spi.system.ServiceExtension; +import org.eclipse.edc.spi.system.ServiceExtensionContext; +import org.eclipse.edc.spi.types.TypeManager; +import org.eclipse.edc.web.spi.WebService; +import org.eclipse.tractusx.edc.mock.api.instrumentation.InstrumentationApiController; +import org.eclipse.tractusx.edc.mock.services.AssetServiceStub; +import org.eclipse.tractusx.edc.mock.services.ContractAgreementServiceStub; +import org.eclipse.tractusx.edc.mock.services.ContractDefinitionServiceStub; +import org.eclipse.tractusx.edc.mock.services.ContractNegotiationServiceStub; +import org.eclipse.tractusx.edc.mock.services.PolicyDefinitionServiceStub; +import org.eclipse.tractusx.edc.mock.services.TransferProcessServiceStub; + +import java.util.Queue; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentLinkedQueue; + +public class MockServiceExtension implements ServiceExtension { + private final Queue> recordedRequests = new ConcurrentLinkedQueue<>(); + @Inject + private TypeManager typeManager; + + @Inject + private WebService webService; + + private Monitor monitor; + + @Override + public void initialize(ServiceExtensionContext context) { + monitor = context.getMonitor().withPrefix("ResponseQueue"); + webService.registerResource(new InstrumentationApiController(new ResponseQueue(recordedRequests, monitor))); + + // register custom deserializer for the ServiceFailure + var mapper = typeManager.getMapper(); + var module = new SimpleModule(); + module.addDeserializer(ServiceFailure.class, new ServiceFailureDeserializer()); + mapper.registerModule(module); + } + + @Provider + public AssetService mockAssetService(ServiceExtensionContext context) { + var monitor = context.getMonitor().withPrefix("ResponseQueue"); + return new AssetServiceStub(new ResponseQueue(recordedRequests, monitor)); + } + + @Provider + public CatalogService mockCatalogService() { + return new CatalogService() { + @Override + public CompletableFuture> requestCatalog(String counterPartyId, String counterPartyAddress, String protocol, QuerySpec querySpec) { + return null; + } + + @Override + public CompletableFuture> requestDataset(String id, String counterPartyId, String counterPartyAddress, String protocol) { + return null; + } + }; + } + + @Provider + public ContractAgreementService mockContractAgreementService() { + return new ContractAgreementServiceStub(new ResponseQueue(recordedRequests, monitor)); + } + + @Provider + public ContractDefinitionService mockContractDefService() { + return new ContractDefinitionServiceStub(new ResponseQueue(recordedRequests, monitor)); + } + + @Provider + public ContractNegotiationService mockContractNegService() { + return new ContractNegotiationServiceStub(new ResponseQueue(recordedRequests, monitor)); + } + + @Provider + public PolicyDefinitionService mockPolicyDefService() { + return new PolicyDefinitionServiceStub(new ResponseQueue(recordedRequests, monitor)); + } + + @Provider + public TransferProcessService mockTransferProcessService() { + return new TransferProcessServiceStub(new ResponseQueue(recordedRequests, monitor)); + } + +} \ No newline at end of file diff --git a/edc-tests/runtime/mock-connector/src/main/java/org/eclipse/tractusx/edc/mock/RecordedRequest.java b/edc-tests/runtime/mock-connector/src/main/java/org/eclipse/tractusx/edc/mock/RecordedRequest.java new file mode 100644 index 000000000..6b47b81be --- /dev/null +++ b/edc-tests/runtime/mock-connector/src/main/java/org/eclipse/tractusx/edc/mock/RecordedRequest.java @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft + * + * 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.mock; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import org.eclipse.edc.spi.result.ServiceFailure; + +/** + * Represents a request, that the service stub will replay. It has an input object (can be null), an output object, some metadata + * like name and description and a {@link MatchType}. + * In addition, it can have a {@link ServiceFailure}, in which case the {@code output} object is disregarded and the failure is always returned. + * This can be used to mock a failed API call. + */ +@JsonDeserialize(using = RecordedResponseDeserializer.class) +public final class RecordedRequest { + private final I input; + private final O output; + private String description; + private String name; + private MatchType inputMatchType; + private ServiceFailure failure; + + private RecordedRequest(I input, O output) { + this.input = input; + this.output = output; + } + + public I getInput() { + return input; + } + + public O getOutput() { + return output; + } + + public MatchType getInputMatchType() { + return inputMatchType; + } + + public ServiceFailure getFailure() { + return failure; + } + + public String getName() { + return name; + } + + public String getDescription() { + return description; + } + + public static class Builder { + private final RecordedRequest instance; + + public Builder(I input, O output) { + instance = new RecordedRequest<>(input, output); + } + + public Builder inputMatchType(MatchType input) { + instance.inputMatchType = input; + return this; + } + + public Builder name(String name) { + instance.name = name; + return this; + } + + public Builder description(String description) { + instance.description = description; + return this; + } + + public Builder failure(ServiceFailure failure) { + instance.failure = failure; + return this; + } + + public RecordedRequest build() { + return instance; + } + } +} diff --git a/edc-tests/runtime/mock-connector/src/main/java/org/eclipse/tractusx/edc/mock/RecordedResponseDeserializer.java b/edc-tests/runtime/mock-connector/src/main/java/org/eclipse/tractusx/edc/mock/RecordedResponseDeserializer.java new file mode 100644 index 000000000..d532ae1db --- /dev/null +++ b/edc-tests/runtime/mock-connector/src/main/java/org/eclipse/tractusx/edc/mock/RecordedResponseDeserializer.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft + * + * 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.mock; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import org.eclipse.edc.spi.result.ServiceFailure; + +import java.io.IOException; +import java.util.Optional; + +/** + * Custom deserializer for a {@link RecordedRequest} object, to be able to instantiate the input and output objects according + * to their class description. + */ +class RecordedResponseDeserializer extends StdDeserializer> { + + public static final String INPUT_OBJECT = "input"; + public static final String OUTPUT_OBJECT = "output"; + public static final String CLASS_FIELD = "class"; + public static final String DATA_FIELD = "data"; + public static final String INPUT_MATCH_TYPE_FIELD = "match_type"; + public static final String NAME_FIELD = "name"; + public static final String DESCRIPTION_FIELD = "description"; + private static final String FAILURE_OBJECT = "failure"; + + RecordedResponseDeserializer() { + this(null); + } + + protected RecordedResponseDeserializer(Class vc) { + super(vc); + } + + @Override + public RecordedRequest deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException { + JsonNode node = jp.getCodec().readTree(jp); + var input = node.get(INPUT_OBJECT); + var output = node.get(OUTPUT_OBJECT); + try { + var inputClass = Class.forName(input.get(CLASS_FIELD).asText()); + var outputClass = Class.forName(output.get(CLASS_FIELD).asText()); + + var inputObj = ctxt.readTreeAsValue(input.get(DATA_FIELD), inputClass); + var matchType = Optional.ofNullable(input.get("matchType")).map(JsonNode::asText).map(MatchType::valueOf).orElse(MatchType.CLASS); + var outputObj = ctxt.readTreeAsValue(output.get(DATA_FIELD), outputClass); + + return new RecordedRequest.Builder(inputObj, outputObj) + .inputMatchType(matchType) + .failure(ctxt.readTreeAsValue(node.get(FAILURE_OBJECT), ServiceFailure.class)) + .name(Optional.ofNullable(node.get(NAME_FIELD)).map(JsonNode::asText).orElse(null)) + .description(Optional.ofNullable(node.get(DESCRIPTION_FIELD)).map(JsonNode::asText).orElse(null)) + .build(); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } +} diff --git a/edc-tests/runtime/mock-connector/src/main/java/org/eclipse/tractusx/edc/mock/ResponseQueue.java b/edc-tests/runtime/mock-connector/src/main/java/org/eclipse/tractusx/edc/mock/ResponseQueue.java new file mode 100644 index 000000000..d4839b1d1 --- /dev/null +++ b/edc-tests/runtime/mock-connector/src/main/java/org/eclipse/tractusx/edc/mock/ResponseQueue.java @@ -0,0 +1,144 @@ +/* + * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft + * + * 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.mock; + +import org.eclipse.edc.spi.EdcException; +import org.eclipse.edc.spi.monitor.Monitor; +import org.eclipse.edc.spi.result.Result; +import org.eclipse.edc.spi.result.ServiceFailure; +import org.eclipse.edc.spi.result.ServiceResult; +import org.eclipse.edc.web.spi.exception.InvalidRequestException; + +import java.lang.reflect.InvocationTargetException; +import java.util.Arrays; +import java.util.List; +import java.util.Queue; + +/** + * Container object that maintains the queue of {@link RecordedRequest} objects and grants high-level access to it. + */ +public class ResponseQueue { + private final Queue> recordedRequests; // todo guard access with locks? + private final Monitor monitor; + + public ResponseQueue(Queue> recordedRequests, Monitor monitor) { + this.recordedRequests = recordedRequests; + this.monitor = monitor; + } + + public ServiceResult getNext(Class outputClass, String errorMessageTemplate) { + try { + return getNext(outputClass); + } catch (ClassCastException ex) { + var message = errorMessageTemplate.formatted(ex.getMessage()); + monitor.severe(message); // no need for the entire stack trace + return ServiceResult.badRequest(message); + } + } + + /** + * Gets the next item from the request queue and wraps it in a {@link ServiceResult}, where the generic type is a list type. + * To do that, the class of list type is expected as parameter. + * + * @param arrayElementClass The type of elements that are expected to be in the list + * @param errorMessageTemplate An error string template that should contain the '%s' placeholder to receive additional information + * @return A {@link ServiceResult} that contains the payload of the next request + */ + @SuppressWarnings("unchecked") + public ServiceResult> getNextAsList(Class arrayElementClass, String errorMessageTemplate) { + if (arrayElementClass.isArray()) { + return ServiceResult.badRequest("First parameter must be type of list elements. For example, pass Object.class if a List is expected, but '%s' was passed".formatted(arrayElementClass.getName())); + } + var r = Result.ofThrowable(() -> { + T[] serviceResult = (T[]) getNext(arrayElementClass.arrayType(), errorMessageTemplate).orElseThrow(f -> new InvalidRequestException(f.getFailureDetail())); + return Arrays.asList(serviceResult); + }); + if (r.succeeded()) { + return ServiceResult.success(r.getContent()); + } + monitor.severe(errorMessageTemplate.formatted(r.getFailureDetail())); + return ServiceResult.badRequest(r.getFailureDetail()); + } + + /** + * clear the queue + */ + public void clear() { + recordedRequests.clear(); + } + + /** + * adds a {@link RecordedRequest} + */ + public void append(RecordedRequest recordedRequest) { + recordedRequests.offer(recordedRequest); + } + + /** + * provides the contents of the queue as a list + */ + public List> toList() { + return recordedRequests.stream().toList(); //immutable + } + + /** + * Takes the next element from the queue and converts it into a {@link ServiceResult} + * + * @param outputType The desired output type. If the type description in the {@link RecordedRequest} does not match, a failure is returned. + */ + @SuppressWarnings("unchecked") + private ServiceResult getNext(Class outputType) { + monitor.debug("Get next recorded request, expect output of type %s".formatted(outputType)); + var r = recordedRequests.poll(); + + + if (r != null) { + + if (r.getFailure() != null) { + return createResult(r.getFailure()); + } + + monitor.debug("Recorded request fetched, %d remaining.".formatted(recordedRequests.size())); + var recipeOutputType = r.getOutput().getClass(); + if (!recipeOutputType.isAssignableFrom(outputType)) { + return ServiceResult.badRequest("Type mismatch: service invocation requires output type '%s', but Recipe specifies '%s'".formatted(outputType, recipeOutputType)); + } + var output = (T) r.getOutput(); + return ServiceResult.success(output); + } + var message = "Failure: no recorded request left in queue."; + monitor.debug(message); + return ServiceResult.badRequest(message); + } + + // hack to access a protected constructor of the ServiceFailure + private ServiceResult createResult(ServiceFailure failure) { + try { + var ctor = ServiceResult.class.getDeclaredConstructor(Object.class, ServiceFailure.class); + ctor.setAccessible(true); // hack hack hack! I feel dirty doing this... + return (ServiceResult) ctor.newInstance(null, failure); + } catch (NoSuchMethodException | IllegalAccessException | InstantiationException | + InvocationTargetException e) { + throw new EdcException(e); + } + } + + //todo: add method getNextWithMatch that accepts an input and a match type +} diff --git a/edc-tests/runtime/mock-connector/src/main/java/org/eclipse/tractusx/edc/mock/ServiceFailureDeserializer.java b/edc-tests/runtime/mock-connector/src/main/java/org/eclipse/tractusx/edc/mock/ServiceFailureDeserializer.java new file mode 100644 index 000000000..4fa07eb41 --- /dev/null +++ b/edc-tests/runtime/mock-connector/src/main/java/org/eclipse/tractusx/edc/mock/ServiceFailureDeserializer.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft + * + * 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.mock; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import org.eclipse.edc.spi.result.ServiceFailure; + +import java.io.IOException; +import java.util.Arrays; + +/** + * Custom deserializer for {@link ServiceFailure}. We need this because there is no default + * CTor, and the public constructor with args is not annotated with {@link com.fasterxml.jackson.annotation.JsonProperty}. + */ +public class ServiceFailureDeserializer extends StdDeserializer { + public static final String REASON_FIELD = "reason"; + public static final String MESSAGES_FIELD = "messages"; + + protected ServiceFailureDeserializer(Class vc) { + super(vc); + } + + public ServiceFailureDeserializer() { + this(null); + } + + @Override + public ServiceFailure deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException { + JsonNode node = jp.getCodec().readTree(jp); + var reason = ServiceFailure.Reason.valueOf(node.get(REASON_FIELD).asText()); + var msgs = Arrays.asList(ctxt.readTreeAsValue(node.get(MESSAGES_FIELD), String[].class)); + + return new ServiceFailure(msgs, reason); + } +} diff --git a/edc-tests/runtime/mock-connector/src/main/java/org/eclipse/tractusx/edc/mock/api/instrumentation/InstrumentationApi.java b/edc-tests/runtime/mock-connector/src/main/java/org/eclipse/tractusx/edc/mock/api/instrumentation/InstrumentationApi.java new file mode 100644 index 000000000..192069756 --- /dev/null +++ b/edc-tests/runtime/mock-connector/src/main/java/org/eclipse/tractusx/edc/mock/api/instrumentation/InstrumentationApi.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft + * + * 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.mock.api.instrumentation; + +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.info.Info; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.eclipse.edc.web.spi.ApiErrorDetail; +import org.eclipse.tractusx.edc.mock.RecordedRequest; + +import java.util.List; + +@OpenAPIDefinition(info = @Info(description = "This API allows to insert ", title = "Business Partner Group API")) +@Tag(name = "Business Partner Group") +public interface InstrumentationApi { + + @Operation(description = "Adds a new RecordedRequest to the end of the queue.", + responses = { + @ApiResponse(responseCode = "204", description = "The negotiation was successfully initiated."), + @ApiResponse(responseCode = "400", description = "Request body was malformed", + content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)))), + }) + void addNewRequest(RecordedRequest recordedRequest); + + @Operation(description = "Clears the entire request queue.", + responses = { + @ApiResponse(responseCode = "204", description = "The queue was successfully cleared.") + }) + void clearQueue(); + + @Operation(description = "Return the entire request queue.", + responses = { + @ApiResponse(responseCode = "200", description = "The list of RecordedRequest objects.", + content = @Content(array = @ArraySchema(schema = @Schema(implementation = RecordedRequest.class)))), + }) + List> getRequests(); + + @Operation(description = "Return amount of items currently in the queue.", + responses = { + @ApiResponse(responseCode = "200") + }) + int count(); +} diff --git a/edc-tests/runtime/mock-connector/src/main/java/org/eclipse/tractusx/edc/mock/api/instrumentation/InstrumentationApiController.java b/edc-tests/runtime/mock-connector/src/main/java/org/eclipse/tractusx/edc/mock/api/instrumentation/InstrumentationApiController.java new file mode 100644 index 000000000..ab2d100b7 --- /dev/null +++ b/edc-tests/runtime/mock-connector/src/main/java/org/eclipse/tractusx/edc/mock/api/instrumentation/InstrumentationApiController.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft + * + * 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.mock.api.instrumentation; + + +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import org.eclipse.tractusx.edc.mock.RecordedRequest; +import org.eclipse.tractusx.edc.mock.ResponseQueue; + +import java.util.List; + +@Consumes({ MediaType.APPLICATION_JSON }) +@Produces({ MediaType.APPLICATION_JSON }) +@Path("/instrumentation") +public class InstrumentationApiController implements InstrumentationApi { + + private final ResponseQueue responseQueue; + + public InstrumentationApiController(ResponseQueue responseQueue) { + this.responseQueue = responseQueue; + } + + @Override + @POST + public void addNewRequest(RecordedRequest recordedRequest) { + responseQueue.append(recordedRequest); + } + + @Override + @DELETE + public void clearQueue() { + responseQueue.clear(); + } + + @Override + @GET + public List> getRequests() { + return responseQueue.toList(); + } + + @Override + @GET + @Path("/count") + public int count() { + return responseQueue.toList().size(); + } +} diff --git a/edc-tests/runtime/mock-connector/src/main/java/org/eclipse/tractusx/edc/mock/services/AbstractServiceStub.java b/edc-tests/runtime/mock-connector/src/main/java/org/eclipse/tractusx/edc/mock/services/AbstractServiceStub.java new file mode 100644 index 000000000..56fae0e66 --- /dev/null +++ b/edc-tests/runtime/mock-connector/src/main/java/org/eclipse/tractusx/edc/mock/services/AbstractServiceStub.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft + * + * 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.mock.services; + +import org.eclipse.tractusx.edc.mock.ResponseQueue; + +public abstract class AbstractServiceStub { + protected final ResponseQueue responseQueue; + + public AbstractServiceStub(ResponseQueue responseQueue) { + this.responseQueue = responseQueue; + } +} diff --git a/edc-tests/runtime/mock-connector/src/main/java/org/eclipse/tractusx/edc/mock/services/AssetServiceStub.java b/edc-tests/runtime/mock-connector/src/main/java/org/eclipse/tractusx/edc/mock/services/AssetServiceStub.java new file mode 100644 index 000000000..1b58cdd94 --- /dev/null +++ b/edc-tests/runtime/mock-connector/src/main/java/org/eclipse/tractusx/edc/mock/services/AssetServiceStub.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft + * + * 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.mock.services; + +import org.eclipse.edc.connector.controlplane.asset.spi.domain.Asset; +import org.eclipse.edc.connector.controlplane.services.spi.asset.AssetService; +import org.eclipse.edc.spi.query.QuerySpec; +import org.eclipse.edc.spi.result.ServiceResult; +import org.eclipse.edc.web.spi.exception.InvalidRequestException; +import org.eclipse.tractusx.edc.mock.ResponseQueue; + +import java.util.List; + +public class AssetServiceStub implements AssetService { + + private final ResponseQueue responseQueue; + + public AssetServiceStub(ResponseQueue responseQueue) { + this.responseQueue = responseQueue; + } + + @Override + public Asset findById(String assetId) { + return responseQueue.getNext(Asset.class, "Error finding asset by ID: %s") + .orElseThrow(InvalidRequestException::new); + } + + @Override + public ServiceResult> search(QuerySpec query) { + return responseQueue.getNextAsList(Asset.class, "Error executing asset search: %s"); + } + + @Override + public ServiceResult create(Asset asset) { + return responseQueue.getNext(Asset.class, "Error executing asset creation: %s"); + } + + @Override + public ServiceResult delete(String assetId) { + return responseQueue.getNext(Asset.class, "Error executing asset deletion: %s"); + } + + @Override + public ServiceResult update(Asset asset) { + return responseQueue.getNext(Asset.class, "Error executing asset update: %s"); + } +} diff --git a/edc-tests/runtime/mock-connector/src/main/java/org/eclipse/tractusx/edc/mock/services/ContractAgreementServiceStub.java b/edc-tests/runtime/mock-connector/src/main/java/org/eclipse/tractusx/edc/mock/services/ContractAgreementServiceStub.java new file mode 100644 index 000000000..637e213a6 --- /dev/null +++ b/edc-tests/runtime/mock-connector/src/main/java/org/eclipse/tractusx/edc/mock/services/ContractAgreementServiceStub.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft + * + * 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.mock.services; + +import org.eclipse.edc.connector.controlplane.contract.spi.types.agreement.ContractAgreement; +import org.eclipse.edc.connector.controlplane.contract.spi.types.negotiation.ContractNegotiation; +import org.eclipse.edc.connector.controlplane.services.spi.contractagreement.ContractAgreementService; +import org.eclipse.edc.spi.query.QuerySpec; +import org.eclipse.edc.spi.result.ServiceResult; +import org.eclipse.edc.web.spi.exception.InvalidRequestException; +import org.eclipse.tractusx.edc.mock.ResponseQueue; + +import java.util.List; + +public class ContractAgreementServiceStub extends AbstractServiceStub implements ContractAgreementService { + + public ContractAgreementServiceStub(ResponseQueue responseQueue) { + super(responseQueue); + } + + @Override + public ContractAgreement findById(String contractAgreementId) { + return responseQueue.getNext(ContractAgreement.class, "Error finding ContractAgreement: %s") + .orElseThrow(InvalidRequestException::new); + } + + @Override + public ServiceResult> search(QuerySpec query) { + return responseQueue.getNextAsList(ContractAgreement.class, "Error searching ContractAgreement: %s"); + } + + @Override + public ContractNegotiation findNegotiation(String contractAgreementId) { + return responseQueue.getNext(ContractNegotiation.class, "Error finding ContractNegotiation: %s") + .orElseThrow(InvalidRequestException::new); + } +} diff --git a/edc-tests/runtime/mock-connector/src/main/java/org/eclipse/tractusx/edc/mock/services/ContractDefinitionServiceStub.java b/edc-tests/runtime/mock-connector/src/main/java/org/eclipse/tractusx/edc/mock/services/ContractDefinitionServiceStub.java new file mode 100644 index 000000000..9435ff6f1 --- /dev/null +++ b/edc-tests/runtime/mock-connector/src/main/java/org/eclipse/tractusx/edc/mock/services/ContractDefinitionServiceStub.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft + * + * 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.mock.services; + +import org.eclipse.edc.connector.controlplane.contract.spi.types.offer.ContractDefinition; +import org.eclipse.edc.connector.controlplane.services.spi.contractdefinition.ContractDefinitionService; +import org.eclipse.edc.spi.query.QuerySpec; +import org.eclipse.edc.spi.result.ServiceResult; +import org.eclipse.edc.web.spi.exception.InvalidRequestException; +import org.eclipse.tractusx.edc.mock.ResponseQueue; + +import java.util.List; + +public class ContractDefinitionServiceStub extends AbstractServiceStub implements ContractDefinitionService { + + + public ContractDefinitionServiceStub(ResponseQueue responseQueue) { + super(responseQueue); + + } + + @Override + public ContractDefinition findById(String contractDefinitionId) { + return responseQueue.getNext(ContractDefinition.class, "Error finding ContractDefinition: %s") + .orElseThrow(InvalidRequestException::new); + } + + @Override + public ServiceResult> search(QuerySpec query) { + return responseQueue.getNextAsList(ContractDefinition.class, "Error searching ContractDefinition: %s"); + } + + @Override + public ServiceResult create(ContractDefinition contractDefinition) { + return responseQueue.getNext(ContractDefinition.class, "Error creating ContractDefinition: %s"); + } + + @Override + public ServiceResult update(ContractDefinition contractDefinition) { + return responseQueue.getNext(Void.class, "Error updating ContractDefinition: %s"); + } + + @Override + public ServiceResult delete(String contractDefinitionId) { + return responseQueue.getNext(ContractDefinition.class, "Error deleting ContractDefinition: %s"); + } +} diff --git a/edc-tests/runtime/mock-connector/src/main/java/org/eclipse/tractusx/edc/mock/services/ContractNegotiationServiceStub.java b/edc-tests/runtime/mock-connector/src/main/java/org/eclipse/tractusx/edc/mock/services/ContractNegotiationServiceStub.java new file mode 100644 index 000000000..ada18ab41 --- /dev/null +++ b/edc-tests/runtime/mock-connector/src/main/java/org/eclipse/tractusx/edc/mock/services/ContractNegotiationServiceStub.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft + * + * 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.mock.services; + +import org.eclipse.edc.connector.controlplane.contract.spi.types.agreement.ContractAgreement; +import org.eclipse.edc.connector.controlplane.contract.spi.types.command.TerminateNegotiationCommand; +import org.eclipse.edc.connector.controlplane.contract.spi.types.negotiation.ContractNegotiation; +import org.eclipse.edc.connector.controlplane.contract.spi.types.negotiation.ContractRequest; +import org.eclipse.edc.connector.controlplane.services.spi.contractnegotiation.ContractNegotiationService; +import org.eclipse.edc.spi.query.QuerySpec; +import org.eclipse.edc.spi.result.ServiceResult; +import org.eclipse.edc.web.spi.exception.InvalidRequestException; +import org.eclipse.tractusx.edc.mock.ResponseQueue; + +import java.util.List; + +public class ContractNegotiationServiceStub extends AbstractServiceStub implements ContractNegotiationService { + public ContractNegotiationServiceStub(ResponseQueue responseQueue) { + super(responseQueue); + } + + @Override + public ContractNegotiation findbyId(String contractNegotiationId) { + return responseQueue.getNext(ContractNegotiation.class, "Error finding ContractNegotiation: %s") + .orElseThrow(InvalidRequestException::new); + } + + @Override + public ServiceResult> search(QuerySpec query) { + return responseQueue.getNextAsList(ContractNegotiation.class, "Error searching for ContractNegotiation: %s"); + } + + @Override + public String getState(String negotiationId) { + return responseQueue.getNext(String.class, "Error getting state of ContractNegotiation: %s") + .orElseThrow(InvalidRequestException::new); + } + + @Override + public ContractAgreement getForNegotiation(String negotiationId) { + return responseQueue.getNext(ContractAgreement.class, "Error getting ContractAgreement: %s") + .orElseThrow(InvalidRequestException::new); + } + + @Override + public ContractNegotiation initiateNegotiation(ContractRequest request) { + return responseQueue.getNext(ContractNegotiation.class, "Error initiating ContractNegotiation: %s") + .orElseThrow(InvalidRequestException::new); + } + + @Override + public ServiceResult terminate(TerminateNegotiationCommand command) { + return responseQueue.getNext(Void.class, "Error terminating ContractAgreement: %s"); + } +} diff --git a/edc-tests/runtime/mock-connector/src/main/java/org/eclipse/tractusx/edc/mock/services/PolicyDefinitionServiceStub.java b/edc-tests/runtime/mock-connector/src/main/java/org/eclipse/tractusx/edc/mock/services/PolicyDefinitionServiceStub.java new file mode 100644 index 000000000..fc214945b --- /dev/null +++ b/edc-tests/runtime/mock-connector/src/main/java/org/eclipse/tractusx/edc/mock/services/PolicyDefinitionServiceStub.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft + * + * 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.mock.services; + +import org.eclipse.edc.connector.controlplane.policy.spi.PolicyDefinition; +import org.eclipse.edc.connector.controlplane.services.spi.policydefinition.PolicyDefinitionService; +import org.eclipse.edc.spi.query.QuerySpec; +import org.eclipse.edc.spi.result.ServiceResult; +import org.eclipse.edc.web.spi.exception.InvalidRequestException; +import org.eclipse.tractusx.edc.mock.ResponseQueue; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +public class PolicyDefinitionServiceStub extends AbstractServiceStub implements PolicyDefinitionService { + + public PolicyDefinitionServiceStub(ResponseQueue responseQueue) { + super(responseQueue); + } + + @Override + public PolicyDefinition findById(String policyId) { + return responseQueue.getNext(PolicyDefinition.class, "Error finding PolicyDefinition by id: %s") + .orElseThrow(InvalidRequestException::new); + } + + @Override + public ServiceResult> search(QuerySpec query) { + return responseQueue.getNextAsList(PolicyDefinition.class, "Error executing PolicyDefinition search: %s"); + } + + @Override + public @NotNull ServiceResult deleteById(String policyId) { + return responseQueue.getNext(PolicyDefinition.class, "Error deleting PolicyDefinition: %s"); + } + + @Override + public @NotNull ServiceResult create(PolicyDefinition policy) { + return responseQueue.getNext(PolicyDefinition.class, "Error creating PolicyDefinition: %s"); + } + + @Override + public ServiceResult update(PolicyDefinition policy) { + return responseQueue.getNext(PolicyDefinition.class, "Error updating PolicyDefinition: %s"); + } + + +} diff --git a/edc-tests/runtime/mock-connector/src/main/java/org/eclipse/tractusx/edc/mock/services/TransferProcessServiceStub.java b/edc-tests/runtime/mock-connector/src/main/java/org/eclipse/tractusx/edc/mock/services/TransferProcessServiceStub.java new file mode 100644 index 000000000..f02f750e2 --- /dev/null +++ b/edc-tests/runtime/mock-connector/src/main/java/org/eclipse/tractusx/edc/mock/services/TransferProcessServiceStub.java @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft + * + * 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.mock.services; + +import org.eclipse.edc.connector.controlplane.services.spi.transferprocess.TransferProcessService; +import org.eclipse.edc.connector.controlplane.transfer.spi.types.DeprovisionedResource; +import org.eclipse.edc.connector.controlplane.transfer.spi.types.ProvisionResponse; +import org.eclipse.edc.connector.controlplane.transfer.spi.types.TransferProcess; +import org.eclipse.edc.connector.controlplane.transfer.spi.types.TransferRequest; +import org.eclipse.edc.connector.controlplane.transfer.spi.types.command.ResumeTransferCommand; +import org.eclipse.edc.connector.controlplane.transfer.spi.types.command.SuspendTransferCommand; +import org.eclipse.edc.connector.controlplane.transfer.spi.types.command.TerminateTransferCommand; +import org.eclipse.edc.spi.query.QuerySpec; +import org.eclipse.edc.spi.result.ServiceResult; +import org.eclipse.edc.web.spi.exception.InvalidRequestException; +import org.eclipse.tractusx.edc.mock.ResponseQueue; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +public class TransferProcessServiceStub extends AbstractServiceStub implements TransferProcessService { + + public TransferProcessServiceStub(ResponseQueue responseQueue) { + super(responseQueue); + } + + @Override + public @Nullable TransferProcess findById(String transferProcessId) { + return responseQueue.getNext(TransferProcess.class, "Error finding TransferProcess: %s").orElseThrow(f -> new InvalidRequestException(f.getFailureDetail())); + } + + @Override + public ServiceResult> search(QuerySpec query) { + return responseQueue.getNextAsList(TransferProcess.class, "Error executing TransferProcess search: %s"); + } + + @Override + public @Nullable String getState(String transferProcessId) { + return responseQueue.getNext(String.class, "Error obtaining TransferProcess status: %s") + .orElseThrow(InvalidRequestException::new); + } + + @Override + public @NotNull ServiceResult complete(String transferProcessId) { + return responseQueue.getNext(Void.class, "Error completing TransferProcess: %s"); + } + + @Override + public @NotNull ServiceResult terminate(TerminateTransferCommand command) { + return responseQueue.getNext(Void.class, "Error terminating TransferProcess: %s"); + } + + @Override + public @NotNull ServiceResult suspend(SuspendTransferCommand command) { + return responseQueue.getNext(Void.class, "Error suspending TransferProcess: %s"); + } + + @Override + public @NotNull ServiceResult resume(ResumeTransferCommand command) { + return responseQueue.getNext(Void.class, "Error resuming TransferProcess: %s"); + } + + @Override + public @NotNull ServiceResult deprovision(String transferProcessId) { + return responseQueue.getNext(Void.class, "Error deprovisioning TransferProcess: %s"); + } + + @Override + public @NotNull ServiceResult initiateTransfer(TransferRequest request) { + return responseQueue.getNext(TransferProcess.class, "Error initiating TransferProcess: %s"); + } + + @Override + public ServiceResult completeDeprovision(String transferProcessId, DeprovisionedResource resource) { + return responseQueue.getNext(Void.class, "Error completing/deprovisioning TransferProcess: %s"); + } + + @Override + public ServiceResult addProvisionedResource(String transferProcessId, ProvisionResponse response) { + return responseQueue.getNext(Void.class, "Error adding Provisioned resource to TransferProcess: %s"); + } +} diff --git a/edc-tests/runtime/mock-connector/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension b/edc-tests/runtime/mock-connector/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension new file mode 100644 index 000000000..3de76bc51 --- /dev/null +++ b/edc-tests/runtime/mock-connector/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension @@ -0,0 +1,21 @@ +################################################################################# +# 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.mock.MockServiceExtension + diff --git a/edc-tests/runtime/mock-connector/src/test/java/org/eclipse/tractusx/edc/mock/RecordedRequestTest.java b/edc-tests/runtime/mock-connector/src/test/java/org/eclipse/tractusx/edc/mock/RecordedRequestTest.java new file mode 100644 index 000000000..e8e2853ee --- /dev/null +++ b/edc-tests/runtime/mock-connector/src/test/java/org/eclipse/tractusx/edc/mock/RecordedRequestTest.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft + * + * 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.mock; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; +import org.eclipse.edc.connector.controlplane.asset.spi.domain.Asset; +import org.eclipse.edc.junit.testfixtures.TestUtils; +import org.eclipse.edc.spi.result.ServiceFailure; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class RecordedRequestTest { + + private final ObjectMapper mapper = new ObjectMapper(); + + @BeforeEach + void setup() { + var module = new SimpleModule(); + module.addDeserializer(ServiceFailure.class, new ServiceFailureDeserializer()); + mapper.registerModule(module); + } + + @Test + void verifySerDes() throws JsonProcessingException { + var json = TestUtils.getResourceFileContentAsString("asset.creation.json"); + var rr = mapper.readValue(json, RecordedRequest.class); + + assertThat(rr).isNotNull(); + assertThat(rr.getInput()).isInstanceOf(Asset.class); + assertThat(rr.getOutput()).isInstanceOf(Asset.class); + assertThat(rr.getInputMatchType()).isEqualTo(MatchType.CLASS); + assertThat(rr.getFailure()).isNull(); + assertThat(rr.getName()).isEqualTo("Asset Creation V3"); + assertThat(rr.getDescription()).isEqualTo("test description"); + } + + @Test + void verifySerDes_withFailure() throws JsonProcessingException { + var json = TestUtils.getResourceFileContentAsString("asset.failure.json"); + var rr = mapper.readValue(json, RecordedRequest.class); + + assertThat(rr).isNotNull(); + assertThat(rr.getInput()).isInstanceOf(Asset.class); + assertThat(rr.getOutput()).isNull(); + assertThat(rr.getInputMatchType()).isEqualTo(MatchType.CLASS); + assertThat(rr.getName()).isEqualTo("Some failed asset request"); + assertThat(rr.getDescription()).isEqualTo("test description"); + assertThat(rr.getFailure()).isInstanceOf(ServiceFailure.class); + assertThat(rr.getFailure().getReason()).isEqualTo(ServiceFailure.Reason.UNAUTHORIZED); + assertThat(rr.getFailure().getMessages()).containsExactlyInAnyOrder("message 1", "message 2"); + } +} \ No newline at end of file diff --git a/edc-tests/runtime/mock-connector/src/test/resources/asset.creation.json b/edc-tests/runtime/mock-connector/src/test/resources/asset.creation.json new file mode 100644 index 000000000..e22569a12 --- /dev/null +++ b/edc-tests/runtime/mock-connector/src/test/resources/asset.creation.json @@ -0,0 +1,24 @@ +{ + "name": "Asset Creation V3", + "description": "test description", + "input": { + "class": "org.eclipse.edc.connector.controlplane.asset.spi.domain.Asset", + "data": { + "id": "asset-1", + "properties": { + "https://w3id.org/edc/v0.0.1/ns/contenttype": "application/json", + "https://w3id.org/edc/v0.0.1/ns/prop1": "value1" + } + } + }, + "output": { + "class": "org.eclipse.edc.connector.controlplane.asset.spi.domain.Asset", + "data": { + "id": "asset-1", + "properties": { + "https://w3id.org/edc/v0.0.1/ns/contenttype": "application/json", + "https://w3id.org/edc/v0.0.1/ns/prop1": "value1" + } + } + } +} \ No newline at end of file diff --git a/edc-tests/runtime/mock-connector/src/test/resources/asset.failure.json b/edc-tests/runtime/mock-connector/src/test/resources/asset.failure.json new file mode 100644 index 000000000..22af58351 --- /dev/null +++ b/edc-tests/runtime/mock-connector/src/test/resources/asset.failure.json @@ -0,0 +1,24 @@ +{ + "name": "Some failed asset request", + "description": "test description", + "input": { + "class": "org.eclipse.edc.connector.controlplane.asset.spi.domain.Asset", + "data": { + "id": "asset-1", + "properties": { + "https://w3id.org/edc/v0.0.1/ns/contenttype": "application/json", + "https://w3id.org/edc/v0.0.1/ns/prop1": "value1" + } + } + }, + "output": { + "class": "org.eclipse.edc.connector.controlplane.asset.spi.domain.Asset" + }, + "failure": { + "reason": "UNAUTHORIZED", + "messages": [ + "message 1", + "message 2" + ] + } +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a0bc70e51..0f6418fcc 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -94,6 +94,7 @@ edc-lib-cryptocommon = { module = "org.eclipse.edc:crypto-common-lib", version.r edc-lib-http = { module = "org.eclipse.edc:http-lib", version.ref = "edc" } edc-lib-jersey-providers = { module = "org.eclipse.edc:jersey-providers-lib", version.ref = "edc" } edc-lib-jsonld = { module = "org.eclipse.edc:json-ld-lib", version.ref = "edc" } +edc-lib-json = { module = "org.eclipse.edc:json-lib", version.ref = "edc" } edc-lib-jws2020 = { module = "org.eclipse.edc:jws2020-lib", version.ref = "edc" } edc-lib-policyengine = { module = "org.eclipse.edc:policy-engine-lib", version.ref = "edc" } edc-lib-query = { module = "org.eclipse.edc:query-lib", version.ref = "edc" } diff --git a/samples/testing-with-mocked-connector/build.gradle.kts b/samples/testing-with-mocked-connector/build.gradle.kts new file mode 100644 index 000000000..32607c4d4 --- /dev/null +++ b/samples/testing-with-mocked-connector/build.gradle.kts @@ -0,0 +1,38 @@ +/******************************************************************************** + * Copyright (c) 2023 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.testcontainers.junit) + testImplementation(libs.netty.mockserver) + testImplementation(libs.edc.junit) + testImplementation(libs.restAssured) + testImplementation(libs.awaitility) +} + +// do not publish +edcBuild { + publish.set(false) +} diff --git a/samples/testing-with-mocked-connector/src/test/java/org/eclipse/tractusx/edc/samples/mockedc/UseMockConnectorSampleTest.java b/samples/testing-with-mocked-connector/src/test/java/org/eclipse/tractusx/edc/samples/mockedc/UseMockConnectorSampleTest.java new file mode 100644 index 000000000..853ac577e --- /dev/null +++ b/samples/testing-with-mocked-connector/src/test/java/org/eclipse/tractusx/edc/samples/mockedc/UseMockConnectorSampleTest.java @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft + * + * 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.samples.mockedc; + +import io.restassured.http.ContentType; +import io.restassured.specification.RequestSpecification; +import jakarta.json.JsonArray; +import org.eclipse.edc.junit.annotations.ComponentTest; +import org.eclipse.edc.junit.testfixtures.TestUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import static io.restassured.RestAssured.given; +import static org.assertj.core.api.Assertions.assertThat; + +/** + * This example demonstrates how to use the Mock-Connector as a drop-in replacement runtime for testing client code that uses EDC's + * Management API. While this is written in Java, the concepts are easily translatable into any language where test containers are + * supported. + */ +@Testcontainers +@ComponentTest +public class UseMockConnectorSampleTest { + @Container + protected static GenericContainer edcContainer = new GenericContainer<>("mock-connector") + .withEnv("WEB_HTTP_PORT", "8080") + .withEnv("WEB_HTTP_PATH", "/api") + .withEnv("WEB_HTTP_MANAGEMENT_PORT", "8081") + .withEnv("WEB_HTTP_MANAGEMENT_PATH", "/api/management") + .withExposedPorts(8080, 8081); + private int managementPort; + private int defaultPort; + + @BeforeEach + void setup() { + managementPort = edcContainer.getMappedPort(8081); + defaultPort = edcContainer.getMappedPort(8080); + } + + @Test + void test_getAsset() { + //prime the mock - post a RecordedRequest + setupNextResponse("asset.request.json"); + + // perform the actual Asset API request. In a real test scenario, this would be the client code we're testing, i.e. the + // System-under-Test (SuT). + var assetArray = mgmtRequest() + .contentType(ContentType.JSON) + .body(""" + { + "@context": { + "@vocab": "https://w3id.org/edc/v0.0.1/ns/" + }, + "@type": "QuerySpec" + } + """) + .post("/v3/assets/request") + .then() + .log().ifError() + .statusCode(200) + .extract().body().as(JsonArray.class); + + // assert the response + assertThat(assetArray).hasSize(1); + assertThat(assetArray.get(0).asJsonObject().get("properties")) + .hasFieldOrProperty("prop1") + .hasFieldOrProperty("id") + .hasFieldOrProperty("contenttype"); + } + + @Test + void test_apiNotAuthenticated_expect400() { + //prime the mock - post a RecordedRequest + setupNextResponse("asset.creation.failure.json"); + + // perform the actual Asset API request. In a real test scenario, this would be the client code we're testing, i.e. the + // System-under-Test (SuT). + var assetArray = mgmtRequest() + .contentType(ContentType.JSON) + .body(""" + { + "@context": { + "@vocab": "https://w3id.org/edc/v0.0.1/ns/" + }, + "@type": "QuerySpec" + } + """) + .post("/v3/assets/request") + .then() + .log().ifError() + .statusCode(400) + .extract().body().as(JsonArray.class); + + // assert the response contains error information + assertThat(assetArray).hasSize(1); + var errorObject = assetArray.get(0).asJsonObject(); + assertThat(errorObject.get("message").toString()).contains("This user is not authorized, This is just a second error message"); + } + + private void setupNextResponse(String resourceFileName) { + var json = TestUtils.getResourceFileContentAsString(resourceFileName); + + apiRequest() + .contentType(ContentType.JSON) + .body(json) + .post("/instrumentation") + .then() + .log().ifError() + .statusCode(204); + } + + private RequestSpecification apiRequest() { + return given() + .baseUri("http://localhost:" + defaultPort + "/api") + .when(); + } + + private RequestSpecification mgmtRequest() { + return given() + .baseUri("http://localhost:" + managementPort + "/api/management") + .when(); + } +} diff --git a/samples/testing-with-mocked-connector/src/test/resources/asset.creation.failure.json b/samples/testing-with-mocked-connector/src/test/resources/asset.creation.failure.json new file mode 100644 index 000000000..32d5b8459 --- /dev/null +++ b/samples/testing-with-mocked-connector/src/test/resources/asset.creation.failure.json @@ -0,0 +1,24 @@ +{ + "name": "Some failed asset request", + "description": "test description", + "input": { + "class": "org.eclipse.edc.connector.controlplane.asset.spi.domain.Asset", + "data": { + "id": "asset-1", + "properties": { + "https://w3id.org/edc/v0.0.1/ns/contenttype": "application/json", + "https://w3id.org/edc/v0.0.1/ns/prop1": "value1" + } + } + }, + "output": { + "class": "org.eclipse.edc.connector.controlplane.asset.spi.domain.Asset" + }, + "failure": { + "reason": "UNAUTHORIZED", + "messages": [ + "This user is not authorized", + "This is just a second error message" + ] + } +} \ No newline at end of file diff --git a/samples/testing-with-mocked-connector/src/test/resources/asset.creation.json b/samples/testing-with-mocked-connector/src/test/resources/asset.creation.json new file mode 100644 index 000000000..1f26e3e33 --- /dev/null +++ b/samples/testing-with-mocked-connector/src/test/resources/asset.creation.json @@ -0,0 +1,23 @@ +{ + "name": "Asset Creation V3", + "input": { + "class": "org.eclipse.edc.connector.controlplane.asset.spi.domain.Asset", + "data": { + "id": "asset-1", + "properties": { + "https://w3id.org/edc/v0.0.1/ns/contenttype": "application/json", + "https://w3id.org/edc/v0.0.1/ns/prop1": "value1" + } + } + }, + "output": { + "class": "org.eclipse.edc.connector.controlplane.asset.spi.domain.Asset", + "data": { + "id": "asset-1", + "properties": { + "https://w3id.org/edc/v0.0.1/ns/contenttype": "application/json", + "https://w3id.org/edc/v0.0.1/ns/prop1": "value1" + } + } + } +} \ No newline at end of file diff --git a/samples/testing-with-mocked-connector/src/test/resources/asset.request.json b/samples/testing-with-mocked-connector/src/test/resources/asset.request.json new file mode 100644 index 000000000..1f3dcafd8 --- /dev/null +++ b/samples/testing-with-mocked-connector/src/test/resources/asset.request.json @@ -0,0 +1,22 @@ +{ + "name": "Asset Query v3", + "input": { + "class": "org.eclipse.edc.spi.query.QuerySpec", + "data": { + "offset": 0, + "sortOrder": "DESC" + } + }, + "output": { + "class": "[Lorg.eclipse.edc.connector.controlplane.asset.spi.domain.Asset;", + "data": [ + { + "id": "asset-1", + "properties": { + "https://w3id.org/edc/v0.0.1/ns/contenttype": "application/json", + "https://w3id.org/edc/v0.0.1/ns/prop1": "value1" + } + } + ] + } +} \ No newline at end of file diff --git a/samples/testing-with-mocked-connector/src/test/resources/contractdef.creation.json b/samples/testing-with-mocked-connector/src/test/resources/contractdef.creation.json new file mode 100644 index 000000000..855ca8b1a --- /dev/null +++ b/samples/testing-with-mocked-connector/src/test/resources/contractdef.creation.json @@ -0,0 +1,33 @@ +{ + "name": "Asset Creation V3", + "input": { + "class": "org.eclipse.edc.connector.controlplane.contract.spi.types.offer.ContractDefinition", + "data": { + "id": "asset-1", + "accessPolicyId": "test-policy1", + "contractPolicyId": "test-policy2", + "assetsSelector": [ + { + "operandLeft": "id", + "operator": "=", + "operandRight": "some-asset-id" + } + ] + } + }, + "output": { + "class": "org.eclipse.edc.connector.controlplane.contract.spi.types.offer.ContractDefinition", + "data": { + "id": "asset-1", + "accessPolicyId": "test-policy1", + "contractPolicyId": "test-policy2", + "assetsSelector": [ + { + "operandLeft": "id", + "operator": "=", + "operandRight": "some-asset-id" + } + ] + } + } +} \ No newline at end of file diff --git a/samples/testing-with-mocked-connector/src/test/resources/transferprocess.request.json b/samples/testing-with-mocked-connector/src/test/resources/transferprocess.request.json new file mode 100644 index 000000000..898eea501 --- /dev/null +++ b/samples/testing-with-mocked-connector/src/test/resources/transferprocess.request.json @@ -0,0 +1,21 @@ +{ + "name": "TransferProcess Query v3", + "input": { + "class": "org.eclipse.edc.spi.query.QuerySpec", + "data": { + "offset": 0, + "sortOrder": "DESC" + } + }, + "output": { + "class": "[Lorg.eclipse.edc.connector.controlplane.transfer.spi.types.TransferProcess;", + "data": [ + { + "id": "transferprocess-1", + "type": "CONSUMER", + "assetId": "some-asset-id", + "contractId": "some-contract-id" + } + ] + } +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 980a606c5..9f5dc1e2b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -67,6 +67,7 @@ include(":edc-tests:edc-controlplane:policy-tests") include(":edc-tests:edc-controlplane:fixtures") include(":edc-tests:runtime:extensions") include(":edc-tests:runtime:runtime-memory") +include(":edc-tests:runtime:mock-connector") include(":edc-tests:runtime:dataplane-cloud") include(":edc-tests:runtime:runtime-postgresql") include(":edc-tests:runtime:iatp:runtime-memory-iatp-ih") @@ -93,6 +94,7 @@ include(":edc-dataplane:edc-dataplane-hashicorp-vault") include(":samples:multi-tenancy") +include(":samples:testing-with-mocked-connector") // this is needed to have access to snapshot builds of plugins