about summary refs log tree commit diff
path: root/src/micropub/mod.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/micropub/mod.rs')
-rw-r--r--src/micropub/mod.rs326
1 files changed, 195 insertions, 131 deletions
diff --git a/src/micropub/mod.rs b/src/micropub/mod.rs
index 8505ae5..5e11033 100644
--- a/src/micropub/mod.rs
+++ b/src/micropub/mod.rs
@@ -1,26 +1,26 @@
 use std::collections::HashMap;
+use std::sync::Arc;
 use url::Url;
 use util::NormalizedPost;
-use std::sync::Arc;
 
 use crate::database::{MicropubChannel, Storage, StorageError};
 use crate::indieauth::backend::AuthBackend;
 use crate::indieauth::User;
 use crate::micropub::util::form_to_mf2_json;
-use axum::extract::{FromRef, Query, State};
 use axum::body::Body as BodyStream;
+use axum::extract::{FromRef, Query, State};
+use axum::http::StatusCode;
+use axum::response::{IntoResponse, Response};
 use axum_extra::extract::Host;
 use axum_extra::headers::ContentType;
-use axum::response::{IntoResponse, Response};
 use axum_extra::TypedHeader;
-use axum::http::StatusCode;
+use kittybox_indieauth::{Scope, TokenData};
+use kittybox_util::micropub::{Error as MicropubError, ErrorKind, QueryType};
 use serde::{Deserialize, Serialize};
 use serde_json::json;
 use tokio::sync::Mutex;
 use tokio::task::JoinSet;
 use tracing::{debug, error, info, warn};
-use kittybox_indieauth::{Scope, TokenData};
-use kittybox_util::micropub::{Error as MicropubError, ErrorKind, QueryType};
 
 #[derive(Serialize, Deserialize, Debug)]
 pub struct MicropubQuery {
@@ -35,7 +35,7 @@ impl From<StorageError> for MicropubError {
                 crate::database::ErrorKind::NotFound => ErrorKind::NotFound,
                 _ => ErrorKind::InternalServerError,
             },
-            format!("backend error: {}", err)
+            format!("backend error: {}", err),
         )
     }
 }
