package dev.relism.flash.testing; import java.net.URI; import java.net.http.HttpClient; import java.net.http.WebSocket; import java.time.Duration; import java.util.Objects; import java.util.concurrent.BlockingQueue; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; import java.util.concurrent.ExecutionException; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; /** * A WebSocket connected to a {@link FlashTest} server, for asserting on what a Flash endpoint * pushes back. * *
{@code
* try (FlashWebSocket socket = app.ws("/live")) {
* socket.sendText("hello");
* assertEquals("echo:hello", socket.awaitText(Duration.ofSeconds(2)));
* }
* }
*
* Backed by {@link java.net.http.WebSocket}, so the RFC 6455 handshake, masking and
* fragmentation are the JDK's, not hand-rolled. Incoming text is queued as it arrives, so a
* message that lands before {@link #awaitText} is called is not lost.
*/
public final class FlashWebSocket implements AutoCloseable {
private final BlockingQueueNever throws: this is cleanup, usually in a try-with-resources, and a connection the * server already dropped must not mask the failure the test was actually reporting. Use * {@link #awaitClose} when the close itself is what you are asserting on. */ @Override public void close() { try { if (!socket.isOutputClosed()) socket.sendClose(WebSocket.NORMAL_CLOSURE, "").join(); } catch (RuntimeException alreadyGone) { // nothing to close } closed.completeOnTimeout(WebSocket.NORMAL_CLOSURE, 1, TimeUnit.SECONDS) .exceptionally(failure -> WebSocket.NORMAL_CLOSURE) .join(); } private String poll(Duration timeout) { try { return received.poll(timeout.toMillis(), TimeUnit.MILLISECONDS); } catch (InterruptedException interrupted) { Thread.currentThread().interrupt(); throw new AssertionError("Interrupted awaiting a WebSocket message", interrupted); } } /** Reassembles fragmented text and queues whole messages. */ private final class QueueingListener implements WebSocket.Listener { private final StringBuilder partial = new StringBuilder(); @Override public void onOpen(WebSocket webSocket) { webSocket.request(1); } @Override public CompletionStage> onText(WebSocket webSocket, CharSequence data, boolean last) { partial.append(data); if (last) { received.add(partial.toString()); partial.setLength(0); } webSocket.request(1); return null; } @Override public CompletionStage> onClose(WebSocket webSocket, int statusCode, String reason) { closed.complete(statusCode); return null; } @Override public void onError(WebSocket webSocket, Throwable error) { closed.completeExceptionally(error); } } }