diff options
| author | Owen Jacobson <owen@grimoire.ca> | 2025-12-09 15:13:21 -0500 |
|---|---|---|
| committer | Owen Jacobson <owen@grimoire.ca> | 2025-12-17 15:48:20 -0500 |
| commit | 3c697f5fb1b8dbad46eac8fa299ed7cebfb36159 (patch) | |
| tree | b7854fb23d1e104f928acfe3bba75ea3b74b83d9 /src/test | |
| parent | 41a5a0f7e13bf5a82aaef59e34eb68f0fe7fa7f5 (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/test')
| -rw-r--r-- | src/test/fixtures/mod.rs | 12 | ||||
| -rw-r--r-- | src/test/fixtures/vapid.rs | 16 | ||||
| -rw-r--r-- | src/test/webpush.rs | 55 |
3 files changed, 71 insertions, 12 deletions
diff --git a/src/test/fixtures/mod.rs b/src/test/fixtures/mod.rs index 53bf31b..85935d6 100644 --- a/src/test/fixtures/mod.rs +++ b/src/test/fixtures/mod.rs @@ -12,8 +12,20 @@ pub mod invite; pub mod login; pub mod message; pub mod user; +pub mod vapid; pub async fn scratch_app() -> App<Client> { + let app = scratch_app_without_vapid().await; + + app.vapid() + .refresh_key(&now()) + .await + .expect("refreshing the VAPID key always succeeds"); + + app +} + +pub async fn scratch_app_without_vapid() -> App<Client> { let pool = db::prepare("sqlite::memory:", "sqlite::memory:") .await .expect("setting up in-memory sqlite database"); diff --git a/src/test/fixtures/vapid.rs b/src/test/fixtures/vapid.rs new file mode 100644 index 0000000..29cdf1a --- /dev/null +++ b/src/test/fixtures/vapid.rs @@ -0,0 +1,16 @@ +use p256::ecdsa::VerifyingKey; + +use crate::{app::App, test::fixtures}; + +pub async fn key<P>(app: &App<P>) -> VerifyingKey { + let boot = app.boot().snapshot().await.expect("boot always succeeds"); + let changed = boot + .events + .into_iter() + .filter_map(fixtures::event::vapid) + .filter_map(fixtures::event::vapid::changed) + .next_back() + .expect("the application has a vapid key"); + + changed.key +} diff --git a/src/test/webpush.rs b/src/test/webpush.rs index a611ad0..f33f03c 100644 --- a/src/test/webpush.rs +++ b/src/test/webpush.rs @@ -1,13 +1,16 @@ use std::{ + any::Any, mem, sync::{Arc, Mutex}, }; -use web_push::{WebPushClient, WebPushError, WebPushMessage}; +use web_push::{PartialVapidSignatureBuilder, SubscriptionInfo, WebPushError}; + +use crate::{error::failed::Failed, push::Publish}; #[derive(Clone)] pub struct Client { - sent: Arc<Mutex<Vec<WebPushMessage>>>, + sent: Arc<Mutex<Vec<Publication>>>, } impl Client { @@ -18,20 +21,48 @@ impl Client { } // Clears the list of sent messages (for all clones of this Client) when called, because we - // can't clone `WebPushMessage`s so we either need to move them or try to reconstruct them, - // either of which sucks but moving them sucks less. - pub fn sent(&self) -> Vec<WebPushMessage> { + // can't clone `Publications`s, so we either need to move them or try to reconstruct them. + pub fn sent(&self) -> Vec<Publication> { let mut sent = self.sent.lock().unwrap(); - mem::take(&mut *sent) + mem::take(&mut sent) } } -#[async_trait::async_trait] -impl WebPushClient for Client { - async fn send(&self, message: WebPushMessage) -> Result<(), WebPushError> { - let mut sent = self.sent.lock().unwrap(); - sent.push(message); +impl Publish for Client { + async fn publish<M>( + &self, + message: M, + _: PartialVapidSignatureBuilder, + subscriptions: impl IntoIterator<Item = SubscriptionInfo> + Send, + ) -> Result<Vec<(SubscriptionInfo, WebPushError)>, Failed> + where + M: Send + 'static, + { + let message: Box<dyn Any + Send> = Box::new(message); + let subscriptions = subscriptions.into_iter().collect(); + let publication = Publication { + message, + subscriptions, + }; + self.sent.lock().unwrap().push(publication); + + Ok(Vec::new()) + } +} + +pub struct Publication { + pub message: Box<dyn Any + Send>, + pub subscriptions: Vec<SubscriptionInfo>, +} - Ok(()) +impl Publication { + pub fn message_eq<M>(&self, candidate: &M) -> bool + where + M: PartialEq + 'static, + { + match self.message.downcast_ref::<M>() { + None => false, + Some(message) => message == candidate, + } } } |
