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