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

View file

@ -64,8 +64,24 @@ class NoKeyring(keyring.backend.KeyringBackend):
raise keyring.errors.NoKeyringError raise keyring.errors.NoKeyringError
# set the keyring for keyring lib class FailedKeyring(keyring.backend.KeyringBackend):
keyring.set_keyring(TestKeyring()) """
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): def ushlex(command):
@ -93,6 +109,18 @@ def open_journal(journal_name="default"):
return Journal.open_journal(journal_name, config) 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}"') @given('we use the config "{config_file}"')
def set_config(context, config_file): def set_config(context, config_file):
full_path = os.path.join("features/configs", 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__)) 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}"') @when('we change directory to "{path}"')
def move_up_dir(context, path): def move_up_dir(context, path):
os.chdir(path) os.chdir(path)
@ -175,16 +213,22 @@ def matches_editor_arg(context, regex):
def _mock_getpass(inputs): def _mock_getpass(inputs):
def prompt_return(prompt="Password: "): def prompt_return(prompt="Password: "):
print(prompt) print(prompt)
try:
return next(inputs) return next(inputs)
except StopIteration:
raise KeyboardInterrupt
return prompt_return return prompt_return
def _mock_input(inputs): def _mock_input(inputs):
def prompt_return(prompt=""): def prompt_return(prompt=""):
try:
val = next(inputs) val = next(inputs)
print(prompt, val) print(prompt, val)
return val return val
except StopIteration:
raise KeyboardInterrupt
return prompt_return return prompt_return
@ -247,11 +291,16 @@ def inputs_were_called(context):
) )
@then("we were prompted for a password") @then("we should be prompted for a password")
def password_was_called(context, method): def password_was_called(context):
assert context.getpass.called 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") @then("all input was used")
def all_input_was_used(context): def all_input_was_used(context):
# all inputs were used (ignore if empty string) # all inputs were used (ignore if empty string)
@ -292,16 +341,11 @@ def load_template(context, filename):
plugins.__exporter_types[exporter.names[0]] = exporter plugins.__exporter_types[exporter.names[0]] = exporter
@when('we set the keychain password of "{journal}" to "{password}"') @when('we set the keyring password of "{journal}" to "{password}"')
def set_keychain(context, journal, password): def set_keyring(context, journal, password):
keyring.set_password("jrnl", 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") @then("we should get an error")
def has_error(context): def has_error(context):
assert context.exit_status != 0, context.exit_status 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') @then('journal "{journal_name}" should not exist')
def journal_doesnt_exist(context, journal_name="default"): def journal_doesnt_exist(context, journal_name="default"):
with open(install.CONFIG_FILE_PATH) as config_file: config = load_config(install.CONFIG_FILE_PATH)
config = yaml.load(config_file, Loader=yaml.FullLoader)
journal_path = config["journals"][journal_name] journal_path = config["journals"][journal_name]
assert not os.path.exists(journal_path) 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 should have "{key}" set to "{value}"')
@then('the config for journal "{journal}" 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): def config_var(context, key, value="", journal=None):
value = value or context.text or "" value = read_value_from_string(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)
config = load_config(install.CONFIG_FILE_PATH) config = load_config(install.CONFIG_FILE_PATH)
if journal: if journal:
config = config["journals"][journal] config = config["journals"][journal]
assert key in config assert key in config
assert config[key] == value 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} entries")
@then("the journal should have {number:d} entry") @then("the journal should have {number:d} entry")
@then('journal "{journal_name}" should have {number:d} entries') @then('journal "{journal_name}" should have {number:d} entries')