Skip to main content

a2a_server/
rest.rs

1// Copyright AGNTCY Contributors (https://github.com/agntcy)
2// SPDX-License-Identifier: Apache-2.0
3use 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
59/// Shared state for REST handlers.
60pub 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
72/// Create an axum router for the REST/HTTP+JSON protocol binding.
73pub 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(&params, 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(&params, 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(&params, 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(&params, 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(&params, 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(&params, 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(&params, 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(&params, 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(&params, 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(&params, 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(&params, 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        // Push not supported → bad request
841        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        // First create a task, then get it with historyLength
922        let app = make_app();
923        // Create task via send_message
924        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        // Each make_app() creates a new store, so this will be not found
951        assert!(resp.status() == StatusCode::OK || resp.status() == StatusCode::NOT_FOUND);
952    }
953}