/* 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"); }