From 4d3415acf2c1d57ee870f04737c1368a4ddd2598 Mon Sep 17 00:00:00 2001 From: KD Date: Thu, 3 Apr 2025 13:59:54 +0000 Subject: [PATCH] Added config file for db (#9) Reviewed-on: https://git.kundeng.us/phoenix/icarus_auth/pulls/9 Co-authored-by: KD Co-committed-by: KD --- .env.sample | 2 + .gitea/workflows/workflow.yml | 14 +++ .gitignore | 1 + Cargo.toml | 7 ++ migrations/20250402221858_init_migrate.sql | 1 + run_migrations.txt | 3 + src/callers/common.rs | 26 +++++ src/callers/mod.rs | 3 +- src/lib.rs | 30 ++++++ src/main.rs | 81 +++++++++++++-- tests/auth_tests.rs | 113 ++++++++++++++++++++- 11 files changed, 272 insertions(+), 9 deletions(-) create mode 100644 .env.sample create mode 100644 migrations/20250402221858_init_migrate.sql create mode 100644 run_migrations.txt diff --git a/.env.sample b/.env.sample new file mode 100644 index 0000000..1625b7a --- /dev/null +++ b/.env.sample @@ -0,0 +1,2 @@ +DATABASE_URL=postgres://username:password@localhost/database_name +TEST_DATABASE_URL=postgres://username:password@localhost/database_name_test diff --git a/.gitea/workflows/workflow.yml b/.gitea/workflows/workflow.yml index d216362..bedf50d 100644 --- a/.gitea/workflows/workflow.yml +++ b/.gitea/workflows/workflow.yml @@ -37,6 +37,20 @@ jobs: - uses: actions-rust-lang/setup-rust-toolchain@v1 with: toolchain: 1.85.0 + # --- Add this step for explicit verification --- + - name: Verify Docker Environment + run: | + echo "Runner User Info:" + id + echo "Checking Docker Version:" + docker --version + echo "Checking Docker Daemon Status (info):" + docker info + echo "Checking Docker Daemon Status (ps):" + docker ps -a + echo "Docker environment check complete." + # NOTE: Do NOT use continue-on-error here. + # If Docker isn't working as expected, the job SHOULD fail here. - run: | mkdir -p ~/.ssh echo "${{ secrets.MYREPO_TOKEN }}" > ~/.ssh/gitlab_deploy_key diff --git a/.gitignore b/.gitignore index 96ef6c0..e551aa3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /target Cargo.lock +.env diff --git a/Cargo.toml b/Cargo.toml index ce7a653..45a6f29 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,4 +11,11 @@ tokio = { version = "1.44.1", features = ["rt-multi-thread"] } tracing-subscriber = { version = "0.3.19" } tower = { version = "0.5.2" } hyper = { version = "1.6.0" } +sqlx = { version = "0.8.3", features = ["postgres", "runtime-tokio-native-tls"] } +dotenv = { version = "0.15" } icarus_models = { git = "ssh://git@git.kundeng.us/phoenix/icarus_models.git", tag = "v0.2.0" } + +[dev-dependencies] +http-body-util = "0.1.3" +reqwest = { version = "0.12.5", features = ["json"] } # For making HTTP requests in tests +once_cell = "1.19" # Useful for lazy initialization in tests/app setup diff --git a/migrations/20250402221858_init_migrate.sql b/migrations/20250402221858_init_migrate.sql new file mode 100644 index 0000000..8ddc1d3 --- /dev/null +++ b/migrations/20250402221858_init_migrate.sql @@ -0,0 +1 @@ +-- Add migration script here diff --git a/run_migrations.txt b/run_migrations.txt new file mode 100644 index 0000000..ae8892a --- /dev/null +++ b/run_migrations.txt @@ -0,0 +1,3 @@ +cargo install sqlx-cli +sqlx migrate add init_migration +sqlx migrate run diff --git a/src/callers/common.rs b/src/callers/common.rs index c39654b..afb5ffb 100644 --- a/src/callers/common.rs +++ b/src/callers/common.rs @@ -1,4 +1,30 @@ +use axum::{Extension, Json, http::StatusCode}; + +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize, Serialize)] +pub struct TestResult { + message: String, +} + // basic handler that responds with a static string pub async fn root() -> &'static str { "Hello, World!" } + +pub async fn db_ping(Extension(pool): Extension) -> (StatusCode, Json) { + match sqlx::query("SELECT 1").execute(&pool).await { + Ok(_) => { + let tr = TestResult { + message: String::from("This works"), + }; + (StatusCode::OK, Json(tr)) + } + Err(e) => ( + StatusCode::BAD_REQUEST, + Json(TestResult { + message: e.to_string(), + }), + ), + } +} diff --git a/src/callers/mod.rs b/src/callers/mod.rs index 79b9089..33ddec1 100644 --- a/src/callers/mod.rs +++ b/src/callers/mod.rs @@ -3,5 +3,6 @@ pub mod register; pub mod endpoints { pub const ROOT: &str = "/"; - pub const REGISTER: &str = "api/v2/register"; + pub const REGISTER: &str = "/api/v2/register"; + pub const DBTEST: &str = "/api/v2/test/db"; } diff --git a/src/lib.rs b/src/lib.rs index 231cc1e..c3c562c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,33 @@ pub mod callers; pub mod config; pub mod models; + +mod keys { + pub const DBURL: &str = "DATABASE_URL"; + + pub mod error { + pub const ERROR: &str = "DATABASE_URL must be set in .env"; + } +} + +mod connection_settings { + pub const MAXCONN: u32 = 5; +} + +pub mod db_pool { + + use sqlx::postgres::PgPoolOptions; + use std::env; + + use crate::{connection_settings, keys}; + + pub async fn create_pool() -> Result { + dotenv::dotenv().ok(); + let database_url = env::var(keys::DBURL).expect(keys::error::ERROR); + + PgPoolOptions::new() + .max_connections(connection_settings::MAXCONN) + .connect(&database_url) + .await + } +} diff --git a/src/main.rs b/src/main.rs index 036d622..80e37ee 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,25 +2,92 @@ use axum::{ Router, routing::{get, post}, }; +// use std::net::SocketAddr; use icarus_auth::callers; use icarus_auth::config; +// use sqlx::Postgres; #[tokio::main] async fn main() { // initialize tracing tracing_subscriber::fmt::init(); - // build our application with a route - let app = Router::new() - .route(callers::endpoints::ROOT, get(callers::common::root)) - .route( - callers::endpoints::REGISTER, - post(callers::register::register_user), - ); + let app = app().await; // run our app with hyper, listening globally on port 3000 let url = config::get_full(); let listener = tokio::net::TcpListener::bind(url).await.unwrap(); axum::serve(listener, app).await.unwrap(); } + +async fn app() -> Router { + let pool = icarus_auth::db_pool::create_pool() + .await + .expect("Failed to create pool"); + + // build our application with a route + Router::new() + .route(callers::endpoints::DBTEST, get(callers::common::db_ping)) + .route(callers::endpoints::ROOT, get(callers::common::root)) + .route( + callers::endpoints::REGISTER, + post(callers::register::register_user), + ) + .layer(axum::Extension(pool)) +} + +#[cfg(test)] +mod tests { + use super::*; + use axum::{ + body::Body, + // extract::connect_info::MockConnectInfo, + http::{Request, StatusCode}, + }; + use http_body_util::BodyExt; + // use http_body_util::BodyExt; // for `collect` + // use serde_json::{Value, json}; + // use tokio::net::TcpListener; + // use tower::{Service, ServiceExt}; // for `call`, `oneshot`, and `ready` + use tower::ServiceExt; // for `call`, `oneshot`, and `ready` + + #[tokio::test] + async fn hello_world() { + let app = app().await; + + // `Router` implements `tower::Service>` so we can + // call it like any tower service, no need to run an HTTP server. + let response = app + .oneshot( + Request::builder() + .uri(callers::endpoints::ROOT) + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + + /* + match response.into_body().collect().await { + Ok(o) => { + let parsed: String = match String::from_utf8(o.to_bytes()) { + Ok(s) => s, + Err(err) => { + String::new() + } + }; + } + Err(err) => { + assert!(false, + "Error: {:?}", err.to_string()); + } + } + */ + + let body = response.into_body().collect().await.unwrap().to_bytes(); + assert_eq!(&body[..], b"Hello, World!"); + } +} diff --git a/tests/auth_tests.rs b/tests/auth_tests.rs index 19e08fe..7268c22 100644 --- a/tests/auth_tests.rs +++ b/tests/auth_tests.rs @@ -1,5 +1,8 @@ extern crate icarus_auth; +use crate::icarus_auth::callers; + +// use axum::Extension; use axum::body::Body; // use axum::response::Response; use axum::{ @@ -7,13 +10,78 @@ use axum::{ http::{Request, StatusCode}, routing::get, }; +// use hyper::client::conn; +// use sqlx::PgPool; +// use sqlx::postgres::{self, PgPoolOptions}; +// use testcontainers_modules::testcontainers::runners::AsyncRunner; +// use hyper::client; +// use sqlx::postgres; // use http::{Request, StatusCode}; // use serde_json::json; // use tower::ServiceExt; // for `.oneshot()` use tower::util::ServiceExt; +// use testcontainers_modules::testcontainers::core::client:: -use crate::icarus_auth::callers; +const TEST_DATABASE_URL_ENV: &str = "TEST_DATABASE_URL"; +const DEFAULT_TEST_DATABASE_URL: &str = + "postgres://icarus_op_test:password@localhost:5432/icarus_auth_test"; +static SETUP: std::sync::Once = std::sync::Once::new(); + +// Ensure tracing is initialized only once for all tests in this file +/* +static TRACING_INIT: Lazy<()> = Lazy::new(|| { + if std::env::var("RUST_LOG").is_err() { + // Set default log level if not provided + unsafe { + std::env::set_var("RUST_LOG", "info,tower_http=debug,your_project_name=debug"); + } + } + tracing_subscriber::fmt() + .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) + .with_test_writer() // Write logs to the test output capture + .init(); +}); +*/ + +/* +async fn setup_database() -> sqlx::PgPool { + let database_url = std::env::var(TEST_DATABASE_URL_ENV) + .unwrap_or_else(|_| DEFAULT_TEST_DATABASE_URL.to_string()); + let pool = sqlx::PgPool::connect(&database_url) + .await + .expect("Failed to connect to test database"); + + let migrator = sqlx::migrate::Migrator::new(std::path::Path::new("./migrations")) + .await + .expect("Failed to create migrator"); + migrator.run(&pool).await.expect("Failed to run migrations"); + + // Seed here if needed + pool +} + */ + +/* +#[tokio::test] +async fn test_db_health() { + SETUP.call_once(|| { + tokio::runtime::Runtime::new().unwrap().block_on(async { + setup_database().await; + }); + }); +} +*/ + +/* +async fn setup_test(pool: sqlx::PgPool) -> Router { + Router::new() + .route(callers::endpoints::DBTEST, get(callers::common::db_ping)) + .layer(Extension(pool)) +} +*/ + +/* #[tokio::test] async fn test_hello_world() { let app = Router::new().route(callers::endpoints::ROOT, get(callers::common::root)); // Replace with your handler @@ -40,3 +108,46 @@ async fn test_hello_world() { assert_eq!(body, "Hello, World!"); } +*/ + +/* +#[tokio::test] +async fn _test_db_health_check() { + let container = testcontainers_modules::postgres::Postgres::default() + .start() + .await + .unwrap(); + let _host_ip = container.get_host().await.unwrap(); + let port = 5432; + let host_port = container.get_host_port_ipv4(port).await.unwrap(); + let conn_string = &format!( + "postgres://postgres:postgres@localhost:{}/postgres", + host_port + ); + + println!("Test Database: {}", conn_string); + + let app = Router::new().route(callers::endpoints::DBTEST, get(callers::common::db_ping)); // Replace with your handler + + let response = app + .oneshot( + Request::builder() + .uri(callers::endpoints::DBTEST) + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + + match PgPoolOptions::new().connect(conn_string).await { + Ok(_) => { + assert!(true, "Success"); + } + Err(err) => { + assert!(false, "Error: {:?}", err.to_string()); + } + }; +} + */