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 0000000..708d879 Binary files /dev/null and b/szurubooru-integration-test/avatar.jpg differ 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"); +}