import os import re import sys import tempfile import time import unittest from pathlib import Path from types import SimpleNamespace ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT)) import app as A # noqa: E402 from fastapi.testclient import TestClient # noqa: E402 OWNER_KEY = "test-owner-key-0123456789" LEGACY_KEY = "legacy-shared-key-xyz" UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/131.0 Safari/537.36" class ModAccessTests(unittest.TestCase): """One-time invite links -> per-device sessions; the old shared key grants nothing.""" def setUp(self): self.tmp = tempfile.TemporaryDirectory() self._saved = (A._MOD_STORE_PATH, A.SM_MOD_KEY, os.environ.get("SM_OWNER_KEY")) A._MOD_STORE_PATH = Path(self.tmp.name) / "mod_access.json" A._MOD = None A._MOD_LAST_SAVE = 0.0 A._MOD_LOGIN_FAILS.clear() A.SM_MOD_KEY = LEGACY_KEY os.environ["SM_OWNER_KEY"] = OWNER_KEY def tearDown(self): A._MOD_STORE_PATH, A.SM_MOD_KEY, owner = self._saved if owner is None: os.environ.pop("SM_OWNER_KEY", None) else: os.environ["SM_OWNER_KEY"] = owner A._MOD = None A._MOD_LOGIN_FAILS.clear() self.tmp.cleanup() # ── helpers ── def device(self, ip="10.0.0.1"): return TestClient(A.app, headers={"X-Forwarded-For": ip, "User-Agent": UA}) def owner(self, ip="10.0.0.1"): c = self.device(ip) r = c.post("/mod/admin/login", data={"key": OWNER_KEY, "name": "matteo-pc"}, follow_redirects=False) self.assertEqual(r.status_code, 303) self.assertIn(A.SM_MOD_SESSION_COOKIE, c.cookies) return c def invite(self, owner, name="Nice Face", role="mod"): r = owner.post("/mod/admin/action", data={"do": "invite", "name": name, "role": role, "hours": "24"}) self.assertEqual(r.status_code, 200) m = re.search(r"/mod/join\?t=([A-Za-z0-9_\-]+)", r.text) self.assertIsNotNone(m, "invite link not shown") return m.group(1) @staticmethod def sees_mod_ui(c): return 'data-tab="room"' in c.get("/").text # ── tests ── def test_old_shared_key_grants_nothing(self): c = self.device() r = c.get(f"/?mod={LEGACY_KEY}", follow_redirects=False) self.assertEqual(r.status_code, 303) self.assertNotIn(A.SM_MOD_SESSION_COOKIE, c.cookies) self.assertFalse(self.sees_mod_ui(c)) legacy = TestClient(A.app, cookies={"sm_mod": LEGACY_KEY}) self.assertFalse(self.sees_mod_ui(legacy)) for path in ("/room-scan", f"/room-scan?mod_key={LEGACY_KEY}", "/usage-admin", f"/room-lookup-log?mod_key={LEGACY_KEY}", "/power-series-admin"): self.assertEqual(legacy.get(path).status_code, 404, path) self.assertEqual(legacy.get("/room-lookup?code=ABCDEF").status_code, 502) def test_admin_is_hidden_and_owner_login_works(self): anon = self.device() self.assertEqual(anon.get("/mod/admin").status_code, 404) self.assertIn("Owner login", anon.get("/mod/admin?login").text) bad = anon.post("/mod/admin/login", data={"key": "wrong-key-wrong-key"}, follow_redirects=False) self.assertEqual(bad.status_code, 403) self.assertNotIn(A.SM_MOD_SESSION_COOKIE, anon.cookies) owner = self.owner() page = owner.get("/mod/admin") self.assertEqual(page.status_code, 200) self.assertIn("Devices with access (1)", page.text) self.assertIn("matteo-pc", page.text) self.assertTrue(self.sees_mod_ui(owner)) def test_owner_login_is_rate_limited_per_ip(self): attacker = self.device("6.6.6.6") for _ in range(5): attacker.post("/mod/admin/login", data={"key": "guess-guess-guess"}) blocked = attacker.post("/mod/admin/login", data={"key": OWNER_KEY}, follow_redirects=False) self.assertEqual(blocked.status_code, 403) self.owner("10.0.0.9") # a different network still gets in def test_owner_login_disabled_without_strong_key(self): os.environ["SM_OWNER_KEY"] = "short" c = self.device() self.assertEqual(c.post("/mod/admin/login", data={"key": "short"}, follow_redirects=False).status_code, 403) def test_invite_link_works_exactly_once_and_previews_dont_burn_it(self): token = self.invite(self.owner()) preview = self.device("162.159.0.1") # e.g. Discord's link preview fetcher self.assertEqual(preview.get(f"/mod/join?t={token}").status_code, 200) self.assertEqual(preview.get(f"/mod/join?t={token}").status_code, 200) self.assertNotIn(A.SM_MOD_SESSION_COOKIE, preview.cookies) mod = self.device("20.0.0.2") r = mod.post("/mod/join", data={"t": token}, follow_redirects=False) self.assertEqual(r.status_code, 303) self.assertTrue(self.sees_mod_ui(mod)) self.assertEqual(mod.get("/room-scan").status_code, 200) self.assertEqual(mod.get("/mod/admin").status_code, 404) # a mod is not the owner thief = self.device("30.0.0.3") self.assertEqual(thief.post("/mod/join", data={"t": token}).status_code, 404) self.assertEqual(thief.get(f"/mod/join?t={token}").status_code, 404) self.assertFalse(self.sees_mod_ui(thief)) def test_removed_device_loses_access_immediately(self): owner = self.owner() mod = self.device("20.0.0.2") mod.post("/mod/join", data={"t": self.invite(owner)}) self.assertTrue(self.sees_mod_ui(mod)) self.assertIn("Nice Face", owner.get("/mod/admin").text) sid = next(s["sid"] for s in A._mod_state()["sessions"].values() if s["name"] == "Nice Face") owner.post("/mod/admin/action", data={"do": "revoke_session", "sid": sid}) self.assertFalse(self.sees_mod_ui(mod)) self.assertEqual(mod.get("/room-scan").status_code, 404) self.assertTrue(self.sees_mod_ui(owner)) def test_cross_site_post_is_rejected(self): owner = self.owner() r = owner.post("/mod/admin/action", data={"do": "invite", "name": "evil"}, headers={"Origin": "https://evil.example"}) self.assertEqual(r.status_code, 404) self.assertFalse(A._mod_state()["invites"]) def test_expired_invite_and_idle_session_stop_working(self): owner = self.owner() token = self.invite(owner) A._mod_state()["invites"][A._mod_hash(token)]["expires_at"] = time.time() - 1 self.assertEqual(self.device().post("/mod/join", data={"t": token}).status_code, 404) mod = self.device("20.0.0.2") mod.post("/mod/join", data={"t": self.invite(owner, name="Idle Mod")}) rec = next(s for s in A._mod_state()["sessions"].values() if s["name"] == "Idle Mod") rec["last_seen"] = time.time() - (A.SM_MOD_SESSION_IDLE_DAYS + 1) * 86400 self.assertFalse(self.sees_mod_ui(mod)) self.assertTrue(rec.get("revoked_at")) def test_store_never_contains_raw_tokens(self): owner = self.owner() token = self.invite(owner) mod = self.device("20.0.0.2") mod.post("/mod/join", data={"t": token}) A._mod_save(force=True) raw = A._MOD_STORE_PATH.read_text(encoding="utf-8") self.assertNotIn(token, raw) self.assertNotIn(mod.cookies.get(A.SM_MOD_SESSION_COOKIE), raw) self.assertNotIn(owner.cookies.get(A.SM_MOD_SESSION_COOKIE), raw) self.assertNotIn(OWNER_KEY, raw) def test_new_network_for_same_device_is_logged(self): owner = self.owner() mod = self.device("20.0.0.2") mod.post("/mod/join", data={"t": self.invite(owner)}) stolen = TestClient(A.app, headers={"X-Forwarded-For": "99.1.2.3", "User-Agent": UA}, cookies={A.SM_MOD_SESSION_COOKIE: mod.cookies.get(A.SM_MOD_SESSION_COOKIE)}) rec = next(s for s in A._mod_state()["sessions"].values() if s["name"] == "Nice Face") rec["last_seen"] = time.time() - 600 # outside the 5-min touch throttle stolen.get("/") self.assertTrue(any(e["kind"] == "new_network" for e in A._mod_state()["events"])) self.assertIn("2 networks", owner.get("/mod/admin").text) def test_leaderboard_proxy_forwards_shared_key_only_for_mods(self): sent = [] class FakeSession: def get(self, url, cookies=None, timeout=None): sent.append(dict(cookies or {})) return SimpleNamespace(raise_for_status=lambda: None, json=lambda: {"rows": []}) saved = (A.SM_LEADERBOARD_PROXY_URL, A._sm_lb_proxy_healthy, A._SM_LB_PROXY_SESSION) try: A.SM_LEADERBOARD_PROXY_URL = "http://prices.example" A._sm_lb_proxy_healthy = lambda: True A._SM_LB_PROXY_SESSION = FakeSession() def req(cookies): return SimpleNamespace(cookies=cookies, url=SimpleNamespace(query="limit=1"), headers={"user-agent": UA}, client=None) A._sm_lb_proxy_fetch(req({"sm_mod": LEGACY_KEY}), "/leaderboard") owner = self.owner() A._sm_lb_proxy_fetch(req({A.SM_MOD_SESSION_COOKIE: owner.cookies.get(A.SM_MOD_SESSION_COOKIE)}), "/leaderboard") finally: A.SM_LEADERBOARD_PROXY_URL, A._sm_lb_proxy_healthy, A._SM_LB_PROXY_SESSION = saved self.assertEqual(sent[0], {}) # old key cookie from a browser: not forwarded self.assertEqual(sent[1], {"sm_mod": LEGACY_KEY}) # real mod: server adds the key itself if __name__ == "__main__": unittest.main()