ggen_cli_lib/cmds/project/
test.rs1use clap::Args;
21use ggen_utils::error::Result;
22use std::fs;
23use std::path::{Component, Path, PathBuf};
24
25#[derive(Args, Debug)]
26pub struct TestArgs {
27 pub template_ref: String,
29
30 #[arg(long, short = 'g')]
32 pub golden: PathBuf,
33
34 #[arg(short = 'v', long = "var")]
36 pub vars: Vec<String>,
37
38 #[arg(long, short = 'u')]
40 pub update: bool,
41
42 #[arg(long)]
44 pub verbose: bool,
45
46 #[arg(long)]
48 pub json: bool,
49
50 #[arg(long)]
52 pub dry_run: bool,
53}
54
55fn validate_path(path: &Path) -> Result<()> {
57 if path.components().any(|c| matches!(c, Component::ParentDir)) {
58 return Err(ggen_utils::error::Error::new(
59 "Path traversal detected: paths containing '..' are not allowed",
60 ));
61 }
62 Ok(())
63}
64
65fn parse_vars(vars: &[String]) -> Result<std::collections::HashMap<String, String>> {
67 let mut map = std::collections::HashMap::new();
68 for var in vars {
69 let parts: Vec<&str> = var.splitn(2, '=').collect();
70 if parts.len() != 2 {
71 return Err(ggen_utils::error::Error::new_fmt(format_args!(
72 "Invalid variable format: '{}'. Expected 'key=value'",
73 var
74 )));
75 }
76 map.insert(parts[0].to_string(), parts[1].to_string());
77 }
78 Ok(map)
79}
80
81pub async fn run(args: &TestArgs) -> Result<()> {
83 if args.template_ref.is_empty() {
85 return Err(ggen_utils::error::Error::new(
86 "Template reference cannot be empty",
87 ));
88 }
89
90 validate_path(&args.golden)?;
91 let _vars = parse_vars(&args.vars)?;
92
93 println!("๐งช Running golden file snapshot tests...");
94
95 if args.update {
96 println!("๐ Update mode: Golden files will be updated");
97 } else {
98 println!("๐ Compare mode: Checking against golden files");
99 }
100
101 if !args.golden.exists() && !args.update {
103 return Err(ggen_utils::error::Error::new_fmt(format_args!(
104 "Golden directory not found: {}",
105 args.golden.display()
106 )));
107 }
108
109 if args.update && !args.dry_run {
110 fs::create_dir_all(&args.golden).map_err(ggen_utils::error::Error::from)?;
111 }
112
113 let mut cmd = std::process::Command::new("cargo");
115 cmd.args(["make", "project-test"]);
116 cmd.arg("--template").arg(&args.template_ref);
117 cmd.arg("--golden").arg(&args.golden);
118
119 for var in &args.vars {
120 cmd.arg("--var").arg(var);
121 }
122
123 if args.update {
124 cmd.arg("--update");
125 }
126
127 if args.verbose {
128 cmd.arg("--verbose");
129 }
130
131 if args.json {
132 cmd.arg("--json");
133 }
134
135 if args.dry_run {
136 cmd.arg("--dry-run");
137 }
138
139 let output = cmd.output().map_err(ggen_utils::error::Error::from)?;
140
141 if !output.status.success() {
142 let stderr = String::from_utf8_lossy(&output.stderr);
143 return Err(ggen_utils::error::Error::new_fmt(format_args!(
144 "Snapshot test failed: {}",
145 stderr
146 )));
147 }
148
149 let stdout = String::from_utf8_lossy(&output.stdout);
150
151 if args.json {
152 println!("{}", stdout);
153 } else {
154 println!("{}", stdout);
155 if args.update {
156 println!("โ
Golden files updated successfully");
157 } else {
158 println!("โ
All snapshot tests passed");
159 }
160 }
161
162 Ok(())
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168
169 #[test]
170 fn test_parse_vars_valid() {
171 let vars = vec!["name=test".to_string(), "version=1.0".to_string()];
172 let result = parse_vars(&vars).unwrap();
173
174 assert_eq!(result.get("name"), Some(&"test".to_string()));
175 assert_eq!(result.get("version"), Some(&"1.0".to_string()));
176 }
177
178 #[test]
179 fn test_parse_vars_invalid() {
180 let vars = vec!["invalid".to_string()];
181 let result = parse_vars(&vars);
182
183 assert!(result.is_err());
184 }
185
186 #[test]
187 fn test_validate_path_safe() {
188 let path = Path::new("golden/snapshots");
189 assert!(validate_path(path).is_ok());
190 }
191
192 #[test]
193 fn test_validate_path_traversal() {
194 let path = Path::new("../etc/passwd");
195 assert!(validate_path(path).is_err());
196 }
197
198 #[tokio::test]
199 async fn test_test_args_validation() {
200 let args = TestArgs {
201 template_ref: "".to_string(),
202 golden: PathBuf::from("golden"),
203 vars: vec![],
204 update: false,
205 verbose: false,
206 json: false,
207 dry_run: false,
208 };
209
210 let result = run(&args).await;
211 assert!(result.is_err());
212 assert!(result
213 .unwrap_err()
214 .to_string()
215 .contains("Template reference cannot be empty"));
216 }
217}