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/handlers/accept/mod.rs | 60 +++++++++ src/invite/handlers/accept/test.rs | 236 +++++++++++++++++++++++++++++++++ src/invite/handlers/get/mod.rs | 41 ++++++ src/invite/handlers/get/test.rs | 65 +++++++++ src/invite/handlers/issue/mod.rs | 22 ++++ src/invite/handlers/issue/test.rs | 27 ++++ src/invite/handlers/mod.rs | 9 ++ src/invite/mod.rs | 2 +- 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 ---- 17 files changed, 461 insertions(+), 460 deletions(-) create mode 100644 src/invite/handlers/accept/mod.rs create mode 100644 src/invite/handlers/accept/test.rs create mode 100644 src/invite/handlers/get/mod.rs create mode 100644 src/invite/handlers/get/test.rs create mode 100644 src/invite/handlers/issue/mod.rs create mode 100644 src/invite/handlers/issue/test.rs create mode 100644 src/invite/handlers/mod.rs 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') diff --git a/src/invite/handlers/accept/mod.rs b/src/invite/handlers/accept/mod.rs new file mode 100644 index 0000000..0d5f08a --- /dev/null +++ b/src/invite/handlers/accept/mod.rs @@ -0,0 +1,60 @@ +use axum::{ + extract::{Json, Path, State}, + http::StatusCode, + response::{IntoResponse, Response}, +}; + +use crate::{ + app::App, + clock::RequestedAt, + error::{Internal, NotFound}, + invite::{app, handlers::PathInfo}, + name::Name, + token::extract::IdentityCookie, + user::{Password, User}, +}; + +#[cfg(test)] +mod test; + +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/handlers/accept/test.rs b/src/invite/handlers/accept/test.rs new file mode 100644 index 0000000..cb13900 --- /dev/null +++ b/src/invite/handlers/accept/test.rs @@ -0,0 +1,236 @@ +use axum::extract::{Json, Path, State}; + +use crate::{invite::app::AcceptError, 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 = super::Request { + name: name.clone(), + password: password.clone(), + }; + let (identity, Json(response)) = super::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 = super::Request { + name: name.clone(), + password: password.clone(), + }; + let super::Error(error) = super::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 = super::Request { + name: name.clone(), + password: password.clone(), + }; + let super::Error(error) = super::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 = super::Request { + name: name.clone(), + password: password.clone(), + }; + let super::Error(error) = super::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 = super::Request { + name: conflicting_name.clone(), + password: password.clone(), + }; + let super::Error(error) = super::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 = super::Request { + name: name.clone(), + password: password.clone(), + }; + let super::Error(error) = super::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/handlers/get/mod.rs b/src/invite/handlers/get/mod.rs new file mode 100644 index 0000000..bb72586 --- /dev/null +++ b/src/invite/handlers/get/mod.rs @@ -0,0 +1,41 @@ +use axum::{ + extract::{Json, Path, State}, + response::{IntoResponse, Response}, +}; + +use crate::{ + app::App, + error::{Internal, NotFound}, + invite::{Id, Summary, handlers::PathInfo}, +}; + +#[cfg(test)] +mod test; + +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/handlers/get/test.rs b/src/invite/handlers/get/test.rs new file mode 100644 index 0000000..0f2f725 --- /dev/null +++ b/src/invite/handlers/get/test.rs @@ -0,0 +1,65 @@ +use axum::extract::{Json, Path, State}; + +use crate::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) = super::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 = super::handler(State(app), Path(invite.clone())) + .await + .expect_err("get for a nonexistent invite fails"); + + // Verify response + + assert!(matches!(error, super::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 = super::handler(State(app), Path(invite.id.clone())) + .await + .expect_err("get for an expired invite fails"); + + // Verify response + + assert!(matches!(error, super::Error::NotFound(error_id) if invite.id == error_id)); +} diff --git a/src/invite/handlers/issue/mod.rs b/src/invite/handlers/issue/mod.rs new file mode 100644 index 0000000..6085f7a --- /dev/null +++ b/src/invite/handlers/issue/mod.rs @@ -0,0 +1,22 @@ +use axum::extract::{Json, State}; + +use crate::{ + app::App, clock::RequestedAt, error::Internal, invite::Invite, token::extract::Identity, +}; + +#[cfg(test)] +mod test; + +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/handlers/issue/test.rs b/src/invite/handlers/issue/test.rs new file mode 100644 index 0000000..2bf5400 --- /dev/null +++ b/src/invite/handlers/issue/test.rs @@ -0,0 +1,27 @@ +use axum::extract::{Json, State}; + +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) = super::handler( + State(app), + issued_at.clone(), + issuer.clone(), + Json(super::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); +} diff --git a/src/invite/handlers/mod.rs b/src/invite/handlers/mod.rs new file mode 100644 index 0000000..07a2bbe --- /dev/null +++ b/src/invite/handlers/mod.rs @@ -0,0 +1,9 @@ +mod accept; +mod get; +mod issue; + +type PathInfo = crate::invite::Id; + +pub use accept::handler as accept; +pub use get::handler as get; +pub use issue::handler as issue; diff --git a/src/invite/mod.rs b/src/invite/mod.rs index 3932eea..74b9ceb 100644 --- a/src/invite/mod.rs +++ b/src/invite/mod.rs @@ -1,9 +1,9 @@ use crate::{clock::DateTime, normalize::nfc, user}; pub mod app; +pub mod handlers; mod id; mod repo; -pub mod routes; pub use self::id::Id; 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