use std::fs; use std::os::unix::fs::PermissionsExt; use std::path::Path; use wireguard_admin::app::{Application, RestartChoice, RestartStage}; use wireguard_admin::config::{ResolvedSettings, SettingsFile}; use wireguard_admin::domain::{ Client, ClientAddresses, ClientStates, RuntimeObservation, Snapshot, }; use wireguard_admin::persistence::SavedStore; use wireguard_admin::vault::WireGuardPrivateKey; fn settings(service: &str, timeout: u64) -> String { format!( r#" interface_name = "wg0" server_config_path = "server.conf" client_source_directory = "clients" server_endpoint = "vpn.example.test:51820" ipv4_allocation_range = "10.20.0.0/24" ipv6_allocation_range = "fd20::/64" dns_addresses = ["10.20.0.1"] systemd_unit = "{service}" restart_timeout_seconds = {timeout} "# ) } struct Fixture { _root: tempfile::TempDir, store: SavedStore, initial_settings: String, } fn fixture(service: &str, timeout: u64) -> Fixture { let root = tempfile::tempdir().expect("temporary saved store"); let initial_settings = settings(service, timeout); let settings = SettingsFile::parse_toml(&initial_settings) .expect("synthetic settings parse") .validate() .expect("synthetic settings validate") .resolve_paths(root.path()); fs::create_dir_all(&settings.client_source_directory).expect("client directory"); fs::create_dir_all(&settings.data_directory).expect("data directory"); fs::create_dir_all(&settings.archives_directory).expect("archive directory"); fs::write( ResolvedSettings::settings_path(root.path()), initial_settings.as_bytes(), ) .expect("install synthetic settings"); fs::write( &settings.server_config_path, b"[Interface]\nPrivateKey = synthetic-server-key\nAddress = 10.20.0.1/24\n", ) .expect("install synthetic server configuration"); fs::set_permissions( &settings.server_config_path, fs::Permissions::from_mode(0o600), ) .expect("restrict synthetic server configuration"); let store = SavedStore::new(root.path(), settings.clone()).expect("saved store mapping"); Fixture { _root: root, store, initial_settings, } } fn open(fixture: &Fixture) -> Application { let states = ClientStates::new(Snapshot::default(), None, RuntimeObservation::Unknown) .expect("empty saved states"); Application::open(fixture.store.clone(), states).expect("open synthetic application") } fn archive_name(path: &Path) -> &str { path.file_name() .and_then(|name| name.to_str()) .expect("archive filename") } #[test] fn public_restart_preview_and_cancel_preserve_draft_and_saved_files() { let fixture = fixture("wg-quick@wg0.service", 30); let mut app = open(&fixture); let original_server_config = app.saved_bytes().server_config.clone(); let secret = WireGuardPrivateKey::generate().expect("synthetic client key"); let client = Client::new( secret.public_key(), "draft client", ClientAddresses { ipv4: "10.20.0.2".parse().expect("synthetic IPv4"), ipv6: Some("fd20::2".parse().expect("synthetic IPv6")), }, ) .expect("synthetic client"); app.add_client(client.clone(), None) .expect("stage client draft"); let preview = app.preview_restart().expect("preview saved generation"); assert!(preview.has_draft()); assert_eq!(preview.interface(), "wg0"); assert_eq!(preview.systemd_unit(), "wg-quick@wg0.service"); assert!(app.list_backups().expect("list archives").is_empty()); // The public cancellation path returns before either real host adapter can run. let report = app.restart(preview, RestartChoice::Cancel); assert_eq!(report.stage, RestartStage::Cancelled); assert!(!report.restart_submitted); assert_eq!(app.saved_bytes().server_config, original_server_config); assert!(!app.document().render().is_empty()); assert_eq!( app.states() .draft() .get(&client.identity) .map(|saved| saved.alias.as_str()), Some("draft client") ); assert!( app.list_backups() .expect("no cancellation backup") .is_empty() ); assert_eq!(app.states().applied(), &RuntimeObservation::Unknown); } #[test] fn restore_changes_the_settings_used_by_the_next_restart_preview() { let fixture = fixture("wg-quick@wg0.service", 30); let app = open(&fixture); let archive = app .create_manual_backup() .expect("archive original saved settings"); let replacement = settings("wg-quick@wg9.service", 90); fs::write( ResolvedSettings::settings_path(fixture._root.path()), replacement.as_bytes(), ) .expect("install a second synthetic saved settings generation"); drop(app); let mut app = open(&fixture); let preview = app .preview_restore(archive_name(&archive)) .expect("preview archived saved settings"); app.confirm_restore(preview) .expect("restore archived saved settings"); assert_eq!( app.saved_bytes().settings, fixture.initial_settings.as_bytes() ); assert_eq!(app.states().applied(), &RuntimeObservation::Unknown); let restart = app .preview_restart() .expect("preview restored generation without invoking adapters"); assert_eq!(restart.interface(), "wg0"); assert_eq!(restart.systemd_unit(), "wg-quick@wg0.service"); assert!(!restart.has_draft()); }