new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestAccountAuthenticatorStage.java
@@ -0,0 +1,138 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.android.sync.net.test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.io.IOException;
+import java.io.PrintStream;
+import java.net.URISyntaxException;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mozilla.android.sync.test.helpers.HTTPServerTestHelper;
+import org.mozilla.android.sync.test.helpers.MockServer;
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.background.testhelpers.WaitHelper;
+import org.mozilla.gecko.sync.setup.auth.AuthenticateAccountStage;
+import org.mozilla.gecko.sync.setup.auth.AuthenticateAccountStage.AuthenticateAccountStageDelegate;
+import org.simpleframework.http.Request;
+import org.simpleframework.http.Response;
+
+import ch.boye.httpclientandroidlib.HttpResponse;
+
+/**
+ * Tests the authentication request stage of manual Account setup.
+ * @author liuche
+ *
+ */
+public class TestAccountAuthenticatorStage {
+ private static final int TEST_PORT = HTTPServerTestHelper.getTestPort();
+ private static final String TEST_SERVER = "http://localhost:" + TEST_PORT;
+
+ private static final String USERNAME = "john-hashed";
+ private static final String PASSWORD = "password";
+
+ private MockServer authServer;
+ private HTTPServerTestHelper serverHelper = new HTTPServerTestHelper();
+ private AuthenticateAccountStage authStage = new AuthenticateAccountStage();
+ private AuthenticateAccountStageDelegate testCallback;
+
+ @Before
+ public void setup() {
+ // Make mock server to check authentication header.
+ authServer = new MockServer() {
+ @Override
+ protected void handle(Request request, Response response, int code, String body) {
+ try {
+ final int c;
+ String responseAuth = request.getValue("Authorization");
+ // Trim whitespace, HttpResponse has an extra space?
+ if (expectedBasicAuthHeader.equals(responseAuth.trim())) {
+ c = 200;
+ } else {
+ c = 401;
+ }
+
+ Logger.debug(LOG_TAG, "Handling request...");
+ PrintStream bodyStream = this.handleBasicHeaders(request, response, c, "application/json");
+ bodyStream.println(body);
+ bodyStream.close();
+ } catch (IOException e) {
+ Logger.error(LOG_TAG, "Oops.", e);
+ }
+ }
+ };
+ authServer.expectedBasicAuthHeader = authStage.makeAuthHeader(USERNAME, PASSWORD);
+
+ // Authentication delegate to handle HTTP responses.
+ testCallback = new AuthenticateAccountStageDelegate() {
+ protected int numFailedTries = 0;
+
+ @Override
+ public void handleSuccess(boolean isSuccess) {
+ if (isSuccess) {
+ // Succeed on retry (after failing first attempt).
+ assertEquals(1, numFailedTries);
+ testWaiter().performNotify();
+ } else {
+ numFailedTries++;
+ // Fail only once.
+ if (numFailedTries != 1) {
+ testWaiter().performNotify(new Exception("Failed on retry."));
+ return;
+ }
+ String authHeader = authStage.makeAuthHeader(USERNAME, PASSWORD);
+ try {
+ authStage.authenticateAccount(testCallback, TEST_SERVER, authHeader);
+ } catch (URISyntaxException e) {
+ fail("Malformed URI.");
+ }
+ }
+ }
+
+ @Override
+ public void handleFailure(HttpResponse response) {
+ fail("Unexpected response " + response.getStatusLine().getStatusCode());
+ }
+
+ @Override
+ public void handleError(Exception e) {
+ fail("Unexpected error during authentication.");
+ }
+ };
+ assertTrue(testWaiter().isIdle());
+
+ }
+
+ @After
+ public void cleanup() {
+ serverHelper.stopHTTPServer();
+ assertTrue(testWaiter().isIdle());
+ }
+
+ @Test
+ public void testAuthenticationRetry() {
+ serverHelper.startHTTPServer(authServer);
+ testWaiter().performWait(new Runnable() {
+ @Override
+ public void run() {
+ // Try auth request with incorrect password. We want to fail the first time.
+ String authHeader = authStage.makeAuthHeader(USERNAME, "wrong-password");
+ try {
+ authStage.authenticateAccount(testCallback, TEST_SERVER, authHeader);
+ } catch (URISyntaxException e) {
+ fail("Malformed URI.");
+ }
+ }
+ });
+ }
+
+ protected static WaitHelper testWaiter() {
+ return WaitHelper.getTestWaiter();
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestBackoff.java
@@ -0,0 +1,113 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.android.sync.net.test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import org.junit.Test;
+import org.mozilla.gecko.background.testhelpers.MockGlobalSession;
+import org.mozilla.gecko.background.testhelpers.MockSharedPreferences;
+import org.mozilla.gecko.sync.GlobalSession;
+import org.mozilla.gecko.sync.SyncConfiguration;
+import org.mozilla.gecko.sync.crypto.KeyBundle;
+import org.mozilla.gecko.sync.net.BasicAuthHeaderProvider;
+
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.ProtocolVersion;
+import ch.boye.httpclientandroidlib.message.BasicHttpResponse;
+import ch.boye.httpclientandroidlib.message.BasicStatusLine;
+
+import org.mozilla.android.sync.test.helpers.MockGlobalSessionCallback;
+
+public class TestBackoff {
+ 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 = 1201;
+
+ /**
+ * Test that interpretHTTPFailure calls requestBackoff if
+ * X-Weave-Backoff is present.
+ */
+ @Test
+ public void testBackoffCalledIfBackoffHeaderPresent() {
+ try {
+ 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);
+
+ final HttpResponse response = new BasicHttpResponse(
+ new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK"));
+ response.addHeader("X-Weave-Backoff", Long.toString(TEST_BACKOFF_IN_SECONDS)); // Backoff given in seconds.
+
+ session.interpretHTTPFailure(response); // This is synchronous...
+
+ assertEquals(false, callback.calledSuccess); // ... so we can test immediately.
+ assertEquals(false, callback.calledError);
+ assertEquals(false, callback.calledAborted);
+ assertEquals(true, callback.calledRequestBackoff);
+ assertEquals(TEST_BACKOFF_IN_SECONDS * 1000, callback.weaveBackoff); // Backoff returned in milliseconds.
+ } catch (Exception e) {
+ e.printStackTrace();
+ fail("Got exception.");
+ }
+ }
+
+ /**
+ * Test that interpretHTTPFailure does not call requestBackoff if
+ * X-Weave-Backoff is not present.
+ */
+ @Test
+ public void testBackoffNotCalledIfBackoffHeaderNotPresent() {
+ try {
+ 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);
+
+ final HttpResponse response = new BasicHttpResponse(
+ new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK"));
+
+ session.interpretHTTPFailure(response); // This is synchronous...
+
+ assertEquals(false, callback.calledSuccess); // ... so we can test immediately.
+ assertEquals(false, callback.calledError);
+ assertEquals(false, callback.calledAborted);
+ assertEquals(false, callback.calledRequestBackoff);
+ } catch (Exception e) {
+ e.printStackTrace();
+ fail("Got exception.");
+ }
+ }
+
+ /**
+ * Test that interpretHTTPFailure calls requestBackoff with the
+ * largest specified value if X-Weave-Backoff and Retry-After are
+ * present.
+ */
+ @Test
+ public void testBackoffCalledIfMultipleBackoffHeadersPresent() {
+ try {
+ 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);
+
+ final HttpResponse response = new BasicHttpResponse(
+ new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK"));
+ response.addHeader("Retry-After", Long.toString(TEST_BACKOFF_IN_SECONDS)); // Backoff given in seconds.
+ response.addHeader("X-Weave-Backoff", Long.toString(TEST_BACKOFF_IN_SECONDS + 1)); // If we now add a second header, the larger should be returned.
+
+ session.interpretHTTPFailure(response); // This is synchronous...
+
+ assertEquals(false, callback.calledSuccess); // ... so we can test immediately.
+ assertEquals(false, callback.calledError);
+ assertEquals(false, callback.calledAborted);
+ assertEquals(true, callback.calledRequestBackoff);
+ assertEquals((TEST_BACKOFF_IN_SECONDS + 1) * 1000, callback.weaveBackoff); // Backoff returned in milliseconds.
+ } catch (Exception e) {
+ e.printStackTrace();
+ fail("Got exception.");
+ }
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestBrowserIDAuthHeaderProvider.java
@@ -0,0 +1,21 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.android.sync.net.test;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Test;
+import org.mozilla.gecko.sync.net.BrowserIDAuthHeaderProvider;
+
+import ch.boye.httpclientandroidlib.Header;
+
+public class TestBrowserIDAuthHeaderProvider {
+ @Test
+ public void testHeader() {
+ Header header = new BrowserIDAuthHeaderProvider("assertion").getAuthHeader(null, null, null);
+
+ assertEquals("authorization", header.getName().toLowerCase());
+ assertEquals("BrowserID assertion", header.getValue());
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestClientsEngineStage.java
@@ -0,0 +1,797 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.android.sync.net.test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.io.IOException;
+import java.io.PrintStream;
+import java.math.BigDecimal;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.json.simple.JSONArray;
+import org.json.simple.JSONObject;
+import org.json.simple.parser.ParseException;
+import org.junit.After;
+import org.junit.Test;
+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.android.sync.test.helpers.MockSyncClientsEngineStage;
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.background.testhelpers.CommandHelpers;
+import org.mozilla.gecko.background.testhelpers.MockClientsDataDelegate;
+import org.mozilla.gecko.background.testhelpers.MockClientsDatabaseAccessor;
+import org.mozilla.gecko.background.testhelpers.MockGlobalSession;
+import org.mozilla.gecko.background.testhelpers.MockSharedPreferences;
+import org.mozilla.gecko.background.testhelpers.WaitHelper;
+import org.mozilla.gecko.sync.CollectionKeys;
+import org.mozilla.gecko.sync.CommandProcessor.Command;
+import org.mozilla.gecko.sync.CryptoRecord;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.GlobalSession;
+import org.mozilla.gecko.sync.NonObjectJSONException;
+import org.mozilla.gecko.sync.SyncConfiguration;
+import org.mozilla.gecko.sync.SyncConfigurationException;
+import org.mozilla.gecko.sync.Utils;
+import org.mozilla.gecko.sync.crypto.CryptoException;
+import org.mozilla.gecko.sync.crypto.KeyBundle;
+import org.mozilla.gecko.sync.delegates.ClientsDataDelegate;
+import org.mozilla.gecko.sync.delegates.GlobalSessionCallback;
+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.NullCursorException;
+import org.mozilla.gecko.sync.repositories.android.ClientsDatabaseAccessor;
+import org.mozilla.gecko.sync.repositories.domain.ClientRecord;
+import org.simpleframework.http.Request;
+import org.simpleframework.http.Response;
+
+import ch.boye.httpclientandroidlib.HttpStatus;
+
+public class TestClientsEngineStage extends MockSyncClientsEngineStage {
+ public final static String LOG_TAG = "TestClientsEngSta";
+
+ public TestClientsEngineStage() throws SyncConfigurationException, IllegalArgumentException, NonObjectJSONException, IOException, ParseException, CryptoException, URISyntaxException {
+ super();
+ session = initializeSession();
+ }
+
+ // Static so we can set it during the constructor. This is so evil.
+ private static MockGlobalSessionCallback callback;
+ private static GlobalSession initializeSession() throws SyncConfigurationException, IllegalArgumentException, NonObjectJSONException, IOException, ParseException, CryptoException, URISyntaxException {
+ callback = new MockGlobalSessionCallback(TEST_SERVER);
+ SyncConfiguration config = new SyncConfiguration(USERNAME, new BasicAuthHeaderProvider(USERNAME, PASSWORD), new MockSharedPreferences());
+ config.syncKeyBundle = new KeyBundle(USERNAME, SYNC_KEY);
+ GlobalSession session = new MockClientsGlobalSession(config, callback);
+ session.config.setClusterURL(new URI(TEST_SERVER));
+ session.config.setCollectionKeys(CollectionKeys.generateCollectionKeys());
+ return session;
+ }
+
+ private static final int TEST_PORT = HTTPServerTestHelper.getTestPort();
+ private static final String TEST_SERVER = "http://localhost:" + TEST_PORT;
+
+ private static final String USERNAME = "john";
+ private static final String PASSWORD = "password";
+ private static final String SYNC_KEY = "abcdeabcdeabcdeabcdeabcdea";
+
+ private HTTPServerTestHelper data = new HTTPServerTestHelper();
+ private int numRecordsFromGetRequest = 0;
+
+ private ArrayList<ClientRecord> expectedClients = new ArrayList<ClientRecord>();
+ private ArrayList<ClientRecord> downloadedClients = new ArrayList<ClientRecord>();
+
+ // For test purposes.
+ private ClientRecord lastComputedLocalClientRecord;
+ private ClientRecord uploadedRecord;
+ private String uploadBodyTimestamp;
+ private long uploadHeaderTimestamp;
+ private MockServer currentUploadMockServer;
+ private MockServer currentDownloadMockServer;
+
+ private boolean stubUpload = false;
+
+ protected static WaitHelper testWaiter() {
+ return WaitHelper.getTestWaiter();
+ }
+
+ @Override
+ protected ClientRecord newLocalClientRecord(ClientsDataDelegate delegate) {
+ lastComputedLocalClientRecord = super.newLocalClientRecord(delegate);
+ return lastComputedLocalClientRecord;
+ }
+
+ @After
+ public void teardown() {
+ stubUpload = false;
+ getMockDataAccessor().resetVars();
+ }
+
+ @Override
+ public synchronized ClientsDatabaseAccessor getClientsDatabaseAccessor() {
+ if (db == null) {
+ db = new MockClientsDatabaseAccessor();
+ }
+ return db;
+ }
+
+ // For test use.
+ private MockClientsDatabaseAccessor getMockDataAccessor() {
+ return (MockClientsDatabaseAccessor) getClientsDatabaseAccessor();
+ }
+
+ private synchronized boolean mockDataAccessorIsClosed() {
+ if (db == null) {
+ return true;
+ }
+ return ((MockClientsDatabaseAccessor) db).closed;
+ }
+
+ @Override
+ protected ClientDownloadDelegate makeClientDownloadDelegate() {
+ return clientDownloadDelegate;
+ }
+
+ @Override
+ protected void downloadClientRecords() {
+ BaseResource.rewriteLocalhost = false;
+ data.startHTTPServer(currentDownloadMockServer);
+ super.downloadClientRecords();
+ }
+
+ @Override
+ protected void uploadClientRecord(CryptoRecord record) {
+ BaseResource.rewriteLocalhost = false;
+ if (stubUpload) {
+ session.advance();
+ return;
+ }
+ data.startHTTPServer(currentUploadMockServer);
+ super.uploadClientRecord(record);
+ }
+
+ @Override
+ protected void uploadClientRecords(JSONArray records) {
+ BaseResource.rewriteLocalhost = false;
+ if (stubUpload) {
+ return;
+ }
+ data.startHTTPServer(currentUploadMockServer);
+ super.uploadClientRecords(records);
+ }
+
+ public static class MockClientsGlobalSession extends MockGlobalSession {
+ private ClientsDataDelegate clientsDataDelegate = new MockClientsDataDelegate();
+
+ public MockClientsGlobalSession(SyncConfiguration config,
+ GlobalSessionCallback callback)
+ throws SyncConfigurationException,
+ IllegalArgumentException,
+ IOException,
+ ParseException,
+ NonObjectJSONException {
+ super(config, callback);
+ }
+
+ @Override
+ public ClientsDataDelegate getClientsDelegate() {
+ return clientsDataDelegate;
+ }
+ }
+
+ public class TestSuccessClientDownloadDelegate extends TestClientDownloadDelegate {
+ public TestSuccessClientDownloadDelegate(HTTPServerTestHelper data) {
+ super(data);
+ }
+
+ @Override
+ public void handleRequestFailure(SyncStorageResponse response) {
+ super.handleRequestFailure(response);
+ assertTrue(getMockDataAccessor().closed);
+ fail("Should not error.");
+ }
+
+ @Override
+ public void handleRequestError(Exception ex) {
+ super.handleRequestError(ex);
+ assertTrue(getMockDataAccessor().closed);
+ fail("Should not fail.");
+ }
+
+ @Override
+ public void handleWBO(CryptoRecord record) {
+ ClientRecord r;
+ try {
+ r = (ClientRecord) factory.createRecord(record.decrypt());
+ downloadedClients.add(r);
+ numRecordsFromGetRequest++;
+ } catch (Exception e) {
+ fail("handleWBO failed.");
+ }
+ }
+ }
+
+ public class TestHandleWBODownloadDelegate extends TestClientDownloadDelegate {
+ public TestHandleWBODownloadDelegate(HTTPServerTestHelper data) {
+ super(data);
+ }
+
+ @Override
+ public void handleRequestFailure(SyncStorageResponse response) {
+ super.handleRequestFailure(response);
+ assertTrue(getMockDataAccessor().closed);
+ fail("Should not error.");
+ }
+
+ @Override
+ public void handleRequestError(Exception ex) {
+ super.handleRequestError(ex);
+ assertTrue(getMockDataAccessor().closed);
+ ex.printStackTrace();
+ fail("Should not fail.");
+ }
+ }
+
+ public class MockSuccessClientUploadDelegate extends MockClientUploadDelegate {
+ public MockSuccessClientUploadDelegate(HTTPServerTestHelper data) {
+ super(data);
+ }
+
+ @Override
+ public void handleRequestSuccess(SyncStorageResponse response) {
+ uploadHeaderTimestamp = response.normalizedWeaveTimestamp();
+ super.handleRequestSuccess(response);
+ }
+
+ @Override
+ public void handleRequestFailure(SyncStorageResponse response) {
+ super.handleRequestFailure(response);
+ fail("Should not fail.");
+ }
+
+ @Override
+ public void handleRequestError(Exception ex) {
+ super.handleRequestError(ex);
+ ex.printStackTrace();
+ fail("Should not error.");
+ }
+ }
+
+ public class MockFailureClientUploadDelegate extends MockClientUploadDelegate {
+ public MockFailureClientUploadDelegate(HTTPServerTestHelper data) {
+ super(data);
+ }
+
+ @Override
+ public void handleRequestSuccess(SyncStorageResponse response) {
+ super.handleRequestSuccess(response);
+ fail("Should not succeed.");
+ }
+
+ @Override
+ public void handleRequestError(Exception ex) {
+ super.handleRequestError(ex);
+ fail("Should not fail.");
+ }
+ }
+
+ public class UploadMockServer extends MockServer {
+ @SuppressWarnings("unchecked")
+ private String postBodyForRecord(ClientRecord cr) {
+ final long now = cr.lastModified;
+ final BigDecimal modified = Utils.millisecondsToDecimalSeconds(now);
+
+ Logger.debug(LOG_TAG, "Now is " + now + " (" + modified + ")");
+ final JSONArray idArray = new JSONArray();
+ idArray.add(cr.guid);
+
+ final JSONObject result = new JSONObject();
+ result.put("modified", modified);
+ result.put("success", idArray);
+ result.put("failed", new JSONObject());
+
+ uploadBodyTimestamp = modified.toString();
+ return result.toJSONString();
+ }
+
+ private String putBodyForRecord(ClientRecord cr) {
+ final String modified = Utils.millisecondsToDecimalSecondsString(cr.lastModified);
+ uploadBodyTimestamp = modified;
+ return modified;
+ }
+
+ protected void handleUploadPUT(Request request, Response response) throws Exception {
+ Logger.debug(LOG_TAG, "Handling PUT: " + request.getPath());
+
+ // Save uploadedRecord to test against.
+ CryptoRecord cryptoRecord = CryptoRecord.fromJSONRecord(request.getContent());
+ cryptoRecord.keyBundle = session.keyBundleForCollection(COLLECTION_NAME);
+ uploadedRecord = (ClientRecord) factory.createRecord(cryptoRecord.decrypt());
+
+ // Note: collection is not saved in CryptoRecord.toJSONObject() upon upload.
+ // So its value is null and is set here so ClientRecord.equals() may be used.
+ uploadedRecord.collection = lastComputedLocalClientRecord.collection;
+
+ // Create response body containing current timestamp.
+ long now = System.currentTimeMillis();
+ PrintStream bodyStream = this.handleBasicHeaders(request, response, 200, "application/json", now);
+ uploadedRecord.lastModified = now;
+
+ bodyStream.println(putBodyForRecord(uploadedRecord));
+ bodyStream.close();
+ }
+
+ protected void handleUploadPOST(Request request, Response response) throws Exception {
+ Logger.debug(LOG_TAG, "Handling POST: " + request.getPath());
+ String content = request.getContent();
+ Logger.debug(LOG_TAG, "Content is " + content);
+ JSONArray array = ExtendedJSONObject.parseJSONArray(content);
+
+ Logger.debug(LOG_TAG, "Content is " + array);
+
+ KeyBundle keyBundle = session.keyBundleForCollection(COLLECTION_NAME);
+ if (array.size() != 1) {
+ Logger.debug(LOG_TAG, "Expecting only one record! Fail!");
+ PrintStream bodyStream = this.handleBasicHeaders(request, response, 400, "text/plain");
+ bodyStream.println("Expecting only one record! Fail!");
+ bodyStream.close();
+ return;
+ }
+
+ CryptoRecord r = CryptoRecord.fromJSONRecord(new ExtendedJSONObject((JSONObject) array.get(0)));
+ r.keyBundle = keyBundle;
+ ClientRecord cr = (ClientRecord) factory.createRecord(r.decrypt());
+ cr.collection = lastComputedLocalClientRecord.collection;
+ uploadedRecord = cr;
+
+ Logger.debug(LOG_TAG, "Record is " + cr);
+ long now = System.currentTimeMillis();
+ PrintStream bodyStream = this.handleBasicHeaders(request, response, 200, "application/json", now);
+ cr.lastModified = now;
+ final String responseBody = postBodyForRecord(cr);
+ Logger.debug(LOG_TAG, "Response is " + responseBody);
+ bodyStream.println(responseBody);
+ bodyStream.close();
+ }
+
+ @Override
+ public void handle(Request request, Response response) {
+ try {
+ String method = request.getMethod();
+ Logger.debug(LOG_TAG, "Handling " + method);
+ if (method.equalsIgnoreCase("post")) {
+ handleUploadPOST(request, response);
+ } else if (method.equalsIgnoreCase("put")) {
+ handleUploadPUT(request, response);
+ } else {
+ PrintStream bodyStream = this.handleBasicHeaders(request, response, 404, "text/plain");
+ bodyStream.close();
+ }
+ } catch (Exception e) {
+ fail("Error handling uploaded client record in UploadMockServer.");
+ }
+ }
+ }
+
+ public class DownloadMockServer extends MockServer {
+ @Override
+ public void handle(Request request, Response response) {
+ try {
+ PrintStream bodyStream = this.handleBasicHeaders(request, response, 200, "application/newlines");
+ for (int i = 0; i < 5; i++) {
+ ClientRecord record = new ClientRecord();
+ if (i != 2) { // So we test null version.
+ record.version = Integer.toString(28 + i);
+ }
+ expectedClients.add(record);
+ CryptoRecord cryptoRecord = cryptoFromClient(record);
+ bodyStream.print(cryptoRecord.toJSONString() + "\n");
+ }
+ bodyStream.close();
+ } catch (IOException e) {
+ fail("Error handling downloaded client records in DownloadMockServer.");
+ }
+ }
+ }
+
+ public class DownloadLocalRecordMockServer extends MockServer {
+ @SuppressWarnings("unchecked")
+ @Override
+ public void handle(Request request, Response response) {
+ try {
+ PrintStream bodyStream = this.handleBasicHeaders(request, response, 200, "application/newlines");
+ ClientRecord record = new ClientRecord(session.getClientsDelegate().getAccountGUID());
+
+ // Timestamp on server is 10 seconds after local timestamp
+ // (would trigger 412 if upload was attempted).
+ CryptoRecord cryptoRecord = cryptoFromClient(record);
+ JSONObject object = cryptoRecord.toJSONObject();
+ final long modified = (setRecentClientRecordTimestamp() + 10000) / 1000;
+ Logger.debug(LOG_TAG, "Setting modified to " + modified);
+ object.put("modified", modified);
+ bodyStream.print(object.toJSONString() + "\n");
+ bodyStream.close();
+ } catch (IOException e) {
+ fail("Error handling downloaded client records in DownloadLocalRecordMockServer.");
+ }
+ }
+ }
+
+ private CryptoRecord cryptoFromClient(ClientRecord record) {
+ CryptoRecord cryptoRecord = record.getEnvelope();
+ cryptoRecord.keyBundle = clientDownloadDelegate.keyBundle();
+ try {
+ cryptoRecord.encrypt();
+ } catch (Exception e) {
+ fail("Cannot encrypt client record.");
+ }
+ return cryptoRecord;
+ }
+
+ private long setRecentClientRecordTimestamp() {
+ long timestamp = System.currentTimeMillis() - (CLIENTS_TTL_REFRESH - 1000);
+ session.config.persistServerClientRecordTimestamp(timestamp);
+ return timestamp;
+ }
+
+ private void performFailingUpload() {
+ // performNotify() occurs in MockGlobalSessionCallback.
+ testWaiter().performWait(new Runnable() {
+ @Override
+ public void run() {
+ clientUploadDelegate = new MockFailureClientUploadDelegate(data);
+ checkAndUpload();
+ }
+ });
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void testShouldUploadNoCommandsToProcess() throws NullCursorException {
+ // shouldUpload() returns true.
+ assertEquals(0, session.config.getPersistedServerClientRecordTimestamp());
+ assertFalse(shouldUploadLocalRecord);
+ assertTrue(shouldUpload());
+
+ // Set the timestamp to be a little earlier than refresh time,
+ // so shouldUpload() returns false.
+ setRecentClientRecordTimestamp();
+ assertFalse(0 == session.config.getPersistedServerClientRecordTimestamp());
+ assertFalse(shouldUploadLocalRecord);
+ assertFalse(shouldUpload());
+
+ // Now simulate observing a client record with the incorrect version.
+
+ ClientRecord outdatedRecord = new ClientRecord("dontmatter12", "clients", System.currentTimeMillis(), false);
+
+ outdatedRecord.version = getLocalClientVersion();
+ outdatedRecord.protocols = getLocalClientProtocols();
+ handleDownloadedLocalRecord(outdatedRecord);
+
+ assertEquals(outdatedRecord.lastModified, session.config.getPersistedServerClientRecordTimestamp());
+ assertFalse(shouldUploadLocalRecord);
+ assertFalse(shouldUpload());
+
+ outdatedRecord.version = outdatedRecord.version + "a1";
+ handleDownloadedLocalRecord(outdatedRecord);
+
+ // Now we think we need to upload because the version is outdated.
+ assertTrue(shouldUploadLocalRecord);
+ assertTrue(shouldUpload());
+
+ shouldUploadLocalRecord = false;
+ assertFalse(shouldUpload());
+
+ // If the protocol list is missing or wrong, we should reupload.
+ outdatedRecord.protocols = new JSONArray();
+ handleDownloadedLocalRecord(outdatedRecord);
+ assertTrue(shouldUpload());
+
+ shouldUploadLocalRecord = false;
+ assertFalse(shouldUpload());
+
+ outdatedRecord.protocols.add("1.0");
+ handleDownloadedLocalRecord(outdatedRecord);
+ assertTrue(shouldUpload());
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void testShouldUploadProcessCommands() throws NullCursorException {
+ // shouldUpload() returns false since array is size 0 and
+ // it has not been long enough yet to require an upload.
+ processCommands(new JSONArray());
+ setRecentClientRecordTimestamp();
+ assertFalse(shouldUploadLocalRecord);
+ assertFalse(shouldUpload());
+
+ // shouldUpload() returns true since array is size 1 even though
+ // it has not been long enough yet to require an upload.
+ JSONArray commands = new JSONArray();
+ commands.add(new JSONObject());
+ processCommands(commands);
+ setRecentClientRecordTimestamp();
+ assertEquals(1, commands.size());
+ assertTrue(shouldUploadLocalRecord);
+ assertTrue(shouldUpload());
+ }
+
+ @Test
+ public void testWipeAndStoreShouldNotWipe() {
+ assertFalse(shouldWipe);
+ wipeAndStore(new ClientRecord());
+ assertFalse(shouldWipe);
+ assertFalse(getMockDataAccessor().clientsTableWiped);
+ assertTrue(getMockDataAccessor().storedRecord);
+ }
+
+ @Test
+ public void testWipeAndStoreShouldWipe() {
+ assertFalse(shouldWipe);
+ shouldWipe = true;
+ wipeAndStore(new ClientRecord());
+ assertFalse(shouldWipe);
+ assertTrue(getMockDataAccessor().clientsTableWiped);
+ assertTrue(getMockDataAccessor().storedRecord);
+ }
+
+ @Test
+ public void testDownloadClientRecord() {
+ // Make sure no upload occurs after a download so we can
+ // test download in isolation.
+ stubUpload = true;
+
+ currentDownloadMockServer = new DownloadMockServer();
+ // performNotify() occurs in MockGlobalSessionCallback.
+ testWaiter().performWait(new Runnable() {
+ @Override
+ public void run() {
+ clientDownloadDelegate = new TestSuccessClientDownloadDelegate(data);
+ downloadClientRecords();
+ }
+ });
+
+ assertEquals(expectedClients.size(), numRecordsFromGetRequest);
+ for (int i = 0; i < downloadedClients.size(); i++) {
+ final ClientRecord downloaded = downloadedClients.get(i);
+ final ClientRecord expected = expectedClients.get(i);
+ assertTrue(expected.guid.equals(downloaded.guid));
+ assertEquals(expected.version, downloaded.version);
+ }
+ assertTrue(mockDataAccessorIsClosed());
+ }
+
+ @Test
+ public void testCheckAndUploadClientRecord() {
+ uploadAttemptsCount.set(MAX_UPLOAD_FAILURE_COUNT);
+ assertFalse(shouldUploadLocalRecord);
+ assertEquals(0, session.config.getPersistedServerClientRecordTimestamp());
+ currentUploadMockServer = new UploadMockServer();
+ // performNotify() occurs in MockGlobalSessionCallback.
+ testWaiter().performWait(new Runnable() {
+ @Override
+ public void run() {
+ clientUploadDelegate = new MockSuccessClientUploadDelegate(data);
+ checkAndUpload();
+ }
+ });
+
+ // Test ClientUploadDelegate.handleRequestSuccess().
+ Logger.debug(LOG_TAG, "Last computed local client record: " + lastComputedLocalClientRecord.guid);
+ Logger.debug(LOG_TAG, "Uploaded client record: " + uploadedRecord.guid);
+ assertTrue(lastComputedLocalClientRecord.equalPayloads(uploadedRecord));
+ assertEquals(0, uploadAttemptsCount.get());
+ assertTrue(callback.calledSuccess);
+
+ assertFalse(0 == session.config.getPersistedServerClientRecordTimestamp());
+
+ // Body and header are the same.
+ assertEquals(Utils.decimalSecondsToMilliseconds(uploadBodyTimestamp),
+ session.config.getPersistedServerClientsTimestamp());
+ assertEquals(uploadedRecord.lastModified,
+ session.config.getPersistedServerClientRecordTimestamp());
+ assertEquals(uploadHeaderTimestamp, session.config.getPersistedServerClientsTimestamp());
+ }
+
+ @Test
+ public void testDownloadHasOurRecord() {
+ // Make sure no upload occurs after a download so we can
+ // test download in isolation.
+ stubUpload = true;
+
+ // We've uploaded our local record recently.
+ long initialTimestamp = setRecentClientRecordTimestamp();
+
+ currentDownloadMockServer = new DownloadLocalRecordMockServer();
+ // performNotify() occurs in MockGlobalSessionCallback.
+ testWaiter().performWait(new Runnable() {
+ @Override
+ public void run() {
+ clientDownloadDelegate = new TestHandleWBODownloadDelegate(data);
+ downloadClientRecords();
+ }
+ });
+
+ // Timestamp got updated (but not reset) since we downloaded our record
+ assertFalse(0 == session.config.getPersistedServerClientRecordTimestamp());
+ assertTrue(initialTimestamp < session.config.getPersistedServerClientRecordTimestamp());
+ assertTrue(mockDataAccessorIsClosed());
+ }
+
+ @Test
+ public void testResetTimestampOnDownload() {
+ // Make sure no upload occurs after a download so we can
+ // test download in isolation.
+ stubUpload = true;
+
+ currentDownloadMockServer = new DownloadMockServer();
+ // performNotify() occurs in MockGlobalSessionCallback.
+ testWaiter().performWait(new Runnable() {
+ @Override
+ public void run() {
+ clientDownloadDelegate = new TestHandleWBODownloadDelegate(data);
+ downloadClientRecords();
+ }
+ });
+
+ // Timestamp got reset since our record wasn't downloaded.
+ assertEquals(0, session.config.getPersistedServerClientRecordTimestamp());
+ assertTrue(mockDataAccessorIsClosed());
+ }
+
+ /**
+ * The following 8 tests are for ClientUploadDelegate.handleRequestFailure().
+ * for the varying values of uploadAttemptsCount, shouldUploadLocalRecord,
+ * and the type of server error.
+ *
+ * The first 4 are for 412 Precondition Failures.
+ * The second 4 represent the functionality given any other type of variable.
+ */
+ @Test
+ public void testHandle412UploadFailureLowCount() {
+ assertFalse(shouldUploadLocalRecord);
+ currentUploadMockServer = new MockServer(HttpStatus.SC_PRECONDITION_FAILED, null);
+ assertEquals(0, uploadAttemptsCount.get());
+ performFailingUpload();
+ assertEquals(0, uploadAttemptsCount.get());
+ assertTrue(callback.calledError);
+ }
+
+ @Test
+ public void testHandle412UploadFailureHighCount() {
+ assertFalse(shouldUploadLocalRecord);
+ currentUploadMockServer = new MockServer(HttpStatus.SC_PRECONDITION_FAILED, null);
+ uploadAttemptsCount.set(MAX_UPLOAD_FAILURE_COUNT);
+ performFailingUpload();
+ assertEquals(MAX_UPLOAD_FAILURE_COUNT, uploadAttemptsCount.get());
+ assertTrue(callback.calledError);
+ }
+
+ @Test
+ public void testHandle412UploadFailureLowCountWithCommand() {
+ shouldUploadLocalRecord = true;
+ currentUploadMockServer = new MockServer(HttpStatus.SC_PRECONDITION_FAILED, null);
+ assertEquals(0, uploadAttemptsCount.get());
+ performFailingUpload();
+ assertEquals(0, uploadAttemptsCount.get());
+ assertTrue(callback.calledError);
+ }
+
+ @Test
+ public void testHandle412UploadFailureHighCountWithCommand() {
+ shouldUploadLocalRecord = true;
+ currentUploadMockServer = new MockServer(HttpStatus.SC_PRECONDITION_FAILED, null);
+ uploadAttemptsCount.set(MAX_UPLOAD_FAILURE_COUNT);
+ performFailingUpload();
+ assertEquals(MAX_UPLOAD_FAILURE_COUNT, uploadAttemptsCount.get());
+ assertTrue(callback.calledError);
+ }
+
+ @Test
+ public void testHandleMiscUploadFailureLowCount() {
+ currentUploadMockServer = new MockServer(HttpStatus.SC_BAD_REQUEST, null);
+ assertFalse(shouldUploadLocalRecord);
+ assertEquals(0, uploadAttemptsCount.get());
+ performFailingUpload();
+ assertEquals(0, uploadAttemptsCount.get());
+ assertTrue(callback.calledError);
+ }
+
+ @Test
+ public void testHandleMiscUploadFailureHighCount() {
+ currentUploadMockServer = new MockServer(HttpStatus.SC_BAD_REQUEST, null);
+ assertFalse(shouldUploadLocalRecord);
+ uploadAttemptsCount.set(MAX_UPLOAD_FAILURE_COUNT);
+ performFailingUpload();
+ assertEquals(MAX_UPLOAD_FAILURE_COUNT, uploadAttemptsCount.get());
+ assertTrue(callback.calledError);
+ }
+
+ @Test
+ public void testHandleMiscUploadFailureHighCountWithCommands() {
+ currentUploadMockServer = new MockServer(HttpStatus.SC_BAD_REQUEST, null);
+ shouldUploadLocalRecord = true;
+ uploadAttemptsCount.set(MAX_UPLOAD_FAILURE_COUNT);
+ performFailingUpload();
+ assertEquals(MAX_UPLOAD_FAILURE_COUNT + 1, uploadAttemptsCount.get());
+ assertTrue(callback.calledError);
+ }
+
+ @Test
+ public void testHandleMiscUploadFailureMaxAttempts() {
+ currentUploadMockServer = new MockServer(HttpStatus.SC_BAD_REQUEST, null);
+ shouldUploadLocalRecord = true;
+ assertEquals(0, uploadAttemptsCount.get());
+ performFailingUpload();
+ assertEquals(MAX_UPLOAD_FAILURE_COUNT + 1, uploadAttemptsCount.get());
+ assertTrue(callback.calledError);
+ }
+
+ class TestAddCommandsMockClientsDatabaseAccessor extends MockClientsDatabaseAccessor {
+ @Override
+ public List<Command> fetchCommandsForClient(String accountGUID) throws NullCursorException {
+ List<Command> commands = new ArrayList<Command>();
+ commands.add(CommandHelpers.getCommand1());
+ commands.add(CommandHelpers.getCommand2());
+ commands.add(CommandHelpers.getCommand3());
+ commands.add(CommandHelpers.getCommand4());
+ return commands;
+ }
+ }
+
+ @Test
+ public void testAddCommandsToUnversionedClient() throws NullCursorException {
+ db = new TestAddCommandsMockClientsDatabaseAccessor();
+
+ final ClientRecord remoteRecord = new ClientRecord();
+ remoteRecord.version = null;
+ final String expectedGUID = remoteRecord.guid;
+
+ this.addCommands(remoteRecord);
+ assertEquals(1, toUpload.size());
+
+ final ClientRecord recordToUpload = toUpload.get(0);
+ assertEquals(4, recordToUpload.commands.size());
+ assertEquals(expectedGUID, recordToUpload.guid);
+ assertEquals(null, recordToUpload.version);
+ }
+
+ @Test
+ public void testAddCommandsToVersionedClient() throws NullCursorException {
+ db = new TestAddCommandsMockClientsDatabaseAccessor();
+
+ final ClientRecord remoteRecord = new ClientRecord();
+ remoteRecord.version = "12a1";
+ final String expectedGUID = remoteRecord.guid;
+
+ this.addCommands(remoteRecord);
+ assertEquals(1, toUpload.size());
+
+ final ClientRecord recordToUpload = toUpload.get(0);
+ assertEquals(4, recordToUpload.commands.size());
+ assertEquals(expectedGUID, recordToUpload.guid);
+ assertEquals("12a1", recordToUpload.version);
+ }
+
+ @Test
+ public void testLastModifiedTimestamp() throws NullCursorException {
+ // If we uploaded a record a moment ago, we shouldn't upload another.
+ final long now = System.currentTimeMillis() - 1;
+ session.config.persistServerClientRecordTimestamp(now);
+ assertEquals(now, session.config.getPersistedServerClientRecordTimestamp());
+ assertFalse(shouldUploadLocalRecord);
+ assertFalse(shouldUpload());
+
+ // But if we change our client data, we should upload.
+ session.getClientsDelegate().setClientName("new name", System.currentTimeMillis());
+ assertTrue(shouldUpload());
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestCredentialsEndToEnd.java
@@ -0,0 +1,70 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.android.sync.net.test;
+
+import static org.junit.Assert.assertEquals;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+
+import org.json.simple.parser.ParseException;
+import org.junit.Test;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.NonObjectJSONException;
+import org.mozilla.gecko.sync.Utils;
+import org.mozilla.gecko.sync.net.BasicAuthHeaderProvider;
+
+import ch.boye.httpclientandroidlib.Header;
+
+/**
+ * Test the transfer of a UTF-8 string from desktop, and ensure that it results in the
+ * correct hashed Basic Auth header.
+ */
+public class TestCredentialsEndToEnd {
+
+ public static final String REAL_PASSWORD = "pïgéons1";
+ public static final String USERNAME = "utvm3mk6hnngiir2sp4jsxf2uvoycrv6";
+ public static final String DESKTOP_PASSWORD_JSON = "{\"password\":\"pïgéons1\"}";
+ public static final String BTOA_PASSWORD = "cMOvZ8Opb25zMQ==";
+ public static final int DESKTOP_ASSERTED_SIZE = 10;
+ public static final String DESKTOP_BASIC_AUTH = "Basic dXR2bTNtazZobm5naWlyMnNwNGpzeGYydXZveWNydjY6cMOvZ8Opb25zMQ==";
+
+ private String getCreds(String password) {
+ Header authenticate = new BasicAuthHeaderProvider(USERNAME, password).getAuthHeader(null, null, null);
+ return authenticate.getValue();
+ }
+
+ @SuppressWarnings("static-method")
+ @Test
+ public void testUTF8() throws UnsupportedEncodingException {
+ final String in = "pïgéons1";
+ final String out = "pïgéons1";
+ assertEquals(out, Utils.decodeUTF8(in));
+ }
+
+ @Test
+ public void testAuthHeaderFromPassword() throws NonObjectJSONException, IOException, ParseException {
+ final ExtendedJSONObject parsed = new ExtendedJSONObject(DESKTOP_PASSWORD_JSON);
+
+ final String password = parsed.getString("password");
+ final String decoded = Utils.decodeUTF8(password);
+
+ final byte[] expectedBytes = Utils.decodeBase64(BTOA_PASSWORD);
+ final String expected = new String(expectedBytes, "UTF-8");
+
+ assertEquals(DESKTOP_ASSERTED_SIZE, password.length());
+ assertEquals(expected, decoded);
+
+ System.out.println("Retrieved password: " + password);
+ System.out.println("Expected password: " + expected);
+ System.out.println("Rescued password: " + decoded);
+
+ assertEquals(getCreds(expected), getCreds(decoded));
+ assertEquals(getCreds(decoded), DESKTOP_BASIC_AUTH);
+ }
+
+ // Note that we do *not* have a test for the J-PAKE setup process
+ // (SetupSyncActivity) that actually stores credentials and requires
+ // decodeUTF8. This will have to suffice.
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestGlobalSession.java
@@ -0,0 +1,436 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.android.sync.net.test;
+
+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;
+
+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 junit.framework.AssertionFailedError;
+
+import org.json.simple.parser.ParseException;
+import org.junit.Before;
+import org.junit.Test;
+import org.mozilla.android.sync.test.helpers.HTTPServerTestHelper;
+import org.mozilla.android.sync.test.helpers.MockGlobalSessionCallback;
+import org.mozilla.android.sync.test.helpers.MockResourceDelegate;
+import org.mozilla.android.sync.test.helpers.MockServer;
+import org.mozilla.gecko.background.testhelpers.MockAbstractNonRepositorySyncStage;
+import org.mozilla.gecko.background.testhelpers.MockGlobalSession;
+import org.mozilla.gecko.background.testhelpers.MockPrefsGlobalSession;
+import org.mozilla.gecko.background.testhelpers.MockServerSyncStage;
+import org.mozilla.gecko.background.testhelpers.MockSharedPreferences;
+import org.mozilla.gecko.background.testhelpers.WaitHelper;
+import org.mozilla.gecko.sync.EngineSettings;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.GlobalSession;
+import org.mozilla.gecko.sync.MetaGlobal;
+import org.mozilla.gecko.sync.NonObjectJSONException;
+import org.mozilla.gecko.sync.SyncConfiguration;
+import org.mozilla.gecko.sync.SyncConfigurationException;
+import org.mozilla.gecko.sync.Utils;
+import org.mozilla.gecko.sync.crypto.CryptoException;
+import org.mozilla.gecko.sync.crypto.KeyBundle;
+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.domain.VersionConstants;
+import org.mozilla.gecko.sync.stage.AndroidBrowserBookmarksServerSyncStage;
+import org.mozilla.gecko.sync.stage.GlobalSyncStage;
+import org.mozilla.gecko.sync.stage.GlobalSyncStage.Stage;
+import org.mozilla.gecko.sync.stage.NoSuchStageException;
+import org.simpleframework.http.Request;
+import org.simpleframework.http.Response;
+
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.ProtocolVersion;
+import ch.boye.httpclientandroidlib.message.BasicHttpResponse;
+import ch.boye.httpclientandroidlib.message.BasicStatusLine;
+
+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;
+
+ public static WaitHelper getTestWaiter() {
+ return WaitHelper.getTestWaiter();
+ }
+
+ @Test
+ public void testGetSyncStagesBy() throws SyncConfigurationException, IllegalArgumentException, NonObjectJSONException, IOException, ParseException, CryptoException, NoSuchStageException {
+
+ final MockGlobalSessionCallback callback = new MockGlobalSessionCallback();
+ GlobalSession s = MockPrefsGlobalSession.getSession(TEST_USERNAME, TEST_PASSWORD,
+ new KeyBundle(TEST_USERNAME, TEST_SYNC_KEY),
+ callback, /* context */ null, null);
+
+ assertTrue(s.getSyncStageByName(Stage.syncBookmarks) instanceof AndroidBrowserBookmarksServerSyncStage);
+
+ final Set<String> empty = new HashSet<String>();
+
+ final Set<String> bookmarksAndTabsNames = new HashSet<String>();
+ bookmarksAndTabsNames.add("bookmarks");
+ bookmarksAndTabsNames.add("tabs");
+
+ final Set<GlobalSyncStage> bookmarksAndTabsSyncStages = new HashSet<GlobalSyncStage>();
+ GlobalSyncStage bookmarksStage = s.getSyncStageByName("bookmarks");
+ GlobalSyncStage tabsStage = s.getSyncStageByName(Stage.syncTabs);
+ bookmarksAndTabsSyncStages.add(bookmarksStage);
+ bookmarksAndTabsSyncStages.add(tabsStage);
+
+ final Set<Stage> bookmarksAndTabsEnums = new HashSet<Stage>();
+ bookmarksAndTabsEnums.add(Stage.syncBookmarks);
+ bookmarksAndTabsEnums.add(Stage.syncTabs);
+
+ assertTrue(s.getSyncStagesByName(empty).isEmpty());
+ assertEquals(bookmarksAndTabsSyncStages, new HashSet<GlobalSyncStage>(s.getSyncStagesByName(bookmarksAndTabsNames)));
+ assertEquals(bookmarksAndTabsSyncStages, new HashSet<GlobalSyncStage>(s.getSyncStagesByEnum(bookmarksAndTabsEnums)));
+ }
+
+ /**
+ * Test that handleHTTPError does in fact backoff.
+ */
+ @Test
+ public void testBackoffCalledByHandleHTTPError() {
+ try {
+ final MockGlobalSessionCallback callback = new MockGlobalSessionCallback(TEST_CLUSTER_URL);
+ 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);
+
+ final HttpResponse response = new BasicHttpResponse(
+ new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 503, "Illegal method/protocol"));
+ response.addHeader("X-Weave-Backoff", Long.toString(TEST_BACKOFF_IN_SECONDS)); // Backoff given in seconds.
+
+ getTestWaiter().performWait(WaitHelper.onThreadRunnable(new Runnable() {
+ @Override
+ public void run() {
+ session.handleHTTPError(new SyncStorageResponse(response), "Illegal method/protocol");
+ }
+ }));
+
+ assertEquals(false, callback.calledSuccess);
+ assertEquals(true, callback.calledError);
+ assertEquals(false, callback.calledAborted);
+ assertEquals(true, callback.calledRequestBackoff);
+ assertEquals(TEST_BACKOFF_IN_SECONDS * 1000, callback.weaveBackoff); // Backoff returned in milliseconds.
+ } catch (Exception e) {
+ e.printStackTrace();
+ fail("Got exception.");
+ }
+ }
+
+ /**
+ * Test that a trivially successful GlobalSession does not fail or backoff.
+ */
+ @Test
+ public void testSuccessCalledAfterStages() {
+ try {
+ final MockGlobalSessionCallback callback = new MockGlobalSessionCallback(TEST_CLUSTER_URL);
+ 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();
+ } catch (Exception e) {
+ final AssertionFailedError error = new AssertionFailedError();
+ error.initCause(e);
+ getTestWaiter().performNotify(error);
+ }
+ }
+ }));
+
+ assertEquals(true, callback.calledSuccess);
+ assertEquals(false, callback.calledError);
+ assertEquals(false, callback.calledAborted);
+ assertEquals(false, callback.calledRequestBackoff);
+ } catch (Exception e) {
+ e.printStackTrace();
+ fail("Got exception.");
+ }
+ }
+
+ /**
+ * Test that a failing GlobalSession does in fact fail and back off.
+ */
+ @Test
+ public void testBackoffCalledInStages() {
+ try {
+ final MockGlobalSessionCallback callback = new MockGlobalSessionCallback(TEST_CLUSTER_URL);
+
+ // Stage fakes a 503 and sets X-Weave-Backoff header to the given seconds.
+ final GlobalSyncStage stage = new MockAbstractNonRepositorySyncStage() {
+ @Override
+ public void execute() {
+ final HttpResponse response = new BasicHttpResponse(
+ new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 503, "Illegal method/protocol"));
+
+ response.addHeader("X-Weave-Backoff", Long.toString(TEST_BACKOFF_IN_SECONDS)); // Backoff given in seconds.
+ session.handleHTTPError(new SyncStorageResponse(response), "Failure fetching info/collections.");
+ }
+ };
+
+ // Session installs fake stage to fetch info/collections.
+ 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();
+ } catch (Exception e) {
+ final AssertionFailedError error = new AssertionFailedError();
+ error.initCause(e);
+ getTestWaiter().performNotify(error);
+ }
+ }
+ }));
+
+ assertEquals(false, callback.calledSuccess);
+ assertEquals(true, callback.calledError);
+ assertEquals(false, callback.calledAborted);
+ assertEquals(true, callback.calledRequestBackoff);
+ assertEquals(TEST_BACKOFF_IN_SECONDS * 1000, callback.weaveBackoff); // Backoff returned in milliseconds.
+ } catch (Exception e) {
+ e.printStackTrace();
+ fail("Got exception.");
+ }
+ }
+
+ private HTTPServerTestHelper data = new HTTPServerTestHelper();
+
+ @SuppressWarnings("static-method")
+ @Before
+ public void setUp() {
+ BaseResource.rewriteLocalhost = false;
+ }
+
+ public void doRequest() {
+ final WaitHelper innerWaitHelper = new WaitHelper();
+ innerWaitHelper.performWait(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ final BaseResource r = new BaseResource(TEST_CLUSTER_URL);
+ r.delegate = new MockResourceDelegate(innerWaitHelper);
+ r.get();
+ } catch (URISyntaxException e) {
+ innerWaitHelper.performNotify(e);
+ }
+ }
+ });
+ }
+
+ public MockGlobalSessionCallback doTestSuccess(final boolean stageShouldBackoff, final boolean stageShouldAdvance) throws SyncConfigurationException, IllegalArgumentException, NonObjectJSONException, IOException, ParseException, CryptoException {
+ MockServer server = new MockServer() {
+ @Override
+ public void handle(Request request, Response response) {
+ if (stageShouldBackoff) {
+ response.set("X-Weave-Backoff", Long.toString(TEST_BACKOFF_IN_SECONDS));
+ }
+ super.handle(request, response);
+ }
+ };
+
+ final MockServerSyncStage stage = new MockServerSyncStage() {
+ @Override
+ public void execute() {
+ // We should have installed our HTTP response observer before starting the sync.
+ assertTrue(BaseResource.isHttpResponseObserver(session));
+
+ doRequest();
+ if (stageShouldAdvance) {
+ session.advance();
+ return;
+ }
+ session.abort(null, "Stage intentionally failed.");
+ }
+ };
+
+ final MockGlobalSessionCallback callback = new MockGlobalSessionCallback(TEST_CLUSTER_URL);
+ 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.syncBookmarks, stage);
+
+ data.startHTTPServer(server);
+ WaitHelper.getTestWaiter().performWait(WaitHelper.onThreadRunnable(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ session.start();
+ } catch (Exception e) {
+ final AssertionFailedError error = new AssertionFailedError();
+ error.initCause(e);
+ WaitHelper.getTestWaiter().performNotify(error);
+ }
+ }
+ }));
+ data.stopHTTPServer();
+
+ // We should have uninstalled our HTTP response observer when the session is terminated.
+ assertFalse(BaseResource.isHttpResponseObserver(session));
+
+ return callback;
+ }
+
+ @Test
+ public void testOnSuccessBackoffAdvanced() throws SyncConfigurationException,
+ IllegalArgumentException, NonObjectJSONException, IOException,
+ ParseException, CryptoException {
+ MockGlobalSessionCallback callback = doTestSuccess(true, true);
+
+ assertTrue(callback.calledError); // TODO: this should be calledAborted.
+ assertTrue(callback.calledRequestBackoff);
+ assertEquals(1000 * TEST_BACKOFF_IN_SECONDS, callback.weaveBackoff);
+ }
+
+ @Test
+ public void testOnSuccessBackoffAborted() throws SyncConfigurationException,
+ IllegalArgumentException, NonObjectJSONException, IOException,
+ ParseException, CryptoException {
+ MockGlobalSessionCallback callback = doTestSuccess(true, false);
+
+ assertTrue(callback.calledError); // TODO: this should be calledAborted.
+ assertTrue(callback.calledRequestBackoff);
+ assertEquals(1000 * TEST_BACKOFF_IN_SECONDS, callback.weaveBackoff);
+ }
+
+ @Test
+ public void testOnSuccessNoBackoffAdvanced() throws SyncConfigurationException,
+ IllegalArgumentException, NonObjectJSONException, IOException,
+ ParseException, CryptoException {
+ MockGlobalSessionCallback callback = doTestSuccess(false, true);
+
+ assertTrue(callback.calledSuccess);
+ assertFalse(callback.calledRequestBackoff);
+ }
+
+ @Test
+ public void testOnSuccessNoBackoffAborted() throws SyncConfigurationException,
+ IllegalArgumentException, NonObjectJSONException, IOException,
+ ParseException, CryptoException {
+ MockGlobalSessionCallback callback = doTestSuccess(false, false);
+
+ assertTrue(callback.calledError); // TODO: this should be calledAborted.
+ assertFalse(callback.calledRequestBackoff);
+ }
+
+ @Test
+ public void testGenerateNewMetaGlobalNonePersisted() throws Exception {
+ final MockGlobalSessionCallback callback = new MockGlobalSessionCallback();
+ final GlobalSession session = MockPrefsGlobalSession.getSession(TEST_USERNAME, TEST_PASSWORD,
+ new KeyBundle(TEST_USERNAME, TEST_SYNC_KEY), callback, null, null);
+
+ // Verify we fill in all of our known engines when none are persisted.
+ session.config.enabledEngineNames = null;
+ MetaGlobal mg = session.generateNewMetaGlobal();
+ assertEquals(Long.valueOf(GlobalSession.STORAGE_VERSION), mg.getStorageVersion());
+ assertEquals(VersionConstants.BOOKMARKS_ENGINE_VERSION, mg.getEngines().getObject("bookmarks").getIntegerSafely("version").intValue());
+ assertEquals(VersionConstants.CLIENTS_ENGINE_VERSION, mg.getEngines().getObject("clients").getIntegerSafely("version").intValue());
+
+ List<String> namesList = new ArrayList<String>(mg.getEnabledEngineNames());
+ Collections.sort(namesList);
+ String[] names = namesList.toArray(new String[namesList.size()]);
+ String[] expected = new String[] { "bookmarks", "clients", "forms", "history", "passwords", "tabs" };
+ assertArrayEquals(expected, names);
+ }
+
+ @Test
+ public void testGenerateNewMetaGlobalSomePersisted() throws Exception {
+ final MockGlobalSessionCallback callback = new MockGlobalSessionCallback();
+ final GlobalSession session = MockPrefsGlobalSession.getSession(TEST_USERNAME, TEST_PASSWORD,
+ new KeyBundle(TEST_USERNAME, TEST_SYNC_KEY), callback, null, null);
+
+ // Verify we preserve engines with version 0 if some are persisted.
+ session.config.enabledEngineNames = new HashSet<String>();
+ session.config.enabledEngineNames.add("bookmarks");
+ session.config.enabledEngineNames.add("clients");
+ session.config.enabledEngineNames.add("addons");
+ session.config.enabledEngineNames.add("prefs");
+
+ MetaGlobal mg = session.generateNewMetaGlobal();
+ assertEquals(Long.valueOf(GlobalSession.STORAGE_VERSION), mg.getStorageVersion());
+ assertEquals(VersionConstants.BOOKMARKS_ENGINE_VERSION, mg.getEngines().getObject("bookmarks").getIntegerSafely("version").intValue());
+ assertEquals(VersionConstants.CLIENTS_ENGINE_VERSION, mg.getEngines().getObject("clients").getIntegerSafely("version").intValue());
+ assertEquals(0, mg.getEngines().getObject("addons").getIntegerSafely("version").intValue());
+ assertEquals(0, mg.getEngines().getObject("prefs").getIntegerSafely("version").intValue());
+
+ List<String> namesList = new ArrayList<String>(mg.getEnabledEngineNames());
+ Collections.sort(namesList);
+ String[] names = namesList.toArray(new String[namesList.size()]);
+ String[] expected = new String[] { "addons", "bookmarks", "clients", "prefs" };
+ assertArrayEquals(expected, names);
+ }
+
+ @Test
+ public void testUploadUpdatedMetaGlobal() throws Exception {
+ // Set up session with meta/global.
+ final MockGlobalSessionCallback callback = new MockGlobalSessionCallback();
+ final GlobalSession session = MockPrefsGlobalSession.getSession(TEST_USERNAME, TEST_PASSWORD,
+ new KeyBundle(TEST_USERNAME, TEST_SYNC_KEY), callback, null, null);
+ session.config.metaGlobal = session.generateNewMetaGlobal();
+ session.enginesToUpdate.clear();
+
+ // Set enabledEngines in meta/global, including a "new engine."
+ String[] origEngines = new String[] { "bookmarks", "clients", "forms", "history", "tabs", "new-engine" };
+
+ ExtendedJSONObject origEnginesJSONObject = new ExtendedJSONObject();
+ for (String engineName : origEngines) {
+ EngineSettings mockEngineSettings = new EngineSettings(Utils.generateGuid(), Integer.valueOf(0));
+ origEnginesJSONObject.put(engineName, mockEngineSettings);
+ }
+ session.config.metaGlobal.setEngines(origEnginesJSONObject);
+
+ // Engines to remove.
+ String[] toRemove = new String[] { "bookmarks", "tabs" };
+ for (String name : toRemove) {
+ session.removeEngineFromMetaGlobal(name);
+ }
+
+ // Engines to add.
+ String[] toAdd = new String[] { "passwords" };
+ for (String name : toAdd) {
+ String syncId = Utils.generateGuid();
+ session.recordForMetaGlobalUpdate(name, new EngineSettings(syncId, Integer.valueOf(1)));
+ }
+
+ // Update engines.
+ session.uploadUpdatedMetaGlobal();
+
+ // Check resulting enabledEngines.
+ Set<String> expected = new HashSet<String>();
+ for (String name : origEngines) {
+ expected.add(name);
+ }
+ for (String name : toRemove) {
+ expected.remove(name);
+ }
+ for (String name : toAdd) {
+ expected.add(name);
+ }
+ assertEquals(expected, session.config.metaGlobal.getEnabledEngineNames());
+ }
+
+ public void testStageAdvance() {
+ assertEquals(GlobalSession.nextStage(Stage.idle), Stage.checkPreconditions);
+ assertEquals(GlobalSession.nextStage(Stage.completed), Stage.idle);
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestHeaderParsing.java
@@ -0,0 +1,26 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.android.sync.net.test;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Test;
+import org.mozilla.gecko.sync.Utils;
+
+public class TestHeaderParsing {
+
+ @SuppressWarnings("static-method")
+ @Test
+ public void testDecimalSecondsToMilliseconds() {
+ assertEquals(Utils.decimalSecondsToMilliseconds(""), -1);
+ assertEquals(Utils.decimalSecondsToMilliseconds("1234.1.1"), -1);
+ assertEquals(Utils.decimalSecondsToMilliseconds("1234"), 1234000);
+ assertEquals(Utils.decimalSecondsToMilliseconds("1234.123"), 1234123);
+ assertEquals(Utils.decimalSecondsToMilliseconds("1234.12"), 1234120);
+
+ assertEquals("1234.000", Utils.millisecondsToDecimalSecondsString(1234000));
+ assertEquals("1234.123", Utils.millisecondsToDecimalSecondsString(1234123));
+ assertEquals("1234.120", Utils.millisecondsToDecimalSecondsString(1234120));
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestLineByLineHandling.java
@@ -0,0 +1,111 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.android.sync.net.test;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.io.IOException;
+import java.io.PrintStream;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+
+import org.junit.Test;
+import org.mozilla.android.sync.test.helpers.HTTPServerTestHelper;
+import org.mozilla.android.sync.test.helpers.MockServer;
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.sync.net.AuthHeaderProvider;
+import org.mozilla.gecko.sync.net.BaseResource;
+import org.mozilla.gecko.sync.net.SyncStorageCollectionRequest;
+import org.mozilla.gecko.sync.net.SyncStorageCollectionRequestDelegate;
+import org.mozilla.gecko.sync.net.SyncStorageResponse;
+import org.simpleframework.http.Request;
+import org.simpleframework.http.Response;
+
+public class TestLineByLineHandling {
+ private static final int TEST_PORT = HTTPServerTestHelper.getTestPort();
+ private static final String TEST_SERVER = "http://localhost:" + TEST_PORT;
+ private static final String LOG_TAG = "TestLineByLineHandling";
+ static String STORAGE_URL = TEST_SERVER + "/1.1/c6o7dvmr2c4ud2fyv6woz2u4zi22bcyd/storage/lines";
+ private HTTPServerTestHelper data = new HTTPServerTestHelper();
+
+ public ArrayList<String> lines = new ArrayList<String>();
+
+ public class LineByLineMockServer extends MockServer {
+ public void handle(Request request, Response response) {
+ try {
+ System.out.println("Handling line-by-line request...");
+ PrintStream bodyStream = this.handleBasicHeaders(request, response, 200, "application/newlines");
+
+ bodyStream.print("First line.\n");
+ bodyStream.print("Second line.\n");
+ bodyStream.print("Third line.\n");
+ bodyStream.print("Fourth line.\n");
+ bodyStream.close();
+ } catch (IOException e) {
+ System.err.println("Oops.");
+ }
+ }
+ }
+
+ public class BaseLineByLineDelegate extends
+ SyncStorageCollectionRequestDelegate {
+
+ @Override
+ public void handleRequestProgress(String progress) {
+ lines.add(progress);
+ }
+
+ @Override
+ public AuthHeaderProvider getAuthHeaderProvider() {
+ return null;
+ }
+
+ @Override
+ public String ifUnmodifiedSince() {
+ return null;
+ }
+
+ @Override
+ public void handleRequestSuccess(SyncStorageResponse res) {
+ Logger.info(LOG_TAG, "Request success.");
+ assertTrue(res.wasSuccessful());
+ assertTrue(res.httpResponse().containsHeader("X-Weave-Timestamp"));
+
+ assertEquals(lines.size(), 4);
+ assertEquals(lines.get(0), "First line.");
+ assertEquals(lines.get(1), "Second line.");
+ assertEquals(lines.get(2), "Third line.");
+ assertEquals(lines.get(3), "Fourth line.");
+ data.stopHTTPServer();
+ }
+
+ @Override
+ public void handleRequestFailure(SyncStorageResponse response) {
+ Logger.info(LOG_TAG, "Got request failure: " + response);
+ BaseResource.consumeEntity(response);
+ fail("Should not be called.");
+ }
+
+ @Override
+ public void handleRequestError(Exception ex) {
+ Logger.error(LOG_TAG, "Got request error: ", ex);
+ fail("Should not be called.");
+ }
+ }
+
+ @Test
+ public void testLineByLine() throws URISyntaxException {
+ BaseResource.rewriteLocalhost = false;
+
+ data.startHTTPServer(new LineByLineMockServer());
+ Logger.info(LOG_TAG, "Server started.");
+ SyncStorageCollectionRequest r = new SyncStorageCollectionRequest(new URI(STORAGE_URL));
+ SyncStorageCollectionRequestDelegate delegate = new BaseLineByLineDelegate();
+ r.delegate = delegate;
+ r.get();
+ // Server is stopped in the callback.
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestMetaGlobal.java
@@ -0,0 +1,346 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.android.sync.net.test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.json.simple.parser.ParseException;
+import org.junit.Before;
+import org.junit.Test;
+import org.mozilla.android.sync.test.helpers.HTTPServerTestHelper;
+import org.mozilla.android.sync.test.helpers.MockServer;
+import org.mozilla.gecko.background.testhelpers.WaitHelper;
+import org.mozilla.gecko.sync.CryptoRecord;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.MetaGlobal;
+import org.mozilla.gecko.sync.delegates.MetaGlobalDelegate;
+import org.mozilla.gecko.sync.net.BaseResource;
+import org.mozilla.gecko.sync.net.BasicAuthHeaderProvider;
+import org.mozilla.gecko.sync.net.SyncStorageResponse;
+import org.simpleframework.http.Request;
+import org.simpleframework.http.Response;
+
+public class TestMetaGlobal {
+ public static Object monitor = new Object();
+
+ private static final int TEST_PORT = HTTPServerTestHelper.getTestPort();
+ private static final String TEST_SERVER = "http://localhost:" + TEST_PORT;
+ private static final String TEST_SYNC_ID = "foobar";
+
+ public static final String USER_PASS = "c6o7dvmr2c4ud2fyv6woz2u4zi22bcyd:password";
+ public static final String META_URL = TEST_SERVER + "/1.1/c6o7dvmr2c4ud2fyv6woz2u4zi22bcyd/storage/meta/global";
+ private HTTPServerTestHelper data = new HTTPServerTestHelper();
+
+
+ public static final String TEST_DECLINED_META_GLOBAL_RESPONSE =
+ "{\"id\":\"global\"," +
+ "\"payload\":" +
+ "\"{\\\"syncID\\\":\\\"zPSQTm7WBVWB\\\"," +
+ "\\\"declined\\\":[\\\"bookmarks\\\"]," +
+ "\\\"storageVersion\\\":5," +
+ "\\\"engines\\\":{" +
+ "\\\"clients\\\":{\\\"version\\\":1,\\\"syncID\\\":\\\"fDg0MS5bDtV7\\\"}," +
+ "\\\"forms\\\":{\\\"version\\\":1,\\\"syncID\\\":\\\"GXF29AFprnvc\\\"}," +
+ "\\\"history\\\":{\\\"version\\\":1,\\\"syncID\\\":\\\"av75g4vm-_rp\\\"}," +
+ "\\\"passwords\\\":{\\\"version\\\":1,\\\"syncID\\\":\\\"LT_ACGpuKZ6a\\\"}," +
+ "\\\"prefs\\\":{\\\"version\\\":2,\\\"syncID\\\":\\\"-3nsksP9wSAs\\\"}," +
+ "\\\"tabs\\\":{\\\"version\\\":1,\\\"syncID\\\":\\\"W4H5lOMChkYA\\\"}}}\"," +
+ "\"username\":\"5817483\"," +
+ "\"modified\":1.32046073744E9}";
+
+ public static final String TEST_META_GLOBAL_RESPONSE =
+ "{\"id\":\"global\"," +
+ "\"payload\":" +
+ "\"{\\\"syncID\\\":\\\"zPSQTm7WBVWB\\\"," +
+ "\\\"storageVersion\\\":5," +
+ "\\\"engines\\\":{" +
+ "\\\"clients\\\":{\\\"version\\\":1,\\\"syncID\\\":\\\"fDg0MS5bDtV7\\\"}," +
+ "\\\"bookmarks\\\":{\\\"version\\\":2,\\\"syncID\\\":\\\"NNaQr6_F-9dm\\\"}," +
+ "\\\"forms\\\":{\\\"version\\\":1,\\\"syncID\\\":\\\"GXF29AFprnvc\\\"}," +
+ "\\\"history\\\":{\\\"version\\\":1,\\\"syncID\\\":\\\"av75g4vm-_rp\\\"}," +
+ "\\\"passwords\\\":{\\\"version\\\":1,\\\"syncID\\\":\\\"LT_ACGpuKZ6a\\\"}," +
+ "\\\"prefs\\\":{\\\"version\\\":2,\\\"syncID\\\":\\\"-3nsksP9wSAs\\\"}," +
+ "\\\"tabs\\\":{\\\"version\\\":1,\\\"syncID\\\":\\\"W4H5lOMChkYA\\\"}}}\"," +
+ "\"username\":\"5817483\"," +
+ "\"modified\":1.32046073744E9}";
+ public static final String TEST_META_GLOBAL_NO_PAYLOAD_RESPONSE = "{\"id\":\"global\"," +
+ "\"username\":\"5817483\",\"modified\":1.32046073744E9}";
+ public static final String TEST_META_GLOBAL_MALFORMED_PAYLOAD_RESPONSE = "{\"id\":\"global\"," +
+ "\"payload\":\"{!!!}\"," +
+ "\"username\":\"5817483\",\"modified\":1.32046073744E9}";
+ public static final String TEST_META_GLOBAL_EMPTY_PAYLOAD_RESPONSE = "{\"id\":\"global\"," +
+ "\"payload\":\"{}\"," +
+ "\"username\":\"5817483\",\"modified\":1.32046073744E9}";
+
+ public MetaGlobal global;
+
+ @SuppressWarnings("static-method")
+ @Before
+ public void setUp() {
+ BaseResource.rewriteLocalhost = false;
+ global = new MetaGlobal(META_URL, new BasicAuthHeaderProvider(USER_PASS));
+ }
+
+ @SuppressWarnings("static-method")
+ @Test
+ public void testSyncID() {
+ global.setSyncID("foobar");
+ assertEquals(global.getSyncID(), "foobar");
+ }
+
+ public class MockMetaGlobalFetchDelegate implements MetaGlobalDelegate {
+ boolean successCalled = false;
+ MetaGlobal successGlobal = null;
+ SyncStorageResponse successResponse = null;
+ boolean failureCalled = false;
+ SyncStorageResponse failureResponse = null;
+ boolean errorCalled = false;
+ Exception errorException = null;
+ boolean missingCalled = false;
+ MetaGlobal missingGlobal = null;
+ SyncStorageResponse missingResponse = null;
+
+ public void handleSuccess(MetaGlobal global, SyncStorageResponse response) {
+ successCalled = true;
+ successGlobal = global;
+ successResponse = response;
+ WaitHelper.getTestWaiter().performNotify();
+ }
+
+ public void handleFailure(SyncStorageResponse response) {
+ failureCalled = true;
+ failureResponse = response;
+ WaitHelper.getTestWaiter().performNotify();
+ }
+
+ public void handleError(Exception e) {
+ errorCalled = true;
+ errorException = e;
+ WaitHelper.getTestWaiter().performNotify();
+ }
+
+ public void handleMissing(MetaGlobal global, SyncStorageResponse response) {
+ missingCalled = true;
+ missingGlobal = global;
+ missingResponse = response;
+ WaitHelper.getTestWaiter().performNotify();
+ }
+ }
+
+ public MockMetaGlobalFetchDelegate doFetch(final MetaGlobal global) {
+ final MockMetaGlobalFetchDelegate delegate = new MockMetaGlobalFetchDelegate();
+ WaitHelper.getTestWaiter().performWait(WaitHelper.onThreadRunnable(new Runnable() {
+ @Override
+ public void run() {
+ global.fetch(delegate);
+ }
+ }));
+
+ return delegate;
+ }
+
+ @Test
+ public void testFetchMissing() {
+ MockServer missingMetaGlobalServer = new MockServer(404, "{}");
+ global.setSyncID(TEST_SYNC_ID);
+ assertEquals(TEST_SYNC_ID, global.getSyncID());
+
+ data.startHTTPServer(missingMetaGlobalServer);
+ final MockMetaGlobalFetchDelegate delegate = doFetch(global);
+ data.stopHTTPServer();
+
+ assertTrue(delegate.missingCalled);
+ assertEquals(404, delegate.missingResponse.getStatusCode());
+ assertEquals(TEST_SYNC_ID, delegate.missingGlobal.getSyncID());
+ }
+
+ @Test
+ public void testFetchExisting() {
+ MockServer existingMetaGlobalServer = new MockServer(200, TEST_META_GLOBAL_RESPONSE);
+ assertNull(global.getSyncID());
+ assertNull(global.getEngines());
+ assertNull(global.getStorageVersion());
+
+ data.startHTTPServer(existingMetaGlobalServer);
+ final MockMetaGlobalFetchDelegate delegate = doFetch(global);
+ data.stopHTTPServer();
+
+ assertTrue(delegate.successCalled);
+ assertEquals(200, delegate.successResponse.getStatusCode());
+ assertEquals("zPSQTm7WBVWB", global.getSyncID());
+ assertTrue(global.getEngines() instanceof ExtendedJSONObject);
+ assertEquals(Long.valueOf(5), global.getStorageVersion());
+ }
+
+ /**
+ * A record that is valid JSON but invalid as a meta/global record will be
+ * downloaded successfully, but will fail later.
+ */
+ @Test
+ public void testFetchNoPayload() {
+ MockServer existingMetaGlobalServer = new MockServer(200, TEST_META_GLOBAL_NO_PAYLOAD_RESPONSE);
+
+ data.startHTTPServer(existingMetaGlobalServer);
+ final MockMetaGlobalFetchDelegate delegate = doFetch(global);
+ data.stopHTTPServer();
+
+ assertTrue(delegate.successCalled);
+ }
+
+ @Test
+ public void testFetchEmptyPayload() {
+ MockServer existingMetaGlobalServer = new MockServer(200, TEST_META_GLOBAL_EMPTY_PAYLOAD_RESPONSE);
+
+ data.startHTTPServer(existingMetaGlobalServer);
+ final MockMetaGlobalFetchDelegate delegate = doFetch(global);
+ data.stopHTTPServer();
+
+ assertTrue(delegate.successCalled);
+ }
+
+ /**
+ * A record that is invalid JSON will fail to download at all.
+ */
+ @Test
+ public void testFetchMalformedPayload() {
+ MockServer existingMetaGlobalServer = new MockServer(200, TEST_META_GLOBAL_MALFORMED_PAYLOAD_RESPONSE);
+
+ data.startHTTPServer(existingMetaGlobalServer);
+ final MockMetaGlobalFetchDelegate delegate = doFetch(global);
+ data.stopHTTPServer();
+
+ assertTrue(delegate.errorCalled);
+ assertNotNull(delegate.errorException);
+ assertEquals(ParseException.class, delegate.errorException.getClass());
+ }
+
+ @SuppressWarnings("static-method")
+ @Test
+ public void testSetFromRecord() throws Exception {
+ MetaGlobal mg = new MetaGlobal(null, null);
+ mg.setFromRecord(CryptoRecord.fromJSONRecord(TEST_META_GLOBAL_RESPONSE));
+ assertEquals("zPSQTm7WBVWB", mg.getSyncID());
+ assertTrue(mg.getEngines() instanceof ExtendedJSONObject);
+ assertEquals(Long.valueOf(5), mg.getStorageVersion());
+ }
+
+ @SuppressWarnings("static-method")
+ @Test
+ public void testAsCryptoRecord() throws Exception {
+ MetaGlobal mg = new MetaGlobal(null, null);
+ mg.setFromRecord(CryptoRecord.fromJSONRecord(TEST_META_GLOBAL_RESPONSE));
+ CryptoRecord rec = mg.asCryptoRecord();
+ assertEquals("global", rec.guid);
+ mg.setFromRecord(rec);
+ assertEquals("zPSQTm7WBVWB", mg.getSyncID());
+ assertTrue(mg.getEngines() instanceof ExtendedJSONObject);
+ assertEquals(Long.valueOf(5), mg.getStorageVersion());
+ }
+
+ @SuppressWarnings("static-method")
+ @Test
+ public void testGetEnabledEngineNames() throws Exception {
+ MetaGlobal mg = new MetaGlobal(null, null);
+ mg.setFromRecord(CryptoRecord.fromJSONRecord(TEST_META_GLOBAL_RESPONSE));
+ assertEquals("zPSQTm7WBVWB", mg.getSyncID());
+ final Set<String> actual = mg.getEnabledEngineNames();
+ final Set<String> expected = new HashSet<String>();
+ for (String name : new String[] { "bookmarks", "clients", "forms", "history", "passwords", "prefs", "tabs" }) {
+ expected.add(name);
+ }
+ assertEquals(expected, actual);
+ }
+
+ @SuppressWarnings("static-method")
+ @Test
+ public void testGetEmptyDeclinedEngineNames() throws Exception {
+ MetaGlobal mg = new MetaGlobal(null, null);
+ mg.setFromRecord(CryptoRecord.fromJSONRecord(TEST_META_GLOBAL_RESPONSE));
+ assertEquals(0, mg.getDeclinedEngineNames().size());
+ }
+
+ @SuppressWarnings("static-method")
+ @Test
+ public void testGetDeclinedEngineNames() throws Exception {
+ MetaGlobal mg = new MetaGlobal(null, null);
+ mg.setFromRecord(CryptoRecord.fromJSONRecord(TEST_DECLINED_META_GLOBAL_RESPONSE));
+ assertEquals(1, mg.getDeclinedEngineNames().size());
+ assertEquals("bookmarks", mg.getDeclinedEngineNames().iterator().next());
+ }
+
+ @SuppressWarnings("static-method")
+ @Test
+ public void testRoundtripDeclinedEngineNames() throws Exception {
+ MetaGlobal mg = new MetaGlobal(null, null);
+ mg.setFromRecord(CryptoRecord.fromJSONRecord(TEST_DECLINED_META_GLOBAL_RESPONSE));
+ assertEquals("bookmarks", mg.getDeclinedEngineNames().iterator().next());
+ assertEquals("bookmarks", mg.asCryptoRecord().payload.getArray("declined").get(0));
+ MetaGlobal again = new MetaGlobal(null, null);
+ again.setFromRecord(mg.asCryptoRecord());
+ assertEquals("bookmarks", again.getDeclinedEngineNames().iterator().next());
+ }
+
+
+ public MockMetaGlobalFetchDelegate doUpload(final MetaGlobal global) {
+ final MockMetaGlobalFetchDelegate delegate = new MockMetaGlobalFetchDelegate();
+ WaitHelper.getTestWaiter().performWait(WaitHelper.onThreadRunnable(new Runnable() {
+ @Override
+ public void run() {
+ global.upload(delegate);
+ }
+ }));
+
+ return delegate;
+ }
+
+ @Test
+ public void testUpload() {
+ long TEST_STORAGE_VERSION = 111;
+ String TEST_SYNC_ID = "testSyncID";
+ global.setSyncID(TEST_SYNC_ID);
+ global.setStorageVersion(Long.valueOf(TEST_STORAGE_VERSION));
+
+ final AtomicBoolean mgUploaded = new AtomicBoolean(false);
+ final MetaGlobal uploadedMg = new MetaGlobal(null, null);
+
+ MockServer server = new MockServer() {
+ public void handle(Request request, Response response) {
+ if (request.getMethod().equals("PUT")) {
+ try {
+ ExtendedJSONObject body = ExtendedJSONObject.parseJSONObject(request.getContent());
+ System.out.println(body.toJSONString());
+ assertTrue(body.containsKey("payload"));
+ assertFalse(body.containsKey("default"));
+
+ CryptoRecord rec = CryptoRecord.fromJSONRecord(body);
+ uploadedMg.setFromRecord(rec);
+ mgUploaded.set(true);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ this.handle(request, response, 200, "success");
+ return;
+ }
+ this.handle(request, response, 404, "missing");
+ }
+ };
+
+ data.startHTTPServer(server);
+ final MockMetaGlobalFetchDelegate delegate = doUpload(global);
+ data.stopHTTPServer();
+
+ assertTrue(delegate.successCalled);
+ assertTrue(mgUploaded.get());
+ assertEquals(TEST_SYNC_ID, uploadedMg.getSyncID());
+ assertEquals(TEST_STORAGE_VERSION, uploadedMg.getStorageVersion().longValue());
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestResource.java
@@ -0,0 +1,100 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.android.sync.net.test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.net.URISyntaxException;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mozilla.android.sync.test.helpers.HTTPServerTestHelper;
+import org.mozilla.android.sync.test.helpers.MockResourceDelegate;
+import org.mozilla.android.sync.test.helpers.MockServer;
+import org.mozilla.gecko.background.testhelpers.WaitHelper;
+import org.mozilla.gecko.sync.net.BaseResource;
+import org.mozilla.gecko.sync.net.HttpResponseObserver;
+
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.client.methods.HttpUriRequest;
+
+public class TestResource {
+ private static final int TEST_PORT = HTTPServerTestHelper.getTestPort();
+ private static final String TEST_SERVER = "http://localhost:" + TEST_PORT;
+
+ private HTTPServerTestHelper data = new HTTPServerTestHelper();
+
+ @SuppressWarnings("static-method")
+ @Before
+ public void setUp() {
+ BaseResource.rewriteLocalhost = false;
+ }
+
+ @SuppressWarnings("static-method")
+ @Test
+ public void testLocalhostRewriting() throws URISyntaxException {
+ BaseResource r = new BaseResource("http://localhost:5000/foo/bar", true);
+ assertEquals("http://10.0.2.2:5000/foo/bar", r.getURI().toASCIIString());
+ }
+
+ @SuppressWarnings("static-method")
+ public MockResourceDelegate doGet() throws URISyntaxException {
+ final BaseResource r = new BaseResource(TEST_SERVER + "/foo/bar");
+ MockResourceDelegate delegate = new MockResourceDelegate();
+ r.delegate = delegate;
+ WaitHelper.getTestWaiter().performWait(new Runnable() {
+ @Override
+ public void run() {
+ r.get();
+ }
+ });
+ return delegate;
+ }
+
+ @Test
+ public void testTrivialFetch() throws URISyntaxException {
+ MockServer server = data.startHTTPServer();
+ server.expectedBasicAuthHeader = MockResourceDelegate.EXPECT_BASIC;
+ MockResourceDelegate delegate = doGet();
+ assertTrue(delegate.handledHttpResponse);
+ data.stopHTTPServer();
+ }
+
+ public static class MockHttpResponseObserver implements HttpResponseObserver {
+ public HttpResponse response = null;
+
+ @Override
+ public void observeHttpResponse(HttpUriRequest request, HttpResponse response) {
+ this.response = response;
+ }
+ }
+
+ @Test
+ public void testObservers() throws URISyntaxException {
+ data.startHTTPServer();
+ // Check that null observer doesn't fail.
+ BaseResource.addHttpResponseObserver(null);
+ doGet(); // HTTP server stopped in callback.
+
+ // Check that multiple non-null observers gets called with reasonable HttpResponse.
+ MockHttpResponseObserver observers[] = { new MockHttpResponseObserver(), new MockHttpResponseObserver() };
+ for (MockHttpResponseObserver observer : observers) {
+ BaseResource.addHttpResponseObserver(observer);
+ assertTrue(BaseResource.isHttpResponseObserver(observer));
+ assertNull(observer.response);
+ }
+
+ doGet(); // HTTP server stopped in callback.
+
+ for (MockHttpResponseObserver observer : observers) {
+ assertNotNull(observer.response);
+ assertEquals(200, observer.response.getStatusLine().getStatusCode());
+ }
+
+ data.stopHTTPServer();
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestRetryAfter.java
@@ -0,0 +1,85 @@
+package org.mozilla.android.sync.net.test;
+
+import java.util.Date;
+
+import org.junit.Test;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import org.mozilla.gecko.sync.net.SyncResponse;
+
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.ProtocolVersion;
+import ch.boye.httpclientandroidlib.impl.cookie.DateUtils;
+import ch.boye.httpclientandroidlib.message.BasicHttpResponse;
+import ch.boye.httpclientandroidlib.message.BasicStatusLine;
+
+public class TestRetryAfter {
+ private int TEST_SECONDS = 120;
+
+ @Test
+ public void testRetryAfterParsesSeconds() {
+ final HttpResponse response = new BasicHttpResponse(
+ new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 503, "Illegal method/protocol"));
+ response.addHeader("Retry-After", Long.toString(TEST_SECONDS)); // Retry-After given in seconds.
+
+ final SyncResponse syncResponse = new SyncResponse(response);
+ assertEquals(TEST_SECONDS, syncResponse.retryAfterInSeconds());
+ }
+
+ @Test
+ public void testRetryAfterParsesHTTPDate() {
+ Date future = new Date(System.currentTimeMillis() + TEST_SECONDS * 1000);
+
+ final HttpResponse response = new BasicHttpResponse(
+ new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 503, "Illegal method/protocol"));
+ response.addHeader("Retry-After", DateUtils.formatDate(future));
+
+ final SyncResponse syncResponse = new SyncResponse(response);
+ assertTrue(syncResponse.retryAfterInSeconds() > TEST_SECONDS - 15);
+ assertTrue(syncResponse.retryAfterInSeconds() < TEST_SECONDS + 15);
+ }
+
+ @SuppressWarnings("static-method")
+ @Test
+ public void testRetryAfterParsesMalformed() {
+ final HttpResponse response = new BasicHttpResponse(
+ new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 503, "Illegal method/protocol"));
+ response.addHeader("Retry-After", "10X");
+
+ final SyncResponse syncResponse = new SyncResponse(response);
+ assertEquals(-1, syncResponse.retryAfterInSeconds());
+ }
+
+ @SuppressWarnings("static-method")
+ @Test
+ public void testRetryAfterParsesNeither() {
+ final HttpResponse response = new BasicHttpResponse(
+ new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 503, "Illegal method/protocol"));
+
+ final SyncResponse syncResponse = new SyncResponse(response);
+ assertEquals(-1, syncResponse.retryAfterInSeconds());
+ }
+
+ @Test
+ public void testRetryAfterParsesLargerRetryAfter() {
+ final HttpResponse response = new BasicHttpResponse(
+ new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 503, "Illegal method/protocol"));
+ response.addHeader("Retry-After", Long.toString(TEST_SECONDS + 1));
+ response.addHeader("X-Weave-Backoff", Long.toString(TEST_SECONDS));
+
+ final SyncResponse syncResponse = new SyncResponse(response);
+ assertEquals(1000 * (TEST_SECONDS + 1), syncResponse.totalBackoffInMilliseconds());
+ }
+
+ @Test
+ public void testRetryAfterParsesLargerXWeaveBackoff() {
+ final HttpResponse response = new BasicHttpResponse(
+ new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 503, "Illegal method/protocol"));
+ response.addHeader("Retry-After", Long.toString(TEST_SECONDS));
+ response.addHeader("X-Weave-Backoff", Long.toString(TEST_SECONDS + 1));
+
+ final SyncResponse syncResponse = new SyncResponse(response);
+ assertEquals(1000 * (TEST_SECONDS + 1), syncResponse.totalBackoffInMilliseconds());
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestServer11Repository.java
@@ -0,0 +1,43 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.android.sync.net.test;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.mozilla.gecko.sync.InfoCollections;
+import org.mozilla.gecko.sync.repositories.Server11Repository;
+
+public class TestServer11Repository {
+
+ private static final String COLLECTION = "bookmarks";
+ private static final String COLLECTION_URL = "http://foo.com/1.1/n6ec3u5bee3tixzp2asys7bs6fve4jfw/storage";
+
+ protected final InfoCollections infoCollections = new InfoCollections();
+
+ public static void assertQueryEquals(String expected, URI u) {
+ Assert.assertEquals(expected, u.getRawQuery());
+ }
+
+ @SuppressWarnings("static-method")
+ @Test
+ public void testCollectionURIFull() throws URISyntaxException {
+ Server11Repository r = new Server11Repository(COLLECTION, COLLECTION_URL, null, infoCollections);
+ assertQueryEquals("full=1&newer=5000.000", r.collectionURI(true, 5000000L, -1, null, null));
+ assertQueryEquals("newer=1230.000", r.collectionURI(false, 1230000L, -1, null, null));
+ assertQueryEquals("newer=5000.000&limit=10", r.collectionURI(false, 5000000L, 10, null, null));
+ assertQueryEquals("full=1&newer=5000.000&sort=index", r.collectionURI(true, 5000000L, 0, "index", null));
+ assertQueryEquals("full=1&ids=123,abc", r.collectionURI(true, -1L, -1, null, "123,abc"));
+ }
+
+ @Test
+ public void testCollectionURI() throws URISyntaxException {
+ Server11Repository noTrailingSlash = new Server11Repository(COLLECTION, COLLECTION_URL, null, infoCollections);
+ Server11Repository trailingSlash = new Server11Repository(COLLECTION, COLLECTION_URL + "/", null, infoCollections);
+ 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());
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestSyncStorageRequest.java
@@ -0,0 +1,265 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.android.sync.net.test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+
+import org.json.simple.JSONObject;
+import org.junit.Test;
+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.sync.net.AuthHeaderProvider;
+import org.mozilla.gecko.sync.net.BaseResource;
+import org.mozilla.gecko.sync.net.BasicAuthHeaderProvider;
+import org.mozilla.gecko.sync.net.SyncStorageRecordRequest;
+import org.mozilla.gecko.sync.net.SyncStorageResponse;
+import org.simpleframework.http.Request;
+import org.simpleframework.http.Response;
+
+public class TestSyncStorageRequest {
+ private static final int TEST_PORT = HTTPServerTestHelper.getTestPort();
+ private static final String TEST_SERVER = "http://localhost:" + TEST_PORT;
+
+ private static final String LOCAL_META_URL = TEST_SERVER + "/1.1/c6o7dvmr2c4ud2fyv6woz2u4zi22bcyd/storage/meta/global";
+ private static final String LOCAL_BAD_REQUEST_URL = TEST_SERVER + "/1.1/c6o7dvmr2c4ud2fyv6woz2u4zi22bcyd/storage/bad";
+
+ private static final String EXPECTED_ERROR_CODE = "12";
+ private static final String EXPECTED_RETRY_AFTER_ERROR_MESSAGE = "{error:'informative error message'}";
+
+ // Corresponds to rnewman+testandroid@mozilla.com.
+ private static final String TEST_USERNAME = "c6o7dvmr2c4ud2fyv6woz2u4zi22bcyd";
+ private static final String TEST_PASSWORD = "password";
+ private final AuthHeaderProvider authHeaderProvider = new BasicAuthHeaderProvider(TEST_USERNAME, TEST_PASSWORD);
+
+ private HTTPServerTestHelper data = new HTTPServerTestHelper();
+
+ public class TestSyncStorageRequestDelegate extends
+ BaseTestStorageRequestDelegate {
+ public TestSyncStorageRequestDelegate(AuthHeaderProvider authHeaderProvider) {
+ super(authHeaderProvider);
+ }
+
+ @Override
+ public void handleRequestSuccess(SyncStorageResponse res) {
+ assertTrue(res.wasSuccessful());
+ assertTrue(res.httpResponse().containsHeader("X-Weave-Timestamp"));
+
+ // Make sure we consume the rest of the body, so we can reuse the
+ // connection. Even test code has to be correct in this regard!
+ try {
+ System.out.println("Success body: " + res.body());
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ BaseResource.consumeEntity(res);
+ data.stopHTTPServer();
+ }
+ }
+
+ public class TestBadSyncStorageRequestDelegate extends
+ BaseTestStorageRequestDelegate {
+
+ public TestBadSyncStorageRequestDelegate(AuthHeaderProvider authHeaderProvider) {
+ super(authHeaderProvider);
+ }
+
+ @Override
+ public void handleRequestFailure(SyncStorageResponse res) {
+ assertTrue(!res.wasSuccessful());
+ assertTrue(res.httpResponse().containsHeader("X-Weave-Timestamp"));
+ try {
+ String responseMessage = res.getErrorMessage();
+ String expectedMessage = SyncStorageResponse.SERVER_ERROR_MESSAGES.get(EXPECTED_ERROR_CODE);
+ assertEquals(expectedMessage, responseMessage);
+ } catch (Exception e) {
+ fail("Got exception fetching error message.");
+ }
+ BaseResource.consumeEntity(res);
+ data.stopHTTPServer();
+ }
+ }
+
+
+ @Test
+ public void testSyncStorageRequest() throws URISyntaxException, IOException {
+ BaseResource.rewriteLocalhost = false;
+ data.startHTTPServer();
+ SyncStorageRecordRequest r = new SyncStorageRecordRequest(new URI(LOCAL_META_URL));
+ TestSyncStorageRequestDelegate delegate = new TestSyncStorageRequestDelegate(authHeaderProvider);
+ r.delegate = delegate;
+ r.get();
+ // Server is stopped in the callback.
+ }
+
+ public class ErrorMockServer extends MockServer {
+ @Override
+ public void handle(Request request, Response response) {
+ super.handle(request, response, 400, EXPECTED_ERROR_CODE);
+ }
+ }
+
+ @Test
+ public void testErrorResponse() throws URISyntaxException {
+ BaseResource.rewriteLocalhost = false;
+ data.startHTTPServer(new ErrorMockServer());
+ SyncStorageRecordRequest r = new SyncStorageRecordRequest(new URI(LOCAL_BAD_REQUEST_URL));
+ TestBadSyncStorageRequestDelegate delegate = new TestBadSyncStorageRequestDelegate(authHeaderProvider);
+ r.delegate = delegate;
+ r.post(new JSONObject());
+ // Server is stopped in the callback.
+ }
+
+ // Test that the Retry-After header is correctly parsed and that handleRequestFailure
+ // is being called.
+ public class TestRetryAfterSyncStorageRequestDelegate extends BaseTestStorageRequestDelegate {
+
+ public TestRetryAfterSyncStorageRequestDelegate(AuthHeaderProvider authHeaderProvider) {
+ super(authHeaderProvider);
+ }
+
+ @Override
+ public void handleRequestFailure(SyncStorageResponse res) {
+ assertTrue(!res.wasSuccessful());
+ assertTrue(res.httpResponse().containsHeader("Retry-After"));
+ assertEquals(res.retryAfterInSeconds(), 3001);
+ try {
+ String responseMessage = res.getErrorMessage();
+ String expectedMessage = EXPECTED_RETRY_AFTER_ERROR_MESSAGE;
+ assertEquals(expectedMessage, responseMessage);
+ } catch (Exception e) {
+ fail("Got exception fetching error message.");
+ }
+ BaseResource.consumeEntity(res);
+ data.stopHTTPServer();
+ }
+ }
+
+ public class RetryAfterMockServer extends MockServer {
+ @Override
+ public void handle(Request request, Response response) {
+ String errorBody = EXPECTED_RETRY_AFTER_ERROR_MESSAGE;
+ response.set("Retry-After", "3001");
+ super.handle(request, response, 503, errorBody);
+ }
+ }
+
+ @Test
+ public void testRetryAfterResponse() throws URISyntaxException {
+ BaseResource.rewriteLocalhost = false;
+ data.startHTTPServer(new RetryAfterMockServer());
+ SyncStorageRecordRequest r = new SyncStorageRecordRequest(new URI(LOCAL_BAD_REQUEST_URL)); // URL not used -- we 503 every response
+ TestRetryAfterSyncStorageRequestDelegate delegate = new TestRetryAfterSyncStorageRequestDelegate(authHeaderProvider);
+ r.delegate = delegate;
+ r.post(new JSONObject());
+ // Server is stopped in the callback.
+ }
+
+ // Test that the X-Weave-Backoff header is correctly parsed and that handleRequestSuccess
+ // is still being called.
+ public class TestWeaveBackoffSyncStorageRequestDelegate extends
+ TestSyncStorageRequestDelegate {
+
+ public TestWeaveBackoffSyncStorageRequestDelegate(AuthHeaderProvider authHeaderProvider) {
+ super(authHeaderProvider);
+ }
+
+ @Override
+ public void handleRequestSuccess(SyncStorageResponse res) {
+ assertTrue(res.httpResponse().containsHeader("X-Weave-Backoff"));
+ assertEquals(res.weaveBackoffInSeconds(), 1801);
+ super.handleRequestSuccess(res);
+ }
+ }
+
+ public class WeaveBackoffMockServer extends MockServer {
+ @Override
+ public void handle(Request request, Response response) {
+ response.set("X-Weave-Backoff", "1801");
+ super.handle(request, response);
+ }
+ }
+
+ @Test
+ public void testWeaveBackoffResponse() throws URISyntaxException {
+ BaseResource.rewriteLocalhost = false;
+ data.startHTTPServer(new WeaveBackoffMockServer());
+ SyncStorageRecordRequest r = new SyncStorageRecordRequest(new URI(LOCAL_META_URL)); // URL re-used -- we need any successful response
+ TestWeaveBackoffSyncStorageRequestDelegate delegate = new TestWeaveBackoffSyncStorageRequestDelegate(new BasicAuthHeaderProvider(TEST_USERNAME, TEST_PASSWORD));
+ r.delegate = delegate;
+ r.post(new JSONObject());
+ // Server is stopped in the callback.
+ }
+
+ // Test that the X-Weave-{Quota-Remaining, Alert, Records} headers are correctly parsed and
+ // that handleRequestSuccess is still being called.
+ public class TestHeadersSyncStorageRequestDelegate extends
+ TestSyncStorageRequestDelegate {
+
+ public TestHeadersSyncStorageRequestDelegate(AuthHeaderProvider authHeaderProvider) {
+ super(authHeaderProvider);
+ }
+
+ @Override
+ public void handleRequestSuccess(SyncStorageResponse res) {
+ assertTrue(res.httpResponse().containsHeader("X-Weave-Quota-Remaining"));
+ assertTrue(res.httpResponse().containsHeader("X-Weave-Alert"));
+ assertTrue(res.httpResponse().containsHeader("X-Weave-Records"));
+ assertEquals(65536, res.weaveQuotaRemaining());
+ assertEquals("First weave alert string", res.weaveAlert());
+ assertEquals(50, res.weaveRecords());
+
+ super.handleRequestSuccess(res);
+ }
+ }
+
+ public class HeadersMockServer extends MockServer {
+ @Override
+ public void handle(Request request, Response response) {
+ response.set("X-Weave-Quota-Remaining", "65536");
+ response.set("X-Weave-Alert", "First weave alert string");
+ response.add("X-Weave-Alert", "Second weave alert string");
+ response.set("X-Weave-Records", "50");
+
+ super.handle(request, response);
+ }
+ }
+
+ @Test
+ public void testHeadersResponse() throws URISyntaxException {
+ BaseResource.rewriteLocalhost = false;
+ data.startHTTPServer(new HeadersMockServer());
+ SyncStorageRecordRequest r = new SyncStorageRecordRequest(new URI(LOCAL_META_URL)); // URL re-used -- we need any successful response
+ TestHeadersSyncStorageRequestDelegate delegate = new TestHeadersSyncStorageRequestDelegate(authHeaderProvider);
+ r.delegate = delegate;
+ r.post(new JSONObject());
+ // Server is stopped in the callback.
+ }
+
+ public class DeleteMockServer extends MockServer {
+ @Override
+ public void handle(Request request, Response response) {
+ assertTrue(request.contains("x-confirm-delete"));
+ assertEquals("1", request.getValue("x-confirm-delete"));
+ super.handle(request, response);
+ }
+ }
+
+ @Test
+ public void testDelete() throws URISyntaxException {
+ BaseResource.rewriteLocalhost = false;
+ data.startHTTPServer(new DeleteMockServer());
+ SyncStorageRecordRequest r = new SyncStorageRecordRequest(new URI(LOCAL_META_URL)); // URL re-used -- we need any successful response
+ TestSyncStorageRequestDelegate delegate = new TestSyncStorageRequestDelegate(authHeaderProvider);
+ r.delegate = delegate;
+ r.delete();
+ // Server is stopped in the callback.
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/SynchronizerHelpers.java
@@ -0,0 +1,283 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.android.sync.test;
+
+import java.util.ArrayList;
+import java.util.concurrent.ExecutorService;
+
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.background.testhelpers.WBORepository;
+import org.mozilla.gecko.sync.repositories.FetchFailedException;
+import org.mozilla.gecko.sync.repositories.InactiveSessionException;
+import org.mozilla.gecko.sync.repositories.InvalidSessionTransitionException;
+import org.mozilla.gecko.sync.repositories.NoStoreDelegateException;
+import org.mozilla.gecko.sync.repositories.StoreFailedException;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionBeginDelegate;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFetchRecordsDelegate;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFinishDelegate;
+import org.mozilla.gecko.sync.repositories.domain.Record;
+
+import android.content.Context;
+
+public class SynchronizerHelpers {
+ public static final String FAIL_SENTINEL = "Fail";
+
+ /**
+ * Store one at a time, failing if the guid contains FAIL_SENTINEL.
+ */
+ public static class FailFetchWBORepository extends WBORepository {
+ @Override
+ public void createSession(RepositorySessionCreationDelegate delegate,
+ Context context) {
+ delegate.deferredCreationDelegate().onSessionCreated(new WBORepositorySession(this) {
+ @Override
+ public void fetchSince(long timestamp,
+ final RepositorySessionFetchRecordsDelegate delegate) {
+ super.fetchSince(timestamp, new RepositorySessionFetchRecordsDelegate() {
+ @Override
+ public void onFetchedRecord(Record record) {
+ if (record.guid.contains(FAIL_SENTINEL)) {
+ delegate.onFetchFailed(new FetchFailedException(), record);
+ } else {
+ delegate.onFetchedRecord(record);
+ }
+ }
+
+ @Override
+ public void onFetchFailed(Exception ex, Record record) {
+ delegate.onFetchFailed(ex, record);
+ }
+
+ @Override
+ public void onFetchCompleted(long fetchEnd) {
+ delegate.onFetchCompleted(fetchEnd);
+ }
+
+ @Override
+ public RepositorySessionFetchRecordsDelegate deferredFetchDelegate(ExecutorService executor) {
+ return this;
+ }
+ });
+ }
+ });
+ }
+ }
+
+ /**
+ * Store one at a time, failing if the guid contains FAIL_SENTINEL.
+ */
+ public static class SerialFailStoreWBORepository extends WBORepository {
+ @Override
+ public void createSession(RepositorySessionCreationDelegate delegate,
+ Context context) {
+ delegate.deferredCreationDelegate().onSessionCreated(new WBORepositorySession(this) {
+ @Override
+ public void store(final Record record) throws NoStoreDelegateException {
+ if (delegate == null) {
+ throw new NoStoreDelegateException();
+ }
+ if (record.guid.contains(FAIL_SENTINEL)) {
+ delegate.onRecordStoreFailed(new StoreFailedException(), record.guid);
+ } else {
+ super.store(record);
+ }
+ }
+ });
+ }
+ }
+
+ /**
+ * Store in batches, failing if any of the batch guids contains "Fail".
+ * <p>
+ * This will drop the final batch.
+ */
+ public static class BatchFailStoreWBORepository extends WBORepository {
+ public final int batchSize;
+ public ArrayList<Record> batch = new ArrayList<Record>();
+ public boolean batchShouldFail = false;
+
+ public class BatchFailStoreWBORepositorySession extends WBORepositorySession {
+ public BatchFailStoreWBORepositorySession(WBORepository repository) {
+ super(repository);
+ }
+
+ public void superStore(final Record record) throws NoStoreDelegateException {
+ super.store(record);
+ }
+
+ @Override
+ public void store(final Record record) throws NoStoreDelegateException {
+ if (delegate == null) {
+ throw new NoStoreDelegateException();
+ }
+ synchronized (batch) {
+ batch.add(record);
+ if (record.guid.contains("Fail")) {
+ batchShouldFail = true;
+ }
+
+ if (batch.size() >= batchSize) {
+ flush();
+ }
+ }
+ }
+
+ public void flush() {
+ final ArrayList<Record> thisBatch = new ArrayList<Record>(batch);
+ final boolean thisBatchShouldFail = batchShouldFail;
+ batchShouldFail = false;
+ batch.clear();
+ storeWorkQueue.execute(new Runnable() {
+ @Override
+ public void run() {
+ Logger.trace("XXX", "Notifying about batch. Failure? " + thisBatchShouldFail);
+ for (Record batchRecord : thisBatch) {
+ if (thisBatchShouldFail) {
+ delegate.onRecordStoreFailed(new StoreFailedException(), batchRecord.guid);
+ } else {
+ try {
+ superStore(batchRecord);
+ } catch (NoStoreDelegateException e) {
+ delegate.onRecordStoreFailed(e, batchRecord.guid);
+ }
+ }
+ }
+ }
+ });
+ }
+
+ @Override
+ public void storeDone() {
+ synchronized (batch) {
+ flush();
+ // Do this in a Runnable so that the timestamp is grabbed after any upload.
+ final Runnable r = new Runnable() {
+ @Override
+ public void run() {
+ synchronized (batch) {
+ Logger.trace("XXX", "Calling storeDone.");
+ storeDone(now());
+ }
+ }
+ };
+ storeWorkQueue.execute(r);
+ }
+ }
+ }
+ public BatchFailStoreWBORepository(int batchSize) {
+ super();
+ this.batchSize = batchSize;
+ }
+
+ @Override
+ public void createSession(RepositorySessionCreationDelegate delegate,
+ Context context) {
+ delegate.deferredCreationDelegate().onSessionCreated(new BatchFailStoreWBORepositorySession(this));
+ }
+ }
+
+ public static class TrackingWBORepository extends WBORepository {
+ @Override
+ public synchronized boolean shouldTrack() {
+ return true;
+ }
+ }
+
+ public static class BeginFailedException extends Exception {
+ private static final long serialVersionUID = -2349459755976915096L;
+ }
+
+ public static class FinishFailedException extends Exception {
+ private static final long serialVersionUID = -4644528423867070934L;
+ }
+
+ public static class BeginErrorWBORepository extends TrackingWBORepository {
+ @Override
+ public void createSession(RepositorySessionCreationDelegate delegate,
+ Context context) {
+ delegate.deferredCreationDelegate().onSessionCreated(new BeginErrorWBORepositorySession(this));
+ }
+
+ public class BeginErrorWBORepositorySession extends WBORepositorySession {
+ public BeginErrorWBORepositorySession(WBORepository repository) {
+ super(repository);
+ }
+
+ @Override
+ public void begin(RepositorySessionBeginDelegate delegate) throws InvalidSessionTransitionException {
+ delegate.onBeginFailed(new BeginFailedException());
+ }
+ }
+ }
+
+ public static class FinishErrorWBORepository extends TrackingWBORepository {
+ @Override
+ public void createSession(RepositorySessionCreationDelegate delegate,
+ Context context) {
+ delegate.deferredCreationDelegate().onSessionCreated(new FinishErrorWBORepositorySession(this));
+ }
+
+ public class FinishErrorWBORepositorySession extends WBORepositorySession {
+ public FinishErrorWBORepositorySession(WBORepository repository) {
+ super(repository);
+ }
+
+ @Override
+ public void finish(final RepositorySessionFinishDelegate delegate) throws InactiveSessionException {
+ delegate.onFinishFailed(new FinishFailedException());
+ }
+ }
+ }
+
+ public static class DataAvailableWBORepository extends TrackingWBORepository {
+ public boolean dataAvailable = true;
+
+ public DataAvailableWBORepository(boolean dataAvailable) {
+ this.dataAvailable = dataAvailable;
+ }
+
+ @Override
+ public void createSession(RepositorySessionCreationDelegate delegate,
+ Context context) {
+ delegate.deferredCreationDelegate().onSessionCreated(new DataAvailableWBORepositorySession(this));
+ }
+
+ public class DataAvailableWBORepositorySession extends WBORepositorySession {
+ public DataAvailableWBORepositorySession(WBORepository repository) {
+ super(repository);
+ }
+
+ @Override
+ public boolean dataAvailable() {
+ return dataAvailable;
+ }
+ }
+ }
+
+ public static class ShouldSkipWBORepository extends TrackingWBORepository {
+ public boolean shouldSkip = true;
+
+ public ShouldSkipWBORepository(boolean shouldSkip) {
+ this.shouldSkip = shouldSkip;
+ }
+
+ @Override
+ public void createSession(RepositorySessionCreationDelegate delegate,
+ Context context) {
+ delegate.deferredCreationDelegate().onSessionCreated(new ShouldSkipWBORepositorySession(this));
+ }
+
+ public class ShouldSkipWBORepositorySession extends WBORepositorySession {
+ public ShouldSkipWBORepositorySession(WBORepository repository) {
+ super(repository);
+ }
+
+ @Override
+ public boolean shouldSkip() {
+ return shouldSkip;
+ }
+ }
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestCollectionKeys.java
@@ -0,0 +1,195 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.android.sync.test;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Set;
+
+import org.json.simple.JSONArray;
+import org.json.simple.parser.ParseException;
+import org.junit.Test;
+import org.mozilla.apache.commons.codec.binary.Base64;
+import org.mozilla.gecko.sync.CollectionKeys;
+import org.mozilla.gecko.sync.CryptoRecord;
+import org.mozilla.gecko.sync.NoCollectionKeysSetException;
+import org.mozilla.gecko.sync.NonObjectJSONException;
+import org.mozilla.gecko.sync.crypto.CryptoException;
+import org.mozilla.gecko.sync.crypto.KeyBundle;
+
+public class TestCollectionKeys {
+
+ @Test
+ public void testDefaultKeys() throws CryptoException, NoCollectionKeysSetException {
+ CollectionKeys ck = new CollectionKeys();
+ try {
+ ck.defaultKeyBundle();
+ fail("defaultKeys should throw.");
+ } catch (NoCollectionKeysSetException ex) {
+ // Good.
+ }
+ KeyBundle testKeys = KeyBundle.withRandomKeys();
+ ck.setDefaultKeyBundle(testKeys);
+ assertEquals(testKeys, ck.defaultKeyBundle());
+ }
+
+ @Test
+ public void testKeyForCollection() throws CryptoException, NoCollectionKeysSetException {
+ CollectionKeys ck = new CollectionKeys();
+ try {
+ ck.keyBundleForCollection("test");
+ fail("keyForCollection should throw.");
+ } catch (NoCollectionKeysSetException ex) {
+ // Good.
+ }
+ KeyBundle testKeys = KeyBundle.withRandomKeys();
+ KeyBundle otherKeys = KeyBundle.withRandomKeys();
+
+ ck.setDefaultKeyBundle(testKeys);
+ assertEquals(testKeys, ck.defaultKeyBundle());
+ assertEquals(testKeys, ck.keyBundleForCollection("test")); // Returns default.
+
+ ck.setKeyBundleForCollection("test", otherKeys);
+ assertEquals(otherKeys, ck.keyBundleForCollection("test")); // Returns default.
+
+ }
+
+ public static void assertSame(byte[] arrayOne, byte[] arrayTwo) {
+ assertTrue(Arrays.equals(arrayOne, arrayTwo));
+ }
+
+
+ @Test
+ public void testSetKeysFromWBO() throws IOException, ParseException, NonObjectJSONException, CryptoException, NoCollectionKeysSetException {
+ String json = "{\"default\":[\"3fI6k1exImMgAKjilmMaAWxGqEIzFX/9K5EjEgH99vc=\",\"/AMaoCX4hzic28WY94XtokNi7N4T0nv+moS1y5wlbug=\"],\"collections\":{},\"collection\":\"crypto\",\"id\":\"keys\"}";
+ CryptoRecord rec = new CryptoRecord(json);
+
+ KeyBundle syncKeyBundle = new KeyBundle("slyjcrjednxd6rf4cr63vqilmkus6zbe", "6m8mv8ex2brqnrmsb9fjuvfg7y");
+ rec.keyBundle = syncKeyBundle;
+
+ rec.encrypt();
+ CollectionKeys ck = new CollectionKeys();
+ ck.setKeyPairsFromWBO(rec, syncKeyBundle);
+ byte[] input = "3fI6k1exImMgAKjilmMaAWxGqEIzFX/9K5EjEgH99vc=".getBytes("UTF-8");
+ byte[] expected = Base64.decodeBase64(input);
+ assertSame(expected, ck.defaultKeyBundle().getEncryptionKey());
+ }
+
+ @Test
+ public void testCryptoRecordFromCollectionKeys() throws CryptoException, NoCollectionKeysSetException, IOException, ParseException, NonObjectJSONException {
+ CollectionKeys ck1 = CollectionKeys.generateCollectionKeys();
+ assertNotNull(ck1.defaultKeyBundle());
+ assertEquals(ck1.keyBundleForCollection("foobar"), ck1.defaultKeyBundle());
+ CryptoRecord rec = ck1.asCryptoRecord();
+ assertEquals(rec.collection, "crypto");
+ assertEquals(rec.guid, "keys");
+ JSONArray defaultKey = (JSONArray) rec.payload.get("default");
+
+ assertSame(Base64.decodeBase64((String) (defaultKey.get(0))), ck1.defaultKeyBundle().getEncryptionKey());
+ CollectionKeys ck2 = new CollectionKeys();
+ ck2.setKeyPairsFromWBO(rec, null);
+ assertSame(ck1.defaultKeyBundle().getEncryptionKey(), ck2.defaultKeyBundle().getEncryptionKey());
+ }
+
+ @Test
+ public void testCreateKeysBundle() throws CryptoException, NonObjectJSONException, IOException, ParseException, NoCollectionKeysSetException {
+ String username = "b6evr62dptbxz7fvebek7btljyu322wp";
+ String friendlyBase32SyncKey = "basuxv2426eqj7frhvpcwkavdi";
+
+ KeyBundle syncKeyBundle = new KeyBundle(username, friendlyBase32SyncKey);
+
+ CollectionKeys ck = CollectionKeys.generateCollectionKeys();
+ CryptoRecord unencrypted = ck.asCryptoRecord();
+ unencrypted.keyBundle = syncKeyBundle;
+ CryptoRecord encrypted = unencrypted.encrypt();
+
+ CollectionKeys ckDecrypted = new CollectionKeys();
+ ckDecrypted.setKeyPairsFromWBO(encrypted, syncKeyBundle);
+
+ // Compare decrypted keys to the keys that were set upon creation
+ assertArrayEquals(ck.defaultKeyBundle().getEncryptionKey(), ckDecrypted.defaultKeyBundle().getEncryptionKey());
+ assertArrayEquals(ck.defaultKeyBundle().getHMACKey(), ckDecrypted.defaultKeyBundle().getHMACKey());
+ }
+
+ @Test
+ public void testDifferences() throws Exception {
+ KeyBundle kb1 = KeyBundle.withRandomKeys();
+ KeyBundle kb2 = KeyBundle.withRandomKeys();
+ KeyBundle kb3 = KeyBundle.withRandomKeys();
+ CollectionKeys a = CollectionKeys.generateCollectionKeys();
+ CollectionKeys b = CollectionKeys.generateCollectionKeys();
+ Set<String> diffs;
+
+ a.setKeyBundleForCollection("1", kb1);
+ b.setKeyBundleForCollection("1", kb1);
+ diffs = CollectionKeys.differences(a, b);
+ assertTrue(diffs.isEmpty());
+
+ a.setKeyBundleForCollection("2", kb2);
+ diffs = CollectionKeys.differences(a, b);
+ assertArrayEquals(new String[] { "2" }, diffs.toArray(new String[diffs.size()]));
+
+ b.setKeyBundleForCollection("3", kb3);
+ diffs = CollectionKeys.differences(a, b);
+ assertEquals(2, diffs.size());
+ assertTrue(diffs.contains("2"));
+ assertTrue(diffs.contains("3"));
+
+ b.setKeyBundleForCollection("1", KeyBundle.withRandomKeys());
+ diffs = CollectionKeys.differences(a, b);
+ assertEquals(3, diffs.size());
+
+ // This tests that explicitly setting a default key works.
+ a = CollectionKeys.generateCollectionKeys();
+ b = CollectionKeys.generateCollectionKeys();
+ b.setDefaultKeyBundle(a.defaultKeyBundle());
+ a.setKeyBundleForCollection("a", a.defaultKeyBundle());
+ b.setKeyBundleForCollection("b", b.defaultKeyBundle());
+ assertTrue(CollectionKeys.differences(a, b).isEmpty());
+ assertTrue(CollectionKeys.differences(b, a).isEmpty());
+ }
+
+ @Test
+ public void testEquals() throws Exception {
+ KeyBundle kb1 = KeyBundle.withRandomKeys();
+ KeyBundle kb2 = KeyBundle.withRandomKeys();
+ CollectionKeys a = CollectionKeys.generateCollectionKeys();
+ CollectionKeys b = CollectionKeys.generateCollectionKeys();
+
+ // Random keys are different.
+ assertFalse(a.equals(b));
+ assertFalse(b.equals(a));
+
+ // keys with unset default key bundles are different.
+ b.setDefaultKeyBundle(null);
+ assertFalse(a.equals(b));
+
+ // keys with equal default key bundles and no other collections are the same.
+ b.setDefaultKeyBundle(a.defaultKeyBundle());
+ assertTrue(a.equals(b));
+
+ // keys with equal defaults and equal collections are the same.
+ a.setKeyBundleForCollection("1", kb1);
+ b.setKeyBundleForCollection("1", kb1);
+ assertTrue(a.equals(b));
+
+ // keys with equal defaults but some collection missing are different.
+ a.setKeyBundleForCollection("2", kb2);
+ assertFalse(a.equals(b));
+ assertFalse(b.equals(a));
+
+ // keys with equal defaults and some collection set to the default are the same.
+ a.setKeyBundleForCollection("2", a.defaultKeyBundle());
+ b.setKeyBundleForCollection("3", b.defaultKeyBundle());
+ assertTrue(a.equals(b));
+ assertTrue(b.equals(a));
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestCommandProcessor.java
@@ -0,0 +1,115 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.android.sync.test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.json.simple.parser.ParseException;
+import org.junit.Test;
+import org.mozilla.gecko.sync.CommandProcessor;
+import org.mozilla.gecko.sync.CommandRunner;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.GlobalSession;
+import org.mozilla.gecko.sync.NonObjectJSONException;
+
+public class TestCommandProcessor extends CommandProcessor {
+
+ public static final String commandType = "displayURI";
+ public static final String commandWithNoArgs = "{\"command\":\"displayURI\"}";
+ public static final String commandWithNoType = "{\"args\":[\"https://bugzilla.mozilla.org/show_bug.cgi?id=731341\",\"PKsljsuqYbGg\"]}";
+ public static final String wellFormedCommand = "{\"args\":[\"https://bugzilla.mozilla.org/show_bug.cgi?id=731341\",\"PKsljsuqYbGg\"],\"command\":\"displayURI\"}";
+ public static final String wellFormedCommandWithNullArgs = "{\"args\":[\"https://bugzilla.mozilla.org/show_bug.cgi?id=731341\",null,\"PKsljsuqYbGg\",null],\"command\":\"displayURI\"}";
+
+ private boolean commandExecuted;
+
+ // Session is not used in these tests.
+ protected final GlobalSession session = null;
+
+ public class MockCommandRunner extends CommandRunner {
+ public MockCommandRunner(int argCount) {
+ super(argCount);
+ }
+
+ @Override
+ public void executeCommand(final GlobalSession session, List<String> args) {
+ commandExecuted = true;
+ }
+ }
+
+ @Test
+ public void testRegisterCommand() throws NonObjectJSONException, IOException, ParseException {
+ assertNull(commands.get(commandType));
+ this.registerCommand(commandType, new MockCommandRunner(1));
+ assertNotNull(commands.get(commandType));
+ }
+
+ @Test
+ public void testProcessRegisteredCommand() throws NonObjectJSONException, IOException, ParseException {
+ commandExecuted = false;
+ ExtendedJSONObject unparsedCommand = new ExtendedJSONObject(wellFormedCommand);
+ this.registerCommand(commandType, new MockCommandRunner(1));
+ this.processCommand(session, unparsedCommand);
+ assertTrue(commandExecuted);
+ }
+
+ @Test
+ public void testProcessUnregisteredCommand() throws NonObjectJSONException, IOException, ParseException {
+ commandExecuted = false;
+ ExtendedJSONObject unparsedCommand = new ExtendedJSONObject(wellFormedCommand);
+ this.processCommand(session, unparsedCommand);
+ assertFalse(commandExecuted);
+ }
+
+ @Test
+ public void testProcessInvalidCommand() throws NonObjectJSONException, IOException, ParseException {
+ ExtendedJSONObject unparsedCommand = new ExtendedJSONObject(commandWithNoType);
+ this.registerCommand(commandType, new MockCommandRunner(1));
+ this.processCommand(session, unparsedCommand);
+ assertFalse(commandExecuted);
+ }
+
+ @Test
+ public void testParseCommandNoType() throws NonObjectJSONException, IOException, ParseException {
+ ExtendedJSONObject unparsedCommand = new ExtendedJSONObject(commandWithNoType);
+ assertNull(CommandProcessor.parseCommand(unparsedCommand));
+ }
+
+ @Test
+ public void testParseCommandNoArgs() throws NonObjectJSONException, IOException, ParseException {
+ ExtendedJSONObject unparsedCommand = new ExtendedJSONObject(commandWithNoArgs);
+ assertNull(CommandProcessor.parseCommand(unparsedCommand));
+ }
+
+ @Test
+ public void testParseWellFormedCommand() throws NonObjectJSONException, IOException, ParseException {
+ ExtendedJSONObject unparsedCommand = new ExtendedJSONObject(wellFormedCommand);
+ Command parsedCommand = CommandProcessor.parseCommand(unparsedCommand);
+ assertNotNull(parsedCommand);
+ assertEquals(2, parsedCommand.args.size());
+ assertEquals(commandType, parsedCommand.commandType);
+ }
+
+ @Test
+ public void testParseCommandNullArg() throws NonObjectJSONException, IOException, ParseException {
+ ExtendedJSONObject unparsedCommand = new ExtendedJSONObject(wellFormedCommandWithNullArgs);
+ Command parsedCommand = CommandProcessor.parseCommand(unparsedCommand);
+ assertNotNull(parsedCommand);
+ assertEquals(4, parsedCommand.args.size());
+ assertEquals(commandType, parsedCommand.commandType);
+ final List<String> expectedArgs = new ArrayList<String>();
+ expectedArgs.add("https://bugzilla.mozilla.org/show_bug.cgi?id=731341");
+ expectedArgs.add(null);
+ expectedArgs.add("PKsljsuqYbGg");
+ expectedArgs.add(null);
+ assertEquals(expectedArgs, parsedCommand.getArgsList());
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestCryptoRecord.java
@@ -0,0 +1,294 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.android.sync.test;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.util.Arrays;
+
+import org.json.simple.JSONArray;
+import org.json.simple.JSONObject;
+import org.json.simple.parser.ParseException;
+import org.junit.Test;
+import org.mozilla.apache.commons.codec.binary.Base64;
+import org.mozilla.gecko.sync.CryptoRecord;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.NonObjectJSONException;
+import org.mozilla.gecko.sync.Utils;
+import org.mozilla.gecko.sync.crypto.CryptoException;
+import org.mozilla.gecko.sync.crypto.KeyBundle;
+import org.mozilla.gecko.sync.repositories.domain.ClientRecord;
+import org.mozilla.gecko.sync.repositories.domain.HistoryRecord;
+import org.mozilla.gecko.sync.repositories.domain.Record;
+
+public class TestCryptoRecord {
+ String base64EncryptionKey = "9K/wLdXdw+nrTtXo4ZpECyHFNr4d7aYHqeg3KW9+m6Q=";
+ String base64HmacKey = "MMntEfutgLTc8FlTLQFms8/xMPmCldqPlq/QQXEjx70=";
+
+ @Test
+ public void testBaseCryptoRecordEncrypt() throws IOException, ParseException, NonObjectJSONException, CryptoException {
+ ExtendedJSONObject clearPayload = ExtendedJSONObject.parseJSONObject("{\"id\":\"5qRsgXWRJZXr\",\"title\":\"Index of file:///Users/jason/Library/Application Support/Firefox/Profiles/ksgd7wpk.LocalSyncServer/weave/logs/\",\"histUri\":\"file:///Users/jason/Library/Application%20Support/Firefox/Profiles/ksgd7wpk.LocalSyncServer/weave/logs/\",\"visits\":[{\"type\":1,\"date\":1319149012372425}]}");
+
+ CryptoRecord record = new CryptoRecord();
+ record.payload = clearPayload;
+ String expectedGUID = "5qRsgXWRJZXr";
+ record.guid = expectedGUID;
+ record.keyBundle = KeyBundle.fromBase64EncodedKeys(base64EncryptionKey, base64HmacKey);
+ record.encrypt();
+ assertTrue(record.payload.get("title") == null);
+ assertTrue(record.payload.get("ciphertext") != null);
+ assertEquals(expectedGUID, record.guid);
+ assertEquals(expectedGUID, record.toJSONObject().get("id"));
+ record.decrypt();
+ assertEquals(expectedGUID, record.toJSONObject().get("id"));
+ }
+
+ @Test
+ public void testEntireRecord() throws Exception {
+ // Check a raw JSON blob from a real Sync account.
+ String inputString = "{\"sortindex\": 131, \"payload\": \"{\\\"ciphertext\\\":\\\"YJB4dr0vZEIWPirfU2FCJvfzeSLiOP5QWasol2R6ILUxdHsJWuUuvTZVhxYQfTVNou6hVV67jfAvi5Cs+bqhhQsv7icZTiZhPTiTdVGt+uuMotxauVA5OryNGVEZgCCTvT3upzhDFdDbJzVd9O3/gU/b7r/CmAHykX8bTlthlbWeZ8oz6gwHJB5tPRU15nM/m/qW1vyKIw5pw/ZwtAy630AieRehGIGDk+33PWqsfyuT4EUFY9/Ly+8JlnqzxfiBCunIfuXGdLuqTjJOxgrK8mI4wccRFEdFEnmHvh5x7fjl1ID52qumFNQl8zkB75C8XK25alXqwvRR6/AQSP+BgQ==\\\",\\\"IV\\\":\\\"v/0BFgicqYQsd70T39rraA==\\\",\\\"hmac\\\":\\\"59605ed696f6e0e6e062a03510cff742bf6b50d695c042e8372a93f4c2d37dac\\\"}\", \"id\": \"0-P9fabp9vJD\", \"modified\": 1326254123.65}";
+ CryptoRecord record = CryptoRecord.fromJSONRecord(inputString);
+ assertEquals("0-P9fabp9vJD", record.guid);
+ assertEquals(1326254123650L, record.lastModified);
+ assertEquals(131, record.sortIndex);
+
+ String b64E = "0A7mU5SZ/tu7ZqwXW1og4qHVHN+zgEi4Xwfwjw+vEJw=";
+ String b64H = "11GN34O9QWXkjR06g8t0gWE1sGgQeWL0qxxWwl8Dmxs=";
+ record.keyBundle = KeyBundle.fromBase64EncodedKeys(b64E, b64H);
+ record.decrypt();
+
+ assertEquals("0-P9fabp9vJD", record.guid);
+ assertEquals(1326254123650L, record.lastModified);
+ assertEquals(131, record.sortIndex);
+
+ assertEquals("Customize Firefox", record.payload.get("title"));
+ assertEquals("0-P9fabp9vJD", record.payload.get("id"));
+ assertTrue(record.payload.get("tags") instanceof JSONArray);
+ }
+
+ @Test
+ public void testBaseCryptoRecordDecrypt() throws Exception {
+ String base64CipherText =
+ "NMsdnRulLwQsVcwxKW9XwaUe7ouJk5Wn"
+ + "80QhbD80l0HEcZGCynh45qIbeYBik0lg"
+ + "cHbKmlIxTJNwU+OeqipN+/j7MqhjKOGI"
+ + "lvbpiPQQLC6/ffF2vbzL0nzMUuSyvaQz"
+ + "yGGkSYM2xUFt06aNivoQTvU2GgGmUK6M"
+ + "vadoY38hhW2LCMkoZcNfgCqJ26lO1O0s"
+ + "EO6zHsk3IVz6vsKiJ2Hq6VCo7hu123wN"
+ + "egmujHWQSGyf8JeudZjKzfi0OFRRvvm4"
+ + "QAKyBWf0MgrW1F8SFDnVfkq8amCB7Nhd"
+ + "whgLWbN+21NitNwWYknoEWe1m6hmGZDg"
+ + "DT32uxzWxCV8QqqrpH/ZggViEr9uMgoy"
+ + "4lYaWqP7G5WKvvechc62aqnsNEYhH26A"
+ + "5QgzmlNyvB+KPFvPsYzxDnSCjOoRSLx7"
+ + "GG86wT59QZw=";
+ String base64IV = "GX8L37AAb2FZJMzIoXlX8w==";
+ String base16Hmac =
+ "b1e6c18ac30deb70236bc0d65a46f7a4"
+ + "dce3b8b0e02cf92182b914e3afa5eebc";
+
+ ExtendedJSONObject body = new ExtendedJSONObject();
+ ExtendedJSONObject payload = new ExtendedJSONObject();
+ payload.put("ciphertext", base64CipherText);
+ payload.put("IV", base64IV);
+ payload.put("hmac", base16Hmac);
+ body.put("payload", payload.toJSONString());
+ CryptoRecord record = CryptoRecord.fromJSONRecord(body);
+ byte[] decodedKey = Base64.decodeBase64(base64EncryptionKey.getBytes("UTF-8"));
+ byte[] decodedHMAC = Base64.decodeBase64(base64HmacKey.getBytes("UTF-8"));
+ record.keyBundle = new KeyBundle(decodedKey, decodedHMAC);
+
+ record.decrypt();
+ String id = (String) record.payload.get("id");
+ assertTrue(id.equals("5qRsgXWRJZXr"));
+ }
+
+ @Test
+ public void testBaseCryptoRecordSyncKeyBundle() throws UnsupportedEncodingException, CryptoException {
+ // These values pulled straight out of Firefox.
+ String key = "6m8mv8ex2brqnrmsb9fjuvfg7y";
+ String user = "c6o7dvmr2c4ud2fyv6woz2u4zi22bcyd";
+
+ // Check our friendly base32 decoding.
+ assertTrue(Arrays.equals(Utils.decodeFriendlyBase32(key), Base64.decodeBase64("8xbKrJfQYwbFkguKmlSm/g==".getBytes("UTF-8"))));
+ KeyBundle bundle = new KeyBundle(user, key);
+ String expectedEncryptKeyBase64 = "/8RzbFT396htpZu5rwgIg2WKfyARgm7dLzsF5pwrVz8=";
+ String expectedHMACKeyBase64 = "NChGjrqoXYyw8vIYP2334cvmMtsjAMUZNqFwV2LGNkM=";
+ byte[] computedEncryptKey = bundle.getEncryptionKey();
+ byte[] computedHMACKey = bundle.getHMACKey();
+ assertTrue(Arrays.equals(computedEncryptKey, Base64.decodeBase64(expectedEncryptKeyBase64.getBytes("UTF-8"))));
+ assertTrue(Arrays.equals(computedHMACKey, Base64.decodeBase64(expectedHMACKeyBase64.getBytes("UTF-8"))));
+ }
+
+ @Test
+ public void testDecrypt() throws Exception {
+ String jsonInput = "{\"sortindex\": 90, \"payload\":" +
+ "\"{\\\"ciphertext\\\":\\\"F4ukf0" +
+ "LM+vhffiKyjaANXeUhfmOPPmQYX1XBoG" +
+ "Rh1LiHeKHB5rqjhzd7yAoxqgmFnkIgQF" +
+ "YPSqRAoCxWiAeGULTX+KM4MU5drbNyR/" +
+ "690JBWSyE1vQSiMGwNIbTKnOLGHKkQVY" +
+ "HDpajg5BNFfvHNQ5Jx7uM9uJcmuEjCI6" +
+ "GRMDKyKjhsTqCd99MONkY5rISutaWQ0e" +
+ "EXFgpA9RZPv4jgWlQhe+YrVnpcrTi20b" +
+ "NgKp3IfIeqEelrZ5FJd2WGZOA021d3e7" +
+ "P3Z4qptefH4Q9/hySrWsELWngBaydyn/" +
+ "IjsheZuKra3kJSST/4SvRZ7qXn\\\",\\" +
+ "\"IV\\\":\\\"GadPajeXhpk75K2YH+L" +
+ "y4w==\\\",\\\"hmac\\\":\\\"71442" +
+ "d946502e3ca475c70a633d3d37f4b4e9" +
+ "313a6d1041d0c0550cd354e7605\\\"}" +
+ "\", \"id\": \"hkZYpC-BH4Xi\", \"" +
+ "modified\": 1320183464.21}";
+ String base64EncryptionKey = "K8fV6PHG8RgugfHexGesbzTeOs2o12cr" +
+ "N/G3bz0Bx1M=";
+ String base64HmacKey = "nbceuI6w1RJbBzh+iCJHEs8p4lElsOma" +
+ "yUhx+OztVgM=";
+ String expectedDecryptedText = "{\"id\":\"hkZYpC-BH4Xi\",\"histU" +
+ "ri\":\"http://hathology.com/2008" +
+ "/06/how-to-edit-your-path-enviro" +
+ "nment-variables-on-mac-os-x/\",\"" +
+ "title\":\"How To Edit Your PATH " +
+ "Environment Variables On Mac OS " +
+ "X\",\"visits\":[{\"date\":131898" +
+ "2074310889,\"type\":1}]}";
+
+ KeyBundle keyBundle = KeyBundle.fromBase64EncodedKeys(base64EncryptionKey, base64HmacKey);
+
+ CryptoRecord encrypted = CryptoRecord.fromJSONRecord(jsonInput);
+ encrypted.keyBundle = keyBundle;
+ CryptoRecord decrypted = encrypted.decrypt();
+
+ // We don't necessarily produce exactly the same JSON but we do have the same values.
+ ExtendedJSONObject expectedJson = new ExtendedJSONObject(expectedDecryptedText);
+ assertEquals(expectedJson.get("id"), decrypted.payload.get("id"));
+ assertEquals(expectedJson.get("title"), decrypted.payload.get("title"));
+ assertEquals(expectedJson.get("histUri"), decrypted.payload.get("histUri"));
+ }
+
+ @Test
+ public void testEncryptDecrypt() throws Exception {
+ String originalText = "{\"id\":\"hkZYpC-BH4Xi\",\"histU" +
+ "ri\":\"http://hathology.com/2008" +
+ "/06/how-to-edit-your-path-enviro" +
+ "nment-variables-on-mac-os-x/\",\"" +
+ "title\":\"How To Edit Your PATH " +
+ "Environment Variables On Mac OS " +
+ "X\",\"visits\":[{\"date\":131898" +
+ "2074310889,\"type\":1}]}";
+ String base64EncryptionKey = "K8fV6PHG8RgugfHexGesbzTeOs2o12cr" +
+ "N/G3bz0Bx1M=";
+ String base64HmacKey = "nbceuI6w1RJbBzh+iCJHEs8p4lElsOma" +
+ "yUhx+OztVgM=";
+
+ KeyBundle keyBundle = KeyBundle.fromBase64EncodedKeys(base64EncryptionKey, base64HmacKey);
+
+ // Encrypt.
+ CryptoRecord unencrypted = new CryptoRecord(originalText);
+ unencrypted.keyBundle = keyBundle;
+ CryptoRecord encrypted = unencrypted.encrypt();
+
+ // Decrypt after round-trip through JSON.
+ CryptoRecord undecrypted = CryptoRecord.fromJSONRecord(encrypted.toJSONString());
+ undecrypted.keyBundle = keyBundle;
+ CryptoRecord decrypted = undecrypted.decrypt();
+
+ // We don't necessarily produce exactly the same JSON but we do have the same values.
+ ExtendedJSONObject expectedJson = new ExtendedJSONObject(originalText);
+ assertEquals(expectedJson.get("id"), decrypted.payload.get("id"));
+ assertEquals(expectedJson.get("title"), decrypted.payload.get("title"));
+ assertEquals(expectedJson.get("histUri"), decrypted.payload.get("histUri"));
+ }
+
+ @Test
+ public void testDecryptKeysBundle() throws Exception {
+ String jsonInput = "{\"payload\": \"{\\\"ciphertext\\" +
+ "\":\\\"L1yRyZBkVYKXC1cTpeUqqfmKg" +
+ "CinYV9YntGiG0PfYZSTLQ2s86WPI0VBb" +
+ "QbLZfx7udk6sf6CFE4w5EgiPx0XP3Fbj" +
+ "L7r4qIT0vjbAOrLKedZwA3cgiquc+PXM" +
+ "Etml8B4Dfm0crJK0iROlRkb+lePAYkzI" +
+ "iQn5Ba8mSWQEFoLy3zAcfCYXumA7E0Fj" +
+ "XYD+TqTG5bqYJY4zvPaB9mn9y3WHw==\\" +
+ "\",\\\"IV\\\":\\\"Jjb2oVI5uvvFfm" +
+ "ZYRY4GaA==\\\",\\\"hmac\\\":\\\"" +
+ "0b59731cb1aaedc85f54917b7058f361" +
+ "60826b70050b0d70cd42b0b609b1d717" +
+ "\\\"}\", \"id\": \"keys\", \"mod" +
+ "ified\": 1320183463.91}";
+ String username = "b6evr62dptbxz7fvebek7btljyu322wp";
+ String friendlyBase32SyncKey = "basuxv2426eqj7frhvpcwkavdi";
+ String expectedDecryptedText = "{\"default\":[\"K8fV6PHG8RgugfHe" +
+ "xGesbzTeOs2o12crN/G3bz0Bx1M=\",\"" +
+ "nbceuI6w1RJbBzh+iCJHEs8p4lElsOma" +
+ "yUhx+OztVgM=\"],\"collections\":" +
+ "{},\"collection\":\"crypto\",\"i" +
+ "d\":\"keys\"}";
+ String expectedBase64EncryptionKey = "K8fV6PHG8RgugfHexGesbzTeOs2o12cr" +
+ "N/G3bz0Bx1M=";
+ String expectedBase64HmacKey = "nbceuI6w1RJbBzh+iCJHEs8p4lElsOma" +
+ "yUhx+OztVgM=";
+
+ KeyBundle syncKeyBundle = new KeyBundle(username, friendlyBase32SyncKey);
+
+ ExtendedJSONObject json = new ExtendedJSONObject(jsonInput);
+ assertEquals("keys", json.get("id"));
+
+ CryptoRecord encrypted = CryptoRecord.fromJSONRecord(jsonInput);
+ encrypted.keyBundle = syncKeyBundle;
+ CryptoRecord decrypted = encrypted.decrypt();
+
+ // We don't necessarily produce exactly the same JSON but we do have the same values.
+ ExtendedJSONObject expectedJson = new ExtendedJSONObject(expectedDecryptedText);
+ assertEquals(expectedJson.get("id"), decrypted.payload.get("id"));
+ assertEquals(expectedJson.get("default"), decrypted.payload.get("default"));
+ assertEquals(expectedJson.get("collection"), decrypted.payload.get("collection"));
+ assertEquals(expectedJson.get("collections"), decrypted.payload.get("collections"));
+
+ // Check that the extracted keys were as expected.
+ JSONArray keys = ExtendedJSONObject.parseJSONObject(decrypted.payload.toJSONString()).getArray("default");
+ KeyBundle keyBundle = KeyBundle.fromBase64EncodedKeys((String)keys.get(0), (String)keys.get(1));
+
+ assertArrayEquals(Base64.decodeBase64(expectedBase64EncryptionKey.getBytes("UTF-8")), keyBundle.getEncryptionKey());
+ assertArrayEquals(Base64.decodeBase64(expectedBase64HmacKey.getBytes("UTF-8")), keyBundle.getHMACKey());
+ }
+
+ @Test
+ public void testTTL() throws UnsupportedEncodingException, CryptoException {
+ Record historyRecord = new HistoryRecord();
+ CryptoRecord cryptoRecord = historyRecord.getEnvelope();
+ assertEquals(historyRecord.ttl, cryptoRecord.ttl);
+
+ // Very important that ttls are set in outbound envelopes.
+ JSONObject o = cryptoRecord.toJSONObject();
+ assertEquals(cryptoRecord.ttl, o.get("ttl"));
+
+ // Most important of all, outbound encrypted record envelopes.
+ KeyBundle keyBundle = KeyBundle.withRandomKeys();
+ cryptoRecord.keyBundle = keyBundle;
+ cryptoRecord.encrypt();
+ assertEquals(historyRecord.ttl, cryptoRecord.ttl); // Should be preserved.
+ o = cryptoRecord.toJSONObject();
+ assertEquals(cryptoRecord.ttl, o.get("ttl"));
+
+ // But we should ignore negative ttls.
+ Record clientRecord = new ClientRecord();
+ clientRecord.ttl = -1; // Don't ttl this record.
+ o = clientRecord.getEnvelope().toJSONObject();
+ assertNull(o.get("ttl"));
+
+ // But we should ignore negative ttls in outbound encrypted record envelopes.
+ cryptoRecord = clientRecord.getEnvelope();
+ cryptoRecord.keyBundle = keyBundle;
+ cryptoRecord.encrypt();
+ o = cryptoRecord.toJSONObject();
+ assertNull(o.get("ttl"));
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestJPakeSetup.java
@@ -0,0 +1,259 @@
+package org.mozilla.android.sync.test;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.math.BigInteger;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+
+import org.json.simple.parser.ParseException;
+import org.junit.Test;
+import org.mozilla.apache.commons.codec.binary.Base64;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.NonObjectJSONException;
+import org.mozilla.gecko.sync.crypto.CryptoException;
+import org.mozilla.gecko.sync.crypto.KeyBundle;
+import org.mozilla.gecko.sync.jpake.Gx3OrGx4IsZeroOrOneException;
+import org.mozilla.gecko.sync.jpake.IncorrectZkpException;
+import org.mozilla.gecko.sync.jpake.JPakeClient;
+import org.mozilla.gecko.sync.jpake.JPakeCrypto;
+import org.mozilla.gecko.sync.jpake.JPakeNumGenerator;
+import org.mozilla.gecko.sync.jpake.JPakeNumGeneratorRandom;
+import org.mozilla.gecko.sync.jpake.JPakeParty;
+import org.mozilla.gecko.sync.jpake.stage.ComputeKeyVerificationStage;
+import org.mozilla.gecko.sync.jpake.stage.VerifyPairingStage;
+import org.mozilla.gecko.sync.setup.Constants;
+
+public class TestJPakeSetup {
+ // Note: will throw NullPointerException if aborts. Only use stateless public
+ // methods.
+
+ @Test
+ public void testGx3OrGx4ZeroOrOneThrowsException()
+ throws UnsupportedEncodingException
+ {
+ JPakeNumGeneratorRandom gen = new JPakeNumGeneratorRandom();
+ JPakeParty p = new JPakeParty("foobar");
+ BigInteger secret = JPakeClient.secretAsBigInteger("secret");
+
+ p.gx4 = new BigInteger("2");
+ p.gx3 = new BigInteger("0");
+ try {
+ JPakeCrypto.round2(secret, p, gen);
+ fail("round2 should fail if gx3 == 0");
+ } catch (Gx3OrGx4IsZeroOrOneException e) {
+ // Hurrah.
+ } catch (Exception e) {
+ fail("Unexpected exception " + e);
+ }
+
+ p.gx3 = new BigInteger("1");
+ try {
+ JPakeCrypto.round2(secret, p, gen);
+ fail("round2 should fail if gx3 == 1");
+ } catch (Gx3OrGx4IsZeroOrOneException e) {
+ // Hurrah.
+ } catch (Exception e) {
+ fail("Unexpected exception " + e);
+ }
+
+ p.gx3 = new BigInteger("3");
+ try {
+ JPakeCrypto.round2(secret, p, gen);
+ } catch (Gx3OrGx4IsZeroOrOneException e) {
+ fail("Unexpected exception " + e);
+ } catch (Exception e) {
+ // There are plenty of other reasons this should fail.
+ }
+
+ p.gx3 = new BigInteger("2");
+ p.gx4 = new BigInteger("0");
+ try {
+ JPakeCrypto.round2(secret, p, gen);
+ fail("round2 should fail if gx4 == 0");
+ } catch (Gx3OrGx4IsZeroOrOneException e) {
+ // Hurrah.
+ } catch (Exception e) {
+ fail("Unexpected exception " + e);
+ }
+
+ p.gx4 = new BigInteger("1");
+ try {
+ JPakeCrypto.round2(secret, p, gen);
+ fail("round2 should fail if gx4 == 1");
+ } catch (Gx3OrGx4IsZeroOrOneException e) {
+ // Hurrah.
+ } catch (Exception e) {
+ fail("Unexpected exception " + e);
+ }
+
+ p.gx4 = new BigInteger("3");
+ try {
+ JPakeCrypto.round2(secret, p, gen);
+ } catch (Gx3OrGx4IsZeroOrOneException e) {
+ fail("Unexpected exception " + e);
+ } catch (Exception e) {
+ // There are plenty of other reasons this should fail.
+ }
+ }
+
+ /*
+ * Tests encryption key and hmac generation from a derived key, using values
+ * taken from a successful run of J-PAKE.
+ */
+ @Test
+ public void testKeyDerivation() throws UnsupportedEncodingException {
+ String keyChars16 = "811565455b22c857a3e303d1f48ff72ae9ef42d9c3fe3740ce7772cb5bfe23491dd5b7ee5af4828ab9b7d5844866f378b4cf0156810aff0504ef2947402e8e40be1179cf7f37b231bc0db9e4e1bb239c849aa5c12ed2b0b4413017599270aae71ee993dd755ee8c045c5fe03d713894692bf72158d9835ad905442edfd8235e1d0c915053debfc49d8248e4dae16608743aef5dab061f49fd6edd0b93ecdf9feafcbe47eb7e6c3678356d96e9bcd87814b13b9eb1a791fd446d69cb040ec7d7194031267e26f266ee3decbc1a85c5203427361997adf9823fbffe16af9946f1347c5354956356732e436ef5f8307e96554cf69a54e4e8a78552e3f506e9310a1c4438d3ddce44a37482270533e47fc40dc84abfe39c1f95328d0d2540074f6301d4f121c2f0eac49c47a2c430614234ca26dede2a429e2fdb6d282a85174886c3a68c3cf5edc87ccb82af4ae4a9a26fffadc7f4d8ded4ff47b3d2d171f374b230e52e6b45963d3a0a6b20cbe6a440fd4a932279d52a6fd7694b4cbc0cb67ff3c";
+ String expectedEncKey64 = "3TXwVlWf6YbuIPcg8m/2U4UXYV4a8RNu6pE2GOVkJJo=";
+ String expectedHmac64 = "L49fnEPAD31G5uEKy5e4bGZ6IF3G/62qW6Ua/1NvBeQ=";
+
+ byte[] encKeyBytes = new byte[32];
+ byte[] hmacBytes = new byte[32];
+
+ try {
+ JPakeCrypto.generateKeyAndHmac(new BigInteger(keyChars16, 16), encKeyBytes, hmacBytes);
+ } catch (Exception e) {
+ fail("Unexpected exception " + e);
+ }
+ String encKey64 = new String(Base64.encodeBase64(encKeyBytes));
+ String hmac64 = new String(Base64.encodeBase64(hmacBytes));
+
+ assertTrue(expectedEncKey64.equals(encKey64));
+ assertTrue(expectedHmac64.equals(hmac64));
+ }
+
+ /*
+ * Test correct key derivation when both parties share a secret.
+ */
+ @Test
+ public void testJPakeCorrectSecret() throws Gx3OrGx4IsZeroOrOneException,
+ IncorrectZkpException, IOException, ParseException,
+ NonObjectJSONException, CryptoException, NoSuchAlgorithmException, InvalidKeyException {
+ BigInteger secret = JPakeClient.secretAsBigInteger("byubd7u75qmq");
+ JPakeNumGenerator gen = new JPakeNumGeneratorRandom();
+ // Keys derived should be the same.
+ assertTrue(jPakeDeriveSameKey(gen, gen, secret, secret));
+ }
+
+ /*
+ * Test incorrect key derivation when parties do not share the same secret.
+ */
+ @Test
+ public void testJPakeIncorrectSecret() throws Gx3OrGx4IsZeroOrOneException,
+ IncorrectZkpException, IOException, ParseException,
+ NonObjectJSONException, CryptoException, NoSuchAlgorithmException, InvalidKeyException {
+ BigInteger secret1 = JPakeClient.secretAsBigInteger("shareSecret1");
+ BigInteger secret2 = JPakeClient.secretAsBigInteger("shareSecret2");
+ JPakeNumGenerator gen = new JPakeNumGeneratorRandom();
+ // Unsuccessful key derivation.
+ assertFalse(jPakeDeriveSameKey(gen, gen, secret1, secret2));
+ }
+
+ /*
+ * Helper simulation of a J-PAKE key derivation between two parties, with
+ * secret1 and secret2. Both parties are assumed to be communicating on the
+ * same channel; otherwise, J-PAKE would have failed immediately.
+ */
+ public boolean jPakeDeriveSameKey(JPakeNumGenerator gen1,
+ JPakeNumGenerator gen2, BigInteger secret1, BigInteger secret2)
+ throws IncorrectZkpException, Gx3OrGx4IsZeroOrOneException, IOException,
+ ParseException, NonObjectJSONException, CryptoException, NoSuchAlgorithmException, InvalidKeyException {
+
+ // Communicating parties.
+ JPakeParty party1 = new JPakeParty("party1");
+ JPakeParty party2 = new JPakeParty("party2");
+
+ JPakeCrypto.round1(party1, gen1);
+ // After party1 round 1, these values should no longer be null.
+ assertNotNull(party1.signerId);
+ assertNotNull(party1.x2);
+ assertNotNull(party1.gx1);
+ assertNotNull(party1.gx2);
+ assertNotNull(party1.zkp1);
+ assertNotNull(party1.zkp2);
+ assertNotNull(party1.zkp1.b);
+ assertNotNull(party1.zkp1.gr);
+ assertNotNull(party1.zkp1.id);
+ assertNotNull(party1.zkp2.b);
+ assertNotNull(party1.zkp2.gr);
+ assertNotNull(party1.zkp2.id);
+
+ // party2 receives the following values from party1.
+ party2.gx3 = party1.gx1;
+ party2.gx4 = party1.gx2;
+ party2.zkp3 = party1.zkp1;
+ party2.zkp4 = party1.zkp2;
+ // TODO Run JPakeClient checks.
+
+ JPakeCrypto.round1(party2, gen2);
+ // After party2 round 1, these values should no longer be null.
+ assertNotNull(party2.signerId);
+ assertNotNull(party2.x2);
+ assertNotNull(party2.gx1);
+ assertNotNull(party2.gx2);
+ assertNotNull(party2.zkp1);
+ assertNotNull(party2.zkp2);
+ assertNotNull(party2.zkp1.b);
+ assertNotNull(party2.zkp1.gr);
+ assertNotNull(party2.zkp1.id);
+ assertNotNull(party2.zkp2.b);
+ assertNotNull(party2.zkp2.gr);
+ assertNotNull(party2.zkp2.id);
+
+ // Pass relevant values to party1.
+ party1.gx3 = party2.gx1;
+ party1.gx4 = party2.gx2;
+ party1.zkp3 = party2.zkp1;
+ party1.zkp4 = party2.zkp2;
+ // TODO Run JPakeClient checks.
+
+ JPakeCrypto.round2(secret1, party1, gen1);
+ // After party1 round 2, these values should no longer be null.
+ assertNotNull(party1.thisA);
+ assertNotNull(party1.thisZkpA);
+ assertNotNull(party1.thisZkpA.b);
+ assertNotNull(party1.thisZkpA.gr);
+ assertNotNull(party1.thisZkpA.id);
+
+ // Pass relevant values to party2.
+ party2.otherA = party1.thisA;
+ party2.otherZkpA = party1.thisZkpA;
+
+ JPakeCrypto.round2(secret2, party2, gen2);
+ // Check for nulls.
+ assertNotNull(party2.thisA);
+ assertNotNull(party2.thisZkpA);
+ assertNotNull(party2.thisZkpA.b);
+ assertNotNull(party2.thisZkpA.gr);
+ assertNotNull(party2.thisZkpA.id);
+
+ // Pass values to party1.
+ party1.otherA = party2.thisA;
+ party1.otherZkpA = party2.thisZkpA;
+
+ KeyBundle keyBundle1 = JPakeCrypto.finalRound(secret1, party1);
+ assertNotNull(keyBundle1);
+
+ // party1 computes the shared key, generates an encrypted message to party2.
+ ExtendedJSONObject verificationMsg = new ComputeKeyVerificationStage()
+ .computeKeyVerification(keyBundle1, party1.signerId);
+ ExtendedJSONObject payload = verificationMsg
+ .getObject(Constants.JSON_KEY_PAYLOAD);
+ String ciphertext1 = (String) payload.get(Constants.JSON_KEY_CIPHERTEXT);
+ String iv1 = (String) payload.get(Constants.JSON_KEY_IV);
+
+ // party2 computes the key as well, using its copy of the secret.
+ KeyBundle keyBundle2 = JPakeCrypto.finalRound(secret2, party2);
+ // party2 fetches the encrypted message and verifies the pairing against its
+ // own derived key.
+
+ boolean isSuccess = new VerifyPairingStage().verifyCiphertext(ciphertext1, iv1,
+ keyBundle2);
+ return isSuccess;
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestRecord.java
@@ -0,0 +1,328 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.android.sync.test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.io.IOException;
+import java.util.ArrayList;
+
+import org.json.simple.JSONArray;
+import org.json.simple.JSONObject;
+import org.json.simple.parser.ParseException;
+import org.junit.Test;
+import org.mozilla.gecko.background.db.Tab;
+import org.mozilla.gecko.sync.CryptoRecord;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.NonObjectJSONException;
+import org.mozilla.gecko.sync.repositories.domain.BookmarkRecord;
+import org.mozilla.gecko.sync.repositories.domain.ClientRecord;
+import org.mozilla.gecko.sync.repositories.domain.HistoryRecord;
+import org.mozilla.gecko.sync.repositories.domain.Record;
+import org.mozilla.gecko.sync.repositories.domain.RecordParseException;
+import org.mozilla.gecko.sync.repositories.domain.TabsRecord;
+
+public class TestRecord {
+
+ @SuppressWarnings("static-method")
+ @Test
+ public void testQueryRecord() throws NonObjectJSONException, IOException, ParseException {
+ final String expectedGUID = "Bl3n3gpKag3s";
+ final String testRecord =
+ "{\"id\":\"" + expectedGUID + "\"," +
+ " \"type\":\"query\"," +
+ " \"title\":\"Downloads\"," +
+ " \"parentName\":\"\"," +
+ " \"bmkUri\":\"place:transition=7&sort=4\"," +
+ " \"tags\":[]," +
+ " \"keyword\":null," +
+ " \"description\":null," +
+ " \"loadInSidebar\":false," +
+ " \"parentid\":\"BxfRgGiNeITG\"}";
+
+ final ExtendedJSONObject o = new ExtendedJSONObject(testRecord);
+ final CryptoRecord cr = new CryptoRecord(o);
+ cr.guid = expectedGUID;
+ cr.lastModified = System.currentTimeMillis();
+ cr.collection = "bookmarks";
+
+ final BookmarkRecord r = new BookmarkRecord("Bl3n3gpKag3s", "bookmarks");
+ r.initFromEnvelope(cr);
+ assertEquals(expectedGUID, r.guid);
+ assertEquals("query", r.type);
+ assertEquals("places:uri=place%3Atransition%3D7%26sort%3D4", r.bookmarkURI);
+
+ // Check that we get the same bookmark URI out the other end,
+ // once we've parsed it into a CryptoRecord, a BookmarkRecord, then
+ // back into a CryptoRecord.
+ assertEquals("place:transition=7&sort=4", r.getEnvelope().payload.getString("bmkUri"));
+ }
+
+ @SuppressWarnings("static-method")
+ @Test
+ public void testRecordGUIDs() {
+ for (int i = 0; i < 50; ++i) {
+ CryptoRecord cryptoRecord = new HistoryRecord().getEnvelope();
+ assertEquals(12, cryptoRecord.guid.length());
+ }
+ }
+
+ @Test
+ public void testRecordEquality() {
+ long now = System.currentTimeMillis();
+ BookmarkRecord bOne = new BookmarkRecord("abcdefghijkl", "bookmarks", now , false);
+ BookmarkRecord bTwo = new BookmarkRecord("abcdefghijkl", "bookmarks", now , false);
+ HistoryRecord hOne = new HistoryRecord("mbcdefghijkm", "history", now , false);
+ HistoryRecord hTwo = new HistoryRecord("mbcdefghijkm", "history", now , false);
+
+ // Identical records.
+ assertFalse(bOne == bTwo);
+ assertTrue(bOne.equals(bTwo));
+ assertTrue(bOne.equalPayloads(bTwo));
+ assertTrue(bOne.congruentWith(bTwo));
+ assertTrue(bTwo.equals(bOne));
+ assertTrue(bTwo.equalPayloads(bOne));
+ assertTrue(bTwo.congruentWith(bOne));
+
+ // Null checking.
+ assertFalse(bOne.equals(null));
+ assertFalse(bOne.equalPayloads(null));
+ assertFalse(bOne.congruentWith(null));
+
+ // Different types.
+ hOne.guid = bOne.guid;
+ assertFalse(bOne.equals(hOne));
+ assertFalse(bOne.equalPayloads(hOne));
+ assertFalse(bOne.congruentWith(hOne));
+ hOne.guid = hTwo.guid;
+
+ // Congruent androidID.
+ bOne.androidID = 1;
+ assertFalse(bOne.equals(bTwo));
+ assertTrue(bOne.equalPayloads(bTwo));
+ assertTrue(bOne.congruentWith(bTwo));
+ assertFalse(bTwo.equals(bOne));
+ assertTrue(bTwo.equalPayloads(bOne));
+ assertTrue(bTwo.congruentWith(bOne));
+
+ // Non-congruent androidID.
+ bTwo.androidID = 2;
+ assertFalse(bOne.equals(bTwo));
+ assertTrue(bOne.equalPayloads(bTwo));
+ assertFalse(bOne.congruentWith(bTwo));
+ assertFalse(bTwo.equals(bOne));
+ assertTrue(bTwo.equalPayloads(bOne));
+ assertFalse(bTwo.congruentWith(bOne));
+
+ // Identical androidID.
+ bOne.androidID = 2;
+ assertTrue(bOne.equals(bTwo));
+ assertTrue(bOne.equalPayloads(bTwo));
+ assertTrue(bOne.congruentWith(bTwo));
+ assertTrue(bTwo.equals(bOne));
+ assertTrue(bTwo.equalPayloads(bOne));
+ assertTrue(bTwo.congruentWith(bOne));
+
+ // Different times.
+ bTwo.lastModified += 1000;
+ assertFalse(bOne.equals(bTwo));
+ assertTrue(bOne.equalPayloads(bTwo));
+ assertTrue(bOne.congruentWith(bTwo));
+ assertFalse(bTwo.equals(bOne));
+ assertTrue(bTwo.equalPayloads(bOne));
+ assertTrue(bTwo.congruentWith(bOne));
+
+ // Add some visits.
+ JSONObject v1 = fakeVisit(now - 1000);
+ JSONObject v2 = fakeVisit(now - 500);
+
+ hOne.fennecDateVisited = now + 2000;
+ hOne.fennecVisitCount = 1;
+ assertFalse(hOne.equals(hTwo));
+ assertTrue(hOne.equalPayloads(hTwo));
+ assertTrue(hOne.congruentWith(hTwo));
+ addVisit(hOne, v1);
+ assertFalse(hOne.equals(hTwo));
+ assertFalse(hOne.equalPayloads(hTwo));
+ assertTrue(hOne.congruentWith(hTwo));
+ addVisit(hTwo, v2);
+ assertFalse(hOne.equals(hTwo));
+ assertFalse(hOne.equalPayloads(hTwo));
+ assertTrue(hOne.congruentWith(hTwo));
+
+ // Now merge the visits.
+ addVisit(hTwo, v1);
+ addVisit(hOne, v2);
+ assertFalse(hOne.equals(hTwo));
+ assertTrue(hOne.equalPayloads(hTwo));
+ assertTrue(hOne.congruentWith(hTwo));
+ hTwo.fennecDateVisited = hOne.fennecDateVisited;
+ hTwo.fennecVisitCount = hOne.fennecVisitCount = 2;
+ assertTrue(hOne.equals(hTwo));
+ assertTrue(hOne.equalPayloads(hTwo));
+ assertTrue(hOne.congruentWith(hTwo));
+ }
+
+ @SuppressWarnings("unchecked")
+ private void addVisit(HistoryRecord r, JSONObject visit) {
+ if (r.visits == null) {
+ r.visits = new JSONArray();
+ }
+ r.visits.add(visit);
+ }
+
+ @SuppressWarnings("unchecked")
+ private JSONObject fakeVisit(long time) {
+ JSONObject object = new JSONObject();
+ object.put("type", 1L);
+ object.put("date", time * 1000);
+ return object;
+ }
+
+ @SuppressWarnings("static-method")
+ @Test
+ public void testTabParsing() throws Exception {
+ String json = "{\"title\":\"mozilla-central mozilla/browser/base/content/syncSetup.js\"," +
+ " \"urlHistory\":[\"http://mxr.mozilla.org/mozilla-central/source/browser/base/content/syncSetup.js#72\"]," +
+ " \"icon\":\"http://mxr.mozilla.org/mxr.png\"," +
+ " \"lastUsed\":\"1306374531\"}";
+ Tab tab = TabsRecord.tabFromJSONObject(ExtendedJSONObject.parseJSONObject(json).object);
+
+ assertEquals("mozilla-central mozilla/browser/base/content/syncSetup.js", tab.title);
+ assertEquals("http://mxr.mozilla.org/mxr.png", tab.icon);
+ assertEquals("http://mxr.mozilla.org/mozilla-central/source/browser/base/content/syncSetup.js#72", tab.history.get(0));
+ assertEquals(1306374531000L, tab.lastUsed);
+
+ String zeroJSON = "{\"title\":\"a\"," +
+ " \"urlHistory\":[\"http://example.com\"]," +
+ " \"icon\":\"\"," +
+ " \"lastUsed\":0}";
+ Tab zero = TabsRecord.tabFromJSONObject(ExtendedJSONObject.parseJSONObject(zeroJSON).object);
+
+ assertEquals("a", zero.title);
+ assertEquals("", zero.icon);
+ assertEquals("http://example.com", zero.history.get(0));
+ assertEquals(0L, zero.lastUsed);
+ }
+
+ @SuppressWarnings({ "unchecked", "static-method" })
+ @Test
+ public void testTabsRecordCreation() throws Exception {
+ final TabsRecord record = new TabsRecord("testGuid");
+ record.clientName = "test client name";
+
+ final JSONArray history1 = new JSONArray();
+ history1.add("http://test.com/test1.html");
+ final Tab tab1 = new Tab("test title 1", "http://test.com/test1.png", history1, 1000);
+
+ final JSONArray history2 = new JSONArray();
+ history2.add("http://test.com/test2.html#1");
+ history2.add("http://test.com/test2.html#2");
+ history2.add("http://test.com/test2.html#3");
+ final Tab tab2 = new Tab("test title 2", "http://test.com/test2.png", history2, 2000);
+
+ record.tabs = new ArrayList<Tab>();
+ record.tabs.add(tab1);
+ record.tabs.add(tab2);
+
+ final TabsRecord parsed = new TabsRecord();
+ parsed.initFromEnvelope(CryptoRecord.fromJSONRecord(record.getEnvelope().toJSONString()));
+
+ assertEquals(record.guid, parsed.guid);
+ assertEquals(record.clientName, parsed.clientName);
+ assertEquals(record.tabs, parsed.tabs);
+
+ // Verify that equality test doesn't always return true.
+ parsed.tabs.get(0).history.add("http://test.com/different.html");
+ assertFalse(record.tabs.equals(parsed.tabs));
+ }
+
+ public static class URITestBookmarkRecord extends BookmarkRecord {
+ public static void doTest() {
+ assertEquals("places:uri=abc%26def+baz&p1=123&p2=bar+baz",
+ encodeUnsupportedTypeURI("abc&def baz", "p1", "123", "p2", "bar baz"));
+ assertEquals("places:uri=abc%26def+baz&p1=123",
+ encodeUnsupportedTypeURI("abc&def baz", "p1", "123", null, "bar baz"));
+ assertEquals("places:p1=123",
+ encodeUnsupportedTypeURI(null, "p1", "123", "p2", null));
+ }
+ }
+
+ @SuppressWarnings("static-method")
+ @Test
+ public void testEncodeURI() {
+ URITestBookmarkRecord.doTest();
+ }
+
+ private static final String payload =
+ "{\"id\":\"M5bwUKK8hPyF\"," +
+ "\"type\":\"livemark\"," +
+ "\"siteUri\":\"http://www.bbc.co.uk/go/rss/int/news/-/news/\"," +
+ "\"feedUri\":\"http://fxfeeds.mozilla.com/en-US/firefox/headlines.xml\"," +
+ "\"parentName\":\"Bookmarks Toolbar\"," +
+ "\"parentid\":\"toolbar\"," +
+ "\"title\":\"Latest Headlines\"," +
+ "\"description\":\"\"," +
+ "\"children\":" +
+ "[\"7oBdEZB-8BMO\", \"SUd1wktMNCTB\", \"eZe4QWzo1BcY\", \"YNBhGwhVnQsN\"," +
+ "\"mNTdpgoRZMbW\", \"-L8Vci6CbkJY\", \"bVzudKSQERc1\", \"Gxl9lb4DXsmL\"," +
+ "\"3Qr13GucOtEh\"]}";
+
+ public class PayloadBookmarkRecord extends BookmarkRecord {
+ public PayloadBookmarkRecord() {
+ super("abcdefghijkl", "bookmarks", 1234, false);
+ }
+
+ public void doTest() throws NonObjectJSONException, IOException, ParseException {
+ this.initFromPayload(new ExtendedJSONObject(payload));
+ assertEquals("abcdefghijkl", this.guid); // Ignores payload.
+ assertEquals("livemark", this.type);
+ assertEquals("Bookmarks Toolbar", this.parentName);
+ assertEquals("toolbar", this.parentID);
+ assertEquals("", this.description);
+ assertEquals(null, this.children);
+
+ final String encodedSite = "http%3A%2F%2Fwww.bbc.co.uk%2Fgo%2Frss%2Fint%2Fnews%2F-%2Fnews%2F";
+ final String encodedFeed = "http%3A%2F%2Ffxfeeds.mozilla.com%2Fen-US%2Ffirefox%2Fheadlines.xml";
+ final String expectedURI = "places:siteUri=" + encodedSite + "&feedUri=" + encodedFeed;
+ assertEquals(expectedURI, this.bookmarkURI);
+ }
+ }
+
+ @Test
+ public void testUnusualBookmarkRecords() throws NonObjectJSONException, IOException, ParseException {
+ PayloadBookmarkRecord record = new PayloadBookmarkRecord();
+ record.doTest();
+ }
+
+ @SuppressWarnings("static-method")
+ @Test
+ public void testTTL() {
+ Record record = new HistoryRecord();
+ assertEquals(HistoryRecord.HISTORY_TTL, record.ttl);
+
+ // ClientRecords are transient, HistoryRecords are not.
+ Record clientRecord = new ClientRecord();
+ assertTrue(clientRecord.ttl < record.ttl);
+
+ CryptoRecord cryptoRecord = record.getEnvelope();
+ assertEquals(record.ttl, cryptoRecord.ttl);
+ }
+
+ @Test
+ public void testStringModified() throws Exception {
+ // modified member is a string, expected a floating point number with 2
+ // decimal digits.
+ String badJson = "{\"sortindex\":\"0\",\"payload\":\"{\\\"syncID\\\":\\\"ZJOqMBjhBthH\\\",\\\"storageVersion\\\":5,\\\"engines\\\":{\\\"clients\\\":{\\\"version\\\":1,\\\"syncID\\\":\\\"4oTBXG20rJH5\\\"},\\\"bookmarks\\\":{\\\"version\\\":2,\\\"syncID\\\":\\\"JiMJXy8xI3fr\\\"},\\\"forms\\\":{\\\"version\\\":1,\\\"syncID\\\":\\\"J17vSloroXBU\\\"},\\\"history\\\":{\\\"version\\\":1,\\\"syncID\\\":\\\"y1HgpbSc3LJT\\\"},\\\"passwords\\\":{\\\"version\\\":1,\\\"syncID\\\":\\\"v3y-RidcCuT5\\\"},\\\"prefs\\\":{\\\"version\\\":2,\\\"syncID\\\":\\\"LvfqmT7cUUm4\\\"},\\\"tabs\\\":{\\\"version\\\":1,\\\"syncID\\\":\\\"MKMRlBah2d9D\\\"},\\\"addons\\\":{\\\"version\\\":1,\\\"syncID\\\":\\\"Ih2hhRrcGjh4\\\"}}}\",\"id\":\"global\",\"modified\":\"1370689360.28\"}";
+ try {
+ CryptoRecord.fromJSONRecord(badJson);
+ fail("Expected exception.");
+ } catch (Exception e) {
+ assertTrue(e instanceof RecordParseException);
+ }
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestRecordsChannel.java
@@ -0,0 +1,226 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.android.sync.test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.junit.Test;
+import org.mozilla.android.sync.test.SynchronizerHelpers.FailFetchWBORepository;
+import org.mozilla.android.sync.test.helpers.ExpectSuccessRepositorySessionCreationDelegate;
+import org.mozilla.android.sync.test.helpers.ExpectSuccessRepositorySessionFinishDelegate;
+import org.mozilla.gecko.background.testhelpers.WBORepository;
+import org.mozilla.gecko.background.testhelpers.WaitHelper;
+import org.mozilla.gecko.sync.repositories.InactiveSessionException;
+import org.mozilla.gecko.sync.repositories.InvalidSessionTransitionException;
+import org.mozilla.gecko.sync.repositories.Repository;
+import org.mozilla.gecko.sync.repositories.RepositorySession;
+import org.mozilla.gecko.sync.repositories.RepositorySessionBundle;
+import org.mozilla.gecko.sync.repositories.domain.BookmarkRecord;
+import org.mozilla.gecko.sync.synchronizer.RecordsChannel;
+import org.mozilla.gecko.sync.synchronizer.RecordsChannelDelegate;
+
+public class TestRecordsChannel {
+
+ protected WBORepository remote;
+ protected WBORepository local;
+
+ protected RepositorySession source;
+ protected RepositorySession sink;
+ protected RecordsChannelDelegate rcDelegate;
+
+ protected AtomicInteger numFlowFetchFailed;
+ protected AtomicInteger numFlowStoreFailed;
+ protected AtomicInteger numFlowCompleted;
+ protected AtomicBoolean flowBeginFailed;
+ protected AtomicBoolean flowFinishFailed;
+
+ public void doFlow(final Repository remote, final Repository local) throws Exception {
+ WaitHelper.getTestWaiter().performWait(new Runnable() {
+ @Override
+ public void run() {
+ remote.createSession(new ExpectSuccessRepositorySessionCreationDelegate(WaitHelper.getTestWaiter()) {
+ @Override
+ public void onSessionCreated(RepositorySession session) {
+ source = session;
+ local.createSession(new ExpectSuccessRepositorySessionCreationDelegate(WaitHelper.getTestWaiter()) {
+ @Override
+ public void onSessionCreated(RepositorySession session) {
+ sink = session;
+ WaitHelper.getTestWaiter().performNotify();
+ }
+ }, null);
+ }
+ }, null);
+ }
+ });
+
+ assertNotNull(source);
+ assertNotNull(sink);
+
+ numFlowFetchFailed = new AtomicInteger(0);
+ numFlowStoreFailed = new AtomicInteger(0);
+ numFlowCompleted = new AtomicInteger(0);
+ flowBeginFailed = new AtomicBoolean(false);
+ flowFinishFailed = new AtomicBoolean(false);
+
+ rcDelegate = new RecordsChannelDelegate() {
+ @Override
+ public void onFlowFetchFailed(RecordsChannel recordsChannel, Exception ex) {
+ numFlowFetchFailed.incrementAndGet();
+ }
+
+ @Override
+ public void onFlowStoreFailed(RecordsChannel recordsChannel, Exception ex, String recordGuid) {
+ numFlowStoreFailed.incrementAndGet();
+ }
+
+ @Override
+ public void onFlowFinishFailed(RecordsChannel recordsChannel, Exception ex) {
+ flowFinishFailed.set(true);
+ WaitHelper.getTestWaiter().performNotify();
+ }
+
+ @Override
+ public void onFlowCompleted(RecordsChannel recordsChannel, long fetchEnd, long storeEnd) {
+ numFlowCompleted.incrementAndGet();
+ try {
+ sink.finish(new ExpectSuccessRepositorySessionFinishDelegate(WaitHelper.getTestWaiter()) {
+ @Override
+ public void onFinishSucceeded(RepositorySession session, RepositorySessionBundle bundle) {
+ try {
+ source.finish(new ExpectSuccessRepositorySessionFinishDelegate(WaitHelper.getTestWaiter()) {
+ @Override
+ public void onFinishSucceeded(RepositorySession session, RepositorySessionBundle bundle) {
+ performNotify();
+ }
+ });
+ } catch (InactiveSessionException e) {
+ WaitHelper.getTestWaiter().performNotify(e);
+ }
+ }
+ });
+ } catch (InactiveSessionException e) {
+ WaitHelper.getTestWaiter().performNotify(e);
+ }
+ }
+
+ @Override
+ public void onFlowBeginFailed(RecordsChannel recordsChannel, Exception ex) {
+ flowBeginFailed.set(true);
+ WaitHelper.getTestWaiter().performNotify();
+ }
+ };
+
+ final RecordsChannel rc = new RecordsChannel(source, sink, rcDelegate);
+ WaitHelper.getTestWaiter().performWait(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ rc.beginAndFlow();
+ } catch (InvalidSessionTransitionException e) {
+ WaitHelper.getTestWaiter().performNotify(e);
+ }
+ }
+ });
+ }
+
+ public static final BookmarkRecord[] inbounds = new BookmarkRecord[] {
+ new BookmarkRecord("inboundSucc1", "bookmarks", 1, false),
+ new BookmarkRecord("inboundSucc2", "bookmarks", 1, false),
+ new BookmarkRecord("inboundFail1", "bookmarks", 1, false),
+ new BookmarkRecord("inboundSucc3", "bookmarks", 1, false),
+ new BookmarkRecord("inboundSucc4", "bookmarks", 1, false),
+ new BookmarkRecord("inboundFail2", "bookmarks", 1, false),
+ };
+ public static final BookmarkRecord[] outbounds = new BookmarkRecord[] {
+ new BookmarkRecord("outboundSucc1", "bookmarks", 1, false),
+ new BookmarkRecord("outboundSucc2", "bookmarks", 1, false),
+ new BookmarkRecord("outboundSucc3", "bookmarks", 1, false),
+ new BookmarkRecord("outboundSucc4", "bookmarks", 1, false),
+ new BookmarkRecord("outboundSucc5", "bookmarks", 1, false),
+ new BookmarkRecord("outboundFail6", "bookmarks", 1, false),
+ };
+
+ protected WBORepository empty() {
+ WBORepository repo = new SynchronizerHelpers.TrackingWBORepository();
+ return repo;
+ }
+
+ protected WBORepository full() {
+ WBORepository repo = new SynchronizerHelpers.TrackingWBORepository();
+ for (BookmarkRecord outbound : outbounds) {
+ repo.wbos.put(outbound.guid, outbound);
+ }
+ return repo;
+ }
+
+ protected WBORepository failingFetch() {
+ WBORepository repo = new FailFetchWBORepository();
+ for (BookmarkRecord outbound : outbounds) {
+ repo.wbos.put(outbound.guid, outbound);
+ }
+ return repo;
+ }
+
+ @Test
+ public void testSuccess() throws Exception {
+ WBORepository source = full();
+ WBORepository sink = empty();
+ doFlow(source, sink);
+ assertEquals(1, numFlowCompleted.get());
+ assertEquals(0, numFlowFetchFailed.get());
+ assertEquals(0, numFlowStoreFailed.get());
+ assertEquals(source.wbos, sink.wbos);
+ }
+
+ @Test
+ public void testFetchFail() throws Exception {
+ WBORepository source = failingFetch();
+ WBORepository sink = empty();
+ doFlow(source, sink);
+ assertEquals(1, numFlowCompleted.get());
+ assertTrue(numFlowFetchFailed.get() > 0);
+ assertEquals(0, numFlowStoreFailed.get());
+ assertTrue(sink.wbos.size() < 6);
+ }
+
+ @Test
+ public void testStoreSerialFail() throws Exception {
+ WBORepository source = full();
+ WBORepository sink = new SynchronizerHelpers.SerialFailStoreWBORepository();
+ doFlow(source, sink);
+ assertEquals(1, numFlowCompleted.get());
+ assertEquals(0, numFlowFetchFailed.get());
+ assertEquals(1, numFlowStoreFailed.get());
+ assertEquals(5, sink.wbos.size());
+ }
+
+ @Test
+ public void testStoreBatchesFail() throws Exception {
+ WBORepository source = full();
+ WBORepository sink = new SynchronizerHelpers.BatchFailStoreWBORepository(3);
+ doFlow(source, sink);
+ assertEquals(1, numFlowCompleted.get());
+ assertEquals(0, numFlowFetchFailed.get());
+ assertEquals(3, numFlowStoreFailed.get()); // One batch fails.
+ assertEquals(3, sink.wbos.size()); // One batch succeeds.
+ }
+
+
+ @Test
+ public void testStoreOneBigBatchFail() throws Exception {
+ WBORepository source = full();
+ WBORepository sink = new SynchronizerHelpers.BatchFailStoreWBORepository(50);
+ doFlow(source, sink);
+ assertEquals(1, numFlowCompleted.get());
+ assertEquals(0, numFlowFetchFailed.get());
+ assertEquals(6, numFlowStoreFailed.get()); // One (big) batch fails.
+ assertEquals(0, sink.wbos.size()); // No batches succeed.
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestResetCommands.java
@@ -0,0 +1,152 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.android.sync.test;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.io.IOException;
+import java.util.HashMap;
+
+import org.json.simple.parser.ParseException;
+import org.junit.Before;
+import org.junit.Test;
+import org.mozilla.gecko.background.testhelpers.DefaultGlobalSessionCallback;
+import org.mozilla.gecko.background.testhelpers.MockPrefsGlobalSession;
+import org.mozilla.gecko.background.testhelpers.MockServerSyncStage;
+import org.mozilla.gecko.background.testhelpers.MockSharedPreferences;
+import org.mozilla.gecko.background.testhelpers.WaitHelper;
+import org.mozilla.gecko.sync.CommandProcessor;
+import org.mozilla.gecko.sync.EngineSettings;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.GlobalSession;
+import org.mozilla.gecko.sync.MetaGlobalException;
+import org.mozilla.gecko.sync.NonObjectJSONException;
+import org.mozilla.gecko.sync.SyncConfiguration;
+import org.mozilla.gecko.sync.SyncConfigurationException;
+import org.mozilla.gecko.sync.crypto.CryptoException;
+import org.mozilla.gecko.sync.crypto.KeyBundle;
+import org.mozilla.gecko.sync.delegates.GlobalSessionCallback;
+import org.mozilla.gecko.sync.net.BasicAuthHeaderProvider;
+import org.mozilla.gecko.sync.stage.GlobalSyncStage;
+import org.mozilla.gecko.sync.stage.GlobalSyncStage.Stage;
+
+import android.content.SharedPreferences;
+
+/**
+ * Test that reset commands properly invoke the reset methods on the correct stage.
+ */
+public class TestResetCommands {
+ private static final String TEST_USERNAME = "johndoe";
+ private static final String TEST_PASSWORD = "password";
+ private static final String TEST_SYNC_KEY = "abcdeabcdeabcdeabcdeabcdea";
+
+ public static void performNotify() {
+ WaitHelper.getTestWaiter().performNotify();
+ }
+
+ public static void performNotify(Throwable e) {
+ WaitHelper.getTestWaiter().performNotify(e);
+ }
+
+ public static void performWait(Runnable runnable) {
+ WaitHelper.getTestWaiter().performWait(runnable);
+ }
+
+ @Before
+ public void setUp() {
+ assertTrue(WaitHelper.getTestWaiter().isIdle());
+ }
+
+ @Test
+ public void testHandleResetCommand() throws SyncConfigurationException, IllegalArgumentException, NonObjectJSONException, IOException, ParseException, CryptoException {
+ // Create a global session.
+ // Set up stage mappings for a real stage name (because they're looked up by name
+ // in an enumeration) pointing to our fake stage.
+ // Send a reset command.
+ // Verify that reset is called on our stage.
+
+ class Result {
+ public boolean called = false;
+ }
+
+ final Result yes = new Result();
+ final Result no = new Result();
+ final GlobalSessionCallback callback = createGlobalSessionCallback();
+
+ // So we can poke at stages separately.
+ final HashMap<Stage, GlobalSyncStage> stagesToRun = new HashMap<Stage, GlobalSyncStage>();
+
+ // Side-effect: modifies global command processor.
+ final SharedPreferences prefs = new MockSharedPreferences();
+ final SyncConfiguration config = new SyncConfiguration(TEST_USERNAME, new BasicAuthHeaderProvider(TEST_USERNAME, TEST_PASSWORD), prefs);
+ config.syncKeyBundle = new KeyBundle(TEST_USERNAME, TEST_SYNC_KEY);
+ final GlobalSession session = new MockPrefsGlobalSession(config, callback, null, null) {
+ @Override
+ public boolean isEngineRemotelyEnabled(String engineName,
+ EngineSettings engineSettings)
+ throws MetaGlobalException {
+ return true;
+ }
+
+ @Override
+ public void advance() {
+ // So we don't proceed and run other stages.
+ }
+
+ @Override
+ public void prepareStages() {
+ this.stages = stagesToRun;
+ }
+ };
+
+ final MockServerSyncStage stageGetsReset = new MockServerSyncStage() {
+ @Override
+ public void resetLocal() {
+ yes.called = true;
+ }
+ };
+
+ final MockServerSyncStage stageNotReset = new MockServerSyncStage() {
+ @Override
+ public void resetLocal() {
+ no.called = true;
+ }
+ };
+
+ stagesToRun.put(Stage.syncBookmarks, stageGetsReset);
+ stagesToRun.put(Stage.syncHistory, stageNotReset);
+
+ final String resetBookmarks = "{\"args\":[\"bookmarks\"],\"command\":\"resetEngine\"}";
+ ExtendedJSONObject unparsedCommand = new ExtendedJSONObject(resetBookmarks);
+ CommandProcessor processor = CommandProcessor.getProcessor();
+ processor.processCommand(session, unparsedCommand);
+
+ assertTrue(yes.called);
+ assertFalse(no.called);
+ }
+
+ public void testHandleWipeCommand() {
+ // TODO
+ }
+
+ private static GlobalSessionCallback createGlobalSessionCallback() {
+ return new DefaultGlobalSessionCallback() {
+
+ @Override
+ public void handleAborted(GlobalSession globalSession, String reason) {
+ performNotify(new Exception("Aborted"));
+ }
+
+ @Override
+ public void handleError(GlobalSession globalSession, Exception ex) {
+ performNotify(ex);
+ }
+
+ @Override
+ public void handleSuccess(GlobalSession globalSession) {
+ }
+ };
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestServer11RepositorySession.java
@@ -0,0 +1,272 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.android.sync.test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.junit.Test;
+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.MockRecord;
+import org.mozilla.gecko.background.testhelpers.WaitHelper;
+import org.mozilla.gecko.sync.InfoCollections;
+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.SyncStorageRecordRequest;
+import org.mozilla.gecko.sync.net.SyncStorageResponse;
+import org.mozilla.gecko.sync.repositories.FetchFailedException;
+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.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.repositories.domain.Record;
+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 ch.boye.httpclientandroidlib.HttpEntity;
+
+public class TestServer11RepositorySession {
+
+ public class POSTMockServer extends MockServer {
+ @Override
+ public void handle(Request request, Response response) {
+ try {
+ String content = request.getContent();
+ System.out.println("Content:" + content);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ ContentType contentType = request.getContentType();
+ 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_REQUEST_URL = LOCAL_BASE_URL + "storage/bookmarks";
+ 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();
+
+ // 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 MockServer11RepositorySession extends Server11RepositorySession {
+ public MockServer11RepositorySession(Repository repository) {
+ super(repository);
+ }
+
+ public RecordUploadRunnable getRecordUploadRunnable() {
+ // TODO: implement upload delegate in the class, too!
+ return new RecordUploadRunnable(null, recordsBuffer, recordGuidsBuffer, byteCount);
+ }
+
+ public void enqueueRecord(Record r) {
+ super.enqueue(r);
+ }
+
+ public HttpEntity getEntity() {
+ return this.getRecordUploadRunnable().getBodyEntity();
+ }
+ }
+
+ public class TestSyncStorageRequestDelegate extends
+ BaseTestStorageRequestDelegate {
+ public TestSyncStorageRequestDelegate(String username, String password) {
+ super(username, password);
+ }
+
+ @Override
+ public void handleRequestSuccess(SyncStorageResponse res) {
+ assertTrue(res.wasSuccessful());
+ assertTrue(res.httpResponse().containsHeader("X-Weave-Timestamp"));
+ BaseResource.consumeEntity(res);
+ data.stopHTTPServer();
+ }
+ }
+
+ @Test
+ public void test() throws URISyntaxException {
+
+ BaseResource.rewriteLocalhost = false;
+ data.startHTTPServer(new POSTMockServer());
+
+ MockServer11RepositorySession session = new MockServer11RepositorySession(
+ null);
+ session.enqueueRecord(new MockRecord(Utils.generateGuid(), null, 0, false));
+ session.enqueueRecord(new MockRecord(Utils.generateGuid(), null, 0, false));
+
+ URI uri = new URI(LOCAL_REQUEST_URL);
+ SyncStorageRecordRequest r = new SyncStorageRecordRequest(uri);
+ TestSyncStorageRequestDelegate delegate = new TestSyncStorageRequestDelegate(TEST_USERNAME, TEST_PASSWORD);
+ r.delegate = delegate;
+ r.post(session.getEntity());
+ }
+
+ @SuppressWarnings("static-method")
+ protected TrackingWBORepository getLocal(int numRecords) {
+ final TrackingWBORepository local = new TrackingWBORepository();
+ for (int i = 0; i < numRecords; i++) {
+ BookmarkRecord outbound = new BookmarkRecord("outboundFail" + i, "bookmarks", 1, false);
+ local.wbos.put(outbound.guid, outbound);
+ }
+ 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);
+ 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;
+
+ data.startHTTPServer(server);
+ try {
+ Exception e = TestServerLocalSynchronizer.doSynchronize(synchronizer);
+ return e;
+ } finally {
+ data.stopHTTPServer();
+ }
+ }
+
+ protected String getCollectionURL(String collection) {
+ return LOCAL_BASE_URL + "/storage/" + collection;
+ }
+
+ @Test
+ public void testFetchFailure() throws Exception {
+ MockServer server = new MockServer(404, "error");
+ Exception e = doSynchronize(server);
+ assertNotNull(e);
+ assertEquals(FetchFailedException.class, e.getClass());
+ }
+
+ @Test
+ public void testStorePostSuccessWithFailingRecords() throws Exception {
+ MockServer server = new MockServer(200, "{ modified: \" + " + Utils.millisecondsToDecimalSeconds(System.currentTimeMillis()) + ", " +
+ "success: []," +
+ "failed: { outboundFail2: [] } }");
+ Exception e = doSynchronize(server);
+ assertNotNull(e);
+ assertEquals(StoreFailedException.class, e.getClass());
+ }
+
+ @Test
+ public void testStorePostFailure() throws Exception {
+ MockServer server = new MockServer() {
+ @Override
+ public void handle(Request request, Response response) {
+ if (request.getMethod().equals("POST")) {
+ this.handle(request, response, 404, "missing");
+ }
+ this.handle(request, response, 200, "success");
+ return;
+ }
+ };
+
+ Exception e = doSynchronize(server);
+ assertNotNull(e);
+ assertEquals(StoreFailedException.class, e.getClass());
+ }
+
+ @Test
+ public void testConstraints() throws Exception {
+ MockServer server = new MockServer() {
+ @Override
+ public void handle(Request request, Response response) {
+ if (request.getMethod().equals("GET")) {
+ if (request.getPath().getPath().endsWith("/info/collection_counts")) {
+ this.handle(request, response, 200, "{\"bookmarks\": 5001}");
+ }
+ }
+ this.handle(request, response, 400, "NOOOO");
+ }
+ };
+ final JSONRecordFetcher countsFetcher = new JSONRecordFetcher(LOCAL_COUNTS_URL, getAuthHeaderProvider());
+ String collection = "bookmarks";
+ final SafeConstrainedServer11Repository remote = new SafeConstrainedServer11Repository(collection,
+ getCollectionURL(collection),
+ getAuthHeaderProvider(),
+ infoCollections,
+ 5000, "sortindex", countsFetcher);
+
+ data.startHTTPServer(server);
+ final AtomicBoolean out = new AtomicBoolean(false);
+
+ // Verify that shouldSkip returns true due to a fetch of too large counts,
+ // rather than due to a timeout failure waiting to fetch counts.
+ try {
+ WaitHelper.getTestWaiter().performWait(
+ SHORT_TIMEOUT,
+ new Runnable() {
+ @Override
+ public void run() {
+ remote.createSession(new RepositorySessionCreationDelegate() {
+ @Override
+ public void onSessionCreated(RepositorySession session) {
+ out.set(session.shouldSkip());
+ WaitHelper.getTestWaiter().performNotify();
+ }
+
+ @Override
+ public void onSessionCreateFailed(Exception ex) {
+ WaitHelper.getTestWaiter().performNotify(ex);
+ }
+
+ @Override
+ public RepositorySessionCreationDelegate deferredCreationDelegate() {
+ return this;
+ }
+ }, null);
+ }
+ });
+ assertTrue(out.get());
+ } finally {
+ data.stopHTTPServer();
+ }
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestServerLocalSynchronizer.java
@@ -0,0 +1,234 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.android.sync.test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import java.util.ArrayList;
+
+import org.junit.Test;
+import org.mozilla.android.sync.test.SynchronizerHelpers.BatchFailStoreWBORepository;
+import org.mozilla.android.sync.test.SynchronizerHelpers.BeginErrorWBORepository;
+import org.mozilla.android.sync.test.SynchronizerHelpers.BeginFailedException;
+import org.mozilla.android.sync.test.SynchronizerHelpers.FailFetchWBORepository;
+import org.mozilla.android.sync.test.SynchronizerHelpers.FinishErrorWBORepository;
+import org.mozilla.android.sync.test.SynchronizerHelpers.FinishFailedException;
+import org.mozilla.android.sync.test.SynchronizerHelpers.SerialFailStoreWBORepository;
+import org.mozilla.android.sync.test.SynchronizerHelpers.TrackingWBORepository;
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.background.testhelpers.WBORepository;
+import org.mozilla.gecko.background.testhelpers.WaitHelper;
+import org.mozilla.gecko.sync.repositories.FetchFailedException;
+import org.mozilla.gecko.sync.repositories.StoreFailedException;
+import org.mozilla.gecko.sync.repositories.domain.BookmarkRecord;
+import org.mozilla.gecko.sync.synchronizer.ServerLocalSynchronizer;
+import org.mozilla.gecko.sync.synchronizer.Synchronizer;
+import org.mozilla.gecko.sync.synchronizer.SynchronizerDelegate;
+
+public class TestServerLocalSynchronizer {
+ public static final String LOG_TAG = "TestServLocSync";
+
+ protected Synchronizer getSynchronizer(WBORepository remote, WBORepository local) {
+ BookmarkRecord[] inbounds = new BookmarkRecord[] {
+ new BookmarkRecord("inboundSucc1", "bookmarks", 1, false),
+ new BookmarkRecord("inboundSucc2", "bookmarks", 1, false),
+ new BookmarkRecord("inboundFail1", "bookmarks", 1, false),
+ new BookmarkRecord("inboundSucc3", "bookmarks", 1, false),
+ new BookmarkRecord("inboundFail2", "bookmarks", 1, false),
+ new BookmarkRecord("inboundFail3", "bookmarks", 1, false),
+ };
+ BookmarkRecord[] outbounds = new BookmarkRecord[] {
+ new BookmarkRecord("outboundFail1", "bookmarks", 1, false),
+ new BookmarkRecord("outboundFail2", "bookmarks", 1, false),
+ new BookmarkRecord("outboundFail3", "bookmarks", 1, false),
+ new BookmarkRecord("outboundFail4", "bookmarks", 1, false),
+ new BookmarkRecord("outboundFail5", "bookmarks", 1, false),
+ new BookmarkRecord("outboundFail6", "bookmarks", 1, false),
+ };
+ for (BookmarkRecord inbound : inbounds) {
+ remote.wbos.put(inbound.guid, inbound);
+ }
+ for (BookmarkRecord outbound : outbounds) {
+ local.wbos.put(outbound.guid, outbound);
+ }
+
+ final Synchronizer synchronizer = new ServerLocalSynchronizer();
+ synchronizer.repositoryA = remote;
+ synchronizer.repositoryB = local;
+ return synchronizer;
+ }
+
+ protected static Exception doSynchronize(final Synchronizer synchronizer) {
+ final ArrayList<Exception> a = new ArrayList<Exception>();
+
+ WaitHelper.getTestWaiter().performWait(new Runnable() {
+ @Override
+ public void run() {
+ synchronizer.synchronize(null, new SynchronizerDelegate() {
+ @Override
+ public void onSynchronized(Synchronizer synchronizer) {
+ Logger.trace(LOG_TAG, "Got onSynchronized.");
+ a.add(null);
+ WaitHelper.getTestWaiter().performNotify();
+ }
+
+ @Override
+ public void onSynchronizeFailed(Synchronizer synchronizer, Exception lastException, String reason) {
+ Logger.trace(LOG_TAG, "Got onSynchronizedFailed.");
+ a.add(lastException);
+ WaitHelper.getTestWaiter().performNotify();
+ }
+ });
+ }
+ });
+
+ assertEquals(1, a.size()); // Should not be called multiple times!
+ return a.get(0);
+ }
+
+ @Test
+ public void testNoErrors() {
+ WBORepository remote = new TrackingWBORepository();
+ WBORepository local = new TrackingWBORepository();
+
+ Synchronizer synchronizer = getSynchronizer(remote, local);
+ assertNull(doSynchronize(synchronizer));
+
+ assertEquals(12, local.wbos.size());
+ assertEquals(12, remote.wbos.size());
+ }
+
+ @Test
+ public void testLocalFetchErrors() {
+ WBORepository remote = new TrackingWBORepository();
+ WBORepository local = new FailFetchWBORepository();
+
+ Synchronizer synchronizer = getSynchronizer(remote, local);
+ Exception e = doSynchronize(synchronizer);
+ assertNotNull(e);
+ assertEquals(FetchFailedException.class, e.getClass());
+
+ // Neither session gets finished successfully, so all records are dropped.
+ assertEquals(6, local.wbos.size());
+ assertEquals(6, remote.wbos.size());
+ }
+
+ @Test
+ public void testRemoteFetchErrors() {
+ WBORepository remote = new FailFetchWBORepository();
+ WBORepository local = new TrackingWBORepository();
+
+ Synchronizer synchronizer = getSynchronizer(remote, local);
+ Exception e = doSynchronize(synchronizer);
+ assertNotNull(e);
+ assertEquals(FetchFailedException.class, e.getClass());
+
+ // Neither session gets finished successfully, so all records are dropped.
+ assertEquals(6, local.wbos.size());
+ assertEquals(6, remote.wbos.size());
+ }
+
+ @Test
+ public void testLocalSerialStoreErrorsAreIgnored() {
+ WBORepository remote = new TrackingWBORepository();
+ WBORepository local = new SerialFailStoreWBORepository();
+
+ Synchronizer synchronizer = getSynchronizer(remote, local);
+ assertNull(doSynchronize(synchronizer));
+
+ assertEquals(9, local.wbos.size());
+ assertEquals(12, remote.wbos.size());
+ }
+
+ @Test
+ public void testLocalBatchStoreErrorsAreIgnored() {
+ final int BATCH_SIZE = 3;
+
+ Synchronizer synchronizer = getSynchronizer(new TrackingWBORepository(), new BatchFailStoreWBORepository(BATCH_SIZE));
+
+ Exception e = doSynchronize(synchronizer);
+ assertNull(e);
+ }
+
+ @Test
+ public void testRemoteSerialStoreErrorsAreNotIgnored() throws Exception {
+ Synchronizer synchronizer = getSynchronizer(new SerialFailStoreWBORepository(), new TrackingWBORepository()); // Tracking so we don't send incoming records back.
+
+ Exception e = doSynchronize(synchronizer);
+ assertNotNull(e);
+ assertEquals(StoreFailedException.class, e.getClass());
+ }
+
+ @Test
+ public void testRemoteBatchStoreErrorsAreNotIgnoredManyBatches() throws Exception {
+ final int BATCH_SIZE = 3;
+
+ Synchronizer synchronizer = getSynchronizer(new BatchFailStoreWBORepository(BATCH_SIZE), new TrackingWBORepository()); // Tracking so we don't send incoming records back.
+
+ Exception e = doSynchronize(synchronizer);
+ assertNotNull(e);
+ assertEquals(StoreFailedException.class, e.getClass());
+ }
+
+ @Test
+ public void testRemoteBatchStoreErrorsAreNotIgnoredOneBigBatch() throws Exception {
+ final int BATCH_SIZE = 20;
+
+ Synchronizer synchronizer = getSynchronizer(new BatchFailStoreWBORepository(BATCH_SIZE), new TrackingWBORepository()); // Tracking so we don't send incoming records back.
+
+ Exception e = doSynchronize(synchronizer);
+ assertNotNull(e);
+ assertEquals(StoreFailedException.class, e.getClass());
+ }
+
+ @Test
+ public void testSessionRemoteBeginError() {
+ Synchronizer synchronizer = getSynchronizer(new BeginErrorWBORepository(), new TrackingWBORepository());
+ Exception e = doSynchronize(synchronizer);
+ assertNotNull(e);
+ assertEquals(BeginFailedException.class, e.getClass());
+ }
+
+ @Test
+ public void testSessionLocalBeginError() {
+ Synchronizer synchronizer = getSynchronizer(new TrackingWBORepository(), new BeginErrorWBORepository());
+ Exception e = doSynchronize(synchronizer);
+ assertNotNull(e);
+ assertEquals(BeginFailedException.class, e.getClass());
+ }
+
+ @Test
+ public void testSessionRemoteFinishError() {
+ Synchronizer synchronizer = getSynchronizer(new FinishErrorWBORepository(), new TrackingWBORepository());
+ Exception e = doSynchronize(synchronizer);
+ assertNotNull(e);
+ assertEquals(FinishFailedException.class, e.getClass());
+ }
+
+ @Test
+ public void testSessionLocalFinishError() {
+ Synchronizer synchronizer = getSynchronizer(new TrackingWBORepository(), new FinishErrorWBORepository());
+ Exception e = doSynchronize(synchronizer);
+ assertNotNull(e);
+ assertEquals(FinishFailedException.class, e.getClass());
+ }
+
+ @Test
+ public void testSessionBothBeginError() {
+ Synchronizer synchronizer = getSynchronizer(new BeginErrorWBORepository(), new BeginErrorWBORepository());
+ Exception e = doSynchronize(synchronizer);
+ assertNotNull(e);
+ assertEquals(BeginFailedException.class, e.getClass());
+ }
+
+ @Test
+ public void testSessionBothFinishError() {
+ Synchronizer synchronizer = getSynchronizer(new FinishErrorWBORepository(), new FinishErrorWBORepository());
+ Exception e = doSynchronize(synchronizer);
+ assertNotNull(e);
+ assertEquals(FinishFailedException.class, e.getClass());
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestSyncConfiguration.java
@@ -0,0 +1,36 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.android.sync.test;
+
+import java.net.URI;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.mozilla.gecko.background.testhelpers.MockSharedPreferences;
+import org.mozilla.gecko.sync.Sync11Configuration;
+import org.mozilla.gecko.sync.SyncConfiguration;
+
+public class TestSyncConfiguration {
+ @Test
+ public void testURLs() throws Exception {
+ final MockSharedPreferences prefs = new MockSharedPreferences();
+
+ // N.B., the username isn't used in the cluster path.
+ SyncConfiguration fxaConfig = new SyncConfiguration("username", null, prefs);
+ fxaConfig.clusterURL = new URI("http://db1.oldsync.dev.lcip.org/1.1/174");
+ Assert.assertEquals("http://db1.oldsync.dev.lcip.org/1.1/174/info/collections", fxaConfig.infoCollectionsURL());
+ Assert.assertEquals("http://db1.oldsync.dev.lcip.org/1.1/174/info/collection_counts", fxaConfig.infoCollectionCountsURL());
+ Assert.assertEquals("http://db1.oldsync.dev.lcip.org/1.1/174/storage/meta/global", fxaConfig.metaURL());
+ Assert.assertEquals("http://db1.oldsync.dev.lcip.org/1.1/174/storage", fxaConfig.storageURL());
+ Assert.assertEquals("http://db1.oldsync.dev.lcip.org/1.1/174/storage/collection", fxaConfig.collectionURI("collection").toASCIIString());
+
+ SyncConfiguration oldConfig = new Sync11Configuration("username", null, prefs);
+ oldConfig.clusterURL = new URI("https://db.com/internal/");
+ Assert.assertEquals("https://db.com/internal/1.1/username/info/collections", oldConfig.infoCollectionsURL());
+ Assert.assertEquals("https://db.com/internal/1.1/username/info/collection_counts", oldConfig.infoCollectionCountsURL());
+ Assert.assertEquals("https://db.com/internal/1.1/username/storage/meta/global", oldConfig.metaURL());
+ Assert.assertEquals("https://db.com/internal/1.1/username/storage", oldConfig.storageURL());
+ Assert.assertEquals("https://db.com/internal/1.1/username/storage/collection", oldConfig.collectionURI("collection").toASCIIString());
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestSyncKeyVerification.java
@@ -0,0 +1,80 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.android.sync.test;
+
+import static org.junit.Assert.fail;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mozilla.gecko.sync.setup.InvalidSyncKeyException;
+import org.mozilla.gecko.sync.setup.activities.ActivityUtils;
+
+public class TestSyncKeyVerification {
+
+ private int[] mutateIndices;
+ private final String validBasicKey = "abcdefghijkmnpqrstuvwxyz23"; // 26 char, valid characters.
+ char[] invalidChars = new char[] { '1', 'l', 'o', '0' };
+
+ @Before
+ public void setUp() {
+ // Generate indicies to mutate.
+ mutateIndices = generateMutationArray();
+ }
+
+ @Test
+ public void testValidKey() {
+ try {
+ ActivityUtils.validateSyncKey(validBasicKey);
+ } catch (InvalidSyncKeyException e) {
+ fail("Threw unexpected InvalidSyncKeyException.");
+ }
+ }
+
+ @Test
+ public void testHyphenationSuccess() {
+ StringBuilder sb = new StringBuilder();
+ int prev = 0;
+ for (int i : mutateIndices) {
+ sb.append(validBasicKey.substring(prev, i));
+ sb.append("-");
+ prev = i;
+ }
+ sb.append(validBasicKey.substring(prev));
+ String hString = sb.toString();
+ try {
+ ActivityUtils.validateSyncKey(hString);
+ } catch (InvalidSyncKeyException e) {
+ fail("Failed validation with hypenation.");
+ }
+ }
+
+ @Test
+ public void testCapitalizationSuccess() {
+
+ char[] mutatedKey = validBasicKey.toCharArray();
+ for (int i : mutateIndices) {
+ mutatedKey[i] = Character.toUpperCase(validBasicKey.charAt(i));
+ }
+ String mKey = new String(mutatedKey);
+ try {
+ ActivityUtils.validateSyncKey(mKey);
+ } catch (InvalidSyncKeyException e) {
+ fail("Failed validation with uppercasing.");
+ }
+ }
+
+ @Test (expected = InvalidSyncKeyException.class)
+ public void testInvalidCharFailure() throws InvalidSyncKeyException {
+ char[] mutatedKey = validBasicKey.toCharArray();
+ for (int i : mutateIndices) {
+ mutatedKey[i] = invalidChars[i % invalidChars.length];
+ }
+ ActivityUtils.validateSyncKey(mutatedKey.toString());
+ }
+
+ private int[] generateMutationArray() {
+ // Hardcoded; change if desired?
+ return new int[] { 2, 4, 5, 9, 16 };
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestSynchronizer.java
@@ -0,0 +1,396 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.android.sync.test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.util.Date;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mozilla.android.sync.test.SynchronizerHelpers.TrackingWBORepository;
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.background.testhelpers.WBORepository;
+import org.mozilla.gecko.background.testhelpers.WaitHelper;
+import org.mozilla.gecko.sync.repositories.RepositorySessionBundle;
+import org.mozilla.gecko.sync.repositories.domain.BookmarkRecord;
+import org.mozilla.gecko.sync.synchronizer.Synchronizer;
+import org.mozilla.gecko.sync.synchronizer.SynchronizerDelegate;
+import org.mozilla.gecko.sync.synchronizer.SynchronizerSession;
+import org.mozilla.gecko.sync.synchronizer.SynchronizerSessionDelegate;
+
+import android.content.Context;
+
+public class TestSynchronizer {
+ public static final String LOG_TAG = "TestSynchronizer";
+
+ public static void assertInRangeInclusive(long earliest, long value, long latest) {
+ assertTrue(earliest <= value);
+ assertTrue(latest >= value);
+ }
+
+ public static void recordEquals(BookmarkRecord r, String guid, long lastModified, boolean deleted, String collection) {
+ assertEquals(r.guid, guid);
+ assertEquals(r.lastModified, lastModified);
+ assertEquals(r.deleted, deleted);
+ assertEquals(r.collection, collection);
+ }
+
+ public static void recordEquals(BookmarkRecord a, BookmarkRecord b) {
+ assertEquals(a.guid, b.guid);
+ assertEquals(a.lastModified, b.lastModified);
+ assertEquals(a.deleted, b.deleted);
+ assertEquals(a.collection, b.collection);
+ }
+
+ @Before
+ public void setUp() {
+ WaitHelper.resetTestWaiter();
+ }
+
+ @After
+ public void tearDown() {
+ WaitHelper.resetTestWaiter();
+ }
+
+ @Test
+ public void testSynchronizerSession() {
+ final Context context = null;
+ final WBORepository repoA = new TrackingWBORepository();
+ final WBORepository repoB = new TrackingWBORepository();
+
+ final String collection = "bookmarks";
+ final boolean deleted = false;
+ final String guidA = "abcdabcdabcd";
+ final String guidB = "ffffffffffff";
+ final String guidC = "xxxxxxxxxxxx";
+ final long lastModifiedA = 312345;
+ final long lastModifiedB = 412340;
+ final long lastModifiedC = 412345;
+ BookmarkRecord bookmarkRecordA = new BookmarkRecord(guidA, collection, lastModifiedA, deleted);
+ BookmarkRecord bookmarkRecordB = new BookmarkRecord(guidB, collection, lastModifiedB, deleted);
+ BookmarkRecord bookmarkRecordC = new BookmarkRecord(guidC, collection, lastModifiedC, deleted);
+
+ repoA.wbos.put(guidA, bookmarkRecordA);
+ repoB.wbos.put(guidB, bookmarkRecordB);
+ repoB.wbos.put(guidC, bookmarkRecordC);
+ Synchronizer synchronizer = new Synchronizer();
+ synchronizer.repositoryA = repoA;
+ synchronizer.repositoryB = repoB;
+ final SynchronizerSession syncSession = new SynchronizerSession(synchronizer, new SynchronizerSessionDelegate() {
+
+ @Override
+ public void onInitialized(SynchronizerSession session) {
+ assertFalse(repoA.wbos.containsKey(guidB));
+ assertFalse(repoA.wbos.containsKey(guidC));
+ assertFalse(repoB.wbos.containsKey(guidA));
+ assertTrue(repoA.wbos.containsKey(guidA));
+ assertTrue(repoB.wbos.containsKey(guidB));
+ assertTrue(repoB.wbos.containsKey(guidC));
+ session.synchronize();
+ }
+
+ @Override
+ public void onSynchronized(SynchronizerSession session) {
+ try {
+ assertEquals(1, session.getInboundCount());
+ assertEquals(2, session.getOutboundCount());
+ WaitHelper.getTestWaiter().performNotify();
+ } catch (Throwable e) {
+ WaitHelper.getTestWaiter().performNotify(e);
+ }
+ }
+
+ @Override
+ public void onSynchronizeFailed(SynchronizerSession session,
+ Exception lastException, String reason) {
+ WaitHelper.getTestWaiter().performNotify(lastException);
+ }
+
+ @Override
+ public void onSynchronizeSkipped(SynchronizerSession synchronizerSession) {
+ WaitHelper.getTestWaiter().performNotify(new RuntimeException());
+ }
+ });
+
+ WaitHelper.getTestWaiter().performWait(new Runnable() {
+ @Override
+ public void run() {
+ syncSession.init(context, new RepositorySessionBundle(0), new RepositorySessionBundle(0));
+ }
+ });
+
+ // Verify contents.
+ assertTrue(repoA.wbos.containsKey(guidA));
+ assertTrue(repoA.wbos.containsKey(guidB));
+ assertTrue(repoA.wbos.containsKey(guidC));
+ assertTrue(repoB.wbos.containsKey(guidA));
+ assertTrue(repoB.wbos.containsKey(guidB));
+ assertTrue(repoB.wbos.containsKey(guidC));
+ BookmarkRecord aa = (BookmarkRecord) repoA.wbos.get(guidA);
+ BookmarkRecord ab = (BookmarkRecord) repoA.wbos.get(guidB);
+ BookmarkRecord ac = (BookmarkRecord) repoA.wbos.get(guidC);
+ BookmarkRecord ba = (BookmarkRecord) repoB.wbos.get(guidA);
+ BookmarkRecord bb = (BookmarkRecord) repoB.wbos.get(guidB);
+ BookmarkRecord bc = (BookmarkRecord) repoB.wbos.get(guidC);
+ recordEquals(aa, guidA, lastModifiedA, deleted, collection);
+ recordEquals(ab, guidB, lastModifiedB, deleted, collection);
+ recordEquals(ac, guidC, lastModifiedC, deleted, collection);
+ recordEquals(ba, guidA, lastModifiedA, deleted, collection);
+ recordEquals(bb, guidB, lastModifiedB, deleted, collection);
+ recordEquals(bc, guidC, lastModifiedC, deleted, collection);
+ recordEquals(aa, ba);
+ recordEquals(ab, bb);
+ recordEquals(ac, bc);
+ }
+
+ public abstract class SuccessfulSynchronizerDelegate implements SynchronizerDelegate {
+ public long syncAOne = 0;
+ public long syncBOne = 0;
+
+ @Override
+ public void onSynchronizeFailed(Synchronizer synchronizer,
+ Exception lastException, String reason) {
+ fail("Should not fail.");
+ }
+ }
+
+ @Test
+ public void testSynchronizerPersists() {
+ final Object monitor = new Object();
+ final long earliest = new Date().getTime();
+
+ Context context = null;
+ final WBORepository repoA = new WBORepository();
+ final WBORepository repoB = new WBORepository();
+ Synchronizer synchronizer = new Synchronizer();
+ synchronizer.bundleA = new RepositorySessionBundle(0);
+ synchronizer.bundleB = new RepositorySessionBundle(0);
+ synchronizer.repositoryA = repoA;
+ synchronizer.repositoryB = repoB;
+
+ final SuccessfulSynchronizerDelegate delegateOne = new SuccessfulSynchronizerDelegate() {
+ @Override
+ public void onSynchronized(Synchronizer synchronizer) {
+ Logger.trace(LOG_TAG, "onSynchronized. Success!");
+ syncAOne = synchronizer.bundleA.getTimestamp();
+ syncBOne = synchronizer.bundleB.getTimestamp();
+ synchronized (monitor) {
+ monitor.notify();
+ }
+ }
+ };
+ final SuccessfulSynchronizerDelegate delegateTwo = new SuccessfulSynchronizerDelegate() {
+ @Override
+ public void onSynchronized(Synchronizer synchronizer) {
+ Logger.trace(LOG_TAG, "onSynchronized. Success!");
+ syncAOne = synchronizer.bundleA.getTimestamp();
+ syncBOne = synchronizer.bundleB.getTimestamp();
+ synchronized (monitor) {
+ monitor.notify();
+ }
+ }
+ };
+ synchronized (monitor) {
+ synchronizer.synchronize(context, delegateOne);
+ try {
+ monitor.wait();
+ } catch (InterruptedException e) {
+ fail("Interrupted.");
+ }
+ }
+ long now = new Date().getTime();
+ Logger.trace(LOG_TAG, "Earliest is " + earliest);
+ Logger.trace(LOG_TAG, "syncAOne is " + delegateOne.syncAOne);
+ Logger.trace(LOG_TAG, "syncBOne is " + delegateOne.syncBOne);
+ Logger.trace(LOG_TAG, "Now: " + now);
+ assertInRangeInclusive(earliest, delegateOne.syncAOne, now);
+ assertInRangeInclusive(earliest, delegateOne.syncBOne, now);
+ try {
+ Thread.sleep(10);
+ } catch (InterruptedException e) {
+ fail("Thread interrupted!");
+ }
+ synchronized (monitor) {
+ synchronizer.synchronize(context, delegateTwo);
+ try {
+ monitor.wait();
+ } catch (InterruptedException e) {
+ fail("Interrupted.");
+ }
+ }
+ now = new Date().getTime();
+ Logger.trace(LOG_TAG, "Earliest is " + earliest);
+ Logger.trace(LOG_TAG, "syncAOne is " + delegateTwo.syncAOne);
+ Logger.trace(LOG_TAG, "syncBOne is " + delegateTwo.syncBOne);
+ Logger.trace(LOG_TAG, "Now: " + now);
+ assertInRangeInclusive(earliest, delegateTwo.syncAOne, now);
+ assertInRangeInclusive(earliest, delegateTwo.syncBOne, now);
+ assertTrue(delegateTwo.syncAOne > delegateOne.syncAOne);
+ assertTrue(delegateTwo.syncBOne > delegateOne.syncBOne);
+ Logger.trace(LOG_TAG, "Reached end of test.");
+ }
+
+ private Synchronizer getTestSynchronizer(long tsA, long tsB) {
+ WBORepository repoA = new TrackingWBORepository();
+ WBORepository repoB = new TrackingWBORepository();
+ Synchronizer synchronizer = new Synchronizer();
+ synchronizer.bundleA = new RepositorySessionBundle(tsA);
+ synchronizer.bundleB = new RepositorySessionBundle(tsB);
+ synchronizer.repositoryA = repoA;
+ synchronizer.repositoryB = repoB;
+ return synchronizer;
+ }
+
+ /**
+ * Let's put data in two repos and synchronize them with last sync
+ * timestamps later than all of the records. Verify that no records
+ * are exchanged.
+ */
+ @Test
+ public void testSynchronizerFakeTimestamps() {
+ final Context context = null;
+
+ final String collection = "bookmarks";
+ final boolean deleted = false;
+ final String guidA = "abcdabcdabcd";
+ final String guidB = "ffffffffffff";
+ final long lastModifiedA = 312345;
+ final long lastModifiedB = 412345;
+ BookmarkRecord bookmarkRecordA = new BookmarkRecord(guidA, collection, lastModifiedA, deleted);
+ BookmarkRecord bookmarkRecordB = new BookmarkRecord(guidB, collection, lastModifiedB, deleted);
+
+ final Synchronizer synchronizer = getTestSynchronizer(lastModifiedA + 10, lastModifiedB + 10);
+ final WBORepository repoA = (WBORepository) synchronizer.repositoryA;
+ final WBORepository repoB = (WBORepository) synchronizer.repositoryB;
+
+ repoA.wbos.put(guidA, bookmarkRecordA);
+ repoB.wbos.put(guidB, bookmarkRecordB);
+
+ WaitHelper.getTestWaiter().performWait(new Runnable() {
+ @Override
+ public void run() {
+ synchronizer.synchronize(context, new SynchronizerDelegate() {
+
+ @Override
+ public void onSynchronized(Synchronizer synchronizer) {
+ try {
+ // No records get sent either way.
+ final SynchronizerSession synchronizerSession = synchronizer.getSynchronizerSession();
+ assertNotNull(synchronizerSession);
+ assertEquals(0, synchronizerSession.getInboundCount());
+ assertEquals(0, synchronizerSession.getOutboundCount());
+ WaitHelper.getTestWaiter().performNotify();
+ } catch (Throwable e) {
+ WaitHelper.getTestWaiter().performNotify(e);
+ }
+ }
+
+ @Override
+ public void onSynchronizeFailed(Synchronizer synchronizer,
+ Exception lastException, String reason) {
+ WaitHelper.getTestWaiter().performNotify(lastException);
+ }
+ });
+ }
+ });
+
+ // Verify contents.
+ assertTrue(repoA.wbos.containsKey(guidA));
+ assertTrue(repoB.wbos.containsKey(guidB));
+ assertFalse(repoB.wbos.containsKey(guidA));
+ assertFalse(repoA.wbos.containsKey(guidB));
+ BookmarkRecord aa = (BookmarkRecord) repoA.wbos.get(guidA);
+ BookmarkRecord ab = (BookmarkRecord) repoA.wbos.get(guidB);
+ BookmarkRecord ba = (BookmarkRecord) repoB.wbos.get(guidA);
+ BookmarkRecord bb = (BookmarkRecord) repoB.wbos.get(guidB);
+ assertNull(ab);
+ assertNull(ba);
+ recordEquals(aa, guidA, lastModifiedA, deleted, collection);
+ recordEquals(bb, guidB, lastModifiedB, deleted, collection);
+ }
+
+
+ @Test
+ public void testSynchronizer() {
+ final Context context = null;
+
+ final String collection = "bookmarks";
+ final boolean deleted = false;
+ final String guidA = "abcdabcdabcd";
+ final String guidB = "ffffffffffff";
+ final String guidC = "gggggggggggg";
+ final long lastModifiedA = 312345;
+ final long lastModifiedB = 412340;
+ final long lastModifiedC = 412345;
+ BookmarkRecord bookmarkRecordA = new BookmarkRecord(guidA, collection, lastModifiedA, deleted);
+ BookmarkRecord bookmarkRecordB = new BookmarkRecord(guidB, collection, lastModifiedB, deleted);
+ BookmarkRecord bookmarkRecordC = new BookmarkRecord(guidC, collection, lastModifiedC, deleted);
+
+ final Synchronizer synchronizer = getTestSynchronizer(0, 0);
+ final WBORepository repoA = (WBORepository) synchronizer.repositoryA;
+ final WBORepository repoB = (WBORepository) synchronizer.repositoryB;
+
+ repoA.wbos.put(guidA, bookmarkRecordA);
+ repoB.wbos.put(guidB, bookmarkRecordB);
+ repoB.wbos.put(guidC, bookmarkRecordC);
+
+ WaitHelper.getTestWaiter().performWait(new Runnable() {
+ @Override
+ public void run() {
+ synchronizer.synchronize(context, new SynchronizerDelegate() {
+
+ @Override
+ public void onSynchronized(Synchronizer synchronizer) {
+ try {
+ // No records get sent either way.
+ final SynchronizerSession synchronizerSession = synchronizer.getSynchronizerSession();
+ assertNotNull(synchronizerSession);
+ assertEquals(1, synchronizerSession.getInboundCount());
+ assertEquals(2, synchronizerSession.getOutboundCount());
+ WaitHelper.getTestWaiter().performNotify();
+ } catch (Throwable e) {
+ WaitHelper.getTestWaiter().performNotify(e);
+ }
+ }
+
+ @Override
+ public void onSynchronizeFailed(Synchronizer synchronizer,
+ Exception lastException, String reason) {
+ WaitHelper.getTestWaiter().performNotify(lastException);
+ }
+ });
+ }
+ });
+
+ // Verify contents.
+ assertTrue(repoA.wbos.containsKey(guidA));
+ assertTrue(repoA.wbos.containsKey(guidB));
+ assertTrue(repoA.wbos.containsKey(guidC));
+ assertTrue(repoB.wbos.containsKey(guidA));
+ assertTrue(repoB.wbos.containsKey(guidB));
+ assertTrue(repoB.wbos.containsKey(guidC));
+ BookmarkRecord aa = (BookmarkRecord) repoA.wbos.get(guidA);
+ BookmarkRecord ab = (BookmarkRecord) repoA.wbos.get(guidB);
+ BookmarkRecord ac = (BookmarkRecord) repoA.wbos.get(guidC);
+ BookmarkRecord ba = (BookmarkRecord) repoB.wbos.get(guidA);
+ BookmarkRecord bb = (BookmarkRecord) repoB.wbos.get(guidB);
+ BookmarkRecord bc = (BookmarkRecord) repoB.wbos.get(guidC);
+ recordEquals(aa, guidA, lastModifiedA, deleted, collection);
+ recordEquals(ab, guidB, lastModifiedB, deleted, collection);
+ recordEquals(ac, guidC, lastModifiedC, deleted, collection);
+ recordEquals(ba, guidA, lastModifiedA, deleted, collection);
+ recordEquals(bb, guidB, lastModifiedB, deleted, collection);
+ recordEquals(bc, guidC, lastModifiedC, deleted, collection);
+ recordEquals(aa, ba);
+ recordEquals(ab, bb);
+ recordEquals(ac, bc);
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestSynchronizerSession.java
@@ -0,0 +1,304 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.android.sync.test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mozilla.android.sync.test.SynchronizerHelpers.DataAvailableWBORepository;
+import org.mozilla.android.sync.test.SynchronizerHelpers.ShouldSkipWBORepository;
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.background.testhelpers.WBORepository;
+import org.mozilla.gecko.background.testhelpers.WaitHelper;
+import org.mozilla.gecko.sync.SynchronizerConfiguration;
+import org.mozilla.gecko.sync.repositories.RepositorySessionBundle;
+import org.mozilla.gecko.sync.repositories.domain.BookmarkRecord;
+import org.mozilla.gecko.sync.repositories.domain.Record;
+import org.mozilla.gecko.sync.synchronizer.Synchronizer;
+import org.mozilla.gecko.sync.synchronizer.SynchronizerSession;
+import org.mozilla.gecko.sync.synchronizer.SynchronizerSessionDelegate;
+
+import android.content.Context;
+
+public class TestSynchronizerSession {
+ public static final String LOG_TAG = TestSynchronizerSession.class.getSimpleName();
+
+ protected static void assertFirstContainsSecond(Map<String, Record> first, Map<String, Record> second) {
+ for (Entry<String, Record> entry : second.entrySet()) {
+ assertTrue("Expected key " + entry.getKey(), first.containsKey(entry.getKey()));
+ Record record = first.get(entry.getKey());
+ assertEquals(entry.getValue(), record);
+ }
+ }
+
+ protected static void assertFirstDoesNotContainSecond(Map<String, Record> first, Map<String, Record> second) {
+ for (Entry<String, Record> entry : second.entrySet()) {
+ assertFalse("Unexpected key " + entry.getKey(), first.containsKey(entry.getKey()));
+ }
+ }
+
+ protected WBORepository repoA = null;
+ protected WBORepository repoB = null;
+ protected SynchronizerSession syncSession = null;
+ protected Map<String, Record> originalWbosA = null;
+ protected Map<String, Record> originalWbosB = null;
+
+ @Before
+ public void setUp() {
+ repoA = new DataAvailableWBORepository(false);
+ repoB = new DataAvailableWBORepository(false);
+
+ final String collection = "bookmarks";
+ final boolean deleted = false;
+ final String guidA = "abcdabcdabcd";
+ final String guidB = "ffffffffffff";
+ final String guidC = "xxxxxxxxxxxx";
+ final long lastModifiedA = 312345;
+ final long lastModifiedB = 412340;
+ final long lastModifiedC = 412345;
+ final BookmarkRecord bookmarkRecordA = new BookmarkRecord(guidA, collection, lastModifiedA, deleted);
+ final BookmarkRecord bookmarkRecordB = new BookmarkRecord(guidB, collection, lastModifiedB, deleted);
+ final BookmarkRecord bookmarkRecordC = new BookmarkRecord(guidC, collection, lastModifiedC, deleted);
+
+ repoA.wbos.put(guidA, bookmarkRecordA);
+ repoB.wbos.put(guidB, bookmarkRecordB);
+ repoB.wbos.put(guidC, bookmarkRecordC);
+
+ originalWbosA = new HashMap<String, Record>(repoA.wbos);
+ originalWbosB = new HashMap<String, Record>(repoB.wbos);
+
+ Synchronizer synchronizer = new Synchronizer();
+ synchronizer.repositoryA = repoA;
+ synchronizer.repositoryB = repoB;
+ syncSession = new SynchronizerSession(synchronizer, new SynchronizerSessionDelegate() {
+ @Override
+ public void onInitialized(SynchronizerSession session) {
+ session.synchronize();
+ }
+
+ @Override
+ public void onSynchronized(SynchronizerSession session) {
+ WaitHelper.getTestWaiter().performNotify();
+ }
+
+ @Override
+ public void onSynchronizeFailed(SynchronizerSession session, Exception lastException, String reason) {
+ WaitHelper.getTestWaiter().performNotify(lastException);
+ }
+
+ @Override
+ public void onSynchronizeSkipped(SynchronizerSession synchronizerSession) {
+ WaitHelper.getTestWaiter().performNotify(new RuntimeException("Not expecting onSynchronizeSkipped"));
+ }
+ });
+ }
+
+ protected void logStats() {
+ // Uncomment this line to print stats to console:
+ // Logger.startLoggingTo(new PrintLogWriter(new PrintWriter(System.out, true)));
+
+ Logger.debug(LOG_TAG, "Repo A fetch done: " + repoA.stats.fetchCompleted);
+ Logger.debug(LOG_TAG, "Repo B store done: " + repoB.stats.storeCompleted);
+ Logger.debug(LOG_TAG, "Repo B fetch done: " + repoB.stats.fetchCompleted);
+ Logger.debug(LOG_TAG, "Repo A store done: " + repoA.stats.storeCompleted);
+
+ SynchronizerConfiguration sc = syncSession.getSynchronizer().save();
+ Logger.debug(LOG_TAG, "Repo A timestamp: " + sc.remoteBundle.getTimestamp());
+ Logger.debug(LOG_TAG, "Repo B timestamp: " + sc.localBundle.getTimestamp());
+ }
+
+ protected void doTest(boolean remoteDataAvailable, boolean localDataAvailable) {
+ ((DataAvailableWBORepository) repoA).dataAvailable = remoteDataAvailable;
+ ((DataAvailableWBORepository) repoB).dataAvailable = localDataAvailable;
+
+ WaitHelper.getTestWaiter().performWait(new Runnable() {
+ @Override
+ public void run() {
+ final Context context = null;
+ syncSession.init(context,
+ new RepositorySessionBundle(0),
+ new RepositorySessionBundle(0));
+ }
+ });
+
+ logStats();
+ }
+
+ @Test
+ public void testSynchronizerSessionBothHaveData() {
+ long before = System.currentTimeMillis();
+ boolean remoteDataAvailable = true;
+ boolean localDataAvailable = true;
+ doTest(remoteDataAvailable, localDataAvailable);
+ long after = System.currentTimeMillis();
+
+ assertEquals(1, syncSession.getInboundCount());
+ assertEquals(2, syncSession.getOutboundCount());
+
+ // Didn't lose any records.
+ assertFirstContainsSecond(repoA.wbos, originalWbosA);
+ assertFirstContainsSecond(repoB.wbos, originalWbosB);
+ // Got new records.
+ assertFirstContainsSecond(repoA.wbos, originalWbosB);
+ assertFirstContainsSecond(repoB.wbos, originalWbosA);
+
+ // Timestamps updated.
+ SynchronizerConfiguration sc = syncSession.getSynchronizer().save();
+ TestSynchronizer.assertInRangeInclusive(before, sc.localBundle.getTimestamp(), after);
+ TestSynchronizer.assertInRangeInclusive(before, sc.remoteBundle.getTimestamp(), after);
+ }
+
+ @Test
+ public void testSynchronizerSessionOnlyLocalHasData() {
+ long before = System.currentTimeMillis();
+ boolean remoteDataAvailable = false;
+ boolean localDataAvailable = true;
+ doTest(remoteDataAvailable, localDataAvailable);
+ long after = System.currentTimeMillis();
+
+ // Record counts updated.
+ assertEquals(0, syncSession.getInboundCount());
+ assertEquals(2, syncSession.getOutboundCount());
+
+ // Didn't lose any records.
+ assertFirstContainsSecond(repoA.wbos, originalWbosA);
+ assertFirstContainsSecond(repoB.wbos, originalWbosB);
+ // Got new records.
+ assertFirstContainsSecond(repoA.wbos, originalWbosB);
+ // Didn't get records we shouldn't have fetched.
+ assertFirstDoesNotContainSecond(repoB.wbos, originalWbosA);
+
+ // Timestamps updated.
+ SynchronizerConfiguration sc = syncSession.getSynchronizer().save();
+ TestSynchronizer.assertInRangeInclusive(before, sc.localBundle.getTimestamp(), after);
+ TestSynchronizer.assertInRangeInclusive(before, sc.remoteBundle.getTimestamp(), after);
+ }
+
+ @Test
+ public void testSynchronizerSessionOnlyRemoteHasData() {
+ long before = System.currentTimeMillis();
+ boolean remoteDataAvailable = true;
+ boolean localDataAvailable = false;
+ doTest(remoteDataAvailable, localDataAvailable);
+ long after = System.currentTimeMillis();
+
+ // Record counts updated.
+ assertEquals(1, syncSession.getInboundCount());
+ assertEquals(0, syncSession.getOutboundCount());
+
+ // Didn't lose any records.
+ assertFirstContainsSecond(repoA.wbos, originalWbosA);
+ assertFirstContainsSecond(repoB.wbos, originalWbosB);
+ // Got new records.
+ assertFirstContainsSecond(repoB.wbos, originalWbosA);
+ // Didn't get records we shouldn't have fetched.
+ assertFirstDoesNotContainSecond(repoA.wbos, originalWbosB);
+
+ // Timestamps updated.
+ SynchronizerConfiguration sc = syncSession.getSynchronizer().save();
+ TestSynchronizer.assertInRangeInclusive(before, sc.localBundle.getTimestamp(), after);
+ TestSynchronizer.assertInRangeInclusive(before, sc.remoteBundle.getTimestamp(), after);
+ }
+
+ @Test
+ public void testSynchronizerSessionNeitherHaveData() {
+ long before = System.currentTimeMillis();
+ boolean remoteDataAvailable = false;
+ boolean localDataAvailable = false;
+ doTest(remoteDataAvailable, localDataAvailable);
+ long after = System.currentTimeMillis();
+
+ // Record counts updated.
+ assertEquals(0, syncSession.getInboundCount());
+ assertEquals(0, syncSession.getOutboundCount());
+
+ // Didn't lose any records.
+ assertFirstContainsSecond(repoA.wbos, originalWbosA);
+ assertFirstContainsSecond(repoB.wbos, originalWbosB);
+ // Didn't get records we shouldn't have fetched.
+ assertFirstDoesNotContainSecond(repoA.wbos, originalWbosB);
+ assertFirstDoesNotContainSecond(repoB.wbos, originalWbosA);
+
+ // Timestamps updated.
+ SynchronizerConfiguration sc = syncSession.getSynchronizer().save();
+ TestSynchronizer.assertInRangeInclusive(before, sc.localBundle.getTimestamp(), after);
+ TestSynchronizer.assertInRangeInclusive(before, sc.remoteBundle.getTimestamp(), after);
+ }
+
+ protected void doSkipTest(boolean remoteShouldSkip, boolean localShouldSkip) {
+ repoA = new ShouldSkipWBORepository(remoteShouldSkip);
+ repoB = new ShouldSkipWBORepository(localShouldSkip);
+
+ Synchronizer synchronizer = new Synchronizer();
+ synchronizer.repositoryA = repoA;
+ synchronizer.repositoryB = repoB;
+
+ syncSession = new SynchronizerSession(synchronizer, new SynchronizerSessionDelegate() {
+ @Override
+ public void onInitialized(SynchronizerSession session) {
+ session.synchronize();
+ }
+
+ @Override
+ public void onSynchronized(SynchronizerSession session) {
+ WaitHelper.getTestWaiter().performNotify(new RuntimeException("Not expecting onSynchronized"));
+ }
+
+ @Override
+ public void onSynchronizeFailed(SynchronizerSession session, Exception lastException, String reason) {
+ WaitHelper.getTestWaiter().performNotify(lastException);
+ }
+
+ @Override
+ public void onSynchronizeSkipped(SynchronizerSession synchronizerSession) {
+ WaitHelper.getTestWaiter().performNotify();
+ }
+ });
+
+ WaitHelper.getTestWaiter().performWait(new Runnable() {
+ @Override
+ public void run() {
+ final Context context = null;
+ syncSession.init(context,
+ new RepositorySessionBundle(100),
+ new RepositorySessionBundle(200));
+ }
+ });
+
+ // If we skip, we don't update timestamps or even un-bundle.
+ SynchronizerConfiguration sc = syncSession.getSynchronizer().save();
+ assertNotNull(sc);
+ assertNull(sc.localBundle);
+ assertNull(sc.remoteBundle);
+ assertEquals(-1, syncSession.getInboundCount());
+ assertEquals(-1, syncSession.getOutboundCount());
+ }
+
+ @Test
+ public void testSynchronizerSessionShouldSkip() {
+ // These combinations should all skip.
+ doSkipTest(true, false);
+
+ doSkipTest(false, true);
+ doSkipTest(true, true);
+
+ try {
+ doSkipTest(false, false);
+ fail("Expected exception.");
+ } catch (WaitHelper.InnerError e) {
+ assertTrue(e.innerError instanceof RuntimeException);
+ assertEquals("Not expecting onSynchronized", e.innerError.getMessage());
+ }
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestUtils.java
@@ -0,0 +1,159 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.android.sync.test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.io.UnsupportedEncodingException;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.Arrays;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.mozilla.gecko.sync.SyncConstants;
+import org.mozilla.gecko.sync.Utils;
+
+public class TestUtils extends Utils {
+
+ @Test
+ public void testGenerateGUID() {
+ for (int i = 0; i < 1000; ++i) {
+ assertEquals(12, Utils.generateGuid().length());
+ }
+ }
+
+ public static final byte[][] BYTE_ARRS = {
+ new byte[] {' '}, // Tab.
+ new byte[] {'0'},
+ new byte[] {'A'},
+ new byte[] {'a'},
+ new byte[] {'I', 'U'},
+ new byte[] {'`', 'h', 'g', ' ', 's', '`'},
+ new byte[] {}
+ };
+ // Indices correspond with the above array.
+ public static final String[] STRING_ARR = {
+ "09",
+ "30",
+ "41",
+ "61",
+ "4955",
+ "606867207360",
+ ""
+ };
+
+ @Test
+ public void testByte2Hex() throws Exception {
+ for (int i = 0; i < BYTE_ARRS.length; ++i) {
+ final byte[] b = BYTE_ARRS[i];
+ final String expected = STRING_ARR[i];
+ assertEquals(expected, Utils.byte2Hex(b));
+ }
+ }
+
+ @Test
+ public void testHex2Byte() throws Exception {
+ for (int i = 0; i < STRING_ARR.length; ++i) {
+ final String s = STRING_ARR[i];
+ final byte[] expected = BYTE_ARRS[i];
+ assertTrue(Arrays.equals(expected, Utils.hex2Byte(s)));
+ }
+ }
+
+ @Test
+ public void testByte2Hex2ByteAndViceVersa() throws Exception { // There and back again!
+ for (int i = 0; i < BYTE_ARRS.length; ++i) {
+ // byte2Hex2Byte
+ final byte[] b = BYTE_ARRS[i];
+ final String s = Utils.byte2Hex(b);
+ assertTrue(Arrays.equals(b, Utils.hex2Byte(s)));
+ }
+
+ // hex2Byte2Hex
+ for (int i = 0; i < STRING_ARR.length; ++i) {
+ final String s = STRING_ARR[i];
+ final byte[] b = Utils.hex2Byte(s);
+ assertEquals(s, Utils.byte2Hex(b));
+ }
+ }
+
+ @Test
+ public void testByte2HexLength() throws Exception {
+ for (int i = 0; i < BYTE_ARRS.length; ++i) {
+ final byte[] b = BYTE_ARRS[i];
+ final String expected = STRING_ARR[i];
+ assertEquals(expected, Utils.byte2Hex(b, b.length));
+ assertEquals("0" + expected, Utils.byte2Hex(b, 2 * b.length + 1));
+ assertEquals("00" + expected, Utils.byte2Hex(b, 2 * b.length + 2));
+ }
+ }
+
+ @Test
+ public void testHex2ByteLength() throws Exception {
+ for (int i = 0; i < STRING_ARR.length; ++i) {
+ final String s = STRING_ARR[i];
+ final byte[] expected = BYTE_ARRS[i];
+ assertTrue(Arrays.equals(expected, Utils.hex2Byte(s)));
+ final byte[] expected1 = new byte[expected.length + 1];
+ System.arraycopy(expected, 0, expected1, 1, expected.length);
+ assertTrue(Arrays.equals(expected1, Utils.hex2Byte("00" + s)));
+ final byte[] expected2 = new byte[expected.length + 2];
+ System.arraycopy(expected, 0, expected2, 2, expected.length);
+ assertTrue(Arrays.equals(expected2, Utils.hex2Byte("0000" + s)));
+ }
+ }
+
+ @Test
+ public void testToCommaSeparatedString() {
+ ArrayList<String> xs = new ArrayList<String>();
+ assertEquals("", Utils.toCommaSeparatedString(null));
+ assertEquals("", Utils.toCommaSeparatedString(xs));
+ xs.add("test1");
+ assertEquals("test1", Utils.toCommaSeparatedString(xs));
+ xs.add("test2");
+ assertEquals("test1, test2", Utils.toCommaSeparatedString(xs));
+ xs.add("test3");
+ assertEquals("test1, test2, test3", Utils.toCommaSeparatedString(xs));
+ }
+
+ @Test
+ public void testUsernameFromAccount() throws NoSuchAlgorithmException, UnsupportedEncodingException {
+ assertEquals("xee7ffonluzpdp66l6xgpyh2v2w6ojkc", Utils.sha1Base32("foobar@baz.com"));
+ assertEquals("xee7ffonluzpdp66l6xgpyh2v2w6ojkc", Utils.usernameFromAccount("foobar@baz.com"));
+ assertEquals("xee7ffonluzpdp66l6xgpyh2v2w6ojkc", Utils.usernameFromAccount("FooBar@Baz.com"));
+ assertEquals("xee7ffonluzpdp66l6xgpyh2v2w6ojkc", Utils.usernameFromAccount("xee7ffonluzpdp66l6xgpyh2v2w6ojkc"));
+ assertEquals("foobar", Utils.usernameFromAccount("foobar"));
+ assertEquals("foobar", Utils.usernameFromAccount("FOOBAr"));
+ }
+
+ @Test
+ public void testGetPrefsPath() throws NoSuchAlgorithmException, UnsupportedEncodingException {
+ assertEquals("ore7dlrwqi6xr7honxdtpvmh6tly4r7k", Utils.sha1Base32("test.url.com:xee7ffonluzpdp66l6xgpyh2v2w6ojkc"));
+
+ assertEquals("sync.prefs.ore7dlrwqi6xr7honxdtpvmh6tly4r7k", Utils.getPrefsPath("product", "foobar@baz.com", "test.url.com", "default", 0));
+ assertEquals("sync.prefs.ore7dlrwqi6xr7honxdtpvmh6tly4r7k", Utils.getPrefsPath("org.mozilla.firefox_beta", "FooBar@Baz.com", "test.url.com", "default", 0));
+ assertEquals("sync.prefs.ore7dlrwqi6xr7honxdtpvmh6tly4r7k", Utils.getPrefsPath("org.mozilla.firefox", "xee7ffonluzpdp66l6xgpyh2v2w6ojkc", "test.url.com", "profile", 0));
+
+ assertEquals("sync.prefs.product.ore7dlrwqi6xr7honxdtpvmh6tly4r7k.default.1", Utils.getPrefsPath("product", "foobar@baz.com", "test.url.com", "default", 1));
+ assertEquals("sync.prefs.with!spaces_underbars!periods.ore7dlrwqi6xr7honxdtpvmh6tly4r7k.default.1", Utils.getPrefsPath("with spaces_underbars.periods", "foobar@baz.com", "test.url.com", "default", 1));
+ assertEquals("sync.prefs.org!mozilla!firefox_beta.ore7dlrwqi6xr7honxdtpvmh6tly4r7k.default.2", Utils.getPrefsPath("org.mozilla.firefox_beta", "FooBar@Baz.com", "test.url.com", "default", 2));
+ assertEquals("sync.prefs.org!mozilla!firefox.ore7dlrwqi6xr7honxdtpvmh6tly4r7k.profile.3", Utils.getPrefsPath("org.mozilla.firefox", "xee7ffonluzpdp66l6xgpyh2v2w6ojkc", "test.url.com", "profile", 3));
+ }
+
+ @Test
+ public void testObfuscateEmail() {
+ assertEquals("XXX@XXX.XXX", Utils.obfuscateEmail("foo@bar.com"));
+ assertEquals("XXXX@XXX.XXXX.XX", Utils.obfuscateEmail("foot@bar.test.ca"));
+ }
+
+ @Test
+ public void testNodeWeaveURL() throws Exception {
+ Assert.assertEquals("http://userapi.com/endpoint/user/1.0/username/node/weave", Utils.nodeWeaveURL("http://userapi.com/endpoint", "username"));
+ Assert.assertEquals("http://userapi.com/endpoint/user/1.0/username/node/weave", Utils.nodeWeaveURL("http://userapi.com/endpoint/", "username"));
+ Assert.assertEquals(SyncConstants.DEFAULT_AUTH_SERVER + "user/1.0/username/node/weave", Utils.nodeWeaveURL(null, "username"));
+ Assert.assertEquals(SyncConstants.DEFAULT_AUTH_SERVER + "user/1.0/username2/node/weave", Utils.nodeWeaveURL(null, "username2"));
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/BaseTestStorageRequestDelegate.java
@@ -0,0 +1,61 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.android.sync.test.helpers;
+
+import static org.junit.Assert.fail;
+
+import java.io.IOException;
+
+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.SyncStorageRequestDelegate;
+import org.mozilla.gecko.sync.net.SyncStorageResponse;
+
+public class BaseTestStorageRequestDelegate implements
+ SyncStorageRequestDelegate {
+
+ protected final AuthHeaderProvider authHeaderProvider;
+
+ public BaseTestStorageRequestDelegate(AuthHeaderProvider authHeaderProvider) {
+ this.authHeaderProvider = authHeaderProvider;
+ }
+
+ public BaseTestStorageRequestDelegate(String username, String password) {
+ this(new BasicAuthHeaderProvider(username, password));
+ }
+
+ @Override
+ public AuthHeaderProvider getAuthHeaderProvider() {
+ return authHeaderProvider;
+ }
+
+ @Override
+ public String ifUnmodifiedSince() {
+ return null;
+ }
+
+ @Override
+ public void handleRequestSuccess(SyncStorageResponse response) {
+ BaseResource.consumeEntity(response);
+ fail("Should not be called.");
+ }
+
+ @Override
+ public void handleRequestFailure(SyncStorageResponse response) {
+ System.out.println("Response: " + response.httpResponse().getStatusLine().getStatusCode());
+ BaseResource.consumeEntity(response);
+ fail("Should not be called.");
+ }
+
+ @Override
+ public void handleRequestError(Exception e) {
+ if (e instanceof IOException) {
+ System.out.println("WARNING: TEST FAILURE IGNORED!");
+ // Assume that this is because Jenkins doesn't have network access.
+ return;
+ }
+ fail("Should not error.");
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/ExpectSuccessDelegate.java
@@ -0,0 +1,35 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.android.sync.test.helpers;
+
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.background.testhelpers.WaitHelper;
+
+public class ExpectSuccessDelegate {
+ public WaitHelper waitHelper;
+
+ public ExpectSuccessDelegate(WaitHelper waitHelper) {
+ this.waitHelper = waitHelper;
+ }
+
+ public void performNotify() {
+ this.waitHelper.performNotify();
+ }
+
+ public void performNotify(Throwable e) {
+ this.waitHelper.performNotify(e);
+ }
+
+ public String logTag() {
+ return this.getClass().getSimpleName();
+ }
+
+ public void log(String message) {
+ Logger.info(logTag(), message);
+ }
+
+ public void log(String message, Throwable throwable) {
+ Logger.warn(logTag(), message, throwable);
+ }
+}
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/ExpectSuccessRepositorySessionBeginDelegate.java
@@ -0,0 +1,39 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.android.sync.test.helpers;
+
+import java.util.concurrent.ExecutorService;
+
+import junit.framework.AssertionFailedError;
+
+import org.mozilla.gecko.background.testhelpers.WaitHelper;
+import org.mozilla.gecko.sync.repositories.RepositorySession;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionBeginDelegate;
+
+public class ExpectSuccessRepositorySessionBeginDelegate
+extends ExpectSuccessDelegate
+implements RepositorySessionBeginDelegate {
+
+ public ExpectSuccessRepositorySessionBeginDelegate(WaitHelper waitHelper) {
+ super(waitHelper);
+ }
+
+ @Override
+ public void onBeginFailed(Exception ex) {
+ log("Session begin failed.", ex);
+ performNotify(new AssertionFailedError("Session begin failed: " + ex.getMessage()));
+ }
+
+ @Override
+ public void onBeginSucceeded(RepositorySession session) {
+ log("Session begin succeeded.");
+ performNotify();
+ }
+
+ @Override
+ public RepositorySessionBeginDelegate deferredBeginDelegate(ExecutorService executor) {
+ log("Session begin delegate deferred.");
+ return this;
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/ExpectSuccessRepositorySessionCreationDelegate.java
@@ -0,0 +1,37 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.android.sync.test.helpers;
+
+import junit.framework.AssertionFailedError;
+
+import org.mozilla.gecko.background.testhelpers.WaitHelper;
+import org.mozilla.gecko.sync.repositories.RepositorySession;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate;
+
+public class ExpectSuccessRepositorySessionCreationDelegate extends
+ ExpectSuccessDelegate implements RepositorySessionCreationDelegate {
+
+ public ExpectSuccessRepositorySessionCreationDelegate(WaitHelper waitHelper) {
+ super(waitHelper);
+ }
+
+ @Override
+ public void onSessionCreateFailed(Exception ex) {
+ log("Session creation failed.", ex);
+ performNotify(new AssertionFailedError("onSessionCreateFailed: session creation should not have failed."));
+ }
+
+ @Override
+ public void onSessionCreated(RepositorySession session) {
+ log("Session creation succeeded.");
+ performNotify();
+ }
+
+ @Override
+ public RepositorySessionCreationDelegate deferredCreationDelegate() {
+ log("Session creation deferred.");
+ return this;
+ }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/ExpectSuccessRepositorySessionFetchRecordsDelegate.java
@@ -0,0 +1,45 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.android.sync.test.helpers;
+
+import java.util.ArrayList;
+import java.util.concurrent.ExecutorService;
+
+import junit.framework.AssertionFailedError;
+
+import org.mozilla.gecko.background.testhelpers.WaitHelper;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFetchRecordsDelegate;
+import org.mozilla.gecko.sync.repositories.domain.Record;
+
+public class ExpectSuccessRepositorySessionFetchRecordsDelegate extends
+ ExpectSuccessDelegate implements RepositorySessionFetchRecordsDelegate {
+ public ArrayList<Record> fetchedRecords = new ArrayList<Record>();
+
+ public ExpectSuccessRepositorySessionFetchRecordsDelegate(WaitHelper waitHelper) {
+ super(waitHelper);
+ }
+
+ @Override
+ public void onFetchFailed(Exception ex, Record record) {
+ log("Fetch failed.", ex);
+ performNotify(new AssertionFailedError("onFetchFailed: fetch should not have failed."));
+ }
+
+ @Override
+ public void onFetchedRecord(Record record) {
+ fetchedRecords.add(record);
+ log("Fetched record with guid '" + record.guid + "'.");
+ }
+
+ @Override
+ public void onFetchCompleted(long end) {
+ log("Fetch completed.");
+ performNotify();
+ }
+
+ @Override
+ public RepositorySessionFetchRecordsDelegate deferredFetchDelegate(ExecutorService executor) {
+ return this;
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/ExpectSuccessRepositorySessionFinishDelegate.java
@@ -0,0 +1,38 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.android.sync.test.helpers;
+
+import java.util.concurrent.ExecutorService;
+
+import junit.framework.AssertionFailedError;
+
+import org.mozilla.gecko.background.testhelpers.WaitHelper;
+import org.mozilla.gecko.sync.repositories.RepositorySession;
+import org.mozilla.gecko.sync.repositories.RepositorySessionBundle;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFinishDelegate;
+
+public class ExpectSuccessRepositorySessionFinishDelegate extends
+ ExpectSuccessDelegate implements RepositorySessionFinishDelegate {
+
+ public ExpectSuccessRepositorySessionFinishDelegate(WaitHelper waitHelper) {
+ super(waitHelper);
+ }
+
+ @Override
+ public void onFinishFailed(Exception ex) {
+ log("Finish failed.", ex);
+ performNotify(new AssertionFailedError("onFinishFailed: finish should not have failed."));
+ }
+
+ @Override
+ public void onFinishSucceeded(RepositorySession session, RepositorySessionBundle bundle) {
+ log("Finish succeeded.");
+ performNotify();
+ }
+
+ @Override
+ public RepositorySessionFinishDelegate deferredFinishDelegate(ExecutorService executor) {
+ return this;
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/ExpectSuccessRepositorySessionStoreDelegate.java
@@ -0,0 +1,40 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.android.sync.test.helpers;
+
+import java.util.concurrent.ExecutorService;
+
+import junit.framework.AssertionFailedError;
+
+import org.mozilla.gecko.background.testhelpers.WaitHelper;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionStoreDelegate;
+
+public class ExpectSuccessRepositorySessionStoreDelegate extends
+ ExpectSuccessDelegate implements RepositorySessionStoreDelegate {
+
+ public ExpectSuccessRepositorySessionStoreDelegate(WaitHelper waitHelper) {
+ super(waitHelper);
+ }
+
+ @Override
+ public void onRecordStoreFailed(Exception ex, String guid) {
+ log("Record store failed.", ex);
+ performNotify(new AssertionFailedError("onRecordStoreFailed: record store should not have failed."));
+ }
+
+ @Override
+ public void onRecordStoreSucceeded(String guid) {
+ log("Record store succeeded.");
+ }
+
+ @Override
+ public void onStoreCompleted(long storeEnd) {
+ log("Record store completed at " + storeEnd);
+ }
+
+ @Override
+ public RepositorySessionStoreDelegate deferredStoreDelegate(ExecutorService executor) {
+ return this;
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/ExpectSuccessRepositoryWipeDelegate.java
@@ -0,0 +1,37 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.android.sync.test.helpers;
+
+import java.util.concurrent.ExecutorService;
+
+import junit.framework.AssertionFailedError;
+
+import org.mozilla.gecko.background.testhelpers.WaitHelper;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionWipeDelegate;
+
+public class ExpectSuccessRepositoryWipeDelegate extends ExpectSuccessDelegate
+ implements RepositorySessionWipeDelegate {
+
+ public ExpectSuccessRepositoryWipeDelegate(WaitHelper waitHelper) {
+ super(waitHelper);
+ }
+
+ @Override
+ public void onWipeSucceeded() {
+ log("Wipe succeeded.");
+ performNotify();
+ }
+
+ @Override
+ public void onWipeFailed(Exception ex) {
+ log("Wipe failed.", ex);
+ performNotify(new AssertionFailedError("onWipeFailed: wipe should not have failed."));
+ }
+
+ @Override
+ public RepositorySessionWipeDelegate deferredWipeDelegate(ExecutorService executor) {
+ log("Wipe deferred.");
+ return this;
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/HTTPServerTestHelper.java
@@ -0,0 +1,225 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.android.sync.test.helpers;
+
+import static org.junit.Assert.fail;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+import java.util.IdentityHashMap;
+import java.util.Map;
+
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.sync.net.BaseResource;
+import org.mozilla.gecko.sync.net.BaseResourceDelegate;
+import org.simpleframework.transport.connect.Connection;
+import org.simpleframework.transport.connect.SocketConnection;
+
+/**
+ * Test helper code to bind <code>MockServer</code> instances to ports.
+ * <p>
+ * Maintains a collection of running servers and (by default) throws helpful
+ * errors if two servers are started "on top" of each other. The
+ * <b>unchecked</b> exception thrown contains a stack trace pointing to where
+ * the new server is being created and where the pre-existing server was
+ * created.
+ * <p>
+ * Parses a system property to determine current test port, which is fixed for
+ * the duration of a test execution.
+ */
+public class HTTPServerTestHelper {
+ private static final String LOG_TAG = "HTTPServerTestHelper";
+
+ /**
+ * Port to run HTTP servers on during this test execution.
+ * <p>
+ * Lazily initialized on first call to {@link #getTestPort}.
+ */
+ public static Integer testPort = null;
+
+ public static final String LOCAL_HTTP_PORT_PROPERTY = "android.sync.local.http.port";
+ public static final int LOCAL_HTTP_PORT_DEFAULT = 15125;
+
+ public final int port;
+
+ public Connection connection;
+ public MockServer server;
+
+ /**
+ * Create a helper to bind <code>MockServer</code> instances.
+ * <p>
+ * Use {@link #getTestPort} to determine the port this helper will bind to.
+ */
+ public HTTPServerTestHelper() {
+ this.port = getTestPort();
+ }
+
+ // For testing only.
+ protected HTTPServerTestHelper(int port) {
+ this.port = port;
+ }
+
+ /**
+ * Lazily initialize test port for this test execution.
+ * <p>
+ * Only called from {@link #getTestPort}.
+ * <p>
+ * If the test port has not been determined, we try to parse it from a system
+ * property; if that fails, we return the default test port.
+ */
+ protected synchronized static void ensureTestPort() {
+ if (testPort != null) {
+ return;
+ }
+
+ String value = System.getProperty(LOCAL_HTTP_PORT_PROPERTY);
+ if (value != null) {
+ try {
+ testPort = Integer.valueOf(value);
+ } catch (NumberFormatException e) {
+ Logger.warn(LOG_TAG, "Got exception parsing local test port; ignoring. ", e);
+ }
+ }
+
+ if (testPort == null) {
+ testPort = Integer.valueOf(LOCAL_HTTP_PORT_DEFAULT);
+ }
+ }
+
+ /**
+ * The port to which all HTTP servers will be found for the duration of this
+ * test execution.
+ * <p>
+ * We try to parse the port from a system property; if that fails, we return
+ * the default test port.
+ *
+ * @return port number.
+ */
+ public synchronized static int getTestPort() {
+ if (testPort == null) {
+ ensureTestPort();
+ }
+
+ return testPort.intValue();
+ }
+
+ /**
+ * Used to maintain a stack trace pointing to where a server was started.
+ */
+ public static class HTTPServerStartedError extends Error {
+ private static final long serialVersionUID = -6778447718799087274L;
+
+ public final HTTPServerTestHelper httpServer;
+
+ public HTTPServerStartedError(HTTPServerTestHelper httpServer) {
+ this.httpServer = httpServer;
+ }
+ }
+
+ /**
+ * Thrown when a server is started "on top" of another server. The cause error
+ * will be an <code>HTTPServerStartedError</code> with a stack trace pointing
+ * to where the pre-existing server was started.
+ */
+ public static class HTTPServerAlreadyRunningError extends Error {
+ private static final long serialVersionUID = -6778447718799087275L;
+
+ public HTTPServerAlreadyRunningError(Throwable e) {
+ super(e);
+ }
+ }
+
+ /**
+ * Maintain a hash of running servers. Each value is an error with a stack
+ * traces pointing to where that server was started.
+ * <p>
+ * We don't key on the server itself because each server is a <it>helper</it>
+ * that may be started many times with different <code>MockServer</code>
+ * instances.
+ * <p>
+ * Synchronize access on the class.
+ */
+ protected static Map<Connection, HTTPServerStartedError> runningServers =
+ new IdentityHashMap<Connection, HTTPServerStartedError>();
+
+ protected synchronized static void throwIfServerAlreadyRunning() {
+ for (HTTPServerStartedError value : runningServers.values()) {
+ throw new HTTPServerAlreadyRunningError(value);
+ }
+ }
+
+ protected synchronized static void registerServerAsRunning(HTTPServerTestHelper httpServer) {
+ if (httpServer == null || httpServer.connection == null) {
+ throw new IllegalArgumentException("HTTPServerTestHelper or connection was null; perhaps server has not been started?");
+ }
+
+ HTTPServerStartedError old = runningServers.put(httpServer.connection, new HTTPServerStartedError(httpServer));
+ if (old != null) {
+ // Should never happen.
+ throw old;
+ }
+ }
+
+ protected synchronized static void unregisterServerAsRunning(HTTPServerTestHelper httpServer) {
+ if (httpServer == null || httpServer.connection == null) {
+ throw new IllegalArgumentException("HTTPServerTestHelper or connection was null; perhaps server has not been started?");
+ }
+
+ runningServers.remove(httpServer.connection);
+ }
+
+ public MockServer startHTTPServer(MockServer server, boolean allowMultipleServers) {
+ BaseResource.rewriteLocalhost = false; // No sense rewriting when we're running the unit tests.
+ BaseResourceDelegate.connectionTimeoutInMillis = 1000; // No sense waiting a long time for a local connection.
+
+ if (!allowMultipleServers) {
+ throwIfServerAlreadyRunning();
+ }
+
+ try {
+ this.server = server;
+ connection = new SocketConnection(server);
+ SocketAddress address = new InetSocketAddress(port);
+ connection.connect(address);
+
+ registerServerAsRunning(this);
+
+ Logger.info(LOG_TAG, "Started HTTP server on port " + port + ".");
+ } catch (IOException ex) {
+ Logger.error(LOG_TAG, "Error starting HTTP server on port " + port + ".", ex);
+ fail(ex.toString());
+ }
+
+ return server;
+ }
+
+ public MockServer startHTTPServer(MockServer server) {
+ return startHTTPServer(server, false);
+ }
+
+ public MockServer startHTTPServer() {
+ return startHTTPServer(new MockServer());
+ }
+
+ public void stopHTTPServer() {
+ try {
+ if (connection != null) {
+ unregisterServerAsRunning(this);
+
+ connection.close();
+ }
+ server = null;
+ connection = null;
+
+ Logger.info(LOG_TAG, "Stopped HTTP server on port " + port + ".");
+
+ Logger.debug(LOG_TAG, "Closing connection pool...");
+ BaseResource.shutdownConnectionManager();
+ } catch (IOException ex) {
+ Logger.error(LOG_TAG, "Error stopping HTTP server on port " + port + ".", ex);
+ fail(ex.toString());
+ }
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/MockGlobalSessionCallback.java
@@ -0,0 +1,129 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.android.sync.test.helpers;
+
+import static org.junit.Assert.assertEquals;
+
+import java.net.URI;
+
+import org.mozilla.gecko.background.testhelpers.WaitHelper;
+import org.mozilla.gecko.sync.GlobalSession;
+import org.mozilla.gecko.sync.delegates.GlobalSessionCallback;
+import org.mozilla.gecko.sync.stage.GlobalSyncStage.Stage;
+
+/**
+ * A callback for use with a GlobalSession that records what happens for later
+ * inspection.
+ *
+ * This callback is expected to be used from within the friendly confines of a
+ * WaitHelper performWait.
+ */
+public class MockGlobalSessionCallback implements GlobalSessionCallback {
+ public final String nodeWeaveURL;
+
+ public MockGlobalSessionCallback(String nodeWeaveURL) {
+ this.nodeWeaveURL = nodeWeaveURL;
+ }
+
+ public MockGlobalSessionCallback() {
+ this(null);
+ }
+
+ protected WaitHelper testWaiter() {
+ return WaitHelper.getTestWaiter();
+ }
+
+ public int stageCounter = Stage.values().length - 1; // Exclude starting state.
+ public boolean calledSuccess = false;
+ public boolean calledError = false;
+ public Exception calledErrorException = null;
+ public boolean calledAborted = false;
+ public boolean calledRequestBackoff = false;
+ public boolean calledInformNodeAuthenticationFailed = false;
+ public boolean calledInformNodeAssigned = false;
+ public boolean calledInformUnauthorizedResponse = false;
+ public boolean calledInformUpgradeRequiredResponse = false;
+ public boolean calledInformMigrated = false;
+ public URI calledInformNodeAuthenticationFailedClusterURL = null;
+ public URI calledInformNodeAssignedOldClusterURL = null;
+ public URI calledInformNodeAssignedNewClusterURL = null;
+ public URI calledInformUnauthorizedResponseClusterURL = null;
+ public long weaveBackoff = -1;
+
+ @Override
+ public void handleSuccess(GlobalSession globalSession) {
+ this.calledSuccess = true;
+ assertEquals(0, this.stageCounter);
+ this.testWaiter().performNotify();
+ }
+
+ @Override
+ public void handleAborted(GlobalSession globalSession, String reason) {
+ this.calledAborted = true;
+ this.testWaiter().performNotify();
+ }
+
+ @Override
+ public void handleError(GlobalSession globalSession, Exception ex) {
+ this.calledError = true;
+ this.calledErrorException = ex;
+ this.testWaiter().performNotify();
+ }
+
+ @Override
+ public void handleStageCompleted(Stage currentState,
+ GlobalSession globalSession) {
+ stageCounter--;
+ }
+
+ @Override
+ public void requestBackoff(long backoff) {
+ this.calledRequestBackoff = true;
+ this.weaveBackoff = backoff;
+ }
+
+ @Override
+ public void informNodeAuthenticationFailed(GlobalSession session, URI clusterURL) {
+ this.calledInformNodeAuthenticationFailed = true;
+ this.calledInformNodeAuthenticationFailedClusterURL = clusterURL;
+ }
+
+ @Override
+ public void informNodeAssigned(GlobalSession session, URI oldClusterURL, URI newClusterURL) {
+ this.calledInformNodeAssigned = true;
+ this.calledInformNodeAssignedOldClusterURL = oldClusterURL;
+ this.calledInformNodeAssignedNewClusterURL = newClusterURL;
+ }
+
+ @Override
+ public void informUnauthorizedResponse(GlobalSession session, URI clusterURL) {
+ this.calledInformUnauthorizedResponse = true;
+ this.calledInformUnauthorizedResponseClusterURL = clusterURL;
+ }
+
+ @Override
+ public void informUpgradeRequiredResponse(GlobalSession session) {
+ this.calledInformUpgradeRequiredResponse = true;
+ }
+
+ @Override
+ public void informMigrated(GlobalSession session) {
+ this.calledInformMigrated = true;
+ }
+
+ @Override
+ public boolean shouldBackOffStorage() {
+ return false;
+ }
+
+ @Override
+ public boolean wantNodeAssignment() {
+ return false;
+ }
+
+ @Override
+ public String nodeWeaveURL() {
+ return nodeWeaveURL;
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/MockResourceDelegate.java
@@ -0,0 +1,86 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.android.sync.test.helpers;
+
+import static org.junit.Assert.assertEquals;
+
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+
+import org.mozilla.gecko.background.testhelpers.WaitHelper;
+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.ResourceDelegate;
+
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.client.ClientProtocolException;
+import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase;
+import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient;
+
+public class MockResourceDelegate implements ResourceDelegate {
+ public WaitHelper waitHelper = null;
+ public static String USER_PASS = "john:password";
+ public static String EXPECT_BASIC = "Basic am9objpwYXNzd29yZA==";
+
+ public boolean handledHttpResponse = false;
+ public HttpResponse httpResponse = null;
+
+ public MockResourceDelegate(WaitHelper waitHelper) {
+ this.waitHelper = waitHelper;
+ }
+
+ public MockResourceDelegate() {
+ this.waitHelper = WaitHelper.getTestWaiter();
+ }
+
+ @Override
+ public String getUserAgent() {
+ return null;
+ }
+
+ @Override
+ public void addHeaders(HttpRequestBase request, DefaultHttpClient client) {
+ }
+
+ @Override
+ public int connectionTimeout() {
+ return 0;
+ }
+
+ @Override
+ public int socketTimeout() {
+ return 0;
+ }
+
+ @Override
+ public AuthHeaderProvider getAuthHeaderProvider() {
+ return new BasicAuthHeaderProvider(USER_PASS);
+ }
+
+ @Override
+ public void handleHttpProtocolException(ClientProtocolException e) {
+ waitHelper.performNotify(e);
+ }
+
+ @Override
+ public void handleHttpIOException(IOException e) {
+ waitHelper.performNotify(e);
+ }
+
+ @Override
+ public void handleTransportException(GeneralSecurityException e) {
+ waitHelper.performNotify(e);
+ }
+
+ @Override
+ public void handleHttpResponse(HttpResponse response) {
+ handledHttpResponse = true;
+ httpResponse = response;
+
+ assertEquals(response.getStatusLine().getStatusCode(), 200);
+ BaseResource.consumeEntity(response);
+ waitHelper.performNotify();
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/MockServer.java
@@ -0,0 +1,72 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.android.sync.test.helpers;
+
+import static org.junit.Assert.assertEquals;
+
+import java.io.IOException;
+import java.io.PrintStream;
+
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.sync.Utils;
+import org.simpleframework.http.Request;
+import org.simpleframework.http.Response;
+import org.simpleframework.http.core.Container;
+
+public class MockServer implements Container {
+ public static final String LOG_TAG = "MockServer";
+
+ public int statusCode = 200;
+ public String body = "Hello World";
+
+ public MockServer() {
+ }
+
+ public MockServer(int statusCode, String body) {
+ this.statusCode = statusCode;
+ this.body = body;
+ }
+
+ public String expectedBasicAuthHeader;
+
+ protected PrintStream handleBasicHeaders(Request request, Response response, int code, String contentType) throws IOException {
+ return this.handleBasicHeaders(request, response, code, contentType, System.currentTimeMillis());
+ }
+
+ protected PrintStream handleBasicHeaders(Request request, Response response, int code, String contentType, long time) throws IOException {
+ Logger.debug(LOG_TAG, "< Auth header: " + request.getValue("Authorization"));
+
+ PrintStream bodyStream = response.getPrintStream();
+ response.setCode(code);
+ response.set("Content-Type", contentType);
+ response.set("Server", "HelloWorld/1.0 (Simple 4.0)");
+ response.setDate("Date", time);
+ response.setDate("Last-Modified", time);
+
+ final String timestampHeader = Utils.millisecondsToDecimalSecondsString(time);
+ response.set("X-Weave-Timestamp", timestampHeader);
+ Logger.debug(LOG_TAG, "> X-Weave-Timestamp header: " + timestampHeader);
+ return bodyStream;
+ }
+
+ protected void handle(Request request, Response response, int code, String body) {
+ try {
+ Logger.debug(LOG_TAG, "Handling request...");
+ PrintStream bodyStream = this.handleBasicHeaders(request, response, code, "application/json");
+
+ if (expectedBasicAuthHeader != null) {
+ Logger.debug(LOG_TAG, "Expecting auth header " + expectedBasicAuthHeader);
+ assertEquals(request.getValue("Authorization"), expectedBasicAuthHeader);
+ }
+
+ bodyStream.println(body);
+ bodyStream.close();
+ } catch (IOException e) {
+ Logger.error(LOG_TAG, "Oops.");
+ }
+ }
+ public void handle(Request request, Response response) {
+ this.handle(request, response, statusCode, body);
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/MockSyncClientsEngineStage.java
@@ -0,0 +1,71 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.android.sync.test.helpers;
+
+import static org.junit.Assert.assertTrue;
+
+import org.mozilla.gecko.sync.net.BaseResource;
+import org.mozilla.gecko.sync.net.SyncStorageResponse;
+import org.mozilla.gecko.sync.stage.SyncClientsEngineStage;
+
+public class MockSyncClientsEngineStage extends SyncClientsEngineStage {
+ public class MockClientUploadDelegate extends ClientUploadDelegate {
+ HTTPServerTestHelper data;
+
+ public MockClientUploadDelegate(HTTPServerTestHelper data) {
+ this.data = data;
+ }
+
+ @Override
+ public void handleRequestSuccess(SyncStorageResponse response) {
+ assertTrue(response.wasSuccessful());
+ data.stopHTTPServer();
+ super.handleRequestSuccess(response);
+ }
+
+ @Override
+ public void handleRequestFailure(SyncStorageResponse response) {
+ BaseResource.consumeEntity(response);
+ data.stopHTTPServer();
+ super.handleRequestFailure(response);
+ }
+
+ @Override
+ public void handleRequestError(Exception ex) {
+ ex.printStackTrace();
+ data.stopHTTPServer();
+ super.handleRequestError(ex);
+ }
+ }
+
+ public class TestClientDownloadDelegate extends ClientDownloadDelegate {
+ HTTPServerTestHelper data;
+
+ public TestClientDownloadDelegate(HTTPServerTestHelper data) {
+ this.data = data;
+ }
+
+ @Override
+ public void handleRequestSuccess(SyncStorageResponse response) {
+ assertTrue(response.wasSuccessful());
+ BaseResource.consumeEntity(response);
+ data.stopHTTPServer();
+ super.handleRequestSuccess(response);
+ }
+
+ @Override
+ public void handleRequestFailure(SyncStorageResponse response) {
+ BaseResource.consumeEntity(response);
+ super.handleRequestFailure(response);
+ data.stopHTTPServer();
+ }
+
+ @Override
+ public void handleRequestError(Exception ex) {
+ ex.printStackTrace();
+ super.handleRequestError(ex);
+ data.stopHTTPServer();
+ }
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/MockWBOServer.java
@@ -0,0 +1,28 @@
+package org.mozilla.android.sync.test.helpers;
+
+import java.util.HashMap;
+
+import org.simpleframework.http.Path;
+import org.simpleframework.http.Request;
+import org.simpleframework.http.Response;
+
+/**
+ * A trivial server that collects and returns WBOs.
+ *
+ * @author rnewman
+ *
+ */
+public class MockWBOServer extends MockServer {
+ public HashMap<String, HashMap<String, String> > collections;
+
+ public MockWBOServer() {
+ collections = new HashMap<String, HashMap<String, String> >();
+ }
+
+ @Override
+ public void handle(Request request, Response response) {
+ Path path = request.getPath();
+ path.getPath(0);
+ // TODO
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/test/TestHTTPServerTestHelper.java
@@ -0,0 +1,99 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.android.sync.test.helpers.test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+import org.junit.Test;
+import org.mozilla.android.sync.test.helpers.HTTPServerTestHelper;
+import org.mozilla.android.sync.test.helpers.MockServer;
+import org.mozilla.android.sync.test.helpers.HTTPServerTestHelper.HTTPServerAlreadyRunningError;
+
+public class TestHTTPServerTestHelper {
+ public static final int TEST_PORT = HTTPServerTestHelper.getTestPort();
+
+ protected MockServer mockServer = new MockServer();
+
+ @Test
+ public void testStartStop() {
+ // Need to be able to start and stop multiple times.
+ for (int i = 0; i < 2; i++) {
+ HTTPServerTestHelper httpServer = new HTTPServerTestHelper();
+
+ assertNull(httpServer.connection);
+ httpServer.startHTTPServer(mockServer);
+
+ assertNotNull(httpServer.connection);
+ httpServer.stopHTTPServer();
+ }
+ }
+
+ public void startAgain() {
+ HTTPServerTestHelper httpServer = new HTTPServerTestHelper();
+ httpServer.startHTTPServer(mockServer);
+ }
+
+ @Test
+ public void testStartTwice() {
+ HTTPServerTestHelper httpServer = new HTTPServerTestHelper();
+
+ httpServer.startHTTPServer(mockServer);
+ assertNotNull(httpServer.connection);
+
+ // Should not be able to start multiple times.
+ try {
+ try {
+ startAgain();
+
+ fail("Expected exception.");
+ } catch (Throwable e) {
+ assertEquals(HTTPServerAlreadyRunningError.class, e.getClass());
+
+ StringWriter sw = new StringWriter();
+ e.printStackTrace(new PrintWriter(sw));
+ String s = sw.toString();
+
+ // Ensure we get a useful stack trace.
+ // We should have the method trying to start the server the second time...
+ assertTrue(s.contains("startAgain"));
+ // ... as well as the the method that started the server the first time.
+ assertTrue(s.contains("testStartTwice"));
+ }
+ } finally {
+ httpServer.stopHTTPServer();
+ }
+ }
+
+ protected static class LeakyHTTPServerTestHelper extends HTTPServerTestHelper {
+ // Make this constructor public, just for this test.
+ public LeakyHTTPServerTestHelper(int port) {
+ super(port);
+ }
+ }
+
+ @Test
+ public void testForceStartTwice() {
+ HTTPServerTestHelper httpServer1 = new HTTPServerTestHelper();
+ HTTPServerTestHelper httpServer2 = new LeakyHTTPServerTestHelper(httpServer1.port + 1);
+
+ // Should be able to start multiple times if we specify it.
+ try {
+ httpServer1.startHTTPServer(mockServer);
+ assertNotNull(httpServer1.connection);
+
+ httpServer2.startHTTPServer(mockServer, true);
+ assertNotNull(httpServer2.connection);
+ } finally {
+ httpServer1.stopHTTPServer();
+ httpServer2.stopHTTPServer();
+ }
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/common/log/writers/test/TestLogWriters.java
@@ -0,0 +1,177 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.common.log.writers.test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.background.common.log.writers.LevelFilteringLogWriter;
+import org.mozilla.gecko.background.common.log.writers.LogWriter;
+import org.mozilla.gecko.background.common.log.writers.PrintLogWriter;
+import org.mozilla.gecko.background.common.log.writers.SimpleTagLogWriter;
+import org.mozilla.gecko.background.common.log.writers.StringLogWriter;
+import org.mozilla.gecko.background.common.log.writers.ThreadLocalTagLogWriter;
+
+import android.util.Log;
+
+public class TestLogWriters {
+
+ public static final String TEST_LOG_TAG_1 = "TestLogTag1";
+ public static final String TEST_LOG_TAG_2 = "TestLogTag2";
+
+ public static final String TEST_MESSAGE_1 = "LOG TEST MESSAGE one";
+ public static final String TEST_MESSAGE_2 = "LOG TEST MESSAGE two";
+ public static final String TEST_MESSAGE_3 = "LOG TEST MESSAGE three";
+
+ @Before
+ public void setUp() {
+ Logger.stopLoggingToAll();
+ }
+
+ @After
+ public void tearDown() {
+ Logger.stopLoggingToAll();
+ }
+
+ @Test
+ public void testStringLogWriter() {
+ StringLogWriter lw = new StringLogWriter();
+
+ Logger.error(TEST_LOG_TAG_1, TEST_MESSAGE_1, new RuntimeException());
+ Logger.startLoggingTo(lw);
+ Logger.error(TEST_LOG_TAG_1, TEST_MESSAGE_2);
+ Logger.warn(TEST_LOG_TAG_1, TEST_MESSAGE_2);
+ Logger.info(TEST_LOG_TAG_1, TEST_MESSAGE_2);
+ Logger.debug(TEST_LOG_TAG_1, TEST_MESSAGE_2);
+ Logger.trace(TEST_LOG_TAG_1, TEST_MESSAGE_2);
+ Logger.stopLoggingTo(lw);
+ Logger.error(TEST_LOG_TAG_2, TEST_MESSAGE_3, new RuntimeException());
+
+ String s = lw.toString();
+ assertFalse(s.contains("RuntimeException"));
+ assertFalse(s.contains(".java"));
+ assertTrue(s.contains(TEST_LOG_TAG_1));
+ assertFalse(s.contains(TEST_LOG_TAG_2));
+ assertFalse(s.contains(TEST_MESSAGE_1));
+ assertTrue(s.contains(TEST_MESSAGE_2));
+ assertFalse(s.contains(TEST_MESSAGE_3));
+ }
+
+ @Test
+ public void testSingleTagLogWriter() {
+ final String SINGLE_TAG = "XXX";
+ StringLogWriter lw = new StringLogWriter();
+
+ Logger.startLoggingTo(new SimpleTagLogWriter(SINGLE_TAG, lw));
+ Logger.error(TEST_LOG_TAG_1, TEST_MESSAGE_1);
+ Logger.warn(TEST_LOG_TAG_2, TEST_MESSAGE_2);
+
+ String s = lw.toString();
+ for (String line : s.split("\n")) {
+ assertTrue(line.startsWith(SINGLE_TAG));
+ }
+ assertTrue(s.startsWith(SINGLE_TAG + " :: E :: " + TEST_LOG_TAG_1));
+ }
+
+ @Test
+ public void testLevelFilteringLogWriter() {
+ StringLogWriter lw = new StringLogWriter();
+
+ assertFalse(new LevelFilteringLogWriter(Log.WARN, lw).shouldLogVerbose(TEST_LOG_TAG_1));
+ assertTrue(new LevelFilteringLogWriter(Log.VERBOSE, lw).shouldLogVerbose(TEST_LOG_TAG_1));
+
+ Logger.startLoggingTo(new LevelFilteringLogWriter(Log.WARN, lw));
+ Logger.error(TEST_LOG_TAG_1, TEST_MESSAGE_2);
+ Logger.warn(TEST_LOG_TAG_1, TEST_MESSAGE_2);
+ Logger.info(TEST_LOG_TAG_1, TEST_MESSAGE_2);
+ Logger.debug(TEST_LOG_TAG_1, TEST_MESSAGE_2);
+ Logger.trace(TEST_LOG_TAG_1, TEST_MESSAGE_2);
+
+ String s = lw.toString();
+ assertTrue(s.contains(PrintLogWriter.ERROR));
+ assertTrue(s.contains(PrintLogWriter.WARN));
+ assertFalse(s.contains(PrintLogWriter.INFO));
+ assertFalse(s.contains(PrintLogWriter.DEBUG));
+ assertFalse(s.contains(PrintLogWriter.VERBOSE));
+ }
+
+ @Test
+ public void testThreadLocalLogWriter() throws InterruptedException {
+ final InheritableThreadLocal<String> logTag = new InheritableThreadLocal<String>() {
+ @Override
+ protected String initialValue() {
+ return "PARENT";
+ }
+ };
+
+ final StringLogWriter stringLogWriter = new StringLogWriter();
+ final LogWriter logWriter = new ThreadLocalTagLogWriter(logTag, stringLogWriter);
+
+ try {
+ Logger.startLoggingTo(logWriter);
+
+ Logger.info("parent tag before", "parent message before");
+
+ int threads = 3;
+ final CountDownLatch latch = new CountDownLatch(threads);
+
+ for (int thread = 0; thread < threads; thread++) {
+ final int threadNumber = thread;
+
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ logTag.set("CHILD" + threadNumber);
+ Logger.info("child tag " + threadNumber, "child message " + threadNumber);
+ } finally {
+ latch.countDown();
+ }
+ }
+ }).start();
+ }
+
+ latch.await();
+
+ Logger.info("parent tag after", "parent message after");
+
+ String s = stringLogWriter.toString();
+ List<String> lines = Arrays.asList(s.split("\n"));
+
+ // Because tests are run in a multi-threaded environment, we get
+ // additional logs that are not generated by this test. So we test that we
+ // get all the messages in a reasonable order.
+ try {
+ int parent1 = lines.indexOf("PARENT :: I :: parent tag before :: parent message before");
+ int parent2 = lines.indexOf("PARENT :: I :: parent tag after :: parent message after");
+
+ assertTrue(parent1 >= 0);
+ assertTrue(parent2 >= 0);
+ assertTrue(parent1 < parent2);
+
+ for (int thread = 0; thread < threads; thread++) {
+ int child = lines.indexOf("CHILD" + thread + " :: I :: child tag " + thread + " :: child message " + thread);
+ assertTrue(child >= 0);
+ assertTrue(parent1 < child);
+ assertTrue(child < parent2);
+ }
+ } catch (Throwable e) {
+ // Shouldn't happen. Let's dump to aid debugging.
+ e.printStackTrace();
+ assertEquals("\0", s);
+ }
+ } finally {
+ Logger.stopLoggingTo(logWriter);
+ }
+ }
+}
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/fxa/test/TestFxAccountAgeLockoutHelper.java
@@ -0,0 +1,71 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.fxa.test;
+
+import java.util.Calendar;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.mozilla.gecko.background.fxa.FxAccountAgeLockoutHelper;
+import org.mozilla.gecko.fxa.FxAccountConstants;
+
+public class TestFxAccountAgeLockoutHelper {
+ @Test
+ public void testPassesAgeCheck() {
+ Calendar today = Calendar.getInstance();
+ int birthMonthIndex = today.get(Calendar.MONTH);
+ int birthDate = today.get(Calendar.DATE);
+ int birthYear = today.get(Calendar.YEAR) - FxAccountConstants.MINIMUM_AGE_TO_CREATE_AN_ACCOUNT;
+ Assert.assertTrue("Minimum age as of today",
+ FxAccountAgeLockoutHelper.passesAgeCheck(birthDate, birthMonthIndex, birthYear));
+
+ Calendar yesterday = Calendar.getInstance();
+ yesterday.add(Calendar.DATE, -1);
+ birthMonthIndex = yesterday.get(Calendar.MONTH);
+ birthDate = yesterday.get(Calendar.DATE);
+ birthYear = yesterday.get(Calendar.YEAR) - FxAccountConstants.MINIMUM_AGE_TO_CREATE_AN_ACCOUNT;
+ Assert.assertTrue("Minimum age more than by a day",
+ FxAccountAgeLockoutHelper.passesAgeCheck(birthDate, birthMonthIndex, birthYear));
+
+ Calendar tomorrow = Calendar.getInstance();
+ tomorrow.add(Calendar.DATE, 1);
+ birthMonthIndex = tomorrow.get(Calendar.MONTH);
+ birthDate = tomorrow.get(Calendar.DATE);
+ birthYear = tomorrow.get(Calendar.YEAR) - FxAccountConstants.MINIMUM_AGE_TO_CREATE_AN_ACCOUNT;
+ Assert.assertFalse("Minimum age fails by a day",
+ FxAccountAgeLockoutHelper.passesAgeCheck(birthDate, birthMonthIndex, birthYear));
+
+ Calendar monthBefore = Calendar.getInstance();
+ monthBefore.add(Calendar.MONTH, -1);
+ birthMonthIndex = monthBefore.get(Calendar.MONTH);
+ birthDate = monthBefore.get(Calendar.DATE);
+ birthYear = monthBefore.get(Calendar.YEAR) - FxAccountConstants.MINIMUM_AGE_TO_CREATE_AN_ACCOUNT;
+ Assert.assertTrue("Minimum age more than by a month",
+ FxAccountAgeLockoutHelper.passesAgeCheck(birthDate, birthMonthIndex, birthYear));
+
+ Calendar monthAfter = Calendar.getInstance();
+ monthAfter.add(Calendar.MONTH, 1);
+ birthMonthIndex = monthAfter.get(Calendar.MONTH);
+ birthDate = monthAfter.get(Calendar.DATE);
+ birthYear = monthAfter.get(Calendar.YEAR) - FxAccountConstants.MINIMUM_AGE_TO_CREATE_AN_ACCOUNT;
+ Assert.assertFalse("Minimum age fails by a month",
+ FxAccountAgeLockoutHelper.passesAgeCheck(birthDate, birthMonthIndex, birthYear));
+ }
+
+ @Test
+ public void testIsMagicYear() {
+ final Calendar today = Calendar.getInstance();
+ int magicYear = today.get(Calendar.YEAR) - FxAccountConstants.MINIMUM_AGE_TO_CREATE_AN_ACCOUNT;
+ Assert.assertTrue("Passes magic year check: year is magic year",
+ FxAccountAgeLockoutHelper.isMagicYear(magicYear));
+
+ int beforeMagicYear = today.get(Calendar.YEAR) - FxAccountConstants.MINIMUM_AGE_TO_CREATE_AN_ACCOUNT - 1;
+ Assert.assertFalse("Fails magic year check: year before magic year",
+ FxAccountAgeLockoutHelper.isMagicYear(beforeMagicYear));
+
+ int afterMagicYear = today.get(Calendar.YEAR) - FxAccountConstants.MINIMUM_AGE_TO_CREATE_AN_ACCOUNT + 1;
+ Assert.assertFalse("Fails magic year: year after magic year",
+ FxAccountAgeLockoutHelper.isMagicYear(afterMagicYear));
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/fxa/test/TestFxAccountClient20.java
@@ -0,0 +1,39 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.fxa.test;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URISyntaxException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
+import junit.framework.Assert;
+
+import org.junit.Test;
+import org.mozilla.gecko.background.fxa.FxAccountClient20;
+import org.mozilla.gecko.sync.net.BaseResource;
+
+public class TestFxAccountClient20 {
+ protected static class MockFxAccountClient20 extends FxAccountClient20 {
+ public MockFxAccountClient20(String serverURI, Executor executor) {
+ super(serverURI, executor);
+ }
+
+ // Public for testing.
+ @Override
+ public BaseResource getBaseResource(final String path, final String... queryParameters) throws UnsupportedEncodingException, URISyntaxException {
+ return super.getBaseResource(path, queryParameters);
+ }
+ }
+
+ @Test
+ public void testGetCreateAccountURI() throws Exception {
+ final String TEST_SERVER = "https://test.com:4430/inner/v1/";
+ final MockFxAccountClient20 client = new MockFxAccountClient20(TEST_SERVER, Executors.newSingleThreadExecutor());
+ Assert.assertEquals(TEST_SERVER + "account/create", client.getBaseResource("account/create").getURIString());
+ Assert.assertEquals(TEST_SERVER + "account/create?service=sync&keys=true", client.getBaseResource("account/create", "service", "sync", "keys", "true").getURIString());
+ Assert.assertEquals(TEST_SERVER + "account/create?service=two+words", client.getBaseResource("account/create", "service", "two words").getURIString());
+ Assert.assertEquals(TEST_SERVER + "account/create?service=symbols%2F%3A%3F%2B", client.getBaseResource("account/create", "service", "symbols/:?+").getURIString());
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/fxa/test/TestFxAccountUtils.java
@@ -0,0 +1,128 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.fxa.test;
+
+import java.math.BigInteger;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.mozilla.apache.commons.codec.binary.Base64;
+import org.mozilla.gecko.background.fxa.FxAccountUtils;
+import org.mozilla.gecko.sync.Utils;
+import org.mozilla.gecko.sync.crypto.KeyBundle;
+import org.mozilla.gecko.sync.net.SRPConstants;
+
+/**
+ * Test vectors from
+ * <a href="https://wiki.mozilla.org/Identity/AttachedServices/KeyServerProtocol#stretch-KDF">https://wiki.mozilla.org/Identity/AttachedServices/KeyServerProtocol#stretch-KDF</a>
+ * and
+ * <a href="https://github.com/mozilla/fxa-auth-server/wiki/onepw-protocol/5a9bc81e499306d769ca19b40b50fa60123df15d">https://github.com/mozilla/fxa-auth-server/wiki/onepw-protocol/5a9bc81e499306d769ca19b40b50fa60123df15d</a>.
+ */
+public class TestFxAccountUtils {
+ protected static void assertEncoding(String base16String, String utf8String) throws Exception {
+ Assert.assertEquals(base16String, FxAccountUtils.bytes(utf8String));
+ }
+
+ @Test
+ public void testUTF8Encoding() throws Exception {
+ assertEncoding("616e6472c3a9406578616d706c652e6f7267", "andré@example.org");
+ assertEncoding("70c3a4737377c3b67264", "pässwörd");
+ }
+
+ @Test
+ public void testHexModN() {
+ BigInteger N = BigInteger.valueOf(14);
+ Assert.assertEquals(4, N.bitLength());
+ Assert.assertEquals(1, (N.bitLength() + 7)/8);
+ Assert.assertEquals("00", FxAccountUtils.hexModN(BigInteger.valueOf(0), N));
+ Assert.assertEquals("05", FxAccountUtils.hexModN(BigInteger.valueOf(5), N));
+ Assert.assertEquals("0b", FxAccountUtils.hexModN(BigInteger.valueOf(11), N));
+ Assert.assertEquals("00", FxAccountUtils.hexModN(BigInteger.valueOf(14), N));
+ Assert.assertEquals("01", FxAccountUtils.hexModN(BigInteger.valueOf(15), N));
+ Assert.assertEquals("02", FxAccountUtils.hexModN(BigInteger.valueOf(16), N));
+ Assert.assertEquals("02", FxAccountUtils.hexModN(BigInteger.valueOf(30), N));
+
+ N = BigInteger.valueOf(260);
+ Assert.assertEquals("00ff", FxAccountUtils.hexModN(BigInteger.valueOf(255), N));
+ Assert.assertEquals("0100", FxAccountUtils.hexModN(BigInteger.valueOf(256), N));
+ Assert.assertEquals("0101", FxAccountUtils.hexModN(BigInteger.valueOf(257), N));
+ Assert.assertEquals("0001", FxAccountUtils.hexModN(BigInteger.valueOf(261), N));
+ }
+
+ @Test
+ public void testSRPVerifierFunctions() throws Exception {
+ byte[] emailUTF8Bytes = Utils.hex2Byte("616e6472c3a9406578616d706c652e6f7267");
+ byte[] srpPWBytes = Utils.hex2Byte("00f9b71800ab5337d51177d8fbc682a3653fa6dae5b87628eeec43a18af59a9d", 32);
+ byte[] srpSaltBytes = Utils.hex2Byte("00f1000000000000000000000000000000000000000000000000000000000179", 32);
+
+ String expectedX = "81925186909189958012481408070938147619474993903899664126296984459627523279550";
+ BigInteger x = FxAccountUtils.srpVerifierLowercaseX(emailUTF8Bytes, srpPWBytes, srpSaltBytes);
+ Assert.assertEquals(expectedX, x.toString(10));
+
+ String expectedV = "11464957230405843056840989945621595830717843959177257412217395741657995431613430369165714029818141919887853709633756255809680435884948698492811770122091692817955078535761033207000504846365974552196983218225819721112680718485091921646083608065626264424771606096544316730881455897489989950697705196721477608178869100211706638584538751009854562396937282582855620488967259498367841284829152987988548996842770025110751388952323221706639434861071834212055174768483159061566055471366772641252573641352721966728239512914666806496255304380341487975080159076396759492553066357163103546373216130193328802116982288883318596822";
+ BigInteger v = FxAccountUtils.srpVerifierLowercaseV(emailUTF8Bytes, srpPWBytes, srpSaltBytes, SRPConstants._2048.g, SRPConstants._2048.N);
+ Assert.assertEquals(expectedV, v.toString(10));
+
+ String expectedVHex = "00173ffa0263e63ccfd6791b8ee2a40f048ec94cd95aa8a3125726f9805e0c8283c658dc0b607fbb25db68e68e93f2658483049c68af7e8214c49fde2712a775b63e545160d64b00189a86708c69657da7a1678eda0cd79f86b8560ebdb1ffc221db360eab901d643a75bf1205070a5791230ae56466b8c3c1eb656e19b794f1ea0d2a077b3a755350208ea0118fec8c4b2ec344a05c66ae1449b32609ca7189451c259d65bd15b34d8729afdb5faff8af1f3437bbdc0c3d0b069a8ab2a959c90c5a43d42082c77490f3afcc10ef5648625c0605cdaace6c6fdc9e9a7e6635d619f50af7734522470502cab26a52a198f5b00a279858916507b0b4e9ef9524d6";
+ Assert.assertEquals(expectedVHex, FxAccountUtils.hexModN(v, SRPConstants._2048.N));
+ }
+
+ @Test
+ public void testGenerateSyncKeyBundle() throws Exception {
+ byte[] kB = Utils.hex2Byte("d02d8fe39f28b601159c543f2deeb8f72bdf2043e8279aa08496fbd9ebaea361");
+ KeyBundle bundle = FxAccountUtils.generateSyncKeyBundle(kB);
+ Assert.assertEquals("rsLwECkgPYeGbYl92e23FskfIbgld9TgeifEaB9ZwTI=", Base64.encodeBase64String(bundle.getEncryptionKey()));
+ Assert.assertEquals("fs75EseCD/VOLodlIGmwNabBjhTYBHFCe7CGIf0t8Tw=", Base64.encodeBase64String(bundle.getHMACKey()));
+ }
+
+ @Test
+ public void testGeneration() throws Exception {
+ byte[] quickStretchedPW = FxAccountUtils.generateQuickStretchedPW(
+ Utils.hex2Byte("616e6472c3a9406578616d706c652e6f7267"),
+ Utils.hex2Byte("70c3a4737377c3b67264"));
+ Assert.assertEquals("e4e8889bd8bd61ad6de6b95c059d56e7b50dacdaf62bd84644af7e2add84345d",
+ Utils.byte2Hex(quickStretchedPW));
+ Assert.assertEquals("247b675ffb4c46310bc87e26d712153abe5e1c90ef00a4784594f97ef54f2375",
+ Utils.byte2Hex(FxAccountUtils.generateAuthPW(quickStretchedPW)));
+ byte[] unwrapkB = FxAccountUtils.generateUnwrapBKey(quickStretchedPW);
+ Assert.assertEquals("de6a2648b78284fcb9ffa81ba95803309cfba7af583c01a8a1a63e567234dd28",
+ Utils.byte2Hex(unwrapkB));
+ byte[] wrapkB = Utils.hex2Byte("7effe354abecbcb234a8dfc2d7644b4ad339b525589738f2d27341bb8622ecd8");
+ Assert.assertEquals("a095c51c1c6e384e8d5777d97e3c487a4fc2128a00ab395a73d57fedf41631f0",
+ Utils.byte2Hex(FxAccountUtils.unwrapkB(unwrapkB, wrapkB)));
+ }
+
+ @Test
+ public void testClientState() throws Exception {
+ final String hexKB = "fd5c747806c07ce0b9d69dcfea144663e630b65ec4963596a22f24910d7dd15d";
+ final byte[] byteKB = Utils.hex2Byte(hexKB);
+ final String clientState = FxAccountUtils.computeClientState(byteKB);
+ final String expected = "6ae94683571c7a7c54dab4700aa3995f";
+ Assert.assertEquals(expected, clientState);
+ }
+
+ @Test
+ public void testGetAudienceForURL() throws Exception {
+ // Sub-domains and path components.
+ Assert.assertEquals("http://sub.test.com", FxAccountUtils.getAudienceForURL("http://sub.test.com"));
+ Assert.assertEquals("http://test.com", FxAccountUtils.getAudienceForURL("http://test.com/"));
+ Assert.assertEquals("http://test.com", FxAccountUtils.getAudienceForURL("http://test.com/path/component"));
+ Assert.assertEquals("http://test.com", FxAccountUtils.getAudienceForURL("http://test.com/path/component/"));
+
+ // No port and default port.
+ Assert.assertEquals("http://test.com", FxAccountUtils.getAudienceForURL("http://test.com"));
+ Assert.assertEquals("http://test.com:80", FxAccountUtils.getAudienceForURL("http://test.com:80"));
+
+ Assert.assertEquals("https://test.com", FxAccountUtils.getAudienceForURL("https://test.com"));
+ Assert.assertEquals("https://test.com:443", FxAccountUtils.getAudienceForURL("https://test.com:443"));
+
+ // Ports that are the default ports for a different scheme.
+ Assert.assertEquals("https://test.com:80", FxAccountUtils.getAudienceForURL("https://test.com:80"));
+ Assert.assertEquals("http://test.com:443", FxAccountUtils.getAudienceForURL("http://test.com:443"));
+
+ // Arbitrary ports.
+ Assert.assertEquals("http://test.com:8080", FxAccountUtils.getAudienceForURL("http://test.com:8080"));
+ Assert.assertEquals("https://test.com:4430", FxAccountUtils.getAudienceForURL("https://test.com:4430"));
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/healthreport/prune/test/TestPrunePolicy.java
@@ -0,0 +1,324 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.healthreport.prune.test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mozilla.gecko.background.healthreport.HealthReportConstants;
+import org.mozilla.gecko.background.healthreport.prune.PrunePolicy;
+import org.mozilla.gecko.background.healthreport.prune.PrunePolicyStorage;
+import org.mozilla.gecko.background.testhelpers.MockSharedPreferences;
+
+import android.content.SharedPreferences;
+
+public class TestPrunePolicy {
+ public static class MockPrunePolicy extends PrunePolicy {
+ public MockPrunePolicy(final PrunePolicyStorage storage, final SharedPreferences sharedPrefs) {
+ super(storage, sharedPrefs);
+ }
+
+ @Override
+ public boolean attemptPruneBySize(final long time) {
+ return super.attemptPruneBySize(time);
+ }
+
+ @Override
+ public boolean attemptExpiration(final long time) {
+ return super.attemptExpiration(time);
+ }
+
+ @Override
+ protected boolean attemptStorageCleanup(final long time) {
+ return super.attemptStorageCleanup(time);
+ }
+ }
+
+ public static class MockPrunePolicyStorage implements PrunePolicyStorage {
+ public int eventCount = -1;
+ public int environmentCount = -1;
+
+ // TODO: Spies - should we be using a framework?
+ // TODO: Each method was called with what args?
+ public boolean wasPruneEventsCalled = false;
+ public boolean wasPruneEnvironmentsCalled = false;
+ public boolean wasDeleteDataBeforeCalled = false;
+ public boolean wasCleanupCalled = false;
+
+ public MockPrunePolicyStorage() { }
+
+ public void pruneEvents(final int maxNumToPrune) {
+ wasPruneEventsCalled = true;
+ }
+
+ public void pruneEnvironments(final int numToPrune) {
+ wasPruneEnvironmentsCalled = true;
+ }
+
+ public int deleteDataBefore(final long time) {
+ wasDeleteDataBeforeCalled = true;
+ return -1;
+ }
+
+ public void cleanup() {
+ wasCleanupCalled = true;
+ }
+
+ public int getEventCount() { return eventCount; }
+ public int getEnvironmentCount() { return environmentCount; }
+
+ public void close() { /* Nothing to cleanup. */ }
+ }
+
+ // An arbitrary value so that each test doesn't need to specify its own time.
+ public static final long START_TIME = 1000;
+
+ public MockPrunePolicy policy;
+ public MockPrunePolicyStorage storage;
+ public SharedPreferences sharedPrefs;
+
+ @Before
+ public void setUp() throws Exception {
+ sharedPrefs = new MockSharedPreferences();
+ storage = new MockPrunePolicyStorage();
+ policy = new MockPrunePolicy(storage, sharedPrefs);
+ }
+
+ public boolean attemptPruneBySize(final long time) {
+ final boolean retval = policy.attemptPruneBySize(time);
+ // This commit may be deferred over multiple methods so we ensure it runs.
+ sharedPrefs.edit().commit();
+ return retval;
+ }
+
+ public boolean attemptExpiration(final long time) {
+ final boolean retval = policy.attemptExpiration(time);
+ // This commit may be deferred over multiple methods so we ensure it runs.
+ sharedPrefs.edit().commit();
+ return retval;
+ }
+
+ public boolean attemptStorageCleanup(final long time) {
+ final boolean retval = policy.attemptStorageCleanup(time);
+ // This commit may be deferred over multiple methods so we ensure it runs.
+ sharedPrefs.edit().commit();
+ return retval;
+ }
+
+ @Test
+ public void testAttemptPruneBySizeInit() throws Exception {
+ assertFalse(containsNextPruneBySizeTime());
+ attemptPruneBySize(START_TIME);
+
+ // Next time should be initialized.
+ assertTrue(containsNextPruneBySizeTime());
+ assertTrue(getNextPruneBySizeTime() > 0);
+ assertFalse(storage.wasPruneEventsCalled);
+ assertFalse(storage.wasPruneEnvironmentsCalled);
+ }
+
+ @Test
+ public void testAttemptPruneBySizeEarly() throws Exception {
+ final long nextTime = START_TIME + 1;
+ setNextPruneBySizeTime(nextTime);
+ attemptPruneBySize(START_TIME);
+
+ // We didn't prune so next time remains the same.
+ assertEquals(nextTime, getNextPruneBySizeTime());
+ assertFalse(storage.wasPruneEventsCalled);
+ assertFalse(storage.wasPruneEnvironmentsCalled);
+ }
+
+ @Test
+ public void testAttemptPruneBySizeSkewed() throws Exception {
+ setNextPruneBySizeTime(START_TIME + getMinimumTimeBetweenPruneBySizeChecks() + 1);
+ attemptPruneBySize(START_TIME);
+
+ // Skewed so the next time is reset.
+ assertEquals(START_TIME + getMinimumTimeBetweenPruneBySizeChecks(), getNextPruneBySizeTime());
+ assertFalse(storage.wasPruneEventsCalled);
+ assertFalse(storage.wasPruneEnvironmentsCalled);
+ }
+
+ @Test
+ public void testAttemptPruneBySizeTooFewEnvironments() throws Exception {
+ setNextPruneBySizeTime(START_TIME - 1);
+ storage.environmentCount = getMaximumEnvironmentCount(); // Too few to prune.
+ attemptPruneBySize(START_TIME);
+
+ assertEquals(START_TIME + getMinimumTimeBetweenPruneBySizeChecks(), getNextPruneBySizeTime());
+ assertFalse(storage.wasPruneEnvironmentsCalled);
+ }
+
+ @Test
+ public void testAttemptPruneBySizeEnvironmentsSuccess() throws Exception {
+ setNextPruneBySizeTime(START_TIME - 1);
+ storage.environmentCount = getMaximumEnvironmentCount() + 1;
+ attemptPruneBySize(START_TIME);
+
+ assertEquals(START_TIME + getMinimumTimeBetweenPruneBySizeChecks(), getNextPruneBySizeTime());
+ assertTrue(storage.wasPruneEnvironmentsCalled);
+ }
+
+ @Test
+ public void testAttemptPruneBySizeTooFewEvents() throws Exception {
+ setNextPruneBySizeTime(START_TIME - 1);
+ storage.eventCount = getMaximumEventCount(); // Too few to prune.
+ attemptPruneBySize(START_TIME);
+
+ assertEquals(START_TIME + getMinimumTimeBetweenPruneBySizeChecks(), getNextPruneBySizeTime());
+ assertFalse(storage.wasPruneEventsCalled);
+ }
+
+ @Test
+ public void testAttemptPruneBySizeEventsSuccess() throws Exception {
+ setNextPruneBySizeTime(START_TIME - 1);
+ storage.eventCount = getMaximumEventCount() + 1;
+ attemptPruneBySize(START_TIME);
+
+ assertEquals(START_TIME + getMinimumTimeBetweenPruneBySizeChecks(), getNextPruneBySizeTime());
+ assertTrue(storage.wasPruneEventsCalled);
+ }
+
+ @Test
+ public void testAttemptExpirationInit() throws Exception {
+ assertFalse(containsNextExpirationTime());
+ attemptExpiration(START_TIME);
+
+ // Next time should be initialized.
+ assertTrue(containsNextExpirationTime());
+ assertTrue(getNextExpirationTime() > 0);
+ assertFalse(storage.wasDeleteDataBeforeCalled);
+ }
+
+ @Test
+ public void testAttemptExpirationEarly() throws Exception {
+ final long nextTime = START_TIME + 1;
+ setNextExpirationTime(nextTime);
+ attemptExpiration(START_TIME);
+
+ // We didn't prune so next time remains the same.
+ assertEquals(nextTime, getNextExpirationTime());
+ assertFalse(storage.wasDeleteDataBeforeCalled);
+ }
+
+ @Test
+ public void testAttemptExpirationSkewed() throws Exception {
+ setNextExpirationTime(START_TIME + getMinimumTimeBetweenExpirationChecks() + 1);
+ attemptExpiration(START_TIME);
+
+ // Skewed so the next time is reset.
+ assertEquals(START_TIME + getMinimumTimeBetweenExpirationChecks(), getNextExpirationTime());
+ assertFalse(storage.wasDeleteDataBeforeCalled);
+ }
+
+ @Test
+ public void testAttemptExpirationSuccess() throws Exception {
+ setNextExpirationTime(START_TIME - 1);
+ attemptExpiration(START_TIME);
+
+ assertEquals(START_TIME + getMinimumTimeBetweenExpirationChecks(), getNextExpirationTime());
+ assertTrue(storage.wasDeleteDataBeforeCalled);
+ }
+
+ @Test
+ public void testAttemptCleanupInit() throws Exception {
+ assertFalse(containsNextCleanupTime());
+ attemptStorageCleanup(START_TIME);
+
+ // Next time should be initialized.
+ assertTrue(containsNextCleanupTime());
+ assertTrue(getNextCleanupTime() > 0);
+ assertFalse(storage.wasCleanupCalled);
+ }
+
+ @Test
+ public void testAttemptCleanupEarly() throws Exception {
+ final long nextTime = START_TIME + 1;
+ setNextCleanupTime(nextTime);
+ attemptStorageCleanup(START_TIME);
+
+ // We didn't prune so next time remains the same.
+ assertEquals(nextTime, getNextCleanupTime());
+ assertFalse(storage.wasCleanupCalled);
+ }
+
+ @Test
+ public void testAttemptCleanupSkewed() throws Exception {
+ setNextCleanupTime(START_TIME + getMinimumTimeBetweenCleanupChecks() + 1);
+ attemptStorageCleanup(START_TIME);
+
+ // Skewed so the next time is reset.
+ assertEquals(START_TIME + getMinimumTimeBetweenCleanupChecks(), getNextCleanupTime());
+ assertFalse(storage.wasCleanupCalled);
+ }
+
+ @Test
+ public void testAttemptCleanupSuccess() throws Exception {
+ setNextCleanupTime(START_TIME - 1);
+ attemptStorageCleanup(START_TIME);
+
+ assertEquals(START_TIME + getMinimumTimeBetweenCleanupChecks(), getNextCleanupTime());
+ assertTrue(storage.wasCleanupCalled);
+ }
+
+ public int getMaximumEnvironmentCount() {
+ return HealthReportConstants.MAX_ENVIRONMENT_COUNT;
+ }
+
+ public int getMaximumEventCount() {
+ return HealthReportConstants.MAX_EVENT_COUNT;
+ }
+
+ public long getMinimumTimeBetweenPruneBySizeChecks() {
+ return HealthReportConstants.MINIMUM_TIME_BETWEEN_PRUNE_BY_SIZE_CHECKS_MILLIS;
+ }
+
+ public long getNextPruneBySizeTime() {
+ return sharedPrefs.getLong(HealthReportConstants.PREF_PRUNE_BY_SIZE_TIME, -1);
+ }
+
+ public void setNextPruneBySizeTime(final long time) {
+ sharedPrefs.edit().putLong(HealthReportConstants.PREF_PRUNE_BY_SIZE_TIME, time).commit();
+ }
+
+ public boolean containsNextPruneBySizeTime() {
+ return sharedPrefs.contains(HealthReportConstants.PREF_PRUNE_BY_SIZE_TIME);
+ }
+
+ public long getMinimumTimeBetweenExpirationChecks() {
+ return HealthReportConstants.MINIMUM_TIME_BETWEEN_EXPIRATION_CHECKS_MILLIS;
+ }
+
+ public long getNextExpirationTime() {
+ return sharedPrefs.getLong(HealthReportConstants.PREF_EXPIRATION_TIME, -1);
+ }
+
+ public void setNextExpirationTime(final long time) {
+ sharedPrefs.edit().putLong(HealthReportConstants.PREF_EXPIRATION_TIME, time).commit();
+ }
+
+ public boolean containsNextExpirationTime() {
+ return sharedPrefs.contains(HealthReportConstants.PREF_EXPIRATION_TIME);
+ }
+
+ public long getMinimumTimeBetweenCleanupChecks() {
+ return HealthReportConstants.MINIMUM_TIME_BETWEEN_CLEANUP_CHECKS_MILLIS;
+ }
+
+ public long getNextCleanupTime() {
+ return sharedPrefs.getLong(HealthReportConstants.PREF_CLEANUP_TIME, -1);
+ }
+
+ public void setNextCleanupTime(final long time) {
+ sharedPrefs.edit().putLong(HealthReportConstants.PREF_CLEANUP_TIME, time).commit();
+ }
+
+ public boolean containsNextCleanupTime() {
+ return sharedPrefs.contains(HealthReportConstants.PREF_CLEANUP_TIME);
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/healthreport/test/HealthReportStorageStub.java
@@ -0,0 +1,64 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.healthreport.test;
+
+import org.json.JSONObject;
+
+import android.database.Cursor;
+import android.util.SparseArray;
+
+import org.mozilla.gecko.background.healthreport.Environment;
+import org.mozilla.gecko.background.healthreport.HealthReportStorage;
+
+public class HealthReportStorageStub implements HealthReportStorage {
+ public void close() { throw new UnsupportedOperationException(); }
+
+ public int getDay(long time) { throw new UnsupportedOperationException(); }
+ public int getDay() { throw new UnsupportedOperationException(); }
+
+ public Environment getEnvironment() { throw new UnsupportedOperationException(); }
+ public SparseArray<String> getEnvironmentHashesByID() { throw new UnsupportedOperationException(); }
+ public SparseArray<Environment> getEnvironmentRecordsByID() { throw new UnsupportedOperationException(); }
+ public Cursor getEnvironmentRecordForID(int id) { throw new UnsupportedOperationException(); }
+
+ public Field getField(String measurement, int measurementVersion, String fieldName) {
+ throw new UnsupportedOperationException();
+ }
+ public SparseArray<Field> getFieldsByID() { throw new UnsupportedOperationException(); }
+
+ public void recordDailyLast(int env, int day, int field, JSONObject value) { throw new UnsupportedOperationException(); }
+ public void recordDailyLast(int env, int day, int field, String value) { throw new UnsupportedOperationException(); }
+ public void recordDailyLast(int env, int day, int field, int value) { throw new UnsupportedOperationException(); }
+ public void recordDailyDiscrete(int env, int day, int field, JSONObject value) { throw new UnsupportedOperationException(); }
+ public void recordDailyDiscrete(int env, int day, int field, String value) { throw new UnsupportedOperationException(); }
+ public void recordDailyDiscrete(int env, int day, int field, int value) { throw new UnsupportedOperationException(); }
+ public void incrementDailyCount(int env, int day, int field, int by) { throw new UnsupportedOperationException(); }
+ public void incrementDailyCount(int env, int day, int field) { throw new UnsupportedOperationException(); }
+
+ public boolean hasEventSince(long time) { throw new UnsupportedOperationException(); }
+
+ public Cursor getRawEventsSince(long time) { throw new UnsupportedOperationException(); }
+
+ public Cursor getEventsSince(long time) { throw new UnsupportedOperationException(); }
+
+ public void ensureMeasurementInitialized(String measurement, int version, MeasurementFields fields) {
+ throw new UnsupportedOperationException();
+ }
+ public Cursor getMeasurementVersions() { throw new UnsupportedOperationException(); }
+ public Cursor getFieldVersions() { throw new UnsupportedOperationException(); }
+ public Cursor getFieldVersions(String measurement, int measurementVersion) { throw new UnsupportedOperationException(); }
+
+ public void deleteEverything() { throw new UnsupportedOperationException(); }
+ public void deleteEnvironments() { throw new UnsupportedOperationException(); }
+ public void deleteMeasurements() { throw new UnsupportedOperationException(); }
+ public int deleteDataBefore(final long time, final int curEnv) { throw new UnsupportedOperationException(); }
+
+ public int getEventCount() { throw new UnsupportedOperationException(); }
+ public int getEnvironmentCount() { throw new UnsupportedOperationException(); }
+
+ public void pruneEvents(final int num) { throw new UnsupportedOperationException(); }
+ public void pruneEnvironments(final int num) { throw new UnsupportedOperationException(); }
+
+ public void enqueueOperation(Runnable runnable) { throw new UnsupportedOperationException(); }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/healthreport/upload/test/MockAndroidSubmissionClient.java
@@ -0,0 +1,117 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.healthreport.upload.test;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+
+import org.mozilla.gecko.background.healthreport.Environment;
+import org.mozilla.gecko.background.healthreport.Environment.UIType;
+import org.mozilla.gecko.background.healthreport.EnvironmentBuilder.ConfigurationProvider;
+import org.mozilla.gecko.background.healthreport.HealthReportStorage;
+import org.mozilla.gecko.background.healthreport.ProfileInformationCache;
+import org.mozilla.gecko.background.healthreport.test.HealthReportStorageStub;
+import org.mozilla.gecko.background.healthreport.upload.AndroidSubmissionClient;
+
+public class MockAndroidSubmissionClient extends AndroidSubmissionClient {
+ public MockAndroidSubmissionClient(final Context context, final SharedPreferences prefs,
+ final String profilePath) {
+ super(context, prefs, profilePath, new MockConfigurationProvider());
+ }
+
+ public static class MockConfigurationProvider implements ConfigurationProvider {
+ @Override
+ public boolean hasHardwareKeyboard() {
+ return false;
+ }
+
+ @Override
+ public UIType getUIType() {
+ return UIType.DEFAULT;
+ }
+
+ @Override
+ public int getUIModeType() {
+ return 0;
+ }
+
+ @Override
+ public int getScreenLayoutSize() {
+ return 0;
+ }
+
+ @Override
+ public int getScreenXInMM() {
+ return 100;
+ }
+
+ @Override
+ public int getScreenYInMM() {
+ return 140;
+ }
+
+ }
+
+ public class MockSubmissionsTracker extends SubmissionsTracker {
+ public MockSubmissionsTracker(final HealthReportStorage storage, final long localTime,
+ final boolean hasUploadBeenRequested) {
+ super(storage, localTime, hasUploadBeenRequested);
+ }
+
+ // Override so we don't touch storage to register the current env.
+ // The returned id value does not matter much.
+ @Override
+ public int registerCurrentEnvironment() {
+ return 0;
+ }
+
+ // Override so we don't touch storage to get cache. Only getCurrentEnviroment uses the cache,
+ // which we override, so we're free to return null.
+ @Override
+ public ProfileInformationCache getProfileInformationCache() {
+ return null;
+ }
+
+ @Override
+ public TrackingGenerator getGenerator() {
+ return new MockTrackingGenerator();
+ }
+
+ public class MockTrackingGenerator extends TrackingGenerator {
+ // Override so it doesn't fail in the constructor when touching the storage stub (below).
+ @Override
+ protected Environment getCurrentEnvironment() {
+ return new Environment() {
+ @Override
+ public int register() {
+ return 0;
+ }
+ };
+ }
+ }
+ }
+
+ /**
+ * Mocked HealthReportStorage class for use within the MockAndroidSubmissionClient and its inner
+ * classes, to prevent access to real storage.
+ */
+ public static class MockHealthReportStorage extends HealthReportStorageStub {
+ // Ensures a unique Field ID for SubmissionsFieldName.getID().
+ @Override
+ public Field getField(String mName, int mVersion, String fieldName) {
+ return new Field(mName, mVersion, fieldName, 0) {
+ @Override
+ public int getID() throws IllegalStateException {
+ return fieldName.hashCode();
+ }
+ };
+ }
+
+ // Called in the SubmissionsTracker constructor.
+ @Override
+ public int getDay(final long millis) {
+ return 0;
+ }
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/healthreport/upload/test/TestObsoleteDocumentTracker.java
@@ -0,0 +1,174 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.healthreport.upload.test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.util.AbstractMap.SimpleImmutableEntry;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map.Entry;
+import java.util.Set;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mozilla.gecko.background.healthreport.HealthReportConstants;
+import org.mozilla.gecko.background.healthreport.upload.ObsoleteDocumentTracker;
+import org.mozilla.gecko.background.testhelpers.MockSharedPreferences;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+
+import android.content.SharedPreferences;
+
+public class TestObsoleteDocumentTracker {
+ public static class MockObsoleteDocumentTracker extends ObsoleteDocumentTracker {
+ public MockObsoleteDocumentTracker(SharedPreferences sharedPrefs) {
+ super(sharedPrefs);
+ }
+
+ @Override
+ public ExtendedJSONObject getObsoleteIds() {
+ return super.getObsoleteIds();
+ }
+
+ @Override
+ public void setObsoleteIds(ExtendedJSONObject ids) {
+ super.setObsoleteIds(ids);
+ }
+};
+ public MockObsoleteDocumentTracker tracker;
+ public MockSharedPreferences sharedPrefs;
+
+ @Before
+ public void setUp() {
+ sharedPrefs = new MockSharedPreferences();
+ tracker = new MockObsoleteDocumentTracker(sharedPrefs);
+ }
+
+ @Test
+ public void testDecrementObsoleteIdAttempts() {
+ ExtendedJSONObject ids = new ExtendedJSONObject();
+ ids.put("id1", HealthReportConstants.DELETION_ATTEMPTS_PER_OBSOLETE_DOCUMENT_ID);
+ ids.put("id2", HealthReportConstants.DELETION_ATTEMPTS_PER_OBSOLETE_DOCUMENT_ID);
+ tracker.setObsoleteIds(ids);
+ assertEquals(ids, tracker.getObsoleteIds());
+
+ tracker.decrementObsoleteIdAttempts("id1");
+ ids.put("id1", HealthReportConstants.DELETION_ATTEMPTS_PER_OBSOLETE_DOCUMENT_ID - 1);
+ assertEquals(ids, tracker.getObsoleteIds());
+
+ for (int i = 0; i < HealthReportConstants.DELETION_ATTEMPTS_PER_OBSOLETE_DOCUMENT_ID; i++) {
+ tracker.decrementObsoleteIdAttempts("id1");
+ }
+ ids.remove("id1");
+ assertEquals(ids, tracker.getObsoleteIds());
+
+ tracker.removeObsoleteId("id2");
+ ids.remove("id2");
+ assertEquals(ids, tracker.getObsoleteIds());
+ }
+
+ @Test
+ public void testAddObsoleteId() {
+ ExtendedJSONObject ids = new ExtendedJSONObject();
+ ids.put("id1", HealthReportConstants.DELETION_ATTEMPTS_PER_OBSOLETE_DOCUMENT_ID);
+ tracker.addObsoleteId("id1");
+ assertEquals(ids, tracker.getObsoleteIds());
+ }
+
+ @Test
+ public void testDecrementObsoleteIdAttemptsSet() {
+ ExtendedJSONObject ids = new ExtendedJSONObject();
+ ids.put("id1", 5L);
+ ids.put("id2", 1L);
+ ids.put("id3", -1L); // This should never happen, but just in case.
+ tracker.setObsoleteIds(ids);
+ assertEquals(ids, tracker.getObsoleteIds());
+
+ HashSet<String> oldIds = new HashSet<String>();
+ oldIds.add("id1");
+ oldIds.add("id2");
+ tracker.decrementObsoleteIdAttempts(oldIds);
+ ids.put("id1", 4L);
+ ids.remove("id2");
+ assertEquals(ids, tracker.getObsoleteIds());
+ }
+
+ @Test
+ public void testMaximumObsoleteIds() {
+ for (int i = 1; i < HealthReportConstants.MAXIMUM_STORED_OBSOLETE_DOCUMENT_IDS + 10; i++) {
+ tracker.addObsoleteId("id" + i);
+ assertTrue(tracker.getObsoleteIds().size() <= HealthReportConstants.MAXIMUM_STORED_OBSOLETE_DOCUMENT_IDS);
+ }
+ }
+
+ @Test
+ public void testMigration() {
+ ExtendedJSONObject ids = new ExtendedJSONObject();
+ assertEquals(ids, tracker.getObsoleteIds());
+
+ sharedPrefs.edit()
+ .putString(HealthReportConstants.PREF_LAST_UPLOAD_DOCUMENT_ID, "id")
+ .commit();
+
+ ids.put("id", HealthReportConstants.DELETION_ATTEMPTS_PER_OBSOLETE_DOCUMENT_ID);
+ assertEquals(ids, tracker.getObsoleteIds());
+
+ assertTrue(sharedPrefs.contains(HealthReportConstants.PREF_OBSOLETE_DOCUMENT_IDS_TO_DELETION_ATTEMPTS_REMAINING));
+ }
+
+ @Test
+ public void testGetBatchOfObsoleteIds() {
+ ExtendedJSONObject ids = new ExtendedJSONObject();
+ for (int i = 0; i < 2 * HealthReportConstants.MAXIMUM_DELETIONS_PER_POST + 10; i++) {
+ ids.put("id" + (100 - i), Long.valueOf(100 - i));
+ }
+ tracker.setObsoleteIds(ids);
+
+ Set<String> expected = new HashSet<String>();
+ for (int i = 0; i < HealthReportConstants.MAXIMUM_DELETIONS_PER_POST; i++) {
+ expected.add("id" + (100 - i));
+ }
+ assertEquals(expected, new HashSet<String>(tracker.getBatchOfObsoleteIds()));
+ }
+
+ @Test
+ public void testPairComparator() {
+ // Make sure that malformed entries get sorted first.
+ ArrayList<Entry<String, Object>> list = new ArrayList<Entry<String,Object>>();
+ list.add(new SimpleImmutableEntry<String, Object>("a", null));
+ list.add(new SimpleImmutableEntry<String, Object>("d", Long.valueOf(5)));
+ list.add(new SimpleImmutableEntry<String, Object>("e", Long.valueOf(1)));
+ list.add(new SimpleImmutableEntry<String, Object>("c", Long.valueOf(10)));
+ list.add(new SimpleImmutableEntry<String, Object>("b", "test"));
+ Collections.sort(list, new ObsoleteDocumentTracker.PairComparator());
+
+ ArrayList<String> got = new ArrayList<String>();
+ for (Entry<String, Object> pair : list) {
+ got.add(pair.getKey());
+ }
+ List<String> exp = Arrays.asList(new String[] { "a", "b", "c", "d", "e" });
+ assertEquals(exp, got);
+ }
+
+ @Test
+ public void testLimitObsoleteIds() {
+ ExtendedJSONObject ids = new ExtendedJSONObject();
+ for (long i = -HealthReportConstants.DELETION_ATTEMPTS_PER_OBSOLETE_DOCUMENT_ID; i < HealthReportConstants.DELETION_ATTEMPTS_PER_OBSOLETE_DOCUMENT_ID; i++) {
+ long j = 1 + HealthReportConstants.DELETION_ATTEMPTS_PER_OBSOLETE_DOCUMENT_ID + i;
+ ids.put("id" + j, j);
+ }
+ tracker.setObsoleteIds(ids);
+ tracker.limitObsoleteIds();
+
+ assertEquals(ids.keySet(), tracker.getObsoleteIds().keySet());
+ ExtendedJSONObject got = tracker.getObsoleteIds();
+ for (String id : ids.keySet()) {
+ assertTrue(got.getLong(id).longValue() <= HealthReportConstants.DELETION_ATTEMPTS_PER_OBSOLETE_DOCUMENT_ID);
+ }
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/healthreport/upload/test/TestSubmissionPolicy.java
@@ -0,0 +1,435 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.background.healthreport.upload.test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.net.UnknownHostException;
+import java.util.Collection;
+import java.util.HashSet;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mozilla.gecko.background.healthreport.HealthReportConstants;
+import org.mozilla.gecko.background.healthreport.upload.SubmissionClient;
+import org.mozilla.gecko.background.healthreport.upload.SubmissionPolicy;
+import org.mozilla.gecko.background.healthreport.upload.test.TestObsoleteDocumentTracker.MockObsoleteDocumentTracker;
+import org.mozilla.gecko.background.healthreport.upload.test.TestSubmissionPolicy.MockSubmissionClient.Response;
+import org.mozilla.gecko.background.testhelpers.MockSharedPreferences;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+
+import android.content.SharedPreferences;
+
+public class TestSubmissionPolicy {
+ public static class MockSubmissionClient implements SubmissionClient {
+ public String lastId = null;
+ public Collection<String> lastOldIds = null;
+
+ public enum Response { SUCCESS, SOFT_FAILURE, HARD_FAILURE };
+ public Response upload = Response.SUCCESS;
+ public Response delete = Response.SUCCESS;
+ public Exception exception = null;
+
+ protected void response(long localTime, String id, Delegate delegate, Response response) {
+ lastId = id;
+ switch (response) {
+ case SOFT_FAILURE:
+ delegate.onSoftFailure(localTime, id, "Soft failure.", exception);
+ break;
+ case HARD_FAILURE:
+ delegate.onHardFailure(localTime, id, "Hard failure.", exception);
+ break;
+ default:
+ delegate.onSuccess(localTime, id);
+ }
+ }
+
+ @Override
+ public void upload(long localTime, String id, Collection<String> oldIds, Delegate delegate) {
+ lastOldIds = oldIds;
+ response(localTime, id, delegate, upload);
+ }
+
+ @Override
+ public void delete(long localTime, String id, Delegate delegate) {
+ response(localTime, id, delegate, delete);
+ }
+ }
+
+ public MockSubmissionClient client;
+ public SubmissionPolicy policy;
+ public SharedPreferences sharedPrefs;
+ public MockObsoleteDocumentTracker tracker;
+
+
+ public void setMinimumTimeBetweenUploads(long time) {
+ sharedPrefs.edit().putLong(HealthReportConstants.PREF_MINIMUM_TIME_BETWEEN_UPLOADS, time).commit();
+ }
+
+ public void setMinimumTimeBeforeFirstSubmission(long time) {
+ sharedPrefs.edit().putLong(HealthReportConstants.PREF_MINIMUM_TIME_BEFORE_FIRST_SUBMISSION, time).commit();
+ }
+
+ public void setCurrentDayFailureCount(long count) {
+ sharedPrefs.edit().putLong(HealthReportConstants.PREF_CURRENT_DAY_FAILURE_COUNT, count).commit();
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ sharedPrefs = new MockSharedPreferences();
+ client = new MockSubmissionClient();
+ tracker = new MockObsoleteDocumentTracker(sharedPrefs);
+ policy = new SubmissionPolicy(sharedPrefs, client, tracker, true);
+ setMinimumTimeBeforeFirstSubmission(0);
+ }
+
+ @Test
+ public void testNoMinimumTimeBeforeFirstSubmission() {
+ assertTrue(policy.tick(0));
+ }
+
+ @Test
+ public void testMinimumTimeBeforeFirstSubmission() {
+ setMinimumTimeBeforeFirstSubmission(10);
+ assertFalse(policy.tick(0));
+ assertEquals(policy.getMinimumTimeBeforeFirstSubmission(), policy.getNextSubmission());
+ assertFalse(policy.tick(policy.getMinimumTimeBeforeFirstSubmission() - 1));
+ assertTrue(policy.tick(policy.getMinimumTimeBeforeFirstSubmission()));
+ }
+
+ @Test
+ public void testNextUpload() {
+ assertTrue(policy.tick(0));
+ assertEquals(policy.getMinimumTimeBetweenUploads(), policy.getNextSubmission());
+ assertFalse(policy.tick(policy.getMinimumTimeBetweenUploads() - 1));
+ assertTrue(policy.tick(policy.getMinimumTimeBetweenUploads()));
+ }
+
+ @Test
+ public void testLastUploadRequested() {
+ assertTrue(policy.tick(0));
+ assertEquals(0, policy.getLastUploadRequested());
+ assertFalse(policy.tick(1));
+ assertEquals(0, policy.getLastUploadRequested());
+ assertTrue(policy.tick(2*policy.getMinimumTimeBetweenUploads()));
+ assertEquals(2*policy.getMinimumTimeBetweenUploads(), policy.getLastUploadRequested());
+ }
+
+ @Test
+ public void testUploadSuccess() throws Exception {
+ assertTrue(policy.tick(0));
+ setCurrentDayFailureCount(1);
+ client.upload = Response.SUCCESS;
+ assertTrue(policy.tick(2*policy.getMinimumTimeBetweenUploads()));
+ assertEquals(2*policy.getMinimumTimeBetweenUploads(), policy.getLastUploadRequested());
+ assertEquals(2*policy.getMinimumTimeBetweenUploads(), policy.getLastUploadSucceeded());
+ assertTrue(policy.getNextSubmission() > policy.getLastUploadSucceeded());
+ assertEquals(0, policy.getCurrentDayFailureCount());
+ assertNotNull(client.lastId);
+ assertTrue(tracker.getObsoleteIds().containsKey(client.lastId));
+ }
+
+ @Test
+ public void testUploadSoftFailure() throws Exception {
+ final long initialUploadTime = 0;
+ assertTrue(policy.tick(initialUploadTime));
+ client.upload = Response.SOFT_FAILURE;
+
+ final long secondUploadTime = initialUploadTime + policy.getMinimumTimeBetweenUploads();
+ assertTrue(policy.tick(secondUploadTime));
+ assertEquals(secondUploadTime, policy.getLastUploadRequested());
+ assertEquals(secondUploadTime, policy.getLastUploadFailed());
+ assertEquals(1, policy.getCurrentDayFailureCount());
+ assertEquals(policy.getLastUploadFailed() + policy.getMinimumTimeAfterFailure(), policy.getNextSubmission());
+ assertNotNull(client.lastId);
+ assertTrue(tracker.getObsoleteIds().containsKey(client.lastId));
+ client.lastId = null;
+
+ final long thirdUploadTime = secondUploadTime + policy.getMinimumTimeAfterFailure();
+ assertTrue(policy.tick(thirdUploadTime));
+ assertEquals(thirdUploadTime, policy.getLastUploadRequested());
+ assertEquals(thirdUploadTime, policy.getLastUploadFailed());
+ assertEquals(2, policy.getCurrentDayFailureCount());
+ assertEquals(policy.getLastUploadFailed() + policy.getMinimumTimeAfterFailure(), policy.getNextSubmission());
+ assertNotNull(client.lastId);
+ assertTrue(tracker.getObsoleteIds().containsKey(client.lastId));
+ client.lastId = null;
+
+ final long fourthUploadTime = thirdUploadTime + policy.getMinimumTimeAfterFailure();
+ assertTrue(policy.tick(fourthUploadTime));
+ assertEquals(fourthUploadTime, policy.getLastUploadRequested());
+ assertEquals(