org.obm.opush.PingHandlerTest.java Source code

Java tutorial

Introduction

Here is the source code for org.obm.opush.PingHandlerTest.java

Source

/* ***** BEGIN LICENSE BLOCK *****
 * 
 * Copyright (C) 2011-2014  Linagora
 *
 * This program is free software: you can redistribute it and/or 
 * modify it under the terms of the GNU Affero General Public License as 
 * published by the Free Software Foundation, either version 3 of the 
 * License, or (at your option) any later version, provided you comply 
 * with the Additional Terms applicable for OBM connector by Linagora 
 * pursuant to Section 7 of the GNU Affero General Public License, 
 * subsections (b), (c), and (e), pursuant to which you must notably (i) retain 
 * the Message sent thanks to OBM, Free Communication by Linagora? 
 * signature notice appended to any and all outbound messages 
 * (notably e-mail and meeting requests), (ii) retain all hypertext links between 
 * OBM and obm.org, as well as between Linagora and linagora.com, and (iii) refrain 
 * from infringing Linagora intellectual property rights over its trademarks 
 * and commercial brands. Other Additional Terms apply, 
 * see <http://www.linagora.com/licenses/> for more details. 
 *
 * This program is distributed in the hope that it will be useful, 
 * but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 
 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License 
 * for more details. 
 *
 * You should have received a copy of the GNU Affero General Public License 
 * and its applicable Additional Terms for OBM along with this program. If not, 
 * see <http://www.gnu.org/licenses/> for the GNU Affero General Public License version 3 
 * and <http://www.linagora.com/licenses/> for the Additional Terms applicable to 
 * OBM connectors. 
 * 
 * ***** END LICENSE BLOCK ***** */
package org.obm.opush;

import static org.assertj.core.api.Assertions.assertThat;
import static org.easymock.EasyMock.anyLong;
import static org.easymock.EasyMock.anyObject;
import static org.easymock.EasyMock.expect;
import static org.easymock.EasyMock.expectLastCall;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import javax.xml.transform.TransformerException;

import org.apache.http.client.fluent.Async;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.assertj.core.util.Files;
import org.easymock.IMocksControl;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.obm.Configuration;
import org.obm.ConfigurationModule.PolicyConfigurationProvider;
import org.obm.configuration.EmailConfiguration;
import org.obm.guice.GuiceModule;
import org.obm.guice.GuiceRunner;
import org.obm.opush.Users.OpushUser;
import org.obm.opush.env.CassandraServer;
import org.obm.opush.env.DefaultOpushModule;
import org.obm.push.OpushServer;
import org.obm.push.bean.Device;
import org.obm.push.bean.FolderType;
import org.obm.push.bean.ItemSyncState;
import org.obm.push.bean.PingStatus;
import org.obm.push.bean.SyncCollectionOptions;
import org.obm.push.bean.SyncKey;
import org.obm.push.bean.UserDataRequest;
import org.obm.push.bean.change.hierarchy.BackendFolder.BackendId;
import org.obm.push.bean.change.hierarchy.Folder;
import org.obm.push.bean.change.hierarchy.FolderSnapshot;
import org.obm.push.bean.change.hierarchy.MailboxPath;
import org.obm.push.calendar.CalendarBackend;
import org.obm.push.exception.ConversionException;
import org.obm.push.exception.DaoException;
import org.obm.push.exception.UnexpectedObmSyncServerException;
import org.obm.push.exception.activesync.CollectionNotFoundException;
import org.obm.push.exception.activesync.HierarchyChangedException;
import org.obm.push.exception.activesync.NotAllowedException;
import org.obm.push.protocol.PingProtocol;
import org.obm.push.protocol.bean.CollectionId;
import org.obm.push.protocol.bean.PingResponse;
import org.obm.push.service.FolderSnapshotDao;
import org.obm.push.state.FolderSyncKey;
import org.obm.push.store.CollectionDao;
import org.obm.push.store.HeartbeatDao;
import org.obm.push.utils.DOMUtils;
import org.obm.push.utils.DateUtils;
import org.obm.sync.auth.AuthFault;
import org.obm.sync.push.client.OPClient;
import org.w3c.dom.Document;
import org.xml.sax.SAXException;

