//! Tests for [`super`]. use super::*; #[test] fn the_update_hook_guards_the_namespace_validation_reserves() { // Bash cannot read a Rust constant, so the literal in the hook is a // copy. Renaming the reserved prefix without editing the hook would // leave the new one pushable and the old one locked, which is the // failure this pins: two doors, one policy. let reserved = crate::validation::RESERVED_NOTE_NAMESPACE; assert!( UPDATE_HOOK.contains(&format!("refs/notes/{reserved}|refs/notes/{reserved}/*")), "the update hook does not guard refs/notes/{reserved}/*:\n{UPDATE_HOOK}" ); // The bare prefix and the subtree are separate patterns in a glob, and // matching only the subtree would leave `refs/notes/mnw` itself open. assert!(UPDATE_HOOK.contains("exit 1"), "{UPDATE_HOOK}"); } #[tokio::test] async fn read_capped_truncates_to_cap() { // 10k bytes through a 4k cap retains exactly 4k (the rest is drained and // discarded so the child never blocks on a full pipe). let data = vec![b'x'; 10_000]; let out = read_capped(&data[..], 4096).await; assert_eq!(out.len(), 4096); } #[tokio::test] async fn read_capped_returns_all_when_under_cap() { let out = read_capped(&b"hello world"[..], 4096).await; assert_eq!(out, "hello world"); } #[test] fn build_failure_message_partial() { assert_eq!( build_failure_message(1, 2, Some("boom")), "partial build failure (1/3 targets succeeded)" ); assert_eq!( build_failure_message(2, 1, Some("boom")), "partial build failure (2/3 targets succeeded)" ); } #[test] fn build_failure_message_total_failure_uses_first_error() { assert_eq!(build_failure_message(0, 3, Some("ssh down")), "ssh down"); assert_eq!( build_failure_message(0, 0, None), "no targets produced artifacts" ); } #[test] fn rust_target_mapping() { assert_eq!( rust_target("linux", "x86_64"), Some("x86_64-unknown-linux-gnu") ); assert_eq!( rust_target("linux", "aarch64"), Some("aarch64-unknown-linux-gnu") ); assert_eq!(rust_target("darwin", "x86_64"), Some("x86_64-apple-darwin")); assert_eq!( rust_target("darwin", "aarch64"), Some("aarch64-apple-darwin") ); assert_eq!(rust_target("windows", "x86_64"), None); } #[test] fn hook_template_contains_hmac_not_raw_token() { let hook = post_receive_hook("secret-token-123", "alice", "myrepo"); let expected_hmac = repo_hmac("secret-token-123", "alice", "myrepo"); assert!( hook.contains(&expected_hmac), "hook should contain per-repo HMAC" ); assert!( !hook.contains("secret-token-123"), "hook must not contain raw token" ); assert!(!hook.contains("__HMAC__"), "placeholder should be replaced"); assert!(hook.contains("/api/internal/builds/trigger")); } /// The two notes arms answer different refs and must not be confused for /// each other: an inbox push is merged and answered synchronously, a notes /// push is only indexed. A `case` pattern that caught both would either /// merge a ref that is already the namespace or leave a push unindexed. #[test] fn the_hook_indexes_a_notes_push_and_merges_an_inbox_push() { let hook = post_receive_hook("secret-token-123", "alice", "myrepo"); assert!(hook.contains("/api/internal/notes/reindex")); assert!(hook.contains("/api/internal/notes/merge-inbox")); assert!(hook.contains("refs/notes/*)")); assert!(hook.contains("refs/mnw/notes-inbox/*)")); // The inbox lives under refs/mnw/, so nothing an inbox push does can // fall into the indexing arm. `notes_inbox` pins that prefix itself. assert!(!"refs/mnw/notes-inbox/commits".starts_with("refs/notes/")); } /// Fixed vector, duplicated in mnw-cli's `repo_hmac` test. mnw-cli installs /// hooks for repos it auto-creates over SSH, and this endpoint verifies /// them; if either side's derivation moves, both tests have to move /// together or those pushes stop triggering builds. #[test] fn repo_hmac_matches_mnw_cli_vector() { assert_eq!( repo_hmac("test-token", "max", "repo"), "198b4a4f542c0c27c4ca333030dfe09fe670aa44bae34d7a586481bb13fd9eb0" ); } #[test] fn repo_hmac_differs_per_repo() { let h1 = repo_hmac("token", "alice", "repo-a"); let h2 = repo_hmac("token", "alice", "repo-b"); assert_ne!(h1, h2, "different repos should produce different HMACs"); } #[test] fn shell_escape_basic() { assert_eq!(shell_escape("hello"), "'hello'"); assert_eq!(shell_escape("it's"), "'it'\\''s'"); } #[test] fn validate_build_command_accepts_safe_commands() { assert!( validate_build_command("cargo build --release --target x86_64-unknown-linux-gnu").is_ok() ); assert!(validate_build_command("make -j4").is_ok()); assert!(validate_build_command("RUSTFLAGS=--cfg tokio_unstable cargo build").is_ok()); } #[test] fn validate_build_command_rejects_injection() { assert!(validate_build_command("cargo build; curl evil.com").is_err()); assert!(validate_build_command("cargo build && rm -rf /").is_err()); assert!(validate_build_command("cargo build | tee log").is_err()); assert!(validate_build_command("$(whoami)").is_err()); assert!(validate_build_command("`whoami`").is_err()); assert!(validate_build_command("cargo build > /dev/null").is_err()); assert!(validate_build_command("").is_err()); assert!( validate_build_command(" ").is_err(), "whitespace-only has no program" ); assert!( validate_build_command("FOO=bar").is_err(), "assignment with no program" ); } #[test] fn remote_command_parse_separates_env_program_args() { let c = RemoteCommand::parse("cargo build --release").unwrap(); assert!(c.assignments.is_empty()); assert_eq!(c.program, "cargo"); assert_eq!(c.args, vec!["build", "--release"]); let c = RemoteCommand::parse("RUSTFLAGS=--cfg CARGO_INCREMENTAL=0 cargo build").unwrap(); assert_eq!( c.assignments, vec!["RUSTFLAGS=--cfg", "CARGO_INCREMENTAL=0"] ); assert_eq!(c.program, "cargo"); assert_eq!(c.args, vec!["build"]); } #[test] fn remote_command_render_escapes_every_token() { // Plain command: each token individually single-quoted. let c = RemoteCommand::parse("cargo build --release").unwrap(); assert_eq!(c.render(), "'cargo' 'build' '--release'"); // Env prefix: applied via `env`, each element escaped. let c = RemoteCommand::parse("RUSTFLAGS=--cfg cargo build").unwrap(); assert_eq!(c.render(), "env 'RUSTFLAGS=--cfg' 'cargo' 'build'"); } #[test] fn is_env_assignment_recognizes_valid_identifiers_only() { assert!(is_env_assignment("FOO=bar")); assert!(is_env_assignment("_X1=y")); assert!(is_env_assignment("A=")); // empty value is a valid assignment assert!( !is_env_assignment("1FOO=bar"), "identifier can't start with a digit" ); assert!(!is_env_assignment("cargo"), "no '='"); assert!(!is_env_assignment("--target=x"), "not a shell identifier"); } #[test] fn render_defuses_would_be_injection_even_if_charset_bypassed() { // Construct a RemoteCommand directly with a hostile arg (bypassing the // token charset check) to prove render() is the real guard: the shell // sees a single quoted word, not a command separator. let c = RemoteCommand { assignments: vec![], program: "cargo".to_string(), args: vec!["build; rm -rf /".to_string()], }; assert_eq!(c.render(), "'cargo' 'build; rm -rf /'"); } #[test] fn validate_artifact_path_accepts_safe_paths() { assert!(validate_artifact_path("target/x86_64-unknown-linux-gnu/release/myapp").is_ok()); assert!(validate_artifact_path("dist/app-v0.1.0.tar.gz").is_ok()); } #[test] fn validate_artifact_path_rejects_unsafe() { assert!(validate_artifact_path("/etc/passwd").is_err()); assert!(validate_artifact_path("../../../etc/passwd").is_err()); assert!(validate_artifact_path("path with spaces").is_err()); assert!(validate_artifact_path("$(whoami)").is_err()); assert!(validate_artifact_path("").is_err()); }