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/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 bb63428..0000000 --- a/tests/conftest.py +++ /dev/null @@ -1,20 +0,0 @@ -import pytest - - -@pytest.fixture(scope="session", autouse=True) -def _httpie_config_dir(tmp_path_factory: pytest.TempPathFactory): - """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 195a181..1f8b85b 100644 --- a/tests/test_keychain_password_store.py +++ b/tests/test_keychain_password_store.py @@ -7,9 +7,12 @@ import sys import tempfile import textwrap +import typing import pytest +from httpie_credential_store._keychain import PasswordStoreKeychain + _is_macos = sys.platform == "darwin" @@ -29,13 +32,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 +71,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,18 +80,13 @@ 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 - # 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, 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 +95,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..50890f4 100644 --- a/tests/test_keychain_shell.py +++ b/tests/test_keychain_shell.py @@ -1,23 +1,22 @@ """Tests shell keychain provider.""" import os +import pathlib +import typing import pytest +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 - # 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, 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 +24,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 +34,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 +48,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..18a13b4 100644 --- a/tests/test_keychain_system.py +++ b/tests/test_keychain_system.py @@ -1,26 +1,34 @@ """Tests system keychain provider.""" +import typing + import keyring +import keyring.backend +import keyring.compat import pytest +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,25 +38,23 @@ 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 - # 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, 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 +72,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 db2ff68..9ff369e 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,60 +37,57 @@ 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(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() -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, httpie_config_dir: pathlib.Path) -> 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. @@ -92,21 +95,14 @@ def main(args): 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 -@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): +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"]) @@ -119,7 +115,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: HttpieRunT) -> None: """The plugin is deactivated by default.""" httpie_run(["http://example.com"]) @@ -132,10 +128,10 @@ 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: HttpieRunT, store_set: StoreSetT) -> None: """The plugin works for HTTP basic auth.""" - set_credentials( + store_set( [ { "url": "http://example.com", @@ -147,7 +143,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,13 +153,17 @@ 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: 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", @@ -178,7 +178,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 +188,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: HttpieRunT, store_set: StoreSetT) -> None: """The plugin works for HTTP digest auth.""" responses.add( @@ -205,7 +205,7 @@ def test_creds_auth_digest(httpie_run, set_credentials, creds_auth_type): }, ) - set_credentials( + store_set( [ { "url": "http://example.com", @@ -217,7 +217,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,10 +248,10 @@ 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: HttpieRunT, store_set: StoreSetT) -> None: """The plugin works for HTTP token auth.""" - set_credentials( + store_set( [ { "url": "http://example.com", @@ -262,7 +262,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,10 +272,10 @@ 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: HttpieRunT, store_set: StoreSetT) -> None: """The plugin works for HTTP token auth with custom scheme.""" - set_credentials( + store_set( [ { "url": "http://example.com", @@ -287,7 +287,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,13 +297,17 @@ 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: 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", @@ -317,7 +321,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,10 +331,10 @@ 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: HttpieRunT, store_set: StoreSetT) -> None: """The plugin works for HTTP header auth.""" - set_credentials( + store_set( [ { "url": "http://example.com", @@ -342,7 +346,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,13 +356,17 @@ 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: 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", @@ -373,7 +381,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,10 +391,13 @@ 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: HttpieRunT, + store_set: StoreSetT, +) -> None: """The plugin works for third-party auth plugin.""" - set_credentials( + store_set( [ { "url": "http://example.com", @@ -400,7 +411,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,15 +421,17 @@ 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: 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", @@ -435,7 +448,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,10 +458,13 @@ 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: HttpieRunT, + store_set: StoreSetT, +) -> None: """The plugin works for multiple auths.""" - set_credentials( + store_set( [ { "url": "http://example.com", @@ -470,7 +486,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,10 +497,13 @@ 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: HttpieRunT, + store_set: StoreSetT, +) -> None: """The plugin supports usage of the same auth provider twice.""" - set_credentials( + store_set( [ { "url": "http://example.com", @@ -506,7 +525,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,16 +536,18 @@ 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: 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", @@ -554,7 +575,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 +657,17 @@ 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: 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}]) - httpie_run(["-A", creds_auth_type, "http://example.com"]) + store_set([{"url": "http://example.com", "auth": auth}]) + httpie_run(["-A", "store", "http://example.com"]) if _is_windows: # The error messages on Windows doesn't contain class names before @@ -708,12 +733,16 @@ 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: 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, @@ -724,7 +753,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,10 +763,10 @@ 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: HttpieRunT, store_set: StoreSetT) -> None: """The plugin uses auth of first matched credential entry.""" - set_credentials( + store_set( [ { "url": "yoda.ua", @@ -756,7 +785,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,13 +795,13 @@ 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: 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", @@ -791,8 +820,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,10 +848,16 @@ 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: 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, @@ -842,10 +877,10 @@ 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: 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", @@ -872,10 +907,14 @@ 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: HttpieRunT, + store_set: StoreSetT, + httpie_stderr: io.StringIO, +) -> None: """The plugin raises error if no credentials found.""" - set_credentials( + store_set( [ { "id": "yoda", @@ -890,7 +929,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,10 +948,14 @@ 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: 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", @@ -925,7 +968,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 +998,17 @@ def test_creds_permissions_safe(httpie_run, set_credentials, mode, creds_auth_ty pytest.param(0o610, id="0610"), ], ) -def test_creds_permissions_unsafe( - httpie_run, - set_credentials, - mode, - httpie_stderr, - credentials_file, - creds_auth_type, -): +def test_store_permissions_unsafe( + 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) - httpie_run(["-A", creds_auth_type, "http://example.com"]) + store_set([{"url": "http://example.com", "auth": {}}], mode=mode) + httpie_run(["-A", "store", "http://example.com"]) assert httpie_stderr.getvalue().strip() == ( f"http: error: PermissionError: Permissions '{mode:04o}' for " @@ -985,18 +1027,17 @@ def test_creds_permissions_unsafe( pytest.param(0o100, id="0100"), ], ) -def test_creds_permissions_not_enough( - httpie_run, - set_credentials, - mode, - httpie_stderr, - credentials_file, - creds_auth_type, -): +def test_store_permissions_not_enough( + 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) - httpie_run(["-A", creds_auth_type, "http://example.com"]) + store_set([{"url": "http://example.com", "auth": {}}], mode=mode) + httpie_run(["-A", "store", "http://example.com"]) assert httpie_stderr.getvalue().strip() == ( f"http: error: PermissionError: Permissions '{mode:04o}' for " @@ -1006,10 +1047,14 @@ 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: HttpieRunT, + credentials_file: pathlib.Path, + httpie_stderr: io.StringIO, +) -> None: """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 +1090,15 @@ 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 -): - set_credentials([{"url": "http://example.com", "auth": auth}]) - httpie_run(["-A", creds_auth_type, "http://example.com"]) +def test_store_auth_header_value_illegal_characters( + 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 assert httpie_stderr.getvalue().strip() == error @@ -1075,11 +1124,45 @@ 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 -): - set_credentials([{"url": "http://example.com", "auth": auth}]) - httpie_run(["-A", creds_auth_type, "http://example.com"]) +def test_store_auth_header_name_illegal_characters( + 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 assert httpie_stderr.getvalue().strip() == error + + +@responses.activate +@pytest.mark.parametrize("auth_type", ["store", "credential-store", "creds"]) +def test_auth_type_aliases( + httpie_run: HttpieRunT, + store_set: StoreSetT, + auth_type: str, +) -> None: + """The plugin can be invoked via 'creds' alias.""" + + store_set( + [ + { + "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"