1use rcgen::{generate_simple_self_signed, CertifiedKey};
4use std::{fs, path::PathBuf, io::{Error, Result, Write}};
5
6pub const DEFAULT_CERT_FOLDER: &str = "cert";
8
9pub const DEFAULT_CERT_FILE_NAME: &str = "dev-cert.pem";
11
12pub const DEFAULT_KEY_FILE_NAME: &str = "dev-key.pem";
14
15#[cfg(target_os = "windows")]
17pub const DEV_CERT_NAMES: &[&str] = &["localhost"];
18#[cfg(not(target_os = "windows"))]
20pub const DEV_CERT_NAMES: &[&str] = &["localhost", "0.0.0.0"];
21
22#[inline]
24pub fn generate(names: impl Into<Vec<String>>) -> Result<()> {
25 let names = names.into();
26
27 if names.is_empty() {
29 return Err(Error::other("Certificate names cannot be empty"));
30 }
31
32 let CertifiedKey { cert, signing_key } = generate_simple_self_signed(names)
33 .map_err(|err| Error::other(format!("{:?}", err)))?;
34
35 fs::create_dir_all(DEFAULT_CERT_FOLDER)?;
36 fs::write(get_cert_path(), cert.pem())?;
37 fs::write(get_signing_key_path(), signing_key.serialize_pem())?;
38 Ok(())
39}
40
41#[inline]
43pub fn ask_generate() -> Result<bool> {
44 print!("Dev certificate not found. Generate new one? (y/n): ");
45 std::io::stdout().flush()?;
46
47 let mut answer = String::new();
48 std::io::stdin().read_line(&mut answer)?;
49 Ok(answer.trim().eq_ignore_ascii_case("y"))
50}
51
52#[inline]
54pub fn dev_cert_exists() -> bool {
55 get_cert_path().exists() &&
56 get_signing_key_path().exists()
57}
58
59#[inline]
61pub fn get_cert_path() -> PathBuf {
62 PathBuf::from(DEFAULT_CERT_FOLDER)
63 .join(DEFAULT_CERT_FILE_NAME)
64}
65
66#[inline]
68pub fn get_signing_key_path() -> PathBuf {
69 PathBuf::from(DEFAULT_CERT_FOLDER)
70 .join(DEFAULT_KEY_FILE_NAME)
71}
72
73#[cfg(test)]
74mod tests {
75 use super::*;
76 use std::fs;
77 use std::path::Path;
78 use serial_test::serial;
79
80 fn cleanup() {
82 let _ = fs::remove_dir_all(DEFAULT_CERT_FOLDER);
83 }
84
85 #[test]
86 fn it_defines_default_cert_folder_constant() {
87 assert_eq!(DEFAULT_CERT_FOLDER, "cert");
88 }
89
90 #[test]
91 fn it_defines_default_cert_file_name_constant() {
92 assert_eq!(DEFAULT_CERT_FILE_NAME, "dev-cert.pem");
93 }
94
95 #[test]
96 fn it_defines_default_key_file_name_constant() {
97 assert_eq!(DEFAULT_KEY_FILE_NAME, "dev-key.pem");
98 }
99
100 #[test]
101 fn it_defines_dev_cert_names_for_localhost() {
102 assert!(DEV_CERT_NAMES.contains(&"localhost"));
103 }
104
105 #[cfg(not(target_os = "windows"))]
106 #[test]
107 fn it_includes_zero_address_in_dev_cert_names_on_unix() {
108 assert!(DEV_CERT_NAMES.contains(&"0.0.0.0"));
109 assert_eq!(DEV_CERT_NAMES.len(), 2);
110 }
111
112 #[cfg(target_os = "windows")]
113 #[test]
114 fn it_excludes_zero_address_in_dev_cert_names_on_windows() {
115 assert!(!DEV_CERT_NAMES.contains(&"0.0.0.0"));
116 assert_eq!(DEV_CERT_NAMES.len(), 1);
117 }
118
119 #[test]
120 fn it_constructs_cert_path_correctly() {
121 let path = get_cert_path();
122
123 assert_eq!(path.file_name().unwrap(), DEFAULT_CERT_FILE_NAME);
124 assert!(path.to_string_lossy().contains(DEFAULT_CERT_FOLDER));
125 }
126
127 #[test]
128 fn it_constructs_signing_key_path_correctly() {
129 let path = get_signing_key_path();
130
131 assert_eq!(path.file_name().unwrap(), DEFAULT_KEY_FILE_NAME);
132 assert!(path.to_string_lossy().contains(DEFAULT_CERT_FOLDER));
133 }
134
135 #[test]
136 #[serial]
137 fn it_returns_false_when_dev_cert_does_not_exist() {
138 cleanup();
139
140 assert!(!dev_cert_exists());
141
142 cleanup();
143 }
144
145 #[test]
146 #[serial]
147 fn it_returns_false_when_only_cert_file_exists() {
148 cleanup();
149
150 fs::create_dir_all(DEFAULT_CERT_FOLDER).unwrap();
151 fs::write(get_cert_path(), "dummy cert").unwrap();
152
153 assert!(!dev_cert_exists());
154
155 cleanup();
156 }
157
158 #[test]
159 #[serial]
160 fn it_returns_false_when_only_key_file_exists() {
161 cleanup();
162
163 fs::create_dir_all(DEFAULT_CERT_FOLDER).unwrap();
164 fs::write(get_signing_key_path(), "dummy key").unwrap();
165
166 assert!(!dev_cert_exists());
167
168 cleanup();
169 }
170
171 #[test]
172 #[serial]
173 fn it_returns_true_when_both_cert_files_exist() {
174 cleanup();
175
176 fs::create_dir_all(DEFAULT_CERT_FOLDER).unwrap();
177 fs::write(get_cert_path(), "dummy cert").unwrap();
178 fs::write(get_signing_key_path(), "dummy key").unwrap();
179
180 assert!(dev_cert_exists());
181
182 cleanup();
183 }
184
185 #[test]
186 #[serial]
187 fn it_generates_certificate_with_single_name() {
188 cleanup();
189
190 let result = generate(vec!["test.local".to_string()]);
191
192 assert!(result.is_ok());
193 assert!(get_cert_path().exists());
194 assert!(get_signing_key_path().exists());
195
196 cleanup();
197 }
198
199 #[test]
200 #[serial]
201 fn it_generates_certificate_with_multiple_names() {
202 cleanup();
203
204 let names = vec![
205 "localhost".to_string(),
206 "127.0.0.1".to_string(),
207 "test.local".to_string(),
208 ];
209
210 let result = generate(names);
211
212 assert!(result.is_ok());
213 assert!(get_cert_path().exists());
214 assert!(get_signing_key_path().exists());
215
216 cleanup();
217 }
218
219 #[test]
220 #[serial]
221 fn it_creates_cert_folder_if_not_exists() {
222 cleanup();
223
224 let result = generate(vec!["localhost".to_string()]);
225
226 assert!(result.is_ok());
227 assert!(Path::new(DEFAULT_CERT_FOLDER).exists());
228
229 cleanup();
230 }
231
232 #[test]
233 #[serial]
234 fn it_writes_pem_formatted_certificate() {
235 cleanup();
236
237 generate(vec!["localhost".to_string()]).unwrap();
238
239 let cert_content = fs::read_to_string(get_cert_path()).unwrap();
240
241 assert!(cert_content.contains("-----BEGIN CERTIFICATE-----"));
242 assert!(cert_content.contains("-----END CERTIFICATE-----"));
243
244 cleanup();
245 }
246
247 #[test]
248 #[serial]
249 fn it_writes_pem_formatted_signing_key() {
250 cleanup();
251
252 generate(vec!["localhost".to_string()]).unwrap();
253
254 let key_content = fs::read_to_string(get_signing_key_path()).unwrap();
255
256 assert!(key_content.contains("-----BEGIN PRIVATE KEY-----") ||
257 key_content.contains("-----BEGIN RSA PRIVATE KEY-----"));
258 assert!(key_content.contains("-----END PRIVATE KEY-----") ||
259 key_content.contains("-----END RSA PRIVATE KEY-----"));
260
261 cleanup();
262 }
263
264 #[test]
265 #[serial]
266 fn it_overwrites_existing_certificates() {
267 cleanup();
268
269 generate(vec!["first.local".to_string()]).unwrap();
270 let first_cert = fs::read_to_string(get_cert_path()).unwrap();
271
272 std::thread::sleep(std::time::Duration::from_millis(10));
273
274 generate(vec!["second.local".to_string()]).unwrap();
275 let second_cert = fs::read_to_string(get_cert_path()).unwrap();
276
277 assert_ne!(first_cert, second_cert);
278
279 cleanup();
280 }
281
282 #[test]
283 #[serial]
284 fn it_generates_valid_certificate_structure() {
285 cleanup();
286
287 let result = generate(vec!["localhost".to_string()]);
288
289 assert!(result.is_ok());
290
291 let cert_content = fs::read_to_string(get_cert_path()).unwrap();
292 let key_content = fs::read_to_string(get_signing_key_path()).unwrap();
293
294 assert!(!cert_content.is_empty());
295 assert!(!key_content.is_empty());
296
297 assert!(cert_content.lines().count() > 2);
298 assert!(key_content.lines().count() > 2);
299
300 cleanup();
301 }
302
303 #[test]
304 #[serial]
305 fn it_handles_empty_names_vector() {
306 cleanup();
307
308 let result = generate(Vec::<String>::new());
309 assert!(result.is_err());
310
311 cleanup();
312 }
313
314 #[test]
315 #[serial]
316 fn it_generates_certificate_with_default_names() {
317 cleanup();
318
319 let names: Vec<String> = DEV_CERT_NAMES.iter().map(|s| s.to_string()).collect();
320 let result = generate(names);
321
322 assert!(result.is_ok());
323 assert!(dev_cert_exists());
324
325 cleanup();
326 }
327
328 #[test]
329 fn it_constructs_paths_with_correct_separators() {
330 let cert_path = get_cert_path();
331 let key_path = get_signing_key_path();
332
333 assert!(cert_path.is_relative());
334 assert!(key_path.is_relative());
335
336 let cert_components: Vec<_> = cert_path.components().collect();
337 let key_components: Vec<_> = key_path.components().collect();
338
339 assert_eq!(cert_components.len(), 2);
340 assert_eq!(key_components.len(), 2);
341 }
342
343 #[test]
344 #[serial]
345 fn it_generates_different_certificates_for_different_names() {
346 cleanup();
347
348 generate(vec!["name1.local".to_string()]).unwrap();
349 let cert1 = fs::read(get_cert_path()).unwrap();
350
351 std::thread::sleep(std::time::Duration::from_millis(10));
352
353 generate(vec!["name2.local".to_string()]).unwrap();
354 let cert2 = fs::read(get_cert_path()).unwrap();
355
356 assert_ne!(cert1, cert2);
357
358 cleanup();
359 }
360
361 #[test]
362 #[serial]
363 fn it_handles_special_characters_in_names() {
364 cleanup();
365
366 let names = vec![
367 "test-app.local".to_string(),
368 "my_service.dev".to_string(),
369 ];
370
371 let result = generate(names);
372
373 assert!(result.is_ok());
374 assert!(dev_cert_exists());
375
376 cleanup();
377 }
378
379 #[test]
380 #[serial]
381 fn it_verifies_cert_folder_is_created_before_files() {
382 cleanup();
383
384 assert!(!Path::new(DEFAULT_CERT_FOLDER).exists());
385
386 generate(vec!["localhost".to_string()]).unwrap();
387
388 assert!(Path::new(DEFAULT_CERT_FOLDER).exists());
389 assert!(Path::new(DEFAULT_CERT_FOLDER).is_dir());
390
391 cleanup();
392 }
393}