1use std::sync::Arc;
4
5use a2a::*;
6use a2a_pb::protojson_conv::{self, ProtoJsonPayload};
7use axum::{
8 Json,
9 extract::{Path, Query, State},
10 http::{HeaderMap, StatusCode},
11 response::IntoResponse,
12};
13use chrono::{DateTime, Utc};
14use futures::{StreamExt, stream::BoxStream};
15use serde::{Deserialize, Serialize};
16use serde_json::Value;
17use std::collections::HashMap;
18
19use crate::handler::RequestHandler;
20use crate::middleware::{ServiceParams, extract_service_params};
21use crate::sse;
22
23const REST_SEND_MESSAGE_PATH: &str = "/message:send";
24const REST_SEND_MESSAGE_LEGACY_PATH: &str = "/message/send";
25const REST_STREAM_MESSAGE_PATH: &str = "/message:stream";
26const REST_STREAM_MESSAGE_LEGACY_PATH: &str = "/message/stream";
27const REST_EXTENDED_AGENT_CARD_PATH: &str = "/extendedAgentCard";
28const REST_EXTENDED_AGENT_CARD_LEGACY_PATH: &str = "/agent-card/extended";
29const REST_PUSH_CONFIGS_PATH: &str = "/tasks/{id}/pushNotificationConfigs";
30const REST_PUSH_CONFIGS_LEGACY_PATH: &str = "/tasks/{id}/push-configs";
31const REST_PUSH_CONFIG_PATH: &str = "/tasks/{id}/pushNotificationConfigs/{config_id}";
32const REST_PUSH_CONFIG_LEGACY_PATH: &str = "/tasks/{id}/push-configs/{config_id}";
33#[derive(Serialize)]
34struct RestErrorEnvelope {
35 error: RestErrorStatus,
36}
37
38#[derive(Serialize)]
39struct RestErrorStatus {
40 code: u16,
41 status: &'static str,
42 message: String,
43 details: Vec<TypedDetail>,
44}
45
46pub struct RestState<H: RequestHandler> {
48 pub handler: Arc<H>,
49}
50
51impl<H: RequestHandler> Clone for RestState<H> {
52 fn clone(&self) -> Self {
53 RestState {
54 handler: self.handler.clone(),
55 }
56 }
57}
58
59pub fn rest_router<H: RequestHandler>(handler: Arc<H>) -> axum::Router {
61 let state = RestState { handler };
62 axum::Router::new()
63 .route(
64 REST_SEND_MESSAGE_PATH,
65 axum::routing::post(handle_send_message::<H>),
66 )
67 .route(
68 REST_SEND_MESSAGE_LEGACY_PATH,
69 axum::routing::post(handle_send_message::<H>),
70 )
71 .route(
72 REST_STREAM_MESSAGE_PATH,
73 axum::routing::post(handle_stream_message::<H>),
74 )
75 .route(
76 REST_STREAM_MESSAGE_LEGACY_PATH,
77 axum::routing::post(handle_stream_message::<H>),
78 )
79 .route(
80 "/tasks/{id}",
81 axum::routing::get(handle_get_task_or_subscribe::<H>)
82 .post(handle_post_task_action::<H>),
83 )
84 .route("/tasks", axum::routing::get(handle_list_tasks::<H>))
85 .route(
86 "/tasks/{id}/cancel",
87 axum::routing::post(handle_cancel_task_legacy::<H>),
88 )
89 .route(
90 "/tasks/{id}/subscribe",
91 axum::routing::get(handle_subscribe_to_task_legacy::<H>)
92 .post(handle_subscribe_to_task_legacy::<H>),
93 )
94 .route(
95 REST_PUSH_CONFIGS_PATH,
96 axum::routing::post(handle_create_push_config::<H>).get(handle_list_push_configs::<H>),
97 )
98 .route(
99 REST_PUSH_CONFIGS_LEGACY_PATH,
100 axum::routing::post(handle_create_push_config::<H>).get(handle_list_push_configs::<H>),
101 )
102 .route(
103 REST_PUSH_CONFIG_PATH,
104 axum::routing::get(handle_get_push_config::<H>).delete(handle_delete_push_config::<H>),
105 )
106 .route(
107 REST_PUSH_CONFIG_LEGACY_PATH,
108 axum::routing::get(handle_get_push_config::<H>).delete(handle_delete_push_config::<H>),
109 )
110 .route(
111 REST_EXTENDED_AGENT_CARD_PATH,
112 axum::routing::get(handle_get_extended_agent_card::<H>),
113 )
114 .route(
115 REST_EXTENDED_AGENT_CARD_LEGACY_PATH,
116 axum::routing::get(handle_get_extended_agent_card::<H>),
117 )
118 .with_state(state)
119}
120
121async fn handle_send_message<H: RequestHandler>(
122 State(state): State<RestState<H>>,
123 headers: HeaderMap,
124 Json(raw_req): Json<Value>,
125) -> impl IntoResponse {
126 let req = match protojson_conv::from_value::<SendMessageRequest>(raw_req) {
127 Ok(req) => req,
128 Err(e) => return rest_payload_error_response(e),
129 };
130 let params = extract_service_params(&headers);
131 match state.handler.send_message(¶ms, req).await {
132 Ok(resp) => protojson_json_response(&resp),
133 Err(e) => rest_error_response(e),
134 }
135}
136
137async fn handle_stream_message<H: RequestHandler>(
138 State(state): State<RestState<H>>,
139 headers: HeaderMap,
140 Json(raw_req): Json<Value>,
141) -> impl IntoResponse {
142 let req = match protojson_conv::from_value::<SendMessageRequest>(raw_req) {
143 Ok(req) => req,
144 Err(e) => return rest_payload_error_response(e),
145 };
146 let params = extract_service_params(&headers);
147 match state.handler.send_streaming_message(¶ms, req).await {
148 Ok(stream) => sse::sse_from_stream(protojson_stream(stream)).into_response(),
149 Err(e) => rest_error_response(e),
150 }
151}
152
153#[derive(Deserialize)]
154pub struct GetTaskQuery {
155 #[serde(default, rename = "historyLength")]
156 pub history_length: Option<i32>,
157}
158
159async fn handle_get_task_inner<H: RequestHandler>(
160 state: RestState<H>,
161 params: ServiceParams,
162 id: String,
163 query: GetTaskQuery,
164) -> axum::response::Response {
165 let req = GetTaskRequest {
166 id,
167 history_length: query.history_length,
168 tenant: None,
169 };
170 match state.handler.get_task(¶ms, req).await {
171 Ok(task) => protojson_json_response(&task),
172 Err(e) => rest_error_response(e),
173 }
174}
175
176async fn handle_get_task_or_subscribe<H: RequestHandler>(
177 State(state): State<RestState<H>>,
178 Path(id): Path<String>,
179 Query(query): Query<GetTaskQuery>,
180 headers: HeaderMap,
181) -> impl IntoResponse {
182 let params = extract_service_params(&headers);
183 if let Some(task_id) = id.strip_suffix(":subscribe") {
184 return handle_subscribe_to_task_inner(state, params, task_id.to_string()).await;
185 }
186
187 handle_get_task_inner(state, params, id, query).await
188}
189
190#[derive(Deserialize)]
191pub struct ListTasksQuery {
192 #[serde(default, rename = "contextId")]
193 pub context_id: Option<String>,
194 #[serde(default)]
195 pub status: Option<String>,
196 #[serde(default, rename = "pageSize")]
197 pub page_size: Option<i32>,
198 #[serde(default, rename = "pageToken")]
199 pub page_token: Option<String>,
200 #[serde(default, rename = "historyLength")]
201 pub history_length: Option<i32>,
202
203 #[serde(default, rename = "statusTimestampAfter")]
204 pub status_timestamp_after: Option<DateTime<Utc>>,
205
206 #[serde(default, rename = "includeArtifacts")]
207 pub include_artifacts: Option<bool>,
208}
209
210async fn handle_list_tasks<H: RequestHandler>(
211 State(state): State<RestState<H>>,
212 Query(query): Query<ListTasksQuery>,
213 headers: HeaderMap,
214) -> impl IntoResponse {
215 let params = extract_service_params(&headers);
216 let status = query
217 .status
218 .and_then(|s| serde_json::from_value::<TaskState>(serde_json::Value::String(s)).ok());
219 let req = ListTasksRequest {
220 context_id: query.context_id,
221 status,
222 page_size: query.page_size,
223 page_token: query.page_token,
224 history_length: query.history_length,
225 status_timestamp_after: query.status_timestamp_after,
226 include_artifacts: query.include_artifacts,
227 tenant: None,
228 };
229 match state.handler.list_tasks(¶ms, req).await {
230 Ok(resp) => protojson_json_response(&resp),
231 Err(e) => rest_error_response(e),
232 }
233}
234
235async fn handle_cancel_task_inner<H: RequestHandler>(
236 state: RestState<H>,
237 params: ServiceParams,
238 id: String,
239) -> axum::response::Response {
240 let req = CancelTaskRequest {
241 id,
242 metadata: None,
243 tenant: None,
244 };
245 match state.handler.cancel_task(¶ms, req).await {
246 Ok(task) => protojson_json_response(&task),
247 Err(e) => rest_error_response(e),
248 }
249}
250
251async fn handle_post_task_action<H: RequestHandler>(
252 State(state): State<RestState<H>>,
253 Path(id): Path<String>,
254 headers: HeaderMap,
255) -> impl IntoResponse {
256 let params = extract_service_params(&headers);
257 if let Some(task_id) = id.strip_suffix(":cancel") {
258 return handle_cancel_task_inner(state, params, task_id.to_string()).await;
259 }
260 if let Some(task_id) = id.strip_suffix(":subscribe") {
261 return handle_subscribe_to_task_inner(state, params, task_id.to_string()).await;
262 }
263
264 rest_error_response(A2AError::invalid_request("unsupported task action"))
265}
266
267async fn handle_cancel_task_legacy<H: RequestHandler>(
268 State(state): State<RestState<H>>,
269 Path(id): Path<String>,
270 headers: HeaderMap,
271) -> impl IntoResponse {
272 let params = extract_service_params(&headers);
273 handle_cancel_task_inner(state, params, id).await
274}
275
276async fn handle_subscribe_to_task_inner<H: RequestHandler>(
277 state: RestState<H>,
278 params: ServiceParams,
279 id: String,
280) -> axum::response::Response {
281 let req = SubscribeToTaskRequest { id, tenant: None };
282 match state.handler.subscribe_to_task(¶ms, req).await {
283 Ok(stream) => sse::sse_from_stream(protojson_stream(stream)).into_response(),
284 Err(e) => rest_error_response(e),
285 }
286}
287
288async fn handle_subscribe_to_task_legacy<H: RequestHandler>(
289 State(state): State<RestState<H>>,
290 Path(id): Path<String>,
291 headers: HeaderMap,
292) -> impl IntoResponse {
293 let params = extract_service_params(&headers);
294 handle_subscribe_to_task_inner(state, params, id).await
295}
296
297#[derive(Deserialize)]
298pub struct ListPushConfigsQuery {
299 #[serde(default, rename = "pageSize")]
300 pub page_size: Option<i32>,
301
302 #[serde(default, rename = "pageToken")]
303 pub page_token: Option<String>,
304}
305
306async fn handle_create_push_config<H: RequestHandler>(
307 State(state): State<RestState<H>>,
308 Path(id): Path<String>,
309 headers: HeaderMap,
310 Json(raw_config): Json<Value>,
311) -> impl IntoResponse {
312 let req = match parse_create_push_config_request(id, raw_config) {
313 Ok(req) => req,
314 Err(e) => return rest_payload_error_response(e),
315 };
316 let params = extract_service_params(&headers);
317 match state.handler.create_push_config(¶ms, req).await {
318 Ok(resp) => protojson_json_response(&resp),
319 Err(e) => rest_error_response(e),
320 }
321}
322
323async fn handle_get_push_config<H: RequestHandler>(
324 State(state): State<RestState<H>>,
325 Path((id, config_id)): Path<(String, String)>,
326 headers: HeaderMap,
327) -> impl IntoResponse {
328 let params = extract_service_params(&headers);
329 let req = GetTaskPushNotificationConfigRequest {
330 task_id: id,
331 id: config_id,
332 tenant: None,
333 };
334 match state.handler.get_push_config(¶ms, req).await {
335 Ok(resp) => protojson_json_response(&resp),
336 Err(e) => rest_error_response(e),
337 }
338}
339
340async fn handle_list_push_configs<H: RequestHandler>(
341 State(state): State<RestState<H>>,
342 Path(id): Path<String>,
343 Query(query): Query<ListPushConfigsQuery>,
344 headers: HeaderMap,
345) -> impl IntoResponse {
346 let params = extract_service_params(&headers);
347 let req = ListTaskPushNotificationConfigsRequest {
348 task_id: id,
349 page_size: query.page_size,
350 page_token: query.page_token,
351 tenant: None,
352 };
353 match state.handler.list_push_configs(¶ms, req).await {
354 Ok(resp) => protojson_json_response(&resp),
355 Err(e) => rest_error_response(e),
356 }
357}
358
359async fn handle_delete_push_config<H: RequestHandler>(
360 State(state): State<RestState<H>>,
361 Path((id, config_id)): Path<(String, String)>,
362 headers: HeaderMap,
363) -> impl IntoResponse {
364 let params = extract_service_params(&headers);
365 let req = DeleteTaskPushNotificationConfigRequest {
366 task_id: id,
367 id: config_id,
368 tenant: None,
369 };
370 match state.handler.delete_push_config(¶ms, req).await {
371 Ok(()) => StatusCode::OK.into_response(),
372 Err(e) => rest_error_response(e),
373 }
374}
375
376async fn handle_get_extended_agent_card<H: RequestHandler>(
377 State(state): State<RestState<H>>,
378 headers: HeaderMap,
379) -> impl IntoResponse {
380 let params = extract_service_params(&headers);
381 let req = GetExtendedAgentCardRequest { tenant: None };
382 match state.handler.get_extended_agent_card(¶ms, req).await {
383 Ok(card) => protojson_json_response(&card),
384 Err(e) => rest_error_response(e),
385 }
386}
387
388fn protojson_json_response<T: ProtoJsonPayload>(value: &T) -> axum::response::Response {
389 match protojson_conv::to_value(value) {
390 Ok(payload) => Json(payload).into_response(),
391 Err(e) => rest_error_response(A2AError::internal(format!(
392 "failed to serialize ProtoJSON payload: {e}"
393 ))),
394 }
395}
396
397fn protojson_stream(
398 stream: BoxStream<'static, Result<StreamResponse, A2AError>>,
399) -> BoxStream<'static, Result<Value, A2AError>> {
400 Box::pin(stream.map(|item| {
401 item.and_then(|value| {
402 protojson_conv::to_value(&value).map_err(|e| {
403 A2AError::internal(format!("failed to serialize ProtoJSON stream payload: {e}"))
404 })
405 })
406 }))
407}
408
409fn parse_create_push_config_request(
410 task_id: String,
411 raw_config: Value,
412) -> Result<TaskPushNotificationConfig, String> {
413 protojson_conv::from_value::<TaskPushNotificationConfig>(raw_config)
414 .map_err(|e| e.to_string())
415 .and_then(|mut config| {
416 if !config.task_id.is_empty() && config.task_id != task_id {
417 Err(format!(
418 "taskId in request body ({}) did not match task id in path ({task_id})",
419 config.task_id
420 ))
421 } else {
422 config.task_id = task_id;
423 Ok(config)
424 }
425 })
426}
427
428fn rest_payload_error_response(error: impl std::fmt::Display) -> axum::response::Response {
429 let violation = FieldViolation {
430 field: String::new(),
431 description: error.to_string(),
432 };
433 rest_error_response(
434 A2AError::invalid_params("invalid request body")
435 .with_details(vec![TypedDetail::bad_request(vec![violation])]),
436 )
437}
438
439fn rest_error_response(err: A2AError) -> axum::response::Response {
440 let status =
441 StatusCode::from_u16(err.http_status_code()).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
442 let reason = error_reason(err.code);
443 let grpc_status = rest_grpc_status(err.code);
444
445 let mut metadata = HashMap::from([("timestamp".to_string(), Utc::now().to_rfc3339())]);
446 let mut details = Vec::new();
447
448 if let Some(extra) = err.details {
449 for detail in extra {
450 if detail.type_url == errordetails::ERROR_INFO_TYPE {
451 if let Some(Value::Object(meta)) = detail.value.get("metadata") {
452 for (k, v) in meta {
453 if let Some(s) = v.as_str() {
454 metadata.entry(k.clone()).or_insert_with(|| s.to_string());
455 }
456 }
457 }
458 } else {
459 details.push(detail);
460 }
461 }
462 }
463
464 details.push(TypedDetail::error_info(
465 reason,
466 errordetails::PROTOCOL_DOMAIN,
467 Some(metadata),
468 ));
469
470 let body = RestErrorEnvelope {
471 error: RestErrorStatus {
472 code: status.as_u16(),
473 status: grpc_status,
474 message: err.message,
475 details,
476 },
477 };
478
479 (status, Json(body)).into_response()
480}
481
482fn rest_grpc_status(code: i32) -> &'static str {
483 match code {
484 error_code::TASK_NOT_FOUND => "NOT_FOUND",
485 error_code::METHOD_NOT_FOUND => "UNIMPLEMENTED",
486 error_code::TASK_NOT_CANCELABLE
487 | error_code::EXTENDED_CARD_NOT_CONFIGURED
488 | error_code::EXTENSION_SUPPORT_REQUIRED
489 | error_code::PUSH_NOTIFICATION_NOT_SUPPORTED
490 | error_code::UNSUPPORTED_OPERATION
491 | error_code::VERSION_NOT_SUPPORTED => "FAILED_PRECONDITION",
492 error_code::CONTENT_TYPE_NOT_SUPPORTED
493 | error_code::PARSE_ERROR
494 | error_code::INVALID_REQUEST
495 | error_code::INVALID_PARAMS => "INVALID_ARGUMENT",
496 _ => "INTERNAL",
497 }
498}
499
500#[cfg(test)]
501mod tests {
502 use super::*;
503 use crate::InMemoryPushConfigStore;
504 use crate::executor::ExecutorContext;
505 use crate::handler::DefaultRequestHandler;
506 use crate::task_store::InMemoryTaskStore;
507 use a2a_pb::protojson_conv;
508 use axum::body::Body;
509 use axum::http::Request;
510 use futures::stream::BoxStream;
511 use http_body_util::BodyExt;
512
513 use crate::test_util::{CapturingHandler, assert_header_captured, install_crypto_provider};
514 use tower::ServiceExt;
515
516 struct EchoExecutor;
517
518 impl crate::AgentExecutor for EchoExecutor {
519 fn execute(
520 &self,
521 ctx: ExecutorContext,
522 ) -> BoxStream<'static, Result<StreamResponse, A2AError>> {
523 let task = Task {
524 id: ctx.task_id.clone(),
525 context_id: ctx.context_id.clone(),
526 status: TaskStatus {
527 state: TaskState::Completed,
528 message: ctx.message,
529 timestamp: None,
530 },
531 artifacts: None,
532 history: None,
533 metadata: None,
534 };
535 Box::pin(futures::stream::once(async move {
536 Ok(StreamResponse::Task(task))
537 }))
538 }
539
540 fn cancel(
541 &self,
542 ctx: ExecutorContext,
543 ) -> BoxStream<'static, Result<StreamResponse, A2AError>> {
544 let task = Task {
545 id: ctx.task_id.clone(),
546 context_id: ctx.context_id.clone(),
547 status: TaskStatus {
548 state: TaskState::Canceled,
549 message: None,
550 timestamp: None,
551 },
552 artifacts: None,
553 history: None,
554 metadata: None,
555 };
556 Box::pin(futures::stream::once(async move {
557 Ok(StreamResponse::Task(task))
558 }))
559 }
560 }
561
562 fn make_app() -> axum::Router {
563 let handler = Arc::new(DefaultRequestHandler::new(
564 EchoExecutor,
565 InMemoryTaskStore::new(),
566 ));
567 rest_router(handler)
568 }
569
570 fn make_push_app() -> axum::Router {
571 install_crypto_provider();
572 let handler = Arc::new(
573 DefaultRequestHandler::new(EchoExecutor, InMemoryTaskStore::new())
574 .with_push_config_store(InMemoryPushConfigStore::new()),
575 );
576 rest_router(handler)
577 }
578
579 #[tokio::test]
580 async fn test_send_message() {
581 let app = make_app();
582 let body = serde_json::json!({
583 "message": {
584 "messageId": "m1",
585 "role": "ROLE_USER",
586 "parts": [{"text": "hello"}]
587 }
588 });
589 let req = Request::builder()
590 .uri(REST_SEND_MESSAGE_PATH)
591 .method("POST")
592 .header("content-type", "application/json")
593 .body(Body::from(serde_json::to_string(&body).unwrap()))
594 .unwrap();
595 let resp = app.oneshot(req).await.unwrap();
596 assert_eq!(resp.status(), StatusCode::OK);
597 }
598
599 #[tokio::test]
600 async fn test_send_message_legacy_alias() {
601 let app = make_app();
602 let body = serde_json::json!({
603 "message": {
604 "messageId": "m1",
605 "role": "ROLE_USER",
606 "parts": [{"text": "hello"}]
607 }
608 });
609 let req = Request::builder()
610 .uri(REST_SEND_MESSAGE_LEGACY_PATH)
611 .method("POST")
612 .header("content-type", "application/json")
613 .body(Body::from(serde_json::to_string(&body).unwrap()))
614 .unwrap();
615 let resp = app.oneshot(req).await.unwrap();
616 assert_eq!(resp.status(), StatusCode::OK);
617 }
618
619 #[tokio::test]
620 async fn test_get_task_not_found() {
621 let app = make_app();
622 let req = Request::builder()
623 .uri("/tasks/nonexistent")
624 .method("GET")
625 .body(Body::empty())
626 .unwrap();
627 let resp = app.oneshot(req).await.unwrap();
628 assert_eq!(resp.status(), StatusCode::NOT_FOUND);
629 }
630
631 #[tokio::test]
632 async fn test_list_tasks_empty() {
633 let app = make_app();
634 let req = Request::builder()
635 .uri("/tasks")
636 .method("GET")
637 .body(Body::empty())
638 .unwrap();
639 let resp = app.oneshot(req).await.unwrap();
640 assert_eq!(resp.status(), StatusCode::OK);
641 let body = resp.into_body().collect().await.unwrap().to_bytes();
642 let list_resp =
643 protojson_conv::from_str::<ListTasksResponse>(std::str::from_utf8(&body).unwrap())
644 .unwrap();
645 assert!(list_resp.tasks.is_empty());
646 }
647
648 #[tokio::test]
649 async fn test_cancel_task_not_found() {
650 let app = make_app();
651 let req = Request::builder()
652 .uri("/tasks/nonexistent:cancel")
653 .method("POST")
654 .header("content-type", "application/json")
655 .body(Body::empty())
656 .unwrap();
657 let resp = app.oneshot(req).await.unwrap();
658 assert_eq!(resp.status(), StatusCode::NOT_FOUND);
659 }
660
661 #[tokio::test]
662 async fn test_create_push_config_not_supported() {
663 let app = make_app();
664 let body = serde_json::json!({
665 "url": "http://example.com/callback"
666 });
667 let req = Request::builder()
668 .uri("/tasks/t1/pushNotificationConfigs")
669 .method("POST")
670 .header("content-type", "application/json")
671 .body(Body::from(serde_json::to_string(&body).unwrap()))
672 .unwrap();
673 let resp = app.oneshot(req).await.unwrap();
674 assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
675 }
676
677 #[tokio::test]
678 async fn test_create_push_config_accepts_raw_config_body() {
679 let app = make_push_app();
680 let body = serde_json::json!({
681 "id": "cfg1",
682 "url": "http://example.com/callback"
683 });
684 let req = Request::builder()
685 .uri("/tasks/t1/pushNotificationConfigs")
686 .method("POST")
687 .header("content-type", "application/json")
688 .body(Body::from(serde_json::to_string(&body).unwrap()))
689 .unwrap();
690 let resp = app.oneshot(req).await.unwrap();
691 assert_eq!(resp.status(), StatusCode::OK);
692 let body = resp.into_body().collect().await.unwrap().to_bytes();
693 let payload = serde_json::from_slice::<TaskPushNotificationConfig>(&body).unwrap();
694 assert_eq!(payload.task_id, "t1");
695 assert_eq!(payload.id.as_deref(), Some("cfg1"));
696 assert_eq!(payload.url, "http://example.com/callback");
697 }
698
699 #[tokio::test]
700 async fn test_create_push_config_accepts_flat_request_body() {
701 let app = make_push_app();
702 let body = serde_json::json!({
703 "taskId": "t1",
704 "id": "cfg1",
705 "url": "http://example.com/callback"
706 });
707 let req = Request::builder()
708 .uri("/tasks/t1/pushNotificationConfigs")
709 .method("POST")
710 .header("content-type", "application/json")
711 .body(Body::from(serde_json::to_string(&body).unwrap()))
712 .unwrap();
713 let resp = app.oneshot(req).await.unwrap();
714 assert_eq!(resp.status(), StatusCode::OK);
715 let body = resp.into_body().collect().await.unwrap().to_bytes();
716 let payload = serde_json::from_slice::<TaskPushNotificationConfig>(&body).unwrap();
717 assert_eq!(payload.task_id, "t1");
718 assert_eq!(payload.id.as_deref(), Some("cfg1"));
719 assert_eq!(payload.url, "http://example.com/callback");
720 }
721
722 #[tokio::test]
723 async fn test_create_push_config_rejects_mismatched_task_id_in_body() {
724 let app = make_push_app();
725 let body = serde_json::json!({
726 "taskId": "other-task",
727 "url": "http://example.com/callback"
728 });
729 let req = Request::builder()
730 .uri("/tasks/t1/pushNotificationConfigs")
731 .method("POST")
732 .header("content-type", "application/json")
733 .body(Body::from(serde_json::to_string(&body).unwrap()))
734 .unwrap();
735 let resp = app.oneshot(req).await.unwrap();
736 assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
737 }
738
739 #[tokio::test]
740 async fn test_get_extended_agent_card() {
741 let app = make_app();
742 let req = Request::builder()
743 .uri(REST_EXTENDED_AGENT_CARD_PATH)
744 .method("GET")
745 .body(Body::empty())
746 .unwrap();
747 let resp = app.oneshot(req).await.unwrap();
748 assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
749 }
750
751 #[tokio::test]
752 async fn test_rest_error_response_status_codes() {
753 let resp = rest_error_response(A2AError::task_not_found("t1"));
754 assert_eq!(resp.status(), StatusCode::NOT_FOUND);
755
756 let body = resp.into_body().collect().await.unwrap().to_bytes();
757 let payload: serde_json::Value = serde_json::from_slice(&body).unwrap();
758 assert_eq!(payload["error"]["code"], 404);
759 assert_eq!(payload["error"]["status"], "NOT_FOUND");
760 assert_eq!(payload["error"]["details"][0]["reason"], "TASK_NOT_FOUND");
761
762 let resp = rest_error_response(A2AError::internal("boom"));
763 assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
764
765 let resp = rest_error_response(A2AError::content_type_not_supported());
766 assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
767 }
768
769 #[tokio::test]
770 async fn test_rest_error_response_merges_error_info_metadata() {
771 let existing_info = TypedDetail::error_info(
772 "TASK_NOT_FOUND",
773 errordetails::PROTOCOL_DOMAIN,
774 Some(HashMap::from([("taskId".to_string(), "t99".to_string())])),
775 );
776 let err = A2AError::task_not_found("t99").with_details(vec![existing_info]);
777 let resp = rest_error_response(err);
778 assert_eq!(resp.status(), StatusCode::NOT_FOUND);
779
780 let body = resp.into_body().collect().await.unwrap().to_bytes();
781 let payload: serde_json::Value = serde_json::from_slice(&body).unwrap();
782 let details = payload["error"]["details"].as_array().unwrap();
783 assert_eq!(details.len(), 1);
785 assert_eq!(details[0]["@type"], errordetails::ERROR_INFO_TYPE);
786 assert_eq!(details[0]["reason"], "TASK_NOT_FOUND");
787 assert_eq!(details[0]["metadata"]["taskId"], "t99");
789 assert!(details[0]["metadata"]["timestamp"].is_string());
790 }
791
792 #[tokio::test]
793 async fn test_rest_error_response_preserves_non_error_info_details() {
794 let bad_req = TypedDetail::bad_request(vec![FieldViolation {
795 field: "message.parts".into(),
796 description: "required".into(),
797 }]);
798 let err = A2AError::invalid_params("bad request").with_details(vec![bad_req]);
799 let resp = rest_error_response(err);
800 assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
801
802 let body = resp.into_body().collect().await.unwrap().to_bytes();
803 let payload: serde_json::Value = serde_json::from_slice(&body).unwrap();
804 let details = payload["error"]["details"].as_array().unwrap();
805 assert_eq!(details.len(), 2);
807 assert_eq!(details[0]["@type"], errordetails::BAD_REQUEST_TYPE);
808 assert_eq!(details[0]["fieldViolations"][0]["field"], "message.parts");
809 assert_eq!(details[1]["@type"], errordetails::ERROR_INFO_TYPE);
810 assert_eq!(details[1]["reason"], "INVALID_PARAMS");
811 }
812
813 #[tokio::test]
814 async fn test_rest_payload_error_response() {
815 let resp = rest_payload_error_response("missing field 'parts'");
816 assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
817
818 let body = resp.into_body().collect().await.unwrap().to_bytes();
819 let payload: serde_json::Value = serde_json::from_slice(&body).unwrap();
820 assert_eq!(
821 payload["error"]["message"].as_str().unwrap(),
822 "invalid request body"
823 );
824 let details = payload["error"]["details"].as_array().unwrap();
825 assert_eq!(details.len(), 2);
827 assert_eq!(details[0]["@type"], errordetails::BAD_REQUEST_TYPE);
828 assert_eq!(
829 details[0]["fieldViolations"][0]["description"],
830 "missing field 'parts'"
831 );
832 assert_eq!(details[1]["@type"], errordetails::ERROR_INFO_TYPE);
833 assert_eq!(details[1]["reason"], "INVALID_PARAMS");
834 }
835
836 #[tokio::test]
837 async fn test_subscribe_to_task_not_found() {
838 let app = make_app();
839 let req = Request::builder()
840 .uri("/tasks/nonexistent:subscribe")
841 .method("GET")
842 .body(Body::empty())
843 .unwrap();
844 let resp = app.oneshot(req).await.unwrap();
845 assert!(resp.status().is_client_error());
846 }
847
848 #[tokio::test]
849 async fn test_stream_message() {
850 let app = make_app();
851 let body = serde_json::json!({
852 "message": {
853 "messageId": "m1",
854 "role": "ROLE_USER",
855 "parts": [{"text": "hello"}]
856 }
857 });
858 let req = Request::builder()
859 .uri(REST_STREAM_MESSAGE_PATH)
860 .method("POST")
861 .header("content-type", "application/json")
862 .header("accept", "text/event-stream")
863 .body(Body::from(serde_json::to_string(&body).unwrap()))
864 .unwrap();
865 let resp = app.oneshot(req).await.unwrap();
866 assert_eq!(resp.status(), StatusCode::OK);
867 }
868
869 #[tokio::test]
870 async fn test_get_push_config_not_found() {
871 let app = make_app();
872 let req = Request::builder()
873 .uri("/tasks/t1/pushNotificationConfigs/cfg1")
874 .method("GET")
875 .body(Body::empty())
876 .unwrap();
877 let resp = app.oneshot(req).await.unwrap();
878 assert!(resp.status().is_client_error());
880 }
881
882 #[tokio::test]
883 async fn test_list_push_configs() {
884 let app = make_push_app();
885 let req = Request::builder()
886 .uri("/tasks/t1/pushNotificationConfigs")
887 .method("GET")
888 .body(Body::empty())
889 .unwrap();
890 let resp = app.oneshot(req).await.unwrap();
891 assert_eq!(resp.status(), StatusCode::OK);
892 let body = resp.into_body().collect().await.unwrap().to_bytes();
893 let payload =
894 serde_json::from_slice::<ListTaskPushNotificationConfigsResponse>(&body).unwrap();
895 assert!(payload.configs.is_empty());
896 }
897
898 #[tokio::test]
899 async fn test_delete_push_config() {
900 let app = make_app();
901 let req = Request::builder()
902 .uri("/tasks/t1/pushNotificationConfigs/cfg1")
903 .method("DELETE")
904 .body(Body::empty())
905 .unwrap();
906 let resp = app.oneshot(req).await.unwrap();
907 assert!(resp.status().is_client_error());
908 }
909
910 #[tokio::test]
911 async fn test_delete_push_config_with_store_returns_ok() {
912 let app = make_push_app();
913 let create_body = serde_json::json!({
914 "id": "cfg1",
915 "url": "http://example.com/callback"
916 });
917 let create_req = Request::builder()
918 .uri("/tasks/t1/pushNotificationConfigs")
919 .method("POST")
920 .header("content-type", "application/json")
921 .body(Body::from(serde_json::to_string(&create_body).unwrap()))
922 .unwrap();
923 let create_resp = app.clone().oneshot(create_req).await.unwrap();
924 assert_eq!(create_resp.status(), StatusCode::OK);
925
926 let delete_req = Request::builder()
927 .uri("/tasks/t1/pushNotificationConfigs/cfg1")
928 .method("DELETE")
929 .body(Body::empty())
930 .unwrap();
931 let delete_resp = app.clone().oneshot(delete_req).await.unwrap();
932 assert_eq!(delete_resp.status(), StatusCode::OK);
933
934 let list_req = Request::builder()
935 .uri("/tasks/t1/pushNotificationConfigs")
936 .method("GET")
937 .body(Body::empty())
938 .unwrap();
939 let list_resp = app.oneshot(list_req).await.unwrap();
940 assert_eq!(list_resp.status(), StatusCode::OK);
941 let body = list_resp.into_body().collect().await.unwrap().to_bytes();
942 let payload =
943 serde_json::from_slice::<ListTaskPushNotificationConfigsResponse>(&body).unwrap();
944 assert!(payload.configs.is_empty());
945 }
946
947 #[tokio::test]
948 async fn test_list_tasks_with_query_params() {
949 let app = make_app();
950 let req = Request::builder()
951 .uri("/tasks?contextId=c1&pageSize=5&pageToken=tok&historyLength=3&includeArtifacts=true&statusTimestampAfter=2025-01-01T00:00:00Z")
952 .method("GET")
953 .body(Body::empty())
954 .unwrap();
955 let resp = app.oneshot(req).await.unwrap();
956 assert_eq!(resp.status(), StatusCode::OK);
957 }
958
959 #[tokio::test]
960 async fn test_send_message_propagates_headers_as_service_params() {
961 let (handler, captured) = CapturingHandler::new();
962 let app = rest_router(handler);
963
964 let body = serde_json::json!({
965 "message": {
966 "messageId": "m1",
967 "role": "ROLE_USER",
968 "parts": [{"text": "hi"}]
969 }
970 });
971 let req = Request::builder()
972 .uri(REST_SEND_MESSAGE_PATH)
973 .method("POST")
974 .header("content-type", "application/json")
975 .header("authorization", "Bearer test-token-abc")
976 .header("x-tenant-id", "acme")
977 .body(Body::from(serde_json::to_string(&body).unwrap()))
978 .unwrap();
979 let _ = app.oneshot(req).await.unwrap();
980
981 assert_header_captured(
982 &captured,
983 "send_message",
984 "authorization",
985 "Bearer test-token-abc",
986 );
987 assert_header_captured(&captured, "send_message", "x-tenant-id", "acme");
988 }
989
990 #[tokio::test]
991 async fn test_stream_message_propagates_headers_as_service_params() {
992 let (handler, captured) = CapturingHandler::new();
993 let app = rest_router(handler);
994
995 let body = serde_json::json!({
996 "message": {
997 "messageId": "m1",
998 "role": "ROLE_USER",
999 "parts": [{"text": "hi"}]
1000 }
1001 });
1002 let req = Request::builder()
1003 .uri(REST_STREAM_MESSAGE_PATH)
1004 .method("POST")
1005 .header("content-type", "application/json")
1006 .header("accept", "text/event-stream")
1007 .header("authorization", "Bearer streaming-token")
1008 .body(Body::from(serde_json::to_string(&body).unwrap()))
1009 .unwrap();
1010 let resp = app.oneshot(req).await.unwrap();
1011 let _ = resp.into_body().collect().await.unwrap().to_bytes();
1013
1014 assert_header_captured(
1015 &captured,
1016 "send_streaming_message",
1017 "authorization",
1018 "Bearer streaming-token",
1019 );
1020 }
1021
1022 #[tokio::test]
1023 async fn test_get_task_propagates_headers_as_service_params() {
1024 let (handler, captured) = CapturingHandler::new();
1025 let app = rest_router(handler);
1026 let req = Request::builder()
1027 .uri("/tasks/some-id")
1028 .method("GET")
1029 .header("authorization", "Bearer get-task-token")
1030 .header("x-tenant-id", "acme")
1031 .body(Body::empty())
1032 .unwrap();
1033 let _ = app.oneshot(req).await.unwrap();
1034 assert_header_captured(
1035 &captured,
1036 "get_task",
1037 "authorization",
1038 "Bearer get-task-token",
1039 );
1040 assert_header_captured(&captured, "get_task", "x-tenant-id", "acme");
1041 }
1042
1043 #[tokio::test]
1044 async fn test_list_tasks_propagates_headers_as_service_params() {
1045 let (handler, captured) = CapturingHandler::new();
1046 let app = rest_router(handler);
1047 let req = Request::builder()
1048 .uri("/tasks")
1049 .method("GET")
1050 .header("authorization", "Bearer list-tasks-token")
1051 .body(Body::empty())
1052 .unwrap();
1053 let _ = app.oneshot(req).await.unwrap();
1054 assert_header_captured(
1055 &captured,
1056 "list_tasks",
1057 "authorization",
1058 "Bearer list-tasks-token",
1059 );
1060 }
1061
1062 #[tokio::test]
1063 async fn test_cancel_task_propagates_headers_as_service_params() {
1064 let (handler, captured) = CapturingHandler::new();
1065 let app = rest_router(handler);
1066 let req = Request::builder()
1067 .uri("/tasks/some-id:cancel")
1068 .method("POST")
1069 .header("authorization", "Bearer cancel-token")
1070 .body(Body::empty())
1071 .unwrap();
1072 let _ = app.oneshot(req).await.unwrap();
1073 assert_header_captured(
1074 &captured,
1075 "cancel_task",
1076 "authorization",
1077 "Bearer cancel-token",
1078 );
1079 }
1080
1081 #[tokio::test]
1082 async fn test_cancel_task_legacy_propagates_headers_as_service_params() {
1083 let (handler, captured) = CapturingHandler::new();
1085 let app = rest_router(handler);
1086 let req = Request::builder()
1087 .uri("/tasks/some-id/cancel")
1088 .method("POST")
1089 .header("authorization", "Bearer cancel-legacy-token")
1090 .body(Body::empty())
1091 .unwrap();
1092 let _ = app.oneshot(req).await.unwrap();
1093 assert_header_captured(
1094 &captured,
1095 "cancel_task",
1096 "authorization",
1097 "Bearer cancel-legacy-token",
1098 );
1099 }
1100
1101 #[tokio::test]
1102 async fn test_create_push_config_propagates_headers_as_service_params() {
1103 let (handler, captured) = CapturingHandler::new();
1104 let app = rest_router(handler);
1105 let body = serde_json::json!({
1106 "id": "cfg1",
1107 "url": "http://example.com/callback"
1108 });
1109 let req = Request::builder()
1110 .uri("/tasks/t1/pushNotificationConfigs")
1111 .method("POST")
1112 .header("content-type", "application/json")
1113 .header("authorization", "Bearer push-create-token")
1114 .body(Body::from(serde_json::to_string(&body).unwrap()))
1115 .unwrap();
1116 let _ = app.oneshot(req).await.unwrap();
1117 assert_header_captured(
1118 &captured,
1119 "create_push_config",
1120 "authorization",
1121 "Bearer push-create-token",
1122 );
1123 }
1124
1125 #[tokio::test]
1126 async fn test_list_push_configs_propagates_headers_as_service_params() {
1127 let (handler, captured) = CapturingHandler::new();
1128 let app = rest_router(handler);
1129 let req = Request::builder()
1130 .uri("/tasks/t1/pushNotificationConfigs")
1131 .method("GET")
1132 .header("authorization", "Bearer push-list-token")
1133 .body(Body::empty())
1134 .unwrap();
1135 let _ = app.oneshot(req).await.unwrap();
1136 assert_header_captured(
1137 &captured,
1138 "list_push_configs",
1139 "authorization",
1140 "Bearer push-list-token",
1141 );
1142 }
1143
1144 #[tokio::test]
1145 async fn test_get_push_config_propagates_headers_as_service_params() {
1146 let (handler, captured) = CapturingHandler::new();
1147 let app = rest_router(handler);
1148 let req = Request::builder()
1149 .uri("/tasks/t1/pushNotificationConfigs/cfg1")
1150 .method("GET")
1151 .header("authorization", "Bearer push-get-token")
1152 .body(Body::empty())
1153 .unwrap();
1154 let _ = app.oneshot(req).await.unwrap();
1155 assert_header_captured(
1156 &captured,
1157 "get_push_config",
1158 "authorization",
1159 "Bearer push-get-token",
1160 );
1161 }
1162
1163 #[tokio::test]
1164 async fn test_delete_push_config_propagates_headers_as_service_params() {
1165 let (handler, captured) = CapturingHandler::new();
1166 let app = rest_router(handler);
1167 let req = Request::builder()
1168 .uri("/tasks/t1/pushNotificationConfigs/cfg1")
1169 .method("DELETE")
1170 .header("authorization", "Bearer push-delete-token")
1171 .body(Body::empty())
1172 .unwrap();
1173 let _ = app.oneshot(req).await.unwrap();
1174 assert_header_captured(
1175 &captured,
1176 "delete_push_config",
1177 "authorization",
1178 "Bearer push-delete-token",
1179 );
1180 }
1181
1182 #[tokio::test]
1183 async fn test_get_extended_agent_card_propagates_headers_as_service_params() {
1184 let (handler, captured) = CapturingHandler::new();
1185 let app = rest_router(handler);
1186 let req = Request::builder()
1187 .uri(REST_EXTENDED_AGENT_CARD_PATH)
1188 .method("GET")
1189 .header("authorization", "Bearer card-token")
1190 .body(Body::empty())
1191 .unwrap();
1192 let _ = app.oneshot(req).await.unwrap();
1193 assert_header_captured(
1194 &captured,
1195 "get_extended_agent_card",
1196 "authorization",
1197 "Bearer card-token",
1198 );
1199 }
1200
1201 #[tokio::test]
1202 async fn test_subscribe_to_task_propagates_headers_as_service_params() {
1203 let (handler, captured) = CapturingHandler::new();
1204 let app = rest_router(handler);
1205 let req = Request::builder()
1206 .uri("/tasks/some-id:subscribe")
1207 .method("GET")
1208 .header("authorization", "Bearer subscribe-token")
1209 .body(Body::empty())
1210 .unwrap();
1211 let resp = app.oneshot(req).await.unwrap();
1212 let _ = resp.into_body().collect().await.unwrap().to_bytes();
1213 assert_header_captured(
1214 &captured,
1215 "subscribe_to_task",
1216 "authorization",
1217 "Bearer subscribe-token",
1218 );
1219 }
1220
1221 #[tokio::test]
1222 async fn test_subscribe_to_task_legacy_propagates_headers_as_service_params() {
1223 let (handler, captured) = CapturingHandler::new();
1225 let app = rest_router(handler);
1226 let req = Request::builder()
1227 .uri("/tasks/some-id/subscribe")
1228 .method("GET")
1229 .header("authorization", "Bearer subscribe-legacy-token")
1230 .body(Body::empty())
1231 .unwrap();
1232 let resp = app.oneshot(req).await.unwrap();
1233 let _ = resp.into_body().collect().await.unwrap().to_bytes();
1234 assert_header_captured(
1235 &captured,
1236 "subscribe_to_task",
1237 "authorization",
1238 "Bearer subscribe-legacy-token",
1239 );
1240 }
1241
1242 #[tokio::test]
1243 async fn test_get_task_with_history_length() {
1244 let app = make_app();
1246 let body = serde_json::json!({
1248 "message": {
1249 "messageId": "m1",
1250 "role": "ROLE_USER",
1251 "parts": [{"text": "hello"}]
1252 }
1253 });
1254 let req = Request::builder()
1255 .uri(REST_SEND_MESSAGE_PATH)
1256 .method("POST")
1257 .header("content-type", "application/json")
1258 .body(Body::from(serde_json::to_string(&body).unwrap()))
1259 .unwrap();
1260 let resp = app.oneshot(req).await.unwrap();
1261 assert_eq!(resp.status(), StatusCode::OK);
1262 let resp_body = resp.into_body().collect().await.unwrap().to_bytes();
1263 let send_resp: serde_json::Value = serde_json::from_slice(&resp_body).unwrap();
1264 let task_id = send_resp["task"]["id"].as_str().unwrap();
1265
1266 let app = make_app();
1267 let req = Request::builder()
1268 .uri(format!("/tasks/{}?historyLength=5", task_id))
1269 .method("GET")
1270 .body(Body::empty())
1271 .unwrap();
1272 let resp = app.oneshot(req).await.unwrap();
1273 assert!(resp.status() == StatusCode::OK || resp.status() == StatusCode::NOT_FOUND);
1275 }
1276}