summaryrefslogtreecommitdiff
path: root/src/invite/routes
diff options
context:
space:
mode:
authorOwen Jacobson <owen@grimoire.ca>2025-06-17 02:11:45 -0400
committerOwen Jacobson <owen@grimoire.ca>2025-06-18 18:31:40 -0400
commit4e3d5ccac99b24934c972e088cd7eb02bb95df06 (patch)
treec94f5a42f7e734b81892c1289a1d2b566706ba7c /src/invite/routes
parent5ed96f8e8b9d9f19ee249f5c73a5a21ef6bca09f (diff)
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.
Diffstat (limited to 'src/invite/routes')
-rw-r--r--src/invite/routes/invite/get.rs38
-rw-r--r--src/invite/routes/invite/mod.rs6
-rw-r--r--src/invite/routes/invite/post.rs57
-rw-r--r--src/invite/routes/invite/test/get.rs65
-rw-r--r--src/invite/routes/invite/test/mod.rs2
-rw-r--r--src/invite/routes/invite/test/post.rs240
-rw-r--r--src/invite/routes/mod.rs4
-rw-r--r--src/invite/routes/post.rs19
-rw-r--r--src/invite/routes/test.rs28
9 files changed, 0 insertions, 459 deletions
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<App>,
- Path(invite): Path<super::PathInfo>,
-) -> Result<Json<Summary>, 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<App>,
- RequestedAt(accepted_at): RequestedAt,
- identity: IdentityCookie,
- Path(invite): Path<super::PathInfo>,
- Json(request): Json<Request>,
-) -> Result<(IdentityCookie, Json<User>), 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<App>,
- RequestedAt(issued_at): RequestedAt,
- identity: Identity,
- _: Json<Request>,
-) -> Result<Json<Invite>, 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);
-}