@@ -0,0 +1,177 @@
|
||||
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 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
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user