1use crate::error::{Result, ThingsError};
4use std::path::{Path, PathBuf};
5
6#[derive(Debug, Clone)]
8pub struct ThingsConfig {
9 pub database_path: PathBuf,
11 pub fallback_to_default: bool,
13}
14
15impl ThingsConfig {
16 #[must_use]
22 pub fn new<P: AsRef<Path>>(database_path: P, fallback_to_default: bool) -> Self {
23 Self {
24 database_path: database_path.as_ref().to_path_buf(),
25 fallback_to_default,
26 }
27 }
28
29 #[must_use]
31 pub fn with_default_path() -> Self {
32 Self {
33 database_path: Self::get_default_database_path(),
34 fallback_to_default: false,
35 }
36 }
37
38 pub fn get_effective_database_path(&self) -> Result<PathBuf> {
43 if self.database_path.exists() {
45 return Ok(self.database_path.clone());
46 }
47
48 if self.fallback_to_default {
50 let default_path = Self::get_default_database_path();
51 if default_path.exists() {
52 return Ok(default_path);
53 }
54 }
55
56 Err(ThingsError::configuration(format!(
57 "Database not found at {} and fallback is {}",
58 self.database_path.display(),
59 if self.fallback_to_default {
60 "enabled but default path also not found"
61 } else {
62 "disabled"
63 }
64 )))
65 }
66
67 #[must_use]
72 pub fn get_default_database_path() -> PathBuf {
73 crate::database::get_default_database_path()
74 }
75
76 #[must_use]
81 pub fn from_env() -> Self {
82 let database_path = match std::env::var("THINGS_DB_PATH") {
83 Ok(v) => PathBuf::from(v),
84 Err(_) => match std::env::var("THINGS_DATABASE_PATH") {
85 Ok(v) => {
86 tracing::warn!(
87 "THINGS_DATABASE_PATH is deprecated; please use THINGS_DB_PATH instead"
88 );
89 PathBuf::from(v)
90 }
91 Err(_) => Self::get_default_database_path(),
92 },
93 };
94
95 let fallback_to_default = if let Ok(v) = std::env::var("THINGS_FALLBACK_TO_DEFAULT") {
96 let lower = v.to_lowercase();
97 match lower.as_str() {
98 "true" | "1" | "yes" | "on" => true,
99 _ => false, }
101 } else {
102 true
103 };
104
105 Self::new(database_path, fallback_to_default)
106 }
107
108 pub fn for_testing() -> Result<Self> {
113 use tempfile::NamedTempFile;
114 let temp_file = NamedTempFile::new()?;
115 let db_path = temp_file.path().to_path_buf();
116 Ok(Self::new(db_path, false))
117 }
118}
119
120impl Default for ThingsConfig {
121 fn default() -> Self {
122 Self::with_default_path()
123 }
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129 use serial_test::serial;
130 use tempfile::NamedTempFile;
131
132 #[test]
133 fn test_config_creation() {
134 let config = ThingsConfig::new("/path/to/db.sqlite", true);
135 assert_eq!(config.database_path, PathBuf::from("/path/to/db.sqlite"));
136 assert!(config.fallback_to_default);
137 }
138
139 #[test]
140 fn test_default_config() {
141 let config = ThingsConfig::default();
142 assert!(config
143 .database_path
144 .to_string_lossy()
145 .contains("Things Database.thingsdatabase"));
146 assert!(!config.fallback_to_default);
147 }
148
149 #[test]
150 #[serial]
151 fn test_config_from_env() {
152 let test_path = "/custom/path/db.sqlite";
153
154 let original_db_path = std::env::var("THINGS_DB_PATH").ok();
155 let original_legacy = std::env::var("THINGS_DATABASE_PATH").ok();
156 let original_fallback = std::env::var("THINGS_FALLBACK_TO_DEFAULT").ok();
157
158 std::env::remove_var("THINGS_DB_PATH");
159 std::env::set_var("THINGS_DATABASE_PATH", test_path);
160 std::env::set_var("THINGS_FALLBACK_TO_DEFAULT", "true");
161
162 let config = ThingsConfig::from_env();
163 let path_matches = config.database_path.as_os_str() == test_path;
164 let fallback_set = config.fallback_to_default;
165
166 if let Some(v) = original_db_path {
167 std::env::set_var("THINGS_DB_PATH", v);
168 }
169 if let Some(v) = original_legacy {
170 std::env::set_var("THINGS_DATABASE_PATH", v);
171 } else {
172 std::env::remove_var("THINGS_DATABASE_PATH");
173 }
174 if let Some(v) = original_fallback {
175 std::env::set_var("THINGS_FALLBACK_TO_DEFAULT", v);
176 } else {
177 std::env::remove_var("THINGS_FALLBACK_TO_DEFAULT");
178 }
179
180 assert!(path_matches);
181 assert!(fallback_set);
182 }
183
184 #[test]
185 #[serial]
186 fn test_from_env_reads_things_db_path() {
187 let test_path = "/custom/path/via_db_path.sqlite";
188
189 let original_db_path = std::env::var("THINGS_DB_PATH").ok();
190 let original_legacy = std::env::var("THINGS_DATABASE_PATH").ok();
191
192 std::env::remove_var("THINGS_DATABASE_PATH");
193 std::env::set_var("THINGS_DB_PATH", test_path);
194
195 let config = ThingsConfig::from_env();
196 assert_eq!(config.database_path, PathBuf::from(test_path));
197
198 if let Some(v) = original_db_path {
199 std::env::set_var("THINGS_DB_PATH", v);
200 } else {
201 std::env::remove_var("THINGS_DB_PATH");
202 }
203 if let Some(v) = original_legacy {
204 std::env::set_var("THINGS_DATABASE_PATH", v);
205 }
206 }
207
208 #[test]
209 #[serial]
210 fn test_from_env_prefers_things_db_path_over_legacy() {
211 let new_path = "/new/preferred.sqlite";
212 let legacy_path = "/legacy/ignored.sqlite";
213
214 let original_db_path = std::env::var("THINGS_DB_PATH").ok();
215 let original_legacy = std::env::var("THINGS_DATABASE_PATH").ok();
216
217 std::env::set_var("THINGS_DB_PATH", new_path);
218 std::env::set_var("THINGS_DATABASE_PATH", legacy_path);
219
220 let config = ThingsConfig::from_env();
221 assert_eq!(config.database_path, PathBuf::from(new_path));
222
223 if let Some(v) = original_db_path {
224 std::env::set_var("THINGS_DB_PATH", v);
225 } else {
226 std::env::remove_var("THINGS_DB_PATH");
227 }
228 if let Some(v) = original_legacy {
229 std::env::set_var("THINGS_DATABASE_PATH", v);
230 } else {
231 std::env::remove_var("THINGS_DATABASE_PATH");
232 }
233 }
234
235 #[test]
236 #[serial]
237 fn test_from_env_falls_back_to_legacy_things_database_path() {
238 let legacy_path = "/legacy/only.sqlite";
239
240 let original_db_path = std::env::var("THINGS_DB_PATH").ok();
241 let original_legacy = std::env::var("THINGS_DATABASE_PATH").ok();
242
243 std::env::remove_var("THINGS_DB_PATH");
244 std::env::set_var("THINGS_DATABASE_PATH", legacy_path);
245
246 let config = ThingsConfig::from_env();
247 assert_eq!(config.database_path, PathBuf::from(legacy_path));
248
249 if let Some(v) = original_db_path {
250 std::env::set_var("THINGS_DB_PATH", v);
251 }
252 if let Some(v) = original_legacy {
253 std::env::set_var("THINGS_DATABASE_PATH", v);
254 } else {
255 std::env::remove_var("THINGS_DATABASE_PATH");
256 }
257 }
258
259 #[test]
260 fn test_effective_database_path() {
261 let temp_file = NamedTempFile::new().unwrap();
263 let db_path = temp_file.path();
264 let config = ThingsConfig::new(db_path, false);
265
266 let effective_path = config.get_effective_database_path().unwrap();
267 assert_eq!(effective_path, db_path);
268 }
269
270 #[test]
271 fn test_fallback_behavior() {
272 let config = ThingsConfig::new("/nonexistent/path.sqlite", true);
274 let result = config.get_effective_database_path();
275
276 if ThingsConfig::get_default_database_path().exists() {
278 assert!(result.is_ok());
279 assert_eq!(result.unwrap(), ThingsConfig::get_default_database_path());
280 } else {
281 assert!(result.is_err());
283 }
284 }
285
286 #[test]
287 fn test_fallback_disabled() {
288 let config = ThingsConfig::new("/nonexistent/path.sqlite", false);
290 let result = config.get_effective_database_path();
291
292 assert!(result.is_err());
294 }
295
296 #[test]
297 fn test_config_with_fallback_enabled() {
298 let config = ThingsConfig::new("/nonexistent/path", true);
299 assert_eq!(config.database_path, PathBuf::from("/nonexistent/path"));
300 assert!(config.fallback_to_default);
301 }
302
303 #[test]
304 #[serial]
305 fn test_config_from_env_with_custom_path() {
306 let test_path = "/test/env/custom/path";
307
308 let original_db_path = std::env::var("THINGS_DB_PATH").ok();
309 let original_legacy = std::env::var("THINGS_DATABASE_PATH").ok();
310 let original_fallback = std::env::var("THINGS_FALLBACK_TO_DEFAULT").ok();
311
312 std::env::remove_var("THINGS_DB_PATH");
313 std::env::set_var("THINGS_DATABASE_PATH", test_path);
314 std::env::set_var("THINGS_FALLBACK_TO_DEFAULT", "false");
315
316 let config = ThingsConfig::from_env();
317 let path_matches = config.database_path.as_os_str() == test_path;
318 let fallback_off = !config.fallback_to_default;
319
320 if let Some(v) = original_db_path {
321 std::env::set_var("THINGS_DB_PATH", v);
322 }
323 if let Some(v) = original_legacy {
324 std::env::set_var("THINGS_DATABASE_PATH", v);
325 } else {
326 std::env::remove_var("THINGS_DATABASE_PATH");
327 }
328 if let Some(v) = original_fallback {
329 std::env::set_var("THINGS_FALLBACK_TO_DEFAULT", v);
330 } else {
331 std::env::remove_var("THINGS_FALLBACK_TO_DEFAULT");
332 }
333
334 assert!(path_matches);
335 assert!(fallback_off);
336 }
337
338 #[test]
339 #[serial]
340 fn test_config_from_env_with_fallback() {
341 let test_id = std::thread::current().id();
342 let test_path = format!("/test/env/path/fallback_{test_id:?}");
343
344 let original_db_path = std::env::var("THINGS_DB_PATH").ok();
345 let original_legacy = std::env::var("THINGS_DATABASE_PATH").ok();
346 let original_fallback = std::env::var("THINGS_FALLBACK_TO_DEFAULT").ok();
347
348 std::env::remove_var("THINGS_DB_PATH");
349 std::env::set_var("THINGS_DATABASE_PATH", &test_path);
350 std::env::set_var("THINGS_FALLBACK_TO_DEFAULT", "true");
351
352 let config = ThingsConfig::from_env();
353 let path_matches =
354 config.database_path.to_string_lossy() == PathBuf::from(&test_path).to_string_lossy();
355 let fallback_set = config.fallback_to_default;
356
357 if let Some(v) = original_db_path {
358 std::env::set_var("THINGS_DB_PATH", v);
359 }
360 if let Some(v) = original_legacy {
361 std::env::set_var("THINGS_DATABASE_PATH", v);
362 } else {
363 std::env::remove_var("THINGS_DATABASE_PATH");
364 }
365 if let Some(v) = original_fallback {
366 std::env::set_var("THINGS_FALLBACK_TO_DEFAULT", v);
367 } else {
368 std::env::remove_var("THINGS_FALLBACK_TO_DEFAULT");
369 }
370
371 assert!(path_matches);
372 assert!(fallback_set);
373 }
374
375 #[test]
376 #[serial]
377 fn test_config_from_env_with_invalid_fallback() {
378 let test_id = std::thread::current().id();
379 let test_path = format!("/test/env/path/invalid_{test_id:?}");
380
381 let original_db_path = std::env::var("THINGS_DB_PATH").ok();
382 let original_legacy = std::env::var("THINGS_DATABASE_PATH").ok();
383 let original_fallback = std::env::var("THINGS_FALLBACK_TO_DEFAULT").ok();
384
385 std::env::remove_var("THINGS_DB_PATH");
386 std::env::set_var("THINGS_DATABASE_PATH", &test_path);
387 std::env::set_var("THINGS_FALLBACK_TO_DEFAULT", "invalid");
388
389 let config = ThingsConfig::from_env();
390 let path_matches =
391 config.database_path.to_string_lossy() == PathBuf::from(&test_path).to_string_lossy();
392 let fallback_off = !config.fallback_to_default;
393
394 if let Some(v) = original_db_path {
395 std::env::set_var("THINGS_DB_PATH", v);
396 }
397 if let Some(v) = original_legacy {
398 std::env::set_var("THINGS_DATABASE_PATH", v);
399 } else {
400 std::env::remove_var("THINGS_DATABASE_PATH");
401 }
402 if let Some(v) = original_fallback {
403 std::env::set_var("THINGS_FALLBACK_TO_DEFAULT", v);
404 } else {
405 std::env::remove_var("THINGS_FALLBACK_TO_DEFAULT");
406 }
407
408 assert!(path_matches);
409 assert!(fallback_off);
410 }
411
412 #[test]
413 fn test_config_debug_formatting() {
414 let config = ThingsConfig::new("/test/path", true);
415 let debug_str = format!("{config:?}");
416 assert!(debug_str.contains("/test/path"));
417 assert!(debug_str.contains("true"));
418 }
419
420 #[test]
421 fn test_config_clone() {
422 let config1 = ThingsConfig::new("/test/path", true);
423 let config2 = config1.clone();
424
425 assert_eq!(config1.database_path, config2.database_path);
426 assert_eq!(config1.fallback_to_default, config2.fallback_to_default);
427 }
428
429 #[test]
430 fn test_config_with_different_path_types() {
431 let config = ThingsConfig::new("relative/path", false);
433 assert_eq!(config.database_path, PathBuf::from("relative/path"));
434
435 let config = ThingsConfig::new("/absolute/path", false);
437 assert_eq!(config.database_path, PathBuf::from("/absolute/path"));
438
439 let config = ThingsConfig::new(".", false);
441 assert_eq!(config.database_path, PathBuf::from("."));
442 }
443
444 #[test]
445 fn test_config_edge_cases() {
446 let config = ThingsConfig::new("", false);
448 assert_eq!(config.database_path, PathBuf::from(""));
449
450 let long_path = "/".repeat(1000);
452 let config = ThingsConfig::new(&long_path, false);
453 assert_eq!(config.database_path, PathBuf::from(&long_path));
454 }
455
456 #[test]
457 fn test_get_default_database_path() {
458 let default_path = ThingsConfig::get_default_database_path();
459
460 assert!(!default_path.to_string_lossy().is_empty());
462
463 assert!(!default_path.to_string_lossy().is_empty());
465 }
466
467 #[test]
468 fn test_for_testing() {
469 let config = ThingsConfig::for_testing().unwrap();
471
472 assert!(!config.database_path.to_string_lossy().is_empty());
474
475 assert!(!config.fallback_to_default);
477
478 assert!(config.database_path.parent().is_some());
480 }
481
482 #[test]
483 fn test_with_default_path() {
484 let config = ThingsConfig::with_default_path();
485
486 assert_eq!(
488 config.database_path,
489 ThingsConfig::get_default_database_path()
490 );
491
492 assert!(!config.fallback_to_default);
494 }
495
496 #[test]
497 fn test_effective_database_path_fallback_enabled_but_default_missing() {
498 let config = ThingsConfig::new("/nonexistent/path.sqlite", true);
500 let result = config.get_effective_database_path();
501
502 let default_path = ThingsConfig::get_default_database_path();
504 if default_path.exists() {
505 assert!(result.is_ok());
507 assert_eq!(result.unwrap(), default_path);
508 } else {
509 assert!(result.is_err());
511 let error = result.unwrap_err();
512 match error {
513 ThingsError::Configuration { message } => {
514 assert!(message.contains("Database not found at"));
515 assert!(message.contains("fallback is enabled but default path also not found"));
516 }
517 _ => panic!("Expected Configuration error, got: {error:?}"),
518 }
519 }
520 }
521
522 #[test]
523 fn test_effective_database_path_fallback_disabled_error_message() {
524 let config = ThingsConfig::new("/nonexistent/path.sqlite", false);
526 let result = config.get_effective_database_path();
527
528 assert!(result.is_err());
530 let error = result.unwrap_err();
531 match error {
532 ThingsError::Configuration { message } => {
533 assert!(message.contains("Database not found at"));
534 assert!(message.contains("fallback is disabled"));
535 }
536 _ => panic!("Expected Configuration error, got: {error:?}"),
537 }
538 }
539
540 #[test]
541 #[serial]
542 fn test_from_env_without_variables() {
543 let original_db_path = std::env::var("THINGS_DB_PATH").ok();
544 let original_legacy = std::env::var("THINGS_DATABASE_PATH").ok();
545 let original_fallback = std::env::var("THINGS_FALLBACK_TO_DEFAULT").ok();
546
547 std::env::remove_var("THINGS_DB_PATH");
548 std::env::remove_var("THINGS_DATABASE_PATH");
549 std::env::remove_var("THINGS_FALLBACK_TO_DEFAULT");
550
551 let config = ThingsConfig::from_env();
552 let expected_path = ThingsConfig::get_default_database_path();
553
554 if let Some(v) = original_db_path {
555 std::env::set_var("THINGS_DB_PATH", v);
556 }
557 if let Some(v) = original_legacy {
558 std::env::set_var("THINGS_DATABASE_PATH", v);
559 }
560 if let Some(v) = original_fallback {
561 std::env::set_var("THINGS_FALLBACK_TO_DEFAULT", v);
562 }
563
564 assert_eq!(config.database_path, expected_path);
565 assert!(config.fallback_to_default);
566 }
567
568 #[test]
569 fn test_from_env_fallback_parsing() {
570 let test_cases = vec![
572 ("true", true),
573 ("TRUE", true),
574 ("True", true),
575 ("1", true),
576 ("yes", true),
577 ("YES", true),
578 ("on", true),
579 ("ON", true),
580 ("false", false),
581 ("FALSE", false),
582 ("0", false),
583 ("no", false),
584 ("off", false),
585 ("invalid", false),
586 ("", false),
587 ];
588
589 for (value, expected) in test_cases {
590 let fallback = value.to_lowercase();
592 let result =
593 fallback == "true" || fallback == "1" || fallback == "yes" || fallback == "on";
594 assert_eq!(result, expected, "Failed for value: '{value}'");
595 }
596 }
597
598 #[test]
599 fn test_default_trait_implementation() {
600 let config = ThingsConfig::default();
602
603 let expected = ThingsConfig::with_default_path();
605 assert_eq!(config.database_path, expected.database_path);
606 assert_eq!(config.fallback_to_default, expected.fallback_to_default);
607 }
608
609 #[test]
610 fn test_config_with_path_reference() {
611 let path_str = "/test/path/string";
613 let path_buf = PathBuf::from("/test/path/buf");
614
615 let config1 = ThingsConfig::new(path_str, true);
616 let config2 = ThingsConfig::new(&path_buf, false);
617
618 assert_eq!(config1.database_path, PathBuf::from(path_str));
619 assert_eq!(config2.database_path, path_buf);
620 }
621
622 #[test]
623 fn test_effective_database_path_existing_file() {
624 let temp_file = NamedTempFile::new().unwrap();
626 let db_path = temp_file.path().to_path_buf();
627 let config = ThingsConfig::new(&db_path, false);
628
629 let effective_path = config.get_effective_database_path().unwrap();
630 assert_eq!(effective_path, db_path);
631 }
632
633 #[test]
634 fn test_effective_database_path_fallback_success() {
635 let default_path = ThingsConfig::get_default_database_path();
637
638 if default_path.exists() {
640 let config = ThingsConfig::new("/nonexistent/path.sqlite", true);
641 let effective_path = config.get_effective_database_path().unwrap();
642 assert_eq!(effective_path, default_path);
643 }
644 }
645
646 #[test]
647 fn test_config_debug_implementation() {
648 let config = ThingsConfig::new("/test/debug/path", true);
650 let debug_str = format!("{config:?}");
651
652 assert!(debug_str.contains("database_path"));
654 assert!(debug_str.contains("fallback_to_default"));
655 assert!(debug_str.contains("/test/debug/path"));
656 assert!(debug_str.contains("true"));
657 }
658
659 #[test]
660 fn test_config_clone_implementation() {
661 let config1 = ThingsConfig::new("/test/clone/path", true);
663 let config2 = config1.clone();
664
665 assert_eq!(config1.database_path, config2.database_path);
667 assert_eq!(config1.fallback_to_default, config2.fallback_to_default);
668
669 let config3 = ThingsConfig::new("/different/path", false);
671 assert_ne!(config1.database_path, config3.database_path);
672 assert_ne!(config1.fallback_to_default, config3.fallback_to_default);
673 }
674
675 #[test]
676 fn test_get_default_database_path_format() {
677 let default_path = ThingsConfig::get_default_database_path();
681 let path_str = default_path.to_string_lossy();
682
683 assert!(path_str.contains("Library"));
684 assert!(path_str.contains("Group Containers"));
685 assert!(path_str.contains("JLMPQHK86H.com.culturedcode.ThingsMac"));
686 assert!(path_str.contains("ThingsData-"));
687 assert!(path_str.contains("Things Database.thingsdatabase"));
688 assert!(path_str.contains("main.sqlite"));
689 }
690
691 #[test]
692 fn test_home_env_var_fallback() {
693 let default_path = ThingsConfig::get_default_database_path();
696 let path_str = default_path.to_string_lossy();
697
698 assert!(path_str.starts_with('/') || path_str.starts_with('~'));
700 }
701
702 #[test]
703 fn test_config_effective_database_path_existing_file() {
704 let temp_dir = std::env::temp_dir();
706 let temp_file = temp_dir.join("test_db.sqlite");
707 std::fs::File::create(&temp_file).unwrap();
708
709 let config = ThingsConfig::new(temp_file.clone(), false);
710 let effective_path = config.get_effective_database_path().unwrap();
711 assert_eq!(effective_path, temp_file);
712
713 std::fs::remove_file(&temp_file).unwrap();
715 }
716
717 #[test]
718 fn test_config_effective_database_path_fallback_success() {
719 let temp_dir = std::env::temp_dir();
721 let temp_file = temp_dir.join("test_database.sqlite");
722 std::fs::File::create(&temp_file).unwrap();
723
724 let config = ThingsConfig::new(temp_file.clone(), true);
726
727 let effective_path = config.get_effective_database_path().unwrap();
728
729 assert_eq!(effective_path, temp_file);
731
732 std::fs::remove_file(&temp_file).unwrap();
734 }
735
736 #[test]
737 fn test_config_effective_database_path_fallback_disabled_error_message() {
738 let non_existent_path = PathBuf::from("/nonexistent/path/db.sqlite");
739 let config = ThingsConfig::new(non_existent_path, false);
740
741 let result = config.get_effective_database_path();
743 assert!(result.is_err());
744 let error = result.unwrap_err();
745 assert!(matches!(error, ThingsError::Configuration { .. }));
746 }
747
748 #[test]
749 #[serial]
750 fn test_config_effective_database_path_fallback_enabled_but_default_missing() {
751 let original_home = std::env::var("HOME").ok();
753 std::env::set_var("HOME", "/nonexistent/home");
754
755 let non_existent_path = PathBuf::from("/nonexistent/path/db.sqlite");
757 let config = ThingsConfig::new(non_existent_path, true);
758
759 let result = config.get_effective_database_path();
761
762 if let Some(home) = original_home {
764 std::env::set_var("HOME", home);
765 } else {
766 std::env::remove_var("HOME");
767 }
768
769 assert!(
770 result.is_err(),
771 "Expected error when both configured and default paths don't exist"
772 );
773 let error = result.unwrap_err();
774 assert!(matches!(error, ThingsError::Configuration { .. }));
775
776 let error_message = format!("{error}");
778 assert!(error_message.contains("Database not found at /nonexistent/path/db.sqlite"));
779 assert!(error_message.contains("fallback is enabled but default path also not found"));
780 }
781
782 #[test]
783 fn test_config_fallback_behavior() {
784 let path = PathBuf::from("/test/path/db.sqlite");
785
786 let config_with_fallback = ThingsConfig::new(path.clone(), true);
788 assert!(config_with_fallback.fallback_to_default);
789
790 let config_without_fallback = ThingsConfig::new(path, false);
792 assert!(!config_without_fallback.fallback_to_default);
793 }
794
795 #[test]
796 fn test_config_fallback_disabled() {
797 let path = PathBuf::from("/test/path/db.sqlite");
798 let config = ThingsConfig::new(path, false);
799 assert!(!config.fallback_to_default);
800 }
801
802 #[test]
803 #[serial]
804 fn test_config_from_env_without_variables() {
805 let original_db_path = std::env::var("THINGS_DB_PATH").ok();
806 let original_legacy = std::env::var("THINGS_DATABASE_PATH").ok();
807 let original_fallback = std::env::var("THINGS_FALLBACK_TO_DEFAULT").ok();
808
809 std::env::remove_var("THINGS_DB_PATH");
810 std::env::remove_var("THINGS_DATABASE_PATH");
811 std::env::remove_var("THINGS_FALLBACK_TO_DEFAULT");
812
813 let config = ThingsConfig::from_env();
814 let contains_default = config
815 .database_path
816 .to_string_lossy()
817 .contains("Things Database.thingsdatabase");
818
819 if let Some(v) = original_db_path {
820 std::env::set_var("THINGS_DB_PATH", v);
821 }
822 if let Some(v) = original_legacy {
823 std::env::set_var("THINGS_DATABASE_PATH", v);
824 }
825 if let Some(v) = original_fallback {
826 std::env::set_var("THINGS_FALLBACK_TO_DEFAULT", v);
827 }
828
829 assert!(contains_default);
830 }
831
832 #[test]
833 fn test_config_from_env_fallback_parsing() {
834 let test_cases = vec![
838 ("true", true),
839 ("false", false),
840 ("1", true),
841 ("0", false),
842 ("yes", true),
843 ("no", false),
844 ("invalid", false),
845 ];
846
847 for (value, expected) in test_cases {
848 let lower = value.to_lowercase();
850 let result = match lower.as_str() {
851 "true" | "1" | "yes" | "on" => true,
852 _ => false, };
854
855 assert_eq!(
856 result, expected,
857 "Failed for value: '{value}', expected: {expected}, got: {result}"
858 );
859 }
860 }
861
862 #[test]
863 fn test_config_for_testing() {
864 let result = ThingsConfig::for_testing();
865 assert!(result.is_ok(), "Should create test config successfully");
866
867 let config = result.unwrap();
868 assert!(
869 !config.fallback_to_default,
870 "Test config should have fallback disabled"
871 );
872
873 let path_str = config.database_path.to_string_lossy();
875 assert!(
876 path_str.contains("tmp") || !path_str.is_empty(),
877 "Test config should use a temporary path"
878 );
879 }
880
881 #[test]
882 fn test_config_effective_database_path_error_cases() {
883 let non_existent_path = PathBuf::from("/absolutely/non/existent/path/database.db");
885 let config = ThingsConfig::new(&non_existent_path, false);
886
887 let result = config.get_effective_database_path();
888 assert!(
889 result.is_err(),
890 "Should fail when file doesn't exist and fallback is disabled"
891 );
892
893 let error_msg = result.unwrap_err().to_string();
894 assert!(
895 error_msg.contains("fallback is disabled"),
896 "Error message should mention fallback is disabled"
897 );
898 }
899
900 #[test]
901 fn test_config_effective_database_path_with_existing_file() {
902 let temp_file = NamedTempFile::new().unwrap();
904 let temp_path = temp_file.path().to_path_buf();
905
906 let config = ThingsConfig::new(&temp_path, false);
907 let effective_path = config.get_effective_database_path().unwrap();
908
909 assert_eq!(effective_path, temp_path);
910 }
911
912 #[test]
913 fn test_config_get_default_database_path_format() {
914 let path = ThingsConfig::get_default_database_path();
915 let path_str = path.to_string_lossy();
916
917 assert!(
918 path_str.contains("JLMPQHK86H.com.culturedcode.ThingsMac"),
919 "Should contain the correct container identifier"
920 );
921 assert!(
922 path_str.contains("ThingsData-"),
923 "Should contain a ThingsData-* data directory"
924 );
925 assert!(
926 path_str.contains("Things Database.thingsdatabase"),
927 "Should contain Things database directory"
928 );
929 assert!(
930 path_str.contains("main.sqlite"),
931 "Should contain main.sqlite file"
932 );
933 }
934
935 #[test]
936 fn test_config_with_different_path_types_comprehensive() {
937 let string_path = "/test/path/db.sqlite";
939 let config1 = ThingsConfig::new(string_path, false);
940 assert_eq!(config1.database_path, PathBuf::from(string_path));
941 assert!(!config1.fallback_to_default);
942
943 let pathbuf_path = PathBuf::from("/another/test/path.db");
945 let config2 = ThingsConfig::new(&pathbuf_path, true);
946 assert_eq!(config2.database_path, pathbuf_path);
947 assert!(config2.fallback_to_default);
948 }
949
950 #[test]
951 fn test_config_from_env_edge_cases() {
952 let test_cases = vec![
954 ("true", true),
955 ("TRUE", true),
956 ("True", true),
957 ("1", true),
958 ("yes", true),
959 ("YES", true),
960 ("on", true),
961 ("ON", true),
962 ("false", false),
963 ("FALSE", false),
964 ("0", false),
965 ("no", false),
966 ("off", false),
967 ("invalid", false),
968 ("", false),
969 ("random_string", false),
970 ];
971
972 for (value, expected) in test_cases {
973 let lower = value.to_lowercase();
975 let result = matches!(lower.as_str(), "true" | "1" | "yes" | "on");
976 assert_eq!(result, expected, "Failed for value: '{value}'");
977 }
978 }
979
980 #[test]
981 #[serial]
982 fn test_config_from_env_fallback_parsing_with_env_vars() {
983 let original_value = std::env::var("THINGS_FALLBACK_TO_DEFAULT").ok();
985
986 let test_cases = vec![
988 ("true", true),
989 ("false", false),
990 ("1", true),
991 ("0", false),
992 ("yes", true),
993 ("no", false),
994 ("invalid", false),
995 ];
996
997 for (value, expected) in test_cases {
998 std::env::remove_var("THINGS_FALLBACK_TO_DEFAULT");
1000
1001 std::env::set_var("THINGS_FALLBACK_TO_DEFAULT", value);
1003
1004 let env_value = std::env::var("THINGS_FALLBACK_TO_DEFAULT")
1006 .unwrap_or_else(|_| "NOT_SET".to_string());
1007 println!("Environment variable set to: '{env_value}'");
1008
1009 let env_value_check = std::env::var("THINGS_FALLBACK_TO_DEFAULT")
1011 .unwrap_or_else(|_| "NOT_SET".to_string());
1012 println!("Environment variable check before from_env: '{env_value_check}'");
1013
1014 let config = ThingsConfig::from_env();
1015
1016 println!(
1018 "Testing value: '{}', expected: {}, got: {}",
1019 value, expected, config.fallback_to_default
1020 );
1021
1022 assert_eq!(
1023 config.fallback_to_default, expected,
1024 "Failed for value: '{}', expected: {}, got: {}",
1025 value, expected, config.fallback_to_default
1026 );
1027 }
1028
1029 if let Some(original) = original_value {
1031 std::env::set_var("THINGS_FALLBACK_TO_DEFAULT", original);
1032 } else {
1033 std::env::remove_var("THINGS_FALLBACK_TO_DEFAULT");
1034 }
1035 }
1036
1037 #[test]
1038 #[serial]
1039 fn test_config_home_env_var_fallback() {
1040 let original_home = std::env::var("HOME").ok();
1043 let original_db_path = std::env::var("THINGS_DB_PATH").ok();
1044 let original_legacy = std::env::var("THINGS_DATABASE_PATH").ok();
1045
1046 std::env::remove_var("THINGS_DB_PATH");
1047 std::env::remove_var("THINGS_DATABASE_PATH");
1048 std::env::set_var("HOME", "/test/home");
1049
1050 let config = ThingsConfig::from_env();
1051 let contains_default = config
1052 .database_path
1053 .to_string_lossy()
1054 .contains("Things Database.thingsdatabase");
1055
1056 if let Some(home) = original_home {
1057 std::env::set_var("HOME", home);
1058 } else {
1059 std::env::remove_var("HOME");
1060 }
1061 if let Some(v) = original_db_path {
1062 std::env::set_var("THINGS_DB_PATH", v);
1063 }
1064 if let Some(v) = original_legacy {
1065 std::env::set_var("THINGS_DATABASE_PATH", v);
1066 }
1067
1068 assert!(contains_default);
1069 }
1070
1071 #[test]
1072 fn test_config_with_default_path() {
1073 let config = ThingsConfig::with_default_path();
1074 assert!(config
1075 .database_path
1076 .to_string_lossy()
1077 .contains("Things Database.thingsdatabase"));
1078 assert!(!config.fallback_to_default);
1079 }
1080}