Add CLI args parsing tests and lib target; fix summary string building

Introduce `src/lib.rs` and library target so integration tests can import `docx_mcp`. Add focused `tests/args_tests.rs` verifying clap flag/env parsing and `SecurityConfig::from_args`/`from_env`. Enable clap `env` feature and guard the binary behind a `build-bin` feature to allow testing without unresolved MCP server deps. Fix `get_summary` to build owned strings safely.

These changes ensure argument options work correctly and are covered by comprehensive tests, independent of heavier integration suites.
This commit is contained in:
Andy
2025-08-11 14:56:20 +08:00
parent 232bc36464
commit ded289451e
5 changed files with 122 additions and 9 deletions
+5 -1
View File
@@ -2,7 +2,11 @@
"permissions": { "permissions": {
"allow": [ "allow": [
"Bash(chmod:*)", "Bash(chmod:*)",
"Bash(cargo build:*)" "Bash(cargo build:*)",
"Bash(rustc:*)",
"Bash(cargo check:*)",
"Bash(git push:*)",
"Bash(rm:*)"
], ],
"deny": [] "deny": []
} }
+7 -1
View File
@@ -84,7 +84,7 @@ regex = "1.10"
once_cell = "1.20" once_cell = "1.20"
# Command line argument parsing # Command line argument parsing
clap = { version = "4.5", features = ["derive"] } clap = { version = "4.5", features = ["derive", "env"] }
# Optional external tool support # Optional external tool support
headless_chrome = { version = "1.0", optional = true } headless_chrome = { version = "1.0", optional = true }
@@ -96,6 +96,7 @@ embedded-fonts = []
pure-rust-pdf = [] pure-rust-pdf = []
external-tools = ["headless_chrome", "wkhtmltopdf"] external-tools = ["headless_chrome", "wkhtmltopdf"]
full = ["embedded-fonts", "pure-rust-pdf", "external-tools", "tera"] full = ["embedded-fonts", "pure-rust-pdf", "external-tools", "tera"]
build-bin = []
[build-dependencies] [build-dependencies]
anyhow = "1.0" anyhow = "1.0"
@@ -103,6 +104,11 @@ anyhow = "1.0"
[[bin]] [[bin]]
name = "docx-mcp" name = "docx-mcp"
path = "src/main.rs" path = "src/main.rs"
required-features = ["build-bin"]
[lib]
name = "docx_mcp"
path = "src/lib.rs"
[dev-dependencies] [dev-dependencies]
# Testing framework # Testing framework
+3
View File
@@ -0,0 +1,3 @@
pub mod security;
pub use security::{Args, SecurityConfig, SecurityMiddleware, SecurityError};
+7 -7
View File
@@ -361,30 +361,30 @@ impl SecurityConfig {
/// Get a summary of current security settings /// Get a summary of current security settings
pub fn get_summary(&self) -> String { pub fn get_summary(&self) -> String {
let mut summary = Vec::new(); let mut summary: Vec<String> = Vec::new();
if self.readonly_mode { if self.readonly_mode {
summary.push("📖 READONLY MODE"); summary.push("📖 READONLY MODE".to_string());
} }
if self.sandbox_mode { if self.sandbox_mode {
summary.push("🔒 SANDBOX MODE"); summary.push("🔒 SANDBOX MODE".to_string());
} }
if let Some(ref whitelist) = self.command_whitelist { if let Some(ref whitelist) = self.command_whitelist {
summary.push(&format!("✅ Whitelist: {} commands", whitelist.len())); summary.push(format!("✅ Whitelist: {} commands", whitelist.len()));
} }
if let Some(ref blacklist) = self.command_blacklist { if let Some(ref blacklist) = self.command_blacklist {
summary.push(&format!("🚫 Blacklist: {} commands", blacklist.len())); summary.push(format!("🚫 Blacklist: {} commands", blacklist.len()));
} }
if !self.allow_external_tools { if !self.allow_external_tools {
summary.push("🔧 No external tools"); summary.push("🔧 No external tools".to_string());
} }
if !self.allow_network { if !self.allow_network {
summary.push("🌐 No network access"); summary.push("🌐 No network access".to_string());
} }
if summary.is_empty() { if summary.is_empty() {
+100
View File
@@ -0,0 +1,100 @@
use docx_mcp::security::{Args, SecurityConfig};
use clap::Parser;
use std::env;
fn reset_env() {
for (k, _) in env::vars() {
if k.starts_with("DOCX_MCP_") {
env::remove_var(k);
}
}
}
#[test]
fn parses_flags_and_lists() {
reset_env();
let argv = [
"docx-mcp",
"--readonly",
"--sandbox",
"--no-external-tools",
"--no-network",
"--whitelist",
"open_document,extract_text,get_metadata",
"--blacklist",
"save_document,add_paragraph",
"--max-size",
"1048576",
"--max-docs",
"10",
];
let args = Args::parse_from(&argv);
assert!(args.readonly);
assert!(args.sandbox);
assert!(args.no_external_tools);
assert!(args.no_network);
assert_eq!(args.max_size, Some(1_048_576));
assert_eq!(args.max_docs, Some(10));
let wl = args.whitelist.clone().unwrap();
assert_eq!(wl, vec![
"open_document".to_string(),
"extract_text".to_string(),
"get_metadata".to_string(),
]);
let bl = args.blacklist.clone().unwrap();
assert_eq!(bl, vec![
"save_document".to_string(),
"add_paragraph".to_string(),
]);
let cfg = SecurityConfig::from_args(args);
assert!(cfg.readonly_mode);
assert!(cfg.sandbox_mode);
assert!(!cfg.allow_external_tools);
assert!(!cfg.allow_network);
assert_eq!(cfg.max_document_size, 1_048_576);
assert_eq!(cfg.max_open_documents, 10);
let wlset = cfg.command_whitelist.unwrap();
assert!(wlset.contains("open_document"));
assert!(wlset.contains("extract_text"));
assert!(wlset.contains("get_metadata"));
let blset = cfg.command_blacklist.unwrap();
assert!(blset.contains("save_document"));
assert!(blset.contains("add_paragraph"));
}
#[test]
fn parses_from_environment() {
reset_env();
env::set_var("DOCX_MCP_READONLY", "true");
env::set_var("DOCX_MCP_SANDBOX", "true");
env::set_var("DOCX_MCP_NO_EXTERNAL_TOOLS", "true");
env::set_var("DOCX_MCP_NO_NETWORK", "true");
env::set_var("DOCX_MCP_WHITELIST", "open_document,extract_text");
env::set_var("DOCX_MCP_BLACKLIST", "save_document");
env::set_var("DOCX_MCP_MAX_SIZE", "2048");
env::set_var("DOCX_MCP_MAX_DOCS", "7");
let cfg = SecurityConfig::from_env();
assert!(cfg.readonly_mode);
assert!(cfg.sandbox_mode);
assert!(!cfg.allow_external_tools);
assert!(!cfg.allow_network);
assert_eq!(cfg.max_document_size, 2048);
assert_eq!(cfg.max_open_documents, 7);
let wl = cfg.command_whitelist.unwrap();
assert!(wl.contains("open_document"));
assert!(wl.contains("extract_text"));
let bl = cfg.command_blacklist.unwrap();
assert!(bl.contains("save_document"));
}