зеркало из https://github.com/mozilla/gecko-dev.git
Bug 612381 - Merge fx-sync to mozilla-central. a=blocking-beta8
This commit is contained in:
Коммит
6633fdedf0
|
@ -62,6 +62,10 @@ MULTI_DESKTOP_SYNC: 60 * 60 * 1000, // 1 hour
|
|||
MULTI_MOBILE_SYNC: 5 * 60 * 1000, // 5 minutes
|
||||
PARTIAL_DATA_SYNC: 60 * 1000, // 1 minute
|
||||
|
||||
// 50 is hardcoded here because of URL length restrictions.
|
||||
// (GUIDs can be up to 64 chars long)
|
||||
MOBILE_BATCH_SIZE: 50,
|
||||
|
||||
// score thresholds for early syncs
|
||||
SINGLE_USER_THRESHOLD: 1000,
|
||||
MULTI_DESKTOP_THRESHOLD: 500,
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
* Contributor(s):
|
||||
* Dan Mills <thunder@mozilla.com>
|
||||
* Myk Melez <myk@mozilla.org>
|
||||
* Philipp von Weitershausen <philipp@weitershausen.de>
|
||||
*
|
||||
* Alternatively, the contents of this file may be used under the terms of
|
||||
* either the GNU General Public License Version 2 or later (the "GPL"), or
|
||||
|
@ -282,7 +283,6 @@ Engine.prototype = {
|
|||
|
||||
function SyncEngine(name) {
|
||||
Engine.call(this, name || "SyncEngine");
|
||||
this.loadToFetch();
|
||||
}
|
||||
SyncEngine.prototype = {
|
||||
__proto__: Engine.prototype,
|
||||
|
@ -307,6 +307,9 @@ SyncEngine.prototype = {
|
|||
Svc.Prefs.set(this.name + ".syncID", value);
|
||||
},
|
||||
|
||||
/*
|
||||
* lastSync is a timestamp in server time.
|
||||
*/
|
||||
get lastSync() {
|
||||
return parseFloat(Svc.Prefs.get(this.name + ".lastSync", "0"));
|
||||
},
|
||||
|
@ -320,19 +323,27 @@ SyncEngine.prototype = {
|
|||
this._log.debug("Resetting " + this.name + " last sync time");
|
||||
Svc.Prefs.reset(this.name + ".lastSync");
|
||||
Svc.Prefs.set(this.name + ".lastSync", "0");
|
||||
this.lastSyncLocal = 0;
|
||||
},
|
||||
|
||||
get toFetch() this._toFetch,
|
||||
set toFetch(val) {
|
||||
this._toFetch = val;
|
||||
Utils.jsonSave("toFetch/" + this.name, this, val);
|
||||
/*
|
||||
* lastSyncLocal is a timestamp in local time.
|
||||
*/
|
||||
get lastSyncLocal() {
|
||||
return parseInt(Svc.Prefs.get(this.name + ".lastSyncLocal", "0"), 10);
|
||||
},
|
||||
set lastSyncLocal(value) {
|
||||
// Store as a string because pref can only store C longs as numbers.
|
||||
Svc.Prefs.set(this.name + ".lastSyncLocal", value.toString());
|
||||
},
|
||||
|
||||
loadToFetch: function loadToFetch() {
|
||||
// Initialize to empty if there's no file
|
||||
this._toFetch = [];
|
||||
Utils.jsonLoad("toFetch/" + this.name, this, Utils.bind2(this, function(o)
|
||||
this._toFetch = o));
|
||||
/*
|
||||
* Returns a mapping of IDs -> changed timestamp. Engine implementations
|
||||
* can override this method to bypass the tracker for certain or all
|
||||
* changed items.
|
||||
*/
|
||||
getChangedIDs: function getChangedIDs() {
|
||||
return this._tracker.changedIDs;
|
||||
},
|
||||
|
||||
// Create a new record using the store and add in crypto fields
|
||||
|
@ -425,37 +436,41 @@ SyncEngine.prototype = {
|
|||
CryptoMetas.set(meta.uri, meta);
|
||||
}
|
||||
|
||||
// Mark all items to be uploaded, but treat them as changed from long ago
|
||||
if (!this.lastSync) {
|
||||
this._maybeLastSyncLocal = Date.now();
|
||||
if (this.lastSync) {
|
||||
this._modified = this.getChangedIDs();
|
||||
// Clear the tracker now but remember the changed IDs in case we
|
||||
// need to roll back.
|
||||
this._backupChangedIDs = this._tracker.changedIDs;
|
||||
this._tracker.clearChangedIDs();
|
||||
} else {
|
||||
// Mark all items to be uploaded, but treat them as changed from long ago
|
||||
this._log.debug("First sync, uploading all items");
|
||||
this._modified = {};
|
||||
for (let id in this._store.getAllIDs())
|
||||
this._tracker.addChangedID(id, 0);
|
||||
this._modified[id] = 0;
|
||||
}
|
||||
|
||||
let outnum = [i for (i in this._tracker.changedIDs)].length;
|
||||
let outnum = [i for (i in this._modified)].length;
|
||||
this._log.info(outnum + " outgoing items pre-reconciliation");
|
||||
|
||||
// Keep track of what to delete at the end of sync
|
||||
this._delete = {};
|
||||
},
|
||||
|
||||
// Generate outgoing records
|
||||
// Process incoming records
|
||||
_processIncoming: function SyncEngine__processIncoming() {
|
||||
this._log.trace("Downloading & applying server changes");
|
||||
|
||||
// Figure out how many total items to fetch this sync; do less on mobile.
|
||||
// 50 is hardcoded here because of URL length restrictions.
|
||||
// (GUIDs can be up to 64 chars long)
|
||||
let fetchNum = Infinity;
|
||||
|
||||
let batchSize = Infinity;
|
||||
let newitems = new Collection(this.engineURL, this._recordObj);
|
||||
if (Svc.Prefs.get("client.type") == "mobile") {
|
||||
fetchNum = 50;
|
||||
newitems.sort = "index";
|
||||
batchSize = MOBILE_BATCH_SIZE;
|
||||
}
|
||||
newitems.newer = this.lastSync;
|
||||
newitems.full = true;
|
||||
newitems.limit = fetchNum;
|
||||
newitems.limit = batchSize;
|
||||
|
||||
let count = {applied: 0, reconciled: 0};
|
||||
let handled = [];
|
||||
|
@ -489,7 +504,7 @@ SyncEngine.prototype = {
|
|||
|
||||
// Upload a new record to replace the bad one if we have it
|
||||
if (this._store.itemExists(item.id))
|
||||
this._tracker.addChangedID(item.id, 0);
|
||||
this._modified[item.id] = 0;
|
||||
}
|
||||
this._tracker.ignoreAll = false;
|
||||
Sync.sleep(0);
|
||||
|
@ -502,16 +517,13 @@ SyncEngine.prototype = {
|
|||
resp.failureCode = ENGINE_DOWNLOAD_FAIL;
|
||||
throw resp;
|
||||
}
|
||||
|
||||
// Subtract out the number of items we just got
|
||||
fetchNum -= handled.length;
|
||||
}
|
||||
|
||||
// Check if we got the maximum that we requested; get the rest if so
|
||||
// Mobile: check if we got the maximum that we requested; get the rest if so.
|
||||
let toFetch = [];
|
||||
if (handled.length == newitems.limit) {
|
||||
let guidColl = new Collection(this.engineURL);
|
||||
guidColl.newer = this.lastSync;
|
||||
guidColl.sort = "index";
|
||||
|
||||
let guids = guidColl.get();
|
||||
if (!guids.success)
|
||||
|
@ -521,20 +533,18 @@ SyncEngine.prototype = {
|
|||
// were already waiting and prepend the new ones
|
||||
let extra = Utils.arraySub(guids.obj, handled);
|
||||
if (extra.length > 0)
|
||||
this.toFetch = extra.concat(Utils.arraySub(this.toFetch, extra));
|
||||
toFetch = extra.concat(Utils.arraySub(toFetch, extra));
|
||||
}
|
||||
|
||||
// Process any backlog of GUIDs if we haven't fetched too many this sync
|
||||
while (this.toFetch.length > 0 && fetchNum > 0) {
|
||||
// Mobile: process any backlog of GUIDs
|
||||
while (toFetch.length) {
|
||||
// Reuse the original query, but get rid of the restricting params
|
||||
newitems.limit = 0;
|
||||
newitems.newer = 0;
|
||||
|
||||
// Get the first bunch of records and save the rest for later
|
||||
let minFetch = Math.min(150, this.toFetch.length, fetchNum);
|
||||
newitems.ids = this.toFetch.slice(0, minFetch);
|
||||
this.toFetch = this.toFetch.slice(minFetch);
|
||||
fetchNum -= minFetch;
|
||||
newitems.ids = toFetch.slice(0, batchSize);
|
||||
toFetch = toFetch.slice(batchSize);
|
||||
|
||||
// Reuse the existing record handler set earlier
|
||||
let resp = newitems.get();
|
||||
|
@ -548,7 +558,7 @@ SyncEngine.prototype = {
|
|||
this.lastSync = this.lastModified;
|
||||
|
||||
this._log.info(["Records:", count.applied, "applied,", count.reconciled,
|
||||
"reconciled,", this.toFetch.length, "left to fetch"].join(" "));
|
||||
"reconciled."].join(" "));
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -609,7 +619,7 @@ SyncEngine.prototype = {
|
|||
this._log.trace("Incoming: " + item);
|
||||
|
||||
this._log.trace("Reconcile step 1: Check for conflicts");
|
||||
if (item.id in this._tracker.changedIDs) {
|
||||
if (item.id in this._modified) {
|
||||
// If the incoming and local changes are the same, skip
|
||||
if (this._isEqual(item)) {
|
||||
this._tracker.removeChangedID(item.id);
|
||||
|
@ -618,7 +628,7 @@ SyncEngine.prototype = {
|
|||
|
||||
// Records differ so figure out which to take
|
||||
let recordAge = Resource.serverTime - item.modified;
|
||||
let localAge = Date.now() / 1000 - this._tracker.changedIDs[item.id];
|
||||
let localAge = Date.now() / 1000 - this._modified[item.id];
|
||||
this._log.trace("Record age vs local age: " + [recordAge, localAge]);
|
||||
|
||||
// Apply the record if the record is newer (server wins)
|
||||
|
@ -645,7 +655,7 @@ SyncEngine.prototype = {
|
|||
// Upload outgoing records
|
||||
_uploadOutgoing: function SyncEngine__uploadOutgoing() {
|
||||
let failed = {};
|
||||
let outnum = [i for (i in this._tracker.changedIDs)].length;
|
||||
let outnum = [i for (i in this._modified)].length;
|
||||
if (outnum) {
|
||||
this._log.trace("Preparing " + outnum + " outgoing records");
|
||||
|
||||
|
@ -663,7 +673,7 @@ SyncEngine.prototype = {
|
|||
throw resp;
|
||||
}
|
||||
|
||||
// Record the modified time of the upload
|
||||
// Update server timestamp from the upload.
|
||||
let modified = resp.headers["x-weave-timestamp"];
|
||||
if (modified > this.lastSync)
|
||||
this.lastSync = modified;
|
||||
|
@ -672,7 +682,7 @@ SyncEngine.prototype = {
|
|||
// can mark them changed again.
|
||||
let failed_ids = [];
|
||||
for (let id in resp.obj.failed) {
|
||||
failed[id] = this._tracker.changedIDs[id];
|
||||
failed[id] = this._modified[id];
|
||||
failed_ids.push(id);
|
||||
}
|
||||
if (failed_ids.length)
|
||||
|
@ -683,7 +693,7 @@ SyncEngine.prototype = {
|
|||
up.clearRecords();
|
||||
});
|
||||
|
||||
for (let id in this._tracker.changedIDs) {
|
||||
for (let id in this._modified) {
|
||||
try {
|
||||
let out = this._createRecord(id);
|
||||
if (this._log.level <= Log4Moz.Level.Trace)
|
||||
|
@ -707,7 +717,11 @@ SyncEngine.prototype = {
|
|||
if (count % MAX_UPLOAD_RECORDS > 0)
|
||||
doUpload(count >= MAX_UPLOAD_RECORDS ? "last batch" : "all");
|
||||
}
|
||||
this._tracker.clearChangedIDs();
|
||||
|
||||
// Update local timestamp.
|
||||
this.lastSyncLocal = this._maybeLastSyncLocal;
|
||||
delete this._modified;
|
||||
delete this._backupChangedIDs;
|
||||
|
||||
// Mark failed WBOs as changed again so they are reuploaded next time.
|
||||
for (let id in failed) {
|
||||
|
@ -744,6 +758,15 @@ SyncEngine.prototype = {
|
|||
}
|
||||
},
|
||||
|
||||
_rollback: function _rollback() {
|
||||
if (!this._backupChangedIDs)
|
||||
return;
|
||||
|
||||
for (let [id, when] in Iterator(this._backupChangedIDs)) {
|
||||
this._tracker.addChangedID(id, when);
|
||||
}
|
||||
},
|
||||
|
||||
_sync: function SyncEngine__sync() {
|
||||
try {
|
||||
this._syncStartup();
|
||||
|
@ -754,6 +777,7 @@ SyncEngine.prototype = {
|
|||
this._syncFinish();
|
||||
}
|
||||
catch (e) {
|
||||
this._rollback();
|
||||
this._log.warn("Sync failed");
|
||||
throw e;
|
||||
}
|
||||
|
@ -788,7 +812,6 @@ SyncEngine.prototype = {
|
|||
|
||||
_resetClient: function SyncEngine__resetClient() {
|
||||
this.resetLastSync();
|
||||
this.toFetch = [];
|
||||
},
|
||||
|
||||
wipeServer: function wipeServer(ignoreCrypto) {
|
||||
|
|
|
@ -45,6 +45,7 @@ const Cu = Components.utils;
|
|||
const PARENT_ANNO = "weave/parent";
|
||||
const PREDECESSOR_ANNO = "weave/predecessor";
|
||||
const SERVICE_NOT_SUPPORTED = "Service not supported on this platform";
|
||||
const FOLDER_SORTINDEX = 1000000;
|
||||
|
||||
try {
|
||||
Cu.import("resource://gre/modules/PlacesUtils.jsm");
|
||||
|
@ -862,6 +863,10 @@ BookmarksStore.prototype = {
|
|||
},
|
||||
|
||||
_calculateIndex: function _calculateIndex(record) {
|
||||
// Ensure folders have a very high sort index so they're not synced last.
|
||||
if (record.type == "folder")
|
||||
return FOLDER_SORTINDEX;
|
||||
|
||||
// For anything directly under the toolbar, give it a boost of more than an
|
||||
// unvisited bookmark
|
||||
let index = 0;
|
||||
|
|
|
@ -50,31 +50,6 @@ Cu.import("resource://services-sync/type_records/history.js");
|
|||
Cu.import("resource://services-sync/util.js");
|
||||
Cu.import("resource://services-sync/log4moz.js");
|
||||
|
||||
// Create some helper functions to handle GUIDs
|
||||
function setGUID(uri, guid) {
|
||||
if (arguments.length == 1)
|
||||
guid = Utils.makeGUID();
|
||||
|
||||
try {
|
||||
Utils.anno(uri, GUID_ANNO, guid, "WITH_HISTORY");
|
||||
} catch (ex) {
|
||||
let log = Log4Moz.repository.getLogger("Engine.History");
|
||||
log.warn("Couldn't annotate URI " + uri + ": " + ex);
|
||||
}
|
||||
return guid;
|
||||
}
|
||||
function GUIDForUri(uri, create) {
|
||||
try {
|
||||
// Use the existing GUID if it exists
|
||||
return Utils.anno(uri, GUID_ANNO);
|
||||
}
|
||||
catch (ex) {
|
||||
// Give the uri a GUID if it doesn't have one
|
||||
if (create)
|
||||
return setGUID(uri);
|
||||
}
|
||||
}
|
||||
|
||||
function HistoryEngine() {
|
||||
SyncEngine.call(this, "History");
|
||||
}
|
||||
|
@ -87,7 +62,7 @@ HistoryEngine.prototype = {
|
|||
_sync: Utils.batchSync("History", SyncEngine),
|
||||
|
||||
_findDupe: function _findDupe(item) {
|
||||
return GUIDForUri(item.histUri);
|
||||
return this._store.GUIDForUri(item.histUri);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -142,6 +117,127 @@ HistoryStore.prototype = {
|
|||
return this.__haveTempTables;
|
||||
},
|
||||
|
||||
get _addGUIDAnnotationNameStm() {
|
||||
let stmt = this._getStmt(
|
||||
"INSERT OR IGNORE INTO moz_anno_attributes (name) VALUES (:anno_name)");
|
||||
stmt.params.anno_name = GUID_ANNO;
|
||||
return stmt;
|
||||
},
|
||||
|
||||
get _checkGUIDPageAnnotationStm() {
|
||||
let base =
|
||||
"SELECT h.id AS place_id, " +
|
||||
"(SELECT id FROM moz_anno_attributes WHERE name = :anno_name) AS name_id, " +
|
||||
"a.id AS anno_id, a.dateAdded AS anno_date ";
|
||||
let stmt;
|
||||
if (this._haveTempTables) {
|
||||
// Gecko <2.0
|
||||
stmt = this._getStmt(base +
|
||||
"FROM (SELECT id FROM moz_places_temp WHERE url = :page_url " +
|
||||
"UNION " +
|
||||
"SELECT id FROM moz_places WHERE url = :page_url) AS h " +
|
||||
"LEFT JOIN moz_annos a ON a.place_id = h.id " +
|
||||
"AND a.anno_attribute_id = name_id");
|
||||
} else {
|
||||
// Gecko 2.0
|
||||
stmt = this._getStmt(base +
|
||||
"FROM moz_places h " +
|
||||
"LEFT JOIN moz_annos a ON a.place_id = h.id " +
|
||||
"AND a.anno_attribute_id = name_id " +
|
||||
"WHERE h.url = :page_url");
|
||||
}
|
||||
stmt.params.anno_name = GUID_ANNO;
|
||||
return stmt;
|
||||
},
|
||||
|
||||
get _addPageAnnotationStm() {
|
||||
return this._getStmt(
|
||||
"INSERT OR REPLACE INTO moz_annos " +
|
||||
"(id, place_id, anno_attribute_id, mime_type, content, flags, " +
|
||||
"expiration, type, dateAdded, lastModified) " +
|
||||
"VALUES (:id, :place_id, :name_id, :mime_type, :content, :flags, " +
|
||||
":expiration, :type, :date_added, :last_modified)");
|
||||
},
|
||||
|
||||
// Some helper functions to handle GUIDs
|
||||
setGUID: function setGUID(uri, guid) {
|
||||
uri = uri.spec ? uri.spec : uri;
|
||||
|
||||
if (arguments.length == 1)
|
||||
guid = Utils.makeGUID();
|
||||
|
||||
// Ensure annotation name exists
|
||||
Utils.queryAsync(this._addGUIDAnnotationNameStm);
|
||||
|
||||
let stmt = this._checkGUIDPageAnnotationStm;
|
||||
stmt.params.page_url = uri;
|
||||
let result = Utils.queryAsync(stmt, ["place_id", "name_id", "anno_id",
|
||||
"anno_date"])[0];
|
||||
if (!result) {
|
||||
let log = Log4Moz.repository.getLogger("Engine.History");
|
||||
log.warn("Couldn't annotate URI " + uri);
|
||||
return guid;
|
||||
}
|
||||
|
||||
stmt = this._addPageAnnotationStm;
|
||||
if (result.anno_id) {
|
||||
stmt.params.id = result.anno_id;
|
||||
stmt.params.date_added = result.anno_date;
|
||||
} else {
|
||||
stmt.params.id = null;
|
||||
stmt.params.date_added = Date.now() * 1000;
|
||||
}
|
||||
stmt.params.place_id = result.place_id;
|
||||
stmt.params.name_id = result.name_id;
|
||||
stmt.params.content = guid;
|
||||
stmt.params.flags = 0;
|
||||
stmt.params.expiration = Ci.nsIAnnotationService.EXPIRE_WITH_HISTORY;
|
||||
stmt.params.type = Ci.nsIAnnotationService.TYPE_STRING;
|
||||
stmt.params.last_modified = Date.now() * 1000;
|
||||
Utils.queryAsync(stmt);
|
||||
|
||||
return guid;
|
||||
},
|
||||
|
||||
get _guidStm() {
|
||||
let base =
|
||||
"SELECT a.content AS guid " +
|
||||
"FROM moz_annos a " +
|
||||
"JOIN moz_anno_attributes n ON n.id = a.anno_attribute_id ";
|
||||
let stm;
|
||||
if (this._haveTempTables) {
|
||||
// Gecko <2.0
|
||||
stm = this._getStmt(base +
|
||||
"JOIN ( " +
|
||||
"SELECT id FROM moz_places_temp WHERE url = :page_url " +
|
||||
"UNION " +
|
||||
"SELECT id FROM moz_places WHERE url = :page_url " +
|
||||
") AS h ON h.id = a.place_id " +
|
||||
"WHERE n.name = :anno_name");
|
||||
} else {
|
||||
// Gecko 2.0
|
||||
stm = this._getStmt(base +
|
||||
"JOIN moz_places h ON h.id = a.place_id " +
|
||||
"WHERE n.name = :anno_name AND h.url = :page_url");
|
||||
}
|
||||
stm.params.anno_name = GUID_ANNO;
|
||||
return stm;
|
||||
},
|
||||
|
||||
GUIDForUri: function GUIDForUri(uri, create) {
|
||||
let stm = this._guidStm;
|
||||
stm.params.page_url = uri.spec ? uri.spec : uri;
|
||||
|
||||
// Use the existing GUID if it exists
|
||||
let result = Utils.queryAsync(stm, ["guid"])[0];
|
||||
if (result)
|
||||
return result.guid;
|
||||
|
||||
// Give the uri a GUID if it doesn't have one
|
||||
if (create)
|
||||
return this.setGUID(uri);
|
||||
},
|
||||
|
||||
get _visitStm() {
|
||||
// Gecko <2.0
|
||||
if (this._haveTempTables) {
|
||||
|
@ -219,7 +315,7 @@ HistoryStore.prototype = {
|
|||
},
|
||||
|
||||
changeItemID: function HStore_changeItemID(oldID, newID) {
|
||||
setGUID(this._findURLByGUID(oldID).url, newID);
|
||||
this.setGUID(this._findURLByGUID(oldID).url, newID);
|
||||
},
|
||||
|
||||
|
||||
|
@ -229,8 +325,9 @@ HistoryStore.prototype = {
|
|||
this._allUrlStm.params.max_results = 5000;
|
||||
|
||||
let urls = Utils.queryAsync(this._allUrlStm, "url");
|
||||
let self = this;
|
||||
return urls.reduce(function(ids, item) {
|
||||
ids[GUIDForUri(item.url, true)] = item.url;
|
||||
ids[self.GUIDForUri(item.url, true)] = item.url;
|
||||
return ids;
|
||||
}, {});
|
||||
},
|
||||
|
@ -238,7 +335,7 @@ HistoryStore.prototype = {
|
|||
create: function HistStore_create(record) {
|
||||
// Add the url and set the GUID
|
||||
this.update(record);
|
||||
setGUID(record.histUri, record.id);
|
||||
this.setGUID(record.histUri, record.id);
|
||||
},
|
||||
|
||||
remove: function HistStore_remove(record) {
|
||||
|
@ -332,6 +429,11 @@ HistoryTracker.prototype = {
|
|||
}
|
||||
},
|
||||
|
||||
_GUIDForUri: function _GUIDForUri(uri, create) {
|
||||
// Isn't indirection fun...
|
||||
return Engines.get("history")._store.GUIDForUri(uri, create);
|
||||
},
|
||||
|
||||
QueryInterface: XPCOMUtils.generateQI([
|
||||
Ci.nsINavHistoryObserver,
|
||||
Ci.nsINavHistoryObserver_MOZILLA_1_9_1_ADDITIONS,
|
||||
|
@ -354,7 +456,7 @@ HistoryTracker.prototype = {
|
|||
if (this.ignoreAll)
|
||||
return;
|
||||
this._log.trace("onVisit: " + uri.spec);
|
||||
if (this.addChangedID(GUIDForUri(uri, true)))
|
||||
if (this.addChangedID(this._GUIDForUri(uri, true)))
|
||||
this._upScore();
|
||||
},
|
||||
onDeleteVisits: function onDeleteVisits() {
|
||||
|
@ -365,7 +467,7 @@ HistoryTracker.prototype = {
|
|||
if (this.ignoreAll)
|
||||
return;
|
||||
this._log.trace("onBeforeDeleteURI: " + uri.spec);
|
||||
if (this.addChangedID(GUIDForUri(uri, true)))
|
||||
if (this.addChangedID(this._GUIDForUri(uri, true)))
|
||||
this._upScore();
|
||||
},
|
||||
onDeleteURI: function HT_onDeleteURI(uri) {
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
* Contributor(s):
|
||||
* Myk Melez <myk@mozilla.org>
|
||||
* Jono DiCarlo <jdicarlo@mozilla.com>
|
||||
* Philipp von Weitershausen <philipp@weitershausen.de>
|
||||
*
|
||||
* Alternatively, the contents of this file may be used under the terms of
|
||||
* either the GNU General Public License Version 2 or later (the "GPL"), or
|
||||
|
@ -69,6 +70,14 @@ TabEngine.prototype = {
|
|||
_trackerObj: TabTracker,
|
||||
_recordObj: TabSetRecord,
|
||||
|
||||
getChangedIDs: function getChangedIDs() {
|
||||
// No need for a proper timestamp (no conflict resolution needed).
|
||||
let changedIDs = {};
|
||||
if (this._tracker.modified)
|
||||
changedIDs[Clients.localID] = 0;
|
||||
return changedIDs;
|
||||
},
|
||||
|
||||
// API for use by Weave UI code to give user choices of tabs to open:
|
||||
getAllClients: function TabEngine_getAllClients() {
|
||||
return this._store._remoteClients;
|
||||
|
@ -81,6 +90,7 @@ TabEngine.prototype = {
|
|||
_resetClient: function TabEngine__resetClient() {
|
||||
SyncEngine.prototype._resetClient.call(this);
|
||||
this._store.wipe();
|
||||
this._tracker.modified = true;
|
||||
},
|
||||
|
||||
/* The intent is not to show tabs in the menu if they're already
|
||||
|
@ -237,6 +247,14 @@ TabTracker.prototype = {
|
|||
|
||||
QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
|
||||
|
||||
loadChangedIDs: function loadChangedIDs() {
|
||||
// Don't read changed IDs from disk at start up.
|
||||
},
|
||||
|
||||
clearChangedIDs: function clearChangedIDs() {
|
||||
this.modified = false;
|
||||
},
|
||||
|
||||
_topics: ["pageshow", "TabOpen", "TabClose", "TabSelect"],
|
||||
_registerListenersForWindow: function registerListenersFW(window) {
|
||||
this._log.trace("Registering tab listeners in window");
|
||||
|
@ -292,7 +310,7 @@ TabTracker.prototype = {
|
|||
break;
|
||||
case "private-browsing":
|
||||
if (aData == "enter" && !PBPrefs.get("autostart"))
|
||||
this.clearChangedIDs();
|
||||
this.modified = false;
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -303,7 +321,7 @@ TabTracker.prototype = {
|
|||
}
|
||||
|
||||
this._log.trace("onTab event: " + event.type);
|
||||
this.addChangedID(Clients.localID);
|
||||
this.modified = true;
|
||||
|
||||
// For pageshow events, only give a partial score bump (~.1)
|
||||
let chance = .1;
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
* Contributor(s):
|
||||
* Dan Mills <thunder@mozilla.com>
|
||||
* Anant Narayanan <anant@kix.in>
|
||||
* Philipp von Weitershausen <philipp@weitershausen.de>
|
||||
*
|
||||
* Alternatively, the contents of this file may be used under the terms of
|
||||
* either the GNU General Public License Version 2 or later (the "GPL"), or
|
||||
|
@ -35,7 +36,7 @@
|
|||
*
|
||||
* ***** END LICENSE BLOCK ***** */
|
||||
|
||||
const EXPORTED_SYMBOLS = ["Resource"];
|
||||
const EXPORTED_SYMBOLS = ["Resource", "AsyncResource"];
|
||||
|
||||
const Cc = Components.classes;
|
||||
const Ci = Components.interfaces;
|
||||
|
@ -50,24 +51,39 @@ Cu.import("resource://services-sync/ext/Sync.js");
|
|||
Cu.import("resource://services-sync/log4moz.js");
|
||||
Cu.import("resource://services-sync/util.js");
|
||||
|
||||
// = Resource =
|
||||
//
|
||||
// Represents a remote network resource, identified by a URI.
|
||||
function Resource(uri) {
|
||||
/*
|
||||
* AsyncResource represents a remote network resource, identified by a URI.
|
||||
* Create an instance like so:
|
||||
*
|
||||
* let resource = new AsyncResource("http://foobar.com/path/to/resource");
|
||||
*
|
||||
* The 'resource' object has the following methods to issue HTTP requests
|
||||
* of the corresponding HTTP methods:
|
||||
*
|
||||
* get(callback)
|
||||
* put(data, callback)
|
||||
* post(data, callback)
|
||||
* delete(callback)
|
||||
*
|
||||
* 'callback' is a function with the following signature:
|
||||
*
|
||||
* function callback(error, result) {...}
|
||||
*
|
||||
* 'error' will be null on successful requests. Likewise, result will not be
|
||||
* passes (=undefined) when an error occurs. Note that this is independent of
|
||||
* the status of the HTTP response.
|
||||
*/
|
||||
function AsyncResource(uri) {
|
||||
this._log = Log4Moz.repository.getLogger(this._logName);
|
||||
this._log.level =
|
||||
Log4Moz.Level[Utils.prefs.getCharPref("log.logger.network.resources")];
|
||||
this.uri = uri;
|
||||
this._headers = {};
|
||||
this._onComplete = Utils.bind2(this, this._onComplete);
|
||||
}
|
||||
Resource.prototype = {
|
||||
AsyncResource.prototype = {
|
||||
_logName: "Net.Resource",
|
||||
|
||||
// ** {{{ Resource.serverTime }}} **
|
||||
//
|
||||
// Caches the latest server timestamp (X-Weave-Timestamp header).
|
||||
serverTime: null,
|
||||
|
||||
// ** {{{ Resource.authenticator }}} **
|
||||
//
|
||||
// Getter and setter for the authenticator module
|
||||
|
@ -137,7 +153,7 @@ Resource.prototype = {
|
|||
// ** {{{ Resource._createRequest }}} **
|
||||
//
|
||||
// This method returns a new IO Channel for requests to be made
|
||||
// through. It is never called directly, only {{{_request}}} uses it
|
||||
// through. It is never called directly, only {{{_doRequest}}} uses it
|
||||
// to obtain a request channel.
|
||||
//
|
||||
_createRequest: function Res__createRequest() {
|
||||
|
@ -165,14 +181,9 @@ Resource.prototype = {
|
|||
|
||||
_onProgress: function Res__onProgress(channel) {},
|
||||
|
||||
// ** {{{ Resource._request }}} **
|
||||
//
|
||||
// Perform a particular HTTP request on the resource. This method
|
||||
// is never called directly, but is used by the high-level
|
||||
// {{{get}}}, {{{put}}}, {{{post}}} and {{delete}} methods.
|
||||
_request: function Res__request(action, data) {
|
||||
let iter = 0;
|
||||
let channel = this._createRequest();
|
||||
_doRequest: function _doRequest(action, data, callback) {
|
||||
this._callback = callback;
|
||||
let channel = this._channel = this._createRequest();
|
||||
|
||||
if ("undefined" != typeof(data))
|
||||
this._data = data;
|
||||
|
@ -200,29 +211,21 @@ Resource.prototype = {
|
|||
|
||||
// Setup a channel listener so that the actual network operation
|
||||
// is performed asynchronously.
|
||||
let [chanOpen, chanCb] = Sync.withCb(channel.asyncOpen, channel);
|
||||
let listener = new ChannelListener(chanCb, this._onProgress, this._log);
|
||||
let listener = new ChannelListener(this._onComplete, this._onProgress,
|
||||
this._log);
|
||||
channel.requestMethod = action;
|
||||
channel.asyncOpen(listener, null);
|
||||
},
|
||||
|
||||
// The channel listener might get a failure code
|
||||
try {
|
||||
this._data = chanOpen(listener, null);
|
||||
_onComplete: function _onComplete(error, data) {
|
||||
if (error) {
|
||||
this._callback(error);
|
||||
return;
|
||||
}
|
||||
catch(ex) {
|
||||
// Combine the channel stack with this request stack
|
||||
let error = Error(ex.message);
|
||||
let chanStack = [];
|
||||
if (ex.stack)
|
||||
chanStack = ex.stack.trim().split(/\n/).slice(1);
|
||||
let requestStack = error.stack.split(/\n/).slice(1);
|
||||
|
||||
// Strip out the args for the last 2 frames because they're usually HUGE!
|
||||
for (let i = 0; i <= 1; i++)
|
||||
requestStack[i] = requestStack[i].replace(/\(".*"\)@/, "(...)@");
|
||||
|
||||
error.stack = chanStack.concat(requestStack).join("\n");
|
||||
throw error;
|
||||
}
|
||||
this._data = data;
|
||||
let channel = this._channel;
|
||||
let action = channel.requestMethod;
|
||||
|
||||
// Set some default values in-case there's no response header
|
||||
let headers = {};
|
||||
|
@ -240,13 +243,13 @@ Resource.prototype = {
|
|||
|
||||
// Log the status of the request
|
||||
let mesg = [action, success ? "success" : "fail", status,
|
||||
channel.URI.spec].join(" ");
|
||||
channel.URI.spec].join(" ");
|
||||
if (mesg.length > 200)
|
||||
mesg = mesg.substr(0, 200) + "…";
|
||||
this._log.debug(mesg);
|
||||
// Additionally give the full response body when Trace logging
|
||||
if (this._log.level <= Log4Moz.Level.Trace)
|
||||
this._log.trace(action + " body: " + this._data);
|
||||
this._log.trace(action + " body: " + data);
|
||||
|
||||
// This is a server-side safety valve to allow slowing down
|
||||
// clients without hurting performance.
|
||||
|
@ -263,7 +266,7 @@ Resource.prototype = {
|
|||
this._log.debug(action + " cached: " + status);
|
||||
}
|
||||
|
||||
let ret = new String(this._data);
|
||||
let ret = new String(data);
|
||||
ret.headers = headers;
|
||||
ret.status = status;
|
||||
ret.success = success;
|
||||
|
@ -284,22 +287,96 @@ Resource.prototype = {
|
|||
// Do the same type of request but with the new uri
|
||||
if (subject.newUri != "") {
|
||||
this.uri = subject.newUri;
|
||||
return this._request.apply(this, arguments);
|
||||
this._doRequest(action, this._data, this._callback);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
this._callback(null, ret);
|
||||
},
|
||||
|
||||
get: function get(callback) {
|
||||
this._doRequest("GET", undefined, callback);
|
||||
},
|
||||
|
||||
put: function put(data, callback) {
|
||||
if (typeof data == "function")
|
||||
[data, callback] = [undefined, data];
|
||||
this._doRequest("PUT", data, callback);
|
||||
},
|
||||
|
||||
post: function post(data, callback) {
|
||||
if (typeof data == "function")
|
||||
[data, callback] = [undefined, data];
|
||||
this._doRequest("POST", data, callback);
|
||||
},
|
||||
|
||||
delete: function delete(callback) {
|
||||
this._doRequest("DELETE", undefined, callback);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/*
|
||||
* Represent a remote network resource, identified by a URI, with a
|
||||
* synchronous API.
|
||||
*
|
||||
* 'Resource' is not recommended for new code. Use the asynchronous API of
|
||||
* 'AsyncResource' instead.
|
||||
*/
|
||||
function Resource(uri) {
|
||||
AsyncResource.call(this, uri);
|
||||
}
|
||||
Resource.prototype = {
|
||||
|
||||
__proto__: AsyncResource.prototype,
|
||||
|
||||
// ** {{{ Resource.serverTime }}} **
|
||||
//
|
||||
// Caches the latest server timestamp (X-Weave-Timestamp header).
|
||||
serverTime: null,
|
||||
|
||||
// ** {{{ Resource._request }}} **
|
||||
//
|
||||
// Perform a particular HTTP request on the resource. This method
|
||||
// is never called directly, but is used by the high-level
|
||||
// {{{get}}}, {{{put}}}, {{{post}}} and {{delete}} methods.
|
||||
_request: function Res__request(action, data) {
|
||||
let [doRequest, cb] = Sync.withCb(this._doRequest, this);
|
||||
function callback(error, ret) {
|
||||
if (error)
|
||||
cb.throw(error);
|
||||
cb(ret);
|
||||
}
|
||||
|
||||
// The channel listener might get a failure code
|
||||
try {
|
||||
return doRequest(action, data, callback);
|
||||
} catch(ex) {
|
||||
// Combine the channel stack with this request stack
|
||||
let error = Error(ex.message);
|
||||
let chanStack = [];
|
||||
if (ex.stack)
|
||||
chanStack = ex.stack.trim().split(/\n/).slice(1);
|
||||
let requestStack = error.stack.split(/\n/).slice(1);
|
||||
|
||||
// Strip out the args for the last 2 frames because they're usually HUGE!
|
||||
for (let i = 0; i <= 1; i++)
|
||||
requestStack[i] = requestStack[i].replace(/\(".*"\)@/, "(...)@");
|
||||
|
||||
error.stack = chanStack.concat(requestStack).join("\n");
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// ** {{{ Resource.get }}} **
|
||||
//
|
||||
// Perform an asynchronous HTTP GET for this resource.
|
||||
// onComplete will be called on completion of the request.
|
||||
get: function Res_get() {
|
||||
return this._request("GET");
|
||||
},
|
||||
|
||||
// ** {{{ Resource.get }}} **
|
||||
// ** {{{ Resource.put }}} **
|
||||
//
|
||||
// Perform a HTTP PUT for this resource.
|
||||
put: function Res_put(data) {
|
||||
|
@ -357,10 +434,12 @@ ChannelListener.prototype = {
|
|||
this._data = null;
|
||||
|
||||
// Throw the failure code name (and stop execution)
|
||||
if (!Components.isSuccessCode(status))
|
||||
this._onComplete.throw(Error(Components.Exception("", status).name));
|
||||
if (!Components.isSuccessCode(status)) {
|
||||
this._onComplete(Error(Components.Exception("", status).name));
|
||||
return;
|
||||
}
|
||||
|
||||
this._onComplete(this._data);
|
||||
this._onComplete(null, this._data);
|
||||
},
|
||||
|
||||
onDataAvailable: function Channel_onDataAvail(req, cb, stream, off, count) {
|
||||
|
@ -384,7 +463,7 @@ ChannelListener.prototype = {
|
|||
// Ignore any callbacks if we happen to get any now
|
||||
this.onStopRequest = function() {};
|
||||
this.onDataAvailable = function() {};
|
||||
this._onComplete.throw(Error("Aborting due to channel inactivity."));
|
||||
this._onComplete(Error("Aborting due to channel inactivity."));
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -53,6 +53,36 @@ Cu.import("resource://services-sync/log4moz.js");
|
|||
*/
|
||||
|
||||
let Utils = {
|
||||
/**
|
||||
* Execute an arbitrary number of asynchronous functions one after the
|
||||
* other, passing the callback arguments on to the next one. All functions
|
||||
* must take a callback function as their last argument. The 'this' object
|
||||
* will be whatever asyncChain's is.
|
||||
*
|
||||
* @usage this._chain = Utils.asyncChain;
|
||||
* this._chain(this.foo, this.bar, this.baz)(args, for, foo)
|
||||
*
|
||||
* This is equivalent to:
|
||||
*
|
||||
* let self = this;
|
||||
* self.foo(args, for, foo, function (bars, args) {
|
||||
* self.bar(bars, args, function (baz, params) {
|
||||
* self.baz(baz, params);
|
||||
* });
|
||||
* });
|
||||
*/
|
||||
asyncChain: function asyncChain() {
|
||||
let funcs = Array.slice(arguments);
|
||||
let thisObj = this;
|
||||
return function callback() {
|
||||
if (funcs.length) {
|
||||
let args = Array.slice(arguments).concat(callback);
|
||||
let f = funcs.shift();
|
||||
f.apply(thisObj, args);
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Wrap a function to catch all exceptions and log them
|
||||
*
|
||||
|
|
|
@ -20,6 +20,7 @@ pref("services.sync.engine.tabs.filteredUrls", "^(about:.*|chrome://weave/.*|wyc
|
|||
pref("services.sync.log.appender.console", "Warn");
|
||||
pref("services.sync.log.appender.dump", "Error");
|
||||
pref("services.sync.log.appender.debugLog", "Trace");
|
||||
pref("services.sync.log.appender.debugLog.enabled", false);
|
||||
pref("services.sync.log.rootLogger", "Debug");
|
||||
pref("services.sync.log.logger.service.main", "Debug");
|
||||
pref("services.sync.log.logger.authenticator", "Debug");
|
||||
|
|
|
@ -42,6 +42,13 @@ function run_test() {
|
|||
|
||||
_("The calculated sort index is based on frecency data.");
|
||||
do_check_true(newrecord.sortindex >= 150);
|
||||
|
||||
_("Folders have high sort index to ensure they're synced first.");
|
||||
let folder_id = Svc.Bookmark.createFolder(Svc.Bookmark.toolbarFolder,
|
||||
"Test Folder", 0);
|
||||
let folder_guid = Svc.Bookmark.getItemGUID(folder_id);
|
||||
let folder_record = store.createRecord(folder_guid, "http://fake/uri");
|
||||
do_check_eq(folder_record.sortindex, 1000000);
|
||||
} finally {
|
||||
_("Clean up.");
|
||||
store.wipe();
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://services-sync/engines/history.js");
|
||||
Cu.import("resource://services-sync/type_records/history.js");
|
||||
Cu.import("resource://services-sync/ext/Sync.js");
|
||||
Cu.import("resource://services-sync/util.js");
|
||||
|
||||
const TIMESTAMP1 = (Date.now() - 103406528) * 1000;
|
||||
|
@ -28,30 +27,40 @@ function queryHistoryVisits(uri) {
|
|||
return queryPlaces(uri, options);
|
||||
}
|
||||
|
||||
function waitForTitleChanged(test) {
|
||||
Sync(function (callback) {
|
||||
Svc.History.addObserver({
|
||||
onBeginUpdateBatch: function onBeginUpdateBatch() {},
|
||||
onEndUpdateBatch: function onEndUpdateBatch() {},
|
||||
onPageChanged: function onPageChanged() {},
|
||||
onTitleChanged: function onTitleChanged() {
|
||||
Svc.History.removeObserver(this);
|
||||
callback();
|
||||
},
|
||||
onVisit: function onVisit() {},
|
||||
onDeleteVisits: function onDeleteVisits() {},
|
||||
onPageExpired: function onPageExpired() {},
|
||||
onBeforeDeleteURI: function onBeforeDeleteURI() {},
|
||||
onDeleteURI: function onDeleteURI() {},
|
||||
onClearHistory: function onClearHistory() {},
|
||||
QueryInterface: XPCOMUtils.generateQI([
|
||||
Ci.nsINavHistoryObserver,
|
||||
Ci.nsINavHistoryObserver_MOZILLA_1_9_1_ADDITIONS,
|
||||
Ci.nsISupportsWeakReference
|
||||
])
|
||||
}, true);
|
||||
test();
|
||||
})();
|
||||
function onNextTitleChanged(callback) {
|
||||
Svc.History.addObserver({
|
||||
onBeginUpdateBatch: function onBeginUpdateBatch() {},
|
||||
onEndUpdateBatch: function onEndUpdateBatch() {},
|
||||
onPageChanged: function onPageChanged() {},
|
||||
onTitleChanged: function onTitleChanged() {
|
||||
Svc.History.removeObserver(this);
|
||||
Utils.delay(callback, 0, this);
|
||||
},
|
||||
onVisit: function onVisit() {},
|
||||
onDeleteVisits: function onDeleteVisits() {},
|
||||
onPageExpired: function onPageExpired() {},
|
||||
onBeforeDeleteURI: function onBeforeDeleteURI() {},
|
||||
onDeleteURI: function onDeleteURI() {},
|
||||
onClearHistory: function onClearHistory() {},
|
||||
QueryInterface: XPCOMUtils.generateQI([
|
||||
Ci.nsINavHistoryObserver,
|
||||
Ci.nsINavHistoryObserver_MOZILLA_1_9_1_ADDITIONS,
|
||||
Ci.nsISupportsWeakReference
|
||||
])
|
||||
}, true);
|
||||
}
|
||||
|
||||
// Ensure exceptions from inside callbacks leads to test failures while
|
||||
// we still clean up properly.
|
||||
function ensureThrows(func) {
|
||||
return function() {
|
||||
try {
|
||||
func.apply(this, arguments);
|
||||
} catch (ex) {
|
||||
Svc.History.removeAllPages();
|
||||
do_throw(ex);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function run_test() {
|
||||
|
@ -59,15 +68,18 @@ function run_test() {
|
|||
let store = new HistoryEngine()._store;
|
||||
do_check_eq([id for (id in store.getAllIDs())].length, 0);
|
||||
|
||||
try {
|
||||
let fxuri, fxguid, tburi, tbguid;
|
||||
do_test_pending();
|
||||
Utils.asyncChain(function (next) {
|
||||
|
||||
_("Let's create an entry in the database.");
|
||||
let fxuri = Utils.makeURI("http://getfirefox.com/");
|
||||
fxuri = Utils.makeURI("http://getfirefox.com/");
|
||||
Svc.History.addPageWithDetails(fxuri, "Get Firefox!", TIMESTAMP1);
|
||||
|
||||
_("Verify that the entry exists.");
|
||||
let ids = [id for (id in store.getAllIDs())];
|
||||
do_check_eq(ids.length, 1);
|
||||
let fxguid = ids[0];
|
||||
fxguid = ids[0];
|
||||
do_check_true(store.itemExists(fxguid));
|
||||
|
||||
_("If we query a non-existent record, it's marked as deleted.");
|
||||
|
@ -85,36 +97,43 @@ function run_test() {
|
|||
_("Let's modify the record and have the store update the database.");
|
||||
let secondvisit = {date: TIMESTAMP2,
|
||||
type: Ci.nsINavHistoryService.TRANSITION_TYPED};
|
||||
waitForTitleChanged(function() {
|
||||
store.update({histUri: record.histUri,
|
||||
title: "Hol Dir Firefox!",
|
||||
visits: [record.visits[0], secondvisit]});
|
||||
});
|
||||
let queryres = queryHistoryVisits(fxuri);
|
||||
do_check_eq(queryres.length, 2);
|
||||
do_check_eq(queryres[0].time, TIMESTAMP1);
|
||||
do_check_eq(queryres[0].title, "Hol Dir Firefox!");
|
||||
do_check_eq(queryres[1].time, TIMESTAMP2);
|
||||
do_check_eq(queryres[1].title, "Hol Dir Firefox!");
|
||||
onNextTitleChanged(ensureThrows(function() {
|
||||
let queryres = queryHistoryVisits(fxuri);
|
||||
do_check_eq(queryres.length, 2);
|
||||
do_check_eq(queryres[0].time, TIMESTAMP1);
|
||||
do_check_eq(queryres[0].title, "Hol Dir Firefox!");
|
||||
do_check_eq(queryres[1].time, TIMESTAMP2);
|
||||
do_check_eq(queryres[1].title, "Hol Dir Firefox!");
|
||||
next();
|
||||
}));
|
||||
store.update({histUri: record.histUri,
|
||||
title: "Hol Dir Firefox!",
|
||||
visits: [record.visits[0], secondvisit]});
|
||||
|
||||
}, function (next) {
|
||||
|
||||
_("Create a brand new record through the store.");
|
||||
let tbguid = Utils.makeGUID();
|
||||
let tburi = Utils.makeURI("http://getthunderbird.com");
|
||||
waitForTitleChanged(function() {
|
||||
store.create({id: tbguid,
|
||||
histUri: tburi.spec,
|
||||
title: "The bird is the word!",
|
||||
visits: [{date: TIMESTAMP3,
|
||||
type: Ci.nsINavHistoryService.TRANSITION_TYPED}]});
|
||||
});
|
||||
do_check_eq([id for (id in store.getAllIDs())].length, 2);
|
||||
queryres = queryHistoryVisits(tburi);
|
||||
do_check_eq(queryres.length, 1);
|
||||
do_check_eq(queryres[0].time, TIMESTAMP3);
|
||||
do_check_eq(queryres[0].title, "The bird is the word!");
|
||||
tbguid = Utils.makeGUID();
|
||||
tburi = Utils.makeURI("http://getthunderbird.com");
|
||||
onNextTitleChanged(ensureThrows(function() {
|
||||
do_check_eq([id for (id in store.getAllIDs())].length, 2);
|
||||
let queryres = queryHistoryVisits(tburi);
|
||||
do_check_eq(queryres.length, 1);
|
||||
do_check_eq(queryres[0].time, TIMESTAMP3);
|
||||
do_check_eq(queryres[0].title, "The bird is the word!");
|
||||
next();
|
||||
}));
|
||||
store.create({id: tbguid,
|
||||
histUri: tburi.spec,
|
||||
title: "The bird is the word!",
|
||||
visits: [{date: TIMESTAMP3,
|
||||
type: Ci.nsINavHistoryService.TRANSITION_TYPED}]});
|
||||
|
||||
}, function (next) {
|
||||
|
||||
_("Make sure we handle invalid URLs in places databases gracefully.");
|
||||
let query = "INSERT INTO moz_places "
|
||||
let table = store._haveTempTables ? "moz_places_temp" : "moz_places";
|
||||
let query = "INSERT INTO " + table + " "
|
||||
+ "(url, title, rev_host, visit_count, last_visit_date) "
|
||||
+ "VALUES ('invalid-uri', 'Invalid URI', '.', 1, " + TIMESTAMP3 + ")";
|
||||
let stmt = Utils.createStatement(Svc.History.DBConnection, query);
|
||||
|
@ -124,7 +143,7 @@ function run_test() {
|
|||
_("Remove a record from the store.");
|
||||
store.remove({id: fxguid});
|
||||
do_check_false(store.itemExists(fxguid));
|
||||
queryres = queryHistoryVisits(fxuri);
|
||||
let queryres = queryHistoryVisits(fxuri);
|
||||
do_check_eq(queryres.length, 0);
|
||||
|
||||
_("Make sure wipe works.");
|
||||
|
@ -135,8 +154,10 @@ function run_test() {
|
|||
queryres = queryHistoryVisits(tburi);
|
||||
do_check_eq(queryres.length, 0);
|
||||
|
||||
} finally {
|
||||
_("Clean up.");
|
||||
Svc.History.removeAllPages();
|
||||
}
|
||||
do_test_finished();
|
||||
|
||||
})();
|
||||
|
||||
}
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://services-sync/engines.js");
|
||||
Cu.import("resource://services-sync/engines/history.js");
|
||||
Cu.import("resource://services-sync/util.js");
|
||||
|
||||
function run_test() {
|
||||
Engines.register(HistoryEngine);
|
||||
let tracker = Engines.get("history")._tracker;
|
||||
|
||||
_("Verify we've got an empty tracker to work with.");
|
||||
let tracker = new HistoryEngine()._tracker;
|
||||
do_check_eq([id for (id in tracker.changedIDs)].length, 0);
|
||||
|
||||
let _counter = 0;
|
||||
|
|
|
@ -0,0 +1,629 @@
|
|||
Cu.import("resource://services-sync/auth.js");
|
||||
Cu.import("resource://services-sync/ext/Observers.js");
|
||||
Cu.import("resource://services-sync/identity.js");
|
||||
Cu.import("resource://services-sync/log4moz.js");
|
||||
Cu.import("resource://services-sync/resource.js");
|
||||
Cu.import("resource://services-sync/util.js");
|
||||
|
||||
let logger;
|
||||
|
||||
function server_open(metadata, response) {
|
||||
let body;
|
||||
if (metadata.method == "GET") {
|
||||
body = "This path exists";
|
||||
response.setStatusLine(metadata.httpVersion, 200, "OK");
|
||||
} else {
|
||||
body = "Wrong request method";
|
||||
response.setStatusLine(metadata.httpVersion, 405, "Method Not Allowed");
|
||||
}
|
||||
response.bodyOutputStream.write(body, body.length);
|
||||
}
|
||||
|
||||
function server_protected(metadata, response) {
|
||||
let body;
|
||||
|
||||
// no btoa() in xpcshell. it's guest:guest
|
||||
if (metadata.hasHeader("Authorization") &&
|
||||
metadata.getHeader("Authorization") == "Basic Z3Vlc3Q6Z3Vlc3Q=") {
|
||||
body = "This path exists and is protected";
|
||||
response.setStatusLine(metadata.httpVersion, 200, "OK, authorized");
|
||||
response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false);
|
||||
} else {
|
||||
body = "This path exists and is protected - failed";
|
||||
response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
|
||||
response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false);
|
||||
}
|
||||
|
||||
response.bodyOutputStream.write(body, body.length);
|
||||
}
|
||||
|
||||
function server_404(metadata, response) {
|
||||
let body = "File not found";
|
||||
response.setStatusLine(metadata.httpVersion, 404, "Not Found");
|
||||
response.bodyOutputStream.write(body, body.length);
|
||||
}
|
||||
|
||||
|
||||
let sample_data = {
|
||||
some: "sample_data",
|
||||
injson: "format",
|
||||
number: 42
|
||||
};
|
||||
|
||||
function server_upload(metadata, response) {
|
||||
let body;
|
||||
|
||||
let input = readBytesFromInputStream(metadata.bodyInputStream);
|
||||
if (input == JSON.stringify(sample_data)) {
|
||||
body = "Valid data upload via " + metadata.method;
|
||||
response.setStatusLine(metadata.httpVersion, 200, "OK");
|
||||
} else {
|
||||
body = "Invalid data upload via " + metadata.method + ': ' + input;
|
||||
response.setStatusLine(metadata.httpVersion, 500, "Internal Server Error");
|
||||
}
|
||||
|
||||
response.bodyOutputStream.write(body, body.length);
|
||||
}
|
||||
|
||||
function server_delete(metadata, response) {
|
||||
let body;
|
||||
if (metadata.method == "DELETE") {
|
||||
body = "This resource has been deleted";
|
||||
response.setStatusLine(metadata.httpVersion, 200, "OK");
|
||||
} else {
|
||||
body = "Wrong request method";
|
||||
response.setStatusLine(metadata.httpVersion, 405, "Method Not Allowed");
|
||||
}
|
||||
response.bodyOutputStream.write(body, body.length);
|
||||
}
|
||||
|
||||
function server_json(metadata, response) {
|
||||
let body = JSON.stringify(sample_data);
|
||||
response.setStatusLine(metadata.httpVersion, 200, "OK");
|
||||
response.bodyOutputStream.write(body, body.length);
|
||||
}
|
||||
|
||||
const TIMESTAMP = 1274380461;
|
||||
|
||||
function server_timestamp(metadata, response) {
|
||||
let body = "Thank you for your request";
|
||||
response.setHeader("X-Weave-Timestamp", ''+TIMESTAMP, false);
|
||||
response.setStatusLine(metadata.httpVersion, 200, "OK");
|
||||
response.bodyOutputStream.write(body, body.length);
|
||||
}
|
||||
|
||||
function server_backoff(metadata, response) {
|
||||
let body = "Hey, back off!";
|
||||
response.setHeader("X-Weave-Backoff", '600', false);
|
||||
response.setStatusLine(metadata.httpVersion, 200, "OK");
|
||||
response.bodyOutputStream.write(body, body.length);
|
||||
}
|
||||
|
||||
function server_quota_notice(request, response) {
|
||||
let body = "You're approaching quota.";
|
||||
response.setHeader("X-Weave-Quota-Remaining", '1048576', false);
|
||||
response.setStatusLine(request.httpVersion, 200, "OK");
|
||||
response.bodyOutputStream.write(body, body.length);
|
||||
}
|
||||
|
||||
function server_quota_error(request, response) {
|
||||
let body = "14";
|
||||
response.setHeader("X-Weave-Quota-Remaining", '-1024', false);
|
||||
response.setStatusLine(request.httpVersion, 400, "OK");
|
||||
response.bodyOutputStream.write(body, body.length);
|
||||
}
|
||||
|
||||
function server_headers(metadata, response) {
|
||||
let ignore_headers = ["host", "user-agent", "accept", "accept-language",
|
||||
"accept-encoding", "accept-charset", "keep-alive",
|
||||
"connection", "pragma", "cache-control",
|
||||
"content-length"];
|
||||
let headers = metadata.headers;
|
||||
let header_names = [];
|
||||
while (headers.hasMoreElements()) {
|
||||
let header = headers.getNext().toString();
|
||||
if (ignore_headers.indexOf(header) == -1) {
|
||||
header_names.push(header);
|
||||
}
|
||||
}
|
||||
header_names = header_names.sort();
|
||||
|
||||
headers = {};
|
||||
for each (let header in header_names) {
|
||||
headers[header] = metadata.getHeader(header);
|
||||
}
|
||||
let body = JSON.stringify(headers);
|
||||
response.setStatusLine(metadata.httpVersion, 200, "OK");
|
||||
response.bodyOutputStream.write(body, body.length);
|
||||
}
|
||||
|
||||
|
||||
function run_test() {
|
||||
logger = Log4Moz.repository.getLogger('Test');
|
||||
Log4Moz.repository.rootLogger.addAppender(new Log4Moz.DumpAppender());
|
||||
|
||||
do_test_pending();
|
||||
let server = httpd_setup({
|
||||
"/open": server_open,
|
||||
"/protected": server_protected,
|
||||
"/404": server_404,
|
||||
"/upload": server_upload,
|
||||
"/delete": server_delete,
|
||||
"/json": server_json,
|
||||
"/timestamp": server_timestamp,
|
||||
"/headers": server_headers,
|
||||
"/backoff": server_backoff,
|
||||
"/quota-notice": server_quota_notice,
|
||||
"/quota-error": server_quota_error
|
||||
});
|
||||
|
||||
Utils.prefs.setIntPref("network.numRetries", 1); // speed up test
|
||||
|
||||
let did401 = false;
|
||||
Observers.add("weave:resource:status:401", function() did401 = true);
|
||||
|
||||
let quotaValue;
|
||||
Observers.add("weave:service:quota:remaining",
|
||||
function (subject) quotaValue = subject);
|
||||
|
||||
|
||||
// Ensure exceptions from inside callbacks leads to test failures.
|
||||
function ensureThrows(func) {
|
||||
return function() {
|
||||
try {
|
||||
func.apply(this, arguments);
|
||||
} catch (ex) {
|
||||
do_throw(ex);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let res_upload = new AsyncResource("http://localhost:8080/upload");
|
||||
let res_headers = new AsyncResource("http://localhost:8080/headers");
|
||||
|
||||
_("Resource object memebers");
|
||||
let res = new AsyncResource("http://localhost:8080/open");
|
||||
do_check_true(res.uri instanceof Ci.nsIURI);
|
||||
do_check_eq(res.uri.spec, "http://localhost:8080/open");
|
||||
do_check_eq(res.spec, "http://localhost:8080/open");
|
||||
do_check_eq(typeof res.headers, "object");
|
||||
do_check_eq(typeof res.authenticator, "object");
|
||||
// Initially res.data is null since we haven't performed a GET or
|
||||
// PUT/POST request yet.
|
||||
do_check_eq(res.data, null);
|
||||
|
||||
Utils.asyncChain(function (next) {
|
||||
|
||||
_("GET a non-password-protected resource");
|
||||
do_test_pending();
|
||||
res.get(ensureThrows(function (error, content) {
|
||||
do_check_eq(error, null);
|
||||
do_check_eq(content, "This path exists");
|
||||
do_check_eq(content.status, 200);
|
||||
do_check_true(content.success);
|
||||
// res.data has been updated with the result from the request
|
||||
do_check_eq(res.data, content);
|
||||
|
||||
// Since we didn't receive proper JSON data, accessing content.obj
|
||||
// will result in a SyntaxError from JSON.parse
|
||||
let didThrow = false;
|
||||
try {
|
||||
content.obj;
|
||||
} catch (ex) {
|
||||
didThrow = true;
|
||||
}
|
||||
do_check_true(didThrow);
|
||||
|
||||
do_test_finished();
|
||||
next();
|
||||
}));
|
||||
|
||||
}, function (next) {
|
||||
|
||||
_("GET a password protected resource (test that it'll fail w/o pass, no throw)");
|
||||
let res2 = new AsyncResource("http://localhost:8080/protected");
|
||||
do_test_pending();
|
||||
res2.get(ensureThrows(function (error, content) {
|
||||
do_check_eq(error, null);
|
||||
do_check_true(did401);
|
||||
do_check_eq(content, "This path exists and is protected - failed");
|
||||
do_check_eq(content.status, 401);
|
||||
do_check_false(content.success);
|
||||
do_test_finished();
|
||||
next();
|
||||
}));
|
||||
|
||||
}, function (next) {
|
||||
|
||||
_("GET a password protected resource");
|
||||
let auth = new BasicAuthenticator(new Identity("secret", "guest", "guest"));
|
||||
let res3 = new AsyncResource("http://localhost:8080/protected");
|
||||
res3.authenticator = auth;
|
||||
do_check_eq(res3.authenticator, auth);
|
||||
do_test_pending();
|
||||
res3.get(ensureThrows(function (error, content) {
|
||||
do_check_eq(error, null);
|
||||
do_check_eq(content, "This path exists and is protected");
|
||||
do_check_eq(content.status, 200);
|
||||
do_check_true(content.success);
|
||||
do_test_finished();
|
||||
next();
|
||||
}));
|
||||
|
||||
}, function (next) {
|
||||
|
||||
_("GET a non-existent resource (test that it'll fail, but not throw)");
|
||||
let res4 = new AsyncResource("http://localhost:8080/404");
|
||||
do_test_pending();
|
||||
res4.get(ensureThrows(function (error, content) {
|
||||
do_check_eq(error, null);
|
||||
do_check_eq(content, "File not found");
|
||||
do_check_eq(content.status, 404);
|
||||
do_check_false(content.success);
|
||||
|
||||
// Check some headers of the 404 response
|
||||
do_check_eq(content.headers.connection, "close");
|
||||
do_check_eq(content.headers.server, "httpd.js");
|
||||
do_check_eq(content.headers["content-length"], 14);
|
||||
|
||||
do_test_finished();
|
||||
next();
|
||||
}));
|
||||
|
||||
}, function (next) {
|
||||
|
||||
_("PUT to a resource (string)");
|
||||
do_test_pending();
|
||||
res_upload.put(JSON.stringify(sample_data), ensureThrows(function(error, content) {
|
||||
do_check_eq(error, null);
|
||||
do_check_eq(content, "Valid data upload via PUT");
|
||||
do_check_eq(content.status, 200);
|
||||
do_check_eq(res_upload.data, content);
|
||||
do_test_finished();
|
||||
next();
|
||||
}));
|
||||
|
||||
}, function (next) {
|
||||
|
||||
_("PUT to a resource (object)");
|
||||
do_test_pending();
|
||||
res_upload.put(sample_data, ensureThrows(function (error, content) {
|
||||
do_check_eq(error, null);
|
||||
do_check_eq(content, "Valid data upload via PUT");
|
||||
do_check_eq(content.status, 200);
|
||||
do_check_eq(res_upload.data, content);
|
||||
do_test_finished();
|
||||
next();
|
||||
}));
|
||||
|
||||
}, function (next) {
|
||||
|
||||
_("PUT without data arg (uses resource.data) (string)");
|
||||
do_test_pending();
|
||||
res_upload.data = JSON.stringify(sample_data);
|
||||
res_upload.put(ensureThrows(function (error, content) {
|
||||
do_check_eq(error, null);
|
||||
do_check_eq(content, "Valid data upload via PUT");
|
||||
do_check_eq(content.status, 200);
|
||||
do_check_eq(res_upload.data, content);
|
||||
do_test_finished();
|
||||
next();
|
||||
}));
|
||||
|
||||
}, function (next) {
|
||||
|
||||
_("PUT without data arg (uses resource.data) (object)");
|
||||
do_test_pending();
|
||||
res_upload.data = sample_data;
|
||||
res_upload.put(ensureThrows(function (error, content) {
|
||||
do_check_eq(error, null);
|
||||
do_check_eq(content, "Valid data upload via PUT");
|
||||
do_check_eq(content.status, 200);
|
||||
do_check_eq(res_upload.data, content);
|
||||
do_test_finished();
|
||||
next();
|
||||
}));
|
||||
|
||||
}, function (next) {
|
||||
|
||||
_("POST to a resource (string)");
|
||||
do_test_pending();
|
||||
res_upload.post(JSON.stringify(sample_data), ensureThrows(function (error, content) {
|
||||
do_check_eq(error, null);
|
||||
do_check_eq(content, "Valid data upload via POST");
|
||||
do_check_eq(content.status, 200);
|
||||
do_check_eq(res_upload.data, content);
|
||||
do_test_finished();
|
||||
next();
|
||||
}));
|
||||
|
||||
}, function (next) {
|
||||
|
||||
_("POST to a resource (object)");
|
||||
do_test_pending();
|
||||
res_upload.post(sample_data, ensureThrows(function (error, content) {
|
||||
do_check_eq(error, null);
|
||||
do_check_eq(content, "Valid data upload via POST");
|
||||
do_check_eq(content.status, 200);
|
||||
do_check_eq(res_upload.data, content);
|
||||
do_test_finished();
|
||||
next();
|
||||
}));
|
||||
|
||||
}, function (next) {
|
||||
|
||||
_("POST without data arg (uses resource.data) (string)");
|
||||
do_test_pending();
|
||||
res_upload.data = JSON.stringify(sample_data);
|
||||
res_upload.post(ensureThrows(function (error, content) {
|
||||
do_check_eq(error, null);
|
||||
do_check_eq(content, "Valid data upload via POST");
|
||||
do_check_eq(content.status, 200);
|
||||
do_check_eq(res_upload.data, content);
|
||||
do_test_finished();
|
||||
next();
|
||||
}));
|
||||
|
||||
}, function (next) {
|
||||
|
||||
_("POST without data arg (uses resource.data) (object)");
|
||||
do_test_pending();
|
||||
res_upload.data = sample_data;
|
||||
res_upload.post(ensureThrows(function (error, content) {
|
||||
do_check_eq(error, null);
|
||||
do_check_eq(content, "Valid data upload via POST");
|
||||
do_check_eq(content.status, 200);
|
||||
do_check_eq(res_upload.data, content);
|
||||
do_test_finished();
|
||||
next();
|
||||
}));
|
||||
|
||||
}, function (next) {
|
||||
|
||||
_("DELETE a resource");
|
||||
do_test_pending();
|
||||
let res6 = new AsyncResource("http://localhost:8080/delete");
|
||||
res6.delete(ensureThrows(function (error, content) {
|
||||
do_check_eq(error, null);
|
||||
do_check_eq(content, "This resource has been deleted");
|
||||
do_check_eq(content.status, 200);
|
||||
do_test_finished();
|
||||
next();
|
||||
}));
|
||||
|
||||
}, function (next) {
|
||||
|
||||
_("JSON conversion of response body");
|
||||
do_test_pending();
|
||||
let res7 = new AsyncResource("http://localhost:8080/json");
|
||||
res7.get(ensureThrows(function (error, content) {
|
||||
do_check_eq(error, null);
|
||||
do_check_eq(content, JSON.stringify(sample_data));
|
||||
do_check_eq(content.status, 200);
|
||||
do_check_eq(JSON.stringify(content.obj), JSON.stringify(sample_data));
|
||||
do_test_finished();
|
||||
next();
|
||||
}));
|
||||
|
||||
}, function (next) {
|
||||
|
||||
_("X-Weave-Timestamp header updates Resource.serverTime");
|
||||
do_test_pending();
|
||||
// Before having received any response containing the
|
||||
// X-Weave-Timestamp header, Resource.serverTime is null.
|
||||
do_check_eq(Resource.serverTime, null);
|
||||
let res8 = new AsyncResource("http://localhost:8080/timestamp");
|
||||
res8.get(ensureThrows(function (error, content) {
|
||||
do_check_eq(error, null);
|
||||
do_check_eq(Resource.serverTime, TIMESTAMP);
|
||||
do_test_finished();
|
||||
next();
|
||||
}));
|
||||
|
||||
}, function (next) {
|
||||
|
||||
_("GET: no special request headers");
|
||||
do_test_pending();
|
||||
res_headers.get(ensureThrows(function (error, content) {
|
||||
do_check_eq(error, null);
|
||||
do_check_eq(content, '{}');
|
||||
do_test_finished();
|
||||
next();
|
||||
}));
|
||||
|
||||
}, function (next) {
|
||||
|
||||
_("PUT: Content-Type defaults to text/plain");
|
||||
do_test_pending();
|
||||
res_headers.put('data', ensureThrows(function (error, content) {
|
||||
do_check_eq(error, null);
|
||||
do_check_eq(content, JSON.stringify({"content-type": "text/plain"}));
|
||||
do_test_finished();
|
||||
next();
|
||||
}));
|
||||
|
||||
}, function (next) {
|
||||
|
||||
_("POST: Content-Type defaults to text/plain");
|
||||
do_test_pending();
|
||||
res_headers.post('data', ensureThrows(function (error, content) {
|
||||
do_check_eq(error, null);
|
||||
do_check_eq(content, JSON.stringify({"content-type": "text/plain"}));
|
||||
do_test_finished();
|
||||
next();
|
||||
}));
|
||||
|
||||
}, function (next) {
|
||||
|
||||
_("setHeader(): setting simple header");
|
||||
do_test_pending();
|
||||
res_headers.setHeader('X-What-Is-Weave', 'awesome');
|
||||
do_check_eq(res_headers.headers['x-what-is-weave'], 'awesome');
|
||||
res_headers.get(ensureThrows(function (error, content) {
|
||||
do_check_eq(error, null);
|
||||
do_check_eq(content, JSON.stringify({"x-what-is-weave": "awesome"}));
|
||||
do_test_finished();
|
||||
next();
|
||||
}));
|
||||
|
||||
}, function (next) {
|
||||
|
||||
_("setHeader(): setting multiple headers, overwriting existing header");
|
||||
do_test_pending();
|
||||
res_headers.setHeader('X-WHAT-is-Weave', 'more awesomer',
|
||||
'X-Another-Header', 'hello world');
|
||||
do_check_eq(res_headers.headers['x-what-is-weave'], 'more awesomer');
|
||||
do_check_eq(res_headers.headers['x-another-header'], 'hello world');
|
||||
res_headers.get(ensureThrows(function (error, content) {
|
||||
do_check_eq(error, null);
|
||||
do_check_eq(content, JSON.stringify({"x-another-header": "hello world",
|
||||
"x-what-is-weave": "more awesomer"}));
|
||||
do_test_finished();
|
||||
next();
|
||||
}));
|
||||
|
||||
}, function (next) {
|
||||
|
||||
_("Setting headers object");
|
||||
do_test_pending();
|
||||
res_headers.headers = {};
|
||||
res_headers.get(ensureThrows(function (error, content) {
|
||||
do_check_eq(error, null);
|
||||
do_check_eq(content, "{}");
|
||||
do_test_finished();
|
||||
next();
|
||||
}));
|
||||
|
||||
}, function (next) {
|
||||
|
||||
_("PUT: override default Content-Type");
|
||||
do_test_pending();
|
||||
res_headers.setHeader('Content-Type', 'application/foobar');
|
||||
do_check_eq(res_headers.headers['content-type'], 'application/foobar');
|
||||
res_headers.put('data', ensureThrows(function (error, content) {
|
||||
do_check_eq(error, null);
|
||||
do_check_eq(content, JSON.stringify({"content-type": "application/foobar"}));
|
||||
do_test_finished();
|
||||
next();
|
||||
}));
|
||||
|
||||
}, function (next) {
|
||||
|
||||
_("POST: override default Content-Type");
|
||||
do_test_pending();
|
||||
res_headers.post('data', ensureThrows(function (error, content) {
|
||||
do_check_eq(error, null);
|
||||
do_check_eq(content, JSON.stringify({"content-type": "application/foobar"}));
|
||||
do_test_finished();
|
||||
next();
|
||||
}));
|
||||
|
||||
}, function (next) {
|
||||
|
||||
_("X-Weave-Backoff header notifies observer");
|
||||
let backoffInterval;
|
||||
function onBackoff(subject, data) {
|
||||
backoffInterval = subject;
|
||||
}
|
||||
Observers.add("weave:service:backoff:interval", onBackoff);
|
||||
|
||||
do_test_pending();
|
||||
let res10 = new AsyncResource("http://localhost:8080/backoff");
|
||||
res10.get(ensureThrows(function (error, content) {
|
||||
do_check_eq(error, null);
|
||||
do_check_eq(backoffInterval, 600);
|
||||
do_test_finished();
|
||||
next();
|
||||
}));
|
||||
|
||||
}, function (next) {
|
||||
|
||||
_("X-Weave-Quota-Remaining header notifies observer on successful requests.");
|
||||
do_test_pending();
|
||||
let res10 = new AsyncResource("http://localhost:8080/quota-error");
|
||||
res10.get(ensureThrows(function (error, content) {
|
||||
do_check_eq(error, null);
|
||||
do_check_eq(content.status, 400);
|
||||
do_check_eq(quotaValue, undefined); // HTTP 400, so no observer notification.
|
||||
do_test_finished();
|
||||
next();
|
||||
}));
|
||||
|
||||
}, function (next) {
|
||||
|
||||
do_test_pending();
|
||||
let res10 = new AsyncResource("http://localhost:8080/quota-notice");
|
||||
res10.get(ensureThrows(function (error, content) {
|
||||
do_check_eq(error, null);
|
||||
do_check_eq(content.status, 200);
|
||||
do_check_eq(quotaValue, 1048576);
|
||||
do_test_finished();
|
||||
next();
|
||||
}));
|
||||
|
||||
}, function (next) {
|
||||
|
||||
_("Error handling in ChannelListener etc. preserves exception information");
|
||||
do_test_pending();
|
||||
let res11 = new AsyncResource("http://localhost:12345/does/not/exist");
|
||||
res11.get(ensureThrows(function (error, content) {
|
||||
do_check_neq(error, null);
|
||||
do_check_eq(error.message, "NS_ERROR_CONNECTION_REFUSED");
|
||||
do_check_eq(typeof error.stack, "string");
|
||||
do_test_finished();
|
||||
next();
|
||||
}));
|
||||
|
||||
}, function (next) {
|
||||
|
||||
let redirRequest;
|
||||
let redirToOpen = function(subject) {
|
||||
subject.newUri = "http://localhost:8080/open";
|
||||
redirRequest = subject;
|
||||
};
|
||||
Observers.add("weave:resource:status:401", redirToOpen);
|
||||
|
||||
_("Notification of 401 can redirect to another uri");
|
||||
did401 = false;
|
||||
do_test_pending();
|
||||
let res12 = new AsyncResource("http://localhost:8080/protected");
|
||||
res12.get(ensureThrows(function (error, content) {
|
||||
do_check_eq(error, null);
|
||||
do_check_eq(res12.spec, "http://localhost:8080/open");
|
||||
do_check_eq(content, "This path exists");
|
||||
do_check_eq(content.status, 200);
|
||||
do_check_true(content.success);
|
||||
do_check_eq(res.data, content);
|
||||
do_check_true(did401);
|
||||
do_check_eq(redirRequest.response, "This path exists and is protected - failed");
|
||||
do_check_eq(redirRequest.response.status, 401);
|
||||
do_check_false(redirRequest.response.success);
|
||||
|
||||
Observers.remove("weave:resource:status:401", redirToOpen);
|
||||
do_test_finished();
|
||||
next();
|
||||
}));
|
||||
|
||||
}, function (next) {
|
||||
|
||||
_("Removing the observer should result in the original 401");
|
||||
did401 = false;
|
||||
let res13 = new AsyncResource("http://localhost:8080/protected");
|
||||
res13.get(ensureThrows(function (error, content) {
|
||||
do_check_eq(error, null);
|
||||
do_check_true(did401);
|
||||
do_check_eq(content, "This path exists and is protected - failed");
|
||||
do_check_eq(content.status, 401);
|
||||
do_check_false(content.success);
|
||||
do_test_finished();
|
||||
next();
|
||||
}));
|
||||
|
||||
}, function (next) {
|
||||
|
||||
// Don't quit test harness before server shuts down.
|
||||
server.stop(do_test_finished);
|
||||
|
||||
})();
|
||||
|
||||
}
|
|
@ -43,18 +43,25 @@ function test_syncID() {
|
|||
}
|
||||
|
||||
function test_lastSync() {
|
||||
_("SyncEngine.lastSync corresponds to preference and stores floats");
|
||||
_("SyncEngine.lastSync and SyncEngine.lastSyncLocal correspond to preferences");
|
||||
let engine = makeSteamEngine();
|
||||
try {
|
||||
// Ensure pristine environment
|
||||
do_check_eq(Svc.Prefs.get("steam.lastSync"), undefined);
|
||||
do_check_eq(engine.lastSync, 0);
|
||||
do_check_eq(Svc.Prefs.get("steam.lastSyncLocal"), undefined);
|
||||
do_check_eq(engine.lastSyncLocal, 0);
|
||||
|
||||
// Floats are properly stored as floats and synced with the preference
|
||||
engine.lastSync = 123.45;
|
||||
do_check_eq(engine.lastSync, 123.45);
|
||||
do_check_eq(Svc.Prefs.get("steam.lastSync"), "123.45");
|
||||
|
||||
// Integer is properly stored
|
||||
engine.lastSyncLocal = 67890;
|
||||
do_check_eq(engine.lastSyncLocal, 67890);
|
||||
do_check_eq(Svc.Prefs.get("steam.lastSyncLocal"), "67890");
|
||||
|
||||
// resetLastSync() resets the value (and preference) to 0
|
||||
engine.resetLastSync();
|
||||
do_check_eq(engine.lastSync, 0);
|
||||
|
@ -64,48 +71,20 @@ function test_lastSync() {
|
|||
}
|
||||
}
|
||||
|
||||
function test_toFetch() {
|
||||
_("SyncEngine.toFetch corresponds to file on disk");
|
||||
let engine = makeSteamEngine();
|
||||
try {
|
||||
// Ensure pristine environment
|
||||
do_check_eq(engine.toFetch.length, 0);
|
||||
|
||||
// Write file to disk
|
||||
let toFetch = [Utils.makeGUID(), Utils.makeGUID(), Utils.makeGUID()];
|
||||
engine.toFetch = toFetch;
|
||||
do_check_eq(engine.toFetch, toFetch);
|
||||
let fakefile = syncTesting.fakeFilesystem.fakeContents[
|
||||
"weave/toFetch/steam.json"];
|
||||
do_check_eq(fakefile, JSON.stringify(toFetch));
|
||||
|
||||
// Read file from disk
|
||||
toFetch = [Utils.makeGUID(), Utils.makeGUID()];
|
||||
syncTesting.fakeFilesystem.fakeContents["weave/toFetch/steam.json"]
|
||||
= JSON.stringify(toFetch);
|
||||
engine.loadToFetch();
|
||||
do_check_eq(engine.toFetch.length, 2);
|
||||
do_check_eq(engine.toFetch[0], toFetch[0]);
|
||||
do_check_eq(engine.toFetch[1], toFetch[1]);
|
||||
} finally {
|
||||
syncTesting = new SyncTestingInfrastructure(makeSteamEngine);
|
||||
}
|
||||
}
|
||||
|
||||
function test_resetClient() {
|
||||
_("SyncEngine.resetClient resets lastSync and toFetch");
|
||||
_("SyncEngine.resetClient resets lastSync");
|
||||
let engine = makeSteamEngine();
|
||||
try {
|
||||
// Ensure pristine environment
|
||||
do_check_eq(Svc.Prefs.get("steam.lastSync"), undefined);
|
||||
do_check_eq(engine.toFetch.length, 0);
|
||||
do_check_eq(Svc.Prefs.get("steam.lastSyncLocal"), undefined);
|
||||
|
||||
engine.toFetch = [Utils.makeGUID(), Utils.makeGUID(), Utils.makeGUID()];
|
||||
engine.lastSync = 123.45;
|
||||
engine.lastSyncLocal = 67890;
|
||||
|
||||
engine.resetClient();
|
||||
do_check_eq(engine.lastSync, 0);
|
||||
do_check_eq(engine.toFetch.length, 0);
|
||||
do_check_eq(engine.lastSyncLocal, 0);
|
||||
} finally {
|
||||
syncTesting = new SyncTestingInfrastructure(makeSteamEngine);
|
||||
Svc.Prefs.resetBranch("");
|
||||
|
@ -128,14 +107,12 @@ function test_wipeServer() {
|
|||
|
||||
try {
|
||||
// Some data to reset.
|
||||
engine.toFetch = [Utils.makeGUID(), Utils.makeGUID(), Utils.makeGUID()];
|
||||
engine.lastSync = 123.45;
|
||||
|
||||
_("Wipe server data and reset client.");
|
||||
engine.wipeServer(true);
|
||||
do_check_eq(steamCollection.payload, undefined);
|
||||
do_check_eq(engine.lastSync, 0);
|
||||
do_check_eq(engine.toFetch.length, 0);
|
||||
|
||||
_("We passed a truthy arg earlier in which case it doesn't wipe the crypto collection.");
|
||||
do_check_eq(steamCrypto.payload, PAYLOAD);
|
||||
|
@ -153,7 +130,6 @@ function run_test() {
|
|||
test_url_attributes();
|
||||
test_syncID();
|
||||
test_lastSync();
|
||||
test_toFetch();
|
||||
test_resetClient();
|
||||
test_wipeServer();
|
||||
}
|
||||
|
|
|
@ -181,6 +181,7 @@ function test_syncStartup_emptyOrOutdatedGlobalsResetsSync() {
|
|||
do_check_true(!!collection.wbos.scotsman.payload);
|
||||
|
||||
engine.lastSync = Date.now() / 1000;
|
||||
engine.lastSyncLocal = Date.now();
|
||||
engine._syncStartup();
|
||||
|
||||
// The meta/global WBO has been filled with data about the engine
|
||||
|
@ -190,6 +191,7 @@ function test_syncStartup_emptyOrOutdatedGlobalsResetsSync() {
|
|||
|
||||
// Sync was reset and server data was wiped
|
||||
do_check_eq(engine.lastSync, 0);
|
||||
do_check_eq(engine.lastSyncLocal, 0);
|
||||
do_check_eq(collection.wbos.flying.payload, undefined);
|
||||
do_check_eq(collection.wbos.scotsman.payload, undefined);
|
||||
|
||||
|
@ -197,9 +199,6 @@ function test_syncStartup_emptyOrOutdatedGlobalsResetsSync() {
|
|||
do_check_true(!!crypto_steam.payload);
|
||||
do_check_true(!!crypto_steam.data.keyring);
|
||||
|
||||
// WBO IDs are added to tracker (they're all marked for uploading)
|
||||
do_check_eq(engine._tracker.changedIDs["rekolok"], 0);
|
||||
|
||||
} finally {
|
||||
server.stop(do_test_finished);
|
||||
Svc.Prefs.resetBranch("");
|
||||
|
@ -248,10 +247,12 @@ function test_syncStartup_metaGet404() {
|
|||
do_check_true(!!collection.wbos.scotsman.payload);
|
||||
|
||||
engine.lastSync = Date.now() / 1000;
|
||||
engine.lastSyncLocal = Date.now();
|
||||
engine._syncStartup();
|
||||
|
||||
_("Sync was reset and server data was wiped");
|
||||
do_check_eq(engine.lastSync, 0);
|
||||
do_check_eq(engine.lastSyncLocal, 0);
|
||||
do_check_eq(collection.wbos.flying.payload, undefined);
|
||||
do_check_eq(collection.wbos.scotsman.payload, undefined);
|
||||
|
||||
|
@ -363,6 +364,7 @@ function test_syncStartup_syncIDMismatchResetsClient() {
|
|||
do_check_eq(engine._tracker.changedIDs["rekolok"], undefined);
|
||||
|
||||
engine.lastSync = Date.now() / 1000;
|
||||
engine.lastSyncLocal = Date.now();
|
||||
engine._syncStartup();
|
||||
|
||||
// The engine has assumed the server's syncID
|
||||
|
@ -370,6 +372,7 @@ function test_syncStartup_syncIDMismatchResetsClient() {
|
|||
|
||||
// Sync was reset
|
||||
do_check_eq(engine.lastSync, 0);
|
||||
do_check_eq(engine.lastSyncLocal, 0);
|
||||
|
||||
} finally {
|
||||
server.stop(do_test_finished);
|
||||
|
@ -430,10 +433,12 @@ function test_syncStartup_badKeyWipesServerData() {
|
|||
do_check_true(!!collection.wbos.scotsman.payload);
|
||||
|
||||
engine.lastSync = Date.now() / 1000;
|
||||
engine.lastSyncLocal = Date.now();
|
||||
engine._syncStartup();
|
||||
|
||||
// Sync was reset and server data was wiped
|
||||
do_check_eq(engine.lastSync, 0);
|
||||
do_check_eq(engine.lastSyncLocal, 0);
|
||||
do_check_eq(collection.wbos.flying.payload, undefined);
|
||||
do_check_eq(collection.wbos.scotsman.payload, undefined);
|
||||
|
||||
|
@ -473,7 +478,6 @@ function test_processIncoming_emptyServer() {
|
|||
// Merely ensure that this code path is run without any errors
|
||||
engine._processIncoming();
|
||||
do_check_eq(engine.lastSync, 0);
|
||||
do_check_eq(engine.toFetch.length, 0);
|
||||
|
||||
} finally {
|
||||
server.stop(do_test_finished);
|
||||
|
@ -521,6 +525,10 @@ function test_processIncoming_createFromServer() {
|
|||
createAndUploadSymKey("http://localhost:8080/1.0/foo/storage/crypto/steam");
|
||||
|
||||
let engine = makeSteamEngine();
|
||||
let meta_global = Records.set(engine.metaURL, new WBORecord(engine.metaURL));
|
||||
meta_global.payload.engines = {steam: {version: engine.version,
|
||||
syncID: engine.syncID}};
|
||||
|
||||
try {
|
||||
|
||||
// Confirm initial environment
|
||||
|
@ -531,6 +539,7 @@ function test_processIncoming_createFromServer() {
|
|||
do_check_eq(engine._store.items['../pathological'], undefined);
|
||||
do_check_eq(engine._store.items.wrong_keyuri, undefined);
|
||||
|
||||
engine._syncStartup();
|
||||
engine._processIncoming();
|
||||
|
||||
// Timestamps of last sync and last server modification are set.
|
||||
|
@ -626,6 +635,10 @@ function test_processIncoming_reconcile() {
|
|||
// This record has been changed 2 mins later than the one on the server
|
||||
engine._tracker.addChangedID('olderidentical', Date.now()/1000);
|
||||
|
||||
let meta_global = Records.set(engine.metaURL, new WBORecord(engine.metaURL));
|
||||
meta_global.payload.engines = {steam: {version: engine.version,
|
||||
syncID: engine.syncID}};
|
||||
|
||||
try {
|
||||
|
||||
// Confirm initial environment
|
||||
|
@ -636,7 +649,7 @@ function test_processIncoming_reconcile() {
|
|||
do_check_eq(engine._store.items.nukeme, "Nuke me!");
|
||||
do_check_true(engine._tracker.changedIDs['olderidentical'] > 0);
|
||||
|
||||
engine._delete = {}; // normally set up by _syncStartup
|
||||
engine._syncStartup();
|
||||
engine._processIncoming();
|
||||
|
||||
// Timestamps of last sync and last server modification are set.
|
||||
|
@ -676,14 +689,22 @@ function test_processIncoming_reconcile() {
|
|||
}
|
||||
|
||||
|
||||
function test_processIncoming_fetchNum() {
|
||||
_("SyncEngine._processIncoming doesn't fetch everything at ones on mobile clients");
|
||||
function test_processIncoming_mobile_batchSize() {
|
||||
_("SyncEngine._processIncoming doesn't fetch everything at once on mobile clients");
|
||||
|
||||
Svc.Prefs.set("clusterURL", "http://localhost:8080/");
|
||||
Svc.Prefs.set("username", "foo");
|
||||
Svc.Prefs.set("client.type", "mobile");
|
||||
let crypto_steam = new ServerWBO('steam');
|
||||
|
||||
// A collection that logs each GET
|
||||
let collection = new ServerCollection();
|
||||
collection.get_log = [];
|
||||
collection._get = collection.get;
|
||||
collection.get = function (options) {
|
||||
this.get_log.push(options);
|
||||
return this._get(options);
|
||||
};
|
||||
|
||||
// Let's create some 234 server side records. They're all at least
|
||||
// 10 minutes old.
|
||||
|
@ -704,75 +725,38 @@ function test_processIncoming_fetchNum() {
|
|||
createAndUploadSymKey("http://localhost:8080/1.0/foo/storage/crypto/steam");
|
||||
|
||||
let engine = makeSteamEngine();
|
||||
let meta_global = Records.set(engine.metaURL, new WBORecord(engine.metaURL));
|
||||
meta_global.payload.engines = {steam: {version: engine.version,
|
||||
syncID: engine.syncID}};
|
||||
|
||||
try {
|
||||
|
||||
// On a mobile client, the first sync will only get the first 50
|
||||
// objects from the server
|
||||
// On a mobile client, we get new records from the server in batches of 50.
|
||||
engine._syncStartup();
|
||||
engine._processIncoming();
|
||||
do_check_eq([id for (id in engine._store.items)].length, 50);
|
||||
do_check_eq([id for (id in engine._store.items)].length, 234);
|
||||
do_check_true('record-no-0' in engine._store.items);
|
||||
do_check_true('record-no-49' in engine._store.items);
|
||||
do_check_eq(engine.toFetch.length, 234 - 50);
|
||||
|
||||
|
||||
// The next sync will get another 50 objects, assuming the server
|
||||
// hasn't got any new data.
|
||||
engine._processIncoming();
|
||||
do_check_eq([id for (id in engine._store.items)].length, 100);
|
||||
do_check_true('record-no-50' in engine._store.items);
|
||||
do_check_true('record-no-99' in engine._store.items);
|
||||
do_check_eq(engine.toFetch.length, 234 - 100);
|
||||
|
||||
|
||||
// Now let's say there are some new items on the server
|
||||
for (i=0; i < 5; i++) {
|
||||
let id = 'new-record-no-' + i;
|
||||
let payload = encryptPayload({id: id, denomination: "New record No. " + i});
|
||||
let wbo = new ServerWBO(id, payload);
|
||||
wbo.modified = Date.now()/1000 - 60*i;
|
||||
collection.wbos[id] = wbo;
|
||||
}
|
||||
// Let's tell the engine the server has got newer data. This is
|
||||
// normally done by the WeaveSvc after retrieving info/collections.
|
||||
engine.lastModified = Date.now() / 1000 + 1;
|
||||
|
||||
// Now we'll fetch another 50 items, but 5 of those are the new
|
||||
// ones, so we've only fetched another 45 of the older ones.
|
||||
engine._processIncoming();
|
||||
do_check_eq([id for (id in engine._store.items)].length, 150);
|
||||
do_check_true('new-record-no-0' in engine._store.items);
|
||||
do_check_true('new-record-no-4' in engine._store.items);
|
||||
do_check_true('record-no-100' in engine._store.items);
|
||||
do_check_true('record-no-144' in engine._store.items);
|
||||
do_check_eq(engine.toFetch.length, 234 - 100 - 45);
|
||||
|
||||
|
||||
// Now let's modify a few existing records on the server so that
|
||||
// they have to be refetched.
|
||||
collection.wbos['record-no-3'].modified = Date.now()/1000 + 1;
|
||||
collection.wbos['record-no-41'].modified = Date.now()/1000 + 1;
|
||||
collection.wbos['record-no-122'].modified = Date.now()/1000 + 1;
|
||||
|
||||
// Once again we'll tell the engine that the server's got newer data
|
||||
// and once again we'll fetch 50 items, but 3 of those are the
|
||||
// existing records, so we're only fetching 47 new ones.
|
||||
engine.lastModified = Date.now() / 1000 + 2;
|
||||
engine._processIncoming();
|
||||
do_check_eq([id for (id in engine._store.items)].length, 197);
|
||||
do_check_true('record-no-145' in engine._store.items);
|
||||
do_check_true('record-no-191' in engine._store.items);
|
||||
do_check_eq(engine.toFetch.length, 234 - 100 - 45 - 47);
|
||||
|
||||
|
||||
// Finally let's fetch the rest, making sure that will fetch
|
||||
// everything up to the last record.
|
||||
while(engine.toFetch.length) {
|
||||
engine._processIncoming();
|
||||
}
|
||||
do_check_eq([id for (id in engine._store.items)].length, 234 + 5);
|
||||
do_check_true('record-no-233' in engine._store.items);
|
||||
|
||||
// Verify that the right number of GET requests with the right
|
||||
// kind of parameters were made.
|
||||
do_check_eq(collection.get_log.length,
|
||||
Math.ceil(234 / MOBILE_BATCH_SIZE) + 1);
|
||||
do_check_eq(collection.get_log[0].full, 1);
|
||||
do_check_eq(collection.get_log[0].limit, MOBILE_BATCH_SIZE);
|
||||
do_check_eq(collection.get_log[1].full, undefined);
|
||||
do_check_eq(collection.get_log[1].limit, undefined);
|
||||
for (let i = 1; i <= Math.floor(234 / MOBILE_BATCH_SIZE); i++) {
|
||||
do_check_eq(collection.get_log[i+1].full, 1);
|
||||
do_check_eq(collection.get_log[i+1].limit, undefined);
|
||||
if (i < Math.floor(234 / MOBILE_BATCH_SIZE))
|
||||
do_check_eq(collection.get_log[i+1].ids.length, MOBILE_BATCH_SIZE);
|
||||
else
|
||||
do_check_eq(collection.get_log[i+1].ids.length, 234 % MOBILE_BATCH_SIZE);
|
||||
}
|
||||
|
||||
} finally {
|
||||
server.stop(do_test_finished);
|
||||
Svc.Prefs.resetBranch("");
|
||||
|
@ -804,20 +788,29 @@ function test_uploadOutgoing_toEmptyServer() {
|
|||
createAndUploadSymKey("http://localhost:8080/1.0/foo/storage/crypto/steam");
|
||||
|
||||
let engine = makeSteamEngine();
|
||||
engine.lastSync = 123; // needs to be non-zero so that tracker is queried
|
||||
engine._store.items = {flying: "LNER Class A3 4472",
|
||||
scotsman: "Flying Scotsman"};
|
||||
// Mark one of these records as changed
|
||||
engine._tracker.addChangedID('scotsman', 0);
|
||||
|
||||
let meta_global = Records.set(engine.metaURL, new WBORecord(engine.metaURL));
|
||||
meta_global.payload.engines = {steam: {version: engine.version,
|
||||
syncID: engine.syncID}};
|
||||
|
||||
try {
|
||||
|
||||
// Confirm initial environment
|
||||
do_check_eq(engine.lastSyncLocal, 0);
|
||||
do_check_eq(collection.wbos.flying.payload, undefined);
|
||||
do_check_eq(collection.wbos.scotsman.payload, undefined);
|
||||
do_check_eq(engine._tracker.changedIDs['scotsman'], 0);
|
||||
|
||||
engine._syncStartup();
|
||||
engine._uploadOutgoing();
|
||||
|
||||
// Local timestamp has been set.
|
||||
do_check_true(engine.lastSyncLocal > 0);
|
||||
|
||||
// Ensure the marked record ('scotsman') has been uploaded and is
|
||||
// no longer marked.
|
||||
do_check_eq(collection.wbos.flying.payload, undefined);
|
||||
|
@ -859,10 +852,11 @@ function test_uploadOutgoing_failed() {
|
|||
createAndUploadSymKey("http://localhost:8080/1.0/foo/storage/crypto/steam");
|
||||
|
||||
let engine = makeSteamEngine();
|
||||
engine.lastSync = 123; // needs to be non-zero so that tracker is queried
|
||||
engine._store.items = {flying: "LNER Class A3 4472",
|
||||
scotsman: "Flying Scotsman",
|
||||
peppercorn: "Peppercorn Class"};
|
||||
// Mark one of these records as changed
|
||||
// Mark these records as changed
|
||||
const FLYING_CHANGED = 12345;
|
||||
const SCOTSMAN_CHANGED = 23456;
|
||||
const PEPPERCORN_CHANGED = 34567;
|
||||
|
@ -870,16 +864,25 @@ function test_uploadOutgoing_failed() {
|
|||
engine._tracker.addChangedID('scotsman', SCOTSMAN_CHANGED);
|
||||
engine._tracker.addChangedID('peppercorn', PEPPERCORN_CHANGED);
|
||||
|
||||
let meta_global = Records.set(engine.metaURL, new WBORecord(engine.metaURL));
|
||||
meta_global.payload.engines = {steam: {version: engine.version,
|
||||
syncID: engine.syncID}};
|
||||
|
||||
try {
|
||||
|
||||
// Confirm initial environment
|
||||
do_check_eq(engine.lastSyncLocal, 0);
|
||||
do_check_eq(collection.wbos.flying.payload, undefined);
|
||||
do_check_eq(engine._tracker.changedIDs['flying'], FLYING_CHANGED);
|
||||
do_check_eq(engine._tracker.changedIDs['scotsman'], SCOTSMAN_CHANGED);
|
||||
do_check_eq(engine._tracker.changedIDs['peppercorn'], PEPPERCORN_CHANGED);
|
||||
|
||||
engine._syncStartup();
|
||||
engine._uploadOutgoing();
|
||||
|
||||
// Local timestamp has been set.
|
||||
do_check_true(engine.lastSyncLocal > 0);
|
||||
|
||||
// Ensure the 'flying' record has been uploaded and is no longer marked.
|
||||
do_check_true(!!collection.wbos.flying.payload);
|
||||
do_check_eq(engine._tracker.changedIDs['flying'], undefined);
|
||||
|
@ -925,6 +928,10 @@ function test_uploadOutgoing_MAX_UPLOAD_RECORDS() {
|
|||
collection.wbos[id] = new ServerWBO(id);
|
||||
}
|
||||
|
||||
let meta_global = Records.set(engine.metaURL, new WBORecord(engine.metaURL));
|
||||
meta_global.payload.engines = {steam: {version: engine.version,
|
||||
syncID: engine.syncID}};
|
||||
|
||||
let server = sync_httpd_setup({
|
||||
"/1.0/foo/storage/crypto/steam": crypto_steam.handler(),
|
||||
"/1.0/foo/storage/steam": collection.handler()
|
||||
|
@ -938,6 +945,7 @@ function test_uploadOutgoing_MAX_UPLOAD_RECORDS() {
|
|||
// Confirm initial environment
|
||||
do_check_eq(noOfUploads, 0);
|
||||
|
||||
engine._syncStartup();
|
||||
engine._uploadOutgoing();
|
||||
|
||||
// Ensure all records have been uploaded
|
||||
|
@ -1087,6 +1095,72 @@ function test_syncFinish_deleteLotsInBatches() {
|
|||
}
|
||||
}
|
||||
|
||||
function test_sync_rollback() {
|
||||
_("SyncEngine.sync() rolls back tracker's changedIDs when syncing fails.");
|
||||
Svc.Prefs.set("clusterURL", "http://localhost:8080/");
|
||||
Svc.Prefs.set("username", "foo");
|
||||
|
||||
// Set up a server without a "steam" collection handler, so sync will fail.
|
||||
let crypto_steam = new ServerWBO('steam');
|
||||
let server = sync_httpd_setup({
|
||||
"/1.0/foo/storage/crypto/steam": crypto_steam.handler()
|
||||
});
|
||||
do_test_pending();
|
||||
createAndUploadKeypair();
|
||||
createAndUploadSymKey("http://localhost:8080/1.0/foo/storage/crypto/steam");
|
||||
|
||||
let engine = makeSteamEngine();
|
||||
engine.lastSync = 123; // needs to be non-zero so that tracker is queried
|
||||
engine.lastSyncLocal = 456;
|
||||
engine._store.items = {flying: "LNER Class A3 4472",
|
||||
scotsman: "Flying Scotsman",
|
||||
peppercorn: "Peppercorn Class"};
|
||||
|
||||
// Mark these records as changed
|
||||
const FLYING_CHANGED = 12345;
|
||||
const SCOTSMAN_CHANGED = 23456;
|
||||
const PEPPERCORN_CHANGED = 34567;
|
||||
engine._tracker.addChangedID('flying', FLYING_CHANGED);
|
||||
engine._tracker.addChangedID('scotsman', SCOTSMAN_CHANGED);
|
||||
engine._tracker.addChangedID('peppercorn', PEPPERCORN_CHANGED);
|
||||
|
||||
let meta_global = Records.set(engine.metaURL, new WBORecord(engine.metaURL));
|
||||
meta_global.payload.engines = {steam: {version: engine.version,
|
||||
syncID: engine.syncID}};
|
||||
|
||||
|
||||
try {
|
||||
|
||||
// Confirm initial environment
|
||||
do_check_eq(engine.lastSyncLocal, 456);
|
||||
do_check_eq(engine._tracker.changedIDs['flying'], FLYING_CHANGED);
|
||||
do_check_eq(engine._tracker.changedIDs['scotsman'], SCOTSMAN_CHANGED);
|
||||
do_check_eq(engine._tracker.changedIDs['peppercorn'], PEPPERCORN_CHANGED);
|
||||
|
||||
engine.enabled = true;
|
||||
let error;
|
||||
try {
|
||||
engine.sync();
|
||||
} catch (ex) {
|
||||
error = ex;
|
||||
}
|
||||
do_check_true(!!error);
|
||||
|
||||
// Verify that the tracker state and local timestamp has been rolled back.
|
||||
do_check_eq(engine.lastSyncLocal, 456);
|
||||
do_check_eq(engine._tracker.changedIDs['flying'], FLYING_CHANGED);
|
||||
do_check_eq(engine._tracker.changedIDs['scotsman'], SCOTSMAN_CHANGED);
|
||||
do_check_eq(engine._tracker.changedIDs['peppercorn'], PEPPERCORN_CHANGED);
|
||||
|
||||
} finally {
|
||||
server.stop(do_test_finished);
|
||||
Svc.Prefs.resetBranch("");
|
||||
Records.clearCache();
|
||||
CryptoMetas.clearCache();
|
||||
syncTesting = new SyncTestingInfrastructure(makeSteamEngine);
|
||||
}
|
||||
}
|
||||
|
||||
function test_canDecrypt_noCryptoMeta() {
|
||||
_("SyncEngine.canDecrypt returns false if the engine fails to decrypt items on the server, e.g. due to a missing crypto key.");
|
||||
Svc.Prefs.set("clusterURL", "http://localhost:8080/");
|
||||
|
@ -1162,13 +1236,14 @@ function run_test() {
|
|||
test_processIncoming_emptyServer();
|
||||
test_processIncoming_createFromServer();
|
||||
test_processIncoming_reconcile();
|
||||
test_processIncoming_fetchNum();
|
||||
test_processIncoming_mobile_batchSize();
|
||||
test_uploadOutgoing_toEmptyServer();
|
||||
test_uploadOutgoing_failed();
|
||||
test_uploadOutgoing_MAX_UPLOAD_RECORDS();
|
||||
test_syncFinish_noDelete();
|
||||
test_syncFinish_deleteByIds();
|
||||
test_syncFinish_deleteLotsInBatches();
|
||||
test_sync_rollback();
|
||||
test_canDecrypt_noCryptoMeta();
|
||||
test_canDecrypt_true();
|
||||
}
|
||||
|
|
|
@ -43,11 +43,12 @@ function fakeSvcSession() {
|
|||
|
||||
function run_test() {
|
||||
let engine = new TabEngine();
|
||||
engine._store.wipe();
|
||||
|
||||
_("Verify we have an empty tracker to work with");
|
||||
_("We assume that tabs have changed at startup.");
|
||||
let tracker = engine._tracker;
|
||||
do_check_eq([id for (id in tracker.changedIDs)].length, 0);
|
||||
do_check_true(tracker.modified);
|
||||
do_check_true(Utils.deepEquals([id for (id in engine.getChangedIDs())],
|
||||
[Clients.localID]));
|
||||
|
||||
let logs;
|
||||
|
||||
|
@ -83,15 +84,28 @@ function run_test() {
|
|||
logs = fakeSvcSession();
|
||||
let idx = 0;
|
||||
for each (let evttype in ["TabOpen", "TabClose", "TabSelect"]) {
|
||||
// Pretend we just synced.
|
||||
tracker.clearChangedIDs();
|
||||
do_check_false(tracker.modified);
|
||||
|
||||
// Send a fake tab event
|
||||
tracker.onTab({type: evttype , originalTarget: evttype});
|
||||
do_check_eq([id for (id in tracker.changedIDs)].length, 1);
|
||||
do_check_true(tracker.modified);
|
||||
do_check_true(Utils.deepEquals([id for (id in engine.getChangedIDs())],
|
||||
[Clients.localID]));
|
||||
do_check_eq(logs.length, idx+1);
|
||||
do_check_eq(logs[idx].target, evttype);
|
||||
do_check_eq(logs[idx].prop, "weaveLastUsed");
|
||||
do_check_true(typeof logs[idx].value == "number");
|
||||
idx++;
|
||||
}
|
||||
|
||||
// Pretend we just synced.
|
||||
tracker.clearChangedIDs();
|
||||
do_check_false(tracker.modified);
|
||||
|
||||
tracker.onTab({type: "pageshow", originalTarget: "pageshow"});
|
||||
do_check_eq([id for (id in tracker.changedIDs)].length, 1);
|
||||
do_check_true(Utils.deepEquals([id for (id in engine.getChangedIDs())],
|
||||
[Clients.localID]));
|
||||
do_check_eq(logs.length, idx); // test that setTabValue isn't called
|
||||
}
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
Cu.import("resource://services-sync/util.js");
|
||||
|
||||
function run_test() {
|
||||
_("Chain a few async methods, making sure the 'this' object is correct.");
|
||||
|
||||
let methods = {
|
||||
save: function(x, callback) {
|
||||
this.x = x;
|
||||
callback(x);
|
||||
},
|
||||
addX: function(x, callback) {
|
||||
callback(x + this.x);
|
||||
},
|
||||
double: function(x, callback) {
|
||||
callback(x * 2);
|
||||
},
|
||||
neg: function(x, callback) {
|
||||
callback(-x);
|
||||
}
|
||||
};
|
||||
methods.chain = Utils.asyncChain;
|
||||
|
||||
// ((1 + 1 + 1) * (-1) + 1) * 2 + 1 = -3
|
||||
methods.chain(methods.save, methods.addX, methods.addX, methods.neg,
|
||||
methods.addX, methods.double, methods.addX, methods.save)(1);
|
||||
do_check_eq(methods.x, -3);
|
||||
}
|
Загрузка…
Ссылка в новой задаче