Skip to main content

Crate cratefield_testing

Crate cratefield_testing 

Source
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 — Every module passes it.

cratefield-testing on crates.io cratefield-testing documentation MIT

§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’s postgres feature and a server at FZ_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 (records Messages; SendOk/NotConfigured/Fail modes, switchable mid-test), FakeCaptcha (allow-all or token list), FakeRateLimiter (scripted Decisions + call count), FixedClock, MemoryKeyValue, FakeHttpClient (scripted responses + captured requests), EmptyDatabase (services SELECT 1 only), FakeDefer (collects deferred futures; drain().await runs them), and a Signer with the fixed dummy TEST_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_for hides undeclared ports, the two-concurrent-requests request-id test (ADR 0007), and — for modules with a well_known router — that it serves at the root /.well-known and never under /v1 (#46).
  • full_fake_ports() — a Ports bundle with every Port wired to a fake: the one conformance’s visibility check uses.
  • assert_wasm_safe_deps(env!("CARGO_PKG_NAME")) — cargo tree check: no worker/wasm-bindgen/tokio/reqwest in 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::ask a FakeClassifier answered, 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.
EmptyDatabase
FakeAuth
A cratefield_core::Auth answering from a mode rather than from a key set.
FakeCaptcha
FakeClassifier
An in-memory Classifier for module tests: records every ask it answered and answers according to its ClassifierMode.
FakeDefer
FakeDispatcher
An in-process Dispatcher that answers from an axum Router, 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).
FakeHttpClient
FakeMailer
FakePayments
An in-memory cratefield_core::Payments for module tests: records which calls were made and answers per its PaymentsMode. verify_webhook treats a signature header of "invalid" as a tampered event.
FakePush
An in-memory cratefield_core::Push for module tests: records every (recipient, notification) and answers according to its PushMode.
FakeRateLimiter
FakeRealtime
An in-memory cratefield_core::Realtime for 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 and RoomHandler are the runtime adapter’s job, covered by the native adapter’s own tests.
FakeSidecar
A dispatcher backed by an in-process router.
FakeTextModel
An in-memory TextModel for module tests: records every Prompt it completed and answers according to its TextModelMode.
FakeTracker
An in-memory Tracker for module tests: records every accepted file/status call and answers according to its TrackerMode. A call the mode answered with an error is not recorded, as FakePush does not record a failed send.
FiledCall
One Tracker::file a FakeTracker accepted, for assertions.
FixedClock
MemoryBlob
An in-memory cratefield_core::Blob store for module tests: keeps objects in a map, and has no presigned URLs (so signed_url reports Unsupported, as a directory store does).
MemoryKeyValue
StatusedCall
One Tracker::status a FakeTracker accepted, for assertions.
TempDir
A directory under the system temp directory, removed when this value is dropped — including while a panic unwinds.
TestHarness
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).
TestResponse
A fully-buffered test response.

Enums§

AuthMode
What a FakeAuth answers.
ClassifierMode
How a FakeClassifier answers, mirroring TextModelMode for 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.
MailerMode
How a FakeMailer answers.
PaymentsCall
What a FakePayments recorded, for assertions.
PaymentsMode
How a FakePayments responds.
PushMode
How a FakePush responds, mirroring MailerMode for the push port.
TextModelMode
How a FakeTextModel answers, mirroring PushMode for the text model port (issue #429).
TrackerMode
How a FakeTracker answers, mirroring MailerMode and PushMode for the tracker port (issue #431).

Constants§

TEST_HARNESS_SECRET

Functions§

assert_batch_is_atomic
Proves db’s batch_atomic is 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 db round-trips bound bytes exactly: a Vec<u8> bound as Value::Bytes reads 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 Classifier trait contract against a classifier whose transport is already scripted to answer classifier_conformance_questions about classifier_conformance_state. The suite makes exactly one ask call — script that one request and no other.
classifier_conformance_questions
The canonical question set the conformance suite asks: one Question::Choice, one Question::Score, one Question::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::NotConfigured rather 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::Rejected and 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 state longer than profile().max_state_chars still 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_URL names a server):
conformance_in_process_only
conformance without the sidecar parity axis (issue #64). reason is 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 Ports bundle with every Port wired to a fake, for checks that need a full bundle: conformance’s visibility check (declared ports must survive Ports::view_for, undeclared ones must be hidden), and a runtime proving it carries every port through a copy of its bundle. dispatcher and tenants are not ports and stay None.
push_recipient_conformance
Runs a Push adapter against every Recipient variant and asserts the port’s contract for transports it does not serve (issue #177): a clean PushError::Rejected naming the recipient as unsupported — never a panic, never a silent success, and never a Transient that an outbox would retry forever.
request
Sends a request through the router without a network. json (when Some) becomes a JSON body with content-type: application/json.
request_as
The same, carrying Authorization: Bearer <token>.
shared_sidecar
Wraps a FakeSidecar so the same handle can be given to Ports and 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).