use crate::error::{Error, Result};
use std::collections::HashMap;
pub const DEFAULT_PORT: u16 = 3141;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Transport {
Quic,
Grpc,
}
impl std::fmt::Display for Transport {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Transport::Quic => write!(f, "quic"),
Transport::Grpc => write!(f, "grpc"),
}
}
}
#[derive(Clone)]
pub struct Dsn {
transport: Transport,
host: String,
port: u16,
username: Option<String>,
password: Option<String>,
tls_enabled: bool,
skip_verify: bool,
page_size: usize,
client_name: String,
client_version: String,
conformance: String,
ca_cert: Option<String>,
client_cert: Option<String>,
client_key: Option<String>,
server_name: Option<String>,
connect_timeout_secs: Option<u64>,
graph: Option<String>,
tenant_id: Option<String>,
role: Option<String>,
options: HashMap<String, String>,
}
impl Default for Dsn {
fn default() -> Self {
Self {
transport: Transport::Quic,
host: "localhost".to_string(),
port: DEFAULT_PORT,
username: None,
password: None,
tls_enabled: true,
skip_verify: false,
page_size: 1000,
client_name: "geode-rust".to_string(),
client_version: env!("CARGO_PKG_VERSION").to_string(),
conformance: "min".to_string(),
ca_cert: None,
client_cert: None,
client_key: None,
server_name: None,
connect_timeout_secs: None,
graph: None,
tenant_id: None,
role: None,
options: HashMap::new(),
}
}
}
impl Dsn {
pub fn parse(dsn: &str) -> Result<Self> {
let dsn = dsn.trim();
if dsn.is_empty() {
return Err(Error::invalid_dsn("DSN cannot be empty"));
}
if dsn.starts_with("quic://") {
Self::parse_url(dsn, Transport::Quic)
} else if let Some(rest) = dsn.strip_prefix("grpcs://") {
let rewritten = format!("grpc://{}", rest);
let mut result = Self::parse_url(&rewritten, Transport::Grpc)?;
result.tls_enabled = true;
Ok(result)
} else if dsn.starts_with("grpc://") {
Self::parse_url(dsn, Transport::Grpc)
} else if dsn.contains("://") {
let scheme = dsn.split("://").next().unwrap_or("");
Err(Error::invalid_dsn(format!(
"Unsupported scheme '{}'. Supported schemes: quic://, grpc://, grpcs://",
scheme
)))
} else {
Self::parse_legacy(dsn)
}
}
fn parse_url(dsn: &str, transport: Transport) -> Result<Self> {
let url = url::Url::parse(dsn)
.map_err(|e| Error::invalid_dsn(format!("Invalid URL format: {}", e)))?;
let host_raw = url
.host_str()
.ok_or_else(|| Error::invalid_dsn("Host is required"))?;
let host = if host_raw.starts_with('[') && host_raw.ends_with(']') {
host_raw[1..host_raw.len() - 1].to_string()
} else {
host_raw.to_string()
};
if host.is_empty() {
return Err(Error::invalid_dsn("Host is required"));
}
let port = url.port().unwrap_or(DEFAULT_PORT);
let username = if !url.username().is_empty() {
Some(
urlencoding::decode(url.username())
.map_err(|e| Error::invalid_dsn(format!("Invalid username encoding: {}", e)))?
.into_owned(),
)
} else {
None
};
let password = url.password().map(|p| {
urlencoding::decode(p)
.map(|s| s.into_owned())
.unwrap_or_else(|_| p.to_string())
});
let params: HashMap<String, String> = url.query_pairs().into_owned().collect();
let graph = if !url.path().is_empty() && url.path() != "/" {
Some(url.path().trim_start_matches('/').to_string())
} else {
None
};
let mut result = Self {
transport,
host,
port,
username,
password,
graph,
..Default::default()
};
result.apply_params(¶ms)?;
Ok(result)
}
fn parse_legacy(dsn: &str) -> Result<Self> {
let (host_port, query_str) = if let Some(idx) = dsn.find('?') {
(&dsn[..idx], Some(&dsn[idx + 1..]))
} else {
(dsn, None)
};
let (host, port) = Self::parse_host_port(host_port)?;
if host.is_empty() {
return Err(Error::invalid_dsn("Host is required"));
}
let mut result = Self {
transport: Transport::Quic, host,
port,
..Default::default()
};
if let Some(qs) = query_str {
let params: HashMap<String, String> = qs
.split('&')
.filter_map(|pair| {
let mut parts = pair.splitn(2, '=');
let key = parts.next()?;
let value = parts.next().unwrap_or("");
let decoded_value = urlencoding::decode(value)
.map(|s| s.into_owned())
.unwrap_or_else(|_| value.to_string());
Some((key.to_string(), decoded_value))
})
.collect();
result.apply_params(¶ms)?;
}
Ok(result)
}
fn parse_host_port(s: &str) -> Result<(String, u16)> {
if s.starts_with('[') {
if let Some(bracket_end) = s.find(']') {
let host = s[1..bracket_end].to_string();
let remainder = &s[bracket_end + 1..];
let port = if let Some(rest) = remainder.strip_prefix(':') {
rest.parse::<u16>()
.map_err(|_| Error::invalid_dsn(format!("Invalid port: {}", rest)))?
} else if remainder.is_empty() {
DEFAULT_PORT
} else {
return Err(Error::invalid_dsn("Invalid IPv6 address format"));
};
return Ok((host, port));
} else {
return Err(Error::invalid_dsn("Unclosed bracket in IPv6 address"));
}
}
if let Some(idx) = s.rfind(':') {
let host = s[..idx].to_string();
let port_str = &s[idx + 1..];
let port = port_str
.parse::<u16>()
.map_err(|_| Error::invalid_dsn(format!("Invalid port: {}", port_str)))?;
Ok((host, port))
} else {
Ok((s.to_string(), DEFAULT_PORT))
}
}
fn apply_params(&mut self, params: &HashMap<String, String>) -> Result<()> {
for (key, value) in params {
match key.as_str() {
"tls" => {
self.tls_enabled = parse_bool(value).unwrap_or(true);
}
"insecure_tls_skip_verify"
| "insecure"
| "skip_verify"
| "insecure_skip_verify" => {
self.skip_verify = parse_bool(value).unwrap_or(false);
}
"page_size" => {
self.page_size = value
.parse()
.map_err(|_| Error::invalid_dsn(format!("Invalid page_size: {}", value)))?;
}
"client_name" | "hello_name" => {
self.client_name = value.clone();
}
"client_version" | "hello_ver" => {
self.client_version = value.clone();
}
"conformance" => {
self.conformance = value.clone();
}
"username" | "user" => {
self.username = Some(value.clone());
}
"password" | "pass" => {
self.password = Some(value.clone());
}
"ca" | "ca_cert" => {
self.ca_cert = Some(value.clone());
}
"cert" | "client_cert" => {
self.client_cert = Some(value.clone());
}
"key" | "client_key" => {
self.client_key = Some(value.clone());
}
"server_name" => {
self.server_name = Some(value.clone());
}
"connect_timeout" | "timeout" => {
self.connect_timeout_secs = value.parse().ok();
}
"graph" => {
self.graph = Some(value.to_string());
}
"tenant_id" | "tenant" => {
self.tenant_id = Some(value.clone());
}
"role" => {
self.role = Some(value.clone());
}
_ => {
self.options.insert(key.clone(), value.clone());
}
}
}
Ok(())
}
pub fn transport(&self) -> Transport {
self.transport
}
pub fn host(&self) -> &str {
&self.host
}
pub fn port(&self) -> u16 {
self.port
}
pub fn username(&self) -> Option<&str> {
self.username.as_deref()
}
pub fn password(&self) -> Option<&str> {
self.password.as_deref()
}
pub fn tls_enabled(&self) -> bool {
self.tls_enabled
}
pub fn skip_verify(&self) -> bool {
self.skip_verify
}
pub fn page_size(&self) -> usize {
self.page_size
}
pub fn client_name(&self) -> &str {
&self.client_name
}
pub fn client_version(&self) -> &str {
&self.client_version
}
pub fn conformance(&self) -> &str {
&self.conformance
}
pub fn options(&self) -> &HashMap<String, String> {
&self.options
}
pub fn ca_cert(&self) -> Option<&str> {
self.ca_cert.as_deref()
}
pub fn client_cert(&self) -> Option<&str> {
self.client_cert.as_deref()
}
pub fn client_key(&self) -> Option<&str> {
self.client_key.as_deref()
}
pub fn server_name(&self) -> Option<&str> {
self.server_name.as_deref()
}
pub fn connect_timeout_secs(&self) -> Option<u64> {
self.connect_timeout_secs
}
pub fn graph(&self) -> Option<&str> {
self.graph.as_deref()
}
pub fn tenant_id(&self) -> Option<&str> {
self.tenant_id.as_deref()
}
pub fn role(&self) -> Option<&str> {
self.role.as_deref()
}
pub fn address(&self) -> String {
if self.host.contains(':') {
format!("[{}]:{}", self.host, self.port)
} else {
format!("{}:{}", self.host, self.port)
}
}
}
impl std::fmt::Display for Dsn {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut params: Vec<(String, String)> = Vec::new();
if self.page_size != 1000 {
params.push(("page_size".into(), self.page_size.to_string()));
}
if self.client_name != "geode-rust" {
params.push(("client_name".into(), self.client_name.clone()));
}
if self.client_version != env!("CARGO_PKG_VERSION") {
params.push(("client_version".into(), self.client_version.clone()));
}
if self.conformance != "min" {
params.push(("conformance".into(), self.conformance.clone()));
}
if let Some(ca) = &self.ca_cert {
params.push(("ca".into(), ca.clone()));
}
if let Some(cert) = &self.client_cert {
params.push(("cert".into(), cert.clone()));
}
if let Some(key) = &self.client_key {
params.push(("key".into(), key.clone()));
}
if self.skip_verify {
params.push(("insecure_tls_skip_verify".into(), "true".into()));
}
if !self.tls_enabled {
params.push(("tls".into(), "0".into()));
}
if let Some(graph) = &self.graph {
params.push(("graph".into(), graph.clone()));
}
if let Some(tenant) = &self.tenant_id {
params.push(("tenant".into(), tenant.clone()));
}
if let Some(role) = &self.role {
params.push(("role".into(), role.clone()));
}
if let Some(sn) = &self.server_name {
params.push(("server_name".into(), sn.clone()));
}
if let Some(t) = self.connect_timeout_secs {
params.push(("connect_timeout".into(), t.to_string()));
}
let userinfo = match (&self.username, &self.password) {
(Some(u), Some(p)) => format!("{}:{}@", urlencoding::encode(u), urlencoding::encode(p)),
(Some(u), None) => format!("{}@", urlencoding::encode(u)),
(None, Some(p)) => {
params.push(("pass".into(), p.clone()));
String::new()
}
(None, None) => String::new(),
};
params.sort_by(|a, b| a.0.cmp(&b.0));
let host = if self.host.contains(':') {
format!("[{}]:{}", self.host, self.port)
} else {
format!("{}:{}", self.host, self.port)
};
write!(f, "{}://{}{}", self.transport, userinfo, host)?;
if !params.is_empty() {
let query: Vec<String> = params
.iter()
.map(|(k, v)| format!("{}={}", k, urlencoding::encode(v)))
.collect();
write!(f, "?{}", query.join("&"))?;
}
Ok(())
}
}
impl std::fmt::Debug for Dsn {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Dsn")
.field("transport", &self.transport)
.field("host", &self.host)
.field("port", &self.port)
.field("username", &self.username)
.field("password", &self.password.as_ref().map(|_| "[REDACTED]"))
.field("tls_enabled", &self.tls_enabled)
.field("skip_verify", &self.skip_verify)
.field("page_size", &self.page_size)
.field("client_name", &self.client_name)
.field("client_version", &self.client_version)
.field("conformance", &self.conformance)
.field("ca_cert", &self.ca_cert)
.field("client_cert", &self.client_cert)
.field("client_key", &self.client_key)
.field("server_name", &self.server_name)
.field("connect_timeout_secs", &self.connect_timeout_secs)
.field("graph", &self.graph)
.field("tenant_id", &self.tenant_id)
.field("role", &self.role)
.field("options", &self.options)
.finish()
}
}
fn parse_bool(s: &str) -> Option<bool> {
match s.to_lowercase().as_str() {
"true" | "1" | "yes" | "on" => Some(true),
"false" | "0" | "no" | "off" => Some(false),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_dsn_parse_quic_basic() {
let dsn = Dsn::parse("quic://localhost:3141").unwrap();
assert_eq!(dsn.transport(), Transport::Quic);
assert_eq!(dsn.host(), "localhost");
assert_eq!(dsn.port(), 3141);
assert!(dsn.tls_enabled());
assert!(!dsn.skip_verify());
}
#[test]
fn test_dsn_parse_quic_with_ip() {
let dsn = Dsn::parse("quic://127.0.0.1:3141").unwrap();
assert_eq!(dsn.transport(), Transport::Quic);
assert_eq!(dsn.host(), "127.0.0.1");
assert_eq!(dsn.port(), 3141);
}
#[test]
fn test_dsn_parse_quic_default_port() {
let dsn = Dsn::parse("quic://localhost").unwrap();
assert_eq!(dsn.port(), DEFAULT_PORT);
}
#[test]
fn test_dsn_parse_quic_with_options() {
let dsn = Dsn::parse("quic://localhost:3141?page_size=500&insecure=true").unwrap();
assert_eq!(dsn.page_size(), 500);
assert!(dsn.skip_verify());
}
#[test]
fn test_dsn_parse_grpc_basic() {
let dsn = Dsn::parse("grpc://localhost:50051").unwrap();
assert_eq!(dsn.transport(), Transport::Grpc);
assert_eq!(dsn.host(), "localhost");
assert_eq!(dsn.port(), 50051);
}
#[test]
fn test_dsn_parse_grpc_with_tls_disabled() {
let dsn = Dsn::parse("grpc://127.0.0.1:50051?tls=0").unwrap();
assert_eq!(dsn.transport(), Transport::Grpc);
assert_eq!(dsn.host(), "127.0.0.1");
assert_eq!(dsn.port(), 50051);
assert!(!dsn.tls_enabled());
}
#[test]
fn test_dsn_parse_grpc_with_tls_false() {
let dsn = Dsn::parse("grpc://localhost:50051?tls=false").unwrap();
assert!(!dsn.tls_enabled());
}
#[test]
fn test_dsn_parse_grpcs_scheme() {
let dsn = Dsn::parse("grpcs://localhost:50051").unwrap();
assert_eq!(dsn.transport(), Transport::Grpc);
assert!(dsn.tls_enabled());
assert_eq!(dsn.host(), "localhost");
assert_eq!(dsn.port(), 50051);
}
#[test]
fn test_dsn_parse_grpcs_tls_forced() {
let dsn = Dsn::parse("grpcs://localhost:50051?tls=0").unwrap();
assert_eq!(dsn.transport(), Transport::Grpc);
assert!(dsn.tls_enabled());
}
#[test]
fn test_dsn_parse_unsupported_schemes() {
let err = Dsn::parse("grcp://localhost:50051").unwrap_err();
assert!(err.to_string().contains("Unsupported scheme"));
let err = Dsn::parse("geode://localhost:3141").unwrap_err();
assert!(err.to_string().contains("Unsupported scheme"));
}
#[test]
fn test_dsn_parse_ipv6_grpc() {
let dsn = Dsn::parse("grpc://[::1]:50051").unwrap();
assert_eq!(dsn.transport(), Transport::Grpc);
assert_eq!(dsn.host(), "::1");
assert_eq!(dsn.port(), 50051);
}
#[test]
fn test_dsn_parse_ipv6_quic() {
let dsn = Dsn::parse("quic://[::1]:3141").unwrap();
assert_eq!(dsn.transport(), Transport::Quic);
assert_eq!(dsn.host(), "::1");
assert_eq!(dsn.port(), 3141);
}
#[test]
fn test_dsn_parse_ipv6_full_address() {
let dsn = Dsn::parse("grpc://[2001:db8::1]:50051").unwrap();
assert_eq!(dsn.host(), "2001:db8::1");
assert_eq!(dsn.port(), 50051);
}
#[test]
fn test_dsn_parse_ipv6_default_port() {
let dsn = Dsn::parse("quic://[::1]").unwrap();
assert_eq!(dsn.host(), "::1");
assert_eq!(dsn.port(), DEFAULT_PORT);
}
#[test]
fn test_dsn_address_ipv6() {
let dsn = Dsn::parse("grpc://[::1]:50051").unwrap();
assert_eq!(dsn.address(), "[::1]:50051");
}
#[test]
fn test_dsn_parse_legacy_host_port() {
let dsn = Dsn::parse("localhost:3141").unwrap();
assert_eq!(dsn.transport(), Transport::Quic); assert_eq!(dsn.host(), "localhost");
assert_eq!(dsn.port(), 3141);
}
#[test]
fn test_dsn_parse_legacy_with_options() {
let dsn = Dsn::parse("localhost:3141?insecure=true&page_size=500").unwrap();
assert_eq!(dsn.transport(), Transport::Quic);
assert!(dsn.skip_verify());
assert_eq!(dsn.page_size(), 500);
}
#[test]
fn test_dsn_parse_legacy_ipv6() {
let dsn = Dsn::parse("[::1]:3141").unwrap();
assert_eq!(dsn.host(), "::1");
assert_eq!(dsn.port(), 3141);
}
#[test]
fn test_dsn_parse_with_auth() {
let dsn = Dsn::parse("quic://admin:secret@localhost:3141").unwrap();
assert_eq!(dsn.username(), Some("admin"));
assert_eq!(dsn.password(), Some("secret"));
}
#[test]
fn test_dsn_parse_auth_via_query_params() {
let dsn = Dsn::parse("grpc://localhost:50051?username=admin&password=secret").unwrap();
assert_eq!(dsn.username(), Some("admin"));
assert_eq!(dsn.password(), Some("secret"));
}
#[test]
fn test_dsn_parse_auth_percent_encoded() {
let dsn = Dsn::parse("quic://user%40domain:p%40ss%3Dword@localhost:3141").unwrap();
assert_eq!(dsn.username(), Some("user@domain"));
assert_eq!(dsn.password(), Some("p@ss=word"));
}
#[test]
fn test_dsn_parse_empty() {
let err = Dsn::parse("").unwrap_err();
assert!(err.to_string().contains("empty"));
}
#[test]
fn test_dsn_parse_unsupported_scheme() {
let err = Dsn::parse("http://localhost:3141").unwrap_err();
assert!(err.to_string().contains("Unsupported scheme"));
assert!(err.to_string().contains("http"));
}
#[test]
fn test_dsn_parse_invalid_port() {
let err = Dsn::parse("quic://localhost:invalid").unwrap_err();
assert!(err.to_string().contains("port") || err.to_string().contains("Invalid"));
}
#[test]
fn test_dsn_parse_port_too_large() {
let err = Dsn::parse("quic://localhost:99999").unwrap_err();
assert!(err.to_string().contains("port") || err.to_string().contains("Invalid"));
}
#[test]
fn test_dsn_parse_missing_host() {
let result = Dsn::parse("quic://:3141");
if let Ok(dsn) = result {
assert!(dsn.host().is_empty() || dsn.host() == "");
}
}
#[test]
fn test_dsn_parse_all_options() {
let dsn = Dsn::parse(
"grpc://localhost:50051?tls=1&insecure=false&page_size=2000&client_name=test-app&conformance=full"
).unwrap();
assert!(dsn.tls_enabled());
assert!(!dsn.skip_verify());
assert_eq!(dsn.page_size(), 2000);
assert_eq!(dsn.client_name(), "test-app");
assert_eq!(dsn.conformance(), "full");
}
#[test]
fn test_dsn_parse_unknown_options_preserved() {
let dsn = Dsn::parse("quic://localhost:3141?custom_option=value").unwrap();
assert_eq!(
dsn.options().get("custom_option"),
Some(&"value".to_string())
);
}
#[test]
fn test_dsn_parse_option_aliases() {
let dsn1 = Dsn::parse("grpc://localhost:50051?user=admin").unwrap();
let dsn2 = Dsn::parse("grpc://localhost:50051?username=admin").unwrap();
assert_eq!(dsn1.username(), dsn2.username());
let dsn3 = Dsn::parse("grpc://localhost:50051?pass=secret").unwrap();
let dsn4 = Dsn::parse("grpc://localhost:50051?password=secret").unwrap();
assert_eq!(dsn3.password(), dsn4.password());
let dsn5 = Dsn::parse("grpc://localhost:50051?skip_verify=true").unwrap();
let dsn6 = Dsn::parse("grpc://localhost:50051?insecure=true").unwrap();
assert_eq!(dsn5.skip_verify(), dsn6.skip_verify());
}
#[test]
fn test_dsn_parse_percent_encoded_values() {
let dsn = Dsn::parse("quic://localhost:3141?client_name=My%20App").unwrap();
assert_eq!(dsn.client_name(), "My App");
}
#[test]
fn test_dsn_parse_mtls_options() {
let dsn = Dsn::parse(
"grpc://localhost:50051?ca=/path/ca.crt&cert=/path/client.crt&key=/path/client.key",
)
.unwrap();
assert_eq!(dsn.ca_cert(), Some("/path/ca.crt"));
assert_eq!(dsn.client_cert(), Some("/path/client.crt"));
assert_eq!(dsn.client_key(), Some("/path/client.key"));
}
#[test]
fn test_dsn_parse_mtls_options_alt_names() {
let dsn = Dsn::parse("grpc://localhost:50051?ca_cert=/path/ca.crt&client_cert=/path/client.crt&client_key=/path/client.key").unwrap();
assert_eq!(dsn.ca_cert(), Some("/path/ca.crt"));
assert_eq!(dsn.client_cert(), Some("/path/client.crt"));
assert_eq!(dsn.client_key(), Some("/path/client.key"));
}
#[test]
fn test_dsn_parse_connect_timeout() {
let dsn = Dsn::parse("quic://localhost:3141?connect_timeout=60").unwrap();
assert_eq!(dsn.connect_timeout_secs(), Some(60));
}
#[test]
fn test_dsn_parse_server_name() {
let dsn = Dsn::parse("quic://localhost:3141?server_name=geode.example.com").unwrap();
assert_eq!(dsn.server_name(), Some("geode.example.com"));
}
#[test]
fn test_dsn_parse_insecure_tls_skip_verify_primary() {
let dsn = Dsn::parse("quic://localhost:3141?insecure_tls_skip_verify=true").unwrap();
assert!(dsn.skip_verify());
let dsn = Dsn::parse("quic://localhost:3141?insecure_tls_skip_verify=1").unwrap();
assert!(dsn.skip_verify());
let dsn = Dsn::parse("quic://localhost:3141?insecure_tls_skip_verify=yes").unwrap();
assert!(dsn.skip_verify());
let dsn = Dsn::parse("quic://localhost:3141?insecure_tls_skip_verify=on").unwrap();
assert!(dsn.skip_verify());
let dsn = Dsn::parse("quic://localhost:3141?insecure_tls_skip_verify=false").unwrap();
assert!(!dsn.skip_verify());
}
#[test]
fn test_dsn_parse_insecure_alias() {
let dsn = Dsn::parse("quic://localhost:3141?insecure=true").unwrap();
assert!(dsn.skip_verify());
let dsn = Dsn::parse("grpc://localhost:50051?insecure=1").unwrap();
assert!(dsn.skip_verify());
let dsn = Dsn::parse("localhost:3141?insecure=yes").unwrap();
assert!(dsn.skip_verify());
}
#[test]
fn test_dsn_parse_skip_verify_alias() {
let dsn = Dsn::parse("quic://localhost:3141?skip_verify=true").unwrap();
assert!(dsn.skip_verify());
let dsn = Dsn::parse("grpc://localhost:50051?skip_verify=1").unwrap();
assert!(dsn.skip_verify());
let dsn = Dsn::parse("quic://localhost:3141?skip_verify=on").unwrap();
assert!(dsn.skip_verify());
}
#[test]
fn test_dsn_parse_insecure_skip_verify_alias() {
let dsn = Dsn::parse("quic://localhost:3141?insecure_skip_verify=true").unwrap();
assert!(dsn.skip_verify());
let dsn = Dsn::parse("grpc://localhost:50051?insecure_skip_verify=1").unwrap();
assert!(dsn.skip_verify());
let dsn = Dsn::parse("localhost:3141?insecure_skip_verify=on").unwrap();
assert!(dsn.skip_verify());
let dsn = Dsn::parse("quic://localhost:3141?insecure_skip_verify=false").unwrap();
assert!(!dsn.skip_verify());
}
#[test]
fn test_dsn_skip_verify_default_false() {
let dsn = Dsn::parse("quic://localhost:3141").unwrap();
assert!(!dsn.skip_verify());
let dsn = Dsn::parse("grpc://localhost:50051").unwrap();
assert!(!dsn.skip_verify());
let dsn = Dsn::parse("localhost:3141").unwrap();
assert!(!dsn.skip_verify());
}
#[test]
fn test_transport_display() {
assert_eq!(Transport::Quic.to_string(), "quic");
assert_eq!(Transport::Grpc.to_string(), "grpc");
}
#[test]
fn test_dsn_address_ipv4() {
let dsn = Dsn::parse("quic://192.168.1.1:3141").unwrap();
assert_eq!(dsn.address(), "192.168.1.1:3141");
}
#[test]
fn test_dsn_address_hostname() {
let dsn = Dsn::parse("grpc://geode.example.com:50051").unwrap();
assert_eq!(dsn.address(), "geode.example.com:50051");
}
#[test]
fn test_acceptance_quic_localhost() {
let dsn = Dsn::parse("quic://localhost:1234").unwrap();
assert_eq!(dsn.transport(), Transport::Quic);
assert_eq!(dsn.host(), "localhost");
assert_eq!(dsn.port(), 1234);
}
#[test]
fn test_acceptance_grpc_with_tls_disabled() {
let dsn = Dsn::parse("grpc://127.0.0.1:50051?tls=0").unwrap();
assert_eq!(dsn.transport(), Transport::Grpc);
assert_eq!(dsn.host(), "127.0.0.1");
assert_eq!(dsn.port(), 50051);
assert!(!dsn.tls_enabled());
}
#[test]
fn test_acceptance_invalid_scheme() {
let err = Dsn::parse("http://localhost:1").unwrap_err();
let err_str = err.to_string();
assert!(
err_str.contains("Unsupported scheme") || err_str.contains("unsupported"),
"Expected 'unsupported scheme' error, got: {}",
err_str
);
}
#[test]
fn test_dsn_debug_redacts_password() {
let dsn = Dsn::parse("quic://user:secret@localhost:3141").unwrap();
let debug_str = format!("{:?}", dsn);
assert!(
!debug_str.contains("secret"),
"Debug output must not contain password"
);
assert!(
debug_str.contains("REDACTED"),
"Debug output must show REDACTED"
);
}
#[test]
fn test_dsn_parse_tenant_and_role() {
let dsn = Dsn::parse("quic://localhost:3141?tenant_id=acme&role=analyst").unwrap();
assert_eq!(dsn.tenant_id(), Some("acme"));
assert_eq!(dsn.role(), Some("analyst"));
}
#[test]
fn test_dsn_parse_tenant_alias() {
let dsn = Dsn::parse("quic://localhost:3141?tenant=acme").unwrap();
assert_eq!(dsn.tenant_id(), Some("acme"));
}
#[test]
fn test_dsn_tenant_role_default_none() {
let dsn = Dsn::parse("quic://localhost:3141").unwrap();
assert_eq!(dsn.tenant_id(), None);
assert_eq!(dsn.role(), None);
}
#[test]
fn test_dsn_debug_no_password_shows_none() {
let dsn = Dsn::parse("quic://localhost:3141").unwrap();
let debug_str = format!("{:?}", dsn);
assert!(
debug_str.contains("password: None"),
"Debug output should show None when no password is set"
);
}
#[test]
fn test_dsn_format_basic_quic_omits_defaults() {
let dsn = Dsn::parse("quic://localhost:3141").unwrap();
assert_eq!(dsn.to_string(), "quic://localhost:3141");
}
#[test]
fn test_dsn_format_ipv6_bracketed() {
let dsn = Dsn::parse("grpc://[::1]:50051?tls=0").unwrap();
let s = dsn.to_string();
assert!(s.starts_with("grpc://[::1]:50051"), "got {}", s);
assert!(s.contains("tls=0"));
}
#[test]
fn test_dsn_format_params_sorted_and_nondefault() {
let dsn = Dsn::parse(
"quic://localhost:3141?page_size=500&conformance=full&graph=g&role=r&tenant_id=t",
)
.unwrap();
let s = dsn.to_string();
let q = s.split('?').nth(1).unwrap();
let keys: Vec<&str> = q
.split('&')
.map(|kv| kv.split('=').next().unwrap())
.collect();
let mut sorted = keys.clone();
sorted.sort();
assert_eq!(keys, sorted, "params must be alphabetical: {}", q);
assert!(q.contains("page_size=500"));
assert!(q.contains("conformance=full"));
assert!(q.contains("tenant=t"));
assert!(q.contains("role=r"));
}
#[test]
fn test_dsn_format_userinfo() {
let dsn = Dsn::parse("quic://admin:secret@localhost:3141").unwrap();
let s = dsn.to_string();
assert!(s.contains("admin:secret@"), "got {}", s);
}
#[test]
fn test_dsn_format_roundtrip_reparse() {
let original = "grpc://localhost:50051?conformance=full&page_size=200&tls=0";
let dsn = Dsn::parse(original).unwrap();
let formatted = dsn.to_string();
let reparsed = Dsn::parse(&formatted).unwrap();
assert_eq!(reparsed.transport(), dsn.transport());
assert_eq!(reparsed.host(), dsn.host());
assert_eq!(reparsed.port(), dsn.port());
assert_eq!(reparsed.page_size(), dsn.page_size());
assert_eq!(reparsed.conformance(), dsn.conformance());
assert_eq!(reparsed.tls_enabled(), dsn.tls_enabled());
}
}