-
Notifications
You must be signed in to change notification settings - Fork 36
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
RESTEasy Reactive - Test selecting from multiple media types
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
1 parent
377951f
commit f0c4846
Showing
3 changed files
with
322 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
111 changes: 111 additions & 0 deletions
111
http/jaxrs-reactive/src/main/java/io/quarkus/ts/http/jaxrs/reactive/MediaTypeResource.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} |
210 changes: 210 additions & 0 deletions
210
.../jaxrs-reactive/src/test/java/io/quarkus/ts/http/jaxrs/reactive/MediaTypeSelectionIT.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | ||
} | ||
|
||
} |