Skip to content

Commit

Permalink
Improved coverage and exc chaining of Keyring class
Browse files Browse the repository at this point in the history
Details:

* Improved exception chaining in 'Keyring.delete_password()'.

* Test: Improved test coverage of Keyring class.

Signed-off-by: Andreas Maier <andreas.r.maier@gmx.de>
  • Loading branch information
andy-maier committed Apr 4, 2021
1 parent 5446442 commit 10ffe7e
Show file tree
Hide file tree
Showing 5 changed files with 314 additions and 6 deletions.
2 changes: 2 additions & 0 deletions dev-requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ pytest>=4.4.0; python_version >= '3.7'

testfixtures==6.9.0

mock>=2.0.0,<4.0.0; python_version == '2.7'

# virtualenv
# Virtualenv 20.0.19 has an issue where it does not install pip on Python 3.4.
# Virtualenv 20.0.32 has an issue where it raises AttributeError on Python 3.4.
Expand Down
4 changes: 3 additions & 1 deletion docs/changes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,16 @@ Released: not yet
command. (issue #45)

* Added missing exception handling to 'Keyring.set_password()' and improved
exception chaining in 'Keyring.get_password()'.
exception chaining in 'Keyring.get_password()' and 'Keyring.delete_password()'.

**Enhancements:**

* Added a 'easy-vault delete-password' command that deletes the password for
a vault file in the keyring service. Added a corresponding
'Keyring.delete_password()' method. (issues #33 and #35)

* Test: Improved test coverage of Keyring class.

**Cleanup:**

**Known issues:**
Expand Down
21 changes: 17 additions & 4 deletions easy_vault/_keyring.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,19 +164,28 @@ def delete_password(self, filepath):
try:
pw = keyring.get_password(service, username)
except NO_KEYRING_EXCEPTION as exc:
raise KeyringNotAvailable(str(exc))
new_exc = KeyringNotAvailable(str(exc))
new_exc.__cause__ = None
raise new_exc # KeyringNotAvailable
except keyring.errors.KeyringError as exc:
raise KeyringError(str(exc))
new_exc = KeyringError(str(exc))
new_exc.__cause__ = None
raise new_exc # KeyringError

if pw is None:
return False

try:
keyring.delete_password(service, username)
except NO_KEYRING_EXCEPTION as exc:
raise KeyringNotAvailable(str(exc))
new_exc = KeyringNotAvailable(str(exc))
new_exc.__cause__ = None
raise new_exc # KeyringNotAvailable
except keyring.errors.KeyringError as exc:
raise KeyringError(str(exc))
new_exc = KeyringError(str(exc))
new_exc.__cause__ = None
raise new_exc # KeyringError

return True

def is_available(self):
Expand Down Expand Up @@ -241,6 +250,10 @@ def check_available():
"Keyring test call failed with: {msg}".format(msg=exc))
new_exc.__cause__ = None
raise new_exc # KeyringNotAvailable
except keyring.errors.KeyringError as exc:
new_exc = KeyringError(str(exc))
new_exc.__cause__ = None
raise new_exc # KeyringError
try:
keyring.delete_password(service, username)
except Exception as exc:
Expand Down
2 changes: 2 additions & 0 deletions minimum-constraints.txt
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,8 @@ pytest==4.4.0; python_version >= '3.7'

testfixtures==6.9.0

mock==2.0.0; python_version == '2.7'

# virtualenv
virtualenv==14.0.0; python_version < '3.5'
virtualenv==16.1.0; python_version >= '3.5' and python_version < '3.8'
Expand Down
291 changes: 290 additions & 1 deletion tests/unittest/test_keyring.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,15 @@
"""

from __future__ import absolute_import, print_function
try:
from unittest import mock
except ImportError:
import mock

import pytest
from easy_vault import Keyring, KeyringNotAvailable
import keyring
from easy_vault import Keyring, KeyringNotAvailable, KeyringError
from easy_vault._keyring import NO_KEYRING_EXCEPTION

# pylint: disable=unused-import
from ..utils.keyring_utils import keyring_filepath # noqa: F401
Expand Down Expand Up @@ -79,3 +85,286 @@ def test_keyring_available():
check_avail = False

assert check_avail == is_avail


@pytest.mark.skipif(
not is_keyring_available(), reason="No keyring service available")
@pytest.mark.parametrize(
"keyring_exc, exp_exc",
[
(NO_KEYRING_EXCEPTION, KeyringNotAvailable),
(keyring.errors.KeyringError, KeyringError),
]
)
# pylint: disable=redefined-outer-name
def test_keyring_get_password_fail(keyring_filepath, keyring_exc, exp_exc):
"""
Test function for Keyring.get_password() when it raises an exception
"""
kr = Keyring()
with mock.patch.object(keyring, 'get_password', side_effect=keyring_exc):
with pytest.raises(exp_exc):
# Code to be tested
kr.get_password(keyring_filepath)


@pytest.mark.skipif(
not is_keyring_available(), reason="No keyring service available")
@pytest.mark.parametrize(
"keyring_exc, exp_exc",
[
(NO_KEYRING_EXCEPTION, KeyringNotAvailable),
(keyring.errors.KeyringError, KeyringError),
]
)
# pylint: disable=redefined-outer-name
def test_keyring_set_password_fail(keyring_filepath, keyring_exc, exp_exc):
"""
Test function for Keyring.set_password() when it raises an exception
"""
kr = Keyring()
with mock.patch.object(keyring, 'set_password', side_effect=keyring_exc):
with pytest.raises(exp_exc):
# Code to be tested
kr.set_password(keyring_filepath, 'dummy')


@pytest.mark.skipif(
not is_keyring_available(), reason="No keyring service available")
@pytest.mark.parametrize(
"keyring_exc, exp_exc",
[
(NO_KEYRING_EXCEPTION, KeyringNotAvailable),
(keyring.errors.KeyringError, KeyringError),
]
)
# pylint: disable=redefined-outer-name
def test_keyring_delete_password_fail1(keyring_filepath, keyring_exc, exp_exc):
"""
Test function for Keyring.delete_password() when it raises an exception
in keyring.get_password().
"""
kr = Keyring()
with mock.patch.object(keyring, 'get_password', side_effect=keyring_exc):
with pytest.raises(exp_exc):
# Code to be tested
kr.delete_password(keyring_filepath)


@pytest.mark.skipif(
not is_keyring_available(), reason="No keyring service available")
@pytest.mark.parametrize(
"keyring_exc, exp_exc",
[
(NO_KEYRING_EXCEPTION, KeyringNotAvailable),
(keyring.errors.KeyringError, KeyringError),
]
)
# pylint: disable=redefined-outer-name
def test_keyring_delete_password_fail2(keyring_filepath, keyring_exc, exp_exc):
"""
Test function for Keyring.delete_password() when it raises an exception
in keyring.delete_password().
"""
kr = Keyring()
kr.set_password(keyring_filepath, 'dummy')
with mock.patch.object(keyring, 'delete_password', side_effect=keyring_exc):
with pytest.raises(exp_exc):
# Code to be tested
kr.delete_password(keyring_filepath)


@pytest.mark.skipif(
not is_keyring_available(), reason="No keyring service available")
@pytest.mark.parametrize(
"keyring_exc, exp_result",
[
(NO_KEYRING_EXCEPTION, False),
(keyring.errors.KeyringError, KeyringError),
]
)
def test_keyring_is_available_fail1(keyring_exc, exp_result):
"""
Test function for Keyring.is_available() when it raises an exception
in keyring.set_password().
"""
kr = Keyring()
with mock.patch.object(keyring, 'set_password', side_effect=keyring_exc):
if isinstance(exp_result, type) and issubclass(exp_result, Exception):
with pytest.raises(exp_result):
# Code to be tested
kr.is_available()
else:
# Code to be tested
available = kr.is_available()
assert available == exp_result


@pytest.mark.skipif(
not is_keyring_available(), reason="No keyring service available")
@pytest.mark.parametrize(
"keyring_exc",
[
(NO_KEYRING_EXCEPTION),
(keyring.errors.KeyringError),
]
)
def test_keyring_is_available_fail2(keyring_exc):
"""
Test function for Keyring.is_available() when it raises an exception
in keyring.delete_password().
"""
kr = Keyring()
with mock.patch.object(
keyring, 'delete_password', side_effect=keyring_exc):
# Code to be tested
available = kr.is_available()
assert available is False


@pytest.mark.skipif(
not is_keyring_available(), reason="No keyring service available")
def test_keyring_is_available_fail3():
"""
Test function for Keyring.is_available() when the backend is the Chainer
backend with an empty list of backends.
"""
kr = Keyring()
backend_class = keyring.backends.chainer.ChainerBackend
with mock.patch.object(
keyring, 'get_keyring', return_value=backend_class()):
with mock.patch.object(
backend_class, 'backends',
new_callable=mock.PropertyMock) as backends_mock:
backends_mock.return_value = []
# Code to be tested
available = kr.is_available()
assert available is False


@pytest.mark.skipif(
not is_keyring_available(), reason="No keyring service available")
def test_keyring_is_available_fail4():
"""
Test function for Keyring.is_available() when the backend is the fail
backend.
"""
kr = Keyring()
backend_class = keyring.backends.fail.Keyring
with mock.patch.object(
keyring, 'get_keyring', return_value=backend_class()):
# Code to be tested
available = kr.is_available()
assert available is False


@pytest.mark.skipif(
not is_keyring_available(), reason="No keyring service available")
def test_keyring_is_available_fail5():
"""
Test function for Keyring.is_available() when the backend is the null
backend.
"""
kr = Keyring()
backend_class = keyring.backends.null.Keyring
with mock.patch.object(
keyring, 'get_keyring', return_value=backend_class()):
# Code to be tested
available = kr.is_available()
assert available is False


@pytest.mark.skipif(
not is_keyring_available(), reason="No keyring service available")
@pytest.mark.parametrize(
"keyring_exc, exp_exc",
[
(NO_KEYRING_EXCEPTION, KeyringNotAvailable),
(keyring.errors.KeyringError, KeyringError),
]
)
def test_keyring_check_available_fail1(keyring_exc, exp_exc):
"""
Test function for Keyring.check_available() when it raises an exception
in keyring.set_password().
"""
kr = Keyring()
with mock.patch.object(
keyring, 'set_password', side_effect=keyring_exc):
with pytest.raises(exp_exc):
# Code to be tested
kr.check_available()


@pytest.mark.skipif(
not is_keyring_available(), reason="No keyring service available")
@pytest.mark.parametrize(
"keyring_exc",
[
(NO_KEYRING_EXCEPTION),
(keyring.errors.KeyringError),
]
)
def test_keyring_check_available_fail2(keyring_exc):
"""
Test function for Keyring.check_available() when it raises an exception
in keyring.delete_password().
"""
kr = Keyring()
with mock.patch.object(
keyring, 'delete_password', side_effect=keyring_exc):
with pytest.raises(KeyringNotAvailable):
# Code to be tested
kr.check_available()


@pytest.mark.skipif(
not is_keyring_available(), reason="No keyring service available")
def test_keyring_check_available_fail3():
"""
Test function for Keyring.check_available() when the backend is the Chainer
backend with an empty list of backends.
"""
kr = Keyring()
backend_class = keyring.backends.chainer.ChainerBackend
with mock.patch.object(
keyring, 'get_keyring', return_value=backend_class()):
with mock.patch.object(
backend_class, 'backends',
new_callable=mock.PropertyMock) as backends_mock:
backends_mock.return_value = []
with pytest.raises(KeyringNotAvailable):
# Code to be tested
kr.check_available()


@pytest.mark.skipif(
not is_keyring_available(), reason="No keyring service available")
def test_keyring_check_available_fail4():
"""
Test function for Keyring.check_available() when the backend is the fail
backend.
"""
kr = Keyring()
backend_class = keyring.backends.fail.Keyring
with mock.patch.object(
keyring, 'get_keyring', return_value=backend_class()):
with pytest.raises(KeyringNotAvailable):
# Code to be tested
kr.check_available()


@pytest.mark.skipif(
not is_keyring_available(), reason="No keyring service available")
def test_keyring_check_available_fail5():
"""
Test function for Keyring.check_available() when the backend is the null
backend.
"""
kr = Keyring()
backend_class = keyring.backends.null.Keyring
with mock.patch.object(
keyring, 'get_keyring', return_value=backend_class()):
with pytest.raises(KeyringNotAvailable):
# Code to be tested
kr.check_available()

0 comments on commit 10ffe7e

Please sign in to comment.