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_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());
}
}
}
#[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 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"));
}