diff --git a/Makefile b/Makefile index af8e4a340f..074ad4f1d3 100644 --- a/Makefile +++ b/Makefile @@ -172,6 +172,8 @@ safety: ## Run `safety check` to check python dependencies for vulnerabilities. --ignore 61893 \ --ignore 62019 \ --ignore 62044 \ + --ignore 63066 \ + --ignore 63227 \ --full-report -r $$req_file \ && echo -e '\n' \ || exit 1; \ diff --git a/securedrop/requirements/python3/test-requirements.in b/securedrop/requirements/python3/test-requirements.in index 94e9547ced..bdbc7f0c7f 100644 --- a/securedrop/requirements/python3/test-requirements.in +++ b/securedrop/requirements/python3/test-requirements.in @@ -14,8 +14,8 @@ pytest-cov pytest-mock requests[socks]>=2.31.0 setuptools>=56.0.0 -selenium>=3.141.0 -tbselenium>=0.5.2 +selenium>4.15.1 +tbselenium>=0.8.1 pyvirtualdisplay urllib3>=1.26.5 # mypy and co. diff --git a/securedrop/requirements/python3/test-requirements.txt b/securedrop/requirements/python3/test-requirements.txt index f0f8782353..c9a8490f82 100644 --- a/securedrop/requirements/python3/test-requirements.txt +++ b/securedrop/requirements/python3/test-requirements.txt @@ -11,7 +11,10 @@ apipkg==1.5 \ attrs==22.1.0 \ --hash=sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6 \ --hash=sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c - # via pytest + # via + # outcome + # pytest + # trio beautifulsoup4==4.6.0 \ --hash=sha256:11a9a27b7d3bddc6d86f59fb76afb70e921a25ac2d6cc55b40d072bd68435a76 \ --hash=sha256:7015e76bf32f1f574636c4288399a6de66ce08fb7b2457f628a8d70c0fbabb11 \ @@ -23,7 +26,9 @@ blinker==1.4 \ certifi==2023.7.22 \ --hash=sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082 \ --hash=sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9 - # via requests + # via + # requests + # selenium charset-normalizer==2.0.3 \ --hash=sha256:88fce3fa5b1a84fdcb3f603d889f723d1dd89b26059d0123ca435570e848d5e1 \ --hash=sha256:c46c3ace2d744cfbdebceaa3c19ae691f53ae621b39fd7570f59d14fb7f2fd12 @@ -76,7 +81,10 @@ easyprocess==0.2.3 \ exceptiongroup==1.0.4 \ --hash=sha256:542adf9dea4055530d6e1279602fa5cb11dab2395fa650b8674eaec35fc4a828 \ --hash=sha256:bd14967b79cd9bdb54d97323216f8fdf533e278df937aa2a90089e7d6e06e5ec - # via pytest + # via + # pytest + # trio + # trio-websocket execnet==1.7.1 \ --hash=sha256:cacb9df31c9680ec5f95553976c4da484d407e85e41c83cb812aa014f0eddc50 \ --hash=sha256:d4efd397930c46415f62f8a31388d6be4f27a91d7550eb79bc64a756e0056547 @@ -85,13 +93,19 @@ flaky==3.6.0 \ --hash=sha256:36fa125bceebfe869739b62e203db4653488dff09615e5a4f3d7607d48363c6a \ --hash=sha256:c24e321b3b4b4a2d323b646acff6738e7601849832f4280864d69f00a6a9869d # via -r requirements/python3/test-requirements.in +h11==0.14.0 \ + --hash=sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d \ + --hash=sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761 + # via wsproto html5validator==0.4.0 \ --hash=sha256:3ce6e3e736c9c7b37e5e26eb173ba0777ae00776549bd608933fa14955260d49 # via -r requirements/python3/test-requirements.in idna==2.8 \ --hash=sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407 \ --hash=sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c - # via requests + # via + # requests + # trio iniconfig==1.0.1 \ --hash=sha256:80cf40c597eb564e86346103f609d74efce0f6b4d4f30ec8ce9e2c26411ba437 \ --hash=sha256:e5f92f89355a67de0595932a6c6c02ab4afddc6fcdc0bfc5becd0d60884d3f69 @@ -134,6 +148,10 @@ mypy==1.0.0 \ # via # -r requirements/python3/test-requirements.in # sqlalchemy-stubs +outcome==1.3.0.post0 \ + --hash=sha256:9dcf02e65f2971b80047b377468e72a268e15c0af3cf1238e6ff14f7f91143b8 \ + --hash=sha256:e771c5ce06d1415e356078d3bdd68523f284b4ce5419828922b6871e65eda82b + # via trio packaging==20.4 \ --hash=sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8 \ --hash=sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181 @@ -222,7 +240,9 @@ pyparsing==2.4.7 \ # via packaging pysocks==1.6.8 \ --hash=sha256:3fe52c55890a248676fd69dc9e3c4e811718b777834bcaab7a8125cf9deac672 - # via requests + # via + # requests + # urllib3 pytest-cov==2.5.1 \ --hash=sha256:03aa752cf11db41d281ea1d807d954c4eda35cfa1b21d6971966cc041bbf6e2d \ --hash=sha256:890fe5565400902b0c78b5357004aab1c814115894f4f21370e2433256a3eeec @@ -281,9 +301,9 @@ requests[socks]==2.31.0 \ --hash=sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f \ --hash=sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1 # via -r requirements/python3/test-requirements.in -selenium==3.141.0 \ - --hash=sha256:2d7131d7bc5a5b99a2d9b04aaf2612c411b03b8ca1b1ee8d3de5845a9be2cb3c \ - --hash=sha256:deaf32b60ad91a4611b98d8002757f29e6f2c2d5fcaf202e1c9ad06d6772300d +selenium==4.16.0 \ + --hash=sha256:aec71f4e6ed6cb3ec25c9c1b5ed56ae31b6da0a7f17474c7566d303f84e6219f \ + --hash=sha256:b2e987a445306151f7be0e6dfe2aa72a479c2ac6a91b9d5ef2d6dd4e49ad0435 # via # -r requirements/python3/test-requirements.in # tbselenium @@ -294,12 +314,20 @@ six==1.11.0 \ # mock # packaging # pathlib2 +sniffio==1.3.0 \ + --hash=sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101 \ + --hash=sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384 + # via trio +sortedcontainers==2.4.0 \ + --hash=sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88 \ + --hash=sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0 + # via trio sqlalchemy-stubs==0.4 \ --hash=sha256:5eec7aa110adf9b957b631799a72fef396b23ff99fe296df726645d01e312aa5 \ --hash=sha256:c665d6dd4482ef642f01027fa06c3d5e91befabb219dc71fc2a09e7d7695f7ae # via -r requirements/python3/test-requirements.in -tbselenium==0.5.2 \ - --hash=sha256:84dcc3f250b0c6bb4ce4bdb0d62de1d086343334019accb5469c21897afebb06 +tbselenium==0.8.1 \ + --hash=sha256:b40df4f339459d90e8c9e6fd66f7ebf9baabbc5b79d8ca7c94ebcb092f7e1726 # via -r requirements/python3/test-requirements.in toml==0.10.1 \ --hash=sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f \ @@ -311,6 +339,16 @@ tomli==2.0.1 \ # via # mypy # pytest +trio-websocket==0.11.1 \ + --hash=sha256:18c11793647703c158b1f6e62de638acada927344d534e3c7628eedcb746839f \ + --hash=sha256:520d046b0d030cf970b8b2b2e00c4c2245b3807853ecd44214acd33d74581638 + # via selenium +trio==0.23.2 \ + --hash=sha256:5a0b566fa5d50cf231cfd6b08f3b03aa4179ff004b8f3144059587039e2b26d3 \ + --hash=sha256:da1d35b9a2b17eb32cae2e763b16551f9aa6703634735024e32f325c9285069e + # via + # selenium + # trio-websocket types-mock==4.0.12 \ --hash=sha256:81f98e66bddde1b2a8d96c15c084d5d3ed96cf6137bca9ba8aabc6b6865b9ca7 \ --hash=sha256:f8b1dbe128dc2d5aa038876a8b497dfd1a9405210a5eb3dc4b4757c2e254627c @@ -341,13 +379,17 @@ typing-extensions==4.1.1 \ # via # mypy # sqlalchemy-stubs -urllib3==1.26.6 \ +urllib3[socks]==1.26.6 \ --hash=sha256:39fb8672126159acb139a7718dd10806104dec1e2f0f6c88aab05d17df10c8d4 \ --hash=sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f # via # -r requirements/python3/test-requirements.in # requests # selenium +wsproto==1.2.0 \ + --hash=sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065 \ + --hash=sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736 + # via trio-websocket # The following packages are considered to be unsafe in a requirements file: pip==21.1.1 \ diff --git a/securedrop/tests/functional/app_navigators/_nav_helper.py b/securedrop/tests/functional/app_navigators/_nav_helper.py index 9fbfd11d2d..28aeec2ba5 100644 --- a/securedrop/tests/functional/app_navigators/_nav_helper.py +++ b/securedrop/tests/functional/app_navigators/_nav_helper.py @@ -10,7 +10,6 @@ class NavigationHelper: - _TIMEOUT = 10 _POLL_FREQUENCY = 0.1 @@ -76,7 +75,7 @@ def safe_click_all_by_css_selector(self, selector: str) -> List[WebElement]: selenium.common.exceptions.TimeoutException: If the element cannot be found in time. """ - els = self.wait_for(lambda: self.driver.find_elements_by_css_selector(selector)) + els = self.wait_for(lambda: self.driver.find_elements(By.CSS_SELECTOR, selector)) for el in els: clickable_el = WebDriverWait(self.driver, self._TIMEOUT, self._POLL_FREQUENCY).until( expected_conditions.element_to_be_clickable((By.CSS_SELECTOR, selector)) diff --git a/securedrop/tests/functional/app_navigators/journalist_app_nav.py b/securedrop/tests/functional/app_navigators/journalist_app_nav.py index c5634e270b..ecd192e16c 100644 --- a/securedrop/tests/functional/app_navigators/journalist_app_nav.py +++ b/securedrop/tests/functional/app_navigators/journalist_app_nav.py @@ -37,7 +37,7 @@ def __init__( def is_on_journalist_homepage(self) -> WebElement: return self.nav_helper.wait_for( - lambda: self.driver.find_element_by_css_selector("div.journalist-view-all") + lambda: self.driver.find_element(By.CSS_SELECTOR, "div.journalist-view-all") ) def journalist_goes_to_login_page_and_enters_credentials( @@ -71,7 +71,7 @@ def journalist_logs_in( ) # Successful login should redirect to the index - self.nav_helper.wait_for(lambda: self.driver.find_element_by_id("link-logout")) + self.nav_helper.wait_for(lambda: self.driver.find_element(By.ID, "link-logout")) assert self.is_on_journalist_homepage() def journalist_checks_messages(self) -> None: @@ -83,7 +83,7 @@ def journalist_checks_messages(self) -> None: if not self.accept_languages: # There should be a "1 unread" span in the sole collection entry - unread_span = self.driver.find_element_by_css_selector("tr.unread") + unread_span = self.driver.find_element(By.CSS_SELECTOR, "tr.unread") assert "1 unread" in unread_span.text @staticmethod @@ -100,9 +100,9 @@ def journalist_downloads_first_message(self) -> str: # Select the first submission from the first source in the page self.journalist_selects_the_first_source() self.nav_helper.wait_for( - lambda: self.driver.find_element_by_css_selector("table#submissions") + lambda: self.driver.find_element(By.CSS_SELECTOR, "table#submissions") ) - submissions = self.driver.find_elements_by_css_selector("#submissions a") + submissions = self.driver.find_elements(By.CSS_SELECTOR, "#submissions a") assert len(submissions) == 1 file_url = submissions[0].get_attribute("href") @@ -119,6 +119,7 @@ def cookie_string_from_selenium_cookies( return result cks = cookie_string_from_selenium_cookies(self.driver.get_cookies()) + assert isinstance(file_url, str) raw_content = self._download_content_at_url(file_url, cks) decryption_result = utils.decrypt_as_journalist(raw_content) @@ -130,17 +131,17 @@ def cookie_string_from_selenium_cookies( return decrypted_message.decode() def journalist_selects_the_first_source(self) -> None: - self.driver.find_element_by_css_selector("#un-starred-source-link-1").click() + self.driver.find_element(By.CSS_SELECTOR, "#un-starred-source-link-1").click() def journalist_composes_reply_to_source(self, reply_content: str) -> None: - self.nav_helper.wait_for(lambda: self.driver.find_element_by_id("reply-text-field")) + self.nav_helper.wait_for(lambda: self.driver.find_element(By.ID, "reply-text-field")) self.nav_helper.safe_send_keys_by_id("reply-text-field", reply_content) def journalist_sends_reply_to_source( self, reply_content: str = "Thanks for the documents. Can you submit more? éè" ) -> None: self.journalist_composes_reply_to_source(reply_content=reply_content) - self.driver.find_element_by_id("reply-button").click() + self.driver.find_element(By.ID, "reply-button").click() def reply_stored() -> None: if not self.accept_languages: @@ -150,11 +151,11 @@ def reply_stored() -> None: def journalist_visits_col(self) -> None: self.nav_helper.wait_for( - lambda: self.driver.find_element_by_css_selector("table#collections") + lambda: self.driver.find_element(By.CSS_SELECTOR, "table#collections") ) self.nav_helper.safe_click_by_id("un-starred-source-link-1") self.nav_helper.wait_for( - lambda: self.driver.find_element_by_css_selector("table#submissions") + lambda: self.driver.find_element(By.CSS_SELECTOR, "table#submissions") ) def journalist_selects_first_doc(self) -> None: @@ -168,14 +169,14 @@ def journalist_selects_first_doc(self) -> None: ) ) - assert self.driver.find_element_by_css_selector( - 'input[type="checkbox"][name="doc_names_selected"]' + assert self.driver.find_element( + By.CSS_SELECTOR, 'input[type="checkbox"][name="doc_names_selected"]' ).is_selected() def journalist_clicks_delete_selected_link(self) -> None: self.nav_helper.safe_click_by_css_selector("a#delete-selected-link") self.nav_helper.wait_for( - lambda: self.driver.find_element_by_id("delete-selected-confirmation-modal") + lambda: self.driver.find_element(By.ID, "delete-selected-confirmation-modal") ) def journalist_clicks_delete_all_and_sees_confirmation(self) -> None: @@ -186,19 +187,19 @@ def journalist_confirms_delete_selected(self) -> None: self.nav_helper.wait_for( lambda: expected_conditions.element_to_be_clickable((By.ID, "delete-selected")) ) - confirm_btn = self.driver.find_element_by_id("delete-selected") + confirm_btn = self.driver.find_element(By.ID, "delete-selected") confirm_btn.location_once_scrolled_into_view ActionChains(self.driver).move_to_element(confirm_btn).click().perform() def get_submission_checkboxes_on_current_page(self): - return self.driver.find_elements_by_name("doc_names_selected") + return self.driver.find_elements(By.NAME, "doc_names_selected") def count_submissions_on_current_page(self) -> int: return len(self.get_submission_checkboxes_on_current_page()) def get_sources_on_index_page(self): assert self.is_on_journalist_homepage() - return self.driver.find_elements_by_class_name("code-name") + return self.driver.find_elements(By.CLASS_NAME, "code-name") def count_sources_on_index_page(self) -> int: return len(self.get_sources_on_index_page()) @@ -207,20 +208,20 @@ def journalist_confirm_delete_selected(self) -> None: self.nav_helper.wait_for( lambda: expected_conditions.element_to_be_clickable((By.ID, "delete-selected")) ) - confirm_btn = self.driver.find_element_by_id("delete-selected") + confirm_btn = self.driver.find_element(By.ID, "delete-selected") confirm_btn.location_once_scrolled_into_view ActionChains(self.driver).move_to_element(confirm_btn).click().perform() def journalist_sees_link_to_admin_page(self) -> bool: try: - self.driver.find_element_by_id("link-admin-index") + self.driver.find_element(By.ID, "link-admin-index") return True except NoSuchElementException: return False def admin_visits_admin_interface(self) -> None: self.nav_helper.safe_click_by_id("link-admin-index") - self.nav_helper.wait_for(lambda: self.driver.find_element_by_id("add-user")) + self.nav_helper.wait_for(lambda: self.driver.find_element(By.ID, "add-user")) def admin_creates_a_user( self, @@ -231,14 +232,14 @@ def admin_creates_a_user( callback_before_submitting_2fa_step: Optional[Callable[[], None]] = None, ) -> Optional[Tuple[str, str, str]]: self.nav_helper.safe_click_by_id("add-user") - self.nav_helper.wait_for(lambda: self.driver.find_element_by_id("username")) + self.nav_helper.wait_for(lambda: self.driver.find_element(By.ID, "username")) if not self.accept_languages: # The add user page has a form with an "ADD USER" button - btns = self.driver.find_elements_by_tag_name("button") + btns = self.driver.find_elements(By.TAG_NAME, "button") assert "ADD USER" in [el.text for el in btns] - password = self.driver.find_element_by_css_selector("#password").text.strip() + password = self.driver.find_element(By.CSS_SELECTOR, "#password").text.strip() if not username: final_username = f"journalist{str(randint(1000, 1000000))}" else: @@ -261,10 +262,10 @@ def admin_creates_a_user( self.nav_helper.safe_click_by_css_selector("button[type=submit]") # Submitting the add user form should redirect to the 2FA page - self.nav_helper.wait_for(lambda: self.driver.find_element_by_id("check-token")) + self.nav_helper.wait_for(lambda: self.driver.find_element(By.ID, "check-token")) if self.accept_languages in [None, "en-US"]: expected_title = "Enable YubiKey (OATH-HOTP)" if hotp_secret else "Enable FreeOTP" - h1s = [h1.text for h1 in self.driver.find_elements_by_tag_name("h1")] + h1s = [h1.text for h1 in self.driver.find_elements(By.TAG_NAME, "h1")] assert expected_title in h1s if hotp_secret: @@ -277,7 +278,7 @@ def admin_creates_a_user( else: # We created a totp user otp_secret = ( - self.driver.find_element_by_css_selector("#shared-secret") + self.driver.find_element(By.CSS_SELECTOR, "#shared-secret") .text.strip() .replace(" ", "") ) @@ -296,7 +297,7 @@ def user_token_added(): if not self.accept_languages: # Successfully verifying the code should redirect to the admin # interface, and flash a message indicating success - flash_msg = self.driver.find_elements_by_css_selector(".flash") + flash_msg = self.driver.find_elements(By.CSS_SELECTOR, ".flash") expected_msg = ( f'The two-factor code for user "{final_username}"' f" was verified successfully." @@ -311,7 +312,7 @@ def user_token_added(): def journalist_logs_out(self) -> None: # Click the logout link self.nav_helper.safe_click_by_id("link-logout") - self.nav_helper.wait_for(lambda: self.driver.find_element_by_css_selector(".login-form")) + self.nav_helper.wait_for(lambda: self.driver.find_element(By.CSS_SELECTOR, ".login-form")) # Logging out should redirect back to the login page def login_page(): @@ -323,7 +324,7 @@ def admin_visits_system_config_page(self): self.nav_helper.safe_click_by_id("update-instance-config") def config_page_loaded(): - assert self.driver.find_element_by_id("test-ossec-alert") + assert self.driver.find_element(By.ID, "test-ossec-alert") self.nav_helper.wait_for(config_page_loaded) @@ -333,13 +334,13 @@ def journalist_visits_edit_account(self): def admin_visits_user_edit_page(self, username_of_journalist_to_edit: str) -> None: # Go to the "edit user" page for the supplied journalist's username selector = f'a.edit-user[data-username="{username_of_journalist_to_edit}"]' - new_user_edit_links = self.driver.find_elements_by_css_selector(selector) + new_user_edit_links = self.driver.find_elements(By.CSS_SELECTOR, selector) assert len(new_user_edit_links) == 1 new_user_edit_links[0].click() # Ensure the admin is allowed to edit the journalist def can_edit_user(): - h = self.driver.find_elements_by_tag_name("h1")[0] + h = self.driver.find_elements(By.TAG_NAME, "h1")[0] assert f'Edit user "{username_of_journalist_to_edit}"' == h.text self.nav_helper.wait_for(can_edit_user) diff --git a/securedrop/tests/functional/app_navigators/source_app_nav.py b/securedrop/tests/functional/app_navigators/source_app_nav.py index b5123aaabb..55e11ab067 100644 --- a/securedrop/tests/functional/app_navigators/source_app_nav.py +++ b/securedrop/tests/functional/app_navigators/source_app_nav.py @@ -3,6 +3,7 @@ from contextlib import contextmanager from typing import Generator, Optional +from selenium.webdriver.common.by import By from selenium.webdriver.firefox.webdriver import WebDriver from selenium.webdriver.remote.webelement import WebElement from tests.functional.app_navigators._nav_helper import NavigationHelper @@ -46,14 +47,14 @@ def using_tor_browser_web_driver( ) def _is_on_source_homepage(self) -> WebElement: - return self.nav_helper.wait_for(lambda: self.driver.find_element_by_id("source-index")) + return self.nav_helper.wait_for(lambda: self.driver.find_element(By.ID, "source-index")) def source_visits_source_homepage(self) -> None: self.driver.get(self._source_app_base_url) assert self._is_on_source_homepage() def _is_on_generate_page(self) -> WebElement: - return self.nav_helper.wait_for(lambda: self.driver.find_element_by_id("source-generate")) + return self.nav_helper.wait_for(lambda: self.driver.find_element(By.ID, "source-generate")) def source_clicks_submit_documents_on_homepage(self) -> None: # It's the source's first time visiting this SecureDrop site, so they @@ -69,7 +70,7 @@ def source_continues_to_submit_page(self) -> None: def submit_page_loaded() -> None: if not self.accept_languages: - headline = self.driver.find_element_by_id("submit-heading") + headline = self.driver.find_element(By.ID, "submit-heading") # Message will either be "Submit Messages" or "Submit Files or Messages" depending # on whether file uploads are allowed by the instance's config assert "Submit" in headline.text @@ -78,7 +79,7 @@ def submit_page_loaded() -> None: self.nav_helper.wait_for(submit_page_loaded) def _is_on_logout_page(self) -> WebElement: - return self.nav_helper.wait_for(lambda: self.driver.find_element_by_id("source-logout")) + return self.nav_helper.wait_for(lambda: self.driver.find_element(By.ID, "source-logout")) def source_logs_out(self) -> None: self.nav_helper.safe_click_by_id("logout") @@ -87,21 +88,21 @@ def source_logs_out(self) -> None: def source_retrieves_codename_from_hint(self) -> str: # We use inputs to change CSS states for subsequent elements in the DOM, if it is unchecked # the codename is hidden - content = self.driver.find_element_by_id("codename-show-checkbox") + content = self.driver.find_element(By.ID, "codename-show-checkbox") assert content.get_attribute("checked") is None self.nav_helper.safe_click_by_id("codename-show") assert content.get_attribute("checked") is not None - content_content = self.driver.find_element_by_css_selector("#codename span") + content_content = self.driver.find_element(By.CSS_SELECTOR, "#codename span") return content_content.text def source_chooses_to_login(self) -> None: self.nav_helper.safe_click_by_css_selector("#return-visit a") - self.nav_helper.wait_for(lambda: self.driver.find_elements_by_id("source-login")) + self.nav_helper.wait_for(lambda: self.driver.find_elements(By.ID, "source-login")) def _is_logged_in(self) -> WebElement: - return self.nav_helper.wait_for(lambda: self.driver.find_element_by_id("logout")) + return self.nav_helper.wait_for(lambda: self.driver.find_element(By.ID, "logout")) def source_proceeds_to_login(self, codename: str) -> None: self.nav_helper.safe_send_keys_by_id("codename", codename) @@ -110,7 +111,7 @@ def source_proceeds_to_login(self, codename: str) -> None: # Check that we've logged in assert self._is_logged_in() - replies = self.driver.find_elements_by_id("replies") + replies = self.driver.find_elements(By.ID, "replies") assert len(replies) == 1 def source_submits_a_message(self, message: str = "S3cr3t m3ss4ge") -> str: @@ -123,7 +124,7 @@ def source_submits_a_message(self, message: str = "S3cr3t m3ss4ge") -> str: # Wait for confirmation that the message was submitted def message_submitted(): if not self.accept_languages: - notification = self.driver.find_element_by_css_selector(".success") + notification = self.driver.find_element(By.CSS_SELECTOR, ".success") assert "Thank" in notification.text return notification.text @@ -143,7 +144,7 @@ def source_submits_a_file(self, file_content: str = "S3cr3t f1l3") -> None: def file_submitted() -> None: if not self.accept_languages: - notification = self.driver.find_element_by_css_selector(".success") + notification = self.driver.find_element(By.CSS_SELECTOR, ".success") expected_notification = "Thank you for sending this information to us" assert expected_notification in notification.text diff --git a/securedrop/tests/functional/conftest.py b/securedrop/tests/functional/conftest.py index fbde7fb1f9..2feead5a08 100644 --- a/securedrop/tests/functional/conftest.py +++ b/securedrop/tests/functional/conftest.py @@ -22,14 +22,14 @@ # Function-scoped so that tests can be run in parallel if needed @pytest.fixture() -def firefox_web_driver() -> WebDriver: +def firefox_web_driver() -> WebDriver: # type: ignore with get_web_driver(web_driver_type=WebDriverTypeEnum.FIREFOX) as web_driver: yield web_driver # Function-scoped so that tests can be run in parallel if needed @pytest.fixture() -def tor_browser_web_driver() -> WebDriver: +def tor_browser_web_driver() -> WebDriver: # type: ignore with get_web_driver(web_driver_type=WebDriverTypeEnum.TOR_BROWSER) as web_driver: yield web_driver diff --git a/securedrop/tests/functional/pageslayout/test_journalist_account.py b/securedrop/tests/functional/pageslayout/test_journalist_account.py index f332c81667..faab09d7ee 100644 --- a/securedrop/tests/functional/pageslayout/test_journalist_account.py +++ b/securedrop/tests/functional/pageslayout/test_journalist_account.py @@ -19,6 +19,7 @@ import pytest from selenium.webdriver import ActionChains +from selenium.webdriver.common.by import By from tests.functional.app_navigators.journalist_app_nav import JournalistAppNavigator from tests.functional.pageslayout.utils import list_locales, save_static_data @@ -64,11 +65,11 @@ def _clicks_reset_secret( journ_app_nav: JournalistAppNavigator, otp_type: str, assert_tooltip_text_is: Optional[str] ) -> None: reset_form = journ_app_nav.nav_helper.wait_for( - lambda: journ_app_nav.driver.find_element_by_id(f"reset-two-factor-{otp_type}") + lambda: journ_app_nav.driver.find_element(By.ID, f"reset-two-factor-{otp_type}") ) assert f"/account/reset-2fa-{otp_type}" in reset_form.get_attribute("action") - reset_button = journ_app_nav.driver.find_elements_by_css_selector( - f"#button-reset-two-factor-{otp_type}" + reset_button = journ_app_nav.driver.find_elements( + By.CSS_SELECTOR, f"#button-reset-two-factor-{otp_type}" )[0] # 2FA reset buttons show a tooltip with explanatory text on hover. @@ -77,8 +78,8 @@ def _clicks_reset_secret( ActionChains(journ_app_nav.driver).move_to_element(reset_button).perform() def explanatory_tooltip_is_correct() -> None: - explanatory_tooltip = journ_app_nav.driver.find_element_by_css_selector( - f"#button-reset-two-factor-{otp_type} span" + explanatory_tooltip = journ_app_nav.driver.find_element( + By.CSS_SELECTOR, f"#button-reset-two-factor-{otp_type} span" ) explanatory_tooltip_opacity = explanatory_tooltip.value_of_css_property("opacity") @@ -91,7 +92,7 @@ def explanatory_tooltip_is_correct() -> None: reset_form.submit() - alert = journ_app_nav.driver.switch_to_alert() + alert = journ_app_nav.driver.switch_to.alert alert.accept() def test_account_new_two_factor_totp( diff --git a/securedrop/tests/functional/pageslayout/test_journalist_admin.py b/securedrop/tests/functional/pageslayout/test_journalist_admin.py index a0b09b7569..90a6f85635 100644 --- a/securedrop/tests/functional/pageslayout/test_journalist_admin.py +++ b/securedrop/tests/functional/pageslayout/test_journalist_admin.py @@ -23,6 +23,7 @@ import pytest from selenium.common.exceptions import TimeoutException from selenium.webdriver import ActionChains +from selenium.webdriver.common.by import By from tests.functional.app_navigators.journalist_app_nav import JournalistAppNavigator from tests.functional.pageslayout.utils import list_locales, save_static_data @@ -83,17 +84,19 @@ def screenshot_of_journalist_new_user_two_factor_hotp() -> None: def _admin_visits_reset_2fa_hotp_step() -> None: # 2FA reset buttons show a tooltip with explanatory text on hover. # Also, confirm the text on the tooltip is the correct one. - hotp_reset_button = journ_app_nav.driver.find_elements_by_id("reset-two-factor-hotp")[0] + hotp_reset_button = journ_app_nav.driver.find_elements(By.ID, "reset-two-factor-hotp")[ + 0 + ] hotp_reset_button.location_once_scrolled_into_view ActionChains(journ_app_nav.driver).move_to_element(hotp_reset_button).perform() time.sleep(1) - tip_opacity = journ_app_nav.driver.find_elements_by_css_selector( - "#button-reset-two-factor-hotp span.tooltip" + tip_opacity = journ_app_nav.driver.find_elements( + By.CSS_SELECTOR, "#button-reset-two-factor-hotp span.tooltip" )[0].value_of_css_property("opacity") - tip_text = journ_app_nav.driver.find_elements_by_css_selector( - "#button-reset-two-factor-hotp span.tooltip" + tip_text = journ_app_nav.driver.find_elements( + By.CSS_SELECTOR, "#button-reset-two-factor-hotp span.tooltip" )[0].text assert tip_opacity == "1" @@ -111,7 +114,7 @@ def _admin_visits_reset_2fa_hotp_step() -> None: # Wait for it to succeed journ_app_nav.nav_helper.wait_for( - lambda: journ_app_nav.driver.find_element_by_css_selector('input[name="otp_secret"]') + lambda: journ_app_nav.driver.find_element(By.CSS_SELECTOR, 'input[name="otp_secret"]') ) def test_admin_adds_user_totp_and_edits_totp( @@ -152,23 +155,25 @@ def screenshot_of_journalist_new_user_two_factor_totp() -> None: journ_app_nav.admin_visits_user_edit_page(username_of_journalist_to_edit=new_user_username) def _admin_visits_reset_2fa_totp_step() -> None: - totp_reset_button = journ_app_nav.driver.find_elements_by_id("reset-two-factor-totp")[0] + totp_reset_button = journ_app_nav.driver.find_elements(By.ID, "reset-two-factor-totp")[ + 0 + ] assert "/admin/reset-2fa-totp" in totp_reset_button.get_attribute("action") # 2FA reset buttons show a tooltip with explanatory text on hover. # Also, confirm the text on the tooltip is the correct one. - totp_reset_button = journ_app_nav.driver.find_elements_by_css_selector( - "#button-reset-two-factor-totp" + totp_reset_button = journ_app_nav.driver.find_elements( + By.CSS_SELECTOR, "#button-reset-two-factor-totp" )[0] totp_reset_button.location_once_scrolled_into_view ActionChains(journ_app_nav.driver).move_to_element(totp_reset_button).perform() time.sleep(1) - tip_opacity = journ_app_nav.driver.find_elements_by_css_selector( - "#button-reset-two-factor-totp span.tooltip" + tip_opacity = journ_app_nav.driver.find_elements( + By.CSS_SELECTOR, "#button-reset-two-factor-totp span.tooltip" )[0].value_of_css_property("opacity") - tip_text = journ_app_nav.driver.find_elements_by_css_selector( - "#button-reset-two-factor-totp span.tooltip" + tip_text = journ_app_nav.driver.find_elements( + By.CSS_SELECTOR, "#button-reset-two-factor-totp span.tooltip" )[0].text assert tip_opacity == "1" @@ -197,7 +202,7 @@ def _retry_2fa_pop_ups( try: # This is the button we click to trigger the alert. journ_app_nav.nav_helper.wait_for( - lambda: journ_app_nav.driver.find_elements_by_id(button_to_click)[0] + lambda: journ_app_nav.driver.find_elements(By.ID, button_to_click)[0] ) except IndexError: # If the button isn't there, then the alert is up from the last @@ -251,7 +256,7 @@ def test_admin_changes_logo(self, locale, sd_servers_with_clean_state, firefox_w # Then it succeeds def updated_image() -> None: - flash_msg = journ_app_nav.driver.find_element_by_css_selector(".flash") + flash_msg = journ_app_nav.driver.find_element(By.CSS_SELECTOR, ".flash") assert "Image updated." in flash_msg.text journ_app_nav.nav_helper.wait_for(updated_image, timeout=20) @@ -279,12 +284,12 @@ def test_ossec_alert_button(self, locale, sd_servers, firefox_web_driver): journ_app_nav.admin_visits_system_config_page() # When they try to send an OSSEC alert - alert_button = journ_app_nav.driver.find_element_by_id("test-ossec-alert") + alert_button = journ_app_nav.driver.find_element(By.ID, "test-ossec-alert") alert_button.click() # Then it succeeds def test_alert_sent(): - flash_msg = journ_app_nav.driver.find_element_by_css_selector(".flash") + flash_msg = journ_app_nav.driver.find_element(By.CSS_SELECTOR, ".flash") assert "Test alert sent. Please check your email." in flash_msg.text journ_app_nav.nav_helper.wait_for(test_alert_sent) diff --git a/securedrop/tests/functional/pageslayout/test_source_session_layout.py b/securedrop/tests/functional/pageslayout/test_source_session_layout.py index c2f777b2f4..304abc399f 100644 --- a/securedrop/tests/functional/pageslayout/test_source_session_layout.py +++ b/securedrop/tests/functional/pageslayout/test_source_session_layout.py @@ -3,6 +3,7 @@ from typing import Generator, Tuple import pytest +from selenium.webdriver.common.by import By from tests.factories import SecureDropConfigFactory from tests.functional.app_navigators.source_app_nav import SourceAppNavigator from tests.functional.conftest import SdServersFixtureResult, spawn_sd_servers @@ -45,7 +46,6 @@ def test_source_session_timeout(self, locale, sd_servers_with_short_timeout): source_app_base_url=sd_servers_with_short_timeout.source_app_base_url, accept_languages=locale_with_commas, ) as navigator: - # And they're logged in and are using the app navigator.source_visits_source_homepage() navigator.source_clicks_submit_documents_on_homepage() @@ -58,7 +58,7 @@ def test_source_session_timeout(self, locale, sd_servers_with_short_timeout): navigator.driver.refresh() # Then the source user sees the "session expired" message - notification = navigator.driver.find_element_by_class_name("error") + notification = navigator.driver.find_element(By.CLASS_NAME, "error") assert notification.text if locale == "en_US": expected_text = "You were logged out due to inactivity." diff --git a/securedrop/tests/functional/pageslayout/test_source_static_pages.py b/securedrop/tests/functional/pageslayout/test_source_static_pages.py index 8974e76785..484af40ac2 100644 --- a/securedrop/tests/functional/pageslayout/test_source_static_pages.py +++ b/securedrop/tests/functional/pageslayout/test_source_static_pages.py @@ -1,5 +1,6 @@ import pytest import requests +from selenium.webdriver.common.by import By from tests.functional import tor_utils from tests.functional.pageslayout.utils import list_locales, save_static_data from tests.functional.web_drivers import WebDriverTypeEnum, get_web_driver @@ -16,12 +17,11 @@ def test_notfound(self, locale, sd_servers): web_driver_type=WebDriverTypeEnum.TOR_BROWSER, accept_languages=locale_with_commas, ) as tor_browser_web_driver: - # When they try to access a page that does not exist tor_browser_web_driver.get(f"{sd_servers.source_app_base_url}/does_not_exist") # Then the right error is displayed - message = tor_browser_web_driver.find_element_by_id("page-not-found") + message = tor_browser_web_driver.find_element(By.ID, "page-not-found") assert message.is_displayed() save_static_data(tor_browser_web_driver, locale, "source-notfound") diff --git a/securedrop/tests/functional/pageslayout/test_submit_and_retrieve_file.py b/securedrop/tests/functional/pageslayout/test_submit_and_retrieve_file.py index c2d98ffc4e..366af4b727 100644 --- a/securedrop/tests/functional/pageslayout/test_submit_and_retrieve_file.py +++ b/securedrop/tests/functional/pageslayout/test_submit_and_retrieve_file.py @@ -1,5 +1,6 @@ import pytest from selenium.common.exceptions import NoSuchElementException +from selenium.webdriver.common.by import By from tests.functional.app_navigators.journalist_app_nav import JournalistAppNavigator from tests.functional.app_navigators.source_app_nav import SourceAppNavigator from tests.functional.pageslayout.utils import list_locales, save_static_data @@ -66,14 +67,14 @@ def test_submit_and_retrieve_happy_path( @staticmethod def _source_deletes_journalist_reply(navigator: SourceAppNavigator) -> None: # Get the reply filename so we can use IDs to select the delete buttons - reply_filename_element = navigator.driver.find_element_by_name("reply_filename") + reply_filename_element = navigator.driver.find_element(By.NAME, "reply_filename") reply_filename = reply_filename_element.get_attribute("value") confirm_dialog_id = f"confirm-delete-{reply_filename}" navigator.nav_helper.safe_click_by_css_selector(f"a[href='#{confirm_dialog_id}']") def confirm_displayed(): - confirm_dialog = navigator.driver.find_element_by_id(confirm_dialog_id) + confirm_dialog = navigator.driver.find_element(By.ID, confirm_dialog_id) confirm_dialog.location_once_scrolled_into_view assert confirm_dialog.is_displayed() @@ -86,7 +87,7 @@ def confirm_displayed(): def reply_deleted(): if not navigator.accept_languages: - notification = navigator.driver.find_element_by_class_name("success") + notification = navigator.driver.find_element(By.CLASS_NAME, "success") assert "Reply deleted" in notification.text navigator.nav_helper.wait_for(reply_deleted) @@ -95,22 +96,22 @@ def reply_deleted(): def _journalist_stars_and_unstars_single_message(journ_app_nav: JournalistAppNavigator) -> None: # Message begins unstarred with pytest.raises(NoSuchElementException): - journ_app_nav.driver.find_element_by_id("starred-source-link-1") + journ_app_nav.driver.find_element(By.ID, "starred-source-link-1") # Journalist stars the message - journ_app_nav.driver.find_element_by_class_name("button-star").click() + journ_app_nav.driver.find_element(By.CLASS_NAME, "button-star").click() def message_starred(): - starred = journ_app_nav.driver.find_elements_by_id("starred-source-link-1") + starred = journ_app_nav.driver.find_elements(By.ID, "starred-source-link-1") assert len(starred) == 1 journ_app_nav.nav_helper.wait_for(message_starred) # Journalist unstars the message - journ_app_nav.driver.find_element_by_class_name("button-star").click() + journ_app_nav.driver.find_element(By.CLASS_NAME, "button-star").click() def message_unstarred(): with pytest.raises(NoSuchElementException): - journ_app_nav.driver.find_element_by_id("starred-source-link-1") + journ_app_nav.driver.find_element(By.ID, "starred-source-link-1") journ_app_nav.nav_helper.wait_for(message_unstarred) diff --git a/securedrop/tests/functional/test_admin_interface.py b/securedrop/tests/functional/test_admin_interface.py index 1cc0053c71..82c5152475 100644 --- a/securedrop/tests/functional/test_admin_interface.py +++ b/securedrop/tests/functional/test_admin_interface.py @@ -50,7 +50,7 @@ def test_admin_adds_non_admin_user(self, sd_servers_with_clean_state, firefox_we # Log the admin user out journ_app_nav.journalist_logs_out() journ_app_nav.nav_helper.wait_for( - lambda: journ_app_nav.driver.find_element_by_css_selector(".login-form") + lambda: journ_app_nav.driver.find_element(By.CSS_SELECTOR, ".login-form") ) # And when the new user tries to login @@ -88,7 +88,7 @@ def test_admin_adds_admin_user(self, sd_servers_with_clean_state, firefox_web_dr # Log the admin user out journ_app_nav.journalist_logs_out() journ_app_nav.nav_helper.wait_for( - lambda: journ_app_nav.driver.find_element_by_css_selector(".login-form") + lambda: journ_app_nav.driver.find_element(By.CSS_SELECTOR, ".login-form") ) # And when the new user tries to login @@ -138,7 +138,7 @@ def submit_form_and_stop(): # Then it fails with an error error_msg = journ_app_nav.nav_helper.wait_for( - lambda: journ_app_nav.driver.find_element_by_css_selector(".form-validation-error") + lambda: journ_app_nav.driver.find_element(By.CSS_SELECTOR, ".form-validation-error") ) # And they see the corresponding error message @@ -237,7 +237,7 @@ def test_admin_edits_username(self, sd_servers_with_second_journalist, firefox_w # Then it succeeds def user_edited(): - flash_msg = journ_app_nav.driver.find_element_by_css_selector(".flash") + flash_msg = journ_app_nav.driver.find_element(By.CSS_SELECTOR, ".flash") assert "Account updated." in flash_msg.text journ_app_nav.nav_helper.wait_for(user_edited) @@ -258,7 +258,7 @@ def test_admin_edits_invalid_username( # Then it fails def user_edited(): - flash_msg = journ_app_nav.driver.find_element_by_css_selector(".flash") + flash_msg = journ_app_nav.driver.find_element(By.CSS_SELECTOR, ".flash") assert "Invalid username" in flash_msg.text journ_app_nav.nav_helper.wait_for(user_edited) @@ -289,9 +289,9 @@ def test_admin_resets_password(self, sd_servers_with_second_journalist, firefox_ ) # When they reset the second journalist's password - new_password = journ_app_nav.driver.find_element_by_css_selector("#password").text.strip() + new_password = journ_app_nav.driver.find_element(By.CSS_SELECTOR, "#password").text.strip() assert new_password - reset_pw_btn = journ_app_nav.driver.find_element_by_css_selector("#reset-password") + reset_pw_btn = journ_app_nav.driver.find_element(By.CSS_SELECTOR, "#reset-password") reset_pw_btn.click() # Then it succeeds @@ -343,7 +343,7 @@ def test_admin_deletes_user(self, sd_servers_with_second_journalist, firefox_web # Then it succeeds def user_deleted(): - flash_msg = journ_app_nav.driver.find_element_by_css_selector(".flash") + flash_msg = journ_app_nav.driver.find_element(By.CSS_SELECTOR, ".flash") assert "Deleted user" in flash_msg.text journ_app_nav.nav_helper.wait_for(user_deleted) @@ -392,7 +392,7 @@ def test_disallow_file_submission( # Then they don't see the option to upload a file because uploads were disallowed with pytest.raises(NoSuchElementException): - source_app_nav.driver.find_element_by_class_name("attachment") + source_app_nav.driver.find_element(By.CLASS_NAME, "attachment") @classmethod def _admin_updates_document_upload_instance_setting( @@ -402,8 +402,8 @@ def _admin_updates_document_upload_instance_setting( ) -> None: # Retrieve the instance's current upload setting upload_element_id = "prevent_document_uploads" - instance_currently_allows_file_uploads = not journ_app_nav.driver.find_element_by_id( - upload_element_id + instance_currently_allows_file_uploads = not journ_app_nav.driver.find_element( + By.ID, upload_element_id ).is_selected() # Ensure the new setting is different from the existing setting @@ -417,7 +417,7 @@ def _admin_updates_document_upload_instance_setting( @staticmethod def _admin_submits_instance_settings_form(journ_app_nav: JournalistAppNavigator) -> None: def preferences_saved(): - flash_msg = journ_app_nav.driver.find_element_by_css_selector(".flash") + flash_msg = journ_app_nav.driver.find_element(By.CSS_SELECTOR, ".flash") assert "Preferences saved." in flash_msg.text journ_app_nav.nav_helper.wait_for(preferences_saved, timeout=20) @@ -464,7 +464,7 @@ def test_allow_file_submission( source_app_nav.source_continues_to_submit_page() # Then they see the option to upload a file because uploads were allowed - assert source_app_nav.driver.find_element_by_class_name("attachment") + assert source_app_nav.driver.find_element(By.CLASS_NAME, "attachment") def test_orgname_is_changed( self, sd_servers_with_clean_state, firefox_web_driver, tor_browser_web_driver @@ -488,7 +488,7 @@ def test_orgname_is_changed( # When they update the organization's name assert "SecureDrop" in journ_app_nav.driver.title - journ_app_nav.driver.find_element_by_id("organization_name").clear() + journ_app_nav.driver.find_element(By.ID, "organization_name").clear() new_org_name = "Walden Inquirer" journ_app_nav.nav_helper.safe_send_keys_by_id("organization_name", new_org_name) journ_app_nav.nav_helper.safe_click_by_id("submit-update-org-name") diff --git a/securedrop/tests/functional/test_journalist.py b/securedrop/tests/functional/test_journalist.py index 6f5ea2a4ac..35fa9b4d84 100644 --- a/securedrop/tests/functional/test_journalist.py +++ b/securedrop/tests/functional/test_journalist.py @@ -22,6 +22,7 @@ import pytest from sdconfig import SecureDropConfig from selenium.common.exceptions import NoSuchElementException +from selenium.webdriver.common.by import By from selenium.webdriver.common.keys import Keys from tests.factories import SecureDropConfigFactory from tests.functional.app_navigators.journalist_app_nav import JournalistAppNavigator @@ -122,7 +123,7 @@ def test_journalist_uses_col_delete_collection_button_modal( def _journalist_clicks_delete_collection_link(journ_app_nav: JournalistAppNavigator) -> None: journ_app_nav.nav_helper.safe_click_by_id("delete-collection-link") journ_app_nav.nav_helper.wait_for( - lambda: journ_app_nav.driver.find_element_by_id("delete-collection-confirmation-modal") + lambda: journ_app_nav.driver.find_element(By.ID, "delete-collection-confirmation-modal") ) def test_journalist_uses_index_delete_collections_button_modal( @@ -147,7 +148,7 @@ def test_journalist_uses_index_delete_collections_button_modal( # And the journalist selected all sources on the index page try: # If JavaScript is enabled, use the select_all button. - journ_app_nav.driver.find_element_by_id("select_all") + journ_app_nav.driver.find_element(By.ID, "select_all") journ_app_nav.nav_helper.safe_click_by_id("select_all") except NoSuchElementException: journ_app_nav.nav_helper.safe_click_all_by_css_selector( @@ -179,7 +180,7 @@ def test_journalist_uses_index_delete_collections_button_modal( # Then a message shows up to say that the collection was deleted def collection_deleted(): - flash_msg = journ_app_nav.driver.find_element_by_css_selector(".flash") + flash_msg = journ_app_nav.driver.find_element(By.CSS_SELECTOR, ".flash") assert "The account and all data for the source have been deleted." in flash_msg.text journ_app_nav.nav_helper.wait_for(collection_deleted) @@ -194,7 +195,7 @@ def no_sources(): def _journalist_clicks_delete_collections_link(journ_app_nav: JournalistAppNavigator) -> None: journ_app_nav.nav_helper.safe_click_by_id("delete-collections-link") journ_app_nav.nav_helper.wait_for( - lambda: journ_app_nav.driver.find_element_by_id("delete-sources-modal") + lambda: journ_app_nav.driver.find_element(By.ID, "delete-sources-modal") ) @staticmethod @@ -209,7 +210,7 @@ def _journalist_clicks_delete_collections_on_first_modal( ) -> None: journ_app_nav.nav_helper.safe_click_by_id("delete-collections") journ_app_nav.nav_helper.wait_for( - lambda: journ_app_nav.driver.find_element_by_id("delete-collections-confirm") + lambda: journ_app_nav.driver.find_element(By.ID, "delete-collections-confirm") ) def test_journalist_interface_ui_with_modal( @@ -259,14 +260,14 @@ def test_journalist_interface_ui_with_modal( assert source.text == first_source_designation or source.is_displayed() is False # And when clicking "select all" - select_all = journ_app_nav.driver.find_element_by_id("select_all") + select_all = journ_app_nav.driver.find_element(By.ID, "select_all") select_all.click() # Then only the visible source gets selected - source_rows = journ_app_nav.driver.find_elements_by_css_selector("#cols li.source") + source_rows = journ_app_nav.driver.find_elements(By.CSS_SELECTOR, "#cols li.source") for source_row in source_rows: source_designation = source_row.get_attribute("data-source-designation") - checkbox = source_row.find_element_by_css_selector("input[type=checkbox]") + checkbox = source_row.find_element(By.CSS_SELECTOR, "input[type=checkbox]") if source_designation == first_source_designation: assert checkbox.is_selected() else: @@ -277,18 +278,18 @@ def test_journalist_interface_ui_with_modal( filter_box.send_keys(Keys.RETURN) select_all.click() for source_row in source_rows: - checkbox = source_row.find_element_by_css_selector("input[type=checkbox]") + checkbox = source_row.find_element(By.CSS_SELECTOR, "input[type=checkbox]") assert checkbox.is_selected() # And then they filter again and click "select none" filter_box.send_keys(first_source_designation) - select_none = journ_app_nav.driver.find_element_by_id("select_none") + select_none = journ_app_nav.driver.find_element(By.ID, "select_none") select_none.click() # Then only the visible source gets de-selected for source_row in source_rows: source_designation = source_row.get_attribute("data-source-designation") - checkbox = source_row.find_element_by_css_selector("input[type=checkbox]") + checkbox = source_row.find_element(By.CSS_SELECTOR, "input[type=checkbox]") if source_designation == first_source_designation: assert not checkbox.is_selected() else: @@ -301,24 +302,24 @@ def test_journalist_interface_ui_with_modal( for source_row in source_rows: assert source_row.is_displayed() - checkbox = source_row.find_element_by_css_selector("input[type=checkbox]") + checkbox = source_row.find_element(By.CSS_SELECTOR, "input[type=checkbox]") assert not checkbox.is_selected() # And the journalist clicks "select all" then all sources are selected - journ_app_nav.driver.find_element_by_id("select_all").click() - checkboxes = journ_app_nav.driver.find_elements_by_id("checkbox") + journ_app_nav.driver.find_element(By.ID, "select_all").click() + checkboxes = journ_app_nav.driver.find_elements(By.ID, "checkbox") for checkbox in checkboxes: assert checkbox.is_selected() # And when the journalist clicks "select none" then no sources are selected - journ_app_nav.driver.find_element_by_id("select_none").click() - checkboxes = journ_app_nav.driver.find_elements_by_id("checkbox") + journ_app_nav.driver.find_element(By.ID, "select_none").click() + checkboxes = journ_app_nav.driver.find_elements(By.ID, "checkbox") for checkbox in checkboxes: assert checkbox.is_selected() is False # And when the journalist clicks "select unread" then all unread sources are selected journ_app_nav.journalist_selects_the_first_source() - journ_app_nav.driver.find_element_by_id("select_unread").click() + journ_app_nav.driver.find_element(By.ID, "select_unread").click() checkboxes = journ_app_nav.get_submission_checkboxes_on_current_page() for checkbox in checkboxes: classes = checkbox.get_attribute("class") @@ -349,7 +350,7 @@ def test_journalist_uses_index_delete_files_button_modal( # And the journalist selected all sources on the index page try: # If JavaScript is enabled, use the select_all button. - journ_app_nav.driver.find_element_by_id("select_all") + journ_app_nav.driver.find_element(By.ID, "select_all") journ_app_nav.nav_helper.safe_click_by_id("select_all") except NoSuchElementException: journ_app_nav.nav_helper.safe_click_all_by_css_selector( @@ -365,9 +366,9 @@ def test_journalist_uses_index_delete_files_button_modal( # and messages zeroed, and a success flash message present def one_source_no_files(): assert journ_app_nav.count_sources_on_index_page() == 1 - flash_msg = journ_app_nav.driver.find_element_by_css_selector(".flash") + flash_msg = journ_app_nav.driver.find_element(By.CSS_SELECTOR, ".flash") assert "The files and messages have been deleted" in flash_msg.text - counts = journ_app_nav.driver.find_elements_by_css_selector(".submission-count") + counts = journ_app_nav.driver.find_elements(By.CSS_SELECTOR, ".submission-count") assert "0 docs" in counts[0].text assert "0 messages" in counts[1].text @@ -420,8 +421,8 @@ def test_download_source_unread(self, sd_servers_with_missing_file, firefox_web_ ) # When the journalist clicks on the source's "n unread" button - journ_app_nav.driver.find_element_by_css_selector( - "table#collections tr.source > td.unread a" + journ_app_nav.driver.find_element( + By.CSS_SELECTOR, "table#collections tr.source > td.unread a" ).click() # Then they see the expected error message @@ -430,7 +431,7 @@ def test_download_source_unread(self, sd_servers_with_missing_file, firefox_web_ @staticmethod def _journalist_sees_missing_file_error_message(journ_app_nav: JournalistAppNavigator) -> None: - notification = journ_app_nav.driver.find_element_by_css_selector(".error") + notification = journ_app_nav.driver.find_element(By.CSS_SELECTOR, ".error") # We use a definite article ("the" instead of "a") if a single file # is downloaded directly. @@ -455,10 +456,10 @@ def test_select_source_and_download_all(self, sd_servers_with_missing_file, fire ) # When the journalist selects the source and then clicks the "Download" button - checkboxes = journ_app_nav.driver.find_elements_by_name("cols_selected") + checkboxes = journ_app_nav.driver.find_elements(By.NAME, "cols_selected") assert len(checkboxes) == 1 checkboxes[0].click() - journ_app_nav.driver.find_element_by_xpath("//button[@value='download-all']").click() + journ_app_nav.driver.find_element(By.XPATH, "//button[@value='download-all']").click() # Then they see the expected error message self._journalist_sees_missing_file_error_message(journ_app_nav) @@ -480,10 +481,10 @@ def test_select_source_and_download_unread( ) # When the journalist selects the source then clicks the "Download Unread" button - checkboxes = journ_app_nav.driver.find_elements_by_name("cols_selected") + checkboxes = journ_app_nav.driver.find_elements(By.NAME, "cols_selected") assert len(checkboxes) == 1 checkboxes[0].click() - journ_app_nav.driver.find_element_by_xpath("//button[@value='download-unread']").click() + journ_app_nav.driver.find_element(By.XPATH, "//button[@value='download-unread']").click() # Then they see the expected error message self._journalist_sees_missing_file_error_message(journ_app_nav) @@ -507,9 +508,9 @@ def test_download_message(self, sd_servers_with_missing_file, firefox_web_driver journ_app_nav.journalist_selects_the_first_source() journ_app_nav.nav_helper.wait_for( - lambda: journ_app_nav.driver.find_element_by_css_selector("table#submissions") + lambda: journ_app_nav.driver.find_element(By.CSS_SELECTOR, "table#submissions") ) - submissions = journ_app_nav.driver.find_elements_by_css_selector("#submissions a") + submissions = journ_app_nav.driver.find_elements(By.CSS_SELECTOR, "#submissions a") assert len(submissions) == 1 file_link = submissions[0] @@ -522,7 +523,7 @@ def test_download_message(self, sd_servers_with_missing_file, firefox_web_driver @staticmethod def _journalist_is_on_collection_page(journ_app_nav: JournalistAppNavigator) -> None: return journ_app_nav.nav_helper.wait_for( - lambda: journ_app_nav.driver.find_element_by_css_selector("div.journalist-view-single") + lambda: journ_app_nav.driver.find_element(By.CSS_SELECTOR, "div.journalist-view-single") ) def test_select_message_and_download_selected( @@ -542,10 +543,10 @@ def test_select_message_and_download_selected( # When the journalist selects the individual message from the source page # and clicks "Download Selected" journ_app_nav.journalist_selects_the_first_source() - checkboxes = journ_app_nav.driver.find_elements_by_name("doc_names_selected") + checkboxes = journ_app_nav.driver.find_elements(By.NAME, "doc_names_selected") assert len(checkboxes) == 1 checkboxes[0].click() - journ_app_nav.driver.find_element_by_xpath("//button[@value='download']").click() + journ_app_nav.driver.find_element(By.XPATH, "//button[@value='download']").click() # Then they see the expected error message self._journalist_sees_missing_file_error_message(journ_app_nav) diff --git a/securedrop/tests/functional/test_source.py b/securedrop/tests/functional/test_source.py index f88f8f322b..b77ec69c8a 100644 --- a/securedrop/tests/functional/test_source.py +++ b/securedrop/tests/functional/test_source.py @@ -1,5 +1,6 @@ import requests import werkzeug +from selenium.webdriver.common.by import By from tests.functional import tor_utils from tests.functional.app_navigators.source_app_nav import SourceAppNavigator @@ -9,7 +10,6 @@ class TestSourceAppCodenameHints: - FIRST_SUBMISSION_TEXT = "Please check back later for replies" def test_no_codename_hint_on_second_login(self, sd_servers, tor_browser_web_driver): @@ -29,7 +29,7 @@ def test_no_codename_hint_on_second_login(self, sd_servers, tor_browser_web_driv assert source_codename # And they are able to close the codename hint UI - content = navigator.driver.find_element_by_id("codename-show-checkbox") + content = navigator.driver.find_element(By.ID, "codename-show-checkbox") assert content.get_attribute("checked") is not None navigator.nav_helper.safe_click_by_id("codename-show") assert content.get_attribute("checked") is None @@ -41,7 +41,7 @@ def test_no_codename_hint_on_second_login(self, sd_servers, tor_browser_web_driv navigator.source_proceeds_to_login(codename=source_codename) # The codename hint UI is no longer present - codename = navigator.driver.find_elements_by_css_selector("#codename-reminder") + codename = navigator.driver.find_elements(By.CSS_SELECTOR, "#codename-reminder") assert len(codename) == 0 def test_submission_notifications_on_first_login(self, sd_servers, tor_browser_web_driver): @@ -115,17 +115,17 @@ class TestSourceAppCodenamesInMultipleTabs: @staticmethod def _assert_is_on_lookup_page(navigator: SourceAppNavigator) -> None: - navigator.nav_helper.wait_for(lambda: navigator.driver.find_element_by_id("upload")) + navigator.nav_helper.wait_for(lambda: navigator.driver.find_element(By.ID, "upload")) @staticmethod def _extract_generated_codename(navigator: SourceAppNavigator) -> str: - codename = navigator.driver.find_element_by_css_selector("#codename span").text + codename = navigator.driver.find_element(By.CSS_SELECTOR, "#codename span").text assert codename return codename @staticmethod def _extract_flash_message_content(navigator: SourceAppNavigator) -> str: - notification = navigator.driver.find_element_by_css_selector(".notification").text + notification = navigator.driver.find_element(By.CSS_SELECTOR, ".notification").text assert notification return notification diff --git a/securedrop/tests/functional/test_source_cancels.py b/securedrop/tests/functional/test_source_cancels.py index ca63469ba0..887a4cdc73 100644 --- a/securedrop/tests/functional/test_source_cancels.py +++ b/securedrop/tests/functional/test_source_cancels.py @@ -1,3 +1,4 @@ +from selenium.webdriver.common.by import By from tests.functional.app_navigators.source_app_nav import SourceAppNavigator @@ -30,5 +31,5 @@ def test_source_cancels_at_submit_page(self, sd_servers, tor_browser_web_driver) source_app_nav.nav_helper.safe_click_by_css_selector(".form-controls a") # And the right message is displayed - heading = source_app_nav.driver.find_element_by_id("submit-heading") + heading = source_app_nav.driver.find_element(By.ID, "submit-heading") assert heading.text == "Submit Files or Messages" diff --git a/securedrop/tests/functional/test_source_designation_collision.py b/securedrop/tests/functional/test_source_designation_collision.py index 3ab2a1a375..1d1439950a 100644 --- a/securedrop/tests/functional/test_source_designation_collision.py +++ b/securedrop/tests/functional/test_source_designation_collision.py @@ -1,6 +1,7 @@ from pathlib import Path import pytest +from selenium.webdriver.common.by import By from tests.factories import SecureDropConfigFactory from tests.functional.app_navigators.source_app_nav import SourceAppNavigator from tests.functional.conftest import spawn_sd_servers @@ -55,7 +56,7 @@ def test(self, sd_servers_with_designation_collisions, tor_browser_web_driver): # Then the right error message is displayed navigator.nav_helper.safe_click_by_css_selector("#create-form button") navigator.nav_helper.wait_for( - lambda: navigator.driver.find_element_by_css_selector(".error") + lambda: navigator.driver.find_element(By.CSS_SELECTOR, ".error") ) - flash_error = navigator.driver.find_element_by_css_selector(".error") + flash_error = navigator.driver.find_element(By.CSS_SELECTOR, ".error") assert "There was a temporary problem creating your account" in flash_error.text diff --git a/securedrop/tests/functional/test_source_warnings.py b/securedrop/tests/functional/test_source_warnings.py index 125ce96f91..782735be3f 100644 --- a/securedrop/tests/functional/test_source_warnings.py +++ b/securedrop/tests/functional/test_source_warnings.py @@ -3,6 +3,7 @@ import pytest from selenium import webdriver +from selenium.webdriver.common.by import By from tests.functional.app_navigators.source_app_nav import SourceAppNavigator from tests.functional.web_drivers import _FIREFOX_PATH @@ -18,6 +19,10 @@ def orbot_web_driver(sd_servers): profile = webdriver.FirefoxProfile(f_profile_path2) profile.set_preference("general.useragent.override", orbot_user_agent) + orbot_options = webdriver.FirefoxOptions() + orbot_options.binary_location = _FIREFOX_PATH + orbot_options.profile = profile + if sd_servers.journalist_app_base_url.find(".onion") != -1: # set FF preference to socks proxy in Tor Browser profile.set_preference("network.proxy.type", 1) @@ -27,7 +32,7 @@ def orbot_web_driver(sd_servers): profile.set_preference("network.proxy.socks_remote_dns", True) profile.set_preference("network.dns.blockDotOnion", False) profile.update_preferences() - orbot_web_driver = webdriver.Firefox(firefox_binary=_FIREFOX_PATH, firefox_profile=profile) + orbot_web_driver = webdriver.Firefox(options=orbot_options) try: driver_user_agent = orbot_web_driver.execute_script("return navigator.userAgent") @@ -50,12 +55,12 @@ def test_warning_appears_if_tor_browser_not_in_use(self, sd_servers, firefox_web navigator.source_visits_source_homepage() # Then they see a warning - warning_banner = navigator.driver.find_element_by_id("browser-tb") + warning_banner = navigator.driver.find_element(By.ID, "browser-tb") assert warning_banner.is_displayed() assert "It is recommended to use Tor Browser" in warning_banner.text # And they are able to dismiss the warning - warning_dismiss_button = navigator.driver.find_element_by_id("browser-tb-close") + warning_dismiss_button = navigator.driver.find_element(By.ID, "browser-tb-close") warning_dismiss_button.click() def warning_banner_is_hidden(): @@ -75,12 +80,12 @@ def test_warning_appears_if_orbot_is_used(self, sd_servers, orbot_web_driver): navigator.source_visits_source_homepage() # Then they see a warning - warning_banner = navigator.driver.find_element_by_id("browser-android") + warning_banner = navigator.driver.find_element(By.ID, "browser-android") assert warning_banner.is_displayed() assert "use the desktop version of Tor Browser" in warning_banner.text # And they are able to dismiss the warning - warning_dismiss_button = navigator.driver.find_element_by_id("browser-android-close") + warning_dismiss_button = navigator.driver.find_element(By.ID, "browser-android-close") warning_dismiss_button.click() def warning_banner_is_hidden(): @@ -100,6 +105,6 @@ def test_warning_high_security(self, sd_servers, tor_browser_web_driver): navigator.source_visits_source_homepage() # Then they see a warning - banner = navigator.driver.find_element_by_id("browser-security-level") + banner = navigator.driver.find_element(By.ID, "browser-security-level") assert banner.is_displayed() assert "Security Level is too low" in banner.text diff --git a/securedrop/tests/functional/web_drivers.py b/securedrop/tests/functional/web_drivers.py index 21af70cea3..ee57ebc7d6 100644 --- a/securedrop/tests/functional/web_drivers.py +++ b/securedrop/tests/functional/web_drivers.py @@ -92,12 +92,14 @@ def _create_firefox_driver( profile.set_preference("intl.accept_languages", accept_languages) profile.update_preferences() + firefox_options = webdriver.FirefoxOptions() + firefox_options.binary_location = _FIREFOX_PATH + firefox_options.profile = profile + firefox_driver = None for i in range(_DRIVER_RETRY_COUNT): try: - firefox_driver = webdriver.Firefox( - firefox_binary=_FIREFOX_PATH, firefox_profile=profile - ) + firefox_driver = webdriver.Firefox(options=firefox_options) firefox_driver.set_window_position(0, 0) firefox_driver.set_window_size(*_BROWSER_SIZE) logging.info("Created Firefox web driver")