1use std::{
6 error::Error,
7 io::{IsTerminal, stdin, stdout},
8 path::PathBuf,
9 str::FromStr,
10};
11
12use tokio::fs;
13
14use aimcal_core::{APP_NAME, Config as CoreConfig};
15
16use crate::prompt::{DevModeChoice, prompt_dev_mode_choice};
17
18const AIM_CONFIG_ENV: &str = "AIM_CONFIG";
19const AIM_DEV_ENV: &str = "AIM_DEV";
20
21const AIM_DEV_VALID_TRUE: &[&str] = &["1", "true", "yes"];
22const AIM_DEV_VALID_FALSE: &[&str] = &["0", "false", "no"];
23
24#[tracing::instrument]
25pub async fn parse_config(path: Option<PathBuf>) -> Result<(CoreConfig, Config), Box<dyn Error>> {
26 let dev_mode_strategy = resolve_dev_mode_strategy()?;
27
28 let path = if let Some(path) = path {
29 path
30 } else if should_use_aim_config_env(dev_mode_strategy) {
31 PathBuf::from(std::env::var(AIM_CONFIG_ENV)?)
32 } else if matches!(dev_mode_strategy, DevModeStrategy::ForcedNormal) {
33 let config = get_config_dir()?.join(format!("{APP_NAME}/config.toml"));
34 if !config.exists() {
35 return Err(format!("No config found at: {}", config.display()).into());
36 }
37 config
38 } else if let Ok(env_path) = std::env::var(AIM_CONFIG_ENV) {
39 PathBuf::from(env_path)
40 } else {
41 if matches!(effective_dev_mode(dev_mode_strategy), Some(true)) {
42 return Err(format!(
43 "Development environment detected ({AIM_DEV_ENV} is set): config must be explicitly specified via --config or {AIM_CONFIG_ENV} environment variable",
44 ).into());
45 }
46 let config = get_config_dir()?.join(format!("{APP_NAME}/config.toml"));
48 if !config.exists() {
49 return Err(format!("No config found at: {}", config.display()).into());
50 }
51 config
52 };
53
54 fs::read_to_string(&path)
55 .await
56 .map_err(|e| format!("Failed to read config file at {}: {}", path.display(), e))?
57 .parse::<ConfigRaw>()
58 .map(|mut a| {
59 a.core.config_dir = path.parent().map(PathBuf::from);
60 a.core.dev_mode = is_dev_mode().unwrap_or(false);
61 (a.core, Config {})
62 })
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66enum DevModeStrategy {
67 Environment,
68 ForcedNormal,
69 ForcedDev,
70}
71
72#[derive(Debug, Clone, Copy, serde::Deserialize)]
74pub struct Config;
75
76#[derive(Debug, serde::Deserialize)]
77struct ConfigRaw {
78 core: CoreConfig,
79}
80
81impl FromStr for ConfigRaw {
82 type Err = Box<dyn Error>;
83
84 fn from_str(s: &str) -> Result<Self, Self::Err> {
85 Ok(toml::from_str(s)?)
86 }
87}
88
89fn get_config_dir() -> Result<PathBuf, Box<dyn Error>> {
90 #[cfg(unix)]
91 let config_dir = xdg::BaseDirectories::new().get_config_home();
92 #[cfg(windows)]
93 let config_dir = dirs::config_dir();
94 config_dir.ok_or_else(|| "User-specific home directory not found".into())
95}
96
97fn is_dev_mode() -> Option<bool> {
98 if let Ok(val) = std::env::var(AIM_DEV_ENV) {
99 let lower = val.to_lowercase();
100 if AIM_DEV_VALID_TRUE.contains(&lower.as_str()) {
101 Some(true)
102 } else if AIM_DEV_VALID_FALSE.contains(&lower.as_str()) {
103 Some(false)
104 } else {
105 tracing::warn!(
106 "Unrecognized value for {}: '{}'. Expected one of: {}. Treating as unset.",
107 AIM_DEV_ENV,
108 val,
109 format!(
110 "true: {}, false: {}",
111 AIM_DEV_VALID_TRUE.join(", "),
112 AIM_DEV_VALID_FALSE.join(", ")
113 )
114 );
115 None
116 }
117 } else {
118 None
119 }
120}
121
122fn resolve_dev_mode_strategy() -> Result<DevModeStrategy, Box<dyn Error>> {
123 if cfg!(debug_assertions) || std::env::var_os(AIM_DEV_ENV).is_none() {
124 return Ok(DevModeStrategy::Environment);
125 }
126
127 if !stdin().is_terminal() || !stdout().is_terminal() {
128 return Ok(DevModeStrategy::Environment);
129 }
130
131 match prompt_dev_mode_choice()? {
132 DevModeChoice::Exit => {
133 Err("Aborted because AIM_DEV was detected in the environment".into())
134 }
135 DevModeChoice::Normal => Ok(DevModeStrategy::ForcedNormal),
136 DevModeChoice::Dev => Ok(DevModeStrategy::ForcedDev),
137 }
138}
139
140fn effective_dev_mode(strategy: DevModeStrategy) -> Option<bool> {
141 match strategy {
142 DevModeStrategy::Environment => is_dev_mode(),
143 DevModeStrategy::ForcedNormal => Some(false),
144 DevModeStrategy::ForcedDev => Some(true),
145 }
146}
147
148fn should_use_aim_config_env(strategy: DevModeStrategy) -> bool {
149 match strategy {
150 DevModeStrategy::Environment | DevModeStrategy::ForcedDev => {
151 std::env::var(AIM_CONFIG_ENV).is_ok()
152 }
153 DevModeStrategy::ForcedNormal => false,
154 }
155}
156
157#[cfg(test)]
158#[allow(unsafe_code)]
159mod tests {
160 use super::*;
161 use std::fs;
162 use std::sync::OnceLock;
163 use tempfile::TempDir;
164 use tokio::sync::Mutex;
165
166 static ENV_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
167
168 fn env_lock() -> &'static Mutex<()> {
169 ENV_LOCK.get_or_init(|| Mutex::new(()))
170 }
171
172 #[test]
173 fn forced_normal_disables_aim_config_env() {
174 assert!(!should_use_aim_config_env(DevModeStrategy::ForcedNormal));
175 }
176
177 #[test]
178 fn forced_dev_enables_dev_mode() {
179 assert_eq!(effective_dev_mode(DevModeStrategy::ForcedDev), Some(true));
180 }
181
182 #[test]
183 fn forced_normal_disables_dev_mode() {
184 assert_eq!(
185 effective_dev_mode(DevModeStrategy::ForcedNormal),
186 Some(false)
187 );
188 }
189
190 #[tokio::test]
191 async fn cli_flag_overrides_env_var() {
192 let temp_dir = TempDir::new().unwrap();
193 let config_path = temp_dir.path().join("config.toml");
194 let calendar_dir = temp_dir.path().join("calendar");
195 fs::create_dir(&calendar_dir).unwrap();
196
197 let toml_content = format!(
198 r#"
199[core]
200calendar_path = "{}"
201"#,
202 calendar_dir.to_str().unwrap().replace('\\', "/")
203 );
204 fs::write(&config_path, toml_content).unwrap();
205
206 let env_path = temp_dir.path().join("env_config.toml");
207 let env_calendar_dir = temp_dir.path().join("env_calendar");
208 fs::create_dir(&env_calendar_dir).unwrap();
209 let env_toml_content = format!(
210 r#"
211[core]
212calendar_path = "{}"
213"#,
214 env_calendar_dir.to_str().unwrap().replace('\\', "/")
215 );
216 fs::write(&env_path, env_toml_content).unwrap();
217
218 {
219 let _guard = env_lock().lock().await;
220 unsafe {
221 std::env::remove_var(AIM_CONFIG_ENV);
222 std::env::remove_var(AIM_DEV_ENV);
223 std::env::set_var(AIM_CONFIG_ENV, env_path.to_str().unwrap());
224 }
225
226 let (config, _) = parse_config(Some(config_path.clone())).await.unwrap();
227
228 assert_eq!(config.calendar_path, Some(calendar_dir));
229
230 unsafe {
231 std::env::remove_var(AIM_CONFIG_ENV);
232 }
233 }
234 }
235
236 #[tokio::test]
237 async fn env_var_overrides_default_config() {
238 let temp_dir = TempDir::new().unwrap();
239 let env_config_path = temp_dir.path().join("env_config.toml");
240 let calendar_dir = temp_dir.path().join("calendar");
241 fs::create_dir(&calendar_dir).unwrap();
242
243 let toml_content = format!(
244 r#"
245[core]
246calendar_path = "{}"
247"#,
248 calendar_dir.to_str().unwrap().replace('\\', "/")
249 );
250 fs::write(&env_config_path, toml_content).unwrap();
251
252 {
253 let _guard = env_lock().lock().await;
254 unsafe {
255 std::env::remove_var(AIM_CONFIG_ENV);
256 std::env::remove_var(AIM_DEV_ENV);
257 std::env::set_var(AIM_CONFIG_ENV, env_config_path.to_str().unwrap());
258 }
259
260 let (config, _) = parse_config(None).await.unwrap();
261
262 assert_eq!(config.calendar_path, Some(calendar_dir));
263
264 unsafe {
265 std::env::remove_var(AIM_CONFIG_ENV);
266 }
267 }
268 }
269
270 #[tokio::test]
271 async fn respects_priority_order() {
272 let temp_dir = TempDir::new().unwrap();
273
274 let cli_config_path = temp_dir.path().join("cli_config.toml");
275 let cli_calendar_dir = temp_dir.path().join("cli_calendar");
276 fs::create_dir(&cli_calendar_dir).unwrap();
277 let cli_toml_content = format!(
278 r#"
279[core]
280calendar_path = "{}"
281"#,
282 cli_calendar_dir.to_str().unwrap().replace('\\', "/")
283 );
284 fs::write(&cli_config_path, cli_toml_content).unwrap();
285
286 let env_config_path = temp_dir.path().join("env_config.toml");
287 let env_calendar_dir = temp_dir.path().join("env_calendar");
288 fs::create_dir(&env_calendar_dir).unwrap();
289 let env_toml_content = format!(
290 r#"
291[core]
292calendar_path = "{}"
293"#,
294 env_calendar_dir.to_str().unwrap().replace('\\', "/")
295 );
296 fs::write(&env_config_path, env_toml_content).unwrap();
297
298 {
299 let _guard = env_lock().lock().await;
300 unsafe {
301 std::env::remove_var(AIM_CONFIG_ENV);
302 std::env::remove_var(AIM_DEV_ENV);
303 std::env::set_var(AIM_CONFIG_ENV, env_config_path.to_str().unwrap());
304 }
305
306 let (config, _) = parse_config(Some(cli_config_path)).await.unwrap();
307
308 assert_eq!(config.calendar_path, Some(cli_calendar_dir));
309
310 unsafe {
311 std::env::remove_var(AIM_CONFIG_ENV);
312 }
313 }
314 }
315
316 #[cfg(unix)]
318 #[tokio::test]
319 async fn uses_default_when_no_cli_or_env() {
320 let temp_dir = TempDir::new().unwrap();
321 let default_config_dir = temp_dir.path().join("aim");
322 fs::create_dir_all(&default_config_dir).unwrap();
323 let default_config_path = default_config_dir.join("config.toml");
324 let calendar_dir = temp_dir.path().join("calendar");
325 fs::create_dir(&calendar_dir).unwrap();
326
327 let toml_content = format!(
328 r#"
329[core]
330calendar_path = "{}"
331"#,
332 calendar_dir.to_str().unwrap().replace('\\', "/")
333 );
334 fs::write(&default_config_path, toml_content).unwrap();
335
336 let xdg_config_home = temp_dir.path().to_str().unwrap().to_string();
337 {
338 let _guard = env_lock().lock().await;
339 unsafe {
340 std::env::remove_var(AIM_CONFIG_ENV);
341 std::env::remove_var(AIM_DEV_ENV);
342 std::env::set_var("XDG_CONFIG_HOME", xdg_config_home);
343 }
344
345 let (config, _) = parse_config(None).await.unwrap();
346
347 assert_eq!(config.calendar_path, Some(calendar_dir));
348
349 unsafe {
350 std::env::remove_var("XDG_CONFIG_HOME");
351 }
352 }
353 }
354
355 #[tokio::test]
356 async fn returns_error_when_no_config_found() {
357 let temp_dir = TempDir::new().unwrap();
358 let empty_dir = temp_dir.path().join("empty");
359 fs::create_dir(&empty_dir).unwrap();
360
361 let xdg_config_home = empty_dir.to_str().unwrap().to_string();
362 {
363 let _guard = env_lock().lock().await;
364 unsafe {
365 std::env::remove_var(AIM_CONFIG_ENV);
366 std::env::remove_var(AIM_DEV_ENV);
367 std::env::set_var("XDG_CONFIG_HOME", xdg_config_home);
368 }
369
370 let result = parse_config(None).await;
371
372 assert!(result.is_err());
373
374 unsafe {
375 std::env::remove_var("XDG_CONFIG_HOME");
376 }
377 }
378 }
379
380 #[tokio::test]
381 async fn aim_dev_1_disables_default_discovery() {
382 let temp_dir = TempDir::new().unwrap();
383 let empty_dir = temp_dir.path().join("empty");
384 fs::create_dir(&empty_dir).unwrap();
385
386 let xdg_config_home = empty_dir.to_str().unwrap().to_string();
387 {
388 let _guard = env_lock().lock().await;
389 unsafe {
390 std::env::remove_var(AIM_CONFIG_ENV);
391 std::env::remove_var(AIM_DEV_ENV);
392 std::env::remove_var("XDG_CONFIG_HOME");
393 std::env::set_var("XDG_CONFIG_HOME", xdg_config_home);
394 std::env::set_var(AIM_DEV_ENV, "1");
395 }
396
397 let result = parse_config(None).await;
398
399 assert!(result.is_err());
400 let error_msg = result.unwrap_err().to_string();
401 assert!(error_msg.contains("Development environment detected"));
402 assert!(error_msg.contains(AIM_DEV_ENV));
403
404 unsafe {
405 std::env::remove_var(AIM_DEV_ENV);
406 std::env::remove_var("XDG_CONFIG_HOME");
407 }
408 }
409 }
410
411 #[tokio::test]
412 async fn aim_dev_true_disables_default_discovery() {
413 let temp_dir = TempDir::new().unwrap();
414 let empty_dir = temp_dir.path().join("empty");
415 fs::create_dir(&empty_dir).unwrap();
416
417 let xdg_config_home = empty_dir.to_str().unwrap().to_string();
418 {
419 let _guard = env_lock().lock().await;
420 unsafe {
421 std::env::remove_var(AIM_CONFIG_ENV);
422 std::env::remove_var(AIM_DEV_ENV);
423 std::env::remove_var("XDG_CONFIG_HOME");
424 std::env::set_var("XDG_CONFIG_HOME", xdg_config_home);
425 std::env::set_var(AIM_DEV_ENV, "true");
426 }
427
428 let result = parse_config(None).await;
429
430 assert!(result.is_err());
431 let error_msg = result.unwrap_err().to_string();
432 assert!(error_msg.contains("Development environment detected"));
433
434 unsafe {
435 std::env::remove_var(AIM_DEV_ENV);
436 std::env::remove_var("XDG_CONFIG_HOME");
437 }
438 }
439 }
440
441 #[tokio::test]
442 async fn aim_dev_yes_disables_default_discovery() {
443 let temp_dir = TempDir::new().unwrap();
444 let empty_dir = temp_dir.path().join("empty");
445 fs::create_dir(&empty_dir).unwrap();
446
447 let xdg_config_home = empty_dir.to_str().unwrap().to_string();
448 {
449 let _guard = env_lock().lock().await;
450 unsafe {
451 std::env::remove_var(AIM_CONFIG_ENV);
452 std::env::remove_var(AIM_DEV_ENV);
453 std::env::remove_var("XDG_CONFIG_HOME");
454 std::env::set_var("XDG_CONFIG_HOME", xdg_config_home);
455 std::env::set_var(AIM_DEV_ENV, "yes");
456 }
457
458 let result = parse_config(None).await;
459
460 assert!(result.is_err());
461
462 unsafe {
463 std::env::remove_var(AIM_DEV_ENV);
464 std::env::remove_var("XDG_CONFIG_HOME");
465 }
466 }
467 }
468
469 #[cfg(unix)]
471 #[tokio::test]
472 async fn aim_dev_0_allows_default_discovery() {
473 let temp_dir = TempDir::new().unwrap();
474 let default_config_dir = temp_dir.path().join("aim");
475 fs::create_dir_all(&default_config_dir).unwrap();
476 let default_config_path = default_config_dir.join("config.toml");
477 let calendar_dir = temp_dir.path().join("calendar");
478 fs::create_dir(&calendar_dir).unwrap();
479
480 let toml_content = format!(
481 r#"
482[core]
483calendar_path = "{}"
484"#,
485 calendar_dir.to_str().unwrap().replace('\\', "/")
486 );
487 fs::write(&default_config_path, toml_content).unwrap();
488
489 let xdg_config_home = temp_dir.path().to_str().unwrap().to_string();
490 {
491 let _guard = env_lock().lock().await;
492 unsafe {
493 std::env::remove_var(AIM_CONFIG_ENV);
494 std::env::remove_var(AIM_DEV_ENV);
495 std::env::set_var("XDG_CONFIG_HOME", xdg_config_home);
496 std::env::set_var(AIM_DEV_ENV, "0");
497 }
498
499 let (config, _) = parse_config(None).await.unwrap();
500 assert_eq!(config.calendar_path, Some(calendar_dir));
501
502 unsafe {
503 std::env::remove_var(AIM_DEV_ENV);
504 std::env::remove_var("XDG_CONFIG_HOME");
505 }
506 }
507 }
508
509 #[cfg(unix)]
511 #[tokio::test]
512 async fn aim_dev_false_allows_default_discovery() {
513 let temp_dir = TempDir::new().unwrap();
514 let default_config_dir = temp_dir.path().join("aim");
515 fs::create_dir_all(&default_config_dir).unwrap();
516 let default_config_path = default_config_dir.join("config.toml");
517 let calendar_dir = temp_dir.path().join("calendar");
518 fs::create_dir(&calendar_dir).unwrap();
519
520 let toml_content = format!(
521 r#"
522[core]
523calendar_path = "{}"
524"#,
525 calendar_dir.to_str().unwrap().replace('\\', "/")
526 );
527 fs::write(&default_config_path, toml_content).unwrap();
528
529 let xdg_config_home = temp_dir.path().to_str().unwrap().to_string();
530 {
531 let _guard = env_lock().lock().await;
532 unsafe {
533 std::env::remove_var(AIM_CONFIG_ENV);
534 std::env::remove_var(AIM_DEV_ENV);
535 std::env::remove_var("XDG_CONFIG_HOME");
536 std::env::set_var("XDG_CONFIG_HOME", xdg_config_home);
537 std::env::set_var(AIM_DEV_ENV, "false");
538 }
539
540 let (config, _) = parse_config(None).await.unwrap();
541 assert_eq!(config.calendar_path, Some(calendar_dir));
542
543 unsafe {
544 std::env::remove_var(AIM_DEV_ENV);
545 std::env::remove_var("XDG_CONFIG_HOME");
546 }
547 }
548 }
549
550 #[cfg(unix)]
552 #[tokio::test]
553 async fn aim_dev_no_allows_default_discovery() {
554 let temp_dir = TempDir::new().unwrap();
555 let default_config_dir = temp_dir.path().join("aim");
556 fs::create_dir_all(&default_config_dir).unwrap();
557 let default_config_path = default_config_dir.join("config.toml");
558 let calendar_dir = temp_dir.path().join("calendar");
559 fs::create_dir(&calendar_dir).unwrap();
560
561 let toml_content = format!(
562 r#"
563[core]
564calendar_path = "{}"
565"#,
566 calendar_dir.to_str().unwrap().replace('\\', "/")
567 );
568 fs::write(&default_config_path, toml_content).unwrap();
569
570 let xdg_config_home = temp_dir.path().to_str().unwrap().to_string();
571 {
572 let _guard = env_lock().lock().await;
573 unsafe {
574 std::env::remove_var(AIM_CONFIG_ENV);
575 std::env::remove_var(AIM_DEV_ENV);
576 std::env::remove_var("XDG_CONFIG_HOME");
577 std::env::set_var("XDG_CONFIG_HOME", xdg_config_home);
578 std::env::set_var(AIM_DEV_ENV, "no");
579 }
580
581 let (config, _) = parse_config(None).await.unwrap();
582 assert_eq!(config.calendar_path, Some(calendar_dir));
583
584 unsafe {
585 std::env::remove_var(AIM_DEV_ENV);
586 std::env::remove_var("XDG_CONFIG_HOME");
587 }
588 }
589 }
590
591 #[cfg(unix)]
593 #[tokio::test]
594 async fn aim_dev_case_insensitive() {
595 let temp_dir = TempDir::new().unwrap();
596 let empty_dir = temp_dir.path().join("empty");
597 fs::create_dir(&empty_dir).unwrap();
598
599 let xdg_config_home = empty_dir.to_str().unwrap().to_string();
600 {
601 let _guard = env_lock().lock().await;
602 unsafe {
603 std::env::remove_var(AIM_CONFIG_ENV);
604 std::env::remove_var(AIM_DEV_ENV);
605 std::env::remove_var("XDG_CONFIG_HOME");
606 std::env::set_var("XDG_CONFIG_HOME", xdg_config_home);
607 std::env::set_var(AIM_DEV_ENV, "TRUE");
608 }
609
610 let result = parse_config(None).await;
611 assert!(result.is_err());
612 }
613
614 let default_config_dir = temp_dir.path().join("aim");
615 fs::create_dir_all(&default_config_dir).unwrap();
616 let default_config_path = default_config_dir.join("config.toml");
617 let calendar_dir = temp_dir.path().join("calendar");
618 fs::create_dir(&calendar_dir).unwrap();
619
620 let toml_content = format!(
621 r#"
622[core]
623calendar_path = "{}"
624"#,
625 calendar_dir.to_str().unwrap().replace('\\', "/")
626 );
627 fs::write(&default_config_path, toml_content).unwrap();
628
629 let xdg_config_home = temp_dir.path().to_str().unwrap().to_string();
630 {
631 let _guard = env_lock().lock().await;
632 unsafe {
633 std::env::remove_var(AIM_CONFIG_ENV);
634 std::env::remove_var(AIM_DEV_ENV);
635 std::env::remove_var("XDG_CONFIG_HOME");
636 std::env::set_var("XDG_CONFIG_HOME", xdg_config_home);
637 std::env::set_var(AIM_DEV_ENV, "False");
638 }
639
640 let (config, _) = parse_config(None).await.unwrap();
641 assert_eq!(config.calendar_path, Some(calendar_dir));
642
643 unsafe {
644 std::env::remove_var(AIM_DEV_ENV);
645 std::env::remove_var("XDG_CONFIG_HOME");
646 }
647 }
648 }
649
650 #[tokio::test]
651 async fn aim_dev_cli_flag_overrides() {
652 let temp_dir = TempDir::new().unwrap();
653 let config_path = temp_dir.path().join("config.toml");
654 let calendar_dir = temp_dir.path().join("calendar");
655 fs::create_dir(&calendar_dir).unwrap();
656
657 let toml_content = format!(
658 r#"
659[core]
660calendar_path = "{}"
661"#,
662 calendar_dir.to_str().unwrap().replace('\\', "/")
663 );
664 fs::write(&config_path, toml_content).unwrap();
665
666 {
667 let _guard = env_lock().lock().await;
668 unsafe {
669 std::env::set_var(AIM_DEV_ENV, "1");
670 }
671
672 let (config, _) = parse_config(Some(config_path)).await.unwrap();
673 assert_eq!(config.calendar_path, Some(calendar_dir));
674
675 unsafe {
676 std::env::remove_var(AIM_DEV_ENV);
677 }
678 }
679 }
680
681 #[tokio::test]
682 async fn aim_dev_aim_config_env_var_overrides() {
683 let temp_dir = TempDir::new().unwrap();
684 let env_config_path = temp_dir.path().join("env_config.toml");
685 let calendar_dir = temp_dir.path().join("calendar");
686 fs::create_dir(&calendar_dir).unwrap();
687
688 let toml_content = format!(
689 r#"
690[core]
691calendar_path = "{}"
692"#,
693 calendar_dir.to_str().unwrap().replace('\\', "/")
694 );
695 fs::write(&env_config_path, toml_content).unwrap();
696
697 {
698 let _guard = env_lock().lock().await;
699 unsafe {
700 std::env::remove_var(AIM_CONFIG_ENV);
701 std::env::remove_var(AIM_DEV_ENV);
702 std::env::set_var(AIM_CONFIG_ENV, env_config_path.to_str().unwrap());
703 std::env::set_var(AIM_DEV_ENV, "1");
704 }
705
706 let (config, _) = parse_config(None).await.unwrap();
707 assert_eq!(config.calendar_path, Some(calendar_dir));
708
709 unsafe {
710 std::env::remove_var(AIM_CONFIG_ENV);
711 std::env::remove_var(AIM_DEV_ENV);
712 }
713 }
714 }
715
716 #[cfg(unix)]
718 #[tokio::test]
719 async fn aim_dev_unrecognized_value_allows_default() {
720 let temp_dir = TempDir::new().unwrap();
721 let default_config_dir = temp_dir.path().join("aim");
722 fs::create_dir_all(&default_config_dir).unwrap();
723 let default_config_path = default_config_dir.join("config.toml");
724 let calendar_dir = temp_dir.path().join("calendar");
725 fs::create_dir(&calendar_dir).unwrap();
726
727 let toml_content = format!(
728 r#"
729[core]
730calendar_path = "{}"
731"#,
732 calendar_dir.to_str().unwrap().replace('\\', "/")
733 );
734 fs::write(&default_config_path, toml_content).unwrap();
735
736 let xdg_config_home = temp_dir.path().to_str().unwrap().to_string();
737 {
738 let _guard = env_lock().lock().await;
739 unsafe {
740 std::env::remove_var(AIM_CONFIG_ENV);
741 std::env::remove_var(AIM_DEV_ENV);
742 std::env::remove_var("XDG_CONFIG_HOME");
743 std::env::set_var("XDG_CONFIG_HOME", xdg_config_home);
744 std::env::set_var(AIM_DEV_ENV, "invalid");
745 }
746
747 let (config, _) = parse_config(None).await.unwrap();
748 assert_eq!(config.calendar_path, Some(calendar_dir));
749
750 unsafe {
751 std::env::remove_var(AIM_DEV_ENV);
752 std::env::remove_var("XDG_CONFIG_HOME");
753 }
754 }
755 }
756}