Forked from
briar / briar
6297 commits behind the upstream repository.
H2DatabaseTest.java 55.01 KiB
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.GroupStatus;
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]);
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.addGroup(txn, group);
db.addMessage(txn, message, false);
// 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.addGroup(txn, group);
db.addVisibility(txn, contactId, groupId);
db.setGroups(txn, contactId, Arrays.asList(group), 1);
db.addMessage(txn, message, false);
// 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 {
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.addVisibility(txn, contactId, groupId);
db.addMessage(txn, message, false);
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.addGroup(txn, group);
db.addVisibility(txn, contactId, groupId);
db.setGroups(txn, contactId, Arrays.asList(group), 1);
db.addMessage(txn, message, false);
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, false);
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, false);
db.addStatus(txn, contactId, messageId, false, true);
db.raiseAckFlag(txn, contactId, messageId);
db.addMessage(txn, message1, false);
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, false);
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.addGroup(txn, group);
db.addVisibility(txn, contactId, groupId);
db.setGroups(txn, contactId, Arrays.asList(group), 1);
db.addMessage(txn, message, false);
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, Long.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.addGroup(txn, group);
db.addMessage(txn, message, false);
db.addMessage(txn, message1, false);
// 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.addGroup(txn, group);
db.addMessage(txn, message, false);
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() {
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.addGroup(txn, group);
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, false);
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));
db.addGroup(txn, group);
// 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
db.addGroup(txn, 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, false);
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
db.addGroup(txn, 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, false);
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, false);
db.addMessage(txn, parent, false);
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
db.addGroup(txn, 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, false);
db.addMessage(txn, parent, false);
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));
db.addGroup(txn, group);
// 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, false);
db.addMessage(txn, message1, false);
// 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
db.addGroup(txn, group);
// Store a couple of messages
db.addMessage(txn, message, false);
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, false);
// 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, false);
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, false);
// 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, false);
// 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.addGroup(txn, group);
db.addMessage(txn, message, false);
// 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
db.addGroup(txn, group);
GroupId groupId1 = new GroupId(TestUtils.getRandomId());
Group group1 = new Group(groupId1, "Another group",
new byte[GROUP_SALT_LENGTH]);
db.addGroup(txn, group1);
// Store two messages in the first group
db.addMessage(txn, message, false);
MessageId messageId1 = new MessageId(TestUtils.getRandomId());
Message message1 = new TestMessage(messageId1, null, group, author,
contentType, subject, timestamp, raw);
db.addMessage(txn, message1, false);
// 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, false);
// 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, 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.getOutgoingConnectionCounter());
assertEquals(centre1, s.getWindowCentre());
assertArrayEquals(bitmap1, s.getWindowBitmap());
foundFirst = true;
} else if(s.getPeriod() == 1) {
assertArrayEquals(secret2, s.getSecret());
assertEquals(outgoing2, s.getOutgoingConnectionCounter());
assertEquals(centre2, s.getWindowCentre());
assertArrayEquals(bitmap2, s.getWindowBitmap());
foundSecond = true;
} else if(s.getPeriod() == 2) {
assertArrayEquals(secret3, s.getSecret());
assertEquals(outgoing3, s.getOutgoingConnectionCounter());
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.getOutgoingConnectionCounter());
assertEquals(centre2, s.getWindowCentre());
assertArrayEquals(bitmap2, s.getWindowBitmap());
foundSecond = true;
} else if(s.getPeriod() == 2) {
assertArrayEquals(secret3, s.getSecret());
assertEquals(outgoing3, s.getOutgoingConnectionCounter());
assertEquals(centre3, s.getWindowCentre());
assertArrayEquals(bitmap3, s.getWindowBitmap());
foundThird = true;
} else if(s.getPeriod() == 3) {
assertArrayEquals(secret4, s.getSecret());
assertEquals(outgoing4, s.getOutgoingConnectionCounter());
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 testIncrementConnectionCounter() throws Exception {
// Create an endpoint and a temporary secret
long epoch = 123, 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, the transport, the endpoint and the 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.getOutgoingConnectionCounter());
assertEquals(centre, s.getWindowCentre());
assertArrayEquals(bitmap, s.getWindowBitmap());
// Increment the connection counter twice and retrieve the secret again
assertEquals(outgoing, db.incrementConnectionCounter(txn,
s.getContactId(), s.getTransportId(), s.getPeriod()));
assertEquals(outgoing + 1, db.incrementConnectionCounter(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.getOutgoingConnectionCounter());
assertEquals(centre, s.getWindowCentre());
assertArrayEquals(bitmap, s.getWindowBitmap());
db.commitTransaction(txn);
db.close();
}
@Test
public void testSetConnectionWindow() throws Exception {
// Create an endpoint and a temporary secret
long epoch = 123, 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, the transport, the endpoint and the 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.getOutgoingConnectionCounter());
assertEquals(centre, s.getWindowCentre());
assertArrayEquals(bitmap, s.getWindowBitmap());
// Update the connection window and retrieve the secret again
random.nextBytes(bitmap);
db.setConnectionWindow(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.getOutgoingConnectionCounter());
assertEquals(centre, s.getWindowCentre());
assertArrayEquals(bitmap, s.getWindowBitmap());
// Updating a nonexistent window should not throw an exception
db.setConnectionWindow(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.getOutgoingConnectionCounter());
assertEquals(centre, s.getWindowCentre());
assertArrayEquals(bitmap, s.getWindowBitmap());
db.commitTransaction(txn);
db.close();
}
@Test
public void testContactTransports() throws Exception {
// Create some endpoints
long epoch1 = 123, latency1 = 234;
long epoch2 = 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 contact transports
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 contact transports
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);
// The group should be available, not subscribed, not visible to all
assertEquals(Collections.emptyList(), db.getGroups(txn));
Iterator<GroupStatus> it = db.getAvailableGroups(txn).iterator();
assertTrue(it.hasNext());
GroupStatus status = it.next();
assertEquals(group, status.getGroup());
assertFalse(status.isSubscribed());
assertFalse(status.isVisibleToAll());
assertFalse(it.hasNext());
// Subscribe to the group - it should be available, subscribed,
// not visible to all
db.addGroup(txn, group);
assertEquals(Arrays.asList(group), db.getGroups(txn));
it = db.getAvailableGroups(txn).iterator();
assertTrue(it.hasNext());
status = it.next();
assertEquals(group, status.getGroup());
assertTrue(status.isSubscribed());
assertFalse(status.isVisibleToAll());
assertFalse(it.hasNext());
// The first contact unsubscribes - the group should be available,
// subscribed, not visible to all
db.setGroups(txn, contactId, Collections.<Group>emptyList(), 2);
assertEquals(Arrays.asList(group), db.getGroups(txn));
it = db.getAvailableGroups(txn).iterator();
assertTrue(it.hasNext());
status = it.next();
assertEquals(group, status.getGroup());
assertTrue(status.isSubscribed());
assertFalse(status.isVisibleToAll());
assertFalse(it.hasNext());
// Make the group visible to all contacts - it should be available,
// subscribed, visible to all
db.setVisibleToAll(txn, groupId, true);
assertEquals(Arrays.asList(group), db.getGroups(txn));
it = db.getAvailableGroups(txn).iterator();
assertTrue(it.hasNext());
status = it.next();
assertEquals(group, status.getGroup());
assertTrue(status.isSubscribed());
assertTrue(status.isVisibleToAll());
assertFalse(it.hasNext());
// Unsubscribe from the group - it should be available, not subscribed,
// not visible to all
db.removeGroup(txn, groupId);
assertEquals(Collections.emptyList(), db.getGroups(txn));
it = db.getAvailableGroups(txn).iterator();
assertTrue(it.hasNext());
status = it.next();
assertEquals(group, status.getGroup());
assertFalse(status.isSubscribed());
assertFalse(status.isVisibleToAll());
assertFalse(it.hasNext());
// The second contact unsubscribes - the group 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();
}
@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
db.addMessage(txn, message, false);
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());
assertFalse(header.isRead());
db.commitTransaction(txn);
db.close();
}
@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();
}
@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;
}
@After
public void tearDown() {
TestUtils.deleteTestDirectory(testDir);
}
}