Bug 1291821 - Get tests to work after sync changes r=rnewman draft
authorGrisha Kruglov <gkruglov@mozilla.com>
Tue, 11 Oct 2016 20:02:02 -0700
changeset 489495 0f51adf71bd0f157da201b2ea7e4092c0acaf9a6
parent 489494 da0d451422e4733e5a6ab8a4558150197f08c253
child 489496 96f7211951611ce7785edbef9dce412accb2878d
push id46825
push usergkruglov@mozilla.com
push dateFri, 24 Feb 2017 21:13:39 +0000
reviewersrnewman
bugs1291821
milestone54.0a1
Bug 1291821 - Get tests to work after sync changes r=rnewman MozReview-Commit-ID: 3djnmEmzndU
mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestGlobalSession.java
mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestServer11Repository.java
mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestServer11RepositorySession.java
mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/downloaders/BatchingDownloaderTest.java
mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/uploaders/BatchingUploaderTest.java
mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/stage/test/TestEnsureCrypto5KeysStage.java
mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/stage/test/TestFetchMetaGlobalStage.java
--- a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestGlobalSession.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestGlobalSession.java
@@ -1,13 +1,15 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.android.sync.net.test;
 
+import android.os.SystemClock;
+
 import ch.boye.httpclientandroidlib.HttpResponse;
 import ch.boye.httpclientandroidlib.ProtocolVersion;
 import ch.boye.httpclientandroidlib.message.BasicHttpResponse;
 import ch.boye.httpclientandroidlib.message.BasicStatusLine;
 import junit.framework.AssertionFailedError;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -45,31 +47,33 @@ import org.simpleframework.http.Response
 
 import java.io.IOException;
 import java.net.URISyntaxException;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
