From 27525f55b3a9b43dd8410d381eb661edfb37b46c Mon Sep 17 00:00:00 2001 From: Ro Date: Fri, 2 Feb 2024 20:47:30 -0500 Subject: [PATCH] (WIP) Add functional tests for Export Wizard --- client/tests/conftest.py | 104 +- ... => test_export_wizard_device_locked.yaml} | 0 ...izard_dialog_device_already_unlocked.yaml} | 0 ...est_export_wizard_no_device_then_fail.yaml | 1518 +++++++++++++++++ .../functional/test_export_file_dialog.py | 123 -- .../functional/test_export_file_wizard.py | 272 +++ .../gui/conversation/export/test_device.py | 46 +- .../conversation/export/test_export_wizard.py | 13 + 8 files changed, 1924 insertions(+), 152 deletions(-) rename client/tests/functional/cassettes/{test_export_file_dialog_locked.yaml => test_export_wizard_device_locked.yaml} (100%) rename client/tests/functional/cassettes/{test_export_file_dialog_device_already_unlocked.yaml => test_export_wizard_dialog_device_already_unlocked.yaml} (100%) create mode 100644 client/tests/functional/cassettes/test_export_wizard_no_device_then_fail.yaml delete mode 100644 client/tests/functional/test_export_file_dialog.py create mode 100644 client/tests/functional/test_export_file_wizard.py diff --git a/client/tests/conftest.py b/client/tests/conftest.py index a8be1255c..8607ce826 100644 --- a/client/tests/conftest.py +++ b/client/tests/conftest.py @@ -5,6 +5,7 @@ from configparser import ConfigParser from datetime import datetime from uuid import uuid4 +from unittest import mock import pytest from PyQt5.QtCore import Qt @@ -22,6 +23,7 @@ Source, make_session_maker, ) +from securedrop_client.export_status import ExportStatus from securedrop_client.gui import conversation from securedrop_client.gui.main import Window from securedrop_client.logic import Controller @@ -47,7 +49,7 @@ TIME_CLICK_ACTION = 1000 TIME_RENDER_SOURCE_LIST = 20000 TIME_RENDER_CONV_VIEW = 1000 -TIME_RENDER_EXPORT_DIALOG = 1000 +TIME_RENDER_EXPORT_WIZARD = 1000 TIME_FILE_DOWNLOAD = 5000 @@ -168,13 +170,105 @@ def homedir(i18n): @pytest.fixture(scope="function") -def mock_export(): +def mock_export_locked(): + """ + Represents the following scenario: + * Locked USB already inserted + * "Export" clicked, export wizard launched + * Passphrase successfully entered on first attempt (and export suceeeds) + """ + device = conversation.ExportDevice() + + device.run_export_preflight_checks = lambda: device.export_state_changed.emit( + ExportStatus.DEVICE_LOCKED + ) + device.run_printer_preflight_checks = lambda: None + device.print = lambda filepaths: None + device.export = mock.MagicMock() + device.export.side_effect = [ + lambda filepaths, passphrase: device.export_state_changed.emit( + ExportStatus.DEVICE_WRITABLE + ), + lambda filepaths, passphrase: device.export_state_changed.emit(ExportStatus.SUCCESS_EXPORT), + ] + + return device + + +@pytest.fixture(scope="function") +def mock_export_unlocked(): + """ + Represents the following scenario: + * USB already inserted and unlocked by the user + * Export wizard launched + * Export succeeds + """ + device = conversation.ExportDevice() + + device.run_export_preflight_checks = lambda: device.export_state_changed.emit( + ExportStatus.DEVICE_WRITABLE + ) + device.run_printer_preflight_checks = lambda: None + device.print = lambda filepaths: None + device.export = lambda filepaths, passphrase: device.export_state_changed.emit( + ExportStatus.SUCCESS_EXPORT + ) + + return device + + +@pytest.fixture(scope="function") +def mock_export_no_usb_then_bad_passphrase_then_fail(): + """ + Represents the following scenario: + * Export wizard launched + * Locked USB inserted + * Mistyped Passphrase + * Correct passphrase + * Export fails + """ + device = conversation.ExportDevice() + + device.run_export_preflight_checks = lambda: device.export_state_changed.emit( + ExportStatus.NO_DEVICE_DETECTED + ) + device.run_printer_preflight_checks = lambda: None + device.print = lambda filepaths: None + device.export = mock.MagicMock() + device.export.side_effect = [ + lambda filepaths, passphrase: device.export_state_changed.emit(ExportStatus.DEVICE_LOCKED), + lambda filepaths, passphrase: device.export_state_changed.emit( + ExportStatus.ERROR_UNLOCK_LUKS + ), + lambda filepaths, passphrase: device.export_state_changed.emit( + ExportStatus.DEVICE_WRITABLE + ), + lambda filepaths, passphrase: device.export_state_changed.emit(ExportStatus.ERROR_EXPORT), + ] + + return device + + +@pytest.fixture(scope="function") +def mock_export_fail_early(): + """ + Represents the following scenario: + * Locked USB inserted + * Export wizard launched + * Unrecoverable error before export happens + (eg, mount error) + """ device = conversation.ExportDevice() - device.run_export_preflight_checks = lambda dir: None - device.run_printer_preflight_checks = lambda dir: None + device.run_export_preflight_checks = lambda: device.export_state_changed.emit( + ExportStatus.DEVICE_LOCKED + ) + device.run_printer_preflight_checks = lambda: None device.print = lambda filepaths: None - device.export = lambda filepaths, passphrase: None + device.export = mock.MagicMock() + device.export = lambda filepaths, passphrase: device.export_state_changed.emit( + ExportStatus.ERROR_MOUNT + ) return device diff --git a/client/tests/functional/cassettes/test_export_file_dialog_locked.yaml b/client/tests/functional/cassettes/test_export_wizard_device_locked.yaml similarity index 100% rename from client/tests/functional/cassettes/test_export_file_dialog_locked.yaml rename to client/tests/functional/cassettes/test_export_wizard_device_locked.yaml diff --git a/client/tests/functional/cassettes/test_export_file_dialog_device_already_unlocked.yaml b/client/tests/functional/cassettes/test_export_wizard_dialog_device_already_unlocked.yaml similarity index 100% rename from client/tests/functional/cassettes/test_export_file_dialog_device_already_unlocked.yaml rename to client/tests/functional/cassettes/test_export_wizard_dialog_device_already_unlocked.yaml diff --git a/client/tests/functional/cassettes/test_export_wizard_no_device_then_fail.yaml b/client/tests/functional/cassettes/test_export_wizard_no_device_then_fail.yaml new file mode 100644 index 000000000..d59c25ebb --- /dev/null +++ b/client/tests/functional/cassettes/test_export_wizard_no_device_then_fail.yaml @@ -0,0 +1,1518 @@ +interactions: +- request: + body: '{"username": "journalist", "passphrase": "correct horse battery staple + profanity oil chewy", "one_time_code": "123456"}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '119' + User-Agent: + - python-requests/2.31.0 + method: POST + uri: http://localhost:8081/api/v1/token + response: + body: + string: '{"expiration":"2023-12-08T21:31:36.503560Z","journalist_first_name":null,"journalist_last_name":null,"journalist_uuid":"c63874d0-0723-475e-8773-a5a0eeaaa4f9","token":"IjkwQVJGa05CWDRQd3hwZVBWVTZfakI5Y3RxRy1JeWZNa3g2MkRGNmNlX2ci.ZXNvGA.4vDAGIjsM4zouaM3IhIBR3jzIrM"} + + ' + headers: + Connection: + - close + Content-Length: + - '265' + Content-Type: + - application/json + Date: + - Fri, 08 Dec 2023 19:31:36 GMT + Server: + - Werkzeug/2.2.3 Python/3.8.10 + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Authorization: + - Token IjkwQVJGa05CWDRQd3hwZVBWVTZfakI5Y3RxRy1JeWZNa3g2MkRGNmNlX2ci.ZXNvGA.4vDAGIjsM4zouaM3IhIBR3jzIrM + Connection: + - keep-alive + Content-Type: + - application/json + User-Agent: + - python-requests/2.31.0 + method: GET + uri: http://localhost:8081/api/v1/users + response: + body: + string: '{"users":[{"first_name":null,"last_name":null,"username":"journalist","uuid":"c63874d0-0723-475e-8773-a5a0eeaaa4f9"},{"first_name":null,"last_name":null,"username":"dellsberg","uuid":"ac647c21-82f5-4d19-8350-6657a7d32f6b"},{"first_name":null,"last_name":null,"username":"deleted","uuid":"200a587e-b40c-48eb-b18a-0d1263f8af2e"}]} + + ' + headers: + Connection: + - close + Content-Length: + - '329' + Content-Type: + - application/json + Date: + - Fri, 08 Dec 2023 19:31:36 GMT + Server: + - Werkzeug/2.2.3 Python/3.8.10 + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Authorization: + - Token IjkwQVJGa05CWDRQd3hwZVBWVTZfakI5Y3RxRy1JeWZNa3g2MkRGNmNlX2ci.ZXNvGA.4vDAGIjsM4zouaM3IhIBR3jzIrM + Connection: + - keep-alive + Content-Type: + - application/json + User-Agent: + - python-requests/2.31.0 + method: GET + uri: http://localhost:8081/api/v1/sources + response: + body: + string: '{"sources":[{"add_star_url":"/api/v1/sources/1924d581-a3af-45c6-a3c9-0ec2f1205bc1/add_star","interaction_count":6,"is_flagged":false,"is_starred":false,"journalist_designation":"oriental + hutch","key":{"fingerprint":"DF4DC2E19F0A6A304C8C3188AEF8C5E2BD8AE199","public":"-----BEGIN + PGP PUBLIC KEY BLOCK-----\nComment: DF4D C2E1 9F0A 6A30 4C8C 3188 AEF8 C5E2 + BD8A E199\nComment: Source Key