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, FakeClassifier, FakeDefer, FakeHttpClient, FakeMailer, FakeRateLimiter,
16 FakeTextModel, FakeTracker, FixedClock, MemoryKeyValue,
17};
18
19struct TestRuntime;
20
21impl Runtime for TestRuntime {
22 fn provides(&self) -> Vec<Port> {
23 Port::ALL.to_vec()
24 }
25}
26
27pub struct TestHarness {
33 pub router: axum::Router,
35 pub harness: Harness,
36 pub mailer: FakeMailer,
37 pub captcha: FakeCaptcha,
38 pub rate_limiter: FakeRateLimiter,
39 pub text_model: FakeTextModel,
40 pub classifier: FakeClassifier,
41 pub clock: FixedClock,
42 pub kv: MemoryKeyValue,
43 pub http: FakeHttpClient,
44 pub defer: FakeDefer,
45 pub tracker: FakeTracker,
46 pub signer: Arc<HmacSigner>,
47 pub db: Arc<dyn Database>,
52 pub modules: Vec<Arc<dyn Module>>,
54 pub dialect: &'static str,
56 #[cfg(feature = "postgres")]
57 pg: Option<crate::pg::PgFixture>,
58}
59
60impl Drop for TestHarness {
61 fn drop(&mut self) {
62 #[cfg(feature = "postgres")]
67 if let Some(pg) = self.pg.take() {
68 self.router = axum::Router::new();
69 self.db = Arc::new(crate::fakes::EmptyDatabase);
70 pg.shutdown();
71 }
72 }
73}
74
75impl TestHarness {
76 #[must_use]
85 pub fn new(modules: Vec<Box<dyn Module>>) -> Self {
86 Self::with_ports(modules, |_| {})
87 }
88
89 #[must_use]
98 pub fn with_ports(modules: Vec<Box<dyn Module>>, patch: impl FnOnce(&mut Ports)) -> Self {
99 Self::with_database_and_ports(modules, Dialect::Sqlite, patch)
100 }
101
102 #[must_use]
116 pub fn with_database(modules: Vec<Box<dyn Module>>, dialect: Dialect) -> Self {
117 Self::with_database_and_ports(modules, dialect, |_| {})
118 }
119
120 #[must_use]
127 pub fn with_database_and_ports(
128 modules: Vec<Box<dyn Module>>,
129 dialect: Dialect,
130 patch: impl FnOnce(&mut Ports),
131 ) -> Self {
132 let shared: Vec<Arc<dyn Module>> = modules.into_iter().map(Arc::from).collect();
133 Self::from_arcs(shared, dialect, patch)
134 }
135
136 #[must_use]
149 pub fn all_dialects(make_modules: impl Fn() -> Vec<Box<dyn Module>>) -> Vec<Self> {
150 Dialect::available()
151 .into_iter()
152 .map(|dialect| Self::with_database(make_modules(), dialect))
153 .collect()
154 }
155
156 #[must_use]
164 pub fn all_dialects_with_ports(
165 make_modules: impl Fn() -> Vec<Box<dyn Module>>,
166 patch: impl Fn(&mut Ports) + Clone,
167 ) -> Vec<Self> {
168 Dialect::available()
169 .into_iter()
170 .map(|dialect| {
171 let patch = patch.clone();
172 Self::with_database_and_ports(make_modules(), dialect, move |ports| patch(ports))
173 })
174 .collect()
175 }
176
177 pub(crate) fn from_arcs(
178 shared: Vec<Arc<dyn Module>>,
179 dialect: Dialect,
180 patch: impl FnOnce(&mut Ports),
181 ) -> Self {
182 Self::from_arcs_with_builder(shared, dialect, |builder| builder, patch)
183 }
184
185 #[must_use]
193 pub fn with_builder(
194 modules: Vec<Box<dyn Module>>,
195 configure: impl FnOnce(HarnessBuilder) -> HarnessBuilder,
196 patch: impl FnOnce(&mut Ports),
197 ) -> Self {
198 let shared: Vec<Arc<dyn Module>> = modules.into_iter().map(Arc::from).collect();
199 Self::from_arcs_with_builder(shared, Dialect::Sqlite, configure, patch)
200 }
201
202 fn from_arcs_with_builder(
203 shared: Vec<Arc<dyn Module>>,
204 dialect: Dialect,
205 configure: impl FnOnce(HarnessBuilder) -> HarnessBuilder,
206 patch: impl FnOnce(&mut Ports),
207 ) -> Self {
208 let mut builder = Harness::builder().venture(
209 Venture::new("test-venture", "test.example").cors_origins(["https://test.example"]),
210 );
211 for module in &shared {
212 builder = builder.module_arc(Arc::clone(module));
213 }
214 let harness = configure(builder)
215 .runtime(TestRuntime)
216 .build()
217 .expect("test harness builds");
218
219 let dialect_name = dialect.name();
220 #[cfg(feature = "postgres")]
221 let (db, pg) = backing(dialect, &shared);
222 #[cfg(not(feature = "postgres"))]
223 let (db, _no_postgres_feature) = backing(dialect, &shared);
224
225 let mailer = FakeMailer::new(crate::fakes::MailerMode::SendOk);
226 let captcha = FakeCaptcha::allow_all();
227 let rate_limiter = FakeRateLimiter::always_allow();
228 let text_model = FakeTextModel::default();
229 let classifier = FakeClassifier::default();
230 let clock = FixedClock(
231 time::OffsetDateTime::from_unix_timestamp(1_800_000_000).expect("fixed epoch"),
232 );
233 let kv = MemoryKeyValue::new();
234 let http = FakeHttpClient::ok_json("{}");
235 let defer = FakeDefer::new();
236 let tracker = FakeTracker::new(crate::fakes::TrackerMode::FileOk);
237 let signer = Arc::new(
238 HmacSigner::new(crate::TEST_HARNESS_SECRET, None).expect("test secret is long enough"),
239 );
240
241 let mut ports = Ports::with_config(Arc::new(MapConfig::default()));
242 ports.db = Some(db.clone());
243 ports.mailer = Some(Arc::new(mailer.clone()));
244 ports.captcha = Some(Arc::new(captcha.clone()));
245 ports.rate_limiter = Some(Arc::new(rate_limiter.clone()));
246 ports.text_model = Some(Arc::new(text_model.clone()));
247 ports.classifier = Some(Arc::new(classifier.clone()));
248 ports.signer = Some(signer.clone());
249 ports.kv = Some(Arc::new(kv.clone()));
250 ports.http = Some(Arc::new(http.clone()));
251 ports.clock = Some(Arc::new(clock.clone()));
252 ports.id_gen = Some(Arc::new(UlidIdGen));
253 ports.defer = Some(Arc::new(defer.clone()));
254 ports.tracker = Some(Arc::new(tracker.clone()));
255 patch(&mut ports);
256
257 let router = harness.router(ports);
258 Self {
259 router,
260 harness,
261 mailer,
262 captcha,
263 rate_limiter,
264 text_model,
265 classifier,
266 clock,
267 kv,
268 http,
269 defer,
270 tracker,
271 signer,
272 db,
273 modules: shared,
274 dialect: dialect_name,
275 #[cfg(feature = "postgres")]
276 pg,
277 }
278 }
279}
280
281fn sqlite_backing(modules: &[Arc<dyn Module>]) -> Arc<dyn Database> {
284 let db = Arc::new(SqliteDatabase::in_memory().expect("in-memory sqlite"));
285 for module in modules {
286 db.apply_migrations(module.name(), module.migrations().sqlite)
287 .unwrap_or_else(|err| panic!("migration for {}: {err}", module.name()));
288 }
289 db
290}
291
292#[cfg(feature = "postgres")]
295fn backing(
296 dialect: Dialect,
297 modules: &[Arc<dyn Module>],
298) -> (Arc<dyn Database>, Option<crate::pg::PgFixture>) {
299 match dialect {
300 Dialect::Sqlite => (sqlite_backing(modules), None),
301 Dialect::Postgres { url } => {
302 let fixture = crate::pg::PgFixture::create(&url, modules)
303 .unwrap_or_else(|message| panic!("postgres parity kit: {message}"));
304 let db = fixture.database();
305 (db, Some(fixture))
306 }
307 }
308}
309
310#[cfg(not(feature = "postgres"))]
318#[allow(clippy::needless_pass_by_value)]
319fn backing(
320 dialect: Dialect,
321 modules: &[Arc<dyn Module>],
322) -> (Arc<dyn Database>, Option<std::convert::Infallible>) {
323 match dialect {
324 Dialect::Sqlite => (sqlite_backing(modules), None),
325 Dialect::Postgres { .. } => panic!(
326 "cratefield-testing was built without the `postgres` feature — the Postgres \
327 parity leg needs it (dev-depend on cratefield-testing with \
328 features = [\"postgres\"])"
329 ),
330 }
331}