161 lines
7.3 KiB
Python
161 lines
7.3 KiB
Python
import json
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
|
|
SOURCE = Path(__file__).parents[1] / "frontend" / "today-lock-screen.js"
|
|
ROOT = SOURCE.parents[1]
|
|
|
|
|
|
def run_scenario(scenario: str) -> dict:
|
|
script = SOURCE.read_text() + r"""
|
|
const values = new Map();
|
|
const messages = [];
|
|
const listeners = {};
|
|
const control = {checked:false, disabled:false, addEventListener:(name, fn)=>listeners[name]=fn};
|
|
const status = {textContent:''};
|
|
const serviceWorker = {
|
|
controller:{postMessage:message=>messages.push(message)},
|
|
ready:Promise.resolve({active:{postMessage:message=>messages.push(message)}}),
|
|
addEventListener:(name, fn)=>listeners['sw-' + name]=fn,
|
|
};
|
|
const storage = {getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)};
|
|
let permission = 'default';
|
|
const NotificationRef = {get permission(){return permission;},requestPermission:async()=>{permission='granted';return permission;}};
|
|
const actions = [];
|
|
const locationRef = {href:'https://forge.example/dashboard/#/my-work/today'};
|
|
const historyRef = {replaceState:(_a,_b,url)=>{locationRef.href=new URL(url, locationRef.href).href;}};
|
|
let tokenCounter = 0;
|
|
const tokens = ['opaque-token-1234567890', 'new-opaque-token-0987654321'];
|
|
(async()=>{
|
|
const lockScreen = createTodayLockScreen({storage,getLogin:()=> 'Timmy',serviceWorker,NotificationRef,control,status,locationRef,historyRef,randomToken:()=> tokens[tokenCounter++],fingerprint:async (_token, identity)=>identity.startsWith('issue:') ? 'opaque-issue-fingerprint' : 'opaque-pull-fingerprint',onAction:(action, identity)=>actions.push([action, identity])});
|
|
%SCENARIO%
|
|
})().catch(error=>{console.error(error);process.exit(1)});
|
|
""".replace("%SCENARIO%", scenario)
|
|
completed = subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True)
|
|
return json.loads(completed.stdout)
|
|
|
|
|
|
def test_operator_opt_in_syncs_one_privacy_safe_session_notification():
|
|
result = run_scenario(r"""
|
|
await lockScreen.enable();
|
|
await lockScreen.sync({identity:'issue:secret/repo:42:',running:true,elapsed_ms:125000}, true);
|
|
process.stdout.write(JSON.stringify({checked:control.checked,status:status.textContent,messages,stored:[...values.entries()]}));
|
|
""")
|
|
|
|
assert result["checked"] is True
|
|
assert result["status"] == "Lock-screen Today controls are on."
|
|
assert result["stored"] == [
|
|
["stackchain.today-lock-screen.v1.timmy", "1"],
|
|
["stackchain.today-lock-screen-action.v1.timmy", '{"token":"opaque-token-1234567890","fingerprint":"opaque-issue-fingerprint"}'],
|
|
]
|
|
assert result["messages"][-1] == {
|
|
"type": "stackchain-today-lock-screen",
|
|
"active": True,
|
|
"running": True,
|
|
"actionToken": "opaque-token-1234567890",
|
|
}
|
|
assert "secret" not in json.dumps(result)
|
|
|
|
|
|
def test_disabling_or_ending_session_removes_notification():
|
|
result = run_scenario(r"""
|
|
await lockScreen.enable();
|
|
await lockScreen.sync({identity:'issue:r:42:',running:false,elapsed_ms:0}, false);
|
|
await lockScreen.disable();
|
|
process.stdout.write(JSON.stringify({checked:control.checked,messages,stored:[...values.entries()]}));
|
|
""")
|
|
|
|
assert result["checked"] is False
|
|
assert result["stored"] == []
|
|
assert result["messages"][-2:] == [
|
|
{"type": "stackchain-today-lock-screen", "active": False, "running": False},
|
|
{"type": "stackchain-today-lock-screen", "active": False, "running": False},
|
|
]
|
|
|
|
|
|
def test_valid_notification_action_is_consumed_once_and_removed_from_url():
|
|
result = run_scenario(r"""
|
|
values.set('stackchain.today-lock-screen.v1.timmy','1');
|
|
locationRef.href='https://forge.example/dashboard/?today_timer_action=pause#/my-work/today';
|
|
const consumed = await lockScreen.consumeLaunchAction();
|
|
const second = await lockScreen.consumeLaunchAction();
|
|
listeners['sw-message']({data:{type:'stackchain-today-timer-action',action:'resume'}});
|
|
process.stdout.write(JSON.stringify({consumed,second,actions,href:locationRef.href}));
|
|
""")
|
|
|
|
assert result == {
|
|
"consumed": True,
|
|
"second": False,
|
|
"actions": [["pause", None], ["resume", None]],
|
|
"href": "https://forge.example/dashboard/#/my-work/today",
|
|
}
|
|
|
|
|
|
def test_finish_action_is_bound_to_the_exact_active_identity_and_consumed_once():
|
|
result = run_scenario(r"""
|
|
await lockScreen.enable();
|
|
await lockScreen.sync({identity:'issue:private/repo:42:',running:true}, true);
|
|
const first = await lockScreen.consumeAction('complete', 'opaque-token-1234567890');
|
|
const replay = await lockScreen.consumeAction('complete', 'opaque-token-1234567890');
|
|
await lockScreen.sync({identity:'pull:private/repo:9:',running:true}, true);
|
|
const stale = await lockScreen.consumeAction('complete', 'opaque-token-1234567890');
|
|
process.stdout.write(JSON.stringify({first,replay,stale,actions,messages,stored:[...values.entries()]}));
|
|
""")
|
|
|
|
assert result["first"] is True
|
|
assert result["replay"] is False
|
|
assert result["stale"] is False
|
|
assert result["actions"] == [["complete", "issue:private/repo:42:"]]
|
|
assert all("private/repo" not in json.dumps(message) for message in result["messages"])
|
|
|
|
|
|
def test_cold_launch_finish_action_is_removed_from_history_before_completion():
|
|
result = run_scenario(r"""
|
|
await lockScreen.enable();
|
|
await lockScreen.sync({identity:'issue:private/repo:42:',running:true}, true);
|
|
locationRef.href='https://forge.example/dashboard/?today_timer_action=complete&today_action_token=opaque-token-1234567890#/my-work/today';
|
|
const consumed = await lockScreen.consumeLaunchAction();
|
|
process.stdout.write(JSON.stringify({consumed,actions,href:locationRef.href}));
|
|
""")
|
|
|
|
assert result == {
|
|
"consumed": True,
|
|
"actions": [["complete", "issue:private/repo:42:"]],
|
|
"href": "https://forge.example/dashboard/#/my-work/today",
|
|
}
|
|
|
|
|
|
def test_permission_denial_is_truthful_and_does_not_persist_opt_in():
|
|
result = run_scenario(r"""
|
|
NotificationRef.requestPermission=async()=>{permission='denied';return permission;};
|
|
const enabled = await lockScreen.enable();
|
|
process.stdout.write(JSON.stringify({enabled,checked:control.checked,status:status.textContent,stored:[...values.entries()]}));
|
|
""")
|
|
|
|
assert result == {
|
|
"enabled": False,
|
|
"checked": False,
|
|
"status": "Lock-screen controls are blocked in browser settings.",
|
|
"stored": [],
|
|
}
|
|
|
|
|
|
def test_lock_screen_flow_is_wired_into_the_packaged_today_journey():
|
|
index = (ROOT / "frontend" / "index.html").read_text()
|
|
dashboard = (ROOT / "frontend" / "dashboard.js").read_text()
|
|
bundle = (ROOT / "src" / "frontend_bundle.py").read_text()
|
|
|
|
assert 'id="today-lock-screen"' in index
|
|
assert 'id="today-lock-screen-status"' in index
|
|
assert '<script src="static/today-lock-screen.js"></script>' in index
|
|
assert index.index('static/today-lock-screen.js') < index.index('static/dashboard.js')
|
|
assert 'createTodayLockScreen({' in dashboard
|
|
assert "action === 'complete'" in dashboard
|
|
assert "completeTodayItem(item)" in dashboard
|
|
assert "todayLockScreen.consumeLaunchAction()" in dashboard
|
|
assert dashboard.index("const completeTodayItem = createTodayCompletion({") < dashboard.index("todayLockScreen.consumeLaunchAction()")
|
|
assert "todayLockScreen.sync(snapshot, workSession.checkpointed())" in dashboard
|
|
assert "todayLockScreen.consumeLaunchAction()" in dashboard
|
|
assert '"static/today-lock-screen.js"' in bundle
|