Skip to content

Commit

Permalink
Merge pull request #7 from wwkimball/development
Browse files Browse the repository at this point in the history
Release 1.1.0
  • Loading branch information
wwkimball authored May 9, 2019
2 parents 6f8afcb + ef97559 commit d1c27c3
Show file tree
Hide file tree
Showing 10 changed files with 377 additions and 159 deletions.
5 changes: 5 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
1.1.0
Added support for Regular Expressions as a search mechanism against Hash keys
and values. Also increased unit test coverage and fixed some bugs in that
effort.

1.0.0
Initial release. All features are belived to be working as expected, backed by
comprehensive unit tests and my own in-production use of the bundled
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,11 +106,12 @@ YAML Path understands these forms:
* Greater Than match: `sensitive::accounts.application.db.users[access_level>0].pass`
* Less Than or Equal match: `sensitive::accounts.application.db.users[access_level<=100].pass`
* Greater Than or Equal match: `sensitive::accounts.application.db.users[access_level>=0].pass`
* Regular Expression matches using any delimiter you choose (other than `/`, if you need something else): `sensitive::accounts.application.db.users[access_level=~/^\D+$/].pass`
* Invert any match with `!`, like: `sensitive::accounts.application.db.users[name!=admin].pass`
* Demarcate and/or escape expression values, like: `sensitive::accounts.application.db.users[full\ name="Some User\'s Name"].pass`
* Multi-level matching: `sensitive::accounts.application.db.users[name%admin].pass[encrypted!^ENC\[]`
* Hash key-name searches using `.`, yielding their values, not the keys themselves: `sensitive::accounts.database[.^app_]`
* Complex combinations: `[2].some::deep.hierarchy[with!=""].'any.valid'[.$yaml][data%structure].complexity`
* Hash key-name searches with all search methods above by using `.` (yields their values, not the keys themselves): `sensitive::accounts.database[.^app_]`
* Complex combinations: `some::deep.hierarchy[with!=""].'any.valid'[.$yaml][data%structure].or.[!complexity=~/^.{4}$/][2]`

## Installing

Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

setuptools.setup(
name="yamlpath",
version="1.0.0",
version="1.1.0",
description="Generally-useful YAML and EYAML tools employing a human-friendly YAML Path",
long_description=long_description,
long_description_content_type="text/markdown",
Expand Down
114 changes: 91 additions & 23 deletions tests/test_eyamlpath.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import pytest

from subprocess import run
from types import SimpleNamespace
from subprocess import run, CalledProcessError

from ruamel.yaml import YAML

from yamlpath.eyaml import EYAMLPath
from yamlpath.wrappers import ConsolePrinter
from yamlpath.exceptions import EYAMLCommandException

requireseyaml = pytest.mark.skipif(
not EYAMLPath.get_eyaml_executable("eyaml")
Expand All @@ -16,7 +17,7 @@
)

@pytest.fixture
def eyamlpath():
def eyamlpath_f():
"""Returns an EYAMLPath with a quiet logger."""
args = SimpleNamespace(verbose=False, quiet=True, debug=False)
logger = ConsolePrinter(args)
Expand Down Expand Up @@ -56,6 +57,19 @@ def eyamldata():
aliased_values:
ident: *secretIdentity
phrase: *secretPhrase
array_of_array_idents:
-
- >
ENC[PKCS7,MIIBmQYJKoZIhvcNAQcDoIIBijCCAYYCAQAxggEhMIIBHQIBADAFMAACAQEw
DQYJKoZIhvcNAQEBBQAEggEAxum+Uyt3ETjkaQ9C5PqnpCUVCU6wrUYuVBk+
PV7t7hayWGrG+dixzUUP9HKbIh6kbVYIwGCpEhMOmJQZ8TLiu/ye+KQzX/CE
wz4uk7qvv/OvsFiMqmApxcvzNl2Qq7unCScXfngZKPjv4BxAFI1axzsUmxLx
ChOUSkLMkuIJ5myAw43Sfan9Yx3lk96IoN97gN74ZzXTRGjl3n0zxrHy3obT
M12f+MZqHuaTnuvksakk32nQ7jGX82QqxX3HChEkzUkKXG5ceS/cFzSTj9QG
xbYrUXDNq/uviShfVk6tUey76VJAguLlw1ONqRkjonjAz7iR+YIu4RzvPvyJ
Grz/ezBcBgkqhkiG9w0BBwEwHQYJYIZIAWUDBAEqBBDcP+GwmSrNro9UALHI
FoRFgDCwH91AbH9DOpDMj3HAOlxD2JzAkpy4X9SOZVn6Vht2do38Y1Z02Ccf
pHj/ObATQ9M=]
aliased::secrets:
<<: *anchoredSecrets
Expand Down Expand Up @@ -166,65 +180,119 @@ def eyamlkeys(tmp_path_factory):
new_private_key_file, new_public_key_file
)

