1use std::collections::HashMap;
5use std::time::Duration;
6
7use slim_config::grpc::client::ClientConfig as CoreClientConfig;
8use slim_config::grpc::compression::CompressionType as CoreCompressionType;
9use slim_config::grpc::proxy::ProxyConfig as CoreProxyConfig;
10
11use slim_auth::metadata::MetadataMap;
12use slim_config::backoff::exponential::Config as ExponentialBackoffConfig;
13use slim_config::backoff::fixedinterval::Config as FixedIntervalBackoffConfig;
14use slim_config::grpc::client::{
15 BackoffConfig as CoreBackoffConfig, KeepaliveConfig as CoreKeepaliveConfig,
16};
17
18use crate::common_config::{ClientAuthenticationConfig, TlsClientConfig};
19use crate::errors::SlimError;
20
21use slim_config::component::configuration::Configuration;
22
23#[derive(uniffi::Enum, Clone, Debug, PartialEq)]
25pub enum CompressionType {
26 Gzip,
27 Zlib,
28 Deflate,
29 Snappy,
30 Zstd,
31 Lz4,
32 None,
33 Empty,
34}
35
36impl From<CompressionType> for CoreCompressionType {
37 fn from(compression: CompressionType) -> Self {
38 match compression {
39 CompressionType::Gzip => CoreCompressionType::Gzip,
40 CompressionType::Zlib => CoreCompressionType::Zlib,
41 CompressionType::Deflate => CoreCompressionType::Deflate,
42 CompressionType::Snappy => CoreCompressionType::Snappy,
43 CompressionType::Zstd => CoreCompressionType::Zstd,
44 CompressionType::Lz4 => CoreCompressionType::Lz4,
45 CompressionType::None => CoreCompressionType::None,
46 CompressionType::Empty => CoreCompressionType::Empty,
47 }
48 }
49}
50
51impl From<CoreCompressionType> for CompressionType {
52 fn from(compression: CoreCompressionType) -> Self {
53 match compression {
54 CoreCompressionType::Gzip => CompressionType::Gzip,
55 CoreCompressionType::Zlib => CompressionType::Zlib,
56 CoreCompressionType::Deflate => CompressionType::Deflate,
57 CoreCompressionType::Snappy => CompressionType::Snappy,
58 CoreCompressionType::Zstd => CompressionType::Zstd,
59 CoreCompressionType::Lz4 => CompressionType::Lz4,
60 CoreCompressionType::None => CompressionType::None,
61 CoreCompressionType::Empty => CompressionType::Empty,
62 }
63 }
64}
65
66#[derive(uniffi::Record, Clone, Debug, PartialEq)]
68pub struct KeepaliveConfig {
69 pub tcp_keepalive: Duration,
71 pub http2_keepalive: Duration,
73 pub timeout: Duration,
75 pub keep_alive_while_idle: bool,
77}
78
79impl Default for KeepaliveConfig {
80 fn default() -> Self {
81 let core_defaults = CoreKeepaliveConfig::default();
82 KeepaliveConfig {
83 tcp_keepalive: *core_defaults.tcp_keepalive,
84 http2_keepalive: *core_defaults.http2_keepalive,
85 timeout: *core_defaults.timeout,
86 keep_alive_while_idle: core_defaults.keep_alive_while_idle,
87 }
88 }
89}
90
91impl From<KeepaliveConfig> for CoreKeepaliveConfig {
92 fn from(config: KeepaliveConfig) -> Self {
93 CoreKeepaliveConfig {
94 tcp_keepalive: config.tcp_keepalive.into(),
95 http2_keepalive: config.http2_keepalive.into(),
96 timeout: config.timeout.into(),
97 keep_alive_while_idle: config.keep_alive_while_idle,
98 }
99 }
100}
101
102impl From<CoreKeepaliveConfig> for KeepaliveConfig {
103 fn from(config: CoreKeepaliveConfig) -> Self {
104 KeepaliveConfig {
105 tcp_keepalive: *config.tcp_keepalive,
106 http2_keepalive: *config.http2_keepalive,
107 timeout: *config.timeout,
108 keep_alive_while_idle: config.keep_alive_while_idle,
109 }
110 }
111}
112
113#[derive(uniffi::Record, Clone, Debug, PartialEq)]
115pub struct ProxyConfig {
116 pub url: Option<String>,
118 pub tls: TlsClientConfig,
120 pub username: Option<String>,
122 pub password: Option<String>,
124 pub headers: HashMap<String, String>,
126}
127
128impl Default for ProxyConfig {
129 fn default() -> Self {
130 let core_defaults = CoreProxyConfig::default();
131 ProxyConfig {
132 url: core_defaults.url,
133 tls: core_defaults.tls_setting.into(),
134 username: core_defaults.username,
135 password: core_defaults.password,
136 headers: core_defaults.headers,
137 }
138 }
139}
140
141impl From<ProxyConfig> for CoreProxyConfig {
142 fn from(config: ProxyConfig) -> Self {
143 CoreProxyConfig {
144 url: config.url,
145 tls_setting: config.tls.into(),
146 username: config.username,
147 password: config.password,
148 headers: config.headers,
149 }
150 }
151}
152
153impl From<CoreProxyConfig> for ProxyConfig {
154 fn from(config: CoreProxyConfig) -> Self {
155 ProxyConfig {
156 url: config.url,
157 tls: config.tls_setting.into(),
158 username: config.username,
159 password: config.password,
160 headers: config.headers,
161 }
162 }
163}
164
165#[derive(uniffi::Record, Clone, Debug, PartialEq)]
167pub struct ExponentialBackoff {
168 pub base: Duration,
170 pub factor: u64,
172 pub max_delay: Duration,
174 pub max_attempts: u64,
176 pub jitter: bool,
178}
179
180impl Default for ExponentialBackoff {
181 fn default() -> Self {
182 let core_defaults = ExponentialBackoffConfig::default();
183 ExponentialBackoff {
184 base: Duration::from_millis(core_defaults.base),
185 factor: core_defaults.factor,
186 max_delay: *core_defaults.max_delay,
187 max_attempts: core_defaults.max_attempts as u64,
188 jitter: core_defaults.jitter,
189 }
190 }
191}
192
193#[derive(uniffi::Record, Clone, Debug, PartialEq)]
195pub struct FixedIntervalBackoff {
196 pub interval: Duration,
198 pub max_attempts: u64,
200}
201
202impl Default for FixedIntervalBackoff {
203 fn default() -> Self {
204 let core_defaults = FixedIntervalBackoffConfig::default();
205 FixedIntervalBackoff {
206 interval: *core_defaults.interval,
207 max_attempts: core_defaults.max_attempts as u64,
208 }
209 }
210}
211
212#[derive(uniffi::Enum, Clone, Debug, PartialEq)]
214pub enum BackoffConfig {
215 Exponential { config: ExponentialBackoff },
216 FixedInterval { config: FixedIntervalBackoff },
217}
218
219impl From<BackoffConfig> for CoreBackoffConfig {
220 fn from(config: BackoffConfig) -> Self {
221 match config {
222 BackoffConfig::Exponential { config } => {
223 CoreBackoffConfig::Exponential(ExponentialBackoffConfig::new(
224 config.base.as_millis() as u64,
225 config.factor,
226 config.max_delay,
227 config.max_attempts as usize,
228 config.jitter,
229 ))
230 }
231 BackoffConfig::FixedInterval { config } => CoreBackoffConfig::FixedInterval(
232 FixedIntervalBackoffConfig::new(config.interval, config.max_attempts as usize),
233 ),
234 }
235 }
236}
237
238impl From<CoreBackoffConfig> for BackoffConfig {
239 fn from(config: CoreBackoffConfig) -> Self {
240 match config {
241 CoreBackoffConfig::Exponential(core_config) => BackoffConfig::Exponential {
242 config: ExponentialBackoff {
243 base: Duration::from_millis(core_config.base),
244 factor: core_config.factor,
245 max_delay: *core_config.max_delay,
246 max_attempts: core_config.max_attempts as u64,
247 jitter: core_config.jitter,
248 },
249 },
250 CoreBackoffConfig::FixedInterval(core_config) => BackoffConfig::FixedInterval {
251 config: FixedIntervalBackoff {
252 interval: *core_config.interval,
253 max_attempts: core_config.max_attempts as u64,
254 },
255 },
256 }
257 }
258}
259
260#[derive(uniffi::Record, Clone, Debug, PartialEq)]
262pub struct ClientConfig {
263 pub endpoint: String,
268
269 pub tls: TlsClientConfig,
271
272 pub origin: Option<String>,
274
275 pub server_name: Option<String>,
277
278 pub compression: Option<CompressionType>,
280
281 pub rate_limit: Option<String>,
283
284 pub keepalive: Option<KeepaliveConfig>,
286
287 pub proxy: Option<ProxyConfig>,
289
290 pub connect_timeout: Option<Duration>,
292
293 pub request_timeout: Option<Duration>,
295
296 pub buffer_size: Option<u64>,
298
299 pub headers: Option<HashMap<String, String>>,
301
302 pub auth: Option<ClientAuthenticationConfig>,
304
305 pub backoff: Option<BackoffConfig>,
307
308 pub metadata: Option<String>,
310
311 pub require_header_mac: Option<bool>,
313}
314
315impl From<ClientConfig> for CoreClientConfig {
316 fn from(config: ClientConfig) -> Self {
317 let core_defaults = CoreClientConfig::default();
318 CoreClientConfig {
319 endpoint: config.endpoint,
320 origin: config.origin,
321 server_name: config.server_name,
322 compression: config.compression.map(Into::into),
323 rate_limit: config.rate_limit,
324 tls_setting: config.tls.into(),
325 keepalive: config.keepalive.map(Into::into),
326 proxy: config.proxy.map(Into::into).unwrap_or(core_defaults.proxy),
327 connect_timeout: config
328 .connect_timeout
329 .map(Into::into)
330 .unwrap_or(core_defaults.connect_timeout),
331 request_timeout: config
332 .request_timeout
333 .map(Into::into)
334 .unwrap_or(core_defaults.request_timeout),
335 buffer_size: config.buffer_size.map(|s| s as usize),
336 headers: config.headers.unwrap_or(core_defaults.headers),
337 auth: config.auth.map(Into::into).unwrap_or(core_defaults.auth),
338 backoff: config
339 .backoff
340 .map(Into::into)
341 .unwrap_or(core_defaults.backoff),
342 metadata: config
343 .metadata
344 .and_then(|json| serde_json::from_str::<MetadataMap>(&json).ok()),
345 link_id: core_defaults.link_id,
346 require_header_mac: config
347 .require_header_mac
348 .unwrap_or(core_defaults.require_header_mac),
349 connection_type: core_defaults.connection_type,
350 }
351 }
352}
353
354impl From<CoreClientConfig> for ClientConfig {
355 fn from(config: CoreClientConfig) -> Self {
356 ClientConfig {
357 endpoint: config.endpoint,
358 origin: config.origin,
359 server_name: config.server_name,
360 compression: config.compression.map(Into::into),
361 rate_limit: config.rate_limit,
362 tls: config.tls_setting.into(),
363 keepalive: config.keepalive.map(Into::into),
364 proxy: Some(config.proxy.into()),
365 connect_timeout: Some(*config.connect_timeout),
366 request_timeout: Some(*config.request_timeout),
367 buffer_size: config.buffer_size.map(|s| s as u64),
368 headers: Some(config.headers),
369 auth: Some(config.auth.into()),
370 backoff: Some(config.backoff.into()),
371 metadata: config.metadata.and_then(|m| serde_json::to_string(&m).ok()),
372 require_header_mac: Some(config.require_header_mac),
373 }
374 }
375}
376
377impl Default for ClientConfig {
378 fn default() -> Self {
379 let core_defaults = CoreClientConfig::default();
380 Self {
381 endpoint: core_defaults.endpoint,
382 origin: None,
383 server_name: None,
384 compression: None,
385 rate_limit: None,
386 tls: core_defaults.tls_setting.into(),
387 keepalive: None,
388 proxy: None,
389 connect_timeout: None,
390 request_timeout: None,
391 buffer_size: None,
392 headers: None,
393 auth: None,
394 backoff: None,
395 metadata: None,
396 require_header_mac: None,
397 }
398 }
399}
400
401#[uniffi::export]
403pub fn new_insecure_client_config(endpoint: String) -> ClientConfig {
404 ClientConfig {
405 endpoint,
406 tls: TlsClientConfig {
407 insecure: true,
408 ..Default::default()
409 },
410 ..Default::default()
411 }
412}
413
414#[uniffi::export]
416pub fn new_secure_client_config(endpoint: String) -> ClientConfig {
417 ClientConfig {
418 endpoint,
419 tls: TlsClientConfig::default(),
420 ..Default::default()
421 }
422}
423
424#[uniffi::export]
429pub fn new_config_from_json(json: String) -> Result<ClientConfig, SlimError> {
430 let core: CoreClientConfig =
431 serde_json::from_str(&json).map_err(|e| SlimError::ConfigError {
432 message: format!("invalid JSON for client config: {e}"),
433 })?;
434 core.validate().map_err(|e| SlimError::ConfigError {
435 message: e.to_string(),
436 })?;
437 Ok(core.into())
438}
439
440#[cfg(test)]
441mod tests {
442 use super::*;
443 use crate::common_config::{CaSource, TlsSource};
444 use crate::errors::SlimError;
445 use slim_config::transport::TransportProtocol as CoreTransportProtocol;
446 use std::collections::HashMap;
447
448 #[test]
449 fn test_client_config_creation() {
450 let config = ClientConfig {
451 endpoint: "example.com:443".to_string(),
452 origin: None,
453 server_name: None,
454 compression: None,
455 rate_limit: None,
456 tls: TlsClientConfig {
457 insecure: false,
458 insecure_skip_verify: false,
459 source: TlsSource::None,
460 ca_source: CaSource::File {
461 path: "/ca.pem".to_string(),
462 },
463 include_system_ca_certs_pool: true,
464 tls_version: "tls1.2".to_string(),
465 },
466 keepalive: None,
467 proxy: None,
468 connect_timeout: Some(Duration::from_secs(10)),
469 request_timeout: Some(Duration::from_secs(30)),
470 buffer_size: None,
471 headers: None,
472 auth: None,
473 backoff: None,
474 metadata: None,
475 require_header_mac: None,
476 };
477
478 assert_eq!(config.endpoint, "example.com:443");
479 assert_eq!(config.tls.tls_version, "tls1.2");
480 assert!(!config.tls.insecure);
481 }
482
483 #[test]
484 fn test_client_config_default() {
485 let config = ClientConfig::default();
486
487 assert_eq!(config.endpoint, "");
489 assert_eq!(config.origin, None);
490 assert_eq!(config.server_name, None);
491 assert_eq!(config.compression, None);
492 assert_eq!(config.rate_limit, None);
493 assert_eq!(config.tls, TlsClientConfig::default());
494 assert_eq!(config.keepalive, None);
495 assert_eq!(config.proxy, None);
496 assert_eq!(config.connect_timeout, None);
497 assert_eq!(config.request_timeout, None);
498 assert_eq!(config.buffer_size, None);
499 assert_eq!(config.headers, None);
500 assert_eq!(config.auth, None);
501 assert_eq!(config.backoff, None);
502 assert_eq!(config.metadata, None);
503
504 let core: CoreClientConfig = config.into();
506 assert_eq!(*core.connect_timeout, Duration::from_secs(0));
507 assert_eq!(*core.request_timeout, Duration::from_secs(0));
508 assert!(core.headers.is_empty());
509 assert_eq!(
510 core.auth,
511 slim_config::grpc::client::AuthenticationConfig::None
512 );
513 }
514
515 #[test]
516 fn test_client_config_new_insecure() {
517 let config = new_insecure_client_config("localhost:50051".to_string());
518
519 assert_eq!(config.endpoint, "localhost:50051");
520 assert!(config.tls.insecure);
521 }
522
523 #[test]
524 fn test_client_config_new_secure() {
525 let config = new_secure_client_config("api.example.com:443".to_string());
526
527 assert_eq!(config.endpoint, "api.example.com:443");
528 assert!(!config.tls.insecure);
529 assert!(!config.tls.insecure_skip_verify);
530 assert_eq!(config.tls, TlsClientConfig::default());
531 assert_eq!(config.origin, None);
533 assert_eq!(config.keepalive, None);
534 assert_eq!(config.proxy, None);
535 assert_eq!(config.connect_timeout, None);
536 assert_eq!(config.request_timeout, None);
537 assert_eq!(config.auth, None);
538 assert_eq!(config.backoff, None);
539 }
540
541 #[test]
542 fn new_config_from_json_minimal_insecure() {
543 let json = r#"{"endpoint":"http://127.0.0.1:46357","tls":{"insecure":true}}"#;
544 let cfg = new_config_from_json(json.to_string()).expect("parse");
545 assert_eq!(cfg.endpoint, "http://127.0.0.1:46357");
546 assert!(cfg.tls.insecure);
547 }
548
549 #[test]
550 fn new_config_from_json_invalid_json() {
551 let err = new_config_from_json("{not json".to_string()).unwrap_err();
552 assert!(matches!(err, SlimError::ConfigError { .. }));
553 let SlimError::ConfigError { message } = err else {
554 unreachable!();
555 };
556 assert!(message.contains("invalid JSON"), "message was: {message}");
557 }
558
559 #[test]
560 fn new_config_from_json_missing_endpoint() {
561 let json = r#"{"endpoint":"","tls":{"insecure":true}}"#;
562 let err = new_config_from_json(json.to_string()).unwrap_err();
563 assert!(matches!(err, SlimError::ConfigError { .. }));
564 }
565
566 #[test]
567 fn test_client_config_to_core_conversion() {
568 let mut headers = HashMap::new();
569 headers.insert("x-api-key".to_string(), "test-key".to_string());
570
571 let ffi_config = ClientConfig {
572 endpoint: "ws://api.example.com:443".to_string(),
573 origin: Some("example.com".to_string()),
574 server_name: Some("sni.example.com".to_string()),
575 compression: Some(CompressionType::Gzip),
576 rate_limit: Some("100/s".to_string()),
577 tls: TlsClientConfig::default(),
578 keepalive: Some(KeepaliveConfig {
579 tcp_keepalive: Duration::from_secs(60),
580 http2_keepalive: Duration::from_secs(30),
581 timeout: Duration::from_secs(20),
582 keep_alive_while_idle: true,
583 }),
584 proxy: Some(ProxyConfig::default()),
585 connect_timeout: Some(Duration::from_secs(15)),
586 request_timeout: Some(Duration::from_secs(60)),
587 buffer_size: Some(8192),
588 headers: Some(headers.clone()),
589 auth: Some(ClientAuthenticationConfig::None),
590 backoff: Some(BackoffConfig::FixedInterval {
591 config: FixedIntervalBackoff {
592 interval: Duration::from_secs(1),
593 max_attempts: 5,
594 },
595 }),
596 metadata: Some(r#"{"client":"test"}"#.to_string()),
597 require_header_mac: None,
598 };
599
600 let core_config: CoreClientConfig = ffi_config.into();
601
602 assert_eq!(core_config.endpoint, "ws://api.example.com:443");
603 assert_eq!(
604 core_config.resolved_transport(),
605 CoreTransportProtocol::Websocket
606 );
607 assert_eq!(core_config.origin, Some("example.com".to_string()));
608 assert_eq!(core_config.server_name, Some("sni.example.com".to_string()));
609 assert!(core_config.compression.is_some());
610 assert_eq!(core_config.rate_limit, Some("100/s".to_string()));
611 assert!(core_config.keepalive.is_some());
612 assert_eq!(core_config.buffer_size, Some(8192));
613 assert_eq!(core_config.headers.len(), 1);
614 assert!(core_config.metadata.is_some());
615 }
616
617 #[test]
618 fn test_client_config_from_core_conversion() {
619 let core_config = CoreClientConfig::default();
621
622 let ffi_config: ClientConfig = core_config.clone().into();
624
625 assert_eq!(ffi_config.endpoint, core_config.endpoint);
626 assert_eq!(ffi_config.origin, core_config.origin);
627 assert_eq!(ffi_config.server_name, core_config.server_name);
628 assert_eq!(ffi_config.rate_limit, core_config.rate_limit);
629 assert_eq!(
630 ffi_config.connect_timeout,
631 Some(*core_config.connect_timeout)
632 );
633 assert_eq!(
634 ffi_config.request_timeout,
635 Some(*core_config.request_timeout)
636 );
637 }
638
639 #[test]
640 fn test_client_config_roundtrip_conversion() {
641 let original = ClientConfig {
642 endpoint: "localhost:8080".to_string(),
643 origin: Some("test.local".to_string()),
644 server_name: None,
645 compression: Some(CompressionType::Zstd),
646 rate_limit: Some("50/s".to_string()),
647 tls: TlsClientConfig::default(),
648 keepalive: None,
649 proxy: Some(ProxyConfig::default()),
650 connect_timeout: Some(Duration::from_secs(5)),
651 request_timeout: Some(Duration::from_secs(10)),
652 buffer_size: Some(4096),
653 headers: Some(HashMap::new()),
654 auth: Some(ClientAuthenticationConfig::None),
655 backoff: Some(BackoffConfig::Exponential {
656 config: ExponentialBackoff::default(),
657 }),
658 metadata: None,
659 require_header_mac: None,
660 };
661
662 let core: CoreClientConfig = original.clone().into();
664 let roundtrip: ClientConfig = core.into();
665
666 assert_eq!(roundtrip.endpoint, original.endpoint);
667 assert_eq!(roundtrip.origin, original.origin);
668 assert_eq!(roundtrip.rate_limit, original.rate_limit);
669 assert_eq!(roundtrip.buffer_size, original.buffer_size);
670 }
671
672 #[test]
673 fn test_compression_type_conversion() {
674 let compressions = vec![
675 CompressionType::Gzip,
676 CompressionType::Zlib,
677 CompressionType::Deflate,
678 CompressionType::Snappy,
679 CompressionType::Zstd,
680 CompressionType::Lz4,
681 CompressionType::None,
682 CompressionType::Empty,
683 ];
684
685 for compression in compressions {
686 let core: CoreCompressionType = compression.clone().into();
687 let back: CompressionType = core.into();
688 match (compression, back) {
690 (CompressionType::Gzip, CompressionType::Gzip) => {}
691 (CompressionType::Zlib, CompressionType::Zlib) => {}
692 (CompressionType::Deflate, CompressionType::Deflate) => {}
693 (CompressionType::Snappy, CompressionType::Snappy) => {}
694 (CompressionType::Zstd, CompressionType::Zstd) => {}
695 (CompressionType::Lz4, CompressionType::Lz4) => {}
696 (CompressionType::None, CompressionType::None) => {}
697 (CompressionType::Empty, CompressionType::Empty) => {}
698 _ => panic!("Compression roundtrip failed"),
699 }
700 }
701 }
702
703 #[test]
704 fn test_keepalive_conversion() {
705 let ffi_keepalive = KeepaliveConfig {
706 tcp_keepalive: Duration::from_secs(120),
707 http2_keepalive: Duration::from_secs(60),
708 timeout: Duration::from_secs(30),
709 keep_alive_while_idle: false,
710 };
711
712 let core_keepalive: CoreKeepaliveConfig = ffi_keepalive.into();
713
714 assert_eq!(*core_keepalive.tcp_keepalive, Duration::from_secs(120));
715 assert_eq!(*core_keepalive.http2_keepalive, Duration::from_secs(60));
716 assert_eq!(*core_keepalive.timeout, Duration::from_secs(30));
717 assert!(!core_keepalive.keep_alive_while_idle);
718 }
719
720 #[test]
721 fn test_backoff_exponential_conversion() {
722 let ffi_backoff = BackoffConfig::Exponential {
723 config: ExponentialBackoff {
724 base: Duration::from_millis(100),
725 factor: 2,
726 max_delay: Duration::from_secs(60),
727 max_attempts: 10,
728 jitter: true,
729 },
730 };
731
732 let core_backoff: CoreBackoffConfig = ffi_backoff.into();
733
734 match core_backoff {
735 CoreBackoffConfig::Exponential(config) => {
736 assert_eq!(config.base, 100);
737 assert_eq!(config.factor, 2);
738 assert_eq!(*config.max_delay, Duration::from_secs(60));
739 assert_eq!(config.max_attempts, 10);
740 assert!(config.jitter);
741 }
742 _ => panic!("Expected Exponential backoff"),
743 }
744 }
745
746 #[test]
747 fn test_backoff_fixed_interval_conversion() {
748 let ffi_backoff = BackoffConfig::FixedInterval {
749 config: FixedIntervalBackoff {
750 interval: Duration::from_secs(2),
751 max_attempts: 3,
752 },
753 };
754
755 let core_backoff: CoreBackoffConfig = ffi_backoff.into();
756
757 match core_backoff {
758 CoreBackoffConfig::FixedInterval(config) => {
759 assert_eq!(*config.interval, Duration::from_secs(2));
760 assert_eq!(config.max_attempts, 3);
761 }
762 _ => panic!("Expected FixedInterval backoff"),
763 }
764 }
765
766 #[test]
767 fn test_proxy_conversion() {
768 let mut headers = HashMap::new();
769 headers.insert(
770 "Proxy-Authorization".to_string(),
771 "Bearer token".to_string(),
772 );
773
774 let ffi_proxy = ProxyConfig {
775 url: Some("http://proxy.example.com:8080".to_string()),
776 tls: TlsClientConfig::default(),
777 username: Some("user".to_string()),
778 password: Some("pass".to_string()),
779 headers: headers.clone(),
780 };
781
782 let core_proxy: CoreProxyConfig = ffi_proxy.into();
783
784 assert_eq!(
785 core_proxy.url,
786 Some("http://proxy.example.com:8080".to_string())
787 );
788 assert_eq!(core_proxy.username, Some("user".to_string()));
789 assert_eq!(core_proxy.password, Some("pass".to_string()));
790 assert_eq!(core_proxy.headers.len(), 1);
791 }
792
793 #[test]
794 fn test_metadata_serialization() {
795 let config = ClientConfig {
796 endpoint: "test:443".to_string(),
797 tls: TlsClientConfig::default(),
798 metadata: Some(r#"{"env":"production","region":"us-west"}"#.to_string()),
799 ..Default::default()
800 };
801
802 let core: CoreClientConfig = config.into();
803
804 assert!(core.metadata.is_some());
806 let metadata = core.metadata.unwrap();
807 assert_eq!(metadata.len(), 2);
808 }
809
810 #[test]
811 fn test_metadata_invalid_json() {
812 let config = ClientConfig {
813 endpoint: "test:443".to_string(),
814 tls: TlsClientConfig::default(),
815 metadata: Some("not valid json".to_string()),
816 ..Default::default()
817 };
818
819 let core: CoreClientConfig = config.into();
820
821 assert!(core.metadata.is_none());
823 }
824
825 #[test]
826 fn test_jwt_auth_roundtrip() {
827 use crate::identity_config::{
828 ClientJwtAuth, JwtAlgorithm, JwtKeyConfig, JwtKeyData, JwtKeyFormat, JwtKeyType,
829 };
830
831 let jwt_config = ClientJwtAuth {
832 key: JwtKeyType::Encoding {
833 key: JwtKeyConfig {
834 algorithm: JwtAlgorithm::RS256,
835 format: JwtKeyFormat::Pem,
836 key: JwtKeyData::File {
837 path: "/path/to/private_key.pem".to_string(),
838 },
839 },
840 },
841 audience: Some(vec!["api.example.com".to_string()]),
842 issuer: Some("auth.example.com".to_string()),
843 subject: Some("user123".to_string()),
844 duration: Duration::from_secs(7200),
845 };
846
847 let auth = ClientAuthenticationConfig::Jwt {
848 config: jwt_config.clone(),
849 };
850
851 let core_auth: slim_config::grpc::client::AuthenticationConfig = auth.into();
853 let roundtrip_auth: ClientAuthenticationConfig = core_auth.into();
854
855 if let ClientAuthenticationConfig::Jwt { config } = roundtrip_auth {
857 assert_eq!(config.key, jwt_config.key);
858 assert_eq!(config.audience, jwt_config.audience);
859 assert_eq!(config.issuer, jwt_config.issuer);
860 assert_eq!(config.subject, jwt_config.subject);
861 } else {
863 panic!("Expected Jwt authentication config");
864 }
865 }
866
867 #[test]
868 fn test_basic_auth_roundtrip() {
869 use crate::common_config::BasicAuth;
870
871 let basic_config = BasicAuth {
872 username: "admin".to_string(),
873 password: "secret123".to_string(),
874 };
875
876 let auth = ClientAuthenticationConfig::Basic {
877 config: basic_config.clone(),
878 };
879
880 let core_auth: slim_config::grpc::client::AuthenticationConfig = auth.into();
882 let roundtrip_auth: ClientAuthenticationConfig = core_auth.into();
883
884 if let ClientAuthenticationConfig::Basic { config } = roundtrip_auth {
886 assert_eq!(config.username, basic_config.username);
887 assert_eq!(config.password, basic_config.password);
888 } else {
889 panic!("Expected Basic authentication config");
890 }
891 }
892
893 #[test]
894 fn test_static_jwt_auth_roundtrip() {
895 use crate::identity_config::StaticJwtAuth;
896
897 let jwt_config = StaticJwtAuth {
898 token_file: "/path/to/token.jwt".to_string(),
899 duration: Duration::from_secs(1800),
900 };
901
902 let auth = ClientAuthenticationConfig::StaticJwt {
903 config: jwt_config.clone(),
904 };
905
906 let core_auth: slim_config::grpc::client::AuthenticationConfig = auth.into();
908 let roundtrip_auth: ClientAuthenticationConfig = core_auth.into();
909
910 if let ClientAuthenticationConfig::StaticJwt { config } = roundtrip_auth {
912 assert_eq!(config.token_file, jwt_config.token_file);
913 assert_eq!(config.duration, jwt_config.duration);
914 } else {
915 panic!("Expected StaticJwt authentication config");
916 }
917 }
918
919 #[test]
920 fn test_client_config_from_core_with_all_fields() {
921 use slim_config::backoff::exponential::Config as CoreExponentialBackoffConfig;
923 use slim_config::grpc::client::BackoffConfig as CoreBackoffConfig;
924
925 let mut headers = HashMap::new();
926 headers.insert("X-Custom-Header".to_string(), "value".to_string());
927
928 let mut metadata = MetadataMap::new();
929 metadata.insert("service".to_string(), "test-service".to_string());
930 metadata.insert("version".to_string(), "1.0".to_string());
931
932 let core_config = CoreClientConfig {
933 endpoint: "test.example.com:9443".to_string(),
934 origin: Some("origin.example.com".to_string()),
935 server_name: Some("server.example.com".to_string()),
936 rate_limit: Some("100/s".to_string()),
937 buffer_size: Some(8192),
938 headers: headers.clone(),
939 metadata: Some(metadata),
940 backoff: CoreBackoffConfig::Exponential(CoreExponentialBackoffConfig {
941 base: 50,
942 factor: 3,
943 max_delay: Duration::from_secs(120).into(),
944 max_attempts: 5,
945 jitter: true,
946 }),
947 ..Default::default()
948 };
949
950 let ffi_config: ClientConfig = core_config.clone().into();
952
953 assert_eq!(ffi_config.endpoint, "test.example.com:9443");
955 assert_eq!(ffi_config.origin, Some("origin.example.com".to_string()));
956 assert_eq!(
957 ffi_config.server_name,
958 Some("server.example.com".to_string())
959 );
960 assert_eq!(ffi_config.rate_limit, Some("100/s".to_string()));
961 assert_eq!(ffi_config.buffer_size, Some(8192));
962 let headers_map = ffi_config.headers.unwrap();
963 assert_eq!(headers_map.len(), 1);
964 assert_eq!(
965 headers_map.get("X-Custom-Header"),
966 Some(&"value".to_string())
967 );
968
969 assert!(ffi_config.metadata.is_some());
971 let metadata_str = ffi_config.metadata.unwrap();
972 assert!(metadata_str.contains("test-service"));
973 assert!(metadata_str.contains("1.0"));
974 }
975
976 #[test]
977 fn test_client_config_from_core_with_keepalive() {
978 use slim_config::grpc::client::KeepaliveConfig as CoreKeepaliveConfig;
979
980 let core_config = CoreClientConfig {
981 keepalive: Some(CoreKeepaliveConfig {
982 tcp_keepalive: Duration::from_secs(90).into(),
983 http2_keepalive: Duration::from_secs(45).into(),
984 timeout: Duration::from_secs(20).into(),
985 keep_alive_while_idle: true,
986 }),
987 ..Default::default()
988 };
989
990 let ffi_config: ClientConfig = core_config.into();
991
992 let keepalive = ffi_config.keepalive.unwrap();
993 assert_eq!(keepalive.tcp_keepalive, Duration::from_secs(90));
994 assert_eq!(keepalive.http2_keepalive, Duration::from_secs(45));
995 assert_eq!(keepalive.timeout, Duration::from_secs(20));
996 assert!(keepalive.keep_alive_while_idle);
997 }
998
999 #[test]
1000 fn test_client_config_from_core_with_compression() {
1001 use slim_config::grpc::compression::CompressionType as CoreCompressionType;
1002
1003 let compressions = vec![
1004 CoreCompressionType::Gzip,
1005 CoreCompressionType::Zstd,
1006 CoreCompressionType::Snappy,
1007 ];
1008
1009 for core_compression in compressions {
1010 let core_config = CoreClientConfig {
1011 compression: Some(core_compression.clone()),
1012 ..Default::default()
1013 };
1014
1015 let ffi_config: ClientConfig = core_config.into();
1016
1017 assert!(ffi_config.compression.is_some());
1018 }
1019 }
1020
1021 #[test]
1022 fn test_client_config_from_core_with_proxy() {
1023 use slim_config::grpc::proxy::ProxyConfig as CoreProxyConfig;
1024
1025 let mut proxy_headers = HashMap::new();
1026 proxy_headers.insert("Proxy-Auth".to_string(), "token123".to_string());
1027
1028 let core_config = CoreClientConfig {
1029 proxy: CoreProxyConfig {
1030 url: Some("http://proxy.internal:3128".to_string()),
1031 tls_setting: Default::default(),
1032 username: Some("proxy_user".to_string()),
1033 password: Some("proxy_pass".to_string()),
1034 headers: proxy_headers.clone(),
1035 },
1036 ..Default::default()
1037 };
1038
1039 let ffi_config: ClientConfig = core_config.into();
1040
1041 let proxy = ffi_config.proxy.unwrap();
1042 assert_eq!(proxy.url, Some("http://proxy.internal:3128".to_string()));
1043 assert_eq!(proxy.username, Some("proxy_user".to_string()));
1044 assert_eq!(proxy.password, Some("proxy_pass".to_string()));
1045 assert_eq!(proxy.headers.len(), 1);
1046 }
1047
1048 #[test]
1049 fn test_client_config_from_core_buffer_size_conversion() {
1050 let core_config = CoreClientConfig {
1052 buffer_size: Some(16384),
1053 ..Default::default()
1054 };
1055
1056 let ffi_config: ClientConfig = core_config.into();
1057
1058 assert_eq!(ffi_config.buffer_size, Some(16384u64));
1059 }
1060
1061 #[test]
1062 fn test_client_config_from_core_metadata_serialization_failure() {
1063 let core_config = CoreClientConfig {
1066 metadata: None,
1067 ..Default::default()
1068 };
1069
1070 let ffi_config: ClientConfig = core_config.into();
1071
1072 assert!(ffi_config.metadata.is_none());
1073 }
1074
1075 #[test]
1076 fn test_client_config_from_core_fixed_interval_backoff() {
1077 use slim_config::backoff::fixedinterval::Config as CoreFixedIntervalBackoffConfig;
1078 use slim_config::grpc::client::BackoffConfig as CoreBackoffConfig;
1079
1080 let core_config = CoreClientConfig {
1081 backoff: CoreBackoffConfig::FixedInterval(CoreFixedIntervalBackoffConfig {
1082 interval: Duration::from_secs(5).into(),
1083 max_attempts: 8,
1084 }),
1085 ..Default::default()
1086 };
1087
1088 let ffi_config: ClientConfig = core_config.into();
1089
1090 match ffi_config.backoff.unwrap() {
1091 BackoffConfig::FixedInterval { config } => {
1092 assert_eq!(config.interval, Duration::from_secs(5));
1093 assert_eq!(config.max_attempts, 8);
1094 }
1095 _ => panic!("Expected FixedInterval backoff"),
1096 }
1097 }
1098
1099 #[test]
1100 fn test_client_config_from_core_auth_types() {
1101 use slim_config::auth::basic::Config as BasicAuthConfig;
1102 use slim_config::grpc::client::AuthenticationConfig as CoreAuthConfig;
1103
1104 let core_config = CoreClientConfig {
1106 auth: CoreAuthConfig::Basic(BasicAuthConfig::new("test_user", "test_pass")),
1107 ..Default::default()
1108 };
1109
1110 let ffi_config: ClientConfig = core_config.into();
1111
1112 match ffi_config.auth.unwrap() {
1113 ClientAuthenticationConfig::Basic { config } => {
1114 assert_eq!(config.username, "test_user");
1115 assert_eq!(config.password, "test_pass");
1116 }
1117 _ => panic!("Expected Basic auth"),
1118 }
1119 }
1120}