use std::env;
use std::fs;
use std::path::Path;
use std::thread;
use assert_cmd::Command;
use insta::assert_snapshot;
use predicates::str::contains;
use tempfile::tempdir;
use tiny_http::{Header, Response, Server, StatusCode};
fn write_file(path: &Path, content: &str) {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).expect("mkdir");
}
fs::write(path, content).expect("write");
}
fn create_workspace(root: &Path) {
write_file(
&root.join("Cargo.toml"),
r#"
[workspace]
members = ["demo"]
resolver = "2"
"#,
);
write_file(
&root.join("demo/Cargo.toml"),
r#"
[package]
name = "demo"
version = "0.1.0"
edition = "2021"
"#,
);
write_file(&root.join("demo/src/lib.rs"), "pub fn demo() {}\n");
}
fn normalize_output(raw: &str) -> String {
raw.lines()
.map(|line| {
if line.starts_with("plan_id: ") {
"plan_id: <PLAN_ID>".to_string()
} else if line.starts_with("Plan ID: ") {
"Plan ID: <PLAN_ID>".to_string()
} else if line.starts_with("workspace_root: ") {
"workspace_root: <WORKSPACE_ROOT>".to_string()
} else if line.starts_with("state_dir: ") {
"state_dir: <STATE_DIR>".to_string()
} else if line.starts_with("state: ") {
"state: <STATE_FILE>".to_string()
} else if line.starts_with("receipt: ") {
"receipt: <RECEIPT_FILE>".to_string()
} else if line.starts_with("cargo: ") {
"cargo: <CARGO_VERSION>".to_string()
} else if line.starts_with("git: ") {
"git: <GIT_VERSION>".to_string()
} else if line.starts_with("Timestamp: ") {
"Timestamp: <TIMESTAMP>".to_string()
} else if line.starts_with("Started: ") {
"Started: <TIMESTAMP>".to_string()
} else if line.starts_with("Finished: ") {
"Finished: <TIMESTAMP>".to_string()
} else if line.starts_with("Duration: ") {
"Duration: <DURATION>ms".to_string()
} else if line.starts_with(" Shipper: ") {
" Shipper: <SHIPPER_VERSION>".to_string()
} else if line.starts_with(" Cargo: ") {
" Cargo: <CARGO_VERSION>".to_string()
} else if line.starts_with(" Rust: ") {
" Rust: <RUST_VERSION>".to_string()
} else if line.starts_with(" Commit: ") {
" Commit: <COMMIT>".to_string()
} else if line.starts_with(" Branch: ") {
" Branch: <BRANCH>".to_string()
} else if line.starts_with(" Tag: ") {
" Tag: <TAG>".to_string()
} else if line.starts_with("git_commit: ") {
"git_commit: <GIT_COMMIT>".to_string()
} else if line.starts_with("git_branch: ") {
"git_branch: <GIT_BRANCH>".to_string()
} else if line.starts_with("git_dirty: ") {
"git_dirty: <GIT_DIRTY>".to_string()
} else {
line.replace('\\', "/")
}
})
.collect::<Vec<_>>()
.join("\n")
}
fn path_sep() -> &'static str {
if cfg!(windows) { ";" } else { ":" }
}
fn create_fake_cargo_proxy(bin_dir: &Path) {
#[cfg(windows)]
{
fs::write(
bin_dir.join("cargo.cmd"),
"@echo off\r\nif \"%1\"==\"publish\" (\r\n if \"%SHIPPER_FAKE_PUBLISH_EXIT%\"==\"\" (exit /b 0) else (exit /b %SHIPPER_FAKE_PUBLISH_EXIT%)\r\n)\r\n\"%REAL_CARGO%\" %*\r\nexit /b %ERRORLEVEL%\r\n",
)
.expect("write fake cargo");
}
#[cfg(not(windows))]
{
use std::os::unix::fs::PermissionsExt;
let path = bin_dir.join("cargo");
fs::write(
&path,
"#!/usr/bin/env sh\nif [ \"$1\" = \"publish\" ]; then\n exit \"${SHIPPER_FAKE_PUBLISH_EXIT:-0}\"\nfi\n\"$REAL_CARGO\" \"$@\"\n",
)
.expect("write fake cargo");
let mut perms = fs::metadata(&path).expect("meta").permissions();
perms.set_mode(0o755);
fs::set_permissions(&path, perms).expect("chmod");
}
}
struct TestRegistry {
base_url: String,
handle: thread::JoinHandle<()>,
}
impl TestRegistry {
fn join(self) {
self.handle.join().expect("join server");
}
}
fn spawn_registry(statuses: Vec<u16>, expected_requests: usize) -> TestRegistry {
let server = Server::http("127.0.0.1:0").expect("server");
let base_url = format!("http://{}", server.server_addr());
let handle = thread::spawn(move || {
for idx in 0..expected_requests {
let req = match server.recv_timeout(std::time::Duration::from_secs(30)) {
Ok(Some(r)) => r,
_ => break,
};
let status = statuses
.get(idx)
.copied()
.or_else(|| statuses.last().copied())
.unwrap_or(404);
let resp = Response::from_string("{}")
.with_status_code(StatusCode(status))
.with_header(
Header::from_bytes("Content-Type", "application/json").expect("header"),
);
req.respond(resp).expect("respond");
}
});
TestRegistry { base_url, handle }
}
fn shipper_cmd() -> Command {
Command::new(assert_cmd::cargo::cargo_bin!("shipper-cli"))
}
#[test]
fn plan_command_snapshot() {
let td = tempdir().expect("tempdir");
create_workspace(td.path());
let mut cmd = shipper_cmd();
let out = cmd
.arg("--manifest-path")
.arg(td.path().join("Cargo.toml"))
.arg("plan")
.assert()
.success()
.get_output()
.stdout
.clone();
let stdout = String::from_utf8(out).expect("utf8");
assert_snapshot!(
normalize_output(&stdout),
@"
plan_id: <PLAN_ID>
registry: crates-io (https://crates.io)
workspace_root: <WORKSPACE_ROOT>
Total packages to publish: 1
1. demo@0.1.0
"
);
}
#[test]
fn plan_command_with_package_flag() {
let td = tempdir().expect("tempdir");
create_workspace(td.path());
let mut cmd = shipper_cmd();
let out = cmd
.arg("--manifest-path")
.arg(td.path().join("Cargo.toml"))
.arg("--package")
.arg("demo")
.arg("plan")
.assert()
.success()
.get_output()
.stdout
.clone();
let stdout = String::from_utf8(out).expect("utf8");
assert!(stdout.contains("demo@0.1.0"));
}
#[test]
fn doctor_command_snapshot() {
let td = tempdir().expect("tempdir");
create_workspace(td.path());
fs::create_dir_all(td.path().join("cargo-home")).expect("mkdir");
let mut cmd = shipper_cmd();
let out = cmd
.arg("--manifest-path")
.arg(td.path().join("Cargo.toml"))
.arg("--state-dir")
.arg(".shipper")
.arg("doctor")
.env("CARGO_HOME", td.path().join("cargo-home"))
.env_remove("CARGO_REGISTRY_TOKEN")
.env_remove("CARGO_REGISTRIES_CRATES_IO_TOKEN")
.assert()
.success()
.get_output()
.stdout
.clone();
let stdout = String::from_utf8(out).expect("utf8");
assert_snapshot!(
normalize_output(&stdout),
@"
Shipper Doctor - Diagnostics Report
----------------------------------
workspace_root: <WORKSPACE_ROOT>
registry: crates-io (https://crates.io)
auth_type: NONE FOUND (set CARGO_REGISTRY_TOKEN)
state_dir: <STATE_DIR>
state_dir_exists: false (will be created)
cargo: <CARGO_VERSION>
git: <GIT_VERSION>
registry_reachable: true
index_base: https://index.crates.io
git_commit: <GIT_COMMIT>
git_branch: <GIT_BRANCH>
git_dirty: <GIT_DIRTY>
Diagnostics complete.
"
);
}
#[test]
fn doctor_command_detects_trusted_publishing_auth() {
let td = tempdir().expect("tempdir");
create_workspace(td.path());
fs::create_dir_all(td.path().join("cargo-home")).expect("mkdir");
let mut cmd = shipper_cmd();
let out = cmd
.arg("--manifest-path")
.arg(td.path().join("Cargo.toml"))
.arg("--state-dir")
.arg(".shipper")
.arg("doctor")
.env("CARGO_HOME", td.path().join("cargo-home"))
.env_remove("CARGO_REGISTRY_TOKEN")
.env_remove("CARGO_REGISTRIES_CRATES_IO_TOKEN")
.env(
"ACTIONS_ID_TOKEN_REQUEST_URL",
"https://example.invalid/oidc",
)
.env("ACTIONS_ID_TOKEN_REQUEST_TOKEN", "oidc-token")
.assert()
.success()
.get_output()
.stdout
.clone();
let stdout = String::from_utf8(out).expect("utf8");
assert_snapshot!(
normalize_output(&stdout),
@"
Shipper Doctor - Diagnostics Report
----------------------------------
workspace_root: <WORKSPACE_ROOT>
registry: crates-io (https://crates.io)
auth_type: trusted (detected)
state_dir: <STATE_DIR>
state_dir_exists: false (will be created)
cargo: <CARGO_VERSION>
git: <GIT_VERSION>
registry_reachable: true
index_base: https://index.crates.io
git_commit: <GIT_COMMIT>
git_branch: <GIT_BRANCH>
git_dirty: <GIT_DIRTY>
Diagnostics complete.
"
);
}
#[test]
fn status_command_snapshot() {
let td = tempdir().expect("tempdir");
create_workspace(td.path());
let registry = spawn_registry(vec![404], 1);
let mut cmd = shipper_cmd();
let out = cmd
.arg("--manifest-path")
.arg(td.path().join("Cargo.toml"))
.arg("--api-base")
.arg(®istry.base_url)
.arg("status")
.assert()
.success()
.get_output()
.stdout
.clone();
let stdout = String::from_utf8(out).expect("utf8");
assert_snapshot!(
normalize_output(&stdout),
@r#"
plan_id: <PLAN_ID>
demo@0.1.0: missing
"#
);
registry.join();
}
#[test]
fn preflight_command_snapshot() {
let td = tempdir().expect("tempdir");
create_workspace(td.path());
fs::create_dir_all(td.path().join("cargo-home")).expect("mkdir");
let registry = spawn_registry(vec![404], 2);
let mut cmd = shipper_cmd();
let out = cmd
.arg("--manifest-path")
.arg(td.path().join("Cargo.toml"))
.arg("--api-base")
.arg(®istry.base_url)
.arg("--allow-dirty")
.arg("--skip-ownership-check")
.arg("preflight")
.env("CARGO_HOME", td.path().join("cargo-home"))
.env_remove("CARGO_REGISTRY_TOKEN")
.env_remove("CARGO_REGISTRIES_CRATES_IO_TOKEN")
.assert()
.success()
.get_output()
.stdout
.clone();
let stdout = String::from_utf8(out).expect("utf8");
let normalized = normalize_output(&stdout);
let stripped: String = normalized
.chars()
.fold((String::new(), false), |(mut s, in_esc), c| {
if c == '\x1b' {
(s, true)
} else if in_esc {
(s, c != 'm')
} else {
s.push(c);
(s, false)
}
})
.0;
assert_snapshot!(
stripped,
@r#"
Preflight Report
===============
Plan ID: <PLAN_ID>
Timestamp: <TIMESTAMP>
Token Detected: ✗
Finishability: NOT PROVEN
Packages:
┌─────────────────────┬─────────┬──────────┬──────────┬───────────────┬─────────────┬─────────────┐
│ Package │ Version │ Published│ New Crate │ Auth Type │ Ownership │ Dry-run │
├─────────────────────┼─────────┼──────────┼──────────┼───────────────┼─────────────┼─────────────┤
│ demo │ 0.1.0 │ No │ Yes │ - │ ✗ │ ✓ │
└─────────────────────┴─────────┴──────────┴──────────┴───────────────┴─────────────┴─────────────┘
Summary:
Total packages: 1
Already published: 0
New crates: 1
Ownership verified: 0
Dry-run passed: 1
What to do next:
-----------------
⚠ Some checks could not be verified. You can still publish, but may encounter permission issues. Use `shipper publish --policy fast` to proceed.
"#
);
registry.join();
}
#[test]
fn preflight_command_writes_preflight_events() {
let td = tempdir().expect("tempdir");
create_workspace(td.path());
fs::create_dir_all(td.path().join("cargo-home")).expect("mkdir");
let registry = spawn_registry(vec![404], 2);
shipper_cmd()
.arg("--manifest-path")
.arg(td.path().join("Cargo.toml"))
.arg("--api-base")
.arg(®istry.base_url)
.arg("--state-dir")
.arg(".shipper")
.arg("--allow-dirty")
.arg("--skip-ownership-check")
.arg("preflight")
.env("CARGO_HOME", td.path().join("cargo-home"))
.env_remove("CARGO_REGISTRY_TOKEN")
.env_remove("CARGO_REGISTRIES_CRATES_IO_TOKEN")
.assert()
.success();
let events_path = td.path().join(".shipper").join("events.jsonl");
assert!(events_path.exists(), "expected {}", events_path.display());
let events = fs::read_to_string(&events_path).expect("read events");
assert!(events.contains(r#""type":"preflight_started""#));
assert!(events.contains(r#""type":"preflight_workspace_verify""#));
assert!(events.contains(r#""type":"preflight_new_crate_detected""#));
assert!(events.contains(r#""type":"preflight_ownership_check""#));
assert!(events.contains(r#""type":"preflight_complete""#));
let mut inspect = shipper_cmd();
let out = inspect
.arg("--manifest-path")
.arg(td.path().join("Cargo.toml"))
.arg("--state-dir")
.arg(".shipper")
.arg("inspect-events")
.assert()
.success()
.get_output()
.stdout
.clone();
let stdout = String::from_utf8(out).expect("utf8");
assert!(stdout.contains(r#""type":"preflight_complete""#));
registry.join();
}
#[test]
fn preflight_command_with_json_flag() {
let td = tempdir().expect("tempdir");
create_workspace(td.path());
fs::create_dir_all(td.path().join("cargo-home")).expect("mkdir");
let registry = spawn_registry(vec![404], 2);
let mut cmd = shipper_cmd();
let out = cmd
.arg("--manifest-path")
.arg(td.path().join("Cargo.toml"))
.arg("--api-base")
.arg(®istry.base_url)
.arg("--allow-dirty")
.arg("--skip-ownership-check")
.arg("preflight")
.arg("--format")
.arg("json")
.env("CARGO_HOME", td.path().join("cargo-home"))
.env_remove("CARGO_REGISTRY_TOKEN")
.env_remove("CARGO_REGISTRIES_CRATES_IO_TOKEN")
.assert()
.success()
.get_output()
.stdout
.clone();
let stdout = String::from_utf8(out).expect("utf8");
let json: serde_json::Value = serde_json::from_str(&stdout).expect("valid JSON");
assert!(json["plan_id"].is_string());
assert_eq!(json["token_detected"], false);
assert!(json["finishability"].is_string());
assert!(json["packages"].is_array());
registry.join();
}
#[test]
fn preflight_command_with_policy_flags() {
let td = tempdir().expect("tempdir");
create_workspace(td.path());
fs::create_dir_all(td.path().join("cargo-home")).expect("mkdir");
let registry = spawn_registry(vec![404], 4);
let mut cmd = shipper_cmd();
cmd.arg("--manifest-path")
.arg(td.path().join("Cargo.toml"))
.arg("--api-base")
.arg(®istry.base_url)
.arg("--allow-dirty")
.arg("--policy")
.arg("fast")
.arg("preflight")
.env("CARGO_HOME", td.path().join("cargo-home"))
.env_remove("CARGO_REGISTRY_TOKEN")
.env_remove("CARGO_REGISTRIES_CRATES_IO_TOKEN")
.assert()
.success();
let mut cmd = shipper_cmd();
cmd.arg("--manifest-path")
.arg(td.path().join("Cargo.toml"))
.arg("--api-base")
.arg(®istry.base_url)
.arg("--allow-dirty")
.arg("--verify-mode")
.arg("package")
.arg("preflight")
.env("CARGO_HOME", td.path().join("cargo-home"))
.env_remove("CARGO_REGISTRY_TOKEN")
.env_remove("CARGO_REGISTRIES_CRATES_IO_TOKEN")
.assert()
.success();
registry.join();
}
#[test]
fn preflight_command_reports_already_published() {
let td = tempdir().expect("tempdir");
create_workspace(td.path());
fs::create_dir_all(td.path().join("cargo-home")).expect("mkdir");
let registry = spawn_registry(vec![200], 2);
let mut cmd = shipper_cmd();
let out = cmd
.arg("--manifest-path")
.arg(td.path().join("Cargo.toml"))
.arg("--api-base")
.arg(®istry.base_url)
.arg("--allow-dirty")
.arg("--skip-ownership-check")
.arg("preflight")
.env("CARGO_HOME", td.path().join("cargo-home"))
.env_remove("CARGO_REGISTRY_TOKEN")
.env_remove("CARGO_REGISTRIES_CRATES_IO_TOKEN")
.assert()
.success()
.get_output()
.stdout
.clone();
let stdout = String::from_utf8(out).expect("utf8");
assert!(stdout.contains("Already published: 1"));
registry.join();
}
#[test]
fn publish_command_e2e_with_fake_cargo() {
let td = tempdir().expect("tempdir");
create_workspace(td.path());
let bin_dir = td.path().join("fake-bin");
fs::create_dir_all(&bin_dir).expect("mkdir");
create_fake_cargo_proxy(&bin_dir);
let old_path = env::var("PATH").unwrap_or_default();
let mut new_path = bin_dir.display().to_string();
if !old_path.is_empty() {
new_path.push_str(path_sep());
new_path.push_str(&old_path);
}
let real_cargo = env::var("CARGO").unwrap_or_else(|_| "cargo".to_string());
let registry = spawn_registry(vec![404, 200], 2);
let mut cmd = shipper_cmd();
let out = cmd
.arg("--manifest-path")
.arg(td.path().join("Cargo.toml"))
.arg("--api-base")
.arg(®istry.base_url)
.arg("--allow-dirty")
.arg("--verify-timeout")
.arg("0ms")
.arg("--verify-poll")
.arg("0ms")
.arg("--max-attempts")
.arg("1")
.arg("--state-dir")
.arg(".shipper")
.arg("publish")
.env("PATH", new_path)
.env("REAL_CARGO", real_cargo)
.env("SHIPPER_FAKE_PUBLISH_EXIT", "0")
.assert()
.success()
.get_output()
.stdout
.clone();
let stdout = String::from_utf8(out).expect("utf8");
assert!(stdout.contains("demo@0.1.0: Published"));
assert!(td.path().join(".shipper").join("state.json").exists());
assert!(td.path().join(".shipper").join("receipt.json").exists());
registry.join();
}
#[test]
fn publish_then_resume_e2e_with_absolute_state_dir() {
let td = tempdir().expect("tempdir");
create_workspace(td.path());
let bin_dir = td.path().join("fake-bin");
fs::create_dir_all(&bin_dir).expect("mkdir");
create_fake_cargo_proxy(&bin_dir);
let old_path = env::var("PATH").unwrap_or_default();
let mut new_path = bin_dir.display().to_string();
if !old_path.is_empty() {
new_path.push_str(path_sep());
new_path.push_str(&old_path);
}
let real_cargo = env::var("CARGO").unwrap_or_else(|_| "cargo".to_string());
let abs_state = td.path().join("shipper-state-abs");
let registry = spawn_registry(vec![404, 200], 2);
let mut publish = shipper_cmd();
publish
.arg("--manifest-path")
.arg(td.path().join("Cargo.toml"))
.arg("--api-base")
.arg(®istry.base_url)
.arg("--allow-dirty")
.arg("--verify-timeout")
.arg("0ms")
.arg("--verify-poll")
.arg("0ms")
.arg("--max-attempts")
.arg("1")
.arg("--state-dir")
.arg(&abs_state)
.arg("publish")
.env("PATH", &new_path)
.env("REAL_CARGO", &real_cargo)
.env("SHIPPER_FAKE_PUBLISH_EXIT", "0")
.assert()
.success();
registry.join();
let mut resume = shipper_cmd();
let out = resume
.arg("--manifest-path")
.arg(td.path().join("Cargo.toml"))
.arg("--api-base")
.arg("http://127.0.0.1:9")
.arg("--allow-dirty")
.arg("--force-resume")
.arg("--state-dir")
.arg(&abs_state)
.arg("resume")
.env("PATH", &new_path)
.env("REAL_CARGO", &real_cargo)
.assert()
.success()
.get_output()
.stdout
.clone();
let stdout = String::from_utf8(out).expect("utf8");
assert!(stdout.contains("plan_id:"));
assert!(stdout.contains("state:"));
assert!(stdout.contains("receipt:"));
}
#[test]
fn invalid_duration_flag_fails() {
let td = tempdir().expect("tempdir");
create_workspace(td.path());
shipper_cmd()
.arg("--manifest-path")
.arg(td.path().join("Cargo.toml"))
.arg("--base-delay")
.arg("not-a-duration")
.arg("plan")
.assert()
.failure()
.stderr(contains("invalid duration"));
}
#[test]
fn inspect_receipt_command_displays_new_fields() {
let td = tempdir().expect("tempdir");
create_workspace(td.path());
let bin_dir = td.path().join("fake-bin");
fs::create_dir_all(&bin_dir).expect("mkdir");
create_fake_cargo_proxy(&bin_dir);
let old_path = env::var("PATH").unwrap_or_default();
let mut new_path = bin_dir.display().to_string();
if !old_path.is_empty() {
new_path.push_str(path_sep());
new_path.push_str(&old_path);
}
let real_cargo = env::var("CARGO").unwrap_or_else(|_| "cargo".to_string());
let registry = spawn_registry(vec![404, 200], 2);
let mut publish = shipper_cmd();
publish
.arg("--manifest-path")
.arg(td.path().join("Cargo.toml"))
.arg("--api-base")
.arg(®istry.base_url)
.arg("--allow-dirty")
.arg("--verify-timeout")
.arg("0ms")
.arg("--verify-poll")
.arg("0ms")
.arg("--max-attempts")
.arg("1")
.arg("--state-dir")
.arg(".shipper")
.arg("publish")
.env("PATH", &new_path)
.env("REAL_CARGO", &real_cargo)
.env("SHIPPER_FAKE_PUBLISH_EXIT", "0")
.assert()
.success();
registry.join();
let mut inspect = shipper_cmd();
let out = inspect
.arg("--manifest-path")
.arg(td.path().join("Cargo.toml"))
.arg("--state-dir")
.arg(".shipper")
.arg("inspect-receipt")
.assert()
.success()
.get_output()
.stdout
.clone();
let stdout = String::from_utf8(out).expect("utf8");
assert!(stdout.contains("Receipt"));
assert!(stdout.contains("Git Context") || stdout.contains("Environment"));
assert!(stdout.contains("Shipper:") || stdout.contains("Cargo:") || stdout.contains("Rust:"));
}
#[test]
fn ci_github_actions_includes_cache() {
let td = tempdir().expect("tempdir");
create_workspace(td.path());
let mut cmd = shipper_cmd();
let out = cmd
.arg("--manifest-path")
.arg(td.path().join("Cargo.toml"))
.arg("ci")
.arg("github-actions")
.assert()
.success()
.get_output()
.stdout
.clone();
let stdout = String::from_utf8(out).expect("utf8");
assert!(stdout.contains("actions/cache@v3"));
assert!(stdout.contains("shipper-${{"));
assert!(stdout.contains("restore-keys"));
}
#[test]
fn ci_gitlab_includes_cache() {
let td = tempdir().expect("tempdir");
create_workspace(td.path());
let mut cmd = shipper_cmd();
let out = cmd
.arg("--manifest-path")
.arg(td.path().join("Cargo.toml"))
.arg("ci")
.arg("gitlab")
.assert()
.success()
.get_output()
.stdout
.clone();
let stdout = String::from_utf8(out).expect("utf8");
assert!(stdout.contains("cache:"));
assert!(stdout.contains("key:"));
assert!(stdout.contains("paths:"));
}
#[test]
fn preflight_command_finishability_proven() {
let td = tempdir().expect("tempdir");
create_workspace(td.path());
fs::create_dir_all(td.path().join("cargo-home")).expect("mkdir");
let registry = spawn_registry(vec![404], 2);
let mut cmd = shipper_cmd();
let out = cmd
.arg("--manifest-path")
.arg(td.path().join("Cargo.toml"))
.arg("--api-base")
.arg(®istry.base_url)
.arg("--allow-dirty")
.arg("--skip-ownership-check")
.arg("preflight")
.env("CARGO_HOME", td.path().join("cargo-home"))
.env("CARGO_REGISTRY_TOKEN", "fake-token")
.env_remove("CARGO_REGISTRIES_CRATES_IO_TOKEN")
.assert()
.success()
.get_output()
.stdout
.clone();
let stdout = String::from_utf8(out).expect("utf8");
assert!(stdout.contains("PROVEN"));
registry.join();
}
#[test]
fn preflight_command_finishability_failed() {
let td = tempdir().expect("tempdir");
create_workspace(td.path());
fs::create_dir_all(td.path().join("cargo-home")).expect("mkdir");
let registry = spawn_registry(vec![404], 2);
let bin = td.path().join("bin");
fs::create_dir_all(&bin).expect("mkdir");
#[cfg(windows)]
{
fs::write(
bin.join("cargo.cmd"),
"@echo off\r\nif \"%1\"==\"publish\" if \"%3\"==\"--dry-run\" exit /b 1\r\n\"%REAL_CARGO%\" %*\r\n",
)
.expect("write fake cargo");
}
#[cfg(not(windows))]
{
use std::os::unix::fs::PermissionsExt;
let path = bin.join("cargo");
fs::write(
&path,
"#!/usr/bin/env sh\nif [ \"$1\" = \"publish\" ]; then\n for arg in \"$@\"; do\n [ \"$arg\" = \"--dry-run\" ] && exit 1\n done\nfi\n\"$REAL_CARGO\" \"$@\"\n",
)
.expect("write fake cargo");
let mut perms = fs::metadata(&path).expect("meta").permissions();
perms.set_mode(0o755);
fs::set_permissions(&path, perms).expect("chmod");
}
let old_path = env::var("PATH").unwrap_or_default();
let mut new_path = bin.display().to_string();
if !old_path.is_empty() {
new_path.push_str(path_sep());
new_path.push_str(&old_path);
}
let real_cargo = env::var("CARGO").unwrap_or_else(|_| "cargo".to_string());
#[cfg(windows)]
let fake_cargo_path = bin.join("cargo.cmd");
#[cfg(not(windows))]
let fake_cargo_path = bin.join("cargo");
let mut cmd = shipper_cmd();
let out = cmd
.arg("--manifest-path")
.arg(td.path().join("Cargo.toml"))
.arg("--api-base")
.arg(®istry.base_url)
.arg("--allow-dirty")
.arg("--skip-ownership-check")
.arg("preflight")
.env("PATH", new_path)
.env("REAL_CARGO", real_cargo)
.env("SHIPPER_CARGO_BIN", &fake_cargo_path)
.env("CARGO_HOME", td.path().join("cargo-home"))
.env_remove("CARGO_REGISTRY_TOKEN")
.env_remove("CARGO_REGISTRIES_CRATES_IO_TOKEN")
.assert()
.success()
.get_output()
.stdout
.clone();
let stdout = String::from_utf8(out).expect("utf8");
assert!(stdout.contains("FAILED"));
registry.join();
}
#[test]
fn preflight_command_with_new_crates() {
let td = tempdir().expect("tempdir");
create_workspace(td.path());
fs::create_dir_all(td.path().join("cargo-home")).expect("mkdir");
let registry = spawn_registry(
vec![
(404), (404), ],
2,
);
let mut cmd = shipper_cmd();
let out = cmd
.arg("--manifest-path")
.arg(td.path().join("Cargo.toml"))
.arg("--api-base")
.arg(®istry.base_url)
.arg("--allow-dirty")
.arg("--skip-ownership-check")
.arg("preflight")
.env("CARGO_HOME", td.path().join("cargo-home"))
.env_remove("CARGO_REGISTRY_TOKEN")
.env_remove("CARGO_REGISTRIES_CRATES_IO_TOKEN")
.assert()
.success()
.get_output()
.stdout
.clone();
let stdout = String::from_utf8(out).expect("utf8");
assert!(stdout.contains("New crates: 1"));
registry.join();
}
#[test]
fn inspect_receipt_command_with_git_context() {
let td = tempdir().expect("tempdir");
create_workspace(td.path());
let bin_dir = td.path().join("fake-bin");
fs::create_dir_all(&bin_dir).expect("mkdir");
create_fake_cargo_proxy(&bin_dir);
let old_path = env::var("PATH").unwrap_or_default();
let mut new_path = bin_dir.display().to_string();
if !old_path.is_empty() {
new_path.push_str(path_sep());
new_path.push_str(&old_path);
}
let real_cargo = env::var("CARGO").unwrap_or_else(|_| "cargo".to_string());
let registry = spawn_registry(vec![404, 200], 2);
let mut publish = shipper_cmd();
publish
.arg("--manifest-path")
.arg(td.path().join("Cargo.toml"))
.arg("--api-base")
.arg(®istry.base_url)
.arg("--allow-dirty")
.arg("--verify-timeout")
.arg("0ms")
.arg("--verify-poll")
.arg("0ms")
.arg("--max-attempts")
.arg("1")
.arg("--state-dir")
.arg(".shipper")
.arg("publish")
.env("PATH", new_path.clone())
.env("REAL_CARGO", real_cargo.clone())
.env("SHIPPER_FAKE_PUBLISH_EXIT", "0")
.assert()
.success();
registry.join();
let mut inspect = shipper_cmd();
let out = inspect
.arg("--manifest-path")
.arg(td.path().join("Cargo.toml"))
.arg("--state-dir")
.arg(".shipper")
.arg("inspect-receipt")
.arg("--format")
.arg("json")
.assert()
.success()
.get_output()
.stdout
.clone();
let stdout = String::from_utf8(out).expect("utf8");
let json: serde_json::Value = serde_json::from_str(&stdout).expect("valid JSON");
assert!(json.get("git_context").is_some());
assert!(json.get("environment").is_some());
let env = json.get("environment").unwrap();
assert!(env.get("shipper_version").is_some());
assert!(env.get("cargo_version").is_some());
assert!(env.get("rust_version").is_some());
assert!(env.get("os").is_some());
assert!(env.get("arch").is_some());
}
#[test]
fn inspect_receipt_command_with_environment_fingerprint() {
let td = tempdir().expect("tempdir");
create_workspace(td.path());
let bin_dir = td.path().join("fake-bin");
fs::create_dir_all(&bin_dir).expect("mkdir");
create_fake_cargo_proxy(&bin_dir);
let old_path = env::var("PATH").unwrap_or_default();
let mut new_path = bin_dir.display().to_string();
if !old_path.is_empty() {
new_path.push_str(path_sep());
new_path.push_str(&old_path);
}
let real_cargo = env::var("CARGO").unwrap_or_else(|_| "cargo".to_string());
let registry = spawn_registry(vec![404, 200], 2);
let mut publish = shipper_cmd();
publish
.arg("--manifest-path")
.arg(td.path().join("Cargo.toml"))
.arg("--api-base")
.arg(®istry.base_url)
.arg("--allow-dirty")
.arg("--verify-timeout")
.arg("0ms")
.arg("--verify-poll")
.arg("0ms")
.arg("--max-attempts")
.arg("1")
.arg("--state-dir")
.arg(".shipper")
.arg("publish")
.env("PATH", new_path.clone())
.env("REAL_CARGO", real_cargo.clone())
.env("SHIPPER_FAKE_PUBLISH_EXIT", "0")
.assert()
.success();
registry.join();
let mut inspect = shipper_cmd();
let out = inspect
.arg("--manifest-path")
.arg(td.path().join("Cargo.toml"))
.arg("--state-dir")
.arg(".shipper")
.arg("inspect-receipt")
.assert()
.success()
.get_output()
.stdout
.clone();
let stdout = String::from_utf8(out).expect("utf8");
assert!(stdout.contains("Environment"));
assert!(stdout.contains("Shipper:") || stdout.contains("Shipper Version"));
assert!(stdout.contains("Cargo:") || stdout.contains("Cargo Version"));
assert!(stdout.contains("Rust:") || stdout.contains("Rust Version"));
assert!(stdout.contains("OS:") || stdout.contains("OS"));
assert!(stdout.contains("Arch:") || stdout.contains("Architecture"));
}
#[test]
fn preflight_command_json_output_structure() {
let td = tempdir().expect("tempdir");
create_workspace(td.path());
fs::create_dir_all(td.path().join("cargo-home")).expect("mkdir");
let registry = spawn_registry(vec![404], 2);
let mut cmd = shipper_cmd();
let out = cmd
.arg("--manifest-path")
.arg(td.path().join("Cargo.toml"))
.arg("--api-base")
.arg(®istry.base_url)
.arg("--allow-dirty")
.arg("--skip-ownership-check")
.arg("preflight")
.arg("--format")
.arg("json")
.env("CARGO_HOME", td.path().join("cargo-home"))
.env_remove("CARGO_REGISTRY_TOKEN")
.env_remove("CARGO_REGISTRIES_CRATES_IO_TOKEN")
.assert()
.success()
.get_output()
.stdout
.clone();
let stdout = String::from_utf8(out).expect("utf8");
let json: serde_json::Value = serde_json::from_str(&stdout).expect("valid JSON");
assert!(json.get("plan_id").is_some());
assert!(json.get("token_detected").is_some());
assert!(json.get("finishability").is_some());
assert!(json.get("packages").is_some());
assert!(json.get("timestamp").is_some());
let packages = json.get("packages").unwrap().as_array().unwrap();
assert_eq!(packages.len(), 1);
let pkg = &packages[0];
assert!(pkg.get("name").is_some());
assert!(pkg.get("version").is_some());
assert!(pkg.get("already_published").is_some());
assert!(pkg.get("is_new_crate").is_some());
assert!(pkg.get("auth_type").is_some());
assert!(pkg.get("ownership_verified").is_some());
assert!(pkg.get("dry_run_passed").is_some());
registry.join();
}