From 4e3d5ccac99b24934c972e088cd7eb02bb95df06 Mon Sep 17 00:00:00 2001 From: Owen Jacobson Date: Tue, 17 Jun 2025 02:11:45 -0400 Subject: Handlers are _named operations_, which can be exposed via routes. Each domain module that exposes handlers does so through a `handlers` child module, ideally as a top-level symbol that can be plugged directly into Axum's `MethodRouter`. Modules could make exceptions to this - kill the doctrinaire inside yourself, after all - but none of the API modules that actually exist need such exceptions, and consistency is useful. The related details of request types, URL types, response types, errors, &c &c are then organized into modules under `handlers`, along with their respective tests. --- src/invite/routes/invite/get.rs | 38 ------ src/invite/routes/invite/mod.rs | 6 - src/invite/routes/invite/post.rs | 57 -------- src/invite/routes/invite/test/get.rs | 65 --------- src/invite/routes/invite/test/mod.rs | 2 - src/invite/routes/invite/test/post.rs | 240 ---------------------------------- src/invite/routes/mod.rs | 4 - src/invite/routes/post.rs | 19 --- src/invite/routes/test.rs | 28 ---- 9 files changed, 459 deletions(-) delete mode 100644 src/invite/routes/invite/get.rs delete mode 100644 src/invite/routes/invite/mod.rs delete mode 100644 src/invite/routes/invite/post.rs delete mode 100644 src/invite/routes/invite/test/get.rs delete mode 100644 src/invite/routes/invite/test/mod.rs delete mode 100644 src/invite/routes/invite/test/post.rs delete mode 100644 src/invite/routes/mod.rs delete mode 100644 src/invite/routes/post.rs delete mode 100644 src/invite/routes/test.rs (limited to 'src/invite/routes') diff --git a/src/invite/routes/invite/get.rs b/src/invite/routes/invite/get.rs deleted file mode 100644 index f862833..0000000 --- a/src/invite/routes/invite/get.rs +++ /dev/null @@ -1,38 +0,0 @@ -use axum::{ - extract::{Json, Path, State}, - response::{IntoResponse, Response}, -}; - -use crate::{ - app::App, - error::{Internal, NotFound}, - invite::{Id, Summary}, -}; - -pub async fn handler( - State(app): State, - Path(invite): Path, -) -> Result, Error> { - app.invites() - .get(&invite) - .await? - .map(Json) - .ok_or_else(move || Error::NotFound(invite)) -} - -#[derive(Debug, thiserror::Error)] -pub enum Error { - #[error("invite not found: {0}")] - NotFound(Id), - #[error(transparent)] - Database(#[from] sqlx::Error), -} - -impl IntoResponse for Error { - fn into_response(self) -> Response { - match self { - Self::NotFound(_) => NotFound(self).into_response(), - Self::Database(_) => Internal::from(self).into_response(), - } - } -} diff --git a/src/invite/routes/invite/mod.rs b/src/invite/routes/invite/mod.rs deleted file mode 100644 index c22029a..0000000 --- a/src/invite/routes/invite/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -pub mod get; -pub mod post; -#[cfg(test)] -pub mod test; - -type PathInfo = crate::invite::Id; diff --git a/src/invite/routes/invite/post.rs b/src/invite/routes/invite/post.rs deleted file mode 100644 index 58d15c2..0000000 --- a/src/invite/routes/invite/post.rs +++ /dev/null @@ -1,57 +0,0 @@ -use axum::{ - extract::{Json, Path, State}, - http::StatusCode, - response::{IntoResponse, Response}, -}; - -use crate::{ - app::App, - clock::RequestedAt, - error::{Internal, NotFound}, - invite::app, - name::Name, - token::extract::IdentityCookie, - user::{Password, User}, -}; - -pub async fn handler( - State(app): State, - RequestedAt(accepted_at): RequestedAt, - identity: IdentityCookie, - Path(invite): Path, - Json(request): Json, -) -> Result<(IdentityCookie, Json), Error> { - let (login, secret) = app - .invites() - .accept(&invite, &request.name, &request.password, &accepted_at) - .await - .map_err(Error)?; - let identity = identity.set(secret); - Ok((identity, Json(login))) -} - -#[derive(serde::Deserialize)] -pub struct Request { - pub name: Name, - pub password: Password, -} - -#[derive(Debug, thiserror::Error)] -#[error(transparent)] -pub struct Error(pub app::AcceptError); - -impl IntoResponse for Error { - fn into_response(self) -> Response { - let Self(error) = self; - match error { - app::AcceptError::NotFound(_) => NotFound(error).into_response(), - app::AcceptError::InvalidName(_) => { - (StatusCode::BAD_REQUEST, error.to_string()).into_response() - } - app::AcceptError::DuplicateLogin(_) => { - (StatusCode::CONFLICT, error.to_string()).into_response() - } - other => Internal::from(other).into_response(), - } - } -} diff --git a/src/invite/routes/invite/test/get.rs b/src/invite/routes/invite/test/get.rs deleted file mode 100644 index 0dc8a79..0000000 --- a/src/invite/routes/invite/test/get.rs +++ /dev/null @@ -1,65 +0,0 @@ -use axum::extract::{Json, Path, State}; - -use crate::{invite::routes::invite::get, test::fixtures}; - -#[tokio::test] -async fn valid_invite() { - // Set up the environment - - let app = fixtures::scratch_app().await; - let issuer = fixtures::user::create(&app, &fixtures::now()).await; - let invite = fixtures::invite::issue(&app, &issuer, &fixtures::now()).await; - - // Call endpoint - - let Json(response) = get::handler(State(app), Path(invite.id)) - .await - .expect("get for an existing invite succeeds"); - - // Verify response - - assert_eq!(issuer.name.display(), &response.issuer); - assert_eq!(invite.issued_at, response.issued_at); -} - -#[tokio::test] -async fn nonexistent_invite() { - // Set up the environment - - let app = fixtures::scratch_app().await; - - // Call endpoint - - let invite = fixtures::invite::fictitious(); - let error = get::handler(State(app), Path(invite.clone())) - .await - .expect_err("get for a nonexistent invite fails"); - - // Verify response - - assert!(matches!(error, get::Error::NotFound(error_id) if invite == error_id)); -} - -#[tokio::test] -async fn expired_invite() { - // Set up the environment - - let app = fixtures::scratch_app().await; - let issuer = fixtures::user::create(&app, &fixtures::ancient()).await; - let invite = fixtures::invite::issue(&app, &issuer, &fixtures::ancient()).await; - - app.invites() - .expire(&fixtures::now()) - .await - .expect("expiring invites never fails"); - - // Call endpoint - - let error = get::handler(State(app), Path(invite.id.clone())) - .await - .expect_err("get for an expired invite fails"); - - // Verify response - - assert!(matches!(error, get::Error::NotFound(error_id) if invite.id == error_id)); -} diff --git a/src/invite/routes/invite/test/mod.rs b/src/invite/routes/invite/test/mod.rs deleted file mode 100644 index d6c1f06..0000000 --- a/src/invite/routes/invite/test/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -mod get; -mod post; diff --git a/src/invite/routes/invite/test/post.rs b/src/invite/routes/invite/test/post.rs deleted file mode 100644 index b204b32..0000000 --- a/src/invite/routes/invite/test/post.rs +++ /dev/null @@ -1,240 +0,0 @@ -use axum::extract::{Json, Path, State}; - -use crate::{ - invite::{app::AcceptError, routes::invite::post}, - name::Name, - test::fixtures, -}; - -#[tokio::test] -async fn valid_invite() { - // Set up the environment - - let app = fixtures::scratch_app().await; - let issuer = fixtures::user::create(&app, &fixtures::now()).await; - let invite = fixtures::invite::issue(&app, &issuer, &fixtures::now()).await; - - // Call the endpoint - - let (name, password) = fixtures::user::propose(); - let identity = fixtures::cookie::not_logged_in(); - let request = post::Request { - name: name.clone(), - password: password.clone(), - }; - let (identity, Json(response)) = post::handler( - State(app.clone()), - fixtures::now(), - identity, - Path(invite.id), - Json(request), - ) - .await - .expect("accepting a valid invite succeeds"); - - // Verify the response - - assert!(identity.secret().is_some()); - assert_eq!(name, response.name); - - // Verify that the issued token is valid - - let secret = identity - .secret() - .expect("newly-issued identity has a token secret"); - let (_, login) = app - .tokens() - .validate(&secret, &fixtures::now()) - .await - .expect("newly-issued identity cookie is valid"); - assert_eq!(response, login); - - // Verify that the given credentials can log in - - let (login, _) = app - .tokens() - .login(&name, &password, &fixtures::now()) - .await - .expect("credentials given on signup are valid"); - assert_eq!(response, login); -} - -#[tokio::test] -async fn nonexistent_invite() { - // Set up the environment - - let app = fixtures::scratch_app().await; - let invite = fixtures::invite::fictitious(); - - // Call the endpoint - - let (name, password) = fixtures::user::propose(); - let identity = fixtures::cookie::not_logged_in(); - let request = post::Request { - name: name.clone(), - password: password.clone(), - }; - let post::Error(error) = post::handler( - State(app.clone()), - fixtures::now(), - identity, - Path(invite.clone()), - Json(request), - ) - .await - .expect_err("accepting a nonexistent invite fails"); - - // Verify the response - - assert!(matches!(error, AcceptError::NotFound(error_id) if error_id == invite)); -} - -#[tokio::test] -async fn expired_invite() { - // Set up the environment - - let app = fixtures::scratch_app().await; - let issuer = fixtures::user::create(&app, &fixtures::ancient()).await; - let invite = fixtures::invite::issue(&app, &issuer, &fixtures::ancient()).await; - - app.invites() - .expire(&fixtures::now()) - .await - .expect("expiring invites never fails"); - - // Call the endpoint - - let (name, password) = fixtures::user::propose(); - let identity = fixtures::cookie::not_logged_in(); - let request = post::Request { - name: name.clone(), - password: password.clone(), - }; - let post::Error(error) = post::handler( - State(app.clone()), - fixtures::now(), - identity, - Path(invite.id.clone()), - Json(request), - ) - .await - .expect_err("accepting a nonexistent invite fails"); - - // Verify the response - - assert!(matches!(error, AcceptError::NotFound(error_id) if error_id == invite.id)); -} - -#[tokio::test] -async fn accepted_invite() { - // Set up the environment - - let app = fixtures::scratch_app().await; - let issuer = fixtures::user::create(&app, &fixtures::ancient()).await; - let invite = fixtures::invite::issue(&app, &issuer, &fixtures::ancient()).await; - - let (name, password) = fixtures::user::propose(); - app.invites() - .accept(&invite.id, &name, &password, &fixtures::now()) - .await - .expect("accepting a valid invite succeeds"); - - // Call the endpoint - - let (name, password) = fixtures::user::propose(); - let identity = fixtures::cookie::not_logged_in(); - let request = post::Request { - name: name.clone(), - password: password.clone(), - }; - let post::Error(error) = post::handler( - State(app.clone()), - fixtures::now(), - identity, - Path(invite.id.clone()), - Json(request), - ) - .await - .expect_err("accepting a nonexistent invite fails"); - - // Verify the response - - assert!(matches!(error, AcceptError::NotFound(error_id) if error_id == invite.id)); -} - -#[tokio::test] -async fn conflicting_name() { - // Set up the environment - - let app = fixtures::scratch_app().await; - let issuer = fixtures::user::create(&app, &fixtures::ancient()).await; - let invite = fixtures::invite::issue(&app, &issuer, &fixtures::ancient()).await; - - let existing_name = Name::from("rijksmuseum"); - app.users() - .create( - &existing_name, - &fixtures::user::propose_password(), - &fixtures::now(), - ) - .await - .expect("creating a user in an empty environment succeeds"); - - // Call the endpoint - - let conflicting_name = Name::from("r\u{0133}ksmuseum"); - let password = fixtures::user::propose_password(); - - let identity = fixtures::cookie::not_logged_in(); - let request = post::Request { - name: conflicting_name.clone(), - password: password.clone(), - }; - let post::Error(error) = post::handler( - State(app.clone()), - fixtures::now(), - identity, - Path(invite.id.clone()), - Json(request), - ) - .await - .expect_err("accepting a nonexistent invite fails"); - - // Verify the response - - assert!( - matches!(error, AcceptError::DuplicateLogin(error_name) if error_name == conflicting_name) - ); -} - -#[tokio::test] -async fn invalid_name() { - // Set up the environment - - let app = fixtures::scratch_app().await; - let issuer = fixtures::user::create(&app, &fixtures::now()).await; - let invite = fixtures::invite::issue(&app, &issuer, &fixtures::now()).await; - - // Call the endpoint - - let name = fixtures::user::propose_invalid_name(); - let password = fixtures::user::propose_password(); - let identity = fixtures::cookie::not_logged_in(); - let request = post::Request { - name: name.clone(), - password: password.clone(), - }; - let post::Error(error) = post::handler( - State(app.clone()), - fixtures::now(), - identity, - Path(invite.id), - Json(request), - ) - .await - .expect_err("using an invalid name should fail"); - - // Verify the response - - assert!(matches!(error, AcceptError::InvalidName(error_name) if name == error_name)); -} diff --git a/src/invite/routes/mod.rs b/src/invite/routes/mod.rs deleted file mode 100644 index 8747a4e..0000000 --- a/src/invite/routes/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod invite; -pub mod post; -#[cfg(test)] -mod test; diff --git a/src/invite/routes/post.rs b/src/invite/routes/post.rs deleted file mode 100644 index f7ca76c..0000000 --- a/src/invite/routes/post.rs +++ /dev/null @@ -1,19 +0,0 @@ -use axum::extract::{Json, State}; - -use crate::{ - app::App, clock::RequestedAt, error::Internal, invite::Invite, token::extract::Identity, -}; - -pub async fn handler( - State(app): State, - RequestedAt(issued_at): RequestedAt, - identity: Identity, - _: Json, -) -> Result, Internal> { - let invite = app.invites().issue(&identity.user, &issued_at).await?; - Ok(Json(invite)) -} - -// Require `{}` as the only valid request for this endpoint. -#[derive(Default, serde::Deserialize)] -pub struct Request {} diff --git a/src/invite/routes/test.rs b/src/invite/routes/test.rs deleted file mode 100644 index 4ea8a3d..0000000 --- a/src/invite/routes/test.rs +++ /dev/null @@ -1,28 +0,0 @@ -use axum::extract::{Json, State}; - -use super::post; -use crate::test::fixtures; - -#[tokio::test] -async fn create_invite() { - // Set up the environment - - let app = fixtures::scratch_app().await; - let issuer = fixtures::identity::create(&app, &fixtures::now()).await; - let issued_at = fixtures::now(); - - // Call the endpoint - - let Json(invite) = post::handler( - State(app), - issued_at.clone(), - issuer.clone(), - Json(post::Request {}), - ) - .await - .expect("creating an invite always succeeds"); - - // Verify the response - assert_eq!(issuer.user.id, invite.issuer); - assert_eq!(&*issued_at, &invite.issued_at); -} -- cgit v1.2.3