use cratefield_adapter_sqlite::SqliteDatabase;
use cratefield_core::{
Database, Harness, HarnessBuilder, HmacSigner, MapConfig, Module, Port, Ports, Runtime,
UlidIdGen, Venture,
};
use std::sync::Arc;
use crate::dialect::Dialect;
use crate::fakes::{
FakeCaptcha, FakeDefer, FakeHttpClient, FakeMailer, FakeRateLimiter, FixedClock, MemoryKeyValue,
};
struct TestRuntime;
impl Runtime for TestRuntime {
fn provides(&self) -> Vec<Port> {
Port::ALL.to_vec()
}
}
pub struct TestHarness {
pub router: axum::Router,
pub harness: Harness,
pub mailer: FakeMailer,
pub captcha: FakeCaptcha,
pub rate_limiter: FakeRateLimiter,
pub clock: FixedClock,
pub kv: MemoryKeyValue,
pub http: FakeHttpClient,
pub defer: FakeDefer,
pub signer: Arc<HmacSigner>,
pub db: Arc<dyn Database>,
pub modules: Vec<Arc<dyn Module>>,
pub dialect: &'static str,
#[cfg(feature = "postgres")]
pg: Option<crate::pg::PgFixture>,
}
impl Drop for TestHarness {
fn drop(&mut self) {
#[cfg(feature = "postgres")]
if let Some(pg) = self.pg.take() {
self.router = axum::Router::new();
self.db = Arc::new(crate::fakes::EmptyDatabase);
pg.shutdown();
}
}
}
impl TestHarness {
#[must_use]
pub fn new(modules: Vec<Box<dyn Module>>) -> Self {
Self::with_ports(modules, |_| {})
}
#[must_use]
pub fn with_ports(modules: Vec<Box<dyn Module>>, patch: impl FnOnce(&mut Ports)) -> Self {
Self::with_database_and_ports(modules, Dialect::Sqlite, patch)
}
#[must_use]
pub fn with_database(modules: Vec<Box<dyn Module>>, dialect: Dialect) -> Self {
Self::with_database_and_ports(modules, dialect, |_| {})
}
#[must_use]
pub fn with_database_and_ports(
modules: Vec<Box<dyn Module>>,
dialect: Dialect,
patch: impl FnOnce(&mut Ports),
) -> Self {
let shared: Vec<Arc<dyn Module>> = modules.into_iter().map(Arc::from).collect();
Self::from_arcs(shared, dialect, patch)
}
#[must_use]
pub fn all_dialects(make_modules: impl Fn() -> Vec<Box<dyn Module>>) -> Vec<Self> {
Dialect::available()
.into_iter()
.map(|dialect| Self::with_database(make_modules(), dialect))
.collect()
}
#[must_use]
pub fn all_dialects_with_ports(
make_modules: impl Fn() -> Vec<Box<dyn Module>>,
patch: impl Fn(&mut Ports) + Clone,
) -> Vec<Self> {
Dialect::available()
.into_iter()
.map(|dialect| {
let patch = patch.clone();
Self::with_database_and_ports(make_modules(), dialect, move |ports| patch(ports))
})
.collect()
}
pub(crate) fn from_arcs(
shared: Vec<Arc<dyn Module>>,
dialect: Dialect,
patch: impl FnOnce(&mut Ports),
) -> Self {
Self::from_arcs_with_builder(shared, dialect, |builder| builder, patch)
}
#[must_use]
pub fn with_builder(
modules: Vec<Box<dyn Module>>,
configure: impl FnOnce(HarnessBuilder) -> HarnessBuilder,
patch: impl FnOnce(&mut Ports),
) -> Self {
let shared: Vec<Arc<dyn Module>> = modules.into_iter().map(Arc::from).collect();
Self::from_arcs_with_builder(shared, Dialect::Sqlite, configure, patch)
}
fn from_arcs_with_builder(
shared: Vec<Arc<dyn Module>>,
dialect: Dialect,
configure: impl FnOnce(HarnessBuilder) -> HarnessBuilder,
patch: impl FnOnce(&mut Ports),
) -> Self {
let mut builder = Harness::builder().venture(
Venture::new("test-venture", "test.example").cors_origins(["https://test.example"]),
);
for module in &shared {
builder = builder.module_arc(Arc::clone(module));
}
let harness = configure(builder)
.runtime(TestRuntime)
.build()
.expect("test harness builds");
let dialect_name = dialect.name();
#[cfg(feature = "postgres")]
let (db, pg) = backing(dialect, &shared);
#[cfg(not(feature = "postgres"))]
let (db, _no_postgres_feature) = backing(dialect, &shared);
let mailer = FakeMailer::new(crate::fakes::MailerMode::SendOk);
let captcha = FakeCaptcha::allow_all();
let rate_limiter = FakeRateLimiter::always_allow();
let clock = FixedClock(
time::OffsetDateTime::from_unix_timestamp(1_800_000_000).expect("fixed epoch"),
);
let kv = MemoryKeyValue::new();
let http = FakeHttpClient::ok_json("{}");
let defer = FakeDefer::new();
let signer = Arc::new(
HmacSigner::new(crate::TEST_HARNESS_SECRET, None).expect("test secret is long enough"),
);
let mut ports = Ports::with_config(Arc::new(MapConfig::default()));
ports.db = Some(db.clone());
ports.mailer = Some(Arc::new(mailer.clone()));
ports.captcha = Some(Arc::new(captcha.clone()));
ports.rate_limiter = Some(Arc::new(rate_limiter.clone()));
ports.signer = Some(signer.clone());
ports.kv = Some(Arc::new(kv.clone()));
ports.http = Some(Arc::new(http.clone()));
ports.clock = Some(Arc::new(clock.clone()));
ports.id_gen = Some(Arc::new(UlidIdGen));
ports.defer = Some(Arc::new(defer.clone()));
patch(&mut ports);
let router = harness.router(ports);
Self {
router,
harness,
mailer,
captcha,
rate_limiter,
clock,
kv,
http,
defer,
signer,
db,
modules: shared,
dialect: dialect_name,
#[cfg(feature = "postgres")]
pg,
}
}
}
fn sqlite_backing(modules: &[Arc<dyn Module>]) -> Arc<dyn Database> {
let db = Arc::new(SqliteDatabase::in_memory().expect("in-memory sqlite"));
for module in modules {
db.apply_migrations(module.name(), module.migrations().sqlite)
.unwrap_or_else(|err| panic!("migration for {}: {err}", module.name()));
}
db
}
#[cfg(feature = "postgres")]
fn backing(
dialect: Dialect,
modules: &[Arc<dyn Module>],
) -> (Arc<dyn Database>, Option<crate::pg::PgFixture>) {
match dialect {
Dialect::Sqlite => (sqlite_backing(modules), None),
Dialect::Postgres { url } => {
let fixture = crate::pg::PgFixture::create(&url, modules)
.unwrap_or_else(|message| panic!("postgres parity kit: {message}"));
let db = fixture.database();
(db, Some(fixture))
}
}
}
#[cfg(not(feature = "postgres"))]
fn backing(
dialect: Dialect,
modules: &[Arc<dyn Module>],
) -> (Arc<dyn Database>, Option<std::convert::Infallible>) {
match dialect {
Dialect::Sqlite => (sqlite_backing(modules), None),
Dialect::Postgres { .. } => panic!(
"cratefield-testing was built without the `postgres` feature — the Postgres \
parity leg needs it (dev-depend on cratefield-testing with \
features = [\"postgres\"])"
),
}
}