diff --git a/briar-android-tests/src/test/java/org/briarproject/BlogManagerTest.java b/briar-android-tests/src/test/java/org/briarproject/BlogManagerTest.java new file mode 100644 index 0000000000000000000000000000000000000000..e6f667ad4833a278294bcba35335038c1464a336 --- /dev/null +++ b/briar-android-tests/src/test/java/org/briarproject/BlogManagerTest.java @@ -0,0 +1,387 @@ +package org.briarproject; + +import net.jodah.concurrentunit.Waiter; + +import org.briarproject.api.blogs.Blog; +import org.briarproject.api.blogs.BlogFactory; +import org.briarproject.api.blogs.BlogManager; +import org.briarproject.api.blogs.BlogPost; +import org.briarproject.api.blogs.BlogPostFactory; +import org.briarproject.api.blogs.BlogPostHeader; +import org.briarproject.api.contact.ContactId; +import org.briarproject.api.contact.ContactManager; +import org.briarproject.api.crypto.CryptoComponent; +import org.briarproject.api.crypto.KeyPair; +import org.briarproject.api.crypto.SecretKey; +import org.briarproject.api.db.DbException; +import org.briarproject.api.event.Event; +import org.briarproject.api.event.EventListener; +import org.briarproject.api.event.MessageStateChangedEvent; +import org.briarproject.api.identity.AuthorFactory; +import org.briarproject.api.identity.IdentityManager; +import org.briarproject.api.identity.LocalAuthor; +import org.briarproject.api.lifecycle.LifecycleManager; +import org.briarproject.api.sync.SyncSession; +import org.briarproject.api.sync.SyncSessionFactory; +import org.briarproject.api.system.Clock; +import org.briarproject.blogs.BlogsModule; +import org.briarproject.contact.ContactModule; +import org.briarproject.crypto.CryptoModule; +import org.briarproject.lifecycle.LifecycleModule; +import org.briarproject.properties.PropertiesModule; +import org.briarproject.sync.SyncModule; +import org.briarproject.transport.TransportModule; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.util.Collection; +import java.util.Iterator; +import java.util.concurrent.TimeoutException; +import java.util.logging.Logger; + +import javax.inject.Inject; + +import static junit.framework.Assert.assertFalse; +import static org.briarproject.TestPluginsModule.MAX_LATENCY; +import static org.briarproject.api.sync.ValidationManager.State.DELIVERED; +import static org.briarproject.api.sync.ValidationManager.State.INVALID; +import static org.briarproject.api.sync.ValidationManager.State.PENDING; +import static org.briarproject.api.sync.ValidationManager.State.VALID; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class BlogManagerTest { + + private LifecycleManager lifecycleManager0, lifecycleManager1; + private SyncSessionFactory sync0, sync1; + private BlogManager blogManager0, blogManager1; + private ContactManager contactManager0, contactManager1; + private ContactId contactId0,contactId1; + private IdentityManager identityManager0, identityManager1; + private LocalAuthor author0, author1; + private Blog blog0, blog1; + + @Inject + Clock clock; + @Inject + AuthorFactory authorFactory; + @Inject + CryptoComponent crypto; + @Inject + BlogFactory blogFactory; + @Inject + BlogPostFactory blogPostFactory; + + // objects accessed from background threads need to be volatile + private volatile Waiter validationWaiter; + private volatile Waiter deliveryWaiter; + + private final File testDir = TestUtils.getTestDirectory(); + private final SecretKey master = TestUtils.getSecretKey(); + private final int TIMEOUT = 15000; + private final String AUTHOR1 = "Author 1"; + private final String AUTHOR2 = "Author 2"; + private final String CONTENT_TYPE = "text/plain"; + + private static final Logger LOG = + Logger.getLogger(ForumSharingIntegrationTest.class.getName()); + + private BlogManagerTestComponent t0, t1; + + @Before + public void setUp() throws Exception { + BlogManagerTestComponent component = + DaggerBlogManagerTestComponent.builder().build(); + component.inject(this); + injectEagerSingletons(component); + + assertTrue(testDir.mkdirs()); + File t0Dir = new File(testDir, AUTHOR1); + t0 = DaggerBlogManagerTestComponent.builder() + .testDatabaseModule(new TestDatabaseModule(t0Dir)).build(); + injectEagerSingletons(t0); + File t1Dir = new File(testDir, AUTHOR2); + t1 = DaggerBlogManagerTestComponent.builder() + .testDatabaseModule(new TestDatabaseModule(t1Dir)).build(); + injectEagerSingletons(t1); + + identityManager0 = t0.getIdentityManager(); + identityManager1 = t1.getIdentityManager(); + contactManager0 = t0.getContactManager(); + contactManager1 = t1.getContactManager(); + blogManager0 = t0.getBlogManager(); + blogManager1 = t1.getBlogManager(); + sync0 = t0.getSyncSessionFactory(); + sync1 = t1.getSyncSessionFactory(); + + // initialize waiters fresh for each test + validationWaiter = new Waiter(); + deliveryWaiter = new Waiter(); + } + + @Test + public void testPersonalBlogInitialisation() throws Exception { + startLifecycles(); + + defaultInit(); + + Collection<Blog> blogs0 = blogManager0.getBlogs(); + assertEquals(2, blogs0.size()); + Iterator<Blog> i0 = blogs0.iterator(); + assertEquals(author0, i0.next().getAuthor()); + assertEquals(author1, i0.next().getAuthor()); + + Collection<Blog> blogs1 = blogManager1.getBlogs(); + assertEquals(2, blogs1.size()); + Iterator<Blog> i1 = blogs1.iterator(); + assertEquals(author1, i1.next().getAuthor()); + assertEquals(author0, i1.next().getAuthor()); + + assertEquals(blog0, blogManager0.getPersonalBlog(author0)); + assertEquals(blog0, blogManager1.getPersonalBlog(author0)); + assertEquals(blog1, blogManager0.getPersonalBlog(author1)); + assertEquals(blog1, blogManager1.getPersonalBlog(author1)); + + assertEquals(blog0, blogManager0.getBlog(blog0.getId())); + assertEquals(blog0, blogManager1.getBlog(blog0.getId())); + assertEquals(blog1, blogManager0.getBlog(blog1.getId())); + assertEquals(blog1, blogManager1.getBlog(blog1.getId())); + + assertEquals(1, blogManager0.getBlogs(author0).size()); + assertEquals(1, blogManager1.getBlogs(author0).size()); + assertEquals(1, blogManager0.getBlogs(author1).size()); + assertEquals(1, blogManager1.getBlogs(author1).size()); + + stopLifecycles(); + } + + @Test + public void testBlogPost() throws Exception { + startLifecycles(); + defaultInit(); + + // check that blog0 has no posts + final byte[] body = TestUtils.getRandomBytes(42); + Collection<BlogPostHeader> headers0 = + blogManager0.getPostHeaders(blog0.getId()); + assertEquals(0, headers0.size()); + + // add a post to blog0 + BlogPost p = blogPostFactory + .createBlogPost(blog0.getId(), null, clock.currentTimeMillis(), + null, author0, CONTENT_TYPE, body); + blogManager0.addLocalPost(p); + + // check that post is now in blog0 + headers0 = blogManager0.getPostHeaders(blog0.getId()); + assertEquals(1, headers0.size()); + + // check that body is there + assertArrayEquals(body, + blogManager0.getPostBody(p.getMessage().getId())); + + // make sure that blog0 at author1 doesn't have the post yet + Collection<BlogPostHeader> headers1 = + blogManager1.getPostHeaders(blog0.getId()); + assertEquals(0, headers1.size()); + + // sync the post over + sync0To1(); + deliveryWaiter.await(TIMEOUT, 1); + + // make sure post arrived + headers1 = blogManager1.getPostHeaders(blog0.getId()); + assertEquals(1, headers1.size()); + + // check that body is there + assertArrayEquals(body, + blogManager1.getPostBody(p.getMessage().getId())); + + stopLifecycles(); + } + + @Test + public void testBlogPostInWrongBlog() throws Exception { + startLifecycles(); + defaultInit(); + + // add a post to blog1 + final byte[] body = TestUtils.getRandomBytes(42); + BlogPost p = blogPostFactory + .createBlogPost(blog1.getId(), null, clock.currentTimeMillis(), + null, author0, CONTENT_TYPE, body); + blogManager0.addLocalPost(p); + + // check that post is now in blog1 + Collection<BlogPostHeader> headers0 = + blogManager0.getPostHeaders(blog1.getId()); + assertEquals(1, headers0.size()); + + // sync the post over + sync0To1(); + validationWaiter.await(TIMEOUT, 1); + + // make sure post did not arrive, because of wrong signature + Collection<BlogPostHeader> headers1 = + blogManager1.getPostHeaders(blog1.getId()); + assertEquals(0, headers1.size()); + + stopLifecycles(); + } + + @Test + public void testAddAndRemoveBlog() throws Exception { + startLifecycles(); + defaultInit(); + + String name = "Test Blog"; + String desc = "Description"; + + // add blog + Blog blog = blogManager0.addBlog(author0, name, desc); + Collection<Blog> blogs0 = blogManager0.getBlogs(); + assertEquals(3, blogs0.size()); + assertTrue(blogs0.contains(blog)); + assertEquals(2, blogManager0.getBlogs(author0).size()); + + // remove blog + blogManager0.removeBlog(blog); + blogs0 = blogManager0.getBlogs(); + assertEquals(2, blogs0.size()); + assertFalse(blogs0.contains(blog)); + assertEquals(1, blogManager0.getBlogs(author0).size()); + + stopLifecycles(); + } + + @After + public void tearDown() throws Exception { + TestUtils.deleteTestDirectory(testDir); + } + + private class Listener implements EventListener { + public void eventOccurred(Event e) { + if (e instanceof MessageStateChangedEvent) { + MessageStateChangedEvent event = (MessageStateChangedEvent) e; + if (!event.isLocal()) { + if (event.getState() == DELIVERED) { + deliveryWaiter.resume(); + } else if (event.getState() == VALID || + event.getState() == INVALID || + event.getState() == PENDING) { + validationWaiter.resume(); + } + } + } + } + } + + private void defaultInit() throws DbException { + addDefaultIdentities(); + addDefaultContacts(); + listenToEvents(); + } + + private void addDefaultIdentities() throws DbException { + KeyPair keyPair0 = crypto.generateSignatureKeyPair(); + byte[] publicKey0 = keyPair0.getPublic().getEncoded(); + byte[] privateKey0 = keyPair0.getPrivate().getEncoded(); + author0 = authorFactory + .createLocalAuthor(AUTHOR1, publicKey0, privateKey0); + identityManager0.addLocalAuthor(author0); + blog0 = blogFactory.createPersonalBlog(author0); + + KeyPair keyPair1 = crypto.generateSignatureKeyPair(); + byte[] publicKey1 = keyPair1.getPublic().getEncoded(); + byte[] privateKey1 = keyPair1.getPrivate().getEncoded(); + author1 = authorFactory + .createLocalAuthor(AUTHOR2, publicKey1, privateKey1); + identityManager1.addLocalAuthor(author1); + blog1 = blogFactory.createPersonalBlog(author1); + } + + private void addDefaultContacts() throws DbException { + // sharer adds invitee as contact + contactId1 = contactManager0.addContact(author1, + author0.getId(), master, clock.currentTimeMillis(), true, + true + ); + // invitee adds sharer back + contactId0 = contactManager1.addContact(author0, + author1.getId(), master, clock.currentTimeMillis(), true, + true + ); + } + + private void listenToEvents() { + Listener listener0 = new Listener(); + t0.getEventBus().addListener(listener0); + Listener listener1 = new Listener(); + t1.getEventBus().addListener(listener1); + } + + private void sync0To1() throws IOException, TimeoutException { + deliverMessage(sync0, contactId0, sync1, contactId1, "0 to 1"); + } + + private void sync1To0() throws IOException, TimeoutException { + deliverMessage(sync1, contactId1, sync0, contactId0, "1 to 0"); + } + + private void deliverMessage(SyncSessionFactory fromSync, ContactId fromId, + SyncSessionFactory toSync, ContactId toId, String debug) + throws IOException, TimeoutException { + + if (debug != null) LOG.info("TEST: Sending message from " + debug); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + // Create an outgoing sync session + SyncSession sessionFrom = + fromSync.createSimplexOutgoingSession(toId, MAX_LATENCY, out); + // Write whatever needs to be written + sessionFrom.run(); + out.close(); + + ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); + // Create an incoming sync session + SyncSession sessionTo = toSync.createIncomingSession(fromId, in); + // Read whatever needs to be read + sessionTo.run(); + in.close(); + } + + private void startLifecycles() throws InterruptedException { + // Start the lifecycle manager and wait for it to finish + lifecycleManager0 = t0.getLifecycleManager(); + lifecycleManager1 = t1.getLifecycleManager(); + lifecycleManager0.startServices(); + lifecycleManager1.startServices(); + lifecycleManager0.waitForStartup(); + lifecycleManager1.waitForStartup(); + } + + private void stopLifecycles() throws InterruptedException { + // Clean up + lifecycleManager0.stopServices(); + lifecycleManager1.stopServices(); + lifecycleManager0.waitForShutdown(); + lifecycleManager1.waitForShutdown(); + } + + private void injectEagerSingletons(BlogManagerTestComponent component) { + component.inject(new LifecycleModule.EagerSingletons()); + component.inject(new BlogsModule.EagerSingletons()); + component.inject(new CryptoModule.EagerSingletons()); + component.inject(new ContactModule.EagerSingletons()); + component.inject(new TransportModule.EagerSingletons()); + component.inject(new SyncModule.EagerSingletons()); + component.inject(new PropertiesModule.EagerSingletons()); + } + +} diff --git a/briar-android-tests/src/test/java/org/briarproject/BlogManagerTestComponent.java b/briar-android-tests/src/test/java/org/briarproject/BlogManagerTestComponent.java new file mode 100644 index 0000000000000000000000000000000000000000..41d15eb9e660ce8e94cf9840aee2f1ce4a97700d --- /dev/null +++ b/briar-android-tests/src/test/java/org/briarproject/BlogManagerTestComponent.java @@ -0,0 +1,78 @@ +package org.briarproject; + +import org.briarproject.api.blogs.BlogManager; +import org.briarproject.api.contact.ContactManager; +import org.briarproject.api.event.EventBus; +import org.briarproject.api.identity.IdentityManager; +import org.briarproject.api.lifecycle.LifecycleManager; +import org.briarproject.api.sync.SyncSessionFactory; +import org.briarproject.blogs.BlogsModule; +import org.briarproject.clients.ClientsModule; +import org.briarproject.contact.ContactModule; +import org.briarproject.crypto.CryptoModule; +import org.briarproject.data.DataModule; +import org.briarproject.db.DatabaseModule; +import org.briarproject.event.EventModule; +import org.briarproject.identity.IdentityModule; +import org.briarproject.lifecycle.LifecycleModule; +import org.briarproject.properties.PropertiesModule; +import org.briarproject.sharing.SharingModule; +import org.briarproject.sync.SyncModule; +import org.briarproject.system.SystemModule; +import org.briarproject.transport.TransportModule; + +import javax.inject.Singleton; + +import dagger.Component; + +@Singleton +@Component(modules = { + TestDatabaseModule.class, + TestPluginsModule.class, + TestSeedProviderModule.class, + ClientsModule.class, + ContactModule.class, + CryptoModule.class, + DataModule.class, + DatabaseModule.class, + EventModule.class, + BlogsModule.class, + IdentityModule.class, + LifecycleModule.class, + PropertiesModule.class, + SharingModule.class, + SyncModule.class, + SystemModule.class, + TransportModule.class +}) +interface BlogManagerTestComponent { + + void inject(BlogManagerTest testCase); + + void inject(ContactModule.EagerSingletons init); + + void inject(CryptoModule.EagerSingletons init); + + void inject(BlogsModule.EagerSingletons init); + + void inject(LifecycleModule.EagerSingletons init); + + void inject(PropertiesModule.EagerSingletons init); + + void inject(SyncModule.EagerSingletons init); + + void inject(TransportModule.EagerSingletons init); + + LifecycleManager getLifecycleManager(); + + EventBus getEventBus(); + + IdentityManager getIdentityManager(); + + ContactManager getContactManager(); + + BlogManager getBlogManager(); + + SyncSessionFactory getSyncSessionFactory(); + +} diff --git a/briar-tests/src/org/briarproject/blogs/BlogManagerImplTest.java b/briar-tests/src/org/briarproject/blogs/BlogManagerImplTest.java new file mode 100644 index 0000000000000000000000000000000000000000..745ea254c2ad48653980bde75a09d113f11adb43 --- /dev/null +++ b/briar-tests/src/org/briarproject/blogs/BlogManagerImplTest.java @@ -0,0 +1,320 @@ +package org.briarproject.blogs; + +import org.briarproject.BriarTestCase; +import org.briarproject.api.FormatException; +import org.briarproject.api.blogs.Blog; +import org.briarproject.api.blogs.BlogFactory; +import org.briarproject.api.blogs.BlogPost; +import org.briarproject.api.blogs.BlogPostHeader; +import org.briarproject.api.clients.ClientHelper; +import org.briarproject.api.contact.Contact; +import org.briarproject.api.contact.ContactId; +import org.briarproject.api.data.BdfDictionary; +import org.briarproject.api.data.BdfEntry; +import org.briarproject.api.data.BdfList; +import org.briarproject.api.data.MetadataParser; +import org.briarproject.api.db.DatabaseComponent; +import org.briarproject.api.db.DbException; +import org.briarproject.api.db.Transaction; +import org.briarproject.api.event.BlogPostAddedEvent; +import org.briarproject.api.identity.Author; +import org.briarproject.api.identity.AuthorId; +import org.briarproject.api.identity.IdentityManager; +import org.briarproject.api.identity.LocalAuthor; +import org.briarproject.api.sync.Group; +import org.briarproject.api.sync.GroupId; +import org.briarproject.api.sync.Message; +import org.briarproject.api.sync.MessageId; +import org.jmock.Expectations; +import org.jmock.Mockery; +import org.junit.Test; + +import java.util.Collection; +import java.util.Collections; + +import static org.briarproject.TestUtils.getRandomBytes; +import static org.briarproject.TestUtils.getRandomId; +import static org.briarproject.api.blogs.BlogConstants.KEY_AUTHOR; +import static org.briarproject.api.blogs.BlogConstants.KEY_AUTHOR_ID; +import static org.briarproject.api.blogs.BlogConstants.KEY_AUTHOR_NAME; +import static org.briarproject.api.blogs.BlogConstants.KEY_CONTENT_TYPE; +import static org.briarproject.api.blogs.BlogConstants.KEY_DESCRIPTION; +import static org.briarproject.api.blogs.BlogConstants.KEY_PUBLIC_KEY; +import static org.briarproject.api.blogs.BlogConstants.KEY_READ; +import static org.briarproject.api.blogs.BlogConstants.KEY_TIMESTAMP; +import static org.briarproject.api.blogs.BlogConstants.KEY_TIME_RECEIVED; +import static org.briarproject.api.identity.Author.Status.VERIFIED; +import static org.briarproject.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH; +import static org.briarproject.blogs.BlogManagerImpl.CLIENT_ID; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class BlogManagerImplTest extends BriarTestCase { + + private final Mockery context = new Mockery(); + private final BlogManagerImpl blogManager; + private final DatabaseComponent db = context.mock(DatabaseComponent.class); + private final IdentityManager identityManager = + context.mock(IdentityManager.class); + private final ClientHelper clientHelper = context.mock(ClientHelper.class); + private final MetadataParser metadataParser = + context.mock(MetadataParser.class); + private final BlogFactory blogFactory = context.mock(BlogFactory.class); + + private final Blog blog1, blog2; + private final Message message; + private final MessageId messageId; + + public BlogManagerImplTest() { + blogManager = new BlogManagerImpl(db, identityManager, clientHelper, + metadataParser, blogFactory); + + blog1 = getBlog("Test Blog 1", "Test Description 1"); + blog2 = getBlog("Test Blog 2", "Test Description 2"); + messageId = new MessageId(getRandomId()); + message = new Message(messageId, blog1.getId(), 42, getRandomBytes(42)); + } + + @Test + public void testClientId() { + assertEquals(CLIENT_ID, blogManager.getClientId()); + } + + @Test + public void testCreateLocalState() throws DbException { + final Transaction txn = new Transaction(null, false); + final Collection<LocalAuthor> localAuthors = + Collections.singletonList((LocalAuthor) blog1.getAuthor()); + + final ContactId contactId = new ContactId(0); + final Collection<ContactId> contactIds = + Collections.singletonList(contactId); + + Contact contact = new Contact(contactId, blog2.getAuthor(), + blog1.getAuthor().getId(), true); + final Collection<Contact> contacts = Collections.singletonList(contact); + + context.checking(new Expectations() {{ + oneOf(db).getLocalAuthors(txn); + will(returnValue(localAuthors)); + oneOf(blogFactory).createPersonalBlog(blog1.getAuthor()); + will(returnValue(blog1)); + oneOf(db).containsGroup(txn, blog1.getId()); + will(returnValue(false)); + oneOf(db).addGroup(txn, blog1.getGroup()); + oneOf(db).getContacts(txn, blog1.getAuthor().getId()); + will(returnValue(contactIds)); + oneOf(db).setVisibleToContact(txn, contactId, blog1.getId(), true); + oneOf(db).getContacts(txn); + will(returnValue(contacts)); + oneOf(blogFactory).createPersonalBlog(blog2.getAuthor()); + will(returnValue(blog2)); + oneOf(db).containsGroup(txn, blog2.getId()); + will(returnValue(false)); + oneOf(db).addGroup(txn, blog2.getGroup()); + oneOf(db).setVisibleToContact(txn, contactId, blog2.getId(), true); + oneOf(db).getLocalAuthor(txn, blog1.getAuthor().getId()); + will(returnValue(blog1.getAuthor())); + oneOf(blogFactory).createPersonalBlog(blog1.getAuthor()); + will(returnValue(blog1)); + oneOf(db).setVisibleToContact(txn, contactId, blog1.getId(), true); + }}); + + blogManager.createLocalState(txn); + } + + @Test + public void testRemovingContact() throws DbException { + final Transaction txn = new Transaction(null, false); + + final ContactId contactId = new ContactId(0); + Contact contact = new Contact(contactId, blog2.getAuthor(), + blog1.getAuthor().getId(), true); + + context.checking(new Expectations() {{ + oneOf(blogFactory).createPersonalBlog(blog2.getAuthor()); + will(returnValue(blog2)); + oneOf(db).removeGroup(txn, blog2.getGroup()); + }}); + + blogManager.removingContact(txn, contact); + } + + @Test + public void testAddingIdentity() throws DbException { + final Transaction txn = new Transaction(null, false); + Author a = blog1.getAuthor(); + final LocalAuthor localAuthor = + new LocalAuthor(a.getId(), a.getName(), a.getPublicKey(), + a.getPublicKey(), 0); + + context.checking(new Expectations() {{ + oneOf(blogFactory).createPersonalBlog(localAuthor); + will(returnValue(blog1)); + oneOf(db).addGroup(txn, blog1.getGroup()); + }}); + + blogManager.addingIdentity(txn, localAuthor); + } + + @Test + public void testRemovingIdentity() throws DbException { + final Transaction txn = new Transaction(null, false); + Author a = blog1.getAuthor(); + final LocalAuthor localAuthor = + new LocalAuthor(a.getId(), a.getName(), a.getPublicKey(), + a.getPublicKey(), 0); + + context.checking(new Expectations() {{ + oneOf(blogFactory).createPersonalBlog(localAuthor); + will(returnValue(blog1)); + oneOf(db).removeGroup(txn, blog1.getGroup()); + }}); + + blogManager.removingIdentity(txn, localAuthor); + } + + @Test + public void testIncomingMessage() throws DbException, FormatException { + final Transaction txn = new Transaction(null, false); + BdfList list = new BdfList(); + BdfDictionary author = authorToBdfDictionary(blog1.getAuthor()); + BdfDictionary meta = BdfDictionary.of( + new BdfEntry(KEY_TIMESTAMP, 0), + new BdfEntry(KEY_TIME_RECEIVED, 1), + new BdfEntry(KEY_AUTHOR, author), + new BdfEntry(KEY_CONTENT_TYPE, 0), + new BdfEntry(KEY_READ, false), + new BdfEntry(KEY_CONTENT_TYPE, "text/plain") + ); + + context.checking(new Expectations() {{ + oneOf(identityManager) + .getAuthorStatus(txn, blog1.getAuthor().getId()); + will(returnValue(VERIFIED)); + }}); + + blogManager.incomingMessage(txn, message, list, meta); + + assertEquals(1, txn.getEvents().size()); + assertTrue(txn.getEvents().get(0) instanceof BlogPostAddedEvent); + + BlogPostAddedEvent e = (BlogPostAddedEvent) txn.getEvents().get(0); + assertEquals(blog1.getId(), e.getGroupId()); + + BlogPostHeader h = e.getHeader(); + assertEquals(1, h.getTimeReceived()); + assertEquals(messageId, h.getId()); + assertEquals(null, h.getParentId()); + assertEquals(VERIFIED, h.getAuthorStatus()); + assertEquals("text/plain", h.getContentType()); + assertEquals(blog1.getAuthor(), h.getAuthor()); + } + + @Test + public void testAddBlog() throws DbException, FormatException { + final Transaction txn = new Transaction(null, false); + Author a = blog1.getAuthor(); + final LocalAuthor localAuthor = + new LocalAuthor(a.getId(), a.getName(), a.getPublicKey(), + a.getPublicKey(), 0); + final BdfDictionary meta = BdfDictionary.of( + new BdfEntry(KEY_DESCRIPTION, blog1.getDescription()) + ); + + context.checking(new Expectations() {{ + oneOf(blogFactory) + .createBlog(blog1.getName(), blog1.getDescription(), + blog1.getAuthor()); + will(returnValue(blog1)); + oneOf(db).startTransaction(false); + will(returnValue(txn)); + oneOf(db).addGroup(txn, blog1.getGroup()); + oneOf(clientHelper).mergeGroupMetadata(txn, blog1.getId(), meta); + oneOf(db).endTransaction(txn); + }}); + + blogManager + .addBlog(localAuthor, blog1.getName(), blog1.getDescription()); + assertTrue(txn.isComplete()); + } + + @Test + public void testRemoveBlog() throws DbException, FormatException { + final Transaction txn = new Transaction(null, false); + + context.checking(new Expectations() {{ + oneOf(db).startTransaction(false); + will(returnValue(txn)); + oneOf(db).removeGroup(txn, blog1.getGroup()); + oneOf(db).endTransaction(txn); + }}); + + blogManager.removeBlog(blog1); + assertTrue(txn.isComplete()); + } + + @Test + public void testAddLocalPost() throws DbException, FormatException { + final Transaction txn = new Transaction(null, true); + final BlogPost post = + new BlogPost(null, message, null, blog1.getAuthor(), + "text/plain"); + BdfDictionary authorMeta = authorToBdfDictionary(blog1.getAuthor()); + final BdfDictionary meta = BdfDictionary.of( + new BdfEntry(KEY_TIMESTAMP, message.getTimestamp()), + new BdfEntry(KEY_AUTHOR, authorMeta), + new BdfEntry(KEY_CONTENT_TYPE, "text/plain"), + new BdfEntry(KEY_READ, true) + ); + + context.checking(new Expectations() {{ + oneOf(clientHelper).addLocalMessage(message, CLIENT_ID, meta, true); + oneOf(db).startTransaction(true); + will(returnValue(txn)); + oneOf(identityManager) + .getAuthorStatus(txn, blog1.getAuthor().getId()); + will(returnValue(VERIFIED)); + oneOf(db).endTransaction(txn); + }}); + + blogManager.addLocalPost(post); + assertTrue(txn.isComplete()); + + assertEquals(1, txn.getEvents().size()); + assertTrue(txn.getEvents().get(0) instanceof BlogPostAddedEvent); + + BlogPostAddedEvent e = (BlogPostAddedEvent) txn.getEvents().get(0); + assertEquals(blog1.getId(), e.getGroupId()); + + BlogPostHeader h = e.getHeader(); + assertEquals(message.getTimestamp(), h.getTimeReceived()); + assertEquals(messageId, h.getId()); + assertEquals(null, h.getParentId()); + assertEquals(VERIFIED, h.getAuthorStatus()); + assertEquals("text/plain", h.getContentType()); + assertEquals(blog1.getAuthor(), h.getAuthor()); + } + + private Blog getBlog(String name, String desc) { + final GroupId groupId = new GroupId(getRandomId()); + final Group group = new Group(groupId, CLIENT_ID, getRandomBytes(42)); + final AuthorId authorId = new AuthorId(getRandomId()); + final byte[] publicKey = getRandomBytes(MAX_PUBLIC_KEY_LENGTH); + final byte[] privateKey = getRandomBytes(MAX_PUBLIC_KEY_LENGTH); + final long created = System.currentTimeMillis(); + final LocalAuthor localAuthor = + new LocalAuthor(authorId, "Author", publicKey, privateKey, + created); + return new Blog(group, name, desc, localAuthor, false); + } + + private BdfDictionary authorToBdfDictionary(Author a) { + return BdfDictionary.of( + new BdfEntry(KEY_AUTHOR_ID, a.getId()), + new BdfEntry(KEY_AUTHOR_NAME, a.getName()), + new BdfEntry(KEY_PUBLIC_KEY, a.getPublicKey()) + ); + } + +} diff --git a/briar-tests/src/org/briarproject/blogs/BlogPostValidatorTest.java b/briar-tests/src/org/briarproject/blogs/BlogPostValidatorTest.java new file mode 100644 index 0000000000000000000000000000000000000000..90dba5912728e4ba1da6335c4217be28d29cc26d --- /dev/null +++ b/briar-tests/src/org/briarproject/blogs/BlogPostValidatorTest.java @@ -0,0 +1,166 @@ +package org.briarproject.blogs; + +import org.briarproject.BriarTestCase; +import org.briarproject.TestUtils; +import org.briarproject.api.FormatException; +import org.briarproject.api.blogs.Blog; +import org.briarproject.api.blogs.BlogFactory; +import org.briarproject.api.clients.ClientHelper; +import org.briarproject.api.crypto.CryptoComponent; +import org.briarproject.api.crypto.KeyParser; +import org.briarproject.api.crypto.PublicKey; +import org.briarproject.api.crypto.Signature; +import org.briarproject.api.data.BdfDictionary; +import org.briarproject.api.data.BdfEntry; +import org.briarproject.api.data.BdfList; +import org.briarproject.api.data.MetadataEncoder; +import org.briarproject.api.identity.Author; +import org.briarproject.api.identity.AuthorId; +import org.briarproject.api.sync.ClientId; +import org.briarproject.api.sync.Group; +import org.briarproject.api.sync.GroupId; +import org.briarproject.api.sync.InvalidMessageException; +import org.briarproject.api.sync.Message; +import org.briarproject.api.sync.MessageId; +import org.briarproject.api.system.Clock; +import org.briarproject.system.SystemClock; +import org.jmock.Expectations; +import org.jmock.Mockery; +import org.junit.Test; + +import java.io.IOException; +import java.security.GeneralSecurityException; + +import static org.briarproject.api.blogs.BlogConstants.KEY_AUTHOR; +import static org.briarproject.api.blogs.BlogConstants.KEY_AUTHOR_ID; +import static org.briarproject.api.blogs.BlogConstants.KEY_AUTHOR_NAME; +import static org.briarproject.api.blogs.BlogConstants.KEY_CONTENT_TYPE; +import static org.briarproject.api.blogs.BlogConstants.KEY_PUBLIC_KEY; +import static org.briarproject.api.blogs.BlogConstants.KEY_READ; +import static org.briarproject.api.blogs.BlogConstants.MAX_BLOG_POST_BODY_LENGTH; +import static org.briarproject.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +public class BlogPostValidatorTest extends BriarTestCase { + + private final Mockery context = new Mockery(); + private final Blog blog; + private final Author author; + private final Group group; + private final Message message; + private final BlogPostValidator validator; + private final CryptoComponent cryptoComponent = + context.mock(CryptoComponent.class); + private final BlogFactory blogFactory = context.mock(BlogFactory.class); + private final ClientHelper clientHelper = context.mock(ClientHelper.class); + private final Clock clock = new SystemClock(); + private final byte[] body = TestUtils.getRandomBytes( + MAX_BLOG_POST_BODY_LENGTH); + private final String contentType = "text/plain"; + + public BlogPostValidatorTest() { + GroupId groupId = new GroupId(TestUtils.getRandomId()); + ClientId clientId = new ClientId(TestUtils.getRandomId()); + byte[] descriptor = TestUtils.getRandomBytes(12); + group = new Group(groupId, clientId, descriptor); + AuthorId authorId = new AuthorId(TestUtils.getRandomBytes(AuthorId.LENGTH)); + byte[] publicKey = TestUtils.getRandomBytes(MAX_PUBLIC_KEY_LENGTH); + author = new Author(authorId, "Author", publicKey); + blog = new Blog(group, "Test Blog", "", author, false); + + MessageId messageId = new MessageId(TestUtils.getRandomId()); + long timestamp = System.currentTimeMillis(); + byte[] raw = TestUtils.getRandomBytes(123); + message = new Message(messageId, group.getId(), timestamp, raw); + + MetadataEncoder metadataEncoder = context.mock(MetadataEncoder.class); + validator = new BlogPostValidator(cryptoComponent, blogFactory, + clientHelper, metadataEncoder, clock); + context.assertIsSatisfied(); + } + + @Test + public void testValidateProperBlogPost() + throws IOException, GeneralSecurityException { + // Parent ID, content type, title (optional), post body, attachments + BdfList content = BdfList.of(null, contentType, null, body, null); + final byte[] sigBytes = TestUtils.getRandomBytes(42); + BdfList m = BdfList.of(content, sigBytes); + + expectCrypto(m, true); + final BdfDictionary result = + validator.validateMessage(message, group, m).getDictionary(); + + assertEquals(contentType, result.getString(KEY_CONTENT_TYPE)); + BdfDictionary authorDict = BdfDictionary.of( + new BdfEntry(KEY_AUTHOR_ID, author.getId()), + new BdfEntry(KEY_AUTHOR_NAME, author.getName()), + new BdfEntry(KEY_PUBLIC_KEY, author.getPublicKey()) + ); + assertEquals(authorDict, result.getDictionary(KEY_AUTHOR)); + assertFalse(result.getBoolean(KEY_READ)); + context.assertIsSatisfied(); + } + + @Test(expected = FormatException.class) + public void testValidateBlogPostWithoutAttachments() + throws IOException, GeneralSecurityException { + BdfList content = BdfList.of(null, contentType, null, body); + BdfList m = BdfList.of(content, null); + + validator.validateMessage(message, group, m).getDictionary(); + } + + @Test(expected = FormatException.class) + public void testValidateBlogPostWithoutSignature() + throws IOException, GeneralSecurityException { + BdfList content = BdfList.of(null, contentType, null, body, null); + BdfList m = BdfList.of(content, null); + + validator.validateMessage(message, group, m).getDictionary(); + } + + @Test(expected = InvalidMessageException.class) + public void testValidateBlogPostWithBadSignature() + throws IOException, GeneralSecurityException { + // Parent ID, content type, title (optional), post body, attachments + BdfList content = BdfList.of(null, contentType, null, body, null); + final byte[] sigBytes = TestUtils.getRandomBytes(42); + BdfList m = BdfList.of(content, sigBytes); + + expectCrypto(m, false); + validator.validateMessage(message, group, m).getDictionary(); + } + + private void expectCrypto(BdfList m, final boolean pass) + throws IOException, GeneralSecurityException { + final Signature signature = context.mock(Signature.class); + final KeyParser keyParser = context.mock(KeyParser.class); + final PublicKey publicKey = context.mock(PublicKey.class); + + final BdfList content = m.getList(0); + final byte[] sigBytes = m.getRaw(1); + + final BdfList signed = + BdfList.of(blog.getId(), message.getTimestamp(), content); + + context.checking(new Expectations() {{ + oneOf(blogFactory).parseBlog(group, ""); + will(returnValue(blog)); + oneOf(cryptoComponent).getSignatureKeyParser(); + will(returnValue(keyParser)); + oneOf(keyParser).parsePublicKey(blog.getAuthor().getPublicKey()); + will(returnValue(publicKey)); + oneOf(cryptoComponent).getSignature(); + will(returnValue(signature)); + oneOf(signature).initVerify(publicKey); + oneOf(clientHelper).toByteArray(signed); + will(returnValue(sigBytes)); + oneOf(signature).update(sigBytes); + oneOf(signature).verify(sigBytes); + will(returnValue(pass)); + }}); + } + +}