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