package org.zalando.riptide;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.core.io.ClassPathResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.test.web.client.MockRestServiceServer;
import org.springframework.web.client.RestClientException;
import org.zalando.riptide.model.MediaTypes;
import org.zalando.riptide.model.Success;
import java.net.URI;
import java.util.concurrent.CompletionException;
import java.util.concurrent.atomic.AtomicReference;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
import static org.hobsoft.hamcrest.compose.ComposeMatchers.hasFeature;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.springframework.http.HttpStatus.ACCEPTED;
import static org.springframework.http.HttpStatus.MOVED_PERMANENTLY;
import static org.springframework.http.HttpStatus.NOT_FOUND;
import static org.springframework.http.HttpStatus.OK;
import static org.springframework.http.HttpStatus.Series.CLIENT_ERROR;
import static org.springframework.http.HttpStatus.Series.SUCCESSFUL;
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.http.MediaType.APPLICATION_XML;
import static org.springframework.http.MediaType.TEXT_PLAIN;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
import static org.springframework.test.web.client.response.MockRestResponseCreators.withCreatedEntity;
import static org.springframework.test.web.client.response.MockRestResponseCreators.withStatus;
import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;
import static org.zalando.riptide.Bindings.anyContentType;
import static org.zalando.riptide.Bindings.anySeries;
import static org.zalando.riptide.Bindings.anyStatus;
import static org.zalando.riptide.Bindings.on;
import static org.zalando.riptide.Navigators.contentType;
import static org.zalando.riptide.Navigators.series;
import static org.zalando.riptide.Navigators.status;
import static org.zalando.riptide.Route.pass;
import static org.zalando.riptide.model.MediaTypes.ERROR;
import static org.zalando.riptide.model.MediaTypes.PROBLEM;
import static org.zalando.riptide.model.MediaTypes.SUCCESS;
public final class FailedDispatchTest {
@Rule
public final ExpectedException exception = ExpectedException.none();
private final String url = "https://api.example.com";
private final Rest unit;
private final MockRestServiceServer server;
public FailedDispatchTest() {
final MockSetup setup = new MockSetup();
this.unit = setup.getRest();
this.server = setup.getServer();
}
@Test
public void shouldThrowIfNoMatch() {
server.expect(requestTo(url))
.andRespond(withSuccess()
.body("")
.contentType(APPLICATION_JSON));
exception.expect(CompletionException.class);
exception.expectCause(instanceOf(NoRouteException.class));
exception.expectMessage(containsString("Unable to dispatch response: 200 - OK"));
exception.expectMessage(containsString("Content-Type=[" + APPLICATION_JSON + "]"));
exception.expectCause(hasFeature("response", NoRouteException::getResponse, notNullValue()));
unit.options(url)
.dispatch(contentType(),
// note that we don't match on application/json explicitly
on(SUCCESS).call(pass()),
on(PROBLEM).call(pass()),
on(ERROR).call(pass()))
.join();
}
@Test
public void shouldThrowOnFailedConversionBecauseOfUnknownContentType() {
server.expect(requestTo(url))
.andRespond(withSuccess()
.body("{}")
.contentType(MediaType.APPLICATION_ATOM_XML));
exception.expect(CompletionException.class);
exception.expectCause(instanceOf(RestClientException.class));
exception.expectMessage("no suitable HttpMessageConverter found for response type");
unit.get(url)
.dispatch(status(),
on(HttpStatus.OK)
.dispatch(series(),
on(SUCCESSFUL).call(Success.class, success -> { }),
anySeries().call(pass())),
on(HttpStatus.CREATED).call(pass()),
anyStatus().call(this::fail))
.join();
}
@Test
public void shouldThrowOnFailedConversionBecauseOfFaultyBody() {
server.expect(requestTo(url))
.andRespond(withSuccess()
.body("{")
.contentType(MediaTypes.SUCCESS));
exception.expect(CompletionException.class);
exception.expectCause(instanceOf(HttpMessageNotReadableException.class));
exception.expectMessage("Could not read");
unit.get(url)
.dispatch(status(),
on(HttpStatus.OK)
.dispatch(series(),
on(SUCCESSFUL).call(Success.class, success -> { }),
anySeries().call(pass())),
on(HttpStatus.CREATED).call(pass()),
anyStatus().call(this::fail))
.join();
}
@Test
public void shouldHandleNoBodyAtAll() {
final HttpHeaders headers = new HttpHeaders();
headers.setContentLength(0);
server.expect(requestTo(url))
.andRespond(withStatus(HttpStatus.OK)
.headers(headers)
.contentType(MediaTypes.SUCCESS));
final AtomicReference<Success> success = new AtomicReference<>();
unit.get(url)
.dispatch(status(),
on(HttpStatus.OK)
.dispatch(contentType(),
on(MediaTypes.SUCCESS).call(Success.class, success::set),
anyContentType().call(this::fail)),
on(HttpStatus.CREATED).call(Success.class, success::set),
anyStatus().call(this::fail))
.join();
assertThat(success.get(), is(nullValue()));
}
private void fail(final ClientHttpResponse response) {
throw new AssertionError("Should not have been executed");
}
@Test
public void shouldPropagateIfNoMatch() throws Exception {
server.expect(requestTo(url))
.andRespond(withSuccess()
.body(new ClassPathResource("success.json"))
.contentType(APPLICATION_JSON));
final ClientHttpResponseConsumer consumer = mock(ClientHttpResponseConsumer.class);
unit.get(url)
.dispatch(series(),
on(SUCCESSFUL).dispatch(status(),
on(OK).dispatch(contentType(),
on(APPLICATION_XML).call(pass()),
on(TEXT_PLAIN).call(pass())),
on(ACCEPTED).call(pass()),
anyStatus().call(consumer)),
on(CLIENT_ERROR).call(pass()));
verify(consumer).tryAccept(any());
}
@Test
public void shouldPropagateMultipleLevelsIfNoMatch() throws Exception {
server.expect(requestTo(url))
.andRespond(withSuccess()
.body(new ClassPathResource("success.json"))
.contentType(APPLICATION_JSON));
final ClientHttpResponseConsumer consumer = mock(ClientHttpResponseConsumer.class);
unit.get(url)
.dispatch(series(),
on(SUCCESSFUL).dispatch(status(),
on(OK).dispatch(contentType(),
on(APPLICATION_XML).call(pass()),
on(TEXT_PLAIN).call(pass())),
on(ACCEPTED).call(pass())),
on(CLIENT_ERROR).call(pass()),
anySeries().call(consumer));
verify(consumer).tryAccept(any());
}
@Test
public void shouldPreserveExceptionIfPropagateFailed() {
server.expect(requestTo(url))
.andRespond(withCreatedEntity(URI.create("about:blank"))
.body(new ClassPathResource("success.json"))
.contentType(APPLICATION_JSON));
exception.expect(CompletionException.class);
exception.expectCause(instanceOf(NoRouteException.class));
exception.expectMessage(containsString("Unable to dispatch response: 201 - Created"));
exception.expectMessage(containsString("Content-Type=[" + APPLICATION_JSON + "]"));
exception.expectCause(hasFeature("response", NoRouteException::getResponse, notNullValue()));
unit.post(url)
.dispatch(series(),
on(SUCCESSFUL).dispatch(contentType(),
on(APPLICATION_JSON).dispatch(status(),
on(OK).call(pass()),
on(MOVED_PERMANENTLY).call(pass()),
on(NOT_FOUND).call(pass())),
on(APPLICATION_XML).call(pass()),
on(TEXT_PLAIN).call(pass())),
on(CLIENT_ERROR).call(pass()))
.join();
}
}