Skip to content

Commit 780d021

Browse files
author
Allie Crevier
committed
request usb device on first CalledProcessError
1 parent 69f19d6 commit 780d021

File tree

2 files changed

+129
-6
lines changed

2 files changed

+129
-6
lines changed

securedrop_client/gui/widgets.py

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1886,9 +1886,9 @@ def __init__(self, controller, file_uuid):
18861886
buttons_layout = QHBoxLayout()
18871887
buttons.setLayout(buttons_layout)
18881888
usb_cancel_button = QPushButton(_('CANCEL'))
1889-
export_button = QPushButton(_('EXPORT'))
1889+
retry_export_button = QPushButton(_('CONTINUE'))
18901890
buttons_layout.addWidget(usb_cancel_button)
1891-
buttons_layout.addWidget(export_button)
1891+
buttons_layout.addWidget(retry_export_button)
18921892
usb_form_layout.addWidget(self.usb_error_message)
18931893
usb_form_layout.addWidget(usb_instructions)
18941894
usb_form_layout.addWidget(buttons, alignment=Qt.AlignRight)
@@ -1931,13 +1931,32 @@ def __init__(self, controller, file_uuid):
19311931

19321932
usb_cancel_button.clicked.connect(self.close)
19331933
passphrase_cancel_button.clicked.connect(self.close)
1934-
export_button.clicked.connect(self._export)
1934+
retry_export_button.clicked.connect(self._on_retry_export_button_clicked)
19351935
unlock_disk_button.clicked.connect(self._on_unlock_disk_clicked)
19361936

19371937
self._export()
19381938

1939-
@pyqtSlot()
1939+
19401940
def _export(self):
1941+
try:
1942+
self.controller.run_export_preflight_checks()
1943+
self._request_passphrase()
1944+
except ExportError as e:
1945+
# The first time we see a CALLED_PROCESS_ERROR, tell the user to insert the USB device
1946+
# in case the issue is that the Export VM cannot start due to a USB device being
1947+
# unavailable for attachment. According to the Qubes docs:
1948+
#
1949+
# "If the device is unavailable (physically missing or sourceVM not started), booting
1950+
# the targetVM fails."
1951+
#
1952+
# For information, see https://www.qubes-os.org/doc/device-handling
1953+
if e.status == ExportStatus.CALLED_PROCESS_ERROR.value:
1954+
self._request_to_insert_usb_device()
1955+
else:
1956+
self._update(e.status)
1957+
1958+
@pyqtSlot()
1959+
def _on_retry_export_button_clicked(self):
19411960
try:
19421961
self.controller.run_export_preflight_checks()
19431962
self._request_passphrase()

tests/gui/test_widgets.py

Lines changed: 106 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1444,7 +1444,7 @@ def test_FileWidget__on_export_clicked(mocker, session, source):
14441444

14451445
def test_ExportDialog__export(mocker):
14461446
"""
1447-
Ensure export runs preflight checks and requests password.
1447+
Ensure happy path runs preflight checks and requests passphrase.
14481448
"""
14491449
controller = mocker.MagicMock()
14501450
export_dialog = ExportDialog(controller, 'mock_uuid')
@@ -1456,9 +1456,45 @@ def test_ExportDialog__export(mocker):
14561456
export_dialog._request_passphrase.assert_called_with()
14571457

14581458

1459+
def test_ExportDialog__export_request_to_insert_usb_device_on_CALLED_PROCESS_ERROR(mocker):
1460+
"""
1461+
Ensure request to insert USB device on CALLED_PROCESS_ERROR.
1462+
"""
1463+
controller = mocker.MagicMock()
1464+
called_process_error = ExportError(ExportStatus.CALLED_PROCESS_ERROR.value)
1465+
controller.run_export_preflight_checks = mocker.MagicMock(side_effect=called_process_error)
1466+
export_dialog = ExportDialog(controller, 'mock_uuid')
1467+
export_dialog._request_passphrase = mocker.MagicMock()
1468+
export_dialog._request_to_insert_usb_device = mocker.MagicMock()
1469+
export_dialog._update = mocker.MagicMock()
1470+
1471+
export_dialog._export()
1472+
1473+
export_dialog._request_passphrase.assert_not_called()
1474+
export_dialog._request_to_insert_usb_device.assert_called_once_with()
1475+
export_dialog._update.assert_not_called()
1476+
1477+
1478+
def test_ExportDialog__export_request_to_insert_usb_device_on_USB_NOT_CONNECTED(mocker):
1479+
"""
1480+
Ensure request to insert USB device on USB_NOT_CONNECTED.
1481+
"""
1482+
controller = mocker.MagicMock()
1483+
usb_not_connected_error = ExportError(ExportStatus.USB_NOT_CONNECTED.value)
1484+
controller.run_export_preflight_checks = mocker.MagicMock(side_effect=usb_not_connected_error)
1485+
export_dialog = ExportDialog(controller, 'mock_uuid')
1486+
export_dialog._request_passphrase = mocker.MagicMock()
1487+
export_dialog._update = mocker.MagicMock()
1488+
1489+
export_dialog._export()
1490+
1491+
export_dialog._request_passphrase.assert_not_called()
1492+
export_dialog._update.assert_called_once_with('USB_NOT_CONNECTED')
1493+
1494+
14591495
def test_ExportDialog__export_updates_on_ExportError(mocker):
14601496
"""
1461-
Ensure export runs update and does not ask for password when preflight checks error.
1497+
Ensure update is run for ExportError that is not USB_NOT_CONNECTED or CALLED_PROCESS_ERROR.
14621498
"""
14631499
controller = mocker.MagicMock()
14641500
controller.run_export_preflight_checks = mocker.MagicMock(side_effect=ExportError('mock'))
@@ -1472,6 +1508,74 @@ def test_ExportDialog__export_updates_on_ExportError(mocker):
14721508
export_dialog._update.assert_called_once_with('mock')
14731509

