This commit is contained in:
Jonathan Wren 2021-11-13 14:12:04 -08:00
parent 55dd9484c9
commit f7c12fbede
8 changed files with 165 additions and 125 deletions

1
.tool-versions Normal file
View file

@ -0,0 +1 @@
python 3.8.2

View file

@ -83,10 +83,11 @@ Feature: Multiple journals
these three eyes these three eyes
n n
Then we should see the message "Journal encrypted to features/journals/basic_onefile.journal" Then we should see the message "Journal encrypted to features/journals/basic_onefile.journal"
And the config should contain "encrypt: false" # multiple.yaml remains unchanged And the config should contain "encrypt: false"
Scenario: Don't overwrite main config when decrypting a journal in an alternate config Scenario: Don't overwrite main config when decrypting a journal in an alternate config
Given the config "editor_encrypted.yaml" exists Given the config "editor_encrypted.yaml" exists
And we use the config "basic_encrypted.yaml" And we use the config "basic_encrypted.yaml"
When we run "jrnl --cf editor_encrypted.yaml --decrypt" When we run "jrnl --cf editor_encrypted.yaml --decrypt"
Then the config should contain "encrypt: true" # basic_encrypted remains unchanged Then the config should contain "encrypt: true"

View file

@ -2,6 +2,7 @@ Feature: Encrypting and decrypting journals
Scenario: Decrypting a journal Scenario: Decrypting a journal
Given we use the config "encrypted.yaml" Given we use the config "encrypted.yaml"
# And we use the password "bad doggie no biscuit" if prompted
When we run "jrnl --decrypt" and enter "bad doggie no biscuit" When we run "jrnl --decrypt" and enter "bad doggie no biscuit"
Then we should see the message "Journal decrypted" Then we should see the message "Journal decrypted"
And the config for journal "default" should contain "encrypt: false" And the config for journal "default" should contain "encrypt: false"

View file

