Skip to content

Commit

Permalink
Add Server TCK for Oracle Cloud Function HTTP (#920)
Browse files Browse the repository at this point in the history
This PR adds Micronaut Server TCK for Oracle Cloud Function HTTP.

Currently, 10 of 188 tests (5%) fail.

Fixes:

* Making the request implementation mutable to support filters.
* Allow empty response when an exception is thrown micronaut-servlet#737
* Add support for parsing form data and support empty values in the form data.
* Support binding publisher when there is only one element.
* Support binding body parts for JSON case (so binding JSON properties).
* Fix reading cookies from headers.

Created issues:

Function Server TCK: Body has already been consumed exception #921
Function Server TCK: ControllerConstraintHandlerTest failing because of getBody() #925
Function Server TCK: RequestFilterTest failing #926
  • Loading branch information
andriy-dmytruk authored Jun 18, 2024
1 parent 6adefde commit 7e2cf6c
Show file tree
Hide file tree
Showing 14 changed files with 842 additions and 68 deletions.
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ graal-svm = { module = "org.graalvm.nativeimage:svm", version.ref = "graal-svm"}
javapoet = { module = 'com.squareup:javapoet', version.ref = 'javapoet' }
jetty = { module = 'org.eclipse.jetty:jetty-server', version.ref = 'jetty' }
junit-jupiter = { module = 'org.junit.jupiter:junit-jupiter-engine', version = '' }
junit-platform-engine = { module = "org.junit.platform:junit-platform-suite-engine" }
kotlin-reflect = { module = 'org.jetbrains.kotlin:kotlin-reflect', version.ref = 'kotlin' }
kotlin-stdlib = { module = 'org.jetbrains.kotlin:kotlin-stdlib-jdk8', version.ref = 'kotlin' }
logback-json-classic = { module = 'ch.qos.logback.contrib:logback-json-classic', version.ref = 'logback-json-classic' }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ public void handle(String target, Request baseRequest, HttpServletRequest reques
fn.addSharedClassPrefix("com.sun.");
String queryString = request.getQueryString();
String requestURI = request.getRequestURI();
if (StringUtils.isNotEmpty(requestURI)) {
if (StringUtils.isNotEmpty(queryString)) {
requestURI += "?" + queryString;
}
FnEventBuilder<FnTestingRule> eventBuilder = fn.givenEvent()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import io.micronaut.core.convert.ArgumentConversionContext;
import io.micronaut.core.convert.ConversionError;
import io.micronaut.core.convert.ConversionService;
import io.micronaut.core.convert.value.ConvertibleValues;
import io.micronaut.core.io.IOUtils;
import io.micronaut.core.type.Argument;
import io.micronaut.http.HttpRequest;
Expand All @@ -30,13 +31,17 @@
import io.micronaut.http.codec.CodecException;
import io.micronaut.http.codec.MediaTypeCodec;
import io.micronaut.http.codec.MediaTypeCodecRegistry;
import io.micronaut.json.codec.MapperMediaTypeCodec;
import io.micronaut.json.tree.JsonNode;
import org.reactivestreams.Publisher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Array;
import java.util.Collections;
Expand Down Expand Up @@ -85,24 +90,15 @@ public BindingResult<T> bind(ArgumentConversionContext<T> context, HttpRequest<?
return () -> (Optional<T>) Optional.of(content);
} catch (IOException e) {
LOG.debug("Error occurred reading function body: {}", e.getMessage(), e);
return new BindingResult<T>() {
@Override
public Optional<T> getValue() {
return Optional.empty();
}

@Override
public List<ConversionError> getConversionErrors() {
return Collections.singletonList(
() -> e
);
}
};
return new ConversionFailedBindingResult<>(e);
}
});

} else {
final MediaType mediaType = source.getContentType().orElse(MediaType.APPLICATION_JSON_TYPE);
if (servletHttpRequest.isFormSubmission()) {
return bindFormData(servletHttpRequest, name, context);
}

final MediaTypeCodec codec = mediaTypeCodeRegistry
.findCodec(mediaType, type)
.orElse(null);
Expand All @@ -112,52 +108,15 @@ public List<ConversionError> getConversionErrors() {
return servletHttpRequest.getNativeRequest().consumeBody(inputStream -> {
try {
if (Publishers.isConvertibleToPublisher(type)) {
final Argument<?> typeArg = argument.getFirstTypeVariable().orElse(Argument.OBJECT_ARGUMENT);
if (Publishers.isSingle(type)) {
T content = (T) codec.decode(typeArg, inputStream);
final Publisher<T> publisher = Publishers.just(content);
LOG.trace("Decoded object from function body: {}", content);
final T converted = conversionService.convertRequired(publisher, type);
return () -> Optional.of(converted);
} else {
final Argument<? extends List<?>> containerType = Argument.listOf(typeArg.getType());
T content = (T) codec.decode(containerType, inputStream);
LOG.trace("Decoded object from function body: {}", content);
final Flux flowable = Flux.fromIterable((Iterable) content);
final T converted = conversionService.convertRequired(flowable, type);
return () -> Optional.of(converted);
}
return bindPublisher(argument, type, codec, inputStream);
} else {
if (type.isArray()) {
Class<?> componentType = type.getComponentType();
List<T> content = (List<T>) codec.decode(Argument.listOf(componentType), inputStream);
LOG.trace("Decoded object from function body: {}", content);
Object[] array = content.toArray((Object[]) Array.newInstance(componentType, 0));
return () -> Optional.of((T) array);
} else {
T content = codec.decode(argument, inputStream);
LOG.trace("Decoded object from function body: {}", content);
return () -> Optional.of(content);
}
return bindPojo(argument, type, codec, inputStream, name);
}
} catch (CodecException e) {
LOG.trace("Error occurred decoding function body: {}", e.getMessage(), e);
return new BindingResult<T>() {
@Override
public Optional<T> getValue() {
return Optional.empty();
}

@Override
public List<ConversionError> getConversionErrors() {
return Collections.singletonList(
() -> e
);
}
};
return new ConversionFailedBindingResult<>(e);
}
});

}

}
Expand All @@ -166,8 +125,120 @@ public List<ConversionError> getConversionErrors() {
return defaultBodyBinder.bind(context, source);
}

