smoke test script

This commit is contained in:
kaichaosun 2026-06-11 16:36:39 +08:00
parent 9c572d65db
commit 5752c00463
No known key found for this signature in database
GPG Key ID: 223E0F992F4F03BF
4 changed files with 327 additions and 1 deletions

168
Cargo.lock generated
View File

@ -190,6 +190,12 @@ dependencies = [
"generic-array",
]
[[package]]
name = "bumpalo"
version = "3.20.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
[[package]]
name = "byteorder"
version = "1.5.0"
@ -228,6 +234,7 @@ dependencies = [
"clap",
"ed25519-dalek",
"hex",
"reqwest",
"serde",
"serde_json",
"sqlx",
@ -742,6 +749,7 @@ dependencies = [
"pin-project-lite",
"smallvec",
"tokio",
"want",
]
[[package]]
@ -750,13 +758,21 @@ version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
dependencies = [
"base64",
"bytes",
"futures-channel",
"futures-util",
"http",
"http-body",
"hyper",
"ipnet",
"libc",
"percent-encoding",
"pin-project-lite",
"socket2",
"tokio",
"tower-service",
"tracing",
]
[[package]]
@ -872,6 +888,12 @@ dependencies = [
"hashbrown 0.17.1",
]
[[package]]
name = "ipnet"
version = "2.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
[[package]]
name = "is_terminal_polyfill"
version = "1.70.2"
@ -884,6 +906,17 @@ version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "js-sys"
version = "0.3.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2025f20d7a4fa7785846e7b63d10a76d3f1cee98ee5cb79ea59703f95e42162"
dependencies = [
"cfg-if",
"futures-util",
"wasm-bindgen",
]
[[package]]
name = "lazy_static"
version = "1.5.0"
@ -1248,6 +1281,40 @@ version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
[[package]]
name = "reqwest"
version = "0.12.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
dependencies = [
"base64",
"bytes",
"futures-channel",
"futures-core",
"futures-util",
"http",
"http-body",
"http-body-util",
"hyper",
"hyper-util",
"js-sys",
"log",
"percent-encoding",
"pin-project-lite",
"serde",
"serde_json",
"serde_urlencoded",
"sync_wrapper",
"tokio",
"tower",
"tower-http",
"tower-service",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
]
[[package]]
name = "rsa"
version = "0.9.10"
@ -1698,6 +1765,9 @@ name = "sync_wrapper"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
dependencies = [
"futures-core",
]
[[package]]
name = "synstructure"
@ -1818,6 +1888,24 @@ dependencies = [
"tracing",
]
[[package]]
name = "tower-http"
version = "0.6.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840"
dependencies = [
"bitflags",
"bytes",
"futures-util",
"http",
"http-body",
"pin-project-lite",
"tower",
"tower-layer",
"tower-service",
"url",
]
[[package]]
name = "tower-layer"
version = "0.3.3"
@ -1892,6 +1980,12 @@ dependencies = [
"tracing-log",
]
[[package]]
name = "try-lock"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "typenum"
version = "1.20.1"
@ -1967,6 +2061,15 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "want"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
dependencies = [
"try-lock",
]
[[package]]
name = "wasi"
version = "0.11.1+wasi-snapshot-preview1"
@ -1979,6 +2082,71 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b"
[[package]]
name = "wasm-bindgen"
version = "0.2.123"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a254a4b10c19a76f09a27640e7ffbf9bc30bf67e16a3bf28aaefa4920fe81563"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.73"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54568702fabf5d4849ce2b90fadfa64168a097eaf4b351ce9df8b687a0086aaf"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.123"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24a40fc75b0ec6f3746ceb10d36f53a93dcd68a93b11b6445983945d79eba0dc"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.123"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "908f34bd9b9ce3d4caf07b72dfab63d61504d156856c6bd3cd87fa350cf3985b"
dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.123"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7acbf7616c27b194bbb550bf77ed0c2c3e5b7fd1260a93082b95fb7f47959b92"
dependencies = [
"unicode-ident",
]
[[package]]
name = "web-sys"
version = "0.3.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e0871acf327f283dc6da28a1696cdc64fb355ba9f935d052021fa77f35cce69"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "whoami"
version = "1.6.1"

View File

@ -30,3 +30,8 @@ sqlx = { version = "0.8", default-features = false, features = [
tokio = { version = "1", features = ["rt-multi-thread", "macros", "signal", "sync", "time"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
[dev-dependencies]
# `smoke_test` example only talks to a localhost HTTP server, so no TLS backend
# (and no OpenSSL) is pulled in.
reqwest = { version = "0.12", default-features = false, features = ["blocking", "json"] }

View File

@ -167,7 +167,31 @@ within `--retention-days`. The schema is an internal detail and may change.
## Smoke test
End-to-end check with the real `chat-cli` (which lives in the
The quickest end-to-end check is the bundled [`smoke_test`](examples/smoke_test.rs)
example. It generates throwaway Ed25519 keys, signs and publishes a keypackage and
an account bundle, fetches both back, and confirms the replay guard:
```bash
# Terminal 1 — start a server with a fresh db
cargo run -- --bind 127.0.0.1:8080 --db tmp/chat-store.db
# Terminal 2 — run the example against it (defaults to http://127.0.0.1:8080)
cargo run --example smoke_test
# or point it elsewhere:
cargo run --example smoke_test -- http://127.0.0.1:8080
```
Expected output:
```text
POST /v0/keypackage -> 204 No Content (expect 204)
GET /v0/keypackage/<id> -> 200 OK (expect 200) {"payload":...,"signature":...}
POST /v0/account -> 204 No Content (expect 204)
GET /v0/account/<id> -> 200 OK (expect 200) {"payload":...,"signature":...,"updated_at":...}
POST /v0/account (replay) -> 409 Conflict (expect 409)
```
You can also exercise it with the real `chat-cli` (which lives in the
[libchat](https://github.com/logos-messaging/libchat) repo) against a running
server:

129
examples/smoke_test.rs Normal file
View File

@ -0,0 +1,129 @@
//! End-to-end smoke test for the chat-store HTTP API.
//!
//! Generates throwaway Ed25519 keys, signs a keypackage bundle and an account
//! device-list bundle, POSTs both, then GETs them back. The server verifies the
//! signature over the exact payload bytes, so the bundles must be properly
//! signed — this example does that for you.
//!
//! Run it against a live server:
//!
//! ```text
//! cargo run -- --bind 127.0.0.1:8080 --db tmp/chat-store.db # terminal 1
//! cargo run --example smoke_test # terminal 2
//! cargo run --example smoke_test -- http://host:port # custom target
//! ```
use std::time::{SystemTime, UNIX_EPOCH};
use anyhow::Result;
use base64::Engine;
use base64::engine::general_purpose::STANDARD as BASE64;
use ed25519_dalek::{Signer, SigningKey};
use reqwest::blocking::Client;
use serde_json::json;
/// Domain-separation prefix the server expects on every account bundle payload.
/// Must match `BUNDLE_DOMAIN` in `src/store.rs` (and libchat's account_directory).
const ACCOUNT_BUNDLE_DOMAIN: &[u8] = b"libchat:account-device-bundle\0";
fn main() -> Result<()> {
let base = std::env::args()
.nth(1)
.unwrap_or_else(|| "http://127.0.0.1:8080".to_string());
let base = base.trim_end_matches('/');
let client = Client::new();
println!("Testing chat-store at {base}");
test_keypackage(&client, base)?;
test_account(&client, base)?;
Ok(())
}
fn test_keypackage(client: &Client, base: &str) -> Result<()> {
let key = signing_key(1);
let device_id = pub_hex(&key);
// The keypackage payload is opaque to the server: any bytes work as long as
// the signature matches. Real clients put `timestamp_ms_le[8] || key_package`.
let mut payload = 0u64.to_le_bytes().to_vec();
payload.extend_from_slice(b"hello-keypackage");
let resp = client
.post(format!("{base}/v0/keypackage"))
.json(&json!({
"device_id": device_id,
"payload": BASE64.encode(&payload),
"signature": BASE64.encode(key.sign(&payload).to_bytes()),
}))
.send()?;
println!("POST /v0/keypackage -> {} (expect 204)", resp.status());
let resp = client
.get(format!("{base}/v0/keypackage/{device_id}"))
.send()?;
println!(
"GET /v0/keypackage/<id> -> {} (expect 200) {}",
resp.status(),
resp.text()?
);
Ok(())
}
fn test_account(client: &Client, base: &str) -> Result<()> {
let key = signing_key(2);
let account_id = pub_hex(&key);
// The account payload is NOT arbitrary: it must start with the domain prefix,
// a version byte, and an 8-byte little-endian lamport so the server can run
// its replay check. Device pubkeys would follow, but stay opaque to the server.
let lamport: u64 = 1;
let mut payload = ACCOUNT_BUNDLE_DOMAIN.to_vec();
payload.push(1); // version
payload.extend_from_slice(&lamport.to_le_bytes());
let body = json!({
"account_id": account_id,
"payload": BASE64.encode(&payload),
"signature": BASE64.encode(key.sign(&payload).to_bytes()),
});
let resp = client
.post(format!("{base}/v0/account"))
.json(&body)
.send()?;
println!("POST /v0/account -> {} (expect 204)", resp.status());
let resp = client.get(format!("{base}/v0/account/{account_id}")).send()?;
println!(
"GET /v0/account/<id> -> {} (expect 200) {}",
resp.status(),
resp.text()?
);
// Re-posting the same lamport must be rejected as a stale replay.
let resp = client
.post(format!("{base}/v0/account"))
.json(&body)
.send()?;
println!("POST /v0/account (replay) -> {} (expect 409)", resp.status());
Ok(())
}
/// A throwaway Ed25519 signing key seeded from the current time (plus a salt so
/// the keypackage and account keys differ), so repeated runs use fresh ids.
fn signing_key(salt: u8) -> SigningKey {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_nanos()
.to_le_bytes();
let mut seed = [0u8; 32];
for (i, b) in seed.iter_mut().enumerate() {
*b = nanos[i % nanos.len()] ^ salt ^ (i as u8);
}
SigningKey::from_bytes(&seed)
}
fn pub_hex(key: &SigningKey) -> String {
hex::encode(key.verifying_key().to_bytes())
}