feat: configure deadline reminder horizon (Closes #727)
This commit is contained in:
1 parent
abf8e23685
commit
e860d16d29
10 files changed
+165
-20
No files matched your search
@@ -85,6 +85,48 @@ async def test_deadline_reminder_counts_calendar_days_per_device_timezone(tmp_pa
|
||||
assert sent == {"tokyo": 2, "la": 1}
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_deadline_reminder_uses_each_devices_confirmed_horizon(tmp_path):
|
||||
store = PushSubscriptionStore(tmp_path / "push.sqlite3")
|
||||
for device, reminder_days in (("today", 0), ("week", 7)):
|
||||
store.upsert(device, {
|
||||
"endpoint": f"https://push.example/{device}",
|
||||
"keys": {"p256dh": "public-key", "auth": "auth-secret"},
|
||||
})
|
||||
store.set_deadline_preferences(
|
||||
device,
|
||||
enabled=True,
|
||||
timezone="UTC",
|
||||
reminder_hour=9,
|
||||
reminder_days=reminder_days,
|
||||
)
|
||||
|
||||
async def assigned():
|
||||
return {"complete": True, "items": [
|
||||
{"id": 1, "due_date": "2026-08-13"},
|
||||
{"id": 2, "due_date": "2026-08-15"},
|
||||
{"id": 3, "due_date": "2026-08-20"},
|
||||
{"id": 4, "due_date": "2026-08-21"},
|
||||
]}
|
||||
|
||||
sent = {}
|
||||
|
||||
async def send(subscription, payload):
|
||||
device = subscription["endpoint"].rsplit("/", 1)[-1]
|
||||
sent[device] = json.loads(payload)["deadline_count"]
|
||||
|
||||
delivered = await dispatch_deadline_reminders(
|
||||
store,
|
||||
PushConfiguration("public", "private", "mailto:ops@example.com"),
|
||||
assigned,
|
||||
send,
|
||||
now=datetime(2026, 8, 13, 10, 0, tzinfo=timezone.utc),
|
||||
)
|
||||
|
||||
assert delivered == 2
|
||||
assert sent == {"today": 1, "week": 3}
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_deadline_reminder_fails_closed_for_incomplete_snapshot_and_before_local_hour(tmp_path):
|
||||
store = PushSubscriptionStore(tmp_path / "push.sqlite3")
|
||||
@@ -194,9 +236,37 @@ def test_deadline_preferences_persist_on_the_existing_device_subscription(tmp_pa
|
||||
"enabled": True,
|
||||
"timezone": "Europe/London",
|
||||
"reminder_hour": 8,
|
||||
"reminder_days": 2,
|
||||
}
|
||||
|
||||
|
||||
def test_existing_deadline_preferences_migrate_to_two_day_horizon(tmp_path):
|
||||
database = tmp_path / "push.sqlite3"
|
||||
import sqlite3
|
||||
|
||||
with sqlite3.connect(database) as connection:
|
||||
connection.executescript("""
|
||||
CREATE TABLE push_subscriptions (
|
||||
session_id TEXT PRIMARY KEY,
|
||||
endpoint TEXT NOT NULL UNIQUE,
|
||||
subscription_json TEXT NOT NULL
|
||||
);
|
||||
CREATE TABLE push_deadline_preferences (
|
||||
session_id TEXT PRIMARY KEY,
|
||||
enabled INTEGER NOT NULL DEFAULT 0,
|
||||
timezone TEXT NOT NULL DEFAULT 'UTC',
|
||||
reminder_hour INTEGER NOT NULL DEFAULT 9,
|
||||
delivered_local_day TEXT
|
||||
);
|
||||
INSERT INTO push_subscriptions VALUES ('device-a', 'https://push.example/a', '{}');
|
||||
INSERT INTO push_deadline_preferences VALUES ('device-a', 1, 'UTC', 8, NULL);
|
||||
""")
|
||||
|
||||
store = PushSubscriptionStore(database)
|
||||
|
||||
assert store.deadline_preferences("device-a")["reminder_days"] == 2
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_assigned_deadline_snapshot_is_pagination_complete(monkeypatch):
|
||||
pages = {
|
||||
|
||||
@@ -19,6 +19,7 @@ const deadlineControl = {
|
||||
addEventListener:(_name, callback) => state.deadlineChange = callback,
|
||||
};
|
||||
const deadlineHour = {value:'9', disabled:false, addEventListener:(_name, callback) => state.deadlineHourChange = callback};
|
||||
const deadlineDays = {value:'2', disabled:false, addEventListener:(_name, callback) => state.deadlineDaysChange = callback};
|
||||
const deadlineStatus = {set textContent(value) { state.deadlineText = value; }, get textContent() { return state.deadlineText; }};
|
||||
const status = {set textContent(value) { state.text = value; }, get textContent() { return state.text; }};
|
||||
const existing = {endpoint:'https://push.example/device', toJSON() { return {endpoint:this.endpoint, keys:{p256dh:'key',auth:'auth'}}; }};
|
||||
@@ -27,7 +28,7 @@ const registration = {pushManager:{
|
||||
subscribe: async options => { state.subscriptions.push(options); state.current=existing; return existing; },
|
||||
}};
|
||||
const feature = createPushNotifications({
|
||||
control, status, deadlineControl, deadlineStatus, deadlineHour,
|
||||
control, status, deadlineControl, deadlineStatus, deadlineHour, deadlineDays,
|
||||
notification: {permission:'default', requestPermission:async () => { state.prompts += 1; return state.permission || 'granted'; }},
|
||||
serviceWorker: {ready:Promise.resolve(registration)},
|
||||
fetchJson: async (url, options={}) => { state.requests.push([url,options.method || 'GET',options.body || '']); return state.server || {available:true,subscribed:false,public_key:'AQID'}; },
|
||||
@@ -80,7 +81,7 @@ process.stdout.write(JSON.stringify(state));
|
||||
assert body["enabled"] is True
|
||||
assert body["reminder_hour"] == 9
|
||||
assert isinstance(body["timezone"], str) and body["timezone"]
|
||||
assert result["deadlineText"] == "Deadline reminders enabled for 09:00 local time."
|
||||
assert result["deadlineText"] == "Deadline reminders enabled for 09:00 local time, next 2 days."
|
||||
|
||||
|
||||
def test_deadline_setup_subscribes_once_and_persists_selected_local_hour():
|
||||
@@ -100,7 +101,35 @@ process.stdout.write(JSON.stringify(state));
|
||||
body = json.loads(result["requests"][-1][2])
|
||||
assert body["enabled"] is True
|
||||
assert body["reminder_hour"] == 8
|
||||
assert result["deadlineText"] == "Deadline reminders enabled for 08:00 local time."
|
||||
assert result["deadlineText"] == "Deadline reminders enabled for 08:00 local time, next 2 days."
|
||||
|
||||
|
||||
def test_deadline_setup_persists_and_restores_confirmed_horizon():
|
||||
saved = run_scenario("""
|
||||
state.server = {available:true,subscribed:true,deadline_enabled:true,reminder_hour:9,reminder_days:2,public_key:'AQID'};
|
||||
state.current = existing;
|
||||
await feature.init();
|
||||
deadlineDays.value = '7';
|
||||
await feature.changeDeadline();
|
||||
process.stdout.write(JSON.stringify(state));
|
||||
""")
|
||||
|
||||
assert json.loads(saved["requests"][-1][2])["reminder_days"] == 7
|
||||
assert saved["deadlineText"] == "Deadline reminders enabled for 09:00 local time, next 7 days."
|
||||
|
||||
restored = run_scenario("""
|
||||
state.server = {available:true,subscribed:true,deadline_enabled:true,reminder_hour:17,reminder_days:0,public_key:'AQID'};
|
||||
state.current = existing;
|
||||
await feature.init();
|
||||
process.stdout.write(JSON.stringify({days:deadlineDays.value, readiness:feature.deadlineReadiness()}));
|
||||
""")
|
||||
assert restored == {
|
||||
"days": "0",
|
||||
"readiness": {
|
||||
"state": "complete",
|
||||
"detail": "Deadline reminders enabled for 17:00 local time, due today.",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_deadline_setup_restores_confirmed_hour_and_denial_stays_incomplete():
|
||||
@@ -112,7 +141,7 @@ process.stdout.write(JSON.stringify({hour:deadlineHour.value, checked:deadlineCo
|
||||
""")
|
||||
assert restored == {
|
||||
"hour": "17", "checked": True,
|
||||
"readiness": {"state": "complete", "detail": "Deadline reminders enabled for 17:00 local time."},
|
||||
"readiness": {"state": "complete", "detail": "Deadline reminders enabled for 17:00 local time, next 2 days."},
|
||||
}
|
||||
|
||||
denied = run_scenario("""
|
||||
@@ -142,6 +171,8 @@ def test_mobile_dashboard_mounts_opt_in_and_precaches_its_controller():
|
||||
assert 'id="push-update-status"' in html
|
||||
assert 'id="push-deadlines"' in html
|
||||
assert 'id="push-deadline-hour"' in html
|
||||
assert 'id="push-deadline-days"' in html
|
||||
assert 'id="device-setup-deadline-days"' in html
|
||||
assert 'id="push-deadline-status"' in html
|
||||
assert '<script src="static/push-notifications.js"></script>' in html
|
||||
assert "createPushNotifications({" in dashboard
|
||||
|
||||
@@ -980,6 +980,7 @@ async def test_authenticated_device_can_subscribe_report_status_and_unsubscribe(
|
||||
"deadline_enabled": False,
|
||||
"timezone": "UTC",
|
||||
"reminder_hour": 9,
|
||||
"reminder_days": 2,
|
||||
}
|
||||
assert await main.subscribe_push(payload, request) == {"subscribed": True}
|
||||
assert (await main.push_status(request))["subscribed"] is True
|
||||
@@ -1002,15 +1003,17 @@ async def test_authenticated_device_can_enable_deadline_reminders(tmp_path, monk
|
||||
monkeypatch.setattr(main.dashboard_auth, "session_management_id", management_id)
|
||||
request = SimpleNamespace(state=SimpleNamespace(dashboard_session=object()))
|
||||
payload = main.DeadlineReminderPayload(
|
||||
enabled=True, timezone="America/New_York", reminder_hour=9
|
||||
enabled=True, timezone="America/New_York", reminder_hour=9, reminder_days=7
|
||||
)
|
||||
|
||||
assert await main.update_deadline_reminders(payload, request) == {
|
||||
"deadline_enabled": True,
|
||||
"timezone": "America/New_York",
|
||||
"reminder_hour": 9,
|
||||
"reminder_days": 7,
|
||||
}
|
||||
assert store.deadline_preferences("session-a")["enabled"] is True
|
||||
assert store.deadline_preferences("session-a")["reminder_days"] == 7
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
|
||||
Reference in new issue
Block a user