1use crate::runbeam_api::types::{
2 ApiError, AuthorizeResponse, ConfigChange, ConfigChangeAck, ConfigChangeDetail, RunbeamError,
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!("{}/api/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_config_changes(
209 &self,
210 gateway_token: impl Into<String>,
211 ) -> Result<Vec<ConfigChange>, RunbeamError> {
212 let url = format!("{}/api/harmony/config-changes", self.base_url);
213
214 let response = self
215 .client
216 .get(&url)
217 .header("Authorization", format!("Bearer {}", gateway_token.into()))
218 .send()
219 .await
220 .map_err(ApiError::from)?;
221
222 if !response.status().is_success() {
223 let status = response.status();
224 let error_body = response
225 .text()
226 .await
227 .unwrap_or_else(|_| "Unknown error".to_string());
228 return Err(RunbeamError::Api(ApiError::Http {
229 status: status.as_u16(),
230 message: error_body,
231 }));
232 }
233
234 response.json().await.map_err(|e| {
235 RunbeamError::Api(ApiError::Parse(format!("Failed to parse response: {}", e)))
236 })
237 }
238
239 pub async fn get_config_change(
246 &self,
247 gateway_token: impl Into<String>,
248 change_id: impl Into<String>,
249 ) -> Result<ConfigChangeDetail, RunbeamError> {
250 let url = format!(
251 "{}/api/harmony/config-changes/{}",
252 self.base_url,
253 change_id.into()
254 );
255
256 let response = self
257 .client
258 .get(&url)
259 .header("Authorization", format!("Bearer {}", gateway_token.into()))
260 .send()
261 .await
262 .map_err(ApiError::from)?;
263
264 if !response.status().is_success() {
265 let status = response.status();
266 let error_body = response
267 .text()
268 .await
269 .unwrap_or_else(|_| "Unknown error".to_string());
270 return Err(RunbeamError::Api(ApiError::Http {
271 status: status.as_u16(),
272 message: error_body,
273 }));
274 }
275
276 response.json().await.map_err(|e| {
277 RunbeamError::Api(ApiError::Parse(format!("Failed to parse response: {}", e)))
278 })
279 }
280
281 pub async fn acknowledge_config_change(
288 &self,
289 gateway_token: impl Into<String>,
290 change_id: impl Into<String>,
291 ) -> Result<ConfigChangeAck, RunbeamError> {
292 let url = format!(
293 "{}/api/harmony/config-changes/{}/acknowledge",
294 self.base_url,
295 change_id.into()
296 );
297
298 let response = self
299 .client
300 .post(&url)
301 .header("Authorization", format!("Bearer {}", gateway_token.into()))
302 .send()
303 .await
304 .map_err(ApiError::from)?;
305
306 if !response.status().is_success() {
307 let status = response.status();
308 let error_body = response
309 .text()
310 .await
311 .unwrap_or_else(|_| "Unknown error".to_string());
312 return Err(RunbeamError::Api(ApiError::Http {
313 status: status.as_u16(),
314 message: error_body,
315 }));
316 }
317
318 response.json().await.map_err(|e| {
319 RunbeamError::Api(ApiError::Parse(format!("Failed to parse response: {}", e)))
320 })
321 }
322
323 pub async fn report_config_applied(
330 &self,
331 gateway_token: impl Into<String>,
332 change_id: impl Into<String>,
333 ) -> Result<ConfigChangeAck, RunbeamError> {
334 let url = format!(
335 "{}/api/harmony/config-changes/{}/applied",
336 self.base_url,
337 change_id.into()
338 );
339
340 let response = self
341 .client
342 .post(&url)
343 .header("Authorization", format!("Bearer {}", gateway_token.into()))
344 .send()
345 .await
346 .map_err(ApiError::from)?;
347
348 if !response.status().is_success() {
349 let status = response.status();
350 let error_body = response
351 .text()
352 .await
353 .unwrap_or_else(|_| "Unknown error".to_string());
354 return Err(RunbeamError::Api(ApiError::Http {
355 status: status.as_u16(),
356 message: error_body,
357 }));
358 }
359
360 response.json().await.map_err(|e| {
361 RunbeamError::Api(ApiError::Parse(format!("Failed to parse response: {}", e)))
362 })
363 }
364
365 pub async fn report_config_failed(
373 &self,
374 gateway_token: impl Into<String>,
375 change_id: impl Into<String>,
376 error: impl Into<String>,
377 ) -> Result<ConfigChangeAck, RunbeamError> {
378 let url = format!(
379 "{}/api/harmony/config-changes/{}/failed",
380 self.base_url,
381 change_id.into()
382 );
383
384 #[derive(Serialize)]
385 struct FailurePayload {
386 error: String,
387 }
388
389 let payload = FailurePayload {
390 error: error.into(),
391 };
392
393 let response = self
394 .client
395 .post(&url)
396 .header("Authorization", format!("Bearer {}", gateway_token.into()))
397 .json(&payload)
398 .send()
399 .await
400 .map_err(ApiError::from)?;
401
402 if !response.status().is_success() {
403 let status = response.status();
404 let error_body = response
405 .text()
406 .await
407 .unwrap_or_else(|_| "Unknown error".to_string());
408 return Err(RunbeamError::Api(ApiError::Http {
409 status: status.as_u16(),
410 message: error_body,
411 }));
412 }
413
414 response.json().await.map_err(|e| {
415 RunbeamError::Api(ApiError::Parse(format!("Failed to parse response: {}", e)))
416 })
417 }
418
419 pub async fn list_gateways(
432 &self,
433 token: impl Into<String>,
434 ) -> Result<
435 crate::runbeam_api::resources::PaginatedResponse<crate::runbeam_api::resources::Gateway>,
436 RunbeamError,
437 > {
438 let url = format!("{}/api/gateways", self.base_url);
439
440 let response = self
441 .client
442 .get(&url)
443 .header("Authorization", format!("Bearer {}", token.into()))
444 .send()
445 .await
446 .map_err(ApiError::from)?;
447
448 if !response.status().is_success() {
449 let status = response.status();
450 let error_body = response
451 .text()
452 .await
453 .unwrap_or_else(|_| "Unknown error".to_string());
454 return Err(RunbeamError::Api(ApiError::Http {
455 status: status.as_u16(),
456 message: error_body,
457 }));
458 }
459
460 response.json().await.map_err(|e| {
461 RunbeamError::Api(ApiError::Parse(format!("Failed to parse response: {}", e)))
462 })
463 }
464
465 pub async fn get_gateway(
477 &self,
478 token: impl Into<String>,
479 gateway_id: impl Into<String>,
480 ) -> Result<
481 crate::runbeam_api::resources::ResourceResponse<crate::runbeam_api::resources::Gateway>,
482 RunbeamError,
483 > {
484 let url = format!("{}/api/gateways/{}", self.base_url, gateway_id.into());
485
486 let response = self
487 .client
488 .get(&url)
489 .header("Authorization", format!("Bearer {}", token.into()))
490 .send()
491 .await
492 .map_err(ApiError::from)?;
493
494 if !response.status().is_success() {
495 let status = response.status();
496 let error_body = response
497 .text()
498 .await
499 .unwrap_or_else(|_| "Unknown error".to_string());
500 return Err(RunbeamError::Api(ApiError::Http {
501 status: status.as_u16(),
502 message: error_body,
503 }));
504 }
505
506 response.json().await.map_err(|e| {
507 RunbeamError::Api(ApiError::Parse(format!("Failed to parse response: {}", e)))
508 })
509 }
510
511 pub async fn list_services(
524 &self,
525 token: impl Into<String>,
526 ) -> Result<
527 crate::runbeam_api::resources::PaginatedResponse<crate::runbeam_api::resources::Service>,
528 RunbeamError,
529 > {
530 let url = format!("{}/api/services", self.base_url);
531
532 let response = self
533 .client
534 .get(&url)
535 .header("Authorization", format!("Bearer {}", token.into()))
536 .send()
537 .await
538 .map_err(ApiError::from)?;
539
540 if !response.status().is_success() {
541 let status = response.status();
542 let error_body = response
543 .text()
544 .await
545 .unwrap_or_else(|_| "Unknown error".to_string());
546 return Err(RunbeamError::Api(ApiError::Http {
547 status: status.as_u16(),
548 message: error_body,
549 }));
550 }
551
552 response.json().await.map_err(|e| {
553 RunbeamError::Api(ApiError::Parse(format!("Failed to parse response: {}", e)))
554 })
555 }
556
557 pub async fn get_service(
569 &self,
570 token: impl Into<String>,
571 service_id: impl Into<String>,
572 ) -> Result<
573 crate::runbeam_api::resources::ResourceResponse<crate::runbeam_api::resources::Service>,
574 RunbeamError,
575 > {
576 let url = format!("{}/api/services/{}", self.base_url, service_id.into());
577
578 let response = self
579 .client
580 .get(&url)
581 .header("Authorization", format!("Bearer {}", token.into()))
582 .send()
583 .await
584 .map_err(ApiError::from)?;
585
586 if !response.status().is_success() {
587 let status = response.status();
588 let error_body = response
589 .text()
590 .await
591 .unwrap_or_else(|_| "Unknown error".to_string());
592 return Err(RunbeamError::Api(ApiError::Http {
593 status: status.as_u16(),
594 message: error_body,
595 }));
596 }
597
598 response.json().await.map_err(|e| {
599 RunbeamError::Api(ApiError::Parse(format!("Failed to parse response: {}", e)))
600 })
601 }
602
603 pub async fn list_endpoints(
614 &self,
615 token: impl Into<String>,
616 ) -> Result<
617 crate::runbeam_api::resources::PaginatedResponse<crate::runbeam_api::resources::Endpoint>,
618 RunbeamError,
619 > {
620 let url = format!("{}/api/endpoints", self.base_url);
621
622 let response = self
623 .client
624 .get(&url)
625 .header("Authorization", format!("Bearer {}", token.into()))
626 .send()
627 .await
628 .map_err(ApiError::from)?;
629
630 if !response.status().is_success() {
631 let status = response.status();
632 let error_body = response
633 .text()
634 .await
635 .unwrap_or_else(|_| "Unknown error".to_string());
636 return Err(RunbeamError::Api(ApiError::Http {
637 status: status.as_u16(),
638 message: error_body,
639 }));
640 }
641
642 response.json().await.map_err(|e| {
643 RunbeamError::Api(ApiError::Parse(format!("Failed to parse response: {}", e)))
644 })
645 }
646
647 pub async fn list_backends(
658 &self,
659 token: impl Into<String>,
660 ) -> Result<
661 crate::runbeam_api::resources::PaginatedResponse<crate::runbeam_api::resources::Backend>,
662 RunbeamError,
663 > {
664 let url = format!("{}/api/backends", self.base_url);
665
666 let response = self
667 .client
668 .get(&url)
669 .header("Authorization", format!("Bearer {}", token.into()))
670 .send()
671 .await
672 .map_err(ApiError::from)?;
673
674 if !response.status().is_success() {
675 let status = response.status();
676 let error_body = response
677 .text()
678 .await
679 .unwrap_or_else(|_| "Unknown error".to_string());
680 return Err(RunbeamError::Api(ApiError::Http {
681 status: status.as_u16(),
682 message: error_body,
683 }));
684 }
685
686 response.json().await.map_err(|e| {
687 RunbeamError::Api(ApiError::Parse(format!("Failed to parse response: {}", e)))
688 })
689 }
690
691 pub async fn list_pipelines(
702 &self,
703 token: impl Into<String>,
704 ) -> Result<
705 crate::runbeam_api::resources::PaginatedResponse<crate::runbeam_api::resources::Pipeline>,
706 RunbeamError,
707 > {
708 let url = format!("{}/api/pipelines", self.base_url);
709
710 let response = self
711 .client
712 .get(&url)
713 .header("Authorization", format!("Bearer {}", token.into()))
714 .send()
715 .await
716 .map_err(ApiError::from)?;
717
718 if !response.status().is_success() {
719 let status = response.status();
720 let error_body = response
721 .text()
722 .await
723 .unwrap_or_else(|_| "Unknown error".to_string());
724 return Err(RunbeamError::Api(ApiError::Http {
725 status: status.as_u16(),
726 message: error_body,
727 }));
728 }
729
730 response.json().await.map_err(|e| {
731 RunbeamError::Api(ApiError::Parse(format!("Failed to parse response: {}", e)))
732 })
733 }
734
735 pub async fn get_base_url(
763 &self,
764 token: impl Into<String>,
765 ) -> Result<crate::runbeam_api::resources::BaseUrlResponse, RunbeamError> {
766 let url = format!("{}/gateway/base-url", self.base_url);
767
768 tracing::debug!("Getting base URL from: {}", url);
769
770 let response = self
771 .client
772 .get(&url)
773 .header("Authorization", format!("Bearer {}", token.into()))
774 .send()
775 .await
776 .map_err(ApiError::from)?;
777
778 if !response.status().is_success() {
779 let status = response.status();
780 let error_body = response
781 .text()
782 .await
783 .unwrap_or_else(|_| "Unknown error".to_string());
784 tracing::error!("Failed to get base URL: HTTP {} - {}", status, error_body);
785 return Err(RunbeamError::Api(ApiError::Http {
786 status: status.as_u16(),
787 message: error_body,
788 }));
789 }
790
791 response.json().await.map_err(|e| {
792 tracing::error!("Failed to parse base URL response: {}", e);
793 RunbeamError::Api(ApiError::Parse(format!("Failed to parse response: {}", e)))
794 })
795 }
796
797 pub async fn list_changes(
823 &self,
824 token: impl Into<String>,
825 ) -> Result<
826 crate::runbeam_api::resources::PaginatedResponse<crate::runbeam_api::resources::Change>,
827 RunbeamError,
828 > {
829 let url = format!("{}/gateway/changes", self.base_url);
830
831 tracing::debug!("Listing changes from: {}", url);
832
833 let response = self
834 .client
835 .get(&url)
836 .header("Authorization", format!("Bearer {}", token.into()))
837 .send()
838 .await
839 .map_err(ApiError::from)?;
840
841 if !response.status().is_success() {
842 let status = response.status();
843 let error_body = response
844 .text()
845 .await
846 .unwrap_or_else(|_| "Unknown error".to_string());
847 tracing::error!("Failed to list changes: HTTP {} - {}", status, error_body);
848 return Err(RunbeamError::Api(ApiError::Http {
849 status: status.as_u16(),
850 message: error_body,
851 }));
852 }
853
854 response.json().await.map_err(|e| {
855 tracing::error!("Failed to parse changes response: {}", e);
856 RunbeamError::Api(ApiError::Parse(format!("Failed to parse response: {}", e)))
857 })
858 }
859
860 pub async fn get_change(
886 &self,
887 token: impl Into<String>,
888 change_id: impl Into<String>,
889 ) -> Result<
890 crate::runbeam_api::resources::ResourceResponse<crate::runbeam_api::resources::Change>,
891 RunbeamError,
892 > {
893 let change_id = change_id.into();
894 let url = format!("{}/gateway/changes/{}", self.base_url, change_id);
895
896 tracing::debug!("Getting change {} from: {}", change_id, url);
897
898 let response = self
899 .client
900 .get(&url)
901 .header("Authorization", format!("Bearer {}", token.into()))
902 .send()
903 .await
904 .map_err(ApiError::from)?;
905
906 if !response.status().is_success() {
907 let status = response.status();
908 let error_body = response
909 .text()
910 .await
911 .unwrap_or_else(|_| "Unknown error".to_string());
912 tracing::error!("Failed to get change: HTTP {} - {}", status, error_body);
913 return Err(RunbeamError::Api(ApiError::Http {
914 status: status.as_u16(),
915 message: error_body,
916 }));
917 }
918
919 response.json().await.map_err(|e| {
920 tracing::error!("Failed to parse change response: {}", e);
921 RunbeamError::Api(ApiError::Parse(format!("Failed to parse response: {}", e)))
922 })
923 }
924
925 pub async fn acknowledge_changes(
953 &self,
954 token: impl Into<String>,
955 change_ids: Vec<String>,
956 ) -> Result<serde_json::Value, RunbeamError> {
957 let url = format!("{}/gateway/changes/acknowledge", self.base_url);
958
959 tracing::info!("Acknowledging {} changes", change_ids.len());
960 tracing::debug!("Change IDs: {:?}", change_ids);
961
962 let payload = crate::runbeam_api::resources::AcknowledgeChangesRequest { change_ids };
963
964 let response = self
965 .client
966 .post(&url)
967 .header("Authorization", format!("Bearer {}", token.into()))
968 .header("Content-Type", "application/json")
969 .json(&payload)
970 .send()
971 .await
972 .map_err(ApiError::from)?;
973
974 if !response.status().is_success() {
975 let status = response.status();
976 let error_body = response
977 .text()
978 .await
979 .unwrap_or_else(|_| "Unknown error".to_string());
980 tracing::error!(
981 "Failed to acknowledge changes: HTTP {} - {}",
982 status,
983 error_body
984 );
985 return Err(RunbeamError::Api(ApiError::Http {
986 status: status.as_u16(),
987 message: error_body,
988 }));
989 }
990
991 response.json().await.map_err(|e| {
992 tracing::error!("Failed to parse acknowledge response: {}", e);
993 RunbeamError::Api(ApiError::Parse(format!("Failed to parse response: {}", e)))
994 })
995 }
996
997 pub async fn mark_change_applied(
1023 &self,
1024 token: impl Into<String>,
1025 change_id: impl Into<String>,
1026 ) -> Result<serde_json::Value, RunbeamError> {
1027 let change_id = change_id.into();
1028 let url = format!("{}/gateway/changes/{}/applied", self.base_url, change_id);
1029
1030 tracing::info!("Marking change {} as applied", change_id);
1031
1032 let response = self
1033 .client
1034 .post(&url)
1035 .header("Authorization", format!("Bearer {}", token.into()))
1036 .send()
1037 .await
1038 .map_err(ApiError::from)?;
1039
1040 if !response.status().is_success() {
1041 let status = response.status();
1042 let error_body = response
1043 .text()
1044 .await
1045 .unwrap_or_else(|_| "Unknown error".to_string());
1046 tracing::error!(
1047 "Failed to mark change as applied: HTTP {} - {}",
1048 status,
1049 error_body
1050 );
1051 return Err(RunbeamError::Api(ApiError::Http {
1052 status: status.as_u16(),
1053 message: error_body,
1054 }));
1055 }
1056
1057 response.json().await.map_err(|e| {
1058 tracing::error!("Failed to parse applied response: {}", e);
1059 RunbeamError::Api(ApiError::Parse(format!("Failed to parse response: {}", e)))
1060 })
1061 }
1062
1063 pub async fn mark_change_failed(
1096 &self,
1097 token: impl Into<String>,
1098 change_id: impl Into<String>,
1099 error: String,
1100 details: Option<Vec<String>>,
1101 ) -> Result<serde_json::Value, RunbeamError> {
1102 let change_id = change_id.into();
1103 let url = format!("{}/gateway/changes/{}/failed", self.base_url, change_id);
1104
1105 tracing::warn!("Marking change {} as failed: {}", change_id, error);
1106 if let Some(ref details) = details {
1107 tracing::debug!("Failure details: {:?}", details);
1108 }
1109
1110 let payload = crate::runbeam_api::resources::ChangeFailedRequest { error, details };
1111
1112 let response = self
1113 .client
1114 .post(&url)
1115 .header("Authorization", format!("Bearer {}", token.into()))
1116 .header("Content-Type", "application/json")
1117 .json(&payload)
1118 .send()
1119 .await
1120 .map_err(ApiError::from)?;
1121
1122 if !response.status().is_success() {
1123 let status = response.status();
1124 let error_body = response
1125 .text()
1126 .await
1127 .unwrap_or_else(|_| "Unknown error".to_string());
1128 tracing::error!(
1129 "Failed to mark change as failed: HTTP {} - {}",
1130 status,
1131 error_body
1132 );
1133 return Err(RunbeamError::Api(ApiError::Http {
1134 status: status.as_u16(),
1135 message: error_body,
1136 }));
1137 }
1138
1139 response.json().await.map_err(|e| {
1140 tracing::error!("Failed to parse failed response: {}", e);
1141 RunbeamError::Api(ApiError::Parse(format!("Failed to parse response: {}", e)))
1142 })
1143 }
1144}
1145
1146#[cfg(test)]
1147mod tests {
1148 use super::*;
1149
1150 #[test]
1151 fn test_client_creation() {
1152 let client = RunbeamClient::new("http://example.com");
1153 assert_eq!(client.base_url(), "http://example.com");
1154 }
1155
1156 #[test]
1157 fn test_client_creation_with_string() {
1158 let base_url = String::from("http://example.com");
1159 let client = RunbeamClient::new(base_url);
1160 assert_eq!(client.base_url(), "http://example.com");
1161 }
1162
1163 #[test]
1164 fn test_authorize_request_serialization() {
1165 let request = AuthorizeRequest {
1166 token: "test_token".to_string(),
1167 gateway_code: "gw123".to_string(),
1168 machine_public_key: Some("pubkey123".to_string()),
1169 metadata: None,
1170 };
1171
1172 let json = serde_json::to_string(&request).unwrap();
1173 assert!(json.contains("\"token\":\"test_token\""));
1174 assert!(json.contains("\"gateway_code\":\"gw123\""));
1175 assert!(json.contains("\"machine_public_key\":\"pubkey123\""));
1176 }
1177
1178 #[test]
1179 fn test_authorize_request_serialization_without_optional_fields() {
1180 let request = AuthorizeRequest {
1181 token: "test_token".to_string(),
1182 gateway_code: "gw123".to_string(),
1183 machine_public_key: None,
1184 metadata: None,
1185 };
1186
1187 let json = serde_json::to_string(&request).unwrap();
1188 assert!(json.contains("\"token\":\"test_token\""));
1189 assert!(json.contains("\"gateway_code\":\"gw123\""));
1190 assert!(!json.contains("machine_public_key"));
1192 assert!(!json.contains("metadata"));
1193 }
1194
1195 #[test]
1196 fn test_change_serialization() {
1197 use crate::runbeam_api::resources::Change;
1198
1199 let change = Change {
1200 id: "change-123".to_string(),
1201 resource_type: "change".to_string(),
1202 gateway_id: "gateway-456".to_string(),
1203 status: "pending".to_string(),
1204 operation: "create".to_string(),
1205 change_resource_type: "endpoint".to_string(),
1206 resource_id: "endpoint-789".to_string(),
1207 payload: serde_json::json!({"name": "test-endpoint"}),
1208 error: None,
1209 created_at: "2024-01-01T00:00:00Z".to_string(),
1210 updated_at: "2024-01-01T00:00:00Z".to_string(),
1211 };
1212
1213 let json = serde_json::to_string(&change).unwrap();
1214 assert!(json.contains("\"id\":\"change-123\""));
1215 assert!(json.contains("\"gateway_id\":\"gateway-456\""));
1216 assert!(json.contains("\"status\":\"pending\""));
1217 assert!(json.contains("\"operation\":\"create\""));
1218
1219 let deserialized: Change = serde_json::from_str(&json).unwrap();
1221 assert_eq!(deserialized.id, "change-123");
1222 assert_eq!(deserialized.status, "pending");
1223 }
1224
1225 #[test]
1226 fn test_acknowledge_changes_request_serialization() {
1227 use crate::runbeam_api::resources::AcknowledgeChangesRequest;
1228
1229 let request = AcknowledgeChangesRequest {
1230 change_ids: vec![
1231 "change-1".to_string(),
1232 "change-2".to_string(),
1233 "change-3".to_string(),
1234 ],
1235 };
1236
1237 let json = serde_json::to_string(&request).unwrap();
1238 assert!(json.contains("\"change_ids\""));
1239 assert!(json.contains("\"change-1\""));
1240 assert!(json.contains("\"change-2\""));
1241 assert!(json.contains("\"change-3\""));
1242
1243 let deserialized: AcknowledgeChangesRequest = serde_json::from_str(&json).unwrap();
1245 assert_eq!(deserialized.change_ids.len(), 3);
1246 assert_eq!(deserialized.change_ids[0], "change-1");
1247 }
1248
1249 #[test]
1250 fn test_change_failed_request_serialization() {
1251 use crate::runbeam_api::resources::ChangeFailedRequest;
1252
1253 let request_with_details = ChangeFailedRequest {
1255 error: "Configuration parse error".to_string(),
1256 details: Some(vec![
1257 "Invalid JSON at line 42".to_string(),
1258 "Missing required field 'name'".to_string(),
1259 ]),
1260 };
1261
1262 let json = serde_json::to_string(&request_with_details).unwrap();
1263 assert!(json.contains("\"error\":\"Configuration parse error\""));
1264 assert!(json.contains("\"details\""));
1265 assert!(json.contains("Invalid JSON at line 42"));
1266
1267 let request_without_details = ChangeFailedRequest {
1269 error: "Unknown error".to_string(),
1270 details: None,
1271 };
1272
1273 let json = serde_json::to_string(&request_without_details).unwrap();
1274 assert!(json.contains("\"error\":\"Unknown error\""));
1275 assert!(!json.contains("\"details\"")); let deserialized: ChangeFailedRequest =
1279 serde_json::from_str(&serde_json::to_string(&request_with_details).unwrap()).unwrap();
1280 assert_eq!(deserialized.error, "Configuration parse error");
1281 assert!(deserialized.details.is_some());
1282 assert_eq!(deserialized.details.unwrap().len(), 2);
1283 }
1284
1285 #[test]
1286 fn test_base_url_response_serialization() {
1287 use crate::runbeam_api::resources::BaseUrlResponse;
1288
1289 let response = BaseUrlResponse {
1290 base_url: "https://api.runbeam.io".to_string(),
1291 };
1292
1293 let json = serde_json::to_string(&response).unwrap();
1294 assert!(json.contains("\"base_url\":\"https://api.runbeam.io\""));
1295
1296 let deserialized: BaseUrlResponse = serde_json::from_str(&json).unwrap();
1298 assert_eq!(deserialized.base_url, "https://api.runbeam.io");
1299 }
1300}