def test_download_message(self, _sd_servers_v2_with_missing_file, firefox_web_driver): # Given an SD server with a submission whose file was deleted from disk # And a journalist logged into the journalist interface journ_app_nav = JournalistAppNavigator( journalist_app_base_url=_sd_servers_v2_with_missing_file. journalist_app_base_url, web_driver=firefox_web_driver, ) journ_app_nav.journalist_logs_in( username=_sd_servers_v2_with_missing_file.journalist_username, password=_sd_servers_v2_with_missing_file.journalist_password, otp_secret=_sd_servers_v2_with_missing_file.journalist_otp_secret, ) # When the journalist clicks on the individual message from the source page journ_app_nav.journalist_checks_messages() 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")) submissions = journ_app_nav.driver.find_elements_by_css_selector( "#submissions a") assert 1 == len(submissions) file_link = submissions[0] file_link.click() # Then they see the expected error message self._journalist_sees_missing_file_error_message(journ_app_nav) self._journalist_is_on_collection_page(journ_app_nav)
def test_journalist_uses_index_delete_files_button_modal( self, sd_servers_v2_with_submitted_file, firefox_web_driver): # Given an SD server with a file submitted by a source # And a journalist logged into the journalist interface journ_app_nav = JournalistAppNavigator( journalist_app_base_url=sd_servers_v2_with_submitted_file. journalist_app_base_url, web_driver=firefox_web_driver, ) journ_app_nav.journalist_logs_in( username=sd_servers_v2_with_submitted_file.journalist_username, password=sd_servers_v2_with_submitted_file.journalist_password, otp_secret=sd_servers_v2_with_submitted_file.journalist_otp_secret, ) # And at least one source previously used the app initial_sources_count = journ_app_nav.count_sources_on_index_page() assert initial_sources_count > 0 # 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.nav_helper.safe_click_by_id("select_all") except NoSuchElementException: journ_app_nav.nav_helper.safe_click_all_by_css_selector( 'input[type="checkbox"][name="cols_selected"]') # When the journalist clicks the delete collection button... self._journalist_clicks_delete_collections_link(journ_app_nav) # ...and then clicks the delete files button on the first modal journ_app_nav.nav_helper.safe_click_by_id("delete-files-and-messages") # Then they are redirected to the index with the source present, files # 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") assert "The files and messages have been deleted" in flash_msg.text 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 journ_app_nav.nav_helper.wait_for(one_source_no_files)
def test_select_message_and_download_selected( self, _sd_servers_v2_with_missing_file, firefox_web_driver): # Given an SD server with a submission whose file was deleted from disk # And a journalist logged into the journalist interface journ_app_nav = JournalistAppNavigator( journalist_app_base_url=_sd_servers_v2_with_missing_file. journalist_app_base_url, web_driver=firefox_web_driver, ) journ_app_nav.journalist_logs_in( username=_sd_servers_v2_with_missing_file.journalist_username, password=_sd_servers_v2_with_missing_file.journalist_password, otp_secret=_sd_servers_v2_with_missing_file.journalist_otp_secret, ) # 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") assert len(checkboxes) == 1 checkboxes[0].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) self._journalist_is_on_collection_page(journ_app_nav)
def test_journalist_uses_col_delete_collection_button_modal( self, sd_servers_v2_with_submitted_file, firefox_web_driver): # Given an SD server with a file submitted by a source # And a journalist logged into the journalist interface journ_app_nav = JournalistAppNavigator( journalist_app_base_url=sd_servers_v2_with_submitted_file. journalist_app_base_url, web_driver=firefox_web_driver, ) journ_app_nav.journalist_logs_in( username=sd_servers_v2_with_submitted_file.journalist_username, password=sd_servers_v2_with_submitted_file.journalist_password, otp_secret=sd_servers_v2_with_submitted_file.journalist_otp_secret, ) # And the journalist went to the individual source's page journ_app_nav.journalist_visits_col() # And the source has at least one submission initial_submissions_count = journ_app_nav.count_submissions_on_current_page( ) assert initial_submissions_count > 0 # When the journalist clicks the delete collection button... self._journalist_clicks_delete_collection_link(journ_app_nav) # ...but then cancels the deletion journ_app_nav.nav_helper.safe_click_by_id( "cancel-collection-deletions") # Then they see the same number of submissions as before submissions_after_canceling_count = journ_app_nav.count_submissions_on_current_page( ) assert submissions_after_canceling_count == initial_submissions_count # When the journalist clicks the delete collection button... self._journalist_clicks_delete_collection_link(journ_app_nav) # ... and then confirms the deletion journ_app_nav.nav_helper.safe_click_by_id("delete-collection-button") # Then the journalist was redirected to the home page assert journ_app_nav.is_on_journalist_homepage()
def test_submit_and_retrieve_happy_path(self, sd_servers_v2_with_clean_state, tor_browser_web_driver, firefox_web_driver): # Given a source user accessing the app from their browser source_app_nav = SourceAppNagivator( source_app_base_url=sd_servers_v2_with_clean_state. source_app_base_url, web_driver=tor_browser_web_driver, ) # And they created an account source_app_nav.source_visits_source_homepage() source_app_nav.source_clicks_submit_documents_on_homepage() source_app_nav.source_continues_to_submit_page() # And the source user submitted a message submitted_message = "Confidential message with some international characters: éèö" source_app_nav.source_submits_a_message(message=submitted_message) source_app_nav.source_logs_out() # When a journalist logs in journ_app_nav = JournalistAppNavigator( journalist_app_base_url=sd_servers_v2_with_clean_state. journalist_app_base_url, web_driver=firefox_web_driver, ) journ_app_nav.journalist_logs_in( username=sd_servers_v2_with_clean_state.journalist_username, password=sd_servers_v2_with_clean_state.journalist_password, otp_secret=sd_servers_v2_with_clean_state.journalist_otp_secret, ) journ_app_nav.journalist_checks_messages() # And they try to download the message # Then it succeeds and the journalist sees correct message servers_sd_config = sd_servers_v2_with_clean_state.config_in_use retrieved_message = journ_app_nav.journalist_downloads_first_message( encryption_mgr_to_use_for_decryption=EncryptionManager( gpg_key_dir=Path(servers_sd_config.GPG_KEY_DIR), journalist_key_fingerprint=servers_sd_config.JOURNALIST_KEY, )) assert retrieved_message == submitted_message
def test_download_source_unread(self, _sd_servers_v2_with_missing_file, firefox_web_driver): # Given an SD server with a submission whose file was deleted from disk # And a journalist logged into the journalist interface journ_app_nav = JournalistAppNavigator( journalist_app_base_url=_sd_servers_v2_with_missing_file. journalist_app_base_url, web_driver=firefox_web_driver, ) journ_app_nav.journalist_logs_in( username=_sd_servers_v2_with_missing_file.journalist_username, password=_sd_servers_v2_with_missing_file.journalist_password, otp_secret=_sd_servers_v2_with_missing_file.journalist_otp_secret, ) # 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").click() # Then they see the expected error message self._journalist_sees_missing_file_error_message(journ_app_nav) journ_app_nav.is_on_journalist_homepage()
def test_submit_and_retrieve_happy_path(self, locale, sd_servers_v2_with_clean_state, tor_browser_web_driver, firefox_web_driver): # Given a source user accessing the app from their browser locale_with_commas = locale.replace("_", "-") source_app_nav = SourceAppNagivator( source_app_base_url=sd_servers_v2_with_clean_state. source_app_base_url, web_driver=tor_browser_web_driver, accept_languages=locale_with_commas, ) # And they created an account source_app_nav.source_visits_source_homepage() source_app_nav.source_clicks_submit_documents_on_homepage() source_app_nav.source_continues_to_submit_page() source_codename = source_app_nav.source_retrieves_codename_from_hint() # And the source user submitted a file submitted_content = "Confidential file with some international characters: éèö" source_app_nav.source_submits_a_file(file_content=submitted_content) source_app_nav.source_logs_out() # And a journalist logs in journ_app_nav = JournalistAppNavigator( journalist_app_base_url=sd_servers_v2_with_clean_state. journalist_app_base_url, web_driver=firefox_web_driver, ) journ_app_nav.journalist_logs_in( username=sd_servers_v2_with_clean_state.journalist_username, password=sd_servers_v2_with_clean_state.journalist_password, otp_secret=sd_servers_v2_with_clean_state.journalist_otp_secret, ) journ_app_nav.journalist_checks_messages() # When they star and unstar the submission, then it succeeds self._journalist_stars_and_unstars_single_message(journ_app_nav) # And when they try to download the file # Then it succeeds and the journalist sees the correct content apps_sd_config = sd_servers_v2_with_clean_state.config_in_use retrieved_message = journ_app_nav.journalist_downloads_first_message( encryption_mgr_to_use_for_decryption=EncryptionManager( gpg_key_dir=Path(apps_sd_config.GPG_KEY_DIR), journalist_key_fingerprint=apps_sd_config.JOURNALIST_KEY, )) assert retrieved_message == submitted_content # And when they reply to the source, it succeeds journ_app_nav.journalist_sends_reply_to_source() # And when the source user comes back source_app_nav.source_visits_source_homepage() source_app_nav.source_chooses_to_login() source_app_nav.source_proceeds_to_login(codename=source_codename) save_screenshot_and_html(source_app_nav.driver, locale, "source-checks_for_reply") # When they delete the journalist's reply, it succeeds self._source_deletes_journalist_reply(source_app_nav) save_screenshot_and_html(source_app_nav.driver, locale, "source-deletes_reply")
def test_journalist_verifies_deletion_of_one_submission_modal( self, sd_servers_v2_with_submitted_file, firefox_web_driver): # Given an SD server with a file submitted by a source # And a journalist logged into the journalist interface journ_app_nav = JournalistAppNavigator( journalist_app_base_url=sd_servers_v2_with_submitted_file. journalist_app_base_url, web_driver=firefox_web_driver, ) journ_app_nav.journalist_logs_in( username=sd_servers_v2_with_submitted_file.journalist_username, password=sd_servers_v2_with_submitted_file.journalist_password, otp_secret=sd_servers_v2_with_submitted_file.journalist_otp_secret, ) # And the journalist went to the individual source's page journ_app_nav.journalist_visits_col() # And the source has at least one submission initial_submissions_count = journ_app_nav.count_submissions_on_current_page( ) assert initial_submissions_count > 0 # And the journalist selected the first submission journ_app_nav.journalist_selects_first_doc() # When the journalist clicks the delete button... journ_app_nav.journalist_clicks_delete_selected_link() # ...but then cancels the deletion journ_app_nav.nav_helper.safe_click_by_id("cancel-selected-deletions") # Then they see the same number of submissions as before submissions_after_canceling_count = journ_app_nav.count_submissions_on_current_page( ) assert submissions_after_canceling_count == initial_submissions_count # And when the journalist clicks the delete button... journ_app_nav.journalist_clicks_delete_selected_link() # ... and then confirms the deletion journ_app_nav.nav_helper.safe_click_by_id("delete-selected") # Then they see less submissions than before because one was deleted def submission_deleted(): submissions_after_confirming_count = journ_app_nav.count_submissions_on_current_page( ) assert submissions_after_confirming_count < initial_submissions_count journ_app_nav.nav_helper.wait_for(submission_deleted)
def test_journalist_interface_ui_with_modal( self, sd_servers_v2_with_submitted_file, firefox_web_driver): # Given an SD server with a file submitted by a source # And a journalist logged into the journalist interface journ_app_nav = JournalistAppNavigator( journalist_app_base_url=sd_servers_v2_with_submitted_file. journalist_app_base_url, web_driver=firefox_web_driver, ) journ_app_nav.journalist_logs_in( username=sd_servers_v2_with_submitted_file.journalist_username, password=sd_servers_v2_with_submitted_file.journalist_password, otp_secret=sd_servers_v2_with_submitted_file.journalist_otp_secret, ) # When the journalist uses the filter by sources to find text that doesn't match any source filter_box = journ_app_nav.nav_helper.safe_send_keys_by_id( "filter", "thiswordisnotinthewordlist") # Then no sources are displayed on the page sources = journ_app_nav.get_sources_on_index_page() assert len(sources) > 0 for source in sources: assert source.is_displayed() is False # And when the journalist clears the filter filter_box.clear() filter_box.send_keys(Keys.RETURN) # Then all sources are displayed for source in sources: assert source.is_displayed() is True # And given the journalist designation of the first source sources = journ_app_nav.get_sources_on_index_page() assert len(sources) > 0 first_source_designation = sources[0].text # When the journalist uses the filter find this source designation filter_box.send_keys(first_source_designation) # Then only the corresponding source is displayed for source in sources: 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.click() # Then only the visible source gets selected 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]") if source_designation == first_source_designation: assert checkbox.is_selected() else: assert not checkbox.is_selected() # And when the journalist clears the filter and then selects all sources filter_box.clear() 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]") 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.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]") if source_designation == first_source_designation: assert not checkbox.is_selected() else: assert checkbox.is_selected() # And when the journalist clears the filter and leaves none selected filter_box.clear() filter_box.send_keys(Keys.RETURN) select_none.click() for source_row in source_rows: assert source_row.is_displayed() 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") 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") 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() checkboxes = journ_app_nav.get_submission_checkboxes_on_current_page() for checkbox in checkboxes: classes = checkbox.get_attribute("class") assert "unread-cb" in classes # And when the journalist clicks the delete button, it succeeds journ_app_nav.nav_helper.safe_click_all_by_css_selector( "[name=doc_names_selected]") journ_app_nav.nav_helper.safe_click_by_css_selector( "a#delete-selected-link")
def test_journalist_uses_index_delete_collections_button_modal( self, sd_servers_v2_with_submitted_file, firefox_web_driver): # Given an SD server with a file submitted by a source # And a journalist logged into the journalist interface journ_app_nav = JournalistAppNavigator( journalist_app_base_url=sd_servers_v2_with_submitted_file. journalist_app_base_url, web_driver=firefox_web_driver, ) journ_app_nav.journalist_logs_in( username=sd_servers_v2_with_submitted_file.journalist_username, password=sd_servers_v2_with_submitted_file.journalist_password, otp_secret=sd_servers_v2_with_submitted_file.journalist_otp_secret, ) # And at least one source previously used the app initial_sources_count = journ_app_nav.count_sources_on_index_page() assert initial_sources_count > 0 # 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.nav_helper.safe_click_by_id("select_all") except NoSuchElementException: journ_app_nav.nav_helper.safe_click_all_by_css_selector( 'input[type="checkbox"][name="cols_selected"]') # When the journalist clicks the delete collection button... self._journalist_clicks_delete_collections_link(journ_app_nav) # ...but then cancels the deletion self._journalist_clicks_delete_collections_cancel_on_first_modal( journ_app_nav) # Then they see the same number of sources as before assert initial_sources_count == journ_app_nav.count_sources_on_index_page( ) # And when the journalist clicks the delete collection button again... self._journalist_clicks_delete_collections_link(journ_app_nav) # ...and then confirms the deletion... self._journalist_clicks_delete_collections_on_first_modal( journ_app_nav) # ... and cancels the deletion on the second modal/confirmation prompt journ_app_nav.nav_helper.safe_click_by_id( "cancel-collections-deletions") # Then they see the same number of sources as before assert initial_sources_count == journ_app_nav.count_sources_on_index_page( ) # And when the journalist clicks the delete collection button again and confirms it self._journalist_clicks_delete_collections_link(journ_app_nav) self._journalist_clicks_delete_collections_on_first_modal( journ_app_nav) journ_app_nav.nav_helper.safe_click_by_id("delete-collections-confirm") # 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") 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) # And the journalist gets redirected to the index with the source not present anymore def no_sources(): assert journ_app_nav.count_sources_on_index_page() == 0 journ_app_nav.nav_helper.wait_for(no_sources)