From f50cde28afb86fd6613843c5630d66b96cab59a3 Mon Sep 17 00:00:00 2001 From: Yagami Light Date: Mon, 25 May 2026 17:09:14 +0800 Subject: [PATCH 1/2] fix: set-connection will also fail if the config file does not exist --- scopeql/src/config.rs | 113 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 108 insertions(+), 5 deletions(-) diff --git a/scopeql/src/config.rs b/scopeql/src/config.rs index d106180..cb6d903 100644 --- a/scopeql/src/config.rs +++ b/scopeql/src/config.rs @@ -344,17 +344,48 @@ fn do_use_connection(name: &str) -> Result<(), Error> { } pub(crate) fn set_connection(name: String) { - do_set_connection(name).unwrap_or_else(|err| { + set_connection_impl(name, do_set_connection) +} + +fn set_connection_impl(name: String, do_fn: F) +where + F: FnOnce(String, PathBuf, DocumentMut) -> Result<(), Error>, +{ + let (path, doc) = load_document().unwrap_or_else(|_err| { + let path = candidate_config_paths() + .into_iter() + .next() + .expect("no candidate config paths"); + + println!("Creating new config file at {}", path.display()); + + let parent = path.parent().unwrap(); + if let Err(err) = std::fs::create_dir_all(parent) { + eprintln!( + "failed to create config directory {}: {err}", + parent.display() + ); + std::process::exit(1); + } + + let mut doc = DocumentMut::new(); + doc["default_connection"] = toml_edit::value(&name); + (path, doc) + }); + do_fn(name, path, doc).unwrap_or_else(|err| { eprintln!("Failed to set connection: {err}"); std::process::exit(1); }); } -fn do_set_connection(name: String) -> Result<(), Error> { - let (path, mut doc) = load_document()?; +fn do_set_connection(name: String, path: PathBuf, doc: DocumentMut) -> Result<(), Error> { let mut config = deserialize_toml(&path, doc.clone())?; + let conn = prompt_connection_spec(&mut config, &name); + do_set_connection_with_spec(name, path, doc, conn) +} - let conn = if let Some(conn) = config.connections.get_mut(&name) { +fn prompt_connection_spec(config: &mut Config, name: &str) -> ConnectionSpec { + if let Some(conn) = config.connections.get_mut(name) { if Confirm::new() .with_prompt("Change endpoint?") .default(false) @@ -384,8 +415,15 @@ fn do_set_connection(name: String) -> Result<(), Error> { headers: vec![], auth, } - }; + } +} +pub(crate) fn do_set_connection_with_spec( + name: String, + path: PathBuf, + mut doc: DocumentMut, + conn: ConnectionSpec, +) -> Result<(), Error> { write_connection(&mut doc, &name, &conn); std::fs::write(&path, doc.to_string()).map_err(|err| { @@ -691,4 +729,69 @@ headers = ["X-Tenant: acme"] ["X-Tenant: acme", "X-Trace: demo"] ); } + + #[test] + fn set_connection_creates_new_file_when_no_config_exists() { + let candidates = candidate_config_paths(); + + // Backup existing config files by renaming them + let mut backups: Vec<(PathBuf, Option)> = Vec::new(); + for path in &candidates { + if path.exists() { + let backup = path.with_extension("toml.bak"); + let _ = std::fs::remove_file(&backup); + std::fs::rename(path, &backup).unwrap(); + backups.push((path.clone(), Some(backup))); + } else { + backups.push((path.clone(), None)); + } + } + + // Verify no config files remain + for path in &candidates { + assert!( + !path.exists(), + "candidate {path:?} still exists after moving" + ); + } + + let expected_first_path = candidates.first().cloned().unwrap(); + let conn_name = "test-conn".to_string(); + + // Invoke set_connection_impl with a mocked ConnectionSpec to avoid + // interactive dialoguer prompts. Lines 384-414 (prompt_connection_spec) + // are bypassed; the real file-writing path (do_set_connection_with_spec) + // is exercised. + set_connection_impl(conn_name.clone(), |name, path, doc| { + let conn = ConnectionSpec { + endpoint: DEFAULT_URL.to_string(), + headers: vec![], + auth: ConnectionAuthSpec::Direct, + }; + do_set_connection_with_spec(name, path, doc, conn) + }); + + // Verify the config file was written with the correct content + let content = std::fs::read_to_string(&expected_first_path).unwrap(); + let config: Config = toml::from_str(&content).unwrap(); + + assert_eq!(config.default_connection, conn_name); + let written_conn = config.get_connection(&conn_name).unwrap(); + assert_eq!(written_conn.endpoint(), DEFAULT_URL); + assert_matches!(written_conn.auth(), ConnectionAuthSpec::Direct); + + // Clean up the created file and directory + std::fs::remove_file(&expected_first_path).unwrap(); + let parent = expected_first_path.parent().unwrap(); + if parent.exists() && parent.read_dir().unwrap().next().is_none() { + let _ = std::fs::remove_dir(parent); + } + + // Restore original config files + for (original, backup) in backups { + if let Some(backup) = backup { + std::fs::rename(&backup, &original).unwrap(); + } + } + } } From b37feeec6dc2357aa71d7836d8d7bcfeb804e1aa Mon Sep 17 00:00:00 2001 From: Yagami Light Date: Mon, 25 May 2026 17:14:00 +0800 Subject: [PATCH 2/2] test a custom endpoint --- scopeql/src/config.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scopeql/src/config.rs b/scopeql/src/config.rs index cb6d903..282bb99 100644 --- a/scopeql/src/config.rs +++ b/scopeql/src/config.rs @@ -757,6 +757,7 @@ headers = ["X-Tenant: acme"] let expected_first_path = candidates.first().cloned().unwrap(); let conn_name = "test-conn".to_string(); + let conn_endpoint = "https://example.scopedb.com:9876".to_string(); // Invoke set_connection_impl with a mocked ConnectionSpec to avoid // interactive dialoguer prompts. Lines 384-414 (prompt_connection_spec) @@ -764,7 +765,7 @@ headers = ["X-Tenant: acme"] // is exercised. set_connection_impl(conn_name.clone(), |name, path, doc| { let conn = ConnectionSpec { - endpoint: DEFAULT_URL.to_string(), + endpoint: conn_endpoint.clone(), headers: vec![], auth: ConnectionAuthSpec::Direct, }; @@ -777,7 +778,7 @@ headers = ["X-Tenant: acme"] assert_eq!(config.default_connection, conn_name); let written_conn = config.get_connection(&conn_name).unwrap(); - assert_eq!(written_conn.endpoint(), DEFAULT_URL); + assert_eq!(written_conn.endpoint(), &conn_endpoint); assert_matches!(written_conn.auth(), ConnectionAuthSpec::Direct); // Clean up the created file and directory