test_profile_client.js (12496B)
1 /* Any copyright is dedicated to the Public Domain. 2 * http://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 "use strict"; 5 6 const { 7 ERRNO_NETWORK, 8 ERRNO_PARSE, 9 ERRNO_UNKNOWN_ERROR, 10 ERROR_CODE_METHOD_NOT_ALLOWED, 11 ERROR_MSG_METHOD_NOT_ALLOWED, 12 ERROR_NETWORK, 13 ERROR_PARSE, 14 ERROR_UNKNOWN, 15 } = ChromeUtils.importESModule( 16 "resource://gre/modules/FxAccountsCommon.sys.mjs" 17 ); 18 const { FxAccountsProfileClient, FxAccountsProfileClientError } = 19 ChromeUtils.importESModule( 20 "resource://gre/modules/FxAccountsProfileClient.sys.mjs" 21 ); 22 23 const STATUS_SUCCESS = 200; 24 25 /** 26 * Mock request responder 27 * 28 * @param {string} response 29 * Mocked raw response from the server 30 * @returns {Function} 31 */ 32 let mockResponse = function (response) { 33 let Request = function (requestUri) { 34 // Store the request uri so tests can inspect it 35 Request._requestUri = requestUri; 36 Request.ifNoneMatchSet = false; 37 return { 38 setHeader(header, value) { 39 if (header == "If-None-Match" && value == "bogusETag") { 40 Request.ifNoneMatchSet = true; 41 } 42 }, 43 async dispatch() { 44 this.response = response; 45 return this.response; 46 }, 47 }; 48 }; 49 50 return Request; 51 }; 52 53 // A simple mock FxA that hands out tokens without checking them and doesn't 54 // expect tokens to be revoked. We have specific token tests further down that 55 // has more checks here. 56 let mockFxaInternal = { 57 getOAuthToken(options) { 58 Assert.equal(options.scope, "profile"); 59 return "token"; 60 }, 61 }; 62 63 const PROFILE_OPTIONS = { 64 serverURL: "http://127.0.0.1:1111/v1", 65 fxai: mockFxaInternal, 66 }; 67 68 /** 69 * Mock request error responder 70 * 71 * @param {Error} error 72 * Error object 73 * @returns {Function} 74 */ 75 let mockResponseError = function (error) { 76 return function () { 77 return { 78 setHeader() {}, 79 async dispatch() { 80 throw error; 81 }, 82 }; 83 }; 84 }; 85 86 add_test(function successfulResponse() { 87 let client = new FxAccountsProfileClient(PROFILE_OPTIONS); 88 let response = { 89 success: true, 90 status: STATUS_SUCCESS, 91 headers: { etag: "bogusETag" }, 92 body: '{"email":"someone@restmail.net","uid":"0d5c1a89b8c54580b8e3e8adadae864a"}', 93 }; 94 95 client._Request = new mockResponse(response); 96 client.fetchProfile().then(function (result) { 97 Assert.equal( 98 client._Request._requestUri, 99 "http://127.0.0.1:1111/v1/profile" 100 ); 101 Assert.equal(result.body.email, "someone@restmail.net"); 102 Assert.equal(result.body.uid, "0d5c1a89b8c54580b8e3e8adadae864a"); 103 Assert.equal(result.etag, "bogusETag"); 104 run_next_test(); 105 }); 106 }); 107 108 add_test(function setsIfNoneMatchETagHeader() { 109 let client = new FxAccountsProfileClient(PROFILE_OPTIONS); 110 let response = { 111 success: true, 112 status: STATUS_SUCCESS, 113 headers: {}, 114 body: '{"email":"someone@restmail.net","uid":"0d5c1a89b8c54580b8e3e8adadae864a"}', 115 }; 116 117 let req = new mockResponse(response); 118 client._Request = req; 119 client.fetchProfile("bogusETag").then(function (result) { 120 Assert.equal( 121 client._Request._requestUri, 122 "http://127.0.0.1:1111/v1/profile" 123 ); 124 Assert.equal(result.body.email, "someone@restmail.net"); 125 Assert.equal(result.body.uid, "0d5c1a89b8c54580b8e3e8adadae864a"); 126 Assert.ok(req.ifNoneMatchSet); 127 run_next_test(); 128 }); 129 }); 130 131 add_test(function successful304Response() { 132 let client = new FxAccountsProfileClient(PROFILE_OPTIONS); 133 let response = { 134 success: true, 135 headers: { etag: "bogusETag" }, 136 status: 304, 137 }; 138 139 client._Request = new mockResponse(response); 140 client.fetchProfile().then(function (result) { 141 Assert.equal(result, null); 142 run_next_test(); 143 }); 144 }); 145 146 add_test(function parseErrorResponse() { 147 let client = new FxAccountsProfileClient(PROFILE_OPTIONS); 148 let response = { 149 success: true, 150 status: STATUS_SUCCESS, 151 body: "unexpected", 152 }; 153 154 client._Request = new mockResponse(response); 155 client.fetchProfile().catch(function (e) { 156 Assert.equal(e.name, "FxAccountsProfileClientError"); 157 Assert.equal(e.code, STATUS_SUCCESS); 158 Assert.equal(e.errno, ERRNO_PARSE); 159 Assert.equal(e.error, ERROR_PARSE); 160 Assert.equal(e.message, "unexpected"); 161 run_next_test(); 162 }); 163 }); 164 165 add_test(function serverErrorResponse() { 166 let client = new FxAccountsProfileClient(PROFILE_OPTIONS); 167 let response = { 168 status: 500, 169 body: '{ "code": 500, "errno": 100, "error": "Bad Request", "message": "Something went wrong", "reason": "Because the internet" }', 170 }; 171 172 client._Request = new mockResponse(response); 173 client.fetchProfile().catch(function (e) { 174 Assert.equal(e.name, "FxAccountsProfileClientError"); 175 Assert.equal(e.code, 500); 176 Assert.equal(e.errno, 100); 177 Assert.equal(e.error, "Bad Request"); 178 Assert.equal(e.message, "Something went wrong"); 179 run_next_test(); 180 }); 181 }); 182 183 // Test that we get a token, then if we get a 401 we revoke it, get a new one 184 // and retry. 185 add_test(function server401ResponseThenSuccess() { 186 // The last token we handed out. 187 let lastToken = -1; 188 // The number of times our removeCachedOAuthToken function was called. 189 let numTokensRemoved = 0; 190 191 let mockFxaWithRemove = { 192 getOAuthToken(options) { 193 Assert.equal(options.scope, "profile"); 194 return "" + ++lastToken; // tokens are strings. 195 }, 196 removeCachedOAuthToken(options) { 197 // This test never has more than 1 token alive at once, so the token 198 // being revoked must always be the last token we handed out. 199 Assert.equal(parseInt(options.token), lastToken); 200 ++numTokensRemoved; 201 }, 202 }; 203 let profileOptions = { 204 serverURL: "http://127.0.0.1:1111/v1", 205 fxai: mockFxaWithRemove, 206 }; 207 let client = new FxAccountsProfileClient(profileOptions); 208 209 // 2 responses - first one implying the token has expired, second works. 210 let responses = [ 211 { 212 status: 401, 213 body: '{ "code": 401, "errno": 100, "error": "Token expired", "message": "That token is too old", "reason": "Because security" }', 214 }, 215 { 216 success: true, 217 status: STATUS_SUCCESS, 218 headers: {}, 219 body: '{"avatar":"http://example.com/image.jpg","id":"0d5c1a89b8c54580b8e3e8adadae864a"}', 220 }, 221 ]; 222 223 let numRequests = 0; 224 let numAuthHeaders = 0; 225 // Like mockResponse but we want access to headers etc. 226 client._Request = function () { 227 return { 228 setHeader(name, value) { 229 if (name == "Authorization") { 230 numAuthHeaders++; 231 Assert.equal(value, "Bearer " + lastToken); 232 } 233 }, 234 async dispatch() { 235 this.response = responses[numRequests]; 236 ++numRequests; 237 return this.response; 238 }, 239 }; 240 }; 241 242 client.fetchProfile().then(result => { 243 Assert.equal(result.body.avatar, "http://example.com/image.jpg"); 244 Assert.equal(result.body.id, "0d5c1a89b8c54580b8e3e8adadae864a"); 245 // should have been exactly 2 requests and exactly 2 auth headers. 246 Assert.equal(numRequests, 2); 247 Assert.equal(numAuthHeaders, 2); 248 // and we should have seen one token revoked. 249 Assert.equal(numTokensRemoved, 1); 250 251 run_next_test(); 252 }); 253 }); 254 255 // Test that we get a token, then if we get a 401 we revoke it, get a new one 256 // and retry - but we *still* get a 401 on the retry, so the caller sees that. 257 add_test(function server401ResponsePersists() { 258 // The last token we handed out. 259 let lastToken = -1; 260 // The number of times our removeCachedOAuthToken function was called. 261 let numTokensRemoved = 0; 262 263 let mockFxaWithRemove = { 264 getOAuthToken(options) { 265 Assert.equal(options.scope, "profile"); 266 return "" + ++lastToken; // tokens are strings. 267 }, 268 removeCachedOAuthToken(options) { 269 // This test never has more than 1 token alive at once, so the token 270 // being revoked must always be the last token we handed out. 271 Assert.equal(parseInt(options.token), lastToken); 272 ++numTokensRemoved; 273 }, 274 }; 275 let profileOptions = { 276 serverURL: "http://127.0.0.1:1111/v1", 277 fxai: mockFxaWithRemove, 278 }; 279 let client = new FxAccountsProfileClient(profileOptions); 280 281 let response = { 282 status: 401, 283 body: '{ "code": 401, "errno": 100, "error": "It\'s not your token, it\'s you!", "message": "I don\'t like you", "reason": "Because security" }', 284 }; 285 286 let numRequests = 0; 287 let numAuthHeaders = 0; 288 client._Request = function () { 289 return { 290 setHeader(name, value) { 291 if (name == "Authorization") { 292 numAuthHeaders++; 293 Assert.equal(value, "Bearer " + lastToken); 294 } 295 }, 296 async dispatch() { 297 this.response = response; 298 ++numRequests; 299 return this.response; 300 }, 301 }; 302 }; 303 304 client.fetchProfile().catch(function (e) { 305 Assert.equal(e.name, "FxAccountsProfileClientError"); 306 Assert.equal(e.code, 401); 307 Assert.equal(e.errno, 100); 308 Assert.equal(e.error, "It's not your token, it's you!"); 309 // should have been exactly 2 requests and exactly 2 auth headers. 310 Assert.equal(numRequests, 2); 311 Assert.equal(numAuthHeaders, 2); 312 // and we should have seen both tokens revoked. 313 Assert.equal(numTokensRemoved, 2); 314 run_next_test(); 315 }); 316 }); 317 318 add_test(function networkErrorResponse() { 319 let client = new FxAccountsProfileClient({ 320 serverURL: "http://domain.dummy", 321 fxai: mockFxaInternal, 322 }); 323 client.fetchProfile().catch(function (e) { 324 Assert.equal(e.name, "FxAccountsProfileClientError"); 325 Assert.equal(e.code, null); 326 Assert.equal(e.errno, ERRNO_NETWORK); 327 Assert.equal(e.error, ERROR_NETWORK); 328 run_next_test(); 329 }); 330 }); 331 332 add_test(function unsupportedMethod() { 333 let client = new FxAccountsProfileClient(PROFILE_OPTIONS); 334 335 return client._createRequest("/profile", "PUT").catch(function (e) { 336 Assert.equal(e.name, "FxAccountsProfileClientError"); 337 Assert.equal(e.code, ERROR_CODE_METHOD_NOT_ALLOWED); 338 Assert.equal(e.errno, ERRNO_NETWORK); 339 Assert.equal(e.error, ERROR_NETWORK); 340 Assert.equal(e.message, ERROR_MSG_METHOD_NOT_ALLOWED); 341 run_next_test(); 342 }); 343 }); 344 345 add_test(function onCompleteRequestError() { 346 let client = new FxAccountsProfileClient(PROFILE_OPTIONS); 347 client._Request = new mockResponseError(new Error("onComplete error")); 348 client.fetchProfile().catch(function (e) { 349 Assert.equal(e.name, "FxAccountsProfileClientError"); 350 Assert.equal(e.code, null); 351 Assert.equal(e.errno, ERRNO_NETWORK); 352 Assert.equal(e.error, ERROR_NETWORK); 353 Assert.equal(e.message, "Error: onComplete error"); 354 run_next_test(); 355 }); 356 }); 357 358 add_test(function constructorTests() { 359 validationHelper( 360 undefined, 361 "Error: Missing 'serverURL' configuration option" 362 ); 363 364 validationHelper({}, "Error: Missing 'serverURL' configuration option"); 365 366 validationHelper({ serverURL: "badUrl" }, "Error: Invalid 'serverURL'"); 367 368 run_next_test(); 369 }); 370 371 add_test(function errorTests() { 372 let error1 = new FxAccountsProfileClientError(); 373 Assert.equal(error1.name, "FxAccountsProfileClientError"); 374 Assert.equal(error1.code, null); 375 Assert.equal(error1.errno, ERRNO_UNKNOWN_ERROR); 376 Assert.equal(error1.error, ERROR_UNKNOWN); 377 Assert.equal(error1.message, null); 378 379 let error2 = new FxAccountsProfileClientError({ 380 code: STATUS_SUCCESS, 381 errno: 1, 382 error: "Error", 383 message: "Something", 384 }); 385 let fields2 = error2._toStringFields(); 386 let statusCode = 1; 387 388 Assert.equal(error2.name, "FxAccountsProfileClientError"); 389 Assert.equal(error2.code, STATUS_SUCCESS); 390 Assert.equal(error2.errno, statusCode); 391 Assert.equal(error2.error, "Error"); 392 Assert.equal(error2.message, "Something"); 393 394 Assert.equal(fields2.name, "FxAccountsProfileClientError"); 395 Assert.equal(fields2.code, STATUS_SUCCESS); 396 Assert.equal(fields2.errno, statusCode); 397 Assert.equal(fields2.error, "Error"); 398 Assert.equal(fields2.message, "Something"); 399 400 Assert.ok(error2.toString().includes("Something")); 401 run_next_test(); 402 }); 403 404 /** 405 * Quick way to test the "FxAccountsProfileClient" constructor. 406 * 407 * @param {object} options 408 * FxAccountsProfileClient constructor options 409 * @param {string} expected 410 * Expected error message 411 * @returns {*} 412 */ 413 function validationHelper(options, expected) { 414 // add fxai to options - that missing isn't what we are testing here. 415 if (options) { 416 options.fxai = mockFxaInternal; 417 } 418 try { 419 new FxAccountsProfileClient(options); 420 } catch (e) { 421 return Assert.equal(e.toString(), expected); 422 } 423 throw new Error("Validation helper error"); 424 }