I am trying to unit test a Server Sent Event resource with an additional cookie. I am already using Jersey for the EventSource and JavaX for the client. The following code works fine:
WebTarget target = ClientBuilder.newBuilder()
.register(SseFeature.class)
.build()
.target("http://localhost:8080/sse");
EventSource eventSource = EventSource.target(target).build();
EventListener listener = new EventListener() {
@Override
public void onEvent(InboundEvent inboundEvent) {
LOG.info(inboundEvent.readData(String.class));
}
};
eventSource.register(listener);
eventSource.open();
serverEventManager.send("/sse", "foo");
eventSource.close();
Hoewever, for an additional unit test I need to add an additional cookie to the request. I have already tryed the following
target.(...).request.cookie("foo", "bar");
But this returns a builder from which I can't create the required WebTarget for the EventSource.
A server-sent event is when a web page automatically gets updates from a server. This was also possible before, but the web page would have to ask if any updates were available. With server-sent events, the updates come automatically.
The server-sent events streaming can be started by the client's GET request to Server. Accept: text/event-stream indicates the client waiting for event stream from the server, Cache-Control: no-cache indicates that disabling the caching and Connection: keep-alive indicates the persistent connection.
A connection over SSE typically begins with client-initiated communication between client and server. The client creates a new JavaScript EventSource object, passing the URL of an endpoint to the server over a regular HTTP request. The client expects a response with a stream of event messages over time.
Under the hood, SSE provides an efficient, cross-browser implementation of XHR streaming; the actual delivery of the messages is done over a single, long-lived HTTP connection.
Here's what's happens within EventSource to establish a conection to the given WebTarget:
private Invocation.Builder prepareHandshakeRequest() {
Invocation.Builder request = EventSource.this.target
.request(new MediaType[] { SseFeature.SERVER_SENT_EVENTS_TYPE });
if ((this.lastEventId != null) && (!(this.lastEventId.isEmpty()))) {
request.header("Last-Event-ID", this.lastEventId);
}
if (EventSource.this.disableKeepAlive) {
request.header("Connection", "close");
}
return request;
}
As we can see - no chance to add a cookie here.
So WebTarget.request(new MediaType[] { SseFeature.SERVER_SENT_EVENTS_TYPE })
needs to return a Builder that
already has that desired cookie added.
Consider this delegating class adding the desired cookie to all request*
type methods:
public class CookieAddedWebTarget implements WebTarget {
private WebTarget base;
private Cookie cookie;
public CookieAddedWebTarget(WebTarget base, Cookie cookie) {
this.base = base;
this.cookie = cookie;
}
// Inject that cookie whenever someone requests a Builder (like EventSource does):
public Builder request() {
return base.request().cookie(cookie);
}
public Builder request(String... paramArrayOfString) {
return base.request(paramArrayOfString).cookie(cookie);
}
public Builder request(MediaType... paramArrayOfMediaType) {
return base.request(paramArrayOfMediaType).cookie(cookie);
}
public Configuration getConfiguration() {
return base.getConfiguration();
}
//All other methods from WebTarget are delegated as-is:
public URI getUri() {
return base.getUri();
}
public UriBuilder getUriBuilder() {
return base.getUriBuilder();
}
public WebTarget path(String paramString) {
return base.path(paramString);
}
public WebTarget matrixParam(String paramString, Object... paramArrayOfObject) {
return base.matrixParam(paramString, paramArrayOfObject);
}
public WebTarget property(String paramString, Object paramObject) {
return base.property(paramString, paramObject);
}
public WebTarget queryParam(String paramString, Object... paramArrayOfObject) {
return base.queryParam(paramString, paramArrayOfObject);
}
public WebTarget register(Class<?> paramClass, Class<?>... paramArrayOfClass) {
return base.register(paramClass, paramArrayOfClass);
}
public WebTarget register(Class<?> paramClass, int paramInt) {
return base.register(paramClass, paramInt);
}
public WebTarget register(Class<?> paramClass, Map<Class<?>, Integer> paramMap) {
return base.register(paramClass, paramMap);
}
public WebTarget register(Class<?> paramClass) {
return base.register(paramClass);
}
public WebTarget register(Object paramObject, Class<?>... paramArrayOfClass) {
return base.register(paramObject, paramArrayOfClass);
}
public WebTarget register(Object paramObject, int paramInt) {
return base.register(paramObject, paramInt);
}
public WebTarget register(Object paramObject, Map<Class<?>, Integer> paramMap) {
return base.register(paramObject, paramMap);
}
public WebTarget register(Object paramObject) {
return base.register(paramObject);
}
public WebTarget resolveTemplate(String paramString, Object paramObject) {
return base.resolveTemplate(paramString, paramObject);
}
public WebTarget resolveTemplate(String paramString, Object paramObject, boolean paramBoolean) {
return base.resolveTemplate(paramString, paramObject, paramBoolean);
}
public WebTarget resolveTemplateFromEncoded(String paramString, Object paramObject) {
return base.resolveTemplateFromEncoded(paramString, paramObject);
}
public WebTarget resolveTemplates(Map<String, Object> paramMap) {
return base.resolveTemplates(paramMap);
}
public WebTarget resolveTemplates(Map<String, Object> paramMap, boolean paramBoolean) {
return base.resolveTemplates(paramMap, paramBoolean);
}
public WebTarget resolveTemplatesFromEncoded(Map<String, Object> paramMap) {
return base.resolveTemplatesFromEncoded(paramMap);
}
}
Now you should be able to reerite your test to:
EventSource eventSource = EventSource.target(new CookieAddedWebTarget(target,
new Cookie("name", "value"))).build();
And the cookie should be inserted.
Caveat: I have no way of testing this. Solution based only on reading source-code of jersey-media-sse-2.22.1.
Good Luck.
You could set the cookie in a ClientRequestFilter
. Though the getCookies()
on the ClientRequestContext
is immutable, you should remember that a cookie is technically nothing more than a header. And the headers map on the request context is mutable. So you could do something like
public static class SseCookieFilter implements ClientRequestFilter {
@Override
public void filter(ClientRequestContext requestContext) throws IOException {
Cookie cookie = new Cookie("foo", "bar");
requestContext.getHeaders().add("Cookie", cookie.toString());
}
}
Just register the filter with the client (client.register(new SseCookieFilter())
). It would be the same result as if you were to do
target.(...).request().cookie("foo", "bar");
Here is a complete example using Jersey Test Framework
public class SseCookieFilterTest extends JerseyTest {
@Path("events")
public static class SseResource {
@GET
@Produces(SseFeature.SERVER_SENT_EVENTS)
public EventOutput getServerSentEvents(@CookieParam("foo") String foo) {
final EventOutput eventOutput = new EventOutput();
new Thread(() -> {
try {
final OutboundEvent.Builder eventBuilder
= new OutboundEvent.Builder();
eventBuilder.name("message");
eventBuilder.data(String.class, "Blah " + foo + "!!!");
final OutboundEvent event = eventBuilder.build();
eventOutput.write(event);
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
try {
eventOutput.close();
} catch (IOException ioClose) {
throw new RuntimeException(ioClose);
}
}
}).start();
return eventOutput;
}
}
public static class SseCookieFilter implements ClientRequestFilter {
@Override
public void filter(ClientRequestContext requestContext) throws IOException {
Cookie cookie = new Cookie("foo", "bar");
requestContext.getHeaders().add("Cookie", cookie.toString());
}
}
@Override
public ResourceConfig configure() {
return new ResourceConfig(SseResource.class)
.register(new LoggingFilter());
}
@Test
public void doit() throws Exception {
Client client = ClientBuilder.newBuilder()
.register(SseFeature.class).build();
client.register(new SseCookieFilter());
WebTarget target = client.target("http://localhost:9998/events");
EventSource eventSource = EventSource.target(target).build();
EventListener listener = (InboundEvent inboundEvent) -> {
System.out.println("From server ---====++++> "
+ inboundEvent.readData(String.class));
};
eventSource.register(listener, "message");
eventSource.open();
Thread.sleep(100);
eventSource.close();
}
}
These are the only dependencies needed to test
<dependency>
<groupId>org.glassfish.jersey.test-framework.providers</groupId>
<artifactId>jersey-test-framework-provider-grizzly2</artifactId>
<version>${jersey2.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.media</groupId>
<artifactId>jersey-media-sse</artifactId>
<version>${jersey2.version}</version>
<scope>test</scope>
</dependency>
Here is the server side result from the LoggingFilter
I registered on the server in the test
INFO: 1 * Server has received a request on thread grizzly-http-server-2
1 > GET http://localhost:9998/events
1 > accept: text/event-stream
1 > connection: close
1 > cookie: $Version=1;foo=bar
1 > host: localhost:9998
1 > user-agent: Jersey/2.19 (HttpUrlConnection 1.8.0_31)
INFO: 1 * Server responded with a response on thread grizzly-http-server-2
1 < 200
1 < Content-Type: text/event-stream
From server ---====++++> Blah bar!!!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With