added CLI parsing tests

This commit is contained in:
JasterV 2026-01-28 13:46:56 +01:00
commit 039ba1da20

View file

@ -6,7 +6,7 @@ use std::path::PathBuf;
use clap::{Args, Parser, Subcommand}; use clap::{Args, Parser, Subcommand};
#[derive(Parser)] #[derive(Parser, Debug)]
#[command(name = "granc", version, about = "Dynamic gRPC CLI")] #[command(name = "granc", version, about = "Dynamic gRPC CLI")]
pub struct Cli { pub struct Cli {
#[command(subcommand)] #[command(subcommand)]
@ -24,14 +24,14 @@ pub enum Commands {
endpoint: (String, String), endpoint: (String, String),
/// The server URL to connect to (e.g. http://localhost:50051) /// The server URL to connect to (e.g. http://localhost:50051)
#[arg(long, short = 'u', value_parser = parse_body)] #[arg(long, short = 'u')]
url: String, url: String,
/// "JSON body (Object for Unary, Array for Streaming)" /// "JSON body (Object for Unary, Array for Streaming)"
#[arg(long, short = 'b', value_parser = parse_body)] #[arg(long, short = 'b', value_parser = parse_body)]
body: serde_json::Value, body: serde_json::Value,
#[arg(short = 'h', long = "header", value_parser = parse_header)] #[arg(short = 'H', long = "header", value_parser = parse_header)]
headers: Vec<(String, String)>, headers: Vec<(String, String)>,
/// Optional path to a file descriptor set (.bin) to use instead of reflection /// Optional path to a file descriptor set (.bin) to use instead of reflection
@ -116,3 +116,222 @@ fn parse_header(s: &str) -> Result<(String, String), String> {
fn parse_body(value: &str) -> Result<serde_json::Value, String> { fn parse_body(value: &str) -> Result<serde_json::Value, String> {
serde_json::from_str(value).map_err(|e| format!("Invalid JSON: {e}")) serde_json::from_str(value).map_err(|e| format!("Invalid JSON: {e}"))
} }
#[cfg(test)]
mod tests {
use super::*;
use clap::Parser;
#[test]
fn test_call_command_reflection() {
let args = vec![
"granc",
"call",
"helloworld.Greeter/SayHello",
"--url",
"http://localhost:50051",
"--body",
r#"{"name": "Ferris"}"#,
];
let cli = Cli::try_parse_from(&args).expect("Parsing failed");
match cli.command {
Commands::Call {
endpoint,
url,
body,
file_descriptor_set,
..
} => {
assert_eq!(
endpoint,
("helloworld.Greeter".to_string(), "SayHello".to_string())
);
assert_eq!(url, "http://localhost:50051");
assert_eq!(body, serde_json::json!({"name": "Ferris"}));
assert!(file_descriptor_set.is_none());
}
_ => panic!("Expected Call command"),
}
}
#[test]
fn test_call_command_with_file_descriptor() {
let args = vec![
"granc",
"call",
"helloworld.Greeter/SayHello",
"--url",
"http://localhost:50051",
"--body",
r#"{"name": "Ferris"}"#,
"--file-descriptor-set",
"./descriptors.bin",
];
let cli = Cli::try_parse_from(&args).expect("Parsing failed");
match cli.command {
Commands::Call {
file_descriptor_set,
..
} => {
assert_eq!(
file_descriptor_set.unwrap().to_str().unwrap(),
"./descriptors.bin"
);
}
_ => panic!("Expected Call command"),
}
}
#[test]
fn test_call_command_short_flags() {
let args = vec![
"granc",
"call",
"svc/mthd",
"-u",
"http://localhost:50051",
"-b",
"{}",
"-f",
"desc.bin",
"-H",
"auth:bearer",
];
let cli = Cli::try_parse_from(&args).expect("Parsing failed");
match cli.command {
Commands::Call {
url,
file_descriptor_set,
headers,
body,
..
} => {
assert_eq!(url, "http://localhost:50051");
assert_eq!(file_descriptor_set.unwrap().to_str().unwrap(), "desc.bin");
assert_eq!(body, serde_json::json!({}));
assert_eq!(headers[0], ("auth".to_string(), "bearer".to_string()));
}
_ => panic!("Expected Call command"),
}
}
#[test]
fn test_list_command_reflection() {
let args = vec!["granc", "list", "--url", "http://localhost:50051"];
let cli = Cli::try_parse_from(&args).expect("Parsing failed");
match cli.command {
Commands::List { source } => {
assert_eq!(source.url.unwrap(), "http://localhost:50051");
assert!(source.file_descriptor_set.is_none());
}
_ => panic!("Expected List command"),
}
}
#[test]
fn test_list_command_offline() {
let args = vec!["granc", "list", "--file-descriptor-set", "desc.bin"];
let cli = Cli::try_parse_from(&args).expect("Parsing failed");
match cli.command {
Commands::List { source } => {
assert_eq!(
source.file_descriptor_set.unwrap().to_str().unwrap(),
"desc.bin"
);
assert!(source.url.is_none());
}
_ => panic!("Expected List command"),
}
}
#[test]
fn test_describe_command() {
let args = vec![
"granc",
"describe",
"helloworld.Greeter",
"--url",
"http://localhost:50051",
];
let cli = Cli::try_parse_from(&args).expect("Parsing failed");
match cli.command {
Commands::Describe { symbol, source } => {
assert_eq!(symbol, "helloworld.Greeter");
assert!(source.url.is_some());
}
_ => panic!("Expected Describe command"),
}
}
// --- Failure Cases ---
#[test]
fn test_fail_invalid_json_body() {
let args = vec!["granc", "call", "s/m", "-u", "x", "--body", "{invalid_json"];
let err = Cli::try_parse_from(&args).unwrap_err();
// Should verify that the error comes from the body parser
assert!(err.to_string().contains("Invalid JSON"));
}
#[test]
fn test_fail_invalid_endpoint_format() {
let args = vec![
"granc",
"call",
"OnlyServiceNoMethod", // Missing '/'
"-u",
"x",
"-b",
"{}",
];
let err = Cli::try_parse_from(&args).unwrap_err();
assert!(err.to_string().contains("Invalid endpoint format"));
}
#[test]
fn test_fail_list_requires_source() {
let args = vec!["granc", "list"];
let err = Cli::try_parse_from(&args).unwrap_err();
// Clap error for missing required arguments in group
assert!(err.kind() == clap::error::ErrorKind::MissingRequiredArgument);
}
#[test]
fn test_fail_list_mutual_exclusion() {
let args = vec![
"granc",
"list",
"--url",
"http://host",
"--file-descriptor-set",
"file.bin",
];
let err = Cli::try_parse_from(&args).unwrap_err();
// Clap error for argument conflict
assert!(err.kind() == clap::error::ErrorKind::ArgumentConflict);
}
#[test]
fn test_fail_describe_mutual_exclusion() {
let args = vec![
"granc",
"describe",
"Symbol",
"-u",
"http://host",
"-f",
"file.bin",
];
let err = Cli::try_parse_from(&args).unwrap_err();
assert!(err.kind() == clap::error::ErrorKind::ArgumentConflict);
}
}