use std::net::{Ipv4Addr, SocketAddr};
use a7n_ipc::config::Config;
use tempfile::TempDir;
use super::*;
use crate::settings_store::SettingsStore;
fn daemon() -> (Daemon, TempDir) {
let dir = tempfile::tempdir().unwrap();
let mut daemon = Daemon::load(dir.path(), SocketAddr::from((Ipv4Addr::LOCALHOST, 0))).unwrap();
daemon.detect_settings_edit().unwrap();
(daemon, dir)
}
fn edit(daemon: &Daemon, values: &[(&str, &str)]) -> SettingsSync {
let mut wire = daemon.settings.state.as_ref().unwrap().to_proto();
wire.set_at += 1;
wire.values = values
.iter()
.map(|(key, value)| ((*key).into(), (*value).into()))
.collect();
wire
}
#[tokio::test]
async fn receive_settings_swapped_home_and_lock_accepts_snapshot() {
let (mut daemon, _dir) = daemon();
daemon.config.set("hotkeys.home", "escape").unwrap();
let home = daemon.config.get("hotkeys.home").unwrap();
let lock = daemon.config.get("hotkeys.lock").unwrap();
let wire = edit(&daemon, &[("hotkeys.home", &lock), ("hotkeys.lock", &home)]);
daemon.receive_settings(&wire).await.unwrap();
assert_eq!(daemon.config.get("hotkeys.home"), Some(lock.clone()));
assert_eq!(daemon.config.get("hotkeys.lock"), Some(home.clone()));
let saved = Config::load(&daemon.config_path).unwrap();
assert_eq!(saved.get("hotkeys.home"), Some(lock));
assert_eq!(saved.get("hotkeys.lock"), Some(home));
assert_eq!(
daemon.settings.state,
Some(Settings::try_from(&wire).unwrap())
);
}
#[tokio::test]
async fn receive_settings_peer_security_key_keeps_local_policy() {
let (a, a_dir) = daemon();
let (mut b, b_dir) = daemon();
let clipboard_key = format!("peers.{}.clipboard", a.identity.machine_id);
b.config.set(&clipboard_key, "off").unwrap();
let files_key = format!("peers.{}.files", a.identity.machine_id);
b.config.set(&files_key, "off").unwrap();
let receive_dir = b_dir.path().join("received");
b.config
.set("files.receive_dir", receive_dir.to_str().unwrap())
.unwrap();
b.config.clipboard.deny_apps = vec!["secret.app".into()];
let key = format!("peers.{}.allow_drive", a.identity.machine_id);
b.config.set(&key, "true").unwrap();
b.config.save(&b.config_path).unwrap();
let wire = edit(
&b,
&[
("input.speed", "2"),
(&key, "false"),
(&clipboard_key, "both"),
(&files_key, "both"),
("files.receive_dir", a_dir.path().to_str().unwrap()),
("clipboard.deny_apps", "[]"),
("name", "hostile"),
],
);
b.receive_settings(&wire).await.unwrap();
let saved = Config::load(&b.config_path).unwrap();
assert_eq!(saved.get(&key).as_deref(), Some("true"));
assert_eq!(b.config.get(&key).as_deref(), Some("true"));
assert_eq!(saved.get(&clipboard_key).as_deref(), Some("off"));
assert_eq!(b.config.get(&clipboard_key).as_deref(), Some("off"));
assert_eq!(saved.get(&files_key).as_deref(), Some("off"));
assert_eq!(b.config.get(&files_key).as_deref(), Some("off"));
assert_eq!(saved.files.receive_dir.as_ref(), Some(&receive_dir));
assert_eq!(b.config.files.receive_dir.as_ref(), Some(&receive_dir));
assert_eq!(saved.clipboard.deny_apps, ["secret.app"]);
assert!(saved.name.is_none());
assert_eq!(saved.input.speed, 2.0);
let restored = SettingsStore::load(&b_dir.path().join("settings.pb"));
let values = restored.state.unwrap().values;
assert!(!values.contains_key(&key));
assert!(!values.contains_key(&clipboard_key));
assert!(!values.contains_key(&files_key));
assert!(!values.contains_key("files.receive_dir"));
assert!(!values.contains_key("clipboard.deny_apps"));
}
#[tokio::test]
async fn receive_settings_invalid_value_rejects_entire_message() {
let (mut daemon, _dir) = daemon();
daemon.config.save(&daemon.config_path).unwrap();
let before = std::fs::read(&daemon.config_path).unwrap();
let snapshot = daemon.settings.state.clone();
let wire = edit(
&daemon,
&[("input.natural_scrolling", "true"), ("input.speed", "0")],
);
daemon.receive_settings(&wire).await.unwrap();
assert_eq!(std::fs::read(&daemon.config_path).unwrap(), before);
assert!(!daemon.config.input.natural_scrolling);
assert_eq!(daemon.settings.state, snapshot);
}
#[tokio::test]
async fn receive_settings_old_or_disabled_message_keeps_config_and_snapshot() {
for mode in ["older", "local_off", "remote_off"] {
let (mut daemon, _dir) = daemon();
let mut wire = edit(&daemon, &[("input.speed", "2")]);
match mode {
"older" => wire.set_at = 0,
"local_off" => daemon.config.sync.settings = false,
_ => wire.on = false,
}
daemon.config.save(&daemon.config_path).unwrap();
let before = std::fs::read(&daemon.config_path).unwrap();
let snapshot = daemon.settings.state.clone();
daemon.receive_settings(&wire).await.unwrap();
assert_eq!(std::fs::read(&daemon.config_path).unwrap(), before);
assert_eq!(daemon.config.input.speed, 1.0);
assert_eq!(daemon.settings.state, snapshot);
if mode == "local_off" {
assert!(daemon.settings_message().is_none());
}
}
}
#[tokio::test]
async fn reload_config_unchanged_remote_snapshot_preserves_version() {
let (mut daemon, dir) = daemon();
let mut wire = daemon.settings.state.as_ref().unwrap().to_proto();
wire.set_at += 1;
wire.values.insert("input.speed".into(), "2.0".into());
daemon.receive_settings(&wire).await.unwrap();
let snapshot = daemon.settings.state.clone();
daemon.reload_config().await.unwrap();
assert_eq!(daemon.settings.state, snapshot);
let mut restarted = Daemon::load(dir.path(), SocketAddr::from((Ipv4Addr::LOCALHOST, 0)))
.unwrap()
.with_config(
Config::load(&daemon.config_path).unwrap(),
daemon.config_path.clone(),
);
restarted.detect_settings_edit().unwrap();
assert_eq!(restarted.settings.state, snapshot);
}
#[tokio::test]
async fn detect_settings_edit_clock_behind_snapshot_advances_timestamp() {
let (mut daemon, _dir) = daemon();
let state = daemon.settings.state.as_mut().unwrap();
state.set_at = u64::MAX - 1;
daemon.config.input.speed = 2.0;
daemon.detect_settings_edit().unwrap();
assert_eq!(daemon.settings.state.as_ref().unwrap().set_at, u64::MAX);
}
#[tokio::test]
async fn detect_settings_edit_exhausted_timestamp_still_records_edit() {
let (mut daemon, _dir) = daemon();
let state = daemon.settings.state.as_mut().unwrap();
state.set_at = u64::MAX;
daemon.config.input.speed = 3.0;
let result = daemon.detect_settings_edit();
assert!(result.is_ok());
assert_eq!(
daemon.settings.state.as_ref().unwrap().values["input.speed"],
"3.0"
);
}
#[test]
fn load_missing_corrupt_or_oversized_snapshot_starts_without_settings() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("settings.pb");
assert!(SettingsStore::load(&path).state.is_none());
for bytes in [
vec![255],
vec![],
vec![0; a7n_proto::limits::CONTROL_FRAME_MAX + 1],
] {
std::fs::write(&path, bytes).unwrap();
let store = SettingsStore::load(&path);
assert!(store.state.is_none());
}
}
#[tokio::test]
async fn reload_config_sync_disabled_tracks_edits_without_sending() {
let (mut daemon, _dir) = daemon();
daemon.config.sync.settings = false;
daemon.config.input.speed = 2.0;
daemon.config.save(&daemon.config_path).unwrap();
let old = daemon.settings.state.clone().unwrap();
daemon.reload_config().await.unwrap();
let edited = daemon.settings.state.clone().unwrap();
assert!(edited.set_at > old.set_at);
assert_eq!(edited.values["input.speed"], "2.0");
assert!(daemon.settings_message().is_none());
let mut config = daemon.config.clone();
config.sync.settings = true;
config.save(&daemon.config_path).unwrap();
daemon.reload_config().await.unwrap();
assert_eq!(daemon.settings.state, Some(edited));
assert!(daemon.settings_message().is_some());
}
#[tokio::test]
async fn detect_settings_edit_file_settings_excludes_security_keys_from_message() {
let (mut daemon, dir) = daemon();
let key = format!("peers.{}.files", daemon.identity.machine_id);
daemon.config.set(&key, "both").unwrap();
daemon
.config
.set("files.receive_dir", dir.path().to_str().unwrap())
.unwrap();
daemon.config.input.speed = 2.0;
daemon.detect_settings_edit().unwrap();
let message = daemon.settings_message().unwrap();
let Some(Body::Settings(wire)) = message.body else {
panic!("expected settings message");
};
assert_eq!(wire.values["input.speed"], "2.0");
assert!(!wire.values.contains_key(&key));
assert!(!wire.values.contains_key("files.receive_dir"));
}