3e22637c93
Co-authored-by: Claude <noreply@anthropic.com>
2141 lines
57 KiB
Rust
2141 lines
57 KiB
Rust
use anyhow::Result;
|
||
use assert_cmd::assert::OutputAssertExt;
|
||
use assert_fs::{fixture::PathChild, prelude::FileWriteStr};
|
||
#[cfg(feature = "native-auth")]
|
||
use uv_static::EnvVars;
|
||
|
||
use crate::common::{TestContext, uv_snapshot};
|
||
|
||
#[test]
|
||
#[cfg(feature = "native-auth")]
|
||
fn add_package_native_auth_realm() -> Result<()> {
|
||
let context = TestContext::new("3.12").with_real_home();
|
||
|
||
// Clear state before the test
|
||
context
|
||
.auth_logout()
|
||
.arg("https://pypi-proxy.fly.dev")
|
||
.arg("--username")
|
||
.arg("public")
|
||
.env(EnvVars::UV_PREVIEW_FEATURES, "native-auth")
|
||
.status()?;
|
||
|
||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||
pyproject_toml.write_str(indoc::indoc! { r#"
|
||
[project]
|
||
name = "foo"
|
||
version = "1.0.0"
|
||
requires-python = ">=3.11, <4"
|
||
dependencies = []
|
||
"#
|
||
})?;
|
||
|
||
// Try to add a package without credentials.
|
||
uv_snapshot!(context.add().arg("anyio").arg("--default-index").arg("https://public@pypi-proxy.fly.dev/basic-auth/simple")
|
||
.env(EnvVars::UV_PREVIEW_FEATURES, "native-auth"), @r"
|
||
success: false
|
||
exit_code: 1
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
× No solution found when resolving dependencies:
|
||
╰─▶ Because anyio was not found in the package registry and your project depends on anyio, we can conclude that your project's requirements are unsatisfiable.
|
||
|
||
hint: An index URL (https://pypi-proxy.fly.dev/basic-auth/simple) could not be queried due to a lack of valid authentication credentials (401 Unauthorized).
|
||
help: If you want to add the package regardless of the failed resolution, provide the `--frozen` flag to skip locking and syncing.
|
||
"
|
||
);
|
||
|
||
// Login to the domain
|
||
uv_snapshot!(context.auth_login()
|
||
.arg("pypi-proxy.fly.dev")
|
||
.arg("--username")
|
||
.arg("public")
|
||
.arg("--password")
|
||
.arg("heron")
|
||
.env(EnvVars::UV_PREVIEW_FEATURES, "native-auth"), @r"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Stored credentials for public@https://pypi-proxy.fly.dev/
|
||
"
|
||
);
|
||
|
||
// Try to add the original package without credentials again. This should use credentials
|
||
// storied in the system keyring.
|
||
uv_snapshot!(context.add().arg("anyio").arg("--default-index").arg("https://public@pypi-proxy.fly.dev/basic-auth/simple")
|
||
.env(EnvVars::UV_PREVIEW_FEATURES, "native-auth"), @r"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 4 packages in [TIME]
|
||
Prepared 3 packages in [TIME]
|
||
Installed 3 packages in [TIME]
|
||
+ anyio==4.3.0
|
||
+ idna==3.6
|
||
+ sniffio==1.3.1
|
||
"
|
||
);
|
||
|
||
// Logout of the domain
|
||
uv_snapshot!(context.auth_logout()
|
||
.arg("pypi-proxy.fly.dev")
|
||
.arg("--username")
|
||
.arg("public")
|
||
.env(EnvVars::UV_PREVIEW_FEATURES, "native-auth"), @r"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Removed credentials for public@https://pypi-proxy.fly.dev/
|
||
"
|
||
);
|
||
|
||
// Authentication should fail again
|
||
uv_snapshot!(context.add().arg("iniconfig").arg("--default-index").arg("https://public@pypi-proxy.fly.dev/basic-auth/simple")
|
||
.env(EnvVars::UV_PREVIEW_FEATURES, "native-auth"), @r"
|
||
success: false
|
||
exit_code: 1
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
× No solution found when resolving dependencies:
|
||
╰─▶ Because iniconfig was not found in the package registry and your project depends on iniconfig, we can conclude that your project's requirements are unsatisfiable.
|
||
|
||
hint: An index URL (https://pypi-proxy.fly.dev/basic-auth/simple) could not be queried due to a lack of valid authentication credentials (401 Unauthorized).
|
||
help: If you want to add the package regardless of the failed resolution, provide the `--frozen` flag to skip locking and syncing.
|
||
"
|
||
);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
#[cfg(feature = "native-auth")]
|
||
fn add_package_native_auth() -> Result<()> {
|
||
let context = TestContext::new("3.12").with_real_home();
|
||
|
||
// Clear state before the test
|
||
context
|
||
.auth_logout()
|
||
.arg("https://pypi-proxy.fly.dev/basic-auth/simple")
|
||
.arg("--username")
|
||
.arg("public")
|
||
.env(EnvVars::UV_PREVIEW_FEATURES, "native-auth")
|
||
.status()?;
|
||
|
||
// Configure `pyproject.toml` with native keyring provider.
|
||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||
pyproject_toml.write_str(indoc::indoc! { r#"
|
||
[project]
|
||
name = "foo"
|
||
version = "1.0.0"
|
||
requires-python = ">=3.11, <4"
|
||
dependencies = []
|
||
"#
|
||
})?;
|
||
|
||
// Try to add a package without credentials.
|
||
uv_snapshot!(context.add().arg("anyio").arg("--default-index").arg("https://public@pypi-proxy.fly.dev/basic-auth/simple")
|
||
.env(EnvVars::UV_PREVIEW_FEATURES, "native-auth"), @r"
|
||
success: false
|
||
exit_code: 1
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
× No solution found when resolving dependencies:
|
||
╰─▶ Because anyio was not found in the package registry and your project depends on anyio, we can conclude that your project's requirements are unsatisfiable.
|
||
|
||
hint: An index URL (https://pypi-proxy.fly.dev/basic-auth/simple) could not be queried due to a lack of valid authentication credentials (401 Unauthorized).
|
||
help: If you want to add the package regardless of the failed resolution, provide the `--frozen` flag to skip locking and syncing.
|
||
"
|
||
);
|
||
|
||
// Login to the index
|
||
uv_snapshot!(context.auth_login()
|
||
.arg("https://pypi-proxy.fly.dev/basic-auth/simple")
|
||
.arg("--username")
|
||
.arg("public")
|
||
.arg("--password")
|
||
.arg("heron")
|
||
.env(EnvVars::UV_PREVIEW_FEATURES, "native-auth"), @r"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Stored credentials for public@https://pypi-proxy.fly.dev/basic-auth
|
||
"
|
||
);
|
||
|
||
// Try to add the original package without credentials again. This should use
|
||
// credentials storied in the system keyring.
|
||
uv_snapshot!(context.add().arg("anyio").arg("--default-index").arg("https://public@pypi-proxy.fly.dev/basic-auth/simple")
|
||
.env(EnvVars::UV_PREVIEW_FEATURES, "native-auth"), @r"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Resolved 4 packages in [TIME]
|
||
Prepared 3 packages in [TIME]
|
||
Installed 3 packages in [TIME]
|
||
+ anyio==4.3.0
|
||
+ idna==3.6
|
||
+ sniffio==1.3.1
|
||
"
|
||
);
|
||
|
||
// Logout of the index
|
||
uv_snapshot!(context.auth_logout()
|
||
.arg("https://pypi-proxy.fly.dev/basic-auth/simple")
|
||
.arg("--username")
|
||
.arg("public")
|
||
.env(EnvVars::UV_PREVIEW_FEATURES, "native-auth"), @r"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Removed credentials for public@https://pypi-proxy.fly.dev/basic-auth
|
||
"
|
||
);
|
||
|
||
// Authentication should fail again
|
||
uv_snapshot!(context.add().arg("iniconfig").arg("--default-index").arg("https://public@pypi-proxy.fly.dev/basic-auth/simple")
|
||
.env(EnvVars::UV_PREVIEW_FEATURES, "native-auth"), @r"
|
||
success: false
|
||
exit_code: 1
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
× No solution found when resolving dependencies:
|
||
╰─▶ Because iniconfig was not found in the package registry and your project depends on iniconfig, we can conclude that your project's requirements are unsatisfiable.
|
||
|
||
hint: An index URL (https://pypi-proxy.fly.dev/basic-auth/simple) could not be queried due to a lack of valid authentication credentials (401 Unauthorized).
|
||
help: If you want to add the package regardless of the failed resolution, provide the `--frozen` flag to skip locking and syncing.
|
||
"
|
||
);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
#[cfg(feature = "native-auth")]
|
||
fn token_native_auth() -> Result<()> {
|
||
let context = TestContext::new_with_versions(&[]).with_real_home();
|
||
|
||
// Clear state before the test
|
||
context
|
||
.auth_logout()
|
||
.arg("https://pypi-proxy.fly.dev/basic-auth/simple")
|
||
.arg("--username")
|
||
.arg("public")
|
||
.env(EnvVars::UV_PREVIEW_FEATURES, "native-auth")
|
||
.status()?;
|
||
|
||
// Without persisted credentials
|
||
uv_snapshot!(context.auth_token()
|
||
.arg("https://pypi-proxy.fly.dev/basic-auth/simple")
|
||
.env(EnvVars::UV_PREVIEW_FEATURES, "native-auth"), @r"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
error: Failed to fetch credentials for https://pypi-proxy.fly.dev/basic-auth/simple
|
||
");
|
||
|
||
// Without persisted credentials (with a username in the request)
|
||
uv_snapshot!(context.auth_token()
|
||
.arg("https://pypi-proxy.fly.dev/basic-auth/simple")
|
||
.arg("--username")
|
||
.arg("public")
|
||
.env(EnvVars::UV_PREVIEW_FEATURES, "native-auth"), @r"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
error: Failed to fetch credentials for public@https://pypi-proxy.fly.dev/basic-auth/simple
|
||
");
|
||
|
||
// Login to the index
|
||
uv_snapshot!(context.auth_login()
|
||
.arg("https://pypi-proxy.fly.dev/basic-auth/simple")
|
||
.arg("--username")
|
||
.arg("public")
|
||
.arg("--password")
|
||
.arg("heron")
|
||
.env(EnvVars::UV_PREVIEW_FEATURES, "native-auth"), @r"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Stored credentials for public@https://pypi-proxy.fly.dev/basic-auth
|
||
"
|
||
);
|
||
|
||
// Show the credentials
|
||
uv_snapshot!(context.auth_token()
|
||
.arg("https://pypi-proxy.fly.dev/basic-auth/simple")
|
||
.arg("--username")
|
||
.arg("public")
|
||
.env(EnvVars::UV_PREVIEW_FEATURES, "native-auth"), @r"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
heron
|
||
|
||
----- stderr -----
|
||
");
|
||
|
||
// Without the username
|
||
// TODO(zanieb): Add a hint here if we can?
|
||
uv_snapshot!(context.auth_token()
|
||
.arg("https://pypi-proxy.fly.dev/basic-auth/simple")
|
||
.env(EnvVars::UV_PREVIEW_FEATURES, "native-auth"), @r"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
error: Failed to fetch credentials for https://pypi-proxy.fly.dev/basic-auth/simple
|
||
");
|
||
|
||
// With a mismatched username
|
||
// TODO(zanieb): Add a hint here if we can?
|
||
uv_snapshot!(context.auth_token()
|
||
.arg("https://pypi-proxy.fly.dev/basic-auth/simple")
|
||
.arg("--username")
|
||
.arg("private")
|
||
.env(EnvVars::UV_PREVIEW_FEATURES, "native-auth"), @r"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
error: Failed to fetch credentials for private@https://pypi-proxy.fly.dev/basic-auth/simple
|
||
");
|
||
|
||
// Login to the index with a token
|
||
uv_snapshot!(context.auth_login()
|
||
.arg("https://pypi-proxy.fly.dev/basic-auth/simple")
|
||
.arg("--token")
|
||
.arg("heron")
|
||
.env(EnvVars::UV_PREVIEW_FEATURES, "native-auth"), @r"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Stored credentials for https://pypi-proxy.fly.dev/basic-auth
|
||
"
|
||
);
|
||
|
||
// Retrieve the token without a username
|
||
uv_snapshot!(context.auth_token()
|
||
.arg("https://pypi-proxy.fly.dev/basic-auth/simple")
|
||
.env(EnvVars::UV_PREVIEW_FEATURES, "native-auth"), @r"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
heron
|
||
|
||
----- stderr -----
|
||
");
|
||
|
||
context
|
||
.auth_logout()
|
||
.arg("https://pypi-proxy.fly.dev/basic-auth/simple")
|
||
.arg("--username")
|
||
.arg("public")
|
||
.status()?;
|
||
|
||
// Retrieve token using URL with embedded username (no --username needed)
|
||
uv_snapshot!(context.auth_token()
|
||
.arg("https://public@pypi-proxy.fly.dev/basic-auth/simple")
|
||
.env(EnvVars::UV_PREVIEW_FEATURES, "native-auth"), @r"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
heron
|
||
|
||
----- stderr -----
|
||
");
|
||
|
||
// Conflict between --username and URL username is rejected
|
||
uv_snapshot!(context.auth_token()
|
||
.arg("https://public@pypi-proxy.fly.dev/basic-auth/simple")
|
||
.arg("--username")
|
||
.arg("different")
|
||
.env(EnvVars::UV_PREVIEW_FEATURES, "native-auth"), @r"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
error: Cannot specify a username both via the URL and CLI; found `--username different` and `public`
|
||
");
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
#[cfg(feature = "native-auth")]
|
||
fn token_native_auth_realm() -> Result<()> {
|
||
let context = TestContext::new_with_versions(&[]).with_real_home();
|
||
|
||
// Clear state before the test
|
||
context
|
||
.auth_logout()
|
||
.arg("pypi-proxy.fly.dev")
|
||
.arg("--username")
|
||
.arg("public")
|
||
.env(EnvVars::UV_PREVIEW_FEATURES, "native-auth")
|
||
.status()?;
|
||
context
|
||
.auth_logout()
|
||
.arg("https://pypi-proxy.fly.dev/basic-auth/simple")
|
||
.arg("--username")
|
||
.arg("public")
|
||
.env(EnvVars::UV_PREVIEW_FEATURES, "native-auth")
|
||
.status()?;
|
||
context
|
||
.auth_logout()
|
||
.arg("pypi-proxy.fly.dev")
|
||
.env(EnvVars::UV_PREVIEW_FEATURES, "native-auth")
|
||
.status()?;
|
||
|
||
// Without persisted credentials
|
||
uv_snapshot!(context.auth_token()
|
||
.arg("pypi-proxy.fly.dev")
|
||
.env(EnvVars::UV_PREVIEW_FEATURES, "native-auth"), @r"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
error: Failed to fetch credentials for https://pypi-proxy.fly.dev/
|
||
");
|
||
|
||
// Without persisted credentials (with a username in the request)
|
||
uv_snapshot!(context.auth_token()
|
||
.arg("pypi-proxy.fly.dev")
|
||
.arg("--username")
|
||
.arg("public")
|
||
.env(EnvVars::UV_PREVIEW_FEATURES, "native-auth"), @r"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
error: Failed to fetch credentials for public@https://pypi-proxy.fly.dev/
|
||
");
|
||
|
||
// Login to the index
|
||
uv_snapshot!(context.auth_login()
|
||
.arg("pypi-proxy.fly.dev")
|
||
.arg("--username")
|
||
.arg("public")
|
||
.arg("--password")
|
||
.arg("heron")
|
||
.env(EnvVars::UV_PREVIEW_FEATURES, "native-auth"), @r"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Stored credentials for public@https://pypi-proxy.fly.dev/
|
||
"
|
||
);
|
||
|
||
// Show the credentials
|
||
uv_snapshot!(context.auth_token()
|
||
.arg("pypi-proxy.fly.dev")
|
||
.arg("--username")
|
||
.arg("public")
|
||
.env(EnvVars::UV_PREVIEW_FEATURES, "native-auth"), @r"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
heron
|
||
|
||
----- stderr -----
|
||
");
|
||
|
||
// Show the credentials for a child URL
|
||
uv_snapshot!(context.auth_token()
|
||
.arg("https://pypi-proxy.fly.dev/basic-auth/simple")
|
||
.arg("--username")
|
||
.arg("public")
|
||
.env(EnvVars::UV_PREVIEW_FEATURES, "native-auth"), @r"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
heron
|
||
|
||
----- stderr -----
|
||
");
|
||
|
||
// Without the username (defaults to __token__ which wasn't stored)
|
||
uv_snapshot!(context.auth_token()
|
||
.arg("pypi-proxy.fly.dev")
|
||
.env(EnvVars::UV_PREVIEW_FEATURES, "native-auth"), @r"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
error: Failed to fetch credentials for https://pypi-proxy.fly.dev/
|
||
");
|
||
|
||
// Without the username (defaults to __token__ which wasn't stored)
|
||
uv_snapshot!(context.auth_token()
|
||
.arg("https://pypi-proxy.fly.dev/basic-auth/simple")
|
||
.env(EnvVars::UV_PREVIEW_FEATURES, "native-auth"), @r"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
error: Failed to fetch credentials for https://pypi-proxy.fly.dev/basic-auth/simple
|
||
");
|
||
|
||
// With a mismatched username
|
||
// TODO(zanieb): Add a hint here if we can?
|
||
uv_snapshot!(context.auth_token()
|
||
.arg("pypi-proxy.fly.dev")
|
||
.arg("--username")
|
||
.arg("private")
|
||
.env(EnvVars::UV_PREVIEW_FEATURES, "native-auth"), @r"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
error: Failed to fetch credentials for private@https://pypi-proxy.fly.dev/
|
||
");
|
||
|
||
// With a mismatched port
|
||
uv_snapshot!(context.auth_token()
|
||
.arg("https://pypi-proxy.fly.dev:1000")
|
||
.arg("--username")
|
||
.arg("public")
|
||
.env(EnvVars::UV_PREVIEW_FEATURES, "native-auth"), @r"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
error: Failed to fetch credentials for public@https://pypi-proxy.fly.dev:1000/
|
||
");
|
||
|
||
// Login to the index with a token
|
||
uv_snapshot!(context.auth_login()
|
||
.arg("pypi-proxy.fly.dev")
|
||
.arg("--token")
|
||
.arg("heron")
|
||
.env(EnvVars::UV_PREVIEW_FEATURES, "native-auth"), @r"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Stored credentials for https://pypi-proxy.fly.dev/
|
||
"
|
||
);
|
||
|
||
// Retrieve the token without a username
|
||
uv_snapshot!(context.auth_token()
|
||
.arg("pypi-proxy.fly.dev")
|
||
.env(EnvVars::UV_PREVIEW_FEATURES, "native-auth"), @r"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
heron
|
||
|
||
----- stderr -----
|
||
");
|
||
|
||
context
|
||
.auth_logout()
|
||
.arg("pypi-proxy.fly.dev")
|
||
.arg("--username")
|
||
.arg("public")
|
||
.status()?;
|
||
|
||
// Retrieve token using URL with embedded username (no --username needed)
|
||
uv_snapshot!(context.auth_token()
|
||
.arg("https://public@pypi-proxy.fly.dev/basic-auth/simple")
|
||
.env(EnvVars::UV_PREVIEW_FEATURES, "native-auth"), @r"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
heron
|
||
|
||
----- stderr -----
|
||
");
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
#[cfg(feature = "native-auth")]
|
||
fn login_native_auth() -> Result<()> {
|
||
let context = TestContext::new_with_versions(&[]).with_real_home();
|
||
|
||
// Clear state before the test
|
||
context
|
||
.auth_logout()
|
||
.arg("https://pypi-proxy.fly.dev/basic-auth/simple")
|
||
.arg("--username")
|
||
.arg("public")
|
||
.env(EnvVars::UV_PREVIEW_FEATURES, "native-auth")
|
||
.status()?;
|
||
|
||
// Without a service name
|
||
uv_snapshot!(context.auth_login(), @r"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
error: the following required arguments were not provided:
|
||
<SERVICE>
|
||
|
||
Usage: uv auth login --cache-dir [CACHE_DIR] <SERVICE>
|
||
|
||
For more information, try '--help'.
|
||
");
|
||
|
||
// Without a username (or token)
|
||
uv_snapshot!(context.auth_login()
|
||
.arg("https://pypi-proxy.fly.dev/basic-auth/simple")
|
||
.env(EnvVars::UV_PREVIEW_FEATURES, "native-auth"), @r"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
error: No username provided; did you mean to provide `--username` or `--token`?
|
||
");
|
||
|
||
// Without a password
|
||
uv_snapshot!(context.auth_login()
|
||
.arg("https://pypi-proxy.fly.dev/basic-auth/simple")
|
||
.arg("--username")
|
||
.arg("public")
|
||
.env(EnvVars::UV_PREVIEW_FEATURES, "native-auth"), @r"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
error: No password provided; did you mean to provide `--password` or `--token`?
|
||
");
|
||
|
||
// Successful
|
||
uv_snapshot!(context.auth_login()
|
||
.arg("https://pypi-proxy.fly.dev/basic-auth/simple")
|
||
.arg("--username")
|
||
.arg("public")
|
||
.arg("--password")
|
||
.arg("heron")
|
||
.env(EnvVars::UV_PREVIEW_FEATURES, "native-auth"), @r"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Stored credentials for public@https://pypi-proxy.fly.dev/basic-auth
|
||
"
|
||
);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
#[cfg(feature = "native-auth")]
|
||
fn login_token_native_auth() -> Result<()> {
|
||
let context = TestContext::new_with_versions(&[]).with_real_home();
|
||
|
||
// Clear state before the test
|
||
context
|
||
.auth_logout()
|
||
.arg("https://pypi-proxy.fly.dev/basic-auth/simple")
|
||
.arg("--username")
|
||
.arg("__token__")
|
||
.env(EnvVars::UV_PREVIEW_FEATURES, "native-auth")
|
||
.status()?;
|
||
|
||
// Successful with token
|
||
uv_snapshot!(context.auth_login()
|
||
.arg("https://pypi-proxy.fly.dev/basic-auth/simple")
|
||
.arg("--token")
|
||
.arg("test-token")
|
||
.env(EnvVars::UV_PREVIEW_FEATURES, "native-auth"), @r"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Stored credentials for https://pypi-proxy.fly.dev/basic-auth
|
||
"
|
||
);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
#[cfg(feature = "native-auth")]
|
||
fn logout_native_auth() -> Result<()> {
|
||
let context = TestContext::new_with_versions(&[]).with_real_home();
|
||
|
||
// Clear state before the test
|
||
context
|
||
.auth_logout()
|
||
.arg("https://pypi-proxy.fly.dev/basic-auth/simple")
|
||
.arg("--username")
|
||
.arg("public")
|
||
.env(EnvVars::UV_PREVIEW_FEATURES, "native-auth")
|
||
.status()?;
|
||
context
|
||
.auth_logout()
|
||
.arg("https://pypi-proxy.fly.dev/basic-auth/simple")
|
||
.env(EnvVars::UV_PREVIEW_FEATURES, "native-auth")
|
||
.status()?;
|
||
|
||
// Without a service name
|
||
uv_snapshot!(context.auth_logout(), @r"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
error: the following required arguments were not provided:
|
||
<SERVICE>
|
||
|
||
Usage: uv auth logout --cache-dir [CACHE_DIR] <SERVICE>
|
||
|
||
For more information, try '--help'.
|
||
");
|
||
|
||
// Logout before logging in
|
||
uv_snapshot!(context.auth_logout()
|
||
.arg("https://pypi-proxy.fly.dev/basic-auth/simple")
|
||
.env(EnvVars::UV_PREVIEW_FEATURES, "native-auth"), @r"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
error: Unable to remove credentials for https://pypi-proxy.fly.dev/basic-auth
|
||
Caused by: No matching entry found in secure storage
|
||
");
|
||
|
||
// Logout before logging in (with a username)
|
||
uv_snapshot!(context.auth_logout()
|
||
.arg("https://pypi-proxy.fly.dev/basic-auth/simple")
|
||
.arg("--username")
|
||
.arg("public")
|
||
.env(EnvVars::UV_PREVIEW_FEATURES, "native-auth"), @r"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
error: Unable to remove credentials for public@https://pypi-proxy.fly.dev/basic-auth
|
||
Caused by: No matching entry found in secure storage
|
||
");
|
||
|
||
// Login with a username
|
||
uv_snapshot!(context.auth_login()
|
||
.arg("https://pypi-proxy.fly.dev/basic-auth/simple")
|
||
.arg("--username")
|
||
.arg("public")
|
||
.arg("--password")
|
||
.arg("heron")
|
||
.env(EnvVars::UV_PREVIEW_FEATURES, "native-auth"), @r"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Stored credentials for public@https://pypi-proxy.fly.dev/basic-auth
|
||
"
|
||
);
|
||
|
||
// Logout without a username
|
||
// TODO(zanieb): Add a hint here if we can?
|
||
uv_snapshot!(context.auth_logout()
|
||
.arg("https://pypi-proxy.fly.dev/basic-auth/simple")
|
||
.env(EnvVars::UV_PREVIEW_FEATURES, "native-auth"), @r"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
error: Unable to remove credentials for https://pypi-proxy.fly.dev/basic-auth
|
||
Caused by: No matching entry found in secure storage
|
||
");
|
||
|
||
// Logout with a username
|
||
uv_snapshot!(context.auth_logout()
|
||
.arg("https://pypi-proxy.fly.dev/basic-auth/simple")
|
||
.arg("--username")
|
||
.arg("public")
|
||
.env(EnvVars::UV_PREVIEW_FEATURES, "native-auth"), @r"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Removed credentials for public@https://pypi-proxy.fly.dev/basic-auth
|
||
");
|
||
|
||
// Login again
|
||
context
|
||
.auth_login()
|
||
.arg("https://pypi-proxy.fly.dev/basic-auth/simple")
|
||
.arg("--username")
|
||
.arg("public")
|
||
.arg("--password")
|
||
.arg("heron")
|
||
.env(EnvVars::UV_PREVIEW_FEATURES, "native-auth")
|
||
.assert()
|
||
.success();
|
||
|
||
// Logout with a username in the URL
|
||
uv_snapshot!(context.auth_logout()
|
||
.arg("https://public@pypi-proxy.fly.dev/basic-auth/simple")
|
||
.env(EnvVars::UV_PREVIEW_FEATURES, "native-auth"), @r"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Removed credentials for public@https://pypi-proxy.fly.dev/basic-auth
|
||
");
|
||
|
||
// Conflict between --username and a URL username is rejected
|
||
uv_snapshot!(context.auth_logout()
|
||
.arg("https://public@pypi-proxy.fly.dev/basic-auth/simple")
|
||
.arg("--username")
|
||
.arg("foo")
|
||
.env(EnvVars::UV_PREVIEW_FEATURES, "native-auth"), @r"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
error: Cannot specify a username both via the URL and CLI; found `--username foo` and `public`
|
||
");
|
||
|
||
// Conflict between --token and a URL username is rejected
|
||
uv_snapshot!(context.auth_login()
|
||
.arg("https://public@pypi-proxy.fly.dev/basic-auth/simple")
|
||
.arg("--token")
|
||
.arg("foo")
|
||
.env(EnvVars::UV_PREVIEW_FEATURES, "native-auth"), @r"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
error: When using `--token`, a username cannot not be provided; found: public
|
||
");
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
#[cfg(feature = "native-auth")]
|
||
fn logout_token_native_auth() -> Result<()> {
|
||
let context = TestContext::new_with_versions(&[]).with_real_home();
|
||
|
||
// Clear state before the test
|
||
context
|
||
.auth_logout()
|
||
.arg("https://pypi-proxy.fly.dev/basic-auth/simple")
|
||
.env(EnvVars::UV_PREVIEW_FEATURES, "native-auth")
|
||
.status()?;
|
||
|
||
// Login with a token
|
||
uv_snapshot!(context.auth_login()
|
||
.arg("https://pypi-proxy.fly.dev/basic-auth/simple")
|
||
.arg("--token")
|
||
.arg("test-token")
|
||
.env(EnvVars::UV_PREVIEW_FEATURES, "native-auth"), @r"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Stored credentials for https://pypi-proxy.fly.dev/basic-auth
|
||
"
|
||
);
|
||
|
||
// Logout without a username
|
||
uv_snapshot!(context.auth_logout()
|
||
.arg("https://pypi-proxy.fly.dev/basic-auth/simple")
|
||
.env(EnvVars::UV_PREVIEW_FEATURES, "native-auth"), @r"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Removed credentials for https://pypi-proxy.fly.dev/basic-auth
|
||
");
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
#[cfg(feature = "native-auth")]
|
||
fn login_native_auth_url() {
|
||
let context = TestContext::new_with_versions(&[]).with_real_home();
|
||
|
||
// A domain-only service name gets https:// prepended
|
||
uv_snapshot!(context.auth_login()
|
||
.arg("example.com")
|
||
.arg("--username")
|
||
.arg("test")
|
||
.arg("--password")
|
||
.arg("test")
|
||
.env(EnvVars::UV_PREVIEW_FEATURES, "native-auth"), @r"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Stored credentials for test@https://example.com/
|
||
");
|
||
|
||
// HTTP URLs are not allowed - only HTTPS
|
||
uv_snapshot!(context.auth_login()
|
||
.arg("http://example.com")
|
||
.arg("--username")
|
||
.arg("test")
|
||
.arg("--password")
|
||
.arg("test")
|
||
.env(EnvVars::UV_PREVIEW_FEATURES, "native-auth"), @r"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
error: invalid value 'http://example.com' for '<SERVICE>': HTTPS is required for non-local hosts
|
||
|
||
For more information, try '--help'.
|
||
");
|
||
|
||
// HTTP URLs are fine for localhost
|
||
uv_snapshot!(context.auth_login()
|
||
.arg("http://localhost:1324")
|
||
.arg("--username")
|
||
.arg("test")
|
||
.arg("--password")
|
||
.arg("test")
|
||
.env(EnvVars::UV_PREVIEW_FEATURES, "native-auth"), @r"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Stored credentials for test@http://localhost:1324/
|
||
");
|
||
|
||
uv_snapshot!(context.auth_login()
|
||
.arg("https://example.com")
|
||
.arg("--username")
|
||
.arg("test")
|
||
.arg("--password")
|
||
.arg("test")
|
||
.env(EnvVars::UV_PREVIEW_FEATURES, "native-auth"), @r"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Stored credentials for test@https://example.com/
|
||
");
|
||
|
||
// A domain-only service with a path also gets https:// prepended
|
||
uv_snapshot!(context.auth_login()
|
||
.arg("example.com/simple")
|
||
.arg("--username")
|
||
.arg("test")
|
||
.arg("--password")
|
||
.arg("test")
|
||
.env(EnvVars::UV_PREVIEW_FEATURES, "native-auth"), @r"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Stored credentials for test@https://example.com/
|
||
");
|
||
|
||
// An invalid URL is rejected
|
||
uv_snapshot!(context.auth_login()
|
||
.arg("not a valid url")
|
||
.arg("--username")
|
||
.arg("test")
|
||
.arg("--password")
|
||
.arg("test")
|
||
.env(EnvVars::UV_PREVIEW_FEATURES, "native-auth"), @r"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
error: invalid value 'not a valid url' for '<SERVICE>': invalid international domain name
|
||
|
||
For more information, try '--help'.
|
||
");
|
||
|
||
// URL with embedded credentials works
|
||
uv_snapshot!(context.auth_login()
|
||
.arg("https://test:password@example.com/simple")
|
||
.env(EnvVars::UV_PREVIEW_FEATURES, "native-auth"), @r"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Stored credentials for test@https://example.com/
|
||
");
|
||
|
||
// URL with embedded username and separate password works
|
||
uv_snapshot!(context.auth_login()
|
||
.arg("https://test@example.com/simple")
|
||
.arg("--password")
|
||
.arg("password")
|
||
.env(EnvVars::UV_PREVIEW_FEATURES, "native-auth"), @r"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Stored credentials for test@https://example.com/
|
||
");
|
||
|
||
// Conflict between --username and URL username is rejected
|
||
uv_snapshot!(context.auth_login()
|
||
.arg("https://test@example.com/simple")
|
||
.arg("--username")
|
||
.arg("different")
|
||
.arg("--password")
|
||
.arg("password")
|
||
.env(EnvVars::UV_PREVIEW_FEATURES, "native-auth"), @r"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
error: Cannot specify a username both via the URL and CLI; found `--username different` and `test`
|
||
");
|
||
|
||
// Conflict between --password and URL password is rejected
|
||
uv_snapshot!(context.auth_login()
|
||
.arg("https://test:password@example.com/simple")
|
||
.arg("--password")
|
||
.arg("different")
|
||
.env(EnvVars::UV_PREVIEW_FEATURES, "native-auth"), @r"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
error: Cannot specify a password both via the URL and CLI
|
||
");
|
||
|
||
// Conflict between --token and URL credentials is rejected
|
||
uv_snapshot!(context.auth_login()
|
||
.arg("https://test:password@example.com/simple")
|
||
.arg("--token")
|
||
.arg("some-token")
|
||
.env(EnvVars::UV_PREVIEW_FEATURES, "native-auth"), @r"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
error: When using `--token`, a username cannot not be provided; found: test
|
||
");
|
||
}
|
||
|
||
#[test]
|
||
fn login_text_store() {
|
||
let context = TestContext::new_with_versions(&[]);
|
||
|
||
// Login with a username and password
|
||
uv_snapshot!(context.auth_login()
|
||
.arg("https://pypi-proxy.fly.dev/basic-auth/simple")
|
||
.arg("--username")
|
||
.arg("public")
|
||
.arg("--password")
|
||
.arg("heron"), @"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Stored credentials for public@https://pypi-proxy.fly.dev/basic-auth
|
||
"
|
||
);
|
||
|
||
// Login with a token
|
||
uv_snapshot!(context.auth_login()
|
||
.arg("https://example.com/simple")
|
||
.arg("--token")
|
||
.arg("test-token"), @"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Stored credentials for https://example.com/
|
||
"
|
||
);
|
||
|
||
// Empty username should fail
|
||
uv_snapshot!(context.auth_login()
|
||
.arg("https://example.com/simple")
|
||
.arg("--username")
|
||
.arg("")
|
||
.arg("--password")
|
||
.arg("testpass"), @"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
error: Username cannot be empty
|
||
"
|
||
);
|
||
|
||
// Empty password should fail
|
||
uv_snapshot!(context.auth_login()
|
||
.arg("https://example.com/simple")
|
||
.arg("--username")
|
||
.arg("testuser")
|
||
.arg("--password")
|
||
.arg(""), @"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
error: Password cannot be empty
|
||
"
|
||
);
|
||
|
||
// HTTP should fail
|
||
uv_snapshot!(context.auth_login()
|
||
.arg("http://example.com/simple")
|
||
.arg("--username")
|
||
.arg("testuser")
|
||
.arg("--password")
|
||
.arg("testpass"), @"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
error: invalid value 'http://example.com/simple' for '<SERVICE>': HTTPS is required for non-local hosts
|
||
|
||
For more information, try '--help'.
|
||
");
|
||
|
||
// Other protocol should fail
|
||
uv_snapshot!(context.auth_login()
|
||
.arg("ftp://example.com/simple")
|
||
.arg("--username")
|
||
.arg("testuser")
|
||
.arg("--password")
|
||
.arg("testpass"), @"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
error: invalid value 'ftp://example.com/simple' for '<SERVICE>': Unsupported scheme: ftp
|
||
|
||
For more information, try '--help'.
|
||
");
|
||
|
||
// HTTP should be allowed on localhost
|
||
uv_snapshot!(context.auth_login()
|
||
.arg("http://127.0.0.1/simple")
|
||
.arg("--username")
|
||
.arg("testuser")
|
||
.arg("--password")
|
||
.arg("testpass"), @"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Stored credentials for testuser@http://127.0.0.1/
|
||
");
|
||
|
||
// HTTP should be allowed on localhost
|
||
uv_snapshot!(context.auth_login()
|
||
.arg("http://localhost/simple")
|
||
.arg("--username")
|
||
.arg("testuser")
|
||
.arg("--password")
|
||
.arg("testpass"), @"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Stored credentials for testuser@http://localhost/
|
||
");
|
||
}
|
||
|
||
#[test]
|
||
#[expect(clippy::disallowed_types)]
|
||
fn login_password_stdin() -> Result<()> {
|
||
let context = TestContext::new_with_versions(&[]);
|
||
|
||
// Create a temporary file with the password
|
||
let password_file = context.temp_dir.child("password.txt");
|
||
password_file.write_str("secret-password")?;
|
||
|
||
// Login with password from stdin
|
||
uv_snapshot!(context.auth_login()
|
||
.arg("https://example.com/simple")
|
||
.arg("--username")
|
||
.arg("testuser")
|
||
.arg("--password")
|
||
.arg("-")
|
||
.stdin(std::fs::File::open(password_file)?), @"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Stored credentials for testuser@https://example.com/
|
||
"
|
||
);
|
||
|
||
// Verify the credentials work by retrieving the token
|
||
uv_snapshot!(context.auth_token()
|
||
.arg("https://example.com/simple")
|
||
.arg("--username")
|
||
.arg("testuser"), @"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
secret-password
|
||
|
||
----- stderr -----
|
||
"
|
||
);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
#[expect(clippy::disallowed_types)]
|
||
fn login_token_stdin() -> Result<()> {
|
||
let context = TestContext::new_with_versions(&[]);
|
||
|
||
// Create a temporary file with the token
|
||
let token_file = context.temp_dir.child("token.txt");
|
||
token_file.write_str("secret-token")?;
|
||
|
||
// Login with token from stdin
|
||
uv_snapshot!(context.auth_login()
|
||
.arg("https://example.com/simple")
|
||
.arg("--token")
|
||
.arg("-")
|
||
.stdin(std::fs::File::open(token_file)?), @"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Stored credentials for https://example.com/
|
||
"
|
||
);
|
||
|
||
// Verify the credentials work by retrieving the token
|
||
uv_snapshot!(context.auth_token()
|
||
.arg("https://example.com/simple"), @"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
secret-token
|
||
|
||
----- stderr -----
|
||
"
|
||
);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
fn token_text_store() {
|
||
let context = TestContext::new_with_versions(&[]);
|
||
|
||
// Login first
|
||
context
|
||
.auth_login()
|
||
.arg("https://pypi-proxy.fly.dev/basic-auth/simple")
|
||
.arg("--username")
|
||
.arg("public")
|
||
.arg("--password")
|
||
.arg("heron")
|
||
.assert()
|
||
.success();
|
||
|
||
// Retrieve the token
|
||
uv_snapshot!(context.auth_token()
|
||
.arg("https://pypi-proxy.fly.dev/basic-auth/simple")
|
||
.arg("--username")
|
||
.arg("public"), @"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
heron
|
||
|
||
----- stderr -----
|
||
"
|
||
);
|
||
|
||
// Login with token
|
||
context
|
||
.auth_login()
|
||
.arg("https://example.com/simple")
|
||
.arg("--token")
|
||
.arg("test-token")
|
||
.assert()
|
||
.success();
|
||
|
||
// Retrieve token without username
|
||
uv_snapshot!(context.auth_token()
|
||
.arg("https://example.com/simple"), @"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
test-token
|
||
|
||
----- stderr -----
|
||
"
|
||
);
|
||
|
||
// Empty username should fail
|
||
uv_snapshot!(context.auth_token()
|
||
.arg("https://example.com/simple")
|
||
.arg("--username")
|
||
.arg(""), @"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
error: Username cannot be empty
|
||
"
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn logout_text_store() {
|
||
let context = TestContext::new_with_versions(&[]);
|
||
|
||
// Login first
|
||
context
|
||
.auth_login()
|
||
.arg("https://pypi-proxy.fly.dev/basic-auth/simple")
|
||
.arg("--username")
|
||
.arg("public")
|
||
.arg("--password")
|
||
.arg("heron")
|
||
.assert()
|
||
.success();
|
||
|
||
// Logout
|
||
uv_snapshot!(context.auth_logout()
|
||
.arg("https://pypi-proxy.fly.dev/basic-auth/simple")
|
||
.arg("--username")
|
||
.arg("public"), @"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Removed credentials for public@https://pypi-proxy.fly.dev/basic-auth
|
||
"
|
||
);
|
||
|
||
// Login with token then logout
|
||
context
|
||
.auth_login()
|
||
.arg("https://example.com/simple")
|
||
.arg("--token")
|
||
.arg("test-token")
|
||
.assert()
|
||
.success();
|
||
|
||
uv_snapshot!(context.auth_logout()
|
||
.arg("https://example.com/simple"), @"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Removed credentials for https://example.com/
|
||
"
|
||
);
|
||
|
||
// Empty username should fail
|
||
uv_snapshot!(context.auth_logout()
|
||
.arg("https://example.com/simple")
|
||
.arg("--username")
|
||
.arg(""), @"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
error: Username cannot be empty
|
||
"
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn auth_disabled_provider_uses_text_store() {
|
||
let context = TestContext::new_with_versions(&[]);
|
||
|
||
// Login with disabled provider should use text store
|
||
uv_snapshot!(context.auth_login()
|
||
.arg("https://pypi-proxy.fly.dev/basic-auth/simple")
|
||
.arg("--username")
|
||
.arg("public")
|
||
.arg("--password")
|
||
.arg("heron")
|
||
.arg("--keyring-provider")
|
||
.arg("disabled"), @"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Stored credentials for public@https://pypi-proxy.fly.dev/basic-auth
|
||
"
|
||
);
|
||
|
||
// Token retrieval should work with disabled provider
|
||
uv_snapshot!(context.auth_token()
|
||
.arg("https://pypi-proxy.fly.dev/basic-auth/simple")
|
||
.arg("--username")
|
||
.arg("public")
|
||
.arg("--keyring-provider")
|
||
.arg("disabled"), @"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
heron
|
||
|
||
----- stderr -----
|
||
"
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn login_text_store_strips_simple_suffix() {
|
||
let context = TestContext::new_with_versions(&[]);
|
||
|
||
// Login with `/simple` suffix - should strip it and store credentials for the root URL
|
||
uv_snapshot!(context.auth_login()
|
||
.arg("https://example.com/simple")
|
||
.arg("--username")
|
||
.arg("testuser")
|
||
.arg("--password")
|
||
.arg("testpass"), @"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Stored credentials for testuser@https://example.com/
|
||
"
|
||
);
|
||
|
||
// Login with `/+simple` suffix (devpi format) - should also strip it
|
||
uv_snapshot!(context.auth_login()
|
||
.arg("https://devpi.example.com/root/+simple")
|
||
.arg("--username")
|
||
.arg("devpiuser")
|
||
.arg("--password")
|
||
.arg("devpipass"), @"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Stored credentials for devpiuser@https://devpi.example.com/root
|
||
"
|
||
);
|
||
|
||
// Login with `/Simple` (case insensitive) - should strip it
|
||
uv_snapshot!(context.auth_login()
|
||
.arg("https://registry.example.com/Simple")
|
||
.arg("--username")
|
||
.arg("caseuser")
|
||
.arg("--password")
|
||
.arg("casepass"), @"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Stored credentials for caseuser@https://registry.example.com/
|
||
"
|
||
);
|
||
|
||
// Login without `/simple` suffix - should store as-is
|
||
uv_snapshot!(context.auth_login()
|
||
.arg("https://custom.example.com/api/v1")
|
||
.arg("--username")
|
||
.arg("apiuser")
|
||
.arg("--password")
|
||
.arg("apipass"), @"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Stored credentials for apiuser@https://custom.example.com/api/v1
|
||
"
|
||
);
|
||
|
||
// Login with trailing slash and `/simple` - should strip both
|
||
uv_snapshot!(context.auth_login()
|
||
.arg("https://trailing.example.com/simple/")
|
||
.arg("--username")
|
||
.arg("slashuser")
|
||
.arg("--password")
|
||
.arg("slashpass"), @"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Stored credentials for slashuser@https://trailing.example.com/
|
||
"
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn logout_text_store_strips_simple_suffix() {
|
||
let context = TestContext::new_with_versions(&[]);
|
||
|
||
// Login with `/simple` suffix first
|
||
context
|
||
.auth_login()
|
||
.arg("https://example.com/simple")
|
||
.arg("--username")
|
||
.arg("testuser")
|
||
.arg("--password")
|
||
.arg("testpass")
|
||
.assert()
|
||
.success();
|
||
|
||
// Logout using the same URL with `/simple` - should work
|
||
uv_snapshot!(context.auth_logout()
|
||
.arg("https://example.com/simple")
|
||
.arg("--username")
|
||
.arg("testuser"), @"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Removed credentials for testuser@https://example.com/
|
||
"
|
||
);
|
||
|
||
// Login with `/+simple` suffix
|
||
context
|
||
.auth_login()
|
||
.arg("https://devpi.example.com/root/+simple")
|
||
.arg("--username")
|
||
.arg("devpiuser")
|
||
.arg("--password")
|
||
.arg("devpipass")
|
||
.assert()
|
||
.success();
|
||
|
||
// Logout using URL with `/+simple` - should work
|
||
uv_snapshot!(context.auth_logout()
|
||
.arg("https://devpi.example.com/root/+simple")
|
||
.arg("--username")
|
||
.arg("devpiuser"), @"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Removed credentials for devpiuser@https://devpi.example.com/root
|
||
"
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn token_text_store_strips_simple_suffix() {
|
||
let context = TestContext::new_with_versions(&[]);
|
||
|
||
// Login with `/simple` suffix
|
||
context
|
||
.auth_login()
|
||
.arg("https://example.com/simple")
|
||
.arg("--username")
|
||
.arg("testuser")
|
||
.arg("--password")
|
||
.arg("testpass")
|
||
.assert()
|
||
.success();
|
||
|
||
// Retrieve token using URL with `/simple` - should work
|
||
uv_snapshot!(context.auth_token()
|
||
.arg("https://example.com/simple")
|
||
.arg("--username")
|
||
.arg("testuser"), @"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
testpass
|
||
|
||
----- stderr -----
|
||
"
|
||
);
|
||
|
||
// Login with token and `/simple` suffix
|
||
context
|
||
.auth_login()
|
||
.arg("https://token.example.com/simple")
|
||
.arg("--token")
|
||
.arg("secret-token")
|
||
.assert()
|
||
.success();
|
||
|
||
// Retrieve token using URL with `/simple` - should work
|
||
uv_snapshot!(context.auth_token()
|
||
.arg("https://token.example.com/simple"), @"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
secret-token
|
||
|
||
----- stderr -----
|
||
"
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn token_text_store_username() {
|
||
let context = TestContext::new_with_versions(&[]);
|
||
|
||
// Login with specific username
|
||
context
|
||
.auth_login()
|
||
.arg("https://example.com/simple")
|
||
.arg("--username")
|
||
.arg("testuser")
|
||
.arg("--password")
|
||
.arg("testpass")
|
||
.assert()
|
||
.success();
|
||
|
||
// Retrieve token with matching username
|
||
uv_snapshot!(context.auth_token()
|
||
.arg("https://example.com/simple")
|
||
.arg("--username")
|
||
.arg("testuser"), @"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
testpass
|
||
|
||
----- stderr -----
|
||
"
|
||
);
|
||
|
||
// Retrieve token without username
|
||
uv_snapshot!(context.auth_token()
|
||
.arg("https://example.com/simple"), @"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
error: Failed to fetch credentials for https://example.com/simple
|
||
"
|
||
);
|
||
|
||
// Try to retrieve token with different username - should fail
|
||
uv_snapshot!(context.auth_token()
|
||
.arg("https://example.com/simple")
|
||
.arg("--username")
|
||
.arg("wronguser"), @"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
error: Failed to fetch credentials for wronguser@https://example.com/simple
|
||
"
|
||
);
|
||
|
||
// Login with token (no username)
|
||
context
|
||
.auth_login()
|
||
.arg("https://token.example.com/simple")
|
||
.arg("--token")
|
||
.arg("test-token")
|
||
.assert()
|
||
.success();
|
||
|
||
// Retrieve token without specifying username - should work
|
||
uv_snapshot!(context.auth_token()
|
||
.arg("https://token.example.com/simple"), @"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
test-token
|
||
|
||
----- stderr -----
|
||
"
|
||
);
|
||
|
||
// Login with username at different URL
|
||
context
|
||
.auth_login()
|
||
.arg("https://userexample.com/simple")
|
||
.arg("--username")
|
||
.arg("testuser")
|
||
.arg("--password")
|
||
.arg("testpass")
|
||
.assert()
|
||
.success();
|
||
|
||
// Retrieve token without username should fail
|
||
uv_snapshot!(context.auth_token()
|
||
.arg("https://userexample.com/simple"), @"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
error: Failed to fetch credentials for https://userexample.com/simple
|
||
"
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn logout_text_store_multiple_usernames() {
|
||
let context = TestContext::new_with_versions(&[]);
|
||
|
||
// Login with two different usernames for the same service
|
||
context
|
||
.auth_login()
|
||
.arg("https://example.com/simple")
|
||
.arg("--username")
|
||
.arg("user1")
|
||
.arg("--password")
|
||
.arg("pass1")
|
||
.assert()
|
||
.success();
|
||
|
||
context
|
||
.auth_login()
|
||
.arg("https://example.com/simple")
|
||
.arg("--username")
|
||
.arg("user2")
|
||
.arg("--password")
|
||
.arg("pass2")
|
||
.assert()
|
||
.success();
|
||
|
||
// Logout one specific username
|
||
uv_snapshot!(context.auth_logout()
|
||
.arg("https://example.com/simple")
|
||
.arg("--username")
|
||
.arg("user1"), @"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Removed credentials for user1@https://example.com/
|
||
"
|
||
);
|
||
|
||
// Verify the first user is gone but second remains
|
||
uv_snapshot!(context.auth_token()
|
||
.arg("https://example.com/simple")
|
||
.arg("--username")
|
||
.arg("user1"), @"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
error: Failed to fetch credentials for user1@https://example.com/simple
|
||
"
|
||
);
|
||
|
||
uv_snapshot!(context.auth_token()
|
||
.arg("https://example.com/simple")
|
||
.arg("--username")
|
||
.arg("user2"), @"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
pass2
|
||
|
||
----- stderr -----
|
||
"
|
||
);
|
||
|
||
// Try to logout without specifying username (defaults to `__token__`)
|
||
uv_snapshot!(context.auth_logout()
|
||
.arg("https://example.com/simple"), @"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
error: No matching entry found for https://example.com/
|
||
"
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
#[cfg(feature = "native-auth")]
|
||
fn native_auth_prefix_match() -> Result<()> {
|
||
let context = TestContext::new_with_versions(&[]).with_real_home();
|
||
|
||
// Clear state before the test
|
||
context
|
||
.auth_logout()
|
||
.arg("https://example.com/api")
|
||
.arg("--username")
|
||
.arg("testuser")
|
||
.env(EnvVars::UV_PREVIEW_FEATURES, "native-auth")
|
||
.status()?;
|
||
|
||
// Login with credentials for a path
|
||
uv_snapshot!(context.auth_login()
|
||
.arg("https://example.com/api")
|
||
.arg("--username")
|
||
.arg("testuser")
|
||
.arg("--password")
|
||
.arg("testpass")
|
||
.env(EnvVars::UV_PREVIEW_FEATURES, "native-auth"), @r"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Stored credentials for testuser@https://example.com/api
|
||
"
|
||
);
|
||
|
||
// A request for a child path does not match, the native store does not yet implement prefix
|
||
// matching
|
||
uv_snapshot!(context.auth_token()
|
||
.arg("https://example.com/api/v1")
|
||
.arg("--username")
|
||
.arg("testuser")
|
||
.env(EnvVars::UV_PREVIEW_FEATURES, "native-auth"), @r"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
testpass
|
||
|
||
----- stderr -----
|
||
"
|
||
);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
#[cfg(feature = "native-auth")]
|
||
fn native_auth_host_fallback() -> Result<()> {
|
||
let context = TestContext::new_with_versions(&[]).with_real_home();
|
||
|
||
// Clear state before the test
|
||
context
|
||
.auth_logout()
|
||
.arg("example.com")
|
||
.arg("--username")
|
||
.arg("testuser")
|
||
.env(EnvVars::UV_PREVIEW_FEATURES, "native-auth")
|
||
.status()?;
|
||
|
||
// Login with credentials for the host
|
||
uv_snapshot!(context.auth_login()
|
||
.arg("example.com")
|
||
.arg("--username")
|
||
.arg("testuser")
|
||
.arg("--password")
|
||
.arg("hostpass")
|
||
.env(EnvVars::UV_PREVIEW_FEATURES, "native-auth"), @r"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Stored credentials for testuser@https://example.com/
|
||
"
|
||
);
|
||
|
||
// Should fallback to host-level matching
|
||
uv_snapshot!(context.auth_token()
|
||
.arg("https://example.com/any/path")
|
||
.arg("--username")
|
||
.arg("testuser")
|
||
.env(EnvVars::UV_PREVIEW_FEATURES, "native-auth"), @r"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
hostpass
|
||
|
||
----- stderr -----
|
||
"
|
||
);
|
||
|
||
// A request to another host should not work
|
||
uv_snapshot!(context.auth_token()
|
||
.arg("https://another-example.com/any/path")
|
||
.arg("--username")
|
||
.arg("testuser")
|
||
.env(EnvVars::UV_PREVIEW_FEATURES, "native-auth"), @r"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
error: Failed to fetch credentials for testuser@https://another-example.com/any/path
|
||
"
|
||
);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Test credential helper with basic auth credentials
|
||
#[test]
|
||
fn bazel_helper_basic_auth() {
|
||
let context = TestContext::new("3.12");
|
||
|
||
// Store credentials
|
||
uv_snapshot!(context.filters(), context.auth_login()
|
||
.arg("https://test.example.com")
|
||
.arg("--username").arg("testuser")
|
||
.arg("--password").arg("testpass"), @"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Stored credentials for testuser@https://test.example.com/
|
||
");
|
||
|
||
uv_snapshot!(context.filters(), context.auth_helper()
|
||
.arg("--protocol=bazel")
|
||
.arg("get"),
|
||
input=r#"{"uri":"https://test.example.com/path"}"#,
|
||
@r#"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
{"headers":{"Authorization":["Basic dGVzdHVzZXI6dGVzdHBhc3M="]}}
|
||
|
||
----- stderr -----
|
||
warning: The `uv auth helper` command is experimental and may change without warning. Pass `--preview-features auth-helper` to disable this warning
|
||
"#
|
||
);
|
||
}
|
||
|
||
/// Test credential helper with token credentials
|
||
#[test]
|
||
fn bazel_helper_token() {
|
||
let context = TestContext::new("3.12");
|
||
|
||
// Store token
|
||
uv_snapshot!(context.filters(), context.auth_login()
|
||
.arg("https://api.example.com")
|
||
.arg("--token").arg("mytoken123"), @"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Stored credentials for https://api.example.com/
|
||
");
|
||
|
||
// Test credential helper - tokens are stored as Basic auth with __token__ username
|
||
uv_snapshot!(context.filters(), context.auth_helper()
|
||
.arg("--protocol=bazel")
|
||
.arg("get"),
|
||
input=r#"{"uri":"https://api.example.com/v1/endpoint"}"#,
|
||
@r#"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
{"headers":{"Authorization":["Basic X190b2tlbl9fOm15dG9rZW4xMjM="]}}
|
||
|
||
----- stderr -----
|
||
warning: The `uv auth helper` command is experimental and may change without warning. Pass `--preview-features auth-helper` to disable this warning
|
||
"#
|
||
);
|
||
}
|
||
|
||
/// Test credential helper with no credentials found
|
||
#[test]
|
||
fn bazel_helper_no_credentials() {
|
||
let context = TestContext::new("3.12");
|
||
uv_snapshot!(context.filters(), context.auth_helper()
|
||
.arg("--protocol=bazel")
|
||
.arg("get"),
|
||
input=r#"{"uri":"https://unknown.example.com/path"}"#,
|
||
@r#"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
{"headers":{}}
|
||
|
||
----- stderr -----
|
||
warning: The `uv auth helper` command is experimental and may change without warning. Pass `--preview-features auth-helper` to disable this warning
|
||
"#
|
||
);
|
||
}
|
||
|
||
/// Test credential helper with invalid JSON input
|
||
#[test]
|
||
fn bazel_helper_invalid_json() {
|
||
let context = TestContext::new("3.12");
|
||
|
||
uv_snapshot!(context.filters(), context.auth_helper()
|
||
.arg("--protocol=bazel")
|
||
.arg("get"),
|
||
input="not json",
|
||
@"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
warning: The `uv auth helper` command is experimental and may change without warning. Pass `--preview-features auth-helper` to disable this warning
|
||
error: Failed to parse credential request as JSON
|
||
Caused by: expected ident at line 1 column 2
|
||
"
|
||
);
|
||
}
|
||
|
||
/// Test credential helper with invalid URI
|
||
#[test]
|
||
fn bazel_helper_invalid_uri() {
|
||
let context = TestContext::new("3.12");
|
||
|
||
uv_snapshot!(context.filters(), context.auth_helper()
|
||
.arg("--protocol=bazel")
|
||
.arg("get"),
|
||
input=r#"{"uri":"not a url"}"#,
|
||
@r#"
|
||
success: false
|
||
exit_code: 2
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
warning: The `uv auth helper` command is experimental and may change without warning. Pass `--preview-features auth-helper` to disable this warning
|
||
error: Failed to parse credential request as JSON
|
||
Caused by: relative URL without a base: "not a url" at line 1 column 18
|
||
"#
|
||
);
|
||
}
|
||
|
||
/// Test credential helper with username in URI
|
||
#[test]
|
||
fn bazel_helper_username_in_uri() {
|
||
let context = TestContext::new("3.12");
|
||
|
||
// Store credentials with specific username
|
||
uv_snapshot!(context.filters(), context.auth_login()
|
||
.arg("https://test.example.com")
|
||
.arg("--username").arg("specificuser")
|
||
.arg("--password").arg("specificpass"), @"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Stored credentials for specificuser@https://test.example.com/
|
||
");
|
||
|
||
// Test with username in URI
|
||
uv_snapshot!(context.filters(), context.auth_helper()
|
||
.arg("--protocol=bazel")
|
||
.arg("get"),
|
||
input=r#"{"uri":"https://specificuser@test.example.com/path"}"#,
|
||
@r#"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
{"headers":{"Authorization":["Basic c3BlY2lmaWN1c2VyOnNwZWNpZmljcGFzcw=="]}}
|
||
|
||
----- stderr -----
|
||
warning: The `uv auth helper` command is experimental and may change without warning. Pass `--preview-features auth-helper` to disable this warning
|
||
"#
|
||
);
|
||
}
|
||
|
||
/// Test credential helper with unknown username in URI
|
||
#[test]
|
||
fn bazel_helper_unknown_username_in_uri() {
|
||
let context = TestContext::new("3.12");
|
||
|
||
// Store credentials with specific username
|
||
uv_snapshot!(context.filters(), context.auth_login()
|
||
.arg("https://test.example.com")
|
||
.arg("--username").arg("specificuser")
|
||
.arg("--password").arg("specificpass"), @"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
|
||
----- stderr -----
|
||
Stored credentials for specificuser@https://test.example.com/
|
||
");
|
||
|
||
// Test with username in URI
|
||
uv_snapshot!(context.filters(), context.auth_helper()
|
||
.arg("--protocol=bazel")
|
||
.arg("get"),
|
||
input=r#"{"uri":"https://differentuser@test.example.com/path"}"#,
|
||
@r#"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
{"headers":{}}
|
||
|
||
----- stderr -----
|
||
warning: The `uv auth helper` command is experimental and may change without warning. Pass `--preview-features auth-helper` to disable this warning
|
||
"#
|
||
);
|
||
}
|