Skip to content

Commit

Permalink
RESTEasy Reactive - Test selecting from multiple media types
Browse files Browse the repository at this point in the history
Tests [matching requests to resource methods](https://jakarta.ee/specifications/restful-ws/3.1/jakarta-restful-ws-spec-3.1.html#mapping_requests_to_java_methods) with multiple [media types](https://www.iana.org/assignments/media-types/media-types.xhtml). I combined it with [content negotiation and wildcards](https://www.rfc-editor.org/rfc/rfc9110.html#section-12.4.2) as they are handled by the same [MediaTypeMapper](https://github.com/quarkusio/quarkus/blob/main/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/handlers/MediaTypeMapper.java#L38), however I didn't dig every hole (e.g. left out quality values and so on). Main purpose of this PR is to verify quarkusio/quarkus#29732, intention is not to cover all scenarios (f.e. I didn't cover HTTP status 416) as these situations are very rare and users are unlikely to expose multiple endpoints that needs this mapper, it is also not efficient as the mapper is not optimized.

succees: `mvn clean verify -Dit.test=MediaTypeSelectionIT -Dquarkus.platform.group-id=io.quarkus.platform -Dquarkus.platform.version=2.13.6.Final`
failure: `mvn clean verify -Dit.test=MediaTypeSelectionIT -Dquarkus.platform.group-id=io.quarkus.platform -Dquarkus.platform.version=2.13.5.Final`
  • Loading branch information
michalvavrik committed Dec 22, 2022
1 parent 377951f commit f0c4846
Show file tree
Hide file tree
Showing 3 changed files with 322 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,7 @@ Additional coverage:
- Advanced JSON serialization.
- Gzip compression
- REST Client reactive - support for POJO JSON serialization in multipart forms.
- Request matching - selecting from multiple media types

### `http/rest-client`
Verifies Rest Client configuration using `quarkus-rest-client-jaxb` (XML support) and `quarkus-rest-client-jsonb` (JSON support).
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package io.quarkus.ts.http.jaxrs.reactive;

import static io.quarkus.ts.http.jaxrs.reactive.MediaTypeResource.MEDIA_TYPE_PATH;
import static javax.ws.rs.core.HttpHeaders.CONTENT_TYPE;
import static javax.ws.rs.core.MediaType.APPLICATION_ATOM_XML;
import static javax.ws.rs.core.MediaType.APPLICATION_FORM_URLENCODED;
import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
import static javax.ws.rs.core.MediaType.APPLICATION_JSON_PATCH_JSON;
import static javax.ws.rs.core.MediaType.APPLICATION_OCTET_STREAM;
import static javax.ws.rs.core.MediaType.APPLICATION_SVG_XML;
import static javax.ws.rs.core.MediaType.APPLICATION_XHTML_XML;
import static javax.ws.rs.core.MediaType.APPLICATION_XML;
import static javax.ws.rs.core.MediaType.TEXT_XML;
import static javax.ws.rs.core.MediaType.WILDCARD;

import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.PATCH;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Response;

@Path(MEDIA_TYPE_PATH)
public class MediaTypeResource {

public static final String MEDIA_TYPE_PATH = "/media-type";
public static final String POST_APP_JSON_AND_TEXT_XML_AND_TEXT_PLAIN = "postAppJsonTextXmlTextPlain";
public static final String POST_APP_ATOM_XML_AND_TEXT_XML_EXT_AND_APP_JSON_PATCH_JSON = "postAppAtomXmlAndTextAnyXmlExtAndAppJsonPatch";
public static final String GET_WITHOUT_CONSUMED_MEDIA_TYPES = "getWithoutConsumedMediaTypes";
public static final String PATCH_WITHOUT_CONSUMED_MEDIA_TYPES = "patchWithoutConsumedMediaTypes";
public static final String PATCH_APP_OCTET_STREAM = "patchAppOctetStream";

@Consumes({ "application/soap+xml" })
@POST
public Response postApplicationSoapXml() {
return Response.ok("application/soap+xml").build();
}

@Consumes({ "custom/media-type" })
@POST
public Response postCustomMediaType(@HeaderParam(CONTENT_TYPE) String contentType) {
return Response.ok(contentType).build();
}

@Consumes({ APPLICATION_XML, TEXT_XML, APPLICATION_JSON })
@POST
public Response postAppJsonTextXmlTextPlain() {
return Response.ok(POST_APP_JSON_AND_TEXT_XML_AND_TEXT_PLAIN).build();
}

@Consumes({ APPLICATION_ATOM_XML, "text/xml-external-parsed-entity", APPLICATION_JSON_PATCH_JSON })
@POST
public Response postAppAtomXmlAndTextAnyXmlExtAndAppJsonPatch() {
return Response.ok(POST_APP_ATOM_XML_AND_TEXT_XML_EXT_AND_APP_JSON_PATCH_JSON).build();
}

@GET
public Response getWithoutConsumedMediaTypes() {
return Response.ok(GET_WITHOUT_CONSUMED_MEDIA_TYPES).build();
}

@Consumes(WILDCARD)
@GET
public Response getWildcard() {
// this endpoint is never reached as getWithoutConsumedMediaTypes has priority over wildcard
return Response.ok(WILDCARD).build();
}

@Produces(APPLICATION_XHTML_XML)
@PATCH
public Response patchWithoutConsumedMediaTypes() {
return Response.ok(PATCH_WITHOUT_CONSUMED_MEDIA_TYPES).build();
}

@Produces(WILDCARD)
@Consumes(APPLICATION_SVG_XML)
@PATCH
public Response patchWildcard() {
return Response.ok(APPLICATION_SVG_XML).build();
}

@Produces(APPLICATION_OCTET_STREAM)
@Consumes(APPLICATION_SVG_XML)
@PATCH
public Response patchAppOctetStream() {
return Response.ok(PATCH_APP_OCTET_STREAM).build();
}

@Produces(APPLICATION_FORM_URLENCODED)
@Consumes(APPLICATION_SVG_XML)
@PATCH
public Response patchAppFormUrlEncoded() {
return Response.ok(APPLICATION_FORM_URLENCODED).build();
}

@Produces(APPLICATION_SVG_XML)
@Consumes(APPLICATION_SVG_XML)
@PATCH
public Response patchAppSvgXml() {
return Response.ok(APPLICATION_SVG_XML).build();
}

@Consumes(WILDCARD)
@PUT
public Response putWildcard() {
return Response.ok(WILDCARD).build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
package io.quarkus.ts.http.jaxrs.reactive;

import static io.quarkus.ts.http.jaxrs.reactive.MediaTypeResource.GET_WITHOUT_CONSUMED_MEDIA_TYPES;
import static io.quarkus.ts.http.jaxrs.reactive.MediaTypeResource.MEDIA_TYPE_PATH;
import static io.quarkus.ts.http.jaxrs.reactive.MediaTypeResource.PATCH_APP_OCTET_STREAM;
import static io.quarkus.ts.http.jaxrs.reactive.MediaTypeResource.PATCH_WITHOUT_CONSUMED_MEDIA_TYPES;
import static io.quarkus.ts.http.jaxrs.reactive.MediaTypeResource.POST_APP_ATOM_XML_AND_TEXT_XML_EXT_AND_APP_JSON_PATCH_JSON;
import static io.quarkus.ts.http.jaxrs.reactive.MediaTypeResource.POST_APP_JSON_AND_TEXT_XML_AND_TEXT_PLAIN;
import static io.restassured.http.ContentType.BINARY;
import static io.restassured.http.ContentType.JSON;
import static io.restassured.http.ContentType.MULTIPART;
import static io.restassured.http.ContentType.XML;
import static javax.ws.rs.core.MediaType.APPLICATION_ATOM_XML;
import static javax.ws.rs.core.MediaType.APPLICATION_FORM_URLENCODED;
import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
import static javax.ws.rs.core.MediaType.APPLICATION_JSON_PATCH_JSON;
import static javax.ws.rs.core.MediaType.APPLICATION_SVG_XML;
import static javax.ws.rs.core.MediaType.APPLICATION_XHTML_XML;
import static javax.ws.rs.core.MediaType.APPLICATION_XML;
import static javax.ws.rs.core.MediaType.TEXT_XML;
import static javax.ws.rs.core.MediaType.WILDCARD;

import org.hamcrest.Matchers;
import org.junit.jupiter.api.Test;

import io.quarkus.test.scenarios.QuarkusScenario;
import io.restassured.RestAssured;

@QuarkusScenario
public class MediaTypeSelectionIT {

@Test
public void testUnsupportedMediaType() {
// no resource method consumes multipart/form-data nor wild card
RestAssured.given()
.contentType("multipart/form-data")
.post(MEDIA_TYPE_PATH)
.then()
.statusCode(415);
// no resource method consumes 'application/atom+*' and asterisk is treated as any other character after plus,
// therefore 'application/atom+xml' is not matched
RestAssured.given()
.contentType("application/atom+*")
.post(MEDIA_TYPE_PATH)
.then()
.statusCode(415);
// 'application/atom+xml' is not matched
RestAssured.given()
.contentType("application/atom")
.post(MEDIA_TYPE_PATH)
.then()
.statusCode(415);
// 'text/xml' is not matched
RestAssured.given()
.contentType("text/xml-foo")
.post(MEDIA_TYPE_PATH)
.then()
.statusCode(415);
}

@Test
public void testExactMatchApplicationSoapXml() {
RestAssured.given()
.contentType("application/soap+xml")
.post(MEDIA_TYPE_PATH)
.then()
.statusCode(200)
.body(Matchers.equalTo("application/soap+xml"));
}

@Test
public void testMediaSubTypesRanges() {
// resource method 'postAppJsonTextXmlTextPlain' should match below calls as it
// consumes 'application/xml', 'text/xml' and 'application/json'
RestAssured.given()
.contentType(APPLICATION_XML)
.post(MEDIA_TYPE_PATH)
.then()
.statusCode(200)
.body(Matchers.equalTo(POST_APP_JSON_AND_TEXT_XML_AND_TEXT_PLAIN));
RestAssured.given()
.contentType(TEXT_XML)
.post(MEDIA_TYPE_PATH)
.then()
.statusCode(200)
.body(Matchers.equalTo(POST_APP_JSON_AND_TEXT_XML_AND_TEXT_PLAIN));
RestAssured.given()
.contentType(APPLICATION_JSON)
.post(MEDIA_TYPE_PATH)
.then()
.statusCode(200)
.body(Matchers.equalTo(POST_APP_JSON_AND_TEXT_XML_AND_TEXT_PLAIN));
// resource method 'postAppAtomXmlAndTextAnyXmlExtAndAppJsonPatch' should match below calls as it
// consumes 'application/atom+xml', 'text/xml-external-parsed-entity' and 'application/json-patch+json';
// media types consumed by resource method 'postAppAtomXmlAndTextAnyXmlExtAndAppJsonPatch' are more specific
// than media types consumed by 'postAppJsonTextXmlTextPlain' and the most specific reference is selected
RestAssured.given()
.contentType(APPLICATION_ATOM_XML)
.post(MEDIA_TYPE_PATH)
.then()
.statusCode(200)
.body(Matchers.equalTo(POST_APP_ATOM_XML_AND_TEXT_XML_EXT_AND_APP_JSON_PATCH_JSON));
RestAssured.given()
.contentType("text/xml-external-parsed-entity")
.post(MEDIA_TYPE_PATH)
.then()
.statusCode(200)
.body(Matchers.equalTo(POST_APP_ATOM_XML_AND_TEXT_XML_EXT_AND_APP_JSON_PATCH_JSON));
RestAssured.given()
.contentType(APPLICATION_JSON_PATCH_JSON)
.post(MEDIA_TYPE_PATH)
.then()
.statusCode(200)
.body(Matchers.equalTo(POST_APP_ATOM_XML_AND_TEXT_XML_EXT_AND_APP_JSON_PATCH_JSON));
}

@Test
public void testNoMediaType() {
// resource method without consumes has priority over wildcard
RestAssured.given()
.contentType(APPLICATION_JSON)
.get(MEDIA_TYPE_PATH)
.then()
.statusCode(200)
.body(Matchers.equalTo(GET_WITHOUT_CONSUMED_MEDIA_TYPES));
RestAssured.given()
.contentType(APPLICATION_SVG_XML)
.get(MEDIA_TYPE_PATH)
.then()
.statusCode(200)
.body(Matchers.equalTo(GET_WITHOUT_CONSUMED_MEDIA_TYPES));
}

@Test
public void testAcceptHeaders() {
// here we accept one of xml types, therefore resource method producing application/xhtml+xml is matched
RestAssured.given()
.contentType(APPLICATION_JSON)
.accept(XML)
.patch(MEDIA_TYPE_PATH)
.then()
.statusCode(200)
.body(Matchers.equalTo(PATCH_WITHOUT_CONSUMED_MEDIA_TYPES));
// here we accept one of json types, but no endpoint matches, therefore method producing wildcard is used
RestAssured.given()
.contentType(APPLICATION_SVG_XML)
.accept(JSON)
.patch(MEDIA_TYPE_PATH)
.then()
.statusCode(200)
.body(Matchers.equalTo(APPLICATION_SVG_XML));
// here we accept multipart, but no endpoint matches, therefore method producing wildcard is used
RestAssured.given()
.contentType(APPLICATION_SVG_XML)
.accept(MULTIPART)
.patch(MEDIA_TYPE_PATH)
.then()
.statusCode(200)
.body(Matchers.equalTo(APPLICATION_SVG_XML));
// exact match for accept header has priority over wildcard
RestAssured.given()
.contentType(APPLICATION_SVG_XML)
.accept(BINARY)
.patch(MEDIA_TYPE_PATH)
.then()
.statusCode(200)
.body(Matchers.equalTo(PATCH_APP_OCTET_STREAM));
}

@Test
public void testWildCardMediaType() {
// test wildcard is fallback choice when no other resource method matches
RestAssured.given()
.contentType(APPLICATION_XHTML_XML)
.put(MEDIA_TYPE_PATH)
.then()
.statusCode(200)
.body(Matchers.equalTo(WILDCARD));
}

@Test
public void testMediaSubTypeWildcard() {
// matches '*/*' as that's the only HTTP PUT method
RestAssured.given()
.contentType("application/*")
.put(MEDIA_TYPE_PATH)
.then()
.statusCode(200)
.body(Matchers.equalTo(WILDCARD));
// matches resource method without defined content type as 'application/atom+*' do not match any type
RestAssured.given()
.contentType("application/atom+*")
.patch(MEDIA_TYPE_PATH)
.then()
.statusCode(200)
.body(Matchers.equalTo(PATCH_WITHOUT_CONSUMED_MEDIA_TYPES));
}

@Test
public void testQualityDeterminesFormUrlEncoded() {
RestAssured.given()
.contentType(APPLICATION_SVG_XML)
.accept(String.format("%s;q=0.4, %s;q=0.5", APPLICATION_SVG_XML, APPLICATION_FORM_URLENCODED))
.patch(MEDIA_TYPE_PATH)
.then()
.statusCode(200)
.body(Matchers.equalTo(APPLICATION_FORM_URLENCODED));
}

}

0 comments on commit f0c4846

Please sign in to comment.