diff --git a/Makefile b/Makefile index 0f6ff60987..4303ce3cfe 100644 --- a/Makefile +++ b/Makefile @@ -44,14 +44,6 @@ test-random: ## Run the application tests in random order xvfb-run $$TEST_CMD ; else \ $$TEST_CMD ; fi -FUNCTESTS ?= func_tests -.PHONY: func-test -func-test: - @TEST_CMD="python -m pytest -v --random-order-bucket=global $(TESTOPTS) $(FUNCTESTS)" ; \ - if command -v xvfb-run > /dev/null; then \ - xvfb-run $$TEST_CMD ; else \ - $$TEST_CMD ; fi - .PHONY: lint lint: ## Run the linters @flake8 securedrop_client tests diff --git a/dev-requirements.in b/dev-requirements.in index 53851bd62d..65c7575a47 100644 --- a/dev-requirements.in +++ b/dev-requirements.in @@ -19,6 +19,8 @@ pytest-cov==2.8.1 pytest-mock==1.10.0 pytest-qt==3.3.0 pytest-random-order==1.0.4 +pytest-vcr==1.0.2 pytest-xdist==1.30.0 sip==4.19.8 typed-ast==1.3.4 +vcrpy==4.0.2 diff --git a/dev-requirements.txt b/dev-requirements.txt index ee1becd176..f072ff3c64 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -86,6 +86,25 @@ more-itertools==4.3.0 \ --hash=sha256:c187a73da93e7a8acc0001572aebc7e3c69daf7bf6881a2cea10650bd4420092 \ --hash=sha256:c476b5d3a34e12d40130bc2f935028b5f636df8f372dc2c1c01dc19681b2039e \ --hash=sha256:fcbfeaea0be121980e15bc97b3817b5202ca73d0eae185b4550cbfce2a3ebb3d +multidict==4.7.4 \ + --hash=sha256:13f3ebdb5693944f52faa7b2065b751cb7e578b8dd0a5bb8e4ab05ad0188b85e \ + --hash=sha256:26502cefa86d79b86752e96639352c7247846515c864d7c2eb85d036752b643c \ + --hash=sha256:4fba5204d32d5c52439f88437d33ad14b5f228e25072a192453f658bddfe45a7 \ + --hash=sha256:527124ef435f39a37b279653ad0238ff606b58328ca7989a6df372fd75d7fe26 \ + --hash=sha256:5414f388ffd78c57e77bd253cf829373721f450613de53dc85a08e34d806e8eb \ + --hash=sha256:5eee66f882ab35674944dfa0d28b57fa51e160b4dce0ce19e47f495fdae70703 \ + --hash=sha256:63810343ea07f5cd86ba66ab66706243a6f5af075eea50c01e39b4ad6bc3c57a \ + --hash=sha256:6bd10adf9f0d6a98ccc792ab6f83d18674775986ba9bacd376b643fe35633357 \ + --hash=sha256:83c6ddf0add57c6b8a7de0bc7e2d656be3eefeff7c922af9a9aae7e49f225625 \ + --hash=sha256:93166e0f5379cf6cd29746989f8a594fa7204dcae2e9335ddba39c870a287e1c \ + --hash=sha256:9a7b115ee0b9b92d10ebc246811d8f55d0c57e82dbb6a26b23c9a9a6ad40ce0c \ + --hash=sha256:a38baa3046cce174a07a59952c9f876ae8875ef3559709639c17fdf21f7b30dd \ + --hash=sha256:a6d219f49821f4b2c85c6d426346a5d84dab6daa6f85ca3da6c00ed05b54022d \ + --hash=sha256:a8ed33e8f9b67e3b592c56567135bb42e7e0e97417a4b6a771e60898dfd5182b \ + --hash=sha256:d7d428488c67b09b26928950a395e41cc72bb9c3d5abfe9f0521940ee4f796d4 \ + --hash=sha256:dcfed56aa085b89d644af17442cdc2debaa73388feba4b8026446d168ca8dad7 \ + --hash=sha256:f29b885e4903bd57a7789f09fe9d60b6475a6c1a4c0eca874d8558f00f9d4b51 \ + # via yarl mypy-extensions==0.4.1 \ --hash=sha256:37e0e956f41369209a3d5f34580150bcacfabaa57b33a15c0b25f4b5725e0812 \ --hash=sha256:b16cabe759f55e3409a7d231ebd2841378fb0c27a5d1994719e340e4f429ac3e @@ -165,6 +184,9 @@ pytest-qt==3.3.0 \ pytest-random-order==1.0.4 \ --hash=sha256:6b2159342a4c8c10855bc4fc6d65ee890fc614cb2b4ff688979b008a82a0ff52 \ --hash=sha256:72279a7f823969e18b10e438950f58330d17e0fcffb57cbd7929770cd687ecb2 +pytest-vcr==1.0.2 \ + --hash=sha256:23ee51b75abbcc43d926272773aae4f39f93aceb75ed56852d0bf618f92e1896 \ + --hash=sha256:2f316e0539399bea0296e8b8401145c62b6f85e9066af7e57b6151481b0d6d9c pytest-xdist==1.30.0 \ --hash=sha256:5d1b1d4461518a6023d56dab62fb63670d6f7537f23e2708459a557329accf48 \ --hash=sha256:a8569b027db70112b290911ce2ed732121876632fb3f40b1d39cd2f72f58b147 @@ -176,6 +198,19 @@ python-dateutil==2.7.5 \ --hash=sha256:88f9287c0174266bb0d8cedd395cfba9c58e87e5ad86b2ce58859bc11be3cf02 python-editor==1.0.3 \ --hash=sha256:a3c066acee22a1c94f63938341d4fb374e3fdd69366ed6603d7b24bed1efc565 +pyyaml==5.3 \ + --hash=sha256:059b2ee3194d718896c0ad077dd8c043e5e909d9180f387ce42012662a4946d6 \ + --hash=sha256:1cf708e2ac57f3aabc87405f04b86354f66799c8e62c28c5fc5f88b5521b2dbf \ + --hash=sha256:24521fa2890642614558b492b473bee0ac1f8057a7263156b02e8b14c88ce6f5 \ + --hash=sha256:4fee71aa5bc6ed9d5f116327c04273e25ae31a3020386916905767ec4fc5317e \ + --hash=sha256:70024e02197337533eef7b85b068212420f950319cc8c580261963aefc75f811 \ + --hash=sha256:74782fbd4d4f87ff04159e986886931456a1894c61229be9eaf4de6f6e44b99e \ + --hash=sha256:940532b111b1952befd7db542c370887a8611660d2b9becff75d39355303d82d \ + --hash=sha256:cb1f2f5e426dc9f07a7681419fe39cee823bb74f723f36f70399123f439e9b20 \ + --hash=sha256:dbbb2379c19ed6042e8f11f2a2c66d39cceb8aeace421bfc29d085d93eda3689 \ + --hash=sha256:e3a057b7a64f1222b56e47bcff5e4b94c4f61faac04c7c4ecb1985e18caa3994 \ + --hash=sha256:e9f45bd5b92c7974e59bcd2dcc8631a6b6cc380a904725fce7bc08872e691615 \ + # via vcrpy requests==2.20.0 \ --hash=sha256:99dcfdaaeb17caf6e526f32b6a7b780461512ab3f1d992187801694cba42770c \ --hash=sha256:a84b8c9ab6239b578f22d1c21d51b696dcfe004032bb80ea832398d6909d7279 @@ -223,10 +258,35 @@ typed-ast==1.3.4 \ urllib3==1.24.3 \ --hash=sha256:2393a695cd12afedd0dcb26fe5d50d0cf248e5a66f75dbd89a3d4eb333a61af4 \ --hash=sha256:a637e5fae88995b256e3409dc4d52c2e2e0ba32c42a6365fee8bbd2238de3cfb +vcrpy==4.0.2 \ + --hash=sha256:9740c5b1b63626ec55cefb415259a2c77ce00751e97b0f7f214037baaf13c7bf \ + --hash=sha256:c4ddf1b92c8a431901c56a1738a2c797d965165a96348a26f4b2bbc5fa6d36d9 wcwidth==0.1.7 \ --hash=sha256:3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e \ --hash=sha256:f4ebe71925af7b40a864553f761ed559b43544f8f71746c2d756c7fe788ade7c \ # via pytest +wrapt==1.11.2 \ + --hash=sha256:565a021fd19419476b9362b05eeaa094178de64f8361e44468f9e9d7843901e1 \ + # via vcrpy +yarl==1.4.2 \ + --hash=sha256:0c2ab325d33f1b824734b3ef51d4d54a54e0e7a23d13b86974507602334c2cce \ + --hash=sha256:0ca2f395591bbd85ddd50a82eb1fde9c1066fafe888c5c7cc1d810cf03fd3cc6 \ + --hash=sha256:2098a4b4b9d75ee352807a95cdf5f10180db903bc5b7270715c6bbe2551f64ce \ + --hash=sha256:25e66e5e2007c7a39541ca13b559cd8ebc2ad8fe00ea94a2aad28a9b1e44e5ae \ + --hash=sha256:26d7c90cb04dee1665282a5d1a998defc1a9e012fdca0f33396f81508f49696d \ + --hash=sha256:308b98b0c8cd1dfef1a0311dc5e38ae8f9b58349226aa0533f15a16717ad702f \ + --hash=sha256:3ce3d4f7c6b69c4e4f0704b32eca8123b9c58ae91af740481aa57d7857b5e41b \ + --hash=sha256:58cd9c469eced558cd81aa3f484b2924e8897049e06889e8ff2510435b7ef74b \ + --hash=sha256:5b10eb0e7f044cf0b035112446b26a3a2946bca9d7d7edb5e54a2ad2f6652abb \ + --hash=sha256:6faa19d3824c21bcbfdfce5171e193c8b4ddafdf0ac3f129ccf0cdfcb083e462 \ + --hash=sha256:944494be42fa630134bf907714d40207e646fd5a94423c90d5b514f7b0713fea \ + --hash=sha256:a161de7e50224e8e3de6e184707476b5a989037dcb24292b391a3d66ff158e70 \ + --hash=sha256:a4844ebb2be14768f7994f2017f70aca39d658a96c786211be5ddbe1c68794c1 \ + --hash=sha256:c2b509ac3d4b988ae8769901c66345425e361d518aecbe4acbfc2567e416626a \ + --hash=sha256:c9959d49a77b0e07559e579f38b2f3711c2b8716b8410b320bf9713013215a1b \ + --hash=sha256:d8cdee92bc930d8b09d8bd2043cedd544d9c8bd7436a77678dd602467a993080 \ + --hash=sha256:e15199cdb423316e15f108f51249e44eb156ae5dba232cb73be555324a1d49c2 \ + # via vcrpy zipp==0.6.0 \ --hash=sha256:3718b1cbcd963c7d4c5511a8240812904164b7f381b647143a89d3b98f9bcd8e \ --hash=sha256:f06903e9f1f43b12d371004b4ac7b06ab39a44adc747266928ae6debfa7b3335 \ diff --git a/func_tests/test_client.py b/func_tests/test_client.py deleted file mode 100644 index 4895c58a58..0000000000 --- a/func_tests/test_client.py +++ /dev/null @@ -1,21 +0,0 @@ -from PyQt5.QtCore import Qt - - -from securedrop_client.gui.main import Window -from securedrop_client.gui.widgets import LoginDialog - - -def test_login(qtbot, mocker): - """ - We see an error if incomplete credentials are supplied to the login dialog. - """ - w = Window() - login_dialog = LoginDialog(w) - login_dialog.error_bar.set_message = mocker.MagicMock() - login_dialog.show() - qtbot.addWidget(login_dialog) - assert login_dialog.error_bar.error_status_bar.text() == "" - qtbot.keyClicks(login_dialog.username_field, "journalist") - qtbot.mouseClick(login_dialog.submit, Qt.LeftButton) - assert login_dialog.error_bar.set_message.call_count == 1 - assert login_dialog.error_bar.isVisible() diff --git a/func_tests/__init__.py b/tests/functional/__init__.py similarity index 100% rename from func_tests/__init__.py rename to tests/functional/__init__.py diff --git a/tests/functional/test_client.py b/tests/functional/test_client.py new file mode 100644 index 0000000000..fe523b55b0 --- /dev/null +++ b/tests/functional/test_client.py @@ -0,0 +1,107 @@ +import os +import tempfile +import json +import pytest + + +from sqlalchemy.orm.exc import NoResultFound + + +from PyQt5.QtCore import Qt + + +from securedrop_client.gui.main import Window +from securedrop_client.logic import Controller +from securedrop_client.config import Config +from securedrop_client.gui.widgets import LoginDialog +from securedrop_client.db import Base, make_session_maker, ReplySendStatus, ReplySendStatusCodes +from securedrop_client.utils import safe_mkdir + + +HOSTNAME = "http://localhost:8081/" +USERNAME = "journalist" +PASSWORD = "correct horse battery staple profanity oil chewy" +OTP = "783682" + + +def get_safe_tempdir(): + return tempfile.TemporaryDirectory() + + +def get_test_context(sdc_home, mocker): + """ + Returns a tuple containing a Window instance and a Controller instance that + have been correctly set up and isolated from any other instances of the + application to be run in the test suite. + """ + gui = Window() # The application's wind + # Create all app assets in a new temp directory and sub-directories. + safe_mkdir(os.path.join(sdc_home.name, "gpg")) + safe_mkdir(os.path.join(sdc_home.name, "data")) + # Configure and create the database. + session_maker = make_session_maker(sdc_home.name) + create_dev_data(sdc_home.name, session_maker) + # Create the controller. + controller = Controller(HOSTNAME, gui, session_maker, sdc_home.name, + False, False) + # Link the gui and controller together. + gui.controller = controller + # Et Voila... + with mocker.patch("securedrop_client.logic.sdclientapi") as mock_api: + return (gui, controller, mock_api) + + +def create_dev_data(sdc_home, session_maker): + """ + Based upon the functionality in the script, create_dev_data.py. This is + used to setup and configure the database and GPG keyring related metadata. + """ + session = session_maker() + Base.metadata.create_all(bind=session.get_bind()) + + with open(os.path.join(sdc_home, Config.CONFIG_NAME), 'w') as f: + f.write(json.dumps({ + 'journalist_key_fingerprint': '65A1B5FF195B56353CC63DFFCC40EF1228271441', + })) + + for reply_send_status in ReplySendStatusCodes: + try: + reply_status = session.query(ReplySendStatus).filter_by( + name=reply_send_status.value).one() + except NoResultFound: + reply_status = ReplySendStatus(reply_send_status.value) + session.add(reply_status) + session.commit() + + +def test_login_ensure_errors_displayed(qtbot, mocker): + """ + We see an error if incomplete credentials are supplied to the login dialog. + """ + w = Window() + login_dialog = LoginDialog(w) + login_dialog.show() + assert login_dialog.error_bar.error_status_bar.text() == "" + qtbot.keyClicks(login_dialog.username_field, "journalist") + qtbot.mouseClick(login_dialog.submit, Qt.LeftButton) + expected = "Please enter a username, password and two-factor code." + actual = login_dialog.error_bar.error_status_bar.text() + assert actual == expected + + +def test_login_as_journalist(qtbot, mocker): + """ + The app is visible if the user logs in with apparently correct credentials. + """ + tempdir = get_safe_tempdir() # Once out of scope, is deleted. + gui, controller, mock_api = get_test_context(tempdir, mocker) + login_dialog = LoginDialog(gui) + login_dialog.setup(controller) + gui.login_dialog = login_dialog + login_dialog.show() + assert login_dialog.error_bar.error_status_bar.text() == "" + qtbot.keyClicks(login_dialog.username_field, USERNAME) + qtbot.keyClicks(login_dialog.password_field, PASSWORD) + qtbot.keyClicks(login_dialog.tfa_field, OTP) + qtbot.mouseClick(login_dialog.submit, Qt.LeftButton) + assert gui.isVisible()