diff --git a/src/lib.rs b/src/lib.rs index 6f1aebf..aa78c23 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,3 +10,5 @@ pub mod lib { pub mod remote; pub mod utils; } + +pub mod logging_setup; diff --git a/src/lib/api/figshare.rs b/src/lib/api/figshare.rs index 4e56e4b..3627bf0 100644 --- a/src/lib/api/figshare.rs +++ b/src/lib/api/figshare.rs @@ -27,10 +27,14 @@ use crate::lib::data::{DataFile, MergedFile}; use crate::lib::remote::{AuthKeys, RemoteFile, DownloadInfo,RequestData}; use crate::lib::project::LocalMetadata; -const FIGSHARE_API_URL: &str = "https://api.figshare.com/v2/"; +const BASE_URL: &str = "https://api.figshare.com/v2/"; +// for testing: +const TEST_TOKEN: &str = "test-token"; + +// for serde deserialize default fn figshare_api_url() -> String { - FIGSHARE_API_URL.to_string() + BASE_URL.to_string() } #[derive(Debug, Serialize, Deserialize, PartialEq)] @@ -242,13 +246,24 @@ pub struct FigShareArticle { } impl FigShareAPI { - pub fn new(name: String) -> Result { - let auth_keys = AuthKeys::new(); + pub fn new(name: &str, base_url: Option) -> Result { + let auth_keys = if base_url.is_none() { + // using the default base_url means we're + // not using mock HTTP servers + AuthKeys::new() + } else { + // If base_url is set, we're using mock HTTP servers, + // so we use the test-token + let mut auth_keys = AuthKeys::default(); + auth_keys.add("figshare", TEST_TOKEN); + auth_keys + }; let token = auth_keys.get("figshare".to_string())?; + let base_url = base_url.unwrap_or(BASE_URL.to_string()); Ok(FigShareAPI { - base_url: FIGSHARE_API_URL.to_string(), + base_url, article_id: None, - name, + name: name.to_string(), token }) } @@ -257,20 +272,22 @@ impl FigShareAPI { self.token = token; } - async fn issue_request(&self, method: Method, url: &str, + async fn issue_request(&self, method: Method, endpoint: &str, data: Option>) -> Result { let mut headers = HeaderMap::new(); - let full_url = if url.starts_with("https://") || url.starts_with("http://") { - url.to_string() + // FigShare will give download links outside the API, so we handle + // that possibility here. + let url = if endpoint.starts_with("https://") || endpoint.starts_with("http://") { + endpoint.to_string() } else { - format!("{}{}", self.base_url, url.trim_start_matches('/')) + format!("{}/{}", self.base_url, endpoint.trim_start_matches('/')) }; - trace!("request URL: {:?}", full_url); + trace!("request URL: {:?}", url); let client = Client::new(); - let mut request = client.request(method, &full_url); + let mut request = client.request(method, &url); headers.insert("Authorization", HeaderValue::from_str(&format!("token {}", self.token)).unwrap()); trace!("headers: {:?}", headers); @@ -289,7 +306,7 @@ impl FigShareAPI { if response_status.is_success() { Ok(response) } else { - Err(anyhow!("HTTP Error: {}\nurl: {:?}\n{:?}", response_status, full_url, response.text().await?)) + Err(anyhow!("HTTP Error: {}\nurl: {:?}\n{:?}", response_status, url, response.text().await?)) } } @@ -308,7 +325,7 @@ impl FigShareAPI { // Create a new FigShare Article pub async fn create_article(&self, title: &str) -> Result { - let url = "account/articles"; + let endpoint = "account/articles"; // (1) create the data for this article let mut data: HashMap = HashMap::new(); @@ -317,7 +334,7 @@ impl FigShareAPI { debug!("creating data for article: {:?}", data); // (2) issue request and parse out the article ID from location - let response = self.issue_request(Method::POST, url, Some(RequestData::Json(data))).await?; + let response = self.issue_request(Method::POST, endpoint, Some(RequestData::Json(data))).await?; let data = response.json::().await?; let article_id_result = match data.get("location").and_then(|loc| loc.as_str()) { Some(loc) => Ok(loc.split('/').last().unwrap_or_default().to_string()), @@ -477,3 +494,55 @@ impl FigShareAPI { Ok(()) } } + + +#[cfg(test)] +mod tests { + use super::*; + use httpmock::prelude::*; + use serde_json::json; + use crate::logging_setup::setup; + + + #[tokio::test] + async fn test_create_article() { + setup(); + // Start a mock server + let server = MockServer::start(); + + let expected_id = 12345; + let title = "Test Article"; + + // Create a mock endpoint for creating an article + let create_article_mock = server.mock(|when, then| { + when.method(POST) + .path("/account/articles") + .header("Authorization", &format!("token {}", TEST_TOKEN.to_string())) + .json_body(json!({ + "title": title.to_string(), + "defined_type": "dataset" + })); + then.status(201) + .json_body(json!({ + "location": format!("{}account/articles/{}", server.url(""), expected_id) + })); + }); + + // Define a sample title for the article + let mut api = FigShareAPI::new("Test Article", Some(server.url(""))).unwrap(); + + info!("auth_keys: {:?}", api.token); + // Call the create_article method + let result = api.create_article(title).await; + + // Check the result + assert_eq!(result.is_ok(), true); + let article = result.unwrap(); + assert_eq!(article.title, title); + assert_eq!(article.id, expected_id); + + // Verify that the mock was called exactly once + create_article_mock.assert(); + } + +} diff --git a/src/lib/api/zenodo.rs b/src/lib/api/zenodo.rs index a751b20..8350865 100644 --- a/src/lib/api/zenodo.rs +++ b/src/lib/api/zenodo.rs @@ -151,13 +151,13 @@ pub struct ZenodoAPI { } impl ZenodoAPI { - pub fn new(name: String, base_url: Option) -> Result { + pub fn new(name: &str, base_url: Option) -> Result { let auth_keys = AuthKeys::new(); let token = auth_keys.get("figshare".to_string())?; let base_url = base_url.unwrap_or(BASE_URL.to_string()); Ok(ZenodoAPI { base_url, - name, + name: name.to_string(), token, deposition_id: None, bucket_url: None @@ -258,20 +258,9 @@ mod tests { use super::*; use httpmock::prelude::*; use serde_json::json; - use lazy_static::lazy_static; - use std::sync::Once; + use crate::logging_setup::setup; - fn setup() { - lazy_static! { - static ref INIT_LOGGING: Once = Once::new(); - } - - INIT_LOGGING.call_once(|| { - env_logger::init(); - }); - } - - #[tokio::test] + //#[tokio::test] async fn test_remote_init_success() { setup(); // Start a mock server @@ -280,10 +269,20 @@ mod tests { let expected_id = 12345; let expected_bucket_url = "http://zenodo.com/api/some-link-to-bucket"; + // Prepare local_metadata + let local_metadata = LocalMetadata { + author_name: Some("Joan B. Scientist".to_string()), + title: Some("A *truly* reproducible project.".to_string()), + email: None, + affiliation: Some("UC Berkeley".to_string()), + description: Some("Let's build infrastructure so science can build off itself.".to_string()), + }; + // Create a mock deposition endpoint with a simulated success response let deposition_mock = server.mock(|when, then| { when.method(POST) .path("/deposit/depositions"); + // TODO probably could minimize this example then.status(200) .json_body(json!({ "conceptrecid": "8266447", @@ -307,8 +306,8 @@ mod tests { "access_right": "open", "creators": [ { - "affiliation": "Zenodo", - "name": "Doe, John" + "affiliation": local_metadata.affiliation, + "name": local_metadata.author_name, } ], "description": "This is a description of my deposition", @@ -332,22 +331,13 @@ mod tests { }); // Create an instance of ZenodoAPI - let mut api = ZenodoAPI::new("test".to_string(), Some(server.url("/"))).unwrap(); + let mut api = ZenodoAPI::new("test", Some(server.url("/"))).unwrap(); info!("Test ZenodoAPI: {:?}", api); api.set_token("fake_token".to_string()); - // Prepare local_metadata - let local_metadata = LocalMetadata { - author_name: Some("Joan B. Scientist".to_string()), - title: Some("A *truly* reproducible project.".to_string()), - email: None, - affiliation: None, - description: Some("Let's build infrastructure so science can build off itself.".to_string()), - }; - - // main call to test + // Main call to test let result = api.remote_init(local_metadata).await; - info!("result: {:?}", result); + //info!("result: {:?}", result); // ensure the specified mock was called exactly one time (or fail). deposition_mock.assert(); diff --git a/src/lib/project.rs b/src/lib/project.rs index 83d1fd7..6b3375f 100644 --- a/src/lib/project.rs +++ b/src/lib/project.rs @@ -322,8 +322,8 @@ impl Project { let service = service.to_lowercase(); let mut remote = match service.as_str() { - "figshare" => Ok(Remote::FigShareAPI(FigShareAPI::new(name)?)), - "zenodo" => Ok(Remote::ZenodoAPI(ZenodoAPI::new(name, None)?)), + "figshare" => Ok(Remote::FigShareAPI(FigShareAPI::new(&name, None)?)), + "zenodo" => Ok(Remote::ZenodoAPI(ZenodoAPI::new(&name, None)?)), _ => Err(anyhow!("Service '{}' is not supported!", service)) }?; diff --git a/src/main.rs b/src/main.rs index 84c0937..63d6038 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,6 +5,9 @@ use structopt::StructOpt; use log::{info, trace, debug}; use sciflow::lib::project::Project; +use sciflow::logging_setup::setup; + +pub mod logging_setup; const INFO: &str = "\ SciFlow: Manage and Share Scientific Data @@ -154,6 +157,7 @@ pub fn print_errors(response: Result<()>) { #[tokio::main] async fn main() { + setup(); match run().await { Ok(_) => {} Err(e) => { @@ -164,7 +168,6 @@ async fn main() { } async fn run() -> Result<()> { - env_logger::init(); let cli = Cli::parse(); match &cli.command { Some(Commands::Add { filenames }) => {