Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: extract base controllers for all DSP apis #4666

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions data-protocols/dsp/dsp-catalog/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -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);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -195,7 +48,7 @@ protected Object controller() {

@ApiTest
@Nested
class DspCatalogApiControllerV20241Test extends Tests {
class DspCatalogApiControllerV20241Test extends DspCatalogApiControllerTestBase {

@Override
protected String basePath() {
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
}
Loading
Loading