mirror of
https://github.com/logos-messaging/chat-store.git
synced 2026-07-30 06:53:29 +00:00
smoke test script
This commit is contained in:
parent
9c572d65db
commit
5752c00463
168
Cargo.lock
generated
168
Cargo.lock
generated
@ -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"
|
||||
|
||||
@ -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"] }
|
||||
|
||||
26
README.md
26
README.md
@ -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
129
examples/smoke_test.rs
Normal 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())
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user