def test_find_eyaml_paths(eyamlpath, eyamldata):
def test_find_eyaml_paths(eyamlpath_f, eyamldata):
expected = [
"aliases[&secretIdentity]",
"aliases[&secretPhrase]",
"anchored::secrets.aliased_values.ident",
"anchored::secrets.aliased_values.phrase",
"anchored::secrets.array_of_array_idents[0][0]",
"aliased::secrets.novel_values.ident",
"aliased::secrets.novel_values.phrase",
"aliased::secrets.string_values.ident",
"aliased::secrets.string_values.phrase",
]
actual = []
for node in eyamlpath.find_eyaml_paths(eyamldata):
for node in eyamlpath_f.find_eyaml_paths(eyamldata):
actual.append(node)

assert actual == expected

@pytest.fixture
def force_subprocess_run_cpe(monkeypatch):
import yamlpath.eyaml.eyamlpath as break_module

def fake_run(*args, **kwargs):
raise CalledProcessError(42, "bad eyaml")

monkeypatch.setattr(break_module, "run", fake_run)

@requireseyaml
@pytest.mark.parametrize("search,compare", [
("aliases[&secretIdentity]", "This is not the identity you are looking for."),
("aliases[&secretPhrase]", "There is no secret phrase."),
])
def test_happy_get_eyaml_values(eyamlpath, eyamldata, eyamlkeys, search, compare):
eyamlpath.privatekey = eyamlkeys[0]
eyamlpath.publickey = eyamlkeys[1]
for node in eyamlpath.get_eyaml_values(eyamldata, search, mustexist=True):
eyamlpath.log.warning(node)
def test_happy_get_eyaml_values(eyamlpath_f, eyamldata, eyamlkeys, search, compare):
eyamlpath_f.privatekey = eyamlkeys[0]
eyamlpath_f.publickey = eyamlkeys[1]
for node in eyamlpath_f.get_eyaml_values(eyamldata, search, mustexist=True):
eyamlpath_f.log.warning(node)
assert node == compare

@requireseyaml
@pytest.mark.parametrize("search,compare,mustexist,output", [
("aliases[&secretIdentity]", "This is your new identity.", True, "string"),
("aliases[&brandNewEntry]", "This key doesn't already exist.", False, "block"),
])
def test_happy_set_eyaml_value(eyamlpath, eyamldata, eyamlkeys, search, compare, mustexist, output):
def test_happy_set_eyaml_value(eyamlpath_f, eyamldata, eyamlkeys, search, compare, mustexist, output):
# Assign the asymetric keys
eyamlpath.privatekey = eyamlkeys[0]
eyamlpath.publickey = eyamlkeys[1]
eyamlpath_f.privatekey = eyamlkeys[0]
eyamlpath_f.publickey = eyamlkeys[1]

# Set the test value
eyamlpath.set_eyaml_value(eyamldata, search, compare, mustexist=mustexist, output=output)
eyamlpath_f.set_eyaml_value(eyamldata, search, compare, mustexist=mustexist, output=output)

# Ensure the new value is encrypted
for encnode in eyamlpath.get_nodes(eyamldata, search):
for encnode in eyamlpath_f.get_nodes(eyamldata, search):
assert EYAMLPath.is_eyaml_value(encnode)

# Ensure the new value decrypts back to the original value
for decnode in eyamlpath.get_eyaml_values(eyamldata, search, mustexist=True):
for decnode in eyamlpath_f.get_eyaml_values(eyamldata, search, mustexist=True):
assert decnode == compare

def test_none_eyaml_value():
assert False == EYAMLPath.is_eyaml_value(None)

@pytest.mark.parametrize("exe", [
("/no/such/file/anywhere"),
("this-file-does-not-exist"),
(None),
])
def test_impossible_eyaml_exe(exe):
assert None == EYAMLPath.get_eyaml_executable(exe)

def test_not_can_run_eyaml(eyamlpath_f):
eyamlpath_f.eyaml = None
assert False == eyamlpath_f._can_run_eyaml()

@requireseyaml
def test_impossible_eyaml_exe(eyamlpath, eyamlkeys):
assert None == EYAMLPath.get_eyaml_executable("/no/such/file/anywhere")
assert None == EYAMLPath.get_eyaml_executable("this-file-does-not-exist")
assert None == EYAMLPath.get_eyaml_executable(eyamlkeys[0])
assert None == EYAMLPath.get_eyaml_executable(None)

eyamlpath.eyaml = None
assert False == eyamlpath._can_run_eyaml()
def test_bad_encryption_keys(eyamlpath_f):
eyamlpath_f.privatekey = "/no/such/file"
eyamlpath_f.publickey = "/no/such/file"

with pytest.raises(EYAMLCommandException):
eyamlpath_f.encrypt_eyaml("test")

def test_no_encrypt_without_eyaml(eyamlpath_f):
eyamlpath_f.eyaml = None
with pytest.raises(EYAMLCommandException):
eyamlpath_f.encrypt_eyaml("test")

def test_no_decrypt_without_eyaml(eyamlpath_f):
eyamlpath_f.eyaml = None
with pytest.raises(EYAMLCommandException):
eyamlpath_f.decrypt_eyaml("ENC[...]")

def test_ignore_already_encrypted_cryps(eyamlpath_f):
testval = "ENC[...]"
assert testval == eyamlpath_f.encrypt_eyaml(testval)