import com.google.common.base.Optional;
import com.google.common.base.Stopwatch;
import com.google.common.collect.ImmutableSet;
import com.google.inject.Inject;

@RunWith(GuiceRunner.class)
@GuiceModule(DefaultOpushModule.class)
public class PingHandlerTest {

    @Inject
    Users users;
    @Inject
    OpushServer opushServer;
    @Inject
    IMocksControl mocksControl;
    @Inject
    Configuration configuration;
    @Inject
    PolicyConfigurationProvider policyConfigurationProvider;
    @Inject
    PingProtocol pingProtocol;
    @Inject
    CassandraServer cassandraServer;
    @Inject
    PendingQueriesLock queriesLock;
    @Inject
    IntegrationTestUtils testUtils;
    @Inject
    IntegrationUserAccessUtils userAccessUtils;
    @Inject
    HeartbeatDao heartbeatDao;
    @Inject
    CalendarBackend calendarBackend;
    @Inject
    CollectionDao collectionDao;
    @Inject
    FolderSnapshotDao folderSnapshotDao;

    private CloseableHttpClient httpClient;
    private ExecutorService threadpool;
    private Async async;

    @Before
    public void init() throws Exception {
        cassandraServer.start();

        threadpool = Executors.newFixedThreadPool(4);
        async = Async.newInstance().use(threadpool);
        httpClient = HttpClientBuilder.create().build();

        expect(policyConfigurationProvider.get()).andReturn("fakeConfiguration");
    }

    @After
    public void shutdown() throws Exception {
        opushServer.stop();
        cassandraServer.stop();
        threadpool.shutdown();
        httpClient.close();
        Files.delete(configuration.dataDir);
    }

    @Test
    @Ignore("OBMFULL-4125")
    public void testInterval() throws Exception {
        testHeartbeatInterval(5, 5, 5);
    }

    @Test
    @Ignore("OBMFULL-4125")
    public void testMinInterval() throws Exception {
        testHeartbeatInterval(1, 5, 5);
    }

    @Test
    @Ignore("OBMFULL-5442")
    public void testNoChange() throws Exception {
        prepareMockNoChange(Arrays.asList(users.jaures));

        opushServer.start();

        OPClient opClient = testUtils.buildWBXMLOpushClient(users.jaures, opushServer.getHttpPort(), httpClient);
        Document document = DOMUtils.parse("<?xml version=\"1.0\" encoding=\"UTF-8\"?>" + "<Ping>"
                + "<HeartbeatInterval>5</HeartbeatInterval>" + "<Folders>" + "<Folder>" + "<Id>1</Id>"
                + "<Class>Calendar</Class>" + "</Folder>" + "<Folder>" + "<Id>4</Id>" + "<Class>Contacts</Class>"
                + "</Folder>" + "</Folders>" + "</Ping>");

        Stopwatch stopwatch = Stopwatch.createStarted();
        Document response = opClient.postXml("Ping", document, "Ping", null, false);

        checkExecutionTime(2, 5, stopwatch);
        assertThat(DOMUtils.serialize(response)).isEqualTo(
                "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" + "<Ping>" + "<Status>2</Status>" + "</Ping>");

    }

    @Test
    @Ignore("OBMFULL-4125")
    public void test3BlockingClient() throws Exception {
        prepareMockNoChange(Arrays.asList(users.jaures));

        opushServer.start();

        OPClient opClient = testUtils.buildWBXMLOpushClient(users.jaures, opushServer.getHttpPort(), httpClient);

        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(20, 20, 1, TimeUnit.MINUTES,
                new LinkedBlockingQueue<Runnable>());

        Stopwatch stopwatch = Stopwatch.createStarted();

        List<Future<Document>> futures = new ArrayList<Future<Document>>();
        for (int i = 0; i < 4; ++i) {
            futures.add(queuePingCommand(opClient, users.jaures, threadPoolExecutor));
        }

        for (Future<Document> f : futures) {
            Document response = f.get();
            checkNoChangeResponse(response);
        }

        checkExecutionTime(2, 5, stopwatch);
    }

