From d08138ea633da76d9ca390e4f9e3c5489aee23d1 Mon Sep 17 00:00:00 2001 From: Harald Eilertsen Date: Mon, 5 Jul 2021 22:03:47 +0200 Subject: Update reqwest and make async. This means adding the full tokio as a dependency. While there isn't much gain to going async in the current cli demo app, a full fledged app may have more to gain by it. First foray into async rust, so I might not do it right... --- Cargo.toml | 3 +- src/abconfig.rs | 4 +- src/abook.rs | 4 +- src/bin/zot/main.rs | 19 ++++--- src/bin/zot/zot/abconfig.rs | 4 +- src/bin/zot/zot/abook.rs | 4 +- src/bin/zot/zot/channel_stream.rs | 4 +- src/bin/zot/zot/item.rs | 4 +- src/bin/zot/zot/network_stream.rs | 4 +- src/bin/zot/zot/xchan.rs | 8 +-- src/channel_stream.rs | 4 +- src/client.rs | 40 +++++++------- src/group.rs | 8 +-- src/item.rs | 21 ++++++-- src/network_stream.rs | 4 +- src/xchan.rs | 4 +- tests/zotapi.rs | 106 ++++++++++++++++++++------------------ 17 files changed, 135 insertions(+), 110 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 22565a9..de66e77 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,10 +23,11 @@ edition = "2018" [dependencies] clap = "2.33.0" dotenv = "0.15" -reqwest = "0.9.1" +reqwest = { version = "0.11", features = ["default-tls", "multipart"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" serde_qs = "0.8" +tokio = { version = "1.8", features = ["full"] } url = "2.2" [dev-dependencies] diff --git a/src/abconfig.rs b/src/abconfig.rs index 6348030..ebb7e2b 100644 --- a/src/abconfig.rs +++ b/src/abconfig.rs @@ -61,14 +61,14 @@ impl ABConfigRequest { self } - pub fn fetch(&self, client: &Client) -> Result, Error> { + pub async fn fetch(&self, client: &Client) -> Result, Error> { let mut req = client.get("abconfig"); if let Some(id) = self.abook_id { req = req.query(&[("abook_id", id.to_string())]); } - Ok(serde_json::from_str(&req.send()?.text()?)?) + Ok(serde_json::from_str(&req.send().await?.text().await?)?) } } diff --git a/src/abook.rs b/src/abook.rs index 59981ba..938a48f 100644 --- a/src/abook.rs +++ b/src/abook.rs @@ -114,13 +114,13 @@ impl AbookRequest { self } - pub fn fetch(&self, client: &Client) -> Result, Error> { + pub async fn fetch(&self, client: &Client) -> Result, Error> { let mut req = client.get("abook"); if let Some(id) = self.abook_id { req = req.query(&[("abook_id", id.to_string())]); } - Ok(serde_json::from_str(&req.send()?.text()?)?) + Ok(serde_json::from_str(&req.send().await?.text().await?)?) } } diff --git a/src/bin/zot/main.rs b/src/bin/zot/main.rs index 5828f44..09fdd61 100644 --- a/src/bin/zot/main.rs +++ b/src/bin/zot/main.rs @@ -22,7 +22,8 @@ use std::str::FromStr; mod zot; -fn main() { +#[tokio::main] +async fn main() { dotenv().ok(); let site = env::var("HZ_SITE").expect("SITE variable expected"); let user = env::var("HZ_USER").expect("USER variable expected"); @@ -79,24 +80,25 @@ fn main() { match matches.subcommand() { ("channel", Some(m)) => { let raw = m.is_present("raw"); - zot::channel_stream::fetch(&client, raw); + zot::channel_stream::fetch(&client, raw).await; } ("network", Some(m)) => { let raw = m.is_present("raw"); - zot::network_stream::fetch(&client, raw); + zot::network_stream::fetch(&client, raw).await; } ("abconfig", _) => { - zot::abconfig::fetch(&client); + zot::abconfig::fetch(&client).await; } ("abook", Some(m)) => { let raw = m.is_present("raw"); - zot::abook::fetch(&client, raw); + zot::abook::fetch(&client, raw).await; } ("group", Some(m)) => { if let Some(id) = m.value_of("ID") { let res = zotapi::group_members() .by_group_id(u64::from_str(id).unwrap()) .fetch(&client) + .await .unwrap(); if m.is_present("raw") { @@ -108,6 +110,7 @@ fn main() { let res = zotapi::group_members() .by_group_name(gname) .fetch(&client) + .await .unwrap(); if m.is_present("raw") { @@ -116,7 +119,7 @@ fn main() { zot::group::list_members(&res); } } else { - let res = zotapi::group().fetch(&client).unwrap(); + let res = zotapi::group().fetch(&client).await.unwrap(); if m.is_present("raw") { println!("{}", res); @@ -135,10 +138,10 @@ fn main() { zot::xchan::Type::Addr }; - zot::xchan::fetch(&client, raw, t, m.value_of("ID").unwrap()); + zot::xchan::fetch(&client, raw, t, m.value_of("ID").unwrap()).await; } ("post", Some(m)) => { - zot::item::post(&client, m); + zot::item::post(&client, m).await; } _ => { println!("{}", matches.usage()); diff --git a/src/bin/zot/zot/abconfig.rs b/src/bin/zot/zot/abconfig.rs index 2e357e4..14875c8 100644 --- a/src/bin/zot/zot/abconfig.rs +++ b/src/bin/zot/zot/abconfig.rs @@ -17,8 +17,8 @@ use zotapi; -pub fn fetch(client: &zotapi::Client) { - match zotapi::ABConfig::z().fetch(&client) { +pub async fn fetch(client: &zotapi::Client) { + match zotapi::ABConfig::z().fetch(&client).await { Ok(v) => { println!("Id: Chan: Cat: Key: Val: xchan:"); for entry in v { diff --git a/src/bin/zot/zot/abook.rs b/src/bin/zot/zot/abook.rs index 97025c3..6c33757 100644 --- a/src/bin/zot/zot/abook.rs +++ b/src/bin/zot/zot/abook.rs @@ -15,8 +15,8 @@ * along with this program. If not, see . */ -pub fn fetch(client: &zotapi::Client, _raw: bool) { - match zotapi::Abook::z().fetch(&client) { +pub async fn fetch(client: &zotapi::Client, _raw: bool) { + match zotapi::Abook::z().fetch(&client).await { Ok(abooks) => { for b in abooks { println!("{:?}", b); diff --git a/src/bin/zot/zot/channel_stream.rs b/src/bin/zot/zot/channel_stream.rs index 33ad7cf..68a5402 100644 --- a/src/bin/zot/zot/channel_stream.rs +++ b/src/bin/zot/zot/channel_stream.rs @@ -20,8 +20,8 @@ use zotapi; use serde_json; use std::iter::Iterator; -pub fn fetch(client: &zotapi::Client, raw: bool) { - match zotapi::channel_stream().fetch(&client) { +pub async fn fetch(client: &zotapi::Client, raw: bool) { + match zotapi::channel_stream().fetch(&client).await { Ok(payload) => { if raw { println!("{}", payload); diff --git a/src/bin/zot/zot/item.rs b/src/bin/zot/zot/item.rs index 648a9f0..dbb39ba 100644 --- a/src/bin/zot/zot/item.rs +++ b/src/bin/zot/zot/item.rs @@ -18,7 +18,7 @@ use clap::ArgMatches; use zotapi; -pub fn post(client: &zotapi::Client, args: &ArgMatches) { +pub async fn post(client: &zotapi::Client, args: &ArgMatches<'_>) { let mut msg = zotapi::item(); let body: String; @@ -42,7 +42,7 @@ pub fn post(client: &zotapi::Client, args: &ArgMatches) { } } - match msg.create(&client) { + match msg.create(&client).await { Ok(res) => { if res.success { println!("New item with id {} posted successfully!", res.item_id); diff --git a/src/bin/zot/zot/network_stream.rs b/src/bin/zot/zot/network_stream.rs index 0e6edb4..a5a7142 100644 --- a/src/bin/zot/zot/network_stream.rs +++ b/src/bin/zot/zot/network_stream.rs @@ -20,8 +20,8 @@ use zotapi; use serde_json; use std::iter::Iterator; -pub fn fetch(client: &zotapi::Client, raw: bool) { - match zotapi::network_stream().fetch(&client) { +pub async fn fetch(client: &zotapi::Client, raw: bool) { + match zotapi::network_stream().fetch(&client).await { Ok(payload) => { if raw { println!("{}", payload); diff --git a/src/bin/zot/zot/xchan.rs b/src/bin/zot/zot/xchan.rs index 088d49c..146b2ab 100644 --- a/src/bin/zot/zot/xchan.rs +++ b/src/bin/zot/zot/xchan.rs @@ -23,11 +23,11 @@ pub enum Type { GUID, } -pub fn fetch(client: &zotapi::Client, _raw: bool, t: Type, id: &str) { +pub async fn fetch(client: &zotapi::Client, _raw: bool, t: Type, id: &str) { let res = match t { - Type::Addr => zotapi::XChan::z().by_address(&id).fetch(&client), - Type::Hash => zotapi::XChan::z().by_hash(&id).fetch(&client), - Type::GUID => zotapi::XChan::z().by_guid(&id).fetch(&client), + Type::Addr => zotapi::XChan::z().by_address(&id).fetch(&client).await, + Type::Hash => zotapi::XChan::z().by_hash(&id).fetch(&client).await, + Type::GUID => zotapi::XChan::z().by_guid(&id).fetch(&client).await, }; match res { diff --git a/src/channel_stream.rs b/src/channel_stream.rs index 49f51e7..ed63f11 100644 --- a/src/channel_stream.rs +++ b/src/channel_stream.rs @@ -24,7 +24,7 @@ pub fn channel_stream() -> ChannelStream { } impl ChannelStream { - pub fn fetch(&self, client: &Client) -> Result { - client.fetch_stream("channel/stream", &()) + pub async fn fetch(&self, client: &Client) -> Result { + client.fetch_stream("channel/stream", &()).await } } diff --git a/src/client.rs b/src/client.rs index c681b27..3f43479 100644 --- a/src/client.rs +++ b/src/client.rs @@ -22,6 +22,7 @@ use reqwest::{ }; use serde::Serialize; use std::collections::BTreeMap; +use std::fs::File; use std::io::Read; use url::Url; @@ -56,22 +57,17 @@ impl Client { r.to_string() } - pub fn fetch_stream(&self, path: &str, args: &T) -> Result + pub async fn fetch_stream(&self, path: &str, args: &T) -> Result where T: Serialize + std::fmt::Debug, { let url = dbg!(self.url(path, args)); - let res = self - .inner - .get(&url) - .header(ACCEPT, "application/json") - .basic_auth(self.user.clone(), Some(self.pw.clone())) - .send()?; + let res = self.get(&url).send().await?; - handle_result(res) + handle_result(res).await } - pub fn post_data(&self, path: &str, data: &T) -> Result + pub async fn post_data(&self, path: &str, data: &T) -> Result where T: Serialize + std::fmt::Debug, { @@ -84,12 +80,12 @@ impl Client { .basic_auth(self.user.clone(), Some(self.pw.clone())) .body(serde_qs::to_string(&data)?)) //.form(&data)) - .send()?; + .send().await?; - handle_result(res) + handle_result(res).await } - pub fn post_multipart( + pub async fn post_multipart( &self, path: &str, data: &BTreeMap<&str, T>, @@ -106,7 +102,14 @@ impl Client { } for f in files.iter() { - form = form.file("media", f).unwrap(); + let mut pdata = vec![]; + let path = std::path::Path::new(f); + File::open(path)?.read_to_end(&mut pdata)?; + + let filename = String::from(path.file_name().unwrap().to_str().unwrap()); + let p = reqwest::multipart::Part::bytes(pdata) + .file_name(filename); + form = form.part("media", p); } let res = self @@ -114,9 +117,10 @@ impl Client { .post(&url) .basic_auth(self.user.clone(), Some(self.pw.clone())) .multipart(form) - .send()?; + .send() + .await?; - handle_result(res) + handle_result(res).await } /// Return a RequestBuilder object that's set up with the correct @@ -139,13 +143,11 @@ impl Client { // A common function for handling the response after a request. // // Consumes the response, and return it as a string or an error. -fn handle_result(mut res: reqwest::Response) -> Result { +async fn handle_result(res: reqwest::Response) -> Result { match res.status() { StatusCode::UNAUTHORIZED => Err(Error::Unauthorized), StatusCode::OK => { - let mut body = String::new(); - res.read_to_string(&mut body)?; - Ok(body) + Ok(res.text().await?) } _ => { eprintln!("Received unknown status: {:?}", res.status()); diff --git a/src/group.rs b/src/group.rs index 2ed8e92..cf3a057 100644 --- a/src/group.rs +++ b/src/group.rs @@ -24,8 +24,8 @@ pub fn group() -> Group { } impl Group { - pub fn fetch(&self, client: &Client) -> Result { - client.fetch_stream("group", &()) + pub async fn fetch(&self, client: &Client) -> Result { + client.fetch_stream("group", &()).await } } @@ -57,7 +57,7 @@ impl<'a> GroupMembers<'a> { self } - pub fn fetch(&self, client: &Client) -> Result { - client.fetch_stream("group_members", &self.id.as_ref().unwrap()) + pub async fn fetch(&self, client: &Client) -> Result { + client.fetch_stream("group_members", &self.id.as_ref().unwrap()).await } } diff --git a/src/item.rs b/src/item.rs index 7c5935d..d61401b 100644 --- a/src/item.rs +++ b/src/item.rs @@ -21,6 +21,8 @@ use reqwest::{ }; use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; +use std::fs::File; +use std::io::Read; /// Data type for values that an Item can hold. #[derive(Debug, Serialize)] @@ -81,13 +83,17 @@ pub struct ItemCreatedResponse { /// Typical usage: /// /// ```no_run +/// # async { /// let client = zotapi::client("https://myhub.com", "mychannel", "mypw"); /// let new_post = zotapi::item() /// .title("A title") /// .body("The body of the post") /// .file("/my/photo.jpg") -/// .create(&client)?; +/// .create(&client) +/// .await?; /// # Ok::<(), zotapi::Error>(()) +/// # }; +/// ``` /// ``` #[derive(Debug)] pub struct ItemBuilder<'a> { @@ -133,7 +139,7 @@ impl<'a> ItemBuilder<'a> { } /// Create the item by poting it to the server - pub fn create(&self, client: &Client) -> Result { + pub async fn create(&self, client: &Client) -> Result { dbg!(self); let mut req = client.post("item/update"); @@ -149,13 +155,20 @@ impl<'a> ItemBuilder<'a> { } for f in self.files.iter() { - form = form.file("media", f)?; + let mut pdata = vec![]; + let path = std::path::Path::new(f); + File::open(path)?.read_to_end(&mut pdata)?; + + let filename = String::from(path.file_name().unwrap().to_str().unwrap()); + let p = reqwest::multipart::Part::bytes(pdata) + .file_name(filename); + form = form.part("media", p); } req = req.multipart(form) } - Ok(serde_json::from_str(&req.send()?.text()?)?) + Ok(serde_json::from_str(&req.send().await?.text().await?)?) } } diff --git a/src/network_stream.rs b/src/network_stream.rs index 8204458..80e7c11 100644 --- a/src/network_stream.rs +++ b/src/network_stream.rs @@ -24,7 +24,7 @@ pub fn network_stream() -> NetworkStream { } impl NetworkStream { - pub fn fetch(&self, client: &Client) -> Result { - client.fetch_stream("network/stream", &()) + pub async fn fetch(&self, client: &Client) -> Result { + client.fetch_stream("network/stream", &()).await } } diff --git a/src/xchan.rs b/src/xchan.rs index 994f4b0..6fbe7a2 100644 --- a/src/xchan.rs +++ b/src/xchan.rs @@ -80,7 +80,7 @@ impl<'a> XChanRequest<'a> { self } - pub fn fetch(&self, client: &Client) -> Result { + pub async fn fetch(&self, client: &Client) -> Result { let mut req = client.get("xchan"); if let Some(sel) = &self.data { @@ -91,6 +91,6 @@ impl<'a> XChanRequest<'a> { }; } - Ok(serde_json::from_str(&dbg!(req.send()?.text()?))?) + Ok(serde_json::from_str(&dbg!(req.send().await?.text().await?))?) } } diff --git a/tests/zotapi.rs b/tests/zotapi.rs index af1d524..10a6c74 100644 --- a/tests/zotapi.rs +++ b/tests/zotapi.rs @@ -35,36 +35,36 @@ fn client() -> zotapi::Client { zotapi::client(&mockito::server_url(), "testuser", "test1234") } -#[test] -fn get_channel_stream() { +#[tokio::test] +async fn get_channel_stream() { let m = default_mock("GET", "/api/z/1.0/channel/stream"); - zotapi::channel_stream().fetch(&client()).unwrap(); + zotapi::channel_stream().fetch(&client()).await.unwrap(); m.assert(); } -#[test] -fn get_network_stream() { +#[tokio::test] +async fn get_network_stream() { let m = default_mock("GET", "/api/z/1.0/network/stream"); - zotapi::network_stream().fetch(&client()).unwrap(); + zotapi::network_stream().fetch(&client()).await.unwrap(); m.assert(); } -#[test] -fn return_error_if_invalid_auth_provided() { +#[tokio::test] +async fn return_error_if_invalid_auth_provided() { let m = mock("GET", "/api/z/1.0/channel/stream") .with_status(401) .with_header("content-type", "text") .with_body("This api requires login") .create(); - let data = zotapi::channel_stream().fetch(&client()); + let data = zotapi::channel_stream().fetch(&client()).await; m.assert(); assert!(data.is_err()); assert_eq!(format!("{:?}", data), "Err(Unauthorized)"); } -#[test] -fn create_new_post() { +#[tokio::test] +async fn create_new_post() { let m = mock_with_authorization("POST", "/api/z/1.0/item/update") .match_body("body=This+is+a+test") .with_status(200) @@ -72,13 +72,13 @@ fn create_new_post() { .with_body("{}") .create(); - let _res = zotapi::item().body("This is a test").create(&client()); + let _res = zotapi::item().body("This is a test").create(&client()).await; m.assert(); } -#[test] -fn create_new_post_with_title() { +#[tokio::test] +async fn create_new_post_with_title() { let m = mock_with_authorization("POST", "/api/z/1.0/item/update") .match_body(Matcher::AllOf(vec![ Matcher::UrlEncoded("title".to_string(), "A title".to_string()), @@ -92,13 +92,14 @@ fn create_new_post_with_title() { let _res = zotapi::item() .title("A title") .body("This is a test") - .create(&client()); + .create(&client()) + .await; m.assert(); } -#[test] -fn create_new_post_limited_to_one_privacy_group() { +#[tokio::test] +async fn create_new_post_limited_to_one_privacy_group() { // For some reason this mock does not match. // Visually inspecting the payload it does however seem valid, // so not sure how to handle this with the current framework. @@ -118,17 +119,18 @@ fn create_new_post_limited_to_one_privacy_group() { let _res = zotapi::item() .body("This is a test") .group_allow("grouphash") - .create(&client()); + .create(&client()) + .await; //m.assert(); } -#[test] -fn upload_item_with_media_file() { +#[tokio::test] +async fn upload_item_with_media_file() { let m = mock_with_authorization("POST", "/api/z/1.0/item/update") .match_header( "content-type", - Matcher::Regex("^multipart/form-data; boundary=.+$".into()), + Matcher::Regex("multipart/form-data; boundary=.+".into()), ) .match_body(Matcher::Regex( "--.+\r\n".to_owned() @@ -139,7 +141,6 @@ fn upload_item_with_media_file() { + "\r\nA title\r\n" + "--.+\r\n" + "Content-Disposition: form-data; name=\"media\"; filename=\"testfile.txt\"\r\n" - + "Content-Type: text/plain\r\n" + "\r\ntestfile contents\n" + "\r\n--.+--\r\n", )) @@ -152,13 +153,14 @@ fn upload_item_with_media_file() { .title("A title") .body("This is a test") .file("tests/fixtures/testfile.txt") - .create(&client()); + .create(&client()) + .await; m.assert(); } -#[test] -fn upload_item_with_two_files() { +#[tokio::test] +async fn upload_item_with_two_files() { let m = mock_with_authorization("POST", "/api/z/1.0/item/update") .match_header( "content-type", @@ -173,11 +175,9 @@ fn upload_item_with_two_files() { + "\r\nA title\r\n" + "--.+\r\n" + "Content-Disposition: form-data; name=\"media\"; filename=\"testfile.txt\"\r\n" - + "Content-Type: text/plain\r\n" + "\r\ntestfile contents\n" + "\r\n--.+\r\n" + "Content-Disposition: form-data; name=\"media\"; filename=\"testfile.txt\"\r\n" - + "Content-Type: text/plain\r\n" + "\r\ntestfile contents\n" + "\r\n--.+--\r\n", )) @@ -191,7 +191,8 @@ fn upload_item_with_two_files() { .body("This is a test") .file("tests/fixtures/testfile.txt") .file("tests/fixtures/testfile.txt") - .create(&client()); + .create(&client()) + .await; m.assert(); } @@ -225,8 +226,8 @@ const EMPTY_XCHAN: &str = r#"{ "deleted": 0 }"#; -#[test] -fn fetch_xchan_by_address() { +#[tokio::test] +async fn fetch_xchan_by_address() { let m = mock_with_authorization("GET", "/api/z/1.0/xchan?address=test%40test.com") .with_status(200) .with_header("content-type", "application/json") @@ -236,13 +237,14 @@ fn fetch_xchan_by_address() { let _res = zotapi::XChan::z() .by_address("test@test.com") .fetch(&client()) + .await .unwrap(); m.assert(); } -#[test] -fn fetch_xchan_by_hash() { +#[tokio::test] +async fn fetch_xchan_by_hash() { let m = mock_with_authorization("GET", "/api/z/1.0/xchan?hash=baffebaff") .with_status(200) .with_header("content-type", "application/json") @@ -252,13 +254,14 @@ fn fetch_xchan_by_hash() { let _res = zotapi::XChan::z() .by_hash("baffebaff") .fetch(&client()) + .await .unwrap(); m.assert(); } -#[test] -fn fetch_xchan_by_guid() { +#[tokio::test] +async fn fetch_xchan_by_guid() { let m = mock_with_authorization("GET", "/api/z/1.0/xchan?guid=baffebaff-baff-baff") .with_status(200) .with_header("content-type", "application/json") @@ -268,20 +271,21 @@ fn fetch_xchan_by_guid() { let _res = zotapi::XChan::z() .by_guid("baffebaff-baff-baff") .fetch(&client()) + .await .unwrap(); m.assert(); } -#[test] -fn fetch_connections() { +#[tokio::test] +async fn fetch_connections() { let m = default_mock("GET", "/api/z/1.0/abook"); - let _res = zotapi::Abook::z().fetch(&client()).unwrap(); + let _res = zotapi::Abook::z().fetch(&client()).await.unwrap(); m.assert(); } -#[test] -fn fetch_abconfig() { +#[tokio::test] +async fn fetch_abconfig() { let data = r#" [ { @@ -308,7 +312,7 @@ fn fetch_abconfig() { .with_body(&data) .create(); - let res = zotapi::ABConfig::z().fetch(&client()).unwrap(); + let res = zotapi::ABConfig::z().fetch(&client()).await.unwrap(); m.assert(); assert_eq!(res.len(), 2); @@ -316,27 +320,27 @@ fn fetch_abconfig() { assert_eq!(res[1].k, "key2"); } -#[test] -fn fetch_abconfig_for_specific_contact() { +#[tokio::test] +async fn fetch_abconfig_for_specific_contact() { let m = mock_with_authorization("GET", "/api/z/1.0/abconfig") .match_query(Matcher::UrlEncoded("abook_id".into(), 42.to_string())) .with_status(200) .with_body("[]") .create(); - zotapi::ABConfig::z().with_abook_id(42).fetch(&client()).unwrap(); + zotapi::ABConfig::z().with_abook_id(42).fetch(&client()).await.unwrap(); m.assert(); } -#[test] -fn fetch_privacy_groups() { +#[tokio::test] +async fn fetch_privacy_groups() { let m = default_mock("GET", "/api/z/1.0/group"); - let _res = zotapi::group().fetch(&client()).unwrap(); + let _res = zotapi::group().fetch(&client()).await.unwrap(); m.assert(); } -#[test] -fn fetch_members_of_group_by_group_id() { +#[tokio::test] +async fn fetch_members_of_group_by_group_id() { let m = mock_with_authorization("GET", "/api/z/1.0/group_members") .match_query(Matcher::UrlEncoded( "group_id".to_string(), @@ -350,13 +354,14 @@ fn fetch_members_of_group_by_group_id() { let _res = zotapi::group_members() .by_group_id(42) .fetch(&client()) + .await .unwrap(); m.assert(); } -#[test] -fn fetch_members_of_group_by_group_name() { +#[tokio::test] +async fn fetch_members_of_group_by_group_name() { let m = mock_with_authorization("GET", "/api/z/1.0/group_members") .match_query(Matcher::UrlEncoded( "group_name".into(), @@ -370,6 +375,7 @@ fn fetch_members_of_group_by_group_name() { let _res = zotapi::group_members() .by_group_name("Friends of pain") .fetch(&client()) + .await .unwrap(); m.assert(); -- cgit v1.2.3