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
11pub 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
41pub 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 fn init_db<'a>(&'a self) -> Pin<Box<dyn Future<Output = ()> + Send + 'a>>;
58 fn get_connection_str(&self) -> &str;
60 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 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, }
136
137impl SqliteTest {
138 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}