/* Copyright (c) 2016, Isode Limited, London, England. * All rights reserved. * * Acquisition and use of this software and related materials for any * purpose requires a written license agreement from Isode Limited, * or a written license from an organisation licensed by Isode Limited * to grant such a license. * */ package com.isode.stroke.network; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import java.util.ArrayList; import java.util.Collection; import java.util.List; import org.junit.Before; import org.junit.Test; import com.isode.stroke.base.ByteArray; import com.isode.stroke.base.SafeByteArray; import com.isode.stroke.base.SafeString; import com.isode.stroke.base.URL; import com.isode.stroke.eventloop.DummyEventLoop; import com.isode.stroke.eventloop.Event.Callback; import com.isode.stroke.eventloop.EventLoop; import com.isode.stroke.network.BOSHConnection.BOSHError; import com.isode.stroke.parser.PlatformXMLParserFactory; import com.isode.stroke.signals.Slot; import com.isode.stroke.signals.Slot1; import com.isode.stroke.tls.TLSOptions; /** * Tests for {@link BOSHConnectionPool} */ public class BOSHConnectionPoolTest { private DummyEventLoop eventLoop = new DummyEventLoop(); private MockConnectionFactory connectionFactory = new MockConnectionFactory(eventLoop); private List xmppDataRead = new ArrayList(); private List boshDataRead = new ArrayList(); private List boshDataWritten = new ArrayList(); private PlatformXMLParserFactory parserFactory = new PlatformXMLParserFactory(); private StaticDomainNameResolver resolver = new StaticDomainNameResolver(eventLoop); private TimerFactory timerFactory = new DummyTimerFactory(); private String to = "wonderland.lit"; private String path = "/http-bind"; private String port = "5280"; private String sid = "MyShinySID"; private String initial = ""; private URL boshURL = new URL("http", to, 5280, path); private long initialRID = 2349876; private int sessionStarted = 0; private int sessionTerminated = 0; @Before public void setUp() { resolver.addAddress(to, new HostAddress("127.0.0.1")); } @Test public void testConnectionCount_OneWrite() { BOSHConnectionPool testling = createTestling(); assertEquals(1,connectionFactory.connections.size()); eventLoop.processEvents(); assertEquals(0,sessionStarted); readResponse(initial, connectionFactory.connections.get(0)); assertEquals(1,sessionStarted); assertEquals(1,connectionFactory.connections.size()); testling.write(new SafeByteArray("")); eventLoop.processEvents(); assertEquals(1,connectionFactory.connections.size()); assertEquals(1,sessionStarted); } @Test public void testConnectionCount_TwoWrites() { BOSHConnectionPool testling = createTestling(); assertEquals(1,connectionFactory.connections.size()); eventLoop.processEvents(); readResponse(initial, connectionFactory.connections.get(0)); eventLoop.processEvents(); testling.write(new SafeByteArray("")); eventLoop.processEvents(); assertEquals(1,connectionFactory.connections.size()); testling.write(new SafeByteArray("")); eventLoop.processEvents(); eventLoop.processEvents(); assertEquals(2,connectionFactory.connections.size()); } @Test public void testConnectionCount_ThreeWrites() { BOSHConnectionPool testling = createTestling(); assertEquals(1,connectionFactory.connections.size()); eventLoop.processEvents(); readResponse(initial,connectionFactory.connections.get(0)); testling.restartStream(); readResponse("",connectionFactory.connections.get(0)); testling.restartStream(); readResponse("",connectionFactory.connections.get(0)); testling.write(new SafeByteArray("")); testling.write(new SafeByteArray("")); testling.write(new SafeByteArray("")); eventLoop.processEvents(); assertTrue("2 < "+connectionFactory.connections.size(), 2 >= connectionFactory.connections.size()); } @Test public void testConnectionCount_ThreeWrites_ManualConnect() { connectionFactory.autoFinishConnect = false; BOSHConnectionPool testling = createTestling(); assertEquals(1,connectionFactory.connections.size()); assertEquals(0,boshDataWritten.size()); // Connection not connected yet, can't send data connectionFactory.connections.get(0).onConnectFinished.emit(false); eventLoop.processEvents(); assertEquals(1,boshDataWritten.size()); readResponse(initial, connectionFactory.connections.get(0)); eventLoop.processEvents(); assertEquals(1,connectionFactory.connections.size()); assertEquals(1,boshDataWritten.size()); // Don't respond to initial data with a holding call testling.restartStream();; eventLoop.processEvents(); readResponse("", connectionFactory.connections.get(0)); eventLoop.processEvents(); testling.restartStream(); eventLoop.processEvents(); testling.write(new SafeByteArray("")); eventLoop.processEvents(); assertEquals(2,connectionFactory.connections.size()); assertEquals(3,boshDataWritten.size()); // New connection isn't up yet connectionFactory.connections.get(1).onConnectFinished.emit(false); eventLoop.processEvents(); assertEquals(4,boshDataWritten.size()); // New Connection ready testling.write(new SafeByteArray("")); eventLoop.processEvents(); testling.write(new SafeByteArray("")); assertEquals(4,boshDataWritten.size()); // New data can't be sent, no free connections eventLoop.processEvents(); assertTrue("2 < "+connectionFactory.connections.size(), 2 >= connectionFactory.connections.size()); } @Test public void testConnectionCount_ThreeWritesTwoReads() { MockConnection c0 = null; MockConnection c1 = null; long rid = initialRID; BOSHConnectionPool testling = createTestling(); assertEquals(1,connectionFactory.connections.size()); c0 = connectionFactory.connections.get(0); eventLoop.processEvents(); assertEquals(1,boshDataWritten.size()); // header rid++; readResponse(initial, c0); assertEquals(1,boshDataWritten.size()); assertEquals(1,connectionFactory.connections.size()); assertFalse(c0.pending); rid++; testling.restartStream(); eventLoop.processEvents(); readResponse("", connectionFactory.connections.get(0)); rid++; testling.write(new SafeByteArray("")); eventLoop.processEvents(); assertEquals(2,connectionFactory.connections.size()); // 0 was waiting for response, open and send on 1 assertEquals(4,boshDataWritten.size()); // data c1 = connectionFactory.connections.get(1); String fullBody = ""; // Check empty write assertEquals(fullBody,lastBody()); assertTrue(c0.pending); assertTrue(c1.pending); rid++; readResponse("", c0); // Doesn't include necessary attributes - as the support is improved this'll start to fail eventLoop.processEvents(); assertFalse(c0.pending); assertTrue(c1.pending); assertEquals(4,boshDataWritten.size()); // don't send empty in [0], still have [1] waiting assertEquals(2,connectionFactory.connections.size()); rid++; readResponse("", c1); eventLoop.processEvents(); assertFalse(c1.pending); assertTrue(c0.pending); assertEquals(5,boshDataWritten.size()); // Empty to make room assertEquals(2,connectionFactory.connections.size()); rid++; testling.write(new SafeByteArray("")); eventLoop.processEvents(); assertTrue(c0.pending); assertTrue(c1.pending); assertEquals(6,boshDataWritten.size()); rid++; testling.write(new SafeByteArray("")); assertTrue(c0.pending); assertTrue(c1.pending); assertEquals(6,boshDataWritten.size()); //Don't send data, no room eventLoop.processEvents(); assertEquals(2,connectionFactory.connections.size()); } @Test public void testSession() { to = "prosody.doomsong.co.uk"; resolver.addAddress("prosody.doomsong.co.uk",new HostAddress("127.0.0.1")); path = "/http-bind/"; boshURL = new URL("http", to, 5280, path); BOSHConnectionPool testling = createTestling(); assertEquals(1,connectionFactory.connections.size()); eventLoop.processEvents(); assertEquals(1,boshDataWritten.size()); // header assertEquals(1,connectionFactory.connections.size()); String response = "SCRAM-SHA-1DIGEST-MD5"; readResponse(response, connectionFactory.connections.get(0)); eventLoop.processEvents(); assertEquals(1,boshDataWritten.size()); assertEquals(1,connectionFactory.connections.size()); String send = "biwsbj1hZG1pbixyPWZhOWE5ZDhiLWZmMDctNGE4Yy04N2E3LTg4YWRiNDQxZGUwYg=="; testling.write(new SafeByteArray(send)); eventLoop.processEvents(); assertEquals(2,boshDataWritten.size()); assertEquals(1,connectionFactory.connections.size()); response = "cj1mYTlhOWQ4Yi1mZjA3LTRhOGMtODdhNy04OGFkYjQ0MWRlMGJhZmZlMWNhMy1mMDJkLTQ5NzEtYjkyNS0yM2NlNWQ2MDQyMjYscz1OVGd5WkdWaFptTXRaVE15WXkwMFpXUmhMV0ZqTURRdFpqYzRNbUppWmpGa1pqWXgsaT00MDk2"; readResponse(response, connectionFactory.connections.get(0)); eventLoop.processEvents(); assertEquals(2,boshDataWritten.size()); assertEquals(1,connectionFactory.connections.size()); send = "Yz1iaXdzLHI9ZmE5YTlkOGItZmYwNy00YThjLTg3YTctODhhZGI0NDFkZTBiYWZmZTFjYTMtZjAyZC00OTcxLWI5MjUtMjNjZTVkNjA0MjI2LHA9aU11NWt3dDN2VWplU2RqL01Jb3VIRldkZjBnPQ=="; testling.write(new SafeByteArray(send)); eventLoop.processEvents(); assertEquals(3,boshDataWritten.size()); assertEquals(1,connectionFactory.connections.size()); response = "dj1YNmNBY3BBOWxHNjNOOXF2bVQ5S0FacERrVm89"; readResponse(response, connectionFactory.connections.get(0)); eventLoop.processEvents(); assertEquals(3,boshDataWritten.size()); assertEquals(1,connectionFactory.connections.size()); testling.restartStream(); eventLoop.processEvents(); assertEquals(4,boshDataWritten.size()); assertEquals(1,connectionFactory.connections.size()); response = ""; readResponse(response, connectionFactory.connections.get(0)); eventLoop.processEvents(); assertEquals(5,boshDataWritten.size()); // Now we've authed (restarted) we should be keeping one query in flight so the server can reply to us at any time it wants. assertEquals(1,connectionFactory.connections.size()); send = "d5a9744036cd20a0"; testling.write(new SafeByteArray(send)); eventLoop.processEvents(); assertEquals(6,boshDataWritten.size()); assertEquals(2,connectionFactory.connections.size()); } @Test public void testWrite_Empty() { MockConnection c0 = null; BOSHConnectionPool testling = createTestling(); assertEquals(1,connectionFactory.connections.size()); c0 = connectionFactory.connections.get(0); readResponse(initial, c0); eventLoop.processEvents(); assertEquals(1,boshDataWritten.size()); // Shouldn't have sent anything extra eventLoop.processEvents(); testling.restartStream(); eventLoop.processEvents(); assertEquals(2,boshDataWritten.size()); readResponse("",c0); eventLoop.processEvents(); assertEquals(3,boshDataWritten.size()); String fullBody = ""; String response = boshDataWritten.get(2); int bodyPosition = response.indexOf("\r\n\r\n"); assertEquals(fullBody,response.substring(bodyPosition+4)); } private static class MockConnection extends Connection { private final EventLoop eventLoop; private HostAddressPort hostAddressPort; private final List failingPorts; private final ByteArray dataWritten = new ByteArray(); private boolean disconnected; private boolean pending; private boolean autoFinishConnect; private MockConnection(Collection failingPorts, EventLoop eventLoop,boolean autoFinishConnect) { this.eventLoop = eventLoop; this.failingPorts = new ArrayList(failingPorts); disconnected = false; pending = false; this.autoFinishConnect = autoFinishConnect; } @Override public void listen() { fail(); } @Override public void connect(HostAddressPort address) { hostAddressPort = address; final boolean fail = failingPorts.contains(address); if (autoFinishConnect) { eventLoop.postEvent(new Callback() { @Override public void run() { onConnectFinished.emit(fail); } }); } } @Override public void disconnect() { disconnected = true; onDisconnected.emit(null); } @Override public void write(SafeByteArray data) { dataWritten.append(data); pending = true; } @Override public HostAddressPort getLocalAddress() { return new HostAddressPort(); } public HostAddressPort getRemoteAddress() { return new HostAddressPort(); } } private static class MockConnectionFactory implements ConnectionFactory { private final EventLoop eventLoop; private List connections = new ArrayList(); private List failingPorts = new ArrayList(); private boolean autoFinishConnect; private MockConnectionFactory(EventLoop eventLoop) { this(eventLoop,true); } private MockConnectionFactory(EventLoop eventLoop,boolean autoFinishConnect) { this.eventLoop = eventLoop; this.autoFinishConnect = autoFinishConnect; } @Override public Connection createConnection() { MockConnection connection = new MockConnection(failingPorts, eventLoop, autoFinishConnect); connections.add(connection); return connection; } } private BOSHConnectionPool createTestling() { // make_shared is limited to 9 arguments; instead new is used here. BOSHConnectionPool pool = new BOSHConnectionPool(boshURL, resolver, connectionFactory, parserFactory, null, timerFactory, eventLoop, to, initialRID, new URL(), new SafeString(""), new SafeString(""), new TLSOptions()); pool.open(); pool.onXMPPDataRead.connect(new Slot1() { @Override public void call(SafeByteArray data) { handleXMPPDataRead(data); } }); pool.onBOSHDataRead.connect(new Slot1() { @Override public void call(SafeByteArray data) { handleBOSHDataRead(data); } }); pool.onBOSHDataWritten.connect(new Slot1() { @Override public void call(SafeByteArray data) { handleBOSHDataWritten(data); } }); pool.onSessionStarted.connect(new Slot() { @Override public void call() { handleSessionStarted(); } }); pool.onSessionTerminated.connect(new Slot1() { @Override public void call(BOSHError error) { handleSessionTerminated(); } }); eventLoop.processEvents(); eventLoop.processEvents(); return pool; } private String lastBody() { String response = boshDataWritten.get(boshDataWritten.size() - 1); int bodyPosition = response.indexOf("\r\n\r\n"); return response.substring(bodyPosition+4); } private void handleXMPPDataRead(SafeByteArray d) { xmppDataRead.add(d.toString()); } private void handleBOSHDataRead(SafeByteArray d) { boshDataRead.add(d.toString()); } private void handleBOSHDataWritten(SafeByteArray d) { boshDataWritten.add(d.toString()); } private void handleSessionStarted() { sessionStarted++; } private void handleSessionTerminated() { sessionTerminated++; } private void readResponse(String response, MockConnection connection) { connection.pending = false; SafeByteArray data1 = new SafeByteArray( "HTTP/1.1 200 OK\r\n" +"Content-Type: text/xml; charset=utf-8\r\n" +"Access-Control-Allow-Origin: *\r\n" +"Access-Control-Allow-Headers: Content-Type\r\n" +"Content-Length: "); connection.onDataRead.emit(data1); SafeByteArray data2 = new SafeByteArray(String.valueOf(response.length())); connection.onDataRead.emit(data2); SafeByteArray data3 = new SafeByteArray("\r\n\r\n"); connection.onDataRead.emit(data3); SafeByteArray data4 = new SafeByteArray(response); connection.onDataRead.emit(data4); } private String fullRequestFor(String data) { String result = "POST /" + path + " HTTP/1.1\r\n" + "Host: " + to + ":" + port + "\r\n" + "Content-Type: text/xml; charset=utf-8\r\n" + "Content-Length: " + data.length() + "\r\n\r\n" + data; return result; } }