From 60a20fe6f703eb5ebd8d96eb1c54180209081ccf Mon Sep 17 00:00:00 2001 From: Enrico Risa Date: Mon, 9 Dec 2024 13:00:11 +0100 Subject: [PATCH] chore: extract base controllers for all DSP apis (#4666) --- .../dsp/dsp-catalog/build.gradle.kts | 1 + .../dsp-catalog-http-api/build.gradle.kts | 7 +- .../controller/DspCatalogApiController.java | 80 +---- .../DspCatalogApiController20241.java | 4 +- .../DspCatalogApiControllerTest.java | 151 +-------- .../dsp-catalog-http-api-lib/build.gradle.kts | 35 +++ .../dsp/catalog/http/api/CatalogApiPaths.java | 0 .../BaseDspCatalogApiController.java | 101 ++++++ .../Base64continuationTokenSerDes.java | 0 .../CatalogPaginationResponseDecorator.java | 0 .../ContinuationTokenManagerImpl.java | 0 .../Base64ContinuationTokenSerDesTest.java | 0 ...atalogPaginationResponseDecoratorTest.java | 0 .../ContinuationTokenManagerImplTest.java | 0 .../DspCatalogApiControllerTestBase.java | 169 ++++++++++ .../dsp/dsp-negotiation/build.gradle.kts | 1 + .../dsp-negotiation-http-api/build.gradle.kts | 3 + .../DspNegotiationApiController.java | 272 +--------------- .../DspNegotiationApiController20241.java | 4 +- .../DspNegotiationApiControllerTest.java | 203 +----------- .../build.gradle.kts | 31 ++ .../http/api/NegotiationApiPaths.java | 0 .../BaseDspNegotiationApiController.java | 297 ++++++++++++++++++ .../DspNegotiationApiControllerTestBase.java | 218 +++++++++++++ .../build.gradle.kts | 2 + .../DspTransferProcessApiController.java | 181 +---------- .../DspTransferProcessApiController20241.java | 4 +- .../DspTransferProcessApiControllerTest.java | 168 +--------- .../build.gradle.kts | 31 ++ .../http/api/TransferProcessApiPaths.java | 0 .../BaseDspTransferProcessApiController.java | 205 ++++++++++++ ...pTransferProcessApiControllerBaseTest.java | 180 +++++++++++ settings.gradle.kts | 5 +- 33 files changed, 1303 insertions(+), 1050 deletions(-) create mode 100644 data-protocols/dsp/dsp-catalog/lib/dsp-catalog-http-api-lib/build.gradle.kts rename data-protocols/dsp/dsp-catalog/{dsp-catalog-http-api => lib/dsp-catalog-http-api-lib}/src/main/java/org/eclipse/edc/protocol/dsp/catalog/http/api/CatalogApiPaths.java (100%) create mode 100644 data-protocols/dsp/dsp-catalog/lib/dsp-catalog-http-api-lib/src/main/java/org/eclipse/edc/protocol/dsp/catalog/http/api/controller/BaseDspCatalogApiController.java rename data-protocols/dsp/dsp-catalog/{dsp-catalog-http-api => lib/dsp-catalog-http-api-lib}/src/main/java/org/eclipse/edc/protocol/dsp/catalog/http/api/decorator/Base64continuationTokenSerDes.java (100%) rename data-protocols/dsp/dsp-catalog/{dsp-catalog-http-api => lib/dsp-catalog-http-api-lib}/src/main/java/org/eclipse/edc/protocol/dsp/catalog/http/api/decorator/CatalogPaginationResponseDecorator.java (100%) rename data-protocols/dsp/dsp-catalog/{dsp-catalog-http-api => lib/dsp-catalog-http-api-lib}/src/main/java/org/eclipse/edc/protocol/dsp/catalog/http/api/decorator/ContinuationTokenManagerImpl.java (100%) rename data-protocols/dsp/dsp-catalog/{dsp-catalog-http-api => lib/dsp-catalog-http-api-lib}/src/test/java/org/eclipse/edc/protocol/dsp/catalog/http/api/decorator/Base64ContinuationTokenSerDesTest.java (100%) rename data-protocols/dsp/dsp-catalog/{dsp-catalog-http-api => lib/dsp-catalog-http-api-lib}/src/test/java/org/eclipse/edc/protocol/dsp/catalog/http/api/decorator/CatalogPaginationResponseDecoratorTest.java (100%) rename data-protocols/dsp/dsp-catalog/{dsp-catalog-http-api => lib/dsp-catalog-http-api-lib}/src/test/java/org/eclipse/edc/protocol/dsp/catalog/http/api/decorator/ContinuationTokenManagerImplTest.java (100%) create mode 100644 data-protocols/dsp/dsp-catalog/lib/dsp-catalog-http-api-lib/src/testFixtures/java/org/eclipse/edc/protocol/dsp/catalog/http/api/controller/DspCatalogApiControllerTestBase.java create mode 100644 data-protocols/dsp/dsp-negotiation/lib/dsp-negotiation-http-api-lib/build.gradle.kts rename data-protocols/dsp/dsp-negotiation/{dsp-negotiation-http-api => lib/dsp-negotiation-http-api-lib}/src/main/java/org/eclipse/edc/protocol/dsp/negotiation/http/api/NegotiationApiPaths.java (100%) create mode 100644 data-protocols/dsp/dsp-negotiation/lib/dsp-negotiation-http-api-lib/src/main/java/org/eclipse/edc/protocol/dsp/negotiation/http/api/controller/BaseDspNegotiationApiController.java create mode 100644 data-protocols/dsp/dsp-negotiation/lib/dsp-negotiation-http-api-lib/src/testFixtures/java/org/eclipse/edc/protocol/dsp/negotiation/http/api/controller/DspNegotiationApiControllerTestBase.java create mode 100644 data-protocols/dsp/dsp-transfer-process/lib/dsp-transfer-process-http-api-lib/build.gradle.kts rename data-protocols/dsp/dsp-transfer-process/{dsp-transfer-process-http-api => lib/dsp-transfer-process-http-api-lib}/src/main/java/org/eclipse/edc/protocol/dsp/transferprocess/http/api/TransferProcessApiPaths.java (100%) create mode 100644 data-protocols/dsp/dsp-transfer-process/lib/dsp-transfer-process-http-api-lib/src/main/java/org/eclipse/edc/protocol/dsp/transferprocess/http/api/controller/BaseDspTransferProcessApiController.java create mode 100644 data-protocols/dsp/dsp-transfer-process/lib/dsp-transfer-process-http-api-lib/src/testFixtures/java/org/eclipse/edc/protocol/dsp/transferprocess/http/api/controller/DspTransferProcessApiControllerBaseTest.java diff --git a/data-protocols/dsp/dsp-catalog/build.gradle.kts b/data-protocols/dsp/dsp-catalog/build.gradle.kts index d02db7cb805..02404ab64d5 100644 --- a/data-protocols/dsp/dsp-catalog/build.gradle.kts +++ b/data-protocols/dsp/dsp-catalog/build.gradle.kts @@ -23,4 +23,5 @@ dependencies { api(project(":data-protocols:dsp:dsp-catalog:dsp-catalog-transform")) api(project(":data-protocols:dsp:dsp-catalog:lib:dsp-catalog-validation-lib")) api(project(":data-protocols:dsp:dsp-catalog:lib:dsp-catalog-transform-lib")) + api(project(":data-protocols:dsp:dsp-catalog:lib:dsp-catalog-http-api-lib")) } diff --git a/data-protocols/dsp/dsp-catalog/dsp-catalog-http-api/build.gradle.kts b/data-protocols/dsp/dsp-catalog/dsp-catalog-http-api/build.gradle.kts index e934dd7de90..18907200407 100644 --- a/data-protocols/dsp/dsp-catalog/dsp-catalog-http-api/build.gradle.kts +++ b/data-protocols/dsp/dsp-catalog/dsp-catalog-http-api/build.gradle.kts @@ -25,17 +25,18 @@ dependencies { api(project(":spi:common:json-ld-spi")) api(project(":spi:control-plane:control-plane-spi")) - + implementation(project(":extensions:common:http:lib:jersey-providers-lib")) implementation(project(":data-protocols:dsp:dsp-catalog:lib:dsp-catalog-validation-lib")) + implementation(project(":data-protocols:dsp:dsp-catalog:lib:dsp-catalog-http-api-lib")) implementation(libs.jakarta.rsApi) testImplementation(testFixtures(project(":extensions:common:http:jersey-core"))) testImplementation(project(":core:common:junit")) - testImplementation(project(":core:common:lib:transform-lib")) - testImplementation(project(":data-protocols:dsp:dsp-catalog:dsp-catalog-transform")) testImplementation(libs.restAssured) + testImplementation(testFixtures(project(":data-protocols:dsp:dsp-catalog:lib:dsp-catalog-http-api-lib"))) + } edcBuild { diff --git a/data-protocols/dsp/dsp-catalog/dsp-catalog-http-api/src/main/java/org/eclipse/edc/protocol/dsp/catalog/http/api/controller/DspCatalogApiController.java b/data-protocols/dsp/dsp-catalog/dsp-catalog-http-api/src/main/java/org/eclipse/edc/protocol/dsp/catalog/http/api/controller/DspCatalogApiController.java index 2edc2014c64..e85bcfafc75 100644 --- a/data-protocols/dsp/dsp-catalog/dsp-catalog-http-api/src/main/java/org/eclipse/edc/protocol/dsp/catalog/http/api/controller/DspCatalogApiController.java +++ b/data-protocols/dsp/dsp-catalog/dsp-catalog-http-api/src/main/java/org/eclipse/edc/protocol/dsp/catalog/http/api/controller/DspCatalogApiController.java @@ -14,37 +14,16 @@ package org.eclipse.edc.protocol.dsp.catalog.http.api.controller; -import jakarta.json.JsonObject; -import jakarta.ws.rs.BadRequestException; import jakarta.ws.rs.Consumes; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.HeaderParam; -import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; -import jakarta.ws.rs.PathParam; import jakarta.ws.rs.Produces; -import jakarta.ws.rs.QueryParam; -import jakarta.ws.rs.core.Context; -import jakarta.ws.rs.core.Response; -import jakarta.ws.rs.core.UriInfo; -import org.eclipse.edc.connector.controlplane.catalog.spi.Catalog; -import org.eclipse.edc.connector.controlplane.catalog.spi.CatalogError; -import org.eclipse.edc.connector.controlplane.catalog.spi.CatalogRequestMessage; -import org.eclipse.edc.connector.controlplane.catalog.spi.Dataset; import org.eclipse.edc.connector.controlplane.services.spi.catalog.CatalogProtocolService; -import org.eclipse.edc.jsonld.spi.JsonLdNamespace; import org.eclipse.edc.protocol.dsp.http.spi.message.ContinuationTokenManager; import org.eclipse.edc.protocol.dsp.http.spi.message.DspRequestHandler; -import org.eclipse.edc.protocol.dsp.http.spi.message.GetDspRequest; -import org.eclipse.edc.protocol.dsp.http.spi.message.PostDspRequest; -import static jakarta.ws.rs.core.HttpHeaders.AUTHORIZATION; import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; import static org.eclipse.edc.protocol.dsp.catalog.http.api.CatalogApiPaths.BASE_PATH; -import static org.eclipse.edc.protocol.dsp.catalog.http.api.CatalogApiPaths.CATALOG_REQUEST; -import static org.eclipse.edc.protocol.dsp.catalog.http.api.CatalogApiPaths.DATASET_REQUEST; import static org.eclipse.edc.protocol.dsp.http.spi.types.HttpMessageProtocol.DATASPACE_PROTOCOL_HTTP; -import static org.eclipse.edc.protocol.dsp.spi.type.DspCatalogPropertyAndTypeNames.DSPACE_TYPE_CATALOG_REQUEST_MESSAGE_TERM; import static org.eclipse.edc.protocol.dsp.spi.type.DspConstants.DSP_NAMESPACE_V_08; /** @@ -53,63 +32,10 @@ @Consumes({ APPLICATION_JSON }) @Produces({ APPLICATION_JSON }) @Path(BASE_PATH) -public class DspCatalogApiController { - - private final CatalogProtocolService service; - private final DspRequestHandler dspRequestHandler; - private final ContinuationTokenManager continuationTokenManager; - private final String protocol; - private final JsonLdNamespace namespace; - +public class DspCatalogApiController extends BaseDspCatalogApiController { + public DspCatalogApiController(CatalogProtocolService service, DspRequestHandler dspRequestHandler, ContinuationTokenManager continuationTokenManager) { - this(service, dspRequestHandler, continuationTokenManager, DATASPACE_PROTOCOL_HTTP, DSP_NAMESPACE_V_08); - } - - public DspCatalogApiController(CatalogProtocolService service, DspRequestHandler dspRequestHandler, ContinuationTokenManager continuationTokenManager, String protocol, JsonLdNamespace namespace) { - this.service = service; - this.dspRequestHandler = dspRequestHandler; - this.continuationTokenManager = continuationTokenManager; - this.protocol = protocol; - this.namespace = namespace; - } - - @POST - @Path(CATALOG_REQUEST) - public Response requestCatalog(JsonObject jsonObject, @HeaderParam(AUTHORIZATION) String token, @Context UriInfo uriInfo, - @QueryParam("continuationToken") String continuationToken) { - JsonObject messageJson; - if (continuationToken == null) { - messageJson = jsonObject; - } else { - messageJson = continuationTokenManager.applyQueryFromToken(jsonObject, continuationToken) - .orElseThrow(f -> new BadRequestException(f.getFailureDetail())); - } - - var request = PostDspRequest.Builder.newInstance(CatalogRequestMessage.class, Catalog.class, CatalogError.class) - .token(token) - .expectedMessageType(namespace.toIri(DSPACE_TYPE_CATALOG_REQUEST_MESSAGE_TERM)) - .message(messageJson) - .serviceCall(service::getCatalog) - .errorProvider(CatalogError.Builder::newInstance) - .protocol(protocol) - .build(); - - var responseDecorator = continuationTokenManager.createResponseDecorator(uriInfo.getAbsolutePath().toString()); - return dspRequestHandler.createResource(request, responseDecorator); - } - - @GET - @Path(DATASET_REQUEST + "/{id}") - public Response getDataset(@PathParam("id") String id, @HeaderParam(AUTHORIZATION) String token) { - var request = GetDspRequest.Builder.newInstance(Dataset.class, CatalogError.class) - .token(token) - .id(id) - .serviceCall(service::getDataset) - .errorProvider(CatalogError.Builder::newInstance) - .protocol(protocol) - .build(); - - return dspRequestHandler.getResource(request); + super(service, dspRequestHandler, continuationTokenManager, DATASPACE_PROTOCOL_HTTP, DSP_NAMESPACE_V_08); } } diff --git a/data-protocols/dsp/dsp-catalog/dsp-catalog-http-api/src/main/java/org/eclipse/edc/protocol/dsp/catalog/http/api/controller/DspCatalogApiController20241.java b/data-protocols/dsp/dsp-catalog/dsp-catalog-http-api/src/main/java/org/eclipse/edc/protocol/dsp/catalog/http/api/controller/DspCatalogApiController20241.java index 9544e08b9b2..2d5426429a1 100644 --- a/data-protocols/dsp/dsp-catalog/dsp-catalog-http-api/src/main/java/org/eclipse/edc/protocol/dsp/catalog/http/api/controller/DspCatalogApiController20241.java +++ b/data-protocols/dsp/dsp-catalog/dsp-catalog-http-api/src/main/java/org/eclipse/edc/protocol/dsp/catalog/http/api/controller/DspCatalogApiController20241.java @@ -28,12 +28,12 @@ import static org.eclipse.edc.protocol.dsp.spi.version.DspVersions.V_2024_1_PATH; /** - * Versioned Catalog endpoint, same as {@link DspCatalogApiController} but exposed on the /2024/1 path + * Versioned Catalog endpoint for 2024/1 protocol version */ @Consumes({ APPLICATION_JSON }) @Produces({ APPLICATION_JSON }) @Path(V_2024_1_PATH + BASE_PATH) -public class DspCatalogApiController20241 extends DspCatalogApiController { +public class DspCatalogApiController20241 extends BaseDspCatalogApiController { public DspCatalogApiController20241(CatalogProtocolService service, DspRequestHandler dspRequestHandler, ContinuationTokenManager responseDecorator) { diff --git a/data-protocols/dsp/dsp-catalog/dsp-catalog-http-api/src/test/java/org/eclipse/edc/protocol/dsp/catalog/http/api/controller/DspCatalogApiControllerTest.java b/data-protocols/dsp/dsp-catalog/dsp-catalog-http-api/src/test/java/org/eclipse/edc/protocol/dsp/catalog/http/api/controller/DspCatalogApiControllerTest.java index 957db430896..ba91f4d51ab 100644 --- a/data-protocols/dsp/dsp-catalog/dsp-catalog-http-api/src/test/java/org/eclipse/edc/protocol/dsp/catalog/http/api/controller/DspCatalogApiControllerTest.java +++ b/data-protocols/dsp/dsp-catalog/dsp-catalog-http-api/src/test/java/org/eclipse/edc/protocol/dsp/catalog/http/api/controller/DspCatalogApiControllerTest.java @@ -14,168 +14,21 @@ package org.eclipse.edc.protocol.dsp.catalog.http.api.controller; -import io.restassured.specification.RequestSpecification; -import jakarta.json.Json; -import jakarta.json.JsonObject; -import jakarta.ws.rs.core.HttpHeaders; -import jakarta.ws.rs.core.Response; -import org.eclipse.edc.connector.controlplane.catalog.spi.Catalog; -import org.eclipse.edc.connector.controlplane.catalog.spi.CatalogRequestMessage; -import org.eclipse.edc.connector.controlplane.catalog.spi.Dataset; -import org.eclipse.edc.connector.controlplane.services.spi.catalog.CatalogProtocolService; -import org.eclipse.edc.jsonld.spi.JsonLdKeywords; import org.eclipse.edc.jsonld.spi.JsonLdNamespace; import org.eclipse.edc.junit.annotations.ApiTest; -import org.eclipse.edc.protocol.dsp.http.spi.message.ContinuationTokenManager; -import org.eclipse.edc.protocol.dsp.http.spi.message.DspRequestHandler; -import org.eclipse.edc.protocol.dsp.http.spi.message.GetDspRequest; -import org.eclipse.edc.protocol.dsp.http.spi.message.PostDspRequest; -import org.eclipse.edc.protocol.dsp.http.spi.message.ResponseDecorator; -import org.eclipse.edc.spi.result.Result; -import org.eclipse.edc.transform.spi.TypeTransformerRegistry; -import org.eclipse.edc.web.jersey.testfixtures.RestControllerTestBase; import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.mockito.ArgumentCaptor; -import static io.restassured.RestAssured.given; -import static io.restassured.http.ContentType.JSON; -import static jakarta.json.Json.createObjectBuilder; -import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; -import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON_TYPE; -import static org.assertj.core.api.Assertions.assertThat; -import static org.eclipse.edc.jsonld.spi.JsonLdKeywords.TYPE; import static org.eclipse.edc.protocol.dsp.catalog.http.api.CatalogApiPaths.BASE_PATH; -import static org.eclipse.edc.protocol.dsp.catalog.http.api.CatalogApiPaths.CATALOG_REQUEST; -import static org.eclipse.edc.protocol.dsp.catalog.http.api.CatalogApiPaths.DATASET_REQUEST; -import static org.eclipse.edc.protocol.dsp.spi.type.DspCatalogPropertyAndTypeNames.DSPACE_TYPE_CATALOG_REQUEST_MESSAGE_TERM; import static org.eclipse.edc.protocol.dsp.spi.type.DspConstants.DSP_NAMESPACE_V_08; import static org.eclipse.edc.protocol.dsp.spi.type.DspConstants.DSP_NAMESPACE_V_2024_1; import static org.eclipse.edc.protocol.dsp.spi.version.DspVersions.V_2024_1_PATH; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.isA; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoInteractions; -import static org.mockito.Mockito.when; class DspCatalogApiControllerTest { - abstract static class Tests extends RestControllerTestBase { - - protected final TypeTransformerRegistry transformerRegistry = mock(); - protected final CatalogProtocolService service = mock(); - protected final DspRequestHandler dspRequestHandler = mock(); - protected final ContinuationTokenManager continuationTokenManager = mock(); - - @Test - void getDataset_shouldGetResource() { - when(dspRequestHandler.getResource(any())).thenReturn(Response.ok().type(APPLICATION_JSON).build()); - - baseRequest() - .get(DATASET_REQUEST + "/datasetId") - .then() - .statusCode(200) - .contentType(JSON); - - var captor = ArgumentCaptor.forClass(GetDspRequest.class); - verify(dspRequestHandler).getResource(captor.capture()); - var request = captor.getValue(); - assertThat(request.getToken()).isEqualTo("auth"); - assertThat(request.getResultClass()).isEqualTo(Dataset.class); - assertThat(request.getId()).isEqualTo("datasetId"); - } - - - protected abstract String basePath(); - - protected abstract JsonLdNamespace namespace(); - - private RequestSpecification baseRequest() { - return given() - .baseUri("http://localhost:" + port) - .basePath(basePath()) - .header(HttpHeaders.AUTHORIZATION, "auth") - .when(); - } - - @Nested - class RequestCatalog { - - @Test - void shouldCreateResource() { - var requestBody = createObjectBuilder().add(TYPE, namespace().toIri(DSPACE_TYPE_CATALOG_REQUEST_MESSAGE_TERM)).build(); - var catalog = createObjectBuilder().add(JsonLdKeywords.TYPE, "catalog").build(); - when(transformerRegistry.transform(any(Catalog.class), eq(JsonObject.class))).thenReturn(Result.success(catalog)); - when(dspRequestHandler.createResource(any(), any())).thenReturn(Response.ok().type(APPLICATION_JSON_TYPE).build()); - when(continuationTokenManager.createResponseDecorator(any())).thenReturn(mock()); - - baseRequest() - .contentType(JSON) - .body(requestBody) - .post(CATALOG_REQUEST) - .then() - .statusCode(200) - .contentType(JSON); - - var captor = ArgumentCaptor.forClass(PostDspRequest.class); - verify(dspRequestHandler).createResource(captor.capture(), isA(ResponseDecorator.class)); - var request = captor.getValue(); - assertThat(request.getInputClass()).isEqualTo(CatalogRequestMessage.class); - assertThat(request.getResultClass()).isEqualTo(Catalog.class); - assertThat(request.getExpectedMessageType()).isEqualTo(namespace().toIri(DSPACE_TYPE_CATALOG_REQUEST_MESSAGE_TERM)); - assertThat(request.getProcessId()).isNull(); - assertThat(request.getToken()).isEqualTo("auth"); - assertThat(request.getMessage()).isEqualTo(requestBody); - verify(continuationTokenManager).createResponseDecorator("http://localhost:%d%s".formatted(port, basePath() + CATALOG_REQUEST)); - } - - @Test - void shouldApplyContinuationToken_whenPassed() { - var requestBody = createObjectBuilder().add(TYPE, namespace().toIri(DSPACE_TYPE_CATALOG_REQUEST_MESSAGE_TERM)).build(); - var catalog = createObjectBuilder().add(JsonLdKeywords.TYPE, "catalog").build(); - when(transformerRegistry.transform(any(Catalog.class), eq(JsonObject.class))).thenReturn(Result.success(catalog)); - when(dspRequestHandler.createResource(any(), any())).thenReturn(Response.ok().type(APPLICATION_JSON_TYPE).build()); - when(continuationTokenManager.createResponseDecorator(any())).thenReturn(mock()); - var enrichedRequestBody = createObjectBuilder(requestBody).add("query", Json.createObjectBuilder()).build(); - when(continuationTokenManager.applyQueryFromToken(any(), any())).thenReturn(Result.success(enrichedRequestBody)); - - baseRequest() - .contentType(JSON) - .body(requestBody) - .post(CATALOG_REQUEST + "?continuationToken=pagination-token") - .then() - .statusCode(200) - .contentType(JSON); - - var captor = ArgumentCaptor.forClass(PostDspRequest.class); - verify(dspRequestHandler).createResource(captor.capture(), isA(ResponseDecorator.class)); - var request = captor.getValue(); - assertThat(request.getMessage()).isSameAs(enrichedRequestBody); - verify(continuationTokenManager).applyQueryFromToken(requestBody, "pagination-token"); - } - - @Test - void shouldReturnBadRequest_whenContinuationTokenIsNotValid() { - var requestBody = createObjectBuilder().add(TYPE, namespace().toIri(DSPACE_TYPE_CATALOG_REQUEST_MESSAGE_TERM)).build(); - when(continuationTokenManager.applyQueryFromToken(any(), any())).thenReturn(Result.failure("error")); - - baseRequest() - .contentType(JSON) - .body(requestBody) - .post(CATALOG_REQUEST + "?continuationToken=pagination-token") - .then() - .statusCode(400); - - verifyNoInteractions(dspRequestHandler, transformerRegistry); - } - } - } @ApiTest @Nested - class DspCatalogApiControllerV08Test extends Tests { + class DspCatalogApiControllerV08Test extends DspCatalogApiControllerTestBase { @Override protected String basePath() { @@ -195,7 +48,7 @@ protected Object controller() { @ApiTest @Nested - class DspCatalogApiControllerV20241Test extends Tests { + class DspCatalogApiControllerV20241Test extends DspCatalogApiControllerTestBase { @Override protected String basePath() { diff --git a/data-protocols/dsp/dsp-catalog/lib/dsp-catalog-http-api-lib/build.gradle.kts b/data-protocols/dsp/dsp-catalog/lib/dsp-catalog-http-api-lib/build.gradle.kts new file mode 100644 index 00000000000..db9e00e36aa --- /dev/null +++ b/data-protocols/dsp/dsp-catalog/lib/dsp-catalog-http-api-lib/build.gradle.kts @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +plugins { + `java-library` + `java-test-fixtures` +} + +dependencies { + api(project(":data-protocols:dsp:dsp-spi")) + api(project(":data-protocols:dsp:dsp-http-spi")) + api(project(":spi:common:json-ld-spi")) + + testImplementation(project(":core:common:lib:transform-lib")) + testImplementation(project(":data-protocols:dsp:dsp-catalog:dsp-catalog-transform")) + testImplementation(testFixtures(project(":extensions:common:http:jersey-core"))) + testImplementation(libs.restAssured) + + testFixturesApi(project(":core:common:junit")) + testFixturesImplementation(testFixtures(project(":extensions:common:http:jersey-core"))) + testFixturesImplementation(libs.restAssured) + testFixturesImplementation(libs.assertj) + testFixturesImplementation(libs.mockito.core) +} \ No newline at end of file diff --git a/data-protocols/dsp/dsp-catalog/dsp-catalog-http-api/src/main/java/org/eclipse/edc/protocol/dsp/catalog/http/api/CatalogApiPaths.java b/data-protocols/dsp/dsp-catalog/lib/dsp-catalog-http-api-lib/src/main/java/org/eclipse/edc/protocol/dsp/catalog/http/api/CatalogApiPaths.java similarity index 100% rename from data-protocols/dsp/dsp-catalog/dsp-catalog-http-api/src/main/java/org/eclipse/edc/protocol/dsp/catalog/http/api/CatalogApiPaths.java rename to data-protocols/dsp/dsp-catalog/lib/dsp-catalog-http-api-lib/src/main/java/org/eclipse/edc/protocol/dsp/catalog/http/api/CatalogApiPaths.java diff --git a/data-protocols/dsp/dsp-catalog/lib/dsp-catalog-http-api-lib/src/main/java/org/eclipse/edc/protocol/dsp/catalog/http/api/controller/BaseDspCatalogApiController.java b/data-protocols/dsp/dsp-catalog/lib/dsp-catalog-http-api-lib/src/main/java/org/eclipse/edc/protocol/dsp/catalog/http/api/controller/BaseDspCatalogApiController.java new file mode 100644 index 00000000000..33b2866b378 --- /dev/null +++ b/data-protocols/dsp/dsp-catalog/lib/dsp-catalog-http-api-lib/src/main/java/org/eclipse/edc/protocol/dsp/catalog/http/api/controller/BaseDspCatalogApiController.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.edc.protocol.dsp.catalog.http.api.controller; + +import jakarta.json.JsonObject; +import jakarta.ws.rs.BadRequestException; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.HeaderParam; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.UriInfo; +import org.eclipse.edc.connector.controlplane.catalog.spi.Catalog; +import org.eclipse.edc.connector.controlplane.catalog.spi.CatalogError; +import org.eclipse.edc.connector.controlplane.catalog.spi.CatalogRequestMessage; +import org.eclipse.edc.connector.controlplane.catalog.spi.Dataset; +import org.eclipse.edc.connector.controlplane.services.spi.catalog.CatalogProtocolService; +import org.eclipse.edc.jsonld.spi.JsonLdNamespace; +import org.eclipse.edc.protocol.dsp.http.spi.message.ContinuationTokenManager; +import org.eclipse.edc.protocol.dsp.http.spi.message.DspRequestHandler; +import org.eclipse.edc.protocol.dsp.http.spi.message.GetDspRequest; +import org.eclipse.edc.protocol.dsp.http.spi.message.PostDspRequest; + +import static jakarta.ws.rs.core.HttpHeaders.AUTHORIZATION; +import static org.eclipse.edc.protocol.dsp.catalog.http.api.CatalogApiPaths.CATALOG_REQUEST; +import static org.eclipse.edc.protocol.dsp.catalog.http.api.CatalogApiPaths.DATASET_REQUEST; +import static org.eclipse.edc.protocol.dsp.spi.type.DspCatalogPropertyAndTypeNames.DSPACE_TYPE_CATALOG_REQUEST_MESSAGE_TERM; + + +public abstract class BaseDspCatalogApiController { + + private final CatalogProtocolService service; + private final DspRequestHandler dspRequestHandler; + private final ContinuationTokenManager continuationTokenManager; + private final String protocol; + private final JsonLdNamespace namespace; + + + public BaseDspCatalogApiController(CatalogProtocolService service, DspRequestHandler dspRequestHandler, ContinuationTokenManager continuationTokenManager, String protocol, JsonLdNamespace namespace) { + this.service = service; + this.dspRequestHandler = dspRequestHandler; + this.continuationTokenManager = continuationTokenManager; + this.protocol = protocol; + this.namespace = namespace; + } + + @POST + @Path(CATALOG_REQUEST) + public Response requestCatalog(JsonObject jsonObject, @HeaderParam(AUTHORIZATION) String token, @Context UriInfo uriInfo, + @QueryParam("continuationToken") String continuationToken) { + JsonObject messageJson; + if (continuationToken == null) { + messageJson = jsonObject; + } else { + messageJson = continuationTokenManager.applyQueryFromToken(jsonObject, continuationToken) + .orElseThrow(f -> new BadRequestException(f.getFailureDetail())); + } + + var request = PostDspRequest.Builder.newInstance(CatalogRequestMessage.class, Catalog.class, CatalogError.class) + .token(token) + .expectedMessageType(namespace.toIri(DSPACE_TYPE_CATALOG_REQUEST_MESSAGE_TERM)) + .message(messageJson) + .serviceCall(service::getCatalog) + .errorProvider(CatalogError.Builder::newInstance) + .protocol(protocol) + .build(); + + var responseDecorator = continuationTokenManager.createResponseDecorator(uriInfo.getAbsolutePath().toString()); + return dspRequestHandler.createResource(request, responseDecorator); + } + + @GET + @Path(DATASET_REQUEST + "/{id}") + public Response getDataset(@PathParam("id") String id, @HeaderParam(AUTHORIZATION) String token) { + var request = GetDspRequest.Builder.newInstance(Dataset.class, CatalogError.class) + .token(token) + .id(id) + .serviceCall(service::getDataset) + .errorProvider(CatalogError.Builder::newInstance) + .protocol(protocol) + .build(); + + return dspRequestHandler.getResource(request); + } + +} diff --git a/data-protocols/dsp/dsp-catalog/dsp-catalog-http-api/src/main/java/org/eclipse/edc/protocol/dsp/catalog/http/api/decorator/Base64continuationTokenSerDes.java b/data-protocols/dsp/dsp-catalog/lib/dsp-catalog-http-api-lib/src/main/java/org/eclipse/edc/protocol/dsp/catalog/http/api/decorator/Base64continuationTokenSerDes.java similarity index 100% rename from data-protocols/dsp/dsp-catalog/dsp-catalog-http-api/src/main/java/org/eclipse/edc/protocol/dsp/catalog/http/api/decorator/Base64continuationTokenSerDes.java rename to data-protocols/dsp/dsp-catalog/lib/dsp-catalog-http-api-lib/src/main/java/org/eclipse/edc/protocol/dsp/catalog/http/api/decorator/Base64continuationTokenSerDes.java diff --git a/data-protocols/dsp/dsp-catalog/dsp-catalog-http-api/src/main/java/org/eclipse/edc/protocol/dsp/catalog/http/api/decorator/CatalogPaginationResponseDecorator.java b/data-protocols/dsp/dsp-catalog/lib/dsp-catalog-http-api-lib/src/main/java/org/eclipse/edc/protocol/dsp/catalog/http/api/decorator/CatalogPaginationResponseDecorator.java similarity index 100% rename from data-protocols/dsp/dsp-catalog/dsp-catalog-http-api/src/main/java/org/eclipse/edc/protocol/dsp/catalog/http/api/decorator/CatalogPaginationResponseDecorator.java rename to data-protocols/dsp/dsp-catalog/lib/dsp-catalog-http-api-lib/src/main/java/org/eclipse/edc/protocol/dsp/catalog/http/api/decorator/CatalogPaginationResponseDecorator.java diff --git a/data-protocols/dsp/dsp-catalog/dsp-catalog-http-api/src/main/java/org/eclipse/edc/protocol/dsp/catalog/http/api/decorator/ContinuationTokenManagerImpl.java b/data-protocols/dsp/dsp-catalog/lib/dsp-catalog-http-api-lib/src/main/java/org/eclipse/edc/protocol/dsp/catalog/http/api/decorator/ContinuationTokenManagerImpl.java similarity index 100% rename from data-protocols/dsp/dsp-catalog/dsp-catalog-http-api/src/main/java/org/eclipse/edc/protocol/dsp/catalog/http/api/decorator/ContinuationTokenManagerImpl.java rename to data-protocols/dsp/dsp-catalog/lib/dsp-catalog-http-api-lib/src/main/java/org/eclipse/edc/protocol/dsp/catalog/http/api/decorator/ContinuationTokenManagerImpl.java diff --git a/data-protocols/dsp/dsp-catalog/dsp-catalog-http-api/src/test/java/org/eclipse/edc/protocol/dsp/catalog/http/api/decorator/Base64ContinuationTokenSerDesTest.java b/data-protocols/dsp/dsp-catalog/lib/dsp-catalog-http-api-lib/src/test/java/org/eclipse/edc/protocol/dsp/catalog/http/api/decorator/Base64ContinuationTokenSerDesTest.java similarity index 100% rename from data-protocols/dsp/dsp-catalog/dsp-catalog-http-api/src/test/java/org/eclipse/edc/protocol/dsp/catalog/http/api/decorator/Base64ContinuationTokenSerDesTest.java rename to data-protocols/dsp/dsp-catalog/lib/dsp-catalog-http-api-lib/src/test/java/org/eclipse/edc/protocol/dsp/catalog/http/api/decorator/Base64ContinuationTokenSerDesTest.java diff --git a/data-protocols/dsp/dsp-catalog/dsp-catalog-http-api/src/test/java/org/eclipse/edc/protocol/dsp/catalog/http/api/decorator/CatalogPaginationResponseDecoratorTest.java b/data-protocols/dsp/dsp-catalog/lib/dsp-catalog-http-api-lib/src/test/java/org/eclipse/edc/protocol/dsp/catalog/http/api/decorator/CatalogPaginationResponseDecoratorTest.java similarity index 100% rename from data-protocols/dsp/dsp-catalog/dsp-catalog-http-api/src/test/java/org/eclipse/edc/protocol/dsp/catalog/http/api/decorator/CatalogPaginationResponseDecoratorTest.java rename to data-protocols/dsp/dsp-catalog/lib/dsp-catalog-http-api-lib/src/test/java/org/eclipse/edc/protocol/dsp/catalog/http/api/decorator/CatalogPaginationResponseDecoratorTest.java diff --git a/data-protocols/dsp/dsp-catalog/dsp-catalog-http-api/src/test/java/org/eclipse/edc/protocol/dsp/catalog/http/api/decorator/ContinuationTokenManagerImplTest.java b/data-protocols/dsp/dsp-catalog/lib/dsp-catalog-http-api-lib/src/test/java/org/eclipse/edc/protocol/dsp/catalog/http/api/decorator/ContinuationTokenManagerImplTest.java similarity index 100% rename from data-protocols/dsp/dsp-catalog/dsp-catalog-http-api/src/test/java/org/eclipse/edc/protocol/dsp/catalog/http/api/decorator/ContinuationTokenManagerImplTest.java rename to data-protocols/dsp/dsp-catalog/lib/dsp-catalog-http-api-lib/src/test/java/org/eclipse/edc/protocol/dsp/catalog/http/api/decorator/ContinuationTokenManagerImplTest.java diff --git a/data-protocols/dsp/dsp-catalog/lib/dsp-catalog-http-api-lib/src/testFixtures/java/org/eclipse/edc/protocol/dsp/catalog/http/api/controller/DspCatalogApiControllerTestBase.java b/data-protocols/dsp/dsp-catalog/lib/dsp-catalog-http-api-lib/src/testFixtures/java/org/eclipse/edc/protocol/dsp/catalog/http/api/controller/DspCatalogApiControllerTestBase.java new file mode 100644 index 00000000000..a3c2f3ed077 --- /dev/null +++ b/data-protocols/dsp/dsp-catalog/lib/dsp-catalog-http-api-lib/src/testFixtures/java/org/eclipse/edc/protocol/dsp/catalog/http/api/controller/DspCatalogApiControllerTestBase.java @@ -0,0 +1,169 @@ +/* + * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.edc.protocol.dsp.catalog.http.api.controller; + +import io.restassured.specification.RequestSpecification; +import jakarta.json.Json; +import jakarta.json.JsonObject; +import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.Response; +import org.eclipse.edc.connector.controlplane.catalog.spi.Catalog; +import org.eclipse.edc.connector.controlplane.catalog.spi.CatalogRequestMessage; +import org.eclipse.edc.connector.controlplane.catalog.spi.Dataset; +import org.eclipse.edc.connector.controlplane.services.spi.catalog.CatalogProtocolService; +import org.eclipse.edc.jsonld.spi.JsonLdKeywords; +import org.eclipse.edc.jsonld.spi.JsonLdNamespace; +import org.eclipse.edc.protocol.dsp.http.spi.message.ContinuationTokenManager; +import org.eclipse.edc.protocol.dsp.http.spi.message.DspRequestHandler; +import org.eclipse.edc.protocol.dsp.http.spi.message.GetDspRequest; +import org.eclipse.edc.protocol.dsp.http.spi.message.PostDspRequest; +import org.eclipse.edc.protocol.dsp.http.spi.message.ResponseDecorator; +import org.eclipse.edc.spi.result.Result; +import org.eclipse.edc.transform.spi.TypeTransformerRegistry; +import org.eclipse.edc.web.jersey.testfixtures.RestControllerTestBase; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +import static io.restassured.RestAssured.given; +import static io.restassured.http.ContentType.JSON; +import static jakarta.json.Json.createObjectBuilder; +import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; +import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON_TYPE; +import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.edc.jsonld.spi.JsonLdKeywords.TYPE; +import static org.eclipse.edc.protocol.dsp.catalog.http.api.CatalogApiPaths.CATALOG_REQUEST; +import static org.eclipse.edc.protocol.dsp.catalog.http.api.CatalogApiPaths.DATASET_REQUEST; +import static org.eclipse.edc.protocol.dsp.spi.type.DspCatalogPropertyAndTypeNames.DSPACE_TYPE_CATALOG_REQUEST_MESSAGE_TERM; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isA; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +public abstract class DspCatalogApiControllerTestBase extends RestControllerTestBase { + + + protected final TypeTransformerRegistry transformerRegistry = mock(); + protected final CatalogProtocolService service = mock(); + protected final DspRequestHandler dspRequestHandler = mock(); + protected final ContinuationTokenManager continuationTokenManager = mock(); + + @Test + void getDataset_shouldGetResource() { + when(dspRequestHandler.getResource(any())).thenReturn(Response.ok().type(APPLICATION_JSON).build()); + + baseRequest() + .get(DATASET_REQUEST + "/datasetId") + .then() + .statusCode(200) + .contentType(JSON); + + var captor = ArgumentCaptor.forClass(GetDspRequest.class); + verify(dspRequestHandler).getResource(captor.capture()); + var request = captor.getValue(); + assertThat(request.getToken()).isEqualTo("auth"); + assertThat(request.getResultClass()).isEqualTo(Dataset.class); + assertThat(request.getId()).isEqualTo("datasetId"); + } + + + protected abstract String basePath(); + + protected abstract JsonLdNamespace namespace(); + + private RequestSpecification baseRequest() { + return given() + .baseUri("http://localhost:" + port) + .basePath(basePath()) + .header(HttpHeaders.AUTHORIZATION, "auth") + .when(); + } + + @Nested + class RequestCatalog { + + @Test + void shouldCreateResource() { + var requestBody = createObjectBuilder().add(TYPE, namespace().toIri(DSPACE_TYPE_CATALOG_REQUEST_MESSAGE_TERM)).build(); + var catalog = createObjectBuilder().add(JsonLdKeywords.TYPE, "catalog").build(); + when(transformerRegistry.transform(any(Catalog.class), eq(JsonObject.class))).thenReturn(Result.success(catalog)); + when(dspRequestHandler.createResource(any(), any())).thenReturn(Response.ok().type(APPLICATION_JSON_TYPE).build()); + when(continuationTokenManager.createResponseDecorator(any())).thenReturn(mock()); + + baseRequest() + .contentType(JSON) + .body(requestBody) + .post(CATALOG_REQUEST) + .then() + .statusCode(200) + .contentType(JSON); + + var captor = ArgumentCaptor.forClass(PostDspRequest.class); + verify(dspRequestHandler).createResource(captor.capture(), isA(ResponseDecorator.class)); + var request = captor.getValue(); + assertThat(request.getInputClass()).isEqualTo(CatalogRequestMessage.class); + assertThat(request.getResultClass()).isEqualTo(Catalog.class); + assertThat(request.getExpectedMessageType()).isEqualTo(namespace().toIri(DSPACE_TYPE_CATALOG_REQUEST_MESSAGE_TERM)); + assertThat(request.getProcessId()).isNull(); + assertThat(request.getToken()).isEqualTo("auth"); + assertThat(request.getMessage()).isEqualTo(requestBody); + verify(continuationTokenManager).createResponseDecorator("http://localhost:%d%s".formatted(port, basePath() + CATALOG_REQUEST)); + } + + @Test + void shouldApplyContinuationToken_whenPassed() { + var requestBody = createObjectBuilder().add(TYPE, namespace().toIri(DSPACE_TYPE_CATALOG_REQUEST_MESSAGE_TERM)).build(); + var catalog = createObjectBuilder().add(JsonLdKeywords.TYPE, "catalog").build(); + when(transformerRegistry.transform(any(Catalog.class), eq(JsonObject.class))).thenReturn(Result.success(catalog)); + when(dspRequestHandler.createResource(any(), any())).thenReturn(Response.ok().type(APPLICATION_JSON_TYPE).build()); + when(continuationTokenManager.createResponseDecorator(any())).thenReturn(mock()); + var enrichedRequestBody = createObjectBuilder(requestBody).add("query", Json.createObjectBuilder()).build(); + when(continuationTokenManager.applyQueryFromToken(any(), any())).thenReturn(Result.success(enrichedRequestBody)); + + baseRequest() + .contentType(JSON) + .body(requestBody) + .post(CATALOG_REQUEST + "?continuationToken=pagination-token") + .then() + .statusCode(200) + .contentType(JSON); + + var captor = ArgumentCaptor.forClass(PostDspRequest.class); + verify(dspRequestHandler).createResource(captor.capture(), isA(ResponseDecorator.class)); + var request = captor.getValue(); + assertThat(request.getMessage()).isSameAs(enrichedRequestBody); + verify(continuationTokenManager).applyQueryFromToken(requestBody, "pagination-token"); + } + + @Test + void shouldReturnBadRequest_whenContinuationTokenIsNotValid() { + var requestBody = createObjectBuilder().add(TYPE, namespace().toIri(DSPACE_TYPE_CATALOG_REQUEST_MESSAGE_TERM)).build(); + when(continuationTokenManager.applyQueryFromToken(any(), any())).thenReturn(Result.failure("error")); + + baseRequest() + .contentType(JSON) + .body(requestBody) + .post(CATALOG_REQUEST + "?continuationToken=pagination-token") + .then() + .statusCode(400); + + verifyNoInteractions(dspRequestHandler, transformerRegistry); + } + } + +} diff --git a/data-protocols/dsp/dsp-negotiation/build.gradle.kts b/data-protocols/dsp/dsp-negotiation/build.gradle.kts index 8ddf836bf79..0dd5e49b067 100644 --- a/data-protocols/dsp/dsp-negotiation/build.gradle.kts +++ b/data-protocols/dsp/dsp-negotiation/build.gradle.kts @@ -23,4 +23,5 @@ dependencies { api(project(":data-protocols:dsp:dsp-negotiation:dsp-negotiation-transform")) api(project(":data-protocols:dsp:dsp-negotiation:lib:dsp-negotiation-validation-lib")) api(project(":data-protocols:dsp:dsp-negotiation:lib:dsp-negotiation-transform-lib")) + api(project(":data-protocols:dsp:dsp-negotiation:lib:dsp-negotiation-http-api-lib")) } diff --git a/data-protocols/dsp/dsp-negotiation/dsp-negotiation-http-api/build.gradle.kts b/data-protocols/dsp/dsp-negotiation/dsp-negotiation-http-api/build.gradle.kts index f131c52b8ca..1eef0ccb03e 100644 --- a/data-protocols/dsp/dsp-negotiation/dsp-negotiation-http-api/build.gradle.kts +++ b/data-protocols/dsp/dsp-negotiation/dsp-negotiation-http-api/build.gradle.kts @@ -26,6 +26,7 @@ dependencies { api(project(":extensions:common:json-ld")) implementation(project(":data-protocols:dsp:dsp-negotiation:lib:dsp-negotiation-validation-lib")) + implementation(project(":data-protocols:dsp:dsp-negotiation:lib:dsp-negotiation-http-api-lib")) implementation(project(":extensions:common:http:lib:jersey-providers-lib")) implementation(libs.jakarta.rsApi) @@ -33,6 +34,8 @@ dependencies { testImplementation(project(":core:common:junit")) testImplementation(testFixtures(project(":extensions:common:http:jersey-core"))) testImplementation(libs.restAssured) + testImplementation(testFixtures(project(":data-protocols:dsp:dsp-negotiation:lib:dsp-negotiation-http-api-lib"))) + } edcBuild { diff --git a/data-protocols/dsp/dsp-negotiation/dsp-negotiation-http-api/src/main/java/org/eclipse/edc/protocol/dsp/negotiation/http/api/controller/DspNegotiationApiController.java b/data-protocols/dsp/dsp-negotiation/dsp-negotiation-http-api/src/main/java/org/eclipse/edc/protocol/dsp/negotiation/http/api/controller/DspNegotiationApiController.java index 436b3c76b20..b2f053a13cc 100644 --- a/data-protocols/dsp/dsp-negotiation/dsp-negotiation-http-api/src/main/java/org/eclipse/edc/protocol/dsp/negotiation/http/api/controller/DspNegotiationApiController.java +++ b/data-protocols/dsp/dsp-negotiation/dsp-negotiation-http-api/src/main/java/org/eclipse/edc/protocol/dsp/negotiation/http/api/controller/DspNegotiationApiController.java @@ -14,48 +14,16 @@ package org.eclipse.edc.protocol.dsp.negotiation.http.api.controller; -import jakarta.json.JsonObject; import jakarta.ws.rs.Consumes; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.HeaderParam; -import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; -import jakarta.ws.rs.PathParam; import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; -import org.eclipse.edc.connector.controlplane.contract.spi.types.agreement.ContractAgreementMessage; -import org.eclipse.edc.connector.controlplane.contract.spi.types.agreement.ContractAgreementVerificationMessage; -import org.eclipse.edc.connector.controlplane.contract.spi.types.agreement.ContractNegotiationEventMessage; -import org.eclipse.edc.connector.controlplane.contract.spi.types.negotiation.ContractNegotiation; -import org.eclipse.edc.connector.controlplane.contract.spi.types.negotiation.ContractNegotiationError; -import org.eclipse.edc.connector.controlplane.contract.spi.types.negotiation.ContractNegotiationTerminationMessage; -import org.eclipse.edc.connector.controlplane.contract.spi.types.negotiation.ContractOfferMessage; -import org.eclipse.edc.connector.controlplane.contract.spi.types.negotiation.ContractRequestMessage; import org.eclipse.edc.connector.controlplane.services.spi.contractnegotiation.ContractNegotiationProtocolService; -import org.eclipse.edc.jsonld.spi.JsonLdNamespace; import org.eclipse.edc.protocol.dsp.http.spi.message.DspRequestHandler; -import org.eclipse.edc.protocol.dsp.http.spi.message.GetDspRequest; -import org.eclipse.edc.protocol.dsp.http.spi.message.PostDspRequest; -import static jakarta.ws.rs.core.HttpHeaders.AUTHORIZATION; import static org.eclipse.edc.protocol.dsp.http.spi.types.HttpMessageProtocol.DATASPACE_PROTOCOL_HTTP; -import static org.eclipse.edc.protocol.dsp.negotiation.http.api.NegotiationApiPaths.AGREEMENT; import static org.eclipse.edc.protocol.dsp.negotiation.http.api.NegotiationApiPaths.BASE_PATH; -import static org.eclipse.edc.protocol.dsp.negotiation.http.api.NegotiationApiPaths.CONTRACT_OFFER; -import static org.eclipse.edc.protocol.dsp.negotiation.http.api.NegotiationApiPaths.CONTRACT_REQUEST; -import static org.eclipse.edc.protocol.dsp.negotiation.http.api.NegotiationApiPaths.EVENT; -import static org.eclipse.edc.protocol.dsp.negotiation.http.api.NegotiationApiPaths.INITIAL_CONTRACT_OFFER; -import static org.eclipse.edc.protocol.dsp.negotiation.http.api.NegotiationApiPaths.INITIAL_CONTRACT_REQUEST; -import static org.eclipse.edc.protocol.dsp.negotiation.http.api.NegotiationApiPaths.TERMINATION; -import static org.eclipse.edc.protocol.dsp.negotiation.http.api.NegotiationApiPaths.VERIFICATION; import static org.eclipse.edc.protocol.dsp.spi.type.DspConstants.DSP_NAMESPACE_V_08; -import static org.eclipse.edc.protocol.dsp.spi.type.DspNegotiationPropertyAndTypeNames.DSPACE_TYPE_CONTRACT_AGREEMENT_MESSAGE_TERM; -import static org.eclipse.edc.protocol.dsp.spi.type.DspNegotiationPropertyAndTypeNames.DSPACE_TYPE_CONTRACT_AGREEMENT_VERIFICATION_MESSAGE_TERM; -import static org.eclipse.edc.protocol.dsp.spi.type.DspNegotiationPropertyAndTypeNames.DSPACE_TYPE_CONTRACT_NEGOTIATION_EVENT_MESSAGE_TERM; -import static org.eclipse.edc.protocol.dsp.spi.type.DspNegotiationPropertyAndTypeNames.DSPACE_TYPE_CONTRACT_NEGOTIATION_TERMINATION_MESSAGE_TERM; -import static org.eclipse.edc.protocol.dsp.spi.type.DspNegotiationPropertyAndTypeNames.DSPACE_TYPE_CONTRACT_OFFER_MESSAGE_TERM; -import static org.eclipse.edc.protocol.dsp.spi.type.DspNegotiationPropertyAndTypeNames.DSPACE_TYPE_CONTRACT_REQUEST_MESSAGE_TERM; /** * Provides consumer and provider endpoints for the contract negotiation according to the http binding @@ -64,249 +32,13 @@ @Consumes({ MediaType.APPLICATION_JSON }) @Produces({ MediaType.APPLICATION_JSON }) @Path(BASE_PATH) -public class DspNegotiationApiController { +public class DspNegotiationApiController extends BaseDspNegotiationApiController { - private final DspRequestHandler dspRequestHandler; - private final String protocol; - private final JsonLdNamespace namespace; - private final ContractNegotiationProtocolService protocolService; public DspNegotiationApiController(ContractNegotiationProtocolService protocolService, DspRequestHandler dspRequestHandler) { - this(protocolService, dspRequestHandler, DATASPACE_PROTOCOL_HTTP, DSP_NAMESPACE_V_08); - } - - public DspNegotiationApiController(ContractNegotiationProtocolService protocolService, - DspRequestHandler dspRequestHandler, - String protocol, - JsonLdNamespace namespace) { - this.protocolService = protocolService; - this.dspRequestHandler = dspRequestHandler; - this.protocol = protocol; - this.namespace = namespace; - } - - /** - * Provider-specific endpoint. - * - * @param id of contract negotiation. - * @param token identity token. - * @return the requested contract negotiation or an error. - */ - @GET - @Path("{id}") - public Response getNegotiation(@PathParam("id") String id, @HeaderParam(AUTHORIZATION) String token) { - var request = GetDspRequest.Builder.newInstance(ContractNegotiation.class, ContractNegotiationError.class) - .id(id).token(token).serviceCall(protocolService::findById) - .errorProvider(ContractNegotiationError.Builder::newInstance) - .protocol(protocol) - .build(); - - return dspRequestHandler.getResource(request); - } - - /** - * Provider-specific endpoint. - * - * @param jsonObject dspace:ContractRequestMessage sent by a consumer. - * @param token identity token. - * @return the created contract negotiation or an error. - */ - @POST - @Path(INITIAL_CONTRACT_REQUEST) - public Response initialContractRequest(JsonObject jsonObject, @HeaderParam(AUTHORIZATION) String token) { - var request = PostDspRequest.Builder.newInstance(ContractRequestMessage.class, ContractNegotiation.class, ContractNegotiationError.class) - .expectedMessageType(namespace.toIri(DSPACE_TYPE_CONTRACT_REQUEST_MESSAGE_TERM)) - .message(jsonObject) - .token(token) - .serviceCall(protocolService::notifyRequested) - .errorProvider(ContractNegotiationError.Builder::newInstance) - .protocol(protocol) - .build(); - - return dspRequestHandler.createResource(request); - } - - /** - * Consumer-specific endpoint. - * - * @param jsonObject dspace:ContractOfferMessage sent by a consumer. - * @param token identity token. - * @return the created contract negotiation or an error. - */ - @POST - @Path(INITIAL_CONTRACT_OFFER) - public Response initialContractOffer(JsonObject jsonObject, @HeaderParam(AUTHORIZATION) String token) { - var request = PostDspRequest.Builder.newInstance(ContractOfferMessage.class, ContractNegotiation.class, ContractNegotiationError.class) - .expectedMessageType(namespace.toIri(DSPACE_TYPE_CONTRACT_OFFER_MESSAGE_TERM)) - .message(jsonObject) - .token(token) - .serviceCall(protocolService::notifyOffered) - .errorProvider(ContractNegotiationError.Builder::newInstance) - .protocol(protocol) - .build(); - - return dspRequestHandler.createResource(request); - } - - /** - * Provider-specific endpoint. - * - * @param id of contract negotiation. - * @param jsonObject dspace:ContractRequestMessage sent by a consumer. - * @param token identity token. - * @return the created contract negotiation or an error. - */ - @POST - @Path("{id}" + CONTRACT_REQUEST) - public Response contractRequest(@PathParam("id") String id, - JsonObject jsonObject, - @HeaderParam(AUTHORIZATION) String token) { - var request = PostDspRequest.Builder.newInstance(ContractRequestMessage.class, ContractNegotiation.class, ContractNegotiationError.class) - .expectedMessageType(namespace.toIri(DSPACE_TYPE_CONTRACT_REQUEST_MESSAGE_TERM)) - .processId(id) - .message(jsonObject) - .token(token) - .serviceCall(protocolService::notifyRequested) - .errorProvider(ContractNegotiationError.Builder::newInstance) - .protocol(protocol) - .build(); - - return dspRequestHandler.updateResource(request); - } - - /** - * Endpoint on provider and consumer side. - * - * @param id of contract negotiation. - * @param jsonObject dspace:ContractNegotiationEventMessage sent by consumer or provider. - * @param token identity token. - * @return empty response or error. - */ - @POST - @Path("{id}" + EVENT) - public Response createEvent(@PathParam("id") String id, - JsonObject jsonObject, - @HeaderParam(AUTHORIZATION) String token) { - var request = PostDspRequest.Builder.newInstance(ContractNegotiationEventMessage.class, ContractNegotiation.class, ContractNegotiationError.class) - .expectedMessageType(namespace.toIri(DSPACE_TYPE_CONTRACT_NEGOTIATION_EVENT_MESSAGE_TERM)) - .processId(id) - .message(jsonObject) - .token(token) - .serviceCall((message, claimToken) -> switch (message.getType()) { - case FINALIZED -> protocolService.notifyFinalized(message, claimToken); - case ACCEPTED -> protocolService.notifyAccepted(message, claimToken); - }) - .errorProvider(ContractNegotiationError.Builder::newInstance) - .protocol(protocol) - .build(); - - return dspRequestHandler.updateResource(request); - } - - /** - * Provider-specific endpoint. - * - * @param id of contract negotiation. - * @param jsonObject dspace:ContractAgreementVerificationMessage sent by a consumer. - * @param token identity token. - * @return empty response or error. - */ - @POST - @Path("{id}" + AGREEMENT + VERIFICATION) - public Response verifyAgreement(@PathParam("id") String id, - JsonObject jsonObject, - @HeaderParam(AUTHORIZATION) String token) { - var request = PostDspRequest.Builder.newInstance(ContractAgreementVerificationMessage.class, ContractNegotiation.class, ContractNegotiationError.class) - .expectedMessageType(namespace.toIri(DSPACE_TYPE_CONTRACT_AGREEMENT_VERIFICATION_MESSAGE_TERM)) - .processId(id) - .message(jsonObject) - .token(token) - .serviceCall(protocolService::notifyVerified) - .errorProvider(ContractNegotiationError.Builder::newInstance) - .protocol(protocol) - .build(); - - return dspRequestHandler.updateResource(request); - } - - /** - * Endpoint on provider and consumer side. - * - * @param id of contract negotiation. - * @param jsonObject dspace:ContractNegotiationTerminationMessage sent by consumer or provider. - * @param token identity token. - * @return empty response or error. - */ - @POST - @Path("{id}" + TERMINATION) - public Response terminateNegotiation(@PathParam("id") String id, - JsonObject jsonObject, - @HeaderParam(AUTHORIZATION) String token) { - var request = PostDspRequest.Builder.newInstance(ContractNegotiationTerminationMessage.class, ContractNegotiation.class, ContractNegotiationError.class) - .expectedMessageType(namespace.toIri(DSPACE_TYPE_CONTRACT_NEGOTIATION_TERMINATION_MESSAGE_TERM)) - .processId(id) - .message(jsonObject) - .token(token) - .serviceCall(protocolService::notifyTerminated) - .errorProvider(ContractNegotiationError.Builder::newInstance) - .protocol(protocol) - .build(); - - return dspRequestHandler.updateResource(request); - } - - /** - * Consumer-specific endpoint. - * - * @param id of contract negotiation. - * @param body dspace:ContractOfferMessage sent by a provider. - * @param token identity token. - * @return empty response or error. - */ - @POST - @Path("{id}" + CONTRACT_OFFER) - public Response providerOffer(@PathParam("id") String id, - JsonObject body, - @HeaderParam(AUTHORIZATION) String token) { - var request = PostDspRequest.Builder.newInstance(ContractOfferMessage.class, ContractNegotiation.class, ContractNegotiationError.class) - .expectedMessageType(namespace.toIri(DSPACE_TYPE_CONTRACT_OFFER_MESSAGE_TERM)) - .processId(id) - .message(body) - .token(token) - .serviceCall(protocolService::notifyOffered) - .errorProvider(ContractNegotiationError.Builder::newInstance) - .protocol(protocol) - .build(); - - return dspRequestHandler.updateResource(request); - } - - /** - * Consumer-specific endpoint. - * - * @param id of contract negotiation. - * @param jsonObject dspace:ContractAgreementMessage sent by a provider. - * @param token identity token. - * @return empty response or error. - */ - @POST - @Path("{id}" + AGREEMENT) - public Response createAgreement(@PathParam("id") String id, - JsonObject jsonObject, - @HeaderParam(AUTHORIZATION) String token) { - var request = PostDspRequest.Builder.newInstance(ContractAgreementMessage.class, ContractNegotiation.class, ContractNegotiationError.class) - .expectedMessageType(namespace.toIri(DSPACE_TYPE_CONTRACT_AGREEMENT_MESSAGE_TERM)) - .processId(id) - .message(jsonObject) - .token(token) - .serviceCall(protocolService::notifyAgreed) - .errorProvider(ContractNegotiationError.Builder::newInstance) - .protocol(protocol) - .build(); - - return dspRequestHandler.updateResource(request); + super(protocolService, dspRequestHandler, DATASPACE_PROTOCOL_HTTP, DSP_NAMESPACE_V_08); } } diff --git a/data-protocols/dsp/dsp-negotiation/dsp-negotiation-http-api/src/main/java/org/eclipse/edc/protocol/dsp/negotiation/http/api/controller/DspNegotiationApiController20241.java b/data-protocols/dsp/dsp-negotiation/dsp-negotiation-http-api/src/main/java/org/eclipse/edc/protocol/dsp/negotiation/http/api/controller/DspNegotiationApiController20241.java index 1e765204107..fbf075d32b0 100644 --- a/data-protocols/dsp/dsp-negotiation/dsp-negotiation-http-api/src/main/java/org/eclipse/edc/protocol/dsp/negotiation/http/api/controller/DspNegotiationApiController20241.java +++ b/data-protocols/dsp/dsp-negotiation/dsp-negotiation-http-api/src/main/java/org/eclipse/edc/protocol/dsp/negotiation/http/api/controller/DspNegotiationApiController20241.java @@ -27,12 +27,12 @@ import static org.eclipse.edc.protocol.dsp.spi.type.DspConstants.DSP_NAMESPACE_V_2024_1; /** - * Versioned Negotiation endpoint, same as {@link DspNegotiationApiController} but exposed on the /2024/1 path + * Versioned Negotiation endpoint for 2024/1 protocol version */ @Consumes({ MediaType.APPLICATION_JSON }) @Produces({ MediaType.APPLICATION_JSON }) @Path(DspVersions.V_2024_1_PATH + BASE_PATH) -public class DspNegotiationApiController20241 extends DspNegotiationApiController { +public class DspNegotiationApiController20241 extends BaseDspNegotiationApiController { public DspNegotiationApiController20241(ContractNegotiationProtocolService protocolService, DspRequestHandler dspRequestHandler) { super(protocolService, dspRequestHandler, DATASPACE_PROTOCOL_HTTP_V_2024_1, DSP_NAMESPACE_V_2024_1); diff --git a/data-protocols/dsp/dsp-negotiation/dsp-negotiation-http-api/src/test/java/org/eclipse/edc/protocol/dsp/negotiation/http/api/controller/DspNegotiationApiControllerTest.java b/data-protocols/dsp/dsp-negotiation/dsp-negotiation-http-api/src/test/java/org/eclipse/edc/protocol/dsp/negotiation/http/api/controller/DspNegotiationApiControllerTest.java index 80b230d12af..b5465414b2e 100644 --- a/data-protocols/dsp/dsp-negotiation/dsp-negotiation-http-api/src/test/java/org/eclipse/edc/protocol/dsp/negotiation/http/api/controller/DspNegotiationApiControllerTest.java +++ b/data-protocols/dsp/dsp-negotiation/dsp-negotiation-http-api/src/test/java/org/eclipse/edc/protocol/dsp/negotiation/http/api/controller/DspNegotiationApiControllerTest.java @@ -14,217 +14,20 @@ package org.eclipse.edc.protocol.dsp.negotiation.http.api.controller; -import io.restassured.specification.RequestSpecification; -import jakarta.ws.rs.core.HttpHeaders; -import jakarta.ws.rs.core.Response; -import org.eclipse.edc.connector.controlplane.contract.spi.types.agreement.ContractAgreementMessage; -import org.eclipse.edc.connector.controlplane.contract.spi.types.agreement.ContractAgreementVerificationMessage; -import org.eclipse.edc.connector.controlplane.contract.spi.types.agreement.ContractNegotiationEventMessage; -import org.eclipse.edc.connector.controlplane.contract.spi.types.negotiation.ContractNegotiation; -import org.eclipse.edc.connector.controlplane.contract.spi.types.negotiation.ContractNegotiationTerminationMessage; -import org.eclipse.edc.connector.controlplane.contract.spi.types.negotiation.ContractOfferMessage; -import org.eclipse.edc.connector.controlplane.contract.spi.types.negotiation.ContractRequestMessage; -import org.eclipse.edc.connector.controlplane.services.spi.contractnegotiation.ContractNegotiationProtocolService; import org.eclipse.edc.jsonld.spi.JsonLdNamespace; import org.eclipse.edc.junit.annotations.ApiTest; -import org.eclipse.edc.protocol.dsp.http.spi.message.DspRequestHandler; -import org.eclipse.edc.protocol.dsp.http.spi.message.GetDspRequest; -import org.eclipse.edc.protocol.dsp.http.spi.message.PostDspRequest; -import org.eclipse.edc.web.jersey.testfixtures.RestControllerTestBase; import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtensionContext; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.ArgumentsProvider; -import org.junit.jupiter.params.provider.ArgumentsSource; -import org.mockito.ArgumentCaptor; -import java.util.stream.Stream; - -import static io.restassured.RestAssured.given; -import static jakarta.json.Json.createObjectBuilder; -import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; -import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON_TYPE; -import static org.assertj.core.api.Assertions.assertThat; -import static org.eclipse.edc.protocol.dsp.negotiation.http.api.NegotiationApiPaths.AGREEMENT; import static org.eclipse.edc.protocol.dsp.negotiation.http.api.NegotiationApiPaths.BASE_PATH; -import static org.eclipse.edc.protocol.dsp.negotiation.http.api.NegotiationApiPaths.CONTRACT_OFFER; -import static org.eclipse.edc.protocol.dsp.negotiation.http.api.NegotiationApiPaths.CONTRACT_REQUEST; -import static org.eclipse.edc.protocol.dsp.negotiation.http.api.NegotiationApiPaths.EVENT; -import static org.eclipse.edc.protocol.dsp.negotiation.http.api.NegotiationApiPaths.INITIAL_CONTRACT_OFFER; -import static org.eclipse.edc.protocol.dsp.negotiation.http.api.NegotiationApiPaths.INITIAL_CONTRACT_REQUEST; -import static org.eclipse.edc.protocol.dsp.negotiation.http.api.NegotiationApiPaths.TERMINATION; -import static org.eclipse.edc.protocol.dsp.negotiation.http.api.NegotiationApiPaths.VERIFICATION; import static org.eclipse.edc.protocol.dsp.spi.type.DspConstants.DSP_NAMESPACE_V_08; import static org.eclipse.edc.protocol.dsp.spi.type.DspConstants.DSP_NAMESPACE_V_2024_1; -import static org.eclipse.edc.protocol.dsp.spi.type.DspNegotiationPropertyAndTypeNames.DSPACE_TYPE_CONTRACT_AGREEMENT_MESSAGE_TERM; -import static org.eclipse.edc.protocol.dsp.spi.type.DspNegotiationPropertyAndTypeNames.DSPACE_TYPE_CONTRACT_AGREEMENT_VERIFICATION_MESSAGE_TERM; -import static org.eclipse.edc.protocol.dsp.spi.type.DspNegotiationPropertyAndTypeNames.DSPACE_TYPE_CONTRACT_NEGOTIATION_EVENT_MESSAGE_TERM; -import static org.eclipse.edc.protocol.dsp.spi.type.DspNegotiationPropertyAndTypeNames.DSPACE_TYPE_CONTRACT_NEGOTIATION_TERMINATION_MESSAGE_TERM; -import static org.eclipse.edc.protocol.dsp.spi.type.DspNegotiationPropertyAndTypeNames.DSPACE_TYPE_CONTRACT_OFFER_MESSAGE_TERM; -import static org.eclipse.edc.protocol.dsp.spi.type.DspNegotiationPropertyAndTypeNames.DSPACE_TYPE_CONTRACT_REQUEST_MESSAGE_TERM; import static org.eclipse.edc.protocol.dsp.spi.version.DspVersions.V_2024_1_PATH; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; class DspNegotiationApiControllerTest { - - abstract static class Tests extends RestControllerTestBase { - - protected final ContractNegotiationProtocolService protocolService = mock(); - protected final DspRequestHandler dspRequestHandler = mock(); - - @Test - void getNegotiation_shouldGetResource() { - when(dspRequestHandler.getResource(any())).thenReturn(Response.ok().type(APPLICATION_JSON_TYPE).build()); - - var result = baseRequest() - .get(basePath() + "negotiationId") - .then() - .contentType(APPLICATION_JSON) - .statusCode(200); - - assertThat(result).isNotNull(); - var captor = ArgumentCaptor.forClass(GetDspRequest.class); - verify(dspRequestHandler).getResource(captor.capture()); - var dspMessage = captor.getValue(); - assertThat(dspMessage.getToken()).isEqualTo("auth"); - assertThat(dspMessage.getId()).isEqualTo("negotiationId"); - assertThat(dspMessage.getResultClass()).isEqualTo(ContractNegotiation.class); - } - - @Test - void initialContractRequest_shouldCreateResource() { - var requestBody = createObjectBuilder().add("@type", namespace().toIri(DSPACE_TYPE_CONTRACT_REQUEST_MESSAGE_TERM)).build(); - when(dspRequestHandler.createResource(any())).thenReturn(Response.ok().type(APPLICATION_JSON_TYPE).build()); - - var result = baseRequest() - .contentType(APPLICATION_JSON) - .body(requestBody) - .post(basePath() + INITIAL_CONTRACT_REQUEST) - .then() - .statusCode(200) - .contentType(APPLICATION_JSON); - - assertThat(result).isNotNull(); - var captor = ArgumentCaptor.forClass(PostDspRequest.class); - verify(dspRequestHandler).createResource(captor.capture()); - var request = captor.getValue(); - assertThat(request.getToken()).isEqualTo("auth"); - assertThat(request.getProcessId()).isEqualTo(null); - assertThat(request.getMessage()).isNotNull(); - assertThat(request.getInputClass()).isEqualTo(ContractRequestMessage.class); - assertThat(request.getResultClass()).isEqualTo(ContractNegotiation.class); - assertThat(request.getExpectedMessageType()).isEqualTo(namespace().toIri(DSPACE_TYPE_CONTRACT_REQUEST_MESSAGE_TERM)); - } - - @Test - void initialContractOffer_shouldCreateResource() { - var requestBody = createObjectBuilder().add("@type", namespace().toIri(DSPACE_TYPE_CONTRACT_OFFER_MESSAGE_TERM)).build(); - when(dspRequestHandler.createResource(any())).thenReturn(Response.ok().type(APPLICATION_JSON_TYPE).build()); - - var result = baseRequest() - .contentType(APPLICATION_JSON) - .body(requestBody) - .post(basePath() + INITIAL_CONTRACT_OFFER) - .then() - .statusCode(200) - .contentType(APPLICATION_JSON); - - assertThat(result).isNotNull(); - var captor = ArgumentCaptor.forClass(PostDspRequest.class); - verify(dspRequestHandler).createResource(captor.capture()); - var request = captor.getValue(); - assertThat(request.getToken()).isEqualTo("auth"); - assertThat(request.getProcessId()).isEqualTo(null); - assertThat(request.getMessage()).isNotNull(); - assertThat(request.getInputClass()).isEqualTo(ContractOfferMessage.class); - assertThat(request.getResultClass()).isEqualTo(ContractNegotiation.class); - assertThat(request.getExpectedMessageType()).isEqualTo(namespace().toIri(DSPACE_TYPE_CONTRACT_OFFER_MESSAGE_TERM)); - } - - /** - * Verifies that an endpoint returns 401 if the identity service cannot verify the identity token. - * - * @param path the request path to the endpoint - */ - @ParameterizedTest - @ArgumentsSource(ControllerMethodArguments.class) - void callEndpoint_shouldUpdateResource(String path, Class messageClass, String messageType) { - when(dspRequestHandler.updateResource(any())).thenReturn(Response.ok().type(APPLICATION_JSON_TYPE).build()); - var requestBody = createObjectBuilder().add("http://schema/key", "value").build(); - - baseRequest() - .contentType(APPLICATION_JSON) - .body(requestBody) - .post(basePath() + path) - .then() - .contentType(APPLICATION_JSON) - .statusCode(200); - - var captor = ArgumentCaptor.forClass(PostDspRequest.class); - verify(dspRequestHandler).updateResource(captor.capture()); - var request = captor.getValue(); - assertThat(request.getExpectedMessageType()).isEqualTo(namespace().toIri(messageType)); - assertThat(request.getToken()).isEqualTo("auth"); - assertThat(request.getProcessId()).isEqualTo("testId"); - assertThat(request.getMessage()).isNotNull(); - assertThat(request.getInputClass()).isEqualTo(messageClass); - assertThat(request.getResultClass()).isEqualTo(ContractNegotiation.class); - } - - protected abstract String basePath(); - - protected abstract JsonLdNamespace namespace(); - - private RequestSpecification baseRequest() { - var authHeader = "auth"; - return given() - .baseUri("http://localhost:" + port) - .basePath("/") - .header(HttpHeaders.AUTHORIZATION, authHeader) - .when(); - } - - private static class ControllerMethodArguments implements ArgumentsProvider { - @Override - public Stream provideArguments(ExtensionContext context) { - return Stream.of( - Arguments.of( - "testId" + CONTRACT_REQUEST, - ContractRequestMessage.class, - DSPACE_TYPE_CONTRACT_REQUEST_MESSAGE_TERM), - Arguments.of( - "testId" + EVENT, - ContractNegotiationEventMessage.class, - DSPACE_TYPE_CONTRACT_NEGOTIATION_EVENT_MESSAGE_TERM), - Arguments.of( - "testId" + AGREEMENT + VERIFICATION, - ContractAgreementVerificationMessage.class, - DSPACE_TYPE_CONTRACT_AGREEMENT_VERIFICATION_MESSAGE_TERM), - Arguments.of( - "testId" + TERMINATION, - ContractNegotiationTerminationMessage.class, - DSPACE_TYPE_CONTRACT_NEGOTIATION_TERMINATION_MESSAGE_TERM), - Arguments.of( - "testId" + AGREEMENT, - ContractAgreementMessage.class, - DSPACE_TYPE_CONTRACT_AGREEMENT_MESSAGE_TERM), - Arguments.of( - "testId" + CONTRACT_OFFER, - ContractOfferMessage.class, - DSPACE_TYPE_CONTRACT_OFFER_MESSAGE_TERM) - ); - } - } - } - + @ApiTest @Nested - class DspNegotiationApiControllerV08Test extends Tests { + class DspNegotiationApiControllerV08Test extends DspNegotiationApiControllerTestBase { @Override protected String basePath() { @@ -244,7 +47,7 @@ protected Object controller() { @ApiTest @Nested - class DspNegotiationApiControllerV20241Test extends Tests { + class DspNegotiationApiControllerV20241Test extends DspNegotiationApiControllerTestBase { @Override protected String basePath() { diff --git a/data-protocols/dsp/dsp-negotiation/lib/dsp-negotiation-http-api-lib/build.gradle.kts b/data-protocols/dsp/dsp-negotiation/lib/dsp-negotiation-http-api-lib/build.gradle.kts new file mode 100644 index 00000000000..6437cba347a --- /dev/null +++ b/data-protocols/dsp/dsp-negotiation/lib/dsp-negotiation-http-api-lib/build.gradle.kts @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +plugins { + `java-library` + `java-test-fixtures` +} + +dependencies { + api(project(":data-protocols:dsp:dsp-spi")) + api(project(":data-protocols:dsp:dsp-http-spi")) + api(project(":spi:common:json-ld-spi")) + + testFixturesImplementation(project(":core:common:junit")) + testFixturesImplementation(testFixtures(project(":extensions:common:http:jersey-core"))) + testFixturesImplementation(libs.restAssured) + testFixturesImplementation(libs.assertj) + testFixturesImplementation(libs.mockito.core) + testFixturesImplementation(libs.junit.jupiter.params) +} \ No newline at end of file diff --git a/data-protocols/dsp/dsp-negotiation/dsp-negotiation-http-api/src/main/java/org/eclipse/edc/protocol/dsp/negotiation/http/api/NegotiationApiPaths.java b/data-protocols/dsp/dsp-negotiation/lib/dsp-negotiation-http-api-lib/src/main/java/org/eclipse/edc/protocol/dsp/negotiation/http/api/NegotiationApiPaths.java similarity index 100% rename from data-protocols/dsp/dsp-negotiation/dsp-negotiation-http-api/src/main/java/org/eclipse/edc/protocol/dsp/negotiation/http/api/NegotiationApiPaths.java rename to data-protocols/dsp/dsp-negotiation/lib/dsp-negotiation-http-api-lib/src/main/java/org/eclipse/edc/protocol/dsp/negotiation/http/api/NegotiationApiPaths.java diff --git a/data-protocols/dsp/dsp-negotiation/lib/dsp-negotiation-http-api-lib/src/main/java/org/eclipse/edc/protocol/dsp/negotiation/http/api/controller/BaseDspNegotiationApiController.java b/data-protocols/dsp/dsp-negotiation/lib/dsp-negotiation-http-api-lib/src/main/java/org/eclipse/edc/protocol/dsp/negotiation/http/api/controller/BaseDspNegotiationApiController.java new file mode 100644 index 00000000000..e44b447ddfb --- /dev/null +++ b/data-protocols/dsp/dsp-negotiation/lib/dsp-negotiation-http-api-lib/src/main/java/org/eclipse/edc/protocol/dsp/negotiation/http/api/controller/BaseDspNegotiationApiController.java @@ -0,0 +1,297 @@ +/* + * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.edc.protocol.dsp.negotiation.http.api.controller; + +import jakarta.json.JsonObject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.HeaderParam; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.core.Response; +import org.eclipse.edc.connector.controlplane.contract.spi.types.agreement.ContractAgreementMessage; +import org.eclipse.edc.connector.controlplane.contract.spi.types.agreement.ContractAgreementVerificationMessage; +import org.eclipse.edc.connector.controlplane.contract.spi.types.agreement.ContractNegotiationEventMessage; +import org.eclipse.edc.connector.controlplane.contract.spi.types.negotiation.ContractNegotiation; +import org.eclipse.edc.connector.controlplane.contract.spi.types.negotiation.ContractNegotiationError; +import org.eclipse.edc.connector.controlplane.contract.spi.types.negotiation.ContractNegotiationTerminationMessage; +import org.eclipse.edc.connector.controlplane.contract.spi.types.negotiation.ContractOfferMessage; +import org.eclipse.edc.connector.controlplane.contract.spi.types.negotiation.ContractRequestMessage; +import org.eclipse.edc.connector.controlplane.services.spi.contractnegotiation.ContractNegotiationProtocolService; +import org.eclipse.edc.jsonld.spi.JsonLdNamespace; +import org.eclipse.edc.protocol.dsp.http.spi.message.DspRequestHandler; +import org.eclipse.edc.protocol.dsp.http.spi.message.GetDspRequest; +import org.eclipse.edc.protocol.dsp.http.spi.message.PostDspRequest; + +import static jakarta.ws.rs.core.HttpHeaders.AUTHORIZATION; +import static org.eclipse.edc.protocol.dsp.negotiation.http.api.NegotiationApiPaths.AGREEMENT; +import static org.eclipse.edc.protocol.dsp.negotiation.http.api.NegotiationApiPaths.CONTRACT_OFFER; +import static org.eclipse.edc.protocol.dsp.negotiation.http.api.NegotiationApiPaths.CONTRACT_REQUEST; +import static org.eclipse.edc.protocol.dsp.negotiation.http.api.NegotiationApiPaths.EVENT; +import static org.eclipse.edc.protocol.dsp.negotiation.http.api.NegotiationApiPaths.INITIAL_CONTRACT_OFFER; +import static org.eclipse.edc.protocol.dsp.negotiation.http.api.NegotiationApiPaths.INITIAL_CONTRACT_REQUEST; +import static org.eclipse.edc.protocol.dsp.negotiation.http.api.NegotiationApiPaths.TERMINATION; +import static org.eclipse.edc.protocol.dsp.negotiation.http.api.NegotiationApiPaths.VERIFICATION; +import static org.eclipse.edc.protocol.dsp.spi.type.DspNegotiationPropertyAndTypeNames.DSPACE_TYPE_CONTRACT_AGREEMENT_MESSAGE_TERM; +import static org.eclipse.edc.protocol.dsp.spi.type.DspNegotiationPropertyAndTypeNames.DSPACE_TYPE_CONTRACT_AGREEMENT_VERIFICATION_MESSAGE_TERM; +import static org.eclipse.edc.protocol.dsp.spi.type.DspNegotiationPropertyAndTypeNames.DSPACE_TYPE_CONTRACT_NEGOTIATION_EVENT_MESSAGE_TERM; +import static org.eclipse.edc.protocol.dsp.spi.type.DspNegotiationPropertyAndTypeNames.DSPACE_TYPE_CONTRACT_NEGOTIATION_TERMINATION_MESSAGE_TERM; +import static org.eclipse.edc.protocol.dsp.spi.type.DspNegotiationPropertyAndTypeNames.DSPACE_TYPE_CONTRACT_OFFER_MESSAGE_TERM; +import static org.eclipse.edc.protocol.dsp.spi.type.DspNegotiationPropertyAndTypeNames.DSPACE_TYPE_CONTRACT_REQUEST_MESSAGE_TERM; + +/** + * Provides consumer and provider endpoints for the contract negotiation according to the http binding + * of the dataspace protocol. + */ +public abstract class BaseDspNegotiationApiController { + + private final DspRequestHandler dspRequestHandler; + private final String protocol; + private final JsonLdNamespace namespace; + private final ContractNegotiationProtocolService protocolService; + + public BaseDspNegotiationApiController(ContractNegotiationProtocolService protocolService, + DspRequestHandler dspRequestHandler, + String protocol, + JsonLdNamespace namespace) { + this.protocolService = protocolService; + this.dspRequestHandler = dspRequestHandler; + this.protocol = protocol; + this.namespace = namespace; + } + + /** + * Provider-specific endpoint. + * + * @param id of contract negotiation. + * @param token identity token. + * @return the requested contract negotiation or an error. + */ + @GET + @Path("{id}") + public Response getNegotiation(@PathParam("id") String id, @HeaderParam(AUTHORIZATION) String token) { + var request = GetDspRequest.Builder.newInstance(ContractNegotiation.class, ContractNegotiationError.class) + .id(id).token(token).serviceCall(protocolService::findById) + .errorProvider(ContractNegotiationError.Builder::newInstance) + .protocol(protocol) + .build(); + + return dspRequestHandler.getResource(request); + } + + /** + * Provider-specific endpoint. + * + * @param jsonObject dspace:ContractRequestMessage sent by a consumer. + * @param token identity token. + * @return the created contract negotiation or an error. + */ + @POST + @Path(INITIAL_CONTRACT_REQUEST) + public Response initialContractRequest(JsonObject jsonObject, @HeaderParam(AUTHORIZATION) String token) { + var request = PostDspRequest.Builder.newInstance(ContractRequestMessage.class, ContractNegotiation.class, ContractNegotiationError.class) + .expectedMessageType(namespace.toIri(DSPACE_TYPE_CONTRACT_REQUEST_MESSAGE_TERM)) + .message(jsonObject) + .token(token) + .serviceCall(protocolService::notifyRequested) + .errorProvider(ContractNegotiationError.Builder::newInstance) + .protocol(protocol) + .build(); + + return dspRequestHandler.createResource(request); + } + + /** + * Consumer-specific endpoint. + * + * @param jsonObject dspace:ContractOfferMessage sent by a consumer. + * @param token identity token. + * @return the created contract negotiation or an error. + */ + @POST + @Path(INITIAL_CONTRACT_OFFER) + public Response initialContractOffer(JsonObject jsonObject, @HeaderParam(AUTHORIZATION) String token) { + var request = PostDspRequest.Builder.newInstance(ContractOfferMessage.class, ContractNegotiation.class, ContractNegotiationError.class) + .expectedMessageType(namespace.toIri(DSPACE_TYPE_CONTRACT_OFFER_MESSAGE_TERM)) + .message(jsonObject) + .token(token) + .serviceCall(protocolService::notifyOffered) + .errorProvider(ContractNegotiationError.Builder::newInstance) + .protocol(protocol) + .build(); + + return dspRequestHandler.createResource(request); + } + + /** + * Provider-specific endpoint. + * + * @param id of contract negotiation. + * @param jsonObject dspace:ContractRequestMessage sent by a consumer. + * @param token identity token. + * @return the created contract negotiation or an error. + */ + @POST + @Path("{id}" + CONTRACT_REQUEST) + public Response contractRequest(@PathParam("id") String id, + JsonObject jsonObject, + @HeaderParam(AUTHORIZATION) String token) { + var request = PostDspRequest.Builder.newInstance(ContractRequestMessage.class, ContractNegotiation.class, ContractNegotiationError.class) + .expectedMessageType(namespace.toIri(DSPACE_TYPE_CONTRACT_REQUEST_MESSAGE_TERM)) + .processId(id) + .message(jsonObject) + .token(token) + .serviceCall(protocolService::notifyRequested) + .errorProvider(ContractNegotiationError.Builder::newInstance) + .protocol(protocol) + .build(); + + return dspRequestHandler.updateResource(request); + } + + /** + * Endpoint on provider and consumer side. + * + * @param id of contract negotiation. + * @param jsonObject dspace:ContractNegotiationEventMessage sent by consumer or provider. + * @param token identity token. + * @return empty response or error. + */ + @POST + @Path("{id}" + EVENT) + public Response createEvent(@PathParam("id") String id, + JsonObject jsonObject, + @HeaderParam(AUTHORIZATION) String token) { + var request = PostDspRequest.Builder.newInstance(ContractNegotiationEventMessage.class, ContractNegotiation.class, ContractNegotiationError.class) + .expectedMessageType(namespace.toIri(DSPACE_TYPE_CONTRACT_NEGOTIATION_EVENT_MESSAGE_TERM)) + .processId(id) + .message(jsonObject) + .token(token) + .serviceCall((message, claimToken) -> switch (message.getType()) { + case FINALIZED -> protocolService.notifyFinalized(message, claimToken); + case ACCEPTED -> protocolService.notifyAccepted(message, claimToken); + }) + .errorProvider(ContractNegotiationError.Builder::newInstance) + .protocol(protocol) + .build(); + + return dspRequestHandler.updateResource(request); + } + + /** + * Provider-specific endpoint. + * + * @param id of contract negotiation. + * @param jsonObject dspace:ContractAgreementVerificationMessage sent by a consumer. + * @param token identity token. + * @return empty response or error. + */ + @POST + @Path("{id}" + AGREEMENT + VERIFICATION) + public Response verifyAgreement(@PathParam("id") String id, + JsonObject jsonObject, + @HeaderParam(AUTHORIZATION) String token) { + var request = PostDspRequest.Builder.newInstance(ContractAgreementVerificationMessage.class, ContractNegotiation.class, ContractNegotiationError.class) + .expectedMessageType(namespace.toIri(DSPACE_TYPE_CONTRACT_AGREEMENT_VERIFICATION_MESSAGE_TERM)) + .processId(id) + .message(jsonObject) + .token(token) + .serviceCall(protocolService::notifyVerified) + .errorProvider(ContractNegotiationError.Builder::newInstance) + .protocol(protocol) + .build(); + + return dspRequestHandler.updateResource(request); + } + + /** + * Endpoint on provider and consumer side. + * + * @param id of contract negotiation. + * @param jsonObject dspace:ContractNegotiationTerminationMessage sent by consumer or provider. + * @param token identity token. + * @return empty response or error. + */ + @POST + @Path("{id}" + TERMINATION) + public Response terminateNegotiation(@PathParam("id") String id, + JsonObject jsonObject, + @HeaderParam(AUTHORIZATION) String token) { + var request = PostDspRequest.Builder.newInstance(ContractNegotiationTerminationMessage.class, ContractNegotiation.class, ContractNegotiationError.class) + .expectedMessageType(namespace.toIri(DSPACE_TYPE_CONTRACT_NEGOTIATION_TERMINATION_MESSAGE_TERM)) + .processId(id) + .message(jsonObject) + .token(token) + .serviceCall(protocolService::notifyTerminated) + .errorProvider(ContractNegotiationError.Builder::newInstance) + .protocol(protocol) + .build(); + + return dspRequestHandler.updateResource(request); + } + + /** + * Consumer-specific endpoint. + * + * @param id of contract negotiation. + * @param body dspace:ContractOfferMessage sent by a provider. + * @param token identity token. + * @return empty response or error. + */ + @POST + @Path("{id}" + CONTRACT_OFFER) + public Response providerOffer(@PathParam("id") String id, + JsonObject body, + @HeaderParam(AUTHORIZATION) String token) { + var request = PostDspRequest.Builder.newInstance(ContractOfferMessage.class, ContractNegotiation.class, ContractNegotiationError.class) + .expectedMessageType(namespace.toIri(DSPACE_TYPE_CONTRACT_OFFER_MESSAGE_TERM)) + .processId(id) + .message(body) + .token(token) + .serviceCall(protocolService::notifyOffered) + .errorProvider(ContractNegotiationError.Builder::newInstance) + .protocol(protocol) + .build(); + + return dspRequestHandler.updateResource(request); + } + + /** + * Consumer-specific endpoint. + * + * @param id of contract negotiation. + * @param jsonObject dspace:ContractAgreementMessage sent by a provider. + * @param token identity token. + * @return empty response or error. + */ + @POST + @Path("{id}" + AGREEMENT) + public Response createAgreement(@PathParam("id") String id, + JsonObject jsonObject, + @HeaderParam(AUTHORIZATION) String token) { + var request = PostDspRequest.Builder.newInstance(ContractAgreementMessage.class, ContractNegotiation.class, ContractNegotiationError.class) + .expectedMessageType(namespace.toIri(DSPACE_TYPE_CONTRACT_AGREEMENT_MESSAGE_TERM)) + .processId(id) + .message(jsonObject) + .token(token) + .serviceCall(protocolService::notifyAgreed) + .errorProvider(ContractNegotiationError.Builder::newInstance) + .protocol(protocol) + .build(); + + return dspRequestHandler.updateResource(request); + } + +} diff --git a/data-protocols/dsp/dsp-negotiation/lib/dsp-negotiation-http-api-lib/src/testFixtures/java/org/eclipse/edc/protocol/dsp/negotiation/http/api/controller/DspNegotiationApiControllerTestBase.java b/data-protocols/dsp/dsp-negotiation/lib/dsp-negotiation-http-api-lib/src/testFixtures/java/org/eclipse/edc/protocol/dsp/negotiation/http/api/controller/DspNegotiationApiControllerTestBase.java new file mode 100644 index 00000000000..db5f390558e --- /dev/null +++ b/data-protocols/dsp/dsp-negotiation/lib/dsp-negotiation-http-api-lib/src/testFixtures/java/org/eclipse/edc/protocol/dsp/negotiation/http/api/controller/DspNegotiationApiControllerTestBase.java @@ -0,0 +1,218 @@ +/* + * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.edc.protocol.dsp.negotiation.http.api.controller; + +import io.restassured.specification.RequestSpecification; +import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.Response; +import org.eclipse.edc.connector.controlplane.contract.spi.types.agreement.ContractAgreementMessage; +import org.eclipse.edc.connector.controlplane.contract.spi.types.agreement.ContractAgreementVerificationMessage; +import org.eclipse.edc.connector.controlplane.contract.spi.types.agreement.ContractNegotiationEventMessage; +import org.eclipse.edc.connector.controlplane.contract.spi.types.negotiation.ContractNegotiation; +import org.eclipse.edc.connector.controlplane.contract.spi.types.negotiation.ContractNegotiationTerminationMessage; +import org.eclipse.edc.connector.controlplane.contract.spi.types.negotiation.ContractOfferMessage; +import org.eclipse.edc.connector.controlplane.contract.spi.types.negotiation.ContractRequestMessage; +import org.eclipse.edc.connector.controlplane.services.spi.contractnegotiation.ContractNegotiationProtocolService; +import org.eclipse.edc.jsonld.spi.JsonLdNamespace; +import org.eclipse.edc.protocol.dsp.http.spi.message.DspRequestHandler; +import org.eclipse.edc.protocol.dsp.http.spi.message.GetDspRequest; +import org.eclipse.edc.protocol.dsp.http.spi.message.PostDspRequest; +import org.eclipse.edc.web.jersey.testfixtures.RestControllerTestBase; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.mockito.ArgumentCaptor; + +import java.util.stream.Stream; + +import static io.restassured.RestAssured.given; +import static jakarta.json.Json.createObjectBuilder; +import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; +import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON_TYPE; +import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.edc.protocol.dsp.negotiation.http.api.NegotiationApiPaths.AGREEMENT; +import static org.eclipse.edc.protocol.dsp.negotiation.http.api.NegotiationApiPaths.CONTRACT_OFFER; +import static org.eclipse.edc.protocol.dsp.negotiation.http.api.NegotiationApiPaths.CONTRACT_REQUEST; +import static org.eclipse.edc.protocol.dsp.negotiation.http.api.NegotiationApiPaths.EVENT; +import static org.eclipse.edc.protocol.dsp.negotiation.http.api.NegotiationApiPaths.INITIAL_CONTRACT_OFFER; +import static org.eclipse.edc.protocol.dsp.negotiation.http.api.NegotiationApiPaths.INITIAL_CONTRACT_REQUEST; +import static org.eclipse.edc.protocol.dsp.negotiation.http.api.NegotiationApiPaths.TERMINATION; +import static org.eclipse.edc.protocol.dsp.negotiation.http.api.NegotiationApiPaths.VERIFICATION; +import static org.eclipse.edc.protocol.dsp.spi.type.DspNegotiationPropertyAndTypeNames.DSPACE_TYPE_CONTRACT_AGREEMENT_MESSAGE_TERM; +import static org.eclipse.edc.protocol.dsp.spi.type.DspNegotiationPropertyAndTypeNames.DSPACE_TYPE_CONTRACT_AGREEMENT_VERIFICATION_MESSAGE_TERM; +import static org.eclipse.edc.protocol.dsp.spi.type.DspNegotiationPropertyAndTypeNames.DSPACE_TYPE_CONTRACT_NEGOTIATION_EVENT_MESSAGE_TERM; +import static org.eclipse.edc.protocol.dsp.spi.type.DspNegotiationPropertyAndTypeNames.DSPACE_TYPE_CONTRACT_NEGOTIATION_TERMINATION_MESSAGE_TERM; +import static org.eclipse.edc.protocol.dsp.spi.type.DspNegotiationPropertyAndTypeNames.DSPACE_TYPE_CONTRACT_OFFER_MESSAGE_TERM; +import static org.eclipse.edc.protocol.dsp.spi.type.DspNegotiationPropertyAndTypeNames.DSPACE_TYPE_CONTRACT_REQUEST_MESSAGE_TERM; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public abstract class DspNegotiationApiControllerTestBase extends RestControllerTestBase { + + protected final ContractNegotiationProtocolService protocolService = mock(); + protected final DspRequestHandler dspRequestHandler = mock(); + + @Test + void getNegotiation_shouldGetResource() { + when(dspRequestHandler.getResource(any())).thenReturn(Response.ok().type(APPLICATION_JSON_TYPE).build()); + + var result = baseRequest() + .get(basePath() + "negotiationId") + .then() + .contentType(APPLICATION_JSON) + .statusCode(200); + + assertThat(result).isNotNull(); + var captor = ArgumentCaptor.forClass(GetDspRequest.class); + verify(dspRequestHandler).getResource(captor.capture()); + var dspMessage = captor.getValue(); + assertThat(dspMessage.getToken()).isEqualTo("auth"); + assertThat(dspMessage.getId()).isEqualTo("negotiationId"); + assertThat(dspMessage.getResultClass()).isEqualTo(ContractNegotiation.class); + } + + @Test + void initialContractRequest_shouldCreateResource() { + var requestBody = createObjectBuilder().add("@type", namespace().toIri(DSPACE_TYPE_CONTRACT_REQUEST_MESSAGE_TERM)).build(); + when(dspRequestHandler.createResource(any())).thenReturn(Response.ok().type(APPLICATION_JSON_TYPE).build()); + + var result = baseRequest() + .contentType(APPLICATION_JSON) + .body(requestBody) + .post(basePath() + INITIAL_CONTRACT_REQUEST) + .then() + .statusCode(200) + .contentType(APPLICATION_JSON); + + assertThat(result).isNotNull(); + var captor = ArgumentCaptor.forClass(PostDspRequest.class); + verify(dspRequestHandler).createResource(captor.capture()); + var request = captor.getValue(); + assertThat(request.getToken()).isEqualTo("auth"); + assertThat(request.getProcessId()).isEqualTo(null); + assertThat(request.getMessage()).isNotNull(); + assertThat(request.getInputClass()).isEqualTo(ContractRequestMessage.class); + assertThat(request.getResultClass()).isEqualTo(ContractNegotiation.class); + assertThat(request.getExpectedMessageType()).isEqualTo(namespace().toIri(DSPACE_TYPE_CONTRACT_REQUEST_MESSAGE_TERM)); + } + + @Test + void initialContractOffer_shouldCreateResource() { + var requestBody = createObjectBuilder().add("@type", namespace().toIri(DSPACE_TYPE_CONTRACT_OFFER_MESSAGE_TERM)).build(); + when(dspRequestHandler.createResource(any())).thenReturn(Response.ok().type(APPLICATION_JSON_TYPE).build()); + + var result = baseRequest() + .contentType(APPLICATION_JSON) + .body(requestBody) + .post(basePath() + INITIAL_CONTRACT_OFFER) + .then() + .statusCode(200) + .contentType(APPLICATION_JSON); + + assertThat(result).isNotNull(); + var captor = ArgumentCaptor.forClass(PostDspRequest.class); + verify(dspRequestHandler).createResource(captor.capture()); + var request = captor.getValue(); + assertThat(request.getToken()).isEqualTo("auth"); + assertThat(request.getProcessId()).isEqualTo(null); + assertThat(request.getMessage()).isNotNull(); + assertThat(request.getInputClass()).isEqualTo(ContractOfferMessage.class); + assertThat(request.getResultClass()).isEqualTo(ContractNegotiation.class); + assertThat(request.getExpectedMessageType()).isEqualTo(namespace().toIri(DSPACE_TYPE_CONTRACT_OFFER_MESSAGE_TERM)); + } + + /** + * Verifies that an endpoint returns 401 if the identity service cannot verify the identity token. + * + * @param path the request path to the endpoint + */ + @ParameterizedTest + @ArgumentsSource(ControllerMethodArguments.class) + void callEndpoint_shouldUpdateResource(String path, Class messageClass, String messageType) { + when(dspRequestHandler.updateResource(any())).thenReturn(Response.ok().type(APPLICATION_JSON_TYPE).build()); + var requestBody = createObjectBuilder().add("http://schema/key", "value").build(); + + baseRequest() + .contentType(APPLICATION_JSON) + .body(requestBody) + .post(basePath() + path) + .then() + .contentType(APPLICATION_JSON) + .statusCode(200); + + var captor = ArgumentCaptor.forClass(PostDspRequest.class); + verify(dspRequestHandler).updateResource(captor.capture()); + var request = captor.getValue(); + assertThat(request.getExpectedMessageType()).isEqualTo(namespace().toIri(messageType)); + assertThat(request.getToken()).isEqualTo("auth"); + assertThat(request.getProcessId()).isEqualTo("testId"); + assertThat(request.getMessage()).isNotNull(); + assertThat(request.getInputClass()).isEqualTo(messageClass); + assertThat(request.getResultClass()).isEqualTo(ContractNegotiation.class); + } + + protected abstract String basePath(); + + protected abstract JsonLdNamespace namespace(); + + private RequestSpecification baseRequest() { + var authHeader = "auth"; + return given() + .baseUri("http://localhost:" + port) + .basePath("/") + .header(HttpHeaders.AUTHORIZATION, authHeader) + .when(); + } + + private static class ControllerMethodArguments implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of( + "testId" + CONTRACT_REQUEST, + ContractRequestMessage.class, + DSPACE_TYPE_CONTRACT_REQUEST_MESSAGE_TERM), + Arguments.of( + "testId" + EVENT, + ContractNegotiationEventMessage.class, + DSPACE_TYPE_CONTRACT_NEGOTIATION_EVENT_MESSAGE_TERM), + Arguments.of( + "testId" + AGREEMENT + VERIFICATION, + ContractAgreementVerificationMessage.class, + DSPACE_TYPE_CONTRACT_AGREEMENT_VERIFICATION_MESSAGE_TERM), + Arguments.of( + "testId" + TERMINATION, + ContractNegotiationTerminationMessage.class, + DSPACE_TYPE_CONTRACT_NEGOTIATION_TERMINATION_MESSAGE_TERM), + Arguments.of( + "testId" + AGREEMENT, + ContractAgreementMessage.class, + DSPACE_TYPE_CONTRACT_AGREEMENT_MESSAGE_TERM), + Arguments.of( + "testId" + CONTRACT_OFFER, + ContractOfferMessage.class, + DSPACE_TYPE_CONTRACT_OFFER_MESSAGE_TERM) + ); + } + } +} + + + diff --git a/data-protocols/dsp/dsp-transfer-process/dsp-transfer-process-http-api/build.gradle.kts b/data-protocols/dsp/dsp-transfer-process/dsp-transfer-process-http-api/build.gradle.kts index 6e6eddec206..7efd550351e 100644 --- a/data-protocols/dsp/dsp-transfer-process/dsp-transfer-process-http-api/build.gradle.kts +++ b/data-protocols/dsp/dsp-transfer-process/dsp-transfer-process-http-api/build.gradle.kts @@ -26,12 +26,14 @@ dependencies { implementation(project(":spi:common:json-ld-spi")) implementation(project(":data-protocols:dsp:dsp-transfer-process:lib:dsp-transfer-process-validation-lib")) + implementation(project(":data-protocols:dsp:dsp-transfer-process:lib:dsp-transfer-process-http-api-lib")) implementation(project(":extensions:common:http:lib:jersey-providers-lib")) implementation(libs.jakarta.rsApi) testImplementation(project(":core:common:junit")) testImplementation(testFixtures(project(":extensions:common:http:jersey-core"))) + testImplementation(testFixtures(project(":data-protocols:dsp:dsp-transfer-process:lib:dsp-transfer-process-http-api-lib"))) testImplementation(libs.restAssured) } diff --git a/data-protocols/dsp/dsp-transfer-process/dsp-transfer-process-http-api/src/main/java/org/eclipse/edc/protocol/dsp/transferprocess/http/api/controller/DspTransferProcessApiController.java b/data-protocols/dsp/dsp-transfer-process/dsp-transfer-process-http-api/src/main/java/org/eclipse/edc/protocol/dsp/transferprocess/http/api/controller/DspTransferProcessApiController.java index e03d9e510d9..9b0f99f80a2 100644 --- a/data-protocols/dsp/dsp-transfer-process/dsp-transfer-process-http-api/src/main/java/org/eclipse/edc/protocol/dsp/transferprocess/http/api/controller/DspTransferProcessApiController.java +++ b/data-protocols/dsp/dsp-transfer-process/dsp-transfer-process-http-api/src/main/java/org/eclipse/edc/protocol/dsp/transferprocess/http/api/controller/DspTransferProcessApiController.java @@ -14,43 +14,16 @@ package org.eclipse.edc.protocol.dsp.transferprocess.http.api.controller; -import jakarta.json.JsonObject; import jakarta.ws.rs.Consumes; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.HeaderParam; -import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; -import jakarta.ws.rs.PathParam; import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; import org.eclipse.edc.connector.controlplane.services.spi.transferprocess.TransferProcessProtocolService; -import org.eclipse.edc.connector.controlplane.transfer.spi.types.TransferProcess; -import org.eclipse.edc.connector.controlplane.transfer.spi.types.protocol.TransferCompletionMessage; -import org.eclipse.edc.connector.controlplane.transfer.spi.types.protocol.TransferError; -import org.eclipse.edc.connector.controlplane.transfer.spi.types.protocol.TransferRequestMessage; -import org.eclipse.edc.connector.controlplane.transfer.spi.types.protocol.TransferStartMessage; -import org.eclipse.edc.connector.controlplane.transfer.spi.types.protocol.TransferSuspensionMessage; -import org.eclipse.edc.connector.controlplane.transfer.spi.types.protocol.TransferTerminationMessage; -import org.eclipse.edc.jsonld.spi.JsonLdNamespace; import org.eclipse.edc.protocol.dsp.http.spi.message.DspRequestHandler; -import org.eclipse.edc.protocol.dsp.http.spi.message.GetDspRequest; -import org.eclipse.edc.protocol.dsp.http.spi.message.PostDspRequest; -import static jakarta.ws.rs.core.HttpHeaders.AUTHORIZATION; import static org.eclipse.edc.protocol.dsp.http.spi.types.HttpMessageProtocol.DATASPACE_PROTOCOL_HTTP; import static org.eclipse.edc.protocol.dsp.spi.type.DspConstants.DSP_NAMESPACE_V_08; -import static org.eclipse.edc.protocol.dsp.spi.type.DspTransferProcessPropertyAndTypeNames.DSPACE_TYPE_TRANSFER_COMPLETION_MESSAGE_TERM; -import static org.eclipse.edc.protocol.dsp.spi.type.DspTransferProcessPropertyAndTypeNames.DSPACE_TYPE_TRANSFER_REQUEST_MESSAGE_TERM; -import static org.eclipse.edc.protocol.dsp.spi.type.DspTransferProcessPropertyAndTypeNames.DSPACE_TYPE_TRANSFER_START_MESSAGE_TERM; -import static org.eclipse.edc.protocol.dsp.spi.type.DspTransferProcessPropertyAndTypeNames.DSPACE_TYPE_TRANSFER_SUSPENSION_MESSAGE_TERM; -import static org.eclipse.edc.protocol.dsp.spi.type.DspTransferProcessPropertyAndTypeNames.DSPACE_TYPE_TRANSFER_TERMINATION_MESSAGE_TERM; import static org.eclipse.edc.protocol.dsp.transferprocess.http.api.TransferProcessApiPaths.BASE_PATH; -import static org.eclipse.edc.protocol.dsp.transferprocess.http.api.TransferProcessApiPaths.TRANSFER_COMPLETION; -import static org.eclipse.edc.protocol.dsp.transferprocess.http.api.TransferProcessApiPaths.TRANSFER_INITIAL_REQUEST; -import static org.eclipse.edc.protocol.dsp.transferprocess.http.api.TransferProcessApiPaths.TRANSFER_START; -import static org.eclipse.edc.protocol.dsp.transferprocess.http.api.TransferProcessApiPaths.TRANSFER_SUSPENSION; -import static org.eclipse.edc.protocol.dsp.transferprocess.http.api.TransferProcessApiPaths.TRANSFER_TERMINATION; /** * Provides the endpoints for receiving messages regarding transfers, like initiating, completing @@ -59,160 +32,10 @@ @Consumes({ MediaType.APPLICATION_JSON }) @Produces({ MediaType.APPLICATION_JSON }) @Path(BASE_PATH) -public class DspTransferProcessApiController { - - private final TransferProcessProtocolService protocolService; - private final DspRequestHandler dspRequestHandler; - private final String protocol; - private final JsonLdNamespace namespace; +public class DspTransferProcessApiController extends BaseDspTransferProcessApiController { public DspTransferProcessApiController(TransferProcessProtocolService protocolService, DspRequestHandler dspRequestHandler) { - this(protocolService, dspRequestHandler, DATASPACE_PROTOCOL_HTTP, DSP_NAMESPACE_V_08); - } - - public DspTransferProcessApiController(TransferProcessProtocolService protocolService, DspRequestHandler dspRequestHandler, String protocol, JsonLdNamespace namespace) { - this.protocolService = protocolService; - this.dspRequestHandler = dspRequestHandler; - this.protocol = protocol; - this.namespace = namespace; - } - - /** - * Retrieves an existing transfer process. This functionality is not yet supported. - * - * @param id the ID of the process - * @return the requested transfer process or an error. - */ - @GET - @Path("/{id}") - public Response getTransferProcess(@PathParam("id") String id, @HeaderParam(AUTHORIZATION) String token) { - var request = GetDspRequest.Builder.newInstance(TransferProcess.class, TransferError.class) - .id(id) - .token(token) - .serviceCall(protocolService::findById) - .protocol(protocol) - .errorProvider(TransferError.Builder::newInstance) - .build(); - - return dspRequestHandler.getResource(request); - } - - /** - * Initiates a new transfer process that has been requested by the counter-party. - * - * @param jsonObject the {@link TransferRequestMessage} in JSON-LD expanded form - * @param token the authorization header - * @return the created transfer process or an error. - */ - @POST - @Path(TRANSFER_INITIAL_REQUEST) - public Response initiateTransferProcess(JsonObject jsonObject, @HeaderParam(AUTHORIZATION) String token) { - var request = PostDspRequest.Builder.newInstance(TransferRequestMessage.class, TransferProcess.class, TransferError.class) - .message(jsonObject) - .token(token) - .expectedMessageType(namespace.toIri(DSPACE_TYPE_TRANSFER_REQUEST_MESSAGE_TERM)) - .serviceCall(protocolService::notifyRequested) - .errorProvider(TransferError.Builder::newInstance) - .protocol(protocol) - .build(); - - return dspRequestHandler.createResource(request); - } - - /** - * Notifies the connector that a transfer process has been started by the counter-part. - * - * @param id the ID of the process - * @param jsonObject the {@link TransferStartMessage} in JSON-LD expanded form - * @param token the authorization header - * @return empty response or error. - */ - @POST - @Path("{id}" + TRANSFER_START) - public Response transferProcessStart(@PathParam("id") String id, JsonObject jsonObject, @HeaderParam(AUTHORIZATION) String token) { - var request = PostDspRequest.Builder.newInstance(TransferStartMessage.class, TransferProcess.class, TransferError.class) - .processId(id) - .expectedMessageType(namespace.toIri(DSPACE_TYPE_TRANSFER_START_MESSAGE_TERM)) - .message(jsonObject) - .token(token) - .serviceCall(protocolService::notifyStarted) - .errorProvider(TransferError.Builder::newInstance) - .protocol(protocol) - .build(); - - return dspRequestHandler.updateResource(request); - } - - /** - * Notifies the connector that a transfer process has been completed by the counter-part. - * - * @param id the ID of the process - * @param jsonObject the {@link TransferCompletionMessage} in JSON-LD expanded form - * @param token the authorization header - * @return empty response or error. - */ - @POST - @Path("{id}" + TRANSFER_COMPLETION) - public Response transferProcessCompletion(@PathParam("id") String id, JsonObject jsonObject, @HeaderParam(AUTHORIZATION) String token) { - var request = PostDspRequest.Builder.newInstance(TransferCompletionMessage.class, TransferProcess.class, TransferError.class) - .processId(id) - .expectedMessageType(namespace.toIri(DSPACE_TYPE_TRANSFER_COMPLETION_MESSAGE_TERM)) - .message(jsonObject) - .token(token) - .serviceCall(protocolService::notifyCompleted) - .errorProvider(TransferError.Builder::newInstance) - .protocol(protocol) - .build(); - - return dspRequestHandler.updateResource(request); - } - - /** - * Notifies the connector that a transfer process has been terminated by the counter-part. - * - * @param id the ID of the process - * @param jsonObject the {@link TransferTerminationMessage} in JSON-LD expanded form - * @param token the authorization header - * @return empty response or error. - */ - @POST - @Path("{id}" + TRANSFER_TERMINATION) - public Response transferProcessTermination(@PathParam("id") String id, JsonObject jsonObject, @HeaderParam(AUTHORIZATION) String token) { - var request = PostDspRequest.Builder.newInstance(TransferTerminationMessage.class, TransferProcess.class, TransferError.class) - .processId(id) - .expectedMessageType(namespace.toIri(DSPACE_TYPE_TRANSFER_TERMINATION_MESSAGE_TERM)) - .message(jsonObject) - .token(token) - .serviceCall(protocolService::notifyTerminated) - .errorProvider(TransferError.Builder::newInstance) - .protocol(protocol) - .build(); - - return dspRequestHandler.updateResource(request); - } - - /** - * Notifies the connector that a transfer process has been suspended by the counter-part. - * - * @param id the ID of the process - * @param jsonObject the {@link TransferSuspensionMessage} in JSON-LD expanded form - * @param token the authorization header - * @return empty response or error. - */ - @POST - @Path("{id}" + TRANSFER_SUSPENSION) - public Response transferProcessSuspension(@PathParam("id") String id, JsonObject jsonObject, @HeaderParam(AUTHORIZATION) String token) { - var request = PostDspRequest.Builder.newInstance(TransferSuspensionMessage.class, TransferProcess.class, TransferError.class) - .processId(id) - .expectedMessageType(namespace.toIri(DSPACE_TYPE_TRANSFER_SUSPENSION_MESSAGE_TERM)) - .message(jsonObject) - .token(token) - .serviceCall(protocolService::notifySuspended) - .errorProvider(TransferError.Builder::newInstance) - .protocol(protocol) - .build(); - - return dspRequestHandler.updateResource(request); + super(protocolService, dspRequestHandler, DATASPACE_PROTOCOL_HTTP, DSP_NAMESPACE_V_08); } } diff --git a/data-protocols/dsp/dsp-transfer-process/dsp-transfer-process-http-api/src/main/java/org/eclipse/edc/protocol/dsp/transferprocess/http/api/controller/DspTransferProcessApiController20241.java b/data-protocols/dsp/dsp-transfer-process/dsp-transfer-process-http-api/src/main/java/org/eclipse/edc/protocol/dsp/transferprocess/http/api/controller/DspTransferProcessApiController20241.java index 0f257c425ab..73fc6b9c04f 100644 --- a/data-protocols/dsp/dsp-transfer-process/dsp-transfer-process-http-api/src/main/java/org/eclipse/edc/protocol/dsp/transferprocess/http/api/controller/DspTransferProcessApiController20241.java +++ b/data-protocols/dsp/dsp-transfer-process/dsp-transfer-process-http-api/src/main/java/org/eclipse/edc/protocol/dsp/transferprocess/http/api/controller/DspTransferProcessApiController20241.java @@ -28,12 +28,12 @@ /** - * Versioned Transfer endpoint, same as {@link DspTransferProcessApiController} but exposed on the /2024/1 path + * Versioned Transfer endpoint for 2024/1 protocol version */ @Consumes({ MediaType.APPLICATION_JSON }) @Produces({ MediaType.APPLICATION_JSON }) @Path(DspVersions.V_2024_1_PATH + BASE_PATH) -public class DspTransferProcessApiController20241 extends DspTransferProcessApiController { +public class DspTransferProcessApiController20241 extends BaseDspTransferProcessApiController { public DspTransferProcessApiController20241(TransferProcessProtocolService protocolService, DspRequestHandler dspRequestHandler) { super(protocolService, dspRequestHandler, DATASPACE_PROTOCOL_HTTP_V_2024_1, DSP_NAMESPACE_V_2024_1); diff --git a/data-protocols/dsp/dsp-transfer-process/dsp-transfer-process-http-api/src/test/java/org/eclipse/edc/protocol/dsp/transferprocess/http/api/controller/DspTransferProcessApiControllerTest.java b/data-protocols/dsp/dsp-transfer-process/dsp-transfer-process-http-api/src/test/java/org/eclipse/edc/protocol/dsp/transferprocess/http/api/controller/DspTransferProcessApiControllerTest.java index 15c25bc8dbb..543517130ae 100644 --- a/data-protocols/dsp/dsp-transfer-process/dsp-transfer-process-http-api/src/test/java/org/eclipse/edc/protocol/dsp/transferprocess/http/api/controller/DspTransferProcessApiControllerTest.java +++ b/data-protocols/dsp/dsp-transfer-process/dsp-transfer-process-http-api/src/test/java/org/eclipse/edc/protocol/dsp/transferprocess/http/api/controller/DspTransferProcessApiControllerTest.java @@ -14,183 +14,21 @@ package org.eclipse.edc.protocol.dsp.transferprocess.http.api.controller; -import io.restassured.specification.RequestSpecification; -import jakarta.json.Json; -import jakarta.ws.rs.core.HttpHeaders; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; -import org.eclipse.edc.connector.controlplane.services.spi.transferprocess.TransferProcessProtocolService; -import org.eclipse.edc.connector.controlplane.transfer.spi.types.TransferProcess; -import org.eclipse.edc.connector.controlplane.transfer.spi.types.protocol.TransferCompletionMessage; -import org.eclipse.edc.connector.controlplane.transfer.spi.types.protocol.TransferRequestMessage; -import org.eclipse.edc.connector.controlplane.transfer.spi.types.protocol.TransferStartMessage; -import org.eclipse.edc.connector.controlplane.transfer.spi.types.protocol.TransferSuspensionMessage; -import org.eclipse.edc.connector.controlplane.transfer.spi.types.protocol.TransferTerminationMessage; import org.eclipse.edc.jsonld.spi.JsonLdNamespace; import org.eclipse.edc.junit.annotations.ApiTest; -import org.eclipse.edc.protocol.dsp.http.spi.message.DspRequestHandler; -import org.eclipse.edc.protocol.dsp.http.spi.message.GetDspRequest; -import org.eclipse.edc.protocol.dsp.http.spi.message.PostDspRequest; -import org.eclipse.edc.web.jersey.testfixtures.RestControllerTestBase; import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtensionContext; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.ArgumentsProvider; -import org.junit.jupiter.params.provider.ArgumentsSource; -import org.mockito.ArgumentCaptor; -import java.util.stream.Stream; - -import static io.restassured.RestAssured.given; -import static jakarta.json.Json.createObjectBuilder; -import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON_TYPE; -import static org.assertj.core.api.Assertions.assertThat; -import static org.eclipse.edc.jsonld.spi.JsonLdKeywords.TYPE; import static org.eclipse.edc.protocol.dsp.spi.type.DspConstants.DSP_NAMESPACE_V_08; import static org.eclipse.edc.protocol.dsp.spi.type.DspConstants.DSP_NAMESPACE_V_2024_1; -import static org.eclipse.edc.protocol.dsp.spi.type.DspTransferProcessPropertyAndTypeNames.DSPACE_TYPE_TRANSFER_COMPLETION_MESSAGE_TERM; -import static org.eclipse.edc.protocol.dsp.spi.type.DspTransferProcessPropertyAndTypeNames.DSPACE_TYPE_TRANSFER_REQUEST_MESSAGE_TERM; -import static org.eclipse.edc.protocol.dsp.spi.type.DspTransferProcessPropertyAndTypeNames.DSPACE_TYPE_TRANSFER_START_MESSAGE_TERM; -import static org.eclipse.edc.protocol.dsp.spi.type.DspTransferProcessPropertyAndTypeNames.DSPACE_TYPE_TRANSFER_SUSPENSION_MESSAGE_TERM; -import static org.eclipse.edc.protocol.dsp.spi.type.DspTransferProcessPropertyAndTypeNames.DSPACE_TYPE_TRANSFER_TERMINATION_MESSAGE_TERM; import static org.eclipse.edc.protocol.dsp.spi.version.DspVersions.V_2024_1_PATH; import static org.eclipse.edc.protocol.dsp.transferprocess.http.api.TransferProcessApiPaths.BASE_PATH; -import static org.eclipse.edc.protocol.dsp.transferprocess.http.api.TransferProcessApiPaths.TRANSFER_COMPLETION; -import static org.eclipse.edc.protocol.dsp.transferprocess.http.api.TransferProcessApiPaths.TRANSFER_INITIAL_REQUEST; -import static org.eclipse.edc.protocol.dsp.transferprocess.http.api.TransferProcessApiPaths.TRANSFER_START; -import static org.eclipse.edc.protocol.dsp.transferprocess.http.api.TransferProcessApiPaths.TRANSFER_SUSPENSION; -import static org.eclipse.edc.protocol.dsp.transferprocess.http.api.TransferProcessApiPaths.TRANSFER_TERMINATION; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; @ApiTest class DspTransferProcessApiControllerTest { - - abstract static class Tests extends RestControllerTestBase { - - private static final String PROCESS_ID = "testId"; - protected final TransferProcessProtocolService protocolService = mock(); - protected final DspRequestHandler dspRequestHandler = mock(); - - @Test - void getTransferProcess_shouldGetResource() { - var id = "transferProcessId"; - - when(dspRequestHandler.getResource(any())).thenReturn(Response.ok().type(APPLICATION_JSON_TYPE).build()); - - baseRequest() - .get(basePath() + id) - .then() - .contentType(MediaType.APPLICATION_JSON) - .statusCode(200); - - var captor = ArgumentCaptor.forClass(GetDspRequest.class); - verify(dspRequestHandler).getResource(captor.capture()); - var dspRequest = captor.getValue(); - assertThat(dspRequest.getId()).isEqualTo("transferProcessId"); - assertThat(dspRequest.getResultClass()).isEqualTo(TransferProcess.class); - assertThat(dspRequest.getToken()).isEqualTo("auth"); - } - - @Test - void initiateTransferProcess_shouldCreateResource() { - when(dspRequestHandler.createResource(any())).thenReturn(Response.ok().type(APPLICATION_JSON_TYPE).build()); - - var result = baseRequest() - .contentType(MediaType.APPLICATION_JSON) - .body(Json.createObjectBuilder().add(TYPE, namespace().toIri(DSPACE_TYPE_TRANSFER_REQUEST_MESSAGE_TERM)).build()) - .post(basePath() + TRANSFER_INITIAL_REQUEST) - .then() - .statusCode(200) - .contentType(MediaType.APPLICATION_JSON); - - assertThat(result).isNotNull(); - var captor = ArgumentCaptor.forClass(PostDspRequest.class); - verify(dspRequestHandler).createResource(captor.capture()); - var request = captor.getValue(); - assertThat(request.getToken()).isEqualTo("auth"); - assertThat(request.getProcessId()).isEqualTo(null); - assertThat(request.getInputClass()).isEqualTo(TransferRequestMessage.class); - assertThat(request.getResultClass()).isEqualTo(TransferProcess.class); - assertThat(request.getMessage()).isNotNull(); - assertThat(request.getExpectedMessageType()).isEqualTo(namespace().toIri(DSPACE_TYPE_TRANSFER_REQUEST_MESSAGE_TERM)); - } - - /** - * Verifies that an endpoint returns 401 if the identity service cannot verify the identity token. - * - * @param path the request path to the endpoint - */ - @ParameterizedTest - @ArgumentsSource(ControllerMethodArguments.class) - void callEndpoint_shouldUpdateResource(String path, Class messageClass, String messageType) { - when(dspRequestHandler.updateResource(any())).thenReturn(Response.ok().type(APPLICATION_JSON_TYPE).build()); - var requestBody = createObjectBuilder().add("http://schema/key", "value").build(); - - baseRequest() - .contentType(MediaType.APPLICATION_JSON) - .body(requestBody) - .post(basePath() + path) - .then() - .contentType(MediaType.APPLICATION_JSON) - .statusCode(200); - - var captor = ArgumentCaptor.forClass(PostDspRequest.class); - verify(dspRequestHandler).updateResource(captor.capture()); - var request = captor.getValue(); - assertThat(request.getToken()).isEqualTo("auth"); - assertThat(request.getProcessId()).isEqualTo(PROCESS_ID); - assertThat(request.getMessage()).isNotNull(); - assertThat(request.getInputClass()).isEqualTo(messageClass); - assertThat(request.getResultClass()).isEqualTo(TransferProcess.class); - assertThat(request.getExpectedMessageType()).isEqualTo(namespace().toIri(messageType)); - } - - protected abstract String basePath(); - - protected abstract JsonLdNamespace namespace(); - - private RequestSpecification baseRequest() { - return given() - .baseUri("http://localhost:" + port) - .basePath("/") - .header(HttpHeaders.AUTHORIZATION, "auth") - .when(); - } - - private static class ControllerMethodArguments implements ArgumentsProvider { - @Override - public Stream provideArguments(ExtensionContext context) { - return Stream.of( - Arguments.of( - PROCESS_ID + TRANSFER_START, - TransferStartMessage.class, - DSPACE_TYPE_TRANSFER_START_MESSAGE_TERM), - Arguments.of( - PROCESS_ID + TRANSFER_COMPLETION, - TransferCompletionMessage.class, - DSPACE_TYPE_TRANSFER_COMPLETION_MESSAGE_TERM), - Arguments.of( - PROCESS_ID + TRANSFER_SUSPENSION, - TransferSuspensionMessage.class, - DSPACE_TYPE_TRANSFER_SUSPENSION_MESSAGE_TERM), - Arguments.of( - PROCESS_ID + TRANSFER_TERMINATION, - TransferTerminationMessage.class, - DSPACE_TYPE_TRANSFER_TERMINATION_MESSAGE_TERM) - ); - } - } - - } - + @ApiTest @Nested - class DspTransferProcessApiControllerV08Test extends Tests { + class DspTransferProcessApiControllerV08Test extends DspTransferProcessApiControllerBaseTest { @Override protected String basePath() { @@ -210,7 +48,7 @@ protected Object controller() { @ApiTest @Nested - class DspTransferProcessControllerV20241Test extends Tests { + class DspTransferProcessControllerV20241Test extends DspTransferProcessApiControllerBaseTest { @Override protected String basePath() { diff --git a/data-protocols/dsp/dsp-transfer-process/lib/dsp-transfer-process-http-api-lib/build.gradle.kts b/data-protocols/dsp/dsp-transfer-process/lib/dsp-transfer-process-http-api-lib/build.gradle.kts new file mode 100644 index 00000000000..d15718acfee --- /dev/null +++ b/data-protocols/dsp/dsp-transfer-process/lib/dsp-transfer-process-http-api-lib/build.gradle.kts @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +plugins { + `java-library` + `java-test-fixtures` +} + +dependencies { + api(project(":data-protocols:dsp:dsp-spi")) + api(project(":data-protocols:dsp:dsp-http-spi")) + api(project(":spi:common:json-ld-spi")) + + testFixturesImplementation(project(":core:common:junit")) + testFixturesImplementation(testFixtures(project(":extensions:common:http:jersey-core"))) + testFixturesImplementation(libs.restAssured) + testFixturesImplementation(libs.assertj) + testFixturesImplementation(libs.mockito.core) + testFixturesImplementation(libs.junit.jupiter.params) +} \ No newline at end of file diff --git a/data-protocols/dsp/dsp-transfer-process/dsp-transfer-process-http-api/src/main/java/org/eclipse/edc/protocol/dsp/transferprocess/http/api/TransferProcessApiPaths.java b/data-protocols/dsp/dsp-transfer-process/lib/dsp-transfer-process-http-api-lib/src/main/java/org/eclipse/edc/protocol/dsp/transferprocess/http/api/TransferProcessApiPaths.java similarity index 100% rename from data-protocols/dsp/dsp-transfer-process/dsp-transfer-process-http-api/src/main/java/org/eclipse/edc/protocol/dsp/transferprocess/http/api/TransferProcessApiPaths.java rename to data-protocols/dsp/dsp-transfer-process/lib/dsp-transfer-process-http-api-lib/src/main/java/org/eclipse/edc/protocol/dsp/transferprocess/http/api/TransferProcessApiPaths.java diff --git a/data-protocols/dsp/dsp-transfer-process/lib/dsp-transfer-process-http-api-lib/src/main/java/org/eclipse/edc/protocol/dsp/transferprocess/http/api/controller/BaseDspTransferProcessApiController.java b/data-protocols/dsp/dsp-transfer-process/lib/dsp-transfer-process-http-api-lib/src/main/java/org/eclipse/edc/protocol/dsp/transferprocess/http/api/controller/BaseDspTransferProcessApiController.java new file mode 100644 index 00000000000..6d63153ff18 --- /dev/null +++ b/data-protocols/dsp/dsp-transfer-process/lib/dsp-transfer-process-http-api-lib/src/main/java/org/eclipse/edc/protocol/dsp/transferprocess/http/api/controller/BaseDspTransferProcessApiController.java @@ -0,0 +1,205 @@ +/* + * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.edc.protocol.dsp.transferprocess.http.api.controller; + +import jakarta.json.JsonObject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.HeaderParam; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.core.Response; +import org.eclipse.edc.connector.controlplane.services.spi.transferprocess.TransferProcessProtocolService; +import org.eclipse.edc.connector.controlplane.transfer.spi.types.TransferProcess; +import org.eclipse.edc.connector.controlplane.transfer.spi.types.protocol.TransferCompletionMessage; +import org.eclipse.edc.connector.controlplane.transfer.spi.types.protocol.TransferError; +import org.eclipse.edc.connector.controlplane.transfer.spi.types.protocol.TransferRequestMessage; +import org.eclipse.edc.connector.controlplane.transfer.spi.types.protocol.TransferStartMessage; +import org.eclipse.edc.connector.controlplane.transfer.spi.types.protocol.TransferSuspensionMessage; +import org.eclipse.edc.connector.controlplane.transfer.spi.types.protocol.TransferTerminationMessage; +import org.eclipse.edc.jsonld.spi.JsonLdNamespace; +import org.eclipse.edc.protocol.dsp.http.spi.message.DspRequestHandler; +import org.eclipse.edc.protocol.dsp.http.spi.message.GetDspRequest; +import org.eclipse.edc.protocol.dsp.http.spi.message.PostDspRequest; + +import static jakarta.ws.rs.core.HttpHeaders.AUTHORIZATION; +import static org.eclipse.edc.protocol.dsp.spi.type.DspTransferProcessPropertyAndTypeNames.DSPACE_TYPE_TRANSFER_COMPLETION_MESSAGE_TERM; +import static org.eclipse.edc.protocol.dsp.spi.type.DspTransferProcessPropertyAndTypeNames.DSPACE_TYPE_TRANSFER_REQUEST_MESSAGE_TERM; +import static org.eclipse.edc.protocol.dsp.spi.type.DspTransferProcessPropertyAndTypeNames.DSPACE_TYPE_TRANSFER_START_MESSAGE_TERM; +import static org.eclipse.edc.protocol.dsp.spi.type.DspTransferProcessPropertyAndTypeNames.DSPACE_TYPE_TRANSFER_SUSPENSION_MESSAGE_TERM; +import static org.eclipse.edc.protocol.dsp.spi.type.DspTransferProcessPropertyAndTypeNames.DSPACE_TYPE_TRANSFER_TERMINATION_MESSAGE_TERM; +import static org.eclipse.edc.protocol.dsp.transferprocess.http.api.TransferProcessApiPaths.TRANSFER_COMPLETION; +import static org.eclipse.edc.protocol.dsp.transferprocess.http.api.TransferProcessApiPaths.TRANSFER_INITIAL_REQUEST; +import static org.eclipse.edc.protocol.dsp.transferprocess.http.api.TransferProcessApiPaths.TRANSFER_START; +import static org.eclipse.edc.protocol.dsp.transferprocess.http.api.TransferProcessApiPaths.TRANSFER_SUSPENSION; +import static org.eclipse.edc.protocol.dsp.transferprocess.http.api.TransferProcessApiPaths.TRANSFER_TERMINATION; + +/** + * Provides the endpoints for receiving messages regarding transfers, like initiating, completing + * and terminating a transfer process. + */ +public abstract class BaseDspTransferProcessApiController { + + private final TransferProcessProtocolService protocolService; + private final DspRequestHandler dspRequestHandler; + private final String protocol; + private final JsonLdNamespace namespace; + + public BaseDspTransferProcessApiController(TransferProcessProtocolService protocolService, DspRequestHandler dspRequestHandler, String protocol, JsonLdNamespace namespace) { + this.protocolService = protocolService; + this.dspRequestHandler = dspRequestHandler; + this.protocol = protocol; + this.namespace = namespace; + } + + /** + * Retrieves an existing transfer process. This functionality is not yet supported. + * + * @param id the ID of the process + * @return the requested transfer process or an error. + */ + @GET + @Path("/{id}") + public Response getTransferProcess(@PathParam("id") String id, @HeaderParam(AUTHORIZATION) String token) { + var request = GetDspRequest.Builder.newInstance(TransferProcess.class, TransferError.class) + .id(id) + .token(token) + .serviceCall(protocolService::findById) + .protocol(protocol) + .errorProvider(TransferError.Builder::newInstance) + .build(); + + return dspRequestHandler.getResource(request); + } + + /** + * Initiates a new transfer process that has been requested by the counter-party. + * + * @param jsonObject the {@link TransferRequestMessage} in JSON-LD expanded form + * @param token the authorization header + * @return the created transfer process or an error. + */ + @POST + @Path(TRANSFER_INITIAL_REQUEST) + public Response initiateTransferProcess(JsonObject jsonObject, @HeaderParam(AUTHORIZATION) String token) { + var request = PostDspRequest.Builder.newInstance(TransferRequestMessage.class, TransferProcess.class, TransferError.class) + .message(jsonObject) + .token(token) + .expectedMessageType(namespace.toIri(DSPACE_TYPE_TRANSFER_REQUEST_MESSAGE_TERM)) + .serviceCall(protocolService::notifyRequested) + .errorProvider(TransferError.Builder::newInstance) + .protocol(protocol) + .build(); + + return dspRequestHandler.createResource(request); + } + + /** + * Notifies the connector that a transfer process has been started by the counter-part. + * + * @param id the ID of the process + * @param jsonObject the {@link TransferStartMessage} in JSON-LD expanded form + * @param token the authorization header + * @return empty response or error. + */ + @POST + @Path("{id}" + TRANSFER_START) + public Response transferProcessStart(@PathParam("id") String id, JsonObject jsonObject, @HeaderParam(AUTHORIZATION) String token) { + var request = PostDspRequest.Builder.newInstance(TransferStartMessage.class, TransferProcess.class, TransferError.class) + .processId(id) + .expectedMessageType(namespace.toIri(DSPACE_TYPE_TRANSFER_START_MESSAGE_TERM)) + .message(jsonObject) + .token(token) + .serviceCall(protocolService::notifyStarted) + .errorProvider(TransferError.Builder::newInstance) + .protocol(protocol) + .build(); + + return dspRequestHandler.updateResource(request); + } + + /** + * Notifies the connector that a transfer process has been completed by the counter-part. + * + * @param id the ID of the process + * @param jsonObject the {@link TransferCompletionMessage} in JSON-LD expanded form + * @param token the authorization header + * @return empty response or error. + */ + @POST + @Path("{id}" + TRANSFER_COMPLETION) + public Response transferProcessCompletion(@PathParam("id") String id, JsonObject jsonObject, @HeaderParam(AUTHORIZATION) String token) { + var request = PostDspRequest.Builder.newInstance(TransferCompletionMessage.class, TransferProcess.class, TransferError.class) + .processId(id) + .expectedMessageType(namespace.toIri(DSPACE_TYPE_TRANSFER_COMPLETION_MESSAGE_TERM)) + .message(jsonObject) + .token(token) + .serviceCall(protocolService::notifyCompleted) + .errorProvider(TransferError.Builder::newInstance) + .protocol(protocol) + .build(); + + return dspRequestHandler.updateResource(request); + } + + /** + * Notifies the connector that a transfer process has been terminated by the counter-part. + * + * @param id the ID of the process + * @param jsonObject the {@link TransferTerminationMessage} in JSON-LD expanded form + * @param token the authorization header + * @return empty response or error. + */ + @POST + @Path("{id}" + TRANSFER_TERMINATION) + public Response transferProcessTermination(@PathParam("id") String id, JsonObject jsonObject, @HeaderParam(AUTHORIZATION) String token) { + var request = PostDspRequest.Builder.newInstance(TransferTerminationMessage.class, TransferProcess.class, TransferError.class) + .processId(id) + .expectedMessageType(namespace.toIri(DSPACE_TYPE_TRANSFER_TERMINATION_MESSAGE_TERM)) + .message(jsonObject) + .token(token) + .serviceCall(protocolService::notifyTerminated) + .errorProvider(TransferError.Builder::newInstance) + .protocol(protocol) + .build(); + + return dspRequestHandler.updateResource(request); + } + + /** + * Notifies the connector that a transfer process has been suspended by the counter-part. + * + * @param id the ID of the process + * @param jsonObject the {@link TransferSuspensionMessage} in JSON-LD expanded form + * @param token the authorization header + * @return empty response or error. + */ + @POST + @Path("{id}" + TRANSFER_SUSPENSION) + public Response transferProcessSuspension(@PathParam("id") String id, JsonObject jsonObject, @HeaderParam(AUTHORIZATION) String token) { + var request = PostDspRequest.Builder.newInstance(TransferSuspensionMessage.class, TransferProcess.class, TransferError.class) + .processId(id) + .expectedMessageType(namespace.toIri(DSPACE_TYPE_TRANSFER_SUSPENSION_MESSAGE_TERM)) + .message(jsonObject) + .token(token) + .serviceCall(protocolService::notifySuspended) + .errorProvider(TransferError.Builder::newInstance) + .protocol(protocol) + .build(); + + return dspRequestHandler.updateResource(request); + } + +} diff --git a/data-protocols/dsp/dsp-transfer-process/lib/dsp-transfer-process-http-api-lib/src/testFixtures/java/org/eclipse/edc/protocol/dsp/transferprocess/http/api/controller/DspTransferProcessApiControllerBaseTest.java b/data-protocols/dsp/dsp-transfer-process/lib/dsp-transfer-process-http-api-lib/src/testFixtures/java/org/eclipse/edc/protocol/dsp/transferprocess/http/api/controller/DspTransferProcessApiControllerBaseTest.java new file mode 100644 index 00000000000..0ad5a296b51 --- /dev/null +++ b/data-protocols/dsp/dsp-transfer-process/lib/dsp-transfer-process-http-api-lib/src/testFixtures/java/org/eclipse/edc/protocol/dsp/transferprocess/http/api/controller/DspTransferProcessApiControllerBaseTest.java @@ -0,0 +1,180 @@ +/* + * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.edc.protocol.dsp.transferprocess.http.api.controller; + +import io.restassured.specification.RequestSpecification; +import jakarta.json.Json; +import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.eclipse.edc.connector.controlplane.services.spi.transferprocess.TransferProcessProtocolService; +import org.eclipse.edc.connector.controlplane.transfer.spi.types.TransferProcess; +import org.eclipse.edc.connector.controlplane.transfer.spi.types.protocol.TransferCompletionMessage; +import org.eclipse.edc.connector.controlplane.transfer.spi.types.protocol.TransferRequestMessage; +import org.eclipse.edc.connector.controlplane.transfer.spi.types.protocol.TransferStartMessage; +import org.eclipse.edc.connector.controlplane.transfer.spi.types.protocol.TransferSuspensionMessage; +import org.eclipse.edc.connector.controlplane.transfer.spi.types.protocol.TransferTerminationMessage; +import org.eclipse.edc.jsonld.spi.JsonLdNamespace; +import org.eclipse.edc.protocol.dsp.http.spi.message.DspRequestHandler; +import org.eclipse.edc.protocol.dsp.http.spi.message.GetDspRequest; +import org.eclipse.edc.protocol.dsp.http.spi.message.PostDspRequest; +import org.eclipse.edc.web.jersey.testfixtures.RestControllerTestBase; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.mockito.ArgumentCaptor; + +import java.util.stream.Stream; + +import static io.restassured.RestAssured.given; +import static jakarta.json.Json.createObjectBuilder; +import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON_TYPE; +import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.edc.jsonld.spi.JsonLdKeywords.TYPE; +import static org.eclipse.edc.protocol.dsp.spi.type.DspTransferProcessPropertyAndTypeNames.DSPACE_TYPE_TRANSFER_COMPLETION_MESSAGE_TERM; +import static org.eclipse.edc.protocol.dsp.spi.type.DspTransferProcessPropertyAndTypeNames.DSPACE_TYPE_TRANSFER_REQUEST_MESSAGE_TERM; +import static org.eclipse.edc.protocol.dsp.spi.type.DspTransferProcessPropertyAndTypeNames.DSPACE_TYPE_TRANSFER_START_MESSAGE_TERM; +import static org.eclipse.edc.protocol.dsp.spi.type.DspTransferProcessPropertyAndTypeNames.DSPACE_TYPE_TRANSFER_SUSPENSION_MESSAGE_TERM; +import static org.eclipse.edc.protocol.dsp.spi.type.DspTransferProcessPropertyAndTypeNames.DSPACE_TYPE_TRANSFER_TERMINATION_MESSAGE_TERM; +import static org.eclipse.edc.protocol.dsp.transferprocess.http.api.TransferProcessApiPaths.TRANSFER_COMPLETION; +import static org.eclipse.edc.protocol.dsp.transferprocess.http.api.TransferProcessApiPaths.TRANSFER_INITIAL_REQUEST; +import static org.eclipse.edc.protocol.dsp.transferprocess.http.api.TransferProcessApiPaths.TRANSFER_START; +import static org.eclipse.edc.protocol.dsp.transferprocess.http.api.TransferProcessApiPaths.TRANSFER_SUSPENSION; +import static org.eclipse.edc.protocol.dsp.transferprocess.http.api.TransferProcessApiPaths.TRANSFER_TERMINATION; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public abstract class DspTransferProcessApiControllerBaseTest extends RestControllerTestBase { + + private static final String PROCESS_ID = "testId"; + protected final TransferProcessProtocolService protocolService = mock(); + protected final DspRequestHandler dspRequestHandler = mock(); + + @Test + void getTransferProcess_shouldGetResource() { + var id = "transferProcessId"; + + when(dspRequestHandler.getResource(any())).thenReturn(Response.ok().type(APPLICATION_JSON_TYPE).build()); + + baseRequest() + .get(basePath() + id) + .then() + .contentType(MediaType.APPLICATION_JSON) + .statusCode(200); + + var captor = ArgumentCaptor.forClass(GetDspRequest.class); + verify(dspRequestHandler).getResource(captor.capture()); + var dspRequest = captor.getValue(); + assertThat(dspRequest.getId()).isEqualTo("transferProcessId"); + assertThat(dspRequest.getResultClass()).isEqualTo(TransferProcess.class); + assertThat(dspRequest.getToken()).isEqualTo("auth"); + } + + @Test + void initiateTransferProcess_shouldCreateResource() { + when(dspRequestHandler.createResource(any())).thenReturn(Response.ok().type(APPLICATION_JSON_TYPE).build()); + + var result = baseRequest() + .contentType(MediaType.APPLICATION_JSON) + .body(Json.createObjectBuilder().add(TYPE, namespace().toIri(DSPACE_TYPE_TRANSFER_REQUEST_MESSAGE_TERM)).build()) + .post(basePath() + TRANSFER_INITIAL_REQUEST) + .then() + .statusCode(200) + .contentType(MediaType.APPLICATION_JSON); + + assertThat(result).isNotNull(); + var captor = ArgumentCaptor.forClass(PostDspRequest.class); + verify(dspRequestHandler).createResource(captor.capture()); + var request = captor.getValue(); + assertThat(request.getToken()).isEqualTo("auth"); + assertThat(request.getProcessId()).isEqualTo(null); + assertThat(request.getInputClass()).isEqualTo(TransferRequestMessage.class); + assertThat(request.getResultClass()).isEqualTo(TransferProcess.class); + assertThat(request.getMessage()).isNotNull(); + assertThat(request.getExpectedMessageType()).isEqualTo(namespace().toIri(DSPACE_TYPE_TRANSFER_REQUEST_MESSAGE_TERM)); + } + + /** + * Verifies that an endpoint returns 401 if the identity service cannot verify the identity token. + * + * @param path the request path to the endpoint + */ + @ParameterizedTest + @ArgumentsSource(ControllerMethodArguments.class) + void callEndpoint_shouldUpdateResource(String path, Class messageClass, String messageType) { + when(dspRequestHandler.updateResource(any())).thenReturn(Response.ok().type(APPLICATION_JSON_TYPE).build()); + var requestBody = createObjectBuilder().add("http://schema/key", "value").build(); + + baseRequest() + .contentType(MediaType.APPLICATION_JSON) + .body(requestBody) + .post(basePath() + path) + .then() + .contentType(MediaType.APPLICATION_JSON) + .statusCode(200); + + var captor = ArgumentCaptor.forClass(PostDspRequest.class); + verify(dspRequestHandler).updateResource(captor.capture()); + var request = captor.getValue(); + assertThat(request.getToken()).isEqualTo("auth"); + assertThat(request.getProcessId()).isEqualTo(PROCESS_ID); + assertThat(request.getMessage()).isNotNull(); + assertThat(request.getInputClass()).isEqualTo(messageClass); + assertThat(request.getResultClass()).isEqualTo(TransferProcess.class); + assertThat(request.getExpectedMessageType()).isEqualTo(namespace().toIri(messageType)); + } + + protected abstract String basePath(); + + protected abstract JsonLdNamespace namespace(); + + private RequestSpecification baseRequest() { + return given() + .baseUri("http://localhost:" + port) + .basePath("/") + .header(HttpHeaders.AUTHORIZATION, "auth") + .when(); + } + + private static class ControllerMethodArguments implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of( + PROCESS_ID + TRANSFER_START, + TransferStartMessage.class, + DSPACE_TYPE_TRANSFER_START_MESSAGE_TERM), + Arguments.of( + PROCESS_ID + TRANSFER_COMPLETION, + TransferCompletionMessage.class, + DSPACE_TYPE_TRANSFER_COMPLETION_MESSAGE_TERM), + Arguments.of( + PROCESS_ID + TRANSFER_SUSPENSION, + TransferSuspensionMessage.class, + DSPACE_TYPE_TRANSFER_SUSPENSION_MESSAGE_TERM), + Arguments.of( + PROCESS_ID + TRANSFER_TERMINATION, + TransferTerminationMessage.class, + DSPACE_TYPE_TRANSFER_TERMINATION_MESSAGE_TERM) + ); + } + } + +} diff --git a/settings.gradle.kts b/settings.gradle.kts index f8e37f07fcf..9e8ce4f3693 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -88,6 +88,7 @@ include(":data-protocols:dsp:dsp-catalog:dsp-catalog-http-dispatcher") include(":data-protocols:dsp:dsp-catalog:dsp-catalog-transform") include(":data-protocols:dsp:dsp-catalog:lib:dsp-catalog-validation-lib") include(":data-protocols:dsp:dsp-catalog:lib:dsp-catalog-transform-lib") +include(":data-protocols:dsp:dsp-catalog:lib:dsp-catalog-http-api-lib") include(":data-protocols:dsp:dsp-spi") include(":data-protocols:dsp:dsp-negotiation") include(":data-protocols:dsp:dsp-negotiation:dsp-negotiation-http-api") @@ -95,6 +96,7 @@ include(":data-protocols:dsp:dsp-negotiation:dsp-negotiation-http-dispatcher") include(":data-protocols:dsp:dsp-negotiation:dsp-negotiation-transform") include(":data-protocols:dsp:dsp-negotiation:lib:dsp-negotiation-validation-lib") include(":data-protocols:dsp:dsp-negotiation:lib:dsp-negotiation-transform-lib") +include(":data-protocols:dsp:dsp-negotiation:lib:dsp-negotiation-http-api-lib") include(":data-protocols:dsp:dsp-http-core") include(":data-protocols:dsp:dsp-http-spi") include(":data-protocols:dsp:dsp-transfer-process") @@ -103,6 +105,7 @@ include(":data-protocols:dsp:dsp-transfer-process:dsp-transfer-process-http-disp include(":data-protocols:dsp:dsp-transfer-process:dsp-transfer-process-transform") include(":data-protocols:dsp:dsp-transfer-process:lib:dsp-transfer-process-validation-lib") include(":data-protocols:dsp:dsp-transfer-process:lib:dsp-transfer-process-transform-lib") +include(":data-protocols:dsp:dsp-transfer-process:lib:dsp-transfer-process-http-api-lib") include(":data-protocols:dsp:dsp-version:dsp-version-http-api") include(":data-protocols:dsp:dsp-version:dsp-version-http-dispatcher") @@ -312,4 +315,4 @@ include(":dist:bom:dataplane-feature-sql-bom") include(":dist:bom:sts-feature-bom") -include(":version-catalog") \ No newline at end of file +include(":version-catalog")