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)]
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 tenant_id: Option<String>,
77 role: Option<String>,
78 options: HashMap<String, String>,
79}
80
81impl Default for Dsn {
82 fn default() -> Self {
83 Self {
84 transport: Transport::Quic,
85 host: "localhost".to_string(),
86 port: DEFAULT_PORT,
87 username: None,
88 password: None,
89 tls_enabled: true,
90 skip_verify: false,
91 page_size: 1000,
92 client_name: "geode-rust".to_string(),
93 client_version: env!("CARGO_PKG_VERSION").to_string(),
94 conformance: "min".to_string(),
95 ca_cert: None,
96 client_cert: None,
97 client_key: None,
98 server_name: None,
99 connect_timeout_secs: None,
100 graph: None,
101 tenant_id: None,
102 role: None,
103 options: HashMap::new(),
104 }
105 }
106}
107
108impl Dsn {
109 pub fn parse(dsn: &str) -> Result<Self> {
152 let dsn = dsn.trim();
153 if dsn.is_empty() {
154 return Err(Error::invalid_dsn("DSN cannot be empty"));
155 }
156
157 if dsn.starts_with("quic://") {
159 Self::parse_url(dsn, Transport::Quic)
160 } else if let Some(rest) = dsn.strip_prefix("grpcs://") {
161 let rewritten = format!("grpc://{}", rest);
163 let mut result = Self::parse_url(&rewritten, Transport::Grpc)?;
164 result.tls_enabled = true;
165 Ok(result)
166 } else if dsn.starts_with("grpc://") {
167 Self::parse_url(dsn, Transport::Grpc)
168 } else if dsn.contains("://") {
169 let scheme = dsn.split("://").next().unwrap_or("");
171 Err(Error::invalid_dsn(format!(
172 "Unsupported scheme '{}'. Supported schemes: quic://, grpc://, grpcs://",
173 scheme
174 )))
175 } else {
176 Self::parse_legacy(dsn)
178 }
179 }
180
181 fn parse_url(dsn: &str, transport: Transport) -> Result<Self> {
183 let url = url::Url::parse(dsn)
185 .map_err(|e| Error::invalid_dsn(format!("Invalid URL format: {}", e)))?;
186
187 let host_raw = url
188 .host_str()
189 .ok_or_else(|| Error::invalid_dsn("Host is required"))?;
190
191 let host = if host_raw.starts_with('[') && host_raw.ends_with(']') {
193 host_raw[1..host_raw.len() - 1].to_string()
194 } else {
195 host_raw.to_string()
196 };
197
198 if host.is_empty() {
199 return Err(Error::invalid_dsn("Host is required"));
200 }
201
202 let port = url.port().unwrap_or(DEFAULT_PORT);
203
204 let username = if !url.username().is_empty() {
206 Some(
207 urlencoding::decode(url.username())
208 .map_err(|e| Error::invalid_dsn(format!("Invalid username encoding: {}", e)))?
209 .into_owned(),
210 )
211 } else {
212 None
213 };
214
215 let password = url.password().map(|p| {
216 urlencoding::decode(p)
217 .map(|s| s.into_owned())
218 .unwrap_or_else(|_| p.to_string())
219 });
220
221 let params: HashMap<String, String> = url.query_pairs().into_owned().collect();
223
224 let graph = if !url.path().is_empty() && url.path() != "/" {
226 Some(url.path().trim_start_matches('/').to_string())
227 } else {
228 None
229 };
230
231 let mut result = Self {
232 transport,
233 host,
234 port,
235 username,
236 password,
237 graph,
238 ..Default::default()
239 };
240
241 result.apply_params(¶ms)?;
242
243 Ok(result)
244 }
245
246 fn parse_legacy(dsn: &str) -> Result<Self> {
248 let (host_port, query_str) = if let Some(idx) = dsn.find('?') {
250 (&dsn[..idx], Some(&dsn[idx + 1..]))
251 } else {
252 (dsn, None)
253 };
254
255 let (host, port) = Self::parse_host_port(host_port)?;
257
258 if host.is_empty() {
259 return Err(Error::invalid_dsn("Host is required"));
260 }
261
262 let mut result = Self {
263 transport: Transport::Quic, host,
265 port,
266 ..Default::default()
267 };
268
269 if let Some(qs) = query_str {
271 let params: HashMap<String, String> = qs
272 .split('&')
273 .filter_map(|pair| {
274 let mut parts = pair.splitn(2, '=');
275 let key = parts.next()?;
276 let value = parts.next().unwrap_or("");
277 let decoded_value = urlencoding::decode(value)
279 .map(|s| s.into_owned())
280 .unwrap_or_else(|_| value.to_string());
281 Some((key.to_string(), decoded_value))
282 })
283 .collect();
284
285 result.apply_params(¶ms)?;
286 }
287
288 Ok(result)
289 }
290
291 fn parse_host_port(s: &str) -> Result<(String, u16)> {
293 if s.starts_with('[') {
295 if let Some(bracket_end) = s.find(']') {
297 let host = s[1..bracket_end].to_string();
298 let remainder = &s[bracket_end + 1..];
299 let port = if let Some(rest) = remainder.strip_prefix(':') {
300 rest.parse::<u16>()
301 .map_err(|_| Error::invalid_dsn(format!("Invalid port: {}", rest)))?
302 } else if remainder.is_empty() {
303 DEFAULT_PORT
304 } else {
305 return Err(Error::invalid_dsn("Invalid IPv6 address format"));
306 };
307 return Ok((host, port));
308 } else {
309 return Err(Error::invalid_dsn("Unclosed bracket in IPv6 address"));
310 }
311 }
312
313 if let Some(idx) = s.rfind(':') {
315 let host = s[..idx].to_string();
316 let port_str = &s[idx + 1..];
317 let port = port_str
318 .parse::<u16>()
319 .map_err(|_| Error::invalid_dsn(format!("Invalid port: {}", port_str)))?;
320 Ok((host, port))
321 } else {
322 Ok((s.to_string(), DEFAULT_PORT))
324 }
325 }
326
327 fn apply_params(&mut self, params: &HashMap<String, String>) -> Result<()> {
329 for (key, value) in params {
330 match key.as_str() {
331 "tls" => {
332 self.tls_enabled = parse_bool(value).unwrap_or(true);
333 }
334 "insecure_tls_skip_verify"
335 | "insecure"
336 | "skip_verify"
337 | "insecure_skip_verify" => {
338 self.skip_verify = parse_bool(value).unwrap_or(false);
339 }
340 "page_size" => {
341 self.page_size = value
342 .parse()
343 .map_err(|_| Error::invalid_dsn(format!("Invalid page_size: {}", value)))?;
344 }
345 "client_name" | "hello_name" => {
346 self.client_name = value.clone();
347 }
348 "client_version" | "hello_ver" => {
349 self.client_version = value.clone();
350 }
351 "conformance" => {
352 self.conformance = value.clone();
353 }
354 "username" | "user" => {
355 self.username = Some(value.clone());
356 }
357 "password" | "pass" => {
358 self.password = Some(value.clone());
359 }
360 "ca" | "ca_cert" => {
361 self.ca_cert = Some(value.clone());
362 }
363 "cert" | "client_cert" => {
364 self.client_cert = Some(value.clone());
365 }
366 "key" | "client_key" => {
367 self.client_key = Some(value.clone());
368 }
369 "server_name" => {
370 self.server_name = Some(value.clone());
371 }
372 "connect_timeout" | "timeout" => {
373 self.connect_timeout_secs = value.parse().ok();
374 }
375 "graph" => {
376 self.graph = Some(value.to_string());
377 }
378 "tenant_id" | "tenant" => {
379 self.tenant_id = Some(value.clone());
380 }
381 "role" => {
382 self.role = Some(value.clone());
383 }
384 _ => {
385 self.options.insert(key.clone(), value.clone());
387 }
388 }
389 }
390 Ok(())
391 }
392
393 pub fn transport(&self) -> Transport {
395 self.transport
396 }
397
398 pub fn host(&self) -> &str {
400 &self.host
401 }
402
403 pub fn port(&self) -> u16 {
405 self.port
406 }
407
408 pub fn username(&self) -> Option<&str> {
410 self.username.as_deref()
411 }
412
413 pub fn password(&self) -> Option<&str> {
415 self.password.as_deref()
416 }
417
418 pub fn tls_enabled(&self) -> bool {
420 self.tls_enabled
421 }
422
423 pub fn skip_verify(&self) -> bool {
425 self.skip_verify
426 }
427
428 pub fn page_size(&self) -> usize {
430 self.page_size
431 }
432
433 pub fn client_name(&self) -> &str {
435 &self.client_name
436 }
437
438 pub fn client_version(&self) -> &str {
440 &self.client_version
441 }
442
443 pub fn conformance(&self) -> &str {
445 &self.conformance
446 }
447
448 pub fn options(&self) -> &HashMap<String, String> {
450 &self.options
451 }
452
453 pub fn ca_cert(&self) -> Option<&str> {
455 self.ca_cert.as_deref()
456 }
457
458 pub fn client_cert(&self) -> Option<&str> {
460 self.client_cert.as_deref()
461 }
462
463 pub fn client_key(&self) -> Option<&str> {
465 self.client_key.as_deref()
466 }
467
468 pub fn server_name(&self) -> Option<&str> {
470 self.server_name.as_deref()
471 }
472
473 pub fn connect_timeout_secs(&self) -> Option<u64> {
475 self.connect_timeout_secs
476 }
477
478 pub fn graph(&self) -> Option<&str> {
480 self.graph.as_deref()
481 }
482
483 pub fn tenant_id(&self) -> Option<&str> {
485 self.tenant_id.as_deref()
486 }
487
488 pub fn role(&self) -> Option<&str> {
490 self.role.as_deref()
491 }
492
493 pub fn address(&self) -> String {
495 if self.host.contains(':') {
496 format!("[{}]:{}", self.host, self.port)
498 } else {
499 format!("{}:{}", self.host, self.port)
500 }
501 }
502}
503
504impl std::fmt::Display for Dsn {
505 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
506 let mut params: Vec<(String, String)> = Vec::new();
509
510 if self.page_size != 1000 {
511 params.push(("page_size".into(), self.page_size.to_string()));
512 }
513 if self.client_name != "geode-rust" {
514 params.push(("client_name".into(), self.client_name.clone()));
515 }
516 if self.client_version != env!("CARGO_PKG_VERSION") {
517 params.push(("client_version".into(), self.client_version.clone()));
518 }
519 if self.conformance != "min" {
520 params.push(("conformance".into(), self.conformance.clone()));
521 }
522 if let Some(ca) = &self.ca_cert {
523 params.push(("ca".into(), ca.clone()));
524 }
525 if let Some(cert) = &self.client_cert {
526 params.push(("cert".into(), cert.clone()));
527 }
528 if let Some(key) = &self.client_key {
529 params.push(("key".into(), key.clone()));
530 }
531 if self.skip_verify {
532 params.push(("insecure_tls_skip_verify".into(), "true".into()));
533 }
534 if !self.tls_enabled {
535 params.push(("tls".into(), "0".into()));
536 }
537 if let Some(graph) = &self.graph {
538 params.push(("graph".into(), graph.clone()));
539 }
540 if let Some(tenant) = &self.tenant_id {
541 params.push(("tenant".into(), tenant.clone()));
542 }
543 if let Some(role) = &self.role {
544 params.push(("role".into(), role.clone()));
545 }
546 if let Some(sn) = &self.server_name {
547 params.push(("server_name".into(), sn.clone()));
548 }
549 if let Some(t) = self.connect_timeout_secs {
550 params.push(("connect_timeout".into(), t.to_string()));
551 }
552
553 let userinfo = match (&self.username, &self.password) {
555 (Some(u), Some(p)) => format!("{}:{}@", urlencoding::encode(u), urlencoding::encode(p)),
556 (Some(u), None) => format!("{}@", urlencoding::encode(u)),
557 (None, Some(p)) => {
558 params.push(("pass".into(), p.clone()));
559 String::new()
560 }
561 (None, None) => String::new(),
562 };
563
564 params.sort_by(|a, b| a.0.cmp(&b.0));
565
566 let host = if self.host.contains(':') {
567 format!("[{}]:{}", self.host, self.port)
568 } else {
569 format!("{}:{}", self.host, self.port)
570 };
571
572 write!(f, "{}://{}{}", self.transport, userinfo, host)?;
573
574 if !params.is_empty() {
575 let query: Vec<String> = params
576 .iter()
577 .map(|(k, v)| format!("{}={}", k, urlencoding::encode(v)))
578 .collect();
579 write!(f, "?{}", query.join("&"))?;
580 }
581 Ok(())
582 }
583}
584
585impl std::fmt::Debug for Dsn {
586 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
587 f.debug_struct("Dsn")
588 .field("transport", &self.transport)
589 .field("host", &self.host)
590 .field("port", &self.port)
591 .field("username", &self.username)
592 .field("password", &self.password.as_ref().map(|_| "[REDACTED]"))
593 .field("tls_enabled", &self.tls_enabled)
594 .field("skip_verify", &self.skip_verify)
595 .field("page_size", &self.page_size)
596 .field("client_name", &self.client_name)
597 .field("client_version", &self.client_version)
598 .field("conformance", &self.conformance)
599 .field("ca_cert", &self.ca_cert)
600 .field("client_cert", &self.client_cert)
601 .field("client_key", &self.client_key)
602 .field("server_name", &self.server_name)
603 .field("connect_timeout_secs", &self.connect_timeout_secs)
604 .field("graph", &self.graph)
605 .field("tenant_id", &self.tenant_id)
606 .field("role", &self.role)
607 .field("options", &self.options)
608 .finish()
609 }
610}
611
612fn parse_bool(s: &str) -> Option<bool> {
614 match s.to_lowercase().as_str() {
615 "true" | "1" | "yes" | "on" => Some(true),
616 "false" | "0" | "no" | "off" => Some(false),
617 _ => None,
618 }
619}
620
621#[cfg(test)]
622mod tests {
623 use super::*;
624
625 #[test]
630 fn test_dsn_parse_quic_basic() {
631 let dsn = Dsn::parse("quic://localhost:3141").unwrap();
632 assert_eq!(dsn.transport(), Transport::Quic);
633 assert_eq!(dsn.host(), "localhost");
634 assert_eq!(dsn.port(), 3141);
635 assert!(dsn.tls_enabled());
636 assert!(!dsn.skip_verify());
637 }
638
639 #[test]
640 fn test_dsn_parse_quic_with_ip() {
641 let dsn = Dsn::parse("quic://127.0.0.1:3141").unwrap();
642 assert_eq!(dsn.transport(), Transport::Quic);
643 assert_eq!(dsn.host(), "127.0.0.1");
644 assert_eq!(dsn.port(), 3141);
645 }
646
647 #[test]
648 fn test_dsn_parse_quic_default_port() {
649 let dsn = Dsn::parse("quic://localhost").unwrap();
650 assert_eq!(dsn.port(), DEFAULT_PORT);
651 }
652
653 #[test]
654 fn test_dsn_parse_quic_with_options() {
655 let dsn = Dsn::parse("quic://localhost:3141?page_size=500&insecure=true").unwrap();
656 assert_eq!(dsn.page_size(), 500);
657 assert!(dsn.skip_verify());
658 }
659
660 #[test]
665 fn test_dsn_parse_grpc_basic() {
666 let dsn = Dsn::parse("grpc://localhost:50051").unwrap();
667 assert_eq!(dsn.transport(), Transport::Grpc);
668 assert_eq!(dsn.host(), "localhost");
669 assert_eq!(dsn.port(), 50051);
670 }
671
672 #[test]
673 fn test_dsn_parse_grpc_with_tls_disabled() {
674 let dsn = Dsn::parse("grpc://127.0.0.1:50051?tls=0").unwrap();
675 assert_eq!(dsn.transport(), Transport::Grpc);
676 assert_eq!(dsn.host(), "127.0.0.1");
677 assert_eq!(dsn.port(), 50051);
678 assert!(!dsn.tls_enabled());
679 }
680
681 #[test]
682 fn test_dsn_parse_grpc_with_tls_false() {
683 let dsn = Dsn::parse("grpc://localhost:50051?tls=false").unwrap();
684 assert!(!dsn.tls_enabled());
685 }
686
687 #[test]
688 fn test_dsn_parse_grpcs_scheme() {
689 let dsn = Dsn::parse("grpcs://localhost:50051").unwrap();
690 assert_eq!(dsn.transport(), Transport::Grpc);
691 assert!(dsn.tls_enabled());
692 assert_eq!(dsn.host(), "localhost");
693 assert_eq!(dsn.port(), 50051);
694 }
695
696 #[test]
697 fn test_dsn_parse_grpcs_tls_forced() {
698 let dsn = Dsn::parse("grpcs://localhost:50051?tls=0").unwrap();
700 assert_eq!(dsn.transport(), Transport::Grpc);
701 assert!(dsn.tls_enabled());
702 }
703
704 #[test]
705 fn test_dsn_parse_unsupported_schemes() {
706 let err = Dsn::parse("grcp://localhost:50051").unwrap_err();
708 assert!(err.to_string().contains("Unsupported scheme"));
709
710 let err = Dsn::parse("geode://localhost:3141").unwrap_err();
712 assert!(err.to_string().contains("Unsupported scheme"));
713 }
714
715 #[test]
720 fn test_dsn_parse_ipv6_grpc() {
721 let dsn = Dsn::parse("grpc://[::1]:50051").unwrap();
722 assert_eq!(dsn.transport(), Transport::Grpc);
723 assert_eq!(dsn.host(), "::1");
724 assert_eq!(dsn.port(), 50051);
725 }
726
727 #[test]
728 fn test_dsn_parse_ipv6_quic() {
729 let dsn = Dsn::parse("quic://[::1]:3141").unwrap();
730 assert_eq!(dsn.transport(), Transport::Quic);
731 assert_eq!(dsn.host(), "::1");
732 assert_eq!(dsn.port(), 3141);
733 }
734
735 #[test]
736 fn test_dsn_parse_ipv6_full_address() {
737 let dsn = Dsn::parse("grpc://[2001:db8::1]:50051").unwrap();
738 assert_eq!(dsn.host(), "2001:db8::1");
739 assert_eq!(dsn.port(), 50051);
740 }
741
742 #[test]
743 fn test_dsn_parse_ipv6_default_port() {
744 let dsn = Dsn::parse("quic://[::1]").unwrap();
745 assert_eq!(dsn.host(), "::1");
746 assert_eq!(dsn.port(), DEFAULT_PORT);
747 }
748
749 #[test]
750 fn test_dsn_address_ipv6() {
751 let dsn = Dsn::parse("grpc://[::1]:50051").unwrap();
752 assert_eq!(dsn.address(), "[::1]:50051");
753 }
754
755 #[test]
760 fn test_dsn_parse_legacy_host_port() {
761 let dsn = Dsn::parse("localhost:3141").unwrap();
762 assert_eq!(dsn.transport(), Transport::Quic); assert_eq!(dsn.host(), "localhost");
764 assert_eq!(dsn.port(), 3141);
765 }
766
767 #[test]
768 fn test_dsn_parse_legacy_with_options() {
769 let dsn = Dsn::parse("localhost:3141?insecure=true&page_size=500").unwrap();
770 assert_eq!(dsn.transport(), Transport::Quic);
771 assert!(dsn.skip_verify());
772 assert_eq!(dsn.page_size(), 500);
773 }
774
775 #[test]
776 fn test_dsn_parse_legacy_ipv6() {
777 let dsn = Dsn::parse("[::1]:3141").unwrap();
778 assert_eq!(dsn.host(), "::1");
779 assert_eq!(dsn.port(), 3141);
780 }
781
782 #[test]
787 fn test_dsn_parse_with_auth() {
788 let dsn = Dsn::parse("quic://admin:secret@localhost:3141").unwrap();
789 assert_eq!(dsn.username(), Some("admin"));
790 assert_eq!(dsn.password(), Some("secret"));
791 }
792
793 #[test]
794 fn test_dsn_parse_auth_via_query_params() {
795 let dsn = Dsn::parse("grpc://localhost:50051?username=admin&password=secret").unwrap();
796 assert_eq!(dsn.username(), Some("admin"));
797 assert_eq!(dsn.password(), Some("secret"));
798 }
799
800 #[test]
801 fn test_dsn_parse_auth_percent_encoded() {
802 let dsn = Dsn::parse("quic://user%40domain:p%40ss%3Dword@localhost:3141").unwrap();
803 assert_eq!(dsn.username(), Some("user@domain"));
804 assert_eq!(dsn.password(), Some("p@ss=word"));
805 }
806
807 #[test]
812 fn test_dsn_parse_empty() {
813 let err = Dsn::parse("").unwrap_err();
814 assert!(err.to_string().contains("empty"));
815 }
816
817 #[test]
818 fn test_dsn_parse_unsupported_scheme() {
819 let err = Dsn::parse("http://localhost:3141").unwrap_err();
820 assert!(err.to_string().contains("Unsupported scheme"));
821 assert!(err.to_string().contains("http"));
822 }
823
824 #[test]
825 fn test_dsn_parse_invalid_port() {
826 let err = Dsn::parse("quic://localhost:invalid").unwrap_err();
827 assert!(err.to_string().contains("port") || err.to_string().contains("Invalid"));
828 }
829
830 #[test]
831 fn test_dsn_parse_port_too_large() {
832 let err = Dsn::parse("quic://localhost:99999").unwrap_err();
833 assert!(err.to_string().contains("port") || err.to_string().contains("Invalid"));
834 }
835
836 #[test]
837 fn test_dsn_parse_missing_host() {
838 let result = Dsn::parse("quic://:3141");
841 if let Ok(dsn) = result {
843 assert!(dsn.host().is_empty() || dsn.host() == "");
844 }
845 }
846
847 #[test]
852 fn test_dsn_parse_all_options() {
853 let dsn = Dsn::parse(
854 "grpc://localhost:50051?tls=1&insecure=false&page_size=2000&client_name=test-app&conformance=full"
855 ).unwrap();
856
857 assert!(dsn.tls_enabled());
858 assert!(!dsn.skip_verify());
859 assert_eq!(dsn.page_size(), 2000);
860 assert_eq!(dsn.client_name(), "test-app");
861 assert_eq!(dsn.conformance(), "full");
862 }
863
864 #[test]
865 fn test_dsn_parse_unknown_options_preserved() {
866 let dsn = Dsn::parse("quic://localhost:3141?custom_option=value").unwrap();
867 assert_eq!(
868 dsn.options().get("custom_option"),
869 Some(&"value".to_string())
870 );
871 }
872
873 #[test]
874 fn test_dsn_parse_option_aliases() {
875 let dsn1 = Dsn::parse("grpc://localhost:50051?user=admin").unwrap();
877 let dsn2 = Dsn::parse("grpc://localhost:50051?username=admin").unwrap();
878 assert_eq!(dsn1.username(), dsn2.username());
879
880 let dsn3 = Dsn::parse("grpc://localhost:50051?pass=secret").unwrap();
882 let dsn4 = Dsn::parse("grpc://localhost:50051?password=secret").unwrap();
883 assert_eq!(dsn3.password(), dsn4.password());
884
885 let dsn5 = Dsn::parse("grpc://localhost:50051?skip_verify=true").unwrap();
887 let dsn6 = Dsn::parse("grpc://localhost:50051?insecure=true").unwrap();
888 assert_eq!(dsn5.skip_verify(), dsn6.skip_verify());
889 }
890
891 #[test]
892 fn test_dsn_parse_percent_encoded_values() {
893 let dsn = Dsn::parse("quic://localhost:3141?client_name=My%20App").unwrap();
894 assert_eq!(dsn.client_name(), "My App");
895 }
896
897 #[test]
898 fn test_dsn_parse_mtls_options() {
899 let dsn = Dsn::parse(
900 "grpc://localhost:50051?ca=/path/ca.crt&cert=/path/client.crt&key=/path/client.key",
901 )
902 .unwrap();
903 assert_eq!(dsn.ca_cert(), Some("/path/ca.crt"));
904 assert_eq!(dsn.client_cert(), Some("/path/client.crt"));
905 assert_eq!(dsn.client_key(), Some("/path/client.key"));
906 }
907
908 #[test]
909 fn test_dsn_parse_mtls_options_alt_names() {
910 let dsn = Dsn::parse("grpc://localhost:50051?ca_cert=/path/ca.crt&client_cert=/path/client.crt&client_key=/path/client.key").unwrap();
911 assert_eq!(dsn.ca_cert(), Some("/path/ca.crt"));
912 assert_eq!(dsn.client_cert(), Some("/path/client.crt"));
913 assert_eq!(dsn.client_key(), Some("/path/client.key"));
914 }
915
916 #[test]
917 fn test_dsn_parse_connect_timeout() {
918 let dsn = Dsn::parse("quic://localhost:3141?connect_timeout=60").unwrap();
919 assert_eq!(dsn.connect_timeout_secs(), Some(60));
920 }
921
922 #[test]
923 fn test_dsn_parse_server_name() {
924 let dsn = Dsn::parse("quic://localhost:3141?server_name=geode.example.com").unwrap();
925 assert_eq!(dsn.server_name(), Some("geode.example.com"));
926 }
927
928 #[test]
933 fn test_dsn_parse_insecure_tls_skip_verify_primary() {
934 let dsn = Dsn::parse("quic://localhost:3141?insecure_tls_skip_verify=true").unwrap();
936 assert!(dsn.skip_verify());
937
938 let dsn = Dsn::parse("quic://localhost:3141?insecure_tls_skip_verify=1").unwrap();
939 assert!(dsn.skip_verify());
940
941 let dsn = Dsn::parse("quic://localhost:3141?insecure_tls_skip_verify=yes").unwrap();
942 assert!(dsn.skip_verify());
943
944 let dsn = Dsn::parse("quic://localhost:3141?insecure_tls_skip_verify=on").unwrap();
945 assert!(dsn.skip_verify());
946
947 let dsn = Dsn::parse("quic://localhost:3141?insecure_tls_skip_verify=false").unwrap();
948 assert!(!dsn.skip_verify());
949 }
950
951 #[test]
952 fn test_dsn_parse_insecure_alias() {
953 let dsn = Dsn::parse("quic://localhost:3141?insecure=true").unwrap();
955 assert!(dsn.skip_verify());
956
957 let dsn = Dsn::parse("grpc://localhost:50051?insecure=1").unwrap();
958 assert!(dsn.skip_verify());
959
960 let dsn = Dsn::parse("localhost:3141?insecure=yes").unwrap();
961 assert!(dsn.skip_verify());
962 }
963
964 #[test]
965 fn test_dsn_parse_skip_verify_alias() {
966 let dsn = Dsn::parse("quic://localhost:3141?skip_verify=true").unwrap();
968 assert!(dsn.skip_verify());
969
970 let dsn = Dsn::parse("grpc://localhost:50051?skip_verify=1").unwrap();
971 assert!(dsn.skip_verify());
972
973 let dsn = Dsn::parse("quic://localhost:3141?skip_verify=on").unwrap();
974 assert!(dsn.skip_verify());
975 }
976
977 #[test]
978 fn test_dsn_parse_insecure_skip_verify_alias() {
979 let dsn = Dsn::parse("quic://localhost:3141?insecure_skip_verify=true").unwrap();
981 assert!(dsn.skip_verify());
982
983 let dsn = Dsn::parse("grpc://localhost:50051?insecure_skip_verify=1").unwrap();
984 assert!(dsn.skip_verify());
985
986 let dsn = Dsn::parse("localhost:3141?insecure_skip_verify=on").unwrap();
987 assert!(dsn.skip_verify());
988
989 let dsn = Dsn::parse("quic://localhost:3141?insecure_skip_verify=false").unwrap();
990 assert!(!dsn.skip_verify());
991 }
992
993 #[test]
994 fn test_dsn_skip_verify_default_false() {
995 let dsn = Dsn::parse("quic://localhost:3141").unwrap();
997 assert!(!dsn.skip_verify());
998
999 let dsn = Dsn::parse("grpc://localhost:50051").unwrap();
1000 assert!(!dsn.skip_verify());
1001
1002 let dsn = Dsn::parse("localhost:3141").unwrap();
1003 assert!(!dsn.skip_verify());
1004 }
1005
1006 #[test]
1011 fn test_transport_display() {
1012 assert_eq!(Transport::Quic.to_string(), "quic");
1013 assert_eq!(Transport::Grpc.to_string(), "grpc");
1014 }
1015
1016 #[test]
1021 fn test_dsn_address_ipv4() {
1022 let dsn = Dsn::parse("quic://192.168.1.1:3141").unwrap();
1023 assert_eq!(dsn.address(), "192.168.1.1:3141");
1024 }
1025
1026 #[test]
1027 fn test_dsn_address_hostname() {
1028 let dsn = Dsn::parse("grpc://geode.example.com:50051").unwrap();
1029 assert_eq!(dsn.address(), "geode.example.com:50051");
1030 }
1031
1032 #[test]
1037 fn test_acceptance_quic_localhost() {
1038 let dsn = Dsn::parse("quic://localhost:1234").unwrap();
1040 assert_eq!(dsn.transport(), Transport::Quic);
1041 assert_eq!(dsn.host(), "localhost");
1042 assert_eq!(dsn.port(), 1234);
1043 }
1044
1045 #[test]
1046 fn test_acceptance_grpc_with_tls_disabled() {
1047 let dsn = Dsn::parse("grpc://127.0.0.1:50051?tls=0").unwrap();
1049 assert_eq!(dsn.transport(), Transport::Grpc);
1050 assert_eq!(dsn.host(), "127.0.0.1");
1051 assert_eq!(dsn.port(), 50051);
1052 assert!(!dsn.tls_enabled());
1053 }
1054
1055 #[test]
1056 fn test_acceptance_invalid_scheme() {
1057 let err = Dsn::parse("http://localhost:1").unwrap_err();
1059 let err_str = err.to_string();
1060 assert!(
1061 err_str.contains("Unsupported scheme") || err_str.contains("unsupported"),
1062 "Expected 'unsupported scheme' error, got: {}",
1063 err_str
1064 );
1065 }
1066
1067 #[test]
1072 fn test_dsn_debug_redacts_password() {
1073 let dsn = Dsn::parse("quic://user:secret@localhost:3141").unwrap();
1074 let debug_str = format!("{:?}", dsn);
1075 assert!(
1076 !debug_str.contains("secret"),
1077 "Debug output must not contain password"
1078 );
1079 assert!(
1080 debug_str.contains("REDACTED"),
1081 "Debug output must show REDACTED"
1082 );
1083 }
1084
1085 #[test]
1086 fn test_dsn_parse_tenant_and_role() {
1087 let dsn = Dsn::parse("quic://localhost:3141?tenant_id=acme&role=analyst").unwrap();
1088 assert_eq!(dsn.tenant_id(), Some("acme"));
1089 assert_eq!(dsn.role(), Some("analyst"));
1090 }
1091
1092 #[test]
1093 fn test_dsn_parse_tenant_alias() {
1094 let dsn = Dsn::parse("quic://localhost:3141?tenant=acme").unwrap();
1095 assert_eq!(dsn.tenant_id(), Some("acme"));
1096 }
1097
1098 #[test]
1099 fn test_dsn_tenant_role_default_none() {
1100 let dsn = Dsn::parse("quic://localhost:3141").unwrap();
1101 assert_eq!(dsn.tenant_id(), None);
1102 assert_eq!(dsn.role(), None);
1103 }
1104
1105 #[test]
1106 fn test_dsn_debug_no_password_shows_none() {
1107 let dsn = Dsn::parse("quic://localhost:3141").unwrap();
1108 let debug_str = format!("{:?}", dsn);
1109 assert!(
1110 debug_str.contains("password: None"),
1111 "Debug output should show None when no password is set"
1112 );
1113 }
1114
1115 #[test]
1120 fn test_dsn_format_basic_quic_omits_defaults() {
1121 let dsn = Dsn::parse("quic://localhost:3141").unwrap();
1122 assert_eq!(dsn.to_string(), "quic://localhost:3141");
1124 }
1125
1126 #[test]
1127 fn test_dsn_format_ipv6_bracketed() {
1128 let dsn = Dsn::parse("grpc://[::1]:50051?tls=0").unwrap();
1129 let s = dsn.to_string();
1130 assert!(s.starts_with("grpc://[::1]:50051"), "got {}", s);
1131 assert!(s.contains("tls=0"));
1132 }
1133
1134 #[test]
1135 fn test_dsn_format_params_sorted_and_nondefault() {
1136 let dsn = Dsn::parse(
1137 "quic://localhost:3141?page_size=500&conformance=full&graph=g&role=r&tenant_id=t",
1138 )
1139 .unwrap();
1140 let s = dsn.to_string();
1141 let q = s.split('?').nth(1).unwrap();
1143 let keys: Vec<&str> = q
1144 .split('&')
1145 .map(|kv| kv.split('=').next().unwrap())
1146 .collect();
1147 let mut sorted = keys.clone();
1148 sorted.sort();
1149 assert_eq!(keys, sorted, "params must be alphabetical: {}", q);
1150 assert!(q.contains("page_size=500"));
1151 assert!(q.contains("conformance=full"));
1152 assert!(q.contains("tenant=t"));
1153 assert!(q.contains("role=r"));
1154 }
1155
1156 #[test]
1157 fn test_dsn_format_userinfo() {
1158 let dsn = Dsn::parse("quic://admin:secret@localhost:3141").unwrap();
1159 let s = dsn.to_string();
1160 assert!(s.contains("admin:secret@"), "got {}", s);
1161 }
1162
1163 #[test]
1164 fn test_dsn_format_roundtrip_reparse() {
1165 let original = "grpc://localhost:50051?conformance=full&page_size=200&tls=0";
1166 let dsn = Dsn::parse(original).unwrap();
1167 let formatted = dsn.to_string();
1168 let reparsed = Dsn::parse(&formatted).unwrap();
1169 assert_eq!(reparsed.transport(), dsn.transport());
1170 assert_eq!(reparsed.host(), dsn.host());
1171 assert_eq!(reparsed.port(), dsn.port());
1172 assert_eq!(reparsed.page_size(), dsn.page_size());
1173 assert_eq!(reparsed.conformance(), dsn.conformance());
1174 assert_eq!(reparsed.tls_enabled(), dsn.tls_enabled());
1175 }
1176}