From 7fd7db9cd12d59f7c523dc2a1a167f7445e5b2f3 Mon Sep 17 00:00:00 2001 From: Simeon Simeonov Date: Mon, 4 May 2026 18:38:58 +0200 Subject: Add support for the %e{key} format and add support for type checkers (ty) --- tests/conftest.py | 38 ++++++------- tests/test_cli.py | 91 +++++++++++++++++--------------- tests/test_envtoolkit_instance.py | 17 +++--- tests/test_envtoolkit_instance_static.py | 80 +++++++++++++--------------- 4 files changed, 108 insertions(+), 118 deletions(-) (limited to 'tests') diff --git a/tests/conftest.py b/tests/conftest.py index 6cc8b9f..2e8eaf1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -16,14 +16,14 @@ """Common fixtures""" import json +import pathlib import pytest @pytest.fixture -def config_data(): +def config_data() -> dict: """config_data for testing""" - return { 'general': { 'MASTER_PASSWORD_HASH': ( @@ -60,18 +60,16 @@ def config_data(): @pytest.fixture -def config_file(tmp_path, config_data): +def config_file(tmp_path: pathlib.Path, config_data: dict) -> str: """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 long_encrypted_value(): +def long_encrypted_value() -> str: """enc. value corresponding to 'Nobody expects the Spanish inquisition'""" - return ( 'enc-val$2$uYpZM1VfAGq0CDZL2duITs076CQj+hIFEgx+F4mn80o=$' 'UWP5YeRsh5/2vZ2J1UOS+BJti73Kbp6C1pJmCo8hF' @@ -80,27 +78,26 @@ def long_encrypted_value(): @pytest.fixture -def long_value(): +def long_value() -> str: """standard value (> 32 bytes)""" return 'Nobody expects the Spanish inquisition' @pytest.fixture -def master_password(): +def master_password() -> str: """Master passord""" return 'The very secret passwd' @pytest.fixture -def new_master_password(): +def new_master_password() -> str: """Master passord""" return 'New very secret passwd' @pytest.fixture -def non_random_bytes_32(): +def non_random_bytes_32() -> bytes: """always use the same bytes instead of os.urandom(32)""" - return ( b'\xb9\x8aY3U_\x00j\xb4\x086K\xd9\xdb\x88N' b'\xcd;\xe8$#\xfa\x12\x05\x12\x0c~\x17\x89\xa7\xf3J' @@ -108,9 +105,8 @@ def non_random_bytes_32(): @pytest.fixture -def non_random_bytes_57(): +def non_random_bytes_57() -> bytes: """always use the same bytes instead of os.urandom(57)""" - return ( b'D$\x99\xaa\xafiZ\xb4C\xa0%XTz)\xca\xedK\xcd\xa2F~\xff+\xa1[\xe2\xaa' b'\xb2\xd3\x07\x13\xedb\xc2\x84\xfe\tS\r\xf0\x02_\xef\xe3\xde\xf1?e' @@ -119,16 +115,14 @@ def non_random_bytes_57(): @pytest.fixture -def nonexistent_config_file(tmp_path): +def nonexistent_config_file(tmp_path: pathlib.Path) -> str: """temporary config file for testing that includes config_data""" - return str(tmp_path / 'etoolkitt.json') @pytest.fixture -def password_hash(): +def password_hash() -> str: """password hash for testing, corresponding to 'The very secret passwd'""" - return ( 'pbkdf2_sha256$500000$UY3o78KUM1Btzxk3k3JCsijnwtJ2lx+hH9NewpVKxo8=$' 'tHwDm8OVKanC4DoYTigTCb0R3lQIa/CbBYj0B3TZtHg=' @@ -136,9 +130,8 @@ def password_hash(): @pytest.fixture -def short_encrypted_value(): +def short_encrypted_value() -> str: """enc. value corresponding to 'secret1'""" - return ( 'enc-val$2$RCSZqq9pWrRDoCVYVHopyu1LzaJGfv8roVviqrLTBxM=$' '+YYrZbwTBuG0Pl+WMQrvxLUtq5j8qYuQqzoIwgoGt7AaWZCJz+E7qoDeg3wke70ST8U=' @@ -146,9 +139,8 @@ def short_encrypted_value(): @pytest.fixture -def short_encrypted_value_v1(): +def short_encrypted_value_v1() -> str: """enc. value (enc-val 1) corresponding to 'secret1'""" - return ( 'enc-val$1$/cXpEMoZrTlb9yokGhw8tLTSUkqnqJ4ZoAkurNgMYx' 'w=$1VdkSMcZnLRwLiu1M8VlYcbelwmiVNY=' @@ -156,12 +148,12 @@ def short_encrypted_value_v1(): @pytest.fixture -def short_value(): +def short_value() -> str: """standard value (< 32 bytes)""" return 'secret1' @pytest.fixture -def wrong_master_password(): +def wrong_master_password() -> str: """Wrong master passord""" return 'the very secret passwd' diff --git a/tests/test_cli.py b/tests/test_cli.py index b7913da..6e28378 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -26,9 +26,13 @@ from etoolkit.__main__ import main @unittest.mock.patch('builtins.input') -def test_decrypt_v1(binput, capsys, config_file, master_password): +def test_decrypt_v1( + binput: unittest.mock.MagicMock, + capsys: pytest.CaptureFixture, + config_file: str, + master_password: str, +) -> None: """Tests v1 decryption via the CLI interface""" - binput.return_value = ( 'enc-val$1$rye0sMGEnd35gOWyISE1FQa6dzS+8/jf6aopMO5tPr4=$' 'RjnRY0bUJWFOiejTlM3OhKNimQ==' @@ -44,15 +48,14 @@ def test_decrypt_v1(binput, capsys, config_file, master_password): @unittest.mock.patch('builtins.input') def test_decrypt_v2( - binput, - capsys, - config_file, - master_password, - short_encrypted_value, - short_value, -): + binput: unittest.mock.MagicMock, + capsys: pytest.CaptureFixture, + config_file: str, + master_password: str, + short_encrypted_value: str, + short_value: str, +) -> None: """Tests v2 decryption via the CLI interface""" - binput.return_value = short_encrypted_value with unittest.mock.patch.dict( os.environ, {'ETOOLKIT_MASTER_PASSWORD': master_password} @@ -68,17 +71,16 @@ def test_decrypt_v2( @unittest.mock.patch('os.urandom') @unittest.mock.patch('builtins.input') def test_encrypt_with_echo( - binput, - urandom, - capsys, - non_random_bytes_57, - config_file, - master_password, - short_encrypted_value, - short_value, -): + binput: unittest.mock.MagicMock, + urandom: unittest.mock.MagicMock, + capsys: pytest.CaptureFixture, + non_random_bytes_57: bytes, + config_file: str, + master_password: str, + short_encrypted_value: str, + short_value: str, +) -> None: """Tests encryption via the CLI interface""" - binput.return_value = short_value urandom.return_value = non_random_bytes_57 with unittest.mock.patch.dict( @@ -95,17 +97,16 @@ def test_encrypt_with_echo( @unittest.mock.patch('os.urandom') @unittest.mock.patch('getpass.getpass') def test_encrypt_without_echo( - getpass, - urandom, - capsys, - non_random_bytes_57, - config_file, - master_password, - short_encrypted_value, - short_value, -): + getpass: unittest.mock.MagicMock, + urandom: unittest.mock.MagicMock, + capsys: pytest.CaptureFixture, + non_random_bytes_57: bytes, + config_file: str, + master_password: str, + short_encrypted_value: str, + short_value: str, +) -> None: """Tests encryption via the CLI interface""" - getpass.return_value = short_value urandom.return_value = non_random_bytes_57 with unittest.mock.patch.dict( @@ -119,20 +120,26 @@ def test_encrypt_without_echo( ) -def test_fetch_encrypted_value(config_file, master_password, short_value): +def test_fetch_encrypted_value( + config_file: str, master_password: str, short_value: str +) -> None: """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']) + with pytest.raises(SystemExit) as exit_info: + main(['-c', f'{config_file}', '-q', '-s', '/bin/false', 'secret']) + assert exit_info.value.code == 0 assert os.environ.get('ETOOLKIT_TEST_PASSWORD') == short_value -def test_list(capsys, config_file, nonexistent_config_file): +def test_list( + capsys: pytest.CaptureFixture, + config_file: str, + nonexistent_config_file: str, +) -> None: """Tests list via the CLI interface""" - with pytest.raises(SystemExit) as exit_info: main(['-c', nonexistent_config_file, '-l']) assert exit_info.value.code == errno.EIO @@ -142,9 +149,8 @@ def test_list(capsys, config_file, nonexistent_config_file): assert capsys.readouterr().out.strip() == f'dev{os.linesep}secret' -def test_help(capsys): +def test_help(capsys: pytest.CaptureFixture) -> None: """Dummy test checking if the CLI is available at all""" - with pytest.raises(SystemExit) as exit_info: main(['-h']) assert exit_info.value.code == 0 @@ -154,10 +160,12 @@ def test_help(capsys): @unittest.mock.patch('os.urandom') @unittest.mock.patch('getpass.getpass') def test_generate_master_password_hash( - getpass, urandom, capsys, non_random_bytes_32 -): + getpass: unittest.mock.MagicMock, + urandom: unittest.mock.MagicMock, + capsys: pytest.CaptureFixture, + non_random_bytes_32: bytes, +) -> None: """Tests master password hash generation via the CLI interface""" - getpass.return_value = 'The very secret passwd' urandom.return_value = non_random_bytes_32 with pytest.raises(SystemExit) as exit_info: @@ -169,9 +177,8 @@ def test_generate_master_password_hash( ) -def test_version(capsys): +def test_version(capsys: pytest.CaptureFixture) -> None: """Dummy test checking if the CLI is available at all""" - with pytest.raises(SystemExit) as exit_info: main(['-v']) assert exit_info.value.code == 0 diff --git a/tests/test_envtoolkit_instance.py b/tests/test_envtoolkit_instance.py index 5137e9f..3235736 100644 --- a/tests/test_envtoolkit_instance.py +++ b/tests/test_envtoolkit_instance.py @@ -20,9 +20,8 @@ import pytest import etoolkit -def test_instantiation(config_data): +def test_instantiation(config_data: dict) -> None: """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 @@ -43,10 +42,12 @@ def test_instantiation(config_data): def test_get_environ( - config_data, master_password, short_value, wrong_master_password -): + config_data: dict, + master_password: str, + short_value: str, + wrong_master_password: str, +) -> None: """Tests the EtoolkitInstance.get_environ method""" - instance = etoolkit.EtoolkitInstance('secret', config_data) with pytest.raises(etoolkit.EtoolkitInstanceError) as exc_info: env = instance.get_environ() @@ -68,16 +69,14 @@ def test_get_environ( assert 'ETOOLKIT_TEST_PASSWORD2' in instance.sensitive_env_variables -def test_get_full_name(config_data): +def test_get_full_name(config_data: dict) -> None: """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): +def test_parent_vars(config_data: dict) -> None: """Tests the EtoolkitInstance.get_full_name method""" - instance = etoolkit.EtoolkitInstance('dev', config_data) assert instance.get_full_name('->') == '_default->dev' assert instance.get_environ()['ETOOLKIT_TEST_PYTHONPATH'] == ( diff --git a/tests/test_envtoolkit_instance_static.py b/tests/test_envtoolkit_instance_static.py index e13d439..8c21a0f 100644 --- a/tests/test_envtoolkit_instance_static.py +++ b/tests/test_envtoolkit_instance_static.py @@ -23,9 +23,10 @@ import etoolkit @unittest.mock.patch('getpass.getpass') -def test_confirm_password_prompt(getpass, password_hash, master_password): +def test_confirm_password_prompt( + getpass: unittest.mock.MagicMock, password_hash: str, master_password: str +) -> None: """Tests the static EtoolkitInstance.confirm_password_prompt method""" - getpass.return_value = master_password assert ( etoolkit.EtoolkitInstance.confirm_password_prompt(password_hash) @@ -37,9 +38,10 @@ def test_confirm_password_prompt(getpass, password_hash, master_password): ) -def test_decrypt_v1(master_password, short_encrypted_value_v1, short_value): +def test_decrypt_v1( + master_password: str, short_encrypted_value_v1: str, short_value: str +) -> None: """Tests the static EtoolkitInstance.decrypt method""" - assert ( etoolkit.EtoolkitInstance.decrypt( master_password, short_encrypted_value_v1 @@ -59,10 +61,9 @@ def test_decrypt_v1(master_password, short_encrypted_value_v1, short_value): def test_decrypt_v2_no_padding( - master_password, long_encrypted_value, long_value -): + master_password: str, long_encrypted_value: str, long_value: str +) -> None: """Tests the static EtoolkitInstance.decrypt method for v2 - no padding""" - assert ( etoolkit.EtoolkitInstance.decrypt( master_password, long_encrypted_value @@ -80,10 +81,9 @@ def test_decrypt_v2_no_padding( def test_decrypt_v2_with_padding( - master_password, short_encrypted_value, short_value -): + master_password: str, short_encrypted_value: str, short_value: str +) -> None: """Tests the static EtoolkitInstance.decrypt method for v2 with padding""" - assert ( etoolkit.EtoolkitInstance.decrypt( master_password, short_encrypted_value @@ -99,9 +99,8 @@ def test_decrypt_v2_with_padding( assert exc_info.value.args[0] == f'Invalid tag when decrypting: {edata}' -def test_encrypt_no_padding(master_password, long_value): +def test_encrypt_no_padding(master_password: str, long_value: str) -> None: """Tests the static EtoolkitInstance.encrypt method with a long string""" - edata = etoolkit.EtoolkitInstance.encrypt(master_password, long_value) assert edata.startswith('enc-val$2$') assert len(edata) == 131 @@ -111,9 +110,8 @@ def test_encrypt_no_padding(master_password, long_value): ) -def test_encrypt_with_padding(master_password, short_value): +def test_encrypt_with_padding(master_password: str, short_value: str) -> None: """Tests the static EtoolkitInstance.encrypt method with a short string""" - edata = etoolkit.EtoolkitInstance.encrypt(master_password, short_value) assert edata.startswith('enc-val$2$') assert len(edata) == 123 @@ -125,14 +123,13 @@ def test_encrypt_with_padding(master_password, short_value): @unittest.mock.patch('os.urandom') def test_encrypt_staticly_no_padding( - urandom, - master_password, - non_random_bytes_32, - long_encrypted_value, - long_value, -): + urandom: unittest.mock.MagicMock, + master_password: str, + non_random_bytes_32: bytes, + long_encrypted_value: str, + long_value: str, +) -> None: """Tests the EtoolkitInstance.encrypt method always with the same salt""" - urandom.return_value = non_random_bytes_32 edata = etoolkit.EtoolkitInstance.encrypt(master_password, long_value) assert edata == long_encrypted_value @@ -144,14 +141,13 @@ def test_encrypt_staticly_no_padding( @unittest.mock.patch('os.urandom') def test_encrypt_staticly_with_padding( - urandom, - master_password, - non_random_bytes_57, - short_encrypted_value, - short_value, -): + urandom: unittest.mock.MagicMock, + master_password: str, + non_random_bytes_57: bytes, + short_encrypted_value: str, + short_value: str, +) -> None: """Tests the EtoolkitInstance.encrypt method always with the same salt""" - urandom.return_value = non_random_bytes_57 edata = etoolkit.EtoolkitInstance.encrypt(master_password, short_value) assert edata == short_encrypted_value @@ -160,9 +156,8 @@ def test_encrypt_staticly_with_padding( ) -def test_get_new_password_hash(master_password): +def test_get_new_password_hash(master_password: str) -> None: """Tests the static EtoolkitInstance.get_new_password_hash method""" - new_hash = etoolkit.EtoolkitInstance.get_new_password_hash(master_password) # all pbkdf2 params are the same / hardcoded for the time being assert new_hash.startswith('pbkdf2_sha256$500000$') @@ -173,9 +168,8 @@ def test_get_new_password_hash(master_password): ) -def test_parse_value(): +def test_parse_value() -> None: """Tests the static EtoolkitInstance.parse_value method""" - assert ( etoolkit.EtoolkitInstance.parse_value('t%bs%t', {'%b': 'e', '%t': 't'}) == 'test' @@ -183,10 +177,9 @@ def test_parse_value(): def test_password_matches( - password_hash, master_password, wrong_master_password -): + password_hash: str, master_password: str, wrong_master_password: str +) -> None: """Tests the static EtoolkitInstance.password_matches method""" - assert etoolkit.EtoolkitInstance.password_matches( master_password, password_hash ) @@ -197,16 +190,15 @@ def test_password_matches( @unittest.mock.patch('os.urandom') def test_reencrypt_staticly_with_padding( - urandom, - master_password, - new_master_password, - non_random_bytes_57, - short_encrypted_value, - short_encrypted_value_v1, - short_value, -): + urandom: unittest.mock.MagicMock, + master_password: str, + new_master_password: str, + non_random_bytes_57: bytes, + short_encrypted_value: str, + short_encrypted_value_v1: str, + short_value: str, +) -> None: """Tests the EtoolkitInstance.reencrypt method always with the same salt""" - urandom.return_value = non_random_bytes_57 # reencrypt (migrate) v1 to current using the same password edata = etoolkit.EtoolkitInstance.reencrypt( -- cgit v1.3