    @Test
    @Ignore("OBMFULL-4125")
    public void testPushNotificationOnBackendChangeShort() throws Exception {
        prepareMockHasChanges(1, Arrays.asList(users.jaures));

        opushServer.start();

        OPClient opClient = testUtils.buildWBXMLOpushClient(users.jaures, opushServer.getHttpPort(), httpClient);
        Document document = buildPingCommand(20, users.jaures.hashCode());
        Stopwatch stopwatch = Stopwatch.createStarted();

        Document response = opClient.postXml("Ping", document, "Ping", null, false);

        checkExecutionTime(5, 1, stopwatch);
        checkHasChangeResponse(response);
    }

    @Test
    @Ignore("OBMFULL-4125")
    public void testPushNotificationOnBackendChangeLong() throws Exception {
        prepareMockHasChanges(2, Arrays.asList(users.jaures));

        opushServer.start();

        Document document = buildPingCommand(20, users.jaures.hashCode());
        Stopwatch stopwatch = Stopwatch.createStarted();

        OPClient opClient = testUtils.buildWBXMLOpushClient(users.jaures, opushServer.getHttpPort(), httpClient);
        Document response = opClient.postXml("Ping", document, "Ping", null, false);

        checkExecutionTime(5, 6, stopwatch);
        checkHasChangeResponse(response);
    }

    @Test
    @Ignore("OBMFULL-4125")
    public void testPushNotificationOnBackendHierarchyChangedException() throws Exception {
        prepareMockHierarchyChangedException(Arrays.asList(users.jaures));

        opushServer.start();

        Document document = buildPingCommand(20, users.jaures.hashCode());

        OPClient opClient = testUtils.buildWBXMLOpushClient(users.jaures, opushServer.getHttpPort(), httpClient);
        Document response = opClient.postXml("Ping", document, "Ping", null, false);

        checkFolderSyncRequiredResponse(response);
    }

    @Test
    public void testPingAfterPingTimeout() throws Exception {
        int heartbeat = 5;
        CollectionId collectionId = CollectionId.of(users.jaures.hashCode());

        prepareMockHasChanges(1, Arrays.asList(users.jaures));

        Folder folder = Folder.builder().collectionId(collectionId)
                .backendId(MailboxPath.of(EmailConfiguration.IMAP_INBOX_NAME))
                .displayName(EmailConfiguration.IMAP_INBOX_NAME).folderType(FolderType.DEFAULT_INBOX_FOLDER)
                .parentBackendIdOpt(Optional.<BackendId>absent()).build();

        FolderSyncKey folderSyncKey = new FolderSyncKey("c8355d6c-9325-490a-87ec-2522b2e23b99");
        folderSnapshotDao.create(users.jaures.user, users.jaures.device, folderSyncKey,
                FolderSnapshot.nextId(2).folders(ImmutableSet.of(folder)));

        opushServer.start();

        OPClient opClient = testUtils.buildWBXMLOpushClient(users.jaures, opushServer.getHttpPort(), httpClient);

        Future<PingResponse> response1 = opClient.pingASync(async, pingProtocol, collectionId, heartbeat);
        PingResponse pingResponse1 = response1.get(heartbeat * 2, TimeUnit.SECONDS);
        Future<PingResponse> response2 = opClient.pingASync(async, pingProtocol, collectionId, heartbeat);
        PingResponse pingResponse2 = response2.get(heartbeat * 2, TimeUnit.SECONDS);

        assertThat(pingResponse1.getPingStatus()).isEqualTo(PingStatus.NO_CHANGES);
        assertThat(pingResponse2.getPingStatus()).isEqualTo(PingStatus.NO_CHANGES);
    }

