From 6f14bd7db16fa61edd9f96e210c9c539ca0f1c42 Mon Sep 17 00:00:00 2001 From: Piotr Dec Date: Thu, 11 Jun 2026 14:48:34 +0200 Subject: [PATCH 1/2] test: env variant --- tests/files/test1/test.mo.env | 7 ++++ tests/test_mo_env.py | 64 +++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 tests/files/test1/test.mo.env create mode 100644 tests/test_mo_env.py diff --git a/tests/files/test1/test.mo.env b/tests/files/test1/test.mo.env new file mode 100644 index 0000000..e3b4cb0 --- /dev/null +++ b/tests/files/test1/test.mo.env @@ -0,0 +1,7 @@ +value=${sample} +nested=${some/nested/value} +mixed=${some/nested/value.username} +custom=${custom.field} + +uname=${sample.username} +invalid=${double.dot.example} diff --git a/tests/test_mo_env.py b/tests/test_mo_env.py new file mode 100644 index 0000000..bbbf0da --- /dev/null +++ b/tests/test_mo_env.py @@ -0,0 +1,64 @@ +from pathlib import Path + +import pytest +import yaml + +from karl.services import Passwords +from karl.services.mo import Mo + + +@pytest.fixture(scope='class') +def target_path(): + # p = Path('tests/files/test1/test.env') + p = Path('files/test1/test.env') + # posprzątaj przed testem, gdyby plik istniał z poprzednich uruchomień + if p.exists(): + p.unlink() + yield p + # sprzątanie po teście + if p.exists(): + p.unlink() + + +@pytest.fixture(scope='class') +def test1_content(target_path: Path): + mo = Mo(Passwords()) + mo.process(Path('files/test1/test.mo.env').absolute()) + # mo.process(Path('tests/files/test1/test.mo.env').absolute()) + + assert target_path.absolute().exists() + + content = target_path.read_text() + assert '${' not in content + + + props = {} + prop_lines = content.split('\n') + for line in prop_lines: + if len(line)> 1: + splitted = line.split('=') + props[splitted[0]] = splitted[1] + # props = {prop[0]:prop[1] for prop in content.split('\n')} + print(props) + yield props + + +class TestEnvParsing: + + def test_simple(self, test1_content: dict): + assert test1_content['value'] == 'some_pass' + + def test_nested(self, test1_content: dict): + assert test1_content['nested'] == 'nested_pass' + + def test_mixed(self, test1_content: dict): + assert test1_content['mixed'] == 'nested_username' + + def test_custom_field(self, test1_content: dict): + assert test1_content['custom'] == 'custom_content' + + def test_username_field(self, test1_content: dict): + assert test1_content['uname'] == 'sample_username' + + def test_invalid_key(self, test1_content: dict): + assert test1_content.get('invalid') == 'None' From 3ec1942b4e4b4b385b285c7c470230fbb1bbe247 Mon Sep 17 00:00:00 2001 From: Piotr Dec Date: Thu, 11 Jun 2026 14:58:17 +0200 Subject: [PATCH 2/2] chore: passwords logging --- src/karl/services/passwords.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/karl/services/passwords.py b/src/karl/services/passwords.py index bf2cda0..9f662b0 100644 --- a/src/karl/services/passwords.py +++ b/src/karl/services/passwords.py @@ -6,6 +6,10 @@ import keyring from injectable import injectable from pykeepass import PyKeePass, create_database +import logging + +logger = logging.getLogger(__name__) + class KeyRequest: def __init__(self, prompt: str): self.field_name = None @@ -14,7 +18,8 @@ class KeyRequest: self._parse_prompt(prompt) def _parse_prompt(self, prompt: str): - prompt_parts = prompt.split(".") + logger.debug(f"Got prompt: {prompt}") + prompt_parts = prompt.replace('\n', '').split(".") key = None match len(prompt_parts): case 1: @@ -26,6 +31,7 @@ class KeyRequest: case _: key = None if key is None: + logger.warning(f"Prompt {prompt} cannot be parsed") return key_parts = key.split("/") self.path = key_parts[:] if len(key_parts) > 1 else None @@ -57,6 +63,8 @@ class Passwords: request = KeyRequest(k) with self.open() as kp: kp_entry = kp.find_entries(path=request.path, first=True, title=request.entry_name) + if kp_entry is None: + logger.warning(f"No value found for key {k}") output[k] = self._get_field_value(kp_entry, request.field_name) return output