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]
69 pub fn get_default_database_path() -> PathBuf {
70 let home = std::env::var("HOME").unwrap_or_else(|_| "~".to_string());
71 PathBuf::from(format!(
72 "{home}/Library/Group Containers/JLMPQHK86H.com.culturedcode.ThingsMac/ThingsData-0Z0Z2/Things Database.thingsdatabase/main.sqlite"
73 ))
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();
679 let path_str = default_path.to_string_lossy();
680
681 assert!(path_str.contains("Library"));
683 assert!(path_str.contains("Group Containers"));
684 assert!(path_str.contains("JLMPQHK86H.com.culturedcode.ThingsMac"));
685 assert!(path_str.contains("ThingsData-0Z0Z2"));
686 assert!(path_str.contains("Things Database.thingsdatabase"));
687 assert!(path_str.contains("main.sqlite"));
688 }
689
690 #[test]
691 fn test_home_env_var_fallback() {
692 let default_path = ThingsConfig::get_default_database_path();
695 let path_str = default_path.to_string_lossy();
696
697 assert!(path_str.starts_with('/') || path_str.starts_with('~'));
699 }
700
701 #[test]
702 fn test_config_effective_database_path_existing_file() {
703 let temp_dir = std::env::temp_dir();
705 let temp_file = temp_dir.join("test_db.sqlite");
706 std::fs::File::create(&temp_file).unwrap();
707
708 let config = ThingsConfig::new(temp_file.clone(), false);
709 let effective_path = config.get_effective_database_path().unwrap();
710 assert_eq!(effective_path, temp_file);
711
712 std::fs::remove_file(&temp_file).unwrap();
714 }
715
716 #[test]
717 fn test_config_effective_database_path_fallback_success() {
718 let temp_dir = std::env::temp_dir();
720 let temp_file = temp_dir.join("test_database.sqlite");
721 std::fs::File::create(&temp_file).unwrap();
722
723 let config = ThingsConfig::new(temp_file.clone(), true);
725
726 let effective_path = config.get_effective_database_path().unwrap();
727
728 assert_eq!(effective_path, temp_file);
730
731 std::fs::remove_file(&temp_file).unwrap();
733 }
734
735 #[test]
736 fn test_config_effective_database_path_fallback_disabled_error_message() {
737 let non_existent_path = PathBuf::from("/nonexistent/path/db.sqlite");
738 let config = ThingsConfig::new(non_existent_path, false);
739
740 let result = config.get_effective_database_path();
742 assert!(result.is_err());
743 let error = result.unwrap_err();
744 assert!(matches!(error, ThingsError::Configuration { .. }));
745 }
746
747 #[test]
748 #[serial]
749 fn test_config_effective_database_path_fallback_enabled_but_default_missing() {
750 let original_home = std::env::var("HOME").ok();
752 std::env::set_var("HOME", "/nonexistent/home");
753
754 let non_existent_path = PathBuf::from("/nonexistent/path/db.sqlite");
756 let config = ThingsConfig::new(non_existent_path, true);
757
758 let result = config.get_effective_database_path();
760
761 if let Some(home) = original_home {
763 std::env::set_var("HOME", home);
764 } else {
765 std::env::remove_var("HOME");
766 }
767
768 assert!(
769 result.is_err(),
770 "Expected error when both configured and default paths don't exist"
771 );
772 let error = result.unwrap_err();
773 assert!(matches!(error, ThingsError::Configuration { .. }));
774
775 let error_message = format!("{error}");
777 assert!(error_message.contains("Database not found at /nonexistent/path/db.sqlite"));
778 assert!(error_message.contains("fallback is enabled but default path also not found"));
779 }
780
781 #[test]
782 fn test_config_fallback_behavior() {
783 let path = PathBuf::from("/test/path/db.sqlite");
784
785 let config_with_fallback = ThingsConfig::new(path.clone(), true);
787 assert!(config_with_fallback.fallback_to_default);
788
789 let config_without_fallback = ThingsConfig::new(path, false);
791 assert!(!config_without_fallback.fallback_to_default);
792 }
793
794 #[test]
795 fn test_config_fallback_disabled() {
796 let path = PathBuf::from("/test/path/db.sqlite");
797 let config = ThingsConfig::new(path, false);
798 assert!(!config.fallback_to_default);
799 }
800
801 #[test]
802 #[serial]
803 fn test_config_from_env_without_variables() {
804 let original_db_path = std::env::var("THINGS_DB_PATH").ok();
805 let original_legacy = std::env::var("THINGS_DATABASE_PATH").ok();
806 let original_fallback = std::env::var("THINGS_FALLBACK_TO_DEFAULT").ok();
807
808 std::env::remove_var("THINGS_DB_PATH");
809 std::env::remove_var("THINGS_DATABASE_PATH");
810 std::env::remove_var("THINGS_FALLBACK_TO_DEFAULT");
811
812 let config = ThingsConfig::from_env();
813 let contains_default = config
814 .database_path
815 .to_string_lossy()
816 .contains("Things Database.thingsdatabase");
817
818 if let Some(v) = original_db_path {
819 std::env::set_var("THINGS_DB_PATH", v);
820 }
821 if let Some(v) = original_legacy {
822 std::env::set_var("THINGS_DATABASE_PATH", v);
823 }
824 if let Some(v) = original_fallback {
825 std::env::set_var("THINGS_FALLBACK_TO_DEFAULT", v);
826 }
827
828 assert!(contains_default);
829 }
830
831 #[test]
832 fn test_config_from_env_fallback_parsing() {
833 let test_cases = vec![
837 ("true", true),
838 ("false", false),
839 ("1", true),
840 ("0", false),
841 ("yes", true),
842 ("no", false),
843 ("invalid", false),
844 ];
845
846 for (value, expected) in test_cases {
847 let lower = value.to_lowercase();
849 let result = match lower.as_str() {
850 "true" | "1" | "yes" | "on" => true,
851 _ => false, };
853
854 assert_eq!(
855 result, expected,
856 "Failed for value: '{value}', expected: {expected}, got: {result}"
857 );
858 }
859 }
860
861 #[test]
862 fn test_config_for_testing() {
863 let result = ThingsConfig::for_testing();
864 assert!(result.is_ok(), "Should create test config successfully");
865
866 let config = result.unwrap();
867 assert!(
868 !config.fallback_to_default,
869 "Test config should have fallback disabled"
870 );
871
872 let path_str = config.database_path.to_string_lossy();
874 assert!(
875 path_str.contains("tmp") || !path_str.is_empty(),
876 "Test config should use a temporary path"
877 );
878 }
879
880 #[test]
881 fn test_config_effective_database_path_error_cases() {
882 let non_existent_path = PathBuf::from("/absolutely/non/existent/path/database.db");
884 let config = ThingsConfig::new(&non_existent_path, false);
885
886 let result = config.get_effective_database_path();
887 assert!(
888 result.is_err(),
889 "Should fail when file doesn't exist and fallback is disabled"
890 );
891
892 let error_msg = result.unwrap_err().to_string();
893 assert!(
894 error_msg.contains("fallback is disabled"),
895 "Error message should mention fallback is disabled"
896 );
897 }
898
899 #[test]
900 fn test_config_effective_database_path_with_existing_file() {
901 let temp_file = NamedTempFile::new().unwrap();
903 let temp_path = temp_file.path().to_path_buf();
904
905 let config = ThingsConfig::new(&temp_path, false);
906 let effective_path = config.get_effective_database_path().unwrap();
907
908 assert_eq!(effective_path, temp_path);
909 }
910
911 #[test]
912 fn test_config_get_default_database_path_format() {
913 let path = ThingsConfig::get_default_database_path();
914 let path_str = path.to_string_lossy();
915
916 assert!(
918 path_str.contains("JLMPQHK86H.com.culturedcode.ThingsMac"),
919 "Should contain the correct container identifier"
920 );
921 assert!(
922 path_str.contains("ThingsData-0Z0Z2"),
923 "Should contain the correct 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}