use super::*;
#[test]
fn mixed_old_and_new_google_drive_source() {
let toml_str = r#"
[[content_sources.sources]]
source_type = "google_drive"
folder_id = "1aBcD_eFgHiJkLmNoPqRsTuVwXyZ"
service_account_key = "~/keys/sa.json"
connection_id = 42
watch = true
file_patterns = ["*.md", "*.txt"]
loop_back_enabled = false
poll_interval_seconds = 300
"#;
let config: Config = toml::from_str(toml_str).expect("valid TOML");
let source = &config.content_sources.sources[0];
assert_eq!(source.source_type, "google_drive");
assert_eq!(
source.service_account_key.as_deref(),
Some("~/keys/sa.json")
);
assert_eq!(source.connection_id, Some(42));
let toml_out = toml::to_string_pretty(&config).expect("serialize");
let roundtripped: Config = toml::from_str(&toml_out).expect("re-parse");
let rt_src = &roundtripped.content_sources.sources[0];
assert_eq!(rt_src.connection_id, Some(42));
assert_eq!(
rt_src.service_account_key.as_deref(),
Some("~/keys/sa.json")
);
let mut valid_config = config;
valid_config.business.product_name = "Test".to_string();
valid_config.business.product_keywords = vec!["test".to_string()];
valid_config.business.product_description = "A test product".to_string();
valid_config.business.industry_topics = vec!["testing".to_string()];
valid_config.llm.provider = "ollama".to_string();
valid_config.x_api.client_id = "test-id".to_string();
assert!(valid_config.validate().is_ok());
}
#[test]
fn legacy_local_fs_config_unaffected_by_deployment_mode() {
let mut config = valid_test_config();
config.deployment_mode = DeploymentMode::Desktop;
config.content_sources.sources.push(ContentSourceEntry {
source_type: "local_fs".to_string(),
path: Some("~/Obsidian/vault".to_string()),
folder_id: None,
service_account_key: None,
connection_id: None,
watch: true,
file_patterns: vec!["*.md".to_string()],
loop_back_enabled: true,
analytics_sync_enabled: false,
poll_interval_seconds: None,
enabled: None,
change_detection: "auto".to_string(),
});
assert!(config.validate().is_ok());
config.deployment_mode = DeploymentMode::SelfHost;
assert!(config.validate().is_ok());
}
#[test]
fn legacy_sa_key_only_config_still_valid() {
let mut config = valid_test_config();
config.content_sources.sources.push(ContentSourceEntry {
source_type: "google_drive".to_string(),
path: None,
folder_id: Some("abc123".to_string()),
service_account_key: Some("/keys/sa.json".to_string()),
connection_id: None,
watch: true,
file_patterns: vec!["*.md".to_string()],
loop_back_enabled: false,
analytics_sync_enabled: false,
poll_interval_seconds: Some(300),
enabled: None,
change_detection: "auto".to_string(),
});
for mode in &[
DeploymentMode::Desktop,
DeploymentMode::SelfHost,
DeploymentMode::Cloud,
] {
config.deployment_mode = mode.clone();
assert!(
config.validate().is_ok(),
"legacy SA-key config should be valid in {:?} mode",
mode
);
}
}
#[test]
fn empty_content_sources_valid() {
let mut config = valid_test_config();
config.content_sources.sources = vec![];
assert!(config.validate().is_ok());
}
#[test]
fn connection_id_without_sa_key_valid() {
let mut config = valid_test_config();
config.content_sources.sources.push(ContentSourceEntry {
source_type: "google_drive".to_string(),
path: None,
folder_id: Some("folder_xyz".to_string()),
service_account_key: None,
connection_id: Some(7),
watch: true,
file_patterns: vec!["*.md".to_string()],
loop_back_enabled: false,
analytics_sync_enabled: false,
poll_interval_seconds: None,
enabled: None,
change_detection: "auto".to_string(),
});
assert!(config.validate().is_ok());
}
#[test]
fn google_drive_source_no_auth_warns() {
let mut config = valid_test_config();
config.content_sources.sources.push(ContentSourceEntry {
source_type: "google_drive".to_string(),
path: None,
folder_id: Some("folder_abc".to_string()),
service_account_key: None,
connection_id: None,
watch: true,
file_patterns: vec!["*.md".to_string()],
loop_back_enabled: false,
analytics_sync_enabled: false,
poll_interval_seconds: None,
enabled: None,
change_detection: "auto".to_string(),
});
assert!(config.validate().is_ok());
}
#[test]
fn source_is_enabled_defaults_to_watch() {
let entry = ContentSourceEntry {
source_type: "local_fs".to_string(),
path: Some("~/vault".to_string()),
folder_id: None,
service_account_key: None,
connection_id: None,
watch: true,
file_patterns: vec![],
loop_back_enabled: false,
analytics_sync_enabled: false,
poll_interval_seconds: None,
enabled: None,
change_detection: "auto".to_string(),
};
assert!(entry.is_enabled());
let entry2 = ContentSourceEntry {
watch: false,
enabled: None,
..entry.clone()
};
assert!(!entry2.is_enabled());
}
#[test]
fn source_enabled_overrides_watch() {
let entry = ContentSourceEntry {
source_type: "local_fs".to_string(),
path: Some("~/vault".to_string()),
folder_id: None,
service_account_key: None,
connection_id: None,
watch: true,
file_patterns: vec![],
loop_back_enabled: false,
analytics_sync_enabled: false,
poll_interval_seconds: None,
enabled: Some(false),
change_detection: "auto".to_string(),
};
assert!(
!entry.is_enabled(),
"enabled=false should override watch=true"
);
}
#[test]
fn source_change_detection_defaults_auto() {
let toml_str = r#"
[x_api]
client_id = "test-id"
[business]
product_name = "Test"
product_keywords = ["test"]
[llm]
provider = "ollama"
model = "llama2"
[[content_sources.sources]]
source_type = "local_fs"
path = "~/vault"
"#;
let config: Config = toml::from_str(toml_str).expect("valid TOML");
let source = &config.content_sources.sources[0];
assert_eq!(source.change_detection, "auto");
assert!(source.is_enabled());
assert!(!source.is_poll_only());
assert!(!source.is_scan_only());
}
#[test]
fn source_change_detection_poll() {
let entry = ContentSourceEntry {
source_type: "local_fs".to_string(),
path: Some("~/vault".to_string()),
folder_id: None,
service_account_key: None,
connection_id: None,
watch: true,
file_patterns: vec![],
loop_back_enabled: false,
analytics_sync_enabled: false,
poll_interval_seconds: None,
enabled: None,
change_detection: "poll".to_string(),
};
assert!(entry.is_poll_only());
assert!(!entry.is_scan_only());
}
#[test]
fn source_change_detection_none_means_scan_only() {
let entry = ContentSourceEntry {
source_type: "local_fs".to_string(),
path: Some("~/vault".to_string()),
folder_id: None,
service_account_key: None,
connection_id: None,
watch: true,
file_patterns: vec![],
loop_back_enabled: false,
analytics_sync_enabled: false,
poll_interval_seconds: None,
enabled: None,
change_detection: "none".to_string(),
};
assert!(entry.is_scan_only());
}
#[test]
fn validate_invalid_change_detection() {
let mut config = Config::default();
config.business.product_name = "Test".to_string();
config.business.product_keywords = vec!["test".to_string()];
config.llm.provider = "ollama".to_string();
config.x_api.client_id = "test-id".to_string();
config.content_sources.sources.push(ContentSourceEntry {
source_type: "local_fs".to_string(),
path: Some("~/vault".to_string()),
folder_id: None,
service_account_key: None,
connection_id: None,
watch: true,
file_patterns: vec!["*.md".to_string()],
loop_back_enabled: false,
analytics_sync_enabled: false,
poll_interval_seconds: None,
enabled: None,
change_detection: "invalid_value".to_string(),
});
let result = config.validate();
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors.iter().any(|e| {
matches!(e, crate::error::ConfigError::InvalidValue { field, .. } if field.contains("change_detection"))
}));
}
#[test]
fn validate_poll_interval_too_low() {
let mut config = Config::default();
config.business.product_name = "Test".to_string();
config.business.product_keywords = vec!["test".to_string()];
config.llm.provider = "ollama".to_string();
config.x_api.client_id = "test-id".to_string();
config.content_sources.sources.push(ContentSourceEntry {
source_type: "local_fs".to_string(),
path: Some("~/vault".to_string()),
folder_id: None,
service_account_key: None,
connection_id: None,
watch: true,
file_patterns: vec!["*.md".to_string()],
loop_back_enabled: false,
analytics_sync_enabled: false,
poll_interval_seconds: Some(10),
enabled: None,
change_detection: "auto".to_string(),
});
let result = config.validate();
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors.iter().any(|e| {
matches!(e, crate::error::ConfigError::InvalidValue { field, .. } if field.contains("poll_interval_seconds"))
}));
}
#[test]
fn validate_enabled_local_fs_without_path() {
let mut config = Config::default();
config.business.product_name = "Test".to_string();
config.business.product_keywords = vec!["test".to_string()];
config.llm.provider = "ollama".to_string();
config.x_api.client_id = "test-id".to_string();
config.content_sources.sources.push(ContentSourceEntry {
source_type: "local_fs".to_string(),
path: None,
folder_id: None,
service_account_key: None,
connection_id: None,
watch: true,
file_patterns: vec!["*.md".to_string()],
loop_back_enabled: false,
analytics_sync_enabled: false,
poll_interval_seconds: None,
enabled: Some(true),
change_detection: "auto".to_string(),
});
let result = config.validate();
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors.iter().any(|e| {
matches!(e, crate::error::ConfigError::MissingField { field } if field.contains("path"))
}));
}
#[test]
fn validate_disabled_source_skips_path_check() {
let mut config = valid_test_config();
config.content_sources.sources.push(ContentSourceEntry {
source_type: "local_fs".to_string(),
path: None,
folder_id: None,
service_account_key: None,
connection_id: None,
watch: true,
file_patterns: vec!["*.md".to_string()],
loop_back_enabled: false,
analytics_sync_enabled: false,
poll_interval_seconds: None,
enabled: Some(false),
change_detection: "auto".to_string(),
});
assert!(config.validate().is_ok());
}
#[test]
fn source_legacy_watch_false_parses_as_disabled() {
let toml_str = r#"
[x_api]
client_id = "test-id"
[business]
product_name = "Test"
product_keywords = ["test"]
[llm]
provider = "ollama"
model = "llama2"
[[content_sources.sources]]
source_type = "local_fs"
path = "~/vault"
watch = false
"#;
let config: Config = toml::from_str(toml_str).expect("valid TOML");
let source = &config.content_sources.sources[0];
assert!(!source.watch);
assert!(source.enabled.is_none());
assert!(
!source.is_enabled(),
"watch=false with no enabled override should be disabled"
);
}