"""Tests for `handle_bulk_action`, edit flow, and the bulk-delete confirm step. The bulk-delete confirmation flow is a data-loss guard added in commit 8dffd8c — a stray POST without `confirmed=1` must render the confirmation page instead of actually deleting. """ from db import get_db, return_db from handlers import ( handle_bulk_action, handle_edit_form, handle_edit_submit, handle_pages, ) def _all_urls(seeded_db): db = get_db() try: return {r["url"] for r in db.execute("SELECT url FROM pages").fetchall()} finally: return_db(db) def _page_id(seeded_db, url): db = get_db() try: return db.execute("SELECT id FROM pages WHERE url = ?", (url,)).fetchone()["id"] finally: return_db(db) def test_bulk_delete_without_confirmed_renders_confirm_page(seeded_db, csrf_session): """Regression for 8dffd8c: bulk delete must NOT delete until confirmed=1 is set.""" pid = _page_id(seeded_db, "https://example.com/rust-intro") urls_before = _all_urls(seeded_db) resp = handle_bulk_action({ "ids": [str(pid)], "action": ["delete"], }) assert resp["status"] == 200 assert "confirm delete" in resp["body"].lower() assert "Rust Intro" in resp["body"] # Must still show a hidden confirmed=1 field in the follow-up form. assert 'name="confirmed" value="1"' in resp["body"] # Crucially: nothing should have been deleted. assert _all_urls(seeded_db) == urls_before def test_bulk_delete_with_confirmed_actually_deletes(seeded_db, csrf_session): pid = _page_id(seeded_db, "https://example.com/rust-intro") resp = handle_bulk_action({ "ids": [str(pid)], "action": ["delete"], "confirmed": ["1"], }) # Confirmed delete redirects back to /pages. assert resp["status"] in (302, 303) urls = _all_urls(seeded_db) assert "https://example.com/rust-intro" not in urls # Other pages untouched. assert "https://example.com/python-tips" in urls def test_bulk_delete_with_no_ids_redirects(seeded_db, csrf_session): resp = handle_bulk_action({ "ids": [], "action": ["delete"], "confirmed": ["1"], }) assert resp["status"] in (302, 303) assert _all_urls(seeded_db) == { "https://example.com/rust-intro", "https://example.com/python-tips", "https://example.com/ocaml-why", "https://news.example.org/mesh", } def test_bulk_delete_rejects_non_integer_ids(seeded_db, csrf_session): resp = handle_bulk_action({ "ids": ["not-a-number"], "action": ["delete"], "confirmed": ["1"], }) assert resp["status"] == 400 def test_bulk_retag_add_mode_merges_tags(seeded_db, csrf_session): pid = _page_id(seeded_db, "https://example.com/python-tips") handle_bulk_action({ "ids": [str(pid)], "action": ["retag"], "bulk_tags": ["scripting, tutorials"], "tag_mode": ["add"], }) db = get_db() try: rows = db.execute( "SELECT t.name FROM tags t JOIN page_tags pt ON pt.tag_id = t.id " "WHERE pt.page_id = ? ORDER BY t.name", (pid,), ).fetchall() finally: return_db(db) tags = [r["name"] for r in rows] assert "python" in tags # existing kept assert "scripting" in tags # new added assert "tutorials" in tags def test_bulk_retag_replace_mode_overwrites_tags(seeded_db, csrf_session): pid = _page_id(seeded_db, "https://example.com/python-tips") handle_bulk_action({ "ids": [str(pid)], "action": ["retag"], "bulk_tags": ["one, two"], "tag_mode": ["replace"], }) db = get_db() try: rows = db.execute( "SELECT t.name FROM tags t JOIN page_tags pt ON pt.tag_id = t.id " "WHERE pt.page_id = ?", (pid,), ).fetchall() finally: return_db(db) tags = {r["name"] for r in rows} assert tags == {"one", "two"} assert "python" not in tags def test_edit_form_renders_current_values(seeded_db, csrf_session): pid = _page_id(seeded_db, "https://example.com/rust-intro") resp = handle_edit_form(pid) assert resp["status"] == 200 assert "Rust Intro" in resp["body"] # Existing tags should appear in the tag field. assert "rust" in resp["body"] def test_edit_form_404_for_unknown_page(temp_db, csrf_session): resp = handle_edit_form(99999) assert resp["status"] == 404 def test_edit_submit_updates_title_and_note(seeded_db, csrf_session): pid = _page_id(seeded_db, "https://example.com/rust-intro") handle_edit_submit(pid, { "title": ["New Rust Title"], "note": ["new annotation"], "tags": ["rust, updated"], }) db = get_db() try: row = db.execute("SELECT title, note FROM pages WHERE id = ?", (pid,)).fetchone() finally: return_db(db) assert row["title"] == "New Rust Title" assert row["note"] == "new annotation" def test_handle_pages_lists_indexed_pages(seeded_db, csrf_session): resp = handle_pages({}) assert resp["status"] == 200 # Every seeded page title appears on the list page. for title in ("Rust Intro", "Python Tips", "Why OCaml", "Mesh Networking"): assert title in resp["body"]