diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/versioning/ClientVersioningManager.java b/bramble-api/src/main/java/org/briarproject/bramble/api/versioning/ClientVersioningManager.java index 00b3d672c..76b261d1b 100644 --- a/bramble-api/src/main/java/org/briarproject/bramble/api/versioning/ClientVersioningManager.java +++ b/bramble-api/src/main/java/org/briarproject/bramble/api/versioning/ClientVersioningManager.java @@ -38,6 +38,13 @@ public interface ClientVersioningManager { Visibility getClientVisibility(Transaction txn, ContactId contactId, ClientId clientId, int majorVersion) throws DbException; + /** + * Returns the minor version of the given client that is supported by the + * given contact, or -1 if the contact does not support the client. + */ + int getClientMinorVersion(Transaction txn, ContactId contactId, + ClientId clientId, int majorVersion) throws DbException; + interface ClientVersioningHook { void onClientVisibilityChanging(Transaction txn, Contact c, diff --git a/bramble-core/src/main/java/org/briarproject/bramble/versioning/ClientVersioningManagerImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/versioning/ClientVersioningManagerImpl.java index a7a6ac312..ccc7b41bb 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/versioning/ClientVersioningManagerImpl.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/versioning/ClientVersioningManagerImpl.java @@ -89,14 +89,9 @@ class ClientVersioningManagerImpl implements ClientVersioningManager, Client, public Visibility getClientVisibility(Transaction txn, ContactId contactId, ClientId clientId, int majorVersion) throws DbException { try { - Contact contact = db.getContact(txn, contactId); - Group g = getContactGroup(contact); - // Contact may be in the process of being added or removed, so - // contact group may not exist - if (!db.containsGroup(txn, g.getId())) return INVISIBLE; - LatestUpdates latest = findLatestUpdates(txn, g.getId()); + LatestUpdates latest = findLatestUpdates(txn, contactId); + if (latest == null || latest.remote == null) return INVISIBLE; if (latest.local == null) throw new DbException(); - if (latest.remote == null) return INVISIBLE; Update localUpdate = loadUpdate(txn, latest.local.messageId); Update remoteUpdate = loadUpdate(txn, latest.remote.messageId); Map visibilities = @@ -110,6 +105,24 @@ class ClientVersioningManagerImpl implements ClientVersioningManager, Client, } } + @Override + public int getClientMinorVersion(Transaction txn, ContactId contactId, + ClientId clientId, int majorVersion) throws DbException { + try { + LatestUpdates latest = findLatestUpdates(txn, contactId); + if (latest == null || latest.remote == null) return -1; + Update remoteUpdate = loadUpdate(txn, latest.remote.messageId); + ClientMajorVersion cv = + new ClientMajorVersion(clientId, majorVersion); + for (ClientState remote : remoteUpdate.states) { + if (remote.majorVersion.equals(cv)) return remote.minorVersion; + } + return -1; + } catch (FormatException e) { + throw new DbException(e); + } + } + @Override public void createLocalState(Transaction txn) throws DbException { if (db.containsGroup(txn, localGroup.getId())) return; @@ -336,6 +349,17 @@ class ClientVersioningManagerImpl implements ClientVersioningManager, Client, MAJOR_VERSION, c); } + @Nullable + private LatestUpdates findLatestUpdates(Transaction txn, ContactId c) + throws DbException, FormatException { + Contact contact = db.getContact(txn, c); + Group g = getContactGroup(contact); + // Contact may be in the process of being added or removed, so + // contact group may not exist + if (!db.containsGroup(txn, g.getId())) return null; + return findLatestUpdates(txn, g.getId()); + } + private LatestUpdates findLatestUpdates(Transaction txn, GroupId g) throws DbException, FormatException { Map metadata = diff --git a/bramble-core/src/test/java/org/briarproject/bramble/versioning/ClientVersioningManagerImplTest.java b/bramble-core/src/test/java/org/briarproject/bramble/versioning/ClientVersioningManagerImplTest.java index a4287fa78..1c6253c8b 100644 --- a/bramble-core/src/test/java/org/briarproject/bramble/versioning/ClientVersioningManagerImplTest.java +++ b/bramble-core/src/test/java/org/briarproject/bramble/versioning/ClientVersioningManagerImplTest.java @@ -8,6 +8,7 @@ import org.briarproject.bramble.api.data.BdfDictionary; import org.briarproject.bramble.api.data.BdfEntry; import org.briarproject.bramble.api.data.BdfList; import org.briarproject.bramble.api.db.DatabaseComponent; +import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.db.Metadata; import org.briarproject.bramble.api.db.Transaction; import org.briarproject.bramble.api.sync.ClientId; @@ -43,6 +44,7 @@ import static org.briarproject.bramble.util.StringUtils.getRandomString; import static org.briarproject.bramble.versioning.ClientVersioningConstants.GROUP_KEY_CONTACT_ID; import static org.briarproject.bramble.versioning.ClientVersioningConstants.MSG_KEY_LOCAL; import static org.briarproject.bramble.versioning.ClientVersioningConstants.MSG_KEY_UPDATE_VERSION; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; public class ClientVersioningManagerImplTest extends BrambleMockTestCase { @@ -657,4 +659,327 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase { c.registerClient(clientId, 123, 234, hook); assertFalse(c.incomingMessage(txn, newRemoteUpdate, new Metadata())); } + + @Test + public void testReturnsInvisibleIfContactGroupDoesNotExist() + throws Exception { + expectGetContactGroup(false); + + ClientVersioningManagerImpl c = createInstance(); + assertEquals(INVISIBLE, c.getClientVisibility(txn, contact.getId(), + clientId, 123)); + } + + @Test + public void testReturnsInvisibleIfNoRemoteUpdateExists() throws Exception { + MessageId localUpdateId = new MessageId(getRandomId()); + BdfDictionary localUpdateMeta = BdfDictionary.of( + new BdfEntry(MSG_KEY_UPDATE_VERSION, 1L), + new BdfEntry(MSG_KEY_LOCAL, true)); + + expectGetContactGroup(true); + context.checking(new Expectations() {{ + oneOf(clientHelper).getMessageMetadataAsDictionary(txn, + contactGroup.getId()); + will(returnValue(singletonMap(localUpdateId, localUpdateMeta))); + }}); + + ClientVersioningManagerImpl c = createInstance(); + assertEquals(INVISIBLE, c.getClientVisibility(txn, contact.getId(), + clientId, 123)); + } + + @Test(expected = DbException.class) + public void testThrowsExceptionIfNoLocalUpdateExists() throws Exception { + MessageId remoteUpdateId = new MessageId(getRandomId()); + BdfDictionary remoteUpdateMeta = BdfDictionary.of( + new BdfEntry(MSG_KEY_UPDATE_VERSION, 1L), + new BdfEntry(MSG_KEY_LOCAL, false)); + + expectGetContactGroup(true); + context.checking(new Expectations() {{ + oneOf(clientHelper).getMessageMetadataAsDictionary(txn, + contactGroup.getId()); + will(returnValue(singletonMap(remoteUpdateId, remoteUpdateMeta))); + }}); + + ClientVersioningManagerImpl c = createInstance(); + c.getClientVisibility(txn, contact.getId(), clientId, 123); + } + + @Test + public void testReturnsInvisibleIfClientNotSupportedLocally() + throws Exception { + MessageId localUpdateId = new MessageId(getRandomId()); + BdfDictionary localUpdateMeta = BdfDictionary.of( + new BdfEntry(MSG_KEY_UPDATE_VERSION, 1L), + new BdfEntry(MSG_KEY_LOCAL, true)); + MessageId remoteUpdateId = new MessageId(getRandomId()); + BdfDictionary remoteUpdateMeta = BdfDictionary.of( + new BdfEntry(MSG_KEY_UPDATE_VERSION, 1L), + new BdfEntry(MSG_KEY_LOCAL, false)); + Map messageMetadata = new HashMap<>(); + messageMetadata.put(localUpdateId, localUpdateMeta); + messageMetadata.put(remoteUpdateId, remoteUpdateMeta); + // The client is supported remotely but not locally + BdfList localUpdateBody = BdfList.of(new BdfList(), 1L); + BdfList remoteUpdateBody = BdfList.of(BdfList.of( + BdfList.of(clientId.getString(), 123, 234, false)), 1L); + + expectGetContactGroup(true); + context.checking(new Expectations() {{ + oneOf(clientHelper).getMessageMetadataAsDictionary(txn, + contactGroup.getId()); + will(returnValue(messageMetadata)); + oneOf(clientHelper).getMessageAsList(txn, localUpdateId); + will(returnValue(localUpdateBody)); + oneOf(clientHelper).getMessageAsList(txn, remoteUpdateId); + will(returnValue(remoteUpdateBody)); + }}); + + ClientVersioningManagerImpl c = createInstance(); + assertEquals(INVISIBLE, c.getClientVisibility(txn, contact.getId(), + clientId, 123)); + } + + @Test + public void testReturnsInvisibleIfClientNotSupportedRemotely() + throws Exception { + MessageId localUpdateId = new MessageId(getRandomId()); + BdfDictionary localUpdateMeta = BdfDictionary.of( + new BdfEntry(MSG_KEY_UPDATE_VERSION, 1L), + new BdfEntry(MSG_KEY_LOCAL, true)); + MessageId remoteUpdateId = new MessageId(getRandomId()); + BdfDictionary remoteUpdateMeta = BdfDictionary.of( + new BdfEntry(MSG_KEY_UPDATE_VERSION, 1L), + new BdfEntry(MSG_KEY_LOCAL, false)); + Map messageMetadata = new HashMap<>(); + messageMetadata.put(localUpdateId, localUpdateMeta); + messageMetadata.put(remoteUpdateId, remoteUpdateMeta); + // The client is supported locally but not remotely + BdfList localUpdateBody = BdfList.of(BdfList.of( + BdfList.of(clientId.getString(), 123, 234, false)), 1L); + BdfList remoteUpdateBody = BdfList.of(new BdfList(), 1L); + + expectGetContactGroup(true); + context.checking(new Expectations() {{ + oneOf(clientHelper).getMessageMetadataAsDictionary(txn, + contactGroup.getId()); + will(returnValue(messageMetadata)); + oneOf(clientHelper).getMessageAsList(txn, localUpdateId); + will(returnValue(localUpdateBody)); + oneOf(clientHelper).getMessageAsList(txn, remoteUpdateId); + will(returnValue(remoteUpdateBody)); + }}); + + ClientVersioningManagerImpl c = createInstance(); + assertEquals(INVISIBLE, c.getClientVisibility(txn, contact.getId(), + clientId, 123)); + } + + @Test + public void testReturnsVisibleIfClientNotActiveRemotely() throws Exception { + MessageId localUpdateId = new MessageId(getRandomId()); + BdfDictionary localUpdateMeta = BdfDictionary.of( + new BdfEntry(MSG_KEY_UPDATE_VERSION, 1L), + new BdfEntry(MSG_KEY_LOCAL, true)); + MessageId remoteUpdateId = new MessageId(getRandomId()); + BdfDictionary remoteUpdateMeta = BdfDictionary.of( + new BdfEntry(MSG_KEY_UPDATE_VERSION, 1L), + new BdfEntry(MSG_KEY_LOCAL, false)); + Map messageMetadata = new HashMap<>(); + messageMetadata.put(localUpdateId, localUpdateMeta); + messageMetadata.put(remoteUpdateId, remoteUpdateMeta); + // The client is supported locally and remotely but not active + BdfList localUpdateBody = BdfList.of(BdfList.of( + BdfList.of(clientId.getString(), 123, 234, false)), 1L); + BdfList remoteUpdateBody = BdfList.of(BdfList.of( + BdfList.of(clientId.getString(), 123, 234, false)), 1L); + + expectGetContactGroup(true); + context.checking(new Expectations() {{ + oneOf(clientHelper).getMessageMetadataAsDictionary(txn, + contactGroup.getId()); + will(returnValue(messageMetadata)); + oneOf(clientHelper).getMessageAsList(txn, localUpdateId); + will(returnValue(localUpdateBody)); + oneOf(clientHelper).getMessageAsList(txn, remoteUpdateId); + will(returnValue(remoteUpdateBody)); + }}); + + ClientVersioningManagerImpl c = createInstance(); + assertEquals(VISIBLE, c.getClientVisibility(txn, contact.getId(), + clientId, 123)); + } + + @Test + public void testReturnsSharedIfClientActiveRemotely() throws Exception { + MessageId localUpdateId = new MessageId(getRandomId()); + BdfDictionary localUpdateMeta = BdfDictionary.of( + new BdfEntry(MSG_KEY_UPDATE_VERSION, 1L), + new BdfEntry(MSG_KEY_LOCAL, true)); + MessageId remoteUpdateId = new MessageId(getRandomId()); + BdfDictionary remoteUpdateMeta = BdfDictionary.of( + new BdfEntry(MSG_KEY_UPDATE_VERSION, 1L), + new BdfEntry(MSG_KEY_LOCAL, false)); + Map messageMetadata = new HashMap<>(); + messageMetadata.put(localUpdateId, localUpdateMeta); + messageMetadata.put(remoteUpdateId, remoteUpdateMeta); + // The client is supported locally and remotely and active + BdfList localUpdateBody = BdfList.of(BdfList.of( + BdfList.of(clientId.getString(), 123, 234, true)), 1L); + BdfList remoteUpdateBody = BdfList.of(BdfList.of( + BdfList.of(clientId.getString(), 123, 234, true)), 1L); + + expectGetContactGroup(true); + context.checking(new Expectations() {{ + oneOf(clientHelper).getMessageMetadataAsDictionary(txn, + contactGroup.getId()); + will(returnValue(messageMetadata)); + oneOf(clientHelper).getMessageAsList(txn, localUpdateId); + will(returnValue(localUpdateBody)); + oneOf(clientHelper).getMessageAsList(txn, remoteUpdateId); + will(returnValue(remoteUpdateBody)); + }}); + + ClientVersioningManagerImpl c = createInstance(); + assertEquals(SHARED, c.getClientVisibility(txn, contact.getId(), + clientId, 123)); + } + + @Test + public void testReturnsNegativeIfContactGroupDoesNotExist() + throws Exception { + expectGetContactGroup(false); + + ClientVersioningManagerImpl c = createInstance(); + assertEquals(-1, c.getClientMinorVersion(txn, contact.getId(), + clientId, 123)); + } + + @Test + public void testReturnsNegativeIfNoRemoteUpdateExists() throws Exception { + MessageId localUpdateId = new MessageId(getRandomId()); + BdfDictionary localUpdateMeta = BdfDictionary.of( + new BdfEntry(MSG_KEY_UPDATE_VERSION, 1L), + new BdfEntry(MSG_KEY_LOCAL, true)); + + expectGetContactGroup(true); + context.checking(new Expectations() {{ + oneOf(clientHelper).getMessageMetadataAsDictionary(txn, + contactGroup.getId()); + will(returnValue(singletonMap(localUpdateId, localUpdateMeta))); + }}); + + ClientVersioningManagerImpl c = createInstance(); + assertEquals(-1, c.getClientMinorVersion(txn, contact.getId(), + clientId, 123)); + } + + @Test + public void testReturnsNegativeIfClientNotSupportedRemotely() + throws Exception { + MessageId localUpdateId = new MessageId(getRandomId()); + BdfDictionary localUpdateMeta = BdfDictionary.of( + new BdfEntry(MSG_KEY_UPDATE_VERSION, 1L), + new BdfEntry(MSG_KEY_LOCAL, true)); + MessageId remoteUpdateId = new MessageId(getRandomId()); + BdfDictionary remoteUpdateMeta = BdfDictionary.of( + new BdfEntry(MSG_KEY_UPDATE_VERSION, 1L), + new BdfEntry(MSG_KEY_LOCAL, false)); + Map messageMetadata = new HashMap<>(); + messageMetadata.put(localUpdateId, localUpdateMeta); + messageMetadata.put(remoteUpdateId, remoteUpdateMeta); + // The client is not supported remotely + BdfList remoteUpdateBody = BdfList.of(new BdfList(), 1L); + + expectGetContactGroup(true); + context.checking(new Expectations() {{ + oneOf(clientHelper).getMessageMetadataAsDictionary(txn, + contactGroup.getId()); + will(returnValue(messageMetadata)); + oneOf(clientHelper).getMessageAsList(txn, remoteUpdateId); + will(returnValue(remoteUpdateBody)); + }}); + + ClientVersioningManagerImpl c = createInstance(); + assertEquals(-1, c.getClientMinorVersion(txn, contact.getId(), + clientId, 123)); + } + + @Test + public void testReturnsMinorVersionIfClientNotActiveRemotely() + throws Exception { + MessageId localUpdateId = new MessageId(getRandomId()); + BdfDictionary localUpdateMeta = BdfDictionary.of( + new BdfEntry(MSG_KEY_UPDATE_VERSION, 1L), + new BdfEntry(MSG_KEY_LOCAL, true)); + MessageId remoteUpdateId = new MessageId(getRandomId()); + BdfDictionary remoteUpdateMeta = BdfDictionary.of( + new BdfEntry(MSG_KEY_UPDATE_VERSION, 1L), + new BdfEntry(MSG_KEY_LOCAL, false)); + Map messageMetadata = new HashMap<>(); + messageMetadata.put(localUpdateId, localUpdateMeta); + messageMetadata.put(remoteUpdateId, remoteUpdateMeta); + // The client is supported remotely but not active + BdfList remoteUpdateBody = BdfList.of(BdfList.of( + BdfList.of(clientId.getString(), 123, 234, false)), 1L); + + expectGetContactGroup(true); + context.checking(new Expectations() {{ + oneOf(clientHelper).getMessageMetadataAsDictionary(txn, + contactGroup.getId()); + will(returnValue(messageMetadata)); + oneOf(clientHelper).getMessageAsList(txn, remoteUpdateId); + will(returnValue(remoteUpdateBody)); + }}); + + ClientVersioningManagerImpl c = createInstance(); + assertEquals(234, c.getClientMinorVersion(txn, contact.getId(), + clientId, 123)); + } + + @Test + public void testReturnsMinorVersionIfClientActiveRemotely() + throws Exception { + MessageId localUpdateId = new MessageId(getRandomId()); + BdfDictionary localUpdateMeta = BdfDictionary.of( + new BdfEntry(MSG_KEY_UPDATE_VERSION, 1L), + new BdfEntry(MSG_KEY_LOCAL, true)); + MessageId remoteUpdateId = new MessageId(getRandomId()); + BdfDictionary remoteUpdateMeta = BdfDictionary.of( + new BdfEntry(MSG_KEY_UPDATE_VERSION, 1L), + new BdfEntry(MSG_KEY_LOCAL, false)); + Map messageMetadata = new HashMap<>(); + messageMetadata.put(localUpdateId, localUpdateMeta); + messageMetadata.put(remoteUpdateId, remoteUpdateMeta); + // The client is supported remotely and active + BdfList remoteUpdateBody = BdfList.of(BdfList.of( + BdfList.of(clientId.getString(), 123, 234, true)), 1L); + + expectGetContactGroup(true); + context.checking(new Expectations() {{ + oneOf(clientHelper).getMessageMetadataAsDictionary(txn, + contactGroup.getId()); + will(returnValue(messageMetadata)); + oneOf(clientHelper).getMessageAsList(txn, remoteUpdateId); + will(returnValue(remoteUpdateBody)); + }}); + + ClientVersioningManagerImpl c = createInstance(); + assertEquals(234, c.getClientMinorVersion(txn, contact.getId(), + clientId, 123)); + } + + private void expectGetContactGroup(boolean exists) throws Exception { + context.checking(new Expectations() {{ + oneOf(db).getContact(txn, contact.getId()); + will(returnValue(contact)); + oneOf(contactGroupFactory).createContactGroup(CLIENT_ID, + MAJOR_VERSION, contact); + will(returnValue(contactGroup)); + oneOf(db).containsGroup(txn, contactGroup.getId()); + will(returnValue(exists)); + }}); + } }