@ -8,17 +8,19 @@ import tempfile
from keyring import backend from keyring import backend
from keyring import errors from keyring import errors
from keyring import set_keyring
from pytest import fixture from pytest import fixture
from unittest.mock import patch
from .helpers import get_fixture
import toml import toml
from jrnl.config import load_config from jrnl.config import load_config
from jrnl.os_compat import split_args
# --- Keyring --- # # --- Keyring --- #
@fixture @fixture
def keyring(): def keyring():
set_keyring(NoKeyring()) return NoKeyring()
@fixture @fixture
@ -75,13 +77,90 @@ class FailedKeyring(backend.KeyringBackend):
# ----- Misc ----- # # ----- Misc ----- #
@fixture @fixture
def cli_run(): def cli_run(
return {"status": 0, "stdout": None, "stderr": None} mock_factories,
mock_args,
mock_is_tty,
mock_config_path,
mock_editor,
mock_user_input,
mock_overrides,
mock_password,
):
# Check if we need more mocks
mock_factories.update(mock_args)
mock_factories.update(mock_is_tty)
mock_factories.update(mock_overrides)
mock_factories.update(mock_editor)
mock_factories.update(mock_config_path)
mock_factories.update(mock_user_input)
mock_factories.update(mock_password)
return {
"status": 0,
"stdout": None,
"stderr": None,
"mocks": {},
"mock_factories": mock_factories,
}
@fixture @fixture
def mocks(): def mock_factories():
return dict() return {}
@fixture
def mock_args(cache_dir, request):
def _mock_args():
command = get_fixture(request, "command", "")
if cache_dir["exists"]:
command = command.format(cache_dir=cache_dir["path"])
args = split_args(command)
return patch("sys.argv", ["jrnl"] + args)
return {"args": _mock_args}
@fixture
def mock_is_tty(is_tty):
return {"is_tty": lambda: patch("sys.stdin.isatty", return_value=is_tty)}
@fixture
def mock_overrides(config_in_memory):
from jrnl.override import apply_overrides
def my_overrides(*args, **kwargs):
result = apply_overrides(*args, **kwargs)
config_in_memory["overrides"] = result
return result
return {
"overrides": lambda: patch(
"jrnl.jrnl.apply_overrides", side_effect=my_overrides
)
}
@fixture
def mock_config_path(request):
config_path = get_fixture(request, "config_path")
if not config_path:
return {}
return {
"config_path_install": lambda: patch(
"jrnl.install.get_config_path", return_value=config_path
),
"config_path_config": lambda: patch(
"jrnl.config.get_config_path", return_value=config_path
),
}
@fixture @fixture
@ -94,12 +173,6 @@ def working_dir(request):
return os.path.join(request.config.rootpath, "tests") return os.path.join(request.config.rootpath, "tests")
@fixture
def config_path(temp_dir):
os.chdir(temp_dir.name)
return temp_dir.name + "/jrnl.yaml"
@fixture @fixture
def toml_version(working_dir): def toml_version(working_dir):
pyproject = os.path.join(working_dir, "..", "pyproject.toml") pyproject = os.path.join(working_dir, "..", "pyproject.toml")
@ -108,8 +181,20 @@ def toml_version(working_dir):
@fixture @fixture
def password(): def mock_password(request):
return "" password = get_fixture(request, "password")
user_input = get_fixture(request, "user_input")
if password:
password = password.splitlines()
elif user_input:
password = user_input.splitlines()
if not password:
return {}
return {"getpass": lambda: patch("getpass.getpass", side_effect=password)}
@fixture @fixture
@ -127,19 +212,28 @@ def str_value():
return "" return ""
@fixture
def command():
return ""
@fixture @fixture
def should_not(): def should_not():
return False return False
@fixture @fixture
def user_input(): def mock_user_input(request, is_tty):
return "" user_input = get_fixture(request, "user_input", "")
user_input = user_input.splitlines() if is_tty else [user_input]
if not user_input:
return {}
return {
"stdin": lambda: patch("sys.stdin.read", side_effect=user_input),
"input": lambda: patch("builtins.input", side_effect=user_input),
}
@fixture
def is_tty(input_method):
assert input_method in ["", "enter", "pipe"]
return input_method != "pipe"
@fixture @fixture
@ -187,7 +281,7 @@ def editor_state():
@fixture @fixture
def editor(editor_state): def mock_editor(editor_state):
def _mock_editor(editor_command): def _mock_editor(editor_command):
tmpfile = editor_command[-1] tmpfile = editor_command[-1]
@ -203,4 +297,4 @@ def editor(editor_state):
file_content = f.read() file_content = f.read()
editor_state["tmpfile"]["content"] = file_content editor_state["tmpfile"]["content"] = file_content
return _mock_editor return {"editor": lambda: patch("subprocess.call", side_effect=_mock_editor)}

View file

@ -11,7 +11,6 @@ from unittest.mock import MagicMock
from unittest.mock import patch from unittest.mock import patch
from xml.etree import ElementTree from xml.etree import ElementTree
from keyring import set_keyring
from pytest_bdd import given from pytest_bdd import given
from pytest_bdd.parsers import parse from pytest_bdd.parsers import parse
@ -36,9 +35,8 @@ def we_enter_editor(editor_method, editor_input, editor_state):
editor_state["intent"] = {"method": file_method, "input": editor_input} editor_state["intent"] = {"method": file_method, "input": editor_input}
@given(parse('now is "<date_str>"'))
@given(parse('now is "{date_str}"')) @given(parse('now is "{date_str}"'))
def now_is_str(date_str, mocks): def now_is_str(date_str, mock_factories):
class DatetimeMagicMock(MagicMock): class DatetimeMagicMock(MagicMock):
# needed because jrnl does some reflection on datetime # needed because jrnl does some reflection on datetime
def __instancecheck__(self, subclass): def __instancecheck__(self, subclass):
@ -63,8 +61,8 @@ def now_is_str(date_str, mocks):
date_str_input, mocked_now() date_str_input, mocked_now()
) )
mocks["datetime"] = patch("datetime.datetime", new=datetime_mock) mock_factories["datetime"] = lambda: patch("datetime.datetime", new=datetime_mock)
mocks["calendar_parse"] = patch( mock_factories["calendar_parse"] = lambda: patch(
"jrnl.time.__get_pdt_calendar", return_value=calendar_mock "jrnl.time.__get_pdt_calendar", return_value=calendar_mock
) )
@ -73,13 +71,12 @@ def now_is_str(date_str, mocks):
@given(parse("we have a {keyring_type} keyring"), target_fixture="keyring") @given(parse("we have a {keyring_type} keyring"), target_fixture="keyring")
def we_have_type_of_keyring(keyring_type): def we_have_type_of_keyring(keyring_type):
if keyring_type == "failed": if keyring_type == "failed":
set_keyring(FailedKeyring()) return FailedKeyring()
else: else:
set_keyring(TestKeyring()) return TestKeyring()
@given(parse('we use the config "{config_file}"'), target_fixture="config_path") @given(parse('we use the config "{config_file}"'), target_fixture="config_path")
@given('we use the config "<config_file>"', target_fixture="config_path")
def we_use_the_config(config_file, temp_dir, working_dir): def we_use_the_config(config_file, temp_dir, working_dir):
# Move into temp dir as cwd # Move into temp dir as cwd
os.chdir(temp_dir.name) os.chdir(temp_dir.name)
@ -106,7 +103,6 @@ def we_use_the_config(config_file, temp_dir, working_dir):
@given(parse('the config "{config_file}" exists'), target_fixture="config_path") @given(parse('the config "{config_file}" exists'), target_fixture="config_path")
@given('the config "<config_file>" exists', target_fixture="config_path")
def config_exists(config_file, temp_dir, working_dir): def config_exists(config_file, temp_dir, working_dir):
config_source = os.path.join(working_dir, "data", "configs", config_file) config_source = os.path.join(working_dir, "data", "configs", config_file)
config_dest = os.path.join(temp_dir.name, config_file) config_dest = os.path.join(temp_dir.name, config_file)