private BindingResult<T> bindFormData(
FnServletRequest<?> servletHttpRequest, String name, ArgumentConversionContext<T> context
) {
Optional<ConvertibleValues> form = servletHttpRequest.getBody(FnServletRequest.CONVERTIBLE_VALUES_ARGUMENT);
if (form.isEmpty()) {
return BindingResult.empty();
}
if (name != null) {
return () -> form.get().get(name, context);
}
return () -> conversionService.convert(form.get().asMap(), context);
}

private BindingResult<T> bindPojo(
Argument<T> argument, Class<?> type, MediaTypeCodec codec, InputStream inputStream, String name
) {
Argument<?> requiredArg = type.isArray() ? Argument.listOf(type.getComponentType()) : argument;
Object converted;

if (name != null && codec instanceof MapperMediaTypeCodec jsonCodec) {
// Special case where a particular part of body is required
try {
JsonNode node = jsonCodec.getJsonMapper()
.readValue(inputStream, JsonNode.class);
JsonNode field = node.get(name);
if (field == null) {
return Optional::empty;
}
converted = jsonCodec.decode(requiredArg, field);
} catch (IOException e) {
throw new CodecException("Error decoding JSON stream for type [JsonNode]: " + e.getMessage(), e);
}
} else {
converted = codec.decode(argument, inputStream);
}

if (type.isArray()) {
converted = ((List<?>) converted).toArray((Object[]) Array.newInstance(type.getComponentType(), 0));
}
T content = (T) converted;
LOG.trace("Decoded object from function body: {}", converted);
return () -> Optional.of(content);
}

private BindingResult<T> bindPublisher(
Argument<T> argument, Class<T> type, MediaTypeCodec codec, InputStream inputStream
) {
final Argument<?> typeArg = argument.getFirstTypeVariable().orElse(Argument.OBJECT_ARGUMENT);
if (Publishers.isSingle(type)) {
T content = (T) codec.decode(typeArg, inputStream);
final Publisher<T> publisher = Publishers.just(content);
LOG.trace("Decoded object from function body: {}", content);
final T converted = conversionService.convertRequired(publisher, type);
return () -> Optional.of(converted);
} else {
final Argument<? extends List<?>> containerType = Argument.listOf(typeArg.getType());
if (codec instanceof MapperMediaTypeCodec jsonCodec) {
// Special JSON case: we can accept both array and a single value
try {
JsonNode node = jsonCodec.getJsonMapper()
.readValue(inputStream, JsonNode.class);
T converted;
if (node.isArray()) {
converted = Publishers.convertPublisher(
conversionService,
Flux.fromIterable(node.values())
.map(itemNode -> jsonCodec.decode(typeArg, itemNode)),
type
);
} else {
converted = Publishers.convertPublisher(
conversionService,
Mono.just(jsonCodec.decode(typeArg, node)),
type
);
}
return () -> Optional.of(converted);
} catch (IOException e) {
throw new CodecException("Error decoding JSON stream for type [JsonNode]: " + e.getMessage(), e);
}
}
T content = (T) codec.decode(containerType, inputStream);
LOG.trace("Decoded object from function body: {}", content);
final Flux flowable = Flux.fromIterable((Iterable) content);
final T converted = conversionService.convertRequired(flowable, type);
return () -> Optional.of(converted);
}
}

@Override
public Class<Body> getAnnotationType() {
return Body.class;
}

/**
* A binding result implementation for the case when conversion error was thrown.
*
* @param <T> The type to be bound
* @param e The conversion error
*/
private record ConversionFailedBindingResult<T>(
Exception e
) implements BindingResult<T> {

@Override
public Optional<T> getValue() {
return Optional.empty();
}

@Override
public List<ConversionError> getConversionErrors() {
return Collections.singletonList(() -> e);
}

}

}
Loading

0 comments on commit 7e2cf6c

Please sign in to comment.