14741510

1511+
def test_ExportDialog__on_retry_export_button_clicked(mocker):
1512+
"""
1513+
Ensure happy path runs preflight checks and requests passphrase.
1514+
"""
1515+
controller = mocker.MagicMock()
1516+
export_dialog = ExportDialog(controller, 'mock_uuid')
1517+
export_dialog._request_passphrase = mocker.MagicMock()
1518+
1519+
export_dialog._on_retry_export_button_clicked()
1520+
1521+
controller.run_export_preflight_checks.assert_called_with()
1522+
export_dialog._request_passphrase.assert_called_with()
1523+
1524+
1525+
def test_ExportDialog__on_retry_export_button_clicked_USB_NOT_CONNECTED(mocker):
1526+
"""
1527+
Ensure request to insert USB device on USB_NOT_CONNECTED.
1528+
"""
1529+
controller = mocker.MagicMock()
1530+
usb_not_connected_error = ExportError(ExportStatus.USB_NOT_CONNECTED.value)
1531+
controller.run_export_preflight_checks = mocker.MagicMock(side_effect=usb_not_connected_error)
1532+
export_dialog = ExportDialog(controller, 'mock_uuid')
1533+
export_dialog._request_passphrase = mocker.MagicMock()
1534+
export_dialog._update = mocker.MagicMock()
1535+
1536+
export_dialog._on_retry_export_button_clicked()
1537+
1538+
export_dialog._request_passphrase.assert_not_called()
1539+
export_dialog._update.assert_called_once_with('USB_NOT_CONNECTED')
1540+
1541+
1542+
def test_ExportDialog__on_retry_export_button_clicked_CALLED_PROCESS_ERROR(mocker):
1543+
"""
1544+
Ensure update is run on CALLED_PROCESS_ERROR.
1545+
"""
1546+
controller = mocker.MagicMock()
1547+
called_process_error = ExportError(ExportStatus.CALLED_PROCESS_ERROR.value)
1548+
controller.run_export_preflight_checks = mocker.MagicMock(side_effect=called_process_error)
1549+
export_dialog = ExportDialog(controller, 'mock_uuid')
1550+
export_dialog._request_passphrase = mocker.MagicMock()
1551+
export_dialog._request_to_insert_usb_device = mocker.MagicMock()
1552+
export_dialog._update = mocker.MagicMock()
1553+
1554+
export_dialog._on_retry_export_button_clicked()
1555+
1556+
export_dialog._request_passphrase.assert_not_called()
1557+
export_dialog._request_to_insert_usb_device.assert_not_called()
1558+
export_dialog._update.assert_called_once_with('CALLED_PROCESS_ERROR')
1559+
1560+
1561+
def test_ExportDialog__on_retry_export_button_clicked_updates_on_ExportError(mocker):
1562+
"""
1563+
Ensure update is run for ExportError that is not USB_NOT_CONNECTED.
1564+
"""
1565+
controller = mocker.MagicMock()
1566+
controller.run_export_preflight_checks = mocker.MagicMock(side_effect=ExportError('mock'))
1567+
export_dialog = ExportDialog(controller, 'mock_uuid')
1568+
export_dialog._request_passphrase = mocker.MagicMock()
1569+
export_dialog._request_to_insert_usb_device = mocker.MagicMock()
1570+
export_dialog._update = mocker.MagicMock()
1571+
1572+
export_dialog._on_retry_export_button_clicked()
1573+
1574+
export_dialog._request_passphrase.assert_not_called()
1575+
export_dialog._request_to_insert_usb_device.assert_not_called()
1576+
export_dialog._update.assert_called_once_with('mock')
1577+
1578+
14751579
def test_ExportDialog__request_to_insert_usb_device(mocker):
14761580
"""Ensure that the correct widgets are visible or hidden."""
14771581
export_dialog = ExportDialog(mocker.MagicMock(), 'mock_uuid')

0 commit comments

Comments
 (0)