Newer
Older
package org.briarproject.db;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.briarproject.api.AuthorConstants.MAX_PUBLIC_KEY_LENGTH;
import static org.briarproject.api.messaging.MessagingConstants.GROUP_SALT_LENGTH;
import static org.junit.Assert.assertArrayEquals;
import java.io.File;
import java.sql.Connection;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;
import org.briarproject.BriarTestCase;
import org.briarproject.TestDatabaseConfig;
import org.briarproject.TestFileUtils;
import org.briarproject.TestMessage;
import org.briarproject.TestUtils;
import org.briarproject.api.Author;
import org.briarproject.api.AuthorId;
import org.briarproject.api.ContactId;
import org.briarproject.api.LocalAuthor;
import org.briarproject.api.TransportConfig;
import org.briarproject.api.TransportId;
import org.briarproject.api.TransportProperties;
import org.briarproject.api.db.DbException;
import org.briarproject.api.db.MessageHeader;
import org.briarproject.api.messaging.Group;
import org.briarproject.api.messaging.GroupId;
import org.briarproject.api.messaging.Message;
import org.briarproject.api.messaging.MessageId;
import org.briarproject.api.transport.Endpoint;
import org.briarproject.api.transport.TemporarySecret;
import org.briarproject.system.SystemClock;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
public class H2DatabaseTest extends BriarTestCase {
private static final int ONE_MEGABYTE = 1024 * 1024;
private static final int MAX_SIZE = 5 * ONE_MEGABYTE;
private final File testDir = TestUtils.getTestDirectory();
private final Random random = new Random();
private final GroupId groupId;
private final Group group;
private final AuthorId authorId;
private final Author author;
private final AuthorId localAuthorId;
private final LocalAuthor localAuthor;
private final MessageId messageId;
private final String contentType, subject;
private final long timestamp;
private final int size;
private final byte[] raw;
private final Message message;
private final TransportId transportId;
private final ContactId contactId;
public H2DatabaseTest() throws Exception {
groupId = new GroupId(TestUtils.getRandomId());
group = new Group(groupId, "Group", new byte[GROUP_SALT_LENGTH]);
authorId = new AuthorId(TestUtils.getRandomId());
author = new Author(authorId, "Alice", new byte[MAX_PUBLIC_KEY_LENGTH]);
localAuthorId = new AuthorId(TestUtils.getRandomId());
localAuthor = new LocalAuthor(localAuthorId, "Bob",
new byte[MAX_PUBLIC_KEY_LENGTH], new byte[100], 1234);
messageId = new MessageId(TestUtils.getRandomId());
contentType = "text/plain";
subject = "Foo";
timestamp = System.currentTimeMillis();
size = 1234;
raw = new byte[size];
random.nextBytes(raw);
message = new TestMessage(messageId, null, group, author, contentType,
subject, timestamp, raw);
transportId = new TransportId("id");
contactId = new ContactId(1);
}
@Before
public void setUp() {
testDir.mkdirs();
}
@Test
public void testPersistence() throws Exception {
// Store some records
Database<Connection> db = open(false);
Connection txn = db.startTransaction();
assertFalse(db.containsContact(txn, contactId));
db.addLocalAuthor(txn, localAuthor);
assertEquals(contactId, db.addContact(txn, author, localAuthorId));
assertTrue(db.containsContact(txn, contactId));
assertFalse(db.containsGroup(txn, groupId));
db.addGroup(txn, group);
assertTrue(db.containsGroup(txn, groupId));
assertFalse(db.containsMessage(txn, messageId));
db.addMessage(txn, message, true);
assertTrue(db.containsMessage(txn, messageId));
db.commitTransaction(txn);
db.close();
// Check that the records are still there
db = open(true);
txn = db.startTransaction();
assertTrue(db.containsContact(txn, contactId));
assertTrue(db.containsGroup(txn, groupId));
assertTrue(db.containsMessage(txn, messageId));
byte[] raw1 = db.getRawMessage(txn, messageId);
assertArrayEquals(raw, raw1);
// Delete the records
db.removeMessage(txn, messageId);
db.removeContact(txn, contactId);
db.removeGroup(txn, groupId);
db.commitTransaction(txn);
db.close();
// Check that the records are gone
db = open(true);
txn = db.startTransaction();
assertFalse(db.containsContact(txn, contactId));
assertEquals(Collections.emptyMap(),
db.getRemoteProperties(txn, transportId));
assertFalse(db.containsGroup(txn, groupId));
assertFalse(db.containsMessage(txn, messageId));
db.commitTransaction(txn);
db.close();
}
@Test
public void testUnsubscribingRemovesMessage() throws Exception {
Database<Connection> db = open(false);
Connection txn = db.startTransaction();
// Subscribe to a group and store a message
db.addMessage(txn, message, true);
// Unsubscribing from the group should remove the message
assertTrue(db.containsMessage(txn, messageId));
db.removeGroup(txn, groupId);
assertFalse(db.containsMessage(txn, messageId));
db.commitTransaction(txn);
db.close();
}
@Test
public void testSendableMessagesMustHaveSeenFlagFalse() throws Exception {
Database<Connection> db = open(false);
Connection txn = db.startTransaction();
// Add a contact, subscribe to a group and store a message
db.addLocalAuthor(txn, localAuthor);
assertEquals(contactId, db.addContact(txn, author, localAuthorId));
db.addVisibility(txn, contactId, groupId);
db.setGroups(txn, contactId, Arrays.asList(group), 1);
db.addMessage(txn, message, true);
// The message has no status yet, so it should not be sendable
Collection<MessageId> ids = db.getMessagesToSend(txn, contactId,
ONE_MEGABYTE);
assertTrue(ids.isEmpty());
// Adding a status with seen = false should make the message sendable
db.addStatus(txn, contactId, messageId, false, false);
ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
assertFalse(ids.isEmpty());
Iterator<MessageId> it = ids.iterator();
assertTrue(it.hasNext());
assertEquals(messageId, it.next());
assertFalse(it.hasNext());
// Changing the status to seen = true should make the message unsendable
db.raiseSeenFlag(txn, contactId, messageId);
ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
assertTrue(ids.isEmpty());
db.commitTransaction(txn);
db.close();
}
@Test
public void testSendableMessagesMustBeSubscribed() throws Exception {
db.addLocalAuthor(txn, localAuthor);
assertEquals(contactId, db.addContact(txn, author, localAuthorId));
db.addVisibility(txn, contactId, groupId);
db.addMessage(txn, message, true);
db.addStatus(txn, contactId, messageId, false, false);
// The contact is not subscribed, so the message should not be sendable
Collection<MessageId> ids = db.getMessagesToSend(txn, contactId,
ONE_MEGABYTE);
assertTrue(ids.isEmpty());
// The contact subscribing should make the message sendable
db.setGroups(txn, contactId, Arrays.asList(group), 1);
ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
assertFalse(ids.isEmpty());
Iterator<MessageId> it = ids.iterator();
assertTrue(it.hasNext());
assertEquals(messageId, it.next());
assertFalse(it.hasNext());
// The contact unsubscribing should make the message unsendable
db.setGroups(txn, contactId, Collections.<Group>emptyList(), 2);
ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
assertTrue(ids.isEmpty());
db.commitTransaction(txn);
db.close();
}
@Test
public void testSendableMessagesMustFitCapacity() throws Exception {
Database<Connection> db = open(false);
Connection txn = db.startTransaction();
// Add a contact, subscribe to a group and store a message
db.addLocalAuthor(txn, localAuthor);
assertEquals(contactId, db.addContact(txn, author, localAuthorId));
db.addVisibility(txn, contactId, groupId);
db.setGroups(txn, contactId, Arrays.asList(group), 1);
db.addMessage(txn, message, true);
db.addStatus(txn, contactId, messageId, false, false);
// The message is sendable, but too large to send
Collection<MessageId> ids = db.getMessagesToSend(txn, contactId,
size - 1);
assertTrue(ids.isEmpty());
// The message is just the right size to send
ids = db.getMessagesToSend(txn, contactId, size);
assertFalse(ids.isEmpty());
Iterator<MessageId> it = ids.iterator();
assertTrue(it.hasNext());
assertEquals(messageId, it.next());
assertFalse(it.hasNext());
db.commitTransaction(txn);
db.close();
}
@Test
public void testSendableMessagesMustBeVisible() throws Exception {
Database<Connection> db = open(false);
Connection txn = db.startTransaction();
// Add a contact, subscribe to a group and store a message
db.addLocalAuthor(txn, localAuthor);
assertEquals(contactId, db.addContact(txn, author, localAuthorId));
db.addGroup(txn, group);
db.setGroups(txn, contactId, Arrays.asList(group), 1);
db.addMessage(txn, message, true);
db.addStatus(txn, contactId, messageId, false, false);
// The subscription is not visible to the contact, so the message
// should not be sendable
Collection<MessageId> ids = db.getMessagesToSend(txn, contactId,
ONE_MEGABYTE);
assertTrue(ids.isEmpty());
// Making the subscription visible should make the message sendable
db.addVisibility(txn, contactId, groupId);
ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
assertFalse(ids.isEmpty());
Iterator<MessageId> it = ids.iterator();
assertTrue(it.hasNext());
assertEquals(messageId, it.next());
assertFalse(it.hasNext());
db.commitTransaction(txn);
db.close();
}
@Test
public void testMessagesToAck() throws Exception {
Database<Connection> db = open(false);
Connection txn = db.startTransaction();
// Add a contact and subscribe to a group
db.addLocalAuthor(txn, localAuthor);
assertEquals(contactId, db.addContact(txn, author, localAuthorId));
db.addGroup(txn, group);
db.setGroups(txn, contactId, Arrays.asList(group), 1);
// Add some messages to ack
MessageId messageId1 = new MessageId(TestUtils.getRandomId());
Message message1 = new TestMessage(messageId1, null, group, author,
contentType, subject, timestamp, raw);
db.addMessage(txn, message, true);
db.addStatus(txn, contactId, messageId, false, true);
db.raiseAckFlag(txn, contactId, messageId);
db.addMessage(txn, message1, true);
db.addStatus(txn, contactId, messageId1, false, true);
db.raiseAckFlag(txn, contactId, messageId1);
// Both message IDs should be returned
Collection<MessageId> ids = Arrays.asList(messageId, messageId1);
assertEquals(ids, db.getMessagesToAck(txn, contactId, 1234));
// Remove both message IDs
db.lowerAckFlag(txn, contactId, Arrays.asList(messageId, messageId1));
// Both message IDs should have been removed
assertEquals(Collections.emptyList(), db.getMessagesToAck(txn,
contactId, 1234));
db.commitTransaction(txn);
db.close();
}
@Test
public void testDuplicateMessageReceived() throws Exception {
Database<Connection> db = open(false);
Connection txn = db.startTransaction();
// Add a contact and subscribe to a group
db.addLocalAuthor(txn, localAuthor);
assertEquals(contactId, db.addContact(txn, author, localAuthorId));
db.addGroup(txn, group);
db.setGroups(txn, contactId, Arrays.asList(group), 1);
// Receive the same message twice
db.addMessage(txn, message, true);
db.addStatus(txn, contactId, messageId, false, true);
db.raiseAckFlag(txn, contactId, messageId);
db.raiseAckFlag(txn, contactId, messageId);
// The message ID should only be returned once
Collection<MessageId> ids = db.getMessagesToAck(txn, contactId, 1234);
assertEquals(Arrays.asList(messageId), ids);
// Remove the message ID
db.lowerAckFlag(txn, contactId, Arrays.asList(messageId));
// The message ID should have been removed
assertEquals(Collections.emptyList(), db.getMessagesToAck(txn,
contactId, 1234));
db.commitTransaction(txn);
db.close();
}
@Test
public void testOutstandingMessageAcked() throws Exception {
Database<Connection> db = open(false);
Connection txn = db.startTransaction();
// Add a contact, subscribe to a group and store a message
db.addLocalAuthor(txn, localAuthor);
assertEquals(contactId, db.addContact(txn, author, localAuthorId));
db.addVisibility(txn, contactId, groupId);
db.setGroups(txn, contactId, Arrays.asList(group), 1);
db.addMessage(txn, message, true);
db.addStatus(txn, contactId, messageId, false, false);
// Retrieve the message from the database and mark it as sent
Iterator<MessageId> it =
db.getMessagesToSend(txn, contactId, ONE_MEGABYTE).iterator();
assertTrue(it.hasNext());
assertEquals(messageId, it.next());
assertFalse(it.hasNext());
db.updateExpiryTime(txn, contactId, messageId, Integer.MAX_VALUE);
// The message should no longer be sendable
it = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE).iterator();
assertFalse(it.hasNext());
// Pretend that the message was acked
db.raiseSeenFlag(txn, contactId, messageId);
// The message still should not be sendable
it = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE).iterator();
assertFalse(it.hasNext());
db.commitTransaction(txn);
db.close();
}
@Test
public void testGetOldMessages() throws Exception {
MessageId messageId1 = new MessageId(TestUtils.getRandomId());
Message message1 = new TestMessage(messageId1, null, group, author,
contentType, subject, timestamp + 1000, raw);
Database<Connection> db = open(false);
Connection txn = db.startTransaction();
// Subscribe to a group and store two messages
db.addMessage(txn, message, true);
db.addMessage(txn, message1, true);
// Allowing enough capacity for one message should return the older one
Iterator<MessageId> it = db.getOldMessages(txn, size).iterator();
assertTrue(it.hasNext());
assertEquals(messageId, it.next());
assertFalse(it.hasNext());
// Allowing enough capacity for both messages should return both
Collection<MessageId> ids = new HashSet<MessageId>();
for(MessageId id : db.getOldMessages(txn, size * 2)) ids.add(id);
assertEquals(2, ids.size());
assertTrue(ids.contains(messageId));
assertTrue(ids.contains(messageId1));
db.commitTransaction(txn);
db.close();
}
@Test
public void testGetFreeSpace() throws Exception {
byte[] largeBody = new byte[ONE_MEGABYTE];
for(int i = 0; i < largeBody.length; i++) largeBody[i] = (byte) i;
Message message = new TestMessage(messageId, null, group, author,
contentType, subject, timestamp, largeBody);
Database<Connection> db = open(false);
// Sanity check: there should be enough space on disk for this test
assertTrue(testDir.getFreeSpace() > MAX_SIZE);
// The free space should not be more than the allowed maximum size
long free = db.getFreeSpace();
assertTrue(free <= MAX_SIZE);
assertTrue(free > 0);
// Storing a message should reduce the free space
Connection txn = db.startTransaction();
db.addMessage(txn, message, true);
db.commitTransaction(txn);
assertTrue(db.getFreeSpace() < free);
db.close();
}
@Test
public void testCloseWaitsForCommit() throws Exception {
final CountDownLatch closing = new CountDownLatch(1);
final CountDownLatch closed = new CountDownLatch(1);
final AtomicBoolean transactionFinished = new AtomicBoolean(false);
final AtomicBoolean error = new AtomicBoolean(false);
final Database<Connection> db = open(false);
// Start a transaction
Connection txn = db.startTransaction();
// In another thread, close the database
Thread close = new Thread() {
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
public void run() {
try {
closing.countDown();
db.close();
if(!transactionFinished.get()) error.set(true);
closed.countDown();
} catch(Exception e) {
error.set(true);
}
}
};
close.start();
closing.await();
// Do whatever the transaction needs to do
Thread.sleep(10);
transactionFinished.set(true);
// Commit the transaction
db.commitTransaction(txn);
// The other thread should now terminate
assertTrue(closed.await(5, SECONDS));
// Check that the other thread didn't encounter an error
assertFalse(error.get());
}
@Test
public void testCloseWaitsForAbort() throws Exception {
final CountDownLatch closing = new CountDownLatch(1);
final CountDownLatch closed = new CountDownLatch(1);
final AtomicBoolean transactionFinished = new AtomicBoolean(false);
final AtomicBoolean error = new AtomicBoolean(false);
final Database<Connection> db = open(false);
// Start a transaction
Connection txn = db.startTransaction();
// In another thread, close the database
Thread close = new Thread() {
public void run() {
try {
closing.countDown();
db.close();
if(!transactionFinished.get()) error.set(true);
closed.countDown();
} catch(Exception e) {
error.set(true);
}
}
};
close.start();
closing.await();
// Do whatever the transaction needs to do
Thread.sleep(10);
transactionFinished.set(true);
// Abort the transaction
db.abortTransaction(txn);
// The other thread should now terminate
assertTrue(closed.await(5, SECONDS));
// Check that the other thread didn't encounter an error
assertFalse(error.get());
}
@Test
public void testUpdateRemoteTransportProperties() throws Exception {
Database<Connection> db = open(false);
Connection txn = db.startTransaction();
// Add a contact with a transport
db.addLocalAuthor(txn, localAuthor);
assertEquals(contactId, db.addContact(txn, author, localAuthorId));
TransportProperties p = new TransportProperties(
Collections.singletonMap("foo", "bar"));
db.setRemoteProperties(txn, contactId, transportId, p, 1);
assertEquals(Collections.singletonMap(contactId, p),
db.getRemoteProperties(txn, transportId));
// Replace the transport properties
TransportProperties p1 = new TransportProperties(
Collections.singletonMap("baz", "bam"));
db.setRemoteProperties(txn, contactId, transportId, p1, 2);
assertEquals(Collections.singletonMap(contactId, p1),
db.getRemoteProperties(txn, transportId));
// Remove the transport properties
TransportProperties p2 = new TransportProperties();
db.setRemoteProperties(txn, contactId, transportId, p2, 3);
assertEquals(Collections.emptyMap(),
db.getRemoteProperties(txn, transportId));
db.commitTransaction(txn);
db.close();
}
@Test
public void testUpdateLocalTransportProperties() throws Exception {
Database<Connection> db = open(false);
Connection txn = db.startTransaction();
// Add a transport to the database
db.addTransport(txn, transportId, 123);
// Set the transport properties
TransportProperties p = new TransportProperties();
p.put("foo", "foo");
p.put("bar", "bar");
db.mergeLocalProperties(txn, transportId, p);
assertEquals(p, db.getLocalProperties(txn, transportId));
assertEquals(Collections.singletonMap(transportId, p),
db.getLocalProperties(txn));
// Update one of the properties and add another
TransportProperties p1 = new TransportProperties();
p1.put("bar", "baz");
p1.put("bam", "bam");
db.mergeLocalProperties(txn, transportId, p1);
TransportProperties merged = new TransportProperties();
merged.put("foo", "foo");
merged.put("bar", "baz");
merged.put("bam", "bam");
assertEquals(merged, db.getLocalProperties(txn, transportId));
assertEquals(Collections.singletonMap(transportId, merged),
db.getLocalProperties(txn));
db.commitTransaction(txn);
db.close();
}
@Test
public void testUpdateTransportConfig() throws Exception {
Database<Connection> db = open(false);
Connection txn = db.startTransaction();
// Add a transport to the database
db.addTransport(txn, transportId, 123);
// Set the transport config
TransportConfig c = new TransportConfig();
c.put("foo", "foo");
c.put("bar", "bar");
db.mergeConfig(txn, transportId, c);
assertEquals(c, db.getConfig(txn, transportId));
// Update one of the properties and add another
TransportConfig c1 = new TransportConfig();
c1.put("bar", "baz");
c1.put("bam", "bam");
db.mergeConfig(txn, transportId, c1);
TransportConfig merged = new TransportConfig();
merged.put("foo", "foo");
merged.put("bar", "baz");
merged.put("bam", "bam");
assertEquals(merged, db.getConfig(txn, transportId));
db.commitTransaction(txn);
db.close();
}
@Test
public void testTransportsNotUpdatedIfVersionIsOld() throws Exception {
Database<Connection> db = open(false);
Connection txn = db.startTransaction();
// Add a contact
db.addLocalAuthor(txn, localAuthor);
assertEquals(contactId, db.addContact(txn, author, localAuthorId));
// Initialise the transport properties with version 1
TransportProperties p = new TransportProperties(
Collections.singletonMap("foo", "bar"));
assertTrue(db.setRemoteProperties(txn, contactId, transportId, p, 1));
assertEquals(Collections.singletonMap(contactId, p),
db.getRemoteProperties(txn, transportId));
// Replace the transport properties with version 2
TransportProperties p1 = new TransportProperties(
Collections.singletonMap("baz", "bam"));
assertTrue(db.setRemoteProperties(txn, contactId, transportId, p1, 2));
assertEquals(Collections.singletonMap(contactId, p1),
db.getRemoteProperties(txn, transportId));
// Try to replace the transport properties with version 1
TransportProperties p2 = new TransportProperties(
Collections.singletonMap("quux", "etc"));
assertFalse(db.setRemoteProperties(txn, contactId, transportId, p2, 1));
// Version 2 of the properties should still be there
assertEquals(Collections.singletonMap(contactId, p1),
db.getRemoteProperties(txn, transportId));
db.commitTransaction(txn);
db.close();
}
@Test
public void testContainsVisibleMessageRequiresMessageInDatabase()
throws Exception {
Database<Connection> db = open(false);
Connection txn = db.startTransaction();
// Add a contact and subscribe to a group
db.addLocalAuthor(txn, localAuthor);
assertEquals(contactId, db.addContact(txn, author, localAuthorId));
db.addVisibility(txn, contactId, groupId);
db.setGroups(txn, contactId, Arrays.asList(group), 1);
// The message is not in the database
assertFalse(db.containsVisibleMessage(txn, contactId, messageId));
db.commitTransaction(txn);
db.close();
}
@Test
public void testContainsVisibleMessageRequiresLocalSubscription()
throws Exception {
Database<Connection> db = open(false);
Connection txn = db.startTransaction();
// Add a contact with a subscription
db.addLocalAuthor(txn, localAuthor);
assertEquals(contactId, db.addContact(txn, author, localAuthorId));
db.setGroups(txn, contactId, Arrays.asList(group), 1);
// There's no local subscription for the group
assertFalse(db.containsVisibleMessage(txn, contactId, messageId));
db.commitTransaction(txn);
db.close();
}
@Test
public void testContainsVisibleMessageRequiresVisibileSubscription()
throws Exception {
Database<Connection> db = open(false);
Connection txn = db.startTransaction();
// Add a contact, subscribe to a group and store a message
db.addLocalAuthor(txn, localAuthor);
assertEquals(contactId, db.addContact(txn, author, localAuthorId));
db.addGroup(txn, group);
db.setGroups(txn, contactId, Arrays.asList(group), 1);
db.addMessage(txn, message, true);
db.addStatus(txn, contactId, messageId, false, false);
// The subscription is not visible
assertFalse(db.containsVisibleMessage(txn, contactId, messageId));
db.commitTransaction(txn);
db.close();
}
@Test
public void testVisibility() throws Exception {
Database<Connection> db = open(false);
Connection txn = db.startTransaction();
// Add a contact and subscribe to a group
db.addLocalAuthor(txn, localAuthor);
assertEquals(contactId, db.addContact(txn, author, localAuthorId));
// The group should not be visible to the contact
assertEquals(Collections.emptyList(), db.getVisibility(txn, groupId));
// Make the group visible to the contact
db.addVisibility(txn, contactId, groupId);
assertEquals(Arrays.asList(contactId), db.getVisibility(txn, groupId));
// Make the group invisible again
db.removeVisibility(txn, contactId, groupId);
assertEquals(Collections.emptyList(), db.getVisibility(txn, groupId));
db.commitTransaction(txn);
db.close();
}
@Test
public void testGetParentWithNoParent() throws Exception {
Database<Connection> db = open(false);
Connection txn = db.startTransaction();
// Subscribe to a group
// A message with no parent should return null
MessageId childId = new MessageId(TestUtils.getRandomId());
Message child = new TestMessage(childId, null, group, null, contentType,
subject, timestamp, raw);
db.addMessage(txn, child, true);
assertTrue(db.containsMessage(txn, childId));
assertNull(db.getParent(txn, childId));
db.commitTransaction(txn);
db.close();
}
@Test
public void testGetParentWithAbsentParent() throws Exception {
Database<Connection> db = open(false);
Connection txn = db.startTransaction();
// Subscribe to a group
// A message with an absent parent should return null
MessageId childId = new MessageId(TestUtils.getRandomId());
MessageId parentId = new MessageId(TestUtils.getRandomId());
Message child = new TestMessage(childId, parentId, group, null,
contentType, subject, timestamp, raw);
db.addMessage(txn, child, true);
assertTrue(db.containsMessage(txn, childId));
assertFalse(db.containsMessage(txn, parentId));
assertNull(db.getParent(txn, childId));
db.commitTransaction(txn);
db.close();
}
@Test
public void testGetParentWithParentInAnotherGroup() throws Exception {
GroupId groupId1 = new GroupId(TestUtils.getRandomId());
Group group1 = new Group(groupId1, "Another group",
new byte[GROUP_SALT_LENGTH]);
Database<Connection> db = open(false);
Connection txn = db.startTransaction();
// Subscribe to two groups
db.addGroup(txn, group);
db.addGroup(txn, group1);
// A message with a parent in another group should return null
MessageId childId = new MessageId(TestUtils.getRandomId());
MessageId parentId = new MessageId(TestUtils.getRandomId());
Message child = new TestMessage(childId, parentId, group, null,
contentType, subject, timestamp, raw);
Message parent = new TestMessage(parentId, null, group1, null,
contentType, subject, timestamp, raw);
db.addMessage(txn, child, true);
db.addMessage(txn, parent, true);
assertTrue(db.containsMessage(txn, childId));
assertTrue(db.containsMessage(txn, parentId));
assertNull(db.getParent(txn, childId));
db.commitTransaction(txn);
db.close();
}
@Test
public void testGetParentWithParentInSameGroup() throws Exception {
Database<Connection> db = open(false);
Connection txn = db.startTransaction();
// Subscribe to a group
// A message with a parent in the same group should return the parent
MessageId childId = new MessageId(TestUtils.getRandomId());
MessageId parentId = new MessageId(TestUtils.getRandomId());
Message child = new TestMessage(childId, parentId, group, null,
contentType, subject, timestamp, raw);
Message parent = new TestMessage(parentId, null, group, null,
contentType, subject, timestamp, raw);
db.addMessage(txn, child, true);
db.addMessage(txn, parent, true);
assertTrue(db.containsMessage(txn, childId));
assertTrue(db.containsMessage(txn, parentId));
assertEquals(parentId, db.getParent(txn, childId));
db.commitTransaction(txn);
db.close();
}
@Test
public void testGetMessageBody() throws Exception {
Database<Connection> db = open(false);
Connection txn = db.startTransaction();
// Add a contact and subscribe to a group
db.addLocalAuthor(txn, localAuthor);
assertEquals(contactId, db.addContact(txn, author, localAuthorId));
// Store a couple of messages
int bodyLength = raw.length - 20;
Message message = new TestMessage(messageId, null, group, null,
contentType, subject, timestamp, raw, 5, bodyLength);
MessageId messageId1 = new MessageId(TestUtils.getRandomId());
Message message1 = new TestMessage(messageId1, null, group, null,
contentType, subject, timestamp, raw, 10, bodyLength);
db.addMessage(txn, message, true);
db.addMessage(txn, message1, true);
// Calculate the expected message bodies
byte[] expectedBody = new byte[bodyLength];
System.arraycopy(raw, 5, expectedBody, 0, bodyLength);
assertFalse(Arrays.equals(expectedBody, new byte[bodyLength]));
byte[] expectedBody1 = new byte[bodyLength];
System.arraycopy(raw, 10, expectedBody1, 0, bodyLength);
System.arraycopy(raw, 10, expectedBody1, 0, bodyLength);
// Retrieve the raw messages
assertArrayEquals(raw, db.getRawMessage(txn, messageId));
assertArrayEquals(raw, db.getRawMessage(txn, messageId1));
// Retrieve the message bodies
byte[] body = db.getMessageBody(txn, messageId);
assertArrayEquals(expectedBody, body);
byte[] body1 = db.getMessageBody(txn, messageId1);
assertArrayEquals(expectedBody1, body1);
db.commitTransaction(txn);
db.close();
}
@Test
public void testGetMessageHeaders() throws Exception {
Database<Connection> db = open(false);
Connection txn = db.startTransaction();
// Subscribe to a group
// Store a couple of messages
db.addMessage(txn, message, true);
MessageId messageId1 = new MessageId(TestUtils.getRandomId());
MessageId parentId = new MessageId(TestUtils.getRandomId());
long timestamp1 = System.currentTimeMillis();
Message message1 = new TestMessage(messageId1, parentId, group, author,
contentType, subject, timestamp1, raw);
db.addMessage(txn, message1, true);
// Mark one of the messages read
db.setReadFlag(txn, messageId, true);
// Retrieve the message headers (order is undefined)
Collection<MessageHeader> headers = db.getMessageHeaders(txn, groupId);
assertEquals(2, headers.size());
boolean firstFound = false, secondFound = false;
for(MessageHeader header : headers) {
if(messageId.equals(header.getId())) {
assertHeadersMatch(message, header);
assertTrue(header.isRead());
firstFound = true;
} else if(messageId1.equals(header.getId())) {
assertHeadersMatch(message1, header);
assertFalse(header.isRead());
secondFound = true;
} else {
fail();
}
}
// Both the headers should have been retrieved
assertTrue(firstFound);
assertTrue(secondFound);
db.commitTransaction(txn);
db.close();
}
private void assertHeadersMatch(Message m, MessageHeader h) {
assertEquals(m.getId(), h.getId());
if(m.getParent() == null) assertNull(h.getParent());
else assertEquals(m.getParent(), h.getParent());
assertEquals(m.getGroup().getId(), h.getGroupId());
if(m.getAuthor() == null) assertNull(h.getAuthor());
else assertEquals(m.getAuthor(), h.getAuthor());
assertEquals(m.getContentType(), h.getContentType());
assertEquals(m.getTimestamp(), h.getTimestamp());
}
@Test
public void testAuthorStatus() throws Exception {
Database<Connection> db = open(false);
Connection txn = db.startTransaction();
// Add a contact and subscribe to a group
db.addLocalAuthor(txn, localAuthor);
assertEquals(contactId, db.addContact(txn, author, localAuthorId));
db.addGroup(txn, group);
// Store a message from the contact - status VERIFIED
db.addMessage(txn, message, true);
AuthorId authorId1 = new AuthorId(TestUtils.getRandomId());
// Store a message from an unknown author - status UNKNOWN
Author author1 = new Author(authorId1, "Bob",
new byte[MAX_PUBLIC_KEY_LENGTH]);
MessageId messageId1 = new MessageId(TestUtils.getRandomId());
Message message1 = new TestMessage(messageId1, null, group, author1,
contentType, subject, timestamp, raw);
db.addMessage(txn, message1, true);
// Store an anonymous message - status ANONYMOUS
MessageId messageId2 = new MessageId(TestUtils.getRandomId());
Message message2 = new TestMessage(messageId2, null, group, null,
contentType, subject, timestamp, raw);
db.addMessage(txn, message2, true);
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
// Retrieve the message headers (order is undefined)
Collection<MessageHeader> headers = db.getMessageHeaders(txn, groupId);
assertEquals(3, headers.size());
boolean firstFound = false, secondFound = false, thirdFound = false;
for(MessageHeader header : headers) {
if(messageId.equals(header.getId())) {
assertHeadersMatch(message, header);
assertEquals(Author.Status.VERIFIED, header.getAuthorStatus());
firstFound = true;
} else if(messageId1.equals(header.getId())) {
assertHeadersMatch(message1, header);
assertEquals(Author.Status.UNKNOWN, header.getAuthorStatus());
secondFound = true;
} else if(messageId2.equals(header.getId())) {
assertHeadersMatch(message2, header);
assertEquals(Author.Status.ANONYMOUS, header.getAuthorStatus());
thirdFound = true;
} else {
fail();
}
}
// All of the headers should have been retrieved
assertTrue(firstFound);
assertTrue(secondFound);
assertTrue(thirdFound);
db.commitTransaction(txn);
db.close();
}
@Test
public void testReadFlag() throws Exception {
Database<Connection> db = open(false);
Connection txn = db.startTransaction();
// Subscribe to a group and store a message
db.addMessage(txn, message, true);
// The message should be unread by default
assertFalse(db.getReadFlag(txn, messageId));
// Mark the message read
db.setReadFlag(txn, messageId, true);
// The message should be read
assertTrue(db.getReadFlag(txn, messageId));
// Mark the message unread
db.setReadFlag(txn, messageId, false);
// The message should be unread
assertFalse(db.getReadFlag(txn, messageId));
db.commitTransaction(txn);
db.close();
}
@Test
public void testGetUnreadMessageCounts() throws Exception {
Database<Connection> db = open(false);
Connection txn = db.startTransaction();
// Subscribe to a couple of groups
GroupId groupId1 = new GroupId(TestUtils.getRandomId());
Group group1 = new Group(groupId1, "Another group",
new byte[GROUP_SALT_LENGTH]);
// Store two messages in the first group
db.addMessage(txn, message, true);
MessageId messageId1 = new MessageId(TestUtils.getRandomId());
Message message1 = new TestMessage(messageId1, null, group, author,
contentType, subject, timestamp, raw);
db.addMessage(txn, message1, true);
// Store one message in the second group
MessageId messageId2 = new MessageId(TestUtils.getRandomId());
Message message2 = new TestMessage(messageId2, null, group1, author,
contentType, subject, timestamp, raw);
db.addMessage(txn, message2, true);
// Mark one of the messages in the first group read
db.setReadFlag(txn, messageId, true);
// There should be one unread message in each group
Map<GroupId, Integer> counts = db.getUnreadMessageCounts(txn);
assertEquals(2, counts.size());
Integer count = counts.get(groupId);
assertNotNull(count);
assertEquals(1, count.intValue());
count = counts.get(groupId1);
assertNotNull(count);
assertEquals(1, count.intValue());
// Mark the read message unread
db.setReadFlag(txn, messageId, false);
// Mark the message in the second group read
db.setReadFlag(txn, messageId2, true);
// There should be two unread messages in the first group, none in
// the second group
counts = db.getUnreadMessageCounts(txn);
assertEquals(1, counts.size());
count = counts.get(groupId);
assertNotNull(count);
assertEquals(2, count.intValue());
db.commitTransaction(txn);
db.close();
}
@Test
public void testMultipleSubscriptionsAndUnsubscriptions() throws Exception {
// Create some groups
List<Group> groups = new ArrayList<Group>();
for(int i = 0; i < 100; i++) {
GroupId id = new GroupId(TestUtils.getRandomId());
String name = "Group " + i;
groups.add(new Group(id, name, new byte[GROUP_SALT_LENGTH]));
}
Database<Connection> db = open(false);
Connection txn = db.startTransaction();
// Add a contact and subscribe to the groups
db.addLocalAuthor(txn, localAuthor);
assertEquals(contactId, db.addContact(txn, author, localAuthorId));
for(Group g : groups) db.addGroup(txn, g);
// Make the groups visible to the contact
Collections.shuffle(groups);
for(Group g : groups) db.addVisibility(txn, contactId, g.getId());
// Make some of the groups invisible to the contact and remove them all
Collections.shuffle(groups);
for(Group g : groups) {
if(Math.random() < 0.5)
db.removeVisibility(txn, contactId, g.getId());
db.removeGroup(txn, g.getId());
}
db.commitTransaction(txn);
db.close();
}
@Test
public void testTemporarySecrets() throws Exception {
// Create an endpoint and four consecutive temporary secrets
long epoch = 123;
int latency = 234;
boolean alice = false;
long outgoing1 = 345, centre1 = 456;
long outgoing2 = 567, centre2 = 678;
long outgoing3 = 789, centre3 = 890;
long outgoing4 = 901, centre4 = 123;
Endpoint ep = new Endpoint(contactId, transportId, epoch, alice);
Random random = new Random();
byte[] secret1 = new byte[32], bitmap1 = new byte[4];
random.nextBytes(secret1);
random.nextBytes(bitmap1);
TemporarySecret s1 = new TemporarySecret(contactId, transportId, epoch,
alice, 0, secret1, outgoing1, centre1, bitmap1);
byte[] secret2 = new byte[32], bitmap2 = new byte[4];
random.nextBytes(secret2);
random.nextBytes(bitmap2);
TemporarySecret s2 = new TemporarySecret(contactId, transportId, epoch,
alice, 1, secret2, outgoing2, centre2, bitmap2);
byte[] secret3 = new byte[32], bitmap3 = new byte[4];
random.nextBytes(secret3);
random.nextBytes(bitmap3);
TemporarySecret s3 = new TemporarySecret(contactId, transportId, epoch,
alice, 2, secret3, outgoing3, centre3, bitmap3);
byte[] secret4 = new byte[32], bitmap4 = new byte[4];
random.nextBytes(secret4);
random.nextBytes(bitmap4);
TemporarySecret s4 = new TemporarySecret(contactId, transportId, epoch,
alice, 3, secret4, outgoing4, centre4, bitmap4);
Database<Connection> db = open(false);
Connection txn = db.startTransaction();
// Initially there should be no secrets in the database
assertEquals(Collections.emptyList(), db.getSecrets(txn));
// Add the contact, the transport, the endpoint and the first three
// secrets (periods 0, 1 and 2)
db.addLocalAuthor(txn, localAuthor);
assertEquals(contactId, db.addContact(txn, author, localAuthorId));
db.addTransport(txn, transportId, latency);
db.addEndpoint(txn, ep);
db.addSecrets(txn, Arrays.asList(s1, s2, s3));
// Retrieve the first three secrets
Collection<TemporarySecret> secrets = db.getSecrets(txn);
assertEquals(3, secrets.size());
boolean foundFirst = false, foundSecond = false, foundThird = false;
for(TemporarySecret s : secrets) {
assertEquals(contactId, s.getContactId());
assertEquals(transportId, s.getTransportId());
assertEquals(epoch, s.getEpoch());
assertEquals(alice, s.getAlice());
if(s.getPeriod() == 0) {
assertArrayEquals(secret1, s.getSecret());
assertEquals(outgoing1, s.getOutgoingStreamCounter());
assertEquals(centre1, s.getWindowCentre());
assertArrayEquals(bitmap1, s.getWindowBitmap());
foundFirst = true;
} else if(s.getPeriod() == 1) {
assertArrayEquals(secret2, s.getSecret());
assertEquals(outgoing2, s.getOutgoingStreamCounter());
assertEquals(centre2, s.getWindowCentre());
assertArrayEquals(bitmap2, s.getWindowBitmap());
foundSecond = true;
} else if(s.getPeriod() == 2) {
assertArrayEquals(secret3, s.getSecret());
assertEquals(outgoing3, s.getOutgoingStreamCounter());
assertEquals(centre3, s.getWindowCentre());
assertArrayEquals(bitmap3, s.getWindowBitmap());
foundThird = true;
} else {
fail();
}
}
assertTrue(foundFirst);
assertTrue(foundSecond);
assertTrue(foundThird);
// Adding the fourth secret (period 3) should delete the first
db.addSecrets(txn, Arrays.asList(s4));
secrets = db.getSecrets(txn);
assertEquals(3, secrets.size());
foundSecond = foundThird = false;
boolean foundFourth = false;
for(TemporarySecret s : secrets) {
assertEquals(contactId, s.getContactId());
assertEquals(transportId, s.getTransportId());
assertEquals(epoch, s.getEpoch());
assertEquals(alice, s.getAlice());
if(s.getPeriod() == 1) {
assertArrayEquals(secret2, s.getSecret());
assertEquals(outgoing2, s.getOutgoingStreamCounter());
assertEquals(centre2, s.getWindowCentre());
assertArrayEquals(bitmap2, s.getWindowBitmap());
foundSecond = true;
} else if(s.getPeriod() == 2) {
assertArrayEquals(secret3, s.getSecret());
assertEquals(outgoing3, s.getOutgoingStreamCounter());
assertEquals(centre3, s.getWindowCentre());
assertArrayEquals(bitmap3, s.getWindowBitmap());
foundThird = true;
} else if(s.getPeriod() == 3) {
assertArrayEquals(secret4, s.getSecret());
assertEquals(outgoing4, s.getOutgoingStreamCounter());
assertEquals(centre4, s.getWindowCentre());
assertArrayEquals(bitmap4, s.getWindowBitmap());
foundFourth = true;
} else {
fail();
}
}
assertTrue(foundSecond);
assertTrue(foundThird);
assertTrue(foundFourth);
// Removing the contact should remove the secrets
db.removeContact(txn, contactId);
assertEquals(Collections.emptyList(), db.getSecrets(txn));
db.commitTransaction(txn);
db.close();
}
@Test
public void testIncrementStreamCounter() throws Exception {
// Create an endpoint and a temporary secret
long epoch = 123;
int latency = 234;
boolean alice = false;
long period = 345, outgoing = 456, centre = 567;
Endpoint ep = new Endpoint(contactId, transportId, epoch, alice);
Random random = new Random();
byte[] secret = new byte[32], bitmap = new byte[4];
random.nextBytes(secret);
TemporarySecret s = new TemporarySecret(contactId, transportId, epoch,
alice, period, secret, outgoing, centre, bitmap);
Database<Connection> db = open(false);
Connection txn = db.startTransaction();
// Add the contact, transport, endpoint and temporary secret
db.addLocalAuthor(txn, localAuthor);
assertEquals(contactId, db.addContact(txn, author, localAuthorId));
db.addTransport(txn, transportId, latency);
db.addEndpoint(txn, ep);
db.addSecrets(txn, Arrays.asList(s));
// Retrieve the secret
Collection<TemporarySecret> secrets = db.getSecrets(txn);
assertEquals(1, secrets.size());
s = secrets.iterator().next();
assertEquals(contactId, s.getContactId());
assertEquals(transportId, s.getTransportId());
assertEquals(period, s.getPeriod());
assertArrayEquals(secret, s.getSecret());
assertEquals(outgoing, s.getOutgoingStreamCounter());
assertEquals(centre, s.getWindowCentre());
assertArrayEquals(bitmap, s.getWindowBitmap());
// Increment the stream counter twice and retrieve the secret again
assertEquals(outgoing, db.incrementStreamCounter(txn,
s.getContactId(), s.getTransportId(), s.getPeriod()));
assertEquals(outgoing + 1, db.incrementStreamCounter(txn,
s.getContactId(), s.getTransportId(), s.getPeriod()));
secrets = db.getSecrets(txn);
assertEquals(1, secrets.size());
s = secrets.iterator().next();
assertEquals(contactId, s.getContactId());
assertEquals(transportId, s.getTransportId());
assertEquals(period, s.getPeriod());
assertArrayEquals(secret, s.getSecret());
assertEquals(outgoing + 2, s.getOutgoingStreamCounter());
assertEquals(centre, s.getWindowCentre());
assertArrayEquals(bitmap, s.getWindowBitmap());
db.commitTransaction(txn);
db.close();
}
@Test
public void testSetReorderingWindow() throws Exception {
// Create an endpoint and a temporary secret
long epoch = 123;
int latency = 234;
boolean alice = false;
long period = 345, outgoing = 456, centre = 567;
Endpoint ep = new Endpoint(contactId, transportId, epoch, alice);
Random random = new Random();
byte[] secret = new byte[32], bitmap = new byte[4];
random.nextBytes(secret);
TemporarySecret s = new TemporarySecret(contactId, transportId, epoch,
alice, period, secret, outgoing, centre, bitmap);
Database<Connection> db = open(false);
Connection txn = db.startTransaction();
// Add the contact, transport, endpoint and temporary secret
db.addLocalAuthor(txn, localAuthor);
assertEquals(contactId, db.addContact(txn, author, localAuthorId));
db.addTransport(txn, transportId, latency);
db.addEndpoint(txn, ep);
db.addSecrets(txn, Arrays.asList(s));
// Retrieve the secret
Collection<TemporarySecret> secrets = db.getSecrets(txn);
assertEquals(1, secrets.size());
s = secrets.iterator().next();
assertEquals(contactId, s.getContactId());
assertEquals(transportId, s.getTransportId());
assertEquals(period, s.getPeriod());
assertArrayEquals(secret, s.getSecret());
assertEquals(outgoing, s.getOutgoingStreamCounter());
assertEquals(centre, s.getWindowCentre());
assertArrayEquals(bitmap, s.getWindowBitmap());
// Update the reordering window and retrieve the secret again
random.nextBytes(bitmap);
db.setReorderingWindow(txn, contactId, transportId, period, centre,
bitmap);
secrets = db.getSecrets(txn);
assertEquals(1, secrets.size());
s = secrets.iterator().next();
assertEquals(contactId, s.getContactId());
assertEquals(transportId, s.getTransportId());
assertEquals(period, s.getPeriod());
assertArrayEquals(secret, s.getSecret());
assertEquals(outgoing, s.getOutgoingStreamCounter());
assertEquals(centre, s.getWindowCentre());
assertArrayEquals(bitmap, s.getWindowBitmap());
// Updating a nonexistent window should not throw an exception
db.setReorderingWindow(txn, contactId, transportId, period + 1, 1,
bitmap);
// The nonexistent window should not have been created
secrets = db.getSecrets(txn);
assertEquals(1, secrets.size());
s = secrets.iterator().next();
assertEquals(contactId, s.getContactId());
assertEquals(transportId, s.getTransportId());
assertEquals(period, s.getPeriod());
assertArrayEquals(secret, s.getSecret());
assertEquals(outgoing, s.getOutgoingStreamCounter());
assertEquals(centre, s.getWindowCentre());
assertArrayEquals(bitmap, s.getWindowBitmap());
db.commitTransaction(txn);
db.close();
}
@Test
public void testEndpoints() throws Exception {
// Create some endpoints
long epoch1 = 123, epoch2 = 234;
int latency1 = 345, latency2 = 456;
boolean alice1 = true, alice2 = false;
TransportId transportId1 = new TransportId("bar");
TransportId transportId2 = new TransportId("baz");
Endpoint ep1 = new Endpoint(contactId, transportId1, epoch1, alice1);
Endpoint ep2 = new Endpoint(contactId, transportId2, epoch2, alice2);
Database<Connection> db = open(false);
Connection txn = db.startTransaction();
// Initially there should be no endpoints in the database
assertEquals(Collections.emptyList(), db.getEndpoints(txn));
// Add the contact, the transports and the endpoints
db.addLocalAuthor(txn, localAuthor);
assertEquals(contactId, db.addContact(txn, author, localAuthorId));
db.addTransport(txn, transportId1, latency1);
db.addTransport(txn, transportId2, latency2);
db.addEndpoint(txn, ep1);
db.addEndpoint(txn, ep2);
// Retrieve the endpoints
Collection<Endpoint> endpoints = db.getEndpoints(txn);
assertEquals(2, endpoints.size());
boolean foundFirst = false, foundSecond = false;
for(Endpoint ep : endpoints) {
assertEquals(contactId, ep.getContactId());
if(ep.getTransportId().equals(transportId1)) {
assertEquals(epoch1, ep.getEpoch());
assertEquals(alice1, ep.getAlice());
foundFirst = true;
} else if(ep.getTransportId().equals(transportId2)) {
assertEquals(epoch2, ep.getEpoch());
assertEquals(alice2, ep.getAlice());
foundSecond = true;
} else {
fail();
}
}
assertTrue(foundFirst);
assertTrue(foundSecond);
// Removing the contact should remove the endpoints
db.removeContact(txn, contactId);
assertEquals(Collections.emptyList(), db.getEndpoints(txn));
db.commitTransaction(txn);
db.close();
}
@Test
public void testGetAvailableGroups() throws Exception {
ContactId contactId1 = new ContactId(2);
AuthorId authorId1 = new AuthorId(TestUtils.getRandomId());
Author author1 = new Author(authorId1, "Carol",
new byte[MAX_PUBLIC_KEY_LENGTH]);
Database<Connection> db = open(false);
Connection txn = db.startTransaction();
// Add two contacts who subscribe to a group
db.addLocalAuthor(txn, localAuthor);
assertEquals(contactId, db.addContact(txn, author, localAuthorId));
assertEquals(contactId1, db.addContact(txn, author1, localAuthorId));
db.setGroups(txn, contactId, Arrays.asList(group), 1);
db.setGroups(txn, contactId1, Arrays.asList(group), 1);
assertEquals(Collections.emptyList(), db.getGroups(txn));
assertEquals(Arrays.asList(group), db.getAvailableGroups(txn));
// Subscribe to the group - it should no longer be available
db.addGroup(txn, group);
assertEquals(Arrays.asList(group), db.getGroups(txn));
assertEquals(Collections.emptyList(), db.getAvailableGroups(txn));
// Unsubscribe from the group - it should be available again
db.removeGroup(txn, groupId);
assertEquals(Collections.emptyList(), db.getGroups(txn));
assertEquals(Arrays.asList(group), db.getAvailableGroups(txn));
// The first contact unsubscribes - it should still be available
db.setGroups(txn, contactId, Collections.<Group>emptyList(), 2);
assertEquals(Collections.emptyList(), db.getGroups(txn));
assertEquals(Arrays.asList(group), db.getAvailableGroups(txn));
// The second contact unsubscribes - it should no longer be available
db.setGroups(txn, contactId1, Collections.<Group>emptyList(), 2);
assertEquals(Collections.emptyList(), db.getGroups(txn));
assertEquals(Collections.emptyList(), db.getAvailableGroups(txn));
db.commitTransaction(txn);
db.close();
}
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
@Test
public void testGetContactsByLocalAuthorId() throws Exception {
Database<Connection> db = open(false);
Connection txn = db.startTransaction();
// Add a local author - no contacts should be associated
db.addLocalAuthor(txn, localAuthor);
Collection<ContactId> contacts = db.getContacts(txn, localAuthorId);
assertEquals(Collections.emptyList(), contacts);
// Add a contact associated with the local author
assertEquals(contactId, db.addContact(txn, author, localAuthorId));
contacts = db.getContacts(txn, localAuthorId);
assertEquals(Collections.singletonList(contactId), contacts);
// Remove the local author - the contact should be removed
db.removeLocalAuthor(txn, localAuthorId);
contacts = db.getContacts(txn, localAuthorId);
assertEquals(Collections.emptyList(), contacts);
assertFalse(db.containsContact(txn, contactId));
db.commitTransaction(txn);
db.close();
}
@Test
public void testGetInboxMessageHeaders() throws Exception {
Database<Connection> db = open(false);
Connection txn = db.startTransaction();
// Add a contact and an inbox group - no headers should be returned
db.addLocalAuthor(txn, localAuthor);
assertEquals(contactId, db.addContact(txn, author, localAuthorId));
db.addGroup(txn, group);
db.setInboxGroup(txn, contactId, group);
assertEquals(Collections.emptyList(),
db.getInboxMessageHeaders(txn, contactId));
// Add a message to the inbox group - the header should be returned
boolean local = true, seen = false;
db.addMessage(txn, message, local);
db.addStatus(txn, contactId, messageId, false, seen);
Collection<MessageHeader> headers =
db.getInboxMessageHeaders(txn, contactId);
assertEquals(1, headers.size());
MessageHeader header = headers.iterator().next();
assertEquals(messageId, header.getId());
assertNull(header.getParent());
assertEquals(groupId, header.getGroupId());
assertEquals(localAuthor, header.getAuthor());
assertEquals(contentType, header.getContentType());
assertEquals(timestamp, header.getTimestamp());
assertEquals(local, header.isLocal());
assertEquals(false, header.isRead());
assertEquals(seen, header.isDelivered());
assertFalse(header.isRead());
db.commitTransaction(txn);
db.close();
}
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
@Test
public void testOfferedMessages() throws Exception {
Database<Connection> db = open(false);
Connection txn = db.startTransaction();
// Add a contact - initially there should be no offered messages
db.addLocalAuthor(txn, localAuthor);
assertEquals(contactId, db.addContact(txn, author, localAuthorId));
assertEquals(0, db.countOfferedMessages(txn, contactId));
// Add some offered messages and count them
List<MessageId> ids = new ArrayList<MessageId>();
for(int i = 0; i < 10; i++) {
MessageId m = new MessageId(TestUtils.getRandomId());
db.addOfferedMessage(txn, contactId, m);
ids.add(m);
}
assertEquals(10, db.countOfferedMessages(txn, contactId));
// Remove some of the offered messages and count again
List<MessageId> half = ids.subList(0, 5);
db.removeOfferedMessages(txn, contactId, half);
assertTrue(db.removeOfferedMessage(txn, contactId, ids.get(5)));
assertEquals(4, db.countOfferedMessages(txn, contactId));
db.commitTransaction(txn);
db.close();
}
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
@Test
public void testContactUnsubscribingResetsMessageStatus() throws Exception {
Database<Connection> db = open(false);
Connection txn = db.startTransaction();
// Add a contact who subscribes to a group
db.addLocalAuthor(txn, localAuthor);
assertEquals(contactId, db.addContact(txn, author, localAuthorId));
db.setGroups(txn, contactId, Arrays.asList(group), 1);
// Subscribe to the group and make it visible to the contact
db.addGroup(txn, group);
db.addVisibility(txn, contactId, groupId);
// Add a message - it should be sendable to the contact
db.addMessage(txn, message, true);
db.addStatus(txn, contactId, messageId, false, false);
Collection<MessageId> sendable = db.getMessagesToSend(txn, contactId,
ONE_MEGABYTE);
assertEquals(Arrays.asList(messageId), sendable);
// Mark the message as seen - it should no longer be sendable
db.raiseSeenFlag(txn, contactId, messageId);
sendable = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
assertEquals(Collections.emptyList(), sendable);
// The contact unsubscribes - the message should not be sendable
db.setGroups(txn, contactId, Collections.<Group>emptyList(), 2);
sendable = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
assertEquals(Collections.emptyList(), sendable);
// The contact resubscribes - the message should be sendable again
db.setGroups(txn, contactId, Arrays.asList(group), 3);
sendable = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
assertEquals(Arrays.asList(messageId), sendable);
db.commitTransaction(txn);
db.close();
}
@Test
public void testExceptionHandling() throws Exception {
Database<Connection> db = open(false);
Connection txn = db.startTransaction();
try {
// Ask for a nonexistent message - an exception should be thrown
db.getRawMessage(txn, messageId);
fail();
} catch(DbException expected) {
// It should be possible to abort the transaction without error
db.abortTransaction(txn);
}
// It should be possible to close the database cleanly
db.close();
}
private Database<Connection> open(boolean resume) throws Exception {
Database<Connection> db = new H2Database(new TestDatabaseConfig(testDir,
MAX_SIZE), new TestFileUtils(), new SystemClock());
if(!resume) TestUtils.deleteTestDirectory(testDir);
db.open();
return db;
}