Skip to content

Commit

Permalink
Improve tests
Browse files Browse the repository at this point in the history
  • Loading branch information
blackm0re committed Apr 30, 2024
1 parent c31fa58 commit bb9a844
Show file tree
Hide file tree
Showing 6 changed files with 151 additions and 83 deletions.
14 changes: 11 additions & 3 deletions src/etoolkit/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,13 @@ def main(inargs=None):
sys.exit(0)

inst = etoolkit.EtoolkitInstance(args.instance, config_dict)
inst.prompt_func = etoolkit.EtoolkitInstance.confirm_password_prompt
if (
args.master_password_prompt
or os.environ.get('ETOOLKIT_MASTER_PASSWORD') is None
):
inst.prompt_func = (
etoolkit.EtoolkitInstance.confirm_password_prompt
)
env = inst.get_environ()

if args.dump_output:
Expand All @@ -328,9 +334,11 @@ def main(inargs=None):
os.environ.update(env)

if args.spawn:
subprocess.run(args.spawn.split(), check=True)
subprocess.run(args.spawn.split(), check=False)
else:
subprocess.run(os.environ.get('SHELL', 'bash').split(), check=True)
subprocess.run(
os.environ.get('SHELL', 'bash').split(), check=False
)
except KeyboardInterrupt:
logger.debug('KeyboardInterrupt')
print(os.linesep)
Expand Down
19 changes: 12 additions & 7 deletions src/etoolkit/etoolkit.py
Original file line number Diff line number Diff line change
Expand Up @@ -385,7 +385,7 @@ def get_environ(self) -> dict:
if self._parent is not None
else ''
)
if isinstance(value, str) and value.startswith('enc-val$1$'):
if isinstance(value, str) and value.startswith('enc-val$'):
value = self._decrypt_value(value)
if isinstance(value, str) and value.endswith(':'):
# if 'value' ends with ':', append the existing value of
Expand Down Expand Up @@ -440,11 +440,16 @@ def _decrypt_value(self, evalue: str) -> str:
"""
if self._master_password is None:
if self._prompt_func is None:
raise EtoolkitInstanceError(
'Neither password or prompt function set'
if (
mp_from_env := os.environ.get('ETOOLKIT_MASTER_PASSWORD')
) is None:
raise EtoolkitInstanceError(
'Neither password or prompt function set'
)
self.master_password = mp_from_env
else:
# use master_password setter in order to propagate to parent
self.master_password = self._prompt_func(
self._master_password_hash, confirm=False
)
# use master_password setter in order to propagate to parent
self.master_password = self._prompt_func(
self._master_password_hash, confirm=False
)
return EtoolkitInstance.decrypt(self._master_password, evalue)
35 changes: 24 additions & 11 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,26 +27,27 @@ def config_data():
return {
'general': {
'MASTER_PASSWORD_HASH': (
'pbkdf2_sha256$100000$uYpZM1VfAGq0CDZL2duITs076CQj+hIFEgx+F4m'
'n80o=$h3PSPLCd37fP15zKdW4CBGn7CXE+q5UiydaF3vbeZHo='
'pbkdf2_sha256$500000$UY3o78KUM1Btzxk3k3JCsijnwtJ2lx+hH9Newp'
'VKxo8=$tHwDm8OVKanC4DoYTigTCb0R3lQIa/CbBYj0B3TZtHg='
)
},
'instances': {
'_default': {
'ETOOLKIT_PROMPT': '(%i)',
'PYTHONPATH': '/home/foo/%i/python',
'ETOOLKIT_TEST_PYTHONPATH': '/home/foo/%i/python',
},
'dev': {
'ETOOLKIT_PARENT': '_default',
'PYTHONPATH': '%p:/home/user/%i/.pythonpath',
'ETOOLKIT_TEST_PYTHONPATH': '%p:/home/user/%i/.pythonpath',
},
'secret': {
'ETOOLKIT_PARENT': '_default',
'ETOOLKIT_SENSITIVE': ['PASSWORD'],
'ETOOLKIT_SENSITIVE': ['ETOOLKIT_TEST_PASSWORD'],
'GNUPGHOME': '%h/private/.gnupg',
'PASSWORD': (
'enc-val$1$vIBcoCNiYrsDLtF41uLuSEnppBjhliD0B8jwcBJcj/c=$Kw'
'OGe/y1dlxktDaCnJPIVNuaQ4Q7yNo='
'ETOOLKIT_TEST_PASSWORD': (
'enc-val$2$v6F2M7LeUDbQWNLg6WW5mUcbuYYo7aGynSxzWAENVBI=$'
'ZcyWzf9Kp0aYI8N+biKMSmu4RGGi199ayq'
'EYdJl+qdq7b1HSutwYlC7UR2GsSofu4Xo='
),
},
},
Expand All @@ -55,13 +56,19 @@ def config_data():

@pytest.fixture()
def config_file(tmp_path, config_data):
"""temporary config file for testing that includes config_data"""
"""Temporary config file for testing that includes config_data"""

cf = tmp_path / 'etoolkit.json'
cf.write_text(json.dumps(config_data))
return str(cf)


@pytest.fixture()
def master_password():
"""Master passord"""
return 'The very secret passwd'


@pytest.fixture()
def non_random_bytes_32():
"""always use the same bytes instead of os.urandom(32)"""
Expand Down Expand Up @@ -95,6 +102,12 @@ def password_hash():
"""password hash for testing, corresponding to 'The very secret passwd'"""

return (
'pbkdf2_sha256$100000$uYpZM1VfAGq0CDZL2duITs076CQj+hIFEgx+F4mn80o=$h3'
'PSPLCd37fP15zKdW4CBGn7CXE+q5UiydaF3vbeZHo='
'pbkdf2_sha256$500000$UY3o78KUM1Btzxk3k3JCsijnwtJ2lx+hH9NewpVKxo8=$'
'tHwDm8OVKanC4DoYTigTCb0R3lQIa/CbBYj0B3TZtHg='
)


@pytest.fixture()
def wrong_master_password():
"""Wrong master passord"""
return 'the very secret passwd'
67 changes: 46 additions & 21 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,15 @@


@unittest.mock.patch('builtins.input')
def test_decrypt_v1(binput, capsys, config_file):
def test_decrypt_v1(binput, capsys, config_file, master_password):
"""Tests v1 decryption via the CLI interface"""

binput.return_value = (
'enc-val$1$uYpZM1VfAGq0CDZL2duITs076CQj+'
'hIFEgx+F4mn80o=$xdF/1S+R2MGlEQMCOLG6OjEuzw=='
'enc-val$1$rye0sMGEnd35gOWyISE1FQa6dzS+8/jf6aopMO5tPr4=$'
'RjnRY0bUJWFOiejTlM3OhKNimQ=='
)
with unittest.mock.patch.dict(
os.environ, {'ETOOLKIT_MASTER_PASSWORD': 'the very secret passwd'}
os.environ, {'ETOOLKIT_MASTER_PASSWORD': master_password}
):
with pytest.raises(SystemExit) as exit_info:
main(['-c', f'{config_file}', '-d'])
Expand All @@ -43,15 +44,16 @@ def test_decrypt_v1(binput, capsys, config_file):


@unittest.mock.patch('builtins.input')
def test_decrypt_v2(binput, capsys, config_file):
def test_decrypt_v2(binput, capsys, config_file, master_password):
"""Tests v2 decryption via the CLI interface"""

binput.return_value = (
'enc-val$2$RCSZqq9pWrRDoCVYVHopyu1LzaJGfv8roVviq'
'rLTBxM=$VW3UZ6l12yDtyaqWHb7i0QEDiS9s9np'
'7huAACK54BtZVV7RZoIhbu4K6zZuz+LRCyio='
'rLTBxM=$+Yo6Ya2MAVcBLTQHuATkyFc+dzYsL/E'
'SvA6ofOUDsiKZvIff35cUHAmoNxVuGG+MXv4='
)
with unittest.mock.patch.dict(
os.environ, {'ETOOLKIT_MASTER_PASSWORD': 'the very secret passwd'}
os.environ, {'ETOOLKIT_MASTER_PASSWORD': master_password}
):
with pytest.raises(SystemExit) as exit_info:
main(['-c', f'{config_file}', '-d'])
Expand All @@ -61,45 +63,65 @@ def test_decrypt_v2(binput, capsys, config_file):


@unittest.mock.patch('os.urandom')
@unittest.mock.patch('builtins.input', lambda *args: 'bar')
def test_encrypt_with_echo(urandom, capsys, non_random_bytes_61, config_file):
@unittest.mock.patch('builtins.input')
def test_encrypt_with_echo(
binput, urandom, capsys, non_random_bytes_61, config_file, master_password
):
"""Tests encryption via the CLI interface"""

binput.return_value = 'bar'
urandom.return_value = non_random_bytes_61
with unittest.mock.patch.dict(
os.environ, {'ETOOLKIT_MASTER_PASSWORD': 'the very secret passwd'}
os.environ, {'ETOOLKIT_MASTER_PASSWORD': master_password}
):
with pytest.raises(SystemExit) as exit_info:
main(['-c', f'{config_file}', '-e', '-E'])
assert exit_info.type == SystemExit
assert exit_info.value.code == 0
assert capsys.readouterr().out.strip() == (
'Encrypted value: enc-val$2$RCSZqq9pWrRDoCVYVHopyu1LzaJGfv8roVviq'
'rLTBxM=$VW3UZ6l12yDtyaqWHb7i0QEDiS9s9np'
'7huAACK54BtZVV7RZoIhbu4K6zZuz+LRCyio='
'rLTBxM=$+Yo6Ya2MAVcBLTQHuATkyFc+dzYsL/E'
'SvA6ofOUDsiKZvIff35cUHAmoNxVuGG+MXv4='
)


@unittest.mock.patch('os.urandom')
@unittest.mock.patch('getpass.getpass', lambda *args: 'bar')
def test_encrypt_without_echo(gpass, capsys, non_random_bytes_61, config_file):
@unittest.mock.patch('getpass.getpass')
def test_encrypt_without_echo(
getpass, urandom, capsys, non_random_bytes_61, config_file, master_password
):
"""Tests encryption via the CLI interface"""
gpass.return_value = non_random_bytes_61

getpass.return_value = 'bar'
urandom.return_value = non_random_bytes_61
with unittest.mock.patch.dict(
os.environ, {'ETOOLKIT_MASTER_PASSWORD': 'the very secret passwd'}
os.environ, {'ETOOLKIT_MASTER_PASSWORD': master_password}
):
with pytest.raises(SystemExit) as exit_info:
main(['-c', f'{config_file}', '-e'])
assert exit_info.type == SystemExit
assert exit_info.value.code == 0
assert capsys.readouterr().out.strip() == (
'Encrypted value: enc-val$2$RCSZqq9pWrRDoCVYVHopyu1LzaJGfv8roVviq'
'rLTBxM=$VW3UZ6l12yDtyaqWHb7i0QEDiS9s9np'
'7huAACK54BtZVV7RZoIhbu4K6zZuz+LRCyio='
'rLTBxM=$+Yo6Ya2MAVcBLTQHuATkyFc+dzYsL/E'
'SvA6ofOUDsiKZvIff35cUHAmoNxVuGG+MXv4='
)


def test_fetch_encrypted_value(config_file, master_password):
"""Tests decryption of encrypted value"""

with unittest.mock.patch.dict(
os.environ, {'ETOOLKIT_MASTER_PASSWORD': master_password}
):
assert os.environ.get('ETOOLKIT_TEST_PASSWORD') is None
main(['-c', f'{config_file}', '-q', '-s', '/bin/false', 'secret'])
assert os.environ.get('ETOOLKIT_TEST_PASSWORD') == 'bar'


def test_list(capsys, config_file, nonexistent_config_file):
"""Tests list via the CLI interface"""

with pytest.raises(SystemExit) as exit_info:
main(['-c', nonexistent_config_file, '-l'])
assert exit_info.type == SystemExit
Expand All @@ -113,6 +135,7 @@ def test_list(capsys, config_file, nonexistent_config_file):

def test_help(capsys):
"""Dummy test checking if the CLI is available at all"""

with pytest.raises(SystemExit) as exit_info:
main(['-h'])
assert exit_info.type == SystemExit
Expand All @@ -123,11 +146,12 @@ def test_help(capsys):
@unittest.mock.patch('os.urandom')
@unittest.mock.patch('getpass.getpass')
def test_generate_master_password_hash(
gpass, urandom, capsys, non_random_bytes_32
getpass, urandom, capsys, non_random_bytes_32
):
"""Tests master password hash generation via the CLI interface"""

getpass.return_value = 'The very secret passwd'
urandom.return_value = non_random_bytes_32
gpass.return_value = 'The very secret passwd'
with pytest.raises(SystemExit) as exit_info:
main(['--generate-master-password-hash'])
assert exit_info.type == SystemExit
Expand All @@ -140,6 +164,7 @@ def test_generate_master_password_hash(

def test_version(capsys):
"""Dummy test checking if the CLI is available at all"""

with pytest.raises(SystemExit) as exit_info:
main(['-v'])
assert exit_info.type == SystemExit
Expand Down
16 changes: 10 additions & 6 deletions tests/test_envtoolkit_instance.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

def test_instantiation(config_data):
"""Tests for object instatiation"""

with pytest.raises(etoolkit.EtoolkitInstanceError) as exc_info:
instance = etoolkit.EtoolkitInstance('devv', config_data)
assert exc_info.type is etoolkit.EtoolkitInstanceError
Expand All @@ -31,7 +32,7 @@ def test_instantiation(config_data):
assert 'ETOOLKIT_PARENT' not in instance.raw_env_variables
assert 'ETOOLKIT_SENSITIVE' not in instance.raw_env_variables
assert 'DB_CONNECTION' not in instance.sensitive_env_variables
assert 'PASSWORD' in instance.sensitive_env_variables
assert 'ETOOLKIT_TEST_PASSWORD' in instance.sensitive_env_variables
assert instance.name == 'secret'
assert (
instance.master_password_hash
Expand All @@ -40,38 +41,41 @@ def test_instantiation(config_data):
assert instance.master_password is None


def test_get_environ(config_data):
def test_get_environ(config_data, master_password, wrong_master_password):
"""Tests the EtoolkitInstance.get_environ method"""

instance = etoolkit.EtoolkitInstance('secret', config_data)
with pytest.raises(etoolkit.EtoolkitInstanceError) as exc_info:
env = instance.get_environ()
assert exc_info.type is etoolkit.EtoolkitInstanceError
assert exc_info.value.args[0] == 'Neither password or prompt function set'

instance.master_password = 'The very secret passwd' # wrong passwd
instance.master_password = wrong_master_password
with pytest.raises(etoolkit.EtoolkitInstanceError) as exc_info:
env = instance.get_environ()
assert exc_info.type is etoolkit.EtoolkitInstanceError
assert exc_info.value.args[0].startswith(
'Invalid tag when decrypting: enc-val$'
)

instance.master_password = 'the very secret passwd' # correct passwd
instance.master_password = master_password
env = instance.get_environ()
assert isinstance(env, dict)
assert env['PASSWORD'] == 'secret2'
assert env['ETOOLKIT_TEST_PASSWORD'] == 'bar'


def test_get_full_name(config_data):
"""Tests the EtoolkitInstance.get_full_name method"""

instance = etoolkit.EtoolkitInstance('secret', config_data)
assert instance.get_full_name('->') == '_default->secret'


def test_parent_vars(config_data):
"""Tests the EtoolkitInstance.get_full_name method"""

instance = etoolkit.EtoolkitInstance('dev', config_data)
assert instance.get_full_name('->') == '_default->dev'
assert instance.get_environ()['PYTHONPATH'] == (
assert instance.get_environ()['ETOOLKIT_TEST_PYTHONPATH'] == (
'/home/foo/_default/python:/home/user/dev/.pythonpath'
)
Loading

0 comments on commit bb9a844

Please sign in to comment.