diff options
| author | ojacobson <ojacobson@noreply.codeberg.org> | 2025-11-07 23:17:15 +0100 |
|---|---|---|
| committer | ojacobson <ojacobson@noreply.codeberg.org> | 2025-11-07 23:17:15 +0100 |
| commit | 9e6f19f0f188eaa7f8b6be21c8405786cfb0dddd (patch) | |
| tree | b2999341645dec61e8143d7bb1b8a9d0056e0db1 /src/push/handlers/subscribe/test.rs | |
| parent | 3c588861ef5814de329743147398dbae22c1aeeb (diff) | |
| parent | 78d901328261d2306cf59c8e83fc217a63aa4a64 (diff) | |
Set up infrastructure for push message subscriptions.
A subscription allows an application server (here, the Pilcrow server) to send web push messages to a user agent.
On the server, Pilcrow records subscriptions verbatim, in the clear. Each subscription has an associated key, which will be used to encrypt messages for the corresponding client, but we store them in the clear, for the same broad reason that we store the VAPID key in the clear. They allow anyone who obtains them to impersonate the server and send push messages to clients, but they're rotated regularly - clients must rotate them whenever the server's VAPID key changes.
On the client, we monitor VAPID key change events to drive automatic subscription management, once the user sets up an initial subscription manually (which we must do as it can involve a user-interaction-only prompt for permission to send notifications). This isn't the final UI, but rather a bare-minimum version to let us move on with testing push notifications.
Merges push-subscribe into push-notify.
Diffstat (limited to 'src/push/handlers/subscribe/test.rs')
| -rw-r--r-- | src/push/handlers/subscribe/test.rs | 236 |
1 files changed, 236 insertions, 0 deletions
diff --git a/src/push/handlers/subscribe/test.rs b/src/push/handlers/subscribe/test.rs new file mode 100644 index 0000000..b72624d --- /dev/null +++ b/src/push/handlers/subscribe/test.rs @@ -0,0 +1,236 @@ +use axum::{ + extract::{Json, State}, + http::StatusCode, +}; + +use crate::{ + push::app::SubscribeError, + test::{fixtures, fixtures::event}, +}; + +#[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"); + + // Create a dummy subscription with that key. + + let request = super::Request { + subscription: super::Subscription { + endpoint: String::from("https://push.example.com/endpoint"), + keys: super::Keys { + p256dh: String::from("test-p256dh-value"), + auth: String::from("test-auth-value"), + }, + }, + vapid: vapid.key, + }; + let response = super::handler(State(app.push()), subscriber, Json(request)) + .await + .expect("test request will succeed on a fresh app"); + + // Check that the response looks as expected. + + assert_eq!(StatusCode::CREATED, response); +} + +#[tokio::test] +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"); + + // Create a dummy subscription with that key. + + let request = super::Request { + subscription: super::Subscription { + endpoint: String::from("https://push.example.com/endpoint"), + keys: super::Keys { + p256dh: String::from("test-p256dh-value"), + auth: String::from("test-auth-value"), + }, + }, + vapid: vapid.key, + }; + let response = super::handler(State(app.push()), subscriber.clone(), Json(request.clone())) + .await + .expect("test request will succeed on a fresh app"); + + // Check that the response looks as expected. + + assert_eq!(StatusCode::CREATED, response); + + // Repeat the request + + let response = super::handler(State(app.push()), subscriber, Json(request)) + .await + .expect("test request will succeed twice on a fresh app"); + + // Check that the second response also looks as expected. + + assert_eq!(StatusCode::CREATED, response); +} + +#[tokio::test] +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"); + + // Create a dummy subscription with that key. + + let request = super::Request { + subscription: super::Subscription { + endpoint: String::from("https://push.example.com/endpoint"), + keys: super::Keys { + p256dh: String::from("test-p256dh-value"), + auth: String::from("test-auth-value"), + }, + }, + vapid: vapid.key, + }; + super::handler(State(app.push()), subscriber.clone(), Json(request)) + .await + .expect("test request will succeed on a fresh app"); + + // Repeat the request with different keys + + let request = super::Request { + subscription: super::Subscription { + endpoint: String::from("https://push.example.com/endpoint"), + keys: super::Keys { + p256dh: String::from("different-test-p256dh-value"), + auth: String::from("different-test-auth-value"), + }, + }, + vapid: vapid.key, + }; + let response = super::handler(State(app.push()), subscriber, Json(request)) + .await + .expect_err("request with duplicate endpoint should fail"); + + // Make sure we got the error we expected. + + assert!(matches!(response, super::Error(SubscribeError::Duplicate))); +} + +#[tokio::test] +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"); + + // Change the VAPID key. + + app.vapid() + .rotate_key() + .await + .expect("key rotation always succeeds"); + app.vapid() + .refresh_key(&fixtures::now()) + .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"); + + // Create a dummy subscription with the original key. + + let request = super::Request { + subscription: super::Subscription { + endpoint: String::from("https://push.example.com/endpoint"), + keys: super::Keys { + p256dh: String::from("test-p256dh-value"), + auth: String::from("test-auth-value"), + }, + }, + vapid: vapid.key, + }; + let response = super::handler(State(app.push()), subscriber, Json(request)) + .await + .expect_err("test request has a stale vapid key"); + + // Check that the response looks as expected. + + assert!(matches!( + response, + super::Error(SubscribeError::StaleVapidKey(key)) if key == fresh_vapid.key + )); +} |
