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:
rzuasti
2026-06-09 14:06:56 -04:00
co-authored by Claude Opus 4.8
parent 0075072196
commit 475edcb2cf
8 changed files with 82 additions and 33 deletions
+4 -4
View File
@@ -18,10 +18,10 @@ 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> {
/// "Send test notification" button to verify end-to-end push delivery. Returns the number of devices
/// the relay confirmed delivery to (0 when none are registered). Unlike scan-driven notifications it
/// is not persisted to the notifications list.
pub async fn send_test_push() -> Result<usize, DeliveryError> {
let config = crate::settings::get_settings()
.notifications
.push
+13 -6
View File
@@ -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
/// 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
/// caller) but never loses the event, which is already persisted in the notifications table.
pub async fn send(config: &Push, title: String, body: String) -> Result<(), DeliveryError> {
/// relay reports as dead. Returns the number of devices the relay confirmed delivery to. Best-effort:
/// a relay/network failure returns an error (logged by the caller) but never loses the event, which
/// 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?;
if stored.is_empty() {
debug!("No push tokens registered; nothing to deliver via the push relay");
return Ok(());
return Ok(0);
}
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?;
}
Ok(())
let delivered = parsed
.results
.iter()
.filter(|result| result.status == "ok")
.count();
Ok(delivered)
}
#[cfg(test)]
@@ -143,7 +149,8 @@ mod tests {
let config = 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();
assert!(
+2
View File
@@ -8,6 +8,7 @@ use crate::model::notifications::{Notification, NotificationListResponse, Notifi
use crate::model::push_tokens::{PushPlatform, PushToken};
use crate::settings::get_settings;
use crate::web_server::devices::{RegisterDevicePayload, UpdateDevicePayload};
use crate::web_server::notifications::TestNotificationResponse;
use crate::web_server::push_tokens::RegisterPushTokenPayload;
use crate::web_server::scanner_status::{
ActiveScannerStatusResponse, PassiveScannerStatusResponse,
@@ -83,6 +84,7 @@ pub mod utils;
Notification,
NotificationListResponse,
NotificationType,
TestNotificationResponse,
PushToken,
PushPlatform,
RegisterPushTokenPayload,
+16 -9
View File
@@ -7,6 +7,8 @@ use axum::{
response::IntoResponse,
};
use log::error;
use serde::Serialize;
use utoipa::ToSchema;
use crate::{
db, notifications,
@@ -14,6 +16,12 @@ use crate::{
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(
get,
path = "/api/notifications/{id}",
@@ -123,20 +131,17 @@ pub async fn mark_all_as_old() -> impl IntoResponse {
path = "/api/notifications/test",
tag = "notifications",
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"),
),
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 {
Ok(()) => (StatusCode::OK, "Test notification sent"),
Ok(delivered) => Ok(Json(TestNotificationResponse { delivered })),
Err(err) => {
error!("Error sending test push notification: {err}");
(
StatusCode::INTERNAL_SERVER_ERROR,
"Error sending test notification, check your logs",
)
Err(StatusCode::INTERNAL_SERVER_ERROR)
}
}
}
@@ -206,7 +211,9 @@ mod tests {
db::push_tokens::delete_many(&tokens).unwrap();
}
let response = send_test().await.into_response();
assert_eq!(response.status(), StatusCode::OK);
let result = send_test()
.await
.expect("test send should succeed when no devices are registered");
assert_eq!(result.0.delivered, 0, "No devices means nothing delivered");
}
}