diff options
Diffstat (limited to 'services/fxaccounts/tests/xpcshell/test_profile_client.js')
-rw-r--r-- | services/fxaccounts/tests/xpcshell/test_profile_client.js | 422 |
1 files changed, 422 insertions, 0 deletions
diff --git a/services/fxaccounts/tests/xpcshell/test_profile_client.js b/services/fxaccounts/tests/xpcshell/test_profile_client.js new file mode 100644 index 0000000000..22fcc293f8 --- /dev/null +++ b/services/fxaccounts/tests/xpcshell/test_profile_client.js @@ -0,0 +1,422 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { + ERRNO_NETWORK, + ERRNO_PARSE, + ERRNO_UNKNOWN_ERROR, + ERROR_CODE_METHOD_NOT_ALLOWED, + ERROR_MSG_METHOD_NOT_ALLOWED, + ERROR_NETWORK, + ERROR_PARSE, + ERROR_UNKNOWN, +} = ChromeUtils.importESModule( + "resource://gre/modules/FxAccountsCommon.sys.mjs" +); +const { FxAccountsProfileClient, FxAccountsProfileClientError } = + ChromeUtils.importESModule( + "resource://gre/modules/FxAccountsProfileClient.sys.mjs" + ); + +const STATUS_SUCCESS = 200; + +/** + * Mock request responder + * @param {String} response + * Mocked raw response from the server + * @returns {Function} + */ +let mockResponse = function (response) { + let Request = function (requestUri) { + // Store the request uri so tests can inspect it + Request._requestUri = requestUri; + Request.ifNoneMatchSet = false; + return { + setHeader(header, value) { + if (header == "If-None-Match" && value == "bogusETag") { + Request.ifNoneMatchSet = true; + } + }, + async dispatch(method, payload) { + this.response = response; + return this.response; + }, + }; + }; + + return Request; +}; + +// A simple mock FxA that hands out tokens without checking them and doesn't +// expect tokens to be revoked. We have specific token tests further down that +// has more checks here. +let mockFxaInternal = { + getOAuthToken(options) { + Assert.equal(options.scope, "profile"); + return "token"; + }, +}; + +const PROFILE_OPTIONS = { + serverURL: "http://127.0.0.1:1111/v1", + fxai: mockFxaInternal, +}; + +/** + * Mock request error responder + * @param {Error} error + * Error object + * @returns {Function} + */ +let mockResponseError = function (error) { + return function () { + return { + setHeader() {}, + async dispatch(method, payload) { + throw error; + }, + }; + }; +}; + +add_test(function successfulResponse() { + let client = new FxAccountsProfileClient(PROFILE_OPTIONS); + let response = { + success: true, + status: STATUS_SUCCESS, + headers: { etag: "bogusETag" }, + body: '{"email":"someone@restmail.net","uid":"0d5c1a89b8c54580b8e3e8adadae864a"}', + }; + + client._Request = new mockResponse(response); + client.fetchProfile().then(function (result) { + Assert.equal( + client._Request._requestUri, + "http://127.0.0.1:1111/v1/profile" + ); + Assert.equal(result.body.email, "someone@restmail.net"); + Assert.equal(result.body.uid, "0d5c1a89b8c54580b8e3e8adadae864a"); + Assert.equal(result.etag, "bogusETag"); + run_next_test(); + }); +}); + +add_test(function setsIfNoneMatchETagHeader() { + let client = new FxAccountsProfileClient(PROFILE_OPTIONS); + let response = { + success: true, + status: STATUS_SUCCESS, + headers: {}, + body: '{"email":"someone@restmail.net","uid":"0d5c1a89b8c54580b8e3e8adadae864a"}', + }; + + let req = new mockResponse(response); + client._Request = req; + client.fetchProfile("bogusETag").then(function (result) { + Assert.equal( + client._Request._requestUri, + "http://127.0.0.1:1111/v1/profile" + ); + Assert.equal(result.body.email, "someone@restmail.net"); + Assert.equal(result.body.uid, "0d5c1a89b8c54580b8e3e8adadae864a"); + Assert.ok(req.ifNoneMatchSet); + run_next_test(); + }); +}); + +add_test(function successful304Response() { + let client = new FxAccountsProfileClient(PROFILE_OPTIONS); + let response = { + success: true, + headers: { etag: "bogusETag" }, + status: 304, + }; + + client._Request = new mockResponse(response); + client.fetchProfile().then(function (result) { + Assert.equal(result, null); + run_next_test(); + }); +}); + +add_test(function parseErrorResponse() { + let client = new FxAccountsProfileClient(PROFILE_OPTIONS); + let response = { + success: true, + status: STATUS_SUCCESS, + body: "unexpected", + }; + + client._Request = new mockResponse(response); + client.fetchProfile().catch(function (e) { + Assert.equal(e.name, "FxAccountsProfileClientError"); + Assert.equal(e.code, STATUS_SUCCESS); + Assert.equal(e.errno, ERRNO_PARSE); + Assert.equal(e.error, ERROR_PARSE); + Assert.equal(e.message, "unexpected"); + run_next_test(); + }); +}); + +add_test(function serverErrorResponse() { + let client = new FxAccountsProfileClient(PROFILE_OPTIONS); + let response = { + status: 500, + body: '{ "code": 500, "errno": 100, "error": "Bad Request", "message": "Something went wrong", "reason": "Because the internet" }', + }; + + client._Request = new mockResponse(response); + client.fetchProfile().catch(function (e) { + Assert.equal(e.name, "FxAccountsProfileClientError"); + Assert.equal(e.code, 500); + Assert.equal(e.errno, 100); + Assert.equal(e.error, "Bad Request"); + Assert.equal(e.message, "Something went wrong"); + run_next_test(); + }); +}); + +// Test that we get a token, then if we get a 401 we revoke it, get a new one +// and retry. +add_test(function server401ResponseThenSuccess() { + // The last token we handed out. + let lastToken = -1; + // The number of times our removeCachedOAuthToken function was called. + let numTokensRemoved = 0; + + let mockFxaWithRemove = { + getOAuthToken(options) { + Assert.equal(options.scope, "profile"); + return "" + ++lastToken; // tokens are strings. + }, + removeCachedOAuthToken(options) { + // This test never has more than 1 token alive at once, so the token + // being revoked must always be the last token we handed out. + Assert.equal(parseInt(options.token), lastToken); + ++numTokensRemoved; + }, + }; + let profileOptions = { + serverURL: "http://127.0.0.1:1111/v1", + fxai: mockFxaWithRemove, + }; + let client = new FxAccountsProfileClient(profileOptions); + + // 2 responses - first one implying the token has expired, second works. + let responses = [ + { + status: 401, + body: '{ "code": 401, "errno": 100, "error": "Token expired", "message": "That token is too old", "reason": "Because security" }', + }, + { + success: true, + status: STATUS_SUCCESS, + headers: {}, + body: '{"avatar":"http://example.com/image.jpg","id":"0d5c1a89b8c54580b8e3e8adadae864a"}', + }, + ]; + + let numRequests = 0; + let numAuthHeaders = 0; + // Like mockResponse but we want access to headers etc. + client._Request = function (requestUri) { + return { + setHeader(name, value) { + if (name == "Authorization") { + numAuthHeaders++; + Assert.equal(value, "Bearer " + lastToken); + } + }, + async dispatch(method, payload) { + this.response = responses[numRequests]; + ++numRequests; + return this.response; + }, + }; + }; + + client.fetchProfile().then(result => { + Assert.equal(result.body.avatar, "http://example.com/image.jpg"); + Assert.equal(result.body.id, "0d5c1a89b8c54580b8e3e8adadae864a"); + // should have been exactly 2 requests and exactly 2 auth headers. + Assert.equal(numRequests, 2); + Assert.equal(numAuthHeaders, 2); + // and we should have seen one token revoked. + Assert.equal(numTokensRemoved, 1); + + run_next_test(); + }); +}); + +// Test that we get a token, then if we get a 401 we revoke it, get a new one +// and retry - but we *still* get a 401 on the retry, so the caller sees that. +add_test(function server401ResponsePersists() { + // The last token we handed out. + let lastToken = -1; + // The number of times our removeCachedOAuthToken function was called. + let numTokensRemoved = 0; + + let mockFxaWithRemove = { + getOAuthToken(options) { + Assert.equal(options.scope, "profile"); + return "" + ++lastToken; // tokens are strings. + }, + removeCachedOAuthToken(options) { + // This test never has more than 1 token alive at once, so the token + // being revoked must always be the last token we handed out. + Assert.equal(parseInt(options.token), lastToken); + ++numTokensRemoved; + }, + }; + let profileOptions = { + serverURL: "http://127.0.0.1:1111/v1", + fxai: mockFxaWithRemove, + }; + let client = new FxAccountsProfileClient(profileOptions); + + let response = { + status: 401, + body: '{ "code": 401, "errno": 100, "error": "It\'s not your token, it\'s you!", "message": "I don\'t like you", "reason": "Because security" }', + }; + + let numRequests = 0; + let numAuthHeaders = 0; + client._Request = function (requestUri) { + return { + setHeader(name, value) { + if (name == "Authorization") { + numAuthHeaders++; + Assert.equal(value, "Bearer " + lastToken); + } + }, + async dispatch(method, payload) { + this.response = response; + ++numRequests; + return this.response; + }, + }; + }; + + client.fetchProfile().catch(function (e) { + Assert.equal(e.name, "FxAccountsProfileClientError"); + Assert.equal(e.code, 401); + Assert.equal(e.errno, 100); + Assert.equal(e.error, "It's not your token, it's you!"); + // should have been exactly 2 requests and exactly 2 auth headers. + Assert.equal(numRequests, 2); + Assert.equal(numAuthHeaders, 2); + // and we should have seen both tokens revoked. + Assert.equal(numTokensRemoved, 2); + run_next_test(); + }); +}); + +add_test(function networkErrorResponse() { + let client = new FxAccountsProfileClient({ + serverURL: "http://domain.dummy", + fxai: mockFxaInternal, + }); + client.fetchProfile().catch(function (e) { + Assert.equal(e.name, "FxAccountsProfileClientError"); + Assert.equal(e.code, null); + Assert.equal(e.errno, ERRNO_NETWORK); + Assert.equal(e.error, ERROR_NETWORK); + run_next_test(); + }); +}); + +add_test(function unsupportedMethod() { + let client = new FxAccountsProfileClient(PROFILE_OPTIONS); + + return client._createRequest("/profile", "PUT").catch(function (e) { + Assert.equal(e.name, "FxAccountsProfileClientError"); + Assert.equal(e.code, ERROR_CODE_METHOD_NOT_ALLOWED); + Assert.equal(e.errno, ERRNO_NETWORK); + Assert.equal(e.error, ERROR_NETWORK); + Assert.equal(e.message, ERROR_MSG_METHOD_NOT_ALLOWED); + run_next_test(); + }); +}); + +add_test(function onCompleteRequestError() { + let client = new FxAccountsProfileClient(PROFILE_OPTIONS); + client._Request = new mockResponseError(new Error("onComplete error")); + client.fetchProfile().catch(function (e) { + Assert.equal(e.name, "FxAccountsProfileClientError"); + Assert.equal(e.code, null); + Assert.equal(e.errno, ERRNO_NETWORK); + Assert.equal(e.error, ERROR_NETWORK); + Assert.equal(e.message, "Error: onComplete error"); + run_next_test(); + }); +}); + +add_test(function constructorTests() { + validationHelper( + undefined, + "Error: Missing 'serverURL' configuration option" + ); + + validationHelper({}, "Error: Missing 'serverURL' configuration option"); + + validationHelper({ serverURL: "badUrl" }, "Error: Invalid 'serverURL'"); + + run_next_test(); +}); + +add_test(function errorTests() { + let error1 = new FxAccountsProfileClientError(); + Assert.equal(error1.name, "FxAccountsProfileClientError"); + Assert.equal(error1.code, null); + Assert.equal(error1.errno, ERRNO_UNKNOWN_ERROR); + Assert.equal(error1.error, ERROR_UNKNOWN); + Assert.equal(error1.message, null); + + let error2 = new FxAccountsProfileClientError({ + code: STATUS_SUCCESS, + errno: 1, + error: "Error", + message: "Something", + }); + let fields2 = error2._toStringFields(); + let statusCode = 1; + + Assert.equal(error2.name, "FxAccountsProfileClientError"); + Assert.equal(error2.code, STATUS_SUCCESS); + Assert.equal(error2.errno, statusCode); + Assert.equal(error2.error, "Error"); + Assert.equal(error2.message, "Something"); + + Assert.equal(fields2.name, "FxAccountsProfileClientError"); + Assert.equal(fields2.code, STATUS_SUCCESS); + Assert.equal(fields2.errno, statusCode); + Assert.equal(fields2.error, "Error"); + Assert.equal(fields2.message, "Something"); + + Assert.ok(error2.toString().includes("Something")); + run_next_test(); +}); + +/** + * Quick way to test the "FxAccountsProfileClient" constructor. + * + * @param {Object} options + * FxAccountsProfileClient constructor options + * @param {String} expected + * Expected error message + * @returns {*} + */ +function validationHelper(options, expected) { + // add fxai to options - that missing isn't what we are testing here. + if (options) { + options.fxai = mockFxaInternal; + } + try { + new FxAccountsProfileClient(options); + } catch (e) { + return Assert.equal(e.toString(), expected); + } + throw new Error("Validation helper error"); +} |