add rough DB stuff
This commit is contained in:
parent
b5c443506e
commit
610177efd1
12 changed files with 1425 additions and 22 deletions
1
.env
Normal file
1
.env
Normal file
|
@ -0,0 +1 @@
|
||||||
|
DATABASE_URL=postgres://ztoa:0zpVXAVK20@localhost:5432/ztoa
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1 +1,2 @@
|
||||||
/target
|
/target
|
||||||
|
*.db
|
||||||
|
|
978
Cargo.lock
generated
978
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -17,12 +17,18 @@ hyper = "1.1"
|
||||||
pin-project = "1.1.0"
|
pin-project = "1.1.0"
|
||||||
serde = { version = "1.0.164", features = ["derive"] }
|
serde = { version = "1.0.164", features = ["derive"] }
|
||||||
serde_json = "1.0.99"
|
serde_json = "1.0.99"
|
||||||
|
sqlx = { version = "0.8.5", features = ["runtime-tokio", "macros", "postgres", "uuid", "chrono", "migrate"] }
|
||||||
|
tar = "0.4.44"
|
||||||
thiserror = "2.0"
|
thiserror = "2.0"
|
||||||
tokio = { version = "1.28.2", features = ["full"] }
|
tokio = { version = "1.28.2", features = ["full"] }
|
||||||
tokio-stream = "0.1"
|
tokio-stream = "0.1"
|
||||||
|
tokio-util = "0.7.15"
|
||||||
tracing = "0.1.37"
|
tracing = "0.1.37"
|
||||||
tracing-subscriber = { version = "0.3", features =["env-filter"] }
|
tracing-subscriber = { version = "0.3", features =["env-filter"] }
|
||||||
|
uuid = { version = "1.16.0", features = ["v4"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
bollard = { git = "https://github.com/fussybeaver/bollard.git", rev = "50a25a0" }
|
||||||
|
http-body-util = "0.1.3"
|
||||||
reqwest = { version = "0.12", features = ["cookies"] }
|
reqwest = { version = "0.12", features = ["cookies"] }
|
||||||
test-log = { version = "0.2.12", default-features = false, features = ["trace"] }
|
test-log = { version = "0.2.12", default-features = false, features = ["trace"] }
|
||||||
|
|
|
@ -3,4 +3,4 @@ debug = true
|
||||||
listen = "[::]:3742"
|
listen = "[::]:3742"
|
||||||
|
|
||||||
[database]
|
[database]
|
||||||
url = "sqlite://./zero-to-axum.db"
|
url = "postgres://ztoa:0zpVXAVK20@localhost:5432/ztoa"
|
||||||
|
|
7
migrations/20250514191325_create-subscriptions-table.sql
Normal file
7
migrations/20250514191325_create-subscriptions-table.sql
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
-- Create Subscriptions Table
|
||||||
|
CREATE TABLE subscriptions(
|
||||||
|
id uuid NOT NULL PRIMARY KEY,
|
||||||
|
email TEXT NOT NULL UNIQUE,
|
||||||
|
name TEXT,
|
||||||
|
subscribed_at timestamptz NOT NULL
|
||||||
|
)
|
50
scripts/start_dev_db.sh
Executable file
50
scripts/start_dev_db.sh
Executable file
|
@ -0,0 +1,50 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
SUPERUSER_NAME="postgres"
|
||||||
|
SUPERUSER_PASS="nOMvDptXFk"
|
||||||
|
|
||||||
|
USER_NAME="ztoa"
|
||||||
|
USER_PASS="0zpVXAVK20"
|
||||||
|
|
||||||
|
APP_DB_NAME="ztoa"
|
||||||
|
|
||||||
|
CONTAINER_NAME="postgres"
|
||||||
|
|
||||||
|
DATABASE_URL="postgres://ztoa:0zpVXAVK20@localhost:5432/ztoa"
|
||||||
|
|
||||||
|
# Launch postgres
|
||||||
|
podman run \
|
||||||
|
--replace \
|
||||||
|
--env POSTGRES_USER=${SUPERUSER_NAME} \
|
||||||
|
--env POSTGRES_PASSWORD=${SUPERUSER_PASS} \
|
||||||
|
--health-cmd="pg_isready -U ${SUPERUSER_NAME} || exit 1" \
|
||||||
|
--health-interval=1s \
|
||||||
|
--health-timeout=5s \
|
||||||
|
--health-retries=5 \
|
||||||
|
--publish 5432:5432 \
|
||||||
|
--detach \
|
||||||
|
--name "${CONTAINER_NAME}" \
|
||||||
|
postgres -N 1000
|
||||||
|
|
||||||
|
# Wait for Postgres to be ready
|
||||||
|
until [ \
|
||||||
|
"$(podman inspect -f "{{.State.Health.Status}}" ${CONTAINER_NAME})" == \
|
||||||
|
"healthy" \
|
||||||
|
]; do
|
||||||
|
>&2 echo "Postgres is still unavailable - sleeping"
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
>&2 echo "Postgres is up and running"
|
||||||
|
|
||||||
|
# Create the application user
|
||||||
|
CREATE_QUERY="CREATE USER ${USER_NAME} WITH PASSWORD '${USER_PASS}';"
|
||||||
|
podman exec -it "${CONTAINER_NAME}" psql -U "${SUPERUSER_NAME}" -c "${CREATE_QUERY}"
|
||||||
|
|
||||||
|
# Grant create db privileges to the app user
|
||||||
|
GRANT_QUERY="ALTER USER ${USER_NAME} CREATEDB;"
|
||||||
|
podman exec -it "${CONTAINER_NAME}" psql -U "${SUPERUSER_NAME}" -c "${GRANT_QUERY}"
|
||||||
|
|
||||||
|
sqlx database create
|
||||||
|
|
||||||
|
sqlx migrate run
|
|
@ -3,12 +3,12 @@ use config::{Config, Environment, File};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::{env, net::SocketAddr};
|
use std::{env, net::SocketAddr};
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
pub struct Database {
|
pub struct Database {
|
||||||
pub url: String,
|
pub url: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
pub struct Conf {
|
pub struct Conf {
|
||||||
pub debug: bool,
|
pub debug: bool,
|
||||||
|
|
|
@ -16,7 +16,7 @@ async fn main() -> Result<()> {
|
||||||
init_tracing();
|
init_tracing();
|
||||||
|
|
||||||
let conf = Conf::read()?;
|
let conf = Conf::read()?;
|
||||||
let server = ZeroToAxum::serve(conf).await;
|
let server = ZeroToAxum::serve(conf).await?;
|
||||||
|
|
||||||
server.await.context("run server")
|
server.await.context("run server")
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,10 +4,13 @@ use anyhow::Result;
|
||||||
use axum::extract::FromRef;
|
use axum::extract::FromRef;
|
||||||
use axum_extra::extract::cookie::Key;
|
use axum_extra::extract::cookie::Key;
|
||||||
use pin_project::pin_project;
|
use pin_project::pin_project;
|
||||||
|
use sqlx::postgres::PgPoolOptions;
|
||||||
|
use sqlx::PgPool;
|
||||||
use std::future::{Future, IntoFuture};
|
use std::future::{Future, IntoFuture};
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
use std::pin::pin;
|
use std::pin::pin;
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
|
use std::sync::Arc;
|
||||||
use tokio::signal;
|
use tokio::signal;
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
|
|
||||||
|
@ -36,13 +39,20 @@ impl ZeroToAxum {
|
||||||
self.bound_addr
|
self.bound_addr
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn serve(conf: Conf) -> ZeroToAxum {
|
pub async fn serve(conf: Conf) -> Result<ZeroToAxum> {
|
||||||
let state = AppState {
|
let db = PgPoolOptions::new()
|
||||||
|
.max_connections(5)
|
||||||
|
.connect(&conf.database.url)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let app_state = AppState {
|
||||||
|
conf: Arc::new(conf.clone()),
|
||||||
// TODO: pull from config
|
// TODO: pull from config
|
||||||
key: Key::generate(),
|
key: Key::generate(),
|
||||||
|
db,
|
||||||
};
|
};
|
||||||
|
|
||||||
let app = routes::build().with_state(state);
|
let app = routes::build().with_state(app_state);
|
||||||
|
|
||||||
let listener = tokio::net::TcpListener::bind(&conf.listen).await.unwrap();
|
let listener = tokio::net::TcpListener::bind(&conf.listen).await.unwrap();
|
||||||
let bound_addr = listener.local_addr().unwrap();
|
let bound_addr = listener.local_addr().unwrap();
|
||||||
|
@ -50,17 +60,20 @@ impl ZeroToAxum {
|
||||||
|
|
||||||
info!("server started, listening on {bound_addr:?}");
|
info!("server started, listening on {bound_addr:?}");
|
||||||
|
|
||||||
ZeroToAxum {
|
Ok(ZeroToAxum {
|
||||||
server: Box::pin(server.into_future()),
|
server: Box::pin(server.into_future()),
|
||||||
bound_addr,
|
bound_addr,
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
|
#[allow(unused)]
|
||||||
|
conf: Arc<Conf>,
|
||||||
// The key used to encrypt cookies.
|
// The key used to encrypt cookies.
|
||||||
key: Key,
|
key: Key,
|
||||||
|
db: PgPool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromRef<AppState> for Key {
|
impl FromRef<AppState> for Key {
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
use axum::{http::StatusCode, response::IntoResponse, routing::post, Form, Router};
|
use anyhow::Context;
|
||||||
|
use axum::{extract::State, http::StatusCode, response::IntoResponse, routing::post, Form, Router};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
use sqlx::types::chrono::Utc;
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::server::AppState;
|
use crate::server::AppState;
|
||||||
|
|
||||||
|
@ -14,24 +17,46 @@ pub struct SubscribeForm {
|
||||||
email: String,
|
email: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn subscribe(Form(form): Form<SubscribeForm>) -> Result<(), SubscribeError> {
|
pub async fn subscribe(
|
||||||
|
State(AppState { db, .. }): State<AppState>,
|
||||||
|
Form(form): Form<SubscribeForm>,
|
||||||
|
) -> Result<(), SubscribeError> {
|
||||||
info!(form.name, form.email, "subscribe attempt");
|
info!(form.name, form.email, "subscribe attempt");
|
||||||
|
|
||||||
if form.email.is_empty() {
|
if form.email.is_empty() {
|
||||||
return Err(SubscribeError::InvalidEmail);
|
return Err(SubscribeError::InvalidEmail);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sqlx::query!(
|
||||||
|
r#"
|
||||||
|
INSERT INTO subscriptions (id, email, name, subscribed_at)
|
||||||
|
VALUES ($1, $2, $3, $4)
|
||||||
|
"#,
|
||||||
|
Uuid::new_v4(),
|
||||||
|
form.email,
|
||||||
|
form.name,
|
||||||
|
Utc::now()
|
||||||
|
)
|
||||||
|
.execute(&db)
|
||||||
|
.await
|
||||||
|
.context("insert subscription into database")?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(thiserror::Error, Debug)]
|
||||||
pub enum SubscribeError {
|
pub enum SubscribeError {
|
||||||
|
#[error("Invalid Email Address")]
|
||||||
InvalidEmail,
|
InvalidEmail,
|
||||||
|
#[error("Unknown Error: {0}")]
|
||||||
|
Unknown(#[from] anyhow::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoResponse for SubscribeError {
|
impl IntoResponse for SubscribeError {
|
||||||
fn into_response(self) -> axum::response::Response {
|
fn into_response(self) -> axum::response::Response {
|
||||||
match self {
|
match self {
|
||||||
SubscribeError::InvalidEmail => (StatusCode::BAD_REQUEST, "Invalid Email Address"),
|
SubscribeError::InvalidEmail => (StatusCode::BAD_REQUEST, "Invalid Email Address"),
|
||||||
|
SubscribeError::Unknown(_e) => (StatusCode::INTERNAL_SERVER_ERROR, "Unknown Error"),
|
||||||
}
|
}
|
||||||
.into_response()
|
.into_response()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,33 +1,62 @@
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use futures_util::FutureExt;
|
use bollard::query_parameters::CreateImageOptionsBuilder;
|
||||||
|
use bollard::secret::{
|
||||||
|
ContainerCreateBody, ContainerInspectResponse, ContainerState, CreateImageInfo, Health,
|
||||||
|
HealthConfig, HealthStatusEnum, HostConfig, PortBinding,
|
||||||
|
};
|
||||||
|
use bollard::Docker;
|
||||||
|
use futures_util::{FutureExt, StreamExt as _};
|
||||||
|
use sqlx::migrate::MigrateDatabase;
|
||||||
|
use sqlx::{Connection, PgConnection};
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::time::Duration;
|
||||||
|
use tokio::sync::OnceCell;
|
||||||
use tokio::task::JoinHandle;
|
use tokio::task::JoinHandle;
|
||||||
use tracing::info;
|
use tokio::time::sleep;
|
||||||
|
use tracing::{debug, trace};
|
||||||
use zero_to_axum::{Conf, ZeroToAxum};
|
use zero_to_axum::{Conf, ZeroToAxum};
|
||||||
|
|
||||||
|
static SHARED_DB: OnceCell<Arc<TestDb>> = OnceCell::const_new();
|
||||||
|
async fn get_shared_db() -> Arc<TestDb> {
|
||||||
|
SHARED_DB
|
||||||
|
.get_or_init(|| async { Arc::new(TestDb::spawn().await) })
|
||||||
|
.await
|
||||||
|
.clone()
|
||||||
|
}
|
||||||
|
|
||||||
pub struct TestServer {
|
pub struct TestServer {
|
||||||
server_task_handle: JoinHandle<()>,
|
server_task_handle: JoinHandle<()>,
|
||||||
addr: SocketAddr,
|
addr: SocketAddr,
|
||||||
|
db: Arc<TestDb>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TestServer {
|
impl TestServer {
|
||||||
pub async fn spawn() -> TestServer {
|
pub async fn spawn() -> TestServer {
|
||||||
info!("start server");
|
debug!("start test server");
|
||||||
|
|
||||||
|
// TODO: allow per-test DBs in some cases
|
||||||
|
// let db = TestDb::spawn().await;
|
||||||
|
// TODO: share test server between test file, somehow?
|
||||||
|
let db = get_shared_db().await;
|
||||||
|
let url = dbg!(db.get_url());
|
||||||
|
|
||||||
let server = ZeroToAxum::serve(Conf {
|
let server = ZeroToAxum::serve(Conf {
|
||||||
listen: "[::]:0".parse().unwrap(),
|
listen: "[::]:0".parse().unwrap(),
|
||||||
database: zero_to_axum::conf::Database {
|
database: zero_to_axum::conf::Database { url },
|
||||||
url: "memory:".into(),
|
|
||||||
},
|
|
||||||
debug: true,
|
debug: true,
|
||||||
})
|
})
|
||||||
.await;
|
.await
|
||||||
|
.unwrap();
|
||||||
let addr = server.local_addr();
|
let addr = server.local_addr();
|
||||||
let server_task_handle = tokio::spawn(server.map(|res| res.unwrap()));
|
let server_task_handle = tokio::spawn(server.map(|res| res.unwrap()));
|
||||||
info!("server spawned");
|
debug!(?addr, "test server spawned");
|
||||||
|
|
||||||
TestServer {
|
TestServer {
|
||||||
server_task_handle,
|
server_task_handle,
|
||||||
addr,
|
addr,
|
||||||
|
db,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,6 +70,305 @@ impl TestServer {
|
||||||
self.server_task_handle.abort();
|
self.server_task_handle.abort();
|
||||||
let _ = self.server_task_handle.await;
|
let _ = self.server_task_handle.await;
|
||||||
|
|
||||||
|
self.db.stop().await;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const TEST_DB_IMAGE_NAME: &str = "postgres";
|
||||||
|
const TEST_DB_SUPERUSER: &str = "postgres";
|
||||||
|
const TEST_DB_SUPERUSER_PASS: &str = "password";
|
||||||
|
const TEST_DB_APP_USER: &str = "app";
|
||||||
|
const TEST_DB_APP_PASS: &str = "apppass";
|
||||||
|
const TEST_DB_APP_NAME: &str = "ztoa";
|
||||||
|
|
||||||
|
pub struct TestDb {
|
||||||
|
docker: Docker,
|
||||||
|
// image_id: String,
|
||||||
|
container: bollard::secret::ContainerInspectResponse,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TestDb {
|
||||||
|
pub async fn spawn() -> Self {
|
||||||
|
let docker = Docker::connect_with_local_defaults().expect("connect to docker daemon");
|
||||||
|
|
||||||
|
let docker = docker.negotiate_version().await.unwrap();
|
||||||
|
|
||||||
|
let version = docker.version().await.unwrap();
|
||||||
|
|
||||||
|
trace!("version: {version:?}");
|
||||||
|
|
||||||
|
let mut image_id = None;
|
||||||
|
|
||||||
|
// check for image
|
||||||
|
if let Ok(image) = docker.inspect_image(TEST_DB_IMAGE_NAME).await {
|
||||||
|
image_id = Some(image.id.unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
// build docker image from docker file
|
||||||
|
// let mut image_id = None;
|
||||||
|
// {
|
||||||
|
// let filename = "Dockerfile.db";
|
||||||
|
// let image_options = bollard::query_parameters::BuildImageOptionsBuilder::default()
|
||||||
|
// .dockerfile(filename)
|
||||||
|
// .rm(true)
|
||||||
|
// .build();
|
||||||
|
|
||||||
|
// let archive_bytes = {
|
||||||
|
// let mut archive = tar::Builder::new(Vec::new());
|
||||||
|
// archive.append_path(filename).unwrap();
|
||||||
|
// archive.into_inner().unwrap()
|
||||||
|
// };
|
||||||
|
|
||||||
|
// let mut image_build_stream = docker.build_image(
|
||||||
|
// image_options,
|
||||||
|
// None,
|
||||||
|
// Some(http_body_util::Either::Left(http_body_util::Full::new(
|
||||||
|
// archive_bytes.into(),
|
||||||
|
// ))),
|
||||||
|
// );
|
||||||
|
|
||||||
|
// while let Some(msg) = image_build_stream.next().await {
|
||||||
|
// info!("Message: {msg:?}");
|
||||||
|
|
||||||
|
// if let Ok(BuildInfo {
|
||||||
|
// aux: Some(ImageId { id: Some(id) }),
|
||||||
|
// ..
|
||||||
|
// }) = msg
|
||||||
|
// {
|
||||||
|
// trace!("Image ID: {id}");
|
||||||
|
// image_id = Some(id);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// let image_id = image_id.expect("get image id for built docker image");
|
||||||
|
|
||||||
|
// pull image
|
||||||
|
if image_id.is_none() {
|
||||||
|
let image_opts = CreateImageOptionsBuilder::new()
|
||||||
|
.from_image(TEST_DB_IMAGE_NAME)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
trace!(?image_opts, "pull image");
|
||||||
|
let mut image_create_stream = docker.create_image(Some(image_opts), None, None);
|
||||||
|
|
||||||
|
while let Some(msg) = image_create_stream.next().await {
|
||||||
|
trace!("Message: {msg:?}");
|
||||||
|
|
||||||
|
if let Ok(CreateImageInfo { id: Some(id), .. }) = msg {
|
||||||
|
trace!("Image ID: {id}");
|
||||||
|
image_id = Some(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let image_id = image_id.expect("get image id for built docker image");
|
||||||
|
|
||||||
|
// create and start docker container
|
||||||
|
let container_id;
|
||||||
|
{
|
||||||
|
let container_config = ContainerCreateBody {
|
||||||
|
image: Some(image_id.clone()),
|
||||||
|
exposed_ports: Some([("5432/tcp".to_string(), [].into())].into()),
|
||||||
|
host_config: Some(HostConfig {
|
||||||
|
port_bindings: Some(
|
||||||
|
[(
|
||||||
|
"5432/tcp".to_string(),
|
||||||
|
Some(vec![PortBinding {
|
||||||
|
host_ip: Some("127.0.0.1".to_string()),
|
||||||
|
host_port: None, // auto-assign
|
||||||
|
}]),
|
||||||
|
)]
|
||||||
|
.into(),
|
||||||
|
),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
env: Some(vec![
|
||||||
|
format!("POSTGRES_USER={TEST_DB_SUPERUSER}"),
|
||||||
|
format!("POSTGRES_PASSWORD={TEST_DB_SUPERUSER_PASS}"),
|
||||||
|
]),
|
||||||
|
healthcheck: Some(HealthConfig {
|
||||||
|
test: Some(vec!["pg_isready -U postgres || exit 1".to_string()]),
|
||||||
|
// nano seconds
|
||||||
|
interval: Some(1 * 1000 * 1000 * 1000),
|
||||||
|
timeout: Some(5 * 1000 * 1000 * 1000),
|
||||||
|
retries: Some(5 * 1000 * 1000 * 1000),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
trace!("create container");
|
||||||
|
bollard::secret::ContainerCreateResponse {
|
||||||
|
id: container_id,
|
||||||
|
..
|
||||||
|
} = docker
|
||||||
|
.create_container(
|
||||||
|
None::<bollard::query_parameters::CreateContainerOptions>,
|
||||||
|
container_config,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
trace!("start container");
|
||||||
|
docker
|
||||||
|
.start_container(
|
||||||
|
&container_id,
|
||||||
|
None::<bollard::query_parameters::StartContainerOptions>,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// wait for container to be started
|
||||||
|
let container = loop {
|
||||||
|
trace!("inspect container");
|
||||||
|
let container = docker
|
||||||
|
.inspect_container(
|
||||||
|
&container_id,
|
||||||
|
None::<bollard::query_parameters::InspectContainerOptions>,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
if let ContainerInspectResponse {
|
||||||
|
state:
|
||||||
|
Some(ContainerState {
|
||||||
|
health:
|
||||||
|
Some(Health {
|
||||||
|
status: Some(status),
|
||||||
|
..
|
||||||
|
}),
|
||||||
|
..
|
||||||
|
}),
|
||||||
|
..
|
||||||
|
} = &container
|
||||||
|
{
|
||||||
|
trace!("status: {status:?}");
|
||||||
|
if *status == HealthStatusEnum::HEALTHY {
|
||||||
|
break container;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sleep(Duration::from_secs(2)).await;
|
||||||
|
};
|
||||||
|
|
||||||
|
let db = TestDb {
|
||||||
|
docker,
|
||||||
|
// image_id,
|
||||||
|
container,
|
||||||
|
};
|
||||||
|
|
||||||
|
// setup app db
|
||||||
|
{
|
||||||
|
let mut conn = PgConnection::connect(&db.get_superuser_url())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// create application user
|
||||||
|
// Note: In general, string formtting a query is bad practice, but it's required here.
|
||||||
|
sqlx::query(&format!(
|
||||||
|
"CREATE USER {TEST_DB_APP_USER} WITH PASSWORD '{TEST_DB_APP_PASS}';"
|
||||||
|
))
|
||||||
|
.execute(&mut conn)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// grant privs to app user
|
||||||
|
// Note: In general, string formtting a query is bad practice, but it's required here.
|
||||||
|
sqlx::query(&format!("ALTER USER {TEST_DB_APP_USER} CREATEDB;"))
|
||||||
|
.execute(&mut conn)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// create test db
|
||||||
|
sqlx::Postgres::create_database(&dbg!(db.get_url()))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mut conn = PgConnection::connect(&db.get_url()).await.unwrap();
|
||||||
|
|
||||||
|
// run migrations on test db
|
||||||
|
let m = sqlx::migrate::Migrator::new(Path::new("./migrations"))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
m.run(&mut conn).await.unwrap();
|
||||||
|
|
||||||
|
db
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the authenticated URL for accessing the test DB from the host.
|
||||||
|
pub fn get_url(&self) -> String {
|
||||||
|
let binding = self
|
||||||
|
.container
|
||||||
|
.network_settings
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.ports
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.get("5432/tcp")
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.first()
|
||||||
|
.unwrap();
|
||||||
|
let host_ip = binding.host_ip.as_ref().unwrap().clone();
|
||||||
|
let host_port = binding.host_port.as_ref().unwrap().clone();
|
||||||
|
format!("postgres://{TEST_DB_APP_USER}:{TEST_DB_APP_PASS}@{host_ip}:{host_port}/{TEST_DB_APP_NAME}")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the superuser-authenticated URL for accessing the `postgres` db from the host.
|
||||||
|
fn get_superuser_url(&self) -> String {
|
||||||
|
let binding = self
|
||||||
|
.container
|
||||||
|
.network_settings
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.ports
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.get("5432/tcp")
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.first()
|
||||||
|
.unwrap();
|
||||||
|
let host_ip = binding.host_ip.as_ref().unwrap().clone();
|
||||||
|
let host_port = binding.host_port.as_ref().unwrap().clone();
|
||||||
|
format!("postgres://{TEST_DB_SUPERUSER}:{TEST_DB_SUPERUSER_PASS}@{host_ip}:{host_port}/postgres")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn stop(&self) {
|
||||||
|
self.docker
|
||||||
|
.stop_container(
|
||||||
|
&self.container.id.as_deref().unwrap(),
|
||||||
|
#[allow(deprecated)] // is deprecated, but also required
|
||||||
|
None::<bollard::container::StopContainerOptions>,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
self.docker
|
||||||
|
.remove_container(
|
||||||
|
&self.container.id.as_deref().unwrap(),
|
||||||
|
None::<bollard::query_parameters::RemoveContainerOptions>,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// // TODO: images seem like they might be reused, this is probably a bad idea, but it seems to work?
|
||||||
|
// let _ = self
|
||||||
|
// .docker
|
||||||
|
// .remove_image(
|
||||||
|
// &self.image_id,
|
||||||
|
// None::<bollard::query_parameters::RemoveImageOptions>,
|
||||||
|
// None,
|
||||||
|
// )
|
||||||
|
// .await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue