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));
+		}});
+	}
+
+}