diff --git a/README.md b/README.md index 47d2718..37688c9 100644 --- a/README.md +++ b/README.md @@ -51,14 +51,3 @@ One of these, defaults to `help`: | upload-release | Uploads one-or-more files to an existing release, identified by it's tag name. | | help | prints the help text (the usage summary above). | -## Unit Testing - -The unit test~~s~~ require a Gitea server to execute against. This information is supplied by environment variables rather than on the command line, but it is otherwise exactly the same usage. - -| Variable | Description | -|-|-| -| TEST_GITEA_SERVER | Server URL, match `-u`, `--url` | -| TEST_GITEA_REPO | Owner + repo name, match `-u` `--repo` | -| TEST_GITEA_KEY | API key, match `RELEASE_KEY_GITEA`. The use of a new variable for the API token is to help avoid accidentally touching a production environment during test execution. | -| TEST_GITEA_RELEASE_TAG | Git tag used to identify the Release. Same as `upload-release`'s positional argument ``. | - diff --git a/src/api/release.rs b/src/api/release.rs index 09f925a..4250d3f 100644 --- a/src/api/release.rs +++ b/src/api/release.rs @@ -1,9 +1,7 @@ -use serde::{Deserialize, Serialize}; use crate::{ - ApiError, Result, + Result, structs::{ - self, release::{CreateReleaseOption, Release}, }, }; @@ -23,23 +21,21 @@ pub async fn list_releases( let request_url = format!("{gitea_url}/api/v1/repos/{repo}/releases/"); let req = client.get(request_url).send().await; let response = req.map_err(|reqwest_err| crate::Error::WrappedReqwestErr(reqwest_err))?; - let release_list = response - .json::>() - .await - .map_err(|reqwest_err| { - // Convert reqwest errors to my own - // TODO: Create all error variants (see lib.rs) - crate::Error::WrappedReqwestErr(reqwest_err) - })?; - return Ok(release_list); -} - -#[derive(Debug, Deserialize, Serialize)] -#[serde(untagged)] -enum CreateResult { - Success(structs::release::Release), - ErrWithMessage(ApiError), - Empty, + if response.status().is_success() { + let release_list = response + .json::>() + .await + .map_err(|reqwest_err| { + // Convert reqwest errors to my own + // TODO: Create all error variants (see lib.rs) + crate::Error::WrappedReqwestErr(reqwest_err) + })?; + return Ok(release_list); + } else if response.status().is_client_error() { + let mesg = crate::decode_client_error(response).await?; + return Err(crate::Error::ApiErrorMessage(mesg)); + } + panic!("Reached end of list_releases without matching a return pathway."); } pub async fn create_release( @@ -49,27 +45,23 @@ pub async fn create_release( submission: CreateReleaseOption, ) -> Result { let request_url = format!("{gitea_url}/api/v1/repos/{repo}/releases"); - let req = client + let response = client .post(request_url) .json(&submission) .send() .await .map_err(|e| crate::Error::from(e))?; - let new_release = req - .json::() - .await - .map_err(|e| crate::Error::from(e))?; - match new_release { - CreateResult::Success(release) => Ok(release), - CreateResult::ErrWithMessage(api_error) => { - if api_error.message == "token is required" { - Err(crate::Error::MissingAuthToken) - } else { - Err(crate::Error::ApiErrorMessage(api_error)) - } - } - CreateResult::Empty => panic!("How can we have 200 OK and no release info? No. Crash"), + if response.status().is_success() { + let new_release = response + .json::() + .await + .map_err(|e| crate::Error::from(e))?; + return Ok(new_release); + } else if response.status().is_client_error() { + let mesg = crate::decode_client_error(response).await?; + return Err(crate::Error::ApiErrorMessage(mesg)) } + panic!("Reached end of create_release without matching a return path"); } pub fn edit_release(id: u64) -> Result { todo!(); diff --git a/src/api/release_attachment.rs b/src/api/release_attachment.rs index 9af71bf..ae626ef 100644 --- a/src/api/release_attachment.rs +++ b/src/api/release_attachment.rs @@ -1,4 +1,6 @@ -use std::fs; +use std::{fs, path}; + +use crate::structs::Attachment; pub fn check_release_match_repo() {} pub fn get_release_attachment() {} @@ -10,174 +12,45 @@ pub async fn create_release_attachment( gitea_url: &str, repo: &str, release_id: usize, - files: Vec, -) -> crate::Result<()> { + file: String, +) -> crate::Result { let request_url = format!("{gitea_url}/api/v1/repos/{repo}/releases/{release_id}/assets"); - // Ensure all files exists before starting the uploads - for file in &files { - match fs::exists(file) { - Ok(true) => continue, - Ok(false) => return Err(crate::Error::NoSuchFile), - Err(e) => { - eprintln!("Uh oh! The file-exists check couldn't be done: {e}"); - panic!("TODO: Deal with scenario where the file's existence cannot be checked (e.g.: no permission)"); - }, - } + let path = path::Path::new(&file); + match path.try_exists() { + Ok(true) => (), + Ok(false) => return Err(crate::Error::NoSuchFile), + Err(e) => { + eprintln!("Uh oh! The file-exists check couldn't be done: {e}"); + panic!("TODO: Deal with scenario where the file's existence cannot be checked (e.g.: no permission)"); + }, } - for file in files { - println!("Uploading file {}", &file); - let data = reqwest::multipart::Part::stream(fs::read(&file).unwrap()) - .file_name("attachment") - .mime_str("text/plain")?; + println!("Uploading file {}", &file); + let data = reqwest::multipart::Part::stream(fs::read(&file).unwrap()) + .file_name("attachment") + .mime_str("text/plain")?; - let form = reqwest::multipart::Form::new().part("attachment", data); + let form = reqwest::multipart::Form::new().part("attachment", data); - let request = client - .post(&request_url) - .multipart(form) - .query(&[("name", file.split("/").last())]) - .send() - .await?; + let response = client + .post(&request_url) + .multipart(form) + .query(&[("name", file.split("/").last())]) + .send() + .await?; + if response.status().is_success() { + // TODO: create a struct Attachment and return it to the caller. + let attachment_desc = response + .json::() + .await + .map_err(|e| crate::Error::from(e))?; + return Ok(attachment_desc); + } else if response.status().is_client_error() { + let mesg = crate::decode_client_error(response).await?; + return Err(crate::Error::ApiErrorMessage(mesg)); } - Ok(()) + panic!("Reached end of release_attachment without matching a return path"); } pub fn edit_release_attachment() {} pub fn delete_release_attachment() {} - -#[cfg(test)] -mod tests { - use reqwest::header::{self, ACCEPT}; - - use crate::structs::release::Release; - - - #[tokio::test] - async fn attach_file_exists() { - let conf = TestConfig::new(); - let release_candidates = - crate::api::release::list_releases( - &conf.client, - &conf.server, - &conf.repo - ) - .await - .expect("Failed to get releases. Pre-conditions unmet, aborting test!"); - - let release = match_release_by_tag(&conf.release_tag, release_candidates) - .expect("Failed to select matching release. Pre-conditions unmet, aborting test!"); - - let api_result = super::create_release_attachment( - &conf.client, - &conf.server, - &conf.repo, - release.id, - vec![String::from("Cargo.toml")], - ) - .await; - } - - #[tokio::test] - async fn attach_file_missing() { - let conf = TestConfig::new(); - let release_candidates = - crate::api::release::list_releases( - &conf.client, - &conf.server, - &conf.repo - ) - .await - .expect("Failed to get releases. Pre-conditions unmet, aborting test!"); - - let release = match_release_by_tag(&conf.release_tag, release_candidates) - .expect("Failed to select matching release. Pre-conditions unmet, aborting test!"); - - let api_result = super::create_release_attachment( - &conf.client, - &conf.server, - &conf.repo, - release.id, - vec![String::from("./this-file-doesnt-exist")], - ) - .await; - let api_err = api_result.expect_err("Received Ok(()) after uploading non-existent file. That's nonsense, the API Function is wrong."); - match api_err { - crate::Error::Placeholder => panic!("Received dummy response from the API function. Finish implementing it, stupid"), - crate::Error::WrappedReqwestErr(error) => panic!("Received a reqwest::Error from the API function: {error}"), - crate::Error::MissingAuthToken => unreachable!("Missing auth token... in a unit test that already panics without the auth token..."), - crate::Error::NoSuchFile => (), // test passes - crate::Error::ApiErrorMessage(api_error) => panic!("Received an error message from the API: {api_error:?}"), - } - } - - struct TestConfig { - server: String, - repo: String, - release_tag: String, - client: reqwest::Client, - } - - impl TestConfig { - fn new() -> Self { - let server = std::env::var("TEST_GITEA_SERVER") - .expect("Must set server address in env var \"TEST_GITEA_SERVER\""); - let repo = std::env::var("TEST_GITEA_REPO") - .expect("Must set / name in env var \"TEST_GITEA_REPO\""); - let token = format!( - "token {}", - std::env::var("TEST_GITEA_KEY") - .expect("Must set the API token in env var \"TEST_GITEA_KEY\"") - ); - let release_tag = std::env::var("TEST_GITEA_RELEASE_TAG") - .expect("Must set the target release tag in env var \"TEST_GITEA_RELEASE_TAG\""); - - let mut headers = reqwest::header::HeaderMap::new(); - headers.append(ACCEPT, header::HeaderValue::from_static("application/json")); - headers.append("Authorization", token.parse().unwrap()); - - let client = reqwest::Client::builder() - .user_agent(format!( - "gt-tools-autotest-agent{}", - env!("CARGO_PKG_VERSION") - )) - .default_headers(headers) - .build() - .expect("Failed to build reqwest::Client."); - - return Self { - server, - repo, - release_tag, - client - }; - } - } - - // Testing utils - fn match_release_by_tag(tag: &String, releases: Vec) -> Option { - let mut release: Option = None; - for rel in releases { - if rel.tag_name == *tag { - // Only store the value if one hasn't been stored already - if let Some(first_release) = &release { - // if there was already a match, begin the error diagnostic creation. - let first_id = first_release.id; - let second_id = rel.id; - assert!( - first_id != second_id, - "FAILURE: Found the same release ID twice while scanning for duplicate tags. How did we get the same one twice?" - ); - eprintln!("ERROR: Two releases have been found for the tag \"{tag}\"."); - eprintln!("ERROR: first ID: {first_id}"); - eprintln!("ERROR: second ID: {second_id}"); - panic!("ERROR: Nonsense detected, I'm bailing out!"); - } else { - // else, store our first (and hopefully only) match - release = Some(rel); - } - } - } - return release; - } -} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 14cfab7..42cac0a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,6 +10,15 @@ pub struct ApiError { url: String, } +pub (crate) async fn decode_client_error(response: reqwest::Response) -> Result { + response + .json::() + .await + .map_err(|reqwest_err| { + crate::Error::WrappedReqwestErr(reqwest_err) + }) +} + #[derive(Debug)] pub enum Error { Placeholder, // TODO: Enumerate error modes diff --git a/src/main.rs b/src/main.rs index 02bd869..555b843 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,6 @@ + +use std::path; + use gt_tool::cli::Args; use gt_tool::structs::release::{CreateReleaseOption, Release}; @@ -78,14 +81,27 @@ async fn main() -> Result<(), gt_tool::Error> { gt_tool::api::release::list_releases(&client, &args.gitea_url, &args.repo).await?; if let Some(release) = match_release_by_tag(&tag_name, release_candidates) { - gt_tool::api::release_attachment::create_release_attachment( - &client, - &args.gitea_url, - &args.repo, - release.id, - files, - ) - .await?; + for file in &files { + let path = path::Path::new(&file); + match path.try_exists() { + Ok(true) => continue, + Ok(false) => return Err(gt_tool::Error::NoSuchFile), + Err(e) => { + eprintln!("Uh oh! The file-exists check couldn't be done: {e}"); + panic!("TODO: Deal with scenario where the file's existence cannot be checked (e.g.: no permission)"); + }, + } + } + for file in files { + let _attach_desc = gt_tool::api::release_attachment::create_release_attachment( + &client, + &args.gitea_url, + &args.repo, + release.id, + file, + ) + .await?; + } } else { println!("ERR: Couldn't find a release matching the tag \"{tag_name}\"."); return Err(gt_tool::Error::NoSuchRelease); diff --git a/src/structs/mod.rs b/src/structs/mod.rs index 357eccf..5623965 100644 --- a/src/structs/mod.rs +++ b/src/structs/mod.rs @@ -1,2 +1,15 @@ +use serde::{Deserialize, Serialize}; + pub mod release; pub mod repo; + +#[derive(Debug, Deserialize, Serialize)] +pub struct Attachment { + id: usize, + name: String, + size: i64, + download_count: i64, + created: String, // TODO: Date-time struct + uuid: String, + download_url: String, +}