""" 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")