Commit c626bcca by Dave Syer

Add rest template to extract SSE events and assert them

The message has to be sent after the endpoint starts up, which only happens after the first request is accepted, so we put that logic in the response extractor.
parent e2951880
......@@ -15,6 +15,7 @@
<properties>
<main.basedir>${basedir}/..</main.basedir>
<turbine.version>2.0.0-DP.2</turbine.version>
<spring-cloud-contract.version>1.0.5.RELEASE</spring-cloud-contract.version>
</properties>
<build>
<plugins>
......@@ -104,5 +105,11 @@
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-contract-stub-runner</artifactId>
<version>${spring-cloud-contract.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
......@@ -20,16 +20,17 @@ import java.io.IOException;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.integration.annotation.ServiceActivator;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import com.fasterxml.jackson.databind.ObjectMapper;
import rx.subjects.PublishSubject;
/**
......
......@@ -20,17 +20,17 @@ import java.util.Map;
import javax.annotation.PostConstruct;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.config.BindingProperties;
import org.springframework.cloud.stream.config.ChannelBindingServiceProperties;
import org.springframework.cloud.stream.config.BindingServiceProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.fasterxml.jackson.databind.ObjectMapper;
import rx.subjects.PublishSubject;
/**
......@@ -48,7 +48,7 @@ import rx.subjects.PublishSubject;
public class TurbineStreamAutoConfiguration {
@Autowired
private ChannelBindingServiceProperties bindings;
private BindingServiceProperties bindings;
@Autowired
private TurbineStreamProperties properties;
......
......@@ -16,13 +16,25 @@
package org.springframework.cloud.netflix.turbine.stream;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.reactivex.netty.RxNetty;
import io.reactivex.netty.protocol.http.server.HttpServer;
import io.reactivex.netty.protocol.http.sse.ServerSentEvent;
import com.netflix.turbine.aggregator.InstanceKey;
import com.netflix.turbine.aggregator.StreamAggregator;
import com.netflix.turbine.internal.JsonUtility;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.client.actuator.HasFeatures;
......@@ -31,16 +43,8 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.SocketUtils;
import com.netflix.turbine.aggregator.InstanceKey;
import com.netflix.turbine.aggregator.StreamAggregator;
import com.netflix.turbine.internal.JsonUtility;
import static io.reactivex.netty.pipeline.PipelineConfigurators.sseServerConfigurator;
import static io.reactivex.netty.pipeline.PipelineConfigurators.serveSseConfigurator;
import io.netty.buffer.ByteBuf;
import io.reactivex.netty.RxNetty;
import io.reactivex.netty.protocol.http.server.HttpServer;
import io.reactivex.netty.protocol.text.sse.ServerSentEvent;
import rx.Observable;
import rx.subjects.PublishSubject;
......@@ -96,12 +100,12 @@ public class TurbineStreamConfiguration implements SmartLifecycle {
.createHttpServer(this.turbinePort, (request, response) -> {
log.info("SSE Request Received");
response.getHeaders().setHeader("Content-Type", "text/event-stream");
return output
.doOnUnsubscribe(() -> log
.info("Unsubscribing RxNetty server connection"))
return output.doOnUnsubscribe(
() -> log.info("Unsubscribing RxNetty server connection"))
.flatMap(data -> response.writeAndFlush(new ServerSentEvent(
null, null, JsonUtility.mapToJson(data))));
}, sseServerConfigurator());
Unpooled.copiedBuffer(JsonUtility.mapToJson(data),
StandardCharsets.UTF_8))));
}, serveSseConfigurator());
return httpServer;
}
......
......@@ -16,13 +16,13 @@
package org.springframework.cloud.netflix.turbine.stream;
import java.util.Objects;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.netflix.hystrix.HystrixConstants;
import org.springframework.http.MediaType;
import java.util.Objects;
/**
* @author Dave Syer
* @author Gregor Zurowski
......@@ -63,12 +63,13 @@ public class TurbineStreamProperties {
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
TurbineStreamProperties that = (TurbineStreamProperties) o;
return port == that.port &&
Objects.equals(destination, that.destination) &&
Objects.equals(contentType, that.contentType);
return port == that.port && Objects.equals(destination, that.destination)
&& Objects.equals(contentType, that.contentType);
}
@Override
......@@ -78,11 +79,9 @@ public class TurbineStreamProperties {
@Override
public String toString() {
return new StringBuilder("TurbineStreamProperties{")
.append("port=").append(port).append(", ")
.append("destination='").append(destination).append("', ")
.append("contentType='").append(contentType).append("'}")
.toString();
return new StringBuilder("TurbineStreamProperties{").append("port=").append(port)
.append(", ").append("destination='").append(destination).append("', ")
.append("contentType='").append(contentType).append("'}").toString();
}
}
......@@ -16,22 +16,78 @@
package org.springframework.cloud.netflix.turbine.stream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.cloud.contract.stubrunner.StubTrigger;
import org.springframework.cloud.contract.stubrunner.spring.AutoConfigureStubRunner;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpRequest;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.integration.support.management.MessageChannelMetrics;
import org.springframework.messaging.SubscribableChannel;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestTemplate;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Spencer Gibb
*/
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = TurbineStreamTests.Application.class, webEnvironment = WebEnvironment.RANDOM_PORT, value = {
"turbine.stream.port=0", "spring.jmx.enabled=true" })
"turbine.stream.port=0",
// TODO: we don't need this if we harmonize the turbine and hystrix destinations
// https://github.com/spring-cloud/spring-cloud-netflix/issues/1948
"spring.cloud.stream.bindings.turbineStreamInput.destination=hystrixStreamOutput",
"logging.level.org.springframework.cloud.netflix.turbine=DEBUG",
"spring.jmx.enabled=true", "stubrunner.workOffline=true",
"stubrunner.ids=org.springframework.cloud:spring-cloud-netflix-hystrix-stream" })
@AutoConfigureStubRunner
public class TurbineStreamTests {
private static Log log = LogFactory.getLog(TurbineStreamTests.class);
@Autowired
StubTrigger stubTrigger;
@Autowired
ObjectMapper mapper;
@Autowired
@Qualifier(TurbineStreamClient.INPUT)
SubscribableChannel input;
RestTemplate rest = new RestTemplate();
@Autowired
TurbineStreamConfiguration turbine;
private CountDownLatch latch = new CountDownLatch(1);
@EnableAutoConfiguration
@EnableTurbineStream
public static class Application {
......@@ -41,7 +97,102 @@ public class TurbineStreamTests {
}
@Test
public void contextLoads() {
public void contextLoads() throws Exception {
rest.getInterceptors().add(new NonClosingInterceptor());
int count = ((MessageChannelMetrics) input).getSendCount();
ResponseEntity<String> response = rest.execute(
new URI("http://localhost:" + turbine.getTurbinePort() + "/"),
HttpMethod.GET, null, this::extract);
assertThat(response.getHeaders().getContentType())
.isEqualTo(MediaType.TEXT_EVENT_STREAM);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
Map<String, Object> metrics = extractMetrics(response.getBody());
assertThat(metrics).containsEntry("type", "HystrixCommand");
assertThat(((MessageChannelMetrics) input).getSendCount()).isEqualTo(count + 1);
}
@SuppressWarnings("unchecked")
private Map<String, Object> extractMetrics(String body) throws Exception {
String[] split = body.split("data:");
for (String value : split) {
if (value.contains("Ping") || value.length() == 0) {
continue;
}
else {
return mapper.readValue(value, Map.class);
}
}
return null;
}
private ResponseEntity<String> extract(ClientHttpResponse response)
throws IOException {
// The message has to be sent after the endpoint is activated, so this is a
// convenient place to put it
stubTrigger.trigger("metrics");
byte[] bytes = new byte[1024];
StringBuilder builder = new StringBuilder();
int read = 0;
while (read >= 0
&& StringUtils.countOccurrencesOf(builder.toString(), "\n") < 2) {
read = response.getBody().read(bytes, 0, bytes.length);
if (read > 0) {
latch.countDown();
builder.append(new String(bytes, 0, read));
}
log.info("Building: " + builder);
}
log.info("Done: " + builder);
return ResponseEntity.status(response.getStatusCode())
.headers(response.getHeaders()).body(builder.toString());
}
private class NonClosingInterceptor implements ClientHttpRequestInterceptor {
private class NonClosingResponse implements ClientHttpResponse {
private ClientHttpResponse delegate;
public NonClosingResponse(ClientHttpResponse delegate) {
this.delegate = delegate;
}
@Override
public InputStream getBody() throws IOException {
return delegate.getBody();
}
@Override
public HttpHeaders getHeaders() {
return delegate.getHeaders();
}
@Override
public HttpStatus getStatusCode() throws IOException {
return delegate.getStatusCode();
}
@Override
public int getRawStatusCode() throws IOException {
return delegate.getRawStatusCode();
}
@Override
public String getStatusText() throws IOException {
return delegate.getStatusText();
}
@Override
public void close() {
}
}
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body,
ClientHttpRequestExecution execution) throws IOException {
return new NonClosingResponse(execution.execute(request, body));
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment