From f715a8d7de0d57f064041e570db304f8a29ddb33 Mon Sep 17 00:00:00 2001 From: Ihor Kalnytskyi Date: Wed, 8 May 2024 19:14:42 +0300 Subject: [PATCH 1/3] Reduce test matrix Do not run each test for each supported auth-type. Currently master runs each test for 'store', 'credential-store' and 'creds' auth types. There's no need to run each test with each auth type alias. This patch changes this by always running tests with 'store' auth type, and introducing a separate tests to verify that aliases works. --- tests/test_plugin.py | 151 +++++++++++++++++++++++-------------------- 1 file changed, 80 insertions(+), 71 deletions(-) diff --git a/tests/test_plugin.py b/tests/test_plugin.py index db2ff68..0d6fc54 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -98,13 +98,6 @@ def main(args): return main -@pytest.fixture(params=["store", "credential-store", "creds"]) -def creds_auth_type(request): - """All possible aliases.""" - - return request.param - - @responses.activate def test_basic_auth_plugin(httpie_run): """The plugin neither breaks nor overwrites existing auth plugins.""" @@ -119,7 +112,7 @@ def test_basic_auth_plugin(httpie_run): @responses.activate -def test_creds_auth_deactivated_by_default(httpie_run): +def test_store_auth_deactivated_by_default(httpie_run): """The plugin is deactivated by default.""" httpie_run(["http://example.com"]) @@ -132,7 +125,7 @@ def test_creds_auth_deactivated_by_default(httpie_run): @responses.activate -def test_creds_auth_basic(httpie_run, set_credentials, creds_auth_type): +def test_store_auth_basic(httpie_run, set_credentials): """The plugin works for HTTP basic auth.""" set_credentials( @@ -147,7 +140,7 @@ def test_creds_auth_basic(httpie_run, set_credentials, creds_auth_type): } ] ) - httpie_run(["-A", creds_auth_type, "http://example.com"]) + httpie_run(["-A", "store", "http://example.com"]) assert len(responses.calls) == 1 request = responses.calls[0].request @@ -157,7 +150,7 @@ def test_creds_auth_basic(httpie_run, set_credentials, creds_auth_type): @responses.activate -def test_creds_auth_basic_keychain(httpie_run, set_credentials, creds_auth_type, tmp_path): +def test_store_auth_basic_keychain(httpie_run, set_credentials, tmp_path): """The plugin retrieves secrets from keychain for HTTP basic auth.""" secrettxt = tmp_path.joinpath("secret.txt") @@ -178,7 +171,7 @@ def test_creds_auth_basic_keychain(httpie_run, set_credentials, creds_auth_type, } ] ) - httpie_run(["-A", creds_auth_type, "http://example.com"]) + httpie_run(["-A", "store", "http://example.com"]) assert len(responses.calls) == 1 request = responses.calls[0].request @@ -188,7 +181,7 @@ def test_creds_auth_basic_keychain(httpie_run, set_credentials, creds_auth_type, @responses.activate -def test_creds_auth_digest(httpie_run, set_credentials, creds_auth_type): +def test_store_auth_digest(httpie_run, set_credentials): """The plugin works for HTTP digest auth.""" responses.add( @@ -217,7 +210,7 @@ def test_creds_auth_digest(httpie_run, set_credentials, creds_auth_type): } ] ) - httpie_run(["-A", creds_auth_type, "http://example.com"]) + httpie_run(["-A", "store", "http://example.com"]) assert len(responses.calls) == 2 request = responses.calls[0].request @@ -248,7 +241,7 @@ def test_creds_auth_digest(httpie_run, set_credentials, creds_auth_type): @responses.activate -def test_creds_auth_token(httpie_run, set_credentials, creds_auth_type): +def test_store_auth_token(httpie_run, set_credentials): """The plugin works for HTTP token auth.""" set_credentials( @@ -262,7 +255,7 @@ def test_creds_auth_token(httpie_run, set_credentials, creds_auth_type): } ] ) - httpie_run(["-A", creds_auth_type, "http://example.com"]) + httpie_run(["-A", "store", "http://example.com"]) assert len(responses.calls) == 1 request = responses.calls[0].request @@ -272,7 +265,7 @@ def test_creds_auth_token(httpie_run, set_credentials, creds_auth_type): @responses.activate -def test_creds_auth_token_scheme(httpie_run, set_credentials, creds_auth_type): +def test_store_auth_token_scheme(httpie_run, set_credentials): """The plugin works for HTTP token auth with custom scheme.""" set_credentials( @@ -287,7 +280,7 @@ def test_creds_auth_token_scheme(httpie_run, set_credentials, creds_auth_type): } ] ) - httpie_run(["-A", creds_auth_type, "http://example.com"]) + httpie_run(["-A", "store", "http://example.com"]) assert len(responses.calls) == 1 request = responses.calls[0].request @@ -297,7 +290,7 @@ def test_creds_auth_token_scheme(httpie_run, set_credentials, creds_auth_type): @responses.activate -def test_creds_auth_token_keychain(httpie_run, set_credentials, creds_auth_type, tmp_path): +def test_store_auth_token_keychain(httpie_run, set_credentials, tmp_path): """The plugin retrieves secrets from keychain for HTTP token auth.""" secrettxt = tmp_path.joinpath("secret.txt") @@ -317,7 +310,7 @@ def test_creds_auth_token_keychain(httpie_run, set_credentials, creds_auth_type, } ] ) - httpie_run(["-A", creds_auth_type, "http://example.com"]) + httpie_run(["-A", "store", "http://example.com"]) assert len(responses.calls) == 1 request = responses.calls[0].request @@ -327,7 +320,7 @@ def test_creds_auth_token_keychain(httpie_run, set_credentials, creds_auth_type, @responses.activate -def test_creds_auth_header(httpie_run, set_credentials, creds_auth_type): +def test_store_auth_header(httpie_run, set_credentials): """The plugin works for HTTP header auth.""" set_credentials( @@ -342,7 +335,7 @@ def test_creds_auth_header(httpie_run, set_credentials, creds_auth_type): } ] ) - httpie_run(["-A", creds_auth_type, "http://example.com"]) + httpie_run(["-A", "store", "http://example.com"]) assert len(responses.calls) == 1 request = responses.calls[0].request @@ -352,7 +345,7 @@ def test_creds_auth_header(httpie_run, set_credentials, creds_auth_type): @responses.activate -def test_creds_auth_header_keychain(httpie_run, set_credentials, creds_auth_type, tmp_path): +def test_store_auth_header_keychain(httpie_run, set_credentials, tmp_path): """The plugin retrieves secrets from keychain for HTTP header auth.""" secrettxt = tmp_path.joinpath("secret.txt") @@ -373,7 +366,7 @@ def test_creds_auth_header_keychain(httpie_run, set_credentials, creds_auth_type } ] ) - httpie_run(["-A", creds_auth_type, "http://example.com"]) + httpie_run(["-A", "store", "http://example.com"]) assert len(responses.calls) == 1 request = responses.calls[0].request @@ -383,7 +376,7 @@ def test_creds_auth_header_keychain(httpie_run, set_credentials, creds_auth_type @responses.activate -def test_creds_auth_3rd_party_plugin(httpie_run, set_credentials, creds_auth_type): +def test_store_auth_3rd_party_plugin(httpie_run, set_credentials): """The plugin works for third-party auth plugin.""" set_credentials( @@ -400,7 +393,7 @@ def test_creds_auth_3rd_party_plugin(httpie_run, set_credentials, creds_auth_typ # The 'Date' request header is supplied to make sure that produced HMAC # is always the same. - httpie_run(["-A", creds_auth_type, "http://example.com", "Date: Wed, 08 May 2024 00:00:00 GMT"]) + httpie_run(["-A", "store", "http://example.com", "Date: Wed, 08 May 2024 00:00:00 GMT"]) assert len(responses.calls) == 1 request = responses.calls[0].request @@ -410,9 +403,7 @@ def test_creds_auth_3rd_party_plugin(httpie_run, set_credentials, creds_auth_typ @responses.activate -def test_creds_auth_3rd_party_plugin_keychain( - httpie_run, set_credentials, creds_auth_type, tmp_path -): +def test_store_auth_3rd_party_plugin_keychain(httpie_run, set_credentials, tmp_path): """The plugin retrieves secrets from keychain for third-party auth plugins.""" secrettxt = tmp_path.joinpath("secret.txt") @@ -435,7 +426,7 @@ def test_creds_auth_3rd_party_plugin_keychain( # The 'Date' request header is supplied to make sure that produced HMAC # is always the same. - httpie_run(["-A", creds_auth_type, "http://example.com", "Date: Wed, 08 May 2024 00:00:00 GMT"]) + httpie_run(["-A", "store", "http://example.com", "Date: Wed, 08 May 2024 00:00:00 GMT"]) assert len(responses.calls) == 1 request = responses.calls[0].request @@ -445,7 +436,7 @@ def test_creds_auth_3rd_party_plugin_keychain( @responses.activate -def test_creds_auth_multiple_token_header(httpie_run, set_credentials, creds_auth_type): +def test_store_auth_multiple_token_header(httpie_run, set_credentials): """The plugin works for multiple auths.""" set_credentials( @@ -470,7 +461,7 @@ def test_creds_auth_multiple_token_header(httpie_run, set_credentials, creds_aut } ] ) - httpie_run(["-A", creds_auth_type, "http://example.com"]) + httpie_run(["-A", "store", "http://example.com"]) assert len(responses.calls) == 1 request = responses.calls[0].request @@ -481,7 +472,7 @@ def test_creds_auth_multiple_token_header(httpie_run, set_credentials, creds_aut @responses.activate -def test_creds_auth_multiple_header_header(httpie_run, set_credentials, creds_auth_type): +def test_store_auth_multiple_header_header(httpie_run, set_credentials): """The plugin supports usage of the same auth provider twice.""" set_credentials( @@ -506,7 +497,7 @@ def test_creds_auth_multiple_header_header(httpie_run, set_credentials, creds_au } ] ) - httpie_run(["-A", creds_auth_type, "http://example.com"]) + httpie_run(["-A", "store", "http://example.com"]) assert len(responses.calls) == 1 request = responses.calls[0].request @@ -517,9 +508,7 @@ def test_creds_auth_multiple_header_header(httpie_run, set_credentials, creds_au @responses.activate -def test_creds_auth_multiple_token_header_keychain( - httpie_run, set_credentials, creds_auth_type, tmp_path -): +def test_store_auth_multiple_token_header_keychain(httpie_run, set_credentials, tmp_path): """The plugin retrieves secrets from keychains for combination of auths.""" tokentxt, secrettxt = tmp_path.joinpath("token.txt"), tmp_path.joinpath("secret.txt") @@ -554,7 +543,7 @@ def test_creds_auth_multiple_token_header_keychain( } ] ) - httpie_run(["-A", creds_auth_type, "http://example.com"]) + httpie_run(["-A", "store", "http://example.com"]) assert len(responses.calls) == 1 request = responses.calls[0].request @@ -636,13 +625,11 @@ def test_creds_auth_multiple_token_header_keychain( ), ], ) -def test_creds_auth_missing( - httpie_run, set_credentials, httpie_stderr, auth, error_message, creds_auth_type -): +def test_store_auth_missing(httpie_run, set_credentials, httpie_stderr, auth, error_message): """The plugin raises error on wrong parameters.""" set_credentials([{"url": "http://example.com", "auth": auth}]) - httpie_run(["-A", creds_auth_type, "http://example.com"]) + httpie_run(["-A", "store", "http://example.com"]) if _is_windows: # The error messages on Windows doesn't contain class names before @@ -708,9 +695,7 @@ def test_creds_auth_missing( ), ], ) -def test_creds_lookup_regexp( - httpie_run, set_credentials, regexp, url, normalized_url, creds_auth_type -): +def test_store_lookup_regexp(httpie_run, set_credentials, regexp, url, normalized_url): """The plugin uses pattern matching to find credentials.""" set_credentials( @@ -724,7 +709,7 @@ def test_creds_lookup_regexp( } ] ) - httpie_run(["-A", creds_auth_type, url]) + httpie_run(["-A", "store", url]) assert len(responses.calls) == 1 request = responses.calls[0].request @@ -734,7 +719,7 @@ def test_creds_lookup_regexp( @responses.activate -def test_creds_lookup_1st_matched_wins(httpie_run, set_credentials, creds_auth_type): +def test_store_lookup_1st_matched_wins(httpie_run, set_credentials): """The plugin uses auth of first matched credential entry.""" set_credentials( @@ -756,7 +741,7 @@ def test_creds_lookup_1st_matched_wins(httpie_run, set_credentials, creds_auth_t }, ] ) - httpie_run(["-A", creds_auth_type, "https://yoda.ua/v2/the-force"]) + httpie_run(["-A", "store", "https://yoda.ua/v2/the-force"]) assert len(responses.calls) == 1 request = responses.calls[0].request @@ -766,7 +751,7 @@ def test_creds_lookup_1st_matched_wins(httpie_run, set_credentials, creds_auth_t @responses.activate -def test_creds_lookup_many_credentials(httpie_run, set_credentials, creds_auth_type): +def test_store_lookup_many_credentials(httpie_run, set_credentials): """The plugin works with many URLs and credentials.""" responses.add(responses.GET, "https://yoda.ua/about/", status=200) @@ -791,8 +776,8 @@ def test_creds_lookup_many_credentials(httpie_run, set_credentials, creds_auth_t }, ] ) - httpie_run(["-A", creds_auth_type, "https://yoda.ua/about/"]) - httpie_run(["-A", creds_auth_type, "http://skywalker.com"]) + httpie_run(["-A", "store", "https://yoda.ua/about/"]) + httpie_run(["-A", "store", "http://skywalker.com"]) assert len(responses.calls) == 2 request = responses.calls[0].request @@ -819,7 +804,7 @@ def test_creds_lookup_many_credentials(httpie_run, set_credentials, creds_auth_t ), ], ) -def test_creds_lookup_error(httpie_run, set_credentials, regexp, url, httpie_stderr): +def test_store_lookup_error(httpie_run, set_credentials, regexp, url, httpie_stderr): """The plugin raises error if no credentials found.""" set_credentials( @@ -842,7 +827,7 @@ def test_creds_lookup_error(httpie_run, set_credentials, regexp, url, httpie_std @responses.activate -def test_creds_lookup_by_id(httpie_run, set_credentials): +def test_store_lookup_by_id(httpie_run, set_credentials): """The plugin uses a given credential ID as a hint for 2+ matches.""" set_credentials( @@ -872,7 +857,7 @@ def test_creds_lookup_by_id(httpie_run, set_credentials): @responses.activate -def test_creds_lookup_by_id_error(httpie_run, set_credentials, httpie_stderr, creds_auth_type): +def test_store_lookup_by_id_error(httpie_run, set_credentials, httpie_stderr): """The plugin raises error if no credentials found.""" set_credentials( @@ -890,7 +875,7 @@ def test_creds_lookup_by_id_error(httpie_run, set_credentials, httpie_stderr, cr ] ) - httpie_run(["-A", creds_auth_type, "-a", "vader", "https://yoda.ua/about/"]) + httpie_run(["-A", "store", "-a", "vader", "https://yoda.ua/about/"]) assert len(responses.calls) == 0 assert httpie_stderr.getvalue().strip() == ( "http: error: LookupError: No credentials found for a given URL: " @@ -909,7 +894,7 @@ def test_creds_lookup_by_id_error(httpie_run, set_credentials, httpie_stderr, cr pytest.param(0o400, id="0400"), ], ) -def test_creds_permissions_safe(httpie_run, set_credentials, mode, creds_auth_type): +def test_store_permissions_safe(httpie_run, set_credentials, mode): """The plugin doesn't complain if credentials file has safe permissions.""" set_credentials( @@ -925,7 +910,7 @@ def test_creds_permissions_safe(httpie_run, set_credentials, mode, creds_auth_ty ], mode=mode, ) - httpie_run(["-A", creds_auth_type, "http://example.com"]) + httpie_run(["-A", "store", "http://example.com"]) assert len(responses.calls) == 1 request = responses.calls[0].request @@ -955,18 +940,17 @@ def test_creds_permissions_safe(httpie_run, set_credentials, mode, creds_auth_ty pytest.param(0o610, id="0610"), ], ) -def test_creds_permissions_unsafe( +def test_store_permissions_unsafe( httpie_run, set_credentials, mode, httpie_stderr, credentials_file, - creds_auth_type, ): """The plugin complains if credentials file has unsafe permissions.""" set_credentials([{"url": "http://example.com", "auth": {}}], mode=mode) - httpie_run(["-A", creds_auth_type, "http://example.com"]) + httpie_run(["-A", "store", "http://example.com"]) assert httpie_stderr.getvalue().strip() == ( f"http: error: PermissionError: Permissions '{mode:04o}' for " @@ -985,18 +969,17 @@ def test_creds_permissions_unsafe( pytest.param(0o100, id="0100"), ], ) -def test_creds_permissions_not_enough( +def test_store_permissions_not_enough( httpie_run, set_credentials, mode, httpie_stderr, credentials_file, - creds_auth_type, ): """The plugin complains if credentials file has unsafe permissions.""" set_credentials([{"url": "http://example.com", "auth": {}}], mode=mode) - httpie_run(["-A", creds_auth_type, "http://example.com"]) + httpie_run(["-A", "store", "http://example.com"]) assert httpie_stderr.getvalue().strip() == ( f"http: error: PermissionError: Permissions '{mode:04o}' for " @@ -1006,10 +989,10 @@ def test_creds_permissions_not_enough( @responses.activate -def test_creds_auth_no_database(httpie_run, credentials_file, httpie_stderr, creds_auth_type): +def test_store_auth_no_database(httpie_run, credentials_file, httpie_stderr): """The plugin raises error if credentials file does not exist.""" - httpie_run(["-A", creds_auth_type, "http://example.com"]) + httpie_run(["-A", "store", "http://example.com"]) assert len(responses.calls) == 0 assert httpie_stderr.getvalue().strip() == ( @@ -1045,11 +1028,11 @@ def test_creds_auth_no_database(httpie_run, credentials_file, httpie_stderr, cre ), ], ) -def test_creds_auth_header_value_illegal_characters( - httpie_run, set_credentials, httpie_stderr, creds_auth_type, auth, error +def test_store_auth_header_value_illegal_characters( + httpie_run, set_credentials, httpie_stderr, auth, error ): set_credentials([{"url": "http://example.com", "auth": auth}]) - httpie_run(["-A", creds_auth_type, "http://example.com"]) + httpie_run(["-A", "store", "http://example.com"]) assert len(responses.calls) == 0 assert httpie_stderr.getvalue().strip() == error @@ -1075,11 +1058,37 @@ def test_creds_auth_header_value_illegal_characters( ), ], ) -def test_creds_auth_header_name_illegal_characters( - httpie_run, set_credentials, httpie_stderr, creds_auth_type, auth, error +def test_store_auth_header_name_illegal_characters( + httpie_run, set_credentials, httpie_stderr, auth, error ): set_credentials([{"url": "http://example.com", "auth": auth}]) - httpie_run(["-A", creds_auth_type, "http://example.com"]) + httpie_run(["-A", "store", "http://example.com"]) assert len(responses.calls) == 0 assert httpie_stderr.getvalue().strip() == error + + +@responses.activate +@pytest.mark.parametrize("auth_type", ["store", "credential-store", "creds"]) +def test_auth_type_aliases(httpie_run, set_credentials, auth_type): + """The plugin can be invoked via 'creds' alias.""" + + set_credentials( + [ + { + "url": "http://example.com", + "auth": { + "provider": "basic", + "username": "user", + "password": "p@ss", + }, + } + ] + ) + httpie_run(["-A", auth_type, "http://example.com"]) + + assert len(responses.calls) == 1 + request = responses.calls[0].request + + assert request.url == "http://example.com/" + assert request.headers["Authorization"] == b"Basic dXNlcjpwQHNz" From 3e58b10e6fae75911cd4b729aa5e4a256ecb6462 Mon Sep 17 00:00:00 2001 From: Ihor Kalnytskyi Date: Wed, 8 May 2024 21:15:19 +0300 Subject: [PATCH 2/3] Add type annotations to tests Even though type annotations are nasty, they do bring one significant benefit - the code completion in text editors becomes much better. This patch adds type annotations to python modules in the 'tests' directory. --- pyproject.toml | 3 +- tests/conftest.py | 7 +- tests/test_keychain_password_store.py | 17 +- tests/test_keychain_shell.py | 20 ++- tests/test_keychain_system.py | 36 +++- tests/test_plugin.py | 247 +++++++++++++++++--------- 6 files changed, 224 insertions(+), 106 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 395d819..03ba7e6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,9 +44,10 @@ target-version = "py38" [tool.ruff.lint] select = ["ALL"] -ignore = ["ANN", "D", "PTH", "PLR", "PT005", "ISC001", "INP001", "S603", "S607", "COM812"] +ignore = ["D", "PTH", "PLR", "PT005", "ISC001", "INP001", "S603", "S607", "COM812", "FA100", "ANN101"] [tool.ruff.lint.per-file-ignores] +"src/*" = ["ANN"] "src/httpie_credential_store/_keychain.py" = ["S602"] "tests/*" = ["S101", "INP001"] diff --git a/tests/conftest.py b/tests/conftest.py index bb63428..4096426 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,8 +1,13 @@ +import pathlib +import typing + import pytest @pytest.fixture(scope="session", autouse=True) -def _httpie_config_dir(tmp_path_factory: pytest.TempPathFactory): +def _httpie_config_dir( + tmp_path_factory: pytest.TempPathFactory, +) -> typing.Generator[pathlib.Path, None, None]: """Set path to HTTPie configuration directory.""" # HTTPie can optionally read a path to configuration directory from diff --git a/tests/test_keychain_password_store.py b/tests/test_keychain_password_store.py index 195a181..609c976 100644 --- a/tests/test_keychain_password_store.py +++ b/tests/test_keychain_password_store.py @@ -7,10 +7,15 @@ import sys import tempfile import textwrap +import typing import pytest +if typing.TYPE_CHECKING: + from httpie_credential_store._keychain import PasswordStoreKeychain + + _is_macos = sys.platform == "darwin" @@ -29,13 +34,13 @@ # override 'tmp_path' fixture to return much shorter path to a temporary # directory. @pytest.fixture() - def tmp_path(): + def tmp_path() -> typing.Generator[pathlib.Path, None, None]: with tempfile.TemporaryDirectory() as path: yield pathlib.Path(path) @pytest.fixture() -def gpg_key_id(monkeypatch, tmp_path): +def gpg_key_id(monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path) -> str: """Return a Key ID of just generated GPG key.""" gpghome = tmp_path.joinpath(".gnupg") @@ -68,7 +73,7 @@ def gpg_key_id(monkeypatch, tmp_path): @pytest.fixture(autouse=True) -def password_store_dir(monkeypatch, tmp_path): +def password_store_dir(monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path) -> pathlib.Path: """Set password-store home directory to a temporary one.""" passstore = tmp_path.joinpath(".password-store") @@ -77,7 +82,7 @@ def password_store_dir(monkeypatch, tmp_path): @pytest.fixture() -def testkeychain(): +def testkeychain() -> "PasswordStoreKeychain": """Keychain instance under test.""" # For the same reasons as in tests/test_plugin.py, all imports that trigger @@ -88,7 +93,7 @@ def testkeychain(): return _keychain.PasswordStoreKeychain() -def test_secret_retrieved(testkeychain, gpg_key_id): +def test_secret_retrieved(testkeychain: "PasswordStoreKeychain", gpg_key_id: str) -> None: """The keychain returns stored secret, no bullshit.""" subprocess.check_call(["pass", "init", gpg_key_id]) @@ -97,7 +102,7 @@ def test_secret_retrieved(testkeychain, gpg_key_id): assert testkeychain.get(name="service/user") == "f00b@r" -def test_secret_not_found(testkeychain): +def test_secret_not_found(testkeychain: "PasswordStoreKeychain") -> None: """LookupError is raised when no secrets are found in the keychain.""" with pytest.raises(LookupError) as excinfo: diff --git a/tests/test_keychain_shell.py b/tests/test_keychain_shell.py index bef4eeb..ba7460a 100644 --- a/tests/test_keychain_shell.py +++ b/tests/test_keychain_shell.py @@ -1,12 +1,18 @@ """Tests shell keychain provider.""" import os +import pathlib +import typing import pytest +if typing.TYPE_CHECKING: + from httpie_credential_store._keychain import ShellKeychain + + @pytest.fixture() -def testkeychain(): +def testkeychain() -> "ShellKeychain": """Keychain instance under test.""" # For the same reasons as in tests/test_plugin.py, all imports that trigger @@ -17,7 +23,7 @@ def testkeychain(): return _keychain.ShellKeychain() -def test_secret_retrieved(testkeychain, tmp_path): +def test_secret_retrieved(testkeychain: "ShellKeychain", tmp_path: pathlib.Path) -> None: """The keychain returns stored secret, no bullshit.""" secrettxt = tmp_path.joinpath("secret.txt") @@ -25,7 +31,7 @@ def test_secret_retrieved(testkeychain, tmp_path): assert testkeychain.get(command=f"cat {secrettxt}") == "p@ss" -def test_secret_retrieved_pipe(testkeychain, tmp_path): +def test_secret_retrieved_pipe(testkeychain: "ShellKeychain", tmp_path: pathlib.Path) -> None: """The keychain returns stored secret even when pipes are used.""" secrettxt = tmp_path.joinpath("secret.txt") @@ -35,7 +41,7 @@ def test_secret_retrieved_pipe(testkeychain, tmp_path): assert testkeychain.get(command=command) == "p@ss" -def test_secret_not_found(testkeychain, tmp_path): +def test_secret_not_found(testkeychain: "ShellKeychain", tmp_path: pathlib.Path) -> None: """LookupError is raised when no secrets are found in the keychain.""" secrettxt = tmp_path.joinpath("secret.txt") @@ -49,6 +55,10 @@ def test_secret_not_found(testkeychain, tmp_path): @pytest.mark.parametrize(("args", "kwargs"), [pytest.param(["echo p@ss"], {}, id="args")]) -def test_keywords_only_arguments(testkeychain, args, kwargs): +def test_keywords_only_arguments( + testkeychain: "ShellKeychain", + args: typing.List[str], + kwargs: typing.Mapping[str, str], +) -> None: with pytest.raises(TypeError): testkeychain.get(*args, **kwargs) diff --git a/tests/test_keychain_system.py b/tests/test_keychain_system.py index ebf9356..397cbe9 100644 --- a/tests/test_keychain_system.py +++ b/tests/test_keychain_system.py @@ -1,26 +1,36 @@ """Tests system keychain provider.""" +import typing + import keyring +import keyring.backend +import keyring.compat import pytest +if typing.TYPE_CHECKING: + from httpie_credential_store._keychain import SystemKeychain + + class _InmemoryKeyring(keyring.backend.KeyringBackend): """Keyring backend that stores secrets in-memory.""" - priority = 1 + @keyring.compat.properties.classproperty + def priority(self) -> float: + return 1.0 - def __init__(self): + def __init__(self) -> None: self._keyring = {} - def get_password(self, service, username): + def get_password(self, service: str, username: str) -> typing.Optional[str]: return self._keyring.get((service, username)) - def set_password(self, service, username, password): + def set_password(self, service: str, username: str, password: str) -> None: self._keyring[(service, username)] = password @pytest.fixture(autouse=True) -def keyring_backend(): +def keyring_backend() -> typing.Generator[keyring.backend.KeyringBackend, None, None]: """Temporary set in-memory keyring as current backend.""" prev_backend = keyring.get_keyring() @@ -30,7 +40,7 @@ def keyring_backend(): @pytest.fixture() -def testkeychain(): +def testkeychain() -> "SystemKeychain": """Keychain instance under test.""" # For the same reasons as in tests/test_plugin.py, all imports that trigger @@ -41,14 +51,17 @@ def testkeychain(): return _keychain.SystemKeychain() -def test_secret_retrieved(testkeychain, keyring_backend): +def test_secret_retrieved( + testkeychain: "SystemKeychain", + keyring_backend: keyring.backend.KeyringBackend, +) -> None: """The keychain returns stored secret, no bullshit.""" keyring_backend.set_password("testsvc", "testuser", "p@ss") assert testkeychain.get(service="testsvc", username="testuser") == "p@ss" -def test_secret_not_found(testkeychain): +def test_secret_not_found(testkeychain: "SystemKeychain") -> None: """LookupError is raised when no secrets are found in the keychain.""" with pytest.raises(LookupError) as excinfo: @@ -66,7 +79,12 @@ def test_secret_not_found(testkeychain): pytest.param(["testsvc"], {"username": "testuser"}, id="args-kwargs"), ], ) -def test_keywords_only_arguments(testkeychain, keyring_backend, args, kwargs): +def test_keywords_only_arguments( + testkeychain: "SystemKeychain", + keyring_backend: keyring.backend.KeyringBackend, + args: typing.List[str], + kwargs: typing.Mapping[str, str], +) -> None: keyring_backend.set_password("testsvc", "testuser", "p@ss") with pytest.raises(TypeError): diff --git a/tests/test_plugin.py b/tests/test_plugin.py index 0d6fc54..ad2ba31 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -2,9 +2,10 @@ import io import json -import os +import pathlib import re import sys +import typing from urllib.request import parse_http_list, parse_keqv_list @@ -15,13 +16,18 @@ _is_windows = sys.platform == "win32" +HttpieRunT = typing.Callable[[typing.List[typing.Union[str, bytes]]], int] +StoreSetT = typing.Callable[..., None] + + class _DigestAuthHeader: """Assert that a given Authorization header has expected digest parameters.""" - def __init__(self, parameters): + def __init__(self, parameters: typing.Mapping[str, typing.Any]) -> None: self._parameters = parameters - def __eq__(self, authorization_header_value): + def __eq__(self, authorization_header_value: object) -> bool: + assert isinstance(authorization_header_value, str) auth_type, auth_value = authorization_header_value.split(maxsplit=1) assert auth_type.lower() == "digest" assert parse_keqv_list(parse_http_list(auth_value)) == self._parameters @@ -31,18 +37,21 @@ def __eq__(self, authorization_header_value): class _RegExp: """Assert that a given string meets some expectations.""" - def __init__(self, pattern, flags=0): + def __init__(self, pattern: str, flags: int = 0) -> None: self._regex = re.compile(pattern, flags) - def __eq__(self, actual): + def __eq__(self, actual: object) -> bool: + assert isinstance(actual, str) return bool(self._regex.match(actual)) - def __repr__(self): + def __repr__(self) -> str: return self._regex.pattern @pytest.fixture(autouse=True) -def httpie_config_dir(_httpie_config_dir): +def httpie_config_dir( + _httpie_config_dir: pathlib.Path, +) -> typing.Generator[pathlib.Path, None, None]: """Return a path to HTTPie configuration directory.""" yield _httpie_config_dir @@ -55,36 +64,35 @@ def httpie_config_dir(_httpie_config_dir): @pytest.fixture() -def credentials_file(httpie_config_dir): +def credentials_file(httpie_config_dir: pathlib.Path) -> pathlib.Path: """Return a path to credentials file.""" - return os.path.join(httpie_config_dir, "credentials.json") + return httpie_config_dir / "credentials.json" @pytest.fixture() -def set_credentials(credentials_file): +def store_set(credentials_file: pathlib.Path) -> StoreSetT: """Render given credentials to credentials.json.""" - def render(credentials, mode=0o600): - with open(credentials_file, "w", encoding="UTF-8") as f: - f.write(json.dumps(credentials, indent=4)) - os.chmod(credentials_file, mode) + def render(credentials: typing.Union[typing.Mapping, typing.List], mode: int = 0o600) -> None: + credentials_file.write_text(json.dumps(credentials, indent=4)) + credentials_file.chmod(mode) return render @pytest.fixture() -def httpie_stderr(): +def httpie_stderr() -> io.StringIO: """Return captured standard error stream of HTTPie.""" return io.StringIO() @pytest.fixture() -def httpie_run(httpie_stderr): +def httpie_run(httpie_stderr: io.StringIO) -> HttpieRunT: """Run HTTPie from within this process.""" - def main(args): + def main(args: typing.List[typing.Union[str, bytes]]) -> int: # Imports of HTTPie internals must be local because otherwise they # won't take into account patched HTTPIE_CONFIG_DIR environment # variable. @@ -99,7 +107,7 @@ def main(args): @responses.activate -def test_basic_auth_plugin(httpie_run): +def test_basic_auth_plugin(httpie_run: HttpieRunT) -> None: """The plugin neither breaks nor overwrites existing auth plugins.""" httpie_run(["-A", "basic", "-a", "user:p@ss", "http://example.com"]) @@ -112,7 +120,7 @@ def test_basic_auth_plugin(httpie_run): @responses.activate -def test_store_auth_deactivated_by_default(httpie_run): +def test_store_auth_deactivated_by_default(httpie_run: HttpieRunT) -> None: """The plugin is deactivated by default.""" httpie_run(["http://example.com"]) @@ -125,10 +133,10 @@ def test_store_auth_deactivated_by_default(httpie_run): @responses.activate -def test_store_auth_basic(httpie_run, set_credentials): +def test_store_auth_basic(httpie_run: HttpieRunT, store_set: StoreSetT) -> None: """The plugin works for HTTP basic auth.""" - set_credentials( + store_set( [ { "url": "http://example.com", @@ -150,13 +158,17 @@ def test_store_auth_basic(httpie_run, set_credentials): @responses.activate -def test_store_auth_basic_keychain(httpie_run, set_credentials, tmp_path): +def test_store_auth_basic_keychain( + httpie_run: HttpieRunT, + store_set: StoreSetT, + tmp_path: pathlib.Path, +) -> None: """The plugin retrieves secrets from keychain for HTTP basic auth.""" secrettxt = tmp_path.joinpath("secret.txt") secrettxt.write_text("p@ss", encoding="UTF-8") - set_credentials( + store_set( [ { "url": "http://example.com", @@ -181,7 +193,7 @@ def test_store_auth_basic_keychain(httpie_run, set_credentials, tmp_path): @responses.activate -def test_store_auth_digest(httpie_run, set_credentials): +def test_store_auth_digest(httpie_run: HttpieRunT, store_set: StoreSetT) -> None: """The plugin works for HTTP digest auth.""" responses.add( @@ -198,7 +210,7 @@ def test_store_auth_digest(httpie_run, set_credentials): }, ) - set_credentials( + store_set( [ { "url": "http://example.com", @@ -241,10 +253,10 @@ def test_store_auth_digest(httpie_run, set_credentials): @responses.activate -def test_store_auth_token(httpie_run, set_credentials): +def test_store_auth_token(httpie_run: HttpieRunT, store_set: StoreSetT) -> None: """The plugin works for HTTP token auth.""" - set_credentials( + store_set( [ { "url": "http://example.com", @@ -265,10 +277,10 @@ def test_store_auth_token(httpie_run, set_credentials): @responses.activate -def test_store_auth_token_scheme(httpie_run, set_credentials): +def test_store_auth_token_scheme(httpie_run: HttpieRunT, store_set: StoreSetT) -> None: """The plugin works for HTTP token auth with custom scheme.""" - set_credentials( + store_set( [ { "url": "http://example.com", @@ -290,13 +302,17 @@ def test_store_auth_token_scheme(httpie_run, set_credentials): @responses.activate -def test_store_auth_token_keychain(httpie_run, set_credentials, tmp_path): +def test_store_auth_token_keychain( + httpie_run: HttpieRunT, + store_set: StoreSetT, + tmp_path: pathlib.Path, +) -> None: """The plugin retrieves secrets from keychain for HTTP token auth.""" secrettxt = tmp_path.joinpath("secret.txt") secrettxt.write_text("token-can-be-anything", encoding="UTF-8") - set_credentials( + store_set( [ { "url": "http://example.com", @@ -320,10 +336,10 @@ def test_store_auth_token_keychain(httpie_run, set_credentials, tmp_path): @responses.activate -def test_store_auth_header(httpie_run, set_credentials): +def test_store_auth_header(httpie_run: HttpieRunT, store_set: StoreSetT) -> None: """The plugin works for HTTP header auth.""" - set_credentials( + store_set( [ { "url": "http://example.com", @@ -345,13 +361,17 @@ def test_store_auth_header(httpie_run, set_credentials): @responses.activate -def test_store_auth_header_keychain(httpie_run, set_credentials, tmp_path): +def test_store_auth_header_keychain( + httpie_run: HttpieRunT, + store_set: StoreSetT, + tmp_path: pathlib.Path, +) -> None: """The plugin retrieves secrets from keychain for HTTP header auth.""" secrettxt = tmp_path.joinpath("secret.txt") secrettxt.write_text("value-can-be-anything", encoding="UTF-8") - set_credentials( + store_set( [ { "url": "http://example.com", @@ -376,10 +396,13 @@ def test_store_auth_header_keychain(httpie_run, set_credentials, tmp_path): @responses.activate -def test_store_auth_3rd_party_plugin(httpie_run, set_credentials): +def test_store_auth_3rd_party_plugin( + httpie_run: HttpieRunT, + store_set: StoreSetT, +) -> None: """The plugin works for third-party auth plugin.""" - set_credentials( + store_set( [ { "url": "http://example.com", @@ -403,13 +426,17 @@ def test_store_auth_3rd_party_plugin(httpie_run, set_credentials): @responses.activate -def test_store_auth_3rd_party_plugin_keychain(httpie_run, set_credentials, tmp_path): +def test_store_auth_3rd_party_plugin_keychain( + httpie_run: HttpieRunT, + store_set: StoreSetT, + tmp_path: pathlib.Path, +) -> None: """The plugin retrieves secrets from keychain for third-party auth plugins.""" secrettxt = tmp_path.joinpath("secret.txt") secrettxt.write_text("secret:rice", encoding="UTF-8") - set_credentials( + store_set( [ { "url": "http://example.com", @@ -436,10 +463,13 @@ def test_store_auth_3rd_party_plugin_keychain(httpie_run, set_credentials, tmp_p @responses.activate -def test_store_auth_multiple_token_header(httpie_run, set_credentials): +def test_store_auth_multiple_token_header( + httpie_run: HttpieRunT, + store_set: StoreSetT, +) -> None: """The plugin works for multiple auths.""" - set_credentials( + store_set( [ { "url": "http://example.com", @@ -472,10 +502,13 @@ def test_store_auth_multiple_token_header(httpie_run, set_credentials): @responses.activate -def test_store_auth_multiple_header_header(httpie_run, set_credentials): +def test_store_auth_multiple_header_header( + httpie_run: HttpieRunT, + store_set: StoreSetT, +) -> None: """The plugin supports usage of the same auth provider twice.""" - set_credentials( + store_set( [ { "url": "http://example.com", @@ -508,14 +541,18 @@ def test_store_auth_multiple_header_header(httpie_run, set_credentials): @responses.activate -def test_store_auth_multiple_token_header_keychain(httpie_run, set_credentials, tmp_path): +def test_store_auth_multiple_token_header_keychain( + httpie_run: HttpieRunT, + store_set: StoreSetT, + tmp_path: pathlib.Path, +) -> None: """The plugin retrieves secrets from keychains for combination of auths.""" tokentxt, secrettxt = tmp_path.joinpath("token.txt"), tmp_path.joinpath("secret.txt") tokentxt.write_text("token-can-be-anything", encoding="UTF-8") secrettxt.write_text("secret-can-be-anything", encoding="UTF-8") - set_credentials( + store_set( [ { "url": "http://example.com", @@ -625,10 +662,16 @@ def test_store_auth_multiple_token_header_keychain(httpie_run, set_credentials, ), ], ) -def test_store_auth_missing(httpie_run, set_credentials, httpie_stderr, auth, error_message): +def test_store_auth_missing( + httpie_run: HttpieRunT, + store_set: StoreSetT, + httpie_stderr: io.StringIO, + auth: typing.Mapping[str, str], + error_message: str, +) -> None: """The plugin raises error on wrong parameters.""" - set_credentials([{"url": "http://example.com", "auth": auth}]) + store_set([{"url": "http://example.com", "auth": auth}]) httpie_run(["-A", "store", "http://example.com"]) if _is_windows: @@ -695,10 +738,16 @@ def test_store_auth_missing(httpie_run, set_credentials, httpie_stderr, auth, er ), ], ) -def test_store_lookup_regexp(httpie_run, set_credentials, regexp, url, normalized_url): +def test_store_lookup_regexp( + httpie_run: HttpieRunT, + store_set: StoreSetT, + regexp: str, + url: str, + normalized_url: str, +) -> None: """The plugin uses pattern matching to find credentials.""" - set_credentials( + store_set( [ { "url": regexp, @@ -719,10 +768,10 @@ def test_store_lookup_regexp(httpie_run, set_credentials, regexp, url, normalize @responses.activate -def test_store_lookup_1st_matched_wins(httpie_run, set_credentials): +def test_store_lookup_1st_matched_wins(httpie_run: HttpieRunT, store_set: StoreSetT) -> None: """The plugin uses auth of first matched credential entry.""" - set_credentials( + store_set( [ { "url": "yoda.ua", @@ -751,13 +800,13 @@ def test_store_lookup_1st_matched_wins(httpie_run, set_credentials): @responses.activate -def test_store_lookup_many_credentials(httpie_run, set_credentials): +def test_store_lookup_many_credentials(httpie_run: HttpieRunT, store_set: StoreSetT) -> None: """The plugin works with many URLs and credentials.""" responses.add(responses.GET, "https://yoda.ua/about/", status=200) responses.add(responses.GET, "http://skywalker.com", status=200) - set_credentials( + store_set( [ { "url": "yoda.ua", @@ -804,10 +853,16 @@ def test_store_lookup_many_credentials(httpie_run, set_credentials): ), ], ) -def test_store_lookup_error(httpie_run, set_credentials, regexp, url, httpie_stderr): +def test_store_lookup_error( + httpie_run: HttpieRunT, + store_set: StoreSetT, + regexp: str, + url: str, + httpie_stderr: io.StringIO, +) -> None: """The plugin raises error if no credentials found.""" - set_credentials( + store_set( [ { "url": regexp, @@ -827,10 +882,10 @@ def test_store_lookup_error(httpie_run, set_credentials, regexp, url, httpie_std @responses.activate -def test_store_lookup_by_id(httpie_run, set_credentials): +def test_store_lookup_by_id(httpie_run: HttpieRunT, store_set: StoreSetT) -> None: """The plugin uses a given credential ID as a hint for 2+ matches.""" - set_credentials( + store_set( [ { "url": "yoda.ua", @@ -857,10 +912,14 @@ def test_store_lookup_by_id(httpie_run, set_credentials): @responses.activate -def test_store_lookup_by_id_error(httpie_run, set_credentials, httpie_stderr): +def test_store_lookup_by_id_error( + httpie_run: HttpieRunT, + store_set: StoreSetT, + httpie_stderr: io.StringIO, +) -> None: """The plugin raises error if no credentials found.""" - set_credentials( + store_set( [ { "id": "yoda", @@ -894,10 +953,14 @@ def test_store_lookup_by_id_error(httpie_run, set_credentials, httpie_stderr): pytest.param(0o400, id="0400"), ], ) -def test_store_permissions_safe(httpie_run, set_credentials, mode): +def test_store_permissions_safe( + httpie_run: HttpieRunT, + store_set: StoreSetT, + mode: int, +) -> None: """The plugin doesn't complain if credentials file has safe permissions.""" - set_credentials( + store_set( [ { "url": "http://example.com", @@ -941,15 +1004,15 @@ def test_store_permissions_safe(httpie_run, set_credentials, mode): ], ) def test_store_permissions_unsafe( - httpie_run, - set_credentials, - mode, - httpie_stderr, - credentials_file, -): + httpie_run: HttpieRunT, + store_set: StoreSetT, + mode: int, + httpie_stderr: io.StringIO, + credentials_file: pathlib.Path, +) -> None: """The plugin complains if credentials file has unsafe permissions.""" - set_credentials([{"url": "http://example.com", "auth": {}}], mode=mode) + store_set([{"url": "http://example.com", "auth": {}}], mode=mode) httpie_run(["-A", "store", "http://example.com"]) assert httpie_stderr.getvalue().strip() == ( @@ -970,15 +1033,15 @@ def test_store_permissions_unsafe( ], ) def test_store_permissions_not_enough( - httpie_run, - set_credentials, - mode, - httpie_stderr, - credentials_file, -): + httpie_run: HttpieRunT, + store_set: StoreSetT, + mode: int, + httpie_stderr: io.StringIO, + credentials_file: pathlib.Path, +) -> None: """The plugin complains if credentials file has unsafe permissions.""" - set_credentials([{"url": "http://example.com", "auth": {}}], mode=mode) + store_set([{"url": "http://example.com", "auth": {}}], mode=mode) httpie_run(["-A", "store", "http://example.com"]) assert httpie_stderr.getvalue().strip() == ( @@ -989,7 +1052,11 @@ def test_store_permissions_not_enough( @responses.activate -def test_store_auth_no_database(httpie_run, credentials_file, httpie_stderr): +def test_store_auth_no_database( + httpie_run: HttpieRunT, + credentials_file: pathlib.Path, + httpie_stderr: io.StringIO, +) -> None: """The plugin raises error if credentials file does not exist.""" httpie_run(["-A", "store", "http://example.com"]) @@ -1029,9 +1096,13 @@ def test_store_auth_no_database(httpie_run, credentials_file, httpie_stderr): ], ) def test_store_auth_header_value_illegal_characters( - httpie_run, set_credentials, httpie_stderr, auth, error -): - set_credentials([{"url": "http://example.com", "auth": auth}]) + httpie_run: HttpieRunT, + store_set: StoreSetT, + httpie_stderr: io.StringIO, + auth: typing.Mapping[str, str], + error: str, +) -> None: + store_set([{"url": "http://example.com", "auth": auth}]) httpie_run(["-A", "store", "http://example.com"]) assert len(responses.calls) == 0 @@ -1059,9 +1130,13 @@ def test_store_auth_header_value_illegal_characters( ], ) def test_store_auth_header_name_illegal_characters( - httpie_run, set_credentials, httpie_stderr, auth, error -): - set_credentials([{"url": "http://example.com", "auth": auth}]) + httpie_run: HttpieRunT, + store_set: StoreSetT, + httpie_stderr: io.StringIO, + auth: typing.Mapping[str, str], + error: str, +) -> None: + store_set([{"url": "http://example.com", "auth": auth}]) httpie_run(["-A", "store", "http://example.com"]) assert len(responses.calls) == 0 @@ -1070,10 +1145,14 @@ def test_store_auth_header_name_illegal_characters( @responses.activate @pytest.mark.parametrize("auth_type", ["store", "credential-store", "creds"]) -def test_auth_type_aliases(httpie_run, set_credentials, auth_type): +def test_auth_type_aliases( + httpie_run: HttpieRunT, + store_set: StoreSetT, + auth_type: str, +) -> None: """The plugin can be invoked via 'creds' alias.""" - set_credentials( + store_set( [ { "url": "http://example.com", From 721f807ad95a58ce5edf313fab97a429763fe964 Mon Sep 17 00:00:00 2001 From: Ihor Kalnytskyi Date: Wed, 8 May 2024 21:48:15 +0300 Subject: [PATCH 3/3] Remove tests/conftest.py w/ a hack The conftest.py today contains a hack that creates a session scoped temporary directory, and set the HTTPIE_CONFIG_DIR environment variable to that temporary directory. That previously has been done to make sure I can run tests with configuration store elsewhere. Unfortunately, this is a bit inconvenient because (1) we have to make sure there's no imports to httpie package in global scope because otherwise the patch we have won't work, (2) the creation and cleaning phase is scatted across the code base. This patch removes this hack in favor of (1) explicitly passing the temporary directory to executing httpie environment context, and (2) patching required attribute so the plugin can use the up to date version whenever it runs. --- src/httpie_credential_store/_store.py | 4 ++-- tests/conftest.py | 25 ------------------------- tests/test_keychain_password_store.py | 17 +++++------------ tests/test_keychain_shell.py | 21 +++++++-------------- tests/test_keychain_system.py | 19 ++++++------------- tests/test_plugin.py | 19 +++++++------------ 6 files changed, 27 insertions(+), 78 deletions(-) delete mode 100644 tests/conftest.py diff --git a/src/httpie_credential_store/_store.py b/src/httpie_credential_store/_store.py index 0845bb6..b98a800 100644 --- a/src/httpie_credential_store/_store.py +++ b/src/httpie_credential_store/_store.py @@ -34,11 +34,11 @@ def get_auth_for(self, url, credential_id=None): raise LookupError(message) -def get_credential_store(name, directory=httpie.config.DEFAULT_CONFIG_DIR): +def get_credential_store(name, directory=None): """Returns a credential store that can be used to lookup credentials.""" credentials = [] - credential_file = os.path.join(directory, name) + credential_file = os.path.join(directory or httpie.config.DEFAULT_CONFIG_DIR, name) if not os.path.exists(credential_file): error_message = ( diff --git a/tests/conftest.py b/tests/conftest.py deleted file mode 100644 index 4096426..0000000 --- a/tests/conftest.py +++ /dev/null @@ -1,25 +0,0 @@ -import pathlib -import typing - -import pytest - - -@pytest.fixture(scope="session", autouse=True) -def _httpie_config_dir( - tmp_path_factory: pytest.TempPathFactory, -) -> typing.Generator[pathlib.Path, None, None]: - """Set path to HTTPie configuration directory.""" - - # HTTPie can optionally read a path to configuration directory from - # environment variable. In order to avoid messing with user's local - # configuration, HTTPIE_CONFIG_DIR environment variable is patched to point - # to a temporary directory instead. But here's the thing, HTTPie is not ran - # in subprocess in these tests, and so the environment variable is read - # just once on first package import. That's why it must be set before - # HTTPie package is imported and that's why the very same value must be - # used for all tests (session scope). Otherwise, tests may fail because - # they will look for credentials file in different directory. - with pytest.MonkeyPatch.context() as monkeypatch: - tmp_path = tmp_path_factory.mktemp(".httpie") - monkeypatch.setenv("HTTPIE_CONFIG_DIR", str(tmp_path)) - yield tmp_path diff --git a/tests/test_keychain_password_store.py b/tests/test_keychain_password_store.py index 609c976..1f8b85b 100644 --- a/tests/test_keychain_password_store.py +++ b/tests/test_keychain_password_store.py @@ -11,9 +11,7 @@ import pytest - -if typing.TYPE_CHECKING: - from httpie_credential_store._keychain import PasswordStoreKeychain +from httpie_credential_store._keychain import PasswordStoreKeychain _is_macos = sys.platform == "darwin" @@ -82,18 +80,13 @@ def password_store_dir(monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path) @pytest.fixture() -def testkeychain() -> "PasswordStoreKeychain": +def testkeychain() -> PasswordStoreKeychain: """Keychain instance under test.""" - # For the same reasons as in tests/test_plugin.py, all imports that trigger - # HTTPie importing must be postponed till one of our fixtures is evaluated - # and patched a path to HTTPie configuration. - from httpie_credential_store import _keychain - - return _keychain.PasswordStoreKeychain() + return PasswordStoreKeychain() -def test_secret_retrieved(testkeychain: "PasswordStoreKeychain", gpg_key_id: str) -> None: +def test_secret_retrieved(testkeychain: PasswordStoreKeychain, gpg_key_id: str) -> None: """The keychain returns stored secret, no bullshit.""" subprocess.check_call(["pass", "init", gpg_key_id]) @@ -102,7 +95,7 @@ def test_secret_retrieved(testkeychain: "PasswordStoreKeychain", gpg_key_id: str assert testkeychain.get(name="service/user") == "f00b@r" -def test_secret_not_found(testkeychain: "PasswordStoreKeychain") -> None: +def test_secret_not_found(testkeychain: PasswordStoreKeychain) -> None: """LookupError is raised when no secrets are found in the keychain.""" with pytest.raises(LookupError) as excinfo: diff --git a/tests/test_keychain_shell.py b/tests/test_keychain_shell.py index ba7460a..50890f4 100644 --- a/tests/test_keychain_shell.py +++ b/tests/test_keychain_shell.py @@ -6,24 +6,17 @@ import pytest - -if typing.TYPE_CHECKING: - from httpie_credential_store._keychain import ShellKeychain +from httpie_credential_store._keychain import ShellKeychain @pytest.fixture() -def testkeychain() -> "ShellKeychain": +def testkeychain() -> ShellKeychain: """Keychain instance under test.""" - # For the same reasons as in tests/test_plugin.py, all imports that trigger - # HTTPie importing must be postponed till one of our fixtures is evaluated - # and patched a path to HTTPie configuration. - from httpie_credential_store import _keychain - - return _keychain.ShellKeychain() + return ShellKeychain() -def test_secret_retrieved(testkeychain: "ShellKeychain", tmp_path: pathlib.Path) -> None: +def test_secret_retrieved(testkeychain: ShellKeychain, tmp_path: pathlib.Path) -> None: """The keychain returns stored secret, no bullshit.""" secrettxt = tmp_path.joinpath("secret.txt") @@ -31,7 +24,7 @@ def test_secret_retrieved(testkeychain: "ShellKeychain", tmp_path: pathlib.Path) assert testkeychain.get(command=f"cat {secrettxt}") == "p@ss" -def test_secret_retrieved_pipe(testkeychain: "ShellKeychain", tmp_path: pathlib.Path) -> None: +def test_secret_retrieved_pipe(testkeychain: ShellKeychain, tmp_path: pathlib.Path) -> None: """The keychain returns stored secret even when pipes are used.""" secrettxt = tmp_path.joinpath("secret.txt") @@ -41,7 +34,7 @@ def test_secret_retrieved_pipe(testkeychain: "ShellKeychain", tmp_path: pathlib. assert testkeychain.get(command=command) == "p@ss" -def test_secret_not_found(testkeychain: "ShellKeychain", tmp_path: pathlib.Path) -> None: +def test_secret_not_found(testkeychain: ShellKeychain, tmp_path: pathlib.Path) -> None: """LookupError is raised when no secrets are found in the keychain.""" secrettxt = tmp_path.joinpath("secret.txt") @@ -56,7 +49,7 @@ def test_secret_not_found(testkeychain: "ShellKeychain", tmp_path: pathlib.Path) @pytest.mark.parametrize(("args", "kwargs"), [pytest.param(["echo p@ss"], {}, id="args")]) def test_keywords_only_arguments( - testkeychain: "ShellKeychain", + testkeychain: ShellKeychain, args: typing.List[str], kwargs: typing.Mapping[str, str], ) -> None: diff --git a/tests/test_keychain_system.py b/tests/test_keychain_system.py index 397cbe9..18a13b4 100644 --- a/tests/test_keychain_system.py +++ b/tests/test_keychain_system.py @@ -7,9 +7,7 @@ import keyring.compat import pytest - -if typing.TYPE_CHECKING: - from httpie_credential_store._keychain import SystemKeychain +from httpie_credential_store._keychain import SystemKeychain class _InmemoryKeyring(keyring.backend.KeyringBackend): @@ -40,19 +38,14 @@ def keyring_backend() -> typing.Generator[keyring.backend.KeyringBackend, None, @pytest.fixture() -def testkeychain() -> "SystemKeychain": +def testkeychain() -> SystemKeychain: """Keychain instance under test.""" - # For the same reasons as in tests/test_plugin.py, all imports that trigger - # HTTPie importing must be postponed till one of our fixtures is evaluated - # and patched a path to HTTPie configuration. - from httpie_credential_store import _keychain - - return _keychain.SystemKeychain() + return SystemKeychain() def test_secret_retrieved( - testkeychain: "SystemKeychain", + testkeychain: SystemKeychain, keyring_backend: keyring.backend.KeyringBackend, ) -> None: """The keychain returns stored secret, no bullshit.""" @@ -61,7 +54,7 @@ def test_secret_retrieved( assert testkeychain.get(service="testsvc", username="testuser") == "p@ss" -def test_secret_not_found(testkeychain: "SystemKeychain") -> None: +def test_secret_not_found(testkeychain: SystemKeychain) -> None: """LookupError is raised when no secrets are found in the keychain.""" with pytest.raises(LookupError) as excinfo: @@ -80,7 +73,7 @@ def test_secret_not_found(testkeychain: "SystemKeychain") -> None: ], ) def test_keywords_only_arguments( - testkeychain: "SystemKeychain", + testkeychain: SystemKeychain, keyring_backend: keyring.backend.KeyringBackend, args: typing.List[str], kwargs: typing.Mapping[str, str], diff --git a/tests/test_plugin.py b/tests/test_plugin.py index ad2ba31..9ff369e 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -49,18 +49,13 @@ def __repr__(self) -> str: @pytest.fixture(autouse=True) -def httpie_config_dir( - _httpie_config_dir: pathlib.Path, -) -> typing.Generator[pathlib.Path, None, None]: +def httpie_config_dir(monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path) -> pathlib.Path: """Return a path to HTTPie configuration directory.""" - yield _httpie_config_dir - - # Since we cannot use new directory for HTTPie configuration for every test - # (see reasons in `_httpie_config_dir` fixture), we must at least ensure - # that there's no side effect between tests by emptying the directory. - for path in _httpie_config_dir.iterdir(): - path.unlink() + config_dir = tmp_path.joinpath(".httpie") + config_dir.mkdir() + monkeypatch.setattr("httpie.config.DEFAULT_CONFIG_DIR", config_dir) + return config_dir @pytest.fixture() @@ -89,7 +84,7 @@ def httpie_stderr() -> io.StringIO: @pytest.fixture() -def httpie_run(httpie_stderr: io.StringIO) -> HttpieRunT: +def httpie_run(httpie_stderr: io.StringIO, httpie_config_dir: pathlib.Path) -> HttpieRunT: """Run HTTPie from within this process.""" def main(args: typing.List[typing.Union[str, bytes]]) -> int: @@ -100,7 +95,7 @@ def main(args: typing.List[typing.Union[str, bytes]]) -> int: import httpie.core args = ["http", "--ignore-stdin", *args] - env = httpie.context.Environment(stderr=httpie_stderr) + env = httpie.context.Environment(stderr=httpie_stderr, config_dir=httpie_config_dir) return httpie.core.main(args, env=env) return main