mod common;
use std::sync::Arc;
use common::{run_cursus, temp_git_repo};
use cursus::command::RealCommandRunner;
use cursus::filesystem::LocalFilesystem;
use cursus::test_logging::{init_test_logger, take_logs};
async fn run_cursus_with_token(
args: impl IntoIterator<Item = impl Into<std::ffi::OsString> + Clone>,
cwd: &std::path::Path,
token: &str,
) -> anyhow::Result<std::process::ExitCode> {
let octocrab_client = octocrab::Octocrab::builder()
.personal_token(token.to_string())
.build()
.unwrap();
let forge_client = Arc::new(cursus::forge::github::OctocrabGitHubClient::new(
octocrab_client,
cursus::forge::github::remote::GitHubRepo::new("acme", "app").unwrap(),
)) as Arc<dyn cursus::forge::CodeForgeClient>;
let runner = Arc::new(RealCommandRunner) as Arc<dyn cursus::command::CommandRunner>;
let path = cursus::path::AbsolutePath::new(cwd).unwrap();
let git = Arc::new(cursus::git::GitWorkdir::new(Arc::clone(&runner), path));
let env = cursus::Env::new(runner, Arc::new(LocalFilesystem), git)
.with_code_forge_client(forge_client);
let cli: cursus::cli::Cli = clap::Parser::parse_from(args);
let config = cursus::model::config::load(env.fs(), env.git().path()).await?;
cursus::run(cli, env, config).await
}
fn write_config(dir: &std::path::Path, toml: &str) {
let config_dir = dir.join(".cursus");
std::fs::create_dir_all(&config_dir).unwrap();
std::fs::write(config_dir.join("config.toml"), toml).unwrap();
}
#[tokio::test]
async fn github_config_section_loads_correctly() {
let dir = temp_git_repo();
write_config(
dir.path(),
"[cargo]\nenabled = true\n[github]\nenabled = true\n",
);
std::fs::write(
dir.path().join("Cargo.toml"),
"[package]\nname = \"my-app\"\nversion = \"0.1.0\"\n",
)
.unwrap();
let result = run_cursus(
["cursus", "publish", "--dry-run", "--no-interactive"],
dir.path(),
)
.await;
assert!(result.is_ok(), "Expected Ok, got: {result:?}");
}
#[tokio::test]
async fn github_unknown_field_causes_parse_error() {
let dir = temp_git_repo();
write_config(
dir.path(),
"[cargo]\nenabled = true\n[github]\nunknown_field = true\n",
);
let result = run_cursus(
["cursus", "publish", "--dry-run", "--no-interactive"],
dir.path(),
)
.await;
let err = result.unwrap_err();
let msg = format!("{err:#}");
assert!(
msg.contains("unknown field"),
"Expected 'unknown field' error, got: {msg}"
);
}
#[tokio::test]
async fn github_enabled_implies_git_enabled_integration() {
let dir = temp_git_repo();
write_config(
dir.path(),
"[cargo]\nenabled = true\n[github]\nenabled = true\n",
);
std::fs::write(
dir.path().join("Cargo.toml"),
"[package]\nname = \"my-app\"\nversion = \"0.1.0\"\n",
)
.unwrap();
let result = run_cursus(
["cursus", "publish", "--dry-run", "--no-interactive"],
dir.path(),
)
.await;
assert!(result.is_ok(), "Expected Ok, got: {result:?}");
}
#[tokio::test]
async fn publish_dry_run_with_github_shows_would_create() {
init_test_logger();
let _ = take_logs();
let dir = temp_git_repo();
write_config(
dir.path(),
"[cargo]\nenabled = true\n[github]\nenabled = true\nowner = \"acme\"\nrepo = \"app\"\n",
);
std::fs::write(
dir.path().join("Cargo.toml"),
"[package]\nname = \"my-app\"\nversion = \"1.0.0\"\n",
)
.unwrap();
std::fs::write(dir.path().join("CHANGELOG.md"), "# Changelog\n").unwrap();
let result = run_cursus_with_token(
["cursus", "publish", "--dry-run", "--no-interactive"],
dir.path(),
"test-token",
)
.await;
assert!(result.is_ok(), "Expected success, got: {result:?}");
let logs = take_logs();
assert!(
logs.iter()
.any(|(_, m)| m.contains("Would create GitHub Release for v1.0.0")),
"Expected 'Would create GitHub Release for v1.0.0' in logs, got: {logs:?}"
);
assert!(
logs.iter()
.any(|(_, m)| m.contains("Would publish release after artifact upload")),
"Expected 'Would publish release after artifact upload' in logs, got: {logs:?}"
);
}
#[tokio::test]
async fn publish_dry_run_with_github_no_build_command_executed() {
let dir = temp_git_repo();
write_config(
dir.path(),
"[cargo]\nenabled = true\n[github]\nenabled = true\nowner = \"acme\"\nrepo = \"app\"\nbuild_command = \"false\"\n",
);
std::fs::write(
dir.path().join("Cargo.toml"),
"[package]\nname = \"my-app\"\nversion = \"1.0.0\"\n",
)
.unwrap();
let result = run_cursus_with_token(
["cursus", "publish", "--dry-run", "--no-interactive"],
dir.path(),
"test-token",
)
.await;
assert!(
result.is_ok(),
"Expected success (build_command skipped in dry-run), got: {result:?}"
);
}
#[tokio::test]
async fn publish_build_command_failure_aborts_before_publishing() {
cursus::test_logging::init_test_logger();
let _ = cursus::test_logging::take_logs();
let dir = temp_git_repo();
write_config(
dir.path(),
"[cargo]\nenabled = true\n[github]\nenabled = true\nowner = \"acme\"\nrepo = \"app\"\nbuild_command = \"false\"\n",
);
std::fs::write(
dir.path().join("Cargo.toml"),
"[package]\nname = \"my-app\"\nversion = \"1.0.0\"\n",
)
.unwrap();
let result = run_cursus_with_token(
["cursus", "publish", "--no-interactive"],
dir.path(),
"test-token",
)
.await;
assert!(result.is_ok(), "Expected Ok(ExitCode), got: {result:?}");
assert_eq!(
result.unwrap(),
std::process::ExitCode::FAILURE,
"Expected ExitCode::FAILURE when build command fails"
);
let logs = cursus::test_logging::take_logs();
assert!(
logs.iter().any(|(_, m)| m.contains("Build command failed")),
"Expected 'Build command failed' in logs, got: {logs:?}"
);
assert!(
!logs
.iter()
.any(|(_, m)| m.contains("Published") || m.contains("Would publish")),
"No publish should have been attempted after build command failure, got: {logs:?}"
);
}
#[tokio::test]
async fn publish_github_missing_token_fails() {
let dir = temp_git_repo();
write_config(
dir.path(),
"[cargo]\nenabled = true\n[github]\nenabled = true\nowner = \"acme\"\nrepo = \"app\"\n",
);
std::fs::write(
dir.path().join("Cargo.toml"),
"[package]\nname = \"my-app\"\nversion = \"1.0.0\"\n",
)
.unwrap();
let result = run_cursus(["cursus", "publish", "--no-interactive"], dir.path()).await;
let err = result.unwrap_err();
let msg = format!("{err:#}");
assert!(
msg.contains("code forge client is unavailable"),
"Expected forge client unavailable error, got: {msg}"
);
}
#[tokio::test]
async fn publish_dry_run_with_artifacts_shows_would_attach() {
init_test_logger();
let _ = take_logs();
let dir = temp_git_repo();
write_config(
dir.path(),
"[cargo]\nenabled = true\n[github]\nenabled = true\nowner = \"acme\"\nrepo = \"app\"\n[github.artifacts.my-app]\n\"linux-amd64\" = \"target/release/my-app\"\n",
);
std::fs::write(
dir.path().join("Cargo.toml"),
"[package]\nname = \"my-app\"\nversion = \"1.0.0\"\n",
)
.unwrap();
std::fs::write(dir.path().join("CHANGELOG.md"), "# Changelog\n").unwrap();
let result = run_cursus_with_token(
["cursus", "publish", "--dry-run", "--no-interactive"],
dir.path(),
"test-token",
)
.await;
assert!(result.is_ok(), "Expected success, got: {result:?}");
let logs = take_logs();
assert!(
logs.iter()
.any(|(_, m)| m.contains("Would attach: linux-amd64")),
"Expected 'Would attach: linux-amd64' in logs, got: {logs:?}"
);
assert!(
logs.iter()
.any(|(_, m)| m.contains("Would publish release after artifact upload")),
"Expected 'Would publish release after artifact upload' in logs, got: {logs:?}"
);
}