From 874aeea4c1e7d90f919b371c095192f794207fd4 Mon Sep 17 00:00:00 2001 From: mfw78 Date: Fri, 17 Jul 2026 08:59:33 +0000 Subject: [PATCH] sdk: rename the conformance kit to videre-test and align mock-grant fidelity --- Cargo.lock | 36 +++---- Cargo.toml | 2 +- crates/cow-venue/Cargo.toml | 2 +- crates/cow-venue/src/body.rs | 2 +- crates/nexum-sdk-test/src/lib.rs | 59 +++++++++++- .../Cargo.toml | 2 +- .../goldens/reference-header.json | 2 +- .../src/codec.rs | 0 .../src/fixture.rs | 0 .../src/header.rs | 0 .../src/lib.rs | 8 +- .../src/reference.rs | 6 +- .../src/report.rs | 0 .../src/transport.rs | 95 +++++++++++++++++-- .../tests/conformance.rs | 8 +- .../vectors/reference-body.json | 2 +- docs/05-sdk-design.md | 4 +- modules/examples/echo-venue/Cargo.toml | 2 +- modules/examples/echo-venue/src/lib.rs | 6 +- 19 files changed, 185 insertions(+), 51 deletions(-) rename crates/{nexum-venue-test => videre-test}/Cargo.toml (98%) rename crates/{nexum-venue-test => videre-test}/goldens/reference-header.json (96%) rename crates/{nexum-venue-test => videre-test}/src/codec.rs (100%) rename crates/{nexum-venue-test => videre-test}/src/fixture.rs (100%) rename crates/{nexum-venue-test => videre-test}/src/header.rs (100%) rename crates/{nexum-venue-test => videre-test}/src/lib.rs (93%) rename crates/{nexum-venue-test => videre-test}/src/reference.rs (97%) rename crates/{nexum-venue-test => videre-test}/src/report.rs (100%) rename crates/{nexum-venue-test => videre-test}/src/transport.rs (68%) rename crates/{nexum-venue-test => videre-test}/tests/conformance.rs (97%) rename crates/{nexum-venue-test => videre-test}/vectors/reference-body.json (97%) diff --git a/Cargo.lock b/Cargo.lock index 2a6896a6..5919a1ae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1559,11 +1559,11 @@ version = "0.1.0" dependencies = [ "borsh", "nexum-sdk", - "nexum-venue-test", "serde", "thiserror 2.0.18", "toml 1.1.2+spec-1.1.0", "videre-sdk", + "videre-test", ] [[package]] @@ -2163,8 +2163,8 @@ dependencies = [ name = "echo-venue" version = "0.1.0" dependencies = [ - "nexum-venue-test", "videre-sdk", + "videre-test", "wit-bindgen 0.59.0", ] @@ -3693,22 +3693,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "nexum-venue-test" -version = "0.1.0" -dependencies = [ - "borsh", - "hex", - "http", - "nexum-sdk", - "nexum-sdk-test", - "serde", - "serde_json", - "tempfile", - "thiserror 2.0.18", - "videre-sdk", -] - [[package]] name = "nexum-world" version = "0.1.0" @@ -6084,6 +6068,22 @@ dependencies = [ "wit-bindgen 0.59.0", ] +[[package]] +name = "videre-test" +version = "0.1.0" +dependencies = [ + "borsh", + "hex", + "http", + "nexum-sdk", + "nexum-sdk-test", + "serde", + "serde_json", + "tempfile", + "thiserror 2.0.18", + "videre-sdk", +] + [[package]] name = "wait-timeout" version = "0.2.1" diff --git a/Cargo.toml b/Cargo.toml index d8e1be24..a21dfb89 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,6 @@ members = [ "crates/nexum-sdk-test", "crates/nexum-status-body", "crates/nexum-tasks", - "crates/nexum-venue-test", "crates/nexum-world", "crates/no-std-probe", "crates/shepherd", @@ -20,6 +19,7 @@ members = [ "crates/videre-host", "crates/videre-macros", "crates/videre-sdk", + "crates/videre-test", "modules/ethflow-watcher", "modules/example", "modules/examples/balance-tracker", diff --git a/crates/cow-venue/Cargo.toml b/crates/cow-venue/Cargo.toml index 8365b586..eb061887 100644 --- a/crates/cow-venue/Cargo.toml +++ b/crates/cow-venue/Cargo.toml @@ -40,7 +40,7 @@ serde = { workspace = true } toml = { workspace = true } thiserror = { workspace = true } # The conformance kit: holds the body codec to its published vector set. -nexum-venue-test = { path = "../nexum-venue-test" } +videre-test = { path = "../videre-test" } [features] # The body-type + codec slice ships by default; the `client` slice layers diff --git a/crates/cow-venue/src/body.rs b/crates/cow-venue/src/body.rs index b346fd5e..a4ff6aa3 100644 --- a/crates/cow-venue/src/body.rs +++ b/crates/cow-venue/src/body.rs @@ -36,7 +36,7 @@ pub enum CowIntentBody { #[cfg(test)] mod tests { use super::*; - use nexum_venue_test::{CodecVectors, Expectation}; + use videre_test::{CodecVectors, Expectation}; use crate::order::{BuyTokenDestination, OrderKind, SellTokenSource}; diff --git a/crates/nexum-sdk-test/src/lib.rs b/crates/nexum-sdk-test/src/lib.rs index a520855f..ceee3ec0 100644 --- a/crates/nexum-sdk-test/src/lib.rs +++ b/crates/nexum-sdk-test/src/lib.rs @@ -387,9 +387,12 @@ impl MockMessaging { }); } - /// Confine the mock to `topics`, mirroring the component's - /// `messaging_topics` grant: any other topic fails as - /// [`Fault::Denied`]. Untouched, every topic is allowed. + /// Confine the mock to `topics`, playing the component's + /// `messaging_topics` grant with the host's matching: a topic is + /// admitted when it equals a grant entry or descends from one read + /// as a `/`-bounded path prefix; anything else fails as + /// [`Fault::Denied`]. An empty grant is unscoped, the host's module + /// default, as is an untouched mock. pub fn scope_topics(&self, topics: impl IntoIterator>) { *self.scope.borrow_mut() = Some(topics.into_iter().map(Into::into).collect()); } @@ -423,7 +426,7 @@ impl MockMessaging { } } if let Some(scope) = self.scope.borrow().as_ref() - && !scope.iter().any(|topic| topic == content_topic) + && !topic_in_scope(content_topic, scope) { return Err(Fault::Denied(format!( "MockMessaging: {content_topic} is outside the scoped topics" @@ -433,6 +436,25 @@ impl MockMessaging { } } +/// The host's `messaging_topics` matching: an empty scope admits every +/// topic; otherwise a topic is admitted when it equals a scope entry or +/// descends from one read as a path prefix bounded at `/`, so a grant +/// never leaks into a longer sibling segment. +fn topic_in_scope(topic: &str, scope: &[String]) -> bool { + if scope.is_empty() { + return true; + } + scope.iter().any(|allowed| { + if topic == allowed { + return true; + } + let prefix = allowed.strip_suffix('/').unwrap_or(allowed); + topic + .strip_prefix(prefix) + .is_some_and(|rest| rest.starts_with('/')) + }) +} + impl MessagingHost for MockMessaging { fn publish(&self, content_topic: &str, payload: &[u8]) -> Result<(), Fault> { self.admit(content_topic)?; @@ -1361,6 +1383,35 @@ mod tests { assert_eq!(messaging.publish_count(), 1); } + #[test] + fn messaging_scope_matches_the_host_grant() { + // A prefix grant admits the family beneath it, bounded at `/`. + let messaging = MockMessaging::default(); + messaging.scope_topics(["/nexum/1/"]); + messaging + .publish("/nexum/1/acme-orders/proto", b"x") + .unwrap(); + messaging.publish("/nexum/1/twap/proto", b"x").unwrap(); + let err = messaging.publish("/nexum/2/acme/proto", b"x").unwrap_err(); + assert!(matches!(err, Fault::Denied(_))); + + // No trailing slash still bounds on the separator: a grant never + // leaks into a longer sibling segment. + let messaging = MockMessaging::default(); + messaging.scope_topics(["/nexum/1/acme"]); + messaging.publish("/nexum/1/acme", b"x").unwrap(); + messaging.publish("/nexum/1/acme/orders", b"x").unwrap(); + let err = messaging + .publish("/nexum/1/acme-orders/proto", b"x") + .unwrap_err(); + assert!(matches!(err, Fault::Denied(_))); + + // An empty grant is unscoped, the host's module default. + let messaging = MockMessaging::default(); + messaging.scope_topics(Vec::::new()); + messaging.publish("/anywhere/at/all", b"x").unwrap(); + } + #[test] fn messaging_fault_injection_fires_by_prefix() { let messaging = MockMessaging::default(); diff --git a/crates/nexum-venue-test/Cargo.toml b/crates/videre-test/Cargo.toml similarity index 98% rename from crates/nexum-venue-test/Cargo.toml rename to crates/videre-test/Cargo.toml index c4fd08dc..02198e5a 100644 --- a/crates/nexum-venue-test/Cargo.toml +++ b/crates/videre-test/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "nexum-venue-test" +name = "videre-test" version = "0.1.0" edition.workspace = true license.workspace = true diff --git a/crates/nexum-venue-test/goldens/reference-header.json b/crates/videre-test/goldens/reference-header.json similarity index 96% rename from crates/nexum-venue-test/goldens/reference-header.json rename to crates/videre-test/goldens/reference-header.json index a90bc3c4..c56703fd 100644 --- a/crates/nexum-venue-test/goldens/reference-header.json +++ b/crates/videre-test/goldens/reference-header.json @@ -1,6 +1,6 @@ { "version": 1, - "venue": "nexum-venue-test/reference", + "venue": "videre-test/reference", "goldens": [ { "name": "v1-small", diff --git a/crates/nexum-venue-test/src/codec.rs b/crates/videre-test/src/codec.rs similarity index 100% rename from crates/nexum-venue-test/src/codec.rs rename to crates/videre-test/src/codec.rs diff --git a/crates/nexum-venue-test/src/fixture.rs b/crates/videre-test/src/fixture.rs similarity index 100% rename from crates/nexum-venue-test/src/fixture.rs rename to crates/videre-test/src/fixture.rs diff --git a/crates/nexum-venue-test/src/header.rs b/crates/videre-test/src/header.rs similarity index 100% rename from crates/nexum-venue-test/src/header.rs rename to crates/videre-test/src/header.rs diff --git a/crates/nexum-venue-test/src/lib.rs b/crates/videre-test/src/lib.rs similarity index 93% rename from crates/nexum-venue-test/src/lib.rs rename to crates/videre-test/src/lib.rs index 9eae0b8e..2485b000 100644 --- a/crates/nexum-venue-test/src/lib.rs +++ b/crates/videre-test/src/lib.rs @@ -1,4 +1,4 @@ -//! # nexum-venue-test +//! # videre-test //! //! Conformance kit for venue adapters: file-published codec vectors, //! header-derivation goldens, and an in-memory transport mock, so an @@ -25,16 +25,16 @@ //! //! ```toml //! [dev-dependencies] -//! nexum-venue-test = { path = "../../crates/nexum-venue-test" } +//! videre-test = { path = "../../crates/videre-test" } //! ``` //! //! Hold the adapter to its published fixtures: //! //! ```rust -//! use nexum_venue_test::reference::{ +//! use videre_test::reference::{ //! CODEC_VECTORS_JSON, HEADER_GOLDENS_JSON, ReferenceBody, derive_reference_header, //! }; -//! use nexum_venue_test::{CodecVectors, HeaderGoldens}; +//! use videre_test::{CodecVectors, HeaderGoldens}; //! //! // In a real adapter test these load the venue's own published //! // files; the kit's reference venue stands in here. diff --git a/crates/nexum-venue-test/src/reference.rs b/crates/videre-test/src/reference.rs similarity index 97% rename from crates/nexum-venue-test/src/reference.rs rename to crates/videre-test/src/reference.rs index 6ad7e546..020cd6f9 100644 --- a/crates/nexum-venue-test/src/reference.rs +++ b/crates/videre-test/src/reference.rs @@ -129,7 +129,7 @@ mod tests { /// Rebuild the published codec vectors from the reference schema. fn build_codec_vectors() -> CodecVectors { - let mut vectors = CodecVectors::new("nexum-venue-test/reference-body"); + let mut vectors = CodecVectors::new("videre-test/reference-body"); vectors .push_round_trip("v1-small", &v1_small()) @@ -218,7 +218,7 @@ mod tests { /// Rebuild the published header goldens from the reference /// derivation. fn build_header_goldens() -> HeaderGoldens { - let mut goldens = HeaderGoldens::new("nexum-venue-test/reference"); + let mut goldens = HeaderGoldens::new("videre-test/reference"); goldens .record( "v1-small", @@ -259,7 +259,7 @@ mod tests { } /// Rewrite the published files from the reference schema. Run with - /// `cargo test -p nexum-venue-test -- --ignored regenerate` after a + /// `cargo test -p videre-test -- --ignored regenerate` after a /// deliberate schema change, then commit the diff; the tests above /// compare against the compiled-in copy, so they go green on the /// next build. diff --git a/crates/nexum-venue-test/src/report.rs b/crates/videre-test/src/report.rs similarity index 100% rename from crates/nexum-venue-test/src/report.rs rename to crates/videre-test/src/report.rs diff --git a/crates/nexum-venue-test/src/transport.rs b/crates/videre-test/src/transport.rs similarity index 68% rename from crates/nexum-venue-test/src/transport.rs rename to crates/videre-test/src/transport.rs index 37f9e7c1..a7295c97 100644 --- a/crates/nexum-venue-test/src/transport.rs +++ b/crates/videre-test/src/transport.rs @@ -4,9 +4,12 @@ //! [`MockTransport`] composes the three behind the same seams the SDK //! wrappers implement ([`ChainHost`], [`MessagingHost`], [`Fetch`]), so //! adapter logic written against `&impl Seam` runs unchanged in unit -//! tests. Scoping mirrors the host's: [`MockMessaging::scope_topics`] -//! plays the adapter's `messaging_topics` grant and refuses off-scope -//! topics as a typed `denied`, exactly as the host would. +//! tests. Grant play mirrors the host's: [`MockMessaging::scope_topics`] +//! plays the adapter's `messaging_topics` grant with the host's +//! `/`-bounded prefix matching, and [`MockFetch::scope_hosts`] plays the +//! `[capabilities.http].allow` list with the host's exact-or-`*.suffix` +//! matching; both refuse off-grant calls as a typed `denied`, exactly as +//! the host would. use std::cell::RefCell; use std::collections::HashMap; @@ -92,16 +95,29 @@ struct StoredResponse { } /// In-memory [`Fetch`] backed by a `(method, uri)` -> response map. -/// Records every request so tests can assert dispatch shape; an -/// allowlist refusal is programmed as [`FetchError::Denied`] via -/// [`fail_with`](Self::fail_with). +/// Records every request so tests can assert dispatch shape. An +/// optional host scope plays the adapter's `[capabilities.http].allow` +/// grant ([`scope_hosts`](Self::scope_hosts)); one-off refusals can +/// still be programmed via [`fail_with`](Self::fail_with). #[derive(Default)] pub struct MockFetch { responses: RefCell>>, requests: RefCell>, + scope: RefCell>>, } impl MockFetch { + /// Confine the mock to `hosts`, playing the adapter's + /// `[capabilities.http].allow` grant with the host's matching: + /// case-insensitive, an entry is an exact hostname or a `*.suffix` + /// wildcard, and an off-grant request fails as + /// [`FetchError::Denied`]. An empty grant denies every host, the + /// host's posture for an absent allow list; an untouched mock is + /// unscoped. + pub fn scope_hosts(&self, hosts: impl IntoIterator>) { + *self.scope.borrow_mut() = Some(hosts.into_iter().map(Into::into).collect()); + } + /// Program a response for the `(method, uri)` pair. Overwrites any /// prior entry. /// @@ -164,6 +180,14 @@ impl Fetch for MockFetch { body: request.body().clone(), options, }); + if let Some(scope) = self.scope.borrow().as_ref() + && !request + .uri() + .host() + .is_some_and(|host| host_allowed(host, scope)) + { + return Err(FetchError::Denied); + } match self.responses.borrow().get(&(method.clone(), uri.clone())) { Some(Ok(stored)) => Ok(http::Response::builder() .status(stored.status) @@ -177,6 +201,21 @@ impl Fetch for MockFetch { } } +/// The host's `[capabilities.http].allow` matching: host-only and +/// case-insensitive, an entry admits its exact hostname or, as +/// `*.suffix`, any strict subdomain of the suffix. +fn host_allowed(host: &str, allowlist: &[String]) -> bool { + let host = host.to_ascii_lowercase(); + allowlist.iter().any(|pat| { + let pat = pat.to_ascii_lowercase(); + if let Some(suffix) = pat.strip_prefix("*.") { + host.ends_with(&format!(".{suffix}")) + } else { + host == pat + } + }) +} + #[cfg(test)] mod tests { use super::*; @@ -233,6 +272,50 @@ mod tests { assert_eq!(fetch.request_count(), 2); } + #[test] + fn fetch_scope_matches_the_host_grant() { + let fetch = MockFetch::default(); + fetch.scope_hosts(["api.acme.example", "*.discord.com"]); + fetch.respond_to(http::Method::GET, "https://api.acme.example/v1", 200, "ok"); + fetch.respond_to(http::Method::GET, "https://API.ACME.EXAMPLE/v1", 200, "ok"); + fetch.respond_to(http::Method::GET, "https://a.b.discord.com/", 200, "ok"); + + // Exact entry, case-insensitively; a wildcard admits strict + // subdomains only. + let get = |uri: &str| { + fetch.fetch( + http::Request::builder() + .uri(uri) + .body(Vec::new()) + .expect("test request builds"), + ) + }; + assert!(get("https://api.acme.example/v1").is_ok()); + assert!(get("https://API.ACME.EXAMPLE/v1").is_ok()); + assert!(get("https://a.b.discord.com/").is_ok()); + assert_eq!( + get("https://evil.api.acme.example/").unwrap_err(), + FetchError::Denied, + ); + assert_eq!(get("https://discord.com/").unwrap_err(), FetchError::Denied); + + // Refused requests are still recorded. + assert_eq!(fetch.request_count(), 5); + + // An empty grant denies every host, the host's posture for an + // absent allow list. + let sealed = MockFetch::default(); + sealed.scope_hosts(Vec::::new()); + sealed.respond_to(http::Method::GET, "https://anywhere.example/", 200, ""); + let denied = sealed.fetch( + http::Request::builder() + .uri("https://anywhere.example/") + .body(Vec::new()) + .expect("test request builds"), + ); + assert_eq!(denied.unwrap_err(), FetchError::Denied); + } + #[test] fn transport_dispatches_through_every_seam() { let transport = MockTransport::new(); diff --git a/crates/nexum-venue-test/tests/conformance.rs b/crates/videre-test/tests/conformance.rs similarity index 97% rename from crates/nexum-venue-test/tests/conformance.rs rename to crates/videre-test/tests/conformance.rs index 0faa7196..da0bc538 100644 --- a/crates/nexum-venue-test/tests/conformance.rs +++ b/crates/videre-test/tests/conformance.rs @@ -3,15 +3,15 @@ //! golden files, and a deliberately divergent adapter is caught by //! them. -use nexum_venue_test::reference::{ - CODEC_VECTORS_JSON, HEADER_GOLDENS_JSON, ReferenceBody, derive_reference_header, -}; -use nexum_venue_test::{CodecVectors, HeaderGoldens, MessagingHost, MockTransport}; use videre_sdk::value_flow::{Asset, AssetAmount}; use videre_sdk::{ AuthScheme, Config, Fault, IntentHeader, IntentStatus, Quotation, SubmitOutcome, VenueAdapter, VenueError, }; +use videre_test::reference::{ + CODEC_VECTORS_JSON, HEADER_GOLDENS_JSON, ReferenceBody, derive_reference_header, +}; +use videre_test::{CodecVectors, HeaderGoldens, MessagingHost, MockTransport}; /// An adapter under test: the reference venue implemented through the /// SDK trait, transport injected through the seams so the kit's mocks diff --git a/crates/nexum-venue-test/vectors/reference-body.json b/crates/videre-test/vectors/reference-body.json similarity index 97% rename from crates/nexum-venue-test/vectors/reference-body.json rename to crates/videre-test/vectors/reference-body.json index a2ffd1db..4bc46024 100644 --- a/crates/nexum-venue-test/vectors/reference-body.json +++ b/crates/videre-test/vectors/reference-body.json @@ -1,6 +1,6 @@ { "version": 1, - "schema": "nexum-venue-test/reference-body", + "schema": "videre-test/reference-body", "vectors": [ { "name": "v1-small", diff --git a/docs/05-sdk-design.md b/docs/05-sdk-design.md index 1722ca1a..eff5eed4 100755 --- a/docs/05-sdk-design.md +++ b/docs/05-sdk-design.md @@ -34,7 +34,7 @@ things from the SDK: to know the venue's wire format. This persona is planned but not yet shipped: the crate (`videre-sdk`), the per-venue crates (e.g. a `cow-venue` crate carrying CoW's intent-body codec), the - `#[nexum::venue]` macro, and the `nexum-venue-test` conformance kit + `#[nexum::venue]` macro, and the `videre-test` conformance kit are all tracked by the SDK-surfaces epic and have no code in the tree yet. See [Venue-adapter persona (planned)](#venue-adapter-persona-planned) below for the shape of the plan. @@ -276,7 +276,7 @@ The planned shape: manifest's declared capabilities (retiring the import-elision dependency for the venue side from day one, rather than as follow-on work). -- **`nexum-venue-test`** - a conformance kit: published codec +- **`videre-test`** - a conformance kit: published codec round-trip vectors (so a non-Rust adapter author can prove byte-exact `IntentBody` encoding without linking Rust), header- derivation golden fixtures, and a `MockTransport` for adapter unit diff --git a/modules/examples/echo-venue/Cargo.toml b/modules/examples/echo-venue/Cargo.toml index 8db8e22d..a1d818f1 100644 --- a/modules/examples/echo-venue/Cargo.toml +++ b/modules/examples/echo-venue/Cargo.toml @@ -19,4 +19,4 @@ wit-bindgen = { version = "0.59", default-features = false, features = ["macros" # The conformance kit: holds this adapter's header derivation to the kit's # golden mirror types, so echo-venue is both the tutorial artefact and the # kit's worked test target. -nexum-venue-test = { path = "../../../crates/nexum-venue-test" } +videre-test = { path = "../../../crates/videre-test" } diff --git a/modules/examples/echo-venue/src/lib.rs b/modules/examples/echo-venue/src/lib.rs index feb69b1a..2a3d8d50 100644 --- a/modules/examples/echo-venue/src/lib.rs +++ b/modules/examples/echo-venue/src/lib.rs @@ -6,7 +6,7 @@ //! smallest end-to-end demonstration of `#[videre_sdk::venue]` - the //! attribute takes the `impl VenueAdapter` block and supplies the //! per-cdylib wit-bindgen for a world derived from `module.toml` plus the -//! export glue - and as the `nexum-venue-test` conformance target (see the +//! export glue - and as the `videre-test` conformance target (see the //! tests below). //! //! It declares one capability (`chain`), so the built component imports @@ -107,14 +107,14 @@ fn minimal_be(value: u64) -> Vec { first.map_or(Vec::new(), |index| bytes[index..].to_vec()) } -/// echo-venue as the `nexum-venue-test` conformance target: the adapter's +/// echo-venue as the `videre-test` conformance target: the adapter's /// pure header derivation is held to a hand-written golden. The macro /// remaps the type interfaces onto the SDK bindings, so the derivation /// feeds the kit directly through its `From` mirror. #[cfg(test)] mod conformance { use super::*; - use nexum_venue_test::{ + use videre_test::{ FormatVersion, GoldenAsset, GoldenAssetAmount, GoldenAuthScheme, GoldenHeader, GoldenSettlement, HeaderGolden, HeaderGoldens, };