From 259a96f768e42f3ec480cbcc867e2a06eb05c0f0 Mon Sep 17 00:00:00 2001 From: "William W. Kimball" Date: Tue, 28 May 2019 17:18:57 -0500 Subject: [PATCH 01/16] WIP: Code reorg to enable testing commands --- setup.py | 13 ++++++++----- tests/test_commands_yaml_get.py | 8 ++++++++ .../commands/eyaml_rotate_keys.py | 3 +-- bin/yaml-get => yamlpath/commands/yaml_get.py | 2 -- bin/yaml-set => yamlpath/commands/yaml_set.py | 2 -- 5 files changed, 17 insertions(+), 11 deletions(-) create mode 100644 tests/test_commands_yaml_get.py rename bin/eyaml-rotate-keys => yamlpath/commands/eyaml_rotate_keys.py (99%) mode change 100755 => 100644 rename bin/yaml-get => yamlpath/commands/yaml_get.py (98%) mode change 100755 => 100644 rename bin/yaml-set => yamlpath/commands/yaml_set.py (99%) mode change 100755 => 100644 diff --git a/setup.py b/setup.py index 696d3752..969f8cd8 100644 --- a/setup.py +++ b/setup.py @@ -22,11 +22,13 @@ license="ISC", keywords="yaml eyaml yaml-path", packages=setuptools.find_packages(), - scripts=[ - "bin/eyaml-rotate-keys", - "bin/yaml-get", - "bin/yaml-set", - ], + entry_points={ + "console-scripts": [ + "eyaml-rotate-keys = yamlpath.commands.eyaml_rotate_keys:main", + "yaml-get = yamlpath.commands.yaml_get:main", + "yaml-set = yamlpath.commands.yaml_set:main", + ] + }, python_requires=">3.6.0", install_requires=[ "ruamel.yaml>=0.15.96", @@ -34,6 +36,7 @@ tests_require=[ "pytest", "pytest-cov", + "pytest-console-scripts", ], include_package_data=True, zip_safe=False diff --git a/tests/test_commands_yaml_get.py b/tests/test_commands_yaml_get.py new file mode 100644 index 00000000..c3895519 --- /dev/null +++ b/tests/test_commands_yaml_get.py @@ -0,0 +1,8 @@ +import pytest + +class Test_yaml_get(): + """Tests for the yaml-get command-line interface.""" + def test_bad_options(script_runner): + ret = script_runner.run("yaml-get") + assert ret.error + assert -1 < ret.stderr.find("ERROR") diff --git a/bin/eyaml-rotate-keys b/yamlpath/commands/eyaml_rotate_keys.py old mode 100755 new mode 100644 similarity index 99% rename from bin/eyaml-rotate-keys rename to yamlpath/commands/eyaml_rotate_keys.py index 6821cfa9..e8a01619 --- a/bin/eyaml-rotate-keys +++ b/yamlpath/commands/eyaml_rotate_keys.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 -# pylint: disable=locally-disabled,invalid-name """ Rotates the encryption keys used for all EYAML values within a set of YAML files, decrypting with old keys and re-encrypting using replacement keys. @@ -19,6 +17,7 @@ from yamlpath.eyaml.exceptions import EYAMLCommandException from yamlpath.eyaml import EYAMLProcessor +# pylint: disable=locally-disabled,unused-import import yamlpath.patches from yamlpath.wrappers import ConsolePrinter diff --git a/bin/yaml-get b/yamlpath/commands/yaml_get.py old mode 100755 new mode 100644 similarity index 98% rename from bin/yaml-get rename to yamlpath/commands/yaml_get.py index 4c046d72..73f36c77 --- a/bin/yaml-get +++ b/yamlpath/commands/yaml_get.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 -# pylint: disable=locally-disabled,invalid-name """ Retrieves one or more values from a YAML file at a specified YAML Path. Output is printed to STDOUT, one line per match. When a result is a complex diff --git a/bin/yaml-set b/yamlpath/commands/yaml_set.py old mode 100755 new mode 100644 similarity index 99% rename from bin/yaml-set rename to yamlpath/commands/yaml_set.py index 779ef1d6..56378a32 --- a/bin/yaml-set +++ b/yamlpath/commands/yaml_set.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 -# pylint: disable=locally-disabled,invalid-name """ Changes one or more values in a YAML file at a specified YAML Path. Matched values can be checked before they are replaced to mitigate accidental change. From fc11d36b1b90fb5d8774ca80828af2a0b1817fe9 Mon Sep 17 00:00:00 2001 From: William Kimball <30981667+wwkimball@users.noreply.github.com> Date: Wed, 29 May 2019 04:51:08 -0500 Subject: [PATCH 02/16] WIP: Adding yaml-get tests --- setup.py | 2 +- tests/test_commands_yaml_get.py | 42 +++++++++++++++++++++++++++++---- yamlpath/commands/__init__.py | 4 ++++ 3 files changed, 42 insertions(+), 6 deletions(-) create mode 100644 yamlpath/commands/__init__.py diff --git a/setup.py b/setup.py index 969f8cd8..ad4d1c79 100644 --- a/setup.py +++ b/setup.py @@ -23,7 +23,7 @@ keywords="yaml eyaml yaml-path", packages=setuptools.find_packages(), entry_points={ - "console-scripts": [ + "console_scripts": [ "eyaml-rotate-keys = yamlpath.commands.eyaml_rotate_keys:main", "yaml-get = yamlpath.commands.yaml_get:main", "yaml-set = yamlpath.commands.yaml_set:main", diff --git a/tests/test_commands_yaml_get.py b/tests/test_commands_yaml_get.py index c3895519..ee17a489 100644 --- a/tests/test_commands_yaml_get.py +++ b/tests/test_commands_yaml_get.py @@ -1,8 +1,40 @@ import pytest +@pytest.fixture +def yaml_file_f(tmp_path_factory): + """Creates a sample YAML_FILE for CLI tests.""" + file_name = "test.yaml" + yaml_file = tmp_path_factory.mktemp("test_files") / file_name + + file_content = """--- + aliases: + - &plainScalar Plain scalar string + """ + + with open(yaml_file, 'w') as fhnd: + fhnd.write(file_content) + + return yaml_file + class Test_yaml_get(): - """Tests for the yaml-get command-line interface.""" - def test_bad_options(script_runner): - ret = script_runner.run("yaml-get") - assert ret.error - assert -1 < ret.stderr.find("ERROR") + """Tests for the yaml-get command-line interface.""" + + def test_no_options(self, script_runner): + result = script_runner.run("yaml-get") + assert not result.success, result.stderr + assert "the following arguments are required: -p/--query, YAML_FILE" in result.stderr + + def test_no_input_file(self, script_runner): + result = script_runner.run("yaml-get", "--query='/test'") + assert not result.success, result.stderr + assert "the following arguments are required: YAML_FILE" in result.stderr + + def test_no_query(self, script_runner, yaml_file_f): + result = script_runner.run("yaml-get", yaml_file_f) + assert not result.success, result.stderr + assert "the following arguments are required: -p/--query" in result.stderr + + def test_query_anchor(self, script_runner, yaml_file_f): + result = script_runner.run("yaml-get", "--query=aliases[&plainScalar]", yaml_file_f) + assert result.success, result.stderr + assert "Plain scalar string" in result.stdout diff --git a/yamlpath/commands/__init__.py b/yamlpath/commands/__init__.py new file mode 100644 index 00000000..bea44e4c --- /dev/null +++ b/yamlpath/commands/__init__.py @@ -0,0 +1,4 @@ +"""Make all of the command APIs available.""" +from yamlpath.commands import eyaml_rotate_keys +from yamlpath.commands import yaml_get +from yamlpath.commands import yaml_set From 0c408885735aad56b72484172720b8faaae1528f Mon Sep 17 00:00:00 2001 From: William Kimball <30981667+wwkimball@users.noreply.github.com> Date: Wed, 29 May 2019 10:14:48 -0500 Subject: [PATCH 03/16] WIP: More yaml-get tests --- tests/test_commands_yaml_get.py | 65 +++++++++++++++++++++----- yamlpath/commands/eyaml_rotate_keys.py | 12 ++++- yamlpath/commands/yaml_get.py | 19 +++++++- yamlpath/commands/yaml_set.py | 16 ++++++- 4 files changed, 98 insertions(+), 14 deletions(-) diff --git a/tests/test_commands_yaml_get.py b/tests/test_commands_yaml_get.py index ee17a489..8507b1f1 100644 --- a/tests/test_commands_yaml_get.py +++ b/tests/test_commands_yaml_get.py @@ -1,20 +1,38 @@ import pytest @pytest.fixture -def yaml_file_f(tmp_path_factory): +def input_files(tmp_path_factory): """Creates a sample YAML_FILE for CLI tests.""" - file_name = "test.yaml" - yaml_file = tmp_path_factory.mktemp("test_files") / file_name + good_yaml_file_name = "good.test.yaml" + badsyntax_yaml_file_name = "badsyntax.test.yaml" + badcmp_yaml_file_name = "bad-composition.test.yaml" + good_yaml_file = tmp_path_factory.mktemp("test_files") / good_yaml_file_name + badsyntax_yaml_file = tmp_path_factory.mktemp("test_files") / badsyntax_yaml_file_name + badcmp_yaml_file = tmp_path_factory.mktemp("test_files") / badcmp_yaml_file_name - file_content = """--- + good_yaml_content = """--- aliases: - &plainScalar Plain scalar string """ - with open(yaml_file, 'w') as fhnd: - fhnd.write(file_content) + badsyntax_yaml_content = """--- + # This YAML content contains a critical syntax error + & bad_anchor: is bad + """ + + badcmp_yaml_content = """--- + # This YAML file is improperly composed + this is a parsing error: *no such capability + """ + + with open(good_yaml_file, 'w') as fhnd: + fhnd.write(good_yaml_content) + with open(badsyntax_yaml_file, 'w') as fhnd: + fhnd.write(badsyntax_yaml_content) + with open(badcmp_yaml_file, 'w') as fhnd: + fhnd.write(badcmp_yaml_content) - return yaml_file + return [good_yaml_file, badsyntax_yaml_file, badcmp_yaml_file] class Test_yaml_get(): """Tests for the yaml-get command-line interface.""" @@ -29,12 +47,37 @@ def test_no_input_file(self, script_runner): assert not result.success, result.stderr assert "the following arguments are required: YAML_FILE" in result.stderr - def test_no_query(self, script_runner, yaml_file_f): - result = script_runner.run("yaml-get", yaml_file_f) + def test_bad_input_file(self, script_runner): + result = script_runner.run("yaml-get", "--query='/test'", "no-such-file") + assert not result.success, result.stderr + assert "YAML_FILE not found:" in result.stderr + + def test_no_query(self, script_runner, input_files): + result = script_runner.run("yaml-get", input_files[0]) assert not result.success, result.stderr assert "the following arguments are required: -p/--query" in result.stderr - def test_query_anchor(self, script_runner, yaml_file_f): - result = script_runner.run("yaml-get", "--query=aliases[&plainScalar]", yaml_file_f) + def test_bad_privatekey(self, script_runner, input_files): + result = script_runner.run("yaml-get", "--query=aliases", "--privatekey=no-such-file", input_files[0]) + assert not result.success, result.stderr + assert "EYAML private key is not a readable file" in result.stderr + + def test_bad_publickey(self, script_runner, input_files): + result = script_runner.run("yaml-get", "--query=aliases", "--publickey=no-such-file", input_files[0]) + assert not result.success, result.stderr + assert "EYAML public key is not a readable file" in result.stderr + + def test_yaml_syntax_error(self, script_runner, input_files): + result = script_runner.run("yaml-get", "--query=aliases[&plainScalar]", input_files[1]) + assert not result.success, result.stderr + assert "YAML syntax error" in result.stderr + + def test_yaml_composition_error(self, script_runner, input_files): + result = script_runner.run("yaml-get", "--query=/", input_files[2]) + assert not result.success, result.stderr + assert "YAML composition error" in result.stderr + + def test_query_anchor(self, script_runner, input_files): + result = script_runner.run("yaml-get", "--query=aliases[&plainScalar]", input_files[0]) assert result.success, result.stderr assert "Plain scalar string" in result.stdout diff --git a/yamlpath/commands/eyaml_rotate_keys.py b/yamlpath/commands/eyaml_rotate_keys.py index e8a01619..8aae97b1 100644 --- a/yamlpath/commands/eyaml_rotate_keys.py +++ b/yamlpath/commands/eyaml_rotate_keys.py @@ -12,6 +12,8 @@ from ruamel.yaml import YAML from ruamel.yaml.parser import ParserError +from ruamel.yaml.composer import ComposerError +from ruamel.yaml.scanner import ScannerError from ruamel.yaml.scalarstring import FoldedScalarString from yamlpath.eyaml.exceptions import EYAMLCommandException @@ -22,7 +24,7 @@ from yamlpath.wrappers import ConsolePrinter # Implied Constants -MY_VERSION = "1.0.1" +MY_VERSION = "1.0.2" def processcli(): """Process command-line arguments.""" @@ -135,6 +137,14 @@ def main(): log.error("YAML parsing error {}: {}" .format(str(ex.problem_mark).lstrip(), ex.problem)) continue + except ComposerError as ex: + log.error("YAML composition error {}: {}" + .format(str(ex.problem_mark).lstrip(), ex.problem)) + continue + except ScannerError as ex: + log.error("YAML syntax error {}: {}" + .format(str(ex.problem_mark).lstrip(), ex.problem)) + continue # Process all EYAML values processor.data = yaml_data diff --git a/yamlpath/commands/yaml_get.py b/yamlpath/commands/yaml_get.py index 73f36c77..e148156d 100644 --- a/yamlpath/commands/yaml_get.py +++ b/yamlpath/commands/yaml_get.py @@ -14,6 +14,8 @@ from ruamel.yaml import YAML from ruamel.yaml.parser import ParserError +from ruamel.yaml.composer import ComposerError +from ruamel.yaml.scanner import ScannerError from yamlpath import YAMLPath from yamlpath.exceptions import YAMLPathException @@ -24,7 +26,7 @@ from yamlpath.wrappers import ConsolePrinter # Implied Constants -MY_VERSION = "1.0.3" +MY_VERSION = "1.0.4" def processcli(): """Process command-line arguments.""" @@ -95,6 +97,7 @@ def validateargs(args, log): if args.privatekey and not ( isfile(args.privatekey) and access(args.privatekey, R_OK) ): + has_errors = True log.error( "EYAML private key is not a readable file: " + args.privatekey ) @@ -103,6 +106,7 @@ def validateargs(args, log): if args.publickey and not ( isfile(args.publickey) and access(args.publickey, R_OK) ): + has_errors = True log.error( "EYAML public key is not a readable file: " + args.publickey ) @@ -114,6 +118,7 @@ def validateargs(args, log): (args.publickey and not args.privatekey) or (args.privatekey and not args.publickey) ): + has_errors = True log.error("Both private and public EYAML keys must be set.") if has_errors: @@ -146,6 +151,18 @@ def main(): .format(str(ex.problem_mark).lstrip(), ex.problem) , 1 ) + except ComposerError as ex: + log.critical( + "YAML composition error {}: {}" + .format(str(ex.problem_mark).lstrip(), ex.problem) + , 1 + ) + except ScannerError as ex: + log.critical( + "YAML syntax error {}: {}" + .format(str(ex.problem_mark).lstrip(), ex.problem) + , 1 + ) # Seek the queried value(s) discovered_nodes = [] diff --git a/yamlpath/commands/yaml_set.py b/yamlpath/commands/yaml_set.py index 56378a32..f726eaad 100644 --- a/yamlpath/commands/yaml_set.py +++ b/yamlpath/commands/yaml_set.py @@ -17,6 +17,8 @@ from ruamel.yaml import YAML from ruamel.yaml.parser import ParserError +from ruamel.yaml.composer import ComposerError +from ruamel.yaml.scanner import ScannerError from yamlpath import YAMLPath from yamlpath.exceptions import YAMLPathException @@ -31,7 +33,7 @@ from yamlpath.wrappers import ConsolePrinter # Implied Constants -MY_VERSION = "1.0.5" +MY_VERSION = "1.0.6" def processcli(): """Process command-line arguments.""" @@ -218,6 +220,18 @@ def main(): .format(str(ex.problem_mark).lstrip(), ex.problem) , 1 ) + except ComposerError as ex: + log.critical( + "YAML composition error {}: {}" + .format(str(ex.problem_mark).lstrip(), ex.problem) + , 1 + ) + except ScannerError as ex: + log.critical( + "YAML syntax error {}: {}" + .format(str(ex.problem_mark).lstrip(), ex.problem) + , 1 + ) # Load the present value at the specified YAML Path change_nodes = [] From 77acd4c1043113596eb1da942c6138920d83af09 Mon Sep 17 00:00:00 2001 From: William Kimball <30981667+wwkimball@users.noreply.github.com> Date: Wed, 29 May 2019 10:30:21 -0500 Subject: [PATCH 04/16] WIP: Created a YAML Parsing error test --- tests/test_commands_yaml_get.py | 17 ++++++++++++++--- yamlpath/yamlpath.py | 2 +- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/tests/test_commands_yaml_get.py b/tests/test_commands_yaml_get.py index 8507b1f1..ecf041dd 100644 --- a/tests/test_commands_yaml_get.py +++ b/tests/test_commands_yaml_get.py @@ -4,9 +4,11 @@ def input_files(tmp_path_factory): """Creates a sample YAML_FILE for CLI tests.""" good_yaml_file_name = "good.test.yaml" + imparsible_yaml_file_name = "imparsible.test.yaml" badsyntax_yaml_file_name = "badsyntax.test.yaml" badcmp_yaml_file_name = "bad-composition.test.yaml" good_yaml_file = tmp_path_factory.mktemp("test_files") / good_yaml_file_name + imparsible_yaml_file = tmp_path_factory.mktemp("test_files") / imparsible_yaml_file_name badsyntax_yaml_file = tmp_path_factory.mktemp("test_files") / badsyntax_yaml_file_name badcmp_yaml_file = tmp_path_factory.mktemp("test_files") / badcmp_yaml_file_name @@ -15,6 +17,8 @@ def input_files(tmp_path_factory): - &plainScalar Plain scalar string """ + imparsible_yaml_content = '''{"json": "is YAML", "but_bad_json": "isn't anything!"''' + badsyntax_yaml_content = """--- # This YAML content contains a critical syntax error & bad_anchor: is bad @@ -27,12 +31,14 @@ def input_files(tmp_path_factory): with open(good_yaml_file, 'w') as fhnd: fhnd.write(good_yaml_content) + with open(imparsible_yaml_file, 'w') as fhnd: + fhnd.write(imparsible_yaml_content) with open(badsyntax_yaml_file, 'w') as fhnd: fhnd.write(badsyntax_yaml_content) with open(badcmp_yaml_file, 'w') as fhnd: fhnd.write(badcmp_yaml_content) - return [good_yaml_file, badsyntax_yaml_file, badcmp_yaml_file] + return [good_yaml_file, imparsible_yaml_file, badsyntax_yaml_file, badcmp_yaml_file] class Test_yaml_get(): """Tests for the yaml-get command-line interface.""" @@ -67,13 +73,18 @@ def test_bad_publickey(self, script_runner, input_files): assert not result.success, result.stderr assert "EYAML public key is not a readable file" in result.stderr + def test_yaml_parsing_error(self, script_runner, input_files): + result = script_runner.run("yaml-get", "--query=/", input_files[1]) + assert not result.success, result.stderr + assert "YAML parsing error" in result.stderr + def test_yaml_syntax_error(self, script_runner, input_files): - result = script_runner.run("yaml-get", "--query=aliases[&plainScalar]", input_files[1]) + result = script_runner.run("yaml-get", "--query=/", input_files[2]) assert not result.success, result.stderr assert "YAML syntax error" in result.stderr def test_yaml_composition_error(self, script_runner, input_files): - result = script_runner.run("yaml-get", "--query=/", input_files[2]) + result = script_runner.run("yaml-get", "--query=/", input_files[3]) assert not result.success, result.stderr assert "YAML composition error" in result.stderr diff --git a/yamlpath/yamlpath.py b/yamlpath/yamlpath.py index 1a60bbff..c887f232 100644 --- a/yamlpath/yamlpath.py +++ b/yamlpath/yamlpath.py @@ -252,7 +252,7 @@ def _parse_path(self, # Infer the first possible position for a top-level Anchor mark first_anchor_pos = 0 - if self.seperator is PathSeperators.FSLASH: + if self.seperator is PathSeperators.FSLASH and len(yaml_path) > 1: first_anchor_pos = 1 seeking_anchor_mark = yaml_path[first_anchor_pos] == "&" From 3d5779518d5a5d2da589a9cbc5229538276fdb13 Mon Sep 17 00:00:00 2001 From: William Kimball <30981667+wwkimball@users.noreply.github.com> Date: Wed, 29 May 2019 14:26:31 -0500 Subject: [PATCH 05/16] WIP: generating reusible test fixtures --- tests/conftest.py | 54 +++++++++++++++++++++++++++++++++ tests/test_commands_yaml_get.py | 6 ++-- 2 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 tests/conftest.py diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..afebc30e --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,54 @@ +"""Define reusable pytest fixtures.""" +import tempfile + +import pytest + + +def create_test_file(tmp_path_factory, content): + """Creates a test YAML input file.""" + fhnd = tempfile.NamedTemporaryFile(mode='w', + dir=tmp_path_factory.getbasetemp(), + suffix='.yaml', + delete=False) + fhnd.write(content) + return fhnd.name + +@pytest.fixture(scope="session") +def imparsible_yaml_file(tmp_path_factory): + """ + Creates a YAML file that causes a ParserError when read by ruamel.yaml. + """ + content = '''{"json": "is YAML", "but_bad_json": "isn't anything!"''' + return create_test_file(tmp_path_factory, content) + +@pytest.fixture(scope="module") +def badsyntax_yaml_file(request, tmp_path_factory): + """ + Creates a YAML file that causes a ScannerError when read by ruamel.yaml. + """ + tmp_subdir = getattr(request.module, "tmpdir", "test_files") + file_name = "badsyntax.test.yaml" + yaml_file = tmp_path_factory.mktemp(tmp_subdir) / file_name + file_content = """--- + # This YAML content contains a critical syntax error + & bad_anchor: is bad + """ + with open(yaml_file, 'w') as fhnd: + fhnd.write(file_content) + return yaml_file + +@pytest.fixture(scope="module") +def badcmp_yaml_file(request, tmp_path_factory): + """ + Creates a YAML file that causes a ComposerError when read by ruamel.yaml. + """ + tmp_subdir = getattr(request.module, "tmpdir", "test_files") + file_name = "badcmp.test.yaml" + yaml_file = tmp_path_factory.mktemp(tmp_subdir) / file_name + file_content = """--- + # This YAML file is improperly composed + this is a parsing error: *no such capability + """ + with open(yaml_file, 'w') as fhnd: + fhnd.write(file_content) + return yaml_file diff --git a/tests/test_commands_yaml_get.py b/tests/test_commands_yaml_get.py index ecf041dd..69ce95e5 100644 --- a/tests/test_commands_yaml_get.py +++ b/tests/test_commands_yaml_get.py @@ -1,3 +1,5 @@ +import tempfile + import pytest @pytest.fixture @@ -73,8 +75,8 @@ def test_bad_publickey(self, script_runner, input_files): assert not result.success, result.stderr assert "EYAML public key is not a readable file" in result.stderr - def test_yaml_parsing_error(self, script_runner, input_files): - result = script_runner.run("yaml-get", "--query=/", input_files[1]) + def test_yaml_parsing_error(self, script_runner, imparsible_yaml_file): + result = script_runner.run("yaml-get", "--query=/", imparsible_yaml_file) assert not result.success, result.stderr assert "YAML parsing error" in result.stderr From 344d72456e379a0c4378fd4dc5e67faf817575d6 Mon Sep 17 00:00:00 2001 From: "William W. Kimball" Date: Wed, 29 May 2019 17:18:10 -0500 Subject: [PATCH 06/16] WIP: More centralized fixtures --- tests/conftest.py | 30 ++++++++++-------------------- tests/test_commands_yaml_get.py | 10 ++++------ 2 files changed, 14 insertions(+), 26 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index afebc30e..44af69d6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,7 +4,7 @@ import pytest -def create_test_file(tmp_path_factory, content): +def create_temp_yaml_file(tmp_path_factory, content): """Creates a test YAML input file.""" fhnd = tempfile.NamedTemporaryFile(mode='w', dir=tmp_path_factory.getbasetemp(), @@ -19,36 +19,26 @@ def imparsible_yaml_file(tmp_path_factory): Creates a YAML file that causes a ParserError when read by ruamel.yaml. """ content = '''{"json": "is YAML", "but_bad_json": "isn't anything!"''' - return create_test_file(tmp_path_factory, content) + return create_temp_yaml_file(tmp_path_factory, content) -@pytest.fixture(scope="module") -def badsyntax_yaml_file(request, tmp_path_factory): +@pytest.fixture(scope="session") +def badsyntax_yaml_file(tmp_path_factory): """ Creates a YAML file that causes a ScannerError when read by ruamel.yaml. """ - tmp_subdir = getattr(request.module, "tmpdir", "test_files") - file_name = "badsyntax.test.yaml" - yaml_file = tmp_path_factory.mktemp(tmp_subdir) / file_name - file_content = """--- + content = """--- # This YAML content contains a critical syntax error & bad_anchor: is bad """ - with open(yaml_file, 'w') as fhnd: - fhnd.write(file_content) - return yaml_file + return create_temp_yaml_file(tmp_path_factory, content) -@pytest.fixture(scope="module") -def badcmp_yaml_file(request, tmp_path_factory): +@pytest.fixture(scope="session") +def badcmp_yaml_file(tmp_path_factory): """ Creates a YAML file that causes a ComposerError when read by ruamel.yaml. """ - tmp_subdir = getattr(request.module, "tmpdir", "test_files") - file_name = "badcmp.test.yaml" - yaml_file = tmp_path_factory.mktemp(tmp_subdir) / file_name - file_content = """--- + content = """--- # This YAML file is improperly composed this is a parsing error: *no such capability """ - with open(yaml_file, 'w') as fhnd: - fhnd.write(file_content) - return yaml_file + return create_temp_yaml_file(tmp_path_factory, content) diff --git a/tests/test_commands_yaml_get.py b/tests/test_commands_yaml_get.py index 69ce95e5..89b45918 100644 --- a/tests/test_commands_yaml_get.py +++ b/tests/test_commands_yaml_get.py @@ -1,5 +1,3 @@ -import tempfile - import pytest @pytest.fixture @@ -80,13 +78,13 @@ def test_yaml_parsing_error(self, script_runner, imparsible_yaml_file): assert not result.success, result.stderr assert "YAML parsing error" in result.stderr - def test_yaml_syntax_error(self, script_runner, input_files): - result = script_runner.run("yaml-get", "--query=/", input_files[2]) + def test_yaml_syntax_error(self, script_runner, badsyntax_yaml_file): + result = script_runner.run("yaml-get", "--query=/", badsyntax_yaml_file) assert not result.success, result.stderr assert "YAML syntax error" in result.stderr - def test_yaml_composition_error(self, script_runner, input_files): - result = script_runner.run("yaml-get", "--query=/", input_files[3]) + def test_yaml_composition_error(self, script_runner, badcmp_yaml_file): + result = script_runner.run("yaml-get", "--query=/", badcmp_yaml_file) assert not result.success, result.stderr assert "YAML composition error" in result.stderr From eec7c91c377e8e786983d8f87d9deae9974dff9b Mon Sep 17 00:00:00 2001 From: William Kimball <30981667+wwkimball@users.noreply.github.com> Date: Wed, 29 May 2019 19:47:43 -0500 Subject: [PATCH 07/16] WIP: 100% rudimentary coverage for yaml-get --- tests/test_commands_yaml_get.py | 110 ++++++++++++++----------- yamlpath/commands/eyaml_rotate_keys.py | 2 +- yamlpath/commands/yaml_get.py | 7 +- yamlpath/commands/yaml_set.py | 2 +- 4 files changed, 67 insertions(+), 54 deletions(-) diff --git a/tests/test_commands_yaml_get.py b/tests/test_commands_yaml_get.py index 89b45918..afda71e3 100644 --- a/tests/test_commands_yaml_get.py +++ b/tests/test_commands_yaml_get.py @@ -1,44 +1,7 @@ import pytest -@pytest.fixture -def input_files(tmp_path_factory): - """Creates a sample YAML_FILE for CLI tests.""" - good_yaml_file_name = "good.test.yaml" - imparsible_yaml_file_name = "imparsible.test.yaml" - badsyntax_yaml_file_name = "badsyntax.test.yaml" - badcmp_yaml_file_name = "bad-composition.test.yaml" - good_yaml_file = tmp_path_factory.mktemp("test_files") / good_yaml_file_name - imparsible_yaml_file = tmp_path_factory.mktemp("test_files") / imparsible_yaml_file_name - badsyntax_yaml_file = tmp_path_factory.mktemp("test_files") / badsyntax_yaml_file_name - badcmp_yaml_file = tmp_path_factory.mktemp("test_files") / badcmp_yaml_file_name - - good_yaml_content = """--- - aliases: - - &plainScalar Plain scalar string - """ - - imparsible_yaml_content = '''{"json": "is YAML", "but_bad_json": "isn't anything!"''' - - badsyntax_yaml_content = """--- - # This YAML content contains a critical syntax error - & bad_anchor: is bad - """ - - badcmp_yaml_content = """--- - # This YAML file is improperly composed - this is a parsing error: *no such capability - """ - - with open(good_yaml_file, 'w') as fhnd: - fhnd.write(good_yaml_content) - with open(imparsible_yaml_file, 'w') as fhnd: - fhnd.write(imparsible_yaml_content) - with open(badsyntax_yaml_file, 'w') as fhnd: - fhnd.write(badsyntax_yaml_content) - with open(badcmp_yaml_file, 'w') as fhnd: - fhnd.write(badcmp_yaml_content) - - return [good_yaml_file, imparsible_yaml_file, badsyntax_yaml_file, badcmp_yaml_file] +from tests.conftest import create_temp_yaml_file + class Test_yaml_get(): """Tests for the yaml-get command-line interface.""" @@ -58,18 +21,30 @@ def test_bad_input_file(self, script_runner): assert not result.success, result.stderr assert "YAML_FILE not found:" in result.stderr - def test_no_query(self, script_runner, input_files): - result = script_runner.run("yaml-get", input_files[0]) + def test_no_query(self, script_runner, tmp_path_factory): + content = """--- + no: '' + """ + yaml_file = create_temp_yaml_file(tmp_path_factory, content) + result = script_runner.run("yaml-get", yaml_file) assert not result.success, result.stderr assert "the following arguments are required: -p/--query" in result.stderr - def test_bad_privatekey(self, script_runner, input_files): - result = script_runner.run("yaml-get", "--query=aliases", "--privatekey=no-such-file", input_files[0]) + def test_bad_privatekey(self, script_runner, tmp_path_factory): + content = """--- + no: '' + """ + yaml_file = create_temp_yaml_file(tmp_path_factory, content) + result = script_runner.run("yaml-get", "--query=aliases", "--privatekey=no-such-file", yaml_file) assert not result.success, result.stderr assert "EYAML private key is not a readable file" in result.stderr - def test_bad_publickey(self, script_runner, input_files): - result = script_runner.run("yaml-get", "--query=aliases", "--publickey=no-such-file", input_files[0]) + def test_bad_publickey(self, script_runner, tmp_path_factory): + content = """--- + no: '' + """ + yaml_file = create_temp_yaml_file(tmp_path_factory, content) + result = script_runner.run("yaml-get", "--query=aliases", "--publickey=no-such-file", yaml_file) assert not result.success, result.stderr assert "EYAML public key is not a readable file" in result.stderr @@ -88,7 +63,48 @@ def test_yaml_composition_error(self, script_runner, badcmp_yaml_file): assert not result.success, result.stderr assert "YAML composition error" in result.stderr - def test_query_anchor(self, script_runner, input_files): - result = script_runner.run("yaml-get", "--query=aliases[&plainScalar]", input_files[0]) + def test_bad_yaml_path(self, script_runner, tmp_path_factory): + content = """--- + aliases: + - &plainScalar Plain scalar string + """ + yaml_file = create_temp_yaml_file(tmp_path_factory, content) + result = script_runner.run("yaml-get", "--query=aliases[1]", yaml_file) + assert not result.success, result.stderr + assert "Required YAML Path does not match any nodes" in result.stderr + + def test_bad_eyaml_value(self, script_runner, tmp_path_factory): + content = """--- + aliases: + - &encryptedScalar > + ENC[PKCS7,MIIx...broken-on-purpose...==] + """ + yaml_file = create_temp_yaml_file(tmp_path_factory, content) + result = script_runner.run( + "yaml-get", + "--query=aliases[&encryptedScalar]", + "--eyaml=/does/not/exist-on-most/systems", + yaml_file + ) + assert not result.success, result.stderr + assert "No accessible eyaml command" in result.stderr + + def test_query_anchor(self, script_runner, tmp_path_factory): + content = """--- + aliases: + - &plainScalar Plain scalar string + """ + yaml_file = create_temp_yaml_file(tmp_path_factory, content) + result = script_runner.run("yaml-get", "--query=aliases[&plainScalar]", yaml_file) assert result.success, result.stderr assert "Plain scalar string" in result.stdout + + def test_query_list(self, script_runner, tmp_path_factory): + content = """--- + aliases: + - &plainScalar Plain scalar string + """ + yaml_file = create_temp_yaml_file(tmp_path_factory, content) + result = script_runner.run("yaml-get", "--query=aliases", yaml_file) + assert result.success, result.stderr + assert '["Plain scalar string"]' in result.stdout diff --git a/yamlpath/commands/eyaml_rotate_keys.py b/yamlpath/commands/eyaml_rotate_keys.py index 8aae97b1..aafd607a 100644 --- a/yamlpath/commands/eyaml_rotate_keys.py +++ b/yamlpath/commands/eyaml_rotate_keys.py @@ -100,7 +100,7 @@ def main(): args = processcli() log = ConsolePrinter(args) validateargs(args, log) - processor = EYAMLProcessor(log, None, eyaml=args.eyaml) + processor = EYAMLProcessor(log, None, binary=args.eyaml) # Prep the YAML parser yaml = YAML() diff --git a/yamlpath/commands/yaml_get.py b/yamlpath/commands/yaml_get.py index e148156d..510f9de7 100644 --- a/yamlpath/commands/yaml_get.py +++ b/yamlpath/commands/yaml_get.py @@ -167,7 +167,7 @@ def main(): # Seek the queried value(s) discovered_nodes = [] processor = EYAMLProcessor( - log, yaml_data, eyaml=args.eyaml, + log, yaml_data, binary=args.eyaml, publickey=args.publickey, privatekey=args.privatekey) try: for node in processor.get_eyaml_values(yaml_path, mustexist=True): @@ -178,9 +178,6 @@ def main(): except EYAMLCommandException as ex: log.critical(ex, 2) - if not discovered_nodes: - log.critical("No matches for {}!".format(yaml_path), 3) - for node in discovered_nodes: if isinstance(node, (dict, list)): print(json.dumps(node)) @@ -188,4 +185,4 @@ def main(): print("{}".format(str(node).replace("\n", r"\n"))) if __name__ == "__main__": - main() + main() # pragma: no cover diff --git a/yamlpath/commands/yaml_set.py b/yamlpath/commands/yaml_set.py index f726eaad..e6280672 100644 --- a/yamlpath/commands/yaml_set.py +++ b/yamlpath/commands/yaml_set.py @@ -237,7 +237,7 @@ def main(): change_nodes = [] old_format = YAMLValueFormats.DEFAULT processor = EYAMLProcessor( - log, yaml_data, eyaml=args.eyaml, + log, yaml_data, binary=args.eyaml, publickey=args.publickey, privatekey=args.privatekey) try: for node in processor.get_nodes( From bb93f2daca117085b58dbc0335b094cc6ece40bc Mon Sep 17 00:00:00 2001 From: William Kimball <30981667+wwkimball@users.noreply.github.com> Date: Thu, 30 May 2019 00:14:09 -0500 Subject: [PATCH 08/16] WIP: Chipping away at yaml-set tests --- tests/test_commands_yaml_get.py | 27 +++--- tests/test_commands_yaml_set.py | 165 ++++++++++++++++++++++++++++++++ yamlpath/commands/yaml_set.py | 7 +- 3 files changed, 180 insertions(+), 19 deletions(-) create mode 100644 tests/test_commands_yaml_set.py diff --git a/tests/test_commands_yaml_get.py b/tests/test_commands_yaml_get.py index afda71e3..606a10d4 100644 --- a/tests/test_commands_yaml_get.py +++ b/tests/test_commands_yaml_get.py @@ -5,19 +5,20 @@ class Test_yaml_get(): """Tests for the yaml-get command-line interface.""" + command = "yaml-get" def test_no_options(self, script_runner): - result = script_runner.run("yaml-get") + result = script_runner.run(self.command) assert not result.success, result.stderr assert "the following arguments are required: -p/--query, YAML_FILE" in result.stderr def test_no_input_file(self, script_runner): - result = script_runner.run("yaml-get", "--query='/test'") + result = script_runner.run(self.command, "--query='/test'") assert not result.success, result.stderr assert "the following arguments are required: YAML_FILE" in result.stderr def test_bad_input_file(self, script_runner): - result = script_runner.run("yaml-get", "--query='/test'", "no-such-file") + result = script_runner.run(self.command, "--query='/test'", "no-such-file") assert not result.success, result.stderr assert "YAML_FILE not found:" in result.stderr @@ -26,7 +27,7 @@ def test_no_query(self, script_runner, tmp_path_factory): no: '' """ yaml_file = create_temp_yaml_file(tmp_path_factory, content) - result = script_runner.run("yaml-get", yaml_file) + result = script_runner.run(self.command, yaml_file) assert not result.success, result.stderr assert "the following arguments are required: -p/--query" in result.stderr @@ -35,7 +36,7 @@ def test_bad_privatekey(self, script_runner, tmp_path_factory): no: '' """ yaml_file = create_temp_yaml_file(tmp_path_factory, content) - result = script_runner.run("yaml-get", "--query=aliases", "--privatekey=no-such-file", yaml_file) + result = script_runner.run(self.command, "--query=aliases", "--privatekey=no-such-file", yaml_file) assert not result.success, result.stderr assert "EYAML private key is not a readable file" in result.stderr @@ -44,22 +45,22 @@ def test_bad_publickey(self, script_runner, tmp_path_factory): no: '' """ yaml_file = create_temp_yaml_file(tmp_path_factory, content) - result = script_runner.run("yaml-get", "--query=aliases", "--publickey=no-such-file", yaml_file) + result = script_runner.run(self.command, "--query=aliases", "--publickey=no-such-file", yaml_file) assert not result.success, result.stderr assert "EYAML public key is not a readable file" in result.stderr def test_yaml_parsing_error(self, script_runner, imparsible_yaml_file): - result = script_runner.run("yaml-get", "--query=/", imparsible_yaml_file) + result = script_runner.run(self.command, "--query=/", imparsible_yaml_file) assert not result.success, result.stderr assert "YAML parsing error" in result.stderr def test_yaml_syntax_error(self, script_runner, badsyntax_yaml_file): - result = script_runner.run("yaml-get", "--query=/", badsyntax_yaml_file) + result = script_runner.run(self.command, "--query=/", badsyntax_yaml_file) assert not result.success, result.stderr assert "YAML syntax error" in result.stderr def test_yaml_composition_error(self, script_runner, badcmp_yaml_file): - result = script_runner.run("yaml-get", "--query=/", badcmp_yaml_file) + result = script_runner.run(self.command, "--query=/", badcmp_yaml_file) assert not result.success, result.stderr assert "YAML composition error" in result.stderr @@ -69,7 +70,7 @@ def test_bad_yaml_path(self, script_runner, tmp_path_factory): - &plainScalar Plain scalar string """ yaml_file = create_temp_yaml_file(tmp_path_factory, content) - result = script_runner.run("yaml-get", "--query=aliases[1]", yaml_file) + result = script_runner.run(self.command, "--query=aliases[1]", yaml_file) assert not result.success, result.stderr assert "Required YAML Path does not match any nodes" in result.stderr @@ -81,7 +82,7 @@ def test_bad_eyaml_value(self, script_runner, tmp_path_factory): """ yaml_file = create_temp_yaml_file(tmp_path_factory, content) result = script_runner.run( - "yaml-get", + self.command, "--query=aliases[&encryptedScalar]", "--eyaml=/does/not/exist-on-most/systems", yaml_file @@ -95,7 +96,7 @@ def test_query_anchor(self, script_runner, tmp_path_factory): - &plainScalar Plain scalar string """ yaml_file = create_temp_yaml_file(tmp_path_factory, content) - result = script_runner.run("yaml-get", "--query=aliases[&plainScalar]", yaml_file) + result = script_runner.run(self.command, "--query=aliases[&plainScalar]", yaml_file) assert result.success, result.stderr assert "Plain scalar string" in result.stdout @@ -105,6 +106,6 @@ def test_query_list(self, script_runner, tmp_path_factory): - &plainScalar Plain scalar string """ yaml_file = create_temp_yaml_file(tmp_path_factory, content) - result = script_runner.run("yaml-get", "--query=aliases", yaml_file) + result = script_runner.run(self.command, "--query=aliases", yaml_file) assert result.success, result.stderr assert '["Plain scalar string"]' in result.stdout diff --git a/tests/test_commands_yaml_set.py b/tests/test_commands_yaml_set.py new file mode 100644 index 00000000..e9e39392 --- /dev/null +++ b/tests/test_commands_yaml_set.py @@ -0,0 +1,165 @@ +import pytest + +from tests.conftest import create_temp_yaml_file + + +class Test_yaml_set(): + """Tests for the yaml-set command-line interface.""" + command = "yaml-set" + + def test_no_options(self, script_runner): + result = script_runner.run(self.command) + assert not result.success, result.stderr + assert "the following arguments are required: -g/--change, YAML_FILE" in result.stderr + + def test_no_input_file(self, script_runner): + result = script_runner.run(self.command, "--change='/test'") + assert not result.success, result.stderr + assert "the following arguments are required: YAML_FILE" in result.stderr + + def test_no_input_param(self, script_runner): + result = script_runner.run(self.command, "--change='/test'", "no-such-file") + assert not result.success, result.stderr + assert "Exactly one of the following must be set:" in result.stderr + + def test_bad_yaml_file(self, script_runner): + result = script_runner.run(self.command, "--change='/test'", "--random=1", "no-such-file") + assert not result.success, result.stderr + assert "YAML_FILE not found:" in result.stderr + + def test_identical_saveto_change_error(self, script_runner): + result = script_runner.run(self.command, "--change='/test'", "--random=1", "--saveto='/test'", "no-such-file") + assert not result.success, result.stderr + assert "Impossible to save the old value to the same YAML Path as the new value!" in result.stderr + + def test_bad_privatekey(self, script_runner): + result = script_runner.run(self.command, "--change='/test'", "--random=1", "--privatekey=no-such-file", "no-such-file") + assert not result.success, result.stderr + assert "EYAML private key is not a readable file" in result.stderr + + def test_bad_publickey(self, script_runner): + result = script_runner.run(self.command, "--change='/test'", "--random=1", "--publickey=no-such-file", "no-such-file") + assert not result.success, result.stderr + assert "EYAML public key is not a readable file" in result.stderr + + def test_input_by_value(self, script_runner, tmp_path_factory): + import re + + content = """--- + key: value + """ + yaml_file = create_temp_yaml_file(tmp_path_factory, content) + result = script_runner.run(self.command, "--change=/key", "--value=abc", yaml_file) + assert result.success, result.stderr + + with open(yaml_file, 'r') as fhnd: + filedat = fhnd.read() + assert re.findall(r"^key:\s+abc$", filedat, re.M), filedat + + def test_input_by_stdin(self, tmp_path_factory): + import re + import subprocess + + content = """--- + key: value + """ + yaml_file = create_temp_yaml_file(tmp_path_factory, content) + result = subprocess.run( + [self.command, + "--change=/key", + "--stdin", + yaml_file], + stdout=subprocess.PIPE, + input="abc".encode(), + ) + assert 0 == result.returncode, result.stderr + + with open(yaml_file, 'r') as fhnd: + filedat = fhnd.read() + assert re.findall(r"^key:\s+abc$", filedat, re.M), filedat + + def test_input_by_file(self, script_runner, tmp_path_factory): + import re + + content = """--- + key: value + """ + input_file = create_temp_yaml_file(tmp_path_factory, "abc\n") + yaml_file = create_temp_yaml_file(tmp_path_factory, content) + result = script_runner.run( + self.command, + "--change=/key", + "--file={}".format(input_file), + yaml_file + ) + assert result.success, result.stderr + + with open(yaml_file, 'r') as fhnd: + filedat = fhnd.read() + assert re.findall(r"^key:\s+abc$", filedat, re.M), filedat + + def test_input_by_random(self, script_runner, tmp_path_factory): + import re + + content = """--- + key: value + """ + yaml_file = create_temp_yaml_file(tmp_path_factory, content) + result = script_runner.run( + self.command, + "--change=/key", + "--random=50", + yaml_file + ) + assert result.success, result.stderr + + with open(yaml_file, 'r') as fhnd: + filedat = fhnd.read() + assert re.findall(r"^key:\s+[A-Za-z0-9]{50}$", filedat, re.M), filedat + + def test_yaml_parsing_error(self, script_runner, imparsible_yaml_file): + result = script_runner.run(self.command, "--change=/", "--random=1", imparsible_yaml_file) + assert not result.success, result.stderr + assert "YAML parsing error" in result.stderr + + def test_yaml_syntax_error(self, script_runner, badsyntax_yaml_file): + result = script_runner.run(self.command, "--change=/", "--random=1", badsyntax_yaml_file) + assert not result.success, result.stderr + assert "YAML syntax error" in result.stderr + + def test_yaml_composition_error(self, script_runner, badcmp_yaml_file): + result = script_runner.run(self.command, "--change=/", "--random=1", badcmp_yaml_file) + assert not result.success, result.stderr + assert "YAML composition error" in result.stderr + + def test_bad_yaml_path(self, script_runner, tmp_path_factory): + content = """--- + key: value + """ + yaml_file = create_temp_yaml_file(tmp_path_factory, content) + + # Explicit --mustexist + result = script_runner.run(self.command, "--change=key2", "--random=1", "--mustexist", yaml_file) + assert not result.success, result.stderr + assert "Required YAML Path does not match any nodes" in result.stderr + + # Implicit --mustexist via --saveto + result = script_runner.run(self.command, "--change=key3", "--random=1", "--saveto=save_here", yaml_file) + assert not result.success, result.stderr + assert "Required YAML Path does not match any nodes" in result.stderr + + def test_checked_replace(self, script_runner, tmp_path_factory): + import re + + content = """--- + key: value + """ + yaml_file = create_temp_yaml_file(tmp_path_factory, content) + result = script_runner.run( + self.command, + "--change=/key", + "--value=abc", + "--check=value", + yaml_file + ) + assert result.success, result.stderr diff --git a/yamlpath/commands/yaml_set.py b/yamlpath/commands/yaml_set.py index e6280672..ea9ca450 100644 --- a/yamlpath/commands/yaml_set.py +++ b/yamlpath/commands/yaml_set.py @@ -198,8 +198,6 @@ def main(): string.ascii_uppercase + string.ascii_lowercase + string.digits ) for _ in range(args.random) ) - else: - log.critical("Unsupported input method.", 1) # Prep the YAML parser yaml = YAML() @@ -248,10 +246,7 @@ def main(): except YAMLPathException as ex: log.critical(ex, 1) - if not change_nodes: - log.warning("Nothing to do!") - exit(0) - elif len(change_nodes) == 1: + if len(change_nodes) == 1: # When there is exactly one result, its old format can be known. This # is necessary to retain whether the replacement value should be # represented later as a multi-line string when the new value is to be From 85259387d2c6c0fe6745d0ee34049deecf04f2f0 Mon Sep 17 00:00:00 2001 From: William Kimball <30981667+wwkimball@users.noreply.github.com> Date: Thu, 30 May 2019 08:04:31 -0500 Subject: [PATCH 09/16] WIP: Refactored fixtures --- tests/conftest.py | 108 +++++++++++++++++++++ tests/test_commands_yaml_set.py | 19 +++- tests/test_eyaml_eyamlprocessor.py | 146 ++++++----------------------- tests/test_processor.py | 94 +++++++++---------- 4 files changed, 198 insertions(+), 169 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 44af69d6..deb28c75 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,8 +1,116 @@ """Define reusable pytest fixtures.""" import tempfile +from types import SimpleNamespace import pytest +from yamlpath.wrappers import ConsolePrinter +from yamlpath.eyaml import EYAMLProcessor + +# Implied constants +EYAML_PRIVATE_KEY_FILENAME = "private_key.pkcs7.pem" +EYAML_PUBLIC_KEY_FILENAME = "public_key.pkcs7.pem" + +# pylint: disable=locally-disabled,invalid-name +requireseyaml = pytest.mark.skipif( + EYAMLProcessor.get_eyaml_executable("eyaml") is None + , reason="The 'eyaml' command must be installed and accessible on the PATH" + + " to test and use EYAML features. Try: 'gem install hiera-eyaml'" + + " after intalling ruby and rubygems." +) + +@pytest.fixture +def quiet_logger(): + """Returns a quiet ConsolePrinter.""" + args = SimpleNamespace(verbose=False, quiet=True, debug=False) + return ConsolePrinter(args) + +@pytest.fixture(scope="session") +def old_eyaml_keys(tmp_path_factory): + """Creates temporary keys for encryption/decryption tests.""" + old_key_path_name = "old-keys" + old_key_dir = tmp_path_factory.mktemp(old_key_path_name) + old_private_key_file = old_key_dir / EYAML_PRIVATE_KEY_FILENAME + old_public_key_file = old_key_dir / EYAML_PUBLIC_KEY_FILENAME + + old_private_key = r"""-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA1BuytnsdHdt6NkNfLoGJIlf9hrWux8raPP3W57cONh2MrQ6d +aoAX+L+igTSjvSTI6oxsO0dqdYXZO1+rOK3gI9OnZQhkCjq9IRoWx7AIvM7skaD0 +Lne9YsvA7mGY/z9lm3IALI6OBVV5k6xnBR2PVi6A7FnDm0CRLit2Bn9eHLN3k4oL +S/ynxgXBmWWgnKtJNJwGmeD5PwzJfXCcJ3kPItiktFizJZoPmAlBP7LIzamlfSXV +VoniRs45aGrTGpmZSdvossL41KBCYJGjP+lIL/UpDJHBeiuqVQoDl4/UZqb5xF9S +C2p2Of21fmZmj4uUAT5FPtKMKCspmLWQeUEfiwIDAQABAoIBAEyXR9wu7p+mbiYE +A+22Jr+5CDpJhrhsXovhmWWIq2ANIYwoF92qLX3MLTD8whd9nfNcC4UIT7/qOjv/ +WsOXvbUSK4MHGaC7/ylh01H+Fdmf2rrnZOUWpdN0AdHSej3JNbaA3uE4BL6WU9Vo +TrcBKo4TMsilzUVVdlc2qGLGQUSZPLnIJWMLQIdCe2kZ9YvUlGloD4uGT3RH6+vH +TOtXqDgLGS/79K0rnflnBsUBkXpukxzOcTRHxR0s7SJ2XCB0JfdLWfR6X1nzM4mh +rn/m2nzEOG9ICe5hoHqAEZ/ezKd/jwxMg1YMZnGAzDMw7/UPWo87wgVdxxOHOsHG +v/pK+okCgYEA/SToT82qtvWIiUTbkdDislGhTet2eEu2Bi9wCxAUQn045Y812r9d +TvJyfKJyvvpxzejaecJb8oOXyypMDay7aPOVBI1E2OqfImxF8pJ0QqejAUCinXrj +KnV72L/hjTavivWq1vHZYXSxufAMG0C7UeztwkOfk85N3wuuYYWYrc0CgYEA1oBG +2fQ0PXDyionE3c4bpRGZMJxD+3RFRMCJiE+xheRR22ObSDLUH123ZGmU0m2FTS9O +M+GJbZgdV1Oe0EJ5rWfzFYUmVJIreH+oQWaY/HMkoe705LAMcPyd0bSjRVWWiz+l +anIGjj5HaPSI7XFqdQu7j3ww67k4BBAca8++arcCgYA/cIhnt3sY7t+QxxjfqiGl +3p82D9RYwWCUnD7QBu+M2iTwIru0XlDcABaA9ZUcF1d96uUVroesdx4LZEY7BxbQ +bnrh8SVX1zSaQ9gjumA4dBp9rd0S6kET2u12nF/CK/pCMN7njySTL9N6bZYbHlXT +ajULgjbzq7gINb01420n4QKBgQCqu0epy9qY3QHwi2ALPDZ82NkZ/AeQaieIZcgS +m3wtmmIdQdcjTHHS1YFXh0JRi6MCoJiaavY8KUuRapmKIp8/CvJNOsIbpoy7SMDf +7Y3vwqZxzgVW0VnVxPzJIgKi+VDuXSaI52GYbrHgNGOYuyGFMGWF+8/kkHSppzk4 +Bw8FWQKBgQCo/7cV19n3e7ZlZ/aGhIOtSCTopMBV8/9PIw+s+ZDiQ7vRSj9DwkAQ ++x97V0idgh16tnly0xKvGCGTQR7qDsDTjHmmF4LZUGjcq7pHsTi/umCM/toE+BCk +7ayr+G0DWr5FjhQ7uCt2Rz1NKcj6EkDcM1WZxkDLAvBXjlj+T+eqtQ== +-----END RSA PRIVATE KEY----- +""" + old_public_key = r"""-----BEGIN CERTIFICATE----- +MIIC2TCCAcGgAwIBAgIBATANBgkqhkiG9w0BAQsFADAAMCAXDTE5MDUwNzE4MDAw +NVoYDzIwNjkwNDI0MTgwMDA1WjAAMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB +CgKCAQEA1BuytnsdHdt6NkNfLoGJIlf9hrWux8raPP3W57cONh2MrQ6daoAX+L+i +gTSjvSTI6oxsO0dqdYXZO1+rOK3gI9OnZQhkCjq9IRoWx7AIvM7skaD0Lne9YsvA +7mGY/z9lm3IALI6OBVV5k6xnBR2PVi6A7FnDm0CRLit2Bn9eHLN3k4oLS/ynxgXB +mWWgnKtJNJwGmeD5PwzJfXCcJ3kPItiktFizJZoPmAlBP7LIzamlfSXVVoniRs45 +aGrTGpmZSdvossL41KBCYJGjP+lIL/UpDJHBeiuqVQoDl4/UZqb5xF9SC2p2Of21 +fmZmj4uUAT5FPtKMKCspmLWQeUEfiwIDAQABo1wwWjAPBgNVHRMBAf8EBTADAQH/ +MB0GA1UdDgQWBBTUHb3HX8dBfYFL1J1sCv+uCum5AzAoBgNVHSMEITAfgBTUHb3H +X8dBfYFL1J1sCv+uCum5A6EEpAIwAIIBATANBgkqhkiG9w0BAQsFAAOCAQEAcw+0 +dfHSLNAZD95G2pDnT2qShjmdLdbrDQhAXWhLeoWpXsKvC0iUyQaOF9ckl++tHM2g +ejm1vEOrZ+1uXK3qnMXPF99Wet686OhyoDt262Mt3wzGHNijAHEvQtjap8ZIwfOM +zFTvjmOlUScqF/Yg+htcGnJdQhWIrsD+exiY5Kz2IMtuW+yWLLP8bY5vPg6qfrp2 +4VVJ3Md1gdSownd1Au5tqPXm6VfSgLiCm9iDPVsjDII9h8ydate1d2TBHPup+4tN +JZ5/muctimydC+S2oCn7ucsilxZD89N7rJjKXNfoUOGHjOEVQMa8RtZLzH2sUEaS +FktE6rH8a+8SwO+TGw== +-----END CERTIFICATE----- +""" + + with open(old_private_key_file, 'w') as key_file: + key_file.write(old_private_key) + + with open(old_public_key_file, 'w') as key_file: + key_file.write(old_public_key) + + return (old_private_key_file, old_public_key_file) + +@requireseyaml +@pytest.fixture(scope="session") +def new_eyaml_keys(tmp_path_factory): + """Creates temporary keys for encryption/decryption tests.""" + from subprocess import run + + new_key_path_name = "new-keys" + new_key_dir = tmp_path_factory.mktemp(new_key_path_name) + new_private_key_file = new_key_dir / EYAML_PRIVATE_KEY_FILENAME + new_public_key_file = new_key_dir / EYAML_PUBLIC_KEY_FILENAME + + run( + "{} createkeys --pkcs7-private-key={} --pkcs7-public-key={}" + .format( + EYAMLProcessor.get_eyaml_executable("eyaml"), + new_private_key_file, + new_public_key_file + ) + .split() + ) + + return (new_private_key_file, new_public_key_file) def create_temp_yaml_file(tmp_path_factory, content): """Creates a test YAML input file.""" diff --git a/tests/test_commands_yaml_set.py b/tests/test_commands_yaml_set.py index e9e39392..d6593f71 100644 --- a/tests/test_commands_yaml_set.py +++ b/tests/test_commands_yaml_set.py @@ -1,6 +1,6 @@ import pytest -from tests.conftest import create_temp_yaml_file +from tests.conftest import create_temp_yaml_file, requireseyaml, old_eyaml_keys class Test_yaml_set(): @@ -163,3 +163,20 @@ def test_checked_replace(self, script_runner, tmp_path_factory): yaml_file ) assert result.success, result.stderr + + @requireseyaml + def test_missing_key(self, script_runner, tmp_path_factory, old_eyaml_keys): + content = """--- + encrypted: ENC[PKCS7,MIIx...blahblahblah...==] + """ + yaml_file = create_temp_yaml_file(tmp_path_factory, content) + result = script_runner.run( + self.command, + "--change=encrypted", + "--random=1", + "--check=n/a" + "--privatekey={}".format(old_eyaml_keys[0]), + yaml_file + ) + assert not result.success, result.stderr + assert "Neither or both private and public EYAML keys must be set" in result.stderr diff --git a/tests/test_eyaml_eyamlprocessor.py b/tests/test_eyaml_eyamlprocessor.py index 78d7e235..0bf5a3c3 100644 --- a/tests/test_eyaml_eyamlprocessor.py +++ b/tests/test_eyaml_eyamlprocessor.py @@ -1,6 +1,5 @@ import pytest -from types import SimpleNamespace from subprocess import run, CalledProcessError from ruamel.yaml import YAML @@ -12,18 +11,8 @@ from yamlpath.wrappers import ConsolePrinter from yamlpath.eyaml.exceptions import EYAMLCommandException -requireseyaml = pytest.mark.skipif( - EYAMLProcessor.get_eyaml_executable("eyaml") is None - , reason="The 'eyaml' command must be installed and accessible on the PATH" - + " to test and use EYAML features. Try: 'gem install hiera-eyaml'" - + " after intalling ruby and rubygems." -) +from tests.conftest import requireseyaml, quiet_logger, old_eyaml_keys -@pytest.fixture -def logger_f(): - """Returns a quiet ConsolePrinter.""" - args = SimpleNamespace(verbose=False, quiet=True, debug=False) - return ConsolePrinter(args) @requireseyaml @pytest.fixture @@ -106,83 +95,6 @@ def eyamldata_f(): yaml = YAML() return yaml.load(data) -@requireseyaml -@pytest.fixture(scope="module") -def eyamlkeys(tmp_path_factory): - """Creates temporary keys for encryption/decryption tests.""" - private_key_file_name = "private_key.pkcs7.pem" - public_key_file_name = "public_key.pkcs7.pem" - old_key_path_name = "old-keys" - new_key_path_name = "new-keys" - old_private_key_file = tmp_path_factory.mktemp(old_key_path_name) / private_key_file_name - old_public_key_file = tmp_path_factory.mktemp(old_key_path_name) / public_key_file_name - new_private_key_file = tmp_path_factory.mktemp(new_key_path_name) / private_key_file_name - new_public_key_file = tmp_path_factory.mktemp(new_key_path_name) / public_key_file_name - - old_private_key = r"""-----BEGIN RSA PRIVATE KEY----- -MIIEpAIBAAKCAQEA1BuytnsdHdt6NkNfLoGJIlf9hrWux8raPP3W57cONh2MrQ6d -aoAX+L+igTSjvSTI6oxsO0dqdYXZO1+rOK3gI9OnZQhkCjq9IRoWx7AIvM7skaD0 -Lne9YsvA7mGY/z9lm3IALI6OBVV5k6xnBR2PVi6A7FnDm0CRLit2Bn9eHLN3k4oL -S/ynxgXBmWWgnKtJNJwGmeD5PwzJfXCcJ3kPItiktFizJZoPmAlBP7LIzamlfSXV -VoniRs45aGrTGpmZSdvossL41KBCYJGjP+lIL/UpDJHBeiuqVQoDl4/UZqb5xF9S -C2p2Of21fmZmj4uUAT5FPtKMKCspmLWQeUEfiwIDAQABAoIBAEyXR9wu7p+mbiYE -A+22Jr+5CDpJhrhsXovhmWWIq2ANIYwoF92qLX3MLTD8whd9nfNcC4UIT7/qOjv/ -WsOXvbUSK4MHGaC7/ylh01H+Fdmf2rrnZOUWpdN0AdHSej3JNbaA3uE4BL6WU9Vo -TrcBKo4TMsilzUVVdlc2qGLGQUSZPLnIJWMLQIdCe2kZ9YvUlGloD4uGT3RH6+vH -TOtXqDgLGS/79K0rnflnBsUBkXpukxzOcTRHxR0s7SJ2XCB0JfdLWfR6X1nzM4mh -rn/m2nzEOG9ICe5hoHqAEZ/ezKd/jwxMg1YMZnGAzDMw7/UPWo87wgVdxxOHOsHG -v/pK+okCgYEA/SToT82qtvWIiUTbkdDislGhTet2eEu2Bi9wCxAUQn045Y812r9d -TvJyfKJyvvpxzejaecJb8oOXyypMDay7aPOVBI1E2OqfImxF8pJ0QqejAUCinXrj -KnV72L/hjTavivWq1vHZYXSxufAMG0C7UeztwkOfk85N3wuuYYWYrc0CgYEA1oBG -2fQ0PXDyionE3c4bpRGZMJxD+3RFRMCJiE+xheRR22ObSDLUH123ZGmU0m2FTS9O -M+GJbZgdV1Oe0EJ5rWfzFYUmVJIreH+oQWaY/HMkoe705LAMcPyd0bSjRVWWiz+l -anIGjj5HaPSI7XFqdQu7j3ww67k4BBAca8++arcCgYA/cIhnt3sY7t+QxxjfqiGl -3p82D9RYwWCUnD7QBu+M2iTwIru0XlDcABaA9ZUcF1d96uUVroesdx4LZEY7BxbQ -bnrh8SVX1zSaQ9gjumA4dBp9rd0S6kET2u12nF/CK/pCMN7njySTL9N6bZYbHlXT -ajULgjbzq7gINb01420n4QKBgQCqu0epy9qY3QHwi2ALPDZ82NkZ/AeQaieIZcgS -m3wtmmIdQdcjTHHS1YFXh0JRi6MCoJiaavY8KUuRapmKIp8/CvJNOsIbpoy7SMDf -7Y3vwqZxzgVW0VnVxPzJIgKi+VDuXSaI52GYbrHgNGOYuyGFMGWF+8/kkHSppzk4 -Bw8FWQKBgQCo/7cV19n3e7ZlZ/aGhIOtSCTopMBV8/9PIw+s+ZDiQ7vRSj9DwkAQ -+x97V0idgh16tnly0xKvGCGTQR7qDsDTjHmmF4LZUGjcq7pHsTi/umCM/toE+BCk -7ayr+G0DWr5FjhQ7uCt2Rz1NKcj6EkDcM1WZxkDLAvBXjlj+T+eqtQ== ------END RSA PRIVATE KEY----- -""" - old_public_key = r"""-----BEGIN CERTIFICATE----- -MIIC2TCCAcGgAwIBAgIBATANBgkqhkiG9w0BAQsFADAAMCAXDTE5MDUwNzE4MDAw -NVoYDzIwNjkwNDI0MTgwMDA1WjAAMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB -CgKCAQEA1BuytnsdHdt6NkNfLoGJIlf9hrWux8raPP3W57cONh2MrQ6daoAX+L+i -gTSjvSTI6oxsO0dqdYXZO1+rOK3gI9OnZQhkCjq9IRoWx7AIvM7skaD0Lne9YsvA -7mGY/z9lm3IALI6OBVV5k6xnBR2PVi6A7FnDm0CRLit2Bn9eHLN3k4oLS/ynxgXB -mWWgnKtJNJwGmeD5PwzJfXCcJ3kPItiktFizJZoPmAlBP7LIzamlfSXVVoniRs45 -aGrTGpmZSdvossL41KBCYJGjP+lIL/UpDJHBeiuqVQoDl4/UZqb5xF9SC2p2Of21 -fmZmj4uUAT5FPtKMKCspmLWQeUEfiwIDAQABo1wwWjAPBgNVHRMBAf8EBTADAQH/ -MB0GA1UdDgQWBBTUHb3HX8dBfYFL1J1sCv+uCum5AzAoBgNVHSMEITAfgBTUHb3H -X8dBfYFL1J1sCv+uCum5A6EEpAIwAIIBATANBgkqhkiG9w0BAQsFAAOCAQEAcw+0 -dfHSLNAZD95G2pDnT2qShjmdLdbrDQhAXWhLeoWpXsKvC0iUyQaOF9ckl++tHM2g -ejm1vEOrZ+1uXK3qnMXPF99Wet686OhyoDt262Mt3wzGHNijAHEvQtjap8ZIwfOM -zFTvjmOlUScqF/Yg+htcGnJdQhWIrsD+exiY5Kz2IMtuW+yWLLP8bY5vPg6qfrp2 -4VVJ3Md1gdSownd1Au5tqPXm6VfSgLiCm9iDPVsjDII9h8ydate1d2TBHPup+4tN -JZ5/muctimydC+S2oCn7ucsilxZD89N7rJjKXNfoUOGHjOEVQMa8RtZLzH2sUEaS -FktE6rH8a+8SwO+TGw== ------END CERTIFICATE----- -""" - - with open(old_private_key_file, 'w') as key_file: - key_file.write(old_private_key) - with open(old_public_key_file, 'w') as key_file: - key_file.write(old_public_key) - - eyaml_cmd = EYAMLProcessor.get_eyaml_executable("eyaml") - run( - "{} createkeys --pkcs7-private-key={} --pkcs7-public-key={}" - .format(eyaml_cmd, new_private_key_file, new_public_key_file).split() - ) - - return ( - old_private_key_file, old_public_key_file, - new_private_key_file, new_public_key_file - ) - @pytest.fixture def force_subprocess_run_cpe(monkeypatch): import yamlpath.eyaml.eyamlprocessor as break_module @@ -202,8 +114,8 @@ def fake_access(*args, **kwargs): monkeypatch.setattr(break_module, "access", fake_access) class Test_eyaml_EYAMLProcessor(): - def test_find_eyaml_paths(self, logger_f, eyamldata_f): - processor = EYAMLProcessor(logger_f, eyamldata_f) + def test_find_eyaml_paths(self, quiet_logger, eyamldata_f): + processor = EYAMLProcessor(quiet_logger, eyamldata_f) expected = [ "aliases[&secretIdentity]", "aliases[&secretPhrase]", @@ -226,8 +138,8 @@ def test_find_eyaml_paths(self, logger_f, eyamldata_f): ("aliases[&secretIdentity]", "This is not the identity you are looking for."), ("aliases[&secretPhrase]", "There is no secret phrase."), ]) - def test_happy_get_eyaml_values(self, logger_f, eyamldata_f, eyamlkeys, yaml_path, compare): - processor = EYAMLProcessor(logger_f, eyamldata_f, privatekey=eyamlkeys[0], publickey=eyamlkeys[1]) + def test_happy_get_eyaml_values(self, quiet_logger, eyamldata_f, old_eyaml_keys, yaml_path, compare): + processor = EYAMLProcessor(quiet_logger, eyamldata_f, privatekey=old_eyaml_keys[0], publickey=old_eyaml_keys[1]) for node in processor.get_eyaml_values(yaml_path, True): assert node == compare @@ -236,8 +148,8 @@ def test_happy_get_eyaml_values(self, logger_f, eyamldata_f, eyamlkeys, yaml_pat ("aliases[&secretIdentity]", "This is your new identity.", True, EYAMLOutputFormats.STRING), ("aliases[&brandNewEntry]", "This key doesn't already exist.", False, EYAMLOutputFormats.BLOCK), ]) - def test_happy_set_eyaml_value(self, logger_f, eyamldata_f, eyamlkeys, yaml_path, compare, mustexist, output_format): - processor = EYAMLProcessor(logger_f, eyamldata_f, privatekey=eyamlkeys[0], publickey=eyamlkeys[1]) + def test_happy_set_eyaml_value(self, quiet_logger, eyamldata_f, old_eyaml_keys, yaml_path, compare, mustexist, output_format): + processor = EYAMLProcessor(quiet_logger, eyamldata_f, privatekey=old_eyaml_keys[0], publickey=old_eyaml_keys[1]) # Set the test value processor.set_eyaml_value(yaml_path, compare, output_format, mustexist) @@ -255,8 +167,8 @@ def test_happy_set_eyaml_value(self, logger_f, eyamldata_f, eyamlkeys, yaml_path ("/aliased::secrets/novel_values/ident", "New, novel, encrypted identity in BLOCK format", EYAMLOutputFormats.BLOCK, YAMLValueFormats.FOLDED), ("/aliased::secrets/string_values/ident", "New, novel, encrypted identity in STRING format", EYAMLOutputFormats.STRING, YAMLValueFormats.BARE), ]) - def test_preserve_old_blockiness(self, logger_f, eyamldata_f, eyamlkeys, yaml_path, newval, eoformat, yvformat): - processor = EYAMLProcessor(logger_f, eyamldata_f, privatekey=eyamlkeys[0], publickey=eyamlkeys[1]) + def test_preserve_old_blockiness(self, quiet_logger, eyamldata_f, old_eyaml_keys, yaml_path, newval, eoformat, yvformat): + processor = EYAMLProcessor(quiet_logger, eyamldata_f, privatekey=old_eyaml_keys[0], publickey=old_eyaml_keys[1]) processor.set_eyaml_value(yaml_path, newval, output=eoformat) encvalue = None @@ -279,59 +191,59 @@ def test_none_eyaml_value(self): def test_impossible_eyaml_exe(self, exe): assert None == EYAMLProcessor.get_eyaml_executable(exe) - def test_not_can_run_eyaml(self, logger_f): - processor = EYAMLProcessor(logger_f, None) + def test_not_can_run_eyaml(self, quiet_logger): + processor = EYAMLProcessor(quiet_logger, None) processor.eyaml = None assert False == processor._can_run_eyaml() @requireseyaml - def test_bad_encryption_keys(self, logger_f): - processor = EYAMLProcessor(logger_f, None) + def test_bad_encryption_keys(self, quiet_logger): + processor = EYAMLProcessor(quiet_logger, None) processor.privatekey = "/no/such/file" processor.publickey = "/no/such/file" with pytest.raises(EYAMLCommandException): processor.encrypt_eyaml("test") - def test_no_encrypt_without_eyaml(self, logger_f): - processor = EYAMLProcessor(logger_f, None) + def test_no_encrypt_without_eyaml(self, quiet_logger): + processor = EYAMLProcessor(quiet_logger, None) processor.eyaml = None with pytest.raises(EYAMLCommandException): processor.encrypt_eyaml("test") - def test_no_decrypt_without_eyaml(self, logger_f): - processor = EYAMLProcessor(logger_f, None) + def test_no_decrypt_without_eyaml(self, quiet_logger): + processor = EYAMLProcessor(quiet_logger, None) processor.eyaml = None with pytest.raises(EYAMLCommandException): processor.decrypt_eyaml("ENC[...]") - def test_ignore_already_encrypted_cryps(self, logger_f): - processor = EYAMLProcessor(logger_f, None) + def test_ignore_already_encrypted_cryps(self, quiet_logger): + processor = EYAMLProcessor(quiet_logger, None) testval = "ENC[...]" assert testval == processor.encrypt_eyaml(testval) - def test_ignore_already_decrypted_cryps(self, logger_f): - processor = EYAMLProcessor(logger_f, None) + def test_ignore_already_decrypted_cryps(self, quiet_logger): + processor = EYAMLProcessor(quiet_logger, None) testval = "some value" assert testval == processor.decrypt_eyaml(testval) @requireseyaml - def test_impossible_decryption(self, logger_f, eyamlkeys): - processor = EYAMLProcessor(logger_f, None) + def test_impossible_decryption(self, quiet_logger, old_eyaml_keys): + processor = EYAMLProcessor(quiet_logger, None) testval = "ENC[...]" with pytest.raises(EYAMLCommandException): processor.decrypt_eyaml(testval) - def test_encrypt_calledprocesserror(self, logger_f, force_subprocess_run_cpe): - processor = EYAMLProcessor(logger_f, None) + def test_encrypt_calledprocesserror(self, quiet_logger, force_subprocess_run_cpe): + processor = EYAMLProcessor(quiet_logger, None) with pytest.raises(EYAMLCommandException): processor.encrypt_eyaml("any value") - def test_decrypt_calledprocesserror(self, logger_f, force_subprocess_run_cpe): - processor = EYAMLProcessor(logger_f, None) + def test_decrypt_calledprocesserror(self, quiet_logger, force_subprocess_run_cpe): + processor = EYAMLProcessor(quiet_logger, None) with pytest.raises(EYAMLCommandException): processor.decrypt_eyaml("ENC[...]") @requireseyaml - def test_non_executable(self, eyamlkeys, force_no_access): - assert EYAMLProcessor.get_eyaml_executable(str(eyamlkeys[0])) is None + def test_non_executable(self, old_eyaml_keys, force_no_access): + assert EYAMLProcessor.get_eyaml_executable(str(old_eyaml_keys[0])) is None diff --git a/tests/test_processor.py b/tests/test_processor.py index 0c8b854a..6cce8005 100644 --- a/tests/test_processor.py +++ b/tests/test_processor.py @@ -1,7 +1,5 @@ import pytest -from types import SimpleNamespace - from ruamel.yaml import YAML from yamlpath.exceptions import YAMLPathException @@ -11,22 +9,16 @@ PathSearchMethods, YAMLValueFormats, ) -from yamlpath.wrappers import ConsolePrinter from yamlpath.path import SearchTerms from yamlpath import YAMLPath, Processor +from tests.conftest import quiet_logger -@pytest.fixture -def logger_f(): - """Returns a quiet ConsolePrinter.""" - args = SimpleNamespace(verbose=False, quiet=True, debug=False) - return ConsolePrinter(args) - class Test_Processor(): """Tests for the Processor class.""" - def test_get_none_data_nodes(self, logger_f): - processor = Processor(logger_f, None) + def test_get_none_data_nodes(self, quiet_logger): + processor = Processor(quiet_logger, None) yamlpath = YAMLPath("abc") matches = 0 for node in processor.get_nodes(yamlpath, mustexist=False): @@ -70,7 +62,7 @@ def test_get_none_data_nodes(self, logger_f): ("does.not.previously.exist[7]", ["Huzzah!"], False, "Huzzah!"), ("/number_keys/1", ["one"], True, None), ]) - def test_get_nodes(self, logger_f, yamlpath, results, mustexist, default): + def test_get_nodes(self, quiet_logger, yamlpath, results, mustexist, default): yamldata = """--- aliases: - &aliasAnchorOne Anchored Scalar Value @@ -101,7 +93,7 @@ def test_get_nodes(self, logger_f, yamlpath, results, mustexist, default): 3: three """ yaml = YAML() - processor = Processor(logger_f, yaml.load(yamldata)) + processor = Processor(quiet_logger, yaml.load(yamldata)) matchidx = 0 for node in processor.get_nodes( yamlpath, mustexist=mustexist, default_value=default @@ -110,13 +102,13 @@ def test_get_nodes(self, logger_f, yamlpath, results, mustexist, default): matchidx += 1 assert len(results) == matchidx - def test_enforce_pathsep(self, logger_f): + def test_enforce_pathsep(self, quiet_logger): yamldata = """--- aliases: - &aliasAnchorOne Anchored Scalar Value """ yaml = YAML() - processor = Processor(logger_f, yaml.load(yamldata)) + processor = Processor(quiet_logger, yaml.load(yamldata)) yamlpath = YAMLPath("aliases[&firstAlias]") for node in processor.get_nodes(yamlpath, pathsep=PathSeperators.FSLASH): assert node == "Anchored Scalar Value" @@ -134,7 +126,7 @@ def test_enforce_pathsep(self, logger_f): ("/floats/[.>=4.F]", True), ("/floats/[.<=4.F]", True), ]) - def test_get_impossible_nodes_error(self, logger_f, yamlpath, mustexist): + def test_get_impossible_nodes_error(self, quiet_logger, yamlpath, mustexist): yamldata = """--- ints: - 1 @@ -148,17 +140,17 @@ def test_get_impossible_nodes_error(self, logger_f, yamlpath, mustexist): - 3.3 """ yaml = YAML() - processor = Processor(logger_f, yaml.load(yamldata)) + processor = Processor(quiet_logger, yaml.load(yamldata)) with pytest.raises(YAMLPathException) as ex: nodes = list(processor.get_nodes(yamlpath, mustexist=mustexist)) assert -1 < str(ex.value).find("does not match any nodes") - def test_set_value_in_none_data(self, capsys, logger_f): + def test_set_value_in_none_data(self, capsys, quiet_logger): import sys yamldata = "" yaml = YAML() data = yaml.load(yamldata) - processor = Processor(logger_f, data) + processor = Processor(quiet_logger, data) processor.set_value("abc", "void") yaml.dump(data, sys.stdout) assert -1 == capsys.readouterr().out.find("abc") @@ -168,7 +160,7 @@ def test_set_value_in_none_data(self, capsys, logger_f): (YAMLPath("top_scalar"), "New top-level value", 1, False, YAMLValueFormats.DEFAULT, PathSeperators.DOT), ("/top_array/2", 42, 1, False, YAMLValueFormats.INT, PathSeperators.FSLASH), ]) - def test_set_value(self, logger_f, yamlpath, value, tally, mustexist, vformat, pathsep): + def test_set_value(self, quiet_logger, yamlpath, value, tally, mustexist, vformat, pathsep): yamldata = """--- aliases: - &testAnchor Initial Value @@ -182,7 +174,7 @@ def test_set_value(self, logger_f, yamlpath, value, tally, mustexist, vformat, p """ yaml = YAML() data = yaml.load(yamldata) - processor = Processor(logger_f, data) + processor = Processor(quiet_logger, data) processor.set_value(yamlpath, value, mustexist=mustexist, value_format=vformat, pathsep=pathsep) matchtally = 0 for node in processor.get_nodes(yamlpath, mustexist=mustexist): @@ -190,41 +182,41 @@ def test_set_value(self, logger_f, yamlpath, value, tally, mustexist, vformat, p matchtally += 1 assert matchtally == tally - def test_cannot_set_nonexistent_required_node_error(self, logger_f): + def test_cannot_set_nonexistent_required_node_error(self, quiet_logger): yamldata = """--- key: value """ yaml = YAML() data = yaml.load(yamldata) - processor = Processor(logger_f, data) + processor = Processor(quiet_logger, data) with pytest.raises(YAMLPathException) as ex: processor.set_value("abc", "void", mustexist=True) assert -1 < str(ex.value).find("No nodes matched") - def test_none_data_to_get_nodes_by_path_segment(self, capsys, logger_f): + def test_none_data_to_get_nodes_by_path_segment(self, capsys, quiet_logger): import sys yamldata = "" yaml = YAML() data = yaml.load(yamldata) - processor = Processor(logger_f, data) + processor = Processor(quiet_logger, data) nodes = list(processor._get_nodes_by_path_segment(data, YAMLPath("abc"), 0)) yaml.dump(data, sys.stdout) assert -1 == capsys.readouterr().out.find("abc") - def test_bad_segment_index_for_get_nodes_by_path_segment(self, capsys, logger_f): + def test_bad_segment_index_for_get_nodes_by_path_segment(self, capsys, quiet_logger): import sys yamldata = """--- key: value """ yaml = YAML() data = yaml.load(yamldata) - processor = Processor(logger_f, data) + processor = Processor(quiet_logger, data) nodes = list(processor._get_nodes_by_path_segment(data, YAMLPath("abc"), 10)) yaml.dump(data, sys.stdout) assert -1 == capsys.readouterr().out.find("abc") - def test_get_nodes_by_unknown_path_segment_error(self, logger_f): + def test_get_nodes_by_unknown_path_segment_error(self, quiet_logger): from collections import deque from enum import Enum from yamlpath.enums import PathSegmentTypes @@ -236,7 +228,7 @@ def test_get_nodes_by_unknown_path_segment_error(self, logger_f): """ yaml = YAML() data = yaml.load(yamldata) - processor = Processor(logger_f, data) + processor = Processor(quiet_logger, data) path = YAMLPath("abc") stringified = str(path) # Force Path to parse path._escaped = deque([ @@ -246,7 +238,7 @@ def test_get_nodes_by_unknown_path_segment_error(self, logger_f): with pytest.raises(NotImplementedError): nodes = list(processor._get_nodes_by_path_segment(data, path, 0)) - def test_non_int_slice_error(self, logger_f): + def test_non_int_slice_error(self, quiet_logger): yamldata = """--- - step: 1 - step: 2 @@ -254,13 +246,13 @@ def test_non_int_slice_error(self, logger_f): """ yaml = YAML() data = yaml.load(yamldata) - processor = Processor(logger_f, data) + processor = Processor(quiet_logger, data) with pytest.raises(YAMLPathException) as ex: processor.set_value("[1:4F]", "") assert -1 < str(ex.value).find("is not an integer array slice") - def test_non_int_array_index_error(self, logger_f): + def test_non_int_array_index_error(self, quiet_logger): from collections import deque yamldata = """--- - 1 @@ -268,7 +260,7 @@ def test_non_int_array_index_error(self, logger_f): yaml = YAML() data = yaml.load(yamldata) path = YAMLPath("[0]") - processor = Processor(logger_f, data) + processor = Processor(quiet_logger, data) strp = str(path) path._escaped = deque([ @@ -282,7 +274,7 @@ def test_non_int_array_index_error(self, logger_f): nodes = list(processor._get_nodes_by_index(data, path, 0)) assert -1 < str(ex.value).find("is not an integer array index") - def test_nonexistant_path_search_method_error(self, logger_f): + def test_nonexistant_path_search_method_error(self, quiet_logger): from enum import Enum from yamlpath.enums import PathSearchMethods names = [m.name for m in PathSearchMethods] + ['DNF'] @@ -293,7 +285,7 @@ def test_nonexistant_path_search_method_error(self, logger_f): """ yaml = YAML() data = yaml.load(yamldata) - processor = Processor(logger_f, data) + processor = Processor(quiet_logger, data) with pytest.raises(NotImplementedError): nodes = list(processor._get_nodes_by_search( @@ -301,19 +293,19 @@ def test_nonexistant_path_search_method_error(self, logger_f): SearchTerms(True, PathSearchMethods.DNF, ".", "top_scalar") )) - def test_adjoined_collectors_error(self, logger_f): + def test_adjoined_collectors_error(self, quiet_logger): yamldata = """--- key: value """ yaml = YAML() data = yaml.load(yamldata) - processor = Processor(logger_f, data) + processor = Processor(quiet_logger, data) with pytest.raises(YAMLPathException) as ex: nodes = list(processor.get_nodes("(&arrayOfHashes.step)(disabled_steps)")) assert -1 < str(ex.value).find("has no meaning") - def test_no_attrs_to_arrays_error(self, logger_f): + def test_no_attrs_to_arrays_error(self, quiet_logger): yamldata = """--- array: - one @@ -321,13 +313,13 @@ def test_no_attrs_to_arrays_error(self, logger_f): """ yaml = YAML() data = yaml.load(yamldata) - processor = Processor(logger_f, data) + processor = Processor(quiet_logger, data) with pytest.raises(YAMLPathException) as ex: nodes = list(processor.get_nodes("array.attr")) assert -1 < str(ex.value).find("Cannot add") - def test_no_index_to_hashes_error(self, logger_f): + def test_no_index_to_hashes_error(self, quiet_logger): # Using [#] syntax is a disambiguated INDEX ELEMENT NUMBER. In # DICTIONARY context, this would create an ambiguous request to access # either the #th value or a value whose key is the literal #. As such, @@ -340,13 +332,13 @@ def test_no_index_to_hashes_error(self, logger_f): """ yaml = YAML() data = yaml.load(yamldata) - processor = Processor(logger_f, data) + processor = Processor(quiet_logger, data) with pytest.raises(YAMLPathException) as ex: nodes = list(processor.get_nodes("hash[6]")) assert -1 < str(ex.value).find("Cannot add") - def test_get_nodes_array_impossible_type_error(self, logger_f): + def test_get_nodes_array_impossible_type_error(self, quiet_logger): yamldata = """--- array: - 1 @@ -354,19 +346,19 @@ def test_get_nodes_array_impossible_type_error(self, logger_f): """ yaml = YAML() data = yaml.load(yamldata) - processor = Processor(logger_f, data) + processor = Processor(quiet_logger, data) with pytest.raises(YAMLPathException) as ex: nodes = list(processor.get_nodes(r"/array/(.=~/^.{3,4}$/)", default_value="New value")) assert -1 < str(ex.value).find("Cannot add") - def test_no_attrs_to_scalars_errors(self, logger_f): + def test_no_attrs_to_scalars_errors(self, quiet_logger): yamldata = """--- scalar: value """ yaml = YAML() data = yaml.load(yamldata) - processor = Processor(logger_f, data) + processor = Processor(quiet_logger, data) with pytest.raises(YAMLPathException) as ex: nodes = list(processor.get_nodes("scalar[6]")) @@ -382,7 +374,7 @@ def test_no_attrs_to_scalars_errors(self, logger_f): ("/anchorKeys[&recursiveAnchorKey]", "Recurse more", 1, True, YAMLValueFormats.DEFAULT, PathSeperators.AUTO), ("/hash[&recursiveAnchorKey]", "Recurse even more", 1, True, YAMLValueFormats.DEFAULT, PathSeperators.AUTO), ]) - def test_key_anchor_changes(self, logger_f, yamlpath, value, tally, mustexist, vformat, pathsep): + def test_key_anchor_changes(self, quiet_logger, yamlpath, value, tally, mustexist, vformat, pathsep): yamldata = """--- anchorKeys: &keyOne aliasOne: 11A1 @@ -399,7 +391,7 @@ def test_key_anchor_changes(self, logger_f, yamlpath, value, tally, mustexist, v """ yaml = YAML() data = yaml.load(yamldata) - processor = Processor(logger_f, data) + processor = Processor(quiet_logger, data) yamlpath = YAMLPath(yamlpath) processor.set_value(yamlpath, value, mustexist=mustexist, value_format=vformat, pathsep=pathsep) @@ -409,7 +401,7 @@ def test_key_anchor_changes(self, logger_f, yamlpath, value, tally, mustexist, v matchtally += 1 assert matchtally == tally - def test_key_anchor_children(self, logger_f): + def test_key_anchor_children(self, quiet_logger): yamldata = """--- anchorKeys: &keyOne aliasOne: 1 1 Alpha 1 @@ -423,7 +415,7 @@ def test_key_anchor_children(self, logger_f): """ yaml = YAML() data = yaml.load(yamldata) - processor = Processor(logger_f, data) + processor = Processor(quiet_logger, data) yamlpath = YAMLPath("hash[&keyTwo].subval") newvalue = "Mute audibles" @@ -434,7 +426,7 @@ def test_key_anchor_children(self, logger_f): matchtally += 1 assert matchtally == 1 - def test_cannot_add_novel_alias_keys(self, logger_f): + def test_cannot_add_novel_alias_keys(self, quiet_logger): yamldata = """--- anchorKeys: &keyOne aliasOne: 1 1 Alpha 1 @@ -448,7 +440,7 @@ def test_cannot_add_novel_alias_keys(self, logger_f): """ yaml = YAML() data = yaml.load(yamldata) - processor = Processor(logger_f, data) + processor = Processor(quiet_logger, data) yamlpath = YAMLPath("hash[&keyThree].subval") newvalue = "Abort" From 0e4d0011fc7e507720146cd9b16a5365480ceeda Mon Sep 17 00:00:00 2001 From: William Kimball <30981667+wwkimball@users.noreply.github.com> Date: Thu, 30 May 2019 09:22:30 -0500 Subject: [PATCH 10/16] WIP: Chipping away at yaml-set test coverage --- tests/test_commands_yaml_set.py | 198 +++++++++++++++++++++++++++++++- yamlpath/commands/yaml_set.py | 8 +- 2 files changed, 196 insertions(+), 10 deletions(-) diff --git a/tests/test_commands_yaml_set.py b/tests/test_commands_yaml_set.py index d6593f71..ba5b3ffa 100644 --- a/tests/test_commands_yaml_set.py +++ b/tests/test_commands_yaml_set.py @@ -149,8 +149,6 @@ def test_bad_yaml_path(self, script_runner, tmp_path_factory): assert "Required YAML Path does not match any nodes" in result.stderr def test_checked_replace(self, script_runner, tmp_path_factory): - import re - content = """--- key: value """ @@ -174,9 +172,203 @@ def test_missing_key(self, script_runner, tmp_path_factory, old_eyaml_keys): self.command, "--change=encrypted", "--random=1", - "--check=n/a" + "--check=n/a", "--privatekey={}".format(old_eyaml_keys[0]), yaml_file ) assert not result.success, result.stderr assert "Neither or both private and public EYAML keys must be set" in result.stderr + + result = script_runner.run( + self.command, + "--change=encrypted", + "--random=1", + "--check=n/a", + "--publickey={}".format(old_eyaml_keys[1]), + yaml_file + ) + assert not result.success, result.stderr + assert "Neither or both private and public EYAML keys must be set" in result.stderr + + @requireseyaml + def test_bad_decryption(self, script_runner, tmp_path_factory, old_eyaml_keys): + content = """--- + encrypted: ENC[PKCS7,MIIx...broken-on-purpose...==] + """ + yaml_file = create_temp_yaml_file(tmp_path_factory, content) + result = script_runner.run( + self.command, + "--change=encrypted", + "--random=1", + "--check=n/a", + "--privatekey={}".format(old_eyaml_keys[0]), + "--publickey={}".format(old_eyaml_keys[1]), + yaml_file + ) + assert not result.success, result.stderr + assert "Unable to decrypt value!" in result.stderr + + def test_bad_value_check(self, script_runner, tmp_path_factory): + content = """--- + key: value + """ + yaml_file = create_temp_yaml_file(tmp_path_factory, content) + result = script_runner.run( + self.command, + "--change=key", + "--random=1", + "--check=abc", + yaml_file + ) + assert not result.success, result.stderr + assert "does not match the check value" in result.stderr + + def test_cannot_save_multiple_matches(self, script_runner, tmp_path_factory): + content = """--- + key1: value1 + key2: value2 + """ + yaml_file = create_temp_yaml_file(tmp_path_factory, content) + result = script_runner.run( + self.command, + "--change=/[.^key]", + "--random=1", + "--saveto=/backup", + yaml_file + ) + assert not result.success, result.stderr + assert "It is impossible to meaningly save more than one" in result.stderr + + def test_save_old_plain_value(self, script_runner, tmp_path_factory): + import re + + content = """--- + key: value + """ + yaml_file = create_temp_yaml_file(tmp_path_factory, content) + result = script_runner.run( + self.command, + "--change=key", + "--value=new", + "--saveto=backup", + yaml_file + ) + assert result.success, result.stderr + + with open(yaml_file, 'r') as fhnd: + filedat = fhnd.read() + assert re.findall(r"^backup:\s+value$", filedat, re.M), filedat + + def test_save_old_crypt_value(self, script_runner, tmp_path_factory): + import re + + content = """--- + encrypted: > + ENC[PKCS7,MIIB...] + """ + yaml_file = create_temp_yaml_file(tmp_path_factory, content) + result = script_runner.run( + self.command, + "--change=encrypted", + "--value=now_plaintext", + "--saveto=backup", + yaml_file + ) + assert result.success, result.stderr + + with open(yaml_file, 'r') as fhnd: + filedat = fhnd.read() + assert re.findall(r"^backup:\s+>$", filedat, re.M), filedat + + def test_broken_saveto(self, script_runner, tmp_path_factory): + content = """--- + key: value + """ + yaml_file = create_temp_yaml_file(tmp_path_factory, content) + result = script_runner.run( + self.command, + "--change=key", + "--random=1", + "--saveto=[2]", + yaml_file + ) + assert not result.success, result.stderr + assert "Cannot add" in result.stderr + + @requireseyaml + def test_bad_decryption(self, script_runner, tmp_path_factory, old_eyaml_keys): + content = """--- + encrypted: ENC[PKCS7,MIIx...broken-on-purpose...==] + """ + yaml_file = create_temp_yaml_file(tmp_path_factory, content) + result = script_runner.run( + self.command, + "--change=encrypted", + "--random=1", + "--check=n/a", + "--privatekey={}".format(old_eyaml_keys[0]), + "--publickey={}".format(old_eyaml_keys[1]), + yaml_file + ) + assert not result.success, result.stderr + assert "Unable to decrypt value!" in result.stderr + + @requireseyaml + def test_good_encryption(self, script_runner, tmp_path_factory, old_eyaml_keys): + import re + + content = """--- + key: > + old + multiline + value + """ + yaml_file = create_temp_yaml_file(tmp_path_factory, content) + result = script_runner.run( + self.command, + "--change=key", + "--value=now_encrypted", + "--eyamlcrypt", + "--privatekey={}".format(old_eyaml_keys[0]), + "--publickey={}".format(old_eyaml_keys[1]), + yaml_file + ) + assert result.success, result.stderr + + with open(yaml_file, 'r') as fhnd: + filedat = fhnd.read() + assert re.findall(r"\nkey:\s+>\n\s{2}ENC\[.+\n", filedat), filedat + + @requireseyaml + def test_bad_crypt_path(self, script_runner, tmp_path_factory, old_eyaml_keys): + content = """--- + key: value + """ + yaml_file = create_temp_yaml_file(tmp_path_factory, content) + result = script_runner.run( + self.command, + "--change=[0]", + "--random=1", + "--eyamlcrypt", + "--privatekey={}".format(old_eyaml_keys[0]), + "--publickey={}".format(old_eyaml_keys[1]), + yaml_file + ) + assert not result.success, result.stderr + assert "Cannot add" in result.stderr + + def test_bad_eyaml_command(self, script_runner, tmp_path_factory): + content = """--- + key: value + """ + yaml_file = create_temp_yaml_file(tmp_path_factory, content) + result = script_runner.run( + self.command, + "--change=key", + "--random=1", + "--eyamlcrypt", + "--eyaml=/does/not/exist/on-most/systems", + yaml_file + ) + assert not result.success, result.stderr + assert "The eyaml binary is not executable" in result.stderr diff --git a/yamlpath/commands/yaml_set.py b/yamlpath/commands/yaml_set.py index ea9ca450..732badfb 100644 --- a/yamlpath/commands/yaml_set.py +++ b/yamlpath/commands/yaml_set.py @@ -325,15 +325,9 @@ def main(): # Set the requested value log.verbose("Setting the new value for {}.".format(change_path)) if args.eyamlcrypt: - try: - format_type = YAMLValueFormats.from_str(args.format) - except NameError: - log.critical( - "Unknown YAML value format ,{}.".format(args.format), 1 - ) - # If the user hasn't specified a format, use the same format as the # value being replaced, if known. + format_type = YAMLValueFormats.from_str(args.format) if format_type is YAMLValueFormats.DEFAULT: format_type = old_format From 805cee2f984a62df836401b5b63ea59b51d9e3b6 Mon Sep 17 00:00:00 2001 From: "William W. Kimball" Date: Thu, 30 May 2019 11:12:36 -0500 Subject: [PATCH 11/16] WIP: Rudimentary 100% test coverage for yaml-set --- tests/test_commands_yaml_set.py | 61 ++++++++++++++++++++++++++ yamlpath/commands/eyaml_rotate_keys.py | 2 +- yamlpath/commands/yaml_set.py | 11 +---- 3 files changed, 64 insertions(+), 10 deletions(-) diff --git a/tests/test_commands_yaml_set.py b/tests/test_commands_yaml_set.py index ba5b3ffa..4da43fbd 100644 --- a/tests/test_commands_yaml_set.py +++ b/tests/test_commands_yaml_set.py @@ -280,6 +280,20 @@ def test_save_old_crypt_value(self, script_runner, tmp_path_factory): filedat = fhnd.read() assert re.findall(r"^backup:\s+>$", filedat, re.M), filedat + def test_broken_change(self, script_runner, tmp_path_factory): + content = """--- + key: value + """ + yaml_file = create_temp_yaml_file(tmp_path_factory, content) + result = script_runner.run( + self.command, + "--change=[0]", + "--random=1", + yaml_file + ) + assert not result.success, result.stderr + assert "Cannot add" in result.stderr + def test_broken_saveto(self, script_runner, tmp_path_factory): content = """--- key: value @@ -372,3 +386,50 @@ def test_bad_eyaml_command(self, script_runner, tmp_path_factory): ) assert not result.success, result.stderr assert "The eyaml binary is not executable" in result.stderr + + def test_backup_file(self, script_runner, tmp_path_factory): + import os + + content = """--- + key: value + """ + yaml_file = create_temp_yaml_file(tmp_path_factory, content) + backup_file = yaml_file + ".bak" + result = script_runner.run( + self.command, + "--change=key", + "--random=1", + "--backup", + yaml_file + ) + assert result.success, result.stderr + assert os.path.isfile(backup_file) + + with open(backup_file, 'r') as fhnd: + filedat = fhnd.read() + assert filedat == content + + def test_replace_backup_file(self, script_runner, tmp_path_factory): + import os + + content = """--- + key: value + """ + yaml_file = create_temp_yaml_file(tmp_path_factory, content) + backup_file = yaml_file + ".bak" + with open(backup_file, 'w') as fhnd: + fhnd.write(content + "\nkey2: value2") + + result = script_runner.run( + self.command, + "--change=key", + "--random=1", + "--backup", + yaml_file + ) + assert result.success, result.stderr + assert os.path.isfile(backup_file) + + with open(backup_file, 'r') as fhnd: + filedat = fhnd.read() + assert filedat == content diff --git a/yamlpath/commands/eyaml_rotate_keys.py b/yamlpath/commands/eyaml_rotate_keys.py index aafd607a..5bef0984 100644 --- a/yamlpath/commands/eyaml_rotate_keys.py +++ b/yamlpath/commands/eyaml_rotate_keys.py @@ -207,4 +207,4 @@ def main(): yaml.dump(yaml_data, yaml_dump) if __name__ == "__main__": - main() + main() # pragma: no cover diff --git a/yamlpath/commands/yaml_set.py b/yamlpath/commands/yaml_set.py index 732badfb..bd316a3c 100644 --- a/yamlpath/commands/yaml_set.py +++ b/yamlpath/commands/yaml_set.py @@ -341,17 +341,10 @@ def main(): , output=output_type , mustexist=False ) - except YAMLPathException as ex: - log.critical(ex, 1) except EYAMLCommandException as ex: log.critical(ex, 2) else: - try: - processor.set_value( - change_path, new_value, value_format=args.format - ) - except YAMLPathException as ex: - log.critical(ex, 1) + processor.set_value(change_path, new_value, value_format=args.format) # Save a backup of the original file, if requested if args.backup: @@ -368,4 +361,4 @@ def main(): yaml.dump(yaml_data, yaml_dump) if __name__ == "__main__": - main() + main() # pragma: no cover From e5f5089f4342b8b4f92e6afdde20d7ad3c190fdd Mon Sep 17 00:00:00 2001 From: "William W. Kimball" Date: Thu, 30 May 2019 13:00:18 -0500 Subject: [PATCH 12/16] WIP: Adding tests for eyaml-rotate-keys --- tests/test_commands_eyaml_rotate_keys.py | 124 +++++++++++++++++++++++ yamlpath/commands/eyaml_rotate_keys.py | 13 ++- 2 files changed, 133 insertions(+), 4 deletions(-) create mode 100644 tests/test_commands_eyaml_rotate_keys.py diff --git a/tests/test_commands_eyaml_rotate_keys.py b/tests/test_commands_eyaml_rotate_keys.py new file mode 100644 index 00000000..155aa91b --- /dev/null +++ b/tests/test_commands_eyaml_rotate_keys.py @@ -0,0 +1,124 @@ +import pytest + +from tests.conftest import ( + create_temp_yaml_file, + requireseyaml, + old_eyaml_keys, + new_eyaml_keys, +) + +class Test_eyaml_rotate_keys(): + """Tests for the eyaml-rotate-keys command-line interface.""" + command = "eyaml-rotate-keys" + + def test_no_options(self, script_runner): + result = script_runner.run(self.command) + assert not result.success, result.stderr + assert "usage: {}".format(self.command) in result.stderr + + def test_duplicate_keys(self, script_runner): + bunk_key = "/does/not/exist/on-most/systems" + result = script_runner.run( + self.command, + "--newprivatekey={}".format(bunk_key), + "--newpublickey={}".format(bunk_key), + "--oldprivatekey={}".format(bunk_key), + "--oldpublickey={}".format(bunk_key), + bunk_key + ) + assert not result.success, result.stderr + assert "The new and old EYAML keys must be different." in result.stderr + + def test_bad_keys(self, script_runner): + bunk_file = "/does/not/exist/on-most/systems" + bunk_old_key = "/does/not/exist/on-most/systems/old" + bunk_new_key = "/does/not/exist/on-most/systems/new" + result = script_runner.run( + self.command, + "--newprivatekey={}".format(bunk_new_key), + "--newpublickey={}".format(bunk_new_key), + "--oldprivatekey={}".format(bunk_old_key), + "--oldpublickey={}".format(bunk_old_key), + bunk_file + ) + assert not result.success, result.stderr + assert "EYAML key is not a readable file:" in result.stderr + + @requireseyaml + def test_no_yaml_files(self, script_runner, old_eyaml_keys, new_eyaml_keys): + bunk_file = "/does/not/exist/on-most/systems" + result = script_runner.run( + self.command, + "--newprivatekey={}".format(new_eyaml_keys[0]), + "--newpublickey={}".format(new_eyaml_keys[1]), + "--oldprivatekey={}".format(old_eyaml_keys[0]), + "--oldpublickey={}".format(old_eyaml_keys[1]), + bunk_file + ) + assert not result.success, result.stderr + assert "Not a file:" in result.stderr + + @requireseyaml + def test_good_replace(self, script_runner, tmp_path_factory, old_eyaml_keys, new_eyaml_keys): + content = """--- + encrypted: > + ENC[PKCS7,MIIBiQYJKoZIhvcNAQcDoIIBejCCAXYCAQAxggEhMIIBHQIBADAFMAACAQEw + DQYJKoZIhvcNAQEBBQAEggEArvk6OYa1gACTdrWq2SpCrtGRlc61la5AGU7L + aLTyKfqD9vqx71RDjobfOF96No07kLsEpoAJ+LKKHNjdG6kjvpGPmttj9Dkm + XVoU6A+YCmm4iYFKD/NkoSOEyAkoDOXSqdjrgt0f37GefEsXt6cqAavDpUJm + pmc0KI4TCG5zpfCxqttMs+stOY3Y+0WokkulQujZ7K3SdWUSHIysgMrWiect + Wdg5unxN1A/aeyvhgvYSNPjU9KBco7SDnigSs9InW/QghJFrZRrDhTp1oTUc + qK5lKvaseHkVGi91vPWeLQxZt1loJB5zL6j5BxMbvRfJK+wc3ax2u4x8WTAB + EurCwzBMBgkqhkiG9w0BBwEwHQYJYIZIAWUDBAEqBBAwcy7jvcOGcMfLEtug + LEXbgCBkocdckuDe14mVGmUmM++xN34OEVRCeGVWWUnWq1DJ4Q==] + """ + yaml_file = create_temp_yaml_file(tmp_path_factory, content) + result = script_runner.run( + self.command, + "--newprivatekey={}".format(new_eyaml_keys[0]), + "--newpublickey={}".format(new_eyaml_keys[1]), + "--oldprivatekey={}".format(old_eyaml_keys[0]), + "--oldpublickey={}".format(old_eyaml_keys[1]), + yaml_file + ) + assert result.success, result.stderr + + with open(yaml_file, 'r') as fhnd: + filedat = fhnd.read() + assert not filedat == content + + def test_yaml_parsing_error(self, script_runner, imparsible_yaml_file, old_eyaml_keys, new_eyaml_keys): + result = script_runner.run( + self.command, + "--newprivatekey={}".format(new_eyaml_keys[0]), + "--newpublickey={}".format(new_eyaml_keys[1]), + "--oldprivatekey={}".format(old_eyaml_keys[0]), + "--oldpublickey={}".format(old_eyaml_keys[1]), + imparsible_yaml_file + ) + assert not result.success, result.stderr + assert "YAML parsing error" in result.stderr + + def test_yaml_syntax_error(self, script_runner, badsyntax_yaml_file, old_eyaml_keys, new_eyaml_keys): + result = script_runner.run( + self.command, + "--newprivatekey={}".format(new_eyaml_keys[0]), + "--newpublickey={}".format(new_eyaml_keys[1]), + "--oldprivatekey={}".format(old_eyaml_keys[0]), + "--oldpublickey={}".format(old_eyaml_keys[1]), + badsyntax_yaml_file + ) + assert not result.success, result.stderr + assert "YAML syntax error" in result.stderr + + def test_yaml_composition_error(self, script_runner, badcmp_yaml_file, old_eyaml_keys, new_eyaml_keys): + result = script_runner.run( + self.command, + "--newprivatekey={}".format(new_eyaml_keys[0]), + "--newpublickey={}".format(new_eyaml_keys[1]), + "--oldprivatekey={}".format(old_eyaml_keys[0]), + "--oldpublickey={}".format(old_eyaml_keys[1]), + badcmp_yaml_file + ) + assert not result.success, result.stderr + assert "YAML composition error" in result.stderr diff --git a/yamlpath/commands/eyaml_rotate_keys.py b/yamlpath/commands/eyaml_rotate_keys.py index 5bef0984..488f16b7 100644 --- a/yamlpath/commands/eyaml_rotate_keys.py +++ b/yamlpath/commands/eyaml_rotate_keys.py @@ -111,6 +111,7 @@ def main(): # Process the input file(s) in_file_count = len(args.yaml_files) + exit_state = 0 for yaml_file in args.yaml_files: file_changed = False backup_file = yaml_file + ".bak" @@ -119,6 +120,7 @@ def main(): # Each YAML_FILE must actually be a file if not isfile(yaml_file): log.error("Not a file: {}".format(yaml_file)) + exit_state = 2 continue # Don't bother with the file change update when there's only one input @@ -130,20 +132,20 @@ def main(): try: with open(yaml_file, 'r') as fhnd: yaml_data = yaml.load(fhnd) - except FileNotFoundError: - log.error("YAML_FILE not found: {}".format(args.yaml_file)) - continue except ParserError as ex: log.error("YAML parsing error {}: {}" .format(str(ex.problem_mark).lstrip(), ex.problem)) + exit_state = 3 continue except ComposerError as ex: log.error("YAML composition error {}: {}" .format(str(ex.problem_mark).lstrip(), ex.problem)) + exit_state = 3 continue except ScannerError as ex: log.error("YAML syntax error {}: {}" .format(str(ex.problem_mark).lstrip(), ex.problem)) + exit_state = 3 continue # Process all EYAML values @@ -152,7 +154,7 @@ def main(): # Use ::get_nodes() instead of ::get_eyaml_values() here in order # to ignore values that have already been decrypted via their # Anchors. - for node in processor.get_nodes(yaml_data, yaml_path): + for node in processor.get_nodes(yaml_path): # Ignore values which are Aliases for those already decrypted anchor_name = ( node.anchor.value if hasattr(node, "anchor") else None @@ -174,6 +176,7 @@ def main(): if txtval is None: # A warning about this failure has already been printed + exit_state = 3 continue # Prefer block (folded) values unless the original YAML value @@ -206,5 +209,7 @@ def main(): with open(yaml_file, 'w') as yaml_dump: yaml.dump(yaml_data, yaml_dump) + exit(exit_state) + if __name__ == "__main__": main() # pragma: no cover From 0e15832c0b423e90980c7db5e37f272c80e9fcda Mon Sep 17 00:00:00 2001 From: "William W. Kimball" Date: Thu, 30 May 2019 14:49:14 -0500 Subject: [PATCH 13/16] WIP: 100% rudimentary coverage, eyaml-rotate-keys --- tests/test_commands_eyaml_rotate_keys.py | 174 ++++++++++++++++++++--- yamlpath/commands/eyaml_rotate_keys.py | 9 +- 2 files changed, 161 insertions(+), 22 deletions(-) diff --git a/tests/test_commands_eyaml_rotate_keys.py b/tests/test_commands_eyaml_rotate_keys.py index 155aa91b..a926c0d4 100644 --- a/tests/test_commands_eyaml_rotate_keys.py +++ b/tests/test_commands_eyaml_rotate_keys.py @@ -59,33 +59,60 @@ def test_no_yaml_files(self, script_runner, old_eyaml_keys, new_eyaml_keys): assert "Not a file:" in result.stderr @requireseyaml - def test_good_replace(self, script_runner, tmp_path_factory, old_eyaml_keys, new_eyaml_keys): - content = """--- - encrypted: > - ENC[PKCS7,MIIBiQYJKoZIhvcNAQcDoIIBejCCAXYCAQAxggEhMIIBHQIBADAFMAACAQEw - DQYJKoZIhvcNAQEBBQAEggEArvk6OYa1gACTdrWq2SpCrtGRlc61la5AGU7L - aLTyKfqD9vqx71RDjobfOF96No07kLsEpoAJ+LKKHNjdG6kjvpGPmttj9Dkm - XVoU6A+YCmm4iYFKD/NkoSOEyAkoDOXSqdjrgt0f37GefEsXt6cqAavDpUJm - pmc0KI4TCG5zpfCxqttMs+stOY3Y+0WokkulQujZ7K3SdWUSHIysgMrWiect - Wdg5unxN1A/aeyvhgvYSNPjU9KBco7SDnigSs9InW/QghJFrZRrDhTp1oTUc - qK5lKvaseHkVGi91vPWeLQxZt1loJB5zL6j5BxMbvRfJK+wc3ax2u4x8WTAB - EurCwzBMBgkqhkiG9w0BBwEwHQYJYIZIAWUDBAEqBBAwcy7jvcOGcMfLEtug - LEXbgCBkocdckuDe14mVGmUmM++xN34OEVRCeGVWWUnWq1DJ4Q==] + def test_good_multi_replacements(self, script_runner, tmp_path_factory, old_eyaml_keys, new_eyaml_keys): + simple_content = """--- + encrypted_string: ENC[PKCS7,MIIBiQYJKoZIhvcNAQcDoIIBejCCAXYCAQAxggEhMIIBHQIBADAFMAACAQEwDQYJKoZIhvcNAQEBBQAEggEAHA4rPcTzvgzPLtnGz3yoyX/kVlQ5TnPXcScXK2bwjguGZLkuzv/JVPAsOm4t6GlnROpy4zb/lUMHRJDChJhPLrSj919B8//huoMgw0EU5XTcaN6jeDDjL+vhjswjvLFOux66UwvMo8sRci/e2tlFiam8VgxzV0hpF2qRrL/l84V04gL45kq4PCYDWrJNynOwYVbSIF+qc5HaF25H8kHq1lD3RB6Ob/J942Q7k5Qt7W9mNm9cKZmxwgtUgIZWXW6mcPJ2dXDB/RuPJJSrLsb1VU/DkhdgxaNzvLCA+MViyoFUkCfHFNZbaHKNkoYXBy7dLmoh/E5tKv99FeG/7CzL3DBMBgkqhkiG9w0BBwEwHQYJYIZIAWUDBAEqBBCVU5Mjt8+4dLkoqB9YArfkgCDkdIhXR9T1M4YYa1qTE6by61VPU3g1aMExRmo4tNZ8FQ==] + encrypted_block: > + ENC[PKCS7,MIIBeQYJKoZIhvcNAQcDoIIBajCCAWYCAQAxggEhMIIBHQIBADAFMAACAQEw + DQYJKoZIhvcNAQEBBQAEggEAnxQVqyIgRTb/+VP4Q+DLJcnlS8YPouXEW8+z + it9uwUA02CEPxCEU944GcHpgTY3EEtkm+2Z/jgXI119VMML+OOQ1NkwUiAw/ + wq0vwz2D16X31XzhedQN5FZbfZ1C+2tWSQfCjE0bu7IeHfyR+k2ssD11kNZh + JDEr2bM2dwOdT0y7VGcQ06vI9gw6UXcwYAgS6FoLm7WmFftjcYiNB+0EJSW0 + VcTn2gveaw9iOQcum/Grby+9Ybs28fWd8BoU+ZWDpoIMEceujNa9okIXNPJO + jcvv1sgauwJ3RX6WFQIy/beS2RT5EOLhWIZCAQCcgJWgovu3maB7dEUZ0NLG + OYUR7zA8BgkqhkiG9w0BBwEwHQYJYIZIAWUDBAEqBBAbO16EzQ5/cdcvgB0g + tpKIgBAEgTLT5n9Jtc9venK0CKso] """ - yaml_file = create_temp_yaml_file(tmp_path_factory, content) + anchored_content = """--- + aliases: + - &blockStyle > + ENC[PKCS7,MIIBiQYJKoZIhvcNAQcDoIIBejCCAXYCAQAxggEhMIIBHQIBADAFMAACAQEw + DQYJKoZIhvcNAQEBBQAEggEArvk6OYa1gACTdrWq2SpCrtGRlc61la5AGU7L + aLTyKfqD9vqx71RDjobfOF96No07kLsEpoAJ+LKKHNjdG6kjvpGPmttj9Dkm + XVoU6A+YCmm4iYFKD/NkoSOEyAkoDOXSqdjrgt0f37GefEsXt6cqAavDpUJm + pmc0KI4TCG5zpfCxqttMs+stOY3Y+0WokkulQujZ7K3SdWUSHIysgMrWiect + Wdg5unxN1A/aeyvhgvYSNPjU9KBco7SDnigSs9InW/QghJFrZRrDhTp1oTUc + qK5lKvaseHkVGi91vPWeLQxZt1loJB5zL6j5BxMbvRfJK+wc3ax2u4x8WTAB + EurCwzBMBgkqhkiG9w0BBwEwHQYJYIZIAWUDBAEqBBAwcy7jvcOGcMfLEtug + LEXbgCBkocdckuDe14mVGmUmM++xN34OEVRCeGVWWUnWq1DJ4Q==] + - &stringStyle ENC[PKCS7,MIIBiQYJKoZIhvcNAQcDoIIBejCCAXYCAQAxggEhMIIBHQIBADAFMAACAQEwDQYJKoZIhvcNAQEBBQAEggEAIu44u62q5sVfzC7kytLi2Z/EzH2DKr4vDsoqDBeSZ71aRku/uSrjyiO4lyoq9Kva+eBAyjBay5fnqPVBaU3Rud2pdEoZEoyofi02jn4hxUKpAO1W0AUgsQolGe53qOdM4U8RbwnTR0gr3gp2mCd18pH3SRMP9ryrsBAxGzJ6mR3RgdZnlTlqVGXCeWUeVpbH+lcHw3uvd+o/xkvJ/3ypxz+rWILiAZ3QlCirzn/qb2fHuKf3VBh8RVFuQDaM5voajZlgjD6KzNCsbATOqOA6eJI4j0ngPdDlIjGHAnahuyluQ5f5SIaIjLC+ZeCOfIYni0MQ+BHO0JNbccjq2Unb7TBMBgkqhkiG9w0BBwEwHQYJYIZIAWUDBAEqBBCYmAI0Ao3Ok1cSmVw0SgQGgCBK62z1r5RfRjf1xKfqDxTsGUHfsUmM3EjGJfnWzCRvuQ==] + block: *blockStyle + string: *stringStyle + """ + simple_file = create_temp_yaml_file(tmp_path_factory, simple_content) + anchored_file = create_temp_yaml_file(tmp_path_factory, anchored_content) + result = script_runner.run( self.command, "--newprivatekey={}".format(new_eyaml_keys[0]), "--newpublickey={}".format(new_eyaml_keys[1]), "--oldprivatekey={}".format(old_eyaml_keys[0]), "--oldpublickey={}".format(old_eyaml_keys[1]), - yaml_file + simple_file, + anchored_file ) assert result.success, result.stderr - with open(yaml_file, 'r') as fhnd: - filedat = fhnd.read() - assert not filedat == content + with open(simple_file, 'r') as fhnd: + simple_data = fhnd.read() + + with open(anchored_file, 'r') as fhnd: + anchored_data = fhnd.read() + + assert not simple_data == simple_content + assert not anchored_data == anchored_content + + # FIXME: Verify that block and string formatting is correct def test_yaml_parsing_error(self, script_runner, imparsible_yaml_file, old_eyaml_keys, new_eyaml_keys): result = script_runner.run( @@ -122,3 +149,116 @@ def test_yaml_composition_error(self, script_runner, badcmp_yaml_file, old_eyaml ) assert not result.success, result.stderr assert "YAML composition error" in result.stderr + + def test_corrupted_eyaml_value(self, script_runner, tmp_path_factory, old_eyaml_keys, new_eyaml_keys): + content = """--- + key: > + ENC[PKCS7,MII ... corrupted-value ... + DBAEqBBAwcy7jvcOGcMfLEtugGVWWUnWq1DJ4Q==] + """ + yaml_file = create_temp_yaml_file(tmp_path_factory, content) + result = script_runner.run( + self.command, + "--newprivatekey={}".format(new_eyaml_keys[0]), + "--newpublickey={}".format(new_eyaml_keys[1]), + "--oldprivatekey={}".format(old_eyaml_keys[0]), + "--oldpublickey={}".format(old_eyaml_keys[1]), + yaml_file + ) + assert not result.success, result.stderr + assert "Unable to decrypt value!" in result.stderr + + def test_bad_recryption_key(self, script_runner, tmp_path_factory, old_eyaml_keys, new_eyaml_keys): + content = """--- + key: > + ENC[PKCS7,MIIBmQYJKoZIhvcNAQcDoIIBijCCAYYCAQAxggEhMIIBHQIBADAFMAACAQEw + DQYJKoZIhvcNAQEBBQAEggEAs+8byhxSVFkzPAfGFazxsifEJQO3RH5MNf2g + o/x0oh+y1SB6bwB/lPtCBnCwDgKUKR8VzqWM8sTYkLTkSWq5BxS+Hix0zL1u + zqdzNbuFDNS3PoUM4XaBRPOhGL/xUGc8EuUmdc3RaGRqisZvqACAMDDMme5m + sCJVHw/QC//hAH6zrPmPA8D5S6ibMHGURifqTmLvi1BxxzMIWXWBmRpadAaq + nYqhYsI/IWyQBmF7OAwsREREu+qEiDDBOS5IchDcDnlxtoooB5xin4HDS9ED + MJMlKfpB1FCNtrC4RJz4uqFuwvX482cct3TtS+/UrPLP7rm6EILs7QSQGsdM + G+8k8DBcBgkqhkiG9w0BBwEwHQYJYIZIAWUDBAEqBBD+Bx88oA/j57i5UB5U + BHEogDDpFaKbtiGSTxOK44MpjLOGCZ4ME6lJz5EYVJQ3VJw95z98mvj6CgzL + NI/TSIF7M9U=] + """ + yaml_file = create_temp_yaml_file(tmp_path_factory, content) + result = script_runner.run( + self.command, + "--newprivatekey={}".format(new_eyaml_keys[1]), + "--newpublickey={}".format(new_eyaml_keys[0]), + "--oldprivatekey={}".format(old_eyaml_keys[0]), + "--oldpublickey={}".format(old_eyaml_keys[1]), + yaml_file + ) + assert not result.success, result.stderr + assert "unable to encrypt" in result.stderr + + def test_backup_file(self, script_runner, tmp_path_factory, old_eyaml_keys, new_eyaml_keys): + import os + + content = """--- + key: > + ENC[PKCS7,MIIBiQYJKoZIhvcNAQcDoIIBejCCAXYCAQAxggEhMIIBHQIBADAFMAACAQEw + DQYJKoZIhvcNAQEBBQAEggEAPGA1g1Wx50RK8F/Y118w1VT/SnCa7PMfN2OM + d82vGeWXm6INmoURMDWEvBUEFCmGZoOMLVlK3LALtUcPEW1N9ztJTypBrqqI + 1K8L9aZWRNFt7uwsaoHWvk1XjMujP+nn2ZO3OiFYkiWFh0PcFw7cT1TmexB4 + cNbBtNi7oJ88L17/8rbtJW465cWyj0pPCmwo3OvK39JcuJ2xosujNk4u5AUf + TjWwklk3yjPvjG6AvoS4TK+vkmqUcCkyy0tLZR8Xu+3IzYCq+DYH4QBrrrZf + pKer9VawzMzxgVXeCgKGEsa3XeSzWtgbyoZVtoBdl3uv2f8rGi5qAlwZ9syO + Aold9zBMBgkqhkiG9w0BBwEwHQYJYIZIAWUDBAEqBBDGUmDGJfp2Iqn7bATf + r0H9gCBNamGg9iiM92wGcVSkNmGJtVk8yEe3EOVn/QNzQ6v0fw==] + """ + yaml_file = create_temp_yaml_file(tmp_path_factory, content) + backup_file = yaml_file + ".bak" + result = script_runner.run( + self.command, + "--newprivatekey={}".format(new_eyaml_keys[0]), + "--newpublickey={}".format(new_eyaml_keys[1]), + "--oldprivatekey={}".format(old_eyaml_keys[0]), + "--oldpublickey={}".format(old_eyaml_keys[1]), + "--backup", + yaml_file + ) + assert result.success, result.stderr + assert os.path.isfile(backup_file) + + with open(backup_file, 'r') as fhnd: + filedat = fhnd.read() + assert filedat == content + + def test_replace_backup_file(self, script_runner, tmp_path_factory, old_eyaml_keys, new_eyaml_keys): + import os + + content = """--- + key: > + ENC[PKCS7,MIIBiQYJKoZIhvcNAQcDoIIBejCCAXYCAQAxggEhMIIBHQIBADAFMAACAQEw + DQYJKoZIhvcNAQEBBQAEggEAPGA1g1Wx50RK8F/Y118w1VT/SnCa7PMfN2OM + d82vGeWXm6INmoURMDWEvBUEFCmGZoOMLVlK3LALtUcPEW1N9ztJTypBrqqI + 1K8L9aZWRNFt7uwsaoHWvk1XjMujP+nn2ZO3OiFYkiWFh0PcFw7cT1TmexB4 + cNbBtNi7oJ88L17/8rbtJW465cWyj0pPCmwo3OvK39JcuJ2xosujNk4u5AUf + TjWwklk3yjPvjG6AvoS4TK+vkmqUcCkyy0tLZR8Xu+3IzYCq+DYH4QBrrrZf + pKer9VawzMzxgVXeCgKGEsa3XeSzWtgbyoZVtoBdl3uv2f8rGi5qAlwZ9syO + Aold9zBMBgkqhkiG9w0BBwEwHQYJYIZIAWUDBAEqBBDGUmDGJfp2Iqn7bATf + r0H9gCBNamGg9iiM92wGcVSkNmGJtVk8yEe3EOVn/QNzQ6v0fw==] + """ + yaml_file = create_temp_yaml_file(tmp_path_factory, content) + backup_file = yaml_file + ".bak" + with open(backup_file, 'w') as fhnd: + fhnd.write(content + "\nkey2: plain scalar string value") + + result = script_runner.run( + self.command, + "--newprivatekey={}".format(new_eyaml_keys[0]), + "--newpublickey={}".format(new_eyaml_keys[1]), + "--oldprivatekey={}".format(old_eyaml_keys[0]), + "--oldpublickey={}".format(old_eyaml_keys[1]), + "--backup", + yaml_file + ) + assert result.success, result.stderr + assert os.path.isfile(backup_file) + + with open(backup_file, 'r') as fhnd: + filedat = fhnd.read() + assert filedat == content diff --git a/yamlpath/commands/eyaml_rotate_keys.py b/yamlpath/commands/eyaml_rotate_keys.py index 488f16b7..9edd6d56 100644 --- a/yamlpath/commands/eyaml_rotate_keys.py +++ b/yamlpath/commands/eyaml_rotate_keys.py @@ -172,10 +172,7 @@ def main(): try: txtval = processor.decrypt_eyaml(node) except EYAMLCommandException as ex: - log.critical(ex, 2) - - if txtval is None: - # A warning about this failure has already been printed + log.error(ex) exit_state = 3 continue @@ -192,7 +189,9 @@ def main(): try: processor.set_eyaml_value(yaml_path, txtval, output=output) except EYAMLCommandException as ex: - log.critical(ex, 2) + log.error(ex) + exit_state = 3 + continue file_changed = True From 84f8517046214d6136bf8f408c7c03c35189c2de Mon Sep 17 00:00:00 2001 From: "William W. Kimball" Date: Thu, 30 May 2019 15:26:34 -0500 Subject: [PATCH 14/16] WIP: Some pylint cleanup --- .pylintrc | 11 +++++++---- tests/test_func.py | 4 ++++ yamlpath/commands/eyaml_rotate_keys.py | 10 +++------- yamlpath/commands/yaml_get.py | 13 ++++--------- yamlpath/commands/yaml_set.py | 18 +++++++----------- yamlpath/func.py | 18 ++++++++++++++++++ 6 files changed, 43 insertions(+), 31 deletions(-) diff --git a/.pylintrc b/.pylintrc index 048884ee..aa50ed50 100644 --- a/.pylintrc +++ b/.pylintrc @@ -62,10 +62,13 @@ confidence= # --disable=W". # WWK-2019-05-24: Disable R0401 because I cannot get pylint to throw it on my -# OSX box yet Travis throws it on every run. Until OSX and Travis can agree on -# when an R0401 occurs, I cannot troubleshoot what otherwise appear to be -# false-positives. -disable=R0401 +# OSX box yet Travis throws it on every run. Until OSX and Travis can agree +# on when an R0401 occurs, I cannot troubleshoot what otherwise appear to be +# false-positives. +# WWK-2019-05-30: Disable R0801 because similar command-line tools necessarily +# share duplicate code. This is by design so each can stand-alone. +disable=R0401, + R0801 # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option diff --git a/tests/test_func.py b/tests/test_func.py index b7a1f5b3..5ada9c85 100644 --- a/tests/test_func.py +++ b/tests/test_func.py @@ -14,12 +14,16 @@ append_list_element, build_next_node, clone_node, + get_yaml_editor, make_new_node, wrap_type, ) class Test_func(): + def test_get_yaml_editor(self): + assert get_yaml_editor() + def test_anchorless_list_element_error(self): with pytest.raises(ValueError) as ex: append_list_element({}, YAMLPath("foo"), "bar") diff --git a/yamlpath/commands/eyaml_rotate_keys.py b/yamlpath/commands/eyaml_rotate_keys.py index 9edd6d56..b07a53a9 100644 --- a/yamlpath/commands/eyaml_rotate_keys.py +++ b/yamlpath/commands/eyaml_rotate_keys.py @@ -4,13 +4,11 @@ Copyright 2018, 2019 William W. Kimball, Jr. MBA MSIS """ -import sys import argparse from shutil import copy2 from os import remove, access, R_OK from os.path import isfile, exists -from ruamel.yaml import YAML from ruamel.yaml.parser import ParserError from ruamel.yaml.composer import ComposerError from ruamel.yaml.scanner import ScannerError @@ -22,6 +20,7 @@ # pylint: disable=locally-disabled,unused-import import yamlpath.patches from yamlpath.wrappers import ConsolePrinter +from yamlpath.func import get_yaml_editor # Implied Constants MY_VERSION = "1.0.2" @@ -94,6 +93,7 @@ def validateargs(args, log): if has_errors: exit(1) +# pylint: disable=locally-disabled,too-many-locals,too-many-branches,too-many-statements def main(): """Main code.""" # Process any command-line arguments @@ -103,11 +103,7 @@ def main(): processor = EYAMLProcessor(log, None, binary=args.eyaml) # Prep the YAML parser - yaml = YAML() - yaml.indent(mapping=2, sequence=4, offset=2) - yaml.explicit_start = True - yaml.preserve_quotes = True - yaml.width = sys.maxsize + yaml = get_yaml_editor() # Process the input file(s) in_file_count = len(args.yaml_files) diff --git a/yamlpath/commands/yaml_get.py b/yamlpath/commands/yaml_get.py index 510f9de7..93895fc3 100644 --- a/yamlpath/commands/yaml_get.py +++ b/yamlpath/commands/yaml_get.py @@ -6,13 +6,11 @@ Copyright 2018, 2019 William W. Kimball, Jr. MBA MSIS """ -import sys import argparse import json from os import access, R_OK from os.path import isfile -from ruamel.yaml import YAML from ruamel.yaml.parser import ParserError from ruamel.yaml.composer import ComposerError from ruamel.yaml.scanner import ScannerError @@ -24,6 +22,7 @@ from yamlpath.eyaml import EYAMLProcessor from yamlpath.wrappers import ConsolePrinter +from yamlpath.func import get_yaml_editor # Implied Constants MY_VERSION = "1.0.4" @@ -132,17 +131,13 @@ def main(): yaml_path = YAMLPath(args.query, pathsep=args.pathsep) # Prep the YAML parser - yaml = YAML() - yaml.indent(mapping=2, sequence=4, offset=2) - yaml.explicit_start = True - yaml.preserve_quotes = True - yaml.width = sys.maxsize + yaml = get_yaml_editor() # Attempt to open the YAML file; check for parsing errors try: - with open(args.yaml_file, 'r') as f: - yaml_data = yaml.load(f) + with open(args.yaml_file, 'r') as fhnd: + yaml_data = yaml.load(fhnd) except FileNotFoundError: log.critical("YAML_FILE not found: {}".format(args.yaml_file), 2) except ParserError as ex: diff --git a/yamlpath/commands/yaml_set.py b/yamlpath/commands/yaml_set.py index bd316a3c..bb842ba0 100644 --- a/yamlpath/commands/yaml_set.py +++ b/yamlpath/commands/yaml_set.py @@ -15,7 +15,6 @@ from os.path import isfile, exists from shutil import copy2 -from ruamel.yaml import YAML from ruamel.yaml.parser import ParserError from ruamel.yaml.composer import ComposerError from ruamel.yaml.scanner import ScannerError @@ -29,7 +28,7 @@ # pylint: disable=locally-disabled,unused-import import yamlpath.patches -from yamlpath.func import clone_node +from yamlpath.func import get_yaml_editor, clone_node from yamlpath.wrappers import ConsolePrinter # Implied Constants @@ -176,6 +175,7 @@ def validateargs(args, log): if has_errors: exit(1) +# pylint: disable=locally-disabled,too-many-locals,too-many-branches,too-many-statements def main(): """Main code.""" args = processcli() @@ -190,8 +190,8 @@ def main(): elif args.stdin: new_value = ''.join(sys.stdin.readlines()) elif args.file: - with open(args.file, 'r') as f: - new_value = f.read().rstrip() + with open(args.file, 'r') as fhnd: + new_value = fhnd.read().rstrip() elif args.random is not None: new_value = ''.join( secrets.choice( @@ -200,16 +200,12 @@ def main(): ) # Prep the YAML parser - yaml = YAML() - yaml.indent(mapping=2, sequence=4, offset=2) - yaml.explicit_start = True - yaml.preserve_quotes = True - yaml.width = sys.maxsize + yaml = get_yaml_editor() # Attempt to open the YAML file; check for parsing errors try: - with open(args.yaml_file, 'r') as f: - yaml_data = yaml.load(f) + with open(args.yaml_file, 'r') as fhnd: + yaml_data = yaml.load(fhnd) except FileNotFoundError: log.critical("YAML_FILE not found: {}".format(args.yaml_file), 2) except ParserError as ex: diff --git a/yamlpath/func.py b/yamlpath/func.py index 1346d5df..30a94234 100644 --- a/yamlpath/func.py +++ b/yamlpath/func.py @@ -3,9 +3,11 @@ Copyright 2018, 2019 William W. Kimball, Jr. MBA MSIS """ +from sys import maxsize from distutils.util import strtobool from typing import Any +from ruamel.yaml import YAML from ruamel.yaml.comments import CommentedSeq, CommentedMap from ruamel.yaml.scalarstring import ( PlainScalarString, @@ -24,6 +26,22 @@ PathSegmentTypes, ) +def get_yaml_editor() -> object: + """ + Builds and returns a generic YAML editor based on ruamel.yaml. + + Parameters: N/A + + Returns (object) The ready-for-use YAML editor. + + Raises: N/A + """ + yaml = YAML() + yaml.indent(mapping=2, sequence=4, offset=2) + yaml.explicit_start = True + yaml.preserve_quotes = True + yaml.width = maxsize + return yaml def build_next_node(yaml_path: YAMLPath, depth: int, value: Any = None) -> Any: From 8fddee09f49d7d8fd0e3980d55939c64db3f451e Mon Sep 17 00:00:00 2001 From: "William W. Kimball" Date: Thu, 30 May 2019 15:30:27 -0500 Subject: [PATCH 15/16] TravisCI update --- .travis.yml | 2 +- yamlpath/func.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 1472c227..e3d28400 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,7 +7,7 @@ python: - 3.6 install: - - pip install --upgrade ruamel.yaml mypy pytest pytest-cov pylint coveralls + - pip install --upgrade ruamel.yaml mypy pytest pytest-cov pytest-console-scripts pylint coveralls - gem install hiera-eyaml script: diff --git a/yamlpath/func.py b/yamlpath/func.py index 30a94234..228c181f 100644 --- a/yamlpath/func.py +++ b/yamlpath/func.py @@ -26,13 +26,13 @@ PathSegmentTypes, ) -def get_yaml_editor() -> object: +def get_yaml_editor() -> Any: """ Builds and returns a generic YAML editor based on ruamel.yaml. Parameters: N/A - Returns (object) The ready-for-use YAML editor. + Returns (Any) The ready-for-use YAML editor. Raises: N/A """ From b2871a0530535f115dbe7d0b82da6f9415fc9b42 Mon Sep 17 00:00:00 2001 From: "William W. Kimball" Date: Thu, 30 May 2019 15:45:02 -0500 Subject: [PATCH 16/16] Prep release 2.0.2 --- CHANGES | 12 ++++++++++++ setup.py | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index d74ed424..b4e23aa0 100644 --- a/CHANGES +++ b/CHANGES @@ -1,3 +1,15 @@ +2.0.2: +Bug Fixes: +* eyaml-rotate-keys was broken by the refactoring for 2.0.0. eyaml-rotate-keys + v1.0.2 restores functionality. + +Enhancements: +* Command-line tools are now managed via pip as entry_points/console_scripts + rather than external binaries. This enables superior cross-platform + compatibility as well as unit testing. As such, all of the CLI tools have + been updated pursuant to (generally trivial, excepting eyaml-rotate-keys) + issues discovered during their newfound CI tests. + 2.0.1: Bug Fixes: * yaml-set v1.0.4 lost track of EYAML block formatting between read and write, diff --git a/setup.py b/setup.py index ad4d1c79..1cdae3c7 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name="yamlpath", - version="2.0.1", + version="2.0.2", description="Read and change YAML/Compatible data using powerful, intuitive, command-line friendly syntax", long_description=long_description, long_description_content_type="text/markdown",