diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 0c65b14..aadee75 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -11,8 +11,24 @@ env: jobs: build: - runs-on: ubuntu-latest + + services: + postgres: + image: postgres + + env: + POSTGRES_PASSWORD: postgres + POSTGRES_USER: postgres + POSTGRES_DB: postgres + TEST_DATABASE_URL: postgres://postgres:postgres@localhost:5432 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 steps: - uses: actions/checkout@v3 @@ -31,7 +47,16 @@ jobs: ~/.cargo/git target key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + - name: Build env variables + run: | + mv rocket.ci.toml rocket.toml + cat rocket.toml - name: Build run: cargo build --verbose - name: Run tests - run: cargo test --verbose \ No newline at end of file + run: cargo test --verbose + env: + ROCKET_DATABASES: | + { test_db = { url = postgres://postgres:postgres@localhost:5432 } } + DATABASE_URL: postgres://postgres:postgres@localhost:5432 + TEST_DATABASE_URL: postgres://postgres:postgres@localhost:5432 \ No newline at end of file diff --git a/rocket.ci.toml b/rocket.ci.toml new file mode 100644 index 0000000..4c5bb9d --- /dev/null +++ b/rocket.ci.toml @@ -0,0 +1,15 @@ +[default.databases] +test_db = { url = "postgres://postgres:postgres@localhost:5432/postgres"} + +[default.limits] +data-form = "32 MiB" + +#[default.tls] +#certs = "ssl/localhost.pem" +#key = "ssl/localhost-key.pem" + +[release] +test_db = { url = "postgres://postgres:postgres@localhost:5432/postgres"} +log_level="normal" +address = "0.0.0.0" +port = 8000 \ No newline at end of file diff --git a/src/db/mod.rs b/src/db/mod.rs index 21905c8..e347da8 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -6,9 +6,14 @@ pub mod igniter; pub mod models; pub mod schema; +#[cfg(not(test))] #[database("main_db")] pub struct PostgresConn(pub diesel::PgConnection); +#[cfg(test)] +#[database("test_db")] +pub struct PostgresConn(pub diesel::PgConnection); + impl PostgresConn { pub async fn find_api_payment(self, payment_request: String) -> Option { self.run(|c| ApiPayment::find_one_by_request(payment_request, c)) diff --git a/src/main.rs b/src/main.rs index cd83c2f..381c8a7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,6 @@ #![recursion_limit = "1024"] - +#[cfg(test)] +mod tests; #[macro_use] extern crate juniper; @@ -33,8 +34,8 @@ use app::Schema; use db::igniter::run_db_migrations; use dotenv::dotenv; use juniper::EmptySubscription; -use rocket::Rocket; use rocket::{fairing::AdHoc, Route}; +use rocket::{Build, Rocket}; use routes::{auth::login, file::get_file, utils::graphiql, utils::static_index}; use std::env; @@ -60,7 +61,12 @@ async fn main() -> Result<(), rocket::Error> { dotenv().ok(); config::init(); - let _rocket = Rocket::build() + let _rocket = app_build().launch().await.expect("server to launch"); + Ok(()) +} + +fn app_build() -> Rocket { + let rocket = Rocket::build() .attach(PostgresConn::fairing()) .attach(Cors) .attach(AdHoc::try_on_ignite( @@ -75,12 +81,9 @@ async fn main() -> Result<(), rocket::Error> { Mutation, EmptySubscription::::new(), )) - .mount("/", routes_builder()) - .launch() - .await - .expect("server to launch"); + .mount("/", routes_builder()); - Ok(()) + return rocket; } fn routes_builder() -> Vec { diff --git a/src/rocket.rs b/src/rocket.rs new file mode 100644 index 0000000..1d95743 --- /dev/null +++ b/src/rocket.rs @@ -0,0 +1,21 @@ +pub fn rocket_instance() -> Rocket { + Rocket::build() + .attach(PostgresConn::fairing()) + .attach(Cors) + .attach(AdHoc::try_on_ignite( + "Database Migrations", + run_db_migrations, + )) + .manage(Cors) + // .configure(figment) + .register("/", catchers![payment_required]) + .manage(Schema::new( + Query, + Mutation, + EmptySubscription::::new(), + )) + .mount("/", routes_builder()) + .launch() + .await + .expect("server to launch") +} \ No newline at end of file diff --git a/src/tests/database.rs b/src/tests/database.rs new file mode 100644 index 0000000..ece80d3 --- /dev/null +++ b/src/tests/database.rs @@ -0,0 +1,82 @@ +#[cfg(test)] +pub mod tests { + + use diesel::{sql_query, Connection, PgConnection, RunQueryDsl}; + use diesel_migrations::RunMigrationsError; + + pub struct TestDb { + default_db_url: String, + url: String, + name: String, + delete_on_drop: bool, + } + impl TestDb { + pub fn new() -> Self { + let db_name = format!("test_{}", env!("CARGO_PKG_NAME")); + let default_db_url = std::env::var("TEST_DATABASE_URL").expect("TEST_DATABASE_URL"); + let conn = PgConnection::establish(&default_db_url).unwrap(); + + sql_query(format!("DROP DATABASE IF EXISTS {};", &db_name)) + .execute(&conn) + .unwrap(); + + sql_query(format!("CREATE DATABASE {};", db_name)) + .execute(&conn) + .unwrap(); + + let url = format!("{}/{}", &default_db_url, &db_name); + + let _migrations = Self::run_migrations(&url); + + Self { + default_db_url, + url, + name: db_name, + delete_on_drop: true, + } + } + + pub fn _url(&self) -> &str { + &self.url + } + + pub fn run_migrations(db_url: &String) -> Result<(), RunMigrationsError> { + let conn = PgConnection::establish(db_url).unwrap(); + + diesel_migrations::run_pending_migrations(&conn) + } + + // For further implementation of fixtures loading in test DB. + // pub fn load_fixtures(&self) -> () { + // let connection = &self.conn(); + + // } + + pub fn conn(&self) -> PgConnection { + PgConnection::establish(&self.url.as_str()).unwrap() + } + + pub fn _leak(&mut self) { + self.delete_on_drop = false; + } + } + + impl Drop for TestDb { + fn drop(&mut self) { + if !self.delete_on_drop { + warn!("TestDb leaking database {}", self.name); + return; + } + let conn = PgConnection::establish(&self.default_db_url).unwrap(); + sql_query(format!( + "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '{}'", + self.name + )) + .execute(&conn) + .unwrap(); + sql_query(format!("DROP DATABASE {}", self.name)) + .execute(&conn) + .unwrap(); + } + } +} diff --git a/src/tests/db/media.rs b/src/tests/db/media.rs new file mode 100644 index 0000000..4eeb8d6 --- /dev/null +++ b/src/tests/db/media.rs @@ -0,0 +1,36 @@ +#[cfg(test)] +mod tests { + use diesel::Connection; + use lazy_static::lazy_static; + + use crate::tests::database::tests::TestDb; + + lazy_static! { + static ref TEST_DB: TestDb = TestDb::new(); + } + + #[test] + pub fn test_media_insert() -> () { + use crate::db::models::media::*; + use dotenv::dotenv; + + dotenv().ok(); + + // let test_db = TestDb::new(); + + let conn = &TEST_DB.conn(); + + let new_media = NewMedia { + uuid: uuid::Uuid::new_v4(), + title: "test".to_string(), + description: Some("description test".to_string()), + absolute_path: "nowhere".to_string(), + published: true, + price: 100, + }; + + let result = &conn.test_transaction(|| Media::create(new_media, &conn)); + + assert_eq!("test".to_string(), result.title); + } +} diff --git a/src/tests/db/mod.rs b/src/tests/db/mod.rs new file mode 100644 index 0000000..514fafc --- /dev/null +++ b/src/tests/db/mod.rs @@ -0,0 +1 @@ +mod media; diff --git a/src/tests/fixtures/medias.rs b/src/tests/fixtures/medias.rs new file mode 100644 index 0000000..d80a4b9 --- /dev/null +++ b/src/tests/fixtures/medias.rs @@ -0,0 +1,5 @@ +use crate::db::models::media::{Media, NewMedia}; + +pub static Medias: Vec = [ + +].to_vec(); \ No newline at end of file diff --git a/src/tests/fixtures/mod.rs b/src/tests/fixtures/mod.rs new file mode 100644 index 0000000..38c54de --- /dev/null +++ b/src/tests/fixtures/mod.rs @@ -0,0 +1 @@ +// mod medias; diff --git a/src/tests/mod.rs b/src/tests/mod.rs new file mode 100644 index 0000000..1429707 --- /dev/null +++ b/src/tests/mod.rs @@ -0,0 +1,5 @@ +mod database; +mod db; +mod fixtures; +#[cfg(test)] +mod rocket; diff --git a/src/tests/rocket.rs b/src/tests/rocket.rs new file mode 100644 index 0000000..e9970da --- /dev/null +++ b/src/tests/rocket.rs @@ -0,0 +1,43 @@ +#[cfg(test)] +mod tests { + + use crate::app_build; + use rocket::http::ContentType; + + // Sets environment variables for testing as .env file is not loaded + fn set_test_env_variables() -> () { + std::env::set_var("LND_ADDRESS", "https://umbrel.local:10009"); + std::env::set_var("LND_CERTFILE_PATH", "src/lnd/config/lnd.cert"); + std::env::set_var("LND_MACAROON_PATH", "src/lnd/config/admin.macaroon"); + } + + // Tests rocket ignition + #[rocket::async_test] + async fn test_rocket() { + use rocket::local::asynchronous::Client; + let _client = Client::tracked(app_build()).await.unwrap(); + } + + // This test ensures graphql gets provided through the expected endpoint + #[ignore] + #[rocket::async_test] + async fn test_graphql_http_endpoint() { + use rocket::local::asynchronous::Client; + + set_test_env_variables(); + + let client = Client::tracked(app_build()).await.unwrap(); + + let request = client + .post(uri!(crate::app::post_graphql_handler)) + .header(ContentType::JSON) + .body(r#"{"query":"query IntrospectionQuery {__schema {queryType { name }mutationType { name } subscriptionType { name }}}","operationName":"IntrospectionQuery"}"#); + let response = request.dispatch().await; + + let content = response.into_string().await; + + assert!(content.is_some()); + } + + // async fn test_graphql_ +}