1use crate::error::{Error, Result};
31use std::collections::HashMap;
32
33pub const DEFAULT_PORT: u16 = 3141;
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
38pub enum Transport {
39 Quic,
41 Grpc,
43}
44
45impl std::fmt::Display for Transport {
46 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47 match self {
48 Transport::Quic => write!(f, "quic"),
49 Transport::Grpc => write!(f, "grpc"),
50 }
51 }
52}
53
54#[derive(Clone, PartialEq)]
58pub struct Dsn {
59 transport: Transport,
60 host: String,
61 port: u16,
62 username: Option<String>,
63 password: Option<String>,
64 tls_enabled: bool,
65 skip_verify: bool,
66 page_size: usize,
67 client_name: String,
68 client_version: String,
69 conformance: String,
70 ca_cert: Option<String>,
71 client_cert: Option<String>,
72 client_key: Option<String>,
73 server_name: Option<String>,
74 connect_timeout_secs: Option<u64>,
75 graph: Option<String>,
76 options: HashMap<String, String>,
77}
78
79impl Default for Dsn {
80 fn default() -> Self {
81 Self {
82 transport: Transport::Quic,
83 host: "localhost".to_string(),
84 port: DEFAULT_PORT,
85 username: None,
86 password: None,
87 tls_enabled: true,
88 skip_verify: false,
89 page_size: 1000,
90 client_name: "geode-rust".to_string(),
91 client_version: env!("CARGO_PKG_VERSION").to_string(),
92 conformance: "min".to_string(),
93 ca_cert: None,
94 client_cert: None,
95 client_key: None,
96 server_name: None,
97 connect_timeout_secs: None,
98 graph: None,
99 options: HashMap::new(),
100 }
101 }
102}
103
104impl Dsn {
105 pub fn parse(dsn: &str) -> Result<Self> {
148 let dsn = dsn.trim();
149 if dsn.is_empty() {
150 return Err(Error::invalid_dsn("DSN cannot be empty"));
151 }
152
153 if dsn.starts_with("quic://") {
155 Self::parse_url(dsn, Transport::Quic)
156 } else if let Some(rest) = dsn.strip_prefix("grpcs://") {
157 let rewritten = format!("grpc://{}", rest);
159 let mut result = Self::parse_url(&rewritten, Transport::Grpc)?;
160 result.tls_enabled = true;
161 Ok(result)
162 } else if dsn.starts_with("grpc://") {
163 Self::parse_url(dsn, Transport::Grpc)
164 } else if dsn.contains("://") {
165 let scheme = dsn.split("://").next().unwrap_or("");
167 Err(Error::invalid_dsn(format!(
168 "Unsupported scheme '{}'. Supported schemes: quic://, grpc://, grpcs://",
169 scheme
170 )))
171 } else {
172 Self::parse_legacy(dsn)
174 }
175 }
176
177 fn parse_url(dsn: &str, transport: Transport) -> Result<Self> {
179 let url = url::Url::parse(dsn)
181 .map_err(|e| Error::invalid_dsn(format!("Invalid URL format: {}", e)))?;
182
183 let host_raw = url
184 .host_str()
185 .ok_or_else(|| Error::invalid_dsn("Host is required"))?;
186
187 let host = if host_raw.starts_with('[') && host_raw.ends_with(']') {
189 host_raw[1..host_raw.len() - 1].to_string()
190 } else {
191 host_raw.to_string()
192 };
193
194 if host.is_empty() {
195 return Err(Error::invalid_dsn("Host is required"));
196 }
197
198 let port = url.port().unwrap_or(DEFAULT_PORT);
199
200 let username = if !url.username().is_empty() {
202 Some(
203 urlencoding::decode(url.username())
204 .map_err(|e| Error::invalid_dsn(format!("Invalid username encoding: {}", e)))?
205 .into_owned(),
206 )
207 } else {
208 None
209 };
210
211 let password = url.password().map(|p| {
212 urlencoding::decode(p)
213 .map(|s| s.into_owned())
214 .unwrap_or_else(|_| p.to_string())
215 });
216
217 let params: HashMap<String, String> = url.query_pairs().into_owned().collect();
219
220 let graph = if !url.path().is_empty() && url.path() != "/" {
222 Some(url.path().trim_start_matches('/').to_string())
223 } else {
224 None
225 };
226
227 let mut result = Self {
228 transport,
229 host,
230 port,
231 username,
232 password,
233 graph,
234 ..Default::default()
235 };
236
237 result.apply_params(¶ms)?;
238
239 Ok(result)
240 }
241
242 fn parse_legacy(dsn: &str) -> Result<Self> {
244 let (host_port, query_str) = if let Some(idx) = dsn.find('?') {
246 (&dsn[..idx], Some(&dsn[idx + 1..]))
247 } else {
248 (dsn, None)
249 };
250
251 let (host, port) = Self::parse_host_port(host_port)?;
253
254 if host.is_empty() {
255 return Err(Error::invalid_dsn("Host is required"));
256 }
257
258 let mut result = Self {
259 transport: Transport::Quic, host,
261 port,
262 ..Default::default()
263 };
264
265 if let Some(qs) = query_str {
267 let params: HashMap<String, String> = qs
268 .split('&')
269 .filter_map(|pair| {
270 let mut parts = pair.splitn(2, '=');
271 let key = parts.next()?;
272 let value = parts.next().unwrap_or("");
273 let decoded_value = urlencoding::decode(value)
275 .map(|s| s.into_owned())
276 .unwrap_or_else(|_| value.to_string());
277 Some((key.to_string(), decoded_value))
278 })
279 .collect();
280
281 result.apply_params(¶ms)?;
282 }
283
284 Ok(result)
285 }
286
287 fn parse_host_port(s: &str) -> Result<(String, u16)> {
289 if s.starts_with('[') {
291 if let Some(bracket_end) = s.find(']') {
293 let host = s[1..bracket_end].to_string();
294 let remainder = &s[bracket_end + 1..];
295 let port = if let Some(rest) = remainder.strip_prefix(':') {
296 rest.parse::<u16>()
297 .map_err(|_| Error::invalid_dsn(format!("Invalid port: {}", rest)))?
298 } else if remainder.is_empty() {
299 DEFAULT_PORT
300 } else {
301 return Err(Error::invalid_dsn("Invalid IPv6 address format"));
302 };
303 return Ok((host, port));
304 } else {
305 return Err(Error::invalid_dsn("Unclosed bracket in IPv6 address"));
306 }
307 }
308
309 if let Some(idx) = s.rfind(':') {
311 let host = s[..idx].to_string();
312 let port_str = &s[idx + 1..];
313 let port = port_str
314 .parse::<u16>()
315 .map_err(|_| Error::invalid_dsn(format!("Invalid port: {}", port_str)))?;
316 Ok((host, port))
317 } else {
318 Ok((s.to_string(), DEFAULT_PORT))
320 }
321 }
322
323 fn apply_params(&mut self, params: &HashMap<String, String>) -> Result<()> {
325 for (key, value) in params {
326 match key.as_str() {
327 "tls" => {
328 self.tls_enabled = parse_bool(value).unwrap_or(true);
329 }
330 "insecure_tls_skip_verify"
331 | "insecure"
332 | "skip_verify"
333 | "insecure_skip_verify" => {
334 self.skip_verify = parse_bool(value).unwrap_or(false);
335 }
336 "page_size" => {
337 self.page_size = value
338 .parse()
339 .map_err(|_| Error::invalid_dsn(format!("Invalid page_size: {}", value)))?;
340 }
341 "client_name" | "hello_name" => {
342 self.client_name = value.clone();
343 }
344 "client_version" | "hello_ver" => {
345 self.client_version = value.clone();
346 }
347 "conformance" => {
348 self.conformance = value.clone();
349 }
350 "username" | "user" => {
351 self.username = Some(value.clone());
352 }
353 "password" | "pass" => {
354 self.password = Some(value.clone());
355 }
356 "ca" | "ca_cert" => {
357 self.ca_cert = Some(value.clone());
358 }
359 "cert" | "client_cert" => {
360 self.client_cert = Some(value.clone());
361 }
362 "key" | "client_key" => {
363 self.client_key = Some(value.clone());
364 }
365 "server_name" => {
366 self.server_name = Some(value.clone());
367 }
368 "connect_timeout" | "timeout" => {
369 self.connect_timeout_secs = value.parse().ok();
370 }
371 "graph" => {
372 self.graph = Some(value.to_string());
373 }
374 _ => {
375 self.options.insert(key.clone(), value.clone());
377 }
378 }
379 }
380 Ok(())
381 }
382
383 pub fn transport(&self) -> Transport {
385 self.transport
386 }
387
388 pub fn host(&self) -> &str {
390 &self.host
391 }
392
393 pub fn port(&self) -> u16 {
395 self.port
396 }
397
398 pub fn username(&self) -> Option<&str> {
400 self.username.as_deref()
401 }
402
403 pub fn password(&self) -> Option<&str> {
405 self.password.as_deref()
406 }
407
408 pub fn tls_enabled(&self) -> bool {
410 self.tls_enabled
411 }
412
413 pub fn skip_verify(&self) -> bool {
415 self.skip_verify
416 }
417
418 pub fn page_size(&self) -> usize {
420 self.page_size
421 }
422
423 pub fn client_name(&self) -> &str {
425 &self.client_name
426 }
427
428 pub fn client_version(&self) -> &str {
430 &self.client_version
431 }
432
433 pub fn conformance(&self) -> &str {
435 &self.conformance
436 }
437
438 pub fn options(&self) -> &HashMap<String, String> {
440 &self.options
441 }
442
443 pub fn ca_cert(&self) -> Option<&str> {
445 self.ca_cert.as_deref()
446 }
447
448 pub fn client_cert(&self) -> Option<&str> {
450 self.client_cert.as_deref()
451 }
452
453 pub fn client_key(&self) -> Option<&str> {
455 self.client_key.as_deref()
456 }
457
458 pub fn server_name(&self) -> Option<&str> {
460 self.server_name.as_deref()
461 }
462
463 pub fn connect_timeout_secs(&self) -> Option<u64> {
465 self.connect_timeout_secs
466 }
467
468 pub fn graph(&self) -> Option<&str> {
470 self.graph.as_deref()
471 }
472
473 pub fn address(&self) -> String {
475 if self.host.contains(':') {
476 format!("[{}]:{}", self.host, self.port)
478 } else {
479 format!("{}:{}", self.host, self.port)
480 }
481 }
482
483 pub fn to_dsn(&self) -> String {
507 let defaults = Dsn::default();
508
509 let mut params: Vec<(String, String)> = Vec::new();
511
512 if self.page_size != defaults.page_size {
513 params.push(("page_size".to_string(), self.page_size.to_string()));
514 }
515 if self.client_name != defaults.client_name {
516 params.push(("hello_name".to_string(), self.client_name.clone()));
517 }
518 if self.client_version != defaults.client_version {
519 params.push(("hello_ver".to_string(), self.client_version.clone()));
520 }
521 if self.conformance != defaults.conformance {
522 params.push(("conformance".to_string(), self.conformance.clone()));
523 }
524 if let Some(ca) = &self.ca_cert {
525 params.push(("ca".to_string(), ca.clone()));
526 }
527 if let Some(cert) = &self.client_cert {
528 params.push(("cert".to_string(), cert.clone()));
529 }
530 if let Some(key) = &self.client_key {
531 params.push(("key".to_string(), key.clone()));
532 }
533 if let Some(sn) = &self.server_name {
534 params.push(("server_name".to_string(), sn.clone()));
535 }
536 if self.skip_verify {
537 params.push(("insecure_tls_skip_verify".to_string(), "true".to_string()));
538 }
539 if !self.tls_enabled {
540 params.push(("tls".to_string(), "0".to_string()));
541 }
542 if let Some(graph) = &self.graph {
543 params.push(("graph".to_string(), graph.clone()));
544 }
545 if let Some(timeout) = self.connect_timeout_secs {
546 params.push(("connect_timeout".to_string(), timeout.to_string()));
547 }
548 for (key, value) in &self.options {
550 params.push((key.clone(), value.clone()));
551 }
552
553 let userinfo = match (&self.username, &self.password) {
556 (Some(user), Some(pass)) => format!(
557 "{}:{}@",
558 urlencoding::encode(user),
559 urlencoding::encode(pass)
560 ),
561 (Some(user), None) => format!("{}@", urlencoding::encode(user)),
562 (None, Some(pass)) => {
563 params.push(("pass".to_string(), pass.clone()));
564 String::new()
565 }
566 (None, None) => String::new(),
567 };
568
569 params.sort_by(|a, b| a.0.cmp(&b.0).then_with(|| a.1.cmp(&b.1)));
571
572 let mut dsn = format!("{}://{}{}", self.transport, userinfo, self.address());
573
574 if !params.is_empty() {
575 let query: Vec<String> = params
576 .iter()
577 .map(|(k, v)| format!("{}={}", urlencoding::encode(k), urlencoding::encode(v)))
578 .collect();
579 dsn.push('?');
580 dsn.push_str(&query.join("&"));
581 }
582
583 dsn
584 }
585}
586
587impl std::fmt::Display for Dsn {
588 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
589 f.write_str(&self.to_dsn())
590 }
591}
592
593impl std::fmt::Debug for Dsn {
594 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
595 f.debug_struct("Dsn")
596 .field("transport", &self.transport)
597 .field("host", &self.host)
598 .field("port", &self.port)
599 .field("username", &self.username)
600 .field("password", &self.password.as_ref().map(|_| "[REDACTED]"))
601 .field("tls_enabled", &self.tls_enabled)
602 .field("skip_verify", &self.skip_verify)
603 .field("page_size", &self.page_size)
604 .field("client_name", &self.client_name)
605 .field("client_version", &self.client_version)
606 .field("conformance", &self.conformance)
607 .field("ca_cert", &self.ca_cert)
608 .field("client_cert", &self.client_cert)
609 .field("client_key", &self.client_key)
610 .field("server_name", &self.server_name)
611 .field("connect_timeout_secs", &self.connect_timeout_secs)
612 .field("graph", &self.graph)
613 .field("options", &self.options)
614 .finish()
615 }
616}
617
618fn parse_bool(s: &str) -> Option<bool> {
620 match s.to_lowercase().as_str() {
621 "true" | "1" | "yes" | "on" => Some(true),
622 "false" | "0" | "no" | "off" => Some(false),
623 _ => None,
624 }
625}
626
627#[cfg(test)]
628mod tests {
629 use super::*;
630
631 #[test]
636 fn test_dsn_parse_quic_basic() {
637 let dsn = Dsn::parse("quic://localhost:3141").unwrap();
638 assert_eq!(dsn.transport(), Transport::Quic);
639 assert_eq!(dsn.host(), "localhost");
640 assert_eq!(dsn.port(), 3141);
641 assert!(dsn.tls_enabled());
642 assert!(!dsn.skip_verify());
643 }
644
645 #[test]
646 fn test_dsn_parse_quic_with_ip() {
647 let dsn = Dsn::parse("quic://127.0.0.1:3141").unwrap();
648 assert_eq!(dsn.transport(), Transport::Quic);
649 assert_eq!(dsn.host(), "127.0.0.1");
650 assert_eq!(dsn.port(), 3141);
651 }
652
653 #[test]
654 fn test_dsn_parse_quic_default_port() {
655 let dsn = Dsn::parse("quic://localhost").unwrap();
656 assert_eq!(dsn.port(), DEFAULT_PORT);
657 }
658
659 #[test]
660 fn test_dsn_parse_quic_with_options() {
661 let dsn = Dsn::parse("quic://localhost:3141?page_size=500&insecure=true").unwrap();
662 assert_eq!(dsn.page_size(), 500);
663 assert!(dsn.skip_verify());
664 }
665
666 #[test]
671 fn test_dsn_parse_grpc_basic() {
672 let dsn = Dsn::parse("grpc://localhost:50051").unwrap();
673 assert_eq!(dsn.transport(), Transport::Grpc);
674 assert_eq!(dsn.host(), "localhost");
675 assert_eq!(dsn.port(), 50051);
676 }
677
678 #[test]
679 fn test_dsn_parse_grpc_with_tls_disabled() {
680 let dsn = Dsn::parse("grpc://127.0.0.1:50051?tls=0").unwrap();
681 assert_eq!(dsn.transport(), Transport::Grpc);
682 assert_eq!(dsn.host(), "127.0.0.1");
683 assert_eq!(dsn.port(), 50051);
684 assert!(!dsn.tls_enabled());
685 }
686
687 #[test]
688 fn test_dsn_parse_grpc_with_tls_false() {
689 let dsn = Dsn::parse("grpc://localhost:50051?tls=false").unwrap();
690 assert!(!dsn.tls_enabled());
691 }
692
693 #[test]
694 fn test_dsn_parse_grpcs_scheme() {
695 let dsn = Dsn::parse("grpcs://localhost:50051").unwrap();
696 assert_eq!(dsn.transport(), Transport::Grpc);
697 assert!(dsn.tls_enabled());
698 assert_eq!(dsn.host(), "localhost");
699 assert_eq!(dsn.port(), 50051);
700 }
701
702 #[test]
703 fn test_dsn_parse_grpcs_tls_forced() {
704 let dsn = Dsn::parse("grpcs://localhost:50051?tls=0").unwrap();
706 assert_eq!(dsn.transport(), Transport::Grpc);
707 assert!(dsn.tls_enabled());
708 }
709
710 #[test]
711 fn test_dsn_parse_unsupported_schemes() {
712 let err = Dsn::parse("grcp://localhost:50051").unwrap_err();
714 assert!(err.to_string().contains("Unsupported scheme"));
715
716 let err = Dsn::parse("geode://localhost:3141").unwrap_err();
718 assert!(err.to_string().contains("Unsupported scheme"));
719 }
720
721 #[test]
726 fn test_dsn_parse_ipv6_grpc() {
727 let dsn = Dsn::parse("grpc://[::1]:50051").unwrap();
728 assert_eq!(dsn.transport(), Transport::Grpc);
729 assert_eq!(dsn.host(), "::1");
730 assert_eq!(dsn.port(), 50051);
731 }
732
733 #[test]
734 fn test_dsn_parse_ipv6_quic() {
735 let dsn = Dsn::parse("quic://[::1]:3141").unwrap();
736 assert_eq!(dsn.transport(), Transport::Quic);
737 assert_eq!(dsn.host(), "::1");
738 assert_eq!(dsn.port(), 3141);
739 }
740
741 #[test]
742 fn test_dsn_parse_ipv6_full_address() {
743 let dsn = Dsn::parse("grpc://[2001:db8::1]:50051").unwrap();
744 assert_eq!(dsn.host(), "2001:db8::1");
745 assert_eq!(dsn.port(), 50051);
746 }
747
748 #[test]
749 fn test_dsn_parse_ipv6_default_port() {
750 let dsn = Dsn::parse("quic://[::1]").unwrap();
751 assert_eq!(dsn.host(), "::1");
752 assert_eq!(dsn.port(), DEFAULT_PORT);
753 }
754
755 #[test]
756 fn test_dsn_address_ipv6() {
757 let dsn = Dsn::parse("grpc://[::1]:50051").unwrap();
758 assert_eq!(dsn.address(), "[::1]:50051");
759 }
760
761 #[test]
766 fn test_dsn_parse_legacy_host_port() {
767 let dsn = Dsn::parse("localhost:3141").unwrap();
768 assert_eq!(dsn.transport(), Transport::Quic); assert_eq!(dsn.host(), "localhost");
770 assert_eq!(dsn.port(), 3141);
771 }
772
773 #[test]
774 fn test_dsn_parse_legacy_with_options() {
775 let dsn = Dsn::parse("localhost:3141?insecure=true&page_size=500").unwrap();
776 assert_eq!(dsn.transport(), Transport::Quic);
777 assert!(dsn.skip_verify());
778 assert_eq!(dsn.page_size(), 500);
779 }
780
781 #[test]
782 fn test_dsn_parse_legacy_ipv6() {
783 let dsn = Dsn::parse("[::1]:3141").unwrap();
784 assert_eq!(dsn.host(), "::1");
785 assert_eq!(dsn.port(), 3141);
786 }
787
788 #[test]
793 fn test_dsn_parse_with_auth() {
794 let dsn = Dsn::parse("quic://admin:secret@localhost:3141").unwrap();
795 assert_eq!(dsn.username(), Some("admin"));
796 assert_eq!(dsn.password(), Some("secret"));
797 }
798
799 #[test]
800 fn test_dsn_parse_auth_via_query_params() {
801 let dsn = Dsn::parse("grpc://localhost:50051?username=admin&password=secret").unwrap();
802 assert_eq!(dsn.username(), Some("admin"));
803 assert_eq!(dsn.password(), Some("secret"));
804 }
805
806 #[test]
807 fn test_dsn_parse_auth_percent_encoded() {
808 let dsn = Dsn::parse("quic://user%40domain:p%40ss%3Dword@localhost:3141").unwrap();
809 assert_eq!(dsn.username(), Some("user@domain"));
810 assert_eq!(dsn.password(), Some("p@ss=word"));
811 }
812
813 #[test]
818 fn test_dsn_parse_empty() {
819 let err = Dsn::parse("").unwrap_err();
820 assert!(err.to_string().contains("empty"));
821 }
822
823 #[test]
824 fn test_dsn_parse_unsupported_scheme() {
825 let err = Dsn::parse("http://localhost:3141").unwrap_err();
826 assert!(err.to_string().contains("Unsupported scheme"));
827 assert!(err.to_string().contains("http"));
828 }
829
830 #[test]
831 fn test_dsn_parse_invalid_port() {
832 let err = Dsn::parse("quic://localhost:invalid").unwrap_err();
833 assert!(err.to_string().contains("port") || err.to_string().contains("Invalid"));
834 }
835
836 #[test]
837 fn test_dsn_parse_port_too_large() {
838 let err = Dsn::parse("quic://localhost:99999").unwrap_err();
839 assert!(err.to_string().contains("port") || err.to_string().contains("Invalid"));
840 }
841
842 #[test]
843 fn test_dsn_parse_missing_host() {
844 let result = Dsn::parse("quic://:3141");
847 if let Ok(dsn) = result {
849 assert!(dsn.host().is_empty() || dsn.host() == "");
850 }
851 }
852
853 #[test]
858 fn test_dsn_parse_all_options() {
859 let dsn = Dsn::parse(
860 "grpc://localhost:50051?tls=1&insecure=false&page_size=2000&client_name=test-app&conformance=full"
861 ).unwrap();
862
863 assert!(dsn.tls_enabled());
864 assert!(!dsn.skip_verify());
865 assert_eq!(dsn.page_size(), 2000);
866 assert_eq!(dsn.client_name(), "test-app");
867 assert_eq!(dsn.conformance(), "full");
868 }
869
870 #[test]
871 fn test_dsn_parse_unknown_options_preserved() {
872 let dsn = Dsn::parse("quic://localhost:3141?custom_option=value").unwrap();
873 assert_eq!(
874 dsn.options().get("custom_option"),
875 Some(&"value".to_string())
876 );
877 }
878
879 #[test]
880 fn test_dsn_parse_option_aliases() {
881 let dsn1 = Dsn::parse("grpc://localhost:50051?user=admin").unwrap();
883 let dsn2 = Dsn::parse("grpc://localhost:50051?username=admin").unwrap();
884 assert_eq!(dsn1.username(), dsn2.username());
885
886 let dsn3 = Dsn::parse("grpc://localhost:50051?pass=secret").unwrap();
888 let dsn4 = Dsn::parse("grpc://localhost:50051?password=secret").unwrap();
889 assert_eq!(dsn3.password(), dsn4.password());
890
891 let dsn5 = Dsn::parse("grpc://localhost:50051?skip_verify=true").unwrap();
893 let dsn6 = Dsn::parse("grpc://localhost:50051?insecure=true").unwrap();
894 assert_eq!(dsn5.skip_verify(), dsn6.skip_verify());
895 }
896
897 #[test]
898 fn test_dsn_parse_percent_encoded_values() {
899 let dsn = Dsn::parse("quic://localhost:3141?client_name=My%20App").unwrap();
900 assert_eq!(dsn.client_name(), "My App");
901 }
902
903 #[test]
904 fn test_dsn_parse_mtls_options() {
905 let dsn = Dsn::parse(
906 "grpc://localhost:50051?ca=/path/ca.crt&cert=/path/client.crt&key=/path/client.key",
907 )
908 .unwrap();
909 assert_eq!(dsn.ca_cert(), Some("/path/ca.crt"));
910 assert_eq!(dsn.client_cert(), Some("/path/client.crt"));
911 assert_eq!(dsn.client_key(), Some("/path/client.key"));
912 }
913
914 #[test]
915 fn test_dsn_parse_mtls_options_alt_names() {
916 let dsn = Dsn::parse("grpc://localhost:50051?ca_cert=/path/ca.crt&client_cert=/path/client.crt&client_key=/path/client.key").unwrap();
917 assert_eq!(dsn.ca_cert(), Some("/path/ca.crt"));
918 assert_eq!(dsn.client_cert(), Some("/path/client.crt"));
919 assert_eq!(dsn.client_key(), Some("/path/client.key"));
920 }
921
922 #[test]
923 fn test_dsn_parse_connect_timeout() {
924 let dsn = Dsn::parse("quic://localhost:3141?connect_timeout=60").unwrap();
925 assert_eq!(dsn.connect_timeout_secs(), Some(60));
926 }
927
928 #[test]
929 fn test_dsn_parse_server_name() {
930 let dsn = Dsn::parse("quic://localhost:3141?server_name=geode.example.com").unwrap();
931 assert_eq!(dsn.server_name(), Some("geode.example.com"));
932 }
933
934 #[test]
939 fn test_dsn_parse_insecure_tls_skip_verify_primary() {
940 let dsn = Dsn::parse("quic://localhost:3141?insecure_tls_skip_verify=true").unwrap();
942 assert!(dsn.skip_verify());
943
944 let dsn = Dsn::parse("quic://localhost:3141?insecure_tls_skip_verify=1").unwrap();
945 assert!(dsn.skip_verify());
946
947 let dsn = Dsn::parse("quic://localhost:3141?insecure_tls_skip_verify=yes").unwrap();
948 assert!(dsn.skip_verify());
949
950 let dsn = Dsn::parse("quic://localhost:3141?insecure_tls_skip_verify=on").unwrap();
951 assert!(dsn.skip_verify());
952
953 let dsn = Dsn::parse("quic://localhost:3141?insecure_tls_skip_verify=false").unwrap();
954 assert!(!dsn.skip_verify());
955 }
956
957 #[test]
958 fn test_dsn_parse_insecure_alias() {
959 let dsn = Dsn::parse("quic://localhost:3141?insecure=true").unwrap();
961 assert!(dsn.skip_verify());
962
963 let dsn = Dsn::parse("grpc://localhost:50051?insecure=1").unwrap();
964 assert!(dsn.skip_verify());
965
966 let dsn = Dsn::parse("localhost:3141?insecure=yes").unwrap();
967 assert!(dsn.skip_verify());
968 }
969
970 #[test]
971 fn test_dsn_parse_skip_verify_alias() {
972 let dsn = Dsn::parse("quic://localhost:3141?skip_verify=true").unwrap();
974 assert!(dsn.skip_verify());
975
976 let dsn = Dsn::parse("grpc://localhost:50051?skip_verify=1").unwrap();
977 assert!(dsn.skip_verify());
978
979 let dsn = Dsn::parse("quic://localhost:3141?skip_verify=on").unwrap();
980 assert!(dsn.skip_verify());
981 }
982
983 #[test]
984 fn test_dsn_parse_insecure_skip_verify_alias() {
985 let dsn = Dsn::parse("quic://localhost:3141?insecure_skip_verify=true").unwrap();
987 assert!(dsn.skip_verify());
988
989 let dsn = Dsn::parse("grpc://localhost:50051?insecure_skip_verify=1").unwrap();
990 assert!(dsn.skip_verify());
991
992 let dsn = Dsn::parse("localhost:3141?insecure_skip_verify=on").unwrap();
993 assert!(dsn.skip_verify());
994
995 let dsn = Dsn::parse("quic://localhost:3141?insecure_skip_verify=false").unwrap();
996 assert!(!dsn.skip_verify());
997 }
998
999 #[test]
1000 fn test_dsn_skip_verify_default_false() {
1001 let dsn = Dsn::parse("quic://localhost:3141").unwrap();
1003 assert!(!dsn.skip_verify());
1004
1005 let dsn = Dsn::parse("grpc://localhost:50051").unwrap();
1006 assert!(!dsn.skip_verify());
1007
1008 let dsn = Dsn::parse("localhost:3141").unwrap();
1009 assert!(!dsn.skip_verify());
1010 }
1011
1012 #[test]
1017 fn test_transport_display() {
1018 assert_eq!(Transport::Quic.to_string(), "quic");
1019 assert_eq!(Transport::Grpc.to_string(), "grpc");
1020 }
1021
1022 #[test]
1027 fn test_dsn_address_ipv4() {
1028 let dsn = Dsn::parse("quic://192.168.1.1:3141").unwrap();
1029 assert_eq!(dsn.address(), "192.168.1.1:3141");
1030 }
1031
1032 #[test]
1033 fn test_dsn_address_hostname() {
1034 let dsn = Dsn::parse("grpc://geode.example.com:50051").unwrap();
1035 assert_eq!(dsn.address(), "geode.example.com:50051");
1036 }
1037
1038 #[test]
1043 fn test_acceptance_quic_localhost() {
1044 let dsn = Dsn::parse("quic://localhost:1234").unwrap();
1046 assert_eq!(dsn.transport(), Transport::Quic);
1047 assert_eq!(dsn.host(), "localhost");
1048 assert_eq!(dsn.port(), 1234);
1049 }
1050
1051 #[test]
1052 fn test_acceptance_grpc_with_tls_disabled() {
1053 let dsn = Dsn::parse("grpc://127.0.0.1:50051?tls=0").unwrap();
1055 assert_eq!(dsn.transport(), Transport::Grpc);
1056 assert_eq!(dsn.host(), "127.0.0.1");
1057 assert_eq!(dsn.port(), 50051);
1058 assert!(!dsn.tls_enabled());
1059 }
1060
1061 #[test]
1062 fn test_acceptance_invalid_scheme() {
1063 let err = Dsn::parse("http://localhost:1").unwrap_err();
1065 let err_str = err.to_string();
1066 assert!(
1067 err_str.contains("Unsupported scheme") || err_str.contains("unsupported"),
1068 "Expected 'unsupported scheme' error, got: {}",
1069 err_str
1070 );
1071 }
1072
1073 #[test]
1078 fn test_dsn_debug_redacts_password() {
1079 let dsn = Dsn::parse("quic://user:secret@localhost:3141").unwrap();
1080 let debug_str = format!("{:?}", dsn);
1081 assert!(
1082 !debug_str.contains("secret"),
1083 "Debug output must not contain password"
1084 );
1085 assert!(
1086 debug_str.contains("REDACTED"),
1087 "Debug output must show REDACTED"
1088 );
1089 }
1090
1091 fn assert_roundtrip(dsn_str: &str) {
1096 let parsed = Dsn::parse(dsn_str).unwrap();
1097 let formatted = parsed.to_dsn();
1098 let reparsed = Dsn::parse(&formatted).unwrap_or_else(|e| {
1099 panic!(
1100 "reparse of {:?} (from {:?}) failed: {}",
1101 formatted, dsn_str, e
1102 )
1103 });
1104 assert_eq!(
1105 parsed, reparsed,
1106 "round-trip mismatch: {:?} -> {:?}",
1107 dsn_str, formatted
1108 );
1109 }
1110
1111 #[test]
1112 fn test_to_dsn_roundtrip_quic_basic() {
1113 assert_roundtrip("quic://localhost:3141");
1114 }
1115
1116 #[test]
1117 fn test_to_dsn_roundtrip_grpc_tls_disabled() {
1118 assert_roundtrip("grpc://127.0.0.1:50051?tls=0");
1119 }
1120
1121 #[test]
1122 fn test_to_dsn_roundtrip_page_size() {
1123 assert_roundtrip("quic://localhost:3141?page_size=2500");
1124 }
1125
1126 #[test]
1127 fn test_to_dsn_roundtrip_graph() {
1128 assert_roundtrip("quic://localhost:3141?graph=mygraph");
1129 }
1130
1131 #[test]
1132 fn test_to_dsn_roundtrip_userpass() {
1133 assert_roundtrip("quic://admin:secret@localhost:3141");
1134 }
1135
1136 #[test]
1137 fn test_to_dsn_roundtrip_user_only() {
1138 assert_roundtrip("quic://admin@localhost:3141");
1139 }
1140
1141 #[test]
1142 fn test_to_dsn_roundtrip_password_only() {
1143 let parsed = Dsn::parse("quic://localhost:3141?pass=secret").unwrap();
1145 let formatted = parsed.to_dsn();
1146 assert!(
1147 formatted.contains("pass=secret"),
1148 "password-only DSN should emit pass= query param, got: {}",
1149 formatted
1150 );
1151 let reparsed = Dsn::parse(&formatted).unwrap();
1152 assert_eq!(parsed, reparsed);
1153 assert_eq!(reparsed.password(), Some("secret"));
1154 assert_eq!(reparsed.username(), None);
1155 }
1156
1157 #[test]
1158 fn test_to_dsn_roundtrip_ipv6() {
1159 assert_roundtrip("quic://[::1]:3141");
1160 assert_roundtrip("grpc://[2001:db8::1]:50051?tls=0");
1161 }
1162
1163 #[test]
1164 fn test_to_dsn_roundtrip_skip_verify() {
1165 assert_roundtrip("quic://localhost:3141?insecure_tls_skip_verify=true");
1166 }
1167
1168 #[test]
1169 fn test_to_dsn_roundtrip_full() {
1170 assert_roundtrip(
1171 "grpc://admin:p%40ss@host.example.com:50051?tls=0&page_size=500&conformance=full&graph=g1&connect_timeout=60",
1172 );
1173 }
1174
1175 #[test]
1176 fn test_to_dsn_scheme_present() {
1177 let dsn = Dsn::parse("grpc://localhost:50051").unwrap();
1178 assert!(dsn.to_dsn().starts_with("grpc://"));
1179 let dsn = Dsn::parse("quic://localhost:3141").unwrap();
1180 assert!(dsn.to_dsn().starts_with("quic://"));
1181 }
1182
1183 #[test]
1184 fn test_dsn_debug_no_password_shows_none() {
1185 let dsn = Dsn::parse("quic://localhost:3141").unwrap();
1186 let debug_str = format!("{:?}", dsn);
1187 assert!(
1188 debug_str.contains("password: None"),
1189 "Debug output should show None when no password is set"
1190 );
1191 }
1192}