summaryrefslogtreecommitdiff
path: root/src/test
diff options
context:
space:
mode:
authorojacobson <ojacobson@noreply.codeberg.org>2025-10-28 18:41:41 +0100
committerojacobson <ojacobson@noreply.codeberg.org>2025-10-28 18:41:41 +0100
commit0ef69c7d256380e660edc45ace7f1d6151226340 (patch)
treea6eea3af13f237393057aac1d80f024bc1b40b10 /src/test
parent58e6496558a01052537c5272169aea3e79ccbc8e (diff)
parentdc7074bbf39aff895ba52abd5e7b7e9bb643cf27 (diff)
Use freestanding structs for `App` components.
A "component" is a struct that provides domain-specific operations on the service. `App` largely acts as a way to obtain components for domain-specific operations: for example, given `let app: App = todo!();`, then `app.tokens()` provides a component (`Tokens`) that supports operations on authentication tokens, and `app.conversations()` provides a component (`Conversations`) that supports operations on conversations. This has been a major piece of the server's internal organization for a long time. Historically, these components have been built as short-lived view structs, which hold onto their functional dependencies by reference. A given component was therefore bound to its source `App`, and had a lifetime bounded by the life of that `App` instance or reference. This change turns components into freestanding structs - that is, they can outlive the `App` that provided them. They hold their dependencies by value, not by reference; `App` provides clones when creating a component, instead of borrowing its own state. For the functional dependencies we have today, cloning is a supported and cheap way to share access; details are documented in the individual commits. I'm making this change because we're working on web push, and we discovered while prototyping that it will be useful to be able to support multiple distinct types of web push client. A running Pilcrow server would use a "real" client (which sends real HTTP requests to deliver push messages), while tests would use a client stub (which doesn't). However, to make that work, we'll need to make `App` generic over the client, and the resulting type parameter would then end up in every handler and in most other things that touch the `App` type. This refactoring dramatically reduces the number of places we mention the `App` type, by making most uses rely on specific components instead of relying on `App` generally. There are still a few places that work on `App` generally, rather than on specific components, because an operation requires the use of two or more components. I don't love all this cloning, even if I know in my head that it's fine. The alternatives that we looked at include: * Provider traits, as we have for `Transaction`, that allow endpoints to specify that they want any type that can provide a `Tokens` or a `Conversation` instead of specifically an `App` (`App<PushClient>`). This is wordy enough that we've opted to punt on that approach for now. * Accept the type parameter as the cost of doing business. This is still an open alternative. * Use `dyn` dispatch instead of a type parameter for the push client. This is still an open alternative, though not one I love as we'd be incurring function call indirection without getting any generality out of it. Merges freestanding-app-components into main.
Diffstat (limited to 'src/test')
-rw-r--r--src/test/fixtures/boot.rs11
-rw-r--r--src/test/fixtures/conversation.rs11
-rw-r--r--src/test/fixtures/cookie.rs14
-rw-r--r--src/test/fixtures/identity.rs21
-rw-r--r--src/test/fixtures/invite.rs12
-rw-r--r--src/test/fixtures/message.rs13
-rw-r--r--src/test/verify/identity.rs26
-rw-r--r--src/test/verify/login.rs23
-rw-r--r--src/test/verify/token.rs29
9 files changed, 108 insertions, 52 deletions
diff --git a/src/test/fixtures/boot.rs b/src/test/fixtures/boot.rs
index 120726f..7421512 100644
--- a/src/test/fixtures/boot.rs
+++ b/src/test/fixtures/boot.rs
@@ -1,7 +1,12 @@
-use crate::{app::App, event::Sequence};
+use axum::extract::FromRef;
-pub async fn resume_point(app: &App) -> Sequence {
- app.boot()
+use crate::{boot::app::Boot, event::Sequence};
+
+pub async fn resume_point<App>(app: &App) -> Sequence
+where
+ Boot: FromRef<App>,
+{
+ Boot::from_ref(app)
.snapshot()
.await
.expect("boot always succeeds")
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: &App, created_at: &RequestedAt) -> Conversation
+where
+ Conversations: FromRef<App>,
+{
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/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: &App,
credentials: &(Name, Password),
now: &RequestedAt,
-) -> IdentityCookie {
+) -> IdentityCookie
+where
+ Logins: FromRef<App>,
+{
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");
diff --git a/src/test/fixtures/identity.rs b/src/test/fixtures/identity.rs
index 93e4a38..20929f9 100644
--- a/src/test/fixtures/identity.rs
+++ b/src/test/fixtures/identity.rs
@@ -1,11 +1,15 @@
+use axum::extract::FromRef;
+
use crate::{
app::App,
clock::RequestedAt,
+ login::app::Logins,
name::Name,
password::Password,
test::fixtures,
token::{
Token,
+ app::Tokens,
extract::{Identity, IdentityCookie},
},
};
@@ -15,23 +19,30 @@ pub async fn create(app: &App, created_at: &RequestedAt) -> Identity {
logged_in(app, &credentials, created_at).await
}
-pub async fn from_cookie(
+pub async fn from_cookie<App>(
app: &App,
cookie: &IdentityCookie,
validated_at: &RequestedAt,
-) -> Identity {
+) -> Identity
+where
+ Tokens: FromRef<App>,
+{
let secret = cookie.secret().expect("identity token has a secret");
- app.tokens()
+ Tokens::from_ref(app)
.validate(&secret, validated_at)
.await
.expect("always validates newly-issued secret")
}
-pub async fn logged_in(
+pub async fn logged_in<App>(
app: &App,
credentials: &(Name, Password),
issued_at: &RequestedAt,
-) -> Identity {
+) -> Identity
+where
+ Tokens: FromRef<App>,
+ Logins: FromRef<App>,
+{
let secret = fixtures::cookie::logged_in(app, credentials, issued_at).await;
from_cookie(app, &secret, issued_at).await
}
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: &App, issuer: &Login, issued_at: &DateTime) -> Invite
+where
+ Invites: FromRef<App>,
+{
+ Invites::from_ref(app)
.issue(issuer, issued_at)
.await
.expect("issuing invites never fails")
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: &App,
conversation: &Conversation,
sender: &Login,
sent_at: &RequestedAt,
-) -> Message {
+) -> Message
+where
+ Messages: FromRef<App>,
+{
let body = propose();
- app.messages()
+ Messages::from_ref(app)
.send(&conversation.id, sender, sent_at, &body)
.await
.expect("should succeed if the conversation exists")
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: &App, identity: &IdentityCookie, name: &Name)
+where
+ Tokens: FromRef<App>,
+{
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: &App, identity: &IdentityCookie, login: &Login)
+where
+ Tokens: FromRef<App>,
+{
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: &App, identity: &IdentityCookie)
+where
+ Tokens: FromRef<App>,
+{
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/login.rs b/src/test/verify/login.rs
index ae2e91e..aad01bc 100644
--- a/src/test/verify/login.rs
+++ b/src/test/verify/login.rs
@@ -1,23 +1,30 @@
+use axum::extract::FromRef;
+
use crate::{
- app::App,
- login::app::LoginError,
+ login::app::{LoginError, Logins},
name::Name,
password::Password,
test::{fixtures, verify},
+ token::app::Tokens,
};
-pub async fn valid_login(app: &App, name: &Name, password: &Password) {
- let secret = app
- .logins()
+pub async fn valid_login<App>(app: &App, name: &Name, password: &Password)
+where
+ Logins: FromRef<App>,
+ Tokens: FromRef<App>,
+{
+ let secret = Logins::from_ref(app)
.with_password(name, password, &fixtures::now())
.await
.expect("login credentials expected to be valid");
verify::token::valid_for_name(&app, &secret, &name).await;
}
-pub async fn invalid_login(app: &App, name: &Name, password: &Password) {
- let error = app
- .logins()
+pub async fn invalid_login<App>(app: &App, name: &Name, password: &Password)
+where
+ Logins: FromRef<App>,
+{
+ let error = Logins::from_ref(app)
.with_password(name, password, &fixtures::now())
.await
.expect_err("login credentials expected not to be valid");
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: &App, secret: &Secret, name: &Name)
+where
+ Tokens: FromRef<App>,
+{
+ 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: &App, secret: &Secret, login: &Login)
+where
+ Tokens: FromRef<App>,
+{
+ 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: &App, secret: &Secret)
+where
+ Tokens: FromRef<App>,
+{
+ let error = Tokens::from_ref(app)
.validate(secret, &fixtures::now())
.await
.expect_err("provided secret is invalid");