198 lines
6.8 KiB
Rust
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
|
|
);
|
|
}
|