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/push/handlers/ping | |
| parent | 3c697f5fb1b8dbad46eac8fa299ed7cebfb36159 (diff) | |
Now that testing push-generating endpoints is more feasible, implement the missing test cases for `ping`.
Diffstat (limited to 'src/push/handlers/ping')
| -rw-r--r-- | src/push/handlers/ping/test.rs | 356 |
1 files changed, 343 insertions, 13 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)) + ); +} |
