From e27a1fce603cc70ec9dd460ee9b84b4b2a099e6f Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 12 Aug 2024 15:56:01 -0400 Subject: [PATCH] Add more tests for uv lock (#6040) ## Summary Misc. cases we're missing vis-a-vis pip install. --- crates/uv/tests/common/mod.rs | 33 +++++ crates/uv/tests/lock.rs | 221 ++++++++++++++++++++++++++++++++- crates/uv/tests/pip_install.rs | 53 ++------ 3 files changed, 260 insertions(+), 47 deletions(-) diff --git a/crates/uv/tests/common/mod.rs b/crates/uv/tests/common/mod.rs index 02dbab979..d8d369c32 100644 --- a/crates/uv/tests/common/mod.rs +++ b/crates/uv/tests/common/mod.rs @@ -12,7 +12,9 @@ use std::str::FromStr; use assert_cmd::assert::{Assert, OutputAssertExt}; use assert_fs::assert::PathAssert; use assert_fs::fixture::{ChildPath, PathChild, PathCreateDir, SymlinkToFile}; +use base64::{prelude::BASE64_STANDARD as base64, Engine}; use indoc::formatdoc; +use itertools::Itertools; use predicates::prelude::predicate; use regex::Regex; @@ -989,6 +991,37 @@ pub fn make_project(dir: &Path, name: &str, body: &str) -> anyhow::Result<()> { Ok(()) } +// This is a fine-grained token that only has read-only access to the `uv-private-pypackage` repository +pub const READ_ONLY_GITHUB_TOKEN: &[&str] = &[ + "Z2l0aHViX3BhdA==", + "MTFCR0laQTdRMGdXeGsweHV6ekR2Mg==", + "NVZMaExzZmtFMHZ1ZEVNd0pPZXZkV040WUdTcmk2WXREeFB4TFlybGlwRTZONEpHV01FMnFZQWJVUm4=", +]; + +// This is a fine-grained token that only has read-only access to the `uv-private-pypackage-2` repository +#[cfg(not(windows))] +pub const READ_ONLY_GITHUB_TOKEN_2: &[&str] = &[ + "Z2l0aHViX3BhdA==", + "MTFCR0laQTdRMHV1MEpwaFp4dFFyRwo=", + "cnNmNXJwMHk2WWpteVZvb2ZFc0c5WUs5b2NPcFY1aVpYTnNmdE05eEhaM0lGSExSSktDWTcxeVBVZXkK", +]; + +/// Decode a split, base64 encoded authentication token. +/// We split and encode the token to bypass revoke by GitHub's secret scanning +pub fn decode_token(content: &[&str]) -> String { + let token = content + .iter() + .map(|part| base64.decode(part).unwrap()) + .map(|decoded| { + std::str::from_utf8(decoded.as_slice()) + .unwrap() + .trim_end() + .to_string() + }) + .join("_"); + token +} + /// Utility macro to return the name of the current function. /// /// https://stackoverflow.com/a/40234666/3549270 diff --git a/crates/uv/tests/lock.rs b/crates/uv/tests/lock.rs index c56130616..93e3de8a9 100644 --- a/crates/uv/tests/lock.rs +++ b/crates/uv/tests/lock.rs @@ -7,10 +7,11 @@ use indoc::{formatdoc, indoc}; use insta::assert_snapshot; use url::Url; -use crate::common::{build_vendor_links_url, packse_index_url}; use common::{uv_snapshot, TestContext}; use uv_fs::Simplified; +use crate::common::{build_vendor_links_url, decode_token, packse_index_url}; + mod common; /// Wraps a group of snapshots and runs them multiple times in sequence. @@ -5134,9 +5135,9 @@ fn lock_dev_transitive() -> Result<()> { Ok(()) } -/// Avoid persisting credentials in `uv.lock`. +/// Avoid persisting registry credentials in `uv.lock`. #[test] -fn lock_redact() -> Result<()> { +fn lock_redact_https() -> Result<()> { let context = TestContext::new("3.12"); let pyproject_toml = context.temp_dir.child("pyproject.toml"); @@ -5220,7 +5221,7 @@ fn lock_redact() -> Result<()> { Caused by: HTTP status client error (401 Unauthorized) for url (https://pypi-proxy.fly.dev/basic-auth/files/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl) "###); - // Installing from the lockfile should succeed when credentials are included. + // Installing from the lockfile should succeed when credentials are included on the command-line. uv_snapshot!(context.filters(), context.sync().arg("--frozen").arg("--index-url").arg("https://public:heron@pypi-proxy.fly.dev/basic-auth/simple"), @r###" success: true exit_code: 0 @@ -5234,6 +5235,218 @@ fn lock_redact() -> Result<()> { + iniconfig==2.0.0 "###); + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "foo" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["iniconfig"] + + [tool.uv] + index-url = "https://public:heron@pypi-proxy.fly.dev/basic-auth/simple" + "#, + )?; + + // Installing from the lockfile should succeed when credentials are included via `pyproject.toml`. + uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + warning: `uv sync` is experimental and may change without warning + Prepared 1 package in [TIME] + Uninstalled 1 package in [TIME] + Installed 1 package in [TIME] + - foo==0.1.0 (from file://[TEMP_DIR]/) + + foo==0.1.0 (from file://[TEMP_DIR]/) + "###); + + Ok(()) +} + +/// However, we don't currently avoid persisting Git credentials in `uv.lock`. +#[test] +fn lock_redact_git() -> Result<()> { + let context = TestContext::new("3.12"); + let token = decode_token(common::READ_ONLY_GITHUB_TOKEN); + + let filters: Vec<_> = [(token.as_str(), "***")] + .into_iter() + .chain(context.filters()) + .collect(); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str(&formatdoc! { + r#" + [project] + name = "foo" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["uv-private-pypackage @ git+https://{token}@github.com/astral-test/uv-private-pypackage"] + + [tool.uv] + index-url = "https://public:heron@pypi-proxy.fly.dev/basic-auth/simple" + "#, + token = token, + })?; + + uv_snapshot!(&filters, context.lock(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + warning: `uv lock` is experimental and may change without warning + Resolved 2 packages in [TIME] + "###); + + let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap(); + + insta::with_settings!({ + filters => filters.clone(), + }, { + assert_snapshot!( + lock, @r###" + version = 1 + requires-python = ">=3.12" + + [options] + exclude-newer = "2024-03-25 00:00:00 UTC" + + [[package]] + name = "foo" + version = "0.1.0" + source = { editable = "." } + dependencies = [ + { name = "uv-private-pypackage" }, + ] + + [[package]] + name = "uv-private-pypackage" + version = "0.1.0" + source = { git = "https://***@github.com/astral-test/uv-private-pypackage#d780faf0ac91257d4d5a4f0c5a0e4509608c0071" } + "### + ); + }); + + // Re-run with `--locked`. + uv_snapshot!(&filters, context.lock().arg("--locked"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + warning: `uv lock` is experimental and may change without warning + Resolved 2 packages in [TIME] + "###); + + // Install from the lockfile. + uv_snapshot!(&filters, context.sync().arg("--frozen"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + warning: `uv sync` is experimental and may change without warning + Prepared 2 packages in [TIME] + Installed 2 packages in [TIME] + + foo==0.1.0 (from file://[TEMP_DIR]/) + + uv-private-pypackage==0.1.0 (from git+https://***@github.com/astral-test/uv-private-pypackage@d780faf0ac91257d4d5a4f0c5a0e4509608c0071) + "###); + + Ok(()) +} + +/// Resolve against an index that uses relative links. +#[test] +fn lock_relative_index() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "foo" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["iniconfig"] + + [tool.uv] + index-url = "https://pypi-proxy.fly.dev/relative/simple" + "#, + )?; + + uv_snapshot!(context.filters(), context.lock(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + warning: `uv lock` is experimental and may change without warning + Resolved 2 packages in [TIME] + "###); + + let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap(); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lock, @r###" + version = 1 + requires-python = ">=3.12" + + [options] + exclude-newer = "2024-03-25 00:00:00 UTC" + + [[package]] + name = "foo" + version = "0.1.0" + source = { editable = "." } + dependencies = [ + { name = "iniconfig" }, + ] + + [[package]] + name = "iniconfig" + version = "2.0.0" + source = { registry = "https://pypi-proxy.fly.dev/relative/simple" } + sdist = { url = "https://pypi-proxy.fly.dev/files/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } + wheels = [ + { url = "https://pypi-proxy.fly.dev/files/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, + ] + "### + ); + }); + + // Re-run with `--locked`. + uv_snapshot!(context.filters(), context.lock().arg("--locked"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + warning: `uv lock` is experimental and may change without warning + Resolved 2 packages in [TIME] + "###); + + // Install from the lockfile. + uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + warning: `uv sync` is experimental and may change without warning + Prepared 2 packages in [TIME] + Installed 2 packages in [TIME] + + foo==0.1.0 (from file://[TEMP_DIR]/) + + iniconfig==2.0.0 + "###); + Ok(()) } diff --git a/crates/uv/tests/pip_install.rs b/crates/uv/tests/pip_install.rs index 0f7fe0af8..60b7d8dca 100644 --- a/crates/uv/tests/pip_install.rs +++ b/crates/uv/tests/pip_install.rs @@ -5,51 +5,18 @@ use std::process::Command; use anyhow::Result; use assert_cmd::prelude::*; use assert_fs::prelude::*; -use base64::{prelude::BASE64_STANDARD as base64, Engine}; use fs_err as fs; use indoc::indoc; -use itertools::Itertools; use predicates::prelude::predicate; use url::Url; use common::{uv_snapshot, TestContext}; use uv_fs::Simplified; -use crate::common::{build_vendor_links_url, get_bin, venv_bin_path}; +use crate::common::{build_vendor_links_url, decode_token, get_bin, venv_bin_path}; mod common; -// This is a fine-grained token that only has read-only access to the `uv-private-pypackage` repository -const READ_ONLY_GITHUB_TOKEN: &[&str] = &[ - "Z2l0aHViX3BhdA==", - "MTFCR0laQTdRMGdXeGsweHV6ekR2Mg==", - "NVZMaExzZmtFMHZ1ZEVNd0pPZXZkV040WUdTcmk2WXREeFB4TFlybGlwRTZONEpHV01FMnFZQWJVUm4=", -]; - -// This is a fine-grained token that only has read-only access to the `uv-private-pypackage-2` repository -#[cfg(not(windows))] -const READ_ONLY_GITHUB_TOKEN_2: &[&str] = &[ - "Z2l0aHViX3BhdA==", - "MTFCR0laQTdRMHV1MEpwaFp4dFFyRwo=", - "cnNmNXJwMHk2WWpteVZvb2ZFc0c5WUs5b2NPcFY1aVpYTnNmdE05eEhaM0lGSExSSktDWTcxeVBVZXkK", -]; - -/// Decode a split, base64 encoded authentication token. -/// We split and encode the token to bypass revoke by GitHub's secret scanning -fn decode_token(content: &[&str]) -> String { - let token = content - .iter() - .map(|part| base64.decode(part).unwrap()) - .map(|decoded| { - std::str::from_utf8(decoded.as_slice()) - .unwrap() - .trim_end() - .to_string() - }) - .join("_"); - token -} - #[test] fn missing_requirements_txt() { let context = TestContext::new("3.12"); @@ -1507,7 +1474,7 @@ fn install_git_public_https_missing_commit() { #[cfg(all(not(windows), feature = "git"))] fn install_git_private_https_pat() { let context = TestContext::new("3.8"); - let token = decode_token(READ_ONLY_GITHUB_TOKEN); + let token = decode_token(common::READ_ONLY_GITHUB_TOKEN); let filters: Vec<_> = [(token.as_str(), "***")] .into_iter() @@ -1540,7 +1507,7 @@ fn install_git_private_https_pat() { #[cfg(all(not(windows), feature = "git"))] fn install_git_private_https_pat_mixed_with_public() { let context = TestContext::new("3.8"); - let token = decode_token(READ_ONLY_GITHUB_TOKEN); + let token = decode_token(common::READ_ONLY_GITHUB_TOKEN); let filters: Vec<_> = [(token.as_str(), "***")] .into_iter() @@ -1573,8 +1540,8 @@ fn install_git_private_https_pat_mixed_with_public() { #[cfg(all(not(windows), feature = "git"))] fn install_git_private_https_multiple_pat() { let context = TestContext::new("3.8"); - let token_1 = decode_token(READ_ONLY_GITHUB_TOKEN); - let token_2 = decode_token(READ_ONLY_GITHUB_TOKEN_2); + let token_1 = decode_token(common::READ_ONLY_GITHUB_TOKEN); + let token_2 = decode_token(common::READ_ONLY_GITHUB_TOKEN_2); let filters: Vec<_> = [(token_1.as_str(), "***_1"), (token_2.as_str(), "***_2")] .into_iter() @@ -1610,7 +1577,7 @@ fn install_git_private_https_multiple_pat() { #[cfg(feature = "git")] fn install_git_private_https_pat_at_ref() { let context = TestContext::new("3.8"); - let token = decode_token(READ_ONLY_GITHUB_TOKEN); + let token = decode_token(common::READ_ONLY_GITHUB_TOKEN); let mut filters: Vec<_> = [(token.as_str(), "***")] .into_iter() @@ -1654,7 +1621,7 @@ fn install_git_private_https_pat_at_ref() { #[ignore] fn install_git_private_https_pat_and_username() { let context = TestContext::new("3.8"); - let token = decode_token(READ_ONLY_GITHUB_TOKEN); + let token = decode_token(common::READ_ONLY_GITHUB_TOKEN); let user = "astral-test-bot"; let filters: Vec<_> = [(token.as_str(), "***")] @@ -1719,7 +1686,7 @@ fn install_git_private_https_pat_not_authorized() { #[cfg(not(windows))] fn install_github_artifact_private_https_pat_mixed_with_public() { let context = TestContext::new("3.8"); - let token = decode_token(READ_ONLY_GITHUB_TOKEN); + let token = decode_token(common::READ_ONLY_GITHUB_TOKEN); let filters: Vec<_> = [(token.as_str(), "***")] .into_iter() @@ -1754,8 +1721,8 @@ fn install_github_artifact_private_https_pat_mixed_with_public() { #[cfg(not(windows))] fn install_github_artifact_private_https_multiple_pat() { let context = TestContext::new("3.8"); - let token_1 = decode_token(READ_ONLY_GITHUB_TOKEN); - let token_2 = decode_token(READ_ONLY_GITHUB_TOKEN_2); + let token_1 = decode_token(common::READ_ONLY_GITHUB_TOKEN); + let token_2 = decode_token(common::READ_ONLY_GITHUB_TOKEN_2); let filters: Vec<_> = [(token_1.as_str(), "***_1"), (token_2.as_str(), "***_2")] .into_iter()