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.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.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.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.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.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);
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
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
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
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.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));
// 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, 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
// 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
// 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));
// 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
// 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());
}
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
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
@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);