kittynode_core/application/
set_server_url.rs

1use crate::domain::config::Config;
2use crate::infra::config::ConfigStore;
3use eyre::{Result, eyre};
4use url::Url;
5
6fn validate_server_url(endpoint: &str) -> Result<()> {
7    if endpoint.is_empty() {
8        return Ok(());
9    }
10
11    let parsed = Url::parse(endpoint).map_err(|e| eyre!("invalid server URL '{endpoint}': {e}"))?;
12
13    match parsed.scheme() {
14        "http" | "https" => {}
15        other => {
16            return Err(eyre!(
17                "invalid server URL '{endpoint}': unsupported scheme '{other}' (expected http or https)"
18            ));
19        }
20    }
21
22    if parsed.host_str().is_none() {
23        return Err(eyre!("invalid server URL '{endpoint}': missing host"));
24    }
25
26    if !parsed.username().is_empty() || parsed.password().is_some() {
27        return Err(eyre!(
28            "invalid server URL '{endpoint}': credentials are not supported"
29        ));
30    }
31
32    Ok(())
33}
34
35fn apply_server_url(config: &mut Config, endpoint: &str) -> Result<()> {
36    let trimmed = endpoint.trim();
37
38    validate_server_url(trimmed)?;
39
40    if trimmed.is_empty() {
41        config.server_url.clear();
42        config.has_remote_server = false;
43    } else {
44        let normalized = trimmed.to_string();
45        config.server_url = normalized.clone();
46        config.last_server_url = normalized;
47        config.has_remote_server = true;
48    }
49
50    Ok(())
51}
52
53pub fn set_server_url(endpoint: String) -> Result<()> {
54    let mut config = ConfigStore::load()?;
55    apply_server_url(&mut config, &endpoint)?;
56    ConfigStore::save_normalized(&mut config)?;
57    Ok(())
58}
59
60#[cfg(test)]
61mod tests {
62    use super::{apply_server_url, validate_server_url};
63    use crate::domain::config::Config;
64
65    #[test]
66    fn validate_allows_empty_endpoint() {
67        assert!(validate_server_url("").is_ok());
68    }
69
70    #[test]
71    fn validate_rejects_invalid_scheme() {
72        let err =
73            validate_server_url("ftp://example.com").expect_err("expected validation failure");
74        assert!(err.to_string().contains("unsupported scheme"));
75    }
76
77    #[test]
78    fn apply_sets_server_url_and_last() {
79        let mut config = Config::default();
80        apply_server_url(&mut config, " http://example.com ").expect("apply should succeed");
81
82        assert_eq!(config.server_url, "http://example.com");
83        assert_eq!(config.last_server_url, "http://example.com");
84        assert!(config.has_remote_server);
85    }
86
87    #[test]
88    fn apply_clears_server_but_preserves_last() {
89        let mut config = Config::default();
90        apply_server_url(&mut config, "http://example.com").expect("initial apply should succeed");
91        apply_server_url(&mut config, "").expect("clearing should succeed");
92
93        assert_eq!(config.server_url, "");
94        assert_eq!(config.last_server_url, "http://example.com");
95        assert!(!config.has_remote_server);
96    }
97
98    #[test]
99    fn apply_preserves_trailing_slash() {
100        let mut config = Config::default();
101        apply_server_url(&mut config, "https://example.com/ ").expect("apply should succeed");
102
103        assert_eq!(config.server_url, "https://example.com/");
104        assert_eq!(config.last_server_url, "https://example.com/");
105        assert!(config.has_remote_server);
106    }
107
108    #[test]
109    fn apply_does_not_mutate_on_validation_error() {
110        let mut config = Config::default();
111        let _err =
112            apply_server_url(&mut config, "not a url").expect_err("expected validation failure");
113
114        assert_eq!(config.server_url, "");
115        assert_eq!(config.last_server_url, "");
116        assert!(!config.has_remote_server);
117    }
118}