1use 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
47pub 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
60pub 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(¶ms, 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(¶ms, 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(¶ms, 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(¶ms, 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(¶ms, 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(¶ms, 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(¶ms, 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(¶ms, 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(¶ms, 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(¶ms, 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(¶ms, 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
407fn 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 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 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 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 let resp = rest_error_response(A2AError::task_not_found("t1"));
866 assert_eq!(resp.status(), StatusCode::NOT_FOUND);
867 }
868
869 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 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 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 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 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 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 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 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 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 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 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 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 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 let app = make_app();
1427 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 assert!(resp.status() == StatusCode::OK || resp.status() == StatusCode::NOT_FOUND);
1456 }
1457
1458 #[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}