use a2a_protocol_client::ClientBuilder;
use a2a_protocol_client::config::{BINDING_HTTP_JSON, BINDING_JSONRPC};
use a2a_protocol_types::params::TaskQueryParams;
use bytes::Bytes;
use http_body_util::{BodyExt, Full};
async fn serve(status: u16, respond: fn(&serde_json::Value) -> String) -> String {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
.await
.expect("bind");
let addr = listener.local_addr().expect("addr");
tokio::spawn(async move {
while let Ok((stream, _)) = listener.accept().await {
tokio::spawn(async move {
let svc = hyper::service::service_fn(
move |req: hyper::Request<hyper::body::Incoming>| async move {
let body = req.into_body().collect().await.expect("body").to_bytes();
let id = serde_json::from_slice::<serde_json::Value>(&body)
.ok()
.and_then(|v| v.get("id").cloned())
.unwrap_or(serde_json::Value::Null);
Ok::<_, std::convert::Infallible>(
hyper::Response::builder()
.status(status)
.header("content-type", "application/json")
.body(Full::new(Bytes::from(respond(&id))))
.expect("response"),
)
},
);
let _ = hyper::server::conn::http1::Builder::new()
.serve_connection(hyper_util::rt::TokioIo::new(stream), svc)
.await;
});
}
});
format!("http://{addr}")
}
fn client(url: &str, binding: &str) -> a2a_protocol_client::A2aClient {
ClientBuilder::new(url)
.with_protocol_binding(binding)
.build()
.expect("client")
}
fn get_task_params() -> TaskQueryParams {
TaskQueryParams {
tenant: None,
id: "t-1".into(),
history_length: None,
}
}
fn no_result(id: &serde_json::Value) -> String {
format!(r#"{{"jsonrpc":"2.0","id":{id}}}"#)
}
#[tokio::test]
async fn jsonrpc_delete_with_no_result_member_succeeds() {
let url = serve(200, no_result).await;
client(&url, BINDING_JSONRPC)
.delete_push_config("t-1", "cfg-1")
.await
.expect("a2a-go's delete response is a success");
}
#[tokio::test]
async fn jsonrpc_delete_with_null_result_succeeds() {
let url = serve(200, |id| {
format!(r#"{{"jsonrpc":"2.0","id":{id},"result":null}}"#)
})
.await;
client(&url, BINDING_JSONRPC)
.delete_push_config("t-1", "cfg-1")
.await
.expect("a null result is a success for an Empty method");
}
#[tokio::test]
async fn jsonrpc_delete_error_is_still_an_error() {
let url = serve(200, |id| {
format!(
r#"{{"jsonrpc":"2.0","id":{id},"error":{{"code":-32001,"message":"Task not found"}}}}"#
)
})
.await;
let err = client(&url, BINDING_JSONRPC)
.delete_push_config("t-1", "cfg-1")
.await
.expect_err("an error member is an error");
assert!(err.to_string().contains("Task not found"), "{err}");
}
#[tokio::test]
async fn jsonrpc_delete_with_a_foreign_id_is_rejected() {
let url = serve(200, |_| {
r#"{"jsonrpc":"2.0","id":"someone-else"}"#.to_owned()
})
.await;
let err = client(&url, BINDING_JSONRPC)
.delete_push_config("t-1", "cfg-1")
.await
.expect_err("a response to another request is not this one's success");
assert!(err.to_string().contains("id"), "{err}");
}
#[tokio::test]
async fn jsonrpc_get_task_with_no_result_is_still_an_error() {
let url = serve(200, no_result).await;
client(&url, BINDING_JSONRPC)
.get_task(get_task_params())
.await
.expect_err("GetTask must carry a Task");
}
#[tokio::test]
async fn rest_delete_with_an_empty_200_succeeds() {
let url = serve(200, |_| String::new()).await;
client(&url, BINDING_HTTP_JSON)
.delete_push_config("t-1", "cfg-1")
.await
.expect("a2a-go's empty 200 is a success");
}
#[tokio::test]
async fn rest_delete_with_an_empty_204_succeeds() {
let url = serve(204, |_| String::new()).await;
client(&url, BINDING_HTTP_JSON)
.delete_push_config("t-1", "cfg-1")
.await
.expect("204 No Content is a success");
}
#[tokio::test]
async fn rest_delete_with_an_empty_object_succeeds() {
let url = serve(200, |_| "{}".to_owned()).await;
client(&url, BINDING_HTTP_JSON)
.delete_push_config("t-1", "cfg-1")
.await
.expect("{} is Empty");
}
#[tokio::test]
async fn rest_get_task_with_an_empty_200_is_still_an_error() {
let url = serve(200, |_| String::new()).await;
client(&url, BINDING_HTTP_JSON)
.get_task(get_task_params())
.await
.expect_err("GetTask must carry a Task");
}