Skip to content

Commit df7b0c8

Browse files
committed
test(security): add unit tests for build_isolated_env and detect_collisions
1 parent 4882df4 commit df7b0c8

File tree

2 files changed

+148
-0
lines changed

2 files changed

+148
-0
lines changed

crates/zeph-mcp/src/security.rs

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,4 +327,72 @@ mod tests {
327327
assert!(err.to_string().contains("LD_PRELOAD"));
328328
assert!(err.to_string().contains("blocked"));
329329
}
330+
331+
// --- build_isolated_env ---
332+
333+
#[test]
334+
fn build_isolated_env_base_vars_present_when_set() {
335+
// PATH and HOME are almost always set in CI and local environments.
336+
// We test the ones most likely to be present.
337+
let result = build_isolated_env(&HashMap::new());
338+
// At minimum, PATH should appear (always set in any shell environment).
339+
// We do a soft check: the result must be a strict subset of BASE_ENV_VARS + server_env.
340+
for key in result.keys() {
341+
assert!(
342+
BASE_ENV_VARS.contains(&key.as_str()),
343+
"unexpected key in isolated env: {key}"
344+
);
345+
}
346+
}
347+
348+
#[test]
349+
fn build_isolated_env_non_base_vars_absent() {
350+
// build_isolated_env only propagates variables listed in BASE_ENV_VARS.
351+
// Any key in the result must be in BASE_ENV_VARS (or in server_env, which is empty here).
352+
let result = build_isolated_env(&HashMap::new());
353+
for key in result.keys() {
354+
assert!(
355+
BASE_ENV_VARS.contains(&key.as_str()),
356+
"unexpected key in isolated env (not in BASE_ENV_VARS and not in server_env): {key}"
357+
);
358+
}
359+
}
360+
361+
#[test]
362+
fn build_isolated_env_server_env_merged() {
363+
let mut server_env = HashMap::new();
364+
server_env.insert("MY_TOOL_TOKEN".into(), "tok_abc".into());
365+
let result = build_isolated_env(&server_env);
366+
assert_eq!(
367+
result.get("MY_TOOL_TOKEN").map(String::as_str),
368+
Some("tok_abc"),
369+
"server-declared env must appear in isolated env"
370+
);
371+
}
372+
373+
#[test]
374+
fn build_isolated_env_server_env_can_override_base_var() {
375+
// Operator can pin a specific PATH — server_env merges after base vars.
376+
let mut server_env = HashMap::new();
377+
server_env.insert("PATH".into(), "/usr/local/bin:/custom/bin".into());
378+
let result = build_isolated_env(&server_env);
379+
assert_eq!(
380+
result.get("PATH").map(String::as_str),
381+
Some("/usr/local/bin:/custom/bin"),
382+
"server-declared PATH must override the base PATH"
383+
);
384+
}
385+
386+
#[test]
387+
fn build_isolated_env_xdg_vars_in_base() {
388+
// XDG_RUNTIME_DIR and XDG_CONFIG_HOME must be in BASE_ENV_VARS.
389+
assert!(
390+
BASE_ENV_VARS.contains(&"XDG_RUNTIME_DIR"),
391+
"XDG_RUNTIME_DIR must be in BASE_ENV_VARS"
392+
);
393+
assert!(
394+
BASE_ENV_VARS.contains(&"XDG_CONFIG_HOME"),
395+
"XDG_CONFIG_HOME must be in BASE_ENV_VARS"
396+
);
397+
}
330398
}

crates/zeph-mcp/src/tool.rs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -551,4 +551,84 @@ mod tests {
551551
assert!(meta.capabilities.contains(&CapabilityClass::Network));
552552
assert_eq!(meta.data_sensitivity, DataSensitivity::Medium);
553553
}
554+
555+
// --- detect_collisions ---
556+
557+
fn trust_map(
558+
entries: &[(&str, crate::manager::McpTrustLevel)],
559+
) -> std::collections::HashMap<String, crate::manager::McpTrustLevel> {
560+
entries.iter().map(|(k, v)| ((*k).to_owned(), *v)).collect()
561+
}
562+
563+
#[test]
564+
fn detect_collisions_no_collision_happy_path() {
565+
let tools = vec![
566+
make_tool("server_a", "tool_one"),
567+
make_tool("server_b", "tool_two"),
568+
];
569+
let tm = trust_map(&[
570+
("server_a", crate::manager::McpTrustLevel::Trusted),
571+
("server_b", crate::manager::McpTrustLevel::Trusted),
572+
]);
573+
let cols = detect_collisions(&tools, &tm);
574+
assert!(cols.is_empty(), "different sanitized_ids must not collide");
575+
}
576+
577+
#[test]
578+
fn detect_collisions_different_trust_collision() {
579+
// "a.b:c" and "a:b_c" both sanitize to "a_b_c" — collision across trust levels.
580+
let tool_a = make_tool("a.b", "c");
581+
let tool_b = make_tool("a", "b_c");
582+
let tm = trust_map(&[
583+
("a.b", crate::manager::McpTrustLevel::Trusted),
584+
("a", crate::manager::McpTrustLevel::Untrusted),
585+
]);
586+
let cols = detect_collisions(&[tool_a, tool_b], &tm);
587+
assert_eq!(cols.len(), 1);
588+
let col = &cols[0];
589+
assert_eq!(col.sanitized_id, "a_b_c");
590+
assert_eq!(col.server_a, "a.b");
591+
assert_eq!(col.server_b, "a");
592+
assert_eq!(col.trust_a, crate::manager::McpTrustLevel::Trusted);
593+
assert_eq!(col.trust_b, crate::manager::McpTrustLevel::Untrusted);
594+
}
595+
596+
#[test]
597+
fn detect_collisions_same_trust_collision() {
598+
// Both servers are Untrusted and share a sanitized_id.
599+
let tool_a = make_tool("a.b", "c");
600+
let tool_b = make_tool("a", "b_c");
601+
let tm = trust_map(&[
602+
("a.b", crate::manager::McpTrustLevel::Untrusted),
603+
("a", crate::manager::McpTrustLevel::Untrusted),
604+
]);
605+
let cols = detect_collisions(&[tool_a, tool_b], &tm);
606+
assert_eq!(cols.len(), 1);
607+
assert_eq!(cols[0].trust_a, crate::manager::McpTrustLevel::Untrusted);
608+
assert_eq!(cols[0].trust_b, crate::manager::McpTrustLevel::Untrusted);
609+
}
610+
611+
#[test]
612+
fn detect_collisions_multiple_collisions_reported() {
613+
// Three tools, all sharing the same sanitized_id "srv_tool".
614+
let t1 = make_tool("srv", "tool");
615+
let t2 = make_tool("srv.x", "tool"); // "srv_x_tool" — different, no collision with t1
616+
let t3 = make_tool("srv", "tool"); // exact duplicate of t1 — collision
617+
let tm = trust_map(&[("srv", crate::manager::McpTrustLevel::Untrusted)]);
618+
let cols = detect_collisions(&[t1, t2, t3], &tm);
619+
// t1 and t3 share "srv_tool"; t2 is "srv_x_tool" — one collision
620+
assert_eq!(cols.len(), 1);
621+
assert_eq!(cols[0].sanitized_id, "srv_tool");
622+
}
623+
624+
#[test]
625+
fn detect_collisions_unknown_server_defaults_to_untrusted() {
626+
let tool_a = make_tool("a.b", "c");
627+
let tool_b = make_tool("a", "b_c");
628+
// No entries in trust_map — both should default to Untrusted.
629+
let cols = detect_collisions(&[tool_a, tool_b], &std::collections::HashMap::new());
630+
assert_eq!(cols.len(), 1);
631+
assert_eq!(cols[0].trust_a, crate::manager::McpTrustLevel::Untrusted);
632+
assert_eq!(cols[0].trust_b, crate::manager::McpTrustLevel::Untrusted);
633+
}
554634
}

0 commit comments

Comments
 (0)