/*
* Copyright 2012-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.hateoas.mvc;
import static org.hamcrest.MatcherAssert.*;
import static org.hamcrest.Matchers.*;
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.*;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import org.hamcrest.Matchers;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.mockito.Mockito;
import org.springframework.hateoas.Identifiable;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.TemplateVariable;
import org.springframework.hateoas.TemplateVariable.VariableType;
import org.springframework.hateoas.TestUtils;
import org.springframework.http.HttpEntity;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
/**
* Unit tests for {@link ControllerLinkBuilder}.
*
* @author Oliver Gierke
* @author Dietrich Schulten
* @author Kamill Sokol
* @author Oemer Yildiz
* @author Greg Turnquist
* @author Kevin Conaway
* @author Oliver Trosien
*/
public class ControllerLinkBuilderUnitTest extends TestUtils {
public @Rule ExpectedException exception = ExpectedException.none();
@Test
public void createsLinkToControllerRoot() {
Link link = linkTo(PersonControllerImpl.class).withSelfRel();
assertThat(link.getRel(), is(Link.REL_SELF));
assertThat(link.getHref(), Matchers.endsWith("/people"));
}
@Test
public void createsLinkToParameterizedControllerRoot() {
Link link = linkTo(PersonsAddressesController.class, 15).withSelfRel();
assertThat(link.getRel(), is(Link.REL_SELF));
assertThat(link.getHref(), endsWith("/people/15/addresses"));
}
/**
* @see #70
*/
@Test
public void createsLinkToMethodOnParameterizedControllerRoot() {
Link link = linkTo(methodOn(PersonsAddressesController.class, 15).getAddressesForCountry("DE")).withSelfRel();
assertThat(link.getRel(), is(Link.REL_SELF));
assertThat(link.getHref(), endsWith("/people/15/addresses/DE"));
}
@Test
public void createsLinkToSubResource() {
Link link = linkTo(PersonControllerImpl.class).slash("something").withSelfRel();
assertThat(link.getRel(), is(Link.REL_SELF));
assertThat(link.getHref(), endsWith("/people/something"));
}
@Test
public void createsLinkWithCustomRel() {
Link link = linkTo(PersonControllerImpl.class).withRel(Link.REL_NEXT);
assertThat(link.getRel(), is(Link.REL_NEXT));
assertThat(link.getHref(), endsWith("/people"));
}
/**
* @see #186
*/
@Test
public void usesFirstMappingInCaseMultipleOnesAreDefined() {
assertThat(linkTo(InvalidController.class).withSelfRel().getHref(), endsWith("/persons"));
}
@Test
public void createsLinkToUnmappedController() {
Link link = linkTo(UnmappedController.class).withSelfRel();
assertThat(link.getHref(), is("http://localhost"));
}
@Test
@SuppressWarnings("unchecked")
public void usesIdOfIdentifyableForPathSegment() {
Identifiable<Long> identifyable = Mockito.mock(Identifiable.class);
Mockito.when(identifyable.getId()).thenReturn(10L);
Link link = linkTo(PersonControllerImpl.class).slash(identifyable).withSelfRel();
assertThat(link.getHref(), endsWith("/people/10"));
}
@Test
public void appendingNullIsANoOp() {
Link link = linkTo(PersonControllerImpl.class).slash(null).withSelfRel();
assertThat(link.getHref(), endsWith("/people"));
link = linkTo(PersonControllerImpl.class).slash((Object) null).withSelfRel();
assertThat(link.getHref(), endsWith("/people"));
}
@Test
public void linksToMethod() {
Link link = linkTo(methodOn(ControllerWithMethods.class).myMethod(null)).withSelfRel();
assertPointsToMockServer(link);
assertThat(link.getHref(), endsWith("/something/else"));
}
@Test
public void linksToMethodWithPathVariable() {
Link link = linkTo(methodOn(ControllerWithMethods.class).methodWithPathVariable("1")).withSelfRel();
assertPointsToMockServer(link);
assertThat(link.getHref(), endsWith("/something/1/foo"));
}
/**
* @see #33
*/
@Test
public void usesForwardedHostAsHostIfHeaderIsSet() {
request.addHeader("X-Forwarded-Host", "somethingDifferent");
Link link = linkTo(PersonControllerImpl.class).withSelfRel();
assertThat(link.getHref(), startsWith("http://somethingDifferent"));
}
/**
* @see #112
*/
@Test
public void usesForwardedSslIfHeaderIsSet() {
request.addHeader("X-Forwarded-Ssl", "on");
Link link = linkTo(PersonControllerImpl.class).withSelfRel();
assertThat(link.getHref(), startsWith("https://"));
}
/**
* @see #112
*/
@Test
public void usesForwardedSslIfHeaderIsSetOff() {
request.addHeader("X-Forwarded-Ssl", "off");
Link link = linkTo(PersonControllerImpl.class).withSelfRel();
assertThat(link.getHref(), startsWith("http://"));
}
/**
* @see #112
*/
@Test
public void usesForwardedSslAndHostIfHeaderIsSet() {
request.addHeader("X-Forwarded-Host", "somethingDifferent");
request.addHeader("X-Forwarded-Ssl", "on");
Link link = linkTo(PersonControllerImpl.class).withSelfRel();
assertThat(link.getHref(), startsWith("https://somethingDifferent"));
}
/**
* @see #26, #39
*/
@Test
public void addsRequestParametersHandedIntoSlashCorrectly() {
Link link = linkTo(PersonController.class).slash("?foo=bar").withSelfRel();
UriComponents components = toComponents(link);
assertThat(components.getQuery(), is("foo=bar"));
}
/**
* @see #26, #39
*/
@Test
public void linksToMethodWithPathVariableAndRequestParams() {
Link link = linkTo(methodOn(ControllerWithMethods.class).methodForNextPage("1", 10, 5)).withSelfRel();
UriComponents components = toComponents(link);
assertThat(components.getPath(), is("/something/1/foo"));
MultiValueMap<String, String> queryParams = components.getQueryParams();
assertThat(queryParams.get("limit"), contains("5"));
assertThat(queryParams.get("offset"), contains("10"));
}
/**
* @see #26, #39
*/
@Test
public void linksToMethodWithPathVariableAndMultiValueRequestParams() {
Link link = linkTo(
methodOn(ControllerWithMethods.class).methodWithMultiValueRequestParams("1", Arrays.asList(3, 7), 5))
.withSelfRel();
UriComponents components = toComponents(link);
assertThat(components.getPath(), is("/something/1/foo"));
MultiValueMap<String, String> queryParams = components.getQueryParams();
assertThat(queryParams.get("limit"), contains("5"));
assertThat(queryParams.get("items"), containsInAnyOrder("3", "7"));
}
/**
* @see #26, #39
*/
@Test
public void returnsUriComponentsBuilder() {
UriComponents components = linkTo(PersonController.class).slash("something?foo=bar").toUriComponentsBuilder()
.build();
assertThat(components.getPath(), is("/people/something"));
assertThat(components.getQuery(), is("foo=bar"));
}
/**
* @see #90
*/
@Test
public void usesForwardedHostAndPortFromHeader() {
request.addHeader("X-Forwarded-Host", "foobar:8088");
Link link = linkTo(PersonControllerImpl.class).withSelfRel();
assertThat(link.getHref(), startsWith("http://foobar:8088"));
}
/**
* @see #90
*/
@Test
public void usesFirstHostOfXForwardedHost() {
request.addHeader("X-Forwarded-Host", "barfoo:8888, localhost:8088");
Link link = linkTo(PersonControllerImpl.class).withSelfRel();
assertThat(link.getHref(), startsWith("http://barfoo:8888"));
}
/**
* @see #122, #169
*/
@Test
public void appendsOptionalParameterVariableForUnsetParameter() {
Link link = linkTo(methodOn(ControllerWithMethods.class).methodForOptionalNextPage(null)).withSelfRel();
assertThat(link.getVariables(), contains(new TemplateVariable("offset", VariableType.REQUEST_PARAM)));
assertThat(link.expand().getHref(), endsWith("/foo"));
}
/**
* @see #122, #169
*/
@Test
public void rejectsMissingPathVariable() {
Link link = linkTo(methodOn(ControllerWithMethods.class).methodWithPathVariable(null))//
.withSelfRel();
exception.expect(IllegalArgumentException.class);
link.expand();
}
/**
* @see #122, #169
*/
@Test(expected = IllegalArgumentException.class)
public void rejectsMissingRequiredRequestParam() {
Link link = linkTo(methodOn(ControllerWithMethods.class).methodWithRequestParam(null)).withSelfRel();
assertThat(link.getVariableNames(), contains("id"));
link.expand();
}
/**
* @see #170
*/
@Test
public void usesForwardedPortFromHeader() {
request.addHeader("X-Forwarded-Host", "foobarhost");
request.addHeader("X-Forwarded-Port", "9090");
request.setServerPort(8080);
Link link = linkTo(PersonControllerImpl.class).withSelfRel();
assertThat(link.getHref(), startsWith("http://foobarhost:9090/"));
}
/**
* @see #170
*/
@Test
public void usesForwardedHostFromHeaderWithDefaultPort() {
request.addHeader("X-Forwarded-Host", "foobarhost");
request.setServerPort(8080);
Link link = linkTo(PersonControllerImpl.class).withSelfRel();
assertThat(link.getHref(), startsWith("http://foobarhost/"));
}
/**
* @see #114
*/
@Test
public void discoversParentClassTypeMappingForInvocation() {
Link link = linkTo(methodOn(ChildController.class).myMethod()).withSelfRel();
assertThat(link.getHref(), endsWith("/parent/child"));
}
/**
* @see #114
*/
@Test
public void includesTypeMappingFromChildClass() {
Link link = linkTo(methodOn(ChildWithTypeMapping.class).myMethod()).withSelfRel();
assertThat(link.getHref(), endsWith("/child/parent"));
}
/**
* @see #96
*/
@Test
public void linksToMethodWithPathVariableContainingBlank() {
Link link = linkTo(methodOn(ControllerWithMethods.class).methodWithPathVariable("with blank")).withSelfRel();
assertThat(link.getRel(), is(Link.REL_SELF));
assertThat(link.getHref(), endsWith("/something/with%20blank/foo"));
}
/**
* @see #192
*/
@Test
public void usesRootMappingOfTargetClassForMethodsOfParentClass() {
Link link = linkTo(methodOn(ChildControllerWithRootMapping.class).someEmptyMappedMethod()).withSelfRel();
assertThat(link.getHref(), endsWith("/root"));
}
/**
* @see #192
*/
@Test
public void usesRootMappingOfTargetClassForMethodsOfParen() throws Exception {
Method method = ParentControllerWithoutRootMapping.class.getMethod("someEmptyMappedMethod");
Link link = linkTo(ChildControllerWithRootMapping.class, method).withSelfRel();
assertThat(link.getHref(), endsWith("/root"));
}
/**
* @see #257, #107
*/
@Test
public void usesXForwardedProtoHeaderAsLinkSchema() {
for (String proto : Arrays.asList("http", "https")) {
setUp();
request.addHeader("X-Forwarded-Proto", proto);
Link link = linkTo(PersonControllerImpl.class).withSelfRel();
assertThat(link.getHref(), startsWith(proto + "://"));
}
}
/**
* @see #257, #107
*/
@Test
public void usesProtoValueFromForwardedHeaderAsLinkSchema() {
for (String proto : Arrays.asList("http", "https")) {
setUp();
request.addHeader("Forwarded", new String[] { "proto=" + proto });
Link link = linkTo(PersonControllerImpl.class).withSelfRel();
assertThat(link.getHref(), startsWith(proto.concat("://")));
}
}
/**
* @see #257, #107
*/
@Test
public void favorsStandardForwardHeaderOverXForwardedProto() {
request.addHeader("X-Forwarded-Proto", "foo");
request.addHeader(ForwardedHeader.NAME, "proto=bar");
Link link = linkTo(PersonControllerImpl.class).withSelfRel();
assertThat(link.getHref(), startsWith("bar://"));
}
/**
* @see #331
*/
@Test
public void linksToMethodWithRequestParamImplicitlySetToFalse() {
Link link = linkTo(methodOn(ControllerWithMethods.class).methodForOptionalSizeWithDefaultValue(null)).withSelfRel();
assertThat(link.getHref(), endsWith("/bar"));
}
/**
* @see #342
*/
@Test
public void mentionsRequiredUsageWithinWebRequestInException() {
exception.expect(IllegalStateException.class);
exception.expectMessage("request");
exception.expectMessage("Spring MVC");
RequestContextHolder.setRequestAttributes(null);
linkTo(methodOn(ControllerLinkBuilderUnitTest.PersonsAddressesController.class, 15).getAddressesForCountry("DE"))
.withSelfRel();
}
/**
* @see #398
*/
@Test
public void encodesRequestParameterWithSpecialValue() {
Link link = linkTo(methodOn(ControllerWithMethods.class).methodWithRequestParam("Spring#\n")).withSelfRel();
assertThat(link.getRel(), is(Link.REL_SELF));
assertThat(link.getHref(), endsWith("/something/foo?id=Spring%23%0A"));
}
/**
* @see #169
*/
@Test
public void createsPartiallyExpandedLink() {
Link link = linkTo(methodOn(PersonsAddressesController.class, "some id").getAddressesForCountry(null))
.withSelfRel();
assertThat(link.isTemplated(), is(true));
assertThat(link.getHref(), containsString("some%20id"));
}
/**
* @see #169
*/
@Test
public void addsRequestParameterVariablesForMissingRequiredParameter() {
Link link = linkTo(methodOn(ControllerWithMethods.class).methodForNextPage("1", 10, null)).withSelfRel();
assertThat(link.getVariableNames(), contains("limit"));
exception.expect(IllegalArgumentException.class);
exception.expectMessage("limit");
link.expand();
}
/**
* @see #169
*/
@Test
public void addsOptionalRequestParameterTemplateForMissingValue() {
Link link = linkTo(methodOn(ControllerWithMethods.class).methodForNextPage("1", null, 5)).withSelfRel();
assertThat(link.getVariables(), contains(new TemplateVariable("offset", VariableType.REQUEST_PARAM_CONTINUED)));
UriComponents components = toComponents(link);
assertThat(components.getQueryParams().get("query"), is(nullValue()));
}
/**
* @see #509
*/
@Test
public void supportsTwoProxiesAddingXForwardedPort() {
request.addHeader("X-Forwarded-Port", "1443,8443");
request.addHeader("X-Forwarded-Host", "proxy1,proxy2");
assertThat(linkTo(PersonControllerImpl.class).withSelfRel().getHref(), startsWith("http://proxy1:1443"));
}
/**
* @see #509
*/
@Test
public void resolvesAmbiguousXForwardedHeaders() {
request.addHeader("X-Forwarded-Proto", "http");
request.addHeader("X-Forwarded-Ssl", "on");
assertThat(linkTo(PersonControllerImpl.class).withSelfRel().getHref(), startsWith("http://"));
}
/**
* @see #527
*/
@Test
public void createsLinkRelativeToContextRoot() {
request.setContextPath("/ctx");
request.setServletPath("/foo");
request.setRequestURI("/ctx/foo");
assertThat(linkTo(PersonControllerImpl.class).withSelfRel().getHref(), endsWith("/ctx/people"));
}
private static UriComponents toComponents(Link link) {
return UriComponentsBuilder.fromUriString(link.expand().getHref()).build();
}
static class Person implements Identifiable<Long> {
Long id;
@Override
public Long getId() {
return id;
}
}
@RequestMapping("/people")
interface PersonController {}
class PersonControllerImpl implements PersonController {}
@RequestMapping("/people/{id}/addresses")
static class PersonsAddressesController {
@RequestMapping("/{country}")
public HttpEntity<Void> getAddressesForCountry(@PathVariable String country) {
return null;
}
}
@RequestMapping({ "/persons", "/people" })
class InvalidController {
}
class UnmappedController {
}
@RequestMapping("/something")
static class ControllerWithMethods {
@RequestMapping("/else")
HttpEntity<Void> myMethod(@RequestBody Object payload) {
return null;
}
@RequestMapping("/{id}/foo")
HttpEntity<Void> methodWithPathVariable(@PathVariable String id) {
return null;
}
@RequestMapping("/foo")
HttpEntity<Void> methodWithRequestParam(@RequestParam String id) {
return null;
}
@RequestMapping(value = "/{id}/foo")
HttpEntity<Void> methodForNextPage(@PathVariable String id, @RequestParam(required = false) Integer offset,
@RequestParam Integer limit) {
return null;
}
@RequestMapping(value = "/{id}/foo")
HttpEntity<Void> methodWithMultiValueRequestParams(@PathVariable String id, @RequestParam List<Integer> items,
@RequestParam Integer limit) {
return null;
}
@RequestMapping(value = "/foo")
HttpEntity<Void> methodForOptionalNextPage(@RequestParam(required = false) Integer offset) {
return null;
}
@RequestMapping(value = "/bar")
HttpEntity<Void> methodForOptionalSizeWithDefaultValue(@RequestParam(defaultValue = "10") Integer size) {
return null;
}
}
@RequestMapping("/parent")
interface ParentController {}
interface ChildController extends ParentController {
@RequestMapping("/child")
Object myMethod();
}
interface ParentWithMethod {
@RequestMapping("/parent")
Object myMethod();
}
@RequestMapping("/child")
interface ChildWithTypeMapping extends ParentWithMethod {}
interface ParentControllerWithoutRootMapping {
@RequestMapping
Object someEmptyMappedMethod();
}
@RequestMapping("/root")
interface ChildControllerWithRootMapping extends ParentControllerWithoutRootMapping {
}
}