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
+19
View File
@@ -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
+2
View File
@@ -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),
+50 -1
View File
@@ -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);
}
}