diff options
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/conftest.py | 31 | ||||
| -rw-r--r-- | tests/test_cli.py | 97 | ||||
| -rw-r--r-- | tests/test_envtoolkit_instance.py | 8 | ||||
| -rw-r--r-- | tests/test_envtoolkit_instance_static.py | 43 |
4 files changed, 155 insertions, 24 deletions
diff --git a/tests/conftest.py b/tests/conftest.py index 1f70259..0770adb 100644 --- a/tests/conftest.py +++ b/tests/conftest.py | |||
| @@ -1,5 +1,5 @@ | |||
| 1 | # etoolkit | 1 | # etoolkit |
| 2 | # Copyright (C) 2021 Simeon Simeonov | 2 | # Copyright (C) 2021-2022 Simeon Simeonov |
| 3 | 3 | ||
| 4 | # This program is free software: you can redistribute it and/or modify | 4 | # This program is free software: you can redistribute it and/or modify |
| 5 | # it under the terms of the GNU General Public License as published by | 5 | # it under the terms of the GNU General Public License as published by |
| @@ -14,6 +14,8 @@ | |||
| 14 | # You should have received a copy of the GNU General Public License | 14 | # You should have received a copy of the GNU General Public License |
| 15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | 15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
| 16 | """Common fixtures""" | 16 | """Common fixtures""" |
| 17 | import json | ||
| 18 | |||
| 17 | import pytest | 19 | import pytest |
| 18 | 20 | ||
| 19 | 21 | ||
| @@ -23,8 +25,8 @@ def config_data(): | |||
| 23 | return { | 25 | return { |
| 24 | 'general': { | 26 | 'general': { |
| 25 | 'MASTER_PASSWORD_HASH': ( | 27 | 'MASTER_PASSWORD_HASH': ( |
| 26 | 'pbkdf2_sha256$100000$kFOQkAPtStZ/Ny/O4501ygHGQnqh5Y+ySxF9qVHr' | 28 | 'pbkdf2_sha256$100000$uYpZM1VfAGq0CDZL2duITs076CQj+hIFEgx+F4m' |
| 27 | 'iv8=$3BujuWzn3CfDnw4yiD9m3F+GjeW1MHHW40R/ThHNcn0=' | 29 | 'n80o=$h3PSPLCd37fP15zKdW4CBGn7CXE+q5UiydaF3vbeZHo=' |
| 28 | ) | 30 | ) |
| 29 | }, | 31 | }, |
| 30 | 'instances': { | 32 | 'instances': { |
| @@ -51,9 +53,26 @@ def config_data(): | |||
| 51 | 53 | ||
| 52 | 54 | ||
| 53 | @pytest.fixture | 55 | @pytest.fixture |
| 56 | def config_file(tmp_path, config_data): | ||
| 57 | """temporary config file for testing that includes config_data""" | ||
| 58 | cf = tmp_path / "etoolkit.json" | ||
| 59 | cf.write_text(json.dumps(config_data)) | ||
| 60 | return str(cf) | ||
| 61 | |||
| 62 | |||
| 63 | @pytest.fixture | ||
| 64 | def non_random_bytes_32(): | ||
| 65 | """always use the same bytes instead of os.urandom(32)""" | ||
| 66 | return ( | ||
| 67 | b'\xb9\x8aY3U_\x00j\xb4\x086K\xd9\xdb\x88N' | ||
| 68 | b'\xcd;\xe8$#\xfa\x12\x05\x12\x0c~\x17\x89\xa7\xf3J' | ||
| 69 | ) | ||
| 70 | |||
| 71 | |||
| 72 | @pytest.fixture | ||
| 54 | def password_hash(): | 73 | def password_hash(): |
| 55 | """password hash for testing, corresponding to 'the very secret passwd'""" | 74 | """password hash for testing, corresponding to 'The very secret passwd'""" |
| 56 | return ( | 75 | return ( |
| 57 | 'pbkdf2_sha256$100000$kFOQkAPtStZ/Ny/O4501ygHGQnqh5Y+ySxF9qVHr' | 76 | 'pbkdf2_sha256$100000$uYpZM1VfAGq0CDZL2duITs076CQj+hIFEgx+F4mn80o=$h3' |
| 58 | 'iv8=$3BujuWzn3CfDnw4yiD9m3F+GjeW1MHHW40R/ThHNcn0=' | 77 | 'PSPLCd37fP15zKdW4CBGn7CXE+q5UiydaF3vbeZHo=' |
| 59 | ) | 78 | ) |
diff --git a/tests/test_cli.py b/tests/test_cli.py index 1bd9be1..884e676 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py | |||
| @@ -1,5 +1,5 @@ | |||
| 1 | # etoolkit | 1 | # etoolkit |
| 2 | # Copyright (C) 2021 Simeon Simeonov | 2 | # Copyright (C) 2021-2022 Simeon Simeonov |
| 3 | 3 | ||
| 4 | # This program is free software: you can redistribute it and/or modify | 4 | # This program is free software: you can redistribute it and/or modify |
| 5 | # it under the terms of the GNU General Public License as published by | 5 | # it under the terms of the GNU General Public License as published by |
| @@ -14,12 +14,86 @@ | |||
| 14 | # You should have received a copy of the GNU General Public License | 14 | # You should have received a copy of the GNU General Public License |
| 15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | 15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
| 16 | """Tests for the CLI (etoolkit.__main__""" | 16 | """Tests for the CLI (etoolkit.__main__""" |
| 17 | import errno | ||
| 18 | import os | ||
| 19 | import unittest.mock | ||
| 20 | |||
| 17 | import pytest | 21 | import pytest |
| 18 | 22 | ||
| 19 | import etoolkit | 23 | import etoolkit |
| 20 | from etoolkit.__main__ import main | 24 | from etoolkit.__main__ import main |
| 21 | 25 | ||
| 22 | 26 | ||
| 27 | @unittest.mock.patch('os.urandom') | ||
| 28 | @unittest.mock.patch('builtins.input') | ||
| 29 | def test_decrypt(binput, urandom, capsys, non_random_bytes_32, config_file): | ||
| 30 | """Tests encryption via the CLI interface""" | ||
| 31 | urandom.return_value = non_random_bytes_32 | ||
| 32 | binput.return_value = ( | ||
| 33 | 'enc-val$1$uYpZM1VfAGq0CDZL2duITs076CQj+' | ||
| 34 | 'hIFEgx+F4mn80o=$xdF/1S+R2MGlEQMCOLG6OjEuzw==' | ||
| 35 | ) | ||
| 36 | with unittest.mock.patch.dict( | ||
| 37 | os.environ, {'ETOOLKIT_MASTER_PASSWORD': 'the very secret passwd'} | ||
| 38 | ): | ||
| 39 | with pytest.raises(SystemExit) as exit_info: | ||
| 40 | main(['-c', f'{config_file}', '-d']) | ||
| 41 | assert exit_info.type == SystemExit | ||
| 42 | assert exit_info.value.code == 0 | ||
| 43 | assert capsys.readouterr().out.strip() == 'Decrypted value: bar' | ||
| 44 | |||
| 45 | |||
| 46 | @unittest.mock.patch('os.urandom') | ||
| 47 | @unittest.mock.patch('builtins.input', lambda *args: 'bar') | ||
| 48 | def test_encrypt_with_echo(urandom, capsys, non_random_bytes_32, config_file): | ||
| 49 | """Tests encryption via the CLI interface""" | ||
| 50 | urandom.return_value = non_random_bytes_32 | ||
| 51 | with unittest.mock.patch.dict( | ||
| 52 | os.environ, {'ETOOLKIT_MASTER_PASSWORD': 'the very secret passwd'} | ||
| 53 | ): | ||
| 54 | with pytest.raises(SystemExit) as exit_info: | ||
| 55 | main(['-c', f'{config_file}', '-e', '-E']) | ||
| 56 | assert exit_info.type == SystemExit | ||
| 57 | assert exit_info.value.code == 0 | ||
| 58 | assert capsys.readouterr().out.strip() == ( | ||
| 59 | 'Encrypted value: enc-val$1$uYpZM1VfAGq0CDZL2duITs076CQj+' | ||
| 60 | 'hIFEgx+F4mn80o=$xdF/1S+R2MGlEQMCOLG6OjEuzw==' | ||
| 61 | ) | ||
| 62 | |||
| 63 | |||
| 64 | @unittest.mock.patch('os.urandom') | ||
| 65 | @unittest.mock.patch('getpass.getpass', lambda *args: 'bar') | ||
| 66 | def test_encrypt_without_echo(gpass, capsys, non_random_bytes_32, config_file): | ||
| 67 | """Tests encryption via the CLI interface""" | ||
| 68 | gpass.return_value = non_random_bytes_32 | ||
| 69 | with unittest.mock.patch.dict( | ||
| 70 | os.environ, {'ETOOLKIT_MASTER_PASSWORD': 'the very secret passwd'} | ||
| 71 | ): | ||
| 72 | with pytest.raises(SystemExit) as exit_info: | ||
| 73 | main(['-c', f'{config_file}', '-e']) | ||
| 74 | assert exit_info.type == SystemExit | ||
| 75 | assert exit_info.value.code == 0 | ||
| 76 | assert capsys.readouterr().out.strip() == ( | ||
| 77 | 'Encrypted value: enc-val$1$uYpZM1VfAGq0CDZL2duITs076CQj+' | ||
| 78 | 'hIFEgx+F4mn80o=$xdF/1S+R2MGlEQMCOLG6OjEuzw==' | ||
| 79 | ) | ||
| 80 | |||
| 81 | |||
| 82 | def test_list(capsys, config_file): | ||
| 83 | """Tests list via the CLI interface""" | ||
| 84 | with pytest.raises(SystemExit) as exit_info: | ||
| 85 | main(['-l']) | ||
| 86 | assert exit_info.type == SystemExit | ||
| 87 | assert exit_info.value.code == errno.EIO | ||
| 88 | with pytest.raises(SystemExit) as exit_info: | ||
| 89 | main(['-c', f'{config_file}', '-l']) | ||
| 90 | assert exit_info.type == SystemExit | ||
| 91 | assert exit_info.value.code == 0 | ||
| 92 | assert capsys.readouterr().out.strip() == ( | ||
| 93 | f'default{os.linesep}dev{os.linesep}secret' | ||
| 94 | ) | ||
| 95 | |||
| 96 | |||
| 23 | def test_help(capsys): | 97 | def test_help(capsys): |
| 24 | """Dummy test checking if the CLI is available at all""" | 98 | """Dummy test checking if the CLI is available at all""" |
| 25 | with pytest.raises(SystemExit) as exit_info: | 99 | with pytest.raises(SystemExit) as exit_info: |
| @@ -29,6 +103,27 @@ def test_help(capsys): | |||
| 29 | assert capsys.readouterr().out.startswith('usage: etoolkit') | 103 | assert capsys.readouterr().out.startswith('usage: etoolkit') |
| 30 | 104 | ||
| 31 | 105 | ||
| 106 | @unittest.mock.patch('os.urandom') | ||
| 107 | @unittest.mock.patch('getpass.getpass') | ||
| 108 | def test_generate_master_password_hash( | ||
| 109 | gpass, | ||
| 110 | urandom, | ||
| 111 | capsys, | ||
| 112 | non_random_bytes_32, | ||
| 113 | ): | ||
| 114 | """Tests master password hash generation via the CLI interface""" | ||
| 115 | urandom.return_value = non_random_bytes_32 | ||
| 116 | gpass.return_value = 'The very secret passwd' | ||
| 117 | with pytest.raises(SystemExit) as exit_info: | ||
| 118 | main(['--generate-master-password-hash']) | ||
| 119 | assert exit_info.type == SystemExit | ||
| 120 | assert exit_info.value.code == 0 | ||
| 121 | assert capsys.readouterr().out.strip() == ( | ||
| 122 | 'Master password hash: pbkdf2_sha256$100000$uYpZM1VfAGq0CDZL2duITs076' | ||
| 123 | 'CQj+hIFEgx+F4mn80o=$h3PSPLCd37fP15zKdW4CBGn7CXE+q5UiydaF3vbeZHo=' | ||
| 124 | ) | ||
| 125 | |||
| 126 | |||
| 32 | def test_version(capsys): | 127 | def test_version(capsys): |
| 33 | """Dummy test checking if the CLI is available at all""" | 128 | """Dummy test checking if the CLI is available at all""" |
| 34 | with pytest.raises(SystemExit) as exit_info: | 129 | with pytest.raises(SystemExit) as exit_info: |
diff --git a/tests/test_envtoolkit_instance.py b/tests/test_envtoolkit_instance.py index 2ffacd2..9061dc8 100644 --- a/tests/test_envtoolkit_instance.py +++ b/tests/test_envtoolkit_instance.py | |||
| @@ -1,5 +1,5 @@ | |||
| 1 | # etoolkit | 1 | # etoolkit |
| 2 | # Copyright (C) 2021 Simeon Simeonov | 2 | # Copyright (C) 2021-22 Simeon Simeonov |
| 3 | 3 | ||
| 4 | # This program is free software: you can redistribute it and/or modify | 4 | # This program is free software: you can redistribute it and/or modify |
| 5 | # it under the terms of the GNU General Public License as published by | 5 | # it under the terms of the GNU General Public License as published by |
| @@ -19,7 +19,7 @@ import pytest | |||
| 19 | import etoolkit | 19 | import etoolkit |
| 20 | 20 | ||
| 21 | 21 | ||
| 22 | def test_instantiation(config_data, password_hash): | 22 | def test_instantiation(config_data): |
| 23 | """Tests for object instatiation""" | 23 | """Tests for object instatiation""" |
| 24 | with pytest.raises(etoolkit.EtoolkitInstanceError) as exc_info: | 24 | with pytest.raises(etoolkit.EtoolkitInstanceError) as exc_info: |
| 25 | instance = etoolkit.EtoolkitInstance('devv', config_data) | 25 | instance = etoolkit.EtoolkitInstance('devv', config_data) |
| @@ -32,7 +32,9 @@ def test_instantiation(config_data, password_hash): | |||
| 32 | assert 'DB_CONNECTION' not in instance.sensitive_env_variables | 32 | assert 'DB_CONNECTION' not in instance.sensitive_env_variables |
| 33 | assert 'PASSWORD' in instance.sensitive_env_variables | 33 | assert 'PASSWORD' in instance.sensitive_env_variables |
| 34 | assert instance.name == 'secret' | 34 | assert instance.name == 'secret' |
| 35 | assert instance.master_password_hash == password_hash | 35 | assert instance.master_password_hash == ( |
| 36 | config_data['general']['MASTER_PASSWORD_HASH'] | ||
| 37 | ) | ||
| 36 | assert instance.master_password is None | 38 | assert instance.master_password is None |
| 37 | 39 | ||
| 38 | 40 | ||
diff --git a/tests/test_envtoolkit_instance_static.py b/tests/test_envtoolkit_instance_static.py index d7ac857..73a5f36 100644 --- a/tests/test_envtoolkit_instance_static.py +++ b/tests/test_envtoolkit_instance_static.py | |||
| @@ -1,5 +1,5 @@ | |||
| 1 | # etoolkit | 1 | # etoolkit |
| 2 | # Copyright (C) 2021 Simeon Simeonov | 2 | # Copyright (C) 2021-2022 Simeon Simeonov |
| 3 | 3 | ||
| 4 | # This program is free software: you can redistribute it and/or modify | 4 | # This program is free software: you can redistribute it and/or modify |
| 5 | # it under the terms of the GNU General Public License as published by | 5 | # it under the terms of the GNU General Public License as published by |
| @@ -24,14 +24,14 @@ import etoolkit | |||
| 24 | @unittest.mock.patch('getpass.getpass') | 24 | @unittest.mock.patch('getpass.getpass') |
| 25 | def test_confirm_password_prompt(getpass, password_hash): | 25 | def test_confirm_password_prompt(getpass, password_hash): |
| 26 | """Tests the static EtoolkitInstance.confirm_password_prompt method""" | 26 | """Tests the static EtoolkitInstance.confirm_password_prompt method""" |
| 27 | getpass.return_value = 'the very secret passwd' | 27 | getpass.return_value = 'The very secret passwd' |
| 28 | assert ( | 28 | assert ( |
| 29 | etoolkit.EtoolkitInstance.confirm_password_prompt(password_hash) | 29 | etoolkit.EtoolkitInstance.confirm_password_prompt(password_hash) |
| 30 | == 'the very secret passwd' | 30 | == 'The very secret passwd' |
| 31 | ) | 31 | ) |
| 32 | assert ( | 32 | assert ( |
| 33 | etoolkit.EtoolkitInstance.confirm_password_prompt(password_hash, False) | 33 | etoolkit.EtoolkitInstance.confirm_password_prompt(password_hash, False) |
| 34 | == 'the very secret passwd' | 34 | == 'The very secret passwd' |
| 35 | ) | 35 | ) |
| 36 | 36 | ||
| 37 | 37 | ||
| @@ -39,10 +39,10 @@ def test_decrypt(): | |||
| 39 | """Tests the static EtoolkitInstance.decrypt method""" | 39 | """Tests the static EtoolkitInstance.decrypt method""" |
| 40 | assert ( | 40 | assert ( |
| 41 | etoolkit.EtoolkitInstance.decrypt( | 41 | etoolkit.EtoolkitInstance.decrypt( |
| 42 | 'the very secret passwd', | 42 | 'The very secret passwd', |
| 43 | ( | 43 | ( |
| 44 | 'enc-val$1$Y/TBb1F3siHTw6qZg9ERzZfA8PLPf2CwGSQLpu9jYWw=$FT5tS9' | 44 | 'enc-val$1$/cXpEMoZrTlb9yokGhw8tLTSUkqnqJ4ZoAkurNgMYx' |
| 45 | 'o+ABvsxogIXpJim16Gz5SVtV8=' | 45 | 'w=$1VdkSMcZnLRwLiu1M8VlYcbelwmiVNY=' |
| 46 | ), | 46 | ), |
| 47 | ) | 47 | ) |
| 48 | == 'secret1' | 48 | == 'secret1' |
| @@ -51,11 +51,11 @@ def test_decrypt(): | |||
| 51 | # now test with modified edata | 51 | # now test with modified edata |
| 52 | with pytest.raises(etoolkit.EtoolkitInstanceError) as exc_info: | 52 | with pytest.raises(etoolkit.EtoolkitInstanceError) as exc_info: |
| 53 | edata = ( | 53 | edata = ( |
| 54 | 'enc-val$1$Y/TBb1F3siHTw6qZg9ERzZfA8PLPf2CwGSQLpu9jYWw=$FT5tS9' | 54 | 'enc-val$1$/cXpEMoZrTlb9yokGhw8tLTSUkqnqJ5ZoAkurNgMYx' |
| 55 | 'o+ABvsxogIXpJim17Gz5SVtV8=' | 55 | 'w=$1VdkSMcZnLRwLiu1M8VlYcbelwmiVNY=' |
| 56 | ) | 56 | ) |
| 57 | etoolkit.EtoolkitInstance.decrypt( | 57 | etoolkit.EtoolkitInstance.decrypt( |
| 58 | 'the very secret passwd', edata | 58 | 'The very secret passwd', edata |
| 59 | ) == 'secret1' | 59 | ) == 'secret1' |
| 60 | assert exc_info.type is etoolkit.EtoolkitInstanceError | 60 | assert exc_info.type is etoolkit.EtoolkitInstanceError |
| 61 | assert exc_info.value.args[0] == f'Invalid tag when decrypting: {edata}' | 61 | assert exc_info.value.args[0] == f'Invalid tag when decrypting: {edata}' |
| @@ -70,17 +70,32 @@ def test_encrypt(): | |||
| 70 | assert edata != etoolkit.EtoolkitInstance.encrypt('foo', 'bar') | 70 | assert edata != etoolkit.EtoolkitInstance.encrypt('foo', 'bar') |
| 71 | 71 | ||
| 72 | 72 | ||
| 73 | @unittest.mock.patch('os.urandom') | ||
| 74 | def test_encrypt_staticly(urandom, non_random_bytes_32): | ||
| 75 | """Tests the EtoolkitInstance.encrypt method always with the same salt""" | ||
| 76 | urandom.return_value = non_random_bytes_32 | ||
| 77 | edata = etoolkit.EtoolkitInstance.encrypt('The very secret passwd', 'bar') | ||
| 78 | assert edata == ( | ||
| 79 | 'enc-val$1$uYpZM1VfAGq0CDZL2duITs076CQj+hIFEgx+F4mn80' | ||
| 80 | 'o=$HjPFNv6xC5hbMrFc0L5lSkWdfQ==' | ||
| 81 | ) | ||
| 82 | assert edata == etoolkit.EtoolkitInstance.encrypt( | ||
| 83 | 'The very secret passwd', | ||
| 84 | 'bar', | ||
| 85 | ) | ||
| 86 | |||
| 87 | |||
| 73 | def test_get_new_password_hash(): | 88 | def test_get_new_password_hash(): |
| 74 | """Tests the static EtoolkitInstance.get_new_password_hash method""" | 89 | """Tests the static EtoolkitInstance.get_new_password_hash method""" |
| 75 | new_hash = etoolkit.EtoolkitInstance.get_new_password_hash( | 90 | new_hash = etoolkit.EtoolkitInstance.get_new_password_hash( |
| 76 | 'the very secret passwd' | 91 | 'The very secret passwd' |
| 77 | ) | 92 | ) |
| 78 | # all pbkdf2 params are the same / hardcoded for the time being | 93 | # all pbkdf2 params are the same / hardcoded for the time being |
| 79 | assert new_hash.startswith('pbkdf2_sha256$100000$') | 94 | assert new_hash.startswith('pbkdf2_sha256$100000$') |
| 80 | assert len(new_hash) == 110 | 95 | assert len(new_hash) == 110 |
| 81 | # the hash should always be different because of random salting | 96 | # the hash should always be different because of random salting |
| 82 | assert new_hash != etoolkit.EtoolkitInstance.get_new_password_hash( | 97 | assert new_hash != etoolkit.EtoolkitInstance.get_new_password_hash( |
| 83 | 'the very secret passwd' | 98 | 'The very secret passwd' |
| 84 | ) | 99 | ) |
| 85 | 100 | ||
| 86 | 101 | ||
| @@ -95,8 +110,8 @@ def test_parse_value(): | |||
| 95 | def test_password_matches(password_hash): | 110 | def test_password_matches(password_hash): |
| 96 | """Tests the static EtoolkitInstance.password_matches method""" | 111 | """Tests the static EtoolkitInstance.password_matches method""" |
| 97 | assert etoolkit.EtoolkitInstance.password_matches( | 112 | assert etoolkit.EtoolkitInstance.password_matches( |
| 98 | 'the very secret passwd', password_hash | 113 | 'The very secret passwd', password_hash |
| 99 | ) | 114 | ) |
| 100 | assert not etoolkit.EtoolkitInstance.password_matches( | 115 | assert not etoolkit.EtoolkitInstance.password_matches( |
| 101 | 'the very secret passwdo', password_hash | 116 | 'The very secret passwdo', password_hash |
| 102 | ) | 117 | ) |