    @Test(expected = TimeoutException.class)
    public void testTwoUsersPingWithSameDevice() throws Exception {
        CollectionId collectionId = CollectionId.of(users.jaures.hashCode());
        int heartbeat = 10;

        prepareMockHasChanges(1, Arrays.asList(users.jaures, users.blum));

        Folder folder = Folder.builder().collectionId(collectionId)
                .backendId(MailboxPath.of(EmailConfiguration.IMAP_INBOX_NAME))
                .displayName(EmailConfiguration.IMAP_INBOX_NAME).folderType(FolderType.DEFAULT_INBOX_FOLDER)
                .parentBackendIdOpt(Optional.<BackendId>absent()).build();

        FolderSyncKey folderSyncKey = new FolderSyncKey("c8355d6c-9325-490a-87ec-2522b2e23b99");
        folderSnapshotDao.create(users.jaures.user, users.jaures.device, folderSyncKey,
                FolderSnapshot.nextId(2).folders(ImmutableSet.of(folder)));

        opushServer.start();

        OPClient opClientJaures = testUtils.buildWBXMLOpushClient(users.jaures, opushServer.getHttpPort(),
                httpClient);
        OPClient opClientBlum = testUtils.buildWBXMLOpushClient(users.blum, opushServer.getHttpPort(), httpClient);

        queriesLock.expectedQueriesCountToBeStarted(1);
        Future<PingResponse> response1 = opClientJaures.pingASync(async, pingProtocol, collectionId, 1000);
        queriesLock.waitingStart(3, TimeUnit.SECONDS);
        queriesLock.waitingClose(3, TimeUnit.SECONDS);
        opClientBlum.pingASync(async, pingProtocol, CollectionId.of(users.blum.hashCode()), heartbeat);
        response1.get(1, TimeUnit.SECONDS);
    }

    private void prepareMockNoChange(List<OpushUser> users) throws DaoException, CollectionNotFoundException,
            UnexpectedObmSyncServerException, AuthFault, ConversionException, HierarchyChangedException {
        userAccessUtils.mockUsersAccess(users);
        mockForPingNeeds();
        mockForNoChangePing();
        mocksControl.replay();
    }

    private void prepareMockHasChanges(int noChangeIterationCount, List<OpushUser> users)
            throws DaoException, CollectionNotFoundException, UnexpectedObmSyncServerException, AuthFault,
            ConversionException, HierarchyChangedException {
        userAccessUtils.mockUsersAccess(users);
        mockForPingNeeds();
        mockForCalendarHasChangePing(noChangeIterationCount, users);
        mocksControl.replay();
    }

    private void prepareMockHierarchyChangedException(List<OpushUser> users)
            throws DaoException, CollectionNotFoundException, UnexpectedObmSyncServerException, AuthFault,
            ConversionException, HierarchyChangedException {
        userAccessUtils.mockUsersAccess(users);
        mockForPingNeeds();
        mockForCalendarHierarchyChangedException(users);
        mocksControl.replay();
    }

    public void testHeartbeatInterval(int heartbeatInterval, int delta, int expected) throws Exception {
        prepareMockNoChange(Arrays.asList(users.jaures));

        opushServer.start();

        Document document = buildPingCommand(heartbeatInterval, users.jaures.hashCode());
        Stopwatch stopwatch = Stopwatch.createStarted();

        OPClient opClient = testUtils.buildWBXMLOpushClient(users.jaures, opushServer.getHttpPort(), httpClient);
        Document response = opClient.postXml("Ping", document, "Ping", null, false);

        checkExecutionTime(delta, expected, stopwatch);
        checkNoChangeResponse(response);
    }

    private void checkExecutionTime(int delta, int expected, Stopwatch stopwatch) {
        stopwatch.stop();
        long elapsedTime = stopwatch.elapsed(TimeUnit.SECONDS);
        assertThat(elapsedTime).isGreaterThanOrEqualTo(expected).isLessThan(expected + delta);
    }

    private void checkNoChangeResponse(Document response) throws TransformerException {
        assertThat(DOMUtils.serialize(response)).isEqualTo(
                "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" + "<Ping><Status>1</Status><Folders/></Ping>");
    }

    private void checkHasChangeResponse(Document response) throws TransformerException {
        assertThat(DOMUtils.serialize(response)).isEqualTo("<?xml version=\"1.0\" encoding=\"UTF-8\"?><Ping>"
                + "<Status>2</Status>" + "<Folders><Folder>1432</Folder></Folders></Ping>");
    }

    private void checkFolderSyncRequiredResponse(Document response) throws TransformerException {
        assertThat(DOMUtils.serialize(response))
                .isEqualTo("<?xml version=\"1.0\" encoding=\"UTF-8\"?><Ping>" + "<Status>7</Status>" + "</Ping>");
    }

    private void mockForPingNeeds() throws DaoException {
        mockHeartbeatDao();
    }

    private void mockForNoChangePing() throws DaoException, CollectionNotFoundException,
            UnexpectedObmSyncServerException, ConversionException, HierarchyChangedException {
        mockCalendarBackendHasNoChange();
        testUtils.expectUserCollectionsNeverChange();
    }

