use std::{fmt, future::Future, time::Duration};
use bytes::Bytes;
use futures::{Stream, StreamExt};
#[cfg(feature = "asyncapi")]
use serde_json::Value;
use tokio::time::{Instant, timeout, timeout_at};
use super::helpers::unique_subject;
#[cfg(feature = "asyncapi")]
use crate::ConnectedBroker;
#[cfg(feature = "asyncapi")]
use crate::DescribeServer;
#[cfg(feature = "asyncapi")]
use crate::asyncapi::build_spec;
#[cfg(feature = "asyncapi")]
use crate::runtime::{AppInfo, RustStream};
use crate::{
AckError, Broker, Connected, HeaderMap, IncomingMessage, OutgoingFor, OutgoingMessage,
PublishPolicy, Publisher, Subscriber, SubscriptionSource, runtime::RETRY_COUNT_HEADER,
};
const DELIVERY_TIMEOUT: Duration = Duration::from_secs(10);
const STEP_TIMEOUT: Duration = DELIVERY_TIMEOUT;
const QUIET_PERIOD: Duration = Duration::from_millis(500);
const ORDERED: usize = 32;
const MANY_HEADERS: usize = 24;
const PER_KEY: usize = 8;
const KEYS: [&str; 3] = [
"conformance-key-a",
"conformance-key-b",
"conformance-key-c",
];
const TRACEPARENT: (&str, &str) = (
"traceparent",
"00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
);
const BINARY_VALUE: &[u8] = &[0x00, 0x80, b'\r', b'\n', 0xfe, 0xff, 0xc3];
type SubscriberMessage<S> = <S as Subscriber>::Message;
pub(crate) async fn publisher_carries<B, MkBroker, Src, MkSrc, Pub, MkPub>(
make_broker: MkBroker,
make_source: MkSrc,
make_publisher: MkPub,
) -> (MkBroker, MkSrc, MkPub)
where
B: Broker,
MkBroker: Fn() -> B,
Src: SubscriptionSource<Connected<B>> + Send,
Src::Subscriber: Send,
MkSrc: Fn(&str) -> Src,
Pub: Publisher,
MkPub: Fn(&Connected<B>) -> Pub,
{
let subject = unique_subject("conformance.lifecycle");
let connected = within(make_broker().connect(), "message shape: connect")
.await
.expect("message shape: broker must connect after synchronous construction");
let mut subscriber = within(
make_source(&subject).subscribe(&connected),
"message shape: subscribe",
)
.await
.expect("message shape: subscription source must open against the connected form");
let publisher = make_publisher(&connected);
let sent = publish_every_shape(&publisher, &subject).await;
{
let mut stream = std::pin::pin!(subscriber.stream());
receive_every_shape(&mut stream, &sent).await;
}
drop(subscriber);
super::lifecycle::shutdown_within(connected, "lifecycle").await;
(make_broker, make_source, make_publisher)
}
struct HeaderCase {
label: &'static str,
headers: HeaderMap,
}
fn header_cases() -> [HeaderCase; 4] {
let mut many = HeaderMap::new();
for index in 0..MANY_HEADERS {
many.insert(
format!("x-conformance-entry-{index:02}"),
format!("value-{index}"),
);
}
let mut empty = HeaderMap::new();
empty.insert("x-conformance-empty", Bytes::new());
empty.insert("x-conformance-present", "yes");
let mut binary = HeaderMap::new();
binary.insert("x-conformance-binary", Bytes::from_static(BINARY_VALUE));
let mut framework = HeaderMap::new();
framework.insert(RETRY_COUNT_HEADER, "2");
framework.insert(TRACEPARENT.0, TRACEPARENT.1);
[
HeaderCase {
label: "many entries",
headers: many,
},
HeaderCase {
label: "an empty value",
headers: empty,
},
HeaderCase {
label: "a non-UTF-8 value",
headers: binary,
},
HeaderCase {
label: "the framework's retry count and trace context",
headers: framework,
},
]
}
enum Awaited {
Plain,
Carrying(HeaderCase),
Refused(&'static str),
}
struct Sent {
payload: String,
awaited: Awaited,
}
async fn publish_every_shape<Pub: Publisher>(publisher: &Pub, subject: &str) -> Vec<Sent> {
let cases = header_cases();
let mut sent = Vec::with_capacity(cases.len() + ORDERED);
for (index, case) in cases.into_iter().enumerate() {
let payload = format!("headers:{index}");
let what = format!("header round trip: the publish carrying {}", case.label);
let answer = publish_within(
publisher,
OutgoingMessage::new(subject, payload.as_bytes()).with_headers(case.headers.clone()),
None,
&what,
)
.await;
let awaited = match answer {
Ok(()) => Awaited::Carrying(case),
Err(_) => Awaited::Refused(case.label),
};
sent.push(Sent { payload, awaited });
}
for index in 0..ORDERED {
let payload = format!("unkeyed:{index:04}");
let what = format!("unkeyed run: publish {index} through the broker's publisher");
publish_within(
publisher,
OutgoingMessage::new(subject, payload.as_bytes()),
None,
&what,
)
.await
.unwrap_or_else(|err| panic!("{what} failed: {err}"));
sent.push(Sent {
payload,
awaited: Awaited::Plain,
});
}
sent
}
async fn receive_every_shape<S, M, E>(stream: &mut S, sent: &[Sent])
where
S: Stream<Item = Result<M, E>> + Unpin,
M: IncomingMessage,
E: fmt::Debug,
{
let mut arrived = vec![false; sent.len()];
let mut deadline = Instant::now() + DELIVERY_TIMEOUT;
while let Some(missing) = sent
.iter()
.zip(&arrived)
.position(|(sent, arrived)| !arrived && !matches!(sent.awaited, Awaited::Refused(_)))
{
let label = match &sent[missing].awaited {
Awaited::Carrying(case) => format!(
"header round trip: waiting for the accepted publish carrying {}",
case.label
),
_ => format!("unkeyed run: waiting for {}", sent[missing].payload),
};
let msg = next_delivery(stream, deadline, &label)
.await
.unwrap_or_else(|| panic!("{label}: no delivery within {DELIVERY_TIMEOUT:?}"));
let index = sent
.iter()
.position(|sent| sent.payload.as_bytes() == msg.payload())
.unwrap_or_else(|| {
panic!(
"message shape: a delivery nobody published arrived: {:?}; the payload must \
arrive as it was published",
String::from_utf8_lossy(msg.payload()),
)
});
match &sent[index].awaited {
Awaited::Refused(label) => {
refused_arrived(
"header round trip",
&format!("the publish carrying {label}"),
);
}
Awaited::Carrying(case) => {
for (name, value) in case.headers.iter() {
assert_eq!(
msg.headers().get(name),
Some(value),
"header round trip ({}): header {name:?} must arrive byte for byte; a \
transport that cannot carry it must refuse the publish instead of \
dropping or rewriting it",
case.label,
);
}
}
Awaited::Plain => {}
}
if !arrived[index] {
arrived[index] = true;
deadline = Instant::now() + DELIVERY_TIMEOUT;
}
settle(msg, "message shape").await;
}
nothing_refused_arrives(stream, "header round trip", |payload| {
sent.iter()
.find(|sent| sent.payload.as_bytes() == payload)
.and_then(|sent| match sent.awaited {
Awaited::Refused(label) => Some(format!("the publish carrying {label}")),
_ => None,
})
})
.await;
}
fn refused_arrived(check: &str, what: &str) -> ! {
panic!("{check}: {what} was refused, and the message arrived anyway")
}
pub async fn keyed_order<B, MkBroker, Src, MkSrc, Pub, MkPub, KeyWith>(
make_broker: MkBroker,
subject: &str,
make_source: MkSrc,
make_publisher: MkPub,
key_with: KeyWith,
) where
B: Broker,
MkBroker: Fn() -> B,
Src: SubscriptionSource<Connected<B>> + Send,
Src::Subscriber: Send,
MkSrc: Fn(&str) -> Src,
Pub: Publisher,
MkPub: Fn(&Connected<B>) -> Pub,
KeyWith: Fn(&[u8], &mut HeaderMap) -> Option<Pub::Options>,
{
let connected = within(make_broker().connect(), "keyed order: connect")
.await
.expect("keyed order: broker must connect after synchronous construction");
let mut subscriber = within(
make_source(subject).subscribe(&connected),
"keyed order: subscribe",
)
.await
.expect("keyed order: subscription source must open against the connected form");
let publisher = make_publisher(&connected);
for seq in 0..PER_KEY {
for key in KEYS {
let payload = format!("{key}:{seq:02}");
let mut headers = HeaderMap::new();
let options = key_with(key.as_bytes(), &mut headers);
let msg = OutgoingMessage::new(subject, payload.as_bytes()).with_headers(headers);
let what = format!("keyed order: publishing {payload}");
publish_within(&publisher, msg, options.as_ref(), &what)
.await
.unwrap_or_else(|err| panic!("{what} failed: {err}"));
}
}
{
let mut next = [0usize; KEYS.len()];
let mut stream = std::pin::pin!(subscriber.stream());
for _ in 0..PER_KEY * KEYS.len() {
let msg = expect_delivery(&mut stream, "keyed order").await;
let payload = String::from_utf8_lossy(msg.payload()).into_owned();
let (slot, seq) = payload
.rsplit_once(':')
.and_then(|(key, seq)| {
Some((
KEYS.iter().position(|known| *known == key)?,
seq.parse::<usize>().ok()?,
))
})
.unwrap_or_else(|| panic!("keyed order: a delivery nobody published: {payload:?}"));
let key = KEYS[slot];
assert_eq!(
msg.partition_key(),
Some(key.as_bytes()),
"keyed order: {payload} was published under key {key:?}; the delivery must report \
that key from partition_key()",
);
assert_eq!(
seq, next[slot],
"keyed order: the messages of key {key:?} must arrive in publish order",
);
next[slot] += 1;
settle(msg, "keyed order").await;
}
}
drop(subscriber);
super::lifecycle::shutdown_within(connected, "keyed order").await;
}
#[derive(Debug, Clone)]
pub struct OptionCases<Options, Observed> {
policy_default: Observed,
overrides: Vec<(Options, Observed)>,
refused: Vec<Options>,
}
impl<Options, Observed> OptionCases<Options, Observed> {
#[must_use]
pub const fn new(policy_default: Observed) -> Self {
Self {
policy_default,
overrides: Vec::new(),
refused: Vec::new(),
}
}
#[must_use]
pub fn overrides(mut self, options: Options, observed: Observed) -> Self {
self.overrides.push((options, observed));
self
}
#[must_use]
pub fn refuses(mut self, options: Options) -> Self {
self.refused.push(options);
self
}
}
pub async fn publish_options<B, MkBroker, Src, MkSrc, Policy, Observed, Observe>(
make_broker: MkBroker,
subject: &str,
make_source: MkSrc,
policy: Policy,
cases: OptionCases<<Policy::Live as Publisher>::Options, Observed>,
observe: Observe,
) where
B: Broker,
MkBroker: Fn() -> B,
Src: SubscriptionSource<Connected<B>> + Send,
Src::Subscriber: Send,
MkSrc: Fn(&str) -> Src,
Policy: PublishPolicy<Connected<B>>,
Policy::Live: Publisher,
Observed: PartialEq + fmt::Debug + Sync,
Observe: Fn(&SubscriberMessage<Src::Subscriber>) -> Observed + Sync,
{
assert!(
!cases.overrides.is_empty(),
"publish options: pass at least one override; with none the check cannot tell a \
publisher that ignores the call's options from one that honours them",
);
let connected = within(make_broker().connect(), "publish options: connect")
.await
.expect("publish options: broker must connect after synchronous construction");
let mut subscriber = within(
make_source(subject).subscribe(&connected),
"publish options: subscribe",
)
.await
.expect("publish options: subscription source must open against the connected form");
let publisher = within(
policy.pair(&connected),
"publish options: pairing the policy",
)
.await
.expect("publish options: the policy must pair with the connected broker");
{
let mut stream = std::pin::pin!(subscriber.stream());
let (publisher, observe, delivered) = (&publisher, &observe, &mut stream);
let mut round = 0usize;
let mut expect = async move |options: Option<&<Policy::Live as Publisher>::Options>,
expected: &Observed,
what: &str| {
let payload = format!("options:{round}");
round += 1;
let what = format!("publish options: {what}");
publish_within(
publisher,
OutgoingMessage::new(subject, payload.as_bytes()),
options,
&what,
)
.await
.unwrap_or_else(|err| panic!("{what}: the publish failed: {err}"));
let msg = expect_delivery(&mut *delivered, &what).await;
assert_eq!(
String::from_utf8_lossy(msg.payload()),
payload,
"{what}: a message arrived out of order, or one that was refused",
);
let observed = observe(&msg);
assert_eq!(
&observed, expected,
"{what}: the delivery shows another setting",
);
settle(msg, "publish options").await;
};
expect(
None,
&cases.policy_default,
"a publish with no options takes the policy's",
)
.await;
for (index, (options, observed)) in cases.overrides.iter().enumerate() {
let what = format!("override {index} must win over the policy");
expect(Some(options), observed, &what).await;
let what =
format!("the publish after override {index} takes the policy's setting again");
expect(None, &cases.policy_default, &what).await;
}
for (index, options) in cases.refused.iter().enumerate() {
let payload = format!("options:refused:{index}");
let refused = publish_within(
publisher,
OutgoingMessage::new(subject, payload.as_bytes()),
Some(options),
&format!("publish options: refused value {index}"),
)
.await;
assert!(
refused.is_err(),
"publish options: refused value {index} was accepted; a value the transport cannot \
honour must fail the publish, never fall back to another setting",
);
}
expect(
None,
&cases.policy_default,
"the publish after the refused values",
)
.await;
nothing_refused_arrives(&mut stream, "publish options", |payload| {
let index = payload.strip_prefix(b"options:refused:")?;
Some(format!("refused value {}", String::from_utf8_lossy(index)))
})
.await;
}
drop(subscriber);
super::lifecycle::shutdown_within(connected, "publish options").await;
}
#[cfg(feature = "asyncapi")]
pub fn publishes_without_credentials<C, Policy>(policy: &Policy, secret: &str)
where
C: ConnectedBroker,
Policy: PublishPolicy<C>,
{
assert!(
!secret.is_empty(),
"pass the password the policy was configured with; scanning for an empty string passes \
whatever the policy does",
);
let channel = "conformance.credentials";
let described = serde_json::json!({
"channel": policy.channel_bindings(channel),
"operation": policy.operation_bindings(channel),
"message": policy.message_bindings(channel),
"reply": policy.reply_address_location(),
});
assert!(
!carries_secret(&described, secret),
"the publish policy's bindings carry the password. A published document is shared: keep \
credentials out of every binding body.",
);
}
#[cfg(feature = "asyncapi")]
pub(crate) fn carries_secret(value: &Value, secret: &str) -> bool {
match value {
Value::String(text) => text.contains(secret),
Value::Array(items) => items.iter().any(|item| carries_secret(item, secret)),
Value::Object(fields) => fields
.iter()
.any(|(key, field)| key.contains(secret) || carries_secret(field, secret)),
Value::Null | Value::Bool(_) | Value::Number(_) => false,
}
}
#[cfg(feature = "asyncapi")]
pub fn describes_addresses_without_credentials<B, MkBroker>(make_broker: MkBroker, scheme: &str)
where
B: DescribeServer,
MkBroker: FnOnce(&[&str]) -> B,
{
const USER: &str = "conformance-user";
const SECRET: &str = "conformance-secret-7f3a";
const HOSTS: [&str; 3] = [
"conformance-a.invalid:4101",
"conformance-b.invalid:4102",
"conformance-c.invalid:4103",
];
let urls = HOSTS.map(|host| format!("{scheme}://{USER}:{SECRET}@{host}"));
let addrs = urls.each_ref().map(String::as_str);
let broker = make_broker(&addrs);
let app = RustStream::new(AppInfo::new("conformance", "0.0.0"))
.server("broker", broker.describe_server());
let document = build_spec(&app)
.to_json()
.expect("the generated document must serialize");
for leaked in [SECRET, USER] {
assert!(
!document.contains(leaked),
"the server description of a broker configured with several addresses carries their \
userinfo. Describe each address through ServerSpec::host_from_url. Got: {document}",
);
}
assert!(
HOSTS.iter().any(|host| document.contains(host)),
"the server description names none of the configured addresses, so the scan for their \
credentials proves nothing. Describe each address through ServerSpec::host_from_url. \
Got: {document}",
);
}
async fn within<Work: Future>(work: Work, what: &str) -> Work::Output {
timeout(STEP_TIMEOUT, work)
.await
.unwrap_or_else(|_| panic!("{what} hung for {STEP_TIMEOUT:?}"))
}
async fn publish_within<Pub: Publisher>(
publisher: &Pub,
msg: OutgoingFor<'_, Pub::Payload>,
options: Option<&Pub::Options>,
what: &str,
) -> Result<(), Pub::Error> {
within(publisher.publish(msg, options), what).await
}
async fn expect_delivery<S, M, E>(stream: &mut S, label: &str) -> M
where
S: Stream<Item = Result<M, E>> + Unpin,
M: IncomingMessage,
E: fmt::Debug,
{
next_delivery(stream, Instant::now() + DELIVERY_TIMEOUT, label)
.await
.unwrap_or_else(|| panic!("{label}: no delivery within {DELIVERY_TIMEOUT:?}"))
}
async fn next_delivery<S, M, E>(stream: &mut S, deadline: Instant, label: &str) -> Option<M>
where
S: Stream<Item = Result<M, E>> + Unpin,
M: IncomingMessage,
E: fmt::Debug,
{
let item = timeout_at(deadline, stream.next()).await.ok()?;
Some(
item.unwrap_or_else(|| {
panic!("{label}: the subscription ended before the expected delivery")
})
.unwrap_or_else(|err| panic!("{label}: the subscription yielded an error: {err:?}")),
)
}
async fn nothing_refused_arrives<S, M, E, Refused>(stream: &mut S, check: &str, refused: Refused)
where
S: Stream<Item = Result<M, E>> + Unpin,
M: IncomingMessage,
E: fmt::Debug,
Refused: Fn(&[u8]) -> Option<String>,
{
let until = Instant::now() + QUIET_PERIOD;
while let Some(msg) = next_delivery(stream, until, check).await {
if let Some(what) = refused(msg.payload()) {
refused_arrived(check, &what);
}
settle(msg, check).await;
}
}
async fn settle<M: IncomingMessage>(msg: M, label: &str) {
match msg.ack().await {
Ok(()) | Err(AckError::Unsupported) => {}
Err(other) => panic!("{label}: ack must succeed or be unsupported, got: {other:?}"),
}
}
#[cfg(all(test, feature = "memory"))]
mod tests;