View file

@ -49,3 +49,24 @@ def get_nested_val(dictionary, path, *default):
if default: if default:
return default[0] return default[0]
raise raise
# @see: https://stackoverflow.com/a/41599695/569146
def spy_wrapper(wrapped_function):
from unittest import mock
mock = mock.MagicMock()
def wrapper(self, *args, **kwargs):
mock(*args, **kwargs)
return wrapped_function(self, *args, **kwargs)
wrapper.mock = mock
return wrapper
def get_fixture(request, name, default=None):
result = default
if name in request.node.fixturenames:
result = request.getfixturevalue(name)
return result

View file

@ -32,7 +32,6 @@ def output_should_match(regex, cli_run):
@then(parse("the output should contain\n{expected_output}")) @then(parse("the output should contain\n{expected_output}"))
@then(parse('the output should contain "{expected_output}"')) @then(parse('the output should contain "{expected_output}"'))
@then('the output should contain "<expected_output>"')
@then(parse("the {which_output_stream} output should contain\n{expected_output}")) @then(parse("the {which_output_stream} output should contain\n{expected_output}"))
@then(parse('the {which_output_stream} output should contain "{expected_output}"')) @then(parse('the {which_output_stream} output should contain "{expected_output}"'))
def output_should_contain(expected_output, which_output_stream, cli_run): def output_should_contain(expected_output, which_output_stream, cli_run):
@ -54,14 +53,12 @@ def output_should_contain(expected_output, which_output_stream, cli_run):
@then(parse("the output should not contain\n{expected_output}")) @then(parse("the output should not contain\n{expected_output}"))
@then(parse('the output should not contain "{expected_output}"')) @then(parse('the output should not contain "{expected_output}"'))
@then('the output should not contain "<expected_output>"')
def output_should_not_contain(expected_output, cli_run): def output_should_not_contain(expected_output, cli_run):
assert expected_output not in cli_run["stdout"] assert expected_output not in cli_run["stdout"]
@then(parse("the output should be\n{expected_output}")) @then(parse("the output should be\n{expected_output}"))
@then(parse('the output should be "{expected_output}"')) @then(parse('the output should be "{expected_output}"'))
@then('the output should be "<expected_output>"')
def output_should_be(expected_output, cli_run): def output_should_be(expected_output, cli_run):
actual = cli_run["stdout"].strip() actual = cli_run["stdout"].strip()
expected = expected_output.strip() expected = expected_output.strip()
@ -75,7 +72,6 @@ def output_should_be_empty(cli_run):
@then(parse('the output should contain the date "{date}"')) @then(parse('the output should contain the date "{date}"'))
@then('the output should contain the date "<date>"')
def output_should_contain_date(date, cli_run): def output_should_contain_date(date, cli_run):
assert date and date in cli_run["stdout"] assert date and date in cli_run["stdout"]

View file

