From 0ee7bc448b6443cd85fc82887c37cd19c5316504 Mon Sep 17 00:00:00 2001 From: Johannes Wienke Date: Sun, 3 Jan 2021 17:55:22 +0100 Subject: [PATCH 1/5] Lint and clean markdown README * .github/workflows/ci.yml: add markdown linting to Github actions * .markdownlintrc: desired README markdown configuration * README.md: update according to linting config --- .github/workflows/ci.yml | 13 ++++++++++++- .markdownlintrc | 5 +++++ README.md | 39 ++++++++++++++++++++++++++------------- 3 files changed, 43 insertions(+), 14 deletions(-) create mode 100644 .markdownlintrc diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 621b2d7..6feb37f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,7 +7,7 @@ on: - cron: '0 0 * * 0' jobs: - lint: + lint-code: runs-on: ubuntu-latest steps: @@ -24,6 +24,17 @@ jobs: - name: Lint with tox run: tox -e check + lint-readme: + runs-on: ubuntu-latest + + steps: + - name: Clone repo + uses: actions/checkout@v2 + - name: Lint README.md + uses: docker://avtodev/markdown-lint:v1 + with: + args: './README.md' + test: runs-on: ubuntu-latest diff --git a/.markdownlintrc b/.markdownlintrc new file mode 100644 index 0000000..1ba2fd6 --- /dev/null +++ b/.markdownlintrc @@ -0,0 +1,5 @@ +{ + "default": true, + "line_length": false, + "MD041": false +} diff --git a/README.md b/README.md index 76cd240..40a45d8 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ # pass-git-helper -[![Debian CI](https://badges.debian.net/badges/debian/testing/pass-git-helper/version.svg)](https://buildd.debian.org/pass-git-helper) +[![Debian CI](https://badges.debian.net/badges/debian/testing/pass-git-helper/version.svg)](https://buildd.debian.org/pass-git-helper) [![AUR](https://img.shields.io/aur/version/pass-git-helper.svg)](https://aur.archlinux.org/packages/pass-git-helper/) [![Homebrew](https://img.shields.io/badge/dynamic/json.svg?url=https://formulae.brew.sh/api/formula/pass-git-helper.json&query=$.versions.stable&label=homebrew) @@ -15,10 +15,12 @@ It is recommended to configure GPG to use a graphical pinentry program. That way, you can also use this helper when [git] is invoked via GUI programs such as your IDE. For a configuration example, refer to the [ArchWiki](https://wiki.archlinux.org/index.php/GnuPG#pinentry). In case you really want to use the terminal for pinentry (via `pinentry-curses`), be sure to [appropriately configure the environment variable `GPG_TTY`](https://www.gnupg.org/documentation/manuals/gnupg/Invoking-GPG_002dAGENT.html), most likely by adding the following lines to your shell initialization: + ```sh GPG_TTY=$(tty) export GPG_TTY ``` + If you use this setup for remote work via SSH, also consider the alternative of [GPG agent forwarding](https://wiki.gnupg.org/AgentForwarding). ## Installation @@ -35,11 +37,13 @@ sudo pip install . This might potentially install Python packages without the knowledge of your system's package manager. If all package preconditions are already met, you can also copy the script file to to your system to avoid this problem: + ```sh sudo cp passgithelper.py /usr/local/bin/pass-git-helper ``` Another option is to install the script in an isolated [virtualenv](https://virtualenv.pypa.io/en/latest/): + ```sh virtualenv /your/env /your/env/pip install . @@ -54,6 +58,7 @@ Matching supports wildcards (using the python [fnmatch module](https://docs.pyth Each section needs to contain a `target` entry pointing to the entry in the password store with the password (and optionally username) to use. Example: + ```ini [github.com*] target=dev/github @@ -62,59 +67,65 @@ target=dev/github target=dev/fooo-bar ``` -To instruct git to use the helper, set the `credential.helper` configuration option of git to: -``` -/full/path/to/pass-git-helper -``` -In case you do not want to include a full path, a workaround using a shell fragment needs to be used, i.e.: -``` -!pass-git-helper $@ -``` +To instruct git to use the helper, set the `credential.helper` configuration option of git to `/full/path/to/pass-git-helper`. +In case you do not want to include a full path, a workaround using a shell fragment needs to be used, i.e. `!pass-git-helper $@` must be the option value. +The option can be set using the CLI with: -The option can be set e.g. via: ```sh git config credential.helper '!pass-git-helper $@' ``` If you want to match entries not only based on the host, but also based on the path on a host, set `credential.useHttpPath` to `true` in your git config, e.g. via: + ```sh git config credential.useHttpPath true ``` + Afterwards, entries can be matched against `host.com/path/to/repo` in the mapping. -This means that in order to use a specific account for a certain github project, you can then use the following mapping pattern: +This means that in order to use a specific account for a certain Github project, you can then use the following mapping pattern: + ```ini [github.com/username/project*] target=dev/github ``` + Please note that when including the path in the mapping, the mapping expressions need to match against the whole path. -As a consequence, in case you want to use the same account for all github projects, you need to make sure that a wildcard covers the path of the URL, as shown here: +As a consequence, in case you want to use the same account for all Github projects, you need to make sure that a wildcard covers the path of the URL, as shown here: + ```ini [github.com*] target=dev/github ``` + The host can be used as a variable to address a pass entry. This is especially helpful for wildcard matches: + ```ini [*] target=git-logins/${host} ``` + The above configuration directive will lead to any host that did not match any previous section in the ini file to being looked up under the `git-logins` directory in your passwordstore. Using the `includeIf` directive available in git >= 2.13, it is also possible to perform matching based on the current working directory by invoking `pass-git-helper` with a conditional `MAPPING-FILE`. To achieve this, edit your `.gitconfig`, e.g. like this: + ```ini [includeIf "gitdir:~/src/user1/"] path=~/.config/git/gitconfig_user1 [includeIf "gitdir:~/src/user2/"] path=~/.config/git/gitconfig_user2 ``` + With the following contents of `gitconfig_user1` (and `gitconfig_user2` repspectively), `mapping_user1.ini`, which could contain a `target` entry to e.g. `github.com/user1` would always be invoked in `~/src/user1`: + ```ini [user] name = user1 [credential] helper=/full/path/to/pass-git-helper -m /full/path/to/mapping_user1.ini ``` + See also the offical [documentation](https://git-scm.com/docs/git-config#_includes) for `.gitconfig`. ### DEFAULT section @@ -165,14 +176,16 @@ Extracts the data from a line indexed by its line number. Optionally a fixed-length prefix can be stripped before returning the line contents. Configuration: + * `line_username`: Line number containing the username, **0-based**. Default: 1 (second line) -* `skip_username`: Number of characters to skip at the beginning of the line, for instance to skip a `user: ` prefix. Similar to `skip_password`. Default: 0. +* `skip_username`: Number of characters to skip at the beginning of the line, for instance to skip a `user:` prefix. Similar to `skip_password`. Default: 0. #### Strategy "regex_search" Searches for the first line that matches a provided regular expressions and returns the contents of that line that are captured in a regular expression capture group. Configuration: + * `regex_username`: The regular expression to apply. Has to contain a single capture group for indicating the data to extract. Default: `^username: +(.*)$`. #### Strategy "entry_name" From d9fa5aa9a0213356117de1deba8c0b7b0e777ae4 Mon Sep 17 00:00:00 2001 From: Johannes Wienke Date: Sun, 3 Jan 2021 18:44:12 +0100 Subject: [PATCH 2/5] Use type hints everywhere --- passgithelper.py | 19 +++++---- setup.cfg | 5 +++ test_passgithelper.py | 92 ++++++++++++++++++++++++++++--------------- 3 files changed, 74 insertions(+), 42 deletions(-) diff --git a/passgithelper.py b/passgithelper.py index 1b98fb9..3b639cd 100755 --- a/passgithelper.py +++ b/passgithelper.py @@ -17,7 +17,7 @@ import re import subprocess import sys -from typing import Dict, Optional, Pattern, Sequence, Text +from typing import Dict, IO, Mapping, Optional, Pattern, Sequence, Text import xdg.BaseDirectory @@ -75,7 +75,7 @@ def parse_arguments(argv: Optional[Sequence[str]] = None) -> argparse.Namespace: return args -def parse_mapping(mapping_file: Optional[str]) -> configparser.ConfigParser: +def parse_mapping(mapping_file: Optional[IO]) -> configparser.ConfigParser: """ Parse the file containing the mappings from hosts to pass entries. @@ -86,7 +86,7 @@ def parse_mapping(mapping_file: Optional[str]) -> configparser.ConfigParser: """ LOGGER.debug("Parsing mapping file. Command line: %s", mapping_file) - def parse(mapping_file): + def parse(mapping_file: IO) -> configparser.ConfigParser: config = configparser.ConfigParser() config.read_file(mapping_file) return config @@ -103,9 +103,9 @@ def parse(mapping_file): "No mapping configured so far at any XDG config location. " "Please create {config_file}".format(config_file=DEFAULT_CONFIG_FILE) ) - mapping_file = os.path.join(xdg_config_dir, CONFIG_FILE_NAME) + default_file = os.path.join(xdg_config_dir, CONFIG_FILE_NAME) LOGGER.debug("Parsing mapping file %s", mapping_file) - with open(mapping_file, "r") as file_handle: + with open(default_file, "r") as file_handle: return parse(file_handle) @@ -196,7 +196,7 @@ def __init__(self, prefix_length: int, option_suffix: Text = "") -> None: self._prefix_length = prefix_length @abc.abstractmethod - def configure(self, config: configparser.SectionProxy): + def configure(self, config: configparser.SectionProxy) -> None: """Configure the amount of characters to skip.""" self._prefix_length = config.getint( "skip{suffix}".format(suffix=self._option_suffix), @@ -325,7 +325,9 @@ def get_value( } -def get_password(request, mapping) -> None: +def get_password( + request: Mapping[str, str], mapping: configparser.ConfigParser +) -> None: """ Resolve the given credential request in the provided mapping definition. @@ -346,9 +348,6 @@ def get_password(request, mapping) -> None: if "path" in request: host = "/".join([host, request["path"]]) - def skip(line, skip): - return line[skip:] - LOGGER.debug('Iterating mapping to match against host "%s"', host) for section in mapping.sections(): if fnmatch.fnmatch(host, section): diff --git a/setup.cfg b/setup.cfg index aca12b5..c6e8562 100644 --- a/setup.cfg +++ b/setup.cfg @@ -40,3 +40,8 @@ exclude_lines = [mypy] ignore_missing_imports=True +disallow_untyped_defs = True +check_untyped_defs = True +no_implicit_optional = True +warn_unused_configs = True +warn_unused_ignores = True diff --git a/test_passgithelper.py b/test_passgithelper.py index 2698e48..0936830 100644 --- a/test_passgithelper.py +++ b/test_passgithelper.py @@ -1,25 +1,27 @@ import configparser import io import logging +from typing import Any, Optional, Sequence, Text import pytest +from pytest_mock import MockFixture import passgithelper @pytest.fixture -def xdg_dir(request, mocker): +def xdg_dir(request: Any, mocker: MockFixture) -> None: xdg_mock = mocker.patch("xdg.BaseDirectory.load_first_config") xdg_mock.return_value = request.param -def test_handle_skip_nothing(monkeypatch): +def test_handle_skip_nothing(monkeypatch: Any) -> None: monkeypatch.delenv("PASS_GIT_HELPER_SKIP", raising=False) passgithelper.handle_skip() # should do nothing normally -def test_handle_skip_exits(monkeypatch): +def test_handle_skip_exits(monkeypatch: Any) -> None: monkeypatch.setenv("PASS_GIT_HELPER_SKIP", "1") with pytest.raises(SystemExit): passgithelper.handle_skip() @@ -27,20 +29,22 @@ def test_handle_skip_exits(monkeypatch): class TestSkippingDataExtractor: class ExtractorImplementation(passgithelper.SkippingDataExtractor): - def configure(self, config): + def configure(self, config: configparser.SectionProxy) -> None: pass - def __init__(self, skip_characters: int = 0): + def __init__(self, skip_characters: int = 0) -> None: super().__init__(skip_characters) - def _get_raw(self, entry_text, entry_lines): + def _get_raw( + self, entry_text: Text, entry_lines: Sequence[Text] + ) -> Optional[Text]: return entry_lines[0] - def test_smoke(self): + def test_smoke(self) -> None: extractor = self.ExtractorImplementation(4) assert extractor.get_value("foo", ["testthis"]) == "this" - def test_too_short(self): + def test_too_short(self) -> None: extractor = self.ExtractorImplementation(8) assert extractor.get_value("foo", ["testthis"]) == "" extractor = self.ExtractorImplementation(10) @@ -48,19 +52,19 @@ def test_too_short(self): class TestSpecificLineExtractor: - def test_smoke(self): + def test_smoke(self) -> None: extractor = passgithelper.SpecificLineExtractor(1, 6) assert ( extractor.get_value("foo", ["line 1", "user: bar", "more lines"]) == "bar" ) - def test_no_such_line(self): + def test_no_such_line(self) -> None: extractor = passgithelper.SpecificLineExtractor(3, 6) assert extractor.get_value("foo", ["line 1", "user: bar", "more lines"]) is None class TestRegexSearchExtractor: - def test_smoke(self): + def test_smoke(self) -> None: extractor = passgithelper.RegexSearchExtractor("^username: (.*)$", "") assert ( extractor.get_value( @@ -75,11 +79,11 @@ def test_smoke(self): == "user" ) - def test_missing_group(self): + def test_missing_group(self) -> None: with pytest.raises(ValueError): passgithelper.RegexSearchExtractor("^username: .*$", "") - def test_configuration(self): + def test_configuration(self) -> None: extractor = passgithelper.RegexSearchExtractor("^username: (.*)$", "_username") config = configparser.ConfigParser() config.read_string( @@ -89,7 +93,7 @@ def test_configuration(self): extractor.configure(config["test"]) assert extractor._regex.pattern == r"^foo: (.*)$" - def test_configuration_checks_groups(self): + def test_configuration_checks_groups(self) -> None: extractor = passgithelper.RegexSearchExtractor("^username: (.*)$", "_username") config = configparser.ConfigParser() config.read_string( @@ -101,7 +105,7 @@ def test_configuration_checks_groups(self): class TestEntryNameExtractor: - def test_smoke(self): + def test_smoke(self) -> None: assert passgithelper.EntryNameExtractor().get_value("foo/bar", []) == "bar" @@ -110,7 +114,7 @@ def test_smoke(self): [None], indirect=True, ) -def test_parse_mapping_file_missing(xdg_dir): +def test_parse_mapping_file_missing(xdg_dir: None) -> None: with pytest.raises(RuntimeError): passgithelper.parse_mapping(None) @@ -120,20 +124,20 @@ def test_parse_mapping_file_missing(xdg_dir): ["test_data/smoke"], indirect=True, ) -def test_parse_mapping_from_xdg(xdg_dir): +def test_parse_mapping_from_xdg(xdg_dir: None) -> None: config = passgithelper.parse_mapping(None) assert "mytest.com" in config assert config["mytest.com"]["target"] == "dev/mytest" class TestScript: - def test_help(self, capsys): + def test_help(self, capsys: Any) -> None: with pytest.raises(SystemExit): passgithelper.main(["--help"]) assert "usage: " in capsys.readouterr().out - def test_skip(self, monkeypatch, capsys): + def test_skip(self, monkeypatch: Any, capsys: Any) -> None: monkeypatch.setenv("PASS_GIT_HELPER_SKIP", "1") with pytest.raises(SystemExit): passgithelper.main(["get"]) @@ -146,7 +150,9 @@ def test_skip(self, monkeypatch, capsys): ["test_data/smoke"], indirect=True, ) - def test_smoke_resolve(self, xdg_dir, monkeypatch, mocker, capsys): + def test_smoke_resolve( + self, xdg_dir: None, monkeypatch: Any, mocker: MockFixture, capsys: Any + ) -> None: monkeypatch.setattr( "sys.stdin", io.StringIO( @@ -171,7 +177,9 @@ def test_smoke_resolve(self, xdg_dir, monkeypatch, mocker, capsys): ["test_data/smoke"], indirect=True, ) - def test_path_used_if_present_fails(self, xdg_dir, monkeypatch, caplog): + def test_path_used_if_present_fails( + self, xdg_dir: None, monkeypatch: Any, caplog: Any + ) -> None: monkeypatch.setattr( "sys.stdin", io.StringIO( @@ -194,7 +202,9 @@ def test_path_used_if_present_fails(self, xdg_dir, monkeypatch, caplog): ["test_data/with-path"], indirect=True, ) - def test_path_used_if_present(self, xdg_dir, monkeypatch, mocker, capsys): + def test_path_used_if_present( + self, xdg_dir: None, monkeypatch: Any, mocker: MockFixture, capsys: Any + ) -> None: monkeypatch.setattr( "sys.stdin", io.StringIO( @@ -221,7 +231,9 @@ def test_path_used_if_present(self, xdg_dir, monkeypatch, mocker, capsys): ["test_data/wildcard"], indirect=True, ) - def test_wildcard_matching(self, xdg_dir, monkeypatch, mocker, capsys): + def test_wildcard_matching( + self, xdg_dir: None, monkeypatch: Any, mocker: MockFixture, capsys: Any + ) -> None: monkeypatch.setattr( "sys.stdin", io.StringIO( @@ -251,7 +263,9 @@ def test_wildcard_matching(self, xdg_dir, monkeypatch, mocker, capsys): ["test_data/with-username"], indirect=True, ) - def test_username_provided(self, xdg_dir, monkeypatch, mocker, capsys): + def test_username_provided( + self, xdg_dir: None, monkeypatch: Any, mocker: MockFixture, capsys: Any + ) -> None: monkeypatch.setattr( "sys.stdin", io.StringIO( @@ -277,7 +291,9 @@ def test_username_provided(self, xdg_dir, monkeypatch, mocker, capsys): ["test_data/with-username"], indirect=True, ) - def test_username_skipped_if_provided(self, xdg_dir, monkeypatch, mocker, capsys): + def test_username_skipped_if_provided( + self, xdg_dir: None, monkeypatch: Any, mocker: MockFixture, capsys: Any + ) -> None: monkeypatch.setattr( "sys.stdin", io.StringIO( @@ -304,7 +320,9 @@ def test_username_skipped_if_provided(self, xdg_dir, monkeypatch, mocker, capsys ["test_data/with-username"], indirect=True, ) - def test_custom_mapping_used(self, xdg_dir, monkeypatch, mocker, capsys): + def test_custom_mapping_used( + self, xdg_dir: None, monkeypatch: Any, mocker: MockFixture, capsys: Any + ) -> None: # this would fail for the default file from with-username monkeypatch.setattr( "sys.stdin", @@ -330,7 +348,9 @@ def test_custom_mapping_used(self, xdg_dir, monkeypatch, mocker, capsys): ["test_data/with-username-skip"], indirect=True, ) - def test_prefix_skipping(self, xdg_dir, monkeypatch, mocker, capsys): + def test_prefix_skipping( + self, xdg_dir: None, monkeypatch: Any, mocker: MockFixture, capsys: Any + ) -> None: monkeypatch.setattr( "sys.stdin", io.StringIO( @@ -355,7 +375,9 @@ def test_prefix_skipping(self, xdg_dir, monkeypatch, mocker, capsys): ["test_data/unknown-username-extractor"], indirect=True, ) - def test_select_unknown_extractor(self, xdg_dir, monkeypatch, capsys): + def test_select_unknown_extractor( + self, xdg_dir: None, monkeypatch: Any, capsys: Any + ) -> None: monkeypatch.setattr( "sys.stdin", io.StringIO( @@ -373,7 +395,9 @@ def test_select_unknown_extractor(self, xdg_dir, monkeypatch, capsys): ["test_data/regex-extraction"], indirect=True, ) - def test_regex_username_selection(self, xdg_dir, monkeypatch, mocker, capsys): + def test_regex_username_selection( + self, xdg_dir: None, monkeypatch: Any, mocker: MockFixture, capsys: Any + ) -> None: monkeypatch.setattr( "sys.stdin", io.StringIO( @@ -400,7 +424,9 @@ def test_regex_username_selection(self, xdg_dir, monkeypatch, mocker, capsys): ["test_data/entry-name-extraction"], indirect=True, ) - def test_entry_name_is_user(self, xdg_dir, monkeypatch, mocker, capsys): + def test_entry_name_is_user( + self, xdg_dir: None, monkeypatch: Any, mocker: MockFixture, capsys: Any + ) -> None: monkeypatch.setattr( "sys.stdin", io.StringIO( @@ -426,7 +452,7 @@ def test_entry_name_is_user(self, xdg_dir, monkeypatch, mocker, capsys): indirect=True, ) def test_uses_configured_encoding( - self, xdg_dir, monkeypatch, mocker, capsys + self, xdg_dir: None, monkeypatch: Any, mocker: MockFixture, capsys: Any ) -> None: monkeypatch.setattr( "sys.stdin", @@ -453,7 +479,9 @@ def test_uses_configured_encoding( ["test_data/smoke"], indirect=True, ) - def test_uses_utf8_by_default(self, xdg_dir, mocker, monkeypatch, capsys) -> None: + def test_uses_utf8_by_default( + self, xdg_dir: None, mocker: MockFixture, monkeypatch: Any, capsys: Any + ) -> None: monkeypatch.setattr( "sys.stdin", io.StringIO( From dade79c24f135ec6d023fe5132d473a666c3da77 Mon Sep 17 00:00:00 2001 From: Johannes Wienke Date: Sun, 3 Jan 2021 19:35:31 +0100 Subject: [PATCH 3/5] Modernize linting setup Add more linters and update the code to match the new requirements. --- passgithelper.py | 117 +++++++++++++++++++++++++----------------- setup.cfg | 16 +++++- test_passgithelper.py | 96 ++++++++++++++++++---------------- tox.ini | 38 ++++++++++---- 4 files changed, 164 insertions(+), 103 deletions(-) diff --git a/passgithelper.py b/passgithelper.py index 3b639cd..cddfc6b 100755 --- a/passgithelper.py +++ b/passgithelper.py @@ -70,9 +70,7 @@ def parse_arguments(argv: Optional[Sequence[str]] = None) -> argparse.Namespace: help="Action to preform as specified in the git credential API", ) - args = parser.parse_args(argv) - - return args + return parser.parse_args(argv) def parse_mapping(mapping_file: Optional[IO]) -> configparser.ConfigParser: @@ -325,6 +323,48 @@ def get_value( } +def find_mapping_section( + mapping: configparser.ConfigParser, request_header: str +) -> configparser.SectionProxy: + """Select the mapping entry matching the request header.""" + + LOGGER.debug('Searching mapping to match against header "%s"', request_header) + for section in mapping.sections(): + if fnmatch.fnmatch(request_header, section): + LOGGER.debug( + 'Section "%s" matches requested header "%s"', section, request_header + ) + return mapping[section] + + raise ValueError( + f"No mapping section in {mapping.sections()} matches request {request_header}" + ) + + +def get_request_section_header(request: Mapping[str, str]) -> str: + """Return the canonical host + optional path for section header matching.""" + + if "host" not in request: + LOGGER.error("host= entry missing in request. Cannot query without a host") + raise ValueError("Request lacks host entry") + + host = request["host"] + if "path" in request: + host = "/".join([host, request["path"]]) + return host + + +def define_pass_target( + section: configparser.SectionProxy, request: Mapping[str, str] +) -> str: + """Determine the pass target by filling in potentially used variables.""" + + pass_target = section.get("target").replace("${host}", request["host"]) + if "username" in request: + pass_target = pass_target.replace("${username}", request["username"]) + return pass_target + + def get_password( request: Mapping[str, str], mapping: configparser.ConfigParser ) -> None: @@ -339,51 +379,36 @@ def get_password( mapping: The mapping configuration as a ConfigParser instance. """ - LOGGER.debug('Received request "%s"', request) - if "host" not in request: - LOGGER.error("host= entry missing in request. Cannot query without a host") - return - host = request["host"] - if "path" in request: - host = "/".join([host, request["path"]]) + LOGGER.debug('Received request "%s"', request) - LOGGER.debug('Iterating mapping to match against host "%s"', host) - for section in mapping.sections(): - if fnmatch.fnmatch(host, section): - LOGGER.debug('Section "%s" matches requested host "%s"', section, host) - # TODO handle exceptions - pass_target = mapping.get(section, "target").replace( - "${host}", request["host"] - ) - if "username" in request: - pass_target = pass_target.replace("${username}", request["username"]) - - password_extractor = SpecificLineExtractor(0, 0, option_suffix="_password") - password_extractor.configure(mapping[section]) - username_extractor = _username_extractors[ - mapping[section].get( - "username_extractor", fallback=_line_extractor_name - ) - ] - username_extractor.configure(mapping[section]) - - LOGGER.debug('Requesting entry "%s" from pass', pass_target) - output = subprocess.check_output(["pass", "show", pass_target]).decode( - mapping[section].get("encoding", "UTF-8") - ) - lines = output.splitlines() - - password = password_extractor.get_value(pass_target, lines) - username = username_extractor.get_value(pass_target, lines) - if password: - print("password={password}".format(password=password)) # noqa: T001 - if "username" not in request and username: - print("username={username}".format(username=username)) # noqa: T001 - return - - LOGGER.warning("No mapping matched") - sys.exit(1) + header = get_request_section_header(request) + section = find_mapping_section(mapping, header) + + pass_target = define_pass_target(section, request) + + password_extractor = SpecificLineExtractor(0, 0, option_suffix="_password") + password_extractor.configure(section) + username_extractor = _username_extractors[ + section.get("username_extractor", fallback=_line_extractor_name) + ] + username_extractor.configure(section) + + LOGGER.debug('Requesting entry "%s" from pass', pass_target) + # silence the subprocess injection warnings as it is the user's + # responsibility to provide a safe mapping and execution environment + output = subprocess.check_output( # noqa: S603, S607 + ["pass", "show", pass_target] + ).decode(section.get("encoding", "UTF-8")) + lines = output.splitlines() + + password = password_extractor.get_value(pass_target, lines) + username = username_extractor.get_value(pass_target, lines) + if password: + print("password={password}".format(password=password)) # noqa: T001 + if "username" not in request and username: + print("username={username}".format(username=username)) # noqa: T001 + return def handle_skip() -> None: diff --git a/setup.cfg b/setup.cfg index c6e8562..b57858b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -14,9 +14,21 @@ exclude = .eggs, env, .mypy_cache -ignore = D413, E203, W503 +ignore = + ANN101, + ANN102, + D202, + D413, + E203, + S101, + S404, + TYP101, + TYP102, + TYP002, + TYP003, + W503, per-file-ignores = - test_*: D1 + test_*: D1, S105 setup.py: D1 application-import-names = passgithelper import-order-style = google diff --git a/test_passgithelper.py b/test_passgithelper.py index 0936830..22c31ef 100644 --- a/test_passgithelper.py +++ b/test_passgithelper.py @@ -1,6 +1,5 @@ import configparser import io -import logging from typing import Any, Optional, Sequence, Text import pytest @@ -9,8 +8,8 @@ import passgithelper -@pytest.fixture -def xdg_dir(request: Any, mocker: MockFixture) -> None: +@pytest.fixture() +def _xdg_dir(request: Any, mocker: MockFixture) -> None: xdg_mock = mocker.patch("xdg.BaseDirectory.load_first_config") xdg_mock.return_value = request.param @@ -80,7 +79,7 @@ def test_smoke(self) -> None: ) def test_missing_group(self) -> None: - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="must contain"): passgithelper.RegexSearchExtractor("^username: .*$", "") def test_configuration(self) -> None: @@ -100,7 +99,7 @@ def test_configuration_checks_groups(self) -> None: r"""[test] regex_username=^foo: .*$""" ) - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="must contain"): extractor.configure(config["test"]) @@ -110,21 +109,23 @@ def test_smoke(self) -> None: @pytest.mark.parametrize( - "xdg_dir", + "_xdg_dir", [None], indirect=True, ) -def test_parse_mapping_file_missing(xdg_dir: None) -> None: +@pytest.mark.usefixtures("_xdg_dir") +def test_parse_mapping_file_missing() -> None: with pytest.raises(RuntimeError): passgithelper.parse_mapping(None) @pytest.mark.parametrize( - "xdg_dir", + "_xdg_dir", ["test_data/smoke"], indirect=True, ) -def test_parse_mapping_from_xdg(xdg_dir: None) -> None: +@pytest.mark.usefixtures("_xdg_dir") +def test_parse_mapping_from_xdg() -> None: config = passgithelper.parse_mapping(None) assert "mytest.com" in config assert config["mytest.com"]["target"] == "dev/mytest" @@ -146,12 +147,13 @@ def test_skip(self, monkeypatch: Any, capsys: Any) -> None: assert not err @pytest.mark.parametrize( - "xdg_dir", + "_xdg_dir", ["test_data/smoke"], indirect=True, ) + @pytest.mark.usefixtures("_xdg_dir") def test_smoke_resolve( - self, xdg_dir: None, monkeypatch: Any, mocker: MockFixture, capsys: Any + self, monkeypatch: Any, mocker: MockFixture, capsys: Any ) -> None: monkeypatch.setattr( "sys.stdin", @@ -173,13 +175,12 @@ def test_smoke_resolve( assert out == "password=narf\n" @pytest.mark.parametrize( - "xdg_dir", + "_xdg_dir", ["test_data/smoke"], indirect=True, ) - def test_path_used_if_present_fails( - self, xdg_dir: None, monkeypatch: Any, caplog: Any - ) -> None: + @pytest.mark.usefixtures("_xdg_dir") + def test_path_used_if_present_fails(self, monkeypatch: Any) -> None: monkeypatch.setattr( "sys.stdin", io.StringIO( @@ -190,20 +191,17 @@ def test_path_used_if_present_fails( ), ) - with caplog.at_level(logging.WARNING): - with pytest.raises(SystemExit): - passgithelper.main(["get"]) - assert caplog.record_tuples == [ - ("root", logging.WARNING, "No mapping matched"), - ] + with pytest.raises(ValueError, match="No mapping section"): + passgithelper.main(["get"]) @pytest.mark.parametrize( - "xdg_dir", + "_xdg_dir", ["test_data/with-path"], indirect=True, ) + @pytest.mark.usefixtures("_xdg_dir") def test_path_used_if_present( - self, xdg_dir: None, monkeypatch: Any, mocker: MockFixture, capsys: Any + self, monkeypatch: Any, mocker: MockFixture, capsys: Any ) -> None: monkeypatch.setattr( "sys.stdin", @@ -227,12 +225,13 @@ def test_path_used_if_present( assert out == "password=narf\n" @pytest.mark.parametrize( - "xdg_dir", + "_xdg_dir", ["test_data/wildcard"], indirect=True, ) + @pytest.mark.usefixtures("_xdg_dir") def test_wildcard_matching( - self, xdg_dir: None, monkeypatch: Any, mocker: MockFixture, capsys: Any + self, monkeypatch: Any, mocker: MockFixture, capsys: Any ) -> None: monkeypatch.setattr( "sys.stdin", @@ -259,12 +258,13 @@ def test_wildcard_matching( assert out == "password=narf-wildcard\n" @pytest.mark.parametrize( - "xdg_dir", + "_xdg_dir", ["test_data/with-username"], indirect=True, ) + @pytest.mark.usefixtures("_xdg_dir") def test_username_provided( - self, xdg_dir: None, monkeypatch: Any, mocker: MockFixture, capsys: Any + self, monkeypatch: Any, mocker: MockFixture, capsys: Any ) -> None: monkeypatch.setattr( "sys.stdin", @@ -287,12 +287,13 @@ def test_username_provided( assert out == "password=password\nusername=username\n" @pytest.mark.parametrize( - "xdg_dir", + "_xdg_dir", ["test_data/with-username"], indirect=True, ) + @pytest.mark.usefixtures("_xdg_dir") def test_username_skipped_if_provided( - self, xdg_dir: None, monkeypatch: Any, mocker: MockFixture, capsys: Any + self, monkeypatch: Any, mocker: MockFixture, capsys: Any ) -> None: monkeypatch.setattr( "sys.stdin", @@ -316,12 +317,13 @@ def test_username_skipped_if_provided( assert out == "password=password\n" @pytest.mark.parametrize( - "xdg_dir", + "_xdg_dir", ["test_data/with-username"], indirect=True, ) + @pytest.mark.usefixtures("_xdg_dir") def test_custom_mapping_used( - self, xdg_dir: None, monkeypatch: Any, mocker: MockFixture, capsys: Any + self, monkeypatch: Any, mocker: MockFixture, capsys: Any ) -> None: # this would fail for the default file from with-username monkeypatch.setattr( @@ -344,12 +346,13 @@ def test_custom_mapping_used( assert out == "password=narf\n" @pytest.mark.parametrize( - "xdg_dir", + "_xdg_dir", ["test_data/with-username-skip"], indirect=True, ) + @pytest.mark.usefixtures("_xdg_dir") def test_prefix_skipping( - self, xdg_dir: None, monkeypatch: Any, mocker: MockFixture, capsys: Any + self, monkeypatch: Any, mocker: MockFixture, capsys: Any ) -> None: monkeypatch.setattr( "sys.stdin", @@ -371,13 +374,12 @@ def test_prefix_skipping( assert out == "password=xyz\nusername=tester\n" @pytest.mark.parametrize( - "xdg_dir", + "_xdg_dir", ["test_data/unknown-username-extractor"], indirect=True, ) - def test_select_unknown_extractor( - self, xdg_dir: None, monkeypatch: Any, capsys: Any - ) -> None: + @pytest.mark.usefixtures("_xdg_dir") + def test_select_unknown_extractor(self, monkeypatch: Any, capsys: Any) -> None: monkeypatch.setattr( "sys.stdin", io.StringIO( @@ -391,12 +393,13 @@ def test_select_unknown_extractor( passgithelper.main(["get"]) @pytest.mark.parametrize( - "xdg_dir", + "_xdg_dir", ["test_data/regex-extraction"], indirect=True, ) + @pytest.mark.usefixtures("_xdg_dir") def test_regex_username_selection( - self, xdg_dir: None, monkeypatch: Any, mocker: MockFixture, capsys: Any + self, monkeypatch: Any, mocker: MockFixture, capsys: Any ) -> None: monkeypatch.setattr( "sys.stdin", @@ -420,12 +423,13 @@ def test_regex_username_selection( assert out == "password=xyz\nusername=tester\n" @pytest.mark.parametrize( - "xdg_dir", + "_xdg_dir", ["test_data/entry-name-extraction"], indirect=True, ) + @pytest.mark.usefixtures("_xdg_dir") def test_entry_name_is_user( - self, xdg_dir: None, monkeypatch: Any, mocker: MockFixture, capsys: Any + self, monkeypatch: Any, mocker: MockFixture, capsys: Any ) -> None: monkeypatch.setattr( "sys.stdin", @@ -447,12 +451,13 @@ def test_entry_name_is_user( assert out == "password=xyz\nusername=myuser\n" @pytest.mark.parametrize( - "xdg_dir", + "_xdg_dir", ["test_data/with-encoding"], indirect=True, ) + @pytest.mark.usefixtures("_xdg_dir") def test_uses_configured_encoding( - self, xdg_dir: None, monkeypatch: Any, mocker: MockFixture, capsys: Any + self, monkeypatch: Any, mocker: MockFixture, capsys: Any ) -> None: monkeypatch.setattr( "sys.stdin", @@ -475,12 +480,13 @@ def test_uses_configured_encoding( assert out == f"password={password}\n" @pytest.mark.parametrize( - "xdg_dir", + "_xdg_dir", ["test_data/smoke"], indirect=True, ) + @pytest.mark.usefixtures("_xdg_dir") def test_uses_utf8_by_default( - self, xdg_dir: None, mocker: MockFixture, monkeypatch: Any, capsys: Any + self, mocker: MockFixture, monkeypatch: Any, capsys: Any ) -> None: monkeypatch.setattr( "sys.stdin", diff --git a/tox.ini b/tox.ini index fbae71d..3c51e4a 100644 --- a/tox.ini +++ b/tox.ini @@ -30,17 +30,35 @@ commands = [testenv:check] deps = - flake8 - flake8-black~=0.1.0 - flake8-bugbear~=20.1.0 - flake8-builtins~=0.1.0 - flake8-comprehensions~=3.2.0 + pydocstyle~=5.0.0 + flake8~=3.7.0 + dlint~=0.10.0 + flake8-annotations~=2.3.0 + flake8-bandit~=2.1.0 + flake8-black~=0.2.1 + flake8-bugbear~=20.1.4 + flake8-builtins~=1.5.3 + flake8-cognitive-complexity~=0.1.0 + flake8-comprehensions~=3.2.3 + flake8-debugger~=3.2.1 flake8-docstrings~=1.5.0 - flake8-import-order~=0.18.0 - flake8-print~=3.1.0 - flake8-string-format~=0.2.0 - pep8-naming~=0.9.0 - mypy==0.761 + flake8-eradicate~=0.4.0 + flake8-expression-complexity~=0.0.6 + flake8-import-order~=0.18.1 + flake8-junit-report~=2.1.0 + flake8-logging-format~=0.6.0 + flake8-mock~=0.3 + flake8-mutable~=1.2.0 + flake8-pep3101~=1.3.0 + flake8-pie~=0.5.0 + flake8-print~=3.1.4 + flake8-pytest-style~=1.2.3 + flake8-simplify~=0.10.0 + flake8-string-format~=0.3.0 + flake8-tidy-imports~=4.1.0 + flake8-variables-names~=0.0.3 + pep8-naming~=0.11.1 + mypy==0.790 black==20.8b0 commands = {envbindir}/python -V From f76ad1ac8782eaa37d6a6771ed1ad4e6906404d3 Mon Sep 17 00:00:00 2001 From: Johannes Wienke Date: Sun, 3 Jan 2021 22:27:44 +0100 Subject: [PATCH 4/5] Simplify test code Provide a unified fixture for all integration test cases that avoids repeated code. --- test_passgithelper.py | 458 ++++++++++++++++++------------------------ 1 file changed, 200 insertions(+), 258 deletions(-) diff --git a/test_passgithelper.py b/test_passgithelper.py index 22c31ef..334c992 100644 --- a/test_passgithelper.py +++ b/test_passgithelper.py @@ -1,6 +1,7 @@ import configparser +from dataclasses import dataclass import io -from typing import Any, Optional, Sequence, Text +from typing import Any, Iterable, Optional, Sequence, Text import pytest from pytest_mock import MockFixture @@ -8,10 +9,31 @@ import passgithelper +@dataclass +class HelperConfig: + xdg_dir: Optional[str] + request: str + entry_data: bytes + entry_name: Optional[str] = None + + @pytest.fixture() -def _xdg_dir(request: Any, mocker: MockFixture) -> None: +def _helper_config(mocker: MockFixture, request: Any) -> Iterable[None]: xdg_mock = mocker.patch("xdg.BaseDirectory.load_first_config") - xdg_mock.return_value = request.param + xdg_mock.return_value = request.param.xdg_dir + + mocker.patch("sys.stdin.readlines").return_value = io.StringIO( + request.param.request + ) + + subprocess_mock = mocker.patch("subprocess.check_output") + subprocess_mock.return_value = request.param.entry_data + + yield + + if request.param.entry_name is not None: + subprocess_mock.assert_called_once() + subprocess_mock.assert_called_with(["pass", "show", request.param.entry_name]) def test_handle_skip_nothing(monkeypatch: Any) -> None: @@ -109,22 +131,34 @@ def test_smoke(self) -> None: @pytest.mark.parametrize( - "_xdg_dir", - [None], + "_helper_config", + [ + HelperConfig( + None, + "", + b"ignored", + ), + ], indirect=True, ) -@pytest.mark.usefixtures("_xdg_dir") +@pytest.mark.usefixtures("_helper_config") def test_parse_mapping_file_missing() -> None: with pytest.raises(RuntimeError): passgithelper.parse_mapping(None) @pytest.mark.parametrize( - "_xdg_dir", - ["test_data/smoke"], + "_helper_config", + [ + HelperConfig( + "test_data/smoke", + "", + b"ignored", + ), + ], indirect=True, ) -@pytest.mark.usefixtures("_xdg_dir") +@pytest.mark.usefixtures("_helper_config") def test_parse_mapping_from_xdg() -> None: config = passgithelper.parse_mapping(None) assert "mytest.com" in config @@ -147,364 +181,272 @@ def test_skip(self, monkeypatch: Any, capsys: Any) -> None: assert not err @pytest.mark.parametrize( - "_xdg_dir", - ["test_data/smoke"], - indirect=True, - ) - @pytest.mark.usefixtures("_xdg_dir") - def test_smoke_resolve( - self, monkeypatch: Any, mocker: MockFixture, capsys: Any - ) -> None: - monkeypatch.setattr( - "sys.stdin", - io.StringIO( + "_helper_config", + [ + HelperConfig( + "test_data/smoke", """ protocol=https -host=mytest.com""" +host=mytest.com""", + b"narf", + "dev/mytest", ), - ) - subprocess_mock = mocker.patch("subprocess.check_output") - subprocess_mock.return_value = b"narf" - + ], + indirect=True, + ) + @pytest.mark.usefixtures("_helper_config") + def test_smoke_resolve(self, capsys: Any) -> None: passgithelper.main(["get"]) - subprocess_mock.assert_called_once() - subprocess_mock.assert_called_with(["pass", "show", "dev/mytest"]) - out, _ = capsys.readouterr() assert out == "password=narf\n" @pytest.mark.parametrize( - "_xdg_dir", - ["test_data/smoke"], - indirect=True, - ) - @pytest.mark.usefixtures("_xdg_dir") - def test_path_used_if_present_fails(self, monkeypatch: Any) -> None: - monkeypatch.setattr( - "sys.stdin", - io.StringIO( + "_helper_config", + [ + HelperConfig( + "test_data/smoke", """ protocol=https host=mytest.com -path=/foo/bar.git""" +path=/foo/bar.git""", + b"ignored", ), - ) - + ], + indirect=True, + ) + @pytest.mark.usefixtures("_helper_config") + def test_path_used_if_present_fails(self) -> None: with pytest.raises(ValueError, match="No mapping section"): passgithelper.main(["get"]) @pytest.mark.parametrize( - "_xdg_dir", - ["test_data/with-path"], - indirect=True, - ) - @pytest.mark.usefixtures("_xdg_dir") - def test_path_used_if_present( - self, monkeypatch: Any, mocker: MockFixture, capsys: Any - ) -> None: - monkeypatch.setattr( - "sys.stdin", - io.StringIO( + "_helper_config", + [ + HelperConfig( + "test_data/with-path", """ protocol=https host=mytest.com -path=subpath/bar.git""" +path=subpath/bar.git""", + b"narf", + "dev/mytest", ), - ) - - subprocess_mock = mocker.patch("subprocess.check_output") - subprocess_mock.return_value = b"narf" - + ], + indirect=True, + ) + @pytest.mark.usefixtures("_helper_config") + def test_path_used_if_present(self, capsys: Any) -> None: passgithelper.main(["get"]) - subprocess_mock.assert_called_once() - subprocess_mock.assert_called_with(["pass", "show", "dev/mytest"]) - out, _ = capsys.readouterr() assert out == "password=narf\n" @pytest.mark.parametrize( - "_xdg_dir", - ["test_data/wildcard"], - indirect=True, - ) - @pytest.mark.usefixtures("_xdg_dir") - def test_wildcard_matching( - self, monkeypatch: Any, mocker: MockFixture, capsys: Any - ) -> None: - monkeypatch.setattr( - "sys.stdin", - io.StringIO( + "_helper_config", + [ + HelperConfig( + "test_data/wildcard", """ protocol=https host=wildcard.com username=wildcard -path=subpath/bar.git""" +path=subpath/bar.git""", + b"narf-wildcard", + "dev/wildcard.com/wildcard", ), - ) - - subprocess_mock = mocker.patch("subprocess.check_output") - subprocess_mock.return_value = b"narf-wildcard" - + ], + indirect=True, + ) + @pytest.mark.usefixtures("_helper_config") + def test_wildcard_matching(self, capsys: Any) -> None: passgithelper.main(["get"]) - subprocess_mock.assert_called_once() - subprocess_mock.assert_called_with( - ["pass", "show", "dev/wildcard.com/wildcard"] - ) - out, _ = capsys.readouterr() assert out == "password=narf-wildcard\n" @pytest.mark.parametrize( - "_xdg_dir", - ["test_data/with-username"], - indirect=True, - ) - @pytest.mark.usefixtures("_xdg_dir") - def test_username_provided( - self, monkeypatch: Any, mocker: MockFixture, capsys: Any - ) -> None: - monkeypatch.setattr( - "sys.stdin", - io.StringIO( + "_helper_config", + [ + HelperConfig( + "test_data/with-username", """ -protocol=https -host=plainline.com""" +host=plainline.com""", + b"password\nusername", + "dev/plainline", ), - ) - - subprocess_mock = mocker.patch("subprocess.check_output") - subprocess_mock.return_value = b"password\nusername" - + ], + indirect=True, + ) + @pytest.mark.usefixtures("_helper_config") + def test_username_provided(self, capsys: Any) -> None: passgithelper.main(["get"]) - subprocess_mock.assert_called_once() - subprocess_mock.assert_called_with(["pass", "show", "dev/plainline"]) - out, _ = capsys.readouterr() assert out == "password=password\nusername=username\n" @pytest.mark.parametrize( - "_xdg_dir", - ["test_data/with-username"], - indirect=True, - ) - @pytest.mark.usefixtures("_xdg_dir") - def test_username_skipped_if_provided( - self, monkeypatch: Any, mocker: MockFixture, capsys: Any - ) -> None: - monkeypatch.setattr( - "sys.stdin", - io.StringIO( + "_helper_config", + [ + HelperConfig( + "test_data/with-username", """ -protocol=https host=plainline.com -username=narf""" +username=narf""", + b"password\nusername", + "dev/plainline", ), - ) - - subprocess_mock = mocker.patch("subprocess.check_output") - subprocess_mock.return_value = b"password\nusername" - + ], + indirect=True, + ) + @pytest.mark.usefixtures("_helper_config") + def test_username_skipped_if_provided(self, capsys: Any) -> None: passgithelper.main(["get"]) - subprocess_mock.assert_called_once() - subprocess_mock.assert_called_with(["pass", "show", "dev/plainline"]) - out, _ = capsys.readouterr() assert out == "password=password\n" @pytest.mark.parametrize( - "_xdg_dir", - ["test_data/with-username"], - indirect=True, - ) - @pytest.mark.usefixtures("_xdg_dir") - def test_custom_mapping_used( - self, monkeypatch: Any, mocker: MockFixture, capsys: Any - ) -> None: - # this would fail for the default file from with-username - monkeypatch.setattr( - "sys.stdin", - io.StringIO( + "_helper_config", + [ + HelperConfig( + "test_data/with-username", """ protocol=https -host=mytest.com""" +host=mytest.com""", + b"narf", + "dev/mytest", ), - ) - subprocess_mock = mocker.patch("subprocess.check_output") - subprocess_mock.return_value = b"narf" - + ], + indirect=True, + ) + @pytest.mark.usefixtures("_helper_config") + def test_custom_mapping_used(self, capsys: Any) -> None: + # this would fail for the default file from with-username passgithelper.main(["-m", "test_data/smoke/git-pass-mapping.ini", "get"]) - subprocess_mock.assert_called_once() - subprocess_mock.assert_called_with(["pass", "show", "dev/mytest"]) - out, _ = capsys.readouterr() assert out == "password=narf\n" @pytest.mark.parametrize( - "_xdg_dir", - ["test_data/with-username-skip"], - indirect=True, - ) - @pytest.mark.usefixtures("_xdg_dir") - def test_prefix_skipping( - self, monkeypatch: Any, mocker: MockFixture, capsys: Any - ) -> None: - monkeypatch.setattr( - "sys.stdin", - io.StringIO( + "_helper_config", + [ + HelperConfig( + "test_data/with-username-skip", """ protocol=https -host=mytest.com""" +host=mytest.com""", + b"password: xyz\nuser: tester", + "dev/mytest", ), - ) - subprocess_mock = mocker.patch("subprocess.check_output") - subprocess_mock.return_value = b"password: xyz\nuser: tester" - + ], + indirect=True, + ) + @pytest.mark.usefixtures("_helper_config") + def test_prefix_skipping(self, capsys: Any) -> None: passgithelper.main(["get"]) - subprocess_mock.assert_called_once() - subprocess_mock.assert_called_with(["pass", "show", "dev/mytest"]) - out, _ = capsys.readouterr() assert out == "password=xyz\nusername=tester\n" @pytest.mark.parametrize( - "_xdg_dir", - ["test_data/unknown-username-extractor"], - indirect=True, - ) - @pytest.mark.usefixtures("_xdg_dir") - def test_select_unknown_extractor(self, monkeypatch: Any, capsys: Any) -> None: - monkeypatch.setattr( - "sys.stdin", - io.StringIO( + "_helper_config", + [ + HelperConfig( + "test_data/unknown-username-extractor", """ protocol=https -host=mytest.com""" +host=mytest.com""", + b"ignored", ), - ) - + ], + indirect=True, + ) + @pytest.mark.usefixtures("_helper_config") + def test_select_unknown_extractor(self) -> None: with pytest.raises(KeyError): passgithelper.main(["get"]) @pytest.mark.parametrize( - "_xdg_dir", - ["test_data/regex-extraction"], - indirect=True, - ) - @pytest.mark.usefixtures("_xdg_dir") - def test_regex_username_selection( - self, monkeypatch: Any, mocker: MockFixture, capsys: Any - ) -> None: - monkeypatch.setattr( - "sys.stdin", - io.StringIO( + "_helper_config", + [ + HelperConfig( + "test_data/regex-extraction", """ protocol=https -host=mytest.com""" +host=mytest.com""", + b"xyz\nsomeline\nmyuser: tester\n" b"morestuff\nmyuser: ignore", + "dev/mytest", ), - ) - subprocess_mock = mocker.patch("subprocess.check_output") - subprocess_mock.return_value = ( - b"xyz\nsomeline\nmyuser: tester\n" b"morestuff\nmyuser: ignore" - ) - + ], + indirect=True, + ) + @pytest.mark.usefixtures("_helper_config") + def test_regex_username_selection(self, capsys: Any) -> None: passgithelper.main(["get"]) - subprocess_mock.assert_called_once() - subprocess_mock.assert_called_with(["pass", "show", "dev/mytest"]) - out, _ = capsys.readouterr() assert out == "password=xyz\nusername=tester\n" @pytest.mark.parametrize( - "_xdg_dir", - ["test_data/entry-name-extraction"], - indirect=True, - ) - @pytest.mark.usefixtures("_xdg_dir") - def test_entry_name_is_user( - self, monkeypatch: Any, mocker: MockFixture, capsys: Any - ) -> None: - monkeypatch.setattr( - "sys.stdin", - io.StringIO( + "_helper_config", + [ + HelperConfig( + "test_data/entry-name-extraction", """ protocol=https -host=mytest.com""" +host=mytest.com""", + b"xyz", + "dev/mytest/myuser", ), - ) - subprocess_mock = mocker.patch("subprocess.check_output") - subprocess_mock.return_value = b"xyz" - + ], + indirect=True, + ) + @pytest.mark.usefixtures("_helper_config") + def test_entry_name_is_user(self, capsys: Any) -> None: passgithelper.main(["get"]) - subprocess_mock.assert_called_once() - subprocess_mock.assert_called_with(["pass", "show", "dev/mytest/myuser"]) - out, _ = capsys.readouterr() assert out == "password=xyz\nusername=myuser\n" @pytest.mark.parametrize( - "_xdg_dir", - ["test_data/with-encoding"], - indirect=True, - ) - @pytest.mark.usefixtures("_xdg_dir") - def test_uses_configured_encoding( - self, monkeypatch: Any, mocker: MockFixture, capsys: Any - ) -> None: - monkeypatch.setattr( - "sys.stdin", - io.StringIO( + "_helper_config", + [ + HelperConfig( + "test_data/with-encoding", """ protocol=https -host=mytest.com""" +host=mytest.com""", + "täßt".encode("LATIN1"), + "dev/mytest", ), - ) - subprocess_mock = mocker.patch("subprocess.check_output") - password = "täßt" - subprocess_mock.return_value = password.encode("LATIN1") - + ], + indirect=True, + ) + @pytest.mark.usefixtures("_helper_config") + def test_uses_configured_encoding(self, capsys: Any) -> None: passgithelper.main(["get"]) - subprocess_mock.assert_called_once() - subprocess_mock.assert_called_with(["pass", "show", "dev/mytest"]) - out, _ = capsys.readouterr() - assert out == f"password={password}\n" + assert out == f"password=täßt\n" @pytest.mark.parametrize( - "_xdg_dir", - ["test_data/smoke"], - indirect=True, - ) - @pytest.mark.usefixtures("_xdg_dir") - def test_uses_utf8_by_default( - self, mocker: MockFixture, monkeypatch: Any, capsys: Any - ) -> None: - monkeypatch.setattr( - "sys.stdin", - io.StringIO( + "_helper_config", + [ + HelperConfig( + "test_data/smoke", """ protocol=https -host=mytest.com""" +host=mytest.com""", + "täßt".encode("UTF-8"), + "dev/mytest", ), - ) - - subprocess_mock = mocker.patch("subprocess.check_output") - password = "täßt" - subprocess_mock.return_value = password.encode("UTF-8") - + ], + indirect=True, + ) + @pytest.mark.usefixtures("_helper_config") + def test_uses_utf8_by_default(self, capsys: Any) -> None: passgithelper.main(["get"]) - subprocess_mock.assert_called_once() - subprocess_mock.assert_called_with(["pass", "show", "dev/mytest"]) - out, _ = capsys.readouterr() - assert out == f"password={password}\n" + assert out == "password=täßt\n" From 37e165b6b92fcb71b9ea2b679e607dab6a0dfa67 Mon Sep 17 00:00:00 2001 From: Johannes Wienke Date: Sun, 3 Jan 2021 22:30:52 +0100 Subject: [PATCH 5/5] Remove useless return statement --- passgithelper.py | 1 - 1 file changed, 1 deletion(-) diff --git a/passgithelper.py b/passgithelper.py index cddfc6b..dfce6eb 100755 --- a/passgithelper.py +++ b/passgithelper.py @@ -408,7 +408,6 @@ def get_password( print("password={password}".format(password=password)) # noqa: T001 if "username" not in request and username: print("username={username}".format(username=username)) # noqa: T001 - return def handle_skip() -> None: