diff --git a/Cargo.lock b/Cargo.lock index c71519c54..52baad54f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6465,6 +6465,7 @@ name = "uv-publish" version = "0.0.7" dependencies = [ "ambient-id", + "anstream", "astral-reqwest-middleware", "astral-reqwest-retry", "astral-tokio-tar", diff --git a/crates/uv-publish/Cargo.toml b/crates/uv-publish/Cargo.toml index ac3f9b95b..c16216923 100644 --- a/crates/uv-publish/Cargo.toml +++ b/crates/uv-publish/Cargo.toml @@ -49,6 +49,7 @@ url = { workspace = true } wiremock = { workspace = true } [dev-dependencies] +anstream = { workspace = true } insta = { workspace = true } fastrand = { workspace = true } diff --git a/crates/uv-publish/src/lib.rs b/crates/uv-publish/src/lib.rs index 018891195..ab6ff56bf 100644 --- a/crates/uv-publish/src/lib.rs +++ b/crates/uv-publish/src/lib.rs @@ -1194,19 +1194,20 @@ mod tests { use insta::{allow_duplicates, assert_debug_snapshot, assert_snapshot}; use itertools::Itertools; - + use thiserror::__private17::AsDynError; use uv_auth::Credentials; use uv_client::{AuthIntegration, BaseClientBuilder, RedirectPolicy}; use uv_distribution_filename::DistFilename; use uv_redacted::DisplaySafeUrl; - use tokio::sync::Semaphore; - use wiremock::matchers::{method, path}; - use wiremock::{Mock, MockServer, ResponseTemplate}; - use crate::{ FormMetadata, Reporter, UploadDistribution, build_upload_request, group_files, upload, }; + use tokio::sync::Semaphore; + use uv_warnings::owo_colors::AnsiColors; + use uv_warnings::write_error_chain; + use wiremock::matchers::{method, path}; + use wiremock::{Mock, MockServer, ResponseTemplate}; struct DummyReporter; @@ -1837,4 +1838,75 @@ mod tests { assert!(response); } + + #[tokio::test] + async fn upload_infinite_redirects() { + let mock_server = MockServer::start().await; + Mock::given(method("POST")) + .and(path("/final")) + .respond_with( + ResponseTemplate::new(308) + .insert_header("Location", format!("{}/final/", &mock_server.uri())), + ) + .mount(&mock_server) + .await; + Mock::given(method("POST")) + .and(path("/final/")) + .respond_with( + ResponseTemplate::new(308) + .insert_header("Location", format!("{}/final", &mock_server.uri())), + ) + .mount(&mock_server) + .await; + + let raw_filename = "tqdm-4.66.1-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl"; + let file = PathBuf::from("../../test/links/").join(raw_filename); + let filename = DistFilename::try_from_normalized_filename(raw_filename).unwrap(); + + let group = UploadDistribution { + file, + raw_filename: raw_filename.to_string(), + filename, + attestations: vec![], + }; + + let form_metadata = FormMetadata::read_from_file(&group.file, &group.filename) + .await + .unwrap(); + + let client = BaseClientBuilder::default() + .redirect(RedirectPolicy::NoRedirect) + .retries(0) + .auth_integration(AuthIntegration::NoAuthMiddleware) + .build(); + + let download_concurrency = Arc::new(Semaphore::new(1)); + let registry = DisplaySafeUrl::parse(&format!("{}/final/", &mock_server.uri())).unwrap(); + let err = upload( + &group, + &form_metadata, + ®istry, + &client, + client.retry_policy(), + &Credentials::basic(Some("ferris".to_string()), Some("F3RR!S".to_string())), + None, + &download_concurrency, + Arc::new(DummyReporter), + ) + .await + .unwrap_err(); + + let mut capture = String::new(); + write_error_chain(err.as_dyn_error(), &mut capture, "error", AnsiColors::Red).unwrap(); + + let capture = capture.replace(&mock_server.uri(), "[SERVER]"); + let capture = anstream::adapter::strip_str(&capture); + assert_snapshot!( + &capture, + @r" + error: Failed to publish `../../test/links/tqdm-4.66.1-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl` to [SERVER]/final/ + Caused by: Too many redirects, only 10 redirects are allowed + " + ); + } }