Implement test keyrings and add password tests

- Implement TestKeyring
- Implement NoKeyring
- Implement FailedKeyring
- Copy in `read_value_from_string` function from old tests (will
  probably rewrite this later)
- Add fixtures for keyrings
- Implement "we have a keyring" step
- Implement step to check specific config values for tests

Co-authored-by: Micah Jerome Ellison <micah.jerome.ellison@gmail.com>
This commit is contained in:
Jonathan Wren 2021-03-02 20:44:26 -08:00
parent 1c78a30535
commit fe018ee241
3 changed files with 110 additions and 115 deletions

View file

@ -4,113 +4,9 @@ Feature: Using the installed keyring
Given we use the config "multiple.yaml" Given we use the config "multiple.yaml"
And we have a keyring And we have a keyring
When we run "jrnl simple --encrypt" and enter When we run "jrnl simple --encrypt" and enter
"""
sabertooth sabertooth
sabertooth sabertooth
y Y
"""
Then the config for journal "simple" should have "encrypt" set to "bool:True" Then the config for journal "simple" should have "encrypt" set to "bool:True"
When we run "jrnl simple -n 1" When we run "jrnl simple -n 1"
Then the output should contain "2013-06-10 15:40 Life is good" Then the output should contain "2013-06-10 15:40 Life is good"
Scenario: Encrypt journal with no keyring backend and do not store in keyring
Given we use the config "simple.yaml"
And we do not have a keyring
When we run "jrnl test entry"
And we run "jrnl --encrypt" and enter
"""
password
password
n
"""
Then we should get no error
And we should not see the message "Failed to retrieve keyring"
Scenario: Encrypt journal with no keyring backend and do store in keyring
Given we use the config "simple.yaml"
And we do not have a keyring
When we run "jrnl test entry"
And we run "jrnl --encrypt" and enter
"""
password
password
y
"""
Then we should get no error
And we should not see the message "Failed to retrieve keyring"
# @todo add step to check contents of keyring
@todo
Scenario: Open an encrypted journal with wrong password in keyring
# This should ask the user for the password after the keyring fails
@todo
Scenario: Decrypt journal with password in keyring
@todo
Scenario: Decrypt journal without a keyring
Scenario: Encrypt journal when keyring exists but fails
Given we use the config "simple.yaml"
And we have a failed keyring
When we run "jrnl --encrypt" and enter
"""
this password will not be saved in keyring
this password will not be saved in keyring
y
"""
Then we should see the message "Failed to retrieve keyring"
And we should get no error
And we should be prompted for a password
And the config for journal "default" should have "encrypt" set to "bool:True"
Scenario: Decrypt journal when keyring exists but fails
Given we use the config "encrypted.yaml"
And we have a failed keyring
When we run "jrnl --decrypt" and enter "bad doggie no biscuit"
Then we should see the message "Failed to retrieve keyring"
And we should get no error
And we should be prompted for a password
And we should see the message "Journal decrypted"
And the config for journal "default" should have "encrypt" set to "bool:False"
And the journal should have 2 entries
Scenario: Open encrypted journal when keyring exists but fails
# This should ask the user for the password after the keyring fails
Given we use the config "encrypted.yaml"
And we have a failed keyring
When we run "jrnl -n 1" and enter "bad doggie no biscuit"
Then we should see the message "Failed to retrieve keyring"
And we should get no error
And we should be prompted for a password
And the output should contain "2013-06-10 15:40 Life is good"
Scenario: Mistyping your password
Given we use the config "simple.yaml"
When we run "jrnl --encrypt" and enter
"""
swordfish
sordfish
"""
Then we should be prompted for a password
And we should see the message "Passwords did not match"
And the config for journal "default" should not have "encrypt" set
And the journal should have 2 entries
Scenario: Mistyping your password, then getting it right
Given we use the config "simple.yaml"
When we run "jrnl --encrypt" and enter
"""
swordfish
sordfish
swordfish
swordfish
n
"""
Then we should be prompted for a password
And we should see the message "Passwords did not match"
And we should see the message "Journal encrypted"
And the config for journal "default" should have "encrypt" set to "bool:True"
When we run "jrnl -n 1" and enter "swordfish"
Then we should be prompted for a password
And the output should contain "2013-06-10 15:40 Life is good"

View file

