From 9345b5c71b459ce3004bd450d2e83572b42cfe6a Mon Sep 17 00:00:00 2001
From: akwizgran <akwizgran@users.sourceforge.net>
Date: Thu, 24 Nov 2011 22:09:04 +0000
Subject: [PATCH] Avoid DB lookups where possible.

---
 .../event/RemoteTransportsUpdatedEvent.java   |  12 +-
 .../sf/briar/db/DatabaseComponentImpl.java    |   2 +-
 .../transport/ConnectionRecogniserImpl.java   |  68 +--
 .../ConnectionRecogniserImplTest.java         | 530 +++++++++++++++---
 4 files changed, 489 insertions(+), 123 deletions(-)

diff --git a/api/net/sf/briar/api/db/event/RemoteTransportsUpdatedEvent.java b/api/net/sf/briar/api/db/event/RemoteTransportsUpdatedEvent.java
index 8418d23595..501eb886d3 100644
--- a/api/net/sf/briar/api/db/event/RemoteTransportsUpdatedEvent.java
+++ b/api/net/sf/briar/api/db/event/RemoteTransportsUpdatedEvent.java
@@ -1,17 +1,27 @@
 package net.sf.briar.api.db.event;
 
+import java.util.Collection;
+
 import net.sf.briar.api.ContactId;
