package org.briarproject; import net.jodah.concurrentunit.Waiter; import org.briarproject.api.blogs.Blog; import org.briarproject.api.blogs.BlogInvitationRequest; import org.briarproject.api.blogs.BlogInvitationResponse; import org.briarproject.api.blogs.BlogManager; import org.briarproject.api.blogs.BlogPostFactory; import org.briarproject.api.blogs.BlogSharingManager; import org.briarproject.api.clients.ContactGroupFactory; import org.briarproject.api.contact.Contact; 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.BlogInvitationReceivedEvent; import org.briarproject.api.event.BlogInvitationResponseReceivedEvent; 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.sharing.InvitationMessage; import org.briarproject.api.sync.GroupId; import org.briarproject.api.sync.SyncSession; import org.briarproject.api.sync.SyncSessionFactory; import org.briarproject.api.sync.ValidationManager.State; 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.sharing.SharingModule; import org.briarproject.sync.SyncModule; import org.briarproject.transport.TransportModule; import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.concurrent.TimeoutException; import java.util.logging.Logger; import javax.inject.Inject; 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.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; public class BlogSharingIntegrationTest extends BriarIntegrationTest { private LifecycleManager lifecycleManager0, lifecycleManager1, lifecycleManager2; private SyncSessionFactory sync0, sync1; private BlogManager blogManager0, blogManager1; private ContactManager contactManager0, contactManager1, contactManager2; private Contact contact1, contact2, contact01, contact02; private ContactId contactId1, contactId2, contactId01, contactId02; private IdentityManager identityManager0, identityManager1, identityManager2; private LocalAuthor author0, author1, author2; private Blog blog0, blog1, blog2; private SharerListener listener0, listener2; private InviteeListener listener1; @Inject Clock clock; @Inject AuthorFactory authorFactory; @Inject ContactGroupFactory contactGroupFactory; @Inject BlogPostFactory blogPostFactory; @Inject CryptoComponent cryptoComponent; // objects accessed from background threads need to be volatile private volatile BlogSharingManager blogSharingManager0; private volatile BlogSharingManager blogSharingManager1; private volatile BlogSharingManager blogSharingManager2; private volatile Waiter eventWaiter; private volatile Waiter msgWaiter; private final File testDir = TestUtils.getTestDirectory(); private final SecretKey master = TestUtils.getSecretKey(); private final int TIMEOUT = 15000; private final String SHARER = "Sharer"; private final String INVITEE = "Invitee"; private final String CONTACT2 = "Contact2"; private static final Logger LOG = Logger.getLogger(BlogSharingIntegrationTest.class.getName()); private BlogSharingIntegrationTestComponent t0, t1, t2; @Rule public ExpectedException thrown = ExpectedException.none(); @Before public void setUp() { BlogSharingIntegrationTestComponent component = DaggerBlogSharingIntegrationTestComponent.builder().build(); component.inject(this); injectEagerSingletons(component); assertTrue(testDir.mkdirs()); File t0Dir = new File(testDir, SHARER); t0 = DaggerBlogSharingIntegrationTestComponent.builder() .testDatabaseModule(new TestDatabaseModule(t0Dir)).build(); injectEagerSingletons(t0); File t1Dir = new File(testDir, INVITEE); t1 = DaggerBlogSharingIntegrationTestComponent.builder() .testDatabaseModule(new TestDatabaseModule(t1Dir)).build(); injectEagerSingletons(t1); File t2Dir = new File(testDir, CONTACT2); t2 = DaggerBlogSharingIntegrationTestComponent.builder() .testDatabaseModule(new TestDatabaseModule(t2Dir)).build(); injectEagerSingletons(t2); identityManager0 = t0.getIdentityManager(); identityManager1 = t1.getIdentityManager(); identityManager2 = t2.getIdentityManager(); contactManager0 = t0.getContactManager(); contactManager1 = t1.getContactManager(); contactManager2 = t2.getContactManager(); blogManager0 = t0.getBlogManager(); blogManager1 = t1.getBlogManager(); blogSharingManager0 = t0.getBlogSharingManager(); blogSharingManager1 = t1.getBlogSharingManager(); blogSharingManager2 = t2.getBlogSharingManager(); sync0 = t0.getSyncSessionFactory(); sync1 = t1.getSyncSessionFactory(); // initialize waiters fresh for each test eventWaiter = new Waiter(); msgWaiter = new Waiter(); } @Test public void testPersonalBlogCannotBeSharedWithOwner() throws Exception { startLifecycles(); defaultInit(true); assertFalse(blogSharingManager0.canBeShared(blog1.getId(), contact1)); assertFalse(blogSharingManager0.canBeShared(blog2.getId(), contact2)); assertFalse(blogSharingManager1.canBeShared(blog0.getId(), contact01)); assertFalse(blogSharingManager2.canBeShared(blog0.getId(), contact02)); // create invitation blogSharingManager0 .sendInvitation(blog1.getId(), contactId1, "Hi!"); // sync invitation sync0To1(); // make sure the invitee ignored the request for their own blog assertFalse(listener1.requestReceived); stopLifecycles(); } @Test public void testSuccessfulSharing() throws Exception { startLifecycles(); // initialize and let invitee accept all requests defaultInit(true); // send invitation blogSharingManager0 .sendInvitation(blog2.getId(), contactId1, "Hi!"); // invitee has own blog and that of the sharer assertEquals(2, blogManager1.getBlogs().size()); // get sharing group and assert group message count GroupId g = contactGroupFactory .createContactGroup(blogSharingManager0.getClientId(), contact1).getId(); assertGroupCount(blogSharingManager0, g, 1, 0); // sync first request message sync0To1(); eventWaiter.await(TIMEOUT, 1); assertTrue(listener1.requestReceived); assertGroupCount(blogSharingManager1, g, 2, 1); // sync response back sync1To0(); eventWaiter.await(TIMEOUT, 1); assertTrue(listener0.responseReceived); assertGroupCount(blogSharingManager0, g, 2, 1); // blog was added successfully assertEquals(0, blogSharingManager0.getInvitations().size()); assertEquals(3, blogManager1.getBlogs().size()); // invitee has one invitation message from sharer List list = new ArrayList<>(blogSharingManager1 .getInvitationMessages(contactId01)); assertEquals(2, list.size()); // check other things are alright with the message for (InvitationMessage m : list) { if (m instanceof BlogInvitationRequest) { BlogInvitationRequest invitation = (BlogInvitationRequest) m; assertFalse(invitation.isAvailable()); assertEquals(blog2.getAuthor().getName(), invitation.getBlogAuthorName()); assertEquals(contactId1, invitation.getContactId()); assertEquals("Hi!", invitation.getMessage()); } else { BlogInvitationResponse response = (BlogInvitationResponse) m; assertEquals(contactId01, response.getContactId()); assertTrue(response.wasAccepted()); assertTrue(response.isLocal()); } } // sharer has own invitation message and response assertEquals(2, blogSharingManager0.getInvitationMessages(contactId1) .size()); // blog can not be shared again assertFalse(blogSharingManager0.canBeShared(blog2.getId(), contact1)); assertFalse(blogSharingManager1.canBeShared(blog2.getId(), contact01)); // group message count is still correct assertGroupCount(blogSharingManager0, g, 2, 1); assertGroupCount(blogSharingManager1, g, 2, 1); stopLifecycles(); } @Test public void testDeclinedSharing() throws Exception { startLifecycles(); // initialize and let invitee deny all requests defaultInit(false); // send invitation blogSharingManager0 .sendInvitation(blog2.getId(), contactId1, null); // sync first request message sync0To1(); eventWaiter.await(TIMEOUT, 1); assertTrue(listener1.requestReceived); // sync response back sync1To0(); eventWaiter.await(TIMEOUT, 1); assertTrue(listener0.responseReceived); // blog was not added assertEquals(0, blogSharingManager0.getInvitations().size()); assertEquals(2, blogManager1.getBlogs().size()); // blog is no longer available to invitee who declined assertEquals(0, blogSharingManager1.getInvitations().size()); // invitee has one invitation message from sharer and one response List list = new ArrayList<>(blogSharingManager1 .getInvitationMessages(contactId01)); assertEquals(2, list.size()); // check things are alright with the message for (InvitationMessage m : list) { if (m instanceof BlogInvitationRequest) { BlogInvitationRequest invitation = (BlogInvitationRequest) m; assertFalse(invitation.isAvailable()); assertEquals(blog2.getAuthor().getName(), invitation.getBlogAuthorName()); assertEquals(contactId1, invitation.getContactId()); assertEquals(null, invitation.getMessage()); } else { BlogInvitationResponse response = (BlogInvitationResponse) m; assertEquals(contactId01, response.getContactId()); assertFalse(response.wasAccepted()); assertTrue(response.isLocal()); } } // sharer has own invitation message and response assertEquals(2, blogSharingManager0.getInvitationMessages(contactId1) .size()); // blog can be shared again assertTrue(blogSharingManager0.canBeShared(blog2.getId(), contact1)); stopLifecycles(); } @Test public void testInviteeLeavesAfterFinished() throws Exception { startLifecycles(); // initialize and let invitee accept all requests defaultInit(true); // send invitation blogSharingManager0 .sendInvitation(blog2.getId(), contactId1, "Hi!"); // sync first request message sync0To1(); eventWaiter.await(TIMEOUT, 1); assertTrue(listener1.requestReceived); // sync response back sync1To0(); eventWaiter.await(TIMEOUT, 1); assertTrue(listener0.responseReceived); // blog was added successfully assertEquals(0, blogSharingManager0.getInvitations().size()); assertEquals(3, blogManager1.getBlogs().size()); assertTrue(blogManager1.getBlogs().contains(blog2)); // sharer shares blog with invitee assertTrue(blogSharingManager0.getSharedWith(blog2.getId()) .contains(contact1)); // invitee gets blog shared by sharer assertTrue(blogSharingManager1.getSharedBy(blog2.getId()) .contains(contact01)); // invitee un-subscribes from blog blogManager1.removeBlog(blog2); // send leave message to sharer sync1To0(); // blog is gone assertEquals(0, blogSharingManager0.getInvitations().size()); assertEquals(2, blogManager1.getBlogs().size()); // sharer no longer shares blog with invitee assertFalse(blogSharingManager0.getSharedWith(blog2.getId()) .contains(contact1)); // invitee no longer gets blog shared by sharer assertFalse(blogSharingManager1.getSharedBy(blog2.getId()) .contains(contact01)); // blog can be shared again assertTrue(blogSharingManager0.canBeShared(blog2.getId(), contact1)); assertTrue(blogSharingManager1.canBeShared(blog2.getId(), contact01)); stopLifecycles(); } @Test public void testInvitationForExistingBlog() throws Exception { startLifecycles(); // initialize and let invitee accept all requests defaultInit(true); // 1 and 2 are adding each other contactManager1.addContact(author2, author1.getId(), master, clock.currentTimeMillis(), true, true, true ); contactManager2.addContact(author1, author2.getId(), master, clock.currentTimeMillis(), true, true, true ); assertEquals(3, blogManager1.getBlogs().size()); // sharer sends invitation for 2's blog to 1 blogSharingManager0 .sendInvitation(blog2.getId(), contactId1, "Hi!"); // sync first request message sync0To1(); eventWaiter.await(TIMEOUT, 1); assertTrue(listener1.requestReceived); // make sure blog2 is shared by 0 Collection contacts = blogSharingManager1.getSharedBy(blog2.getId()); assertEquals(1, contacts.size()); assertTrue(contacts.contains(contact01)); // make sure 1 knows that they have blog2 already Collection messages = blogSharingManager1.getInvitationMessages(contactId01); assertEquals(2, messages.size()); assertEquals(blog2, blogManager1.getBlog(blog2.getId())); // sync response back sync1To0(); eventWaiter.await(TIMEOUT, 1); assertTrue(listener0.responseReceived); // blog was not added, because it was there already assertEquals(0, blogSharingManager0.getInvitations().size()); assertEquals(3, blogManager1.getBlogs().size()); stopLifecycles(); } @Test public void testRemovingSharedBlog() throws Exception { startLifecycles(); // initialize and let invitee accept all requests defaultInit(true); // send invitation blogSharingManager0 .sendInvitation(blog2.getId(), contactId1, "Hi!"); // sync first request message sync0To1(); eventWaiter.await(TIMEOUT, 1); assertTrue(listener1.requestReceived); // sync response back sync1To0(); eventWaiter.await(TIMEOUT, 1); assertTrue(listener0.responseReceived); // blog was added successfully and is shared both ways assertEquals(3, blogManager1.getBlogs().size()); Collection sharedWith = blogSharingManager0.getSharedWith(blog2.getId()); assertEquals(1, sharedWith.size()); assertEquals(contact1, sharedWith.iterator().next()); Collection sharedBy = blogSharingManager1.getSharedBy(blog2.getId()); assertEquals(1, sharedBy.size()); assertEquals(contact01, sharedBy.iterator().next()); // shared blog can be removed assertTrue(blogManager1.canBeRemoved(blog2.getId())); // invitee removes blog again blogManager1.removeBlog(blog2); // sync LEAVE message sync1To0(); // sharer does not share this blog anymore with invitee sharedWith = blogSharingManager0.getSharedWith(blog2.getId()); assertEquals(0, sharedWith.size()); stopLifecycles(); } @Test public void testSharedBlogBecomesPermanent() throws Exception { startLifecycles(); // initialize and let invitee accept all requests defaultInit(true); // invitee only sees two blogs assertEquals(2, blogManager1.getBlogs().size()); // sharer sends invitation for 2's blog to 1 blogSharingManager0 .sendInvitation(blog2.getId(), contactId1, "Hi!"); // sync first request message sync0To1(); eventWaiter.await(TIMEOUT, 1); assertTrue(listener1.requestReceived); // make sure blog2 is shared by 0 Collection contacts = blogSharingManager1.getSharedBy(blog2.getId()); assertEquals(1, contacts.size()); assertTrue(contacts.contains(contact01)); // sync response back sync1To0(); eventWaiter.await(TIMEOUT, 1); assertTrue(listener0.responseReceived); // blog was added and can be removed assertEquals(3, blogManager1.getBlogs().size()); assertTrue(blogManager1.canBeRemoved(blog2.getId())); // 1 and 2 are adding each other contactManager1.addContact(author2, author1.getId(), master, clock.currentTimeMillis(), true, true, true ); contactManager2.addContact(author1, author2.getId(), master, clock.currentTimeMillis(), true, true, true ); assertEquals(3, blogManager1.getBlogs().size()); // now blog can not be removed anymore assertFalse(blogManager1.canBeRemoved(blog2.getId())); stopLifecycles(); } @After public void tearDown() throws InterruptedException { TestUtils.deleteTestDirectory(testDir); } private class SharerListener implements EventListener { private volatile boolean responseReceived = false; @Override public void eventOccurred(Event e) { if (e instanceof MessageStateChangedEvent) { MessageStateChangedEvent event = (MessageStateChangedEvent) e; State s = event.getState(); if ((s == DELIVERED || s == INVALID) && !event.isLocal()) { LOG.info("TEST: Sharer received message"); msgWaiter.resume(); } } else if (e instanceof BlogInvitationResponseReceivedEvent) { BlogInvitationResponseReceivedEvent event = (BlogInvitationResponseReceivedEvent) e; eventWaiter.assertEquals(contactId1, event.getContactId()); responseReceived = true; eventWaiter.resume(); } // this is only needed for tests where a blog is re-shared else if (e instanceof BlogInvitationReceivedEvent) { BlogInvitationReceivedEvent event = (BlogInvitationReceivedEvent) e; eventWaiter.assertEquals(contactId1, event.getContactId()); Blog b = event.getShareable(); try { Contact c = contactManager0.getContact(contactId1); blogSharingManager0.respondToInvitation(b, c, true); } catch (DbException ex) { eventWaiter.rethrow(ex); } finally { eventWaiter.resume(); } } } } private class InviteeListener implements EventListener { private volatile boolean requestReceived = false; private final boolean accept, answer; private InviteeListener(boolean accept, boolean answer) { this.accept = accept; this.answer = answer; } private InviteeListener(boolean accept) { this(accept, true); } @Override public void eventOccurred(Event e) { if (e instanceof MessageStateChangedEvent) { MessageStateChangedEvent event = (MessageStateChangedEvent) e; State s = event.getState(); if ((s == DELIVERED || s == INVALID) && !event.isLocal()) { LOG.info("TEST: Invitee received message"); msgWaiter.resume(); } } else if (e instanceof BlogInvitationReceivedEvent) { BlogInvitationReceivedEvent event = (BlogInvitationReceivedEvent) e; requestReceived = true; if (!answer) return; Blog b = event.getShareable(); try { eventWaiter.assertEquals(1, blogSharingManager1.getInvitations().size()); Contact c = contactManager1.getContact(event.getContactId()); blogSharingManager1.respondToInvitation(b, c, accept); } catch (DbException ex) { eventWaiter.rethrow(ex); } finally { eventWaiter.resume(); } } // this is only needed for tests where a blog is re-shared else if (e instanceof BlogInvitationResponseReceivedEvent) { BlogInvitationResponseReceivedEvent event = (BlogInvitationResponseReceivedEvent) e; eventWaiter.assertEquals(contactId01, event.getContactId()); eventWaiter.resume(); } } } private void startLifecycles() throws InterruptedException { // Start the lifecycle manager and wait for it to finish lifecycleManager0 = t0.getLifecycleManager(); lifecycleManager1 = t1.getLifecycleManager(); lifecycleManager2 = t2.getLifecycleManager(); lifecycleManager0.startServices(); lifecycleManager1.startServices(); lifecycleManager2.startServices(); lifecycleManager0.waitForStartup(); lifecycleManager1.waitForStartup(); lifecycleManager2.waitForStartup(); } private void stopLifecycles() throws InterruptedException { // Clean up lifecycleManager0.stopServices(); lifecycleManager1.stopServices(); lifecycleManager2.stopServices(); lifecycleManager0.waitForShutdown(); lifecycleManager1.waitForShutdown(); lifecycleManager2.waitForShutdown(); } private void defaultInit(boolean accept) throws DbException { addDefaultIdentities(); addDefaultContacts(); getPersonalBlogOfSharer(); listenToEvents(accept); } private void addDefaultIdentities() throws DbException { KeyPair keyPair = cryptoComponent.generateSignatureKeyPair(); author0 = authorFactory.createLocalAuthor(SHARER, keyPair.getPublic().getEncoded(), keyPair.getPrivate().getEncoded()); identityManager0.addLocalAuthor(author0); keyPair = cryptoComponent.generateSignatureKeyPair(); author1 = authorFactory.createLocalAuthor(INVITEE, keyPair.getPublic().getEncoded(), keyPair.getPrivate().getEncoded()); identityManager1.addLocalAuthor(author1); keyPair = cryptoComponent.generateSignatureKeyPair(); author2 = authorFactory.createLocalAuthor(CONTACT2, keyPair.getPublic().getEncoded(), keyPair.getPrivate().getEncoded()); identityManager2.addLocalAuthor(author2); } private void addDefaultContacts() throws DbException { // sharer adds invitee as contact contactId1 = contactManager0.addContact(author1, author0.getId(), master, clock.currentTimeMillis(), true, true, true ); contact1 = contactManager0.getContact(contactId1); // sharer adds second contact contactId2 = contactManager0.addContact(author2, author0.getId(), master, clock.currentTimeMillis(), true, true, true ); contact2 = contactManager0.getContact(contactId2); // contacts add sharer back contactId01 = contactManager1.addContact(author0, author1.getId(), master, clock.currentTimeMillis(), true, true, true ); contact01 = contactManager1.getContact(contactId01); contactId02 = contactManager2.addContact(author0, author2.getId(), master, clock.currentTimeMillis(), true, true, true ); contact02 = contactManager2.getContact(contactId02); } private void getPersonalBlogOfSharer() throws DbException { blog0 = blogManager0.getPersonalBlog(author0); blog1 = blogManager0.getPersonalBlog(author1); blog2 = blogManager0.getPersonalBlog(author2); } private void listenToEvents(boolean accept) { listener0 = new SharerListener(); t0.getEventBus().addListener(listener0); listener1 = new InviteeListener(accept); t1.getEventBus().addListener(listener1); listener2 = new SharerListener(); t2.getEventBus().addListener(listener2); } private void sync0To1() throws IOException, TimeoutException { deliverMessage(sync0, contactId01, sync1, contactId1, "Sharer to Invitee"); } private void sync1To0() throws IOException, TimeoutException { deliverMessage(sync1, contactId1, sync0, contactId01, "Invitee to Sharer"); } 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(); // wait for message to actually arrive msgWaiter.await(TIMEOUT, 1); } private void injectEagerSingletons( BlogSharingIntegrationTestComponent 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 SharingModule.EagerSingletons()); component.inject(new SyncModule.EagerSingletons()); component.inject(new PropertiesModule.EagerSingletons()); } }