/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.android.sync.net.test;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.io.IOException;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import junit.framework.AssertionFailedError;
import org.json.simple.parser.ParseException;
import org.junit.Before;
import org.junit.Test;
import org.mozilla.android.sync.test.helpers.HTTPServerTestHelper;
import org.mozilla.android.sync.test.helpers.MockGlobalSession;
import org.mozilla.android.sync.test.helpers.MockGlobalSessionCallback;
import org.mozilla.android.sync.test.helpers.MockPrefsGlobalSession;
import org.mozilla.android.sync.test.helpers.MockResourceDelegate;
import org.mozilla.android.sync.test.helpers.MockServer;
import org.mozilla.android.sync.test.helpers.MockServerSyncStage;
import org.mozilla.android.sync.test.helpers.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.delegates.GlobalSessionCallback;
import org.mozilla.gecko.sync.net.BaseResource;
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.FetchInfoCollectionsStage;
import org.mozilla.gecko.sync.stage.GlobalSyncStage;
import org.mozilla.gecko.sync.stage.GlobalSyncStage.Stage;
import org.mozilla.gecko.sync.stage.NoSuchStageException;
import org.simpleframework.http.Request;
import org.simpleframework.http.Response;
import ch.boye.httpclientandroidlib.HttpResponse;
import ch.boye.httpclientandroidlib.ProtocolVersion;
import ch.boye.httpclientandroidlib.message.BasicHttpResponse;
import ch.boye.httpclientandroidlib.message.BasicStatusLine;
public class TestGlobalSession {
private int TEST_PORT = HTTPServerTestHelper.getTestPort();
private final String TEST_CLUSTER_URL = "http://localhost:" + TEST_PORT;
private final String TEST_USERNAME = "johndoe";
private final String TEST_PASSWORD = "password";
private final String TEST_SYNC_KEY = "abcdeabcdeabcdeabcdeabcdea";
private final long TEST_BACKOFF_IN_SECONDS = 2401;
public static WaitHelper getTestWaiter() {
return WaitHelper.getTestWaiter();
}
@Test
public void testGetSyncStagesBy() throws SyncConfigurationException, IllegalArgumentException, NonObjectJSONException, IOException, ParseException, CryptoException, NoSuchStageException {
final MockGlobalSessionCallback callback = new MockGlobalSessionCallback();
GlobalSession s = new MockPrefsGlobalSession(SyncConfiguration.DEFAULT_USER_API, TEST_CLUSTER_URL,
TEST_USERNAME, TEST_PASSWORD, null,
new KeyBundle(TEST_USERNAME, TEST_SYNC_KEY),
callback, /* context */ null, 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)));
}
/**
* A mock GlobalSession that fakes a 503 on info/collections and
* sets X-Weave-Backoff header to the specified number of seconds.
*/
public class MockBackoffGlobalSession extends MockGlobalSession {
public long backoffInSeconds = -1;
public MockBackoffGlobalSession(long backoffInSeconds,
String clusterURL, String username, String password,
KeyBundle syncKeyBundle, GlobalSessionCallback callback)
throws SyncConfigurationException, IllegalArgumentException, IOException, ParseException, NonObjectJSONException {
super(clusterURL, username, password, syncKeyBundle, callback);
this.backoffInSeconds = backoffInSeconds;
}
public class MockBackoffFetchInfoCollectionsStage extends FetchInfoCollectionsStage {
public MockBackoffFetchInfoCollectionsStage(GlobalSession session) {
super(session);
}
@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(backoffInSeconds)); // Backoff given in seconds.
session.handleHTTPError(new SyncStorageResponse(response), "Failure fetching info/collections.");
}
}
@Override
protected void prepareStages() {
super.prepareStages();
HashMap<Stage, GlobalSyncStage> stages = new HashMap<Stage, GlobalSyncStage>(this.stages);
stages.put(Stage.fetchInfoCollections, new MockBackoffFetchInfoCollectionsStage(this));
this.stages = Collections.unmodifiableMap(stages);
}
}
/**
* Test that handleHTTPError does in fact backoff.
*/
@Test
public void testBackoffCalledByHandleHTTPError() {
try {
final MockGlobalSessionCallback callback = new MockGlobalSessionCallback();
final GlobalSession session = new MockGlobalSession(TEST_CLUSTER_URL, TEST_USERNAME, TEST_PASSWORD,
new KeyBundle(TEST_USERNAME, TEST_SYNC_KEY), callback);
final HttpResponse response = new BasicHttpResponse(
new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 503, "Illegal method/protocol"));
response.addHeader("X-Weave-Backoff", Long.toString(TEST_BACKOFF_IN_SECONDS)); // Backoff given in seconds.
getTestWaiter().performWait(WaitHelper.onThreadRunnable(new Runnable() {
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();
final GlobalSession session = new MockGlobalSession(TEST_CLUSTER_URL, TEST_USERNAME, TEST_PASSWORD,
new KeyBundle(TEST_USERNAME, TEST_SYNC_KEY), callback);
getTestWaiter().performWait(WaitHelper.onThreadRunnable(new Runnable() {
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();
final GlobalSession session = new MockBackoffGlobalSession(TEST_BACKOFF_IN_SECONDS, TEST_CLUSTER_URL, TEST_USERNAME, TEST_PASSWORD,
new KeyBundle(TEST_USERNAME, TEST_SYNC_KEY), callback);
getTestWaiter().performWait(WaitHelper.onThreadRunnable(new Runnable() {
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() {
// We should have installed our HTTP response observer before starting the sync.
assertNotNull(BaseResource.getHttpResponseObserver());
final WaitHelper innerWaitHelper = new WaitHelper();
innerWaitHelper.performWait(new Runnable() {
@Override
public void run() {
try {
final BaseResource r = new BaseResource(TEST_CLUSTER_URL);
r.delegate = new MockResourceDelegate(innerWaitHelper);
r.get();
} catch (URISyntaxException e) {
innerWaitHelper.performNotify(e);
}
}
});
}
public MockGlobalSessionCallback doTestSuccess(final boolean stageShouldBackoff, final boolean stageShouldAdvance) throws SyncConfigurationException, IllegalArgumentException, NonObjectJSONException, IOException, ParseException, CryptoException {
MockServer server = new MockServer() {
@Override
public void handle(Request request, Response response) {
if (stageShouldBackoff) {
response.set("X-Weave-Backoff", Long.toString(TEST_BACKOFF_IN_SECONDS));
}
super.handle(request, response);
}
};
// Hang on to this externally so we can poke at the stage.
final HashMap<Stage, GlobalSyncStage> stagesToRun = new HashMap<Stage, GlobalSyncStage>();
final MockGlobalSessionCallback callback = new MockGlobalSessionCallback();
final GlobalSession session = new MockGlobalSession(TEST_CLUSTER_URL, TEST_USERNAME, TEST_PASSWORD,
new KeyBundle(TEST_USERNAME, TEST_SYNC_KEY), callback) {
@Override
protected void prepareStages() {
super.prepareStages();
stagesToRun.putAll(this.stages);
this.stages = stagesToRun;
}
};
final MockServerSyncStage stage = new MockServerSyncStage(session) {
@Override
public void execute() {
doRequest();
if (stageShouldAdvance) {
session.advance();
return;
}
session.abort(null, "Stage intentionally failed.");
}
};
stagesToRun.put(Stage.syncBookmarks, stage);
data.startHTTPServer(server);
WaitHelper.getTestWaiter().performWait(WaitHelper.onThreadRunnable(new Runnable() {
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.
assertNull(BaseResource.getHttpResponseObserver());
return callback;
}
@Test
public void testOnSuccessBackoffAdvanced() throws SyncConfigurationException,
IllegalArgumentException, NonObjectJSONException, IOException,
ParseException, CryptoException {
MockGlobalSessionCallback callback = doTestSuccess(true, true);
assertTrue(callback.calledError); // TODO: this should be calledAborted.
assertTrue(callback.calledRequestBackoff);
assertEquals(1000 * TEST_BACKOFF_IN_SECONDS, callback.weaveBackoff);
}
@Test
public void testOnSuccessBackoffAborted() throws SyncConfigurationException,
IllegalArgumentException, NonObjectJSONException, IOException,
ParseException, CryptoException {
MockGlobalSessionCallback callback = doTestSuccess(true, false);
assertTrue(callback.calledError); // TODO: this should be calledAborted.
assertTrue(callback.calledRequestBackoff);
assertEquals(1000 * TEST_BACKOFF_IN_SECONDS, callback.weaveBackoff);
}
@Test
public void testOnSuccessNoBackoffAdvanced() throws SyncConfigurationException,
IllegalArgumentException, NonObjectJSONException, IOException,
ParseException, CryptoException {
MockGlobalSessionCallback callback = doTestSuccess(false, true);
assertTrue(callback.calledSuccess);
assertFalse(callback.calledRequestBackoff);
}
@Test
public void testOnSuccessNoBackoffAborted() throws SyncConfigurationException,
IllegalArgumentException, NonObjectJSONException, IOException,
ParseException, CryptoException {
MockGlobalSessionCallback callback = doTestSuccess(false, false);
assertTrue(callback.calledError); // TODO: this should be calledAborted.
assertFalse(callback.calledRequestBackoff);
}
@Test
public void testGenerateNewMetaGlobalNonePersisted() throws Exception {
final MockGlobalSessionCallback callback = new MockGlobalSessionCallback();
final GlobalSession session = new MockPrefsGlobalSession(SyncConfiguration.DEFAULT_USER_API, TEST_CLUSTER_URL, TEST_USERNAME, TEST_PASSWORD, null,
new KeyBundle(TEST_USERNAME, TEST_SYNC_KEY), callback, null, 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 = new MockPrefsGlobalSession(SyncConfiguration.DEFAULT_USER_API, TEST_CLUSTER_URL, TEST_USERNAME, TEST_PASSWORD, null,
new KeyBundle(TEST_USERNAME, TEST_SYNC_KEY), callback, null, 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 = new MockPrefsGlobalSession(SyncConfiguration.DEFAULT_USER_API, TEST_CLUSTER_URL, TEST_USERNAME, TEST_PASSWORD, null,
new KeyBundle(TEST_USERNAME, TEST_SYNC_KEY), callback, null, null, null);
session.config.metaGlobal = session.generateNewMetaGlobal();
session.enginesToUpdate.clear();
// Set enabledEngines in meta/global, including a "new engine."
String[] origEngines = new String[] { "bookmarks", "clients", "forms", "history", "tabs", "new-engine" };
ExtendedJSONObject origEnginesJSONObject = new ExtendedJSONObject();
for (String engineName : origEngines) {
EngineSettings mockEngineSettings = new EngineSettings(Utils.generateGuid(), Integer.valueOf(0));
origEnginesJSONObject.put(engineName, mockEngineSettings);
}
session.config.metaGlobal.setEngines(origEnginesJSONObject);
// Engines to remove.
String[] toRemove = new String[] { "bookmarks", "tabs" };
for (String name : toRemove) {
session.removeEngineFromMetaGlobal(name);
}
// Engines to add.
String[] toAdd = new String[] { "passwords" };
for (String name : toAdd) {
String syncId = Utils.generateGuid();
session.recordForMetaGlobalUpdate(name, new EngineSettings(syncId, Integer.valueOf(1)));
}
// Update engines.
session.uploadUpdatedMetaGlobal();
// Check resulting enabledEngines.
Set<String> expected = new HashSet<String>();
for (String name : origEngines) {
expected.add(name);
}
for (String name : toRemove) {
expected.remove(name);
}
for (String name : toAdd) {
expected.add(name);
}
assertEquals(expected, session.config.metaGlobal.getEnabledEngineNames());
}
}