    private void mockForCalendarHasChangePing(int noChangeIterationCount, List<OpushUser> users)
            throws DaoException, CollectionNotFoundException, UnexpectedObmSyncServerException, ConversionException,
            HierarchyChangedException {
        mockCollectionDaoHasChange(noChangeIterationCount, users);
        mockCalendarBackendHasContentChanges();
    }

    private void mockForCalendarHierarchyChangedException(List<OpushUser> users)
            throws DaoException, CollectionNotFoundException, UnexpectedObmSyncServerException, ConversionException,
            HierarchyChangedException {
        mockCollectionDaoHasChange(1, users);
        mockCalendarBackendHierarchyChangedException();
    }

    private void mockCollectionDaoHasChange(int noChangeIterationCount, List<OpushUser> users)
            throws DaoException, CollectionNotFoundException {
        int collectionNoChangeIterationCount = noChangeIterationCount;

        expectCollectionDaoUnchangeForXIteration(collectionNoChangeIterationCount);

        for (OpushUser user : users) {
            CollectionId collectionId = CollectionId.of(user.hashCode());

            ItemSyncState syncState = ItemSyncState.builder().syncKey(new SyncKey("sync state"))
                    .syncDate(DateUtils.getCurrentDate()).build();
            expect(collectionDao.lastKnownState(user.device, collectionId)).andReturn(syncState).once();
        }
    }

    private void expectCollectionDaoUnchangeForXIteration(int noChangeIterationCount) throws DaoException {
        ItemSyncState syncState = ItemSyncState.builder().syncKey(new SyncKey("sync state"))
                .syncDate(DateUtils.getCurrentDate()).build();
        expect(collectionDao.lastKnownState(anyObject(Device.class), anyObject(CollectionId.class)))
                .andReturn(syncState).times(noChangeIterationCount);
    }

    private void mockCalendarBackendHasContentChanges() throws CollectionNotFoundException, DaoException,
            UnexpectedObmSyncServerException, ConversionException, HierarchyChangedException {

        expect(calendarBackend.getItemEstimateSize(anyObject(UserDataRequest.class), anyObject(ItemSyncState.class),
                anyObject(CollectionId.class), anyObject(SyncCollectionOptions.class))).andReturn(1).times(2);
    }

    private void mockCalendarBackendHierarchyChangedException() throws CollectionNotFoundException, DaoException,
            UnexpectedObmSyncServerException, ConversionException, HierarchyChangedException {

        expect(calendarBackend.getItemEstimateSize(anyObject(UserDataRequest.class), anyObject(ItemSyncState.class),
                anyObject(CollectionId.class), anyObject(SyncCollectionOptions.class)))
                        .andThrow(new HierarchyChangedException(new NotAllowedException("Not allowed")));
    }

    private void mockCalendarBackendHasNoChange() throws CollectionNotFoundException, DaoException,
            UnexpectedObmSyncServerException, ConversionException, HierarchyChangedException {

        expect(calendarBackend.getItemEstimateSize(anyObject(UserDataRequest.class), anyObject(ItemSyncState.class),
                anyObject(CollectionId.class), anyObject(SyncCollectionOptions.class))).andReturn(0).anyTimes();
    }

    private Future<Document> queuePingCommand(final OPClient opClient, final OpushUser user,
            ThreadPoolExecutor threadPoolExecutor) {
        return threadPoolExecutor.submit(new Callable<Document>() {
            @Override
            public Document call() throws Exception {
                Document document = buildPingCommand(5, user.hashCode());
                return opClient.postXml("Ping", document, "Ping", null, false);
            }
        });
    }

    private void mockHeartbeatDao() throws DaoException {
        heartbeatDao.updateLastHeartbeat(anyObject(Device.class), anyLong());
        expectLastCall().anyTimes();
    }

    private Document buildPingCommand(int heartbeatInterval, int collectionId) throws SAXException, IOException {
        return DOMUtils.parse("<Ping>" + "<HeartbeatInterval>" + heartbeatInterval + "</HeartbeatInterval>"
                + "<Folders>" + "<Folder>" + "<Id>" + String.valueOf(collectionId) + "</Id>" + "</Folder>"
                + "</Folders>" + "</Ping>");
    }
}