package org.zalando.riptide.stream; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.InputStreamResource; import org.springframework.http.HttpHeaders; import org.springframework.http.client.ClientHttpResponse; import org.springframework.test.web.client.MockRestServiceServer; import org.zalando.fauxpas.ThrowingConsumer; import org.zalando.riptide.Rest; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.UncheckedIOException; import java.net.URI; import java.util.Arrays; import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; import static java.util.Collections.singletonList; import static org.hamcrest.Matchers.instanceOf; import static org.hobsoft.hamcrest.compose.ComposeMatchers.hasFeature; import static org.junit.Assert.assertNotNull; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doCallRealMethod; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.verifyZeroInteractions; import static org.springframework.http.HttpStatus.OK; import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess; import static org.zalando.riptide.Bindings.anyStatus; import static org.zalando.riptide.Bindings.on; import static org.zalando.riptide.Navigators.status; import static org.zalando.riptide.Route.listOf; import static org.zalando.riptide.stream.Streams.APPLICATION_JSON_SEQ; import static org.zalando.riptide.stream.Streams.APPLICATION_X_JSON_STREAM; import static org.zalando.riptide.stream.Streams.forEach; import static org.zalando.riptide.stream.Streams.streamConverter; import static org.zalando.riptide.stream.Streams.streamOf; public class StreamsTest { @Rule public final ExpectedException exception = ExpectedException.none(); private final String baseUrl = "https://api.example.com"; private final URI url = URI.create(baseUrl + "/accounts"); private final Rest unit; private final MockRestServiceServer server; public StreamsTest() { final ObjectMapper mapper = new ObjectMapper().findAndRegisterModules(); final MockSetup setup = new MockSetup(baseUrl, singletonList(streamConverter(mapper))); this.server = setup.getServer(); this.unit = setup.getRest(); } @Test @Deprecated public void shouldCreateStreamConverter() { assertNotNull(streamConverter()); assertNotNull(streamConverter(null)); assertNotNull(streamConverter(null, null)); } @Test public void shouldCallConsumerWithList() throws Exception { server.expect(requestTo(url)).andRespond( withSuccess() .body(new ClassPathResource("account-list.json")) .contentType(APPLICATION_X_JSON_STREAM)); @SuppressWarnings("unchecked") final ThrowingConsumer<List<AccountBody>, Exception> verifier = mock(ThrowingConsumer.class); unit.get("/accounts").dispatch(status(), on(OK).call(streamOf(listOf(AccountBody.class)), forEach(verifier)), anyStatus().call(this::fail)).join(); verify(verifier).accept(Arrays.asList( new AccountBody("1234567890", "Acme Corporation"), new AccountBody("1234567891", "Acme Company"), new AccountBody("1234567892", "Acme GmbH"), new AccountBody("1234567893", "Acme SE"))); verifyNoMoreInteractions(verifier); } @Test public void shouldCallConsumerWithArray() throws Exception { server.expect(requestTo(url)).andRespond( withSuccess() .body(new ClassPathResource("account-list.json")) .contentType(APPLICATION_X_JSON_STREAM)); @SuppressWarnings("unchecked") final ThrowingConsumer<AccountBody[], Exception> verifier = mock(ThrowingConsumer.class); unit.get("/accounts").dispatch(status(), on(OK).call(streamOf(AccountBody[].class), forEach(verifier)), anyStatus().call(this::fail)).join(); verify(verifier).accept(new AccountBody[] { new AccountBody("1234567890", "Acme Corporation"), new AccountBody("1234567891", "Acme Company"), new AccountBody("1234567892", "Acme GmbH"), new AccountBody("1234567893", "Acme SE") }); verifyNoMoreInteractions(verifier); } @Test public void shouldCallConsumerWithJsonList() throws Exception { server.expect(requestTo(url)).andRespond( withSuccess() .body(new ClassPathResource("account-list.json")) .contentType(APPLICATION_X_JSON_STREAM)); @SuppressWarnings("unchecked") final ThrowingConsumer<AccountBody, Exception> verifier = mock(ThrowingConsumer.class); unit.get("/accounts").dispatch(status(), on(OK).call(streamOf(AccountBody.class), forEach(verifier)), anyStatus().call(this::fail)).join(); verify(verifier).accept(new AccountBody("1234567890", "Acme Corporation")); verify(verifier).accept(new AccountBody("1234567891", "Acme Company")); verify(verifier).accept(new AccountBody("1234567892", "Acme GmbH")); verify(verifier).accept(new AccountBody("1234567893", "Acme SE")); verifyNoMoreInteractions(verifier); } @Test public void shouldCallConsumerWithXJsonStream() throws Exception { server.expect(requestTo(url)).andRespond( withSuccess() .body(new ClassPathResource("account-stream.json")) .contentType(APPLICATION_X_JSON_STREAM)); @SuppressWarnings("unchecked") final ThrowingConsumer<AccountBody, Exception> verifier = mock(ThrowingConsumer.class); unit.get("/accounts").dispatch(status(), on(OK).call(streamOf(AccountBody.class), forEach(verifier)), anyStatus().call(this::fail)).join(); verify(verifier).accept(new AccountBody("1234567890", "Acme Corporation")); verify(verifier).accept(new AccountBody("1234567891", "Acme Company")); verify(verifier).accept(new AccountBody("1234567892", "Acme GmbH")); verify(verifier).accept(new AccountBody("1234567893", "Acme SE")); verifyNoMoreInteractions(verifier); } @Test public void shouldCallConsumerWithJsonSequence() throws Exception { server.expect(requestTo(url)).andRespond( withSuccess() .body(new ClassPathResource("account-sequence.json")) .contentType(APPLICATION_JSON_SEQ)); @SuppressWarnings("unchecked") final ThrowingConsumer<AccountBody, Exception> verifier = mock(ThrowingConsumer.class); unit.get("/accounts").dispatch(status(), on(OK).call(streamOf(AccountBody.class), forEach(verifier)), anyStatus().call(this::fail)).join(); verify(verifier).accept(new AccountBody("1234567890", "Acme Corporation")); verify(verifier).accept(new AccountBody("1234567891", "Acme Company")); verify(verifier).accept(new AccountBody("1234567892", "Acme GmbH")); verify(verifier).accept(new AccountBody("1234567893", "Acme SE")); verifyNoMoreInteractions(verifier); } @Test @SuppressWarnings("unchecked") public void shouldCallConsumerWithoutStream() throws Exception { server.expect(requestTo(url)).andRespond( withSuccess() .body(new ClassPathResource("account-item.json")) .contentType(APPLICATION_X_JSON_STREAM)); final ThrowingConsumer<AccountBody, Exception> verifier = mock(ThrowingConsumer.class); unit.get("/accounts").dispatch(status(), on(OK).call(AccountBody.class, verifier), anyStatus().call(this::fail)).join(); verify(verifier).tryAccept( new AccountBody("1234567890", "Acme Corporation")); verifyNoMoreInteractions(verifier); } @Test public void shouldNotCallConsumerForEmptyStream() { final HttpHeaders headers = new HttpHeaders(); headers.setContentLength(0); server.expect(requestTo(url)).andRespond( withSuccess() .headers(headers) .body(new InputStreamResource(new ByteArrayInputStream(new byte[0]))) .contentType(APPLICATION_X_JSON_STREAM)); @SuppressWarnings("unchecked") final ThrowingConsumer<AccountBody, Exception> verifier = mock(ThrowingConsumer.class); unit.get("/accounts").dispatch(status(), on(OK).call(streamOf(AccountBody.class), forEach(verifier)), anyStatus().call(this::fail)).join(); verifyZeroInteractions(verifier); } @Test public void shouldFailOnCallWithConsumerException() throws Exception { exception.expect(CompletionException.class); exception.expectCause(instanceOf(UncheckedIOException.class)); exception.expectCause(hasFeature(Throwable::getCause, instanceOf(IOException.class))); server.expect(requestTo(url)).andRespond( withSuccess() .body(new ClassPathResource("account-sequence.json")) .contentType(APPLICATION_JSON_SEQ)); @SuppressWarnings("unchecked") final ThrowingConsumer<AccountBody, Exception> verifier = mock(ThrowingConsumer.class); doCallRealMethod().when(verifier).accept(any()); doThrow(new IOException()).when(verifier).tryAccept(new AccountBody("1234567892", "Acme GmbH")); final CompletableFuture<Void> future = unit.get("/accounts").dispatch(status(), on(OK).call(streamOf(AccountBody.class), forEach(verifier)), anyStatus().call(this::fail)); verify(verifier).accept(new AccountBody("1234567890", "Acme Corporation")); verify(verifier).accept(new AccountBody("1234567891", "Acme Company")); verify(verifier).accept(new AccountBody("1234567892", "Acme GmbH")); verify(verifier, times(3)).tryAccept(any()); verifyNoMoreInteractions(verifier); future.join(); } @Test public void shouldFailOnCallWithInvalidStream() throws Exception { exception.expect(CompletionException.class); exception.expectCause(instanceOf(UncheckedIOException.class)); server.expect(requestTo(url)).andRespond( withSuccess() .body(new ClassPathResource("account-fail.json")) .contentType(APPLICATION_X_JSON_STREAM)); @SuppressWarnings("unchecked") final ThrowingConsumer<AccountBody, Exception> verifier = mock(ThrowingConsumer.class); final CompletableFuture<Void> future = unit.get("/accounts").dispatch(status(), on(OK).call(streamOf(AccountBody.class), forEach(verifier)), anyStatus().call(this::fail)); verify(verifier).accept(new AccountBody("1234567890", "Acme Corporation")); verifyNoMoreInteractions(verifier); future.join(); } private void fail(final ClientHttpResponse response) throws IOException { throw new AssertionError(response.getRawStatusCode()); } }