mirror of
https://github.com/rzuasti/oott.git
synced 2026-07-08 19:21:54 +02:00
Report delivered device count from the test-notification endpoint
POST /api/notifications/test previously returned a blanket 200 even when there
were no registered devices, so a test that reached nobody looked like a success.
push::send now returns the number of devices the relay confirmed delivery to, and
the endpoint returns it as {"delivered": N}. Settings shows "sent to N device(s)"
on success and an explicit "No devices are registered..." warning when N is 0,
which is the case that previously masqueraded as success.
Backend, API and widget tests updated; 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
0075072196
commit
475edcb2cf
@@ -18,10 +18,10 @@ pub use delivery::run_delivery;
|
|||||||
pub use error::DeliveryError;
|
pub use error::DeliveryError;
|
||||||
|
|
||||||
/// Send a one-off test push to every registered device through the push relay. Used by the app's
|
/// 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
|
/// "Send test notification" button to verify end-to-end push delivery. Returns the number of devices
|
||||||
/// notifications it is not persisted to the notifications list, and it is a no-op (still `Ok`) when
|
/// the relay confirmed delivery to (0 when none are registered). Unlike scan-driven notifications it
|
||||||
/// no devices are registered.
|
/// is not persisted to the notifications list.
|
||||||
pub async fn send_test_push() -> Result<(), DeliveryError> {
|
pub async fn send_test_push() -> Result<usize, DeliveryError> {
|
||||||
let config = crate::settings::get_settings()
|
let config = crate::settings::get_settings()
|
||||||
.notifications
|
.notifications
|
||||||
.push
|
.push
|
||||||
|
|||||||
@@ -48,13 +48,14 @@ fn dead_tokens(results: &[RelayTokenResult]) -> Vec<String> {
|
|||||||
|
|
||||||
/// Deliver a notification to all registered devices through the project-operated push relay. Loads
|
/// Deliver a notification to all registered devices through the project-operated push relay. Loads
|
||||||
/// the stored tokens, forwards only the already-sanitized title/body, and prunes any tokens the
|
/// the stored tokens, forwards only the already-sanitized title/body, and prunes any tokens the
|
||||||
/// relay reports as dead. Best-effort: a relay/network failure returns an error (logged by the
|
/// relay reports as dead. Returns the number of devices the relay confirmed delivery to. Best-effort:
|
||||||
/// caller) but never loses the event, which is already persisted in the notifications table.
|
/// a relay/network failure returns an error (logged by the caller) but never loses the event, which
|
||||||
pub async fn send(config: &Push, title: String, body: String) -> Result<(), DeliveryError> {
|
/// is already persisted in the notifications table.
|
||||||
|
pub async fn send(config: &Push, title: String, body: String) -> Result<usize, DeliveryError> {
|
||||||
let stored = db::run_blocking(db::push_tokens::list).await?;
|
let stored = db::run_blocking(db::push_tokens::list).await?;
|
||||||
if stored.is_empty() {
|
if stored.is_empty() {
|
||||||
debug!("No push tokens registered; nothing to deliver via the push relay");
|
debug!("No push tokens registered; nothing to deliver via the push relay");
|
||||||
return Ok(());
|
return Ok(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
let request = RelayRequest {
|
let request = RelayRequest {
|
||||||
@@ -80,7 +81,12 @@ pub async fn send(config: &Push, title: String, body: String) -> Result<(), Deli
|
|||||||
db::run_blocking(move || db::push_tokens::delete_many(&dead)).await?;
|
db::run_blocking(move || db::push_tokens::delete_many(&dead)).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
let delivered = parsed
|
||||||
|
.results
|
||||||
|
.iter()
|
||||||
|
.filter(|result| result.status == "ok")
|
||||||
|
.count();
|
||||||
|
Ok(delivered)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -143,7 +149,8 @@ mod tests {
|
|||||||
let config = Push {
|
let config = Push {
|
||||||
relay_url: format!("http://{addr}/v1/push"),
|
relay_url: format!("http://{addr}/v1/push"),
|
||||||
};
|
};
|
||||||
send(&config, "title".into(), "body".into()).await.unwrap();
|
let delivered = send(&config, "title".into(), "body".into()).await.unwrap();
|
||||||
|
assert_eq!(delivered, 1, "Only the live token should count as delivered");
|
||||||
|
|
||||||
let all = db::push_tokens::list().unwrap();
|
let all = db::push_tokens::list().unwrap();
|
||||||
assert!(
|
assert!(
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use crate::model::notifications::{Notification, NotificationListResponse, Notifi
|
|||||||
use crate::model::push_tokens::{PushPlatform, PushToken};
|
use crate::model::push_tokens::{PushPlatform, PushToken};
|
||||||
use crate::settings::get_settings;
|
use crate::settings::get_settings;
|
||||||
use crate::web_server::devices::{RegisterDevicePayload, UpdateDevicePayload};
|
use crate::web_server::devices::{RegisterDevicePayload, UpdateDevicePayload};
|
||||||
|
use crate::web_server::notifications::TestNotificationResponse;
|
||||||
use crate::web_server::push_tokens::RegisterPushTokenPayload;
|
use crate::web_server::push_tokens::RegisterPushTokenPayload;
|
||||||
use crate::web_server::scanner_status::{
|
use crate::web_server::scanner_status::{
|
||||||
ActiveScannerStatusResponse, PassiveScannerStatusResponse,
|
ActiveScannerStatusResponse, PassiveScannerStatusResponse,
|
||||||
@@ -83,6 +84,7 @@ pub mod utils;
|
|||||||
Notification,
|
Notification,
|
||||||
NotificationListResponse,
|
NotificationListResponse,
|
||||||
NotificationType,
|
NotificationType,
|
||||||
|
TestNotificationResponse,
|
||||||
PushToken,
|
PushToken,
|
||||||
PushPlatform,
|
PushPlatform,
|
||||||
RegisterPushTokenPayload,
|
RegisterPushTokenPayload,
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ use axum::{
|
|||||||
response::IntoResponse,
|
response::IntoResponse,
|
||||||
};
|
};
|
||||||
use log::error;
|
use log::error;
|
||||||
|
use serde::Serialize;
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
db, notifications,
|
db, notifications,
|
||||||
@@ -14,6 +16,12 @@ use crate::{
|
|||||||
web_server::utils,
|
web_server::utils,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Result of a test-notification request: how many devices the relay confirmed delivery to.
|
||||||
|
#[derive(Serialize, ToSchema)]
|
||||||
|
pub struct TestNotificationResponse {
|
||||||
|
pub delivered: usize,
|
||||||
|
}
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
get,
|
get,
|
||||||
path = "/api/notifications/{id}",
|
path = "/api/notifications/{id}",
|
||||||
@@ -123,20 +131,17 @@ pub async fn mark_all_as_old() -> impl IntoResponse {
|
|||||||
path = "/api/notifications/test",
|
path = "/api/notifications/test",
|
||||||
tag = "notifications",
|
tag = "notifications",
|
||||||
responses(
|
responses(
|
||||||
(status = 200, description = "Test notification dispatched to the push relay"),
|
(status = 200, description = "Test notification dispatched", body = TestNotificationResponse),
|
||||||
(status = 500, description = "Internal server error"),
|
(status = 500, description = "Internal server error"),
|
||||||
),
|
),
|
||||||
security(("bearer_auth" = []))
|
security(("bearer_auth" = []))
|
||||||
)]
|
)]
|
||||||
pub async fn send_test() -> impl IntoResponse {
|
pub async fn send_test() -> Result<Json<TestNotificationResponse>, StatusCode> {
|
||||||
match notifications::send_test_push().await {
|
match notifications::send_test_push().await {
|
||||||
Ok(()) => (StatusCode::OK, "Test notification sent"),
|
Ok(delivered) => Ok(Json(TestNotificationResponse { delivered })),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!("Error sending test push notification: {err}");
|
error!("Error sending test push notification: {err}");
|
||||||
(
|
Err(StatusCode::INTERNAL_SERVER_ERROR)
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
|
||||||
"Error sending test notification, check your logs",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -206,7 +211,9 @@ mod tests {
|
|||||||
db::push_tokens::delete_many(&tokens).unwrap();
|
db::push_tokens::delete_many(&tokens).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
let response = send_test().await.into_response();
|
let result = send_test()
|
||||||
assert_eq!(response.status(), StatusCode::OK);
|
.await
|
||||||
|
.expect("test send should succeed when no devices are registered");
|
||||||
|
assert_eq!(result.0.delivered, 0, "No devices means nothing delivered");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -143,12 +143,22 @@ class _SettingsState extends State<Settings> {
|
|||||||
Future<void> _sendTestNotification() async {
|
Future<void> _sendTestNotification() async {
|
||||||
setState(() => _testBusy = true);
|
setState(() => _testBusy = true);
|
||||||
try {
|
try {
|
||||||
await BackendAPI.instance.sendTestNotification();
|
final delivered = await BackendAPI.instance.sendTestNotification();
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
UISnackbars.showSuccess(
|
if (delivered == 0) {
|
||||||
context,
|
// The request succeeded but no device received it — usually the backend
|
||||||
'Test notification sent. It should arrive shortly.',
|
// lost this device's token (e.g. after a restart); re-toggle to re-register.
|
||||||
);
|
UISnackbars.showError(
|
||||||
|
context,
|
||||||
|
'No devices are registered to receive push notifications.',
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
final devices = delivered == 1 ? 'device' : 'devices';
|
||||||
|
UISnackbars.showSuccess(
|
||||||
|
context,
|
||||||
|
'Test notification sent to $delivered $devices.',
|
||||||
|
);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('Failed to send test notification: $e');
|
debugPrint('Failed to send test notification: $e');
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
|||||||
@@ -15,9 +15,11 @@ extension NotificationApi on BackendAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Asks the backend to deliver a one-off test push to every registered device,
|
/// Asks the backend to deliver a one-off test push to every registered device,
|
||||||
/// used from Settings to verify end-to-end push delivery.
|
/// used from Settings to verify end-to-end push delivery. Returns the number of
|
||||||
Future<void> sendTestNotification() async {
|
/// devices the relay confirmed delivery to (0 when none are registered).
|
||||||
await _dio.post('/notifications/test');
|
Future<int> sendTestNotification() async {
|
||||||
|
final response = await _dio.post('/notifications/test');
|
||||||
|
return (response.data as Map<String, dynamic>)['delivered'] as int;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<({List<Notification> items, int totalCount})> listNotifications(
|
Future<({List<Notification> items, int totalCount})> listNotifications(
|
||||||
|
|||||||
@@ -36,13 +36,13 @@ void main() {
|
|||||||
await BackendAPI.instance.markAllNotificationsAsRead();
|
await BackendAPI.instance.markAllNotificationsAsRead();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('sendTestNotification POSTs to /notifications/test', () async {
|
test('sendTestNotification POSTs and returns the delivered count', () async {
|
||||||
adapter.onPost(
|
adapter.onPost(
|
||||||
'/notifications/test',
|
'/notifications/test',
|
||||||
(server) => server.reply(200, null),
|
(server) => server.reply(200, {'delivered': 2}),
|
||||||
);
|
);
|
||||||
|
|
||||||
await BackendAPI.instance.sendTestNotification();
|
expect(await BackendAPI.instance.sendTestNotification(), 2);
|
||||||
});
|
});
|
||||||
|
|
||||||
test(
|
test(
|
||||||
|
|||||||
@@ -173,11 +173,13 @@ void main() {
|
|||||||
expect(find.text('Send test notification'), findsNothing);
|
expect(find.text('Send test notification'), findsNothing);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('sends a test notification when push is on', (tester) async {
|
testWidgets('reports the device count when a test notification is sent', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
stubNotificationMethod('push');
|
stubNotificationMethod('push');
|
||||||
adapter.onPost(
|
adapter.onPost(
|
||||||
'/notifications/test',
|
'/notifications/test',
|
||||||
(server) => server.reply(200, null),
|
(server) => server.reply(200, {'delivered': 2}),
|
||||||
);
|
);
|
||||||
await PrefUtil.setValue('push_enabled', true);
|
await PrefUtil.setValue('push_enabled', true);
|
||||||
await pumpScreen(tester, Settings(pushService: _FakePushService()));
|
await pumpScreen(tester, Settings(pushService: _FakePushService()));
|
||||||
@@ -186,7 +188,26 @@ void main() {
|
|||||||
await tester.tap(find.text('Send test notification'));
|
await tester.tap(find.text('Send test notification'));
|
||||||
await pumpUntilFound(
|
await pumpUntilFound(
|
||||||
tester,
|
tester,
|
||||||
find.text('Test notification sent. It should arrive shortly.'),
|
find.text('Test notification sent to 2 devices.'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('warns when a test notification reaches no devices', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
stubNotificationMethod('push');
|
||||||
|
adapter.onPost(
|
||||||
|
'/notifications/test',
|
||||||
|
(server) => server.reply(200, {'delivered': 0}),
|
||||||
|
);
|
||||||
|
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('No devices are registered to receive push notifications.'),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user