use base64::Engine;
use flate2::write::GzEncoder;
use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf};
use tar::Builder;
use tempfile::TempDir;
use terraphim_update::signature::{VerificationResult, verify_archive_signature};
fn create_test_archive(dir: &Path, name: &str) -> PathBuf {
let archive_path = dir.join(name);
let file = fs::File::create(&archive_path).unwrap();
let enc = GzEncoder::new(file, flate2::Compression::default());
let mut tar = Builder::new(enc);
let mut header = tar::Header::new_gnu();
header.set_path("test.txt").unwrap();
header.set_size(12);
header.set_mode(0o644);
header.set_cksum();
let mut data = "Hello World\n".as_bytes();
tar.append(&header, &mut data).unwrap();
tar.into_inner().unwrap().finish().unwrap();
archive_path
}
#[cfg(feature = "integration-signing")]
fn sign_archive(archive_path: &Path, private_key: &str) -> Result<(), Box<dyn std::error::Error>> {
use std::process::Command;
let output = Command::new("zipsign")
.args(["sign", "tar", archive_path.to_str().unwrap(), private_key])
.output()?;
if !output.status.success() {
return Err(format!(
"zipsign failed: {}",
String::from_utf8_lossy(&output.stderr)
)
.into());
}
Ok(())
}
#[test]
fn test_real_key_rejects_unsigned_archive() {
let temp_dir = TempDir::new().unwrap();
let archive = create_test_archive(temp_dir.path(), "test.tar.gz");
let result = verify_archive_signature(&archive, None).unwrap();
assert!(matches!(result, VerificationResult::Invalid { .. }));
if let VerificationResult::Invalid { reason } = result {
assert!(
reason.contains("Signature verification failed")
|| reason.contains("Failed to read signatures")
|| reason.contains("magic")
);
}
}
#[test]
fn test_nonexistent_archive_returns_error() {
let result = verify_archive_signature(&PathBuf::from("/nonexistent/archive.tar.gz"), None);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("not found"));
}
#[test]
fn test_invalid_base64_key_returns_error() {
let temp_dir = TempDir::new().unwrap();
let archive = create_test_archive(temp_dir.path(), "test.tar.gz");
let result = verify_archive_signature(&archive, Some("not-valid-base64!!!"));
assert!(result.is_err());
}
#[test]
fn test_wrong_length_key_returns_invalid() {
let temp_dir = TempDir::new().unwrap();
let archive = create_test_archive(temp_dir.path(), "test.tar.gz");
let short_key = base64::engine::general_purpose::STANDARD.encode(b"short");
let result = verify_archive_signature(&archive, Some(&short_key)).unwrap();
assert!(matches!(result, VerificationResult::Invalid { .. }));
if let VerificationResult::Invalid { reason } = result {
assert!(reason.contains("32") || reason.contains("length") || reason.contains("Invalid"));
}
}
#[test]
fn test_empty_archive_without_signature() {
let temp_dir = TempDir::new().unwrap();
let archive_path = temp_dir.path().to_path_buf().join("empty.tar.gz");
let file = fs::File::create(&archive_path).unwrap();
let enc = GzEncoder::new(file, flate2::Compression::default());
let _tar = Builder::new(enc);
let result = verify_archive_signature(&archive_path, None).unwrap();
assert!(matches!(result, VerificationResult::Invalid { .. }));
}
#[test]
fn test_verification_result_equality() {
let valid1 = VerificationResult::Valid;
let valid2 = VerificationResult::Valid;
assert_eq!(valid1, valid2);
let invalid1 = VerificationResult::Invalid {
reason: "test".to_string(),
};
let invalid2 = VerificationResult::Invalid {
reason: "test".to_string(),
};
assert_eq!(invalid1, invalid2);
let missing1 = VerificationResult::MissingSignature;
let missing2 = VerificationResult::MissingSignature;
assert_eq!(missing1, missing2);
assert_ne!(valid1, missing1);
assert_ne!(invalid1, missing1);
}
#[test]
fn test_verification_result_debug_format() {
let valid = VerificationResult::Valid;
assert_eq!(format!("{:?}", valid), "Valid");
let missing = VerificationResult::MissingSignature;
assert_eq!(format!("{:?}", missing), "MissingSignature");
let invalid = VerificationResult::Invalid {
reason: "test error".to_string(),
};
let formatted = format!("{:?}", invalid);
assert!(formatted.contains("test error"));
}
#[test]
fn test_corrupted_archive_returns_error() {
let temp_dir = TempDir::new().unwrap();
let archive_path = temp_dir.path().to_path_buf().join("corrupted.tar.gz");
let mut file = fs::File::create(&archive_path).unwrap();
file.write_all(b"This is not a valid gzip file").unwrap();
let result = verify_archive_signature(&archive_path, None).unwrap();
assert!(matches!(result, VerificationResult::Invalid { .. }));
if let VerificationResult::Invalid { reason } = result {
assert!(
reason.contains("Signature verification failed")
|| reason.contains("magic")
|| reason.contains("corrupted")
);
}
}
#[test]
fn test_verification_with_custom_public_key() {
let temp_dir = TempDir::new().unwrap();
let archive = create_test_archive(temp_dir.path(), "test.tar.gz");
let public_key = base64::engine::general_purpose::STANDARD.encode(vec![0u8; 32]);
let result = verify_archive_signature(&archive, Some(&public_key)).unwrap();
assert!(matches!(result, VerificationResult::Invalid { .. }));
}
#[test]
fn test_multiple_verifications_same_archive() {
let temp_dir = TempDir::new().unwrap();
let archive = create_test_archive(temp_dir.path(), "test.tar.gz");
let result1 = verify_archive_signature(&archive, None).unwrap();
let result2 = verify_archive_signature(&archive, None).unwrap();
let result3 = verify_archive_signature(&archive, None).unwrap();
assert!(matches!(result1, VerificationResult::Invalid { .. }));
assert!(matches!(result2, VerificationResult::Invalid { .. }));
assert!(matches!(result3, VerificationResult::Invalid { .. }));
assert_eq!(result1, result2);
assert_eq!(result2, result3);
}
#[test]
fn test_verification_non_file_path() {
let temp_dir = TempDir::new().unwrap();
let result = verify_archive_signature(temp_dir.path(), None);
match result {
Ok(_) => {} Err(e) => {
assert!(
e.to_string().contains("Failed to read") || e.to_string().contains("archive file")
);
}
}
}
#[cfg(feature = "integration-signing")]
mod integration_tests {
use super::*;
use std::process::Command;
#[test]
fn test_signed_archive_verification() {
if !Command::new("zipsign")
.arg("--version")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
{
println!("Skipping test: zipsign not installed");
return;
}
let temp_dir = TempDir::new().unwrap();
let key_dir = temp_dir.path().to_path_buf().join("keys");
fs::create_dir(&key_dir).unwrap();
let private_key = key_dir.join("private.key");
let public_key = key_dir.join("public.key");
let output = Command::new("zipsign")
.args([
"gen-key",
private_key.to_str().unwrap(),
public_key.to_str().unwrap(),
])
.output()
.unwrap();
assert!(output.status.success(), "Failed to generate key pair");
let archive = create_test_archive(temp_dir.path(), "signed.tar.gz");
sign_archive(&archive, private_key.to_str().unwrap()).unwrap();
let public_key_bytes = fs::read_to_string(&public_key).unwrap();
let public_key_b64 = public_key_bytes.trim();
let result = verify_archive_signature(&archive, Some(public_key_b64)).unwrap();
assert_eq!(result, VerificationResult::Valid);
}
#[test]
fn test_wrong_key_rejects_signed_archive() {
if !Command::new("zipsign")
.arg("--version")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
{
println!("Skipping test: zipsign not installed");
return;
}
let temp_dir = TempDir::new().unwrap();
let key_dir = temp_dir.path().to_path_buf().join("keys");
fs::create_dir(&key_dir).unwrap();
let private_key = key_dir.join("private.key");
let public_key = key_dir.join("public.key");
let output = Command::new("zipsign")
.args([
"gen-key",
private_key.to_str().unwrap(),
public_key.to_str().unwrap(),
])
.output()
.unwrap();
assert!(output.status.success(), "Failed to generate key pair");
let archive = create_test_archive(temp_dir.path(), "signed.tar.gz");
sign_archive(&archive, private_key.to_str().unwrap()).unwrap();
let wrong_key = base64::engine::general_purpose::STANDARD.encode(vec![255u8; 32]);
let result = verify_archive_signature(&archive, Some(&wrong_key)).unwrap();
assert!(matches!(result, VerificationResult::Invalid { .. }));
}
#[test]
fn test_tampered_archive_rejected() {
if !Command::new("zipsign")
.arg("--version")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
{
println!("Skipping test: zipsign not installed");
return;
}
let temp_dir = TempDir::new().unwrap();
let key_dir = temp_dir.path().to_path_buf().join("keys");
fs::create_dir(&key_dir).unwrap();
let private_key = key_dir.join("private.key");
let public_key = key_dir.join("public.key");
let output = Command::new("zipsign")
.args([
"gen-key",
private_key.to_str().unwrap(),
public_key.to_str().unwrap(),
])
.output()
.unwrap();
assert!(output.status.success(), "Failed to generate key pair");
let archive = create_test_archive(temp_dir.path(), "tampered.tar.gz");
sign_archive(&archive, private_key.to_str().unwrap()).unwrap();
{
let mut file = fs::OpenOptions::new().append(true).open(&archive).unwrap();
file.write_all(b"TAMPERED DATA").unwrap();
}
let public_key_bytes = fs::read_to_string(&public_key).unwrap();
let public_key_b64 = public_key_bytes.trim();
let result = verify_archive_signature(&archive, Some(public_key_b64)).unwrap();
assert!(matches!(result, VerificationResult::Invalid { .. }));
}
}
#[test]
fn test_verification_deterministic() {
let temp_dir = TempDir::new().unwrap();
let archive = create_test_archive(temp_dir.path(), "test.tar.gz");
let mut results = Vec::new();
for _ in 0..10 {
let result = verify_archive_signature(&archive, None).unwrap();
results.push(result);
}
for result in &results[1..] {
assert_eq!(results[0], *result);
}
}
#[test]
fn test_verification_no_panic() {
let test_cases: Vec<PathBuf> = vec![
PathBuf::from("/nonexistent/file.tar.gz"),
PathBuf::from("/tmp"),
PathBuf::from(""),
];
for path in test_cases {
let _ = verify_archive_signature(&path, None);
}
}
#[test]
fn test_verification_performance_small_archive() {
let temp_dir = TempDir::new().unwrap();
let archive = create_test_archive(temp_dir.path(), "small.tar.gz");
let start = std::time::Instant::now();
let _result = verify_archive_signature(&archive, None).unwrap();
let elapsed = start.elapsed();
assert!(
elapsed.as_millis() < 100,
"Verification took too long: {:?}",
elapsed
);
}
#[test]
fn test_verification_multiple_archives_performance() {
let temp_dir = TempDir::new().unwrap();
let archives: Vec<PathBuf> = (0..10)
.map(|i| create_test_archive(temp_dir.path(), &format!("test{}.tar.gz", i)))
.collect();
let start = std::time::Instant::now();
for archive in &archives {
let _result = verify_archive_signature(archive, None).unwrap();
}
let elapsed = start.elapsed();
assert!(
elapsed.as_secs() < 1,
"Batch verification took too long: {:?}",
elapsed
);
}