From 2d05e6fb933d8c33078232b3bdfbc2aa05106d80 Mon Sep 17 00:00:00 2001 From: Owen Jacobson Date: Mon, 27 Oct 2025 17:07:53 -0400 Subject: Make `Boot` a freestanding app type, rather than a view of `crate::app::App`'s internals. In the course of working on web push, I determined that we probably need to make `App` generic over the web push client we're using, so that tests can use a dummy client while the real app uses a client created at startup and maintained over the life of the program's execution. The most direct implementation of that is to render App as `App

`, where the parameter is occupied by the specific web push client type in use. However, doing this requires refactoring at _every_ site that mentions `App`, including every handler, even though the vast majority of those sites will not be concerned with web push. I reviewed a few options with @wlonk: * Accept the type parameter and apply it everywhere, as the cost of supporting web push. * Hard-code the use of a specific web push client. * Insulate handlers &c from `App` via provider traits, mimicing what we do for repository provider traits today. * Treat each app type as a freestanding state in its own right, so that only push-related components need to consider push clients (as far as is feasible). This is a prototype towards that last point, using a simple app component (boot) as a testbed. `FromRef` allows handlers that take a `Boot` to be used in routes that provide an `App`, so this is a contained change. However, the structure of `FromRef` prevents `Boot` from carrying any lifetime narrower than `'static`, so it now holds clones of the state fields it acquires from App, instead of references. This is fine - that's just a database pool, and sqlx's pool type is designed to be shared via cloning. From : > Cloning Pool is cheap as it is simply a reference-counted handle to the inner pool state. --- src/app.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) (limited to 'src/app.rs') diff --git a/src/app.rs b/src/app.rs index e61672f..e33ee39 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,3 +1,4 @@ +use axum::extract::FromRef; use sqlx::sqlite::SqlitePool; #[cfg(test)] @@ -33,8 +34,8 @@ impl App { } impl App { - pub const fn boot(&self) -> Boot<'_> { - Boot::new(&self.db) + pub fn boot(&self) -> Boot { + Boot::new(self.db.clone()) } pub const fn conversations(&self) -> Conversations<'_> { @@ -70,3 +71,9 @@ impl App { Users::new(&self.db, &self.events) } } + +impl FromRef for Boot { + fn from_ref(app: &App) -> Self { + app.boot() + } +} -- cgit v1.2.3 From 398bc79a3f1f4316e6cc33b6e6bce133c1db3e4c Mon Sep 17 00:00:00 2001 From: Owen Jacobson Date: Mon, 27 Oct 2025 17:29:36 -0400 Subject: Convert the `Conversations` component into a freestanding struct. Unlike the previous example, this involves cloning an event broadcaster, as well. This is, per the documentation, how the type may be used. From : > The Sender can be cloned to send to the same channel from multiple points in the process or it can be used concurrently from an `Arc`. The language is less firm than the language sqlx uses for its pool, but the intent is clear enough, and it works in practice. --- src/app.rs | 10 ++++- src/conversation/app.rs | 10 ++--- src/conversation/handlers/create/mod.rs | 8 ++-- src/conversation/handlers/create/test.rs | 66 +++++++++++++++++++++----------- src/conversation/handlers/delete/mod.rs | 9 ++--- src/conversation/handlers/delete/test.rs | 12 +++--- src/test/fixtures/conversation.rs | 11 ++++-- src/ui/handlers/conversation.rs | 7 ++-- 8 files changed, 78 insertions(+), 55 deletions(-) (limited to 'src/app.rs') diff --git a/src/app.rs b/src/app.rs index e33ee39..3f3885a 100644 --- a/src/app.rs +++ b/src/app.rs @@ -38,8 +38,8 @@ impl App { Boot::new(self.db.clone()) } - pub const fn conversations(&self) -> Conversations<'_> { - Conversations::new(&self.db, &self.events) + pub fn conversations(&self) -> Conversations { + Conversations::new(self.db.clone(), self.events.clone()) } pub const fn events(&self) -> Events<'_> { @@ -77,3 +77,9 @@ impl FromRef for Boot { app.boot() } } + +impl FromRef for Conversations { + fn from_ref(app: &App) -> Self { + app.conversations() + } +} diff --git a/src/conversation/app.rs b/src/conversation/app.rs index 26886af..2b62e77 100644 --- a/src/conversation/app.rs +++ b/src/conversation/app.rs @@ -15,13 +15,13 @@ use crate::{ name::{self, Name}, }; -pub struct Conversations<'a> { - db: &'a SqlitePool, - events: &'a Broadcaster, +pub struct Conversations { + db: SqlitePool, + events: Broadcaster, } -impl<'a> Conversations<'a> { - pub const fn new(db: &'a SqlitePool, events: &'a Broadcaster) -> Self { +impl Conversations { + pub const fn new(db: SqlitePool, events: Broadcaster) -> Self { Self { db, events } } diff --git a/src/conversation/handlers/create/mod.rs b/src/conversation/handlers/create/mod.rs index 18eca1f..2b7fa39 100644 --- a/src/conversation/handlers/create/mod.rs +++ b/src/conversation/handlers/create/mod.rs @@ -5,9 +5,8 @@ use axum::{ }; use crate::{ - app::App, clock::RequestedAt, - conversation::{Conversation, app}, + conversation::{Conversation, app, app::Conversations}, error::Internal, name::Name, token::extract::Identity, @@ -17,13 +16,12 @@ use crate::{ mod test; pub async fn handler( - State(app): State, + State(conversations): State, _: Identity, // requires auth, but doesn't actually care who you are RequestedAt(created_at): RequestedAt, Json(request): Json, ) -> Result { - let conversation = app - .conversations() + let conversation = conversations .create(&request.name, &created_at) .await .map_err(Error)?; diff --git a/src/conversation/handlers/create/test.rs b/src/conversation/handlers/create/test.rs index bc05b00..380bb13 100644 --- a/src/conversation/handlers/create/test.rs +++ b/src/conversation/handlers/create/test.rs @@ -22,10 +22,14 @@ async fn new_conversation() { let name = fixtures::conversation::propose(); let request = super::Request { name: name.clone() }; - let super::Response(response) = - super::handler(State(app.clone()), creator, fixtures::now(), Json(request)) - .await - .expect("creating a conversation in an empty app succeeds"); + let super::Response(response) = super::handler( + State(app.conversations()), + creator, + fixtures::now(), + Json(request), + ) + .await + .expect("creating a conversation in an empty app succeeds"); // Verify the structure of the response @@ -77,10 +81,14 @@ async fn duplicate_name() { let request = super::Request { name: conversation.name.clone(), }; - let super::Error(error) = - super::handler(State(app.clone()), creator, fixtures::now(), Json(request)) - .await - .expect_err("duplicate conversation name should fail the request"); + let super::Error(error) = super::handler( + State(app.conversations()), + creator, + fixtures::now(), + Json(request), + ) + .await + .expect_err("duplicate conversation name should fail the request"); // Verify the structure of the response @@ -110,10 +118,14 @@ async fn conflicting_canonical_name() { let request = super::Request { name: conflicting_name.clone(), }; - let super::Error(error) = - super::handler(State(app.clone()), creator, fixtures::now(), Json(request)) - .await - .expect_err("duplicate conversation name should fail the request"); + let super::Error(error) = super::handler( + State(app.conversations()), + creator, + fixtures::now(), + Json(request), + ) + .await + .expect_err("duplicate conversation name should fail the request"); // Verify the structure of the response @@ -135,7 +147,7 @@ async fn invalid_name() { let name = fixtures::conversation::propose_invalid_name(); let request = super::Request { name: name.clone() }; let super::Error(error) = crate::conversation::handlers::create::handler( - State(app.clone()), + State(app.conversations()), creator, fixtures::now(), Json(request), @@ -163,7 +175,7 @@ async fn name_reusable_after_delete() { let request = super::Request { name: name.clone() }; let super::Response(response) = super::handler( - State(app.clone()), + State(app.conversations()), creator.clone(), fixtures::now(), Json(request), @@ -181,10 +193,14 @@ async fn name_reusable_after_delete() { // Call the endpoint (second time) let request = super::Request { name: name.clone() }; - let super::Response(response) = - super::handler(State(app.clone()), creator, fixtures::now(), Json(request)) - .await - .expect("creation succeeds after original conversation deleted"); + let super::Response(response) = super::handler( + State(app.conversations()), + creator, + fixtures::now(), + Json(request), + ) + .await + .expect("creation succeeds after original conversation deleted"); // Verify the structure of the response @@ -212,7 +228,7 @@ async fn name_reusable_after_expiry() { let request = super::Request { name: name.clone() }; let super::Response(_) = super::handler( - State(app.clone()), + State(app.conversations()), creator.clone(), fixtures::ancient(), Json(request), @@ -230,10 +246,14 @@ async fn name_reusable_after_expiry() { // Call the endpoint (second time) let request = super::Request { name: name.clone() }; - let super::Response(response) = - super::handler(State(app.clone()), creator, fixtures::now(), Json(request)) - .await - .expect("creation succeeds after original conversation expired"); + let super::Response(response) = super::handler( + State(app.conversations()), + creator, + fixtures::now(), + Json(request), + ) + .await + .expect("creation succeeds after original conversation expired"); // Verify the structure of the response diff --git a/src/conversation/handlers/delete/mod.rs b/src/conversation/handlers/delete/mod.rs index 272165a..231e433 100644 --- a/src/conversation/handlers/delete/mod.rs +++ b/src/conversation/handlers/delete/mod.rs @@ -5,9 +5,8 @@ use axum::{ }; use crate::{ - app::App, clock::RequestedAt, - conversation::{self, app, handlers::PathInfo}, + conversation::{self, app, app::Conversations, handlers::PathInfo}, error::{Internal, NotFound}, token::extract::Identity, }; @@ -16,14 +15,12 @@ use crate::{ mod test; pub async fn handler( - State(app): State, + State(conversations): State, Path(conversation): Path, RequestedAt(deleted_at): RequestedAt, _: Identity, ) -> Result { - app.conversations() - .delete(&conversation, &deleted_at) - .await?; + conversations.delete(&conversation, &deleted_at).await?; Ok(Response { id: conversation }) } diff --git a/src/conversation/handlers/delete/test.rs b/src/conversation/handlers/delete/test.rs index 2718d3b..e9e882a 100644 --- a/src/conversation/handlers/delete/test.rs +++ b/src/conversation/handlers/delete/test.rs @@ -14,7 +14,7 @@ pub async fn valid_conversation() { let deleter = fixtures::identity::create(&app, &fixtures::now()).await; let response = super::handler( - State(app.clone()), + State(app.conversations()), Path(conversation.id.clone()), fixtures::now(), deleter, @@ -52,7 +52,7 @@ pub async fn invalid_conversation_id() { let deleter = fixtures::identity::create(&app, &fixtures::now()).await; let conversation = fixtures::conversation::fictitious(); let super::Error(error) = super::handler( - State(app.clone()), + State(app.conversations()), Path(conversation.clone()), fixtures::now(), deleter, @@ -81,7 +81,7 @@ pub async fn conversation_deleted() { let deleter = fixtures::identity::create(&app, &fixtures::now()).await; let super::Error(error) = super::handler( - State(app.clone()), + State(app.conversations()), Path(conversation.id.clone()), fixtures::now(), deleter, @@ -110,7 +110,7 @@ pub async fn conversation_expired() { let deleter = fixtures::identity::create(&app, &fixtures::now()).await; let super::Error(error) = super::handler( - State(app.clone()), + State(app.conversations()), Path(conversation.id.clone()), fixtures::now(), deleter, @@ -144,7 +144,7 @@ pub async fn conversation_purged() { let deleter = fixtures::identity::create(&app, &fixtures::now()).await; let super::Error(error) = super::handler( - State(app.clone()), + State(app.conversations()), Path(conversation.id.clone()), fixtures::now(), deleter, @@ -170,7 +170,7 @@ pub async fn conversation_not_empty() { let deleter = fixtures::identity::create(&app, &fixtures::now()).await; let super::Error(error) = super::handler( - State(app.clone()), + State(app.conversations()), Path(conversation.id.clone()), fixtures::now(), deleter, diff --git a/src/test/fixtures/conversation.rs b/src/test/fixtures/conversation.rs index fb2f58d..f0d8c8c 100644 --- a/src/test/fixtures/conversation.rs +++ b/src/test/fixtures/conversation.rs @@ -1,3 +1,4 @@ +use axum::extract::FromRef; use faker_rand::{ en_us::{addresses::CityName, names::FullName}, faker_impl_from_templates, @@ -6,15 +7,17 @@ use faker_rand::{ use rand; use crate::{ - app::App, clock::RequestedAt, - conversation::{self, Conversation}, + conversation::{self, Conversation, app::Conversations}, name::Name, }; -pub async fn create(app: &App, created_at: &RequestedAt) -> Conversation { +pub async fn create(app: &App, created_at: &RequestedAt) -> Conversation +where + Conversations: FromRef, +{ let name = propose(); - app.conversations() + Conversations::from_ref(app) .create(&name, created_at) .await .expect("should always succeed if the conversation is actually new") diff --git a/src/ui/handlers/conversation.rs b/src/ui/handlers/conversation.rs index f1bb319..2ff090c 100644 --- a/src/ui/handlers/conversation.rs +++ b/src/ui/handlers/conversation.rs @@ -4,8 +4,7 @@ use axum::{ }; use crate::{ - app::App, - conversation::{self, app}, + conversation::{self, app, app::Conversations}, error::Internal, token::extract::Identity, ui::{ @@ -15,12 +14,12 @@ use crate::{ }; pub async fn handler( - State(app): State, + State(conversations): State, identity: Option, Path(conversation): Path, ) -> Result { let _ = identity.ok_or(Error::NotLoggedIn)?; - app.conversations() + conversations .get(&conversation) .await .map_err(Error::from)?; -- cgit v1.2.3 From 6de7402a002791c6216b12a40e74af9c8ab82c02 Mon Sep 17 00:00:00 2001 From: Owen Jacobson Date: Mon, 27 Oct 2025 17:33:30 -0400 Subject: Convert the `Events` app component into a freestanding struct. This one doesn't need a FromRef impl at this time, as it's only ever used in a handler that also uses other components and so will need to continue receiving `App`. However, there's little reason not to make the implementatino of the `Events` struct consistent. --- src/app.rs | 4 ++-- src/event/app.rs | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) (limited to 'src/app.rs') diff --git a/src/app.rs b/src/app.rs index 3f3885a..0b560dd 100644 --- a/src/app.rs +++ b/src/app.rs @@ -42,8 +42,8 @@ impl App { Conversations::new(self.db.clone(), self.events.clone()) } - pub const fn events(&self) -> Events<'_> { - Events::new(&self.db, &self.events) + pub fn events(&self) -> Events { + Events::new(self.db.clone(), self.events.clone()) } pub const fn invites(&self) -> Invites<'_> { diff --git a/src/event/app.rs b/src/event/app.rs index 7359bfb..8fa760a 100644 --- a/src/event/app.rs +++ b/src/event/app.rs @@ -13,13 +13,13 @@ use crate::{ user::{self, repo::Provider as _}, }; -pub struct Events<'a> { - db: &'a SqlitePool, - events: &'a Broadcaster, +pub struct Events { + db: SqlitePool, + events: Broadcaster, } -impl<'a> Events<'a> { - pub const fn new(db: &'a SqlitePool, events: &'a Broadcaster) -> Self { +impl Events { + pub const fn new(db: SqlitePool, events: Broadcaster) -> Self { Self { db, events } } -- cgit v1.2.3 From f305e487d619f1d993d11d728c8cf7261bf3b371 Mon Sep 17 00:00:00 2001 From: Owen Jacobson Date: Mon, 27 Oct 2025 17:41:22 -0400 Subject: Convert `Invites` into a freestanding component. --- src/app.rs | 10 ++++++++-- src/invite/app.rs | 12 ++++++------ src/invite/handlers/accept/mod.rs | 8 +++----- src/invite/handlers/accept/test.rs | 12 ++++++------ src/invite/handlers/get/mod.rs | 7 +++---- src/invite/handlers/get/test.rs | 6 +++--- src/invite/handlers/issue/mod.rs | 9 ++++++--- src/invite/handlers/issue/test.rs | 2 +- src/test/fixtures/invite.rs | 12 ++++++++---- src/ui/handlers/invite.rs | 6 +++--- 10 files changed, 47 insertions(+), 37 deletions(-) (limited to 'src/app.rs') diff --git a/src/app.rs b/src/app.rs index 0b560dd..2d6d62b 100644 --- a/src/app.rs +++ b/src/app.rs @@ -46,8 +46,8 @@ impl App { Events::new(self.db.clone(), self.events.clone()) } - pub const fn invites(&self) -> Invites<'_> { - Invites::new(&self.db, &self.events) + pub fn invites(&self) -> Invites { + Invites::new(self.db.clone(), self.events.clone()) } pub const fn logins(&self) -> Logins<'_> { @@ -83,3 +83,9 @@ impl FromRef for Conversations { app.conversations() } } + +impl FromRef for Invites { + fn from_ref(app: &App) -> Self { + app.invites() + } +} diff --git a/src/invite/app.rs b/src/invite/app.rs index 6684d03..6f58d0a 100644 --- a/src/invite/app.rs +++ b/src/invite/app.rs @@ -17,13 +17,13 @@ use crate::{ }, }; -pub struct Invites<'a> { - db: &'a SqlitePool, - events: &'a Broadcaster, +pub struct Invites { + db: SqlitePool, + events: Broadcaster, } -impl<'a> Invites<'a> { - pub const fn new(db: &'a SqlitePool, events: &'a Broadcaster) -> Self { +impl Invites { + pub const fn new(db: SqlitePool, events: Broadcaster) -> Self { Self { db, events } } @@ -88,7 +88,7 @@ impl<'a> Invites<'a> { tx.tokens().create(&token, &secret).await?; tx.commit().await?; - stored.publish(self.events); + stored.publish(&self.events); Ok(secret) } diff --git a/src/invite/handlers/accept/mod.rs b/src/invite/handlers/accept/mod.rs index cdf385f..8bdaa51 100644 --- a/src/invite/handlers/accept/mod.rs +++ b/src/invite/handlers/accept/mod.rs @@ -5,11 +5,10 @@ use axum::{ }; use crate::{ - app::App, clock::RequestedAt, empty::Empty, error::{Internal, NotFound}, - invite::{app, handlers::PathInfo}, + invite::{app, app::Invites, handlers::PathInfo}, name::Name, password::Password, token::extract::IdentityCookie, @@ -19,14 +18,13 @@ use crate::{ mod test; pub async fn handler( - State(app): State, + State(invites): State, RequestedAt(accepted_at): RequestedAt, identity: IdentityCookie, Path(invite): Path, Json(request): Json, ) -> Result<(IdentityCookie, Empty), Error> { - let secret = app - .invites() + let secret = invites .accept(&invite, &request.name, &request.password, &accepted_at) .await .map_err(Error)?; diff --git a/src/invite/handlers/accept/test.rs b/src/invite/handlers/accept/test.rs index 283ec76..446dbf9 100644 --- a/src/invite/handlers/accept/test.rs +++ b/src/invite/handlers/accept/test.rs @@ -24,7 +24,7 @@ async fn valid_invite() { password: password.clone(), }; let (identity, Empty) = super::handler( - State(app.clone()), + State(app.invites()), fixtures::now(), identity, Path(invite.id), @@ -67,7 +67,7 @@ async fn nonexistent_invite() { password: password.clone(), }; let super::Error(error) = super::handler( - State(app.clone()), + State(app.invites()), fixtures::now(), identity, Path(invite.clone()), @@ -103,7 +103,7 @@ async fn expired_invite() { password: password.clone(), }; let super::Error(error) = super::handler( - State(app.clone()), + State(app.invites()), fixtures::now(), identity, Path(invite.id.clone()), @@ -140,7 +140,7 @@ async fn accepted_invite() { password: password.clone(), }; let super::Error(error) = super::handler( - State(app.clone()), + State(app.invites()), fixtures::now(), identity, Path(invite.id.clone()), @@ -183,7 +183,7 @@ async fn conflicting_name() { password: password.clone(), }; let super::Error(error) = super::handler( - State(app.clone()), + State(app.invites()), fixtures::now(), identity, Path(invite.id.clone()), @@ -217,7 +217,7 @@ async fn invalid_name() { password: password.clone(), }; let super::Error(error) = super::handler( - State(app.clone()), + State(app.invites()), fixtures::now(), identity, Path(invite.id), diff --git a/src/invite/handlers/get/mod.rs b/src/invite/handlers/get/mod.rs index bb72586..d5fd9c2 100644 --- a/src/invite/handlers/get/mod.rs +++ b/src/invite/handlers/get/mod.rs @@ -4,19 +4,18 @@ use axum::{ }; use crate::{ - app::App, error::{Internal, NotFound}, - invite::{Id, Summary, handlers::PathInfo}, + invite::{Id, Summary, app::Invites, handlers::PathInfo}, }; #[cfg(test)] mod test; pub async fn handler( - State(app): State, + State(invites): State, Path(invite): Path, ) -> Result, Error> { - app.invites() + invites .get(&invite) .await? .map(Json) diff --git a/src/invite/handlers/get/test.rs b/src/invite/handlers/get/test.rs index 0f2f725..a08c510 100644 --- a/src/invite/handlers/get/test.rs +++ b/src/invite/handlers/get/test.rs @@ -12,7 +12,7 @@ async fn valid_invite() { // Call endpoint - let Json(response) = super::handler(State(app), Path(invite.id)) + let Json(response) = super::handler(State(app.invites()), Path(invite.id)) .await .expect("get for an existing invite succeeds"); @@ -31,7 +31,7 @@ async fn nonexistent_invite() { // Call endpoint let invite = fixtures::invite::fictitious(); - let error = super::handler(State(app), Path(invite.clone())) + let error = super::handler(State(app.invites()), Path(invite.clone())) .await .expect_err("get for a nonexistent invite fails"); @@ -55,7 +55,7 @@ async fn expired_invite() { // Call endpoint - let error = super::handler(State(app), Path(invite.id.clone())) + let error = super::handler(State(app.invites()), Path(invite.id.clone())) .await .expect_err("get for an expired invite fails"); diff --git a/src/invite/handlers/issue/mod.rs b/src/invite/handlers/issue/mod.rs index 4ac74cc..0549c78 100644 --- a/src/invite/handlers/issue/mod.rs +++ b/src/invite/handlers/issue/mod.rs @@ -1,19 +1,22 @@ use axum::extract::{Json, State}; use crate::{ - app::App, clock::RequestedAt, error::Internal, invite::Invite, token::extract::Identity, + clock::RequestedAt, + error::Internal, + invite::{Invite, app::Invites}, + token::extract::Identity, }; #[cfg(test)] mod test; pub async fn handler( - State(app): State, + State(invites): State, RequestedAt(issued_at): RequestedAt, identity: Identity, _: Json, ) -> Result, Internal> { - let invite = app.invites().issue(&identity.login, &issued_at).await?; + let invite = invites.issue(&identity.login, &issued_at).await?; Ok(Json(invite)) } diff --git a/src/invite/handlers/issue/test.rs b/src/invite/handlers/issue/test.rs index 4421705..dc89243 100644 --- a/src/invite/handlers/issue/test.rs +++ b/src/invite/handlers/issue/test.rs @@ -13,7 +13,7 @@ async fn create_invite() { // Call the endpoint let Json(invite) = super::handler( - State(app), + State(app.invites()), issued_at.clone(), issuer.clone(), Json(super::Request {}), diff --git a/src/test/fixtures/invite.rs b/src/test/fixtures/invite.rs index 654d1b4..5a5d4d0 100644 --- a/src/test/fixtures/invite.rs +++ b/src/test/fixtures/invite.rs @@ -1,12 +1,16 @@ +use axum::extract::FromRef; + use crate::{ - app::App, clock::DateTime, - invite::{self, Invite}, + invite::{self, Invite, app::Invites}, login::Login, }; -pub async fn issue(app: &App, issuer: &Login, issued_at: &DateTime) -> Invite { - app.invites() +pub async fn issue(app: &App, issuer: &Login, issued_at: &DateTime) -> Invite +where + Invites: FromRef, +{ + Invites::from_ref(app) .issue(issuer, issued_at) .await .expect("issuing invites never fails") diff --git a/src/ui/handlers/invite.rs b/src/ui/handlers/invite.rs index 0f9580a..edd6dc1 100644 --- a/src/ui/handlers/invite.rs +++ b/src/ui/handlers/invite.rs @@ -4,9 +4,9 @@ use axum::{ }; use crate::{ - app::App, error::Internal, invite, + invite::app::Invites, ui::{ assets::{Asset, Assets}, error::NotFound, @@ -14,10 +14,10 @@ use crate::{ }; pub async fn handler( - State(app): State, + State(invites): State, Path(invite): Path, ) -> Result { - app.invites() + invites .get(&invite) .await .map_err(Error::internal)? -- cgit v1.2.3 From 38ac83aef9667f1a4fe86e03e53565376081179f Mon Sep 17 00:00:00 2001 From: Owen Jacobson Date: Mon, 27 Oct 2025 17:47:30 -0400 Subject: Convert `Logins` into a freestanding component. --- src/app.rs | 10 ++++++++-- src/login/app.rs | 10 +++++----- src/login/handlers/login/mod.rs | 14 +++++++++----- src/login/handlers/login/test.rs | 8 ++++---- src/login/handlers/password/mod.rs | 8 +++----- src/login/handlers/password/test.rs | 2 +- src/test/fixtures/cookie.rs | 14 +++++++++----- 7 files changed, 39 insertions(+), 27 deletions(-) (limited to 'src/app.rs') diff --git a/src/app.rs b/src/app.rs index 2d6d62b..793bdab 100644 --- a/src/app.rs +++ b/src/app.rs @@ -50,8 +50,8 @@ impl App { Invites::new(self.db.clone(), self.events.clone()) } - pub const fn logins(&self) -> Logins<'_> { - Logins::new(&self.db, &self.token_events) + pub fn logins(&self) -> Logins { + Logins::new(self.db.clone(), self.token_events.clone()) } pub const fn messages(&self) -> Messages<'_> { @@ -89,3 +89,9 @@ impl FromRef for Invites { app.invites() } } + +impl FromRef for Logins { + fn from_ref(app: &App) -> Self { + app.logins() + } +} diff --git a/src/login/app.rs b/src/login/app.rs index e471000..a2f9636 100644 --- a/src/login/app.rs +++ b/src/login/app.rs @@ -9,13 +9,13 @@ use crate::{ token::{Broadcaster, Event as TokenEvent, Secret, Token, repo::Provider as _}, }; -pub struct Logins<'a> { - db: &'a SqlitePool, - token_events: &'a Broadcaster, +pub struct Logins { + db: SqlitePool, + token_events: Broadcaster, } -impl<'a> Logins<'a> { - pub const fn new(db: &'a SqlitePool, token_events: &'a Broadcaster) -> Self { +impl Logins { + pub const fn new(db: SqlitePool, token_events: Broadcaster) -> Self { Self { db, token_events } } diff --git a/src/login/handlers/login/mod.rs b/src/login/handlers/login/mod.rs index 6591984..2ce8a67 100644 --- a/src/login/handlers/login/mod.rs +++ b/src/login/handlers/login/mod.rs @@ -5,21 +5,25 @@ use axum::{ }; use crate::{ - app::App, clock::RequestedAt, empty::Empty, error::Internal, login::app, name::Name, - password::Password, token::extract::IdentityCookie, + clock::RequestedAt, + empty::Empty, + error::Internal, + login::{app, app::Logins}, + name::Name, + password::Password, + token::extract::IdentityCookie, }; #[cfg(test)] mod test; pub async fn handler( - State(app): State, + State(logins): State, RequestedAt(now): RequestedAt, identity: IdentityCookie, Json(request): Json, ) -> Result<(IdentityCookie, Empty), Error> { - let secret = app - .logins() + let secret = logins .with_password(&request.name, &request.password, &now) .await .map_err(Error)?; diff --git a/src/login/handlers/login/test.rs b/src/login/handlers/login/test.rs index f3911d0..7bb56b6 100644 --- a/src/login/handlers/login/test.rs +++ b/src/login/handlers/login/test.rs @@ -22,7 +22,7 @@ async fn correct_credentials() { password, }; let (identity, Empty) = - super::handler(State(app.clone()), logged_in_at, identity, Json(request)) + super::handler(State(app.logins()), logged_in_at, identity, Json(request)) .await .expect("logged in with valid credentials"); @@ -52,7 +52,7 @@ async fn invalid_name() { password, }; let super::Error(error) = - super::handler(State(app.clone()), logged_in_at, identity, Json(request)) + super::handler(State(app.logins()), logged_in_at, identity, Json(request)) .await .expect_err("logged in with an incorrect password fails"); @@ -77,7 +77,7 @@ async fn incorrect_password() { password: fixtures::user::propose_password(), }; let super::Error(error) = - super::handler(State(app.clone()), logged_in_at, identity, Json(request)) + super::handler(State(app.logins()), logged_in_at, identity, Json(request)) .await .expect_err("logged in with an incorrect password"); @@ -98,7 +98,7 @@ async fn token_expires() { let logged_in_at = fixtures::ancient(); let identity = fixtures::cookie::not_logged_in(); let request = super::Request { name, password }; - let (identity, _) = super::handler(State(app.clone()), logged_in_at, identity, Json(request)) + let (identity, _) = super::handler(State(app.logins()), logged_in_at, identity, Json(request)) .await .expect("logged in with valid credentials"); let secret = identity.secret().expect("logged in with valid credentials"); diff --git a/src/login/handlers/password/mod.rs b/src/login/handlers/password/mod.rs index 94c7fb4..8b82605 100644 --- a/src/login/handlers/password/mod.rs +++ b/src/login/handlers/password/mod.rs @@ -5,11 +5,10 @@ use axum::{ }; use crate::{ - app::App, clock::RequestedAt, empty::Empty, error::Internal, - login::app, + login::{app, app::Logins}, password::Password, token::extract::{Identity, IdentityCookie}, }; @@ -18,14 +17,13 @@ use crate::{ mod test; pub async fn handler( - State(app): State, + State(logins): State, RequestedAt(now): RequestedAt, identity: Identity, cookie: IdentityCookie, Json(request): Json, ) -> Result<(IdentityCookie, Empty), Error> { - let secret = app - .logins() + let secret = logins .change_password(&identity.login, &request.password, &request.to, &now) .await .map_err(Error)?; diff --git a/src/login/handlers/password/test.rs b/src/login/handlers/password/test.rs index ba2f28f..61d5b5a 100644 --- a/src/login/handlers/password/test.rs +++ b/src/login/handlers/password/test.rs @@ -21,7 +21,7 @@ async fn password_change() { to: to.clone(), }; let (new_cookie, Empty) = super::handler( - State(app.clone()), + State(app.logins()), fixtures::now(), identity.clone(), cookie.clone(), diff --git a/src/test/fixtures/cookie.rs b/src/test/fixtures/cookie.rs index 7dc5083..0b5ec9b 100644 --- a/src/test/fixtures/cookie.rs +++ b/src/test/fixtures/cookie.rs @@ -1,21 +1,25 @@ +use axum::extract::FromRef; use uuid::Uuid; use crate::{ - app::App, clock::RequestedAt, name::Name, password::Password, token::extract::IdentityCookie, + clock::RequestedAt, login::app::Logins, name::Name, password::Password, + token::extract::IdentityCookie, }; pub fn not_logged_in() -> IdentityCookie { IdentityCookie::new() } -pub async fn logged_in( +pub async fn logged_in( app: &App, credentials: &(Name, Password), now: &RequestedAt, -) -> IdentityCookie { +) -> IdentityCookie +where + Logins: FromRef, +{ let (name, password) = credentials; - let secret = app - .logins() + let secret = Logins::from_ref(app) .with_password(name, password, now) .await .expect("should succeed given known-valid credentials"); -- cgit v1.2.3 From c2a3a010c67776b9a459d7ba0930630ff25a3a51 Mon Sep 17 00:00:00 2001 From: Owen Jacobson Date: Mon, 27 Oct 2025 18:03:36 -0400 Subject: Convert the `Messages` component to a freestanding struct. --- src/app.rs | 10 ++++++++-- src/conversation/handlers/send/mod.rs | 11 ++++++----- src/conversation/handlers/send/test.rs | 6 +++--- src/message/app.rs | 10 +++++----- src/message/handlers/delete/mod.rs | 10 ++++++---- src/message/handlers/delete/test.rs | 12 ++++++------ src/test/fixtures/message.rs | 13 ++++++++----- 7 files changed, 42 insertions(+), 30 deletions(-) (limited to 'src/app.rs') diff --git a/src/app.rs b/src/app.rs index 793bdab..74e1070 100644 --- a/src/app.rs +++ b/src/app.rs @@ -54,8 +54,8 @@ impl App { Logins::new(self.db.clone(), self.token_events.clone()) } - pub const fn messages(&self) -> Messages<'_> { - Messages::new(&self.db, &self.events) + pub fn messages(&self) -> Messages { + Messages::new(self.db.clone(), self.events.clone()) } pub const fn setup(&self) -> Setup<'_> { @@ -95,3 +95,9 @@ impl FromRef for Logins { app.logins() } } + +impl FromRef for Messages { + fn from_ref(app: &App) -> Self { + app.messages() + } +} diff --git a/src/conversation/handlers/send/mod.rs b/src/conversation/handlers/send/mod.rs index c8be59c..ff63652 100644 --- a/src/conversation/handlers/send/mod.rs +++ b/src/conversation/handlers/send/mod.rs @@ -5,11 +5,13 @@ use axum::{ }; use crate::{ - app::App, clock::RequestedAt, conversation::handlers::PathInfo, error::{Internal, NotFound}, - message::{Body, Message, app::SendError}, + message::{ + Body, Message, + app::{Messages, SendError}, + }, token::extract::Identity, }; @@ -17,14 +19,13 @@ use crate::{ mod test; pub async fn handler( - State(app): State, + State(messages): State, Path(conversation): Path, RequestedAt(sent_at): RequestedAt, identity: Identity, Json(request): Json, ) -> Result { - let message = app - .messages() + let message = messages .send(&conversation, &identity.login, &sent_at, &request.body) .await?; diff --git a/src/conversation/handlers/send/test.rs b/src/conversation/handlers/send/test.rs index 8863090..013aaa4 100644 --- a/src/conversation/handlers/send/test.rs +++ b/src/conversation/handlers/send/test.rs @@ -28,7 +28,7 @@ async fn messages_in_order() { let request = super::Request { body: body.clone() }; let _ = super::handler( - State(app.clone()), + State(app.messages()), Path(conversation.id.clone()), sent_at.clone(), sender.clone(), @@ -75,7 +75,7 @@ async fn nonexistent_conversation() { body: fixtures::message::propose(), }; let super::Error(error) = super::handler( - State(app), + State(app.messages()), Path(conversation.clone()), sent_at, sender, @@ -112,7 +112,7 @@ async fn deleted_conversation() { body: fixtures::message::propose(), }; let super::Error(error) = super::handler( - State(app), + State(app.messages()), Path(conversation.id.clone()), sent_at, sender, diff --git a/src/message/app.rs b/src/message/app.rs index 647152e..cbcbff9 100644 --- a/src/message/app.rs +++ b/src/message/app.rs @@ -13,13 +13,13 @@ use crate::{ user::{self, repo::Provider as _}, }; -pub struct Messages<'a> { - db: &'a SqlitePool, - events: &'a Broadcaster, +pub struct Messages { + db: SqlitePool, + events: Broadcaster, } -impl<'a> Messages<'a> { - pub const fn new(db: &'a SqlitePool, events: &'a Broadcaster) -> Self { +impl Messages { + pub const fn new(db: SqlitePool, events: Broadcaster) -> Self { Self { db, events } } diff --git a/src/message/handlers/delete/mod.rs b/src/message/handlers/delete/mod.rs index 3e9a212..c09a752 100644 --- a/src/message/handlers/delete/mod.rs +++ b/src/message/handlers/delete/mod.rs @@ -5,10 +5,12 @@ use axum::{ }; use crate::{ - app::App, clock::RequestedAt, error::{Internal, NotFound}, - message::{self, app::DeleteError}, + message::{ + self, + app::{DeleteError, Messages}, + }, token::extract::Identity, }; @@ -16,12 +18,12 @@ use crate::{ mod test; pub async fn handler( - State(app): State, + State(messages): State, Path(message): Path, RequestedAt(deleted_at): RequestedAt, identity: Identity, ) -> Result { - app.messages() + messages .delete(&identity.login, &message, &deleted_at) .await?; diff --git a/src/message/handlers/delete/test.rs b/src/message/handlers/delete/test.rs index 05d9344..198728b 100644 --- a/src/message/handlers/delete/test.rs +++ b/src/message/handlers/delete/test.rs @@ -16,7 +16,7 @@ pub async fn delete_message() { // Send the request let response = super::handler( - State(app.clone()), + State(app.messages()), Path(message.id.clone()), fixtures::now(), sender, @@ -52,7 +52,7 @@ pub async fn delete_invalid_message_id() { let deleter = fixtures::identity::create(&app, &fixtures::now()).await; let message = fixtures::message::fictitious(); let super::Error(error) = super::handler( - State(app.clone()), + State(app.messages()), Path(message.clone()), fixtures::now(), deleter, @@ -83,7 +83,7 @@ pub async fn delete_deleted() { // Send the request let super::Error(error) = super::handler( - State(app.clone()), + State(app.messages()), Path(message.id.clone()), fixtures::now(), sender, @@ -114,7 +114,7 @@ pub async fn delete_expired() { // Send the request let super::Error(error) = super::handler( - State(app.clone()), + State(app.messages()), Path(message.id.clone()), fixtures::now(), sender, @@ -150,7 +150,7 @@ pub async fn delete_purged() { let deleter = fixtures::identity::create(&app, &fixtures::now()).await; let super::Error(error) = super::handler( - State(app.clone()), + State(app.messages()), Path(message.id.clone()), fixtures::now(), deleter, @@ -176,7 +176,7 @@ pub async fn delete_not_sender() { let deleter = fixtures::identity::create(&app, &fixtures::now()).await; let super::Error(error) = super::handler( - State(app.clone()), + State(app.messages()), Path(message.id.clone()), fixtures::now(), deleter.clone(), diff --git a/src/test/fixtures/message.rs b/src/test/fixtures/message.rs index 92ac1f5..0bd0b7a 100644 --- a/src/test/fixtures/message.rs +++ b/src/test/fixtures/message.rs @@ -1,22 +1,25 @@ +use axum::extract::FromRef; use faker_rand::lorem::Paragraphs; use crate::{ - app::App, clock::RequestedAt, conversation::Conversation, login::Login, - message::{self, Body, Message}, + message::{self, Body, Message, app::Messages}, }; -pub async fn send( +pub async fn send( app: &App, conversation: &Conversation, sender: &Login, sent_at: &RequestedAt, -) -> Message { +) -> Message +where + Messages: FromRef, +{ let body = propose(); - app.messages() + Messages::from_ref(app) .send(&conversation.id, sender, sent_at, &body) .await .expect("should succeed if the conversation exists") -- cgit v1.2.3 From d66728889105f6f1ef5113d9ceb223e362df0008 Mon Sep 17 00:00:00 2001 From: Owen Jacobson Date: Mon, 27 Oct 2025 18:15:25 -0400 Subject: Convert the `Setup` component into a freestanding struct. The changes to the setup-requiring middleware are probably more general than was strictly needed, but they will make it work with anything that can provide a `Setup` component rather than being bolted to `App` specifically, which feels tidier. --- src/app.rs | 10 ++++++++-- src/setup/app.rs | 13 +++++++------ src/setup/handlers/setup/mod.rs | 14 +++++++++----- src/setup/handlers/setup/test.rs | 6 +++--- src/setup/required.rs | 31 ++++++++++++++++++------------- src/ui/handlers/setup.rs | 7 +++---- 6 files changed, 48 insertions(+), 33 deletions(-) (limited to 'src/app.rs') diff --git a/src/app.rs b/src/app.rs index 74e1070..202d542 100644 --- a/src/app.rs +++ b/src/app.rs @@ -58,8 +58,8 @@ impl App { Messages::new(self.db.clone(), self.events.clone()) } - pub const fn setup(&self) -> Setup<'_> { - Setup::new(&self.db, &self.events) + pub fn setup(&self) -> Setup { + Setup::new(self.db.clone(), self.events.clone()) } pub const fn tokens(&self) -> Tokens<'_> { @@ -101,3 +101,9 @@ impl FromRef for Messages { app.messages() } } + +impl FromRef for Setup { + fn from_ref(app: &App) -> Self { + app.setup() + } +} diff --git a/src/setup/app.rs b/src/setup/app.rs index 2a8ec30..9539406 100644 --- a/src/setup/app.rs +++ b/src/setup/app.rs @@ -10,13 +10,14 @@ use crate::{ user::create::{self, Create}, }; -pub struct Setup<'a> { - db: &'a SqlitePool, - events: &'a Broadcaster, +#[derive(Clone)] +pub struct Setup { + db: SqlitePool, + events: Broadcaster, } -impl<'a> Setup<'a> { - pub const fn new(db: &'a SqlitePool, events: &'a Broadcaster) -> Self { +impl Setup { + pub const fn new(db: SqlitePool, events: Broadcaster) -> Self { Self { db, events } } @@ -41,7 +42,7 @@ impl<'a> Setup<'a> { tx.tokens().create(&token, &secret).await?; tx.commit().await?; - stored.publish(self.events); + stored.publish(&self.events); Ok(secret) } diff --git a/src/setup/handlers/setup/mod.rs b/src/setup/handlers/setup/mod.rs index fe24798..2977da8 100644 --- a/src/setup/handlers/setup/mod.rs +++ b/src/setup/handlers/setup/mod.rs @@ -5,21 +5,25 @@ use axum::{ }; use crate::{ - app::App, clock::RequestedAt, empty::Empty, error::Internal, name::Name, password::Password, - setup::app, token::extract::IdentityCookie, + clock::RequestedAt, + empty::Empty, + error::Internal, + name::Name, + password::Password, + setup::{app, app::Setup}, + token::extract::IdentityCookie, }; #[cfg(test)] mod test; pub async fn handler( - State(app): State, + State(setup): State, RequestedAt(setup_at): RequestedAt, identity: IdentityCookie, Json(request): Json, ) -> Result<(IdentityCookie, Empty), Error> { - let secret = app - .setup() + let secret = setup .initial(&request.name, &request.password, &setup_at) .await .map_err(Error)?; diff --git a/src/setup/handlers/setup/test.rs b/src/setup/handlers/setup/test.rs index 283fe8b..670c111 100644 --- a/src/setup/handlers/setup/test.rs +++ b/src/setup/handlers/setup/test.rs @@ -20,7 +20,7 @@ async fn fresh_instance() { password: password.clone(), }; let (identity, Empty) = - super::handler(State(app.clone()), fixtures::now(), identity, Json(request)) + super::handler(State(app.setup()), fixtures::now(), identity, Json(request)) .await .expect("setup in a fresh app succeeds"); @@ -43,7 +43,7 @@ async fn login_exists() { let (name, password) = fixtures::user::propose(); let request = super::Request { name, password }; let super::Error(error) = - super::handler(State(app.clone()), fixtures::now(), identity, Json(request)) + super::handler(State(app.setup()), fixtures::now(), identity, Json(request)) .await .expect_err("setup in a populated app fails"); @@ -68,7 +68,7 @@ async fn invalid_name() { password: password.clone(), }; let super::Error(error) = - super::handler(State(app.clone()), fixtures::now(), identity, Json(request)) + super::handler(State(app.setup()), fixtures::now(), identity, Json(request)) .await .expect_err("setup with an invalid name fails"); diff --git a/src/setup/required.rs b/src/setup/required.rs index a2aed18..e475381 100644 --- a/src/setup/required.rs +++ b/src/setup/required.rs @@ -4,26 +4,29 @@ use std::{ }; use axum::{ - extract::Request, + extract::{FromRef, Request}, http::StatusCode, response::{IntoResponse, Response}, }; use tower::{Layer, Service}; -use crate::{app::App, error::Internal}; +use crate::{error::Internal, setup::app::Setup}; #[derive(Clone)] -pub struct Required(pub App); +pub struct Required(pub App); -impl Required { - pub fn with_fallback(self, fallback: F) -> WithFallback { +impl Required { + pub fn with_fallback(self, fallback: F) -> WithFallback { let Self(app) = self; WithFallback { app, fallback } } } -impl Layer for Required { - type Service = Middleware; +impl Layer for Required +where + Self: Clone, +{ + type Service = Middleware; fn layer(&self, inner: S) -> Self::Service { let Self(app) = self.clone(); @@ -36,16 +39,16 @@ impl Layer for Required { } #[derive(Clone)] -pub struct WithFallback { +pub struct WithFallback { app: App, fallback: F, } -impl Layer for WithFallback +impl Layer for WithFallback where Self: Clone, { - type Service = Middleware; + type Service = Middleware; fn layer(&self, inner: S) -> Self::Service { let Self { app, fallback } = self.clone(); @@ -58,17 +61,19 @@ where } #[derive(Clone)] -pub struct Middleware { +pub struct Middleware { inner: S, app: App, fallback: F, } -impl Service for Middleware +impl Service for Middleware where + Setup: FromRef, Self: Clone, S: Service + Send + 'static, S::Future: Send, + App: Send + 'static, F: IntoResponse + Clone + Send + 'static, { type Response = S::Response; @@ -87,7 +92,7 @@ where } = self.clone(); Box::pin(async move { - match app.setup().completed().await { + match Setup::from_ref(&app).completed().await { Ok(true) => inner.call(req).await, Ok(false) => Ok(fallback.into_response()), Err(error) => Ok(Internal::from(error).into_response()), diff --git a/src/ui/handlers/setup.rs b/src/ui/handlers/setup.rs index 49821cf..5707765 100644 --- a/src/ui/handlers/setup.rs +++ b/src/ui/handlers/setup.rs @@ -4,14 +4,13 @@ use axum::{ }; use crate::{ - app::App, error::Internal, + setup::app::Setup, ui::assets::{Asset, Assets}, }; -pub async fn handler(State(app): State) -> Result { - if app - .setup() +pub async fn handler(State(setup): State) -> Result { + if setup .completed() .await .map_err(Internal::from) -- cgit v1.2.3 From be21b088f0d1b591cbd8dcfed1e06f2742a524d0 Mon Sep 17 00:00:00 2001 From: Owen Jacobson Date: Mon, 27 Oct 2025 18:23:34 -0400 Subject: Convert the `Tokens` component into a freestanding struct. As with the `Setup` component, I've generalized the associated middleware across anything that can provide a `Tokens`, where possible. --- src/app.rs | 10 ++++++++-- src/login/handlers/logout/mod.rs | 9 ++++----- src/login/handlers/logout/test.rs | 25 +++++++++++++++++-------- src/test/verify/identity.rs | 26 +++++++++++++++++++------- src/test/verify/token.rs | 29 ++++++++++++++++++----------- src/token/app.rs | 10 +++++----- src/token/extract/identity.rs | 24 +++++++++++++++++------- 7 files changed, 88 insertions(+), 45 deletions(-) (limited to 'src/app.rs') diff --git a/src/app.rs b/src/app.rs index 202d542..6f6e8ba 100644 --- a/src/app.rs +++ b/src/app.rs @@ -62,8 +62,8 @@ impl App { Setup::new(self.db.clone(), self.events.clone()) } - pub const fn tokens(&self) -> Tokens<'_> { - Tokens::new(&self.db, &self.token_events) + pub fn tokens(&self) -> Tokens { + Tokens::new(self.db.clone(), self.token_events.clone()) } #[cfg(test)] @@ -107,3 +107,9 @@ impl FromRef for Setup { app.setup() } } + +impl FromRef for Tokens { + fn from_ref(app: &App) -> Self { + app.tokens() + } +} diff --git a/src/login/handlers/logout/mod.rs b/src/login/handlers/logout/mod.rs index 73efe73..ce4cb1a 100644 --- a/src/login/handlers/logout/mod.rs +++ b/src/login/handlers/logout/mod.rs @@ -4,25 +4,24 @@ use axum::{ }; use crate::{ - app::App, clock::RequestedAt, empty::Empty, error::{Internal, Unauthorized}, - token::{app, extract::IdentityCookie}, + token::{app, app::Tokens, extract::IdentityCookie}, }; #[cfg(test)] mod test; pub async fn handler( - State(app): State, + State(tokens): State, RequestedAt(now): RequestedAt, identity: IdentityCookie, Json(_): Json, ) -> Result<(IdentityCookie, Empty), Error> { if let Some(secret) = identity.secret() { - let identity = app.tokens().validate(&secret, &now).await?; - app.tokens().logout(&identity.token).await?; + let identity = tokens.validate(&secret, &now).await?; + tokens.logout(&identity.token).await?; } let identity = identity.clear(); diff --git a/src/login/handlers/logout/test.rs b/src/login/handlers/logout/test.rs index e7b7dd4..18744ed 100644 --- a/src/login/handlers/logout/test.rs +++ b/src/login/handlers/logout/test.rs @@ -18,7 +18,7 @@ async fn successful() { // Call the endpoint let (response_identity, Empty) = super::handler( - State(app.clone()), + State(app.tokens()), fixtures::now(), identity.clone(), Json::default(), @@ -42,9 +42,14 @@ async fn no_identity() { // Call the endpoint let identity = fixtures::cookie::not_logged_in(); - let (identity, Empty) = super::handler(State(app), fixtures::now(), identity, Json::default()) - .await - .expect("logged out with no token succeeds"); + let (identity, Empty) = super::handler( + State(app.tokens()), + fixtures::now(), + identity, + Json::default(), + ) + .await + .expect("logged out with no token succeeds"); // Verify the return value's basic structure @@ -60,10 +65,14 @@ async fn invalid_token() { // Call the endpoint let identity = fixtures::cookie::fictitious(); - let super::Error(error) = - super::handler(State(app), fixtures::now(), identity, Json::default()) - .await - .expect_err("logged out with an invalid token fails"); + let super::Error(error) = super::handler( + State(app.tokens()), + fixtures::now(), + identity, + Json::default(), + ) + .await + .expect_err("logged out with an invalid token fails"); // Verify the return value's basic structure diff --git a/src/test/verify/identity.rs b/src/test/verify/identity.rs index 8e2d36e..fba2a4d 100644 --- a/src/test/verify/identity.rs +++ b/src/test/verify/identity.rs @@ -1,31 +1,43 @@ +use axum::extract::FromRef; + use crate::{ - app::App, login::Login, name::Name, test::{fixtures, verify}, - token::{app::ValidateError, extract::IdentityCookie}, + token::{ + app::{Tokens, ValidateError}, + extract::IdentityCookie, + }, }; -pub async fn valid_for_name(app: &App, identity: &IdentityCookie, name: &Name) { +pub async fn valid_for_name(app: &App, identity: &IdentityCookie, name: &Name) +where + Tokens: FromRef, +{ let secret = identity .secret() .expect("identity cookie must be set to be valid"); verify::token::valid_for_name(app, &secret, name).await; } -pub async fn valid_for_login(app: &App, identity: &IdentityCookie, login: &Login) { +pub async fn valid_for_login(app: &App, identity: &IdentityCookie, login: &Login) +where + Tokens: FromRef, +{ let secret = identity .secret() .expect("identity cookie must be set to be valid"); verify::token::valid_for_login(app, &secret, login).await; } -pub async fn invalid(app: &App, identity: &IdentityCookie) { +pub async fn invalid(app: &App, identity: &IdentityCookie) +where + Tokens: FromRef, +{ let secret = identity .secret() .expect("identity cookie must be set to be invalid"); - let validate_err = app - .tokens() + let validate_err = Tokens::from_ref(app) .validate(&secret, &fixtures::now()) .await .expect_err("identity cookie secret must be invalid"); diff --git a/src/test/verify/token.rs b/src/test/verify/token.rs index adc4397..1b61a19 100644 --- a/src/test/verify/token.rs +++ b/src/test/verify/token.rs @@ -1,32 +1,39 @@ +use axum::extract::FromRef; + use crate::{ - app::App, login::Login, name::Name, test::fixtures, - token::{Secret, app}, + token::{Secret, app, app::Tokens}, }; -pub async fn valid_for_name(app: &App, secret: &Secret, name: &Name) { - let identity = app - .tokens() +pub async fn valid_for_name(app: &App, secret: &Secret, name: &Name) +where + Tokens: FromRef, +{ + let identity = Tokens::from_ref(app) .validate(secret, &fixtures::now()) .await .expect("provided secret is valid"); assert_eq!(name, &identity.login.name); } -pub async fn valid_for_login(app: &App, secret: &Secret, login: &Login) { - let identity = app - .tokens() +pub async fn valid_for_login(app: &App, secret: &Secret, login: &Login) +where + Tokens: FromRef, +{ + let identity = Tokens::from_ref(app) .validate(secret, &fixtures::now()) .await .expect("provided secret is valid"); assert_eq!(login, &identity.login); } -pub async fn invalid(app: &App, secret: &Secret) { - let error = app - .tokens() +pub async fn invalid(app: &App, secret: &Secret) +where + Tokens: FromRef, +{ + let error = Tokens::from_ref(app) .validate(secret, &fixtures::now()) .await .expect_err("provided secret is invalid"); diff --git a/src/token/app.rs b/src/token/app.rs index 1d68f32..332473d 100644 --- a/src/token/app.rs +++ b/src/token/app.rs @@ -12,13 +12,13 @@ use super::{ }; use crate::{clock::DateTime, db::NotFound as _, name}; -pub struct Tokens<'a> { - db: &'a SqlitePool, - token_events: &'a Broadcaster, +pub struct Tokens { + db: SqlitePool, + token_events: Broadcaster, } -impl<'a> Tokens<'a> { - pub const fn new(db: &'a SqlitePool, token_events: &'a Broadcaster) -> Self { +impl Tokens { + pub const fn new(db: SqlitePool, token_events: Broadcaster) -> Self { Self { db, token_events } } diff --git a/src/token/extract/identity.rs b/src/token/extract/identity.rs index bee4e31..5c004ef 100644 --- a/src/token/extract/identity.rs +++ b/src/token/extract/identity.rs @@ -1,16 +1,18 @@ use axum::{ - extract::{FromRequestParts, OptionalFromRequestParts, State}, + extract::{FromRef, FromRequestParts, OptionalFromRequestParts, State}, http::request::Parts, response::{IntoResponse, Response}, }; use super::IdentityCookie; use crate::{ - app::App, clock::RequestedAt, error::{Internal, Unauthorized}, login::Login, - token::{Token, app::ValidateError}, + token::{ + Token, + app::{Tokens, ValidateError}, + }, }; #[derive(Clone, Debug)] @@ -19,7 +21,11 @@ pub struct Identity { pub login: Login, } -impl FromRequestParts for Identity { +impl FromRequestParts for Identity +where + Tokens: FromRef, + App: Send + Sync, +{ type Rejection = LoginError; async fn from_request_parts(parts: &mut Parts, state: &App) -> Result { @@ -28,8 +34,8 @@ impl FromRequestParts for Identity { let secret = cookie.secret().ok_or(LoginError::Unauthorized)?; - let app = State::::from_request_parts(parts, state).await?; - app.tokens() + let tokens = State::::from_request_parts(parts, state).await?; + tokens .validate(&secret, &used_at) .await .map_err(|err| match err { @@ -39,7 +45,11 @@ impl FromRequestParts for Identity { } } -impl OptionalFromRequestParts for Identity { +impl OptionalFromRequestParts for Identity +where + Tokens: FromRef, + App: Send + Sync, +{ type Rejection = LoginError; async fn from_request_parts( -- cgit v1.2.3 From 9bd5ea4b4292761b0b2f823c887f114459c80d8f Mon Sep 17 00:00:00 2001 From: Owen Jacobson Date: Mon, 27 Oct 2025 18:26:41 -0400 Subject: Convert the `Users` component into a freestanding struct. Because `Users` is test-only and is not used in any endpoints, it doesn't need a FromRef impl. --- src/app.rs | 4 ++-- src/user/app.rs | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) (limited to 'src/app.rs') diff --git a/src/app.rs b/src/app.rs index 6f6e8ba..ad19bc0 100644 --- a/src/app.rs +++ b/src/app.rs @@ -67,8 +67,8 @@ impl App { } #[cfg(test)] - pub const fn users(&self) -> Users<'_> { - Users::new(&self.db, &self.events) + pub fn users(&self) -> Users { + Users::new(self.db.clone(), self.events.clone()) } } diff --git a/src/user/app.rs b/src/user/app.rs index 0d6046c..891e3b9 100644 --- a/src/user/app.rs +++ b/src/user/app.rs @@ -3,13 +3,13 @@ use sqlx::sqlite::SqlitePool; use super::create::{self, Create}; use crate::{clock::DateTime, event::Broadcaster, login::Login, name::Name, password::Password}; -pub struct Users<'a> { - db: &'a SqlitePool, - events: &'a Broadcaster, +pub struct Users { + db: SqlitePool, + events: Broadcaster, } -impl<'a> Users<'a> { - pub const fn new(db: &'a SqlitePool, events: &'a Broadcaster) -> Self { +impl Users { + pub const fn new(db: SqlitePool, events: Broadcaster) -> Self { Self { db, events } } @@ -27,7 +27,7 @@ impl<'a> Users<'a> { tx.commit().await?; let login = stored.login().to_owned(); - stored.publish(self.events); + stored.publish(&self.events); Ok(login) } -- cgit v1.2.3