Update projektu

This commit is contained in:
lachtan
2026-07-22 12:32:02 +02:00
parent 19014ed3d9
commit 8e66d6b92a
22 changed files with 1995 additions and 503 deletions

View File

@@ -0,0 +1,180 @@
"""Tests for bookmark.py — reading-list CRUD, article-content storage, migration."""
import io
import sqlite3
import subprocess
import sys
from pathlib import Path
import pytest
SCRIPTS = Path(__file__).resolve().parent.parent / "scripts"
sys.path.insert(0, str(SCRIPTS))
import bookmark # noqa: E402
HTML_TO_MARKDOWN = SCRIPTS / "html_to_markdown.py"
@pytest.fixture
def db(tmp_path, monkeypatch):
path = tmp_path / "db" / "bookmark.sqlite"
monkeypatch.setattr(bookmark, "DB_PATH", path)
return path
def _run(argv, stdin=None, monkeypatch=None):
monkeypatch.setattr(sys, "argv", ["bookmark.py"] + argv)
if stdin is not None:
monkeypatch.setattr(sys, "stdin", io.StringIO(stdin))
bookmark.main()
def _capture(capsys):
return capsys.readouterr().out
def test_add_stores_content_from_stdin(db, monkeypatch, capsys):
_run(["add", "https://ex.com/a", "Test", "--content-file", "-"],
stdin="# Title\n\nBody paragraph.\n", monkeypatch=monkeypatch)
assert "article content stored" in _capture(capsys)
conn = sqlite3.connect(db)
content = conn.execute("SELECT content FROM bookmarks").fetchone()[0]
conn.close()
assert content == "# Title\n\nBody paragraph."
def test_add_without_content_leaves_null(db, monkeypatch, capsys):
_run(["add", "https://ex.com/b", "No content"], monkeypatch=monkeypatch)
conn = sqlite3.connect(db)
content = conn.execute("SELECT content FROM bookmarks").fetchone()[0]
conn.close()
assert content is None
def test_content_command_prints_stored_markdown(db, monkeypatch, capsys):
_run(["add", "https://ex.com/a", "Test", "--content-file", "-"],
stdin="# Title\n\nBody.\n", monkeypatch=monkeypatch)
capsys.readouterr()
_run(["content", "1"], monkeypatch=monkeypatch)
assert "# Title\n\nBody." in _capture(capsys)
def test_content_command_reports_empty(db, monkeypatch, capsys):
_run(["add", "https://ex.com/b", "No content"], monkeypatch=monkeypatch)
capsys.readouterr()
_run(["content", "1"], monkeypatch=monkeypatch)
assert "no stored article content" in _capture(capsys)
def test_list_marks_only_rows_with_content(db, monkeypatch, capsys):
_run(["add", "https://ex.com/plain", "Plain"], monkeypatch=monkeypatch)
_run(["add", "https://ex.com/full", "Full", "--content-file", "-"],
stdin="# Doc\n\ntext\n", monkeypatch=monkeypatch)
capsys.readouterr()
_run(["list"], monkeypatch=monkeypatch)
out = _capture(capsys)
# Newest first: #1 is the one WITH content (has 📄), #2 plain (no marker).
lines = [ln for ln in out.splitlines() if ln.startswith("#")]
assert "📄" in lines[0]
assert "📄" not in lines[1]
def test_migration_adds_columns_to_legacy_db(db, monkeypatch, capsys):
db.parent.mkdir(parents=True)
conn = sqlite3.connect(db)
conn.executescript(
"""
CREATE TABLE bookmarks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
url TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '',
tags TEXT NOT NULL DEFAULT '[]',
created_at TEXT NOT NULL,
read_at TEXT
);
INSERT INTO bookmarks (url, description, tags, created_at)
VALUES ('https://old.com', 'legacy', '[]', '2026-01-01T00:00:00+00:00');
"""
)
conn.commit()
conn.close()
_run(["list"], monkeypatch=monkeypatch) # triggers migration
assert "legacy" in _capture(capsys)
conn = sqlite3.connect(db)
cols = {row[1] for row in conn.execute("PRAGMA table_info(bookmarks)")}
count = conn.execute("SELECT COUNT(*) FROM bookmarks").fetchone()[0]
conn.close()
assert "content" in cols
assert "deleted_at" in cols
assert count == 1
def test_delete_soft_hides_but_keeps_row(db, monkeypatch, capsys):
_run(["add", "https://ex.com/keep", "Keep"], monkeypatch=monkeypatch)
_run(["add", "https://ex.com/dup", "Dup"], monkeypatch=monkeypatch)
capsys.readouterr()
_run(["delete", "1"], monkeypatch=monkeypatch) # newest (dup) is #1
assert "Deleted bookmark #1" in _capture(capsys)
_run(["list"], monkeypatch=monkeypatch)
out = _capture(capsys)
assert "Keep" in out
assert "Dup" not in out
conn = sqlite3.connect(db)
total = conn.execute("SELECT COUNT(*) FROM bookmarks").fetchone()[0]
deleted = conn.execute(
"SELECT COUNT(*) FROM bookmarks WHERE deleted_at IS NOT NULL"
).fetchone()[0]
conn.close()
assert total == 2 # row kept in DB
assert deleted == 1
def test_delete_out_of_range(db, monkeypatch, capsys):
_run(["add", "https://ex.com/a", "One"], monkeypatch=monkeypatch)
capsys.readouterr()
_run(["delete", "9"], monkeypatch=monkeypatch)
assert "No unread bookmark #9" in _capture(capsys)
ARTICLE_HTML = """<!DOCTYPE html><html><head><title>EFI Boot</title></head><body>
<header><nav><a href="/">Home</a> <a href="/login">Login</a></nav></header>
<aside class="ad">Buy our product now! Special offer!</aside>
<main><article>
<h1>Understanding EFI Boot</h1>
<p>The EFI system partition stores the bootloader and kernel images that the
firmware loads at startup, which is a crucial part of the modern boot process.</p>
<h2>How it works</h2>
<p>When the machine powers on, the firmware reads the EFI variables and locates
the boot entry, then hands control to the kernel without a traditional bootloader.</p>
</article></main>
<footer>Copyright 2026 Example Inc. Share Tweet Subscribe to our newsletter.</footer>
</body></html>"""
def _run_html(html_input: str) -> subprocess.CompletedProcess:
return subprocess.run(
["uv", "run", str(HTML_TO_MARKDOWN)],
input=html_input,
capture_output=True,
text=True,
)
def test_html_to_markdown_extracts_clean_article():
result = _run_html(ARTICLE_HTML)
assert result.returncode == 0, result.stderr
assert "# Understanding EFI Boot" in result.stdout
assert "## How it works" in result.stdout
# Boilerplate must be gone.
assert "Buy our product" not in result.stdout
assert "Subscribe to our newsletter" not in result.stdout
def test_html_to_markdown_empty_input():
result = _run_html("")
assert result.returncode == 1