test_merinoClient.js (26242B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 // Test for MerinoClient. 6 7 "use strict"; 8 9 ChromeUtils.defineESModuleGetters(this, { 10 MerinoClient: "moz-src:///browser/components/urlbar/MerinoClient.sys.mjs", 11 NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs", 12 NimbusTestUtils: "resource://testing-common/NimbusTestUtils.sys.mjs", 13 ObliviousHTTP: "resource://gre/modules/ObliviousHTTP.sys.mjs", 14 }); 15 16 // Set the `merino.timeoutMs` pref to a large value so that the client will not 17 // inadvertently time out during fetches. This is especially important on CI and 18 // when running this test in verify mode. Tasks that specifically test timeouts 19 // may need to set a more reasonable value for their duration. 20 const TEST_TIMEOUT_MS = 30000; 21 22 // The expected suggestion objects returned from `MerinoClient.fetch()`. 23 const EXPECTED_MERINO_SUGGESTIONS = []; 24 25 const { SEARCH_PARAMS } = MerinoClient; 26 27 let gClient; 28 29 add_setup(async function init() { 30 // Set up FOG (Glean). 31 do_get_profile(); 32 Services.fog.initializeFOG(); 33 34 UrlbarPrefs.set("merino.timeoutMs", TEST_TIMEOUT_MS); 35 registerCleanupFunction(() => { 36 UrlbarPrefs.clear("merino.timeoutMs"); 37 }); 38 39 gClient = new MerinoClient(); 40 await MerinoTestUtils.server.start(); 41 42 for (let suggestion of MerinoTestUtils.server.response.body.suggestions) { 43 EXPECTED_MERINO_SUGGESTIONS.push({ 44 ...suggestion, 45 request_id: MerinoTestUtils.server.response.body.request_id, 46 source: "merino", 47 }); 48 } 49 }); 50 51 // Checks client names. 52 add_task(async function name() { 53 Assert.equal( 54 gClient.name, 55 "anonymous", 56 "gClient name is 'anonymous' since it wasn't given a name" 57 ); 58 59 let client = new MerinoClient("New client"); 60 Assert.equal(client.name, "New client", "newClient name is correct"); 61 }); 62 63 // Does a successful fetch. 64 add_task(async function success() { 65 await fetchAndCheckSuggestions({ 66 expected: EXPECTED_MERINO_SUGGESTIONS, 67 }); 68 69 Assert.equal( 70 gClient.lastFetchStatus, 71 "success", 72 "The request successfully finished" 73 ); 74 assertTelemetry({ 75 latency: { 76 200: { 77 count: 1, 78 }, 79 }, 80 }); 81 }); 82 83 // Does a successful fetch that doesn't return any suggestions. 84 add_task(async function noSuggestions() { 85 let { suggestions } = MerinoTestUtils.server.response.body; 86 MerinoTestUtils.server.response.body.suggestions = []; 87 88 await fetchAndCheckSuggestions({ 89 expected: [], 90 }); 91 92 Assert.equal( 93 gClient.lastFetchStatus, 94 "no_suggestion", 95 "The request successfully finished without suggestions" 96 ); 97 assertTelemetry({ 98 latency: { 99 200: { 100 count: 1, 101 }, 102 }, 103 }); 104 105 MerinoTestUtils.server.response.body.suggestions = suggestions; 106 }); 107 108 // Tests `MerinoClient`'s simplistic caching mechanism. 109 add_task(async function cache() { 110 MerinoTestUtils.enableClientCache(true); 111 112 // Stub `MerinoClient`'s `Date.now()` so we can artificially set the date. 113 let sandbox = sinon.createSandbox(); 114 let dateNowStub = sandbox.stub( 115 Cu.getGlobalForObject(MerinoClient).Date, 116 "now" 117 ); 118 let startDateMs = Date.now(); 119 dateNowStub.returns(startDateMs); 120 121 // We'll do fetches with this client and pass it this provider. 122 let provider = "test-provider"; 123 let client = new MerinoClient("cache-test", { cachePeriodMs: 60 * 1000 }); 124 125 // We'll do each of these fetches in order. 126 let fetches = [ 127 { 128 timeOffsetMs: 0, 129 query: "aaa", 130 shouldCallMerino: true, 131 }, 132 { 133 timeOffsetMs: 30 * 1000, 134 query: "aaa", 135 shouldCallMerino: false, 136 }, 137 // This call is exactly when the cache period expires, so the cache should 138 // not be used. 139 { 140 timeOffsetMs: 60 * 1000, 141 query: "aaa", 142 shouldCallMerino: true, 143 }, 144 { 145 timeOffsetMs: 90 * 1000, 146 query: "aaa", 147 shouldCallMerino: false, 148 }, 149 // This call is well past the previous cache period (which expired at an 150 // offset of 120), so the cache should not be used. 151 { 152 timeOffsetMs: 200 * 1000, 153 query: "aaa", 154 shouldCallMerino: true, 155 }, 156 { 157 timeOffsetMs: 259 * 1000, 158 query: "aaa", 159 shouldCallMerino: false, 160 }, 161 // This call is exactly when the cache period expires, so the cache should 162 // not be used. 163 { 164 timeOffsetMs: 260 * 1000, 165 query: "aaa", 166 shouldCallMerino: true, 167 }, 168 { 169 timeOffsetMs: 261 * 1000, 170 query: "aaa", 171 shouldCallMerino: false, 172 }, 173 // A different query creates a different request URL, so the cache should 174 // not be used even though this request is within the current cache period. 175 { 176 timeOffsetMs: 262 * 1000, 177 query: "bbb", 178 shouldCallMerino: true, 179 }, 180 { 181 timeOffsetMs: 263 * 1000, 182 query: "bbb", 183 shouldCallMerino: false, 184 }, 185 { 186 timeOffsetMs: 264 * 1000, 187 query: "aaa", 188 shouldCallMerino: true, 189 }, 190 { 191 timeOffsetMs: 265 * 1000, 192 query: "aaa", 193 shouldCallMerino: false, 194 }, 195 ]; 196 197 // Do each fetch one after another. 198 for (let i = 0; i < fetches.length; i++) { 199 let fetch = fetches[i]; 200 info(`Fetch ${i} start: ` + JSON.stringify(fetch)); 201 202 let { timeOffsetMs, query, shouldCallMerino } = fetch; 203 204 // Set the date forward. 205 dateNowStub.returns(startDateMs + timeOffsetMs); 206 207 // Do the fetch. 208 let callsByProvider = await doFetchAndGetCalls(client, { 209 query, 210 providers: [provider], 211 }); 212 info(`Fetch ${i} callsByProvider: ` + JSON.stringify(callsByProvider)); 213 214 // Stringify each `URLSearchParams` in the `calls` array in each 215 // `[provider, calls]` for easier comparison. 216 let actualCalls = Object.entries(callsByProvider).map(([p, calls]) => [ 217 p, 218 calls.map(params => { 219 params.delete(MerinoClient.SEARCH_PARAMS.SESSION_ID); 220 params.delete(MerinoClient.SEARCH_PARAMS.SEQUENCE_NUMBER); 221 return params.toString(); 222 }), 223 ]); 224 225 if (!shouldCallMerino) { 226 Assert.deepEqual( 227 actualCalls, 228 [], 229 `Fetch ${i} should not have called Merino` 230 ); 231 } else { 232 // Build the expected params. 233 let expectedParams = new URLSearchParams(); 234 expectedParams.set(MerinoClient.SEARCH_PARAMS.PROVIDERS, provider); 235 expectedParams.set(MerinoClient.SEARCH_PARAMS.QUERY, query); 236 expectedParams.sort(); 237 238 Assert.deepEqual( 239 actualCalls, 240 [[provider, [expectedParams.toString()]]], 241 `Fetch ${i} should have called Merino` 242 ); 243 } 244 } 245 246 sandbox.restore(); 247 MerinoTestUtils.enableClientCache(false); 248 }); 249 250 async function doFetchAndGetCalls(client, fetchArgs) { 251 let callsByProvider = {}; 252 253 MerinoTestUtils.server.requestHandler = req => { 254 let params = new URLSearchParams(req.queryString); 255 params.sort(); 256 let provider = params.get("providers"); 257 callsByProvider[provider] ||= []; 258 callsByProvider[provider].push(params); 259 return { 260 body: { 261 request_id: "request_id", 262 suggestions: [{ foo: "bar" }], 263 }, 264 }; 265 }; 266 267 await client.fetch(fetchArgs); 268 269 MerinoTestUtils.server.requestHandler = null; 270 return callsByProvider; 271 } 272 273 // Checks a 204 "No content" response. 274 add_task(async function noContent() { 275 Services.fog.testResetFOG(); 276 277 MerinoTestUtils.server.response = { status: 204 }; 278 await fetchAndCheckSuggestions({ expected: [] }); 279 280 Assert.equal( 281 gClient.lastFetchStatus, 282 "no_suggestion", 283 "The request should have been recorded as no_suggestion" 284 ); 285 assertTelemetry({ 286 latency: { 287 204: { 288 count: 1, 289 }, 290 }, 291 }); 292 293 MerinoTestUtils.server.reset(); 294 }); 295 296 // Checks a response that's valid but also has some unexpected properties. 297 add_task(async function unexpectedResponseProperties() { 298 MerinoTestUtils.server.response.body.unexpectedString = "some value"; 299 MerinoTestUtils.server.response.body.unexpectedArray = ["a", "b", "c"]; 300 MerinoTestUtils.server.response.body.unexpectedObject = { foo: "bar" }; 301 302 await fetchAndCheckSuggestions({ 303 expected: EXPECTED_MERINO_SUGGESTIONS, 304 }); 305 306 Assert.equal( 307 gClient.lastFetchStatus, 308 "success", 309 "The request successfully finished" 310 ); 311 assertTelemetry({ 312 latency: { 313 200: { 314 count: 1, 315 }, 316 }, 317 }); 318 }); 319 320 // Checks some responses with unexpected response bodies. 321 add_task(async function unexpectedResponseBody() { 322 let responses = [ 323 { body: {} }, 324 { body: { bogus: [] } }, 325 { body: { suggestions: {} } }, 326 { body: { suggestions: [] } }, 327 { body: "" }, 328 { body: "bogus", contentType: "text/html" }, 329 ]; 330 331 for (let r of responses) { 332 info("Testing response: " + JSON.stringify(r)); 333 334 MerinoTestUtils.server.response = r; 335 await fetchAndCheckSuggestions({ expected: [] }); 336 337 Assert.equal( 338 gClient.lastFetchStatus, 339 "no_suggestion", 340 "The request successfully finished without suggestions" 341 ); 342 assertTelemetry({ 343 latency: { 344 200: { 345 count: 1, 346 }, 347 }, 348 }); 349 } 350 351 MerinoTestUtils.server.reset(); 352 }); 353 354 // Tests with a network error. 355 add_task(async function networkError() { 356 // This promise will be resolved when the client processes the network error. 357 let responsePromise = gClient.waitForNextResponse(); 358 359 await MerinoTestUtils.server.withNetworkError(async () => { 360 await fetchAndCheckSuggestions({ expected: [] }); 361 }); 362 363 // The client should have nulled out the timeout timer before `fetch()` 364 // returned. 365 Assert.strictEqual( 366 gClient._test_timeoutTimer, 367 null, 368 "timeoutTimer does not exist after fetch finished" 369 ); 370 371 // Wait for the client to process the network error. 372 await responsePromise; 373 374 Assert.equal( 375 gClient.lastFetchStatus, 376 "network_error", 377 "The request failed with a network error" 378 ); 379 }); 380 381 // Tests with an HTTP error. 382 add_task(async function httpError() { 383 MerinoTestUtils.server.response = { status: 500 }; 384 await fetchAndCheckSuggestions({ expected: [] }); 385 386 Assert.equal( 387 gClient.lastFetchStatus, 388 "http_error", 389 "The request failed with an HTTP error" 390 ); 391 assertTelemetry({ 392 latency: { 393 500: { 394 count: 1, 395 }, 396 }, 397 }); 398 399 MerinoTestUtils.server.reset(); 400 }); 401 402 // Tests a client timeout. 403 add_task(async function clientTimeout() { 404 await doClientTimeoutTest({ 405 prefTimeoutMs: 200, 406 responseDelayMs: 400, 407 }); 408 }); 409 410 // Tests a client timeout followed by an HTTP error. Only the timeout should be 411 // recorded. 412 add_task(async function clientTimeoutFollowedByHTTPError() { 413 MerinoTestUtils.server.response = { status: 500 }; 414 await doClientTimeoutTest({ 415 prefTimeoutMs: 200, 416 responseDelayMs: 400, 417 expectedResponseStatus: 500, 418 }); 419 }); 420 421 // Tests a client timeout when a timeout value is passed to `fetch()`, which 422 // should override the value in the `merino.timeoutMs` pref. 423 add_task(async function timeoutPassedToFetch() { 424 // Set up a timeline like this: 425 // 426 // 1ms: The timeout passed to `fetch()` elapses 427 // 400ms: Merino returns a response 428 // 30000ms: The timeout in the pref elapses 429 // 430 // The expected behavior is that the 1ms timeout is hit, the request fails 431 // with a timeout, and Merino later returns a response. If the 1ms timeout is 432 // not hit, then Merino will return a response before the 30000ms timeout 433 // elapses and the request will complete successfully. 434 435 await doClientTimeoutTest({ 436 prefTimeoutMs: 30000, 437 responseDelayMs: 400, 438 fetchArgs: { query: "search", timeoutMs: 1 }, 439 }); 440 }); 441 442 async function doClientTimeoutTest({ 443 prefTimeoutMs, 444 responseDelayMs, 445 fetchArgs = { query: "search" }, 446 expectedResponseStatus = 200, 447 } = {}) { 448 let originalPrefTimeoutMs = UrlbarPrefs.get("merino.timeoutMs"); 449 UrlbarPrefs.set("merino.timeoutMs", prefTimeoutMs); 450 451 // Make the server return a delayed response so the client times out waiting 452 // for it. 453 MerinoTestUtils.server.response.delay = responseDelayMs; 454 455 let responsePromise = gClient.waitForNextResponse(); 456 await fetchAndCheckSuggestions({ args: fetchArgs, expected: [] }); 457 458 Assert.equal(gClient.lastFetchStatus, "timeout", "The request timed out"); 459 460 // The client should have nulled out the timeout timer. 461 Assert.strictEqual( 462 gClient._test_timeoutTimer, 463 null, 464 "timeoutTimer does not exist after fetch finished" 465 ); 466 467 // The fetch controller should still exist because the fetch should remain 468 // ongoing. 469 Assert.ok( 470 gClient._test_fetchController, 471 "fetchController still exists after fetch finished" 472 ); 473 Assert.ok( 474 !gClient._test_fetchController.signal.aborted, 475 "fetchController is not aborted" 476 ); 477 478 // Wait for the client to receive the response. 479 let httpResponse = await responsePromise; 480 Assert.ok(httpResponse, "Response was received"); 481 Assert.equal(httpResponse.status, expectedResponseStatus, "Response status"); 482 483 // The client should have nulled out the fetch controller. 484 Assert.ok(!gClient._test_fetchController, "fetchController no longer exists"); 485 486 assertTelemetry({ 487 latency: { 488 [expectedResponseStatus.toString()]: { 489 count: 1, 490 minDurationMs: responseDelayMs, 491 }, 492 }, 493 }); 494 495 MerinoTestUtils.server.reset(); 496 UrlbarPrefs.set("merino.timeoutMs", originalPrefTimeoutMs); 497 } 498 499 // By design, when a fetch times out, the client allows it to finish so we can 500 // record its latency. But when a second fetch starts before the first finishes, 501 // the client should abort the first so that there is at most one fetch at a 502 // time. 503 add_task(async function newFetchAbortsPrevious() { 504 // Make the server return a very delayed response so that it would time out 505 // and we can start a second fetch that will abort the first fetch. 506 MerinoTestUtils.server.response.delay = 507 100 * UrlbarPrefs.get("merino.timeoutMs"); 508 509 // Do the first fetch. 510 await fetchAndCheckSuggestions({ expected: [] }); 511 512 // At this point, the timeout timer has fired, causing our `fetch()` call to 513 // return. However, the client's internal fetch should still be ongoing. 514 515 Assert.equal(gClient.lastFetchStatus, "timeout", "The request timed out"); 516 517 // The client should have nulled out the timeout timer. 518 Assert.strictEqual( 519 gClient._test_timeoutTimer, 520 null, 521 "timeoutTimer does not exist after first fetch finished" 522 ); 523 524 // The fetch controller should still exist because the fetch should remain 525 // ongoing. 526 Assert.ok( 527 gClient._test_fetchController, 528 "fetchController still exists after first fetch finished" 529 ); 530 Assert.ok( 531 !gClient._test_fetchController.signal.aborted, 532 "fetchController is not aborted" 533 ); 534 535 // Do the second fetch. This time don't delay the response. 536 delete MerinoTestUtils.server.response.delay; 537 await fetchAndCheckSuggestions({ 538 expected: EXPECTED_MERINO_SUGGESTIONS, 539 }); 540 541 Assert.equal( 542 gClient.lastFetchStatus, 543 "success", 544 "The request finished successfully" 545 ); 546 547 // The fetch was successful, so the client should have nulled out both 548 // properties. 549 Assert.ok( 550 !gClient._test_fetchController, 551 "fetchController does not exist after second fetch finished" 552 ); 553 Assert.strictEqual( 554 gClient._test_timeoutTimer, 555 null, 556 "timeoutTimer does not exist after second fetch finished" 557 ); 558 559 MerinoTestUtils.server.reset(); 560 }); 561 562 // The client should not include the `clientVariants` and `providers` search 563 // params when they are not set. 564 add_task(async function clientVariants_providers_notSet() { 565 UrlbarPrefs.set("merino.clientVariants", ""); 566 UrlbarPrefs.set("merino.providers", ""); 567 568 await fetchAndCheckSuggestions({ 569 expected: EXPECTED_MERINO_SUGGESTIONS, 570 }); 571 572 MerinoTestUtils.server.checkAndClearRequests([ 573 { 574 params: { 575 [SEARCH_PARAMS.QUERY]: "search", 576 [SEARCH_PARAMS.SEQUENCE_NUMBER]: 0, 577 }, 578 }, 579 ]); 580 581 UrlbarPrefs.clear("merino.clientVariants"); 582 UrlbarPrefs.clear("merino.providers"); 583 }); 584 585 // The client should include the `clientVariants` and `providers` search params 586 // when they are set using preferences. 587 add_task(async function clientVariants_providers_preferences() { 588 UrlbarPrefs.set("merino.clientVariants", "green"); 589 UrlbarPrefs.set("merino.providers", "pink"); 590 591 await fetchAndCheckSuggestions({ 592 expected: EXPECTED_MERINO_SUGGESTIONS, 593 }); 594 595 MerinoTestUtils.server.checkAndClearRequests([ 596 { 597 params: { 598 [SEARCH_PARAMS.QUERY]: "search", 599 [SEARCH_PARAMS.SEQUENCE_NUMBER]: 0, 600 [SEARCH_PARAMS.CLIENT_VARIANTS]: "green", 601 [SEARCH_PARAMS.PROVIDERS]: "pink", 602 }, 603 }, 604 ]); 605 606 UrlbarPrefs.clear("merino.clientVariants"); 607 UrlbarPrefs.clear("merino.providers"); 608 }); 609 610 // The client should include the `providers` search param when it's set by 611 // passing in the `providers` argument to `fetch()`. The argument should 612 // override the pref. This tests a single provider. 613 add_task(async function providers_arg_single() { 614 UrlbarPrefs.set("merino.providers", "prefShouldNotBeUsed"); 615 616 await fetchAndCheckSuggestions({ 617 args: { query: "search", providers: ["argShouldBeUsed"] }, 618 expected: EXPECTED_MERINO_SUGGESTIONS, 619 }); 620 621 MerinoTestUtils.server.checkAndClearRequests([ 622 { 623 params: { 624 [SEARCH_PARAMS.QUERY]: "search", 625 [SEARCH_PARAMS.SEQUENCE_NUMBER]: 0, 626 [SEARCH_PARAMS.PROVIDERS]: "argShouldBeUsed", 627 }, 628 }, 629 ]); 630 631 UrlbarPrefs.clear("merino.providers"); 632 }); 633 634 // The client should include the `providers` search param when it's set by 635 // passing in the `providers` argument to `fetch()`. The argument should 636 // override the pref. This tests multiple providers. 637 add_task(async function providers_arg_many() { 638 UrlbarPrefs.set("merino.providers", "prefShouldNotBeUsed"); 639 640 await fetchAndCheckSuggestions({ 641 args: { query: "search", providers: ["one", "two", "three"] }, 642 expected: EXPECTED_MERINO_SUGGESTIONS, 643 }); 644 645 MerinoTestUtils.server.checkAndClearRequests([ 646 { 647 params: { 648 [SEARCH_PARAMS.QUERY]: "search", 649 [SEARCH_PARAMS.SEQUENCE_NUMBER]: 0, 650 [SEARCH_PARAMS.PROVIDERS]: "one,two,three", 651 }, 652 }, 653 ]); 654 655 UrlbarPrefs.clear("merino.providers"); 656 }); 657 658 // The client should include the `providers` search param when it's set by 659 // passing in the `providers` argument to `fetch()` even when it's an empty 660 // array. The argument should override the pref. 661 add_task(async function providers_arg_empty() { 662 UrlbarPrefs.set("merino.providers", "prefShouldNotBeUsed"); 663 664 await fetchAndCheckSuggestions({ 665 args: { query: "search", providers: [] }, 666 expected: EXPECTED_MERINO_SUGGESTIONS, 667 }); 668 669 MerinoTestUtils.server.checkAndClearRequests([ 670 { 671 params: { 672 [SEARCH_PARAMS.QUERY]: "search", 673 [SEARCH_PARAMS.SEQUENCE_NUMBER]: 0, 674 [SEARCH_PARAMS.PROVIDERS]: "", 675 }, 676 }, 677 ]); 678 679 UrlbarPrefs.clear("merino.providers"); 680 }); 681 682 // Passes invalid `providers` arguments to `fetch()`. 683 add_task(async function providers_arg_invalid() { 684 let providersValues = ["", "nonempty", {}]; 685 686 for (let providers of providersValues) { 687 info("Calling fetch() with providers: " + JSON.stringify(providers)); 688 689 // `Assert.throws()` doesn't seem to work with async functions... 690 let error; 691 try { 692 await gClient.fetch({ providers, query: "search" }); 693 } catch (e) { 694 error = e; 695 } 696 Assert.ok(error, "fetch() threw an error"); 697 Assert.equal( 698 error.message, 699 "providers must be an array if given", 700 "Expected error was thrown" 701 ); 702 } 703 }); 704 705 // Tests setting the endpoint URL and query parameters via Nimbus. 706 add_task(async function nimbus() { 707 // Clear the endpoint pref so we know the URL is not being fetched from it. 708 let originalEndpointURL = UrlbarPrefs.get("merino.endpointURL"); 709 UrlbarPrefs.set("merino.endpointURL", ""); 710 711 await UrlbarTestUtils.initNimbusFeature(); 712 713 // First, with the endpoint pref set to an empty string, make sure no Merino 714 // suggestion are returned. 715 await fetchAndCheckSuggestions({ expected: [] }); 716 717 // Now install an experiment that sets the endpoint and other Merino-related 718 // variables. Make sure a suggestion is returned and the request includes the 719 // correct query params. 720 721 // `param`: The param name in the request URL 722 // `value`: The value to use for the param 723 // `variable`: The name of the Nimbus variable corresponding to the param 724 let expectedParams = [ 725 { 726 param: SEARCH_PARAMS.CLIENT_VARIANTS, 727 value: "test-client-variants", 728 variable: "merinoClientVariants", 729 }, 730 { 731 param: SEARCH_PARAMS.PROVIDERS, 732 value: "test-providers", 733 variable: "merinoProviders", 734 }, 735 ]; 736 737 // Set up the Nimbus variable values to create the experiment with. 738 let experimentValues = { 739 merinoEndpointURL: MerinoTestUtils.server.url.toString(), 740 }; 741 for (let { variable, value } of expectedParams) { 742 experimentValues[variable] = value; 743 } 744 745 await withExperiment(experimentValues, async () => { 746 await fetchAndCheckSuggestions({ expected: EXPECTED_MERINO_SUGGESTIONS }); 747 748 let params = { 749 [SEARCH_PARAMS.QUERY]: "search", 750 [SEARCH_PARAMS.SEQUENCE_NUMBER]: 0, 751 }; 752 for (let { param, value } of expectedParams) { 753 params[param] = value; 754 } 755 MerinoTestUtils.server.checkAndClearRequests([{ params }]); 756 }); 757 758 UrlbarPrefs.set("merino.endpointURL", originalEndpointURL); 759 }); 760 761 // OHTTP should be used when `client.allowOhttp` is true and the Merino OHTTP 762 // prefs are defined. 763 add_task(async function ohttp() { 764 Services.fog.testResetFOG(); 765 766 // Stub the `ObliviousHTTP` functions. 767 let sandbox = sinon.createSandbox(); 768 sandbox.stub(ObliviousHTTP, "getOHTTPConfig").resolves({}); 769 sandbox.stub(ObliviousHTTP, "ohttpRequest").resolves({ 770 status: 200, 771 json: async () => [], 772 }); 773 774 // Try all combinations of URLs and `allowOhttp`. OHTTP should be used only 775 // when both URLs are defined and `allowOhttp` is true. 776 for (let configUrl of ["", "https://example.com/config"]) { 777 for (let relayUrl of ["", "https://example.com/relay"]) { 778 for (let allowOhttp of [false, true]) { 779 UrlbarPrefs.set("merino.ohttpConfigURL", configUrl); 780 UrlbarPrefs.set("merino.ohttpRelayURL", relayUrl); 781 782 let client = new MerinoClient("ohttp client", { allowOhttp }); 783 await client.fetch({ query: "test" }); 784 785 let shouldUseOhttp = configUrl && relayUrl && allowOhttp; 786 let expectedCallCount = shouldUseOhttp ? 1 : 0; 787 788 Assert.equal( 789 ObliviousHTTP.getOHTTPConfig.callCount, 790 expectedCallCount, 791 "getOHTTPConfig should have been called the expected number of times" 792 ); 793 Assert.equal( 794 ObliviousHTTP.ohttpRequest.callCount, 795 expectedCallCount, 796 "ohttpRequest should have been called the expected number of times" 797 ); 798 799 let expectedLatencyLabel = "200"; 800 if (shouldUseOhttp) { 801 expectedLatencyLabel += "_ohttp"; 802 } 803 assertTelemetry({ 804 latency: { 805 [expectedLatencyLabel]: { 806 count: 1, 807 }, 808 }, 809 }); 810 } 811 } 812 } 813 814 sandbox.restore(); 815 }); 816 817 // If the client uses OHTTP but can't get an OHTTP config, it should return an 818 // empty suggestions array. 819 add_task(async function ohttp_noConfig() { 820 // Stub the `ObliviousHTTP` functions so that `getOHTTPConfig` returns null. 821 let sandbox = sinon.createSandbox(); 822 sandbox.stub(ObliviousHTTP, "getOHTTPConfig").resolves(null); 823 sandbox.stub(ObliviousHTTP, "ohttpRequest").resolves({ 824 status: 200, 825 json: async () => [], 826 }); 827 828 UrlbarPrefs.set("merino.ohttpConfigURL", "https://example.com/config"); 829 UrlbarPrefs.set("merino.ohttpRelayURL", "https://example.com/relay"); 830 831 let client = new MerinoClient("ohttp client", { allowOhttp: true }); 832 let suggestions = await client.fetch({ query: "test" }); 833 834 Assert.equal( 835 ObliviousHTTP.getOHTTPConfig.callCount, 836 1, 837 "getOHTTPConfig should have been called once" 838 ); 839 Assert.equal( 840 ObliviousHTTP.ohttpRequest.callCount, 841 0, 842 "ohttpRequest should not have been called" 843 ); 844 845 Assert.deepEqual( 846 suggestions, 847 [], 848 "The client should have returned an empty suggestions array" 849 ); 850 851 sandbox.restore(); 852 }); 853 854 async function fetchAndCheckSuggestions({ 855 expected, 856 args = { 857 query: "search", 858 }, 859 }) { 860 let actual = await gClient.fetch(args); 861 Assert.deepEqual(actual, expected, "Expected suggestions"); 862 gClient.resetSession(); 863 } 864 865 async function withExperiment(values, callback) { 866 const doExperimentCleanup = await NimbusTestUtils.enrollWithFeatureConfig( 867 { 868 featureId: NimbusFeatures.urlbar.featureId, 869 value: { 870 enabled: true, 871 ...values, 872 }, 873 }, 874 { 875 slug: "mock-experiment", 876 branchSlug: "treatment", 877 } 878 ); 879 await callback(); 880 await doExperimentCleanup(); 881 } 882 883 function assertTelemetry({ latency, reset = true }) { 884 for (let [label, expected] of Object.entries(latency)) { 885 let metric = Glean.urlbarMerino.latencyByResponseStatus[label]; 886 Assert.ok(metric, "A metric should exist for the expected label: " + label); 887 if (!metric) { 888 continue; 889 } 890 891 let actual = metric.testGetValue(); 892 Assert.ok(actual, "The metric should have a value for label: " + label); 893 if (!actual) { 894 continue; 895 } 896 897 info( 898 `Actual value for latency metric label '${label}': ` + 899 JSON.stringify(actual) 900 ); 901 902 let { count, minDurationMs = 0 } = expected; 903 904 Assert.strictEqual(actual.count, count, "count should be correct"); 905 906 Assert.greater(actual.sum, 0, "sum should be > 0"); 907 for (let bucket of Object.keys(actual.values)) { 908 Assert.greater(parseInt(bucket), 0, "bucket should be > 0"); 909 } 910 911 Assert.greaterOrEqual( 912 actual.sum, 913 minDurationMs, 914 "sum should be >= expected min duration" 915 ); 916 for (let bucket of Object.keys(actual.values)) { 917 Assert.greaterOrEqual( 918 parseInt(bucket), 919 minDurationMs, 920 "bucket should be >= expected min duration" 921 ); 922 } 923 } 924 925 if (reset) { 926 Services.fog.testResetFOG(); 927 } 928 }