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, v) in &map {
593 tracing::warn!("unrecognized gRPC URI parameter '{k}={v}' — ignored");
594 }
595
596 Ok(GrpcConfig {
597 proto_file,
598 service,
599 method,
600 reflection,
601 transport_intent,
602 client_transport,
603 server_transport,
604 max_receive_message_length,
605 deadline_ms,
606 metadata,
607 connect_timeout_ms,
608 default_deadline_ms,
609 auth,
610 interceptors: InterceptorConfig::default(),
611 consumer_strategy,
612 producer_strategy,
613 retry: NetworkRetryPolicy::default(),
614 })
615}
616
617pub fn parse_grpc_uri(uri: &str) -> Result<(String, u16, String, String, GrpcConfig), CamelError> {
618 let parsed = url::Url::parse(uri).map_err(|e| CamelError::RouteError(e.to_string()))?;
619 let host = parsed
620 .host_str()
621 .ok_or_else(|| CamelError::RouteError("missing host".to_string()))?
622 .to_string();
623 let port = parsed
624 .port()
625 .ok_or_else(|| CamelError::RouteError("missing port".to_string()))?;
626 let path = parsed.path().trim_start_matches('/');
627 let (service, method) = path.rsplit_once('/').ok_or_else(|| {
628 CamelError::RouteError("URI path must be package.Service/Method".to_string())
629 })?;
630 let config = parse_grpc_query_params(
631 parsed
632 .query_pairs()
633 .map(|(k, v)| (k.to_string(), v.to_string())),
634 )?;
635 if let Some(ref proto) = config.proto_file
636 && (proto.starts_with('/') || proto.contains(".."))
637 {
638 return Err(CamelError::RouteError(format!(
639 "proto path '{}' must be relative and cannot contain '..'",
640 proto
641 )));
642 }
643 if config.reflection {
644 tracing::warn!("gRPC reflection is not supported in v1 — parameter ignored");
645 }
646 Ok((host, port, service.to_string(), method.to_string(), config))
647}
648
649pub fn apply_config_metadata<T>(config: &GrpcConfig, request: &mut tonic::Request<T>) {
654 if let Some(ref metadata_str) = config.metadata {
655 for pair in metadata_str.split(',') {
656 let pair = pair.trim();
657 if let Some((key, value)) = pair.split_once('=') {
658 let key = key.trim();
659 let value = value.trim();
660 if let Ok(name) = tonic::metadata::MetadataKey::from_bytes(key.as_bytes())
661 && let Ok(meta_val) = tonic::metadata::MetadataValue::try_from(value)
662 {
663 request.metadata_mut().insert(name, meta_val);
664 tracing::debug!(key = key, "applied config metadata to gRPC request");
665 }
666 }
667 }
668 }
669}
670
671pub async fn apply_auth_metadata<T>(
677 auth: &AuthConfig,
678 request: &mut tonic::Request<T>,
679) -> Result<(), camel_api::CamelError> {
680 match auth {
681 AuthConfig::Bearer { token } => {
682 if let Ok(name) = tonic::metadata::MetadataKey::from_bytes("authorization".as_bytes()) {
683 let value = format!("Bearer {token}"); if let Ok(meta_val) = tonic::metadata::MetadataValue::try_from(value.as_str()) {
685 request.metadata_mut().insert(name, meta_val);
686 tracing::debug!("applied bearer auth to gRPC request");
687 } else {
688 return Err(camel_api::CamelError::ProcessorError(
689 "bearer token contains invalid characters".into(),
690 ));
691 }
692 }
693 }
694 AuthConfig::OAuth2 { token_provider } => {
695 let token = token_provider.get_token().await.map_err(|e| {
696 let message = format!("failed to acquire OAuth2 token for gRPC producer: {e}"); camel_api::CamelError::ProcessorError(message)
698 })?;
699 if let Ok(name) = tonic::metadata::MetadataKey::from_bytes("authorization".as_bytes()) {
700 let value = format!("Bearer {token}"); if let Ok(meta_val) = tonic::metadata::MetadataValue::try_from(value.as_str()) {
702 request.metadata_mut().insert(name, meta_val);
703 }
704 }
705 }
706 AuthConfig::None | AuthConfig::GoogleServiceAccount { .. } => {}
707 }
708 Ok(())
709}
710
711#[cfg(test)]
712mod tests {
713 use super::*;
714
715 #[test]
716 fn test_parse_grpc_uri_valid() {
717 let uri = "grpc://localhost:50051/com.example.MyService/MyMethod?transport=plaintext";
718 let (host, port, service, method, config) = parse_grpc_uri(uri).unwrap();
719 assert_eq!(host, "localhost");
720 assert_eq!(port, 50051);
721 assert_eq!(service, "com.example.MyService");
722 assert_eq!(method, "MyMethod");
723 assert_eq!(config.max_receive_message_length, 4 * 1024 * 1024);
724 assert!(!config.reflection);
725 assert_eq!(config.transport_intent, TransportIntent::Plaintext);
726 }
727
728 #[test]
729 fn test_parse_grpc_uri_bool_query_params_case_insensitive() {
730 let uri = "grpc://localhost:50051/pkg.Svc/Method?reflection=true&transport=plaintext";
731 let (_, _, _, _, config) = parse_grpc_uri(uri).unwrap();
732 assert!(config.reflection);
733
734 let uri = "grpc://localhost:50051/pkg.Svc/Method?reflection=TRUE&transport=plaintext";
735 let (_, _, _, _, config) = parse_grpc_uri(uri).unwrap();
736 assert!(config.reflection);
737
738 let uri = "grpc://localhost:50051/pkg.Svc/Method?reflection=1&transport=plaintext";
739 let (_, _, _, _, config) = parse_grpc_uri(uri).unwrap();
740 assert!(config.reflection);
741
742 let uri = "grpc://localhost:50051/pkg.Svc/Method?reflection=yes&transport=plaintext";
743 let (_, _, _, _, config) = parse_grpc_uri(uri).unwrap();
744 assert!(config.reflection);
745
746 let uri = "grpc://localhost:50051/pkg.Svc/Method?reflection=false&transport=plaintext";
747 let (_, _, _, _, config) = parse_grpc_uri(uri).unwrap();
748 assert!(!config.reflection);
749
750 let uri = "grpc://localhost:50051/pkg.Svc/Method?reflection=0&transport=plaintext";
751 let (_, _, _, _, config) = parse_grpc_uri(uri).unwrap();
752 assert!(!config.reflection);
753
754 let uri = "grpc://localhost:50051/pkg.Svc/Method?reflection=no&transport=plaintext";
755 let (_, _, _, _, config) = parse_grpc_uri(uri).unwrap();
756 assert!(!config.reflection);
757 }
758
759 #[test]
760 fn test_parse_grpc_uri_bool_query_params_invalid() {
761 let uri = "grpc://localhost:50051/pkg.Svc/Method?reflection=maybe&transport=plaintext";
762 let result = parse_grpc_uri(uri);
763 assert!(result.is_err());
764 assert!(
765 result
766 .unwrap_err()
767 .to_string()
768 .contains("invalid bool value")
769 );
770 }
771
772 #[test]
773 fn test_parse_grpc_uri_with_proto_file() {
774 let uri = "grpc://localhost:50051/pkg.Svc/Method?protoFile=my.proto&transport=plaintext";
775 let (_, _, _, _, config) = parse_grpc_uri(uri).unwrap();
776 assert_eq!(config.proto_file, Some("my.proto".to_string()));
777 }
778
779 #[test]
780 fn test_parse_grpc_uri_numeric_query_params_work() {
781 let uri = "grpc://localhost:50051/pkg.Svc/Method?max_receive_message_length=8388608&transport=plaintext";
782 let (_, _, _, _, config) = parse_grpc_uri(uri).unwrap();
783 assert_eq!(config.max_receive_message_length, 8388608);
784
785 let uri = "grpc://localhost:50051/pkg.Svc/Method?deadline_ms=5000&transport=plaintext";
786 let (_, _, _, _, config) = parse_grpc_uri(uri).unwrap();
787 assert_eq!(config.deadline_ms, Some(5000));
788 }
789
790 #[test]
791 fn test_parse_grpc_uri_connect_timeout_and_default_deadline() {
792 let uri = "grpc://localhost:50051/pkg.Svc/Method?connectTimeoutMs=5000&defaultDeadlineMs=15000&transport=plaintext";
793 let (_, _, _, _, config) = parse_grpc_uri(uri).unwrap();
794 assert_eq!(config.connect_timeout_ms, 5000);
795 assert_eq!(config.default_deadline_ms, 15000);
796 }
797
798 #[test]
799 fn test_parse_grpc_uri_numeric_query_params_invalid() {
800 let uri =
801 "grpc://localhost:50051/pkg.Svc/Method?deadline_ms=notanumber&transport=plaintext";
802 let result = parse_grpc_uri(uri);
803 assert!(result.is_err());
804 assert!(
805 result
806 .unwrap_err()
807 .to_string()
808 .contains("invalid numeric value")
809 );
810 }
811
812 #[test]
813 fn test_parse_grpc_uri_with_metadata() {
814 let uri = "grpc://localhost:50051/pkg.Svc/Method?metadata=some-value&transport=plaintext";
815 let (_, _, _, _, config) = parse_grpc_uri(uri).unwrap();
816 assert_eq!(config.metadata, Some("some-value".to_string()));
817 }
818
819 #[test]
820 fn test_parse_grpc_uri_invalid_uri() {
821 let result = parse_grpc_uri("not-a-valid-uri");
822 assert!(result.is_err());
823 assert!(result.unwrap_err().to_string().contains("relative URL"));
824 }
825
826 #[test]
827 fn test_parse_grpc_uri_missing_host() {
828 let result = parse_grpc_uri("grpc:/pkg.Svc/Method");
829 assert!(result.is_err());
830 let err = result.unwrap_err().to_string();
831 assert!(err.contains("missing host") || err.contains("empty host"));
832 }
833
834 #[test]
835 fn test_parse_grpc_uri_missing_port() {
836 let result = parse_grpc_uri("grpc://localhost/pkg.Svc/Method?transport=plaintext");
837 assert!(result.is_err());
838 assert!(result.unwrap_err().to_string().contains("missing port"));
839 }
840
841 #[test]
842 fn test_parse_grpc_uri_missing_method_separator() {
843 let result = parse_grpc_uri("grpc://localhost:50051/NoSlashHere?transport=plaintext");
844 assert!(result.is_err());
845 assert!(
846 result
847 .unwrap_err()
848 .to_string()
849 .contains("package.Service/Method")
850 );
851 }
852
853 #[test]
854 fn test_parse_grpc_uri_proto_absolute_path_rejected() {
855 let uri = "grpc://localhost:50051/pkg.Svc/Method?protoFile=/etc/passwd&transport=plaintext";
856 let result = parse_grpc_uri(uri);
857 assert!(result.is_err());
858 assert!(result.unwrap_err().to_string().contains("proto path"));
859 }
860
861 #[test]
862 fn test_parse_grpc_uri_proto_traversal_rejected() {
863 let uri =
864 "grpc://localhost:50051/pkg.Svc/Method?protoFile=../secret.proto&transport=plaintext";
865 let result = parse_grpc_uri(uri);
866 assert!(result.is_err());
867 assert!(result.unwrap_err().to_string().contains(".."));
868 }
869
870 #[test]
875 fn test_parse_grpc_uri_tls_true_without_config_rejected() {
876 let uri = "grpc://localhost:50051/pkg.Svc/Method?tls=true&transport=plaintext";
877 let result = parse_grpc_uri(uri);
878 assert!(
879 result.is_err(),
880 "tls=true via URI must fail-closed at parse"
881 );
882 let msg = result.unwrap_err().to_string();
883 assert!(msg.contains("tls"), "error must mention tls: {msg}");
884 }
885
886 #[test]
887 fn test_grpc_config_defaults_via_deserialize() {
888 let result: Result<GrpcConfig, _> = serde_json::from_value(serde_json::json!({}));
890 assert!(
891 result.is_err(),
892 "GrpcConfig without explicit transport must fail (ADR-0033)"
893 );
894
895 let config: GrpcConfig = serde_json::from_value(serde_json::json!({
897 "client_transport": "Plaintext",
898 "server_transport": "Plaintext"
899 }))
900 .unwrap();
901 assert_eq!(config.max_receive_message_length, 4 * 1024 * 1024);
902 assert!(!config.reflection);
903 assert_eq!(config.transport_intent, TransportIntent::Plaintext);
904 assert!(config.proto_file.is_none());
905 assert!(config.service.is_none());
906 assert!(config.method.is_none());
907 assert!(config.deadline_ms.is_none());
908 assert!(config.metadata.is_none());
909 assert_eq!(config.connect_timeout_ms, 10_000);
910 assert_eq!(config.default_deadline_ms, 30_000);
911 }
912
913 #[test]
914 fn test_grpc_config_deserialize_all_fields() {
915 let config: GrpcConfig = serde_json::from_value(serde_json::json!({
916 "protoFile": "test.proto",
917 "service": "MyService",
918 "method": "MyMethod",
919 "reflection": true,
920 "transport_intent": "Tls",
921 "client_transport": {"Tls": {"server_name": "example.com"}},
922 "server_transport": "Plaintext",
923 "max_receive_message_length": 1024,
924 "deadline_ms": 3000,
925 "metadata": "auth-token"
926 }))
927 .unwrap();
928 assert_eq!(config.proto_file, Some("test.proto".to_string()));
929 assert_eq!(config.service, Some("MyService".to_string()));
930 assert_eq!(config.method, Some("MyMethod".to_string()));
931 assert!(config.reflection);
932 assert_eq!(config.transport_intent, TransportIntent::Tls);
933 assert_eq!(config.max_receive_message_length, 1024);
934 assert_eq!(config.deadline_ms, Some(3000));
935 assert_eq!(config.metadata, Some("auth-token".to_string()));
936 }
937
938 #[test]
939 fn test_grpc_config_clone_and_debug() {
940 let config: GrpcConfig = serde_json::from_value(serde_json::json!({
941 "protoFile": "test.proto",
942 "client_transport": "Plaintext",
943 "server_transport": "Plaintext"
944 }))
945 .unwrap();
946 let cloned = config.clone();
947 assert_eq!(config.proto_file, cloned.proto_file);
948 let debug_str = format!("{config:?}");
949 assert!(debug_str.contains("GrpcConfig"));
950 }
951
952 #[test]
955 fn test_server_config_default() {
956 let cfg = GrpcServerConfig::default();
957 assert!(cfg.max_receive_message_len.is_none());
958 assert!(matches!(cfg.transport, ServerTransport::Plaintext));
959 }
960
961 #[test]
962 fn test_server_config_max_receive_message_len_applied() {
963 let cfg = GrpcServerConfig {
964 max_receive_message_len: Some(4096),
965 transport: ServerTransport::Plaintext,
966 };
967 assert_eq!(cfg.max_receive_message_len, Some(4096));
968 }
969
970 #[test]
971 fn test_server_config_clone_and_debug() {
972 let cfg = GrpcServerConfig {
973 max_receive_message_len: Some(8192),
974 transport: ServerTransport::Plaintext,
975 };
976 let cloned = cfg.clone();
977 assert_eq!(cfg.max_receive_message_len, cloned.max_receive_message_len);
978 let debug_str = format!("{cfg:?}");
979 assert!(debug_str.contains("GrpcServerConfig"));
980 }
981
982 #[test]
985 fn test_bool_param_case_insensitive() {
986 assert!(parse_bool_param("True").unwrap());
987 assert!(!parse_bool_param("FALSE").unwrap());
988 assert!(parse_bool_param("1").unwrap());
989 assert!(!parse_bool_param("0").unwrap());
990 assert!(parse_bool_param("yes").unwrap());
991 assert!(!parse_bool_param("no").unwrap());
992 assert!(parse_bool_param("YES").unwrap());
993 assert!(!parse_bool_param("NO").unwrap());
994 }
995
996 #[test]
997 fn test_bool_param_invalid_values() {
998 assert!(parse_bool_param("maybe").is_err());
999 assert!(parse_bool_param("").is_err());
1000 assert!(parse_bool_param("2").is_err());
1001 assert!(parse_bool_param("-1").is_err());
1002 }
1003
1004 #[test]
1007 fn test_apply_config_metadata_single_pair() {
1008 let config: GrpcConfig = serde_json::from_value(serde_json::json!({
1009 "metadata": "x-custom=hello",
1010 "client_transport": "Plaintext",
1011 "server_transport": "Plaintext"
1012 }))
1013 .unwrap();
1014 let mut request = tonic::Request::new(());
1015 apply_config_metadata(&config, &mut request);
1016 assert_eq!(
1017 request
1018 .metadata()
1019 .get("x-custom")
1020 .unwrap()
1021 .to_str()
1022 .unwrap(),
1023 "hello"
1024 );
1025 }
1026
1027 #[test]
1028 fn test_apply_config_metadata_multiple_pairs() {
1029 let config: GrpcConfig = serde_json::from_value(serde_json::json!({
1030 "metadata": "x-a=1, x-b=2",
1031 "client_transport": "Plaintext",
1032 "server_transport": "Plaintext"
1033 }))
1034 .unwrap();
1035 let mut request = tonic::Request::new(());
1036 apply_config_metadata(&config, &mut request);
1037 assert_eq!(
1038 request.metadata().get("x-a").unwrap().to_str().unwrap(),
1039 "1"
1040 );
1041 assert_eq!(
1042 request.metadata().get("x-b").unwrap().to_str().unwrap(),
1043 "2"
1044 );
1045 }
1046
1047 #[test]
1048 fn test_apply_config_metadata_empty_metadata() {
1049 let config: GrpcConfig =
1050 serde_json::from_value(serde_json::json!({"metadata": "", "client_transport": "Plaintext", "server_transport": "Plaintext"})).unwrap();
1051 let mut request = tonic::Request::new(());
1052 apply_config_metadata(&config, &mut request);
1053 assert!(request.metadata().is_empty());
1054 }
1055
1056 #[test]
1059 fn test_client_tls_config_default() {
1060 let tls = ClientTlsConfig::default();
1061 assert!(tls.ca_cert_path.is_none());
1062 assert!(tls.client_cert_path.is_none());
1063 assert!(tls.client_key_path.is_none());
1064 assert!(!tls.insecure_skip_verify);
1065 assert!(tls.server_name.is_none());
1066 }
1067
1068 #[test]
1069 fn test_client_tls_config_deserialize() {
1070 let tls: ClientTlsConfig = serde_json::from_value(serde_json::json!({
1071 "ca_cert_path": "/path/to/ca.pem",
1072 "client_cert_path": "/path/to/client.pem",
1073 "client_key_path": "/path/to/client.key",
1074 "insecure_skip_verify": true,
1075 "server_name": "grpc.example.com"
1076 }))
1077 .unwrap();
1078 assert_eq!(tls.ca_cert_path, Some("/path/to/ca.pem".to_string()));
1079 assert_eq!(
1080 tls.client_cert_path,
1081 Some("/path/to/client.pem".to_string())
1082 );
1083 assert_eq!(tls.client_key_path, Some("/path/to/client.key".to_string()));
1084 assert!(tls.insecure_skip_verify);
1085 assert_eq!(tls.server_name, Some("grpc.example.com".to_string()));
1086 }
1087
1088 #[test]
1089 fn test_client_tls_config_clone_and_debug() {
1090 let tls = ClientTlsConfig {
1091 ca_cert_path: Some("/ca.pem".to_string()),
1092 client_cert_path: None,
1093 client_key_path: None,
1094 insecure_skip_verify: false,
1095 server_name: None,
1096 };
1097 let cloned = tls.clone();
1098 assert_eq!(tls.ca_cert_path, cloned.ca_cert_path);
1099 let debug_str = format!("{tls:?}");
1100 assert!(debug_str.contains("ClientTlsConfig"));
1101 }
1102
1103 #[test]
1106 fn test_auth_config_default_is_none() {
1107 let auth = AuthConfig::default();
1108 assert!(matches!(auth, AuthConfig::None));
1109 }
1110
1111 #[tokio::test]
1112 async fn test_auth_config_bearer_applies_metadata() {
1113 let auth = AuthConfig::Bearer {
1114 token: "my-secret-token".to_string(),
1115 };
1116 let mut request = tonic::Request::new(());
1117 apply_auth_metadata(&auth, &mut request).await.unwrap();
1118 let val = request
1119 .metadata()
1120 .get("authorization")
1121 .unwrap()
1122 .to_str()
1123 .unwrap();
1124 assert_eq!(val, "Bearer my-secret-token");
1125 }
1126
1127 #[tokio::test]
1128 async fn test_auth_config_none_no_metadata() {
1129 let auth = AuthConfig::None;
1130 let mut request = tonic::Request::new(());
1131 apply_auth_metadata(&auth, &mut request).await.unwrap();
1132 assert!(request.metadata().get("authorization").is_none());
1133 }
1134
1135 #[tokio::test]
1136 async fn test_auth_config_google_scaffold_no_metadata() {
1137 let auth = AuthConfig::GoogleServiceAccount {
1138 json_path: "/path/to/sa.json".to_string(),
1139 };
1140 let mut request = tonic::Request::new(());
1141 apply_auth_metadata(&auth, &mut request).await.unwrap();
1142 assert!(request.metadata().get("authorization").is_none());
1143 }
1144
1145 #[tokio::test]
1146 async fn test_auth_config_oauth2_sets_bearer() {
1147 #[derive(Debug)]
1148 struct MockProvider;
1149 #[async_trait::async_trait]
1150 impl camel_auth::TokenProvider for MockProvider {
1151 async fn get_token(&self) -> Result<String, camel_auth::AuthError> {
1152 Ok("mock-oauth2-token".to_string())
1153 }
1154 }
1155 let auth = AuthConfig::OAuth2 {
1156 token_provider: std::sync::Arc::new(MockProvider),
1157 };
1158 let mut request = tonic::Request::new(());
1159 apply_auth_metadata(&auth, &mut request).await.unwrap();
1160 let auth_header = request.metadata().get("authorization").unwrap();
1161 assert_eq!(auth_header, "Bearer mock-oauth2-token");
1162 }
1163
1164 #[tokio::test]
1165 async fn test_auth_config_oauth2_failure_returns_error() {
1166 #[derive(Debug)]
1167 struct FailingProvider;
1168 #[async_trait::async_trait]
1169 impl camel_auth::TokenProvider for FailingProvider {
1170 async fn get_token(&self) -> Result<String, camel_auth::AuthError> {
1171 Err(camel_auth::AuthError::ProviderUnavailable(
1172 "mock failure".into(),
1173 ))
1174 }
1175 }
1176 let auth = AuthConfig::OAuth2 {
1177 token_provider: std::sync::Arc::new(FailingProvider),
1178 };
1179 let mut request = tonic::Request::new(());
1180 let result = apply_auth_metadata(&auth, &mut request).await;
1181 assert!(result.is_err());
1182 assert!(request.metadata().get("authorization").is_none());
1183 }
1184
1185 #[test]
1188 fn test_interceptor_config_default_empty() {
1189 let ic = InterceptorConfig::default();
1190 assert!(ic.interceptors.is_empty());
1191 }
1192
1193 #[test]
1194 fn test_interceptor_config_deserialize() {
1195 let ic: InterceptorConfig = serde_json::from_value(serde_json::json!({
1196 "interceptors": ["logging", "auth"]
1197 }))
1198 .unwrap();
1199 assert_eq!(ic.interceptors.len(), 2);
1200 assert_eq!(ic.interceptors[0], "logging");
1201 assert_eq!(ic.interceptors[1], "auth");
1202 }
1203
1204 #[test]
1207 fn test_consumer_strategy_default() {
1208 assert_eq!(ConsumerStrategy::default(), ConsumerStrategy::RoundRobin);
1209 }
1210
1211 #[test]
1212 fn test_consumer_strategy_from_str() {
1213 assert_eq!(
1214 ConsumerStrategy::from_str("roundRobin").unwrap(),
1215 ConsumerStrategy::RoundRobin
1216 );
1217 assert_eq!(
1218 ConsumerStrategy::from_str("first").unwrap(),
1219 ConsumerStrategy::First
1220 );
1221 assert_eq!(
1222 ConsumerStrategy::from_str("last").unwrap(),
1223 ConsumerStrategy::Last
1224 );
1225 }
1226
1227 #[test]
1228 fn test_consumer_strategy_display() {
1229 assert_eq!(ConsumerStrategy::RoundRobin.to_string(), "roundRobin");
1230 assert_eq!(ConsumerStrategy::First.to_string(), "first");
1231 assert_eq!(ConsumerStrategy::Last.to_string(), "last");
1232 }
1233
1234 #[test]
1235 fn test_consumer_strategy_invalid() {
1236 assert!(ConsumerStrategy::from_str("invalid").is_err());
1237 }
1238
1239 #[test]
1242 fn test_producer_strategy_default() {
1243 assert_eq!(ProducerStrategy::default(), ProducerStrategy::RequestReply);
1244 }
1245
1246 #[test]
1247 fn test_producer_strategy_from_str() {
1248 assert_eq!(
1249 ProducerStrategy::from_str("fireAndForget").unwrap(),
1250 ProducerStrategy::FireAndForget
1251 );
1252 assert_eq!(
1253 ProducerStrategy::from_str("requestReply").unwrap(),
1254 ProducerStrategy::RequestReply
1255 );
1256 }
1257
1258 #[test]
1259 fn test_producer_strategy_display() {
1260 assert_eq!(ProducerStrategy::FireAndForget.to_string(), "fireAndForget");
1261 assert_eq!(ProducerStrategy::RequestReply.to_string(), "requestReply");
1262 }
1263
1264 #[test]
1265 fn test_producer_strategy_invalid() {
1266 assert!(ProducerStrategy::from_str("invalid").is_err());
1267 }
1268
1269 #[test]
1272 fn test_grpc_config_debug_redacts_metadata() {
1273 let config: GrpcConfig = serde_json::from_value(serde_json::json!({
1274 "metadata": "secret-key=value",
1275 "client_transport": "Plaintext",
1276 "server_transport": "Plaintext"
1277 }))
1278 .unwrap();
1279 let debug_str = format!("{config:?}");
1280 assert!(debug_str.contains("[REDACTED]"));
1281 assert!(!debug_str.contains("secret-key=value"));
1282 }
1283
1284 #[test]
1285 fn test_grpc_config_debug_no_redaction_without_secrets() {
1286 let config: GrpcConfig = serde_json::from_value(serde_json::json!({
1287 "protoFile": "test.proto",
1288 "client_transport": "Plaintext",
1289 "server_transport": "Plaintext"
1290 }))
1291 .unwrap();
1292 let debug_str = format!("{config:?}");
1293 assert!(debug_str.contains("test.proto"));
1294 }
1295
1296 #[test]
1297 fn test_auth_config_debug_redacts_bearer_token() {
1298 let auth = AuthConfig::Bearer {
1299 token: "super-secret-token".to_string(),
1300 };
1301 let debug_str = format!("{auth:?}");
1302 assert!(debug_str.contains("[REDACTED]"));
1303 assert!(!debug_str.contains("super-secret-token"));
1304 }
1305
1306 #[test]
1307 fn test_auth_config_debug_redacts_google_json_path() {
1308 let auth = AuthConfig::GoogleServiceAccount {
1309 json_path: "/secret/sa.json".to_string(),
1310 };
1311 let debug_str = format!("{auth:?}");
1312 assert!(debug_str.contains("[REDACTED]"));
1313 assert!(!debug_str.contains("/secret/sa.json"));
1314 }
1315
1316 #[test]
1317 fn test_auth_config_debug_none_is_clean() {
1318 let auth = AuthConfig::None;
1319 let debug_str = format!("{auth:?}");
1320 assert_eq!(debug_str, "None");
1321 }
1322
1323 #[test]
1326 fn test_parse_grpc_uri_bearer_token() {
1327 let uri = "grpc://localhost:50051/pkg.Svc/Method?bearerToken=my-token&transport=plaintext";
1328 let (_, _, _, _, config) = parse_grpc_uri(uri).unwrap();
1329 match config.auth {
1330 AuthConfig::Bearer { ref token } => assert_eq!(token, "my-token"),
1331 _ => panic!("expected Bearer auth"),
1332 }
1333 }
1334
1335 #[test]
1338 fn grpc_config_has_retry_policy_toml() {
1339 let toml_str = r#"
1340 protoFile = "helloworld.proto"
1341 service = "Greeter"
1342 method = "SayHello"
1343 client_transport = "Plaintext"
1344 server_transport = "Plaintext"
1345 [retry]
1346 max_attempts = 3
1347 initial_delay_ms = 500
1348 "#;
1349 let cfg: GrpcConfig = toml::from_str(toml_str).expect("parse");
1350 assert_eq!(cfg.retry.max_attempts, 3);
1351 assert_eq!(
1352 cfg.retry.initial_delay,
1353 std::time::Duration::from_millis(500)
1354 );
1355 }
1356
1357 #[test]
1358 fn grpc_config_retry_defaults_when_not_specified() {
1359 let toml_str = r#"
1360 protoFile = "helloworld.proto"
1361 client_transport = "Plaintext"
1362 server_transport = "Plaintext"
1363 "#;
1364 let cfg: GrpcConfig = toml::from_str(toml_str).expect("parse");
1365 assert_eq!(
1366 cfg.retry,
1367 camel_component_api::NetworkRetryPolicy::default()
1368 );
1369 }
1370
1371 #[test]
1374 fn test_parse_grpc_uri_consumer_strategy() {
1375 let uri =
1376 "grpc://localhost:50051/pkg.Svc/Method?consumerStrategy=first&transport=plaintext";
1377 let (_, _, _, _, config) = parse_grpc_uri(uri).unwrap();
1378 assert_eq!(config.consumer_strategy, ConsumerStrategy::First);
1379 }
1380
1381 #[test]
1382 fn test_parse_grpc_uri_producer_strategy() {
1383 let uri = "grpc://localhost:50051/pkg.Svc/Method?producerStrategy=fireAndForget&transport=plaintext";
1384 let (_, _, _, _, config) = parse_grpc_uri(uri).unwrap();
1385 assert_eq!(config.producer_strategy, ProducerStrategy::FireAndForget);
1386 }
1387
1388 #[test]
1392 fn test_parse_rejects_omitted_transport() {
1393 let res =
1394 parse_grpc_uri("grpc://127.0.0.1:50051/helloworld.Greeter/SayHello?protoFile=a.proto");
1395 assert!(res.is_err(), "omitted transport must fail-closed");
1396 assert!(
1397 res.unwrap_err().to_string().contains("transport"),
1398 "error must mention transport"
1399 );
1400 }
1401
1402 #[test]
1404 fn test_parse_rejects_legacy_tls_key_alone() {
1405 let res = parse_grpc_uri(
1406 "grpc://127.0.0.1:50051/helloworld.Greeter/SayHello?protoFile=a.proto&tls=true",
1407 );
1408 assert!(res.is_err(), "legacy tls= alone must fail");
1409 assert!(
1410 res.unwrap_err().to_string().contains("tls"),
1411 "error must mention tls"
1412 );
1413 }
1414
1415 #[test]
1417 fn test_parse_rejects_client_ca_without_server_certs() {
1418 let res = parse_grpc_uri(
1419 "grpc://127.0.0.1:50051/pkg.Svc/Method?transport=tls&clientCaPath=/ca.pem",
1420 );
1421 assert!(
1422 res.is_err(),
1423 "clientCaPath without serverCertPath/serverKeyPath must fail at parse"
1424 );
1425 let msg = res.unwrap_err().to_string();
1426 assert!(
1427 msg.contains("clientCaPath") || msg.contains("serverCertPath"),
1428 "error must name the missing params: {msg}"
1429 );
1430 }
1431
1432 #[test]
1434 fn test_parse_rejects_tls_with_server_cert_only() {
1435 let res = parse_grpc_uri(
1436 "grpc://127.0.0.1:50051/pkg.Svc/Method?transport=tls&serverCertPath=/c.pem",
1437 );
1438 assert!(
1439 res.is_err(),
1440 "serverCertPath without serverKeyPath must fail"
1441 );
1442 let msg = res.unwrap_err().to_string();
1443 assert!(
1444 msg.contains("serverCertPath") && msg.contains("serverKeyPath"),
1445 "error must name both params: {msg}"
1446 );
1447 }
1448
1449 #[test]
1451 fn test_parse_rejects_plaintext_with_ca_cert() {
1452 let res = parse_grpc_uri(
1453 "grpc://127.0.0.1:50051/pkg.Svc/Method?transport=plaintext&caCertPath=/ca.pem",
1454 );
1455 assert!(res.is_err(), "plaintext + caCertPath must conflict");
1456 assert!(
1457 res.unwrap_err().to_string().contains("conflicting"),
1458 "error must mention conflicting intent"
1459 );
1460 }
1461
1462 #[test]
1464 fn test_parse_rejects_plaintext_with_server_cert() {
1465 let res = parse_grpc_uri(
1466 "grpc://127.0.0.1:50051/pkg.Svc/Method?transport=plaintext&serverCertPath=/cert.pem",
1467 );
1468 assert!(res.is_err(), "plaintext + serverCertPath must conflict");
1469 assert!(
1470 res.unwrap_err().to_string().contains("conflicting"),
1471 "error must mention conflicting intent"
1472 );
1473 }
1474
1475 #[test]
1477 fn test_parse_rejects_plaintext_with_client_cert() {
1478 let res = parse_grpc_uri(
1479 "grpc://127.0.0.1:50051/pkg.Svc/Method?transport=plaintext&clientCertPath=/cert.pem",
1480 );
1481 assert!(res.is_err(), "plaintext + clientCertPath must conflict");
1482 assert!(
1483 res.unwrap_err().to_string().contains("conflicting"),
1484 "error must mention conflicting intent"
1485 );
1486 }
1487
1488 #[test]
1490 fn test_parse_rejects_plaintext_with_client_key() {
1491 let res = parse_grpc_uri(
1492 "grpc://127.0.0.1:50051/pkg.Svc/Method?transport=plaintext&clientKeyPath=/key.pem",
1493 );
1494 assert!(res.is_err(), "plaintext + clientKeyPath must conflict");
1495 assert!(
1496 res.unwrap_err().to_string().contains("conflicting"),
1497 "error must mention conflicting intent"
1498 );
1499 }
1500
1501 #[test]
1503 fn test_parse_rejects_plaintext_with_server_name() {
1504 let res = parse_grpc_uri(
1505 "grpc://127.0.0.1:50051/pkg.Svc/Method?transport=plaintext&serverName=example.com",
1506 );
1507 assert!(res.is_err(), "plaintext + serverName must conflict");
1508 assert!(
1509 res.unwrap_err().to_string().contains("conflicting"),
1510 "error must mention conflicting intent"
1511 );
1512 }
1513
1514 #[test]
1516 fn test_parse_rejects_plaintext_with_client_ca() {
1517 let res = parse_grpc_uri(
1518 "grpc://127.0.0.1:50051/pkg.Svc/Method?transport=plaintext&clientCaPath=/ca.pem",
1519 );
1520 assert!(res.is_err(), "plaintext + clientCaPath must conflict");
1521 assert!(
1522 res.unwrap_err().to_string().contains("conflicting"),
1523 "error must mention conflicting intent"
1524 );
1525 }
1526}