use super::common::make_workspace;
use crate::{
view::{ConfigFileKind, EditCommand, EditValue, NodeKind},
workspace::Workspace,
};
#[test]
fn save_is_noop_when_nothing_changed() {
let (_dir, root) = make_workspace();
let mut ws = Workspace::load(root).expect("load");
let result = ws.save().expect("save");
assert_eq!(result.changed_files.len(), 0);
assert_eq!(result.diff_summary.len(), 0);
assert!(!result.requires_reload);
}
#[test]
fn save_persists_rule_set_edit_and_round_trips() {
let (_dir, root) = make_workspace();
let mut ws = Workspace::load(root.clone()).expect("load");
let snap = ws.snapshot();
let resp_id = snap
.files
.iter()
.find(|f| matches!(f.kind, ConfigFileKind::RuleSet))
.unwrap()
.nodes
.iter()
.find(|n| matches!(n.kind, NodeKind::Respond))
.unwrap()
.id;
ws.apply(EditCommand::UpdateRespond {
id: resp_id,
respond: crate::view::RespondPayload {
text: Some("HELLO_FROM_SAVE".to_owned()),
..Default::default()
},
})
.expect("apply");
assert!(ws.has_unsaved_changes());
let save = ws.save().expect("save");
assert!(save.changed_files.iter().any(|p| p.file_name()
.map(|n| n.to_string_lossy().contains("apimock-rule-set"))
.unwrap_or(false)));
assert!(!save.diff_summary.is_empty());
assert!(!ws.has_unsaved_changes());
let ws2 = Workspace::load(root).expect("reload");
let snap2 = ws2.snapshot();
let any_respond_has_text = snap2
.files
.iter()
.flat_map(|f| f.nodes.iter())
.any(|n| n.display_name.contains("HELLO_FROM_SAVE"));
assert!(
any_respond_has_text,
"expected the saved text to round-trip through disk"
);
}
#[test]
fn save_persists_root_edit_and_flags_reload() {
let (_dir, root) = make_workspace();
let mut ws = Workspace::load(root.clone()).expect("load");
ws.apply(EditCommand::UpdateRootSetting {
key: crate::view::RootSettingKey::ListenerPort,
value: EditValue::Integer(8888),
})
.expect("apply");
let save = ws.save().expect("save");
assert!(
save.changed_files.iter().any(|p| p == &root),
"root file should be in changed_files"
);
assert!(
save.requires_reload,
"listener port change should request reload"
);
assert!(save.diff_summary.iter().any(|d| matches!(
d.kind,
crate::view::DiffKind::Updated
)));
let ws2 = Workspace::load(root).expect("reload");
assert_eq!(
ws2.config.listener.as_ref().unwrap().port,
8888,
"port edit must round-trip through disk"
);
}
#[test]
fn save_atomic_write_does_not_corrupt_on_concurrent_read() {
let (_dir, root) = make_workspace();
let mut ws = Workspace::load(root.clone()).expect("load");
ws.apply(EditCommand::UpdateRootSetting {
key: crate::view::RootSettingKey::ListenerPort,
value: EditValue::Integer(9001),
})
.expect("apply");
let _ = ws.save().expect("save");
let text = std::fs::read_to_string(&root).expect("read after save");
assert!(text.contains("port"));
assert!(text.contains("9001"));
let _: toml::Value = toml::from_str(&text).expect("post-save TOML must parse");
}
#[test]
fn has_unsaved_changes_tracks_edit_state() {
let (_dir, root) = make_workspace();
let mut ws = Workspace::load(root).expect("load");
assert!(!ws.has_unsaved_changes());
ws.apply(EditCommand::UpdateRootSetting {
key: crate::view::RootSettingKey::ListenerPort,
value: EditValue::Integer(8080),
})
.expect("apply");
assert!(ws.has_unsaved_changes());
let _ = ws.save().expect("save");
assert!(!ws.has_unsaved_changes());
}
#[test]
fn snapshot_routes_populated_from_rule_sets() {
let (_dir, root) = make_workspace();
let ws = Workspace::load(root).expect("load");
let snap = ws.snapshot();
assert_eq!(snap.routes.rule_sets.len(), 1);
let rs_view = &snap.routes.rule_sets[0];
assert_eq!(rs_view.index, 0);
assert_eq!(rs_view.rules.len(), 2);
let r0 = &rs_view.rules[0];
let url_path = r0.when.url_path.as_ref().expect("url_path present");
assert_eq!(url_path.value, "/api/users");
assert_eq!(url_path.op, "equal");
assert!(r0.when.method.is_none());
}
#[test]
fn snapshot_when_view_summary_has_method_and_path() {
let dir = tempfile::tempdir().unwrap();
let fallback = dir.path().join("fallback");
std::fs::create_dir_all(&fallback).unwrap();
let rs_toml = concat!(
"[[rules]]\n",
"when.request.url_path = \"/api/v1\"\n",
"when.request.method = \"GET\"\n",
"respond = { text = \"ok\" }\n",
);
std::fs::write(dir.path().join("apimock-rule-set.toml"), rs_toml).unwrap();
std::fs::write(
dir.path().join("apimock.toml"),
format!(
"[listener]\nip_address = \"127.0.0.1\"\nport = 3001\n[service]\nrule_sets = [\"apimock-rule-set.toml\"]\nfallback_respond_dir = \"{}\"\n",
fallback.file_name().unwrap().to_string_lossy()
),
)
.unwrap();
let ws = Workspace::load(dir.path().join("apimock.toml")).unwrap();
let snap = ws.snapshot();
let when = &snap.routes.rule_sets[0].rules[0].when;
assert_eq!(when.method.as_deref(), Some("GET"));
let summary = when.summary();
assert!(summary.contains("GET"));
assert!(summary.contains("/api/v1"));
}
#[test]
fn snapshot_file_tree_depth1_eager() {
let dir = tempfile::tempdir().unwrap();
let fallback = dir.path().join("fallback");
std::fs::create_dir_all(&fallback).unwrap();
std::fs::write(fallback.join("users.json"), "{}").unwrap();
let subdir = fallback.join("subdir");
std::fs::create_dir_all(&subdir).unwrap();
std::fs::write(subdir.join("hidden.json"), "{}").unwrap();
let rs_toml = concat!(
"[[rules]]\n",
"when.request.url_path = \"/api/users\"\n",
"respond = { text = \"ok\" }\n",
);
std::fs::write(dir.path().join("apimock-rule-set.toml"), rs_toml).unwrap();
std::fs::write(
dir.path().join("apimock.toml"),
format!(
"[listener]\nip_address = \"127.0.0.1\"\nport = 3001\n[service]\nrule_sets = [\"apimock-rule-set.toml\"]\nfallback_respond_dir = \"{}\"\n",
fallback.file_name().unwrap().to_string_lossy()
),
)
.unwrap();
let ws = Workspace::load(dir.path().join("apimock.toml")).unwrap();
let snap = ws.snapshot();
let tree = snap.routes.file_tree.as_ref().expect("file tree present");
assert_eq!(tree.entries.len(), 2);
let names: Vec<&str> = tree.entries.iter().map(|e| e.name.as_str()).collect();
assert!(names.contains(&"users.json"));
assert!(names.contains(&"subdir"));
let subdir_entry = tree.entries.iter().find(|e| e.name == "subdir").unwrap();
assert!(matches!(
subdir_entry.kind,
apimock_routing::view::FileNodeKind::Directory
));
let children = subdir_entry
.children
.as_ref()
.expect("directory has Some(children) flag");
assert!(
children.is_empty(),
"subdirectory should not be eagerly expanded"
);
let file_entry = tree.entries.iter().find(|e| e.name == "users.json").unwrap();
assert_eq!(file_entry.route_hint.as_deref(), Some("/users"));
assert!(file_entry.children.is_none());
let expanded = ws.list_directory(std::path::Path::new(&subdir_entry.path));
assert_eq!(expanded.len(), 1);
assert_eq!(expanded[0].name, "hidden.json");
}
#[test]
fn save_per_rule_diff_after_rule_edit() {
let (_dir, root) = make_workspace();
let mut ws = Workspace::load(root.clone()).expect("load");
let snap = ws.snapshot();
let resp_id = snap
.files
.iter()
.find(|f| matches!(f.kind, ConfigFileKind::RuleSet))
.unwrap()
.nodes
.iter()
.find(|n| matches!(n.kind, NodeKind::Respond))
.unwrap()
.id;
ws.apply(EditCommand::UpdateRespond {
id: resp_id,
respond: crate::view::RespondPayload {
text: Some("CHANGED".to_owned()),
..Default::default()
},
})
.expect("apply");
let save = ws.save().expect("save");
let rule_diffs: Vec<_> = save
.diff_summary
.iter()
.filter(|d| d.summary.contains("rule #"))
.collect();
assert!(
!rule_diffs.is_empty(),
"expected per-rule diff entries; got {:?}",
save.diff_summary
);
let updated_rule_1 = rule_diffs.iter().find(|d| {
d.summary.contains("rule #1")
&& matches!(d.kind, crate::view::DiffKind::Updated)
});
assert!(
updated_rule_1.is_some(),
"expected an Updated entry for rule #1; got {:?}",
save.diff_summary
);
}
#[test]
fn snapshot_script_routes_present_when_middlewares_configured() {
let dir = tempfile::tempdir().unwrap();
let fallback = dir.path().join("fallback");
std::fs::create_dir_all(&fallback).unwrap();
let mw = dir.path().join("noop.rhai");
std::fs::write(&mw, "// noop\n").unwrap();
let rs_toml = concat!(
"[[rules]]\n",
"when.request.url_path = \"/x\"\n",
"respond = { text = \"ok\" }\n",
);
std::fs::write(dir.path().join("apimock-rule-set.toml"), rs_toml).unwrap();
std::fs::write(
dir.path().join("apimock.toml"),
format!(
"[listener]\nip_address = \"127.0.0.1\"\nport = 3001\n[service]\nrule_sets = [\"apimock-rule-set.toml\"]\nmiddlewares = [\"noop.rhai\"]\nfallback_respond_dir = \"{}\"\n",
fallback.file_name().unwrap().to_string_lossy()
),
)
.unwrap();
let ws = Workspace::load(dir.path().join("apimock.toml")).unwrap();
let snap = ws.snapshot();
assert_eq!(snap.routes.script_routes.len(), 1);
assert_eq!(snap.routes.script_routes[0].index, 0);
assert_eq!(snap.routes.script_routes[0].source_file, "noop.rhai");
assert_eq!(snap.routes.script_routes[0].display_name, "noop.rhai");
}