Skip to main content

a2a_server/
rest.rs

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