cli_testing_specialist/generator/
assert_cmd_generator.rs

1use crate::error::Result;
2use crate::generator::test_generator_trait::TestGenerator as TestGeneratorTrait;
3use crate::types::analysis::CliAnalysis;
4use crate::types::test_case::TestCategory;
5use handlebars::Handlebars;
6use serde_json::json;
7
8/// Generator for assert_cmd-based Rust tests
9///
10/// Generates idiomatic Rust test code using the assert_cmd and predicates crates.
11/// Tests are self-contained and can be run with `cargo test`.
12///
13/// # Example Output
14///
15/// ```rust,ignore
16/// use assert_cmd::Command;
17/// use predicates::prelude::*;
18///
19/// #[test]
20/// fn test_help() {
21///     let mut cmd = Command::cargo_bin("my-cli").unwrap();
22///     cmd.arg("--help")
23///         .assert()
24///         .success()
25///         .stdout(predicate::str::contains("Usage:"));
26/// }
27/// ```
28pub struct AssertCmdGenerator {
29    handlebars: Handlebars<'static>,
30    cli_name: String,
31}
32
33impl AssertCmdGenerator {
34    /// Create a new AssertCmdGenerator
35    ///
36    /// # Arguments
37    ///
38    /// * `analysis` - CLI analysis results
39    ///
40    /// # Returns
41    ///
42    /// New AssertCmdGenerator instance
43    pub fn new(analysis: &CliAnalysis) -> Result<Self> {
44        let mut handlebars = Handlebars::new();
45
46        // Register templates
47        Self::register_templates(&mut handlebars)?;
48
49        // Configure Handlebars
50        handlebars.set_strict_mode(true);
51
52        Ok(Self {
53            handlebars,
54            cli_name: analysis.binary_name.clone(),
55        })
56    }
57
58    /// Register all test templates
59    fn register_templates(handlebars: &mut Handlebars) -> Result<()> {
60        // Basic tests template
61        handlebars
62            .register_template_string("basic", include_str!("../templates/assert_cmd/basic.hbs"))?;
63
64        // Security tests template
65        handlebars.register_template_string(
66            "security",
67            include_str!("../templates/assert_cmd/security.hbs"),
68        )?;
69
70        // Help tests template
71        handlebars
72            .register_template_string("help", include_str!("../templates/assert_cmd/help.hbs"))?;
73
74        // Path tests template
75        handlebars
76            .register_template_string("path", include_str!("../templates/assert_cmd/path.hbs"))?;
77
78        // InputValidation tests template
79        handlebars.register_template_string(
80            "input_validation",
81            include_str!("../templates/assert_cmd/input_validation.hbs"),
82        )?;
83
84        // DestructiveOps tests template
85        handlebars.register_template_string(
86            "destructive_ops",
87            include_str!("../templates/assert_cmd/destructive_ops.hbs"),
88        )?;
89
90        // Performance tests template
91        handlebars.register_template_string(
92            "performance",
93            include_str!("../templates/assert_cmd/performance.hbs"),
94        )?;
95
96        // MultiShell tests template
97        handlebars.register_template_string(
98            "multi_shell",
99            include_str!("../templates/assert_cmd/multi_shell.hbs"),
100        )?;
101
102        Ok(())
103    }
104
105    /// Sanitize string for Rust code generation
106    ///
107    /// Escapes special characters to prevent code injection and ensure valid Rust syntax.
108    ///
109    /// # Security
110    ///
111    /// This function implements the security recommendation from the v1.1.0 design review:
112    /// - Explicitly escape backslashes, quotes, and newlines
113    /// - Does NOT rely on Handlebars default escaping
114    ///
115    /// # Arguments
116    ///
117    /// * `input` - Raw string to sanitize
118    ///
119    /// # Returns
120    ///
121    /// Sanitized string safe for Rust string literals
122    pub fn sanitize_for_rust_string(input: &str) -> String {
123        input
124            .replace('\\', "\\\\") // Backslash must be first
125            .replace('"', "\\\"") // Double quote
126            .replace('\n', "\\n") // Newline
127            .replace('\r', "\\r") // Carriage return
128            .replace('\t', "\\t") // Tab
129    }
130}
131
132impl TestGeneratorTrait for AssertCmdGenerator {
133    fn generate(&self, analysis: &CliAnalysis, category: TestCategory) -> Result<String> {
134        let template_name = match category {
135            TestCategory::Basic => "basic",
136            TestCategory::Security => "security",
137            TestCategory::Help => "help",
138            TestCategory::Path => "path",
139            TestCategory::InputValidation => "input_validation",
140            TestCategory::DestructiveOps => "destructive_ops",
141            TestCategory::DirectoryTraversal => "security", // Reuse security template
142            TestCategory::Performance => "performance",
143            TestCategory::MultiShell => "multi_shell",
144        };
145
146        // Prepare template data
147        let data = json!({
148            "cli_name": Self::sanitize_for_rust_string(&self.cli_name),
149            "version": analysis.version.as_ref().map(|v| Self::sanitize_for_rust_string(v)),
150            "subcommands": analysis.subcommands.iter().map(|sc| {
151                json!({
152                    "name": Self::sanitize_for_rust_string(&sc.name),
153                    "description": sc.description.as_ref().map(|d| Self::sanitize_for_rust_string(d)),
154                })
155            }).collect::<Vec<_>>(),
156        });
157
158        // Render template
159        let test_code = self.handlebars.render(template_name, &data)?;
160
161        Ok(test_code)
162    }
163
164    fn file_extension(&self) -> &str {
165        "rs"
166    }
167
168    fn name(&self) -> &str {
169        "assert_cmd"
170    }
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176
177    #[test]
178    fn test_sanitize_for_rust_string() {
179        assert_eq!(
180            AssertCmdGenerator::sanitize_for_rust_string("hello"),
181            "hello"
182        );
183        assert_eq!(
184            AssertCmdGenerator::sanitize_for_rust_string("hello\\world"),
185            "hello\\\\world"
186        );
187        assert_eq!(
188            AssertCmdGenerator::sanitize_for_rust_string("hello\"world"),
189            "hello\\\"world"
190        );
191        assert_eq!(
192            AssertCmdGenerator::sanitize_for_rust_string("hello\nworld"),
193            "hello\\nworld"
194        );
195        assert_eq!(
196            AssertCmdGenerator::sanitize_for_rust_string("test; rm -rf /"),
197            "test; rm -rf /"
198        );
199    }
200
201    #[test]
202    fn test_sanitize_complex_string() {
203        let input = "test\\path\"with\nnewline\tand\rtab";
204        let expected = "test\\\\path\\\"with\\nnewline\\tand\\rtab";
205        assert_eq!(
206            AssertCmdGenerator::sanitize_for_rust_string(input),
207            expected
208        );
209    }
210}