""" smoke_admin.py - the P2 admin write paths against a throwaway DB. Runs in-process with no container, no network and no dependencies beyond the stdlib, so it can be run before anything is committed. STORE_DB=/tmp/x.db python3 tests/smoke_admin.py It covers the rules that are expensive to get wrong and invisible when they are: nearby rows bump verified_at on every save and deactivate rather than delete, unit edits are meeting fields only and unit-scoped, and settings accept only registered keys and fall back to the code default on anything absent or mangled. """ import os, sys, tempfile DB = os.environ.get("STORE_DB") or os.path.join(tempfile.mkdtemp(), "smoke.db") os.environ["STORE_DB"] = DB sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "app")) import identity as I import nearby as N import store as S 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 raises(label, status, exc, fn, *a, **kw): try: fn(*a, **kw) except exc as e: check("%s -> %d" % (label, status), e.status == status) return except Exception as e: check("%s -> %d (got %r)" % (label, status, e), False) return check("%s -> %d (no error raised)" % (label, status), False) print("db: %s\n" % DB) S.init() I.init() print("nearby: writes are validated") raises("bad unit_type", 422, S.NearbyRejected, S.create_nearby, {"unit_type": "trop", "unit_number": "1"}) raises("missing unit_number", 422, S.NearbyRejected, S.create_nearby, {"unit_type": "pack"}) raises("bad serves", 422, S.NearbyRejected, S.create_nearby, {"unit_type": "pack", "unit_number": "1", "serves": "everyone"}) raises("http link", 422, S.NearbyRejected, S.create_nearby, {"unit_type": "pack", "unit_number": "1", "link_url": "http://x.test"}) raises("link with an attribute breakout", 422, S.NearbyRejected, S.create_nearby, {"unit_type": "pack", "unit_number": "1", "link_url": 'https://x.test" onmouseover="alert(1)'}) raises("contact without @", 422, S.NearbyRejected, S.create_nearby, {"unit_type": "pack", "unit_number": "1", "contact": "call Steve"}) raises("contact with an attribute breakout", 422, S.NearbyRejected, S.create_nearby, {"unit_type": "pack", "unit_number": "1", "contact": 'a@b.test" onfocus="alert(1)'}) check("renderer escapes quotes as a second line", N._esc('a"b') == "a"b") raises("unknown field rejected, not dropped", 422, S.NearbyRejected, S.create_nearby, {"unit_type": "pack", "unit_number": "1", "unit_typo": "pack"}) raises("verified_at must be a date", 422, S.NearbyRejected, S.create_nearby, {"unit_type": "pack", "unit_number": "1", "verified_at": "yesterday"}) raises("sort_order must be an int", 422, S.NearbyRejected, S.create_nearby, {"unit_type": "pack", "unit_number": "1", "sort_order": "soon"}) print("\nnearby: create, update, verified_at") a = S.create_nearby({"unit_type": "pack", "unit_number": "244", "town": "Harleysville", "area": "North Penn", "serves": "family"}) check("row created", a and a["unit_number"] == "244") check("active and sort_order defaulted", a["active"] == 1 and a["sort_order"] == 100) check("verified_at defaults to today", a["verified_at"] == S._today()) b = S.create_nearby({"unit_type": "troop", "unit_number": "27", "area": "North Penn", "verified_at": "2026-08-01"}) check("explicit verified_at kept", b["verified_at"] == "2026-08-01") b2 = S.update_nearby(b["id"], {"town": "Lansdale"}) check("partial update lands", b2["town"] == "Lansdale" and b2["unit_number"] == "27") check("saving bumps verified_at", b2["verified_at"] == S._today()) b3 = S.update_nearby(b["id"], {"notes": "meets in the annexe", "verified_at": "2026-08-15"}) check("explicit verified_at wins on update", b3["verified_at"] == "2026-08-15") check("update of missing row is None", S.update_nearby("nope", {"town": "x"}) is None) raises("empty update", 422, S.NearbyRejected, S.update_nearby, b["id"], {}) print("\nnearby: deactivate, never delete") check("deactivate", S.deactivate_nearby(a["id"])) check("second deactivate is a no-op", not S.deactivate_nearby(a["id"])) check("row survives", S.get_nearby(a["id"])["active"] == 0) check("default list hides it", all(r["id"] != a["id"] for r in S.list_nearby())) check("include_inactive shows it", any(r["id"] == a["id"] for r in S.list_nearby(include_inactive=True))) back = S.update_nearby(a["id"], {"active": 1}) check("reactivate via update", back["active"] == 1) groups = N.listing() check("public page groups the live rows", len(groups) == 1 and groups[0][0] == "North Penn" and len(groups[0][1]) == 2) print("\nunits: meeting fields only, structured") pack = I.get_unit("pack73") check("seed row present", pack and pack["meets_weekday"] == 2) u = I.update_unit_meets("pack73", {"meets_time": "18:30"}) check("time updated", u["meets_time"] == "18:30") u = I.update_unit_meets(pack["id"], {"meets_weekday": 4, "meets_at": None}) check("update by id, null allowed", u["meets_weekday"] == 4 and u["meets_at"] is None) check("other fields untouched", u["display_name"] == pack["display_name"]) raises("weekday 8", 422, I.IdentityError, I.update_unit_meets, "pack73", {"meets_weekday": 8}) raises("weekday as bool", 422, I.IdentityError, I.update_unit_meets, "pack73", {"meets_weekday": True}) raises("display time string", 422, I.IdentityError, I.update_unit_meets, "pack73", {"meets_time": "7pm"}) raises("renaming is not a meeting edit", 422, I.IdentityError, I.update_unit_meets, "pack73", {"display_name": "Pack 99"}) raises("unknown unit", 404, I.IdentityError, I.update_unit_meets, "pack99", {"meets_time": "18:00"}) raises("nothing to update", 422, I.IdentityError, I.update_unit_meets, "pack73", {}) print("\nsettings: typed keys, default fallback") check("default when unset", I.get_setting("nearby_source_name") == "Continental District unit list") s = I.set_setting("nearby_source_url", "https://example.test/units", actor="a@example.test") check("set and read back", I.get_setting("nearby_source_url") == "https://example.test/units") check("set_setting reports itself", s["is_set"] and s["updated_by"] == "a@example.test") raises("unknown key", 422, I.IdentityError, I.set_setting, "nearby_src_url", "https://x.test") raises("blank value", 422, I.IdentityError, I.set_setting, "nearby_source_name", " ") raises("http url", 422, I.IdentityError, I.set_setting, "nearby_source_url", "http://x.test") cleared = I.set_setting("nearby_source_url", None) check("null clears to default", not cleared["is_set"] and I.get_setting("nearby_source_url") == "https://tinyurl.com/ContinentalScouts") con = I.connect() con.execute("INSERT INTO settings (key, value, updated_at) VALUES (?,?,?)", ("nearby_source_url", "ftp://mangled", I._now())) con.commit(); con.close() check("mangled row falls back to default", I.get_setting("nearby_source_url") == "https://tinyurl.com/ContinentalScouts") check("all_settings covers the registry", {s["key"] for s in I.all_settings()} == set(I.SETTINGS_KEYS)) print("\ncapabilities behind the new routes") leader = {"memberships": [{"unit_id": "u1", "role": "leader"}]} member = {"memberships": [{"unit_id": "u1", "role": "member"}]} admin = {"global_role": "admin", "memberships": []} check("leader can edit nearby", I.can(leader, "nearby:write")) check("member cannot", not I.can(member, "nearby:write")) check("leader edits own unit", I.can(leader, "unit:write_own", "u1")) check("but not the other one", not I.can(leader, "unit:write_own", "u2")) check("leader cannot touch settings", not I.can(leader, "settings:write")) check("admin spans units", I.can(admin, "unit:write_own", "u1") and I.can(admin, "unit:write_own", "u2")) check("admin holds settings:write", I.can(admin, "settings:write")) print("\nlan rule") import admin_api as A class _Req: def __init__(self, ip, cookie=None, bearer=None): self.headers = {"x-forwarded-for": ip} if ip else {} if bearer: self.headers["authorization"] = "Bearer " + bearer self.cookies = {"s73_session": cookie} if cookie else {} self.client = None for ip, want in (("10.0.0.55", True), ("10.0.1.20", True), ("127.0.0.1", True), ("172.19.0.31", True), ("108.36.248.87", False), ("192.168.1.9", False), ("", False), ("garbage", False)): check("client_is_lan(%r) is %s" % (ip, want), A.client_is_lan(_Req(ip)) == want) check("forged first hop does not make an outsider LAN: last hop wins", A.client_is_lan(_Req("10.0.0.1, 108.36.248.87")) is False) check("and a forged outside hop does not make a LAN caller outside", A.client_is_lan(_Req("203.0.113.9, 10.0.0.55")) is True) check("api_keys_from defaults to lan", I.get_setting("api_keys_from") == "lan") raises("api_keys_from rejects other values", 422, I.IdentityError, I.set_setting, "api_keys_from", "vpn") # a key from outside is refused while lan, allowed once anywhere, admin token never from outside A.ADMIN_TOKEN = "t" import uuid as _uuid con = I.connect() pid = str(_uuid.uuid4()) con.execute("INSERT INTO people (id, email, full_name, global_role, created_at) VALUES (?,?,?,?,?)", (pid, "lanrule@example.test", "Lan Rule", "admin", I._now())); con.commit(); con.close() person = I.get_person(pid) full, _row = I.mint_api_key(person, "outside", ["leads:read"]) def _hits(req, cap="leads:read"): try: A._auth(req, None, cap); return 200 except Exception as e: return getattr(e, "status_code", 500) check("key from the LAN passes", _hits(_Req("10.0.0.55", bearer=full)) == 200) check("key from outside is 403 while lan", _hits(_Req("108.36.248.87", bearer=full)) == 403) I.set_setting("api_keys_from", "anywhere", actor="test") check("key from outside passes once anywhere", _hits(_Req("108.36.248.87", bearer=full)) == 200) check("key still cannot exceed its scopes from anywhere", _hits(_Req("108.36.248.87", bearer=full), "nearby:write") == 403) I.set_setting("api_keys_from", None, actor="test") check("clearing the setting restores lan", I.get_setting("api_keys_from") == "lan" and _hits(_Req("108.36.248.87", bearer=full)) == 403) tok = I.start_session(pid) check("a session from outside is never restricted", _hits(_Req("108.36.248.87", cookie=tok)) == 200) def _tok(req): try: A._auth(req, "t", "leads:read"); return 200 except Exception as e: return getattr(e, "status_code", 500) check("admin token from the LAN passes", _tok(_Req("10.0.0.55")) == 200) check("admin token from outside is 403, regardless of the setting", _tok(_Req("108.36.248.87")) == 403) print("\ncalendar write-back") import calendar_write as C raises("title required", 422, C.CalendarRejected, C.clean, {"date": "2026-10-03"}) raises("date required", 422, C.CalendarRejected, C.clean, {"title": "x"}) raises("bad unit", 422, C.CalendarRejected, C.clean, {"title": "x", "date": "2026-10-03", "unit": "den"}) raises("end before start", 422, C.CalendarRejected, C.clean, {"title": "x", "date": "2026-10-03", "end": "2026-10-02"}) raises("bad time", 422, C.CalendarRejected, C.clean, {"title": "x", "date": "2026-10-03", "time": "1pm"}) raises("end_time without time", 422, C.CalendarRejected, C.clean, {"title": "x", "date": "2026-10-03", "end_time": "14:00"}) raises("end_time before time same day", 422, C.CalendarRejected, C.clean, {"title": "x", "date": "2026-10-03", "time": "14:00", "end_time": "13:00"}) ev = C.clean({"title": "Pack Hike; again", "unit": "Pack", "date": "2026-10-03", "end": "2026-10-03", "time": "13:00", "location": "Foy Park", "badge": "", "description": "Bring water,\nand a hat"}) check("clean normalises: unit lower, same-day end dropped, blank badge absent", ev["unit"] == "pack" and ev["end"] is None and ev["badge"] is None and ev["time"] == "13:00") uid = "t1" + C.UID_SUFFIX ics = C.build_ics(uid, ev, stamp="20260904T000000Z").decode() check("timed event carries VTIMEZONE and TZID start, 90-minute default end", "BEGIN:VTIMEZONE" in ics and "DTSTART;TZID=America/New_York:20261003T130000" in ics and "DTEND;TZID=America/New_York:20261003T143000" in ics) check("text is escaped and newlines encoded", "SUMMARY:Pack Hike\\; again" in ics and "DESCRIPTION:Bring water\\,\\nand a hat" in ics) check("unit is CATEGORIES, crlf line ends", "CATEGORIES:pack" in ics and ics.endswith("END:VCALENDAR\r\n") and ics.count("\n") == ics.count("\r\n")) allday = C.build_ics(uid, C.clean({"title": "Fall Campout", "unit": "both", "date": "2026-10-17", "end": "2026-10-18", "badge": "OVERNIGHT"}), stamp="20260904T000000Z").decode() check("all-day multi-day: VALUE=DATE with exclusive end, badge, no VTIMEZONE", "DTSTART;VALUE=DATE:20261017" in allday and "DTEND;VALUE=DATE:20261019" in allday and "X-SCOUT73-BADGE:OVERNIGHT" in allday and "VTIMEZONE" not in allday) multi = C.build_ics(uid, C.clean({"title": "x", "date": "2026-10-17", "end": "2026-10-18", "time": "16:00"}), stamp="20260904T000000Z").decode() check("timed multi-day with no end_time ends at noon on the end date, as seed.py did", "DTEND;TZID=America/New_York:20261018T120000" in multi) longt = C.build_ics(uid, C.clean({"title": "A" * 120, "date": "2026-10-03"}), stamp="20260904T000000Z").decode() check("long lines are folded at 75 octets", all(len(l.encode()) <= 75 for l in longt.split("\r\n"))) check("ownership by suffix", C.owns("x" + C.UID_SUFFIX) and not C.owns("abc@greenlanescouts73.org") and not C.owns("x@band.us") and not C.owns(None)) check("new uids are owned and unique", C.owns(C.new_uid()) and C.new_uid() != C.new_uid()) check("slug is stable and marked", C.slug("a" + C.UID_SUFFIX).startswith("site-") and C.slug("a" + C.UID_SUFFIX) == C.slug("a" + C.UID_SUFFIX)) # fail closed and never touch a foreign uid, with the transport watched calls = [] class _Resp: status = 201 def __enter__(self): return self def __exit__(self, *a): return False def _fake(req, timeout=None): calls.append((req.get_method(), req.full_url, req.get_header("Authorization") is not None)); return _Resp() C._urlopen = _fake C.RADICALE_URL = ""; C.RADICALE_USER = ""; C.RADICALE_PASS = "" raises("unconfigured put is 503", 503, C.CalendarRejected, C.put_event, uid, ev) check("and nothing was sent", calls == []) C.RADICALE_URL = "http://radicale.test/scouts/site73/"; C.RADICALE_USER = "scoutsite"; C.RADICALE_PASS = "p" raises("foreign uid put is 403", 403, C.CalendarRejected, C.put_event, "abc@greenlanescouts73.org", ev) raises("foreign uid delete is 403", 403, C.CalendarRejected, C.delete_event, "x@band.us") check("still nothing sent for foreign uids", calls == []) check("owned put goes to the slug with auth", C.put_event(uid, ev) == 201 and calls[-1][0] == "PUT" and calls[-1][1] == "http://radicale.test/scouts/site73/" + C.slug(uid) and calls[-1][2]) check("owned delete", C.delete_event(uid) == 201 and calls[-1][0] == "DELETE") print("\nseeded events are the site's too") check("both namespaces owned, nothing else", C.owns("x" + C.UID_SUFFIX) and C.owns("site73-abc123@greenlanescouts73.org") and not C.owns("abc@greenlanescouts73.org") and not C.owns("x@band.us") and not C.owns("site73-x@site73.example")) check("object name per namespace", C.object_name("a" + C.UID_SUFFIX) == C.slug("a" + C.UID_SUFFIX) and C.object_name("site73-abc@greenlanescouts73.org") == "site73-abc%40greenlanescouts73.org.ics") calls.clear(); C.put_event("site73-abc@greenlanescouts73.org", ev) check("seeded put goes to the seeder's object name", calls[-1][1].endswith("/site73-abc%40greenlanescouts73.org.ics")) print("\naction log") class _Sess: def __init__(self, tok): self.headers = {"x-forwarded-for": "10.0.0.5"}; self.cookies = {"s73_session": tok}; self.client = None req = _Sess(I.start_session(pid)) n0 = len(I.list_events(limit=500)) A.update_unit(req, "pack73", {"meets_time": "18:15"}, None) A.update_unit(req, "pack73", {"meets_time": "18:00"}, None) A.put_setting(req, "nearby_source_name", {"value": "Test source"}, None) A.put_setting(req, "nearby_source_name", {"value": None}, None) nb = A.create_nearby(req, {"unit_type": "pack", "unit_number": "ZZ1", "town": "A"}, None) A.update_nearby(req, nb["id"], {"town": "B"}, None) A.deactivate_nearby(req, nb["id"], None) an = A.create_announcement(req, {"message": "log me", "ends_at": "2036-01-02T00:00:00+00:00"}, None) A.revoke_announcement(req, an["id"], None) ev_rows = I.list_events(limit=500) kinds = [e["kind"] for e in ev_rows[:len(ev_rows) - n0]] check("every P2 write lands in the log", set(kinds) >= {"unit.updated", "setting.updated", "nearby.created", "nearby.updated", "nearby.deactivated", "announcement.created", "announcement.revoked"}) check("attributed to the person", all(e["actor_email"] == "lanrule@example.test" for e in ev_rows[:len(ev_rows) - n0])) unit_ev = [e for e in ev_rows if e["kind"] == "unit.updated"][-1] check("diff is before -> after", "-> '18:15'" in unit_ev["detail"] and "meets_time:" in unit_ev["detail"]) nb_ev = [e for e in ev_rows if e["kind"] == "nearby.updated"][0] check("nearby diff names the field", "town: 'A' -> 'B'" in nb_ev["detail"]) set_ev = [e for e in ev_rows if e["kind"] == "setting.updated"][0] check("clearing a setting is said so", "cleared to default" in set_ev["detail"]) check("kind prefix filter and limit", all(e["kind"].startswith("nearby.") for e in I.list_events(kind_prefix="nearby.")) and len(I.list_events(limit=2)) == 2) check("history is admin and above, and not scopable on a key", "history:read" in I.CAPS["admin"] and "history:read" not in I.CAPS["leader"] and "history:read" in I.KEY_UNSCOPABLE) print("\napi docs registry") import admin_api as A reg = A.describe_routes() check("registry is non-trivial", len(reg) >= 18) missing = [d for d in reg if not d["capability"] and d["path"] != "/api/admin/whoami"] check("every route's capability is read from its own _auth call (except whoami): %s" % ", ".join(d["path"] for d in missing), not missing) check("key routes gate on apikeys:own", all(d["capability"] == "apikeys:own" for d in reg if "/keys" in d["path"])) check("docs carry the handler docstring", all(d["doc"] for d in reg if "/keys" in d["path"])) class _R: headers = {"authorization": "", "x-forwarded-for": "127.0.0.1"}; cookies = {}; client = None os.environ["ADMIN_TOKEN"] = "t"; A.ADMIN_TOKEN = "t" page = A.api_docs(_R(), "t").body.decode() check("docs page renders every route", page.count("") == len(reg)) print("\n%d passed, %d failed" % (PASS, FAIL)) sys.exit(1 if FAIL else 0)