/*
* Copyright (C) 2014 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package okhttp3.internal.ws;
import java.io.EOFException;
import java.io.IOException;
import java.net.ProtocolException;
import java.util.Random;
import java.util.concurrent.TimeUnit;
import okhttp3.Protocol;
import okhttp3.Request;
import okhttp3.Response;
import okio.ByteString;
import okio.Okio;
import okio.Pipe;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public final class RealWebSocketTest {
// NOTE: Fields are named 'client' and 'server' for cognitive simplicity. This differentiation has
// zero effect on the behavior of the WebSocket API which is why tests are only written once
// from the perspective of a single peer.
private final Random random = new Random(0);
private final Pipe client2Server = new Pipe(1024L);
private final Pipe server2client = new Pipe(1024L);
private TestStreams client = new TestStreams(true, server2client, client2Server);
private TestStreams server = new TestStreams(false, client2Server, server2client);
@Before public void setUp() throws IOException {
client.initWebSocket(random, 0);
server.initWebSocket(random, 0);
}
@After public void tearDown() throws Exception {
client.listener.assertExhausted();
server.listener.assertExhausted();
server.source.close();
client.source.close();
server.webSocket.tearDown();
client.webSocket.tearDown();
}
@Test public void close() throws IOException {
client.webSocket.close(1000, "Hello!");
assertFalse(server.processNextFrame()); // This will trigger a close response.
server.listener.assertClosing(1000, "Hello!");
server.webSocket.close(1000, "Goodbye!");
assertFalse(client.processNextFrame());
client.listener.assertClosing(1000, "Goodbye!");
server.listener.assertClosed(1000, "Hello!");
client.listener.assertClosed(1000, "Goodbye!");
}
@Test public void clientCloseThenMethodsReturnFalse() throws IOException {
client.webSocket.close(1000, "Hello!");
assertFalse(client.webSocket.close(1000, "Hello!"));
assertFalse(client.webSocket.send("Hello!"));
}
@Test public void afterSocketClosedPingFailsWebSocket() throws IOException {
client2Server.source().close();
client.webSocket.pong(ByteString.encodeUtf8("Ping!"));
client.listener.assertFailure(IOException.class, "source is closed");
assertFalse(client.webSocket.send("Hello!"));
}
@Test public void socketClosedDuringMessageKillsWebSocket() throws IOException {
client2Server.source().close();
assertTrue(client.webSocket.send("Hello!"));
client.listener.assertFailure(IOException.class, "source is closed");
// A failed write prevents further use of the WebSocket instance.
assertFalse(client.webSocket.send("Hello!"));
assertFalse(client.webSocket.pong(ByteString.encodeUtf8("Ping!")));
}
@Test public void serverCloseThenWritingPingSucceeds() throws IOException {
server.webSocket.close(1000, "Hello!");
client.processNextFrame();
client.listener.assertClosing(1000, "Hello!");
assertTrue(client.webSocket.pong(ByteString.encodeUtf8("Pong?")));
}
@Test public void clientCanWriteMessagesAfterServerClose() throws IOException {
server.webSocket.close(1000, "Hello!");
client.processNextFrame();
client.listener.assertClosing(1000, "Hello!");
assertTrue(client.webSocket.send("Hi!"));
server.processNextFrame();
server.listener.assertTextMessage("Hi!");
}
@Test public void serverCloseThenClientClose() throws IOException {
server.webSocket.close(1000, "Hello!");
client.processNextFrame();
client.listener.assertClosing(1000, "Hello!");
assertTrue(client.webSocket.close(1000, "Bye!"));
}
@Test public void emptyCloseInitiatesShutdown() throws IOException {
server.sink.write(ByteString.decodeHex("8800")).emit(); // Close without code.
client.processNextFrame();
client.listener.assertClosing(1005, "");
assertTrue(client.webSocket.close(1000, "Bye!"));
server.processNextFrame();
server.listener.assertClosing(1000, "Bye!");
client.listener.assertClosed(1005, "");
}
@Test public void clientCloseClosesConnection() throws IOException {
client.webSocket.close(1000, "Hello!");
assertFalse(client.closed);
server.processNextFrame(); // Read client closing, send server close.
server.listener.assertClosing(1000, "Hello!");
server.webSocket.close(1000, "Goodbye!");
client.processNextFrame(); // Read server closing, close connection.
assertTrue(client.closed);
client.listener.assertClosing(1000, "Goodbye!");
// Server and client both finished closing, connection is closed.
server.listener.assertClosed(1000, "Hello!");
client.listener.assertClosed(1000, "Goodbye!");
}
@Test public void serverCloseClosesConnection() throws IOException {
server.webSocket.close(1000, "Hello!");
client.processNextFrame(); // Read server close, send client close, close connection.
assertFalse(client.closed);
client.listener.assertClosing(1000, "Hello!");
client.webSocket.close(1000, "Hello!");
server.processNextFrame();
server.listener.assertClosing(1000, "Hello!");
client.listener.assertClosed(1000, "Hello!");
server.listener.assertClosed(1000, "Hello!");
}
@Test public void clientAndServerCloseClosesConnection() throws Exception {
// Send close from both sides at the same time.
server.webSocket.close(1000, "Hello!");
client.processNextFrame(); // Read close, close connection close.
assertFalse(client.closed);
client.webSocket.close(1000, "Hi!");
server.processNextFrame();
client.listener.assertClosing(1000, "Hello!");
server.listener.assertClosing(1000, "Hi!");
client.listener.assertClosed(1000, "Hello!");
server.listener.assertClosed(1000, "Hi!");
client.webSocket.awaitTermination(5, TimeUnit.SECONDS);
assertTrue(client.closed);
server.listener.assertExhausted(); // Client should not have sent second close.
client.listener.assertExhausted(); // Server should not have sent second close.
}
@Test public void serverCloseBreaksReadMessageLoop() throws IOException {
server.webSocket.send("Hello!");
server.webSocket.close(1000, "Bye!");
assertTrue(client.processNextFrame());
client.listener.assertTextMessage("Hello!");
assertFalse(client.processNextFrame());
client.listener.assertClosing(1000, "Bye!");
}
@Test public void protocolErrorBeforeCloseSendsFailure() throws IOException {
server.sink.write(ByteString.decodeHex("0a00")).emit(); // Invalid non-final ping frame.
client.processNextFrame(); // Detects error, send close, close connection.
assertTrue(client.closed);
client.listener.assertFailure(ProtocolException.class, "Control frames must be final.");
server.processNextFrame();
server.listener.assertFailure(EOFException.class);
}
@Test public void protocolErrorInCloseResponseClosesConnection() throws IOException {
client.webSocket.close(1000, "Hello");
server.processNextFrame();
assertFalse(client.closed); // Not closed until close reply is received.
// Manually write an invalid masked close frame.
server.sink.write(ByteString.decodeHex("888760b420bb635c68de0cd84f")).emit();
client.processNextFrame();// Detects error, disconnects immediately since close already sent.
assertTrue(client.closed);
client.listener.assertFailure(
ProtocolException.class, "Server-sent frames must not be masked.");
server.listener.assertClosing(1000, "Hello");
server.listener.assertExhausted(); // Client should not have sent second close.
}
@Test public void protocolErrorAfterCloseDoesNotSendClose() throws IOException {
client.webSocket.close(1000, "Hello!");
server.processNextFrame();
assertFalse(client.closed); // Not closed until close reply is received.
server.sink.write(ByteString.decodeHex("0a00")).emit(); // Invalid non-final ping frame.
client.processNextFrame(); // Detects error, disconnects immediately since close already sent.
assertTrue(client.closed);
client.listener.assertFailure(ProtocolException.class, "Control frames must be final.");
server.listener.assertClosing(1000, "Hello!");
server.listener.assertExhausted(); // Client should not have sent second close.
}
@Test public void networkErrorReportedAsFailure() throws IOException {
server.sink.close();
client.processNextFrame();
client.listener.assertFailure(EOFException.class);
}
@Test public void closeThrowingFailsConnection() throws IOException {
client2Server.source().close();
client.webSocket.close(1000, null);
client.listener.assertFailure(IOException.class, "source is closed");
}
@Ignore // TODO(jwilson): come up with a way to test unchecked exceptions on the writer thread.
@Test public void closeMessageAndConnectionCloseThrowingDoesNotMaskOriginal() throws IOException {
client.sink.close();
client.closeThrows = true;
client.webSocket.close(1000, "Bye!");
client.listener.assertFailure(IOException.class, "failure");
assertTrue(client.closed);
}
@Ignore // TODO(jwilson): come up with a way to test unchecked exceptions on the writer thread.
@Test public void peerConnectionCloseThrowingPropagates() throws IOException {
client.closeThrows = true;
server.webSocket.close(1000, "Bye from Server!");
client.processNextFrame();
client.listener.assertClosing(1000, "Bye from Server!");
client.webSocket.close(1000, "Bye from Client!");
server.processNextFrame();
server.listener.assertClosing(1000, "Bye from Client!");
}
@Test public void pingOnInterval() throws IOException {
long startNanos = System.nanoTime();
client.initWebSocket(random, 500);
server.processNextFrame(); // Ping.
client.processNextFrame(); // Pong.
long elapsedUntilPing1 = System.nanoTime() - startNanos;
assertEquals(500, TimeUnit.NANOSECONDS.toMillis(elapsedUntilPing1), 250d);
server.processNextFrame(); // Ping.
client.processNextFrame(); // Pong.
long elapsedUntilPing2 = System.nanoTime() - startNanos;
assertEquals(1000, TimeUnit.NANOSECONDS.toMillis(elapsedUntilPing2), 250d);
server.processNextFrame(); // Ping.
client.processNextFrame(); // Pong.
long elapsedUntilPing3 = System.nanoTime() - startNanos;
assertEquals(1500, TimeUnit.NANOSECONDS.toMillis(elapsedUntilPing3), 250d);
}
/** One peer's streams, listener, and web socket in the test. */
private static class TestStreams extends RealWebSocket.Streams {
private final String name;
private final WebSocketRecorder listener;
private RealWebSocket webSocket;
boolean closeThrows;
boolean closed;
public TestStreams(boolean client, Pipe source, Pipe sink) {
super(client, Okio.buffer(source.source()), Okio.buffer(sink.sink()));
this.name = client ? "client" : "server";
this.listener = new WebSocketRecorder(name);
}
public void initWebSocket(Random random, int pingIntervalMillis) throws IOException {
String url = "http://example.com/websocket";
Response response = new Response.Builder()
.code(101)
.message("OK")
.request(new Request.Builder().url(url).build())
.protocol(Protocol.HTTP_1_1)
.build();
webSocket = new RealWebSocket(response.request(), listener, random);
webSocket.initReaderAndWriter(name, pingIntervalMillis, this);
}
public boolean processNextFrame() throws IOException {
return webSocket.processNextFrame();
}
@Override public void close() throws IOException {
source.close();
sink.close();
if (closed) {
throw new AssertionError("Already closed");
}
closed = true;
if (closeThrows) {
throw new RuntimeException("Oops!");
}
}
}
}