Files
quicproquo/docs/src/contributing/testing.md
Christian Nennemann 2e081ead8e chore: rename quicproquo → quicprochat in docs, Docker, CI, and packaging
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
2026-03-21 19:14:06 +01:00

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:?}") for Result checks.
  • 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::decode on each generated type and verify it either parses correctly or returns a typed error (no panics, no undefined behaviour).
  • MLS message handler: Feed arbitrary MLSMessage bytes to the GroupMember::receive_message path.

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 .bin files in tests/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