From 1b9f97519951b83d72d99337a7514e26b1fc37bf Mon Sep 17 00:00:00 2001
From: akwizgran <michael@briarproject.org>
Date: Mon, 30 Apr 2018 13:55:40 +0100
Subject: [PATCH] Return default message status if group is invisible.

---
 .../bramble/api/db/DatabaseComponent.java     |  24 ++--
 .../org/briarproject/bramble/db/Database.java |  27 +++--
 .../bramble/db/DatabaseComponentImpl.java     |  11 +-
 .../briarproject/bramble/db/JdbcDatabase.java |  74 ++++--------
 .../bramble/db/DatabaseComponentImplTest.java | 111 ++++++++++++++++++
 .../bramble/db/JdbcDatabaseTest.java          |  33 ++++++
 6 files changed, 205 insertions(+), 75 deletions(-)

diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/db/DatabaseComponent.java b/bramble-api/src/main/java/org/briarproject/bramble/api/db/DatabaseComponent.java
index 3715674413..680ddcad96 100644
--- a/bramble-api/src/main/java/org/briarproject/bramble/api/db/DatabaseComponent.java
+++ b/bramble-api/src/main/java/org/briarproject/bramble/api/db/DatabaseComponent.java
@@ -136,8 +136,8 @@ public interface DatabaseComponent {
 
 	/**
 	 * Deletes the message with the given ID. Unlike
-	 * {@link #removeMessage(Transaction, MessageId)}, the message ID and any
-	 * other associated data are not deleted.
+	 * {@link #removeMessage(Transaction, MessageId)}, the message ID,
+	 * dependencies, metadata, and any other associated state are not deleted.
 	 */
 	void deleteMessage(Transaction txn, MessageId m) throws DbException;
 
@@ -268,7 +268,7 @@ public interface DatabaseComponent {
 	Collection<LocalAuthor> getLocalAuthors(Transaction txn) throws DbException;
 
 	/**
-	 * Returns the IDs of all messages in the given group.
+	 * Returns the IDs of all delivered messages in the given group.
 	 * <p/>
 	 * Read-only.
 	 */
@@ -319,9 +319,9 @@ public interface DatabaseComponent {
 			throws DbException;
 
 	/**
-	 * Returns the metadata for any messages in the given group with metadata
-	 * that matches all entries in the given query. If the query is empty, the
-	 * metadata for all messages is returned.
+	 * Returns the metadata for any delivered messages in the given group with
+	 * metadata that matches all entries in the given query. If the query is
+	 * empty, the metadata for all delivered messages is returned.
 	 * <p/>
 	 * Read-only.
 	 */
@@ -337,8 +337,8 @@ public interface DatabaseComponent {
 			throws DbException;
 
 	/**
-	 * Returns the metadata for the given delivered and pending message.
-	 * This is meant to be only used by the ValidationManager
+	 * Returns the metadata for the given delivered or pending message.
+	 * This is only meant to be used by the ValidationManager.
 	 * <p/>
 	 * Read-only.
 	 */
@@ -346,8 +346,8 @@ public interface DatabaseComponent {
 			throws DbException;
 
 	/**
-	 * Returns the status of all messages in the given group with respect to
-	 * the given contact.
+	 * Returns the status of all delivered messages in the given group with
+	 * respect to the given contact.
 	 * <p/>
 	 * Read-only.
 	 */
@@ -382,8 +382,8 @@ public interface DatabaseComponent {
 	State getMessageState(Transaction txn, MessageId m) throws DbException;
 
 	/**
-	 * Returns the status of the given message with respect to the given
-	 * contact.
+	 * Returns the status of the given delivered message with respect to the
+	 * given contact.
 	 * <p/>
 	 * Read-only.
 	 */
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/db/Database.java b/bramble-core/src/main/java/org/briarproject/bramble/db/Database.java
index f20c0da5ed..bfcb3a2f1a 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/db/Database.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/db/Database.java
@@ -322,16 +322,16 @@ interface Database<T> {
 			throws DbException;
 
 	/**
-	 * Returns the IDs of all messages in the given group.
+	 * Returns the IDs of all delivered messages in the given group.
 	 * <p/>
 	 * Read-only.
 	 */
 	Collection<MessageId> getMessageIds(T txn, GroupId g) throws DbException;
 
 	/**
-	 * Returns the IDs of any messages in the given group with metadata
-	 * matching all entries in the given query. If the query is empty, the IDs
-	 * of all messages are returned.
+	 * Returns the IDs of any delivered messages in the given group with
+	 * metadata that matches all entries in the given query. If the query is
+	 * empty, the IDs of all delivered messages are returned.
 	 * <p/>
 	 * Read-only.
 	 */
@@ -347,9 +347,9 @@ interface Database<T> {
 			throws DbException;
 
 	/**
-	 * Returns the metadata for any messages in the given group with metadata
-	 * matching all entries in the given query. If the query is empty, the
-	 * metadata for all messages is returned.
+	 * Returns the metadata for any delivered messages in the given group with
+	 * metadata that matches all entries in the given query. If the query is
+	 * empty, the metadata for all delivered messages is returned.
 	 * <p/>
 	 * Read-only.
 	 */
@@ -357,7 +357,8 @@ interface Database<T> {
 			Metadata query) throws DbException;
 
 	/**
-	 * Returns the metadata for the given delivered message.
+	 * Returns the metadata for the given delivered or pending message.
+	 * This is only meant to be used by the ValidationManager.
 	 * <p/>
 	 * Read-only.
 	 */
@@ -365,7 +366,7 @@ interface Database<T> {
 			throws DbException;
 
 	/**
-	 * Returns the metadata for the given message.
+	 * Returns the metadata for the given delivered message.
 	 * <p/>
 	 * Read-only.
 	 */
@@ -379,8 +380,8 @@ interface Database<T> {
 	State getMessageState(T txn, MessageId m) throws DbException;
 
 	/**
-	 * Returns the status of all messages in the given group with respect
-	 * to the given contact.
+	 * Returns the status of all delivered messages in the given group with
+	 * respect to the given contact.
 	 * <p/>
 	 * Read-only.
 	 */
@@ -388,11 +389,13 @@ interface Database<T> {
 			throws DbException;
 
 	/**
-	 * Returns the status of the given message with respect to the given
+	 * Returns the status of the given delivered message with respect to the
+	 * given contact, or null if the message's group is invisible to the
 	 * contact.
 	 * <p/>
 	 * Read-only.
 	 */
+	@Nullable
 	MessageStatus getMessageStatus(T txn, ContactId c, MessageId m)
 			throws DbException;
 
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/db/DatabaseComponentImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/db/DatabaseComponentImpl.java
index 1534a9d81b..ad83111180 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/db/DatabaseComponentImpl.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/db/DatabaseComponentImpl.java
@@ -560,6 +560,13 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
 			throw new NoSuchContactException();
 		if (!db.containsGroup(txn, g))
 			throw new NoSuchGroupException();
+		if (db.getGroupVisibility(txn, c, g) == INVISIBLE) {
+			// No status rows exist - return default statuses
+			Collection<MessageStatus> statuses = new ArrayList<>();
+			for (MessageId m : db.getMessageIds(txn, g))
+				statuses.add(new MessageStatus(m, c, false, false));
+			return statuses;
+		}
 		return db.getMessageStatus(txn, c, g);
 	}
 
@@ -571,7 +578,9 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
 			throw new NoSuchContactException();
 		if (!db.containsMessage(txn, m))
 			throw new NoSuchMessageException();
-		return db.getMessageStatus(txn, c, m);
+		MessageStatus status = db.getMessageStatus(txn, c, m);
+		if (status == null) return new MessageStatus(m, c, false, false);
+		return status;
 	}
 
 	@Override
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/db/JdbcDatabase.java b/bramble-core/src/main/java/org/briarproject/bramble/db/JdbcDatabase.java
index 7115808e28..21103c2d43 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/db/JdbcDatabase.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/db/JdbcDatabase.java
@@ -1515,33 +1515,12 @@ abstract class JdbcDatabase implements Database<Connection> {
 			throws DbException {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
-		try {
-			String sql = "SELECT messageId FROM messages WHERE groupId = ?";
-			ps = txn.prepareStatement(sql);
-			ps.setBytes(1, g.getBytes());
-			rs = ps.executeQuery();
-			List<MessageId> ids = new ArrayList<>();
-			while (rs.next()) ids.add(new MessageId(rs.getBytes(1)));
-			rs.close();
-			ps.close();
-			return ids;
-		} catch (SQLException e) {
-			tryToClose(rs);
-			tryToClose(ps);
-			throw new DbException(e);
-		}
-	}
-
-	private Collection<MessageId> getMessageIds(Connection txn, GroupId g,
-			State state) throws DbException {
-		PreparedStatement ps = null;
-		ResultSet rs = null;
 		try {
 			String sql = "SELECT messageId FROM messages"
-					+ " WHERE state = ? AND groupId = ?";
+					+ " WHERE groupId = ? AND state = ?";
 			ps = txn.prepareStatement(sql);
-			ps.setInt(1, state.getValue());
-			ps.setBytes(2, g.getBytes());
+			ps.setBytes(1, g.getBytes());
+			ps.setInt(2, DELIVERED.getValue());
 			rs = ps.executeQuery();
 			List<MessageId> ids = new ArrayList<>();
 			while (rs.next()) ids.add(new MessageId(rs.getBytes(1)));
@@ -1559,7 +1538,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 	public Collection<MessageId> getMessageIds(Connection txn, GroupId g,
 			Metadata query) throws DbException {
 		// If there are no query terms, return all delivered messages
-		if (query.isEmpty()) return getMessageIds(txn, g, DELIVERED);
+		if (query.isEmpty()) return getMessageIds(txn, g);
 		PreparedStatement ps = null;
 		ResultSet rs = null;
 		try {
@@ -1718,10 +1697,11 @@ abstract class JdbcDatabase implements Database<Connection> {
 		ResultSet rs = null;
 		try {
 			String sql = "SELECT messageId, txCount > 0, seen FROM statuses"
-					+ " WHERE groupId = ? AND contactId = ?";
+					+ " WHERE groupId = ? AND contactId = ? AND state = ?";
 			ps = txn.prepareStatement(sql);
 			ps.setBytes(1, g.getBytes());
 			ps.setInt(2, c.getInt());
+			ps.setInt(3, DELIVERED.getValue());
 			rs = ps.executeQuery();
 			List<MessageStatus> statuses = new ArrayList<>();
 			while (rs.next()) {
@@ -1741,24 +1721,29 @@ abstract class JdbcDatabase implements Database<Connection> {
 	}
 
 	@Override
+	@Nullable
 	public MessageStatus getMessageStatus(Connection txn, ContactId c,
 			MessageId m) throws DbException {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
 		try {
 			String sql = "SELECT txCount > 0, seen FROM statuses"
-					+ " WHERE messageId = ? AND contactId = ?";
+					+ " WHERE messageId = ? AND contactId = ? AND state = ?";
 			ps = txn.prepareStatement(sql);
 			ps.setBytes(1, m.getBytes());
 			ps.setInt(2, c.getInt());
+			ps.setInt(3, DELIVERED.getValue());
 			rs = ps.executeQuery();
-			if (!rs.next()) throw new DbStateException();
-			boolean sent = rs.getBoolean(1);
-			boolean seen = rs.getBoolean(2);
+			MessageStatus status = null;
+			if (rs.next()) {
+				boolean sent = rs.getBoolean(1);
+				boolean seen = rs.getBoolean(2);
+				status = new MessageStatus(m, c, sent, seen);
+			}
 			if (rs.next()) throw new DbStateException();
 			rs.close();
 			ps.close();
-			return new MessageStatus(m, c, sent, seen);
+			return status;
 		} catch (SQLException e) {
 			tryToClose(rs);
 			tryToClose(ps);
@@ -2578,7 +2563,14 @@ abstract class JdbcDatabase implements Database<Connection> {
 			if (affected != 1) throw new DbStateException();
 			ps.close();
 			// Remove status rows for the messages in the group
-			for (MessageId m : getMessageIds(txn, g)) removeStatus(txn, c, m);
+			sql = "DELETE FROM statuses"
+					+ " WHERE contactId = ? AND groupId = ?";
+			ps = txn.prepareStatement(sql);
+			ps.setInt(1, c.getInt());
+			ps.setBytes(2, g.getBytes());
+			affected = ps.executeUpdate();
+			if (affected < 0) throw new DbStateException();
+			ps.close();
 		} catch (SQLException e) {
 			tryToClose(ps);
 			throw new DbException(e);
@@ -2662,24 +2654,6 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
-	private void removeStatus(Connection txn, ContactId c, MessageId m)
-			throws DbException {
-		PreparedStatement ps = null;
-		try {
-			String sql = "DELETE FROM statuses"
-					+ " WHERE messageId = ? AND contactId = ?";
-			ps = txn.prepareStatement(sql);
-			ps.setBytes(1, m.getBytes());
-			ps.setInt(2, c.getInt());
-			int affected = ps.executeUpdate();
-			if (affected != 1) throw new DbStateException();
-			ps.close();
-		} catch (SQLException e) {
-			tryToClose(ps);
-			throw new DbException(e);
-		}
-	}
-
 	@Override
 	public void removeTransport(Connection txn, TransportId t)
 			throws DbException {
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/db/DatabaseComponentImplTest.java b/bramble-core/src/test/java/org/briarproject/bramble/db/DatabaseComponentImplTest.java
index c228bf67eb..1fec34ce9d 100644
--- a/bramble-core/src/test/java/org/briarproject/bramble/db/DatabaseComponentImplTest.java
+++ b/bramble-core/src/test/java/org/briarproject/bramble/db/DatabaseComponentImplTest.java
@@ -30,6 +30,7 @@ import org.briarproject.bramble.api.sync.Group;
 import org.briarproject.bramble.api.sync.GroupId;
 import org.briarproject.bramble.api.sync.Message;
 import org.briarproject.bramble.api.sync.MessageId;
+import org.briarproject.bramble.api.sync.MessageStatus;
 import org.briarproject.bramble.api.sync.Offer;
 import org.briarproject.bramble.api.sync.Request;
 import org.briarproject.bramble.api.sync.event.GroupAddedEvent;
@@ -77,6 +78,7 @@ import static org.briarproject.bramble.test.TestUtils.getTransportId;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
 public class DatabaseComponentImplTest extends BrambleMockTestCase {
@@ -1319,6 +1321,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
 		TransportKeys transportKeys = createTransportKeys();
 		KeySet ks = new KeySet(keySetId, contactId, transportKeys);
 		Collection<KeySet> keys = singletonList(ks);
+
 		context.checking(new Expectations() {{
 			// startTransaction()
 			oneOf(database).startTransaction();
@@ -1348,6 +1351,114 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
 		}
 	}
 
+	@Test
+	public void testGetMessageStatusByGroupId() throws Exception {
+		MessageStatus status =
+				new MessageStatus(messageId, contactId, true, true);
+
+		context.checking(new Expectations() {{
+			// startTransaction()
+			oneOf(database).startTransaction();
+			will(returnValue(txn));
+			// getMessageStatus()
+			oneOf(database).containsContact(txn, contactId);
+			will(returnValue(true));
+			oneOf(database).containsGroup(txn, groupId);
+			will(returnValue(true));
+			oneOf(database).getGroupVisibility(txn, contactId, groupId);
+			will(returnValue(VISIBLE));
+			oneOf(database).getMessageStatus(txn, contactId, groupId);
+			will(returnValue(singletonList(status)));
+			// getMessageStatus() again
+			oneOf(database).containsContact(txn, contactId);
+			will(returnValue(true));
+			oneOf(database).containsGroup(txn, groupId);
+			will(returnValue(true));
+			oneOf(database).getGroupVisibility(txn, contactId, groupId);
+			will(returnValue(INVISIBLE));
+			oneOf(database).getMessageIds(txn, groupId);
+			will(returnValue(singletonList(messageId)));
+			// endTransaction()
+			oneOf(database).commitTransaction(txn);
+		}});
+		DatabaseComponent db = createDatabaseComponent(database, eventBus,
+				shutdown);
+
+		Transaction transaction = db.startTransaction(true);
+		try {
+			// With visible group - return stored status
+			Collection<MessageStatus> statuses =
+					db.getMessageStatus(transaction, contactId, groupId);
+			assertEquals(1, statuses.size());
+			MessageStatus s = statuses.iterator().next();
+			assertEquals(messageId, s.getMessageId());
+			assertEquals(contactId, s.getContactId());
+			assertTrue(s.isSent());
+			assertTrue(s.isSeen());
+			// With invisible group - return default status
+			statuses = db.getMessageStatus(transaction, contactId, groupId);
+			assertEquals(1, statuses.size());
+			s = statuses.iterator().next();
+			assertEquals(messageId, s.getMessageId());
+			assertEquals(contactId, s.getContactId());
+			assertFalse(s.isSent());
+			assertFalse(s.isSeen());
+			db.commitTransaction(transaction);
+		} finally {
+			db.endTransaction(transaction);
+		}
+	}
+
+	@Test
+	public void testGetMessageStatusByMessageId() throws Exception {
+		MessageStatus status =
+				new MessageStatus(messageId, contactId, true, true);
+
+		context.checking(new Expectations() {{
+			// startTransaction()
+			oneOf(database).startTransaction();
+			will(returnValue(txn));
+			// getMessageStatus()
+			oneOf(database).containsContact(txn, contactId);
+			will(returnValue(true));
+			oneOf(database).containsMessage(txn, messageId);
+			will(returnValue(true));
+			oneOf(database).getMessageStatus(txn, contactId, messageId);
+			will(returnValue(status));
+			// getMessageStatus() again
+			oneOf(database).containsContact(txn, contactId);
+			will(returnValue(true));
+			oneOf(database).containsMessage(txn, messageId);
+			will(returnValue(true));
+			oneOf(database).getMessageStatus(txn, contactId, messageId);
+			will(returnValue(null));
+			// endTransaction()
+			oneOf(database).commitTransaction(txn);
+		}});
+		DatabaseComponent db = createDatabaseComponent(database, eventBus,
+				shutdown);
+
+		Transaction transaction = db.startTransaction(true);
+		try {
+			// With visible group - return stored status
+			MessageStatus s =
+					db.getMessageStatus(transaction, contactId, messageId);
+			assertEquals(messageId, s.getMessageId());
+			assertEquals(contactId, s.getContactId());
+			assertTrue(s.isSent());
+			assertTrue(s.isSeen());
+			// With invisible group - return default status
+			s = db.getMessageStatus(transaction, contactId, messageId);
+			assertEquals(messageId, s.getMessageId());
+			assertEquals(contactId, s.getContactId());
+			assertFalse(s.isSent());
+			assertFalse(s.isSeen());
+			db.commitTransaction(transaction);
+		} finally {
+			db.endTransaction(transaction);
+		}
+	}
+
 	private TransportKeys createTransportKeys() {
 		SecretKey inPrevTagKey = getSecretKey();
 		SecretKey inPrevHeaderKey = getSecretKey();
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/db/JdbcDatabaseTest.java b/bramble-core/src/test/java/org/briarproject/bramble/db/JdbcDatabaseTest.java
index d5b29dc657..c8dc8dccc6 100644
--- a/bramble-core/src/test/java/org/briarproject/bramble/db/JdbcDatabaseTest.java
+++ b/bramble-core/src/test/java/org/briarproject/bramble/db/JdbcDatabaseTest.java
@@ -1596,6 +1596,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
 
 		// The message should not be sent or seen
 		MessageStatus status = db.getMessageStatus(txn, contactId, messageId);
+		assertNotNull(status);
 		assertEquals(messageId, status.getMessageId());
 		assertEquals(contactId, status.getContactId());
 		assertFalse(status.isSent());
@@ -1616,6 +1617,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
 
 		// The message should be sent but not seen
 		status = db.getMessageStatus(txn, contactId, messageId);
+		assertNotNull(status);
 		assertEquals(messageId, status.getMessageId());
 		assertEquals(contactId, status.getContactId());
 		assertTrue(status.isSent());
@@ -1635,6 +1637,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
 
 		// The message should be sent and seen
 		status = db.getMessageStatus(txn, contactId, messageId);
+		assertNotNull(status);
 		assertEquals(messageId, status.getMessageId());
 		assertEquals(contactId, status.getContactId());
 		assertTrue(status.isSent());
@@ -1649,6 +1652,36 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
 		assertTrue(status.isSent());
 		assertTrue(status.isSeen());
 
+		// Make the group invisible to the contact
+		db.removeGroupVisibility(txn, contactId, groupId);
+
+		// Null should be returned when querying by message
+		assertNull(db.getMessageStatus(txn, contactId, messageId));
+
+		// No statuses should be returned when querying by group
+		statuses = db.getMessageStatus(txn, contactId, groupId);
+		assertEquals(0, statuses.size());
+
+		// Make the group visible to the contact again
+		db.addGroupVisibility(txn, contactId, groupId, false);
+
+		// The default status should be returned when querying by message
+		status = db.getMessageStatus(txn, contactId, messageId);
+		assertNotNull(status);
+		assertEquals(messageId, status.getMessageId());
+		assertEquals(contactId, status.getContactId());
+		assertFalse(status.isSent());
+		assertFalse(status.isSeen());
+
+		// The default status should be returned when querying by group
+		statuses = db.getMessageStatus(txn, contactId, groupId);
+		assertEquals(1, statuses.size());
+		status = statuses.iterator().next();
+		assertEquals(messageId, status.getMessageId());
+		assertEquals(contactId, status.getContactId());
+		assertFalse(status.isSent());
+		assertFalse(status.isSeen());
+
 		db.commitTransaction(txn);
 		db.close();
 	}
-- 
GitLab