+import net.sf.briar.api.protocol.Transport;
 
 /** An event that is broadcast when a contact's transports are updated. */
 public class RemoteTransportsUpdatedEvent extends DatabaseEvent {
 
 	private final ContactId contactId;
+	private final Collection<Transport> transports;
 
-	public RemoteTransportsUpdatedEvent(ContactId contactId) {
+	public RemoteTransportsUpdatedEvent(ContactId contactId,
+			Collection<Transport> transports) {
 		this.contactId = contactId;
+		this.transports = transports;
 	}
 
 	public ContactId getContactId() {
 		return contactId;
 	}
+
+	public Collection<Transport> getTransports() {
+		return transports;
+	}
 }
diff --git a/components/net/sf/briar/db/DatabaseComponentImpl.java b/components/net/sf/briar/db/DatabaseComponentImpl.java
index 56b9f82532..03f2f8a74b 100644
--- a/components/net/sf/briar/db/DatabaseComponentImpl.java
+++ b/components/net/sf/briar/db/DatabaseComponentImpl.java
@@ -1226,7 +1226,7 @@ DatabaseCleaner.Callback {
 			contactLock.readLock().unlock();
 		}
 		// Call the listeners outside the lock
-		callListeners(new RemoteTransportsUpdatedEvent(c));
+		callListeners(new RemoteTransportsUpdatedEvent(c, t.getTransports()));
 	}
 
 	public void removeContact(ContactId c) throws DbException {
diff --git a/components/net/sf/briar/transport/ConnectionRecogniserImpl.java b/components/net/sf/briar/transport/ConnectionRecogniserImpl.java
index 6be76b664a..744333eca7 100644
--- a/components/net/sf/briar/transport/ConnectionRecogniserImpl.java
+++ b/components/net/sf/briar/transport/ConnectionRecogniserImpl.java
@@ -51,6 +51,7 @@ DatabaseListener {
 	private final DatabaseComponent db;
 	private final Executor executor;
 	private final Cipher ivCipher; // Locking: this
+	private final Set<TransportId> localTransportIds; // Locking: this
 	private final Map<Bytes, Context> expected; // Locking: this
 
 	private boolean initialised = false; // Locking: this
@@ -62,9 +63,15 @@ DatabaseListener {
 		this.db = db;
 		this.executor = executor;
 		ivCipher = crypto.getIvCipher();
+		localTransportIds = new HashSet<TransportId>();
 		expected = new HashMap<Bytes, Context>();
 	}
 
+	// Package access for testing
+	synchronized boolean isInitialised() {
+		return initialised;
+	}
+
 	// Locking: this
 	private void initialise() throws DbException {
 		assert !initialised;
@@ -76,7 +83,7 @@ DatabaseListener {
 			try {
 				for(TransportId t : transports) {
 					TransportIndex i = db.getRemoteIndex(c, t);
-					if(i == null) continue;
+					if(i == null) continue; // Contact doesn't support transport
 					ConnectionWindow w = db.getConnectionWindow(c, i);
 					for(Entry<Long, byte[]> e : w.getUnseen().entrySet()) {
 						Context ctx = new Context(c, t, i, e.getKey());
@@ -89,6 +96,7 @@ DatabaseListener {
 				continue;
 			}
 		}
+		localTransportIds.addAll(transports);
 		expected.putAll(ivs);
 		initialised = true;
 	}
@@ -100,7 +108,8 @@ DatabaseListener {
 		ErasableKey ivKey = crypto.deriveIvKey(secret, true);
 		try {
 			ivCipher.init(Cipher.ENCRYPT_MODE, ivKey);
-			return new Bytes(ivCipher.doFinal(iv));
+			byte[] encryptedIv = ivCipher.doFinal(iv);
+			return new Bytes(encryptedIv);
 		} catch(BadPaddingException badCipher) {
 			throw new RuntimeException(badCipher);
 		} catch(IllegalBlockSizeException badCipher) {
@@ -127,8 +136,9 @@ DatabaseListener {
 		});
 	}
 
-	private ConnectionContext acceptConnection(TransportId t,
-			byte[] encryptedIv) throws DbException {
+	// Package access for testing
+	ConnectionContext acceptConnection(TransportId t, byte[] encryptedIv)
+	throws DbException {
 		if(encryptedIv.length != IV_LENGTH)
 			throw new IllegalArgumentException();
 		synchronized(this) {
@@ -189,11 +199,12 @@ DatabaseListener {
 			});
 		} else if(e instanceof RemoteTransportsUpdatedEvent) {
 			// Update the expected IVs for the contact
-			final ContactId c =
-				((RemoteTransportsUpdatedEvent) e).getContactId();
+			RemoteTransportsUpdatedEvent r = (RemoteTransportsUpdatedEvent) e;
+			final ContactId c = r.getContactId();
+			final Collection<Transport> transports = r.getTransports();
 			executor.execute(new Runnable() {
 				public void run() {
-					updateContact(c);
+					updateContact(c, transports);
 				}
 			});
 		}
@@ -212,7 +223,7 @@ DatabaseListener {
 			for(ContactId c : db.getContacts()) {
 				try {
 					TransportIndex i = db.getRemoteIndex(c, t);
-					if(i == null) continue;
+					if(i == null) continue; // Contact doesn't support transport
 					ConnectionWindow w = db.getConnectionWindow(c, i);
 					for(Entry<Long, byte[]> e : w.getUnseen().entrySet()) {
 						Context ctx = new Context(c, t, i, e.getKey());
@@ -228,33 +239,26 @@ DatabaseListener {
 			if(LOG.isLoggable(Level.WARNING)) LOG.warning(e.getMessage());
 			return;
 		}
+		localTransportIds.add(t);
 		expected.putAll(ivs);
 	}
 
-	private synchronized void updateContact(ContactId c) {
+	private synchronized void updateContact(ContactId c,
+			Collection<Transport> transports) {
 		if(!initialised) return;
-		// Don't recalculate IVs for transports that are already known
-		Set<TransportIndex> known = new HashSet<TransportIndex>();
-		for(Context ctx : expected.values()) {
-			if(ctx.contactId.equals(c)) known.add(ctx.transportIndex);
-		}
-		Set<TransportIndex> current = new HashSet<TransportIndex>();
+		// The ID <-> index mappings may have changed, so recalculate everything
 		Map<Bytes, Context> ivs = new HashMap<Bytes, Context>();
 		try {
-			for(Transport transport : db.getLocalTransports()) {
+			for(Transport transport: transports) {
 				TransportId t = transport.getId();
-				TransportIndex i = db.getRemoteIndex(c, t);
-				if(i == null) continue;
-				current.add(i);
-				// If the transport is not already known, calculate the IVs
-				if(!known.contains(i)) {
-					ConnectionWindow w = db.getConnectionWindow(c, i);
-					for(Entry<Long, byte[]> e : w.getUnseen().entrySet()) {
-						Context ctx = new Context(c, t, i, e.getKey());
-						ivs.put(calculateIv(ctx, e.getValue()), ctx);
-					}
-					w.erase();
+				if(!localTransportIds.contains(t)) continue;
+				TransportIndex i = transport.getIndex();
+				ConnectionWindow w = db.getConnectionWindow(c, i);
+				for(Entry<Long, byte[]> e : w.getUnseen().entrySet()) {
+					Context ctx = new Context(c, t, i, e.getKey());
+					ivs.put(calculateIv(ctx, e.getValue()), ctx);
 				}
+				w.erase();
 			}
 		} catch(NoSuchContactException e) {
 			// The contact was removed - clean up in removeContact()
@@ -263,14 +267,10 @@ DatabaseListener {
 			if(LOG.isLoggable(Level.WARNING)) LOG.warning(e.getMessage());
 			return;
 		}
-		// Remove any IVs that are no longer current
+		// Remove the old IVs
 		Iterator<Context> it = expected.values().iterator();
-		while(it.hasNext()) {
-			Context ctx = it.next();
-			if(ctx.contactId.equals(c) && !current.contains(ctx.transportIndex))
-				it.remove();
-		}
-		// Add any IVs that were not previously known
+		while(it.hasNext()) if(it.next().contactId.equals(c)) it.remove();
+		// Store the new IVs
 		expected.putAll(ivs);
 	}
 
diff --git a/test/net/sf/briar/transport/ConnectionRecogniserImplTest.java b/test/net/sf/briar/transport/ConnectionRecogniserImplTest.java
index b7ae59a940..efd5cf0337 100644
--- a/test/net/sf/briar/transport/ConnectionRecogniserImplTest.java
+++ b/test/net/sf/briar/transport/ConnectionRecogniserImplTest.java
@@ -2,6 +2,7 @@ package net.sf.briar.transport;
 
 import static net.sf.briar.api.transport.TransportConstants.IV_LENGTH;
 
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Map;
@@ -16,13 +17,13 @@ import net.sf.briar.api.ContactId;
 import net.sf.briar.api.crypto.CryptoComponent;
 import net.sf.briar.api.crypto.ErasableKey;
 import net.sf.briar.api.db.DatabaseComponent;
-import net.sf.briar.api.db.DbException;
+import net.sf.briar.api.db.event.ContactRemovedEvent;
+import net.sf.briar.api.db.event.RemoteTransportsUpdatedEvent;
+import net.sf.briar.api.db.event.TransportAddedEvent;
 import net.sf.briar.api.protocol.Transport;
 import net.sf.briar.api.protocol.TransportId;
 import net.sf.briar.api.protocol.TransportIndex;
 import net.sf.briar.api.transport.ConnectionContext;
-import net.sf.briar.api.transport.ConnectionRecogniser;
-import net.sf.briar.api.transport.ConnectionRecogniser.Callback;
 import net.sf.briar.api.transport.ConnectionWindow;
 import net.sf.briar.crypto.CryptoModule;
 import net.sf.briar.plugins.ImmediateExecutor;
@@ -41,8 +42,7 @@ public class ConnectionRecogniserImplTest extends TestCase {
 	private final byte[] inSecret;
 	private final TransportId transportId;
 	private final TransportIndex localIndex, remoteIndex;
-	private final Collection<Transport> transports;
-	private final ConnectionWindow connectionWindow;
+	private final Collection<Transport> localTransports, remoteTransports;
 
 	public ConnectionRecogniserImplTest() {
 		super();
@@ -54,140 +54,496 @@ public class ConnectionRecogniserImplTest extends TestCase {
 		transportId = new TransportId(TestUtils.getRandomId());
 		localIndex = new TransportIndex(13);
 		remoteIndex = new TransportIndex(7);
-		Transport transport = new Transport(transportId, localIndex,
-				Collections.singletonMap("foo", "bar"));
-		transports = Collections.singletonList(transport);
-		connectionWindow = new ConnectionWindowImpl(crypto, remoteIndex,
-				inSecret);
+		Map<String, String> properties = Collections.singletonMap("foo", "bar");
+		Transport localTransport = new Transport(transportId, localIndex,
+				properties);
+		localTransports = Collections.singletonList(localTransport);
+		Transport remoteTransport = new Transport(transportId, remoteIndex,
+				properties);
+		remoteTransports = Collections.singletonList(remoteTransport);
 	}
 
 	@Test
 	public void testUnexpectedIv() throws Exception {
+		final ConnectionWindow window = createConnectionWindow(remoteIndex);
 		Mockery context = new Mockery();
 		final DatabaseComponent db = context.mock(DatabaseComponent.class);
 		context.checking(new Expectations() {{
-			oneOf(db).addListener(with(any(ConnectionRecogniserImpl.class)));
 			// Initialise
+			oneOf(db).addListener(with(any(ConnectionRecogniserImpl.class)));
 			oneOf(db).getLocalTransports();
-			will(returnValue(transports));
+			will(returnValue(localTransports));
 			oneOf(db).getContacts();
 			will(returnValue(Collections.singletonList(contactId)));
 			oneOf(db).getRemoteIndex(contactId, transportId);
 			will(returnValue(remoteIndex));
 			oneOf(db).getConnectionWindow(contactId, remoteIndex);
-			will(returnValue(connectionWindow));
+			will(returnValue(window));
 		}});
 		Executor executor = new ImmediateExecutor();
-		ConnectionRecogniser c = new ConnectionRecogniserImpl(crypto, db,
+		ConnectionRecogniserImpl c = new ConnectionRecogniserImpl(crypto, db,
 				executor);
-		c.acceptConnection(transportId, new byte[IV_LENGTH], new Callback() {
+		assertNull(c.acceptConnection(transportId, new byte[IV_LENGTH]));
+		context.assertIsSatisfied();
+	}
 
-			public void connectionAccepted(ConnectionContext ctx) {
-				fail();
-			}
+	@Test
+	public void testExpectedIv() throws Exception {
+		final ConnectionWindow window = createConnectionWindow(remoteIndex);
+		Mockery context = new Mockery();
+		final DatabaseComponent db = context.mock(DatabaseComponent.class);
+		context.checking(new Expectations() {{
+			// Initialise
+			oneOf(db).addListener(with(any(ConnectionRecogniserImpl.class)));
+			oneOf(db).getLocalTransports();
+			will(returnValue(localTransports));
+			oneOf(db).getContacts();
+			will(returnValue(Collections.singletonList(contactId)));
+			oneOf(db).getRemoteIndex(contactId, transportId);
+			will(returnValue(remoteIndex));
+			oneOf(db).getConnectionWindow(contactId, remoteIndex);
+			will(returnValue(window));
+			// Update the window
+			oneOf(db).getConnectionWindow(contactId, remoteIndex);
+			will(returnValue(window));
+			oneOf(db).setConnectionWindow(contactId, remoteIndex, window);
+		}});
+		Executor executor = new ImmediateExecutor();
+		ConnectionRecogniserImpl c = new ConnectionRecogniserImpl(crypto, db,
+				executor);
+		byte[] encryptedIv = calculateIv();
+		// The IV should not be expected by the wrong transport
+		TransportId wrong = new TransportId(TestUtils.getRandomId());
+		assertNull(c.acceptConnection(wrong, encryptedIv));
+		// The IV should be expected by the right transport
+		ConnectionContext ctx = c.acceptConnection(transportId, encryptedIv);
+		assertNotNull(ctx);
+		assertEquals(contactId, ctx.getContactId());
+		assertEquals(remoteIndex, ctx.getTransportIndex());
+		assertEquals(3, ctx.getConnectionNumber());
+		// The IV should no longer be expected
+		assertNull(c.acceptConnection(transportId, encryptedIv));
+		// The window should have advanced
+		Map<Long, byte[]> unseen = window.getUnseen();
+		assertEquals(19, unseen.size());
+		for(int i = 0; i < 19; i++) {
+			assertEquals(i != 3, unseen.containsKey(Long.valueOf(i)));
+		}
+		context.assertIsSatisfied();
+	}
 
-			public void connectionRejected() {
-				// Expected
-			}
+	@Test
+	public void testContactRemovedAfterInit() throws Exception {
+		final ConnectionWindow window = createConnectionWindow(remoteIndex);
+		Mockery context = new Mockery();
+		final DatabaseComponent db = context.mock(DatabaseComponent.class);
+		context.checking(new Expectations() {{
+			// Initialise before removing contact
+			oneOf(db).addListener(with(any(ConnectionRecogniserImpl.class)));
+			oneOf(db).getLocalTransports();
+			will(returnValue(localTransports));
+			oneOf(db).getContacts();
+			will(returnValue(Collections.singletonList(contactId)));
+			oneOf(db).getRemoteIndex(contactId, transportId);
+			will(returnValue(remoteIndex));
+			oneOf(db).getConnectionWindow(contactId, remoteIndex);
+			will(returnValue(window));
+		}});
+		Executor executor = new ImmediateExecutor();
+		ConnectionRecogniserImpl c = new ConnectionRecogniserImpl(crypto, db,
+				executor);
+		byte[] encryptedIv = calculateIv();
+		// Ensure the recogniser is initialised
+		assertFalse(c.isInitialised());
+		assertNull(c.acceptConnection(transportId, new byte[IV_LENGTH]));
+		assertTrue(c.isInitialised());
+		// Remove the contact
+		c.eventOccurred(new ContactRemovedEvent(contactId));
+		// The IV should not be expected
+		assertNull(c.acceptConnection(transportId, encryptedIv));
+		context.assertIsSatisfied();
+	}
 
-			public void handleException(DbException e) {
-				fail();
-			}
-		});
+	@Test
+	public void testContactRemovedBeforeInit() throws Exception {
+		Mockery context = new Mockery();
+		final DatabaseComponent db = context.mock(DatabaseComponent.class);
+		context.checking(new Expectations() {{
+			// Initialise after removing contact
+			oneOf(db).addListener(with(any(ConnectionRecogniserImpl.class)));
+			oneOf(db).getLocalTransports();
+			will(returnValue(localTransports));
+			oneOf(db).getContacts();
+			will(returnValue(Collections.emptyList()));
+		}});
+		Executor executor = new ImmediateExecutor();
+		ConnectionRecogniserImpl c = new ConnectionRecogniserImpl(crypto, db,
+				executor);
+		byte[] encryptedIv = calculateIv();
+		// Remove the contact
+		c.eventOccurred(new ContactRemovedEvent(contactId));
+		// The IV should not be expected
+		assertFalse(c.isInitialised());
+		assertNull(c.acceptConnection(transportId, encryptedIv));
+		assertTrue(c.isInitialised());
 		context.assertIsSatisfied();
 	}
 
 	@Test
-	public void testExpectedIv() throws Exception {
-		// Calculate the shared secret for connection number 3
-		byte[] secret = inSecret;
-		for(int i = 0; i < 4; i++) {
-			secret = crypto.deriveNextSecret(secret, remoteIndex.getInt(), i);
+	public void testLocalTransportAddedAfterInit() throws Exception {
+		final ConnectionWindow window = createConnectionWindow(remoteIndex);
+		Mockery context = new Mockery();
+		final DatabaseComponent db = context.mock(DatabaseComponent.class);
+		context.checking(new Expectations() {{
+			// Initialise before adding transport
+			oneOf(db).addListener(with(any(ConnectionRecogniserImpl.class)));
+			oneOf(db).getLocalTransports();
+			will(returnValue(Collections.emptyList()));
+			oneOf(db).getContacts();
+			will(returnValue(Collections.singletonList(contactId)));
+			// Add the transport
+			oneOf(db).getContacts();
+			will(returnValue(Collections.singletonList(contactId)));
+			oneOf(db).getRemoteIndex(contactId, transportId);
+			will(returnValue(remoteIndex));
+			oneOf(db).getConnectionWindow(contactId, remoteIndex);
+			will(returnValue(window));
+			// Update the window
+			oneOf(db).getConnectionWindow(contactId, remoteIndex);
+			will(returnValue(window));
+			oneOf(db).setConnectionWindow(contactId, remoteIndex, window);
+		}});
+		Executor executor = new ImmediateExecutor();
+		ConnectionRecogniserImpl c = new ConnectionRecogniserImpl(crypto, db,
+				executor);
+		byte[] encryptedIv = calculateIv();
+		// The IV should not be expected
+		assertFalse(c.isInitialised());
+		assertNull(c.acceptConnection(transportId, encryptedIv));
+		assertTrue(c.isInitialised());
+		// Add the transport
+		c.eventOccurred(new TransportAddedEvent(transportId));
+		// The IV should be expected
+		ConnectionContext ctx = c.acceptConnection(transportId, encryptedIv);
+		assertNotNull(ctx);
+		assertEquals(contactId, ctx.getContactId());
+		assertEquals(remoteIndex, ctx.getTransportIndex());
+		assertEquals(3, ctx.getConnectionNumber());
+		// The IV should no longer be expected
+		assertNull(c.acceptConnection(transportId, encryptedIv));
+		// The window should have advanced
+		Map<Long, byte[]> unseen = window.getUnseen();
+		assertEquals(19, unseen.size());
+		for(int i = 0; i < 19; i++) {
+			assertEquals(i != 3, unseen.containsKey(Long.valueOf(i)));
 		}
-		// Calculate the expected IV for connection number 3
-		ErasableKey ivKey = crypto.deriveIvKey(secret, true);
-		Cipher ivCipher = crypto.getIvCipher();
-		ivCipher.init(Cipher.ENCRYPT_MODE, ivKey);
-		byte[] iv = IvEncoder.encodeIv(true, remoteIndex.getInt(), 3);
-		byte[] encryptedIv = ivCipher.doFinal(iv);
+		context.assertIsSatisfied();
+	}
 
+	@Test
+	public void testLocalTransportAddedBeforeInit() throws Exception {
+		final ConnectionWindow window = createConnectionWindow(remoteIndex);
 		Mockery context = new Mockery();
 		final DatabaseComponent db = context.mock(DatabaseComponent.class);
 		context.checking(new Expectations() {{
+			// Initialise after adding transport
 			oneOf(db).addListener(with(any(ConnectionRecogniserImpl.class)));
-			// Initialise
 			oneOf(db).getLocalTransports();
-			will(returnValue(transports));
+			will(returnValue(localTransports));
 			oneOf(db).getContacts();
 			will(returnValue(Collections.singletonList(contactId)));
 			oneOf(db).getRemoteIndex(contactId, transportId);
 			will(returnValue(remoteIndex));
 			oneOf(db).getConnectionWindow(contactId, remoteIndex);
-			will(returnValue(connectionWindow));
+			will(returnValue(window));
 			// Update the window
 			oneOf(db).getConnectionWindow(contactId, remoteIndex);
-			will(returnValue(connectionWindow));
-			oneOf(db).setConnectionWindow(contactId, remoteIndex,
-					connectionWindow);
+			will(returnValue(window));
+			oneOf(db).setConnectionWindow(contactId, remoteIndex, window);
 		}});
 		Executor executor = new ImmediateExecutor();
-		ConnectionRecogniser c = new ConnectionRecogniserImpl(crypto, db,
+		ConnectionRecogniserImpl c = new ConnectionRecogniserImpl(crypto, db,
 				executor);
-		// The IV should not be expected by the wrong transport
-		TransportId wrong = new TransportId(TestUtils.getRandomId());
-		c.acceptConnection(wrong, encryptedIv, new Callback() {
-
-			public void connectionAccepted(ConnectionContext ctx) {
-				fail();
-			}
+		byte[] encryptedIv = calculateIv();
+		// Add the transport
+		c.eventOccurred(new TransportAddedEvent(transportId));
+		// The IV should be expected
+		assertFalse(c.isInitialised());
+		ConnectionContext ctx = c.acceptConnection(transportId, encryptedIv);
+		assertTrue(c.isInitialised());
+		assertNotNull(ctx);
+		assertEquals(contactId, ctx.getContactId());
+		assertEquals(remoteIndex, ctx.getTransportIndex());
+		assertEquals(3, ctx.getConnectionNumber());
+		// The IV should no longer be expected
+		assertNull(c.acceptConnection(transportId, encryptedIv));
+		// The window should have advanced
+		Map<Long, byte[]> unseen = window.getUnseen();
+		assertEquals(19, unseen.size());
+		for(int i = 0; i < 19; i++) {
+			assertEquals(i != 3, unseen.containsKey(Long.valueOf(i)));
+		}
+		context.assertIsSatisfied();
+	}
 
-			public void connectionRejected() {
-				// Expected
-			}
+	@Test
+	public void testRemoteTransportAddedAfterInit() throws Exception {
+		final ConnectionWindow window = createConnectionWindow(remoteIndex);
+		Mockery context = new Mockery();
+		final DatabaseComponent db = context.mock(DatabaseComponent.class);
+		context.checking(new Expectations() {{
+			// Initialise before updating the contact
+			oneOf(db).addListener(with(any(ConnectionRecogniserImpl.class)));
+			oneOf(db).getLocalTransports();
+			will(returnValue(localTransports));
+			oneOf(db).getContacts();
+			will(returnValue(Collections.singletonList(contactId)));
+			oneOf(db).getRemoteIndex(contactId, transportId);
+			will(returnValue(null));
+			// Update the contact
+			oneOf(db).getConnectionWindow(contactId, remoteIndex);
+			will(returnValue(window));
+			// Update the window
+			oneOf(db).getConnectionWindow(contactId, remoteIndex);
+			will(returnValue(window));
+			oneOf(db).setConnectionWindow(contactId, remoteIndex, window);
+		}});
+		Executor executor = new ImmediateExecutor();
+		ConnectionRecogniserImpl c = new ConnectionRecogniserImpl(crypto, db,
+				executor);
+		byte[] encryptedIv = calculateIv();
+		// The IV should not be expected
+		assertFalse(c.isInitialised());
+		assertNull(c.acceptConnection(transportId, encryptedIv));
+		assertTrue(c.isInitialised());
+		// Update the contact
+		c.eventOccurred(new RemoteTransportsUpdatedEvent(contactId,
+				remoteTransports));
+		// The IV should be expected
+		ConnectionContext ctx = c.acceptConnection(transportId, encryptedIv);
+		assertNotNull(ctx);
+		assertEquals(contactId, ctx.getContactId());
+		assertEquals(remoteIndex, ctx.getTransportIndex());
+		assertEquals(3, ctx.getConnectionNumber());
+		// The IV should no longer be expected
+		assertNull(c.acceptConnection(transportId, encryptedIv));
+		// The window should have advanced
+		Map<Long, byte[]> unseen = window.getUnseen();
+		assertEquals(19, unseen.size());
+		for(int i = 0; i < 19; i++) {
+			assertEquals(i != 3, unseen.containsKey(Long.valueOf(i)));
+		}
+		context.assertIsSatisfied();
+	}
 
-			public void handleException(DbException e) {
-				fail();
-			}
-		});
-		// The IV should be expected by the right transport
-		c.acceptConnection(transportId, encryptedIv, new Callback() {
-
-			public void connectionAccepted(ConnectionContext ctx) {
-				assertNotNull(ctx);
-				assertEquals(contactId, ctx.getContactId());
-				assertEquals(remoteIndex, ctx.getTransportIndex());
-				assertEquals(3L, ctx.getConnectionNumber());
-			}
-
-			public void connectionRejected() {
-				fail();
-			}
-
-			public void handleException(DbException e) {
-				fail();
-			}
-		});
+	@Test
+	public void testRemoteTransportAddedBeforeInit() throws Exception {
+		final ConnectionWindow window = createConnectionWindow(remoteIndex);
+		Mockery context = new Mockery();
+		final DatabaseComponent db = context.mock(DatabaseComponent.class);
+		context.checking(new Expectations() {{
+			// Initialise after updating the contact
+			oneOf(db).addListener(with(any(ConnectionRecogniserImpl.class)));
+			oneOf(db).getLocalTransports();
+			will(returnValue(localTransports));
+			oneOf(db).getContacts();
+			will(returnValue(Collections.singletonList(contactId)));
+			oneOf(db).getRemoteIndex(contactId, transportId);
+			will(returnValue(remoteIndex));
+			oneOf(db).getConnectionWindow(contactId, remoteIndex);
+			will(returnValue(window));
+			// Update the window
+			oneOf(db).getConnectionWindow(contactId, remoteIndex);
+			will(returnValue(window));
+			oneOf(db).setConnectionWindow(contactId, remoteIndex, window);
+		}});
+		Executor executor = new ImmediateExecutor();
+		ConnectionRecogniserImpl c = new ConnectionRecogniserImpl(crypto, db,
+				executor);
+		byte[] encryptedIv = calculateIv();
+		// Update the contact
+		c.eventOccurred(new RemoteTransportsUpdatedEvent(contactId,
+				remoteTransports));
+		// The IV should be expected
+		assertFalse(c.isInitialised());
+		ConnectionContext ctx = c.acceptConnection(transportId, encryptedIv);
+		assertTrue(c.isInitialised());
+		assertNotNull(ctx);
+		assertEquals(contactId, ctx.getContactId());
+		assertEquals(remoteIndex, ctx.getTransportIndex());
+		assertEquals(3, ctx.getConnectionNumber());
 		// The IV should no longer be expected
-		c.acceptConnection(transportId, encryptedIv, new Callback() {
+		assertNull(c.acceptConnection(transportId, encryptedIv));
+		// The window should have advanced
+		Map<Long, byte[]> unseen = window.getUnseen();
+		assertEquals(19, unseen.size());
+		for(int i = 0; i < 19; i++) {
+			assertEquals(i != 3, unseen.containsKey(Long.valueOf(i)));
+		}
+		context.assertIsSatisfied();
+	}
+
+	@Test
+	public void testRemoteTransportRemovedAfterInit() throws Exception {
+		final ConnectionWindow window = createConnectionWindow(remoteIndex);
+		Mockery context = new Mockery();
+		final DatabaseComponent db = context.mock(DatabaseComponent.class);
+		context.checking(new Expectations() {{
+			// Initialise before updating the contact
+			oneOf(db).addListener(with(any(ConnectionRecogniserImpl.class)));
+			oneOf(db).getLocalTransports();
+			will(returnValue(localTransports));
+			oneOf(db).getContacts();
+			will(returnValue(Collections.singletonList(contactId)));
+			oneOf(db).getRemoteIndex(contactId, transportId);
+			will(returnValue(remoteIndex));
+			oneOf(db).getConnectionWindow(contactId, remoteIndex);
+			will(returnValue(window));
+		}});
+		Executor executor = new ImmediateExecutor();
+		ConnectionRecogniserImpl c = new ConnectionRecogniserImpl(crypto, db,
+				executor);
+		byte[] encryptedIv = calculateIv();
+		// Ensure the recogniser is initialised
+		assertFalse(c.isInitialised());
+		assertNull(c.acceptConnection(transportId, new byte[IV_LENGTH]));
+		assertTrue(c.isInitialised());
+		// Update the contact
+		c.eventOccurred(new RemoteTransportsUpdatedEvent(contactId,
+				Collections.<Transport>emptyList()));
+		// The IV should not be expected
+		assertNull(c.acceptConnection(transportId, encryptedIv));
+		context.assertIsSatisfied();
+	}
 
-			public void connectionAccepted(ConnectionContext ctx) {
-				fail();
-			}
+	@Test
+	public void testRemoteTransportRemovedBeforeInit() throws Exception {
+		Mockery context = new Mockery();
+		final DatabaseComponent db = context.mock(DatabaseComponent.class);
+		context.checking(new Expectations() {{
+			// Initialise after updating the contact
+			oneOf(db).addListener(with(any(ConnectionRecogniserImpl.class)));
+			oneOf(db).getLocalTransports();
+			will(returnValue(localTransports));
+			oneOf(db).getContacts();
+			will(returnValue(Collections.singletonList(contactId)));
+			oneOf(db).getRemoteIndex(contactId, transportId);
+			will(returnValue(null));
+		}});
+		Executor executor = new ImmediateExecutor();
+		ConnectionRecogniserImpl c = new ConnectionRecogniserImpl(crypto, db,
+				executor);
+		byte[] encryptedIv = calculateIv();
+		// Update the contact
+		c.eventOccurred(new RemoteTransportsUpdatedEvent(contactId,
+				Collections.<Transport>emptyList()));
+		// The IV should not be expected
+		assertFalse(c.isInitialised());
+		assertNull(c.acceptConnection(transportId, encryptedIv));
+		assertTrue(c.isInitialised());
+		context.assertIsSatisfied();
+	}
 
-			public void connectionRejected() {
-				// Expected
-			}
+	@Test
+	public void testRemoteTransportIndexChangedAfterInit() throws Exception {
+		// The contact changes the transport ID <-> index relationships
+		final TransportId transportId1 =
+			new TransportId(TestUtils.getRandomId());
+		final TransportIndex remoteIndex1 = new TransportIndex(11);
+		Map<String, String> properties = Collections.singletonMap("foo", "bar");
+		Transport remoteTransport = new Transport(transportId, remoteIndex1,
+				properties);
+		Transport remoteTransport1 = new Transport(transportId1, remoteIndex,
+				properties);
+		Collection<Transport> remoteTransports1 = Arrays.asList(
+				new Transport[] {remoteTransport, remoteTransport1});
+		// Use two local transports for this test
+		TransportIndex localIndex1 = new TransportIndex(17);
+		Transport localTransport = new Transport(transportId, localIndex,
+				properties);
+		Transport localTransport1 = new Transport(transportId1, localIndex1,
+				properties);
+		final Collection<Transport> localTransports1 = Arrays.asList(
+				new Transport[] {localTransport, localTransport1});
 
-			public void handleException(DbException e) {
-				fail();
-			}
-		});
+		final ConnectionWindow window = createConnectionWindow(remoteIndex);
+		final ConnectionWindow window1 = createConnectionWindow(remoteIndex1);
+		Mockery context = new Mockery();
+		final DatabaseComponent db = context.mock(DatabaseComponent.class);
+		context.checking(new Expectations() {{
+			// Initialise before updating the contact
+			oneOf(db).addListener(with(any(ConnectionRecogniserImpl.class)));
+			oneOf(db).getLocalTransports();
+			will(returnValue(localTransports1));
+			oneOf(db).getContacts();
+			will(returnValue(Collections.singletonList(contactId)));
+			// First, transportId <-> remoteIndex, transportId1 <-> remoteIndex
+			oneOf(db).getRemoteIndex(contactId, transportId);
+			will(returnValue(remoteIndex));
+			oneOf(db).getConnectionWindow(contactId, remoteIndex);
+			will(returnValue(window));
+			oneOf(db).getRemoteIndex(contactId, transportId1);
+			will(returnValue(remoteIndex1));
+			oneOf(db).getConnectionWindow(contactId, remoteIndex1);
+			will(returnValue(window1));
+			// Later, transportId <-> remoteIndex1, transportId1 <-> remoteIndex
+			oneOf(db).getConnectionWindow(contactId, remoteIndex);
+			will(returnValue(window));
+			oneOf(db).getConnectionWindow(contactId, remoteIndex1);
+			will(returnValue(window1));
+			// Update the window
+			oneOf(db).getConnectionWindow(contactId, remoteIndex);
+			will(returnValue(window));
+			oneOf(db).setConnectionWindow(contactId, remoteIndex, window);
+		}});
+		Executor executor = new ImmediateExecutor();
+		ConnectionRecogniserImpl c = new ConnectionRecogniserImpl(crypto, db,
+				executor);
+		byte[] encryptedIv = calculateIv();
+		// Ensure the recogniser is initialised
+		assertFalse(c.isInitialised());
+		assertNull(c.acceptConnection(transportId, new byte[IV_LENGTH]));
+		assertTrue(c.isInitialised());
+		// Update the contact
+		c.eventOccurred(new RemoteTransportsUpdatedEvent(contactId,
+				remoteTransports1));
+		// The IV should not be expected by the old transport
+		assertNull(c.acceptConnection(transportId, encryptedIv));
+		// The IV should be expected by the new transport
+		ConnectionContext ctx = c.acceptConnection(transportId1, encryptedIv);
+		assertNotNull(ctx);
+		assertEquals(contactId, ctx.getContactId());
+		assertEquals(remoteIndex, ctx.getTransportIndex());
+		assertEquals(3, ctx.getConnectionNumber());
+		// The IV should no longer be expected
+		assertNull(c.acceptConnection(transportId1, encryptedIv));
 		// The window should have advanced
-		Map<Long, byte[]> unseen = connectionWindow.getUnseen();
+		Map<Long, byte[]> unseen = window.getUnseen();
 		assertEquals(19, unseen.size());
 		for(int i = 0; i < 19; i++) {
 			assertEquals(i != 3, unseen.containsKey(Long.valueOf(i)));
 		}
 		context.assertIsSatisfied();
 	}
+
+	private ConnectionWindow createConnectionWindow(TransportIndex index) {
+		return new ConnectionWindowImpl(crypto, index, inSecret) {
+			@Override
+			public void erase() {}
+		};
+	}
+
+	private byte[] calculateIv() throws Exception {
+		// Calculate the shared secret for connection number 3
+		byte[] secret = inSecret;
+		for(int i = 0; i < 4; i++) {
+			secret = crypto.deriveNextSecret(secret, remoteIndex.getInt(), i);
+		}
+		// Calculate the expected IV for connection number 3
+		ErasableKey ivKey = crypto.deriveIvKey(secret, true);
+		Cipher ivCipher = crypto.getIvCipher();
+		ivCipher.init(Cipher.ENCRYPT_MODE, ivKey);
+		byte[] iv = IvEncoder.encodeIv(true, remoteIndex.getInt(), 3);
+		return ivCipher.doFinal(iv);
+	}
 }
-- 
GitLab