1use a2a::*;
4use a2a_pb::protojson_conv::{self, ProtoJsonPayload};
5use async_trait::async_trait;
6use futures::stream::BoxStream;
7use reqwest::Client;
8use serde::Deserialize;
9use serde_json::Value;
10
11use crate::push_config_compat::{
12 deserialize_list_task_push_notification_configs_response,
13 deserialize_task_push_notification_config,
14};
15use crate::transport::{ServiceParams, Transport, TransportFactory};
16
17const REST_SEND_MESSAGE_PATH: &str = "/message:send";
18const REST_STREAM_MESSAGE_PATH: &str = "/message:stream";
19const REST_EXTENDED_AGENT_CARD_PATH: &str = "/extendedAgentCard";
20
21#[derive(Debug, Deserialize)]
22struct RestErrorEnvelope {
23 error: RestErrorStatus,
24}
25
26#[derive(Debug, Deserialize)]
27struct RestErrorStatus {
28 message: String,
29
30 #[serde(default)]
31 details: Vec<TypedDetail>,
32}
33
34pub struct RestTransport {
38 client: Client,
39 base_url: String,
40}
41
42impl RestTransport {
43 pub fn new(client: Client, base_url: String) -> Self {
45 let base_url = base_url.trim_end_matches('/').to_string();
46 RestTransport { client, base_url }
47 }
48
49 fn build_request(
50 &self,
51 method: reqwest::Method,
52 path: &str,
53 params: &ServiceParams,
54 ) -> reqwest::RequestBuilder {
55 let url = format!("{}{}", self.base_url, path);
56 let mut builder = self.client.request(method, &url);
57 for (key, values) in params {
58 for v in values {
59 builder = builder.header(key, v);
60 }
61 }
62 builder
63 }
64
65 fn build_request_with_query(
66 &self,
67 method: reqwest::Method,
68 path: &str,
69 params: &ServiceParams,
70 query: &[(String, String)],
71 ) -> reqwest::RequestBuilder {
72 let builder = self.build_request(method, path, params);
73 if query.is_empty() {
74 builder
75 } else {
76 builder.query(query)
77 }
78 }
79
80 async fn send(&self, builder: reqwest::RequestBuilder) -> Result<reqwest::Response, A2AError> {
81 builder
82 .send()
83 .await
84 .map_err(|e| A2AError::internal(format!("HTTP request failed: {e}")))
85 }
86
87 async fn into_rest_error(resp: reqwest::Response) -> A2AError {
88 let status = resp.status();
89 let body = resp.text().await.unwrap_or_default();
90 parse_rest_error(status, &body)
91 }
92
93 async fn post_value<Req>(
94 &self,
95 path: &str,
96 params: &ServiceParams,
97 body: &Req,
98 ) -> Result<Value, A2AError>
99 where
100 Req: ProtoJsonPayload,
101 {
102 let payload = protojson_conv::to_value(body).map_err(|e| {
103 A2AError::internal(format!("failed to serialize request as ProtoJSON: {e}"))
104 })?;
105 let resp = self
106 .send(
107 self.build_request(reqwest::Method::POST, path, params)
108 .json(&payload),
109 )
110 .await?;
111
112 if !resp.status().is_success() {
113 return Err(Self::into_rest_error(resp).await);
114 }
115 let payload = resp
116 .json::<Value>()
117 .await
118 .map_err(|e| A2AError::internal(format!("failed to parse response: {e}")))?;
119
120 Ok(payload)
121 }
122
123 async fn post_json<Req, Resp>(
124 &self,
125 path: &str,
126 params: &ServiceParams,
127 body: &Req,
128 ) -> Result<Resp, A2AError>
129 where
130 Req: ProtoJsonPayload,
131 Resp: ProtoJsonPayload,
132 {
133 let payload = self.post_value(path, params, body).await?;
134
135 protojson_conv::from_value(payload).map_err(|e| {
136 A2AError::internal(format!("failed to deserialize response as ProtoJSON: {e}"))
137 })
138 }
139
140 async fn get_value(
141 &self,
142 path: &str,
143 params: &ServiceParams,
144 query: &[(String, String)],
145 ) -> Result<Value, A2AError> {
146 let resp = self
147 .send(self.build_request_with_query(reqwest::Method::GET, path, params, query))
148 .await?;
149
150 if !resp.status().is_success() {
151 return Err(Self::into_rest_error(resp).await);
152 }
153 let payload = resp
154 .json::<Value>()
155 .await
156 .map_err(|e| A2AError::internal(format!("failed to parse response: {e}")))?;
157
158 Ok(payload)
159 }
160
161 async fn get_json<Resp>(
162 &self,
163 path: &str,
164 params: &ServiceParams,
165 query: &[(String, String)],
166 ) -> Result<Resp, A2AError>
167 where
168 Resp: ProtoJsonPayload,
169 {
170 let payload = self.get_value(path, params, query).await?;
171
172 protojson_conv::from_value(payload).map_err(|e| {
173 A2AError::internal(format!("failed to deserialize response as ProtoJSON: {e}"))
174 })
175 }
176
177 async fn post_streaming<Req>(
178 &self,
179 path: &str,
180 params: &ServiceParams,
181 body: &Req,
182 ) -> Result<BoxStream<'static, Result<StreamResponse, A2AError>>, A2AError>
183 where
184 Req: ProtoJsonPayload,
185 {
186 let payload = protojson_conv::to_value(body).map_err(|e| {
187 A2AError::internal(format!("failed to serialize request as ProtoJSON: {e}"))
188 })?;
189 let resp = self
190 .send(
191 self.build_request(reqwest::Method::POST, path, params)
192 .header("Accept", "text/event-stream")
193 .json(&payload),
194 )
195 .await?;
196
197 if !resp.status().is_success() {
198 return Err(Self::into_rest_error(resp).await);
199 }
200
201 let stream = resp.bytes_stream();
202 Ok(crate::jsonrpc::parse_sse_stream_rest(stream))
203 }
204
205 async fn get_streaming(
206 &self,
207 path: &str,
208 params: &ServiceParams,
209 ) -> Result<BoxStream<'static, Result<StreamResponse, A2AError>>, A2AError> {
210 let resp = self
211 .send(
212 self.build_request(reqwest::Method::GET, path, params)
213 .header("Accept", "text/event-stream"),
214 )
215 .await?;
216
217 if !resp.status().is_success() {
218 return Err(Self::into_rest_error(resp).await);
219 }
220
221 let stream = resp.bytes_stream();
222 Ok(crate::jsonrpc::parse_sse_stream_rest(stream))
223 }
224
225 async fn delete(&self, path: &str, params: &ServiceParams) -> Result<(), A2AError> {
226 let resp = self
227 .send(self.build_request(reqwest::Method::DELETE, path, params))
228 .await?;
229
230 if !resp.status().is_success() {
231 return Err(Self::into_rest_error(resp).await);
232 }
233 Ok(())
234 }
235}
236
237fn parse_rest_error(status: reqwest::StatusCode, body: &str) -> A2AError {
238 let Ok(envelope) = serde_json::from_str::<RestErrorEnvelope>(body) else {
239 return A2AError::internal(format!("HTTP {status}: {body}"));
240 };
241
242 crate::a2a_error_from_details(
243 error_code::INTERNAL_ERROR,
244 envelope.error.message,
245 envelope.error.details,
246 )
247}
248
249fn should_retry_subscribe_with_legacy_path(err: &A2AError) -> bool {
250 if matches!(
251 err.code,
252 error_code::METHOD_NOT_FOUND | error_code::UNSUPPORTED_OPERATION
253 ) {
254 return true;
255 }
256
257 if err.code != error_code::INTERNAL_ERROR {
258 return false;
259 }
260
261 let msg = err.message.to_ascii_lowercase();
262 msg.contains("http 404")
263 || msg.contains("http 405")
264 || msg.contains("not found")
265 || msg.contains("method not allowed")
266}
267
268#[async_trait]
269impl Transport for RestTransport {
270 async fn send_message(
271 &self,
272 params: &ServiceParams,
273 req: &SendMessageRequest,
274 ) -> Result<SendMessageResponse, A2AError> {
275 self.post_json(REST_SEND_MESSAGE_PATH, params, req).await
276 }
277
278 async fn send_streaming_message(
279 &self,
280 params: &ServiceParams,
281 req: &SendMessageRequest,
282 ) -> Result<BoxStream<'static, Result<StreamResponse, A2AError>>, A2AError> {
283 self.post_streaming(REST_STREAM_MESSAGE_PATH, params, req)
284 .await
285 }
286
287 async fn get_task(
288 &self,
289 params: &ServiceParams,
290 req: &GetTaskRequest,
291 ) -> Result<Task, A2AError> {
292 let path = format!("/tasks/{}", req.id);
293 let mut query_parts = Vec::new();
294 if let Some(hl) = req.history_length {
295 query_parts.push(("historyLength".to_string(), hl.to_string()));
296 }
297 self.get_json(&path, params, &query_parts).await
298 }
299
300 async fn list_tasks(
301 &self,
302 params: &ServiceParams,
303 req: &ListTasksRequest,
304 ) -> Result<ListTasksResponse, A2AError> {
305 let mut query_parts = Vec::new();
306 if let Some(ref cid) = req.context_id {
307 query_parts.push(("contextId".to_string(), cid.clone()));
308 }
309 if let Some(ref status) = req.status {
310 let s = serde_json::to_value(status)
311 .ok()
312 .and_then(|v| v.as_str().map(String::from))
313 .unwrap_or_default();
314 query_parts.push(("status".to_string(), s));
315 }
316 if let Some(ps) = req.page_size {
317 query_parts.push(("pageSize".to_string(), ps.to_string()));
318 }
319 if let Some(ref pt) = req.page_token {
320 query_parts.push(("pageToken".to_string(), pt.clone()));
321 }
322 if let Some(hl) = req.history_length {
323 query_parts.push(("historyLength".to_string(), hl.to_string()));
324 }
325 if let Some(ref ts) = req.status_timestamp_after {
326 query_parts.push(("statusTimestampAfter".to_string(), ts.to_rfc3339()));
327 }
328 if let Some(ia) = req.include_artifacts {
329 query_parts.push(("includeArtifacts".to_string(), ia.to_string()));
330 }
331 self.get_json("/tasks", params, &query_parts).await
332 }
333
334 async fn cancel_task(
335 &self,
336 params: &ServiceParams,
337 req: &CancelTaskRequest,
338 ) -> Result<Task, A2AError> {
339 self.post_json(&format!("/tasks/{}:cancel", req.id), params, req)
340 .await
341 }
342
343 async fn subscribe_to_task(
344 &self,
345 params: &ServiceParams,
346 req: &SubscribeToTaskRequest,
347 ) -> Result<BoxStream<'static, Result<StreamResponse, A2AError>>, A2AError> {
348 let canonical_path = format!("/tasks/{}:subscribe", req.id);
349 match self.get_streaming(&canonical_path, params).await {
350 Ok(stream) => Ok(stream),
351 Err(err) if should_retry_subscribe_with_legacy_path(&err) => {
352 self.get_streaming(&format!("/tasks/{}/subscribe", req.id), params)
353 .await
354 }
355 Err(err) => Err(err),
356 }
357 }
358
359 async fn create_push_config(
360 &self,
361 params: &ServiceParams,
362 req: &TaskPushNotificationConfig,
363 ) -> Result<TaskPushNotificationConfig, A2AError> {
364 let payload = self
365 .post_value(
366 &format!("/tasks/{}/pushNotificationConfigs", req.task_id),
367 params,
368 req,
369 )
370 .await?;
371 deserialize_task_push_notification_config(payload)
372 }
373
374 async fn get_push_config(
375 &self,
376 params: &ServiceParams,
377 req: &GetTaskPushNotificationConfigRequest,
378 ) -> Result<TaskPushNotificationConfig, A2AError> {
379 let payload = self
380 .get_value(
381 &format!("/tasks/{}/pushNotificationConfigs/{}", req.task_id, req.id),
382 params,
383 &[],
384 )
385 .await?;
386 deserialize_task_push_notification_config(payload)
387 }
388
389 async fn list_push_configs(
390 &self,
391 params: &ServiceParams,
392 req: &ListTaskPushNotificationConfigsRequest,
393 ) -> Result<ListTaskPushNotificationConfigsResponse, A2AError> {
394 let mut query_parts = Vec::new();
395 if let Some(page_size) = req.page_size {
396 query_parts.push(("pageSize".to_string(), page_size.to_string()));
397 }
398 if let Some(ref page_token) = req.page_token {
399 query_parts.push(("pageToken".to_string(), page_token.clone()));
400 }
401
402 let payload = self
403 .get_value(
404 &format!("/tasks/{}/pushNotificationConfigs", req.task_id),
405 params,
406 &query_parts,
407 )
408 .await?;
409 deserialize_list_task_push_notification_configs_response(payload)
410 }
411
412 async fn delete_push_config(
413 &self,
414 params: &ServiceParams,
415 req: &DeleteTaskPushNotificationConfigRequest,
416 ) -> Result<(), A2AError> {
417 self.delete(
418 &format!("/tasks/{}/pushNotificationConfigs/{}", req.task_id, req.id),
419 params,
420 )
421 .await
422 }
423
424 async fn get_extended_agent_card(
425 &self,
426 params: &ServiceParams,
427 _req: &GetExtendedAgentCardRequest,
428 ) -> Result<AgentCard, A2AError> {
429 self.get_json(REST_EXTENDED_AGENT_CARD_PATH, params, &[])
430 .await
431 }
432
433 async fn destroy(&self) -> Result<(), A2AError> {
434 Ok(())
435 }
436}
437
438pub struct RestTransportFactory {
440 client: Client,
441}
442
443impl RestTransportFactory {
444 pub fn new(client: Option<Client>) -> Self {
445 RestTransportFactory {
446 client: client
447 .unwrap_or_else(|| crate::default_reqwest_client(None).expect("default client")),
448 }
449 }
450
451 #[cfg(any(
452 feature = "rustls-tls",
453 feature = "rustls-no-provider",
454 feature = "native-tls"
455 ))]
456 pub fn with_root_certificates_pem(pem: &[u8]) -> Result<Self, A2AError> {
457 Ok(Self {
458 client: crate::default_reqwest_client(Some(pem))?,
459 })
460 }
461}
462
463#[async_trait]
464impl TransportFactory for RestTransportFactory {
465 fn protocol(&self) -> &str {
466 TRANSPORT_PROTOCOL_HTTP_JSON
467 }
468
469 async fn create(
470 &self,
471 _card: &AgentCard,
472 iface: &AgentInterface,
473 ) -> Result<Box<dyn Transport>, A2AError> {
474 Ok(Box::new(RestTransport::new(
475 self.client.clone(),
476 iface.url.clone(),
477 )))
478 }
479}
480
481#[cfg(test)]
482mod tests {
483 use super::*;
484 use futures::StreamExt;
485 use serde_json::json;
486 use tokio::io::{AsyncReadExt, AsyncWriteExt};
487 use tokio::net::{TcpListener, TcpStream};
488
489 async fn read_http_request(socket: &mut TcpStream) -> String {
490 let mut buffer = Vec::new();
491 let mut chunk = [0_u8; 1024];
492
493 loop {
494 let read = socket.read(&mut chunk).await.unwrap();
495 if read == 0 {
496 break;
497 }
498 buffer.extend_from_slice(&chunk[..read]);
499 if buffer.windows(4).any(|window| window == b"\r\n\r\n") {
500 break;
501 }
502 }
503
504 String::from_utf8(buffer).unwrap()
505 }
506
507 async fn spawn_subscribe_fallback_server() -> String {
508 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
509 let addr = listener.local_addr().unwrap();
510
511 tokio::spawn(async move {
512 let (mut first_socket, _) = listener.accept().await.unwrap();
513 let first_request = read_http_request(&mut first_socket).await;
514 assert!(
515 first_request.starts_with("GET /tasks/task-1:subscribe HTTP/1.1"),
516 "unexpected first request: {first_request}"
517 );
518
519 let first_body = json!({
520 "error": {
521 "code": 404,
522 "status": "NOT_FOUND",
523 "message": "not found",
524 "details": []
525 }
526 })
527 .to_string();
528 let first_response = format!(
529 "HTTP/1.1 404 Not Found\r\ncontent-type: application/json\r\ncontent-length: {}\r\nconnection: close\r\n\r\n{}",
530 first_body.len(),
531 first_body,
532 );
533 first_socket
534 .write_all(first_response.as_bytes())
535 .await
536 .unwrap();
537
538 let (mut second_socket, _) = listener.accept().await.unwrap();
539 let second_request = read_http_request(&mut second_socket).await;
540 assert!(
541 second_request.starts_with("GET /tasks/task-1/subscribe HTTP/1.1"),
542 "unexpected second request: {second_request}"
543 );
544
545 let status_update = StreamResponse::StatusUpdate(TaskStatusUpdateEvent {
546 task_id: "task-1".into(),
547 context_id: "ctx-1".into(),
548 status: TaskStatus {
549 state: TaskState::Working,
550 message: None,
551 timestamp: None,
552 },
553 metadata: None,
554 });
555 let sse_payload =
556 serde_json::to_string(&protojson_conv::to_value(&status_update).unwrap()).unwrap();
557 let second_body = format!("data: {sse_payload}\n\n");
558 let second_response = format!(
559 "HTTP/1.1 200 OK\r\ncontent-type: text/event-stream\r\ncontent-length: {}\r\ncache-control: no-cache\r\nconnection: close\r\n\r\n{}",
560 second_body.len(),
561 second_body,
562 );
563 second_socket
564 .write_all(second_response.as_bytes())
565 .await
566 .unwrap();
567 });
568
569 format!("http://{addr}")
570 }
571
572 #[test]
573 fn test_rest_transport_new_strips_trailing_slash() {
574 let t = RestTransport::new(
575 crate::default_reqwest_client(None).unwrap(),
576 "http://localhost:8080/".into(),
577 );
578 assert_eq!(t.base_url, "http://localhost:8080");
579 }
580
581 #[test]
582 fn test_rest_transport_new_no_trailing_slash() {
583 let t = RestTransport::new(
584 crate::default_reqwest_client(None).unwrap(),
585 "http://localhost:8080".into(),
586 );
587 assert_eq!(t.base_url, "http://localhost:8080");
588 }
589
590 #[test]
591 fn test_rest_transport_factory_protocol() {
592 let f = RestTransportFactory::new(None);
593 assert_eq!(f.protocol(), "HTTP+JSON");
594 }
595
596 #[tokio::test]
597 async fn test_rest_transport_factory_create() {
598 let f = RestTransportFactory::new(None);
599 let card = AgentCard {
600 name: "Test".into(),
601 description: "Test".into(),
602 version: "1.0".into(),
603 supported_interfaces: vec![],
604 capabilities: AgentCapabilities::default(),
605 default_input_modes: vec!["text/plain".into()],
606 default_output_modes: vec!["text/plain".into()],
607 skills: vec![],
608 provider: None,
609 documentation_url: None,
610 icon_url: None,
611 security_schemes: None,
612 security_requirements: None,
613 signatures: None,
614 };
615 let iface = AgentInterface::new("http://localhost:8080/", "HTTP+JSON");
616 let transport = f.create(&card, &iface).await.unwrap();
617 transport.destroy().await.unwrap();
618 }
619
620 #[test]
621 fn test_build_request_adds_params() {
622 let t = RestTransport::new(
623 crate::default_reqwest_client(None).unwrap(),
624 "http://localhost:8080".into(),
625 );
626 let mut params = ServiceParams::new();
627 params.insert("X-Custom".into(), vec!["val1".into(), "val2".into()]);
628 let builder = t.build_request(reqwest::Method::GET, "/test", ¶ms);
629 let req = builder.build().unwrap();
630 let vals: Vec<_> = req
631 .headers()
632 .get_all("X-Custom")
633 .iter()
634 .map(|v| v.to_str().unwrap().to_string())
635 .collect();
636 assert_eq!(vals, vec!["val1", "val2"]);
637 }
638
639 #[test]
640 fn test_parse_rest_error_preserves_a2a_error_code() {
641 let body = json!({
642 "error": {
643 "code": 404,
644 "status": "NOT_FOUND",
645 "message": "task not found: t1",
646 "details": [
647 {
648 "@type": errordetails::ERROR_INFO_TYPE,
649 "reason": "TASK_NOT_FOUND",
650 "domain": errordetails::PROTOCOL_DOMAIN,
651 "metadata": {
652 "taskId": "t1"
653 }
654 },
655 {
656 "resource": "task"
657 }
658 ]
659 }
660 })
661 .to_string();
662
663 let err = parse_rest_error(reqwest::StatusCode::NOT_FOUND, &body);
664
665 assert_eq!(err.code, error_code::TASK_NOT_FOUND);
666 assert_eq!(err.message, "task not found: t1");
667 let details = err.details.expect("expected structured details");
668 assert_eq!(details.len(), 2);
669 assert_eq!(details[0].type_url, errordetails::ERROR_INFO_TYPE);
670 assert_eq!(
671 details[1].value.get("resource"),
672 Some(&Value::String("task".into()))
673 );
674 }
675
676 #[test]
677 fn test_parse_rest_error_accepts_go_reason_aliases() {
678 let body = json!({
679 "error": {
680 "code": 400,
681 "status": "INVALID_ARGUMENT",
682 "message": "incompatible content types",
683 "details": [
684 {
685 "@type": errordetails::ERROR_INFO_TYPE,
686 "reason": "UNSUPPORTED_CONTENT_TYPE",
687 "domain": errordetails::PROTOCOL_DOMAIN,
688 "metadata": {}
689 }
690 ]
691 }
692 })
693 .to_string();
694
695 let err = parse_rest_error(reqwest::StatusCode::BAD_REQUEST, &body);
696 assert_eq!(err.code, error_code::CONTENT_TYPE_NOT_SUPPORTED);
697 }
698
699 #[test]
700 fn test_parse_rest_error_bad_request_fallback() {
701 let body = json!({
702 "error": {
703 "code": 400,
704 "status": "INVALID_ARGUMENT",
705 "message": "invalid request parameters",
706 "details": [
707 {
708 "@type": errordetails::BAD_REQUEST_TYPE,
709 "fieldViolations": [
710 {
711 "field": "message.parts",
712 "description": "At least one part is required"
713 }
714 ]
715 }
716 ]
717 }
718 })
719 .to_string();
720
721 let err = parse_rest_error(reqwest::StatusCode::BAD_REQUEST, &body);
722 assert_eq!(err.code, error_code::INVALID_PARAMS);
723 assert!(
724 err.message
725 .contains("message.parts: At least one part is required")
726 );
727 let details = err.details.expect("expected details");
728 assert_eq!(details.len(), 1);
729 assert_eq!(details[0].type_url, errordetails::BAD_REQUEST_TYPE);
730 let violations = details[0].value.get("fieldViolations").unwrap();
731 assert_eq!(violations[0]["field"], "message.parts");
732 }
733
734 #[test]
735 fn test_parse_rest_error_bad_request_with_error_info_uses_reason() {
736 let body = json!({
737 "error": {
738 "code": 400,
739 "status": "INVALID_ARGUMENT",
740 "message": "bad params",
741 "details": [
742 {
743 "@type": errordetails::BAD_REQUEST_TYPE,
744 "fieldViolations": [
745 {"field": "task.id", "description": "required"}
746 ]
747 },
748 {
749 "@type": errordetails::ERROR_INFO_TYPE,
750 "reason": "INVALID_PARAMS",
751 "domain": errordetails::PROTOCOL_DOMAIN,
752 "metadata": {}
753 }
754 ]
755 }
756 })
757 .to_string();
758
759 let err = parse_rest_error(reqwest::StatusCode::BAD_REQUEST, &body);
760 assert_eq!(err.code, error_code::INVALID_PARAMS);
761 }
762
763 #[test]
764 fn test_should_retry_subscribe_with_legacy_path() {
765 assert!(should_retry_subscribe_with_legacy_path(&A2AError {
766 code: error_code::METHOD_NOT_FOUND,
767 message: "method missing".to_string(),
768 details: None,
769 }));
770
771 assert!(should_retry_subscribe_with_legacy_path(&A2AError {
772 code: error_code::UNSUPPORTED_OPERATION,
773 message: "unsupported".to_string(),
774 details: None,
775 }));
776
777 assert!(should_retry_subscribe_with_legacy_path(&A2AError {
778 code: error_code::INTERNAL_ERROR,
779 message: "HTTP 404: not found".to_string(),
780 details: None,
781 }));
782
783 assert!(!should_retry_subscribe_with_legacy_path(&A2AError {
784 code: error_code::TASK_NOT_FOUND,
785 message: "task not found".to_string(),
786 details: None,
787 }));
788 }
789
790 #[tokio::test]
791 async fn test_subscribe_to_task_falls_back_to_legacy_rest_path() {
792 let base_url = spawn_subscribe_fallback_server().await;
793 let transport = RestTransport::new(crate::default_reqwest_client(None).unwrap(), base_url);
794
795 let mut stream = Transport::subscribe_to_task(
796 &transport,
797 &ServiceParams::new(),
798 &SubscribeToTaskRequest {
799 id: "task-1".into(),
800 tenant: None,
801 },
802 )
803 .await
804 .unwrap();
805
806 let item = stream.next().await.unwrap().unwrap();
807 assert!(matches!(item, StreamResponse::StatusUpdate(_)));
808 }
809
810 #[cfg(any(feature = "rustls-tls", feature = "native-tls"))]
811 #[test]
812 fn test_with_root_certificates_pem_valid() {
813 let pem = crate::test_utils::rcgen_self_signed_ca_pem();
814 let f = RestTransportFactory::with_root_certificates_pem(&pem).unwrap();
815 assert_eq!(f.protocol(), TRANSPORT_PROTOCOL_HTTP_JSON);
816 }
817
818 #[cfg(any(feature = "rustls-tls", feature = "native-tls"))]
819 #[tokio::test]
820 async fn test_with_root_certificates_pem_factory_create() {
821 let pem = crate::test_utils::rcgen_self_signed_ca_pem();
822 let f = RestTransportFactory::with_root_certificates_pem(&pem).unwrap();
823 let card = AgentCard {
824 name: "Test".into(),
825 description: "Test".into(),
826 version: "1.0".into(),
827 supported_interfaces: vec![],
828 capabilities: AgentCapabilities::default(),
829 default_input_modes: vec!["text/plain".into()],
830 default_output_modes: vec!["text/plain".into()],
831 skills: vec![],
832 provider: None,
833 documentation_url: None,
834 icon_url: None,
835 security_schemes: None,
836 security_requirements: None,
837 signatures: None,
838 };
839 let iface = AgentInterface::new("https://localhost:3443/rest", "HTTP+JSON");
840 let transport = f.create(&card, &iface).await.unwrap();
841 transport.destroy().await.unwrap();
842 }
843}