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