Contract-suite scaffold: alkstore-contract-suite crate + ADR-022 (suite layout decision), engine dev-dep edges (ADR-017 §4.2 discharged, ADR-012 §2 mirror)
This commit is contained in:
1 parent
eefee9ec1d
commit
621415cc47
15 files changed
+1028
-7
No files matched your search
@@ -0,0 +1,276 @@
|
||||
//! The suite's property rows — one named, independently-runnable
|
||||
//! property per backlog row, version-stamped per the
|
||||
//! [convention](crate::version_stamp).
|
||||
//!
|
||||
//! Assertion posture: rows panic with a label naming the entry point
|
||||
//! and the broken expectation — a failing row is a test failure, which
|
||||
//! is the suite's purpose as a test-side artifact (relaxing the
|
||||
//! family-wide no-panic rule here only).
|
||||
|
||||
use serde_json::json;
|
||||
|
||||
use alkstore::{BoxedFuture, Error, RESERVED_PREFIX, Result, validate_shared_name};
|
||||
|
||||
use crate::factory::StoreFactory;
|
||||
|
||||
fn expect_invalid<T>(label: &str, out: alkstore::Result<T>) {
|
||||
match out {
|
||||
Err(Error::InvalidName { .. }) => {}
|
||||
Err(other) => {
|
||||
panic!("{label}: expected `InvalidName`, got {other:?} — entry-point validation broke")
|
||||
}
|
||||
Ok(_) => panic!("{label}: expected `InvalidName` rejection, got Ok"),
|
||||
}
|
||||
}
|
||||
|
||||
fn expect_reserved<T>(label: &str, out: alkstore::Result<T>) {
|
||||
match out {
|
||||
Err(Error::ReservedName { .. }) => {}
|
||||
Err(other) => {
|
||||
panic!("{label}: expected `ReservedName`, got {other:?} — entry-point validation broke")
|
||||
}
|
||||
Ok(_) => panic!("{label}: expected `ReservedName` rejection, got Ok"),
|
||||
}
|
||||
}
|
||||
|
||||
fn expect_ok<T>(label: &str, out: alkstore::Result<T>) {
|
||||
if let Err(other) = out {
|
||||
panic!("{label}: expected success (no validation error), got {other:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// **Exemplar row** — name validation at every entry point,
|
||||
/// auto-commit and tx paths alike.
|
||||
///
|
||||
/// Every name-bearing entry point on the [`Store`](alkstore::Store)
|
||||
/// surface — and every name-bearing `*_tx` twin on the
|
||||
/// [`TxHandle`](alkstore::TxHandle) — rejects, before any engine round
|
||||
/// trip: the empty (or whitespace-only) name with
|
||||
/// [`Error::InvalidName`], and the reserved-prefix name
|
||||
/// (`__alkstore_...`) with [`Error::ReservedName`], on the
|
||||
/// shared-namespace kinds — channels, streams, queues, outboxes,
|
||||
/// locks, schedule names, and `schedule()`'s queue argument.
|
||||
/// Consumer-local identifiers take the non-empty rule only: `owner`
|
||||
/// reserved-prefixed is legal, `owner` empty is not. The tx path is
|
||||
/// exercised through `begin_tx` and `with_tx` (the provided wrapper):
|
||||
/// validation must not be bypassable by the commit-atomic path.
|
||||
///
|
||||
/// Contract stamp: ADR-008 §4 (reserved namespace + entry-point
|
||||
/// validation); ADR-021 §3 (the schedule queue argument); ADR-015 §2
|
||||
/// (the keyed publish's non-empty-key rule).
|
||||
///
|
||||
/// Engine-independent, pure contract: runnable against any factory.
|
||||
/// The happy-path spot-checks prove the rejections are the
|
||||
/// validation's doing, not a store-wide failure — valid names
|
||||
/// construct handles without a validation error, and a
|
||||
/// reserved-prefixed *local* identifier is accepted.
|
||||
pub async fn name_validation_rejects_empty_and_reserved(factory: &dyn StoreFactory) {
|
||||
let store = factory.open().await.expect("factory opens a store");
|
||||
|
||||
// Auto-commit paths — empty names.
|
||||
expect_invalid("notify(empty channel)", store.notify("", json!(null)).await);
|
||||
expect_invalid("listen(empty channel)", store.listen("").await);
|
||||
expect_invalid("stream(empty name)", store.stream("").await);
|
||||
expect_invalid(
|
||||
"queue(empty name)",
|
||||
store.queue("", alkstore::QueueOpts::default()).await,
|
||||
);
|
||||
expect_invalid("outbox(empty name)", store.outbox("").await);
|
||||
expect_invalid(
|
||||
"try_lock(empty name)",
|
||||
store.try_lock("", "worker", 60).await,
|
||||
);
|
||||
expect_invalid(
|
||||
"try_lock(empty owner)",
|
||||
store.try_lock("lock", "", 60).await,
|
||||
);
|
||||
expect_invalid(
|
||||
"schedule(empty name)",
|
||||
store
|
||||
.schedule("", "@every 1s", "work", json!(null), Default::default())
|
||||
.await,
|
||||
);
|
||||
expect_invalid(
|
||||
"schedule(empty queue argument)",
|
||||
store
|
||||
.schedule("tick", "@every 1s", "", json!(null), Default::default())
|
||||
.await,
|
||||
);
|
||||
|
||||
// Auto-commit paths — reserved-prefix names.
|
||||
expect_reserved(
|
||||
"notify(reserved channel)",
|
||||
store.notify(RESERVED_PREFIX, json!(null)).await,
|
||||
);
|
||||
expect_reserved(
|
||||
"listen(reserved channel)",
|
||||
store.listen(RESERVED_PREFIX).await,
|
||||
);
|
||||
expect_reserved("stream(reserved name)", store.stream(RESERVED_PREFIX).await);
|
||||
expect_reserved(
|
||||
"queue(reserved name)",
|
||||
store
|
||||
.queue(RESERVED_PREFIX, alkstore::QueueOpts::default())
|
||||
.await,
|
||||
);
|
||||
expect_reserved("outbox(reserved name)", store.outbox(RESERVED_PREFIX).await);
|
||||
expect_reserved(
|
||||
"try_lock(reserved name)",
|
||||
store.try_lock(RESERVED_PREFIX, "worker", 60).await,
|
||||
);
|
||||
expect_reserved(
|
||||
"schedule(reserved name)",
|
||||
store
|
||||
.schedule(
|
||||
RESERVED_PREFIX,
|
||||
"@every 1s",
|
||||
"work",
|
||||
json!(null),
|
||||
Default::default(),
|
||||
)
|
||||
.await,
|
||||
);
|
||||
expect_reserved(
|
||||
"schedule(reserved queue argument)",
|
||||
store
|
||||
.schedule(
|
||||
"tick",
|
||||
"@every 1s",
|
||||
RESERVED_PREFIX,
|
||||
json!(null),
|
||||
Default::default(),
|
||||
)
|
||||
.await,
|
||||
);
|
||||
|
||||
// Happy-path spot-checks: valid names pass validation; a
|
||||
// reserved-prefixed owner (consumer-local identifier) is legal.
|
||||
expect_ok("stream(valid name)", store.stream("work").await);
|
||||
expect_ok(
|
||||
"queue(valid name)",
|
||||
store.queue("work", alkstore::QueueOpts::default()).await,
|
||||
);
|
||||
expect_ok(
|
||||
"try_lock(reserved-prefixed owner is legal)",
|
||||
store.try_lock("lock", RESERVED_PREFIX, 60).await,
|
||||
);
|
||||
|
||||
// Tx paths — the twins validate identically (begin_tx route).
|
||||
let mut tx = store.begin_tx().await.expect("begin_tx opens");
|
||||
expect_invalid(
|
||||
"enqueue_tx(empty queue)",
|
||||
tx.enqueue_tx("", Default::default(), json!(null)).await,
|
||||
);
|
||||
expect_reserved(
|
||||
"enqueue_tx(reserved queue)",
|
||||
tx.enqueue_tx(RESERVED_PREFIX, Default::default(), json!(null))
|
||||
.await,
|
||||
);
|
||||
expect_invalid(
|
||||
"publish_tx(empty stream)",
|
||||
tx.publish_tx("", json!(null)).await,
|
||||
);
|
||||
expect_reserved(
|
||||
"publish_tx(reserved stream)",
|
||||
tx.publish_tx(RESERVED_PREFIX, json!(null)).await,
|
||||
);
|
||||
expect_invalid(
|
||||
"notify_tx(empty channel)",
|
||||
tx.notify_tx("", json!(null)).await,
|
||||
);
|
||||
expect_reserved(
|
||||
"notify_tx(reserved channel)",
|
||||
tx.notify_tx(RESERVED_PREFIX, json!(null)).await,
|
||||
);
|
||||
expect_invalid(
|
||||
"publish_with_key_tx(empty stream)",
|
||||
tx.publish_with_key_tx("", None, json!(null)).await,
|
||||
);
|
||||
expect_reserved(
|
||||
"publish_with_key_tx(reserved stream)",
|
||||
tx.publish_with_key_tx(RESERVED_PREFIX, None, json!(null))
|
||||
.await,
|
||||
);
|
||||
expect_invalid(
|
||||
"publish_with_key_tx(empty key)",
|
||||
tx.publish_with_key_tx("work", Some(String::new()), json!(null))
|
||||
.await,
|
||||
);
|
||||
expect_invalid(
|
||||
"publish_with_key_tx(whitespace key)",
|
||||
tx.publish_with_key_tx("work", Some(" ".to_string()), json!(null))
|
||||
.await,
|
||||
);
|
||||
expect_invalid(
|
||||
"save_offset_tx(empty stream)",
|
||||
tx.save_offset_tx("", "consumer", 0).await,
|
||||
);
|
||||
expect_reserved(
|
||||
"save_offset_tx(reserved stream)",
|
||||
tx.save_offset_tx(RESERVED_PREFIX, "consumer", 0).await,
|
||||
);
|
||||
expect_invalid(
|
||||
"save_offset_tx(empty consumer)",
|
||||
tx.save_offset_tx("work", "", 0).await,
|
||||
);
|
||||
expect_invalid("get_job_tx(empty queue)", tx.get_job_tx("", 1).await);
|
||||
expect_reserved(
|
||||
"get_job_tx(reserved queue)",
|
||||
tx.get_job_tx(RESERVED_PREFIX, 1).await,
|
||||
);
|
||||
expect_invalid(
|
||||
"get_offset_tx(empty stream)",
|
||||
tx.get_offset_tx("", "consumer").await,
|
||||
);
|
||||
expect_reserved(
|
||||
"get_offset_tx(reserved stream)",
|
||||
tx.get_offset_tx(RESERVED_PREFIX, "consumer").await,
|
||||
);
|
||||
expect_invalid(
|
||||
"read_since_tx(empty stream)",
|
||||
tx.read_since_tx("", 0, 10).await,
|
||||
);
|
||||
expect_reserved(
|
||||
"read_since_tx(reserved stream)",
|
||||
tx.read_since_tx(RESERVED_PREFIX, 0, 10).await,
|
||||
);
|
||||
expect_invalid(
|
||||
"read_from_consumer_tx(empty stream)",
|
||||
tx.read_from_consumer_tx("", "consumer", 10).await,
|
||||
);
|
||||
expect_reserved(
|
||||
"read_from_consumer_tx(reserved stream)",
|
||||
tx.read_from_consumer_tx(RESERVED_PREFIX, "consumer", 10)
|
||||
.await,
|
||||
);
|
||||
expect_invalid(
|
||||
"read_from_consumer_tx(empty consumer)",
|
||||
tx.read_from_consumer_tx("work", "", 10).await,
|
||||
);
|
||||
expect_invalid(
|
||||
"outbox_enqueue_tx(empty outbox)",
|
||||
tx.outbox_enqueue_tx("", Default::default(), json!(null))
|
||||
.await,
|
||||
);
|
||||
expect_reserved(
|
||||
"outbox_enqueue_tx(reserved outbox)",
|
||||
tx.outbox_enqueue_tx(RESERVED_PREFIX, Default::default(), json!(null))
|
||||
.await,
|
||||
);
|
||||
|
||||
// Tx path via `with_tx` (the provided wrapper): a validation error
|
||||
// inside the closure propagates as the wrapper's `Err` (and the
|
||||
// disposition rolls back) — the commit-atomic path cannot bypass
|
||||
// validation either.
|
||||
let result = store
|
||||
.with_tx(Box::new(|tx| {
|
||||
Box::pin(async move {
|
||||
validate_shared_name("")?;
|
||||
tx.notify_tx("work", json!(null)).await
|
||||
}) as BoxedFuture<'_, Result<()>>
|
||||
}))
|
||||
.await;
|
||||
expect_invalid("with_tx(inner empty-name rejection)", result);
|
||||
|
||||
factory.teardown().await.expect("factory teardown");
|
||||
}
|
||||
Reference in new issue
Block a user