@@ -59,7 +59,8 @@ fn populate_reply_context(
         array
             .iter()
             .map(|i| {
-                let mut item = i.as_str()
+                let mut item = i
+                    .as_str()
                     .and_then(|i| i.parse::<Url>().ok())
                     .and_then(|url| ctxs.get(&url))
                     .and_then(|ctx| ctx.mf2["items"].get(0))
@@ -69,7 +70,12 @@ fn populate_reply_context(
                 if item.is_object() && (i != &item) {
                     if let Some(props) = item["properties"].as_object_mut() {
                         // Fixup the item: if it lacks a URL, add one.
-                        if !props.get("url").and_then(serde_json::Value::as_array).map(|a| !a.is_empty()).unwrap_or(false) {
+                        if !props
+                            .get("url")
+                            .and_then(serde_json::Value::as_array)
+                            .map(|a| !a.is_empty())
+                            .unwrap_or(false)
+                        {
                             props.insert("url".to_owned(), json!([i.as_str()]));
                         }
                     }
@@ -145,11 +151,14 @@ async fn background_processing<D: 'static + Storage>(
                     .get("webmention")
                     .and_then(|i| i.first().cloned());
 
-                dbg!(Some((url.clone(), FetchedPostContext {
-                    url,
-                    mf2: serde_json::to_value(mf2).unwrap(),
-                    webmention
-                })))
+                dbg!(Some((
+                    url.clone(),
+                    FetchedPostContext {
+                        url,
+                        mf2: serde_json::to_value(mf2).unwrap(),
+                        webmention
+                    }
+                )))
             })
             .collect::<HashMap<Url, FetchedPostContext>>()
             .await
@@ -161,7 +170,11 @@ async fn background_processing<D: 'static + Storage>(
     };
     for prop in context_props {
         if let Some(json) = populate_reply_context(&mf2, prop, &post_contexts) {
-            update.replace.as_mut().unwrap().insert(prop.to_owned(), json);
+            update
+                .replace
+                .as_mut()
+                .unwrap()
+                .insert(prop.to_owned(), json);
         }
     }
     if !update.replace.as_ref().unwrap().is_empty() {
@@ -250,7 +263,7 @@ pub(crate) async fn _post<D: 'static + Storage>(
     if !user.check_scope(&Scope::Create) {
         return Err(MicropubError::from_static(
             ErrorKind::InvalidScope,
-            "Not enough privileges - try acquiring the \"create\" scope."
+            "Not enough privileges - try acquiring the \"create\" scope.",
         ));
     }
 
@@ -264,7 +277,7 @@ pub(crate) async fn _post<D: 'static + Storage>(
     {
         return Err(MicropubError::from_static(
             ErrorKind::Forbidden,
-            "You're posting to a website that's not yours."
+            "You're posting to a website that's not yours.",
         ));
     }
 
@@ -272,7 +285,7 @@ pub(crate) async fn _post<D: 'static + Storage>(
     if db.post_exists(&uid).await? {
         return Err(MicropubError::from_static(
             ErrorKind::AlreadyExists,
-            "UID clash was detected, operation aborted."
+            "UID clash was detected, operation aborted.",
         ));
     }
     // Save the post
@@ -309,13 +322,18 @@ pub(crate) async fn _post<D: 'static + Storage>(
         }
     }
 
-    let reply =
-        IntoResponse::into_response((StatusCode::ACCEPTED, [("Location", uid.as_str())]));
+    let reply = IntoResponse::into_response((StatusCode::ACCEPTED, [("Location", uid.as_str())]));
 
     #[cfg(not(tokio_unstable))]
-    let _ = jobset.lock().await.spawn(background_processing(db, mf2, http));
+    let _ = jobset
+        .lock()
+        .await
+        .spawn(background_processing(db, mf2, http));
     #[cfg(tokio_unstable)]
-    let _ = jobset.lock().await.build_task()
+    let _ = jobset
+        .lock()
+        .await
+        .build_task()
         .name(format!("Kittybox background processing for post {}", uid.as_str()).as_str())
         .spawn(background_processing(db, mf2, http));
 
@@ -333,7 +351,7 @@ enum ActionType {
 #[serde(untagged)]
 pub enum MicropubPropertyDeletion {
     Properties(Vec<String>),
-    Values(HashMap<String, Vec<serde_json::Value>>)
+    Values(HashMap<String, Vec<serde_json::Value>>),
 }
 #[derive(Serialize, Deserialize)]
 struct MicropubFormAction {
@@ -347,7 +365,7 @@ pub struct MicropubAction {
     url: String,
     #[serde(flatten)]
     #[serde(skip_serializing_if = "Option::is_none")]
-    update: Option<MicropubUpdate>
+    update: Option<MicropubUpdate>,
 }
 
 #[derive(Serialize, Deserialize, Debug, Default)]
@@ -362,39 +380,43 @@ pub struct MicropubUpdate {
 impl MicropubUpdate {
     pub fn check_validity(&self) -> Result<(), MicropubError> {
         if let Some(add) = &self.add {
-            if add.iter().map(|(k, _)| k.as_str()).any(|k| {
-                k.to_lowercase().as_str() == "uid"
-            }) {
+            if add
+                .iter()
+                .map(|(k, _)| k.as_str())
+                .any(|k| k.to_lowercase().as_str() == "uid")
+            {
                 return Err(MicropubError::from_static(
                     ErrorKind::InvalidRequest,
-                    "Update cannot modify the post UID"
+                    "Update cannot modify the post UID",
                 ));
             }
         }
         if let Some(replace) = &self.replace {
-            if replace.iter().map(|(k, _)| k.as_str()).any(|k| {
-                k.to_lowercase().as_str() == "uid"
-            }) {
+            if replace
+                .iter()
+                .map(|(k, _)| k.as_str())
+                .any(|k| k.to_lowercase().as_str() == "uid")
+            {
                 return Err(MicropubError::from_static(
                     ErrorKind::InvalidRequest,
-                    "Update cannot modify the post UID"
+                    "Update cannot modify the post UID",
                 ));
             }
         }
         let iter = match &self.delete {
             Some(MicropubPropertyDeletion::Properties(keys)) => {
                 Some(Box::new(keys.iter().map(|k| k.as_str())) as Box<dyn Iterator<Item = &str>>)
-            },
+            }
             Some(MicropubPropertyDeletion::Values(map)) => {
                 Some(Box::new(map.iter().map(|(k, _)| k.as_str())) as Box<dyn Iterator<Item = &str>>)
-            },
+            }
             None => None,
         };
         if let Some(mut iter) = iter {
             if iter.any(|k| k.to_lowercase().as_str() == "uid") {
                 return Err(MicropubError::from_static(
                     ErrorKind::InvalidRequest,
-                    "Update cannot modify the post UID"
+                    "Update cannot modify the post UID",
                 ));
             }
         }
@@ -412,8 +434,9 @@ impl MicropubUpdate {
         } else if let Some(MicropubPropertyDeletion::Values(ref delete)) = self.delete {
             if let Some(props) = post["properties"].as_object_mut() {
                 for (key, values) in delete {
-                    if let Some(prop) = props.get_mut(key).and_then(serde_json::Value::as_array_mut) {
-                        prop.retain(|v| { values.iter().all(|i| i != v) })
+                    if let Some(prop) = props.get_mut(key).and_then(serde_json::Value::as_array_mut)
+                    {
+                        prop.retain(|v| values.iter().all(|i| i != v))
                     }
                 }
             }
@@ -428,7 +451,10 @@ impl MicropubUpdate {
         if let Some(add) = self.add {
             if let Some(props) = post["properties"].as_object_mut() {
                 for (key, value) in add {
-                    if let Some(prop) = props.get_mut(&key).and_then(serde_json::Value::as_array_mut) {
+                    if let Some(prop) = props
+                        .get_mut(&key)
+                        .and_then(serde_json::Value::as_array_mut)
+                    {
                         prop.extend_from_slice(value.as_slice());
                     } else {
                         props.insert(key, serde_json::Value::Array(value));
@@ -445,7 +471,7 @@ impl From<MicropubFormAction> for MicropubAction {
         Self {
             action: a.action,
             url: a.url,
-            update: None
+            update: None,
         }
     }
 }
@@ -458,10 +484,12 @@ async fn post_action<D: Storage, A: AuthBackend>(
 ) -> Result<(), MicropubError> {
     let uri = match action.url.parse::<hyper::Uri>() {
         Ok(uri) => uri,
-        Err(err) => return Err(MicropubError::new(
-            ErrorKind::InvalidRequest,
-            format!("url parsing error: {}", err)
-        ))
+        Err(err) => {
+            return Err(MicropubError::new(
+                ErrorKind::InvalidRequest,
+                format!("url parsing error: {}", err),
+            ))
+        }
     };
 
     if uri.authority().unwrap()
@@ -475,7 +503,7 @@ async fn post_action<D: Storage, A: AuthBackend>(
     {
         return Err(MicropubError::from_static(
             ErrorKind::Forbidden,
-            "Don't tamper with others' posts!"
+            "Don't tamper with others' posts!",
         ));
     }
 
@@ -484,7 +512,7 @@ async fn post_action<D: Storage, A: AuthBackend>(
             if !user.check_scope(&Scope::Delete) {
                 return Err(MicropubError::from_static(
                     ErrorKind::InvalidScope,
-                    "You need a \"delete\" scope for this."
+                    "You need a \"delete\" scope for this.",
                 ));
             }
 
@@ -494,7 +522,7 @@ async fn post_action<D: Storage, A: AuthBackend>(
             if !user.check_scope(&Scope::Update) {
                 return Err(MicropubError::from_static(
                     ErrorKind::InvalidScope,
-                    "You need an \"update\" scope for this."
+                    "You need an \"update\" scope for this.",
                 ));
             }
 
@@ -503,7 +531,7 @@ async fn post_action<D: Storage, A: AuthBackend>(
             } else {
                 return Err(MicropubError::from_static(
                     ErrorKind::InvalidRequest,
-                    "Update request is not set."
+                    "Update request is not set.",
                 ));
             };
 
@@ -555,7 +583,7 @@ async fn dispatch_body(
         } else {
             Err(MicropubError::from_static(
                 ErrorKind::InvalidRequest,
-                "Invalid JSON object passed."
+                "Invalid JSON object passed.",
             ))
         }
     } else if content_type == ContentType::form_url_encoded() {
@@ -566,7 +594,7 @@ async fn dispatch_body(
         } else {
             Err(MicropubError::from_static(
                 ErrorKind::InvalidRequest,
-                "Invalid form-encoded data. Try h=entry&content=Hello!"
+                "Invalid form-encoded data. Try h=entry&content=Hello!",
             ))
         }
     } else {
@@ -605,7 +633,10 @@ pub(crate) async fn post<D: Storage + 'static, A: AuthBackend>(
 #[tracing::instrument(skip(db))]
 pub(crate) async fn query<D: Storage, A: AuthBackend>(
     State(db): State<D>,
-    query: Result<Query<MicropubQuery>, <Query<MicropubQuery> as axum::extract::FromRequestParts<()>>::Rejection>,
+    query: Result<
+        Query<MicropubQuery>,
+        <Query<MicropubQuery> as axum::extract::FromRequestParts<()>>::Rejection,
+    >,
     Host(host): Host,
     user: User<A>,
 ) -> axum::response::Response {
@@ -616,8 +647,9 @@ pub(crate) async fn query<D: Storage, A: AuthBackend>(
     } else {
         return MicropubError::from_static(
             ErrorKind::InvalidRequest,
-            "Invalid query provided. Try ?q=config to see what you can do."
-        ).into_response();
+            "Invalid query provided. Try ?q=config to see what you can do.",
+        )
+        .into_response();
     };
 
     if axum::http::Uri::try_from(user.me.as_str())
@@ -630,7 +662,7 @@ pub(crate) async fn query<D: Storage, A: AuthBackend>(
             ErrorKind::NotAuthorized,
             "This website doesn't belong to you.",
         )
-            .into_response();
+        .into_response();
     }
 
     // TODO: consider replacing by `user.me.authority()`?
@@ -644,7 +676,7 @@ pub(crate) async fn query<D: Storage, A: AuthBackend>(
                         ErrorKind::InternalServerError,
                         format!("Error fetching channels: {}", err),
                     )
-                        .into_response()
+                    .into_response()
                 }
             };
 
@@ -654,35 +686,36 @@ pub(crate) async fn query<D: Storage, A: AuthBackend>(
                     QueryType::Config,
                     QueryType::Channel,
                     QueryType::SyndicateTo,
-                    QueryType::Category
+                    QueryType::Category,
                 ],
                 channels: Some(channels),
                 syndicate_to: None,
                 media_endpoint: Some(user.me.join("/.kittybox/media").unwrap()),
                 other: {
                     let mut map = std::collections::HashMap::new();
-                    map.insert("kittybox_authority".to_string(), serde_json::Value::String(user.me.to_string()));
+                    map.insert(
+                        "kittybox_authority".to_string(),
+                        serde_json::Value::String(user.me.to_string()),
+                    );
 
                     map
-                }
+                },
             })
-                .into_response()
+            .into_response()
         }
         QueryType::Source => {
             match query.url {
-                Some(url) => {
-                    match db.get_post(&url).await {
-                        Ok(some) => match some {
-                            Some(post) => axum::response::Json(&post).into_response(),
-                            None => MicropubError::from_static(
-                                ErrorKind::NotFound,
-                                "The specified MF2 object was not found in database.",
-                            )
-                                .into_response(),
-                        },
-                        Err(err) => MicropubError::from(err).into_response(),
-                    }
-                }
+                Some(url) => match db.get_post(&url).await {
+                    Ok(some) => match some {
+                        Some(post) => axum::response::Json(&post).into_response(),
+                        None => MicropubError::from_static(
+                            ErrorKind::NotFound,
+                            "The specified MF2 object was not found in database.",
+                        )
+                        .into_response(),
+                    },
+                    Err(err) => MicropubError::from(err).into_response(),
+                },
                 None => {
                     // Here, one should probably attempt to query at least the main feed and collect posts
                     // Using a pre-made query function can't be done because it does unneeded filtering
@@ -691,7 +724,7 @@ pub(crate) async fn query<D: Storage, A: AuthBackend>(
                         ErrorKind::InvalidRequest,
                         "Querying for post list is not implemented yet.",
                     )
-                        .into_response()
+                    .into_response()
                 }
             }
         }
@@ -701,46 +734,45 @@ pub(crate) async fn query<D: Storage, A: AuthBackend>(
                 ErrorKind::InternalServerError,
                 format!("error fetching channels: backend error: {}", err),
             )
-                .into_response(),
+            .into_response(),
         },
         QueryType::SyndicateTo => {
             axum::response::Json(json!({ "syndicate-to": [] })).into_response()
-        },
+        }
         QueryType::Category => {
             let categories = match db.categories(user_domain).await {
                 Ok(categories) => categories,
                 Err(err) => {
                     return MicropubError::new(
                         ErrorKind::InternalServerError,
-                        format!("error fetching categories: backend error: {}", err)
-                    ).into_response()
+                        format!("error fetching categories: backend error: {}", err),
+                    )
+                    .into_response()
                 }
             };
             axum::response::Json(json!({ "categories": categories })).into_response()
-        },
-        QueryType::Unknown(q) => return MicropubError::new(
-            ErrorKind::InvalidRequest,
-            format!("Invalid query: {}", q)
-        ).into_response(),
+        }
+        QueryType::Unknown(q) => {
+            return MicropubError::new(ErrorKind::InvalidRequest, format!("Invalid query: {}", q))
+                .into_response()
+        }
     }
 }
 
-
 pub fn router<A, S, St: Send + Sync + Clone + 'static>() -> axum::routing::MethodRouter<St>
 where
     S: Storage + FromRef<St> + 'static,
     A: AuthBackend + FromRef<St>,
     reqwest_middleware::ClientWithMiddleware: FromRef<St>,
-    Arc<Mutex<JoinSet<()>>>: FromRef<St>
+    Arc<Mutex<JoinSet<()>>>: FromRef<St>,
 {
     axum::routing::get(query::<S, A>)
         .post(post::<S, A>)
-        .layer::<_, _>(tower_http::cors::CorsLayer::new()
-               .allow_methods([
-                   axum::http::Method::GET,
-                   axum::http::Method::POST,
-               ])
-            .allow_origin(tower_http::cors::Any))
+        .layer::<_, _>(
+            tower_http::cors::CorsLayer::new()
+                .allow_methods([axum::http::Method::GET, axum::http::Method::POST])
+                .allow_origin(tower_http::cors::Any),
+        )
 }
 
 #[cfg(test)]
@@ -765,16 +797,19 @@ impl MicropubQuery {
 mod tests {
     use std::sync::Arc;
 
-    use crate::{database::Storage, micropub::{util::NormalizedPost, MicropubError}};
+    use crate::{
+        database::Storage,
+        micropub::{util::NormalizedPost, MicropubError},
+    };
     use bytes::Bytes;
     use futures::StreamExt;
     use serde_json::json;
     use tokio::sync::Mutex;
 
     use super::FetchedPostContext;
-    use kittybox_indieauth::{Scopes, Scope, TokenData};
     use axum::extract::State;
     use axum_extra::extract::Host;
+    use kittybox_indieauth::{Scope, Scopes, TokenData};
 
     #[test]
     fn test_populate_reply_context() {
@@ -801,16 +836,27 @@ mod tests {
             }
         });
         let fetched_ctx_url: url::Url = "https://fireburn.ru/posts/example".parse().unwrap();
-        let reply_contexts = vec![(fetched_ctx_url.clone(), FetchedPostContext {
-            url: fetched_ctx_url.clone(),
-            mf2: json!({ "items": [test_ctx] }),
-            webmention: None,
-        })].into_iter().collect();
+        let reply_contexts = vec![(
+            fetched_ctx_url.clone(),
+            FetchedPostContext {
+                url: fetched_ctx_url.clone(),
+                mf2: json!({ "items": [test_ctx] }),
+                webmention: None,
+            },
+        )]
+        .into_iter()
+        .collect();
 
         let like_of = super::populate_reply_context(&mf2, "like-of", &reply_contexts).unwrap();
 
-        assert_eq!(like_of[0]["properties"]["content"], test_ctx["properties"]["content"]);
-        assert_eq!(like_of[0]["properties"]["url"][0].as_str().unwrap(), reply_contexts[&fetched_ctx_url].url.as_str());
+        assert_eq!(
+            like_of[0]["properties"]["content"],
+            test_ctx["properties"]["content"]
+        );
+        assert_eq!(
+            like_of[0]["properties"]["url"][0].as_str().unwrap(),
+            reply_contexts[&fetched_ctx_url].url.as_str()
+        );
 
         assert_eq!(like_of[1], already_expanded_reply_ctx);
         assert_eq!(like_of[2], "https://fireburn.ru/posts/non-existent");
@@ -830,20 +876,21 @@ mod tests {
             me: "https://localhost:8080/".parse().unwrap(),
             client_id: "https://kittybox.fireburn.ru/".parse().unwrap(),
             scope: Scopes::new(vec![Scope::Profile]),
-            iat: None, exp: None
+            iat: None,
+            exp: None,
         };
         let NormalizedPost { id, post } = super::normalize_mf2(post, &user);
 
         let err = super::_post(
-            &user, id, post, db.clone(),
-            reqwest_middleware::ClientWithMiddleware::new(
-                reqwest::Client::new(),
-                Box::default()
-            ),
-            Arc::new(Mutex::new(tokio::task::JoinSet::new()))
+            &user,
+            id,
+            post,
+            db.clone(),
+            reqwest_middleware::ClientWithMiddleware::new(reqwest::Client::new(), Box::default()),
+            Arc::new(Mutex::new(tokio::task::JoinSet::new())),
         )
-            .await
-            .unwrap_err();
+        .await
+        .unwrap_err();
 
         assert_eq!(err.error, super::ErrorKind::InvalidScope);
 
@@ -866,21 +913,27 @@ mod tests {
         let user = TokenData {
             me: "https://aaronparecki.com/".parse().unwrap(),
             client_id: "https://kittybox.fireburn.ru/".parse().unwrap(),
-            scope: Scopes::new(vec![Scope::Profile, Scope::Create, Scope::Update, Scope::Media]),
-            iat: None, exp: None
+            scope: Scopes::new(vec![
+                Scope::Profile,
+                Scope::Create,
+                Scope::Update,
+                Scope::Media,
+            ]),
+            iat: None,
+            exp: None,
         };
         let NormalizedPost { id, post } = super::normalize_mf2(post, &user);
 
         let err = super::_post(
-            &user, id, post, db.clone(),
-            reqwest_middleware::ClientWithMiddleware::new(
-                reqwest::Client::new(),
-                Box::default()
-            ),
-            Arc::new(Mutex::new(tokio::task::JoinSet::new()))
+            &user,
+            id,
+            post,
+            db.clone(),
+            reqwest_middleware::ClientWithMiddleware::new(reqwest::Client::new(), Box::default()),
+            Arc::new(Mutex::new(tokio::task::JoinSet::new())),
         )
-            .await
-            .unwrap_err();
+        .await
+        .unwrap_err();
 
         assert_eq!(err.error, super::ErrorKind::Forbidden);
 
@@ -902,20 +955,21 @@ mod tests {
             me: "https://localhost:8080/".parse().unwrap(),
             client_id: "https://kittybox.fireburn.ru/".parse().unwrap(),
             scope: Scopes::new(vec![Scope::Profile, Scope::Create]),
-            iat: None, exp: None
+            iat: None,
+            exp: None,
         };
         let NormalizedPost { id, post } = super::normalize_mf2(post, &user);
 
         let res = super::_post(
-            &user, id, post, db.clone(),
-            reqwest_middleware::ClientWithMiddleware::new(
-                reqwest::Client::new(),
-                Box::default()
-            ),
-            Arc::new(Mutex::new(tokio::task::JoinSet::new()))
+            &user,
+            id,
+            post,
+            db.clone(),
+            reqwest_middleware::ClientWithMiddleware::new(reqwest::Client::new(), Box::default()),
+            Arc::new(Mutex::new(tokio::task::JoinSet::new())),
         )
-            .await
-            .unwrap();
+        .await
+        .unwrap();
 
         assert!(res.headers().contains_key("Location"));
         let location = res.headers().get("Location").unwrap();
@@ -938,10 +992,17 @@ mod tests {
                 TokenData {
                     me: "https://fireburn.ru/".parse().unwrap(),
                     client_id: "https://kittybox.fireburn.ru/".parse().unwrap(),
-                    scope: Scopes::new(vec![Scope::Profile, Scope::Create, Scope::Update, Scope::Media]),
-                    iat: None, exp: None
-                }, std::marker::PhantomData
-            )
+                    scope: Scopes::new(vec![
+                        Scope::Profile,
+                        Scope::Create,
+                        Scope::Update,
+                        Scope::Media,
+                    ]),
+                    iat: None,
+                    exp: None,
+                },
+                std::marker::PhantomData,
+            ),
         )
         .await;
 
@@ -954,7 +1015,10 @@ mod tests {
             .into_iter()
             .map(Result::unwrap)
             .by_ref()
-            .fold(Vec::new(), |mut a, i| { a.extend(i); a});
+            .fold(Vec::new(), |mut a, i| {
+                a.extend(i);
+                a
+            });
         let json: MicropubError = serde_json::from_slice(&body as &[u8]).unwrap();
         assert_eq!(json.error, super::ErrorKind::NotAuthorized);
     }