From 907f80e1889781a69edded00126b3e78ddeb7b8e Mon Sep 17 00:00:00 2001
From: Simeon Simeonov
Date: Fri, 4 Mar 2022 20:48:20 +0100
Subject: Add the -P param, bash completion script and improve the README.md
---
tests/conftest.py | 31 ++++++++--
tests/test_cli.py | 97 +++++++++++++++++++++++++++++++-
tests/test_envtoolkit_instance.py | 8 ++-
tests/test_envtoolkit_instance_static.py | 43 +++++++++-----
4 files changed, 155 insertions(+), 24 deletions(-)
(limited to 'tests')
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 @@
# etoolkit
-# Copyright (C) 2021 Simeon Simeonov
+# Copyright (C) 2021-2022 Simeon Simeonov
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -14,6 +14,8 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
"""Common fixtures"""
+import json
+
import pytest
@@ -23,8 +25,8 @@ def config_data():
return {
'general': {
'MASTER_PASSWORD_HASH': (
- 'pbkdf2_sha256$100000$kFOQkAPtStZ/Ny/O4501ygHGQnqh5Y+ySxF9qVHr'
- 'iv8=$3BujuWzn3CfDnw4yiD9m3F+GjeW1MHHW40R/ThHNcn0='
+ 'pbkdf2_sha256$100000$uYpZM1VfAGq0CDZL2duITs076CQj+hIFEgx+F4m'
+ 'n80o=$h3PSPLCd37fP15zKdW4CBGn7CXE+q5UiydaF3vbeZHo='
)
},
'instances': {
@@ -50,10 +52,27 @@ def config_data():
}
+@pytest.fixture
+def config_file(tmp_path, 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 non_random_bytes_32():
+ """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'
+ )
+
+
@pytest.fixture
def password_hash():
- """password hash for testing, corresponding to 'the very secret passwd'"""
+ """password hash for testing, corresponding to 'The very secret passwd'"""
return (
- 'pbkdf2_sha256$100000$kFOQkAPtStZ/Ny/O4501ygHGQnqh5Y+ySxF9qVHr'
- 'iv8=$3BujuWzn3CfDnw4yiD9m3F+GjeW1MHHW40R/ThHNcn0='
+ 'pbkdf2_sha256$100000$uYpZM1VfAGq0CDZL2duITs076CQj+hIFEgx+F4mn80o=$h3'
+ 'PSPLCd37fP15zKdW4CBGn7CXE+q5UiydaF3vbeZHo='
)
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 @@
# etoolkit
-# Copyright (C) 2021 Simeon Simeonov
+# Copyright (C) 2021-2022 Simeon Simeonov
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -14,12 +14,86 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
"""Tests for the CLI (etoolkit.__main__"""
+import errno
+import os
+import unittest.mock
+
import pytest
import etoolkit
from etoolkit.__main__ import main
+@unittest.mock.patch('os.urandom')
+@unittest.mock.patch('builtins.input')
+def test_decrypt(binput, urandom, capsys, non_random_bytes_32, config_file):
+ """Tests encryption via the CLI interface"""
+ urandom.return_value = non_random_bytes_32
+ binput.return_value = (
+ 'enc-val$1$uYpZM1VfAGq0CDZL2duITs076CQj+'
+ 'hIFEgx+F4mn80o=$xdF/1S+R2MGlEQMCOLG6OjEuzw=='
+ )
+ with unittest.mock.patch.dict(
+ os.environ, {'ETOOLKIT_MASTER_PASSWORD': 'the very secret passwd'}
+ ):
+ with pytest.raises(SystemExit) as exit_info:
+ main(['-c', f'{config_file}', '-d'])
+ assert exit_info.type == SystemExit
+ assert exit_info.value.code == 0
+ assert capsys.readouterr().out.strip() == 'Decrypted value: bar'
+
+
+@unittest.mock.patch('os.urandom')
+@unittest.mock.patch('builtins.input', lambda *args: 'bar')
+def test_encrypt_with_echo(urandom, capsys, non_random_bytes_32, config_file):
+ """Tests encryption via the CLI interface"""
+ urandom.return_value = non_random_bytes_32
+ with unittest.mock.patch.dict(
+ os.environ, {'ETOOLKIT_MASTER_PASSWORD': 'the very secret passwd'}
+ ):
+ 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$1$uYpZM1VfAGq0CDZL2duITs076CQj+'
+ 'hIFEgx+F4mn80o=$xdF/1S+R2MGlEQMCOLG6OjEuzw=='
+ )
+
+
+@unittest.mock.patch('os.urandom')
+@unittest.mock.patch('getpass.getpass', lambda *args: 'bar')
+def test_encrypt_without_echo(gpass, capsys, non_random_bytes_32, config_file):
+ """Tests encryption via the CLI interface"""
+ gpass.return_value = non_random_bytes_32
+ with unittest.mock.patch.dict(
+ os.environ, {'ETOOLKIT_MASTER_PASSWORD': 'the very secret passwd'}
+ ):
+ 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$1$uYpZM1VfAGq0CDZL2duITs076CQj+'
+ 'hIFEgx+F4mn80o=$xdF/1S+R2MGlEQMCOLG6OjEuzw=='
+ )
+
+
+def test_list(capsys, config_file):
+ """Tests list via the CLI interface"""
+ with pytest.raises(SystemExit) as exit_info:
+ main(['-l'])
+ assert exit_info.type == SystemExit
+ assert exit_info.value.code == errno.EIO
+ with pytest.raises(SystemExit) as exit_info:
+ main(['-c', f'{config_file}', '-l'])
+ assert exit_info.type == SystemExit
+ assert exit_info.value.code == 0
+ assert capsys.readouterr().out.strip() == (
+ f'default{os.linesep}dev{os.linesep}secret'
+ )
+
+
def test_help(capsys):
"""Dummy test checking if the CLI is available at all"""
with pytest.raises(SystemExit) as exit_info:
@@ -29,6 +103,27 @@ def test_help(capsys):
assert capsys.readouterr().out.startswith('usage: etoolkit')
+@unittest.mock.patch('os.urandom')
+@unittest.mock.patch('getpass.getpass')
+def test_generate_master_password_hash(
+ gpass,
+ urandom,
+ capsys,
+ non_random_bytes_32,
+):
+ """Tests master password hash generation via the CLI interface"""
+ 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
+ assert exit_info.value.code == 0
+ assert capsys.readouterr().out.strip() == (
+ 'Master password hash: pbkdf2_sha256$100000$uYpZM1VfAGq0CDZL2duITs076'
+ 'CQj+hIFEgx+F4mn80o=$h3PSPLCd37fP15zKdW4CBGn7CXE+q5UiydaF3vbeZHo='
+ )
+
+
def test_version(capsys):
"""Dummy test checking if the CLI is available at all"""
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 @@
# etoolkit
-# Copyright (C) 2021 Simeon Simeonov
+# Copyright (C) 2021-22 Simeon Simeonov
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -19,7 +19,7 @@ import pytest
import etoolkit
-def test_instantiation(config_data, password_hash):
+def test_instantiation(config_data):
"""Tests for object instatiation"""
with pytest.raises(etoolkit.EtoolkitInstanceError) as exc_info:
instance = etoolkit.EtoolkitInstance('devv', config_data)
@@ -32,7 +32,9 @@ def test_instantiation(config_data, password_hash):
assert 'DB_CONNECTION' not in instance.sensitive_env_variables
assert 'PASSWORD' in instance.sensitive_env_variables
assert instance.name == 'secret'
- assert instance.master_password_hash == password_hash
+ assert instance.master_password_hash == (
+ config_data['general']['MASTER_PASSWORD_HASH']
+ )
assert instance.master_password is None
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 @@
# etoolkit
-# Copyright (C) 2021 Simeon Simeonov
+# Copyright (C) 2021-2022 Simeon Simeonov
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -24,14 +24,14 @@ import etoolkit
@unittest.mock.patch('getpass.getpass')
def test_confirm_password_prompt(getpass, password_hash):
"""Tests the static EtoolkitInstance.confirm_password_prompt method"""
- getpass.return_value = 'the very secret passwd'
+ getpass.return_value = 'The very secret passwd'
assert (
etoolkit.EtoolkitInstance.confirm_password_prompt(password_hash)
- == 'the very secret passwd'
+ == 'The very secret passwd'
)
assert (
etoolkit.EtoolkitInstance.confirm_password_prompt(password_hash, False)
- == 'the very secret passwd'
+ == 'The very secret passwd'
)
@@ -39,10 +39,10 @@ def test_decrypt():
"""Tests the static EtoolkitInstance.decrypt method"""
assert (
etoolkit.EtoolkitInstance.decrypt(
- 'the very secret passwd',
+ 'The very secret passwd',
(
- 'enc-val$1$Y/TBb1F3siHTw6qZg9ERzZfA8PLPf2CwGSQLpu9jYWw=$FT5tS9'
- 'o+ABvsxogIXpJim16Gz5SVtV8='
+ 'enc-val$1$/cXpEMoZrTlb9yokGhw8tLTSUkqnqJ4ZoAkurNgMYx'
+ 'w=$1VdkSMcZnLRwLiu1M8VlYcbelwmiVNY='
),
)
== 'secret1'
@@ -51,11 +51,11 @@ def test_decrypt():
# now test with modified edata
with pytest.raises(etoolkit.EtoolkitInstanceError) as exc_info:
edata = (
- 'enc-val$1$Y/TBb1F3siHTw6qZg9ERzZfA8PLPf2CwGSQLpu9jYWw=$FT5tS9'
- 'o+ABvsxogIXpJim17Gz5SVtV8='
+ 'enc-val$1$/cXpEMoZrTlb9yokGhw8tLTSUkqnqJ5ZoAkurNgMYx'
+ 'w=$1VdkSMcZnLRwLiu1M8VlYcbelwmiVNY='
)
etoolkit.EtoolkitInstance.decrypt(
- 'the very secret passwd', edata
+ 'The very secret passwd', edata
) == 'secret1'
assert exc_info.type is etoolkit.EtoolkitInstanceError
assert exc_info.value.args[0] == f'Invalid tag when decrypting: {edata}'
@@ -70,17 +70,32 @@ def test_encrypt():
assert edata != etoolkit.EtoolkitInstance.encrypt('foo', 'bar')
+@unittest.mock.patch('os.urandom')
+def test_encrypt_staticly(urandom, non_random_bytes_32):
+ """Tests the EtoolkitInstance.encrypt method always with the same salt"""
+ urandom.return_value = non_random_bytes_32
+ edata = etoolkit.EtoolkitInstance.encrypt('The very secret passwd', 'bar')
+ assert edata == (
+ 'enc-val$1$uYpZM1VfAGq0CDZL2duITs076CQj+hIFEgx+F4mn80'
+ 'o=$HjPFNv6xC5hbMrFc0L5lSkWdfQ=='
+ )
+ assert edata == etoolkit.EtoolkitInstance.encrypt(
+ 'The very secret passwd',
+ 'bar',
+ )
+
+
def test_get_new_password_hash():
"""Tests the static EtoolkitInstance.get_new_password_hash method"""
new_hash = etoolkit.EtoolkitInstance.get_new_password_hash(
- 'the very secret passwd'
+ 'The very secret passwd'
)
# all pbkdf2 params are the same / hardcoded for the time being
assert new_hash.startswith('pbkdf2_sha256$100000$')
assert len(new_hash) == 110
# the hash should always be different because of random salting
assert new_hash != etoolkit.EtoolkitInstance.get_new_password_hash(
- 'the very secret passwd'
+ 'The very secret passwd'
)
@@ -95,8 +110,8 @@ def test_parse_value():
def test_password_matches(password_hash):
"""Tests the static EtoolkitInstance.password_matches method"""
assert etoolkit.EtoolkitInstance.password_matches(
- 'the very secret passwd', password_hash
+ 'The very secret passwd', password_hash
)
assert not etoolkit.EtoolkitInstance.password_matches(
- 'the very secret passwdo', password_hash
+ 'The very secret passwdo', password_hash
)
--
cgit v1.3