Skip to main content

sova_testing/
app.rs

1//! Build an [`App`] with sqlite + migrator + plugins.
2
3use crate::sqlite::{apply_migrations, SqliteTestDb};
4use sova_core::{App, Plugin};
5use sova_db::Db;
6use sea_orm_migration::MigratorTrait;
7use std::future::Future;
8use std::pin::Pin;
9
10type InstallFn = Box<dyn FnOnce(&mut App) + Send>;
11type DbInstallFn = Box<dyn FnOnce(&mut App, String) + Send>;
12type MigrateFn = Box<dyn FnOnce(&str) -> Pin<Box<dyn Future<Output = ()> + Send>> + Send>;
13
14/// Fluent bootstrap: migrate sqlite, install `Db` + plugins, `run_startup`.
15pub struct TestApp;
16
17impl TestApp {
18    pub fn builder() -> TestAppBuilder {
19        TestAppBuilder {
20            migrate: None,
21            install_db: None,
22            plugins: Vec::new(),
23            env: Vec::new(),
24        }
25    }
26}
27
28pub struct TestAppBuilder {
29    migrate: Option<MigrateFn>,
30    install_db: Option<DbInstallFn>,
31    plugins: Vec<InstallFn>,
32    env: Vec<(String, String)>,
33}
34
35impl TestAppBuilder {
36    /// Apply `M` to a new sqlite file and install `Db` with an explicit URL
37    /// (avoids `DATABASE_URL` races across parallel tests).
38    pub fn migrator<M: MigratorTrait + Send + Sync + 'static>(mut self) -> Self {
39        self.migrate = Some(Box::new(|url| {
40            let url = url.to_string();
41            Box::pin(async move {
42                apply_migrations::<M>(&url).await;
43            })
44        }));
45        self.install_db = Some(Box::new(|app, url| {
46            app.install(Db::from_env().url(url).migrations::<M>());
47        }));
48        self
49    }
50
51    /// Extra env vars (e.g. `FORTIFY_SECRET`) set before plugin install.
52    pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
53        self.env.push((key.into(), value.into()));
54        self
55    }
56
57    /// Install a plugin after `Db`.
58    pub fn install<P: Plugin + Send + 'static>(mut self, plugin: P) -> Self {
59        self.plugins.push(Box::new(move |app| {
60            app.install(plugin);
61        }));
62        self
63    }
64
65    /// Mutate the [`App`] after plugins (routes, extra middleware) before startup.
66    pub fn configure<F>(mut self, f: F) -> Self
67    where
68        F: FnOnce(&mut App) + Send + 'static,
69    {
70        self.plugins.push(Box::new(f));
71        self
72    }
73
74    /// Create sqlite, migrate, install plugins, [`App::run_startup`].
75    pub async fn build(self) -> (SqliteTestDb, App) {
76        let db = SqliteTestDb::create();
77        let url = db.url().to_string();
78        if let Some(migrate) = self.migrate {
79            migrate(&url).await;
80        }
81        for (k, v) in &self.env {
82            std::env::set_var(k, v);
83        }
84        // Keep DATABASE_URL aligned with this test's file for any late readers.
85        std::env::set_var("DATABASE_URL", &url);
86        let mut app = App::new();
87        if let Some(install_db) = self.install_db {
88            install_db(&mut app, url);
89        }
90        for install in self.plugins {
91            install(&mut app);
92        }
93        app.run_startup().await.expect("run_startup");
94        (db, app)
95    }
96}