def test_ignore_already_decrypted_cryps(eyamlpath_f):
testval = "some value"
assert testval == eyamlpath_f.decrypt_eyaml(testval)

@requireseyaml
def test_impossible_decryption(eyamlpath_f, eyamlkeys):
testval = "ENC[...]"
with pytest.raises(EYAMLCommandException):
eyamlpath_f.decrypt_eyaml(testval)

def test_encrypt_calledprocesserror(eyamlpath_f, force_subprocess_run_cpe):
with pytest.raises(EYAMLCommandException):
eyamlpath_f.encrypt_eyaml("any value")

def test_decrypt_calledprocesserror(eyamlpath_f, force_subprocess_run_cpe):
with pytest.raises(EYAMLCommandException):
eyamlpath_f.decrypt_eyaml("ENC[...]")

# 60, 67, 98, 101, 123-128, 138-143, 163, 190-191
69 changes: 41 additions & 28 deletions tests/test_parser.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import pytest

from types import SimpleNamespace
from collections import deque

from yamlpath.parser import Parser
from yamlpath.exceptions import YAMLPathException
Expand Down Expand Up @@ -61,38 +62,50 @@ def test_empty_str_path(parser):
('some[!search < 42]', "some[search!<42]"),
('some[!search >= 5280]', "some[search!>=5280]"),
('some[!search <= 14000]', "some[search!<=14000]"),
(r'some[search =~ /^\d{5}$/]', r'some[search=~/^\d{5}$/]'),
])
def test_happy_str_path_translations(parser, yaml_path, stringified):
assert parser.str_path(yaml_path) == stringified

def test_happy_parse_path_list_to_deque(parser):
assert isinstance(parser.parse_path(["item1", "item2"]), deque)

# Unhappy searches
@pytest.mark.parametrize("yaml_path,stringified", [
('some[search ^^ "Name "]', r"some[search^Name\ ]"),
('some[search $$ " Here"]', r"some[search$\ Here]"),
(r'some[search %% "e H"]', r"some[search%e\ H]"),
('some[search >> 50]', "some[search>50]"),
('some[search << 42]', "some[search<42]"),
('some[search >>== 5280]', "some[search>=5280]"),
('some[search <<== 14000]', "some[search<=14000]"),
('some[search !!= "Name Here"]', r"some[search!=Name\ Here]"),
('some[search !!== "Name Here"]', r"some[search!=Name\ Here]"),
('some[search !!^ "Name "]', r"some[search!^Name\ ]"),
('some[search !!$ " Here"]', r"some[search!$\ Here]"),
('some[search !!% "e H"]', r"some[search!%e\ H]"),
('some[search !!> 50]', "some[search!>50]"),
('some[search !!< 42]', "some[search!<42]"),
('some[search !!>= 5280]', "some[search!>=5280]"),
('some[search !!<= 14000]', "some[search!<=14000]"),
('some[!search != "Name Here"]', r"some[search!=Name\ Here]"),
('some[!search !== "Name Here"]', r"some[search!=Name\ Here]"),
('some[!search !^ "Name "]', r"some[search!^Name\ ]"),
('some[!search !$ " Here"]', r"some[search!$\ Here]"),
('some[!search !% "e H"]', r"some[search!%e\ H]"),
('some[!search !> 50]', "some[search!>50]"),
('some[!search !< 42]', "some[search!<42]"),
('some[!search !>= 5280]', "some[search!>=5280]"),
('some[!search !<= 14000]', "some[search!<=14000]"),
@pytest.mark.parametrize("yaml_path", [
('some[search ^^ "Name "]'),
('some[search $$ " Here"]'),
(r'some[search %% "e H"]'),
('some[search >> 50]'),
('some[search << 42]'),
('some[search >>== 5280]'),
('some[search <<== 14000]'),
('some[search !!= "Name Here"]'),
('some[search !!== "Name Here"]'),
('some[search !!^ "Name "]'),
('some[search !!$ " Here"]'),
('some[search !!% "e H"]'),
('some[search !!> 50]'),
('some[search !!< 42]'),
('some[search !!>= 5280]'),
('some[search !!<= 14000]'),
('some[!search != "Name Here"]'),
('some[!search !== "Name Here"]'),
('some[!search !^ "Name "]'),
('some[!search !$ " Here"]'),
('some[!search !% "e H"]'),
('some[!search !> 50]'),
('some[!search !< 42]'),
('some[!search !>= 5280]'),
('some[!search !<= 14000]'),
('some[= "missing LHS operand"]'),
('some[search ~ "meaningless tilde"]'),
('some[search = "unterminated demarcation]'),
('some[search =~ /unterminated RegEx]'),
('some[search ^= "meaningless operator"]'),
({}),
])
def test_uphappy_str_path_translations(parser, yaml_path, stringified):
def test_uphappy_str_path_translations(parser, yaml_path):
with pytest.raises(YAMLPathException):
parser.str_path(yaml_path) == stringified
parser.str_path(yaml_path)

# 105, 107
Loading

0 comments on commit d1c27c3

Please sign in to comment.