tests: test commands

This commit is contained in:
cătălin 2023-04-03 18:40:11 +02:00
commit 570c29d9f1
Signed by: catalin
GPG key ID: 686088EF78EE4083
16 changed files with 235 additions and 52 deletions

1
.gitignore vendored
View file

@ -215,3 +215,4 @@ report.xml
.pdm-python
reportlog.json
.ruff_cache/
.pdm.toml

View file

@ -12,5 +12,13 @@ tests:
dist:
pdm build
publish-pypi:
pdm publish -u $(PYPI_REGISTRY_USERNAME) -P $(PYPI_REGISTRY_PASSWORD)
publish-roboces:
pdm publish --build -u $(ROBOCES_REGISTRY_USERNAME) -p $(ROBOCES_REGISTRY_PASSWORD) -r roboces
publish:
pdm publish -u $(ROBOCES_REGISTRY_USERNAME) -P $(ROBOCES_REGISTRY_PASSWORD) -r https://git.roboces.dev/api/packages/catalin/pypi
make publish-pypi
make publish-roboces

View file

@ -2,12 +2,10 @@
[(r)age](https://github.com/woodruffw/pyrage) encrypted note-taking CLI app
## install and init
You can use plain `pip` but I'd recommend `pipx`
## install
```shell
pipx install halig
pip install halig
```
## cli mode
@ -16,8 +14,4 @@ pipx install halig
$ halig edit some_notebook # edit today's note
$ halig edit some_notebook/foo # edit /path/to/some_notebook/foo.age
$ halig notebooks # list current notebooks
```
## tui mode
wip
```

View file

View file

@ -1,23 +1,14 @@
from abc import ABC, abstractmethod
import pendulum
from pendulum.datetime import DateTime
from pendulum.tz import local_timezone
from halig.settings import Settings
class ICommand(ABC):
@abstractmethod
def run(self):
...
... # pragma: no cover
class BaseCommand(ICommand):
@property
def today(self) -> DateTime:
tz = local_timezone()
return pendulum.now(tz)
def __init__(self, settings: Settings, *args, **kwargs):
self.settings = settings

View file

@ -3,6 +3,7 @@ import subprocess
import tempfile
from pathlib import Path
from halig import utils
from halig.commands.base import BaseCommand
from halig.encryption import Encryptor
from halig.settings import Settings
@ -30,7 +31,7 @@ class EditCommand(BaseCommand):
self.encryptor = Encryptor(self.settings)
if self.note_path.is_dir():
self.note_path /= f"{self.today.date()}.age"
self.note_path /= f"{utils.now().date()}.age"
if not self.note_path.name.endswith(".age"):
err = f"File {self.note_path.name} is not a valid AGE file"
@ -43,7 +44,7 @@ class EditCommand(BaseCommand):
def edit_contents(self, original_contents: bytes) -> bytes:
"""Let the user edit the contents by opening an in-memory tempfile
using $EDITOR and encrypting the new contents
using $EDITOR and encrypt the new contents
Args:
original_contents (bytes): original data that will be dumped into the

View file

@ -28,4 +28,4 @@ class NotebooksCommand(BaseCommand):
return tree
def run(self):
print(self.build_tree(self.settings.notebooks_root_path))
print(self.build_tree(self.settings.notebooks_root_path)) # pragma: no cover

View file

@ -3,6 +3,7 @@ from pathlib import Path
from rich.console import Console
from rich.markdown import Markdown
from halig import utils
from halig.commands.base import BaseCommand
from halig.encryption import Encryptor
from halig.settings import Settings
@ -16,7 +17,7 @@ class ShowCommand(BaseCommand):
self.encryptor = Encryptor(self.settings)
self.console = Console()
if self.note_path.is_dir():
self.note_path /= f"{self.today.date()}.age"
self.note_path /= f"{utils.now().date()}.age"
if not self.note_path.exists():
err = f"File {self.note_path.name} does not exist"
@ -26,9 +27,11 @@ class ShowCommand(BaseCommand):
err = f"File {self.note_path.name} is not a valid AGE file"
raise ValueError(err)
def run(self):
def decrypt(self) -> str:
with self.note_path.open("rb") as f:
data = f.read()
contents = self.encryptor.decrypt(data)
md_contents = Markdown(contents.decode())
return self.encryptor.decrypt(data).decode()
def run(self): # pragma: no cover
md_contents = Markdown(self.decrypt())
self.console.print(md_contents)

7
halig/utils.py Normal file
View file

@ -0,0 +1,7 @@
import pendulum
from pendulum.tz import local_timezone
def now():
tz = local_timezone()
return pendulum.now(tz)

49
pdm.lock generated
View file

@ -85,6 +85,12 @@ version = "0.1.2"
requires_python = ">=3.7"
summary = "Markdown URL utilities"
[[package]]
name = "mock"
version = "5.0.1"
requires_python = ">=3.6"
summary = "Rolling backport of unittest.mock for all Pythons"
[[package]]
name = "mypy"
version = "1.1.1"
@ -166,7 +172,7 @@ dependencies = [
[[package]]
name = "pyfakefs"
version = "5.1.0"
version = "5.2.0"
requires_python = ">=3.7"
summary = "pyfakefs implements a fake file system that mocks the Python file system modules."
@ -182,15 +188,6 @@ version = "1.0.3"
requires_python = ">=3.7"
summary = "Python bindings for rage (age in Rust)"
[[package]]
name = "pyrage-stubs"
version = "1.0.1"
requires_python = ">= 3.7"
summary = "A PEP 561 stub package for pyrage's types"
dependencies = [
"pyrage<2.0,>=1.0.0",
]
[[package]]
name = "pyright"
version = "1.1.301"
@ -247,6 +244,15 @@ dependencies = [
"typer>=0.3.2",
]
[[package]]
name = "pytest-mock"
version = "3.10.0"
requires_python = ">=3.7"
summary = "Thin-wrapper around the mock package for easier use with pytest"
dependencies = [
"pytest>=5.0",
]
[[package]]
name = "pytest-pretty"
version = "1.1.1"
@ -342,9 +348,8 @@ requires_python = ">=3.7"
summary = "Backported and Experimental Type Hints for Python 3.7+"
[metadata]
lock_version = "4.2"
groups = ["default", "linters", "linting", "testing"]
content_hash = "sha256:bd20ae8c3a5d73b6301022d3e5fd8844e31baf827ff41860ca8afd55d5b3ffe9"
lock_version = "4.1"
content_hash = "sha256:3f09b4f252ecdcbfd074ccc8da97ac8700aef25a5ae1a37c7eae9b53b0d17008"
[metadata.files]
"attrs 22.2.0" = [
@ -459,6 +464,10 @@ content_hash = "sha256:bd20ae8c3a5d73b6301022d3e5fd8844e31baf827ff41860ca8afd55d
{url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"},
{url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"},
]
"mock 5.0.1" = [
{url = "https://files.pythonhosted.org/packages/a9/c8/7f5fc5ee6a666d7e4ee7a3222bcb37ebebaea3697d7bf54517728f56bb28/mock-5.0.1.tar.gz", hash = "sha256:e3ea505c03babf7977fd21674a69ad328053d414f05e6433c30d8fa14a534a6b"},
{url = "https://files.pythonhosted.org/packages/e6/88/8a05e7ad0bb823246b2add3d2e97f990c41c71a40762c8db77a4bd78eedf/mock-5.0.1-py3-none-any.whl", hash = "sha256:c41cfb1e99ba5d341fbcc5308836e7d7c9786d302f995b2c271ce2144dece9eb"},
]
"mypy 1.1.1" = [
{url = "https://files.pythonhosted.org/packages/2a/28/8485aad67750b3374443d28bad3eed947737cf425a640ea4be4ac70a7827/mypy-1.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce61663faf7a8e5ec6f456857bfbcec2901fbdb3ad958b778403f63b9e606a1b"},
{url = "https://files.pythonhosted.org/packages/30/da/808ceaf2bcf23a9e90156c7b11b41add8dd5a009ee48159ec820d04d97bd/mypy-1.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9401e33814cec6aec8c03a9548e9385e0e228fc1b8b0a37b9ea21038e64cdd8a"},
@ -579,9 +588,9 @@ content_hash = "sha256:bd20ae8c3a5d73b6301022d3e5fd8844e31baf827ff41860ca8afd55d
{url = "https://files.pythonhosted.org/packages/fa/c2/3df79cd00e65678fce12e59e8c95378a992a93d7b9f9510d4f1f65df1936/pydantic-1.10.7-cp311-cp311-win_amd64.whl", hash = "sha256:b4a849d10f211389502059c33332e91327bc154acc1845f375a99eca3afa802d"},
{url = "https://files.pythonhosted.org/packages/fd/66/3da2e7c0306251435bd61ae9da52db8a00672fdf2b2db1e3efe1692f41dd/pydantic-1.10.7-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:950ce33857841f9a337ce07ddf46bc84e1c4946d2a3bba18f8280297157a3fd1"},
]
"pyfakefs 5.1.0" = [
{url = "https://files.pythonhosted.org/packages/38/4b/0e12c26797eeb1ea4574f3b94937817ee781a13d3fb800c2dc4c115a56f3/pyfakefs-5.1.0.tar.gz", hash = "sha256:316c6026640d14a6b4fbde71fd9674576d1b5710deda8fabde8aad51d785dbc3"},
{url = "https://files.pythonhosted.org/packages/a4/db/1b738a4d422eee1bb9688c2ea01d9b4bcc02e4e98ad71e13f5db3ff3cd5a/pyfakefs-5.1.0-py3-none-any.whl", hash = "sha256:e6f34a8224b41f1b1ab25aa8d430121dac42e3c6e981e01eae76b3343fba47d0"},
"pyfakefs 5.2.0" = [
{url = "https://files.pythonhosted.org/packages/41/63/c06696cd4e6305f83fdb6ea275771ae04a9fd97afbec41b2bdaa3c7c7945/pyfakefs-5.2.0-py3-none-any.whl", hash = "sha256:cf465f90c9657018e381668b6fae38e034417d937ef596968ff8b2550ad158d7"},
{url = "https://files.pythonhosted.org/packages/9e/99/92012cd1dd8c41171efee7086610ddc7997b6ed11e1be80105b8d9ca5e9a/pyfakefs-5.2.0.tar.gz", hash = "sha256:db570ae847f0abd44b5b67ab379f0b111e6bdcd35486eeece0ea2439c9e30766"},
]
"pygments 2.14.0" = [
{url = "https://files.pythonhosted.org/packages/0b/42/d9d95cc461f098f204cd20c85642ae40fbff81f74c300341b8d0e0df14e0/Pygments-2.14.0-py3-none-any.whl", hash = "sha256:fa7bd7bd2771287c0de303af8bfdfc731f51bd2c6a47ab69d117138893b82717"},
@ -592,10 +601,6 @@ content_hash = "sha256:bd20ae8c3a5d73b6301022d3e5fd8844e31baf827ff41860ca8afd55d
{url = "https://files.pythonhosted.org/packages/d9/a4/785af8296a882132b63de046c54d9a45c8d1d634e22c5d40463accd8eefd/pyrage-1.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c554d7669539c0a7a870464faf55f0121c117339b1b0f57d6ca5eb85079c107"},
{url = "https://files.pythonhosted.org/packages/fb/9c/54a5c8aad8442ba26845c325c8b4eaa90cc3b470d0e57465d82ca54ad843/pyrage-1.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:32b63a06f138a1c231285c61f61a1800b7b894851b06cb763b7b35b18ccd2b11"},
]
"pyrage-stubs 1.0.1" = [
{url = "https://files.pythonhosted.org/packages/00/5c/5e0430c00a0094d7e86b3487839de6b4213cf34f46faf27cc98acac54084/pyrage_stubs-1.0.1-py3-none-any.whl", hash = "sha256:729d9c8ac43f9aac9e22fe6ae3cf285a4f0740bece7b793a57eb229fb6db93a7"},
{url = "https://files.pythonhosted.org/packages/6f/c3/352df4e5927d4f34fefdd0e9e0332061c3b98937e39b871c91f8e66098ea/pyrage-stubs-1.0.1.tar.gz", hash = "sha256:e33bc444bfda7b5a783d3821aa99ba9269d2a9f2c39ccd8aba46a7b8525fbd22"},
]
"pyright 1.1.301" = [
{url = "https://files.pythonhosted.org/packages/1f/f9/35360780b41236428be6a2a8f8f53007d5460e757d8bfe007b7303f63682/pyright-1.1.301.tar.gz", hash = "sha256:6ac4afc0004dca3a977a4a04a8ba25b5b5aa55f8289550697bfc20e11be0d5f2"},
{url = "https://files.pythonhosted.org/packages/4c/af/7a7bd8513361934d269aca00bdea5f879d92e5b89f79e4923d1f1b06d498/pyright-1.1.301-py3-none-any.whl", hash = "sha256:ecc3752ba8c866a8041c90becf6be79bd52f4c51f98472e4776cae6d55e12826"},
@ -615,6 +620,10 @@ content_hash = "sha256:bd20ae8c3a5d73b6301022d3e5fd8844e31baf827ff41860ca8afd55d
{url = "https://files.pythonhosted.org/packages/07/e1/5f71f28d1f77ade128b54c20cee04e34508f7480f599d418baf26547c64e/pytest_duration_insights-0.1.1-py2.py3-none-any.whl", hash = "sha256:27f771077f58b558a62fee0af396c1a2efc4aa1217aad9e0cf11d120b2067e86"},
{url = "https://files.pythonhosted.org/packages/cf/09/b2129ad0ff70f0283d86e76497ddac9b53f0aa876fec8462dc3408d3721d/pytest-duration-insights-0.1.1.tar.gz", hash = "sha256:9dcc6aca40bfbdab2b0b34f0e55c3b5bd4ac41f3a419a2b7b8a22fb7ebe62933"},
]
"pytest-mock 3.10.0" = [
{url = "https://files.pythonhosted.org/packages/91/84/c951790e199cd54ddbf1021965b62a5415b81193ebdb4f4af2659fd06a73/pytest_mock-3.10.0-py3-none-any.whl", hash = "sha256:f4c973eeae0282963eb293eb173ce91b091a79c1334455acfac9ddee8a1c784b"},
{url = "https://files.pythonhosted.org/packages/f6/2b/137a7db414aeaf3d753d415a2bc3b90aba8c5f61dff7a7a736d84b2ec60d/pytest-mock-3.10.0.tar.gz", hash = "sha256:fbbdb085ef7c252a326fd8cdcac0aa3b1333d8811f131bdcc701002e1be7ed4f"},
]
"pytest-pretty 1.1.1" = [
{url = "https://files.pythonhosted.org/packages/ba/8f/7cafa3856d81175264fcc8929d0c5c60e59b787b1b2891095b77a7aaebb7/pytest_pretty-1.1.1.tar.gz", hash = "sha256:dfabbeee334ff5e77f3f731623cd44f147b3db7001df8d9ef8ea1f54986b41e2"},
{url = "https://files.pythonhosted.org/packages/c5/98/859ccc1482b23c008e434da91880bb1197e29d9fe3644c3124b58e9b6bd9/pytest_pretty-1.1.1-py3-none-any.whl", hash = "sha256:22d19d319f2ce07d62ea64f487616d99433cae8530cd20813487bad8a7d5f088"},

View file

@ -1,5 +1,6 @@
[tool.pdm.build]
includes = []
[build-system]
requires = ["pdm-backend"]
build-backend = "pdm.backend"
@ -19,9 +20,10 @@ dependencies = [
"pendulum>=2.1.2",
]
name = "halig"
version = "0.1.1"
version = "0.1.2"
description = "age-encrypted, file-based, note-taking CLI app"
readme = "README.md"
[project.scripts]
halig = "halig.main:app"
@ -34,6 +36,8 @@ testing = [
"pytest-reportlog>=0.2.1",
"pytest-duration-insights>=0.1.1",
"pytest-pretty>=1.1.1",
"pytest-mock>=3.10.0",
"mock>=5.0.1",
]
linting = [
"black>=23.3.0",
@ -41,11 +45,9 @@ linting = [
"pyright>=1.1.301",
"mypy>=1.1.1",
"types-PyYAML>=6.0.12.9",
"pyrage-stubs>=1.0.1",
]
linters = [
"pyrage-stubs>=1.0.1",
]
[tool.pytest]
mock_use_standalone_module = true
[tool.pyright]
reportMissingImports = false
@ -63,5 +65,6 @@ warn_unused_configs = true
module = [
"pyrage",
"pyrage.ssh",
"pyrage.x25519",
]
ignore_missing_imports = true

View file

@ -0,0 +1,63 @@
from pathlib import Path
import pendulum
import pytest as pytest
from halig import utils
from halig.commands.notebooks import NotebooksCommand
from halig.settings import Settings
@pytest.fixture()
def notes(notebooks_path: Path):
personal = (notebooks_path / "Personal")
work = (notebooks_path / "Work")
personal.mkdir()
work.mkdir()
personal_todos = (personal / "todos.age")
personal_todos.touch()
work_todos = (work / "todos.age")
work_todos.touch()
dailies = work / "Dailies"
dailies.mkdir()
dt = pendulum.now()
for day_offset in range(10):
dt = dt.subtract(days=day_offset)
(dailies / f"{dt.date()}.age").touch()
@pytest.fixture
def notebooks_command(settings: Settings):
return NotebooksCommand(max_depth=float("inf"), settings=settings)
@pytest.fixture()
def current_note(notes, settings, encryptor) -> Path:
note_path = settings.notebooks_root_path / f"{utils.now().date()}.age"
note_path.touch()
data = encryptor.encrypt("foo".encode())
with note_path.open("wb") as f:
f.write(data)
return note_path
@pytest.fixture
def current_daily(notes, settings, encryptor) -> Path:
note_path = settings.notebooks_root_path / "Work" / "Dailies" / f"{utils.now().date()}.age"
data = encryptor.encrypt("foo".encode())
with note_path.open("wb") as f:
f.write(data)
return note_path
@pytest.fixture
def mock_edit(mocker):
def edit(callargs: list):
with open(callargs[1], "wb") as f:
f.write("edited".encode())
mocker.patch('halig.commands.edit.subprocess.call', side_effect=edit)

View file

@ -0,0 +1,30 @@
import pytest
from halig.commands.edit import EditCommand
from halig.settings import Settings
def test_edit_raises_invalid_age_file(notes, settings: Settings):
note_path = settings.notebooks_root_path / "foo.txt"
note_path.touch()
with pytest.raises(ValueError, match="is not a valid AGE file"):
EditCommand(note_path, settings=settings, )
def test_edit_current_note(mock_edit, current_note, settings: Settings, encryptor):
edit_command = EditCommand(note_path=settings.notebooks_root_path, settings=settings)
assert edit_command.note_path == current_note
edit_command.run()
with current_note.open("rb") as f:
contents = encryptor.decrypt(f.read()).decode()
assert contents == "edited"
def test_edit_current_daily(mock_edit, current_daily, settings, encryptor):
current_daily.unlink()
edit_command = EditCommand(note_path=current_daily, settings=settings)
assert edit_command.note_path == current_daily
edit_command.run()
with current_daily.open("rb") as f:
contents = encryptor.decrypt(f.read()).decode()
assert contents == "edited"

View file

@ -0,0 +1,42 @@
from halig.commands.notebooks import NotebooksCommand
def test_build_tree_max_depth_0(notes, notebooks_command: NotebooksCommand):
notebooks_command.max_depth = 0
tree = notebooks_command.build_tree(notebooks_command.settings.notebooks_root_path)
assert not tree.children
def test_build_tree_max_depth_1(notes, notebooks_command: NotebooksCommand):
notebooks_command.max_depth = 1
tree = notebooks_command.build_tree(notebooks_command.settings.notebooks_root_path)
personal = tree.children[0]
work = tree.children[1]
assert personal.label == "Personal"
assert work.label == "Work"
assert not personal.children
assert not work.children
def test_build_tree_max_depth_2(notes, notebooks_command: NotebooksCommand):
notebooks_command.max_depth = 2
tree = notebooks_command.build_tree(notebooks_command.settings.notebooks_root_path)
personal = tree.children[0]
work = tree.children[1]
assert personal.label == "Personal"
assert work.label == "Work"
assert len(work.children) == 2
assert len(personal.children) == 1
def test_build_tree_max_depth_inf(notes, notebooks_command: NotebooksCommand):
tree = notebooks_command.build_tree(notebooks_command.settings.notebooks_root_path)
personal = tree.children[0]
work = tree.children[1]
assert personal.label == "Personal"
assert work.label == "Work"
assert len(work.children) == 2
assert len(personal.children) == 1
assert work.children[0].label == "Dailies"
assert len(work.children[0].children) == 10

View file

@ -0,0 +1,30 @@
from pathlib import Path
import pytest
from halig.commands.show import ShowCommand
from halig.settings import Settings
def test_show_raises_note_path_does_not_exist(notes, settings: Settings):
with pytest.raises(ValueError, match="does not exist"):
ShowCommand(Path('foo'), settings=settings, )
def test_show_raises_note_path_is_not_age_valid(notes, settings: Settings):
note_path = settings.notebooks_root_path / "foo.txt"
note_path.touch()
with pytest.raises(ValueError, match="is not a valid AGE file"):
ShowCommand(note_path, settings=settings, )
def test_show_current_note(current_note, settings):
show_command = ShowCommand(note_path=settings.notebooks_root_path, settings=settings)
assert show_command.note_path == current_note
assert show_command.decrypt() == "foo"
def test_show_current_daily(current_daily, settings: Settings):
show_command = ShowCommand(note_path=current_daily, settings=settings)
assert show_command.note_path == current_daily
assert show_command.decrypt() == "foo"

View file

@ -41,6 +41,7 @@ def ssh_recipient(halig_ssh_public_key: str) -> Recipient:
@pytest.fixture()
def halig_path(fs, halig_ssh_public_key, halig_ssh_private_key) -> Path:
fs.add_real_paths(["/etc/localtime"])
ssh_path = Path("~/.ssh").expanduser()
ssh_path.mkdir(parents=True)