Add a "Send test notification" button to verify push delivery

Adds POST /api/notifications/test, which delivers a canned test push to every
registered device through the existing relay path (not persisted to the
notifications list), wired into the router and OpenAPI. In Settings, a "Send test
notification" button appears under the push toggle, only when push is enabled on
this device, so the full backend -> relay -> FCM -> APNs -> device path can be
verified on demand without waiting for a real device event.

Backend, API and widget tests added; clippy and dart analyze clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
rzuasti
2026-06-09 12:42:20 -04:00
co-authored by Claude Opus 4.8
parent c7c5b45ae8
commit 817ec5872c
7 changed files with 147 additions and 1 deletions
+33
View File
@@ -30,6 +30,7 @@ class _SettingsState extends State<Settings> {
late final PushService _pushService;
bool _pushEnabled = false;
bool _pushBusy = false;
bool _testBusy = false;
// Whether the backend delivers notifications via push. The per-device push
// toggle only makes sense then, so it stays hidden until this is confirmed.
bool _pushMethodActive = false;
@@ -136,6 +137,27 @@ class _SettingsState extends State<Settings> {
}
}
// Ask the backend to deliver a test push to every registered device, so the
// user can confirm push is working end to end without waiting for a real
// device event.
Future<void> _sendTestNotification() async {
setState(() => _testBusy = true);
try {
await BackendAPI.instance.sendTestNotification();
if (!mounted) return;
UISnackbars.showSuccess(
context,
'Test notification sent. It should arrive shortly.',
);
} catch (e) {
debugPrint('Failed to send test notification: $e');
if (!mounted) return;
UISnackbars.showError(context, 'Failed to send test notification');
} finally {
if (mounted) setState(() => _testBusy = false);
}
}
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
@@ -287,6 +309,17 @@ class _SettingsState extends State<Settings> {
value: _pushEnabled,
onChanged: _pushBusy ? null : _togglePush,
),
// Only useful once push is on for this device; sends a test push
// through the full backend → relay → device path.
if (_pushEnabled)
Align(
alignment: Alignment.centerLeft,
child: TextButton.icon(
onPressed: _testBusy ? null : _sendTestNotification,
icon: const Icon(Icons.notifications_active_outlined),
label: const Text('Send test notification'),
),
),
],
],
),
@@ -14,6 +14,12 @@ extension NotificationApi on BackendAPI {
await _dio.post('/notifications/mark_all_as_old');
}
/// Asks the backend to deliver a one-off test push to every registered device,
/// used from Settings to verify end-to-end push delivery.
Future<void> sendTestNotification() async {
await _dio.post('/notifications/test');
}
Future<({List<Notification> items, int totalCount})> listNotifications(
bool? isNew, {
int page = 0,
@@ -36,6 +36,15 @@ void main() {
await BackendAPI.instance.markAllNotificationsAsRead();
});
test('sendTestNotification POSTs to /notifications/test', () async {
adapter.onPost(
'/notifications/test',
(server) => server.reply(200, null),
);
await BackendAPI.instance.sendTestNotification();
});
test(
'listNotifications sends is_new + pagination and reports the total',
() async {
@@ -161,4 +161,32 @@ void main() {
expect(service.disableCalls, 1);
expect(PrefUtil.getValue('push_enabled', false), isFalse);
});
testWidgets('hides the test button when push is off on this device', (
tester,
) async {
stubNotificationMethod('push');
await PrefUtil.setValue('push_enabled', false);
await pumpScreen(tester, Settings(pushService: _FakePushService()));
await pumpUntilFound(tester, find.byType(SwitchListTile));
expect(find.text('Send test notification'), findsNothing);
});
testWidgets('sends a test notification when push is on', (tester) async {
stubNotificationMethod('push');
adapter.onPost(
'/notifications/test',
(server) => server.reply(200, null),
);
await PrefUtil.setValue('push_enabled', true);
await pumpScreen(tester, Settings(pushService: _FakePushService()));
await pumpUntilFound(tester, find.text('Send test notification'));
await tester.tap(find.text('Send test notification'));
await pumpUntilFound(
tester,
find.text('Test notification sent. It should arrive shortly.'),
);
});
}