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.
107 lines
4 KiB
Python
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"
|