use std::path::Path;
use serial_test::serial;
use socket_patch_cli::commands::scan::{run as scan_run, ScanArgs};
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
const ORG: &str = "test-org";
fn write_dist_info(site_packages: &Path, name: &str, version: &str) {
let canon = name.to_lowercase().replace(['-', '.'], "_");
let dist = site_packages.join(format!("{canon}-{version}.dist-info"));
std::fs::create_dir_all(&dist).unwrap();
std::fs::write(
dist.join("METADATA"),
format!("Metadata-Version: 2.1\nName: {name}\nVersion: {version}\n"),
)
.unwrap();
let pkg = site_packages.join(&canon);
std::fs::create_dir_all(&pkg).unwrap();
std::fs::write(pkg.join("__init__.py"), "VERSION = '0'\n").unwrap();
}
async fn mock_batch_empty(server: &MockServer) {
Mock::given(method("POST"))
.and(path(format!("/v0/orgs/{ORG}/patches/batch")))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"packages": [], "canAccessPaidPatches": false,
})))
.mount(server)
.await;
}
fn default_args(cwd: &Path, api_url: String) -> ScanArgs {
ScanArgs {
common: socket_patch_cli::args::GlobalArgs {
cwd: cwd.to_path_buf(),
org: Some(ORG.to_string()),
json: true,
yes: true,
global: false,
global_prefix: None,
api_url: api_url,
api_token: Some("fake".to_string()),
ecosystems: Some(vec!["pypi".to_string()]),
download_mode: "diff".to_string(),
dry_run: false,
..socket_patch_cli::args::GlobalArgs::default()
},
batch_size: 100,
apply: false,
prune: false,
sync: false,
all_releases: false,
vex: Default::default(),
}
}
#[tokio::test]
#[serial]
async fn pypi_venv_layout_discovered() {
let tmp = tempfile::tempdir().unwrap();
let site = tmp.path().join(".venv/lib/python3.11/site-packages");
std::fs::create_dir_all(&site).unwrap();
write_dist_info(&site, "venv_pkg", "1.0.0");
let server = MockServer::start().await;
mock_batch_empty(&server).await;
assert_eq!(scan_run(default_args(tmp.path(), server.uri())).await, 0);
}
#[tokio::test]
#[serial]
async fn pypi_venv_python312_layout_discovered() {
let tmp = tempfile::tempdir().unwrap();
let site = tmp.path().join(".venv/lib/python3.12/site-packages");
std::fs::create_dir_all(&site).unwrap();
write_dist_info(&site, "venv_pkg_312", "1.0.0");
let server = MockServer::start().await;
mock_batch_empty(&server).await;
assert_eq!(scan_run(default_args(tmp.path(), server.uri())).await, 0);
}
#[tokio::test]
#[serial]
async fn pypi_venv_python313_layout_discovered() {
let tmp = tempfile::tempdir().unwrap();
let site = tmp.path().join(".venv/lib/python3.13/site-packages");
std::fs::create_dir_all(&site).unwrap();
write_dist_info(&site, "venv_pkg_313", "1.0.0");
let server = MockServer::start().await;
mock_batch_empty(&server).await;
assert_eq!(scan_run(default_args(tmp.path(), server.uri())).await, 0);
}
#[tokio::test]
#[serial]
async fn pypi_alternate_venv_dir_names() {
for venv_name in &["env", "venv", ".env"] {
let tmp = tempfile::tempdir().unwrap();
let site = tmp
.path()
.join(venv_name)
.join("lib/python3.11/site-packages");
std::fs::create_dir_all(&site).unwrap();
write_dist_info(&site, &format!("alt_{venv_name}"), "1.0.0");
let server = MockServer::start().await;
mock_batch_empty(&server).await;
let res = scan_run(default_args(tmp.path(), server.uri())).await;
assert_eq!(res, 0, "venv name {venv_name} should be discovered");
}
}
#[tokio::test]
#[serial]
async fn pypi_virtual_env_env_var_override() {
let tmp = tempfile::tempdir().unwrap();
let custom_venv = tmp.path().join("custom-venv");
let site = custom_venv.join("lib/python3.11/site-packages");
std::fs::create_dir_all(&site).unwrap();
write_dist_info(&site, "venv_override", "1.0.0");
let server = MockServer::start().await;
mock_batch_empty(&server).await;
std::env::set_var("VIRTUAL_ENV", &custom_venv);
let res = scan_run(default_args(tmp.path(), server.uri())).await;
std::env::remove_var("VIRTUAL_ENV");
assert_eq!(res, 0);
}
#[tokio::test]
#[serial]
async fn pypi_dist_info_only_layout() {
let tmp = tempfile::tempdir().unwrap();
let site = tmp.path().join(".venv/lib/python3.11/site-packages");
std::fs::create_dir_all(&site).unwrap();
let dist = site.join("dist_only-1.0.0.dist-info");
std::fs::create_dir_all(&dist).unwrap();
std::fs::write(
dist.join("METADATA"),
"Metadata-Version: 2.1\nName: dist_only\nVersion: 1.0.0\n",
)
.unwrap();
let server = MockServer::start().await;
mock_batch_empty(&server).await;
assert_eq!(scan_run(default_args(tmp.path(), server.uri())).await, 0);
}
#[tokio::test]
#[serial]
async fn pypi_canonical_name_normalization() {
let tmp = tempfile::tempdir().unwrap();
let site = tmp.path().join(".venv/lib/python3.11/site-packages");
std::fs::create_dir_all(&site).unwrap();
let dist = site.join("SQLAlchemy-2.0.30.dist-info");
std::fs::create_dir_all(&dist).unwrap();
std::fs::write(
dist.join("METADATA"),
"Metadata-Version: 2.1\nName: SQLAlchemy\nVersion: 2.0.30\n",
)
.unwrap();
let server = MockServer::start().await;
mock_batch_empty(&server).await;
assert_eq!(scan_run(default_args(tmp.path(), server.uri())).await, 0);
}
#[tokio::test]
#[serial]
async fn pypi_multiple_python_versions_in_venvs() {
let tmp = tempfile::tempdir().unwrap();
let site311 = tmp.path().join(".venv/lib/python3.11/site-packages");
std::fs::create_dir_all(&site311).unwrap();
write_dist_info(&site311, "pkg311", "1.0.0");
let site312 = tmp.path().join("venv/lib/python3.12/site-packages");
std::fs::create_dir_all(&site312).unwrap();
write_dist_info(&site312, "pkg312", "1.0.0");
let server = MockServer::start().await;
mock_batch_empty(&server).await;
assert_eq!(scan_run(default_args(tmp.path(), server.uri())).await, 0);
}
#[tokio::test]
#[serial]
async fn pypi_empty_site_packages_safe() {
let tmp = tempfile::tempdir().unwrap();
let site = tmp.path().join(".venv/lib/python3.11/site-packages");
std::fs::create_dir_all(&site).unwrap();
let server = MockServer::start().await;
mock_batch_empty(&server).await;
assert_eq!(scan_run(default_args(tmp.path(), server.uri())).await, 0);
}
#[tokio::test]
#[serial]
async fn pypi_malformed_metadata_handled_gracefully() {
let tmp = tempfile::tempdir().unwrap();
let site = tmp.path().join(".venv/lib/python3.11/site-packages");
std::fs::create_dir_all(&site).unwrap();
let dist = site.join("malformed-1.0.0.dist-info");
std::fs::create_dir_all(&dist).unwrap();
std::fs::write(dist.join("METADATA"), "Not a real METADATA file").unwrap();
let server = MockServer::start().await;
mock_batch_empty(&server).await;
assert_eq!(scan_run(default_args(tmp.path(), server.uri())).await, 0);
}
#[tokio::test]
#[serial]
async fn pypi_egg_info_layout_handled() {
let tmp = tempfile::tempdir().unwrap();
let site = tmp.path().join(".venv/lib/python3.11/site-packages");
std::fs::create_dir_all(&site).unwrap();
let egg = site.join("legacy_pkg-1.0.0.egg-info");
std::fs::create_dir_all(&egg).unwrap();
std::fs::write(
egg.join("PKG-INFO"),
"Metadata-Version: 1.0\nName: legacy_pkg\nVersion: 1.0.0\n",
)
.unwrap();
let server = MockServer::start().await;
mock_batch_empty(&server).await;
let res = scan_run(default_args(tmp.path(), server.uri())).await;
assert!(res == 0 || res == 1, "egg-info layout must not crash");
}