Files
scout-website/tests/smoke_documents.py
T
thethreemagi 8c8dab12e3 P1: gate members documents per unit, session auth on the admin API
documents.visible() now takes the viewer's unit set. A members document is
served and listed only to a signed-in member of the matching unit; 'both'
reaches any member; owner and admin reach everything including unit types
added later. Everyone else gets 404, never 403.

The gate moved INSIDE find(), so there is one path from a slug to a file and
no route can forget to check. listed() and visible() stay separate functions.

admin_api takes a session first and falls back to X-Admin-Token as break
glass. Still fails closed: no session and no ADMIN_TOKEN is 503. Capability,
not role, decides per route. announcements.created_by now comes from the
session and ignores any value in the request body.

tests/smoke_documents.py, 24 checks, including the invariant that the index
can never list something serving would refuse.
2026-09-04 12:09:19 -04:00

112 lines
4.8 KiB
Python

"""
smoke_documents.py - the members document gate, per unit.
Builds a throwaway /docs tree and manifest, then checks who can SEE and who can
LIST each document. Runs in-process, stdlib only.
python3 tests/smoke_documents.py
The rules under test are the ones that fail silently: a members document must
be unservable AND unlisted to the wrong viewer, unlisted must stay servable but
off the index, and listing must never be able to show something serving would
refuse.
"""
import json, os, sys, tempfile
TMP = tempfile.mkdtemp()
DOCS = os.path.join(TMP, "docs"); os.makedirs(DOCS)
os.environ["DOCS_DIR"] = DOCS
os.environ["STORE_DB"] = os.path.join(TMP, "smoke.db")
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "app"))
FILES = ["open.pdf", "hidden.pdf", "packonly.pdf", "trooponly.pdf", "bothunits.pdf"]
for f in FILES:
open(os.path.join(DOCS, f), "w").write("x")
json.dump({
"categories": [{"id": "forms", "title": "Forms", "blurb": ""}],
"documents": [
{"slug": "open", "file": "open.pdf", "title": "Open", "category": "forms",
"visibility": "public", "unit": "both"},
{"slug": "hidden", "file": "hidden.pdf", "title": "Hidden", "category": "forms",
"visibility": "unlisted", "unit": "both"},
{"slug": "packonly", "file": "packonly.pdf", "title": "Pack only", "category": "forms",
"visibility": "members", "unit": "pack"},
{"slug": "trooponly", "file": "trooponly.pdf", "title": "Troop only", "category": "forms",
"visibility": "members", "unit": "troop"},
{"slug": "bothunits", "file": "bothunits.pdf", "title": "Both", "category": "forms",
"visibility": "members", "unit": "both"},
]}, open(os.path.join(DOCS, "manifest.json"), "w"))
import documents as D
PASS = FAIL = 0
def check(label, cond):
global PASS, FAIL
if cond:
PASS += 1; print(" ok %s" % label)
else:
FAIL += 1; print(" FAIL %s" % label)
def served(units):
return {s for s in ("open", "hidden", "packonly", "trooponly", "bothunits")
if D.find(s, units) is not None}
def listed(units):
return {d["slug"] for _, rows in D.listing(units) for d in rows}
anon = frozenset()
pack = D.units_for({"global_role": None,
"memberships": [{"unit_type": "pack", "role": "member"}]})
troop = D.units_for({"global_role": None,
"memberships": [{"unit_type": "troop", "role": "leader"}]})
both = D.units_for({"global_role": None,
"memberships": [{"unit_type": "pack", "role": "member"},
{"unit_type": "troop", "role": "leader"}]})
admin = D.units_for({"global_role": "admin", "memberships": []})
crew = D.units_for({"global_role": None,
"memberships": [{"unit_type": "crew", "role": "leader"}]})
off = D.units_for({"global_role": "owner", "memberships": [], "disabled_at": "2026-01-01T00:00:00+00:00"})
print("units_for")
check("anonymous gets nothing", D.units_for(None) == frozenset())
check("pack member -> {pack}", pack == frozenset({"pack"}))
check("admin -> every unit type", admin == frozenset(D.DOC_UNITS))
check("crew maps to nothing (manifest has no crew)", crew == frozenset())
check("disabled owner gets nothing", off == frozenset())
print("\nserving")
check("anon: public and unlisted only", served(anon) == {"open", "hidden"})
check("pack member: + pack and both", served(pack) == {"open", "hidden", "packonly", "bothunits"})
check("troop leader: + troop and both", served(troop) == {"open", "hidden", "trooponly", "bothunits"})
check("in both units: everything", served(both) == set(FILES and
{"open", "hidden", "packonly", "trooponly", "bothunits"}))
check("admin: everything", served(admin) == {"open", "hidden", "packonly", "trooponly", "bothunits"})
check("pack member cannot reach troop doc", D.find("trooponly", pack) is None)
check("crew leader reaches no members doc", served(crew) == {"open", "hidden"})
print("\nlisting")
check("anon index is public only", listed(anon) == {"open"})
check("unlisted never listed, for anyone", "hidden" not in listed(admin))
check("pack member index", listed(pack) == {"open", "packonly", "bothunits"})
check("troop leader index", listed(troop) == {"open", "trooponly", "bothunits"})
check("admin index", listed(admin) == {"open", "packonly", "trooponly", "bothunits"})
print("\nlisting can never exceed serving")
for name, u in (("anon", anon), ("pack", pack), ("troop", troop), ("admin", admin), ("crew", crew)):
check("%s: listed is a subset of served" % name, listed(u) <= served(u))
print("\nmissing file on disk")
os.remove(os.path.join(DOCS, "packonly.pdf"))
D._cache["key"] = None
check("a members doc with no file is not served", D.find("packonly", pack) is None)
check("and not listed", "packonly" not in listed(pack))
print("\n%d passed, %d failed" % (PASS, FAIL))
sys.exit(1 if FAIL else 0)