Expand description
cratefield-testing: the conformance kit every Factory Zero module runs
against (issue #9) — fake ports, an in-memory SQLite Database, and
request helpers over the axum router with no network.
use cratefield_testing::{conformance, TestHarness, request};
#[test]
fn my_module_conforms() {
conformance(Box::new(my_module::MyModule::new()));
}
#[pollster::test]
async fn join_returns_202() {
let kit = TestHarness::new(vec![Box::new(my_module::MyModule::new())]);
let response = request(&kit.router, http::Method::POST,
"/v1/my-module/join", Some(r#"{"email":"nick@example.com"}"#)).await;
assert_eq!(response.status, http::StatusCode::ACCEPTED);
}§cratefield-testing
The conformance kit every Cratefield module runs against — public and
private modules alike. Fake ports, an in-memory SQLite Database, and
request helpers over the axum router with no network.
§A 15-line module test
ⓘ
use cratefield_testing::{conformance, request, TestHarness};
#[test]
fn my_module_conforms() {
conformance(Box::new(my_module::MyModule::new()));
}
#[pollster::test]
async fn join_accepts_an_email() {
let kit = TestHarness::new(vec![Box::new(my_module::MyModule::new())]);
let res = request(&kit.router, http::Method::POST, "/v1/my-module/join",
Some(r#"{"email":"nick@example.com"}"#)).await;
assert_eq!(res.status, http::StatusCode::ACCEPTED);
assert_eq!(kit.mailer.sent().len(), 1); // the confirmation mail
}§What you get
TestHarness::new(vec![Box<dyn Module>])— builds the harness with every port faked, applies each module’s sqlite migrations to a fresh in-memory database, assembles the router. Exposes{ router, mailer, captcha, rate_limiter, db, clock, kv, http, defer, signer, events, modules, dialect }.- Parity (issue #20):
TestHarness::with_database(modules, Dialect::Sqlite | Dialect::Postgres { url })runs the same harness on a throwaway Postgres 16 database (per-harness, dropped on drop; needs the crate’spostgresfeature and a server atFZ_TEST_POSTGRES_URL).TestHarness::all_dialects(make_modules)/all_dialects_with_ports(make_modules, patch)build one kit per dialect available in the environment so a module suite loops over them — one test definition, every engine. The module factory runs once per dialect: modules may carry per-build state, so kits never share instances. - Fakes:
FakeMailer(recordsMessages;SendOk/NotConfigured/Failmodes, switchable mid-test),FakeCaptcha(allow-all or token list),FakeRateLimiter(scriptedDecisions + call count),FixedClock,MemoryKeyValue,FakeHttpClient(scripted responses + captured requests),EmptyDatabase(servicesSELECT 1only),FakeDefer(collects deferred futures;drain().awaitruns them), and aSignerwith the fixed dummyTEST_HARNESS_SECRET. request(&router, method, path, json?) -> TestResponse { status, headers, json() }—tower::ServiceExt::oneshot, no network.conformance(module)— the shared suite, run once per available dialect: mounts + health listing, request under the prefix, migrations apply twice on fresh databases,view_forhides undeclared ports, the two-concurrent-requests request-id test (ADR 0007), and — for modules with awell_knownrouter — that it serves at the root/.well-knownand never under/v1(#46).full_fake_ports()— aPortsbundle with everyPortwired to a fake: the one conformance’s visibility check uses.assert_wasm_safe_deps(env!("CARGO_PKG_NAME"))—cargo treecheck: noworker/wasm-bindgen/tokio/reqwestin the module’s normal dependency tree.
The in-memory Database is cratefield-adapter-sqlite; assertions on
kit.db see exactly what the module wrote. On the Postgres leg kit.db
is a pool on the kit’s own tokio runtime marshalled per call, so
pollster-driven tests, spawned threads and deferred handlers all reach
it.
MIT. Built in the open for Cratefield, a Factory Zero venture.
Modules§
- vectors
- The Web Push test vectors the workspace shares.
Structs§
- Ask
- One
Classifier::askaFakeClassifieranswered, for assertions: the state as it arrived (untruncated — this fake has no provider budget to spend) and the question ids in the set, in map order. - Empty
Database - Fake
Auth - A
cratefield_core::Authanswering from a mode rather than from a key set. - Fake
Captcha - Fake
Classifier - An in-memory
Classifierfor module tests: records every ask it answered and answers according to itsClassifierMode. - Fake
Defer - Fake
Dispatcher - An in-process
Dispatcherthat answers from an axumRouter, so a module can be exercised through a sidecar mount without a network or a second Worker (ADR 0009). The conformance kit uses it to run the same assertions against both mounts (#64). - Fake
Http Client - Fake
Mailer - Fake
Payments - An in-memory
cratefield_core::Paymentsfor module tests: records which calls were made and answers per itsPaymentsMode.verify_webhooktreats a signature header of"invalid"as a tampered event. - Fake
Push - An in-memory
cratefield_core::Pushfor module tests: records every(recipient, notification)and answers according to itsPushMode. - Fake
Rate Limiter - Fake
Realtime - An in-memory
cratefield_core::Realtimefor module tests: records every broadcast per room and reports a fixed member list. It exercises the port a module holds (broadcast/members from outside a socket); the socket lifecycle andRoomHandlerare the runtime adapter’s job, covered by the native adapter’s own tests. - Fake
Sidecar - A dispatcher backed by an in-process router.
- Fake
Text Model - An in-memory
TextModelfor module tests: records everyPromptit completed and answers according to itsTextModelMode. - Fake
Tracker - An in-memory
Trackerfor module tests: records every acceptedfile/statuscall and answers according to itsTrackerMode. A call the mode answered with an error is not recorded, asFakePushdoes not record a failed send. - Filed
Call - One
Tracker::fileaFakeTrackeraccepted, for assertions. - Fixed
Clock - Memory
Blob - An in-memory
cratefield_core::Blobstore for module tests: keeps objects in a map, and has no presigned URLs (sosigned_urlreportsUnsupported, as a directory store does). - Memory
KeyValue - Statused
Call - One
Tracker::statusaFakeTrackeraccepted, for assertions. - TempDir
- A directory under the system temp directory, removed when this value is dropped — including while a panic unwinds.
- Test
Harness - A harness with every port faked and a migrated database, migrations
applied per module on creation. The same database handle is wired
into the router and exposed for assertions — SQLite in memory by
default (
TestHarness::new), or a throwaway Postgres 16 database for the parity suite (TestHarness::with_database, issue #20). - Test
Response - A fully-buffered test response.
Enums§
- Auth
Mode - What a
FakeAuthanswers. - Classifier
Mode - How a
FakeClassifieranswers, mirroringTextModelModefor the classifier port (issue #456). - Dialect
- The database engine backing a test harness.
- Fault
- A way the hop can be wrong. Used by the kit’s own tests to prove the parity checks fail when the forwarder misbehaves; never by a module.
- Mailer
Mode - How a
FakeMaileranswers. - Payments
Call - What a
FakePaymentsrecorded, for assertions. - Payments
Mode - How a
FakePaymentsresponds. - Push
Mode - How a
FakePushresponds, mirroringMailerModefor the push port. - Text
Model Mode - How a
FakeTextModelanswers, mirroringPushModefor the text model port (issue #429). - Tracker
Mode - How a
FakeTrackeranswers, mirroringMailerModeandPushModefor the tracker port (issue #431).
Constants§
Functions§
- assert_
batch_ is_ atomic - Proves
db’sbatch_atomicis all-or-nothing: a committed batch is fully visible, a failing batch is invisible. Runs its own probe table, so it can be called against any adapter with a writable connection. - assert_
blob_ round_ trips - Proves
dbround-trips bound bytes exactly: aVec<u8>bound asValue::Bytesreads back byte-for-byte identical, and a NULL blob reads back as no bytes. Runs its own probe table, so it can be called against any adapter with a writable connection. - assert_
wasm_ safe_ deps - Asserts the named crate’s normal dependency tree carries none of the
dependencies a wasm module must never see:
worker,wasm-bindgen,tokio,reqwest. Shell-out check (cargo tree --edges normal) so the assertion is made against the graph cargo actually resolves, not a copy of it that could drift. - classifier_
conformance - Asserts the
Classifiertrait contract against a classifier whose transport is already scripted to answerclassifier_conformance_questionsaboutclassifier_conformance_state. The suite makes exactly oneaskcall — script that one request and no other. - classifier_
conformance_ questions - The canonical question set the conformance suite asks: one
Question::Choice, oneQuestion::Score, oneQuestion::Noul, so every shape of the port is exercised once. The ids are fixed —topic,severity,urgent— and an adapter test scripts its transport to answer exactly these before running the suite. - classifier_
conformance_ state - The state the conformance suite asks about: short and fixed, so a scripted transport can match it.
- classifier_
not_ configured - Asserts an adapter with no key, binding or model behind it answers
ClassifierError::NotConfiguredrather than panicking — the unwired port is an error the caller matches, so a module can degrade on it. - classifier_
rejects_ malformed_ questions - Asserts the contract that needs no transport: a malformed question set
— empty, blank id, blank instructions, a one-criterion choice, a
one-level score — comes back as
ClassifierError::Rejectedand never panics. Safe to run against an unconfigured adapter: the set is refused before any provider is reached. - classifier_
truncates_ long_ state - Asserts that asking with a
statelonger thanprofile().max_state_charsstill succeeds — truncation is the adapter’s job, so an over-limit state must never surface as an error to the caller. - conformance
- Runs the shared conformance suite against one module, once per
dialect available in the environment (issue #20 — SQLite always,
Postgres when
FZ_TEST_POSTGRES_URLnames a server): - conformance_
in_ process_ only conformancewithout the sidecar parity axis (issue #64).reasonis recorded in code and printed by the run, so a module that opts out says why in the same place it opts out. Use it only for a module that genuinely cannot be sidecar-mounted; “it fails” is not a reason.- full_
fake_ ports - A
Portsbundle with everyPortwired to a fake, for checks that need a full bundle: conformance’s visibility check (declared ports must survivePorts::view_for, undeclared ones must be hidden), and a runtime proving it carries every port through a copy of its bundle.dispatcherandtenantsare not ports and stayNone. - push_
recipient_ conformance - Runs a
Pushadapter against everyRecipientvariant and asserts the port’s contract for transports it does not serve (issue #177): a cleanPushError::Rejectednaming the recipient as unsupported — never a panic, never a silent success, and never aTransientthat an outbox would retry forever. - request
- Sends a request through the router without a network.
json(whenSome) becomes a JSON body withcontent-type: application/json. - request_
as - The same, carrying
Authorization: Bearer <token>. - shared_
sidecar - Wraps a
FakeSidecarso the same handle can be given toPortsand still be asked how many calls it saw. - sidecar_
parity - Asserts that a module answers identically in-process and behind a sidecar mount (issue #64, ADR 0009: a caller cannot tell which).