@ -3,14 +3,12 @@
from contextlib import ExitStack from contextlib import ExitStack
import os import os
from unittest.mock import patch
from pytest_bdd import parsers
from pytest_bdd import when from pytest_bdd import when
from pytest_bdd.parsers import parse from pytest_bdd.parsers import parse
from pytest_bdd.parsers import re
from jrnl.cli import cli from jrnl.cli import cli
from jrnl.os_compat import split_args
@when(parse('we change directory to "{directory_name}"')) @when(parse('we change directory to "{directory_name}"'))
@ -21,103 +19,35 @@ def when_we_change_directory(directory_name):
os.chdir(directory_name) os.chdir(directory_name)
@when(parse('we run "jrnl {command}" and {input_method}\n{user_input}')) command = '(?P<command>[^"]+)'
@when( input_method = '(?P<input_method>enter|pipe)'
parsers.re( user_input = '(?P<user_input>[^"]+)'
'we run "jrnl (?P<command>[^"]+)" and (?P<input_method>enter|pipe) "(?P<user_input>[^"]+)"' @when(re(f'we run "jrnl {command}" and {input_method}\n{user_input}'))
) @when(re(f'we run "jrnl" and {input_method}\n{user_input}'))
) @when(re(f'we run "jrnl {command}" and {input_method} "{user_input}"'))
@when(parse('we run "jrnl" and {input_method} "{user_input}"')) @when(re(f'we run "jrnl" and {input_method} "{user_input}"'))
@when(parse('we run "jrnl {command}"')) @when(parse('we run "jrnl {command}"'))
@when('we run "jrnl <command>"')
@when('we run "jrnl"') @when('we run "jrnl"')
def we_run( def we_run_jrnl(cli_run, capsys, keyring):
command, from keyring import set_keyring
config_path,
config_in_memory,
user_input,
cli_run,
capsys,
password,
cache_dir,
editor,
keyring,
input_method,
mocks,
):
assert input_method in ["", "enter", "pipe"]
is_tty = input_method != "pipe"
if cache_dir["exists"]: set_keyring(keyring)
command = command.format(cache_dir=cache_dir["path"])
args = split_args(command)
status = 0
if user_input:
user_input = user_input.splitlines() if is_tty else [user_input]
if password:
password = password.splitlines()
if not password and user_input:
password = user_input
with ExitStack() as stack: with ExitStack() as stack:
# Always mock mocks = cli_run["mocks"]
from jrnl.override import apply_overrides factories = cli_run["mock_factories"]
for id in factories:
def my_overrides(*args, **kwargs): mocks[id] = stack.enter_context(factories[id]())
result = apply_overrides(*args, **kwargs)
config_in_memory["overrides"] = result
return result
stack.enter_context(
patch("jrnl.jrnl.apply_overrides", side_effect=my_overrides)
)
# Conditionally mock
stack.enter_context(patch("sys.argv", ["jrnl"] + args))
mock_stdin = stack.enter_context(
patch("sys.stdin.read", side_effect=user_input)
)
stack.enter_context(patch("sys.stdin.isatty", return_value=is_tty))
mock_input = stack.enter_context(
patch("builtins.input", side_effect=user_input)
)
mock_getpass = stack.enter_context(
patch("getpass.getpass", side_effect=password)
)
if "datetime" in mocks:
stack.enter_context(mocks["datetime"])
stack.enter_context(mocks["calendar_parse"])
stack.enter_context(
patch("jrnl.install.get_config_path", return_value=config_path)
)
stack.enter_context(
patch("jrnl.config.get_config_path", return_value=config_path)
)
mock_editor = stack.enter_context(patch("subprocess.call", side_effect=editor))
try: try:
cli(args) cli()
except StopIteration: except StopIteration:
# This happens when input is expected, but don't have any input left # This happens when input is expected, but don't have any input left
pass pass
except SystemExit as e: except SystemExit as e:
status = e.code cli_run["status"] = e.code
captured = capsys.readouterr() captured = capsys.readouterr()
cli_run["status"] = status
cli_run["stdout"] = captured.out cli_run["stdout"] = captured.out
cli_run["stderr"] = captured.err cli_run["stderr"] = captured.err
cli_run["mocks"] = {
"stdin": mock_stdin,
"input": mock_input,
"getpass": mock_getpass,
"editor": mock_editor,
}