testkit
Broker fixtures for the acceptance suite. Every milestone is verified against a real broker in a container — there are no mocked brokers in this workspace, by rule — and this crate is what makes that affordable.
publish = false.
Module map
| File | Lines | What |
|---|---|---|
config.rs | 616 | BrokerConfig, Security, SaslMechanism, SaslUser |
image.rs | 291 | the container image and its environment |
kafka.rs | 233 | single_broker, cluster, and the _with variants |
harness.rs | 173 | the Cluster trait and ExternalCluster |
error.rs | 58 | fixture errors |
The usual shapes
use testkit::{BrokerConfig, SaslMechanism, Security};
async fn example() -> testkit::Result<()> {
// The common case.
let broker = testkit::single_broker().await?;
let addr = broker.bootstrap_csv();
// Three nodes, for anything replica-aware.
let cluster = testkit::cluster(3).await?;
// Or configured.
let sasl = testkit::single_broker_with(
BrokerConfig::new()
.with_security(Security::SaslPlaintext)
.with_mechanism(SaslMechanism::Plain)
.with_user("alice", "alice-pw"),
)
.await?;
Ok(())
}
apache/kafka:4.3.1 in KRaft mode.
Tests take &dyn Cluster, never a concrete type
This is the crate's most important design decision and it is not abstraction for its own sake.
kaas-lib is the natural conformance harness for the
kaas broker, and that only works if the
acceptance suite can be pointed at something other than apache/kafka:4.3.1
without touching a single test. A hardcoded image in the fixtures would
quietly foreclose that — not with an error, but by making the alternative
expensive enough that nobody does it.
use std::fmt;
trait Cluster: fmt::Debug + Send + Sync {
/// Bootstrap addresses reachable from the *test process* — for container
/// fixtures the host-mapped port, not the in-container one.
fn bootstrap(&self) -> &[String];
fn nodes(&self) -> usize;
// …plus exec, for driving the Kafka shell tools inside a node
}
ExternalCluster is the other half of the bargain: it makes "point the suite
at a broker that is already running" a supported mode today, so the seam
gets exercised rather than rotting into a trait nobody has implemented twice.
exec matters more than it looks
exec(node, argv) runs a command inside a fixture node, and several fixtures
are generated by the Kafka shell tools rather than by a Rust client.
The group-kinds fixture is the reason. rdkafka
cannot create a KIP-932 share group — librdkafka has no share-group support
at all — so no Rust client in the workspace can generate that fixture. The
apache/kafka image already ships kafka-console-consumer.sh and
kafka-console-share-consumer.sh, and driving those through exec reaches
every group kind with zero build dependencies.
The config builder exists from the start
single_broker_with(BrokerConfig) was built before anything needed it,
because three later milestones do: SASL_PLAINTEXT/PLAIN and
SASL_SSL/SCRAM-SHA-512 brokers, an authorizer-enabled broker for ACLs, and
aggressive compaction settings for the backward-scan test.
The same argument applies to cluster(3). Retrofitting multi-broker fixtures
after the fact is painful, and leader spread, log dirs and reassignment are
not observable on one broker.
It paid off again for OAUTHBEARER, which arrived long after: a mechanism,
one JAAS entry naming
OAuthBearerUnsecuredValidatorCallbackHandler, and unsecured_jws to mint
tokens. That handler is what makes the fixture possible at all — Kafka ships it
so a broker can validate OAUTHBEARER with no identity provider anywhere, which
takes the issuer out of the picture without taking the broker out of it.
Note also that OAUTHBEARER is the first mechanism with no users: the principal
is the token's sub claim, so validate() now asks whether any enabled
mechanism needs credentials rather than assuming SASL means users.
Start reading at harness.rs — it is short, and its module docs state
the conformance-harness argument in full.