diff --git a/app/services/mo.py b/app/services/mo.py index 33fa240..84792f7 100644 --- a/app/services/mo.py +++ b/app/services/mo.py @@ -7,13 +7,9 @@ from injectable import injectable, autowired, Autowired from app.services import Passwords -class ComplexValueTemplate(Template): - # Pozwala na kropki w nazwach placeholderów, np. ${user.name.first} - idpattern = r'[_a-zA-Z][_a-zA-Z0-9.]*' - - -class SimpleValueTemplate(ComplexValueTemplate): - delimiter = '%' +class ValueTemplate(Template): + # Pozwala na kropki i ukośniki w nazwach placeholderów, np. ${user.name/first} + idpattern = r'[_a-zA-Z][_a-zA-Z0-9.\/]*' @injectable @@ -26,12 +22,9 @@ class Mo: raw = '' with open(mo_file, "r") as mo: raw = mo.read() - cmp = ComplexValueTemplate(raw) - rendered = cmp.substitute(self._passwords.get_values(cmp.get_identifiers())) - smp = SimpleValueTemplate(rendered) - ids = [_id + '.password' for _id in smp.get_identifiers()] - mappings = {k.replace('.password', ''): v for k, v in self._passwords.get_values(ids).items()} - rendered = smp.substitute(mappings) + parsed = ValueTemplate(raw) + mappings = self._passwords.get_values(parsed.get_identifiers()) + rendered = parsed.safe_substitute(mappings) de_mo_ified = str(mo_file).replace(".mo", "") with open(de_mo_ified, "w") as mo: mo.write(rendered) diff --git a/app/services/passwords.py b/app/services/passwords.py index 39ff368..e851d4e 100644 --- a/app/services/passwords.py +++ b/app/services/passwords.py @@ -6,6 +6,30 @@ import keyring from injectable import injectable from pykeepass import PyKeePass, create_database +class KeyRequest: + def __init__(self, prompt: str): + self.field_name = None + self.entry_name = None + self.path = None + self._parse_prompt(prompt) + + def _parse_prompt(self, prompt: str): + prompt_parts = prompt.split("/") + key = None + match len(prompt_parts): + case 1: + self.field_name = 'password' + key = prompt_parts[0] + case 2: + self.field_name = prompt_parts[1] + key = prompt_parts[0] + case _: + key = None + if key is None: + return + key_parts = key.split(".") + self.path = key_parts[:] if len(key_parts) > 1 else None + self.entry_name = key_parts[-1] @injectable(singleton=True) class Passwords: @@ -28,13 +52,10 @@ class Passwords: def get_values(self, keys: list[str]) -> dict[str, str]: output = {} for k in keys: - key_parts = k.split(".") - path = key_parts[:-1] if len(key_parts) > 2 else None - entry_name = key_parts[-2] - field_name = key_parts[-1] + request = KeyRequest(k) with self.open() as kp: - kp_entry = kp.find_entries(path=path, first=True, title=entry_name) - output[k] = self._get_field_value(kp_entry, field_name) + kp_entry = kp.find_entries(path=request.path, first=True, title=request.entry_name) + output[k] = self._get_field_value(kp_entry, request.field_name) return output @staticmethod diff --git a/pyproject.toml b/pyproject.toml index 71a9de1..ebe74fb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,11 +20,11 @@ dependencies = [ "keyring>=25.6.0", ] -[project.optional-dependencies] +[dependency-groups] dev = [ "httpx>=0.27.0", - "pytest>=8.3.0", - "pytest-asyncio>=0.23.0", + "pytest==9.0.1", + "pytest-asyncio>=1.3.0", "ruff>=0.6.0", "mypy>=1.11.0", "types-Jinja2>=2.11.9", diff --git a/tests/files/test1/test.mo.yaml b/tests/files/test1/test.mo.yaml index c494bd1..11bd6b5 100644 --- a/tests/files/test1/test.mo.yaml +++ b/tests/files/test1/test.mo.yaml @@ -1,3 +1,5 @@ -value: %{sample} -nested: %{some.nested.value} -custom: ${custom.field} +value: ${sample} +nested: ${some.nested.value} +custom: ${custom/field} +uname: ${sample/username} +invalid: ${double/slash/example} diff --git a/tests/test_mo.py b/tests/test_mo.py index b3d25d4..01e63c0 100644 --- a/tests/test_mo.py +++ b/tests/test_mo.py @@ -1,24 +1,50 @@ -import os from pathlib import Path -from unittest import TestCase +import pytest import yaml from app.services import Passwords from app.services.mo import Mo -class TestMo(TestCase): - def test_process(self): - target_path = Path('tests/files/test1/test.yaml') - mo = Mo(Passwords()) - mo.process(Path('tests/files/test1/test.mo.yaml').absolute()) - self.assertTrue(os.path.exists(target_path)) - with open(target_path, 'r') as f: - content = f.read() - self.assertFalse(content.__contains__('${')) - self.assertFalse(content.__contains__('%{')) - parsed = yaml.load(content, Loader=yaml.FullLoader) - self.assertEqual('some_pass', parsed['value']) - self.assertEqual('nested_pass', parsed['nested']) - self.assertEqual('custom_content', parsed['custom']) +@pytest.fixture(scope='class') +def target_path(): + p = Path('tests/files/test1/test.yaml') + # 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('tests/files/test1/test.mo.yaml').absolute()) + + assert target_path.exists() + + content = target_path.read_text() + assert '${' not in content + + yield yaml.load(content, Loader=yaml.FullLoader) + + +class TestParsing: + + 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_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'