kittynode_core/application/
set_server_url.rs1use 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}