/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.sync.stage.test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import java.io.IOException;
import java.net.URI;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import org.json.simple.parser.ParseException;
import org.junit.Before;
import org.junit.Test;
import org.mozilla.android.sync.net.test.TestMetaGlobal;
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.MockServer;
import org.mozilla.android.sync.test.helpers.WaitHelper;
import org.mozilla.gecko.sync.AlreadySyncingException;
import org.mozilla.gecko.sync.CollectionKeys;
import org.mozilla.gecko.sync.CredentialsSource;
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.stage.FetchMetaGlobalStage;
import org.mozilla.gecko.sync.stage.GlobalSyncStage;
import org.mozilla.gecko.sync.stage.GlobalSyncStage.Stage;
import org.simpleframework.http.Request;
import org.simpleframework.http.Response;
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/";
static String TEST_NW_URL = TEST_SERVER + "/1.0/c6o7dvmr2c4ud2fyv6woz2u4zi22bcyd/node/weave"; // GET https://server/pathname/version/username/node/weave
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;
@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(ExtendedJSONObject.parseJSONObject(TEST_INFO_COLLECTIONS_JSON));
syncKeyBundle = new KeyBundle(TEST_USERNAME, TEST_SYNC_KEY);
callback = new MockGlobalSessionCallback();
session = new MockGlobalSession(TEST_CLUSTER_URL, TEST_USERNAME, TEST_PASSWORD,
syncKeyBundle, callback) {
@Override
protected void prepareStages() {
super.prepareStages();
Map<Stage, GlobalSyncStage> stages = new HashMap<Stage, GlobalSyncStage>(this.stages);
stages.put(Stage.fetchMetaGlobal, new FetchMetaGlobalStage(this));
this.stages = stages;
}
@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 CredentialsSource credentials, 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() {
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);
}
/**
* 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));
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);
}
/**
* 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));
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);
}
@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(ParseException.class, callback.calledErrorException.getClass());
}
protected void doFreshStart(MockServer server) {
data.startHTTPServer(server);
WaitHelper.getTestWaiter().performWait(WaitHelper.onThreadRunnable(new Runnable() {
public void run() {
session.freshStart();
}
}));
data.stopHTTPServer();
}
@Test
public void testFreshStart() throws SyncConfigurationException, IllegalArgumentException, NonObjectJSONException, IOException, ParseException, CryptoException {
final AtomicBoolean mgUploaded = new AtomicBoolean(false);
final AtomicBoolean mgDownloaded = new AtomicBoolean(false);
final MetaGlobal uploadedMg = new MetaGlobal(null, null);
MockServer server = new MockServer() {
public void handle(Request request, Response response) {
if (request.getMethod().equals("PUT")) {
try {
ExtendedJSONObject body = ExtendedJSONObject.parseJSONObject(request.getContent());
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());
}
}