Rename all project references from quicproquo/qpq to quicprochat/qpc across documentation, Docker configuration, CI workflows, packaging scripts, operational configs, and build tooling. - Docker: crate paths, binary names, user/group, data dirs, env vars - CI: workflow crate references, binary names, artifact names - Docs: all markdown files under docs/, SDK READMEs, book.toml - Packaging: OpenWrt Makefile, init script, UCI config (file renames) - Scripts: justfile, dev-shell, screenshot, cross-compile, ai_team - Operations: Prometheus config, alert rules, Grafana dashboard - Config: .env.example (QPQ_* → QPC_*), CODEOWNERS paths - Top-level: README, CONTRIBUTING, ROADMAP, CLAUDE.md
9.3 KiB
Testing Strategy
This page describes the testing structure, conventions, and current coverage for
quicprochat. All tests run with cargo test --workspace (or just test) and
must pass before any code is merged.
For the coding standards that tests must follow, see Coding Standards.
Test Organisation
Unit Tests
Unit tests live alongside the code they test, in #[cfg(test)] mod tests blocks
at the bottom of each source file. They test individual functions and types in
isolation.
quicprochat-core (96 tests):
| Module | Tests | What they cover |
|---|---|---|
codec |
7 | Length-prefixed frame encoding/decoding, edge cases (empty payload, max size, partial frame, exact boundary) |
keypair |
3 | Ed25519 keypair generation, public key extraction, deterministic re-derivation |
group |
2 | Group round-trip (create + add + join + send + recv), group_id lifecycle |
hybrid_kem |
11 | Encapsulate/decapsulate round-trip, key generation, combiner correctness, wrong-key rejection, serialisation |
opaque_auth |
12 | OPAQUE registration + login full flow, bad password rejection |
mls_* |
61 | MLS key schedule, member add/remove, Welcome processing, key exhaustion |
quicprochat-rpc (18 tests):
| Module | Tests | What they cover |
|---|---|---|
framing |
8 | Wire framing round-trips, method ID encoding, length-prefix correctness |
dispatch |
10 | Handler dispatch, method not found, middleware chain, timeout enforcement |
quicprochat-sdk (30 tests):
| Module | Tests | What they cover |
|---|---|---|
client |
15 | QpqClient connect, send, receive, event broadcast |
conversation_store |
15 | ConversationStore CRUD, pagination, message ordering |
quicprochat-server (65 tests):
| Module | Tests | What they cover |
|---|---|---|
auth |
20 | OPAQUE registration, login, session management, rate limiting |
node_service |
20 | KeyPackage upload/fetch, message enqueue/deliver, sealed sender |
storage |
15 | FileBackedStore and SqlStore CRUD, MLS entity serialisation |
federation |
10 | Federation peer relay, mTLS validation, domain routing |
quicprochat-kt (21 tests):
| Module | Tests | What they cover |
|---|---|---|
merkle_log |
21 | Merkle tree insertion, consistency proofs, root hash correctness |
quicprochat-p2p (34 tests):
| Module | Tests | What they cover |
|---|---|---|
| iroh mesh | 34 | P2P peer discovery, relay, mesh join/leave |
Integration and E2E Tests
E2E tests live in crates/quicprochat-client/tests/e2e.rs (20 tests) and
exercise the full client-server stack in-process. Each test spawns a real server
using tokio::spawn, runs client operations against it, and asserts on the
results.
quicprochat-client unit (16 tests):
| File | What it covers |
|---|---|
src/lib.rs |
CLI command parsing, client state machine, error formatting |
quicprochat-client E2E (20 tests):
| Test | What it covers |
|---|---|
auth_failure |
Rejected OPAQUE login (wrong password) |
message_ordering |
Sequential message delivery order preserved |
opaque_flow |
Full OPAQUE registration + login round-trip |
key_exhaustion |
Behaviour when KeyPackage queue is empty |
rate_limit |
Rate limiting rejects excess requests |
mls_group_round_trip |
Full MLS group: create, add member, send, receive |
keypackage_single_use |
KeyPackage consumed on first fetch |
| and 13 more | Additional protocol scenarios |
Test Pattern
All E2E tests follow the same pattern:
#[tokio::test]
async fn test_something() {
// 1. Acquire shared lock to avoid port conflicts
let _lock = AUTH_LOCK.lock().await;
// 2. Start server in background
let server_handle = tokio::spawn(async move {
server::run(config).await.expect("server failed");
});
// 3. Wait for server to be ready
tokio::time::sleep(Duration::from_millis(100)).await;
// 4. Run client operations
let result = client::do_something(server_addr).await;
// 5. Assert
assert!(result.is_ok());
// 6. Cleanup
server_handle.abort();
}
This pattern ensures tests are self-contained and do not require an external server process.
Running Tests
Full Workspace
just test
# or
cargo test --workspace
This runs all unit tests and integration tests across all nine crates (301 tests total).
E2E Tests (serialised)
The E2E test suite shares an AUTH_LOCK tokio::Mutex to prevent port binding
conflicts when tests run in parallel. Always run E2E tests with a single thread:
cargo test -p quicprochat-client --test e2e -- --test-threads 1
Running without --test-threads 1 may cause intermittent bind errors if two
tests try to use the same port concurrently.
Single Crate
cargo test -p quicprochat-core
cargo test -p quicprochat-rpc
cargo test -p quicprochat-sdk
cargo test -p quicprochat-server
cargo test -p quicprochat-kt
cargo test -p quicprochat-p2p
Single Test
cargo test -p quicprochat-core -- codec::tests::test_round_trip
cargo test -p quicprochat-client --test e2e -- opaque_flow --test-threads 1
With Output
cargo test --workspace -- --nocapture
Current Results
All 301 tests pass on branch v2.
| Crate | Unit / Integration Tests | E2E Tests | Total |
|---|---|---|---|
quicprochat-core |
96 | -- | 96 |
quicprochat-rpc |
18 | -- | 18 |
quicprochat-sdk |
30 | -- | 30 |
quicprochat-server |
65 | -- | 65 |
quicprochat-kt |
21 | -- | 21 |
quicprochat-p2p |
34 | -- | 34 |
quicprochat-client |
16 unit + 1 doctest | 20 | 37 |
| Total | 281 | 20 | 301 |
Test Conventions
Naming
Test functions use descriptive names that state what is being tested and the expected outcome:
#[test]
fn encode_decode_round_trip_preserves_payload() { ... }
#[test]
fn empty_payload_produces_length_zero_frame() { ... }
#[test]
fn fetch_consumes_keypackage_single_use() { ... }
Assertions
- Use
assert_eq!with both expected and actual values. - Use
assert!(result.is_ok(), "descriptive message: {result:?}")forResultchecks. - For crypto operations, assert on specific error variants, not just
is_err().
No External Dependencies
Tests must not depend on external services, network access, or filesystem state
outside the test's temporary directory. The tokio::spawn pattern for
E2E tests ensures everything runs in-process.
Determinism
Tests must be deterministic. If randomness is needed (e.g., key generation), the test must not depend on specific random values — only on the properties of the output (correct length, successful round-trip, etc.).
No .unwrap() in Test Setup
.unwrap() is acceptable in test assertions, but test setup that fails silently
is not. Use expect("descriptive message") on setup operations so failures
report clearly.
Planned Testing Enhancements
Fuzzing Targets (M5+)
Fuzz testing for parser and deserialisation code:
- Protobuf message parser: Feed arbitrary bytes to
prost::Message::decodeon each generated type and verify it either parses correctly or returns a typed error (no panics, no undefined behaviour). - MLS message handler: Feed arbitrary
MLSMessagebytes to theGroupMember::receive_messagepath.
Tool: cargo-fuzz with libfuzzer.
Golden-Wire Fixtures (M5+)
Serialised test vectors for regression testing across versions:
- Capture the wire bytes of known-good Protobuf messages at the current version.
- Store as
.binfiles intests/fixtures/. - Each test deserialises the fixture and verifies the expected field values.
- When the wire format changes, fixtures are updated with a version bump.
This catches accidental wire-format regressions that would break client-server compatibility.
N-1 Compatibility Tests (M5+)
Test that a client built at version N can communicate with a server built at version N-1 (and vice versa):
- Build two versions of the binary (current and previous release).
- Run the older server with the newer client and verify all RPCs succeed.
- Run the newer server with the older client and verify graceful degradation.
Criterion Benchmarks (M5)
Performance benchmarks using Criterion.rs:
- Key generation latency (Ed25519, X25519, ML-KEM-768).
- MLS encap/decap (KeyPackage generation, Welcome processing).
- Group-add latency scaling: 2, 10, 100, 1000 members.
- Protobuf serialise/deserialise throughput.
Benchmarks run separately from tests (cargo bench) and are not part of the
CI gate, but are tracked for regression detection.
Docker-based E2E Tests (Phase 5)
End-to-end tests using testcontainers-rs:
- Spin up server container from the Docker image.
- Run client operations from the test process against the containerised server.
- Verify real network boundaries, container startup, and multi-process interactions.
Cross-references
- Coding Standards -- quality requirements for test code
- Milestones -- which tests were added at each milestone
- Production Readiness WBS -- Phase 5 (E2E Harness and Security Tests)