Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import static io.neonbee.endpoint.odatav4.internal.olingo.processor.NavigationPropertyHelper.chooseEntitySet;
import static io.neonbee.endpoint.odatav4.internal.olingo.processor.NavigationPropertyHelper.fetchNavigationTargetEntity;
import static io.neonbee.endpoint.odatav4.internal.olingo.processor.ProcessorHelper.forwardRequest;
import static org.apache.olingo.commons.api.http.HttpStatusCode.CREATED;
import static org.apache.olingo.commons.api.http.HttpStatusCode.INTERNAL_SERVER_ERROR;
import static org.apache.olingo.commons.api.http.HttpStatusCode.NOT_FOUND;
import static org.apache.olingo.commons.api.http.HttpStatusCode.NO_CONTENT;
Expand All @@ -16,6 +17,7 @@
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

Expand Down Expand Up @@ -68,6 +70,8 @@ public class EntityProcessor extends AsynchronousProcessor

private static final EntityComparison ENTITY_COMPARISON = new EntityComparison() {};

private static final String LOCATION_HEADER = "Location";

private OData odata;

private ServiceMetadata serviceMetadata;
Expand Down Expand Up @@ -106,20 +110,26 @@ public void createEntity(ODataRequest request, ODataResponse response, UriInfo u
Promise<Void> processPromise = getProcessPromise();
UriResourceEntitySet uriResourceEntitySet = (UriResourceEntitySet) uriInfo.getUriResourceParts().get(0);
Entity entity = parseBody(request, uriResourceEntitySet, requestFormat);

forwardRequest(request, CREATE, entity, uriInfo, vertx, routingContext, processPromise).onSuccess(ew -> {
/*
* TODO: Upon successful completion, the response MUST contain a Location header that contains the edit URL
* or read URL of the created entity.
*
* TODO: Upon successful, completion the service MUST respond with either 201 Created (in this case, the
* response body MUST contain the resource created), or 204 No Content (in this case the response body is
* empty) if the request included a return Prefer header with a value of return=minimal. See
* https://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-protocol.html#_Toc31358871
*/
response.setStatusCode(NO_CONTENT.getStatusCode());
processPromise.complete();
});
forwardRequest(request, CREATE, entity, uriInfo, vertx, routingContext, processPromise)
.onSuccess(createdEntity -> {
try {
ContextURL contextUrl =
ContextURL.with().entitySet(uriResourceEntitySet.getEntitySet()).build();
EntitySerializerOptions opts = EntitySerializerOptions.with().contextURL(contextUrl).build();
response.setContent(odata.createSerializer(responseFormat)
.entity(serviceMetadata, uriResourceEntitySet.getEntitySet().getEntityType(),
createdEntity.getEntity(), opts)
.getContent());
response.setStatusCode(CREATED.getStatusCode());
response.setHeader(HttpHeader.CONTENT_TYPE, responseFormat.toContentTypeString());
Optional.ofNullable(
routingContext.<String>get(ProcessorHelper.RESPONSE_HEADER_PREFIX + LOCATION_HEADER))
.ifPresent(location -> response.setHeader(LOCATION_HEADER, location));
processPromise.complete();
} catch (SerializerException e) {
processPromise.fail(e);
}
});
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,21 +101,22 @@ public static Future<EntityWrapper> forwardRequest(ODataRequest request, DataAct
DataQuery query = odataRequestToQuery(request, action, body);
DataContext dataContext = new DataContextImpl(routingContext);
return requestEntity(vertx, new DataRequest(entityType.getFullQualifiedName(), query), dataContext)
.map(result -> {
transferResponseHint(dataContext, routingContext);
return result;
}).onFailure(processPromise::fail);
.map(result -> transferResponseHint(dataContext, routingContext, result))
.onFailure(processPromise::fail);
}

/**
* Transfer response hints from data context into routing context.
*
* @param dataContext data context
* @param routingContext routing context
* @param result entity wrapper result
* @return the entity wrapper result
*/
@VisibleForTesting
static void transferResponseHint(DataContext dataContext, RoutingContext routingContext) {
dataContext.responseData().entrySet()
.forEach(entry -> routingContext.put(RESPONSE_HEADER_PREFIX + entry.getKey(), entry.getValue()));
static EntityWrapper transferResponseHint(DataContext dataContext, RoutingContext routingContext,
EntityWrapper result) {
dataContext.responseData().forEach((key, value) -> routingContext.put(RESPONSE_HEADER_PREFIX + key, value));
return result;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import io.neonbee.data.DataContext;
import io.neonbee.data.internal.DataContextImpl;
import io.neonbee.entity.EntityWrapper;
import io.vertx.core.http.HttpServerRequest;
import io.vertx.core.http.impl.HttpServerRequestInternal;
import io.vertx.ext.web.RoutingContext;
Expand All @@ -36,11 +37,13 @@ void transferResponseHint() {
dataContext.responseData().put(ODATA_FILTER_KEY, Boolean.TRUE);
dataContext.responseData().put(ODATA_EXPAND_KEY, Boolean.FALSE);
dataContext.responseData().put(ODATA_COUNT_SIZE_KEY, 42);
ProcessorHelper.transferResponseHint(dataContext, routingContext);
EntityWrapper entityWrapper = Mockito.mock(EntityWrapper.class);
EntityWrapper result = ProcessorHelper.transferResponseHint(dataContext, routingContext, entityWrapper);
assertThat(routingContext.<Boolean>get(RESPONSE_HEADER_PREFIX + ODATA_FILTER_KEY)).isTrue();
assertThat(routingContext.<Boolean>get(RESPONSE_HEADER_PREFIX + ODATA_EXPAND_KEY)).isFalse();
assertThat(routingContext.<Boolean>get(RESPONSE_HEADER_PREFIX + ODATA_SKIP_KEY)).isNull();
assertThat(routingContext.<Boolean>get(RESPONSE_HEADER_PREFIX + ODATA_TOP_KEY)).isNull();
assertThat(routingContext.<Integer>get(RESPONSE_HEADER_PREFIX + ODATA_COUNT_SIZE_KEY)).isEqualTo(42);
assertThat(result).isSameInstanceAs(entityWrapper);
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
package io.neonbee.test.endpoint.odata;

import static com.google.common.truth.Truth.assertThat;
import static io.neonbee.test.endpoint.odata.verticle.TestService1EntityVerticle.EXPECTED_ENTITY_DATA_1;
import static io.neonbee.test.endpoint.odata.verticle.TestService1EntityVerticle.TEST_ENTITY_SET_FQN;
import static io.neonbee.test.endpoint.odata.verticle.TestService3EntityVerticle.ENTITY_DATA_1;
import static io.neonbee.test.endpoint.odata.verticle.TestService3EntityVerticle.ENTITY_URL;
import static io.neonbee.test.endpoint.odata.verticle.TestService3EntityVerticle.TEST_ENTITY_SET_FQN;

import java.nio.file.Path;
import java.util.List;
Expand All @@ -15,32 +16,56 @@

import io.neonbee.test.base.ODataEndpointTestBase;
import io.neonbee.test.base.ODataRequest;
import io.neonbee.test.endpoint.odata.verticle.TestService1EntityVerticle;
import io.neonbee.test.endpoint.odata.verticle.TestService3EntityVerticle;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.http.HttpHeaders;
import io.vertx.core.http.HttpMethod;
import io.vertx.core.json.JsonObject;
import io.vertx.junit5.VertxTestContext;

class ODataCreateEntityTest extends ODataEndpointTestBase {

@Override
protected List<Path> provideEntityModels() {
return List.of(TestService1EntityVerticle.getDeclaredEntityModel());
return List.of(TestService3EntityVerticle.getDeclaredEntityModel());
}

@BeforeEach
void setUp(VertxTestContext testContext) {
deployVerticle(new TestService1EntityVerticle()).onComplete(testContext.succeedingThenComplete());
deployVerticle(new TestService3EntityVerticle()).onComplete(testContext.succeedingThenComplete());
}

@Test
@DisplayName("Respond with 204 NO CONTENT if an entity was successfully created")
@DisplayName("Respond with 201 CREATED with Location header if an entity was successfully created")
void createEntityTest(VertxTestContext testContext) {
ODataRequest oDataRequest = new ODataRequest(TEST_ENTITY_SET_FQN).setMethod(HttpMethod.POST)
.setBody(EXPECTED_ENTITY_DATA_1.toBuffer())
.setBody(ENTITY_DATA_1.toBuffer())
.addHeader(HttpHeaders.CONTENT_TYPE.toString(), MediaType.JSON_UTF_8.toString());

requestOData(oDataRequest).onComplete(testContext.succeeding(response -> {
testContext.verify(() -> assertThat(response.statusCode()).isEqualTo(204));
testContext.verify(() -> {
assertThat(response.statusCode()).isEqualTo(201);
assertThat(response.getHeader("Location")).isEqualTo(ENTITY_URL);
JsonObject body = response.body().toJsonObject();
assertThat(body.getString("ID")).isEqualTo(ENTITY_DATA_1.getString("ID"));
assertThat(body.getString("name")).isEqualTo(ENTITY_DATA_1.getString("name"));
assertThat(body.getString("description")).isEqualTo(ENTITY_DATA_1.getString("description"));
});
testContext.completeNow();
}));
}

@Test
@DisplayName("Respond with 400")
void createEntityTestWithWrongPayload(VertxTestContext testContext) {
ODataRequest oDataRequest = new ODataRequest(TEST_ENTITY_SET_FQN).setMethod(HttpMethod.POST)
.setBody(Buffer.buffer("wrong JSON"))
.addHeader(HttpHeaders.CONTENT_TYPE.toString(), MediaType.JSON_UTF_8.toString());

requestOData(oDataRequest).onComplete(testContext.succeeding(response -> {
testContext.verify(() -> {
assertThat(response.statusCode()).isEqualTo(400);
});
testContext.completeNow();
}));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ public class TestService3EntityVerticle extends EntityVerticle {
public static final FullQualifiedName TEST_ENTITY_SET_FQN =
new FullQualifiedName("io.neonbee.test3.TestService3", "TestCars");

public static final String ENTITY_URL = "/io.neonbee.test3.TestService3/TestCars(/0)";

public static final JsonObject ENTITY_DATA_1 =
new JsonObject().put("ID", 0).put("name", "Car 0").put("description", "This is Car 0");

Expand All @@ -40,6 +42,12 @@ public Future<EntityWrapper> retrieveData(DataQuery query, DataContext context)
List.of(createEntity(ENTITY_DATA_1), createEntity(ENTITY_DATA_2), createEntity(ENTITY_DATA_3))));
}

@Override
public Future<EntityWrapper> createData(DataQuery query, DataContext context) {
context.responseData().put("Location", ENTITY_URL);
return Future.succeededFuture(new EntityWrapper(TEST_ENTITY_SET_FQN, createEntity(ENTITY_DATA_1)));
}

public static Path getDeclaredEntityModel() {
return TEST_RESOURCES.resolveRelated("TestService3.csn");
}
Expand Down