diff options
author | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
---|---|---|
committer | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
commit | 5f8de423f190bbb79a62f804151bc24824fa32d8 (patch) | |
tree | 10027f336435511475e392454359edea8e25895d /mobile/android/tests/background/junit4/src | |
parent | 49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff) | |
download | UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip |
Add m-esr52 at 52.6.0
Diffstat (limited to 'mobile/android/tests/background/junit4/src')
171 files changed, 25365 insertions, 0 deletions
diff --git a/mobile/android/tests/background/junit4/src/com/keepsafe/switchboard/TestSwitchboard.java b/mobile/android/tests/background/junit4/src/com/keepsafe/switchboard/TestSwitchboard.java new file mode 100644 index 000000000..5726f12db --- /dev/null +++ b/mobile/android/tests/background/junit4/src/com/keepsafe/switchboard/TestSwitchboard.java @@ -0,0 +1,142 @@ +package com.keepsafe.switchboard; + +import android.content.Context; + +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.Experiments; +import org.mozilla.gecko.util.IOUtils; +import org.robolectric.RuntimeEnvironment; + +import java.io.BufferedInputStream; +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.URL; +import java.util.List; +import java.util.UUID; + +import static org.junit.Assert.*; + +@RunWith(TestRunner.class) +public class TestSwitchboard { + + /** + * Create a JSON response from a JSON file. + */ + private String readFromFile(String fileName) throws IOException { + URL url = getClass().getResource("/" + fileName); + if (url == null) { + throw new FileNotFoundException(fileName); + } + + InputStream inputStream = null; + ByteArrayOutputStream outputStream = null; + + try { + inputStream = new BufferedInputStream(new FileInputStream(url.getPath())); + InputStreamReader inputStreamReader = new InputStreamReader(inputStream); + BufferedReader bufferReader = new BufferedReader(inputStreamReader, 8192); + String line; + StringBuilder resultContent = new StringBuilder(); + while ((line = bufferReader.readLine()) != null) { + resultContent.append(line); + } + bufferReader.close(); + + return resultContent.toString(); + + } finally { + IOUtils.safeStreamClose(inputStream); + } + } + + @Before + public void setUp() throws IOException { + final Context c = RuntimeEnvironment.application; + Preferences.setDynamicConfigJson(c, readFromFile("experiments.json")); + } + + @Test + public void testDeviceUuidFactory() { + final Context c = RuntimeEnvironment.application; + final DeviceUuidFactory df = new DeviceUuidFactory(c); + final UUID uuid = df.getDeviceUuid(); + assertNotNull("UUID is not null", uuid); + assertEquals("DeviceUuidFactory always returns the same UUID", df.getDeviceUuid(), uuid); + } + + @Test + public void testIsInExperiment() { + final Context c = RuntimeEnvironment.application; + assertTrue("active-experiment is active", SwitchBoard.isInExperiment(c, "active-experiment")); + assertFalse("inactive-experiment is inactive", SwitchBoard.isInExperiment(c, "inactive-experiment")); + } + + @Test + public void testExperimentValues() throws JSONException { + final Context c = RuntimeEnvironment.application; + assertTrue("active-experiment has values", SwitchBoard.hasExperimentValues(c, "active-experiment")); + assertFalse("inactive-experiment doesn't have values", SwitchBoard.hasExperimentValues(c, "inactive-experiment")); + + final JSONObject values = SwitchBoard.getExperimentValuesFromJson(c, "active-experiment"); + assertNotNull("active-experiment values are not null", values); + assertTrue("\"foo\" extra value is true", values.getBoolean("foo")); + } + + @Test + public void testGetActiveExperiments() { + final Context c = RuntimeEnvironment.application; + final List<String> experiments = SwitchBoard.getActiveExperiments(c); + assertNotNull("List of active experiments is not null", experiments); + + assertTrue("List of active experiments contains active-experiment", experiments.contains("active-experiment")); + assertFalse("List of active experiments does not contain inactive-experiment", experiments.contains("inactive-experiment")); + } + + @Test + public void testOverride() { + final Context c = RuntimeEnvironment.application; + + Experiments.setOverride(c, "active-experiment", false); + assertFalse("active-experiment is not active because of override", SwitchBoard.isInExperiment(c, "active-experiment")); + assertFalse("List of active experiments does not contain active-experiment", SwitchBoard.getActiveExperiments(c).contains("active-experiment")); + + Experiments.clearOverride(c, "active-experiment"); + assertTrue("active-experiment is active after override is cleared", SwitchBoard.isInExperiment(c, "active-experiment")); + assertTrue("List of active experiments contains active-experiment again", SwitchBoard.getActiveExperiments(c).contains("active-experiment")); + + Experiments.setOverride(c, "inactive-experiment", true); + assertTrue("inactive-experiment is active because of override", SwitchBoard.isInExperiment(c, "inactive-experiment")); + assertTrue("List of active experiments contains inactive-experiment", SwitchBoard.getActiveExperiments(c).contains("inactive-experiment")); + + Experiments.clearOverride(c, "inactive-experiment"); + assertFalse("inactive-experiment is inactive after override is cleared", SwitchBoard.isInExperiment(c, "inactive-experiment")); + assertFalse("List of active experiments does not contain inactive-experiment again", SwitchBoard.getActiveExperiments(c).contains("inactive-experiment")); + } + + @Test + public void testMatching() { + final Context c = RuntimeEnvironment.application; + assertTrue("is-experiment is matching", SwitchBoard.isInExperiment(c, "is-matching")); + assertFalse("is-not-matching is not matching", SwitchBoard.isInExperiment(c, "is-not-matching")); + } + + @Test + public void testNotExisting() { + final Context c = RuntimeEnvironment.application; + assertFalse("F0O does not exists", SwitchBoard.isInExperiment(c, "F0O")); + assertFalse("BaAaz does not exists", SwitchBoard.hasExperimentValues(c, "BaAaz")); + } + + + +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestBackoff.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestBackoff.java new file mode 100644 index 000000000..8e8152e36 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestBackoff.java @@ -0,0 +1,114 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.android.sync.net.test; + +import ch.boye.httpclientandroidlib.HttpResponse; +import ch.boye.httpclientandroidlib.ProtocolVersion; +import ch.boye.httpclientandroidlib.message.BasicHttpResponse; +import ch.boye.httpclientandroidlib.message.BasicStatusLine; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.android.sync.test.helpers.MockGlobalSessionCallback; +import org.mozilla.gecko.background.testhelpers.MockGlobalSession; +import org.mozilla.gecko.background.testhelpers.MockSharedPreferences; +import org.mozilla.gecko.background.testhelpers.TestRunner; +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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +@RunWith(TestRunner.class) +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."); + } + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestBrowserIDAuthHeaderProvider.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestBrowserIDAuthHeaderProvider.java new file mode 100644 index 000000000..6a14c6d29 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestBrowserIDAuthHeaderProvider.java @@ -0,0 +1,23 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.android.sync.net.test; + +import ch.boye.httpclientandroidlib.Header; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.sync.net.BrowserIDAuthHeaderProvider; + +import static org.junit.Assert.assertEquals; + +@RunWith(TestRunner.class) +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()); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestClientsEngineStage.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestClientsEngineStage.java new file mode 100644 index 000000000..920cafb35 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestClientsEngineStage.java @@ -0,0 +1,806 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.android.sync.net.test; + +import ch.boye.httpclientandroidlib.HttpStatus; +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.junit.After; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.android.sync.test.helpers.HTTPServerTestHelper; +import org.mozilla.android.sync.test.helpers.MockGlobalSessionCallback; +import org.mozilla.android.sync.test.helpers.MockServer; +import org.mozilla.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.TestRunner; +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 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * Some tests in this class run client/server multi-threaded code but JUnit assertions triggered + * from background threads do not fail the test. If you see unexplained connection-related test failures, + * an assertion on the server may have been thrown. Unfortunately, it is non-trivial to get the background + * threads to transfer failures back to the test thread so we leave the tests in this state for now. + * + * One reason the server might throw an assertion is if you have not installed the crypto policies. See + * https://wiki.mozilla.org/Mobile/Fennec/Android/Testing#JUnit4_tests for more information. + */ +@RunWith(TestRunner.class) +public class TestClientsEngineStage extends MockSyncClientsEngineStage { + public final static String LOG_TAG = "TestClientsEngSta"; + + public TestClientsEngineStage() throws SyncConfigurationException, IllegalArgumentException, NonObjectJSONException, IOException, 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, CryptoException, URISyntaxException { + callback = new MockGlobalSessionCallback(); + 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, + 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 // client/server multi-threaded + 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, modifiedClientsToUpload.size()); + + final ClientRecord recordToUpload = modifiedClientsToUpload.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, modifiedClientsToUpload.size()); + + final ClientRecord recordToUpload = modifiedClientsToUpload.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()); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestCredentialsEndToEnd.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestCredentialsEndToEnd.java new file mode 100644 index 000000000..0f568a81e --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestCredentialsEndToEnd.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.net.test; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.TestRunner; +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 java.io.IOException; +import java.io.UnsupportedEncodingException; + +import ch.boye.httpclientandroidlib.Header; + +import static org.junit.Assert.assertEquals; + +/** + * Test the transfer of a UTF-8 string from desktop, and ensure that it results in the + * correct hashed Basic Auth header. + */ +@RunWith(TestRunner.class) +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 { + 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. +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestGlobalSession.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestGlobalSession.java new file mode 100644 index 000000000..c00da9b26 --- /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 ch.boye.httpclientandroidlib.HttpResponse; +import ch.boye.httpclientandroidlib.ProtocolVersion; +import ch.boye.httpclientandroidlib.message.BasicHttpResponse; +import ch.boye.httpclientandroidlib.message.BasicStatusLine; +import junit.framework.AssertionFailedError; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +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.TestRunner; +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 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 static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +@RunWith(TestRunner.class) +public class TestGlobalSession { + private int TEST_PORT = HTTPServerTestHelper.getTestPort(); + private final String TEST_CLUSTER_URL = "http://localhost:" + TEST_PORT; + private final String TEST_USERNAME = "johndoe"; + private final String TEST_PASSWORD = "password"; + private final String TEST_SYNC_KEY = "abcdeabcdeabcdeabcdeabcdea"; + private final long TEST_BACKOFF_IN_SECONDS = 2401; + + public static WaitHelper getTestWaiter() { + return WaitHelper.getTestWaiter(); + } + + @Test + public void testGetSyncStagesBy() throws SyncConfigurationException, IllegalArgumentException, NonObjectJSONException, IOException, 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(); + 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.setHeader("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(); + 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(); + + // 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, CryptoException { + MockServer server = new MockServer() { + @Override + public void handle(Request request, Response response) { + if (stageShouldBackoff) { + response.addValue("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(); + 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, + 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, + 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, + CryptoException { + MockGlobalSessionCallback callback = doTestSuccess(false, true); + + assertTrue(callback.calledSuccess); + assertFalse(callback.calledRequestBackoff); + } + + @Test + public void testOnSuccessNoBackoffAborted() throws SyncConfigurationException, + IllegalArgumentException, NonObjectJSONException, IOException, + 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.toJSONObject()); + } + 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); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestHeaderParsing.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestHeaderParsing.java new file mode 100644 index 000000000..532d60d13 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestHeaderParsing.java @@ -0,0 +1,29 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.android.sync.net.test; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.sync.Utils; + +import static org.junit.Assert.assertEquals; + +@RunWith(TestRunner.class) +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)); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestLineByLineHandling.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestLineByLineHandling.java new file mode 100644 index 000000000..b7f11adbf --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestLineByLineHandling.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.net.test; + +import org.junit.Test; +import org.junit.runner.RunWith; +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.TestRunner; +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; + +import java.io.IOException; +import java.io.PrintStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +@RunWith(TestRunner.class) +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. + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestMetaGlobal.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestMetaGlobal.java new file mode 100644 index 000000000..ec4c03859 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestMetaGlobal.java @@ -0,0 +1,347 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.android.sync.net.test; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.android.sync.test.helpers.HTTPServerTestHelper; +import org.mozilla.android.sync.test.helpers.MockServer; +import org.mozilla.gecko.background.testhelpers.TestRunner; +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.NonObjectJSONException; +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; + +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; + +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; + +@RunWith(TestRunner.class) +public class TestMetaGlobal { + 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(NonObjectJSONException.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 = new ExtendedJSONObject(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()); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestResource.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestResource.java new file mode 100644 index 000000000..e53d02d33 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestResource.java @@ -0,0 +1,102 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.android.sync.net.test; + +import ch.boye.httpclientandroidlib.HttpResponse; +import ch.boye.httpclientandroidlib.client.methods.HttpUriRequest; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.android.sync.test.helpers.HTTPServerTestHelper; +import org.mozilla.android.sync.test.helpers.MockResourceDelegate; +import org.mozilla.android.sync.test.helpers.MockServer; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.background.testhelpers.WaitHelper; +import org.mozilla.gecko.sync.net.BaseResource; +import org.mozilla.gecko.sync.net.HttpResponseObserver; + +import java.net.URISyntaxException; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +@RunWith(TestRunner.class) +public class 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(); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestRetryAfter.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestRetryAfter.java new file mode 100644 index 000000000..429ad29d4 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestRetryAfter.java @@ -0,0 +1,87 @@ +package org.mozilla.android.sync.net.test; + +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; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.sync.net.SyncResponse; + +import java.util.Date; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +@RunWith(TestRunner.class) +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()); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestServer11Repository.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestServer11Repository.java new file mode 100644 index 000000000..3aa0a91ec --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestServer11Repository.java @@ -0,0 +1,48 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.android.sync.net.test; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.sync.InfoCollections; +import org.mozilla.gecko.sync.InfoConfiguration; +import org.mozilla.gecko.sync.repositories.Server11Repository; + +import java.net.URI; +import java.net.URISyntaxException; + +@RunWith(TestRunner.class) +public class TestServer11Repository { + + private static final String COLLECTION = "bookmarks"; + private static final String COLLECTION_URL = "http://foo.com/1.1/n6ec3u5bee3tixzp2asys7bs6fve4jfw/storage"; + + protected final InfoCollections infoCollections = new InfoCollections(); + protected final InfoConfiguration infoConfiguration = new InfoConfiguration(); + + public static void assertQueryEquals(String expected, URI u) { + Assert.assertEquals(expected, u.getRawQuery()); + } + + @SuppressWarnings("static-method") + @Test + public void testCollectionURIFull() throws URISyntaxException { + Server11Repository r = new Server11Repository(COLLECTION, COLLECTION_URL, null, infoCollections, infoConfiguration); + assertQueryEquals("full=1&newer=5000.000", r.collectionURI(true, 5000000L, -1, null, null, null)); + assertQueryEquals("newer=1230.000", r.collectionURI(false, 1230000L, -1, null, null, null)); + assertQueryEquals("newer=5000.000&limit=10", r.collectionURI(false, 5000000L, 10, null, null, null)); + assertQueryEquals("full=1&newer=5000.000&sort=index", r.collectionURI(true, 5000000L, 0, "index", null, null)); + assertQueryEquals("full=1&ids=123,abc", r.collectionURI(true, -1L, -1, null, "123,abc", null)); + } + + @Test + public void testCollectionURI() throws URISyntaxException { + Server11Repository noTrailingSlash = new Server11Repository(COLLECTION, COLLECTION_URL, null, infoCollections, infoConfiguration); + Server11Repository trailingSlash = new Server11Repository(COLLECTION, COLLECTION_URL + "/", null, infoCollections, infoConfiguration); + Assert.assertEquals("http://foo.com/1.1/n6ec3u5bee3tixzp2asys7bs6fve4jfw/storage/bookmarks", noTrailingSlash.collectionURI().toASCIIString()); + Assert.assertEquals("http://foo.com/1.1/n6ec3u5bee3tixzp2asys7bs6fve4jfw/storage/bookmarks", trailingSlash.collectionURI().toASCIIString()); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestSyncStorageRequest.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestSyncStorageRequest.java new file mode 100644 index 000000000..0e6447c27 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestSyncStorageRequest.java @@ -0,0 +1,269 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.android.sync.net.test; + +import org.json.simple.JSONObject; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.android.sync.test.helpers.BaseTestStorageRequestDelegate; +import org.mozilla.android.sync.test.helpers.HTTPServerTestHelper; +import org.mozilla.android.sync.test.helpers.MockServer; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.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; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +@RunWith(TestRunner.class) +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.setValue("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.setValue("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.setValue("X-Weave-Quota-Remaining", "65536"); + response.setValue("X-Weave-Alert", "First weave alert string"); + response.addValue("X-Weave-Alert", "Second weave alert string"); + response.setValue("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) { + assertNotNull(request.getValue("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. + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/SynchronizerHelpers.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/SynchronizerHelpers.java new file mode 100644 index 000000000..f67f7e334 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/SynchronizerHelpers.java @@ -0,0 +1,282 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.android.sync.test; + +import android.content.Context; +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 java.util.ArrayList; +import java.util.concurrent.ExecutorService; + +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; + } + } + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestCollectionKeys.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestCollectionKeys.java new file mode 100644 index 000000000..76791a6ed --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestCollectionKeys.java @@ -0,0 +1,197 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.android.sync.test; + +import org.json.simple.JSONArray; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.apache.commons.codec.binary.Base64; +import org.mozilla.gecko.background.testhelpers.TestRunner; +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; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Set; + +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; + +@RunWith(TestRunner.class) +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, 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, 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, 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)); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestCommandProcessor.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestCommandProcessor.java new file mode 100644 index 000000000..adab2d738 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestCommandProcessor.java @@ -0,0 +1,117 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.android.sync.test; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.TestRunner; +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; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +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; + +@RunWith(TestRunner.class) +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 { + assertNull(commands.get(commandType)); + this.registerCommand(commandType, new MockCommandRunner(1)); + assertNotNull(commands.get(commandType)); + } + + @Test + public void testProcessRegisteredCommand() throws NonObjectJSONException, IOException { + 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 { + commandExecuted = false; + ExtendedJSONObject unparsedCommand = new ExtendedJSONObject(wellFormedCommand); + this.processCommand(session, unparsedCommand); + assertFalse(commandExecuted); + } + + @Test + public void testProcessInvalidCommand() throws NonObjectJSONException, IOException { + ExtendedJSONObject unparsedCommand = new ExtendedJSONObject(commandWithNoType); + this.registerCommand(commandType, new MockCommandRunner(1)); + this.processCommand(session, unparsedCommand); + assertFalse(commandExecuted); + } + + @Test + public void testParseCommandNoType() throws NonObjectJSONException, IOException { + ExtendedJSONObject unparsedCommand = new ExtendedJSONObject(commandWithNoType); + assertNull(CommandProcessor.parseCommand(unparsedCommand)); + } + + @Test + public void testParseCommandNoArgs() throws NonObjectJSONException, IOException { + ExtendedJSONObject unparsedCommand = new ExtendedJSONObject(commandWithNoArgs); + assertNull(CommandProcessor.parseCommand(unparsedCommand)); + } + + @Test + public void testParseWellFormedCommand() throws NonObjectJSONException, IOException { + 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 { + 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()); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestCryptoRecord.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestCryptoRecord.java new file mode 100644 index 000000000..a6b91eaf8 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestCryptoRecord.java @@ -0,0 +1,302 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.android.sync.test; + +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.apache.commons.codec.binary.Base64; +import org.mozilla.gecko.background.testhelpers.TestRunner; +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; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.util.Arrays; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +@RunWith(TestRunner.class) +public class TestCryptoRecord { + String base64EncryptionKey = "9K/wLdXdw+nrTtXo4ZpECyHFNr4d7aYHqeg3KW9+m6Q="; + String base64HmacKey = "MMntEfutgLTc8FlTLQFms8/xMPmCldqPlq/QQXEjx70="; + + @Test + public void testBaseCryptoRecordEncrypt() throws IOException, NonObjectJSONException, CryptoException { + + ExtendedJSONObject clearPayload = new ExtendedJSONObject("{\"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 = new ExtendedJSONObject(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")); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestRecord.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestRecord.java new file mode 100644 index 000000000..473534aac --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestRecord.java @@ -0,0 +1,330 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.android.sync.test; + +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.db.Tab; +import org.mozilla.gecko.background.testhelpers.TestRunner; +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; + +import java.io.IOException; +import java.util.ArrayList; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +@RunWith(TestRunner.class) +public class TestRecord { + + @SuppressWarnings("static-method") + @Test + public void testQueryRecord() throws NonObjectJSONException, IOException { + 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(new ExtendedJSONObject(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(new ExtendedJSONObject(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 { + 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 { + 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); + } + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestRecordsChannel.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestRecordsChannel.java new file mode 100644 index 000000000..69d3c32e7 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestRecordsChannel.java @@ -0,0 +1,229 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.android.sync.test; + +import org.junit.Test; +import org.junit.runner.RunWith; +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.TestRunner; +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; + +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +@RunWith(TestRunner.class) +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. + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestResetCommands.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestResetCommands.java new file mode 100644 index 000000000..22bcc5093 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestResetCommands.java @@ -0,0 +1,153 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.android.sync.test; + +import android.content.SharedPreferences; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +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.TestRunner; +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 java.io.IOException; +import java.util.HashMap; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * Test that reset commands properly invoke the reset methods on the correct stage. + */ +@RunWith(TestRunner.class) +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, 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) { + } + }; + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestServer11RepositorySession.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestServer11RepositorySession.java new file mode 100644 index 000000000..96a366c2d --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestServer11RepositorySession.java @@ -0,0 +1,231 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.android.sync.test; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.android.sync.test.SynchronizerHelpers.TrackingWBORepository; +import org.mozilla.android.sync.test.helpers.BaseTestStorageRequestDelegate; +import org.mozilla.android.sync.test.helpers.HTTPServerTestHelper; +import org.mozilla.android.sync.test.helpers.MockServer; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.background.testhelpers.WaitHelper; +import org.mozilla.gecko.sync.InfoCollections; +import org.mozilla.gecko.sync.InfoConfiguration; +import org.mozilla.gecko.sync.JSONRecordFetcher; +import org.mozilla.gecko.sync.Utils; +import org.mozilla.gecko.sync.crypto.KeyBundle; +import org.mozilla.gecko.sync.middleware.Crypto5MiddlewareRepository; +import org.mozilla.gecko.sync.net.AuthHeaderProvider; +import org.mozilla.gecko.sync.net.BaseResource; +import org.mozilla.gecko.sync.net.BasicAuthHeaderProvider; +import org.mozilla.gecko.sync.net.SyncStorageResponse; +import org.mozilla.gecko.sync.repositories.FetchFailedException; +import org.mozilla.gecko.sync.repositories.RepositorySession; +import org.mozilla.gecko.sync.repositories.Server11Repository; +import org.mozilla.gecko.sync.repositories.StoreFailedException; +import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate; +import org.mozilla.gecko.sync.repositories.domain.BookmarkRecord; +import org.mozilla.gecko.sync.repositories.domain.BookmarkRecordFactory; +import org.mozilla.gecko.sync.stage.SafeConstrainedServer11Repository; +import org.mozilla.gecko.sync.synchronizer.ServerLocalSynchronizer; +import org.mozilla.gecko.sync.synchronizer.Synchronizer; +import org.simpleframework.http.ContentType; +import org.simpleframework.http.Request; +import org.simpleframework.http.Response; + +import java.io.IOException; +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +@RunWith(TestRunner.class) +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_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(); + protected final InfoConfiguration infoConfiguration = new InfoConfiguration(); + + // Few-second timeout so that our longer operations don't time out and cause spurious error-handling results. + private static final int SHORT_TIMEOUT = 10000; + + public AuthHeaderProvider getAuthHeaderProvider() { + return new BasicAuthHeaderProvider(TEST_USERNAME, TEST_PASSWORD); + } + + private HTTPServerTestHelper data = new HTTPServerTestHelper(); + + public class TestSyncStorageRequestDelegate extends + BaseTestStorageRequestDelegate { + 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(); + } + } + + @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, infoConfiguration); + KeyBundle collectionKey = new KeyBundle(TEST_USERNAME, SYNC_KEY); + Crypto5MiddlewareRepository cryptoRepo = new Crypto5MiddlewareRepository(remote, collectionKey); + cryptoRepo.recordFactory = new BookmarkRecordFactory(); + + final Synchronizer synchronizer = new ServerLocalSynchronizer(); + synchronizer.repositoryA = cryptoRepo; + synchronizer.repositoryB = local; + + 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, + infoConfiguration, + 5000, 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(); + } + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestServerLocalSynchronizer.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestServerLocalSynchronizer.java new file mode 100644 index 000000000..267798672 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestServerLocalSynchronizer.java @@ -0,0 +1,237 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.android.sync.test; + +import org.junit.Test; +import org.junit.runner.RunWith; +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.TestRunner; +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; + +import java.util.ArrayList; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +@RunWith(TestRunner.class) +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()); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestSyncConfiguration.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestSyncConfiguration.java new file mode 100644 index 000000000..974d799de --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestSyncConfiguration.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; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.MockSharedPreferences; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.sync.Sync11Configuration; +import org.mozilla.gecko.sync.SyncConfiguration; + +import java.net.URI; + +@RunWith(TestRunner.class) +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()); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestSynchronizer.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestSynchronizer.java new file mode 100644 index 000000000..65157beee --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestSynchronizer.java @@ -0,0 +1,398 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.android.sync.test; + +import android.content.Context; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.android.sync.test.SynchronizerHelpers.TrackingWBORepository; +import org.mozilla.gecko.background.common.log.Logger; +import org.mozilla.gecko.background.testhelpers.TestRunner; +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 java.util.Date; + +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; + +@RunWith(TestRunner.class) +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); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestSynchronizerSession.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestSynchronizerSession.java new file mode 100644 index 000000000..ddc3ae68e --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestSynchronizerSession.java @@ -0,0 +1,306 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.android.sync.test; + +import android.content.Context; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +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.TestRunner; +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 java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; + +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; + +@RunWith(TestRunner.class) +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()); + } + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestUtils.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestUtils.java new file mode 100644 index 000000000..bc9a99dae --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestUtils.java @@ -0,0 +1,154 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.android.sync.test; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.sync.SyncConstants; +import org.mozilla.gecko.sync.Utils; + +import java.io.UnsupportedEncodingException; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Arrays; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +@RunWith(TestRunner.class) +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")); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/BaseTestStorageRequestDelegate.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/BaseTestStorageRequestDelegate.java new file mode 100644 index 000000000..a87925608 --- /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 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; + +import java.io.IOException; + +import static org.junit.Assert.fail; + +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."); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/ExpectSuccessDelegate.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/ExpectSuccessDelegate.java new file mode 100644 index 000000000..cf3545c1e --- /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 diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/ExpectSuccessRepositorySessionBeginDelegate.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/ExpectSuccessRepositorySessionBeginDelegate.java new file mode 100644 index 000000000..d7cb186f8 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/ExpectSuccessRepositorySessionBeginDelegate.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 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; + +import java.util.concurrent.ExecutorService; + +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; + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/ExpectSuccessRepositorySessionCreationDelegate.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/ExpectSuccessRepositorySessionCreationDelegate.java new file mode 100644 index 000000000..8860baf77 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/ExpectSuccessRepositorySessionCreationDelegate.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.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; + } + +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/ExpectSuccessRepositorySessionFetchRecordsDelegate.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/ExpectSuccessRepositorySessionFetchRecordsDelegate.java new file mode 100644 index 000000000..5f5cf8995 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/ExpectSuccessRepositorySessionFetchRecordsDelegate.java @@ -0,0 +1,44 @@ +/* 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.delegates.RepositorySessionFetchRecordsDelegate; +import org.mozilla.gecko.sync.repositories.domain.Record; + +import java.util.ArrayList; +import java.util.concurrent.ExecutorService; + +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; + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/ExpectSuccessRepositorySessionFinishDelegate.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/ExpectSuccessRepositorySessionFinishDelegate.java new file mode 100644 index 000000000..4435d5fa2 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/ExpectSuccessRepositorySessionFinishDelegate.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.RepositorySessionBundle; +import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFinishDelegate; + +import java.util.concurrent.ExecutorService; + +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; + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/ExpectSuccessRepositorySessionStoreDelegate.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/ExpectSuccessRepositorySessionStoreDelegate.java new file mode 100644 index 000000000..cfca180fa --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/ExpectSuccessRepositorySessionStoreDelegate.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 junit.framework.AssertionFailedError; +import org.mozilla.gecko.background.testhelpers.WaitHelper; +import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionStoreDelegate; + +import java.util.concurrent.ExecutorService; + +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; + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/ExpectSuccessRepositoryWipeDelegate.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/ExpectSuccessRepositoryWipeDelegate.java new file mode 100644 index 000000000..0f248dda7 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/ExpectSuccessRepositoryWipeDelegate.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.helpers; + +import junit.framework.AssertionFailedError; +import org.mozilla.gecko.background.testhelpers.WaitHelper; +import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionWipeDelegate; + +import java.util.concurrent.ExecutorService; + +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; + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/HTTPServerTestHelper.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/HTTPServerTestHelper.java new file mode 100644 index 000000000..1829bdd12 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/HTTPServerTestHelper.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.helpers; + +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.http.core.ContainerSocketProcessor; +import org.simpleframework.transport.connect.Connection; +import org.simpleframework.transport.connect.SocketConnection; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.util.IdentityHashMap; +import java.util.Map; + +import static org.junit.Assert.fail; + +/** + * 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(new ContainerSocketProcessor(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()); + } + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/MockGlobalSessionCallback.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/MockGlobalSessionCallback.java new file mode 100644 index 000000000..5d7e8edd1 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/MockGlobalSessionCallback.java @@ -0,0 +1,91 @@ +/* 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.testhelpers.WaitHelper; +import org.mozilla.gecko.sync.GlobalSession; +import org.mozilla.gecko.sync.delegates.GlobalSessionCallback; +import org.mozilla.gecko.sync.stage.GlobalSyncStage.Stage; + +import java.net.URI; + +import static org.junit.Assert.assertEquals; + +/** + * 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 { + 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 calledInformUnauthorizedResponse = false; + public boolean calledInformUpgradeRequiredResponse = false; + public boolean calledInformMigrated = false; + 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 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; + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/MockResourceDelegate.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/MockResourceDelegate.java new file mode 100644 index 000000000..2cac07904 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/MockResourceDelegate.java @@ -0,0 +1,85 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.android.sync.test.helpers; + +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; +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 java.io.IOException; +import java.security.GeneralSecurityException; + +import static org.junit.Assert.assertEquals; + +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(); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/MockServer.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/MockServer.java new file mode 100644 index 000000000..4e1d6d7ad --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/MockServer.java @@ -0,0 +1,73 @@ +/* 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.sync.Utils; +import org.simpleframework.http.Request; +import org.simpleframework.http.Response; +import org.simpleframework.http.core.Container; + +import java.io.IOException; +import java.io.PrintStream; + +import static org.junit.Assert.assertEquals; + +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.setValue("Content-Type", contentType); + response.setValue("Server", "HelloWorld/1.0 (Simple 4.0)"); + response.setDate("Date", time); + response.setDate("Last-Modified", time); + + final String timestampHeader = Utils.millisecondsToDecimalSecondsString(time); + response.setValue("X-Weave-Timestamp", timestampHeader); + Logger.debug(LOG_TAG, "> X-Weave-Timestamp header: " + timestampHeader); + response.setValue("X-Last-Modified", "12345678"); + 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); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/MockSyncClientsEngineStage.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/MockSyncClientsEngineStage.java new file mode 100644 index 000000000..efd379e13 --- /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 org.mozilla.gecko.sync.net.BaseResource; +import org.mozilla.gecko.sync.net.SyncStorageResponse; +import org.mozilla.gecko.sync.stage.SyncClientsEngineStage; + +import static org.junit.Assert.assertTrue; + +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(); + } + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/MockWBOServer.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/MockWBOServer.java new file mode 100644 index 000000000..164274bac --- /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 org.simpleframework.http.Path; +import org.simpleframework.http.Request; +import org.simpleframework.http.Response; + +import java.util.HashMap; + +/** + * 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 + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/test/TestHTTPServerTestHelper.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/test/TestHTTPServerTestHelper.java new file mode 100644 index 000000000..d79998cc9 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/helpers/test/TestHTTPServerTestHelper.java @@ -0,0 +1,102 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.android.sync.test.helpers.test; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.android.sync.test.helpers.HTTPServerTestHelper; +import org.mozilla.android.sync.test.helpers.HTTPServerTestHelper.HTTPServerAlreadyRunningError; +import org.mozilla.android.sync.test.helpers.MockServer; +import org.mozilla.gecko.background.testhelpers.TestRunner; + +import java.io.PrintWriter; +import java.io.StringWriter; + +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; + +@RunWith(TestRunner.class) +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(); + } + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/GeckoNetworkManagerTest.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/GeckoNetworkManagerTest.java new file mode 100644 index 000000000..8b07d7448 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/GeckoNetworkManagerTest.java @@ -0,0 +1,51 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.GeckoNetworkManager.ManagerState; +import org.mozilla.gecko.GeckoNetworkManager.ManagerEvent; + +import static org.junit.Assert.*; + +@RunWith(TestRunner.class) +public class GeckoNetworkManagerTest { + /** + * Tests the transition matrix. + */ + @Test + public void testGetNextState() { + ManagerState testingState; + + testingState = ManagerState.OffNoListeners; + assertNull(GeckoNetworkManager.getNextState(testingState, ManagerEvent.disableNotifications)); + assertNull(GeckoNetworkManager.getNextState(testingState, ManagerEvent.stop)); + assertNull(GeckoNetworkManager.getNextState(testingState, ManagerEvent.receivedUpdate)); + assertEquals(ManagerState.OnNoListeners, GeckoNetworkManager.getNextState(testingState, ManagerEvent.start)); + assertEquals(ManagerState.OffWithListeners, GeckoNetworkManager.getNextState(testingState, ManagerEvent.enableNotifications)); + + testingState = ManagerState.OnNoListeners; + assertNull(GeckoNetworkManager.getNextState(testingState, ManagerEvent.start)); + assertNull(GeckoNetworkManager.getNextState(testingState, ManagerEvent.disableNotifications)); + assertEquals(ManagerState.OnWithListeners, GeckoNetworkManager.getNextState(testingState, ManagerEvent.enableNotifications)); + assertEquals(ManagerState.OffNoListeners, GeckoNetworkManager.getNextState(testingState, ManagerEvent.stop)); + assertEquals(ManagerState.OnNoListeners, GeckoNetworkManager.getNextState(testingState, ManagerEvent.receivedUpdate)); + + testingState = ManagerState.OnWithListeners; + assertNull(GeckoNetworkManager.getNextState(testingState, ManagerEvent.start)); + assertNull(GeckoNetworkManager.getNextState(testingState, ManagerEvent.enableNotifications)); + assertEquals(ManagerState.OffWithListeners, GeckoNetworkManager.getNextState(testingState, ManagerEvent.stop)); + assertEquals(ManagerState.OnNoListeners, GeckoNetworkManager.getNextState(testingState, ManagerEvent.disableNotifications)); + assertEquals(ManagerState.OnWithListeners, GeckoNetworkManager.getNextState(testingState, ManagerEvent.receivedUpdate)); + + testingState = ManagerState.OffWithListeners; + assertNull(GeckoNetworkManager.getNextState(testingState, ManagerEvent.stop)); + assertNull(GeckoNetworkManager.getNextState(testingState, ManagerEvent.enableNotifications)); + assertNull(GeckoNetworkManager.getNextState(testingState, ManagerEvent.receivedUpdate)); + assertEquals(ManagerState.OnWithListeners, GeckoNetworkManager.getNextState(testingState, ManagerEvent.start)); + assertEquals(ManagerState.OffNoListeners, GeckoNetworkManager.getNextState(testingState, ManagerEvent.disableNotifications)); + } +}
\ No newline at end of file diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/GlobalPageMetadataTest.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/GlobalPageMetadataTest.java new file mode 100644 index 000000000..f30ee7a2c --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/GlobalPageMetadataTest.java @@ -0,0 +1,174 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko; + +import android.content.ContentProviderClient; +import android.content.ContentValues; +import android.database.Cursor; +import android.os.RemoteException; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.db.DelegatingTestContentProvider; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.db.BrowserContract; +import org.mozilla.gecko.db.BrowserContract.PageMetadata; +import org.mozilla.gecko.db.BrowserDB; +import org.mozilla.gecko.db.BrowserProvider; +import org.mozilla.gecko.db.LocalBrowserDB; +import org.robolectric.shadows.ShadowContentResolver; + +import static org.junit.Assert.*; + +@RunWith(TestRunner.class) +public class GlobalPageMetadataTest { + @Test + public void testQueueing() throws Exception { + BrowserDB db = new LocalBrowserDB("default"); + + BrowserProvider provider = new BrowserProvider(); + try { + provider.onCreate(); + ShadowContentResolver.registerProvider(BrowserContract.AUTHORITY, new DelegatingTestContentProvider(provider)); + + ShadowContentResolver cr = new ShadowContentResolver(); + ContentProviderClient pageMetadataClient = cr.acquireContentProviderClient(PageMetadata.CONTENT_URI); + + assertEquals(0, GlobalPageMetadata.getInstance().getMetadataQueueSize()); + + // There's not history record for this uri, so test that queueing works. + GlobalPageMetadata.getInstance().doAddOrQueue(db, pageMetadataClient, "https://mozilla.org", false, "{type: 'article'}"); + + assertPageMetadataCountForGUID(0, "guid1", pageMetadataClient); + assertEquals(1, GlobalPageMetadata.getInstance().getMetadataQueueSize()); + + // Test that queue doesn't duplicate metadata for the same history item. + GlobalPageMetadata.getInstance().doAddOrQueue(db, pageMetadataClient, "https://mozilla.org", false, "{type: 'article'}"); + assertEquals(1, GlobalPageMetadata.getInstance().getMetadataQueueSize()); + + // Test that queue is limited to 15 metadata items. + for (int i = 0; i < 20; i++) { + GlobalPageMetadata.getInstance().doAddOrQueue(db, pageMetadataClient, "https://mozilla.org/" + i, false, "{type: 'article'}"); + } + assertEquals(15, GlobalPageMetadata.getInstance().getMetadataQueueSize()); + } finally { + provider.shutdown(); + } + } + + @Test + public void testInsertingMetadata() throws Exception { + BrowserDB db = new LocalBrowserDB("default"); + + // Start listening for events. + GlobalPageMetadata.getInstance().init(); + + BrowserProvider provider = new BrowserProvider(); + try { + provider.onCreate(); + ShadowContentResolver.registerProvider(BrowserContract.AUTHORITY, new DelegatingTestContentProvider(provider)); + + ShadowContentResolver cr = new ShadowContentResolver(); + ContentProviderClient historyClient = cr.acquireContentProviderClient(BrowserContract.History.CONTENT_URI); + ContentProviderClient pageMetadataClient = cr.acquireContentProviderClient(PageMetadata.CONTENT_URI); + + // Insert required history item... + ContentValues cv = new ContentValues(); + cv.put(BrowserContract.History.GUID, "guid1"); + cv.put(BrowserContract.History.URL, "https://mozilla.org"); + historyClient.insert(BrowserContract.History.CONTENT_URI, cv); + + // TODO: Main test runner thread finishes before EventDispatcher events are processed... + // Fire off a message saying that history has been inserted. + // Bundle message = new Bundle(); + // message.putString(GlobalHistory.EVENT_PARAM_URI, "https://mozilla.org"); + // EventDispatcher.getInstance().dispatch(GlobalHistory.EVENT_URI_AVAILABLE_IN_HISTORY, message); + + // For now, let's just try inserting again. + GlobalPageMetadata.getInstance().doAddOrQueue(db, pageMetadataClient, "https://mozilla.org", false, "{type: 'article', description: 'test article'}"); + + assertPageMetadataCountForGUID(1, "guid1", pageMetadataClient); + assertPageMetadataValues(pageMetadataClient, "guid1", false, "{\"type\":\"article\",\"description\":\"test article\"}"); + + // Test that inserting empty metadata deletes existing metadata record. + GlobalPageMetadata.getInstance().doAddOrQueue(db, pageMetadataClient, "https://mozilla.org", false, "{}"); + assertPageMetadataCountForGUID(0, "guid1", pageMetadataClient); + + // Test that inserting new metadata overrides existing metadata record. + GlobalPageMetadata.getInstance().doAddOrQueue(db, pageMetadataClient, "https://mozilla.org", true, "{type: 'article', description: 'test article', image_url: 'https://example.com/test.png'}"); + assertPageMetadataValues(pageMetadataClient, "guid1", true, "{\"type\":\"article\",\"description\":\"test article\",\"image_url\":\"https:\\/\\/example.com\\/test.png\"}"); + + // Insert another history item... + cv = new ContentValues(); + cv.put(BrowserContract.History.GUID, "guid2"); + cv.put(BrowserContract.History.URL, "https://planet.mozilla.org"); + historyClient.insert(BrowserContract.History.CONTENT_URI, cv); + // Test that empty metadata doesn't get inserted for a new history. + GlobalPageMetadata.getInstance().doAddOrQueue(db, pageMetadataClient, "https://planet.mozilla.org", false, "{}"); + + assertPageMetadataCountForGUID(0, "guid2", pageMetadataClient); + + } finally { + provider.shutdown(); + } + } + + /** + * Expects cursor to be at the correct position. + */ + private void assertCursorValues(Cursor cursor, String json, int hasImage, String guid) { + assertNotNull(cursor); + assertEquals(json, cursor.getString(cursor.getColumnIndexOrThrow(PageMetadata.JSON))); + assertEquals(hasImage, cursor.getInt(cursor.getColumnIndexOrThrow(PageMetadata.HAS_IMAGE))); + assertEquals(guid, cursor.getString(cursor.getColumnIndexOrThrow(PageMetadata.HISTORY_GUID))); + } + + private void assertPageMetadataValues(ContentProviderClient client, String guid, boolean hasImage, String json) { + final Cursor cursor; + + try { + cursor = client.query(PageMetadata.CONTENT_URI, new String[]{ + PageMetadata.HISTORY_GUID, + PageMetadata.HAS_IMAGE, + PageMetadata.JSON, + PageMetadata.DATE_CREATED + }, PageMetadata.HISTORY_GUID + " = ?", new String[]{guid}, null); + } catch (RemoteException e) { + fail(); + return; + } + + assertNotNull(cursor); + try { + assertEquals(1, cursor.getCount()); + assertTrue(cursor.moveToFirst()); + assertCursorValues(cursor, json, hasImage ? 1 : 0, guid); + } finally { + cursor.close(); + } + } + + private void assertPageMetadataCountForGUID(int expected, String guid, ContentProviderClient client) { + final Cursor cursor; + + try { + cursor = client.query(PageMetadata.CONTENT_URI, new String[]{ + PageMetadata.HISTORY_GUID, + PageMetadata.HAS_IMAGE, + PageMetadata.JSON, + PageMetadata.DATE_CREATED + }, PageMetadata.HISTORY_GUID + " = ?", new String[]{guid}, null); + } catch (RemoteException e) { + fail(); + return; + } + + assertNotNull(cursor); + try { + assertEquals(expected, cursor.getCount()); + } finally { + cursor.close(); + } + } +}
\ No newline at end of file diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/TestGeckoProfile.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/TestGeckoProfile.java new file mode 100644 index 000000000..c01c2d21a --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/TestGeckoProfile.java @@ -0,0 +1,254 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko; + +import android.content.Context; + +import org.json.JSONObject; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.util.FileUtils; +import org.robolectric.RuntimeEnvironment; + +import java.io.File; +import java.io.IOException; +import java.util.UUID; + +import static org.junit.Assert.*; + +/** + * Unit test methods of the GeckoProfile class. + */ +@RunWith(TestRunner.class) +public class TestGeckoProfile { + private static final String PROFILE_NAME = "profileName"; + + private static final String CLIENT_ID_JSON_ATTR = "clientID"; + private static final String PROFILE_CREATION_DATE_JSON_ATTR = "created"; + + @Rule + public TemporaryFolder dirContainingProfile = new TemporaryFolder(); + + private File profileDir; + private GeckoProfile profile; + + private File clientIdFile; + private File timesFile; + + @Before + public void setUp() throws IOException { + final Context context = RuntimeEnvironment.application; + profileDir = dirContainingProfile.newFolder(); + profile = GeckoProfile.get(context, PROFILE_NAME, profileDir); + + clientIdFile = new File(profileDir, "datareporting/state.json"); + timesFile = new File(profileDir, "times.json"); + } + + public void assertValidClientId(final String clientId) { + // This isn't the method we use in the main GeckoProfile code, but it should be equivalent. + UUID.fromString(clientId); // assert: will throw if null or invalid UUID. + } + + @Test + public void testGetDir() { + assertEquals("Profile dir argument during construction and returned value are equal", + profileDir, profile.getDir()); + } + + @Test + public void testGetClientIdFreshProfile() throws Exception { + assertFalse("client ID file does not exist", clientIdFile.exists()); + + // No existing client ID file: we're expected to create one. + final String clientId = profile.getClientId(); + assertValidClientId(clientId); + assertTrue("client ID file exists", clientIdFile.exists()); + + assertEquals("Returned client ID is the same as the one previously returned", clientId, profile.getClientId()); + assertEquals("clientID file format matches expectations", clientId, readClientIdFromFile(clientIdFile)); + } + + @Test + public void testGetClientIdFileAlreadyExists() throws Exception { + final String validClientId = "905de1c0-0ea6-4a43-95f9-6170035f5a82"; + assertTrue("Created the parent dirs of the client ID file", clientIdFile.getParentFile().mkdirs()); + writeClientIdToFile(clientIdFile, validClientId); + + final String clientIdFromProfile = profile.getClientId(); + assertEquals("Client ID from method matches ID written to disk", validClientId, clientIdFromProfile); + } + + @Test + public void testGetClientIdMigrateFromFHR() throws Exception { + final File fhrClientIdFile = new File(profileDir, "healthreport/state.json"); + final String fhrClientId = "905de1c0-0ea6-4a43-95f9-6170035f5a82"; + + assertFalse("client ID file does not exist", clientIdFile.exists()); + assertTrue("Created FHR data directory", new File(profileDir, "healthreport").mkdirs()); + writeClientIdToFile(fhrClientIdFile, fhrClientId); + assertEquals("Migrated Client ID equals FHR client ID", fhrClientId, profile.getClientId()); + + // Verify migration wrote to contemporary client ID file. + assertTrue("Client ID file created during migration", clientIdFile.exists()); + assertEquals("Migrated client ID on disk equals value returned from method", + fhrClientId, readClientIdFromFile(clientIdFile)); + + assertTrue("Deleted FHR clientID file", fhrClientIdFile.delete()); + assertEquals("Ensure method calls read from newly created client ID file & not FHR client ID file", + fhrClientId, profile.getClientId()); + } + + @Test + public void testGetClientIdInvalidIdOnDisk() throws Exception { + assertTrue("Created the parent dirs of the client ID file", clientIdFile.getParentFile().mkdirs()); + writeClientIdToFile(clientIdFile, ""); + final String clientIdForEmptyString = profile.getClientId(); + assertValidClientId(clientIdForEmptyString); + assertNotEquals("A new client ID was created when the empty String was written to disk", "", clientIdForEmptyString); + + writeClientIdToFile(clientIdFile, "invalidClientId"); + final String clientIdForInvalidClientId = profile.getClientId(); + assertValidClientId(clientIdForInvalidClientId); + assertNotEquals("A new client ID was created when an invalid client ID was written to disk", + "invalidClientId", clientIdForInvalidClientId); + } + + @Test + public void testGetClientIdMissingClientIdJSONAttr() throws Exception { + final String validClientId = "905de1c0-0ea6-4a43-95f9-6170035f5a82"; + final JSONObject objMissingClientId = new JSONObject(); + objMissingClientId.put("irrelevantKey", validClientId); + assertTrue("Created the parent dirs of the client ID file", clientIdFile.getParentFile().mkdirs()); + FileUtils.writeJSONObjectToFile(clientIdFile, objMissingClientId); + + final String clientIdForMissingAttr = profile.getClientId(); + assertValidClientId(clientIdForMissingAttr); + assertNotEquals("Did not use other attr when JSON attr was missing", validClientId, clientIdForMissingAttr); + } + + @Test + public void testGetClientIdInvalidIdFileFormat() throws Exception { + final String validClientId = "905de1c0-0ea6-4a43-95f9-6170035f5a82"; + assertTrue("Created the parent dirs of the client ID file", clientIdFile.getParentFile().mkdirs()); + FileUtils.writeStringToFile(clientIdFile, "clientID: \"" + validClientId + "\""); + + final String clientIdForInvalidFormat = profile.getClientId(); + assertValidClientId(clientIdForInvalidFormat); + assertNotEquals("Created new ID when file format was invalid", validClientId, clientIdForInvalidFormat); + } + + @Test + public void testEnsureParentDirs() { + final File grandParentDir = new File(profileDir, "grandParent"); + final File parentDir = new File(grandParentDir, "parent"); + final File childFile = new File(parentDir, "child"); + + // Assert initial state. + assertFalse("Topmost parent dir should not exist yet", grandParentDir.exists()); + assertFalse("Bottommost parent dir should not exist yet", parentDir.exists()); + assertFalse("Child file should not exist", childFile.exists()); + + final String fakeFullPath = "grandParent/parent/child"; + assertTrue("Parent directories should be created", profile.ensureParentDirs(fakeFullPath)); + assertTrue("Topmost parent dir should have been created", grandParentDir.exists()); + assertTrue("Bottommost parent dir should have been created", parentDir.exists()); + assertFalse("Child file should not have been created", childFile.exists()); + + // Parents already exist because this is the second time we're calling ensureParentDirs. + assertTrue("Expect true if parent directories already exist", profile.ensureParentDirs(fakeFullPath)); + + // Assert error condition. + assertTrue("Ensure we can change permissions on profile dir for testing", profileDir.setReadOnly()); + assertFalse("Expect false if the parent dir could not be created", profile.ensureParentDirs("unwritableDir/child")); + } + + @Test + public void testIsClientIdValid() { + final String[] validClientIds = new String[] { + "905de1c0-0ea6-4a43-95f9-6170035f5a82", + "905de1c0-0ea6-4a43-95f9-6170035f5a83", + "57472f82-453d-4c55-b59c-d3c0e97b76a1", + "895745d1-f31e-46c3-880e-b4dd72963d4f", + }; + for (final String validClientId : validClientIds) { + assertTrue("Client ID, " + validClientId + ", is valid", profile.isClientIdValid(validClientId)); + } + + final String[] invalidClientIds = new String[] { + null, + "", + "a", + "anInvalidClientId", + "905de1c0-0ea6-4a43-95f9-6170035f5a820", // too long (last section) + "905de1c0-0ea6-4a43-95f9-6170035f5a8", // too short (last section) + "05de1c0-0ea6-4a43-95f9-6170035f5a82", // too short (first section) + "905de1c0-0ea6-4a43-95f9-6170035f5a8!", // contains a symbol + }; + for (final String invalidClientId : invalidClientIds) { + assertFalse("Client ID, " + invalidClientId + ", is invalid", profile.isClientIdValid(invalidClientId)); + } + + // We generate client IDs using UUID - better make sure they're valid. + for (int i = 0; i < 30; ++i) { + final String generatedClientId = UUID.randomUUID().toString(); + assertTrue("Generated client ID from UUID, " + generatedClientId + ", is valid", + profile.isClientIdValid(generatedClientId)); + } + } + + @Test + public void testGetProfileCreationDateFromTimesFile() throws Exception { + final long expectedDate = System.currentTimeMillis(); + final JSONObject expectedObj = new JSONObject(); + expectedObj.put(PROFILE_CREATION_DATE_JSON_ATTR, expectedDate); + FileUtils.writeJSONObjectToFile(timesFile, expectedObj); + + final Context context = RuntimeEnvironment.application; + final long actualDate = profile.getAndPersistProfileCreationDate(context); + assertEquals("Date from disk equals date inserted to disk", expectedDate, actualDate); + + final long actualDateFromDisk = readProfileCreationDateFromFile(timesFile); + assertEquals("Date in times.json has not changed after accessing profile creation date", + expectedDate, actualDateFromDisk); + } + + @Test + public void testGetProfileCreationDateTimesFileDoesNotExist() throws Exception { + assertFalse("Times.json does not already exist", timesFile.exists()); + + final Context context = RuntimeEnvironment.application; + final long actualDate = profile.getAndPersistProfileCreationDate(context); + // I'd prefer to mock so we can return and verify a specific value but we can't mock + // GeckoProfile because it's final. Instead, we check if the value is at least reasonable. + assertTrue("Date from method is positive", actualDate >= 0); + assertTrue("Date from method is less than current time", actualDate < System.currentTimeMillis()); + + assertTrue("Times.json exists after getting profile", timesFile.exists()); + final long actualDateFromDisk = readProfileCreationDateFromFile(timesFile); + assertEquals("Date from disk equals returned value", actualDate, actualDateFromDisk); + } + + private static long readProfileCreationDateFromFile(final File file) throws Exception { + final JSONObject actualObj = FileUtils.readJSONObjectFromFile(file); + return actualObj.getLong(PROFILE_CREATION_DATE_JSON_ATTR); + } + + private String readClientIdFromFile(final File file) throws Exception { + final JSONObject obj = FileUtils.readJSONObjectFromFile(file); + return obj.getString(CLIENT_ID_JSON_ATTR); + } + + private void writeClientIdToFile(final File file, final String clientId) throws Exception { + final JSONObject obj = new JSONObject(); + obj.put(CLIENT_ID_JSON_ATTR, clientId); + FileUtils.writeJSONObjectToFile(file, obj); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/activitystream/TestActivityStream.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/activitystream/TestActivityStream.java new file mode 100644 index 000000000..71f01b437 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/activitystream/TestActivityStream.java @@ -0,0 +1,85 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.activitystream; + +import android.os.SystemClock; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.robolectric.Robolectric; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.shadows.ShadowLooper; + +import static org.junit.Assert.assertEquals; + +@RunWith(TestRunner.class) +public class TestActivityStream { + /** + * Unit tests for ActivityStream.extractLabel(). + * + * Most test cases are based on this list: + * https://gist.github.com/nchapman/36502ad115e8825d522a66549971a3f0 + */ + @Test + public void testExtractLabelWithPath() { + // Empty values + assertLabelEquals("", "", true); + assertLabelEquals("", null, true); + + // Without path + assertLabelEquals("news.ycombinator", "https://news.ycombinator.com/", true); + assertLabelEquals("sql.telemetry.mozilla", "https://sql.telemetry.mozilla.org/", true); + assertLabelEquals("sso.mozilla", "http://sso.mozilla.com/", true); + assertLabelEquals("youtube", "http://youtube.com/", true); + assertLabelEquals("images.google", "http://images.google.com/", true); + assertLabelEquals("smile.amazon", "http://smile.amazon.com/", true); + assertLabelEquals("localhost", "http://localhost:5000/", true); + assertLabelEquals("independent", "http://www.independent.co.uk/", true); + + // With path + assertLabelEquals("firefox", "https://addons.mozilla.org/en-US/firefox/", true); + assertLabelEquals("activity-stream", "https://trello.com/b/KX3hV8XS/activity-stream", true); + assertLabelEquals("activity-stream", "https://github.com/mozilla/activity-stream", true); + assertLabelEquals("sidekiq", "https://dispatch-news.herokuapp.com/sidekiq", true); + assertLabelEquals("nchapman", "https://github.com/nchapman/", true); + + // Unusable paths + assertLabelEquals("phonebook.mozilla","https://phonebook.mozilla.org/mellon/login?ReturnTo=https%3A%2F%2Fphonebook.mozilla.org%2F&IdP=http%3A%2F%2Fwww.okta.com", true); + assertLabelEquals("ipay.adp", "https://ipay.adp.com/iPay/index.jsf", true); + assertLabelEquals("calendar.google", "https://calendar.google.com/calendar/render?pli=1#main_7", true); + assertLabelEquals("myworkday", "https://www.myworkday.com/vhr_mozilla/d/home.htmld", true); + assertLabelEquals("mail.google", "https://mail.google.com/mail/u/1/#inbox", true); + assertLabelEquals("docs.google", "https://docs.google.com/presentation/d/11cyrcwhKTmBdEBIZ3szLO0-_Imrx2CGV2B9_LZHDrds/edit#slide=id.g15d41bb0f3_0_82", true); + + // Special cases + assertLabelEquals("irccloud.mozilla", "https://irccloud.mozilla.com/#!/ircs://irc1.dmz.scl3.mozilla.com:6697/%23universal-search", true); + } + + @Test + public void testExtractLabelWithoutPath() { + assertLabelEquals("addons.mozilla", "https://addons.mozilla.org/en-US/firefox/", false); + assertLabelEquals("trello", "https://trello.com/b/KX3hV8XS/activity-stream", false); + assertLabelEquals("github", "https://github.com/mozilla/activity-stream", false); + assertLabelEquals("dispatch-news", "https://dispatch-news.herokuapp.com/sidekiq", false); + assertLabelEquals("github", "https://github.com/nchapman/", false); + } + + private void assertLabelEquals(String expectedLabel, String url, boolean usePath) { + final String[] actualLabel = new String[1]; + + ActivityStream.LabelCallback callback = new ActivityStream.LabelCallback() { + @Override + public void onLabelExtracted(String label) { + actualLabel[0] = label; + } + }; + + ActivityStream.extractLabel(RuntimeEnvironment.application, url, usePath, callback); + + ShadowLooper.runUiThreadTasks(); + + assertEquals(expectedLabel, actualLabel[0]); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/common/log/writers/test/TestLogWriters.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/common/log/writers/test/TestLogWriters.java new file mode 100644 index 000000000..61dd33965 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/common/log/writers/test/TestLogWriters.java @@ -0,0 +1,179 @@ +/* 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 android.util.Log; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +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 org.mozilla.gecko.background.testhelpers.TestRunner; + +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CountDownLatch; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +@RunWith(TestRunner.class) +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 diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/db/DelegatingTestContentProvider.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/db/DelegatingTestContentProvider.java new file mode 100644 index 000000000..91a36f7d1 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/db/DelegatingTestContentProvider.java @@ -0,0 +1,86 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.background.db; + +import android.content.ContentProvider; +import android.content.ContentProviderOperation; +import android.content.ContentProviderResult; +import android.content.ContentValues; +import android.content.OperationApplicationException; +import android.database.Cursor; +import android.net.Uri; + +import org.mozilla.gecko.db.BrowserContract; + +import java.util.ArrayList; + +/** + * Wrap a ContentProvider, appending &test=1 to all queries. + */ +public class DelegatingTestContentProvider extends ContentProvider { + protected final ContentProvider mTargetProvider; + + protected static Uri appendUriParam(Uri uri, String param, String value) { + return uri.buildUpon().appendQueryParameter(param, value).build(); + } + + public DelegatingTestContentProvider(ContentProvider targetProvider) { + super(); + mTargetProvider = targetProvider; + } + + private Uri appendTestParam(Uri uri) { + return appendUriParam(uri, BrowserContract.PARAM_IS_TEST, "1"); + } + + @Override + public boolean onCreate() { + return mTargetProvider.onCreate(); + } + + @Override + public String getType(Uri uri) { + return mTargetProvider.getType(uri); + } + + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + return mTargetProvider.delete(appendTestParam(uri), selection, selectionArgs); + } + + @Override + public Uri insert(Uri uri, ContentValues values) { + return mTargetProvider.insert(appendTestParam(uri), values); + } + + @Override + public int update(Uri uri, ContentValues values, String selection, + String[] selectionArgs) { + return mTargetProvider.update(appendTestParam(uri), values, + selection, selectionArgs); + } + + @Override + public Cursor query(Uri uri, String[] projection, String selection, + String[] selectionArgs, String sortOrder) { + return mTargetProvider.query(appendTestParam(uri), projection, selection, + selectionArgs, sortOrder); + } + + @Override + public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) + throws OperationApplicationException { + return mTargetProvider.applyBatch(operations); + } + + @Override + public int bulkInsert(Uri uri, ContentValues[] values) { + return mTargetProvider.bulkInsert(appendTestParam(uri), values); + } + + public ContentProvider getTargetProvider() { + return mTargetProvider; + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/db/TestTabsProvider.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/db/TestTabsProvider.java new file mode 100644 index 000000000..f0d468e07 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/db/TestTabsProvider.java @@ -0,0 +1,338 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.db; + +import android.content.ContentProviderClient; +import android.content.ContentValues; +import android.database.Cursor; +import android.net.Uri; +import android.os.RemoteException; + +import org.json.simple.JSONArray; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.db.BrowserContract; +import org.mozilla.gecko.db.TabsProvider; +import org.mozilla.gecko.sync.repositories.android.BrowserContractHelpers; +import org.mozilla.gecko.sync.repositories.android.FennecTabsRepository; +import org.mozilla.gecko.sync.repositories.domain.TabsRecord; +import org.robolectric.shadows.ShadowContentResolver; + +@RunWith(TestRunner.class) +public class TestTabsProvider { + public static final String TEST_CLIENT_GUID = "test guid"; // Real GUIDs never contain spaces. + public static final String TEST_CLIENT_NAME = "test client name"; + + public static final String CLIENTS_GUID_IS = BrowserContract.Clients.GUID + " = ?"; + public static final String TABS_CLIENT_GUID_IS = BrowserContract.Tabs.CLIENT_GUID + " = ?"; + + protected Tab testTab1; + protected Tab testTab2; + protected Tab testTab3; + + protected TabsProvider provider; + + @Before + public void setUp() { + provider = new TabsProvider(); + provider.onCreate(); + ShadowContentResolver.registerProvider(BrowserContract.TABS_AUTHORITY, new DelegatingTestContentProvider(provider)); + } + + @After + public void tearDown() throws Exception { + provider.shutdown(); + provider = null; + } + + protected ContentProviderClient getClientsClient() { + final ShadowContentResolver cr = new ShadowContentResolver(); + return cr.acquireContentProviderClient(BrowserContractHelpers.CLIENTS_CONTENT_URI); + } + + protected ContentProviderClient getTabsClient() { + final ShadowContentResolver cr = new ShadowContentResolver(); + return cr.acquireContentProviderClient(BrowserContractHelpers.TABS_CONTENT_URI); + } + + protected int deleteTestClient(final ContentProviderClient clientsClient) throws RemoteException { + if (clientsClient == null) { + throw new IllegalStateException("Provided ContentProviderClient is null"); + } + return clientsClient.delete(BrowserContractHelpers.CLIENTS_CONTENT_URI, CLIENTS_GUID_IS, new String[] { TEST_CLIENT_GUID }); + } + + protected int deleteAllTestTabs(final ContentProviderClient tabsClient) throws RemoteException { + if (tabsClient == null) { + throw new IllegalStateException("Provided ContentProviderClient is null"); + } + return tabsClient.delete(BrowserContractHelpers.TABS_CONTENT_URI, TABS_CLIENT_GUID_IS, new String[] { TEST_CLIENT_GUID }); + } + + protected void insertTestClient(final ContentProviderClient clientsClient) throws RemoteException { + ContentValues cv = new ContentValues(); + cv.put(BrowserContract.Clients.GUID, TEST_CLIENT_GUID); + cv.put(BrowserContract.Clients.NAME, TEST_CLIENT_NAME); + clientsClient.insert(BrowserContractHelpers.CLIENTS_CONTENT_URI, cv); + } + + @SuppressWarnings("unchecked") + protected void insertSomeTestTabs(ContentProviderClient tabsClient) throws RemoteException { + final JSONArray history1 = new JSONArray(); + history1.add("http://test.com/test1.html"); + testTab1 = 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"); + testTab2 = new Tab("test title 2", "http://test.com/test2.png", history2, 2000); + + final JSONArray history3 = new JSONArray(); + history3.add("http://test.com/test3.html#1"); + history3.add("http://test.com/test3.html#2"); + testTab3 = new Tab("test title 3", "http://test.com/test3.png", history3, 3000); + + tabsClient.insert(BrowserContractHelpers.TABS_CONTENT_URI, testTab1.toContentValues(TEST_CLIENT_GUID, 0)); + tabsClient.insert(BrowserContractHelpers.TABS_CONTENT_URI, testTab2.toContentValues(TEST_CLIENT_GUID, 1)); + tabsClient.insert(BrowserContractHelpers.TABS_CONTENT_URI, testTab3.toContentValues(TEST_CLIENT_GUID, 2)); + } + + // Sanity. + @Test + public void testObtainCP() { + final ContentProviderClient clientsClient = getClientsClient(); + Assert.assertNotNull(clientsClient); + clientsClient.release(); + + final ContentProviderClient tabsClient = getTabsClient(); + Assert.assertNotNull(tabsClient); + tabsClient.release(); + } + + @Test + public void testDeleteEmptyClients() throws RemoteException { + final Uri uri = BrowserContractHelpers.CLIENTS_CONTENT_URI; + final ContentProviderClient clientsClient = getClientsClient(); + + // Have to ensure that it's empty… + clientsClient.delete(uri, null, null); + + int deleted = clientsClient.delete(uri, null, null); + Assert.assertEquals(0, deleted); + } + + @Test + public void testDeleteEmptyTabs() throws RemoteException { + final ContentProviderClient tabsClient = getTabsClient(); + + // Have to ensure that it's empty… + deleteAllTestTabs(tabsClient); + + int deleted = deleteAllTestTabs(tabsClient); + Assert.assertEquals(0, deleted); + } + + @Test + public void testStoreAndRetrieveClients() throws RemoteException { + final Uri uri = BrowserContractHelpers.CLIENTS_CONTENT_URI; + final ContentProviderClient clientsClient = getClientsClient(); + + // Have to ensure that it's empty… + clientsClient.delete(uri, null, null); + + final long now = System.currentTimeMillis(); + final ContentValues first = new ContentValues(); + final ContentValues second = new ContentValues(); + first.put(BrowserContract.Clients.GUID, "abcdefghijkl"); + first.put(BrowserContract.Clients.NAME, "Frist Psot"); + first.put(BrowserContract.Clients.LAST_MODIFIED, now + 1); + second.put(BrowserContract.Clients.GUID, "mnopqrstuvwx"); + second.put(BrowserContract.Clients.NAME, "Second!!1!"); + second.put(BrowserContract.Clients.LAST_MODIFIED, now + 2); + + ContentValues[] values = new ContentValues[] { first, second }; + final int inserted = clientsClient.bulkInsert(uri, values); + Assert.assertEquals(2, inserted); + + final String since = BrowserContract.Clients.LAST_MODIFIED + " >= ?"; + final String[] nowArg = new String[] { String.valueOf(now) }; + final String guidAscending = BrowserContract.Clients.GUID + " ASC"; + Cursor cursor = clientsClient.query(uri, null, since, nowArg, guidAscending); + + Assert.assertNotNull(cursor); + try { + Assert.assertTrue(cursor.moveToFirst()); + Assert.assertEquals(2, cursor.getCount()); + + final String g1 = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.Clients.GUID)); + final String n1 = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.Clients.NAME)); + final long m1 = cursor.getLong(cursor.getColumnIndexOrThrow(BrowserContract.Clients.LAST_MODIFIED)); + Assert.assertEquals(first.get(BrowserContract.Clients.GUID), g1); + Assert.assertEquals(first.get(BrowserContract.Clients.NAME), n1); + Assert.assertEquals(now + 1, m1); + + Assert.assertTrue(cursor.moveToNext()); + final String g2 = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.Clients.GUID)); + final String n2 = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.Clients.NAME)); + final long m2 = cursor.getLong(cursor.getColumnIndexOrThrow(BrowserContract.Clients.LAST_MODIFIED)); + Assert.assertEquals(second.get(BrowserContract.Clients.GUID), g2); + Assert.assertEquals(second.get(BrowserContract.Clients.NAME), n2); + Assert.assertEquals(now + 2, m2); + + Assert.assertFalse(cursor.moveToNext()); + } finally { + cursor.close(); + } + + int deleted = clientsClient.delete(uri, null, null); + Assert.assertEquals(2, deleted); + } + + @Test + public void testTabFromCursor() throws Exception { + final ContentProviderClient tabsClient = getTabsClient(); + final ContentProviderClient clientsClient = getClientsClient(); + + deleteAllTestTabs(tabsClient); + deleteTestClient(clientsClient); + insertTestClient(clientsClient); + insertSomeTestTabs(tabsClient); + + final String positionAscending = BrowserContract.Tabs.POSITION + " ASC"; + Cursor cursor = null; + try { + cursor = tabsClient.query(BrowserContractHelpers.TABS_CONTENT_URI, null, TABS_CLIENT_GUID_IS, new String[] { TEST_CLIENT_GUID }, positionAscending); + Assert.assertEquals(3, cursor.getCount()); + + cursor.moveToFirst(); + final Tab parsed1 = Tab.fromCursor(cursor); + Assert.assertEquals(testTab1, parsed1); + + cursor.moveToNext(); + final Tab parsed2 = Tab.fromCursor(cursor); + Assert.assertEquals(testTab2, parsed2); + + cursor.moveToPosition(2); + final Tab parsed3 = Tab.fromCursor(cursor); + Assert.assertEquals(testTab3, parsed3); + } finally { + cursor.close(); + } + } + + @Test + public void testDeletingClientDeletesTabs() throws Exception { + final ContentProviderClient tabsClient = getTabsClient(); + final ContentProviderClient clientsClient = getClientsClient(); + + deleteAllTestTabs(tabsClient); + deleteTestClient(clientsClient); + insertTestClient(clientsClient); + insertSomeTestTabs(tabsClient); + + // Delete just the client... + clientsClient.delete(BrowserContractHelpers.CLIENTS_CONTENT_URI, CLIENTS_GUID_IS, new String [] { TEST_CLIENT_GUID }); + + Cursor cursor = null; + try { + cursor = tabsClient.query(BrowserContractHelpers.TABS_CONTENT_URI, null, TABS_CLIENT_GUID_IS, new String[] { TEST_CLIENT_GUID }, null); + // ... and all that client's tabs should be removed. + Assert.assertEquals(0, cursor.getCount()); + } finally { + cursor.close(); + } + } + + @Test + public void testTabsRecordFromCursor() throws Exception { + final ContentProviderClient tabsClient = getTabsClient(); + + deleteAllTestTabs(tabsClient); + insertTestClient(getClientsClient()); + insertSomeTestTabs(tabsClient); + + final String positionAscending = BrowserContract.Tabs.POSITION + " ASC"; + Cursor cursor = null; + try { + cursor = tabsClient.query(BrowserContractHelpers.TABS_CONTENT_URI, null, TABS_CLIENT_GUID_IS, new String[] { TEST_CLIENT_GUID }, positionAscending); + Assert.assertEquals(3, cursor.getCount()); + + cursor.moveToPosition(1); + + final TabsRecord tabsRecord = FennecTabsRepository.tabsRecordFromCursor(cursor, TEST_CLIENT_GUID, TEST_CLIENT_NAME); + + // Make sure we clean up after ourselves. + Assert.assertEquals(1, cursor.getPosition()); + + Assert.assertEquals(TEST_CLIENT_GUID, tabsRecord.guid); + Assert.assertEquals(TEST_CLIENT_NAME, tabsRecord.clientName); + + Assert.assertEquals(3, tabsRecord.tabs.size()); + Assert.assertEquals(testTab1, tabsRecord.tabs.get(0)); + Assert.assertEquals(testTab2, tabsRecord.tabs.get(1)); + Assert.assertEquals(testTab3, tabsRecord.tabs.get(2)); + + Assert.assertEquals(Math.max(Math.max(testTab1.lastUsed, testTab2.lastUsed), testTab3.lastUsed), tabsRecord.lastModified); + } finally { + cursor.close(); + } + } + + // Verify that we can fetch a record when there are no local tabs at all. + @Test + public void testEmptyTabsRecordFromCursor() throws Exception { + final ContentProviderClient tabsClient = getTabsClient(); + + deleteAllTestTabs(tabsClient); + + final String positionAscending = BrowserContract.Tabs.POSITION + " ASC"; + Cursor cursor = null; + try { + cursor = tabsClient.query(BrowserContractHelpers.TABS_CONTENT_URI, null, TABS_CLIENT_GUID_IS, new String[] { TEST_CLIENT_GUID }, positionAscending); + Assert.assertEquals(0, cursor.getCount()); + + final TabsRecord tabsRecord = FennecTabsRepository.tabsRecordFromCursor(cursor, TEST_CLIENT_GUID, TEST_CLIENT_NAME); + + Assert.assertEquals(TEST_CLIENT_GUID, tabsRecord.guid); + Assert.assertEquals(TEST_CLIENT_NAME, tabsRecord.clientName); + + Assert.assertNotNull(tabsRecord.tabs); + Assert.assertEquals(0, tabsRecord.tabs.size()); + + Assert.assertEquals(0, tabsRecord.lastModified); + } finally { + cursor.close(); + } + } + + // Not much of a test, but verifies the tabs record at least agrees with the + // disk data and doubles as a database inspector. + @Test + public void testLocalTabs() throws Exception { + final ContentProviderClient tabsClient = getTabsClient(); + + final String positionAscending = BrowserContract.Tabs.POSITION + " ASC"; + Cursor cursor = null; + try { + // Keep this in sync with the Fennec schema. + cursor = tabsClient.query(BrowserContractHelpers.TABS_CONTENT_URI, null, BrowserContract.Tabs.CLIENT_GUID + " IS NULL", null, positionAscending); + CursorDumper.dumpCursor(cursor); + + final TabsRecord tabsRecord = FennecTabsRepository.tabsRecordFromCursor(cursor, TEST_CLIENT_GUID, TEST_CLIENT_NAME); + + Assert.assertEquals(TEST_CLIENT_GUID, tabsRecord.guid); + Assert.assertEquals(TEST_CLIENT_NAME, tabsRecord.clientName); + + Assert.assertNotNull(tabsRecord.tabs); + Assert.assertEquals(cursor.getCount(), tabsRecord.tabs.size()); + } finally { + cursor.close(); + } + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/db/TestTabsProviderRemoteTabs.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/db/TestTabsProviderRemoteTabs.java new file mode 100644 index 000000000..e63cb9b46 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/db/TestTabsProviderRemoteTabs.java @@ -0,0 +1,244 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.db; + +import android.content.ContentProviderClient; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.GeckoProfile; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.db.BrowserContract; +import org.mozilla.gecko.db.LocalTabsAccessor; +import org.mozilla.gecko.db.RemoteClient; +import org.mozilla.gecko.db.TabsProvider; +import org.mozilla.gecko.sync.repositories.android.BrowserContractHelpers; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.internal.runtime.RuntimeAdapter; +import org.robolectric.shadows.ShadowContentResolver; + +import java.util.List; + +@RunWith(TestRunner.class) +public class TestTabsProviderRemoteTabs { + private static final long ONE_DAY_IN_MILLISECONDS = 1000 * 60 * 60 * 24; + private static final long ONE_WEEK_IN_MILLISECONDS = 7 * ONE_DAY_IN_MILLISECONDS; + private static final long THREE_WEEKS_IN_MILLISECONDS = 3 * ONE_WEEK_IN_MILLISECONDS; + + protected TabsProvider provider; + + @Before + public void setUp() { + provider = new TabsProvider(); + provider.onCreate(); + ShadowContentResolver.registerProvider(BrowserContract.TABS_AUTHORITY, new DelegatingTestContentProvider(provider)); + } + + @After + public void tearDown() throws Exception { + provider.shutdown(); + provider = null; + } + + protected ContentProviderClient getClientsClient() { + final ShadowContentResolver cr = new ShadowContentResolver(); + return cr.acquireContentProviderClient(BrowserContractHelpers.CLIENTS_CONTENT_URI); + } + + @Test + public void testGetClientsWithoutTabsByRecencyFromCursor() throws Exception { + final Uri uri = BrowserContractHelpers.CLIENTS_CONTENT_URI; + final ContentProviderClient cpc = getClientsClient(); + final LocalTabsAccessor accessor = new LocalTabsAccessor("test"); // The profile name given doesn't matter. + + try { + // Delete all tabs to begin with. + cpc.delete(uri, null, null); + Cursor allClients = cpc.query(uri, null, null, null, null); + try { + Assert.assertEquals(0, allClients.getCount()); + } finally { + allClients.close(); + } + + // Insert a local and remote1 client record, neither with tabs. + final long now = System.currentTimeMillis(); + // Local client has GUID = null. + final ContentValues local = new ContentValues(); + local.put(BrowserContract.Clients.NAME, "local"); + local.put(BrowserContract.Clients.LAST_MODIFIED, now + 1); + // Remote clients have GUID != null. + final ContentValues remote1 = new ContentValues(); + remote1.put(BrowserContract.Clients.GUID, "guid1"); + remote1.put(BrowserContract.Clients.NAME, "remote1"); + remote1.put(BrowserContract.Clients.LAST_MODIFIED, now + 2); + + final ContentValues remote2 = new ContentValues(); + remote2.put(BrowserContract.Clients.GUID, "guid2"); + remote2.put(BrowserContract.Clients.NAME, "remote2"); + remote2.put(BrowserContract.Clients.LAST_MODIFIED, now + 3); + + ContentValues[] values = new ContentValues[]{local, remote1, remote2}; + int inserted = cpc.bulkInsert(uri, values); + Assert.assertEquals(3, inserted); + + allClients = cpc.query(BrowserContract.Clients.CONTENT_RECENCY_URI, null, null, null, null); + try { + CursorDumper.dumpCursor(allClients); + // The local client is not ignored. + Assert.assertEquals(3, allClients.getCount()); + final List<RemoteClient> clients = accessor.getClientsWithoutTabsByRecencyFromCursor(allClients); + Assert.assertEquals(3, clients.size()); + for (RemoteClient client : clients) { + // Each client should not have any tabs. + Assert.assertNotNull(client.tabs); + Assert.assertEquals(0, client.tabs.size()); + } + // Since there are no tabs, the order should be based on last_modified. + Assert.assertEquals("guid2", clients.get(0).guid); + Assert.assertEquals("guid1", clients.get(1).guid); + Assert.assertEquals(null, clients.get(2).guid); + } finally { + allClients.close(); + } + + // Now let's add a few tabs to one client. The times are chosen so that one tab's + // last used is not relevant, and the other tab is the most recent used. + final ContentValues remoteTab1 = new ContentValues(); + remoteTab1.put(BrowserContract.Tabs.CLIENT_GUID, "guid1"); + remoteTab1.put(BrowserContract.Tabs.TITLE, "title1"); + remoteTab1.put(BrowserContract.Tabs.URL, "http://test.com/test1"); + remoteTab1.put(BrowserContract.Tabs.HISTORY, "[\"http://test.com/test1\"]"); + remoteTab1.put(BrowserContract.Tabs.LAST_USED, now); + remoteTab1.put(BrowserContract.Tabs.POSITION, 0); + + final ContentValues remoteTab2 = new ContentValues(); + remoteTab2.put(BrowserContract.Tabs.CLIENT_GUID, "guid1"); + remoteTab2.put(BrowserContract.Tabs.TITLE, "title2"); + remoteTab2.put(BrowserContract.Tabs.URL, "http://test.com/test2"); + remoteTab2.put(BrowserContract.Tabs.HISTORY, "[\"http://test.com/test2\"]"); + remoteTab2.put(BrowserContract.Tabs.LAST_USED, now + 5); + remoteTab2.put(BrowserContract.Tabs.POSITION, 1); + + values = new ContentValues[]{remoteTab1, remoteTab2}; + inserted = cpc.bulkInsert(BrowserContract.Tabs.CONTENT_URI, values); + Assert.assertEquals(2, inserted); + + allClients = cpc.query(BrowserContract.Clients.CONTENT_RECENCY_URI, null, BrowserContract.Clients.GUID + " IS NOT NULL", null, null); + try { + CursorDumper.dumpCursor(allClients); + // The local client is ignored. + Assert.assertEquals(2, allClients.getCount()); + final List<RemoteClient> clients = accessor.getClientsWithoutTabsByRecencyFromCursor(allClients); + Assert.assertEquals(2, clients.size()); + for (RemoteClient client : clients) { + // Each client should be remote and should not have any tabs. + Assert.assertNotNull(client.guid); + Assert.assertNotNull(client.tabs); + Assert.assertEquals(0, client.tabs.size()); + } + // Since now there is a tab attached to the remote2 client more recent than the + // remote1 client modified time, it should be first. + Assert.assertEquals("guid1", clients.get(0).guid); + Assert.assertEquals("guid2", clients.get(1).guid); + } finally { + allClients.close(); + } + } finally { + cpc.release(); + } + } + + @Test + public void testGetRecentRemoteClientsUpToOneWeekOld() throws Exception { + final Uri uri = BrowserContractHelpers.CLIENTS_CONTENT_URI; + final ContentProviderClient cpc = getClientsClient(); + final LocalTabsAccessor accessor = new LocalTabsAccessor("test"); // The profile name given doesn't matter. + final Context context = RuntimeEnvironment.application.getApplicationContext(); + + try { + // Start Clean + cpc.delete(uri, null, null); + final Cursor allClients = cpc.query(uri, null, null, null, null); + try { + Assert.assertEquals(0, allClients.getCount()); + } finally { + allClients.close(); + } + + // Insert a local and remote1 client record, neither with tabs. + final long now = System.currentTimeMillis(); + // Local client has GUID = null. + final ContentValues local = new ContentValues(); + local.put(BrowserContract.Clients.NAME, "local"); + local.put(BrowserContract.Clients.LAST_MODIFIED, now + 1); + // Remote clients have GUID != null. + final ContentValues remote1 = new ContentValues(); + remote1.put(BrowserContract.Clients.GUID, "guid1"); + remote1.put(BrowserContract.Clients.NAME, "remote1"); + remote1.put(BrowserContract.Clients.LAST_MODIFIED, now + 2); + + // Insert a Remote Client that is 6 days old. + final ContentValues remote2 = new ContentValues(); + remote2.put(BrowserContract.Clients.GUID, "guid2"); + remote2.put(BrowserContract.Clients.NAME, "remote2"); + remote2.put(BrowserContract.Clients.LAST_MODIFIED, now - ONE_WEEK_IN_MILLISECONDS + ONE_DAY_IN_MILLISECONDS); + + // Insert a Remote Client with the same name as previous but with more than 3 weeks old + final ContentValues remote3 = new ContentValues(); + remote3.put(BrowserContract.Clients.GUID, "guid21"); + remote3.put(BrowserContract.Clients.NAME, "remote2"); + remote3.put(BrowserContract.Clients.LAST_MODIFIED, now - THREE_WEEKS_IN_MILLISECONDS - ONE_DAY_IN_MILLISECONDS); + + // Insert another remote client with the same name as previous but with 3 weeks - 1 day old. + final ContentValues remote4 = new ContentValues(); + remote4.put(BrowserContract.Clients.GUID, "guid22"); + remote4.put(BrowserContract.Clients.NAME, "remote2"); + remote4.put(BrowserContract.Clients.LAST_MODIFIED, now - THREE_WEEKS_IN_MILLISECONDS + ONE_DAY_IN_MILLISECONDS); + + // Insert a Remote Client that is exactly one week old. + final ContentValues remote5 = new ContentValues(); + remote5.put(BrowserContract.Clients.GUID, "guid3"); + remote5.put(BrowserContract.Clients.NAME, "remote3"); + remote5.put(BrowserContract.Clients.LAST_MODIFIED, now - ONE_WEEK_IN_MILLISECONDS); + + ContentValues[] values = new ContentValues[]{local, remote1, remote2, remote3, remote4, remote5}; + int inserted = cpc.bulkInsert(uri, values); + Assert.assertEquals(values.length, inserted); + + final Cursor remoteClients = + accessor.getRemoteClientsByRecencyCursor(context); + + try { + CursorDumper.dumpCursor(remoteClients); + // Local client is not included. + // (remote1, guid1), (remote2, guid2), (remote3, guid3) are expected. + Assert.assertEquals(3, remoteClients.getCount()); + + // Check the inner data, according to recency. + List<RemoteClient> recentRemoteClientsList = + accessor.getClientsWithoutTabsByRecencyFromCursor(remoteClients); + Assert.assertEquals(3, recentRemoteClientsList.size()); + Assert.assertEquals("remote1", recentRemoteClientsList.get(0).name); + Assert.assertEquals("guid1", recentRemoteClientsList.get(0).guid); + Assert.assertEquals("remote2", recentRemoteClientsList.get(1).name); + Assert.assertEquals("guid2", recentRemoteClientsList.get(1).guid); + Assert.assertEquals("remote3", recentRemoteClientsList.get(2).name); + Assert.assertEquals("guid3", recentRemoteClientsList.get(2).guid); + } finally { + remoteClients.close(); + } + } finally { + cpc.release(); + } + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/fxa/test/TestFxAccountClient20.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/fxa/test/TestFxAccountClient20.java new file mode 100644 index 000000000..d075cc0ec --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/fxa/test/TestFxAccountClient20.java @@ -0,0 +1,41 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.fxa.test; + +import junit.framework.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.fxa.FxAccountClient20; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.sync.net.BaseResource; + +import java.io.UnsupportedEncodingException; +import java.net.URISyntaxException; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; + +@RunWith(TestRunner.class) +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()); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/fxa/test/TestFxAccountUtils.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/fxa/test/TestFxAccountUtils.java new file mode 100644 index 000000000..e6461776e --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/fxa/test/TestFxAccountUtils.java @@ -0,0 +1,131 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.fxa.test; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.apache.commons.codec.binary.Base64; +import org.mozilla.gecko.background.fxa.FxAccountUtils; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.sync.Utils; +import org.mozilla.gecko.sync.crypto.KeyBundle; +import org.mozilla.gecko.sync.net.SRPConstants; + +import java.math.BigInteger; + +/** + * 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>. + */ +@RunWith(TestRunner.class) +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")); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/test/EntityTestHelper.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/test/EntityTestHelper.java new file mode 100644 index 000000000..976a8eda1 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/test/EntityTestHelper.java @@ -0,0 +1,34 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.test; + +import ch.boye.httpclientandroidlib.HttpEntity; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +public class EntityTestHelper { + private static final int DEFAULT_SIZE = 1024; + + public static byte[] bytesFromEntity(final HttpEntity entity) throws IOException { + final InputStream is = entity.getContent(); + + if (is instanceof ByteArrayInputStream) { + final int size = is.available(); + final byte[] buffer = new byte[size]; + is.read(buffer, 0, size); + return buffer; + } + + final ByteArrayOutputStream bos = new ByteArrayOutputStream(); + final byte[] buffer = new byte[DEFAULT_SIZE]; + int len; + while ((len = is.read(buffer, 0, DEFAULT_SIZE)) != -1) { + bos.write(buffer, 0, len); + } + return bos.toByteArray(); + } +}
\ No newline at end of file diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/BaseMockServerSyncStage.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/BaseMockServerSyncStage.java new file mode 100644 index 000000000..d9aa936f0 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/BaseMockServerSyncStage.java @@ -0,0 +1,72 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.testhelpers; + +import org.mozilla.gecko.sync.NoCollectionKeysSetException; +import org.mozilla.gecko.sync.NonObjectJSONException; +import org.mozilla.gecko.sync.SynchronizerConfiguration; +import org.mozilla.gecko.sync.repositories.RecordFactory; +import org.mozilla.gecko.sync.repositories.Repository; +import org.mozilla.gecko.sync.stage.ServerSyncStage; + +import java.io.IOException; +import java.net.URISyntaxException; + +/** + * A stage that joins two Repositories with no wrapping. + */ +public abstract class BaseMockServerSyncStage extends ServerSyncStage { + + public Repository local; + public Repository remote; + public String name; + public String collection; + public int version = 1; + + @Override + public boolean isEnabled() { + return true; + } + + @Override + protected String getCollection() { + return collection; + } + + @Override + protected Repository getLocalRepository() { + return local; + } + + @Override + protected Repository getRemoteRepository() throws URISyntaxException { + return remote; + } + + @Override + protected String getEngineName() { + return name; + } + + @Override + public Integer getStorageVersion() { + return version; + } + + @Override + protected RecordFactory getRecordFactory() { + return null; + } + + @Override + protected Repository wrappedServerRepo() + throws NoCollectionKeysSetException, URISyntaxException { + return getRemoteRepository(); + } + + public SynchronizerConfiguration leakConfig() + throws NonObjectJSONException, IOException { + return this.getConfig(); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/CommandHelpers.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/CommandHelpers.java new file mode 100644 index 000000000..48217f1b0 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/CommandHelpers.java @@ -0,0 +1,40 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.testhelpers; + +import org.json.simple.JSONArray; +import org.mozilla.gecko.sync.CommandProcessor.Command; + +public class CommandHelpers { + + @SuppressWarnings("unchecked") + public static Command getCommand1() { + JSONArray args = new JSONArray(); + args.add("argsA"); + return new Command("displayURI", args); + } + + @SuppressWarnings("unchecked") + public static Command getCommand2() { + JSONArray args = new JSONArray(); + args.add("argsB"); + return new Command("displayURI", args); + } + + @SuppressWarnings("unchecked") + public static Command getCommand3() { + JSONArray args = new JSONArray(); + args.add("argsC"); + return new Command("displayURI", args); + } + + @SuppressWarnings("unchecked") + public static Command getCommand4() { + JSONArray args = new JSONArray(); + args.add("URI of Page"); + args.add("Sender ID"); + args.add("Title of Page"); + return new Command("displayURI", args); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/DefaultGlobalSessionCallback.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/DefaultGlobalSessionCallback.java new file mode 100644 index 000000000..373dd4eab --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/DefaultGlobalSessionCallback.java @@ -0,0 +1,51 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.testhelpers; + +import org.mozilla.gecko.sync.GlobalSession; +import org.mozilla.gecko.sync.delegates.GlobalSessionCallback; +import org.mozilla.gecko.sync.stage.GlobalSyncStage.Stage; + +import java.net.URI; + +public class DefaultGlobalSessionCallback implements GlobalSessionCallback { + + @Override + public void requestBackoff(long backoff) { + } + + @Override + public void informUnauthorizedResponse(GlobalSession globalSession, + URI oldClusterURL) { + } + @Override + public void informUpgradeRequiredResponse(GlobalSession session) { + } + + @Override + public void informMigrated(GlobalSession globalSession) { + } + + @Override + public void handleAborted(GlobalSession globalSession, String reason) { + } + + @Override + public void handleError(GlobalSession globalSession, Exception ex) { + } + + @Override + public void handleSuccess(GlobalSession globalSession) { + } + + @Override + public void handleStageCompleted(Stage currentState, + GlobalSession globalSession) { + } + + @Override + public boolean shouldBackOffStorage() { + return false; + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/MockAbstractNonRepositorySyncStage.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/MockAbstractNonRepositorySyncStage.java new file mode 100644 index 000000000..d8380df97 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/MockAbstractNonRepositorySyncStage.java @@ -0,0 +1,13 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.testhelpers; + +import org.mozilla.gecko.sync.stage.AbstractNonRepositorySyncStage; + +public class MockAbstractNonRepositorySyncStage extends AbstractNonRepositorySyncStage { + @Override + public void execute() { + session.advance(); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/MockClientsDataDelegate.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/MockClientsDataDelegate.java new file mode 100644 index 000000000..f4af51f64 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/MockClientsDataDelegate.java @@ -0,0 +1,66 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.testhelpers; + +import org.mozilla.gecko.sync.Utils; +import org.mozilla.gecko.sync.delegates.ClientsDataDelegate; + +public class MockClientsDataDelegate implements ClientsDataDelegate { + private String accountGUID; + private String clientName; + private int clientsCount; + private long clientDataTimestamp = 0; + + @Override + public synchronized String getAccountGUID() { + if (accountGUID == null) { + accountGUID = Utils.generateGuid(); + } + return accountGUID; + } + + @Override + public synchronized String getDefaultClientName() { + return "Default client"; + } + + @Override + public synchronized void setClientName(String clientName, long now) { + this.clientName = clientName; + this.clientDataTimestamp = now; + } + + @Override + public synchronized String getClientName() { + if (clientName == null) { + setClientName(getDefaultClientName(), System.currentTimeMillis()); + } + return clientName; + } + + @Override + public synchronized void setClientsCount(int clientsCount) { + this.clientsCount = clientsCount; + } + + @Override + public synchronized int getClientsCount() { + return clientsCount; + } + + @Override + public synchronized boolean isLocalGUID(String guid) { + return getAccountGUID().equals(guid); + } + + @Override + public synchronized long getLastModifiedTimestamp() { + return clientDataTimestamp; + } + + @Override + public String getFormFactor() { + return "phone"; + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/MockClientsDatabaseAccessor.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/MockClientsDatabaseAccessor.java new file mode 100644 index 000000000..b1aeb7cd1 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/MockClientsDatabaseAccessor.java @@ -0,0 +1,76 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.testhelpers; + +import org.mozilla.gecko.sync.CommandProcessor.Command; +import org.mozilla.gecko.sync.repositories.NullCursorException; +import org.mozilla.gecko.sync.repositories.android.ClientsDatabaseAccessor; +import org.mozilla.gecko.sync.repositories.domain.ClientRecord; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +public class MockClientsDatabaseAccessor extends ClientsDatabaseAccessor { + public boolean storedRecord = false; + public boolean dbWiped = false; + public boolean clientsTableWiped = false; + public boolean closed = false; + public boolean storedArrayList = false; + public boolean storedCommand; + + @Override + public void store(ClientRecord record) { + storedRecord = true; + } + + @Override + public void store(Collection<ClientRecord> records) { + storedArrayList = false; + } + + @Override + public void store(String accountGUID, Command command) throws NullCursorException { + storedCommand = true; + } + + @Override + public ClientRecord fetchClient(String profileID) throws NullCursorException { + return null; + } + + @Override + public Map<String, ClientRecord> fetchAllClients() throws NullCursorException { + return null; + } + + @Override + public List<Command> fetchCommandsForClient(String accountGUID) throws NullCursorException { + return null; + } + + @Override + public int clientsCount() { + return 0; + } + + @Override + public void wipeDB() { + dbWiped = true; + } + + @Override + public void wipeClientsTable() { + clientsTableWiped = true; + } + + @Override + public void close() { + closed = true; + } + + public void resetVars() { + storedRecord = dbWiped = clientsTableWiped = closed = storedArrayList = false; + } +}
\ No newline at end of file diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/MockGlobalSession.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/MockGlobalSession.java new file mode 100644 index 000000000..63afdd1ac --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/MockGlobalSession.java @@ -0,0 +1,57 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.testhelpers; + +import org.mozilla.gecko.sync.EngineSettings; +import org.mozilla.gecko.sync.NonObjectJSONException; +import org.mozilla.gecko.sync.SyncConfiguration; +import org.mozilla.gecko.sync.SyncConfigurationException; +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.CompletedStage; +import org.mozilla.gecko.sync.stage.GlobalSyncStage; +import org.mozilla.gecko.sync.stage.GlobalSyncStage.Stage; + +import java.io.IOException; +import java.util.HashMap; + + +public class MockGlobalSession extends MockPrefsGlobalSession { + + public MockGlobalSession(String username, String password, KeyBundle keyBundle, GlobalSessionCallback callback) throws SyncConfigurationException, IllegalArgumentException, NonObjectJSONException, IOException { + this(new SyncConfiguration(username, new BasicAuthHeaderProvider(username, password), new MockSharedPreferences(), keyBundle), callback); + } + + public MockGlobalSession(SyncConfiguration config, GlobalSessionCallback callback) + throws SyncConfigurationException, IllegalArgumentException, IOException, NonObjectJSONException { + super(config, callback, null, null); + } + + @Override + public boolean isEngineRemotelyEnabled(String engine, EngineSettings engineSettings) { + return false; + } + + @Override + protected void prepareStages() { + super.prepareStages(); + HashMap<Stage, GlobalSyncStage> newStages = new HashMap<Stage, GlobalSyncStage>(this.stages); + + for (Stage stage : this.stages.keySet()) { + newStages.put(stage, new MockServerSyncStage()); + } + + // This signals that the global session is complete. + newStages.put(Stage.completed, new CompletedStage()); + + this.stages = newStages; + } + + public MockGlobalSession withStage(Stage stage, GlobalSyncStage syncStage) { + stages.put(stage, syncStage); + + return this; + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/MockPrefsGlobalSession.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/MockPrefsGlobalSession.java new file mode 100644 index 000000000..c864cdf80 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/MockPrefsGlobalSession.java @@ -0,0 +1,60 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.testhelpers; + +import android.content.Context; +import android.content.SharedPreferences; + +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.crypto.KeyBundle; +import org.mozilla.gecko.sync.delegates.ClientsDataDelegate; +import org.mozilla.gecko.sync.delegates.GlobalSessionCallback; +import org.mozilla.gecko.sync.net.AuthHeaderProvider; +import org.mozilla.gecko.sync.net.BasicAuthHeaderProvider; + +import java.io.IOException; + +/** + * GlobalSession touches the Android prefs system. Stub that out. + */ +public class MockPrefsGlobalSession extends GlobalSession { + + public MockSharedPreferences prefs; + + public MockPrefsGlobalSession( + SyncConfiguration config, GlobalSessionCallback callback, Context context, + ClientsDataDelegate clientsDelegate) + throws SyncConfigurationException, IllegalArgumentException, IOException, NonObjectJSONException { + super(config, callback, context, clientsDelegate); + } + + public static MockPrefsGlobalSession getSession( + String username, String password, + KeyBundle syncKeyBundle, GlobalSessionCallback callback, Context context, + ClientsDataDelegate clientsDelegate) + throws SyncConfigurationException, IllegalArgumentException, IOException, NonObjectJSONException { + return getSession(username, new BasicAuthHeaderProvider(username, password), null, + syncKeyBundle, callback, context, clientsDelegate); + } + + public static MockPrefsGlobalSession getSession( + String username, AuthHeaderProvider authHeaderProvider, String prefsPath, + KeyBundle syncKeyBundle, GlobalSessionCallback callback, Context context, + ClientsDataDelegate clientsDelegate) + throws SyncConfigurationException, IllegalArgumentException, IOException, NonObjectJSONException { + + final SharedPreferences prefs = new MockSharedPreferences(); + final SyncConfiguration config = new SyncConfiguration(username, authHeaderProvider, prefs); + config.syncKeyBundle = syncKeyBundle; + return new MockPrefsGlobalSession(config, callback, context, clientsDelegate); + } + + @Override + public Context getContext() { + return null; + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/MockRecord.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/MockRecord.java new file mode 100644 index 000000000..9876b7867 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/MockRecord.java @@ -0,0 +1,51 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.testhelpers; + +import org.mozilla.gecko.sync.ExtendedJSONObject; +import org.mozilla.gecko.sync.repositories.domain.Record; + +import java.util.Random; + +public class MockRecord extends Record { + private final int payloadByteCount; + public MockRecord(String guid, String collection, long lastModified, boolean deleted) { + super(guid, collection, lastModified, deleted); + // Payload used to be "foo", so let's not stray too far. + // Perhaps some tests "depend" on that payload size. + payloadByteCount = 3; + } + + public MockRecord(String guid, String collection, long lastModified, boolean deleted, int payloadByteCount) { + super(guid, collection, lastModified, deleted); + this.payloadByteCount = payloadByteCount; + } + + @Override + protected void populatePayload(ExtendedJSONObject payload) { + } + + @Override + protected void initFromPayload(ExtendedJSONObject payload) { + } + + @Override + public Record copyWithIDs(String guid, long androidID) { + MockRecord r = new MockRecord(guid, this.collection, this.lastModified, this.deleted); + r.androidID = androidID; + return r; + } + + @Override + public String toJSONString() { + // Build up a randomish payload string based on the length we were asked for. + final Random random = new Random(); + final char[] payloadChars = new char[payloadByteCount]; + for (int i = 0; i < payloadByteCount; i++) { + payloadChars[i] = (char) (random.nextInt(26) + 'a'); + } + final String payloadString = new String(payloadChars); + return "{\"id\":\"" + guid + "\", \"payload\": \"" + payloadString+ "\"}"; + } +}
\ No newline at end of file diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/MockServerSyncStage.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/MockServerSyncStage.java new file mode 100644 index 000000000..28a4e58b9 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/MockServerSyncStage.java @@ -0,0 +1,11 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.testhelpers; + +public class MockServerSyncStage extends BaseMockServerSyncStage { + @Override + public void execute() { + session.advance(); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/MockSharedPreferences.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/MockSharedPreferences.java new file mode 100644 index 000000000..bc49fa7fb --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/MockSharedPreferences.java @@ -0,0 +1,137 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.testhelpers; + +import android.content.SharedPreferences; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +/** + * A programmable mock content provider. + */ +public class MockSharedPreferences implements SharedPreferences, SharedPreferences.Editor { + private HashMap<String, Object> mValues; + private HashMap<String, Object> mTempValues; + + public MockSharedPreferences() { + mValues = new HashMap<String, Object>(); + mTempValues = new HashMap<String, Object>(); + } + + public Editor edit() { + return this; + } + + public boolean contains(String key) { + return mValues.containsKey(key); + } + + public Map<String, ?> getAll() { + return new HashMap<String, Object>(mValues); + } + + public boolean getBoolean(String key, boolean defValue) { + if (mValues.containsKey(key)) { + return ((Boolean)mValues.get(key)).booleanValue(); + } + return defValue; + } + + public float getFloat(String key, float defValue) { + if (mValues.containsKey(key)) { + return ((Float)mValues.get(key)).floatValue(); + } + return defValue; + } + + public int getInt(String key, int defValue) { + if (mValues.containsKey(key)) { + return ((Integer)mValues.get(key)).intValue(); + } + return defValue; + } + + public long getLong(String key, long defValue) { + if (mValues.containsKey(key)) { + return ((Long)mValues.get(key)).longValue(); + } + return defValue; + } + + public String getString(String key, String defValue) { + if (mValues.containsKey(key)) + return (String)mValues.get(key); + return defValue; + } + + @SuppressWarnings("unchecked") + public Set<String> getStringSet(String key, Set<String> defValues) { + if (mValues.containsKey(key)) { + return (Set<String>) mValues.get(key); + } + return defValues; + } + + public void registerOnSharedPreferenceChangeListener( + OnSharedPreferenceChangeListener listener) { + throw new UnsupportedOperationException(); + } + + public void unregisterOnSharedPreferenceChangeListener( + OnSharedPreferenceChangeListener listener) { + throw new UnsupportedOperationException(); + } + + public Editor putBoolean(String key, boolean value) { + mTempValues.put(key, Boolean.valueOf(value)); + return this; + } + + public Editor putFloat(String key, float value) { + mTempValues.put(key, value); + return this; + } + + public Editor putInt(String key, int value) { + mTempValues.put(key, value); + return this; + } + + public Editor putLong(String key, long value) { + mTempValues.put(key, value); + return this; + } + + public Editor putString(String key, String value) { + mTempValues.put(key, value); + return this; + } + + public Editor putStringSet(String key, Set<String> values) { + mTempValues.put(key, values); + return this; + } + + public Editor remove(String key) { + mTempValues.remove(key); + return this; + } + + public Editor clear() { + mTempValues.clear(); + return this; + } + + @SuppressWarnings("unchecked") + public boolean commit() { + mValues = (HashMap<String, Object>)mTempValues.clone(); + return true; + } + + public void apply() { + commit(); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/TestRunner.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/TestRunner.java new file mode 100644 index 000000000..ccb5276ed --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/TestRunner.java @@ -0,0 +1,125 @@ +/** + * The MIT License + * + * Copyright (c) 2010 Xtreme Labs and Pivotal Labs + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package org.mozilla.gecko.background.testhelpers; + +import org.junit.runners.model.InitializationError; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +import org.robolectric.manifest.AndroidManifest; +import org.robolectric.res.FileFsFile; +import org.robolectric.res.FsFile; +import org.robolectric.util.Logger; +import org.robolectric.util.ReflectionHelpers; + +/** + * Test runner customized for running unit tests either through the Gradle CLI or + * Android Studio. The runner uses the build type and build flavor to compute the + * resource, asset, and AndroidManifest paths. + * + * This test runner requires that you set the 'constants' field on the @Config + * annotation (or the org.robolectric.Config.properties file) for your tests. + * + * This is a modified version of + * https://github.com/robolectric/robolectric/blob/8676da2daa4c140679fb5903696b8191415cec8f/robolectric/src/main/java/org/robolectric/RobolectricGradleTestRunner.java + * that uses a Gradle `buildConfigField` to find build outputs. + * See https://github.com/robolectric/robolectric/issues/1648#issuecomment-113731011. + */ +public class TestRunner extends RobolectricTestRunner { + private FsFile buildFolder; + + public TestRunner(Class<?> klass) throws InitializationError { + super(klass); + } + + @Override + protected AndroidManifest getAppManifest(Config config) { + if (config.constants() == Void.class) { + Logger.error("Field 'constants' not specified in @Config annotation"); + Logger.error("This is required when using RobolectricGradleTestRunner!"); + throw new RuntimeException("No 'constants' field in @Config annotation!"); + } + + buildFolder = FileFsFile.from(getBuildDir(config)).join("intermediates"); + + final String type = getType(config); + final String flavor = getFlavor(config); + final String packageName = getPackageName(config); + + final FsFile assets = buildFolder.join("assets", flavor, type);; + final FsFile manifest = buildFolder.join("manifests", "full", flavor, type, "AndroidManifest.xml"); + + final FsFile res; + if (buildFolder.join("res", "merged").exists()) { + res = buildFolder.join("res", "merged", flavor, type); + } else if(buildFolder.join("res").exists()) { + res = buildFolder.join("res", flavor, type); + } else { + throw new IllegalStateException("No resource folder found"); + } + + Logger.debug("Robolectric assets directory: " + assets.getPath()); + Logger.debug(" Robolectric res directory: " + res.getPath()); + Logger.debug(" Robolectric manifest path: " + manifest.getPath()); + Logger.debug(" Robolectric package name: " + packageName); + return new AndroidManifest(manifest, res, assets, packageName); + } + + private static String getType(Config config) { + try { + return ReflectionHelpers.getStaticField(config.constants(), "BUILD_TYPE"); + } catch (Throwable e) { + return null; + } + } + + private static String getFlavor(Config config) { + try { + return ReflectionHelpers.getStaticField(config.constants(), "FLAVOR"); + } catch (Throwable e) { + return null; + } + } + + private static String getPackageName(Config config) { + try { + final String packageName = config.packageName(); + if (packageName != null && !packageName.isEmpty()) { + return packageName; + } else { + return ReflectionHelpers.getStaticField(config.constants(), "APPLICATION_ID"); + } + } catch (Throwable e) { + return null; + } + } + + private String getBuildDir(Config config) { + try { + return ReflectionHelpers.getStaticField(config.constants(), "BUILD_DIR"); + } catch (Throwable e) { + return null; + } + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/WBORepository.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/WBORepository.java new file mode 100644 index 000000000..672b0a602 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/WBORepository.java @@ -0,0 +1,230 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.testhelpers; + +import android.content.Context; +import org.mozilla.gecko.background.common.log.Logger; +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.RecordFilter; +import org.mozilla.gecko.sync.repositories.Repository; +import org.mozilla.gecko.sync.repositories.StoreTrackingRepositorySession; +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.delegates.RepositorySessionGuidsSinceDelegate; +import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionWipeDelegate; +import org.mozilla.gecko.sync.repositories.domain.Record; + +import java.util.Map.Entry; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public class WBORepository extends Repository { + + public class WBORepositoryStats { + public long created = -1; + public long begun = -1; + public long fetchBegan = -1; + public long fetchCompleted = -1; + public long storeBegan = -1; + public long storeCompleted = -1; + public long finished = -1; + } + + public static final String LOG_TAG = "WBORepository"; + + // Access to stats is not guarded. + public WBORepositoryStats stats; + + // Whether or not to increment the timestamp of stored records. + public final boolean bumpTimestamps; + + public class WBORepositorySession extends StoreTrackingRepositorySession { + + protected WBORepository wboRepository; + protected ExecutorService delegateExecutor = Executors.newSingleThreadExecutor(); + public ConcurrentHashMap<String, Record> wbos; + + public WBORepositorySession(WBORepository repository) { + super(repository); + + wboRepository = repository; + wbos = new ConcurrentHashMap<String, Record>(); + stats = new WBORepositoryStats(); + stats.created = now(); + } + + @Override + protected synchronized void trackGUID(String guid) { + if (wboRepository.shouldTrack()) { + super.trackGUID(guid); + } + } + + @Override + public void guidsSince(long timestamp, + RepositorySessionGuidsSinceDelegate delegate) { + throw new RuntimeException("guidsSince not implemented."); + } + + @Override + public void fetchSince(long timestamp, + RepositorySessionFetchRecordsDelegate delegate) { + long fetchBegan = now(); + stats.fetchBegan = fetchBegan; + RecordFilter filter = storeTracker.getFilter(); + + for (Entry<String, Record> entry : wbos.entrySet()) { + Record record = entry.getValue(); + if (record.lastModified >= timestamp) { + if (filter != null && + filter.excludeRecord(record)) { + Logger.debug(LOG_TAG, "Excluding record " + record.guid); + continue; + } + delegate.deferredFetchDelegate(delegateExecutor).onFetchedRecord(record); + } + } + long fetchCompleted = now(); + stats.fetchCompleted = fetchCompleted; + delegate.deferredFetchDelegate(delegateExecutor).onFetchCompleted(fetchCompleted); + } + + @Override + public void fetch(final String[] guids, + final RepositorySessionFetchRecordsDelegate delegate) { + long fetchBegan = now(); + stats.fetchBegan = fetchBegan; + for (String guid : guids) { + if (wbos.containsKey(guid)) { + delegate.deferredFetchDelegate(delegateExecutor).onFetchedRecord(wbos.get(guid)); + } + } + long fetchCompleted = now(); + stats.fetchCompleted = fetchCompleted; + delegate.deferredFetchDelegate(delegateExecutor).onFetchCompleted(fetchCompleted); + } + + @Override + public void fetchAll(final RepositorySessionFetchRecordsDelegate delegate) { + long fetchBegan = now(); + stats.fetchBegan = fetchBegan; + for (Entry<String, Record> entry : wbos.entrySet()) { + Record record = entry.getValue(); + delegate.deferredFetchDelegate(delegateExecutor).onFetchedRecord(record); + } + long fetchCompleted = now(); + stats.fetchCompleted = fetchCompleted; + delegate.deferredFetchDelegate(delegateExecutor).onFetchCompleted(fetchCompleted); + } + + @Override + public void store(final Record record) throws NoStoreDelegateException { + if (delegate == null) { + throw new NoStoreDelegateException(); + } + final long now = now(); + if (stats.storeBegan < 0) { + stats.storeBegan = now; + } + Record existing = wbos.get(record.guid); + Logger.debug(LOG_TAG, "Existing record is " + (existing == null ? "<null>" : (existing.guid + ", " + existing))); + if (existing != null && + existing.lastModified > record.lastModified) { + Logger.debug(LOG_TAG, "Local record is newer. Not storing."); + delegate.deferredStoreDelegate(delegateExecutor).onRecordStoreSucceeded(record.guid); + return; + } + if (existing != null) { + Logger.debug(LOG_TAG, "Replacing local record."); + } + + // Store a copy of the record with an updated modified time. + Record toStore = record.copyWithIDs(record.guid, record.androidID); + if (bumpTimestamps) { + toStore.lastModified = now; + } + wbos.put(record.guid, toStore); + + trackRecord(toStore); + delegate.deferredStoreDelegate(delegateExecutor).onRecordStoreSucceeded(record.guid); + } + + @Override + public void wipe(final RepositorySessionWipeDelegate delegate) { + if (!isActive()) { + delegate.onWipeFailed(new InactiveSessionException(null)); + return; + } + + Logger.info(LOG_TAG, "Wiping WBORepositorySession."); + this.wbos = new ConcurrentHashMap<String, Record>(); + + // Wipe immediately for the convenience of test code. + wboRepository.wbos = new ConcurrentHashMap<String, Record>(); + delegate.deferredWipeDelegate(delegateExecutor).onWipeSucceeded(); + } + + @Override + public void finish(RepositorySessionFinishDelegate delegate) throws InactiveSessionException { + Logger.info(LOG_TAG, "Finishing WBORepositorySession: handing back " + this.wbos.size() + " WBOs."); + wboRepository.wbos = this.wbos; + stats.finished = now(); + super.finish(delegate); + } + + @Override + public void begin(RepositorySessionBeginDelegate delegate) throws InvalidSessionTransitionException { + this.wbos = wboRepository.cloneWBOs(); + stats.begun = now(); + super.begin(delegate); + } + + @Override + public void storeDone(long end) { + // TODO: this is not guaranteed to be called after all of the record + // store callbacks have completed! + if (stats.storeBegan < 0) { + stats.storeBegan = end; + } + stats.storeCompleted = end; + delegate.deferredStoreDelegate(delegateExecutor).onStoreCompleted(end); + } + } + + public ConcurrentHashMap<String, Record> wbos; + + public WBORepository(boolean bumpTimestamps) { + super(); + this.bumpTimestamps = bumpTimestamps; + this.wbos = new ConcurrentHashMap<String, Record>(); + } + + public WBORepository() { + this(false); + } + + public synchronized boolean shouldTrack() { + return false; + } + + @Override + public void createSession(RepositorySessionCreationDelegate delegate, + Context context) { + delegate.deferredCreationDelegate().onSessionCreated(new WBORepositorySession(this)); + } + + public ConcurrentHashMap<String, Record> cloneWBOs() { + ConcurrentHashMap<String, Record> out = new ConcurrentHashMap<String, Record>(); + for (Entry<String, Record> entry : wbos.entrySet()) { + out.put(entry.getKey(), entry.getValue()); // Assume that records are + // immutable. + } + return out; + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/WaitHelper.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/WaitHelper.java new file mode 100644 index 000000000..dad748df1 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/WaitHelper.java @@ -0,0 +1,172 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.testhelpers; + +import org.mozilla.gecko.background.common.log.Logger; + +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; + +/** + * Implements waiting for asynchronous test events. + * + * Call WaitHelper.getTestWaiter() to get the unique instance. + * + * Call performWait(runnable) to execute runnable synchronously. + * runnable *must* call performNotify() on all exit paths to signal to + * the TestWaiter that the runnable has completed. + * + * @author rnewman + * @author nalexander + */ +public class WaitHelper { + + public static final String LOG_TAG = "WaitHelper"; + + public static class Result { + public Throwable error; + public Result() { + error = null; + } + + public Result(Throwable error) { + this.error = error; + } + } + + public static abstract class WaitHelperError extends Error { + private static final long serialVersionUID = 7074690961681883619L; + } + + /** + * Immutable. + * + * @author rnewman + */ + public static class TimeoutError extends WaitHelperError { + private static final long serialVersionUID = 8591672555848651736L; + public final int waitTimeInMillis; + + public TimeoutError(int waitTimeInMillis) { + this.waitTimeInMillis = waitTimeInMillis; + } + } + + public static class MultipleNotificationsError extends WaitHelperError { + private static final long serialVersionUID = -9072736521571635495L; + } + + public static class InterruptedError extends WaitHelperError { + private static final long serialVersionUID = 8383948170038639308L; + } + + public static class InnerError extends WaitHelperError { + private static final long serialVersionUID = 3008502618576773778L; + public Throwable innerError; + + public InnerError(Throwable e) { + innerError = e; + if (e != null) { + // Eclipse prints the stack trace of the cause. + this.initCause(e); + } + } + } + + public BlockingQueue<Result> queue = new ArrayBlockingQueue<Result>(1); + + /** + * How long performWait should wait for, in milliseconds, with the + * convention that a negative value means "wait forever". + */ + public static int defaultWaitTimeoutInMillis = -1; + + public void performWait(Runnable action) throws WaitHelperError { + this.performWait(defaultWaitTimeoutInMillis, action); + } + + public void performWait(int waitTimeoutInMillis, Runnable action) throws WaitHelperError { + Logger.debug(LOG_TAG, "performWait called."); + + Result result = null; + + try { + if (action != null) { + try { + action.run(); + Logger.debug(LOG_TAG, "Action done."); + } catch (Exception ex) { + Logger.debug(LOG_TAG, "Performing action threw: " + ex.getMessage()); + throw new InnerError(ex); + } + } + + if (waitTimeoutInMillis < 0) { + result = queue.take(); + } else { + result = queue.poll(waitTimeoutInMillis, TimeUnit.MILLISECONDS); + } + Logger.debug(LOG_TAG, "Got result from queue: " + result); + } catch (InterruptedException e) { + // We were interrupted. + Logger.debug(LOG_TAG, "performNotify interrupted with InterruptedException " + e); + final InterruptedError interruptedError = new InterruptedError(); + interruptedError.initCause(e); + throw interruptedError; + } + + if (result == null) { + // We timed out. + throw new TimeoutError(waitTimeoutInMillis); + } else if (result.error != null) { + Logger.debug(LOG_TAG, "Notified with error: " + result.error.getMessage()); + + // Rethrow any assertion with which we were notified. + InnerError innerError = new InnerError(result.error); + throw innerError; + } + // Success! + } + + public void performNotify(final Throwable e) { + if (e != null) { + Logger.debug(LOG_TAG, "performNotify called with Throwable: " + e.getMessage()); + } else { + Logger.debug(LOG_TAG, "performNotify called."); + } + + if (!queue.offer(new Result(e))) { + // This could happen if performNotify is called multiple times (which is an error). + throw new MultipleNotificationsError(); + } + } + + public void performNotify() { + this.performNotify(null); + } + + public static Runnable onThreadRunnable(final Runnable r) { + return new Runnable() { + @Override + public void run() { + new Thread(r).start(); + } + }; + } + + private static WaitHelper singleWaiter = new WaitHelper(); + public static WaitHelper getTestWaiter() { + return singleWaiter; + } + + public static void resetTestWaiter() { + singleWaiter = new WaitHelper(); + } + + public boolean isIdle() { + return queue.isEmpty(); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/browserid/test/TestASNUtils.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/browserid/test/TestASNUtils.java new file mode 100644 index 000000000..f0b1f98b5 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/browserid/test/TestASNUtils.java @@ -0,0 +1,45 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.browserid.test; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.browserid.ASNUtils; +import org.mozilla.gecko.sync.Utils; + +import java.math.BigInteger; + +@RunWith(TestRunner.class) +public class TestASNUtils { + public void doTestEncodeDecodeArrays(int length1, int length2) { + if (4 + length1 + length2 > 127) { + throw new IllegalArgumentException("Total length must be < 128 - 4."); + } + byte[] first = Utils.generateRandomBytes(length1); + byte[] second = Utils.generateRandomBytes(length2); + byte[] encoded = ASNUtils.encodeTwoArraysToASN1(first, second); + byte[][] arrays = ASNUtils.decodeTwoArraysFromASN1(encoded); + Assert.assertArrayEquals(first, arrays[0]); + Assert.assertArrayEquals(second, arrays[1]); + } + + @Test + public void testEncodeDecodeArrays() { + doTestEncodeDecodeArrays(0, 0); + doTestEncodeDecodeArrays(0, 10); + doTestEncodeDecodeArrays(10, 0); + doTestEncodeDecodeArrays(10, 10); + } + + @Test + public void testEncodeDecodeRandomSizeArrays() { + for (int i = 0; i < 10; i++) { + int length1 = Utils.generateBigIntegerLessThan(BigInteger.valueOf(50)).intValue() + 10; + int length2 = Utils.generateBigIntegerLessThan(BigInteger.valueOf(50)).intValue() + 10; + doTestEncodeDecodeArrays(length1, length2); + } + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/browserid/test/TestDSACryptoImplementation.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/browserid/test/TestDSACryptoImplementation.java new file mode 100644 index 000000000..62427e5e1 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/browserid/test/TestDSACryptoImplementation.java @@ -0,0 +1,60 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.browserid.test; + +import junit.framework.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.browserid.BrowserIDKeyPair; +import org.mozilla.gecko.browserid.DSACryptoImplementation; +import org.mozilla.gecko.sync.ExtendedJSONObject; + +import java.math.BigInteger; + +@RunWith(TestRunner.class) +public class TestDSACryptoImplementation { + @Test + public void testToJSONObject() throws Exception { + BigInteger p = new BigInteger("fca682ce8e12caba26efccf7110e526db078b05edecbcd1eb4a208f3ae1617ae01f35b91a47e6df63413c5e12ed0899bcd132acd50d99151bdc43ee737592e17", 16); + BigInteger q = new BigInteger("962eddcc369cba8ebb260ee6b6a126d9346e38c5", 16); + BigInteger g = new BigInteger("678471b27a9cf44ee91a49c5147db1a9aaf244f05a434d6486931d2d14271b9e35030b71fd73da179069b32e2935630e1c2062354d0da20a6c416e50be794ca4", 16); + BigInteger x = new BigInteger("9516d860392003db5a4f168444903265467614db", 16); + BigInteger y = new BigInteger("455152a0e499f5c9d11f9f1868c8b868b1443ca853843226a5a9552dd909b4bdba879acc504acb690df0348d60e63ea37e8c7f075302e0df5bcdc76a383888a0", 16); + + BrowserIDKeyPair keyPair = new BrowserIDKeyPair( + DSACryptoImplementation.createPrivateKey(x, p, q, g), + DSACryptoImplementation.createPublicKey(y, p, q, g)); + + ExtendedJSONObject o = new ExtendedJSONObject("{\"publicKey\":{\"g\":\"678471b27a9cf44ee91a49c5147db1a9aaf244f05a434d6486931d2d14271b9e35030b71fd73da179069b32e2935630e1c2062354d0da20a6c416e50be794ca4\",\"q\":\"962eddcc369cba8ebb260ee6b6a126d9346e38c5\",\"p\":\"fca682ce8e12caba26efccf7110e526db078b05edecbcd1eb4a208f3ae1617ae01f35b91a47e6df63413c5e12ed0899bcd132acd50d99151bdc43ee737592e17\",\"y\":\"455152a0e499f5c9d11f9f1868c8b868b1443ca853843226a5a9552dd909b4bdba879acc504acb690df0348d60e63ea37e8c7f075302e0df5bcdc76a383888a0\",\"algorithm\":\"DS\"},\"privateKey\":{\"g\":\"678471b27a9cf44ee91a49c5147db1a9aaf244f05a434d6486931d2d14271b9e35030b71fd73da179069b32e2935630e1c2062354d0da20a6c416e50be794ca4\",\"q\":\"962eddcc369cba8ebb260ee6b6a126d9346e38c5\",\"p\":\"fca682ce8e12caba26efccf7110e526db078b05edecbcd1eb4a208f3ae1617ae01f35b91a47e6df63413c5e12ed0899bcd132acd50d99151bdc43ee737592e17\",\"x\":\"9516d860392003db5a4f168444903265467614db\",\"algorithm\":\"DS\"}}"); + Assert.assertEquals(o.getObject("privateKey"), keyPair.toJSONObject().getObject("privateKey")); + Assert.assertEquals(o.getObject("publicKey"), keyPair.toJSONObject().getObject("publicKey")); + } + + @Test + public void testFromJSONObject() throws Exception { + BigInteger p = new BigInteger("fca682ce8e12caba26efccf7110e526db078b05edecbcd1eb4a208f3ae1617ae01f35b91a47e6df63413c5e12ed0899bcd132acd50d99151bdc43ee737592e17", 16); + BigInteger q = new BigInteger("962eddcc369cba8ebb260ee6b6a126d9346e38c5", 16); + BigInteger g = new BigInteger("678471b27a9cf44ee91a49c5147db1a9aaf244f05a434d6486931d2d14271b9e35030b71fd73da179069b32e2935630e1c2062354d0da20a6c416e50be794ca4", 16); + BigInteger x = new BigInteger("9516d860392003db5a4f168444903265467614db", 16); + BigInteger y = new BigInteger("455152a0e499f5c9d11f9f1868c8b868b1443ca853843226a5a9552dd909b4bdba879acc504acb690df0348d60e63ea37e8c7f075302e0df5bcdc76a383888a0", 16); + + BrowserIDKeyPair keyPair = new BrowserIDKeyPair( + DSACryptoImplementation.createPrivateKey(x, p, q, g), + DSACryptoImplementation.createPublicKey(y, p, q, g)); + + ExtendedJSONObject o = new ExtendedJSONObject("{\"publicKey\":{\"g\":\"678471b27a9cf44ee91a49c5147db1a9aaf244f05a434d6486931d2d14271b9e35030b71fd73da179069b32e2935630e1c2062354d0da20a6c416e50be794ca4\",\"q\":\"962eddcc369cba8ebb260ee6b6a126d9346e38c5\",\"p\":\"fca682ce8e12caba26efccf7110e526db078b05edecbcd1eb4a208f3ae1617ae01f35b91a47e6df63413c5e12ed0899bcd132acd50d99151bdc43ee737592e17\",\"y\":\"455152a0e499f5c9d11f9f1868c8b868b1443ca853843226a5a9552dd909b4bdba879acc504acb690df0348d60e63ea37e8c7f075302e0df5bcdc76a383888a0\",\"algorithm\":\"DS\"},\"privateKey\":{\"g\":\"678471b27a9cf44ee91a49c5147db1a9aaf244f05a434d6486931d2d14271b9e35030b71fd73da179069b32e2935630e1c2062354d0da20a6c416e50be794ca4\",\"q\":\"962eddcc369cba8ebb260ee6b6a126d9346e38c5\",\"p\":\"fca682ce8e12caba26efccf7110e526db078b05edecbcd1eb4a208f3ae1617ae01f35b91a47e6df63413c5e12ed0899bcd132acd50d99151bdc43ee737592e17\",\"x\":\"9516d860392003db5a4f168444903265467614db\",\"algorithm\":\"DS\"}}"); + + Assert.assertEquals(keyPair.getPublic().toJSONObject(), DSACryptoImplementation.createPublicKey(o.getObject("publicKey")).toJSONObject()); + Assert.assertEquals(keyPair.getPrivate().toJSONObject(), DSACryptoImplementation.createPrivateKey(o.getObject("privateKey")).toJSONObject()); + } + + @Test + public void testRoundTrip() throws Exception { + BrowserIDKeyPair keyPair = DSACryptoImplementation.generateKeyPair(512); + ExtendedJSONObject o = keyPair.toJSONObject(); + BrowserIDKeyPair keyPair2 = DSACryptoImplementation.fromJSONObject(o); + Assert.assertEquals(o, keyPair2.toJSONObject()); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/browserid/test/TestJSONWebTokenUtils.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/browserid/test/TestJSONWebTokenUtils.java new file mode 100644 index 000000000..7e1f9287e --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/browserid/test/TestJSONWebTokenUtils.java @@ -0,0 +1,151 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.browserid.test; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.browserid.BrowserIDKeyPair; +import org.mozilla.gecko.browserid.DSACryptoImplementation; +import org.mozilla.gecko.browserid.JSONWebTokenUtils; +import org.mozilla.gecko.browserid.RSACryptoImplementation; +import org.mozilla.gecko.browserid.SigningPrivateKey; +import org.mozilla.gecko.browserid.VerifyingPublicKey; +import org.mozilla.gecko.sync.ExtendedJSONObject; + +import java.math.BigInteger; +import java.security.GeneralSecurityException; + +@RunWith(TestRunner.class) +public class TestJSONWebTokenUtils { + public void doTestEncodeDecode(BrowserIDKeyPair keyPair) throws Exception { + SigningPrivateKey privateKey = keyPair.getPrivate(); + VerifyingPublicKey publicKey = keyPair.getPublic(); + + ExtendedJSONObject o = new ExtendedJSONObject(); + o.put("key", "value"); + + String token = JSONWebTokenUtils.encode(o.toJSONString(), privateKey); + Assert.assertNotNull(token); + + String payload = JSONWebTokenUtils.decode(token, publicKey); + Assert.assertEquals(o.toJSONString(), payload); + + try { + JSONWebTokenUtils.decode(token + "x", publicKey); + Assert.fail("Expected exception."); + } catch (GeneralSecurityException e) { + // Do nothing. + } + } + + @Test + public void testEncodeDecodeSuccessRSA() throws Exception { + doTestEncodeDecode(RSACryptoImplementation.generateKeyPair(1024)); + doTestEncodeDecode(RSACryptoImplementation.generateKeyPair(2048)); + } + + @Test + public void testEncodeDecodeSuccessDSA() throws Exception { + doTestEncodeDecode(DSACryptoImplementation.generateKeyPair(512)); + doTestEncodeDecode(DSACryptoImplementation.generateKeyPair(1024)); + } + + public static String TEST_ASSERTION_ISSUER = "127.0.0.1"; + public static String TEST_AUDIENCE = "http://localhost:8080"; + + @Test + public void testRSAGeneration() throws Exception { + // This test uses (now out-dated) MockMyID RSA data but doesn't rely on this + // data actually being MockMyID's data. + final BigInteger MOCKMYID_MODULUS = new BigInteger("15498874758090276039465094105837231567265546373975960480941122651107772824121527483107402353899846252489837024870191707394743196399582959425513904762996756672089693541009892030848825079649783086005554442490232900875792851786203948088457942416978976455297428077460890650409549242124655536986141363719589882160081480785048965686285142002320767066674879737238012064156675899512503143225481933864507793118457805792064445502834162315532113963746801770187685650408560424682654937744713813773896962263709692724630650952159596951348264005004375017610441835956073275708740239518011400991972811669493356682993446554779893834303"); + final BigInteger MOCKMYID_PUBLIC_EXPONENT = new BigInteger("65537"); + final BigInteger MOCKMYID_PRIVATE_EXPONENT = new BigInteger("6539906961872354450087244036236367269804254381890095841127085551577495913426869112377010004955160417265879626558436936025363204803913318582680951558904318308893730033158178650549970379367915856087364428530828396795995781364659413467784853435450762392157026962694408807947047846891301466649598749901605789115278274397848888140105306063608217776127549926721544215720872305194645129403056801987422794114703255989202755511523434098625000826968430077091984351410839837395828971692109391386427709263149504336916566097901771762648090880994773325283207496645630792248007805177873532441314470502254528486411726581424522838833"); + + BigInteger n = new BigInteger("20332459213245328760269530796942625317006933400814022542511832260333163206808672913301254872114045771215470352093046136365629411384688395020388553744886954869033696089099714200452682590914843971683468562019706059388121176435204818734091361033445697933682779095713376909412972373727850278295874361806633955236862180792787906413536305117030045164276955491725646610368132167655556353974515423042221261732084368978523747789654468953860772774078384556028728800902433401131226904244661160767916883680495122225202542023841606998867411022088440946301191503335932960267228470933599974787151449279465703844493353175088719018221"); + BigInteger e = new BigInteger("65537"); + BigInteger d = new BigInteger("9362542596354998418106014928820888151984912891492829581578681873633736656469965533631464203894863562319612803232737938923691416707617473868582415657005943574434271946791143554652502483003923911339605326222297167404896789026986450703532494518628015811567189641735787240372075015553947628033216297520493759267733018808392882741098489889488442349031883643894014316243251108104684754879103107764521172490019661792943030921873284592436328217485953770574054344056638447333651425231219150676837203185544359148474983670261712939626697233692596362322419559401320065488125670905499610998631622562652935873085671353890279911361"); + + long iat = 1352995809210L; + long dur = 60 * 60 * 1000; + long exp = iat + dur; + + VerifyingPublicKey mockMyIdPublicKey = RSACryptoImplementation.createPublicKey(MOCKMYID_MODULUS, MOCKMYID_PUBLIC_EXPONENT);; + SigningPrivateKey mockMyIdPrivateKey = RSACryptoImplementation.createPrivateKey(MOCKMYID_MODULUS, MOCKMYID_PRIVATE_EXPONENT); + VerifyingPublicKey publicKeyToSign = RSACryptoImplementation.createPublicKey(n, e); + SigningPrivateKey privateKeyToSignWith = RSACryptoImplementation.createPrivateKey(n, d); + + String certificate = JSONWebTokenUtils.createCertificate(publicKeyToSign, "test@mockmyid.com", "mockmyid.com", iat, exp, mockMyIdPrivateKey); + String assertion = JSONWebTokenUtils.createAssertion(privateKeyToSignWith, certificate, TEST_AUDIENCE, TEST_ASSERTION_ISSUER, iat, exp); + String payload = JSONWebTokenUtils.decode(certificate, mockMyIdPublicKey); + + String EXPECTED_PAYLOAD = "{\"exp\":1352999409210,\"iat\":1352995809210,\"iss\":\"mockmyid.com\",\"principal\":{\"email\":\"test@mockmyid.com\"},\"public-key\":{\"e\":\"65537\",\"n\":\"20332459213245328760269530796942625317006933400814022542511832260333163206808672913301254872114045771215470352093046136365629411384688395020388553744886954869033696089099714200452682590914843971683468562019706059388121176435204818734091361033445697933682779095713376909412972373727850278295874361806633955236862180792787906413536305117030045164276955491725646610368132167655556353974515423042221261732084368978523747789654468953860772774078384556028728800902433401131226904244661160767916883680495122225202542023841606998867411022088440946301191503335932960267228470933599974787151449279465703844493353175088719018221\",\"algorithm\":\"RS\"}}"; + Assert.assertEquals(EXPECTED_PAYLOAD, payload); + + // Really(!) brittle tests below. The RSA signature algorithm is deterministic, so we can test the actual signature. + String EXPECTED_CERTIFICATE = "eyJhbGciOiJSUzI1NiJ9.eyJleHAiOjEzNTI5OTk0MDkyMTAsImlhdCI6MTM1Mjk5NTgwOTIxMCwiaXNzIjoibW9ja215aWQuY29tIiwicHJpbmNpcGFsIjp7ImVtYWlsIjoidGVzdEBtb2NrbXlpZC5jb20ifSwicHVibGljLWtleSI6eyJlIjoiNjU1MzciLCJuIjoiMjAzMzI0NTkyMTMyNDUzMjg3NjAyNjk1MzA3OTY5NDI2MjUzMTcwMDY5MzM0MDA4MTQwMjI1NDI1MTE4MzIyNjAzMzMxNjMyMDY4MDg2NzI5MTMzMDEyNTQ4NzIxMTQwNDU3NzEyMTU0NzAzNTIwOTMwNDYxMzYzNjU2Mjk0MTEzODQ2ODgzOTUwMjAzODg1NTM3NDQ4ODY5NTQ4NjkwMzM2OTYwODkwOTk3MTQyMDA0NTI2ODI1OTA5MTQ4NDM5NzE2ODM0Njg1NjIwMTk3MDYwNTkzODgxMjExNzY0MzUyMDQ4MTg3MzQwOTEzNjEwMzM0NDU2OTc5MzM2ODI3NzkwOTU3MTMzNzY5MDk0MTI5NzIzNzM3Mjc4NTAyNzgyOTU4NzQzNjE4MDY2MzM5NTUyMzY4NjIxODA3OTI3ODc5MDY0MTM1MzYzMDUxMTcwMzAwNDUxNjQyNzY5NTU0OTE3MjU2NDY2MTAzNjgxMzIxNjc2NTU1NTYzNTM5NzQ1MTU0MjMwNDIyMjEyNjE3MzIwODQzNjg5Nzg1MjM3NDc3ODk2NTQ0Njg5NTM4NjA3NzI3NzQwNzgzODQ1NTYwMjg3Mjg4MDA5MDI0MzM0MDExMzEyMjY5MDQyNDQ2NjExNjA3Njc5MTY4ODM2ODA0OTUxMjIyMjUyMDI1NDIwMjM4NDE2MDY5OTg4Njc0MTEwMjIwODg0NDA5NDYzMDExOTE1MDMzMzU5MzI5NjAyNjcyMjg0NzA5MzM1OTk5NzQ3ODcxNTE0NDkyNzk0NjU3MDM4NDQ0OTMzNTMxNzUwODg3MTkwMTgyMjEiLCJhbGdvcml0aG0iOiJSUyJ9fQ.ZgT0ezITaE6rRQCxEA6OHkjwAsFdE-R8943UEmiCvKKpsbxlSlI1Iya1Oho2wrhet5bjBGM77EffzC2YwzD5qa7SrVpNwSCIW6AwnlJ6YePoNblkn0y7NQ_qThvLoaP4Vlk_XM0LbK_QPHqaWU7ldm8LF5Zp4oHgayMP4YhiyKYS2TwWWcvswT2g9IhU6YdYcF0TwT2YkJ4t3h7_sVn-OmQQu4k1KKGFLpT6HOj2EGaKmw-mzayHL0r7L3-5g_7Q83RMBe_k_4YeLG8InxO3M3GreqcaImv4XO5D-C__txfFuaLJjTzKBLrIIosckaNwp4JmN1Nf8x9t5RXHLCsrjw"; + Assert.assertEquals(EXPECTED_CERTIFICATE, certificate); + + String EXPECTED_ASSERTION = "eyJhbGciOiJSUzI1NiJ9.eyJleHAiOjEzNTI5OTk0MDkyMTAsImlhdCI6MTM1Mjk5NTgwOTIxMCwiaXNzIjoibW9ja215aWQuY29tIiwicHJpbmNpcGFsIjp7ImVtYWlsIjoidGVzdEBtb2NrbXlpZC5jb20ifSwicHVibGljLWtleSI6eyJlIjoiNjU1MzciLCJuIjoiMjAzMzI0NTkyMTMyNDUzMjg3NjAyNjk1MzA3OTY5NDI2MjUzMTcwMDY5MzM0MDA4MTQwMjI1NDI1MTE4MzIyNjAzMzMxNjMyMDY4MDg2NzI5MTMzMDEyNTQ4NzIxMTQwNDU3NzEyMTU0NzAzNTIwOTMwNDYxMzYzNjU2Mjk0MTEzODQ2ODgzOTUwMjAzODg1NTM3NDQ4ODY5NTQ4NjkwMzM2OTYwODkwOTk3MTQyMDA0NTI2ODI1OTA5MTQ4NDM5NzE2ODM0Njg1NjIwMTk3MDYwNTkzODgxMjExNzY0MzUyMDQ4MTg3MzQwOTEzNjEwMzM0NDU2OTc5MzM2ODI3NzkwOTU3MTMzNzY5MDk0MTI5NzIzNzM3Mjc4NTAyNzgyOTU4NzQzNjE4MDY2MzM5NTUyMzY4NjIxODA3OTI3ODc5MDY0MTM1MzYzMDUxMTcwMzAwNDUxNjQyNzY5NTU0OTE3MjU2NDY2MTAzNjgxMzIxNjc2NTU1NTYzNTM5NzQ1MTU0MjMwNDIyMjEyNjE3MzIwODQzNjg5Nzg1MjM3NDc3ODk2NTQ0Njg5NTM4NjA3NzI3NzQwNzgzODQ1NTYwMjg3Mjg4MDA5MDI0MzM0MDExMzEyMjY5MDQyNDQ2NjExNjA3Njc5MTY4ODM2ODA0OTUxMjIyMjUyMDI1NDIwMjM4NDE2MDY5OTg4Njc0MTEwMjIwODg0NDA5NDYzMDExOTE1MDMzMzU5MzI5NjAyNjcyMjg0NzA5MzM1OTk5NzQ3ODcxNTE0NDkyNzk0NjU3MDM4NDQ0OTMzNTMxNzUwODg3MTkwMTgyMjEiLCJhbGdvcml0aG0iOiJSUyJ9fQ.ZgT0ezITaE6rRQCxEA6OHkjwAsFdE-R8943UEmiCvKKpsbxlSlI1Iya1Oho2wrhet5bjBGM77EffzC2YwzD5qa7SrVpNwSCIW6AwnlJ6YePoNblkn0y7NQ_qThvLoaP4Vlk_XM0LbK_QPHqaWU7ldm8LF5Zp4oHgayMP4YhiyKYS2TwWWcvswT2g9IhU6YdYcF0TwT2YkJ4t3h7_sVn-OmQQu4k1KKGFLpT6HOj2EGaKmw-mzayHL0r7L3-5g_7Q83RMBe_k_4YeLG8InxO3M3GreqcaImv4XO5D-C__txfFuaLJjTzKBLrIIosckaNwp4JmN1Nf8x9t5RXHLCsrjw~eyJhbGciOiJSUzI1NiJ9.eyJhdWQiOiJodHRwOlwvXC9sb2NhbGhvc3Q6ODA4MCIsImV4cCI6MTM1Mjk5OTQwOTIxMCwiaWF0IjoxMzUyOTk1ODA5MjEwLCJpc3MiOiIxMjcuMC4wLjEifQ.gj5Q9KXR_mPEltn3SXKAjIHMOpQq0FP6NdPOB-Zu149LKhQrfXS90woVJYg8WpaasmiS6gjBFni3urq3adPktzw4RoMm1qVMvSRXXIRZzgsV_vHlSenIY0KlAk4140pAlAPcdJhB2bvKUPPDq0TLzlWHgQpheAAFMGPY1OGgwgHtsCQC_vyE2wFi8M58IGYQ-05KmWc6Zo33CJG6LjVvkTPvPTEzQKFYKwDQGc4NTkqZbCNZE6iRq4mlX9LGFddzEDiSUDmS53SwR4nfFzPQE6Q1xnU4a_BLhfNpdfOc-uHGoJGbm0ZJpLdKf7zadp34ImFA9IUBhjegingZhm2i5g"; + Assert.assertEquals(EXPECTED_ASSERTION, assertion); + } + + @Test + public void testDSAGeneration() throws Exception { + // This test uses MockMyID DSA data but doesn't rely on this data actually + // being MockMyID's data. + final BigInteger MOCKMYID_x = new BigInteger("385cb3509f086e110c5e24bdd395a84b335a09ae", 16); + final BigInteger MOCKMYID_y = new BigInteger("738ec929b559b604a232a9b55a5295afc368063bb9c20fac4e53a74970a4db7956d48e4c7ed523405f629b4cc83062f13029c4d615bbacb8b97f5e56f0c7ac9bc1d4e23809889fa061425c984061fca1826040c399715ce7ed385c4dd0d402256912451e03452d3c961614eb458f188e3e8d2782916c43dbe2e571251ce38262", 16); + final BigInteger MOCKMYID_p = new BigInteger("ff600483db6abfc5b45eab78594b3533d550d9f1bf2a992a7a8daa6dc34f8045ad4e6e0c429d334eeeaaefd7e23d4810be00e4cc1492cba325ba81ff2d5a5b305a8d17eb3bf4a06a349d392e00d329744a5179380344e82a18c47933438f891e22aeef812d69c8f75e326cb70ea000c3f776dfdbd604638c2ef717fc26d02e17", 16); + final BigInteger MOCKMYID_q = new BigInteger("e21e04f911d1ed7991008ecaab3bf775984309c3", 16); + final BigInteger MOCKMYID_g = new BigInteger("c52a4a0ff3b7e61fdf1867ce84138369a6154f4afa92966e3c827e25cfa6cf508b90e5de419e1337e07a2e9e2a3cd5dea704d175f8ebf6af397d69e110b96afb17c7a03259329e4829b0d03bbc7896b15b4ade53e130858cc34d96269aa89041f409136c7242a38895c9d5bccad4f389af1d7a4bd1398bd072dffa896233397a", 16); + + BigInteger g = new BigInteger("f7e1a085d69b3ddecbbcab5c36b857b97994afbbfa3aea82f9574c0b3d0782675159578ebad4594fe67107108180b449167123e84c281613b7cf09328cc8a6e13c167a8b547c8d28e0a3ae1e2bb3a675916ea37f0bfa213562f1fb627a01243bcca4f1bea8519089a883dfe15ae59f06928b665e807b552564014c3bfecf492a", 16); + BigInteger q = new BigInteger("9760508f15230bccb292b982a2eb840bf0581cf5", 16); + BigInteger p = new BigInteger("fd7f53811d75122952df4a9c2eece4e7f611b7523cef4400c31e3f80b6512669455d402251fb593d8d58fabfc5f5ba30f6cb9b556cd7813b801d346ff26660b76b9950a5a49f9fe8047b1022c24fbba9d7feb7c61bf83b57e7c6a8a6150f04fb83f6d3c51ec3023554135a169132f675f3ae2b61d72aeff22203199dd14801c7", 16); + BigInteger x = new BigInteger("b137fc5b8faaa53b170563eb03c18b46b657bb6", 16); + BigInteger y = new BigInteger("ea809be508bc94485553efac8ef2a8debdcdb3545ce433e8bd5889ec9d0880a13b2a8af35451161e58229d1e2be69e74a7251465a394913e8e64b0c33fde39a637b6047d7370178cf4404c0a7b4c2ed31d9cfe03ab79dbcc64667e6e7bc244eb1c127c28d725db94aff29b858bdb636f1307bdf48b3c91f387c2ab588086b6c8", 16); + + long iat = 1380070362995L; + long dur = 60 * 60 * 1000; + long exp = iat + dur; + + VerifyingPublicKey mockMyIdPublicKey = DSACryptoImplementation.createPublicKey(MOCKMYID_y, MOCKMYID_p, MOCKMYID_q, MOCKMYID_g); + SigningPrivateKey mockMyIdPrivateKey = DSACryptoImplementation.createPrivateKey(MOCKMYID_x, MOCKMYID_p, MOCKMYID_q, MOCKMYID_g); + VerifyingPublicKey publicKeyToSign = DSACryptoImplementation.createPublicKey(y, p, q, g); + SigningPrivateKey privateKeyToSignWith = DSACryptoImplementation.createPrivateKey(x, p, q, g); + + String certificate = JSONWebTokenUtils.createCertificate(publicKeyToSign, "test@mockmyid.com", "mockmyid.com", iat, exp, mockMyIdPrivateKey); + String assertion = JSONWebTokenUtils.createAssertion(privateKeyToSignWith, certificate, TEST_AUDIENCE, TEST_ASSERTION_ISSUER, iat, exp); + String payload = JSONWebTokenUtils.decode(certificate, mockMyIdPublicKey); + + String EXPECTED_PAYLOAD = "{\"exp\":1380073962995,\"iat\":1380070362995,\"iss\":\"mockmyid.com\",\"principal\":{\"email\":\"test@mockmyid.com\"},\"public-key\":{\"g\":\"f7e1a085d69b3ddecbbcab5c36b857b97994afbbfa3aea82f9574c0b3d0782675159578ebad4594fe67107108180b449167123e84c281613b7cf09328cc8a6e13c167a8b547c8d28e0a3ae1e2bb3a675916ea37f0bfa213562f1fb627a01243bcca4f1bea8519089a883dfe15ae59f06928b665e807b552564014c3bfecf492a\",\"q\":\"9760508f15230bccb292b982a2eb840bf0581cf5\",\"p\":\"fd7f53811d75122952df4a9c2eece4e7f611b7523cef4400c31e3f80b6512669455d402251fb593d8d58fabfc5f5ba30f6cb9b556cd7813b801d346ff26660b76b9950a5a49f9fe8047b1022c24fbba9d7feb7c61bf83b57e7c6a8a6150f04fb83f6d3c51ec3023554135a169132f675f3ae2b61d72aeff22203199dd14801c7\",\"y\":\"ea809be508bc94485553efac8ef2a8debdcdb3545ce433e8bd5889ec9d0880a13b2a8af35451161e58229d1e2be69e74a7251465a394913e8e64b0c33fde39a637b6047d7370178cf4404c0a7b4c2ed31d9cfe03ab79dbcc64667e6e7bc244eb1c127c28d725db94aff29b858bdb636f1307bdf48b3c91f387c2ab588086b6c8\",\"algorithm\":\"DS\"}}"; + Assert.assertEquals(EXPECTED_PAYLOAD, payload); + + // Really(!) brittle tests below. The DSA signature algorithm is not deterministic, so we can't test the actual signature. + String EXPECTED_CERTIFICATE_PREFIX = "eyJhbGciOiJEUzEyOCJ9.eyJleHAiOjEzODAwNzM5NjI5OTUsImlhdCI6MTM4MDA3MDM2Mjk5NSwiaXNzIjoibW9ja215aWQuY29tIiwicHJpbmNpcGFsIjp7ImVtYWlsIjoidGVzdEBtb2NrbXlpZC5jb20ifSwicHVibGljLWtleSI6eyJnIjoiZjdlMWEwODVkNjliM2RkZWNiYmNhYjVjMzZiODU3Yjk3OTk0YWZiYmZhM2FlYTgyZjk1NzRjMGIzZDA3ODI2NzUxNTk1NzhlYmFkNDU5NGZlNjcxMDcxMDgxODBiNDQ5MTY3MTIzZTg0YzI4MTYxM2I3Y2YwOTMyOGNjOGE2ZTEzYzE2N2E4YjU0N2M4ZDI4ZTBhM2FlMWUyYmIzYTY3NTkxNmVhMzdmMGJmYTIxMzU2MmYxZmI2MjdhMDEyNDNiY2NhNGYxYmVhODUxOTA4OWE4ODNkZmUxNWFlNTlmMDY5MjhiNjY1ZTgwN2I1NTI1NjQwMTRjM2JmZWNmNDkyYSIsInEiOiI5NzYwNTA4ZjE1MjMwYmNjYjI5MmI5ODJhMmViODQwYmYwNTgxY2Y1IiwicCI6ImZkN2Y1MzgxMWQ3NTEyMjk1MmRmNGE5YzJlZWNlNGU3ZjYxMWI3NTIzY2VmNDQwMGMzMWUzZjgwYjY1MTI2Njk0NTVkNDAyMjUxZmI1OTNkOGQ1OGZhYmZjNWY1YmEzMGY2Y2I5YjU1NmNkNzgxM2I4MDFkMzQ2ZmYyNjY2MGI3NmI5OTUwYTVhNDlmOWZlODA0N2IxMDIyYzI0ZmJiYTlkN2ZlYjdjNjFiZjgzYjU3ZTdjNmE4YTYxNTBmMDRmYjgzZjZkM2M1MWVjMzAyMzU1NDEzNWExNjkxMzJmNjc1ZjNhZTJiNjFkNzJhZWZmMjIyMDMxOTlkZDE0ODAxYzciLCJ5IjoiZWE4MDliZTUwOGJjOTQ0ODU1NTNlZmFjOGVmMmE4ZGViZGNkYjM1NDVjZTQzM2U4YmQ1ODg5ZWM5ZDA4ODBhMTNiMmE4YWYzNTQ1MTE2MWU1ODIyOWQxZTJiZTY5ZTc0YTcyNTE0NjVhMzk0OTEzZThlNjRiMGMzM2ZkZTM5YTYzN2I2MDQ3ZDczNzAxNzhjZjQ0MDRjMGE3YjRjMmVkMzFkOWNmZTAzYWI3OWRiY2M2NDY2N2U2ZTdiYzI0NGViMWMxMjdjMjhkNzI1ZGI5NGFmZjI5Yjg1OGJkYjYzNmYxMzA3YmRmNDhiM2M5MWYzODdjMmFiNTg4MDg2YjZjOCIsImFsZ29yaXRobSI6IkRTIn19"; + String[] expectedCertificateParts = EXPECTED_CERTIFICATE_PREFIX.split("\\."); + String[] certificateParts = certificate.split("\\."); + Assert.assertEquals(expectedCertificateParts[0], certificateParts[0]); + Assert.assertEquals(expectedCertificateParts[1], certificateParts[1]); + + String EXPECTED_ASSERTION_FRAGMENT = "eyJhbGciOiJEUzEyOCJ9.eyJhdWQiOiJodHRwOlwvXC9sb2NhbGhvc3Q6ODA4MCIsImV4cCI6MTM4MDA3Mzk2Mjk5NSwiaWF0IjoxMzgwMDcwMzYyOTk1LCJpc3MiOiIxMjcuMC4wLjEifQ"; + String[] expectedAssertionParts = EXPECTED_ASSERTION_FRAGMENT.split("\\."); + String[] assertionParts = assertion.split("~")[1].split("\\."); + Assert.assertEquals(expectedAssertionParts[0], assertionParts[0]); + Assert.assertEquals(expectedAssertionParts[1], assertionParts[1]); + } + + @Test + public void testGetPayloadString() throws Exception { + String s; + s = JSONWebTokenUtils.getPayloadString("{}", "audience", "issuer", 1L, 2L); + Assert.assertEquals("{\"aud\":\"audience\",\"exp\":2,\"iat\":1,\"iss\":\"issuer\"}", s); + + // Make sure we don't include null issuedAt. + s = JSONWebTokenUtils.getPayloadString("{}", "audience", "issuer", null, 3L); + Assert.assertEquals("{\"aud\":\"audience\",\"exp\":3,\"iss\":\"issuer\"}", s); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/browserid/test/TestRSACryptoImplementation.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/browserid/test/TestRSACryptoImplementation.java new file mode 100644 index 000000000..6dfa88ebf --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/browserid/test/TestRSACryptoImplementation.java @@ -0,0 +1,56 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.browserid.test; + +import junit.framework.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.browserid.BrowserIDKeyPair; +import org.mozilla.gecko.browserid.RSACryptoImplementation; +import org.mozilla.gecko.sync.ExtendedJSONObject; + +import java.math.BigInteger; + +@RunWith(TestRunner.class) +public class TestRSACryptoImplementation { + @Test + public void testToJSONObject() throws Exception { + BigInteger n = new BigInteger("7042170764319402120473546823641395184140303948430445023576085129538272863656735924617881022040465877164076593767104512065359975488480629290310209335113577"); + BigInteger e = new BigInteger("65537"); + BigInteger d = new BigInteger("2050102629239206449128199335463237235732683202345308155771672920433658970744825199440426256856862541525088288448769859770132714705204296375901885294992205"); + + BrowserIDKeyPair keyPair = new BrowserIDKeyPair( + RSACryptoImplementation.createPrivateKey(n, d), + RSACryptoImplementation.createPublicKey(n, e)); + + ExtendedJSONObject o = new ExtendedJSONObject("{\"publicKey\":{\"e\":\"65537\",\"n\":\"7042170764319402120473546823641395184140303948430445023576085129538272863656735924617881022040465877164076593767104512065359975488480629290310209335113577\",\"algorithm\":\"RS\"},\"privateKey\":{\"d\":\"2050102629239206449128199335463237235732683202345308155771672920433658970744825199440426256856862541525088288448769859770132714705204296375901885294992205\",\"n\":\"7042170764319402120473546823641395184140303948430445023576085129538272863656735924617881022040465877164076593767104512065359975488480629290310209335113577\",\"algorithm\":\"RS\"}}"); + Assert.assertEquals(o.getObject("privateKey"), keyPair.toJSONObject().getObject("privateKey")); + Assert.assertEquals(o.getObject("publicKey"), keyPair.toJSONObject().getObject("publicKey")); + } + + @Test + public void testFromJSONObject() throws Exception { + BigInteger n = new BigInteger("7042170764319402120473546823641395184140303948430445023576085129538272863656735924617881022040465877164076593767104512065359975488480629290310209335113577"); + BigInteger e = new BigInteger("65537"); + BigInteger d = new BigInteger("2050102629239206449128199335463237235732683202345308155771672920433658970744825199440426256856862541525088288448769859770132714705204296375901885294992205"); + + BrowserIDKeyPair keyPair = new BrowserIDKeyPair( + RSACryptoImplementation.createPrivateKey(n, d), + RSACryptoImplementation.createPublicKey(n, e)); + + ExtendedJSONObject o = new ExtendedJSONObject("{\"publicKey\":{\"e\":\"65537\",\"n\":\"7042170764319402120473546823641395184140303948430445023576085129538272863656735924617881022040465877164076593767104512065359975488480629290310209335113577\",\"algorithm\":\"RS\"},\"privateKey\":{\"d\":\"2050102629239206449128199335463237235732683202345308155771672920433658970744825199440426256856862541525088288448769859770132714705204296375901885294992205\",\"n\":\"7042170764319402120473546823641395184140303948430445023576085129538272863656735924617881022040465877164076593767104512065359975488480629290310209335113577\",\"algorithm\":\"RS\"}}"); + + Assert.assertEquals(keyPair.getPublic().toJSONObject(), RSACryptoImplementation.createPublicKey(o.getObject("publicKey")).toJSONObject()); + Assert.assertEquals(keyPair.getPrivate().toJSONObject(), RSACryptoImplementation.createPrivateKey(o.getObject("privateKey")).toJSONObject()); + } + + @Test + public void testRoundTrip() throws Exception { + BrowserIDKeyPair keyPair = RSACryptoImplementation.generateKeyPair(512); + ExtendedJSONObject o = keyPair.toJSONObject(); + BrowserIDKeyPair keyPair2 = RSACryptoImplementation.fromJSONObject(o); + Assert.assertEquals(o, keyPair2.toJSONObject()); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/cleanup/TestFileCleanupController.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/cleanup/TestFileCleanupController.java new file mode 100644 index 000000000..6858b65a7 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/cleanup/TestFileCleanupController.java @@ -0,0 +1,92 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package org.mozilla.gecko.cleanup; + +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.robolectric.RuntimeEnvironment; + +import java.util.ArrayList; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.atMost; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +/** + * Tests functionality of the {@link FileCleanupController}. + */ +@RunWith(TestRunner.class) +public class TestFileCleanupController { + + @Test + public void testStartIfReadyEmptySharedPrefsRunsCleanup() { + final Context context = mock(Context.class); + FileCleanupController.startIfReady(context, getSharedPreferences(), ""); + verify(context).startService(any(Intent.class)); + } + + @Test + public void testStartIfReadyLastRunNowDoesNotRun() { + final SharedPreferences sharedPrefs = getSharedPreferences(); + sharedPrefs.edit() + .putLong(FileCleanupController.PREF_LAST_CLEANUP_MILLIS, System.currentTimeMillis()) + .commit(); // synchronous to finish before test runs. + + final Context context = mock(Context.class); + FileCleanupController.startIfReady(context, sharedPrefs, ""); + + verify(context, never()).startService((any(Intent.class))); + } + + /** + * Depends on {@link #testStartIfReadyEmptySharedPrefsRunsCleanup()} success – + * i.e. we expect the cleanup to run with empty prefs. + */ + @Test + public void testStartIfReadyDoesNotRunTwiceInSuccession() { + final Context context = mock(Context.class); + final SharedPreferences sharedPrefs = getSharedPreferences(); + + FileCleanupController.startIfReady(context, sharedPrefs, ""); + verify(context).startService(any(Intent.class)); + + // Note: the Controller relies on SharedPrefs.apply, but + // robolectric made this a synchronous call. Yay! + FileCleanupController.startIfReady(context, sharedPrefs, ""); + verify(context, atMost(1)).startService(any(Intent.class)); + } + + @Test + public void testGetFilesToCleanupContainsProfilePath() { + final String profilePath = "/a/profile/path"; + final ArrayList<String> fileList = FileCleanupController.getFilesToCleanup(profilePath); + assertNotNull("Returned file list is non-null", fileList); + + boolean atLeastOneStartsWithProfilePath = false; + final String pathToCheck = profilePath + "/"; // Ensure the calling code adds a slash to divide the path. + for (final String path : fileList) { + if (path.startsWith(pathToCheck)) { + // It'd be great if we could assert these individually so + // we could display the Strings in console output. + atLeastOneStartsWithProfilePath = true; + } + } + assertTrue("At least one returned String starts with a profile path", atLeastOneStartsWithProfilePath); + } + + private SharedPreferences getSharedPreferences() { + return RuntimeEnvironment.application.getSharedPreferences("TestFileCleanupController", 0); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/cleanup/TestFileCleanupService.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/cleanup/TestFileCleanupService.java new file mode 100644 index 000000000..0326adb6a --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/cleanup/TestFileCleanupService.java @@ -0,0 +1,106 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package org.mozilla.gecko.cleanup; + +import android.content.Intent; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.TestRunner; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.*; + +/** + * Tests the methods of {@link FileCleanupService}. + */ +@RunWith(TestRunner.class) +public class TestFileCleanupService { + @Rule + public final TemporaryFolder tempFolder = new TemporaryFolder(); + + private void assertAllFilesExist(final List<File> fileList) { + for (final File file : fileList) { + assertTrue("File exists", file.exists()); + } + } + + private void assertAllFilesDoNotExist(final List<File> fileList) { + for (final File file : fileList) { + assertFalse("File does not exist", file.exists()); + } + } + + private void onHandleIntent(final ArrayList<String> filePaths) { + final FileCleanupService service = new FileCleanupService(); + final Intent intent = new Intent(FileCleanupService.ACTION_DELETE_FILES); + intent.putExtra(FileCleanupService.EXTRA_FILE_PATHS_TO_DELETE, filePaths); + service.onHandleIntent(intent); + } + + @Test + public void testOnHandleIntentDeleteSpecifiedFiles() throws Exception { + final int fileListCount = 3; + final ArrayList<File> filesToDelete = generateFileList(fileListCount); + + final ArrayList<String> pathsToDelete = new ArrayList<>(fileListCount); + for (final File file : filesToDelete) { + pathsToDelete.add(file.getAbsolutePath()); + } + + assertAllFilesExist(filesToDelete); + onHandleIntent(pathsToDelete); + assertAllFilesDoNotExist(filesToDelete); + } + + @Test + public void testOnHandleIntentDoesNotDeleteUnrelatedFiles() throws Exception { + final ArrayList<File> filesShouldNotBeDeleted = generateFileList(3); + assertAllFilesExist(filesShouldNotBeDeleted); + onHandleIntent(new ArrayList<String>()); + assertAllFilesExist(filesShouldNotBeDeleted); + } + + @Test + public void testOnHandleIntentDeletesEmptyDirectory() throws Exception { + final File dir = tempFolder.newFolder(); + final ArrayList<String> filesToDelete = new ArrayList<>(1); + filesToDelete.add(dir.getAbsolutePath()); + + assertTrue("Empty directory exists", dir.exists()); + onHandleIntent(filesToDelete); + assertFalse("Empty directory deleted by service", dir.exists()); + } + + @Test + public void testOnHandleIntentDoesNotDeleteNonEmptyDirectory() throws Exception { + final File dir = tempFolder.newFolder(); + final ArrayList<String> filesCannotDelete = new ArrayList<>(1); + filesCannotDelete.add(dir.getAbsolutePath()); + assertTrue("Directory exists", dir.exists()); + + final File fileInDir = new File(dir, "file_in_dir"); + assertTrue("File in dir created", fileInDir.createNewFile()); + + onHandleIntent(filesCannotDelete); + assertTrue("Non-empty directory not deleted", dir.exists()); + assertTrue("File in directory not deleted", fileInDir.exists()); + } + + private ArrayList<File> generateFileList(final int size) throws IOException { + final ArrayList<File> fileList = new ArrayList<>(size); + for (int i = 0; i < size; ++i) { + fileList.add(tempFolder.newFile()); + } + return fileList; + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/db/BrowserContractTest.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/db/BrowserContractTest.java new file mode 100644 index 000000000..e36153d0e --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/db/BrowserContractTest.java @@ -0,0 +1,67 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.db; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.TestRunner; + +import static org.junit.Assert.*; + +@RunWith(TestRunner.class) +public class BrowserContractTest { + @Test + /** + * Test that bookmark and sorting order clauses are set correctly + */ + public void testGetCombinedFrecencySortOrder() throws Exception { + String sqlNoBookmarksDesc = BrowserContract.getCombinedFrecencySortOrder(false, false); + String sqlNoBookmarksAsc = BrowserContract.getCombinedFrecencySortOrder(false, true); + String sqlBookmarksDesc = BrowserContract.getCombinedFrecencySortOrder(true, false); + String sqlBookmarksAsc = BrowserContract.getCombinedFrecencySortOrder(true, true); + + assertTrue(sqlBookmarksAsc.endsWith(" ASC")); + assertTrue(sqlBookmarksDesc.endsWith(" DESC")); + assertTrue(sqlNoBookmarksAsc.endsWith(" ASC")); + assertTrue(sqlNoBookmarksDesc.endsWith(" DESC")); + + assertTrue(sqlBookmarksAsc.startsWith("(CASE WHEN bookmark_id > -1 THEN 100 ELSE 0 END) + ")); + assertTrue(sqlBookmarksDesc.startsWith("(CASE WHEN bookmark_id > -1 THEN 100 ELSE 0 END) + ")); + } + + @Test + /** + * Test that calculation string is correct for remote visits + * maxFrecency=1, scaleConst=110, correct sql params for visit count and last date + * and that time is converted to microseconds. + */ + public void testGetRemoteFrecencySQL() throws Exception { + long now = 1; + String sql = BrowserContract.getRemoteFrecencySQL(now); + String ageExpr = "(" + now * 1000 + " - remoteDateLastVisited) / 86400000000"; + + assertEquals( + "remoteVisitCount * MAX(1, 100 * 110 / (" + ageExpr + " * " + ageExpr + " + 110))", + sql + ); + } + + @Test + /** + * Test that calculation string is correct for remote visits + * maxFrecency=2, scaleConst=225, correct sql params for visit count and last date + * and that time is converted to microseconds. + */ + public void testGetLocalFrecencySQL() throws Exception { + long now = 1; + String sql = BrowserContract.getLocalFrecencySQL(now); + String ageExpr = "(" + now * 1000 + " - localDateLastVisited) / 86400000000"; + String visitCountExpr = "(localVisitCount + 2) * (localVisitCount + 2)"; + + assertEquals( + visitCountExpr + " * MAX(2, 100 * 225 / (" + ageExpr + " * " + ageExpr + " + 225))", + sql + ); + } +}
\ No newline at end of file diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/db/BrowserProviderHighlightsTest.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/db/BrowserProviderHighlightsTest.java new file mode 100644 index 000000000..f8af41e32 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/db/BrowserProviderHighlightsTest.java @@ -0,0 +1,438 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.db; + +import android.content.ContentProviderClient; +import android.content.ContentValues; +import android.database.Cursor; +import android.net.Uri; +import android.os.RemoteException; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.sync.repositories.android.BrowserContractHelpers; +import org.mozilla.gecko.sync.setup.Constants; + +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +import static org.mozilla.gecko.db.BrowserContract.PARAM_PROFILE; + +/** + * Unit tests for the highlights query (Activity Stream). + */ +@RunWith(TestRunner.class) +public class BrowserProviderHighlightsTest extends BrowserProviderHistoryVisitsTestBase { + private ContentProviderClient highlightsClient; + private ContentProviderClient activityStreamBlocklistClient; + private ContentProviderClient bookmarksClient; + + private Uri highlightsTestUri; + private Uri activityStreamBlocklistTestUri; + private Uri bookmarksTestUri; + + private Uri expireHistoryNormalUri; + + @Before + public void setUp() throws Exception { + super.setUp(); + + final Uri highlightsClientUri = BrowserContract.Highlights.CONTENT_URI.buildUpon() + .appendQueryParameter(PARAM_PROFILE, Constants.DEFAULT_PROFILE) + .build(); + + final Uri activityStreamBlocklistClientUri = BrowserContract.ActivityStreamBlocklist.CONTENT_URI.buildUpon() + .appendQueryParameter(PARAM_PROFILE, Constants.DEFAULT_PROFILE) + .build(); + + highlightsClient = contentResolver.acquireContentProviderClient(highlightsClientUri); + activityStreamBlocklistClient = contentResolver.acquireContentProviderClient(activityStreamBlocklistClientUri); + bookmarksClient = contentResolver.acquireContentProviderClient(BrowserContractHelpers.BOOKMARKS_CONTENT_URI); + + highlightsTestUri = testUri(BrowserContract.Highlights.CONTENT_URI); + activityStreamBlocklistTestUri = testUri(BrowserContract.ActivityStreamBlocklist.CONTENT_URI); + bookmarksTestUri = testUri(BrowserContract.Bookmarks.CONTENT_URI); + + expireHistoryNormalUri = testUri(BrowserContract.History.CONTENT_OLD_URI).buildUpon() + .appendQueryParameter( + BrowserContract.PARAM_EXPIRE_PRIORITY, + BrowserContract.ExpirePriority.NORMAL.toString() + ).build(); + } + + @After + public void tearDown() { + highlightsClient.release(); + activityStreamBlocklistClient.release(); + bookmarksClient.release(); + + super.tearDown(); + } + + /** + * Scenario: Empty database, no history, no bookmarks. + * + * Assert that: + * - Empty cursor (not null) is returned. + */ + @Test + public void testEmptyDatabase() throws Exception { + final Cursor cursor = highlightsClient.query(highlightsTestUri, null, null, null, null); + Assert.assertNotNull(cursor); + + Assert.assertEquals(0, cursor.getCount()); + + cursor.close(); + } + + /** + * Scenario: The database only contains very recent history (now, 5 minutes ago, 20 minutes). + * + * Assert that: + * - No highlight is returned from recent history. + */ + @Test + public void testOnlyRecentHistory() throws Exception { + final long now = System.currentTimeMillis(); + final long fiveMinutesAgo = now - 1000 * 60 * 5; + final long twentyMinutes = now - 1000 * 60 * 20; + + insertHistoryItem(createUniqueUrl(), createGUID(), now, 1, createUniqueTitle()); + insertHistoryItem(createUniqueUrl(), createGUID(), fiveMinutesAgo, 1, createUniqueTitle()); + insertHistoryItem(createUniqueUrl(), createGUID(), twentyMinutes, 1, createUniqueTitle()); + + final Cursor cursor = highlightsClient.query(highlightsTestUri, null, null, null, null); + + Assert.assertNotNull(cursor); + + Assert.assertEquals(0, cursor.getCount()); + + cursor.close(); + } + + /** + * Scenario: The database contains recent (but not too fresh) history (1 hour, 5 days). + * + * Assert that: + * - Highlights are returned from history. + */ + @Test + public void testHighlightsArePickedFromHistory() throws Exception { + final String url1 = createUniqueUrl(); + final String url2 = createUniqueUrl(); + final String title1 = createUniqueTitle(); + final String title2 = createUniqueTitle(); + + final long oneHourAgo = System.currentTimeMillis() - 1000 * 60 * 60; + final long fiveDaysAgo = System.currentTimeMillis() - 1000 * 60 * 60 * 24 * 5; + + insertHistoryItem(url1, createGUID(), oneHourAgo, 1, title1); + insertHistoryItem(url2, createGUID(), fiveDaysAgo, 1, title2); + + final Cursor cursor = highlightsClient.query(highlightsTestUri, null, null, null, null); + Assert.assertNotNull(cursor); + + Assert.assertEquals(2, cursor.getCount()); + + assertCursorContainsEntry(cursor, url1, title1); + assertCursorContainsEntry(cursor, url2, title2); + + cursor.close(); + } + + /** + * Scenario: The database contains history that is visited frequently and rarely. + * + * Assert that: + * - Highlights are picked from rarely visited websites. + * - Highlights are not picked from frequently visited websites. + */ + @Test + public void testOftenVisitedPagesAreNotPicked() throws Exception { + final String url1 = createUniqueUrl(); + final String title1 = createUniqueTitle(); + + final long oneHourAgo = System.currentTimeMillis() - 1000 * 60 * 60; + final long fiveDaysAgo = System.currentTimeMillis() - 1000 * 60 * 60 * 24 * 5; + + insertHistoryItem(url1, createGUID(), oneHourAgo, 2, title1); + insertHistoryItem(createUniqueUrl(), createGUID(), fiveDaysAgo, 25, createUniqueTitle()); + + final Cursor cursor = highlightsClient.query(highlightsTestUri, null, null, null, null); + Assert.assertNotNull(cursor); + + // Verify that only the first URL (with one visit) is picked and the second URL with 25 visits is ignored. + + Assert.assertEquals(1, cursor.getCount()); + + cursor.moveToNext(); + assertCursor(cursor, url1, title1); + + cursor.close(); + } + + /** + * Scenario: The database contains history with and without titles. + * + * Assert that: + * - History without titles is not picked for highlights. + */ + @Test + public void testHistoryWithoutTitlesIsNotPicked() throws Exception { + final String url1 = createUniqueUrl(); + final String url2 = createUniqueUrl(); + final String title1 = ""; + final String title2 = createUniqueTitle(); + + final long oneHourAgo = System.currentTimeMillis() - 1000 * 60 * 60; + final long fiveDaysAgo = System.currentTimeMillis() - 1000 * 60 * 60 * 24 * 5; + + insertHistoryItem(url1, createGUID(), oneHourAgo, 1, title1); + insertHistoryItem(url2, createGUID(), fiveDaysAgo, 1, title2); + + final Cursor cursor = highlightsClient.query(highlightsTestUri, null, null, null, null); + Assert.assertNotNull(cursor); + + // Only one bookmark will be picked for highlights + Assert.assertEquals(1, cursor.getCount()); + + cursor.moveToNext(); + assertCursor(cursor, url2, title2); + + cursor.close(); + } + + /** + * Scenario: Database contains two bookmarks (unvisited). + * + * Assert that: + * - One bookmark is picked for highlights. + */ + @Test + public void testPickingBookmarkForHighlights() throws Exception { + final long oneHourAgo = System.currentTimeMillis() - 1000 * 60 * 60; + final long fiveDaysAgo = System.currentTimeMillis() - 1000 * 60 * 60 * 24 * 5; + + final String url1 = createUniqueUrl(); + final String url2 = createUniqueUrl(); + final String title1 = createUniqueTitle(); + final String title2 = createUniqueTitle(); + + insertBookmarkItem(url1, title1, oneHourAgo); + insertBookmarkItem(url2, title2, fiveDaysAgo); + + final Cursor cursor = highlightsClient.query(highlightsTestUri, null, null, null, null); + Assert.assertNotNull(cursor); + + Assert.assertEquals(1, cursor.getCount()); + + cursor.moveToNext(); + assertCursor(cursor, url1, title1); + + cursor.close(); + } + + /** + * Scenario: Database contains an often visited bookmark. + * + * Assert that: + * - Bookmark is not selected for highlights. + */ + @Test + public void testOftenVisitedBookmarksWillNotBePicked() throws Exception { + final String url = createUniqueUrl(); + final long oneHourAgo = System.currentTimeMillis() - 1000 * 60 * 60; + + insertBookmarkItem(url, createUniqueTitle(), oneHourAgo); + insertHistoryItem(url, createGUID(), oneHourAgo, 25, createUniqueTitle()); + + final Cursor cursor = highlightsClient.query(highlightsTestUri, null, null, null, null); + Assert.assertNotNull(cursor); + + Assert.assertEquals(0, cursor.getCount()); + + cursor.close(); + } + + /** + * Scenario: Database contains URL as bookmark and in history (not visited often). + * + * Assert that: + * - URL is not picked twice (as bookmark and from history) + */ + @Test + public void testSameUrlIsNotPickedFromHistoryAndBookmarks() throws Exception { + final String url = createUniqueUrl(); + + final long oneHourAgo = System.currentTimeMillis() - 1000 * 60 * 60; + + // Insert bookmark that is picked for highlights + insertBookmarkItem(url, createUniqueTitle(), oneHourAgo); + // Insert history for same URL that would be picked for highlights too + insertHistoryItem(url, createGUID(), oneHourAgo, 2, createUniqueTitle()); + + final Cursor cursor = highlightsClient.query(highlightsTestUri, null, null, null, null); + Assert.assertNotNull(cursor); + + Assert.assertEquals(1, cursor.getCount()); + + cursor.close(); + } + + /** + * Scenario: Database contains only old bookmarks. + * + * Assert that: + * - Old bookmarks are not selected as highlight. + */ + @Test + public void testVeryOldBookmarksAreNotSelected() throws Exception { + final long oneWeekAgo = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(7); + final long oneMonthAgo = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(31); + final long oneYearAgo = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(365); + + insertBookmarkItem(createUniqueUrl(), createUniqueTitle(), oneWeekAgo); + insertBookmarkItem(createUniqueUrl(), createUniqueTitle(), oneMonthAgo); + insertBookmarkItem(createUniqueUrl(), createUniqueTitle(), oneYearAgo); + + final Cursor cursor = highlightsClient.query(highlightsTestUri, null, null, null, null); + Assert.assertNotNull(cursor); + + Assert.assertEquals(0, cursor.getCount()); + + cursor.close(); + } + + @Test + public void testBlocklistItemsAreNotSelected() throws Exception { + final long oneDayAgo = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1); + + final String blockURL = createUniqueUrl(); + + insertBookmarkItem(blockURL, createUniqueTitle(), oneDayAgo); + + Cursor cursor = highlightsClient.query(highlightsTestUri, null, null, null, null); + Assert.assertNotNull(cursor); + Assert.assertEquals(1, cursor.getCount()); + cursor.close(); + + insertBlocklistItem(blockURL); + + cursor = highlightsClient.query(highlightsTestUri, null, null, null, null); + Assert.assertNotNull(cursor); + Assert.assertEquals(0, cursor.getCount()); + cursor.close(); + } + + @Test + public void testBlocklistItemsExpire() throws Exception { + final long oneDayAgo = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1); + + final String blockURL = createUniqueUrl(); + final String blockTitle = createUniqueTitle(); + + insertBookmarkItem(blockURL, blockTitle, oneDayAgo); + insertBlocklistItem(blockURL); + + { + final Cursor cursor = highlightsClient.query(highlightsTestUri, null, null, null, null); + Assert.assertNotNull(cursor); + Assert.assertEquals(0, cursor.getCount()); + cursor.close(); + } + + // Add (2000 / 10) items in the loop -> 201 items total + int itemsNeeded = BrowserProvider.DEFAULT_EXPIRY_RETAIN_COUNT / BrowserProvider.ACTIVITYSTREAM_BLOCKLIST_EXPIRY_FACTOR; + for (int i = 0; i < itemsNeeded; i++) { + insertBlocklistItem(createUniqueUrl()); + } + + // We still have zero highlights: the item is still blocked + { + final Cursor cursor = highlightsClient.query(highlightsTestUri, null, null, null, null); + Assert.assertNotNull(cursor); + Assert.assertEquals(0, cursor.getCount()); + cursor.close(); + } + + // expire the original blocked URL - only most recent 200 items are retained + historyClient.delete(expireHistoryNormalUri, null, null); + + // And the original URL is now in highlights again (note: this shouldn't happen in real life, + // since the URL will no longer be eligible for highlights by the time we expire it) + { + final Cursor cursor = highlightsClient.query(highlightsTestUri, null, null, null, null); + Assert.assertNotNull(cursor); + Assert.assertEquals(1, cursor.getCount()); + + cursor.moveToFirst(); + assertCursor(cursor, blockURL, blockTitle); + cursor.close(); + } + } + + private void insertBookmarkItem(String url, String title, long createdAt) throws RemoteException { + ContentValues values = new ContentValues(); + + values.put(BrowserContract.Bookmarks.URL, url); + values.put(BrowserContract.Bookmarks.TITLE, title); + values.put(BrowserContract.Bookmarks.PARENT, 0); + values.put(BrowserContract.Bookmarks.TYPE, BrowserContract.Bookmarks.TYPE_BOOKMARK); + values.put(BrowserContract.Bookmarks.DATE_CREATED, createdAt); + + bookmarksClient.insert(bookmarksTestUri, values); + } + + private void insertBlocklistItem(String url) throws RemoteException { + final ContentValues values = new ContentValues(); + values.put(BrowserContract.ActivityStreamBlocklist.URL, url); + + activityStreamBlocklistClient.insert(activityStreamBlocklistTestUri, values); + } + + private void assertCursor(Cursor cursor, String url, String title) { + final String actualTitle = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.Combined.TITLE)); + Assert.assertEquals(title, actualTitle); + + final String actualUrl = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.Combined.URL)); + Assert.assertEquals(url, actualUrl); + } + + private void assertCursorContainsEntry(Cursor cursor, String url, String title) { + cursor.moveToFirst(); + + do { + final String actualTitle = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.Combined.TITLE)); + final String actualUrl = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.Combined.URL)); + + if (actualTitle.equals(title) && actualUrl.equals(url)) { + return; + } + } while (cursor.moveToNext()); + + Assert.fail("Could not find entry title=" + title + ", url=" + url); + } + + private String createUniqueUrl() { + return new Uri.Builder() + .scheme("https") + .authority(UUID.randomUUID().toString() + ".example.org") + .appendPath(UUID.randomUUID().toString()) + .appendPath(UUID.randomUUID().toString()) + .build() + .toString(); + } + + private String createUniqueTitle() { + return "Title " + UUID.randomUUID().toString(); + } + + private String createGUID() { + return UUID.randomUUID().toString(); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/db/BrowserProviderHistoryTest.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/db/BrowserProviderHistoryTest.java new file mode 100644 index 000000000..850841432 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/db/BrowserProviderHistoryTest.java @@ -0,0 +1,341 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.db; + +import android.content.ContentProviderClient; +import android.content.ContentValues; +import android.database.Cursor; +import android.net.Uri; +import android.os.RemoteException; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.robolectric.shadows.ShadowContentResolver; + +import static org.junit.Assert.*; + +/** + * Testing functionality exposed by BrowserProvider ContentProvider (history, bookmarks, etc). + * This is WIP junit4 port of robocop tests at org.mozilla.gecko.tests.testBrowserProvider. + * See Bug 1269492 + */ +@RunWith(TestRunner.class) +public class BrowserProviderHistoryTest extends BrowserProviderHistoryVisitsTestBase { + private ContentProviderClient thumbnailClient; + private Uri thumbnailTestUri; + private Uri expireHistoryNormalUri; + private Uri expireHistoryAggressiveUri; + + private static final long THREE_MONTHS = 1000L * 60L * 60L * 24L * 30L * 3L; + + @Before + @Override + public void setUp() throws Exception { + super.setUp(); + final ShadowContentResolver cr = new ShadowContentResolver(); + thumbnailClient = cr.acquireContentProviderClient(BrowserContract.Thumbnails.CONTENT_URI); + thumbnailTestUri = testUri(BrowserContract.Thumbnails.CONTENT_URI); + expireHistoryNormalUri = testUri(BrowserContract.History.CONTENT_OLD_URI).buildUpon() + .appendQueryParameter( + BrowserContract.PARAM_EXPIRE_PRIORITY, + BrowserContract.ExpirePriority.NORMAL.toString() + ).build(); + expireHistoryAggressiveUri = testUri(BrowserContract.History.CONTENT_OLD_URI).buildUpon() + .appendQueryParameter( + BrowserContract.PARAM_EXPIRE_PRIORITY, + BrowserContract.ExpirePriority.AGGRESSIVE.toString() + ).build(); + } + + @After + @Override + public void tearDown() { + thumbnailClient.release(); + super.tearDown(); + } + + /** + * Test aggressive expiration on new (recent) history items + */ + @Test + public void testHistoryExpirationAggressiveNew() throws Exception { + final int historyItemsCount = 3000; + insertHistory(historyItemsCount, System.currentTimeMillis()); + + historyClient.delete(expireHistoryAggressiveUri, null, null); + + /** + * Aggressive expiration should leave 500 history items + * See {@link BrowserProvider.AGGRESSIVE_EXPIRY_RETAIN_COUNT} + */ + assertRowCount(historyClient, historyTestUri, 500); + + /** + * Aggressive expiration should leave 15 thumbnails + * See {@link BrowserProvider.DEFAULT_EXPIRY_THUMBNAIL_COUNT} + */ + assertRowCount(thumbnailClient, thumbnailTestUri, 15); + } + + /** + * Test normal expiration on new (recent) history items + */ + @Test + public void testHistoryExpirationNormalNew() throws Exception { + final int historyItemsCount = 3000; + insertHistory(historyItemsCount, System.currentTimeMillis()); + + historyClient.delete(expireHistoryNormalUri, null, null); + + // Normal expiration shouldn't expire new items + assertRowCount(historyClient, historyTestUri, 3000); + + /** + * Normal expiration should leave 15 thumbnails + * See {@link BrowserProvider.DEFAULT_EXPIRY_THUMBNAIL_COUNT} + */ + assertRowCount(thumbnailClient, thumbnailTestUri, 15); + } + + /** + * Test aggressive expiration on old history items + */ + @Test + public void testHistoryExpirationAggressiveOld() throws Exception { + final int historyItemsCount = 3000; + insertHistory(historyItemsCount, System.currentTimeMillis() - THREE_MONTHS); + + historyClient.delete(expireHistoryAggressiveUri, null, null); + + /** + * Aggressive expiration should leave 500 history items + * See {@link BrowserProvider.AGGRESSIVE_EXPIRY_RETAIN_COUNT} + */ + assertRowCount(historyClient, historyTestUri, 500); + + /** + * Aggressive expiration should leave 15 thumbnails + * See {@link BrowserProvider.DEFAULT_EXPIRY_THUMBNAIL_COUNT} + */ + assertRowCount(thumbnailClient, thumbnailTestUri, 15); + } + + /** + * Test normal expiration on old history items + */ + @Test + public void testHistoryExpirationNormalOld() throws Exception { + final int historyItemsCount = 3000; + insertHistory(historyItemsCount, System.currentTimeMillis() - THREE_MONTHS); + + historyClient.delete(expireHistoryNormalUri, null, null); + + /** + * Normal expiration of old items should retain at most 2000 items + * See {@link BrowserProvider.DEFAULT_EXPIRY_RETAIN_COUNT} + */ + assertRowCount(historyClient, historyTestUri, 2000); + + /** + * Normal expiration should leave 15 thumbnails + * See {@link BrowserProvider.DEFAULT_EXPIRY_THUMBNAIL_COUNT} + */ + assertRowCount(thumbnailClient, thumbnailTestUri, 15); + } + + /** + * Test that we update aggregates at the appropriate times. Local visit aggregates are only updated + * when updating history record with PARAM_INCREMENT_VISITS=true. Remote aggregate values are updated + * only if set directly. Aggregate values are not set when inserting a new history record via insertHistory. + * Local aggregate values are set when inserting a new history record via update. + * @throws Exception + */ + @Test + public void testHistoryVisitAggregates() throws Exception { + final long baseDate = System.currentTimeMillis(); + final String url = "https://www.mozilla.org"; + final Uri historyIncrementVisitsUri = historyTestUri.buildUpon() + .appendQueryParameter(BrowserContract.PARAM_INCREMENT_VISITS, "true") + .appendQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED, "true").build(); + + // Test default values + insertHistoryItem(url, null, baseDate, null); + assertHistoryAggregates(BrowserContract.History.URL + " = ?", new String[] {url}, + 0, 0, 0, 0, 0); + + // Test setting visit count on new history item creation + final String url2 = "https://www.eff.org"; + insertHistoryItem(url2, null, baseDate, 17); + assertHistoryAggregates(BrowserContract.History.URL + " = ?", new String[] {url2}, + 17, 0, 0, 0, 0); + + // Test setting visit count on new history item creation via .update + final String url3 = "https://www.torproject.org"; + final ContentValues cv = new ContentValues(); + cv.put(BrowserContract.History.URL, url3); + cv.put(BrowserContract.History.VISITS, 13); + cv.put(BrowserContract.History.DATE_LAST_VISITED, baseDate); + historyClient.update(historyIncrementVisitsUri, cv, BrowserContract.History.URL + " = ?", new String[] {url3}); + assertHistoryAggregates(BrowserContract.History.URL + " = ?", new String[] {url3}, + 13, 13, baseDate, 0, 0); + + // Test that updating meta doesn't touch aggregates + cv.clear(); + cv.put(BrowserContract.History.TITLE, "New title"); + historyClient.update(historyTestUri, cv, BrowserContract.History.URL + " = ?", new String[] {url}); + assertHistoryAggregates(BrowserContract.History.URL + " = ?", new String[] {url}, + 0, 0, 0, 0, 0); + + // Test that incrementing visits without specifying visit count updates local aggregate values + final long lastVisited = System.currentTimeMillis(); + cv.clear(); + cv.put(BrowserContract.History.DATE_LAST_VISITED, lastVisited); + historyClient.update(historyIncrementVisitsUri, + cv, BrowserContract.History.URL + " = ?", new String[] {url}); + assertHistoryAggregates(BrowserContract.History.URL + " = ?", new String[] {url}, + 1, 1, lastVisited, 0, 0); + + // Test that incrementing visits by a specified visit count updates local aggregate values + // We don't support bumping visit count by more than 1. This doesn't make sense when we keep + // detailed information about our individual visits. + final long lastVisited2 = System.currentTimeMillis(); + cv.clear(); + cv.put(BrowserContract.History.DATE_LAST_VISITED, lastVisited2); + cv.put(BrowserContract.History.VISITS, 10); + historyClient.update( + historyTestUri.buildUpon() + .appendQueryParameter(BrowserContract.PARAM_INCREMENT_VISITS, "true").build(), + cv, BrowserContract.History.URL + " = ?", new String[] {url}); + assertHistoryAggregates(BrowserContract.History.URL + " = ?", new String[] {url}, + 2, 2, lastVisited2, 0, 0); + + // Test that we can directly update aggregate values + // NB: visits is unchanged (2) + final long lastVisited3 = System.currentTimeMillis(); + cv.clear(); + cv.put(BrowserContract.History.LOCAL_DATE_LAST_VISITED, lastVisited3); + cv.put(BrowserContract.History.LOCAL_VISITS, 19); + cv.put(BrowserContract.History.REMOTE_DATE_LAST_VISITED, lastVisited3 - 100); + cv.put(BrowserContract.History.REMOTE_VISITS, 3); + historyClient.update(historyTestUri, cv, BrowserContract.History.URL + " = ?", new String[] {url}); + assertHistoryAggregates(BrowserContract.History.URL + " = ?", new String[] {url}, + 2, 19, lastVisited3, 3, lastVisited3 - 100); + + // Test that we can set remote aggregate count to a specific value + cv.clear(); + cv.put(BrowserContract.History.REMOTE_VISITS, 5); + historyClient.update(historyTestUri, cv, BrowserContract.History.URL + " = ?", new String[] {url}); + assertHistoryAggregates(BrowserContract.History.URL + " = ?", new String[] {url}, + 2, 19, lastVisited3, 5, lastVisited3 - 100); + + // Test that we can increment remote aggregate value by setting a query param in the URI + final Uri historyIncrementRemoteAggregateUri = historyTestUri.buildUpon() + .appendQueryParameter(BrowserContract.PARAM_INCREMENT_REMOTE_AGGREGATES, "true") + .build(); + cv.clear(); + cv.put(BrowserContract.History.REMOTE_DATE_LAST_VISITED, lastVisited3); + cv.put(BrowserContract.History.REMOTE_VISITS, 3); + historyClient.update(historyIncrementRemoteAggregateUri, cv, BrowserContract.History.URL + " = ?", new String[] {url}); + // NB: remoteVisits=8. Previous value was 5, and we're incrementing by 3. + assertHistoryAggregates(BrowserContract.History.URL + " = ?", new String[] {url}, + 2, 19, lastVisited3, 8, lastVisited3); + + // Test that we throw when trying to increment REMOTE_VISITS without passing in "increment by" value + cv.clear(); + try { + historyClient.update(historyIncrementRemoteAggregateUri, cv, BrowserContract.History.URL + " = ?", new String[]{url}); + assertTrue("Expected to throw IllegalArgumentException", false); + } catch (IllegalArgumentException e) { + assertTrue(true); + + // NB: same values as above, to ensure throwing update didn't actually change anything. + assertHistoryAggregates(BrowserContract.History.URL + " = ?", new String[] {url}, + 2, 19, lastVisited3, 8, lastVisited3); + } + } + + private void assertHistoryAggregates(String selection, String[] selectionArg, int visits, int localVisits, long localLastVisited, int remoteVisits, long remoteLastVisited) throws Exception { + final Cursor c = historyClient.query(historyTestUri, new String[] { + BrowserContract.History.VISITS, + BrowserContract.History.LOCAL_VISITS, + BrowserContract.History.REMOTE_VISITS, + BrowserContract.History.LOCAL_DATE_LAST_VISITED, + BrowserContract.History.REMOTE_DATE_LAST_VISITED + }, selection, selectionArg, null); + + assertNotNull(c); + try { + assertTrue(c.moveToFirst()); + + final int visitsCol = c.getColumnIndexOrThrow(BrowserContract.History.VISITS); + final int localVisitsCol = c.getColumnIndexOrThrow(BrowserContract.History.LOCAL_VISITS); + final int remoteVisitsCol = c.getColumnIndexOrThrow(BrowserContract.History.REMOTE_VISITS); + final int localDateLastVisitedCol = c.getColumnIndexOrThrow(BrowserContract.History.LOCAL_DATE_LAST_VISITED); + final int remoteDateLastVisitedCol = c.getColumnIndexOrThrow(BrowserContract.History.REMOTE_DATE_LAST_VISITED); + + assertEquals(visits, c.getInt(visitsCol)); + + assertEquals(localVisits, c.getInt(localVisitsCol)); + assertEquals(localLastVisited, c.getLong(localDateLastVisitedCol)); + + assertEquals(remoteVisits, c.getInt(remoteVisitsCol)); + assertEquals(remoteLastVisited, c.getLong(remoteDateLastVisitedCol)); + } finally { + c.close(); + } + } + + /** + * Insert <code>count</code> history records with thumbnails, and for a third of records insert a visit. + * Inserting visits only for some of the history records is in order to ensure we're correctly JOIN-ing + * History and Visits tables in the Combined view. + * Will ensure that date_created and date_modified for new records are the same as last visited date. + * + * @param count number of history records to insert + * @param baseTime timestamp which will be used as a basis for last visited date + * @throws RemoteException + */ + private void insertHistory(int count, long baseTime) throws RemoteException { + Uri incrementUri = historyTestUri.buildUpon() + .appendQueryParameter(BrowserContract.PARAM_INCREMENT_VISITS, "true").build(); + + for (int i = 0; i < count; i++) { + final String url = "https://www.mozilla" + i + ".org"; + insertHistoryItem(url, "testGUID" + i, baseTime - i, null); + if (i % 3 == 0) { + assertEquals(1, historyClient.update(incrementUri, new ContentValues(), BrowserContract.History.URL + " = ?", new String[]{url})); + } + + // inserting a new entry sets the date created and modified automatically, so let's reset them + ContentValues cv = new ContentValues(); + cv.put(BrowserContract.History.DATE_CREATED, baseTime - i); + cv.put(BrowserContract.History.DATE_MODIFIED, baseTime - i); + assertEquals(1, historyClient.update(historyTestUri, cv, BrowserContract.History.URL + " = ?", + new String[] { "https://www.mozilla" + i + ".org" })); + } + + // insert thumbnails for history items + ContentValues[] thumbs = new ContentValues[count]; + for (int i = 0; i < count; i++) { + thumbs[i] = new ContentValues(); + thumbs[i].put(BrowserContract.Thumbnails.DATA, i); + thumbs[i].put(BrowserContract.Thumbnails.URL, "https://www.mozilla" + i + ".org"); + } + assertEquals(count, thumbnailClient.bulkInsert(thumbnailTestUri, thumbs)); + } + + private void assertRowCount(final ContentProviderClient client, final Uri uri, final int count) throws RemoteException { + final Cursor c = client.query(uri, null, null, null, null); + assertNotNull(c); + try { + assertEquals(count, c.getCount()); + } finally { + c.close(); + } + } +}
\ No newline at end of file diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/db/BrowserProviderHistoryVisitsTest.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/db/BrowserProviderHistoryVisitsTest.java new file mode 100644 index 000000000..71c21166d --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/db/BrowserProviderHistoryVisitsTest.java @@ -0,0 +1,338 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.db; + +import android.content.ContentValues; +import android.database.Cursor; +import android.net.Uri; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.TestRunner; + +import org.mozilla.gecko.db.BrowserContract.History; + +import static org.junit.Assert.*; + +@RunWith(TestRunner.class) +/** + * Testing insertion/deletion of visits as by-product of updating history records through BrowserProvider + */ +public class BrowserProviderHistoryVisitsTest extends BrowserProviderHistoryVisitsTestBase { + @Test + /** + * Testing updating history records without affecting visits + */ + public void testUpdateNoVisit() throws Exception { + insertHistoryItem("https://www.mozilla.org", "testGUID"); + + Cursor cursor = visitsClient.query(visitsTestUri, null, null, null, null); + assertNotNull(cursor); + assertEquals(0, cursor.getCount()); + cursor.close(); + + ContentValues historyUpdate = new ContentValues(); + historyUpdate.put(History.TITLE, "Mozilla!"); + assertEquals(1, + historyClient.update( + historyTestUri, historyUpdate, History.URL + " = ?", new String[] {"https://www.mozilla.org"} + ) + ); + + cursor = visitsClient.query(visitsTestUri, null, null, null, null); + assertNotNull(cursor); + assertEquals(0, cursor.getCount()); + cursor.close(); + + ContentValues historyToInsert = new ContentValues(); + historyToInsert.put(History.URL, "https://www.eff.org"); + assertEquals(1, + historyClient.update( + historyTestUri.buildUpon() + .appendQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED, "true").build(), + historyToInsert, null, null + ) + ); + + cursor = visitsClient.query(visitsTestUri, null, null, null, null); + assertNotNull(cursor); + assertEquals(0, cursor.getCount()); + cursor.close(); + } + + @Test + /** + * Testing INCREMENT_VISITS flag for multiple history records at once + */ + public void testUpdateMultipleHistoryIncrementVisit() throws Exception { + insertHistoryItem("https://www.mozilla.org", "testGUID"); + insertHistoryItem("https://www.mozilla.org", "testGUID2"); + + // test that visits get inserted when updating existing history records + assertEquals(2, historyClient.update( + historyTestUri.buildUpon() + .appendQueryParameter(BrowserContract.PARAM_INCREMENT_VISITS, "true").build(), + new ContentValues(), History.URL + " = ?", new String[] {"https://www.mozilla.org"} + )); + + Cursor cursor = visitsClient.query( + visitsTestUri, new String[] {BrowserContract.Visits.HISTORY_GUID}, null, null, null); + assertNotNull(cursor); + assertEquals(2, cursor.getCount()); + assertTrue(cursor.moveToFirst()); + + String guid1 = cursor.getString(cursor.getColumnIndex(BrowserContract.Visits.HISTORY_GUID)); + cursor.moveToNext(); + String guid2 = cursor.getString(cursor.getColumnIndex(BrowserContract.Visits.HISTORY_GUID)); + cursor.close(); + + assertNotEquals(guid1, guid2); + + assertTrue(guid1.equals("testGUID") || guid1.equals("testGUID2")); + } + + @Test + /** + * Testing INCREMENT_VISITS flag and its interplay with INSERT_IF_NEEDED + */ + public void testUpdateHistoryIncrementVisit() throws Exception { + insertHistoryItem("https://www.mozilla.org", "testGUID"); + + // test that visit gets inserted when updating an existing histor record + assertEquals(1, historyClient.update( + historyTestUri.buildUpon() + .appendQueryParameter(BrowserContract.PARAM_INCREMENT_VISITS, "true").build(), + new ContentValues(), History.URL + " = ?", new String[] {"https://www.mozilla.org"} + )); + + Cursor cursor = visitsClient.query( + visitsTestUri, new String[] {BrowserContract.Visits.HISTORY_GUID}, null, null, null); + assertNotNull(cursor); + assertEquals(1, cursor.getCount()); + assertTrue(cursor.moveToFirst()); + assertEquals( + "testGUID", + cursor.getString(cursor.getColumnIndex(BrowserContract.Visits.HISTORY_GUID)) + ); + cursor.close(); + + // test that visit gets inserted when updatingOrInserting a new history record + ContentValues historyItem = new ContentValues(); + historyItem.put(History.URL, "https://www.eff.org"); + + assertEquals(1, historyClient.update( + historyTestUri.buildUpon() + .appendQueryParameter(BrowserContract.PARAM_INCREMENT_VISITS, "true") + .appendQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED, "true").build(), + historyItem, null, null + )); + + cursor = historyClient.query( + historyTestUri, + new String[] {History.GUID}, History.URL + " = ?", new String[] {"https://www.eff.org"}, null + ); + assertNotNull(cursor); + assertEquals(1, cursor.getCount()); + assertTrue(cursor.moveToFirst()); + String insertedGUID = cursor.getString(cursor.getColumnIndex(History.GUID)); + cursor.close(); + + cursor = visitsClient.query( + visitsTestUri, new String[] {BrowserContract.Visits.HISTORY_GUID}, null, null, null); + assertNotNull(cursor); + assertEquals(2, cursor.getCount()); + assertTrue(cursor.moveToFirst()); + assertEquals(insertedGUID, + cursor.getString(cursor.getColumnIndex(BrowserContract.Visits.HISTORY_GUID)) + ); + cursor.close(); + } + + @Test + /** + * Test that for locally generated visits, we store their timestamps in microseconds, and not in + * milliseconds like history does. + */ + public void testTimestampConversionOnInsertion() throws Exception { + insertHistoryItem("https://www.mozilla.org", "testGUID"); + + Long lastVisited = System.currentTimeMillis(); + ContentValues updatedVisitedTime = new ContentValues(); + updatedVisitedTime.put(History.DATE_LAST_VISITED, lastVisited); + + // test with last visited date passed in + assertEquals(1, historyClient.update( + historyTestUri.buildUpon() + .appendQueryParameter(BrowserContract.PARAM_INCREMENT_VISITS, "true").build(), + updatedVisitedTime, History.URL + " = ?", new String[] {"https://www.mozilla.org"} + )); + + Cursor cursor = visitsClient.query(visitsTestUri, new String[] {BrowserContract.Visits.DATE_VISITED}, null, null, null); + assertNotNull(cursor); + assertEquals(1, cursor.getCount()); + assertTrue(cursor.moveToFirst()); + + assertEquals(lastVisited * 1000, cursor.getLong(cursor.getColumnIndex(BrowserContract.Visits.DATE_VISITED))); + cursor.close(); + + // test without last visited date + assertEquals(1, historyClient.update( + historyTestUri.buildUpon() + .appendQueryParameter(BrowserContract.PARAM_INCREMENT_VISITS, "true").build(), + new ContentValues(), History.URL + " = ?", new String[] {"https://www.mozilla.org"} + )); + + cursor = visitsClient.query(visitsTestUri, new String[] {BrowserContract.Visits.DATE_VISITED}, null, null, null); + assertNotNull(cursor); + assertEquals(2, cursor.getCount()); + assertTrue(cursor.moveToFirst()); + + // CP should generate time off of current time upon insertion and convert to microseconds. + // This also tests correct ordering (DESC on date). + assertTrue(lastVisited * 1000 < cursor.getLong(cursor.getColumnIndex(BrowserContract.Visits.DATE_VISITED))); + cursor.close(); + } + + @Test + /** + * This should perform `DELETE FROM visits WHERE history_guid in IN (?, ?, ?, ..., ?)` sort of statement + * SQLite has a variable count limit (999 by default), so we're testing here that our deletion + * code does the right thing and chunks deletes to account for this limitation. + */ + public void testDeletingLotsOfHistory() throws Exception { + Uri incrementUri = historyTestUri.buildUpon() + .appendQueryParameter(BrowserContract.PARAM_INCREMENT_VISITS, "true").build(); + + // insert bunch of history records, and for each insert a visit + for (int i = 0; i < 2100; i++) { + final String url = "https://www.mozilla" + i + ".org"; + insertHistoryItem(url, "testGUID" + i); + assertEquals(1, historyClient.update(incrementUri, new ContentValues(), History.URL + " = ?", new String[] {url})); + } + + // sanity check + Cursor cursor = visitsClient.query(visitsTestUri, null, null, null, null); + assertNotNull(cursor); + assertEquals(2100, cursor.getCount()); + cursor.close(); + + // delete all of the history items - this will trigger chunked deletion of visits as well + assertEquals(2100, + historyClient.delete(historyTestUri, null, null) + ); + + // check that all visits where deleted + cursor = visitsClient.query(visitsTestUri, null, null, null, null); + assertNotNull(cursor); + assertEquals(0, cursor.getCount()); + cursor.close(); + } + + @Test + /** + * Test visit deletion as by-product of history deletion - both explicit (from outside of Sync), + * and implicit (cascaded, from Sync). + */ + public void testDeletingHistory() throws Exception { + insertHistoryItem("https://www.mozilla.org", "testGUID"); + insertHistoryItem("https://www.eff.org", "testGUID2"); + + // insert some visits + assertEquals(1, historyClient.update( + historyTestUri.buildUpon() + .appendQueryParameter(BrowserContract.PARAM_INCREMENT_VISITS, "true").build(), + new ContentValues(), History.URL + " = ?", new String[] {"https://www.mozilla.org"} + )); + assertEquals(1, historyClient.update( + historyTestUri.buildUpon() + .appendQueryParameter(BrowserContract.PARAM_INCREMENT_VISITS, "true").build(), + new ContentValues(), History.URL + " = ?", new String[] {"https://www.mozilla.org"} + )); + assertEquals(1, historyClient.update( + historyTestUri.buildUpon() + .appendQueryParameter(BrowserContract.PARAM_INCREMENT_VISITS, "true").build(), + new ContentValues(), History.URL + " = ?", new String[] {"https://www.eff.org"} + )); + + Cursor cursor = visitsClient.query(visitsTestUri, null, null, null, null); + assertNotNull(cursor); + assertEquals(3, cursor.getCount()); + cursor.close(); + + // test that corresponding visit records are deleted if Sync isn't involved + assertEquals(1, + historyClient.delete(historyTestUri, History.URL + " = ?", new String[] {"https://www.mozilla.org"}) + ); + + cursor = visitsClient.query(visitsTestUri, null, null, null, null); + assertNotNull(cursor); + assertEquals(1, cursor.getCount()); + cursor.close(); + + // test that corresponding visit records are deleted if Sync is involved + // insert some more visits + ContentValues moz = new ContentValues(); + moz.put(History.URL, "https://www.mozilla.org"); + moz.put(History.GUID, "testGUID3"); + assertEquals(1, historyClient.update( + historyTestUri.buildUpon() + .appendQueryParameter(BrowserContract.PARAM_INCREMENT_VISITS, "true") + .appendQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED, "true").build(), + moz, History.URL + " = ?", new String[] {"https://www.mozilla.org"} + )); + assertEquals(1, historyClient.update( + historyTestUri.buildUpon() + .appendQueryParameter(BrowserContract.PARAM_INCREMENT_VISITS, "true") + .appendQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED, "true").build(), + new ContentValues(), History.URL + " = ?", new String[] {"https://www.eff.org"} + )); + + assertEquals(1, + historyClient.delete( + historyTestUri.buildUpon().appendQueryParameter(BrowserContract.PARAM_IS_SYNC, "true").build(), + History.URL + " = ?", new String[] {"https://www.eff.org"}) + ); + + cursor = visitsClient.query(visitsTestUri, new String[] {BrowserContract.Visits.HISTORY_GUID}, null, null, null); + assertNotNull(cursor); + assertEquals(1, cursor.getCount()); + assertTrue(cursor.moveToFirst()); + assertEquals("testGUID3", cursor.getString(cursor.getColumnIndex(BrowserContract.Visits.HISTORY_GUID))); + cursor.close(); + } + + @Test + /** + * Test that changes to History GUID are cascaded to individual visits. + * See UPDATE CASCADED on Visit's HISTORY_GUID foreign key. + */ + public void testHistoryGUIDUpdate() throws Exception { + insertHistoryItem("https://www.mozilla.org", "testGUID"); + insertHistoryItem("https://www.eff.org", "testGUID2"); + + // insert some visits + assertEquals(1, historyClient.update( + historyTestUri.buildUpon() + .appendQueryParameter(BrowserContract.PARAM_INCREMENT_VISITS, "true").build(), + new ContentValues(), History.URL + " = ?", new String[] {"https://www.mozilla.org"} + )); + assertEquals(1, historyClient.update( + historyTestUri.buildUpon() + .appendQueryParameter(BrowserContract.PARAM_INCREMENT_VISITS, "true").build(), + new ContentValues(), History.URL + " = ?", new String[] {"https://www.mozilla.org"} + )); + + // change testGUID -> testGUIDNew + ContentValues newGuid = new ContentValues(); + newGuid.put(History.GUID, "testGUIDNew"); + assertEquals(1, historyClient.update( + historyTestUri, newGuid, History.URL + " = ?", new String[] {"https://www.mozilla.org"} + )); + + Cursor cursor = visitsClient.query(visitsTestUri, null, BrowserContract.Visits.HISTORY_GUID + " = ?", new String[] {"testGUIDNew"}, null); + assertNotNull(cursor); + assertEquals(2, cursor.getCount()); + cursor.close(); + } +}
\ No newline at end of file diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/db/BrowserProviderHistoryVisitsTestBase.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/db/BrowserProviderHistoryVisitsTestBase.java new file mode 100644 index 000000000..b8ee0bb36 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/db/BrowserProviderHistoryVisitsTestBase.java @@ -0,0 +1,77 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.db; + +import android.content.ContentProviderClient; +import android.content.ContentValues; +import android.net.Uri; +import android.os.RemoteException; + +import org.junit.After; +import org.junit.Before; +import org.mozilla.gecko.background.db.DelegatingTestContentProvider; +import org.mozilla.gecko.sync.repositories.android.BrowserContractHelpers; +import org.robolectric.shadows.ShadowContentResolver; + +import java.util.UUID; + +public class BrowserProviderHistoryVisitsTestBase { + /* package-private */ ShadowContentResolver contentResolver; + /* package-private */ ContentProviderClient historyClient; + /* package-private */ ContentProviderClient visitsClient; + /* package-private */ Uri historyTestUri; + /* package-private */ Uri visitsTestUri; + + private BrowserProvider provider; + + @Before + public void setUp() throws Exception { + provider = new BrowserProvider(); + provider.onCreate(); + ShadowContentResolver.registerProvider(BrowserContract.AUTHORITY, new DelegatingTestContentProvider(provider)); + + contentResolver = new ShadowContentResolver(); + historyClient = contentResolver.acquireContentProviderClient(BrowserContractHelpers.HISTORY_CONTENT_URI); + visitsClient = contentResolver.acquireContentProviderClient(BrowserContractHelpers.VISITS_CONTENT_URI); + + historyTestUri = testUri(BrowserContract.History.CONTENT_URI); + visitsTestUri = testUri(BrowserContract.Visits.CONTENT_URI); + } + + @After + public void tearDown() { + historyClient.release(); + visitsClient.release(); + provider.shutdown(); + } + + /* package-private */ Uri testUri(Uri baseUri) { + return baseUri.buildUpon().appendQueryParameter(BrowserContract.PARAM_IS_TEST, "1").build(); + } + + /* package-private */ Uri insertHistoryItem(String url, String guid) throws RemoteException { + return insertHistoryItem(url, guid, System.currentTimeMillis(), null, null); + } + + /* package-private */ Uri insertHistoryItem(String url, String guid, Long lastVisited, Integer visitCount) throws RemoteException { + return insertHistoryItem(url, guid, lastVisited, visitCount, null); + } + + /* package-private */ Uri insertHistoryItem(String url, String guid, Long lastVisited, Integer visitCount, String title) throws RemoteException { + ContentValues historyItem = new ContentValues(); + historyItem.put(BrowserContract.History.URL, url); + if (guid != null) { + historyItem.put(BrowserContract.History.GUID, guid); + } + if (visitCount != null) { + historyItem.put(BrowserContract.History.VISITS, visitCount); + } + historyItem.put(BrowserContract.History.DATE_LAST_VISITED, lastVisited); + if (title != null) { + historyItem.put(BrowserContract.History.TITLE, title); + } + + return historyClient.insert(historyTestUri, historyItem); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/db/BrowserProviderVisitsTest.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/db/BrowserProviderVisitsTest.java new file mode 100644 index 000000000..928657e82 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/db/BrowserProviderVisitsTest.java @@ -0,0 +1,301 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.db; + +import android.content.ContentValues; +import android.database.Cursor; +import android.net.Uri; +import android.os.RemoteException; + +import static org.junit.Assert.*; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.db.BrowserContract.Visits; + +@RunWith(TestRunner.class) +/** + * Testing direct interactions with visits through BrowserProvider + */ +public class BrowserProviderVisitsTest extends BrowserProviderHistoryVisitsTestBase { + @Test + /** + * Test that default visit parameters are set on insert. + */ + public void testDefaultVisit() throws RemoteException { + String url = "https://www.mozilla.org"; + String guid = "testGuid"; + + assertNotNull(insertHistoryItem(url, guid)); + + ContentValues visitItem = new ContentValues(); + Long visitedDate = System.currentTimeMillis(); + visitItem.put(Visits.HISTORY_GUID, guid); + visitItem.put(Visits.DATE_VISITED, visitedDate); + Uri insertedVisitUri = visitsClient.insert(visitsTestUri, visitItem); + assertNotNull(insertedVisitUri); + + Cursor cursor = visitsClient.query(visitsTestUri, null, null, null, null); + assertNotNull(cursor); + try { + assertTrue(cursor.moveToFirst()); + String insertedGuid = cursor.getString(cursor.getColumnIndex(Visits.HISTORY_GUID)); + assertEquals(guid, insertedGuid); + + Long insertedDate = cursor.getLong(cursor.getColumnIndex(Visits.DATE_VISITED)); + assertEquals(visitedDate, insertedDate); + + Integer insertedType = cursor.getInt(cursor.getColumnIndex(Visits.VISIT_TYPE)); + assertEquals(insertedType, Integer.valueOf(1)); + + Integer insertedIsLocal = cursor.getInt(cursor.getColumnIndex(Visits.IS_LOCAL)); + assertEquals(insertedIsLocal, Integer.valueOf(1)); + } finally { + cursor.close(); + } + } + + @Test + /** + * Test that we can't insert visit for non-existing GUID. + */ + public void testMissingHistoryGuid() throws RemoteException { + ContentValues visitItem = new ContentValues(); + visitItem.put(Visits.HISTORY_GUID, "blah"); + visitItem.put(Visits.DATE_VISITED, System.currentTimeMillis()); + assertNull(visitsClient.insert(visitsTestUri, visitItem)); + } + + @Test + /** + * Test that visit insert uses non-conflict insert. + */ + public void testNonConflictInsert() throws RemoteException { + String url = "https://www.mozilla.org"; + String guid = "testGuid"; + + assertNotNull(insertHistoryItem(url, guid)); + + ContentValues visitItem = new ContentValues(); + Long visitedDate = System.currentTimeMillis(); + visitItem.put(Visits.HISTORY_GUID, guid); + visitItem.put(Visits.DATE_VISITED, visitedDate); + Uri insertedVisitUri = visitsClient.insert(visitsTestUri, visitItem); + assertNotNull(insertedVisitUri); + + Uri insertedVisitUri2 = visitsClient.insert(visitsTestUri, visitItem); + assertEquals(insertedVisitUri, insertedVisitUri2); + } + + @Test + /** + * Test that non-default visit parameters won't get overridden. + */ + public void testNonDefaultInsert() throws RemoteException { + assertNotNull(insertHistoryItem("https://www.mozilla.org", "testGuid")); + + Integer typeToInsert = 5; + Integer isLocalToInsert = 0; + + ContentValues visitItem = new ContentValues(); + visitItem.put(Visits.HISTORY_GUID, "testGuid"); + visitItem.put(Visits.DATE_VISITED, System.currentTimeMillis()); + visitItem.put(Visits.VISIT_TYPE, typeToInsert); + visitItem.put(Visits.IS_LOCAL, isLocalToInsert); + + assertNotNull(visitsClient.insert(visitsTestUri, visitItem)); + + Cursor cursor = visitsClient.query(visitsTestUri, null, null, null, null); + assertNotNull(cursor); + try { + assertTrue(cursor.moveToFirst()); + + Integer insertedVisitType = cursor.getInt(cursor.getColumnIndex(Visits.VISIT_TYPE)); + assertEquals(typeToInsert, insertedVisitType); + + Integer insertedIsLocal = cursor.getInt(cursor.getColumnIndex(Visits.IS_LOCAL)); + assertEquals(isLocalToInsert, insertedIsLocal); + } finally { + cursor.close(); + } + } + + @Test + /** + * Test that default sorting order (DATE_VISITED DESC) is set if we don't specify any sorting params + */ + public void testDefaultSortingOrder() throws RemoteException { + assertNotNull(insertHistoryItem("https://www.mozilla.org", "testGuid")); + + Long time1 = System.currentTimeMillis(); + Long time2 = time1 + 100; + Long time3 = time1 + 200; + + ContentValues visitItem = new ContentValues(); + visitItem.put(Visits.DATE_VISITED, time1); + visitItem.put(Visits.HISTORY_GUID, "testGuid"); + assertNotNull(visitsClient.insert(visitsTestUri, visitItem)); + + visitItem.put(Visits.DATE_VISITED, time3); + assertNotNull(visitsClient.insert(visitsTestUri, visitItem)); + + visitItem.put(Visits.DATE_VISITED, time2); + assertNotNull(visitsClient.insert(visitsTestUri, visitItem)); + + Cursor cursor = visitsClient.query(visitsTestUri, null, null, null, null); + assertNotNull(cursor); + try { + assertEquals(3, cursor.getCount()); + assertTrue(cursor.moveToFirst()); + + Long timeInserted = cursor.getLong(cursor.getColumnIndex(Visits.DATE_VISITED)); + assertEquals(time3, timeInserted); + + cursor.moveToNext(); + + timeInserted = cursor.getLong(cursor.getColumnIndex(Visits.DATE_VISITED)); + assertEquals(time2, timeInserted); + + cursor.moveToNext(); + + timeInserted = cursor.getLong(cursor.getColumnIndex(Visits.DATE_VISITED)); + assertEquals(time1, timeInserted); + } finally { + cursor.close(); + } + } + + @Test + /** + * Test that if we pass sorting params, they're not overridden + */ + public void testNonDefaultSortingOrder() throws RemoteException { + assertNotNull(insertHistoryItem("https://www.mozilla.org", "testGuid")); + + Long time1 = System.currentTimeMillis(); + Long time2 = time1 + 100; + Long time3 = time1 + 200; + + ContentValues visitItem = new ContentValues(); + visitItem.put(Visits.DATE_VISITED, time1); + visitItem.put(Visits.HISTORY_GUID, "testGuid"); + assertNotNull(visitsClient.insert(visitsTestUri, visitItem)); + + visitItem.put(Visits.DATE_VISITED, time3); + assertNotNull(visitsClient.insert(visitsTestUri, visitItem)); + + visitItem.put(Visits.DATE_VISITED, time2); + assertNotNull(visitsClient.insert(visitsTestUri, visitItem)); + + Cursor cursor = visitsClient.query(visitsTestUri, null, null, null, Visits.DATE_VISITED + " ASC"); + assertNotNull(cursor); + assertEquals(3, cursor.getCount()); + assertTrue(cursor.moveToFirst()); + + Long timeInserted = cursor.getLong(cursor.getColumnIndex(Visits.DATE_VISITED)); + assertEquals(time1, timeInserted); + + cursor.moveToNext(); + + timeInserted = cursor.getLong(cursor.getColumnIndex(Visits.DATE_VISITED)); + assertEquals(time2, timeInserted); + + cursor.moveToNext(); + + timeInserted = cursor.getLong(cursor.getColumnIndex(Visits.DATE_VISITED)); + assertEquals(time3, timeInserted); + + cursor.close(); + } + + @Test + /** + * Tests deletion of all visits, and by some selection (GUID, IS_LOCAL) + */ + public void testVisitDeletion() throws RemoteException { + assertNotNull(insertHistoryItem("https://www.mozilla.org", "testGuid")); + assertNotNull(insertHistoryItem("https://www.eff.org", "testGuid2")); + + Long time1 = System.currentTimeMillis(); + + ContentValues visitItem = new ContentValues(); + visitItem.put(Visits.DATE_VISITED, time1); + visitItem.put(Visits.HISTORY_GUID, "testGuid"); + assertNotNull(visitsClient.insert(visitsTestUri, visitItem)); + + visitItem = new ContentValues(); + visitItem.put(Visits.DATE_VISITED, time1 + 100); + visitItem.put(Visits.HISTORY_GUID, "testGuid"); + assertNotNull(visitsClient.insert(visitsTestUri, visitItem)); + + ContentValues visitItem2 = new ContentValues(); + visitItem2.put(Visits.DATE_VISITED, time1); + visitItem2.put(Visits.HISTORY_GUID, "testGuid2"); + assertNotNull(visitsClient.insert(visitsTestUri, visitItem2)); + + Cursor cursor = visitsClient.query(visitsTestUri, null, null, null, null); + assertNotNull(cursor); + assertEquals(3, cursor.getCount()); + cursor.close(); + + assertEquals(3, visitsClient.delete(visitsTestUri, null, null)); + + cursor = visitsClient.query(visitsTestUri, null, null, null, null); + assertNotNull(cursor); + assertEquals(0, cursor.getCount()); + cursor.close(); + + // test selective deletion - by IS_LOCAL + visitItem = new ContentValues(); + visitItem.put(Visits.DATE_VISITED, time1); + visitItem.put(Visits.HISTORY_GUID, "testGuid"); + visitItem.put(Visits.IS_LOCAL, 0); + assertNotNull(visitsClient.insert(visitsTestUri, visitItem)); + + visitItem = new ContentValues(); + visitItem.put(Visits.DATE_VISITED, time1 + 100); + visitItem.put(Visits.HISTORY_GUID, "testGuid"); + visitItem.put(Visits.IS_LOCAL, 1); + assertNotNull(visitsClient.insert(visitsTestUri, visitItem)); + + visitItem2 = new ContentValues(); + visitItem2.put(Visits.DATE_VISITED, time1); + visitItem2.put(Visits.HISTORY_GUID, "testGuid2"); + visitItem2.put(Visits.IS_LOCAL, 0); + assertNotNull(visitsClient.insert(visitsTestUri, visitItem2)); + + cursor = visitsClient.query(visitsTestUri, null, null, null, null); + assertNotNull(cursor); + assertEquals(3, cursor.getCount()); + cursor.close(); + + assertEquals(2, + visitsClient.delete(visitsTestUri, Visits.IS_LOCAL + " = ?", new String[]{"0"})); + cursor = visitsClient.query(visitsTestUri, null, null, null, null); + assertNotNull(cursor); + assertEquals(1, cursor.getCount()); + assertTrue(cursor.moveToFirst()); + assertEquals(time1 + 100, cursor.getLong(cursor.getColumnIndex(Visits.DATE_VISITED))); + assertEquals("testGuid", cursor.getString(cursor.getColumnIndex(Visits.HISTORY_GUID))); + assertEquals(1, cursor.getInt(cursor.getColumnIndex(Visits.IS_LOCAL))); + cursor.close(); + + // test selective deletion - by HISTORY_GUID + assertNotNull(visitsClient.insert(visitsTestUri, visitItem2)); + cursor = visitsClient.query(visitsTestUri, null, null, null, null); + assertNotNull(cursor); + assertEquals(2, cursor.getCount()); + cursor.close(); + + assertEquals(1, + visitsClient.delete(visitsTestUri, Visits.HISTORY_GUID + " = ?", new String[]{"testGuid"})); + cursor = visitsClient.query(visitsTestUri, null, null, null, null); + assertNotNull(cursor); + assertEquals(1, cursor.getCount()); + assertTrue(cursor.moveToFirst()); + assertEquals("testGuid2", cursor.getString(cursor.getColumnIndex(Visits.HISTORY_GUID))); + cursor.close(); + } +}
\ No newline at end of file diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/distribution/TestReferrerDescriptor.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/distribution/TestReferrerDescriptor.java new file mode 100644 index 000000000..9e553cf44 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/distribution/TestReferrerDescriptor.java @@ -0,0 +1,33 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.distribution; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.TestRunner; + +@RunWith(TestRunner.class) +public class TestReferrerDescriptor { + @Test + public void testReferrerDescriptor() { + String referrerString1 = "utm_source%3Dsource%26utm_content%3Dcontent%26utm_campaign%3Dcampaign%26utm_medium%3Dmedium%26utm_term%3Dterm"; + String referrerString2 = "utm_source=source&utm_content=content&utm_campaign=campaign&utm_medium=medium&utm_term=term"; + ReferrerDescriptor referrer1 = new ReferrerDescriptor(referrerString1); + Assert.assertNotNull(referrer1); + Assert.assertEquals(referrer1.source, "source"); + Assert.assertEquals(referrer1.content, "content"); + Assert.assertEquals(referrer1.campaign, "campaign"); + Assert.assertEquals(referrer1.medium, "medium"); + Assert.assertEquals(referrer1.term, "term"); + ReferrerDescriptor referrer2 = new ReferrerDescriptor(referrerString2); + Assert.assertNotNull(referrer2); + Assert.assertEquals(referrer2.source, "source"); + Assert.assertEquals(referrer2.content, "content"); + Assert.assertEquals(referrer2.campaign, "campaign"); + Assert.assertEquals(referrer2.medium, "medium"); + Assert.assertEquals(referrer2.term, "term"); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/dlc/TestDownloadAction.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/dlc/TestDownloadAction.java new file mode 100644 index 000000000..2252c90c8 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/dlc/TestDownloadAction.java @@ -0,0 +1,607 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.dlc; + +import android.content.Context; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.dlc.catalog.DownloadContent; +import org.mozilla.gecko.dlc.catalog.DownloadContentBuilder; +import org.mozilla.gecko.dlc.catalog.DownloadContentCatalog; +import org.robolectric.RuntimeEnvironment; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.net.HttpURLConnection; +import java.util.Arrays; +import java.util.Collections; + +import static org.mockito.Matchers.*; +import static org.mockito.Mockito.*; + +/** + * DownloadAction: Download content that has been scheduled during "study" or "verify". + */ +@RunWith(TestRunner.class) +public class TestDownloadAction { + private static final String TEST_URL = "http://example.org"; + + private static final int STATUS_OK = 200; + private static final int STATUS_PARTIAL_CONTENT = 206; + + /** + * Scenario: The current network is metered. + * + * Verify that: + * * No download is performed on a metered network + */ + @Test + public void testNothingIsDoneOnMeteredNetwork() throws Exception { + DownloadAction action = spy(new DownloadAction(null)); + doReturn(true).when(action).isActiveNetworkMetered(RuntimeEnvironment.application); + + action.perform(RuntimeEnvironment.application, null); + + verify(action, never()).buildHttpURLConnection(anyString()); + verify(action, never()).download(anyString(), any(File.class)); + } + + /** + * Scenario: No (connected) network is available. + * + * Verify that: + * * No download is performed + */ + @Test + public void testNothingIsDoneIfNoNetworkIsAvailable() throws Exception { + DownloadAction action = spy(new DownloadAction(null)); + doReturn(false).when(action).isConnectedToNetwork(RuntimeEnvironment.application); + + action.perform(RuntimeEnvironment.application, null); + + verify(action, never()).isActiveNetworkMetered(any(Context.class)); + verify(action, never()).buildHttpURLConnection(anyString()); + verify(action, never()).download(anyString(), any(File.class)); + } + + /** + * Scenario: Content is scheduled for download but already exists locally (with correct checksum). + * + * Verify that: + * * No download is performed for existing file + * * Content is marked as downloaded in the catalog + */ + @Test + public void testExistingAndVerifiedFilesAreNotDownloadedAgain() throws Exception { + DownloadContent content = new DownloadContentBuilder().build(); + + DownloadContentCatalog catalog = mock(DownloadContentCatalog.class); + doReturn(Collections.singletonList(content)).when(catalog).getScheduledDownloads(); + + DownloadAction action = spy(new DownloadAction(null)); + doReturn(false).when(action).isActiveNetworkMetered(RuntimeEnvironment.application); + + File file = mock(File.class); + doReturn(true).when(file).exists(); + doReturn(file).when(action).createTemporaryFile(RuntimeEnvironment.application, content); + doReturn(file).when(action).getDestinationFile(RuntimeEnvironment.application, content); + doReturn(true).when(action).verify(eq(file), anyString()); + + action.perform(RuntimeEnvironment.application, catalog); + + verify(action, never()).download(anyString(), any(File.class)); + verify(catalog).markAsDownloaded(content); + } + + /** + * Scenario: Server returns a server error (HTTP 500). + * + * Verify that: + * * Situation is treated as recoverable (RecoverableDownloadContentException) + */ + @Test(expected=BaseAction.RecoverableDownloadContentException.class) + public void testServerErrorsAreRecoverable() throws Exception { + HttpURLConnection connection = mockHttpURLConnection(500, ""); + + File temporaryFile = mock(File.class); + doReturn(false).when(temporaryFile).exists(); + + DownloadAction action = spy(new DownloadAction(null)); + doReturn(connection).when(action).buildHttpURLConnection(anyString()); + action.download(TEST_URL, temporaryFile); + + verify(connection).getInputStream(); + } + + /** + * Scenario: Server returns a client error (HTTP 404). + * + * Verify that: + * * Situation is treated as unrecoverable (UnrecoverableDownloadContentException) + */ + @Test(expected=BaseAction.UnrecoverableDownloadContentException.class) + public void testClientErrorsAreUnrecoverable() throws Exception { + HttpURLConnection connection = mockHttpURLConnection(404, ""); + + File temporaryFile = mock(File.class); + doReturn(false).when(temporaryFile).exists(); + + DownloadAction action = spy(new DownloadAction(null)); + doReturn(connection).when(action).buildHttpURLConnection(anyString()); + action.download(TEST_URL, temporaryFile); + + verify(connection).getInputStream(); + } + + /** + * Scenario: A successful download has been performed. + * + * Verify that: + * * The content will be extracted to the destination + * * The content is marked as downloaded in the catalog + */ + @Test + public void testSuccessfulDownloadsAreMarkedAsDownloaded() throws Exception { + DownloadContent content = new DownloadContentBuilder() + .setKind(DownloadContent.KIND_FONT) + .setType(DownloadContent.TYPE_ASSET_ARCHIVE) + .build(); + + DownloadContentCatalog catalog = mock(DownloadContentCatalog.class); + doReturn(Collections.singletonList(content)).when(catalog).getScheduledDownloads(); + + DownloadAction action = spy(new DownloadAction(null)); + doReturn(false).when(action).isActiveNetworkMetered(RuntimeEnvironment.application); + + File file = mockNotExistingFile(); + doReturn(file).when(action).createTemporaryFile(RuntimeEnvironment.application, content); + doReturn(file).when(action).getDestinationFile(RuntimeEnvironment.application, content); + + doReturn(false).when(action).verify(eq(file), anyString()); + doNothing().when(action).download(anyString(), eq(file)); + doReturn(true).when(action).verify(eq(file), anyString()); + doNothing().when(action).extract(eq(file), eq(file), anyString()); + + action.perform(RuntimeEnvironment.application, catalog); + + verify(action).download(anyString(), eq(file)); + verify(action).extract(eq(file), eq(file), anyString()); + verify(catalog).markAsDownloaded(content); + } + + /** + * Scenario: Pretend a partially downloaded file already exists. + * + * Verify that: + * * Range header is set in request + * * Content will be appended to existing file + * * Content will be marked as downloaded in catalog + */ + @Test + public void testResumingDownloadFromExistingFile() throws Exception { + DownloadContent content = new DownloadContentBuilder() + .setKind(DownloadContent.KIND_FONT) + .setType(DownloadContent.TYPE_ASSET_ARCHIVE) + .setSize(4223) + .build(); + + DownloadContentCatalog catalog = mock(DownloadContentCatalog.class); + doReturn(Collections.singletonList(content)).when(catalog).getScheduledDownloads(); + + DownloadAction action = spy(new DownloadAction(null)); + doReturn(false).when(action).isActiveNetworkMetered(RuntimeEnvironment.application); + + File temporaryFile = mockFileWithSize(1337L); + doReturn(temporaryFile).when(action).createTemporaryFile(RuntimeEnvironment.application, content); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + doReturn(outputStream).when(action).openFile(eq(temporaryFile), anyBoolean()); + + HttpURLConnection connection = mockHttpURLConnection(STATUS_PARTIAL_CONTENT, "HelloWorld"); + doReturn(connection).when(action).buildHttpURLConnection(anyString()); + + File destinationFile = mockNotExistingFile(); + doReturn(destinationFile).when(action).getDestinationFile(RuntimeEnvironment.application, content); + + doReturn(true).when(action).verify(eq(temporaryFile), anyString()); + doNothing().when(action).extract(eq(temporaryFile), eq(destinationFile), anyString()); + + action.perform(RuntimeEnvironment.application, catalog); + + verify(connection).getInputStream(); + verify(connection).setRequestProperty("Range", "bytes=1337-"); + + Assert.assertEquals("HelloWorld", new String(outputStream.toByteArray(), "UTF-8")); + + verify(action).openFile(eq(temporaryFile), eq(true)); + verify(catalog).markAsDownloaded(content); + verify(temporaryFile).delete(); + } + + /** + * Scenario: Download fails with IOException. + * + * Verify that: + * * Partially downloaded file will not be deleted + * * Content will not be marked as downloaded in catalog + */ + @Test + public void testTemporaryFileIsNotDeletedAfterDownloadAborted() throws Exception { + DownloadContent content = new DownloadContentBuilder() + .setKind(DownloadContent.KIND_FONT) + .setType(DownloadContent.TYPE_ASSET_ARCHIVE) + .setSize(4223) + .build(); + + DownloadContentCatalog catalog = mock(DownloadContentCatalog.class); + doReturn(Collections.singletonList(content)).when(catalog).getScheduledDownloads(); + + DownloadAction action = spy(new DownloadAction(null)); + doReturn(false).when(action).isActiveNetworkMetered(RuntimeEnvironment.application); + + File temporaryFile = mockFileWithSize(1337L); + doReturn(temporaryFile).when(action).createTemporaryFile(RuntimeEnvironment.application, content); + + ByteArrayOutputStream outputStream = spy(new ByteArrayOutputStream()); + doReturn(outputStream).when(action).openFile(eq(temporaryFile), anyBoolean()); + doThrow(IOException.class).when(outputStream).write(any(byte[].class), anyInt(), anyInt()); + + HttpURLConnection connection = mockHttpURLConnection(STATUS_PARTIAL_CONTENT, "HelloWorld"); + doReturn(connection).when(action).buildHttpURLConnection(anyString()); + + doReturn(mockNotExistingFile()).when(action).getDestinationFile(RuntimeEnvironment.application, content); + + action.perform(RuntimeEnvironment.application, catalog); + + verify(catalog, never()).markAsDownloaded(content); + verify(action, never()).verify(any(File.class), anyString()); + verify(temporaryFile, never()).delete(); + } + + /** + * Scenario: Partially downloaded file is already complete. + * + * Verify that: + * * No download request is made + * * File is treated as completed and will be verified and extracted + * * Content is marked as downloaded in catalog + */ + @Test + public void testNoRequestIsSentIfFileIsAlreadyComplete() throws Exception { + DownloadContent content = new DownloadContentBuilder() + .setKind(DownloadContent.KIND_FONT) + .setType(DownloadContent.TYPE_ASSET_ARCHIVE) + .setSize(1337L) + .build(); + + DownloadContentCatalog catalog = mock(DownloadContentCatalog.class); + doReturn(Collections.singletonList(content)).when(catalog).getScheduledDownloads(); + + DownloadAction action = spy(new DownloadAction(null)); + doReturn(false).when(action).isActiveNetworkMetered(RuntimeEnvironment.application); + + File temporaryFile = mockFileWithSize(1337L); + doReturn(temporaryFile).when(action).createTemporaryFile(RuntimeEnvironment.application, content); + + File destinationFile = mockNotExistingFile(); + doReturn(destinationFile).when(action).getDestinationFile(RuntimeEnvironment.application, content); + + doReturn(true).when(action).verify(eq(temporaryFile), anyString()); + doNothing().when(action).extract(eq(temporaryFile), eq(destinationFile), anyString()); + + action.perform(RuntimeEnvironment.application, catalog); + + verify(action, never()).download(anyString(), eq(temporaryFile)); + verify(action).verify(eq(temporaryFile), anyString()); + verify(action).extract(eq(temporaryFile), eq(destinationFile), anyString()); + verify(catalog).markAsDownloaded(content); + } + + /** + * Scenario: Download is completed but verification (checksum) failed. + * + * Verify that: + * * Downloaded file is deleted + * * File will not be extracted + * * Content is not marked as downloaded in the catalog + */ + @Test + public void testTemporaryFileWillBeDeletedIfVerificationFails() throws Exception { + DownloadContent content = new DownloadContentBuilder() + .setKind(DownloadContent.KIND_FONT) + .setType(DownloadContent.TYPE_ASSET_ARCHIVE) + .setSize(1337L) + .build(); + + DownloadContentCatalog catalog = mock(DownloadContentCatalog.class); + doReturn(Collections.singletonList(content)).when(catalog).getScheduledDownloads(); + + DownloadAction action = spy(new DownloadAction(null)); + doReturn(false).when(action).isActiveNetworkMetered(RuntimeEnvironment.application); + doNothing().when(action).download(anyString(), any(File.class)); + doReturn(false).when(action).verify(any(File.class), anyString()); + + File temporaryFile = mockNotExistingFile(); + doReturn(temporaryFile).when(action).createTemporaryFile(RuntimeEnvironment.application, content); + + File destinationFile = mockNotExistingFile(); + doReturn(destinationFile).when(action).getDestinationFile(RuntimeEnvironment.application, content); + + action.perform(RuntimeEnvironment.application, catalog); + + verify(temporaryFile).delete(); + verify(action, never()).extract(any(File.class), any(File.class), anyString()); + verify(catalog, never()).markAsDownloaded(content); + } + + /** + * Scenario: Not enough storage space for content is available. + * + * Verify that: + * * No download will per performed + */ + @Test + public void testNoDownloadIsPerformedIfNotEnoughStorageIsAvailable() throws Exception { + DownloadContent content = createFontWithSize(1337L); + DownloadContentCatalog catalog = mockCatalogWithScheduledDownloads(content); + + DownloadAction action = spy(new DownloadAction(null)); + doReturn(false).when(action).isActiveNetworkMetered(RuntimeEnvironment.application); + doReturn(true).when(action).isConnectedToNetwork(RuntimeEnvironment.application); + + File temporaryFile = mockNotExistingFile(); + doReturn(temporaryFile).when(action).createTemporaryFile(RuntimeEnvironment.application, content); + + File destinationFile = mockNotExistingFile(); + doReturn(destinationFile).when(action).getDestinationFile(RuntimeEnvironment.application, content); + + doReturn(true).when(action).hasEnoughDiskSpace(content, destinationFile, temporaryFile); + + verify(action, never()).buildHttpURLConnection(anyString()); + verify(action, never()).download(anyString(), any(File.class)); + verify(action, never()).verify(any(File.class), anyString()); + verify(catalog, never()).markAsDownloaded(content); + } + + /** + * Scenario: Not enough storage space for temporary file available. + * + * Verify that: + * * hasEnoughDiskSpace() returns false + */ + @Test + public void testWithNotEnoughSpaceForTemporaryFile() throws Exception{ + DownloadContent content = createFontWithSize(2048); + File destinationFile = mockNotExistingFile(); + File temporaryFile = mockNotExistingFileWithUsableSpace(1024); + + DownloadAction action = new DownloadAction(null); + Assert.assertFalse(action.hasEnoughDiskSpace(content, destinationFile, temporaryFile)); + } + + /** + * Scenario: Not enough storage space for destination file available. + * + * Verify that: + * * hasEnoughDiskSpace() returns false + */ + @Test + public void testWithNotEnoughSpaceForDestinationFile() throws Exception { + DownloadContent content = createFontWithSize(2048); + File destinationFile = mockNotExistingFileWithUsableSpace(1024); + File temporaryFile = mockNotExistingFile(); + + DownloadAction action = new DownloadAction(null); + Assert.assertFalse(action.hasEnoughDiskSpace(content, destinationFile, temporaryFile)); + } + + /** + * Scenario: Enough storage space for temporary and destination file available. + * + * Verify that: + * * hasEnoughDiskSpace() returns true + */ + @Test + public void testWithEnoughSpaceForEverything() throws Exception { + DownloadContent content = createFontWithSize(2048); + File destinationFile = mockNotExistingFileWithUsableSpace(4096); + File temporaryFile = mockNotExistingFileWithUsableSpace(4096); + + DownloadAction action = new DownloadAction(null); + Assert.assertTrue(action.hasEnoughDiskSpace(content, destinationFile, temporaryFile)); + } + + /** + * Scenario: Download failed with network I/O error. + * + * Verify that: + * * Error is not counted as failure + */ + @Test + public void testNetworkErrorIsNotCountedAsFailure() throws Exception { + DownloadContent content = createFont(); + DownloadContentCatalog catalog = mockCatalogWithScheduledDownloads(content); + + DownloadAction action = spy(new DownloadAction(null)); + doReturn(true).when(action).isConnectedToNetwork(RuntimeEnvironment.application); + doReturn(false).when(action).isActiveNetworkMetered(RuntimeEnvironment.application); + doReturn(mockNotExistingFile()).when(action).createTemporaryFile(RuntimeEnvironment.application, content); + doReturn(mockNotExistingFile()).when(action).getDestinationFile(RuntimeEnvironment.application, content); + doReturn(true).when(action).hasEnoughDiskSpace(eq(content), any(File.class), any(File.class)); + + HttpURLConnection connection = mockHttpURLConnection(STATUS_OK, ""); + doThrow(IOException.class).when(connection).getInputStream(); + doReturn(connection).when(action).buildHttpURLConnection(anyString()); + + action.perform(RuntimeEnvironment.application, catalog); + + verify(catalog, never()).rememberFailure(eq(content), anyInt()); + verify(catalog, never()).markAsDownloaded(content); + } + + /** + * Scenario: Disk IO Error when extracting file. + * + * Verify that: + * * Error is counted as failure + * * After multiple errors the content is marked as permanently failed + */ + @Test + public void testDiskIOErrorIsCountedAsFailure() throws Exception { + DownloadContent content = createFont(); + DownloadContentCatalog catalog = mockCatalogWithScheduledDownloads(content); + doCallRealMethod().when(catalog).rememberFailure(eq(content), anyInt()); + doCallRealMethod().when(catalog).markAsPermanentlyFailed(content); + + Assert.assertEquals(DownloadContent.STATE_NONE, content.getState()); + + DownloadAction action = spy(new DownloadAction(null)); + doReturn(true).when(action).isConnectedToNetwork(RuntimeEnvironment.application); + doReturn(false).when(action).isActiveNetworkMetered(RuntimeEnvironment.application); + doReturn(mockNotExistingFile()).when(action).createTemporaryFile(RuntimeEnvironment.application, content); + doReturn(mockNotExistingFile()).when(action).getDestinationFile(RuntimeEnvironment.application, content); + doReturn(true).when(action).hasEnoughDiskSpace(eq(content), any(File.class), any(File.class)); + doNothing().when(action).download(anyString(), any(File.class)); + doReturn(true).when(action).verify(any(File.class), anyString()); + + File destinationFile = mock(File.class); + doReturn(false).when(destinationFile).exists(); + File parentFile = mock(File.class); + doReturn(false).when(parentFile).mkdirs(); + doReturn(false).when(parentFile).exists(); + doReturn(parentFile).when(destinationFile).getParentFile(); + doReturn(destinationFile).when(action).getDestinationFile(RuntimeEnvironment.application, content); + + for (int i = 0; i < 10; i++) { + action.perform(RuntimeEnvironment.application, catalog); + + Assert.assertEquals(DownloadContent.STATE_NONE, content.getState()); + } + + action.perform(RuntimeEnvironment.application, catalog); + + Assert.assertEquals(DownloadContent.STATE_FAILED, content.getState()); + verify(catalog, times(11)).rememberFailure(eq(content), anyInt()); + } + + /** + * Scenario: If the file to be downloaded is of kind - "hyphenation" + * + * Verify that: + * * isHyphenationDictionary returns true for a download content with kind "hyphenation" + * * isHyphenationDictionary returns false for a download content with unknown/different kind like "Font" + */ + @Test + public void testIsHyphenationDictionary() throws Exception { + DownloadContent hyphenationContent = createHyphenationDictionary(); + Assert.assertTrue(hyphenationContent.isHyphenationDictionary()); + DownloadContent fontContent = createFont(); + Assert.assertFalse(fontContent.isHyphenationDictionary()); + DownloadContent unknownContent = createUnknownContent(1024L); + Assert.assertFalse(unknownContent.isHyphenationDictionary()); + } + + /** + * Scenario: If the content to be downloaded is known + * + * Verify that: + * * isKnownContent returns true for a downloadable content with a known kind and type. + * * isKnownContent returns false for a downloadable content with unknown kind and type. + */ + @Test + public void testIsKnownContent() throws Exception { + DownloadContent fontContent = createFontWithSize(1024L); + DownloadContent hyphenationContent = createHyphenationDictionaryWithSize(1024L); + DownloadContent unknownContent = createUnknownContent(1024L); + DownloadContent contentWithUnknownType = createContentWithoutType(1024L); + + Assert.assertTrue(fontContent.isKnownContent()); + Assert.assertTrue(hyphenationContent.isKnownContent()); + Assert.assertFalse(unknownContent.isKnownContent()); + Assert.assertFalse(contentWithUnknownType.isKnownContent()); + } + + private DownloadContent createUnknownContent(long size) { + return new DownloadContentBuilder() + .setSize(size) + .build(); + } + + private DownloadContent createContentWithoutType(long size) { + return new DownloadContentBuilder() + .setKind(DownloadContent.KIND_HYPHENATION_DICTIONARY) + .setSize(size) + .build(); + } + + private DownloadContent createFont() { + return createFontWithSize(102400L); + } + + private DownloadContent createFontWithSize(long size) { + return new DownloadContentBuilder() + .setKind(DownloadContent.KIND_FONT) + .setType(DownloadContent.TYPE_ASSET_ARCHIVE) + .setSize(size) + .build(); + } + + private DownloadContent createHyphenationDictionary() { + return createHyphenationDictionaryWithSize(102400L); + } + + private DownloadContent createHyphenationDictionaryWithSize(long size) { + return new DownloadContentBuilder() + .setKind(DownloadContent.KIND_HYPHENATION_DICTIONARY) + .setType(DownloadContent.TYPE_ASSET_ARCHIVE) + .setSize(size) + .build(); + } + + private DownloadContentCatalog mockCatalogWithScheduledDownloads(DownloadContent... content) { + DownloadContentCatalog catalog = mock(DownloadContentCatalog.class); + doReturn(Arrays.asList(content)).when(catalog).getScheduledDownloads(); + return catalog; + } + + private static File mockNotExistingFile() { + return mockFileWithUsableSpace(false, 0, Long.MAX_VALUE); + } + + private static File mockNotExistingFileWithUsableSpace(long usableSpace) { + return mockFileWithUsableSpace(false, 0, usableSpace); + } + + private static File mockFileWithSize(long length) { + return mockFileWithUsableSpace(true, length, Long.MAX_VALUE); + } + + private static File mockFileWithUsableSpace(boolean exists, long length, long usableSpace) { + File file = mock(File.class); + doReturn(exists).when(file).exists(); + doReturn(length).when(file).length(); + + File parentFile = mock(File.class); + doReturn(usableSpace).when(parentFile).getUsableSpace(); + doReturn(parentFile).when(file).getParentFile(); + + return file; + } + + private static HttpURLConnection mockHttpURLConnection(int statusCode, String content) throws Exception { + HttpURLConnection connection = mock(HttpURLConnection.class); + + doReturn(statusCode).when(connection).getResponseCode(); + doReturn(new ByteArrayInputStream(content.getBytes("UTF-8"))).when(connection).getInputStream(); + + return connection; + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/dlc/TestStudyAction.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/dlc/TestStudyAction.java new file mode 100644 index 000000000..6b2ce83df --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/dlc/TestStudyAction.java @@ -0,0 +1,119 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.dlc; + +import android.content.Context; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.dlc.catalog.DownloadContent; +import org.mozilla.gecko.dlc.catalog.DownloadContentBuilder; +import org.mozilla.gecko.dlc.catalog.DownloadContentCatalog; +import org.robolectric.RuntimeEnvironment; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; + +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * StudyAction: Scan the catalog for "new" content available for download. + */ +@RunWith(TestRunner.class) +public class TestStudyAction { + /** + * Scenario: Catalog is empty. + * + * Verify that: + * * No download is scheduled + * * Download action is not started + */ + @Test + public void testPerformWithEmptyCatalog() { + DownloadContentCatalog catalog = mock(DownloadContentCatalog.class); + when(catalog.getContentToStudy()).thenReturn(new ArrayList<DownloadContent>()); + + StudyAction action = spy(new StudyAction()); + action.perform(RuntimeEnvironment.application, catalog); + + verify(catalog).getContentToStudy(); + verify(catalog, never()).markAsDownloaded(any(DownloadContent.class)); + verify(action, never()).startDownloads(any(Context.class)); + } + + /** + * Scenario: Catalog contains two items that have not been downloaded yet. + * + * Verify that: + * * Both items are scheduled to be downloaded + */ + @Test + public void testPerformWithNewContent() { + DownloadContent content1 = new DownloadContentBuilder() + .setType(DownloadContent.TYPE_ASSET_ARCHIVE) + .setKind(DownloadContent.KIND_FONT) + .build(); + DownloadContent content2 = new DownloadContentBuilder() + .setType(DownloadContent.TYPE_ASSET_ARCHIVE) + .setKind(DownloadContent.KIND_FONT) + .build(); + + DownloadContentCatalog catalog = mock(DownloadContentCatalog.class); + when(catalog.getContentToStudy()).thenReturn(Arrays.asList(content1, content2)); + + StudyAction action = spy(new StudyAction()); + action.perform(RuntimeEnvironment.application, catalog); + + verify(catalog).scheduleDownload(content1); + verify(catalog).scheduleDownload(content2); + } + + /** + * Scenario: Catalog contains item that are scheduled for download. + * + * Verify that: + * * Download action is started + */ + @Test + public void testStartingDownloadsAfterScheduling() { + DownloadContentCatalog catalog = mock(DownloadContentCatalog.class); + when(catalog.hasScheduledDownloads()).thenReturn(true); + + StudyAction action = spy(new StudyAction()); + action.perform(RuntimeEnvironment.application, catalog); + + verify(action).startDownloads(any(Context.class)); + } + + /** + * Scenario: Catalog contains unknown content. + * + * Verify that: + * * Unknown content is not scheduled for download. + */ + @Test + public void testPerformWithUnknownContent() { + DownloadContent content = new DownloadContentBuilder() + .setType("Unknown-Type") + .setKind("Unknown-Kind") + .build(); + + DownloadContentCatalog catalog = mock(DownloadContentCatalog.class); + when(catalog.getContentToStudy()).thenReturn(Collections.singletonList(content)); + + StudyAction action = spy(new StudyAction()); + action.perform(RuntimeEnvironment.application, catalog); + + verify(catalog, never()).scheduleDownload(content); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/dlc/TestSyncAction.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/dlc/TestSyncAction.java new file mode 100644 index 000000000..1e494975e --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/dlc/TestSyncAction.java @@ -0,0 +1,276 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.dlc; + +import android.content.Context; +import android.support.v4.util.ArrayMap; +import android.support.v4.util.AtomicFile; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.dlc.catalog.DownloadContent; +import org.mozilla.gecko.dlc.catalog.DownloadContentBuilder; +import org.mozilla.gecko.dlc.catalog.DownloadContentCatalog; +import org.mozilla.gecko.util.IOUtils; +import org.robolectric.RuntimeEnvironment; + +import java.io.BufferedInputStream; +import java.io.ByteArrayOutputStream; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.List; + +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyLong; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +/** + * SyncAction: Synchronize catalog from a (mocked) Kinto instance. + */ +@RunWith(TestRunner.class) +public class TestSyncAction { + /** + * Scenario: The server returns an empty record set. + */ + @Test + public void testEmptyResult() throws Exception { + SyncAction action = spy(new SyncAction()); + doReturn(true).when(action).isSyncEnabledForClient(RuntimeEnvironment.application); + doReturn(new JSONArray()).when(action).fetchRawCatalog(anyLong()); + + action.perform(RuntimeEnvironment.application, mockCatalog()); + + verify(action, never()).createContent(anyCatalog(), anyJSONObject()); + verify(action, never()).updateContent(anyCatalog(), anyJSONObject(), anyContent()); + verify(action, never()).deleteContent(anyCatalog(), anyString()); + + verify(action, never()).startStudyAction(anyContext()); + } + + /** + * Scenario: The server returns an item that is not in the catalog yet. + */ + @Test + public void testAddingNewContent() throws Exception { + SyncAction action = spy(new SyncAction()); + doReturn(true).when(action).isSyncEnabledForClient(RuntimeEnvironment.application); + doReturn(fromFile("dlc_sync_single_font.json")).when(action).fetchRawCatalog(anyLong()); + + DownloadContentCatalog catalog = mockCatalog(); + + action.perform(RuntimeEnvironment.application, catalog); + + // A new content item has been created + verify(action).createContent(anyCatalog(), anyJSONObject()); + + // No content item has been updated or deleted + verify(action, never()).updateContent(anyCatalog(), anyJSONObject(), anyContent()); + verify(action, never()).deleteContent(anyCatalog(), anyString()); + + // A new item has been added to the catalog + ArgumentCaptor<DownloadContent> captor = ArgumentCaptor.forClass(DownloadContent.class); + verify(catalog).add(captor.capture()); + + // The item matches the values from the server response + DownloadContent content = captor.getValue(); + Assert.assertEquals("c906275c-3747-fe27-426f-6187526a6f06", content.getId()); + Assert.assertEquals("4ed509317f1bb441b185ea13bf1c9d19d1a0b396962efa3b5dc3190ad88f2067", content.getChecksum()); + Assert.assertEquals("960be4fc5a92c1dc488582b215d5d75429fd4ffbee463105d29992cd792a912e", content.getDownloadChecksum()); + Assert.assertEquals("CharisSILCompact-R.ttf", content.getFilename()); + Assert.assertEquals(DownloadContent.KIND_FONT, content.getKind()); + Assert.assertEquals("/attachments/0d28a72d-a51f-46f8-9e5a-f95c61de904e.gz", content.getLocation()); + Assert.assertEquals(DownloadContent.TYPE_ASSET_ARCHIVE, content.getType()); + Assert.assertEquals(1455710632607L, content.getLastModified()); + Assert.assertEquals(1727656L, content.getSize()); + Assert.assertEquals(DownloadContent.STATE_NONE, content.getState()); + } + + /** + * Scenario: The catalog is using the old format, we want to make sure we abort cleanly. + */ + @Test + public void testUpdatingWithOldCatalog() throws Exception{ + SyncAction action = spy(new SyncAction()); + doReturn(true).when(action).isSyncEnabledForClient(RuntimeEnvironment.application); + doReturn(fromFile("dlc_sync_old_format.json")).when(action).fetchRawCatalog(anyLong()); + + DownloadContent existingContent = createTestContent("c906275c-3747-fe27-426f-6187526a6f06"); + DownloadContentCatalog catalog = spy(new MockedContentCatalog(existingContent)); + + action.perform(RuntimeEnvironment.application, catalog); + + // make sure nothing was done + verify(action, never()).createContent(anyCatalog(), anyJSONObject()); + verify(action, never()).updateContent(anyCatalog(), anyJSONObject(), anyContent()); + verify(action, never()).deleteContent(anyCatalog(), anyString()); + verify(action, never()).startStudyAction(anyContext()); + } + + + /** + * Scenario: The catalog contains one item and the server returns a new version. + */ + @Test + public void testUpdatingExistingContent() throws Exception{ + SyncAction action = spy(new SyncAction()); + doReturn(true).when(action).isSyncEnabledForClient(RuntimeEnvironment.application); + doReturn(fromFile("dlc_sync_single_font.json")).when(action).fetchRawCatalog(anyLong()); + + DownloadContent existingContent = createTestContent("c906275c-3747-fe27-426f-6187526a6f06"); + DownloadContentCatalog catalog = spy(new MockedContentCatalog(existingContent)); + + action.perform(RuntimeEnvironment.application, catalog); + + // A content item has been updated + verify(action).updateContent(anyCatalog(), anyJSONObject(), eq(existingContent)); + + // No content item has been created or deleted + verify(action, never()).createContent(anyCatalog(), anyJSONObject()); + verify(action, never()).deleteContent(anyCatalog(), anyString()); + + // An item has been updated in the catalog + ArgumentCaptor<DownloadContent> captor = ArgumentCaptor.forClass(DownloadContent.class); + verify(catalog).update(captor.capture()); + + // The item has the new values from the sever response + DownloadContent content = captor.getValue(); + Assert.assertEquals("c906275c-3747-fe27-426f-6187526a6f06", content.getId()); + Assert.assertEquals("4ed509317f1bb441b185ea13bf1c9d19d1a0b396962efa3b5dc3190ad88f2067", content.getChecksum()); + Assert.assertEquals("960be4fc5a92c1dc488582b215d5d75429fd4ffbee463105d29992cd792a912e", content.getDownloadChecksum()); + Assert.assertEquals("CharisSILCompact-R.ttf", content.getFilename()); + Assert.assertEquals(DownloadContent.KIND_FONT, content.getKind()); + Assert.assertEquals("/attachments/0d28a72d-a51f-46f8-9e5a-f95c61de904e.gz", content.getLocation()); + Assert.assertEquals(DownloadContent.TYPE_ASSET_ARCHIVE, content.getType()); + Assert.assertEquals(1455710632607L, content.getLastModified()); + Assert.assertEquals(1727656L, content.getSize()); + Assert.assertEquals(DownloadContent.STATE_UPDATED, content.getState()); + } + + /** + * Scenario: Catalog contains one item and the server returns that it has been deleted. + */ + @Test + public void testDeletingExistingContent() throws Exception { + SyncAction action = spy(new SyncAction()); + doReturn(true).when(action).isSyncEnabledForClient(RuntimeEnvironment.application); + doReturn(fromFile("dlc_sync_deleted_item.json")).when(action).fetchRawCatalog(anyLong()); + + final String id = "c906275c-3747-fe27-426f-6187526a6f06"; + DownloadContent existingContent = createTestContent(id); + DownloadContentCatalog catalog = spy(new MockedContentCatalog(existingContent)); + + action.perform(RuntimeEnvironment.application, catalog); + + // A content item has been deleted + verify(action).deleteContent(anyCatalog(), eq(id)); + + // No content item has been created or updated + verify(action, never()).createContent(anyCatalog(), anyJSONObject()); + verify(action, never()).updateContent(anyCatalog(), anyJSONObject(), anyContent()); + + // An item has been marked for deletion in the catalog + ArgumentCaptor<DownloadContent> captor = ArgumentCaptor.forClass(DownloadContent.class); + verify(catalog).markAsDeleted(captor.capture()); + + DownloadContent content = captor.getValue(); + Assert.assertEquals(id, content.getId()); + + List<DownloadContent> contentToDelete = catalog.getContentToDelete(); + Assert.assertEquals(1, contentToDelete.size()); + Assert.assertEquals(id, contentToDelete.get(0).getId()); + } + + /** + * Create a DownloadContent object with arbitrary data. + */ + private DownloadContent createTestContent(String id) { + return new DownloadContentBuilder() + .setId(id) + .setLocation("/somewhere/something") + .setFilename("some.file") + .setChecksum("Some-checksum") + .setDownloadChecksum("Some-download-checksum") + .setLastModified(4223) + .setType("Some-type") + .setKind("Some-kind") + .setSize(27) + .setState(DownloadContent.STATE_SCHEDULED) + .build(); + } + + /** + * Create a Kinto response from a JSON file. + */ + private JSONArray fromFile(String fileName) throws IOException, JSONException { + URL url = getClass().getResource("/" + fileName); + if (url == null) { + throw new FileNotFoundException(fileName); + } + + InputStream inputStream = null; + ByteArrayOutputStream outputStream = null; + + try { + inputStream = new BufferedInputStream(new FileInputStream(url.getPath())); + outputStream = new ByteArrayOutputStream(); + + IOUtils.copy(inputStream, outputStream); + + JSONObject object = new JSONObject(outputStream.toString()); + + return object.getJSONArray("data"); + } finally { + IOUtils.safeStreamClose(inputStream); + IOUtils.safeStreamClose(outputStream); + } + } + + private static class MockedContentCatalog extends DownloadContentCatalog { + public MockedContentCatalog(DownloadContent content) { + super(mock(AtomicFile.class)); + + ArrayMap<String, DownloadContent> map = new ArrayMap<>(); + map.put(content.getId(), content); + + onCatalogLoaded(map); + } + } + + private DownloadContentCatalog mockCatalog() { + return mock(DownloadContentCatalog.class); + } + + private DownloadContentCatalog anyCatalog() { + return any(DownloadContentCatalog.class); + } + + private JSONObject anyJSONObject() { + return any(JSONObject.class); + } + + private DownloadContent anyContent() { + return any(DownloadContent.class); + } + + private Context anyContext() { + return any(Context.class); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/dlc/TestVerifyAction.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/dlc/TestVerifyAction.java new file mode 100644 index 000000000..6a347376e --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/dlc/TestVerifyAction.java @@ -0,0 +1,123 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.dlc; + +import android.content.Context; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.dlc.catalog.DownloadContent; +import org.mozilla.gecko.dlc.catalog.DownloadContentBuilder; +import org.mozilla.gecko.dlc.catalog.DownloadContentCatalog; +import org.robolectric.RuntimeEnvironment; + +import java.io.File; +import java.util.Collections; + +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * VerifyAction: Validate downloaded content. Does it still exist and does it have the correct checksum? + */ +@RunWith(TestRunner.class) +public class TestVerifyAction { + /** + * Scenario: Downloaded file does not exist anymore. + * + * Verify that: + * * Content is re-scheduled for download. + */ + @Test + public void testReschedulingIfFileDoesNotExist() throws Exception { + DownloadContent content = new DownloadContentBuilder().build(); + DownloadContentCatalog catalog = mock(DownloadContentCatalog.class); + when(catalog.getDownloadedContent()).thenReturn(Collections.singletonList(content)); + + File file = mock(File.class); + when(file.exists()).thenReturn(false); + + VerifyAction action = spy(new VerifyAction()); + doReturn(file).when(action).getDestinationFile(RuntimeEnvironment.application, content); + + action.perform(RuntimeEnvironment.application, catalog); + + verify(catalog).scheduleDownload(content); + } + + /** + * Scenario: Content has been scheduled for download. + * + * Verify that: + * * Download action is started + */ + @Test + public void testStartingDownloadsAfterScheduling() { + DownloadContentCatalog catalog = mock(DownloadContentCatalog.class); + when(catalog.hasScheduledDownloads()).thenReturn(true); + + VerifyAction action = spy(new VerifyAction()); + action.perform(RuntimeEnvironment.application, catalog); + + verify(action).startDownloads(any(Context.class)); + } + + /** + * Scenario: Checksum of existing file does not match expectation. + * + * Verify that: + * * Content is re-scheduled for download. + */ + @Test + public void testReschedulingIfVerificationFailed() throws Exception { + DownloadContent content = new DownloadContentBuilder().build(); + DownloadContentCatalog catalog = mock(DownloadContentCatalog.class); + when(catalog.getDownloadedContent()).thenReturn(Collections.singletonList(content)); + + File file = mock(File.class); + when(file.exists()).thenReturn(true); + + VerifyAction action = spy(new VerifyAction()); + doReturn(file).when(action).getDestinationFile(RuntimeEnvironment.application, content); + doReturn(false).when(action).verify(eq(file), anyString()); + + action.perform(RuntimeEnvironment.application, catalog); + + verify(catalog).scheduleDownload(content); + } + + /** + * Scenario: Downloaded file exists and has the correct checksum. + * + * Verify that: + * * No download is scheduled + * * Download action is not started + */ + @Test + public void testSuccessfulVerification() throws Exception { + DownloadContent content = new DownloadContentBuilder().build(); + DownloadContentCatalog catalog = mock(DownloadContentCatalog.class); + when(catalog.getDownloadedContent()).thenReturn(Collections.singletonList(content)); + + File file = mock(File.class); + when(file.exists()).thenReturn(true); + + VerifyAction action = spy(new VerifyAction()); + doReturn(file).when(action).getDestinationFile(RuntimeEnvironment.application, content); + doReturn(true).when(action).verify(eq(file), anyString()); + + verify(catalog, never()).scheduleDownload(content); + verify(action, never()).startDownloads(RuntimeEnvironment.application); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/dlc/catalog/TestDownloadContentBuilder.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/dlc/catalog/TestDownloadContentBuilder.java new file mode 100644 index 000000000..147b5da5b --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/dlc/catalog/TestDownloadContentBuilder.java @@ -0,0 +1,69 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +package org.mozilla.gecko.dlc.catalog; + +import org.json.JSONException; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.TestRunner; + +@RunWith(TestRunner.class) +public class TestDownloadContentBuilder { + /** + * Verify that the values passed to the builder are all set on the DownloadContent object. + */ + @Test + public void testBuilder() { + DownloadContent content = createTestContent(); + + Assert.assertEquals("Some-ID", content.getId()); + Assert.assertEquals("/somewhere/something", content.getLocation()); + Assert.assertEquals("some.file", content.getFilename()); + Assert.assertEquals("Some-checksum", content.getChecksum()); + Assert.assertEquals("Some-download-checksum", content.getDownloadChecksum()); + Assert.assertEquals(4223, content.getLastModified()); + Assert.assertEquals("Some-type", content.getType()); + Assert.assertEquals("Some-kind", content.getKind()); + Assert.assertEquals(27, content.getSize()); + Assert.assertEquals(DownloadContent.STATE_SCHEDULED, content.getState()); + } + + /** + * Verify that a DownloadContent object exported to JSON and re-imported from JSON does not change. + */ + public void testJSONSerializationAndDeserialization() throws JSONException { + DownloadContent content = DownloadContentBuilder.fromJSON(DownloadContentBuilder.toJSON(createTestContent())); + + Assert.assertEquals("Some-ID", content.getId()); + Assert.assertEquals("/somewhere/something", content.getLocation()); + Assert.assertEquals("some.file", content.getFilename()); + Assert.assertEquals("Some-checksum", content.getChecksum()); + Assert.assertEquals("Some-download-checksum", content.getDownloadChecksum()); + Assert.assertEquals(4223, content.getLastModified()); + Assert.assertEquals("Some-type", content.getType()); + Assert.assertEquals("Some-kind", content.getKind()); + Assert.assertEquals(27, content.getSize()); + Assert.assertEquals(DownloadContent.STATE_SCHEDULED, content.getState()); + } + + /** + * Create a DownloadContent object with arbitrary data. + */ + private DownloadContent createTestContent() { + return new DownloadContentBuilder() + .setId("Some-ID") + .setLocation("/somewhere/something") + .setFilename("some.file") + .setChecksum("Some-checksum") + .setDownloadChecksum("Some-download-checksum") + .setLastModified(4223) + .setType("Some-type") + .setKind("Some-kind") + .setSize(27) + .setState(DownloadContent.STATE_SCHEDULED) + .build(); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/dlc/catalog/TestDownloadContentCatalog.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/dlc/catalog/TestDownloadContentCatalog.java new file mode 100644 index 000000000..5b5912cdd --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/dlc/catalog/TestDownloadContentCatalog.java @@ -0,0 +1,262 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.dlc.catalog; + +import android.support.v4.util.ArrayMap; +import android.support.v4.util.AtomicFile; + +import org.junit.Assert; +import org.junit.Assume; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.AppConstants; +import org.mozilla.gecko.background.testhelpers.TestRunner; + +import java.io.FileNotFoundException; +import java.io.FileOutputStream; + +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +@RunWith(TestRunner.class) +public class TestDownloadContentCatalog { + /** + * Scenario: Create a new, fresh catalog. + * + * Verify that: + * * Catalog has not changed + * * Unchanged catalog will not be saved to disk + */ + @Test + public void testUntouchedCatalogHasNotChangedAndWillNotBePersisted() throws Exception { + AtomicFile file = mock(AtomicFile.class); + doReturn("{content:[]}".getBytes("UTF-8")).when(file).readFully(); + + DownloadContentCatalog catalog = spy(new DownloadContentCatalog(file)); + catalog.loadFromDisk(); + + Assert.assertFalse("Catalog has not changed", catalog.hasCatalogChanged()); + + catalog.writeToDisk(); + + Assert.assertFalse("Catalog has not changed", catalog.hasCatalogChanged()); + + verify(file, never()).startWrite(); + } + + /** + * Scenario: Create a new, fresh catalog. + * + * Verify that: + * * Catalog is bootstrapped with items. + */ + @Test + public void testCatalogIsBootstrappedIfFileDoesNotExist() throws Exception { + // The catalog is only bootstrapped if fonts are excluded from the build. If this is a build + // with fonts included then ignore this test. + Assume.assumeTrue("Fonts are excluded from build", AppConstants.MOZ_ANDROID_EXCLUDE_FONTS); + + AtomicFile file = mock(AtomicFile.class); + doThrow(FileNotFoundException.class).when(file).readFully(); + + DownloadContentCatalog catalog = spy(new DownloadContentCatalog(file)); + catalog.loadFromDisk(); + + Assert.assertTrue("Catalog is not empty", catalog.getContentToStudy().size() > 0); + } + + /** + * Scenario: Schedule downloading an item from the catalog. + * + * Verify that: + * * Catalog has changed + */ + @Test + public void testCatalogHasChangedWhenDownloadIsScheduled() throws Exception { + DownloadContentCatalog catalog = spy(new DownloadContentCatalog(mock(AtomicFile.class))); + DownloadContent content = new DownloadContentBuilder().build(); + catalog.onCatalogLoaded(createMapOfContent(content)); + + Assert.assertFalse("Catalog has not changed", catalog.hasCatalogChanged()); + + catalog.scheduleDownload(content); + + Assert.assertTrue("Catalog has changed", catalog.hasCatalogChanged()); + } + + /** + * Scenario: Mark an item in the catalog as downloaded. + * + * Verify that: + * * Catalog has changed + */ + @Test + public void testCatalogHasChangedWhenContentIsDownloaded() throws Exception { + DownloadContentCatalog catalog = spy(new DownloadContentCatalog(mock(AtomicFile.class))); + DownloadContent content = new DownloadContentBuilder().build(); + catalog.onCatalogLoaded(createMapOfContent(content)); + + Assert.assertFalse("Catalog has not changed", catalog.hasCatalogChanged()); + + catalog.markAsDownloaded(content); + + Assert.assertTrue("Catalog has changed", catalog.hasCatalogChanged()); + } + + /** + * Scenario: Mark an item in the catalog as permanently failed. + * + * Verify that: + * * Catalog has changed + */ + @Test + public void testCatalogHasChangedIfDownloadHasFailedPermanently() throws Exception { + DownloadContentCatalog catalog = spy(new DownloadContentCatalog(mock(AtomicFile.class))); + DownloadContent content = new DownloadContentBuilder().build(); + catalog.onCatalogLoaded(createMapOfContent(content)); + + Assert.assertFalse("Catalog has not changed", catalog.hasCatalogChanged()); + + catalog.markAsPermanentlyFailed(content); + + Assert.assertTrue("Catalog has changed", catalog.hasCatalogChanged()); + } + + /** + * Scenario: A changed catalog is written to disk. + * + * Verify that: + * * Before write: Catalog has changed + * * After write: Catalog has not changed. + */ + @Test + public void testCatalogHasNotChangedAfterWritingToDisk() throws Exception { + AtomicFile file = mock(AtomicFile.class); + doReturn(mock(FileOutputStream.class)).when(file).startWrite(); + + DownloadContentCatalog catalog = spy(new DownloadContentCatalog(file)); + DownloadContent content = new DownloadContentBuilder().build(); + catalog.onCatalogLoaded(createMapOfContent(content)); + + catalog.scheduleDownload(content); + + Assert.assertTrue("Catalog has changed", catalog.hasCatalogChanged()); + + catalog.writeToDisk(); + + Assert.assertFalse("Catalog has not changed", catalog.hasCatalogChanged()); + } + + /** + * Scenario: A catalog with multiple items in different states. + * + * Verify that: + * * getContentWithoutState(), getDownloadedContent() and getScheduledDownloads() returns + * the correct items depenending on their state. + */ + @Test + public void testContentClassification() { + DownloadContentCatalog catalog = spy(new DownloadContentCatalog(mock(AtomicFile.class))); + + DownloadContent content1 = new DownloadContentBuilder().setId("A").setState(DownloadContent.STATE_NONE).build(); + DownloadContent content2 = new DownloadContentBuilder().setId("B").setState(DownloadContent.STATE_NONE).build(); + DownloadContent content3 = new DownloadContentBuilder().setId("C").setState(DownloadContent.STATE_SCHEDULED).build(); + DownloadContent content4 = new DownloadContentBuilder().setId("D").setState(DownloadContent.STATE_SCHEDULED).build(); + DownloadContent content5 = new DownloadContentBuilder().setId("E").setState(DownloadContent.STATE_SCHEDULED).build(); + DownloadContent content6 = new DownloadContentBuilder().setId("F").setState(DownloadContent.STATE_DOWNLOADED).build(); + DownloadContent content7 = new DownloadContentBuilder().setId("G").setState(DownloadContent.STATE_FAILED).build(); + DownloadContent content8 = new DownloadContentBuilder().setId("H").setState(DownloadContent.STATE_UPDATED).build(); + DownloadContent content9 = new DownloadContentBuilder().setId("I").setState(DownloadContent.STATE_DELETED).build(); + DownloadContent content10 = new DownloadContentBuilder().setId("J").setState(DownloadContent.STATE_DELETED).build(); + + catalog.onCatalogLoaded(createMapOfContent(content1, content2, content3, content4, content5, content6, + content7, content8, content9, content10)); + + Assert.assertTrue(catalog.hasScheduledDownloads()); + + Assert.assertEquals(3, catalog.getContentToStudy().size()); + Assert.assertEquals(1, catalog.getDownloadedContent().size()); + Assert.assertEquals(3, catalog.getScheduledDownloads().size()); + Assert.assertEquals(2, catalog.getContentToDelete().size()); + + Assert.assertTrue(catalog.getContentToStudy().contains(content1)); + Assert.assertTrue(catalog.getContentToStudy().contains(content2)); + Assert.assertTrue(catalog.getContentToStudy().contains(content8)); + + Assert.assertTrue(catalog.getDownloadedContent().contains(content6)); + + Assert.assertTrue(catalog.getScheduledDownloads().contains(content3)); + Assert.assertTrue(catalog.getScheduledDownloads().contains(content4)); + Assert.assertTrue(catalog.getScheduledDownloads().contains(content5)); + + Assert.assertTrue(catalog.getContentToDelete().contains(content9)); + Assert.assertTrue(catalog.getContentToDelete().contains(content10)); + } + + /** + * Scenario: Calling rememberFailure() on a catalog with varying values + */ + @Test + public void testRememberingFailures() { + DownloadContentCatalog catalog = new DownloadContentCatalog(mock(AtomicFile.class)); + Assert.assertFalse(catalog.hasCatalogChanged()); + + DownloadContent content = new DownloadContentBuilder().build(); + Assert.assertEquals(0, content.getFailures()); + + catalog.rememberFailure(content, 42); + Assert.assertEquals(1, content.getFailures()); + Assert.assertTrue(catalog.hasCatalogChanged()); + + catalog.rememberFailure(content, 42); + Assert.assertEquals(2, content.getFailures()); + + // Failure counter is reset if different failure has been reported + catalog.rememberFailure(content, 23); + Assert.assertEquals(1, content.getFailures()); + + // Failure counter is reset after successful download + catalog.markAsDownloaded(content); + Assert.assertEquals(0, content.getFailures()); + } + + /** + * Scenario: Content has failed multiple times with the same failure type. + * + * Verify that: + * * Content is marked as permanently failed + */ + @Test + public void testContentWillBeMarkedAsPermanentlyFailedAfterMultipleFailures() { + DownloadContentCatalog catalog = new DownloadContentCatalog(mock(AtomicFile.class)); + + DownloadContent content = new DownloadContentBuilder().build(); + Assert.assertEquals(DownloadContent.STATE_NONE, content.getState()); + + for (int i = 0; i < 10; i++) { + catalog.rememberFailure(content, 42); + + Assert.assertEquals(i + 1, content.getFailures()); + Assert.assertEquals(DownloadContent.STATE_NONE, content.getState()); + } + + catalog.rememberFailure(content, 42); + Assert.assertEquals(10, content.getFailures()); + Assert.assertEquals(DownloadContent.STATE_FAILED, content.getState()); + } + + private ArrayMap<String, DownloadContent> createMapOfContent(DownloadContent... content) { + ArrayMap<String, DownloadContent> map = new ArrayMap<>(); + for (DownloadContent currentContent : content) { + map.put(currentContent.getId(), currentContent); + } + return map; + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/feeds/knownsites/TestKnownSiteBlogger.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/feeds/knownsites/TestKnownSiteBlogger.java new file mode 100644 index 000000000..628b572ce --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/feeds/knownsites/TestKnownSiteBlogger.java @@ -0,0 +1,74 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.feeds.knownsites; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.helpers.AssertUtil; + +@RunWith(TestRunner.class) +public class TestKnownSiteBlogger { + /** + * Test that the search string is a substring of some known URLs. + */ + @Test + public void testURLSearchString() { + final KnownSite blogger = new KnownSiteBlogger(); + final String searchString = blogger.getURLSearchString(); + + AssertUtil.assertContains( + "http://mykzilla.blogspot.com/", + searchString); + + AssertUtil.assertContains( + "http://example.blogspot.com", + searchString); + + AssertUtil.assertContains( + "https://mykzilla.blogspot.com/2015/06/introducing-pluotsorbet.html", + searchString); + + AssertUtil.assertContains( + "http://android-developers.blogspot.com/2016/02/android-support-library-232.html", + searchString); + + AssertUtil.assertContainsNot( + "http://www.mozilla.org", + searchString); + } + + /** + * Test that we get a feed URL for valid Blogger URLs. + */ + @Test + public void testGettingFeedFromURL() { + final KnownSite blogger = new KnownSiteBlogger(); + + Assert.assertEquals( + "https://mykzilla.blogspot.com/feeds/posts/default", + blogger.getFeedFromURL("http://mykzilla.blogspot.com/")); + + Assert.assertEquals( + "https://example.blogspot.com/feeds/posts/default", + blogger.getFeedFromURL("http://example.blogspot.com")); + + Assert.assertEquals( + "https://mykzilla.blogspot.com/feeds/posts/default", + blogger.getFeedFromURL("https://mykzilla.blogspot.com/2015/06/introducing-pluotsorbet.html")); + + Assert.assertEquals( + "https://android-developers.blogspot.com/feeds/posts/default", + blogger.getFeedFromURL("http://android-developers.blogspot.com/2016/02/android-support-library-232.html")); + + Assert.assertEquals( + "https://example.blogspot.com/feeds/posts/default", + blogger.getFeedFromURL("http://example.blogspot.com/2016/03/i-moved-to-example.blogspot.com")); + + Assert.assertNull(blogger.getFeedFromURL("http://www.mozilla.org")); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/feeds/knownsites/TestKnownSiteMedium.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/feeds/knownsites/TestKnownSiteMedium.java new file mode 100644 index 000000000..77f05e0d0 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/feeds/knownsites/TestKnownSiteMedium.java @@ -0,0 +1,66 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.feeds.knownsites; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.helpers.AssertUtil; + +@RunWith(TestRunner.class) +public class TestKnownSiteMedium { + /** + * Test that the search string is a substring of some known URLs. + */ + @Test + public void testURLSearchString() { + final KnownSite medium = new KnownSiteMedium(); + final String searchString = medium.getURLSearchString(); + + AssertUtil.assertContains( + "https://medium.com/@Antlam/", + searchString); + + AssertUtil.assertContains( + "https://medium.com/google-developers", + searchString); + + AssertUtil.assertContains( + "http://medium.com/@brandonshin/how-slackbot-forced-us-to-workout-7b4741a2de73", + searchString + ); + + AssertUtil.assertContainsNot( + "http://www.mozilla.org", + searchString); + } + + /** + * Test that we get a feed URL for valid Medium URLs. + */ + @Test + public void testGettingFeedFromURL() { + final KnownSite medium = new KnownSiteMedium(); + + Assert.assertEquals( + "https://medium.com/feed/@Antlam", + medium.getFeedFromURL("https://medium.com/@Antlam/") + ); + + Assert.assertEquals( + "https://medium.com/feed/google-developers", + medium.getFeedFromURL("https://medium.com/google-developers") + ); + + Assert.assertEquals( + "https://medium.com/feed/@brandonshin", + medium.getFeedFromURL("http://medium.com/@brandonshin/how-slackbot-forced-us-to-workout-7b4741a2de73") + ); + + Assert.assertNull(medium.getFeedFromURL("http://www.mozilla.org")); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/feeds/knownsites/TestKnownSiteTumblr.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/feeds/knownsites/TestKnownSiteTumblr.java new file mode 100644 index 000000000..f83272f82 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/feeds/knownsites/TestKnownSiteTumblr.java @@ -0,0 +1,62 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.feeds.knownsites; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.helpers.AssertUtil; + +@RunWith(TestRunner.class) +public class TestKnownSiteTumblr { + /** + * Test that the search string is a substring of some known URLs. + */ + @Test + public void testURLSearchString() { + final KnownSite tumblr = new KnownSiteTumblr(); + final String searchString = tumblr.getURLSearchString(); + + AssertUtil.assertContains( + "http://contentnotifications.tumblr.com/", + searchString); + + AssertUtil.assertContains( + "https://contentnotifications.tumblr.com", + searchString); + + AssertUtil.assertContains( + "http://contentnotifications.tumblr.com/post/142684202402/content-notification-firefox-for-android-480", + searchString); + + AssertUtil.assertContainsNot( + "http://www.mozilla.org", + searchString); + } + + /** + * Test that we get a feed URL for valid Medium URLs. + */ + @Test + public void testGettingFeedFromURL() { + final KnownSite tumblr = new KnownSiteTumblr(); + + Assert.assertEquals( + "http://contentnotifications.tumblr.com/rss", + tumblr.getFeedFromURL("http://contentnotifications.tumblr.com/") + ); + + Assert.assertEquals( + "http://staff.tumblr.com/rss", + tumblr.getFeedFromURL("https://staff.tumblr.com/post/141928246566/replies-are-back-and-the-sun-is-shining-on-the") + ); + + Assert.assertNull(tumblr.getFeedFromURL("https://www.tumblr.com")); + + Assert.assertNull(tumblr.getFeedFromURL("http://www.mozilla.org")); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/feeds/parser/TestSimpleFeedParser.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/feeds/parser/TestSimpleFeedParser.java new file mode 100644 index 000000000..fa2fffbad --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/feeds/parser/TestSimpleFeedParser.java @@ -0,0 +1,323 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.feeds.parser; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.TestRunner; + +import java.io.BufferedInputStream; +import java.io.BufferedReader; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.UnsupportedEncodingException; +import java.net.URISyntaxException; +import java.net.URL; +import java.text.SimpleDateFormat; +import java.util.Locale; + +@RunWith(TestRunner.class) +public class TestSimpleFeedParser { + /** + * Parse and verify the RSS example from Wikipedia: + * https://en.wikipedia.org/wiki/RSS#Example + */ + @Test + public void testRSSExample() throws Exception { + InputStream stream = openFeed("feed_rss_wikipedia.xml"); + + SimpleFeedParser parser = new SimpleFeedParser(); + Feed feed = parser.parse(stream); + + Assert.assertNotNull(feed); + Assert.assertEquals("RSS Title", feed.getTitle()); + Assert.assertEquals("http://www.example.com/main.html", feed.getWebsiteURL()); + Assert.assertNull(feed.getFeedURL()); + Assert.assertTrue(feed.isSufficientlyComplete()); + + Item item = feed.getLastItem(); + + Assert.assertNotNull(item); + Assert.assertEquals("Example entry", item.getTitle()); + Assert.assertEquals("http://www.example.com/blog/post/1", item.getURL()); + Assert.assertEquals(1252254000000L, item.getTimestamp()); + } + + /** + * Parse and verify the ATOM example from Wikipedia: + * https://en.wikipedia.org/wiki/Atom_%28standard%29#Example_of_an_Atom_1.0_feed + */ + @Test + public void testATOMExample() throws Exception { + InputStream stream = openFeed("feed_atom_wikipedia.xml"); + + SimpleFeedParser parser = new SimpleFeedParser(); + Feed feed = parser.parse(stream); + + Assert.assertNotNull(feed); + Assert.assertEquals("Example Feed", feed.getTitle()); + Assert.assertEquals("http://example.org/", feed.getWebsiteURL()); + Assert.assertEquals("http://example.org/feed/", feed.getFeedURL()); + Assert.assertTrue(feed.isSufficientlyComplete()); + + Item item = feed.getLastItem(); + + Assert.assertNotNull(item); + Assert.assertEquals("Atom-Powered Robots Run Amok", item.getTitle()); + Assert.assertEquals("http://example.org/2003/12/13/atom03.html", item.getURL()); + Assert.assertEquals(1071340202000L, item.getTimestamp()); + } + + /** + * Parse and verify a snapshot of a Medium feed. + */ + @Test + public void testMediumFeed() throws Exception { + InputStream stream = openFeed("feed_rss_medium.xml"); + + SimpleFeedParser parser = new SimpleFeedParser(); + Feed feed = parser.parse(stream); + + Assert.assertNotNull(feed); + Assert.assertEquals("Anthony Lam on Medium", feed.getTitle()); + Assert.assertEquals("https://medium.com/@antlam?source=rss-59f49b9e4b19------2", feed.getWebsiteURL()); + Assert.assertEquals("https://medium.com/feed/@antlam", feed.getFeedURL()); + Assert.assertTrue(feed.isSufficientlyComplete()); + + Item item = feed.getLastItem(); + + Assert.assertNotNull(item); + Assert.assertEquals("UX thoughts for 2016", item.getTitle()); + Assert.assertEquals("https://medium.com/@antlam/ux-thoughts-for-2016-1fc1d6e515e8?source=rss-59f49b9e4b19------2", item.getURL()); + Assert.assertEquals(1452537838000L, item.getTimestamp()); + } + + /** + * Parse and verify a snapshot of planet.mozilla.org ATOM feed. + */ + @Test + public void testPlanetMozillaATOMFeed() throws Exception { + InputStream stream = openFeed("feed_atom_planetmozilla.xml"); + + SimpleFeedParser parser = new SimpleFeedParser(); + Feed feed = parser.parse(stream); + + Assert.assertNotNull(feed); + Assert.assertEquals("Planet Mozilla", feed.getTitle()); + Assert.assertEquals("http://planet.mozilla.org/", feed.getWebsiteURL()); + Assert.assertEquals("http://planet.mozilla.org/atom.xml", feed.getFeedURL()); + Assert.assertTrue(feed.isSufficientlyComplete()); + + Item item = feed.getLastItem(); + + Assert.assertNotNull(item); + Assert.assertEquals("Firefox 45.0 Beta 3 Testday, February 5th", item.getTitle()); + Assert.assertEquals("https://quality.mozilla.org/2016/01/firefox-45-0-beta-3-testday-february-5th/", item.getURL()); + Assert.assertEquals(1453819255000L, item.getTimestamp()); + } + + /** + * Parse and verify a snapshot of planet.mozilla.org RSS 2.0 feed. + */ + @Test + public void testPlanetMozillaRSS20Feed() throws Exception { + InputStream stream = openFeed("feed_rss20_planetmozilla.xml"); + + SimpleFeedParser parser = new SimpleFeedParser(); + Feed feed = parser.parse(stream); + + Assert.assertNotNull(feed); + Assert.assertEquals("Planet Mozilla", feed.getTitle()); + Assert.assertEquals("http://planet.mozilla.org/", feed.getWebsiteURL()); + Assert.assertEquals("http://planet.mozilla.org/rss20.xml", feed.getFeedURL()); + Assert.assertTrue(feed.isSufficientlyComplete()); + + Item item = feed.getLastItem(); + + Assert.assertNotNull(item); + Assert.assertEquals("Aaron Klotz: Announcing Mozdbgext", item.getTitle()); + Assert.assertEquals("http://dblohm7.ca/blog/2016/01/26/announcing-mozdbgext/", item.getURL()); + Assert.assertEquals(1453837500000L, item.getTimestamp()); + } + + /** + * Parse and verify a snapshot of planet.mozilla.org RSS 1.0 feed. + */ + @Test + public void testPlanetMozillaRSS10Feed() throws Exception { + InputStream stream = openFeed("feed_rss10_planetmozilla.xml"); + + SimpleFeedParser parser = new SimpleFeedParser(); + Feed feed = parser.parse(stream); + + Assert.assertNotNull(feed); + Assert.assertEquals("Planet Mozilla", feed.getTitle()); + Assert.assertEquals("http://planet.mozilla.org/", feed.getWebsiteURL()); + Assert.assertEquals("http://planet.mozilla.org/rss10.xml", feed.getFeedURL()); + Assert.assertTrue(feed.isSufficientlyComplete()); + + Item item = feed.getLastItem(); + + Assert.assertNotNull(item); + Assert.assertEquals("Aaron Klotz: Announcing Mozdbgext", item.getTitle()); + Assert.assertEquals("http://dblohm7.ca/blog/2016/01/26/announcing-mozdbgext/", item.getURL()); + Assert.assertEquals(1453837500000L, item.getTimestamp()); + } + + /** + * Parse an verify a snapshot of a feedburner ATOM feed. + */ + @Test + public void testFeedburnerAtomFeed() throws Exception { + InputStream stream = openFeed("feed_atom_feedburner.xml"); + + SimpleFeedParser parser = new SimpleFeedParser(); + Feed feed = parser.parse(stream); + + Assert.assertNotNull(feed); + Assert.assertEquals("Android Zeitgeist", feed.getTitle()); + Assert.assertEquals("http://www.androidzeitgeist.com/", feed.getWebsiteURL()); + Assert.assertEquals("http://feeds.feedburner.com/AndroidZeitgeist", feed.getFeedURL()); + Assert.assertTrue(feed.isSufficientlyComplete()); + + Item item = feed.getLastItem(); + + Assert.assertNotNull(item); + Assert.assertEquals("Support for restricted profiles in Firefox 42", item.getTitle()); + Assert.assertEquals("http://feedproxy.google.com/~r/AndroidZeitgeist/~3/xaSicfGuwOU/support-restricted-profiles-firefox.html", item.getURL()); + Assert.assertEquals(1442511968239L, item.getTimestamp()); + } + + /** + * Parse and verify a snapshot of a Tumblr RSS feed. + */ + @Test + public void testTumblrRssFeed() throws Exception { + InputStream stream = openFeed("feed_rss_tumblr.xml"); + + SimpleFeedParser parser = new SimpleFeedParser(); + Feed feed = parser.parse(stream); + + Assert.assertNotNull(feed); + Assert.assertEquals("Tumblr Staff", feed.getTitle()); + Assert.assertEquals("http://staff.tumblr.com/", feed.getWebsiteURL()); + Assert.assertNull(feed.getFeedURL()); + Assert.assertTrue(feed.isSufficientlyComplete()); + + Item item = feed.getLastItem(); + + Assert.assertNotNull(item); + Assert.assertEquals("hardyboyscovers: Can Nancy Drew see things through and solve...", item.getTitle()); + Assert.assertEquals("http://staff.tumblr.com/post/138124026275", item.getURL()); + Assert.assertEquals(1453861812000L, item.getTimestamp()); + } + + /** + * Parse and verify a snapshot of a Spiegel (German news magazine) RSS feed. + */ + @Test + public void testSpiegelRssFeed() throws Exception { + InputStream stream = openFeed("feed_rss_spon.xml"); + + SimpleFeedParser parser = new SimpleFeedParser(); + Feed feed = parser.parse(stream); + + Assert.assertNotNull(feed); + Assert.assertEquals("SPIEGEL ONLINE - Schlagzeilen", feed.getTitle()); + Assert.assertEquals("http://www.spiegel.de", feed.getWebsiteURL()); + Assert.assertNull(feed.getFeedURL()); + Assert.assertTrue(feed.isSufficientlyComplete()); + + Item item = feed.getLastItem(); + + Assert.assertNotNull(item); + Assert.assertEquals("Angebliche Vergewaltigung einer 13-Jährigen: Steinmeier kanzelt russischen Minister Lawrow ab", item.getTitle()); + Assert.assertEquals("http://www.spiegel.de/politik/ausland/steinmeier-kanzelt-lawrow-ab-aerger-um-angebliche-vergewaltigung-a-1074292.html#ref=rss", item.getURL()); + Assert.assertEquals(1453914976000L, item.getTimestamp()); + } + + /** + * Parse and verify a snapshot of a Heise (German tech news) RSS feed. + */ + @Test + public void testHeiseRssFeed() throws Exception { + InputStream stream = openFeed("feed_rss_heise.xml"); + + SimpleFeedParser parser = new SimpleFeedParser(); + Feed feed = parser.parse(stream); + + Assert.assertNotNull(feed); + Assert.assertEquals("heise online News", feed.getTitle()); + Assert.assertEquals("http://www.heise.de/newsticker/", feed.getWebsiteURL()); + Assert.assertNull(feed.getFeedURL()); + Assert.assertTrue(feed.isSufficientlyComplete()); + + Item item = feed.getLastItem(); + + Assert.assertNotNull(item); + Assert.assertEquals("Google: “Dramatische Verbesserungen” für Chrome in iOS", item.getTitle()); + Assert.assertEquals("http://www.heise.de/newsticker/meldung/Google-Dramatische-Verbesserungen-fuer-Chrome-in-iOS-3085808.html?wt_mc=rss.ho.beitrag.atom", item.getURL()); + Assert.assertEquals(1453915920000L, item.getTimestamp()); + } + + @Test + public void testWordpressFeed() throws Exception { + InputStream stream = openFeed("feed_rss_wordpress.xml"); + + SimpleFeedParser parser = new SimpleFeedParser(); + Feed feed = parser.parse(stream); + + Assert.assertNotNull(feed); + Assert.assertEquals("justasimpletest2016", feed.getTitle()); + Assert.assertEquals("https://justasimpletest2016.wordpress.com", feed.getWebsiteURL()); + Assert.assertEquals("https://justasimpletest2016.wordpress.com/feed/", feed.getFeedURL()); + Assert.assertTrue(feed.isSufficientlyComplete()); + + Item item = feed.getLastItem(); + + Assert.assertNotNull(item); + Assert.assertEquals("Hello World!", item.getTitle()); + Assert.assertEquals("https://justasimpletest2016.wordpress.com/2016/02/26/hello-world/", item.getURL()); + Assert.assertEquals(1456524466000L, item.getTimestamp()); + } + + /** + * Parse and test a snapshot of mykzilla.blogspot.com + */ + @Test + public void testBloggerFeed() throws Exception { + InputStream stream = openFeed("feed_atom_blogger.xml"); + + SimpleFeedParser parser = new SimpleFeedParser(); + Feed feed = parser.parse(stream); + + Assert.assertNotNull(feed); + Assert.assertEquals("mykzilla", feed.getTitle()); + Assert.assertEquals("http://mykzilla.blogspot.com/", feed.getWebsiteURL()); + Assert.assertEquals("http://www.blogger.com/feeds/18929277/posts/default", feed.getFeedURL()); + Assert.assertTrue(feed.isSufficientlyComplete()); + + Item item = feed.getLastItem(); + + Assert.assertNotNull(item); + Assert.assertEquals("URL Has Been Changed", item.getTitle()); + Assert.assertEquals("http://mykzilla.blogspot.com/2016/01/url-has-been-changed.html", item.getURL()); + Assert.assertEquals(1452531451366L, item.getTimestamp()); + } + + private InputStream openFeed(String fileName) throws URISyntaxException, FileNotFoundException, UnsupportedEncodingException { + URL url = getClass().getResource("/" + fileName); + if (url == null) { + throw new FileNotFoundException(fileName); + } + + return new BufferedInputStream(new FileInputStream(url.getPath())); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/fxa/TestSkewHandler.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/fxa/TestSkewHandler.java new file mode 100644 index 000000000..2b4fe3e03 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/fxa/TestSkewHandler.java @@ -0,0 +1,70 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.fxa; + +import ch.boye.httpclientandroidlib.impl.cookie.DateUtils; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.fxa.SkewHandler; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.sync.net.BaseResource; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +@RunWith(TestRunner.class) +public class TestSkewHandler { + public TestSkewHandler() { + } + + @Test + public void testSkewUpdating() throws Throwable { + SkewHandler h = new SkewHandler("foo.com"); + assertEquals(0L, h.getSkewInSeconds()); + assertEquals(0L, h.getSkewInMillis()); + + long server = 1390101197865L; + long local = server - 4500L; + h.updateSkewFromServerMillis(server, local); + assertEquals(4500L, h.getSkewInMillis()); + assertEquals(4L, h.getSkewInSeconds()); + + local = server; + h.updateSkewFromServerMillis(server, local); + assertEquals(0L, h.getSkewInMillis()); + assertEquals(0L, h.getSkewInSeconds()); + + local = server + 500L; + h.updateSkewFromServerMillis(server, local); + assertEquals(-500L, h.getSkewInMillis()); + assertEquals(0L, h.getSkewInSeconds()); + + String date = "Sat, 18 Jan 2014 19:16:52 PST"; + long dateInMillis = 1390101412000L; // Obviously this can differ somewhat due to precision. + long parsed = DateUtils.parseDate(date).getTime(); + assertEquals(parsed, dateInMillis); + + h.updateSkewFromHTTPDateString(date, dateInMillis); + assertEquals(0L, h.getSkewInMillis()); + assertEquals(0L, h.getSkewInSeconds()); + + h.updateSkewFromHTTPDateString(date, dateInMillis + 1100L); + assertEquals(-1100L, h.getSkewInMillis()); + assertEquals(Math.round(-1100L / 1000L), h.getSkewInSeconds()); + } + + @Test + public void testSkewSingleton() throws Exception { + SkewHandler h1 = SkewHandler.getSkewHandlerFromEndpointString("http://foo.com/bar"); + SkewHandler h2 = SkewHandler.getSkewHandlerForHostname("foo.com"); + SkewHandler h3 = SkewHandler.getSkewHandlerForResource(new BaseResource("http://foo.com/baz")); + assertTrue(h1 == h2); + assertTrue(h1 == h3); + + SkewHandler.getSkewHandlerForHostname("foo.com").updateSkewFromServerMillis(1390101412000L, 1390001412000L); + final long actual = SkewHandler.getSkewHandlerForHostname("foo.com").getSkewInMillis(); + assertEquals(100000000L, actual); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/fxa/login/MockFxAccountClient.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/fxa/login/MockFxAccountClient.java new file mode 100644 index 000000000..868e90cd2 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/fxa/login/MockFxAccountClient.java @@ -0,0 +1,226 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.fxa.login; + +import android.text.TextUtils; + +import org.mozilla.gecko.background.fxa.FxAccountClient; +import org.mozilla.gecko.background.fxa.FxAccountClient20; +import org.mozilla.gecko.background.fxa.FxAccountClient20.AccountStatusResponse; +import org.mozilla.gecko.background.fxa.FxAccountClient20.RequestDelegate; +import org.mozilla.gecko.background.fxa.FxAccountClient20.RecoveryEmailStatusResponse; +import org.mozilla.gecko.background.fxa.FxAccountClient20.TwoKeys; +import org.mozilla.gecko.background.fxa.FxAccountClient20.LoginResponse; +import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException; +import org.mozilla.gecko.background.fxa.FxAccountRemoteError; +import org.mozilla.gecko.background.fxa.FxAccountUtils; +import org.mozilla.gecko.fxa.FxAccountDevice; +import org.mozilla.gecko.browserid.MockMyIDTokenFactory; +import org.mozilla.gecko.browserid.RSACryptoImplementation; +import org.mozilla.gecko.sync.ExtendedJSONObject; +import org.mozilla.gecko.sync.Utils; + +import java.io.UnsupportedEncodingException; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import ch.boye.httpclientandroidlib.HttpStatus; +import ch.boye.httpclientandroidlib.ProtocolVersion; +import ch.boye.httpclientandroidlib.entity.StringEntity; +import ch.boye.httpclientandroidlib.message.BasicHttpResponse; + +public class MockFxAccountClient implements FxAccountClient { + protected static MockMyIDTokenFactory mockMyIdTokenFactory = new MockMyIDTokenFactory(); + + public final String serverURI = "http://testServer.com"; + + public final Map<String, User> users = new HashMap<String, User>(); + public final Map<String, String> sessionTokens = new HashMap<String, String>(); + public final Map<String, String> keyFetchTokens = new HashMap<String, String>(); + + public static class User { + public final String email; + public final byte[] quickStretchedPW; + public final String uid; + public boolean verified; + public final byte[] kA; + public final byte[] wrapkB; + public final Map<String, FxAccountDevice> devices; + + public User(String email, byte[] quickStretchedPW) { + this.email = email; + this.quickStretchedPW = quickStretchedPW; + this.uid = "uid/" + this.email; + this.verified = false; + this.kA = Utils.generateRandomBytes(FxAccountUtils.CRYPTO_KEY_LENGTH_BYTES); + this.wrapkB = Utils.generateRandomBytes(FxAccountUtils.CRYPTO_KEY_LENGTH_BYTES); + this.devices = new HashMap<String, FxAccountDevice>(); + } + } + + protected LoginResponse addLogin(User user, byte[] sessionToken, byte[] keyFetchToken) { + // byte[] sessionToken = Utils.generateRandomBytes(8); + if (sessionToken != null) { + sessionTokens.put(Utils.byte2Hex(sessionToken), user.email); + } + // byte[] keyFetchToken = Utils.generateRandomBytes(8); + if (keyFetchToken != null) { + keyFetchTokens.put(Utils.byte2Hex(keyFetchToken), user.email); + } + return new LoginResponse(user.email, user.uid, user.verified, sessionToken, keyFetchToken); + } + + public void addUser(String email, byte[] quickStretchedPW, boolean verified, byte[] sessionToken, byte[] keyFetchToken) { + User user = new User(email, quickStretchedPW); + users.put(email, user); + if (verified) { + verifyUser(email); + } + addLogin(user, sessionToken, keyFetchToken); + } + + public void verifyUser(String email) { + users.get(email).verified = true; + } + + public void clearAllUserTokens() throws UnsupportedEncodingException { + sessionTokens.clear(); + keyFetchTokens.clear(); + } + + protected BasicHttpResponse makeHttpResponse(int statusCode, String body) { + BasicHttpResponse httpResponse = new BasicHttpResponse(new ProtocolVersion("HTTP", 1, 1), statusCode, body); + httpResponse.setEntity(new StringEntity(body, "UTF-8")); + return httpResponse; + } + + protected <T> void handleFailure(RequestDelegate<T> requestDelegate, int code, int errno, String message) { + requestDelegate.handleFailure(new FxAccountClientRemoteException(makeHttpResponse(code, message), + code, errno, "Bad authorization", message, null, new ExtendedJSONObject())); + } + + @Override + public void accountStatus(String uid, RequestDelegate<AccountStatusResponse> requestDelegate) { + boolean userFound = false; + for (User user : users.values()) { + if (user.uid.equals(uid)) { + userFound = true; + break; + } + } + requestDelegate.handleSuccess(new AccountStatusResponse(userFound)); + } + + @Override + public void recoveryEmailStatus(byte[] sessionToken, RequestDelegate<RecoveryEmailStatusResponse> requestDelegate) { + String email = sessionTokens.get(Utils.byte2Hex(sessionToken)); + User user = users.get(email); + if (email == null || user == null) { + handleFailure(requestDelegate, HttpStatus.SC_UNAUTHORIZED, FxAccountRemoteError.INVALID_AUTHENTICATION_TOKEN, "invalid sessionToken"); + return; + } + requestDelegate.handleSuccess(new RecoveryEmailStatusResponse(email, user.verified)); + } + + @Override + public void keys(byte[] keyFetchToken, RequestDelegate<TwoKeys> requestDelegate) { + String email = keyFetchTokens.get(Utils.byte2Hex(keyFetchToken)); + User user = users.get(email); + if (email == null || user == null) { + handleFailure(requestDelegate, HttpStatus.SC_UNAUTHORIZED, FxAccountRemoteError.INVALID_AUTHENTICATION_TOKEN, "invalid keyFetchToken"); + return; + } + if (!user.verified) { + handleFailure(requestDelegate, HttpStatus.SC_BAD_REQUEST, FxAccountRemoteError.ATTEMPT_TO_OPERATE_ON_AN_UNVERIFIED_ACCOUNT, "user is unverified"); + return; + } + requestDelegate.handleSuccess(new TwoKeys(user.kA, user.wrapkB)); + } + + @Override + public void sign(byte[] sessionToken, ExtendedJSONObject publicKey, long certificateDurationInMilliseconds, RequestDelegate<String> requestDelegate) { + String email = sessionTokens.get(Utils.byte2Hex(sessionToken)); + User user = users.get(email); + if (email == null || user == null) { + handleFailure(requestDelegate, HttpStatus.SC_UNAUTHORIZED, FxAccountRemoteError.INVALID_AUTHENTICATION_TOKEN, "invalid sessionToken"); + return; + } + if (!user.verified) { + handleFailure(requestDelegate, HttpStatus.SC_BAD_REQUEST, FxAccountRemoteError.ATTEMPT_TO_OPERATE_ON_AN_UNVERIFIED_ACCOUNT, "user is unverified"); + return; + } + try { + final long iat = System.currentTimeMillis(); + final long dur = certificateDurationInMilliseconds; + final long exp = iat + dur; + String certificate = mockMyIdTokenFactory.createMockMyIDCertificate(RSACryptoImplementation.createPublicKey(publicKey), "test", iat, exp); + requestDelegate.handleSuccess(certificate); + } catch (Exception e) { + requestDelegate.handleError(e); + } + } + + @Override + public void registerOrUpdateDevice(byte[] sessionToken, FxAccountDevice deviceToRegister, RequestDelegate<FxAccountDevice> requestDelegate) { + String email = sessionTokens.get(Utils.byte2Hex(sessionToken)); + User user = users.get(email); + if (email == null || user == null) { + handleFailure(requestDelegate, HttpStatus.SC_UNAUTHORIZED, FxAccountRemoteError.INVALID_AUTHENTICATION_TOKEN, "invalid sessionToken"); + return; + } + if (!user.verified) { + handleFailure(requestDelegate, HttpStatus.SC_BAD_REQUEST, FxAccountRemoteError.ATTEMPT_TO_OPERATE_ON_AN_UNVERIFIED_ACCOUNT, "user is unverified"); + return; + } + try { + String deviceId = deviceToRegister.id; + if (TextUtils.isEmpty(deviceId)) { // Create + deviceId = UUID.randomUUID().toString(); + FxAccountDevice device = new FxAccountDevice(deviceToRegister.name, deviceId, deviceToRegister.type, null, null, null, null); + requestDelegate.handleSuccess(device); + } else { // Update + FxAccountDevice existingDevice = user.devices.get(deviceId); + if (existingDevice != null) { + String deviceName = existingDevice.name; + if (!TextUtils.isEmpty(deviceToRegister.name)) { + deviceName = deviceToRegister.name; + } // We could also update the other fields.. + FxAccountDevice device = new FxAccountDevice(deviceName, existingDevice.id, existingDevice.type, + existingDevice.isCurrentDevice, existingDevice.pushCallback, existingDevice.pushPublicKey,existingDevice.pushAuthKey); + requestDelegate.handleSuccess(device); + } else { // Device unknown + handleFailure(requestDelegate, HttpStatus.SC_BAD_REQUEST, FxAccountRemoteError.UNKNOWN_DEVICE, "device is unknown"); + return; + } + } + } catch (Exception e) { + requestDelegate.handleError(e); + } + } + + @Override + public void deviceList(byte[] sessionToken, RequestDelegate<FxAccountDevice[]> requestDelegate) { + String email = sessionTokens.get(Utils.byte2Hex(sessionToken)); + User user = users.get(email); + if (email == null || user == null) { + handleFailure(requestDelegate, HttpStatus.SC_UNAUTHORIZED, FxAccountRemoteError.INVALID_AUTHENTICATION_TOKEN, "invalid sessionToken"); + return; + } + if (!user.verified) { + handleFailure(requestDelegate, HttpStatus.SC_BAD_REQUEST, FxAccountRemoteError.ATTEMPT_TO_OPERATE_ON_AN_UNVERIFIED_ACCOUNT, "user is unverified"); + return; + } + Collection<FxAccountDevice> devices = user.devices.values(); + FxAccountDevice[] devicesArray = devices.toArray(new FxAccountDevice[devices.size()]); + requestDelegate.handleSuccess(devicesArray); + } + + @Override + public void notifyDevices(byte[] sessionToken, List<String> deviceIds, ExtendedJSONObject payload, Long TTL, RequestDelegate<ExtendedJSONObject> requestDelegate) { + requestDelegate.handleSuccess(new ExtendedJSONObject()); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/fxa/login/TestFxAccountLoginStateMachine.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/fxa/login/TestFxAccountLoginStateMachine.java new file mode 100644 index 000000000..1496f6d79 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/fxa/login/TestFxAccountLoginStateMachine.java @@ -0,0 +1,205 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.fxa.login; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.fxa.FxAccountClient; +import org.mozilla.gecko.background.fxa.FxAccountUtils; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.background.testhelpers.WaitHelper; +import org.mozilla.gecko.browserid.BrowserIDKeyPair; +import org.mozilla.gecko.browserid.RSACryptoImplementation; +import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine.LoginStateMachineDelegate; +import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.Transition; +import org.mozilla.gecko.fxa.login.State.StateLabel; +import org.mozilla.gecko.sync.Utils; + +import java.security.NoSuchAlgorithmException; +import java.util.LinkedList; + +@RunWith(TestRunner.class) +public class TestFxAccountLoginStateMachine { + // private static final String TEST_AUDIENCE = "http://testAudience.com"; + private static final String TEST_EMAIL = "test@test.com"; + private static byte[] TEST_EMAIL_UTF8; + private static final String TEST_PASSWORD = "testtest"; + private static byte[] TEST_PASSWORD_UTF8; + private static byte[] TEST_QUICK_STRETCHED_PW; + private static byte[] TEST_UNWRAPKB; + private static final byte[] TEST_SESSION_TOKEN = Utils.generateRandomBytes(32); + private static final byte[] TEST_KEY_FETCH_TOKEN = Utils.generateRandomBytes(32); + + protected MockFxAccountClient client; + protected FxAccountLoginStateMachine sm; + + @Before + public void setUp() throws Exception { + if (TEST_EMAIL_UTF8 == null) { + TEST_EMAIL_UTF8 = TEST_EMAIL.getBytes("UTF-8"); + } + if (TEST_PASSWORD_UTF8 == null) { + TEST_PASSWORD_UTF8 = TEST_PASSWORD.getBytes("UTF-8"); + } + if (TEST_QUICK_STRETCHED_PW == null) { + TEST_QUICK_STRETCHED_PW = FxAccountUtils.generateQuickStretchedPW(TEST_EMAIL_UTF8, TEST_PASSWORD_UTF8); + } + if (TEST_UNWRAPKB == null) { + TEST_UNWRAPKB = FxAccountUtils.generateUnwrapBKey(TEST_QUICK_STRETCHED_PW); + } + client = new MockFxAccountClient(); + sm = new FxAccountLoginStateMachine(); + } + + protected static class Trace { + public final LinkedList<State> states; + public final LinkedList<Transition> transitions; + + public Trace(LinkedList<State> states, LinkedList<Transition> transitions) { + this.states = states; + this.transitions = transitions; + } + + public void assertEquals(String string) { + Assert.assertArrayEquals(string.split(", "), toString().split(", ")); + } + + @Override + public String toString() { + final LinkedList<State> states = new LinkedList<State>(this.states); + final LinkedList<Transition> transitions = new LinkedList<Transition>(this.transitions); + LinkedList<String> names = new LinkedList<String>(); + State state; + while ((state = states.pollFirst()) != null) { + names.add(state.getStateLabel().name()); + Transition transition = transitions.pollFirst(); + if (transition != null) { + names.add(">" + transition.toString()); + } + } + return names.toString(); + } + + public String stateString() { + LinkedList<String> names = new LinkedList<String>(); + for (State state : states) { + names.add(state.getStateLabel().name()); + } + return names.toString(); + } + + public String transitionString() { + LinkedList<String> names = new LinkedList<String>(); + for (Transition transition : transitions) { + names.add(transition.toString()); + } + return names.toString(); + } + } + + protected Trace trace(final State initialState, final StateLabel desiredState) { + final LinkedList<Transition> transitions = new LinkedList<Transition>(); + final LinkedList<State> states = new LinkedList<State>(); + states.add(initialState); + + WaitHelper.getTestWaiter().performWait(new Runnable() { + @Override + public void run() { + sm.advance(initialState, desiredState, new LoginStateMachineDelegate() { + @Override + public void handleTransition(Transition transition, State state) { + transitions.add(transition); + states.add(state); + } + + @Override + public void handleFinal(State state) { + WaitHelper.getTestWaiter().performNotify(); + } + + @Override + public FxAccountClient getClient() { + return client; + } + + @Override + public long getCertificateDurationInMilliseconds() { + return 30 * 1000; + } + + @Override + public long getAssertionDurationInMilliseconds() { + return 10 * 1000; + } + + @Override + public BrowserIDKeyPair generateKeyPair() throws NoSuchAlgorithmException { + return RSACryptoImplementation.generateKeyPair(512); + } + }); + } + }); + + return new Trace(states, transitions); + } + + @Test + public void testEnagedUnverified() throws Exception { + client.addUser(TEST_EMAIL, TEST_QUICK_STRETCHED_PW, false, TEST_SESSION_TOKEN, TEST_KEY_FETCH_TOKEN); + Trace trace = trace(new Engaged(TEST_EMAIL, "uid", true, TEST_UNWRAPKB, TEST_SESSION_TOKEN, TEST_KEY_FETCH_TOKEN), StateLabel.Married); + trace.assertEquals("[Engaged, >AccountNeedsVerification, Engaged]"); + } + + @Test + public void testEngagedTransitionToAccountVerified() throws Exception { + client.addUser(TEST_EMAIL, TEST_QUICK_STRETCHED_PW, true, TEST_SESSION_TOKEN, TEST_KEY_FETCH_TOKEN); + Trace trace = trace(new Engaged(TEST_EMAIL, "uid", false, TEST_UNWRAPKB, TEST_SESSION_TOKEN, TEST_KEY_FETCH_TOKEN), StateLabel.Married); + trace.assertEquals("[Engaged, >AccountVerified, Cohabiting, >LogMessage('sign succeeded'), Married]"); + } + + @Test + public void testEngagedVerified() throws Exception { + client.addUser(TEST_EMAIL, TEST_QUICK_STRETCHED_PW, true, TEST_SESSION_TOKEN, TEST_KEY_FETCH_TOKEN); + Trace trace = trace(new Engaged(TEST_EMAIL, "uid", true, TEST_UNWRAPKB, TEST_SESSION_TOKEN, TEST_KEY_FETCH_TOKEN), StateLabel.Married); + trace.assertEquals("[Engaged, >LogMessage('keys succeeded'), Cohabiting, >LogMessage('sign succeeded'), Married]"); + } + + @Test + public void testPartial() throws Exception { + client.addUser(TEST_EMAIL, TEST_QUICK_STRETCHED_PW, true, TEST_SESSION_TOKEN, TEST_KEY_FETCH_TOKEN); + // What if we stop at Cohabiting? + Trace trace = trace(new Engaged(TEST_EMAIL, "uid", true, TEST_UNWRAPKB, TEST_SESSION_TOKEN, TEST_KEY_FETCH_TOKEN), StateLabel.Cohabiting); + trace.assertEquals("[Engaged, >LogMessage('keys succeeded'), Cohabiting]"); + } + + @Test + public void testBadSessionToken() throws Exception { + client.addUser(TEST_EMAIL, TEST_QUICK_STRETCHED_PW, true, TEST_SESSION_TOKEN, TEST_KEY_FETCH_TOKEN); + client.sessionTokens.clear(); + Trace trace = trace(new Engaged(TEST_EMAIL, "uid", true, TEST_UNWRAPKB, TEST_SESSION_TOKEN, TEST_KEY_FETCH_TOKEN), StateLabel.Married); + trace.assertEquals("[Engaged, >LogMessage('keys succeeded'), Cohabiting, >Log(<FxAccountClientRemoteException 401 [110]: invalid sessionToken>), Separated, >PasswordRequired, Separated]"); + } + + @Test + public void testBadKeyFetchToken() throws Exception { + client.addUser(TEST_EMAIL, TEST_QUICK_STRETCHED_PW, true, TEST_SESSION_TOKEN, TEST_KEY_FETCH_TOKEN); + client.keyFetchTokens.clear(); + Trace trace = trace(new Engaged(TEST_EMAIL, "uid", true, TEST_UNWRAPKB, TEST_SESSION_TOKEN, TEST_KEY_FETCH_TOKEN), StateLabel.Married); + trace.assertEquals("[Engaged, >Log(<FxAccountClientRemoteException 401 [110]: invalid keyFetchToken>), Separated, >PasswordRequired, Separated]"); + } + + @Test + public void testMarried() throws Exception { + client.addUser(TEST_EMAIL, TEST_QUICK_STRETCHED_PW, true, TEST_SESSION_TOKEN, TEST_KEY_FETCH_TOKEN); + Trace trace = trace(new Engaged(TEST_EMAIL, "uid", true, TEST_UNWRAPKB, TEST_SESSION_TOKEN, TEST_KEY_FETCH_TOKEN), StateLabel.Married); + trace.assertEquals("[Engaged, >LogMessage('keys succeeded'), Cohabiting, >LogMessage('sign succeeded'), Married]"); + // What if we're already in the desired state? + State married = trace.states.getLast(); + Assert.assertEquals(StateLabel.Married, married.getStateLabel()); + trace = trace(married, StateLabel.Married); + trace.assertEquals("[Married]"); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/fxa/login/TestStateFactory.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/fxa/login/TestStateFactory.java new file mode 100644 index 000000000..80d7d7f9f --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/fxa/login/TestStateFactory.java @@ -0,0 +1,91 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.fxa.login; + +import junit.framework.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.browserid.BrowserIDKeyPair; +import org.mozilla.gecko.browserid.DSACryptoImplementation; +import org.mozilla.gecko.fxa.login.State.StateLabel; +import org.mozilla.gecko.sync.ExtendedJSONObject; +import org.mozilla.gecko.sync.Utils; + +@RunWith(TestRunner.class) +public class TestStateFactory { + @Test + public void testGetStateV3() throws Exception { + MigratedFromSync11 migrated = new MigratedFromSync11("email", "uid", true, "password"); + + // For the current version, we expect to read back what we wrote. + ExtendedJSONObject o; + State state; + + o = migrated.toJSONObject(); + Assert.assertEquals(3, o.getLong("version").intValue()); + state = StateFactory.fromJSONObject(migrated.stateLabel, o); + Assert.assertEquals(StateLabel.MigratedFromSync11, state.stateLabel); + Assert.assertEquals(o, state.toJSONObject()); + + // Null passwords are OK. + MigratedFromSync11 migratedNullPassword = new MigratedFromSync11("email", "uid", true, null); + + o = migratedNullPassword.toJSONObject(); + Assert.assertEquals(3, o.getLong("version").intValue()); + state = StateFactory.fromJSONObject(migratedNullPassword.stateLabel, o); + Assert.assertEquals(StateLabel.MigratedFromSync11, state.stateLabel); + Assert.assertEquals(o, state.toJSONObject()); + } + + @Test + public void testGetStateV2() throws Exception { + byte[] sessionToken = Utils.generateRandomBytes(32); + byte[] kA = Utils.generateRandomBytes(32); + byte[] kB = Utils.generateRandomBytes(32); + BrowserIDKeyPair keyPair = DSACryptoImplementation.generateKeyPair(512); + Cohabiting cohabiting = new Cohabiting("email", "uid", sessionToken, kA, kB, keyPair); + String certificate = "certificate"; + Married married = new Married("email", "uid", sessionToken, kA, kB, keyPair, certificate); + + // For the current version, we expect to read back what we wrote. + ExtendedJSONObject o; + State state; + + o = married.toJSONObject(); + Assert.assertEquals(3, o.getLong("version").intValue()); + state = StateFactory.fromJSONObject(married.stateLabel, o); + Assert.assertEquals(StateLabel.Married, state.stateLabel); + Assert.assertEquals(o, state.toJSONObject()); + + o = cohabiting.toJSONObject(); + Assert.assertEquals(3, o.getLong("version").intValue()); + state = StateFactory.fromJSONObject(cohabiting.stateLabel, o); + Assert.assertEquals(StateLabel.Cohabiting, state.stateLabel); + Assert.assertEquals(o, state.toJSONObject()); + } + + @Test + public void testGetStateV1() throws Exception { + // We can't rely on generating correct V1 objects (since the generation code + // may change); so we hard code a few test examples here. These examples + // have RSA key pairs; when they're parsed, we return DSA key pairs. + ExtendedJSONObject o = new ExtendedJSONObject("{\"uid\":\"uid\",\"sessionToken\":\"4e2830da6ce466ddb401fbca25b96a621209eea83851254800f84cc4069ef011\",\"certificate\":\"certificate\",\"keyPair\":{\"publicKey\":{\"e\":\"65537\",\"n\":\"7598360104379019497828904063491254083855849024432238665262988260947462372141971045236693389494635158997975098558915846889960089362159921622822266839560631\",\"algorithm\":\"RS\"},\"privateKey\":{\"d\":\"6807533330618101360064115400338014782301295929300445938471117364691566605775022173055292460962170873583673516346599808612503093914221141089102289381448225\",\"n\":\"7598360104379019497828904063491254083855849024432238665262988260947462372141971045236693389494635158997975098558915846889960089362159921622822266839560631\",\"algorithm\":\"RS\"}},\"email\":\"email\",\"verified\":true,\"kB\":\"0b048f285c19067f200da7bfbe734ed213cefcd8f543f0fdd4a8ccab48cbbc89\",\"kA\":\"59a9edf2d41de8b24e69df9133bc88e96913baa75421882f4c55d842d18fc8a1\",\"version\":1}"); + // A Married state is regressed to a Cohabited state. + Cohabiting state = (Cohabiting) StateFactory.fromJSONObject(StateLabel.Married, o); + + Assert.assertEquals(StateLabel.Cohabiting, state.stateLabel); + Assert.assertEquals("uid", state.uid); + Assert.assertEquals("4e2830da6ce466ddb401fbca25b96a621209eea83851254800f84cc4069ef011", Utils.byte2Hex(state.sessionToken)); + Assert.assertEquals("DS128", state.keyPair.getPrivate().getAlgorithm()); + + o = new ExtendedJSONObject("{\"uid\":\"uid\",\"sessionToken\":\"4e2830da6ce466ddb401fbca25b96a621209eea83851254800f84cc4069ef011\",\"keyPair\":{\"publicKey\":{\"e\":\"65537\",\"n\":\"7598360104379019497828904063491254083855849024432238665262988260947462372141971045236693389494635158997975098558915846889960089362159921622822266839560631\",\"algorithm\":\"RS\"},\"privateKey\":{\"d\":\"6807533330618101360064115400338014782301295929300445938471117364691566605775022173055292460962170873583673516346599808612503093914221141089102289381448225\",\"n\":\"7598360104379019497828904063491254083855849024432238665262988260947462372141971045236693389494635158997975098558915846889960089362159921622822266839560631\",\"algorithm\":\"RS\"}},\"email\":\"email\",\"verified\":true,\"kB\":\"0b048f285c19067f200da7bfbe734ed213cefcd8f543f0fdd4a8ccab48cbbc89\",\"kA\":\"59a9edf2d41de8b24e69df9133bc88e96913baa75421882f4c55d842d18fc8a1\",\"version\":1}"); + state = (Cohabiting) StateFactory.fromJSONObject(StateLabel.Cohabiting, o); + + Assert.assertEquals(StateLabel.Cohabiting, state.stateLabel); + Assert.assertEquals("uid", state.uid); + Assert.assertEquals("4e2830da6ce466ddb401fbca25b96a621209eea83851254800f84cc4069ef011", Utils.byte2Hex(state.sessionToken)); + Assert.assertEquals("DS128", state.keyPair.getPrivate().getAlgorithm()); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/helpers/AssertUtil.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/helpers/AssertUtil.java new file mode 100644 index 000000000..8102bf1ee --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/helpers/AssertUtil.java @@ -0,0 +1,29 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.helpers; + +import org.junit.Assert; + +/** + * Some additional assert methods on top of org.junit.Assert. + */ +public class AssertUtil { + /** + * Asserts that the String {@code text} contains the String {@code sequence}. If it doesn't then + * an {@link AssertionError} will be thrown. + */ + public static void assertContains(String text, String sequence) { + Assert.assertTrue(text.contains(sequence)); + } + + /** + * Asserts that the String {@code text} contains not the String {@code sequence}. If it does + * then an {@link AssertionError} will be thrown. + */ + public static void assertContainsNot(String text, String sequence) { + Assert.assertFalse(text.contains(sequence)); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/home/TestHomeConfigPrefsBackendMigration.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/home/TestHomeConfigPrefsBackendMigration.java new file mode 100644 index 000000000..b6f12a05e --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/home/TestHomeConfigPrefsBackendMigration.java @@ -0,0 +1,264 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +package org.mozilla.gecko.home; + +import android.content.Context; +import android.util.Pair; +import android.util.SparseArray; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.home.HomeConfig.PanelConfig; +import org.mozilla.gecko.home.HomeConfig.PanelType; +import org.robolectric.RuntimeEnvironment; + +import java.util.EnumSet; +import java.util.HashSet; +import java.util.Set; + +import static org.junit.Assert.*; + +@RunWith(TestRunner.class) +public class TestHomeConfigPrefsBackendMigration { + + // Each Pair consists of a list of panels that exist going into a given migration, and a list containing + // the expected default output panel corresponding to each given input panel in the list of existing panels. + // E.g. if a given N->N+1 migration starts with panels Foo and Bar, and removes Bar, the two lists would + // be {Foo, Bar} and {Foo, Foo}. + // Note: the index where each pair is inserted corresponds to the HomeConfig version before the migration. + // The final item in this list denotes the current HomeCOnfig version, and therefore only needs to contain + // the list of panel types that are expected by default (but no list for after the non-existent migration). + final SparseArray<Pair<PanelType[], PanelType[]>> migrationConstellations = new SparseArray<>(); + { + // 6->7: the recent tabs panel was merged into the combined history panel + migrationConstellations.put(6, new Pair<>( + /* Panels that are expected to exist before this migration happens */ + new PanelType[] { + PanelType.TOP_SITES, + PanelType.BOOKMARKS, + PanelType.COMBINED_HISTORY, + PanelType.DEPRECATED_RECENT_TABS + }, + /* The expected default panel that is expected after the migration */ + new PanelType[] { + PanelType.TOP_SITES, /* TOP_SITES remains the default if it was previously the default */ + PanelType.BOOKMARKS, /* same as TOP_SITES */ + PanelType.COMBINED_HISTORY, /* same as TOP_SITES */ + PanelType.COMBINED_HISTORY /* DEPRECATED_RECENT_TABS is replaced by COMBINED_HISTORY during this migration and is therefore the new default */ + } + )); + + // 7->8: no changes, this was a fixup migration since 6->7 was previously botched + migrationConstellations.put(7, new Pair<>( + new PanelType[] { + PanelType.TOP_SITES, + PanelType.BOOKMARKS, + PanelType.COMBINED_HISTORY, + }, + new PanelType[] { + PanelType.TOP_SITES, + PanelType.BOOKMARKS, + PanelType.COMBINED_HISTORY, + } + )); + + migrationConstellations.put(8, new Pair<>( + new PanelType[] { + PanelType.TOP_SITES, + PanelType.BOOKMARKS, + PanelType.COMBINED_HISTORY, + }, + new PanelType[] { + // Last version: no migration exists yet, we only need to define a list + // of expected panels. + } + )); + } + + private JSONArray createDisabledConfigsForList(Context context, + PanelType[] panels) throws JSONException { + final JSONArray jsonPanels = new JSONArray(); + + for (int i = 0; i < panels.length; i++) { + final PanelType panel = panels[i]; + + jsonPanels.put(HomeConfig.createBuiltinPanelConfig(context, panel, + EnumSet.of(PanelConfig.Flags.DISABLED_PANEL)).toJSON()); + } + + return jsonPanels; + + } + + + private JSONArray createConfigsForList(Context context, PanelType[] panels, + int defaultIndex) throws JSONException { + if (defaultIndex < 0 || defaultIndex >= panels.length) { + throw new IllegalArgumentException("defaultIndex must point to panel in the array"); + } + + final JSONArray jsonPanels = new JSONArray(); + + for (int i = 0; i < panels.length; i++) { + final PanelType panel = panels[i]; + final PanelConfig config; + + if (i == defaultIndex) { + config = HomeConfig.createBuiltinPanelConfig(context, panel, + EnumSet.of(PanelConfig.Flags.DEFAULT_PANEL)); + } else { + config = HomeConfig.createBuiltinPanelConfig(context, panel); + } + + jsonPanels.put(config.toJSON()); + } + + return jsonPanels; + } + + private PanelType getDefaultPanel(final JSONArray jsonPanels) throws JSONException { + assertTrue("panel list must not be empty", jsonPanels.length() > 0); + + for (int i = 0; i < jsonPanels.length(); i++) { + final JSONObject jsonPanelConfig = jsonPanels.getJSONObject(i); + final PanelConfig panelConfig = new PanelConfig(jsonPanelConfig); + + if (panelConfig.isDefault()) { + return panelConfig.getType(); + } + } + + return null; + } + + private void checkAllPanelsAreDisabled(JSONArray jsonPanels) throws JSONException { + for (int i = 0; i < jsonPanels.length(); i++) { + final JSONObject jsonPanelConfig = jsonPanels.getJSONObject(i); + final PanelConfig config = new PanelConfig(jsonPanelConfig); + + assertTrue("Non disabled panel \"" + config.getType().name() + "\" found in list, excpected all panels to be disabled", config.isDisabled()); + } + } + + private void checkListContainsExpectedPanels(JSONArray jsonPanels, + PanelType[] expected) throws JSONException { + // Given the short lists we have here an ArraySet might be more appropriate, but it requires API >= 23. + final Set<PanelType> expectedSet = new HashSet<>(); + for (PanelType panelType : expected) { + expectedSet.add(panelType); + } + + for (int i = 0; i < jsonPanels.length(); i++) { + final JSONObject jsonPanelConfig = jsonPanels.getJSONObject(i); + final PanelType panelType = new PanelConfig(jsonPanelConfig).getType(); + + assertTrue("Unexpected panel of type " + panelType.name() + " found in list", + expectedSet.contains(panelType)); + + expectedSet.remove(panelType); + } + + assertEquals("Expected panels not contained in list", + 0, expectedSet.size()); + } + + @Test + public void testMigrationRetainsDefaultAfter6() throws JSONException { + final Context context = RuntimeEnvironment.application; + + final Pair<PanelType[], PanelType[]> finalConstellation = migrationConstellations.get(HomeConfigPrefsBackend.VERSION); + assertNotNull("It looks like you added a HomeConfig migration, please add an appropriate entry to migrationConstellations", + finalConstellation); + + // We want to calculate the number of iterations here to make sure we cover all provided constellations. + // Iterating over the array and manually checking for each version could result in constellations + // being skipped if there are any gaps in the array + final int firstTestedVersion = HomeConfigPrefsBackend.VERSION - (migrationConstellations.size() - 1); + + // The last constellation is only used for the counts / expected outputs, hence we start + // with the second-last constellation + for (int testVersion = HomeConfigPrefsBackend.VERSION - 1; testVersion >= firstTestedVersion; testVersion--) { + + final Pair<PanelType[], PanelType[]> currentConstellation = migrationConstellations.get(testVersion); + assertNotNull("No constellation for version " + testVersion + " - you must provide a constellation for every version upgrade in the list", + currentConstellation); + + final PanelType[] inputList = currentConstellation.first; + final PanelType[] expectedDefaults = currentConstellation.second; + + for (int i = 0; i < inputList.length; i++) { + JSONArray jsonPanels = createConfigsForList(context, inputList, i); + + + // Verify that we still have a default panel, and that it is the expected default panel + + // No need to pass in the prefsEditor since that is only used for the 0->1 migration + jsonPanels = HomeConfigPrefsBackend.migratePrefsFromVersionToVersion(context, testVersion, testVersion + 1, jsonPanels, null); + + final PanelType oldDefaultPanelType = inputList[i]; + final PanelType expectedNewDefaultPanelType = expectedDefaults[i]; + final PanelType newDefaultPanelType = getDefaultPanel(jsonPanels); + + assertNotNull("No default panel set when migrating from " + testVersion + " to " + testVersion + 1 + ", with previous default as " + oldDefaultPanelType.name(), + newDefaultPanelType); + + assertEquals("Migration changed to unexpected default panel - migrating from " + oldDefaultPanelType.name() + ", expected " + expectedNewDefaultPanelType.name() + " but got " + newDefaultPanelType.name(), + newDefaultPanelType, expectedNewDefaultPanelType); + + + // Verify that the panels remaining after the migration correspond to the input panels + // for the next migration + final PanelType[] expectedOutputList = migrationConstellations.get(testVersion + 1).first; + + assertEquals("Number of panels after migration doesn't match expected count", + jsonPanels.length(), expectedOutputList.length); + + checkListContainsExpectedPanels(jsonPanels, expectedOutputList); + } + } + } + + // Test that if all panels are disabled, the migration retains all panels as being disabled + // (in addition to correctly removing panels as necessary). + @Test + public void testMigrationRetainsAllPanelsHiddenAfter6() throws JSONException { + final Context context = RuntimeEnvironment.application; + + final Pair<PanelType[], PanelType[]> finalConstellation = migrationConstellations.get(HomeConfigPrefsBackend.VERSION); + assertNotNull("It looks like you added a HomeConfig migration, please add an appropriate entry to migrationConstellations", + finalConstellation); + + final int firstTestedVersion = HomeConfigPrefsBackend.VERSION - (migrationConstellations.size() - 1); + + for (int testVersion = HomeConfigPrefsBackend.VERSION - 1; testVersion >= firstTestedVersion; testVersion--) { + final Pair<PanelType[], PanelType[]> currentConstellation = migrationConstellations.get(testVersion); + assertNotNull("No constellation for version " + testVersion + " - you must provide a constellation for every version upgrade in the list", + currentConstellation); + + final PanelType[] inputList = currentConstellation.first; + + JSONArray jsonPanels = createDisabledConfigsForList(context, inputList); + + jsonPanels = HomeConfigPrefsBackend.migratePrefsFromVersionToVersion(context, testVersion, testVersion + 1, jsonPanels, null); + + // All panels should remain disabled after the migration + checkAllPanelsAreDisabled(jsonPanels); + + // Duplicated from previous test: + // Verify that the panels remaining after the migration correspond to the input panels + // for the next migration + final PanelType[] expectedOutputList = migrationConstellations.get(testVersion + 1).first; + + assertEquals("Number of panels after migration doesn't match expected count", + jsonPanels.length(), expectedOutputList.length); + + checkListContainsExpectedPanels(jsonPanels, expectedOutputList); + } + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/TestIconDescriptor.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/TestIconDescriptor.java new file mode 100644 index 000000000..05e4576e5 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/TestIconDescriptor.java @@ -0,0 +1,56 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.icons; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.TestRunner; + +@RunWith(TestRunner.class) +public class TestIconDescriptor { + private static final String ICON_URL = "https://www.mozilla.org/favicon.ico"; + private static final String MIME_TYPE = "image/png"; + private static final int ICON_SIZE = 64; + + @Test + public void testGenericIconDescriptor() { + final IconDescriptor descriptor = IconDescriptor.createGenericIcon(ICON_URL); + + Assert.assertEquals(ICON_URL, descriptor.getUrl()); + Assert.assertNull(descriptor.getMimeType()); + Assert.assertEquals(0, descriptor.getSize()); + Assert.assertEquals(IconDescriptor.TYPE_GENERIC, descriptor.getType()); + } + + @Test + public void testFaviconIconDescriptor() { + final IconDescriptor descriptor = IconDescriptor.createFavicon(ICON_URL, ICON_SIZE, MIME_TYPE); + + Assert.assertEquals(ICON_URL, descriptor.getUrl()); + Assert.assertEquals(MIME_TYPE, descriptor.getMimeType()); + Assert.assertEquals(ICON_SIZE, descriptor.getSize()); + Assert.assertEquals(IconDescriptor.TYPE_FAVICON, descriptor.getType()); + } + + @Test + public void testTouchIconDescriptor() { + final IconDescriptor descriptor = IconDescriptor.createTouchicon(ICON_URL, ICON_SIZE, MIME_TYPE); + + Assert.assertEquals(ICON_URL, descriptor.getUrl()); + Assert.assertEquals(MIME_TYPE, descriptor.getMimeType()); + Assert.assertEquals(ICON_SIZE, descriptor.getSize()); + Assert.assertEquals(IconDescriptor.TYPE_TOUCHICON, descriptor.getType()); + } + + @Test + public void testLookupIconDescriptor() { + final IconDescriptor descriptor = IconDescriptor.createLookupIcon(ICON_URL); + + Assert.assertEquals(ICON_URL, descriptor.getUrl()); + Assert.assertNull(descriptor.getMimeType()); + Assert.assertEquals(0, descriptor.getSize()); + Assert.assertEquals(IconDescriptor.TYPE_LOOKUP, descriptor.getType()); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/TestIconDescriptorComparator.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/TestIconDescriptorComparator.java new file mode 100644 index 000000000..1f4664d08 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/TestIconDescriptorComparator.java @@ -0,0 +1,152 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.icons; + +import org.junit.Assert; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.TestRunner; + +import java.util.TreeSet; + +@RunWith(TestRunner.class) +public class TestIconDescriptorComparator { + private static final String TEST_ICON_URL_1 = "http://www.mozilla.org/favicon.ico"; + private static final String TEST_ICON_URL_2 = "http://www.example.org/favicon.ico"; + private static final String TEST_ICON_URL_3 = "http://www.example.com/favicon.ico"; + + private static final String TEST_MIME_TYPE = "image/png"; + private static final int TEST_SIZE = 32; + + @Test + public void testIconsWithTheSameUrlAreTreatedAsEqual() { + final IconDescriptor descriptor1 = IconDescriptor.createGenericIcon(TEST_ICON_URL_1); + final IconDescriptor descriptor2 = IconDescriptor.createGenericIcon(TEST_ICON_URL_1); + + final IconDescriptorComparator comparator = new IconDescriptorComparator(); + + Assert.assertEquals(0, comparator.compare(descriptor1, descriptor2)); + Assert.assertEquals(0, comparator.compare(descriptor2, descriptor1)); + } + + @Test + public void testTouchIconsAreRankedHigherThanFavicons() { + final IconDescriptor faviconDescriptor = IconDescriptor.createFavicon(TEST_ICON_URL_1, TEST_SIZE, TEST_MIME_TYPE); + final IconDescriptor touchIconDescriptor = IconDescriptor.createTouchicon(TEST_ICON_URL_2, TEST_SIZE, TEST_MIME_TYPE); + + final IconDescriptorComparator comparator = new IconDescriptorComparator(); + + Assert.assertEquals(1, comparator.compare(faviconDescriptor, touchIconDescriptor)); + Assert.assertEquals(-1, comparator.compare(touchIconDescriptor, faviconDescriptor)); + } + + @Test + public void testFaviconsAndTouchIconsAreRankedHigherThanGenericIcons() { + final IconDescriptor genericDescriptor = IconDescriptor.createGenericIcon(TEST_ICON_URL_1); + + final IconDescriptor faviconDescriptor = IconDescriptor.createFavicon(TEST_ICON_URL_2, TEST_SIZE, TEST_MIME_TYPE); + final IconDescriptor touchIconDescriptor = IconDescriptor.createTouchicon(TEST_ICON_URL_3, TEST_SIZE, TEST_MIME_TYPE); + + final IconDescriptorComparator comparator = new IconDescriptorComparator(); + + Assert.assertEquals(1, comparator.compare(genericDescriptor, faviconDescriptor)); + Assert.assertEquals(-1, comparator.compare(faviconDescriptor, genericDescriptor)); + + Assert.assertEquals(1, comparator.compare(genericDescriptor, touchIconDescriptor)); + Assert.assertEquals(-1, comparator.compare(touchIconDescriptor, genericDescriptor)); + } + + @Test + public void testLookupIconsAreRankedHigherThanGenericIcons() { + final IconDescriptor genericDescriptor = IconDescriptor.createGenericIcon(TEST_ICON_URL_1); + final IconDescriptor lookupDescriptor = IconDescriptor.createLookupIcon(TEST_ICON_URL_2); + + final IconDescriptorComparator comparator = new IconDescriptorComparator(); + + Assert.assertEquals(1, comparator.compare(genericDescriptor, lookupDescriptor)); + Assert.assertEquals(-1, comparator.compare(lookupDescriptor, genericDescriptor)); + } + + @Test + public void testFaviconsAndTouchIconsAreRankedHigherThanLookupIcons() { + final IconDescriptor lookupDescriptor = IconDescriptor.createLookupIcon(TEST_ICON_URL_1); + + final IconDescriptor faviconDescriptor = IconDescriptor.createFavicon(TEST_ICON_URL_2, TEST_SIZE, TEST_MIME_TYPE); + final IconDescriptor touchIconDescriptor = IconDescriptor.createTouchicon(TEST_ICON_URL_3, TEST_SIZE, TEST_MIME_TYPE); + + final IconDescriptorComparator comparator = new IconDescriptorComparator(); + + Assert.assertEquals(1, comparator.compare(lookupDescriptor, faviconDescriptor)); + Assert.assertEquals(-1, comparator.compare(faviconDescriptor, lookupDescriptor)); + + Assert.assertEquals(1, comparator.compare(lookupDescriptor, touchIconDescriptor)); + Assert.assertEquals(-1, comparator.compare(touchIconDescriptor, lookupDescriptor)); + } + + @Test + public void testLargestIconOfSameTypeIsSelected() { + final IconDescriptor smallDescriptor = IconDescriptor.createFavicon(TEST_ICON_URL_1, 16, TEST_MIME_TYPE); + final IconDescriptor largeDescriptor = IconDescriptor.createFavicon(TEST_ICON_URL_2, 128, TEST_MIME_TYPE); + + final IconDescriptorComparator comparator = new IconDescriptorComparator(); + + Assert.assertEquals(1, comparator.compare(smallDescriptor, largeDescriptor)); + Assert.assertEquals(-1, comparator.compare(largeDescriptor, smallDescriptor)); + } + + @Test + public void testContainerTypesArePreferred() { + final IconDescriptor containerDescriptor = IconDescriptor.createFavicon(TEST_ICON_URL_1, TEST_SIZE, "image/x-icon"); + final IconDescriptor faviconDescriptor = IconDescriptor.createFavicon(TEST_ICON_URL_2, TEST_SIZE, "image/png"); + + final IconDescriptorComparator comparator = new IconDescriptorComparator(); + + Assert.assertEquals(1, comparator.compare(faviconDescriptor, containerDescriptor)); + Assert.assertEquals(-1, comparator.compare(containerDescriptor, faviconDescriptor)); + } + + @Test + public void testWithNoDifferences() { + final IconDescriptor descriptor1 = IconDescriptor.createFavicon(TEST_ICON_URL_1, TEST_SIZE, TEST_MIME_TYPE); + final IconDescriptor descriptor2 = IconDescriptor.createFavicon(TEST_ICON_URL_2, TEST_SIZE, TEST_MIME_TYPE); + + final IconDescriptorComparator comparator = new IconDescriptorComparator(); + + Assert.assertNotEquals(0, comparator.compare(descriptor1, descriptor2)); + Assert.assertNotEquals(0, comparator.compare(descriptor2, descriptor1)); + } + + @Test + public void testWithSameObject() { + final IconDescriptor descriptor = IconDescriptor.createTouchicon(TEST_ICON_URL_1, TEST_SIZE, TEST_MIME_TYPE); + + final IconDescriptorComparator comparator = new IconDescriptorComparator(); + Assert.assertEquals(0, comparator.compare(descriptor, descriptor)); + } + + /** + * This test reconstructs the scenario from bug 1331808. A comparator implementation that does + * not return a consistent order can break the implementation of remove() of the TreeSet class. + */ + @Test + public void testBug1331808() { + TreeSet<IconDescriptor> set = new TreeSet<>(new IconDescriptorComparator()); + + set.add(IconDescriptor.createFavicon("http://example.org/new-logo32.jpg", 0, "")); + set.add(IconDescriptor.createTouchicon("http://example.org/new-logo57.jpg", 0, "")); + set.add(IconDescriptor.createTouchicon("http://example.org/new-logo76.jpg", 76, "")); + set.add(IconDescriptor.createTouchicon("http://example.org/new-logo120.jpg", 120, "")); + set.add(IconDescriptor.createTouchicon("http://example.org/new-logo152.jpg", 114, "")); + set.add(IconDescriptor.createFavicon("http://example.org/02.png", 32, "")); + set.add(IconDescriptor.createFavicon("http://example.org/01.png", 192, "")); + set.add(IconDescriptor.createTouchicon("http://example.org/03.png", 0, "")); + + for (int i = 8; i > 0; i--) { + Assert.assertEquals("items in set before deleting: " + i, i, set.size()); + Assert.assertTrue("item removed successfully: " + i, set.remove(set.first())); + Assert.assertEquals("items in set after deleting: " + i, i - 1, set.size()); + } + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/TestIconRequest.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/TestIconRequest.java new file mode 100644 index 000000000..d77ad6a53 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/TestIconRequest.java @@ -0,0 +1,81 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.icons; + +import junit.framework.Assert; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.robolectric.RuntimeEnvironment; + +import java.util.TreeSet; + +import static org.hamcrest.Matchers.any; +import static org.mockito.Matchers.anyObject; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; + +@RunWith(TestRunner.class) +public class TestIconRequest { + private static final String TEST_PAGE_URL = "http://www.mozilla.org"; + private static final String TEST_ICON_URL_1 = "http://www.mozilla.org/favicon.ico"; + private static final String TEST_ICON_URL_2 = "http://www.example.org/favicon.ico"; + + @Test + public void testIconHandling() { + final IconRequest request = Icons.with(RuntimeEnvironment.application) + .pageUrl(TEST_PAGE_URL) + .build(); + + Assert.assertEquals(0, request.getIconCount()); + Assert.assertFalse(request.hasIconDescriptors()); + + request.modify() + .icon(IconDescriptor.createGenericIcon(TEST_ICON_URL_1)) + .deferBuild(); + + Assert.assertEquals(1, request.getIconCount()); + Assert.assertTrue(request.hasIconDescriptors()); + + request.modify() + .icon(IconDescriptor.createGenericIcon(TEST_ICON_URL_2)) + .deferBuild(); + + Assert.assertEquals(2, request.getIconCount()); + Assert.assertTrue(request.hasIconDescriptors()); + + Assert.assertEquals(TEST_ICON_URL_2, request.getBestIcon().getUrl()); + + request.moveToNextIcon(); + + Assert.assertEquals(1, request.getIconCount()); + Assert.assertTrue(request.hasIconDescriptors()); + + Assert.assertEquals(TEST_ICON_URL_1, request.getBestIcon().getUrl()); + + request.moveToNextIcon(); + + Assert.assertEquals(0, request.getIconCount()); + Assert.assertFalse(request.hasIconDescriptors()); + } + + /** + * If removing an icon from the internal set failed then we want to throw an exception. + */ + @Test(expected = IllegalStateException.class) + public void testMoveToNextIconThrowsException() { + final IconRequest request = Icons.with(RuntimeEnvironment.application) + .pageUrl(TEST_PAGE_URL) + .build(); + + //noinspection unchecked - Creating a mock of a generic type + request.icons = (TreeSet<IconDescriptor>) mock(TreeSet.class); + + //noinspection SuspiciousMethodCalls + doReturn(false).when(request.icons).remove(anyObject()); + + request.moveToNextIcon(); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/TestIconRequestBuilder.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/TestIconRequestBuilder.java new file mode 100644 index 000000000..0743b42d8 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/TestIconRequestBuilder.java @@ -0,0 +1,159 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.icons; + +import org.junit.Assert; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.GeckoAppShell; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.robolectric.RuntimeEnvironment; + +@RunWith(TestRunner.class) +public class TestIconRequestBuilder { + private static final String TEST_PAGE_URL_1 = "http://www.mozilla.org"; + private static final String TEST_PAGE_URL_2 = "http://www.example.org"; + private static final String TEST_ICON_URL_1 = "http://www.mozilla.org/favicon.ico"; + private static final String TEST_ICON_URL_2 = "http://www.example.org/favicon.ico"; + + @Test + public void testPrivileged() { + IconRequest request = Icons.with(RuntimeEnvironment.application) + .pageUrl(TEST_PAGE_URL_1) + .build(); + + Assert.assertFalse(request.isPrivileged()); + + request.modify() + .privileged(true) + .deferBuild(); + + Assert.assertTrue(request.isPrivileged()); + } + + @Test + public void testPageUrl() { + IconRequest request = Icons.with(RuntimeEnvironment.application) + .pageUrl(TEST_PAGE_URL_1) + .build(); + + Assert.assertEquals(TEST_PAGE_URL_1, request.getPageUrl()); + + request.modify() + .pageUrl(TEST_PAGE_URL_2) + .deferBuild(); + + Assert.assertEquals(TEST_PAGE_URL_2, request.getPageUrl()); + } + + @Test + public void testIcons() { + // Initially a request is empty. + IconRequest request = Icons.with(RuntimeEnvironment.application) + .pageUrl(TEST_PAGE_URL_1) + .build(); + + Assert.assertEquals(0, request.getIconCount()); + + // Adding one icon URL. + request.modify() + .icon(IconDescriptor.createGenericIcon(TEST_ICON_URL_1)) + .deferBuild(); + + Assert.assertEquals(1, request.getIconCount()); + + // Adding the same icon URL again is ignored. + request.modify() + .icon(IconDescriptor.createGenericIcon(TEST_ICON_URL_1)) + .deferBuild(); + + Assert.assertEquals(1, request.getIconCount()); + + // Adding another new icon URL. + request.modify() + .icon(IconDescriptor.createGenericIcon(TEST_ICON_URL_2)) + .deferBuild(); + + Assert.assertEquals(2, request.getIconCount()); + } + + @Test + public void testSkipNetwork() { + IconRequest request = Icons.with(RuntimeEnvironment.application) + .pageUrl(TEST_PAGE_URL_1) + .build(); + + Assert.assertFalse(request.shouldSkipNetwork()); + + request.modify() + .skipNetwork() + .deferBuild(); + + Assert.assertTrue(request.shouldSkipNetwork()); + } + + @Test + public void testSkipDisk() { + IconRequest request = Icons.with(RuntimeEnvironment.application) + .pageUrl(TEST_PAGE_URL_1) + .build(); + + Assert.assertFalse(request.shouldSkipDisk()); + + request.modify() + .skipDisk() + .deferBuild(); + + Assert.assertTrue(request.shouldSkipDisk()); + } + + @Test + public void testSkipMemory() { + IconRequest request = Icons.with(RuntimeEnvironment.application) + .pageUrl(TEST_PAGE_URL_1) + .build(); + + Assert.assertFalse(request.shouldSkipMemory()); + + request.modify() + .skipMemory() + .deferBuild(); + + Assert.assertTrue(request.shouldSkipMemory()); + } + + @Test + public void testExecutionOnBackgroundThread() { + IconRequest request = Icons.with(RuntimeEnvironment.application) + .pageUrl(TEST_PAGE_URL_1) + .build(); + + Assert.assertFalse(request.shouldRunOnBackgroundThread()); + + request.modify() + .executeCallbackOnBackgroundThread() + .deferBuild(); + + Assert.assertTrue(request.shouldRunOnBackgroundThread()); + } + + @Test + public void testForLauncherIcon() { + // This code will call into GeckoAppShell to determine the launcher icon size for this configuration + GeckoAppShell.setApplicationContext(RuntimeEnvironment.application); + + IconRequest request = Icons.with(RuntimeEnvironment.application) + .pageUrl(TEST_PAGE_URL_1) + .build(); + + Assert.assertEquals(32, request.getTargetSize()); + + request.modify() + .forLauncherIcon() + .deferBuild(); + + Assert.assertEquals(48, request.getTargetSize()); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/TestIconResponse.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/TestIconResponse.java new file mode 100644 index 000000000..4c7faa4f8 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/TestIconResponse.java @@ -0,0 +1,148 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.icons; + +import android.graphics.Bitmap; +import android.graphics.Color; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.TestRunner; + +import static org.mockito.Mockito.mock; + +@RunWith(TestRunner.class) +public class TestIconResponse { + private static final String ICON_URL = "http://www.mozilla.org/favicon.ico"; + + @Test + public void testDefaultResponse() { + final Bitmap bitmap = mock(Bitmap.class); + + final IconResponse response = IconResponse.create(bitmap); + + Assert.assertEquals(bitmap, response.getBitmap()); + Assert.assertFalse(response.hasUrl()); + Assert.assertNull(response.getUrl()); + + Assert.assertFalse(response.hasColor()); + Assert.assertEquals(0, response.getColor()); + + Assert.assertFalse(response.isGenerated()); + Assert.assertFalse(response.isFromNetwork()); + Assert.assertFalse(response.isFromDisk()); + Assert.assertFalse(response.isFromMemory()); + } + + @Test + public void testNetworkResponse() { + final Bitmap bitmap = mock(Bitmap.class); + + final IconResponse response = IconResponse.createFromNetwork(bitmap, ICON_URL); + + Assert.assertEquals(bitmap, response.getBitmap()); + Assert.assertTrue(response.hasUrl()); + Assert.assertEquals(ICON_URL, response.getUrl()); + + Assert.assertFalse(response.hasColor()); + Assert.assertEquals(0, response.getColor()); + + Assert.assertFalse(response.isGenerated()); + Assert.assertTrue(response.isFromNetwork()); + Assert.assertFalse(response.isFromDisk()); + Assert.assertFalse(response.isFromMemory()); + } + + @Test + public void testGeneratedResponse() { + final Bitmap bitmap = mock(Bitmap.class); + + final IconResponse response = IconResponse.createGenerated(bitmap, Color.CYAN); + + Assert.assertEquals(bitmap, response.getBitmap()); + Assert.assertFalse(response.hasUrl()); + Assert.assertNull(response.getUrl()); + + Assert.assertTrue(response.hasColor()); + Assert.assertEquals(Color.CYAN, response.getColor()); + + Assert.assertTrue(response.isGenerated()); + Assert.assertFalse(response.isFromNetwork()); + Assert.assertFalse(response.isFromDisk()); + Assert.assertFalse(response.isFromMemory()); + } + + @Test + public void testMemoryResponse() { + final Bitmap bitmap = mock(Bitmap.class); + + final IconResponse response = IconResponse.createFromMemory(bitmap, ICON_URL, Color.CYAN); + + Assert.assertEquals(bitmap, response.getBitmap()); + Assert.assertTrue(response.hasUrl()); + Assert.assertEquals(ICON_URL, response.getUrl()); + + Assert.assertTrue(response.hasColor()); + Assert.assertEquals(Color.CYAN, response.getColor()); + + Assert.assertFalse(response.isGenerated()); + Assert.assertFalse(response.isFromNetwork()); + Assert.assertFalse(response.isFromDisk()); + Assert.assertTrue(response.isFromMemory()); + } + + @Test + public void testDiskResponse() { + final Bitmap bitmap = mock(Bitmap.class); + + final IconResponse response = IconResponse.createFromDisk(bitmap, ICON_URL); + + Assert.assertEquals(bitmap, response.getBitmap()); + Assert.assertTrue(response.hasUrl()); + Assert.assertEquals(ICON_URL, response.getUrl()); + + Assert.assertFalse(response.hasColor()); + Assert.assertEquals(0, response.getColor()); + + Assert.assertFalse(response.isGenerated()); + Assert.assertFalse(response.isFromNetwork()); + Assert.assertTrue(response.isFromDisk()); + Assert.assertFalse(response.isFromMemory()); + } + + @Test + public void testUpdatingColor() { + final IconResponse response = IconResponse.create(mock(Bitmap.class)); + + Assert.assertFalse(response.hasColor()); + Assert.assertEquals(0, response.getColor()); + + response.updateColor(Color.YELLOW); + + Assert.assertTrue(response.hasColor()); + Assert.assertEquals(Color.YELLOW, response.getColor()); + + response.updateColor(Color.MAGENTA); + + Assert.assertTrue(response.hasColor()); + Assert.assertEquals(Color.MAGENTA, response.getColor()); + } + + @Test + public void testUpdatingBitmap() { + final Bitmap originalBitmap = mock(Bitmap.class); + final Bitmap updatedBitmap = mock(Bitmap.class); + + final IconResponse response = IconResponse.create(originalBitmap); + + Assert.assertEquals(originalBitmap, response.getBitmap()); + Assert.assertNotEquals(updatedBitmap, response.getBitmap()); + + response.updateBitmap(updatedBitmap); + + Assert.assertNotEquals(originalBitmap, response.getBitmap()); + Assert.assertEquals(updatedBitmap, response.getBitmap()); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/TestIconTask.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/TestIconTask.java new file mode 100644 index 000000000..77a801988 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/TestIconTask.java @@ -0,0 +1,575 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.icons; + +import android.graphics.Bitmap; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.icons.loader.IconLoader; +import org.mozilla.gecko.icons.preparation.Preparer; +import org.mozilla.gecko.icons.processing.Processor; +import org.robolectric.RuntimeEnvironment; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +@RunWith(TestRunner.class) +public class TestIconTask { + @Test + public void testGeneratorIsInvokedIfAllLoadersFail() { + final List<IconLoader> loaders = Arrays.asList( + createFailingLoader(), + createFailingLoader(), + createFailingLoader()); + + final Bitmap bitmap = mock(Bitmap.class); + final IconLoader generator = createSuccessfulLoader(bitmap); + + final IconRequest request = createIconRequest(); + + final IconTask task = new IconTask( + request, + Collections.<Preparer>emptyList(), + loaders, + Collections.<Processor>emptyList(), + generator); + + final IconResponse response = task.call(); + + // Verify all loaders have been tried + for (IconLoader loader : loaders) { + verify(loader).load(request); + } + + // Verify generator was called + verify(generator).load(request); + + // Verify response contains generated bitmap + Assert.assertEquals(bitmap, response.getBitmap()); + } + + @Test + public void testGeneratorIsNotCalledIfOneLoaderWasSuccessful() { + final List<IconLoader> loaders = Collections.singletonList( + createSuccessfulLoader(mock(Bitmap.class))); + + final IconLoader generator = createSuccessfulLoader(mock(Bitmap.class)); + + final IconRequest request = createIconRequest(); + + final IconTask task = new IconTask( + request, + Collections.<Preparer>emptyList(), + loaders, + Collections.<Processor>emptyList(), + generator); + + final IconResponse response = task.call(); + + // Verify all loaders have been tried + for (IconLoader loader : loaders) { + verify(loader).load(request); + } + + // Verify generator was NOT called + verify(generator, never()).load(request); + + Assert.assertNotNull(response); + } + + @Test + public void testNoLoaderIsInvokedForRequestWithoutUrls() { + final List<IconLoader> loaders = Collections.singletonList( + createSuccessfulLoader(mock(Bitmap.class))); + + final Bitmap bitmap = mock(Bitmap.class); + final IconLoader generator = createSuccessfulLoader(bitmap); + + final IconRequest request = createIconRequestWithoutUrls(); + + final IconTask task = new IconTask( + request, + Collections.<Preparer>emptyList(), + loaders, + Collections.<Processor>emptyList(), + generator); + + final IconResponse response = task.call(); + + // Verify NO loaders have been called + for (IconLoader loader : loaders) { + verify(loader, never()).load(request); + } + + // Verify generator was called + verify(generator).load(request); + + // Verify response contains generated bitmap + Assert.assertEquals(bitmap, response.getBitmap()); + } + + @Test + public void testAllPreparersAreCalledBeforeLoading() { + final List<Preparer> preparers = Arrays.asList( + mock(Preparer.class), + mock(Preparer.class), + mock(Preparer.class), + mock(Preparer.class), + mock(Preparer.class), + mock(Preparer.class)); + + final IconRequest request = createIconRequest(); + + final IconTask task = new IconTask( + request, + preparers, + createListWithSuccessfulLoader(), + Collections.<Processor>emptyList(), + createGenerator()); + + task.call(); + + // Verify all preparers have been called + for (Preparer preparer : preparers) { + verify(preparer).prepare(request); + } + } + + @Test + public void testSubsequentLoadersAreNotCalledAfterSuccessfulLoad() { + final Bitmap bitmap = mock(Bitmap.class); + + final List<IconLoader> loaders = Arrays.asList( + createFailingLoader(), + createFailingLoader(), + createSuccessfulLoader(bitmap), + createSuccessfulLoader(mock(Bitmap.class)), + createFailingLoader(), + createSuccessfulLoader(mock(Bitmap.class))); + + final IconRequest request = createIconRequest(); + + final IconTask task = new IconTask( + request, + Collections.<Preparer>emptyList(), + loaders, + Collections.<Processor>emptyList(), + createGenerator()); + + final IconResponse response = task.call(); + + // First loaders are called + verify(loaders.get(0)).load(request); + verify(loaders.get(1)).load(request); + verify(loaders.get(2)).load(request); + + // Loaders after successful load are not called + verify(loaders.get(3), never()).load(request); + verify(loaders.get(4), never()).load(request); + verify(loaders.get(5), never()).load(request); + + Assert.assertNotNull(response); + Assert.assertEquals(bitmap, response.getBitmap()); + } + + @Test + public void testNoProcessorIsCalledForUnsuccessfulLoads() { + final IconRequest request = createIconRequest(); + + final List<IconLoader> loaders = createListWithFailingLoaders(); + + final List<Processor> processors = Arrays.asList( + createProcessor(), + createProcessor(), + createProcessor()); + + final IconTask task = new IconTask( + request, + Collections.<Preparer>emptyList(), + loaders, + processors, + createFailingLoader()); + + task.call(); + + // Verify all loaders have been tried + for (IconLoader loader : loaders) { + verify(loader).load(request); + } + + // Verify no processor was called + for (Processor processor : processors) { + verify(processor, never()).process(any(IconRequest.class), any(IconResponse.class)); + } + } + + @Test + public void testAllProcessorsAreCalledAfterSuccessfulLoad() { + final IconRequest request = createIconRequest(); + + final List<Processor> processors = Arrays.asList( + createProcessor(), + createProcessor(), + createProcessor()); + + final IconTask task = new IconTask( + request, + Collections.<Preparer>emptyList(), + createListWithSuccessfulLoader(), + processors, + createGenerator()); + + IconResponse response = task.call(); + + Assert.assertNotNull(response); + + // Verify that all processors have been called + for (Processor processor : processors) { + verify(processor).process(request, response); + } + } + + @Test + public void testCallbackIsExecutedForSuccessfulLoads() { + final IconCallback callback = mock(IconCallback.class); + + final IconRequest request = createIconRequest(); + request.setCallback(callback); + + final IconTask task = new IconTask( + request, + Collections.<Preparer>emptyList(), + createListWithSuccessfulLoader(), + Collections.<Processor>emptyList(), + createGenerator()); + + final IconResponse response = task.call(); + + verify(callback).onIconResponse(response); + } + + @Test + public void testCallbackIsNotExecutedIfLoadingFailed() { + final IconCallback callback = mock(IconCallback.class); + + final IconRequest request = createIconRequest(); + request.setCallback(callback); + + final IconTask task = new IconTask( + request, + Collections.<Preparer>emptyList(), + createListWithFailingLoaders(), + Collections.<Processor>emptyList(), + createFailingLoader()); + + task.call(); + + verify(callback, never()).onIconResponse(any(IconResponse.class)); + } + + @Test + public void testCallbackIsExecutedWithGeneratorResult() { + final IconCallback callback = mock(IconCallback.class); + + final IconRequest request = createIconRequest(); + request.setCallback(callback); + + final IconTask task = new IconTask( + request, + Collections.<Preparer>emptyList(), + createListWithFailingLoaders(), + Collections.<Processor>emptyList(), + createGenerator()); + + final IconResponse response = task.call(); + + verify(callback).onIconResponse(response); + } + + @Test + public void testTaskCancellationWhileLoading() { + // We simulate the cancellation by injecting a loader that interrupts the thread. + final IconLoader cancellingLoader = spy(new IconLoader() { + @Override + public IconResponse load(IconRequest request) { + Thread.currentThread().interrupt(); + return null; + } + }); + + final List<Preparer> preparers = createListOfPreparers(); + final List<Processor> processors = createListOfProcessors(); + + final List<IconLoader> loaders = Arrays.asList( + createFailingLoader(), + createFailingLoader(), + cancellingLoader, + createFailingLoader(), + createSuccessfulLoader(mock(Bitmap.class))); + + final IconRequest request = createIconRequest(); + + final IconTask task = new IconTask( + request, + preparers, + loaders, + processors, + createGenerator()); + + final IconResponse response = task.call(); + Assert.assertNull(response); + + // Verify that all preparers are called + for (Preparer preparer : preparers) { + verify(preparer).prepare(request); + } + + // Verify that first loaders are called + verify(loaders.get(0)).load(request); + verify(loaders.get(1)).load(request); + + // Verify that our loader that interrupts the thread is called + verify(loaders.get(2)).load(request); + + // Verify that all other loaders are not called + verify(loaders.get(3), never()).load(request); + verify(loaders.get(4), never()).load(request); + + // Verify that no processors are called + for (Processor processor : processors) { + verify(processor, never()).process(eq(request), any(IconResponse.class)); + } + } + + @Test + public void testTaskCancellationWhileProcessing() { + final Processor cancellingProcessor = spy(new Processor() { + @Override + public void process(IconRequest request, IconResponse response) { + Thread.currentThread().interrupt(); + } + }); + + final List<Preparer> preparers = createListOfPreparers(); + + final List<IconLoader> loaders = Arrays.asList( + createFailingLoader(), + createFailingLoader(), + createSuccessfulLoader(mock(Bitmap.class))); + + final List<Processor> processors = Arrays.asList( + createProcessor(), + createProcessor(), + cancellingProcessor, + createProcessor(), + createProcessor()); + + final IconRequest request = createIconRequest(); + + final IconTask task = new IconTask( + request, + preparers, + loaders, + processors, + createGenerator()); + + final IconResponse response = task.call(); + Assert.assertNull(response); + + // Verify that all preparers are called + for (Preparer preparer : preparers) { + verify(preparer).prepare(request); + } + + // Verify that all loaders are called + for (IconLoader loader : loaders) { + verify(loader).load(request); + } + + // Verify that first processors are called + verify(processors.get(0)).process(eq(request), any(IconResponse.class)); + verify(processors.get(1)).process(eq(request), any(IconResponse.class)); + + // Verify that cancelling processor is called + verify(processors.get(2)).process(eq(request), any(IconResponse.class)); + + // Verify that subsequent processors are not called + verify(processors.get(3), never()).process(eq(request), any(IconResponse.class)); + verify(processors.get(4), never()).process(eq(request), any(IconResponse.class)); + } + + @Test + public void testTaskCancellationWhilePerparing() { + final Preparer failingPreparer = spy(new Preparer() { + @Override + public void prepare(IconRequest request) { + Thread.currentThread().interrupt(); + } + }); + + final List<Preparer> preparers = Arrays.asList( + mock(Preparer.class), + mock(Preparer.class), + failingPreparer, + mock(Preparer.class), + mock(Preparer.class)); + + final List<IconLoader> loaders = createListWithSuccessfulLoader(); + final List<Processor> processors = createListOfProcessors(); + + final IconRequest request = createIconRequest(); + + final IconTask task = new IconTask( + request, + preparers, + loaders, + processors, + createGenerator()); + + final IconResponse response = task.call(); + Assert.assertNull(response); + + // Verify that first preparers are called + verify(preparers.get(0)).prepare(request); + verify(preparers.get(1)).prepare(request); + + // Verify that cancelling preparer is called + verify(preparers.get(2)).prepare(request); + + // Verify that subsequent preparers are not called + verify(preparers.get(3), never()).prepare(request); + verify(preparers.get(4), never()).prepare(request); + + // Verify that no loaders are called + for (IconLoader loader : loaders) { + verify(loader, never()).load(request); + } + + // Verify that no processors are called + for (Processor processor : processors) { + verify(processor, never()).process(eq(request), any(IconResponse.class)); + } + } + + @Test + public void testNoLoadersOrProcessorsAreExecutedForPrepareOnlyTasks() { + final List<Preparer> preparers = createListOfPreparers(); + final List<IconLoader> loaders = createListWithSuccessfulLoader(); + final List<Processor> processors = createListOfProcessors(); + final IconLoader generator = createGenerator(); + + final IconRequest request = createIconRequest() + .modify() + .prepareOnly() + .build(); + + final IconTask task = new IconTask( + request, + preparers, + loaders, + processors, + generator); + + IconResponse response = task.call(); + + Assert.assertNull(response); + + // Verify that all preparers are called + for (Preparer preparer : preparers) { + verify(preparer).prepare(request); + } + + // Verify that no loaders are called + for (IconLoader loader : loaders) { + verify(loader, never()).load(request); + } + + // Verify that no processors are called + for (Processor processor : processors) { + verify(processor, never()).process(eq(request), any(IconResponse.class)); + } + } + + public List<IconLoader> createListWithSuccessfulLoader() { + return Arrays.asList( + createFailingLoader(), + createFailingLoader(), + createSuccessfulLoader(mock(Bitmap.class)), + createFailingLoader()); + } + + public List<IconLoader> createListWithFailingLoaders() { + return Arrays.asList( + createFailingLoader(), + createFailingLoader(), + createFailingLoader(), + createFailingLoader(), + createFailingLoader()); + } + + public List<Preparer> createListOfPreparers() { + return Arrays.asList( + mock(Preparer.class), + mock(Preparer.class), + mock(Preparer.class), + mock(Preparer.class), + mock(Preparer.class)); + } + + public IconLoader createFailingLoader() { + final IconLoader loader = mock(IconLoader.class); + doReturn(null).when(loader).load(any(IconRequest.class)); + return loader; + } + + public IconLoader createSuccessfulLoader(Bitmap bitmap) { + IconResponse response = IconResponse.create(bitmap); + + final IconLoader loader = mock(IconLoader.class); + doReturn(response).when(loader).load(any(IconRequest.class)); + return loader; + } + + public List<Processor> createListOfProcessors() { + return Arrays.asList( + mock(Processor.class), + mock(Processor.class), + mock(Processor.class), + mock(Processor.class), + mock(Processor.class)); + } + + public IconRequest createIconRequest() { + return Icons.with(RuntimeEnvironment.application) + .pageUrl("http://www.mozilla.org") + .icon(IconDescriptor.createGenericIcon("http://www.mozilla.org/favicon.ico")) + .build(); + } + + public IconRequest createIconRequestWithoutUrls() { + return Icons.with(RuntimeEnvironment.application) + .pageUrl("http://www.mozilla.org") + .build(); + } + + public IconLoader createGenerator() { + return createSuccessfulLoader(mock(Bitmap.class)); + } + + public Processor createProcessor() { + return mock(Processor.class); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/TestIconsHelper.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/TestIconsHelper.java new file mode 100644 index 000000000..f40e2f629 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/TestIconsHelper.java @@ -0,0 +1,139 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.icons; + +import android.annotation.SuppressLint; + +import junit.framework.Assert; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.util.GeckoJarReader; +import org.robolectric.RuntimeEnvironment; + +@RunWith(TestRunner.class) +public class TestIconsHelper { + @SuppressLint("AuthLeak") // Lint and Android Studio try to prevent developers from writing code + // with credentials in the URL (user:password@host). But in this case + // we explicitly want to do that, so we suppress the warnings. + @Test + public void testGuessDefaultFaviconURL() { + // Empty values + + Assert.assertNull(IconsHelper.guessDefaultFaviconURL(null)); + Assert.assertNull(IconsHelper.guessDefaultFaviconURL("")); + Assert.assertNull(IconsHelper.guessDefaultFaviconURL(" ")); + + // Special about: URLs. + + Assert.assertEquals( + "about:home", + IconsHelper.guessDefaultFaviconURL("about:home")); + + Assert.assertEquals( + "about:", + IconsHelper.guessDefaultFaviconURL("about:")); + + Assert.assertEquals( + "about:addons", + IconsHelper.guessDefaultFaviconURL("about:addons")); + + // Non http(s) URLS + + final String jarUrl = GeckoJarReader.getJarURL(RuntimeEnvironment.application, "chrome/chrome/content/branding/favicon64.png"); + Assert.assertEquals(jarUrl, IconsHelper.guessDefaultFaviconURL(jarUrl)); + + Assert.assertNull(IconsHelper.guessDefaultFaviconURL("content://some.random.provider/icons")); + + Assert.assertNull(IconsHelper.guessDefaultFaviconURL("ftp://ftp.public.mozilla.org/this/is/made/up")); + + Assert.assertNull(IconsHelper.guessDefaultFaviconURL("file:///")); + + Assert.assertNull(IconsHelper.guessDefaultFaviconURL("file:///system/path")); + + // Various http(s) URLs + + Assert.assertEquals("http://www.mozilla.org/favicon.ico", + IconsHelper.guessDefaultFaviconURL("http://www.mozilla.org/")); + + Assert.assertEquals("https://www.mozilla.org/favicon.ico", + IconsHelper.guessDefaultFaviconURL("https://www.mozilla.org/en-US/firefox/products/")); + + Assert.assertEquals("https://example.org/favicon.ico", + IconsHelper.guessDefaultFaviconURL("https://example.org")); + + Assert.assertEquals("http://user:password@example.org:9991/favicon.ico", + IconsHelper.guessDefaultFaviconURL("http://user:password@example.org:9991/status/760492829949001728")); + + Assert.assertEquals("https://localhost:8888/favicon.ico", + IconsHelper.guessDefaultFaviconURL("https://localhost:8888/path/folder/file?some=query¶ms=none")); + + Assert.assertEquals("http://192.168.0.1/favicon.ico", + IconsHelper.guessDefaultFaviconURL("http://192.168.0.1/local/action.cgi")); + + Assert.assertEquals("https://medium.com/favicon.ico", + IconsHelper.guessDefaultFaviconURL("https://medium.com/firefox-mobile-engineering/firefox-for-android-hack-week-recap-f1ab12f5cc44#.rpmzz15ia")); + + // Some broken, partial URLs + + Assert.assertNull(IconsHelper.guessDefaultFaviconURL("http:")); + Assert.assertNull(IconsHelper.guessDefaultFaviconURL("http://")); + Assert.assertNull(IconsHelper.guessDefaultFaviconURL("https:/")); + } + + @Test + public void testIsContainerType() { + // Empty values + Assert.assertFalse(IconsHelper.isContainerType(null)); + Assert.assertFalse(IconsHelper.isContainerType("")); + Assert.assertFalse(IconsHelper.isContainerType(" ")); + + // Values that don't make any sense. + Assert.assertFalse(IconsHelper.isContainerType("Hello World")); + Assert.assertFalse(IconsHelper.isContainerType("no/no/no")); + Assert.assertFalse(IconsHelper.isContainerType("42")); + + // Actual image MIME types that are not container types + Assert.assertFalse(IconsHelper.isContainerType("image/png")); + Assert.assertFalse(IconsHelper.isContainerType("application/bmp")); + Assert.assertFalse(IconsHelper.isContainerType("image/gif")); + Assert.assertFalse(IconsHelper.isContainerType("image/x-windows-bitmap")); + Assert.assertFalse(IconsHelper.isContainerType("image/jpeg")); + Assert.assertFalse(IconsHelper.isContainerType("application/x-png")); + + // MIME types of image container + Assert.assertTrue(IconsHelper.isContainerType("image/vnd.microsoft.icon")); + Assert.assertTrue(IconsHelper.isContainerType("image/ico")); + Assert.assertTrue(IconsHelper.isContainerType("image/icon")); + Assert.assertTrue(IconsHelper.isContainerType("image/x-icon")); + Assert.assertTrue(IconsHelper.isContainerType("text/ico")); + Assert.assertTrue(IconsHelper.isContainerType("application/ico")); + } + + @Test + public void testCanDecodeType() { + // Empty values + Assert.assertFalse(IconsHelper.canDecodeType(null)); + Assert.assertFalse(IconsHelper.canDecodeType("")); + Assert.assertFalse(IconsHelper.canDecodeType(" ")); + + // Some things we can't decode (or that just aren't images) + Assert.assertFalse(IconsHelper.canDecodeType("image/svg+xml")); + Assert.assertFalse(IconsHelper.canDecodeType("video/avi")); + Assert.assertFalse(IconsHelper.canDecodeType("text/plain")); + Assert.assertFalse(IconsHelper.canDecodeType("image/x-quicktime")); + Assert.assertFalse(IconsHelper.canDecodeType("image/tiff")); + Assert.assertFalse(IconsHelper.canDecodeType("application/zip")); + + // Some image MIME types we definitely can decode + Assert.assertTrue(IconsHelper.canDecodeType("image/bmp")); + Assert.assertTrue(IconsHelper.canDecodeType("image/x-icon")); + Assert.assertTrue(IconsHelper.canDecodeType("image/png")); + Assert.assertTrue(IconsHelper.canDecodeType("image/jpg")); + Assert.assertTrue(IconsHelper.canDecodeType("image/jpeg")); + Assert.assertTrue(IconsHelper.canDecodeType("image/ico")); + Assert.assertTrue(IconsHelper.canDecodeType("image/icon")); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/loader/TestContentProviderLoader.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/loader/TestContentProviderLoader.java new file mode 100644 index 000000000..58bb3ddf9 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/loader/TestContentProviderLoader.java @@ -0,0 +1,31 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.icons.loader; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.icons.IconDescriptor; +import org.mozilla.gecko.icons.IconRequest; +import org.mozilla.gecko.icons.IconResponse; +import org.mozilla.gecko.icons.Icons; +import org.robolectric.RuntimeEnvironment; + +@RunWith(TestRunner.class) +public class TestContentProviderLoader { + @Test + public void testNothingIsLoadedForHttpUrls() { + final IconRequest request = Icons.with(RuntimeEnvironment.application) + .pageUrl("http://www.mozilla.org") + .icon(IconDescriptor.createGenericIcon( + "https://www.mozilla.org/media/img/favicon/apple-touch-icon-180x180.00050c5b754e.png")) + .build(); + + IconLoader loader = new ContentProviderLoader(); + IconResponse response = loader.load(request); + + Assert.assertNull(response); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/loader/TestDataUriLoader.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/loader/TestDataUriLoader.java new file mode 100644 index 000000000..1fe6ad1a7 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/loader/TestDataUriLoader.java @@ -0,0 +1,46 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.icons.loader; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.icons.IconDescriptor; +import org.mozilla.gecko.icons.IconRequest; +import org.mozilla.gecko.icons.IconResponse; +import org.mozilla.gecko.icons.Icons; +import org.robolectric.RuntimeEnvironment; + +@RunWith(TestRunner.class) +public class TestDataUriLoader { + @Test + public void testNothingIsLoadedForHttpUrls() { + final IconRequest request = Icons.with(RuntimeEnvironment.application) + .pageUrl("http://www.mozilla.org") + .icon(IconDescriptor.createGenericIcon( + "https://www.mozilla.org/media/img/favicon/apple-touch-icon-180x180.00050c5b754e.png")) + .build(); + + IconLoader loader = new DataUriLoader(); + IconResponse response = loader.load(request); + + Assert.assertNull(response); + } + + @Test + public void testIconIsLoadedFromDataUri() { + final IconRequest request = Icons.with(RuntimeEnvironment.application) + .pageUrl("http://www.mozilla.org") + .icon(IconDescriptor.createGenericIcon( + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAAEklEQVR4AWP4z8AAxCDiP8N/AB3wBPxcBee7AAAAAElFTkSuQmCC")) + .build(); + + IconLoader loader = new DataUriLoader(); + IconResponse response = loader.load(request); + + Assert.assertNotNull(response); + Assert.assertNotNull(response.getBitmap()); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/loader/TestDiskLoader.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/loader/TestDiskLoader.java new file mode 100644 index 000000000..809c35102 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/loader/TestDiskLoader.java @@ -0,0 +1,94 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.icons.loader; + +import android.graphics.Bitmap; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.icons.IconDescriptor; +import org.mozilla.gecko.icons.IconRequest; +import org.mozilla.gecko.icons.IconResponse; +import org.mozilla.gecko.icons.Icons; +import org.mozilla.gecko.icons.storage.DiskStorage; +import org.robolectric.RuntimeEnvironment; + +import java.io.OutputStream; + +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; + +@RunWith(TestRunner.class) +public class TestDiskLoader { + private static final String TEST_PAGE_URL = "http://www.mozilla.org"; + private static final String TEST_ICON_URL = "https://example.org/favicon.ico"; + + @Test + public void testLoadingFromEmptyCache() { + final IconRequest request = Icons.with(RuntimeEnvironment.application) + .pageUrl(TEST_PAGE_URL) + .icon(IconDescriptor.createGenericIcon(TEST_ICON_URL)) + .build(); + + final IconLoader loader = new DiskLoader(); + final IconResponse response = loader.load(request); + + Assert.assertNull(response); + } + + @Test + public void testLoadingAfterAddingEntry() { + final Bitmap bitmap = createMockedBitmap(); + final IconResponse originalResponse = IconResponse.createFromNetwork(bitmap, TEST_ICON_URL); + + DiskStorage.get(RuntimeEnvironment.application) + .putIcon(originalResponse); + + final IconRequest request = Icons.with(RuntimeEnvironment.application) + .pageUrl(TEST_PAGE_URL) + .icon(IconDescriptor.createGenericIcon(TEST_ICON_URL)) + .build(); + + final IconLoader loader = new DiskLoader(); + final IconResponse loadedResponse = loader.load(request); + + Assert.assertNotNull(loadedResponse); + + // The responses are not the same: The original response was stored to disk and loaded from + // disk again. It's a copy effectively. + Assert.assertNotEquals(originalResponse, loadedResponse); + } + + @Test + public void testNothingIsLoadedIfDiskShouldBeSkipped() { + final Bitmap bitmap = createMockedBitmap(); + final IconResponse originalResponse = IconResponse.createFromNetwork(bitmap, TEST_ICON_URL); + + DiskStorage.get(RuntimeEnvironment.application) + .putIcon(originalResponse); + + final IconRequest request = Icons.with(RuntimeEnvironment.application) + .pageUrl(TEST_PAGE_URL) + .icon(IconDescriptor.createGenericIcon(TEST_ICON_URL)) + .skipDisk() + .build(); + + final IconLoader loader = new DiskLoader(); + final IconResponse loadedResponse = loader.load(request); + + Assert.assertNull(loadedResponse); + } + + private Bitmap createMockedBitmap() { + final Bitmap bitmap = mock(Bitmap.class); + + doReturn(true).when(bitmap).compress(any(Bitmap.CompressFormat.class), anyInt(), any(OutputStream.class)); + + return bitmap; + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/loader/TestIconDownloader.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/loader/TestIconDownloader.java new file mode 100644 index 000000000..533f14395 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/loader/TestIconDownloader.java @@ -0,0 +1,112 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.icons.loader; + +import android.content.Context; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.icons.IconDescriptor; +import org.mozilla.gecko.icons.IconRequest; +import org.mozilla.gecko.icons.IconResponse; +import org.mozilla.gecko.icons.Icons; +import org.mozilla.gecko.icons.storage.FailureCache; +import org.robolectric.RuntimeEnvironment; + +import java.net.HttpURLConnection; + +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +@RunWith(TestRunner.class) +public class TestIconDownloader { + /** + * Scenario: A request with a non HTTP URL (data:image/*) is executed. + * + * Verify that: + * * No download is performed. + */ + @Test + public void testDownloaderDoesNothingForNonHttpUrls() throws Exception { + final IconRequest request = Icons.with(RuntimeEnvironment.application) + .pageUrl("http://www.mozilla.org") + .icon(IconDescriptor.createGenericIcon( + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAAEklEQVR4AWP4z8AAxCDiP8N/AB3wBPxcBee7AAAAAElFTkSuQmCC")) + .build(); + + final IconDownloader downloader = spy(new IconDownloader()); + IconResponse response = downloader.load(request); + + Assert.assertNull(response); + + verify(downloader, never()).downloadAndDecodeImage(any(Context.class), anyString()); + verify(downloader, never()).connectTo(anyString()); + } + + /** + * Scenario: Request contains an URL and server returns 301 with location header (always the same URL). + * + * Verify that: + * * Download code stops and does not loop forever. + */ + @Test + public void testRedirectsAreFollowedButNotInCircles() throws Exception { + final IconRequest request = Icons.with(RuntimeEnvironment.application) + .pageUrl("http://www.mozilla.org") + .icon(IconDescriptor.createFavicon( + "https://www.mozilla.org/media/img/favicon.52506929be4c.ico", + 32, + "image/x-icon")) + .build(); + + HttpURLConnection mockedConnection = mock(HttpURLConnection.class); + doReturn(301).when(mockedConnection).getResponseCode(); + doReturn("http://example.org/favicon.ico").when(mockedConnection).getHeaderField("Location"); + + final IconDownloader downloader = spy(new IconDownloader()); + doReturn(mockedConnection).when(downloader).connectTo(anyString()); + IconResponse response = downloader.load(request); + + Assert.assertNull(response); + + verify(downloader).connectTo("https://www.mozilla.org/media/img/favicon.52506929be4c.ico"); + verify(downloader).connectTo("http://example.org/favicon.ico"); + } + + /** + * Scenario: Request contains an URL and server returns HTTP 404. + * + * Verify that: + * * URL is added to failure cache. + */ + @Test + public void testUrlIsAddedToFailureCacheIfServerReturnsClientError() throws Exception { + final String faviconUrl = "https://www.mozilla.org/404.ico"; + + final IconRequest request = Icons.with(RuntimeEnvironment.application) + .pageUrl("http://www.mozilla.org") + .icon(IconDescriptor.createFavicon(faviconUrl, 32, "image/x-icon")) + .build(); + + HttpURLConnection mockedConnection = mock(HttpURLConnection.class); + doReturn(404).when(mockedConnection).getResponseCode(); + + Assert.assertFalse(FailureCache.get().isKnownFailure(faviconUrl)); + + final IconDownloader downloader = spy(new IconDownloader()); + doReturn(mockedConnection).when(downloader).connectTo(anyString()); + IconResponse response = downloader.load(request); + + Assert.assertNull(response); + + Assert.assertTrue(FailureCache.get().isKnownFailure(faviconUrl)); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/loader/TestIconGenerator.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/loader/TestIconGenerator.java new file mode 100644 index 000000000..70e341365 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/loader/TestIconGenerator.java @@ -0,0 +1,128 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.icons.loader; + +import android.graphics.Bitmap; + +import org.junit.Assert; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.R; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.icons.IconDescriptor; +import org.mozilla.gecko.icons.IconRequest; +import org.mozilla.gecko.icons.IconResponse; +import org.mozilla.gecko.icons.Icons; +import org.robolectric.RuntimeEnvironment; + +@RunWith(TestRunner.class) +public class TestIconGenerator { + @Test + public void testNoIconIsGeneratorIfThereAreIconUrlsToLoadFrom() { + final IconRequest request = Icons.with(RuntimeEnvironment.application) + .pageUrl("http://www.mozilla.org") + .icon(IconDescriptor.createGenericIcon( + "https://www.mozilla.org/media/img/favicon/apple-touch-icon-180x180.00050c5b754e.png")) + .icon(IconDescriptor.createGenericIcon( + "https://www.mozilla.org/media/img/favicon.52506929be4c.ico")) + .build(); + + IconLoader loader = new IconGenerator(); + IconResponse response = loader.load(request); + + Assert.assertNull(response); + } + + @Test + public void testIconIsGeneratedForLastUrl() { + final IconRequest request = Icons.with(RuntimeEnvironment.application) + .pageUrl("http://www.mozilla.org") + .icon(IconDescriptor.createGenericIcon( + "https://www.mozilla.org/media/img/favicon/apple-touch-icon-180x180.00050c5b754e.png")) + .build(); + + IconLoader loader = new IconGenerator(); + IconResponse response = loader.load(request); + + Assert.assertNotNull(response); + Assert.assertNotNull(response.getBitmap()); + } + + @Test + public void testRepresentativeCharacter() { + Assert.assertEquals("M", IconGenerator.getRepresentativeCharacter("https://mozilla.org")); + Assert.assertEquals("W", IconGenerator.getRepresentativeCharacter("http://wikipedia.org")); + Assert.assertEquals("P", IconGenerator.getRepresentativeCharacter("http://plus.google.com")); + Assert.assertEquals("E", IconGenerator.getRepresentativeCharacter("https://en.m.wikipedia.org/wiki/Main_Page")); + + // Stripping common prefixes + Assert.assertEquals("T", IconGenerator.getRepresentativeCharacter("http://www.theverge.com")); + Assert.assertEquals("F", IconGenerator.getRepresentativeCharacter("https://m.facebook.com")); + Assert.assertEquals("T", IconGenerator.getRepresentativeCharacter("https://mobile.twitter.com")); + + // Special urls + Assert.assertEquals("?", IconGenerator.getRepresentativeCharacter("file:///")); + Assert.assertEquals("S", IconGenerator.getRepresentativeCharacter("file:///system/")); + Assert.assertEquals("P", IconGenerator.getRepresentativeCharacter("ftp://people.mozilla.org/test")); + + // No values + Assert.assertEquals("?", IconGenerator.getRepresentativeCharacter("")); + Assert.assertEquals("?", IconGenerator.getRepresentativeCharacter(null)); + + // Rubbish + Assert.assertEquals("Z", IconGenerator.getRepresentativeCharacter("zZz")); + Assert.assertEquals("Ö", IconGenerator.getRepresentativeCharacter("ölkfdpou3rkjaslfdköasdfo8")); + Assert.assertEquals("?", IconGenerator.getRepresentativeCharacter("_*+*'##")); + Assert.assertEquals("ツ", IconGenerator.getRepresentativeCharacter("¯\\_(ツ)_/¯")); + Assert.assertEquals("ಠ", IconGenerator.getRepresentativeCharacter("ಠ_ಠ Look of Disapproval")); + + // Non-ASCII + Assert.assertEquals("Ä", IconGenerator.getRepresentativeCharacter("http://www.ätzend.de")); + Assert.assertEquals("名", IconGenerator.getRepresentativeCharacter("http://名がドメイン.com")); + Assert.assertEquals("C", IconGenerator.getRepresentativeCharacter("http://√.com")); + Assert.assertEquals("ß", IconGenerator.getRepresentativeCharacter("http://ß.de")); + Assert.assertEquals("Ԛ", IconGenerator.getRepresentativeCharacter("http://ԛәлп.com/")); // cyrillic + + // Punycode + Assert.assertEquals("X", IconGenerator.getRepresentativeCharacter("http://xn--tzend-fra.de")); // ätzend.de + Assert.assertEquals("X", IconGenerator.getRepresentativeCharacter("http://xn--V8jxj3d1dzdz08w.com")); // 名がドメイン.com + + // Numbers + Assert.assertEquals("1", IconGenerator.getRepresentativeCharacter("https://www.1and1.com/")); + + // IP + Assert.assertEquals("1", IconGenerator.getRepresentativeCharacter("https://192.168.0.1")); + } + + @Test + public void testPickColor() { + final int color = IconGenerator.pickColor("http://m.facebook.com"); + + // Color does not change + for (int i = 0; i < 100; i++) { + Assert.assertEquals(color, IconGenerator.pickColor("http://m.facebook.com")); + } + + // Color is stable for "similar" hosts. + Assert.assertEquals(color, IconGenerator.pickColor("https://m.facebook.com")); + Assert.assertEquals(color, IconGenerator.pickColor("http://facebook.com")); + Assert.assertEquals(color, IconGenerator.pickColor("http://www.facebook.com")); + Assert.assertEquals(color, IconGenerator.pickColor("http://www.facebook.com/foo/bar/foobar?mobile=1")); + } + + @Test + public void testGeneratingFavicon() { + final IconResponse response = IconGenerator.generate(RuntimeEnvironment.application, "http://m.facebook.com"); + final Bitmap bitmap = response.getBitmap(); + + Assert.assertNotNull(bitmap); + + final int size = RuntimeEnvironment.application.getResources().getDimensionPixelSize(R.dimen.favicon_bg); + Assert.assertEquals(size, bitmap.getWidth()); + Assert.assertEquals(size, bitmap.getHeight()); + + Assert.assertEquals(Bitmap.Config.ARGB_8888, bitmap.getConfig()); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/loader/TestJarLoader.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/loader/TestJarLoader.java new file mode 100644 index 000000000..48f0c26eb --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/loader/TestJarLoader.java @@ -0,0 +1,31 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.icons.loader; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.icons.IconDescriptor; +import org.mozilla.gecko.icons.IconRequest; +import org.mozilla.gecko.icons.IconResponse; +import org.mozilla.gecko.icons.Icons; +import org.robolectric.RuntimeEnvironment; + +@RunWith(TestRunner.class) +public class TestJarLoader { + @Test + public void testNothingIsLoadedForHttpUrls() { + final IconRequest request = Icons.with(RuntimeEnvironment.application) + .pageUrl("http://www.mozilla.org") + .icon(IconDescriptor.createGenericIcon( + "https://www.mozilla.org/media/img/favicon/apple-touch-icon-180x180.00050c5b754e.png")) + .build(); + + IconLoader loader = new JarLoader(); + IconResponse response = loader.load(request); + + Assert.assertNull(response); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/loader/TestLegacyLoader.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/loader/TestLegacyLoader.java new file mode 100644 index 000000000..eecf76788 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/loader/TestLegacyLoader.java @@ -0,0 +1,152 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.icons.loader; + +import android.graphics.Bitmap; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.GeckoProfile; +import org.mozilla.gecko.background.db.DelegatingTestContentProvider; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.db.BrowserContract; +import org.mozilla.gecko.db.BrowserDB; +import org.mozilla.gecko.db.BrowserProvider; +import org.mozilla.gecko.icons.IconDescriptor; +import org.mozilla.gecko.icons.IconRequest; +import org.mozilla.gecko.icons.IconResponse; +import org.mozilla.gecko.icons.Icons; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.shadows.ShadowContentResolver; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.Iterator; +import java.util.concurrent.ConcurrentHashMap; + +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +@RunWith(TestRunner.class) +public class TestLegacyLoader { + private static final String TEST_PAGE_URL = "http://www.mozilla.org"; + private static final String TEST_ICON_URL = "https://example.org/favicon.ico"; + private static final String TEST_ICON_URL_2 = "https://example.com/page/favicon.ico"; + private static final String TEST_ICON_URL_3 = "https://example.net/icon/favicon.ico"; + + @Test + public void testDatabaseIsQueriesForNormalRequestsWithNetworkSkipped() { + // We're going to query BrowserProvider via LegacyLoader, and will access a database. + // We need to ensure we close our db connection properly. + // This is the only test in this class that actually accesses a database. If that changes, + // move BrowserProvider registration into a @Before method, and provider.shutdown into @After. + final BrowserProvider provider = new BrowserProvider(); + provider.onCreate(); + ShadowContentResolver.registerProvider(BrowserContract.AUTHORITY, new DelegatingTestContentProvider(provider)); + try { + final IconRequest request = Icons.with(RuntimeEnvironment.application) + .pageUrl(TEST_PAGE_URL) + .icon(IconDescriptor.createGenericIcon(TEST_ICON_URL)) + .skipNetwork() + .build(); + + final LegacyLoader loader = spy(new LegacyLoader()); + final IconResponse response = loader.load(request); + + verify(loader).loadBitmapFromDatabase(request); + Assert.assertNull(response); + // Close any open db connections. + } finally { + provider.shutdown(); + } + } + + @Test + public void testNothingIsLoadedIfNetworkIsNotSkipped() { + final IconRequest request = Icons.with(RuntimeEnvironment.application) + .pageUrl(TEST_PAGE_URL) + .icon(IconDescriptor.createGenericIcon(TEST_ICON_URL)) + .build(); + + final LegacyLoader loader = spy(new LegacyLoader()); + final IconResponse response = loader.load(request); + + verify(loader, never()).loadBitmapFromDatabase(request); + + Assert.assertNull(response); + } + + @Test + public void testNothingIsLoadedIfDiskSHouldBeSkipped() { + final IconRequest request = Icons.with(RuntimeEnvironment.application) + .pageUrl(TEST_PAGE_URL) + .icon(IconDescriptor.createGenericIcon(TEST_ICON_URL)) + .skipDisk() + .build(); + + final LegacyLoader loader = spy(new LegacyLoader()); + final IconResponse response = loader.load(request); + + verify(loader, never()).loadBitmapFromDatabase(request); + + Assert.assertNull(response); + } + + @Test + public void testLoadedBitmapIsReturnedAsResponse() { + final IconRequest request = Icons.with(RuntimeEnvironment.application) + .pageUrl(TEST_PAGE_URL) + .icon(IconDescriptor.createGenericIcon(TEST_ICON_URL)) + .skipNetwork() + .build(); + + final Bitmap bitmap = mock(Bitmap.class); + + final LegacyLoader loader = spy(new LegacyLoader()); + doReturn(bitmap).when(loader).loadBitmapFromDatabase(request); + + final IconResponse response = loader.load(request); + + Assert.assertNotNull(response); + Assert.assertEquals(bitmap, response.getBitmap()); + } + + @Test + public void testLoaderOnlyLoadsIfThereIsOneIconLeft() { + final IconRequest request = Icons.with(RuntimeEnvironment.application) + .pageUrl(TEST_PAGE_URL) + .icon(IconDescriptor.createGenericIcon(TEST_ICON_URL)) + .icon(IconDescriptor.createGenericIcon(TEST_ICON_URL_2)) + .icon(IconDescriptor.createGenericIcon(TEST_ICON_URL_3)) + .skipNetwork() + .build(); + + final LegacyLoader loader = spy(new LegacyLoader()); + doReturn(mock(Bitmap.class)).when(loader).loadBitmapFromDatabase(request); + + // First load doesn't load an icon. + Assert.assertNull(loader.load(request)); + + // Second load doesn't load an icon. + removeFirstIcon(request); + Assert.assertNull(loader.load(request)); + + // Now only one icon is left and a response will be returned. + removeFirstIcon(request); + Assert.assertNotNull(loader.load(request)); + } + + private void removeFirstIcon(IconRequest request) { + final Iterator<IconDescriptor> iterator = request.getIconIterator(); + if (iterator.hasNext()) { + iterator.next(); + iterator.remove(); + } + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/loader/TestMemoryLoader.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/loader/TestMemoryLoader.java new file mode 100644 index 000000000..414ac8cc7 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/loader/TestMemoryLoader.java @@ -0,0 +1,78 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.icons.loader; + +import android.graphics.Bitmap; +import android.graphics.Color; + +import junit.framework.Assert; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.icons.IconDescriptor; +import org.mozilla.gecko.icons.IconRequest; +import org.mozilla.gecko.icons.IconResponse; +import org.mozilla.gecko.icons.Icons; +import org.mozilla.gecko.icons.storage.MemoryStorage; +import org.robolectric.RuntimeEnvironment; + +import static org.mockito.Mockito.mock; + +@RunWith(TestRunner.class) +public class TestMemoryLoader { + private static final String TEST_PAGE_URL = "http://www.mozilla.org"; + private static final String TEST_ICON_URL = "https://example.org/favicon.ico"; + + @Before + public void setUp() { + // Make sure to start with an empty memory cache. + MemoryStorage.get().evictAll(); + } + + @Test + public void testStoringAndLoadingFromMemory() { + final IconRequest request = Icons.with(RuntimeEnvironment.application) + .pageUrl(TEST_PAGE_URL) + .icon(IconDescriptor.createGenericIcon(TEST_ICON_URL)) + .build(); + + final IconLoader loader = new MemoryLoader(); + + Assert.assertNull(loader.load(request)); + + final Bitmap bitmap = mock(Bitmap.class); + final IconResponse response = IconResponse.create(bitmap); + response.updateColor(Color.MAGENTA); + + MemoryStorage.get().putIcon(TEST_ICON_URL, response); + + final IconResponse loadedResponse = loader.load(request); + + Assert.assertNotNull(loadedResponse); + Assert.assertEquals(bitmap, loadedResponse.getBitmap()); + Assert.assertEquals(Color.MAGENTA, loadedResponse.getColor()); + } + + @Test + public void testNothingIsLoadedIfMemoryShouldBeSkipped() { + final IconRequest request = Icons.with(RuntimeEnvironment.application) + .pageUrl(TEST_PAGE_URL) + .icon(IconDescriptor.createGenericIcon(TEST_ICON_URL)) + .skipMemory() + .build(); + + final IconLoader loader = new MemoryLoader(); + + Assert.assertNull(loader.load(request)); + + final Bitmap bitmap = mock(Bitmap.class); + final IconResponse response = IconResponse.create(bitmap); + + MemoryStorage.get().putIcon(TEST_ICON_URL, response); + + Assert.assertNull(loader.load(request)); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/preparation/TestAboutPagesPreparer.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/preparation/TestAboutPagesPreparer.java new file mode 100644 index 000000000..f0d4cb7e2 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/preparation/TestAboutPagesPreparer.java @@ -0,0 +1,73 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +package org.mozilla.gecko.icons.preparation; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.AboutPages; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.icons.IconRequest; +import org.mozilla.gecko.icons.Icons; +import org.robolectric.RuntimeEnvironment; + +@RunWith(TestRunner.class) +public class TestAboutPagesPreparer { + private static final String[] ABOUT_PAGES = { + AboutPages.ACCOUNTS, + AboutPages.ADDONS, + AboutPages.CONFIG, + AboutPages.DOWNLOADS, + AboutPages.FIREFOX, + AboutPages.HEALTHREPORT, + AboutPages.HOME, + AboutPages.UPDATER + }; + + @Test + public void testPreparerAddsUrlsForAllAboutPages() { + final Preparer preparer = new AboutPagesPreparer(); + + for (String url : ABOUT_PAGES) { + final IconRequest request = Icons.with(RuntimeEnvironment.application) + .pageUrl(url) + .build(); + + Assert.assertEquals(0, request.getIconCount()); + + preparer.prepare(request); + + Assert.assertEquals("Added icon URL for URL: " + url, 1, request.getIconCount()); + } + } + + @Test + public void testPrepareDoesNotAddUrlForGenericHttpUrl() { + final IconRequest request = Icons.with(RuntimeEnvironment.application) + .pageUrl("http://www.mozilla.org") + .build(); + + Assert.assertEquals(0, request.getIconCount()); + + final Preparer preparer = new AboutPagesPreparer(); + preparer.prepare(request); + + Assert.assertEquals(0, request.getIconCount()); + } + + @Test + public void testAddedUrlHasJarScheme() { + final IconRequest request = Icons.with(RuntimeEnvironment.application) + .pageUrl(AboutPages.DOWNLOADS) + .build(); + + final Preparer preparer = new AboutPagesPreparer(); + preparer.prepare(request); + + Assert.assertEquals(1, request.getIconCount()); + + final String url = request.getBestIcon().getUrl(); + Assert.assertNotNull(url); + Assert.assertTrue(url.startsWith("jar:jar:")); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/preparation/TestAddDefaultIconUrl.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/preparation/TestAddDefaultIconUrl.java new file mode 100644 index 000000000..ce5e82d0b --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/preparation/TestAddDefaultIconUrl.java @@ -0,0 +1,79 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.icons.preparation; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.icons.IconDescriptor; +import org.mozilla.gecko.icons.IconRequest; +import org.mozilla.gecko.icons.Icons; +import org.robolectric.RuntimeEnvironment; + +import java.util.Iterator; + +@RunWith(TestRunner.class) +public class TestAddDefaultIconUrl { + @Test + public void testAddingDefaultUrl() { + final IconRequest request = Icons.with(RuntimeEnvironment.application) + .pageUrl("http://www.mozilla.org") + .icon(IconDescriptor.createTouchicon( + "https://www.mozilla.org/media/img/favicon/apple-touch-icon-180x180.00050c5b754e.png", + 180, + "image/png")) + .icon(IconDescriptor.createFavicon( + "https://www.mozilla.org/media/img/favicon.52506929be4c.ico", + 32, + "image/x-icon")) + .icon(IconDescriptor.createFavicon( + "jar:jar:wtf.png", + 16, + "image/png")) + .build(); + + + Assert.assertEquals(3, request.getIconCount()); + Assert.assertFalse(containsUrl(request, "http://www.mozilla.org/favicon.ico")); + + Preparer preparer = new AddDefaultIconUrl(); + preparer.prepare(request); + + Assert.assertEquals(4, request.getIconCount()); + Assert.assertTrue(containsUrl(request, "http://www.mozilla.org/favicon.ico")); + } + + @Test + public void testDefaultUrlIsNotAddedIfItAlreadyExists() { + final IconRequest request = Icons.with(RuntimeEnvironment.application) + .pageUrl("http://www.mozilla.org") + .icon(IconDescriptor.createFavicon( + "http://www.mozilla.org/favicon.ico", + 32, + "image/x-icon")) + .build(); + + Assert.assertEquals(1, request.getIconCount()); + + Preparer preparer = new AddDefaultIconUrl(); + preparer.prepare(request); + + Assert.assertEquals(1, request.getIconCount()); + } + + private boolean containsUrl(IconRequest request, String url) { + final Iterator<IconDescriptor> iterator = request.getIconIterator(); + + while (iterator.hasNext()) { + IconDescriptor descriptor = iterator.next(); + + if (descriptor.getUrl().equals(url)) { + return true; + } + } + + return false; + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/preparation/TestFilterKnownFailureUrls.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/preparation/TestFilterKnownFailureUrls.java new file mode 100644 index 000000000..67584c4cf --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/preparation/TestFilterKnownFailureUrls.java @@ -0,0 +1,60 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.icons.preparation; + +import junit.framework.Assert; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.icons.IconDescriptor; +import org.mozilla.gecko.icons.IconRequest; +import org.mozilla.gecko.icons.Icons; +import org.mozilla.gecko.icons.storage.FailureCache; +import org.robolectric.RuntimeEnvironment; + +@RunWith(TestRunner.class) +public class TestFilterKnownFailureUrls { + private static final String TEST_PAGE_URL = "http://www.mozilla.org"; + private static final String TEST_ICON_URL = "https://example.org/favicon.ico"; + + @Before + public void setUp() { + // Make sure we always start with an empty cache. + FailureCache.get().evictAll(); + } + + @Test + public void testFilterDoesNothingByDefault() { + final IconRequest request = Icons.with(RuntimeEnvironment.application) + .pageUrl(TEST_PAGE_URL) + .icon(IconDescriptor.createGenericIcon(TEST_ICON_URL)) + .build(); + + Assert.assertEquals(1, request.getIconCount()); + + final Preparer preparer = new FilterKnownFailureUrls(); + preparer.prepare(request); + + Assert.assertEquals(1, request.getIconCount()); + } + + @Test + public void testFilterKnownFailureUrls() { + final IconRequest request = Icons.with(RuntimeEnvironment.application) + .pageUrl(TEST_PAGE_URL) + .icon(IconDescriptor.createGenericIcon(TEST_ICON_URL)) + .build(); + + Assert.assertEquals(1, request.getIconCount()); + + FailureCache.get().rememberFailure(TEST_ICON_URL); + + final Preparer preparer = new FilterKnownFailureUrls(); + preparer.prepare(request); + + Assert.assertEquals(0, request.getIconCount()); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/preparation/TestFilterMimeTypes.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/preparation/TestFilterMimeTypes.java new file mode 100644 index 000000000..e8339b4e9 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/preparation/TestFilterMimeTypes.java @@ -0,0 +1,67 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.icons.preparation; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.icons.IconDescriptor; +import org.mozilla.gecko.icons.IconRequest; +import org.mozilla.gecko.icons.Icons; +import org.robolectric.RuntimeEnvironment; + +@RunWith(TestRunner.class) +public class TestFilterMimeTypes { + private static final String TEST_PAGE_URL = "http://www.mozilla.org"; + private static final String TEST_ICON_URL = "https://example.org/favicon.ico"; + private static final String TEST_ICON_URL_2 = "https://mozilla.org/favicon.ico"; + + @Test + public void testUrlsWithoutMimeTypesAreNotFiltered() { + final IconRequest request = Icons.with(RuntimeEnvironment.application) + .pageUrl(TEST_PAGE_URL) + .icon(IconDescriptor.createGenericIcon(TEST_ICON_URL)) + .build(); + + Assert.assertEquals(1, request.getIconCount()); + + final Preparer preparer = new FilterMimeTypes(); + preparer.prepare(request); + + Assert.assertEquals(1, request.getIconCount()); + } + + @Test + public void testUnknownMimeTypesAreFiltered() { + final IconRequest request = Icons.with(RuntimeEnvironment.application) + .pageUrl(TEST_PAGE_URL) + .icon(IconDescriptor.createFavicon(TEST_ICON_URL, 256, "image/zaphod")) + .icon(IconDescriptor.createFavicon(TEST_ICON_URL_2, 128, "audio/mpeg")) + .build(); + + Assert.assertEquals(2, request.getIconCount()); + + final Preparer preparer = new FilterMimeTypes(); + preparer.prepare(request); + + Assert.assertEquals(0, request.getIconCount()); + } + + @Test + public void testKnownMimeTypesAreNotFiltered() { + final IconRequest request = Icons.with(RuntimeEnvironment.application) + .pageUrl(TEST_PAGE_URL) + .icon(IconDescriptor.createFavicon(TEST_ICON_URL, 256, "image/x-icon")) + .icon(IconDescriptor.createFavicon(TEST_ICON_URL_2, 128, "image/png")) + .build(); + + Assert.assertEquals(2, request.getIconCount()); + + final Preparer preparer = new FilterMimeTypes(); + preparer.prepare(request); + + Assert.assertEquals(2, request.getIconCount()); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/preparation/TestFilterPrivilegedUrls.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/preparation/TestFilterPrivilegedUrls.java new file mode 100644 index 000000000..53fcbd05a --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/preparation/TestFilterPrivilegedUrls.java @@ -0,0 +1,86 @@ +package org.mozilla.gecko.icons.preparation; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.icons.IconDescriptor; +import org.mozilla.gecko.icons.IconRequest; +import org.mozilla.gecko.icons.Icons; +import org.robolectric.RuntimeEnvironment; + +import java.util.Iterator; + +@RunWith(TestRunner.class) +public class TestFilterPrivilegedUrls { + private static final String TEST_PAGE_URL = "http://www.mozilla.org"; + + private static final String TEST_ICON_HTTP_URL = "https://www.mozilla.org/media/img/favicon/apple-touch-icon-180x180.00050c5b754e.png"; + private static final String TEST_ICON_HTTP_URL_2 = "https://www.mozilla.org/media/img/favicon.52506929be4c.ico"; + private static final String TEST_ICON_JAR_URL = "jar:jar:wtf.png"; + + @Test + public void testFiltering() { + final IconRequest request = Icons.with(RuntimeEnvironment.application) + .pageUrl(TEST_PAGE_URL) + .icon(IconDescriptor.createGenericIcon(TEST_ICON_HTTP_URL)) + .icon(IconDescriptor.createGenericIcon(TEST_ICON_HTTP_URL_2)) + .icon(IconDescriptor.createGenericIcon(TEST_ICON_JAR_URL)) + .build(); + + Assert.assertEquals(3, request.getIconCount()); + + Assert.assertTrue(containsUrl(request, TEST_ICON_HTTP_URL)); + Assert.assertTrue(containsUrl(request, TEST_ICON_HTTP_URL_2)); + Assert.assertTrue(containsUrl(request, TEST_ICON_JAR_URL)); + + Preparer preparer = new FilterPrivilegedUrls(); + preparer.prepare(request); + + Assert.assertEquals(2, request.getIconCount()); + + Assert.assertTrue(containsUrl(request, TEST_ICON_HTTP_URL)); + Assert.assertTrue(containsUrl(request, TEST_ICON_HTTP_URL_2)); + Assert.assertFalse(containsUrl(request, TEST_ICON_JAR_URL)); + } + + @Test + public void testNothingIsFilteredForPrivilegedRequests() { + final IconRequest request = Icons.with(RuntimeEnvironment.application) + .pageUrl(TEST_PAGE_URL) + .icon(IconDescriptor.createGenericIcon(TEST_ICON_HTTP_URL)) + .icon(IconDescriptor.createGenericIcon(TEST_ICON_HTTP_URL_2)) + .icon(IconDescriptor.createGenericIcon(TEST_ICON_JAR_URL)) + .privileged(true) + .build(); + + Assert.assertEquals(3, request.getIconCount()); + + Assert.assertTrue(containsUrl(request, TEST_ICON_HTTP_URL)); + Assert.assertTrue(containsUrl(request, TEST_ICON_HTTP_URL_2)); + Assert.assertTrue(containsUrl(request, TEST_ICON_JAR_URL)); + + Preparer preparer = new FilterPrivilegedUrls(); + preparer.prepare(request); + + Assert.assertEquals(3, request.getIconCount()); + + Assert.assertTrue(containsUrl(request, TEST_ICON_HTTP_URL)); + Assert.assertTrue(containsUrl(request, TEST_ICON_HTTP_URL_2)); + Assert.assertTrue(containsUrl(request, TEST_ICON_JAR_URL)); + } + + private boolean containsUrl(IconRequest request, String url) { + final Iterator<IconDescriptor> iterator = request.getIconIterator(); + + while (iterator.hasNext()) { + IconDescriptor descriptor = iterator.next(); + + if (descriptor.getUrl().equals(url)) { + return true; + } + } + + return false; + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/preparation/TestLookupIconUrl.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/preparation/TestLookupIconUrl.java new file mode 100644 index 000000000..99bac076b --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/preparation/TestLookupIconUrl.java @@ -0,0 +1,101 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.icons.preparation; + +import junit.framework.Assert; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.icons.IconRequest; +import org.mozilla.gecko.icons.Icons; +import org.mozilla.gecko.icons.storage.DiskStorage; +import org.mozilla.gecko.icons.storage.MemoryStorage; +import org.robolectric.RuntimeEnvironment; + +@RunWith(TestRunner.class) +public class TestLookupIconUrl { + private static final String TEST_PAGE_URL = "http://www.mozilla.org"; + + private static final String TEST_ICON_URL_1 = "http://www.mozilla.org/favicon.ico"; + private static final String TEST_ICON_URL_2 = "http://example.org/favicon.ico"; + private static final String TEST_ICON_URL_3 = "http://example.com/favicon.ico"; + private static final String TEST_ICON_URL_4 = "http://example.net/favicon.ico"; + + + @Before + public void setUp() { + MemoryStorage.get().evictAll(); + } + + @Test + public void testNoIconUrlIsAddedByDefault() { + final IconRequest request = Icons.with(RuntimeEnvironment.application) + .pageUrl(TEST_PAGE_URL) + .build(); + + Assert.assertEquals(0, request.getIconCount()); + + Preparer preparer = new LookupIconUrl(); + preparer.prepare(request); + + Assert.assertEquals(0, request.getIconCount()); + } + + @Test + public void testIconUrlIsAddedFromMemory() { + final IconRequest request = Icons.with(RuntimeEnvironment.application) + .pageUrl(TEST_PAGE_URL) + .build(); + + MemoryStorage.get().putMapping(request, TEST_ICON_URL_1); + + Assert.assertEquals(0, request.getIconCount()); + + Preparer preparer = new LookupIconUrl(); + preparer.prepare(request); + + Assert.assertEquals(1, request.getIconCount()); + + Assert.assertEquals(TEST_ICON_URL_1, request.getBestIcon().getUrl()); + } + + @Test + public void testIconUrlIsAddedFromDisk() { + final IconRequest request = Icons.with(RuntimeEnvironment.application) + .pageUrl(TEST_PAGE_URL) + .build(); + + DiskStorage.get(RuntimeEnvironment.application).putMapping(request, TEST_ICON_URL_2); + + Assert.assertEquals(0, request.getIconCount()); + + Preparer preparer = new LookupIconUrl(); + preparer.prepare(request); + + Assert.assertEquals(1, request.getIconCount()); + + Assert.assertEquals(TEST_ICON_URL_2, request.getBestIcon().getUrl()); + } + + @Test + public void testIconUrlIsAddedFromMemoryBeforeUsingDiskStorage() { + final IconRequest request = Icons.with(RuntimeEnvironment.application) + .pageUrl(TEST_PAGE_URL) + .build(); + + MemoryStorage.get().putMapping(request, TEST_ICON_URL_3); + DiskStorage.get(RuntimeEnvironment.application).putMapping(request, TEST_ICON_URL_4); + + Assert.assertEquals(0, request.getIconCount()); + + Preparer preparer = new LookupIconUrl(); + preparer.prepare(request); + + Assert.assertEquals(1, request.getIconCount()); + + Assert.assertEquals(TEST_ICON_URL_3, request.getBestIcon().getUrl()); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/processing/TestColorProcessor.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/processing/TestColorProcessor.java new file mode 100644 index 000000000..6057c0776 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/processing/TestColorProcessor.java @@ -0,0 +1,59 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.icons.processing; + +import android.graphics.Bitmap; +import android.graphics.Color; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.icons.IconResponse; + +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; + +@RunWith(TestRunner.class) +public class TestColorProcessor { + @Test + public void testExtractingColor() { + final IconResponse response = IconResponse.create(createRedBitmapMock()); + + Assert.assertFalse(response.hasColor()); + Assert.assertEquals(0, response.getColor()); + + final Processor processor = new ColorProcessor(); + processor.process(null, response); + + Assert.assertTrue(response.hasColor()); + Assert.assertEquals(Color.RED, response.getColor()); + } + + private Bitmap createRedBitmapMock() { + final Bitmap bitmap = mock(Bitmap.class); + + doReturn(1).when(bitmap).getWidth(); + doReturn(1).when(bitmap).getHeight(); + + doAnswer(new Answer<Void>() { + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + Object[] args = invocation.getArguments(); + int[] pixels = (int[]) args[0]; + for (int i = 0; i < pixels.length; i++) { + pixels[i] = Color.RED; + } + return null; + } + }).when(bitmap).getPixels(any(int[].class), anyInt(), anyInt(), anyInt(), anyInt(), anyInt(), anyInt()); + + return bitmap; + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/processing/TestDiskProcessor.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/processing/TestDiskProcessor.java new file mode 100644 index 000000000..eea5c9bf6 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/processing/TestDiskProcessor.java @@ -0,0 +1,100 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.icons.processing; + +import android.graphics.Bitmap; +import android.graphics.Color; + +import junit.framework.Assert; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.icons.IconDescriptor; +import org.mozilla.gecko.icons.IconRequest; +import org.mozilla.gecko.icons.IconResponse; +import org.mozilla.gecko.icons.Icons; +import org.mozilla.gecko.icons.storage.DiskStorage; +import org.robolectric.RuntimeEnvironment; + +import java.io.OutputStream; + +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; + +@RunWith(TestRunner.class) +public class TestDiskProcessor { + private static final String PAGE_URL = "https://www.mozilla.org"; + private static final String ICON_URL = "https://www.mozilla.org/favicon.ico"; + + @Test + public void testNetworkResponseIsStoredInCache() { + final IconRequest request = createTestRequest(); + final IconResponse response = createTestNetworkResponse(); + + final DiskStorage storage = DiskStorage.get(RuntimeEnvironment.application); + Assert.assertNull(storage.getIcon(ICON_URL)); + + final Processor processor = new DiskProcessor(); + processor.process(request, response); + + Assert.assertNotNull(storage.getIcon(ICON_URL)); + } + + @Test + public void testGeneratedResponseIsNotStored() { + final IconRequest request = createTestRequest(); + final IconResponse response = createGeneratedResponse(); + + final DiskStorage storage = DiskStorage.get(RuntimeEnvironment.application); + Assert.assertNull(storage.getIcon(ICON_URL)); + + final Processor processor = new DiskProcessor(); + processor.process(request, response); + + Assert.assertNull(storage.getIcon(ICON_URL)); + } + + @Test + public void testNothingIsStoredIfDiskShouldBeSkipped() { + final IconRequest request = createTestRequest() + .modify() + .skipDisk() + .build(); + final IconResponse response = createTestNetworkResponse(); + + final DiskStorage storage = DiskStorage.get(RuntimeEnvironment.application); + Assert.assertNull(storage.getIcon(ICON_URL)); + + final Processor processor = new DiskProcessor(); + processor.process(request, response); + + Assert.assertNull(storage.getIcon(ICON_URL)); + } + + private IconRequest createTestRequest() { + return Icons.with(RuntimeEnvironment.application) + .pageUrl(PAGE_URL) + .icon(IconDescriptor.createGenericIcon(ICON_URL)) + .build(); + } + + public IconResponse createTestNetworkResponse() { + return IconResponse.createFromNetwork(createMockedBitmap(), ICON_URL); + } + + public IconResponse createGeneratedResponse() { + return IconResponse.createGenerated(createMockedBitmap(), Color.WHITE); + } + + private Bitmap createMockedBitmap() { + final Bitmap bitmap = mock(Bitmap.class); + + doReturn(true).when(bitmap).compress(any(Bitmap.CompressFormat.class), anyInt(), any(OutputStream.class)); + + return bitmap; + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/processing/TestMemoryProcessor.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/processing/TestMemoryProcessor.java new file mode 100644 index 000000000..fbc1e0baf --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/processing/TestMemoryProcessor.java @@ -0,0 +1,134 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.icons.processing; + +import android.graphics.Bitmap; +import android.graphics.Color; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.icons.IconDescriptor; +import org.mozilla.gecko.icons.IconRequest; +import org.mozilla.gecko.icons.IconResponse; +import org.mozilla.gecko.icons.Icons; +import org.mozilla.gecko.icons.storage.MemoryStorage; +import org.robolectric.RuntimeEnvironment; + +import static org.mockito.Mockito.mock; + +@RunWith(TestRunner.class) +public class TestMemoryProcessor { + private static final String PAGE_URL = "https://www.mozilla.org"; + private static final String ICON_URL = "https://www.mozilla.org/favicon.ico"; + private static final String DATA_URL = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAAEklEQVR4AWP4z8AAxCDiP8N/AB3wBPxcBee7AAAAAElFTkSuQmCC"; + + @Before + public void setUp() { + MemoryStorage.get().evictAll(); + } + + @Test + public void testResponsesAreStoredInMemory() { + final IconRequest request = createTestRequest(); + + Assert.assertNull(MemoryStorage.get().getIcon(ICON_URL)); + Assert.assertNull(MemoryStorage.get().getMapping(PAGE_URL)); + + final Processor processor = new MemoryProcessor(); + processor.process(request, createTestResponse()); + + Assert.assertNotNull(MemoryStorage.get().getIcon(ICON_URL)); + Assert.assertNotNull(MemoryStorage.get().getMapping(PAGE_URL)); + } + + @Test + public void testNothingIsStoredIfMemoryShouldBeSkipped() { + final IconRequest request = createTestRequest() + .modify() + .skipMemory() + .build(); + + Assert.assertNull(MemoryStorage.get().getIcon(ICON_URL)); + Assert.assertNull(MemoryStorage.get().getMapping(PAGE_URL)); + + final Processor processor = new MemoryProcessor(); + processor.process(request, createTestResponse()); + + Assert.assertNull(MemoryStorage.get().getIcon(ICON_URL)); + Assert.assertNull(MemoryStorage.get().getMapping(PAGE_URL)); + } + + @Test + public void testNothingIsStoredForRequestsWithoutUrl() { + final IconRequest request = createTestRequestWithoutIconUrl(); + + Assert.assertNull(MemoryStorage.get().getIcon(ICON_URL)); + Assert.assertNull(MemoryStorage.get().getMapping(PAGE_URL)); + + final Processor processor = new MemoryProcessor(); + processor.process(request, createTestResponse()); + + Assert.assertNull(MemoryStorage.get().getIcon(ICON_URL)); + Assert.assertNull(MemoryStorage.get().getMapping(PAGE_URL)); + } + + @Test + public void testNothingIsStoredForGeneratedResponses() { + final IconRequest request = createTestRequest(); + + Assert.assertNull(MemoryStorage.get().getIcon(ICON_URL)); + Assert.assertNull(MemoryStorage.get().getMapping(PAGE_URL)); + + final Processor processor = new MemoryProcessor(); + processor.process(request, createGeneratedTestResponse()); + + Assert.assertNull(MemoryStorage.get().getIcon(ICON_URL)); + Assert.assertNull(MemoryStorage.get().getMapping(PAGE_URL)); + } + + @Test + public void testNothingIsStoredForDataUris() { + final IconRequest request = createDataUriTestRequest(); + + Assert.assertNull(MemoryStorage.get().getIcon(DATA_URL)); + Assert.assertNull(MemoryStorage.get().getMapping(PAGE_URL)); + + final Processor processor = new MemoryProcessor(); + processor.process(request, createTestResponse()); + + Assert.assertNull(MemoryStorage.get().getIcon(DATA_URL)); + Assert.assertNull(MemoryStorage.get().getMapping(PAGE_URL)); + } + + private IconRequest createTestRequest() { + return Icons.with(RuntimeEnvironment.application) + .pageUrl(PAGE_URL) + .icon(IconDescriptor.createGenericIcon(ICON_URL)) + .build(); + } + + private IconRequest createTestRequestWithoutIconUrl() { + return Icons.with(RuntimeEnvironment.application) + .pageUrl(PAGE_URL) + .build(); + } + + private IconRequest createDataUriTestRequest() { + return Icons.with(RuntimeEnvironment.application) + .pageUrl(PAGE_URL) + .icon(IconDescriptor.createGenericIcon(DATA_URL)) + .build(); + } + + private IconResponse createTestResponse() { + return IconResponse.create(mock(Bitmap.class)); + } + + private IconResponse createGeneratedTestResponse() { + return IconResponse.createGenerated(mock(Bitmap.class), Color.GREEN); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/processing/TestResizingProcessor.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/processing/TestResizingProcessor.java new file mode 100644 index 000000000..dbcb4e2ee --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/processing/TestResizingProcessor.java @@ -0,0 +1,111 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.icons.processing; + +import android.graphics.Bitmap; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.icons.IconDescriptor; +import org.mozilla.gecko.icons.IconRequest; +import org.mozilla.gecko.icons.IconResponse; +import org.mozilla.gecko.icons.Icons; +import org.robolectric.RuntimeEnvironment; + +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +@RunWith(TestRunner.class) +public class TestResizingProcessor { + private static final String PAGE_URL = "https://www.mozilla.org"; + private static final String ICON_URL = "https://www.mozilla.org/favicon.ico"; + + @Test + public void testBitmapIsNotResizedIfItAlreadyHasTheTargetSize() { + final IconRequest request = createTestRequest(); + + final Bitmap bitmap = createBitmapMock(request.getTargetSize()); + final IconResponse response = spy(IconResponse.create(bitmap)); + + final ResizingProcessor processor = spy(new ResizingProcessor()); + processor.process(request, response); + + verify(processor, never()).resize(any(Bitmap.class), anyInt()); + verify(bitmap, never()).recycle(); + verify(response, never()).updateBitmap(any(Bitmap.class)); + } + + @Test + public void testLargerBitmapsAreResized() { + final IconRequest request = createTestRequest(); + + final Bitmap bitmap = createBitmapMock(request.getTargetSize() * 2); + final IconResponse response = spy(IconResponse.create(bitmap)); + + final ResizingProcessor processor = spy(new ResizingProcessor()); + final Bitmap resizedBitmap = mock(Bitmap.class); + doReturn(resizedBitmap).when(processor).resize(any(Bitmap.class), anyInt()); + processor.process(request, response); + + verify(processor).resize(bitmap, request.getTargetSize()); + verify(bitmap).recycle(); + verify(response).updateBitmap(resizedBitmap); + } + + @Test + public void testBitmapIsUpscaledToTargetSize() { + final IconRequest request = createTestRequest(); + + final Bitmap bitmap = createBitmapMock(request.getTargetSize() / 2 + 1); + final IconResponse response = spy(IconResponse.create(bitmap)); + + final ResizingProcessor processor = spy(new ResizingProcessor()); + final Bitmap resizedBitmap = mock(Bitmap.class); + doReturn(resizedBitmap).when(processor).resize(any(Bitmap.class), anyInt()); + processor.process(request, response); + + verify(processor).resize(bitmap, request.getTargetSize()); + verify(bitmap).recycle(); + verify(response).updateBitmap(resizedBitmap); + } + + @Test + public void testBitmapIsNotScaledMoreThanTwoTimesTheSize() { + final IconRequest request = createTestRequest(); + + final Bitmap bitmap = createBitmapMock(5); + final IconResponse response = spy(IconResponse.create(bitmap)); + + final ResizingProcessor processor = spy(new ResizingProcessor()); + final Bitmap resizedBitmap = mock(Bitmap.class); + doReturn(resizedBitmap).when(processor).resize(any(Bitmap.class), anyInt()); + processor.process(request, response); + + verify(processor).resize(bitmap, 10); + verify(bitmap).recycle(); + verify(response).updateBitmap(resizedBitmap); + } + + private IconRequest createTestRequest() { + return Icons.with(RuntimeEnvironment.application) + .pageUrl(PAGE_URL) + .icon(IconDescriptor.createGenericIcon(ICON_URL)) + .build(); + } + + private Bitmap createBitmapMock(int size) { + final Bitmap bitmap = mock(Bitmap.class); + + doReturn(size).when(bitmap).getWidth(); + doReturn(size).when(bitmap).getHeight(); + + return bitmap; + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/permissions/TestPermissions.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/permissions/TestPermissions.java new file mode 100644 index 000000000..07fbab493 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/permissions/TestPermissions.java @@ -0,0 +1,253 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.permissions; + +import android.Manifest; +import android.app.Activity; +import android.content.Context; +import android.content.pm.PackageManager; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Matchers; + +import org.mozilla.gecko.background.testhelpers.TestRunner; + +import static org.mockito.Mockito.*; + +@RunWith(TestRunner.class) +public class TestPermissions { + @Test + public void testSuccessRunnableIsExecutedIfPermissionsAreGranted() { + Permissions.setPermissionHelper(mockGrantingHelper()); + + Runnable onPermissionsGranted = mock(Runnable.class); + Runnable onPermissionsDenied = mock(Runnable.class); + + Permissions.from(mockActivity()) + .withPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE) + .andFallback(onPermissionsDenied) + .run(onPermissionsGranted); + + verify(onPermissionsDenied, never()).run(); + verify(onPermissionsGranted).run(); + } + + @Test + public void testFallbackRunnableIsExecutedIfPermissionsAreDenied() { + Permissions.setPermissionHelper(mockDenyingHelper()); + + Runnable onPermissionsGranted = mock(Runnable.class); + Runnable onPermissionsDenied = mock(Runnable.class); + + Activity activity = mockActivity(); + + Permissions.from(activity) + .withPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE) + .andFallback(onPermissionsDenied) + .run(onPermissionsGranted); + + Permissions.onRequestPermissionsResult(activity, new String[]{ + Manifest.permission.WRITE_EXTERNAL_STORAGE + }, new int[]{ + PackageManager.PERMISSION_DENIED + }); + + verify(onPermissionsDenied).run(); + verify(onPermissionsGranted, never()).run(); + } + + @Test + public void testPromptingForNotGrantedPermissions() { + Activity activity = mockActivity(); + + PermissionsHelper helper = mockDenyingHelper(); + Permissions.setPermissionHelper(helper); + + Permissions.from(activity) + .withPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE) + .andFallback(mock(Runnable.class)) + .run(mock(Runnable.class)); + + verify(helper).prompt(anyActivity(), any(String[].class)); + + Permissions.onRequestPermissionsResult(activity, new String[0], new int[0]); + } + + @Test + public void testMultipleRequestsAreQueuedAndDispatchedSequentially() { + Activity activity = mockActivity(); + + PermissionsHelper helper = mockDenyingHelper(); + Permissions.setPermissionHelper(helper); + + Runnable onFirstPermissionGranted = mock(Runnable.class); + Runnable onSecondPermissionDenied = mock(Runnable.class); + + Permissions.from(activity) + .withPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE) + .andFallback(mock(Runnable.class)) + .run(onFirstPermissionGranted); + + Permissions.from(activity) + .withPermissions(Manifest.permission.CAMERA) + .andFallback(onSecondPermissionDenied) + .run(mock(Runnable.class)); + + + Permissions.onRequestPermissionsResult(activity, new String[] { + Manifest.permission.WRITE_EXTERNAL_STORAGE + }, new int[] { + PackageManager.PERMISSION_GRANTED + }); + + verify(onFirstPermissionGranted).run(); + verify(onSecondPermissionDenied, never()).run(); // Second request is queued but not executed yet + + Permissions.onRequestPermissionsResult(activity, new String[]{ + Manifest.permission.CAMERA + }, new int[]{ + PackageManager.PERMISSION_DENIED + }); + + verify(onFirstPermissionGranted).run(); + verify(onSecondPermissionDenied).run(); + + verify(helper, times(2)).prompt(anyActivity(), any(String[].class)); + } + + @Test + public void testSecondRequestWillNotPromptIfPermissionHasBeenGranted() { + Activity activity = mockActivity(); + + PermissionsHelper helper = mock(PermissionsHelper.class); + Permissions.setPermissionHelper(helper); + when(helper.hasPermissions(anyContext(), anyPermissions())) + .thenReturn(false) + .thenReturn(false) + .thenReturn(true); // Revaluation is successful + + Permissions.from(activity) + .withPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE) + .andFallback(mock(Runnable.class)) + .run(mock(Runnable.class)); + + Permissions.from(activity) + .withPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE) + .andFallback(mock(Runnable.class)) + .run(mock(Runnable.class)); + + Permissions.onRequestPermissionsResult(activity, new String[]{ + Manifest.permission.WRITE_EXTERNAL_STORAGE + }, new int[]{ + PackageManager.PERMISSION_GRANTED + }); + + verify(helper, times(1)).prompt(anyActivity(), any(String[].class)); + } + + @Test + public void testEmptyPermissionsArrayWillExecuteRunnableAndNotTryToPrompt() { + PermissionsHelper helper = spy(new PermissionsHelper()); + Permissions.setPermissionHelper(helper); + + Runnable onPermissionGranted = mock(Runnable.class); + Runnable onPermissionDenied = mock(Runnable.class); + + Permissions.from(mockActivity()) + .withPermissions() + .andFallback(onPermissionDenied) + .run(onPermissionGranted); + + verify(onPermissionGranted).run(); + verify(onPermissionDenied, never()).run(); + verify(helper, never()).prompt(anyActivity(), any(String[].class)); + } + + @Test + public void testDoNotPromptBehavior() { + PermissionsHelper helper = mockDenyingHelper(); + Permissions.setPermissionHelper(helper); + + Permissions.from(mockActivity()) + .withPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE) + .doNotPrompt() + .andFallback(mock(Runnable.class)) + .run(mock(Runnable.class)); + + verify(helper, never()).prompt(anyActivity(), any(String[].class)); + } + + @Test(expected = IllegalStateException.class) + public void testThrowsExceptionIfNeedstoPromptWithNonActivityContext() { + Permissions.setPermissionHelper(mockDenyingHelper()); + + Permissions.from(mock(Context.class)) + .withPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE) + .andFallback(mock(Runnable.class)) + .run(mock(Runnable.class)); + } + + @Test + public void testDoNotPromptIfFalse() { + Activity activity = mockActivity(); + + PermissionsHelper helper = mockDenyingHelper(); + Permissions.setPermissionHelper(helper); + + Permissions.from(activity) + .withPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE) + .doNotPromptIf(false) + .andFallback(mock(Runnable.class)) + .run(mock(Runnable.class)); + + verify(helper).prompt(anyActivity(), any(String[].class)); + + Permissions.onRequestPermissionsResult(activity, new String[0], new int[0]); + } + + @Test + public void testDoNotPromptIfTrue() { + PermissionsHelper helper = mockDenyingHelper(); + Permissions.setPermissionHelper(helper); + + Permissions.from(mockActivity()) + .withPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE) + .doNotPromptIf(true) + .andFallback(mock(Runnable.class)) + .run(mock(Runnable.class)); + + verify(helper, never()).prompt(anyActivity(), any(String[].class)); + } + + private Activity mockActivity() { + return mock(Activity.class); + } + + private PermissionsHelper mockGrantingHelper() { + PermissionsHelper helper = mock(PermissionsHelper.class); + doReturn(true).when(helper).hasPermissions(any(Context.class), anyPermissions()); + return helper; + } + + private PermissionsHelper mockDenyingHelper() { + PermissionsHelper helper = mock(PermissionsHelper.class); + doReturn(false).when(helper).hasPermissions(any(Context.class), anyPermissions()); + return helper; + } + + private String anyPermissions() { + return Matchers.anyVararg(); + } + + private Activity anyActivity() { + return any(Activity.class); + } + + private Context anyContext() { + return any(Context.class); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/push/TestPushManager.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/push/TestPushManager.java new file mode 100644 index 000000000..42ae0f543 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/push/TestPushManager.java @@ -0,0 +1,238 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.push; + +import org.json.JSONObject; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.AppConstants; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.gcm.GcmTokenClient; +import org.robolectric.RuntimeEnvironment; + +import java.util.UUID; + +import static org.mockito.Matchers.anyBoolean; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Matchers.isNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + + +@RunWith(TestRunner.class) +public class TestPushManager { + private PushState state; + private GcmTokenClient gcmTokenClient; + private PushClient pushClient; + private PushManager manager; + + @Before + public void setUp() throws Exception { + state = new PushState(RuntimeEnvironment.application, "test.json"); + gcmTokenClient = mock(GcmTokenClient.class); + doReturn(new Fetched("opaque-gcm-token", System.currentTimeMillis())).when(gcmTokenClient).getToken(anyString(), anyBoolean()); + + // Configure a mock PushClient. + pushClient = mock(PushClient.class); + doReturn(new RegisterUserAgentResponse("opaque-uaid", "opaque-secret")) + .when(pushClient) + .registerUserAgent(anyString()); + + doReturn(new SubscribeChannelResponse("opaque-chid", "https://localhost:8085/opaque-push-endpoint")) + .when(pushClient) + .subscribeChannel(anyString(), anyString(), isNull(String.class)); + + PushManager.PushClientFactory pushClientFactory = mock(PushManager.PushClientFactory.class); + doReturn(pushClient).when(pushClientFactory).getPushClient(anyString(), anyBoolean()); + + manager = new PushManager(state, gcmTokenClient, pushClientFactory); + } + + private void assertOnlyConfigured(PushRegistration registration, String endpoint, boolean debug) { + Assert.assertNotNull(registration); + Assert.assertEquals(registration.autopushEndpoint, endpoint); + Assert.assertEquals(registration.debug, debug); + Assert.assertNull(registration.uaid.value); + } + + private void assertRegistered(PushRegistration registration, String endpoint, boolean debug) { + Assert.assertNotNull(registration); + Assert.assertEquals(registration.autopushEndpoint, endpoint); + Assert.assertEquals(registration.debug, debug); + Assert.assertNotNull(registration.uaid.value); + } + + private void assertSubscribed(PushSubscription subscription) { + Assert.assertNotNull(subscription); + Assert.assertNotNull(subscription.chid); + } + + @Test + public void testConfigure() throws Exception { + PushRegistration registration = manager.configure("default", "http://localhost:8081", false, System.currentTimeMillis()); + assertOnlyConfigured(registration, "http://localhost:8081", false); + + registration = manager.configure("default", "http://localhost:8082", true, System.currentTimeMillis()); + assertOnlyConfigured(registration, "http://localhost:8082", true); + } + + @Test(expected=PushManager.ProfileNeedsConfigurationException.class) + public void testRegisterBeforeConfigure() throws Exception { + PushRegistration registration = state.getRegistration("default"); + Assert.assertNull(registration); + + // Trying to register a User Agent fails before configuration. + manager.registerUserAgent("default", System.currentTimeMillis()); + } + + @Test + public void testRegister() throws Exception { + PushRegistration registration = manager.configure("default", "http://localhost:8082", false, System.currentTimeMillis()); + assertOnlyConfigured(registration, "http://localhost:8082", false); + + // Let's register a User Agent, so that we can witness unregistration. + registration = manager.registerUserAgent("default", System.currentTimeMillis()); + assertRegistered(registration, "http://localhost:8082", false); + + // Changing the debug flag should update but not try to unregister the User Agent. + registration = manager.configure("default", "http://localhost:8082", true, System.currentTimeMillis()); + assertRegistered(registration, "http://localhost:8082", true); + + // Changing the configuration endpoint should update and try to unregister the User Agent. + registration = manager.configure("default", "http://localhost:8083", true, System.currentTimeMillis()); + assertOnlyConfigured(registration, "http://localhost:8083", true); + } + + @Test + public void testRegisterMultipleProfiles() throws Exception { + PushRegistration registration1 = manager.configure("default1", "http://localhost:8081", true, System.currentTimeMillis()); + PushRegistration registration2 = manager.configure("default2", "http://localhost:8082", true, System.currentTimeMillis()); + assertOnlyConfigured(registration1, "http://localhost:8081", true); + assertOnlyConfigured(registration2, "http://localhost:8082", true); + verify(gcmTokenClient, times(0)).getToken(anyString(), anyBoolean()); + + registration1 = manager.registerUserAgent("default1", System.currentTimeMillis()); + assertRegistered(registration1, "http://localhost:8081", true); + + registration2 = manager.registerUserAgent("default2", System.currentTimeMillis()); + assertRegistered(registration2, "http://localhost:8082", true); + + // Just the debug flag should not unregister the User Agent. + registration1 = manager.configure("default1", "http://localhost:8081", false, System.currentTimeMillis()); + assertRegistered(registration1, "http://localhost:8081", false); + + // But the configuration endpoint should unregister the correct User Agent. + registration2 = manager.configure("default2", "http://localhost:8083", false, System.currentTimeMillis()); + } + + @Test + public void testSubscribeChannel() throws Exception { + manager.configure("default", "http://localhost:8080", false, System.currentTimeMillis()); + PushRegistration registration = manager.registerUserAgent("default", System.currentTimeMillis()); + assertRegistered(registration, "http://localhost:8080", false); + + // We should be able to register with non-null serviceData. + final JSONObject webpushData = new JSONObject(); + webpushData.put("version", 5); + PushSubscription subscription = manager.subscribeChannel("default", "webpush", webpushData, null, System.currentTimeMillis()); + assertSubscribed(subscription); + + subscription = manager.registrationForSubscription(subscription.chid).getSubscription(subscription.chid); + Assert.assertNotNull(subscription); + Assert.assertEquals(5, subscription.serviceData.get("version")); + + // We should be able to register with null serviceData. + subscription = manager.subscribeChannel("default", "sync", null, null, System.currentTimeMillis()); + assertSubscribed(subscription); + + subscription = manager.registrationForSubscription(subscription.chid).getSubscription(subscription.chid); + Assert.assertNotNull(subscription); + Assert.assertNull(subscription.serviceData); + } + + @Test + public void testUnsubscribeChannel() throws Exception { + manager.configure("default", "http://localhost:8080", false, System.currentTimeMillis()); + PushRegistration registration = manager.registerUserAgent("default", System.currentTimeMillis()); + assertRegistered(registration, "http://localhost:8080", false); + + // We should be able to register with non-null serviceData. + final JSONObject webpushData = new JSONObject(); + webpushData.put("version", 5); + PushSubscription subscription = manager.subscribeChannel("default", "webpush", webpushData, null, System.currentTimeMillis()); + assertSubscribed(subscription); + + // No exception is success. + manager.unsubscribeChannel(subscription.chid); + } + + public void testUnsubscribeUnknownChannel() throws Exception { + manager.configure("default", "http://localhost:8080", false, System.currentTimeMillis()); + PushRegistration registration = manager.registerUserAgent("default", System.currentTimeMillis()); + assertRegistered(registration, "http://localhost:8080", false); + + doThrow(new RuntimeException()) + .when(pushClient) + .unsubscribeChannel(anyString(), anyString(), anyString()); + + // Un-subscribing from an unknown channel succeeds: we just ignore the request. + manager.unsubscribeChannel(UUID.randomUUID().toString()); + } + + @Test + public void testStartupBeforeConfiguration() throws Exception { + verify(gcmTokenClient, never()).getToken(anyString(), anyBoolean()); + manager.startup(System.currentTimeMillis()); + verify(gcmTokenClient, times(1)).getToken(AppConstants.MOZ_ANDROID_GCM_SENDERID, false); + } + + @Test + public void testStartupBeforeRegistration() throws Exception { + PushRegistration registration = manager.configure("default", "http://localhost:8080", true, System.currentTimeMillis()); + assertOnlyConfigured(registration, "http://localhost:8080", true); + + manager.startup(System.currentTimeMillis()); + verify(gcmTokenClient, times(1)).getToken(anyString(), anyBoolean()); + } + + @Test + public void testStartupAfterRegistration() throws Exception { + PushRegistration registration = manager.configure("default", "http://localhost:8080", true, System.currentTimeMillis()); + assertOnlyConfigured(registration, "http://localhost:8080", true); + + registration = manager.registerUserAgent("default", System.currentTimeMillis()); + assertRegistered(registration, "http://localhost:8080", true); + + manager.startup(System.currentTimeMillis()); + + // Rather tautological. + PushRegistration updatedRegistration = manager.state.getRegistration("default"); + Assert.assertEquals(registration.uaid, updatedRegistration.uaid); + } + + @Test + public void testStartupAfterSubscription() throws Exception { + PushRegistration registration = manager.configure("default", "http://localhost:8080", true, System.currentTimeMillis()); + assertOnlyConfigured(registration, "http://localhost:8080", true); + + registration = manager.registerUserAgent("default", System.currentTimeMillis()); + assertRegistered(registration, "http://localhost:8080", true); + + PushSubscription subscription = manager.subscribeChannel("default", "webpush", null, null, System.currentTimeMillis()); + assertSubscribed(subscription); + + manager.startup(System.currentTimeMillis()); + + // Rather tautological. + registration = manager.registrationForSubscription(subscription.chid); + PushSubscription updatedSubscription = registration.getSubscription(subscription.chid); + Assert.assertEquals(subscription.chid, updatedSubscription.chid); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/push/TestPushState.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/push/TestPushState.java new file mode 100644 index 000000000..cb7c7ec68 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/push/TestPushState.java @@ -0,0 +1,70 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.push; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.robolectric.RuntimeEnvironment; + +import java.io.File; +import java.io.FileOutputStream; + +@RunWith(TestRunner.class) +public class TestPushState { + @Test + public void testRoundTrip() throws Exception { + final PushState state = new PushState(RuntimeEnvironment.application, "test.json"); + // Fresh state should have no registrations (and no subscriptions). + Assert.assertTrue(state.registrations.isEmpty()); + + final PushRegistration registration = new PushRegistration("endpoint", true, Fetched.now("uaid"), "secret"); + final PushSubscription subscription = new PushSubscription("chid", "profileName", "webpushEndpoint", "service", null); + registration.putSubscription("chid", subscription); + state.putRegistration("profileName", registration); + Assert.assertEquals(1, state.registrations.size()); + state.checkpoint(); + + final PushState readState = new PushState(RuntimeEnvironment.application, "test.json"); + Assert.assertEquals(1, readState.registrations.size()); + final PushRegistration storedRegistration = readState.getRegistration("profileName"); + Assert.assertEquals(registration, storedRegistration); + + Assert.assertEquals(1, storedRegistration.subscriptions.size()); + final PushSubscription storedSubscription = storedRegistration.getSubscription("chid"); + Assert.assertEquals(subscription, storedSubscription); + } + + @Test + public void testMissingRegistration() throws Exception { + final PushState state = new PushState(RuntimeEnvironment.application, "testMissingRegistration.json"); + Assert.assertNull(state.getRegistration("missingProfileName")); + } + + @Test + public void testMissingSubscription() throws Exception { + final PushRegistration registration = new PushRegistration("endpoint", true, Fetched.now("uaid"), "secret"); + Assert.assertNull(registration.getSubscription("missingChid")); + } + + @Test + public void testCorruptedJSON() throws Exception { + // Write some malformed JSON. + // TODO: use mcomella's helpers! + final File file = new File(RuntimeEnvironment.application.getApplicationInfo().dataDir, "testCorruptedJSON.json"); + FileOutputStream fos = null; + try { + fos = new FileOutputStream(file); + fos.write("}".getBytes("UTF-8")); + } finally { + if (fos != null) { + fos.close(); + } + } + + final PushState state = new PushState(RuntimeEnvironment.application, "testCorruptedJSON.json"); + Assert.assertTrue(state.getRegistrations().isEmpty()); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/push/autopush/test/TestAutopushClient.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/push/autopush/test/TestAutopushClient.java new file mode 100644 index 000000000..93e0d14e5 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/push/autopush/test/TestAutopushClient.java @@ -0,0 +1,30 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.push.autopush.test; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.background.testhelpers.WaitHelper; +import org.mozilla.gecko.push.autopush.AutopushClient; +import org.mozilla.gecko.push.autopush.AutopushClientException; +import org.mozilla.gecko.sync.Utils; + +@RunWith(TestRunner.class) +public class TestAutopushClient { + @Test + public void testGetSenderID() throws Exception { + final AutopushClient client = new AutopushClient("https://updates-autopush-dev.stage.mozaws.net/v1/gcm/829133274407", + Utils.newSynchronousExecutor()); + Assert.assertEquals("829133274407", client.getSenderIDFromServerURI()); + } + + @Test(expected=AutopushClientException.class) + public void testGetNoSenderID() throws Exception { + final AutopushClient client = new AutopushClient("https://updates-autopush-dev.stage.mozaws.net/v1/gcm", + Utils.newSynchronousExecutor()); + client.getSenderIDFromServerURI(); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/push/autopush/test/TestLiveAutopushClient.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/push/autopush/test/TestLiveAutopushClient.java new file mode 100644 index 000000000..102ea34e4 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/push/autopush/test/TestLiveAutopushClient.java @@ -0,0 +1,171 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.push.autopush.test; + +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.PublicKey; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mozilla.apache.commons.codec.binary.Base64; +import org.mozilla.gecko.background.fxa.FxAccountUtils; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.background.testhelpers.WaitHelper; +import org.mozilla.gecko.push.RegisterUserAgentResponse; +import org.mozilla.gecko.push.SubscribeChannelResponse; +import org.mozilla.gecko.push.autopush.AutopushClient; +import org.mozilla.gecko.push.autopush.AutopushClient.RequestDelegate; +import org.mozilla.gecko.push.autopush.AutopushClientException; +import org.mozilla.gecko.sync.Utils; +import org.mozilla.gecko.sync.net.BaseResource; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.startsWith; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +/** + * This test straddles an awkward line: it uses Mockito, but doesn't actually mock the service + * endpoint. That's why it's a <b>live</b> test: most of its value is checking that the client + * implementation and the upstream server implementation are corresponding correctly. + */ +@RunWith(TestRunner.class) +@Ignore("Live test that requires network connection -- remove this line to run this test.") +public class TestLiveAutopushClient { + final String serverURL = "https://updates-autopush.stage.mozaws.net/v1/gcm/829133274407"; + + protected AutopushClient client; + + @Before + public void setUp() throws Exception { + BaseResource.rewriteLocalhost = false; + client = new AutopushClient(serverURL, Utils.newSynchronousExecutor()); + } + + protected <T> T assertSuccess(RequestDelegate<T> delegate, Class<T> klass) { + verify(delegate, never()).handleError(any(Exception.class)); + verify(delegate, never()).handleFailure(any(AutopushClientException.class)); + + final ArgumentCaptor<T> register = ArgumentCaptor.forClass(klass); + verify(delegate).handleSuccess(register.capture()); + + return register.getValue(); + } + + protected <T> AutopushClientException assertFailure(RequestDelegate<T> delegate, Class<T> klass) { + verify(delegate, never()).handleError(any(Exception.class)); + verify(delegate, never()).handleSuccess(any(klass)); + + final ArgumentCaptor<AutopushClientException> failure = ArgumentCaptor.forClass(AutopushClientException.class); + verify(delegate).handleFailure(failure.capture()); + + return failure.getValue(); + } + + @Test + public void testUserAgent() throws Exception { + final RequestDelegate<RegisterUserAgentResponse> registerDelegate = mock(RequestDelegate.class); + client.registerUserAgent(Utils.generateGuid(), registerDelegate); + + final RegisterUserAgentResponse registerResponse = assertSuccess(registerDelegate, RegisterUserAgentResponse.class); + Assert.assertNotNull(registerResponse); + Assert.assertNotNull(registerResponse.uaid); + Assert.assertNotNull(registerResponse.secret); + + // Reregistering with a new GUID should succeed. + final RequestDelegate<Void> reregisterDelegate = mock(RequestDelegate.class); + client.reregisterUserAgent(registerResponse.uaid, registerResponse.secret, Utils.generateGuid(), reregisterDelegate); + + Assert.assertNull(assertSuccess(reregisterDelegate, Void.class)); + + // Unregistering should succeed. + final RequestDelegate<Void> unregisterDelegate = mock(RequestDelegate.class); + client.unregisterUserAgent(registerResponse.uaid, registerResponse.secret, unregisterDelegate); + + Assert.assertNull(assertSuccess(unregisterDelegate, Void.class)); + + // Trying to unregister a second time should give a 404. + final RequestDelegate<Void> reunregisterDelegate = mock(RequestDelegate.class); + client.unregisterUserAgent(registerResponse.uaid, registerResponse.secret, reunregisterDelegate); + + final AutopushClientException failureException = assertFailure(reunregisterDelegate, Void.class); + Assert.assertThat(failureException, instanceOf(AutopushClientException.AutopushClientRemoteException.class)); + Assert.assertTrue(((AutopushClientException.AutopushClientRemoteException) failureException).isGone()); + } + + @Test + public void testChannel() throws Exception { + final RequestDelegate<RegisterUserAgentResponse> registerDelegate = mock(RequestDelegate.class); + client.registerUserAgent(Utils.generateGuid(), registerDelegate); + + final RegisterUserAgentResponse registerResponse = assertSuccess(registerDelegate, RegisterUserAgentResponse.class); + Assert.assertNotNull(registerResponse); + Assert.assertNotNull(registerResponse.uaid); + Assert.assertNotNull(registerResponse.secret); + + // We should be able to subscribe to a channel. + final RequestDelegate<SubscribeChannelResponse> subscribeDelegate = mock(RequestDelegate.class); + client.subscribeChannel(registerResponse.uaid, registerResponse.secret, null, subscribeDelegate); + + final SubscribeChannelResponse subscribeResponse = assertSuccess(subscribeDelegate, SubscribeChannelResponse.class); + Assert.assertNotNull(subscribeResponse); + Assert.assertNotNull(subscribeResponse.channelID); + Assert.assertNotNull(subscribeResponse.endpoint); + Assert.assertThat(subscribeResponse.endpoint, startsWith(FxAccountUtils.getAudienceForURL(serverURL))); + Assert.assertThat(subscribeResponse.endpoint, containsString("/v1/")); + + // And we should be able to unsubscribe. + final RequestDelegate<Void> unsubscribeDelegate = mock(RequestDelegate.class); + client.unsubscribeChannel(registerResponse.uaid, registerResponse.secret, subscribeResponse.channelID, unsubscribeDelegate); + + Assert.assertNull(assertSuccess(unsubscribeDelegate, Void.class)); + + // We should be able to create a restricted subscription by specifying + // an ECDSA public key using the P-256 curve. + final RequestDelegate<SubscribeChannelResponse> subscribeWithKeyDelegate = mock(RequestDelegate.class); + final KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("ECDSA"); + keyPairGenerator.initialize(256); + final KeyPair keyPair = keyPairGenerator.generateKeyPair(); + final PublicKey publicKey = keyPair.getPublic(); + String appServerKey = Base64.encodeBase64URLSafeString(publicKey.getEncoded()); + client.subscribeChannel(registerResponse.uaid, registerResponse.secret, appServerKey, subscribeWithKeyDelegate); + + final SubscribeChannelResponse subscribeWithKeyResponse = assertSuccess(subscribeWithKeyDelegate, SubscribeChannelResponse.class); + Assert.assertNotNull(subscribeWithKeyResponse); + Assert.assertNotNull(subscribeWithKeyResponse.channelID); + Assert.assertNotNull(subscribeWithKeyResponse.endpoint); + Assert.assertThat(subscribeWithKeyResponse.endpoint, startsWith(FxAccountUtils.getAudienceForURL(serverURL))); + Assert.assertThat(subscribeWithKeyResponse.endpoint, containsString("/v2/")); + + // And we should be able to drop the restricted subscription. + final RequestDelegate<Void> unsubscribeWithKeyDelegate = mock(RequestDelegate.class); + client.unsubscribeChannel(registerResponse.uaid, registerResponse.secret, subscribeWithKeyResponse.channelID, unsubscribeWithKeyDelegate); + + Assert.assertNull(assertSuccess(unsubscribeWithKeyDelegate, Void.class)); + + // Trying to unsubscribe a second time should give a 410. + final RequestDelegate<Void> reunsubscribeDelegate = mock(RequestDelegate.class); + client.unsubscribeChannel(registerResponse.uaid, registerResponse.secret, subscribeResponse.channelID, reunsubscribeDelegate); + + final AutopushClientException reunsubscribeFailureException = assertFailure(reunsubscribeDelegate, Void.class); + Assert.assertThat(reunsubscribeFailureException, instanceOf(AutopushClientException.AutopushClientRemoteException.class)); + Assert.assertTrue(((AutopushClientException.AutopushClientRemoteException) reunsubscribeFailureException).isGone()); + + // Trying to unsubscribe from a non-existent channel should give a 404. Right now it gives a 401! + final RequestDelegate<Void> badUnsubscribeDelegate = mock(RequestDelegate.class); + client.unsubscribeChannel(registerResponse.uaid + "BAD", registerResponse.secret, subscribeResponse.channelID, badUnsubscribeDelegate); + + final AutopushClientException badUnsubscribeFailureException = assertFailure(badUnsubscribeDelegate, Void.class); + Assert.assertThat(badUnsubscribeFailureException, instanceOf(AutopushClientException.AutopushClientRemoteException.class)); + Assert.assertTrue(((AutopushClientException.AutopushClientRemoteException) badUnsubscribeFailureException).isInvalidAuthentication()); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/crypto/test/TestBase32.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/crypto/test/TestBase32.java new file mode 100644 index 000000000..7047d67d3 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/crypto/test/TestBase32.java @@ -0,0 +1,53 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.sync.crypto.test; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.apache.commons.codec.binary.Base32; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.sync.Utils; + +import java.io.UnsupportedEncodingException; +import java.util.Arrays; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +@RunWith(TestRunner.class) +public class TestBase32 { + + public static void assertSame(byte[] arrayOne, byte[] arrayTwo) { + assertTrue(Arrays.equals(arrayOne, arrayTwo)); + } + + @Test + public void testBase32() throws UnsupportedEncodingException { + byte[] decoded = new Base32().decode("MZXW6YTBOI======"); + byte[] expected = "foobar".getBytes(); + assertSame(decoded, expected); + + byte[] encoded = new Base32().encode("fooba".getBytes()); + expected = "MZXW6YTB".getBytes(); + assertSame(encoded, expected); + } + + @Test + public void testFriendlyBase32() { + // These checks are drawn from Firefox, test_utils_encodeBase32.js. + byte[] decoded = Utils.decodeFriendlyBase32("mzxw6ytb9jrgcztpn5rgc4tcme"); + byte[] expected = "foobarbafoobarba".getBytes(); + assertEquals(decoded.length, 16); + assertSame(decoded, expected); + + // These are real values extracted from the Service object in a Firefox profile. + String base32Key = "6m8mv8ex2brqnrmsb9fjuvfg7y"; + String expectedHex = "f316caac97d06306c5920b8a9a54a6fe"; + + byte[] computedBytes = Utils.decodeFriendlyBase32(base32Key); + byte[] expectedBytes = Utils.hex2Byte(expectedHex); + + assertSame(computedBytes, expectedBytes); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/crypto/test/TestCryptoInfo.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/crypto/test/TestCryptoInfo.java new file mode 100644 index 000000000..3e8d90e2f --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/crypto/test/TestCryptoInfo.java @@ -0,0 +1,144 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.sync.crypto.test; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.apache.commons.codec.binary.Base64; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.sync.Utils; +import org.mozilla.gecko.sync.crypto.CryptoException; +import org.mozilla.gecko.sync.crypto.CryptoInfo; +import org.mozilla.gecko.sync.crypto.KeyBundle; + +import java.io.UnsupportedEncodingException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +@RunWith(TestRunner.class) +public class TestCryptoInfo { + + @Test + public void testEncryptedHMACIsSet() throws CryptoException, UnsupportedEncodingException, InvalidKeyException, NoSuchAlgorithmException { + KeyBundle kb = KeyBundle.withRandomKeys(); + CryptoInfo encrypted = CryptoInfo.encrypt("plaintext".getBytes("UTF-8"), kb); + assertSame(kb, encrypted.getKeys()); + assertTrue(encrypted.generatedHMACIsHMAC()); + } + + @Test + public void testRandomEncryptedDecrypted() throws CryptoException, UnsupportedEncodingException, InvalidKeyException, NoSuchAlgorithmException { + KeyBundle kb = KeyBundle.withRandomKeys(); + byte[] plaintext = "plaintext".getBytes("UTF-8"); + CryptoInfo info = CryptoInfo.encrypt(plaintext, kb); + byte[] iv = info.getIV(); + info.decrypt(); + assertArrayEquals(plaintext, info.getMessage()); + assertSame(null, info.getHMAC()); + assertArrayEquals(iv, info.getIV()); + assertSame(kb, info.getKeys()); + } + + @Test + public void testDecrypt() throws CryptoException { + 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"; + String base64EncryptionKey = "9K/wLdXdw+nrTtXo4ZpECyHFNr4d7aYH" + + "qeg3KW9+m6Q="; + String base64HmacKey = "MMntEfutgLTc8FlTLQFms8/xMPmCldqP" + + "lq/QQXEjx70="; + String base64ExpectedBytes = "eyJpZCI6IjVxUnNnWFdSSlpYciIsImhp" + + "c3RVcmkiOiJmaWxlOi8vL1VzZXJzL2ph" + + "c29uL0xpYnJhcnkvQXBwbGljYXRpb24l" + + "MjBTdXBwb3J0L0ZpcmVmb3gvUHJvZmls" + + "ZXMva3NnZDd3cGsuTG9jYWxTeW5jU2Vy" + + "dmVyL3dlYXZlL2xvZ3MvIiwidGl0bGUi" + + "OiJJbmRleCBvZiBmaWxlOi8vL1VzZXJz" + + "L2phc29uL0xpYnJhcnkvQXBwbGljYXRp" + + "b24gU3VwcG9ydC9GaXJlZm94L1Byb2Zp" + + "bGVzL2tzZ2Q3d3BrLkxvY2FsU3luY1Nl" + + "cnZlci93ZWF2ZS9sb2dzLyIsInZpc2l0" + + "cyI6W3siZGF0ZSI6MTMxOTE0OTAxMjM3" + + "MjQyNSwidHlwZSI6MX1dfQ=="; + + CryptoInfo decrypted = CryptoInfo.decrypt( + Base64.decodeBase64(base64CipherText), + Base64.decodeBase64(base64IV), + Utils.hex2Byte(base16Hmac), + new KeyBundle( + Base64.decodeBase64(base64EncryptionKey), + Base64.decodeBase64(base64HmacKey)) + ); + + assertArrayEquals(decrypted.getMessage(), Base64.decodeBase64(base64ExpectedBytes)); + } + + @Test + public void testEncrypt() throws CryptoException { + 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"; + String base64EncryptionKey = "9K/wLdXdw+nrTtXo4ZpECyHFNr4d7aYH" + + "qeg3KW9+m6Q="; + String base64HmacKey = "MMntEfutgLTc8FlTLQFms8/xMPmCldqP" + + "lq/QQXEjx70="; + String base64ExpectedBytes = "eyJpZCI6IjVxUnNnWFdSSlpYciIsImhp" + + "c3RVcmkiOiJmaWxlOi8vL1VzZXJzL2ph" + + "c29uL0xpYnJhcnkvQXBwbGljYXRpb24l" + + "MjBTdXBwb3J0L0ZpcmVmb3gvUHJvZmls" + + "ZXMva3NnZDd3cGsuTG9jYWxTeW5jU2Vy" + + "dmVyL3dlYXZlL2xvZ3MvIiwidGl0bGUi" + + "OiJJbmRleCBvZiBmaWxlOi8vL1VzZXJz" + + "L2phc29uL0xpYnJhcnkvQXBwbGljYXRp" + + "b24gU3VwcG9ydC9GaXJlZm94L1Byb2Zp" + + "bGVzL2tzZ2Q3d3BrLkxvY2FsU3luY1Nl" + + "cnZlci93ZWF2ZS9sb2dzLyIsInZpc2l0" + + "cyI6W3siZGF0ZSI6MTMxOTE0OTAxMjM3" + + "MjQyNSwidHlwZSI6MX1dfQ=="; + + CryptoInfo encrypted = CryptoInfo.encrypt( + Base64.decodeBase64(base64ExpectedBytes), + Base64.decodeBase64(base64IV), + new KeyBundle( + Base64.decodeBase64(base64EncryptionKey), + Base64.decodeBase64(base64HmacKey)) + ); + + assertArrayEquals(Base64.decodeBase64(base64CipherText), encrypted.getMessage()); + assertArrayEquals(Utils.hex2Byte(base16Hmac), encrypted.getHMAC()); + } +}
\ No newline at end of file diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/crypto/test/TestHKDF.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/crypto/test/TestHKDF.java new file mode 100644 index 000000000..09973eeff --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/crypto/test/TestHKDF.java @@ -0,0 +1,143 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.sync.crypto.test; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.apache.commons.codec.binary.Base64; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.sync.Utils; +import org.mozilla.gecko.sync.crypto.HKDF; +import org.mozilla.gecko.sync.crypto.KeyBundle; + +import java.util.Arrays; + +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/* + * This class tests the HKDF.java class. + * The tests are the 3 HMAC-based test cases + * from the RFC 5869 specification. + */ +@RunWith(TestRunner.class) +public class TestHKDF { + @Test + public void testCase1() { + String IKM = "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b"; + String salt = "000102030405060708090a0b0c"; + String info = "f0f1f2f3f4f5f6f7f8f9"; + int L = 42; + String PRK = "077709362c2e32df0ddc3f0dc47bba63" + + "90b6c73bb50f9c3122ec844ad7c2b3e5"; + String OKM = "3cb25f25faacd57a90434f64d0362f2a" + + "2d2d0a90cf1a5a4c5db02d56ecc4c5bf" + + "34007208d5b887185865"; + + assertTrue(doStep1(IKM, salt, PRK)); + assertTrue(doStep2(PRK, info, L, OKM)); + } + + @Test + public void testCase2() { + String IKM = "000102030405060708090a0b0c0d0e0f" + + "101112131415161718191a1b1c1d1e1f" + + "202122232425262728292a2b2c2d2e2f" + + "303132333435363738393a3b3c3d3e3f" + + "404142434445464748494a4b4c4d4e4f"; + String salt = "606162636465666768696a6b6c6d6e6f" + + "707172737475767778797a7b7c7d7e7f" + + "808182838485868788898a8b8c8d8e8f" + + "909192939495969798999a9b9c9d9e9f" + + "a0a1a2a3a4a5a6a7a8a9aaabacadaeaf"; + String info = "b0b1b2b3b4b5b6b7b8b9babbbcbdbebf" + + "c0c1c2c3c4c5c6c7c8c9cacbcccdcecf" + + "d0d1d2d3d4d5d6d7d8d9dadbdcdddedf" + + "e0e1e2e3e4e5e6e7e8e9eaebecedeeef" + + "f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff"; + int L = 82; + String PRK = "06a6b88c5853361a06104c9ceb35b45c" + + "ef760014904671014a193f40c15fc244"; + String OKM = "b11e398dc80327a1c8e7f78c596a4934" + + "4f012eda2d4efad8a050cc4c19afa97c" + + "59045a99cac7827271cb41c65e590e09" + + "da3275600c2f09b8367793a9aca3db71" + + "cc30c58179ec3e87c14c01d5c1f3434f" + + "1d87"; + + assertTrue(doStep1(IKM, salt, PRK)); + assertTrue(doStep2(PRK, info, L, OKM)); + } + + @Test + public void testCase3() { + String IKM = "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b"; + String salt = ""; + String info = ""; + int L = 42; + String PRK = "19ef24a32c717b167f33a91d6f648bdf" + + "96596776afdb6377ac434c1c293ccb04"; + String OKM = "8da4e775a563c18f715f802a063c5a31" + + "b8a11f5c5ee1879ec3454e5f3c738d2d" + + "9d201395faa4b61a96c8"; + + assertTrue(doStep1(IKM, salt, PRK)); + assertTrue(doStep2(PRK, info, L, OKM)); + } + + /* + * Tests the code for getting the keys necessary to + * decrypt the crypto keys bundle for Mozilla Sync. + * + * This operation is just a tailored version of the + * standard to get only the 2 keys we need. + */ + @Test + public void testGetCryptoKeysBundleKeys() { + String username = "smqvooxj664hmrkrv6bw4r4vkegjhkns"; + String friendlyBase32SyncKey = "gbh7teqqcgyzd65svjgibd7tqy"; + String base64EncryptionKey = "069EnS3EtDK4y1tZ1AyKX+U7WEsWRp9bRIKLdW/7aoE="; + String base64HmacKey = "LF2YCS1QCgSNCf0BCQvQ06SGH8jqJDi9dKj0O+b0fwI="; + + KeyBundle bundle = null; + try { + bundle = new KeyBundle(username, friendlyBase32SyncKey); + } catch (Exception e) { + fail("Unexpected exception " + e); + } + + byte[] expectedEncryptionKey = Base64.decodeBase64(base64EncryptionKey); + byte[] expectedHMACKey = Base64.decodeBase64(base64HmacKey); + assertTrue(Arrays.equals(bundle.getEncryptionKey(), expectedEncryptionKey)); + assertTrue(Arrays.equals(bundle.getHMACKey(), expectedHMACKey)); + } + + /* + * Helper to do step 1 of RFC 5869. + */ + private boolean doStep1(String IKM, String salt, String PRK) { + try { + byte[] prkResult = HKDF.hkdfExtract(Utils.hex2Byte(salt), Utils.hex2Byte(IKM)); + byte[] prkExpect = Utils.hex2Byte(PRK); + return Arrays.equals(prkResult, prkExpect); + } catch (Exception e) { + fail("Unexpected exception " + e); + } + return false; + } + + /* + * Helper to do step 2 of RFC 5869. + */ + private boolean doStep2(String PRK, String info, int L, String OKM) { + try { + byte[] okmResult = HKDF.hkdfExpand(Utils.hex2Byte(PRK), Utils.hex2Byte(info), L); + byte[] okmExpect = Utils.hex2Byte(OKM); + return Arrays.equals(okmResult, okmExpect); + } catch (Exception e) { + fail("Unexpected exception " + e); + } + return false; + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/crypto/test/TestKeyBundle.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/crypto/test/TestKeyBundle.java new file mode 100644 index 000000000..3c3edb9f8 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/crypto/test/TestKeyBundle.java @@ -0,0 +1,65 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.sync.crypto.test; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.apache.commons.codec.binary.Base64; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.sync.crypto.CryptoException; +import org.mozilla.gecko.sync.crypto.KeyBundle; + +import java.io.UnsupportedEncodingException; +import java.util.Arrays; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +@RunWith(TestRunner.class) +public class TestKeyBundle { + @Test + public void testCreateKeyBundle() throws UnsupportedEncodingException, CryptoException { + String username = "smqvooxj664hmrkrv6bw4r4vkegjhkns"; + String friendlyBase32SyncKey = "gbh7teqqcgyzd65svjgibd7tqy"; + String base64EncryptionKey = "069EnS3EtDK4y1tZ1AyKX+U7WEsWRp9b" + + "RIKLdW/7aoE="; + String base64HmacKey = "LF2YCS1QCgSNCf0BCQvQ06SGH8jqJDi9" + + "dKj0O+b0fwI="; + + KeyBundle keys = new KeyBundle(username, friendlyBase32SyncKey); + assertArrayEquals(keys.getEncryptionKey(), Base64.decodeBase64(base64EncryptionKey.getBytes("UTF-8"))); + assertArrayEquals(keys.getHMACKey(), Base64.decodeBase64(base64HmacKey.getBytes("UTF-8"))); + } + + /* + * Basic sanity check to make sure length of keys is correct (32 bytes). + * Also make sure that the two keys are different. + */ + @Test + public void testGenerateRandomKeys() throws CryptoException { + KeyBundle keys = KeyBundle.withRandomKeys(); + + assertEquals(32, keys.getEncryptionKey().length); + assertEquals(32, keys.getHMACKey().length); + + boolean equal = Arrays.equals(keys.getEncryptionKey(), keys.getHMACKey()); + assertEquals(false, equal); + } + + @Test + public void testEquals() throws CryptoException { + KeyBundle k = KeyBundle.withRandomKeys(); + KeyBundle o = KeyBundle.withRandomKeys(); + assertFalse(k.equals("test")); + assertFalse(k.equals(o)); + assertTrue(k.equals(k)); + assertTrue(o.equals(o)); + o.setHMACKey(k.getHMACKey()); + assertFalse(o.equals(k)); + o.setEncryptionKey(k.getEncryptionKey()); + assertTrue(o.equals(k)); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/crypto/test/TestPBKDF2.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/crypto/test/TestPBKDF2.java new file mode 100644 index 000000000..d2d1d8271 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/crypto/test/TestPBKDF2.java @@ -0,0 +1,124 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.sync.crypto.test; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.sync.Utils; +import org.mozilla.gecko.sync.crypto.PBKDF2; + +import java.io.UnsupportedEncodingException; +import java.security.GeneralSecurityException; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +/** + * Test PBKDF2 implementations against vectors from + * <dl> + * <dt>SHA-256</dt> + * <dd><a href="https://github.com/ircmaxell/PHP-PasswordLib/blob/master/test/Data/Vectors/pbkdf2-draft-josefsson-sha256.test-vectors">https://github.com/ircmaxell/PHP-PasswordLib/blob/master/test/Data/Vectors/pbkdf2-draft-josefsson-sha256.test-vectors</a></dd> + * <dd><a href="https://gitorious.org/scrypt/nettle-scrypt/blobs/37c0d5288e991604fe33dba2f1724986a8dddf56/testsuite/pbkdf2-test.c">https://gitorious.org/scrypt/nettle-scrypt/blobs/37c0d5288e991604fe33dba2f1724986a8dddf56/testsuite/pbkdf2-test.c</a></dd> + * </dl> + */ +@RunWith(TestRunner.class) +public class TestPBKDF2 { + + @Test + public final void testPBKDF2SHA256A() throws UnsupportedEncodingException, GeneralSecurityException { + String p = "password"; + String s = "salt"; + int dkLen = 32; + + checkPBKDF2SHA256(p, s, 1, dkLen, "120fb6cffcf8b32c43e7225256c4f837a86548c92ccc35480805987cb70be17b"); + checkPBKDF2SHA256(p, s, 4096, dkLen, "c5e478d59288c841aa530db6845c4c8d962893a001ce4e11a4963873aa98134a"); + } + + @Test + public final void testPBKDF2SHA256B() throws UnsupportedEncodingException, GeneralSecurityException { + String p = "passwordPASSWORDpassword"; + String s = "saltSALTsaltSALTsaltSALTsaltSALTsalt"; + int dkLen = 40; + + checkPBKDF2SHA256(p, s, 4096, dkLen, "348c89dbcbd32b2f32d814b8116e84cf2b17347ebc1800181c4e2a1fb8dd53e1c635518c7dac47e9"); + } + + @Test + public final void testPBKDF2SHA256scryptA() throws UnsupportedEncodingException, GeneralSecurityException { + String p = "passwd"; + String s = "salt"; + int dkLen = 64; + + checkPBKDF2SHA256(p, s, 1, dkLen, "55ac046e56e3089fec1691c22544b605f94185216dde0465e68b9d57c20dacbc49ca9cccf179b645991664b39d77ef317c71b845b1e30bd509112041d3a19783"); + } + + /* + // This test takes eight seconds or so to run, so we don't run it. + @Test + public final void testPBKDF2SHA256scryptB() throws UnsupportedEncodingException, GeneralSecurityException { + String p = "Password"; + String s = "NaCl"; + int dkLen = 64; + + checkPBKDF2SHA256(p, s, 80000, dkLen, "4ddcd8f60b98be21830cee5ef22701f9641a4418d04c0414aeff08876b34ab56a1d425a1225833549adb841b51c9b3176a272bdebba1d078478f62b397f33c8d"); + } + */ + + @Test + public final void testPBKDF2SHA256C() throws UnsupportedEncodingException, GeneralSecurityException { + String p = "pass\0word"; + String s = "sa\0lt"; + int dkLen = 16; + + checkPBKDF2SHA256(p, s, 4096, dkLen, "89b69d0516f829893c696226650a8687"); + } + + /* + // This test takes two or three minutes to run, so we don't run it. + public final void testPBKDF2SHA256D() throws UnsupportedEncodingException, GeneralSecurityException { + String p = "password"; + String s = "salt"; + int dkLen = 32; + + checkPBKDF2SHA256(p, s, 16777216, dkLen, "cf81c66fe8cfc04d1f31ecb65dab4089f7f179e89b3b0bcb17ad10e3ac6eba46"); + } + */ + + /* + // This test takes eight seconds or so to run, so we don't run it. + @Test + public final void testTimePBKDF2SHA256() throws UnsupportedEncodingException, GeneralSecurityException { + checkPBKDF2SHA256("password", "salt", 80000, 32, null); + } + */ + + private void checkPBKDF2SHA256(String p, String s, int c, int dkLen, + final String expectedStr) + throws GeneralSecurityException, UnsupportedEncodingException { + long start = System.currentTimeMillis(); + byte[] key = PBKDF2.pbkdf2SHA256(p.getBytes("US-ASCII"), s.getBytes("US-ASCII"), c, dkLen); + assertNotNull(key); + + long end = System.currentTimeMillis(); + + System.err.println("SHA-256 " + c + " took " + (end - start) + "ms"); + if (expectedStr == null) { + return; + } + + assertEquals(dkLen, Utils.hex2Byte(expectedStr).length); + assertExpectedBytes(expectedStr, key); + } + + public static void assertExpectedBytes(final String expectedStr, byte[] key) { + assertEquals(expectedStr, Utils.byte2Hex(key)); + byte[] expected = Utils.hex2Byte(expectedStr); + + assertEquals(expected.length, key.length); + for (int i = 0; i < key.length; i++) { + assertEquals(expected[i], key[i]); + } + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/crypto/test/TestPersistedCrypto5Keys.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/crypto/test/TestPersistedCrypto5Keys.java new file mode 100644 index 000000000..f5ffc5a8a --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/crypto/test/TestPersistedCrypto5Keys.java @@ -0,0 +1,83 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.sync.crypto.test; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.MockSharedPreferences; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.sync.CollectionKeys; +import org.mozilla.gecko.sync.NoCollectionKeysSetException; +import org.mozilla.gecko.sync.crypto.CryptoException; +import org.mozilla.gecko.sync.crypto.KeyBundle; +import org.mozilla.gecko.sync.crypto.PersistedCrypto5Keys; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +@RunWith(TestRunner.class) +public class TestPersistedCrypto5Keys { + MockSharedPreferences prefs = null; + + @Before + public void setUp() { + prefs = new MockSharedPreferences(); + } + + @Test + public void testPersistLastModified() throws CryptoException, NoCollectionKeysSetException { + long LAST_MODIFIED = System.currentTimeMillis(); + KeyBundle syncKeyBundle = KeyBundle.withRandomKeys(); + PersistedCrypto5Keys persisted = new PersistedCrypto5Keys(prefs, syncKeyBundle); + + // Test fresh start. + assertEquals(-1, persisted.lastModified()); + + // Test persisting. + persisted.persistLastModified(LAST_MODIFIED); + assertEquals(LAST_MODIFIED, persisted.lastModified()); + + // Test clearing. + persisted.persistLastModified(0); + assertEquals(-1, persisted.lastModified()); + } + + @Test + public void testPersistKeys() throws CryptoException, NoCollectionKeysSetException { + KeyBundle syncKeyBundle = KeyBundle.withRandomKeys(); + KeyBundle testKeyBundle = KeyBundle.withRandomKeys(); + + PersistedCrypto5Keys persisted = new PersistedCrypto5Keys(prefs, syncKeyBundle); + + // Test fresh start. + assertNull(persisted.keys()); + + // Test persisting. + CollectionKeys keys = new CollectionKeys(); + keys.setDefaultKeyBundle(syncKeyBundle); + keys.setKeyBundleForCollection("test", testKeyBundle); + persisted.persistKeys(keys); + + CollectionKeys persistedKeys = persisted.keys(); + assertNotNull(persistedKeys); + assertArrayEquals(syncKeyBundle.getEncryptionKey(), persistedKeys.defaultKeyBundle().getEncryptionKey()); + assertArrayEquals(syncKeyBundle.getHMACKey(), persistedKeys.defaultKeyBundle().getHMACKey()); + assertArrayEquals(testKeyBundle.getEncryptionKey(), persistedKeys.keyBundleForCollection("test").getEncryptionKey()); + assertArrayEquals(testKeyBundle.getHMACKey(), persistedKeys.keyBundleForCollection("test").getHMACKey()); + + // Test clearing. + persisted.persistKeys(null); + assertNull(persisted.keys()); + + // Test loading a persisted bundle with wrong syncKeyBundle. + persisted.persistKeys(keys); + assertNotNull(persisted.keys()); + + persisted = new PersistedCrypto5Keys(prefs, testKeyBundle); + assertNull(persisted.keys()); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/crypto/test/TestSRPConstants.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/crypto/test/TestSRPConstants.java new file mode 100644 index 000000000..9188bba24 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/crypto/test/TestSRPConstants.java @@ -0,0 +1,45 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.sync.crypto.test; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.sync.net.SRPConstants; + +import java.math.BigInteger; + +@RunWith(TestRunner.class) +public class TestSRPConstants extends SRPConstants { + public void assertSRPConstants(SRPConstants.Parameters params, int bitLength) { + Assert.assertNotNull(params.g); + Assert.assertNotNull(params.N); + Assert.assertEquals(bitLength, bitLength); + Assert.assertEquals(bitLength / 8, params.byteLength); + Assert.assertEquals(bitLength / 4, params.hexLength); + BigInteger N = params.N; + BigInteger g = params.g; + // Each prime N is of the form 2*q + 1, with q also prime. + BigInteger q = N.subtract(new BigInteger("1")).divide(new BigInteger("2")); + // Check that g is a generator: the order of g is exactly 2*q (not 2, not q). + Assert.assertFalse(new BigInteger("1").equals(g.modPow(new BigInteger("2"), N))); + Assert.assertFalse(new BigInteger("1").equals(g.modPow(q, N))); + Assert.assertTrue(new BigInteger("1").equals(g.modPow((N.subtract(new BigInteger("1"))), N))); + // Even probable primality checking is too expensive to do here. + // Assert.assertTrue(N.isProbablePrime(3)); + // Assert.assertTrue(q.isProbablePrime(3)); + } + + @Test + public void testConstants() { + assertSRPConstants(SRPConstants._1024, 1024); + assertSRPConstants(SRPConstants._1536, 1536); + assertSRPConstants(SRPConstants._2048, 2048); + assertSRPConstants(SRPConstants._3072, 3072); + assertSRPConstants(SRPConstants._4096, 4096); + assertSRPConstants(SRPConstants._6144, 6144); + assertSRPConstants(SRPConstants._8192, 8192); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/middleware/test/TestCrypto5MiddlewareRepositorySession.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/middleware/test/TestCrypto5MiddlewareRepositorySession.java new file mode 100644 index 000000000..d38a4caf2 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/middleware/test/TestCrypto5MiddlewareRepositorySession.java @@ -0,0 +1,291 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.sync.middleware.test; + +import junit.framework.AssertionFailedError; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.android.sync.test.helpers.ExpectSuccessRepositorySessionBeginDelegate; +import org.mozilla.android.sync.test.helpers.ExpectSuccessRepositorySessionCreationDelegate; +import org.mozilla.android.sync.test.helpers.ExpectSuccessRepositorySessionFetchRecordsDelegate; +import org.mozilla.android.sync.test.helpers.ExpectSuccessRepositorySessionFinishDelegate; +import org.mozilla.android.sync.test.helpers.ExpectSuccessRepositorySessionStoreDelegate; +import org.mozilla.android.sync.test.helpers.ExpectSuccessRepositoryWipeDelegate; +import org.mozilla.gecko.background.testhelpers.MockRecord; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.background.testhelpers.WBORepository; +import org.mozilla.gecko.background.testhelpers.WaitHelper; +import org.mozilla.gecko.sync.CryptoRecord; +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.middleware.Crypto5MiddlewareRepository; +import org.mozilla.gecko.sync.middleware.Crypto5MiddlewareRepositorySession; +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.RepositorySession; +import org.mozilla.gecko.sync.repositories.domain.BookmarkRecord; +import org.mozilla.gecko.sync.repositories.domain.Record; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +@RunWith(TestRunner.class) +public class TestCrypto5MiddlewareRepositorySession { + public static WaitHelper getTestWaiter() { + return WaitHelper.getTestWaiter(); + } + + public static void performWait(Runnable runnable) { + getTestWaiter().performWait(runnable); + } + + protected static void performNotify(InactiveSessionException e) { + final AssertionFailedError failed = new AssertionFailedError("Inactive session."); + failed.initCause(e); + getTestWaiter().performNotify(failed); + } + + protected static void performNotify(InvalidSessionTransitionException e) { + final AssertionFailedError failed = new AssertionFailedError("Invalid session transition."); + failed.initCause(e); + getTestWaiter().performNotify(failed); + } + + public Runnable onThreadRunnable(Runnable runnable) { + return WaitHelper.onThreadRunnable(runnable); + } + + public WBORepository wboRepo; + public KeyBundle keyBundle; + public Crypto5MiddlewareRepository cmwRepo; + public Crypto5MiddlewareRepositorySession cmwSession; + + @Before + public void setUp() throws CryptoException { + wboRepo = new WBORepository(); + keyBundle = KeyBundle.withRandomKeys(); + cmwRepo = new Crypto5MiddlewareRepository(wboRepo, keyBundle); + cmwSession = null; + } + + /** + * Run `runnable` in performWait(... onBeginSucceeded { } ). + * + * The Crypto5MiddlewareRepositorySession is available in self.cmwSession. + * + * @param runnable + */ + public void runInOnBeginSucceeded(final Runnable runnable) { + final TestCrypto5MiddlewareRepositorySession self = this; + performWait(onThreadRunnable(new Runnable() { + @Override + public void run() { + cmwRepo.createSession(new ExpectSuccessRepositorySessionCreationDelegate(getTestWaiter()) { + @Override + public void onSessionCreated(RepositorySession session) { + self.cmwSession = (Crypto5MiddlewareRepositorySession)session; + assertSame(RepositorySession.SessionStatus.UNSTARTED, cmwSession.getStatus()); + + try { + session.begin(new ExpectSuccessRepositorySessionBeginDelegate(getTestWaiter()) { + @Override + public void onBeginSucceeded(RepositorySession _session) { + assertSame(self.cmwSession, _session); + runnable.run(); + } + }); + } catch (InvalidSessionTransitionException e) { + TestCrypto5MiddlewareRepositorySession.performNotify(e); + } + } + }, null); + } + })); + } + + @Test + /** + * Verify that the status is actually being advanced. + */ + public void testStatus() { + runInOnBeginSucceeded(new Runnable() { + @Override public void run() { + assertSame(RepositorySession.SessionStatus.ACTIVE, cmwSession.getStatus()); + try { + cmwSession.finish(new ExpectSuccessRepositorySessionFinishDelegate(getTestWaiter())); + } catch (InactiveSessionException e) { + performNotify(e); + } + } + }); + assertSame(RepositorySession.SessionStatus.DONE, cmwSession.getStatus()); + } + + @Test + /** + * Verify that wipe is actually wiping the underlying repository. + */ + public void testWipe() { + Record record = new MockRecord("nncdefghiaaa", "coll", System.currentTimeMillis(), false); + wboRepo.wbos.put(record.guid, record); + assertEquals(1, wboRepo.wbos.size()); + + runInOnBeginSucceeded(new Runnable() { + @Override public void run() { + cmwSession.wipe(new ExpectSuccessRepositoryWipeDelegate(getTestWaiter())); + } + }); + performWait(onThreadRunnable(new Runnable() { + @Override public void run() { + try { + cmwSession.finish(new ExpectSuccessRepositorySessionFinishDelegate(getTestWaiter())); + } catch (InactiveSessionException e) { + performNotify(e); + } + } + })); + assertEquals(0, wboRepo.wbos.size()); + } + + @Test + /** + * Verify that store is actually writing encrypted data to the underlying repository. + */ + public void testStoreEncrypts() throws NonObjectJSONException, CryptoException, IOException { + final BookmarkRecord record = new BookmarkRecord("nncdefghiaaa", "coll", System.currentTimeMillis(), false); + record.title = "unencrypted title"; + + runInOnBeginSucceeded(new Runnable() { + @Override public void run() { + try { + try { + cmwSession.setStoreDelegate(new ExpectSuccessRepositorySessionStoreDelegate(getTestWaiter())); + cmwSession.store(record); + } catch (NoStoreDelegateException e) { + getTestWaiter().performNotify(new AssertionFailedError("Should not happen.")); + } + cmwSession.storeDone(); + cmwSession.finish(new ExpectSuccessRepositorySessionFinishDelegate(getTestWaiter())); + } catch (InactiveSessionException e) { + performNotify(e); + } + } + }); + assertEquals(1, wboRepo.wbos.size()); + assertTrue(wboRepo.wbos.containsKey(record.guid)); + + Record storedRecord = wboRepo.wbos.get(record.guid); + CryptoRecord cryptoRecord = (CryptoRecord)storedRecord; + assertSame(cryptoRecord.keyBundle, keyBundle); + + cryptoRecord = cryptoRecord.decrypt(); + BookmarkRecord decryptedRecord = new BookmarkRecord(); + decryptedRecord.initFromEnvelope(cryptoRecord); + assertEquals(record.title, decryptedRecord.title); + } + + @Test + /** + * Verify that fetch is actually retrieving encrypted data from the underlying repository and is correctly decrypting it. + */ + public void testFetchDecrypts() throws UnsupportedEncodingException, CryptoException { + final BookmarkRecord record1 = new BookmarkRecord("nncdefghiaaa", "coll", System.currentTimeMillis(), false); + record1.title = "unencrypted title"; + final BookmarkRecord record2 = new BookmarkRecord("XXXXXXXXXXXX", "coll", System.currentTimeMillis(), false); + record2.title = "unencrypted second title"; + + CryptoRecord encryptedRecord1 = record1.getEnvelope(); + encryptedRecord1.keyBundle = keyBundle; + encryptedRecord1 = encryptedRecord1.encrypt(); + wboRepo.wbos.put(record1.guid, encryptedRecord1); + + CryptoRecord encryptedRecord2 = record2.getEnvelope(); + encryptedRecord2.keyBundle = keyBundle; + encryptedRecord2 = encryptedRecord2.encrypt(); + wboRepo.wbos.put(record2.guid, encryptedRecord2); + + final ExpectSuccessRepositorySessionFetchRecordsDelegate fetchRecordsDelegate = new ExpectSuccessRepositorySessionFetchRecordsDelegate(getTestWaiter()); + runInOnBeginSucceeded(new Runnable() { + @Override public void run() { + try { + cmwSession.fetch(new String[] { record1.guid }, fetchRecordsDelegate); + } catch (InactiveSessionException e) { + performNotify(e); + } + } + }); + performWait(onThreadRunnable(new Runnable() { + @Override public void run() { + try { + cmwSession.finish(new ExpectSuccessRepositorySessionFinishDelegate(getTestWaiter())); + } catch (InactiveSessionException e) { + performNotify(e); + } + } + })); + + assertEquals(1, fetchRecordsDelegate.fetchedRecords.size()); + BookmarkRecord decryptedRecord = new BookmarkRecord(); + decryptedRecord.initFromEnvelope((CryptoRecord)fetchRecordsDelegate.fetchedRecords.get(0)); + assertEquals(record1.title, decryptedRecord.title); + } + + @Test + /** + * Verify that fetchAll is actually retrieving encrypted data from the underlying repository and is correctly decrypting it. + */ + public void testFetchAllDecrypts() throws UnsupportedEncodingException, CryptoException { + final BookmarkRecord record1 = new BookmarkRecord("nncdefghiaaa", "coll", System.currentTimeMillis(), false); + record1.title = "unencrypted title"; + final BookmarkRecord record2 = new BookmarkRecord("XXXXXXXXXXXX", "coll", System.currentTimeMillis(), false); + record2.title = "unencrypted second title"; + + CryptoRecord encryptedRecord1 = record1.getEnvelope(); + encryptedRecord1.keyBundle = keyBundle; + encryptedRecord1 = encryptedRecord1.encrypt(); + wboRepo.wbos.put(record1.guid, encryptedRecord1); + + CryptoRecord encryptedRecord2 = record2.getEnvelope(); + encryptedRecord2.keyBundle = keyBundle; + encryptedRecord2 = encryptedRecord2.encrypt(); + wboRepo.wbos.put(record2.guid, encryptedRecord2); + + final ExpectSuccessRepositorySessionFetchRecordsDelegate fetchAllRecordsDelegate = new ExpectSuccessRepositorySessionFetchRecordsDelegate(getTestWaiter()); + runInOnBeginSucceeded(new Runnable() { + @Override public void run() { + cmwSession.fetchAll(fetchAllRecordsDelegate); + } + }); + performWait(onThreadRunnable(new Runnable() { + @Override public void run() { + try { + cmwSession.finish(new ExpectSuccessRepositorySessionFinishDelegate(getTestWaiter())); + } catch (InactiveSessionException e) { + performNotify(e); + } + } + })); + + assertEquals(2, fetchAllRecordsDelegate.fetchedRecords.size()); + BookmarkRecord decryptedRecord1 = new BookmarkRecord(); + decryptedRecord1.initFromEnvelope((CryptoRecord)fetchAllRecordsDelegate.fetchedRecords.get(0)); + BookmarkRecord decryptedRecord2 = new BookmarkRecord(); + decryptedRecord2.initFromEnvelope((CryptoRecord)fetchAllRecordsDelegate.fetchedRecords.get(1)); + + // We should get two different decrypted records + assertFalse(decryptedRecord1.guid.equals(decryptedRecord2.guid)); + assertFalse(decryptedRecord1.title.equals(decryptedRecord2.title)); + // And we should know about both. + assertTrue(record1.title.equals(decryptedRecord1.title) || record1.title.equals(decryptedRecord2.title)); + assertTrue(record2.title.equals(decryptedRecord1.title) || record2.title.equals(decryptedRecord2.title)); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/net/test/TestHMACAuthHeaderProvider.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/net/test/TestHMACAuthHeaderProvider.java new file mode 100644 index 000000000..675351be9 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/net/test/TestHMACAuthHeaderProvider.java @@ -0,0 +1,165 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.sync.net.test; + +import ch.boye.httpclientandroidlib.client.methods.HttpGet; +import ch.boye.httpclientandroidlib.client.methods.HttpPost; +import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase; +import ch.boye.httpclientandroidlib.client.methods.HttpUriRequest; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.sync.net.HMACAuthHeaderProvider; + +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; + +import static org.junit.Assert.assertEquals; + +@RunWith(TestRunner.class) +public class TestHMACAuthHeaderProvider { + // Expose a few protected static member functions as public for testing. + protected static class LeakyHMACAuthHeaderProvider extends HMACAuthHeaderProvider { + public LeakyHMACAuthHeaderProvider(String identifier, String key) { + super(identifier, key); + } + + public static String getRequestString(HttpUriRequest request, long timestampInSeconds, String nonce, String extra) { + return HMACAuthHeaderProvider.getRequestString(request, timestampInSeconds, nonce, extra); + } + + public static String getSignature(String requestString, String key) + throws InvalidKeyException, NoSuchAlgorithmException, UnsupportedEncodingException { + return HMACAuthHeaderProvider.getSignature(requestString, key); + } + } + + @Test + public void testGetRequestStringSpecExample1() throws Exception { + long timestamp = 1336363200; + String nonceString = "dj83hs9s"; + String extra = ""; + URI uri = new URI("http://example.com/resource/1?b=1&a=2"); + + HttpUriRequest req = new HttpGet(uri); + + String expected = "1336363200\n" + + "dj83hs9s\n" + + "GET\n" + + "/resource/1?b=1&a=2\n" + + "example.com\n" + + "80\n" + + "\n"; + + assertEquals(expected, LeakyHMACAuthHeaderProvider.getRequestString(req, timestamp, nonceString, extra)); + } + + @Test + public void testGetRequestStringSpecExample2() throws Exception { + long timestamp = 264095; + String nonceString = "7d8f3e4a"; + String extra = "a,b,c"; + URI uri = new URI("http://example.com/request?b5=%3D%253D&a3=a&c%40=&a2=r%20b&c2&a3=2+q"); + + HttpUriRequest req = new HttpPost(uri); + + String expected = "264095\n" + + "7d8f3e4a\n" + + "POST\n" + + "/request?b5=%3D%253D&a3=a&c%40=&a2=r%20b&c2&a3=2+q\n" + + "example.com\n" + + "80\n" + + "a,b,c\n"; + + assertEquals(expected, LeakyHMACAuthHeaderProvider.getRequestString(req, timestamp, nonceString, extra)); + } + + @Test + public void testPort() throws Exception { + long timestamp = 264095; + String nonceString = "7d8f3e4a"; + String extra = "a,b,c"; + URI uri = new URI("http://example.com:88/request?b5=%3D%253D&a3=a&c%40=&a2=r%20b&c2&a3=2+q"); + + HttpUriRequest req = new HttpPost(uri); + + String expected = "264095\n" + + "7d8f3e4a\n" + + "POST\n" + + "/request?b5=%3D%253D&a3=a&c%40=&a2=r%20b&c2&a3=2+q\n" + + "example.com\n" + + "88\n" + + "a,b,c\n"; + + assertEquals(expected, LeakyHMACAuthHeaderProvider.getRequestString(req, timestamp, nonceString, extra)); + } + + @Test + public void testHTTPS() throws Exception { + long timestamp = 264095; + String nonceString = "7d8f3e4a"; + String extra = "a,b,c"; + URI uri = new URI("https://example.com/request?b5=%3D%253D&a3=a&c%40=&a2=r%20b&c2&a3=2+q"); + + HttpUriRequest req = new HttpPost(uri); + + String expected = "264095\n" + + "7d8f3e4a\n" + + "POST\n" + + "/request?b5=%3D%253D&a3=a&c%40=&a2=r%20b&c2&a3=2+q\n" + + "example.com\n" + + "443\n" + + "a,b,c\n"; + + assertEquals(expected, LeakyHMACAuthHeaderProvider.getRequestString(req, timestamp, nonceString, extra)); + } + + @Test + public void testSpecSignatureExample() throws Exception { + String extra = ""; + long timestampInSeconds = 1336363200; + String nonceString = "dj83hs9s"; + + URI uri = new URI("http://example.com/resource/1?b=1&a=2"); + HttpRequestBase req = new HttpGet(uri); + + String requestString = LeakyHMACAuthHeaderProvider.getRequestString(req, timestampInSeconds, nonceString, extra); + + String expected = "1336363200\n" + + "dj83hs9s\n" + + "GET\n" + + "/resource/1?b=1&a=2\n" + + "example.com\n" + + "80\n" + + "\n"; + + assertEquals(expected, requestString); + + // There appears to be an error in the current spec. + // Spec is at https://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01#section-1.1 + // Error is reported at http://www.ietf.org/mail-archive/web/oauth/current/msg09741.html + // assertEquals("bhCQXTVyfj5cmA9uKkPFx1zeOXM=", HMACAuthHeaderProvider.getSignature(requestString, keyString)); + } + + @Test + public void testCompatibleWithDesktopFirefox() throws Exception { + // These are test values used in the FF Sync Client testsuite. + + // String identifier = "vmo1txkttblmn51u2p3zk2xiy16hgvm5ok8qiv1yyi86ffjzy9zj0ez9x6wnvbx7"; + String keyString = "b8u1cc5iiio5o319og7hh8faf2gi5ym4aq0zwf112cv1287an65fudu5zj7zo7dz"; + + String extra = ""; + long timestampInSeconds = 1329181221; + String nonceString = "wGX71"; + + URI uri = new URI("http://10.250.2.176/alias/"); + HttpRequestBase req = new HttpGet(uri); + + String requestString = LeakyHMACAuthHeaderProvider.getRequestString(req, timestampInSeconds, nonceString, extra); + + assertEquals("jzh5chjQc2zFEvLbyHnPdX11Yck=", LeakyHMACAuthHeaderProvider.getSignature(requestString, keyString)); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/net/test/TestHawkAuthHeaderProvider.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/net/test/TestHawkAuthHeaderProvider.java new file mode 100644 index 000000000..3dab313a0 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/net/test/TestHawkAuthHeaderProvider.java @@ -0,0 +1,145 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.sync.net.test; + +import ch.boye.httpclientandroidlib.Header; +import ch.boye.httpclientandroidlib.client.methods.HttpGet; +import ch.boye.httpclientandroidlib.client.methods.HttpPost; +import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase; +import ch.boye.httpclientandroidlib.client.methods.HttpUriRequest; +import ch.boye.httpclientandroidlib.entity.StringEntity; +import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient; +import ch.boye.httpclientandroidlib.message.BasicHeader; +import ch.boye.httpclientandroidlib.protocol.BasicHttpContext; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.sync.net.HawkAuthHeaderProvider; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; + +import static org.junit.Assert.assertEquals; + +/** + * These test vectors were taken from + * <a href="https://github.com/hueniverse/hawk/blob/871cc597973110900467bd3dfb84a3c892f678fb/README.md">https://github.com/hueniverse/hawk/blob/871cc597973110900467bd3dfb84a3c892f678fb/README.md</a>. + */ +@RunWith(TestRunner.class) +public class TestHawkAuthHeaderProvider { + // Expose a few protected static member functions as public for testing. + protected static class LeakyHawkAuthHeaderProvider extends HawkAuthHeaderProvider { + public LeakyHawkAuthHeaderProvider(String tokenId, byte[] reqHMACKey) { + // getAuthHeader takes includePayloadHash as a parameter. + super(tokenId, reqHMACKey, false, 0L); + } + + // Public for testing. + public static String getRequestString(HttpUriRequest request, String type, long timestamp, String nonce, String hash, String extra, String app, String dlg) { + return HawkAuthHeaderProvider.getRequestString(request, type, timestamp, nonce, hash, extra, app, dlg); + } + + // Public for testing. + public static String getSignature(String requestString, String key) + throws InvalidKeyException, NoSuchAlgorithmException, UnsupportedEncodingException { + return HawkAuthHeaderProvider.getSignature(requestString.getBytes("UTF-8"), key.getBytes("UTF-8")); + } + + // Public for testing. + @Override + public Header getAuthHeader(HttpRequestBase request, BasicHttpContext context, DefaultHttpClient client, + long timestamp, String nonce, String extra, boolean includePayloadHash) + throws InvalidKeyException, NoSuchAlgorithmException, IOException { + return super.getAuthHeader(request, context, client, timestamp, nonce, extra, includePayloadHash); + } + + // Public for testing. + public static String getBaseContentType(Header contentTypeHeader) { + return HawkAuthHeaderProvider.getBaseContentType(contentTypeHeader); + } + } + + @Test + public void testSpecRequestString() throws Exception { + long timestamp = 1353832234; + String nonce = "j4h3g2"; + String extra = "some-app-ext-data"; + String hash = null; + String app = null; + String dlg = null; + + URI uri = new URI("http://example.com:8000/resource/1?b=1&a=2"); + HttpUriRequest req = new HttpGet(uri); + + String expected = "hawk.1.header\n" + + "1353832234\n" + + "j4h3g2\n" + + "GET\n" + + "/resource/1?b=1&a=2\n" + + "example.com\n" + + "8000\n" + + "\n" + + "some-app-ext-data\n"; + + // LeakyHawkAuthHeaderProvider. + assertEquals(expected, LeakyHawkAuthHeaderProvider.getRequestString(req, "header", timestamp, nonce, hash, extra, app, dlg)); + } + + @Test + public void testSpecSignatureExample() throws Exception { + String input = "hawk.1.header\n" + + "1353832234\n" + + "j4h3g2\n" + + "GET\n" + + "/resource/1?b=1&a=2\n" + + "example.com\n" + + "8000\n" + + "\n" + + "some-app-ext-data\n"; + + assertEquals("6R4rV5iE+NPoym+WwjeHzjAGXUtLNIxmo1vpMofpLAE=", LeakyHawkAuthHeaderProvider.getSignature(input, "werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn")); + } + + @Test + public void testSpecPayloadExample() throws Exception { + LeakyHawkAuthHeaderProvider provider = new LeakyHawkAuthHeaderProvider("dh37fgj492je", "werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn".getBytes("UTF-8")); + URI uri = new URI("http://example.com:8000/resource/1?b=1&a=2"); + HttpPost req = new HttpPost(uri); + String body = "Thank you for flying Hawk"; + req.setEntity(new StringEntity(body)); + Header header = provider.getAuthHeader(req, null, null, 1353832234L, "j4h3g2", "some-app-ext-data", true); + String expected = "Hawk id=\"dh37fgj492je\", ts=\"1353832234\", nonce=\"j4h3g2\", hash=\"Yi9LfIIFRtBEPt74PVmbTF/xVAwPn7ub15ePICfgnuY=\", ext=\"some-app-ext-data\", mac=\"aSe1DERmZuRl3pI36/9BdZmnErTw3sNzOOAUlfeKjVw=\""; + assertEquals("Authorization", header.getName()); + assertEquals(expected, header.getValue()); + } + + @Test + public void testSpecAuthorizationHeader() throws Exception { + LeakyHawkAuthHeaderProvider provider = new LeakyHawkAuthHeaderProvider("dh37fgj492je", "werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn".getBytes("UTF-8")); + URI uri = new URI("http://example.com:8000/resource/1?b=1&a=2"); + HttpGet req = new HttpGet(uri); + Header header = provider.getAuthHeader(req, null, null, 1353832234L, "j4h3g2", "some-app-ext-data", false); + String expected = "Hawk id=\"dh37fgj492je\", ts=\"1353832234\", nonce=\"j4h3g2\", ext=\"some-app-ext-data\", mac=\"6R4rV5iE+NPoym+WwjeHzjAGXUtLNIxmo1vpMofpLAE=\""; + assertEquals("Authorization", header.getName()); + assertEquals(expected, header.getValue()); + + // For a non-POST, non-PUT request, a request to include the payload verification hash is silently ignored. + header = provider.getAuthHeader(req, null, null, 1353832234L, "j4h3g2", "some-app-ext-data", true); + assertEquals("Authorization", header.getName()); + assertEquals(expected, header.getValue()); + } + + @Test + public void testGetBaseContentType() throws Exception { + assertEquals("text/plain", LeakyHawkAuthHeaderProvider.getBaseContentType(new BasicHeader("Content-Type", "text/plain"))); + assertEquals("text/plain", LeakyHawkAuthHeaderProvider.getBaseContentType(new BasicHeader("Content-Type", "text/plain;one"))); + assertEquals("text/plain", LeakyHawkAuthHeaderProvider.getBaseContentType(new BasicHeader("Content-Type", "text/plain;one;two"))); + assertEquals("text/html", LeakyHawkAuthHeaderProvider.getBaseContentType(new BasicHeader("Content-Type", "text/html;charset=UTF-8"))); + assertEquals("text/html", LeakyHawkAuthHeaderProvider.getBaseContentType(new BasicHeader("Content-Type", "text/html; charset=UTF-8"))); + assertEquals("text/html", LeakyHawkAuthHeaderProvider.getBaseContentType(new BasicHeader("Content-Type", "text/html ;charset=UTF-8"))); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/net/test/TestLiveHawkAuth.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/net/test/TestLiveHawkAuth.java new file mode 100644 index 000000000..8f136e3d0 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/net/test/TestLiveHawkAuth.java @@ -0,0 +1,181 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.sync.net.test; + +import ch.boye.httpclientandroidlib.Header; +import ch.boye.httpclientandroidlib.HttpResponse; +import ch.boye.httpclientandroidlib.client.ClientProtocolException; +import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase; +import ch.boye.httpclientandroidlib.entity.StringEntity; +import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient; +import ch.boye.httpclientandroidlib.message.BasicHeader; +import ch.boye.httpclientandroidlib.protocol.BasicHttpContext; +import org.junit.Assert; +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.BaseResourceDelegate; +import org.mozilla.gecko.sync.net.HawkAuthHeaderProvider; +import org.mozilla.gecko.sync.net.Resource; +import org.mozilla.gecko.sync.net.SyncResponse; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.security.GeneralSecurityException; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + + +public class TestLiveHawkAuth { + /** + * Hawk comes with an example/usage.js server. Modify it to serve indefinitely, + * un-comment the following line, and verify that the port and credentials + * have not changed; then the following test should pass. + */ + // @org.junit.Test + public void testHawkUsage() throws Exception { + // Id and credentials are hard-coded in example/usage.js. + final String id = "dh37fgj492je"; + final byte[] key = "werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn".getBytes("UTF-8"); + final BaseResource resource = new BaseResource("http://localhost:8000/", false); + + // Basic GET. + resource.delegate = new TestBaseResourceDelegate(resource, new HawkAuthHeaderProvider(id, key, false, 0L)); + WaitHelper.getTestWaiter().performWait(new Runnable() { + @Override + public void run() { + resource.get(); + } + }); + + // PUT with payload verification. + resource.delegate = new TestBaseResourceDelegate(resource, new HawkAuthHeaderProvider(id, key, true, 0L)); + WaitHelper.getTestWaiter().performWait(new Runnable() { + @Override + public void run() { + try { + resource.put(new StringEntity("Thank you for flying Hawk")); + } catch (UnsupportedEncodingException e) { + WaitHelper.getTestWaiter().performNotify(e); + } + } + }); + + // PUT with a large (32k or so) body and payload verification. + final StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 16000; i++) { + sb.append(Integer.valueOf(i % 100).toString()); + } + resource.delegate = new TestBaseResourceDelegate(resource, new HawkAuthHeaderProvider(id, key, true, 0L)); + WaitHelper.getTestWaiter().performWait(new Runnable() { + @Override + public void run() { + try { + resource.put(new StringEntity(sb.toString())); + } catch (UnsupportedEncodingException e) { + WaitHelper.getTestWaiter().performNotify(e); + } + } + }); + + // PUT without payload verification. + resource.delegate = new TestBaseResourceDelegate(resource, new HawkAuthHeaderProvider(id, key, false, 0L)); + WaitHelper.getTestWaiter().performWait(new Runnable() { + @Override + public void run() { + try { + resource.put(new StringEntity("Thank you for flying Hawk")); + } catch (UnsupportedEncodingException e) { + WaitHelper.getTestWaiter().performNotify(e); + } + } + }); + + // PUT with *bad* payload verification. + HawkAuthHeaderProvider provider = new HawkAuthHeaderProvider(id, key, true, 0L) { + @Override + public Header getAuthHeader(HttpRequestBase request, BasicHttpContext context, DefaultHttpClient client) throws GeneralSecurityException { + Header header = super.getAuthHeader(request, context, client); + // Here's a cheap way of breaking the hash. + String newValue = header.getValue().replaceAll("hash=\"....", "hash=\"XXXX"); + return new BasicHeader(header.getName(), newValue); + } + }; + + resource.delegate = new TestBaseResourceDelegate(resource, provider); + try { + WaitHelper.getTestWaiter().performWait(new Runnable() { + @Override + public void run() { + try { + resource.put(new StringEntity("Thank you for flying Hawk")); + } catch (UnsupportedEncodingException e) { + WaitHelper.getTestWaiter().performNotify(e); + } + } + }); + fail("Expected assertion after 401 response."); + } catch (WaitHelper.InnerError e) { + assertTrue(e.innerError instanceof AssertionError); + assertEquals("expected:<200> but was:<401>", ((AssertionError) e.innerError).getMessage()); + } + } + + protected final class TestBaseResourceDelegate extends BaseResourceDelegate { + protected final HawkAuthHeaderProvider provider; + + protected TestBaseResourceDelegate(Resource resource, HawkAuthHeaderProvider provider) throws UnsupportedEncodingException { + super(resource); + this.provider = provider; + } + + @Override + public AuthHeaderProvider getAuthHeaderProvider() { + return provider; + } + + @Override + public int connectionTimeout() { + return 1000; + } + + @Override + public int socketTimeout() { + return 1000; + } + + @Override + public void handleHttpResponse(HttpResponse response) { + SyncResponse res = new SyncResponse(response); + try { + Assert.assertEquals(200, res.getStatusCode()); + WaitHelper.getTestWaiter().performNotify(); + } catch (Throwable e) { + WaitHelper.getTestWaiter().performNotify(e); + } + } + + @Override + public void handleTransportException(GeneralSecurityException e) { + WaitHelper.getTestWaiter().performNotify(e); + } + + @Override + public void handleHttpProtocolException(ClientProtocolException e) { + WaitHelper.getTestWaiter().performNotify(e); + } + + @Override + public void handleHttpIOException(IOException e) { + WaitHelper.getTestWaiter().performNotify(e); + } + + @Override + public String getUserAgent() { + return null; + } + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/net/test/TestUserAgentHeaders.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/net/test/TestUserAgentHeaders.java new file mode 100644 index 000000000..30b8a38ec --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/net/test/TestUserAgentHeaders.java @@ -0,0 +1,131 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.sync.net.test; + +import ch.boye.httpclientandroidlib.protocol.HTTP; +import junit.framework.Assert; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.android.sync.test.helpers.HTTPServerTestHelper; +import org.mozilla.android.sync.test.helpers.MockServer; +import org.mozilla.gecko.background.fxa.FxAccountClient20; +import org.mozilla.gecko.background.fxa.FxAccountClient20.RequestDelegate; +import org.mozilla.gecko.background.fxa.FxAccountClient20.RecoveryEmailStatusResponse; +import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.background.testhelpers.WaitHelper; +import org.mozilla.gecko.fxa.FxAccountConstants; +import org.mozilla.gecko.sync.SyncConstants; +import org.mozilla.gecko.sync.net.AuthHeaderProvider; +import org.mozilla.gecko.sync.net.BaseResource; +import org.mozilla.gecko.sync.net.SyncStorageRecordRequest; +import org.mozilla.gecko.sync.net.SyncStorageRequestDelegate; +import org.mozilla.gecko.sync.net.SyncStorageResponse; +import org.simpleframework.http.Request; +import org.simpleframework.http.Response; + +import java.util.concurrent.Executors; + +@RunWith(TestRunner.class) +public class TestUserAgentHeaders { + private static final int TEST_PORT = HTTPServerTestHelper.getTestPort(); + private static final String TEST_SERVER = "http://localhost:" + TEST_PORT; + + protected final HTTPServerTestHelper data = new HTTPServerTestHelper(); + + protected class UserAgentServer extends MockServer { + public String lastUserAgent = null; + + @Override + public void handle(Request request, Response response) { + lastUserAgent = request.getValue(HTTP.USER_AGENT); + super.handle(request, response); + } + } + + protected final UserAgentServer userAgentServer = new UserAgentServer(); + + @Before + public void setUp() { + BaseResource.rewriteLocalhost = false; + data.startHTTPServer(userAgentServer); + } + + @After + public void tearDown() { + data.stopHTTPServer(); + } + + @Test + public void testSyncUserAgent() throws Exception { + final SyncStorageRecordRequest request = new SyncStorageRecordRequest(TEST_SERVER); + request.delegate = new SyncStorageRequestDelegate() { + @Override + public String ifUnmodifiedSince() { + return null; + } + + @Override + public void handleRequestSuccess(SyncStorageResponse response) { + WaitHelper.getTestWaiter().performNotify(); + } + + @Override + public void handleRequestFailure(SyncStorageResponse response) { + WaitHelper.getTestWaiter().performNotify(); + } + + @Override + public void handleRequestError(Exception ex) { + WaitHelper.getTestWaiter().performNotify(); + } + + @Override + public AuthHeaderProvider getAuthHeaderProvider() { + return null; + } + }; + + WaitHelper.getTestWaiter().performWait(new Runnable() { + @Override + public void run() { + request.get(); + } + }); + + // Verify that we're getting the value from the correct place. + Assert.assertEquals(SyncConstants.USER_AGENT, userAgentServer.lastUserAgent); + } + + @Test + public void testFxAccountClientUserAgent() throws Exception { + final FxAccountClient20 client = new FxAccountClient20(TEST_SERVER, Executors.newSingleThreadExecutor()); + WaitHelper.getTestWaiter().performWait(new Runnable() { + @Override + public void run() { + client.recoveryEmailStatus(new byte[] { 0 }, new RequestDelegate<RecoveryEmailStatusResponse>() { + @Override + public void handleSuccess(RecoveryEmailStatusResponse result) { + WaitHelper.getTestWaiter().performNotify(); + } + + @Override + public void handleFailure(FxAccountClientRemoteException e) { + WaitHelper.getTestWaiter().performNotify(); + } + + @Override + public void handleError(Exception e) { + WaitHelper.getTestWaiter().performNotify(); + } + }); + } + }); + + // Verify that we're getting the value from the correct place. + Assert.assertEquals(FxAccountConstants.USER_AGENT, userAgentServer.lastUserAgent); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/android/BrowserContractHelpersTest.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/android/BrowserContractHelpersTest.java new file mode 100644 index 000000000..eecfa8dc2 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/android/BrowserContractHelpersTest.java @@ -0,0 +1,33 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.sync.repositories.android; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.TestRunner; + +import static org.junit.Assert.*; + +@RunWith(TestRunner.class) +public class BrowserContractHelpersTest { + @Test + public void testBookmarkCodes() { + final String[] strings = { + // Observe omissions: "microsummary", "item". + "folder", "bookmark", "separator", "livemark", "query" + }; + for (int i = 0; i < strings.length; ++i) { + assertEquals(strings[i], BrowserContractHelpers.typeStringForCode(i)); + assertEquals(i, BrowserContractHelpers.typeCodeForString(strings[i])); + } + assertEquals(null, BrowserContractHelpers.typeStringForCode(-1)); + assertEquals(null, BrowserContractHelpers.typeStringForCode(100)); + + assertEquals(-1, BrowserContractHelpers.typeCodeForString(null)); + assertEquals(-1, BrowserContractHelpers.typeCodeForString("folder ")); + assertEquals(-1, BrowserContractHelpers.typeCodeForString("FOLDER")); + assertEquals(-1, BrowserContractHelpers.typeCodeForString("")); + assertEquals(-1, BrowserContractHelpers.typeCodeForString("nope")); + } +}
\ No newline at end of file diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/android/VisitsHelperTest.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/android/VisitsHelperTest.java new file mode 100644 index 000000000..67bbca089 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/android/VisitsHelperTest.java @@ -0,0 +1,144 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.sync.repositories.android; + +import android.content.ContentProviderClient; +import android.content.ContentValues; +import android.net.Uri; + +import junit.framework.Assert; + +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.db.DelegatingTestContentProvider; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.db.BrowserContract; +import org.mozilla.gecko.db.BrowserProvider; +import org.robolectric.shadows.ShadowContentResolver; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +@RunWith(TestRunner.class) +public class VisitsHelperTest { + @Test + public void testBulkInsertRemoteVisits() throws Exception { + JSONArray toInsert = new JSONArray(); + Assert.assertEquals(0, VisitsHelper.getVisitsContentValues("testGUID", toInsert).length); + + JSONObject visit = new JSONObject(); + Long date = Long.valueOf(123432552344l); + visit.put("date", date); + visit.put("type", 2l); + toInsert.add(visit); + + JSONObject visit2 = new JSONObject(); + visit2.put("date", date + 1000); + visit2.put("type", 5l); + toInsert.add(visit2); + + ContentValues[] cvs = VisitsHelper.getVisitsContentValues("testGUID", toInsert); + Assert.assertEquals(2, cvs.length); + ContentValues cv1 = cvs[0]; + ContentValues cv2 = cvs[1]; + Assert.assertEquals(Integer.valueOf(2), cv1.getAsInteger(BrowserContract.Visits.VISIT_TYPE)); + Assert.assertEquals(Integer.valueOf(5), cv2.getAsInteger(BrowserContract.Visits.VISIT_TYPE)); + + Assert.assertEquals(date, cv1.getAsLong("date")); + Assert.assertEquals(Long.valueOf(date + 1000), cv2.getAsLong(BrowserContract.Visits.DATE_VISITED)); + } + + @Test + public void testGetRecentHistoryVisitsForGUID() throws Exception { + Uri historyTestUri = testUri(BrowserContract.History.CONTENT_URI); + Uri visitsTestUri = testUri(BrowserContract.Visits.CONTENT_URI); + + BrowserProvider provider = new BrowserProvider(); + try { + provider.onCreate(); + ShadowContentResolver.registerProvider(BrowserContract.AUTHORITY, new DelegatingTestContentProvider(provider)); + + final ShadowContentResolver cr = new ShadowContentResolver(); + ContentProviderClient historyClient = cr.acquireContentProviderClient(BrowserContractHelpers.HISTORY_CONTENT_URI); + ContentProviderClient visitsClient = cr.acquireContentProviderClient(BrowserContractHelpers.VISITS_CONTENT_URI); + + ContentValues historyItem = new ContentValues(); + historyItem.put(BrowserContract.History.URL, "https://www.mozilla.org"); + historyItem.put(BrowserContract.History.GUID, "testGUID"); + historyClient.insert(historyTestUri, historyItem); + + Long baseDate = System.currentTimeMillis(); + for (int i = 0; i < 30; i++) { + ContentValues visitItem = new ContentValues(); + visitItem.put(BrowserContract.Visits.HISTORY_GUID, "testGUID"); + visitItem.put(BrowserContract.Visits.DATE_VISITED, baseDate - i * 100); + visitItem.put(BrowserContract.Visits.VISIT_TYPE, 1); + visitItem.put(BrowserContract.Visits.IS_LOCAL, 1); + visitsClient.insert(visitsTestUri, visitItem); + } + + // test that limit worked, that sorting is correct, and that both date and type are present + JSONArray recentVisits = VisitsHelper.getRecentHistoryVisitsForGUID(visitsClient, "testGUID", 10); + Assert.assertEquals(10, recentVisits.size()); + for (int i = 0; i < recentVisits.size(); i++) { + JSONObject v = (JSONObject) recentVisits.get(i); + Long date = (Long) v.get("date"); + Long type = (Long) v.get("type"); + Assert.assertEquals(Long.valueOf(baseDate - i * 100), date); + Assert.assertEquals(Long.valueOf(1), type); + } + } finally { + provider.shutdown(); + } + } + + @Test + public void testGetVisitContentValues() throws Exception { + JSONObject visit = new JSONObject(); + Long date = Long.valueOf(123432552344l); + visit.put("date", date); + visit.put("type", Long.valueOf(2)); + + ContentValues cv = VisitsHelper.getVisitContentValues("testGUID", visit, true); + assertTrue(cv.containsKey(BrowserContract.Visits.VISIT_TYPE)); + assertTrue(cv.containsKey(BrowserContract.Visits.DATE_VISITED)); + assertTrue(cv.containsKey(BrowserContract.Visits.HISTORY_GUID)); + assertTrue(cv.containsKey(BrowserContract.Visits.IS_LOCAL)); + assertEquals(4, cv.size()); + + assertEquals(date, cv.getAsLong(BrowserContract.Visits.DATE_VISITED)); + assertEquals(Long.valueOf(2), cv.getAsLong(BrowserContract.Visits.VISIT_TYPE)); + assertEquals("testGUID", cv.getAsString(BrowserContract.Visits.HISTORY_GUID)); + assertEquals(Integer.valueOf(1), cv.getAsInteger(BrowserContract.Visits.IS_LOCAL)); + + cv = VisitsHelper.getVisitContentValues("testGUID", visit, false); + assertEquals(Integer.valueOf(0), cv.getAsInteger(BrowserContract.Visits.IS_LOCAL)); + + try { + JSONObject visit2 = new JSONObject(); + visit.put("date", date); + VisitsHelper.getVisitContentValues("testGUID", visit2, false); + assertTrue("Must check that visit type key is present", false); + } catch (IllegalArgumentException e) {} + + try { + JSONObject visit3 = new JSONObject(); + visit.put("type", Long.valueOf(2)); + VisitsHelper.getVisitContentValues("testGUID", visit3, false); + assertTrue("Must check that visit date key is present", false); + } catch (IllegalArgumentException e) {} + + try { + JSONObject visit4 = new JSONObject(); + VisitsHelper.getVisitContentValues("testGUID", visit4, false); + assertTrue("Must check that visit type and date keys are present", false); + } catch (IllegalArgumentException e) {} + } + + private Uri testUri(Uri baseUri) { + return baseUri.buildUpon().appendQueryParameter(BrowserContract.PARAM_IS_TEST, "1").build(); + } +}
\ No newline at end of file diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/android/test/TestBookmarksInsertionManager.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/android/test/TestBookmarksInsertionManager.java new file mode 100644 index 000000000..cbf5c37d3 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/android/test/TestBookmarksInsertionManager.java @@ -0,0 +1,221 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.sync.repositories.android.test; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.common.log.Logger; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.sync.Utils; +import org.mozilla.gecko.sync.repositories.android.BookmarksInsertionManager; +import org.mozilla.gecko.sync.repositories.domain.BookmarkRecord; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +@RunWith(TestRunner.class) +public class TestBookmarksInsertionManager { + public BookmarksInsertionManager manager; + public ArrayList<String[]> insertions; + + @Before + public void setUp() { + insertions = new ArrayList<String[]>(); + Set<String> writtenFolders = new HashSet<String>(); + writtenFolders.add("mobile"); + + BookmarksInsertionManager.BookmarkInserter inserter = new BookmarksInsertionManager.BookmarkInserter() { + @Override + public boolean insertFolder(BookmarkRecord record) { + if (record.guid == "fail") { + return false; + } + Logger.debug(BookmarksInsertionManager.LOG_TAG, "Inserted folder (" + record.guid + ")."); + insertions.add(new String[] { record.guid }); + return true; + } + + @Override + public void bulkInsertNonFolders(Collection<BookmarkRecord> records) { + ArrayList<String> guids = new ArrayList<String>(); + for (BookmarkRecord record : records) { + guids.add(record.guid); + } + String[] guidList = guids.toArray(new String[guids.size()]); + insertions.add(guidList); + Logger.debug(BookmarksInsertionManager.LOG_TAG, "Inserted non-folders (" + Utils.toCommaSeparatedString(guids) + ")."); + } + }; + manager = new BookmarksInsertionManager(3, writtenFolders, inserter); + BookmarksInsertionManager.DEBUG = true; + } + + protected static BookmarkRecord bookmark(String guid, String parent) { + BookmarkRecord bookmark = new BookmarkRecord(guid); + bookmark.type = "bookmark"; + bookmark.parentID = parent; + return bookmark; + } + + protected static BookmarkRecord folder(String guid, String parent) { + BookmarkRecord bookmark = new BookmarkRecord(guid); + bookmark.type = "folder"; + bookmark.parentID = parent; + return bookmark; + } + + @Test + public void testChildrenBeforeFolder() { + BookmarkRecord folder = folder("folder", "mobile"); + BookmarkRecord child1 = bookmark("child1", "folder"); + BookmarkRecord child2 = bookmark("child2", "folder"); + + manager.enqueueRecord(child1); + assertTrue(insertions.isEmpty()); + manager.enqueueRecord(child2); + assertTrue(insertions.isEmpty()); + manager.enqueueRecord(folder); + assertEquals(1, insertions.size()); + manager.finishUp(); + assertTrue(manager.isClear()); + assertEquals(2, insertions.size()); + assertArrayEquals(new String[] { "folder" }, insertions.get(0)); + assertArrayEquals(new String[] { "child1", "child2" }, insertions.get(1)); + } + + @Test + public void testChildAfterFolder() { + BookmarkRecord folder = folder("folder", "mobile"); + BookmarkRecord child1 = bookmark("child1", "folder"); + BookmarkRecord child2 = bookmark("child2", "folder"); + + manager.enqueueRecord(child1); + assertTrue(insertions.isEmpty()); + manager.enqueueRecord(folder); + assertEquals(1, insertions.size()); + manager.enqueueRecord(child2); + assertEquals(1, insertions.size()); + manager.finishUp(); + assertTrue(manager.isClear()); + assertEquals(2, insertions.size()); + assertArrayEquals(new String[] { "folder" }, insertions.get(0)); + assertArrayEquals(new String[] { "child1", "child2" }, insertions.get(1)); + } + + @Test + public void testFolderAfterFolder() { + manager.enqueueRecord(bookmark("child1", "folder1")); + assertEquals(0, insertions.size()); + manager.enqueueRecord(folder("folder1", "mobile")); + assertEquals(1, insertions.size()); + manager.enqueueRecord(bookmark("child2", "folder2")); + assertEquals(1, insertions.size()); + manager.enqueueRecord(folder("folder2", "folder1")); + assertEquals(2, insertions.size()); + manager.enqueueRecord(bookmark("child3", "folder1")); + manager.enqueueRecord(bookmark("child4", "folder2")); + assertEquals(3, insertions.size()); + + manager.finishUp(); + assertTrue(manager.isClear()); + assertEquals(4, insertions.size()); + assertArrayEquals(new String[] { "folder1" }, insertions.get(0)); + assertArrayEquals(new String[] { "folder2" }, insertions.get(1)); + assertArrayEquals(new String[] { "child1", "child2", "child3" }, insertions.get(2)); + assertArrayEquals(new String[] { "child4" }, insertions.get(3)); + } + + @Test + public void testFolderRecursion() { + manager.enqueueRecord(folder("1", "mobile")); + manager.enqueueRecord(folder("2", "1")); + assertEquals(2, insertions.size()); + manager.enqueueRecord(bookmark("3a", "3")); + manager.enqueueRecord(bookmark("3b", "3")); + manager.enqueueRecord(bookmark("3c", "3")); + manager.enqueueRecord(bookmark("4a", "4")); + manager.enqueueRecord(bookmark("4b", "4")); + manager.enqueueRecord(bookmark("4c", "4")); + assertEquals(2, insertions.size()); + manager.enqueueRecord(folder("3", "2")); + assertEquals(4, insertions.size()); + manager.enqueueRecord(folder("4", "2")); + assertEquals(6, insertions.size()); + + assertTrue(manager.isClear()); + manager.finishUp(); + assertTrue(manager.isClear()); + // Folders in order. + assertArrayEquals(new String[] { "1" }, insertions.get(0)); + assertArrayEquals(new String[] { "2" }, insertions.get(1)); + assertArrayEquals(new String[] { "3" }, insertions.get(2)); + // Then children in batches of 3. + assertArrayEquals(new String[] { "3a", "3b", "3c" }, insertions.get(3)); + // Then last folder. + assertArrayEquals(new String[] { "4" }, insertions.get(4)); + assertArrayEquals(new String[] { "4a", "4b", "4c" }, insertions.get(5)); + } + + @Test + public void testFailedFolderInsertion() { + manager.enqueueRecord(bookmark("failA", "fail")); + manager.enqueueRecord(bookmark("failB", "fail")); + assertEquals(0, insertions.size()); + manager.enqueueRecord(folder("fail", "mobile")); + assertEquals(0, insertions.size()); + manager.enqueueRecord(bookmark("failC", "fail")); + assertEquals(0, insertions.size()); + manager.finishUp(); // Children inserted at the end; they will be treated as orphans. + assertTrue(manager.isClear()); + assertEquals(1, insertions.size()); + assertArrayEquals(new String[] { "failA", "failB", "failC" }, insertions.get(0)); + } + + @Test + public void testIncrementalFlush() { + manager.enqueueRecord(bookmark("a", "1")); + manager.enqueueRecord(bookmark("b", "1")); + manager.enqueueRecord(folder("1", "mobile")); + assertEquals(1, insertions.size()); + manager.enqueueRecord(bookmark("c", "1")); + assertEquals(2, insertions.size()); + manager.enqueueRecord(bookmark("d", "1")); + manager.enqueueRecord(bookmark("e", "1")); + manager.enqueueRecord(bookmark("f", "1")); + assertEquals(3, insertions.size()); + manager.enqueueRecord(bookmark("g", "1")); // Start of new batch. + assertEquals(3, insertions.size()); + manager.finishUp(); // Children inserted at the end; they will be treated as orphans. + assertTrue(manager.isClear()); + assertEquals(4, insertions.size()); + assertArrayEquals(new String[] { "1" }, insertions.get(0)); + assertArrayEquals(new String[] { "a", "b", "c"}, insertions.get(1)); + assertArrayEquals(new String[] { "d", "e", "f"}, insertions.get(2)); + assertArrayEquals(new String[] { "g" }, insertions.get(3)); + } + + @Test + public void testFinishUp() { + manager.enqueueRecord(bookmark("a", "1")); + manager.enqueueRecord(bookmark("b", "1")); + manager.enqueueRecord(folder("2", "1")); + manager.enqueueRecord(bookmark("c", "1")); + manager.enqueueRecord(bookmark("d", "1")); + manager.enqueueRecord(folder("3", "1")); + assertEquals(0, insertions.size()); + manager.finishUp(); // Children inserted at the end; they will be treated as orphans. + assertTrue(manager.isClear()); + assertEquals(3, insertions.size()); + assertArrayEquals(new String[] { "2" }, insertions.get(0)); + assertArrayEquals(new String[] { "3" }, insertions.get(1)); + assertArrayEquals(new String[] { "a", "b", "c", "d" }, insertions.get(2)); // Last insertion could be big. + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/domain/TestClientRecord.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/domain/TestClientRecord.java new file mode 100644 index 000000000..790d47620 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/domain/TestClientRecord.java @@ -0,0 +1,103 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.sync.repositories.domain; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.sync.CryptoRecord; +import org.mozilla.gecko.sync.Utils; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +@RunWith(TestRunner.class) +public class TestClientRecord { + + @Test + public void testEnsureDefaults() { + // Ensure defaults. + ClientRecord record = new ClientRecord(); + assertEquals(ClientRecord.COLLECTION_NAME, record.collection); + assertEquals(0, record.lastModified); + assertEquals(false, record.deleted); + assertEquals("Default Name", record.name); + assertEquals(ClientRecord.CLIENT_TYPE, record.type); + assertTrue(null == record.os); + assertTrue(null == record.device); + assertTrue(null == record.application); + assertTrue(null == record.appPackage); + assertTrue(null == record.formfactor); + } + + @Test + public void testGetPayload() { + // Test ClientRecord.getPayload(). + ClientRecord record = new ClientRecord(); + CryptoRecord cryptoRecord = record.getEnvelope(); + assertEquals(record.guid, cryptoRecord.payload.get("id")); + assertEquals(null, cryptoRecord.payload.get("collection")); + assertEquals(null, cryptoRecord.payload.get("lastModified")); + assertEquals(null, cryptoRecord.payload.get("deleted")); + assertEquals(null, cryptoRecord.payload.get("version")); + assertEquals(record.name, cryptoRecord.payload.get("name")); + assertEquals(record.type, cryptoRecord.payload.get("type")); + } + + @Test + public void testInitFromPayload() { + // Test ClientRecord.initFromPayload() in ClientRecordFactory. + ClientRecord record1 = new ClientRecord(); + CryptoRecord cryptoRecord = record1.getEnvelope(); + ClientRecordFactory factory = new ClientRecordFactory(); + ClientRecord record2 = (ClientRecord) factory.createRecord(cryptoRecord); + assertEquals(cryptoRecord.payload.get("id"), record2.guid); + assertEquals(ClientRecord.COLLECTION_NAME, record2.collection); + assertEquals(0, record2.lastModified); + assertEquals(false, record2.deleted); + assertEquals(cryptoRecord.payload.get("name"), record2.name); + assertEquals(cryptoRecord.payload.get("type"), record2.type); + } + + @Test + public void testCopyWithIDs() { + // Test ClientRecord.copyWithIDs. + ClientRecord record1 = new ClientRecord(); + record1.version = "20"; + String newGUID = Utils.generateGuid(); + ClientRecord record2 = (ClientRecord) record1.copyWithIDs(newGUID, 0); + assertEquals(newGUID, record2.guid); + assertEquals(0, record2.androidID); + assertEquals(record1.collection, record2.collection); + assertEquals(record1.lastModified, record2.lastModified); + assertEquals(record1.deleted, record2.deleted); + assertEquals(record1.name, record2.name); + assertEquals(record1.type, record2.type); + assertEquals(record1.version, record2.version); + } + + @Test + public void testEquals() { + // Test ClientRecord.equals(). + ClientRecord record1 = new ClientRecord(); + ClientRecord record2 = new ClientRecord(); + record2.guid = record1.guid; + record2.version = "20"; + record1.version = null; + + ClientRecord record3 = new ClientRecord(Utils.generateGuid()); + record3.name = "New Name"; + + ClientRecord record4 = new ClientRecord(Utils.generateGuid()); + record4.name = ClientRecord.DEFAULT_CLIENT_NAME; + record4.type = "desktop"; + + assertTrue(record2.equals(record1)); + assertFalse(record3.equals(record1)); + assertFalse(record3.equals(record2)); + assertFalse(record4.equals(record1)); + assertFalse(record4.equals(record2)); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/domain/test/TestFormHistoryRecord.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/domain/test/TestFormHistoryRecord.java new file mode 100644 index 000000000..c0682e90e --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/domain/test/TestFormHistoryRecord.java @@ -0,0 +1,92 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.sync.repositories.domain.test; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.sync.CryptoRecord; +import org.mozilla.gecko.sync.Utils; +import org.mozilla.gecko.sync.repositories.domain.FormHistoryRecord; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +@RunWith(TestRunner.class) +public class TestFormHistoryRecord { + public static FormHistoryRecord withIdFieldNameAndValue(long id, String fieldName, String value) { + FormHistoryRecord fr = new FormHistoryRecord(); + fr.androidID = id; + fr.fieldName = fieldName; + fr.fieldValue = value; + + return fr; + } + + @Test + public void testCollection() { + FormHistoryRecord fr = new FormHistoryRecord(); + assertEquals("forms", fr.collection); + } + + @Test + public void testGetPayload() { + FormHistoryRecord fr = withIdFieldNameAndValue(0, "username", "aUsername"); + CryptoRecord rec = fr.getEnvelope(); + assertEquals("username", rec.payload.get("name")); + assertEquals("aUsername", rec.payload.get("value")); + } + + @Test + public void testCopyWithIDs() { + FormHistoryRecord fr = withIdFieldNameAndValue(0, "username", "aUsername"); + String guid = Utils.generateGuid(); + FormHistoryRecord fr2 = (FormHistoryRecord)fr.copyWithIDs(guid, 9999); + assertEquals(guid, fr2.guid); + assertEquals(9999, fr2.androidID); + assertEquals(fr.fieldName, fr2.fieldName); + assertEquals(fr.fieldValue, fr2.fieldValue); + } + + @Test + public void testEquals() { + FormHistoryRecord fr1a = withIdFieldNameAndValue(0, "username1", "Alice"); + FormHistoryRecord fr1b = withIdFieldNameAndValue(0, "username1", "Bob"); + FormHistoryRecord fr2a = withIdFieldNameAndValue(0, "username2", "Alice"); + FormHistoryRecord fr2b = withIdFieldNameAndValue(0, "username2", "Bob"); + + assertFalse(fr1a.equals(fr1b)); + assertFalse(fr1a.equals(fr2a)); + assertFalse(fr1a.equals(fr2b)); + assertFalse(fr1b.equals(fr2a)); + assertFalse(fr1b.equals(fr2b)); + assertFalse(fr2a.equals(fr2b)); + + assertFalse(fr1a.equals(withIdFieldNameAndValue(fr1a.androidID, fr1a.fieldName, fr1b.fieldValue))); + assertFalse(fr1a.equals(fr1a.copyWithIDs(fr2a.guid, 9999))); + assertTrue(fr1a.equals(fr1a)); + } + + @Test + public void testEqualsForDeleted() { + FormHistoryRecord fr1 = withIdFieldNameAndValue(0, "username1", "Alice"); + FormHistoryRecord fr2 = (FormHistoryRecord)fr1.copyWithIDs(fr1.guid, fr1.androidID); + assertTrue(fr1.equals(fr2)); + fr1.deleted = true; + assertFalse(fr1.equals(fr2)); + fr2.deleted = true; + assertTrue(fr1.equals(fr2)); + FormHistoryRecord fr3 = (FormHistoryRecord)fr2.copyWithIDs(Utils.generateGuid(), 9999); + assertFalse(fr2.equals(fr3)); + } + + @Test + public void testTTL() { + FormHistoryRecord fr = withIdFieldNameAndValue(0, "username", "aUsername"); + assertEquals(FormHistoryRecord.FORMS_TTL, fr.ttl); + CryptoRecord rec = fr.getEnvelope(); + assertEquals(FormHistoryRecord.FORMS_TTL, rec.ttl); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/downloaders/BatchingDownloaderDelegateTest.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/downloaders/BatchingDownloaderDelegateTest.java new file mode 100644 index 000000000..da2bbac18 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/downloaders/BatchingDownloaderDelegateTest.java @@ -0,0 +1,186 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.sync.repositories.downloaders; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.sync.CryptoRecord; +import org.mozilla.gecko.sync.HTTPFailureException; +import org.mozilla.gecko.sync.InfoCollections; +import org.mozilla.gecko.sync.InfoConfiguration; +import org.mozilla.gecko.sync.net.SyncResponse; +import org.mozilla.gecko.sync.net.SyncStorageCollectionRequest; +import org.mozilla.gecko.sync.net.SyncStorageResponse; +import org.mozilla.gecko.sync.repositories.Server11Repository; +import org.mozilla.gecko.sync.repositories.Server11RepositorySession; +import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFetchRecordsDelegate; +import org.mozilla.gecko.sync.repositories.domain.Record; + +import java.net.URI; +import java.util.concurrent.ExecutorService; + +import ch.boye.httpclientandroidlib.ProtocolVersion; +import ch.boye.httpclientandroidlib.client.ClientProtocolException; +import ch.boye.httpclientandroidlib.message.BasicHttpResponse; +import ch.boye.httpclientandroidlib.message.BasicStatusLine; + +import static org.junit.Assert.*; + +@RunWith(TestRunner.class) +public class BatchingDownloaderDelegateTest { + private Server11Repository server11Repository; + private Server11RepositorySession repositorySession; + private MockDownloader mockDownloader; + private String DEFAULT_COLLECTION_URL = "http://dummy.url/"; + + class MockDownloader extends BatchingDownloader { + public boolean isSuccess = false; + public boolean isFetched = false; + public boolean isFailure = false; + public Exception ex; + + public MockDownloader(Server11Repository repository, Server11RepositorySession repositorySession) { + super(repository, repositorySession); + } + + @Override + public void onFetchCompleted(SyncStorageResponse response, + final RepositorySessionFetchRecordsDelegate fetchRecordsDelegate, + final SyncStorageCollectionRequest request, + long l, long newerTimestamp, boolean full, String sort, String ids) { + this.isSuccess = true; + } + + @Override + public void onFetchFailed(final Exception ex, + final RepositorySessionFetchRecordsDelegate fetchRecordsDelegate, + final SyncStorageCollectionRequest request) { + this.isFailure = true; + this.ex = ex; + } + + @Override + public void onFetchedRecord(CryptoRecord record, + RepositorySessionFetchRecordsDelegate fetchRecordsDelegate) { + this.isFetched = true; + } + } + + class SimpleSessionFetchRecordsDelegate implements RepositorySessionFetchRecordsDelegate { + @Override + public void onFetchFailed(Exception ex, Record record) { + + } + + @Override + public void onFetchedRecord(Record record) { + + } + + @Override + public void onFetchCompleted(long fetchEnd) { + + } + + @Override + public RepositorySessionFetchRecordsDelegate deferredFetchDelegate(ExecutorService executor) { + return null; + } + } + + @Before + public void setUp() throws Exception { + server11Repository = new Server11Repository( + "dummyCollection", + DEFAULT_COLLECTION_URL, + null, + new InfoCollections(), + new InfoConfiguration()); + repositorySession = new Server11RepositorySession(server11Repository); + mockDownloader = new MockDownloader(server11Repository, repositorySession); + } + + @Test + public void testIfUnmodifiedSince() throws Exception { + BatchingDownloader downloader = new BatchingDownloader(server11Repository, repositorySession); + RepositorySessionFetchRecordsDelegate delegate = new SimpleSessionFetchRecordsDelegate(); + BatchingDownloaderDelegate downloaderDelegate = new BatchingDownloaderDelegate(downloader, delegate, + new SyncStorageCollectionRequest(new URI(DEFAULT_COLLECTION_URL)), 0, 0, true, null, null); + String lastModified = "12345678"; + SyncStorageResponse response = makeSyncStorageResponse(200, lastModified); + downloaderDelegate.handleRequestSuccess(response); + assertEquals(lastModified, downloaderDelegate.ifUnmodifiedSince()); + } + + @Test + public void testSuccess() throws Exception { + BatchingDownloaderDelegate downloaderDelegate = new BatchingDownloaderDelegate(mockDownloader, null, + new SyncStorageCollectionRequest(new URI(DEFAULT_COLLECTION_URL)), 0, 0, true, null, null); + SyncStorageResponse response = makeSyncStorageResponse(200, "12345678"); + downloaderDelegate.handleRequestSuccess(response); + assertTrue(mockDownloader.isSuccess); + assertFalse(mockDownloader.isFailure); + assertFalse(mockDownloader.isFetched); + } + + @Test + public void testFailureMissingLMHeader() throws Exception { + BatchingDownloaderDelegate downloaderDelegate = new BatchingDownloaderDelegate(mockDownloader, null, + new SyncStorageCollectionRequest(new URI(DEFAULT_COLLECTION_URL)), 0, 0, true, null, null); + SyncStorageResponse response = makeSyncStorageResponse(200, null); + downloaderDelegate.handleRequestSuccess(response); + assertTrue(mockDownloader.isFailure); + assertEquals(IllegalStateException.class, mockDownloader.ex.getClass()); + assertFalse(mockDownloader.isSuccess); + assertFalse(mockDownloader.isFetched); + } + + @Test + public void testFailureHTTPException() throws Exception { + BatchingDownloaderDelegate downloaderDelegate = new BatchingDownloaderDelegate(mockDownloader, null, + new SyncStorageCollectionRequest(new URI(DEFAULT_COLLECTION_URL)), 0, 0, true, null, null); + SyncStorageResponse response = makeSyncStorageResponse(400, null); + downloaderDelegate.handleRequestFailure(response); + assertTrue(mockDownloader.isFailure); + assertEquals(HTTPFailureException.class, mockDownloader.ex.getClass()); + assertFalse(mockDownloader.isSuccess); + assertFalse(mockDownloader.isFetched); + } + + @Test + public void testFailureRequestError() throws Exception { + BatchingDownloaderDelegate downloaderDelegate = new BatchingDownloaderDelegate(mockDownloader, null, + new SyncStorageCollectionRequest(new URI(DEFAULT_COLLECTION_URL)), 0, 0, true, null, null); + downloaderDelegate.handleRequestError(new ClientProtocolException()); + assertTrue(mockDownloader.isFailure); + assertEquals(ClientProtocolException.class, mockDownloader.ex.getClass()); + assertFalse(mockDownloader.isSuccess); + assertFalse(mockDownloader.isFetched); + } + + @Test + public void testFetchRecord() throws Exception { + BatchingDownloaderDelegate downloaderDelegate = new BatchingDownloaderDelegate(mockDownloader, null, + new SyncStorageCollectionRequest(new URI(DEFAULT_COLLECTION_URL)), 0, 0, true, null, null); + CryptoRecord record = new CryptoRecord(); + downloaderDelegate.handleWBO(record); + assertTrue(mockDownloader.isFetched); + assertFalse(mockDownloader.isSuccess); + assertFalse(mockDownloader.isFailure); + } + + private SyncStorageResponse makeSyncStorageResponse(int code, String lastModified) { + BasicHttpResponse response = new BasicHttpResponse( + new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), code, null)); + + if (lastModified != null) { + response.addHeader(SyncResponse.X_LAST_MODIFIED, lastModified); + } + + return new SyncStorageResponse(response); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/downloaders/BatchingDownloaderTest.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/downloaders/BatchingDownloaderTest.java new file mode 100644 index 000000000..fbbd9cae9 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/downloaders/BatchingDownloaderTest.java @@ -0,0 +1,543 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.sync.repositories.downloaders; + +import android.support.annotation.NonNull; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.sync.CryptoRecord; +import org.mozilla.gecko.sync.InfoCollections; +import org.mozilla.gecko.sync.InfoConfiguration; +import org.mozilla.gecko.sync.net.AuthHeaderProvider; +import org.mozilla.gecko.sync.net.SyncResponse; +import org.mozilla.gecko.sync.net.SyncStorageCollectionRequest; +import org.mozilla.gecko.sync.net.SyncStorageResponse; +import org.mozilla.gecko.sync.repositories.Repository; +import org.mozilla.gecko.sync.repositories.Server11Repository; +import org.mozilla.gecko.sync.repositories.Server11RepositorySession; +import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFetchRecordsDelegate; +import org.mozilla.gecko.sync.repositories.domain.Record; + +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.concurrent.ExecutorService; + +import ch.boye.httpclientandroidlib.ProtocolVersion; +import ch.boye.httpclientandroidlib.message.BasicHttpResponse; +import ch.boye.httpclientandroidlib.message.BasicStatusLine; + +import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; + +@RunWith(TestRunner.class) +public class BatchingDownloaderTest { + private MockSever11Repository serverRepository; + private Server11RepositorySession repositorySession; + private MockSessionFetchRecordsDelegate sessionFetchRecordsDelegate; + private MockDownloader mockDownloader; + private String DEFAULT_COLLECTION_NAME = "dummyCollection"; + private String DEFAULT_COLLECTION_URL = "http://dummy.url/"; + private long DEFAULT_NEWER = 1; + private String DEFAULT_SORT = "index"; + private String DEFAULT_IDS = "1"; + private String DEFAULT_LMHEADER = "12345678"; + + class MockSessionFetchRecordsDelegate implements RepositorySessionFetchRecordsDelegate { + public boolean isFailure; + public boolean isFetched; + public boolean isSuccess; + public Exception ex; + public Record record; + + @Override + public void onFetchFailed(Exception ex, Record record) { + this.isFailure = true; + this.ex = ex; + this.record = record; + } + + @Override + public void onFetchedRecord(Record record) { + this.isFetched = true; + this.record = record; + } + + @Override + public void onFetchCompleted(long fetchEnd) { + this.isSuccess = true; + } + + @Override + public RepositorySessionFetchRecordsDelegate deferredFetchDelegate(ExecutorService executor) { + return null; + } + } + + class MockRequest extends SyncStorageCollectionRequest { + + public MockRequest(URI uri) { + super(uri); + } + + @Override + public void get() { + + } + } + + class MockDownloader extends BatchingDownloader { + public long newer; + public long limit; + public boolean full; + public String sort; + public String ids; + public String offset; + public boolean abort; + + public MockDownloader(Server11Repository repository, Server11RepositorySession repositorySession) { + super(repository, repositorySession); + } + + @Override + public void fetchWithParameters(long newer, + long batchLimit, + boolean full, + String sort, + String ids, + SyncStorageCollectionRequest request, + RepositorySessionFetchRecordsDelegate fetchRecordsDelegate) + throws UnsupportedEncodingException, URISyntaxException { + this.newer = newer; + this.limit = batchLimit; + this.full = full; + this.sort = sort; + this.ids = ids; + MockRequest mockRequest = new MockRequest(new URI(DEFAULT_COLLECTION_URL)); + super.fetchWithParameters(newer, batchLimit, full, sort, ids, mockRequest, fetchRecordsDelegate); + } + + @Override + public void abortRequests() { + this.abort = true; + } + + @Override + public SyncStorageCollectionRequest makeSyncStorageCollectionRequest(long newer, + long batchLimit, + boolean full, + String sort, + String ids, + String offset) + throws URISyntaxException, UnsupportedEncodingException { + this.offset = offset; + return super.makeSyncStorageCollectionRequest(newer, batchLimit, full, sort, ids, offset); + } + } + + class MockSever11Repository extends Server11Repository { + public MockSever11Repository(@NonNull String collection, @NonNull String storageURL, + AuthHeaderProvider authHeaderProvider, @NonNull InfoCollections infoCollections, + @NonNull InfoConfiguration infoConfiguration) throws URISyntaxException { + super(collection, storageURL, authHeaderProvider, infoCollections, infoConfiguration); + } + + @Override + public long getDefaultTotalLimit() { + return 200; + } + } + + class MockRepositorySession extends Server11RepositorySession { + public boolean abort; + + public MockRepositorySession(Repository repository) { + super(repository); + } + + @Override + public void abort() { + this.abort = true; + } + } + + @Before + public void setUp() throws Exception { + sessionFetchRecordsDelegate = new MockSessionFetchRecordsDelegate(); + + serverRepository = new MockSever11Repository(DEFAULT_COLLECTION_NAME, DEFAULT_COLLECTION_URL, null, + new InfoCollections(), new InfoConfiguration()); + repositorySession = new Server11RepositorySession(serverRepository); + mockDownloader = new MockDownloader(serverRepository, repositorySession); + } + + @Test + public void testFlattenId() { + String[] emptyGuid = new String[]{}; + String flatten = BatchingDownloader.flattenIDs(emptyGuid); + assertEquals("", flatten); + + String guid0 = "123456789abc"; + String[] singleGuid = new String[1]; + singleGuid[0] = guid0; + flatten = BatchingDownloader.flattenIDs(singleGuid); + assertEquals("123456789abc", flatten); + + String guid1 = "456789abc"; + String guid2 = "789abc"; + String[] multiGuid = new String[3]; + multiGuid[0] = guid0; + multiGuid[1] = guid1; + multiGuid[2] = guid2; + flatten = BatchingDownloader.flattenIDs(multiGuid); + assertEquals("123456789abc,456789abc,789abc", flatten); + } + + @Test + public void testEncodeParam() throws Exception { + String param = "123&123"; + String encodedParam = mockDownloader.encodeParam(param); + assertEquals("123%26123", encodedParam); + } + + @Test(expected=IllegalArgumentException.class) + public void testOverTotalLimit() throws Exception { + // Per-batch limits exceed total. + Server11Repository repository = new Server11Repository(DEFAULT_COLLECTION_NAME, DEFAULT_COLLECTION_URL, + null, new InfoCollections(), new InfoConfiguration()) { + @Override + public long getDefaultTotalLimit() { + return 100; + } + @Override + public long getDefaultBatchLimit() { + return 200; + } + }; + MockDownloader mockDownloader = new MockDownloader(repository, repositorySession); + + assertNull(mockDownloader.getLastModified()); + mockDownloader.fetchSince(DEFAULT_NEWER, sessionFetchRecordsDelegate); + } + + @Test + public void testTotalLimit() throws Exception { + // Total and per-batch limits are the same. + Server11Repository repository = new Server11Repository(DEFAULT_COLLECTION_NAME, DEFAULT_COLLECTION_URL, + null, new InfoCollections(), new InfoConfiguration()) { + @Override + public long getDefaultTotalLimit() { + return 100; + } + @Override + public long getDefaultBatchLimit() { + return 100; + } + }; + MockDownloader mockDownloader = new MockDownloader(repository, repositorySession); + + assertNull(mockDownloader.getLastModified()); + mockDownloader.fetchSince(DEFAULT_NEWER, sessionFetchRecordsDelegate); + + SyncStorageResponse response = makeSyncStorageResponse(200, DEFAULT_LMHEADER, "100", "100"); + SyncStorageCollectionRequest request = new SyncStorageCollectionRequest(new URI(DEFAULT_COLLECTION_URL)); + long limit = repository.getDefaultBatchLimit(); + mockDownloader.onFetchCompleted(response, sessionFetchRecordsDelegate, request, + DEFAULT_NEWER, limit, true, DEFAULT_SORT, DEFAULT_IDS); + + assertEquals(DEFAULT_LMHEADER, mockDownloader.getLastModified()); + assertTrue(sessionFetchRecordsDelegate.isSuccess); + assertFalse(sessionFetchRecordsDelegate.isFetched); + assertFalse(sessionFetchRecordsDelegate.isFailure); + } + + @Test + public void testOverHalfOfTotalLimit() throws Exception { + // Per-batch limit is just a bit lower than total. + Server11Repository repository = new Server11Repository(DEFAULT_COLLECTION_NAME, DEFAULT_COLLECTION_URL, + null, new InfoCollections(), new InfoConfiguration()) { + @Override + public long getDefaultTotalLimit() { + return 100; + } + @Override + public long getDefaultBatchLimit() { + return 75; + } + }; + MockDownloader mockDownloader = new MockDownloader(repository, repositorySession); + + assertNull(mockDownloader.getLastModified()); + mockDownloader.fetchSince(DEFAULT_NEWER, sessionFetchRecordsDelegate); + + String offsetHeader = "75"; + String recordsHeader = "75"; + SyncStorageResponse response = makeSyncStorageResponse(200, DEFAULT_LMHEADER, offsetHeader, recordsHeader); + SyncStorageCollectionRequest request = new SyncStorageCollectionRequest(new URI(DEFAULT_COLLECTION_URL)); + long limit = repository.getDefaultBatchLimit(); + + mockDownloader.onFetchCompleted(response, sessionFetchRecordsDelegate, request, DEFAULT_NEWER, + limit, true, DEFAULT_SORT, DEFAULT_IDS); + + assertEquals(DEFAULT_LMHEADER, mockDownloader.getLastModified()); + // Verify the same parameters are used in the next fetch. + assertSameParameters(mockDownloader, limit); + assertEquals(offsetHeader, mockDownloader.offset); + assertFalse(sessionFetchRecordsDelegate.isSuccess); + assertFalse(sessionFetchRecordsDelegate.isFetched); + assertFalse(sessionFetchRecordsDelegate.isFailure); + + // The next batch, we still have an offset token but we complete our fetch since we have reached the total limit. + offsetHeader = "150"; + response = makeSyncStorageResponse(200, DEFAULT_LMHEADER, offsetHeader, recordsHeader); + mockDownloader.onFetchCompleted(response, sessionFetchRecordsDelegate, request, DEFAULT_NEWER, + limit, true, DEFAULT_SORT, DEFAULT_IDS); + + assertEquals(DEFAULT_LMHEADER, mockDownloader.getLastModified()); + assertTrue(sessionFetchRecordsDelegate.isSuccess); + assertFalse(sessionFetchRecordsDelegate.isFetched); + assertFalse(sessionFetchRecordsDelegate.isFailure); + } + + @Test + public void testHalfOfTotalLimit() throws Exception { + // Per-batch limit is half of total. + Server11Repository repository = new Server11Repository(DEFAULT_COLLECTION_NAME, DEFAULT_COLLECTION_URL, + null, new InfoCollections(), new InfoConfiguration()) { + @Override + public long getDefaultTotalLimit() { + return 100; + } + @Override + public long getDefaultBatchLimit() { + return 50; + } + }; + mockDownloader = new MockDownloader(repository, repositorySession); + + assertNull(mockDownloader.getLastModified()); + mockDownloader.fetchSince(DEFAULT_NEWER, sessionFetchRecordsDelegate); + + String offsetHeader = "50"; + String recordsHeader = "50"; + SyncStorageResponse response = makeSyncStorageResponse(200, DEFAULT_LMHEADER, offsetHeader, recordsHeader); + SyncStorageCollectionRequest request = new SyncStorageCollectionRequest(new URI(DEFAULT_COLLECTION_URL)); + long limit = repository.getDefaultBatchLimit(); + mockDownloader.onFetchCompleted(response, sessionFetchRecordsDelegate, request, DEFAULT_NEWER, + limit, true, DEFAULT_SORT, DEFAULT_IDS); + + assertEquals(DEFAULT_LMHEADER, mockDownloader.getLastModified()); + // Verify the same parameters are used in the next fetch. + assertSameParameters(mockDownloader, limit); + assertEquals(offsetHeader, mockDownloader.offset); + assertFalse(sessionFetchRecordsDelegate.isSuccess); + assertFalse(sessionFetchRecordsDelegate.isFetched); + assertFalse(sessionFetchRecordsDelegate.isFailure); + + // The next batch, we still have an offset token but we complete our fetch since we have reached the total limit. + offsetHeader = "100"; + response = makeSyncStorageResponse(200, DEFAULT_LMHEADER, offsetHeader, recordsHeader); + mockDownloader.onFetchCompleted(response, sessionFetchRecordsDelegate, request, DEFAULT_NEWER, + limit, true, DEFAULT_SORT, DEFAULT_IDS); + + assertEquals(DEFAULT_LMHEADER, mockDownloader.getLastModified()); + assertTrue(sessionFetchRecordsDelegate.isSuccess); + assertFalse(sessionFetchRecordsDelegate.isFetched); + assertFalse(sessionFetchRecordsDelegate.isFailure); + } + + @Test + public void testFractionOfTotalLimit() throws Exception { + // Per-batch limit is a small fraction of the total. + Server11Repository repository = new Server11Repository(DEFAULT_COLLECTION_NAME, DEFAULT_COLLECTION_URL, + null, new InfoCollections(), new InfoConfiguration()) { + @Override + public long getDefaultTotalLimit() { + return 100; + } + @Override + public long getDefaultBatchLimit() { + return 25; + } + }; + mockDownloader = new MockDownloader(repository, repositorySession); + + assertNull(mockDownloader.getLastModified()); + mockDownloader.fetchSince(DEFAULT_NEWER, sessionFetchRecordsDelegate); + + String offsetHeader = "25"; + String recordsHeader = "25"; + SyncStorageResponse response = makeSyncStorageResponse(200, DEFAULT_LMHEADER, offsetHeader, recordsHeader); + SyncStorageCollectionRequest request = new SyncStorageCollectionRequest(new URI(DEFAULT_COLLECTION_URL)); + long limit = repository.getDefaultBatchLimit(); + mockDownloader.onFetchCompleted(response, sessionFetchRecordsDelegate, request, DEFAULT_NEWER, + limit, true, DEFAULT_SORT, DEFAULT_IDS); + + assertEquals(DEFAULT_LMHEADER, mockDownloader.getLastModified()); + // Verify the same parameters are used in the next fetch. + assertSameParameters(mockDownloader, limit); + assertEquals(offsetHeader, mockDownloader.offset); + assertFalse(sessionFetchRecordsDelegate.isSuccess); + assertFalse(sessionFetchRecordsDelegate.isFetched); + assertFalse(sessionFetchRecordsDelegate.isFailure); + + // The next batch, we still have an offset token and has not exceed the total limit. + offsetHeader = "50"; + response = makeSyncStorageResponse(200, DEFAULT_LMHEADER, offsetHeader, recordsHeader); + mockDownloader.onFetchCompleted(response, sessionFetchRecordsDelegate, request, DEFAULT_NEWER, + limit, true, DEFAULT_SORT, DEFAULT_IDS); + + assertEquals(DEFAULT_LMHEADER, mockDownloader.getLastModified()); + // Verify the same parameters are used in the next fetch. + assertSameParameters(mockDownloader, limit); + assertEquals(offsetHeader, mockDownloader.offset); + assertFalse(sessionFetchRecordsDelegate.isSuccess); + assertFalse(sessionFetchRecordsDelegate.isFetched); + assertFalse(sessionFetchRecordsDelegate.isFailure); + + // The next batch, we still have an offset token and has not exceed the total limit. + offsetHeader = "75"; + response = makeSyncStorageResponse(200, DEFAULT_LMHEADER, offsetHeader, recordsHeader); + mockDownloader.onFetchCompleted(response, sessionFetchRecordsDelegate, request, DEFAULT_NEWER, + limit, true, DEFAULT_SORT, DEFAULT_IDS); + + assertEquals(DEFAULT_LMHEADER, mockDownloader.getLastModified()); + // Verify the same parameters are used in the next fetch. + assertSameParameters(mockDownloader, limit); + assertEquals(offsetHeader, mockDownloader.offset); + assertFalse(sessionFetchRecordsDelegate.isSuccess); + assertFalse(sessionFetchRecordsDelegate.isFetched); + assertFalse(sessionFetchRecordsDelegate.isFailure); + + // The next batch, we still have an offset token but we complete our fetch since we have reached the total limit. + offsetHeader = "100"; + response = makeSyncStorageResponse(200, DEFAULT_LMHEADER, offsetHeader, recordsHeader); + mockDownloader.onFetchCompleted(response, sessionFetchRecordsDelegate, request, DEFAULT_NEWER, + limit, true, DEFAULT_SORT, DEFAULT_IDS); + + assertEquals(DEFAULT_LMHEADER, mockDownloader.getLastModified()); + assertTrue(sessionFetchRecordsDelegate.isSuccess); + assertFalse(sessionFetchRecordsDelegate.isFetched); + assertFalse(sessionFetchRecordsDelegate.isFailure); + } + + @Test + public void testFailureLMChangedMultiBatch() throws Exception { + assertNull(mockDownloader.getLastModified()); + + String lmHeader = "12345678"; + String offsetHeader = "100"; + SyncStorageResponse response = makeSyncStorageResponse(200, lmHeader, offsetHeader, null); + SyncStorageCollectionRequest request = new SyncStorageCollectionRequest(new URI("http://dummy.url")); + long limit = 1; + mockDownloader.onFetchCompleted(response, sessionFetchRecordsDelegate, request, DEFAULT_NEWER, + limit, true, DEFAULT_SORT, DEFAULT_IDS); + + assertEquals(lmHeader, mockDownloader.getLastModified()); + // Verify the same parameters are used in the next fetch. + assertEquals(DEFAULT_NEWER, mockDownloader.newer); + assertEquals(limit, mockDownloader.limit); + assertTrue(mockDownloader.full); + assertEquals(DEFAULT_SORT, mockDownloader.sort); + assertEquals(DEFAULT_IDS, mockDownloader.ids); + assertEquals(offsetHeader, mockDownloader.offset); + assertFalse(sessionFetchRecordsDelegate.isSuccess); + assertFalse(sessionFetchRecordsDelegate.isFetched); + assertFalse(sessionFetchRecordsDelegate.isFailure); + + // Last modified header somehow changed. + lmHeader = "10000000"; + response = makeSyncStorageResponse(200, lmHeader, offsetHeader, null); + mockDownloader.onFetchCompleted(response, sessionFetchRecordsDelegate, request, DEFAULT_NEWER, + limit, true, DEFAULT_SORT, DEFAULT_IDS); + + assertNotEquals(lmHeader, mockDownloader.getLastModified()); + assertTrue(mockDownloader.abort); + assertFalse(sessionFetchRecordsDelegate.isSuccess); + assertFalse(sessionFetchRecordsDelegate.isFetched); + assertTrue(sessionFetchRecordsDelegate.isFailure); + } + + @Test + public void testFailureMissingLMMultiBatch() throws Exception { + assertNull(mockDownloader.getLastModified()); + + String offsetHeader = "100"; + SyncStorageResponse response = makeSyncStorageResponse(200, null, offsetHeader, null); + SyncStorageCollectionRequest request = new SyncStorageCollectionRequest(new URI("http://dummy.url")); + mockDownloader.onFetchCompleted(response, sessionFetchRecordsDelegate, request, DEFAULT_NEWER, + 1, true, DEFAULT_SORT, DEFAULT_IDS); + + // Last modified header somehow missing from response. + assertNull(null, mockDownloader.getLastModified()); + assertTrue(mockDownloader.abort); + assertFalse(sessionFetchRecordsDelegate.isSuccess); + assertFalse(sessionFetchRecordsDelegate.isFetched); + assertTrue(sessionFetchRecordsDelegate.isFailure); + } + + @Test + public void testFailureException() throws Exception { + Exception ex = new IllegalStateException(); + SyncStorageCollectionRequest request = new SyncStorageCollectionRequest(new URI("http://dummy.url")); + mockDownloader.onFetchFailed(ex, sessionFetchRecordsDelegate, request); + + assertFalse(sessionFetchRecordsDelegate.isSuccess); + assertFalse(sessionFetchRecordsDelegate.isFetched); + assertTrue(sessionFetchRecordsDelegate.isFailure); + assertEquals(ex.getClass(), sessionFetchRecordsDelegate.ex.getClass()); + assertNull(sessionFetchRecordsDelegate.record); + } + + @Test + public void testFetchRecord() { + CryptoRecord record = new CryptoRecord(); + mockDownloader.onFetchedRecord(record, sessionFetchRecordsDelegate); + + assertTrue(sessionFetchRecordsDelegate.isFetched); + assertFalse(sessionFetchRecordsDelegate.isSuccess); + assertFalse(sessionFetchRecordsDelegate.isFailure); + assertEquals(record, sessionFetchRecordsDelegate.record); + } + + @Test + public void testAbortRequests() { + MockRepositorySession mockRepositorySession = new MockRepositorySession(serverRepository); + BatchingDownloader downloader = new BatchingDownloader(serverRepository, mockRepositorySession); + assertFalse(mockRepositorySession.abort); + downloader.abortRequests(); + assertTrue(mockRepositorySession.abort); + } + + private void assertSameParameters(MockDownloader mockDownloader, long limit) { + assertEquals(DEFAULT_NEWER, mockDownloader.newer); + assertEquals(limit, mockDownloader.limit); + assertTrue(mockDownloader.full); + assertEquals(DEFAULT_SORT, mockDownloader.sort); + assertEquals(DEFAULT_IDS, mockDownloader.ids); + } + + private SyncStorageResponse makeSyncStorageResponse(int code, String lastModified, String offset, String records) { + BasicHttpResponse response = new BasicHttpResponse( + new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), code, null)); + + if (lastModified != null) { + response.addHeader(SyncResponse.X_LAST_MODIFIED, lastModified); + } + + if (offset != null) { + response.addHeader(SyncResponse.X_WEAVE_NEXT_OFFSET, offset); + } + + if (records != null) { + response.addHeader(SyncResponse.X_WEAVE_RECORDS, records); + } + + return new SyncStorageResponse(response); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/test/TestRepositorySessionBundle.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/test/TestRepositorySessionBundle.java new file mode 100644 index 000000000..e81d13640 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/test/TestRepositorySessionBundle.java @@ -0,0 +1,47 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.sync.repositories.test; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.sync.repositories.RepositorySessionBundle; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +@RunWith(TestRunner.class) +public class TestRepositorySessionBundle { + @Test + public void testSetGetTimestamp() { + RepositorySessionBundle bundle = new RepositorySessionBundle(-1); + assertEquals(-1, bundle.getTimestamp()); + + bundle.setTimestamp(10); + assertEquals(10, bundle.getTimestamp()); + } + + @Test + public void testBumpTimestamp() { + RepositorySessionBundle bundle = new RepositorySessionBundle(50); + assertEquals(50, bundle.getTimestamp()); + + bundle.bumpTimestamp(20); + assertEquals(50, bundle.getTimestamp()); + + bundle.bumpTimestamp(80); + assertEquals(80, bundle.getTimestamp()); + } + + @Test + public void testSerialize() throws Exception { + RepositorySessionBundle bundle = new RepositorySessionBundle(50); + + String json = bundle.toJSONString(); + assertNotNull(json); + + RepositorySessionBundle read = new RepositorySessionBundle(json); + assertEquals(50, read.getTimestamp()); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/test/TestSafeConstrainedServer11Repository.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/test/TestSafeConstrainedServer11Repository.java new file mode 100644 index 000000000..249a7831a --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/test/TestSafeConstrainedServer11Repository.java @@ -0,0 +1,144 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.sync.repositories.test; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.android.sync.test.helpers.HTTPServerTestHelper; +import org.mozilla.android.sync.test.helpers.MockServer; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.background.testhelpers.WaitHelper; +import org.mozilla.gecko.sync.InfoCollections; +import org.mozilla.gecko.sync.InfoConfiguration; +import org.mozilla.gecko.sync.JSONRecordFetcher; +import org.mozilla.gecko.sync.net.AuthHeaderProvider; +import org.mozilla.gecko.sync.repositories.RepositorySession; +import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate; +import org.mozilla.gecko.sync.stage.SafeConstrainedServer11Repository; +import org.simpleframework.http.Request; +import org.simpleframework.http.Response; + +import java.net.URISyntaxException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +@RunWith(TestRunner.class) +public class TestSafeConstrainedServer11Repository { + private static final int TEST_PORT = HTTPServerTestHelper.getTestPort(); + private static final String TEST_SERVER = "http://localhost:" + TEST_PORT + "/"; + private static final String TEST_USERNAME = "c6o7dvmr2c4ud2fyv6woz2u4zi22bcyd"; + private static final String TEST_BASE_PATH = "/1.1/" + TEST_USERNAME + "/"; + + public AuthHeaderProvider getAuthHeaderProvider() { + return null; + } + + protected final InfoCollections infoCollections = new InfoCollections(); + protected final InfoConfiguration infoConfiguration = new InfoConfiguration(); + + private class CountsMockServer extends MockServer { + public final AtomicInteger count = new AtomicInteger(0); + public final AtomicBoolean error = new AtomicBoolean(false); + + @Override + public void handle(Request request, Response response) { + final String path = request.getPath().getPath(); + if (error.get()) { + super.handle(request, response, 503, "Unavailable."); + return; + } + + if (!path.startsWith(TEST_BASE_PATH)) { + super.handle(request, response, 404, "Not Found"); + return; + } + + if (path.endsWith("info/collections")) { + super.handle(request, response, 200, "{\"rotary\": 123456789}"); + return; + } + + if (path.endsWith("info/collection_counts")) { + super.handle(request, response, 200, "{\"rotary\": " + count.get() + "}"); + return; + } + + super.handle(request, response); + } + } + + /** + * Ensure that a {@link SafeConstrainedServer11Repository} will advise + * skipping if the reported collection counts are higher than its limit. + */ + @Test + public void testShouldSkip() throws URISyntaxException { + final HTTPServerTestHelper data = new HTTPServerTestHelper(); + final CountsMockServer server = new CountsMockServer(); + data.startHTTPServer(server); + + try { + String countsURL = TEST_SERVER + TEST_BASE_PATH + "info/collection_counts"; + JSONRecordFetcher countFetcher = new JSONRecordFetcher(countsURL, getAuthHeaderProvider()); + String sort = "sortindex"; + String collection = "rotary"; + + final int TEST_LIMIT = 1000; + final SafeConstrainedServer11Repository repo = new SafeConstrainedServer11Repository( + collection, getCollectionURL(collection), null, infoCollections, infoConfiguration, + TEST_LIMIT, TEST_LIMIT, sort, countFetcher); + + final AtomicBoolean shouldSkipLots = new AtomicBoolean(false); + final AtomicBoolean shouldSkipFew = new AtomicBoolean(true); + final AtomicBoolean shouldSkip503 = new AtomicBoolean (false); + + WaitHelper.getTestWaiter().performWait(2000, new Runnable() { + @Override + public void run() { + repo.createSession(new RepositorySessionCreationDelegate() { + @Override + public void onSessionCreated(RepositorySession session) { + // Try with too many items. + server.count.set(TEST_LIMIT + 1); + shouldSkipLots.set(session.shouldSkip()); + + // … and few enough that we should sync. + server.count.set(TEST_LIMIT - 1); + shouldSkipFew.set(session.shouldSkip()); + + // Now try with an error response. We advise skipping if we can't + // fetch counts, because we'll try again later. + server.error.set(true); + shouldSkip503.set(session.shouldSkip()); + + WaitHelper.getTestWaiter().performNotify(); + } + + @Override + public void onSessionCreateFailed(Exception ex) { + WaitHelper.getTestWaiter().performNotify(ex); + } + + @Override + public RepositorySessionCreationDelegate deferredCreationDelegate() { + return this; + } + }, null); + } + }); + + Assert.assertTrue(shouldSkipLots.get()); + Assert.assertFalse(shouldSkipFew.get()); + Assert.assertTrue(shouldSkip503.get()); + + } finally { + data.stopHTTPServer(); + } + } + + protected String getCollectionURL(String collection) { + return TEST_BASE_PATH + "/storage/" + collection; + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/uploaders/BatchMetaTest.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/uploaders/BatchMetaTest.java new file mode 100644 index 000000000..2e136c117 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/uploaders/BatchMetaTest.java @@ -0,0 +1,282 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.sync.repositories.uploaders; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.TestRunner; + +import static org.junit.Assert.*; + +@RunWith(TestRunner.class) +public class BatchMetaTest { + private BatchMeta batchMeta; + private long byteLimit = 1024; + private long recordLimit = 5; + private Object lock = new Object(); + private Long collectionLastModified = 123L; + + @Before + public void setUp() throws Exception { + batchMeta = new BatchMeta(lock, byteLimit, recordLimit, collectionLastModified); + } + + @Test + public void testConstructor() { + assertEquals(batchMeta.collectionLastModified, collectionLastModified); + + BatchMeta otherBatchMeta = new BatchMeta(lock, byteLimit, recordLimit, null); + assertNull(otherBatchMeta.collectionLastModified); + } + + @Test + public void testGetLastModified() { + // Defaults to collection L-M + assertEquals(batchMeta.getLastModified(), Long.valueOf(123L)); + + try { + batchMeta.setLastModified(333L, true); + } catch (BatchingUploader.LastModifiedChangedUnexpectedly e) { + } catch (BatchingUploader.LastModifiedDidNotChange e) {} + + assertEquals(batchMeta.getLastModified(), Long.valueOf(333L)); + } + + @Test + public void testSetLastModified() { + assertEquals(batchMeta.getLastModified(), collectionLastModified); + + try { + batchMeta.setLastModified(123L, true); + assertEquals(batchMeta.getLastModified(), Long.valueOf(123L)); + } catch (BatchingUploader.LastModifiedChangedUnexpectedly e) { + fail("Should not check for modifications on first L-M set"); + } catch (BatchingUploader.LastModifiedDidNotChange e) { + fail("Should not check for modifications on first L-M set"); + } + + // Now the same, but passing in 'false' for "expecting to change". + batchMeta.reset(); + assertEquals(batchMeta.getLastModified(), collectionLastModified); + + try { + batchMeta.setLastModified(123L, false); + assertEquals(batchMeta.getLastModified(), Long.valueOf(123L)); + } catch (BatchingUploader.LastModifiedChangedUnexpectedly e) { + fail("Should not check for modifications on first L-M set"); + } catch (BatchingUploader.LastModifiedDidNotChange e) { + fail("Should not check for modifications on first L-M set"); + } + + // Test that we can't modify L-M when we're not expecting to + try { + batchMeta.setLastModified(333L, false); + } catch (BatchingUploader.LastModifiedChangedUnexpectedly e) { + assertTrue("Must throw when L-M changes unexpectedly", true); + } catch (BatchingUploader.LastModifiedDidNotChange e) { + fail("Not expecting did-not-change throw"); + } + assertEquals(batchMeta.getLastModified(), Long.valueOf(123L)); + + // Test that we can modify L-M when we're expecting to + try { + batchMeta.setLastModified(333L, true); + } catch (BatchingUploader.LastModifiedChangedUnexpectedly e) { + fail("Not expecting changed-unexpectedly throw"); + } catch (BatchingUploader.LastModifiedDidNotChange e) { + fail("Not expecting did-not-change throw"); + } + assertEquals(batchMeta.getLastModified(), Long.valueOf(333L)); + + // Test that we catch L-M modifications that expect to change but actually don't + try { + batchMeta.setLastModified(333L, true); + } catch (BatchingUploader.LastModifiedChangedUnexpectedly e) { + fail("Not expecting changed-unexpectedly throw"); + } catch (BatchingUploader.LastModifiedDidNotChange e) { + assertTrue("Expected-to-change-but-did-not-change didn't throw", true); + } + assertEquals(batchMeta.getLastModified(), Long.valueOf(333)); + } + + @Test + public void testSetToken() { + assertNull(batchMeta.getToken()); + + try { + batchMeta.setToken("MTIzNA", false); + } catch (BatchingUploader.TokenModifiedException e) { + fail("Should be able to set token for the first time"); + } + assertEquals("MTIzNA", batchMeta.getToken()); + + try { + batchMeta.setToken("XYCvNA", false); + } catch (BatchingUploader.TokenModifiedException e) { + assertTrue("Should not be able to modify a token", true); + } + assertEquals("MTIzNA", batchMeta.getToken()); + + try { + batchMeta.setToken("XYCvNA", true); + } catch (BatchingUploader.TokenModifiedException e) { + assertTrue("Should catch non-null tokens during onCommit sets", true); + } + assertEquals("MTIzNA", batchMeta.getToken()); + + try { + batchMeta.setToken(null, true); + } catch (BatchingUploader.TokenModifiedException e) { + fail("Should be able to set token to null during onCommit set"); + } + assertNull(batchMeta.getToken()); + } + + @Test + public void testRecordSucceeded() { + assertTrue(batchMeta.getSuccessRecordGuids().isEmpty()); + + batchMeta.recordSucceeded("guid1"); + + assertTrue(batchMeta.getSuccessRecordGuids().size() == 1); + assertTrue(batchMeta.getSuccessRecordGuids().contains("guid1")); + + try { + batchMeta.recordSucceeded(null); + fail(); + } catch (IllegalStateException e) { + assertTrue("Should not be able to 'succeed' a null guid", true); + } + } + + @Test + public void testByteLimits() { + assertTrue(batchMeta.canFit(0)); + + // Should just fit + assertTrue(batchMeta.canFit(byteLimit - BatchingUploader.PER_PAYLOAD_OVERHEAD_BYTE_COUNT)); + + // Can't fit a record due to payload overhead. + assertFalse(batchMeta.canFit(byteLimit)); + + assertFalse(batchMeta.canFit(byteLimit + BatchingUploader.PER_PAYLOAD_OVERHEAD_BYTE_COUNT)); + assertFalse(batchMeta.canFit(byteLimit * 1000)); + + long recordDelta = byteLimit / 2; + assertFalse(batchMeta.addAndEstimateIfFull(recordDelta)); + + // Record delta shouldn't fit due to payload overhead. + assertFalse(batchMeta.canFit(recordDelta)); + } + + @Test + public void testCountLimits() { + // Our record limit is 5, let's add 4. + assertFalse(batchMeta.addAndEstimateIfFull(1)); + assertFalse(batchMeta.addAndEstimateIfFull(1)); + assertFalse(batchMeta.addAndEstimateIfFull(1)); + assertFalse(batchMeta.addAndEstimateIfFull(1)); + + // 5th record still fits in + assertTrue(batchMeta.canFit(1)); + + // Add the 5th record + assertTrue(batchMeta.addAndEstimateIfFull(1)); + + // 6th record won't fit + assertFalse(batchMeta.canFit(1)); + } + + @Test + public void testNeedCommit() { + assertFalse(batchMeta.needToCommit()); + + assertFalse(batchMeta.addAndEstimateIfFull(1)); + + assertTrue(batchMeta.needToCommit()); + + assertFalse(batchMeta.addAndEstimateIfFull(1)); + assertFalse(batchMeta.addAndEstimateIfFull(1)); + assertFalse(batchMeta.addAndEstimateIfFull(1)); + + assertTrue(batchMeta.needToCommit()); + + batchMeta.reset(); + + assertFalse(batchMeta.needToCommit()); + } + + @Test + public void testAdd() { + // Ensure we account for payload overhead twice when the batch is empty. + // Payload overhead is either RECORDS_START or RECORDS_END, and for an empty payload + // we need both. + assertTrue(batchMeta.getByteCount() == 2 * BatchingUploader.PER_PAYLOAD_OVERHEAD_BYTE_COUNT); + assertTrue(batchMeta.getRecordCount() == 0); + + assertFalse(batchMeta.addAndEstimateIfFull(1)); + + assertTrue(batchMeta.getByteCount() == (1 + BatchingUploader.PER_PAYLOAD_OVERHEAD_BYTE_COUNT)); + assertTrue(batchMeta.getRecordCount() == 1); + + assertFalse(batchMeta.addAndEstimateIfFull(1)); + assertFalse(batchMeta.addAndEstimateIfFull(1)); + assertFalse(batchMeta.addAndEstimateIfFull(1)); + + assertTrue(batchMeta.getByteCount() == (4 + BatchingUploader.PER_PAYLOAD_OVERHEAD_BYTE_COUNT)); + assertTrue(batchMeta.getRecordCount() == 4); + + assertTrue(batchMeta.addAndEstimateIfFull(1)); + + try { + assertTrue(batchMeta.addAndEstimateIfFull(1)); + fail("BatchMeta should not let us insert records that won't fit"); + } catch (IllegalStateException e) { + assertTrue(true); + } + } + + @Test + public void testReset() { + assertTrue(batchMeta.getByteCount() == 2 * BatchingUploader.PER_PAYLOAD_OVERHEAD_BYTE_COUNT); + assertTrue(batchMeta.getRecordCount() == 0); + assertTrue(batchMeta.getSuccessRecordGuids().isEmpty()); + + // Shouldn't throw even if already empty + batchMeta.reset(); + assertTrue(batchMeta.getByteCount() == 2 * BatchingUploader.PER_PAYLOAD_OVERHEAD_BYTE_COUNT); + assertTrue(batchMeta.getRecordCount() == 0); + assertTrue(batchMeta.getSuccessRecordGuids().isEmpty()); + + assertFalse(batchMeta.addAndEstimateIfFull(1)); + batchMeta.recordSucceeded("guid1"); + try { + batchMeta.setToken("MTIzNA", false); + } catch (BatchingUploader.TokenModifiedException e) {} + try { + batchMeta.setLastModified(333L, true); + } catch (BatchingUploader.LastModifiedChangedUnexpectedly e) { + } catch (BatchingUploader.LastModifiedDidNotChange e) {} + assertEquals(Long.valueOf(333L), batchMeta.getLastModified()); + assertEquals("MTIzNA", batchMeta.getToken()); + assertTrue(batchMeta.getSuccessRecordGuids().size() == 1); + + batchMeta.reset(); + + // Counts must be reset + assertTrue(batchMeta.getByteCount() == 2 * BatchingUploader.PER_PAYLOAD_OVERHEAD_BYTE_COUNT); + assertTrue(batchMeta.getRecordCount() == 0); + assertTrue(batchMeta.getSuccessRecordGuids().isEmpty()); + + // Collection L-M shouldn't change + assertEquals(batchMeta.collectionLastModified, collectionLastModified); + + // Token must be reset + assertNull(batchMeta.getToken()); + + // L-M must be reverted to collection L-M + assertEquals(batchMeta.getLastModified(), collectionLastModified); + } +}
\ No newline at end of file diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/uploaders/BatchingUploaderTest.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/uploaders/BatchingUploaderTest.java new file mode 100644 index 000000000..5ce94b222 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/uploaders/BatchingUploaderTest.java @@ -0,0 +1,441 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.sync.repositories.uploaders; + +import android.support.annotation.NonNull; + +import static org.junit.Assert.*; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.MockRecord; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.sync.ExtendedJSONObject; +import org.mozilla.gecko.sync.InfoCollections; +import org.mozilla.gecko.sync.InfoConfiguration; +import org.mozilla.gecko.sync.Utils; +import org.mozilla.gecko.sync.repositories.Server11Repository; +import org.mozilla.gecko.sync.repositories.Server11RepositorySession; +import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionStoreDelegate; + +import java.net.URISyntaxException; +import java.util.Random; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; + +@RunWith(TestRunner.class) +public class BatchingUploaderTest { + class MockExecutorService implements Executor { + public int totalPayloads = 0; + public int commitPayloads = 0; + + @Override + public void execute(@NonNull Runnable command) { + ++totalPayloads; + if (((RecordUploadRunnable) command).isCommit) { + ++commitPayloads; + } + } + } + + class MockStoreDelegate implements RepositorySessionStoreDelegate { + public int storeFailed = 0; + public int storeSucceeded = 0; + public int storeCompleted = 0; + + @Override + public void onRecordStoreFailed(Exception ex, String recordGuid) { + ++storeFailed; + } + + @Override + public void onRecordStoreSucceeded(String guid) { + ++storeSucceeded; + } + + @Override + public void onStoreCompleted(long storeEnd) { + ++storeCompleted; + } + + @Override + public RepositorySessionStoreDelegate deferredStoreDelegate(ExecutorService executor) { + return null; + } + } + + private Executor workQueue; + private RepositorySessionStoreDelegate storeDelegate; + + @Before + public void setUp() throws Exception { + workQueue = new MockExecutorService(); + storeDelegate = new MockStoreDelegate(); + } + + @Test + public void testProcessEvenPayloadBatch() { + BatchingUploader uploader = makeConstrainedUploader(2, 4); + + MockRecord record = new MockRecord(Utils.generateGuid(), null, 0, false); + // 1st + uploader.process(record); + assertEquals(0, ((MockExecutorService) workQueue).totalPayloads); + assertEquals(0, ((MockExecutorService) workQueue).commitPayloads); + // 2nd -> payload full + uploader.process(record); + assertEquals(1, ((MockExecutorService) workQueue).totalPayloads); + assertEquals(0, ((MockExecutorService) workQueue).commitPayloads); + // 3rd + uploader.process(record); + assertEquals(1, ((MockExecutorService) workQueue).totalPayloads); + assertEquals(0, ((MockExecutorService) workQueue).commitPayloads); + // 4th -> batch & payload full + uploader.process(record); + assertEquals(2, ((MockExecutorService) workQueue).totalPayloads); + assertEquals(1, ((MockExecutorService) workQueue).commitPayloads); + // 5th + uploader.process(record); + assertEquals(2, ((MockExecutorService) workQueue).totalPayloads); + assertEquals(1, ((MockExecutorService) workQueue).commitPayloads); + // 6th -> payload full + uploader.process(record); + assertEquals(3, ((MockExecutorService) workQueue).totalPayloads); + assertEquals(1, ((MockExecutorService) workQueue).commitPayloads); + // 7th + uploader.process(record); + assertEquals(3, ((MockExecutorService) workQueue).totalPayloads); + assertEquals(1, ((MockExecutorService) workQueue).commitPayloads); + // 8th -> batch & payload full + uploader.process(record); + assertEquals(4, ((MockExecutorService) workQueue).totalPayloads); + assertEquals(2, ((MockExecutorService) workQueue).commitPayloads); + // 9th + uploader.process(record); + assertEquals(4, ((MockExecutorService) workQueue).totalPayloads); + assertEquals(2, ((MockExecutorService) workQueue).commitPayloads); + // 10th -> payload full + uploader.process(record); + assertEquals(5, ((MockExecutorService) workQueue).totalPayloads); + assertEquals(2, ((MockExecutorService) workQueue).commitPayloads); + // 11th + uploader.process(record); + assertEquals(5, ((MockExecutorService) workQueue).totalPayloads); + assertEquals(2, ((MockExecutorService) workQueue).commitPayloads); + // 12th -> batch & payload full + uploader.process(record); + assertEquals(6, ((MockExecutorService) workQueue).totalPayloads); + assertEquals(3, ((MockExecutorService) workQueue).commitPayloads); + // 13th + uploader.process(record); + assertEquals(6, ((MockExecutorService) workQueue).totalPayloads); + assertEquals(3, ((MockExecutorService) workQueue).commitPayloads); + } + + @Test + public void testProcessUnevenPayloadBatch() { + BatchingUploader uploader = makeConstrainedUploader(2, 5); + + MockRecord record = new MockRecord(Utils.generateGuid(), null, 0, false); + // 1st + uploader.process(record); + assertEquals(0, ((MockExecutorService) workQueue).totalPayloads); + assertEquals(0, ((MockExecutorService) workQueue).commitPayloads); + // 2nd -> payload full + uploader.process(record); + assertEquals(1, ((MockExecutorService) workQueue).totalPayloads); + assertEquals(0, ((MockExecutorService) workQueue).commitPayloads); + // 3rd + uploader.process(record); + assertEquals(1, ((MockExecutorService) workQueue).totalPayloads); + assertEquals(0, ((MockExecutorService) workQueue).commitPayloads); + // 4th -> payload full + uploader.process(record); + assertEquals(2, ((MockExecutorService) workQueue).totalPayloads); + assertEquals(0, ((MockExecutorService) workQueue).commitPayloads); + // 5th -> batch full + uploader.process(record); + assertEquals(3, ((MockExecutorService) workQueue).totalPayloads); + assertEquals(1, ((MockExecutorService) workQueue).commitPayloads); + // 6th -> starts new batch + uploader.process(record); + assertEquals(3, ((MockExecutorService) workQueue).totalPayloads); + assertEquals(1, ((MockExecutorService) workQueue).commitPayloads); + // 7th -> payload full + uploader.process(record); + assertEquals(4, ((MockExecutorService) workQueue).totalPayloads); + assertEquals(1, ((MockExecutorService) workQueue).commitPayloads); + // 8th + uploader.process(record); + assertEquals(4, ((MockExecutorService) workQueue).totalPayloads); + assertEquals(1, ((MockExecutorService) workQueue).commitPayloads); + // 9th -> payload full + uploader.process(record); + assertEquals(5, ((MockExecutorService) workQueue).totalPayloads); + assertEquals(1, ((MockExecutorService) workQueue).commitPayloads); + // 10th -> batch full + uploader.process(record); + assertEquals(6, ((MockExecutorService) workQueue).totalPayloads); + assertEquals(2, ((MockExecutorService) workQueue).commitPayloads); + // 11th -> starts new batch + uploader.process(record); + assertEquals(6, ((MockExecutorService) workQueue).totalPayloads); + assertEquals(2, ((MockExecutorService) workQueue).commitPayloads); + } + + @Test + public void testNonBatchingOptimization() { + BatchingUploader uploader = makeConstrainedUploader(2, 4); + + MockRecord record = new MockRecord(Utils.generateGuid(), null, 0, false); + // 1st + uploader.process(record); + assertEquals(0, ((MockExecutorService) workQueue).totalPayloads); + assertEquals(0, ((MockExecutorService) workQueue).commitPayloads); + // 2nd + uploader.process(record); + assertEquals(1, ((MockExecutorService) workQueue).totalPayloads); + assertEquals(0, ((MockExecutorService) workQueue).commitPayloads); + // 3rd + uploader.process(record); + assertEquals(1, ((MockExecutorService) workQueue).totalPayloads); + assertEquals(0, ((MockExecutorService) workQueue).commitPayloads); + // 4th + uploader.process(record); + assertEquals(2, ((MockExecutorService) workQueue).totalPayloads); + assertEquals(1, ((MockExecutorService) workQueue).commitPayloads); + + // 5th + uploader.process(record); + assertEquals(2, ((MockExecutorService) workQueue).totalPayloads); + assertEquals(1, ((MockExecutorService) workQueue).commitPayloads); + + // And now we tell uploader that batching isn't supported. + // It shouldn't bother with batches from now on, just payloads. + uploader.setInBatchingMode(false); + + // 6th + uploader.process(record); + assertEquals(3, ((MockExecutorService) workQueue).totalPayloads); + assertEquals(1, ((MockExecutorService) workQueue).commitPayloads); + + // 7th + uploader.process(record); + assertEquals(3, ((MockExecutorService) workQueue).totalPayloads); + assertEquals(1, ((MockExecutorService) workQueue).commitPayloads); + + // 8th + uploader.process(record); + assertEquals(4, ((MockExecutorService) workQueue).totalPayloads); + assertEquals(1, ((MockExecutorService) workQueue).commitPayloads); + + // 9th + uploader.process(record); + assertEquals(4, ((MockExecutorService) workQueue).totalPayloads); + assertEquals(1, ((MockExecutorService) workQueue).commitPayloads); + + // 10th + uploader.process(record); + assertEquals(5, ((MockExecutorService) workQueue).totalPayloads); + assertEquals(1, ((MockExecutorService) workQueue).commitPayloads); + } + + @Test + public void testPreemtiveUploadByteCounts() { + // While processing a record, if we know for sure that another one won't fit, + // we upload the payload. + BatchingUploader uploader = makeConstrainedUploader(3, 6); + + // Payload byte max: 1024; batch byte max: 4096 + MockRecord record = new MockRecord(Utils.generateGuid(), null, 0, false, 400); + + uploader.process(record); + assertEquals(0, ((MockExecutorService) workQueue).totalPayloads); + assertEquals(0, ((MockExecutorService) workQueue).commitPayloads); + + // After 2nd record, byte count is at 800+overhead. Our payload max is 1024, so it's unlikely + // we can fit another record at this pace. Expect payload to be uploaded. + uploader.process(record); + assertEquals(1, ((MockExecutorService) workQueue).totalPayloads); + assertEquals(0, ((MockExecutorService) workQueue).commitPayloads); + + // After this record, we'll have less than 124 bytes of room left in the payload. Expect upload. + record = new MockRecord(Utils.generateGuid(), null, 0, false, 970); + uploader.process(record); + assertEquals(2, ((MockExecutorService) workQueue).totalPayloads); + assertEquals(0, ((MockExecutorService) workQueue).commitPayloads); + + uploader.process(record); + assertEquals(3, ((MockExecutorService) workQueue).totalPayloads); + assertEquals(0, ((MockExecutorService) workQueue).commitPayloads); + + // At this point our byte count for the batch is at 3600+overhead; + // since we have just 496 bytes left in the batch, it's unlikely we'll fit another record. + // Expect a batch commit + uploader.process(record); + assertEquals(4, ((MockExecutorService) workQueue).totalPayloads); + assertEquals(1, ((MockExecutorService) workQueue).commitPayloads); + } + + @Test + public void testRandomPayloadSizesBatching() { + BatchingUploader uploader = makeConstrainedUploader(2, 4); + + final Random random = new Random(); + for (int i = 0; i < 15000; i++) { + uploader.process(new MockRecord(Utils.generateGuid(), null, 0, false, random.nextInt(15000))); + } + } + + @Test + public void testRandomPayloadSizesNonBatching() { + BatchingUploader uploader = makeConstrainedUploader(2, 4); + + final Random random = new Random(); + uploader.setInBatchingMode(false); + for (int i = 0; i < 15000; i++) { + uploader.process(new MockRecord(Utils.generateGuid(), null, 0, false, random.nextInt(15000))); + } + } + + @Test + public void testRandomPayloadSizesNonBatchingDelayed() { + BatchingUploader uploader = makeConstrainedUploader(2, 4); + + final Random random = new Random(); + // Delay telling uploader that batching isn't supported. + // Randomize how many records we wait for. + final int delay = random.nextInt(20); + for (int i = 0; i < 15000; i++) { + if (delay == i) { + uploader.setInBatchingMode(false); + } + uploader.process(new MockRecord(Utils.generateGuid(), null, 0, false, random.nextInt(15000))); + } + } + + @Test + public void testNoMoreRecordsAfterPayloadPost() { + BatchingUploader uploader = makeConstrainedUploader(2, 4); + + // Process two records (payload limit is also two, batch is four), + // and ensure that 'no more records' commits. + MockRecord record = new MockRecord(Utils.generateGuid(), null, 0, false); + uploader.process(record); + uploader.process(record); + uploader.setInBatchingMode(true); + uploader.commitIfNecessaryAfterLastPayload(); + // One will be a payload post, the other one is batch commit (empty payload) + assertEquals(2, ((MockExecutorService) workQueue).totalPayloads); + assertEquals(1, ((MockExecutorService) workQueue).commitPayloads); + } + + @Test + public void testNoMoreRecordsAfterPayloadPostWithOneRecordLeft() { + BatchingUploader uploader = makeConstrainedUploader(2, 4); + + // Process two records (payload limit is also two, batch is four), + // and ensure that 'no more records' commits. + MockRecord record = new MockRecord(Utils.generateGuid(), null, 0, false); + uploader.process(record); + uploader.process(record); + uploader.process(record); + uploader.commitIfNecessaryAfterLastPayload(); + // One will be a payload post, the other one is batch commit (one record payload) + assertEquals(2, ((MockExecutorService) workQueue).totalPayloads); + assertEquals(1, ((MockExecutorService) workQueue).commitPayloads); + } + + @Test + public void testNoMoreRecordsNoOp() { + BatchingUploader uploader = makeConstrainedUploader(2, 4); + + uploader.commitIfNecessaryAfterLastPayload(); + assertEquals(0, ((MockExecutorService) workQueue).totalPayloads); + assertEquals(0, ((MockExecutorService) workQueue).commitPayloads); + } + + @Test + public void testNoMoreRecordsNoOpAfterCommit() { + BatchingUploader uploader = makeConstrainedUploader(2, 4); + + MockRecord record = new MockRecord(Utils.generateGuid(), null, 0, false); + uploader.process(record); + uploader.process(record); + uploader.process(record); + uploader.process(record); + assertEquals(2, ((MockExecutorService) workQueue).totalPayloads); + assertEquals(1, ((MockExecutorService) workQueue).commitPayloads); + + uploader.commitIfNecessaryAfterLastPayload(); + assertEquals(2, ((MockExecutorService) workQueue).totalPayloads); + assertEquals(1, ((MockExecutorService) workQueue).commitPayloads); + } + + @Test + public void testNoMoreRecordsEvenNonBatching() { + BatchingUploader uploader = makeConstrainedUploader(2, 4); + + // Process two records (payload limit is also two, batch is four), + // set non-batching mode, and ensure that 'no more records' doesn't commit. + MockRecord record = new MockRecord(Utils.generateGuid(), null, 0, false); + uploader.process(record); + uploader.process(record); + uploader.setInBatchingMode(false); + uploader.commitIfNecessaryAfterLastPayload(); + // One will be a payload post, the other one is batch commit (one record payload) + assertEquals(1, ((MockExecutorService) workQueue).totalPayloads); + assertEquals(0, ((MockExecutorService) workQueue).commitPayloads); + } + + @Test + public void testNoMoreRecordsIncompletePayload() { + BatchingUploader uploader = makeConstrainedUploader(2, 4); + + // We have one record (payload limit is 2), and "no-more-records" signal should commit it. + MockRecord record = new MockRecord(Utils.generateGuid(), null, 0, false); + uploader.process(record); + + uploader.commitIfNecessaryAfterLastPayload(); + assertEquals(1, ((MockExecutorService) workQueue).totalPayloads); + assertEquals(1, ((MockExecutorService) workQueue).commitPayloads); + } + + private BatchingUploader makeConstrainedUploader(long maxPostRecords, long maxTotalRecords) { + Server11RepositorySession server11RepositorySession = new Server11RepositorySession( + makeCountConstrainedRepository(maxPostRecords, maxTotalRecords) + ); + server11RepositorySession.setStoreDelegate(storeDelegate); + return new BatchingUploader(server11RepositorySession, workQueue, storeDelegate); + } + + private Server11Repository makeCountConstrainedRepository(long maxPostRecords, long maxTotalRecords) { + return makeConstrainedRepository(1024, 1024, maxPostRecords, 4096, maxTotalRecords); + } + + private Server11Repository makeConstrainedRepository(long maxRequestBytes, long maxPostBytes, long maxPostRecords, long maxTotalBytes, long maxTotalRecords) { + ExtendedJSONObject infoConfigurationJSON = new ExtendedJSONObject(); + infoConfigurationJSON.put(InfoConfiguration.MAX_TOTAL_BYTES, maxTotalBytes); + infoConfigurationJSON.put(InfoConfiguration.MAX_TOTAL_RECORDS, maxTotalRecords); + infoConfigurationJSON.put(InfoConfiguration.MAX_POST_RECORDS, maxPostRecords); + infoConfigurationJSON.put(InfoConfiguration.MAX_POST_BYTES, maxPostBytes); + infoConfigurationJSON.put(InfoConfiguration.MAX_REQUEST_BYTES, maxRequestBytes); + + InfoConfiguration infoConfiguration = new InfoConfiguration(infoConfigurationJSON); + + try { + return new Server11Repository( + "dummyCollection", + "http://dummy.url/", + null, + new InfoCollections(), + infoConfiguration + ); + } catch (URISyntaxException e) { + // Won't throw, and this won't happen. + return null; + } + } +}
\ No newline at end of file diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/uploaders/PayloadTest.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/uploaders/PayloadTest.java new file mode 100644 index 000000000..b1d6dd9d0 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/uploaders/PayloadTest.java @@ -0,0 +1,137 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.sync.repositories.uploaders; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.TestRunner; + +import static org.junit.Assert.*; + +@RunWith(TestRunner.class) +public class PayloadTest { + private Payload payload; + private long byteLimit = 1024; + private long recordLimit = 5; + private Object lock = new Object(); + + @Before + public void setUp() throws Exception { + payload = new Payload(lock, byteLimit, recordLimit); + } + + @Test + public void testByteLimits() { + assertTrue(payload.canFit(0)); + + // Should just fit + assertTrue(payload.canFit(byteLimit - BatchingUploader.PER_PAYLOAD_OVERHEAD_BYTE_COUNT)); + + // Can't fit a record due to payload overhead. + assertFalse(payload.canFit(byteLimit)); + + assertFalse(payload.canFit(byteLimit + BatchingUploader.PER_PAYLOAD_OVERHEAD_BYTE_COUNT)); + assertFalse(payload.canFit(byteLimit * 1000)); + + long recordDelta = byteLimit / 2; + assertFalse(payload.addAndEstimateIfFull(recordDelta, new byte[0], null)); + + // Record delta shouldn't fit due to payload overhead. + assertFalse(payload.canFit(recordDelta)); + } + + @Test + public void testCountLimits() { + byte[] bytes = new byte[0]; + + // Our record limit is 5, let's add 4. + assertFalse(payload.addAndEstimateIfFull(1, bytes, null)); + assertFalse(payload.addAndEstimateIfFull(1, bytes, null)); + assertFalse(payload.addAndEstimateIfFull(1, bytes, null)); + assertFalse(payload.addAndEstimateIfFull(1, bytes, null)); + + // 5th record still fits in + assertTrue(payload.canFit(1)); + + // Add the 5th record + assertTrue(payload.addAndEstimateIfFull(1, bytes, null)); + + // 6th record won't fit + assertFalse(payload.canFit(1)); + } + + @Test + public void testAdd() { + assertTrue(payload.getByteCount() == 2 * BatchingUploader.PER_PAYLOAD_OVERHEAD_BYTE_COUNT); + assertTrue(payload.getRecordCount() == 0); + assertTrue(payload.isEmpty()); + assertTrue(payload.getRecordsBuffer().isEmpty()); + assertTrue(payload.getRecordGuidsBuffer().isEmpty()); + + try { + payload.addAndEstimateIfFull(1024); + fail("Simple add is not supported"); + } catch (UnsupportedOperationException e) { + assertTrue(true); + } + + byte[] recordBytes1 = new byte[100]; + assertFalse(payload.addAndEstimateIfFull(1, recordBytes1, "guid1")); + + assertTrue(payload.getRecordsBuffer().size() == 1); + assertTrue(payload.getRecordGuidsBuffer().size() == 1); + assertTrue(payload.getRecordGuidsBuffer().contains("guid1")); + assertTrue(payload.getRecordsBuffer().contains(recordBytes1)); + + assertTrue(payload.getByteCount() == (1 + BatchingUploader.PER_PAYLOAD_OVERHEAD_BYTE_COUNT)); + assertTrue(payload.getRecordCount() == 1); + + assertFalse(payload.isEmpty()); + + assertFalse(payload.addAndEstimateIfFull(1, recordBytes1, "guid2")); + assertFalse(payload.addAndEstimateIfFull(1, recordBytes1, "guid3")); + assertFalse(payload.addAndEstimateIfFull(1, recordBytes1, "guid4")); + + assertTrue(payload.getByteCount() == (4 + BatchingUploader.PER_PAYLOAD_OVERHEAD_BYTE_COUNT)); + assertTrue(payload.getRecordCount() == 4); + + assertTrue(payload.addAndEstimateIfFull(1, recordBytes1, "guid5")); + + try { + assertTrue(payload.addAndEstimateIfFull(1, recordBytes1, "guid6")); + fail("Payload should not let us insert records that won't fit"); + } catch (IllegalStateException e) { + assertTrue(true); + } + } + + @Test + public void testReset() { + assertTrue(payload.getByteCount() == 2 * BatchingUploader.PER_PAYLOAD_OVERHEAD_BYTE_COUNT); + assertTrue(payload.getRecordCount() == 0); + assertTrue(payload.getRecordsBuffer().isEmpty()); + assertTrue(payload.getRecordGuidsBuffer().isEmpty()); + assertTrue(payload.isEmpty()); + + // Shouldn't throw even if already empty + payload.reset(); + assertTrue(payload.getByteCount() == 2 * BatchingUploader.PER_PAYLOAD_OVERHEAD_BYTE_COUNT); + assertTrue(payload.getRecordCount() == 0); + assertTrue(payload.getRecordsBuffer().isEmpty()); + assertTrue(payload.getRecordGuidsBuffer().isEmpty()); + assertTrue(payload.isEmpty()); + + byte[] recordBytes1 = new byte[100]; + assertFalse(payload.addAndEstimateIfFull(1, recordBytes1, "guid1")); + assertFalse(payload.isEmpty()); + payload.reset(); + + assertTrue(payload.getByteCount() == 2 * BatchingUploader.PER_PAYLOAD_OVERHEAD_BYTE_COUNT); + assertTrue(payload.getRecordCount() == 0); + assertTrue(payload.getRecordsBuffer().isEmpty()); + assertTrue(payload.getRecordGuidsBuffer().isEmpty()); + assertTrue(payload.isEmpty()); + } +}
\ No newline at end of file diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/uploaders/PayloadUploadDelegateTest.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/uploaders/PayloadUploadDelegateTest.java new file mode 100644 index 000000000..fc43c2f5e --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/uploaders/PayloadUploadDelegateTest.java @@ -0,0 +1,404 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.sync.repositories.uploaders; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.sync.HTTPFailureException; +import org.mozilla.gecko.sync.InfoCollections; +import org.mozilla.gecko.sync.InfoConfiguration; +import org.mozilla.gecko.sync.NonObjectJSONException; +import org.mozilla.gecko.sync.net.SyncResponse; +import org.mozilla.gecko.sync.net.SyncStorageResponse; +import org.mozilla.gecko.sync.repositories.Server11Repository; +import org.mozilla.gecko.sync.repositories.Server11RepositorySession; +import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionStoreDelegate; + +import java.io.ByteArrayInputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.concurrent.Executor; + +import ch.boye.httpclientandroidlib.HttpResponse; +import ch.boye.httpclientandroidlib.ProtocolVersion; +import ch.boye.httpclientandroidlib.entity.BasicHttpEntity; +import ch.boye.httpclientandroidlib.message.BasicHttpResponse; +import ch.boye.httpclientandroidlib.message.BasicStatusLine; + +import static org.junit.Assert.*; + +@RunWith(TestRunner.class) +public class PayloadUploadDelegateTest { + private BatchingUploader batchingUploader; + + class MockUploader extends BatchingUploader { + public final ArrayList<String> successRecords = new ArrayList<>(); + public final HashMap<String, Exception> failedRecords = new HashMap<>(); + public boolean didLastPayloadFail = false; + + public ArrayList<SyncStorageResponse> successResponses = new ArrayList<>(); + public int commitPayloadsSucceeded = 0; + public int lastPayloadsSucceeded = 0; + + public MockUploader(final Server11RepositorySession repositorySession, final Executor workQueue, final RepositorySessionStoreDelegate sessionStoreDelegate) { + super(repositorySession, workQueue, sessionStoreDelegate); + } + + @Override + public void payloadSucceeded(final SyncStorageResponse response, final boolean isCommit, final boolean isLastPayload) { + successResponses.add(response); + if (isCommit) { + ++commitPayloadsSucceeded; + } + if (isLastPayload) { + ++lastPayloadsSucceeded; + } + } + + @Override + public void recordSucceeded(final String recordGuid) { + successRecords.add(recordGuid); + } + + @Override + public void recordFailed(final String recordGuid) { + recordFailed(new Exception(), recordGuid); + } + + @Override + public void recordFailed(final Exception e, final String recordGuid) { + failedRecords.put(recordGuid, e); + } + + @Override + public void lastPayloadFailed() { + didLastPayloadFail = true; + } + } + + @Before + public void setUp() throws Exception { + Server11Repository server11Repository = new Server11Repository( + "dummyCollection", + "http://dummy.url/", + null, + new InfoCollections(), + new InfoConfiguration() + ); + batchingUploader = new MockUploader( + new Server11RepositorySession(server11Repository), + null, + null + ); + } + + @Test + public void testHandleRequestSuccessNonSuccess() { + ArrayList<String> postedGuids = new ArrayList<>(2); + postedGuids.add("testGuid1"); + postedGuids.add("testGuid2"); + PayloadUploadDelegate payloadUploadDelegate = new PayloadUploadDelegate( + batchingUploader, postedGuids, false, false); + + // Test that non-2* responses aren't processed + payloadUploadDelegate.handleRequestSuccess(makeSyncStorageResponse(404, null, null)); + assertEquals(2, ((MockUploader) batchingUploader).failedRecords.size()); + assertFalse(((MockUploader) batchingUploader).didLastPayloadFail); + assertEquals(IllegalStateException.class, + ((MockUploader) batchingUploader).failedRecords.get("testGuid1").getClass()); + assertEquals(IllegalStateException.class, + ((MockUploader) batchingUploader).failedRecords.get("testGuid2").getClass()); + } + + @Test + public void testHandleRequestSuccessNoHeaders() { + ArrayList<String> postedGuids = new ArrayList<>(2); + postedGuids.add("testGuid1"); + postedGuids.add("testGuid2"); + PayloadUploadDelegate payloadUploadDelegate = new PayloadUploadDelegate( + batchingUploader, postedGuids, false, false); + + // Test that responses without X-Last-Modified header aren't processed + payloadUploadDelegate.handleRequestSuccess(makeSyncStorageResponse(200, null, null)); + assertEquals(2, ((MockUploader) batchingUploader).failedRecords.size()); + assertFalse(((MockUploader) batchingUploader).didLastPayloadFail); + assertEquals(IllegalStateException.class, + ((MockUploader) batchingUploader).failedRecords.get("testGuid1").getClass()); + assertEquals(IllegalStateException.class, + ((MockUploader) batchingUploader).failedRecords.get("testGuid2").getClass()); + } + + @Test + public void testHandleRequestSuccessBadBody() { + ArrayList<String> postedGuids = new ArrayList<>(2); + postedGuids.add("testGuid1"); + postedGuids.add("testGuid2"); + PayloadUploadDelegate payloadUploadDelegate = new PayloadUploadDelegate( + batchingUploader, postedGuids, false, true); + + // Test that we catch json processing errors + payloadUploadDelegate.handleRequestSuccess(makeSyncStorageResponse(200, "non json body", "123")); + assertEquals(2, ((MockUploader) batchingUploader).failedRecords.size()); + assertTrue(((MockUploader) batchingUploader).didLastPayloadFail); + assertEquals(NonObjectJSONException.class, + ((MockUploader) batchingUploader).failedRecords.get("testGuid1").getClass()); + assertEquals(NonObjectJSONException.class, + ((MockUploader) batchingUploader).failedRecords.get("testGuid2").getClass()); + } + + @Test + public void testHandleRequestSuccess202NoToken() { + ArrayList<String> postedGuids = new ArrayList<>(1); + postedGuids.add("testGuid1"); + PayloadUploadDelegate payloadUploadDelegate = new PayloadUploadDelegate( + batchingUploader, postedGuids, false, true); + + // Test that we catch absent tokens in 202 responses + payloadUploadDelegate.handleRequestSuccess(makeSyncStorageResponse(202, "{\"success\": []}", "123")); + assertEquals(1, ((MockUploader) batchingUploader).failedRecords.size()); + assertEquals(IllegalStateException.class, + ((MockUploader) batchingUploader).failedRecords.get("testGuid1").getClass()); + } + + @Test + public void testHandleRequestSuccessBad200() { + ArrayList<String> postedGuids = new ArrayList<>(1); + postedGuids.add("testGuid1"); + + PayloadUploadDelegate payloadUploadDelegate = new PayloadUploadDelegate( + batchingUploader, postedGuids, false, false); + + // Test that if in batching mode and saw the token, 200 must be a response to a commit + try { + batchingUploader.getCurrentBatch().setToken("MTIzNA", true); + } catch (BatchingUploader.BatchingUploaderException e) {} + batchingUploader.setInBatchingMode(true); + + // not a commit, so should fail + payloadUploadDelegate.handleRequestSuccess(makeSyncStorageResponse(200, "{\"success\": []}", "123")); + assertEquals(1, ((MockUploader) batchingUploader).failedRecords.size()); + assertEquals(IllegalStateException.class, + ((MockUploader) batchingUploader).failedRecords.get("testGuid1").getClass()); + } + + @Test + public void testHandleRequestSuccessNonBatchingFailedLM() { + ArrayList<String> postedGuids = new ArrayList<>(1); + postedGuids.add("guid1"); + postedGuids.add("guid2"); + postedGuids.add("guid3"); + PayloadUploadDelegate payloadUploadDelegate = new PayloadUploadDelegate( + batchingUploader, postedGuids, false, false); + + payloadUploadDelegate.handleRequestSuccess( + makeSyncStorageResponse(200, "{\"success\": [\"guid1\", \"guid2\", \"guid3\"]}", "123")); + assertEquals(0, ((MockUploader) batchingUploader).failedRecords.size()); + assertEquals(3, ((MockUploader) batchingUploader).successRecords.size()); + assertFalse(((MockUploader) batchingUploader).didLastPayloadFail); + assertEquals(1, ((MockUploader) batchingUploader).successResponses.size()); + assertEquals(0, ((MockUploader) batchingUploader).commitPayloadsSucceeded); + assertEquals(0, ((MockUploader) batchingUploader).lastPayloadsSucceeded); + + // These should fail, because we're returning a non-changed L-M in a non-batching mode + postedGuids.add("guid4"); + postedGuids.add("guid6"); + payloadUploadDelegate = new PayloadUploadDelegate( + batchingUploader, postedGuids, false, false); + payloadUploadDelegate.handleRequestSuccess( + makeSyncStorageResponse(200, "{\"success\": [\"guid4\", 5, \"guid6\"]}", "123")); + assertEquals(5, ((MockUploader) batchingUploader).failedRecords.size()); + assertEquals(3, ((MockUploader) batchingUploader).successRecords.size()); + assertFalse(((MockUploader) batchingUploader).didLastPayloadFail); + assertEquals(1, ((MockUploader) batchingUploader).successResponses.size()); + assertEquals(0, ((MockUploader) batchingUploader).commitPayloadsSucceeded); + assertEquals(0, ((MockUploader) batchingUploader).lastPayloadsSucceeded); + assertEquals(BatchingUploader.LastModifiedDidNotChange.class, + ((MockUploader) batchingUploader).failedRecords.get("guid4").getClass()); + } + + @Test + public void testHandleRequestSuccessNonBatching() { + ArrayList<String> postedGuids = new ArrayList<>(); + postedGuids.add("guid1"); + postedGuids.add("guid2"); + postedGuids.add("guid3"); + PayloadUploadDelegate payloadUploadDelegate = new PayloadUploadDelegate( + batchingUploader, postedGuids, false, false); + payloadUploadDelegate.handleRequestSuccess( + makeSyncStorageResponse(200, "{\"success\": [\"guid1\", \"guid2\", \"guid3\"], \"failed\": {}}", "123")); + + postedGuids = new ArrayList<>(); + postedGuids.add("guid4"); + postedGuids.add("guid5"); + payloadUploadDelegate = new PayloadUploadDelegate( + batchingUploader, postedGuids, false, false); + payloadUploadDelegate.handleRequestSuccess( + makeSyncStorageResponse(200, "{\"success\": [\"guid4\", \"guid5\"], \"failed\": {}}", "333")); + + postedGuids = new ArrayList<>(); + postedGuids.add("guid6"); + payloadUploadDelegate = new PayloadUploadDelegate( + batchingUploader, postedGuids, false, true); + payloadUploadDelegate.handleRequestSuccess( + makeSyncStorageResponse(200, "{\"success\": [\"guid6\"], \"failed\": {}}", "444")); + + assertEquals(0, ((MockUploader) batchingUploader).failedRecords.size()); + assertEquals(6, ((MockUploader) batchingUploader).successRecords.size()); + assertFalse(((MockUploader) batchingUploader).didLastPayloadFail); + assertEquals(3, ((MockUploader) batchingUploader).successResponses.size()); + assertEquals(0, ((MockUploader) batchingUploader).commitPayloadsSucceeded); + assertEquals(1, ((MockUploader) batchingUploader).lastPayloadsSucceeded); + assertFalse(batchingUploader.getInBatchingMode()); + + postedGuids = new ArrayList<>(); + postedGuids.add("guid7"); + postedGuids.add("guid8"); + payloadUploadDelegate = new PayloadUploadDelegate( + batchingUploader, postedGuids, false, true); + payloadUploadDelegate.handleRequestSuccess( + makeSyncStorageResponse(200, "{\"success\": [\"guid8\"], \"failed\": {\"guid7\": \"reason\"}}", "555")); + assertEquals(1, ((MockUploader) batchingUploader).failedRecords.size()); + assertTrue(((MockUploader) batchingUploader).failedRecords.containsKey("guid7")); + assertEquals(7, ((MockUploader) batchingUploader).successRecords.size()); + assertFalse(((MockUploader) batchingUploader).didLastPayloadFail); + assertEquals(4, ((MockUploader) batchingUploader).successResponses.size()); + assertEquals(0, ((MockUploader) batchingUploader).commitPayloadsSucceeded); + assertEquals(2, ((MockUploader) batchingUploader).lastPayloadsSucceeded); + assertFalse(batchingUploader.getInBatchingMode()); + } + + @Test + public void testHandleRequestSuccessBatching() { + ArrayList<String> postedGuids = new ArrayList<>(); + postedGuids.add("guid1"); + postedGuids.add("guid2"); + postedGuids.add("guid3"); + PayloadUploadDelegate payloadUploadDelegate = new PayloadUploadDelegate( + batchingUploader, postedGuids, false, false); + payloadUploadDelegate.handleRequestSuccess( + makeSyncStorageResponse(202, "{\"batch\": \"MTIzNA\", \"success\": [\"guid1\", \"guid2\", \"guid3\"], \"failed\": {}}", "123")); + + assertTrue(batchingUploader.getInBatchingMode()); + assertEquals("MTIzNA", batchingUploader.getCurrentBatch().getToken()); + + postedGuids = new ArrayList<>(); + postedGuids.add("guid4"); + postedGuids.add("guid5"); + postedGuids.add("guid6"); + payloadUploadDelegate = new PayloadUploadDelegate( + batchingUploader, postedGuids, false, false); + payloadUploadDelegate.handleRequestSuccess( + makeSyncStorageResponse(202, "{\"batch\": \"MTIzNA\", \"success\": [\"guid4\", \"guid5\", \"guid6\"], \"failed\": {}}", "123")); + + assertTrue(batchingUploader.getInBatchingMode()); + assertEquals("MTIzNA", batchingUploader.getCurrentBatch().getToken()); + + postedGuids = new ArrayList<>(); + postedGuids.add("guid7"); + payloadUploadDelegate = new PayloadUploadDelegate( + batchingUploader, postedGuids, true, false); + payloadUploadDelegate.handleRequestSuccess( + makeSyncStorageResponse(200, "{\"success\": [\"guid6\"], \"failed\": {}}", "222")); + + // Even though everything indicates we're not in a batching, we were, so test that + // we don't reset the flag. + assertTrue(batchingUploader.getInBatchingMode()); + assertNull(batchingUploader.getCurrentBatch().getToken()); + + postedGuids = new ArrayList<>(); + postedGuids.add("guid8"); + payloadUploadDelegate = new PayloadUploadDelegate( + batchingUploader, postedGuids, true, true); + payloadUploadDelegate.handleRequestSuccess( + makeSyncStorageResponse(200, "{\"success\": [\"guid7\"], \"failed\": {}}", "333")); + + assertEquals(0, ((MockUploader) batchingUploader).failedRecords.size()); + assertEquals(8, ((MockUploader) batchingUploader).successRecords.size()); + assertFalse(((MockUploader) batchingUploader).didLastPayloadFail); + assertEquals(4, ((MockUploader) batchingUploader).successResponses.size()); + assertEquals(2, ((MockUploader) batchingUploader).commitPayloadsSucceeded); + assertEquals(1, ((MockUploader) batchingUploader).lastPayloadsSucceeded); + assertTrue(batchingUploader.getInBatchingMode()); + } + + @Test + public void testHandleRequestError() { + ArrayList<String> postedGuids = new ArrayList<>(3); + postedGuids.add("testGuid1"); + postedGuids.add("testGuid2"); + postedGuids.add("testGuid3"); + PayloadUploadDelegate payloadUploadDelegate = new PayloadUploadDelegate(batchingUploader, postedGuids, false, false); + + IllegalStateException e = new IllegalStateException(); + payloadUploadDelegate.handleRequestError(e); + + assertEquals(3, ((MockUploader) batchingUploader).failedRecords.size()); + assertEquals(e, ((MockUploader) batchingUploader).failedRecords.get("testGuid1")); + assertEquals(e, ((MockUploader) batchingUploader).failedRecords.get("testGuid2")); + assertEquals(e, ((MockUploader) batchingUploader).failedRecords.get("testGuid3")); + assertFalse(((MockUploader) batchingUploader).didLastPayloadFail); + + payloadUploadDelegate = new PayloadUploadDelegate(batchingUploader, postedGuids, false, true); + payloadUploadDelegate.handleRequestError(e); + assertEquals(3, ((MockUploader) batchingUploader).failedRecords.size()); + assertTrue(((MockUploader) batchingUploader).didLastPayloadFail); + } + + @Test + public void testHandleRequestFailure() { + ArrayList<String> postedGuids = new ArrayList<>(3); + postedGuids.add("testGuid1"); + postedGuids.add("testGuid2"); + postedGuids.add("testGuid3"); + PayloadUploadDelegate payloadUploadDelegate = new PayloadUploadDelegate(batchingUploader, postedGuids, false, false); + + final HttpResponse response = new BasicHttpResponse( + new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 503, "Illegal method/protocol")); + payloadUploadDelegate.handleRequestFailure(new SyncStorageResponse(response)); + assertEquals(3, ((MockUploader) batchingUploader).failedRecords.size()); + assertEquals(HTTPFailureException.class, + ((MockUploader) batchingUploader).failedRecords.get("testGuid1").getClass()); + assertEquals(HTTPFailureException.class, + ((MockUploader) batchingUploader).failedRecords.get("testGuid2").getClass()); + assertEquals(HTTPFailureException.class, + ((MockUploader) batchingUploader).failedRecords.get("testGuid3").getClass()); + + payloadUploadDelegate = new PayloadUploadDelegate(batchingUploader, postedGuids, false, true); + payloadUploadDelegate.handleRequestFailure(new SyncStorageResponse(response)); + assertEquals(3, ((MockUploader) batchingUploader).failedRecords.size()); + assertTrue(((MockUploader) batchingUploader).didLastPayloadFail); + } + + @Test + public void testIfUnmodifiedSince() { + PayloadUploadDelegate payloadUploadDelegate = new PayloadUploadDelegate( + batchingUploader, new ArrayList<String>(), false, false); + + assertNull(payloadUploadDelegate.ifUnmodifiedSince()); + + try { + batchingUploader.getCurrentBatch().setLastModified(1471645412480L, true); + } catch (BatchingUploader.BatchingUploaderException e) {} + + assertEquals("1471645412.480", payloadUploadDelegate.ifUnmodifiedSince()); + } + + private SyncStorageResponse makeSyncStorageResponse(int code, String body, String lastModified) { + BasicHttpResponse response = new BasicHttpResponse( + new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), code, null)); + + if (body != null) { + BasicHttpEntity entity = new BasicHttpEntity(); + entity.setContent(new ByteArrayInputStream(body.getBytes())); + response.setEntity(entity); + } + + if (lastModified != null) { + response.addHeader(SyncResponse.X_LAST_MODIFIED, lastModified); + } + return new SyncStorageResponse(response); + } +}
\ No newline at end of file diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/uploaders/RecordUploadRunnableTest.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/uploaders/RecordUploadRunnableTest.java new file mode 100644 index 000000000..269c25362 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/uploaders/RecordUploadRunnableTest.java @@ -0,0 +1,38 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.sync.repositories.uploaders; + +import android.net.Uri; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.TestRunner; + +import java.net.URI; + +import static org.junit.Assert.*; + +@RunWith(TestRunner.class) +public class RecordUploadRunnableTest { + @Test + public void testBuildPostURI() throws Exception { + BatchMeta batchMeta = new BatchMeta(new Object(), 1, 1, null); + URI postURI = RecordUploadRunnable.buildPostURI( + false, batchMeta, Uri.parse("http://example.com/")); + assertEquals("http://example.com/?batch=true", postURI.toString()); + + postURI = RecordUploadRunnable.buildPostURI( + true, batchMeta, Uri.parse("http://example.com/")); + assertEquals("http://example.com/?batch=true&commit=true", postURI.toString()); + + batchMeta.setToken("MTIzNA", false); + postURI = RecordUploadRunnable.buildPostURI( + false, batchMeta, Uri.parse("http://example.com/")); + assertEquals("http://example.com/?batch=MTIzNA", postURI.toString()); + + postURI = RecordUploadRunnable.buildPostURI( + true, batchMeta, Uri.parse("http://example.com/")); + assertEquals("http://example.com/?batch=MTIzNA&commit=true", postURI.toString()); + } +}
\ No newline at end of file diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/stage/test/TestEnsureCrypto5KeysStage.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/stage/test/TestEnsureCrypto5KeysStage.java new file mode 100644 index 000000000..cb74b427b --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/stage/test/TestEnsureCrypto5KeysStage.java @@ -0,0 +1,237 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.sync.stage.test; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.android.sync.test.helpers.HTTPServerTestHelper; +import org.mozilla.android.sync.test.helpers.MockGlobalSessionCallback; +import org.mozilla.android.sync.test.helpers.MockServer; +import org.mozilla.gecko.background.testhelpers.MockGlobalSession; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.background.testhelpers.WaitHelper; +import org.mozilla.gecko.sync.AlreadySyncingException; +import org.mozilla.gecko.sync.CollectionKeys; +import org.mozilla.gecko.sync.CryptoRecord; +import org.mozilla.gecko.sync.ExtendedJSONObject; +import org.mozilla.gecko.sync.GlobalSession; +import org.mozilla.gecko.sync.InfoCollections; +import org.mozilla.gecko.sync.crypto.KeyBundle; +import org.mozilla.gecko.sync.stage.EnsureCrypto5KeysStage; +import org.mozilla.gecko.sync.stage.GlobalSyncStage.Stage; +import org.simpleframework.http.Request; +import org.simpleframework.http.Response; + +import java.net.URI; +import java.util.ArrayList; +import java.util.Collection; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +@RunWith(TestRunner.class) +public class TestEnsureCrypto5KeysStage { + 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 String TEST_JSON_NO_CRYPTO = + "{\"history\":1.3319567131E9}"; + private final String TEST_JSON_OLD_CRYPTO = + "{\"history\":1.3319567131E9,\"crypto\":1.1E9}"; + private final String TEST_JSON_NEW_CRYPTO = + "{\"history\":1.3319567131E9,\"crypto\":3.1E9}"; + + private HTTPServerTestHelper data = new HTTPServerTestHelper(); + + private KeyBundle syncKeyBundle; + private MockGlobalSessionCallback callback; + private GlobalSession session; + + private boolean calledResetStages; + private Collection<String> stagesReset; + + @Before + public void setUp() throws Exception { + syncKeyBundle = new KeyBundle(TEST_USERNAME, TEST_SYNC_KEY); + callback = new MockGlobalSessionCallback(); + session = new MockGlobalSession(TEST_USERNAME, TEST_PASSWORD, + syncKeyBundle, callback) { + @Override + protected void prepareStages() { + super.prepareStages(); + withStage(Stage.ensureKeysStage, new EnsureCrypto5KeysStage()); + } + + @Override + public void resetStagesByEnum(Collection<Stage> stages) { + calledResetStages = true; + stagesReset = new ArrayList<String>(); + for (Stage stage : stages) { + stagesReset.add(stage.name()); + } + } + + @Override + public void resetStagesByName(Collection<String> names) { + calledResetStages = true; + stagesReset = names; + } + }; + session.config.setClusterURL(new URI(TEST_CLUSTER_URL)); + + // Set info collections to not have crypto. + final ExtendedJSONObject noCrypto = new ExtendedJSONObject(TEST_JSON_NO_CRYPTO); + session.config.infoCollections = new InfoCollections(noCrypto); + calledResetStages = false; + stagesReset = null; + } + + public void doSession(MockServer server) { + data.startHTTPServer(server); + try { + WaitHelper.getTestWaiter().performWait(new Runnable() { + @Override + public void run() { + try { + session.start(); + } catch (AlreadySyncingException e) { + WaitHelper.getTestWaiter().performNotify(e); + } + } + }); + } finally { + data.stopHTTPServer(); + } + } + + @Test + public void testDownloadUsesPersisted() throws Exception { + session.config.infoCollections = new InfoCollections(new ExtendedJSONObject + (TEST_JSON_OLD_CRYPTO)); + session.config.persistedCryptoKeys().persistLastModified(System.currentTimeMillis()); + + assertNull(session.config.collectionKeys); + final CollectionKeys keys = CollectionKeys.generateCollectionKeys(); + keys.setDefaultKeyBundle(syncKeyBundle); + session.config.persistedCryptoKeys().persistKeys(keys); + + MockServer server = new MockServer() { + public void handle(Request request, Response response) { + this.handle(request, response, 404, "should not be called!"); + } + }; + + doSession(server); + + assertTrue(callback.calledSuccess); + assertNotNull(session.config.collectionKeys); + assertTrue(CollectionKeys.differences(session.config.collectionKeys, keys).isEmpty()); + } + + @Test + public void testDownloadFetchesNew() throws Exception { + session.config.infoCollections = new InfoCollections(new ExtendedJSONObject(TEST_JSON_NEW_CRYPTO)); + session.config.persistedCryptoKeys().persistLastModified(System.currentTimeMillis()); + + assertNull(session.config.collectionKeys); + final CollectionKeys keys = CollectionKeys.generateCollectionKeys(); + keys.setDefaultKeyBundle(syncKeyBundle); + session.config.persistedCryptoKeys().persistKeys(keys); + + MockServer server = new MockServer() { + public void handle(Request request, Response response) { + try { + CryptoRecord rec = keys.asCryptoRecord(); + rec.keyBundle = syncKeyBundle; + rec.encrypt(); + this.handle(request, response, 200, rec.toJSONString()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + }; + + doSession(server); + + assertTrue(callback.calledSuccess); + assertNotNull(session.config.collectionKeys); + assertTrue(session.config.collectionKeys.equals(keys)); + } + + /** + * Change the default key but keep one collection key the same. Should reset + * all but that one collection. + */ + @Test + public void testDownloadResetsOnDifferentDefaultKey() throws Exception { + String TEST_COLLECTION = "bookmarks"; + + session.config.infoCollections = new InfoCollections(new ExtendedJSONObject(TEST_JSON_NEW_CRYPTO)); + session.config.persistedCryptoKeys().persistLastModified(System.currentTimeMillis()); + + KeyBundle keyBundle = KeyBundle.withRandomKeys(); + assertNull(session.config.collectionKeys); + final CollectionKeys keys = CollectionKeys.generateCollectionKeys(); + keys.setKeyBundleForCollection(TEST_COLLECTION, keyBundle); + session.config.persistedCryptoKeys().persistKeys(keys); + keys.setDefaultKeyBundle(syncKeyBundle); // Change the default key bundle, but keep "bookmarks" the same. + + MockServer server = new MockServer() { + public void handle(Request request, Response response) { + try { + CryptoRecord rec = keys.asCryptoRecord(); + rec.keyBundle = syncKeyBundle; + rec.encrypt(); + this.handle(request, response, 200, rec.toJSONString()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + }; + + doSession(server); + + assertTrue(calledResetStages); + Collection<String> allButCollection = new ArrayList<String>(); + for (Stage stage : Stage.getNamedStages()) { + allButCollection.add(stage.getRepositoryName()); + } + allButCollection.remove(TEST_COLLECTION); + assertTrue(stagesReset.containsAll(allButCollection)); + assertTrue(allButCollection.containsAll(stagesReset)); + assertTrue(callback.calledError); + } + + @Test + public void testDownloadResetsEngineOnDifferentKey() throws Exception { + final String TEST_COLLECTION = "history"; + + session.config.infoCollections = new InfoCollections(new ExtendedJSONObject(TEST_JSON_NEW_CRYPTO)); + session.config.persistedCryptoKeys().persistLastModified(System.currentTimeMillis()); + + assertNull(session.config.collectionKeys); + final CollectionKeys keys = CollectionKeys.generateCollectionKeys(); + session.config.persistedCryptoKeys().persistKeys(keys); + keys.setKeyBundleForCollection(TEST_COLLECTION, syncKeyBundle); // Change one key bundle. + + CryptoRecord rec = keys.asCryptoRecord(); + rec.keyBundle = syncKeyBundle; + rec.encrypt(); + MockServer server = new MockServer(200, rec.toJSONString()); + + doSession(server); + + assertTrue(calledResetStages); + assertNotNull(stagesReset); + assertEquals(1, stagesReset.size()); + assertTrue(stagesReset.contains(TEST_COLLECTION)); + assertTrue(callback.calledError); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/stage/test/TestFetchMetaGlobalStage.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/stage/test/TestFetchMetaGlobalStage.java new file mode 100644 index 000000000..f7ed7a559 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/stage/test/TestFetchMetaGlobalStage.java @@ -0,0 +1,391 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.sync.stage.test; + +import org.json.simple.JSONArray; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.android.sync.net.test.TestMetaGlobal; +import org.mozilla.android.sync.test.helpers.HTTPServerTestHelper; +import org.mozilla.android.sync.test.helpers.MockGlobalSessionCallback; +import org.mozilla.android.sync.test.helpers.MockServer; +import org.mozilla.gecko.background.testhelpers.MockGlobalSession; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.background.testhelpers.WaitHelper; +import org.mozilla.gecko.sync.AlreadySyncingException; +import org.mozilla.gecko.sync.CollectionKeys; +import org.mozilla.gecko.sync.CryptoRecord; +import org.mozilla.gecko.sync.ExtendedJSONObject; +import org.mozilla.gecko.sync.GlobalSession; +import org.mozilla.gecko.sync.InfoCollections; +import org.mozilla.gecko.sync.MetaGlobal; +import org.mozilla.gecko.sync.NonObjectJSONException; +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.FreshStartDelegate; +import org.mozilla.gecko.sync.delegates.KeyUploadDelegate; +import org.mozilla.gecko.sync.delegates.WipeServerDelegate; +import org.mozilla.gecko.sync.net.AuthHeaderProvider; +import org.mozilla.gecko.sync.stage.FetchMetaGlobalStage; +import org.mozilla.gecko.sync.stage.GlobalSyncStage.Stage; +import org.simpleframework.http.Request; +import org.simpleframework.http.Response; + +import java.io.IOException; +import java.net.URI; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +@RunWith(TestRunner.class) +public class TestFetchMetaGlobalStage { + @SuppressWarnings("unused") + private static final String LOG_TAG = "TestMetaGlobalStage"; + + private static final int TEST_PORT = HTTPServerTestHelper.getTestPort(); + private static final String TEST_SERVER = "http://localhost:" + TEST_PORT + "/"; + private static final String TEST_CLUSTER_URL = TEST_SERVER + "cluster/"; + private HTTPServerTestHelper data = new HTTPServerTestHelper(); + + private final String TEST_USERNAME = "johndoe"; + private final String TEST_PASSWORD = "password"; + private final String TEST_SYNC_KEY = "abcdeabcdeabcdeabcdeabcdea"; + + private final String TEST_INFO_COLLECTIONS_JSON = "{}"; + + private static final String TEST_SYNC_ID = "testSyncID"; + private static final long TEST_STORAGE_VERSION = GlobalSession.STORAGE_VERSION; + + private InfoCollections infoCollections; + private KeyBundle syncKeyBundle; + private MockGlobalSessionCallback callback; + private GlobalSession session; + + private boolean calledRequiresUpgrade = false; + private boolean calledProcessMissingMetaGlobal = false; + private boolean calledFreshStart = false; + private boolean calledWipeServer = false; + private boolean calledUploadKeys = false; + private boolean calledResetAllStages = false; + + private static void assertSameContents(JSONArray expected, Set<String> actual) { + assertEquals(expected.size(), actual.size()); + for (Object o : expected) { + assertTrue(actual.contains(o)); + } + } + + @Before + public void setUp() throws Exception { + calledRequiresUpgrade = false; + calledProcessMissingMetaGlobal = false; + calledFreshStart = false; + calledWipeServer = false; + calledUploadKeys = false; + calledResetAllStages = false; + + // Set info collections to not have crypto. + infoCollections = new InfoCollections(new ExtendedJSONObject(TEST_INFO_COLLECTIONS_JSON)); + + syncKeyBundle = new KeyBundle(TEST_USERNAME, TEST_SYNC_KEY); + callback = new MockGlobalSessionCallback(); + session = new MockGlobalSession(TEST_USERNAME, TEST_PASSWORD, + syncKeyBundle, callback) { + @Override + protected void prepareStages() { + super.prepareStages(); + withStage(Stage.fetchMetaGlobal, new FetchMetaGlobalStage()); + } + + @Override + public void requiresUpgrade() { + calledRequiresUpgrade = true; + this.abort(null, "Requires upgrade"); + } + + @Override + public void processMissingMetaGlobal(MetaGlobal mg) { + calledProcessMissingMetaGlobal = true; + this.abort(null, "Missing meta/global"); + } + + // Don't really uploadKeys. + @Override + public void uploadKeys(CollectionKeys keys, KeyUploadDelegate keyUploadDelegate) { + calledUploadKeys = true; + keyUploadDelegate.onKeysUploaded(); + } + + // On fresh start completed, just stop. + @Override + public void freshStart() { + calledFreshStart = true; + freshStart(this, new FreshStartDelegate() { + @Override + public void onFreshStartFailed(Exception e) { + WaitHelper.getTestWaiter().performNotify(e); + } + + @Override + public void onFreshStart() { + WaitHelper.getTestWaiter().performNotify(); + } + }); + } + + // Don't really wipeServer. + @Override + protected void wipeServer(final AuthHeaderProvider authHeaderProvider, final WipeServerDelegate wipeDelegate) { + calledWipeServer = true; + wipeDelegate.onWiped(System.currentTimeMillis()); + } + + // Don't really resetAllStages. + @Override + public void resetAllStages() { + calledResetAllStages = true; + } + }; + session.config.setClusterURL(new URI(TEST_CLUSTER_URL)); + session.config.infoCollections = infoCollections; + } + + protected void doSession(MockServer server) { + data.startHTTPServer(server); + WaitHelper.getTestWaiter().performWait(WaitHelper.onThreadRunnable(new Runnable() { + @Override + public void run() { + try { + session.start(); + } catch (AlreadySyncingException e) { + WaitHelper.getTestWaiter().performNotify(e); + } + } + })); + data.stopHTTPServer(); + } + + @Test + public void testFetchRequiresUpgrade() throws Exception { + MetaGlobal mg = new MetaGlobal(null, null); + mg.setSyncID(TEST_SYNC_ID); + mg.setStorageVersion(Long.valueOf(TEST_STORAGE_VERSION + 1)); + + MockServer server = new MockServer(200, mg.asCryptoRecord().toJSONString()); + doSession(server); + + assertEquals(true, callback.calledError); + assertTrue(calledRequiresUpgrade); + } + + @SuppressWarnings("unchecked") + private JSONArray makeTestDeclinedArray() { + final JSONArray declined = new JSONArray(); + declined.add("foobar"); + return declined; + } + + /** + * Verify that a fetched meta/global with remote syncID == local syncID does + * not reset. + * + * @throws Exception + */ + @Test + public void testFetchSuccessWithSameSyncID() throws Exception { + session.config.syncID = TEST_SYNC_ID; + + MetaGlobal mg = new MetaGlobal(null, null); + mg.setSyncID(TEST_SYNC_ID); + mg.setStorageVersion(Long.valueOf(TEST_STORAGE_VERSION)); + + // Set declined engines in the server object. + final JSONArray testingDeclinedEngines = makeTestDeclinedArray(); + mg.setDeclinedEngineNames(testingDeclinedEngines); + + MockServer server = new MockServer(200, mg.asCryptoRecord().toJSONString()); + doSession(server); + + assertTrue(callback.calledSuccess); + assertFalse(calledProcessMissingMetaGlobal); + assertFalse(calledResetAllStages); + assertEquals(TEST_SYNC_ID, session.config.metaGlobal.getSyncID()); + assertEquals(TEST_STORAGE_VERSION, session.config.metaGlobal.getStorageVersion().longValue()); + assertEquals(TEST_SYNC_ID, session.config.syncID); + + // Declined engines propagate from the server meta/global. + final Set<String> actual = session.config.metaGlobal.getDeclinedEngineNames(); + assertSameContents(testingDeclinedEngines, actual); + } + + /** + * Verify that a fetched meta/global with remote syncID != local syncID resets + * local and retains remote syncID. + * + * @throws Exception + */ + @Test + public void testFetchSuccessWithDifferentSyncID() throws Exception { + session.config.syncID = "NOT TEST SYNC ID"; + + MetaGlobal mg = new MetaGlobal(null, null); + mg.setSyncID(TEST_SYNC_ID); + mg.setStorageVersion(Long.valueOf(TEST_STORAGE_VERSION)); + + // Set declined engines in the server object. + final JSONArray testingDeclinedEngines = makeTestDeclinedArray(); + mg.setDeclinedEngineNames(testingDeclinedEngines); + + MockServer server = new MockServer(200, mg.asCryptoRecord().toJSONString()); + doSession(server); + + assertEquals(true, callback.calledSuccess); + assertFalse(calledProcessMissingMetaGlobal); + assertTrue(calledResetAllStages); + assertEquals(TEST_SYNC_ID, session.config.metaGlobal.getSyncID()); + assertEquals(TEST_STORAGE_VERSION, session.config.metaGlobal.getStorageVersion().longValue()); + assertEquals(TEST_SYNC_ID, session.config.syncID); + + // Declined engines propagate from the server meta/global. + final Set<String> actual = session.config.metaGlobal.getDeclinedEngineNames(); + assertSameContents(testingDeclinedEngines, actual); + } + + /** + * Verify that a fetched meta/global does not merge declined engines. + * TODO: eventually it should! + */ + @SuppressWarnings("unchecked") + @Test + public void testFetchSuccessWithDifferentSyncIDMergesDeclined() throws Exception { + session.config.syncID = "NOT TEST SYNC ID"; + + // Fake the local declined engine names. + session.config.declinedEngineNames = new HashSet<String>(); + session.config.declinedEngineNames.add("baznoo"); + + MetaGlobal mg = new MetaGlobal(null, null); + mg.setSyncID(TEST_SYNC_ID); + mg.setStorageVersion(Long.valueOf(TEST_STORAGE_VERSION)); + + // Set declined engines in the server object. + final JSONArray testingDeclinedEngines = makeTestDeclinedArray(); + mg.setDeclinedEngineNames(testingDeclinedEngines); + + MockServer server = new MockServer(200, mg.asCryptoRecord().toJSONString()); + doSession(server); + + // Declined engines propagate from the server meta/global, and are NOT merged. + final Set<String> expected = new HashSet<String>(testingDeclinedEngines); + // expected.add("baznoo"); // Not until we merge. Local is lost. + + final Set<String> newDeclined = session.config.metaGlobal.getDeclinedEngineNames(); + assertEquals(expected, newDeclined); + } + + @Test + public void testFetchMissing() throws Exception { + MockServer server = new MockServer(404, "missing"); + doSession(server); + + assertEquals(true, callback.calledError); + assertTrue(calledProcessMissingMetaGlobal); + } + + /** + * Empty payload object has no syncID or storageVersion and should call freshStart. + * @throws Exception + */ + @Test + public void testFetchEmptyPayload() throws Exception { + MockServer server = new MockServer(200, TestMetaGlobal.TEST_META_GLOBAL_EMPTY_PAYLOAD_RESPONSE); + doSession(server); + + assertTrue(calledFreshStart); + } + + /** + * No payload means no syncID or storageVersion and therefore we should call freshStart. + * @throws Exception + */ + @Test + public void testFetchNoPayload() throws Exception { + MockServer server = new MockServer(200, TestMetaGlobal.TEST_META_GLOBAL_NO_PAYLOAD_RESPONSE); + doSession(server); + + assertTrue(calledFreshStart); + } + + /** + * Malformed payload is a server response issue, not a meta/global record + * issue. This should error out of the sync. + * @throws Exception + */ + @Test + public void testFetchMalformedPayload() throws Exception { + MockServer server = new MockServer(200, TestMetaGlobal.TEST_META_GLOBAL_MALFORMED_PAYLOAD_RESPONSE); + doSession(server); + + assertEquals(true, callback.calledError); + assertEquals(NonObjectJSONException.class, callback.calledErrorException.getClass()); + } + + protected void doFreshStart(MockServer server) { + data.startHTTPServer(server); + WaitHelper.getTestWaiter().performWait(WaitHelper.onThreadRunnable(new Runnable() { + @Override + public void run() { + session.freshStart(); + } + })); + data.stopHTTPServer(); + } + + @Test + public void testFreshStart() throws SyncConfigurationException, IllegalArgumentException, NonObjectJSONException, IOException, CryptoException { + final AtomicBoolean mgUploaded = new AtomicBoolean(false); + final AtomicBoolean mgDownloaded = new AtomicBoolean(false); + final MetaGlobal uploadedMg = new MetaGlobal(null, null); + + MockServer server = new MockServer() { + @Override + public void handle(Request request, Response response) { + if (request.getMethod().equals("PUT")) { + try { + ExtendedJSONObject body = new ExtendedJSONObject(request.getContent()); + 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; + } + if (mgUploaded.get()) { + // We shouldn't be trying to download anything after uploading meta/global. + mgDownloaded.set(true); + } + this.handle(request, response, 404, "missing"); + } + }; + doFreshStart(server); + + assertTrue(this.calledFreshStart); + assertTrue(this.calledWipeServer); + assertTrue(this.calledUploadKeys); + assertTrue(mgUploaded.get()); + assertFalse(mgDownloaded.get()); + assertEquals(GlobalSession.STORAGE_VERSION, uploadedMg.getStorageVersion().longValue()); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/stage/test/TestStageLookup.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/stage/test/TestStageLookup.java new file mode 100644 index 000000000..86829844f --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/stage/test/TestStageLookup.java @@ -0,0 +1,41 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.sync.stage.test; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.sync.stage.GlobalSyncStage.Stage; + +import java.util.HashSet; +import java.util.Set; + +import static org.junit.Assert.assertEquals; + +@RunWith(TestRunner.class) +public class TestStageLookup { + + @Test + public void testStageLookupByName() { + Set<Stage> namedStages = new HashSet<Stage>(Stage.getNamedStages()); + Set<Stage> expected = new HashSet<Stage>(); + expected.add(Stage.syncClientsEngine); + expected.add(Stage.syncBookmarks); + expected.add(Stage.syncTabs); + expected.add(Stage.syncFormHistory); + expected.add(Stage.syncHistory); + expected.add(Stage.syncPasswords); + + assertEquals(expected, namedStages); + assertEquals(Stage.syncClientsEngine, Stage.byName("clients")); + assertEquals(Stage.syncTabs, Stage.byName("tabs")); + assertEquals(Stage.syncBookmarks, Stage.byName("bookmarks")); + assertEquals(Stage.syncFormHistory, Stage.byName("forms")); + assertEquals(Stage.syncHistory, Stage.byName("history")); + assertEquals(Stage.syncPasswords, Stage.byName("passwords")); + + assertEquals(null, Stage.byName("foobar")); + assertEquals(null, Stage.byName(null)); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/test/TestExtendedJSONObject.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/test/TestExtendedJSONObject.java new file mode 100644 index 000000000..cff9287df --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/test/TestExtendedJSONObject.java @@ -0,0 +1,203 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.sync.test; + +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.sync.ExtendedJSONObject; +import org.mozilla.gecko.sync.NonArrayJSONException; +import org.mozilla.gecko.sync.NonObjectJSONException; +import org.mozilla.gecko.sync.UnexpectedJSONException.BadRequiredFieldJSONException; + +import java.io.IOException; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +@RunWith(TestRunner.class) +public class TestExtendedJSONObject { + public static String exampleJSON = "{\"modified\":1233702554.25,\"success\":[\"{GXS58IDC}12\",\"{GXS58IDC}13\",\"{GXS58IDC}15\",\"{GXS58IDC}16\",\"{GXS58IDC}18\",\"{GXS58IDC}19\"],\"failed\":{\"{GXS58IDC}11\":[\"invalid parentid\"],\"{GXS58IDC}14\":[\"invalid parentid\"],\"{GXS58IDC}17\":[\"invalid parentid\"],\"{GXS58IDC}20\":[\"invalid parentid\"]}}"; + public static String exampleIntegral = "{\"modified\":1233702554,}"; + + @Test + public void testFractional() throws IOException, NonObjectJSONException { + ExtendedJSONObject o = new ExtendedJSONObject(exampleJSON); + assertTrue(o.containsKey("modified")); + assertTrue(o.containsKey("success")); + assertTrue(o.containsKey("failed")); + assertFalse(o.containsKey(" ")); + assertFalse(o.containsKey("")); + assertFalse(o.containsKey("foo")); + assertTrue(o.get("modified") instanceof Number); + assertTrue(o.get("modified").equals(Double.parseDouble("1233702554.25"))); + assertEquals(Long.valueOf(1233702554250L), o.getTimestamp("modified")); + assertEquals(null, o.getTimestamp("foo")); + } + + @Test + public void testIntegral() throws IOException, NonObjectJSONException { + ExtendedJSONObject o = new ExtendedJSONObject(exampleIntegral); + assertTrue(o.containsKey("modified")); + assertFalse(o.containsKey("success")); + assertTrue(o.get("modified") instanceof Number); + assertTrue(o.get("modified").equals(Long.parseLong("1233702554"))); + assertEquals(Long.valueOf(1233702554000L), o.getTimestamp("modified")); + assertEquals(null, o.getTimestamp("foo")); + } + + @Test + public void testSafeInteger() { + ExtendedJSONObject o = new ExtendedJSONObject(); + o.put("integer", Integer.valueOf(5)); + o.put("string", "66"); + o.put("object", new ExtendedJSONObject()); + o.put("null", (JSONArray) null); + + assertEquals(Integer.valueOf(5), o.getIntegerSafely("integer")); + assertEquals(Integer.valueOf(66), o.getIntegerSafely("string")); + assertNull(o.getIntegerSafely(null)); + } + + @Test + public void testParseJSONArray() throws Exception { + JSONArray result = ExtendedJSONObject.parseJSONArray("[0, 1, {\"test\": 2}]"); + assertNotNull(result); + + assertThat((Long) result.get(0), is(equalTo(0L))); + assertThat((Long) result.get(1), is(equalTo(1L))); + assertThat((Long) ((JSONObject) result.get(2)).get("test"), is(equalTo(2L))); + } + + @Test + public void testBadParseJSONArray() throws Exception { + try { + ExtendedJSONObject.parseJSONArray("[0, "); + fail(); + } catch (NonArrayJSONException e) { + // Do nothing. + } + + try { + ExtendedJSONObject.parseJSONArray("{}"); + fail(); + } catch (NonArrayJSONException e) { + // Do nothing. + } + } + + @Test + public void testParseUTF8AsJSONObject() throws Exception { + String TEST = "{\"key\":\"value\"}"; + + ExtendedJSONObject o = ExtendedJSONObject.parseUTF8AsJSONObject(TEST.getBytes("UTF-8")); + assertNotNull(o); + assertEquals("value", o.getString("key")); + } + + @Test + public void testBadParseUTF8AsJSONObject() throws Exception { + try { + ExtendedJSONObject.parseUTF8AsJSONObject("{}".getBytes("UTF-16")); + fail(); + } catch (NonObjectJSONException e) { + // Do nothing. + } + + try { + ExtendedJSONObject.parseUTF8AsJSONObject("{".getBytes("UTF-8")); + fail(); + } catch (NonObjectJSONException e) { + // Do nothing. + } + } + + @Test + public void testHashCode() throws Exception { + ExtendedJSONObject o = new ExtendedJSONObject(exampleJSON); + assertEquals(o.hashCode(), o.hashCode()); + ExtendedJSONObject p = new ExtendedJSONObject(exampleJSON); + assertEquals(o.hashCode(), p.hashCode()); + + ExtendedJSONObject q = new ExtendedJSONObject(exampleJSON); + q.put("modified", 0); + assertNotSame(o.hashCode(), q.hashCode()); + } + + @Test + public void testEquals() throws Exception { + ExtendedJSONObject o = new ExtendedJSONObject(exampleJSON); + ExtendedJSONObject p = new ExtendedJSONObject(exampleJSON); + assertEquals(o, p); + + ExtendedJSONObject q = new ExtendedJSONObject(exampleJSON); + q.put("modified", 0); + assertNotSame(o, q); + assertNotEquals(o, q); + } + + @Test + public void testGetBoolean() throws Exception { + ExtendedJSONObject o = new ExtendedJSONObject("{\"truekey\":true, \"falsekey\":false, \"stringkey\":\"string\"}"); + assertEquals(true, o.getBoolean("truekey")); + assertEquals(false, o.getBoolean("falsekey")); + try { + o.getBoolean("stringkey"); + fail(); + } catch (Exception e) { + assertTrue(e instanceof ClassCastException); + } + assertEquals(null, o.getBoolean("missingkey")); + } + + @Test + public void testNullLong() throws Exception { + ExtendedJSONObject o = new ExtendedJSONObject("{\"x\": null}"); + Long x = o.getLong("x"); + assertNull(x); + + long y = o.getLong("x", 5L); + assertEquals(5L, y); + } + + protected void assertException(ExtendedJSONObject o, String[] requiredFields, Class<?> requiredFieldClass) { + try { + o.throwIfFieldsMissingOrMisTyped(requiredFields, requiredFieldClass); + fail(); + } catch (Exception e) { + assertTrue(e instanceof BadRequiredFieldJSONException); + } + } + + @Test + public void testThrow() throws Exception { + ExtendedJSONObject o = new ExtendedJSONObject("{\"true\":true, \"false\":false, \"string\":\"string\", \"long\":40000000000, \"int\":40, \"nested\":{\"inner\":10}}"); + o.throwIfFieldsMissingOrMisTyped(new String[] { "true", "false" }, Boolean.class); + o.throwIfFieldsMissingOrMisTyped(new String[] { "string" }, String.class); + o.throwIfFieldsMissingOrMisTyped(new String[] { "long" }, Long.class); + o.throwIfFieldsMissingOrMisTyped(new String[] { "int" }, Long.class); + o.throwIfFieldsMissingOrMisTyped(new String[] { "int" }, null); + + // Perhaps a bit unexpected, but we'll document it here. + o.throwIfFieldsMissingOrMisTyped(new String[] { "nested" }, JSONObject.class); + + // Should fail. + assertException(o, new String[] { "int" }, Integer.class); // Irritating, but... + assertException(o, new String[] { "long" }, Integer.class); // Ditto. + assertException(o, new String[] { "missing" }, String.class); + assertException(o, new String[] { "missing" }, null); + assertException(o, new String[] { "string", "int" }, String.class); // Irritating, but... + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/test/TestInfoCollections.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/test/TestInfoCollections.java new file mode 100644 index 000000000..d850ccc56 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/test/TestInfoCollections.java @@ -0,0 +1,101 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.sync.test; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.sync.ExtendedJSONObject; +import org.mozilla.gecko.sync.InfoCollections; +import org.mozilla.gecko.sync.InfoCounts; +import org.mozilla.gecko.sync.Utils; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +/** + * Test both info/collections and info/collection_counts. + */ +@RunWith(TestRunner.class) +public class TestInfoCollections { + public static final String TEST_COLLECTIONS_JSON = + "{\"history\":1.3319567131E9, " + + " \"bookmarks\":1.33195669592E9, " + + " \"prefs\":1.33115408641E9, " + + " \"crypto\":1.32046063664E9, " + + " \"meta\":1.321E9, " + + " \"forms\":1.33136685374E9, " + + " \"clients\":1.3313667619E9, " + + " \"tabs\":1.35E9" + + "}"; + + + public static final String TEST_COUNTS_JSON = + "{\"passwords\": 390, " + + " \"clients\": 2, " + + " \"crypto\": 1, " + + " \"forms\": 1019, " + + " \"bookmarks\": 766, " + + " \"prefs\": 1, " + + " \"history\": 9278" + + "}"; + + @SuppressWarnings("static-method") + @Test + public void testSetCountsFromRecord() throws Exception { + InfoCounts infoCountsEmpty = new InfoCounts(new ExtendedJSONObject("{}")); + assertEquals(null, infoCountsEmpty.getCount("bookmarks")); + + ExtendedJSONObject record = new ExtendedJSONObject(TEST_COUNTS_JSON); + InfoCounts infoCountsFull = new InfoCounts(record); + assertEquals(Integer.valueOf(766), infoCountsFull.getCount("bookmarks")); + assertEquals(null, infoCountsFull.getCount("notpresent")); + } + + + @SuppressWarnings("static-method") + @Test + public void testSetCollectionsFromRecord() throws Exception { + ExtendedJSONObject record = new ExtendedJSONObject(TEST_COLLECTIONS_JSON); + InfoCollections infoCollections = new InfoCollections(record); + + assertEquals(Utils.decimalSecondsToMilliseconds(1.3319567131E9), infoCollections.getTimestamp("history").longValue()); + assertEquals(Utils.decimalSecondsToMilliseconds(1.321E9), infoCollections.getTimestamp("meta").longValue()); + assertEquals(Utils.decimalSecondsToMilliseconds(1.35E9), infoCollections.getTimestamp("tabs").longValue()); + assertNull(infoCollections.getTimestamp("missing")); + } + + @SuppressWarnings("static-method") + @Test + public void testUpdateNeeded() throws Exception { + ExtendedJSONObject record = new ExtendedJSONObject(TEST_COLLECTIONS_JSON); + InfoCollections infoCollections = new InfoCollections(record); + + long none = -1; + long past = Utils.decimalSecondsToMilliseconds(1.3E9); + long same = Utils.decimalSecondsToMilliseconds(1.35E9); + long future = Utils.decimalSecondsToMilliseconds(1.4E9); + + + // Test with no local timestamp set. + assertTrue(infoCollections.updateNeeded("tabs", none)); + + // Test with local timestamp set in the past. + assertTrue(infoCollections.updateNeeded("tabs", past)); + + // Test with same timestamp. + assertFalse(infoCollections.updateNeeded("tabs", same)); + + // Test with local timestamp set in the future. + assertFalse(infoCollections.updateNeeded("tabs", future)); + + // Test with no collection. + assertTrue(infoCollections.updateNeeded("missing", none)); + assertTrue(infoCollections.updateNeeded("missing", past)); + assertTrue(infoCollections.updateNeeded("missing", same)); + assertTrue(infoCollections.updateNeeded("missing", future)); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/test/TestPersistedMetaGlobal.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/test/TestPersistedMetaGlobal.java new file mode 100644 index 000000000..d1b6cadef --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/test/TestPersistedMetaGlobal.java @@ -0,0 +1,105 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.sync.test; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.MockSharedPreferences; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.sync.CryptoRecord; +import org.mozilla.gecko.sync.ExtendedJSONObject; +import org.mozilla.gecko.sync.MetaGlobal; +import org.mozilla.gecko.sync.NoCollectionKeysSetException; +import org.mozilla.gecko.sync.PersistedMetaGlobal; +import org.mozilla.gecko.sync.crypto.CryptoException; +import org.mozilla.gecko.sync.net.AuthHeaderProvider; +import org.mozilla.gecko.sync.net.BasicAuthHeaderProvider; + +import java.util.Set; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +@RunWith(TestRunner.class) +public class TestPersistedMetaGlobal { + MockSharedPreferences prefs = null; + private final String TEST_META_URL = "metaURL"; + private final String TEST_CREDENTIALS = "credentials"; + + @Before + public void setUp() { + prefs = new MockSharedPreferences(); + } + + @Test + public void testPersistLastModified() throws CryptoException, NoCollectionKeysSetException { + long LAST_MODIFIED = System.currentTimeMillis(); + PersistedMetaGlobal persisted = new PersistedMetaGlobal(prefs); + + // Test fresh start. + assertEquals(-1, persisted.lastModified()); + + // Test persisting. + persisted.persistLastModified(LAST_MODIFIED); + assertEquals(LAST_MODIFIED, persisted.lastModified()); + + // Test clearing. + persisted.persistLastModified(0); + assertEquals(-1, persisted.lastModified()); + } + + @Test + public void testPersistMetaGlobal() throws Exception { + PersistedMetaGlobal persisted = new PersistedMetaGlobal(prefs); + AuthHeaderProvider authHeaderProvider = new BasicAuthHeaderProvider(TEST_CREDENTIALS); + + // Test fresh start. + assertNull(persisted.metaGlobal(TEST_META_URL, authHeaderProvider)); + + // Test persisting. + String body = "{\"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}"; + MetaGlobal mg = new MetaGlobal(TEST_META_URL, authHeaderProvider); + mg.setFromRecord(CryptoRecord.fromJSONRecord(body)); + persisted.persistMetaGlobal(mg); + + MetaGlobal persistedGlobal = persisted.metaGlobal(TEST_META_URL, authHeaderProvider); + assertNotNull(persistedGlobal); + assertEquals("zPSQTm7WBVWB", persistedGlobal.getSyncID()); + assertTrue(persistedGlobal.getEngines() instanceof ExtendedJSONObject); + assertEquals(Long.valueOf(5), persistedGlobal.getStorageVersion()); + + // Test clearing. + persisted.persistMetaGlobal(null); + assertNull(persisted.metaGlobal(null, null)); + } + + @Test + public void testPersistDeclinedEngines() throws Exception { + PersistedMetaGlobal persisted = new PersistedMetaGlobal(prefs); + AuthHeaderProvider authHeaderProvider = new BasicAuthHeaderProvider(TEST_CREDENTIALS); + + // Test fresh start. + assertNull(persisted.metaGlobal(TEST_META_URL, authHeaderProvider)); + + // Test persisting. + String body = "{\"id\":\"global\",\"payload\":\"{\\\"declined\\\":[\\\"bookmarks\\\",\\\"addons\\\"],\\\"syncID\\\":\\\"zPSQTm7WBVWB\\\",\\\"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}"; + MetaGlobal mg = new MetaGlobal(TEST_META_URL, authHeaderProvider); + mg.setFromRecord(CryptoRecord.fromJSONRecord(body)); + persisted.persistMetaGlobal(mg); + + MetaGlobal persistedGlobal = persisted.metaGlobal(TEST_META_URL, authHeaderProvider); + assertNotNull(persistedGlobal); + Set<String> declined = persistedGlobal.getDeclinedEngineNames(); + assertEquals(2, declined.size()); + assertTrue(declined.contains("bookmarks")); + assertTrue(declined.contains("addons")); + + // Test clearing. + persisted.persistMetaGlobal(null); + assertNull(persisted.metaGlobal(null, null)); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/telemetry/measurements/TestSearchCountMeasurements.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/telemetry/measurements/TestSearchCountMeasurements.java new file mode 100644 index 000000000..058461f8e --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/telemetry/measurements/TestSearchCountMeasurements.java @@ -0,0 +1,161 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.telemetry.measurements; + +import android.content.Context; +import android.content.SharedPreferences; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.sync.ExtendedJSONObject; +import org.robolectric.RuntimeEnvironment; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Set; + +import static org.junit.Assert.*; + +/** + * Tests for the class that stores search count measurements. + */ +@RunWith(TestRunner.class) +public class TestSearchCountMeasurements { + + private SharedPreferences sharedPrefs; + + @Before + public void setUp() throws Exception { + sharedPrefs = RuntimeEnvironment.application.getSharedPreferences( + TestSearchCountMeasurements.class.getSimpleName(), Context.MODE_PRIVATE); + } + + private void assertNewValueInsertedNoIncrementedValues(final int expectedKeyCount) { + assertEquals("Shared prefs key count has incremented", expectedKeyCount, sharedPrefs.getAll().size()); + assertTrue("Shared prefs still contains non-incremented initial value", sharedPrefs.getAll().containsValue(1)); + assertFalse("Shared prefs has not incremented any values", sharedPrefs.getAll().containsValue(2)); + } + + @Test + public void testIncrementSearchCanRecreateEngineAndWhere() throws Exception { + final String expectedIdentifier = "google"; + final String expectedWhere = "suggestbar"; + + SearchCountMeasurements.incrementSearch(sharedPrefs, expectedIdentifier, expectedWhere); + assertFalse("Shared prefs has some values", sharedPrefs.getAll().isEmpty()); + assertTrue("Shared prefs contains initial value", sharedPrefs.getAll().containsValue(1)); + + boolean foundEngine = false; + for (final String key : sharedPrefs.getAll().keySet()) { + // We could try to match the exact key, but that's more fragile. + if (key.contains(expectedIdentifier) && key.contains(expectedWhere)) { + foundEngine = true; + } + } + assertTrue("SharedPrefs keyset contains enough info to recreate engine & where", foundEngine); + } + + @Test + public void testIncrementSearchCalledMultipleTimesSameEngine() throws Exception { + final String engineIdentifier = "whatever"; + final String where = "wherever"; + + SearchCountMeasurements.incrementSearch(sharedPrefs, engineIdentifier, where); + assertFalse("Shared prefs has some values", sharedPrefs.getAll().isEmpty()); + assertTrue("Shared prefs contains initial value", sharedPrefs.getAll().containsValue(1)); + + // The initial key count storage saves metadata so we can't verify the number of keys is only 1. However, + // we assume subsequent calls won't add additional metadata and use it to verify the key count. + final int keyCountAfterFirst = sharedPrefs.getAll().size(); + for (int i = 2; i <= 3; ++i) { + SearchCountMeasurements.incrementSearch(sharedPrefs, engineIdentifier, where); + assertEquals("Shared prefs key count has not changed", keyCountAfterFirst, sharedPrefs.getAll().size()); + assertTrue("Shared prefs incremented", sharedPrefs.getAll().containsValue(i)); + } + } + + @Test + public void testIncrementSearchCalledMultipleTimesSameEngineDifferentWhere() throws Exception { + final String engineIdenfitier = "whatever"; + + SearchCountMeasurements.incrementSearch(sharedPrefs, engineIdenfitier, "one place"); + assertFalse("Shared prefs has some values", sharedPrefs.getAll().isEmpty()); + assertTrue("Shared prefs contains initial value", sharedPrefs.getAll().containsValue(1)); + + // The initial key count storage saves metadata so we can't verify the number of keys is only 1. However, + // we assume subsequent calls won't add additional metadata and use it to verify the key count. + final int keyCountAfterFirst = sharedPrefs.getAll().size(); + for (int i = 1; i <= 2; ++i) { + SearchCountMeasurements.incrementSearch(sharedPrefs, engineIdenfitier, "another place " + i); + assertNewValueInsertedNoIncrementedValues(keyCountAfterFirst + i); + } + } + + @Test + public void testIncrementSearchCalledMultipleTimesDifferentEngines() throws Exception { + final String where = "wherever"; + + SearchCountMeasurements.incrementSearch(sharedPrefs, "steam engine", where); + assertFalse("Shared prefs has some values", sharedPrefs.getAll().isEmpty()); + assertTrue("Shared prefs contains initial value", sharedPrefs.getAll().containsValue(1)); + + // The initial key count storage saves metadata so we can't verify the number of keys is only 1. However, + // we assume subsequent calls won't add additional metadata and use it to verify the key count. + final int keyCountAfterFirst = sharedPrefs.getAll().size(); + for (int i = 1; i <= 2; ++i) { + SearchCountMeasurements.incrementSearch(sharedPrefs, "combustion engine" + i, where); + assertNewValueInsertedNoIncrementedValues(keyCountAfterFirst + i); + } + } + + @Test // assumes the format saved in SharedPrefs to store test data + public void testGetAndZeroSearchDeletesPrefs() throws Exception { + assertTrue("Shared prefs is empty", sharedPrefs.getAll().isEmpty()); + + final SharedPreferences.Editor editor = sharedPrefs.edit(); + final Set<String> engineKeys = new HashSet<>(Arrays.asList("whatever.yeah", "lol.what")); + editor.putStringSet(SearchCountMeasurements.PREF_SEARCH_KEYSET, engineKeys); + for (final String key : engineKeys) { + editor.putInt(getEngineSearchCountKey(key), 1); + } + editor.apply(); + assertFalse("Shared prefs is not empty after test data inserted", sharedPrefs.getAll().isEmpty()); + + SearchCountMeasurements.getAndZeroSearch(sharedPrefs); + assertTrue("Shared prefs is empty after zero", sharedPrefs.getAll().isEmpty()); + } + + @Test // assumes the format saved in SharedPrefs to store test data + public void testGetAndZeroSearchVerifyReturnedData() throws Exception { + final HashMap<String, Integer> expected = new HashMap<>(); + expected.put("steamengine.here", 1337); + expected.put("combustionengine.there", 10); + + final SharedPreferences.Editor editor = sharedPrefs.edit(); + editor.putStringSet(SearchCountMeasurements.PREF_SEARCH_KEYSET, expected.keySet()); + for (final String key : expected.keySet()) { + editor.putInt(SearchCountMeasurements.getEngineSearchCountKey(key), expected.get(key)); + } + editor.apply(); + assertFalse("Shared prefs is not empty after test data inserted", sharedPrefs.getAll().isEmpty()); + + final ExtendedJSONObject actual = SearchCountMeasurements.getAndZeroSearch(sharedPrefs); + assertEquals("Returned JSON contains number of items inserted", expected.size(), actual.size()); + for (final String key : expected.keySet()) { + assertEquals("Returned JSON contains inserted value", expected.get(key), (Integer) actual.getIntegerSafely(key)); + } + } + + @Test + public void testGetAndZeroSearchNoData() throws Exception { + final ExtendedJSONObject actual = SearchCountMeasurements.getAndZeroSearch(sharedPrefs); + assertEquals("Returned json is empty", 0, actual.size()); + } + + private String getEngineSearchCountKey(final String engineWhereStr) { + return SearchCountMeasurements.getEngineSearchCountKey(engineWhereStr); + } +}
\ No newline at end of file diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/telemetry/measurements/TestSessionMeasurements.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/telemetry/measurements/TestSessionMeasurements.java new file mode 100644 index 000000000..a5d3ce551 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/telemetry/measurements/TestSessionMeasurements.java @@ -0,0 +1,124 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package org.mozilla.gecko.telemetry.measurements; + +import android.content.Context; +import android.content.SharedPreferences; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.telemetry.measurements.SessionMeasurements.SessionMeasurementsContainer; +import org.robolectric.RuntimeEnvironment; + +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +/** + * Tests the session measurements class. + */ +@RunWith(TestRunner.class) +public class TestSessionMeasurements { + + private SessionMeasurements testMeasurements; + private SharedPreferences sharedPrefs; + private Context context; + + @Before + public void setUp() throws Exception { + testMeasurements = spy(SessionMeasurements.class); + sharedPrefs = RuntimeEnvironment.application.getSharedPreferences( + TestSessionMeasurements.class.getSimpleName(), Context.MODE_PRIVATE); + doReturn(sharedPrefs).when(testMeasurements).getSharedPreferences(any(Context.class)); + + context = RuntimeEnvironment.application; + } + + private void assertSessionCount(final String postfix, final int expectedSessionCount) { + final int actual = sharedPrefs.getInt(SessionMeasurements.PREF_SESSION_COUNT, -1); + assertEquals("Expected number of sessions occurred " + postfix, expectedSessionCount, actual); + } + + private void assertSessionDuration(final String postfix, final long expectedSessionDuration) { + final long actual = sharedPrefs.getLong(SessionMeasurements.PREF_SESSION_DURATION, -1); + assertEquals("Expected session duration received " + postfix, expectedSessionDuration, actual); + } + + private void mockGetSystemTimeNanosToReturn(final long value) { + doReturn(value).when(testMeasurements).getSystemTimeNano(); + } + + @Test + public void testRecordSessionStartAndEndCalledOnce() throws Exception { + final long expectedElapsedSeconds = 4; + mockGetSystemTimeNanosToReturn(0); + testMeasurements.recordSessionStart(); + mockGetSystemTimeNanosToReturn(TimeUnit.SECONDS.toNanos(expectedElapsedSeconds)); + testMeasurements.recordSessionEnd(context); + + final String postfix = "after recordSessionStart/End called once"; + assertSessionCount(postfix, 1); + assertSessionDuration(postfix, expectedElapsedSeconds); + } + + @Test + public void testRecordSessionStartAndEndCalledTwice() throws Exception { + final long expectedElapsedSeconds = 100; + mockGetSystemTimeNanosToReturn(0L); + for (int i = 1; i <= 2; ++i) { + testMeasurements.recordSessionStart(); + mockGetSystemTimeNanosToReturn(TimeUnit.SECONDS.toNanos((expectedElapsedSeconds / 2) * i)); + testMeasurements.recordSessionEnd(context); + } + + final String postfix = "after recordSessionStart/End called twice"; + assertSessionCount(postfix, 2); + assertSessionDuration(postfix, expectedElapsedSeconds); + } + + @Test(expected = IllegalStateException.class) + public void testRecordSessionStartThrowsIfSessionAlreadyStarted() throws Exception { + // First call will start the session, next expected to throw. + for (int i = 0; i < 2; ++i) { + testMeasurements.recordSessionStart(); + } + } + + @Test(expected = IllegalStateException.class) + public void testRecordSessionEndThrowsIfCalledBeforeSessionStarted() { + testMeasurements.recordSessionEnd(context); + } + + @Test // assumes the underlying format in SessionMeasurements + public void testGetAndResetSessionMeasurementsReturnsSetData() throws Exception { + final int expectedSessionCount = 42; + final long expectedSessionDuration = 1234567890; + sharedPrefs.edit() + .putInt(SessionMeasurements.PREF_SESSION_COUNT, expectedSessionCount) + .putLong(SessionMeasurements.PREF_SESSION_DURATION, expectedSessionDuration) + .apply(); + + final SessionMeasurementsContainer actual = testMeasurements.getAndResetSessionMeasurements(context); + assertEquals("Returned session count matches expected", expectedSessionCount, actual.sessionCount); + assertEquals("Returned session duration matches expected", expectedSessionDuration, actual.elapsedSeconds); + } + + @Test + public void testGetAndResetSessionMeasurementsResetsData() throws Exception { + sharedPrefs.edit() + .putInt(SessionMeasurements.PREF_SESSION_COUNT, 10) + .putLong(SessionMeasurements.PREF_SESSION_DURATION, 10) + .apply(); + + testMeasurements.getAndResetSessionMeasurements(context); + final String postfix = "is reset after retrieval"; + assertSessionCount(postfix, 0); + assertSessionDuration(postfix, 0); + } +}
\ No newline at end of file diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/telemetry/pingbuilders/TestTelemetryPingBuilder.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/telemetry/pingbuilders/TestTelemetryPingBuilder.java new file mode 100644 index 000000000..ca0124121 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/telemetry/pingbuilders/TestTelemetryPingBuilder.java @@ -0,0 +1,84 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package org.mozilla.gecko.telemetry.pingbuilders; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.TestRunner; + +import static org.junit.Assert.*; + +/** + * Unit test methods of the {@link TelemetryPingBuilder} class. + */ +@RunWith(TestRunner.class) +public class TestTelemetryPingBuilder { + @Test + public void testMandatoryFieldsNone() { + final NoMandatoryFieldsBuilder builder = new NoMandatoryFieldsBuilder(); + builder.setNonMandatoryField(); + assertNotNull("Builder does not throw and returns a non-null value", builder.build()); + } + + @Test(expected = IllegalArgumentException.class) + public void testMandatoryFieldsMissing() { + final MandatoryFieldsBuilder builder = new MandatoryFieldsBuilder(); + builder.setNonMandatoryField() + .build(); // should throw + } + + @Test + public void testMandatoryFieldsIncluded() { + final MandatoryFieldsBuilder builder = new MandatoryFieldsBuilder(); + builder.setNonMandatoryField() + .setMandatoryField(); + assertNotNull("Builder does not throw and returns non-null value", builder.build()); + } + + private static class NoMandatoryFieldsBuilder extends TelemetryPingBuilder { + @Override + public String getDocType() { + return ""; + } + + @Override + public String[] getMandatoryFields() { + return new String[0]; + } + + public NoMandatoryFieldsBuilder setNonMandatoryField() { + payload.put("non-mandatory", true); + return this; + } + } + + private static class MandatoryFieldsBuilder extends TelemetryPingBuilder { + private static final String MANDATORY_FIELD = "mandatory-field"; + + @Override + public String getDocType() { + return ""; + } + + @Override + public String[] getMandatoryFields() { + return new String[] { + MANDATORY_FIELD, + }; + } + + public MandatoryFieldsBuilder setNonMandatoryField() { + payload.put("non-mandatory", true); + return this; + } + + public MandatoryFieldsBuilder setMandatoryField() { + payload.put(MANDATORY_FIELD, true); + return this; + } + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/telemetry/schedulers/TestTelemetryUploadAllPingsImmediatelyScheduler.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/telemetry/schedulers/TestTelemetryUploadAllPingsImmediatelyScheduler.java new file mode 100644 index 000000000..8093040ee --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/telemetry/schedulers/TestTelemetryUploadAllPingsImmediatelyScheduler.java @@ -0,0 +1,59 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package org.mozilla.gecko.telemetry.schedulers; + +import android.content.Context; +import android.content.Intent; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.telemetry.TelemetryUploadService; +import org.mozilla.gecko.telemetry.stores.TelemetryPingStore; + +import static junit.framework.Assert.*; +import static org.mockito.Mockito.*; + +/** + * Unit tests for the upload immediately scheduler. + * + * When we add more schedulers, we'll likely change the interface + * (e.g. pass in current time) and these tests will be more useful. + */ +@RunWith(TestRunner.class) +public class TestTelemetryUploadAllPingsImmediatelyScheduler { + + private TelemetryUploadAllPingsImmediatelyScheduler testScheduler; + private TelemetryPingStore testStore; + + @Before + public void setUp() { + testScheduler = new TelemetryUploadAllPingsImmediatelyScheduler(); + testStore = mock(TelemetryPingStore.class); + } + + @Test + public void testReadyToUpload() { + assertTrue("Scheduler is always ready to upload", testScheduler.isReadyToUpload(testStore)); + } + + @Test + public void testScheduleUpload() { + final Context context = mock(Context.class); + + testScheduler.scheduleUpload(context, testStore); + + final ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class); + verify(context).startService(intentCaptor.capture()); + final Intent actualIntent = intentCaptor.getValue(); + assertEquals("Intent action is upload", TelemetryUploadService.ACTION_UPLOAD, actualIntent.getAction()); + assertTrue("Intent contains store", actualIntent.hasExtra(TelemetryUploadService.EXTRA_STORE)); + assertEquals("Intent class target is upload service", + TelemetryUploadService.class.getName(), actualIntent.getComponent().getClassName()); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/telemetry/stores/TestTelemetryJSONFilePingStore.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/telemetry/stores/TestTelemetryJSONFilePingStore.java new file mode 100644 index 000000000..a95a8b292 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/telemetry/stores/TestTelemetryJSONFilePingStore.java @@ -0,0 +1,250 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package org.mozilla.gecko.telemetry.stores; + +import org.json.JSONObject; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.sync.ExtendedJSONObject; +import org.mozilla.gecko.telemetry.TelemetryPing; +import org.mozilla.gecko.util.FileUtils; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.FilenameFilter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.UUID; + +import static org.junit.Assert.*; + +/** + * Unit test methods of the {@link TelemetryJSONFilePingStore} class. + */ +@RunWith(TestRunner.class) +public class TestTelemetryJSONFilePingStore { + + @Rule + public TemporaryFolder tempDir = new TemporaryFolder(); + private File testDir; + private TelemetryJSONFilePingStore testStore; + + @Before + public void setUp() throws Exception { + testDir = tempDir.newFolder(); + testStore = new TelemetryJSONFilePingStore(testDir, ""); + } + + private ExtendedJSONObject generateTelemetryPayload() { + final ExtendedJSONObject out = new ExtendedJSONObject(); + out.put("str", "a String"); + out.put("int", 42); + out.put("null", (ExtendedJSONObject) null); + return out; + } + + private void assertIsGeneratedPayload(final ExtendedJSONObject actual) throws Exception { + assertNull("Null field is null", actual.getObject("null")); + assertEquals("int field is correct", 42, (int) actual.getIntegerSafely("int")); + assertEquals("str field is correct", "a String", actual.getString("str")); + } + + private void assertStoreFileCount(final int expectedCount) { + assertEquals("Store contains " + expectedCount + " item(s)", expectedCount, testDir.list().length); + } + + @Test + public void testConstructorOnlyWritesToGivenDir() throws Exception { + // Constructor is called in @Before method + assertTrue("Store dir exists", testDir.exists()); + assertEquals("Temp dir contains one dir (the store dir)", 1, tempDir.getRoot().list().length); + } + + @Test(expected = IllegalStateException.class) + public void testConstructorStoreAlreadyExistsAsNonDirectory() throws Exception { + final File file = tempDir.newFile(); + new TelemetryJSONFilePingStore(file, "profileName"); // expected to throw. + } + + @Test(expected = IllegalStateException.class) + public void testConstructorDirIsNotReadable() throws Exception { + final File dir = tempDir.newFolder(); + dir.setReadable(false); + new TelemetryJSONFilePingStore(dir, "profileName"); // expected to throw. + } + + @Test(expected = IllegalStateException.class) + public void testConstructorDirIsNotWritable() throws Exception { + final File dir = tempDir.newFolder(); + dir.setWritable(false); + new TelemetryJSONFilePingStore(dir, "profileName"); // expected to throw. + } + + @Test(expected = IllegalStateException.class) + public void testConstructorDirIsNotExecutable() throws Exception { + final File dir = tempDir.newFolder(); + dir.setExecutable(false); + new TelemetryJSONFilePingStore(dir, "profileName"); // expected to throw. + } + + @Test + public void testStorePingStoresCorrectData() throws Exception { + assertStoreFileCount(0); + + final String expectedID = getDocID(); + final TelemetryPing expectedPing = new TelemetryPing("a/server/url", generateTelemetryPayload(), expectedID); + testStore.storePing(expectedPing); + + assertStoreFileCount(1); + final String filename = testDir.list()[0]; + assertTrue("Filename contains expected ID", filename.equals(expectedID)); + final JSONObject actual = FileUtils.readJSONObjectFromFile(new File(testDir, filename)); + assertEquals("Ping url paths are equal", expectedPing.getURLPath(), actual.getString(TelemetryJSONFilePingStore.KEY_URL_PATH)); + assertIsGeneratedPayload(new ExtendedJSONObject(actual.getString(TelemetryJSONFilePingStore.KEY_PAYLOAD))); + } + + @Test + public void testStorePingMultiplePingsStoreSeparateFiles() throws Exception { + assertStoreFileCount(0); + for (int i = 1; i < 10; ++i) { + testStore.storePing(new TelemetryPing("server", generateTelemetryPayload(), getDocID())); + assertStoreFileCount(i); + } + } + + @Test + public void testStorePingReleasesFileLock() throws Exception { + assertStoreFileCount(0); + testStore.storePing(new TelemetryPing("server", generateTelemetryPayload(), getDocID())); + assertStoreFileCount(1); + final File file = new File(testDir, testDir.list()[0]); + final FileOutputStream stream = new FileOutputStream(file); + try { + assertNotNull("File lock is released after store write", stream.getChannel().tryLock()); + } finally { + stream.close(); // releases lock + } + } + + @Test + public void testGetAllPingsSavesData() throws Exception { + final String urlPrefix = "url"; + writeTestPingsToStore(3, urlPrefix); + + final ArrayList<TelemetryPing> pings = testStore.getAllPings(); + for (final TelemetryPing ping : pings) { + assertEquals("Expected url path value received", urlPrefix + ping.getDocID(), ping.getURLPath()); + assertIsGeneratedPayload(ping.getPayload()); + } + } + + @Test + public void testGetAllPingsIsSorted() throws Exception { + final List<String> storedDocIDs = writeTestPingsToStore(3, "urlPrefix"); + + final ArrayList<TelemetryPing> pings = testStore.getAllPings(); + for (int i = 0; i < pings.size(); ++i) { + final String expectedDocID = storedDocIDs.get(i); + final TelemetryPing ping = pings.get(i); + + assertEquals("Stored ping " + i + " retrieved in order", expectedDocID, ping.getDocID()); + } + } + + @Test // regression test: bug 1272817 + public void testGetAllPingsHandlesEmptyFiles() throws Exception { + final int expectedPingCount = 3; + writeTestPingsToStore(expectedPingCount, "whatever"); + assertTrue("Empty file is created", testStore.getPingFile(getDocID()).createNewFile()); + assertEquals("Returned pings only contains valid files", expectedPingCount, testStore.getAllPings().size()); + } + + @Test + public void testMaybePrunePingsDoesNothingIfAtMax() throws Exception { + final int pingCount = TelemetryJSONFilePingStore.MAX_PING_COUNT; + writeTestPingsToStore(pingCount, "whatever"); + assertStoreFileCount(pingCount); + testStore.maybePrunePings(); + assertStoreFileCount(pingCount); + } + + @Test + public void testMaybePrunePingsPrunesIfAboveMax() throws Exception { + final int pingCount = TelemetryJSONFilePingStore.MAX_PING_COUNT + 1; + final List<String> expectedDocIDs = writeTestPingsToStore(pingCount, "whatever"); + assertStoreFileCount(pingCount); + testStore.maybePrunePings(); + assertStoreFileCount(TelemetryJSONFilePingStore.MAX_PING_COUNT); + + final HashSet<String> existingIDs = new HashSet<>(Arrays.asList(testDir.list())); + assertFalse("Oldest ping was removed", existingIDs.contains(expectedDocIDs.get(0))); + } + + @Test + public void testOnUploadAttemptCompleted() throws Exception { + final List<String> savedDocIDs = writeTestPingsToStore(10, "url"); + final int halfSize = savedDocIDs.size() / 2; + final Set<String> unuploadedPingIDs = new HashSet<>(savedDocIDs.subList(0, halfSize)); + final Set<String> removedPingIDs = new HashSet<>(savedDocIDs.subList(halfSize, savedDocIDs.size())); + testStore.onUploadAttemptComplete(removedPingIDs); + + for (final String unuploadedDocID : testDir.list()) { + assertFalse("Unuploaded ID is not in removed ping IDs", removedPingIDs.contains(unuploadedDocID)); + assertTrue("Unuploaded ID is in unuploaded ping IDs", unuploadedPingIDs.contains(unuploadedDocID)); + unuploadedPingIDs.remove(unuploadedDocID); + } + assertTrue("All non-successful-upload ping IDs were matched", unuploadedPingIDs.isEmpty()); + } + + @Test + public void testGetPingFileIsDocID() throws Exception { + final String docID = getDocID(); + final File file = testStore.getPingFile(docID); + assertTrue("Ping filename contains ID", file.getName().equals(docID)); + } + + /** + * Writes pings to store without using store API with: + * server = urlPrefix + docID + * payload = generated payload + * + * The docID is stored as the filename. + * + * Note: assumes {@link TelemetryJSONFilePingStore#getPingFile(String)} works. + * + * @return a list of doc IDs saved to disk in ascending order of last modified date + */ + private List<String> writeTestPingsToStore(final int count, final String urlPrefix) throws Exception { + final List<String> savedDocIDs = new ArrayList<>(count); + final long now = System.currentTimeMillis(); + for (int i = 1; i <= count; ++i) { + final String docID = getDocID(); + final JSONObject obj = new JSONObject() + .put(TelemetryJSONFilePingStore.KEY_URL_PATH, urlPrefix + docID) + .put(TelemetryJSONFilePingStore.KEY_PAYLOAD, generateTelemetryPayload()); + final File pingFile = testStore.getPingFile(docID); + FileUtils.writeJSONObjectToFile(pingFile, obj); + + // If we don't set an explicit time, the modified times are all equal. + // Also, last modified times are rounded by second. + assertTrue("Able to set last modified time", pingFile.setLastModified(now - (count * 10_000) + i * 10_000)); + savedDocIDs.add(docID); + } + return savedDocIDs; + } + + private String getDocID() { + return UUID.randomUUID().toString(); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/tokenserver/test/TestTokenServerClient.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/tokenserver/test/TestTokenServerClient.java new file mode 100644 index 000000000..03fe67794 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/tokenserver/test/TestTokenServerClient.java @@ -0,0 +1,335 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.tokenserver.test; + +import ch.boye.httpclientandroidlib.Header; +import ch.boye.httpclientandroidlib.HttpResponse; +import ch.boye.httpclientandroidlib.ProtocolVersion; +import ch.boye.httpclientandroidlib.client.methods.HttpGet; +import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase; +import ch.boye.httpclientandroidlib.entity.StringEntity; +import ch.boye.httpclientandroidlib.message.BasicHeader; +import ch.boye.httpclientandroidlib.message.BasicHttpResponse; +import ch.boye.httpclientandroidlib.message.BasicStatusLine; +import junit.framework.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.common.log.Logger; +import org.mozilla.gecko.background.common.log.writers.StringLogWriter; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.background.testhelpers.WaitHelper; +import org.mozilla.gecko.sync.ExtendedJSONObject; +import org.mozilla.gecko.sync.net.AuthHeaderProvider; +import org.mozilla.gecko.sync.net.BaseResource; +import org.mozilla.gecko.sync.net.SyncResponse; +import org.mozilla.gecko.tokenserver.TokenServerClient; +import org.mozilla.gecko.tokenserver.TokenServerClient.TokenFetchResourceDelegate; +import org.mozilla.gecko.tokenserver.TokenServerClientDelegate; +import org.mozilla.gecko.tokenserver.TokenServerException; +import org.mozilla.gecko.tokenserver.TokenServerException.TokenServerConditionsRequiredException; +import org.mozilla.gecko.tokenserver.TokenServerException.TokenServerInvalidCredentialsException; +import org.mozilla.gecko.tokenserver.TokenServerException.TokenServerMalformedRequestException; +import org.mozilla.gecko.tokenserver.TokenServerException.TokenServerMalformedResponseException; +import org.mozilla.gecko.tokenserver.TokenServerException.TokenServerUnknownServiceException; +import org.mozilla.gecko.tokenserver.TokenServerToken; + +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +@RunWith(TestRunner.class) +public class TestTokenServerClient { + public static final String JSON = "application/json"; + public static final String TEXT = "text/plain"; + + public static final String TEST_TOKEN_RESPONSE = "{\"api_endpoint\": \"https://stage-aitc1.services.mozilla.com/1.0/1659259\"," + + "\"duration\": 300," + + "\"id\": \"eySHORTENED\"," + + "\"key\": \"-plSHORTENED\"," + + "\"uid\": 1659259}"; + + public static final String TEST_CONDITIONS_RESPONSE = "{\"errors\":[{" + + "\"location\":\"header\"," + + "\"description\":\"Need to accept conditions\"," + + "\"condition_urls\":{\"tos\":\"http://url-to-tos.com\"}," + + "\"name\":\"X-Conditions-Accepted\"}]," + + "\"status\":\"error\"}"; + + public static final String TEST_ERROR_RESPONSE = "{\"status\": \"error\"," + + "\"errors\": [{\"location\": \"body\", \"name\": \"\", \"description\": \"Unauthorized EXTENDED\"}]}"; + + public static final String TEST_INVALID_TIMESTAMP_RESPONSE = "{\"status\": \"invalid-timestamp\", " + + "\"errors\": [{\"location\": \"body\", \"name\": \"\", \"description\": \"Unauthorized\"}]}"; + + protected TokenServerClient client; + + @Before + public void setUp() throws Exception { + this.client = new TokenServerClient(new URI("http://unused.com"), Executors.newSingleThreadExecutor()); + } + + protected TokenServerToken doProcessResponse(int statusCode, String contentType, Object token) + throws UnsupportedEncodingException, TokenServerException { + final HttpResponse response = new BasicHttpResponse( + new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), statusCode, "OK")); + + StringEntity stringEntity = new StringEntity(token.toString()); + stringEntity.setContentType(contentType); + response.setEntity(stringEntity); + + return client.processResponse(new SyncResponse(response)); + } + + @SuppressWarnings("rawtypes") + protected TokenServerException expectProcessResponseFailure(int statusCode, String contentType, Object token, Class klass) + throws TokenServerException, UnsupportedEncodingException { + try { + doProcessResponse(statusCode, contentType, token.toString()); + fail("Expected exception of type " + klass + "."); + + return null; + } catch (TokenServerException e) { + assertEquals(klass, e.getClass()); + + return e; + } + } + + @SuppressWarnings("rawtypes") + protected TokenServerException expectProcessResponseFailure(Object token, Class klass) + throws UnsupportedEncodingException, TokenServerException { + return expectProcessResponseFailure(200, "application/json", token, klass); + } + + @Test + public void testProcessResponseSuccess() throws Exception { + TokenServerToken token = doProcessResponse(200, "application/json", TEST_TOKEN_RESPONSE); + + assertEquals("eySHORTENED", token.id); + assertEquals("-plSHORTENED", token.key); + assertEquals("1659259", token.uid); + assertEquals("https://stage-aitc1.services.mozilla.com/1.0/1659259", token.endpoint); + } + + @Test + public void testProcessResponseFailure() throws Exception { + // Wrong Content-Type. + expectProcessResponseFailure(200, TEXT, new ExtendedJSONObject(), TokenServerMalformedResponseException.class); + + // Not valid JSON. + expectProcessResponseFailure("#!", TokenServerMalformedResponseException.class); + + // Status code 400. + expectProcessResponseFailure(400, JSON, new ExtendedJSONObject(), TokenServerMalformedRequestException.class); + + // Status code 401. + expectProcessResponseFailure(401, JSON, new ExtendedJSONObject(), TokenServerInvalidCredentialsException.class); + expectProcessResponseFailure(401, JSON, TEST_INVALID_TIMESTAMP_RESPONSE, TokenServerInvalidCredentialsException.class); + + // Status code 404. + expectProcessResponseFailure(404, JSON, new ExtendedJSONObject(), TokenServerUnknownServiceException.class); + + // Status code 406, which is not specially handled, but with errors. We take + // care that errors are actually printed because we're going to want this to + // work when things go wrong. + StringLogWriter logWriter = new StringLogWriter(); + + Logger.startLoggingTo(logWriter); + try { + expectProcessResponseFailure(406, JSON, TEST_ERROR_RESPONSE, TokenServerException.class); + + assertTrue(logWriter.toString().contains("Unauthorized EXTENDED")); + } finally { + Logger.stopLoggingTo(logWriter); + } + + // Status code 503. + expectProcessResponseFailure(503, JSON, new ExtendedJSONObject(), TokenServerException.class); + } + + @Test + public void testProcessResponseConditionsRequired() throws Exception { + + // Status code 403: conditions need to be accepted, but malformed (no urls). + expectProcessResponseFailure(403, JSON, new ExtendedJSONObject(), TokenServerMalformedResponseException.class); + + // Status code 403, with urls. + TokenServerConditionsRequiredException e = (TokenServerConditionsRequiredException) + expectProcessResponseFailure(403, JSON, TEST_CONDITIONS_RESPONSE, TokenServerConditionsRequiredException.class); + + ExtendedJSONObject expectedUrls = new ExtendedJSONObject(); + expectedUrls.put("tos", "http://url-to-tos.com"); + assertEquals(expectedUrls.toString(), e.conditionUrls.toString()); + } + + @Test + public void testProcessResponseMalformedToken() throws Exception { + ExtendedJSONObject token; + + // Missing key. + token = new ExtendedJSONObject(TEST_TOKEN_RESPONSE); + token.remove("api_endpoint"); + expectProcessResponseFailure(token, TokenServerMalformedResponseException.class); + + // Key has wrong type; expected String. + token = new ExtendedJSONObject(TEST_TOKEN_RESPONSE); + token.put("api_endpoint", new ExtendedJSONObject()); + expectProcessResponseFailure(token, TokenServerMalformedResponseException.class); + + // Key has wrong type; expected number. + token = new ExtendedJSONObject(TEST_TOKEN_RESPONSE); + token.put("uid", "NON NUMERIC"); + expectProcessResponseFailure(token, TokenServerMalformedResponseException.class); + } + + private class MockBaseResource extends BaseResource { + public MockBaseResource(String uri) throws URISyntaxException { + super(uri); + this.request = new HttpGet(this.uri); + } + + public HttpRequestBase prepareHeadersAndReturn() throws Exception { + super.prepareClient(); + return request; + } + } + + @Test + public void testClientStateHeader() throws Exception { + String assertion = "assertion"; + String clientState = "abcdef"; + MockBaseResource resource = new MockBaseResource("http://unused.local/"); + + TokenServerClientDelegate delegate = new TokenServerClientDelegate() { + @Override + public void handleSuccess(TokenServerToken token) { + } + + @Override + public void handleFailure(TokenServerException e) { + } + + @Override + public void handleError(Exception e) { + } + + @Override + public void handleBackoff(int backoffSeconds) { + } + + @Override + public String getUserAgent() { + return null; + } + }; + + resource.delegate = new TokenServerClient.TokenFetchResourceDelegate(client, resource, delegate, assertion, clientState , true) { + @Override + public AuthHeaderProvider getAuthHeaderProvider() { + return null; + } + }; + + HttpRequestBase request = resource.prepareHeadersAndReturn(); + Assert.assertEquals("abcdef", request.getFirstHeader("X-Client-State").getValue()); + Assert.assertEquals("1", request.getFirstHeader("X-Conditions-Accepted").getValue()); + } + + public static class MockTokenServerClient extends TokenServerClient { + public MockTokenServerClient(URI uri, Executor executor) { + super(uri, executor); + } + } + + public static final class MockTokenServerClientDelegate implements + TokenServerClientDelegate { + public volatile boolean backoffCalled; + public volatile boolean succeeded; + public volatile int backoff; + + @Override + public String getUserAgent() { + return null; + } + + @Override + public void handleSuccess(TokenServerToken token) { + succeeded = true; + WaitHelper.getTestWaiter().performNotify(); + } + + @Override + public void handleFailure(TokenServerException e) { + succeeded = false; + WaitHelper.getTestWaiter().performNotify(); + } + + @Override + public void handleError(Exception e) { + succeeded = false; + WaitHelper.getTestWaiter().performNotify(e); + } + + @Override + public void handleBackoff(int backoffSeconds) { + backoffCalled = true; + backoff = backoffSeconds; + } + } + + private void expectDelegateCalls(URI uri, MockTokenServerClient client, int code, Header header, String body, boolean succeeded, long backoff, boolean expectBackoff) throws UnsupportedEncodingException { + final BaseResource resource = new BaseResource(uri); + final String assertion = "someassertion"; + final String clientState = "abcdefabcdefabcdefabcdefabcdefab"; + final boolean conditionsAccepted = true; + + MockTokenServerClientDelegate delegate = new MockTokenServerClientDelegate(); + + final TokenFetchResourceDelegate tokenFetchResourceDelegate = new TokenServerClient.TokenFetchResourceDelegate(client, resource, delegate, assertion, clientState, conditionsAccepted); + + final BasicStatusLine statusline = new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), code, "Whatever"); + final HttpResponse response = new BasicHttpResponse(statusline); + response.setHeader(header); + if (body != null) { + final StringEntity entity = new StringEntity(body); + entity.setContentType("application/json"); + response.setEntity(entity); + } + + WaitHelper.getTestWaiter().performWait(new Runnable() { + @Override + public void run() { + tokenFetchResourceDelegate.handleHttpResponse(response); + } + }); + + assertEquals(expectBackoff, delegate.backoffCalled); + assertEquals(backoff, delegate.backoff); + assertEquals(succeeded, delegate.succeeded); + } + + @Test + public void testBackoffHandling() throws URISyntaxException, UnsupportedEncodingException { + final URI uri = new URI("http://unused.com"); + final MockTokenServerClient client = new MockTokenServerClient(uri, Executors.newSingleThreadExecutor()); + + // Even the 200 code here is false because the body is malformed. + expectDelegateCalls(uri, client, 200, new BasicHeader("x-backoff", "13"), "baaaa", false, 13, true); + expectDelegateCalls(uri, client, 400, new BasicHeader("X-Weave-Backoff", "15"), null, false, 15, true); + expectDelegateCalls(uri, client, 503, new BasicHeader("retry-after", "3"), null, false, 3, true); + + // Retry-After is only processed on 503. + expectDelegateCalls(uri, client, 200, new BasicHeader("retry-after", "13"), null, false, 0, false); + + // Now let's try one with a valid body. + expectDelegateCalls(uri, client, 200, new BasicHeader("X-Backoff", "1234"), TEST_TOKEN_RESPONSE, true, 1234, true); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/util/NetworkUtilsTest.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/util/NetworkUtilsTest.java new file mode 100644 index 000000000..fb2cffc92 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/util/NetworkUtilsTest.java @@ -0,0 +1,185 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.util; + +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.telephony.TelephonyManager; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.util.NetworkUtils.*; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.internal.ShadowExtractor; +import org.robolectric.shadows.ShadowConnectivityManager; +import org.robolectric.shadows.ShadowNetworkInfo; + +import static org.junit.Assert.*; + +@RunWith(TestRunner.class) +public class NetworkUtilsTest { + private ConnectivityManager connectivityManager; + private ShadowConnectivityManager shadowConnectivityManager; + + @Before + public void setUp() { + connectivityManager = (ConnectivityManager) RuntimeEnvironment.application.getSystemService(Context.CONNECTIVITY_SERVICE); + + // Not using Shadows.shadowOf(connectivityManager) because of Robolectric bug when using API23+ + // See: https://github.com/robolectric/robolectric/issues/1862 + shadowConnectivityManager = (ShadowConnectivityManager) ShadowExtractor.extract(connectivityManager); + } + + @Test + public void testIsConnected() throws Exception { + assertFalse(NetworkUtils.isConnected((ConnectivityManager) null)); + + shadowConnectivityManager.setActiveNetworkInfo(null); + assertFalse(NetworkUtils.isConnected(connectivityManager)); + + shadowConnectivityManager.setActiveNetworkInfo( + ShadowNetworkInfo.newInstance(NetworkInfo.DetailedState.CONNECTED, ConnectivityManager.TYPE_WIFI, 0, true, true) + ); + assertTrue(NetworkUtils.isConnected(connectivityManager)); + + shadowConnectivityManager.setActiveNetworkInfo( + ShadowNetworkInfo.newInstance(NetworkInfo.DetailedState.DISCONNECTED, ConnectivityManager.TYPE_WIFI, 0, true, false) + ); + assertFalse(NetworkUtils.isConnected(connectivityManager)); + } + + @Test + public void testGetConnectionSubType() throws Exception { + assertEquals(ConnectionSubType.UNKNOWN, NetworkUtils.getConnectionSubType(null)); + + shadowConnectivityManager.setActiveNetworkInfo(null); + assertEquals(ConnectionSubType.UNKNOWN, NetworkUtils.getConnectionSubType(connectivityManager)); + + // We don't seem to care about figuring out all connection types. So... + shadowConnectivityManager.setActiveNetworkInfo( + ShadowNetworkInfo.newInstance(NetworkInfo.DetailedState.CONNECTED, ConnectivityManager.TYPE_VPN, 0, true, true) + ); + assertEquals(ConnectionSubType.UNKNOWN, NetworkUtils.getConnectionSubType(connectivityManager)); + + // But anything below we should recognize. + shadowConnectivityManager.setActiveNetworkInfo( + ShadowNetworkInfo.newInstance(NetworkInfo.DetailedState.CONNECTED, ConnectivityManager.TYPE_ETHERNET, 0, true, true) + ); + assertEquals(ConnectionSubType.ETHERNET, NetworkUtils.getConnectionSubType(connectivityManager)); + + shadowConnectivityManager.setActiveNetworkInfo( + ShadowNetworkInfo.newInstance(NetworkInfo.DetailedState.CONNECTED, ConnectivityManager.TYPE_WIFI, 0, true, true) + ); + assertEquals(ConnectionSubType.WIFI, NetworkUtils.getConnectionSubType(connectivityManager)); + + shadowConnectivityManager.setActiveNetworkInfo( + ShadowNetworkInfo.newInstance(NetworkInfo.DetailedState.CONNECTED, ConnectivityManager.TYPE_WIMAX, 0, true, true) + ); + assertEquals(ConnectionSubType.WIMAX, NetworkUtils.getConnectionSubType(connectivityManager)); + + // Unknown mobile + shadowConnectivityManager.setActiveNetworkInfo( + ShadowNetworkInfo.newInstance(NetworkInfo.DetailedState.CONNECTED, ConnectivityManager.TYPE_MOBILE, TelephonyManager.NETWORK_TYPE_UNKNOWN, true, true) + ); + assertEquals(ConnectionSubType.UNKNOWN, NetworkUtils.getConnectionSubType(connectivityManager)); + + // 2G mobile types + int[] cell2gTypes = new int[] { + TelephonyManager.NETWORK_TYPE_GPRS, + TelephonyManager.NETWORK_TYPE_EDGE, + TelephonyManager.NETWORK_TYPE_CDMA, + TelephonyManager.NETWORK_TYPE_1xRTT, + TelephonyManager.NETWORK_TYPE_IDEN + }; + for (int i = 0; i < cell2gTypes.length; i++) { + shadowConnectivityManager.setActiveNetworkInfo( + ShadowNetworkInfo.newInstance(NetworkInfo.DetailedState.CONNECTED, ConnectivityManager.TYPE_MOBILE, cell2gTypes[i], true, true) + ); + assertEquals(ConnectionSubType.CELL_2G, NetworkUtils.getConnectionSubType(connectivityManager)); + } + + // 3G mobile types + int[] cell3gTypes = new int[] { + TelephonyManager.NETWORK_TYPE_UMTS, + TelephonyManager.NETWORK_TYPE_EVDO_0, + TelephonyManager.NETWORK_TYPE_EVDO_A, + TelephonyManager.NETWORK_TYPE_HSDPA, + TelephonyManager.NETWORK_TYPE_HSUPA, + TelephonyManager.NETWORK_TYPE_HSPA, + TelephonyManager.NETWORK_TYPE_EVDO_B, + TelephonyManager.NETWORK_TYPE_EHRPD, + TelephonyManager.NETWORK_TYPE_HSPAP + }; + for (int i = 0; i < cell3gTypes.length; i++) { + shadowConnectivityManager.setActiveNetworkInfo( + ShadowNetworkInfo.newInstance(NetworkInfo.DetailedState.CONNECTED, ConnectivityManager.TYPE_MOBILE, cell3gTypes[i], true, true) + ); + assertEquals(ConnectionSubType.CELL_3G, NetworkUtils.getConnectionSubType(connectivityManager)); + } + + // 4G mobile type + shadowConnectivityManager.setActiveNetworkInfo( + ShadowNetworkInfo.newInstance(NetworkInfo.DetailedState.CONNECTED, ConnectivityManager.TYPE_MOBILE, TelephonyManager.NETWORK_TYPE_LTE, true, true) + ); + assertEquals(ConnectionSubType.CELL_4G, NetworkUtils.getConnectionSubType(connectivityManager)); + } + + @Test + public void testGetConnectionType() { + shadowConnectivityManager.setActiveNetworkInfo(null); + assertEquals(ConnectionType.NONE, NetworkUtils.getConnectionType(connectivityManager)); + assertEquals(ConnectionType.NONE, NetworkUtils.getConnectionType(null)); + + shadowConnectivityManager.setActiveNetworkInfo( + ShadowNetworkInfo.newInstance(NetworkInfo.DetailedState.CONNECTED, ConnectivityManager.TYPE_VPN, 0, true, true) + ); + assertEquals(ConnectionType.OTHER, NetworkUtils.getConnectionType(connectivityManager)); + + shadowConnectivityManager.setActiveNetworkInfo( + ShadowNetworkInfo.newInstance(NetworkInfo.DetailedState.CONNECTED, ConnectivityManager.TYPE_WIFI, 0, true, true) + ); + assertEquals(ConnectionType.WIFI, NetworkUtils.getConnectionType(connectivityManager)); + + shadowConnectivityManager.setActiveNetworkInfo( + ShadowNetworkInfo.newInstance(NetworkInfo.DetailedState.CONNECTED, ConnectivityManager.TYPE_MOBILE, 0, true, true) + ); + assertEquals(ConnectionType.CELLULAR, NetworkUtils.getConnectionType(connectivityManager)); + + shadowConnectivityManager.setActiveNetworkInfo( + ShadowNetworkInfo.newInstance(NetworkInfo.DetailedState.CONNECTED, ConnectivityManager.TYPE_ETHERNET, 0, true, true) + ); + assertEquals(ConnectionType.ETHERNET, NetworkUtils.getConnectionType(connectivityManager)); + + shadowConnectivityManager.setActiveNetworkInfo( + ShadowNetworkInfo.newInstance(NetworkInfo.DetailedState.CONNECTED, ConnectivityManager.TYPE_BLUETOOTH, 0, true, true) + ); + assertEquals(ConnectionType.BLUETOOTH, NetworkUtils.getConnectionType(connectivityManager)); + + shadowConnectivityManager.setActiveNetworkInfo( + ShadowNetworkInfo.newInstance(NetworkInfo.DetailedState.CONNECTED, ConnectivityManager.TYPE_WIMAX, 0, true, true) + ); + assertEquals(ConnectionType.CELLULAR, NetworkUtils.getConnectionType(connectivityManager)); + } + + @Test + public void testGetNetworkStatus() { + assertEquals(NetworkStatus.UNKNOWN, NetworkUtils.getNetworkStatus(null)); + + shadowConnectivityManager.setActiveNetworkInfo(null); + assertEquals(NetworkStatus.DOWN, NetworkUtils.getNetworkStatus(connectivityManager)); + + shadowConnectivityManager.setActiveNetworkInfo( + ShadowNetworkInfo.newInstance(NetworkInfo.DetailedState.CONNECTING, ConnectivityManager.TYPE_MOBILE, 0, true, false) + ); + assertEquals(NetworkStatus.DOWN, NetworkUtils.getNetworkStatus(connectivityManager)); + + shadowConnectivityManager.setActiveNetworkInfo( + ShadowNetworkInfo.newInstance(NetworkInfo.DetailedState.CONNECTED, ConnectivityManager.TYPE_MOBILE, 0, true, true) + ); + assertEquals(NetworkStatus.UP, NetworkUtils.getNetworkStatus(connectivityManager)); + } +}
\ No newline at end of file diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/util/TestContextUtils.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/util/TestContextUtils.java new file mode 100644 index 000000000..56b69b684 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/util/TestContextUtils.java @@ -0,0 +1,38 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package org.mozilla.gecko.util; + +import android.content.Context; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.robolectric.RuntimeEnvironment; + +import static org.junit.Assert.*; + +/** + * Unit test methods of the ContextUtils class. + */ +@RunWith(TestRunner.class) +public class TestContextUtils { + + private Context context; + + @Before + public void setUp() { + context = RuntimeEnvironment.application; + } + + @Test + public void testGetPackageInstallTimeReturnsReasonableValue() throws Exception { + // At the time of writing, Robolectric's value is 0, which is reasonable. + final long installTime = ContextUtils.getCurrentPackageInfo(context).firstInstallTime; + assertTrue("Package install time is positive", installTime >= 0); + assertTrue("Package install time is less than current time", installTime < System.currentTimeMillis()); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/util/TestDateUtil.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/util/TestDateUtil.java new file mode 100644 index 000000000..a93c81ef0 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/util/TestDateUtil.java @@ -0,0 +1,89 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package org.mozilla.gecko.util; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.TestRunner; + +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.Locale; +import java.util.TimeZone; + +import static org.junit.Assert.assertEquals; + +/** + * Unit tests for date utilities. + */ +@RunWith(TestRunner.class) +public class TestDateUtil { + @Test + public void testGetDateInHTTPFormatGMT() { + final TimeZone gmt = TimeZone.getTimeZone("GMT"); + final GregorianCalendar calendar = new GregorianCalendar(gmt, Locale.US); + calendar.set(2011, Calendar.FEBRUARY, 1, 14, 0, 0); + final String expectedDate = "Tue, 01 Feb 2011 14:00:00 GMT"; + + final String actualDate = DateUtil.getDateInHTTPFormat(calendar.getTime()); + assertEquals("Returned date is expected", expectedDate, actualDate); + } + + @Test + public void testGetDateInHTTPFormatNonGMT() { + final TimeZone kst = TimeZone.getTimeZone("Asia/Seoul"); // no daylight savings time. + final GregorianCalendar calendar = new GregorianCalendar(kst, Locale.US); + calendar.set(2011, Calendar.FEBRUARY, 1, 14, 0, 0); + final String expectedDate = "Tue, 01 Feb 2011 05:00:00 GMT"; + + final String actualDate = DateUtil.getDateInHTTPFormat(calendar.getTime()); + assertEquals("Returned date is expected", expectedDate, actualDate); + } + + @Test + public void testGetTimezoneOffsetInMinutes() { + assertEquals("GMT has no offset", 0, DateUtil.getTimezoneOffsetInMinutes(TimeZone.getTimeZone("GMT"))); + + // We use custom timezones because they don't have daylight savings time. + assertEquals("Offset for GMT-8 is correct", + -480, DateUtil.getTimezoneOffsetInMinutes(TimeZone.getTimeZone("GMT-8"))); + assertEquals("Offset for GMT+12:45 is correct", + 765, DateUtil.getTimezoneOffsetInMinutes(TimeZone.getTimeZone("GMT+12:45"))); + + // We use a non-custom timezone without DST. + assertEquals("Offset for KST is correct", + 540, DateUtil.getTimezoneOffsetInMinutes(TimeZone.getTimeZone("Asia/Seoul"))); + } + + @Test + public void testGetTimezoneOffsetInMinutesForGivenDateNoDaylightSavingsTime() { + final TimeZone kst = TimeZone.getTimeZone("Asia/Seoul"); + final Calendar[] calendars = + new Calendar[] { getCalendarForMonth(Calendar.DECEMBER), getCalendarForMonth(Calendar.AUGUST) }; + for (final Calendar cal : calendars) { + cal.setTimeZone(kst); + assertEquals("Offset for KST does not change with daylight savings time", + 540, DateUtil.getTimezoneOffsetInMinutesForGivenDate(cal)); + } + } + + @Test + public void testGetTimezoneOffsetInMinutesForGivenDateDaylightSavingsTime() { + final TimeZone pacificTimeZone = TimeZone.getTimeZone("America/Los_Angeles"); + final Calendar pstCalendar = getCalendarForMonth(Calendar.DECEMBER); + final Calendar pdtCalendar = getCalendarForMonth(Calendar.AUGUST); + pstCalendar.setTimeZone(pacificTimeZone); + pdtCalendar.setTimeZone(pacificTimeZone); + assertEquals("Offset for PST is correct", -480, DateUtil.getTimezoneOffsetInMinutesForGivenDate(pstCalendar)); + assertEquals("Offset for PDT is correct", -420, DateUtil.getTimezoneOffsetInMinutesForGivenDate(pdtCalendar)); + + } + + private Calendar getCalendarForMonth(final int month) { + return new GregorianCalendar(2000, month, 1); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/util/TestFileUtils.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/util/TestFileUtils.java new file mode 100644 index 000000000..88fa7307d --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/util/TestFileUtils.java @@ -0,0 +1,339 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package org.mozilla.gecko.util; + +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.util.FileUtils.FileLastModifiedComparator; +import org.mozilla.gecko.util.FileUtils.FilenameRegexFilter; +import org.mozilla.gecko.util.FileUtils.FilenameWhitelistFilter; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.nio.charset.Charset; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import java.util.regex.Pattern; + +import static junit.framework.Assert.*; +import static org.mockito.Mockito.*; + +/** + * Tests the utilities in {@link FileUtils}. + */ +@RunWith(TestRunner.class) +public class TestFileUtils { + + private static final Charset CHARSET = Charset.forName("UTF-8"); + + @Rule + public TemporaryFolder tempDir = new TemporaryFolder(); + public File testFile; + public File nonExistentFile; + + @Before + public void setUp() throws Exception { + testFile = tempDir.newFile(); + nonExistentFile = new File(tempDir.getRoot(), "non-existent-file"); + } + + @Test + public void testReadJSONObjectFromFile() throws Exception { + final JSONObject expected = new JSONObject("{\"str\": \"some str\"}"); + writeStringToFile(testFile, expected.toString()); + + final JSONObject actual = FileUtils.readJSONObjectFromFile(testFile); + assertEquals("JSON contains expected str", expected.getString("str"), actual.getString("str")); + } + + @Test(expected=IOException.class) + public void testReadJSONObjectFromFileEmptyFile() throws Exception { + assertEquals("Test file is empty", 0, testFile.length()); + FileUtils.readJSONObjectFromFile(testFile); // expected to throw + } + + @Test(expected=JSONException.class) + public void testReadJSONObjectFromFileInvalidJSON() throws Exception { + writeStringToFile(testFile, "not a json str"); + FileUtils.readJSONObjectFromFile(testFile); // expected to throw + } + + @Test + public void testReadStringFromFileReadsData() throws Exception { + final String expected = "String to write contains hard characters: !\n \\s..\"'\u00f1"; + writeStringToFile(testFile, expected); + + final String actual = FileUtils.readStringFromFile(testFile); + assertEquals("Read content matches written content", expected, actual); + } + + @Test + public void testReadStringFromFileEmptyFile() throws Exception { + assertEquals("Test file is empty", 0, testFile.length()); + + final String actual = FileUtils.readStringFromFile(testFile); + assertEquals("Read content is empty", "", actual); + } + + @Test(expected=FileNotFoundException.class) + public void testReadStringFromNonExistentFile() throws Exception { + assertFalse("File does not exist", nonExistentFile.exists()); + FileUtils.readStringFromFile(nonExistentFile); + } + + @Test + public void testReadStringFromInputStreamAndCloseStreamBufferLenIsFileLen() throws Exception { + final String expected = "String to write contains hard characters: !\n \\s..\"'\u00f1"; + writeStringToFile(testFile, expected); + + final FileInputStream stream = new FileInputStream(testFile); + final String actual = FileUtils.readStringFromInputStreamAndCloseStream(stream, expected.length()); + assertEquals("Read content matches written content", expected, actual); + } + + @Test + public void testReadStringFromInputStreamAndCloseStreamBufferLenIsBiggerThanFile() throws Exception { + final String expected = "aoeuhtns"; + writeStringToFile(testFile, expected); + + final FileInputStream stream = new FileInputStream(testFile); + final String actual = FileUtils.readStringFromInputStreamAndCloseStream(stream, expected.length() + 1024); + assertEquals("Read content matches written content", expected, actual); + } + + @Test + public void testReadStringFromInputStreamAndCloseStreamBufferLenIsSmallerThanFile() throws Exception { + final String expected = "aoeuhtns aoeusth aoeusth aoeusnth aoeusth aoeusnth aoesuth"; + writeStringToFile(testFile, expected); + + final FileInputStream stream = new FileInputStream(testFile); + final String actual = FileUtils.readStringFromInputStreamAndCloseStream(stream, 8); + assertEquals("Read content matches written content", expected, actual); + } + + @Test(expected=IllegalArgumentException.class) + public void testReadStringFromInputStreamAndCloseStreamBufferLenIsZero() throws Exception { + final String expected = "aoeuhtns aoeusth aoeusth aoeusnth aoeusth aoeusnth aoesuth"; + writeStringToFile(testFile, expected); + + final FileInputStream stream = new FileInputStream(testFile); + FileUtils.readStringFromInputStreamAndCloseStream(stream, 0); // expected to throw. + } + + @Test + public void testReadStringFromInputStreamAndCloseStreamIsEmptyStream() throws Exception { + assertTrue("Test file exists", testFile.exists()); + assertEquals("Test file is empty", 0, testFile.length()); + + final FileInputStream stream = new FileInputStream(testFile); + final String actual = FileUtils.readStringFromInputStreamAndCloseStream(stream, 8); + assertEquals("Read content from stream is empty", "", actual); + } + + @Test(expected=IOException.class) + public void testReadStringFromInputStreamAndCloseStreamClosesStream() throws Exception { + final String expected = "String to write contains hard characters: !\n \\s..\"'\u00f1"; + writeStringToFile(testFile, expected); + + final FileInputStream stream = new FileInputStream(testFile); + try { + stream.read(); // should not throw because stream is open. + FileUtils.readStringFromInputStreamAndCloseStream(stream, expected.length()); + } catch (final IOException e) { + fail("Did not expect method to throw when writing file: " + e); + } + + stream.read(); // expected to throw because stream is closed. + } + + @Test + public void testWriteStringToOutputStreamAndCloseStreamWritesData() throws Exception { + final String expected = "A string with some data in it! \u00f1 \n"; + final FileOutputStream fos = new FileOutputStream(testFile, false); + FileUtils.writeStringToOutputStreamAndCloseStream(fos, expected); + + assertTrue("Written file exists", testFile.exists()); + assertEquals("Read data equals written data", expected, readStringFromFile(testFile, expected.length())); + } + + @Test(expected=IOException.class) + public void testWriteStringToOutputStreamAndCloseStreamClosesStream() throws Exception { + final FileOutputStream fos = new FileOutputStream(testFile, false); + try { + fos.write('c'); // should not throw because stream is open. + FileUtils.writeStringToOutputStreamAndCloseStream(fos, "some string with data"); + } catch (final IOException e) { + fail("Did not expect method to throw when writing file: " + e); + } + + fos.write('c'); // expected to throw because stream is closed. + } + + /** + * The Writer we wrap our stream in can throw in .close(), preventing the underlying stream from closing. + * I added code to prevent ensure we close if the writer .close() throws. + * + * I wrote this test to test that code, however, we'd have to mock the writer [1] and that isn't straight-forward. + * I left this test around because it's a good test of other code. + * + * [1]: We thought we could mock FileOutputStream.flush but it's only flushed if the Writer thinks it should be + * flushed. We can write directly to the Stream, but that doesn't change the Writer state and doesn't affect whether + * it thinks it should be flushed. + */ + @Test(expected=IOException.class) + public void testWriteStringToOutputStreamAndCloseStreamClosesStreamIfWriterThrows() throws Exception { + final FileOutputStream fos = mock(FileOutputStream.class); + doThrow(IOException.class).when(fos).write(any(byte[].class), anyInt(), anyInt()); + doThrow(IOException.class).when(fos).write(anyInt()); + doThrow(IOException.class).when(fos).write(any(byte[].class)); + + boolean exceptionCaught = false; + try { + FileUtils.writeStringToOutputStreamAndCloseStream(fos, "some string with data"); + } catch (final IOException e) { + exceptionCaught = true; + } + assertTrue("Exception caught during tested method", exceptionCaught); // not strictly necessary but documents assumptions + + fos.write('c'); // expected to throw because stream is closed. + } + + @Test + public void testWriteStringToFile() throws Exception { + final String expected = "String to write contains hard characters: !\n \\s..\"'\u00f1"; + FileUtils.writeStringToFile(testFile, expected); + + assertTrue("Written file exists", testFile.exists()); + assertEquals("Read data equals written data", expected, readStringFromFile(testFile, expected.length())); + } + + @Test + public void testWriteStringToFileEmptyString() throws Exception { + final String expected = ""; + FileUtils.writeStringToFile(testFile, expected); + + assertTrue("Written file exists", testFile.exists()); + assertEquals("Written file is empty", 0, testFile.length()); + assertEquals("Read data equals written (empty) data", expected, readStringFromFile(testFile, expected.length())); + } + + @Test + public void testWriteStringToFileCreatesNewFile() throws Exception { + final String expected = "some str to write"; + assertFalse("Non existent file does not exist", nonExistentFile.exists()); + FileUtils.writeStringToFile(nonExistentFile, expected); // expected to create file + + assertTrue("Written file was created", nonExistentFile.exists()); + assertEquals("Read data equals written data", expected, readStringFromFile(nonExistentFile, (int) nonExistentFile.length())); + } + + @Test + public void testWriteStringToFileOverwritesFile() throws Exception { + writeStringToFile(testFile, "data"); + + final String expected = "some str to write"; + FileUtils.writeStringToFile(testFile, expected); + + assertTrue("Written file was created", testFile.exists()); + assertEquals("Read data equals written data", expected, readStringFromFile(testFile, (int) testFile.length())); + } + + @Test + public void testWriteJSONObjectToFile() throws Exception { + final JSONObject expected = new JSONObject() + .put("int", 1) + .put("str", "1") + .put("bool", true) + .put("null", JSONObject.NULL) + .put("raw null", null); + FileUtils.writeJSONObjectToFile(testFile, expected); + + assertTrue("Written file exists", testFile.exists()); + + // JSONObject.equals compares references so we have to assert each key individually. >:( + final JSONObject actual = new JSONObject(readStringFromFile(testFile, (int) testFile.length())); + assertEquals(1, actual.getInt("int")); + assertEquals("1", actual.getString("str")); + assertEquals(true, actual.getBoolean("bool")); + assertEquals(JSONObject.NULL, actual.get("null")); + assertFalse(actual.has("raw null")); + } + + // Since the read methods may not be tested yet. + private static String readStringFromFile(final File file, final int bufferLen) throws IOException { + final char[] buffer = new char[bufferLen]; + try (InputStreamReader reader = new InputStreamReader(new FileInputStream(file), Charset.forName("UTF-8"))) { + reader.read(buffer, 0, buffer.length); + } + return new String(buffer); + } + + // Since the write methods may not be tested yet. + private static void writeStringToFile(final File file, final String str) throws IOException { + try (OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(file, false), CHARSET)) { + writer.write(str); + } + assertTrue("Written file from helper method exists", file.exists()); + } + + @Test + public void testFilenameWhitelistFilter() { + final String[] expectedToAccept = new String[] { "one", "two", "three" }; + final Set<String> whitelist = new HashSet<>(Arrays.asList(expectedToAccept)); + final FilenameWhitelistFilter testFilter = new FilenameWhitelistFilter(whitelist); + for (final String str : expectedToAccept) { + assertTrue("Filename, " + str + ", in whitelist is accepted", testFilter.accept(testFile, str)); + } + + final String[] notExpectedToAccept = new String[] { "not-in-whitelist", "meh", "whatever" }; + for (final String str : notExpectedToAccept) { + assertFalse("Filename, " + str + ", not in whitelist is not accepted", testFilter.accept(testFile, str)); + } + } + + @Test + public void testFilenameRegexFilter() { + final Pattern pattern = Pattern.compile("[a-z]{1,6}"); + final FilenameRegexFilter testFilter = new FilenameRegexFilter(pattern); + final String[] expectedToAccept = new String[] { "duckie", "goes", "quack" }; + for (final String str : expectedToAccept) { + assertTrue("Filename, " + str + ", matching regex expected to accept", testFilter.accept(testFile, str)); + } + + final String[] notExpectedToAccept = new String[] { "DUCKIE", "1337", "2fast" }; + for (final String str : notExpectedToAccept) { + assertFalse("Filename, " + str + ", not matching regex not expected to accept", testFilter.accept(testFile, str)); + } + } + + @Test + public void testFileLastModifiedComparator() { + final FileLastModifiedComparator testComparator = new FileLastModifiedComparator(); + final File oldFile = mock(File.class); + final File newFile = mock(File.class); + final File equallyNewFile = mock(File.class); + when(oldFile.lastModified()).thenReturn(10L); + when(newFile.lastModified()).thenReturn(100L); + when(equallyNewFile.lastModified()).thenReturn(100L); + + assertTrue("Old file is less than new file", testComparator.compare(oldFile, newFile) < 0); + assertTrue("New file is greater than old file", testComparator.compare(newFile, oldFile) > 0); + assertTrue("New files are equal", testComparator.compare(newFile, equallyNewFile) == 0); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/util/TestIntentUtils.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/util/TestIntentUtils.java new file mode 100644 index 000000000..186821451 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/util/TestIntentUtils.java @@ -0,0 +1,73 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package org.mozilla.gecko.util; + +import android.content.Intent; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.mozilla.gecko.mozglue.SafeIntent; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.*; + +/** + * Tests for the Intent utilities. + */ +@RunWith(TestRunner.class) +public class TestIntentUtils { + + private static final Map<String, String> TEST_ENV_VAR_MAP; + static { + final HashMap<String, String> tempMap = new HashMap<>(); + tempMap.put("ZERO", "0"); + tempMap.put("ONE", "1"); + tempMap.put("STRING", "TEXT"); + tempMap.put("L_WHITESPACE", " LEFT"); + tempMap.put("R_WHITESPACE", "RIGHT "); + tempMap.put("ALL_WHITESPACE", " ALL "); + tempMap.put("WHITESPACE_IN_VALUE", "IN THE MIDDLE"); + tempMap.put("WHITESPACE IN KEY", "IS_PROBABLY_NOT_VALID_ANYWAY"); + tempMap.put("BLANK_VAL", ""); + TEST_ENV_VAR_MAP = Collections.unmodifiableMap(tempMap); + } + + private Intent testIntent; + + @Before + public void setUp() throws Exception { + testIntent = getIntentWithTestData(); + } + + private static Intent getIntentWithTestData() { + final Intent out = new Intent(Intent.ACTION_VIEW); + int i = 0; + for (final String key : TEST_ENV_VAR_MAP.keySet()) { + final String value = key + "=" + TEST_ENV_VAR_MAP.get(key); + out.putExtra("env" + i, value); + i += 1; + } + return out; + } + + @Test + public void testGetEnvVarMap() throws Exception { + final HashMap<String, String> actual = IntentUtils.getEnvVarMap(new SafeIntent(testIntent)); + for (final String actualEnvVarName : actual.keySet()) { + assertTrue("Actual key exists in test data: " + actualEnvVarName, + TEST_ENV_VAR_MAP.containsKey(actualEnvVarName)); + + final String expectedValue = TEST_ENV_VAR_MAP.get(actualEnvVarName); + final String actualValue = actual.get(actualEnvVarName); + assertEquals("Actual env var value matches test data", expectedValue, actualValue); + } + } +}
\ No newline at end of file diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/util/TestStringUtils.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/util/TestStringUtils.java new file mode 100644 index 000000000..ee0a705c7 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/util/TestStringUtils.java @@ -0,0 +1,122 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.util; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.TestRunner; + +import java.util.Arrays; +import java.util.Collections; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +@RunWith(TestRunner.class) +public class TestStringUtils { + @Test + public void testIsHttpOrHttps() { + // No value + assertFalse(StringUtils.isHttpOrHttps(null)); + assertFalse(StringUtils.isHttpOrHttps("")); + + // Garbage + assertFalse(StringUtils.isHttpOrHttps("lksdjflasuf")); + + // URLs with http/https + assertTrue(StringUtils.isHttpOrHttps("https://www.google.com")); + assertTrue(StringUtils.isHttpOrHttps("http://www.facebook.com")); + assertTrue(StringUtils.isHttpOrHttps("https://mozilla.org/en-US/firefox/products/")); + + // IP addresses + assertTrue(StringUtils.isHttpOrHttps("https://192.168.0.1")); + assertTrue(StringUtils.isHttpOrHttps("http://63.245.215.20/en-US/firefox/products")); + + // Other protocols + assertFalse(StringUtils.isHttpOrHttps("ftp://people.mozilla.org")); + assertFalse(StringUtils.isHttpOrHttps("javascript:window.google.com")); + assertFalse(StringUtils.isHttpOrHttps("tel://1234567890")); + + // No scheme + assertFalse(StringUtils.isHttpOrHttps("google.com")); + assertFalse(StringUtils.isHttpOrHttps("git@github.com:mozilla/gecko-dev.git")); + } + + @Test + public void testStripRef() { + assertEquals(StringUtils.stripRef(null), null); + assertEquals(StringUtils.stripRef(""), ""); + + assertEquals(StringUtils.stripRef("??AAABBBCCC"), "??AAABBBCCC"); + assertEquals(StringUtils.stripRef("https://mozilla.org"), "https://mozilla.org"); + assertEquals(StringUtils.stripRef("https://mozilla.org#BBBB"), "https://mozilla.org"); + assertEquals(StringUtils.stripRef("https://mozilla.org/#BBBB"), "https://mozilla.org/"); + } + + @Test + public void testStripScheme() { + assertEquals("mozilla.org", StringUtils.stripScheme("http://mozilla.org")); + assertEquals("mozilla.org", StringUtils.stripScheme("http://mozilla.org/")); + assertEquals("https://mozilla.org", StringUtils.stripScheme("https://mozilla.org")); + assertEquals("https://mozilla.org", StringUtils.stripScheme("https://mozilla.org/")); + assertEquals("mozilla.org", StringUtils.stripScheme("https://mozilla.org/", StringUtils.UrlFlags.STRIP_HTTPS)); + assertEquals("mozilla.org", StringUtils.stripScheme("https://mozilla.org", StringUtils.UrlFlags.STRIP_HTTPS)); + assertEquals("", StringUtils.stripScheme("http://")); + assertEquals("", StringUtils.stripScheme("https://", StringUtils.UrlFlags.STRIP_HTTPS)); + // This edge case is not handled properly yet +// assertEquals(StringUtils.stripScheme("https://"), ""); + assertEquals(null, StringUtils.stripScheme(null)); + } + + @Test + public void testIsRTL() { + assertFalse(StringUtils.isRTL("mozilla.org")); + assertFalse(StringUtils.isRTL("something.عربي")); + + assertTrue(StringUtils.isRTL("عربي")); + assertTrue(StringUtils.isRTL("عربي.org")); + + // Text with LTR mark + assertFalse(StringUtils.isRTL("\u200EHello")); + assertFalse(StringUtils.isRTL("\u200Eعربي")); + } + + @Test + public void testForceLTR() { + assertFalse(StringUtils.isRTL(StringUtils.forceLTR("عربي"))); + assertFalse(StringUtils.isRTL(StringUtils.forceLTR("عربي.org"))); + + // Strings that are already LTR are not modified + final String someLtrString = "HelloWorld"; + assertEquals(someLtrString, StringUtils.forceLTR(someLtrString)); + + // We add the LTR mark only once + final String someRtlString = "عربي"; + assertEquals(4, someRtlString.length()); + final String forcedLtrString = StringUtils.forceLTR(someRtlString); + assertEquals(5, forcedLtrString.length()); + final String forcedAgainLtrString = StringUtils.forceLTR(forcedLtrString); + assertEquals(5, forcedAgainLtrString.length()); + } + + @Test + public void testJoin() { + assertEquals("", StringUtils.join("", Collections.<String>emptyList())); + assertEquals("", StringUtils.join("-", Collections.<String>emptyList())); + assertEquals("", StringUtils.join("", Collections.singletonList(""))); + assertEquals("", StringUtils.join(".", Collections.singletonList(""))); + + assertEquals("192.168.0.1", StringUtils.join(".", Arrays.asList("192", "168", "0", "1"))); + assertEquals("www.mozilla.org", StringUtils.join(".", Arrays.asList("www", "mozilla", "org"))); + + assertEquals("hello", StringUtils.join("", Collections.singletonList("hello"))); + assertEquals("helloworld", StringUtils.join("", Arrays.asList("hello", "world"))); + assertEquals("hello world", StringUtils.join(" ", Arrays.asList("hello", "world"))); + + assertEquals("m::o::z::i::l::l::a", StringUtils.join("::", Arrays.asList("m", "o", "z", "i", "l", "l", "a"))); + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/util/TestUUIDUtil.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/util/TestUUIDUtil.java new file mode 100644 index 000000000..732dd21b9 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/util/TestUUIDUtil.java @@ -0,0 +1,51 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package org.mozilla.gecko.util; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.TestRunner; + +import static org.junit.Assert.*; + +/** + * Tests for uuid utils. + */ +@RunWith(TestRunner.class) +public class TestUUIDUtil { + private static final String[] validUUIDs = { + "904cd9f8-af63-4525-8ce0-b9127e5364fa", + "8d584bd2-00ea-4043-a617-ed4ce7018ed0", + "3abad327-2669-4f68-b9ef-7ace8c5314d6", + }; + + private static final String[] invalidUUIDs = { + "its-not-a-uuid-mate", + "904cd9f8-af63-4525-8ce0-b9127e5364falol", + "904cd9f8-af63-4525-8ce0-b9127e5364f", + }; + + @Test + public void testUUIDRegex() { + for (final String uuid : validUUIDs) { + assertTrue("Valid UUID matches UUID-regex", uuid.matches(UUIDUtil.UUID_REGEX)); + } + for (final String uuid : invalidUUIDs) { + assertFalse("Invalid UUID does not match UUID-regex", uuid.matches(UUIDUtil.UUID_REGEX)); + } + } + + @Test + public void testUUIDPattern() { + for (final String uuid : validUUIDs) { + assertTrue("Valid UUID matches UUID-regex", UUIDUtil.UUID_PATTERN.matcher(uuid).matches()); + } + for (final String uuid : invalidUUIDs) { + assertFalse("Invalid UUID does not match UUID-regex", UUIDUtil.UUID_PATTERN.matcher(uuid).matches()); + } + } +} diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/util/publicsuffix/TestPublicSuffix.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/util/publicsuffix/TestPublicSuffix.java new file mode 100644 index 000000000..e47d361c0 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/util/publicsuffix/TestPublicSuffix.java @@ -0,0 +1,62 @@ +package org.mozilla.gecko.util.publicsuffix; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.background.testhelpers.TestRunner; +import org.robolectric.RuntimeEnvironment; + +@RunWith(TestRunner.class) +public class TestPublicSuffix { + @Test + public void testStripPublicSuffix() { + // Test empty value + Assert.assertEquals("", + PublicSuffix.stripPublicSuffix(RuntimeEnvironment.application, "")); + + // Test domains with public suffix + Assert.assertEquals("www.mozilla", + PublicSuffix.stripPublicSuffix(RuntimeEnvironment.application, "www.mozilla.org")); + Assert.assertEquals("www.google", + PublicSuffix.stripPublicSuffix(RuntimeEnvironment.application, "www.google.com")); + Assert.assertEquals("foobar", + PublicSuffix.stripPublicSuffix(RuntimeEnvironment.application, "foobar.blogspot.com")); + Assert.assertEquals("independent", + PublicSuffix.stripPublicSuffix(RuntimeEnvironment.application, "independent.co.uk")); + Assert.assertEquals("biz", + PublicSuffix.stripPublicSuffix(RuntimeEnvironment.application, "biz.com.ua")); + Assert.assertEquals("example", + PublicSuffix.stripPublicSuffix(RuntimeEnvironment.application, "example.org")); + Assert.assertEquals("example", + PublicSuffix.stripPublicSuffix(RuntimeEnvironment.application, "example.pvt.k12.ma.us")); + + // Test domain without public suffix + Assert.assertEquals("localhost", + PublicSuffix.stripPublicSuffix(RuntimeEnvironment.application, "localhost")); + Assert.assertEquals("firefox.mozilla", + PublicSuffix.stripPublicSuffix(RuntimeEnvironment.application, "firefox.mozilla")); + + // IDN domains + Assert.assertEquals("ουτοπία.δπθ", + PublicSuffix.stripPublicSuffix(RuntimeEnvironment.application, "ουτοπία.δπθ.gr")); + Assert.assertEquals("a网络A", + PublicSuffix.stripPublicSuffix(RuntimeEnvironment.application, "a网络A.网络.Cn")); + + // Other non-domain values + Assert.assertEquals("192.168.0.1", + PublicSuffix.stripPublicSuffix(RuntimeEnvironment.application, "192.168.0.1")); + Assert.assertEquals("asdflkj9uahsd", + PublicSuffix.stripPublicSuffix(RuntimeEnvironment.application, "asdflkj9uahsd")); + + // Other trailing and other types of dots + Assert.assertEquals("www.mozilla。home.example", + PublicSuffix.stripPublicSuffix(RuntimeEnvironment.application, "www.mozilla。home.example。org")); + Assert.assertEquals("example", + PublicSuffix.stripPublicSuffix(RuntimeEnvironment.application, "example.org")); + } + + @Test(expected = NullPointerException.class) + public void testStripPublicSuffixThrowsException() { + PublicSuffix.stripPublicSuffix(RuntimeEnvironment.application, null); + } +} |