start of sync refactor with tests

This commit is contained in:
Chris Karlof 2013-02-03 23:12:13 -08:00
Родитель 2c980a76f1
Коммит 2c63bd2286
10 изменённых файлов: 355 добавлений и 102 удалений

Просмотреть файл

@ -1,4 +1,6 @@
<!doctype html> <!doctype html>
<script src="https://cdn.firebase.com/v0/firebase.js"></script>
<script src='https://cdn.firebase.com/v0/firebase-auth-client.js'></script>
<script src="lib/jquery.js"></script> <script src="lib/jquery.js"></script>
<script src="lib/jsuri.js"></script> <script src="lib/jsuri.js"></script>
<script src="lib/tldjs.js"></script> <script src="lib/tldjs.js"></script>
@ -22,6 +24,8 @@
<script src="captured_credential_storage.js"></script> <script src="captured_credential_storage.js"></script>
<script src="site_configs.js"></script> <script src="site_configs.js"></script>
<script src="gombot_sync.js"></script> <script src="gombot_sync.js"></script>
<script src="sync_adapter.js"></script>
<script src="firebase_sync.js"></script>
<script src="models/login_credential.js"></script> <script src="models/login_credential.js"></script>
<script src="collections/login_credential_collection.js"></script> <script src="collections/login_credential_collection.js"></script>
<script src="models/user.js"></script> <script src="models/user.js"></script>

Просмотреть файл

@ -0,0 +1,78 @@
var FirebaseSync = function() {
var dataRef = new Firebase('https://gombot.firebaseIO.com');
var authClient = new FirebaseAuthClient(dataRef, authClientCallback);
var usersRef = dataRef.child('users');
var currentUser;
chrome.webRequest.onBeforeSendHeaders.addListener(
function(details) {
// Remove Origin header if it exists
for (var i = 0; i < details.requestHeaders.length; ++i) {
if (details.requestHeaders[i].name === 'Origin') {
details.requestHeaders.splice(i, 1);
break;
}
}
// Remove Referer header if it exists
for (var i = 0; i < details.requestHeaders.length; ++i) {
if (details.requestHeaders[i].name === 'Referer') {
details.requestHeaders.splice(i, 1);
}
}
// Add a Referer header for gombot.org
details.requestHeaders.push({ name: 'Referer', value: 'https://gombot.org/'});
return {requestHeaders: details.requestHeaders};
},
{ urls: ["https://auth.firebase.com/*"] },
["blocking", "requestHeaders"]
);
function authClientCallback(error, user) {
if (error) {
// an error occurred while attempting login
console.log(error);
} else if (user) {
currentUser = user;
usersRef.child(user.id).on('value', getUserData);
// user authenticated with Firebase
console.log('User ID: ' + user.id + ', Provider: ' + user.provider);
} else {
if (currentUser) usersRef.child(currentUser.id).off('value', getUserData);
currentUser = null;
// user is logged out
}
}
function create(email, password, options) {
authClient.createUser(email, password, function(error, user) {
if (!error) {
console.log('User Id: ' + user.id + ', Email: ' + user.email);
} else {
console.log("FirebaseSync.create error:", error);
}
});
}
function login(email, password, options) {
authClient.login('password', {
email: email,
password: password,
rememberMe: true
});
}
function getUserData(data) {
console.log("User data:", data.val());
}
return {
create: create,
login: login,
getUserData: getUserData
};
};

Просмотреть файл

