1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
use sqlx::{migrate::Migrator, Connection, Executor, PgConnection, PgPool};
use std::{path::Path, thread};
use tokio::runtime::Runtime;
use uuid::Uuid;
pub struct TestDb {
pub host: String,
pub port: u16,
pub user: String,
pub password: String,
pub dbname: String,
}
impl TestDb {
pub fn new(
host: impl Into<String>,
port: u16,
user: impl Into<String>,
password: impl Into<String>,
migration_path: impl Into<String>,
) -> Self {
let host = host.into();
let user = user.into();
let password = password.into();
let uuid = Uuid::new_v4();
let dbname = format!("test_{}", uuid);
let dbname_cloned = dbname.clone();
let tdb = Self {
host,
port,
user,
password,
dbname,
};
let server_url = tdb.server_url();
let url = tdb.url();
let migration_path = migration_path.into();
thread::spawn(move || {
let rt = Runtime::new().unwrap();
rt.block_on(async move {
let mut conn = PgConnection::connect(&server_url).await.unwrap();
conn.execute(format!(r#"CREATE DATABASE "{}""#, dbname_cloned).as_str())
.await
.unwrap();
let mut conn = PgConnection::connect(&url).await.unwrap();
let m = Migrator::new(Path::new(&migration_path)).await.unwrap();
m.run(&mut conn).await.unwrap();
});
})
.join()
.expect("failed to create database");
tdb
}
pub fn server_url(&self) -> String {
if self.password.is_empty() {
format!("postgres://{}@{}:{}", self.user, self.host, self.port)
} else {
format!(
"postgres://{}:{}@{}:{}",
self.user, self.password, self.host, self.port
)
}
}
pub fn url(&self) -> String {
format!("{}/{}", self.server_url(), self.dbname)
}
pub async fn get_pool(&self) -> PgPool {
sqlx::postgres::PgPoolOptions::new()
.max_connections(5)
.connect(&self.url())
.await
.unwrap()
}
}
impl Drop for TestDb {
fn drop(&mut self) {
let server_url = self.server_url();
let dbname = self.dbname.clone();
thread::spawn(move || {
let rt = Runtime::new().unwrap();
rt.block_on(async move {
let mut conn = PgConnection::connect(&server_url).await.unwrap();
sqlx::query(&format!(r#"SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE pid <> pg_backend_pid() AND datname = '{}'"#, dbname))
.execute(&mut conn)
.await
.expect("Terminate all other connections");
conn.execute(format!(r#"DROP DATABASE "{}""#, dbname).as_str())
.await
.expect("Error while querying the drop database");
});
})
.join()
.expect("failed to drop database");
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_db_should_create_and_drop() {
let tdb = TestDb::new("localhost", 5432, "postgres", "postgres", "./migrations");
let pool = tdb.get_pool().await;
sqlx::query("INSERT INTO todos (title) VALUES ('test')")
.execute(&pool)
.await
.unwrap();
let (id, title) = sqlx::query_as::<_, (i32, String)>("SELECT id, title FROM todos")
.fetch_one(&pool)
.await
.unwrap();
assert_eq!(id, 1);
assert_eq!(title, "test");
}
}