174 tests covering URL normalization, FTS5 query sanitization, SSRF/CSRF guards, sharing-mode logic, DB schema and upsert paths, handler end-to-end flows, and gateway body-size / mesh-whitelist guards. Each recent bug-fix commit (6ffd38d,1bc695f,8dffd8c) has an explicit regression test in test_regressions.py. One xfail documents a minor latent bug in clean_url where port 80 is not stripped from upgraded https URLs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8dffd8ccea
commit
44a16dea98
18 changed files with 1673 additions and 0 deletions
174
tests/test_handlers_pages.py
Normal file
174
tests/test_handlers_pages.py
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
"""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"]
|
||||
Loading…
Add table
Add a link
Reference in a new issue