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