@ -45,9 +45,10 @@ var _Gombot = function(importedModules, Gombot) {
Gombot.TldService = getModule("TldService")(getModule("Tld"), getModule("Uri")); Gombot.TldService = getModule("TldService")(getModule("Tld"), getModule("Uri"));
Gombot.SiteConfigs = getModule("SiteConfigs"); Gombot.SiteConfigs = getModule("SiteConfigs");
Gombot.Realms = getModule("Realms")(Gombot, Gombot.SiteConfigs, getModule("Uri")); Gombot.Realms = getModule("Realms")(Gombot, Gombot.SiteConfigs, getModule("Uri"));
Gombot.Storage = getModule("Storage")(Backbone, _, Gombot.LocalStorage); // defined by backbone.localStorage.js Gombot.Storage = getModule("Storage")(Backbone, _, Gombot.LocalStorage); // local sync; defined by backbone.localStorage.js
Gombot.GombotClient = getModule("GombotClient"); //Gombot.GombotClient = getModule("GombotClient");
Gombot.Sync = getModule("GombotSync")(Gombot, Backbone, _); //Gombot.Sync = getModule("GombotSync")(Gombot, Backbone, _); // original sync using our api
//Gombot.FirebaseSync = getModule("FirebaseSync")(Gombot); // sync using firebase
Gombot.LoginCredential = getModule("LoginCredential")(Gombot, Backbone, _); Gombot.LoginCredential = getModule("LoginCredential")(Gombot, Backbone, _);
Gombot.LoginCredentialCollection = getModule("LoginCredentialCollection")(Backbone, _, Gombot.LoginCredential); // LoginCredential need to be initialized Gombot.LoginCredentialCollection = getModule("LoginCredentialCollection")(Backbone, _, Gombot.LoginCredential); // LoginCredential need to be initialized
Gombot.CapturedCredentialStorage = getModule("CapturedCredentialStorage")(Gombot, getModule("Uri")); Gombot.CapturedCredentialStorage = getModule("CapturedCredentialStorage")(Gombot, getModule("Uri"));
@ -55,8 +56,12 @@ var _Gombot = function(importedModules, Gombot) {
Gombot.AccountManager = getModule("AccountManager")(Gombot, _); Gombot.AccountManager = getModule("AccountManager")(Gombot, _);
Gombot.CommandHandler = getModule("CommandHandler")(Gombot, Gombot.Messaging, _); Gombot.CommandHandler = getModule("CommandHandler")(Gombot, Gombot.Messaging, _);
Gombot.Pages = getModule("Pages")(Gombot); Gombot.Pages = getModule("Pages")(Gombot);
Gombot.InfobarManager = getModule("InfobarManager"); Gombot.Crypto = getModule("GombotCrypto");
Gombot.Infobars = getModule("Infobars")(Gombot); Gombot.User = getModule("User")(Backbone, _, Gombot);
if (typeof chrome !== "undefined") {
Gombot.InfobarManager = getModule("InfobarManager");
Gombot.Infobars = getModule("Infobars")(Gombot);
}
var currentUser = null; var currentUser = null;
Gombot.getCurrentUser = function() { Gombot.getCurrentUser = function() {
@ -73,19 +78,24 @@ var _Gombot = function(importedModules, Gombot) {
currentUser.destroy({ localOnly: true, success: function() { currentUser = null; callback(); }}); currentUser.destroy({ localOnly: true, success: function() { currentUser = null; callback(); }});
}; };
new Gombot.Storage("users", function(store) { Gombot.init = function(options) {
Gombot.User = getModule("User")(Backbone, _, Gombot, store); options = options || {};
Gombot.UserCollection = getModule("UserCollection")(Backbone, _, Gombot, store); options.storeName = options.storeName || "users";
checkFirstRun(); options.callback = options.callback || checkFirstRun;
}); new Gombot.Storage(options.storeName, function(store) {
Gombot.SyncAdapter = getModule("SyncAdapter")(Gombot, Gombot.Crypto, store, _);
function checkFirstRun() { Gombot.UserCollection = getModule("UserCollection")(Backbone, _, Gombot, store);
Gombot.LocalStorage.getItem("firstRun", function(firstRun) { options.callback();
initGombot(firstRun);
}); });
} }
function initGombot(firstRun) { function checkFirstRun() {
Gombot.LocalStorage.getItem("firstRun", function(firstRun) {
fetchUsers(firstRun);
});
}
function fetchUsers(firstRun) {
Gombot.users = new Gombot.UserCollection(); Gombot.users = new Gombot.UserCollection();
Gombot.users.fetch({ Gombot.users.fetch({
success: function() { success: function() {
@ -102,6 +112,7 @@ var _Gombot = function(importedModules, Gombot) {
if (typeof module !== "undefined" && module.exports) { if (typeof module !== "undefined" && module.exports) {
module.exports = _Gombot; // export namespace constructor, for Firefox module.exports = _Gombot; // export namespace constructor, for Firefox
} else { // otherwise, just create the global Gombot namespace } else { // otherwise, just create the global Gombot namespace and init
var Gombot = _Gombot({}); var Gombot = _Gombot({});
Gombot.init();
} }

Просмотреть файл

@ -47,33 +47,9 @@ windows.on('open', function(window) {
addToolbarButton(); addToolbarButton();
}); });
/** Load all Gombot modules **/ var GombotModules = require("./modules");
var Gombot = require("./gombot")(GombotModules);
var gombotModules = { Gombot.init();
Backbone: require("./lib/backbone"),
_ : require("./lib/underscore"),
Messaging: require("./messaging"),
LocalStorage: require("./local_storage"),
Tld: require("./lib/tld.js"),
Uri: require("./lib/jsuri"),
TldService: require("./tld_service"),
SiteConfigs: require("./site_configs"),
Realms: require("./realms"),
Storage: require("./storage"),
GombotClient: require("./client/client"),
GombotSync: require("./gombot_sync"),
LoginCredential: require("./models/login_credential"),
LoginCredentialCollection: require("./collections/login_credential_collection"),
CapturedCredentialStorage: require("./captured_credential_storage"),
Linker: require("./linker"),
CommandHandler: require("./command_handler"),
User: require("./models/user"),
UserCollection: require("./collections/user_collection"),
AccountManager: require("./account_manager"),
Pages: require("./pages")
};
var Gombot = require("./gombot")(gombotModules);
/** Tpp panel stuff **/ /** Tpp panel stuff **/
@ -109,3 +85,5 @@ pageMod.PageMod({
Gombot.Messaging.registerPageModWorker(worker); Gombot.Messaging.registerPageModWorker(worker);
} }
}); });
exports.gombot = Gombot;

Просмотреть файл

@ -1,12 +1,9 @@
var User = function(Backbone, _, Gombot, LocalStorage) { var User = function(Backbone, _, Gombot) {
const USER_DATA_VERSIONS = [ const USER_DATA_VERSIONS = [
"identity.mozilla.com/gombot/v1/userData" "identity.mozilla.com/gombot/v1/userData"
]; ];
var GombotSync = Gombot.Sync,
LoginCredentialCollection = Gombot.LoginCredentialCollection;
// attributes should be something like: // attributes should be something like:
// { // {
// "version": "identity.mozilla.com/gombot/v1/userData", // "version": "identity.mozilla.com/gombot/v1/userData",
@ -38,8 +35,6 @@ var User = function(Backbone, _, Gombot, LocalStorage) {
disabledSites: {} disabledSites: {}
}, },
localStorage: LocalStorage,
initialize: function() { initialize: function() {
Backbone.Model.prototype.initialize.apply(this, arguments); Backbone.Model.prototype.initialize.apply(this, arguments);
this.addSyncListener(this.get("logins")); this.addSyncListener(this.get("logins"));
@ -64,6 +59,7 @@ var User = function(Backbone, _, Gombot, LocalStorage) {
}, },
isAuthenticated: function() { isAuthenticated: function() {
return false;
return this.client && ((this.client.isAuthenticated && this.client.isAuthenticated()) || (this.client.keys && this.client.user)); return this.client && ((this.client.isAuthenticated && this.client.isAuthenticated()) || (this.client.keys && this.client.user));
}, },
@ -71,60 +67,61 @@ var User = function(Backbone, _, Gombot, LocalStorage) {
// call model.toJSON({ encrypted: true, ciphertext: <ciphertext> }) // call model.toJSON({ encrypted: true, ciphertext: <ciphertext> })
// Other toJSON() creates a standard plaintext representation of a User object // Other toJSON() creates a standard plaintext representation of a User object
toJSON: function(args) { toJSON: function(args) {
var result; var result = Backbone.Model.prototype.toJSON.apply(this, arguments);
args = args || {}; return _.extend(result, { logins: this.get("logins").toJSON() });
if (args.ciphertext) { },
result = { ciphertext: args.ciphertext, updated: this.updated, id: this.id, email: this.get("email"), version: this.get("version") };
if (this.isAuthenticated()) _.extend(result, { client: this.client.toJSON() }); // Returns an object containing key/values of data that will be
return result; // stored in plaintext with an encrypted copy of this model's data.
} // The metadata should not contain any information that is intended
else { // to be stored encrypted at rest.
result = Backbone.Model.prototype.toJSON.apply(this, arguments); getMetadata: function() {
return _.extend(result, { logins: this.get("logins").toJSON() }); return {
id: this.id,
email: this.get("email"),
version: this.get("version"),
updated: this.updated
} }
}, },
parse: function(resp) { parse: function(resp) {
if (resp.ciphertext) this.ciphertext = resp.ciphertext;
if (resp.updated) this.updated = resp.updated; if (resp.updated) this.updated = resp.updated;
if (resp.client) this.client = resp.client;
delete resp.ciphertext;
delete resp.updated; delete resp.updated;
delete resp.client;
return resp; return resp;
}, },
sync: function(method, model, options) { sync: function(method, model, options) {
var self = this; Gombot.SyncAdapter.sync(method, model, options);
var success = function(resp) { // var self = this;
var s = options.success; // var success = function(resp) {
options.success = function(model, resp, options) { // var s = options.success;
console.log("User.sync finished method="+method+" resp="+JSON.stringify(resp)+" model="+JSON.stringify(model)); // options.success = function(model, resp, options) {
// resp.data is returned by GombotSync calls with plaintext user data // console.log("User.sync finished method="+method+" resp="+JSON.stringify(resp)+" model="+JSON.stringify(model));
if (s) s(model, resp.data || {}, options); // // resp.data is returned by GombotSync calls with plaintext user data
} // if (s) s(model, resp.data || {}, options);
if (resp.updated) self.updated = resp.updated; // }
// ciphertext in resp indicates we need to write it out to local storage // if (resp.updated) self.updated = resp.updated;
if (resp.ciphertext) { // // ciphertext in resp indicates we need to write it out to local storage
if (method === "read") { // if (resp.ciphertext) {
self.save(resp.data, _.extend(options, { localOnly: true, ciphertext: resp.ciphertext })); // if (method === "read") {
} else { // self.save(resp.data, _.extend(options, { localOnly: true, ciphertext: resp.ciphertext }));
console.log("localSync method="+method); // } else {
Backbone.localSync(method, model, _.extend(options, { ciphertext: resp.ciphertext })); // console.log("localSync method="+method);
} // Backbone.localSync(method, model, _.extend(options, { ciphertext: resp.ciphertext }));
} else if (options.success) { // }
options.success(model, resp, options); // } else if (options.success) {
} // options.success(model, resp, options);
}; // }
var error = function(args) { // };
if (options.error) options.error(args); // var error = function(args) {
}; // if (options.error) options.error(args);
var o = _.clone(options); // };
if (options.localOnly) { // var o = _.clone(options);
Backbone.localSync(method, model, options); // if (options.localOnly) {
} else { // Backbone.localSync(method, model, options);
GombotSync.sync(method, model, _.extend(o,{ success: success, error: error })); // } else {
} // GombotSync.sync(method, model, _.extend(o,{ success: success, error: error }));
// }
}, },
set: function(key, val, options) { set: function(key, val, options) {
@ -137,9 +134,9 @@ var User = function(Backbone, _, Gombot, LocalStorage) {
} else { } else {
(attributes = {})[key] = val; (attributes = {})[key] = val;
} }
if (attributes.logins !== undefined && !(attributes.logins instanceof LoginCredentialCollection)) { if (attributes.logins !== undefined && !(attributes.logins instanceof Gombot.LoginCredentialCollection)) {
logins = attributes.logins; logins = attributes.logins;
attributes.logins = this.get("logins") || new LoginCredentialCollection(); attributes.logins = this.get("logins") || new Gombot.LoginCredentialCollection();
} }
result = Backbone.Model.prototype.set.call(this, attributes, options); result = Backbone.Model.prototype.set.call(this, attributes, options);
if (result && logins) { if (result && logins) {

29
background/modules.js Normal file
Просмотреть файл

@ -0,0 +1,29 @@
/** Load all Gombot modules **/
var GombotModules = {
Backbone: require("./lib/backbone"),
_ : require("./lib/underscore"),
Messaging: require("./messaging"),
LocalStorage: require("./local_storage"),
Tld: require("./lib/tld.js"),
Uri: require("./lib/jsuri"),
TldService: require("./tld_service"),
SiteConfigs: require("./site_configs"),
Realms: require("./realms"),
Storage: require("./storage"),
//GombotClient: require("./client/client"),
//GombotSync: require("./gombot_sync"),
LoginCredential: require("./models/login_credential"),
LoginCredentialCollection: require("./collections/login_credential_collection"),
CapturedCredentialStorage: require("./captured_credential_storage"),
Linker: require("./linker"),
CommandHandler: require("./command_handler"),
User: require("./models/user"),
UserCollection: require("./collections/user_collection"),
AccountManager: require("./account_manager"),
Pages: require("./pages"),
GombotCrypto: require("./client/crypto"),
SyncAdapter: require("./sync_adapter")
};
module.exports = GombotModules;

121
background/sync_adapter.js Normal file
Просмотреть файл

@ -0,0 +1,121 @@
var SyncAdapter = function(Gombot, GombotCrypto, SyncStrategy, _) {
// TODO: seed from an actual entropy source
GombotCrypto.seed("oiqwjeciouqh3c89cnkjasdcnasjf84u9jcuqwiench734fhujhwuqhf73f73fhsdjfhasdf734fhdkcnuf"+(new Date().toString()) ,function(err) {
if (err) console.log("GombotCrypto.seed error:", err);
});
function maybeHandleError(handler, err) {
if (err) {
console.log("SyncMediator error", err);
if (handler) handler(err);
return true;
}
else return false;
}
function encryptModel(model, keys, options) {
GombotCrypto.encrypt(keys, JSON.stringify(model), function(err, ciphertext) {
if (err) return maybeHandleError(options.error, err);
options.success(ciphertext);
});
}
function decryptModelData(ciphertext, keys, options) {
GombotCrypto.decrypt(keys, ciphertext, function(err, json) {
var modelData;
try {
if (!err) {
modelData = JSON.parse(json);
}
} catch (e) {
err = new Error("Could not parse decrypted JSON:", json);
}
if (err) return maybeHandleError(options.error, err);
options.success(modelData);
});
}
function createCryptoProxyForModel(model, keys) {
var clone = _.clone(model);
return _.extend(clone, {
toJSON: function(options) {
// missing options means synchronous response to underyling object
if (!options || !options.success) return model.toJSON();
var o = _.clone(options);
encryptModel(model, keys, _.extend(o, { success: function(ciphertext) {
options.success(_.extend(model.getMetadata(), {
ciphertext: ciphertext // encrypted plaintext model
}));
}}));
},
parse: function(resp, options) {
var o = _.clone(options),
ciphertext = resp.ciphertext;
decryptModelData(ciphertext, keys, _.extend(o, { success: function(modelData) {
delete resp.ciphertext;
options.success(_.extend(resp, modelData));
}}));
}
});
}
function getCryptoProxyForModel(model, options) {
if (model.cryptoProxy) return options.success(model.cryptoProxy);
var o = _.clone(options);
deriveKeysForModel(model, _.extend(o, { success: function(keys) {
model.cryptoProxy = createCryptoProxyForModel(model, keys);
options.success(model.cryptoProxy);
}}));
}
var kdf = GombotCrypto.derive;
// Special kdf derivation function we'll pass to GombotClient to handle FX slowness bug
if (typeof require !== "undefined") {
kdf = function (args, callback) {
console.log("in derive")
require("gombot-crypto-jetpack").kdf(args.email, args.password).then(function(keys) {
callback(null, keys);
});
}
}
// options.password must be present
function deriveKeysForModel(model, options) {
kdf({
email: model.get("email"),
password: options.password
}, function(err, keys) {
if (err) return maybeHandleError(options.error, err);
options.success(keys);
});
}
function setSyncStrategy(strategy) {
SyncStrategy = strategy;
}
function sync(method, model, options) {
if (!(model instanceof Gombot.User)) {
if (options.error) options.error("sync only supports syncing instances of Gombot.User");
return false;
}
var o = _.clone(options);
getCryptoProxyForModel(model, _.extend(o, { success: function(cryptoProxyForModel) {
var o = _.clone(options);
// translate model proxy back to original model
SyncStrategy.sync(method, cryptoProxyForModel, _.extend(o, { success: function(modelProxy, resp, modifiedOptions) {
if (options.success) options.success(model, resp, options);
}}));
}}));
}
return {
sync: sync,
setSyncStrategy: setSyncStrategy
};
};
if (typeof module !== "undefined" && module.exports) {
module.exports = SyncAdapter;
}

Просмотреть файл

@ -3,7 +3,7 @@
* *
* https://github.com/jeromegn/Backbone.localStorage * https://github.com/jeromegn/Backbone.localStorage
*/ */
var Storage = function(Backbone, _, LocalStorage) { var Storage = function(Backbone, _, LocalStorage, store) {
return (function (root, factory) { return (function (root, factory) {
// if (typeof define === "function" && define.amd) { // if (typeof define === "function" && define.amd) {
// // AMD. Register as an anonymous module. // // AMD. Register as an anonymous module.
@ -62,9 +62,12 @@ _.extend(Backbone.LocalStorage.prototype, {
var cb = _.after(2, function() { var cb = _.after(2, function() {
callback(model.toJSON()); callback(model.toJSON());
}); });
this.localStorage().setItem(this.name+"-"+model.id, JSON.stringify(model.toJSON({ ciphertext: options.ciphertext })), cb); var o = _.clone(options);
this.records.push(model.id.toString()); model.toJSON(_.extend(o, { success: (function(jsonObj) {
this.save(cb); this.localStorage().setItem(this.name+"-"+model.id, JSON.stringify(jsonObj), cb);
this.records.push(model.id.toString());
this.save(cb);
}).bind(this)}));
return; return;
}, },
@ -116,6 +119,12 @@ _.extend(Backbone.LocalStorage.prototype, {
// fix for "illegal access" error on Android when JSON.parse is passed null // fix for "illegal access" error on Android when JSON.parse is passed null
jsonData: function (data) { jsonData: function (data) {
return data && JSON.parse(data); return data && JSON.parse(data);
},
sync: function(method, model, options) {
var o = _.clone(options);
o.store = this;
Backbone.LocalStorage.sync(method, model, o);
} }
}); });
@ -124,7 +133,7 @@ _.extend(Backbone.LocalStorage.prototype, {
// *localStorage* property, which should be an instance of `Store`. // *localStorage* property, which should be an instance of `Store`.
// window.Store.sync and Backbone.localSync is deprectated, use Backbone.LocalStorage.sync instead // window.Store.sync and Backbone.localSync is deprectated, use Backbone.LocalStorage.sync instead
Backbone.LocalStorage.sync = Backbone.localSync = function(method, model, options) { Backbone.LocalStorage.sync = Backbone.localSync = function(method, model, options) {
var store = model.localStorage || model.collection.localStorage; var store = options.store || model.localStorage || model.collection.localStorage;
var syncDfd = (typeof $ !== "undefined") && $.Deferred && $.Deferred(); //If $ is having Deferred - use it. var syncDfd = (typeof $ !== "undefined") && $.Deferred && $.Deferred(); //If $ is having Deferred - use it.
@ -149,6 +158,8 @@ Backbone.LocalStorage.sync = Backbone.localSync = function(method, model, option
if (options && options.complete) options.complete(resp); if (options && options.complete) options.complete(resp);
}; };
console.log("Backbone.LocalStorage.sync", method, model, options);
switch (method) { switch (method) {
case "read": model.id != undefined ? store.find(model, callback, options) : store.findAll(callback, options); break; case "read": model.id != undefined ? store.find(model, callback, options) : store.findAll(callback, options); break;
case "create": store.create(model, callback, options); break; case "create": store.create(model, callback, options); break;

Просмотреть файл

@ -8,12 +8,14 @@
"notifications", "notifications",
"storage", "storage",
"http://*/", "http://*/",
"https://*/" "https://*/",
"webRequest",
"webRequestBlocking"
], ],
"icons": { "icons": {
"128": "images/gombot-icon-128.png" "128": "images/gombot-icon-128.png"
}, },
"content_security_policy": "script-src 'self' https://cdn.firebase.com https://auth.firebase.com https://*.firebaseio.com; object-src 'self'",
"background": { "background": {
"page": "background/background.html" "page": "background/background.html"
}, },

22
test/test-local-sync.js Normal file
Просмотреть файл

@ -0,0 +1,22 @@
exports.testCreate = function(test) {
var GombotModules = require("./modules");
var Gombot = require("./gombot")(GombotModules);
Gombot.init({ storeName: "testUsers", callback: function() {
var email = "test+"+Math.floor((1+Math.random())*10000)+"@test.com";
var u = new Gombot.User({ email: email });
u.save(null, { success: function() {
console.log("user saved", u);
test.pass();
test.done();
},
error: function(err) {
console.log("error:", err);
test.fail();
test.done();
},
password: "foobar"
});
}});
test.waitUntilDone();
}