summaryrefslogtreecommitdiffstats
path: root/services/fxaccounts/tests/xpcshell/test_profile_client.js
diff options
context:
space:
mode:
Diffstat (limited to 'services/fxaccounts/tests/xpcshell/test_profile_client.js')
-rw-r--r--services/fxaccounts/tests/xpcshell/test_profile_client.js420
1 files changed, 420 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..e8bc47b63c
--- /dev/null
+++ b/services/fxaccounts/tests/xpcshell/test_profile_client.js
@@ -0,0 +1,420 @@
+/* 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.import("resource://gre/modules/FxAccountsCommon.js");
+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");
+}