diff --git a/Cargo.toml b/Cargo.toml index fe3cd55..9d84f05 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dco3" -version = "0.4.0" +version = "0.4.1" edition = "2021" authors = ["Octavio Simone"] repository = "https://github.com/unbekanntes-pferd/dco3" @@ -15,7 +15,7 @@ description = "Async API wrapper for DRACOON in Rust." [dependencies] # HTTP client -reqwest = {version = "0.11.14", features = ["json", "stream"]} +reqwest = {version = "0.11.18", features = ["json", "stream"]} reqwest-middleware = "0.2.2" reqwest-retry = "0.2.2" @@ -23,32 +23,32 @@ reqwest-retry = "0.2.2" dco3_crypto = "0.5.0" # async runtime and utils -tokio = { version = "1.27.0", features = ["full"] } -tokio-util = { version = "0.7.7", features = ["full"] } -async-trait = "0.1.68" +tokio = { version = "1.29.1", features = ["full"] } +tokio-util = { version = "0.7.8", features = ["full"] } +async-trait = "0.1.72" async-stream = "0.3.5" futures-util = "0.3.28" bytes = "1.4.0" # parsing -serde = { version = "1.0.159", features = ["derive"] } +serde = { version = "1.0.178", features = ["derive"] } serde-xml-rs = "0.6.0" -serde_json = "1.0.95" +serde_json = "1.0.104" # error handling -thiserror = "1.0.2" -retry-policies = "0.1.0" +thiserror = "1.0.44" +retry-policies = "0.1.2" # logging and tracing tracing = "0.1.37" tracing-subscriber = {version = "0.3.17", features = ["env-filter"]} # utils -url = "2.3.1" -base64 = "0.21.0" -chrono = "0.4.1" +url = "2.4.0" +base64 = "0.21.2" +chrono = "0.4.26" [dev-dependencies] -mockito = "1.0.2" +mockito = "1.1.0" tokio-test = "0.4.2" diff --git a/src/nodes/upload.rs b/src/nodes/upload.rs index 843341a..a2d7687 100644 --- a/src/nodes/upload.rs +++ b/src/nodes/upload.rs @@ -346,7 +346,6 @@ impl UploadInternal for Dracoon .expect("size not larger than 32 MB") ]; match reader.read_exact(&mut buffer).await { - Ok(0) => unreachable!("last chunk is empty"), Ok(n) => { buffer.truncate(n); let chunk = bytes::Bytes::from(buffer); @@ -579,7 +578,6 @@ impl UploadInternal for Dracoon .expect("size not larger than 32 MB") ]; match crypto_reader.read_exact(&mut buffer).await { - Ok(0) => unreachable!("last chunk is empty"), Ok(n) => { buffer.truncate(n); let chunk = bytes::Bytes::from(buffer); @@ -1210,6 +1208,96 @@ mod tests { assert_node(&node); } + #[tokio::test] + async fn test_upload_to_s3_unencrypted_no_content() { + let (client, mut mock_server) = get_connected_client().await; + + let parent_node: Node = + serde_json::from_str(include_str!("../tests/responses/nodes/node_ok.json")).unwrap(); + + // test 0KB file + let mock_bytes: Vec = vec![]; + + let reader = Cursor::new(mock_bytes); + let reader_clone = BufReader::new(reader); + + let file_meta = FileMeta::builder() + .with_name("test".into()) + .with_size(0) + .build(); + + let upload_options = UploadOptions::default(); + + // mock upload channel + let channel_res = include_str!("../tests/responses/upload/upload_channel_ok.json"); + + let upload_channel_mock = mock_server + .mock("POST", "/api/v4/nodes/files/uploads") + .with_status(201) + .with_body(channel_res) + .with_header("content-type", "application/json") + .create(); + + // mock S3 urls + let s3_urls_response = + include_str!("../tests/responses/upload/s3_urls_ok_with_placeholder.json"); + let s3_urls_response = + s3_urls_response.replace("$base_url/", client.get_base_url().as_str()); + + let s3_urls_mock = mock_server + .mock("POST", "/api/v4/nodes/files/uploads/string/s3_urls") + .with_status(201) + .with_body(s3_urls_response.clone()) + .with_header("content-type", "application/json") + .create(); + + let upload_res = + serde_json::from_str::(s3_urls_response.as_str()).unwrap(); + + // mock upload to S3 + let upload_mock = mock_server + .mock("PUT", "/upload_url") + .with_status(202) + .with_header("etag", "string") + .create(); + + // mock finalize upload + let finalize_mock = mock_server + .mock("PUT", "/api/v4/nodes/files/uploads/string/s3") + .with_status(202) + .create(); + + // mock upload status + let status_res = include_str!("../tests/responses/upload/upload_status_ok.json"); + let status_mock = mock_server + .mock("GET", "/api/v4/nodes/files/uploads/string") + .with_status(200) + .with_body(status_res) + .with_header("content-type", "application/json") + .create(); + + let node = + as UploadInternal>>>::upload_to_s3_unencrypted( + &client, + file_meta, + &parent_node, + upload_options, + reader_clone, + None, + None, + ) + .await + .unwrap(); + + upload_channel_mock.assert(); + s3_urls_mock.assert(); + upload_mock.assert(); + finalize_mock.assert(); + status_mock.assert(); + + assert_node(&node); + } + #[tokio::test] async fn test_upload_to_s3_encrypted() { let (client, mut mock_server) = get_connected_client().await; @@ -1330,4 +1418,124 @@ mod tests { assert_node(&node); } + + #[tokio::test] + async fn test_upload_to_s3_encrypted_no_content() { + let (client, mut mock_server) = get_connected_client().await; + + let parent_node: Node = + serde_json::from_str(include_str!("../tests/responses/nodes/node_ok.json")).unwrap(); + + // empty file + let mock_bytes: Vec = vec![]; + + let reader = Cursor::new(mock_bytes); + let reader_clone = BufReader::new(reader); + + let keypair = + DracoonCrypto::create_plain_user_keypair(dco3_crypto::UserKeyPairVersion::RSA4096) + .unwrap(); + let enc_keypair = + DracoonCrypto::encrypt_private_key("TopSecret1234!", keypair.clone()).unwrap(); + let enc_keypair_json = serde_json::to_string(&enc_keypair).unwrap(); + + let keypair_mock = mock_server + .mock("GET", "/api/v4/user/account/keypair") + .with_status(200) + .with_header("content-type", "application/json") + .with_body(enc_keypair_json) + .create(); + + let _kp = client + .get_keypair(Some("TopSecret1234!".into())) + .await + .unwrap(); + + keypair_mock.assert(); + + let file_meta = FileMeta::builder() + .with_name("test".into()) + .with_size(0) + .build(); + + let upload_options = UploadOptions::default(); + + // mock upload channel + let channel_res = include_str!("../tests/responses/upload/upload_channel_ok.json"); + + let upload_channel_mock = mock_server + .mock("POST", "/api/v4/nodes/files/uploads") + .with_status(201) + .with_body(channel_res) + .with_header("content-type", "application/json") + .create(); + + // mock S3 urls + let s3_urls_response = + include_str!("../tests/responses/upload/s3_urls_ok_with_placeholder.json"); + let s3_urls_response = + s3_urls_response.replace("$base_url/", client.get_base_url().as_str()); + + let s3_urls_mock = mock_server + .mock("POST", "/api/v4/nodes/files/uploads/string/s3_urls") + .with_status(201) + .with_body(s3_urls_response.clone()) + .with_header("content-type", "application/json") + .create(); + + let upload_res = + serde_json::from_str::(s3_urls_response.as_str()).unwrap(); + + // mock upload to S3 + let upload_mock = mock_server + .mock("PUT", "/upload_url") + .with_status(202) + .with_header("etag", "string") + .create(); + + // mock finalize upload + let finalize_mock = mock_server + .mock("PUT", "/api/v4/nodes/files/uploads/string/s3") + .with_status(202) + .create(); + + // mock upload status + let status_res = include_str!("../tests/responses/upload/upload_status_ok.json"); + let status_mock = mock_server + .mock("GET", "/api/v4/nodes/files/uploads/string") + .with_status(200) + .with_body(status_res) + .with_header("content-type", "application/json") + .create(); + + // mock missing file keys + let missing_keys = include_str!("../tests/responses/nodes/missing_file_keys_empty_ok.json"); + let keys_mock = mock_server + .mock("GET", "/api/v4/nodes/missingFileKeys?file_id=2&limit=50") + .with_status(200) + .with_body(missing_keys) + .with_header("content-type", "application/json") + .create(); + + let node = as UploadInternal>>>::upload_to_s3_encrypted( + &client, + file_meta, + &parent_node, + upload_options, + reader_clone, + None, + None, + ) + .await + .unwrap(); + + upload_channel_mock.assert(); + s3_urls_mock.assert(); + upload_mock.assert(); + finalize_mock.assert(); + status_mock.assert(); + keys_mock.assert(); + + assert_node(&node); + } }