tor-browser

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

commit f3616395722e5d80a794ba9a74738acfe40c19f1
parent af4d54f136dd57d3deecc0b44beeee7ece97b0c2
Author: Beatriz Rizental <brizental@torproject.org>
Date:   Tue, 27 May 2025 14:37:29 +0200

TB 43817: Add tests for Tor Browser

This is a catch all commits for adding any tests or
testing infrastructure that doesn't obviously fit
any other commit.

Diffstat:
Mpython/mozbuild/mozbuild/action/test_archive.py | 6++++++
Mtesting/marionette/harness/marionette_harness/tests/integration-tests.toml | 4++++
Mtesting/moz.build | 2++
Atesting/tor/manifest.toml | 6++++++
Atesting/tor/test_circuit_isolation.py | 101+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atesting/tor/test_network_check.py | 72++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
6 files changed, 191 insertions(+), 0 deletions(-)

diff --git a/python/mozbuild/mozbuild/action/test_archive.py b/python/mozbuild/mozbuild/action/test_archive.py @@ -229,6 +229,12 @@ ARCHIVE_FILES = { "pattern": "**", "dest": "certs", }, + { + "source": buildconfig.topsrcdir, + "base": "", + "pattern": "testing/tor", + "dest": "tor", + }, ], "cppunittest": [ {"source": STAGE, "base": "", "pattern": "cppunittest/**"}, diff --git a/testing/marionette/harness/marionette_harness/tests/integration-tests.toml b/testing/marionette/harness/marionette_harness/tests/integration-tests.toml @@ -56,6 +56,10 @@ ["include:../../../../../netwerk/test/marionette/manifest.toml"] +# tor tests + +["include:../../../../../testing/tor/manifest.toml"] + # toolkit tests ["include:../../../../../toolkit/components/antitracking/bouncetrackingprotection/test/marionette/manifest.toml"] diff --git a/testing/moz.build b/testing/moz.build @@ -23,3 +23,5 @@ DIRS += ["manifest", "mozbase", "mozharness"] PERFTESTS_MANIFESTS += [ "performance/perftest.toml", ] + +MARIONETTE_MANIFESTS += ["tor/manifest.toml"] diff --git a/testing/tor/manifest.toml b/testing/tor/manifest.toml @@ -0,0 +1,6 @@ +[DEFAULT] +tags = "tor" + +["test_circuit_isolation.py"] + +["test_network_check.py"] diff --git a/testing/tor/test_circuit_isolation.py b/testing/tor/test_circuit_isolation.py @@ -0,0 +1,101 @@ +from ipaddress import ip_address + +from marionette_driver import By +from marionette_driver.errors import NoSuchElementException +from marionette_harness import MarionetteTestCase + +TOR_BOOTSTRAP_TIMEOUT = 30000 # 30s + + +class TestCircuitIsolation(MarionetteTestCase): + def tearDown(self): + self.marionette.restart(in_app=False, clean=True) + super(TestCircuitIsolation, self).tearDown() + + def bootstrap(self): + with self.marionette.using_context("chrome"): + self.marionette.execute_async_script( + """ + const { TorConnect, TorConnectTopics } = ChromeUtils.importESModule( + "resource://gre/modules/TorConnect.sys.mjs" + ); + const [resolve] = arguments; + + function waitForBootstrap() { + const topic = TorConnectTopics.BootstrapComplete; + Services.obs.addObserver(function observer() { + Services.obs.removeObserver(observer, topic); + resolve(); + }, topic); + TorConnect.beginBootstrapping(); + } + + const stageTopic = TorConnectTopics.StageChange; + function stageObserver() { + if (TorConnect.canBeginNormalBootstrap) { + Services.obs.removeObserver(stageObserver, stageTopic); + waitForBootstrap(); + } + } + Services.obs.addObserver(stageObserver, stageTopic); + stageObserver(); + """, + script_timeout=TOR_BOOTSTRAP_TIMEOUT, + ) + + def extract_from_check_tpo(self): + # Fetch the IP from check.torproject.org. + # In addition to that, since we are loading this page, we + # perform some additional sanity checks. + self.marionette.navigate("https://check.torproject.org/") + # When check.tpo's check succeed (i.e., it thinks we're + # connecting through tor), we should be able to find a h1.on, + # with some message... + on = self.marionette.find_element(By.CLASS_NAME, "on") + self.assertIsNotNone( + on, + "h1.on not found, you might not be connected through tor", + ) + # ... but if it fails, the message is inside a h1.off. We want + # to make sure we do not find that either (even though there is + # no reason for both of the h1 to be outputted at the moment). + self.assertRaises( + NoSuchElementException, + self.marionette.find_element, + By.CLASS_NAME, + "off", + ) + ip = self.marionette.find_element(By.TAG_NAME, "strong") + return ip_address(ip.text.strip()) + + def extract_generic(self, url): + # Fetch the IP address from any generic page that only contains + # the address. + self.marionette.navigate(url) + return ip_address( + self.marionette.execute_script( + "return document.documentElement.textContent" + ).strip() + ) + + def test_circuit_isolation(self): + self.bootstrap() + ips = [ + self.extract_from_check_tpo(), + self.extract_generic("https://am.i.mullvad.net/ip"), + self.extract_generic("https://test1.ifconfig.me/ip"), + ] + self.logger.info(f"Found the following IP addresses: {ips}") + unique_ips = set(ips) + self.logger.info(f"Found the following unique IP addresses: {unique_ips}") + self.assertEqual( + len(ips), + len(unique_ips), + "Some of the IP addresses we got are not unique.", + ) + duplicate = self.extract_generic("https://test2.ifconfig.me/ip") + self.assertEqual( + ips[-1], + duplicate, + "Two IPs that were expected to be equal are different, we might be over isolating!", + ) diff --git a/testing/tor/test_network_check.py b/testing/tor/test_network_check.py @@ -0,0 +1,72 @@ +from marionette_driver import By, Wait, errors +from marionette_driver.localization import L10n +from marionette_harness import MarionetteTestCase + +NETWORK_CHECK_URL = "https://check.torproject.org/" +TOR_BOOTSTRAP_TIMEOUT = 30 # 30s + +STRINGS_LOCATION = "chrome://torbutton/locale/torConnect.properties" + + +class TestNetworkCheck(MarionetteTestCase): + def setUp(self): + MarionetteTestCase.setUp(self) + + self.l10n = L10n(self.marionette) + + def tearDown(self): + self.marionette.restart(in_app=False, clean=True) + super(TestNetworkCheck, self).tearDown() + + def attemptConnection(self, tries=1): + if tries > 3: + self.assertTrue(False, "Failed to connect to Tor after 3 attempts") + + connectBtn = self.marionette.find_element(By.ID, "connectButton") + Wait(self.marionette, timeout=10).until( + lambda _: connectBtn.is_displayed(), + message="Timed out waiting for tor connect button to show up.", + ) + connectBtn.click() + + try: + + def check(m): + if not m.get_url().startswith("about:torconnect"): + # We have finished connecting and have been redirected. + return True + + try: + heading = self.marionette.find_element(By.ID, "tor-connect-heading") + except errors.NoSuchElementException: + # Page is probably redirecting. + return False + + if heading.text not in [ + self.l10n.localize_property( + [STRINGS_LOCATION], "torConnect.torConnecting" + ), + self.l10n.localize_property( + [STRINGS_LOCATION], "torConnect.torConnected" + ), + ]: + raise ValueError("Tor connect page is not connecting or connected") + + return False + + Wait(self.marionette, timeout=TOR_BOOTSTRAP_TIMEOUT).until(check) + except (errors.TimeoutException, ValueError): + cancelBtn = self.marionette.find_element(By.ID, "cancelButton") + if cancelBtn.is_displayed(): + cancelBtn.click() + + self.attemptConnection(tries + 1) + + def test_network_check(self): + self.attemptConnection() + self.marionette.navigate(NETWORK_CHECK_URL) + self.assertRegex( + self.marionette.title, + r"^Congratulations\.", + f"{NETWORK_CHECK_URL} should have the expected title.", + )