Files
ProxyWarden/src-tauri/tests/configuration_transaction_tests.rs
T
dokril c6b66a4ed7
CI / Windows baseline (push) Canceled after 0s
Release v2.0.0
2026-09-11 18:29:33 +03:00

198 lines
6.8 KiB
Rust

use proxywarden_lib::{
configuration_transaction::{read_guard, revision_locked, ConfigurationTransaction},
storage::JsonStorage,
};
use std::{fs, path::PathBuf};
struct Fixture {
root: PathBuf,
storage: JsonStorage,
}
impl Fixture {
fn new() -> Self {
let root = std::env::temp_dir().join(format!("pw-transaction-{}", uuid::Uuid::new_v4()));
let storage = JsonStorage::new(&root);
storage.write_profiles(&[]).unwrap();
storage.write_targets(&[]).unwrap();
Self { root, storage }
}
}
impl Drop for Fixture {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.root);
}
}
#[test]
fn interrupted_commit_restores_primary_and_backup_before_next_read() {
let fixture = Fixture::new();
let path = &fixture.storage.paths().profiles_file;
let before = fs::read(path).unwrap();
let transaction = ConfigurationTransaction::begin(&fixture.storage, None).unwrap();
fixture.storage.write_profiles(&[]).unwrap();
fs::write(path, b"half-written").unwrap();
transaction.abort().unwrap();
assert_eq!(fs::read(path).unwrap(), before);
assert!(!proxywarden_lib::safe_fs::backup_path(path).exists());
let _guard = read_guard(&fixture.storage).unwrap();
assert!(fixture.storage.read_profiles().unwrap().is_empty());
}
#[test]
fn rollback_leaves_unchanged_files_in_place() {
let fixture = Fixture::new();
let path = &fixture.storage.paths().profiles_file;
let transaction = ConfigurationTransaction::begin(&fixture.storage, None).unwrap();
// Deny replacement on Windows, while allowing the recovery code to read.
let mut options = fs::OpenOptions::new();
options.read(true);
#[cfg(windows)]
{
use std::os::windows::fs::OpenOptionsExt;
options.share_mode(1);
}
let held = options.open(path).unwrap();
let modified = held.metadata().unwrap().modified().unwrap();
transaction.abort().unwrap();
assert_eq!(fs::metadata(path).unwrap().modified().unwrap(), modified);
assert!(read_guard(&fixture.storage).is_ok());
}
#[test]
fn shared_lock_rejects_second_writer_and_reader() {
let fixture = Fixture::new();
let transaction = ConfigurationTransaction::begin(&fixture.storage, None).unwrap();
assert!(ConfigurationTransaction::begin(&fixture.storage, None).is_err());
assert!(read_guard(&fixture.storage).is_err());
transaction.commit().unwrap();
assert!(read_guard(&fixture.storage).is_ok());
}
#[test]
fn committed_revision_rejects_delayed_result_even_when_values_are_identical() {
let fixture = Fixture::new();
let revision = {
let _guard = read_guard(&fixture.storage).unwrap();
revision_locked(&fixture.storage).unwrap()
};
ConfigurationTransaction::begin(&fixture.storage, Some(&revision))
.unwrap()
.commit()
.unwrap();
assert!(ConfigurationTransaction::begin(&fixture.storage, Some(&revision)).is_err());
}
#[test]
fn damaged_snapshot_blocks_all_restoration_and_next_writer() {
let fixture = Fixture::new();
let transaction = ConfigurationTransaction::begin(&fixture.storage, None).unwrap();
let path = &fixture.storage.paths().profiles_file;
fs::write(path, b"new-state").unwrap();
fs::write(
fixture
.storage
.paths()
.migrations_dir
.join("configuration-before-2.json"),
b"damaged",
)
.unwrap();
assert!(transaction.abort().is_err());
assert_eq!(
fs::read(path).unwrap(),
b"new-state",
"validate all snapshots before restoring any"
);
assert!(read_guard(&fixture.storage).is_err());
assert!(ConfigurationTransaction::begin(&fixture.storage, None).is_err());
}
#[test]
fn successful_commit_removes_sensitive_fixed_snapshots() {
let fixture = Fixture::new();
ConfigurationTransaction::begin(&fixture.storage, None)
.unwrap()
.commit()
.unwrap();
for item in fs::read_dir(&fixture.storage.paths().migrations_dir).unwrap() {
let name = item.unwrap().file_name().to_string_lossy().to_string();
assert!(
!name.starts_with("configuration-before-") && !name.starts_with("configuration-commit")
);
}
}
#[test]
fn transaction_child() {
let Some(root) = std::env::var_os("PW_TEST_TRANSACTION_ROOT") else {
return;
};
let storage = JsonStorage::new(PathBuf::from(root));
let Ok(_transaction) = ConfigurationTransaction::begin(&storage, None) else {
std::process::exit(2);
};
fs::write(&storage.paths().profiles_file, b"interrupted-child-write").unwrap();
// Deliberately bypass Drop, as a terminated application does.
std::process::exit(0);
}
#[test]
fn committed_marker_survives_partial_snapshot_cleanup_without_rollback() {
let fixture = Fixture::new();
let transaction = ConfigurationTransaction::begin(&fixture.storage, None).unwrap();
fs::write(&fixture.storage.paths().profiles_file, b"committed-state").unwrap();
// Model death after publishing the terminal marker and removing one snapshot.
let journal = fixture
.storage
.paths()
.migrations_dir
.join("configuration-commit.json");
let mut intent: serde_json::Value =
serde_json::from_slice(&fs::read(&journal).unwrap()).unwrap();
intent["committed"] = serde_json::Value::Bool(true);
fs::write(&journal, serde_json::to_vec(&intent).unwrap()).unwrap();
fs::remove_file(
fixture
.storage
.paths()
.migrations_dir
.join("configuration-before-0.json"),
)
.unwrap();
drop(transaction);
let _guard = read_guard(&fixture.storage).unwrap();
assert_eq!(
fs::read(&fixture.storage.paths().profiles_file).unwrap(),
b"committed-state"
);
assert!(!journal.exists());
}
#[test]
fn process_death_is_recovered_before_normal_read_and_lock_excludes_other_processes() {
let fixture = Fixture::new();
let before = fs::read(&fixture.storage.paths().profiles_file).unwrap();
let launch = || {
std::process::Command::new(std::env::current_exe().unwrap())
.args(["--exact", "transaction_child"])
.env("PW_TEST_TRANSACTION_ROOT", &fixture.root)
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.unwrap()
};
let transaction = ConfigurationTransaction::begin(&fixture.storage, None).unwrap();
assert_eq!(launch().code(), Some(2));
transaction.abort().unwrap();
assert!(launch().success());
assert_eq!(
fs::read(&fixture.storage.paths().profiles_file).unwrap(),
b"interrupted-child-write"
);
let _guard = read_guard(&fixture.storage).unwrap();
assert_eq!(
fs::read(&fixture.storage.paths().profiles_file).unwrap(),
before
);
}