from __future__ import annotations import importlib.util import sys from datetime import date, datetime, timedelta from pathlib import Path import pytest def _load_cli(): path = Path(__file__).parent.parent / "cli" / "snotes.py" spec = importlib.util.spec_from_file_location("snotes_cli", path) mod = importlib.util.module_from_spec(spec) sys.modules[spec.name] = mod spec.loader.exec_module(mod) return mod cli = _load_cli() # -- parse_date -------------------------------------------------------------- def test_parse_date_defaults_to_today(): assert cli.parse_date(None) == date.today() assert cli.parse_date("today") == date.today() def test_parse_date_relative_words(): assert cli.parse_date("yesterday") == date.today() - timedelta(days=1) assert cli.parse_date("tomorrow") == date.today() + timedelta(days=1) def test_parse_date_offsets(): assert cli.parse_date("+3") == date.today() + timedelta(days=3) assert cli.parse_date("-7") == date.today() - timedelta(days=7) assert cli.parse_date("0") == date.today() def test_parse_date_iso(): assert cli.parse_date("2026-09-26") == date(2026, 9, 26) def test_parse_date_invalid(): with pytest.raises(cli.CliError, match="bad date"): cli.parse_date("next friday-ish") # -- render_template --------------------------------------------------------- def test_render_template_date(): body = "# Journal for %date%\n$journal\n\n- \n" out = cli.render_template(body, date(2026, 9, 26), "schmeeve") assert out.startswith("# Journal for 2026-09-26\n") def test_render_template_strips_marker_lines(): body = "# T\n$journal\nmiddle\n $other \nend\n" out = cli.render_template(body, date(2026, 1, 1), "u") assert "$journal" not in out assert "$other" not in out assert "middle" in out assert "end" in out def test_render_template_keeps_inline_markers(): # only standalone marker lines are stripped body = "# T\nmentions $journal inline\n" out = cli.render_template(body, date(2026, 1, 1), "u") assert "$journal inline" in out def test_render_template_vars(): now = datetime(2026, 9, 26, 14, 30) body = "%date% | %time% | %datetime% | %user%" out = cli.render_template(body, date(2026, 1, 2), "ilya", now=now) assert out == "2026-01-02 | 14:30 | 2026-01-02 14:30 | ilya\n" def test_render_template_collapses_blank_edges(): body = "$journal\n\n# Title\n\n" out = cli.render_template(body, date(2026, 1, 1), "u") assert out == "# Title\n" # -- extract_title ----------------------------------------------------------- def test_extract_title_heading(): assert cli.extract_title("# Hello\n\nbody") == "Hello" def test_extract_title_first_line(): assert cli.extract_title("plain line\nmore") == "plain line" def test_extract_title_untitled(): assert cli.extract_title(" \n\n") == "Untitled" # -- config ------------------------------------------------------------------ def test_config_round_trip(tmp_path, monkeypatch): path = tmp_path / "cfg" / "config.json" monkeypatch.setattr(cli, "CONFIG_PATH", path) cfg = cli.Config(url="http://sno.tes/", user="schmeeve", token="tok123") cli.save_config(cfg) loaded = cli.load_config() assert loaded.url == "http://sno.tes" # trailing slash stripped assert loaded.user == "schmeeve" assert loaded.token == "tok123" assert (path.stat().st_mode & 0o777) == 0o600 def test_load_config_missing(tmp_path, monkeypatch): monkeypatch.setattr(cli, "CONFIG_PATH", tmp_path / "nope.json") with pytest.raises(cli.CliError, match="no config"): cli.load_config() def test_load_config_bad_json(tmp_path, monkeypatch): path = tmp_path / "config.json" path.write_text("{not json") monkeypatch.setattr(cli, "CONFIG_PATH", path) with pytest.raises(cli.CliError, match="bad config"): cli.load_config()