summaryrefslogtreecommitdiff
path: root/src/push/handlers
diff options
context:
space:
mode:
authorOwen Jacobson <owen@grimoire.ca>2025-12-09 15:13:21 -0500
committerOwen Jacobson <owen@grimoire.ca>2025-12-17 15:48:20 -0500
commit3c697f5fb1b8dbad46eac8fa299ed7cebfb36159 (patch)
treeb7854fb23d1e104f928acfe3bba75ea3b74b83d9 /src/push/handlers
parent41a5a0f7e13bf5a82aaef59e34eb68f0fe7fa7f5 (diff)
Factor push message publication out to its own helper component.
The `Publisher` component handles the details of web push delivery. Callers must provide the subscription set, the current signer, and the message, while the publisher handles encoding and communication with web push endpoints. To facilitate testing, `Publisher` implements `Publish`, which is a new trait with the same interface. Components that might publish web push messages should rely on the trait where possible. The test suite now constructs an app with a dummy `Publish` impl, which captures push messages for examination. Note that the testing implementation of `Publish` is hand-crafted, and presently only acts to record the arguments it receives. The other alternative was to use a mocking library, such as `mockit`, and while I've used that approach before, I'm not super comfortable with the complexity in this situation. I think we can maintain a more reasonable testing `Publish` impl by hand, at least for now, and we can revisit that decision later if need be. Tests for the `ping` endpoint have been migrated to this endpoint.
Diffstat (limited to 'src/push/handlers')
-rw-r--r--src/push/handlers/ping/mod.rs9
-rw-r--r--src/push/handlers/ping/test.rs18
-rw-r--r--src/push/handlers/subscribe/test.rs95
3 files changed, 29 insertions, 93 deletions
diff --git a/src/push/handlers/ping/mod.rs b/src/push/handlers/ping/mod.rs
index db828fa..2a86984 100644
--- a/src/push/handlers/ping/mod.rs
+++ b/src/push/handlers/ping/mod.rs
@@ -1,7 +1,10 @@
use axum::{Json, extract::State, http::StatusCode};
-use web_push::WebPushClient;
-use crate::{error::Internal, push::app::Push, token::extract::Identity};
+use crate::{
+ error::Internal,
+ push::{Publish, app::Push},
+ token::extract::Identity,
+};
#[cfg(test)]
mod test;
@@ -15,7 +18,7 @@ pub async fn handler<P>(
Json(_): Json<Request>,
) -> Result<StatusCode, Internal>
where
- P: WebPushClient,
+ P: Publish,
{
push.ping(&identity.login).await?;
diff --git a/src/push/handlers/ping/test.rs b/src/push/handlers/ping/test.rs
index 5725131..3481139 100644
--- a/src/push/handlers/ping/test.rs
+++ b/src/push/handlers/ping/test.rs
@@ -2,8 +2,9 @@ use axum::{
extract::{Json, State},
http::StatusCode,
};
+use itertools::Itertools;
-use crate::test::fixtures;
+use crate::{event::Heartbeat, test::fixtures};
#[tokio::test]
async fn ping_without_subscriptions() {
@@ -11,18 +12,21 @@ async fn ping_without_subscriptions() {
let recipient = fixtures::identity::create(&app, &fixtures::now()).await;
- app.vapid()
- .refresh_key(&fixtures::now())
- .await
- .expect("refreshing the VAPID key always succeeds");
-
let response = super::handler(State(app.push()), recipient, Json(super::Request {}))
.await
.expect("sending a ping with no subscriptions always succeeds");
assert_eq!(StatusCode::ACCEPTED, response);
- assert!(app.webpush().sent().is_empty());
+ assert!(
+ app.publisher()
+ .sent()
+ .into_iter()
+ .filter(|publish| publish.message_eq(&Heartbeat::Heartbeat)
+ && publish.subscriptions.is_empty())
+ .exactly_one()
+ .is_ok()
+ );
}
// More complete testing requires that we figure out how to generate working p256 ECDH keys for
diff --git a/src/push/handlers/subscribe/test.rs b/src/push/handlers/subscribe/test.rs
index 1bc37a4..793bcef 100644
--- a/src/push/handlers/subscribe/test.rs
+++ b/src/push/handlers/subscribe/test.rs
@@ -3,33 +3,16 @@ use axum::{
http::StatusCode,
};
-use crate::{
- push::app::SubscribeError,
- test::{fixtures, fixtures::event},
-};
+use crate::{push::app::SubscribeError, test::fixtures};
#[tokio::test]
async fn accepts_new_subscription() {
let app = fixtures::scratch_app().await;
let subscriber = fixtures::identity::create(&app, &fixtures::now()).await;
- // Issue a VAPID key.
-
- app.vapid()
- .refresh_key(&fixtures::now())
- .await
- .expect("refreshing the VAPID key always succeeds");
-
// Find out what that VAPID key is.
- let boot = app.boot().snapshot().await.expect("boot always succeeds");
- let vapid = boot
- .events
- .into_iter()
- .filter_map(event::vapid)
- .filter_map(event::vapid::changed)
- .next_back()
- .expect("the application will have a vapid key after a refresh");
+ let vapid = fixtures::vapid::key(&app).await;
// Create a dummy subscription with that key.
@@ -41,7 +24,7 @@ async fn accepts_new_subscription() {
auth: String::from("test-auth-value"),
},
},
- vapid: vapid.key,
+ vapid,
};
let response = super::handler(State(app.push()), subscriber, Json(request))
.await
@@ -57,23 +40,9 @@ async fn accepts_repeat_subscription() {
let app = fixtures::scratch_app().await;
let subscriber = fixtures::identity::create(&app, &fixtures::now()).await;
- // Issue a VAPID key.
-
- app.vapid()
- .refresh_key(&fixtures::now())
- .await
- .expect("refreshing the VAPID key always succeeds");
-
// Find out what that VAPID key is.
- let boot = app.boot().snapshot().await.expect("boot always succeeds");
- let vapid = boot
- .events
- .into_iter()
- .filter_map(event::vapid)
- .filter_map(event::vapid::changed)
- .next_back()
- .expect("the application will have a vapid key after a refresh");
+ let vapid = fixtures::vapid::key(&app).await;
// Create a dummy subscription with that key.
@@ -85,7 +54,7 @@ async fn accepts_repeat_subscription() {
auth: String::from("test-auth-value"),
},
},
- vapid: vapid.key,
+ vapid,
};
let response = super::handler(State(app.push()), subscriber.clone(), Json(request.clone()))
.await
@@ -111,23 +80,9 @@ async fn rejects_duplicate_subscription() {
let app = fixtures::scratch_app().await;
let subscriber = fixtures::identity::create(&app, &fixtures::now()).await;
- // Issue a VAPID key.
-
- app.vapid()
- .refresh_key(&fixtures::now())
- .await
- .expect("refreshing the VAPID key always succeeds");
-
// Find out what that VAPID key is.
- let boot = app.boot().snapshot().await.expect("boot always succeeds");
- let vapid = boot
- .events
- .into_iter()
- .filter_map(event::vapid)
- .filter_map(event::vapid::changed)
- .next_back()
- .expect("the application will have a vapid key after a refresh");
+ let vapid = fixtures::vapid::key(&app).await;
// Create a dummy subscription with that key.
@@ -139,7 +94,7 @@ async fn rejects_duplicate_subscription() {
auth: String::from("test-auth-value"),
},
},
- vapid: vapid.key,
+ vapid,
};
super::handler(State(app.push()), subscriber.clone(), Json(request))
.await
@@ -155,7 +110,7 @@ async fn rejects_duplicate_subscription() {
auth: String::from("different-test-auth-value"),
},
},
- vapid: vapid.key,
+ vapid,
};
let response = super::handler(State(app.push()), subscriber, Json(request))
.await
@@ -170,24 +125,7 @@ async fn rejects_duplicate_subscription() {
async fn rejects_stale_vapid_key() {
let app = fixtures::scratch_app().await;
let subscriber = fixtures::identity::create(&app, &fixtures::now()).await;
-
- // Issue a VAPID key.
-
- app.vapid()
- .refresh_key(&fixtures::now())
- .await
- .expect("refreshing the VAPID key always succeeds");
-
- // Find out what that VAPID key is.
-
- let boot = app.boot().snapshot().await.expect("boot always succeeds");
- let vapid = boot
- .events
- .into_iter()
- .filter_map(event::vapid)
- .filter_map(event::vapid::changed)
- .next_back()
- .expect("the application will have a vapid key after a refresh");
+ let stale_vapid = fixtures::vapid::key(&app).await;
// Change the VAPID key.
@@ -200,16 +138,7 @@ async fn rejects_stale_vapid_key() {
.await
.expect("refreshing the VAPID key always succeeds");
- // Find out what the new VAPID key is.
-
- let boot = app.boot().snapshot().await.expect("boot always succeeds");
- let fresh_vapid = boot
- .events
- .into_iter()
- .filter_map(event::vapid)
- .filter_map(event::vapid::changed)
- .next_back()
- .expect("the application will have a vapid key after a refresh");
+ let fresh_vapid = fixtures::vapid::key(&app).await;
// Create a dummy subscription with the original key.
@@ -221,7 +150,7 @@ async fn rejects_stale_vapid_key() {
auth: String::from("test-auth-value"),
},
},
- vapid: vapid.key,
+ vapid: stale_vapid,
};
let response = super::handler(State(app.push()), subscriber, Json(request))
.await
@@ -231,6 +160,6 @@ async fn rejects_stale_vapid_key() {
assert!(matches!(
response,
- super::Error(SubscribeError::StaleVapidKey(key)) if key == fresh_vapid.key
+ super::Error(SubscribeError::StaleVapidKey(key)) if key == fresh_vapid
));
}