diff --git a/sql/bootstrap/roles.down.sql b/sql/bootstrap/roles.down.sql new file mode 100644 index 0000000..31f2263 --- /dev/null +++ b/sql/bootstrap/roles.down.sql @@ -0,0 +1,4 @@ +DROP USER IF EXISTS service; +REVOKE CONNECT ON DATABASE ratings FROM migration_user; +REVOKE USAGE, CREATE ON SCHEMA public FROM migration_user; +DROP USER IF EXISTS migration_user; diff --git a/sql/bootstrap/roles.up.sql b/sql/bootstrap/roles.up.sql new file mode 100644 index 0000000..9fa0054 --- /dev/null +++ b/sql/bootstrap/roles.up.sql @@ -0,0 +1,6 @@ +CREATE USER migration_user WITH PASSWORD 'strongpassword'; +CREATE USER service WITH PASSWORD 'covfefe!1'; +CREATE DATABASE ratings; +/c ratings; +GRANT CONNECT ON DATABASE ratings TO migration_user; +GRANT USAGE, CREATE ON SCHEMA public TO migration_user; diff --git a/sql/migrations/.gitkeep b/sql/migrations/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/sql/migrations/20230829085908_ratings_init.down.sql b/sql/migrations/20230829085908_ratings_init.down.sql new file mode 100644 index 0000000..a2ba528 --- /dev/null +++ b/sql/migrations/20230829085908_ratings_init.down.sql @@ -0,0 +1,9 @@ +REVOKE ALL PRIVILEGES ON TABLE users FROM service; +REVOKE USAGE, SELECT ON SEQUENCE users_id_seq FROM service; +REVOKE ALL PRIVILEGES ON TABLE votes FROM service; +REVOKE USAGE, SELECT ON SEQUENCE votes_id_seq FROM service; +REVOKE CONNECT ON DATABASE ratings FROM service; + +DROP TABLE IF EXISTS votes; +DROP TABLE IF EXISTS users; + diff --git a/sql/init.sql b/sql/migrations/20230829085908_ratings_init.up.sql similarity index 95% rename from sql/init.sql rename to sql/migrations/20230829085908_ratings_init.up.sql index 1653f81..e780526 100644 --- a/sql/init.sql +++ b/sql/migrations/20230829085908_ratings_init.up.sql @@ -1,4 +1,3 @@ --- -- Create database and then execute the second set of commands when connected -- to the ratings database -- @@ -8,6 +7,7 @@ -- CREATE DATABASE IF EXISTS ratings; -- Stage 2 +-- CREATE TABLE users ( id SERIAL PRIMARY KEY, @@ -31,8 +31,6 @@ CREATE TABLE votes ( -- can't vote more than once for the same snap revision. CREATE UNIQUE INDEX idx_votes_unique_user_snap ON votes (user_id_fk, snap_id, snap_revision); --- Permissions -CREATE USER service WITH PASSWORD 'covfefe!1'; GRANT ALL PRIVILEGES ON TABLE users TO service; GRANT USAGE, SELECT ON SEQUENCE users_id_seq TO service; GRANT ALL PRIVILEGES ON TABLE votes TO service; diff --git a/sql/teardown.sql b/sql/teardown.sql deleted file mode 100644 index 3191ae1..0000000 --- a/sql/teardown.sql +++ /dev/null @@ -1,4 +0,0 @@ -DROP TABLE IF EXISTS users; -DROP TABLE IF EXISTS votes; -DROP DATABASE IF EXISTS ratings; -DROP USER IF EXISTS service; diff --git a/src/utils/config.rs b/src/utils/config.rs index afd9c82..ad91e70 100644 --- a/src/utils/config.rs +++ b/src/utils/config.rs @@ -10,6 +10,7 @@ pub struct Config { pub name: String, pub port: u16, pub postgres_uri: String, + pub migration_postgres_uri: String, } impl Config { diff --git a/src/utils/migrator.rs b/src/utils/migrator.rs new file mode 100644 index 0000000..876a37f --- /dev/null +++ b/src/utils/migrator.rs @@ -0,0 +1,44 @@ +use std::error::Error; +use std::fmt::{Debug, Formatter}; +use std::sync::Arc; + +use sqlx::{postgres::PgPoolOptions, PgPool}; +use tracing::info; + +const MIGRATIONS_PATH: &str = "sql/migrations"; + +#[derive(Clone)] +pub struct Migrator { + pub pool: Arc, +} + +impl Migrator { + pub async fn new(uri: &str) -> Result> { + let pool = PgPoolOptions::new().max_connections(1).connect(uri).await?; + let pool = Arc::new(pool); + Ok(Migrator { pool }) + } + + pub async fn run(&self) -> Result<(), sqlx::Error> { + let m = sqlx::migrate::Migrator::new(std::path::Path::new(MIGRATIONS_PATH)).await?; + + m.run(&mut self.pool.acquire().await?).await?; + info!("migrator.run()"); + Ok(()) + } + + pub async fn revert(&self) -> Result<(), sqlx::Error> { + let m = sqlx::migrate::Migrator::new(std::path::Path::new(MIGRATIONS_PATH)).await?; + + m.undo(&mut self.pool.acquire().await?, 1).await?; + + info!("migrator.revert()"); + Ok(()) + } +} + +impl Debug for Migrator { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_str("Migrator { migrations_pool }") + } +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 5b9dadb..dea872e 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,6 +1,8 @@ pub mod config; pub mod infrastructure; pub mod jwt; +pub mod migrator; pub use config::Config; pub use infrastructure::Infrastructure; +pub use migrator::Migrator; diff --git a/tests/app_tests/lifecycle_test.rs b/tests/app_tests/lifecycle_test.rs index b712bf7..abd1cd6 100644 --- a/tests/app_tests/lifecycle_test.rs +++ b/tests/app_tests/lifecycle_test.rs @@ -1,7 +1,7 @@ use futures::FutureExt; use ratings::{ app::AppContext, - utils::{Config, Infrastructure}, + utils::{Config, Infrastructure, Migrator}, }; use super::super::helpers::with_lifecycle::with_lifecycle; @@ -19,17 +19,22 @@ async fn app_lifecycle_test() -> Result<(), Box> { let infra = Infrastructure::new(&config).await?; let app_ctx = AppContext::new(&config, infra); - with_lifecycle(async { - let data = TestData { - user_client: Some(UserClient::new(&config.socket())), - app_ctx, - id: None, - token: None, - app_client: Some(AppClient::new(&config.socket())), - snap_id: Some(data_faker::rnd_id()), - }; - vote_once(data).then(vote_up).await; - }) + let migrator = Migrator::new(&config.migration_postgres_uri).await?; + let data = TestData { + user_client: Some(UserClient::new(&config.socket())), + app_ctx, + id: None, + token: None, + app_client: Some(AppClient::new(&config.socket())), + snap_id: Some(data_faker::rnd_id()), + }; + + with_lifecycle( + async { + vote_once(data.clone()).then(vote_up).await; + }, + migrator, + ) .await; Ok(()) } diff --git a/tests/helpers/hooks.rs b/tests/helpers/hooks.rs index a5e18f4..3857d8c 100644 --- a/tests/helpers/hooks.rs +++ b/tests/helpers/hooks.rs @@ -1,19 +1,32 @@ -use std::sync::Arc; +use std::sync::{Arc, Once}; use once_cell::sync::Lazy; +use ratings::utils::Migrator; use tokio::sync::Mutex; -static INITIALIZATION_FLAG: Lazy>> = Lazy::new(|| Arc::new(Mutex::new(false))); - -pub async fn before_all() { - let mutex = Arc::clone(&*INITIALIZATION_FLAG); - let mut initialised = mutex.lock().await; - - if !*initialised { - *initialised = true; +static INIT: Once = Once::new(); +static TEST_COUNTER: Lazy>> = Lazy::new(|| Arc::new(Mutex::new(0))); +pub async fn before_all(migrator: Migrator) { + INIT.call_once(|| { tracing_subscriber::fmt().init(); + }); + if let Err(e) = migrator.run().await { + panic!("{}", e) } + + let counter = Arc::clone(&*TEST_COUNTER); + let mut test_counter = counter.lock().await; + *test_counter += 1; } -pub async fn after_all() {} +pub async fn after_all(migrator: Migrator) { + let counter = Arc::clone(&*TEST_COUNTER); + let mut test_counter = counter.lock().await; + *test_counter -= 1; + if *test_counter == 0 { + if let Err(e) = migrator.revert().await { + panic!("{}", e) + } + } +} diff --git a/tests/helpers/with_lifecycle.rs b/tests/helpers/with_lifecycle.rs index 9dae81b..4b4b603 100644 --- a/tests/helpers/with_lifecycle.rs +++ b/tests/helpers/with_lifecycle.rs @@ -1,12 +1,13 @@ use std::future::Future; use crate::helpers::hooks::{after_all, before_all}; +use ratings::utils::Migrator; -pub async fn with_lifecycle(f: F) +pub async fn with_lifecycle(f: F, migrator: Migrator) where F: Future, { - before_all().await; + before_all(migrator.clone()).await; f.await; - after_all().await; + after_all(migrator.clone()).await; } diff --git a/tests/user_tests/reject_invalid_register_test.rs b/tests/user_tests/reject_invalid_register_test.rs index f970604..d1e82bf 100644 --- a/tests/user_tests/reject_invalid_register_test.rs +++ b/tests/user_tests/reject_invalid_register_test.rs @@ -1,4 +1,4 @@ -use ratings::utils::Config; +use ratings::utils::{Config, Migrator}; use tonic::Code; use super::super::helpers::{client_user::UserClient, with_lifecycle::with_lifecycle}; @@ -6,18 +6,22 @@ use super::super::helpers::{client_user::UserClient, with_lifecycle::with_lifecy #[tokio::test] async fn blank() -> Result<(), Box> { let config = Config::load()?; + let migrator = Migrator::new(&config.migration_postgres_uri).await?; - with_lifecycle(async { - let id = ""; - let client = UserClient::new(&config.socket()); + with_lifecycle( + async { + let id = ""; + let client = UserClient::new(&config.socket()); - match client.register(id).await { - Ok(response) => panic!("expected Err but got Ok: {response:?}"), - Err(status) => { - assert_eq!(status.code(), Code::InvalidArgument) + match client.register(id).await { + Ok(response) => panic!("expected Err but got Ok: {response:?}"), + Err(status) => { + assert_eq!(status.code(), Code::InvalidArgument) + } } - } - }) + }, + migrator, + ) .await; Ok(()) } @@ -25,18 +29,22 @@ async fn blank() -> Result<(), Box> { #[tokio::test] async fn wrong_length() -> Result<(), Box> { let config = Config::load()?; + let migrator = Migrator::new(&config.migration_postgres_uri).await?; - with_lifecycle(async { - let client_hash = "foobarbazbun"; - let client = UserClient::new(&config.socket()); + with_lifecycle( + async { + let client_hash = "foobarbazbun"; + let client = UserClient::new(&config.socket()); - match client.register(client_hash).await { - Ok(response) => panic!("expected Err but got Ok: {response:?}"), - Err(status) => { - assert_eq!(status.code(), Code::InvalidArgument) + match client.register(client_hash).await { + Ok(response) => panic!("expected Err but got Ok: {response:?}"), + Err(status) => { + assert_eq!(status.code(), Code::InvalidArgument) + } } - } - }) + }, + migrator, + ) .await; Ok(()) } diff --git a/tests/user_tests/simple_lifecycle_test.rs b/tests/user_tests/simple_lifecycle_test.rs index e41578b..6f1b17c 100644 --- a/tests/user_tests/simple_lifecycle_test.rs +++ b/tests/user_tests/simple_lifecycle_test.rs @@ -6,7 +6,7 @@ use super::super::helpers::client_user::UserClient; use super::super::helpers::with_lifecycle::with_lifecycle; use futures::FutureExt; use ratings::app::AppContext; -use ratings::utils::{self, Infrastructure}; +use ratings::utils::{self, Infrastructure, Migrator}; use sqlx::Row; use utils::Config; @@ -16,22 +16,27 @@ async fn user_simple_lifecycle_test() -> Result<(), Box> let config = Config::load()?; let infra = Infrastructure::new(&config).await?; let app_ctx = AppContext::new(&config, infra); + let migrator = Migrator::new(&config.migration_postgres_uri).await?; + + let data = TestData { + user_client: Some(UserClient::new(&config.socket())), + app_ctx, + id: None, + token: None, + app_client: None, + snap_id: None, + }; - with_lifecycle(async { - let data = TestData { - user_client: Some(UserClient::new(&config.socket())), - app_ctx, - id: None, - token: None, - app_client: None, - snap_id: None, - }; - register(data) - .then(authenticate) - .then(vote) - .then(delete) - .await; - }) + with_lifecycle( + async { + register(data.clone()) + .then(authenticate) + .then(vote) + .then(delete) + .await; + }, + migrator, + ) .await; Ok(()) }