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/setup/handlers/mod.rs | 3 ++ src/setup/handlers/setup/mod.rs | 55 +++++++++++++++++++++++ src/setup/handlers/setup/test.rs | 93 +++++++++++++++++++++++++++++++++++++++ src/setup/mod.rs | 2 +- src/setup/routes/mod.rs | 3 -- src/setup/routes/post.rs | 52 ---------------------- src/setup/routes/test.rs | 94 ---------------------------------------- 7 files changed, 152 insertions(+), 150 deletions(-) create mode 100644 src/setup/handlers/mod.rs create mode 100644 src/setup/handlers/setup/mod.rs create mode 100644 src/setup/handlers/setup/test.rs delete mode 100644 src/setup/routes/mod.rs delete mode 100644 src/setup/routes/post.rs delete mode 100644 src/setup/routes/test.rs (limited to 'src/setup') diff --git a/src/setup/handlers/mod.rs b/src/setup/handlers/mod.rs new file mode 100644 index 0000000..3d4a4e6 --- /dev/null +++ b/src/setup/handlers/mod.rs @@ -0,0 +1,3 @@ +mod setup; + +pub use setup::handler as setup; diff --git a/src/setup/handlers/setup/mod.rs b/src/setup/handlers/setup/mod.rs new file mode 100644 index 0000000..cbb3072 --- /dev/null +++ b/src/setup/handlers/setup/mod.rs @@ -0,0 +1,55 @@ +use axum::{ + extract::{Json, State}, + http::StatusCode, + response::{IntoResponse, Response}, +}; + +use crate::{ + app::App, + clock::RequestedAt, + error::Internal, + name::Name, + setup::app, + token::extract::IdentityCookie, + user::{Password, User}, +}; + +#[cfg(test)] +mod test; + +pub async fn handler( + State(app): State, + RequestedAt(setup_at): RequestedAt, + identity: IdentityCookie, + Json(request): Json, +) -> Result<(IdentityCookie, Json), Error> { + let (user, secret) = app + .setup() + .initial(&request.name, &request.password, &setup_at) + .await + .map_err(Error)?; + let identity = identity.set(secret); + Ok((identity, Json(user))) +} + +#[derive(serde::Deserialize)] +pub struct Request { + pub name: Name, + pub password: Password, +} + +#[derive(Debug)] +pub struct Error(pub app::Error); + +impl IntoResponse for Error { + fn into_response(self) -> Response { + let Self(error) = self; + match error { + app::Error::InvalidName(_) => { + (StatusCode::BAD_REQUEST, error.to_string()).into_response() + } + app::Error::SetupCompleted => (StatusCode::CONFLICT, error.to_string()).into_response(), + other => Internal::from(other).into_response(), + } + } +} diff --git a/src/setup/handlers/setup/test.rs b/src/setup/handlers/setup/test.rs new file mode 100644 index 0000000..8243ac3 --- /dev/null +++ b/src/setup/handlers/setup/test.rs @@ -0,0 +1,93 @@ +use axum::extract::{Json, State}; + +use crate::{setup::app, test::fixtures}; + +#[tokio::test] +async fn fresh_instance() { + // Set up the environment + + let app = fixtures::scratch_app().await; + + // Call the endpoint + let identity = fixtures::cookie::not_logged_in(); + let (name, password) = fixtures::user::propose(); + let request = super::Request { + name: name.clone(), + password: password.clone(), + }; + let (identity, Json(response)) = + super::handler(State(app.clone()), fixtures::now(), identity, Json(request)) + .await + .expect("setup in a fresh app succeeds"); + + // Verify the response + + 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 login_exists() { + // Set up the environment + + let app = fixtures::scratch_app().await; + fixtures::user::create(&app, &fixtures::now()).await; + + // Call the endpoint + let identity = fixtures::cookie::not_logged_in(); + 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)) + .await + .expect_err("setup in a populated app fails"); + + // Verify the response + + assert!(matches!(error, app::Error::SetupCompleted)); +} + +#[tokio::test] +async fn invalid_name() { + // Set up the environment + + let app = fixtures::scratch_app().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 = super::Request { + name: name.clone(), + password: password.clone(), + }; + let super::Error(error) = + super::handler(State(app.clone()), fixtures::now(), identity, Json(request)) + .await + .expect_err("setup with an invalid name fails"); + + // Verify the response + + assert!(matches!(error, app::Error::InvalidName(error_name) if name == error_name)); +} diff --git a/src/setup/mod.rs b/src/setup/mod.rs index e741a60..f5d12df 100644 --- a/src/setup/mod.rs +++ b/src/setup/mod.rs @@ -1,6 +1,6 @@ pub mod app; +pub mod handlers; pub mod repo; mod required; -pub mod routes; pub use self::required::Required; diff --git a/src/setup/routes/mod.rs b/src/setup/routes/mod.rs deleted file mode 100644 index e94a249..0000000 --- a/src/setup/routes/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod post; -#[cfg(test)] -mod test; diff --git a/src/setup/routes/post.rs b/src/setup/routes/post.rs deleted file mode 100644 index 0ff5d69..0000000 --- a/src/setup/routes/post.rs +++ /dev/null @@ -1,52 +0,0 @@ -use axum::{ - extract::{Json, State}, - http::StatusCode, - response::{IntoResponse, Response}, -}; - -use crate::{ - app::App, - clock::RequestedAt, - error::Internal, - name::Name, - setup::app, - token::extract::IdentityCookie, - user::{Password, User}, -}; - -pub async fn handler( - State(app): State, - RequestedAt(setup_at): RequestedAt, - identity: IdentityCookie, - Json(request): Json, -) -> Result<(IdentityCookie, Json), Error> { - let (user, secret) = app - .setup() - .initial(&request.name, &request.password, &setup_at) - .await - .map_err(Error)?; - let identity = identity.set(secret); - Ok((identity, Json(user))) -} - -#[derive(serde::Deserialize)] -pub struct Request { - pub name: Name, - pub password: Password, -} - -#[derive(Debug)] -pub struct Error(pub app::Error); - -impl IntoResponse for Error { - fn into_response(self) -> Response { - let Self(error) = self; - match error { - app::Error::InvalidName(_) => { - (StatusCode::BAD_REQUEST, error.to_string()).into_response() - } - app::Error::SetupCompleted => (StatusCode::CONFLICT, error.to_string()).into_response(), - other => Internal::from(other).into_response(), - } - } -} diff --git a/src/setup/routes/test.rs b/src/setup/routes/test.rs deleted file mode 100644 index e9f5cd6..0000000 --- a/src/setup/routes/test.rs +++ /dev/null @@ -1,94 +0,0 @@ -use axum::extract::{Json, State}; - -use super::post; -use crate::{setup::app, test::fixtures}; - -#[tokio::test] -async fn fresh_instance() { - // Set up the environment - - let app = fixtures::scratch_app().await; - - // Call the endpoint - let identity = fixtures::cookie::not_logged_in(); - let (name, password) = fixtures::user::propose(); - let request = post::Request { - name: name.clone(), - password: password.clone(), - }; - let (identity, Json(response)) = - post::handler(State(app.clone()), fixtures::now(), identity, Json(request)) - .await - .expect("setup in a fresh app succeeds"); - - // Verify the response - - 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 login_exists() { - // Set up the environment - - let app = fixtures::scratch_app().await; - fixtures::user::create(&app, &fixtures::now()).await; - - // Call the endpoint - let identity = fixtures::cookie::not_logged_in(); - let (name, password) = fixtures::user::propose(); - let request = post::Request { name, password }; - let post::Error(error) = - post::handler(State(app.clone()), fixtures::now(), identity, Json(request)) - .await - .expect_err("setup in a populated app fails"); - - // Verify the response - - assert!(matches!(error, app::Error::SetupCompleted)); -} - -#[tokio::test] -async fn invalid_name() { - // Set up the environment - - let app = fixtures::scratch_app().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, Json(request)) - .await - .expect_err("setup with an invalid name fails"); - - // Verify the response - - assert!(matches!(error, app::Error::InvalidName(error_name) if name == error_name)); -} -- cgit v1.2.3