1use cratefield_adapter_sqlite::SqliteDatabase;
7use cratefield_core::{
8 Database, Harness, HarnessBuilder, HmacSigner, MapConfig, Module, Port, Ports, Runtime,
9 UlidIdGen, Venture,
10};
11use std::sync::Arc;
12
13use crate::dialect::Dialect;
14use crate::fakes::{
15 FakeCaptcha, FakeDefer, FakeHttpClient, FakeMailer, FakeRateLimiter, FixedClock, MemoryKeyValue,
16};
17
18struct TestRuntime;
19
20impl Runtime for TestRuntime {
21 fn provides(&self) -> Vec<Port> {
22 Port::ALL.to_vec()
23 }
24}
25
26pub struct TestHarness {
32 pub router: axum::Router,
34 pub harness: Harness,
35 pub mailer: FakeMailer,
36 pub captcha: FakeCaptcha,
37 pub rate_limiter: FakeRateLimiter,
38 pub clock: FixedClock,
39 pub kv: MemoryKeyValue,
40 pub http: FakeHttpClient,
41 pub defer: FakeDefer,
42 pub signer: Arc<HmacSigner>,
43 pub db: Arc<dyn Database>,
48 pub modules: Vec<Arc<dyn Module>>,
50 pub dialect: &'static str,
52 #[cfg(feature = "postgres")]
53 pg: Option<crate::pg::PgFixture>,
54}
55
56impl Drop for TestHarness {
57 fn drop(&mut self) {
58 #[cfg(feature = "postgres")]
63 if let Some(pg) = self.pg.take() {
64 self.router = axum::Router::new();
65 self.db = Arc::new(crate::fakes::EmptyDatabase);
66 pg.shutdown();
67 }
68 }
69}
70
71impl TestHarness {
72 #[must_use]
81 pub fn new(modules: Vec<Box<dyn Module>>) -> Self {
82 Self::with_ports(modules, |_| {})
83 }
84
85 #[must_use]
94 pub fn with_ports(modules: Vec<Box<dyn Module>>, patch: impl FnOnce(&mut Ports)) -> Self {
95 Self::with_database_and_ports(modules, Dialect::Sqlite, patch)
96 }
97
98 #[must_use]
112 pub fn with_database(modules: Vec<Box<dyn Module>>, dialect: Dialect) -> Self {
113 Self::with_database_and_ports(modules, dialect, |_| {})
114 }
115
116 #[must_use]
123 pub fn with_database_and_ports(
124 modules: Vec<Box<dyn Module>>,
125 dialect: Dialect,
126 patch: impl FnOnce(&mut Ports),
127 ) -> Self {
128 let shared: Vec<Arc<dyn Module>> = modules.into_iter().map(Arc::from).collect();
129 Self::from_arcs(shared, dialect, patch)
130 }
131
132 #[must_use]
145 pub fn all_dialects(make_modules: impl Fn() -> Vec<Box<dyn Module>>) -> Vec<Self> {
146 Dialect::available()
147 .into_iter()
148 .map(|dialect| Self::with_database(make_modules(), dialect))
149 .collect()
150 }
151
152 #[must_use]
160 pub fn all_dialects_with_ports(
161 make_modules: impl Fn() -> Vec<Box<dyn Module>>,
162 patch: impl Fn(&mut Ports) + Clone,
163 ) -> Vec<Self> {
164 Dialect::available()
165 .into_iter()
166 .map(|dialect| {
167 let patch = patch.clone();
168 Self::with_database_and_ports(make_modules(), dialect, move |ports| patch(ports))
169 })
170 .collect()
171 }
172
173 pub(crate) fn from_arcs(
174 shared: Vec<Arc<dyn Module>>,
175 dialect: Dialect,
176 patch: impl FnOnce(&mut Ports),
177 ) -> Self {
178 Self::from_arcs_with_builder(shared, dialect, |builder| builder, patch)
179 }
180
181 #[must_use]
189 pub fn with_builder(
190 modules: Vec<Box<dyn Module>>,
191 configure: impl FnOnce(HarnessBuilder) -> HarnessBuilder,
192 patch: impl FnOnce(&mut Ports),
193 ) -> Self {
194 let shared: Vec<Arc<dyn Module>> = modules.into_iter().map(Arc::from).collect();
195 Self::from_arcs_with_builder(shared, Dialect::Sqlite, configure, patch)
196 }
197
198 fn from_arcs_with_builder(
199 shared: Vec<Arc<dyn Module>>,
200 dialect: Dialect,
201 configure: impl FnOnce(HarnessBuilder) -> HarnessBuilder,
202 patch: impl FnOnce(&mut Ports),
203 ) -> Self {
204 let mut builder = Harness::builder().venture(
205 Venture::new("test-venture", "test.example").cors_origins(["https://test.example"]),
206 );
207 for module in &shared {
208 builder = builder.module_arc(Arc::clone(module));
209 }
210 let harness = configure(builder)
211 .runtime(TestRuntime)
212 .build()
213 .expect("test harness builds");
214
215 let dialect_name = dialect.name();
216 #[cfg(feature = "postgres")]
217 let (db, pg) = backing(dialect, &shared);
218 #[cfg(not(feature = "postgres"))]
219 let (db, _no_postgres_feature) = backing(dialect, &shared);
220
221 let mailer = FakeMailer::new(crate::fakes::MailerMode::SendOk);
222 let captcha = FakeCaptcha::allow_all();
223 let rate_limiter = FakeRateLimiter::always_allow();
224 let clock = FixedClock(
225 time::OffsetDateTime::from_unix_timestamp(1_800_000_000).expect("fixed epoch"),
226 );
227 let kv = MemoryKeyValue::new();
228 let http = FakeHttpClient::ok_json("{}");
229 let defer = FakeDefer::new();
230 let signer = Arc::new(
231 HmacSigner::new(crate::TEST_HARNESS_SECRET, None).expect("test secret is long enough"),
232 );
233
234 let mut ports = Ports::with_config(Arc::new(MapConfig::default()));
235 ports.db = Some(db.clone());
236 ports.mailer = Some(Arc::new(mailer.clone()));
237 ports.captcha = Some(Arc::new(captcha.clone()));
238 ports.rate_limiter = Some(Arc::new(rate_limiter.clone()));
239 ports.signer = Some(signer.clone());
240 ports.kv = Some(Arc::new(kv.clone()));
241 ports.http = Some(Arc::new(http.clone()));
242 ports.clock = Some(Arc::new(clock.clone()));
243 ports.id_gen = Some(Arc::new(UlidIdGen));
244 ports.defer = Some(Arc::new(defer.clone()));
245 patch(&mut ports);
246
247 let router = harness.router(ports);
248 Self {
249 router,
250 harness,
251 mailer,
252 captcha,
253 rate_limiter,
254 clock,
255 kv,
256 http,
257 defer,
258 signer,
259 db,
260 modules: shared,
261 dialect: dialect_name,
262 #[cfg(feature = "postgres")]
263 pg,
264 }
265 }
266}
267
268fn sqlite_backing(modules: &[Arc<dyn Module>]) -> Arc<dyn Database> {
271 let db = Arc::new(SqliteDatabase::in_memory().expect("in-memory sqlite"));
272 for module in modules {
273 db.apply_migrations(module.name(), module.migrations().sqlite)
274 .unwrap_or_else(|err| panic!("migration for {}: {err}", module.name()));
275 }
276 db
277}
278
279#[cfg(feature = "postgres")]
282fn backing(
283 dialect: Dialect,
284 modules: &[Arc<dyn Module>],
285) -> (Arc<dyn Database>, Option<crate::pg::PgFixture>) {
286 match dialect {
287 Dialect::Sqlite => (sqlite_backing(modules), None),
288 Dialect::Postgres { url } => {
289 let fixture = crate::pg::PgFixture::create(&url, modules)
290 .unwrap_or_else(|message| panic!("postgres parity kit: {message}"));
291 let db = fixture.database();
292 (db, Some(fixture))
293 }
294 }
295}
296
297#[cfg(not(feature = "postgres"))]
301fn backing(
302 dialect: Dialect,
303 modules: &[Arc<dyn Module>],
304) -> (Arc<dyn Database>, Option<std::convert::Infallible>) {
305 match dialect {
306 Dialect::Sqlite => (sqlite_backing(modules), None),
307 Dialect::Postgres { .. } => panic!(
308 "cratefield-testing was built without the `postgres` feature — the Postgres \
309 parity leg needs it (dev-depend on cratefield-testing with \
310 features = [\"postgres\"])"
311 ),
312 }
313}