+import java.util.concurrent.TimeUnit;
 
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
 @RunWith(TestRunner.class)
 public class TestGlobalSession {
   private int          TEST_PORT                = HTTPServerTestHelper.getTestPort();
   private final String TEST_CLUSTER_URL         = "http://localhost:" + TEST_PORT;
   private final String TEST_USERNAME            = "johndoe";
   private final String TEST_PASSWORD            = "password";
   private final String TEST_SYNC_KEY            = "abcdeabcdeabcdeabcdeabcdea";
   private final long   TEST_BACKOFF_IN_SECONDS  = 2401;
+  private final long   SYNC_DEADLINE            = SystemClock.elapsedRealtime() + TimeUnit.MINUTES.toMillis(30);
 
   public static WaitHelper getTestWaiter() {
     return WaitHelper.getTestWaiter();
   }
 
   @Test
   public void testGetSyncStagesBy() throws SyncConfigurationException, IllegalArgumentException, NonObjectJSONException, IOException, CryptoException, NoSuchStageException {
 
@@ -142,17 +146,17 @@ public class TestGlobalSession {
       final MockGlobalSessionCallback callback = new MockGlobalSessionCallback();
       SyncConfiguration config = new SyncConfiguration(TEST_USERNAME, new BasicAuthHeaderProvider(TEST_USERNAME, TEST_PASSWORD), new MockSharedPreferences(), new KeyBundle(TEST_USERNAME, TEST_SYNC_KEY));
       final GlobalSession session = new MockGlobalSession(config, callback);
 
       getTestWaiter().performWait(WaitHelper.onThreadRunnable(new Runnable() {
         @Override
         public void run() {
           try {
-            session.start();
+            session.start(SYNC_DEADLINE);
           } catch (Exception e) {
             final AssertionFailedError error = new AssertionFailedError();
             error.initCause(e);
             getTestWaiter().performNotify(error);
           }
         }
       }));
 
@@ -190,17 +194,17 @@ public class TestGlobalSession {
       SyncConfiguration config = new SyncConfiguration(TEST_USERNAME, new BasicAuthHeaderProvider(TEST_USERNAME, TEST_PASSWORD), new MockSharedPreferences(), new KeyBundle(TEST_USERNAME, TEST_SYNC_KEY));
       final GlobalSession session = new MockGlobalSession(config, callback)
                                         .withStage(Stage.fetchInfoCollections, stage);
 
       getTestWaiter().performWait(WaitHelper.onThreadRunnable(new Runnable() {
         @Override
         public void run() {
           try {
-            session.start();
+            session.start(SYNC_DEADLINE);
           } catch (Exception e) {
             final AssertionFailedError error = new AssertionFailedError();
             error.initCause(e);
             getTestWaiter().performNotify(error);
           }
         }
       }));
 
@@ -270,17 +274,17 @@ public class TestGlobalSession {
     final GlobalSession session = new MockGlobalSession(config, callback)
                                       .withStage(Stage.syncBookmarks, stage);
 
     data.startHTTPServer(server);
     WaitHelper.getTestWaiter().performWait(WaitHelper.onThreadRunnable(new Runnable() {
       @Override
       public void run() {
         try {
-          session.start();
+          session.start(SYNC_DEADLINE);
         } catch (Exception e) {
           final AssertionFailedError error = new AssertionFailedError();
           error.initCause(e);
           WaitHelper.getTestWaiter().performNotify(error);
         }
       }
     }));
     data.stopHTTPServer();
--- a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestServer11Repository.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestServer11Repository.java
@@ -1,37 +1,41 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.android.sync.net.test;
 
+import android.os.SystemClock;
+
 import org.junit.Assert;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mozilla.gecko.background.testhelpers.TestRunner;
 import org.mozilla.gecko.sync.InfoCollections;
 import org.mozilla.gecko.sync.InfoConfiguration;
 import org.mozilla.gecko.sync.repositories.Server11Repository;
 
 import java.net.URI;
 import java.net.URISyntaxException;
+import java.util.concurrent.TimeUnit;
 
 @RunWith(TestRunner.class)
 public class TestServer11Repository {
 
   private static final String COLLECTION = "bookmarks";
-  private static final String COLLECTION_URL = "http://foo.com/1.1/n6ec3u5bee3tixzp2asys7bs6fve4jfw/storage";
+  private static final String COLLECTION_URL = "http://foo.com/1.5/n6ec3u5bee3tixzp2asys7bs6fve4jfw/storage";
+  private static final long SYNC_DEADLINE = SystemClock.elapsedRealtime() + TimeUnit.MINUTES.toMillis(30);
 
   protected final InfoCollections infoCollections = new InfoCollections();
   protected final InfoConfiguration infoConfiguration = new InfoConfiguration();
 
   public static void assertQueryEquals(String expected, URI u) {
     Assert.assertEquals(expected, u.getRawQuery());
   }
 
   @Test
   public void testCollectionURI() throws URISyntaxException {
-    Server11Repository noTrailingSlash = new Server11Repository(COLLECTION, COLLECTION_URL, null, infoCollections, infoConfiguration);
-    Server11Repository trailingSlash = new Server11Repository(COLLECTION, COLLECTION_URL + "/", null, infoCollections, infoConfiguration);
-    Assert.assertEquals("http://foo.com/1.1/n6ec3u5bee3tixzp2asys7bs6fve4jfw/storage/bookmarks", noTrailingSlash.collectionURI().toASCIIString());
-    Assert.assertEquals("http://foo.com/1.1/n6ec3u5bee3tixzp2asys7bs6fve4jfw/storage/bookmarks", trailingSlash.collectionURI().toASCIIString());
+    Server11Repository noTrailingSlash = new Server11Repository(COLLECTION, SYNC_DEADLINE, COLLECTION_URL, null, infoCollections, infoConfiguration);
+    Server11Repository trailingSlash = new Server11Repository(COLLECTION, SYNC_DEADLINE, COLLECTION_URL + "/", null, infoCollections, infoConfiguration);
+    Assert.assertEquals("http://foo.com/1.5/n6ec3u5bee3tixzp2asys7bs6fve4jfw/storage/bookmarks", noTrailingSlash.collectionURI().toASCIIString());
+    Assert.assertEquals("http://foo.com/1.5/n6ec3u5bee3tixzp2asys7bs6fve4jfw/storage/bookmarks", trailingSlash.collectionURI().toASCIIString());
   }
 }
--- a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestServer11RepositorySession.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestServer11RepositorySession.java
@@ -1,47 +1,44 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.android.sync.test;
 
+import android.os.SystemClock;
+
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mozilla.android.sync.test.SynchronizerHelpers.TrackingWBORepository;
 import org.mozilla.android.sync.test.helpers.BaseTestStorageRequestDelegate;
 import org.mozilla.android.sync.test.helpers.HTTPServerTestHelper;
 import org.mozilla.android.sync.test.helpers.MockServer;
 import org.mozilla.gecko.background.testhelpers.TestRunner;
-import org.mozilla.gecko.background.testhelpers.WaitHelper;
 import org.mozilla.gecko.sync.InfoCollections;
 import org.mozilla.gecko.sync.InfoConfiguration;
-import org.mozilla.gecko.sync.JSONRecordFetcher;
 import org.mozilla.gecko.sync.Utils;
 import org.mozilla.gecko.sync.crypto.KeyBundle;
 import org.mozilla.gecko.sync.middleware.Crypto5MiddlewareRepository;
 import org.mozilla.gecko.sync.net.AuthHeaderProvider;
 import org.mozilla.gecko.sync.net.BaseResource;
 import org.mozilla.gecko.sync.net.BasicAuthHeaderProvider;
 import org.mozilla.gecko.sync.net.SyncStorageResponse;
 import org.mozilla.gecko.sync.repositories.FetchFailedException;
-import org.mozilla.gecko.sync.repositories.RepositorySession;
 import org.mozilla.gecko.sync.repositories.Server11Repository;
 import org.mozilla.gecko.sync.repositories.StoreFailedException;
-import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate;
 import org.mozilla.gecko.sync.repositories.domain.BookmarkRecord;
 import org.mozilla.gecko.sync.repositories.domain.BookmarkRecordFactory;
-import org.mozilla.gecko.sync.stage.SafeConstrainedServer11Repository;
 import org.mozilla.gecko.sync.synchronizer.ServerLocalSynchronizer;
 import org.mozilla.gecko.sync.synchronizer.Synchronizer;
 import org.simpleframework.http.ContentType;
 import org.simpleframework.http.Request;
 import org.simpleframework.http.Response;
 
 import java.io.IOException;
-import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.TimeUnit;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 
 @RunWith(TestRunner.class)
 public class TestServer11RepositorySession {
 
@@ -58,36 +55,31 @@ public class TestServer11RepositorySessi
       System.out.println("Content-Type:" + contentType);
       super.handle(request, response, 200, "{success:[]}");
     }
   }
 
   private static final int    TEST_PORT   = HTTPServerTestHelper.getTestPort();
   private static final String TEST_SERVER = "http://localhost:" + TEST_PORT + "/";
   static final String LOCAL_BASE_URL      = TEST_SERVER + "1.1/n6ec3u5bee3tixzp2asys7bs6fve4jfw/";
-  static final String LOCAL_INFO_BASE_URL = LOCAL_BASE_URL + "info/";
-  static final String LOCAL_COUNTS_URL    = LOCAL_INFO_BASE_URL + "collection_counts";
 
   // Corresponds to rnewman+atest1@mozilla.com, local.
   static final String TEST_USERNAME          = "n6ec3u5bee3tixzp2asys7bs6fve4jfw";
   static final String TEST_PASSWORD          = "passowrd";
   static final String SYNC_KEY          = "eh7ppnb82iwr5kt3z3uyi5vr44";
 
   public final AuthHeaderProvider authHeaderProvider = new BasicAuthHeaderProvider(TEST_USERNAME, TEST_PASSWORD);
   protected final InfoCollections infoCollections = new InfoCollections() {
     @Override
     public Long getTimestamp(String collection) {
       return 0L;
     }
   };
   protected final InfoConfiguration infoConfiguration = new InfoConfiguration();
 
-  // Few-second timeout so that our longer operations don't time out and cause spurious error-handling results.
-  private static final int SHORT_TIMEOUT = 10000;
-
   public AuthHeaderProvider getAuthHeaderProvider() {
     return new BasicAuthHeaderProvider(TEST_USERNAME, TEST_PASSWORD);
   }
 
   private HTTPServerTestHelper data     = new HTTPServerTestHelper();
 
   public class TestSyncStorageRequestDelegate extends
   BaseTestStorageRequestDelegate {
@@ -113,17 +105,19 @@ public class TestServer11RepositorySessi
     }
     return local;
   }
 
   protected Exception doSynchronize(MockServer server) throws Exception {
     final String COLLECTION = "test";
 
     final TrackingWBORepository local = getLocal(100);
-    final Server11Repository remote = new Server11Repository(COLLECTION, getCollectionURL(COLLECTION), authHeaderProvider, infoCollections, infoConfiguration);
+    final Server11Repository remote = new Server11Repository(
+            COLLECTION, SystemClock.elapsedRealtime() + TimeUnit.MINUTES.toMillis(30),
+            getCollectionURL(COLLECTION), authHeaderProvider, infoCollections, infoConfiguration);
     KeyBundle collectionKey = new KeyBundle(TEST_USERNAME, SYNC_KEY);
     Crypto5MiddlewareRepository cryptoRepo = new Crypto5MiddlewareRepository(remote, collectionKey);
     cryptoRepo.recordFactory = new BookmarkRecordFactory();
 
     final Synchronizer synchronizer = new ServerLocalSynchronizer();
     synchronizer.repositoryA = cryptoRepo;
     synchronizer.repositoryB = local;
 
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/downloaders/BatchingDownloaderTest.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/downloaders/BatchingDownloaderTest.java
@@ -1,37 +1,41 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.sync.repositories.downloaders;
 
+import android.net.Uri;
+import android.os.SystemClock;
 import android.support.annotation.NonNull;
 
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mozilla.gecko.background.testhelpers.TestRunner;
 import org.mozilla.gecko.sync.CryptoRecord;
 import org.mozilla.gecko.sync.InfoCollections;
 import org.mozilla.gecko.sync.InfoConfiguration;
 import org.mozilla.gecko.sync.net.AuthHeaderProvider;
 import org.mozilla.gecko.sync.net.SyncResponse;
 import org.mozilla.gecko.sync.net.SyncStorageCollectionRequest;
 import org.mozilla.gecko.sync.net.SyncStorageResponse;
-import org.mozilla.gecko.sync.repositories.Repository;
+import org.mozilla.gecko.sync.repositories.RepositorySession;
 import org.mozilla.gecko.sync.repositories.Server11Repository;
 import org.mozilla.gecko.sync.repositories.Server11RepositorySession;
+import org.mozilla.gecko.sync.repositories.Repository;
 import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFetchRecordsDelegate;
 import org.mozilla.gecko.sync.repositories.domain.Record;
 
 import java.io.UnsupportedEncodingException;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
 
 import ch.boye.httpclientandroidlib.ProtocolVersion;
 import ch.boye.httpclientandroidlib.message.BasicHttpResponse;
 import ch.boye.httpclientandroidlib.message.BasicStatusLine;
 
 import static org.junit.Assert.*;
 import static org.junit.Assert.assertEquals;
 
@@ -39,17 +43,17 @@ import static org.junit.Assert.assertEqu
 public class BatchingDownloaderTest {
     private MockSever11Repository serverRepository;
     private Server11RepositorySession repositorySession;
     private MockSessionFetchRecordsDelegate sessionFetchRecordsDelegate;
     private MockDownloader mockDownloader;
     private String DEFAULT_COLLECTION_NAME = "dummyCollection";
     private String DEFAULT_COLLECTION_URL = "http://dummy.url/";
     private long DEFAULT_NEWER = 1;
-    private String DEFAULT_SORT = "index";
+    private String DEFAULT_SORT = "oldest";
     private String DEFAULT_IDS = "1";
     private String DEFAULT_LMHEADER = "12345678";
 
     class MockSessionFetchRecordsDelegate implements RepositorySessionFetchRecordsDelegate {
         public boolean isFailure;
         public boolean isFetched;
         public boolean isSuccess;
         public int batchesCompleted;
@@ -100,18 +104,18 @@ public class BatchingDownloaderTest {
         public long newer;
         public long limit;
         public boolean full;
         public String sort;
         public String ids;
         public String offset;
         public boolean abort;
 
-        public MockDownloader(Server11Repository repository, Server11RepositorySession repositorySession) {
-            super(repository, repositorySession);
+        public MockDownloader(RepositorySession repositorySession, boolean allowMultipleBatches) {
+            super(null, Uri.EMPTY, SystemClock.elapsedRealtime() + TimeUnit.MINUTES.toMillis(30), allowMultipleBatches, repositorySession);
         }
 
         @Override
         public void fetchWithParameters(long newer,
                                  long batchLimit,
                                  boolean full,
                                  String sort,
                                  String ids,
@@ -144,22 +148,17 @@ public class BatchingDownloaderTest {
             return super.makeSyncStorageCollectionRequest(newer, batchLimit, full, sort, ids, offset);
         }
     }
 
     class MockSever11Repository extends Server11Repository {
         public MockSever11Repository(@NonNull String collection, @NonNull String storageURL,
                                      AuthHeaderProvider authHeaderProvider, @NonNull InfoCollections infoCollections,
                                      @NonNull InfoConfiguration infoConfiguration) throws URISyntaxException {
-            super(collection, storageURL, authHeaderProvider, infoCollections, infoConfiguration);
-        }
-
-        @Override
-        public long getDefaultTotalLimit() {
-            return 200;
+            super(collection, SystemClock.elapsedRealtime() + TimeUnit.MINUTES.toMillis(30), storageURL, authHeaderProvider, infoCollections, infoConfiguration);
         }
     }
 
     class MockRepositorySession extends Server11RepositorySession {
         public boolean abort;
 
         public MockRepositorySession(Repository repository) {
             super(repository);
@@ -173,17 +172,17 @@ public class BatchingDownloaderTest {
 
     @Before
     public void setUp() throws Exception {
         sessionFetchRecordsDelegate = new MockSessionFetchRecordsDelegate();
 
         serverRepository = new MockSever11Repository(DEFAULT_COLLECTION_NAME, DEFAULT_COLLECTION_URL, null,
                 new InfoCollections(), new InfoConfiguration());
         repositorySession = new Server11RepositorySession(serverRepository);
-        mockDownloader = new MockDownloader(serverRepository, repositorySession);
+        mockDownloader = new MockDownloader(repositorySession, true);
     }
 
     @Test
     public void testFlattenId() {
         String[] emptyGuid = new String[]{};
         String flatten =  BatchingDownloader.flattenIDs(emptyGuid);
         assertEquals("", flatten);
 
@@ -199,241 +198,123 @@ public class BatchingDownloaderTest {
         multiGuid[0] = guid0;
         multiGuid[1] = guid1;
         multiGuid[2] = guid2;
         flatten = BatchingDownloader.flattenIDs(multiGuid);
         assertEquals("123456789abc,456789abc,789abc", flatten);
     }
 
     @Test
-    public void testEncodeParam() throws Exception {
-        String param = "123&123";
-        String encodedParam = mockDownloader.encodeParam(param);
-        assertEquals("123%26123", encodedParam);
-    }
-
-    @Test(expected=IllegalArgumentException.class)
-    public void testOverTotalLimit() throws Exception {
-        // Per-batch limits exceed total.
-        Server11Repository repository = new Server11Repository(DEFAULT_COLLECTION_NAME, DEFAULT_COLLECTION_URL,
-                null, new InfoCollections(), new InfoConfiguration()) {
-            @Override
-            public long getDefaultTotalLimit() {
-                return 100;
-            }
-            @Override
-            public long getDefaultBatchLimit() {
-                return 200;
-            }
-        };
-        MockDownloader mockDownloader = new MockDownloader(repository, repositorySession);
+    public void testBatchingTrivial() throws Exception {
+        MockDownloader mockDownloader = new MockDownloader(repositorySession, true);
 
         assertNull(mockDownloader.getLastModified());
-        mockDownloader.fetchSince(DEFAULT_NEWER, sessionFetchRecordsDelegate);
-    }
+        // Number of records == batch limit.
+        final long BATCH_LIMIT = 100;
+        mockDownloader.fetchSince(sessionFetchRecordsDelegate, DEFAULT_NEWER, BATCH_LIMIT, DEFAULT_SORT);
 
-    @Test
-    public void testTotalLimit() throws Exception {
-        // Total and per-batch limits are the same.
-        Server11Repository repository = new Server11Repository(DEFAULT_COLLECTION_NAME, DEFAULT_COLLECTION_URL,
-                null, new InfoCollections(), new InfoConfiguration()) {
-            @Override
-            public long getDefaultTotalLimit() {
-                return 100;
-            }
-            @Override
-            public long getDefaultBatchLimit() {
-                return 100;
-            }
-        };
-        MockDownloader mockDownloader = new MockDownloader(repository, repositorySession);
-
-        assertNull(mockDownloader.getLastModified());
-        mockDownloader.fetchSince(DEFAULT_NEWER, sessionFetchRecordsDelegate);
-
-        SyncStorageResponse response = makeSyncStorageResponse(200, DEFAULT_LMHEADER, "100", "100");
+        SyncStorageResponse response = makeSyncStorageResponse(200, DEFAULT_LMHEADER, null, "100");
         SyncStorageCollectionRequest request = new SyncStorageCollectionRequest(new URI(DEFAULT_COLLECTION_URL));
-        long limit = repository.getDefaultBatchLimit();
         mockDownloader.onFetchCompleted(response, sessionFetchRecordsDelegate, request,
-                DEFAULT_NEWER, limit, true, DEFAULT_SORT, DEFAULT_IDS);
+                DEFAULT_NEWER, BATCH_LIMIT, true, DEFAULT_SORT, DEFAULT_IDS);
 
         assertEquals(DEFAULT_LMHEADER, mockDownloader.getLastModified());
         assertTrue(sessionFetchRecordsDelegate.isSuccess);
         assertFalse(sessionFetchRecordsDelegate.isFetched);
         assertFalse(sessionFetchRecordsDelegate.isFailure);
+        assertEquals(0, sessionFetchRecordsDelegate.batchesCompleted);
     }
 
     @Test
-    public void testOverHalfOfTotalLimit() throws Exception {
-        // Per-batch limit is just a bit lower than total.
-        Server11Repository repository = new Server11Repository(DEFAULT_COLLECTION_NAME, DEFAULT_COLLECTION_URL,
-                null, new InfoCollections(), new InfoConfiguration()) {
-            @Override
-            public long getDefaultTotalLimit() {
-                return 100;
-            }
-            @Override
-            public long getDefaultBatchLimit() {
-                return 75;
-            }
-        };
-        MockDownloader mockDownloader = new MockDownloader(repository, repositorySession);
+    public void testBatchingSingleBatchMode() throws Exception {
+        MockDownloader mockDownloader = new MockDownloader(repositorySession, false);
 
         assertNull(mockDownloader.getLastModified());
-        mockDownloader.fetchSince(DEFAULT_NEWER, sessionFetchRecordsDelegate);
-
-        String offsetHeader = "75";
-        String recordsHeader = "75";
-        SyncStorageResponse response = makeSyncStorageResponse(200,  DEFAULT_LMHEADER, offsetHeader, recordsHeader);
-        SyncStorageCollectionRequest request = new SyncStorageCollectionRequest(new URI(DEFAULT_COLLECTION_URL));
-        long limit = repository.getDefaultBatchLimit();
-
-        mockDownloader.onFetchCompleted(response, sessionFetchRecordsDelegate, request, DEFAULT_NEWER,
-                limit, true, DEFAULT_SORT, DEFAULT_IDS);
+        // Number of records > batch limit. But, we're only allowed to make one batch request.
+        final long BATCH_LIMIT = 100;
+        mockDownloader.fetchSince(sessionFetchRecordsDelegate, DEFAULT_NEWER, BATCH_LIMIT, DEFAULT_SORT);
 
-        assertEquals(DEFAULT_LMHEADER, mockDownloader.getLastModified());
-        // Verify the same parameters are used in the next fetch.
-        assertSameParameters(mockDownloader, limit);
-        assertEquals(offsetHeader, mockDownloader.offset);
-        assertFalse(sessionFetchRecordsDelegate.isSuccess);
-        assertFalse(sessionFetchRecordsDelegate.isFetched);
-        assertFalse(sessionFetchRecordsDelegate.isFailure);
-
-        // The next batch, we still have an offset token but we complete our fetch since we have reached the total limit.
-        offsetHeader = "150";
-        response = makeSyncStorageResponse(200, DEFAULT_LMHEADER, offsetHeader, recordsHeader);
-        mockDownloader.onFetchCompleted(response, sessionFetchRecordsDelegate, request, DEFAULT_NEWER,
-                limit, true, DEFAULT_SORT, DEFAULT_IDS);
+        String offsetHeader = "25";
+        String recordsHeader = "500";
+        SyncStorageResponse response = makeSyncStorageResponse(200, DEFAULT_LMHEADER, offsetHeader, recordsHeader);
+        SyncStorageCollectionRequest request = new SyncStorageCollectionRequest(new URI(DEFAULT_COLLECTION_URL));
+        mockDownloader.onFetchCompleted(response, sessionFetchRecordsDelegate, request,
+                DEFAULT_NEWER, BATCH_LIMIT, true, DEFAULT_SORT, DEFAULT_IDS);
 
         assertEquals(DEFAULT_LMHEADER, mockDownloader.getLastModified());
         assertTrue(sessionFetchRecordsDelegate.isSuccess);
         assertFalse(sessionFetchRecordsDelegate.isFetched);
         assertFalse(sessionFetchRecordsDelegate.isFailure);
+        assertEquals(0, sessionFetchRecordsDelegate.batchesCompleted);
     }
 
     @Test
-    public void testHalfOfTotalLimit() throws Exception {
-        // Per-batch limit is half of total.
-        Server11Repository repository = new Server11Repository(DEFAULT_COLLECTION_NAME, DEFAULT_COLLECTION_URL,
-                null, new InfoCollections(), new InfoConfiguration()) {
-            @Override
-            public long getDefaultTotalLimit() {
-                return 100;
-            }
-            @Override
-            public long getDefaultBatchLimit() {
-                return 50;
-            }
-        };
-        mockDownloader = new MockDownloader(repository, repositorySession);
+    public void testBatching() throws Exception {
+        final long BATCH_LIMIT = 25;
+        mockDownloader = new MockDownloader(repositorySession, true);
 
         assertNull(mockDownloader.getLastModified());
-        mockDownloader.fetchSince(DEFAULT_NEWER, sessionFetchRecordsDelegate);
+        mockDownloader.fetchSince(sessionFetchRecordsDelegate, DEFAULT_NEWER, BATCH_LIMIT, DEFAULT_SORT);
 
-        String offsetHeader = "50";
-        String recordsHeader = "50";
+        String offsetHeader = "25";
+        String recordsHeader = "25";
         SyncStorageResponse response = makeSyncStorageResponse(200,  DEFAULT_LMHEADER, offsetHeader, recordsHeader);
         SyncStorageCollectionRequest request = new SyncStorageCollectionRequest(new URI(DEFAULT_COLLECTION_URL));
-        long limit = repository.getDefaultBatchLimit();
         mockDownloader.onFetchCompleted(response, sessionFetchRecordsDelegate, request, DEFAULT_NEWER,
-                limit, true, DEFAULT_SORT, DEFAULT_IDS);
+                BATCH_LIMIT, true, DEFAULT_SORT, DEFAULT_IDS);
 
         assertEquals(DEFAULT_LMHEADER, mockDownloader.getLastModified());
         // Verify the same parameters are used in the next fetch.
-        assertSameParameters(mockDownloader, limit);
+        assertSameParameters(mockDownloader, BATCH_LIMIT);
         assertEquals(offsetHeader, mockDownloader.offset);
         assertFalse(sessionFetchRecordsDelegate.isSuccess);
         assertFalse(sessionFetchRecordsDelegate.isFetched);
         assertFalse(sessionFetchRecordsDelegate.isFailure);
+        assertEquals(1, sessionFetchRecordsDelegate.batchesCompleted);
 
-        // The next batch, we still have an offset token but we complete our fetch since we have reached the total limit.
-        offsetHeader = "100";
+        // The next batch, we still have an offset token and has not exceed the total limit.
+        offsetHeader = "50";
         response = makeSyncStorageResponse(200, DEFAULT_LMHEADER, offsetHeader, recordsHeader);
         mockDownloader.onFetchCompleted(response, sessionFetchRecordsDelegate, request, DEFAULT_NEWER,
-                limit, true, DEFAULT_SORT, DEFAULT_IDS);
+                BATCH_LIMIT, true, DEFAULT_SORT, DEFAULT_IDS);
+
+        assertEquals(DEFAULT_LMHEADER, mockDownloader.getLastModified());
+        // Verify the same parameters are used in the next fetch.
+        assertSameParameters(mockDownloader, BATCH_LIMIT);
+        assertEquals(offsetHeader, mockDownloader.offset);
+        assertFalse(sessionFetchRecordsDelegate.isSuccess);
+        assertFalse(sessionFetchRecordsDelegate.isFetched);
+        assertFalse(sessionFetchRecordsDelegate.isFailure);
+        assertEquals(2, sessionFetchRecordsDelegate.batchesCompleted);
+
+        // The next batch, we still have an offset token and has not exceed the total limit.
+        offsetHeader = "75";
+        response = makeSyncStorageResponse(200, DEFAULT_LMHEADER, offsetHeader, recordsHeader);
+        mockDownloader.onFetchCompleted(response, sessionFetchRecordsDelegate, request, DEFAULT_NEWER,
+                BATCH_LIMIT, true, DEFAULT_SORT, DEFAULT_IDS);
+
+        assertEquals(DEFAULT_LMHEADER, mockDownloader.getLastModified());
+        // Verify the same parameters are used in the next fetch.
+        assertSameParameters(mockDownloader, BATCH_LIMIT);
+        assertEquals(offsetHeader, mockDownloader.offset);
+        assertFalse(sessionFetchRecordsDelegate.isSuccess);
+        assertFalse(sessionFetchRecordsDelegate.isFetched);
+        assertFalse(sessionFetchRecordsDelegate.isFailure);
+        assertEquals(3, sessionFetchRecordsDelegate.batchesCompleted);
+
+        // No more offset token, so we complete batching.
+        response = makeSyncStorageResponse(200, DEFAULT_LMHEADER, null, recordsHeader);
+        mockDownloader.onFetchCompleted(response, sessionFetchRecordsDelegate, request, DEFAULT_NEWER,
+                BATCH_LIMIT, true, DEFAULT_SORT, DEFAULT_IDS);
 
         assertEquals(DEFAULT_LMHEADER, mockDownloader.getLastModified());
         assertTrue(sessionFetchRecordsDelegate.isSuccess);
         assertFalse(sessionFetchRecordsDelegate.isFetched);
         assertFalse(sessionFetchRecordsDelegate.isFailure);
-    }
-
-    @Test
-    public void testFractionOfTotalLimit() throws Exception {
-        // Per-batch limit is a small fraction of the total.
-        Server11Repository repository = new Server11Repository(DEFAULT_COLLECTION_NAME, DEFAULT_COLLECTION_URL,
-                null, new InfoCollections(), new InfoConfiguration()) {
-            @Override
-            public long getDefaultTotalLimit() {
-                return 100;
-            }
-            @Override
-            public long getDefaultBatchLimit() {
-                return 25;
-            }
-        };
-        mockDownloader = new MockDownloader(repository, repositorySession);
-
-        assertNull(mockDownloader.getLastModified());
-        mockDownloader.fetchSince(DEFAULT_NEWER, sessionFetchRecordsDelegate);
-
-        String offsetHeader = "25";
-        String recordsHeader = "25";
-        SyncStorageResponse response = makeSyncStorageResponse(200,  DEFAULT_LMHEADER, offsetHeader, recordsHeader);
-        SyncStorageCollectionRequest request = new SyncStorageCollectionRequest(new URI(DEFAULT_COLLECTION_URL));
-        long limit = repository.getDefaultBatchLimit();
-        mockDownloader.onFetchCompleted(response, sessionFetchRecordsDelegate, request, DEFAULT_NEWER,
-                limit, true, DEFAULT_SORT, DEFAULT_IDS);
-
-        assertEquals(DEFAULT_LMHEADER, mockDownloader.getLastModified());
-        // Verify the same parameters are used in the next fetch.
-        assertSameParameters(mockDownloader, limit);
-        assertEquals(offsetHeader, mockDownloader.offset);
-        assertFalse(sessionFetchRecordsDelegate.isSuccess);
-        assertFalse(sessionFetchRecordsDelegate.isFetched);
-        assertFalse(sessionFetchRecordsDelegate.isFailure);
-
-        // The next batch, we still have an offset token and has not exceed the total limit.
-        offsetHeader = "50";
-        response = makeSyncStorageResponse(200, DEFAULT_LMHEADER, offsetHeader, recordsHeader);
-        mockDownloader.onFetchCompleted(response, sessionFetchRecordsDelegate, request, DEFAULT_NEWER,
-                limit, true, DEFAULT_SORT, DEFAULT_IDS);
-
-        assertEquals(DEFAULT_LMHEADER, mockDownloader.getLastModified());
-        // Verify the same parameters are used in the next fetch.
-        assertSameParameters(mockDownloader, limit);
-        assertEquals(offsetHeader, mockDownloader.offset);
-        assertFalse(sessionFetchRecordsDelegate.isSuccess);
-        assertFalse(sessionFetchRecordsDelegate.isFetched);
-        assertFalse(sessionFetchRecordsDelegate.isFailure);
-
-        // The next batch, we still have an offset token and has not exceed the total limit.
-        offsetHeader = "75";
-        response = makeSyncStorageResponse(200, DEFAULT_LMHEADER, offsetHeader, recordsHeader);
-        mockDownloader.onFetchCompleted(response, sessionFetchRecordsDelegate, request, DEFAULT_NEWER,
-                limit, true, DEFAULT_SORT, DEFAULT_IDS);
-
-        assertEquals(DEFAULT_LMHEADER, mockDownloader.getLastModified());
-        // Verify the same parameters are used in the next fetch.
-        assertSameParameters(mockDownloader, limit);
-        assertEquals(offsetHeader, mockDownloader.offset);
-        assertFalse(sessionFetchRecordsDelegate.isSuccess);
-        assertFalse(sessionFetchRecordsDelegate.isFetched);
-        assertFalse(sessionFetchRecordsDelegate.isFailure);
-
-        // The next batch, we still have an offset token but we complete our fetch since we have reached the total limit.
-        offsetHeader = "100";
-        response = makeSyncStorageResponse(200, DEFAULT_LMHEADER, offsetHeader, recordsHeader);
-        mockDownloader.onFetchCompleted(response, sessionFetchRecordsDelegate, request, DEFAULT_NEWER,
-                limit, true, DEFAULT_SORT, DEFAULT_IDS);
-
-        assertEquals(DEFAULT_LMHEADER, mockDownloader.getLastModified());
-        assertTrue(sessionFetchRecordsDelegate.isSuccess);
-        assertFalse(sessionFetchRecordsDelegate.isFetched);
-        assertFalse(sessionFetchRecordsDelegate.isFailure);
+        assertEquals(3, sessionFetchRecordsDelegate.batchesCompleted);
     }
 
     @Test
     public void testFailureLMChangedMultiBatch() throws Exception {
         assertNull(mockDownloader.getLastModified());
 
         String lmHeader = "12345678";
         String offsetHeader = "100";
@@ -490,22 +371,38 @@ public class BatchingDownloaderTest {
         assertFalse(sessionFetchRecordsDelegate.isSuccess);
         assertFalse(sessionFetchRecordsDelegate.isFailure);
         assertEquals(record, sessionFetchRecordsDelegate.record);
     }
 
     @Test
     public void testAbortRequests() {
         MockRepositorySession mockRepositorySession = new MockRepositorySession(serverRepository);
-        BatchingDownloader downloader = new BatchingDownloader(serverRepository, mockRepositorySession);
+        BatchingDownloader downloader = new BatchingDownloader(null, Uri.EMPTY, SystemClock.elapsedRealtime() + TimeUnit.MINUTES.toMillis(30), true, mockRepositorySession);
         assertFalse(mockRepositorySession.abort);
         downloader.abortRequests();
         assertTrue(mockRepositorySession.abort);
     }
 
+    @Test
+    public void testBuildCollectionURI() {
+        try {
+            assertEquals("?full=1&newer=5000.000", BatchingDownloader.buildCollectionURI(Uri.EMPTY, true, 5000000L, -1, null, null, null).toString());
+            assertEquals("?newer=1230.000", BatchingDownloader.buildCollectionURI(Uri.EMPTY, false, 1230000L, -1, null, null, null).toString());
+            assertEquals("?newer=5000.000&limit=10", BatchingDownloader.buildCollectionURI(Uri.EMPTY, false, 5000000L, 10, null, null, null).toString());
+            assertEquals("?full=1&newer=5000.000&sort=index", BatchingDownloader.buildCollectionURI(Uri.EMPTY, true, 5000000L, 0, "index", null, null).toString());
+            assertEquals("?full=1&ids=123%2Cabc", BatchingDownloader.buildCollectionURI(Uri.EMPTY, true, -1L, -1, null, "123,abc", null).toString());
+
+            final Uri baseUri = Uri.parse("https://moztest.org/collection/");
+            assertEquals(baseUri + "?full=1&ids=123%2Cabc&offset=1234", BatchingDownloader.buildCollectionURI(baseUri, true, -1L, -1, null, "123,abc", "1234").toString());
+        } catch (URISyntaxException e) {
+            fail();
+        }
+    }
+
     private void assertSameParameters(MockDownloader mockDownloader, long limit) {
         assertEquals(DEFAULT_NEWER, mockDownloader.newer);
         assertEquals(limit, mockDownloader.limit);
         assertTrue(mockDownloader.full);
         assertEquals(DEFAULT_SORT, mockDownloader.sort);
         assertEquals(DEFAULT_IDS, mockDownloader.ids);
     }
 
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/uploaders/BatchingUploaderTest.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/uploaders/BatchingUploaderTest.java
@@ -1,14 +1,15 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.gecko.sync.repositories.uploaders;
 
 import android.net.Uri;
+import android.os.SystemClock;
 import android.support.annotation.NonNull;
 
 import static org.junit.Assert.*;
 
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mozilla.gecko.background.testhelpers.MockRecord;
@@ -20,16 +21,17 @@ import org.mozilla.gecko.sync.Utils;
 import org.mozilla.gecko.sync.repositories.Server11Repository;
 import org.mozilla.gecko.sync.repositories.Server11RepositorySession;
 import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionStoreDelegate;
 
 import java.net.URISyntaxException;
 import java.util.Random;
 import java.util.concurrent.Executor;
 import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
 
 @RunWith(TestRunner.class)
 public class BatchingUploaderTest {
     class MockExecutorService implements Executor {
         int totalPayloads = 0;
         int commitPayloads = 0;
 
         @Override
@@ -475,16 +477,17 @@ public class BatchingUploaderTest {
                 }
             };
         }
 
 
         try {
             return new Server11Repository(
                     "dummyCollection",
+                    SystemClock.elapsedRealtime() + TimeUnit.MINUTES.toMillis(30),
                     "http://dummy.url/",
                     null,
                     infoCollections,
                     infoConfiguration
             );
         } catch (URISyntaxException e) {
             // Won't throw, and this won't happen.
             return null;
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/stage/test/TestEnsureCrypto5KeysStage.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/stage/test/TestEnsureCrypto5KeysStage.java
@@ -1,13 +1,15 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.gecko.sync.stage.test;
 
+import android.os.SystemClock;
+
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mozilla.android.sync.test.helpers.HTTPServerTestHelper;
 import org.mozilla.android.sync.test.helpers.MockGlobalSessionCallback;
 import org.mozilla.android.sync.test.helpers.MockServer;
 import org.mozilla.gecko.background.testhelpers.MockGlobalSession;
 import org.mozilla.gecko.background.testhelpers.TestRunner;
@@ -22,16 +24,17 @@ import org.mozilla.gecko.sync.crypto.Key
 import org.mozilla.gecko.sync.stage.EnsureCrypto5KeysStage;
 import org.mozilla.gecko.sync.stage.GlobalSyncStage.Stage;
 import org.simpleframework.http.Request;
 import org.simpleframework.http.Response;
 
 import java.net.URI;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.concurrent.TimeUnit;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 
 @RunWith(TestRunner.class)
 public class TestEnsureCrypto5KeysStage {
@@ -95,17 +98,17 @@ public class TestEnsureCrypto5KeysStage 
 
   public void doSession(MockServer server) {
     data.startHTTPServer(server);
     try {
       WaitHelper.getTestWaiter().performWait(new Runnable() {
         @Override
         public void run() {
           try {
-            session.start();
+            session.start(SystemClock.elapsedRealtime() + TimeUnit.MINUTES.toMillis(30));
           } catch (AlreadySyncingException e) {
             WaitHelper.getTestWaiter().performNotify(e);
           }
         }
       });
     } finally {
     data.stopHTTPServer();
     }
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/stage/test/TestFetchMetaGlobalStage.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/stage/test/TestFetchMetaGlobalStage.java
@@ -1,13 +1,15 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.gecko.sync.stage.test;
 
+import android.os.SystemClock;
+
 import org.json.simple.JSONArray;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mozilla.android.sync.net.test.TestMetaGlobal;
 import org.mozilla.android.sync.test.helpers.HTTPServerTestHelper;
 import org.mozilla.android.sync.test.helpers.MockGlobalSessionCallback;
 import org.mozilla.android.sync.test.helpers.MockServer;
@@ -33,16 +35,17 @@ import org.mozilla.gecko.sync.stage.Fetc
 import org.mozilla.gecko.sync.stage.GlobalSyncStage.Stage;
 import org.simpleframework.http.Request;
 import org.simpleframework.http.Response;
 
 import java.io.IOException;
 import java.net.URI;
 import java.util.HashSet;
 import java.util.Set;
+import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
 @RunWith(TestRunner.class)
 public class TestFetchMetaGlobalStage {
@@ -158,17 +161,17 @@ public class TestFetchMetaGlobalStage {
   }
 
   protected void doSession(MockServer server) {
     data.startHTTPServer(server);
     WaitHelper.getTestWaiter().performWait(WaitHelper.onThreadRunnable(new Runnable() {
       @Override
       public void run() {
         try {
-          session.start();
+          session.start(SystemClock.elapsedRealtime() + TimeUnit.MINUTES.toMillis(30));
         } catch (AlreadySyncingException e) {
           WaitHelper.getTestWaiter().performNotify(e);
         }
       }
     }));
     data.stopHTTPServer();
   }