use crate::common;
use crate::common::{OrderCreated, TestVndSerializer};
use proptest::prelude::*;
use reliar_core::{
ConversationId, CorrelationId, EndpointAddress, Envelope, JsonSerializer, RequestId, Serializer,
};
use reliar_outbox::{AcquireRequest, OutboxEnqueue, OutboxStore, WorkerId};
use reliar_store_postgres::PostgresOutboxStore;
use sqlx::PgPool;
use time::OffsetDateTime;
use uuid::Uuid;
const MIN_UNIX_SECS: i64 = -377_705_116_800;
const MAX_UNIX_SECS: i64 = 253_402_300_799;
#[allow(clippy::type_complexity)]
fn arb_fields() -> impl Strategy<
Value = (
(
Option<String>,
Option<u128>,
Option<u128>,
Option<String>,
Option<String>,
Option<String>,
Option<String>,
),
(
Option<String>,
Option<String>,
Vec<(String, String)>,
Option<i64>,
Option<i64>,
Option<String>,
),
),
> {
let safe_string = "[a-zA-Z0-9_-]{1,32}";
let future_floor = OffsetDateTime::now_utc().unix_timestamp() + 86_400;
(
(
proptest::option::of(safe_string),
proptest::option::of(any::<u128>()),
proptest::option::of(any::<u128>()),
proptest::option::of(safe_string),
proptest::option::of(safe_string),
proptest::option::of("[0-9a-f]{32}"),
proptest::option::of("[0-9a-f]{16}"),
),
(
proptest::option::of(safe_string),
proptest::option::of(safe_string),
proptest::collection::vec((safe_string, safe_string), 0..4),
proptest::option::of(MIN_UNIX_SECS..=MAX_UNIX_SECS),
proptest::option::of(future_floor..=MAX_UNIX_SECS),
proptest::option::of(safe_string),
),
)
}
#[allow(clippy::too_many_arguments)]
async fn run_roundtrip<Ser: Serializer + Send + Sync + 'static>(
pool: PgPool,
store: &PostgresOutboxStore<Ser>,
serializer: &Ser,
order_id: u64,
correlation_id: Option<String>,
causation: Option<u128>,
request: Option<u128>,
tenant_id: Option<String>,
source: Option<String>,
traceparent: Option<String>,
tracestate: Option<String>,
destination: Option<String>,
reply_to: Option<String>,
headers: Vec<(String, String)>,
sent_at_secs: Option<i64>,
expires_at_secs: Option<i64>,
deduplication_id: Option<String>,
) {
let mut builder = Envelope::builder(OrderCreated { order_id });
if let Some(c) = &correlation_id {
builder = builder.correlation_id(CorrelationId::parse(c.clone()).unwrap());
}
if let Some(c) = causation {
builder = builder.causation(reliar_core::MessageId::from_uuid(Uuid::from_u128(c)));
}
if let Some(tenant) = &tenant_id {
builder = builder.tenant(tenant.clone());
}
if let Some(tp) = &traceparent {
builder = builder.trace(tp.clone(), tracestate.clone());
}
builder = builder.conversation(ConversationId::from_uuid(Uuid::now_v7()));
for (k, v) in &headers {
builder = builder.header(k.clone(), v.clone()).unwrap();
}
let mut envelope = builder.build();
envelope.metadata.routing.source = source.map(|s| EndpointAddress::parse(s).unwrap());
envelope.metadata.routing.destination = destination.map(|s| EndpointAddress::parse(s).unwrap());
envelope.metadata.routing.reply_to = reply_to.map(|s| EndpointAddress::parse(s).unwrap());
if let Some(r) = request {
envelope.metadata.correlation.request_id = Some(RequestId::from_uuid(Uuid::from_u128(r)));
}
envelope.metadata.delivery.sent_at =
sent_at_secs.map(|secs| OffsetDateTime::from_unix_timestamp(secs).unwrap());
envelope.metadata.delivery.expires_at =
expires_at_secs.map(|secs| OffsetDateTime::from_unix_timestamp(secs).unwrap());
envelope.metadata.delivery.deduplication_id = deduplication_id;
let body_bytes = serializer.serialize(&envelope.body).ok().unwrap();
let mut expected = envelope.clone().map_body(|_| body_bytes);
expected.metadata.delivery.content_type = store.content_type().clone();
let mut tx = pool.begin().await.unwrap();
store.enqueue_envelope(&mut tx, envelope).await.unwrap();
tx.commit().await.unwrap();
let worker = WorkerId::generate();
let batch = store
.acquire(AcquireRequest::new(worker).batch_size(10))
.await
.unwrap();
let acquired = batch
.records
.into_iter()
.find(|r| r.envelope.id == expected.id)
.expect("the enqueued row was claimed");
assert_eq!(acquired.envelope, expected);
assert_eq!(acquired.ordering_key, None);
}
#[allow(clippy::too_many_arguments)]
fn json_roundtrip() {
let mut config = ProptestConfig::with_cases(12);
config.source_file = Some(file!());
let mut runner = proptest::test_runner::TestRunner::new(config);
let strategy = (any::<u64>(), arb_fields());
let outcome = runner.run(&strategy, |(order_id, fields)| {
let (
(correlation_id, causation, request, tenant_id, source, traceparent, tracestate),
(destination, reply_to, headers, sent_at_secs, expires_at_secs, deduplication_id),
) = fields;
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let pool = common::fresh_db().await;
let store = PostgresOutboxStore::new(pool.clone());
run_roundtrip(
pool,
&store,
&JsonSerializer,
order_id,
correlation_id,
causation,
request,
tenant_id,
source,
traceparent,
tracestate,
destination,
reply_to,
headers,
sent_at_secs,
expires_at_secs,
deduplication_id,
)
.await;
});
Ok(())
});
if let Err(e) = outcome {
panic!("{e}\n{runner}");
}
}
#[allow(clippy::too_many_arguments)]
fn non_json_content_type_roundtrip() {
let mut config = ProptestConfig::with_cases(12);
config.source_file = Some(file!());
let mut runner = proptest::test_runner::TestRunner::new(config);
let strategy = (any::<u64>(), arb_fields());
let outcome = runner.run(&strategy, |(order_id, fields)| {
let (
(correlation_id, causation, request, tenant_id, source, traceparent, tracestate),
(destination, reply_to, headers, sent_at_secs, expires_at_secs, deduplication_id),
) = fields;
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let pool = common::fresh_db().await;
let store = PostgresOutboxStore::with_serializer(
pool.clone(),
reliar_store_postgres::PostgresOutboxSettings::default(),
TestVndSerializer,
);
run_roundtrip(
pool,
&store,
&TestVndSerializer,
order_id,
correlation_id,
causation,
request,
tenant_id,
source,
traceparent,
tracestate,
destination,
reply_to,
headers,
sent_at_secs,
expires_at_secs,
deduplication_id,
)
.await;
});
Ok(())
});
if let Err(e) = outcome {
panic!("{e}\n{runner}");
}
}
async fn sent_at_round_trips_at_the_exact_min_and_max_representable_instant() {
let pool = common::fresh_db().await;
let store = PostgresOutboxStore::new(pool.clone());
for secs in [MIN_UNIX_SECS, MAX_UNIX_SECS, 0] {
let mut envelope = Envelope::builder(OrderCreated { order_id: 1 }).build();
envelope.metadata.delivery.sent_at =
Some(OffsetDateTime::from_unix_timestamp(secs).unwrap());
let expected_sent_at = envelope.metadata.delivery.sent_at;
let mut tx = pool.begin().await.unwrap();
store.enqueue_envelope(&mut tx, envelope).await.unwrap();
tx.commit().await.unwrap();
let batch = store
.acquire(AcquireRequest::new(WorkerId::generate()).batch_size(1))
.await
.unwrap();
assert_eq!(batch.records.len(), 1, "secs={secs}");
assert_eq!(
batch.records[0].envelope.metadata.delivery.sent_at, expected_sent_at,
"secs={secs}"
);
}
}
pub(crate) fn trials(rt: &'static tokio::runtime::Runtime) -> Vec<libtest_mimic::Trial> {
vec![
libtest_mimic::Trial::test("outbox_roundtrip::json_roundtrip", || {
json_roundtrip();
Ok(())
}),
libtest_mimic::Trial::test("outbox_roundtrip::non_json_content_type_roundtrip", || {
non_json_content_type_roundtrip();
Ok(())
}),
libtest_mimic::Trial::test(
"outbox_roundtrip::sent_at_round_trips_at_the_exact_min_and_max_representable_instant",
move || {
rt.block_on(sent_at_round_trips_at_the_exact_min_and_max_representable_instant());
Ok(())
},
),
]
}