tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

browser_test_local_network_access.js (22905B)


      1 "use strict";
      2 
      3 const PROMPT_ALLOW_BUTTON = -1;
      4 const PROMPT_NOT_NOW_BUTTON = 0;
      5 
      6 const { HttpServer } = ChromeUtils.importESModule(
      7  "resource://testing-common/httpd.sys.mjs"
      8 );
      9 
     10 const baseURL = getRootDirectory(gTestPath).replace(
     11  "chrome://mochitests/content",
     12  "https://example.com"
     13 );
     14 
     15 async function restorePermissions() {
     16  info("Restoring permissions");
     17  Services.obs.notifyObservers(null, "testonly-reload-permissions-from-disk");
     18  Services.perms.removeAll();
     19 }
     20 
     21 add_setup(async function () {
     22  await SpecialPowers.pushPrefEnv({
     23    set: [
     24      ["permissions.manager.defaultsUrl", ""],
     25      ["network.websocket.delay-failed-reconnects", false],
     26      ["network.websocket.max-connections", 1000],
     27      ["network.lna.block_trackers", true],
     28      ["network.lna.blocking", true],
     29      ["network.http.rcwn.enabled", false],
     30      ["network.lna.websocket.enabled", true],
     31      ["network.lna.local-network-to-localhost.skip-checks", false],
     32    ],
     33  });
     34  Services.obs.notifyObservers(null, "testonly-reload-permissions-from-disk");
     35 
     36  const server = new HttpServer();
     37  server.start(21555);
     38  registerServerHandlers(server);
     39 
     40  registerCleanupFunction(async () => {
     41    await restorePermissions();
     42    await new Promise(resolve => {
     43      server.stop(resolve);
     44    });
     45  });
     46 });
     47 
     48 requestLongerTimeout(10);
     49 
     50 function clickDoorhangerButton(buttonIndex, browser, notificationID) {
     51  let popup = PopupNotifications.getNotification(notificationID, browser);
     52  let notification = popup?.owner?.panel?.childNodes?.[0];
     53  ok(notification, "Notification popup is available");
     54 
     55  if (buttonIndex === PROMPT_ALLOW_BUTTON) {
     56    ok(true, "Triggering main action (allow)");
     57    notification.button.doCommand();
     58  } else {
     59    ok(true, "Triggering secondary action (deny)");
     60    notification.secondaryButton.doCommand();
     61  }
     62 }
     63 
     64 function observeAndCheck(testType, rand, expectedStatus, message) {
     65  return new Promise(resolve => {
     66    const url = `http://localhost:21555/?type=${testType}&rand=${rand}`;
     67    const observer = {
     68      observe(subject, topic) {
     69        if (topic !== "http-on-stop-request") {
     70          return;
     71        }
     72 
     73        let channel = subject.QueryInterface(Ci.nsIHttpChannel);
     74        if (!channel || channel.URI.spec !== url) {
     75          return;
     76        }
     77 
     78        is(channel.status, expectedStatus, message);
     79        Services.obs.removeObserver(observer, "http-on-stop-request");
     80        resolve();
     81      },
     82    };
     83    Services.obs.addObserver(observer, "http-on-stop-request");
     84  });
     85 }
     86 
     87 const testCases = [
     88  {
     89    type: "fetch",
     90    allowStatus: Cr.NS_OK,
     91    denyStatus: Cr.NS_ERROR_LOCAL_NETWORK_ACCESS_DENIED,
     92  },
     93  {
     94    type: "xhr",
     95    allowStatus: Cr.NS_OK,
     96    denyStatus: Cr.NS_ERROR_LOCAL_NETWORK_ACCESS_DENIED,
     97  },
     98  {
     99    type: "img",
    100    allowStatus: Cr.NS_OK,
    101    denyStatus: Cr.NS_ERROR_LOCAL_NETWORK_ACCESS_DENIED,
    102  },
    103  {
    104    type: "video",
    105    allowStatus: Cr.NS_OK,
    106    denyStatus: Cr.NS_ERROR_LOCAL_NETWORK_ACCESS_DENIED,
    107  },
    108  {
    109    type: "audio",
    110    allowStatus: Cr.NS_OK,
    111    denyStatus: Cr.NS_ERROR_LOCAL_NETWORK_ACCESS_DENIED,
    112  },
    113  {
    114    type: "iframe",
    115    allowStatus: Cr.NS_OK,
    116    denyStatus: Cr.NS_ERROR_LOCAL_NETWORK_ACCESS_DENIED,
    117  },
    118  {
    119    type: "script",
    120    allowStatus: Cr.NS_OK,
    121    denyStatus: Cr.NS_ERROR_LOCAL_NETWORK_ACCESS_DENIED,
    122  },
    123  {
    124    type: "font",
    125    allowStatus: Cr.NS_OK,
    126    denyStatus: Cr.NS_ERROR_LOCAL_NETWORK_ACCESS_DENIED,
    127  },
    128  {
    129    type: "websocket",
    130    allowStatus: Cr.NS_ERROR_WEBSOCKET_CONNECTION_REFUSED,
    131    denyStatus: Cr.NS_ERROR_LOCAL_NETWORK_ACCESS_DENIED,
    132  },
    133 ];
    134 
    135 function registerServerHandlers(server) {
    136  server.registerPathHandler("/", (request, response) => {
    137    const params = new URLSearchParams(request.queryString);
    138    const type = params.get("type");
    139 
    140    response.setHeader("Access-Control-Allow-Origin", "*", false);
    141 
    142    switch (type) {
    143      case "img":
    144        response.setHeader("Content-Type", "image/gif", false);
    145        response.setStatusLine(request.httpVersion, 200, "OK");
    146        response.write(
    147          atob("R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==")
    148        );
    149        break;
    150      case "audio":
    151        response.setHeader("Content-Type", "audio/wav", false);
    152        response.setStatusLine(request.httpVersion, 200, "OK");
    153        response.write(
    154          atob("UklGRhYAAABXQVZFZm10IBAAAAABAAEAIlYAAESsAAACABAAZGF0YQAAAAA=")
    155        );
    156        break;
    157      case "video":
    158        response.setHeader("Content-Type", "video/mp4", false);
    159        response.setStatusLine(request.httpVersion, 200, "OK");
    160        response.write(
    161          atob(
    162            "GkXfo0AgQoaBAUL3gQFC8oEEQvOBCEKCQAR3ZWJtQoeBAkKFgQIYU4BnQI0VSalmQCgq17FAAw9CQE2AQAZ3aGFtbXlXQUAGd2hhbW15RIlACECPQAAAAAAAFlSua0AxrkAu14EBY8WBAZyBACK1nEADdW5khkAFVl9WUDglhohAA1ZQOIOBAeBABrCBCLqBCB9DtnVAIueBAKNAHIEAAIAwAQCdASoIAAgAAUAmJaQAA3AA/vz0AAA="
    163          )
    164        );
    165        break;
    166      default:
    167        response.setHeader("Content-Type", "text/plain", false);
    168        response.setStatusLine(request.httpVersion, 200, "OK");
    169        response.write("hello");
    170    }
    171  });
    172 }
    173 
    174 async function runSingleTestCase(
    175  test,
    176  rand,
    177  expectedStatus,
    178  description,
    179  userAction = null,
    180  notificationID = null
    181 ) {
    182  info(description);
    183 
    184  const promise = observeAndCheck(test.type, rand, expectedStatus, description);
    185  const tab = await BrowserTestUtils.openNewForegroundTab(
    186    gBrowser,
    187    `${baseURL}page_with_non_trackers.html?test=${test.type}&rand=${rand}`
    188  );
    189 
    190  if (userAction && notificationID) {
    191    const buttonNum =
    192      userAction === "allow" ? PROMPT_ALLOW_BUTTON : PROMPT_NOT_NOW_BUTTON;
    193 
    194    await BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popupshown");
    195    clickDoorhangerButton(buttonNum, gBrowser.selectedBrowser, notificationID);
    196  }
    197 
    198  await promise;
    199  gBrowser.removeTab(tab);
    200 }
    201 
    202 async function runPromptedLnaTest(test, overrideLabel, notificationID) {
    203  const promptActions = ["allow", "deny"];
    204  for (const userAction of promptActions) {
    205    const rand = Math.random();
    206    const expectedStatus =
    207      userAction === "allow" ? test.allowStatus : test.denyStatus;
    208 
    209    await runSingleTestCase(
    210      test,
    211      rand,
    212      expectedStatus,
    213      `LNA test (${overrideLabel}) for ${test.type} with user action: ${userAction}`,
    214      userAction,
    215      notificationID
    216    );
    217 
    218    // Wait some time for cache entry to be updated
    219    // XXX(valentin) though this should not be necessary.
    220    // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
    221    await new Promise(resolve => setTimeout(resolve, 300));
    222 
    223    // Now run the test again with cached main document
    224    await runSingleTestCase(
    225      test,
    226      rand,
    227      expectedStatus,
    228      `LNA test (${overrideLabel}) for ${test.type} with user action: ${userAction}`,
    229      userAction,
    230      notificationID
    231    );
    232  }
    233 }
    234 
    235 add_task(async function test_lna_prompt_behavior() {
    236  // Non-LNA test: no prompt expected
    237  for (const test of testCases) {
    238    const rand = Math.random();
    239    await runSingleTestCase(
    240      test,
    241      rand,
    242      test.allowStatus,
    243      `Non-LNA test for ${test.type}`
    244    );
    245  }
    246 
    247  // Public -> Local test (localhost permission)
    248  Services.prefs.setCharPref(
    249    "network.lna.address_space.public.override",
    250    "127.0.0.1:4443"
    251  );
    252  for (const test of testCases) {
    253    await runPromptedLnaTest(test, "public", "localhost");
    254  }
    255 
    256  // Public -> Private (local-network permission)
    257  Services.prefs.setCharPref(
    258    "network.lna.address_space.private.override",
    259    "127.0.0.1:21555"
    260  );
    261  for (const test of testCases) {
    262    await runPromptedLnaTest(test, "private", "local-network");
    263  }
    264 
    265  Services.prefs.clearUserPref("network.lna.address_space.public.override");
    266  Services.prefs.clearUserPref("network.lna.address_space.private.override");
    267 });
    268 
    269 add_task(async function test_lna_cancellation_during_prompt() {
    270  info("Testing LNA cancellation during permission prompt");
    271 
    272  // Disable RCWN but enable caching for this test
    273  await SpecialPowers.pushPrefEnv({
    274    set: [
    275      ["network.http.rcwn.enabled", false],
    276      ["browser.cache.disk.enable", true],
    277      ["browser.cache.memory.enable", true],
    278      ["network.lna.address_space.public.override", "127.0.0.1:4443"],
    279    ],
    280  });
    281 
    282  const testType = "fetch";
    283  const rand1 = Math.random();
    284 
    285  // Test 1: Cancel request during LNA prompt and verify proper cleanup
    286  info(
    287    "Step 1: Making request that will trigger LNA prompt, then cancelling it"
    288  );
    289 
    290  // Open tab and wait for LNA prompt
    291  const tab1 = await BrowserTestUtils.openNewForegroundTab(
    292    gBrowser,
    293    `${baseURL}page_with_non_trackers.html?test=${testType}&rand=${rand1}`
    294  );
    295 
    296  // Wait for the LNA permission prompt to appear
    297  await BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popupshown");
    298  info("LNA permission prompt appeared");
    299  gBrowser.removeTab(tab1);
    300  // Navigate to a new URL (which should cancel the pending request)
    301  const tab2 = await BrowserTestUtils.openNewForegroundTab(
    302    gBrowser,
    303    `${baseURL}page_with_non_trackers.html?test=${testType}&rand=${rand1}`
    304  );
    305  info("Navigated to new URL, request should be cancelled");
    306 
    307  // Wait for the navigation to complete
    308  await BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popupshown");
    309  clickDoorhangerButton(
    310    PROMPT_ALLOW_BUTTON,
    311    gBrowser.selectedBrowser,
    312    "localhost"
    313  );
    314 
    315  // Close the first tab now that we're done with it
    316  gBrowser.removeTab(tab2);
    317 
    318  // The main test objective is complete - we verified that cancellation
    319  // during LNA prompt works without hanging channels. The navigation
    320  // completed successfully, which means our fix is working correctly.
    321  info(
    322    "Test completed successfully - cancellation during LNA prompt handled correctly"
    323  );
    324 
    325  await SpecialPowers.popPrefEnv();
    326 });
    327 
    328 add_task(async function test_lna_top_level_navigation_bypass() {
    329  info("Testing that top-level navigation to localhost bypasses LNA checks");
    330 
    331  // Set up LNA to trigger for localhost connections and enable top-level navigation bypass
    332  await SpecialPowers.pushPrefEnv({
    333    set: [
    334      ["network.lna.address_space.public.override", "127.0.0.1:4443"],
    335      ["network.lna.allow_top_level_navigation", true],
    336    ],
    337  });
    338 
    339  requestLongerTimeout(1);
    340 
    341  // Observer to verify that the navigation request succeeds without LNA error
    342  const navigationObserver = {
    343    observe(subject, topic) {
    344      if (topic !== "http-on-stop-request") {
    345        return;
    346      }
    347 
    348      let channel = subject.QueryInterface(Ci.nsIHttpChannel);
    349      if (!channel || !channel.URI.spec.includes("localhost:21555")) {
    350        return;
    351      }
    352 
    353      // For top-level navigation, we expect success (not LNA denied)
    354      // The channel status should be NS_OK, not NS_ERROR_LOCAL_NETWORK_ACCESS_DENIED
    355      is(
    356        channel.status,
    357        Cr.NS_OK,
    358        "Top-level navigation to localhost should not be blocked by LNA"
    359      );
    360 
    361      Services.obs.removeObserver(navigationObserver, "http-on-stop-request");
    362    },
    363  };
    364 
    365  Services.obs.addObserver(navigationObserver, "http-on-stop-request");
    366 
    367  try {
    368    // Load the test page which will automatically navigate to localhost
    369    info("Loading test page that will trigger navigation to localhost");
    370 
    371    // Open the initial page - it will automatically navigate to localhost
    372    const tab = await BrowserTestUtils.openNewForegroundTab(
    373      gBrowser,
    374      `${baseURL}page_with_non_trackers.html?isTopLevelNavigation=true`
    375    );
    376 
    377    // Wait for the navigation to complete
    378    info("Waiting for navigation to localhost to complete");
    379    await BrowserTestUtils.browserLoaded(tab.linkedBrowser, false, url =>
    380      url.includes("localhost:21555")
    381    );
    382 
    383    // Verify that no LNA permission prompt appeared
    384    // If our fix works correctly, there should be no popup notification
    385    let popup = PopupNotifications.getNotification(
    386      "localhost",
    387      tab.linkedBrowser
    388    );
    389    ok(
    390      !popup,
    391      "No LNA permission prompt should appear for top-level navigation"
    392    );
    393 
    394    // Verify the page loaded successfully
    395    let location = await SpecialPowers.spawn(tab.linkedBrowser, [], () => {
    396      return content.location.href;
    397    });
    398 
    399    ok(
    400      location.includes("localhost:21555"),
    401      "Top-level navigation to localhost should succeed"
    402    );
    403 
    404    gBrowser.removeTab(tab);
    405 
    406    info("Top-level navigation test completed successfully");
    407  } catch (error) {
    408    ok(false, `Top-level navigation test failed: ${error.message}`);
    409  }
    410 
    411  await SpecialPowers.popPrefEnv();
    412 });
    413 
    414 add_task(async function test_lna_top_level_navigation_disabled() {
    415  info("Testing that top-level navigation LNA bypass can be disabled via pref");
    416 
    417  // Set up LNA to trigger for localhost connections but disable top-level navigation bypass
    418  await SpecialPowers.pushPrefEnv({
    419    set: [
    420      ["network.lna.address_space.public.override", "127.0.0.1:4443"],
    421      ["network.lna.allow_top_level_navigation", false],
    422    ],
    423  });
    424 
    425  requestLongerTimeout(1);
    426 
    427  try {
    428    // Load the test page which will attempt to navigate to localhost
    429    info("Loading test page that will try to navigate to localhost");
    430    const tab = await BrowserTestUtils.openNewForegroundTab(
    431      gBrowser,
    432      `${baseURL}page_with_non_trackers.html?isTopLevelNavigation=true`
    433    );
    434 
    435    // Wait for LNA permission prompt to appear (since bypass is disabled)
    436    info("Waiting for LNA permission prompt to appear");
    437    await BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popupshown");
    438 
    439    // Verify that LNA permission prompt did appear
    440    let popup = PopupNotifications.getNotification(
    441      "localhost",
    442      tab.linkedBrowser
    443    );
    444    ok(popup, "LNA permission prompt should appear when bypass is disabled");
    445 
    446    // Allow the permission to complete the navigation
    447    clickDoorhangerButton(
    448      PROMPT_ALLOW_BUTTON,
    449      gBrowser.selectedBrowser,
    450      "localhost"
    451    );
    452 
    453    // Wait for navigation to complete after permission granted
    454    await BrowserTestUtils.browserLoaded(tab.linkedBrowser, false, url =>
    455      url.includes("localhost:21555")
    456    );
    457 
    458    gBrowser.removeTab(tab);
    459 
    460    info("Top-level navigation disabled test completed successfully");
    461  } catch (error) {
    462    ok(false, `Top-level navigation disabled test failed: ${error.message}`);
    463  }
    464 
    465  await SpecialPowers.popPrefEnv();
    466 });
    467 
    468 add_task(async function test_lna_websocket_preference() {
    469  info("Testing network.lna.websocket.enabled preference");
    470 
    471  // Set up LNA to trigger for localhost connections
    472  await SpecialPowers.pushPrefEnv({
    473    set: [
    474      ["network.lna.address_space.public.override", "127.0.0.1:4443"],
    475      ["network.lna.blocking", true],
    476      ["network.lna.websocket.enabled", false], // Disable WebSocket LNA checks
    477    ],
    478  });
    479 
    480  try {
    481    // Test WebSocket with LNA disabled - should bypass LNA and get connection refused
    482    const websocketTest = {
    483      type: "websocket",
    484      allowStatus: Cr.NS_ERROR_WEBSOCKET_CONNECTION_REFUSED,
    485      denyStatus: Cr.NS_ERROR_LOCAL_NETWORK_ACCESS_DENIED,
    486    };
    487 
    488    const rand = Math.random();
    489    const promise = observeAndCheck(
    490      websocketTest.type,
    491      rand,
    492      websocketTest.allowStatus, // Should get connection refused, not LNA denied
    493      "WebSocket test with LNA disabled should bypass LNA checks"
    494    );
    495 
    496    const tab = await BrowserTestUtils.openNewForegroundTab(
    497      gBrowser,
    498      `${baseURL}page_with_non_trackers.html?test=${websocketTest.type}&rand=${rand}`
    499    );
    500 
    501    await promise;
    502    gBrowser.removeTab(tab);
    503 
    504    info(
    505      "WebSocket LNA disabled test completed - connection was allowed to proceed"
    506    );
    507 
    508    // Now test with WebSocket LNA enabled - should trigger LNA denial
    509    await SpecialPowers.pushPrefEnv({
    510      set: [
    511        ["network.lna.websocket.enabled", true], // Enable WebSocket LNA checks
    512        ["network.localhost.prompt.testing", true],
    513        ["network.localhost.prompt.testing.allow", false],
    514      ],
    515    });
    516 
    517    const rand2 = Math.random();
    518    const promise2 = observeAndCheck(
    519      websocketTest.type,
    520      rand2,
    521      websocketTest.denyStatus, // Should get LNA denied
    522      "WebSocket test with LNA enabled should trigger LNA checks"
    523    );
    524 
    525    const tab2 = await BrowserTestUtils.openNewForegroundTab(
    526      gBrowser,
    527      `${baseURL}page_with_non_trackers.html?test=${websocketTest.type}&rand=${rand2}`
    528    );
    529 
    530    await promise2;
    531    gBrowser.removeTab(tab2);
    532 
    533    info("WebSocket LNA enabled test completed - LNA checks were applied");
    534  } catch (error) {
    535    ok(false, `WebSocket LNA preference test failed: ${error.message}`);
    536  }
    537 
    538  await SpecialPowers.popPrefEnv();
    539 });
    540 
    541 add_task(async function test_lna_prompt_timeout() {
    542  info("Testing LNA permission prompt timeout");
    543 
    544  // Set up a short timeout for testing (1 second instead of 5 minutes)
    545  await SpecialPowers.pushPrefEnv({
    546    set: [
    547      ["network.lna.address_space.public.override", "127.0.0.1:4443"],
    548      ["network.lna.prompt.timeout", 1000], // 1 second timeout for testing
    549    ],
    550  });
    551 
    552  try {
    553    const testType = "fetch";
    554    const rand = Math.random();
    555 
    556    info("Triggering LNA prompt that will timeout");
    557 
    558    // Set up observer to verify request fails with LNA denied status
    559    const promise = observeAndCheck(
    560      testType,
    561      rand,
    562      Cr.NS_ERROR_LOCAL_NETWORK_ACCESS_DENIED,
    563      "LNA request should fail after prompt timeout"
    564    );
    565 
    566    // Open tab that will trigger LNA prompt
    567    const tab = await BrowserTestUtils.openNewForegroundTab(
    568      gBrowser,
    569      `${baseURL}page_with_non_trackers.html?test=${testType}&rand=${rand}`
    570    );
    571 
    572    // Wait for LNA permission prompt to appear
    573    await BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popupshown");
    574    info("LNA permission prompt appeared");
    575 
    576    // Verify prompt is visible
    577    let popup = PopupNotifications.getNotification(
    578      "localhost",
    579      tab.linkedBrowser
    580    );
    581    ok(popup, "LNA permission prompt should be visible");
    582 
    583    // Do NOT click any button - let it timeout
    584    info("Waiting for prompt to timeout (1 second)...");
    585 
    586    // Wait for timeout + a small buffer to ensure timeout has fired
    587    // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
    588    await new Promise(resolve => setTimeout(resolve, 1500));
    589 
    590    // Verify prompt has been dismissed
    591    popup = PopupNotifications.getNotification("localhost", tab.linkedBrowser);
    592    ok(!popup, "LNA permission prompt should be dismissed after timeout");
    593 
    594    // Wait for the network request to complete with denial status
    595    await promise;
    596 
    597    gBrowser.removeTab(tab);
    598 
    599    info("LNA prompt timeout test completed successfully");
    600  } catch (error) {
    601    ok(false, `LNA prompt timeout test failed: ${error.message}`);
    602  }
    603 
    604  await SpecialPowers.popPrefEnv();
    605 });
    606 
    607 // Test that telemetry is recorded when LNA prompt is shown
    608 // and not incremented for subsequent requests with cached permission
    609 add_task(async function test_lna_prompt_telemetry() {
    610  await restorePermissions();
    611 
    612  // Reset telemetry
    613  Services.fog.testResetFOG();
    614  await SpecialPowers.pushPrefEnv({
    615    set: [["network.lna.address_space.public.override", "127.0.0.1:4443"]],
    616  });
    617 
    618  const rand1 = Math.random();
    619  const tab = await BrowserTestUtils.openNewForegroundTab(
    620    gBrowser,
    621    `${baseURL}page_with_non_trackers.html?test=fetch&rand=${rand1}`
    622  );
    623 
    624  // Wait for the prompt to appear
    625  await BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popupshown");
    626 
    627  // Verify telemetry was recorded
    628  let metricValue =
    629    await Glean.networking.localNetworkAccessPromptsShown.localhost.testGetValue();
    630  is(metricValue, 1, "Should record telemetry when localhost prompt is shown");
    631 
    632  // Grant permission
    633  clickDoorhangerButton(
    634    PROMPT_ALLOW_BUTTON,
    635    gBrowser.selectedBrowser,
    636    "localhost"
    637  );
    638 
    639  // Wait for permission to be saved
    640  // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
    641  await new Promise(resolve => setTimeout(resolve, 300));
    642 
    643  // Make a second request in the same tab with cached permission
    644  const rand2 = Math.random();
    645  const promise = observeAndCheck(
    646    "fetch",
    647    rand2,
    648    Cr.NS_OK,
    649    "Second request should succeed without prompt"
    650  );
    651  await SpecialPowers.spawn(tab.linkedBrowser, [rand2], async rand => {
    652    await content.fetch(`http://localhost:21555/?type=fetch&rand=${rand}`);
    653  });
    654  await promise;
    655 
    656  // Verify telemetry was not incremented
    657  metricValue =
    658    await Glean.networking.localNetworkAccessPromptsShown.localhost.testGetValue();
    659  is(
    660    metricValue,
    661    1,
    662    "Telemetry should not increment for requests with cached permission"
    663  );
    664 
    665  gBrowser.removeTab(tab);
    666  await SpecialPowers.popPrefEnv();
    667 });
    668 
    669 // Test that telemetry is recorded when user denies LNA prompt
    670 // and not incremented for subsequent requests with temporary deny permission
    671 add_task(async function test_lna_prompt_telemetry_deny() {
    672  await restorePermissions();
    673 
    674  // Reset telemetry
    675  Services.fog.testResetFOG();
    676  await SpecialPowers.pushPrefEnv({
    677    set: [["network.lna.address_space.public.override", "127.0.0.1:4443"]],
    678  });
    679 
    680  const rand1 = Math.random();
    681  const promise1 = observeAndCheck(
    682    "fetch",
    683    rand1,
    684    Cr.NS_ERROR_LOCAL_NETWORK_ACCESS_DENIED,
    685    "First request should be denied"
    686  );
    687  const tab = await BrowserTestUtils.openNewForegroundTab(
    688    gBrowser,
    689    `${baseURL}page_with_non_trackers.html?test=fetch&rand=${rand1}`
    690  );
    691 
    692  // Wait for the prompt to appear
    693  await BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popupshown");
    694 
    695  // Verify telemetry was recorded
    696  let metricValue =
    697    await Glean.networking.localNetworkAccessPromptsShown.localhost.testGetValue();
    698  is(metricValue, 1, "Should record telemetry when localhost prompt is shown");
    699 
    700  // Deny permission
    701  clickDoorhangerButton(
    702    PROMPT_NOT_NOW_BUTTON,
    703    gBrowser.selectedBrowser,
    704    "localhost"
    705  );
    706 
    707  await promise1;
    708 
    709  // Wait for permission to be saved
    710  // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
    711  await new Promise(resolve => setTimeout(resolve, 300));
    712 
    713  // Make a second request - should be auto-denied without showing prompt
    714  // because a temporary deny permission was saved
    715  const rand2 = Math.random();
    716  const promise2 = observeAndCheck(
    717    "fetch",
    718    rand2,
    719    Cr.NS_ERROR_LOCAL_NETWORK_ACCESS_DENIED,
    720    "Second request should be auto-denied with temporary permission"
    721  );
    722  await SpecialPowers.spawn(tab.linkedBrowser, [rand2], async rand => {
    723    await content
    724      .fetch(`http://localhost:21555/?type=fetch&rand=${rand}`)
    725      .catch(() => {});
    726  });
    727 
    728  await promise2;
    729 
    730  // Verify telemetry was not incremented (no prompt shown with temporary deny)
    731  metricValue =
    732    await Glean.networking.localNetworkAccessPromptsShown.localhost.testGetValue();
    733  is(
    734    metricValue,
    735    1,
    736    "Telemetry should not increment for requests with temporary deny permission"
    737  );
    738 
    739  gBrowser.removeTab(tab);
    740  await SpecialPowers.popPrefEnv();
    741 });