From 25183f2ed7802375f15cb0069af7bee6dd2c7afd Mon Sep 17 00:00:00 2001 From: Vika Date: Sun, 10 Jul 2022 14:49:55 +0300 Subject: indieauth: rename to tokenauth This frees up the name for the future in-house IndieAuth implementation and also clarifies the purpose of this module. Its future is uncertain - most probably when the token endpoint gets finished, it will transform into a way to query that token endpoint. But then, the media endpoint also depends on it, so I might have to copy that implementation (that queries an external token endpoint) and make it generic enough so I could both query an external endpoint or use internal data. --- kittybox-rs/src/frontend/onboarding.rs | 2 +- kittybox-rs/src/indieauth.rs | 367 --------------------------------- kittybox-rs/src/lib.rs | 2 +- kittybox-rs/src/main.rs | 2 +- kittybox-rs/src/media/mod.rs | 2 +- kittybox-rs/src/micropub/mod.rs | 14 +- kittybox-rs/src/micropub/util.rs | 2 +- kittybox-rs/src/tokenauth.rs | 367 +++++++++++++++++++++++++++++++++ 8 files changed, 379 insertions(+), 379 deletions(-) delete mode 100644 kittybox-rs/src/indieauth.rs create mode 100644 kittybox-rs/src/tokenauth.rs diff --git a/kittybox-rs/src/frontend/onboarding.rs b/kittybox-rs/src/frontend/onboarding.rs index 9027201..08a05ee 100644 --- a/kittybox-rs/src/frontend/onboarding.rs +++ b/kittybox-rs/src/frontend/onboarding.rs @@ -56,7 +56,7 @@ async fn onboard( // Create a user to pass to the backend // At this point the site belongs to nobody, so it is safe to do let user = - crate::indieauth::User::new(user_uid.as_str(), "https://kittybox.fireburn.ru/", "create"); + crate::tokenauth::User::new(user_uid.as_str(), "https://kittybox.fireburn.ru/", "create"); if data.user["type"][0] != "h-card" || data.first_post["type"][0] != "h-entry" { return Err(FrontendError::with_code( diff --git a/kittybox-rs/src/indieauth.rs b/kittybox-rs/src/indieauth.rs deleted file mode 100644 index 103f514..0000000 --- a/kittybox-rs/src/indieauth.rs +++ /dev/null @@ -1,367 +0,0 @@ -use serde::{Deserialize, Serialize}; -use url::Url; - -#[derive(Deserialize, Serialize, Debug, PartialEq, Clone)] -pub struct User { - pub me: Url, - pub client_id: Url, - scope: String, -} - -#[derive(Debug, Clone, PartialEq, Copy)] -pub enum ErrorKind { - PermissionDenied, - NotAuthorized, - TokenEndpointError, - JsonParsing, - InvalidHeader, - Other, -} - -#[derive(Deserialize, Serialize, Debug, Clone)] -pub struct TokenEndpointError { - error: String, - error_description: String, -} - -#[derive(Debug)] -pub struct IndieAuthError { - source: Option>, - kind: ErrorKind, - msg: String, -} - -impl std::error::Error for IndieAuthError { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - self.source - .as_ref() - .map(|e| e.as_ref() as &dyn std::error::Error) - } -} - -impl std::fmt::Display for IndieAuthError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "{}: {}", - match self.kind { - ErrorKind::TokenEndpointError => "token endpoint returned an error: ", - ErrorKind::JsonParsing => "error while parsing token endpoint response: ", - ErrorKind::NotAuthorized => "token endpoint did not recognize the token: ", - ErrorKind::PermissionDenied => "token endpoint rejected the token: ", - ErrorKind::InvalidHeader => "authorization header parsing error: ", - ErrorKind::Other => "token endpoint communication error: ", - }, - self.msg - ) - } -} - -impl From for IndieAuthError { - fn from(err: serde_json::Error) -> Self { - Self { - msg: format!("{}", err), - source: Some(Box::new(err)), - kind: ErrorKind::JsonParsing, - } - } -} - -impl From for IndieAuthError { - fn from(err: reqwest::Error) -> Self { - Self { - msg: format!("{}", err), - source: Some(Box::new(err)), - kind: ErrorKind::Other, - } - } -} - -impl From for IndieAuthError { - fn from(err: axum::extract::rejection::TypedHeaderRejection) -> Self { - Self { - msg: format!("{:?}", err.reason()), - source: Some(Box::new(err)), - kind: ErrorKind::InvalidHeader, - } - } -} - -impl axum::response::IntoResponse for IndieAuthError { - fn into_response(self) -> axum::response::Response { - let status_code: StatusCode = match self.kind { - ErrorKind::PermissionDenied => StatusCode::FORBIDDEN, - ErrorKind::NotAuthorized => StatusCode::UNAUTHORIZED, - ErrorKind::TokenEndpointError => StatusCode::INTERNAL_SERVER_ERROR, - ErrorKind::JsonParsing => StatusCode::BAD_REQUEST, - ErrorKind::InvalidHeader => StatusCode::UNAUTHORIZED, - ErrorKind::Other => StatusCode::INTERNAL_SERVER_ERROR, - }; - - let body = serde_json::json!({ - "error": match self.kind { - ErrorKind::PermissionDenied => "forbidden", - ErrorKind::NotAuthorized => "unauthorized", - ErrorKind::TokenEndpointError => "token_endpoint_error", - ErrorKind::JsonParsing => "invalid_request", - ErrorKind::InvalidHeader => "unauthorized", - ErrorKind::Other => "unknown_error", - }, - "error_description": self.msg - }); - - (status_code, axum::response::Json(body)).into_response() - } -} - -impl User { - pub fn check_scope(&self, scope: &str) -> bool { - self.scopes().any(|i| i == scope) - } - pub fn scopes(&self) -> std::str::SplitAsciiWhitespace<'_> { - self.scope.split_ascii_whitespace() - } - pub fn new(me: &str, client_id: &str, scope: &str) -> Self { - Self { - me: Url::parse(me).unwrap(), - client_id: Url::parse(client_id).unwrap(), - scope: scope.to_string(), - } - } -} - -use axum::{ - extract::{Extension, FromRequest, RequestParts, TypedHeader}, - headers::{ - authorization::{Bearer, Credentials}, - Authorization, - }, - http::StatusCode, -}; - -// this newtype is required due to axum::Extension retrieving items by type -// it's based on compiler magic matching extensions by their type's hashes -#[derive(Debug, Clone)] -pub struct TokenEndpoint(pub url::Url); - -#[async_trait::async_trait] -impl FromRequest for User -where - B: Send, -{ - type Rejection = IndieAuthError; - - #[cfg_attr( - all(debug_assertions, not(test)), - allow(unreachable_code, unused_variables) - )] - async fn from_request(req: &mut RequestParts) -> Result { - // Return a fake user if we're running a debug build - // I don't wanna bother with authentication - #[cfg(all(debug_assertions, not(test)))] - return Ok(User::new( - "http://localhost:8080/", - "https://quill.p3k.io/", - "create update delete media", - )); - - let TypedHeader(Authorization(token)) = - TypedHeader::>::from_request(req) - .await - .map_err(IndieAuthError::from)?; - - let Extension(TokenEndpoint(token_endpoint)): Extension = - Extension::from_request(req).await.unwrap(); - - let Extension(http): Extension = - Extension::from_request(req).await.unwrap(); - - match http - .get(token_endpoint) - .header("Authorization", token.encode()) - .header("Accept", "application/json") - .send() - .await - { - Ok(res) => match res.status() { - StatusCode::OK => match res.json::().await { - Ok(json) => match serde_json::from_value::(json.clone()) { - Ok(user) => Ok(user), - Err(err) => { - if let Some(false) = json["active"].as_bool() { - Err(IndieAuthError { - source: None, - kind: ErrorKind::NotAuthorized, - msg: "The token is not active for this user.".to_owned(), - }) - } else { - Err(IndieAuthError::from(err)) - } - } - }, - Err(err) => Err(IndieAuthError::from(err)), - }, - StatusCode::BAD_REQUEST => match res.json::().await { - Ok(err) => { - if err.error == "unauthorized" { - Err(IndieAuthError { - source: None, - kind: ErrorKind::NotAuthorized, - msg: err.error_description, - }) - } else { - Err(IndieAuthError { - source: None, - kind: ErrorKind::TokenEndpointError, - msg: err.error_description, - }) - } - } - Err(err) => Err(IndieAuthError::from(err)), - }, - _ => Err(IndieAuthError { - source: None, - msg: format!("Token endpoint returned {}", res.status()), - kind: ErrorKind::TokenEndpointError, - }), - }, - Err(err) => Err(IndieAuthError::from(err)), - } - } -} - -#[cfg(test)] -mod tests { - use super::User; - use axum::{ - extract::FromRequest, - http::{Method, Request}, - }; - use httpmock::prelude::*; - - #[test] - fn user_scopes_are_checkable() { - let user = User::new( - "https://fireburn.ru/", - "https://quill.p3k.io/", - "create update media", - ); - - assert!(user.check_scope("create")); - assert!(!user.check_scope("delete")); - } - - #[inline] - fn get_http_client() -> reqwest::Client { - reqwest::Client::new() - } - - fn request>, T: TryInto + std::fmt::Debug>( - auth: A, - endpoint: T, - ) -> Request<()> - where - >::Error: std::fmt::Debug, - { - let request = Request::builder().method(Method::GET); - - match auth.into() { - Some(auth) => request.header("Authorization", auth), - None => request, - } - .extension(super::TokenEndpoint(endpoint.try_into().unwrap())) - .extension(get_http_client()) - .body(()) - .unwrap() - } - - #[tokio::test] - async fn test_require_token_with_token() { - let server = MockServer::start_async().await; - server - .mock_async(|when, then| { - when.path("/token").header("Authorization", "Bearer token"); - - then.status(200) - .header("Content-Type", "application/json") - .json_body( - serde_json::to_value(User::new( - "https://fireburn.ru/", - "https://quill.p3k.io/", - "create update media", - )) - .unwrap(), - ); - }) - .await; - - let request = request("Bearer token", server.url("/token").as_str()); - let mut parts = axum::extract::RequestParts::new(request); - let user = User::from_request(&mut parts).await.unwrap(); - - assert_eq!(user.me.as_str(), "https://fireburn.ru/") - } - - #[tokio::test] - async fn test_require_token_fake_token() { - let server = MockServer::start_async().await; - server - .mock_async(|when, then| { - when.path("/refuse_token"); - - then.status(200) - .json_body(serde_json::json!({"active": false})); - }) - .await; - - let request = request("Bearer token", server.url("/refuse_token").as_str()); - let mut parts = axum::extract::RequestParts::new(request); - let err = User::from_request(&mut parts).await.unwrap_err(); - - assert_eq!(err.kind, super::ErrorKind::NotAuthorized) - } - - #[tokio::test] - async fn test_require_token_no_token() { - let server = MockServer::start_async().await; - let mock = server - .mock_async(|when, then| { - when.path("/should_never_be_called"); - - then.status(500); - }) - .await; - - let request = request(None, server.url("/should_never_be_called").as_str()); - let mut parts = axum::extract::RequestParts::new(request); - let err = User::from_request(&mut parts).await.unwrap_err(); - - assert_eq!(err.kind, super::ErrorKind::InvalidHeader); - - mock.assert_hits_async(0).await; - } - - #[tokio::test] - async fn test_require_token_400_error_unauthorized() { - let server = MockServer::start_async().await; - server - .mock_async(|when, then| { - when.path("/refuse_token_with_400"); - - then.status(400).json_body(serde_json::json!({ - "error": "unauthorized", - "error_description": "The token provided was malformed" - })); - }) - .await; - - let request = request( - "Bearer token", - server.url("/refuse_token_with_400").as_str(), - ); - let mut parts = axum::extract::RequestParts::new(request); - let err = User::from_request(&mut parts).await.unwrap_err(); - - assert_eq!(err.kind, super::ErrorKind::NotAuthorized); - } -} diff --git a/kittybox-rs/src/lib.rs b/kittybox-rs/src/lib.rs index 84e9d60..c1683d9 100644 --- a/kittybox-rs/src/lib.rs +++ b/kittybox-rs/src/lib.rs @@ -4,7 +4,7 @@ /// Database abstraction layer for Kittybox, allowing the CMS to work with any kind of database. pub mod database; pub mod frontend; -pub mod indieauth; +pub mod tokenauth; pub mod media; pub mod micropub; diff --git a/kittybox-rs/src/main.rs b/kittybox-rs/src/main.rs index 6cc8c1d..50c0ca5 100644 --- a/kittybox-rs/src/main.rs +++ b/kittybox-rs/src/main.rs @@ -181,7 +181,7 @@ async fn main() { )) .layer(axum::Extension(database)) .layer(axum::Extension(http)) - .layer(axum::Extension(kittybox::indieauth::TokenEndpoint( + .layer(axum::Extension(kittybox::tokenauth::TokenEndpoint( token_endpoint, ))) .layer(axum::Extension(blobstore)) diff --git a/kittybox-rs/src/media/mod.rs b/kittybox-rs/src/media/mod.rs index 1bf3958..e9e91ac 100644 --- a/kittybox-rs/src/media/mod.rs +++ b/kittybox-rs/src/media/mod.rs @@ -2,7 +2,7 @@ use axum::{ extract::{Extension, Host, multipart::{Multipart, MultipartError}, Path}, response::{IntoResponse, Response}, headers::HeaderValue, }; -use crate::{micropub::{MicropubError, ErrorType}, indieauth::User}; +use crate::{micropub::{MicropubError, ErrorType}, tokenauth::User}; pub mod storage; use storage::{MediaStore, MediaStoreError, Metadata, ErrorKind}; diff --git a/kittybox-rs/src/micropub/mod.rs b/kittybox-rs/src/micropub/mod.rs index 8550849..1fa442a 100644 --- a/kittybox-rs/src/micropub/mod.rs +++ b/kittybox-rs/src/micropub/mod.rs @@ -1,5 +1,5 @@ use crate::database::{MicropubChannel, Storage, StorageError}; -use crate::indieauth::User; +use crate::tokenauth::User; use crate::micropub::util::form_to_mf2_json; use axum::extract::{BodyStream, Query}; use axum::headers::ContentType; @@ -293,7 +293,7 @@ async fn background_processing( // TODO actually save the post to the database and schedule post-processing pub(crate) async fn _post( - user: crate::indieauth::User, + user: User, uid: String, mf2: serde_json::Value, db: D, @@ -681,7 +681,7 @@ impl MicropubQuery { #[cfg(test)] mod tests { - use crate::{database::Storage, micropub::MicropubError}; + use crate::{database::Storage, micropub::MicropubError, tokenauth::User}; use hyper::body::HttpBody; use serde_json::json; @@ -734,7 +734,7 @@ mod tests { "content": ["Hello world!"] } }); - let user = crate::indieauth::User::new( + let user = User::new( "https://localhost:8080/", "https://kittybox.fireburn.ru/", "profile", @@ -763,7 +763,7 @@ mod tests { "url": ["https://fireburn.ru/posts/hello"] } }); - let user = crate::indieauth::User::new( + let user = User::new( "https://aaronparecki.com/", "https://kittybox.fireburn.ru/", "create update media", @@ -790,7 +790,7 @@ mod tests { "content": ["Hello world!"] } }); - let user = crate::indieauth::User::new( + let user = User::new( "https://localhost:8080/", "https://kittybox.fireburn.ru/", "create", @@ -817,7 +817,7 @@ mod tests { axum::extract::Query(super::MicropubQuery::source( "https://aaronparecki.com/feeds/main", )), - crate::indieauth::User::new( + User::new( "https://fireburn.ru/", "https://quill.p3k.io/", "create update media", diff --git a/kittybox-rs/src/micropub/util.rs b/kittybox-rs/src/micropub/util.rs index 97ec09a..7c6a0b1 100644 --- a/kittybox-rs/src/micropub/util.rs +++ b/kittybox-rs/src/micropub/util.rs @@ -1,5 +1,5 @@ use crate::database::Storage; -use crate::indieauth::User; +use crate::tokenauth::User; use chrono::prelude::*; use core::iter::Iterator; use newbase60::num_to_sxg; diff --git a/kittybox-rs/src/tokenauth.rs b/kittybox-rs/src/tokenauth.rs new file mode 100644 index 0000000..103f514 --- /dev/null +++ b/kittybox-rs/src/tokenauth.rs @@ -0,0 +1,367 @@ +use serde::{Deserialize, Serialize}; +use url::Url; + +#[derive(Deserialize, Serialize, Debug, PartialEq, Clone)] +pub struct User { + pub me: Url, + pub client_id: Url, + scope: String, +} + +#[derive(Debug, Clone, PartialEq, Copy)] +pub enum ErrorKind { + PermissionDenied, + NotAuthorized, + TokenEndpointError, + JsonParsing, + InvalidHeader, + Other, +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +pub struct TokenEndpointError { + error: String, + error_description: String, +} + +#[derive(Debug)] +pub struct IndieAuthError { + source: Option>, + kind: ErrorKind, + msg: String, +} + +impl std::error::Error for IndieAuthError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + self.source + .as_ref() + .map(|e| e.as_ref() as &dyn std::error::Error) + } +} + +impl std::fmt::Display for IndieAuthError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}: {}", + match self.kind { + ErrorKind::TokenEndpointError => "token endpoint returned an error: ", + ErrorKind::JsonParsing => "error while parsing token endpoint response: ", + ErrorKind::NotAuthorized => "token endpoint did not recognize the token: ", + ErrorKind::PermissionDenied => "token endpoint rejected the token: ", + ErrorKind::InvalidHeader => "authorization header parsing error: ", + ErrorKind::Other => "token endpoint communication error: ", + }, + self.msg + ) + } +} + +impl From for IndieAuthError { + fn from(err: serde_json::Error) -> Self { + Self { + msg: format!("{}", err), + source: Some(Box::new(err)), + kind: ErrorKind::JsonParsing, + } + } +} + +impl From for IndieAuthError { + fn from(err: reqwest::Error) -> Self { + Self { + msg: format!("{}", err), + source: Some(Box::new(err)), + kind: ErrorKind::Other, + } + } +} + +impl From for IndieAuthError { + fn from(err: axum::extract::rejection::TypedHeaderRejection) -> Self { + Self { + msg: format!("{:?}", err.reason()), + source: Some(Box::new(err)), + kind: ErrorKind::InvalidHeader, + } + } +} + +impl axum::response::IntoResponse for IndieAuthError { + fn into_response(self) -> axum::response::Response { + let status_code: StatusCode = match self.kind { + ErrorKind::PermissionDenied => StatusCode::FORBIDDEN, + ErrorKind::NotAuthorized => StatusCode::UNAUTHORIZED, + ErrorKind::TokenEndpointError => StatusCode::INTERNAL_SERVER_ERROR, + ErrorKind::JsonParsing => StatusCode::BAD_REQUEST, + ErrorKind::InvalidHeader => StatusCode::UNAUTHORIZED, + ErrorKind::Other => StatusCode::INTERNAL_SERVER_ERROR, + }; + + let body = serde_json::json!({ + "error": match self.kind { + ErrorKind::PermissionDenied => "forbidden", + ErrorKind::NotAuthorized => "unauthorized", + ErrorKind::TokenEndpointError => "token_endpoint_error", + ErrorKind::JsonParsing => "invalid_request", + ErrorKind::InvalidHeader => "unauthorized", + ErrorKind::Other => "unknown_error", + }, + "error_description": self.msg + }); + + (status_code, axum::response::Json(body)).into_response() + } +} + +impl User { + pub fn check_scope(&self, scope: &str) -> bool { + self.scopes().any(|i| i == scope) + } + pub fn scopes(&self) -> std::str::SplitAsciiWhitespace<'_> { + self.scope.split_ascii_whitespace() + } + pub fn new(me: &str, client_id: &str, scope: &str) -> Self { + Self { + me: Url::parse(me).unwrap(), + client_id: Url::parse(client_id).unwrap(), + scope: scope.to_string(), + } + } +} + +use axum::{ + extract::{Extension, FromRequest, RequestParts, TypedHeader}, + headers::{ + authorization::{Bearer, Credentials}, + Authorization, + }, + http::StatusCode, +}; + +// this newtype is required due to axum::Extension retrieving items by type +// it's based on compiler magic matching extensions by their type's hashes +#[derive(Debug, Clone)] +pub struct TokenEndpoint(pub url::Url); + +#[async_trait::async_trait] +impl FromRequest for User +where + B: Send, +{ + type Rejection = IndieAuthError; + + #[cfg_attr( + all(debug_assertions, not(test)), + allow(unreachable_code, unused_variables) + )] + async fn from_request(req: &mut RequestParts) -> Result { + // Return a fake user if we're running a debug build + // I don't wanna bother with authentication + #[cfg(all(debug_assertions, not(test)))] + return Ok(User::new( + "http://localhost:8080/", + "https://quill.p3k.io/", + "create update delete media", + )); + + let TypedHeader(Authorization(token)) = + TypedHeader::>::from_request(req) + .await + .map_err(IndieAuthError::from)?; + + let Extension(TokenEndpoint(token_endpoint)): Extension = + Extension::from_request(req).await.unwrap(); + + let Extension(http): Extension = + Extension::from_request(req).await.unwrap(); + + match http + .get(token_endpoint) + .header("Authorization", token.encode()) + .header("Accept", "application/json") + .send() + .await + { + Ok(res) => match res.status() { + StatusCode::OK => match res.json::().await { + Ok(json) => match serde_json::from_value::(json.clone()) { + Ok(user) => Ok(user), + Err(err) => { + if let Some(false) = json["active"].as_bool() { + Err(IndieAuthError { + source: None, + kind: ErrorKind::NotAuthorized, + msg: "The token is not active for this user.".to_owned(), + }) + } else { + Err(IndieAuthError::from(err)) + } + } + }, + Err(err) => Err(IndieAuthError::from(err)), + }, + StatusCode::BAD_REQUEST => match res.json::().await { + Ok(err) => { + if err.error == "unauthorized" { + Err(IndieAuthError { + source: None, + kind: ErrorKind::NotAuthorized, + msg: err.error_description, + }) + } else { + Err(IndieAuthError { + source: None, + kind: ErrorKind::TokenEndpointError, + msg: err.error_description, + }) + } + } + Err(err) => Err(IndieAuthError::from(err)), + }, + _ => Err(IndieAuthError { + source: None, + msg: format!("Token endpoint returned {}", res.status()), + kind: ErrorKind::TokenEndpointError, + }), + }, + Err(err) => Err(IndieAuthError::from(err)), + } + } +} + +#[cfg(test)] +mod tests { + use super::User; + use axum::{ + extract::FromRequest, + http::{Method, Request}, + }; + use httpmock::prelude::*; + + #[test] + fn user_scopes_are_checkable() { + let user = User::new( + "https://fireburn.ru/", + "https://quill.p3k.io/", + "create update media", + ); + + assert!(user.check_scope("create")); + assert!(!user.check_scope("delete")); + } + + #[inline] + fn get_http_client() -> reqwest::Client { + reqwest::Client::new() + } + + fn request>, T: TryInto + std::fmt::Debug>( + auth: A, + endpoint: T, + ) -> Request<()> + where + >::Error: std::fmt::Debug, + { + let request = Request::builder().method(Method::GET); + + match auth.into() { + Some(auth) => request.header("Authorization", auth), + None => request, + } + .extension(super::TokenEndpoint(endpoint.try_into().unwrap())) + .extension(get_http_client()) + .body(()) + .unwrap() + } + + #[tokio::test] + async fn test_require_token_with_token() { + let server = MockServer::start_async().await; + server + .mock_async(|when, then| { + when.path("/token").header("Authorization", "Bearer token"); + + then.status(200) + .header("Content-Type", "application/json") + .json_body( + serde_json::to_value(User::new( + "https://fireburn.ru/", + "https://quill.p3k.io/", + "create update media", + )) + .unwrap(), + ); + }) + .await; + + let request = request("Bearer token", server.url("/token").as_str()); + let mut parts = axum::extract::RequestParts::new(request); + let user = User::from_request(&mut parts).await.unwrap(); + + assert_eq!(user.me.as_str(), "https://fireburn.ru/") + } + + #[tokio::test] + async fn test_require_token_fake_token() { + let server = MockServer::start_async().await; + server + .mock_async(|when, then| { + when.path("/refuse_token"); + + then.status(200) + .json_body(serde_json::json!({"active": false})); + }) + .await; + + let request = request("Bearer token", server.url("/refuse_token").as_str()); + let mut parts = axum::extract::RequestParts::new(request); + let err = User::from_request(&mut parts).await.unwrap_err(); + + assert_eq!(err.kind, super::ErrorKind::NotAuthorized) + } + + #[tokio::test] + async fn test_require_token_no_token() { + let server = MockServer::start_async().await; + let mock = server + .mock_async(|when, then| { + when.path("/should_never_be_called"); + + then.status(500); + }) + .await; + + let request = request(None, server.url("/should_never_be_called").as_str()); + let mut parts = axum::extract::RequestParts::new(request); + let err = User::from_request(&mut parts).await.unwrap_err(); + + assert_eq!(err.kind, super::ErrorKind::InvalidHeader); + + mock.assert_hits_async(0).await; + } + + #[tokio::test] + async fn test_require_token_400_error_unauthorized() { + let server = MockServer::start_async().await; + server + .mock_async(|when, then| { + when.path("/refuse_token_with_400"); + + then.status(400).json_body(serde_json::json!({ + "error": "unauthorized", + "error_description": "The token provided was malformed" + })); + }) + .await; + + let request = request( + "Bearer token", + server.url("/refuse_token_with_400").as_str(), + ); + let mut parts = axum::extract::RequestParts::new(request); + let err = User::from_request(&mut parts).await.unwrap_err(); + + assert_eq!(err.kind, super::ErrorKind::NotAuthorized); + } +} -- cgit 1.4.1