From 1a438119a3a392acc143c4a257c2ddabddfaab80 Mon Sep 17 00:00:00 2001 From: Scott Lyons Date: Sun, 11 Aug 2024 00:37:15 -0700 Subject: [PATCH] fix(client): fixes for user and token CRUD operations --- Cargo.lock | 1 + szurubooru-client/src/client.rs | 111 ++++++++++++---------- szurubooru-client/src/models.rs | 2 +- szurubooru-integration-test/Cargo.toml | 1 + szurubooru-integration-test/avatar.jpg | Bin 0 -> 3509 bytes szurubooru-integration-test/src/main.rs | 120 ++++++++++++++++++++++-- 6 files changed, 180 insertions(+), 55 deletions(-) create mode 100644 szurubooru-integration-test/avatar.jpg diff --git a/Cargo.lock b/Cargo.lock index 1cb7c7b..f1ab679 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1421,6 +1421,7 @@ dependencies = [ name = "szurubooru-integration-test" version = "0.1.0" dependencies = [ + "chrono", "szurubooru-client", "tokio", "tracing", diff --git a/szurubooru-client/src/client.rs b/szurubooru-client/src/client.rs index fe4039f..4abf15f 100644 --- a/szurubooru-client/src/client.rs +++ b/szurubooru-client/src/client.rs @@ -13,10 +13,7 @@ use serde_json::Value; use sha1::{Digest, Sha1}; use std::fmt::{Display, Formatter}; use std::path::Path; -use std::{ - fs::File, - io::{BufReader, Read}, -}; +use std::{fs::File, io::Read}; use url::Url; /// @@ -324,7 +321,7 @@ impl<'a> SzurubooruRequest<'a> { // This doesn't detect the required `mut` for some reason #[allow(unused_mut)] let mut req = self.client.client.request(method, req_url); - let req = match &self.client.auth { + match &self.client.auth { SzurubooruAuth::TokenAuth(t) => { let mut header_map = HeaderMap::new(); header_map.append(AUTHORIZATION, t.parse().unwrap()); @@ -333,8 +330,7 @@ impl<'a> SzurubooruRequest<'a> { } SzurubooruAuth::BasicAuth(u, p) => req.basic_auth(u, Some(p)), SzurubooruAuth::None => req, - }; - req + } } #[tracing::instrument(skip(self), fields(base_url=self.client.base_url.to_string()))] @@ -767,7 +763,7 @@ impl<'a> SzurubooruRequest<'a> { }; let mut thumbnail_file = if let Some(t) = thumbnail { - if let None = filename { + if filename.is_none() { filename = Some( t.as_ref() .file_name() @@ -832,7 +828,7 @@ impl<'a> SzurubooruRequest<'a> { content_response .bytes() .await - .map_err(|e| SzurubooruClientError::RequestError(e)) + .map_err(SzurubooruClientError::RequestError) } /// Retrieves posts that look like the input image @@ -861,24 +857,12 @@ impl<'a> SzurubooruRequest<'a> { /// Searches for an exact match of a file based on the SHA1 checksum pub async fn posts_for_file( &self, - file: &mut File, + mut file: &mut File, ) -> SzurubooruResult> { - let mut buffer = [0; 1024]; let mut hasher = Sha1::new(); - let mut reader = BufReader::new(file); - - loop { - let n = reader - .read(&mut buffer) - .map_err(SzurubooruClientError::IOError)?; - hasher.update(buffer); - - if n == 0 { - break; - } - } - let result = hasher.finalize(); - let hex_string = hex::encode(result); + std::io::copy(&mut file, &mut hasher).map_err(SzurubooruClientError::IOError)?; + let hash = hasher.finalize(); + let hex_string = hex::encode(hash); let qt = QueryToken::token(PostNamedToken::ContentChecksum, hex_string); self.list_posts(Some(&vec![qt])).await @@ -1188,6 +1172,7 @@ impl<'a> SzurubooruRequest<'a> { path: &str, new_user: &CreateUpdateUser, file: Option<&mut File>, + file_name: Option>, ) -> SzurubooruResult { match file { None => self.do_request(method, path, None, Some(new_user)).await, @@ -1198,7 +1183,9 @@ impl<'a> SzurubooruRequest<'a> { .map_err(SzurubooruClientError::JSONSerializationError)?; let metadata_part = Part::text(metadata_str); - let content_part = self.part_from_file(file)?; + let content_part = self + .part_from_file(file)? + .file_name(file_name.unwrap().as_ref().to_string()); let form = Form::new() .part("avatar", content_part) @@ -1228,10 +1215,17 @@ impl<'a> SzurubooruRequest<'a> { pub async fn create_user_with_avatar_file( &self, avatar: &mut File, + file_name: impl AsRef, new_user: &CreateUpdateUser, ) -> SzurubooruResult { - self.create_update_user(Method::POST, "/users", new_user, Some(avatar)) - .await + self.create_update_user( + Method::POST, + "/users", + new_user, + Some(avatar), + Some(file_name), + ) + .await } /// Create a [UserResource](models::UserResource) with the included Avatar file path @@ -1242,9 +1236,16 @@ impl<'a> SzurubooruRequest<'a> { avatar_path: impl AsRef, new_user: &CreateUpdateUser, ) -> SzurubooruResult { - let mut file = File::open(avatar_path).map_err(SzurubooruClientError::IOError)?; - self.create_update_user(Method::POST, "/users", new_user, Some(&mut file)) - .await + let mut file = File::open(&avatar_path).map_err(SzurubooruClientError::IOError)?; + let filename = avatar_path.as_ref().file_name().unwrap().to_str().unwrap(); + self.create_update_user( + Method::POST, + "/users", + new_user, + Some(&mut file), + Some(filename), + ) + .await } /// Updates user using specified parameters. Names and passwords must match @@ -1262,8 +1263,8 @@ impl<'a> SzurubooruRequest<'a> { where T: AsRef + Display, { - let path = format!("/users/{name}"); - self.do_request(Method::POST, path, None, Some(update_user)) + let path = format!("/user/{name}"); + self.do_request(Method::PUT, path, None, Some(update_user)) .await } @@ -1274,14 +1275,21 @@ impl<'a> SzurubooruRequest<'a> { &self, name: T, avatar: &mut File, + file_name: impl AsRef, update_user: &CreateUpdateUser, ) -> SzurubooruResult where T: AsRef + Display, { - let path = format!("/users/{name}"); - self.create_update_user(Method::POST, &path, update_user, Some(avatar)) - .await + let path = format!("/user/{name}"); + self.create_update_user( + Method::PUT, + &path, + update_user, + Some(avatar), + Some(file_name), + ) + .await } /// Update a [UserResource](models::UserResource) with the included Avatar file path @@ -1296,10 +1304,17 @@ impl<'a> SzurubooruRequest<'a> { where T: AsRef + Display, { - let path = format!("/users/{name}"); - let mut file = File::open(avatar_path).map_err(SzurubooruClientError::IOError)?; - self.create_update_user(Method::POST, &path, new_user, Some(&mut file)) - .await + let path = format!("/user/{name}"); + let mut file = File::open(&avatar_path).map_err(SzurubooruClientError::IOError)?; + let filename = avatar_path.as_ref().file_name().unwrap().to_str().unwrap(); + self.create_update_user( + Method::PUT, + &path, + new_user, + Some(&mut file), + Some(filename), + ) + .await } /// Retrieves information about an existing user @@ -1307,7 +1322,7 @@ impl<'a> SzurubooruRequest<'a> { where T: AsRef + Display, { - let path = format!("/users/{name}"); + let path = format!("/user/{name}"); self.do_request(Method::GET, &path, None, None::<&String>) .await } @@ -1317,10 +1332,11 @@ impl<'a> SzurubooruRequest<'a> { where T: AsRef + Display, { - let path = format!("/users/{name}"); + let path = format!("/user/{name}"); let version_obj = ResourceVersion { version }; - self.do_request(Method::DELETE, &path, None, Some(&version_obj)) + self.do_request::(Method::DELETE, &path, None, Some(&version_obj)) .await + .map(|_| ()) } /// Listing user tokens for the given user. @@ -1346,7 +1362,7 @@ impl<'a> SzurubooruRequest<'a> { where T: AsRef + Display, { - let path = format!("/user-tokens/{name}"); + let path = format!("/user-token/{name}"); self.do_request(Method::POST, &path, None, Some(create_token)) .await } @@ -1363,7 +1379,7 @@ impl<'a> SzurubooruRequest<'a> { where T: AsRef + Display, { - let path = format!("/user-tokens/{name}/{token}"); + let path = format!("/user-token/{name}/{token}"); self.do_request(Method::PUT, &path, None, Some(update_token)) .await } @@ -1380,10 +1396,11 @@ impl<'a> SzurubooruRequest<'a> { where T: AsRef + Display, { - let path = format!("/user-tokens/{name}/{token}"); + let path = format!("/user-token/{name}/{token}"); let version_obj = ResourceVersion { version }; - self.do_request(Method::DELETE, &path, None, Some(&version_obj)) + self.do_request::(Method::DELETE, &path, None, Some(&version_obj)) .await + .map(|_| ()) } /// Sends a confirmation email to given user. The email contains link containing a token. The diff --git a/szurubooru-client/src/models.rs b/szurubooru-client/src/models.rs index 1defae2..6a5b8d6 100644 --- a/szurubooru-client/src/models.rs +++ b/szurubooru-client/src/models.rs @@ -510,7 +510,7 @@ pub struct UserResource { } #[derive(Debug, Clone, Serialize, Deserialize, Default, Builder)] -#[builder(setter(into, strip_option))] +#[builder(setter(strip_option))] #[serde(rename_all = "camelCase")] /// `struct` used to create or update a user resource. The version field is only used when /// updating an existing resource diff --git a/szurubooru-integration-test/Cargo.toml b/szurubooru-integration-test/Cargo.toml index 2c0554f..7801f51 100644 --- a/szurubooru-integration-test/Cargo.toml +++ b/szurubooru-integration-test/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition = "2021" [dependencies] +chrono = "0.4.38" szurubooru-client = { path = "../szurubooru-client" } tokio = { version = "1.39.2", features = ["full", "test-util", "tracing"] } tracing = "0.1.40" diff --git a/szurubooru-integration-test/avatar.jpg b/szurubooru-integration-test/avatar.jpg new file mode 100644 index 0000000000000000000000000000000000000000..708d879ff1bee2f6d51a805184a6ddc5f5296313 GIT binary patch literal 3509 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 KMOC){|4jgKl{Pp4 literal 0 HcmV?d00001 diff --git a/szurubooru-integration-test/src/main.rs b/szurubooru-integration-test/src/main.rs index 9849b65..07a52a5 100644 --- a/szurubooru-integration-test/src/main.rs +++ b/szurubooru-integration-test/src/main.rs @@ -1,3 +1,4 @@ +use chrono::Months; use std::error::Error; use std::fs::File; use std::path::Path; @@ -29,8 +30,8 @@ async fn main() -> Result<(), Box> { let anon_client = start_instance().await; let create_user = CreateUpdateUserBuilder::default() - .name("integration_user") - .password("integration_password") + .name("integration_user".to_string()) + .password("integration_password".to_string()) .rank(UserRank::Administrator) .avatar_style(UserAvatarStyle::Gravatar) .build() @@ -54,6 +55,7 @@ async fn main() -> Result<(), Box> { test_pool_categories(&auth_client).await; test_pools(&auth_client).await; test_comments(&auth_client).await; + test_users(&auth_client).await; Command::new("sh") .current_dir(env!("CARGO_MANIFEST_DIR")) @@ -390,10 +392,18 @@ async fn test_creating_posts(client: &SzurubooruClient) { let folly3_thumbnail = Path::new(env!("CARGO_MANIFEST_DIR")).join("folly3_thumb.jpg"); let folly3_post = client .request() - .create_post_from_file_path(folly3_path, Some(folly3_thumbnail), &folly3_obj) + .create_post_from_file_path(&folly3_path, Some(folly3_thumbnail), &folly3_obj) .await .expect("Could not create post with thumbnail"); + info!("Searching for post by image"); + let matching_posts = client + .request() + .posts_for_file_path(&folly3_path) + .await + .expect("Could not search for post by file path"); + assert_eq!(matching_posts.results.first().unwrap(), &folly3_post); + info!("Testing temporary upload"); let folly4_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("folly4.jpg"); let folly4_temp_upload = client @@ -504,6 +514,7 @@ async fn test_creating_posts(client: &SzurubooruClient) { .get_featured_post() .await .expect("Could not get featured post"); + assert!(featured_post.is_some()); } @@ -725,10 +736,6 @@ async fn test_comments(client: &SzurubooruClient) { .expect("Unable to fetch comment"); info!("Getting all comments for post"); - let query_vec = vec![QueryToken::token( - CommentNamedToken::Post, - post_id.to_string(), - )]; let comment_list = client .request() //.list_comments(None) @@ -755,3 +762,102 @@ async fn test_comments(client: &SzurubooruClient) { .await .expect("Could not delete comment"); } + +#[instrument(skip(client))] +async fn test_users(client: &SzurubooruClient) { + info!("Testing users"); + + info!("Listing users"); + let user_list = client + .request() + .list_users(None) + .await + .expect("Could not list users"); + assert_eq!(user_list.total, 1); + + // Create user is already tested above + info!("Creating user with avatar"); + let avatar_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("avatar.jpg"); + let create_user = CreateUpdateUserBuilder::default() + .name("iu2".to_string()) + .password("ipass2".to_string()) + .rank(UserRank::Regular) + .avatar_style(UserAvatarStyle::Manual) + .build() + .expect("Could not create user creation object"); + let user_obj = client + .request() + .create_user_with_avatar_path(avatar_path, &create_user) + .await + .expect("Could not create user"); + + info!("Updating user"); + let update_user = CreateUpdateUserBuilder::default() + .version(user_obj.version.unwrap()) + .rank(UserRank::Restricted) + .build() + .expect("Could not create user update object"); + let user_obj = client + .request() + .update_user(user_obj.name.unwrap(), &update_user) + .await + .expect("Could not update user"); + + info!("Getting user"); + let user_obj = client + .request() + .get_user(user_obj.name.unwrap()) + .await + .expect("Could not get user"); + + info!("Deleting user"); + client + .request() + .delete_user(user_obj.name.unwrap(), user_obj.version.unwrap()) + .await + .expect("Could not delete user"); + + let username = "integration_user".to_string(); + info!("Listing user tokens"); + let tokens = client + .request() + .list_user_tokens(&username) + .await + .expect("Could not list user tokens"); + assert!(tokens.results.is_empty()); + + info!("Creating user token"); + let create_token = CreateUpdateUserAuthTokenBuilder::default() + .note("My token") + .enabled(true) + .build() + .expect("Could not create token creation object"); + let token = client + .request() + .create_user_token(&username, &create_token) + .await + .expect("Could not create auth token"); + + info!("Updating user token"); + let new_expiration = chrono::offset::Utc::now() + .checked_add_months(Months::new(1)) + .unwrap(); + let update_token = CreateUpdateUserAuthTokenBuilder::default() + .version(token.version.unwrap()) + .expiration_time(new_expiration) + .build() + .expect("Could not create token update object"); + + let token = client + .request() + .update_user_token(&username, &token.token.unwrap(), &update_token) + .await + .expect("Could not update token"); + + info!("Deleting user token"); + client + .request() + .delete_user_token(username, token.token.unwrap(), token.version.unwrap()) + .await + .expect("Could not delete token"); +}