#![allow(dead_code)]
use std::sync::LazyLock;
use reliar_core::{ContentType, Message, Serializer};
use reliar_outbox::OutboxEnqueue;
use serde::{Deserialize, Serialize};
use sqlx::PgPool;
use sqlx::postgres::PgConnectOptions;
use testcontainers::ContainerAsync;
use testcontainers::ImageExt;
use testcontainers::runners::AsyncRunner;
use testcontainers_modules::postgres::Postgres;
use tokio::sync::OnceCell;
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub(crate) struct OrderCreated {
pub order_id: u64,
}
impl Message for OrderCreated {
const TYPE: &'static str = "orders.created";
const VERSION: u16 = 1;
}
#[derive(Clone, Debug, Default)]
pub(crate) struct TestVndSerializer;
#[derive(Debug)]
pub(crate) struct TestVndSerializerError(String);
impl std::fmt::Display for TestVndSerializerError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "test serializer error: {}", self.0)
}
}
impl std::error::Error for TestVndSerializerError {}
impl Serializer for TestVndSerializer {
type Error = TestVndSerializerError;
fn content_type(&self) -> &ContentType {
static CONTENT_TYPE: LazyLock<ContentType> =
LazyLock::new(|| ContentType::parse("application/vnd.reliar-test+json").unwrap());
&CONTENT_TYPE
}
fn serialize<T: Message>(&self, body: &T) -> Result<bytes::Bytes, Self::Error> {
serde_json::to_vec(body)
.map(bytes::Bytes::from)
.map_err(|err| TestVndSerializerError(err.to_string()))
}
fn deserialize<T: Message>(&self, bytes: &[u8]) -> Result<T, Self::Error> {
serde_json::from_slice(bytes).map_err(|err| TestVndSerializerError(err.to_string()))
}
}
#[derive(Clone, Debug, Default)]
pub(crate) struct AlwaysFailingSerializer;
#[derive(Debug)]
pub(crate) struct AlwaysFailingSerializerError;
impl std::fmt::Display for AlwaysFailingSerializerError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "this serializer always fails, by design")
}
}
impl std::error::Error for AlwaysFailingSerializerError {}
impl Serializer for AlwaysFailingSerializer {
type Error = AlwaysFailingSerializerError;
fn content_type(&self) -> &ContentType {
&ContentType::JSON
}
fn serialize<T: Message>(&self, _body: &T) -> Result<bytes::Bytes, Self::Error> {
Err(AlwaysFailingSerializerError)
}
fn deserialize<T: Message>(&self, _bytes: &[u8]) -> Result<T, Self::Error> {
Err(AlwaysFailingSerializerError)
}
}
static ADMIN_URL: OnceCell<String> = OnceCell::const_new();
pub(crate) async fn start_shared_container() -> Option<ContainerAsync<Postgres>> {
if let Ok(url) = std::env::var("DATABASE_URL") {
ADMIN_URL
.set(url)
.expect("start_shared_container must run exactly once");
return None;
}
let container = Postgres::default()
.with_tag("18-alpine")
.with_container_name(format!("reliar-pg-{}", uuid::Uuid::now_v7().simple()))
.with_label("reliar.test", "true")
.start()
.await
.expect("start postgres container");
let port = container
.get_host_port_ipv4(5432)
.await
.expect("mapped port");
let url = format!("postgres://postgres:postgres@127.0.0.1:{port}/postgres");
ADMIN_URL
.set(url)
.expect("start_shared_container must run exactly once");
Some(container)
}
fn admin_url() -> &'static str {
ADMIN_URL
.get()
.expect("start_shared_container must run before any scenario touches Postgres")
}
async fn create_fresh_database() -> PgConnectOptions {
let admin = PgPool::connect(admin_url())
.await
.expect("connect to admin database");
let name = format!("t_{}", uuid::Uuid::now_v7().simple());
sqlx::query(sqlx::AssertSqlSafe(format!(r#"CREATE DATABASE "{name}""#)))
.execute(&admin)
.await
.expect("create test database");
let options: PgConnectOptions = admin_url()
.parse()
.expect("admin url parses as PgConnectOptions");
options.database(&name)
}
pub(crate) async fn fresh_unmigrated_db() -> PgPool {
PgPool::connect_with(create_fresh_database().await)
.await
.expect("connect to fresh database")
}
static TEMPLATE_NAME: OnceCell<String> = OnceCell::const_new();
async fn template_name() -> &'static str {
TEMPLATE_NAME
.get_or_init(|| async {
let options = create_fresh_database().await;
let name = options.get_database().unwrap().to_owned();
let pool = PgPool::connect_with(options)
.await
.expect("connect to template database");
reliar_store_postgres::migrate(&pool, reliar_store_postgres::MigrateOptions::default())
.await
.expect("migrate the template database");
pool.close().await;
name
})
.await
}
pub(crate) async fn fresh_db() -> PgPool {
let admin = PgPool::connect(admin_url())
.await
.expect("connect to admin database");
let name = format!("t_{}", uuid::Uuid::now_v7().simple());
let template = template_name().await;
sqlx::query(sqlx::AssertSqlSafe(format!(
r#"CREATE DATABASE "{name}" TEMPLATE "{template}""#
)))
.execute(&admin)
.await
.expect("clone the migrated template database");
let options: PgConnectOptions = admin_url()
.parse()
.expect("admin url parses as PgConnectOptions");
PgPool::connect_with(
options
.database(&name)
.options([("search_path", "reliar,public")]),
)
.await
.expect("connect with search_path set")
}
pub(crate) async fn seed<Ser: reliar_core::Serializer + Send + Sync + 'static>(
store: &reliar_store_postgres::PostgresOutboxStore<Ser>,
pool: &PgPool,
n: u64,
) -> Vec<reliar_core::Envelope<OrderCreated>> {
let mut envelopes = Vec::with_capacity(n as usize);
for i in 0..n {
let envelope = reliar_core::Envelope::builder(OrderCreated { order_id: i }).build();
let mut tx = pool.begin().await.unwrap();
store
.enqueue_envelope(&mut tx, envelope.clone())
.await
.unwrap();
tx.commit().await.unwrap();
envelopes.push(envelope);
}
envelopes
}
pub(crate) async fn expire_lease(pool: &PgPool, id: uuid::Uuid) {
sqlx::query("UPDATE outbox SET locked_until = now() - interval '1 second' WHERE id = $1")
.bind(id)
.execute(pool)
.await
.unwrap();
}
pub(crate) async fn make_available_now(pool: &PgPool, id: uuid::Uuid) {
sqlx::query("UPDATE outbox SET available_at = now() - interval '1 second' WHERE id = $1")
.bind(id)
.execute(pool)
.await
.unwrap();
}