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
|
"""
"Resume where you left off" belongs to an account, not to a machine.
The position was stored as `mb:pos:<file>` in localStorage — per *device*. Sign
in with a second account on the same computer and the player offered to resume a
film that account had never opened. Wrong on its own terms, and a small
disclosure of what the other person watches: the offer only appears for files
someone has actually been through.
The functions are lifted out of `app.js` and run for real, rather than having
their source inspected, because the thing worth holding is the behaviour of two
accounts sharing one storage — which no assertion about the source text says.
"""
import json
import re
import shutil
import subprocess
from pathlib import Path
import pytest
STATIC = Path(__file__).resolve().parents[1] / "src" / "meshbay_hub" / "static"
APP = STATIC / "video-player.js"
pytestmark = pytest.mark.skipif(
shutil.which("node") is None or not APP.exists(),
reason="node or the SPA sources are not available")
WANTED = ("resumeKey", "readResumePosition", "writeResumePosition",
"purgeUnscopedResumePositions")
def _extract() -> str:
"""The real source of the functions under test, and nothing else.
`app.js` imports preact and cannot be loaded outside a browser, so the
declarations are sliced out by brace matching. A rename breaks this loudly,
which is the intent — a silently skipped test is worse than a failing one.
"""
source = APP.read_text(encoding="utf-8")
out = [
f"const RESUME_MIN_S = {_const(source, 'RESUME_MIN_S')};",
f"const RESUME_MAX_FRACTION = {_const(source, 'RESUME_MAX_FRACTION')};",
]
for name in WANTED:
start = source.index(f"function {name}(")
depth, i = 0, source.index("{", start)
while True:
if source[i] == "{":
depth += 1
elif source[i] == "}":
depth -= 1
if depth == 0:
break
i += 1
out.append(source[start:i + 1])
return "\n".join(out)
def _const(source: str, name: str) -> str:
match = re.search(rf"^const {name} = ([^;]+);", source, re.M)
assert match, f"{name} is gone or was renamed"
return match.group(1)
def _run(body: str, tmp_path: Path):
script = tmp_path / "case.mjs"
script.write_text(
"const store = new Map();\n"
"globalThis.localStorage = {\n"
" get length() { return store.size; },\n"
" key: i => Array.from(store.keys())[i] ?? null,\n"
" getItem: k => (store.has(k) ? store.get(k) : null),\n"
" setItem: (k, v) => store.set(k, String(v)),\n"
" removeItem: k => store.delete(k),\n"
"};\n"
# Whoever is signed in, swapped by the cases below.
"let AUTH = null;\n"
"function loadAuth() { return AUTH; }\n"
f"{_extract()}\n"
"const out = [];\n"
"const say = (...a) => out.push(...a);\n"
"const keys = () => Array.from(store.keys()).sort();\n"
f"{body}\n"
"console.log(JSON.stringify(out));\n",
encoding="utf-8")
proc = subprocess.run(["node", str(script)], capture_output=True, text=True)
assert proc.returncode == 0, proc.stderr
return json.loads(proc.stdout)
# ── The bug ─────────────────────────────────────────────────────────────────
def test_a_second_account_is_not_offered_the_firsts_position(tmp_path):
"""The report: a brand-new account was offered a resume point."""
assert _run("""
AUTH = { userId: 'alice' };
writeResumePosition('film1', 600, 7200);
say(readResumePosition('film1'));
AUTH = { userId: 'bob' };
say(readResumePosition('film1'));
""", tmp_path) == [600, 0]
def test_each_account_keeps_its_own_place_in_the_same_film(tmp_path):
"""Two people watching one film on one machine is the ordinary case, and
neither should move the other's bookmark."""
assert _run("""
AUTH = { userId: 'alice' }; writeResumePosition('film1', 600, 7200);
AUTH = { userId: 'bob' }; writeResumePosition('film1', 1800, 7200);
AUTH = { userId: 'alice' }; say(readResumePosition('film1'));
AUTH = { userId: 'bob' }; say(readResumePosition('film1'));
""", tmp_path) == [600, 1800]
def test_nothing_is_written_when_nobody_is_signed_in(tmp_path):
assert _run("""
AUTH = null;
writeResumePosition('film1', 600, 7200);
say(keys().length, readResumePosition('film1'));
""", tmp_path) == [0, 0]
def test_positions_written_before_the_fix_are_dropped(tmp_path):
"""
They cannot be re-keyed: there is no record of whose they were, and guessing
hands them to whoever signs in next, which is the bug itself.
"""
assert _run("""
localStorage.setItem('mb:pos:film1', '600'); // the old shape
localStorage.setItem('mb:pos:alice:film2', '900'); // the new one
localStorage.setItem('mb_auth', '{}'); // nothing to do with this
purgeUnscopedResumePositions();
say(...keys());
""", tmp_path) == ["mb:pos:alice:film2", "mb_auth"]
# ── What must still hold ────────────────────────────────────────────────────
def test_a_glance_at_the_opening_is_not_a_bookmark(tmp_path):
assert _run("""
AUTH = { userId: 'alice' };
writeResumePosition('film1', 12, 7200);
say(readResumePosition('film1'));
""", tmp_path) == [0]
def test_a_film_watched_to_the_end_stops_offering_to_resume(tmp_path):
"""And the earlier bookmark goes with it, rather than sitting there
offering the last thirty seconds forever."""
assert _run("""
AUTH = { userId: 'alice' };
writeResumePosition('film1', 3600, 7200);
writeResumePosition('film1', 7150, 7200);
say(readResumePosition('film1'), keys().length);
""", tmp_path) == [0, 0]
|