136 lines
4.8 KiB
Python
136 lines
4.8 KiB
Python
import json
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from tests.dashboard_bundle import dashboard
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
OUTBOX = ROOT / "frontend" / "notification-read-outbox.js"
|
|
BACKGROUND_SYNC = ROOT / "frontend" / "background-issue-sync.js"
|
|
WORKER = ROOT / "frontend" / "service-worker.js"
|
|
|
|
|
|
def run_node(script):
|
|
result = subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True)
|
|
return json.loads(result.stdout)
|
|
|
|
|
|
def test_offline_read_admission_is_account_bound_deduplicated_and_suppresses_saved_updates():
|
|
script = f"""
|
|
const createOutbox = require({json.dumps(str(OUTBOX))});
|
|
const values = new Map();
|
|
const storage = {{
|
|
getItem:key => values.has(key) ? values.get(key) : null,
|
|
setItem:(key,value) => values.set(key,value),
|
|
}};
|
|
const reconciled = [];
|
|
const backgroundSync = {{
|
|
reconcile: async (items, lane) => reconciled.push([lane, items]),
|
|
requestSync: async () => true,
|
|
}};
|
|
const outbox = createOutbox({{
|
|
storage, backgroundSync, getOwnerLogin:() => 'timmy', now:() => 1234,
|
|
}});
|
|
(async () => {{
|
|
const first = await outbox.enqueueDurably(42);
|
|
const duplicate = await outbox.enqueueDurably(42);
|
|
const visible = outbox.suppress([
|
|
{{notification_id:42, title:'Queued'}}, {{notification_id:43, title:'Visible'}},
|
|
]);
|
|
process.stdout.write(JSON.stringify({{
|
|
first, duplicate, items:outbox.list(), visible, reconciled,
|
|
}}));
|
|
}})();
|
|
"""
|
|
output = run_node(script)
|
|
|
|
assert output["first"]["item"]["notificationId"] == 42
|
|
assert output["duplicate"]["item"]["id"] == output["first"]["item"]["id"]
|
|
assert len(output["items"]) == 1
|
|
assert output["items"][0]["ownerLogin"] == "timmy"
|
|
assert [item["notification_id"] for item in output["visible"]] == [43]
|
|
assert output["reconciled"][-1][0] == "notification-read"
|
|
assert len(output["reconciled"][-1][1]) == 1
|
|
|
|
|
|
def test_foreground_flush_patches_each_unique_read_and_keeps_transient_failures_queued():
|
|
script = f"""
|
|
const createOutbox = require({json.dumps(str(OUTBOX))});
|
|
const values = new Map();
|
|
const storage = {{getItem:key => values.get(key) || null,setItem:(key,value) => values.set(key,value)}};
|
|
const calls = [];
|
|
const outbox = createOutbox({{
|
|
storage, getOwnerLogin:() => 'timmy',
|
|
fetchJson: async (url, options) => {{
|
|
calls.push([url, options.method]);
|
|
if (url.includes('/43/')) {{ const error = new Error('offline'); error.status = 503; throw error; }}
|
|
return {{ok:true}};
|
|
}},
|
|
}});
|
|
(async () => {{
|
|
await outbox.enqueueDurably(42);
|
|
await outbox.enqueueDurably(43);
|
|
const result = await outbox.flush('timmy');
|
|
process.stdout.write(JSON.stringify({{calls, result, items:outbox.list()}}));
|
|
}})();
|
|
"""
|
|
output = run_node(script)
|
|
|
|
assert output["calls"] == [
|
|
["api/v1/notifications/42/read", "PATCH"],
|
|
["api/v1/notifications/43/read", "PATCH"],
|
|
]
|
|
assert output["result"]["confirmed"] == [42]
|
|
assert [item["notificationId"] for item in output["items"]] == [43]
|
|
assert output["items"][0]["status"] == "queued"
|
|
|
|
|
|
def test_background_delivery_uses_patch_and_reports_read_receipt():
|
|
script = f"""
|
|
const createSync = require({json.dumps(str(BACKGROUND_SYNC))});
|
|
const requests = [];
|
|
const store = {{
|
|
upsert:async () => {{}}, claim:async () => ({{
|
|
id:'notification-read:timmy:42', kind:'notification-read', notificationId:42,
|
|
ownerLogin:'timmy', status:'sending',
|
|
}}), complete:async () => {{}}, release:async () => {{}}, fail:async () => {{}},
|
|
}};
|
|
const sync = createSync({{
|
|
store,
|
|
fetchJson:async (url, options) => {{ requests.push([url, options.method, options.body || null]); return {{ok:true}}; }},
|
|
}});
|
|
(async () => {{
|
|
const result = await sync.send({{
|
|
id:'notification-read:timmy:42', kind:'notification-read', notificationId:42,
|
|
ownerLogin:'timmy', status:'queued',
|
|
}}, 'timmy');
|
|
process.stdout.write(JSON.stringify({{requests, result}}));
|
|
}})();
|
|
"""
|
|
output = run_node(script)
|
|
|
|
assert output["requests"] == [["api/v1/notifications/42/read", "PATCH", None]]
|
|
assert output["result"]["receipt"] == {
|
|
"id": "notification-read:timmy:42",
|
|
"status": "confirmed",
|
|
"kind": "notification-read",
|
|
"route": "#/my-work/updates",
|
|
}
|
|
|
|
|
|
@pytest.mark.anyio
|
|
async def test_dashboard_wires_offline_queue_read_next_through_precached_background_outbox():
|
|
html = await dashboard()
|
|
worker = WORKER.read_text()
|
|
|
|
assert '<script src="static/notification-read-outbox.js"></script>' in html
|
|
assert "const notificationReadOutbox = createNotificationReadOutbox({" in html
|
|
assert "queueRead: notificationId => notificationReadOutbox.enqueueDurably(notificationId)" in html
|
|
assert "notificationReadOutbox.suppress(saved.notifications || [])" in html
|
|
assert "notificationReadOutbox.flush(activeFlushLogin)" in html
|
|
assert "Queue read & next" in html
|
|
assert "BASE + 'static/notification-read-outbox.js'" in worker
|