qute-lastpass: Code-review changes
This commit is contained in:
parent
ab7da95441
commit
e6891388eb
|
|
@ -40,11 +40,13 @@ you decide to submit a crash report!"""
|
|||
import argparse
|
||||
import enum
|
||||
import functools
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
import json
|
||||
|
||||
import tldextract
|
||||
|
||||
argument_parser = argparse.ArgumentParser(
|
||||
|
|
@ -80,7 +82,7 @@ def qute_command(command):
|
|||
fifo.flush()
|
||||
|
||||
def pass_(domain, encoding):
|
||||
domain = domain.replace('.', '\\.')
|
||||
domain = re.escape(domain)
|
||||
args = ['lpass', 'show', '-x', '-j', '-G', '\\b{:s}'.format(domain)]
|
||||
process = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
|
||||
|
|
@ -104,7 +106,7 @@ def fake_key_raw(text):
|
|||
|
||||
for character in text:
|
||||
# Escape all characters by default, space requires special handling
|
||||
sequence = sequence + ('" "' if character == ' ' else '\\{}'.format(character))
|
||||
sequence += ('" "' if character == ' ' else '\\{}'.format(character))
|
||||
qute_command('fake-key {}'.format(sequence))
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014-2020 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
# Copyright 2020 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
|
|
@ -19,17 +19,19 @@
|
|||
|
||||
"""Tests for misc.userscripts.qute-lastpass."""
|
||||
|
||||
import os
|
||||
import json
|
||||
import subprocess
|
||||
import pathlib
|
||||
from importlib.machinery import SourceFileLoader
|
||||
from importlib.util import spec_from_loader, module_from_spec
|
||||
from unittest.mock import MagicMock, call
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import ANY, call
|
||||
|
||||
import attr
|
||||
import pytest
|
||||
|
||||
# qute-lastpass violates naming convention and does not have .py extension
|
||||
dirname = os.path.dirname(__file__)
|
||||
script_path = os.path.join(
|
||||
dirname, "../../../../misc/userscripts/qute-lastpass")
|
||||
repo_root = pathlib.Path(__file__).resolve().parents[4]
|
||||
script_path = str(repo_root / 'misc' / 'userscripts' / 'qute-lastpass')
|
||||
spec = spec_from_loader("qute_lastpass", SourceFileLoader(
|
||||
"qute_lastpass",
|
||||
script_path))
|
||||
|
|
@ -47,24 +49,32 @@ default_lpass_match = [
|
|||
]
|
||||
|
||||
|
||||
def get_response_mock(stdout='', stderr=''):
|
||||
response = MagicMock()
|
||||
response.stdout = stdout.encode()
|
||||
response.stderr = stderr.encode()
|
||||
|
||||
return response
|
||||
@attr.s
|
||||
class FakeOutput:
|
||||
stdout = attr.ib(default='', converter=str.encode)
|
||||
stderr = attr.ib(default='', converter=str.encode)
|
||||
|
||||
|
||||
def setup_subprocess_mock(mocker, stdout='', stderr=''):
|
||||
mocker.patch('subprocess.run')
|
||||
@pytest.fixture
|
||||
def subprocess_mock(mocker):
|
||||
return mocker.patch('subprocess.run')
|
||||
|
||||
subprocess.run.return_value = get_response_mock(stdout, stderr)
|
||||
|
||||
@pytest.fixture
|
||||
def qutecommand_mock(mocker):
|
||||
return mocker.patch.object(qute_lastpass, 'qute_command')
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def stderr_mock(mocker):
|
||||
return mocker.patch.object(qute_lastpass, 'stderr')
|
||||
|
||||
|
||||
# Default arguments passed to qute-lastpass
|
||||
def get_arguments_mock(url):
|
||||
arguments = MagicMock()
|
||||
arguments.url = url
|
||||
@pytest.fixture
|
||||
def arguments_mock():
|
||||
arguments = SimpleNamespace()
|
||||
arguments.url = ''
|
||||
arguments.dmenu_invocation = 'rofi -dmenu'
|
||||
arguments.insert_mode = True
|
||||
arguments.io_encoding = 'UTF-8'
|
||||
|
|
@ -78,50 +88,47 @@ def get_arguments_mock(url):
|
|||
class TestQuteLastPassComponents:
|
||||
"""Test qute-lastpass components."""
|
||||
|
||||
def test_fake_key_raw(self):
|
||||
def test_fake_key_raw(self, qutecommand_mock):
|
||||
"""Test if fake_key_raw properly escapes characters."""
|
||||
qute_lastpass.qute_command = MagicMock()
|
||||
|
||||
qute_lastpass.fake_key_raw('john.doe@example.com ')
|
||||
|
||||
# pylint: disable=line-too-long
|
||||
qute_lastpass.qute_command.assert_called_once_with(
|
||||
qutecommand_mock.assert_called_once_with(
|
||||
'fake-key \\j\\o\\h\\n\\.\\d\\o\\e\\@\\e\\x\\a\\m\\p\\l\\e\\.\\c\\o\\m" "'
|
||||
)
|
||||
|
||||
def test_dmenu(self, mocker):
|
||||
def test_dmenu(self, subprocess_mock):
|
||||
"""Test if dmenu command receives properly formatted lpass entries."""
|
||||
entries = [
|
||||
"1234 | example.com | https://www.example.com | john.doe@example.com",
|
||||
"2345 | example2.com | https://www.example2.com | jane.doe@example.com",
|
||||
]
|
||||
|
||||
setup_subprocess_mock(mocker, entries[1])
|
||||
subprocess_mock.return_value = FakeOutput(stdout=entries[1])
|
||||
|
||||
selected = qute_lastpass.dmenu(entries, 'rofi -dmenu', 'UTF-8')
|
||||
|
||||
# pylint: disable=no-member
|
||||
subprocess.run.assert_called_once_with(
|
||||
subprocess_mock.assert_called_once_with(
|
||||
['rofi', '-dmenu'],
|
||||
input='\n'.join(entries).encode(),
|
||||
stdout=mocker.ANY)
|
||||
stdout=ANY)
|
||||
|
||||
assert selected == entries[1]
|
||||
|
||||
def test_pass_subprocess_args(self, mocker):
|
||||
def test_pass_subprocess_args(self, subprocess_mock):
|
||||
"""Test if pass_ calls subprocess with correct arguments."""
|
||||
setup_subprocess_mock(mocker, '[{}]')
|
||||
subprocess_mock.return_value = FakeOutput(stdout='[{}]')
|
||||
|
||||
qute_lastpass.pass_('example.com', 'utf-8')
|
||||
|
||||
# pylint: disable=no-member
|
||||
subprocess.run.assert_called_once_with(
|
||||
subprocess_mock.assert_called_once_with(
|
||||
['lpass', 'show', '-x', '-j', '-G', '\\bexample\\.com'],
|
||||
stdout=mocker.ANY, stderr=mocker.ANY)
|
||||
stdout=ANY, stderr=ANY)
|
||||
|
||||
def test_pass_returns_candidates(self, mocker):
|
||||
def test_pass_returns_candidates(self, subprocess_mock):
|
||||
"""Test if pass_ returns expected lpass site entry."""
|
||||
setup_subprocess_mock(mocker, json.dumps(default_lpass_match))
|
||||
subprocess_mock.return_value = FakeOutput(
|
||||
stdout=json.dumps(default_lpass_match))
|
||||
|
||||
response = qute_lastpass.pass_('www.example.com', 'utf-8')
|
||||
assert response[1] == ''
|
||||
|
|
@ -131,20 +138,20 @@ class TestQuteLastPassComponents:
|
|||
assert len(candidates) == 1
|
||||
assert candidates[0] == default_lpass_match[0]
|
||||
|
||||
def test_pass_no_accounts(self, mocker):
|
||||
def test_pass_no_accounts(self, subprocess_mock):
|
||||
"""Test if pass_ handles no accounts as an empty lpass result."""
|
||||
error_message = 'Error: Could not find specified account(s).'
|
||||
setup_subprocess_mock(mocker, stderr=error_message)
|
||||
subprocess_mock.return_value = FakeOutput(stderr=error_message)
|
||||
|
||||
response = qute_lastpass.pass_('www.example.com', 'utf-8')
|
||||
assert response[0] == []
|
||||
assert response[1] == ''
|
||||
|
||||
def test_pass_returns_error(self, mocker):
|
||||
def test_pass_returns_error(self, subprocess_mock):
|
||||
"""Test if pass_ returns error from lpass."""
|
||||
# pylint: disable=line-too-long
|
||||
error_message = 'Error: Could not find decryption key. Perhaps you need to login with `lpass login`.'
|
||||
setup_subprocess_mock(mocker, stderr=error_message)
|
||||
subprocess_mock.return_value = FakeOutput(stderr=error_message)
|
||||
|
||||
response = qute_lastpass.pass_('www.example.com', 'utf-8')
|
||||
assert response[0] == []
|
||||
|
|
@ -154,86 +161,88 @@ class TestQuteLastPassComponents:
|
|||
class TestQuteLastPassMain:
|
||||
"""Test qute-lastpass main."""
|
||||
|
||||
def test_main_happy_path(self, mocker):
|
||||
def test_main_happy_path(self, subprocess_mock, arguments_mock,
|
||||
qutecommand_mock):
|
||||
"""Test sending username/password to qutebrowser on *single* match."""
|
||||
setup_subprocess_mock(mocker, json.dumps(default_lpass_match))
|
||||
qute_lastpass.qute_command = MagicMock()
|
||||
subprocess_mock.return_value = FakeOutput(
|
||||
stdout=json.dumps(default_lpass_match))
|
||||
|
||||
arguments = get_arguments_mock(default_lpass_match[0]['url'])
|
||||
exit_code = qute_lastpass.main(arguments)
|
||||
arguments_mock.url = default_lpass_match[0]['url']
|
||||
exit_code = qute_lastpass.main(arguments_mock)
|
||||
|
||||
assert exit_code == qute_lastpass.ExitCodes.SUCCESS
|
||||
|
||||
qute_lastpass.qute_command.assert_has_calls([
|
||||
qutecommand_mock.assert_has_calls([
|
||||
call('fake-key \\f\\a\\k\\e\\@\\f\\a\\k\\e\\.\\c\\o\\m'),
|
||||
call('fake-key <Tab>'),
|
||||
call('fake-key \\f\\o\\o\\b\\a\\r'),
|
||||
call('enter-mode insert')
|
||||
])
|
||||
|
||||
def test_main_no_candidates(self, mocker):
|
||||
def test_main_no_candidates(self, subprocess_mock, arguments_mock,
|
||||
stderr_mock,
|
||||
qutecommand_mock):
|
||||
"""Test correct exit code and message returned on no entries."""
|
||||
error_message = 'Error: Could not find specified account(s).'
|
||||
setup_subprocess_mock(mocker, stderr=error_message)
|
||||
subprocess_mock.return_value = FakeOutput(stderr=error_message)
|
||||
|
||||
qute_lastpass.stderr = MagicMock()
|
||||
qute_lastpass.qute_command = MagicMock()
|
||||
|
||||
arguments = get_arguments_mock(default_lpass_match[0]['url'])
|
||||
exit_code = qute_lastpass.main(arguments)
|
||||
arguments_mock.url = default_lpass_match[0]['url']
|
||||
exit_code = qute_lastpass.main(arguments_mock)
|
||||
|
||||
assert exit_code == qute_lastpass.ExitCodes.NO_PASS_CANDIDATES
|
||||
qute_lastpass.stderr.assert_called_with(
|
||||
stderr_mock.assert_called_with(
|
||||
"No pass candidates for URL 'https://www.example.com' found!")
|
||||
qute_lastpass.qute_command.assert_not_called()
|
||||
qutecommand_mock.assert_not_called()
|
||||
|
||||
def test_main_lpass_failure(self, mocker):
|
||||
def test_main_lpass_failure(self, subprocess_mock, arguments_mock,
|
||||
stderr_mock,
|
||||
qutecommand_mock):
|
||||
"""Test correct exit code and message on lpass failure."""
|
||||
# pylint: disable=line-too-long
|
||||
error_message = 'Error: Could not find decryption key. Perhaps you need to login with `lpass login`.'
|
||||
setup_subprocess_mock(mocker, stderr=error_message)
|
||||
subprocess_mock.return_value = FakeOutput(stderr=error_message)
|
||||
|
||||
qute_lastpass.stderr = MagicMock()
|
||||
qute_lastpass.qute_command = MagicMock()
|
||||
|
||||
arguments = get_arguments_mock(default_lpass_match[0]['url'])
|
||||
exit_code = qute_lastpass.main(arguments)
|
||||
arguments_mock.url = default_lpass_match[0]['url']
|
||||
exit_code = qute_lastpass.main(arguments_mock)
|
||||
|
||||
assert exit_code == qute_lastpass.ExitCodes.FAILURE
|
||||
# pylint: disable=line-too-long
|
||||
qute_lastpass.stderr.assert_called_with(
|
||||
stderr_mock.assert_called_with(
|
||||
"LastPass CLI returned for www.example.com - Error: Could not find decryption key. Perhaps you need to login with `lpass login`.")
|
||||
qute_lastpass.qute_command.assert_not_called()
|
||||
qutecommand_mock.assert_not_called()
|
||||
|
||||
def test_main_username_only_flag(self, mocker):
|
||||
def test_main_username_only_flag(self, subprocess_mock, arguments_mock,
|
||||
qutecommand_mock):
|
||||
"""Test if --username-only flag sends username only."""
|
||||
setup_subprocess_mock(mocker, json.dumps(default_lpass_match))
|
||||
qute_lastpass.qute_command = MagicMock()
|
||||
subprocess_mock.return_value = FakeOutput(
|
||||
stdout=json.dumps(default_lpass_match))
|
||||
|
||||
arguments = get_arguments_mock(default_lpass_match[0]['url'])
|
||||
arguments.username_only = True
|
||||
qute_lastpass.main(arguments)
|
||||
arguments_mock.url = default_lpass_match[0]['url']
|
||||
arguments_mock.username_only = True
|
||||
qute_lastpass.main(arguments_mock)
|
||||
|
||||
qute_lastpass.qute_command.assert_has_calls([
|
||||
qutecommand_mock.assert_has_calls([
|
||||
call('fake-key \\f\\a\\k\\e\\@\\f\\a\\k\\e\\.\\c\\o\\m'),
|
||||
call('enter-mode insert')
|
||||
])
|
||||
|
||||
def test_main_password_only_flag(self, mocker):
|
||||
def test_main_password_only_flag(self, subprocess_mock, arguments_mock,
|
||||
qutecommand_mock):
|
||||
"""Test if --password-only flag sends password only."""
|
||||
setup_subprocess_mock(mocker, json.dumps(default_lpass_match))
|
||||
qute_lastpass.qute_command = MagicMock()
|
||||
subprocess_mock.return_value = FakeOutput(
|
||||
stdout=json.dumps(default_lpass_match))
|
||||
|
||||
arguments = get_arguments_mock(default_lpass_match[0]['url'])
|
||||
arguments.password_only = True
|
||||
qute_lastpass.main(arguments)
|
||||
arguments_mock.url = default_lpass_match[0]['url']
|
||||
arguments_mock.password_only = True
|
||||
qute_lastpass.main(arguments_mock)
|
||||
|
||||
qute_lastpass.qute_command.assert_has_calls([
|
||||
qutecommand_mock.assert_has_calls([
|
||||
call('fake-key \\f\\o\\o\\b\\a\\r'),
|
||||
call('enter-mode insert')
|
||||
])
|
||||
|
||||
def test_main_multiple_candidates(self, mocker):
|
||||
def test_main_multiple_candidates(self, subprocess_mock, arguments_mock,
|
||||
qutecommand_mock):
|
||||
"""Test dmenu-invocation when lpass returns multiple candidates."""
|
||||
multiple_matches = default_lpass_match.copy()
|
||||
multiple_matches.append(
|
||||
|
|
@ -246,30 +255,27 @@ class TestQuteLastPassMain:
|
|||
}
|
||||
)
|
||||
|
||||
mocker.patch('subprocess.run')
|
||||
lpass_response = FakeOutput(stdout=json.dumps(multiple_matches))
|
||||
dmenu_response = FakeOutput(
|
||||
stdout='23456 | Sites/www.example.com | https://www.example.com | john.doe@fake.com')
|
||||
|
||||
lpass_response = get_response_mock(json.dumps(multiple_matches))
|
||||
dmenu_response = get_response_mock(
|
||||
'23456 | Sites/www.example.com | https://www.example.com | john.doe@fake.com')
|
||||
subprocess_mock.side_effect = [lpass_response, dmenu_response]
|
||||
|
||||
subprocess.run.side_effect = [lpass_response, dmenu_response]
|
||||
qute_lastpass.qute_command = MagicMock()
|
||||
|
||||
arguments = get_arguments_mock(multiple_matches[0]['url'])
|
||||
exit_code = qute_lastpass.main(arguments)
|
||||
arguments_mock.url = multiple_matches[0]['url']
|
||||
exit_code = qute_lastpass.main(arguments_mock)
|
||||
|
||||
assert exit_code == qute_lastpass.ExitCodes.SUCCESS
|
||||
|
||||
# pylint: disable=no-member,line-too-long
|
||||
subprocess.run.assert_has_calls([
|
||||
# pylint: disable=line-too-long
|
||||
subprocess_mock.assert_has_calls([
|
||||
call(['lpass', 'show', '-x', '-j', '-G', '\\bwww\\.example\\.com'],
|
||||
stdout=mocker.ANY, stderr=mocker.ANY),
|
||||
stdout=ANY, stderr=ANY),
|
||||
call(['rofi', '-dmenu'],
|
||||
input=b'12345 | www.example.com | https://www.example.com | fake@fake.com\n23456 | Sites/www.example.com | https://www.example.com | john.doe@fake.com',
|
||||
stdout=mocker.ANY)
|
||||
stdout=ANY)
|
||||
])
|
||||
|
||||
qute_lastpass.qute_command.assert_has_calls([
|
||||
qutecommand_mock.assert_has_calls([
|
||||
call(
|
||||
'fake-key \\j\\o\\h\\n\\.\\d\\o\\e\\@\\f\\a\\k\\e\\.\\c\\o\\m'),
|
||||
call('fake-key <Tab>'),
|
||||
|
|
@ -277,7 +283,8 @@ class TestQuteLastPassMain:
|
|||
call('enter-mode insert')
|
||||
])
|
||||
|
||||
def test_main_merge_candidates(self, mocker):
|
||||
def test_main_merge_candidates(self, subprocess_mock, arguments_mock,
|
||||
qutecommand_mock):
|
||||
"""Test merge of multiple responses from lpass."""
|
||||
fqdn_matches = default_lpass_match.copy()
|
||||
fqdn_matches.append(
|
||||
|
|
@ -307,44 +314,41 @@ class TestQuteLastPassMain:
|
|||
}
|
||||
]
|
||||
|
||||
mocker.patch('subprocess.run')
|
||||
|
||||
fqdn_response = get_response_mock(json.dumps(fqdn_matches))
|
||||
domain_response = get_response_mock(json.dumps(domain_matches))
|
||||
no_response = get_response_mock(
|
||||
fqdn_response = FakeOutput(stdout=json.dumps(fqdn_matches))
|
||||
domain_response = FakeOutput(stdout=json.dumps(domain_matches))
|
||||
no_response = FakeOutput(
|
||||
stderr='Error: Could not find specified account(s).')
|
||||
dmenu_response = get_response_mock(
|
||||
'23456 | Sites/www.example.com | https://www.example.com | john.doe@fake.com')
|
||||
dmenu_response = FakeOutput(
|
||||
stdout='23456 | Sites/www.example.com | https://www.example.com | john.doe@fake.com')
|
||||
|
||||
# lpass command will return results for search against
|
||||
# www.example.com, example.com, but not wwwexample.com and its ipv4
|
||||
subprocess.run.side_effect = [fqdn_response, domain_response,
|
||||
no_response, no_response,
|
||||
dmenu_response]
|
||||
qute_lastpass.qute_command = MagicMock()
|
||||
subprocess_mock.side_effect = [fqdn_response, domain_response,
|
||||
no_response, no_response,
|
||||
dmenu_response]
|
||||
|
||||
arguments = get_arguments_mock(fqdn_matches[0]['url'])
|
||||
arguments.merge_candidates = True
|
||||
exit_code = qute_lastpass.main(arguments)
|
||||
arguments_mock.url = fqdn_matches[0]['url']
|
||||
arguments_mock.merge_candidates = True
|
||||
exit_code = qute_lastpass.main(arguments_mock)
|
||||
|
||||
assert exit_code == qute_lastpass.ExitCodes.SUCCESS
|
||||
|
||||
# pylint: disable=no-member,line-too-long
|
||||
subprocess.run.assert_has_calls([
|
||||
# pylint: disable=line-too-long
|
||||
subprocess_mock.assert_has_calls([
|
||||
call(['lpass', 'show', '-x', '-j', '-G', '\\bwww\\.example\\.com'],
|
||||
stdout=mocker.ANY, stderr=mocker.ANY),
|
||||
stdout=ANY, stderr=ANY),
|
||||
call(['lpass', 'show', '-x', '-j', '-G', '\\bexample\\.com'],
|
||||
stdout=mocker.ANY, stderr=mocker.ANY),
|
||||
stdout=ANY, stderr=ANY),
|
||||
call(['lpass', 'show', '-x', '-j', '-G', '\\bwwwexample'],
|
||||
stdout=mocker.ANY, stderr=mocker.ANY),
|
||||
stdout=ANY, stderr=ANY),
|
||||
call(['lpass', 'show', '-x', '-j', '-G', '\\bexample'],
|
||||
stdout=mocker.ANY, stderr=mocker.ANY),
|
||||
stdout=ANY, stderr=ANY),
|
||||
call(['rofi', '-dmenu'],
|
||||
input=b'12345 | www.example.com | https://www.example.com | fake@fake.com\n23456 | Sites/www.example.com | https://www.example.com | john.doe@fake.com\n345 | example.com | https://example.com | joe.doe@fake.com\n456 | Sites/example.com | http://example.com | jane.doe@fake.com',
|
||||
stdout=mocker.ANY)
|
||||
stdout=ANY)
|
||||
])
|
||||
|
||||
qute_lastpass.qute_command.assert_has_calls([
|
||||
qutecommand_mock.assert_has_calls([
|
||||
call(
|
||||
'fake-key \\j\\o\\h\\n\\.\\d\\o\\e\\@\\f\\a\\k\\e\\.\\c\\o\\m'),
|
||||
call('fake-key <Tab>'),
|
||||
|
|
|
|||
Loading…
Reference in New Issue