diff --git a/apps/labrinth/tests/fixtures/dummy_data.sql b/apps/labrinth/fixtures/dummy_data.sql similarity index 80% rename from apps/labrinth/tests/fixtures/dummy_data.sql rename to apps/labrinth/fixtures/dummy_data.sql index a60edfd5c..5ab97bb34 100644 --- a/apps/labrinth/tests/fixtures/dummy_data.sql +++ b/apps/labrinth/fixtures/dummy_data.sql @@ -3,24 +3,22 @@ -- Inserts 5 dummy users for testing, with slight differences -- 'Friend' and 'enemy' function like 'user', but we can use them to simulate 'other' users that may or may not be able to access certain things --- IDs 1-5, 1-5 -INSERT INTO users (id, username, email, role) VALUES (1, 'Admin', 'admin@modrinth.com', 'admin'); INSERT INTO users (id, username, email, role) -VALUES (2, 'Moderator', 'moderator@modrinth.com', 'moderator'); -INSERT INTO users (id, username, email, role) VALUES (3, 'User', 'user@modrinth.com', 'developer'); -INSERT INTO users (id, username, email, role) -VALUES (4, 'Friend', 'friend@modrinth.com', 'developer'); -INSERT INTO users (id, username, email, role) -VALUES (5, 'Enemy', 'enemy@modrinth.com', 'developer'); +VALUES ({{user_id::ADMIN}}, 'Admin', 'admin@modrinth.com', 'admin'), + ({{user_id::MODERATOR}}, 'Moderator', 'moderator@modrinth.com', 'moderator'), + ({{user_id::USER}}, 'User', 'user@modrinth.com', 'developer'), + ({{user_id::FRIEND}}, 'Friend', 'friend@modrinth.com', 'developer'), + ({{user_id::ENEMY}}, 'Enemy', 'enemy@modrinth.com', 'developer'); -- Full PATs for each user, with different scopes -- These are not legal PATs, as they contain all scopes- they mimic permissions of a logged in user -- IDs: 50-54, o p q r s -INSERT INTO pats (id, user_id, name, access_token, scopes, expires) VALUES (50, 1, 'admin-pat', 'mrp_patadmin', $1, '2030-08-18 15:48:58.435729+00'); -INSERT INTO pats (id, user_id, name, access_token, scopes, expires) VALUES (51, 2, 'moderator-pat', 'mrp_patmoderator', $1, '2030-08-18 15:48:58.435729+00'); -INSERT INTO pats (id, user_id, name, access_token, scopes, expires) VALUES (52, 3, 'user-pat', 'mrp_patuser', $1, '2030-08-18 15:48:58.435729+00'); -INSERT INTO pats (id, user_id, name, access_token, scopes, expires) VALUES (53, 4, 'friend-pat', 'mrp_patfriend', $1, '2030-08-18 15:48:58.435729+00'); -INSERT INTO pats (id, user_id, name, access_token, scopes, expires) VALUES (54, 5, 'enemy-pat', 'mrp_patenemy', $1, '2030-08-18 15:48:58.435729+00'); +INSERT INTO pats (id, user_id, name, access_token, scopes, expires) +VALUES (50, {{user_id::ADMIN}}, 'admin-pat', '{{pat::ADMIN}}', {{all_scopes}}, '2030-08-18 15:48:58.435729+00'), + (51, {{user_id::MODERATOR}}, 'moderator-pat', '{{pat::MODERATOR}}', {{all_scopes}}, '2030-08-18 15:48:58.435729+00'), + (52, {{user_id::USER}}, 'user-pat', '{{pat::USER}}', {{all_scopes}}, '2030-08-18 15:48:58.435729+00'), + (53, {{user_id::FRIEND}}, 'friend-pat', '{{pat::FRIEND}}', {{all_scopes}}, '2030-08-18 15:48:58.435729+00'), + (54, {{user_id::ENEMY}}, 'enemy-pat', '{{pat::ENEMY}}', {{all_scopes}}, '2030-08-18 15:48:58.435729+00'); INSERT INTO loaders (id, loader) VALUES (5, 'fabric'); INSERT INTO loaders_project_types (joining_loader_id, joining_project_type_id) VALUES (5, 1); @@ -119,7 +117,7 @@ VALUES ( 1, 'oauth_client_alpha', NULL, - $1, + {{all_scopes}}, '4dbff86cc2ca1bae1e16468a05cb9881c97f1753bce3619034898faa1aabe429955a1bf8ec483d7421fe3c1646613a59ed5441fb0f321389f77f48a879c7b1f1', 3 ); diff --git a/apps/labrinth/src/database/mod.rs b/apps/labrinth/src/database/mod.rs index b59954855..dce09ad57 100644 --- a/apps/labrinth/src/database/mod.rs +++ b/apps/labrinth/src/database/mod.rs @@ -5,5 +5,6 @@ pub use models::DBImage; pub use models::DBProject; pub use models::DBVersion; pub use postgres_database::{ - ReadOnlyPgPool, check_for_migrations, connect_all, register_and_set_metrics, + MIGRATOR, ReadOnlyPgPool, check_for_migrations, connect_all, + register_and_set_metrics, }; diff --git a/apps/labrinth/src/database/postgres_database.rs b/apps/labrinth/src/database/postgres_database.rs index f0cf7b5f3..5be981315 100644 --- a/apps/labrinth/src/database/postgres_database.rs +++ b/apps/labrinth/src/database/postgres_database.rs @@ -1,5 +1,6 @@ +use eyre::Context; use prometheus::{IntGauge, Registry}; -use sqlx::migrate::MigrateDatabase; +use sqlx::migrate::{MigrateDatabase, Migrator}; use sqlx::postgres::{PgPool, PgPoolOptions}; use sqlx::{Connection, PgConnection, Postgres}; use std::ops::{Deref, DerefMut}; @@ -75,25 +76,36 @@ pub async fn connect_all() -> Result<(PgPool, ReadOnlyPgPool), sqlx::Error> { Ok((pool, ro)) } } -pub async fn check_for_migrations() -> Result<(), sqlx::Error> { - let uri = dotenvy::var("DATABASE_URL").expect("`DATABASE_URL` not in .env"); + +pub async fn check_for_migrations() -> eyre::Result<()> { + let uri = + dotenvy::var("DATABASE_URL").wrap_err("`DATABASE_URL` not in .env")?; let uri = uri.as_str(); - if !Postgres::database_exists(uri).await? { + if !Postgres::database_exists(uri) + .await + .wrap_err("failed to check if database exists")? + { info!("Creating database..."); - Postgres::create_database(uri).await?; + Postgres::create_database(uri) + .await + .wrap_err("failed to create database")?; } info!("Applying migrations..."); - let mut conn: PgConnection = PgConnection::connect(uri).await?; - sqlx::migrate!() + let mut conn: PgConnection = PgConnection::connect(uri) + .await + .wrap_err("failed to connect to database")?; + MIGRATOR .run(&mut conn) .await - .expect("Error while running database migrations!"); + .wrap_err("failed to run database migrations")?; Ok(()) } +pub static MIGRATOR: Migrator = sqlx::migrate!(); + pub async fn register_and_set_metrics( pool: &PgPool, registry: &Registry, diff --git a/apps/labrinth/src/lib.rs b/apps/labrinth/src/lib.rs index 2dff70311..b35e4adfc 100644 --- a/apps/labrinth/src/lib.rs +++ b/apps/labrinth/src/lib.rs @@ -37,6 +37,7 @@ pub mod routes; pub mod scheduler; pub mod search; pub mod sync; +pub mod test; pub mod util; pub mod validate; diff --git a/apps/labrinth/src/test/db.rs b/apps/labrinth/src/test/db.rs new file mode 100644 index 000000000..cbfa24126 --- /dev/null +++ b/apps/labrinth/src/test/db.rs @@ -0,0 +1,81 @@ +use eyre::{Context, Result}; +use sqlx::{Executor, PgPool}; + +/// Static personal access token for use in [`AppendPat`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Pat(pub &'static str); + +/// Dummy [`Pat`]s. +#[allow(missing_docs, reason = "self-explanatory")] +pub mod pat { + use super::Pat; + + pub const ADMIN: Pat = Pat("mrp_patadmin"); + pub const MODERATOR: Pat = Pat("mrp_patmoderator"); + pub const USER: Pat = Pat("mrp_patuser"); + pub const FRIEND: Pat = Pat("mrp_patfriend"); + pub const ENEMY: Pat = Pat("mrp_patenemy"); +} + +/// See [`AppendPat::append_pat`]. +pub trait AppendPat { + /// Appends a [`Pat`] authorization token to an + /// [`actix_web::test::TestRequest`]. + #[must_use] + fn append_pat(self, pat: Pat) -> Self; +} + +impl AppendPat for actix_web::test::TestRequest { + fn append_pat(self, pat: Pat) -> Self { + self.append_header(("Authorization", pat.0)) + } +} + +/// Dummy [`DBUserId`]s. +/// +/// [`DBUserId`]: crate::database::models::DBUserId +pub mod user_id { + use crate::database::models::DBUserId; + + pub const ADMIN: DBUserId = DBUserId(1); + pub const MODERATOR: DBUserId = DBUserId(2); + pub const USER: DBUserId = DBUserId(3); + pub const FRIEND: DBUserId = DBUserId(4); + pub const ENEMY: DBUserId = DBUserId(5); +} + +/// Initialize a database with dummy fixture data. +/// +/// # Errors +/// +/// Errors if the fixture could not be applied. +pub async fn add_dummy_data(db: &PgPool) -> Result<()> { + db.execute( + include_str!("../../fixtures/dummy_data.sql") + // + .replace("{{user_id::ADMIN}}", &user_id::ADMIN.0.to_string()) + .replace( + "{{user_id::MODERATOR}}", + &user_id::MODERATOR.0.to_string(), + ) + .replace("{{user_id::USER}}", &user_id::USER.0.to_string()) + .replace("{{user_id::FRIEND}}", &user_id::FRIEND.0.to_string()) + .replace("{{user_id::ENEMY}}", &user_id::ENEMY.0.to_string()) + // + .replace("{{pat::ADMIN}}", pat::ADMIN.0) + .replace("{{pat::MODERATOR}}", pat::MODERATOR.0) + .replace("{{pat::USER}}", pat::USER.0) + .replace("{{pat::FRIEND}}", pat::FRIEND.0) + .replace("{{pat::ENEMY}}", pat::ENEMY.0) + // + .replace( + "{{all_scopes}}", + &crate::models::pats::Scopes::all().bits().to_string(), + ) + .as_str(), + ) + .await + .wrap_err("failed to add dummy data")?; + + Ok(()) +} diff --git a/apps/labrinth/src/test/mod.rs b/apps/labrinth/src/test/mod.rs new file mode 100644 index 000000000..dec10232b --- /dev/null +++ b/apps/labrinth/src/test/mod.rs @@ -0,0 +1 @@ +pub mod db; diff --git a/apps/labrinth/tests/common/dummy_data.rs b/apps/labrinth/tests/common/dummy_data.rs index 7e9ffa842..d44bd839c 100644 --- a/apps/labrinth/tests/common/dummy_data.rs +++ b/apps/labrinth/tests/common/dummy_data.rs @@ -10,11 +10,9 @@ use labrinth::models::ids::ProjectId; use labrinth::models::{ oauth_clients::OAuthClient, organizations::Organization, - pats::Scopes, projects::{Project, Version}, }; use serde_json::json; -use sqlx::Executor; use zip::{CompressionMethod, ZipWriter, write::FileOptions}; use super::{ @@ -291,13 +289,7 @@ pub async fn add_dummy_data(api: &ApiV3, db: TemporaryDatabase) -> DummyData { // Adds basic dummy data to the database directly with sql (user, pats) let pool = &db.pool.clone(); - pool.execute( - include_str!("../fixtures/dummy_data.sql") - .replace("$1", &Scopes::all().bits().to_string()) - .as_str(), - ) - .await - .unwrap(); + labrinth::test::db::add_dummy_data(pool).await.unwrap(); let (alpha_project, alpha_version) = add_project_alpha(api).await; let (beta_project, beta_version) = add_project_beta(api).await;