tinyweb/tests/test_regressions.py
Derick Phan 44a16dea98
Some checks failed
/ build (push) Failing after 5s
Add pytest test suite
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>
2026-04-24 15:03:29 -07:00

107 lines
4 KiB
Python

"""Aggregator of regression tests tied to specific bug-fix commits.
Each test here guards against a specific bug that was once shipped. Running
just this file gives a one-line-per-bug audit:
pytest tests/test_regressions.py -v
The test bodies are intentionally small; for the exhaustive behavior of each
module, see the topical test files (test_fts_sanitizer.py, test_url_cleanup.py,
etc.). This file's job is to make the bug catalog scannable.
"""
import socket
from unittest.mock import patch
import pytest
import app as app_module
import db as db_module
import handlers as handlers_module
from conftest import patch_dns_fail, patch_dns_ok
from db import clean_url
from handlers import _sanitize_fts_query, handle_bulk_action
def test_6ffd38d_clean_url_preserves_www_when_bare_domain_fails(monkeypatch):
"""6ffd38d: `clean_url` used to strip `www.` unconditionally; for sites that
only serve at `www.`, this produced unreachable clean URLs."""
patch_dns_fail(monkeypatch)
assert clean_url("https://www.example.com/page") == "https://www.example.com/page"
def test_1bc695f_fts_sanitizer_strips_colon():
"""1bc695f: FTS5 colon is a column filter — must not appear in sanitized output."""
assert ":" not in _sanitize_fts_query("title:secret body:exposed")
@pytest.mark.parametrize("op", ["AND", "OR", "NOT", "NEAR"])
def test_1bc695f_fts_sanitizer_drops_operator_words(op):
"""1bc695f: operator words (AND/OR/NOT/NEAR) would be interpreted as FTS5
operators if they landed on the unquoted last token."""
out = _sanitize_fts_query(f"foo {op} bar")
# operator itself should not appear in the output
tokens = out.replace('"', '').split()
assert op not in [t.rstrip("*") for t in tokens]
def test_1bc695f_gateway_rejects_oversize_body():
"""1bc695f: 16 MiB body-size cap prevents memory-exhaustion DoS."""
from tests.test_gateway_limits import FakeGatewayHandler
from gateway import MAX_BODY_SIZE
h = FakeGatewayHandler(
path="/add", method="POST",
headers={"Content-Length": str(MAX_BODY_SIZE + 1)},
)
h._forward("POST")
assert h._captured["error"] and h._captured["error"][0] == 413
def test_1bc695f_mesh_rejects_non_whitelisted_paths():
"""1bc695f: Reticulum callers are limited to GET /api/sites; CSRF cannot
authenticate mesh callers."""
resp = app_module.rns_request_handler(
path="/tinyweb",
data={"method": "POST", "path": "/add", "query": {}, "body": {}, "gateway_host": ""},
request_id="x", link_id="y", remote_identity=None, requested_at=0,
)
assert resp["status"] == 403
def test_1bc695f_pool_returns_clean_connection(temp_db, monkeypatch):
"""1bc695f: uncommitted transactions on a pooled connection used to leak
into the next consumer."""
from db import get_db, return_db
db = get_db()
db.execute(
"INSERT INTO pages (url, title, body) VALUES (?, ?, ?)",
("https://leak.example.com/", "should not persist", "body"),
)
return_db(db) # no commit
db2 = get_db()
try:
urls = {r["url"] for r in db2.execute("SELECT url FROM pages").fetchall()}
finally:
return_db(db2)
assert "https://leak.example.com/" not in urls
def test_8dffd8c_bulk_delete_requires_confirmation(seeded_db, csrf_session):
"""8dffd8c: bulk delete without confirmed=1 must render a confirm page
instead of deleting — the JS confirm on /pages is a first-line filter only."""
from db import get_db, return_db
db = get_db()
try:
pid = db.execute("SELECT id FROM pages LIMIT 1").fetchone()["id"]
count_before = db.execute("SELECT count(*) FROM pages").fetchone()[0]
finally:
return_db(db)
resp = handle_bulk_action({"ids": [str(pid)], "action": ["delete"]})
assert "confirm delete" in resp["body"].lower()
db = get_db()
try:
count_after = db.execute("SELECT count(*) FROM pages").fetchone()[0]
finally:
return_db(db)
assert count_before == count_after, "bulk delete ran without confirmation"