by_loco/testing/
db.rs

1use crate::{
2    app::{AppContext, Hooks},
3    db, hash, Error, Result,
4};
5use sqlx::{Pool, Postgres};
6use std::future::Future;
7use std::path::PathBuf;
8use std::pin::Pin;
9use tree_fs::TreeBuilder;
10
11/// Seeds data into the database.
12///
13///
14/// # Errors
15/// When seed fails
16///
17/// # Example
18///
19/// The provided example demonstrates how to boot the test case and run seed
20/// data.
21///
22/// ```rust,ignore
23/// use myapp::app::App;
24/// use loco_rs::testing::prelude::*;
25/// use migration::Migrator;
26///
27/// #[tokio::test]
28/// async fn test_create_user() {
29///     let boot = boot_test::<App, Migrator>().await;
30///     seed::<App>(&boot.app_context).await.unwrap();
31///
32///     /// .....
33///     assert!(false)
34/// }
35/// ```
36pub async fn seed<H: Hooks>(ctx: &AppContext) -> Result<()> {
37    let path = std::path::Path::new("src/fixtures");
38    H::seed(ctx, path).await
39}
40
41/// Initializes a test database connection.
42///
43/// # Errors
44/// Returns an error if could not create a new test db.
45pub fn init_test_db_creation(conn_str: &str) -> Result<Box<dyn TestSupport>> {
46    if conn_str.starts_with("postgres://") {
47        PostgresTest::new(conn_str).map(|test| Box::new(test) as Box<dyn TestSupport>)
48    } else if conn_str.starts_with("sqlite://") {
49        SqliteTest::new(conn_str).map(|test| Box::new(test) as Box<dyn TestSupport>)
50    } else {
51        Ok(Box::new(Any::new(conn_str)))
52    }
53}
54
55pub trait TestSupport: Send + Sync {
56    /// Initializes the database.
57    fn init_db<'a>(&'a self) -> Pin<Box<dyn Future<Output = ()> + Send + 'a>>;
58    /// Returns the connection string.
59    fn get_connection_str(&self) -> &str;
60    /// Cleans up the database.
61    fn cleanup_db(&self);
62}
63
64pub struct PostgresTest {
65    root_connection_string: String,
66    connection_string: String,
67    schema_name: String,
68}
69
70impl PostgresTest {
71    /// Creates a new `PostgreSQL` test database.
72    ///
73    /// # Errors
74    /// Returns an error if could not create DB schema.
75    pub fn new(conn_str: &str) -> Result<Self> {
76        let db_name = db::extract_db_name(conn_str)?;
77
78        let current_timestamp = chrono::Utc::now().timestamp();
79        let test_schema_name: String = hash::random_string(10).to_lowercase();
80        let test_schema_name = format!("_loco_test_{test_schema_name}_{current_timestamp}");
81
82        Ok(Self {
83            root_connection_string: conn_str.replace(db_name, "postgres"),
84            connection_string: conn_str.replace(db_name, &test_schema_name),
85            schema_name: test_schema_name,
86        })
87    }
88}
89
90#[async_trait::async_trait]
91impl TestSupport for PostgresTest {
92    fn get_connection_str(&self) -> &str {
93        &self.connection_string
94    }
95
96    fn init_db<'a>(&'a self) -> Pin<Box<dyn Future<Output = ()> + Send + 'a>> {
97        Box::pin(async move {
98            let pool = Pool::<Postgres>::connect(&self.root_connection_string)
99                .await
100                .expect("db connection should success");
101            let query = format!("CREATE DATABASE {};", self.schema_name);
102
103            sqlx::query(&query)
104                .execute(&pool)
105                .await
106                .expect("create DB schema");
107        })
108    }
109
110    fn cleanup_db(&self) {
111        let connection_string = self.root_connection_string.clone();
112        let table_name = self.schema_name.clone();
113
114        tokio::task::spawn_blocking(move || {
115            let rt = tokio::runtime::Runtime::new().unwrap();
116
117            rt.block_on(async {
118                let pool = Pool::<Postgres>::connect(&connection_string)
119                    .await
120                    .expect("db connection should success");
121                let query = format!("drop database if exists {table_name};");
122                sqlx::query(&query)
123                    .execute(&pool)
124                    .await
125                    .expect("Drop database");
126            });
127        });
128    }
129}
130
131pub struct SqliteTest {
132    connection_string: String,
133    db_folder: PathBuf,
134    _tree: tree_fs::Tree, // Keep the tree alive while the test runs
135}
136
137impl SqliteTest {
138    /// Prepare new `SQLite` connection string.
139    ///
140    /// # Errors
141    /// Returns an error if could not prepare the connection string
142    pub fn new(conn_str: &str) -> Result<Self> {
143        let db_name = db::extract_db_name(conn_str)?;
144
145        let tree = TreeBuilder::default()
146            .add_empty_file("test.sqlite")
147            .create()
148            .map_err(|err| {
149                Error::string(&format!(
150                    "could not create test database directory. err: {err}"
151                ))
152            })?;
153
154        Ok(Self {
155            connection_string: conn_str.replace(
156                db_name,
157                &tree.root.join("test.sqlite").display().to_string(),
158            ),
159            db_folder: tree.root.clone(),
160            _tree: tree,
161        })
162    }
163}
164
165#[async_trait::async_trait]
166impl TestSupport for SqliteTest {
167    fn get_connection_str(&self) -> &str {
168        &self.connection_string
169    }
170    fn init_db<'a>(&'a self) -> Pin<Box<dyn Future<Output = ()> + Send + 'a>> {
171        Box::pin(async move {})
172    }
173
174    fn cleanup_db(&self) {
175        std::fs::remove_dir_all(&self.db_folder).expect("Could not delete sqlite test db");
176    }
177}
178
179pub struct Any {
180    connection_string: String,
181}
182impl Any {
183    #[must_use]
184    pub fn new(conn_str: &str) -> Self {
185        Self {
186            connection_string: conn_str.to_string(),
187        }
188    }
189}
190
191impl TestSupport for Any {
192    fn get_connection_str(&self) -> &str {
193        &self.connection_string
194    }
195    fn init_db<'a>(&'a self) -> Pin<Box<dyn Future<Output = ()> + Send + 'a>> {
196        Box::pin(async move {})
197    }
198
199    fn cleanup_db(&self) {}
200}
201
202#[cfg(test)]
203mod tests {
204
205    use super::*;
206    use sqlx::Row;
207    use std::{thread, time};
208
209    async fn schema_exists(pool: &sqlx::PgPool, schema_name: &str) -> bool {
210        let row =
211            sqlx::query("SELECT EXISTS (SELECT 1 FROM pg_catalog.pg_database  WHERE datname = $1)")
212                .bind(schema_name)
213                .fetch_one(pool)
214                .await
215                .expect("check if table exists");
216
217        println!("schema_name: {row:#?}");
218        row.get(0)
219    }
220
221    #[tokio::test]
222    async fn sqlite_test_support() {
223        let conn = "sqlite://test.sqlite?mode=rwc";
224        let sqlite = SqliteTest::new(conn).expect("create Sqlite test support");
225
226        sqlite.init_db().await;
227
228        assert!(sqlite.db_folder.exists());
229        sqlite.cleanup_db();
230        assert!(!sqlite.db_folder.exists());
231    }
232
233    #[tokio::test]
234    async fn postgres_test_support() {
235        let (conn, _container) = crate::tests_cfg::postgres::setup_postgres_container().await;
236        let pg: PostgresTest = PostgresTest::new(&conn).expect("create Postgres test support");
237
238        pg.init_db().await;
239
240        let pool = Pool::<Postgres>::connect(&conn)
241            .await
242            .expect("db connection should success");
243
244        assert!(schema_exists(&pool, &pg.schema_name).await);
245
246        pg.cleanup_db();
247
248        thread::sleep(time::Duration::from_secs(1));
249        assert!(!schema_exists(&pool, &pg.schema_name).await);
250    }
251}