use super::*;
#[tokio::test]
async fn rest_send_message() {
let (addr, _handle) = start_rest_server().await;
let client = http_client();
let body = serde_json::to_vec(&make_send_params()).unwrap();
let req = hyper::Request::builder()
.method("POST")
.uri(format!("http://{addr}/message:send"))
.header("content-type", "application/json")
.body(Full::new(Bytes::from(body)))
.unwrap();
let resp = client.request(req).await.expect("request");
assert_eq!(resp.status(), 200);
let body = resp.into_body().collect().await.unwrap().to_bytes();
let result: SendMessageResponse = serde_json::from_slice(&body).expect("parse response");
match result {
SendMessageResponse::Task(task) => {
assert_eq!(task.status.state, TaskState::Completed);
}
_ => panic!("expected Task variant"),
}
}
#[tokio::test]
async fn rest_send_streaming() {
let (addr, _handle) = start_rest_server().await;
let client = http_client();
let body = serde_json::to_vec(&make_send_params()).unwrap();
let req = hyper::Request::builder()
.method("POST")
.uri(format!("http://{addr}/message:stream"))
.header("content-type", "application/json")
.body(Full::new(Bytes::from(body)))
.unwrap();
let resp = client.request(req).await.expect("request");
assert_eq!(resp.status(), 200);
assert_eq!(
resp.headers()
.get("content-type")
.and_then(|v| v.to_str().ok()),
Some("text/event-stream")
);
}
#[tokio::test]
async fn rest_get_task_not_found() {
let (addr, _handle) = start_rest_server().await;
let client = http_client();
let req = hyper::Request::builder()
.method("GET")
.uri(format!("http://{addr}/tasks/nonexistent"))
.body(Full::new(Bytes::new()))
.unwrap();
let resp = client.request(req).await.expect("request");
assert_eq!(resp.status(), 404);
}
#[tokio::test]
async fn rest_list_tasks() {
let (addr, _handle) = start_rest_server().await;
let client = http_client();
let req = hyper::Request::builder()
.method("GET")
.uri(format!("http://{addr}/tasks"))
.body(Full::new(Bytes::new()))
.unwrap();
let resp = client.request(req).await.expect("request");
assert_eq!(resp.status(), 200);
let body = resp.into_body().collect().await.unwrap().to_bytes();
let result: serde_json::Value = serde_json::from_slice(&body).expect("parse");
let tasks = result
.get("tasks")
.expect("response should contain 'tasks' field");
assert!(tasks.is_array(), "tasks should be an array");
}
#[tokio::test]
async fn rest_cancel_nonexistent_task() {
let (addr, _handle) = start_rest_server().await;
let client = http_client();
let req = hyper::Request::builder()
.method("POST")
.uri(format!("http://{addr}/tasks/no-such-task:cancel"))
.body(Full::new(Bytes::new()))
.unwrap();
let resp = client.request(req).await.expect("request");
assert_eq!(resp.status(), 404);
}
#[tokio::test]
async fn rest_subscribe_nonexistent_task() {
let (addr, _handle) = start_rest_server().await;
let client = http_client();
let req = hyper::Request::builder()
.method("POST")
.uri(format!("http://{addr}/tasks/no-such-task:subscribe"))
.body(Full::new(Bytes::new()))
.unwrap();
let resp = client.request(req).await.expect("request");
assert_eq!(resp.status(), 404);
}
#[tokio::test]
async fn rest_wellknown_agent_card() {
let (addr, _handle) = start_rest_server().await;
let client = http_client();
let req = hyper::Request::builder()
.method("GET")
.uri(format!("http://{addr}/.well-known/agent-card.json"))
.body(Full::new(Bytes::new()))
.unwrap();
let resp = client.request(req).await.expect("request");
assert_eq!(resp.status(), 200);
assert_eq!(
resp.headers()
.get("content-type")
.and_then(|v| v.to_str().ok()),
Some("application/json")
);
let body = resp.into_body().collect().await.unwrap().to_bytes();
let card: AgentCard = serde_json::from_slice(&body).expect("parse card");
assert_eq!(card.name, "Test Agent");
}
#[tokio::test]
async fn rest_extended_agent_card() {
let (addr, _handle) = start_rest_server().await;
let client = http_client();
let req = hyper::Request::builder()
.method("GET")
.uri(format!("http://{addr}/extendedAgentCard"))
.body(Full::new(Bytes::new()))
.unwrap();
let resp = client.request(req).await.expect("request");
assert_eq!(resp.status(), 200);
let body = resp.into_body().collect().await.unwrap().to_bytes();
let card: AgentCard = serde_json::from_slice(&body).expect("parse card");
assert_eq!(card.name, "Test Agent");
}
#[tokio::test]
async fn rest_not_found_route() {
let (addr, _handle) = start_rest_server().await;
let client = http_client();
let req = hyper::Request::builder()
.method("GET")
.uri(format!("http://{addr}/nonexistent/route"))
.body(Full::new(Bytes::new()))
.unwrap();
let resp = client.request(req).await.expect("request");
assert_eq!(resp.status(), 404);
}
#[tokio::test]
async fn rest_push_config_crud() {
let (addr, _handle) = start_rest_server().await;
let client = http_client();
let config = TaskPushNotificationConfig::new("task-1", "https://example.com/hook");
let body = serde_json::to_vec(&config).unwrap();
let req = hyper::Request::builder()
.method("POST")
.uri(format!(
"http://{addr}/tasks/task-1/pushNotificationConfigs"
))
.header("content-type", "application/json")
.body(Full::new(Bytes::from(body)))
.unwrap();
let resp = client.request(req).await.expect("request");
assert_eq!(resp.status(), 200);
let body = resp.into_body().collect().await.unwrap().to_bytes();
let created: TaskPushNotificationConfig = serde_json::from_slice(&body).expect("parse config");
let config_id_val = created
.id
.as_ref()
.expect("created config should have an ID");
assert!(!config_id_val.is_empty(), "config ID should be non-empty");
let config_id = created.id.unwrap();
let req = hyper::Request::builder()
.method("GET")
.uri(format!(
"http://{addr}/tasks/task-1/pushNotificationConfigs/{config_id}"
))
.body(Full::new(Bytes::new()))
.unwrap();
let resp = client.request(req).await.expect("request");
assert_eq!(resp.status(), 200);
let req = hyper::Request::builder()
.method("GET")
.uri(format!(
"http://{addr}/tasks/task-1/pushNotificationConfigs"
))
.body(Full::new(Bytes::new()))
.unwrap();
let resp = client.request(req).await.expect("request");
assert_eq!(resp.status(), 200);
let body = resp.into_body().collect().await.unwrap().to_bytes();
let list_resp: a2a_protocol_types::responses::ListPushConfigsResponse =
serde_json::from_slice(&body).expect("parse list");
assert_eq!(list_resp.configs.len(), 1);
let req = hyper::Request::builder()
.method("DELETE")
.uri(format!(
"http://{addr}/tasks/task-1/pushNotificationConfigs/{config_id}"
))
.body(Full::new(Bytes::new()))
.unwrap();
let resp = client.request(req).await.expect("request");
assert_eq!(resp.status(), 200);
}
#[tokio::test]
async fn rest_push_config_not_supported_error_status() {
let handler = Arc::new(
RequestHandlerBuilder::new(SimpleExecutor)
.with_agent_card(minimal_agent_card())
.build()
.expect("build handler"),
);
let dispatcher = Arc::new(RestDispatcher::new(handler));
let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
.await
.expect("bind");
let addr = listener.local_addr().expect("local addr");
tokio::spawn(async move {
let (stream, _) = listener.accept().await.expect("accept");
let io = hyper_util::rt::TokioIo::new(stream);
let d = Arc::clone(&dispatcher);
let service = hyper::service::service_fn(move |req| {
let d = Arc::clone(&d);
async move { Ok::<_, std::convert::Infallible>(d.dispatch(req).await) }
});
let _ = hyper_util::server::conn::auto::Builder::new(hyper_util::rt::TokioExecutor::new())
.serve_connection(io, service)
.await;
});
let client = http_client();
let config = TaskPushNotificationConfig::new("task-1", "https://example.com/hook");
let body = serde_json::to_vec(&config).unwrap();
let req = hyper::Request::builder()
.method("POST")
.uri(format!(
"http://{addr}/tasks/task-1/pushNotificationConfigs"
))
.header("content-type", "application/json")
.body(Full::new(Bytes::from(body)))
.unwrap();
let resp = client.request(req).await.expect("request");
assert_eq!(resp.status(), 400);
}
#[tokio::test]
async fn rest_send_then_get_task() {
let (addr, _handle) = start_rest_server().await;
let client = http_client();
let body = serde_json::to_vec(&make_send_params()).unwrap();
let req = hyper::Request::builder()
.method("POST")
.uri(format!("http://{addr}/message:send"))
.header("content-type", "application/json")
.body(Full::new(Bytes::from(body)))
.unwrap();
let resp = client.request(req).await.expect("send");
assert_eq!(resp.status(), 200);
let body = resp.into_body().collect().await.unwrap().to_bytes();
let result: SendMessageResponse = serde_json::from_slice(&body).expect("parse response");
let task_id = match result {
SendMessageResponse::Task(t) => t.id.0,
_ => panic!("expected Task variant"),
};
let req = hyper::Request::builder()
.method("GET")
.uri(format!("http://{addr}/tasks/{task_id}"))
.body(Full::new(Bytes::new()))
.unwrap();
let resp = client.request(req).await.expect("get");
assert_eq!(resp.status(), 200);
let body = resp.into_body().collect().await.unwrap().to_bytes();
let fetched: Task = serde_json::from_slice(&body).expect("parse fetched task");
assert_eq!(fetched.id.0, task_id);
assert_eq!(fetched.status.state, TaskState::Completed);
}
#[tokio::test]
async fn rest_response_has_a2a_version_header() {
let (addr, _handle) = start_rest_server().await;
let client = http_client();
let body = serde_json::to_vec(&make_send_params()).unwrap();
let req = hyper::Request::builder()
.method("POST")
.uri(format!("http://{addr}/message:send"))
.header("content-type", "application/json")
.body(Full::new(Bytes::from(body)))
.unwrap();
let resp = client.request(req).await.expect("send");
assert_eq!(resp.status(), 200);
assert_eq!(
resp.headers()
.get("A2A-Version")
.and_then(|v| v.to_str().ok()),
Some("1.0.0"),
"response should have A2A-Version: 1.0.0 header"
);
assert_eq!(
resp.headers()
.get("content-type")
.and_then(|v| v.to_str().ok()),
Some("application/a2a+json"),
"response should have application/a2a+json content type"
);
}
#[tokio::test]
async fn rest_tenant_prefix_routing() {
let (addr, _handle) = start_rest_server().await;
let client = http_client();
let body = serde_json::to_vec(&make_send_params()).unwrap();
let req = hyper::Request::builder()
.method("POST")
.uri(format!("http://{addr}/tenants/acme/message:send"))
.header("content-type", "application/json")
.body(Full::new(Bytes::from(body)))
.unwrap();
let resp = client.request(req).await.expect("send via tenant prefix");
assert_eq!(resp.status(), 200, "tenant-prefixed route should succeed");
}
#[tokio::test]
async fn rest_get_subscribe_allowed() {
let (addr, _handle) = start_rest_server().await;
let client = http_client();
let body = serde_json::to_vec(&make_send_params()).unwrap();
let req = hyper::Request::builder()
.method("POST")
.uri(format!("http://{addr}/message:send"))
.header("content-type", "application/json")
.body(Full::new(Bytes::from(body)))
.unwrap();
let resp = client.request(req).await.expect("send");
let body = resp.into_body().collect().await.unwrap().to_bytes();
let result: SendMessageResponse = serde_json::from_slice(&body).expect("parse");
let task_id = match result {
SendMessageResponse::Task(t) => t.id.0,
_ => panic!("expected Task"),
};
let req = hyper::Request::builder()
.method("GET")
.uri(format!("http://{addr}/tasks/{task_id}:subscribe"))
.body(Full::new(Bytes::new()))
.unwrap();
let resp = client.request(req).await.expect("subscribe via GET");
assert_ne!(
resp.status(),
404,
"GET /tasks/:id:subscribe should be routed"
);
}