@ -1,7 +1,12 @@
# Copyright (C) 2012-2021 jrnl contributors # Copyright (C) 2012-2021 jrnl contributors
# License: https://www.gnu.org/licenses/gpl-3.0.html # License: https://www.gnu.org/licenses/gpl-3.0.html
import ast
import os import os
from collections import defaultdict
from keyring import backend
from keyring import set_keyring
from keyring import errors
import re import re
import shutil import shutil
import tempfile import tempfile
@ -16,14 +21,75 @@ import toml
from jrnl import __version__ from jrnl import __version__
from jrnl.cli import cli from jrnl.cli import cli
from jrnl.config import load_config
from jrnl.os_compat import split_args from jrnl.os_compat import split_args
class TestKeyring(backend.KeyringBackend):
"""A test keyring that just stores its values in a hash"""
priority = 1
keys = defaultdict(dict)
def set_password(self, servicename, username, password):
self.keys[servicename][username] = password
def get_password(self, servicename, username):
return self.keys[servicename].get(username)
def delete_password(self, servicename, username):
self.keys[servicename][username] = None
class NoKeyring(backend.KeyringBackend):
"""A keyring that simulated an environment with no keyring backend."""
priority = 2
keys = defaultdict(dict)
def set_password(self, servicename, username, password):
raise errors.NoKeyringError
def get_password(self, servicename, username):
raise errors.NoKeyringError
def delete_password(self, servicename, username):
raise errors.NoKeyringError
class FailedKeyring(backend.KeyringBackend):
"""
A keyring that cannot be retrieved.
"""
priority = 2
def set_password(self, servicename, username, password):
raise errors.KeyringError
def get_password(self, servicename, username):
raise errors.KeyringError
def delete_password(self, servicename, username):
raise errors.KeyringError
# ----- UTILS ----- # # ----- UTILS ----- #
def failed_msg(msg, expected, actual): def failed_msg(msg, expected, actual):
return f"{msg}\nExpected:\n{expected}\n---end---\nActual:\n{actual}\n---end---\n" return f"{msg}\nExpected:\n{expected}\n---end---\nActual:\n{actual}\n---end---\n"
def read_value_from_string(string):
if string[0] == "{":
# Handle value being a dictionary
return ast.literal_eval(string)
# Takes strings like "bool:true" or "int:32" and coerces them into proper type
t, value = string.split(":")
value = {"bool": lambda v: v.lower() == "true", "int": int, "str": str}[t](value)
return value
# ----- FIXTURES ----- # # ----- FIXTURES ----- #
@fixture @fixture
def cli_run(): def cli_run():
@ -46,22 +112,38 @@ def toml_version(working_dir):
pyproject_contents = toml.load(pyproject) pyproject_contents = toml.load(pyproject)
return pyproject_contents["tool"]["poetry"]["version"] return pyproject_contents["tool"]["poetry"]["version"]
@fixture @fixture
def password(): def password():
return '' return ""
@fixture @fixture
def command(): def command():
return '' return ""
@fixture @fixture
def user_input(): def user_input():
return '' return ""
@fixture
def keyring():
set_keyring(NoKeyring())
@fixture
def config_data(config_path):
return load_config(config_path)
# ----- STEPS ----- # # ----- STEPS ----- #
@given("we have a keyring", target_fixture="keyring")
def we_have_keyring():
set_keyring(FailedKeyring())
@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") @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):
@ -99,10 +181,13 @@ def use_password_forever(pw):
@when('we run "jrnl"') @when('we run "jrnl"')
@when(parse('we run "jrnl" and enter "{user_input}"')) @when(parse('we run "jrnl" and enter "{user_input}"'))
@when(parse('we run "jrnl {command}" and enter\n{user_input}')) @when(parse('we run "jrnl {command}" and enter\n{user_input}'))
def we_run(command, config_path, user_input, cli_run, capsys, password): def we_run(command, config_path, user_input, cli_run, capsys, password, keyring):
args = split_args(command) args = split_args(command)
status = 0 status = 0
if not password and user_input:
password = user_input
# fmt: off # fmt: off
# see: https://github.com/psf/black/issues/664 # see: https://github.com/psf/black/issues/664
with \ with \
@ -163,9 +248,9 @@ def output_should_not_contain(output, cli_run):
def output_should_be(output, cli_run): def output_should_be(output, cli_run):
actual_out = cli_run["stdout"].strip() actual_out = cli_run["stdout"].strip()
output = output.strip() output = output.strip()
assert ( assert output and output == actual_out, failed_msg(
output and output == actual_out "Output does not match.", output, actual_out
), failed_msg('Output does not match.', output, actual_out) )
@then('the output should contain the date "<date>"') @then('the output should contain the date "<date>"')
@ -183,3 +268,17 @@ def output_should_contain_version(cli_run, toml_version):
def should_see_the_message(text, cli_run): def should_see_the_message(text, cli_run):
out = cli_run["stderr"] out = cli_run["stderr"]
assert text in out, [text, out] assert text in out, [text, out]
@then(parse('the config should have "{key}" set to'))
@then(parse('the config should have "{key}" set to "{value}"'))
@then(parse('the config for journal "{journal}" should have "{key}" set to "{value}"'))
def config_var(config_data, key, value="", journal=None):
value = read_value_from_string(value)
configuration = config_data
if journal:
configuration = configuration["journals"][journal]
assert key in configuration
assert configuration[key] == value

View file

@ -9,7 +9,7 @@ scenarios("../features/delete.feature")
# scenarios("../features/format.feature") # scenarios("../features/format.feature")
# scenarios("../features/import.feature") # scenarios("../features/import.feature")
# scenarios("../features/multiple_journals.feature") # scenarios("../features/multiple_journals.feature")
# scenarios("../features/password.feature") scenarios("../features/password.feature")
# scenarios("../features/search.feature") # scenarios("../features/search.feature")
# scenarios("../features/star.feature") # scenarios("../features/star.feature")
# scenarios("../features/tag.feature") # scenarios("../features/tag.feature")