aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/tests/test_browser_idle_signout.py
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-15 02:16:39 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-15 02:21:01 +0200
commit73ad8e4eb566fe682107fa7e50ef624591199e99 (patch)
treeff0017d014d46d8835487c080dca55c6def7fd6b /packages/meshbay-hub/tests/test_browser_idle_signout.py
parentbdefcd025604f2c3009fe5e0cc01213c2ba62a6a (diff)
downloadmeshbay-73ad8e4eb566fe682107fa7e50ef624591199e99.tar.gz
feat(hub): session lifetime is an admin setting, and a browser signs out when idle
Browser idle sign-out (media playback counts as activity; not the desktop app), refresh idle window and maximum session length, in hours. Sign-out now revokes on the hub, and the profile has "sign out everywhere". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XuNrwLf5EFWCMHzfoEvnpm
Diffstat (limited to 'packages/meshbay-hub/tests/test_browser_idle_signout.py')
-rw-r--r--packages/meshbay-hub/tests/test_browser_idle_signout.py130
1 files changed, 130 insertions, 0 deletions
diff --git a/packages/meshbay-hub/tests/test_browser_idle_signout.py b/packages/meshbay-hub/tests/test_browser_idle_signout.py
new file mode 100644
index 0000000..50f7f04
--- /dev/null
+++ b/packages/meshbay-hub/tests/test_browser_idle_signout.py
@@ -0,0 +1,130 @@
+"""
+A browser signs itself out after a stretch with nobody at it (design ยง7.7).
+
+`idle.js` is executed, not read: a fake document and localStorage stand in for
+the browser, and a clock the test moves stands in for time. What is modelled is
+the environment โ€” the module under test is the real file. The wiring in app.js
+is checked by reading it, the only evidence there is for the shell.
+"""
+
+import json
+import re
+import shutil
+import subprocess
+from pathlib import Path
+
+import pytest
+
+STATIC = Path(__file__).resolve().parents[1] / "src" / "meshbay_hub" / "static"
+IDLE = STATIC / "idle.js"
+APP = STATIC / "app.js"
+NODE = shutil.which("node") or ("/opt/nodejs/bin/node"
+ if Path("/opt/nodejs/bin/node").exists() else None)
+
+HARNESS = r"""
+const store = new Map();
+globalThis.localStorage = {
+ getItem: (k) => (store.has(k) ? store.get(k) : null),
+ setItem: (k, v) => store.set(k, String(v)),
+};
+const media = [];
+globalThis.document = {
+ querySelectorAll: () => media, addEventListener() {}, removeEventListener() {},
+};
+globalThis.window = { addEventListener() {}, removeEventListener() {} };
+let now = 1_700_000_000_000;
+Date.now = () => now;
+const ticks = [];
+globalThis.setInterval = (fn) => { ticks.push(fn); return ticks.length; };
+globalThis.clearInterval = () => {};
+const HOUR = 3600e3;
+const { startIdleWatch, markActive, LAST_ACTIVE_KEY } = await import(process.argv[1]);
+const out = {};
+
+// Signed in just now, then left alone.
+markActive(true);
+let fired = 0;
+const stop1 = startIdleWatch(HOUR, () => fired++);
+now += HOUR - 1000; ticks.at(-1)(); out.justBefore = fired;
+now += 2000; ticks.at(-1)(); out.justAfter = fired;
+ticks.at(-1)(); out.firesOnce = fired;
+stop1();
+
+// A film nobody touches for two hours, then paused and left.
+store.set(LAST_ACTIVE_KEY, String(now));
+let filmFired = 0;
+startIdleWatch(HOUR, () => filmFired++);
+media.push({ paused: false, ended: false });
+for (let i = 0; i < 120; i++) { now += 60e3; ticks.at(-1)(); }
+out.duringFilm = filmFired;
+media[0].paused = true;
+now += HOUR + 60e3; ticks.at(-1)();
+out.afterPause = filmFired;
+media.length = 0;
+
+// A browser closed without signing out and opened again the next day.
+store.set(LAST_ACTIVE_KEY, String(now - 20 * HOUR));
+let reopened = 0;
+startIdleWatch(HOUR, () => reopened++);
+out.reopened = reopened;
+
+// A browser that has never recorded anything is not idle.
+store.delete(LAST_ACTIVE_KEY);
+let fresh = 0;
+startIdleWatch(HOUR, () => fresh++);
+out.noRecord = fresh;
+
+console.log(JSON.stringify(out));
+"""
+
+
+@pytest.fixture(scope="module")
+def outcome():
+ if NODE is None:
+ pytest.skip("node is not available")
+ proc = subprocess.run(
+ [NODE, "--input-type=module", "--eval", HARNESS, IDLE.as_uri()],
+ capture_output=True, text=True, timeout=30)
+ assert proc.returncode == 0, proc.stderr
+ return json.loads(proc.stdout.strip().splitlines()[-1])
+
+
+def test_nobody_at_it_for_the_delay_signs_out_once(outcome):
+ assert outcome["justBefore"] == 0
+ assert outcome["justAfter"] == 1
+ assert outcome["firesOnce"] == 1
+
+
+def test_a_film_playing_is_somebody_watching(outcome):
+ assert outcome["duringFilm"] == 0, "two hours of film signed the browser out"
+ assert outcome["afterPause"] == 1, "a paused film kept the session forever"
+
+
+def test_a_browser_closed_without_signing_out_is_caught_when_reopened(outcome):
+ assert outcome["reopened"] == 1
+
+
+def test_a_browser_with_no_record_is_not_idle(outcome):
+ assert outcome["noRecord"] == 0
+
+
+def test_the_desktop_application_is_not_watched():
+ src = APP.read_text(encoding="utf-8")
+ m = re.search(r"useEffect\(\(\) => \{\n(.*?)startIdleWatch\(", src, re.S)
+ assert m, "app.js no longer starts the idle watch in an effect"
+ assert "platform.isNative" in m.group(1)
+
+
+def test_a_sign_in_resets_the_clock_before_the_session_lands():
+ src = APP.read_text(encoding="utf-8")
+ login = src[src.index("login: async (username, password)"):src.index("logout: () =>")]
+ assert login.index("markActive(true)") < login.index("setAuth(u)"), (
+ "the idle watch would read the previous user's last-active time")
+
+
+def test_signing_out_revokes_on_the_hub_before_forgetting_the_token():
+ src = APP.read_text(encoding="utf-8")
+ logout = src[src.index("logout: () =>"):]
+ logout = logout[:logout.index("},")]
+ assert logout.index("logoutOnHub()") < logout.index("setAuth(null)"), (
+ "the refresh token is cleared before it can be sent for revocation")