1use serde::{Deserialize, Serialize};
19use thiserror::Error;
20
21use hessra_config::{HessraConfig, Protocol};
22
23#[derive(Error, Debug)]
25pub enum ApiError {
26 #[error("HTTP client error: {0}")]
27 HttpClient(#[from] reqwest::Error),
28
29 #[error("SSL configuration error: {0}")]
30 SslConfig(String),
31
32 #[error("Invalid response: {0}")]
33 InvalidResponse(String),
34
35 #[error("Token request error: {0}")]
36 TokenRequest(String),
37
38 #[error("Token verification error: {0}")]
39 TokenVerification(String),
40
41 #[error("Service chain error: {0}")]
42 ServiceChain(String),
43
44 #[error("Internal error: {0}")]
45 Internal(String),
46
47 #[error("Signoff failed: {0}")]
48 SignoffFailed(String),
49
50 #[error("Missing signoff configuration for service: {0}")]
51 MissingSignoffConfig(String),
52
53 #[error("Invalid signoff response from {service}: {reason}")]
54 InvalidSignoffResponse { service: String, reason: String },
55
56 #[error("Signoff collection incomplete: {missing_signoffs} signoffs remaining")]
57 IncompleteSignoffs { missing_signoffs: usize },
58}
59
60#[derive(Serialize, Deserialize)]
63pub struct TokenRequest {
64 pub resource: String,
66 pub operation: String,
68}
69
70#[derive(Serialize, Deserialize)]
72pub struct VerifyTokenRequest {
73 pub token: String,
75 pub subject: String,
77 pub resource: String,
79 pub operation: String,
81}
82
83#[derive(Serialize, Deserialize, Debug, Clone)]
85pub struct SignoffInfo {
86 pub component: String,
87 pub authorization_service: String,
88 pub public_key: String,
89}
90
91#[derive(Serialize, Deserialize, Debug, Clone)]
93pub struct SignTokenRequest {
94 pub token: String,
95 pub resource: String,
96 pub operation: String,
97}
98
99#[derive(Serialize, Deserialize, Debug, Clone)]
101pub struct SignTokenResponse {
102 pub response_msg: String,
103 pub signed_token: Option<String>,
104}
105
106#[derive(Serialize, Deserialize, Debug, Clone)]
108pub struct TokenResponse {
109 pub response_msg: String,
111 pub token: Option<String>,
113 #[serde(skip_serializing_if = "Option::is_none")]
115 pub pending_signoffs: Option<Vec<SignoffInfo>>,
116}
117
118#[derive(Serialize, Deserialize)]
120pub struct VerifyTokenResponse {
121 pub response_msg: String,
123}
124
125#[derive(Serialize, Deserialize)]
127pub struct PublicKeyResponse {
128 pub response_msg: String,
129 pub public_key: String,
130}
131
132#[derive(Serialize, Deserialize)]
134pub struct VerifyServiceChainTokenRequest {
135 pub token: String,
136 pub subject: String,
137 pub resource: String,
138 pub component: Option<String>,
139}
140
141#[derive(Serialize, Deserialize)]
143pub struct IdentityTokenRequest {
144 pub identifier: Option<String>,
146}
147
148#[derive(Serialize, Deserialize)]
150pub struct RefreshIdentityTokenRequest {
151 pub current_token: String,
153 pub identifier: Option<String>,
155}
156
157#[derive(Serialize, Deserialize, Debug, Clone)]
159pub struct IdentityTokenResponse {
160 pub response_msg: String,
162 pub token: Option<String>,
164 pub expires_in: Option<u64>,
166 pub identity: Option<String>,
168}
169
170#[derive(Clone)]
172pub struct BaseConfig {
173 pub base_url: String,
175 pub port: Option<u16>,
177 pub mtls_key: Option<String>,
179 pub mtls_cert: Option<String>,
181 pub server_ca: String,
183 pub public_key: Option<String>,
185 pub personal_keypair: Option<String>,
187}
188
189impl BaseConfig {
190 pub fn get_base_url(&self) -> String {
192 match self.port {
193 Some(port) => format!("{}:{port}", self.base_url),
194 None => self.base_url.clone(),
195 }
196 }
197}
198
199pub struct Http1Client {
201 config: BaseConfig,
203 client: reqwest::Client,
205}
206
207impl Http1Client {
208 pub fn new(config: BaseConfig) -> Result<Self, ApiError> {
210 let cert_der = reqwest::Certificate::from_pem(config.server_ca.as_bytes())
212 .map_err(|e| ApiError::SslConfig(format!("Failed to parse CA certificate: {e}")))?;
213
214 let mut client_builder = reqwest::ClientBuilder::new()
216 .use_rustls_tls()
217 .add_root_certificate(cert_der);
218
219 if let (Some(cert), Some(key)) = (&config.mtls_cert, &config.mtls_key) {
221 let identity_str = format!("{cert}{key}");
222 let identity = reqwest::Identity::from_pem(identity_str.as_bytes()).map_err(|e| {
223 ApiError::SslConfig(format!(
224 "Failed to create identity from certificate and key: {e}"
225 ))
226 })?;
227 client_builder = client_builder.identity(identity);
228 }
229
230 let client = client_builder
231 .build()
232 .map_err(|e| ApiError::SslConfig(e.to_string()))?;
233
234 Ok(Self { config, client })
235 }
236
237 pub async fn send_request<T, R>(&self, endpoint: &str, request_body: &T) -> Result<R, ApiError>
239 where
240 T: Serialize,
241 R: for<'de> Deserialize<'de>,
242 {
243 let base_url = self.config.get_base_url();
244 let url = format!("https://{base_url}/{endpoint}");
245
246 let response = self
247 .client
248 .post(&url)
249 .json(request_body)
250 .send()
251 .await
252 .map_err(ApiError::HttpClient)?;
253
254 if !response.status().is_success() {
255 let status = response.status();
256 let error_text = response.text().await.unwrap_or_default();
257 return Err(ApiError::InvalidResponse(format!(
258 "HTTP error: {status} - {error_text}"
259 )));
260 }
261
262 let result = response
263 .json::<R>()
264 .await
265 .map_err(|e| ApiError::InvalidResponse(format!("Failed to parse response: {e}")))?;
266
267 Ok(result)
268 }
269
270 pub async fn send_request_with_auth<T, R>(
271 &self,
272 endpoint: &str,
273 request_body: &T,
274 auth_header: &str,
275 ) -> Result<R, ApiError>
276 where
277 T: Serialize,
278 R: for<'de> Deserialize<'de>,
279 {
280 let base_url = self.config.get_base_url();
281 let url = format!("https://{base_url}/{endpoint}");
282
283 let response = self
284 .client
285 .post(&url)
286 .header("Authorization", auth_header)
287 .json(request_body)
288 .send()
289 .await
290 .map_err(ApiError::HttpClient)?;
291
292 if !response.status().is_success() {
293 let status = response.status();
294 let error_text = response.text().await.unwrap_or_default();
295 return Err(ApiError::InvalidResponse(format!(
296 "HTTP error: {status} - {error_text}"
297 )));
298 }
299
300 let result = response
301 .json::<R>()
302 .await
303 .map_err(|e| ApiError::InvalidResponse(format!("Failed to parse response: {e}")))?;
304
305 Ok(result)
306 }
307}
308
309#[cfg(feature = "http3")]
311pub struct Http3Client {
312 config: BaseConfig,
314 client: reqwest::Client,
316}
317
318#[cfg(feature = "http3")]
319impl Http3Client {
320 pub fn new(config: BaseConfig) -> Result<Self, ApiError> {
322 let cert_der = reqwest::Certificate::from_pem(config.server_ca.as_bytes())
324 .map_err(|e| ApiError::SslConfig(format!("Failed to parse CA certificate: {e}")))?;
325
326 let mut client_builder = reqwest::ClientBuilder::new()
328 .use_rustls_tls()
329 .http3_prior_knowledge()
330 .add_root_certificate(cert_der);
331
332 if let (Some(cert), Some(key)) = (&config.mtls_cert, &config.mtls_key) {
334 let identity_str = format!("{}{}", cert, key);
335 let identity = reqwest::Identity::from_pem(identity_str.as_bytes()).map_err(|e| {
336 ApiError::SslConfig(format!(
337 "Failed to create identity from certificate and key: {e}"
338 ))
339 })?;
340 client_builder = client_builder.identity(identity);
341 }
342
343 let client = client_builder
344 .build()
345 .map_err(|e| ApiError::SslConfig(e.to_string()))?;
346
347 Ok(Self { config, client })
348 }
349
350 pub async fn send_request<T, R>(&self, endpoint: &str, request_body: &T) -> Result<R, ApiError>
352 where
353 T: Serialize,
354 R: for<'de> Deserialize<'de>,
355 {
356 let base_url = self.config.get_base_url();
357 let url = format!("https://{base_url}/{endpoint}");
358
359 let response = self
360 .client
361 .post(&url)
362 .json(request_body)
363 .send()
364 .await
365 .map_err(ApiError::HttpClient)?;
366
367 if !response.status().is_success() {
368 let status = response.status();
369 let error_text = response.text().await.unwrap_or_default();
370 return Err(ApiError::InvalidResponse(format!(
371 "HTTP error: {status} - {error_text}"
372 )));
373 }
374
375 let result = response
376 .json::<R>()
377 .await
378 .map_err(|e| ApiError::InvalidResponse(format!("Failed to parse response: {e}")))?;
379
380 Ok(result)
381 }
382
383 pub async fn send_request_with_auth<T, R>(
384 &self,
385 endpoint: &str,
386 request_body: &T,
387 auth_header: &str,
388 ) -> Result<R, ApiError>
389 where
390 T: Serialize,
391 R: for<'de> Deserialize<'de>,
392 {
393 let base_url = self.config.get_base_url();
394 let url = format!("https://{base_url}/{endpoint}");
395
396 let response = self
397 .client
398 .post(&url)
399 .header("Authorization", auth_header)
400 .json(request_body)
401 .send()
402 .await
403 .map_err(ApiError::HttpClient)?;
404
405 if !response.status().is_success() {
406 let status = response.status();
407 let error_text = response.text().await.unwrap_or_default();
408 return Err(ApiError::InvalidResponse(format!(
409 "HTTP error: {status} - {error_text}"
410 )));
411 }
412
413 let result = response
414 .json::<R>()
415 .await
416 .map_err(|e| ApiError::InvalidResponse(format!("Failed to parse response: {e}")))?;
417
418 Ok(result)
419 }
420}
421
422pub enum HessraClient {
424 Http1(Http1Client),
426 #[cfg(feature = "http3")]
428 Http3(Http3Client),
429}
430
431pub struct HessraClientBuilder {
433 config: BaseConfig,
435 protocol: hessra_config::Protocol,
437}
438
439impl HessraClientBuilder {
440 pub fn new() -> Self {
442 Self {
443 config: BaseConfig {
444 base_url: String::new(),
445 port: None,
446 mtls_key: None,
447 mtls_cert: None,
448 server_ca: String::new(),
449 public_key: None,
450 personal_keypair: None,
451 },
452 protocol: Protocol::Http1,
453 }
454 }
455
456 pub fn from_config(mut self, config: &HessraConfig) -> Self {
458 self.config.base_url = config.base_url.clone();
459 self.config.port = config.port;
460 self.config.mtls_key = config.mtls_key.clone();
461 self.config.mtls_cert = config.mtls_cert.clone();
462 self.config.server_ca = config.server_ca.clone();
463 self.config.public_key = config.public_key.clone();
464 self.config.personal_keypair = config.personal_keypair.clone();
465 self.protocol = config.protocol.clone();
466 self
467 }
468
469 pub fn base_url(mut self, base_url: impl Into<String>) -> Self {
471 self.config.base_url = base_url.into();
472 self
473 }
474
475 pub fn mtls_key(mut self, mtls_key: impl Into<String>) -> Self {
478 self.config.mtls_key = Some(mtls_key.into());
479 self
480 }
481
482 pub fn mtls_cert(mut self, mtls_cert: impl Into<String>) -> Self {
485 self.config.mtls_cert = Some(mtls_cert.into());
486 self
487 }
488
489 pub fn server_ca(mut self, server_ca: impl Into<String>) -> Self {
492 self.config.server_ca = server_ca.into();
493 self
494 }
495
496 pub fn port(mut self, port: u16) -> Self {
498 self.config.port = Some(port);
499 self
500 }
501
502 pub fn protocol(mut self, protocol: Protocol) -> Self {
504 self.protocol = protocol;
505 self
506 }
507
508 pub fn public_key(mut self, public_key: impl Into<String>) -> Self {
511 self.config.public_key = Some(public_key.into());
512 self
513 }
514
515 pub fn personal_keypair(mut self, keypair: impl Into<String>) -> Self {
519 self.config.personal_keypair = Some(keypair.into());
520 self
521 }
522
523 fn build_http1(&self) -> Result<Http1Client, ApiError> {
525 Http1Client::new(self.config.clone())
526 }
527
528 #[cfg(feature = "http3")]
530 fn build_http3(&self) -> Result<Http3Client, ApiError> {
531 Http3Client::new(self.config.clone())
532 }
533
534 pub fn build(self) -> Result<HessraClient, ApiError> {
536 match self.protocol {
537 Protocol::Http1 => Ok(HessraClient::Http1(self.build_http1()?)),
538 #[cfg(feature = "http3")]
539 Protocol::Http3 => Ok(HessraClient::Http3(self.build_http3()?)),
540 #[allow(unreachable_patterns)]
541 _ => Err(ApiError::Internal("Unsupported protocol".to_string())),
542 }
543 }
544}
545
546impl Default for HessraClientBuilder {
547 fn default() -> Self {
548 Self::new()
549 }
550}
551
552impl HessraClient {
553 pub fn builder() -> HessraClientBuilder {
555 HessraClientBuilder::new()
556 }
557
558 pub async fn fetch_public_key(
562 base_url: impl Into<String>,
563 port: Option<u16>,
564 server_ca: impl Into<String>,
565 ) -> Result<String, ApiError> {
566 let base_url = base_url.into();
567 let server_ca = server_ca.into();
568
569 let cert_pem = server_ca.as_bytes();
571 let cert_der = reqwest::Certificate::from_pem(cert_pem)
572 .map_err(|e| ApiError::SslConfig(e.to_string()))?;
573
574 let client = reqwest::ClientBuilder::new()
575 .use_rustls_tls()
576 .add_root_certificate(cert_der)
577 .build()
578 .map_err(|e| ApiError::SslConfig(e.to_string()))?;
579
580 let url = match port {
582 Some(port) => format!("https://{base_url}:{port}/public_key"),
583 None => format!("https://{base_url}/public_key"),
584 };
585
586 let response = client
588 .get(&url)
589 .send()
590 .await
591 .map_err(ApiError::HttpClient)?;
592
593 if !response.status().is_success() {
594 let status = response.status();
595 let error_text = response.text().await.unwrap_or_default();
596 return Err(ApiError::InvalidResponse(format!(
597 "HTTP error: {status} - {error_text}"
598 )));
599 }
600
601 let result = response
603 .json::<PublicKeyResponse>()
604 .await
605 .map_err(|e| ApiError::InvalidResponse(format!("Failed to parse response: {e}")))?;
606
607 Ok(result.public_key)
608 }
609
610 #[cfg(feature = "http3")]
611 pub async fn fetch_public_key_http3(
612 base_url: impl Into<String>,
613 port: Option<u16>,
614 server_ca: impl Into<String>,
615 ) -> Result<String, ApiError> {
616 let base_url = base_url.into();
617 let server_ca = server_ca.into();
618
619 let cert_pem = server_ca.as_bytes();
621 let cert_der = reqwest::Certificate::from_pem(cert_pem)
622 .map_err(|e| ApiError::SslConfig(e.to_string()))?;
623
624 let client = reqwest::ClientBuilder::new()
625 .use_rustls_tls()
626 .add_root_certificate(cert_der)
627 .http3_prior_knowledge()
628 .build()
629 .map_err(|e| ApiError::SslConfig(e.to_string()))?;
630
631 let url = match port {
633 Some(port) => format!("https://{base_url}:{port}/public_key"),
634 None => format!("https://{base_url}/public_key"),
635 };
636
637 let response = client
639 .get(&url)
640 .send()
641 .await
642 .map_err(ApiError::HttpClient)?;
643
644 if !response.status().is_success() {
645 let status = response.status();
646 let error_text = response.text().await.unwrap_or_default();
647 return Err(ApiError::InvalidResponse(format!(
648 "HTTP error: {status} - {error_text}"
649 )));
650 }
651
652 let result = response
654 .json::<PublicKeyResponse>()
655 .await
656 .map_err(|e| ApiError::InvalidResponse(format!("Failed to parse response: {e}")))?;
657
658 Ok(result.public_key)
659 }
660
661 pub async fn request_token(
664 &self,
665 resource: String,
666 operation: String,
667 ) -> Result<TokenResponse, ApiError> {
668 let request = TokenRequest {
669 resource,
670 operation,
671 };
672
673 let response = match self {
674 HessraClient::Http1(client) => {
675 client
676 .send_request::<_, TokenResponse>("request_token", &request)
677 .await?
678 }
679 #[cfg(feature = "http3")]
680 HessraClient::Http3(client) => {
681 client
682 .send_request::<_, TokenResponse>("request_token", &request)
683 .await?
684 }
685 };
686
687 Ok(response)
688 }
689
690 pub async fn request_token_with_identity(
694 &self,
695 resource: String,
696 operation: String,
697 identity_token: String,
698 ) -> Result<TokenResponse, ApiError> {
699 let request = TokenRequest {
700 resource,
701 operation,
702 };
703
704 let response = match self {
705 HessraClient::Http1(client) => {
706 client
707 .send_request_with_auth::<_, TokenResponse>(
708 "request_token",
709 &request,
710 &format!("Bearer {identity_token}"),
711 )
712 .await?
713 }
714 #[cfg(feature = "http3")]
715 HessraClient::Http3(client) => {
716 client
717 .send_request_with_auth::<_, TokenResponse>(
718 "request_token",
719 &request,
720 &format!("Bearer {identity_token}"),
721 )
722 .await?
723 }
724 };
725
726 Ok(response)
727 }
728
729 pub async fn request_token_simple(
732 &self,
733 resource: String,
734 operation: String,
735 ) -> Result<String, ApiError> {
736 let response = self.request_token(resource, operation).await?;
737
738 match response.token {
739 Some(token) => Ok(token),
740 None => Err(ApiError::TokenRequest(format!(
741 "Failed to get token: {}",
742 response.response_msg
743 ))),
744 }
745 }
746
747 pub async fn verify_token(
750 &self,
751 token: String,
752 subject: String,
753 resource: String,
754 operation: String,
755 ) -> Result<String, ApiError> {
756 let request = VerifyTokenRequest {
757 token,
758 subject,
759 resource,
760 operation,
761 };
762
763 let response = match self {
764 HessraClient::Http1(client) => {
765 client
766 .send_request::<_, VerifyTokenResponse>("verify_token", &request)
767 .await?
768 }
769 #[cfg(feature = "http3")]
770 HessraClient::Http3(client) => {
771 client
772 .send_request::<_, VerifyTokenResponse>("verify_token", &request)
773 .await?
774 }
775 };
776
777 Ok(response.response_msg)
778 }
779
780 pub async fn verify_service_chain_token(
787 &self,
788 token: String,
789 subject: String,
790 resource: String,
791 component: Option<String>,
792 ) -> Result<String, ApiError> {
793 let request = VerifyServiceChainTokenRequest {
794 token,
795 subject,
796 resource,
797 component,
798 };
799
800 let response = match self {
801 HessraClient::Http1(client) => {
802 client
803 .send_request::<_, VerifyTokenResponse>("verify_service_chain_token", &request)
804 .await?
805 }
806 #[cfg(feature = "http3")]
807 HessraClient::Http3(client) => {
808 client
809 .send_request::<_, VerifyTokenResponse>("verify_service_chain_token", &request)
810 .await?
811 }
812 };
813
814 Ok(response.response_msg)
815 }
816
817 pub async fn sign_token(
819 &self,
820 token: &str,
821 resource: &str,
822 operation: &str,
823 ) -> Result<SignTokenResponse, ApiError> {
824 let request = SignTokenRequest {
825 token: token.to_string(),
826 resource: resource.to_string(),
827 operation: operation.to_string(),
828 };
829
830 let response = match self {
831 HessraClient::Http1(client) => {
832 client
833 .send_request::<_, SignTokenResponse>("sign_token", &request)
834 .await?
835 }
836 #[cfg(feature = "http3")]
837 HessraClient::Http3(client) => {
838 client
839 .send_request::<_, SignTokenResponse>("sign_token", &request)
840 .await?
841 }
842 };
843
844 Ok(response)
845 }
846
847 pub async fn get_public_key(&self) -> Result<String, ApiError> {
849 let url_path = "public_key";
850
851 let response = match self {
852 HessraClient::Http1(client) => {
853 let base_url = client.config.get_base_url();
855 let full_url = format!("https://{base_url}/{url_path}");
856
857 let response = client
858 .client
859 .get(&full_url)
860 .send()
861 .await
862 .map_err(ApiError::HttpClient)?;
863
864 if !response.status().is_success() {
865 let status = response.status();
866 let error_text = response.text().await.unwrap_or_default();
867 return Err(ApiError::InvalidResponse(format!(
868 "HTTP error: {status} - {error_text}"
869 )));
870 }
871
872 response.json::<PublicKeyResponse>().await.map_err(|e| {
873 ApiError::InvalidResponse(format!("Failed to parse response: {e}"))
874 })?
875 }
876 #[cfg(feature = "http3")]
877 HessraClient::Http3(client) => {
878 let base_url = client.config.get_base_url();
879 let full_url = format!("https://{base_url}/{url_path}");
880
881 let response = client
882 .client
883 .get(&full_url)
884 .send()
885 .await
886 .map_err(ApiError::HttpClient)?;
887
888 if !response.status().is_success() {
889 let status = response.status();
890 let error_text = response.text().await.unwrap_or_default();
891 return Err(ApiError::InvalidResponse(format!(
892 "HTTP error: {status} - {error_text}"
893 )));
894 }
895
896 response.json::<PublicKeyResponse>().await.map_err(|e| {
897 ApiError::InvalidResponse(format!("Failed to parse response: {e}"))
898 })?
899 }
900 };
901
902 Ok(response.public_key)
903 }
904
905 pub async fn request_identity_token(
913 &self,
914 identifier: Option<String>,
915 ) -> Result<IdentityTokenResponse, ApiError> {
916 let request = IdentityTokenRequest { identifier };
917
918 let response = match self {
919 HessraClient::Http1(client) => {
920 client
921 .send_request::<_, IdentityTokenResponse>("request_identity_token", &request)
922 .await?
923 }
924 #[cfg(feature = "http3")]
925 HessraClient::Http3(client) => {
926 client
927 .send_request::<_, IdentityTokenResponse>("request_identity_token", &request)
928 .await?
929 }
930 };
931
932 Ok(response)
933 }
934
935 pub async fn refresh_identity_token(
945 &self,
946 current_token: String,
947 identifier: Option<String>,
948 ) -> Result<IdentityTokenResponse, ApiError> {
949 let request = RefreshIdentityTokenRequest {
950 current_token,
951 identifier,
952 };
953
954 let response = match self {
955 HessraClient::Http1(client) => {
956 client
957 .send_request::<_, IdentityTokenResponse>("refresh_identity_token", &request)
958 .await?
959 }
960 #[cfg(feature = "http3")]
961 HessraClient::Http3(client) => {
962 client
963 .send_request::<_, IdentityTokenResponse>("refresh_identity_token", &request)
964 .await?
965 }
966 };
967
968 Ok(response)
969 }
970}
971
972#[cfg(test)]
973mod tests {
974 use super::*;
975
976 #[test]
978 fn test_base_config_get_base_url_with_port() {
979 let config = BaseConfig {
980 base_url: "test.hessra.net".to_string(),
981 port: Some(443),
982 mtls_key: None,
983 mtls_cert: None,
984 server_ca: "".to_string(),
985 public_key: None,
986 personal_keypair: None,
987 };
988
989 assert_eq!(config.get_base_url(), "test.hessra.net:443");
990 }
991
992 #[test]
993 fn test_base_config_get_base_url_without_port() {
994 let config = BaseConfig {
995 base_url: "test.hessra.net".to_string(),
996 port: None,
997 mtls_key: None,
998 mtls_cert: None,
999 server_ca: "".to_string(),
1000 public_key: None,
1001 personal_keypair: None,
1002 };
1003
1004 assert_eq!(config.get_base_url(), "test.hessra.net");
1005 }
1006
1007 #[test]
1009 fn test_client_builder_methods() {
1010 let builder = HessraClientBuilder::new()
1011 .base_url("test.hessra.net")
1012 .port(443)
1013 .protocol(Protocol::Http1)
1014 .mtls_cert("CERT")
1015 .mtls_key("KEY")
1016 .server_ca("CA")
1017 .public_key("PUBKEY")
1018 .personal_keypair("KEYPAIR");
1019
1020 assert_eq!(builder.config.base_url, "test.hessra.net");
1021 assert_eq!(builder.config.port, Some(443));
1022 assert_eq!(builder.config.mtls_cert, Some("CERT".to_string()));
1023 assert_eq!(builder.config.mtls_key, Some("KEY".to_string()));
1024 assert_eq!(builder.config.server_ca, "CA");
1025 assert_eq!(builder.config.public_key, Some("PUBKEY".to_string()));
1026 assert_eq!(builder.config.personal_keypair, Some("KEYPAIR".to_string()));
1027 }
1028}