1use std::collections::HashMap;
2use std::fmt;
3use std::str::FromStr;
4use std::sync::Arc;
5
6use camel_api::error::CamelError;
7use camel_auth::oauth2::TokenProvider;
8use camel_component_api::NetworkRetryPolicy;
9use serde::Deserialize;
10use serde::de::{self, MapAccess, Visitor};
11use tracing::error;
12
13#[derive(Debug, Clone, Default, Deserialize, PartialEq)]
18#[non_exhaustive]
19pub struct ClientTlsConfig {
20 #[serde(default)]
21 pub server_name: Option<String>,
22 #[serde(default)]
23 pub ca_cert_path: Option<String>,
24 #[serde(default)]
25 pub client_cert_path: Option<String>,
26 #[serde(default)]
27 pub client_key_path: Option<String>,
28 #[serde(default)]
31 pub insecure_skip_verify: bool,
32}
33
34#[derive(Debug, Clone, Deserialize, PartialEq, Default)]
36pub enum ClientTransport {
37 #[default]
38 Plaintext,
39 Tls(ClientTlsConfig),
40}
41
42#[derive(Debug, Clone, Deserialize, PartialEq)]
45pub struct ServerTlsConfig {
46 pub server_cert_path: String,
47 pub server_key_path: String,
48 #[serde(default)]
52 pub client_ca_path: Option<String>,
53}
54
55#[derive(Debug, Clone, Deserialize, PartialEq, Default)]
56pub enum ServerTransport {
57 #[default]
58 Plaintext,
59 Tls(ServerTlsConfig),
60}
61
62#[derive(Debug, Clone, Deserialize, PartialEq, Default)]
64pub enum TransportIntent {
65 #[default]
66 Plaintext,
67 Tls,
68}
69
70#[derive(Default)]
74#[non_exhaustive]
75pub enum AuthConfig {
76 #[default]
78 None,
79 Bearer { token: String },
81 GoogleServiceAccount { json_path: String },
85 OAuth2 {
87 token_provider: Arc<dyn TokenProvider>,
88 },
89}
90
91impl<'de> Deserialize<'de> for AuthConfig {
92 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
93 where
94 D: de::Deserializer<'de>,
95 {
96 struct AuthConfigVisitor;
97
98 impl<'de> Visitor<'de> for AuthConfigVisitor {
99 type Value = AuthConfig;
100
101 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
102 formatter.write_str("an AuthConfig variant")
103 }
104
105 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
106 where
107 E: de::Error,
108 {
109 match v {
110 "None" => Ok(AuthConfig::None),
111 "OAuth2" => Err(de::Error::custom(
112 "OAuth2 variant cannot be deserialized; construct programmatically",
113 )),
114 other => Err(de::Error::unknown_variant(
115 other,
116 &["None", "Bearer", "GoogleServiceAccount", "OAuth2"],
117 )),
118 }
119 }
120
121 fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
122 where
123 M: MapAccess<'de>,
124 {
125 let variant = map
126 .next_key::<String>()?
127 .ok_or_else(|| de::Error::invalid_length(0, &self))?;
128 match variant.as_str() {
129 "None" => Ok(AuthConfig::None),
130 "Bearer" => {
131 let mut token = None;
132 while let Some(key) = map.next_key::<String>()? {
133 if key == "token" {
134 token = Some(map.next_value()?);
135 } else {
136 let _: de::IgnoredAny = map.next_value()?;
137 }
138 }
139 let token = token.ok_or_else(|| de::Error::missing_field("token"))?;
140 Ok(AuthConfig::Bearer { token })
141 }
142 "GoogleServiceAccount" => {
143 let mut json_path = None;
144 while let Some(key) = map.next_key::<String>()? {
145 if key == "json_path" {
146 json_path = Some(map.next_value()?);
147 } else {
148 let _: de::IgnoredAny = map.next_value()?;
149 }
150 }
151 let json_path =
152 json_path.ok_or_else(|| de::Error::missing_field("json_path"))?;
153 Ok(AuthConfig::GoogleServiceAccount { json_path })
154 }
155 "OAuth2" => Err(de::Error::custom(
156 "OAuth2 variant cannot be deserialized; construct programmatically",
157 )),
158 other => Err(de::Error::unknown_variant(
159 other,
160 &["None", "Bearer", "GoogleServiceAccount"],
161 )),
162 }
163 }
164 }
165
166 deserializer.deserialize_any(AuthConfigVisitor)
167 }
168}
169
170impl fmt::Debug for AuthConfig {
171 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
172 match self {
173 AuthConfig::None => write!(f, "None"), AuthConfig::Bearer { .. } => write!(f, "Bearer {{ token: \"[REDACTED]\" }}"), AuthConfig::GoogleServiceAccount { .. } => {
176 write!(f, "GoogleServiceAccount {{ json_path: \"[REDACTED]\" }}") }
178 AuthConfig::OAuth2 { .. } => write!(f, "OAuth2 {{ token_provider: \"[REDACTED]\" }}"), }
180 }
181}
182
183impl Clone for AuthConfig {
184 fn clone(&self) -> Self {
185 match self {
186 AuthConfig::None => AuthConfig::None,
187 AuthConfig::Bearer { token } => AuthConfig::Bearer {
188 token: token.clone(),
189 },
190 AuthConfig::GoogleServiceAccount { json_path } => AuthConfig::GoogleServiceAccount {
191 json_path: json_path.clone(),
192 },
193 AuthConfig::OAuth2 { token_provider } => AuthConfig::OAuth2 {
194 token_provider: Arc::clone(token_provider),
195 },
196 }
197 }
198}
199
200#[derive(Debug, Clone, Default, Deserialize)]
209#[non_exhaustive]
210pub struct InterceptorConfig {
211 #[serde(default)]
212 pub interceptors: Vec<String>,
213}
214
215#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize)]
219#[serde(rename_all = "camelCase")]
220pub enum ConsumerStrategy {
221 #[default]
222 RoundRobin,
223 First,
224 Last,
225}
226
227impl fmt::Display for ConsumerStrategy {
228 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
229 match self {
230 ConsumerStrategy::RoundRobin => write!(f, "roundRobin"),
231 ConsumerStrategy::First => write!(f, "first"),
232 ConsumerStrategy::Last => write!(f, "last"),
233 }
234 }
235}
236
237impl FromStr for ConsumerStrategy {
238 type Err = CamelError;
239
240 fn from_str(s: &str) -> Result<Self, Self::Err> {
241 match s {
242 "roundRobin" | "round_robin" => Ok(Self::RoundRobin),
243 "first" => Ok(Self::First),
244 "last" => Ok(Self::Last),
245 _ => Err(CamelError::Config(format!("invalid ConsumerStrategy: {s}"))),
246 }
247 }
248}
249
250#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize)]
252#[serde(rename_all = "camelCase")]
253pub enum ProducerStrategy {
254 FireAndForget,
255 #[default]
256 RequestReply,
257}
258
259impl fmt::Display for ProducerStrategy {
260 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
261 match self {
262 ProducerStrategy::FireAndForget => write!(f, "fireAndForget"),
263 ProducerStrategy::RequestReply => write!(f, "requestReply"),
264 }
265 }
266}
267
268impl FromStr for ProducerStrategy {
269 type Err = CamelError;
270
271 fn from_str(s: &str) -> Result<Self, Self::Err> {
272 match s {
273 "fireAndForget" | "fire_and_forget" => Ok(Self::FireAndForget),
274 "requestReply" | "request_reply" => Ok(Self::RequestReply),
275 _ => Err(CamelError::Config(format!("invalid ProducerStrategy: {s}"))),
276 }
277 }
278}
279
280#[derive(Clone, Deserialize)]
283pub struct GrpcConfig {
284 #[serde(rename = "protoFile")]
285 pub proto_file: Option<String>,
286 pub service: Option<String>,
287 pub method: Option<String>,
288 #[serde(default)]
289 pub reflection: bool,
290 #[serde(default)]
291 pub transport_intent: TransportIntent,
292 pub client_transport: ClientTransport,
293 pub server_transport: ServerTransport,
294 #[serde(default = "default_max_msg_len")]
295 pub max_receive_message_length: usize,
296 pub deadline_ms: Option<u64>,
297 pub metadata: Option<String>,
298
299 #[serde(default = "default_connect_timeout_ms")]
301 pub connect_timeout_ms: u64,
302 #[serde(default = "default_deadline_ms")]
303 pub default_deadline_ms: u64,
304
305 #[serde(default)]
307 pub auth: AuthConfig,
308
309 #[serde(default)]
311 pub interceptors: InterceptorConfig,
312
313 #[serde(default)]
315 pub consumer_strategy: ConsumerStrategy,
316 #[serde(default)]
317 pub producer_strategy: ProducerStrategy,
318
319 #[serde(default)]
326 pub retry: NetworkRetryPolicy,
327}
328
329impl fmt::Debug for GrpcConfig {
331 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
332 f.debug_struct("GrpcConfig")
333 .field("proto_file", &self.proto_file)
334 .field("service", &self.service)
335 .field("method", &self.method)
336 .field("reflection", &self.reflection)
337 .field("client_transport", &self.client_transport)
338 .field("server_transport", &self.server_transport)
339 .field(
340 "max_receive_message_length",
341 &self.max_receive_message_length,
342 )
343 .field("deadline_ms", &self.deadline_ms)
344 .field("connect_timeout_ms", &self.connect_timeout_ms)
345 .field("default_deadline_ms", &self.default_deadline_ms)
346 .field("metadata", &self.metadata.as_ref().map(|_| "[REDACTED]"))
347 .field("auth", &self.auth)
348 .field("interceptors", &self.interceptors)
349 .field("consumer_strategy", &self.consumer_strategy)
350 .field("producer_strategy", &self.producer_strategy)
351 .field("retry", &self.retry)
352 .finish()
353 }
354}
355
356fn default_max_msg_len() -> usize {
357 4 * 1024 * 1024
358}
359
360fn default_connect_timeout_ms() -> u64 {
361 10_000
362}
363
364fn default_deadline_ms() -> u64 {
365 30_000
366}
367
368#[derive(Debug, Clone)]
370pub struct GrpcServerConfig {
371 pub max_receive_message_len: Option<usize>,
373 pub transport: ServerTransport,
375}
376
377impl Default for GrpcServerConfig {
378 fn default() -> Self {
379 Self {
380 max_receive_message_len: None,
381 transport: ServerTransport::Plaintext,
382 }
383 }
384}
385
386pub(crate) fn read_tls_file(
389 path: &str,
390 label: &str,
391 runtime: &Arc<dyn camel_component_api::RuntimeObservability>,
392 route_id: &str,
393) -> Result<Vec<u8>, CamelError> {
394 std::fs::read(path).map_err(|e| {
395 runtime.health().force_unhealthy_for_route(
396 route_id,
397 "g:grpc:tls-read",
398 &format!("failed to read {label}: {e}"),
399 );
400 error!(error = %e, "grpc TLS file read failed");
402 CamelError::EndpointCreationFailed(format!("failed to read {label}: {e}"))
403 })
404}
405
406fn parse_bool_param(val: &str) -> Result<bool, CamelError> {
409 match val.to_ascii_lowercase().as_str() {
410 "true" | "1" | "yes" => Ok(true),
411 "false" | "0" | "no" => Ok(false),
412 _ => Err(CamelError::Config(format!("invalid bool value: {val}"))),
413 }
414}
415
416fn parse_numeric_param<T: std::str::FromStr>(val: &str, field: &str) -> Result<T, CamelError>
417where
418 T::Err: std::fmt::Display,
419{
420 val.parse::<T>()
421 .map_err(|e| CamelError::Config(format!("invalid numeric value for {field}: {val} ({e})")))
422}
423
424fn parse_grpc_query_params(
427 pairs: impl Iterator<Item = (String, String)>,
428) -> Result<GrpcConfig, CamelError> {
429 let mut map: HashMap<String, String> = HashMap::new();
430 for (k, v) in pairs {
431 map.insert(k, v);
432 }
433
434 let proto_file = map.remove("protoFile");
435 let service = map.remove("service");
436 let method = map.remove("method");
437 let metadata = map.remove("metadata");
438
439 let reflection = map
440 .remove("reflection")
441 .map(|v| parse_bool_param(&v))
442 .transpose()?
443 .unwrap_or(false);
444
445 let transport_val = map.remove("transport").ok_or_else(|| {
446 CamelError::Config(
447 "gRPC URI missing required 'transport' parameter. \
448 Use transport=plaintext (explicit h2c) or transport=tls \
449 (TLS). Omitted transport is rejected (ADR-0033)."
450 .to_string(),
451 )
452 })?;
453
454 if map.remove("tls").is_some() {
455 return Err(CamelError::Config(
456 "gRPC URI uses legacy 'tls' parameter — removed. \
457 Use transport=plaintext|tls (ADR-0033)."
458 .to_string(),
459 ));
460 }
461
462 let ca_cert_path = map.remove("caCertPath");
463 let client_cert_path = map.remove("clientCertPath");
464 let client_key_path = map.remove("clientKeyPath");
465 let client_server_name = map.remove("serverName");
466 let server_cert_path = map.remove("serverCertPath");
467 let server_key_path = map.remove("serverKeyPath");
468 let client_ca_path = map.remove("clientCaPath");
469
470 let (transport_intent, client_transport, server_transport) = match transport_val.as_str() {
471 "plaintext" => {
472 if [
473 ca_cert_path.as_ref(),
474 client_cert_path.as_ref(),
475 client_key_path.as_ref(),
476 client_server_name.as_ref(),
477 client_ca_path.as_ref(),
478 ]
479 .iter()
480 .any(|v| v.is_some())
481 {
482 return Err(CamelError::Config(
483 "gRPC transport=plaintext with client cert params — conflicting intent. \
484 Remove caCertPath/clientCertPath/clientKeyPath/serverName/clientCaPath."
485 .to_string(),
486 ));
487 }
488 if [server_cert_path.as_ref(), server_key_path.as_ref()]
489 .iter()
490 .any(|v| v.is_some())
491 {
492 return Err(CamelError::Config(
493 "gRPC transport=plaintext with server cert params — conflicting intent. \
494 Remove serverCertPath/serverKeyPath."
495 .to_string(),
496 ));
497 }
498 (
499 TransportIntent::Plaintext,
500 ClientTransport::Plaintext,
501 ServerTransport::Plaintext,
502 )
503 }
504 "tls" => {
505 let client = ClientTransport::Tls(ClientTlsConfig {
506 server_name: client_server_name,
507 ca_cert_path,
508 client_cert_path,
509 client_key_path,
510 insecure_skip_verify: false,
511 });
512 let server = match (server_cert_path, server_key_path) {
513 (Some(cert), Some(key)) => ServerTransport::Tls(ServerTlsConfig {
514 server_cert_path: cert,
515 server_key_path: key,
516 client_ca_path,
517 }),
518 (None, None) => {
519 if client_ca_path.is_some() {
520 return Err(CamelError::Config(
521 "gRPC transport=tls with clientCaPath requires serverCertPath + \
522 serverKeyPath (inbound TLS). clientCaPath cannot apply to \
523 plaintext serve."
524 .to_string(),
525 ));
526 }
527 ServerTransport::Plaintext
528 }
529 _ => {
530 return Err(CamelError::Config(
531 "gRPC transport=tls inbound requires BOTH serverCertPath and \
532 serverKeyPath (or neither for plaintext serve)."
533 .to_string(),
534 ));
535 }
536 };
537 (TransportIntent::Tls, client, server)
538 }
539 other => {
540 return Err(CamelError::Config(format!(
541 "gRPC invalid transport='{other}'. Use transport=plaintext|tls."
542 )));
543 }
544 };
545
546 let max_receive_message_length = map
547 .remove("max_receive_message_length")
548 .map(|v| parse_numeric_param(&v, "max_receive_message_length"))
549 .transpose()?
550 .unwrap_or_else(default_max_msg_len);
551
552 let deadline_ms = map
553 .remove("deadline_ms")
554 .map(|v| parse_numeric_param(&v, "deadline_ms"))
555 .transpose()?;
556
557 let connect_timeout_ms = map
558 .remove("connectTimeoutMs")
559 .map(|v| parse_numeric_param(&v, "connectTimeoutMs"))
560 .transpose()?
561 .unwrap_or_else(default_connect_timeout_ms);
562
563 let default_deadline_ms = map
564 .remove("defaultDeadlineMs")
565 .map(|v| parse_numeric_param(&v, "defaultDeadlineMs"))
566 .transpose()?
567 .unwrap_or_else(default_deadline_ms);
568
569 let auth = if let Some(token) = map.remove("bearerToken") {
571 AuthConfig::Bearer { token }
572 } else if let Some(json_path) = map.remove("googleServiceAccount") {
573 AuthConfig::GoogleServiceAccount { json_path }
574 } else {
575 AuthConfig::None
576 };
577
578 let consumer_strategy = map
580 .remove("consumerStrategy")
581 .map(|v| ConsumerStrategy::from_str(&v))
582 .transpose()?
583 .unwrap_or_default();
584
585 let producer_strategy = map
586 .remove("producerStrategy")
587 .map(|v| ProducerStrategy::from_str(&v))
588 .transpose()?
589 .unwrap_or_default();
590
591 for k in map.keys() {
596 tracing::warn!("unrecognized gRPC URI parameter '{k}' — ignored");
597 }
598
599 Ok(GrpcConfig {
600 proto_file,
601 service,
602 method,
603 reflection,
604 transport_intent,
605 client_transport,
606 server_transport,
607 max_receive_message_length,
608 deadline_ms,
609 metadata,
610 connect_timeout_ms,
611 default_deadline_ms,
612 auth,
613 interceptors: InterceptorConfig::default(),
614 consumer_strategy,
615 producer_strategy,
616 retry: NetworkRetryPolicy::default(),
617 })
618}
619
620pub fn parse_grpc_uri(uri: &str) -> Result<(String, u16, String, String, GrpcConfig), CamelError> {
621 let parsed = url::Url::parse(uri).map_err(|e| CamelError::RouteError(e.to_string()))?;
622 let host = parsed
623 .host_str()
624 .ok_or_else(|| CamelError::RouteError("missing host".to_string()))?
625 .to_string();
626 let port = parsed
627 .port()
628 .ok_or_else(|| CamelError::RouteError("missing port".to_string()))?;
629 let path = parsed.path().trim_start_matches('/');
630 let (service, method) = path.rsplit_once('/').ok_or_else(|| {
631 CamelError::RouteError("URI path must be package.Service/Method".to_string())
632 })?;
633 let config = parse_grpc_query_params(
634 parsed
635 .query_pairs()
636 .map(|(k, v)| (k.to_string(), v.to_string())),
637 )?;
638 if let Some(ref proto) = config.proto_file
639 && (proto.starts_with('/') || proto.contains(".."))
640 {
641 return Err(CamelError::RouteError(format!(
642 "proto path '{}' must be relative and cannot contain '..'",
643 proto
644 )));
645 }
646 if config.reflection {
647 tracing::warn!("gRPC reflection is not supported in v1 — parameter ignored");
648 }
649 Ok((host, port, service.to_string(), method.to_string(), config))
650}
651
652pub fn apply_config_metadata<T>(config: &GrpcConfig, request: &mut tonic::Request<T>) {
657 if let Some(ref metadata_str) = config.metadata {
658 for pair in metadata_str.split(',') {
659 let pair = pair.trim();
660 if let Some((key, value)) = pair.split_once('=') {
661 let key = key.trim();
662 let value = value.trim();
663 if let Ok(name) = tonic::metadata::MetadataKey::from_bytes(key.as_bytes())
664 && let Ok(meta_val) = tonic::metadata::MetadataValue::try_from(value)
665 {
666 request.metadata_mut().insert(name, meta_val);
667 tracing::debug!(key = key, "applied config metadata to gRPC request");
668 }
669 }
670 }
671 }
672}
673
674pub async fn apply_auth_metadata<T>(
680 auth: &AuthConfig,
681 request: &mut tonic::Request<T>,
682) -> Result<(), camel_api::CamelError> {
683 match auth {
684 AuthConfig::Bearer { token } => {
685 if let Ok(name) = tonic::metadata::MetadataKey::from_bytes("authorization".as_bytes()) {
686 let value = format!("Bearer {token}"); if let Ok(meta_val) = tonic::metadata::MetadataValue::try_from(value.as_str()) {
688 request.metadata_mut().insert(name, meta_val);
689 tracing::debug!("applied bearer auth to gRPC request");
690 } else {
691 return Err(camel_api::CamelError::ProcessorError(
692 "bearer token contains invalid characters".into(),
693 ));
694 }
695 }
696 }
697 AuthConfig::OAuth2 { token_provider } => {
698 let token = token_provider.get_token().await.map_err(|e| {
699 let message = format!("failed to acquire OAuth2 token for gRPC producer: {e}"); camel_api::CamelError::ProcessorError(message)
701 })?;
702 if let Ok(name) = tonic::metadata::MetadataKey::from_bytes("authorization".as_bytes()) {
703 let value = format!("Bearer {token}"); if let Ok(meta_val) = tonic::metadata::MetadataValue::try_from(value.as_str()) {
705 request.metadata_mut().insert(name, meta_val);
706 }
707 }
708 }
709 AuthConfig::None | AuthConfig::GoogleServiceAccount { .. } => {}
710 }
711 Ok(())
712}
713
714#[cfg(test)]
715mod tests {
716 use super::*;
717
718 #[test]
719 fn test_parse_grpc_uri_valid() {
720 let uri = "grpc://localhost:50051/com.example.MyService/MyMethod?transport=plaintext";
721 let (host, port, service, method, config) = parse_grpc_uri(uri).unwrap();
722 assert_eq!(host, "localhost");
723 assert_eq!(port, 50051);
724 assert_eq!(service, "com.example.MyService");
725 assert_eq!(method, "MyMethod");
726 assert_eq!(config.max_receive_message_length, 4 * 1024 * 1024);
727 assert!(!config.reflection);
728 assert_eq!(config.transport_intent, TransportIntent::Plaintext);
729 }
730
731 #[test]
732 fn test_parse_grpc_uri_bool_query_params_case_insensitive() {
733 let uri = "grpc://localhost:50051/pkg.Svc/Method?reflection=true&transport=plaintext";
734 let (_, _, _, _, config) = parse_grpc_uri(uri).unwrap();
735 assert!(config.reflection);
736
737 let uri = "grpc://localhost:50051/pkg.Svc/Method?reflection=TRUE&transport=plaintext";
738 let (_, _, _, _, config) = parse_grpc_uri(uri).unwrap();
739 assert!(config.reflection);
740
741 let uri = "grpc://localhost:50051/pkg.Svc/Method?reflection=1&transport=plaintext";
742 let (_, _, _, _, config) = parse_grpc_uri(uri).unwrap();
743 assert!(config.reflection);
744
745 let uri = "grpc://localhost:50051/pkg.Svc/Method?reflection=yes&transport=plaintext";
746 let (_, _, _, _, config) = parse_grpc_uri(uri).unwrap();
747 assert!(config.reflection);
748
749 let uri = "grpc://localhost:50051/pkg.Svc/Method?reflection=false&transport=plaintext";
750 let (_, _, _, _, config) = parse_grpc_uri(uri).unwrap();
751 assert!(!config.reflection);
752
753 let uri = "grpc://localhost:50051/pkg.Svc/Method?reflection=0&transport=plaintext";
754 let (_, _, _, _, config) = parse_grpc_uri(uri).unwrap();
755 assert!(!config.reflection);
756
757 let uri = "grpc://localhost:50051/pkg.Svc/Method?reflection=no&transport=plaintext";
758 let (_, _, _, _, config) = parse_grpc_uri(uri).unwrap();
759 assert!(!config.reflection);
760 }
761
762 #[test]
763 fn test_parse_grpc_uri_bool_query_params_invalid() {
764 let uri = "grpc://localhost:50051/pkg.Svc/Method?reflection=maybe&transport=plaintext";
765 let result = parse_grpc_uri(uri);
766 assert!(result.is_err());
767 assert!(
768 result
769 .unwrap_err()
770 .to_string()
771 .contains("invalid bool value")
772 );
773 }
774
775 #[test]
776 fn test_parse_grpc_uri_with_proto_file() {
777 let uri = "grpc://localhost:50051/pkg.Svc/Method?protoFile=my.proto&transport=plaintext";
778 let (_, _, _, _, config) = parse_grpc_uri(uri).unwrap();
779 assert_eq!(config.proto_file, Some("my.proto".to_string()));
780 }
781
782 #[test]
783 fn test_parse_grpc_uri_numeric_query_params_work() {
784 let uri = "grpc://localhost:50051/pkg.Svc/Method?max_receive_message_length=8388608&transport=plaintext";
785 let (_, _, _, _, config) = parse_grpc_uri(uri).unwrap();
786 assert_eq!(config.max_receive_message_length, 8388608);
787
788 let uri = "grpc://localhost:50051/pkg.Svc/Method?deadline_ms=5000&transport=plaintext";
789 let (_, _, _, _, config) = parse_grpc_uri(uri).unwrap();
790 assert_eq!(config.deadline_ms, Some(5000));
791 }
792
793 #[test]
794 fn test_parse_grpc_uri_connect_timeout_and_default_deadline() {
795 let uri = "grpc://localhost:50051/pkg.Svc/Method?connectTimeoutMs=5000&defaultDeadlineMs=15000&transport=plaintext";
796 let (_, _, _, _, config) = parse_grpc_uri(uri).unwrap();
797 assert_eq!(config.connect_timeout_ms, 5000);
798 assert_eq!(config.default_deadline_ms, 15000);
799 }
800
801 #[test]
802 fn test_parse_grpc_uri_numeric_query_params_invalid() {
803 let uri =
804 "grpc://localhost:50051/pkg.Svc/Method?deadline_ms=notanumber&transport=plaintext";
805 let result = parse_grpc_uri(uri);
806 assert!(result.is_err());
807 assert!(
808 result
809 .unwrap_err()
810 .to_string()
811 .contains("invalid numeric value")
812 );
813 }
814
815 #[test]
816 fn test_parse_grpc_uri_with_metadata() {
817 let uri = "grpc://localhost:50051/pkg.Svc/Method?metadata=some-value&transport=plaintext";
818 let (_, _, _, _, config) = parse_grpc_uri(uri).unwrap();
819 assert_eq!(config.metadata, Some("some-value".to_string()));
820 }
821
822 #[test]
823 fn test_parse_grpc_uri_invalid_uri() {
824 let result = parse_grpc_uri("not-a-valid-uri");
825 assert!(result.is_err());
826 assert!(result.unwrap_err().to_string().contains("relative URL"));
827 }
828
829 #[test]
830 fn test_parse_grpc_uri_missing_host() {
831 let result = parse_grpc_uri("grpc:/pkg.Svc/Method");
832 assert!(result.is_err());
833 let err = result.unwrap_err().to_string();
834 assert!(err.contains("missing host") || err.contains("empty host"));
835 }
836
837 #[test]
838 fn test_parse_grpc_uri_missing_port() {
839 let result = parse_grpc_uri("grpc://localhost/pkg.Svc/Method?transport=plaintext");
840 assert!(result.is_err());
841 assert!(result.unwrap_err().to_string().contains("missing port"));
842 }
843
844 #[test]
845 fn test_parse_grpc_uri_missing_method_separator() {
846 let result = parse_grpc_uri("grpc://localhost:50051/NoSlashHere?transport=plaintext");
847 assert!(result.is_err());
848 assert!(
849 result
850 .unwrap_err()
851 .to_string()
852 .contains("package.Service/Method")
853 );
854 }
855
856 #[test]
857 fn test_parse_grpc_uri_proto_absolute_path_rejected() {
858 let uri = "grpc://localhost:50051/pkg.Svc/Method?protoFile=/etc/passwd&transport=plaintext";
859 let result = parse_grpc_uri(uri);
860 assert!(result.is_err());
861 assert!(result.unwrap_err().to_string().contains("proto path"));
862 }
863
864 #[test]
865 fn test_parse_grpc_uri_proto_traversal_rejected() {
866 let uri =
867 "grpc://localhost:50051/pkg.Svc/Method?protoFile=../secret.proto&transport=plaintext";
868 let result = parse_grpc_uri(uri);
869 assert!(result.is_err());
870 assert!(result.unwrap_err().to_string().contains(".."));
871 }
872
873 #[test]
878 fn test_parse_grpc_uri_tls_true_without_config_rejected() {
879 let uri = "grpc://localhost:50051/pkg.Svc/Method?tls=true&transport=plaintext";
880 let result = parse_grpc_uri(uri);
881 assert!(
882 result.is_err(),
883 "tls=true via URI must fail-closed at parse"
884 );
885 let msg = result.unwrap_err().to_string();
886 assert!(msg.contains("tls"), "error must mention tls: {msg}");
887 }
888
889 #[test]
890 fn test_grpc_config_defaults_via_deserialize() {
891 let result: Result<GrpcConfig, _> = serde_json::from_value(serde_json::json!({}));
893 assert!(
894 result.is_err(),
895 "GrpcConfig without explicit transport must fail (ADR-0033)"
896 );
897
898 let config: GrpcConfig = serde_json::from_value(serde_json::json!({
900 "client_transport": "Plaintext",
901 "server_transport": "Plaintext"
902 }))
903 .unwrap();
904 assert_eq!(config.max_receive_message_length, 4 * 1024 * 1024);
905 assert!(!config.reflection);
906 assert_eq!(config.transport_intent, TransportIntent::Plaintext);
907 assert!(config.proto_file.is_none());
908 assert!(config.service.is_none());
909 assert!(config.method.is_none());
910 assert!(config.deadline_ms.is_none());
911 assert!(config.metadata.is_none());
912 assert_eq!(config.connect_timeout_ms, 10_000);
913 assert_eq!(config.default_deadline_ms, 30_000);
914 }
915
916 #[test]
917 fn test_grpc_config_deserialize_all_fields() {
918 let config: GrpcConfig = serde_json::from_value(serde_json::json!({
919 "protoFile": "test.proto",
920 "service": "MyService",
921 "method": "MyMethod",
922 "reflection": true,
923 "transport_intent": "Tls",
924 "client_transport": {"Tls": {"server_name": "example.com"}},
925 "server_transport": "Plaintext",
926 "max_receive_message_length": 1024,
927 "deadline_ms": 3000,
928 "metadata": "auth-token"
929 }))
930 .unwrap();
931 assert_eq!(config.proto_file, Some("test.proto".to_string()));
932 assert_eq!(config.service, Some("MyService".to_string()));
933 assert_eq!(config.method, Some("MyMethod".to_string()));
934 assert!(config.reflection);
935 assert_eq!(config.transport_intent, TransportIntent::Tls);
936 assert_eq!(config.max_receive_message_length, 1024);
937 assert_eq!(config.deadline_ms, Some(3000));
938 assert_eq!(config.metadata, Some("auth-token".to_string()));
939 }
940
941 #[test]
942 fn test_grpc_config_clone_and_debug() {
943 let config: GrpcConfig = serde_json::from_value(serde_json::json!({
944 "protoFile": "test.proto",
945 "client_transport": "Plaintext",
946 "server_transport": "Plaintext"
947 }))
948 .unwrap();
949 let cloned = config.clone();
950 assert_eq!(config.proto_file, cloned.proto_file);
951 let debug_str = format!("{config:?}");
952 assert!(debug_str.contains("GrpcConfig"));
953 }
954
955 #[test]
958 fn test_server_config_default() {
959 let cfg = GrpcServerConfig::default();
960 assert!(cfg.max_receive_message_len.is_none());
961 assert!(matches!(cfg.transport, ServerTransport::Plaintext));
962 }
963
964 #[test]
965 fn test_server_config_max_receive_message_len_applied() {
966 let cfg = GrpcServerConfig {
967 max_receive_message_len: Some(4096),
968 transport: ServerTransport::Plaintext,
969 };
970 assert_eq!(cfg.max_receive_message_len, Some(4096));
971 }
972
973 #[test]
974 fn test_server_config_clone_and_debug() {
975 let cfg = GrpcServerConfig {
976 max_receive_message_len: Some(8192),
977 transport: ServerTransport::Plaintext,
978 };
979 let cloned = cfg.clone();
980 assert_eq!(cfg.max_receive_message_len, cloned.max_receive_message_len);
981 let debug_str = format!("{cfg:?}");
982 assert!(debug_str.contains("GrpcServerConfig"));
983 }
984
985 #[test]
988 fn test_bool_param_case_insensitive() {
989 assert!(parse_bool_param("True").unwrap());
990 assert!(!parse_bool_param("FALSE").unwrap());
991 assert!(parse_bool_param("1").unwrap());
992 assert!(!parse_bool_param("0").unwrap());
993 assert!(parse_bool_param("yes").unwrap());
994 assert!(!parse_bool_param("no").unwrap());
995 assert!(parse_bool_param("YES").unwrap());
996 assert!(!parse_bool_param("NO").unwrap());
997 }
998
999 #[test]
1000 fn test_bool_param_invalid_values() {
1001 assert!(parse_bool_param("maybe").is_err());
1002 assert!(parse_bool_param("").is_err());
1003 assert!(parse_bool_param("2").is_err());
1004 assert!(parse_bool_param("-1").is_err());
1005 }
1006
1007 #[test]
1010 fn test_apply_config_metadata_single_pair() {
1011 let config: GrpcConfig = serde_json::from_value(serde_json::json!({
1012 "metadata": "x-custom=hello",
1013 "client_transport": "Plaintext",
1014 "server_transport": "Plaintext"
1015 }))
1016 .unwrap();
1017 let mut request = tonic::Request::new(());
1018 apply_config_metadata(&config, &mut request);
1019 assert_eq!(
1020 request
1021 .metadata()
1022 .get("x-custom")
1023 .unwrap()
1024 .to_str()
1025 .unwrap(),
1026 "hello"
1027 );
1028 }
1029
1030 #[test]
1031 fn test_apply_config_metadata_multiple_pairs() {
1032 let config: GrpcConfig = serde_json::from_value(serde_json::json!({
1033 "metadata": "x-a=1, x-b=2",
1034 "client_transport": "Plaintext",
1035 "server_transport": "Plaintext"
1036 }))
1037 .unwrap();
1038 let mut request = tonic::Request::new(());
1039 apply_config_metadata(&config, &mut request);
1040 assert_eq!(
1041 request.metadata().get("x-a").unwrap().to_str().unwrap(),
1042 "1"
1043 );
1044 assert_eq!(
1045 request.metadata().get("x-b").unwrap().to_str().unwrap(),
1046 "2"
1047 );
1048 }
1049
1050 #[test]
1051 fn test_apply_config_metadata_empty_metadata() {
1052 let config: GrpcConfig =
1053 serde_json::from_value(serde_json::json!({"metadata": "", "client_transport": "Plaintext", "server_transport": "Plaintext"})).unwrap();
1054 let mut request = tonic::Request::new(());
1055 apply_config_metadata(&config, &mut request);
1056 assert!(request.metadata().is_empty());
1057 }
1058
1059 #[test]
1062 fn test_client_tls_config_default() {
1063 let tls = ClientTlsConfig::default();
1064 assert!(tls.ca_cert_path.is_none());
1065 assert!(tls.client_cert_path.is_none());
1066 assert!(tls.client_key_path.is_none());
1067 assert!(!tls.insecure_skip_verify);
1068 assert!(tls.server_name.is_none());
1069 }
1070
1071 #[test]
1072 fn test_client_tls_config_deserialize() {
1073 let tls: ClientTlsConfig = serde_json::from_value(serde_json::json!({
1074 "ca_cert_path": "/path/to/ca.pem",
1075 "client_cert_path": "/path/to/client.pem",
1076 "client_key_path": "/path/to/client.key",
1077 "insecure_skip_verify": true,
1078 "server_name": "grpc.example.com"
1079 }))
1080 .unwrap();
1081 assert_eq!(tls.ca_cert_path, Some("/path/to/ca.pem".to_string()));
1082 assert_eq!(
1083 tls.client_cert_path,
1084 Some("/path/to/client.pem".to_string())
1085 );
1086 assert_eq!(tls.client_key_path, Some("/path/to/client.key".to_string()));
1087 assert!(tls.insecure_skip_verify);
1088 assert_eq!(tls.server_name, Some("grpc.example.com".to_string()));
1089 }
1090
1091 #[test]
1092 fn test_client_tls_config_clone_and_debug() {
1093 let tls = ClientTlsConfig {
1094 ca_cert_path: Some("/ca.pem".to_string()),
1095 client_cert_path: None,
1096 client_key_path: None,
1097 insecure_skip_verify: false,
1098 server_name: None,
1099 };
1100 let cloned = tls.clone();
1101 assert_eq!(tls.ca_cert_path, cloned.ca_cert_path);
1102 let debug_str = format!("{tls:?}");
1103 assert!(debug_str.contains("ClientTlsConfig"));
1104 }
1105
1106 #[test]
1109 fn test_auth_config_default_is_none() {
1110 let auth = AuthConfig::default();
1111 assert!(matches!(auth, AuthConfig::None));
1112 }
1113
1114 #[tokio::test]
1115 async fn test_auth_config_bearer_applies_metadata() {
1116 let auth = AuthConfig::Bearer {
1117 token: "my-secret-token".to_string(),
1118 };
1119 let mut request = tonic::Request::new(());
1120 apply_auth_metadata(&auth, &mut request).await.unwrap();
1121 let val = request
1122 .metadata()
1123 .get("authorization")
1124 .unwrap()
1125 .to_str()
1126 .unwrap();
1127 assert_eq!(val, "Bearer my-secret-token");
1128 }
1129
1130 #[tokio::test]
1131 async fn test_auth_config_none_no_metadata() {
1132 let auth = AuthConfig::None;
1133 let mut request = tonic::Request::new(());
1134 apply_auth_metadata(&auth, &mut request).await.unwrap();
1135 assert!(request.metadata().get("authorization").is_none());
1136 }
1137
1138 #[tokio::test]
1139 async fn test_auth_config_google_scaffold_no_metadata() {
1140 let auth = AuthConfig::GoogleServiceAccount {
1141 json_path: "/path/to/sa.json".to_string(),
1142 };
1143 let mut request = tonic::Request::new(());
1144 apply_auth_metadata(&auth, &mut request).await.unwrap();
1145 assert!(request.metadata().get("authorization").is_none());
1146 }
1147
1148 #[tokio::test]
1149 async fn test_auth_config_oauth2_sets_bearer() {
1150 #[derive(Debug)]
1151 struct MockProvider;
1152 #[async_trait::async_trait]
1153 impl camel_auth::TokenProvider for MockProvider {
1154 async fn get_token(&self) -> Result<String, camel_auth::AuthError> {
1155 Ok("mock-oauth2-token".to_string())
1156 }
1157 }
1158 let auth = AuthConfig::OAuth2 {
1159 token_provider: std::sync::Arc::new(MockProvider),
1160 };
1161 let mut request = tonic::Request::new(());
1162 apply_auth_metadata(&auth, &mut request).await.unwrap();
1163 let auth_header = request.metadata().get("authorization").unwrap();
1164 assert_eq!(auth_header, "Bearer mock-oauth2-token");
1165 }
1166
1167 #[tokio::test]
1168 async fn test_auth_config_oauth2_failure_returns_error() {
1169 #[derive(Debug)]
1170 struct FailingProvider;
1171 #[async_trait::async_trait]
1172 impl camel_auth::TokenProvider for FailingProvider {
1173 async fn get_token(&self) -> Result<String, camel_auth::AuthError> {
1174 Err(camel_auth::AuthError::ProviderUnavailable(
1175 "mock failure".into(),
1176 ))
1177 }
1178 }
1179 let auth = AuthConfig::OAuth2 {
1180 token_provider: std::sync::Arc::new(FailingProvider),
1181 };
1182 let mut request = tonic::Request::new(());
1183 let result = apply_auth_metadata(&auth, &mut request).await;
1184 assert!(result.is_err());
1185 assert!(request.metadata().get("authorization").is_none());
1186 }
1187
1188 #[test]
1191 fn test_interceptor_config_default_empty() {
1192 let ic = InterceptorConfig::default();
1193 assert!(ic.interceptors.is_empty());
1194 }
1195
1196 #[test]
1197 fn test_interceptor_config_deserialize() {
1198 let ic: InterceptorConfig = serde_json::from_value(serde_json::json!({
1199 "interceptors": ["logging", "auth"]
1200 }))
1201 .unwrap();
1202 assert_eq!(ic.interceptors.len(), 2);
1203 assert_eq!(ic.interceptors[0], "logging");
1204 assert_eq!(ic.interceptors[1], "auth");
1205 }
1206
1207 #[test]
1210 fn test_consumer_strategy_default() {
1211 assert_eq!(ConsumerStrategy::default(), ConsumerStrategy::RoundRobin);
1212 }
1213
1214 #[test]
1215 fn test_consumer_strategy_from_str() {
1216 assert_eq!(
1217 ConsumerStrategy::from_str("roundRobin").unwrap(),
1218 ConsumerStrategy::RoundRobin
1219 );
1220 assert_eq!(
1221 ConsumerStrategy::from_str("first").unwrap(),
1222 ConsumerStrategy::First
1223 );
1224 assert_eq!(
1225 ConsumerStrategy::from_str("last").unwrap(),
1226 ConsumerStrategy::Last
1227 );
1228 }
1229
1230 #[test]
1231 fn test_consumer_strategy_display() {
1232 assert_eq!(ConsumerStrategy::RoundRobin.to_string(), "roundRobin");
1233 assert_eq!(ConsumerStrategy::First.to_string(), "first");
1234 assert_eq!(ConsumerStrategy::Last.to_string(), "last");
1235 }
1236
1237 #[test]
1238 fn test_consumer_strategy_invalid() {
1239 assert!(ConsumerStrategy::from_str("invalid").is_err());
1240 }
1241
1242 #[test]
1245 fn test_producer_strategy_default() {
1246 assert_eq!(ProducerStrategy::default(), ProducerStrategy::RequestReply);
1247 }
1248
1249 #[test]
1250 fn test_producer_strategy_from_str() {
1251 assert_eq!(
1252 ProducerStrategy::from_str("fireAndForget").unwrap(),
1253 ProducerStrategy::FireAndForget
1254 );
1255 assert_eq!(
1256 ProducerStrategy::from_str("requestReply").unwrap(),
1257 ProducerStrategy::RequestReply
1258 );
1259 }
1260
1261 #[test]
1262 fn test_producer_strategy_display() {
1263 assert_eq!(ProducerStrategy::FireAndForget.to_string(), "fireAndForget");
1264 assert_eq!(ProducerStrategy::RequestReply.to_string(), "requestReply");
1265 }
1266
1267 #[test]
1268 fn test_producer_strategy_invalid() {
1269 assert!(ProducerStrategy::from_str("invalid").is_err());
1270 }
1271
1272 #[test]
1275 fn test_grpc_config_debug_redacts_metadata() {
1276 let config: GrpcConfig = serde_json::from_value(serde_json::json!({
1277 "metadata": "secret-key=value",
1278 "client_transport": "Plaintext",
1279 "server_transport": "Plaintext"
1280 }))
1281 .unwrap();
1282 let debug_str = format!("{config:?}");
1283 assert!(debug_str.contains("[REDACTED]"));
1284 assert!(!debug_str.contains("secret-key=value"));
1285 }
1286
1287 #[test]
1288 fn test_grpc_config_debug_no_redaction_without_secrets() {
1289 let config: GrpcConfig = serde_json::from_value(serde_json::json!({
1290 "protoFile": "test.proto",
1291 "client_transport": "Plaintext",
1292 "server_transport": "Plaintext"
1293 }))
1294 .unwrap();
1295 let debug_str = format!("{config:?}");
1296 assert!(debug_str.contains("test.proto"));
1297 }
1298
1299 #[test]
1300 fn test_auth_config_debug_redacts_bearer_token() {
1301 let auth = AuthConfig::Bearer {
1302 token: "super-secret-token".to_string(),
1303 };
1304 let debug_str = format!("{auth:?}");
1305 assert!(debug_str.contains("[REDACTED]"));
1306 assert!(!debug_str.contains("super-secret-token"));
1307 }
1308
1309 #[test]
1310 fn test_auth_config_debug_redacts_google_json_path() {
1311 let auth = AuthConfig::GoogleServiceAccount {
1312 json_path: "/secret/sa.json".to_string(),
1313 };
1314 let debug_str = format!("{auth:?}");
1315 assert!(debug_str.contains("[REDACTED]"));
1316 assert!(!debug_str.contains("/secret/sa.json"));
1317 }
1318
1319 #[test]
1320 fn test_auth_config_debug_none_is_clean() {
1321 let auth = AuthConfig::None;
1322 let debug_str = format!("{auth:?}");
1323 assert_eq!(debug_str, "None");
1324 }
1325
1326 #[test]
1329 fn test_parse_grpc_uri_bearer_token() {
1330 let uri = "grpc://localhost:50051/pkg.Svc/Method?bearerToken=my-token&transport=plaintext";
1331 let (_, _, _, _, config) = parse_grpc_uri(uri).unwrap();
1332 match config.auth {
1333 AuthConfig::Bearer { ref token } => assert_eq!(token, "my-token"),
1334 _ => panic!("expected Bearer auth"),
1335 }
1336 }
1337
1338 #[test]
1341 fn grpc_config_has_retry_policy_toml() {
1342 let toml_str = r#"
1343 protoFile = "helloworld.proto"
1344 service = "Greeter"
1345 method = "SayHello"
1346 client_transport = "Plaintext"
1347 server_transport = "Plaintext"
1348 [retry]
1349 max_attempts = 3
1350 initial_delay_ms = 500
1351 "#;
1352 let cfg: GrpcConfig = toml::from_str(toml_str).expect("parse");
1353 assert_eq!(cfg.retry.max_attempts, 3);
1354 assert_eq!(
1355 cfg.retry.initial_delay,
1356 std::time::Duration::from_millis(500)
1357 );
1358 }
1359
1360 #[test]
1361 fn grpc_config_retry_defaults_when_not_specified() {
1362 let toml_str = r#"
1363 protoFile = "helloworld.proto"
1364 client_transport = "Plaintext"
1365 server_transport = "Plaintext"
1366 "#;
1367 let cfg: GrpcConfig = toml::from_str(toml_str).expect("parse");
1368 assert_eq!(
1369 cfg.retry,
1370 camel_component_api::NetworkRetryPolicy::default()
1371 );
1372 }
1373
1374 #[test]
1377 fn test_parse_grpc_uri_consumer_strategy() {
1378 let uri =
1379 "grpc://localhost:50051/pkg.Svc/Method?consumerStrategy=first&transport=plaintext";
1380 let (_, _, _, _, config) = parse_grpc_uri(uri).unwrap();
1381 assert_eq!(config.consumer_strategy, ConsumerStrategy::First);
1382 }
1383
1384 #[test]
1385 fn test_parse_grpc_uri_producer_strategy() {
1386 let uri = "grpc://localhost:50051/pkg.Svc/Method?producerStrategy=fireAndForget&transport=plaintext";
1387 let (_, _, _, _, config) = parse_grpc_uri(uri).unwrap();
1388 assert_eq!(config.producer_strategy, ProducerStrategy::FireAndForget);
1389 }
1390
1391 #[test]
1395 fn test_parse_rejects_omitted_transport() {
1396 let res =
1397 parse_grpc_uri("grpc://127.0.0.1:50051/helloworld.Greeter/SayHello?protoFile=a.proto");
1398 assert!(res.is_err(), "omitted transport must fail-closed");
1399 assert!(
1400 res.unwrap_err().to_string().contains("transport"),
1401 "error must mention transport"
1402 );
1403 }
1404
1405 #[test]
1407 fn test_parse_rejects_legacy_tls_key_alone() {
1408 let res = parse_grpc_uri(
1409 "grpc://127.0.0.1:50051/helloworld.Greeter/SayHello?protoFile=a.proto&tls=true",
1410 );
1411 assert!(res.is_err(), "legacy tls= alone must fail");
1412 assert!(
1413 res.unwrap_err().to_string().contains("tls"),
1414 "error must mention tls"
1415 );
1416 }
1417
1418 #[test]
1420 fn test_parse_rejects_client_ca_without_server_certs() {
1421 let res = parse_grpc_uri(
1422 "grpc://127.0.0.1:50051/pkg.Svc/Method?transport=tls&clientCaPath=/ca.pem",
1423 );
1424 assert!(
1425 res.is_err(),
1426 "clientCaPath without serverCertPath/serverKeyPath must fail at parse"
1427 );
1428 let msg = res.unwrap_err().to_string();
1429 assert!(
1430 msg.contains("clientCaPath") || msg.contains("serverCertPath"),
1431 "error must name the missing params: {msg}"
1432 );
1433 }
1434
1435 #[test]
1437 fn test_parse_rejects_tls_with_server_cert_only() {
1438 let res = parse_grpc_uri(
1439 "grpc://127.0.0.1:50051/pkg.Svc/Method?transport=tls&serverCertPath=/c.pem",
1440 );
1441 assert!(
1442 res.is_err(),
1443 "serverCertPath without serverKeyPath must fail"
1444 );
1445 let msg = res.unwrap_err().to_string();
1446 assert!(
1447 msg.contains("serverCertPath") && msg.contains("serverKeyPath"),
1448 "error must name both params: {msg}"
1449 );
1450 }
1451
1452 #[test]
1454 fn test_parse_rejects_plaintext_with_ca_cert() {
1455 let res = parse_grpc_uri(
1456 "grpc://127.0.0.1:50051/pkg.Svc/Method?transport=plaintext&caCertPath=/ca.pem",
1457 );
1458 assert!(res.is_err(), "plaintext + caCertPath must conflict");
1459 assert!(
1460 res.unwrap_err().to_string().contains("conflicting"),
1461 "error must mention conflicting intent"
1462 );
1463 }
1464
1465 #[test]
1467 fn test_parse_rejects_plaintext_with_server_cert() {
1468 let res = parse_grpc_uri(
1469 "grpc://127.0.0.1:50051/pkg.Svc/Method?transport=plaintext&serverCertPath=/cert.pem",
1470 );
1471 assert!(res.is_err(), "plaintext + serverCertPath must conflict");
1472 assert!(
1473 res.unwrap_err().to_string().contains("conflicting"),
1474 "error must mention conflicting intent"
1475 );
1476 }
1477
1478 #[test]
1480 fn test_parse_rejects_plaintext_with_client_cert() {
1481 let res = parse_grpc_uri(
1482 "grpc://127.0.0.1:50051/pkg.Svc/Method?transport=plaintext&clientCertPath=/cert.pem",
1483 );
1484 assert!(res.is_err(), "plaintext + clientCertPath must conflict");
1485 assert!(
1486 res.unwrap_err().to_string().contains("conflicting"),
1487 "error must mention conflicting intent"
1488 );
1489 }
1490
1491 #[test]
1493 fn test_parse_rejects_plaintext_with_client_key() {
1494 let res = parse_grpc_uri(
1495 "grpc://127.0.0.1:50051/pkg.Svc/Method?transport=plaintext&clientKeyPath=/key.pem",
1496 );
1497 assert!(res.is_err(), "plaintext + clientKeyPath must conflict");
1498 assert!(
1499 res.unwrap_err().to_string().contains("conflicting"),
1500 "error must mention conflicting intent"
1501 );
1502 }
1503
1504 #[test]
1506 fn test_parse_rejects_plaintext_with_server_name() {
1507 let res = parse_grpc_uri(
1508 "grpc://127.0.0.1:50051/pkg.Svc/Method?transport=plaintext&serverName=example.com",
1509 );
1510 assert!(res.is_err(), "plaintext + serverName must conflict");
1511 assert!(
1512 res.unwrap_err().to_string().contains("conflicting"),
1513 "error must mention conflicting intent"
1514 );
1515 }
1516
1517 #[test]
1519 fn test_parse_rejects_plaintext_with_client_ca() {
1520 let res = parse_grpc_uri(
1521 "grpc://127.0.0.1:50051/pkg.Svc/Method?transport=plaintext&clientCaPath=/ca.pem",
1522 );
1523 assert!(res.is_err(), "plaintext + clientCaPath must conflict");
1524 assert!(
1525 res.unwrap_err().to_string().contains("conflicting"),
1526 "error must mention conflicting intent"
1527 );
1528 }
1529}