use super::*;
use std::fs;
fn make_workspace() -> (tempfile::TempDir, PathBuf) {
let dir = tempfile::tempdir().expect("tempdir");
let rule_set_toml = concat!(
"[[rules]]\n",
"when.request.url_path = \"/api/users\"\n",
"respond = { text = \"ok\" }\n",
"\n",
"[[rules]]\n",
"when.request.url_path = \"/api/health\"\n",
"respond = { status = 204 }\n",
);
let rs_path = dir.path().join("apimock-rule-set.toml");
fs::write(&rs_path, rule_set_toml).unwrap();
let fallback = dir.path().join("fallback");
fs::create_dir_all(&fallback).unwrap();
let root_toml = format!(
"[listener]\n\
ip_address = \"127.0.0.1\"\n\
port = 3001\n\
\n\
[service]\n\
rule_sets = [\"{}\"]\n\
fallback_respond_dir = \"{}\"\n",
rs_path.file_name().unwrap().to_string_lossy(),
fallback.file_name().unwrap().to_string_lossy(),
);
let root_path = dir.path().join("apimock.toml");
fs::write(&root_path, root_toml).unwrap();
(dir, root_path)
}
#[test]
fn load_returns_workspace_with_seeded_ids() {
let (_dir, root) = make_workspace();
let ws = Workspace::load(root).expect("load");
let n = ws.ids.id_to_address.len();
assert!(n >= 7, "expected at least 7 seeded nodes, got {}", n);
}
#[test]
fn snapshot_shapes_match_spec() {
let (_dir, root) = make_workspace();
let ws = Workspace::load(root).expect("load");
let snap = ws.snapshot();
assert!(!snap.files.is_empty());
let root_view = snap
.files
.iter()
.find(|f| matches!(f.kind, ConfigFileKind::Root))
.expect("root file present");
assert!(root_view.nodes.iter().any(|n| n.display_name == "apimock.toml"));
let rs_view = snap
.files
.iter()
.find(|f| matches!(f.kind, ConfigFileKind::RuleSet))
.expect("rule set present");
assert!(rs_view.nodes.len() >= 5);
assert!(rs_view.nodes.iter().any(|n| matches!(n.kind, NodeKind::RuleSet)));
assert!(rs_view.nodes.iter().any(|n| matches!(n.kind, NodeKind::Rule)));
assert!(rs_view.nodes.iter().any(|n| matches!(n.kind, NodeKind::Respond)));
}
#[test]
fn snapshot_nodes_have_unique_ids() {
let (_dir, root) = make_workspace();
let ws = Workspace::load(root).expect("load");
let snap = ws.snapshot();
let mut seen = std::collections::HashSet::new();
for file in &snap.files {
for node in &file.nodes {
assert!(
seen.insert(node.id),
"duplicate NodeId in snapshot: {}",
node.id
);
}
}
}
#[test]
fn snapshot_respond_node_displays_rule_content() {
let (_dir, root) = make_workspace();
let ws = Workspace::load(root).expect("load");
let snap = ws.snapshot();
let rs_view = snap
.files
.iter()
.find(|f| matches!(f.kind, ConfigFileKind::RuleSet))
.unwrap();
let responds: Vec<_> = rs_view
.nodes
.iter()
.filter(|n| matches!(n.kind, NodeKind::Respond))
.collect();
assert_eq!(responds.len(), 2);
assert!(responds[0].display_name.contains("ok"));
assert!(responds[1].display_name.contains("204"));
}
#[test]
fn apply_add_rule_to_existing_rule_set() {
let (_dir, root) = make_workspace();
let mut ws = Workspace::load(root).expect("load");
let snap = ws.snapshot();
let rs_node = snap
.files
.iter()
.find(|f| matches!(f.kind, ConfigFileKind::RuleSet))
.unwrap()
.nodes
.iter()
.find(|n| matches!(n.kind, NodeKind::RuleSet))
.unwrap();
let rs_id = rs_node.id;
let before = ws.config.service.rule_sets[0].rules.len();
let result = ws
.apply(EditCommand::AddRule {
parent: rs_id,
rule: crate::view::RulePayload {
url_path: Some("/new/rule".to_owned()),
method: None,
respond: crate::view::RespondPayload {
text: Some("hi".to_owned()),
..Default::default()
},
},
})
.expect("apply AddRule");
assert!(result.requires_reload);
assert_eq!(ws.config.service.rule_sets[0].rules.len(), before + 1);
assert!(result.changed_nodes.len() >= 3);
}
#[test]
fn apply_update_respond_changes_content() {
let (_dir, root) = make_workspace();
let mut ws = Workspace::load(root).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("updated-text".to_owned()),
..Default::default()
},
})
.expect("apply UpdateRespond");
let snap2 = ws.snapshot();
let new_display = snap2
.files
.iter()
.find(|f| matches!(f.kind, ConfigFileKind::RuleSet))
.unwrap()
.nodes
.iter()
.find(|n| n.id == resp_id)
.unwrap()
.display_name
.clone();
assert!(new_display.contains("updated-text"), "got: {}", new_display);
}
#[test]
fn apply_delete_rule_shifts_successors_and_preserves_ids() {
let (_dir, root) = make_workspace();
let mut ws = Workspace::load(root).expect("load");
let snap = ws.snapshot();
let rs_view = snap
.files
.iter()
.find(|f| matches!(f.kind, ConfigFileKind::RuleSet))
.unwrap();
let rules: Vec<NodeId> = rs_view
.nodes
.iter()
.filter(|n| matches!(n.kind, NodeKind::Rule))
.map(|n| n.id)
.collect();
assert_eq!(rules.len(), 2);
let first_id = rules[0];
let second_id = rules[1];
ws.apply(EditCommand::DeleteRule { id: first_id })
.expect("delete");
let snap2 = ws.snapshot();
let rs_view2 = snap2
.files
.iter()
.find(|f| matches!(f.kind, ConfigFileKind::RuleSet))
.unwrap();
let rules_after: Vec<NodeId> = rs_view2
.nodes
.iter()
.filter(|n| matches!(n.kind, NodeKind::Rule))
.map(|n| n.id)
.collect();
assert_eq!(rules_after.len(), 1);
assert_eq!(
rules_after[0], second_id,
"second rule's NodeId should survive deletion of the first"
);
}
#[test]
fn apply_move_rule_preserves_ids() {
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 = \"/one\"\n",
"respond = { text = \"a\" }\n",
"\n",
"[[rules]]\n",
"when.request.url_path = \"/two\"\n",
"respond = { text = \"b\" }\n",
"\n",
"[[rules]]\n",
"when.request.url_path = \"/three\"\n",
"respond = { text = \"c\" }\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 mut ws = Workspace::load(dir.path().join("apimock.toml")).unwrap();
let snap = ws.snapshot();
let rs_view = snap
.files
.iter()
.find(|f| matches!(f.kind, ConfigFileKind::RuleSet))
.unwrap();
let rule_ids: Vec<NodeId> = rs_view
.nodes
.iter()
.filter(|n| matches!(n.kind, NodeKind::Rule))
.map(|n| n.id)
.collect();
assert_eq!(rule_ids.len(), 3);
let [id_a, id_b, id_c] = [rule_ids[0], rule_ids[1], rule_ids[2]];
ws.apply(EditCommand::MoveRule {
id: id_a,
new_index: 2,
})
.expect("move");
let snap2 = ws.snapshot();
let rs_view2 = snap2
.files
.iter()
.find(|f| matches!(f.kind, ConfigFileKind::RuleSet))
.unwrap();
let new_rules: Vec<&ConfigNodeView> = rs_view2
.nodes
.iter()
.filter(|n| matches!(n.kind, NodeKind::Rule))
.collect();
assert_eq!(new_rules.len(), 3);
assert_eq!(new_rules[0].id, id_b);
assert_eq!(new_rules[1].id, id_c);
assert_eq!(new_rules[2].id, id_a);
}
#[test]
fn apply_update_root_setting_port() {
let (_dir, root) = make_workspace();
let mut ws = Workspace::load(root).expect("load");
ws.apply(EditCommand::UpdateRootSetting {
key: crate::view::RootSettingKey::ListenerPort,
value: EditValue::Integer(9999),
})
.expect("update port");
assert_eq!(ws.config.listener.as_ref().unwrap().port, 9999);
}
#[test]
fn apply_update_root_setting_bad_port_is_invalid_payload() {
let (_dir, root) = make_workspace();
let mut ws = Workspace::load(root).expect("load");
let result = ws.apply(EditCommand::UpdateRootSetting {
key: crate::view::RootSettingKey::ListenerPort,
value: EditValue::Integer(70_000),
});
assert!(matches!(result, Err(ApplyError::InvalidPayload { .. })));
}
#[test]
fn apply_unknown_node_id() {
let (_dir, root) = make_workspace();
let mut ws = Workspace::load(root).expect("load");
let result = ws.apply(EditCommand::DeleteRule { id: NodeId::new() });
assert!(matches!(result, Err(ApplyError::UnknownNode { .. })));
}
#[test]
fn apply_wrong_kind_error() {
let (_dir, root) = make_workspace();
let mut ws = Workspace::load(root).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;
let result = ws.apply(EditCommand::DeleteRule { id: resp_id });
assert!(matches!(result, Err(ApplyError::WrongNodeKind { .. })));
}
#[test]
fn validate_surfaces_per_node_diagnostics() {
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 = \"/bad\"\n",
"respond = { file_path = \"nonexistent.json\", text = \"x\" }\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();
std::fs::write(
dir.path().join("apimock-rule-set.toml"),
concat!(
"[[rules]]\n",
"when.request.url_path = \"/bad\"\n",
"respond = { text = \"ok\" }\n",
),
)
.unwrap();
let mut ws = Workspace::load(dir.path().join("apimock.toml")).unwrap();
let resp_id = ws
.snapshot()
.files
.iter()
.find(|f| matches!(f.kind, ConfigFileKind::RuleSet))
.unwrap()
.nodes
.iter()
.find(|n| matches!(n.kind, NodeKind::Respond))
.unwrap()
.id;
let result = ws
.apply(EditCommand::UpdateRespond {
id: resp_id,
respond: crate::view::RespondPayload {
file_path: Some("nope.json".to_owned()),
text: Some("also-text".to_owned()),
..Default::default()
},
})
.expect("apply should succeed even with bad payload");
let errors: Vec<_> = result
.diagnostics
.iter()
.filter(|d| matches!(d.severity, Severity::Error))
.collect();
assert!(
errors.len() >= 2,
"expected at least 2 errors; got {}: {:?}",
errors.len(),
result.diagnostics
);
let report = ws.validate();
assert!(!report.is_valid);
}