1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
|
#!/usr/bin/env python3
"""
A group link, `#/name@owner[/path]`, opened in the real application.
`test_group_link.py` runs `group-link.js` on its own; this is where it meets
the router, the account's group list and the sign-in state. Loads the shipped
`app.js` in a real browser with `fetch` stubbed (no node answers, so a group
page goes as far as "offline"), once per case:
handle — `#/demo@someowner`: the group page, the address left as it is
uuid — `#/group/<id>`: the same page, the address showing the handle
file — `#/demo@someowner/<path>`: the page, the path kept in the address
until the index can say what it is (no node here: never)
unknown — `#/demo@stranger1`: not among the account's groups
signed_out — `#/demo@someowner/<path>` with no session: the sign-in form, and
the address untouched under it
group_link_probe.py
Prints JSON: one object per case.
"""
import http.server
import json
import socketserver
import subprocess
import sys
import tempfile
import threading
import time
from pathlib import Path
STATIC = Path(__file__).resolve().parents[2] / "src" / "meshbay_hub" / "static"
PORT = 8774
RECORDS = []
socketserver.TCPServer.allow_reuse_address = True
GROUP = "0f8fad5b-d9cb-469f-a165-70867728950e"
FILE = "backup/city_2015/backup/IMG_0001.JPG"
LINKS = {
"handle": "#/demo@someowner",
"uuid": f"#/group/{GROUP}",
"file": f"#/demo@someowner/{FILE}",
"unknown": "#/demo@stranger1",
"signed_out": f"#/demo@someowner/{FILE}",
}
CASES = list(LINKS)
PAGE = r"""<!doctype html><html><head><meta charset=utf-8></head><body>
<div id="app"></div>
<script type="module">
const CASE = new URLSearchParams(location.search).get('case');
const LINKS = __LINKS__;
const realFetch = window.fetch.bind(window);
const post = (o) => realFetch('/log', { method: 'POST', body: JSON.stringify(o) });
const calls = [];
const json = (body, status = 200) => ({
ok: status < 400, status, statusText: '', headers: new Headers(),
json: async () => body, text: async () => JSON.stringify(body),
});
window.fetch = async (url, init = {}) => {
const u = String(url);
calls.push(u);
if (u.includes('/v1/users/me/preferences')) return json({});
if (u.includes('/v1/users/me')) return json({ user_id: 'u-1', role: 'user' });
if (u.includes('/v1/groups/mine')) return json({ groups: [
{ id: '__GROUP__', name: 'demo', owner_username: 'someowner', visibility: 'private',
created_at: '2026-01-01T00:00:00+00:00', description: '' },
{ id: 'other-group', name: 'demo', owner_username: 'otherowner', visibility: 'private',
created_at: '2026-01-01T00:00:00+00:00', description: '' },
] });
if (u.includes('/nodes')) return json({ nodes: [] });
return json({});
};
if (CASE === 'signed_out') {
localStorage.removeItem('mb_auth');
} else {
localStorage.setItem('mb_auth', JSON.stringify({
username: 'member-account', userId: 'u-1', token: 'tok', refreshToken: 'ref',
role: 'user' }));
}
sessionStorage.clear();
history.replaceState(null, '', '/?case=' + CASE + LINKS[CASE]);
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
(async () => {
const out = { case: CASE };
try {
await import('/app.js');
await wait(2000);
out.hash = decodeURI(location.hash);
out.history_length = history.length;
out.group_page = Boolean(document.querySelector('.group-header'));
out.group_title = (document.querySelector('.group-header h2') || {}).innerText || '';
out.active_sidebar = [...document.querySelectorAll('.sidebar-group.active')]
.map((a) => a.getAttribute('href'));
out.message = (document.querySelector('main .page-message') || {}).innerText || '';
out.spinner = Boolean(document.querySelector('main .page-message .spinner'));
out.login_form = Boolean(document.querySelector('input[type=password]'));
out.hub_calls = calls.map((c) => c.replace(/^https?:\/\/[^/]+/, ''));
} catch (e) {
out.error = String(e && e.stack || e);
}
post(out);
})();
</script></body></html>
""".replace("__GROUP__", GROUP).replace("__LINKS__", json.dumps(LINKS))
class H(http.server.SimpleHTTPRequestHandler):
def log_message(self, *a):
pass
def do_POST(self):
length = int(self.headers.get("Content-Length") or 0)
body = self.rfile.read(length)
if self.path == "/log":
RECORDS.append(json.loads(body.decode()))
self.send_response(204)
self.end_headers()
def _send(self, body: bytes, ctype: str) -> None:
self.send_response(200)
self.send_header("Content-Type", ctype)
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def do_GET(self):
path = self.path.split("?")[0]
if path == "/":
self._send(PAGE.encode(), "text/html; charset=utf-8")
return
asset = (STATIC / path.lstrip("/")).resolve()
if not str(asset).startswith(str(STATIC)) or not asset.is_file():
self.send_response(404)
self.end_headers()
return
ctype = "text/javascript" if asset.suffix in (".js", ".mjs") else (
"application/wasm" if asset.suffix == ".wasm" else "application/octet-stream")
self._send(asset.read_bytes(), ctype)
def _run(case: str) -> dict | None:
before = len(RECORDS)
with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as profile:
proc = subprocess.Popen(
["google-chrome", "--headless=new", "--disable-gpu", "--no-sandbox",
f"--user-data-dir={profile}", f"http://127.0.0.1:{PORT}/?case={case}"],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
for _ in range(300):
if len(RECORDS) > before:
break
time.sleep(0.1)
proc.terminate()
try:
proc.wait(timeout=10)
except subprocess.TimeoutExpired:
proc.kill()
proc.wait()
return RECORDS[before] if len(RECORDS) > before else None
def main() -> int:
with socketserver.TCPServer(("127.0.0.1", PORT), H) as srv:
threading.Thread(target=srv.serve_forever, daemon=True).start()
results = [_run(case) for case in CASES]
if not all(results):
print(json.dumps({"error": "no measurement", "got": results}), file=sys.stderr)
return 1
print(json.dumps(results, indent=1))
return 0
if __name__ == "__main__":
raise SystemExit(main())
|