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}; #[tokio::test] async fn ping_without_subscriptions() { let app = fixtures::scratch_app().await; let recipient = fixtures::identity::create(&app, &fixtures::now()).await; 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.publisher() .sent() .into_iter() .filter(|publish| publish.message_eq(&Heartbeat::Heartbeat) && publish.recipients.is_empty()) .exactly_one() .is_ok() ); } #[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.recipients == 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.recipients == 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.recipients == recipient_subscriptions) .exactly_one() .is_ok() ); // Confirm that it was not sent to the spectator assert!( !sent .iter() .any(|publish| publish.recipients.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.recipients == 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.recipients.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.recipients == 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.recipients == 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.recipients == 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.recipients == subscriptions) .exactly_one() .is_ok() ); assert!( !sent .iter() .any(|publish| publish.recipients.contains(&failing)) ); }