Bug 969892 - Fixes and improvements to FxAccounts test suite; r=ttaubert

This commit is contained in:
Jed Parsons 2014-02-18 09:47:52 -08:00
parent fbc6b76196
commit ed60f7b54c
3 changed files with 127 additions and 90 deletions

View File

@ -342,6 +342,7 @@ add_task(function test_multiple_401_retry_once() {
// Request will have bad timestamp; client will retry once
try {
yield client.request("/maybe", method, credentials);
do_throw("Expected an error");
} catch (err) {
do_check_eq(err.code, 401);
}

View File

@ -67,22 +67,22 @@ AccountState.prototype = {
cert: null,
keyPair: null,
signedInUser: null,
whenVerifiedPromise: null,
whenKeysReadyPromise: null,
whenVerifiedDeferred: null,
whenKeysReadyDeferred: null,
get isCurrent() this.fxaInternal && this.fxaInternal.currentAccountState === this,
abort: function() {
if (this.whenVerifiedPromise) {
this.whenVerifiedPromise.reject(
if (this.whenVerifiedDeferred) {
this.whenVerifiedDeferred.reject(
new Error("Verification aborted; Another user signing in"));
this.whenVerifiedPromise = null;
this.whenVerifiedDeferred = null;
}
if (this.whenKeysReadyPromise) {
this.whenKeysReadyPromise.reject(
if (this.whenKeysReadyDeferred) {
this.whenKeysReadyDeferred.reject(
new Error("Verification aborted; Another user signing in"));
this.whenKeysReadyPromise = null;
this.whenKeysReadyDeferred = null;
}
this.cert = null;
this.keyPair = null;
@ -484,13 +484,13 @@ FxAccountsInternal.prototype = {
if (data.kA && data.kB) {
return data;
}
if (!currentState.whenKeysReadyPromise) {
currentState.whenKeysReadyPromise = Promise.defer();
if (!currentState.whenKeysReadyDeferred) {
currentState.whenKeysReadyDeferred = Promise.defer();
this.fetchAndUnwrapKeys(data.keyFetchToken).then(data => {
currentState.whenKeysReadyPromise.resolve(data);
currentState.whenKeysReadyDeferred.resolve(data);
});
}
return currentState.whenKeysReadyPromise.promise;
return currentState.whenKeysReadyDeferred.promise;
}).then(result => currentState.resolve(result));
},
@ -607,11 +607,11 @@ FxAccountsInternal.prototype = {
log.debug("already verified");
return currentState.resolve(data);
}
if (!currentState.whenVerifiedPromise) {
if (!currentState.whenVerifiedDeferred) {
log.debug("whenVerified promise starts polling for verified email");
this.pollEmailStatus(currentState, data.sessionToken, "start");
}
return currentState.whenVerifiedPromise.promise.then(
return currentState.whenVerifiedDeferred.promise.then(
result => currentState.resolve(result)
);
},
@ -629,8 +629,8 @@ FxAccountsInternal.prototype = {
// if the user requested the verification email to be resent while we
// were already polling for receipt of an earlier email.
this.pollTimeRemaining = this.POLL_SESSION;
if (!currentState.whenVerifiedPromise) {
currentState.whenVerifiedPromise = Promise.defer();
if (!currentState.whenVerifiedDeferred) {
currentState.whenVerifiedDeferred = Promise.defer();
}
}
@ -647,9 +647,9 @@ FxAccountsInternal.prototype = {
})
.then((data) => {
// Now that the user is verified, we can proceed to fetch keys
if (currentState.whenVerifiedPromise) {
currentState.whenVerifiedPromise.resolve(data);
delete currentState.whenVerifiedPromise;
if (currentState.whenVerifiedDeferred) {
currentState.whenVerifiedDeferred.resolve(data);
delete currentState.whenVerifiedDeferred;
}
});
} else {
@ -661,11 +661,11 @@ FxAccountsInternal.prototype = {
this.pollEmailStatus(currentState, sessionToken, "timer")}, this.POLL_STEP);
log.debug("started timer " + this.currentTimer);
} else {
if (currentState.whenVerifiedPromise) {
currentState.whenVerifiedPromise.reject(
if (currentState.whenVerifiedDeferred) {
currentState.whenVerifiedDeferred.reject(
new Error("User email verification timed out.")
);
delete currentState.whenVerifiedPromise;
delete currentState.whenVerifiedDeferred;
}
}
}

View File

@ -1,6 +1,8 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
Cu.import("resource://gre/modules/FxAccountsClient.jsm");
Cu.import("resource://gre/modules/Promise.jsm");
Cu.import("resource://services-common/utils.js");
@ -12,24 +14,43 @@ function run_test() {
run_next_test();
}
// https://wiki.mozilla.org/Identity/AttachedServices/KeyServerProtocol#.2Faccount.2Fkeys
let ACCOUNT_KEYS = {
keyFetch: h("8081828384858687 88898a8b8c8d8e8f"+
"9091929394959697 98999a9b9c9d9e9f"),
response: h("ee5c58845c7c9412 b11bbd20920c2fdd"+
"d83c33c9cd2c2de2 d66b222613364636"+
"c2c0f8cfbb7c6304 72c0bd88451342c6"+
"c05b14ce342c5ad4 6ad89e84464c993c"+
"3927d30230157d08 17a077eef4b20d97"+
"6f7a97363faf3f06 4c003ada7d01aa70"),
kA: h("2021222324252627 28292a2b2c2d2e2f"+
"3031323334353637 38393a3b3c3d3e3f"),
wrapKB: h("4041424344454647 48494a4b4c4d4e4f"+
"5051525354555657 58595a5b5c5d5e5f"),
};
// https://github.com/mozilla/fxa-auth-server/wiki/onepw-protocol#wiki-use-session-certificatesign-etc
let SESSION_KEYS = {
sessionToken: h("a0a1a2a3a4a5a6a7 a8a9aaabacadaeaf"+
"b0b1b2b3b4b5b6b7 b8b9babbbcbdbebf"),
tokenID: h("c0a29dcf46174973 da1378696e4c82ae"+
"10f723cf4f4d9f75 e39f4ae3851595ab"),
reqHMACkey: h("9d8f22998ee7f579 8b887042466b72d5"+
"3e56ab0c094388bf 65831f702d2febc0"),
};
function deferredStop(server) {
let deferred = Promise.defer();
server.stop(deferred.resolve);
return deferred.promise;
}
add_test(function test_hawk_credentials() {
let client = new FxAccountsClient();
let sessionToken = "a0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebf";
let result = client._deriveHawkCredentials(sessionToken, "session");
do_check_eq(result.id, "639503a218ffbb62983e9628be5cd64a0438d0ae81b2b9dadeb900a83470bc6b");
do_check_eq(CommonUtils.bytesAsHex(result.key), "3a0188943837ab228fe74e759566d0e4837cbcc7494157aac4da82025b2811b2");
run_next_test();
});
add_task(function test_authenticated_get_request() {
let message = "{\"msg\": \"Great Success!\"}";
let credentials = {
@ -94,6 +115,7 @@ add_task(function test_500_error() {
try {
yield client._request("/foo", method);
do_throw("Expected to catch an exception");
} catch (e) {
do_check_eq(500, e.code);
do_check_eq("Internal Server Error", e.message);
@ -171,12 +193,14 @@ add_task(function test_signUp() {
created = true;
response.setStatusLine(request.httpVersion, 200, "OK");
return response.bodyOutputStream.write(creationMessage, creationMessage.length);
response.bodyOutputStream.write(creationMessage, creationMessage.length);
return;
}
// Error trying to create same account a second time
response.setStatusLine(request.httpVersion, 400, "Bad request");
return response.bodyOutputStream.write(errorMessage, errorMessage.length);
response.bodyOutputStream.write(errorMessage, errorMessage.length);
return;
},
});
@ -189,6 +213,7 @@ add_task(function test_signUp() {
// Try to create account again. Triggers error path.
try {
result = yield client.signUp('andré@example.org', 'pässwörd');
do_throw("Expected to catch an exception");
} catch(expectedError) {
do_check_eq(101, expectedError.errno);
}
@ -199,6 +224,7 @@ add_task(function test_signUp() {
add_task(function test_signIn() {
let sessionMessage = JSON.stringify({sessionToken: FAKE_SESSION_TOKEN});
let errorMessage = JSON.stringify({code: 400, errno: 102, error: "doesn't exist"});
let server = httpd_setup({
"/account/login": function(request, response) {
let body = CommonUtils.readBytesFromInputStream(request.bodyInputStream);
@ -207,12 +233,14 @@ add_task(function test_signIn() {
if (jsonBody.email == "mé@example.com") {
do_check_eq(jsonBody.authPW, "08b9d111196b8408e8ed92439da49206c8ecfbf343df0ae1ecefcd1e0174a8b6");
response.setStatusLine(request.httpVersion, 200, "OK");
return response.bodyOutputStream.write(sessionMessage, sessionMessage.length);
response.bodyOutputStream.write(sessionMessage, sessionMessage.length);
return;
}
// Error trying to sign in to nonexistent account
response.setStatusLine(request.httpVersion, 400, "Bad request");
return response.bodyOutputStream.write(errorMessage, errorMessage.length);
response.bodyOutputStream.write(errorMessage, errorMessage.length);
return;
},
});
@ -223,6 +251,7 @@ add_task(function test_signIn() {
// Trigger error path
try {
result = yield client.signIn("yøü@bad.example.org", "nofear");
do_throw("Expected to catch an exception");
} catch(expectedError) {
do_check_eq(102, expectedError.errno);
}
@ -241,12 +270,14 @@ add_task(function test_signOut() {
signedOut = true;
do_check_true(request.hasHeader("Authorization"));
response.setStatusLine(request.httpVersion, 200, "OK");
return response.bodyOutputStream.write(signoutMessage, signoutMessage.length);
response.bodyOutputStream.write(signoutMessage, signoutMessage.length);
return;
}
// Error trying to sign out of nonexistent account
response.setStatusLine(request.httpVersion, 400, "Bad request");
return response.bodyOutputStream.write(errorMessage, errorMessage.length);
response.bodyOutputStream.write(errorMessage, errorMessage.length);
return;
},
});
@ -257,6 +288,7 @@ add_task(function test_signOut() {
// Trigger error path
try {
result = yield client.signOut("FakeSession");
do_throw("Expected to catch an exception");
} catch(expectedError) {
do_check_eq(102, expectedError.errno);
}
@ -274,13 +306,16 @@ add_task(function test_recoveryEmailStatus() {
do_check_true(request.hasHeader("Authorization"));
if (tries === 0) {
tries += 1;
response.setStatusLine(request.httpVersion, 200, "OK");
return response.bodyOutputStream.write(emailStatus, emailStatus.length);
response.bodyOutputStream.write(emailStatus, emailStatus.length);
return;
}
// Second call gets an error trying to query a nonexistent account
response.setStatusLine(request.httpVersion, 400, "Bad request");
return response.bodyOutputStream.write(errorMessage, errorMessage.length);
response.bodyOutputStream.write(errorMessage, errorMessage.length);
return;
},
});
@ -291,6 +326,7 @@ add_task(function test_recoveryEmailStatus() {
// Trigger error path
try {
result = yield client.recoveryEmailStatus("some bogus session");
do_throw("Expected to catch an exception");
} catch(expectedError) {
do_check_eq(102, expectedError.errno);
}
@ -307,13 +343,16 @@ add_task(function test_resendVerificationEmail() {
"/recovery_email/resend_code": function(request, response) {
do_check_true(request.hasHeader("Authorization"));
if (tries === 0) {
tries += 1;
response.setStatusLine(request.httpVersion, 200, "OK");
return response.bodyOutputStream.write(emptyMessage, emptyMessage.length);
response.bodyOutputStream.write(emptyMessage, emptyMessage.length);
return;
}
// Second call gets an error trying to query a nonexistent account
response.setStatusLine(request.httpVersion, 400, "Bad request");
return response.bodyOutputStream.write(errorMessage, errorMessage.length);
response.bodyOutputStream.write(errorMessage, errorMessage.length);
return;
},
});
@ -324,6 +363,7 @@ add_task(function test_resendVerificationEmail() {
// Trigger error path
try {
result = yield client.resendVerificationEmail("some bogus session");
do_throw("Expected to catch an exception");
} catch(expectedError) {
do_check_eq(102, expectedError.errno);
}
@ -332,25 +372,11 @@ add_task(function test_resendVerificationEmail() {
});
add_task(function test_accountKeys() {
// Vectors: https://wiki.mozilla.org/Identity/AttachedServices/KeyServerProtocol#.2Faccount.2Fkeys
let keyFetch = h("8081828384858687 88898a8b8c8d8e8f"+
"9091929394959697 98999a9b9c9d9e9f");
let response = h("ee5c58845c7c9412 b11bbd20920c2fdd"+
"d83c33c9cd2c2de2 d66b222613364636"+
"c2c0f8cfbb7c6304 72c0bd88451342c6"+
"c05b14ce342c5ad4 6ad89e84464c993c"+
"3927d30230157d08 17a077eef4b20d97"+
"6f7a97363faf3f06 4c003ada7d01aa70");
let kA = h("2021222324252627 28292a2b2c2d2e2f"+
"3031323334353637 38393a3b3c3d3e3f");
let wrapKB = h("4041424344454647 48494a4b4c4d4e4f"+
"5051525354555657 58595a5b5c5d5e5f");
let responseMessage = JSON.stringify({bundle: response});
// Four calls to accountKeys(). The first one should work correctly, and we
// should get a valid bundle back, in exchange for our keyFetch token, from
// which we correctly derive kA and wrapKB. The subsequent three calls
// should all trigger separate error paths.
let responseMessage = JSON.stringify({bundle: ACCOUNT_KEYS.response});
let errorMessage = JSON.stringify({code: 400, errno: 102, error: "doesn't exist"});
let emptyMessage = "{}";
let attempt = 0;
@ -375,10 +401,12 @@ add_task(function test_accountKeys() {
case 3:
// Return gibberish to trigger client MAC error
let garbage = response;
garbage[0] = 0; // tweak a byte
// Tweak a byte
let garbageResponse = JSON.stringify({
bundle: ACCOUNT_KEYS.response.slice(0, -1) + "1"
});
response.setStatusLine(request.httpVersion, 200, "OK");
response.bodyOutputStream.write(responseMessage, responseMessage.length);
response.bodyOutputStream.write(garbageResponse, garbageResponse.length);
break;
case 4:
@ -393,27 +421,30 @@ add_task(function test_accountKeys() {
let client = new FxAccountsClient(server.baseURI);
// First try, all should be good
let result = yield client.accountKeys(keyFetch);
do_check_eq(CommonUtils.hexToBytes(kA), result.kA);
do_check_eq(CommonUtils.hexToBytes(wrapKB), result.wrapKB);
let result = yield client.accountKeys(ACCOUNT_KEYS.keyFetch);
do_check_eq(CommonUtils.hexToBytes(ACCOUNT_KEYS.kA), result.kA);
do_check_eq(CommonUtils.hexToBytes(ACCOUNT_KEYS.wrapKB), result.wrapKB);
// Second try, empty bundle should trigger error
try {
result = yield client.accountKeys(keyFetch);
result = yield client.accountKeys(ACCOUNT_KEYS.keyFetch);
do_throw("Expected to catch an exception");
} catch(expectedError) {
do_check_eq(expectedError.message, "failed to retrieve keys");
}
// Third try, bad bundle results in MAC error
try {
result = yield client.accountKeys(keyFetch);
result = yield client.accountKeys(ACCOUNT_KEYS.keyFetch);
do_throw("Expected to catch an exception");
} catch(expectedError) {
do_check_eq(expectedError.message, "error unbundling encryption keys");
}
// Fourth try, pretend account doesn't exist
try {
result = yield client.accountKeys(keyFetch);
result = yield client.accountKeys(ACCOUNT_KEYS.keyFetch);
do_throw("Expected to catch an exception");
} catch(expectedError) {
do_check_eq(102, expectedError.errno);
}
@ -437,12 +468,14 @@ add_task(function test_signCertificate() {
do_check_eq(JSON.parse(jsonBody.publicKey).foo, "bar");
do_check_eq(jsonBody.duration, 600);
response.setStatusLine(request.httpVersion, 200, "OK");
return response.bodyOutputStream.write(certSignMessage, certSignMessage.length);
response.bodyOutputStream.write(certSignMessage, certSignMessage.length);
return;
}
// Second attempt, trigger error
response.setStatusLine(request.httpVersion, 400, "Bad request");
return response.bodyOutputStream.write(errorMessage, errorMessage.length);
response.bodyOutputStream.write(errorMessage, errorMessage.length);
return;
},
});
@ -453,6 +486,7 @@ add_task(function test_signCertificate() {
// Account doesn't exist
try {
result = yield client.signCertificate("bogus", JSON.stringify({foo: "bar"}), 600);
do_throw("Expected to catch an exception");
} catch(expectedError) {
do_check_eq(102, expectedError.errno);
}
@ -502,27 +536,18 @@ add_task(function test_accountExists() {
let client = new FxAccountsClient(server.baseURI);
let result;
try {
result = yield client.accountExists("i.exist@example.com");
} catch(expectedError) {
do_check_eq(expectedError.code, 400);
do_check_eq(expectedError.errno, 103);
}
result = yield client.accountExists("i.exist@example.com");
do_check_true(result);
try {
result = yield client.accountExists("i.also.exist@example.com");
} catch(expectedError) {
do_check_eq(expectedError.errno, 103);
}
result = yield client.accountExists("i.also.exist@example.com");
do_check_true(result);
try {
result = yield client.accountExists("i.dont.exist@example.com");
} catch(expectedError) {
do_check_eq(expectedError.errno, 102);
}
result = yield client.accountExists("i.dont.exist@example.com");
do_check_false(result);
try {
result = yield client.accountExists("i.break.things@example.com");
do_throw("Expected to catch an exception");
} catch(unexpectedError) {
do_check_eq(unexpectedError.code, 500);
}
@ -583,6 +608,17 @@ add_task(function test_email_case() {
yield deferredStop(server);
});
add_task(function test__deriveHawkCredentials() {
let client = new FxAccountsClient("https://example.org");
let credentials = client._deriveHawkCredentials(
SESSION_KEYS.sessionToken, "sessionToken");
do_check_eq(credentials.algorithm, "sha256");
do_check_eq(credentials.id, SESSION_KEYS.tokenID);
do_check_eq(CommonUtils.bytesAsHex(credentials.key), SESSION_KEYS.reqHMACkey);
});
// turn formatted test vectors into normal hex strings
function h(hexStr) {
return hexStr.replace(/\s+/g, "");