diff options
| -rw-r--r-- | docs/api/authentication.md | 53 | ||||
| -rw-r--r-- | docs/api/initial-setup.md | 27 | ||||
| -rw-r--r-- | docs/api/invitations.md | 39 | ||||
| -rw-r--r-- | src/empty.rs | 11 | ||||
| -rw-r--r-- | src/event/handlers/stream/test/invite.rs | 14 | ||||
| -rw-r--r-- | src/event/handlers/stream/test/setup.rs | 7 | ||||
| -rw-r--r-- | src/invite/app.rs | 6 | ||||
| -rw-r--r-- | src/invite/handlers/accept/mod.rs | 8 | ||||
| -rw-r--r-- | src/invite/handlers/accept/test.rs | 16 | ||||
| -rw-r--r-- | src/lib.rs | 1 | ||||
| -rw-r--r-- | src/setup/app.rs | 11 | ||||
| -rw-r--r-- | src/setup/handlers/setup/mod.rs | 10 | ||||
| -rw-r--r-- | src/setup/handlers/setup/test.rs | 19 | ||||
| -rw-r--r-- | src/test/fixtures/cookie.rs | 4 | ||||
| -rw-r--r-- | src/token/app.rs | 22 | ||||
| -rw-r--r-- | src/user/app.rs | 3 | ||||
| -rw-r--r-- | src/user/create.rs | 5 | ||||
| -rw-r--r-- | src/user/handlers/login/mod.rs | 8 | ||||
| -rw-r--r-- | src/user/handlers/login/test.rs | 7 | ||||
| -rw-r--r-- | src/user/handlers/logout/mod.rs | 6 | ||||
| -rw-r--r-- | src/user/handlers/logout/test.rs | 13 | ||||
| -rw-r--r-- | src/user/handlers/password/mod.rs | 8 | ||||
| -rw-r--r-- | src/user/handlers/password/test.rs | 20 | ||||
| -rw-r--r-- | src/user/history.rs | 1 |
24 files changed, 138 insertions, 181 deletions
diff --git a/docs/api/authentication.md b/docs/api/authentication.md index fbd5959..189103e 100644 --- a/docs/api/authentication.md +++ b/docs/api/authentication.md @@ -71,32 +71,15 @@ The request must have the following fields: <!-- This prose is duplicated by 03-initial-setup.md and in 04-invitations.md, with small changes for context. If you edit it here, edit it there, too. --> -This endpoint will respond with a status of -`200 Okay` when successful. The body of the response will be a JSON object describing the authenticated user: - -```json -{ - "id": "Uabcd1234", - "name": "Andrea" -} -``` - -The response will include the following fields: - -| Field | Type | Description | -| :----- | :----- | :----------------------------- | -| `id` | string | The authenticated user's ID. | -| `name` | string | The authenticated user's name. | +This endpoint will respond with a status of `204 No Content` when successful. -The response will include a `Set-Cookie` header for the -`identity` cookie, providing the client with a newly-minted identity token associated with the user identified in the request. This token's value must be kept confidential. +The response will include a `Set-Cookie` header for the `identity` cookie, providing the client with a newly-minted identity token associated with the user identified in the request. This token's value must be kept confidential. The cookie will expire if it is not used regularly. ### Authentication failure -This endpoint will respond with a status of -`401 Unauthorized` if the login name and password do not correspond to an existing user. +This endpoint will respond with a status of `401 Unauthorized` if the login name and password do not correspond to an existing user. ## `POST /api/auth/logout` @@ -114,13 +97,16 @@ The request must be an empty JSON object. This endpoint will respond with a status of `204 No Content` when successful. -The response will include a `Set-Cookie` header that clears the -`identity` cookie. Regardless of whether the client clears the cookie, the service also invalidates the token. +The response will include a `Set-Cookie` header that clears the `identity` cookie. Regardless of whether the client clears the cookie, the service also invalidates the token. ## `POST /api/password` Changes the current user's password, and invalidates all outstanding identity tokens. +### Authentication failure + +This endpoint will respond with a status of `401 Unauthorized` if the provided identity token is not valid. + ### Request ```json @@ -139,29 +125,12 @@ The request must have the following fields: ### Success -This endpoint will respond with a status of -`200 Okay` when successful. The body of the response will be a JSON object describing the authenticated user: - -```json -{ - "id": "Uabcd1234", - "name": "Andrea" -} -``` - -The response will include the following fields: - -| Field | Type | Description | -| :----- | :----- | :----------------------------- | -| `id` | string | The authenticated user's ID. | -| `name` | string | The authenticated user's name. | +This endpoint will respond with a status of `204 No Content` when successful. -The response will include a `Set-Cookie` header for the -`identity` cookie, providing the client with a newly-minted identity token associated with the login identified in the request. This token's value must be kept confidential. All previously-created identity cookies will cease to be valid. +The response will include a `Set-Cookie` header for the `identity` cookie, providing the client with a newly-minted identity token associated with the login identified in the request. This token's value must be kept confidential. All previously-created identity cookies will cease to be valid. The cookie will expire if it is not used regularly. ### Authentication failure -This endpoint will respond with a status of `400 Bad Request` if the -`password` does not match the login's current password. +This endpoint will respond with a status of `400 Bad Request` if the `password` does not match the login's current password. diff --git a/docs/api/initial-setup.md b/docs/api/initial-setup.md index a3f6506..b52771f 100644 --- a/docs/api/initial-setup.md +++ b/docs/api/initial-setup.md @@ -17,8 +17,7 @@ New instances of this service require an initial setup step before they can full ## Requests before setup completed -Before the service is set up, all API endpoints, other than those specifically documented as exceptions, will return a status of -`503 Service Unavailable` to all requests. +Before the service is set up, all API endpoints, other than those specifically documented as exceptions, will return a status of `503 Service Unavailable` to all requests. Initial setup can be completed only once. @@ -64,29 +63,9 @@ The proposed `name` must be valid. The precise definition of valid is still up i <!-- This prose is duplicated from authentication.md, with small changes for context. If you edit it here, edit it there, too. --> -This endpoint will respond with a status of -`200 Okay` when successful. The body of the response will be a JSON object describing the newly-created user: +This endpoint will respond with a status of `204 No Content` when successful. -```json -{ - "id": "Uabcd1234", - "name": "Andrea" -} -``` - -The response will include the following fields: - -| Field | Type | Description | -| :----- | :----- | :------------------------------------------------------------------------------------------------------------------------------------------------- | -| `id` | string | A unique identifier for the newly-created user. This can be used to associate the user with other events, or to make API calls targeting the user. | -| `name` | string | The user's name. | - -The returned name may not be identical to the name requested, as the name will be converted to [normalization form C](http://www.unicode.org/reports/tr15/) automatically. The returned name will include this normalization; the service will use the normalized name elsewhere, and does not store the originally requested name. - -The provided password will also be converted to normalization form C. However, the normalized password is not returned to the client. - -The response will include a `Set-Cookie` header for the -`identity` cookie, providing the client with a newly-minted identity token associated with the initial user created for this request. See the [authentication](./authentication) section for details on how this cookie may be used. +The response will include a `Set-Cookie` header for the `identity` cookie, providing the client with a newly-minted identity token associated with the initial user created for this request. See the [authentication](./authentication) section for details on how this cookie may be used. The cookie will expire if it is not used regularly. diff --git a/docs/api/invitations.md b/docs/api/invitations.md index 04e92c8..9b86b9e 100644 --- a/docs/api/invitations.md +++ b/docs/api/invitations.md @@ -39,8 +39,7 @@ The request must be an empty JSON object. ### Success -This endpoint will respond with a status of -`200 Okay` when successful. The body of the response will be a JSON object describing the new invitation: +This endpoint will respond with a status of `200 Okay` when successful. The body of the response will be a JSON object describing the new invitation: ```json { @@ -58,8 +57,7 @@ The response will include the following fields: | `issuer` | string | The user ID of the invitation's issuer. | | `issued_at` | string | The timestamp from which the invitation will expire. | -Clients and their operators are responsible for delivering the invitation to the invitee. Clients are strongly recommended to construct a URL for the invitation so that the invitee can take action on it easily. The included client supports URLs of the format -`https://example.net/invite/:id` (with the `:id` placeholder substituted with the invitation's ID). +Clients and their operators are responsible for delivering the invitation to the invitee. Clients are strongly recommended to construct a URL for the invitation so that the invitee can take action on it easily. The included client supports URLs of the format `https://example.net/invite/:id` (with the `:id` placeholder substituted with the invitation's ID). ## `GET /api/invite/:id` @@ -75,8 +73,7 @@ This endpoint requires the following path parameter: ### On success -This endpoint will respond with a status of -`200 Okay` when successful. The body of the response will be a JSON object describing the invitation: +This endpoint will respond with a status of `200 Okay` when successful. The body of the response will be a JSON object describing the invitation: ```json { @@ -96,13 +93,11 @@ The response will include the following fields: | `issuer` | string | The name of the invitation's issuer. | | `issued_at` | string | The timestamp from which the invitation will expire. | -Clients should present the -`issuer` to the user when presenting an invitation, so as to personalize the invitation and help them understand their connection with the service. +Clients should present the `issuer` to the user when presenting an invitation, so as to personalize the invitation and help them understand their connection with the service. ### Invitation not found -This endpoint will respond with a status of -`404 Not Found` when the invitation ID either does not exist, or has already been accepted. +This endpoint will respond with a status of `404 Not Found` when the invitation ID either does not exist, or has already been accepted. ## `POST /api/invite/:id` @@ -146,29 +141,9 @@ The proposed `name` must be valid. The precise definition of valid is still up i <!-- This prose is duplicated from authentication.md, with small changes for context. If you edit it here, edit it there, too. --> -This endpoint will respond with a status of -`200 Okay` when successful. The body of the response will be a JSON object describing the newly-created user: - -```json -{ - "id": "Uabcd1234", - "name": "Andrea" -} -``` - -The response will include the following fields: - -| Field | Type | Description | -| :----- | :----- | :------------------------------------------------------------------------------------------------------------------------------------------------- | -| `id` | string | A unique identifier for the newly-created user. This can be used to associate the user with other events, or to make API calls targeting the user. | -| `name` | string | The user's name. | - -The returned name may not be identical to the name requested, as the name will be converted to [normalization form C](http://www.unicode.org/reports/tr15/) automatically. The returned name will include this normalization; the service will use the normalized name elsewhere, and does not store the originally requested name. - -The provided password will also be converted to normalization form C. However, the normalized password is not returned to the client. +This endpoint will respond with a status of `204 No Content` when successful. -The response will include a `Set-Cookie` header for the -`identity` cookie, providing the client with a newly-minted identity token associated with the login created for this request. See the [authentication](./authentication.md) section for details on how this cookie may be used. +The response will include a `Set-Cookie` header for the `identity` cookie, providing the client with a newly-minted identity token associated with the login created for this request. See the [authentication](./authentication.md) section for details on how this cookie may be used. The cookie will expire if it is not used regularly. diff --git a/src/empty.rs b/src/empty.rs new file mode 100644 index 0000000..2c66051 --- /dev/null +++ b/src/empty.rs @@ -0,0 +1,11 @@ +use axum::http::StatusCode; +use axum::response::{IntoResponse, Response}; + +#[derive(Debug)] +pub struct Empty; + +impl IntoResponse for Empty { + fn into_response(self) -> Response { + StatusCode::NO_CONTENT.into_response() + } +} diff --git a/src/event/handlers/stream/test/invite.rs b/src/event/handlers/stream/test/invite.rs index 01372ce..f2d6950 100644 --- a/src/event/handlers/stream/test/invite.rs +++ b/src/event/handlers/stream/test/invite.rs @@ -28,11 +28,16 @@ async fn accepting_invite() { // Accept the invite let (name, password) = fixtures::user::propose(); - let (joiner, _) = app + let joiner = app .invites() .accept(&invite.id, &name, &password, &fixtures::now()) .await .expect("accepting an invite succeeds"); + let (_, joiner) = app + .tokens() + .validate(&joiner, &fixtures::now()) + .await + .expect("a newly-issued secret is valid"); // Expect a login created event @@ -57,11 +62,16 @@ async fn previously_accepted_invite() { // Accept the invite let (name, password) = fixtures::user::propose(); - let (joiner, _) = app + let joiner = app .invites() .accept(&invite.id, &name, &password, &fixtures::now()) .await .expect("accepting an invite succeeds"); + let (_, joiner) = app + .tokens() + .validate(&joiner, &fixtures::now()) + .await + .expect("a newly-issued secret is valid"); // Subscribe diff --git a/src/event/handlers/stream/test/setup.rs b/src/event/handlers/stream/test/setup.rs index 992b962..297162e 100644 --- a/src/event/handlers/stream/test/setup.rs +++ b/src/event/handlers/stream/test/setup.rs @@ -17,11 +17,16 @@ async fn previously_completed() { // Complete initial setup let (name, password) = fixtures::user::propose(); - let (owner, _) = app + let secret = app .setup() .initial(&name, &password, &fixtures::now()) .await .expect("initial setup in an empty app succeeds"); + let (_, owner) = app + .tokens() + .validate(&secret, &fixtures::now()) + .await + .expect("secret returned by initial setup should be valid"); // Subscribe to events diff --git a/src/invite/app.rs b/src/invite/app.rs index a286a8f..1c85562 100644 --- a/src/invite/app.rs +++ b/src/invite/app.rs @@ -47,7 +47,7 @@ impl<'a> Invites<'a> { name: &Name, password: &Password, accepted_at: &DateTime, - ) -> Result<(User, Secret), AcceptError> { + ) -> Result<Secret, AcceptError> { let create = Create::begin(name, password, accepted_at); let mut tx = self.db.begin().await?; @@ -74,9 +74,9 @@ impl<'a> Invites<'a> { let secret = tx.tokens().issue(stored.user(), accepted_at).await?; tx.commit().await?; - let login = stored.publish(self.events); + stored.publish(self.events); - Ok((login.as_created(), secret)) + Ok(secret) } pub async fn expire(&self, relative_to: &DateTime) -> Result<(), sqlx::Error> { diff --git a/src/invite/handlers/accept/mod.rs b/src/invite/handlers/accept/mod.rs index 9fa4d6a..cdf385f 100644 --- a/src/invite/handlers/accept/mod.rs +++ b/src/invite/handlers/accept/mod.rs @@ -7,12 +7,12 @@ use axum::{ use crate::{ app::App, clock::RequestedAt, + empty::Empty, error::{Internal, NotFound}, invite::{app, handlers::PathInfo}, name::Name, password::Password, token::extract::IdentityCookie, - user::User, }; #[cfg(test)] @@ -24,14 +24,14 @@ pub async fn handler( identity: IdentityCookie, Path(invite): Path<PathInfo>, Json(request): Json<Request>, -) -> Result<(IdentityCookie, Json<User>), Error> { - let (login, secret) = app +) -> Result<(IdentityCookie, Empty), Error> { + let secret = app .invites() .accept(&invite, &request.name, &request.password, &accepted_at) .await .map_err(Error)?; let identity = identity.set(secret); - Ok((identity, Json(login))) + Ok((identity, Empty)) } #[derive(serde::Deserialize)] diff --git a/src/invite/handlers/accept/test.rs b/src/invite/handlers/accept/test.rs index cb13900..adc7aa4 100644 --- a/src/invite/handlers/accept/test.rs +++ b/src/invite/handlers/accept/test.rs @@ -1,6 +1,6 @@ use axum::extract::{Json, Path, State}; -use crate::{invite::app::AcceptError, name::Name, test::fixtures}; +use crate::{empty::Empty, invite::app::AcceptError, name::Name, test::fixtures}; #[tokio::test] async fn valid_invite() { @@ -18,7 +18,7 @@ async fn valid_invite() { name: name.clone(), password: password.clone(), }; - let (identity, Json(response)) = super::handler( + let (identity, Empty) = super::handler( State(app.clone()), fixtures::now(), identity, @@ -31,7 +31,6 @@ async fn valid_invite() { // Verify the response assert!(identity.secret().is_some()); - assert_eq!(name, response.name); // Verify that the issued token is valid @@ -43,16 +42,21 @@ async fn valid_invite() { .validate(&secret, &fixtures::now()) .await .expect("newly-issued identity cookie is valid"); - assert_eq!(response, login); + assert_eq!(name, login.name); // Verify that the given credentials can log in - let (login, _) = app + let secret = app .tokens() .login(&name, &password, &fixtures::now()) .await .expect("credentials given on signup are valid"); - assert_eq!(response, login); + let (_, login) = app + .tokens() + .validate(&secret, &fixtures::now()) + .await + .expect("validating a newly-issued token secret succeeds"); + assert_eq!(name, login.name); } #[tokio::test] @@ -9,6 +9,7 @@ pub mod cli; mod clock; mod conversation; mod db; +mod empty; mod error; mod event; mod expire; diff --git a/src/setup/app.rs b/src/setup/app.rs index 1210175..1856519 100644 --- a/src/setup/app.rs +++ b/src/setup/app.rs @@ -7,10 +7,7 @@ use crate::{ name::Name, password::Password, token::{Secret, repo::Provider as _}, - user::{ - User, - create::{self, Create}, - }, + user::create::{self, Create}, }; pub struct Setup<'a> { @@ -28,7 +25,7 @@ impl<'a> Setup<'a> { name: &Name, password: &Password, created_at: &DateTime, - ) -> Result<(User, Secret), Error> { + ) -> Result<Secret, Error> { let create = Create::begin(name, password, created_at); let validated = create.validate()?; @@ -42,9 +39,9 @@ impl<'a> Setup<'a> { let secret = tx.tokens().issue(stored.user(), created_at).await?; tx.commit().await?; - let user = stored.publish(self.events); + stored.publish(self.events); - Ok((user.as_created(), secret)) + Ok(secret) } pub async fn completed(&self) -> Result<bool, sqlx::Error> { diff --git a/src/setup/handlers/setup/mod.rs b/src/setup/handlers/setup/mod.rs index 9e31282..fe24798 100644 --- a/src/setup/handlers/setup/mod.rs +++ b/src/setup/handlers/setup/mod.rs @@ -5,8 +5,8 @@ use axum::{ }; use crate::{ - app::App, clock::RequestedAt, error::Internal, name::Name, password::Password, setup::app, - token::extract::IdentityCookie, user::User, + app::App, clock::RequestedAt, empty::Empty, error::Internal, name::Name, password::Password, + setup::app, token::extract::IdentityCookie, }; #[cfg(test)] @@ -17,14 +17,14 @@ pub async fn handler( RequestedAt(setup_at): RequestedAt, identity: IdentityCookie, Json(request): Json<Request>, -) -> Result<(IdentityCookie, Json<User>), Error> { - let (user, secret) = app +) -> Result<(IdentityCookie, Empty), Error> { + let secret = app .setup() .initial(&request.name, &request.password, &setup_at) .await .map_err(Error)?; let identity = identity.set(secret); - Ok((identity, Json(user))) + Ok((identity, Empty)) } #[derive(serde::Deserialize)] diff --git a/src/setup/handlers/setup/test.rs b/src/setup/handlers/setup/test.rs index 8243ac3..4a37690 100644 --- a/src/setup/handlers/setup/test.rs +++ b/src/setup/handlers/setup/test.rs @@ -1,6 +1,6 @@ use axum::extract::{Json, State}; -use crate::{setup::app, test::fixtures}; +use crate::{empty::Empty, setup::app, test::fixtures}; #[tokio::test] async fn fresh_instance() { @@ -15,15 +15,11 @@ async fn fresh_instance() { name: name.clone(), password: password.clone(), }; - let (identity, Json(response)) = + let (identity, Empty) = 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 @@ -34,16 +30,21 @@ async fn fresh_instance() { .validate(&secret, &fixtures::now()) .await .expect("newly-issued identity cookie is valid"); - assert_eq!(response, login); + assert_eq!(name, login.name); // Verify that the given credentials can log in - let (login, _) = app + let secret = app .tokens() .login(&name, &password, &fixtures::now()) .await .expect("credentials given on signup are valid"); - assert_eq!(response, login); + let (_, login) = app + .tokens() + .validate(&secret, &fixtures::now()) + .await + .expect("validating a newly-issued token secret succeeds"); + assert_eq!(name, login.name); } #[tokio::test] diff --git a/src/test/fixtures/cookie.rs b/src/test/fixtures/cookie.rs index 41779db..f5a32a6 100644 --- a/src/test/fixtures/cookie.rs +++ b/src/test/fixtures/cookie.rs @@ -18,13 +18,13 @@ pub async fn logged_in( now: &RequestedAt, ) -> IdentityCookie { let (name, password) = credentials; - let (_, token) = app + let secret = app .tokens() .login(name, password, now) .await .expect("should succeed given known-valid credentials"); - IdentityCookie::new().set(token) + IdentityCookie::new().set(secret) } pub fn secret(identity: &IdentityCookie) -> Secret { diff --git a/src/token/app.rs b/src/token/app.rs index 49f9a45..56c0e21 100644 --- a/src/token/app.rs +++ b/src/token/app.rs @@ -32,7 +32,7 @@ impl<'a> Tokens<'a> { name: &Name, password: &Password, login_at: &DateTime, - ) -> Result<(User, Secret), LoginError> { + ) -> Result<Secret, LoginError> { let mut tx = self.db.begin().await?; let (user, stored_hash) = tx .auth() @@ -47,18 +47,16 @@ impl<'a> Tokens<'a> { // if the account is deleted during that time. tx.commit().await?; - let snapshot = user.as_snapshot().ok_or(LoginError::Rejected)?; + user.as_snapshot().ok_or(LoginError::Rejected)?; - let token = if stored_hash.verify(password)? { + if stored_hash.verify(password)? { let mut tx = self.db.begin().await?; - let token = tx.tokens().issue(&user, login_at).await?; + let secret = tx.tokens().issue(&user, login_at).await?; tx.commit().await?; - token + Ok(secret) } else { - Err(LoginError::Rejected)? - }; - - Ok((snapshot, token)) + Err(LoginError::Rejected) + } } pub async fn change_password( @@ -67,7 +65,7 @@ impl<'a> Tokens<'a> { password: &Password, to: &Password, changed_at: &DateTime, - ) -> Result<(User, Secret), LoginError> { + ) -> Result<Secret, LoginError> { let mut tx = self.db.begin().await?; let (user, stored_hash) = tx .auth() @@ -86,7 +84,7 @@ impl<'a> Tokens<'a> { return Err(LoginError::Rejected); } - let snapshot = user.as_snapshot().ok_or(LoginError::Rejected)?; + user.as_snapshot().ok_or(LoginError::Rejected)?; let to_hash = to.hash()?; let mut tx = self.db.begin().await?; @@ -99,7 +97,7 @@ impl<'a> Tokens<'a> { self.token_events.broadcast(event); } - Ok((snapshot, secret)) + Ok(secret) } pub async fn validate( diff --git a/src/user/app.rs b/src/user/app.rs index 5f58981..301c39c 100644 --- a/src/user/app.rs +++ b/src/user/app.rs @@ -29,7 +29,8 @@ impl<'a> Users<'a> { let stored = validated.store(&mut tx).await?; tx.commit().await?; - let user = stored.publish(self.events); + let user = stored.user().to_owned(); + stored.publish(self.events); Ok(user.as_created()) } diff --git a/src/user/create.rs b/src/user/create.rs index 0e7a118..5d7bf65 100644 --- a/src/user/create.rs +++ b/src/user/create.rs @@ -73,13 +73,10 @@ pub struct Stored { } impl Stored { - #[must_use = "dropping a user creation attempt is likely a mistake"] - pub fn publish(self, events: &Broadcaster) -> History { + pub fn publish(self, events: &Broadcaster) { let Self { user } = self; events.broadcast(user.events().map(Event::from).collect::<Vec<_>>()); - - user } pub fn user(&self) -> &History { diff --git a/src/user/handlers/login/mod.rs b/src/user/handlers/login/mod.rs index da88885..d3e0e8c 100644 --- a/src/user/handlers/login/mod.rs +++ b/src/user/handlers/login/mod.rs @@ -7,11 +7,11 @@ use axum::{ use crate::{ app::App, clock::RequestedAt, + empty::Empty, error::Internal, name::Name, password::Password, token::{app, extract::IdentityCookie}, - user::User, }; #[cfg(test)] @@ -22,14 +22,14 @@ pub async fn handler( RequestedAt(now): RequestedAt, identity: IdentityCookie, Json(request): Json<Request>, -) -> Result<(IdentityCookie, Json<User>), Error> { - let (user, secret) = app +) -> Result<(IdentityCookie, Empty), Error> { + let secret = app .tokens() .login(&request.name, &request.password, &now) .await .map_err(Error)?; let identity = identity.set(secret); - Ok((identity, Json(user))) + Ok((identity, Empty)) } #[derive(serde::Deserialize)] diff --git a/src/user/handlers/login/test.rs b/src/user/handlers/login/test.rs index b8f24f6..bdd1957 100644 --- a/src/user/handlers/login/test.rs +++ b/src/user/handlers/login/test.rs @@ -1,6 +1,6 @@ use axum::extract::{Json, State}; -use crate::{test::fixtures, token::app}; +use crate::{empty::Empty, test::fixtures, token::app}; #[tokio::test] async fn correct_credentials() { @@ -17,14 +17,13 @@ async fn correct_credentials() { name: name.clone(), password, }; - let (identity, Json(response)) = + let (identity, Empty) = super::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!(name, response.name); let secret = identity .secret() .expect("logged in with valid credentials issues an identity cookie"); @@ -38,7 +37,7 @@ async fn correct_credentials() { .await .expect("identity secret is valid"); - assert_eq!(response, validated_login); + assert_eq!(name, validated_login.name); } #[tokio::test] diff --git a/src/user/handlers/logout/mod.rs b/src/user/handlers/logout/mod.rs index 45a376a..4450e4c 100644 --- a/src/user/handlers/logout/mod.rs +++ b/src/user/handlers/logout/mod.rs @@ -1,12 +1,12 @@ use axum::{ extract::{Json, State}, - http::StatusCode, response::{IntoResponse, Response}, }; use crate::{ app::App, clock::RequestedAt, + empty::Empty, error::{Internal, Unauthorized}, token::{app, extract::IdentityCookie}, }; @@ -19,14 +19,14 @@ pub async fn handler( RequestedAt(now): RequestedAt, identity: IdentityCookie, Json(_): Json<Request>, -) -> Result<(IdentityCookie, StatusCode), Error> { +) -> Result<(IdentityCookie, Empty), 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)) + Ok((identity, Empty)) } // This forces the only valid request to be `{}`, and not the infinite diff --git a/src/user/handlers/logout/test.rs b/src/user/handlers/logout/test.rs index 8dc4636..7151ddf 100644 --- a/src/user/handlers/logout/test.rs +++ b/src/user/handlers/logout/test.rs @@ -1,9 +1,6 @@ -use axum::{ - extract::{Json, State}, - http::StatusCode, -}; +use axum::extract::{Json, State}; -use crate::{test::fixtures, token::app}; +use crate::{empty::Empty, test::fixtures, token::app}; #[tokio::test] async fn successful() { @@ -17,7 +14,7 @@ async fn successful() { // Call the endpoint - let (response_identity, response_status) = super::handler( + let (response_identity, Empty) = super::handler( State(app.clone()), fixtures::now(), identity.clone(), @@ -29,7 +26,6 @@ async fn successful() { // 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 @@ -49,14 +45,13 @@ async fn no_identity() { // Call the endpoint let identity = fixtures::cookie::not_logged_in(); - let (identity, status) = super::handler(State(app), fixtures::now(), identity, Json::default()) + let (identity, Empty) = super::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] diff --git a/src/user/handlers/password/mod.rs b/src/user/handlers/password/mod.rs index c327e87..5e69c1c 100644 --- a/src/user/handlers/password/mod.rs +++ b/src/user/handlers/password/mod.rs @@ -7,13 +7,13 @@ use axum::{ use crate::{ app::App, clock::RequestedAt, + empty::Empty, error::Internal, password::Password, token::{ app, extract::{Identity, IdentityCookie}, }, - user::User, }; #[cfg(test)] @@ -25,14 +25,14 @@ pub async fn handler( identity: Identity, cookie: IdentityCookie, Json(request): Json<Request>, -) -> Result<(IdentityCookie, Json<User>), Error> { - let (login, secret) = app +) -> Result<(IdentityCookie, Empty), Error> { + let secret = app .tokens() .change_password(&identity.user, &request.password, &request.to, &now) .await .map_err(Error)?; let cookie = cookie.set(secret); - Ok((cookie, Json(login))) + Ok((cookie, Empty)) } #[derive(serde::Deserialize)] diff --git a/src/user/handlers/password/test.rs b/src/user/handlers/password/test.rs index 42e41d8..ffa12f3 100644 --- a/src/user/handlers/password/test.rs +++ b/src/user/handlers/password/test.rs @@ -1,6 +1,7 @@ use axum::extract::{Json, State}; use crate::{ + empty::Empty, test::fixtures, token::app::{LoginError, ValidateError}, }; @@ -20,7 +21,7 @@ async fn password_change() { password: password.clone(), to: to.clone(), }; - let (new_cookie, Json(response)) = super::handler( + let (new_cookie, Empty) = super::handler( State(app.clone()), fixtures::now(), identity.clone(), @@ -34,7 +35,15 @@ async fn password_change() { assert_ne!(cookie.secret(), new_cookie.secret()); // Verify that we're still ourselves - assert_eq!(identity.user, response); + let new_secret = new_cookie + .secret() + .expect("we should have a secret after changing our password"); + let (_, login) = app + .tokens() + .validate(&new_secret, &fixtures::now()) + .await + .expect("the newly-issued secret should be valid"); + assert_eq!(identity.user, login); // Verify that our original token is no longer valid let validate_err = app @@ -58,10 +67,15 @@ async fn password_change() { assert!(matches!(login_err, LoginError::Rejected)); // Verify that our new password is valid - let (login, _) = app + let secret = app .tokens() .login(&name, &to, &fixtures::now()) .await .expect("logging in with the new password should succeed"); + let (_, login) = app + .tokens() + .validate(&secret, &fixtures::now()) + .await + .expect("validating a newly-issued token secret succeeds"); assert_eq!(identity.user, login); } diff --git a/src/user/history.rs b/src/user/history.rs index 72e0aee..4f99130 100644 --- a/src/user/history.rs +++ b/src/user/history.rs @@ -20,6 +20,7 @@ impl History { // if this returns a redacted or modified version of the user. If we implement // renames by redacting the original name, then this should return the edited // user, not the original, even if that's not how it was "as created.") + #[cfg(test)] pub fn as_created(&self) -> User { self.user.clone() } |
