mirror of
https://github.com/rzuasti/oott.git
synced 2026-07-08 19:21:54 +02:00
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:
co-authored by
Claude Opus 4.8
parent
c7c5b45ae8
commit
817ec5872c
@@ -15,6 +15,25 @@ use crate::model::devices::Device;
|
||||
use crate::model::notifications::{Notification, NotificationType};
|
||||
|
||||
pub use delivery::run_delivery;
|
||||
pub use error::DeliveryError;
|
||||
|
||||
/// Send a one-off test push to every registered device through the push relay. Used by the app's
|
||||
/// "Send test notification" button to verify end-to-end push delivery. Unlike scan-driven
|
||||
/// notifications it is not persisted to the notifications list, and it is a no-op (still `Ok`) when
|
||||
/// no devices are registered.
|
||||
pub async fn send_test_push() -> Result<(), DeliveryError> {
|
||||
let config = crate::settings::get_settings()
|
||||
.notifications
|
||||
.push
|
||||
.clone()
|
||||
.unwrap_or_default();
|
||||
push::send(
|
||||
&config,
|
||||
"OOTT test".to_string(),
|
||||
"Test notification — push is working on this device.".to_string(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Send notifications for the changes detected during a scan (or a single sighting). Changes are
|
||||
/// grouped by notification type: a type with exactly one change produces the usual single-device
|
||||
|
||||
@@ -60,6 +60,7 @@ pub mod utils;
|
||||
devices::update,
|
||||
devices::unregister,
|
||||
notifications::list,
|
||||
notifications::send_test,
|
||||
notifications::read,
|
||||
notifications::read_without_flagging,
|
||||
notifications::mark_as_new,
|
||||
@@ -186,6 +187,7 @@ pub async fn serve() -> Result<(), Box<dyn Error>> {
|
||||
.route("/api/dhcp_scanner/status", get(dhcp_scanner::status))
|
||||
.route("/api/snmp_scanner/status", get(snmp_scanner::status))
|
||||
.route("/api/notifications", get(notifications::list))
|
||||
.route("/api/notifications/test", post(notifications::send_test))
|
||||
.route(
|
||||
"/api/notifications/mark_all_as_old",
|
||||
post(notifications::mark_all_as_old),
|
||||
|
||||
@@ -9,7 +9,7 @@ use axum::{
|
||||
use log::error;
|
||||
|
||||
use crate::{
|
||||
db,
|
||||
db, notifications,
|
||||
model::notifications::{Notification, NotificationListResponse},
|
||||
web_server::utils,
|
||||
};
|
||||
@@ -118,6 +118,29 @@ pub async fn mark_all_as_old() -> impl IntoResponse {
|
||||
.await
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/notifications/test",
|
||||
tag = "notifications",
|
||||
responses(
|
||||
(status = 200, description = "Test notification dispatched to the push relay"),
|
||||
(status = 500, description = "Internal server error"),
|
||||
),
|
||||
security(("bearer_auth" = []))
|
||||
)]
|
||||
pub async fn send_test() -> impl IntoResponse {
|
||||
match notifications::send_test_push().await {
|
||||
Ok(()) => (StatusCode::OK, "Test notification sent"),
|
||||
Err(err) => {
|
||||
error!("Error sending test push notification: {err}");
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"Error sending test notification, check your logs",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/notifications",
|
||||
@@ -161,3 +184,29 @@ pub async fn list(
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::tests_common;
|
||||
|
||||
#[tokio::test]
|
||||
async fn send_test_succeeds_with_no_registered_devices() {
|
||||
tests_common::setup().await;
|
||||
|
||||
// Clear any tokens left by other tests so the relay is never contacted (the no-op path),
|
||||
// keeping this hermetic; actual relay delivery + pruning is covered in the
|
||||
// notifications::push tests with a mock relay.
|
||||
let tokens: Vec<String> = db::push_tokens::list()
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|token| token.token)
|
||||
.collect();
|
||||
if !tokens.is_empty() {
|
||||
db::push_tokens::delete_many(&tokens).unwrap();
|
||||
}
|
||||
|
||||
let response = send_test().await.into_response();
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.'),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user