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::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;
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 Json(raw_req): Json<Value>,
124) -> impl IntoResponse {
125 let req = match protojson_conv::from_value::<SendMessageRequest>(raw_req) {
126 Ok(req) => req,
127 Err(e) => return rest_payload_error_response(e),
128 };
129 let params = ServiceParams::new();
130 match state.handler.send_message(¶ms, req).await {
131 Ok(resp) => protojson_json_response(&resp),
132 Err(e) => rest_error_response(e),
133 }
134}
135
136async fn handle_stream_message<H: RequestHandler>(
137 State(state): State<RestState<H>>,
138 Json(raw_req): Json<Value>,
139) -> impl IntoResponse {
140 let req = match protojson_conv::from_value::<SendMessageRequest>(raw_req) {
141 Ok(req) => req,
142 Err(e) => return rest_payload_error_response(e),
143 };
144 let params = ServiceParams::new();
145 match state.handler.send_streaming_message(¶ms, req).await {
146 Ok(stream) => sse::sse_from_stream(protojson_stream(stream)).into_response(),
147 Err(e) => rest_error_response(e),
148 }
149}
150
151#[derive(Deserialize)]
152pub struct GetTaskQuery {
153 #[serde(default, rename = "historyLength")]
154 pub history_length: Option<i32>,
155}
156
157async fn handle_get_task_inner<H: RequestHandler>(
158 state: RestState<H>,
159 id: String,
160 query: GetTaskQuery,
161) -> axum::response::Response {
162 let params = ServiceParams::new();
163 let req = GetTaskRequest {
164 id,
165 history_length: query.history_length,
166 tenant: None,
167 };
168 match state.handler.get_task(¶ms, req).await {
169 Ok(task) => protojson_json_response(&task),
170 Err(e) => rest_error_response(e),
171 }
172}
173
174async fn handle_get_task_or_subscribe<H: RequestHandler>(
175 State(state): State<RestState<H>>,
176 Path(id): Path<String>,
177 Query(query): Query<GetTaskQuery>,
178) -> impl IntoResponse {
179 if let Some(task_id) = id.strip_suffix(":subscribe") {
180 return handle_subscribe_to_task_inner(state, task_id.to_string()).await;
181 }
182
183 handle_get_task_inner(state, id, query).await
184}
185
186#[derive(Deserialize)]
187pub struct ListTasksQuery {
188 #[serde(default, rename = "contextId")]
189 pub context_id: Option<String>,
190 #[serde(default)]
191 pub status: Option<String>,
192 #[serde(default, rename = "pageSize")]
193 pub page_size: Option<i32>,
194 #[serde(default, rename = "pageToken")]
195 pub page_token: Option<String>,
196 #[serde(default, rename = "historyLength")]
197 pub history_length: Option<i32>,
198
199 #[serde(default, rename = "statusTimestampAfter")]
200 pub status_timestamp_after: Option<DateTime<Utc>>,
201
202 #[serde(default, rename = "includeArtifacts")]
203 pub include_artifacts: Option<bool>,
204}
205
206async fn handle_list_tasks<H: RequestHandler>(
207 State(state): State<RestState<H>>,
208 Query(query): Query<ListTasksQuery>,
209) -> impl IntoResponse {
210 let params = ServiceParams::new();
211 let status = query
212 .status
213 .and_then(|s| serde_json::from_value::<TaskState>(serde_json::Value::String(s)).ok());
214 let req = ListTasksRequest {
215 context_id: query.context_id,
216 status,
217 page_size: query.page_size,
218 page_token: query.page_token,
219 history_length: query.history_length,
220 status_timestamp_after: query.status_timestamp_after,
221 include_artifacts: query.include_artifacts,
222 tenant: None,
223 };
224 match state.handler.list_tasks(¶ms, req).await {
225 Ok(resp) => protojson_json_response(&resp),
226 Err(e) => rest_error_response(e),
227 }
228}
229
230async fn handle_cancel_task_inner<H: RequestHandler>(
231 state: RestState<H>,
232 id: String,
233) -> axum::response::Response {
234 let params = ServiceParams::new();
235 let req = CancelTaskRequest {
236 id,
237 metadata: None,
238 tenant: None,
239 };
240 match state.handler.cancel_task(¶ms, req).await {
241 Ok(task) => protojson_json_response(&task),
242 Err(e) => rest_error_response(e),
243 }
244}
245
246async fn handle_post_task_action<H: RequestHandler>(
247 State(state): State<RestState<H>>,
248 Path(id): Path<String>,
249) -> impl IntoResponse {
250 if let Some(task_id) = id.strip_suffix(":cancel") {
251 return handle_cancel_task_inner(state, task_id.to_string()).await;
252 }
253 if let Some(task_id) = id.strip_suffix(":subscribe") {
254 return handle_subscribe_to_task_inner(state, task_id.to_string()).await;
255 }
256
257 rest_error_response(A2AError::invalid_request("unsupported task action"))
258}
259
260async fn handle_cancel_task_legacy<H: RequestHandler>(
261 State(state): State<RestState<H>>,
262 Path(id): Path<String>,
263) -> impl IntoResponse {
264 handle_cancel_task_inner(state, id).await
265}
266
267async fn handle_subscribe_to_task_inner<H: RequestHandler>(
268 state: RestState<H>,
269 id: String,
270) -> axum::response::Response {
271 let params = ServiceParams::new();
272 let req = SubscribeToTaskRequest { id, tenant: None };
273 match state.handler.subscribe_to_task(¶ms, req).await {
274 Ok(stream) => sse::sse_from_stream(protojson_stream(stream)).into_response(),
275 Err(e) => rest_error_response(e),
276 }
277}
278
279async fn handle_subscribe_to_task_legacy<H: RequestHandler>(
280 State(state): State<RestState<H>>,
281 Path(id): Path<String>,
282) -> impl IntoResponse {
283 handle_subscribe_to_task_inner(state, id).await
284}
285
286#[derive(Deserialize)]
287pub struct ListPushConfigsQuery {
288 #[serde(default, rename = "pageSize")]
289 pub page_size: Option<i32>,
290
291 #[serde(default, rename = "pageToken")]
292 pub page_token: Option<String>,
293}
294
295async fn handle_create_push_config<H: RequestHandler>(
296 State(state): State<RestState<H>>,
297 Path(id): Path<String>,
298 Json(raw_config): Json<Value>,
299) -> impl IntoResponse {
300 let req = match parse_create_push_config_request(id, raw_config) {
301 Ok(req) => req,
302 Err(e) => return rest_payload_error_response(e),
303 };
304 let params = ServiceParams::new();
305 match state.handler.create_push_config(¶ms, req).await {
306 Ok(resp) => protojson_json_response(&resp),
307 Err(e) => rest_error_response(e),
308 }
309}
310
311async fn handle_get_push_config<H: RequestHandler>(
312 State(state): State<RestState<H>>,
313 Path((id, config_id)): Path<(String, String)>,
314) -> impl IntoResponse {
315 let params = ServiceParams::new();
316 let req = GetTaskPushNotificationConfigRequest {
317 task_id: id,
318 id: config_id,
319 tenant: None,
320 };
321 match state.handler.get_push_config(¶ms, req).await {
322 Ok(resp) => protojson_json_response(&resp),
323 Err(e) => rest_error_response(e),
324 }
325}
326
327async fn handle_list_push_configs<H: RequestHandler>(
328 State(state): State<RestState<H>>,
329 Path(id): Path<String>,
330 Query(query): Query<ListPushConfigsQuery>,
331) -> impl IntoResponse {
332 let params = ServiceParams::new();
333 let req = ListTaskPushNotificationConfigsRequest {
334 task_id: id,
335 page_size: query.page_size,
336 page_token: query.page_token,
337 tenant: None,
338 };
339 match state.handler.list_push_configs(¶ms, req).await {
340 Ok(resp) => protojson_json_response(&resp),
341 Err(e) => rest_error_response(e),
342 }
343}
344
345async fn handle_delete_push_config<H: RequestHandler>(
346 State(state): State<RestState<H>>,
347 Path((id, config_id)): Path<(String, String)>,
348) -> impl IntoResponse {
349 let params = ServiceParams::new();
350 let req = DeleteTaskPushNotificationConfigRequest {
351 task_id: id,
352 id: config_id,
353 tenant: None,
354 };
355 match state.handler.delete_push_config(¶ms, req).await {
356 Ok(()) => StatusCode::OK.into_response(),
357 Err(e) => rest_error_response(e),
358 }
359}
360
361async fn handle_get_extended_agent_card<H: RequestHandler>(
362 State(state): State<RestState<H>>,
363) -> impl IntoResponse {
364 let params = ServiceParams::new();
365 let req = GetExtendedAgentCardRequest { tenant: None };
366 match state.handler.get_extended_agent_card(¶ms, req).await {
367 Ok(card) => protojson_json_response(&card),
368 Err(e) => rest_error_response(e),
369 }
370}
371
372fn protojson_json_response<T: ProtoJsonPayload>(value: &T) -> axum::response::Response {
373 match protojson_conv::to_value(value) {
374 Ok(payload) => Json(payload).into_response(),
375 Err(e) => rest_error_response(A2AError::internal(format!(
376 "failed to serialize ProtoJSON payload: {e}"
377 ))),
378 }
379}
380
381fn protojson_stream(
382 stream: BoxStream<'static, Result<StreamResponse, A2AError>>,
383) -> BoxStream<'static, Result<Value, A2AError>> {
384 Box::pin(stream.map(|item| {
385 item.and_then(|value| {
386 protojson_conv::to_value(&value).map_err(|e| {
387 A2AError::internal(format!("failed to serialize ProtoJSON stream payload: {e}"))
388 })
389 })
390 }))
391}
392
393fn parse_create_push_config_request(
394 task_id: String,
395 raw_config: Value,
396) -> Result<TaskPushNotificationConfig, String> {
397 protojson_conv::from_value::<TaskPushNotificationConfig>(raw_config)
398 .map_err(|e| e.to_string())
399 .and_then(|mut config| {
400 if !config.task_id.is_empty() && config.task_id != task_id {
401 Err(format!(
402 "taskId in request body ({}) did not match task id in path ({task_id})",
403 config.task_id
404 ))
405 } else {
406 config.task_id = task_id;
407 Ok(config)
408 }
409 })
410}
411
412fn rest_payload_error_response(error: impl std::fmt::Display) -> axum::response::Response {
413 let violation = FieldViolation {
414 field: String::new(),
415 description: error.to_string(),
416 };
417 rest_error_response(
418 A2AError::invalid_params("invalid request body")
419 .with_details(vec![TypedDetail::bad_request(vec![violation])]),
420 )
421}
422
423fn rest_error_response(err: A2AError) -> axum::response::Response {
424 let status =
425 StatusCode::from_u16(err.http_status_code()).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
426 let reason = error_reason(err.code);
427 let grpc_status = rest_grpc_status(err.code);
428
429 let mut metadata = HashMap::from([("timestamp".to_string(), Utc::now().to_rfc3339())]);
430 let mut details = Vec::new();
431
432 if let Some(extra) = err.details {
433 for detail in extra {
434 if detail.type_url == errordetails::ERROR_INFO_TYPE {
435 if let Some(Value::Object(meta)) = detail.value.get("metadata") {
436 for (k, v) in meta {
437 if let Some(s) = v.as_str() {
438 metadata.entry(k.clone()).or_insert_with(|| s.to_string());
439 }
440 }
441 }
442 } else {
443 details.push(detail);
444 }
445 }
446 }
447
448 details.push(TypedDetail::error_info(
449 reason,
450 errordetails::PROTOCOL_DOMAIN,
451 Some(metadata),
452 ));
453
454 let body = RestErrorEnvelope {
455 error: RestErrorStatus {
456 code: status.as_u16(),
457 status: grpc_status,
458 message: err.message,
459 details,
460 },
461 };
462
463 (status, Json(body)).into_response()
464}
465
466fn rest_grpc_status(code: i32) -> &'static str {
467 match code {
468 error_code::TASK_NOT_FOUND => "NOT_FOUND",
469 error_code::METHOD_NOT_FOUND => "UNIMPLEMENTED",
470 error_code::TASK_NOT_CANCELABLE
471 | error_code::EXTENDED_CARD_NOT_CONFIGURED
472 | error_code::EXTENSION_SUPPORT_REQUIRED
473 | error_code::PUSH_NOTIFICATION_NOT_SUPPORTED
474 | error_code::UNSUPPORTED_OPERATION
475 | error_code::VERSION_NOT_SUPPORTED => "FAILED_PRECONDITION",
476 error_code::CONTENT_TYPE_NOT_SUPPORTED
477 | error_code::PARSE_ERROR
478 | error_code::INVALID_REQUEST
479 | error_code::INVALID_PARAMS => "INVALID_ARGUMENT",
480 _ => "INTERNAL",
481 }
482}
483
484#[cfg(test)]
485mod tests {
486 use super::*;
487 use crate::InMemoryPushConfigStore;
488 use crate::executor::ExecutorContext;
489 use crate::handler::DefaultRequestHandler;
490 use crate::task_store::InMemoryTaskStore;
491 use a2a_pb::protojson_conv;
492 use axum::body::Body;
493 use axum::http::Request;
494 use futures::stream::BoxStream;
495 use http_body_util::BodyExt;
496
497 use crate::test_util::install_crypto_provider;
498 use tower::ServiceExt;
499
500 struct EchoExecutor;
501
502 impl crate::AgentExecutor for EchoExecutor {
503 fn execute(
504 &self,
505 ctx: ExecutorContext,
506 ) -> BoxStream<'static, Result<StreamResponse, A2AError>> {
507 let task = Task {
508 id: ctx.task_id.clone(),
509 context_id: ctx.context_id.clone(),
510 status: TaskStatus {
511 state: TaskState::Completed,
512 message: ctx.message,
513 timestamp: None,
514 },
515 artifacts: None,
516 history: None,
517 metadata: None,
518 };
519 Box::pin(futures::stream::once(async move {
520 Ok(StreamResponse::Task(task))
521 }))
522 }
523
524 fn cancel(
525 &self,
526 ctx: ExecutorContext,
527 ) -> BoxStream<'static, Result<StreamResponse, A2AError>> {
528 let task = Task {
529 id: ctx.task_id.clone(),
530 context_id: ctx.context_id.clone(),
531 status: TaskStatus {
532 state: TaskState::Canceled,
533 message: None,
534 timestamp: None,
535 },
536 artifacts: None,
537 history: None,
538 metadata: None,
539 };
540 Box::pin(futures::stream::once(async move {
541 Ok(StreamResponse::Task(task))
542 }))
543 }
544 }
545
546 fn make_app() -> axum::Router {
547 let handler = Arc::new(DefaultRequestHandler::new(
548 EchoExecutor,
549 InMemoryTaskStore::new(),
550 ));
551 rest_router(handler)
552 }
553
554 fn make_push_app() -> axum::Router {
555 install_crypto_provider();
556 let handler = Arc::new(
557 DefaultRequestHandler::new(EchoExecutor, InMemoryTaskStore::new())
558 .with_push_config_store(InMemoryPushConfigStore::new()),
559 );
560 rest_router(handler)
561 }
562
563 #[tokio::test]
564 async fn test_send_message() {
565 let app = make_app();
566 let body = serde_json::json!({
567 "message": {
568 "messageId": "m1",
569 "role": "ROLE_USER",
570 "parts": [{"text": "hello"}]
571 }
572 });
573 let req = Request::builder()
574 .uri(REST_SEND_MESSAGE_PATH)
575 .method("POST")
576 .header("content-type", "application/json")
577 .body(Body::from(serde_json::to_string(&body).unwrap()))
578 .unwrap();
579 let resp = app.oneshot(req).await.unwrap();
580 assert_eq!(resp.status(), StatusCode::OK);
581 }
582
583 #[tokio::test]
584 async fn test_send_message_legacy_alias() {
585 let app = make_app();
586 let body = serde_json::json!({
587 "message": {
588 "messageId": "m1",
589 "role": "ROLE_USER",
590 "parts": [{"text": "hello"}]
591 }
592 });
593 let req = Request::builder()
594 .uri(REST_SEND_MESSAGE_LEGACY_PATH)
595 .method("POST")
596 .header("content-type", "application/json")
597 .body(Body::from(serde_json::to_string(&body).unwrap()))
598 .unwrap();
599 let resp = app.oneshot(req).await.unwrap();
600 assert_eq!(resp.status(), StatusCode::OK);
601 }
602
603 #[tokio::test]
604 async fn test_get_task_not_found() {
605 let app = make_app();
606 let req = Request::builder()
607 .uri("/tasks/nonexistent")
608 .method("GET")
609 .body(Body::empty())
610 .unwrap();
611 let resp = app.oneshot(req).await.unwrap();
612 assert_eq!(resp.status(), StatusCode::NOT_FOUND);
613 }
614
615 #[tokio::test]
616 async fn test_list_tasks_empty() {
617 let app = make_app();
618 let req = Request::builder()
619 .uri("/tasks")
620 .method("GET")
621 .body(Body::empty())
622 .unwrap();
623 let resp = app.oneshot(req).await.unwrap();
624 assert_eq!(resp.status(), StatusCode::OK);
625 let body = resp.into_body().collect().await.unwrap().to_bytes();
626 let list_resp =
627 protojson_conv::from_str::<ListTasksResponse>(std::str::from_utf8(&body).unwrap())
628 .unwrap();
629 assert!(list_resp.tasks.is_empty());
630 }
631
632 #[tokio::test]
633 async fn test_cancel_task_not_found() {
634 let app = make_app();
635 let req = Request::builder()
636 .uri("/tasks/nonexistent:cancel")
637 .method("POST")
638 .header("content-type", "application/json")
639 .body(Body::empty())
640 .unwrap();
641 let resp = app.oneshot(req).await.unwrap();
642 assert_eq!(resp.status(), StatusCode::NOT_FOUND);
643 }
644
645 #[tokio::test]
646 async fn test_create_push_config_not_supported() {
647 let app = make_app();
648 let body = serde_json::json!({
649 "url": "http://example.com/callback"
650 });
651 let req = Request::builder()
652 .uri("/tasks/t1/pushNotificationConfigs")
653 .method("POST")
654 .header("content-type", "application/json")
655 .body(Body::from(serde_json::to_string(&body).unwrap()))
656 .unwrap();
657 let resp = app.oneshot(req).await.unwrap();
658 assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
659 }
660
661 #[tokio::test]
662 async fn test_create_push_config_accepts_raw_config_body() {
663 let app = make_push_app();
664 let body = serde_json::json!({
665 "id": "cfg1",
666 "url": "http://example.com/callback"
667 });
668 let req = Request::builder()
669 .uri("/tasks/t1/pushNotificationConfigs")
670 .method("POST")
671 .header("content-type", "application/json")
672 .body(Body::from(serde_json::to_string(&body).unwrap()))
673 .unwrap();
674 let resp = app.oneshot(req).await.unwrap();
675 assert_eq!(resp.status(), StatusCode::OK);
676 let body = resp.into_body().collect().await.unwrap().to_bytes();
677 let payload = serde_json::from_slice::<TaskPushNotificationConfig>(&body).unwrap();
678 assert_eq!(payload.task_id, "t1");
679 assert_eq!(payload.id.as_deref(), Some("cfg1"));
680 assert_eq!(payload.url, "http://example.com/callback");
681 }
682
683 #[tokio::test]
684 async fn test_create_push_config_accepts_flat_request_body() {
685 let app = make_push_app();
686 let body = serde_json::json!({
687 "taskId": "t1",
688 "id": "cfg1",
689 "url": "http://example.com/callback"
690 });
691 let req = Request::builder()
692 .uri("/tasks/t1/pushNotificationConfigs")
693 .method("POST")
694 .header("content-type", "application/json")
695 .body(Body::from(serde_json::to_string(&body).unwrap()))
696 .unwrap();
697 let resp = app.oneshot(req).await.unwrap();
698 assert_eq!(resp.status(), StatusCode::OK);
699 let body = resp.into_body().collect().await.unwrap().to_bytes();
700 let payload = serde_json::from_slice::<TaskPushNotificationConfig>(&body).unwrap();
701 assert_eq!(payload.task_id, "t1");
702 assert_eq!(payload.id.as_deref(), Some("cfg1"));
703 assert_eq!(payload.url, "http://example.com/callback");
704 }
705
706 #[tokio::test]
707 async fn test_create_push_config_rejects_mismatched_task_id_in_body() {
708 let app = make_push_app();
709 let body = serde_json::json!({
710 "taskId": "other-task",
711 "url": "http://example.com/callback"
712 });
713 let req = Request::builder()
714 .uri("/tasks/t1/pushNotificationConfigs")
715 .method("POST")
716 .header("content-type", "application/json")
717 .body(Body::from(serde_json::to_string(&body).unwrap()))
718 .unwrap();
719 let resp = app.oneshot(req).await.unwrap();
720 assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
721 }
722
723 #[tokio::test]
724 async fn test_get_extended_agent_card() {
725 let app = make_app();
726 let req = Request::builder()
727 .uri(REST_EXTENDED_AGENT_CARD_PATH)
728 .method("GET")
729 .body(Body::empty())
730 .unwrap();
731 let resp = app.oneshot(req).await.unwrap();
732 assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
733 }
734
735 #[tokio::test]
736 async fn test_rest_error_response_status_codes() {
737 let resp = rest_error_response(A2AError::task_not_found("t1"));
738 assert_eq!(resp.status(), StatusCode::NOT_FOUND);
739
740 let body = resp.into_body().collect().await.unwrap().to_bytes();
741 let payload: serde_json::Value = serde_json::from_slice(&body).unwrap();
742 assert_eq!(payload["error"]["code"], 404);
743 assert_eq!(payload["error"]["status"], "NOT_FOUND");
744 assert_eq!(payload["error"]["details"][0]["reason"], "TASK_NOT_FOUND");
745
746 let resp = rest_error_response(A2AError::internal("boom"));
747 assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
748
749 let resp = rest_error_response(A2AError::content_type_not_supported());
750 assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
751 }
752
753 #[tokio::test]
754 async fn test_rest_error_response_merges_error_info_metadata() {
755 let existing_info = TypedDetail::error_info(
756 "TASK_NOT_FOUND",
757 errordetails::PROTOCOL_DOMAIN,
758 Some(HashMap::from([("taskId".to_string(), "t99".to_string())])),
759 );
760 let err = A2AError::task_not_found("t99").with_details(vec![existing_info]);
761 let resp = rest_error_response(err);
762 assert_eq!(resp.status(), StatusCode::NOT_FOUND);
763
764 let body = resp.into_body().collect().await.unwrap().to_bytes();
765 let payload: serde_json::Value = serde_json::from_slice(&body).unwrap();
766 let details = payload["error"]["details"].as_array().unwrap();
767 assert_eq!(details.len(), 1);
769 assert_eq!(details[0]["@type"], errordetails::ERROR_INFO_TYPE);
770 assert_eq!(details[0]["reason"], "TASK_NOT_FOUND");
771 assert_eq!(details[0]["metadata"]["taskId"], "t99");
773 assert!(details[0]["metadata"]["timestamp"].is_string());
774 }
775
776 #[tokio::test]
777 async fn test_rest_error_response_preserves_non_error_info_details() {
778 let bad_req = TypedDetail::bad_request(vec![FieldViolation {
779 field: "message.parts".into(),
780 description: "required".into(),
781 }]);
782 let err = A2AError::invalid_params("bad request").with_details(vec![bad_req]);
783 let resp = rest_error_response(err);
784 assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
785
786 let body = resp.into_body().collect().await.unwrap().to_bytes();
787 let payload: serde_json::Value = serde_json::from_slice(&body).unwrap();
788 let details = payload["error"]["details"].as_array().unwrap();
789 assert_eq!(details.len(), 2);
791 assert_eq!(details[0]["@type"], errordetails::BAD_REQUEST_TYPE);
792 assert_eq!(details[0]["fieldViolations"][0]["field"], "message.parts");
793 assert_eq!(details[1]["@type"], errordetails::ERROR_INFO_TYPE);
794 assert_eq!(details[1]["reason"], "INVALID_PARAMS");
795 }
796
797 #[tokio::test]
798 async fn test_rest_payload_error_response() {
799 let resp = rest_payload_error_response("missing field 'parts'");
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 assert_eq!(
805 payload["error"]["message"].as_str().unwrap(),
806 "invalid request body"
807 );
808 let details = payload["error"]["details"].as_array().unwrap();
809 assert_eq!(details.len(), 2);
811 assert_eq!(details[0]["@type"], errordetails::BAD_REQUEST_TYPE);
812 assert_eq!(
813 details[0]["fieldViolations"][0]["description"],
814 "missing field 'parts'"
815 );
816 assert_eq!(details[1]["@type"], errordetails::ERROR_INFO_TYPE);
817 assert_eq!(details[1]["reason"], "INVALID_PARAMS");
818 }
819
820 #[tokio::test]
821 async fn test_subscribe_to_task_not_found() {
822 let app = make_app();
823 let req = Request::builder()
824 .uri("/tasks/nonexistent:subscribe")
825 .method("GET")
826 .body(Body::empty())
827 .unwrap();
828 let resp = app.oneshot(req).await.unwrap();
829 assert!(resp.status().is_client_error());
830 }
831
832 #[tokio::test]
833 async fn test_stream_message() {
834 let app = make_app();
835 let body = serde_json::json!({
836 "message": {
837 "messageId": "m1",
838 "role": "ROLE_USER",
839 "parts": [{"text": "hello"}]
840 }
841 });
842 let req = Request::builder()
843 .uri(REST_STREAM_MESSAGE_PATH)
844 .method("POST")
845 .header("content-type", "application/json")
846 .header("accept", "text/event-stream")
847 .body(Body::from(serde_json::to_string(&body).unwrap()))
848 .unwrap();
849 let resp = app.oneshot(req).await.unwrap();
850 assert_eq!(resp.status(), StatusCode::OK);
851 }
852
853 #[tokio::test]
854 async fn test_get_push_config_not_found() {
855 let app = make_app();
856 let req = Request::builder()
857 .uri("/tasks/t1/pushNotificationConfigs/cfg1")
858 .method("GET")
859 .body(Body::empty())
860 .unwrap();
861 let resp = app.oneshot(req).await.unwrap();
862 assert!(resp.status().is_client_error());
864 }
865
866 #[tokio::test]
867 async fn test_list_push_configs() {
868 let app = make_push_app();
869 let req = Request::builder()
870 .uri("/tasks/t1/pushNotificationConfigs")
871 .method("GET")
872 .body(Body::empty())
873 .unwrap();
874 let resp = app.oneshot(req).await.unwrap();
875 assert_eq!(resp.status(), StatusCode::OK);
876 let body = resp.into_body().collect().await.unwrap().to_bytes();
877 let payload =
878 serde_json::from_slice::<ListTaskPushNotificationConfigsResponse>(&body).unwrap();
879 assert!(payload.configs.is_empty());
880 }
881
882 #[tokio::test]
883 async fn test_delete_push_config() {
884 let app = make_app();
885 let req = Request::builder()
886 .uri("/tasks/t1/pushNotificationConfigs/cfg1")
887 .method("DELETE")
888 .body(Body::empty())
889 .unwrap();
890 let resp = app.oneshot(req).await.unwrap();
891 assert!(resp.status().is_client_error());
892 }
893
894 #[tokio::test]
895 async fn test_delete_push_config_with_store_returns_ok() {
896 let app = make_push_app();
897 let create_body = serde_json::json!({
898 "id": "cfg1",
899 "url": "http://example.com/callback"
900 });
901 let create_req = Request::builder()
902 .uri("/tasks/t1/pushNotificationConfigs")
903 .method("POST")
904 .header("content-type", "application/json")
905 .body(Body::from(serde_json::to_string(&create_body).unwrap()))
906 .unwrap();
907 let create_resp = app.clone().oneshot(create_req).await.unwrap();
908 assert_eq!(create_resp.status(), StatusCode::OK);
909
910 let delete_req = Request::builder()
911 .uri("/tasks/t1/pushNotificationConfigs/cfg1")
912 .method("DELETE")
913 .body(Body::empty())
914 .unwrap();
915 let delete_resp = app.clone().oneshot(delete_req).await.unwrap();
916 assert_eq!(delete_resp.status(), StatusCode::OK);
917
918 let list_req = Request::builder()
919 .uri("/tasks/t1/pushNotificationConfigs")
920 .method("GET")
921 .body(Body::empty())
922 .unwrap();
923 let list_resp = app.oneshot(list_req).await.unwrap();
924 assert_eq!(list_resp.status(), StatusCode::OK);
925 let body = list_resp.into_body().collect().await.unwrap().to_bytes();
926 let payload =
927 serde_json::from_slice::<ListTaskPushNotificationConfigsResponse>(&body).unwrap();
928 assert!(payload.configs.is_empty());
929 }
930
931 #[tokio::test]
932 async fn test_list_tasks_with_query_params() {
933 let app = make_app();
934 let req = Request::builder()
935 .uri("/tasks?contextId=c1&pageSize=5&pageToken=tok&historyLength=3&includeArtifacts=true&statusTimestampAfter=2025-01-01T00:00:00Z")
936 .method("GET")
937 .body(Body::empty())
938 .unwrap();
939 let resp = app.oneshot(req).await.unwrap();
940 assert_eq!(resp.status(), StatusCode::OK);
941 }
942
943 #[tokio::test]
944 async fn test_get_task_with_history_length() {
945 let app = make_app();
947 let body = serde_json::json!({
949 "message": {
950 "messageId": "m1",
951 "role": "ROLE_USER",
952 "parts": [{"text": "hello"}]
953 }
954 });
955 let req = Request::builder()
956 .uri(REST_SEND_MESSAGE_PATH)
957 .method("POST")
958 .header("content-type", "application/json")
959 .body(Body::from(serde_json::to_string(&body).unwrap()))
960 .unwrap();
961 let resp = app.oneshot(req).await.unwrap();
962 assert_eq!(resp.status(), StatusCode::OK);
963 let resp_body = resp.into_body().collect().await.unwrap().to_bytes();
964 let send_resp: serde_json::Value = serde_json::from_slice(&resp_body).unwrap();
965 let task_id = send_resp["task"]["id"].as_str().unwrap();
966
967 let app = make_app();
968 let req = Request::builder()
969 .uri(format!("/tasks/{}?historyLength=5", task_id))
970 .method("GET")
971 .body(Body::empty())
972 .unwrap();
973 let resp = app.oneshot(req).await.unwrap();
974 assert!(resp.status() == StatusCode::OK || resp.status() == StatusCode::NOT_FOUND);
976 }
977}