clean up some code around keyrings, clean up more tests

This commit is contained in:
Jonathan Wren 2020-09-05 17:34:15 -07:00
parent 764227d2f2
commit 6ada26d629
No known key found for this signature in database
GPG key ID: 43D5FF8722E7F68A
2 changed files with 106 additions and 38 deletions

View file

@ -1,8 +1,9 @@
Feature: Encrypted journals
Feature: Encrypting and decrypting journals
Scenario: Loading an encrypted journal
Given we use the config "encrypted.yaml"
When we run "jrnl -n 1" and enter "bad doggie no biscuit"
Then the output should contain "Password"
Then we should be prompted for a password
And the output should contain "2013-06-10 15:40 Life is good"
Scenario: Decrypting a journal
@ -12,6 +13,13 @@
And we should see the message "Journal decrypted"
And the journal should have 2 entries
Scenario: Trying to decrypt an unencrypted journal
Given we use the config "basic.yaml"
When we run "jrnl --decrypt"
Then the config for journal "default" should have "encrypt" set to "bool:False"
And we should get no error
And the journal should have 2 entries
Scenario: Encrypting a journal
Given we use the config "basic.yaml"
When we run "jrnl --encrypt" and enter
@ -23,10 +31,22 @@
Then 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 the output should contain "Password"
Then 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 "basic.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 "basic.yaml"
When we run "jrnl --encrypt" and enter
"""
@ -36,22 +56,23 @@
swordfish
n
"""
Then we should see the message "Passwords did not match"
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 the output should contain "Password"
Then we should be prompted for a password
And the output should contain "2013-06-10 15:40 Life is good"
Scenario: Storing a password in Keychain
Scenario: Storing a password in keyring
Given we use the config "multiple.yaml"
And we have a keyring
When we run "jrnl simple --encrypt" and enter
"""
sabertooth
sabertooth
y
"""
When we set the keychain password of "simple" to "sabertooth"
Then the config for journal "simple" should have "encrypt" set to "bool:True"
When we run "jrnl simple -n 1"
Then the output should contain "2013-06-10 15:40 Life is good"
@ -59,8 +80,8 @@
Scenario: Encrypt journal with no keyring backend and do not store in keyring
Given we use the config "basic.yaml"
When we disable the keychain
And we run "jrnl test entry"
And we do not have a keyring
When we run "jrnl test entry"
And we run "jrnl --encrypt" and enter
"""
password
@ -71,8 +92,8 @@
Scenario: Encrypt journal with no keyring backend and do store in keyring
Given we use the config "basic.yaml"
When we disable the keychain
And we run "jrnl test entry"
And we do not have a keyring
When we run "jrnl test entry"
And we run "jrnl --encrypt" and enter
"""
password

View file

@ -64,8 +64,24 @@ class NoKeyring(keyring.backend.KeyringBackend):
raise keyring.errors.NoKeyringError
# set the keyring for keyring lib
keyring.set_keyring(TestKeyring())
class FailedKeyring(keyring.backend.KeyringBackend):
"""
A keyring that simulates an environment with a keyring that has passwords, but fails
to return them.
"""
priority = 2
keys = defaultdict(dict)
def set_password(self, servicename, username, password):
self.keys[servicename][username] = password
def get_password(self, servicename, username):
raise keyring.errors.NoKeyringError
def delete_password(self, servicename, username):
self.keys[servicename][username] = None
def ushlex(command):
@ -93,6 +109,18 @@ def open_journal(journal_name="default"):
return Journal.open_journal(journal_name, config)
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
@given('we use the config "{config_file}"')
def set_config(context, config_file):
full_path = os.path.join("features/configs", config_file)
@ -103,6 +131,16 @@ def set_config(context, config_file):
cf.write("version: {}".format(__version__))
@given('we have a keyring')
def set_keyring(context):
keyring.set_keyring(TestKeyring())
@given('we do not have a keyring')
def disable_keyring(context):
keyring.core.set_keyring(NoKeyring())
@when('we change directory to "{path}"')
def move_up_dir(context, path):
os.chdir(path)
@ -175,16 +213,22 @@ def matches_editor_arg(context, regex):
def _mock_getpass(inputs):
def prompt_return(prompt="Password: "):
print(prompt)
return next(inputs)
try:
return next(inputs)
except StopIteration:
raise KeyboardInterrupt
return prompt_return
def _mock_input(inputs):
def prompt_return(prompt=""):
val = next(inputs)
print(prompt, val)
return val
try:
val = next(inputs)
print(prompt, val)
return val
except StopIteration:
raise KeyboardInterrupt
return prompt_return
@ -247,11 +291,16 @@ def inputs_were_called(context):
)
@then("we were prompted for a password")
def password_was_called(context, method):
@then("we should be prompted for a password")
def password_was_called(context):
assert context.getpass.called
@then("we should not be prompted for a password")
def password_was_not_called(context):
assert not context.getpass.called
@then("all input was used")
def all_input_was_used(context):
# all inputs were used (ignore if empty string)
@ -292,16 +341,11 @@ def load_template(context, filename):
plugins.__exporter_types[exporter.names[0]] = exporter
@when('we set the keychain password of "{journal}" to "{password}"')
def set_keychain(context, journal, password):
@when('we set the keyring password of "{journal}" to "{password}"')
def set_keyring(context, journal, password):
keyring.set_password("jrnl", journal, password)
@when("we disable the keychain")
def disable_keychain(context):
keyring.core.set_keyring(NoKeyring())
@then("we should get an error")
def has_error(context):
assert context.exit_status != 0, context.exit_status
@ -397,8 +441,8 @@ def check_not_journal_content(context, text, journal_name="default"):
@then('journal "{journal_name}" should not exist')
def journal_doesnt_exist(context, journal_name="default"):
with open(install.CONFIG_FILE_PATH) as config_file:
config = yaml.load(config_file, Loader=yaml.FullLoader)
config = load_config(install.CONFIG_FILE_PATH)
journal_path = config["journals"][journal_name]
assert not os.path.exists(journal_path)
@ -407,23 +451,26 @@ def journal_doesnt_exist(context, journal_name="default"):
@then('the config should have "{key}" set to "{value}"')
@then('the config for journal "{journal}" should have "{key}" set to "{value}"')
def config_var(context, key, value="", journal=None):
value = value or context.text or ""
if not value[0] == "{":
t, value = value.split(":")
value = {"bool": lambda v: v.lower() == "true", "int": int, "str": str}[t](
value
)
else:
# Handle value being a dictionary
value = ast.literal_eval(value)
value = read_value_from_string(value or context.text or "")
config = load_config(install.CONFIG_FILE_PATH)
if journal:
config = config["journals"][journal]
assert key in config
assert config[key] == value
@then('the config for journal "{journal}" should not have "{key}" set')
def config_var(context, key, value="", journal=None):
config = load_config(install.CONFIG_FILE_PATH)
if journal:
config = config["journals"][journal]
assert key not in config
@then("the journal should have {number:d} entries")
@then("the journal should have {number:d} entry")
@then('journal "{journal_name}" should have {number:d} entries')