1use crate::runbeam_api::types::{
2 ApiError, AuthorizeResponse, RunbeamError, StoreConfigRequest, StoreConfigResponse,
3};
4use serde::Serialize;
5
6#[derive(Debug, Clone)]
11pub struct RunbeamClient {
12 base_url: String,
14 client: reqwest::Client,
16}
17
18#[derive(Debug, Serialize)]
20struct AuthorizeRequest {
21 token: String,
23 gateway_code: String,
25 #[serde(skip_serializing_if = "Option::is_none")]
27 machine_public_key: Option<String>,
28 #[serde(skip_serializing_if = "Option::is_none")]
30 metadata: Option<Vec<String>>,
31}
32
33impl RunbeamClient {
34 pub fn new(base_url: impl Into<String>) -> Self {
48 let base_url = base_url.into();
49 tracing::debug!("Creating RunbeamClient with base URL: {}", base_url);
50
51 Self {
52 base_url,
53 client: reqwest::Client::new(),
54 }
55 }
56
57 pub async fn authorize_gateway(
114 &self,
115 user_token: impl Into<String>,
116 gateway_code: impl Into<String>,
117 machine_public_key: Option<String>,
118 metadata: Option<Vec<String>>,
119 ) -> Result<AuthorizeResponse, RunbeamError> {
120 let user_token = user_token.into();
121 let gateway_code = gateway_code.into();
122
123 tracing::info!(
124 "Authorizing gateway with Runbeam Cloud: gateway_code={}",
125 gateway_code
126 );
127
128 let url = format!("{}/harmony/authorize", self.base_url);
130
131 let payload = AuthorizeRequest {
133 token: user_token.clone(),
134 gateway_code: gateway_code.clone(),
135 machine_public_key,
136 metadata,
137 };
138
139 tracing::debug!("Sending authorization request to: {}", url);
140
141 let response = self
143 .client
144 .post(&url)
145 .header("Authorization", format!("Bearer {}", user_token))
146 .header("Content-Type", "application/json")
147 .json(&payload)
148 .send()
149 .await
150 .map_err(|e| {
151 tracing::error!("Failed to send authorization request: {}", e);
152 ApiError::from(e)
153 })?;
154
155 let status = response.status();
156 tracing::debug!("Received response with status: {}", status);
157
158 if !status.is_success() {
160 let error_body = response
161 .text()
162 .await
163 .unwrap_or_else(|_| "Unknown error".to_string());
164
165 tracing::error!(
166 "Authorization failed: HTTP {} - {}",
167 status.as_u16(),
168 error_body
169 );
170
171 return Err(RunbeamError::Api(ApiError::Http {
172 status: status.as_u16(),
173 message: error_body,
174 }));
175 }
176
177 let auth_response: AuthorizeResponse = response.json().await.map_err(|e| {
179 tracing::error!("Failed to parse authorization response: {}", e);
180 ApiError::Parse(format!("Failed to parse response JSON: {}", e))
181 })?;
182
183 tracing::info!(
184 "Gateway authorized successfully: gateway_id={}, expires_at={}",
185 auth_response.gateway.id,
186 auth_response.expires_at
187 );
188
189 tracing::debug!(
190 "Machine token length: {}",
191 auth_response.machine_token.len()
192 );
193 tracing::debug!("Gateway abilities: {:?}", auth_response.abilities);
194
195 Ok(auth_response)
196 }
197
198 pub fn base_url(&self) -> &str {
200 &self.base_url
201 }
202
203 pub async fn list_changes(
230 &self,
231 token: impl Into<String>,
232 ) -> Result<
233 crate::runbeam_api::resources::PaginatedResponse<crate::runbeam_api::resources::Change>,
234 RunbeamError,
235 > {
236 let url = format!("{}/harmony/changes", self.base_url);
237
238 tracing::debug!("Listing all changes from: {}", url);
239
240 let response = self
241 .client
242 .get(&url)
243 .header("Authorization", format!("Bearer {}", token.into()))
244 .send()
245 .await
246 .map_err(ApiError::from)?;
247
248 if !response.status().is_success() {
249 let status = response.status();
250 let error_body = response
251 .text()
252 .await
253 .unwrap_or_else(|_| "Unknown error".to_string());
254 tracing::error!("Failed to list changes: HTTP {} - {}", status, error_body);
255 return Err(RunbeamError::Api(ApiError::Http {
256 status: status.as_u16(),
257 message: error_body,
258 }));
259 }
260
261 response.json().await.map_err(|e| {
262 tracing::error!("Failed to parse changes response: {}", e);
263 RunbeamError::Api(ApiError::Parse(format!("Failed to parse response: {}", e)))
264 })
265 }
266
267 pub async fn list_changes_for_gateway(
298 &self,
299 token: impl Into<String>,
300 gateway_id: impl Into<String>,
301 ) -> Result<
302 crate::runbeam_api::resources::PaginatedResponse<crate::runbeam_api::resources::Change>,
303 RunbeamError,
304 > {
305 let gateway_id = gateway_id.into();
306 let url = format!("{}/harmony/changes/{}", self.base_url, gateway_id);
307
308 tracing::debug!("Listing changes for gateway {} from: {}", gateway_id, url);
309
310 let response = self
311 .client
312 .get(&url)
313 .header("Authorization", format!("Bearer {}", token.into()))
314 .send()
315 .await
316 .map_err(ApiError::from)?;
317
318 if !response.status().is_success() {
319 let status = response.status();
320 let error_body = response
321 .text()
322 .await
323 .unwrap_or_else(|_| "Unknown error".to_string());
324 tracing::error!(
325 "Failed to list changes for gateway {}: HTTP {} - {}",
326 gateway_id,
327 status,
328 error_body
329 );
330 return Err(RunbeamError::Api(ApiError::Http {
331 status: status.as_u16(),
332 message: error_body,
333 }));
334 }
335
336 let response_text = response.text().await.map_err(|e| {
337 tracing::error!("Failed to read response body: {}", e);
338 RunbeamError::Api(ApiError::Parse(format!("Failed to read response: {}", e)))
339 })?;
340
341 serde_json::from_str(&response_text).map_err(|e| {
342 tracing::error!(
343 "Failed to parse changes response: {} - Response body: {}",
344 e,
345 response_text
346 );
347 RunbeamError::Api(ApiError::Parse(format!(
348 "Failed to parse response: {} - Body: {}",
349 e, response_text
350 )))
351 })
352 }
353
354 pub async fn get_change(
384 &self,
385 token: impl Into<String>,
386 change_id: impl Into<String>,
387 ) -> Result<
388 crate::runbeam_api::resources::ResourceResponse<crate::runbeam_api::resources::Change>,
389 RunbeamError,
390 > {
391 let change_id = change_id.into();
392 let url = format!("{}/harmony/change/{}", self.base_url, change_id);
393
394 tracing::debug!("Getting change {} from: {}", change_id, url);
395
396 let response = self
397 .client
398 .get(&url)
399 .header("Authorization", format!("Bearer {}", token.into()))
400 .send()
401 .await
402 .map_err(ApiError::from)?;
403
404 if !response.status().is_success() {
405 let status = response.status();
406 let error_body = response
407 .text()
408 .await
409 .unwrap_or_else(|_| "Unknown error".to_string());
410 tracing::error!("Failed to get change: HTTP {} - {}", status, error_body);
411 return Err(RunbeamError::Api(ApiError::Http {
412 status: status.as_u16(),
413 message: error_body,
414 }));
415 }
416
417 response.json().await.map_err(|e| {
418 tracing::error!("Failed to parse change response: {}", e);
419 RunbeamError::Api(ApiError::Parse(format!("Failed to parse response: {}", e)))
420 })
421 }
422
423 pub async fn list_gateways(
436 &self,
437 token: impl Into<String>,
438 ) -> Result<
439 crate::runbeam_api::resources::PaginatedResponse<crate::runbeam_api::resources::Gateway>,
440 RunbeamError,
441 > {
442 let url = format!("{}/gateways", self.base_url);
443
444 let response = self
445 .client
446 .get(&url)
447 .header("Authorization", format!("Bearer {}", token.into()))
448 .send()
449 .await
450 .map_err(ApiError::from)?;
451
452 if !response.status().is_success() {
453 let status = response.status();
454 let error_body = response
455 .text()
456 .await
457 .unwrap_or_else(|_| "Unknown error".to_string());
458 return Err(RunbeamError::Api(ApiError::Http {
459 status: status.as_u16(),
460 message: error_body,
461 }));
462 }
463
464 response.json().await.map_err(|e| {
465 RunbeamError::Api(ApiError::Parse(format!("Failed to parse response: {}", e)))
466 })
467 }
468
469 pub async fn get_gateway(
481 &self,
482 token: impl Into<String>,
483 gateway_id: impl Into<String>,
484 ) -> Result<
485 crate::runbeam_api::resources::ResourceResponse<crate::runbeam_api::resources::Gateway>,
486 RunbeamError,
487 > {
488 let url = format!("{}/gateways/{}", self.base_url, gateway_id.into());
489
490 let response = self
491 .client
492 .get(&url)
493 .header("Authorization", format!("Bearer {}", token.into()))
494 .send()
495 .await
496 .map_err(ApiError::from)?;
497
498 if !response.status().is_success() {
499 let status = response.status();
500 let error_body = response
501 .text()
502 .await
503 .unwrap_or_else(|_| "Unknown error".to_string());
504 return Err(RunbeamError::Api(ApiError::Http {
505 status: status.as_u16(),
506 message: error_body,
507 }));
508 }
509
510 response.json().await.map_err(|e| {
511 RunbeamError::Api(ApiError::Parse(format!("Failed to parse response: {}", e)))
512 })
513 }
514
515 pub async fn list_services(
528 &self,
529 token: impl Into<String>,
530 ) -> Result<
531 crate::runbeam_api::resources::PaginatedResponse<crate::runbeam_api::resources::Service>,
532 RunbeamError,
533 > {
534 let url = format!("{}/api/services", self.base_url);
535
536 let response = self
537 .client
538 .get(&url)
539 .header("Authorization", format!("Bearer {}", token.into()))
540 .send()
541 .await
542 .map_err(ApiError::from)?;
543
544 if !response.status().is_success() {
545 let status = response.status();
546 let error_body = response
547 .text()
548 .await
549 .unwrap_or_else(|_| "Unknown error".to_string());
550 return Err(RunbeamError::Api(ApiError::Http {
551 status: status.as_u16(),
552 message: error_body,
553 }));
554 }
555
556 response.json().await.map_err(|e| {
557 RunbeamError::Api(ApiError::Parse(format!("Failed to parse response: {}", e)))
558 })
559 }
560
561 pub async fn get_service(
573 &self,
574 token: impl Into<String>,
575 service_id: impl Into<String>,
576 ) -> Result<
577 crate::runbeam_api::resources::ResourceResponse<crate::runbeam_api::resources::Service>,
578 RunbeamError,
579 > {
580 let url = format!("{}/api/services/{}", self.base_url, service_id.into());
581
582 let response = self
583 .client
584 .get(&url)
585 .header("Authorization", format!("Bearer {}", token.into()))
586 .send()
587 .await
588 .map_err(ApiError::from)?;
589
590 if !response.status().is_success() {
591 let status = response.status();
592 let error_body = response
593 .text()
594 .await
595 .unwrap_or_else(|_| "Unknown error".to_string());
596 return Err(RunbeamError::Api(ApiError::Http {
597 status: status.as_u16(),
598 message: error_body,
599 }));
600 }
601
602 response.json().await.map_err(|e| {
603 RunbeamError::Api(ApiError::Parse(format!("Failed to parse response: {}", e)))
604 })
605 }
606
607 pub async fn list_endpoints(
618 &self,
619 token: impl Into<String>,
620 ) -> Result<
621 crate::runbeam_api::resources::PaginatedResponse<crate::runbeam_api::resources::Endpoint>,
622 RunbeamError,
623 > {
624 let url = format!("{}/api/endpoints", self.base_url);
625
626 let response = self
627 .client
628 .get(&url)
629 .header("Authorization", format!("Bearer {}", token.into()))
630 .send()
631 .await
632 .map_err(ApiError::from)?;
633
634 if !response.status().is_success() {
635 let status = response.status();
636 let error_body = response
637 .text()
638 .await
639 .unwrap_or_else(|_| "Unknown error".to_string());
640 return Err(RunbeamError::Api(ApiError::Http {
641 status: status.as_u16(),
642 message: error_body,
643 }));
644 }
645
646 response.json().await.map_err(|e| {
647 RunbeamError::Api(ApiError::Parse(format!("Failed to parse response: {}", e)))
648 })
649 }
650
651 pub async fn list_backends(
662 &self,
663 token: impl Into<String>,
664 ) -> Result<
665 crate::runbeam_api::resources::PaginatedResponse<crate::runbeam_api::resources::Backend>,
666 RunbeamError,
667 > {
668 let url = format!("{}/api/backends", self.base_url);
669
670 let response = self
671 .client
672 .get(&url)
673 .header("Authorization", format!("Bearer {}", token.into()))
674 .send()
675 .await
676 .map_err(ApiError::from)?;
677
678 if !response.status().is_success() {
679 let status = response.status();
680 let error_body = response
681 .text()
682 .await
683 .unwrap_or_else(|_| "Unknown error".to_string());
684 return Err(RunbeamError::Api(ApiError::Http {
685 status: status.as_u16(),
686 message: error_body,
687 }));
688 }
689
690 response.json().await.map_err(|e| {
691 RunbeamError::Api(ApiError::Parse(format!("Failed to parse response: {}", e)))
692 })
693 }
694
695 pub async fn list_pipelines(
706 &self,
707 token: impl Into<String>,
708 ) -> Result<
709 crate::runbeam_api::resources::PaginatedResponse<crate::runbeam_api::resources::Pipeline>,
710 RunbeamError,
711 > {
712 let url = format!("{}/api/pipelines", self.base_url);
713
714 let response = self
715 .client
716 .get(&url)
717 .header("Authorization", format!("Bearer {}", token.into()))
718 .send()
719 .await
720 .map_err(ApiError::from)?;
721
722 if !response.status().is_success() {
723 let status = response.status();
724 let error_body = response
725 .text()
726 .await
727 .unwrap_or_else(|_| "Unknown error".to_string());
728 return Err(RunbeamError::Api(ApiError::Http {
729 status: status.as_u16(),
730 message: error_body,
731 }));
732 }
733
734 response.json().await.map_err(|e| {
735 RunbeamError::Api(ApiError::Parse(format!("Failed to parse response: {}", e)))
736 })
737 }
738
739 pub async fn get_transform(
774 &self,
775 token: impl Into<String>,
776 transform_id: impl Into<String>,
777 ) -> Result<
778 crate::runbeam_api::resources::ResourceResponse<crate::runbeam_api::resources::Transform>,
779 RunbeamError,
780 > {
781 let transform_id = transform_id.into();
782 let url = format!("{}/api/transforms/{}", self.base_url, transform_id);
783
784 tracing::debug!("Getting transform {} from: {}", transform_id, url);
785
786 let response = self
787 .client
788 .get(&url)
789 .header("Authorization", format!("Bearer {}", token.into()))
790 .send()
791 .await
792 .map_err(ApiError::from)?;
793
794 if !response.status().is_success() {
795 let status = response.status();
796 let error_body = response
797 .text()
798 .await
799 .unwrap_or_else(|_| "Unknown error".to_string());
800 tracing::error!("Failed to get transform: HTTP {} - {}", status, error_body);
801 return Err(RunbeamError::Api(ApiError::Http {
802 status: status.as_u16(),
803 message: error_body,
804 }));
805 }
806
807 response.json().await.map_err(|e| {
808 tracing::error!("Failed to parse transform response: {}", e);
809 RunbeamError::Api(ApiError::Parse(format!("Failed to parse response: {}", e)))
810 })
811 }
812
813 pub async fn get_base_url(
841 &self,
842 token: impl Into<String>,
843 ) -> Result<crate::runbeam_api::resources::BaseUrlResponse, RunbeamError> {
844 let token = token.into();
845 let candidates = [
847 format!("{}/api/harmony/base-url", self.base_url),
848 format!("{}/harmony/base-url", self.base_url),
849 ];
850
851 let mut last_err: Option<RunbeamError> = None;
852 for url in candidates {
853 tracing::debug!("Getting base URL from: {}", url);
854 let resp = self
855 .client
856 .get(&url)
857 .header("Authorization", format!("Bearer {}", token))
858 .send()
859 .await;
860
861 let response = match resp {
862 Ok(r) => r,
863 Err(e) => {
864 last_err = Some(ApiError::from(e).into());
865 continue;
866 }
867 };
868
869 if !response.status().is_success() {
870 let status = response.status();
871 let error_body = response
872 .text()
873 .await
874 .unwrap_or_else(|_| "Unknown error".to_string());
875 tracing::warn!(
876 "Base URL discovery attempt failed: HTTP {} - {} (url: {})",
877 status,
878 error_body,
879 url
880 );
881 last_err = Some(RunbeamError::Api(ApiError::Http {
882 status: status.as_u16(),
883 message: error_body,
884 }));
885 continue;
886 }
887
888 let parsed = response.json().await.map_err(|e| {
889 tracing::warn!("Failed to parse base URL response from {}: {}", url, e);
890 RunbeamError::Api(ApiError::Parse(format!("Failed to parse response: {}", e)))
891 });
892 if parsed.is_ok() {
893 return parsed;
894 } else {
895 last_err = Some(parsed.err().unwrap());
896 }
897 }
898
899 Err(last_err.unwrap_or_else(|| {
900 RunbeamError::Api(ApiError::Request(
901 "Base URL discovery failed for all candidates".to_string(),
902 ))
903 }))
904 }
905
906 pub async fn discover_base_url(&self, token: impl Into<String>) -> Result<Self, RunbeamError> {
908 let resp = self.get_base_url(token).await?;
909 let discovered = resp.full_url.unwrap_or(resp.base_url);
910 tracing::info!("Discovered Runbeam API base URL: {}", discovered);
911 Ok(Self::new(discovered))
912 }
913
914 pub async fn acknowledge_changes(
942 &self,
943 token: impl Into<String>,
944 change_ids: Vec<String>,
945 ) -> Result<crate::runbeam_api::resources::AcknowledgeChangesResponse, RunbeamError> {
946 let url = format!("{}/harmony/changes/acknowledge", self.base_url);
947
948 tracing::info!("Acknowledging {} changes", change_ids.len());
949 tracing::debug!("Change IDs: {:?}", change_ids);
950
951 let payload = crate::runbeam_api::resources::AcknowledgeChangesRequest { change_ids };
952
953 let response = self
954 .client
955 .post(&url)
956 .header("Authorization", format!("Bearer {}", token.into()))
957 .header("Content-Type", "application/json")
958 .json(&payload)
959 .send()
960 .await
961 .map_err(ApiError::from)?;
962
963 if !response.status().is_success() {
964 let status = response.status();
965 let error_body = response
966 .text()
967 .await
968 .unwrap_or_else(|_| "Unknown error".to_string());
969 tracing::error!(
970 "Failed to acknowledge changes: HTTP {} - {}",
971 status,
972 error_body
973 );
974 return Err(RunbeamError::Api(ApiError::Http {
975 status: status.as_u16(),
976 message: error_body,
977 }));
978 }
979
980 response.json().await.map_err(|e| {
981 tracing::error!("Failed to parse acknowledge response: {}", e);
982 RunbeamError::Api(ApiError::Parse(format!("Failed to parse response: {}", e)))
983 })
984 }
985
986 pub async fn mark_change_applied(
1012 &self,
1013 token: impl Into<String>,
1014 change_id: impl Into<String>,
1015 ) -> Result<crate::runbeam_api::resources::ChangeAppliedResponse, RunbeamError> {
1016 let change_id = change_id.into();
1017 let url = format!("{}/harmony/change/{}/applied", self.base_url, change_id);
1018
1019 tracing::info!("Marking change {} as applied", change_id);
1020
1021 let response = self
1022 .client
1023 .post(&url)
1024 .header("Authorization", format!("Bearer {}", token.into()))
1025 .send()
1026 .await
1027 .map_err(ApiError::from)?;
1028
1029 if !response.status().is_success() {
1030 let status = response.status();
1031 let error_body = response
1032 .text()
1033 .await
1034 .unwrap_or_else(|_| "Unknown error".to_string());
1035 tracing::error!(
1036 "Failed to mark change as applied: HTTP {} - {}",
1037 status,
1038 error_body
1039 );
1040 return Err(RunbeamError::Api(ApiError::Http {
1041 status: status.as_u16(),
1042 message: error_body,
1043 }));
1044 }
1045
1046 response.json().await.map_err(|e| {
1047 tracing::error!("Failed to parse applied response: {}", e);
1048 RunbeamError::Api(ApiError::Parse(format!("Failed to parse response: {}", e)))
1049 })
1050 }
1051
1052 pub async fn mark_change_failed(
1085 &self,
1086 token: impl Into<String>,
1087 change_id: impl Into<String>,
1088 error: String,
1089 details: Option<Vec<String>>,
1090 ) -> Result<crate::runbeam_api::resources::ChangeFailedResponse, RunbeamError> {
1091 let change_id = change_id.into();
1092 let url = format!("{}/harmony/change/{}/failed", self.base_url, change_id);
1093
1094 tracing::warn!("Marking change {} as failed: {}", change_id, error);
1095 if let Some(ref details) = details {
1096 tracing::debug!("Failure details: {:?}", details);
1097 }
1098
1099 let payload = crate::runbeam_api::resources::ChangeFailedRequest { error, details };
1100
1101 let response = self
1102 .client
1103 .post(&url)
1104 .header("Authorization", format!("Bearer {}", token.into()))
1105 .header("Content-Type", "application/json")
1106 .json(&payload)
1107 .send()
1108 .await
1109 .map_err(ApiError::from)?;
1110
1111 if !response.status().is_success() {
1112 let status = response.status();
1113 let error_body = response
1114 .text()
1115 .await
1116 .unwrap_or_else(|_| "Unknown error".to_string());
1117 tracing::error!(
1118 "Failed to mark change as failed: HTTP {} - {}",
1119 status,
1120 error_body
1121 );
1122 return Err(RunbeamError::Api(ApiError::Http {
1123 status: status.as_u16(),
1124 message: error_body,
1125 }));
1126 }
1127
1128 response.json().await.map_err(|e| {
1129 tracing::error!("Failed to parse failed response: {}", e);
1130 RunbeamError::Api(ApiError::Parse(format!("Failed to parse response: {}", e)))
1131 })
1132 }
1133
1134 pub async fn store_config(
1209 &self,
1210 token: impl Into<String>,
1211 config_type: impl Into<String>,
1212 id: Option<String>,
1213 config: impl Into<String>,
1214 ) -> Result<StoreConfigResponse, RunbeamError> {
1215 let config_type = config_type.into();
1216 let config = config.into();
1217 let url = format!("{}/harmony/update", self.base_url);
1218
1219 tracing::info!(
1220 "Storing {} configuration to Runbeam Cloud (id: {:?})",
1221 config_type,
1222 id
1223 );
1224 tracing::debug!("Configuration length: {} bytes", config.len());
1225
1226 let payload = StoreConfigRequest {
1227 config_type: config_type.clone(),
1228 id: id.clone(),
1229 config,
1230 };
1231
1232 let response = self
1233 .client
1234 .post(&url)
1235 .header("Authorization", format!("Bearer {}", token.into()))
1236 .header("Content-Type", "application/json")
1237 .json(&payload)
1238 .send()
1239 .await
1240 .map_err(|e| {
1241 tracing::error!("Failed to send store config request: {}", e);
1242 ApiError::from(e)
1243 })?;
1244
1245 let status = response.status();
1246 tracing::debug!("Received response with status: {}", status);
1247
1248 if !status.is_success() {
1250 let error_body = response
1251 .text()
1252 .await
1253 .unwrap_or_else(|_| "Unknown error".to_string());
1254
1255 tracing::error!(
1256 "Store config failed: HTTP {} - {}",
1257 status.as_u16(),
1258 error_body
1259 );
1260
1261 return Err(RunbeamError::Api(ApiError::Http {
1262 status: status.as_u16(),
1263 message: error_body,
1264 }));
1265 }
1266
1267 let response_data = response.json::<StoreConfigResponse>().await.map_err(|e| {
1269 tracing::error!("Failed to parse store config response: {}", e);
1270 ApiError::Parse(format!("Failed to parse response: {}", e))
1271 })?;
1272
1273 tracing::info!(
1274 "Configuration stored successfully: type={}, id={:?}, action={}",
1275 config_type,
1276 id,
1277 response_data.data.model.action
1278 );
1279
1280 Ok(response_data)
1281 }
1282}
1283
1284#[cfg(test)]
1285mod tests {
1286 use super::*;
1287
1288 #[test]
1289 fn test_client_creation() {
1290 let client = RunbeamClient::new("http://example.com");
1291 assert_eq!(client.base_url(), "http://example.com");
1292 }
1293
1294 #[test]
1295 fn test_client_creation_with_string() {
1296 let base_url = String::from("http://example.com");
1297 let client = RunbeamClient::new(base_url);
1298 assert_eq!(client.base_url(), "http://example.com");
1299 }
1300
1301 #[test]
1302 fn test_authorize_request_serialization() {
1303 let request = AuthorizeRequest {
1304 token: "test_token".to_string(),
1305 gateway_code: "gw123".to_string(),
1306 machine_public_key: Some("pubkey123".to_string()),
1307 metadata: None,
1308 };
1309
1310 let json = serde_json::to_string(&request).unwrap();
1311 assert!(json.contains("\"token\":\"test_token\""));
1312 assert!(json.contains("\"gateway_code\":\"gw123\""));
1313 assert!(json.contains("\"machine_public_key\":\"pubkey123\""));
1314 }
1315
1316 #[test]
1317 fn test_authorize_request_serialization_without_optional_fields() {
1318 let request = AuthorizeRequest {
1319 token: "test_token".to_string(),
1320 gateway_code: "gw123".to_string(),
1321 machine_public_key: None,
1322 metadata: None,
1323 };
1324
1325 let json = serde_json::to_string(&request).unwrap();
1326 assert!(json.contains("\"token\":\"test_token\""));
1327 assert!(json.contains("\"gateway_code\":\"gw123\""));
1328 assert!(!json.contains("machine_public_key"));
1330 assert!(!json.contains("metadata"));
1331 }
1332
1333 #[test]
1334 fn test_change_serialization() {
1335 use crate::runbeam_api::resources::Change;
1336
1337 let change_metadata = Change {
1339 id: "01JBXXXXXXXXXXXXXXXXXXXXXXXXXX".to_string(),
1340 status: Some("pending".to_string()),
1341 resource_type: "gateway".to_string(),
1342 gateway_id: "01JBXXXXXXXXXXXXXXXXXXXXXXXXXX".to_string(),
1343 pipeline_id: None,
1344 toml_config: None,
1345 metadata: None,
1346 created_at: "2025-01-07T01:00:00+00:00".to_string(),
1347 acknowledged_at: None,
1348 applied_at: None,
1349 failed_at: None,
1350 error_message: None,
1351 error_details: None,
1352 };
1353
1354 let json = serde_json::to_string(&change_metadata).unwrap();
1355 assert!(json.contains("\"id\":\"01JBXXXXXXXXXXXXXXXXXXXXXXXXXX\""));
1356 assert!(json.contains("\"gateway_id\":\"01JBXXXXXXXXXXXXXXXXXXXXXXXXXX\""));
1357 assert!(json.contains("\"type\":\"gateway\""));
1358
1359 let deserialized: Change = serde_json::from_str(&json).unwrap();
1361 assert_eq!(deserialized.id, "01JBXXXXXXXXXXXXXXXXXXXXXXXXXX");
1362 assert_eq!(deserialized.status, Some("pending".to_string()));
1363 assert_eq!(deserialized.resource_type, "gateway");
1364
1365 let change_detail = Change {
1367 id: "01JBXXXXXXXXXXXXXXXXXXXXXXXXXX".to_string(),
1368 status: Some("applied".to_string()),
1369 resource_type: "gateway".to_string(),
1370 gateway_id: "01JBXXXXXXXXXXXXXXXXXXXXXXXXXX".to_string(),
1371 pipeline_id: None,
1372 toml_config: Some("[proxy]\nname = \"test\"".to_string()),
1373 metadata: Some(serde_json::json!({"gateway_name": "test-gateway"})),
1374 created_at: "2025-01-07T01:00:00+00:00".to_string(),
1375 acknowledged_at: Some("2025-01-07T01:00:05+00:00".to_string()),
1376 applied_at: Some("2025-01-07T01:00:10+00:00".to_string()),
1377 failed_at: None,
1378 error_message: None,
1379 error_details: None,
1380 };
1381
1382 let json = serde_json::to_string(&change_detail).unwrap();
1383 assert!(json.contains("toml_config"));
1384 assert!(json.contains("acknowledged_at"));
1385 assert!(json.contains("applied_at"));
1386
1387 let deserialized: Change = serde_json::from_str(&json).unwrap();
1389 assert!(deserialized.toml_config.is_some());
1390 assert!(deserialized.acknowledged_at.is_some());
1391 assert!(deserialized.applied_at.is_some());
1392 }
1393
1394 #[test]
1395 fn test_acknowledge_changes_request_serialization() {
1396 use crate::runbeam_api::resources::AcknowledgeChangesRequest;
1397
1398 let request = AcknowledgeChangesRequest {
1399 change_ids: vec![
1400 "change-1".to_string(),
1401 "change-2".to_string(),
1402 "change-3".to_string(),
1403 ],
1404 };
1405
1406 let json = serde_json::to_string(&request).unwrap();
1407 assert!(json.contains("\"change_ids\""));
1408 assert!(json.contains("\"change-1\""));
1409 assert!(json.contains("\"change-2\""));
1410 assert!(json.contains("\"change-3\""));
1411
1412 let deserialized: AcknowledgeChangesRequest = serde_json::from_str(&json).unwrap();
1414 assert_eq!(deserialized.change_ids.len(), 3);
1415 assert_eq!(deserialized.change_ids[0], "change-1");
1416 }
1417
1418 #[test]
1419 fn test_change_failed_request_serialization() {
1420 use crate::runbeam_api::resources::ChangeFailedRequest;
1421
1422 let request_with_details = ChangeFailedRequest {
1424 error: "Configuration parse error".to_string(),
1425 details: Some(vec![
1426 "Invalid JSON at line 42".to_string(),
1427 "Missing required field 'name'".to_string(),
1428 ]),
1429 };
1430
1431 let json = serde_json::to_string(&request_with_details).unwrap();
1432 assert!(json.contains("\"error\":\"Configuration parse error\""));
1433 assert!(json.contains("\"details\""));
1434 assert!(json.contains("Invalid JSON at line 42"));
1435
1436 let request_without_details = ChangeFailedRequest {
1438 error: "Unknown error".to_string(),
1439 details: None,
1440 };
1441
1442 let json = serde_json::to_string(&request_without_details).unwrap();
1443 assert!(json.contains("\"error\":\"Unknown error\""));
1444 assert!(!json.contains("\"details\"")); let deserialized: ChangeFailedRequest =
1448 serde_json::from_str(&serde_json::to_string(&request_with_details).unwrap()).unwrap();
1449 assert_eq!(deserialized.error, "Configuration parse error");
1450 assert!(deserialized.details.is_some());
1451 assert_eq!(deserialized.details.unwrap().len(), 2);
1452 }
1453
1454 #[test]
1455 fn test_base_url_response_serialization() {
1456 use crate::runbeam_api::resources::BaseUrlResponse;
1457
1458 let response = BaseUrlResponse {
1459 base_url: "https://api.runbeam.io".to_string(),
1460 changes_path: Some("/api/changes".to_string()),
1461 full_url: Some("https://api.runbeam.io/api/changes".to_string()),
1462 };
1463
1464 let json = serde_json::to_string(&response).unwrap();
1465 assert!(json.contains("\"base_url\":\"https://api.runbeam.io\""));
1466
1467 let deserialized: BaseUrlResponse = serde_json::from_str(&json).unwrap();
1469 assert_eq!(deserialized.base_url, "https://api.runbeam.io");
1470 assert_eq!(deserialized.changes_path, Some("/api/changes".to_string()));
1471 assert_eq!(
1472 deserialized.full_url,
1473 Some("https://api.runbeam.io/api/changes".to_string())
1474 );
1475 }
1476
1477 #[test]
1478 fn test_store_config_request_serialization_with_id() {
1479 let request = StoreConfigRequest {
1480 config_type: "gateway".to_string(),
1481 id: Some("01k8ek6h9aahhnrv3benret1nn".to_string()),
1482 config: "[proxy]\nid = \"test\"\n".to_string(),
1483 };
1484
1485 let json = serde_json::to_string(&request).unwrap();
1486 assert!(json.contains("\"type\":\"gateway\""));
1488 assert!(json.contains("\"id\":\"01k8ek6h9aahhnrv3benret1nn\""));
1489 assert!(json.contains("\"config\":"));
1490 assert!(json.contains("[proxy]"));
1491
1492 let deserialized: StoreConfigRequest = serde_json::from_str(&json).unwrap();
1494 assert_eq!(deserialized.config_type, "gateway");
1495 assert_eq!(
1496 deserialized.id,
1497 Some("01k8ek6h9aahhnrv3benret1nn".to_string())
1498 );
1499 }
1500
1501 #[test]
1502 fn test_store_config_request_serialization_without_id() {
1503 let request = StoreConfigRequest {
1504 config_type: "pipeline".to_string(),
1505 id: None,
1506 config: "[pipeline]\nname = \"test\"\n".to_string(),
1507 };
1508
1509 let json = serde_json::to_string(&request).unwrap();
1510 assert!(json.contains("\"type\":\"pipeline\""));
1511 assert!(json.contains("\"config\":"));
1512 assert!(!json.contains("\"id\""));
1514
1515 let deserialized: StoreConfigRequest = serde_json::from_str(&json).unwrap();
1517 assert_eq!(deserialized.config_type, "pipeline");
1518 assert_eq!(deserialized.id, None);
1519 }
1520
1521 #[test]
1522 fn test_store_config_request_field_rename() {
1523 let json = r#"{"type":"transform","config":"[transform]\nname = \"test\"\n"}"#;
1525 let request: StoreConfigRequest = serde_json::from_str(json).unwrap();
1526 assert_eq!(request.config_type, "transform");
1527 assert_eq!(request.id, None);
1528
1529 let serialized = serde_json::to_string(&request).unwrap();
1531 assert!(serialized.contains("\"type\":"));
1532 assert!(!serialized.contains("\"config_type\":"));
1533 }
1534
1535 #[test]
1536 fn test_store_config_response_serialization() {
1537 use crate::runbeam_api::types::{StoreConfigModel, StoreConfigResponseData};
1538
1539 let response = StoreConfigResponse {
1540 success: true,
1541 message: "Configuration stored successfully".to_string(),
1542 data: StoreConfigResponseData {
1543 model: StoreConfigModel {
1544 id: "01k9npa4tatmwddk66xxpcr2r0".to_string(),
1545 model_type: "gateway".to_string(),
1546 action: "updated".to_string(),
1547 },
1548 },
1549 };
1550
1551 let json = serde_json::to_string(&response).unwrap();
1552 assert!(json.contains("\"success\":true"));
1553 assert!(json.contains("Configuration stored successfully"));
1554
1555 let deserialized: StoreConfigResponse = serde_json::from_str(&json).unwrap();
1557 assert_eq!(deserialized.success, true);
1558 assert_eq!(deserialized.message, "Configuration stored successfully");
1559 assert_eq!(deserialized.data.model.id, "01k9npa4tatmwddk66xxpcr2r0");
1560 }
1561
1562 #[test]
1563 fn test_acknowledge_changes_response_serialization() {
1564 use crate::runbeam_api::resources::AcknowledgeChangesResponse;
1565
1566 let response = AcknowledgeChangesResponse {
1568 acknowledged: vec![
1569 "change-1".to_string(),
1570 "change-2".to_string(),
1571 "change-3".to_string(),
1572 ],
1573 failed: vec![],
1574 };
1575
1576 let json = serde_json::to_string(&response).unwrap();
1577 assert!(json.contains("\"acknowledged\":"));
1578 assert!(json.contains("\"failed\":"));
1579 assert!(json.contains("change-1"));
1580
1581 let deserialized: AcknowledgeChangesResponse = serde_json::from_str(&json).unwrap();
1583 assert_eq!(deserialized.acknowledged.len(), 3);
1584 assert_eq!(deserialized.failed.len(), 0);
1585
1586 let response_with_failures = AcknowledgeChangesResponse {
1588 acknowledged: vec!["change-1".to_string()],
1589 failed: vec!["change-2".to_string(), "change-3".to_string()],
1590 };
1591
1592 let json = serde_json::to_string(&response_with_failures).unwrap();
1593 let deserialized: AcknowledgeChangesResponse = serde_json::from_str(&json).unwrap();
1594 assert_eq!(deserialized.acknowledged.len(), 1);
1595 assert_eq!(deserialized.failed.len(), 2);
1596 }
1597
1598 #[test]
1599 fn test_change_status_response_serialization() {
1600 use crate::runbeam_api::resources::{
1601 ChangeAppliedResponse, ChangeFailedResponse, ChangeStatusResponse,
1602 };
1603
1604 let response = ChangeStatusResponse {
1606 success: true,
1607 message: "Change marked as applied".to_string(),
1608 };
1609
1610 let json = serde_json::to_string(&response).unwrap();
1611 assert!(json.contains("\"success\":true"));
1612 assert!(json.contains("\"message\":\"Change marked as applied\""));
1613
1614 let deserialized: ChangeStatusResponse = serde_json::from_str(&json).unwrap();
1616 assert_eq!(deserialized.success, true);
1617 assert_eq!(deserialized.message, "Change marked as applied");
1618
1619 let applied_response: ChangeAppliedResponse = ChangeStatusResponse {
1621 success: true,
1622 message: "Change marked as applied".to_string(),
1623 };
1624
1625 let json = serde_json::to_string(&applied_response).unwrap();
1626 let deserialized: ChangeAppliedResponse = serde_json::from_str(&json).unwrap();
1627 assert_eq!(deserialized.success, true);
1628
1629 let failed_response: ChangeFailedResponse = ChangeStatusResponse {
1631 success: true,
1632 message: "Change marked as failed".to_string(),
1633 };
1634
1635 let json = serde_json::to_string(&failed_response).unwrap();
1636 let deserialized: ChangeFailedResponse = serde_json::from_str(&json).unwrap();
1637 assert_eq!(deserialized.success, true);
1638 assert_eq!(deserialized.message, "Change marked as failed");
1639 }
1640}