use dtmrs_core::{GlobalStatus, SagaStep, TransType};
use dtmrs_server::api::Api;
use dtmrs_server::http::{router, App};
use dtmrs_server::tcc_rows;
use dtmrs_store::Store;
async fn store() -> Store {
Store::open("sqlite::memory:").await.unwrap()
}
async fn spawn_http(api: Api) -> String {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
tokio::spawn(async move {
axum::serve(listener, router(App::new(api))).await
});
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
format!("http://{addr}")
}
async fn post(base: &str, path: &str, body: &str) -> (u16, String) {
let r = reqwest::Client::new()
.post(format!("{base}{path}"))
.header("content-type", "application/json")
.body(body.to_string())
.send()
.await
.unwrap();
let code = r.status().as_u16();
(code, r.text().await.unwrap())
}
fn accepted(code: u16, body: &str) -> bool {
(200..300).contains(&code) && !body.contains("FAILURE")
}
#[tokio::test]
async fn 所有路由都挂上了() {
let base = spawn_http(Api::new(store().await)).await;
let cli = reqwest::Client::new();
for (path, 期望非空) in [("/health", true), ("/api/dtmsvr/newGid", true), ("/console", true)] {
let r = cli.get(format!("{base}{path}")).send().await.unwrap();
assert!(r.status().is_success(), "{path} 应该 200,实际 {}", r.status());
if 期望非空 {
assert!(!r.text().await.unwrap().is_empty(), "{path} 不该返回空");
}
}
let html = cli.get(format!("{base}/")).send().await.unwrap().text().await.unwrap();
assert!(html.contains("<"), "管理台应该返回 HTML");
}
#[tokio::test]
async fn new_gid每次都不一样() {
let base = spawn_http(Api::new(store().await)).await;
let cli = reqwest::Client::new();
let mut seen = std::collections::HashSet::new();
for _ in 0..5 {
let t = cli
.get(format!("{base}/api/dtmsvr/newGid"))
.send()
.await
.unwrap()
.text()
.await
.unwrap();
assert!(seen.insert(t.clone()), "gid 重复了: {t}");
}
}
#[tokio::test]
async fn 提交saga并能查回来() {
let st = store().await;
let base = spawn_http(Api::new(st.clone())).await;
let (code, body) = post(
&base,
"/api/dtmsvr/submit",
r#"{"gid":"h1","steps":[{"action":"http://x/a","compensate":"http://x/c"}]}"#,
)
.await;
assert!(accepted(code, &body), "提交应该被受理,得到 {code} {body}");
let q = reqwest::get(format!("{base}/api/dtmsvr/query?gid=h1"))
.await
.unwrap()
.text()
.await
.unwrap();
assert!(q.contains("\"gid\":\"h1\""), "查询应该能查到: {q}");
let all = reqwest::get(format!("{base}/api/dtmsvr/all"))
.await
.unwrap()
.text()
.await
.unwrap();
assert!(all.contains("h1"), "列表里应该有这一笔: {all}");
}
#[tokio::test]
async fn 重复提交同一个gid必须幂等成功() {
let base = spawn_http(Api::new(store().await)).await;
let b = r#"{"gid":"h-dup","steps":[{"action":"http://x/a","compensate":"http://x/c"}]}"#;
for i in 1..=3 {
let (code, body) = post(&base, "/api/dtmsvr/submit", b).await;
assert!(accepted(code, &body), "第 {i} 次提交应该成功,得到 {code} {body}");
}
}
#[tokio::test]
async fn 报文不合法要被拒而不是panic() {
let base = spawn_http(Api::new(store().await)).await;
for (path, body) in [
("/api/dtmsvr/submit", "{}"), ("/api/dtmsvr/submit", "不是json"), ("/api/dtmsvr/registerBranch", r#"{"gid":"x"}"#), ("/api/dtmsvr/abort", r#"{"gid":"不存在"}"#), ] {
let (code, _) = post(&base, path, body).await;
assert!(
code < 500,
"{path} 的烂报文应该是 4xx 客户端错误,不该是 5xx(得到 {code})"
);
let h = reqwest::get(format!("{base}/health")).await.unwrap();
assert!(h.status().is_success(), "{path} 之后服务应该还活着");
}
}
#[cfg(feature = "grpc")]
mod 等价 {
use super::*;
use dtmrs_server::grpc::pb;
use dtmrs_server::grpc::server::TcService;
use tokio_stream::wrappers::TcpListenerStream;
async fn spawn_grpc(api: Api) -> String {
let l = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = l.local_addr().unwrap();
tokio::spawn(async move {
tonic::transport::Server::builder()
.add_service(TcService::new(api).into_server())
.serve_with_incoming(TcpListenerStream::new(l))
.await
});
tokio::time::sleep(std::time::Duration::from_millis(150)).await;
format!("http://{addr}")
}
async fn 两边登记分支(状态: GlobalStatus) -> (bool, bool) {
let st = store().await;
let http_base = spawn_http(Api::new(st.clone())).await;
let grpc_base = spawn_grpc(Api::new(st.clone())).await;
for gid in ["eq-http", "eq-grpc"] {
st.create_global(&tcc_rows(gid), &[]).await.unwrap();
st.set_global_status(gid, 状态, TransType::Tcc, "")
.await
.unwrap();
}
let (c, b) = post(
&http_base,
"/api/dtmsvr/registerBranch",
r#"{"gid":"eq-http","branch_id":"01","confirm":"http://x/c","cancel":"http://x/n"}"#,
)
.await;
let http_ok = accepted(c, &b);
let mut cli = pb::tc_client::TcClient::connect(grpc_base).await.unwrap();
let grpc_ok = cli
.register_branch(pb::RegisterBranchRequest {
gid: "eq-grpc".into(),
branch_id: "01".into(),
confirm: "http://x/c".into(),
cancel: "http://x/n".into(),
r#try: String::new(),
commit: String::new(),
rollback: String::new(),
})
.await
.is_ok();
(http_ok, grpc_ok)
}
#[tokio::test]
async fn 登记分支的受理结论两边必须一致() {
for 状态 in [
GlobalStatus::Prepared,
GlobalStatus::Submitted,
GlobalStatus::Aborting,
GlobalStatus::Succeed,
GlobalStatus::Failed,
] {
let (http_ok, grpc_ok) = 两边登记分支(状态).await;
assert_eq!(
http_ok,
grpc_ok,
"{} 状态下两个协议结论不一致:HTTP {} / gRPC {}",
状态.as_str(),
if http_ok { "受理" } else { "拒绝" },
if grpc_ok { "受理" } else { "拒绝" },
);
}
}
#[tokio::test]
async fn 提交的受理结论两边必须一致() {
let st = store().await;
let http_base = spawn_http(Api::new(st.clone())).await;
let grpc_base = spawn_grpc(Api::new(st.clone())).await;
let mut cli = pb::tc_client::TcClient::connect(grpc_base).await.unwrap();
let (c, b) = post(&http_base, "/api/dtmsvr/submit", r#"{"gid":"eq-s1"}"#).await;
let http_ok = accepted(c, &b);
let grpc_ok = cli
.submit(pb::SubmitRequest {
gid: "eq-s2".into(),
trans_type: String::new(),
steps: vec![],
})
.await
.is_ok();
assert_eq!(http_ok, grpc_ok, "空步骤提交:HTTP {http_ok} / gRPC {grpc_ok}");
let (c, b) = post(&http_base, "/api/dtmsvr/submit", r#"{"gid":""}"#).await;
let http_ok = accepted(c, &b);
let grpc_ok = cli
.submit(pb::SubmitRequest {
gid: String::new(),
trans_type: String::new(),
steps: vec![],
})
.await
.is_ok();
assert_eq!(http_ok, grpc_ok, "空 gid:HTTP {http_ok} / gRPC {grpc_ok}");
}
#[tokio::test]
async fn 中止的受理结论两边必须一致() {
for 状态 in [GlobalStatus::Submitted, GlobalStatus::Succeed] {
let st = store().await;
let http_base = spawn_http(Api::new(st.clone())).await;
let grpc_base = spawn_grpc(Api::new(st.clone())).await;
for gid in ["ab-http", "ab-grpc"] {
let steps = vec![SagaStep::new("http://x/a", "http://x/c")];
let (g, br) = dtmrs_server::saga_rows(gid, &steps);
st.create_global(&g, &br).await.unwrap();
st.set_global_status(gid, 状态, TransType::Saga, "")
.await
.unwrap();
}
let (c, b) = post(&http_base, "/api/dtmsvr/abort", r#"{"gid":"ab-http"}"#).await;
let http_ok = accepted(c, &b);
let mut cli = pb::tc_client::TcClient::connect(grpc_base).await.unwrap();
let grpc_ok = cli
.abort(pb::AbortRequest {
gid: "ab-grpc".into(),
})
.await
.is_ok();
assert_eq!(
http_ok,
grpc_ok,
"{} 状态下中止:HTTP {http_ok} / gRPC {grpc_ok}",
状态.as_str()
);
}
}
}