//! Crash consistency tests for the cache write protocol. //! //! Simulates partial writes at each stage of the crash-consistent write protocol //! and verifies that the read protocol detects corruption and doctor --fix repairs it. mod helpers; use std::fs; /// Helper: fetch a spec into the test environment so we have valid cache state. fn setup_with_cached_spec(env: &helpers::TestEnv) { helpers::fetch_fixture(env, "petstore.json", "petstore"); } /// When raw.json exists but meta.json is missing (crash before commit marker), /// load_index should fail with AliasNotFound (meta is the commit marker). #[test] fn test_crash_before_meta_write() { let env = helpers::TestEnv::new(); setup_with_cached_spec(&env); let alias_dir = env.cache_dir.join("petstore"); let meta_path = alias_dir.join("meta.json"); // Simulate crash: remove meta.json (the commit marker) assert!(meta_path.exists(), "meta.json should exist after fetch"); fs::remove_file(&meta_path).expect("failed to remove meta.json"); // Read should fail — no commit marker let result = helpers::run_cmd(&env, &["list", "petstore", "--robot"]); result.failure(); } /// When meta.json exists but index.json is corrupted (partial write), /// load_index should fail with CacheIntegrity (hash mismatch). #[test] fn test_crash_corrupted_index() { let env = helpers::TestEnv::new(); setup_with_cached_spec(&env); let alias_dir = env.cache_dir.join("petstore"); let index_path = alias_dir.join("index.json"); // Corrupt the index file fs::write(&index_path, b"{ corrupted }").expect("failed to corrupt index"); // Read should fail — hash mismatch let result = helpers::run_cmd(&env, &["list", "petstore", "--robot"]); result.failure(); } /// When meta.json exists but raw.json is corrupted, /// load_raw should fail with CacheIntegrity (hash mismatch). /// Note: list/search only need index.json, so we test via show which needs raw.json. #[test] fn test_crash_corrupted_raw_json() { let env = helpers::TestEnv::new(); setup_with_cached_spec(&env); let alias_dir = env.cache_dir.join("petstore"); let raw_path = alias_dir.join("raw.json"); // Corrupt raw.json fs::write(&raw_path, b"{ corrupted }").expect("failed to corrupt raw.json"); // show needs raw.json for $ref expansion → should fail let result = helpers::run_cmd( &env, &["show", "petstore", "/pets", "--method", "GET", "--robot"], ); result.failure(); } /// Doctor --fix should repair a missing index by rebuilding from raw.json. #[test] fn test_doctor_fix_repairs_missing_index() { let env = helpers::TestEnv::new(); setup_with_cached_spec(&env); let alias_dir = env.cache_dir.join("petstore"); let index_path = alias_dir.join("index.json"); // Remove index.json fs::remove_file(&index_path).expect("failed to remove index.json"); // Doctor --fix should repair let result = helpers::run_cmd(&env, &["doctor", "--fix", "--robot"]); result.success(); // After fix, list should work again let result = helpers::run_cmd(&env, &["list", "petstore", "--robot"]); result.success(); } /// Doctor --fix should repair a corrupted index (hash mismatch) by rebuilding. #[test] fn test_doctor_fix_repairs_corrupted_index() { let env = helpers::TestEnv::new(); setup_with_cached_spec(&env); let alias_dir = env.cache_dir.join("petstore"); let index_path = alias_dir.join("index.json"); // Corrupt the index fs::write(&index_path, b"[]").expect("failed to corrupt index"); // Doctor --fix should detect and repair let result = helpers::run_cmd(&env, &["doctor", "--fix", "--robot"]); result.success(); // After fix, list should work let result = helpers::run_cmd(&env, &["list", "petstore", "--robot"]); result.success(); } /// When meta.json has a generation mismatch with index.json, read should fail. #[test] fn test_generation_mismatch_detected() { let env = helpers::TestEnv::new(); setup_with_cached_spec(&env); let alias_dir = env.cache_dir.join("petstore"); let meta_path = alias_dir.join("meta.json"); // Tamper with generation in meta.json let meta_bytes = fs::read(&meta_path).expect("failed to read meta"); let mut meta: serde_json::Value = serde_json::from_slice(&meta_bytes).expect("failed to parse meta"); meta["generation"] = serde_json::json!(9999); let tampered = serde_json::to_vec_pretty(&meta).expect("failed to serialize"); fs::write(&meta_path, &tampered).expect("failed to write tampered meta"); // Read should fail — generation mismatch let result = helpers::run_cmd(&env, &["list", "petstore", "--robot"]); result.failure(); } /// Leftover .tmp files from a crash should not interfere with normal operation. #[test] fn test_leftover_tmp_files_harmless() { let env = helpers::TestEnv::new(); setup_with_cached_spec(&env); let alias_dir = env.cache_dir.join("petstore"); // Create leftover .tmp files (simulating interrupted write) fs::write(alias_dir.join("raw.json.tmp"), b"partial data").expect("failed to create tmp file"); fs::write(alias_dir.join("index.json.tmp"), b"partial index") .expect("failed to create tmp file"); // Normal operations should still work let result = helpers::run_cmd(&env, &["list", "petstore", "--robot"]); result.success(); } /// A completely empty alias directory (no files at all) should be treated as not found. #[test] fn test_empty_alias_directory() { let env = helpers::TestEnv::new(); // Create an empty directory that looks like an alias let empty_alias_dir = env.cache_dir.join("ghost-alias"); fs::create_dir_all(&empty_alias_dir).expect("failed to create dir"); // Should fail — no meta.json let result = helpers::run_cmd(&env, &["list", "ghost-alias", "--robot"]); result.failure(); }