diff options
Diffstat (limited to 'src/login')
| -rw-r--r-- | src/login/app.rs | 29 | ||||
| -rw-r--r-- | src/login/extract.rs | 15 | ||||
| -rw-r--r-- | src/login/history.rs | 6 | ||||
| -rw-r--r-- | src/login/mod.rs | 2 | ||||
| -rw-r--r-- | src/login/password.rs | 7 | ||||
| -rw-r--r-- | src/login/repo.rs | 140 | ||||
| -rw-r--r-- | src/login/routes.rs | 97 | ||||
| -rw-r--r-- | src/login/routes/login/mod.rs | 4 | ||||
| -rw-r--r-- | src/login/routes/login/post.rs | 52 | ||||
| -rw-r--r-- | src/login/routes/login/test.rs (renamed from src/login/routes/test/login.rs) | 48 | ||||
| -rw-r--r-- | src/login/routes/logout/mod.rs | 4 | ||||
| -rw-r--r-- | src/login/routes/logout/post.rs | 47 | ||||
| -rw-r--r-- | src/login/routes/logout/test.rs | 79 | ||||
| -rw-r--r-- | src/login/routes/mod.rs | 12 | ||||
| -rw-r--r-- | src/login/routes/test/logout.rs | 97 | ||||
| -rw-r--r-- | src/login/routes/test/mod.rs | 2 | ||||
| -rw-r--r-- | src/login/snapshot.rs | 3 |
17 files changed, 346 insertions, 298 deletions
diff --git a/src/login/app.rs b/src/login/app.rs index bb1419b..2f5896f 100644 --- a/src/login/app.rs +++ b/src/login/app.rs @@ -1,24 +1,37 @@ use sqlx::sqlite::SqlitePool; -use super::{repo::Provider as _, Login, Password}; +use super::repo::Provider as _; + +#[cfg(test)] +use super::{Login, Password}; +#[cfg(test)] use crate::{ clock::DateTime, event::{repo::Provider as _, Broadcaster, Event}, + name::Name, }; pub struct Logins<'a> { db: &'a SqlitePool, + #[cfg(test)] events: &'a Broadcaster, } impl<'a> Logins<'a> { + #[cfg(not(test))] + pub const fn new(db: &'a SqlitePool) -> Self { + Self { db } + } + + #[cfg(test)] pub const fn new(db: &'a SqlitePool, events: &'a Broadcaster) -> Self { Self { db, events } } + #[cfg(test)] pub async fn create( &self, - name: &str, + name: &Name, password: &Password, created_at: &DateTime, ) -> Result<Login, CreateError> { @@ -34,11 +47,19 @@ impl<'a> Logins<'a> { Ok(login.as_created()) } + + pub async fn recanonicalize(&self) -> Result<(), sqlx::Error> { + let mut tx = self.db.begin().await?; + tx.logins().recanonicalize().await?; + tx.commit().await?; + + Ok(()) + } } #[derive(Debug, thiserror::Error)] #[error(transparent)] pub enum CreateError { - DatabaseError(#[from] sqlx::Error), - PasswordHashError(#[from] password_hash::Error), + Database(#[from] sqlx::Error), + PasswordHash(#[from] password_hash::Error), } diff --git a/src/login/extract.rs b/src/login/extract.rs deleted file mode 100644 index c2d97f2..0000000 --- a/src/login/extract.rs +++ /dev/null @@ -1,15 +0,0 @@ -use axum::{extract::FromRequestParts, http::request::Parts}; - -use super::Login; -use crate::{app::App, token::extract::Identity}; - -#[async_trait::async_trait] -impl FromRequestParts<App> for Login { - type Rejection = <Identity as FromRequestParts<App>>::Rejection; - - async fn from_request_parts(parts: &mut Parts, state: &App) -> Result<Self, Self::Rejection> { - let identity = Identity::from_request_parts(parts, state).await?; - - Ok(identity.login) - } -} diff --git a/src/login/history.rs b/src/login/history.rs index f8d81bb..daad579 100644 --- a/src/login/history.rs +++ b/src/login/history.rs @@ -20,7 +20,6 @@ impl History { // if this returns a redacted or modified version of the login. If we implement // renames by redacting the original name, then this should return the edited // login, not the original, even if that's not how it was "as created.") - #[cfg(test)] pub fn as_created(&self) -> Login { self.login.clone() } @@ -30,6 +29,11 @@ impl History { .filter(Sequence::up_to(resume_point.into())) .collect() } + + // Snapshot of this login, as of all events recorded in this history. + pub fn as_snapshot(&self) -> Option<Login> { + self.events().collect() + } } // Events interface diff --git a/src/login/mod.rs b/src/login/mod.rs index 98cc3d7..279e9a6 100644 --- a/src/login/mod.rs +++ b/src/login/mod.rs @@ -1,7 +1,5 @@ -#[cfg(test)] pub mod app; pub mod event; -pub mod extract; mod history; mod id; pub mod password; diff --git a/src/login/password.rs b/src/login/password.rs index 14fd981..c27c950 100644 --- a/src/login/password.rs +++ b/src/login/password.rs @@ -4,6 +4,8 @@ use argon2::Argon2; use password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString}; use rand_core::OsRng; +use crate::normalize::nfc; + #[derive(sqlx::Type)] #[sqlx(transparent)] pub struct StoredHash(String); @@ -31,7 +33,7 @@ impl fmt::Debug for StoredHash { #[derive(serde::Deserialize)] #[serde(transparent)] -pub struct Password(String); +pub struct Password(nfc::String); impl Password { pub fn hash(&self) -> Result<StoredHash, password_hash::Error> { @@ -56,9 +58,8 @@ impl fmt::Debug for Password { } } -#[cfg(test)] impl From<String> for Password { fn from(password: String) -> Self { - Self(password) + Password(password.into()) } } diff --git a/src/login/repo.rs b/src/login/repo.rs index 6d6510c..611edd6 100644 --- a/src/login/repo.rs +++ b/src/login/repo.rs @@ -1,9 +1,11 @@ +use futures::stream::{StreamExt as _, TryStreamExt as _}; use sqlx::{sqlite::Sqlite, SqliteConnection, Transaction}; use crate::{ clock::DateTime, event::{Instant, ResumePoint, Sequence}, login::{password::StoredHash, History, Id, Login}, + name::{self, Name}, }; pub trait Provider { @@ -21,80 +23,80 @@ pub struct Logins<'t>(&'t mut SqliteConnection); impl<'c> Logins<'c> { pub async fn create( &mut self, - name: &str, + name: &Name, password_hash: &StoredHash, created: &Instant, ) -> Result<History, sqlx::Error> { let id = Id::generate(); + let display_name = name.display(); + let canonical_name = name.canonical(); - let login = sqlx::query!( + sqlx::query!( r#" insert - into login (id, name, password_hash, created_sequence, created_at) - values ($1, $2, $3, $4, $5) - returning - id as "id: Id", - name, - created_sequence as "created_sequence: Sequence", - created_at as "created_at: DateTime" + into login (id, display_name, canonical_name, password_hash, created_sequence, created_at) + values ($1, $2, $3, $4, $5, $6) "#, id, - name, + display_name, + canonical_name, password_hash, created.sequence, created.at, ) - .map(|row| History { + .execute(&mut *self.0) + .await?; + + let login = History { + created: *created, login: Login { - id: row.id, - name: row.name, - }, - created: Instant { - at: row.created_at, - sequence: row.created_sequence, + id, + name: name.clone(), }, - }) - .fetch_one(&mut *self.0) - .await?; + }; Ok(login) } - pub async fn all(&mut self, resume_at: ResumePoint) -> Result<Vec<History>, sqlx::Error> { - let channels = sqlx::query!( + pub async fn all(&mut self, resume_at: ResumePoint) -> Result<Vec<History>, LoadError> { + let logins = sqlx::query!( r#" select id as "id: Id", - name, + display_name as "display_name: String", + canonical_name as "canonical_name: String", created_sequence as "created_sequence: Sequence", created_at as "created_at: DateTime" from login where coalesce(created_sequence <= $1, true) - order by created_sequence + order by canonical_name "#, resume_at, ) - .map(|row| History { - login: Login { - id: row.id, - name: row.name, - }, - created: Instant { - at: row.created_at, - sequence: row.created_sequence, - }, + .map(|row| { + Ok::<_, LoadError>(History { + login: Login { + id: row.id, + name: Name::new(row.display_name, row.canonical_name)?, + }, + created: Instant::new(row.created_at, row.created_sequence), + }) }) - .fetch_all(&mut *self.0) + .fetch(&mut *self.0) + .map(|res| res?) + .try_collect() .await?; - Ok(channels) + Ok(logins) } - pub async fn replay(&mut self, resume_at: ResumePoint) -> Result<Vec<History>, sqlx::Error> { - let messages = sqlx::query!( + + pub async fn replay(&mut self, resume_at: ResumePoint) -> Result<Vec<History>, LoadError> { + let logins = sqlx::query!( r#" select id as "id: Id", - name, + display_name as "display_name: String", + canonical_name as "canonical_name: String", created_sequence as "created_sequence: Sequence", created_at as "created_at: DateTime" from login @@ -102,25 +104,59 @@ impl<'c> Logins<'c> { "#, resume_at, ) - .map(|row| History { - login: Login { - id: row.id, - name: row.name, - }, - created: Instant { - at: row.created_at, - sequence: row.created_sequence, - }, + .map(|row| { + Ok::<_, name::Error>(History { + login: Login { + id: row.id, + name: Name::new(row.display_name, row.canonical_name)?, + }, + created: Instant::new(row.created_at, row.created_sequence), + }) }) + .fetch(&mut *self.0) + .map(|res| Ok::<_, LoadError>(res??)) + .try_collect() + .await?; + + Ok(logins) + } + + pub async fn recanonicalize(&mut self) -> Result<(), sqlx::Error> { + let logins = sqlx::query!( + r#" + select + id as "id: Id", + display_name as "display_name: String" + from login + "#, + ) .fetch_all(&mut *self.0) .await?; - Ok(messages) + for login in logins { + let name = Name::from(login.display_name); + let canonical_name = name.canonical(); + + sqlx::query!( + r#" + update login + set canonical_name = $1 + where id = $2 + "#, + canonical_name, + login.id, + ) + .execute(&mut *self.0) + .await?; + } + + Ok(()) } } -impl<'t> From<&'t mut SqliteConnection> for Logins<'t> { - fn from(tx: &'t mut SqliteConnection) -> Self { - Self(tx) - } +#[derive(Debug, thiserror::Error)] +#[error(transparent)] +pub enum LoadError { + Database(#[from] sqlx::Error), + Name(#[from] name::Error), } diff --git a/src/login/routes.rs b/src/login/routes.rs deleted file mode 100644 index 6579ae6..0000000 --- a/src/login/routes.rs +++ /dev/null @@ -1,97 +0,0 @@ -use axum::{ - extract::{Json, State}, - http::StatusCode, - response::{IntoResponse, Response}, - routing::post, - Router, -}; - -use crate::{ - app::App, - clock::RequestedAt, - error::{Internal, Unauthorized}, - login::Password, - token::{app, extract::IdentityToken}, -}; - -#[cfg(test)] -mod test; - -pub fn router() -> Router<App> { - Router::new() - .route("/api/auth/login", post(on_login)) - .route("/api/auth/logout", post(on_logout)) -} - -#[derive(serde::Deserialize)] -struct LoginRequest { - name: String, - password: Password, -} - -async fn on_login( - State(app): State<App>, - RequestedAt(now): RequestedAt, - identity: IdentityToken, - Json(request): Json<LoginRequest>, -) -> Result<(IdentityToken, StatusCode), LoginError> { - let token = app - .tokens() - .login(&request.name, &request.password, &now) - .await - .map_err(LoginError)?; - let identity = identity.set(token); - Ok((identity, StatusCode::NO_CONTENT)) -} - -#[derive(Debug)] -struct LoginError(app::LoginError); - -impl IntoResponse for LoginError { - fn into_response(self) -> Response { - let Self(error) = self; - match error { - app::LoginError::Rejected => { - // not error::Unauthorized due to differing messaging - (StatusCode::UNAUTHORIZED, "invalid name or password").into_response() - } - other => Internal::from(other).into_response(), - } - } -} - -#[derive(serde::Deserialize)] -struct LogoutRequest {} - -async fn on_logout( - State(app): State<App>, - RequestedAt(now): RequestedAt, - identity: IdentityToken, - // This forces the only valid request to be `{}`, and not the infinite - // variation allowed when there's no body extractor. - Json(LogoutRequest {}): Json<LogoutRequest>, -) -> Result<(IdentityToken, StatusCode), LogoutError> { - if let Some(secret) = identity.secret() { - let (token, _) = app.tokens().validate(&secret, &now).await?; - app.tokens().logout(&token).await?; - } - - let identity = identity.clear(); - Ok((identity, StatusCode::NO_CONTENT)) -} - -#[derive(Debug, thiserror::Error)] -#[error(transparent)] -enum LogoutError { - ValidateError(#[from] app::ValidateError), - DatabaseError(#[from] sqlx::Error), -} - -impl IntoResponse for LogoutError { - fn into_response(self) -> Response { - match self { - Self::ValidateError(app::ValidateError::InvalidToken) => Unauthorized.into_response(), - other => Internal::from(other).into_response(), - } - } -} diff --git a/src/login/routes/login/mod.rs b/src/login/routes/login/mod.rs new file mode 100644 index 0000000..36b384e --- /dev/null +++ b/src/login/routes/login/mod.rs @@ -0,0 +1,4 @@ +pub mod post; + +#[cfg(test)] +mod test; diff --git a/src/login/routes/login/post.rs b/src/login/routes/login/post.rs new file mode 100644 index 0000000..96da5c5 --- /dev/null +++ b/src/login/routes/login/post.rs @@ -0,0 +1,52 @@ +use axum::{ + extract::{Json, State}, + http::StatusCode, + response::{IntoResponse, Response}, +}; + +use crate::{ + app::App, + clock::RequestedAt, + error::Internal, + login::{Login, Password}, + name::Name, + token::{app, extract::IdentityCookie}, +}; + +pub async fn handler( + State(app): State<App>, + RequestedAt(now): RequestedAt, + identity: IdentityCookie, + Json(request): Json<Request>, +) -> Result<(IdentityCookie, Json<Login>), Error> { + let (login, secret) = app + .tokens() + .login(&request.name, &request.password, &now) + .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(#[from] pub app::LoginError); + +impl IntoResponse for Error { + fn into_response(self) -> Response { + let Self(error) = self; + match error { + app::LoginError::Rejected => { + // not error::Unauthorized due to differing messaging + (StatusCode::UNAUTHORIZED, "invalid name or password").into_response() + } + other => Internal::from(other).into_response(), + } + } +} diff --git a/src/login/routes/test/login.rs b/src/login/routes/login/test.rs index 68c92de..7399796 100644 --- a/src/login/routes/test/login.rs +++ b/src/login/routes/login/test.rs @@ -1,9 +1,7 @@ -use axum::{ - extract::{Json, State}, - http::StatusCode, -}; +use axum::extract::{Json, State}; -use crate::{login::routes, test::fixtures, token::app}; +use super::post; +use crate::{test::fixtures, token::app}; #[tokio::test] async fn correct_credentials() { @@ -14,21 +12,23 @@ async fn correct_credentials() { // Call the endpoint - let identity = fixtures::identity::not_logged_in(); + let identity = fixtures::cookie::not_logged_in(); let logged_in_at = fixtures::now(); - let request = routes::LoginRequest { + let request = post::Request { name: name.clone(), password, }; - let (identity, status) = - routes::on_login(State(app.clone()), logged_in_at, identity, Json(request)) + let (identity, Json(response)) = + post::handler(State(app.clone()), logged_in_at, identity, Json(request)) .await .expect("logged in with valid credentials"); // Verify the return value's basic structure - assert_eq!(StatusCode::NO_CONTENT, status); - let secret = identity.secret().expect("logged in with valid credentials"); + assert_eq!(name, response.name); + let secret = identity + .secret() + .expect("logged in with valid credentials issues an identity cookie"); // Verify the semantics @@ -39,7 +39,7 @@ async fn correct_credentials() { .await .expect("identity secret is valid"); - assert_eq!(name, validated_login.name); + assert_eq!(response, validated_login); } #[tokio::test] @@ -50,17 +50,17 @@ async fn invalid_name() { // Call the endpoint - let identity = fixtures::identity::not_logged_in(); + let identity = fixtures::cookie::not_logged_in(); let logged_in_at = fixtures::now(); let (name, password) = fixtures::login::propose(); - let request = routes::LoginRequest { + let request = post::Request { name: name.clone(), password, }; - let routes::LoginError(error) = - routes::on_login(State(app.clone()), logged_in_at, identity, Json(request)) + let post::Error(error) = + post::handler(State(app.clone()), logged_in_at, identity, Json(request)) .await - .expect_err("logged in with an incorrect password"); + .expect_err("logged in with an incorrect password fails"); // Verify the return value's basic structure @@ -77,13 +77,13 @@ async fn incorrect_password() { // Call the endpoint let logged_in_at = fixtures::now(); - let identity = fixtures::identity::not_logged_in(); - let request = routes::LoginRequest { + let identity = fixtures::cookie::not_logged_in(); + let request = post::Request { name: login.name, password: fixtures::login::propose_password(), }; - let routes::LoginError(error) = - routes::on_login(State(app.clone()), logged_in_at, identity, Json(request)) + let post::Error(error) = + post::handler(State(app.clone()), logged_in_at, identity, Json(request)) .await .expect_err("logged in with an incorrect password"); @@ -102,9 +102,9 @@ async fn token_expires() { // Call the endpoint let logged_in_at = fixtures::ancient(); - let identity = fixtures::identity::not_logged_in(); - let request = routes::LoginRequest { name, password }; - let (identity, _) = routes::on_login(State(app.clone()), logged_in_at, identity, Json(request)) + let identity = fixtures::cookie::not_logged_in(); + let request = post::Request { name, password }; + let (identity, _) = post::handler(State(app.clone()), logged_in_at, identity, Json(request)) .await .expect("logged in with valid credentials"); let secret = identity.secret().expect("logged in with valid credentials"); diff --git a/src/login/routes/logout/mod.rs b/src/login/routes/logout/mod.rs new file mode 100644 index 0000000..36b384e --- /dev/null +++ b/src/login/routes/logout/mod.rs @@ -0,0 +1,4 @@ +pub mod post; + +#[cfg(test)] +mod test; diff --git a/src/login/routes/logout/post.rs b/src/login/routes/logout/post.rs new file mode 100644 index 0000000..bb09b9f --- /dev/null +++ b/src/login/routes/logout/post.rs @@ -0,0 +1,47 @@ +use axum::{ + extract::{Json, State}, + http::StatusCode, + response::{IntoResponse, Response}, +}; + +use crate::{ + app::App, + clock::RequestedAt, + error::{Internal, Unauthorized}, + token::{app, extract::IdentityCookie}, +}; + +pub async fn handler( + State(app): State<App>, + RequestedAt(now): RequestedAt, + identity: IdentityCookie, + Json(_): Json<Request>, +) -> Result<(IdentityCookie, StatusCode), Error> { + if let Some(secret) = identity.secret() { + let (token, _) = app.tokens().validate(&secret, &now).await?; + app.tokens().logout(&token).await?; + } + + let identity = identity.clear(); + Ok((identity, StatusCode::NO_CONTENT)) +} + +// This forces the only valid request to be `{}`, and not the infinite +// variation allowed when there's no body extractor. +#[derive(Default, serde::Deserialize)] +pub struct Request {} + +#[derive(Debug, thiserror::Error)] +#[error(transparent)] +pub struct Error(#[from] pub app::ValidateError); + +impl IntoResponse for Error { + fn into_response(self) -> Response { + let Self(error) = self; + #[allow(clippy::match_wildcard_for_single_variants)] + match error { + app::ValidateError::InvalidToken => Unauthorized.into_response(), + other => Internal::from(other).into_response(), + } + } +} diff --git a/src/login/routes/logout/test.rs b/src/login/routes/logout/test.rs new file mode 100644 index 0000000..775fa9f --- /dev/null +++ b/src/login/routes/logout/test.rs @@ -0,0 +1,79 @@ +use axum::{ + extract::{Json, State}, + http::StatusCode, +}; + +use super::post; +use crate::{test::fixtures, token::app}; + +#[tokio::test] +async fn successful() { + // Set up the environment + + let app = fixtures::scratch_app().await; + let now = fixtures::now(); + let creds = fixtures::login::create_with_password(&app, &fixtures::now()).await; + let identity = fixtures::cookie::logged_in(&app, &creds, &now).await; + let secret = fixtures::cookie::secret(&identity); + + // Call the endpoint + + let (response_identity, response_status) = post::handler( + State(app.clone()), + fixtures::now(), + identity.clone(), + Json::default(), + ) + .await + .expect("logged out with a valid token"); + + // Verify the return value's basic structure + + assert!(response_identity.secret().is_none()); + assert_eq!(StatusCode::NO_CONTENT, response_status); + + // Verify the semantics + let error = app + .tokens() + .validate(&secret, &now) + .await + .expect_err("secret is invalid"); + assert!(matches!(error, app::ValidateError::InvalidToken)); +} + +#[tokio::test] +async fn no_identity() { + // Set up the environment + + let app = fixtures::scratch_app().await; + + // Call the endpoint + + let identity = fixtures::cookie::not_logged_in(); + let (identity, status) = post::handler(State(app), fixtures::now(), identity, Json::default()) + .await + .expect("logged out with no token succeeds"); + + // Verify the return value's basic structure + + assert!(identity.secret().is_none()); + assert_eq!(StatusCode::NO_CONTENT, status); +} + +#[tokio::test] +async fn invalid_token() { + // Set up the environment + + let app = fixtures::scratch_app().await; + + // Call the endpoint + + let identity = fixtures::cookie::fictitious(); + let post::Error(error) = post::handler(State(app), fixtures::now(), identity, Json::default()) + .await + .expect_err("logged out with an invalid token fails"); + + // Verify the return value's basic structure + + assert!(matches!(error, app::ValidateError::InvalidToken)); +} diff --git a/src/login/routes/mod.rs b/src/login/routes/mod.rs new file mode 100644 index 0000000..8cb8852 --- /dev/null +++ b/src/login/routes/mod.rs @@ -0,0 +1,12 @@ +use axum::{routing::post, Router}; + +use crate::app::App; + +mod login; +mod logout; + +pub fn router() -> Router<App> { + Router::new() + .route("/api/auth/login", post(login::post::handler)) + .route("/api/auth/logout", post(logout::post::handler)) +} diff --git a/src/login/routes/test/logout.rs b/src/login/routes/test/logout.rs deleted file mode 100644 index 611829e..0000000 --- a/src/login/routes/test/logout.rs +++ /dev/null @@ -1,97 +0,0 @@ -use axum::{ - extract::{Json, State}, - http::StatusCode, -}; - -use crate::{login::routes, test::fixtures, token::app}; - -#[tokio::test] -async fn successful() { - // Set up the environment - - let app = fixtures::scratch_app().await; - let now = fixtures::now(); - let login = fixtures::login::create_with_password(&app, &fixtures::now()).await; - let identity = fixtures::identity::logged_in(&app, &login, &now).await; - let secret = fixtures::identity::secret(&identity); - - // Call the endpoint - - let (response_identity, response_status) = routes::on_logout( - State(app.clone()), - fixtures::now(), - identity.clone(), - Json(routes::LogoutRequest {}), - ) - .await - .expect("logged out with a valid token"); - - // Verify the return value's basic structure - - assert!(response_identity.secret().is_none()); - assert_eq!(StatusCode::NO_CONTENT, response_status); - - // Verify the semantics - - let error = app - .tokens() - .validate(&secret, &now) - .await - .expect_err("secret is invalid"); - match error { - app::ValidateError::InvalidToken => (), // should be invalid - other @ app::ValidateError::DatabaseError(_) => { - panic!("expected ValidateError::InvalidToken, got {other:#}") - } - } -} - -#[tokio::test] -async fn no_identity() { - // Set up the environment - - let app = fixtures::scratch_app().await; - - // Call the endpoint - - let identity = fixtures::identity::not_logged_in(); - let (identity, status) = routes::on_logout( - State(app), - fixtures::now(), - identity, - Json(routes::LogoutRequest {}), - ) - .await - .expect("logged out with no token"); - - // Verify the return value's basic structure - - assert!(identity.secret().is_none()); - assert_eq!(StatusCode::NO_CONTENT, status); -} - -#[tokio::test] -async fn invalid_token() { - // Set up the environment - - let app = fixtures::scratch_app().await; - - // Call the endpoint - - let identity = fixtures::identity::fictitious(); - let error = routes::on_logout( - State(app), - fixtures::now(), - identity, - Json(routes::LogoutRequest {}), - ) - .await - .expect_err("logged out with an invalid token"); - - // Verify the return value's basic structure - - assert!(matches!( - error, - routes::LogoutError::ValidateError(app::ValidateError::InvalidToken) - )); -} diff --git a/src/login/routes/test/mod.rs b/src/login/routes/test/mod.rs deleted file mode 100644 index 90522c4..0000000 --- a/src/login/routes/test/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -mod login; -mod logout; diff --git a/src/login/snapshot.rs b/src/login/snapshot.rs index 1a92f5c..e1eb96c 100644 --- a/src/login/snapshot.rs +++ b/src/login/snapshot.rs @@ -2,6 +2,7 @@ use super::{ event::{Created, Event}, Id, }; +use crate::name::Name; // This also implements FromRequestParts (see `./extract.rs`). As a result, it // can be used as an extractor for endpoints that want to require login, or for @@ -10,7 +11,7 @@ use super::{ #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)] pub struct Login { pub id: Id, - pub name: String, + pub name: Name, // The omission of the hashed password is deliberate, to minimize the // chance that it ends up tangled up in debug output or in some other chunk // of logic elsewhere. |
