diff options
| author | Owen Jacobson <owen@grimoire.ca> | 2025-12-09 23:21:28 -0500 |
|---|---|---|
| committer | Owen Jacobson <owen@grimoire.ca> | 2025-12-17 15:48:20 -0500 |
| commit | 522c1533a0af7af7d96bb4d6850ce0b2c8762e89 (patch) | |
| tree | b5bacf95cd302ce86cc6dce12dd59c6739e81642 /src | |
| parent | 3c697f5fb1b8dbad46eac8fa299ed7cebfb36159 (diff) | |
Now that testing push-generating endpoints is more feasible, implement the missing test cases for `ping`.
Diffstat (limited to 'src')
| -rw-r--r-- | src/push/handlers/ping/test.rs | 356 | ||||
| -rw-r--r-- | src/test/webpush.rs | 23 |
2 files changed, 364 insertions, 15 deletions
diff --git a/src/push/handlers/ping/test.rs b/src/push/handlers/ping/test.rs index 3481139..c985aaf 100644 --- a/src/push/handlers/ping/test.rs +++ b/src/push/handlers/ping/test.rs @@ -1,8 +1,11 @@ +use std::{collections::HashSet, io}; + use axum::{ extract::{Json, State}, http::StatusCode, }; use itertools::Itertools; +use web_push::{SubscriptionInfo, WebPushError}; use crate::{event::Heartbeat, test::fixtures}; @@ -29,16 +32,343 @@ async fn ping_without_subscriptions() { ); } -// More complete testing requires that we figure out how to generate working p256 ECDH keys for -// testing _with_, as `web_push` will actually parse and use those keys even if push messages are -// ultimately never serialized or sent over HTTP. -// -// Tests that are missing: -// -// * Verify that subscribing and sending a ping causes a ping to be delivered to that subscription. -// * Verify that two subscriptions both get pings. -// * Verify that other users' subscriptions are not pinged. -// * Verify that a ping that causes a permanent error causes the subscription to be deleted. -// * Verify that a ping that causes a non-permanent error does not cause the subscription to be -// deleted. -// * Verify that a failure on one subscription doesn't affect delivery on other subscriptions. +#[tokio::test] +async fn ping() { + let app = fixtures::scratch_app().await; + + let recipient = fixtures::identity::create(&app, &fixtures::now()).await; + let vapid = fixtures::vapid::key(&app).await; + + // Create a subscription + let subscription = SubscriptionInfo::new( + "https://push.example.com/endpoint", + "testing-p256dh-key", + "testing-auth", + ); + app.push() + .subscribe(&recipient, &subscription, &vapid) + .await + .expect("creating a subscription succeeds"); + + // Send a ping + let response = super::handler(State(app.push()), recipient, Json(super::Request {})) + .await + .expect("sending a ping succeeds"); + + assert_eq!(StatusCode::ACCEPTED, response); + + // Confirm that it was actually sent + let subscriptions = HashSet::from([subscription]); + assert!( + app.publisher() + .sent() + .into_iter() + .filter(|publish| publish.message_eq(&Heartbeat::Heartbeat) + && publish.subscriptions == subscriptions) + .exactly_one() + .is_ok() + ); +} + +#[tokio::test] +async fn ping_multiple_subscriptions() { + let app = fixtures::scratch_app().await; + + let recipient = fixtures::identity::create(&app, &fixtures::now()).await; + let vapid = fixtures::vapid::key(&app).await; + + // Create a subscription + let subscriptions = HashSet::from([ + SubscriptionInfo::new( + "https://push.example.com/endpoint-1", + "testing-p256dh-key-1", + "testing-auth-1", + ), + SubscriptionInfo::new( + "https://push.example.com/endpoint-2", + "testing-p256dh-key-2", + "testing-auth-2", + ), + ]); + for subscription in &subscriptions { + app.push() + .subscribe(&recipient, subscription, &vapid) + .await + .expect("creating a subscription succeeds"); + } + + // Send a ping + let response = super::handler(State(app.push()), recipient, Json(super::Request {})) + .await + .expect("sending a ping succeeds"); + + assert_eq!(StatusCode::ACCEPTED, response); + + // Confirm that it was actually sent + assert!( + app.publisher() + .sent() + .into_iter() + .filter(|publish| publish.message_eq(&Heartbeat::Heartbeat) + && publish.subscriptions == subscriptions) + .exactly_one() + .is_ok() + ); +} + +#[tokio::test] +async fn ping_recipient_only() { + let app = fixtures::scratch_app().await; + + let recipient = fixtures::identity::create(&app, &fixtures::now()).await; + let spectator = fixtures::identity::create(&app, &fixtures::now()).await; + + let vapid = fixtures::vapid::key(&app).await; + + // Create subscriptions for each user + let recipient_subscription = SubscriptionInfo::new( + "https://push.example.com/recipient/endpoint", + "recipient-p256dh-key", + "recipient-auth", + ); + app.push() + .subscribe(&recipient, &recipient_subscription, &vapid) + .await + .expect("creating a subscription succeeds"); + + let spectator_subscription = SubscriptionInfo::new( + "https://push.example.com/spectator/endpoint", + "spectator-p256dh-key", + "spectator-auth", + ); + app.push() + .subscribe(&spectator, &spectator_subscription, &vapid) + .await + .expect("creating a subscription succeeds"); + + // Send a ping + let response = super::handler(State(app.push()), recipient, Json(super::Request {})) + .await + .expect("sending a ping succeeds"); + + assert_eq!(StatusCode::ACCEPTED, response); + + // Confirm that it was actually sent to the recipient + let sent = app.publisher().sent(); + + let recipient_subscriptions = HashSet::from([recipient_subscription]); + assert!( + sent.iter() + .filter(|publish| publish.message_eq(&Heartbeat::Heartbeat) + && publish.subscriptions == recipient_subscriptions) + .exactly_one() + .is_ok() + ); + + // Confirm that it was not sent to the spectator + assert!( + !sent + .iter() + .any(|publish| publish.subscriptions.contains(&spectator_subscription)) + ); +} + +#[tokio::test] +async fn ping_permanent_error() { + let app = fixtures::scratch_app().await; + + let recipient = fixtures::identity::create(&app, &fixtures::now()).await; + + let vapid = fixtures::vapid::key(&app).await; + + // Create subscriptions + let subscription = SubscriptionInfo::new( + "https://push.example.com/recipient/endpoint", + "recipient-p256dh-key", + "recipient-auth", + ); + app.push() + .subscribe(&recipient, &subscription, &vapid) + .await + .expect("creating a subscription succeeds"); + + // Prepare the next ping attempt to fail + app.publisher() + .fail_next(&subscription, WebPushError::InvalidUri); + + // Send a ping + super::handler( + State(app.push()), + recipient.clone(), + Json(super::Request {}), + ) + .await + .expect_err("sending a ping with a permanently-failing subscription fails"); + + // Confirm that it was actually sent + let sent = app.publisher().sent(); + + let subscriptions = HashSet::from([subscription.clone()]); + assert!( + sent.iter() + .filter(|publish| publish.message_eq(&Heartbeat::Heartbeat) + && publish.subscriptions == subscriptions) + .exactly_one() + .is_ok() + ); + + // Send a second ping + 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); + + // Confirm that it was _not_ sent this time, since it failed permanently last time + let sent = app.publisher().sent(); + + assert!( + !sent + .iter() + .any(|publish| publish.subscriptions.contains(&subscription)) + ); +} + +#[tokio::test] +async fn ping_temporary_error() { + let app = fixtures::scratch_app().await; + + let recipient = fixtures::identity::create(&app, &fixtures::now()).await; + + let vapid = fixtures::vapid::key(&app).await; + + // Create subscriptions + let subscription = SubscriptionInfo::new( + "https://push.example.com/recipient/endpoint", + "recipient-p256dh-key", + "recipient-auth", + ); + app.push() + .subscribe(&recipient, &subscription, &vapid) + .await + .expect("creating a subscription succeeds"); + + // Prepare the next ping attempt to fail + app.publisher().fail_next( + &subscription, + WebPushError::from(io::Error::other("transient IO error")), + ); + + // Send a ping + super::handler( + State(app.push()), + recipient.clone(), + Json(super::Request {}), + ) + .await + .expect_err("sending a ping with a temporarily-failing subscription fails"); + + // Confirm that it was actually sent + let sent = app.publisher().sent(); + + let subscriptions = HashSet::from([subscription.clone()]); + assert!( + sent.iter() + .filter(|publish| publish.message_eq(&Heartbeat::Heartbeat) + && publish.subscriptions == subscriptions) + .exactly_one() + .is_ok() + ); + + // Send a second ping + let response = super::handler(State(app.push()), recipient, Json(super::Request {})) + .await + .expect("sending a ping subscription succeeds now that the transient error has cleared"); + assert_eq!(StatusCode::ACCEPTED, response); + + // Confirm that it was _not_ sent this time, since it failed permanently last time + let sent = app.publisher().sent(); + + let subscriptions = HashSet::from([subscription]); + assert!( + sent.iter() + .filter(|publish| publish.message_eq(&Heartbeat::Heartbeat) + && publish.subscriptions == subscriptions) + .exactly_one() + .is_ok() + ); +} + +#[tokio::test] +async fn ping_multiple_subscriptions_with_failure() { + let app = fixtures::scratch_app().await; + + let recipient = fixtures::identity::create(&app, &fixtures::now()).await; + let vapid = fixtures::vapid::key(&app).await; + + // Create subscriptions + let failing = SubscriptionInfo::new( + "https://push.example.com/endpoint-failing", + "testing-p256dh-key-failing", + "testing-auth-failing", + ); + let succeeding = SubscriptionInfo::new( + "https://push.example.com/endpoint-succeeding", + "testing-p256dh-key-succeeding", + "testing-auth-succeeding", + ); + let subscriptions = HashSet::from([failing.clone(), succeeding.clone()]); + for subscription in &subscriptions { + app.push() + .subscribe(&recipient, subscription, &vapid) + .await + .expect("creating a subscription succeeds"); + } + + // Rig one of them to fail permanently + app.publisher() + .fail_next(&failing, WebPushError::InvalidUri); + + // Send a ping + super::handler( + State(app.push()), + recipient.clone(), + Json(super::Request {}), + ) + .await + .expect_err("sending a ping with a failing subscription fails"); + + // Confirm that it was actually sent to both subs the first time + let subscriptions = HashSet::from([failing.clone(), succeeding.clone()]); + assert!( + app.publisher() + .sent() + .iter() + .filter(|publish| publish.message_eq(&Heartbeat::Heartbeat) + && publish.subscriptions == subscriptions) + .exactly_one() + .is_ok() + ); + + // Send a second ping + let response = super::handler(State(app.push()), recipient, Json(super::Request {})) + .await + .expect("sending a second ping succeeds after the failing subscription is removed"); + assert_eq!(StatusCode::ACCEPTED, response); + + // Confirm that it was only sent to the succeeding subscription + let subscriptions = HashSet::from([succeeding]); + let sent = app.publisher().sent(); + assert!( + sent.iter() + .filter(|publish| publish.message_eq(&Heartbeat::Heartbeat) + && publish.subscriptions == subscriptions) + .exactly_one() + .is_ok() + ); + assert!( + !sent + .iter() + .any(|publish| publish.subscriptions.contains(&failing)) + ); +} diff --git a/src/test/webpush.rs b/src/test/webpush.rs index f33f03c..96fa843 100644 --- a/src/test/webpush.rs +++ b/src/test/webpush.rs @@ -1,5 +1,6 @@ use std::{ any::Any, + collections::{HashMap, HashSet}, mem, sync::{Arc, Mutex}, }; @@ -11,12 +12,14 @@ use crate::{error::failed::Failed, push::Publish}; #[derive(Clone)] pub struct Client { sent: Arc<Mutex<Vec<Publication>>>, + failures: Arc<Mutex<HashMap<SubscriptionInfo, WebPushError>>>, } impl Client { pub fn new() -> Self { Self { sent: Arc::default(), + failures: Arc::default(), } } @@ -26,6 +29,11 @@ impl Client { let mut sent = self.sent.lock().unwrap(); mem::take(&mut sent) } + + pub fn fail_next(&self, subscription_info: &SubscriptionInfo, err: WebPushError) { + let mut failures = self.failures.lock().unwrap(); + failures.insert(subscription_info.clone(), err); + } } impl Publish for Client { @@ -40,19 +48,30 @@ impl Publish for Client { { let message: Box<dyn Any + Send> = Box::new(message); let subscriptions = subscriptions.into_iter().collect(); + + let mut failures = Vec::new(); + + let mut planned_failures = self.failures.lock().unwrap(); + for subscription in &subscriptions { + if let Some(err) = planned_failures.remove(subscription) { + failures.push((subscription.clone(), err)); + } + } + let publication = Publication { message, subscriptions, }; self.sent.lock().unwrap().push(publication); - Ok(Vec::new()) + Ok(failures) } } +#[derive(Debug)] pub struct Publication { pub message: Box<dyn Any + Send>, - pub subscriptions: Vec<SubscriptionInfo>, + pub subscriptions: HashSet<SubscriptionInfo>, } impl Publication { |
