diff --git a/.github/workflows/rust-ci.yml b/.github/workflows/rust-ci.yml deleted file mode 100644 index 86337199f..000000000 --- a/.github/workflows/rust-ci.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: CI - -on: - push: - branches: ["main"] - pull_request: - types: [opened, synchronize] - merge_group: - types: [ checks_requested ] - -env: - CARGO_TERM_COLOR: always - SQLX_OFFLINE: true - -jobs: - build: - name: Build and Lint (Rust) - runs-on: ubuntu-22.04 - - steps: - - name: Check out code - uses: actions/checkout@v4 - - - name: Get build cache - id: cache-build - uses: actions/cache@v2 - with: - path: target/** - key: ${{ runner.os }}-rust-cache - - - name: Install build dependencies - run: | - sudo apt-get update - sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf - - - name: Build - run: cargo build - - - name: Lint - run: cargo clippy --all-targets --all-features -- -D warnings \ No newline at end of file diff --git a/.github/workflows/turbo-ci.yml b/.github/workflows/turbo-ci.yml index 0e510c66f..7920aad8b 100644 --- a/.github/workflows/turbo-ci.yml +++ b/.github/workflows/turbo-ci.yml @@ -10,7 +10,7 @@ on: jobs: build: - name: Build, Test, and Lint (Turbo) + name: Build, Test, and Lint runs-on: ubuntu-22.04 steps: @@ -66,5 +66,8 @@ jobs: - name: Lint run: pnpm lint + - name: Start docker compose + run: docker-compose up -d + - name: Test run: pnpm test diff --git a/Cargo.lock b/Cargo.lock index 23c3964c9..ddc5ad543 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -183,7 +183,7 @@ dependencies = [ "futures-core", "futures-util", "mio 1.0.2", - "socket2 0.5.7", + "socket2", "tokio", "tracing", ] @@ -246,7 +246,7 @@ dependencies = [ "serde_json", "serde_urlencoded", "smallvec", - "socket2 0.5.7", + "socket2", "time", "url", ] @@ -407,6 +407,12 @@ dependencies = [ "derive_arbitrary", ] +[[package]] +name = "arc-swap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" + [[package]] name = "argon2" version = "0.5.3" @@ -1711,7 +1717,7 @@ dependencies = [ "openssl-probe", "openssl-sys", "schannel", - "socket2 0.5.7", + "socket2", "windows-sys 0.52.0", ] @@ -1861,11 +1867,10 @@ dependencies = [ [[package]] name = "deadpool" -version = "0.10.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb84100978c1c7b37f09ed3ce3e5f843af02c2a2c431bae5b19230dad2c1b490" +checksum = "6541a3916932fe57768d4be0b1ffb5ec7cbf74ca8c903fdfd5c0fe8aa958f0ed" dependencies = [ - "async-trait", "deadpool-runtime", "num_cpus", "tokio", @@ -1873,9 +1878,9 @@ dependencies = [ [[package]] name = "deadpool-redis" -version = "0.14.0" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36f2381b0e993d06a1f6d49f486b33bc4004085bf980340fc05726bacc681fff" +checksum = "bfae6799b68a735270e4344ee3e834365f707c72da09c9a8bb89b45cc3351395" dependencies = [ "deadpool", "redis", @@ -3371,7 +3376,7 @@ dependencies = [ "httpdate", "itoa 1.0.11", "pin-project-lite", - "socket2 0.5.7", + "socket2", "tokio", "tower-service", "tracing", @@ -3474,7 +3479,7 @@ dependencies = [ "http-body 1.0.1", "hyper 1.4.1", "pin-project-lite", - "socket2 0.5.7", + "socket2", "tokio", "tower-service", "tracing", @@ -4212,7 +4217,7 @@ dependencies = [ "nom", "percent-encoding", "quoted_printable", - "socket2 0.5.7", + "socket2", "tokio", "url", ] @@ -4824,6 +4829,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-bigint-dig" version = "0.8.4" @@ -5499,7 +5514,7 @@ dependencies = [ "bincode", "either", "fnv", - "itertools 0.11.0", + "itertools 0.12.1", "lazy_static", "nom", "quick-xml 0.31.0", @@ -5893,7 +5908,7 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls 0.23.13", - "socket2 0.5.7", + "socket2", "thiserror", "tokio", "tracing", @@ -5924,7 +5939,7 @@ checksum = "4fe68c2e9e1a1234e218683dbdf9f9dfcb094113c5ac2b938dfcb9bab4c4140b" dependencies = [ "libc", "once_cell", - "socket2 0.5.7", + "socket2", "tracing", "windows-sys 0.59.0", ] @@ -6085,22 +6100,24 @@ dependencies = [ [[package]] name = "redis" -version = "0.24.0" +version = "0.27.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c580d9cbbe1d1b479e8d67cf9daf6a62c957e6846048408b80b43ac3f6af84cd" +checksum = "81cccf17a692ce51b86564334614d72dcae1def0fd5ecebc9f02956da74352b5" dependencies = [ "ahash 0.8.11", + "arc-swap", "async-trait", "bytes", "combine", "futures-util", "itoa 1.0.11", + "num-bigint", "percent-encoding", "pin-project-lite", "r2d2", "ryu", "sha1_smol", - "socket2 0.4.10", + "socket2", "tokio", "tokio-util", "url", @@ -7325,16 +7342,6 @@ dependencies = [ "serde", ] -[[package]] -name = "socket2" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" -dependencies = [ - "libc", - "winapi", -] - [[package]] name = "socket2" version = "0.5.7" @@ -8650,7 +8657,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.5.7", + "socket2", "tokio-macros", "tracing", "windows-sys 0.52.0", diff --git a/apps/app-playground/package.json b/apps/app-playground/package.json new file mode 100644 index 000000000..0d76eaed8 --- /dev/null +++ b/apps/app-playground/package.json @@ -0,0 +1,10 @@ +{ + "name": "@modrinth/app-playground", + "scripts": { + "build": "cargo build --release", + "lint": "cargo fmt --check && cargo clippy --all-targets --all-features -- -D warnings", + "fix": "cargo fmt && cargo clippy --fix", + "dev": "cargo run", + "test": "cargo test" + } +} diff --git a/apps/app/package.json b/apps/app/package.json index 6b3c35471..0e896878f 100644 --- a/apps/app/package.json +++ b/apps/app/package.json @@ -3,7 +3,10 @@ "scripts": { "build": "tauri build", "tauri": "tauri", - "dev": "tauri dev" + "dev": "tauri dev", + "test": "cargo test", + "lint": "cargo fmt --check && cargo clippy --all-targets --all-features -- -D warnings", + "fix": "cargo fmt && cargo clippy --fix" }, "devDependencies": { "@tauri-apps/cli": "2.0.0-rc.16" @@ -12,4 +15,4 @@ "@modrinth/app-frontend": "workspace:*", "@modrinth/app-lib": "workspace:*" } -} +} \ No newline at end of file diff --git a/apps/labrinth/Cargo.toml b/apps/labrinth/Cargo.toml index 2a8a0c0e4..09dcbf2f1 100644 --- a/apps/labrinth/Cargo.toml +++ b/apps/labrinth/Cargo.toml @@ -88,8 +88,8 @@ rust_decimal = { version = "1.33.1", features = [ "serde-with-float", "serde-with-str", ] } -redis = { version = "0.24.0", features = ["tokio-comp", "ahash", "r2d2"]} -deadpool-redis = "0.14.0" +redis = { version = "0.27.5", features = ["tokio-comp", "ahash", "r2d2"]} +deadpool-redis = "0.18.0" clickhouse = { version = "0.11.2", features = ["uuid", "time"] } uuid = { version = "1.2.2", features = ["v4", "fast-rng", "serde"] } diff --git a/apps/labrinth/package.json b/apps/labrinth/package.json new file mode 100644 index 000000000..64ad0b0e0 --- /dev/null +++ b/apps/labrinth/package.json @@ -0,0 +1,10 @@ +{ + "name": "@modrinth/labrinth", + "scripts": { + "build": "cargo build --release", + "lint": "cargo fmt --check && cargo clippy --all-targets --all-features -- -D warnings", + "fix": "cargo fmt && cargo clippy --fix", + "dev": "cargo run", + "test": "cargo test" + } +} diff --git a/apps/labrinth/src/auth/checks.rs b/apps/labrinth/src/auth/checks.rs index 62e7bc7c0..d347f70ac 100644 --- a/apps/labrinth/src/auth/checks.rs +++ b/apps/labrinth/src/auth/checks.rs @@ -10,11 +10,17 @@ use itertools::Itertools; use sqlx::PgPool; pub trait ValidateAuthorized { - fn validate_authorized(&self, user_option: Option<&User>) -> Result<(), ApiError>; + fn validate_authorized( + &self, + user_option: Option<&User>, + ) -> Result<(), ApiError>; } pub trait ValidateAllAuthorized { - fn validate_all_authorized(self, user_option: Option<&User>) -> Result<(), ApiError>; + fn validate_all_authorized( + self, + user_option: Option<&User>, + ) -> Result<(), ApiError>; } impl<'a, T, A> ValidateAllAuthorized for T @@ -22,7 +28,10 @@ where T: IntoIterator, A: ValidateAuthorized + 'a, { - fn validate_all_authorized(self, user_option: Option<&User>) -> Result<(), ApiError> { + fn validate_all_authorized( + self, + user_option: Option<&User>, + ) -> Result<(), ApiError> { self.into_iter() .try_for_each(|c| c.validate_authorized(user_option)) } @@ -34,9 +43,14 @@ pub async fn is_visible_project( pool: &PgPool, hide_unlisted: bool, ) -> Result { - filter_visible_project_ids(vec![project_data], user_option, pool, hide_unlisted) - .await - .map(|x| !x.is_empty()) + filter_visible_project_ids( + vec![project_data], + user_option, + pool, + hide_unlisted, + ) + .await + .map(|x| !x.is_empty()) } pub async fn is_team_member_project( @@ -99,8 +113,10 @@ pub async fn filter_visible_project_ids( // For hidden projects, return a filtered list of projects for which we are enlisted on the team if !check_projects.is_empty() { - return_projects - .extend(filter_enlisted_projects_ids(check_projects, user_option, pool).await?); + return_projects.extend( + filter_enlisted_projects_ids(check_projects, user_option, pool) + .await?, + ); } Ok(return_projects) @@ -143,7 +159,8 @@ pub async fn filter_enlisted_projects_ids( .fetch(pool) .map_ok(|row| { for x in projects.iter() { - let bool = Some(x.id.0) == row.id && Some(x.team_id.0) == row.team_id; + let bool = + Some(x.id.0) == row.id && Some(x.team_id.0) == row.team_id; if bool { return_projects.push(x.id); } @@ -195,7 +212,10 @@ pub async fn filter_visible_versions( } impl ValidateAuthorized for models::OAuthClient { - fn validate_authorized(&self, user_option: Option<&User>) -> Result<(), ApiError> { + fn validate_authorized( + &self, + user_option: Option<&User>, + ) -> Result<(), ApiError> { if let Some(user) = user_option { return if user.role.is_mod() || user.id == self.created_by.into() { Ok(()) @@ -240,7 +260,8 @@ pub async fn filter_visible_version_ids( // Then, get enlisted versions (Versions that are a part of a project we are a member of) let enlisted_version_ids = - filter_enlisted_version_ids(versions.clone(), user_option, pool, redis).await?; + filter_enlisted_version_ids(versions.clone(), user_option, pool, redis) + .await?; // Return versions that are not hidden, we are a mod of, or we are enlisted on the team of for version in versions { @@ -248,7 +269,8 @@ pub async fn filter_visible_version_ids( // - it's not hidden and we can see the project // - we are a mod // - we are enlisted on the team of the mod - if (!version.status.is_hidden() && visible_project_ids.contains(&version.project_id)) + if (!version.status.is_hidden() + && visible_project_ids.contains(&version.project_id)) || user_option .as_ref() .map(|x| x.role.is_mod()) @@ -292,7 +314,8 @@ pub async fn filter_enlisted_version_ids( .as_ref() .map(|x| x.role.is_mod()) .unwrap_or(false) - || (user_option.is_some() && authorized_project_ids.contains(&version.project_id)) + || (user_option.is_some() + && authorized_project_ids.contains(&version.project_id)) { return_versions.push(version.id); } @@ -307,7 +330,9 @@ pub async fn is_visible_collection( ) -> Result { let mut authorized = !collection_data.status.is_hidden(); if let Some(user) = &user_option { - if !authorized && (user.role.is_mod() || user.id == collection_data.user_id.into()) { + if !authorized + && (user.role.is_mod() || user.id == collection_data.user_id.into()) + { authorized = true; } } diff --git a/apps/labrinth/src/auth/email/mod.rs b/apps/labrinth/src/auth/email/mod.rs index b2f057396..80c8bb8e1 100644 --- a/apps/labrinth/src/auth/email/mod.rs +++ b/apps/labrinth/src/auth/email/mod.rs @@ -16,7 +16,11 @@ pub enum MailError { Smtp(#[from] lettre::transport::smtp::Error), } -pub fn send_email_raw(to: String, subject: String, body: String) -> Result<(), MailError> { +pub fn send_email_raw( + to: String, + subject: String, + body: String, +) -> Result<(), MailError> { let email = Message::builder() .from(Mailbox::new( Some("Modrinth".to_string()), diff --git a/apps/labrinth/src/auth/mod.rs b/apps/labrinth/src/auth/mod.rs index 7ec2c3e3f..30eca4d15 100644 --- a/apps/labrinth/src/auth/mod.rs +++ b/apps/labrinth/src/auth/mod.rs @@ -5,8 +5,9 @@ pub mod templates; pub mod validate; pub use crate::auth::email::send_email; pub use checks::{ - filter_enlisted_projects_ids, filter_enlisted_version_ids, filter_visible_collections, - filter_visible_project_ids, filter_visible_projects, + filter_enlisted_projects_ids, filter_enlisted_version_ids, + filter_visible_collections, filter_visible_project_ids, + filter_visible_projects, }; use serde::{Deserialize, Serialize}; // pub use pat::{generate_pat, PersonalAccessToken}; @@ -55,16 +56,22 @@ impl actix_web::ResponseError for AuthenticationError { match self { AuthenticationError::Env(..) => StatusCode::INTERNAL_SERVER_ERROR, AuthenticationError::Sqlx(..) => StatusCode::INTERNAL_SERVER_ERROR, - AuthenticationError::Database(..) => StatusCode::INTERNAL_SERVER_ERROR, + AuthenticationError::Database(..) => { + StatusCode::INTERNAL_SERVER_ERROR + } AuthenticationError::SerDe(..) => StatusCode::BAD_REQUEST, - AuthenticationError::Reqwest(..) => StatusCode::INTERNAL_SERVER_ERROR, + AuthenticationError::Reqwest(..) => { + StatusCode::INTERNAL_SERVER_ERROR + } AuthenticationError::InvalidCredentials => StatusCode::UNAUTHORIZED, AuthenticationError::Decoding(..) => StatusCode::BAD_REQUEST, AuthenticationError::Mail(..) => StatusCode::INTERNAL_SERVER_ERROR, AuthenticationError::InvalidAuthMethod => StatusCode::UNAUTHORIZED, AuthenticationError::InvalidClientId => StatusCode::UNAUTHORIZED, AuthenticationError::Url => StatusCode::BAD_REQUEST, - AuthenticationError::FileHosting(..) => StatusCode::INTERNAL_SERVER_ERROR, + AuthenticationError::FileHosting(..) => { + StatusCode::INTERNAL_SERVER_ERROR + } AuthenticationError::DuplicateUser => StatusCode::BAD_REQUEST, AuthenticationError::SocketError => StatusCode::BAD_REQUEST, } @@ -99,7 +106,9 @@ impl AuthenticationError { } } -#[derive(Serialize, Deserialize, Default, Eq, PartialEq, Clone, Copy, Debug)] +#[derive( + Serialize, Deserialize, Default, Eq, PartialEq, Clone, Copy, Debug, +)] #[serde(rename_all = "lowercase")] pub enum AuthProvider { #[default] diff --git a/apps/labrinth/src/auth/oauth/errors.rs b/apps/labrinth/src/auth/oauth/errors.rs index 744d507c0..dab6ff850 100644 --- a/apps/labrinth/src/auth/oauth/errors.rs +++ b/apps/labrinth/src/auth/oauth/errors.rs @@ -77,12 +77,16 @@ impl actix_web::ResponseError for OAuthError { | OAuthErrorType::OnlySupportsAuthorizationCodeGrant(_) | OAuthErrorType::RedirectUriChanged(_) | OAuthErrorType::UnauthorizedClient => StatusCode::BAD_REQUEST, - OAuthErrorType::ClientAuthenticationFailed => StatusCode::UNAUTHORIZED, + OAuthErrorType::ClientAuthenticationFailed => { + StatusCode::UNAUTHORIZED + } } } fn error_response(&self) -> HttpResponse { - if let Some(ValidatedRedirectUri(mut redirect_uri)) = self.valid_redirect_uri.clone() { + if let Some(ValidatedRedirectUri(mut redirect_uri)) = + self.valid_redirect_uri.clone() + { redirect_uri = format!( "{}?error={}&error_description={}", redirect_uri, @@ -114,7 +118,9 @@ pub enum OAuthErrorType { ClientMissingRedirectURI { client_id: crate::database::models::OAuthClientId, }, - #[error("The provided redirect URI did not match any configured in the client")] + #[error( + "The provided redirect URI did not match any configured in the client" + )] RedirectUriNotConfigured(String), #[error("The provided scope was malformed or did not correspond to known scopes ({0})")] FailedScopeParse(bitflags::parser::ParseError), @@ -159,14 +165,20 @@ impl OAuthErrorType { // IETF RFC 6749 4.1.2.1 (https://datatracker.ietf.org/doc/html/rfc6749#autoid-38) // And 5.2 (https://datatracker.ietf.org/doc/html/rfc6749#section-5.2) match self { - Self::RedirectUriNotConfigured(_) | Self::ClientMissingRedirectURI { client_id: _ } => { - "invalid_uri" + Self::RedirectUriNotConfigured(_) + | Self::ClientMissingRedirectURI { client_id: _ } => "invalid_uri", + Self::AuthenticationError(_) | Self::InvalidAcceptFlowId => { + "server_error" + } + Self::RedirectUriChanged(_) | Self::MalformedId(_) => { + "invalid_request" } - Self::AuthenticationError(_) | Self::InvalidAcceptFlowId => "server_error", - Self::RedirectUriChanged(_) | Self::MalformedId(_) => "invalid_request", Self::FailedScopeParse(_) | Self::ScopesTooBroad => "invalid_scope", - Self::InvalidClientId(_) | Self::ClientAuthenticationFailed => "invalid_client", - Self::InvalidAuthCode | Self::OnlySupportsAuthorizationCodeGrant(_) => "invalid_grant", + Self::InvalidClientId(_) | Self::ClientAuthenticationFailed => { + "invalid_client" + } + Self::InvalidAuthCode + | Self::OnlySupportsAuthorizationCodeGrant(_) => "invalid_grant", Self::UnauthorizedClient => "unauthorized_client", Self::AccessDenied => "access_denied", } diff --git a/apps/labrinth/src/auth/oauth/mod.rs b/apps/labrinth/src/auth/oauth/mod.rs index 4b9894585..bc0a53884 100644 --- a/apps/labrinth/src/auth/oauth/mod.rs +++ b/apps/labrinth/src/auth/oauth/mod.rs @@ -84,18 +84,19 @@ pub async fn init_oauth( client.id, )?; - let requested_scopes = oauth_info - .scope - .as_ref() - .map_or(Ok(client.max_scopes), |s| { - Scopes::parse_from_oauth_scopes(s).map_err(|e| { - OAuthError::redirect( - OAuthErrorType::FailedScopeParse(e), - &oauth_info.state, - &redirect_uri, - ) - }) - })?; + let requested_scopes = + oauth_info + .scope + .as_ref() + .map_or(Ok(client.max_scopes), |s| { + Scopes::parse_from_oauth_scopes(s).map_err(|e| { + OAuthError::redirect( + OAuthErrorType::FailedScopeParse(e), + &oauth_info.state, + &redirect_uri, + ) + }) + })?; if !client.max_scopes.contains(requested_scopes) { return Err(OAuthError::redirect( @@ -108,9 +109,13 @@ pub async fn init_oauth( let existing_authorization = OAuthClientAuthorization::get(client.id, user.id.into(), &**pool) .await - .map_err(|e| OAuthError::redirect(e, &oauth_info.state, &redirect_uri))?; - let redirect_uris = - OAuthRedirectUris::new(oauth_info.redirect_uri.clone(), redirect_uri.clone()); + .map_err(|e| { + OAuthError::redirect(e, &oauth_info.state, &redirect_uri) + })?; + let redirect_uris = OAuthRedirectUris::new( + oauth_info.redirect_uri.clone(), + redirect_uri.clone(), + ); match existing_authorization { Some(existing_authorization) if existing_authorization.scopes.contains(requested_scopes) => @@ -130,14 +135,17 @@ pub async fn init_oauth( let flow_id = Flow::InitOAuthAppApproval { user_id: user.id.into(), client_id: client.id, - existing_authorization_id: existing_authorization.map(|a| a.id), + existing_authorization_id: existing_authorization + .map(|a| a.id), scopes: requested_scopes, redirect_uris, state: oauth_info.state.clone(), } .insert(Duration::minutes(30), &redis) .await - .map_err(|e| OAuthError::redirect(e, &oauth_info.state, &redirect_uri))?; + .map_err(|e| { + OAuthError::redirect(e, &oauth_info.state, &redirect_uri) + })?; let access_request = OAuthClientAccessRequest { client_id: client.id.into(), @@ -169,7 +177,15 @@ pub async fn accept_client_scopes( redis: Data, session_queue: Data, ) -> Result { - accept_or_reject_client_scopes(true, req, accept_body, pool, redis, session_queue).await + accept_or_reject_client_scopes( + true, + req, + accept_body, + pool, + redis, + session_queue, + ) + .await } #[post("reject")] @@ -180,7 +196,8 @@ pub async fn reject_client_scopes( redis: Data, session_queue: Data, ) -> Result { - accept_or_reject_client_scopes(false, req, body, pool, redis, session_queue).await + accept_or_reject_client_scopes(false, req, body, pool, redis, session_queue) + .await } #[derive(Serialize, Deserialize)] @@ -231,13 +248,17 @@ pub async fn request_token( { // https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.3 if req_client_id != client_id.into() { - return Err(OAuthError::error(OAuthErrorType::UnauthorizedClient)); + return Err(OAuthError::error( + OAuthErrorType::UnauthorizedClient, + )); } if original_redirect_uri != req_params.redirect_uri { - return Err(OAuthError::error(OAuthErrorType::RedirectUriChanged( - req_params.redirect_uri.clone(), - ))); + return Err(OAuthError::error( + OAuthErrorType::RedirectUriChanged( + req_params.redirect_uri.clone(), + ), + )); } if req_params.grant_type != "authorization_code" { @@ -251,7 +272,8 @@ pub async fn request_token( let scopes = scopes - Scopes::restricted(); let mut transaction = pool.begin().await?; - let token_id = generate_oauth_access_token_id(&mut transaction).await?; + let token_id = + generate_oauth_access_token_id(&mut transaction).await?; let token = generate_access_token(); let token_hash = OAuthAccessToken::hash_token(&token); let time_until_expiration = OAuthAccessToken { @@ -323,7 +345,9 @@ pub async fn accept_or_reject_client_scopes( }) = flow { if current_user.id != user_id.into() { - return Err(OAuthError::error(AuthenticationError::InvalidCredentials)); + return Err(OAuthError::error( + AuthenticationError::InvalidCredentials, + )); } if accept { @@ -331,10 +355,19 @@ pub async fn accept_or_reject_client_scopes( let auth_id = match existing_authorization_id { Some(id) => id, - None => generate_oauth_client_authorization_id(&mut transaction).await?, + None => { + generate_oauth_client_authorization_id(&mut transaction) + .await? + } }; - OAuthClientAuthorization::upsert(auth_id, client_id, user_id, scopes, &mut transaction) - .await?; + OAuthClientAuthorization::upsert( + auth_id, + client_id, + user_id, + scopes, + &mut transaction, + ) + .await?; transaction.commit().await?; @@ -402,14 +435,17 @@ async fn init_oauth_code_flow( } .insert(Duration::minutes(10), redis) .await - .map_err(|e| OAuthError::redirect(e, &state, &redirect_uris.validated.clone()))?; + .map_err(|e| { + OAuthError::redirect(e, &state, &redirect_uris.validated.clone()) + })?; let mut redirect_params = vec![format!("code={code}")]; if let Some(state) = state { redirect_params.push(format!("state={state}")); } - let redirect_uri = append_params_to_uri(&redirect_uris.validated.0, &redirect_params); + let redirect_uri = + append_params_to_uri(&redirect_uris.validated.0, &redirect_params); // IETF RFC 6749 Section 4.1.2 (https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2) Ok(HttpResponse::Ok() diff --git a/apps/labrinth/src/auth/oauth/uris.rs b/apps/labrinth/src/auth/oauth/uris.rs index 708aa8a02..edef0c9d4 100644 --- a/apps/labrinth/src/auth/oauth/uris.rs +++ b/apps/labrinth/src/auth/oauth/uris.rs @@ -18,17 +18,20 @@ impl ValidatedRedirectUri { validate_against: impl IntoIterator + Clone, client_id: OAuthClientId, ) -> Result { - if let Some(first_client_redirect_uri) = validate_against.clone().into_iter().next() { + if let Some(first_client_redirect_uri) = + validate_against.clone().into_iter().next() + { if let Some(to_validate) = to_validate { - if validate_against - .into_iter() - .any(|uri| same_uri_except_query_components(uri, to_validate)) - { + if validate_against.into_iter().any(|uri| { + same_uri_except_query_components(uri, to_validate) + }) { Ok(ValidatedRedirectUri(to_validate.clone())) } else { - Err(OAuthError::error(OAuthErrorType::RedirectUriNotConfigured( - to_validate.clone(), - ))) + Err(OAuthError::error( + OAuthErrorType::RedirectUriNotConfigured( + to_validate.clone(), + ), + )) } } else { Ok(ValidatedRedirectUri(first_client_redirect_uri.to_string())) @@ -55,20 +58,26 @@ mod tests { fn validate_for_none_returns_first_valid_uri() { let validate_against = vec!["https://modrinth.com/a"]; - let validated = - ValidatedRedirectUri::validate(&None, validate_against.clone(), OAuthClientId(0)) - .unwrap(); + let validated = ValidatedRedirectUri::validate( + &None, + validate_against.clone(), + OAuthClientId(0), + ) + .unwrap(); assert_eq!(validate_against[0], validated.0); } #[test] - fn validate_for_valid_uri_returns_first_matching_uri_ignoring_query_params() { + fn validate_for_valid_uri_returns_first_matching_uri_ignoring_query_params() + { let validate_against = vec![ "https://modrinth.com/a?q3=p3&q4=p4", "https://modrinth.com/a/b/c?q1=p1&q2=p2", ]; - let to_validate = "https://modrinth.com/a/b/c?query0=param0&query1=param1".to_string(); + let to_validate = + "https://modrinth.com/a/b/c?query0=param0&query1=param1" + .to_string(); let validated = ValidatedRedirectUri::validate( &Some(to_validate.clone()), @@ -85,10 +94,15 @@ mod tests { let validate_against = vec!["https://modrinth.com/a"]; let to_validate = "https://modrinth.com/a/b".to_string(); - let validated = - ValidatedRedirectUri::validate(&Some(to_validate), validate_against, OAuthClientId(0)); + let validated = ValidatedRedirectUri::validate( + &Some(to_validate), + validate_against, + OAuthClientId(0), + ); - assert!(validated - .is_err_and(|e| matches!(e.error_type, OAuthErrorType::RedirectUriNotConfigured(_)))); + assert!(validated.is_err_and(|e| matches!( + e.error_type, + OAuthErrorType::RedirectUriNotConfigured(_) + ))); } } diff --git a/apps/labrinth/src/auth/validate.rs b/apps/labrinth/src/auth/validate.rs index b70a8703b..e69d19431 100644 --- a/apps/labrinth/src/auth/validate.rs +++ b/apps/labrinth/src/auth/validate.rs @@ -21,10 +21,15 @@ where E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, { // Fetch DB user record and minos user from headers - let (scopes, db_user) = - get_user_record_from_bearer_token(req, None, executor, redis, session_queue) - .await? - .ok_or_else(|| AuthenticationError::InvalidCredentials)?; + let (scopes, db_user) = get_user_record_from_bearer_token( + req, + None, + executor, + redis, + session_queue, + ) + .await? + .ok_or_else(|| AuthenticationError::InvalidCredentials)?; let user = User::from_full(db_user); @@ -58,31 +63,37 @@ where let possible_user = match token.split_once('_') { Some(("mrp", _)) => { let pat = - crate::database::models::pat_item::PersonalAccessToken::get(token, executor, redis) - .await? - .ok_or_else(|| AuthenticationError::InvalidCredentials)?; + crate::database::models::pat_item::PersonalAccessToken::get( + token, executor, redis, + ) + .await? + .ok_or_else(|| AuthenticationError::InvalidCredentials)?; if pat.expires < Utc::now() { return Err(AuthenticationError::InvalidCredentials); } - let user = user_item::User::get_id(pat.user_id, executor, redis).await?; + let user = + user_item::User::get_id(pat.user_id, executor, redis).await?; session_queue.add_pat(pat.id).await; user.map(|x| (pat.scopes, x)) } Some(("mra", _)) => { - let session = - crate::database::models::session_item::Session::get(token, executor, redis) - .await? - .ok_or_else(|| AuthenticationError::InvalidCredentials)?; + let session = crate::database::models::session_item::Session::get( + token, executor, redis, + ) + .await? + .ok_or_else(|| AuthenticationError::InvalidCredentials)?; if session.expires < Utc::now() { return Err(AuthenticationError::InvalidCredentials); } - let user = user_item::User::get_id(session.user_id, executor, redis).await?; + let user = + user_item::User::get_id(session.user_id, executor, redis) + .await?; let rate_limit_ignore = dotenvy::var("RATE_LIMIT_IGNORE_KEY")?; if !req @@ -111,7 +122,9 @@ where return Err(AuthenticationError::InvalidCredentials); } - let user = user_item::User::get_id(access_token.user_id, executor, redis).await?; + let user = + user_item::User::get_id(access_token.user_id, executor, redis) + .await?; session_queue.add_oauth_access_token(access_token.id).await; @@ -119,7 +132,8 @@ where } Some(("github", _)) | Some(("gho", _)) | Some(("ghp", _)) => { let user = AuthProvider::GitHub.get_user(token).await?; - let id = AuthProvider::GitHub.get_user_id(&user.id, executor).await?; + let id = + AuthProvider::GitHub.get_user_id(&user.id, executor).await?; let user = user_item::User::get_id( id.ok_or_else(|| AuthenticationError::InvalidCredentials)?, @@ -135,7 +149,9 @@ where Ok(possible_user) } -pub fn extract_authorization_header(req: &HttpRequest) -> Result<&str, AuthenticationError> { +pub fn extract_authorization_header( + req: &HttpRequest, +) -> Result<&str, AuthenticationError> { let headers = req.headers(); let token_val: Option<&HeaderValue> = headers.get(AUTHORIZATION); token_val @@ -154,9 +170,15 @@ pub async fn check_is_moderator_from_headers<'a, 'b, E>( where E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, { - let user = get_user_from_headers(req, executor, redis, session_queue, required_scopes) - .await? - .1; + let user = get_user_from_headers( + req, + executor, + redis, + session_queue, + required_scopes, + ) + .await? + .1; if user.role.is_mod() { Ok(user) diff --git a/apps/labrinth/src/clickhouse/mod.rs b/apps/labrinth/src/clickhouse/mod.rs index a89d47f4a..f74daa6a0 100644 --- a/apps/labrinth/src/clickhouse/mod.rs +++ b/apps/labrinth/src/clickhouse/mod.rs @@ -6,7 +6,8 @@ mod fetch; pub use fetch::*; pub async fn init_client() -> clickhouse::error::Result { - init_client_with_database(&dotenvy::var("CLICKHOUSE_DATABASE").unwrap()).await + init_client_with_database(&dotenvy::var("CLICKHOUSE_DATABASE").unwrap()) + .await } pub async fn init_client_with_database( @@ -16,9 +17,12 @@ pub async fn init_client_with_database( let mut http_connector = HttpConnector::new(); http_connector.enforce_http(false); // allow https URLs - let tls_connector = native_tls::TlsConnector::builder().build().unwrap().into(); - let https_connector = HttpsConnector::from((http_connector, tls_connector)); - let hyper_client = hyper::client::Client::builder().build(https_connector); + let tls_connector = + native_tls::TlsConnector::builder().build().unwrap().into(); + let https_connector = + HttpsConnector::from((http_connector, tls_connector)); + let hyper_client = + hyper::client::Client::builder().build(https_connector); clickhouse::Client::with_http_client(hyper_client) .with_url(dotenvy::var("CLICKHOUSE_URL").unwrap()) diff --git a/apps/labrinth/src/database/models/categories.rs b/apps/labrinth/src/database/models/categories.rs index 4e1a2a797..90abf1adb 100644 --- a/apps/labrinth/src/database/models/categories.rs +++ b/apps/labrinth/src/database/models/categories.rs @@ -86,7 +86,10 @@ impl Category { Ok(result.map(|r| CategoryId(r.id))) } - pub async fn list<'a, E>(exec: E, redis: &RedisPool) -> Result, DatabaseError> + pub async fn list<'a, E>( + exec: E, + redis: &RedisPool, + ) -> Result, DatabaseError> where E: sqlx::Executor<'a, Database = sqlx::Postgres>, { @@ -128,7 +131,10 @@ impl Category { } impl LinkPlatform { - pub async fn get_id<'a, E>(id: &str, exec: E) -> Result, DatabaseError> + pub async fn get_id<'a, E>( + id: &str, + exec: E, + ) -> Result, DatabaseError> where E: sqlx::Executor<'a, Database = sqlx::Postgres>, { @@ -145,7 +151,10 @@ impl LinkPlatform { Ok(result.map(|r| LinkPlatformId(r.id))) } - pub async fn list<'a, E>(exec: E, redis: &RedisPool) -> Result, DatabaseError> + pub async fn list<'a, E>( + exec: E, + redis: &RedisPool, + ) -> Result, DatabaseError> where E: sqlx::Executor<'a, Database = sqlx::Postgres>, { @@ -174,7 +183,12 @@ impl LinkPlatform { .await?; redis - .set_serialized_to_json(TAGS_NAMESPACE, "link_platform", &result, None) + .set_serialized_to_json( + TAGS_NAMESPACE, + "link_platform", + &result, + None, + ) .await?; Ok(result) @@ -182,7 +196,10 @@ impl LinkPlatform { } impl ReportType { - pub async fn get_id<'a, E>(name: &str, exec: E) -> Result, DatabaseError> + pub async fn get_id<'a, E>( + name: &str, + exec: E, + ) -> Result, DatabaseError> where E: sqlx::Executor<'a, Database = sqlx::Postgres>, { @@ -199,7 +216,10 @@ impl ReportType { Ok(result.map(|r| ReportTypeId(r.id))) } - pub async fn list<'a, E>(exec: E, redis: &RedisPool) -> Result, DatabaseError> + pub async fn list<'a, E>( + exec: E, + redis: &RedisPool, + ) -> Result, DatabaseError> where E: sqlx::Executor<'a, Database = sqlx::Postgres>, { @@ -224,7 +244,12 @@ impl ReportType { .await?; redis - .set_serialized_to_json(TAGS_NAMESPACE, "report_type", &result, None) + .set_serialized_to_json( + TAGS_NAMESPACE, + "report_type", + &result, + None, + ) .await?; Ok(result) @@ -232,7 +257,10 @@ impl ReportType { } impl ProjectType { - pub async fn get_id<'a, E>(name: &str, exec: E) -> Result, DatabaseError> + pub async fn get_id<'a, E>( + name: &str, + exec: E, + ) -> Result, DatabaseError> where E: sqlx::Executor<'a, Database = sqlx::Postgres>, { @@ -249,7 +277,10 @@ impl ProjectType { Ok(result.map(|r| ProjectTypeId(r.id))) } - pub async fn list<'a, E>(exec: E, redis: &RedisPool) -> Result, DatabaseError> + pub async fn list<'a, E>( + exec: E, + redis: &RedisPool, + ) -> Result, DatabaseError> where E: sqlx::Executor<'a, Database = sqlx::Postgres>, { @@ -274,7 +305,12 @@ impl ProjectType { .await?; redis - .set_serialized_to_json(TAGS_NAMESPACE, "project_type", &result, None) + .set_serialized_to_json( + TAGS_NAMESPACE, + "project_type", + &result, + None, + ) .await?; Ok(result) diff --git a/apps/labrinth/src/database/models/charge_item.rs b/apps/labrinth/src/database/models/charge_item.rs index 4ea19bf09..80d75de58 100644 --- a/apps/labrinth/src/database/models/charge_item.rs +++ b/apps/labrinth/src/database/models/charge_item.rs @@ -122,9 +122,12 @@ impl ChargeItem { exec: impl sqlx::Executor<'_, Database = sqlx::Postgres>, ) -> Result, DatabaseError> { let user_id = user_id.0; - let res = select_charges_with_predicate!("WHERE user_id = $1 ORDER BY due DESC", user_id) - .fetch_all(exec) - .await?; + let res = select_charges_with_predicate!( + "WHERE user_id = $1 ORDER BY due DESC", + user_id + ) + .fetch_all(exec) + .await?; Ok(res .into_iter() diff --git a/apps/labrinth/src/database/models/collection_item.rs b/apps/labrinth/src/database/models/collection_item.rs index 9bb937611..0a4c440b7 100644 --- a/apps/labrinth/src/database/models/collection_item.rs +++ b/apps/labrinth/src/database/models/collection_item.rs @@ -212,7 +212,10 @@ impl Collection { Ok(val) } - pub async fn clear_cache(id: CollectionId, redis: &RedisPool) -> Result<(), DatabaseError> { + pub async fn clear_cache( + id: CollectionId, + redis: &RedisPool, + ) -> Result<(), DatabaseError> { let mut redis = redis.connect().await?; redis.delete(COLLECTIONS_NAMESPACE, id.0).await?; diff --git a/apps/labrinth/src/database/models/flow_item.rs b/apps/labrinth/src/database/models/flow_item.rs index fab7e140d..95e66b6c0 100644 --- a/apps/labrinth/src/database/models/flow_item.rs +++ b/apps/labrinth/src/database/models/flow_item.rs @@ -68,12 +68,20 @@ impl Flow { .collect::(); redis - .set_serialized_to_json(FLOWS_NAMESPACE, &flow, &self, Some(expires.num_seconds())) + .set_serialized_to_json( + FLOWS_NAMESPACE, + &flow, + &self, + Some(expires.num_seconds()), + ) .await?; Ok(flow) } - pub async fn get(id: &str, redis: &RedisPool) -> Result, DatabaseError> { + pub async fn get( + id: &str, + redis: &RedisPool, + ) -> Result, DatabaseError> { let mut redis = redis.connect().await?; redis.get_deserialized_from_json(FLOWS_NAMESPACE, id).await @@ -95,7 +103,10 @@ impl Flow { Ok(flow) } - pub async fn remove(id: &str, redis: &RedisPool) -> Result, DatabaseError> { + pub async fn remove( + id: &str, + redis: &RedisPool, + ) -> Result, DatabaseError> { let mut redis = redis.connect().await?; redis.delete(FLOWS_NAMESPACE, id).await?; diff --git a/apps/labrinth/src/database/models/ids.rs b/apps/labrinth/src/database/models/ids.rs index be3809240..aa1b99895 100644 --- a/apps/labrinth/src/database/models/ids.rs +++ b/apps/labrinth/src/database/models/ids.rs @@ -264,25 +264,35 @@ generate_ids!( ChargeId ); -#[derive(Copy, Clone, Debug, PartialEq, Eq, Type, Hash, Serialize, Deserialize)] +#[derive( + Copy, Clone, Debug, PartialEq, Eq, Type, Hash, Serialize, Deserialize, +)] #[sqlx(transparent)] pub struct UserId(pub i64); -#[derive(Copy, Clone, Debug, Type, Eq, Hash, PartialEq, Serialize, Deserialize)] +#[derive( + Copy, Clone, Debug, Type, Eq, Hash, PartialEq, Serialize, Deserialize, +)] #[sqlx(transparent)] pub struct TeamId(pub i64); #[derive(Copy, Clone, Debug, Type, Serialize, Deserialize)] #[sqlx(transparent)] pub struct TeamMemberId(pub i64); -#[derive(Copy, Clone, Debug, Type, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive( + Copy, Clone, Debug, Type, PartialEq, Eq, Hash, Serialize, Deserialize, +)] #[sqlx(transparent)] pub struct OrganizationId(pub i64); -#[derive(Copy, Clone, Debug, Type, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive( + Copy, Clone, Debug, Type, PartialEq, Eq, Hash, Serialize, Deserialize, +)] #[sqlx(transparent)] pub struct ProjectId(pub i64); -#[derive(Copy, Clone, Debug, Type, Serialize, Deserialize, PartialEq, Eq, Hash)] +#[derive( + Copy, Clone, Debug, Type, Serialize, Deserialize, PartialEq, Eq, Hash, +)] #[sqlx(transparent)] pub struct ProjectTypeId(pub i32); @@ -292,16 +302,30 @@ pub struct StatusId(pub i32); #[derive(Copy, Clone, Debug, Type, Serialize, Deserialize)] #[sqlx(transparent)] pub struct GameId(pub i32); -#[derive(Copy, Clone, Debug, Type, Serialize, Deserialize, PartialEq, Eq, Hash)] +#[derive( + Copy, Clone, Debug, Type, Serialize, Deserialize, PartialEq, Eq, Hash, +)] #[sqlx(transparent)] pub struct LinkPlatformId(pub i32); #[derive( - Copy, Clone, Debug, Type, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord, + Copy, + Clone, + Debug, + Type, + PartialEq, + Eq, + Hash, + Serialize, + Deserialize, + PartialOrd, + Ord, )] #[sqlx(transparent)] pub struct VersionId(pub i64); -#[derive(Copy, Clone, Debug, Type, Serialize, Deserialize, PartialEq, Eq, Hash)] +#[derive( + Copy, Clone, Debug, Type, Serialize, Deserialize, PartialEq, Eq, Hash, +)] #[sqlx(transparent)] pub struct LoaderId(pub i32); #[derive(Copy, Clone, Debug, Type, Serialize, Deserialize)] @@ -319,11 +343,15 @@ pub struct ReportId(pub i64); #[sqlx(transparent)] pub struct ReportTypeId(pub i32); -#[derive(Copy, Clone, Debug, Type, Hash, Eq, PartialEq, Deserialize, Serialize)] +#[derive( + Copy, Clone, Debug, Type, Hash, Eq, PartialEq, Deserialize, Serialize, +)] #[sqlx(transparent)] pub struct FileId(pub i64); -#[derive(Copy, Clone, Debug, Type, Deserialize, Serialize, Eq, PartialEq, Hash)] +#[derive( + Copy, Clone, Debug, Type, Deserialize, Serialize, Eq, PartialEq, Hash, +)] #[sqlx(transparent)] pub struct PatId(pub i64); @@ -337,64 +365,102 @@ pub struct NotificationActionId(pub i32); #[derive(Copy, Clone, Debug, Type, Serialize, Deserialize, Eq, PartialEq)] #[sqlx(transparent)] pub struct ThreadId(pub i64); -#[derive(Copy, Clone, Debug, Type, Serialize, Deserialize, Eq, PartialEq, Hash)] +#[derive( + Copy, Clone, Debug, Type, Serialize, Deserialize, Eq, PartialEq, Hash, +)] #[sqlx(transparent)] pub struct ThreadMessageId(pub i64); -#[derive(Copy, Clone, Debug, Type, Serialize, Deserialize, Eq, PartialEq, Hash)] +#[derive( + Copy, Clone, Debug, Type, Serialize, Deserialize, Eq, PartialEq, Hash, +)] #[sqlx(transparent)] pub struct SessionId(pub i64); -#[derive(Copy, Clone, Debug, Type, Serialize, Deserialize, Eq, PartialEq, Hash)] +#[derive( + Copy, Clone, Debug, Type, Serialize, Deserialize, Eq, PartialEq, Hash, +)] #[sqlx(transparent)] pub struct ImageId(pub i64); #[derive( - Copy, Clone, Debug, Type, Serialize, Deserialize, Eq, PartialEq, Hash, PartialOrd, Ord, + Copy, + Clone, + Debug, + Type, + Serialize, + Deserialize, + Eq, + PartialEq, + Hash, + PartialOrd, + Ord, )] #[sqlx(transparent)] pub struct LoaderFieldId(pub i32); -#[derive(Copy, Clone, Debug, Type, Serialize, Deserialize, Eq, PartialEq, Hash)] +#[derive( + Copy, Clone, Debug, Type, Serialize, Deserialize, Eq, PartialEq, Hash, +)] #[sqlx(transparent)] pub struct LoaderFieldEnumId(pub i32); -#[derive(Copy, Clone, Debug, Type, Serialize, Deserialize, Eq, PartialEq, Hash)] +#[derive( + Copy, Clone, Debug, Type, Serialize, Deserialize, Eq, PartialEq, Hash, +)] #[sqlx(transparent)] pub struct LoaderFieldEnumValueId(pub i32); -#[derive(Copy, Clone, Debug, Type, Serialize, Deserialize, Eq, PartialEq, Hash)] +#[derive( + Copy, Clone, Debug, Type, Serialize, Deserialize, Eq, PartialEq, Hash, +)] #[sqlx(transparent)] pub struct OAuthClientId(pub i64); -#[derive(Copy, Clone, Debug, Type, Serialize, Deserialize, Eq, PartialEq, Hash)] +#[derive( + Copy, Clone, Debug, Type, Serialize, Deserialize, Eq, PartialEq, Hash, +)] #[sqlx(transparent)] pub struct OAuthClientAuthorizationId(pub i64); -#[derive(Copy, Clone, Debug, Type, Serialize, Deserialize, Eq, PartialEq, Hash)] +#[derive( + Copy, Clone, Debug, Type, Serialize, Deserialize, Eq, PartialEq, Hash, +)] #[sqlx(transparent)] pub struct OAuthRedirectUriId(pub i64); -#[derive(Copy, Clone, Debug, Type, Serialize, Deserialize, Eq, PartialEq, Hash)] +#[derive( + Copy, Clone, Debug, Type, Serialize, Deserialize, Eq, PartialEq, Hash, +)] #[sqlx(transparent)] pub struct OAuthAccessTokenId(pub i64); -#[derive(Copy, Clone, Debug, Type, Serialize, Deserialize, Eq, PartialEq, Hash)] +#[derive( + Copy, Clone, Debug, Type, Serialize, Deserialize, Eq, PartialEq, Hash, +)] #[sqlx(transparent)] pub struct PayoutId(pub i64); -#[derive(Copy, Clone, Debug, Type, Serialize, Deserialize, Eq, PartialEq, Hash)] +#[derive( + Copy, Clone, Debug, Type, Serialize, Deserialize, Eq, PartialEq, Hash, +)] #[sqlx(transparent)] pub struct ProductId(pub i64); -#[derive(Copy, Clone, Debug, Type, Serialize, Deserialize, Eq, PartialEq, Hash)] +#[derive( + Copy, Clone, Debug, Type, Serialize, Deserialize, Eq, PartialEq, Hash, +)] #[sqlx(transparent)] pub struct ProductPriceId(pub i64); -#[derive(Copy, Clone, Debug, Type, Serialize, Deserialize, Eq, PartialEq, Hash)] +#[derive( + Copy, Clone, Debug, Type, Serialize, Deserialize, Eq, PartialEq, Hash, +)] #[sqlx(transparent)] pub struct UserSubscriptionId(pub i64); -#[derive(Copy, Clone, Debug, Type, Serialize, Deserialize, Eq, PartialEq, Hash)] +#[derive( + Copy, Clone, Debug, Type, Serialize, Deserialize, Eq, PartialEq, Hash, +)] #[sqlx(transparent)] pub struct ChargeId(pub i64); diff --git a/apps/labrinth/src/database/models/image_item.rs b/apps/labrinth/src/database/models/image_item.rs index 1386429c0..d0ef66ab3 100644 --- a/apps/labrinth/src/database/models/image_item.rs +++ b/apps/labrinth/src/database/models/image_item.rs @@ -223,7 +223,10 @@ impl Image { Ok(val) } - pub async fn clear_cache(id: ImageId, redis: &RedisPool) -> Result<(), DatabaseError> { + pub async fn clear_cache( + id: ImageId, + redis: &RedisPool, + ) -> Result<(), DatabaseError> { let mut redis = redis.connect().await?; redis.delete(IMAGES_NAMESPACE, id.0).await?; diff --git a/apps/labrinth/src/database/models/legacy_loader_fields.rs b/apps/labrinth/src/database/models/legacy_loader_fields.rs index adb4e463d..e7fa76140 100644 --- a/apps/labrinth/src/database/models/legacy_loader_fields.rs +++ b/apps/labrinth/src/database/models/legacy_loader_fields.rs @@ -12,7 +12,9 @@ use serde_json::json; use crate::database::redis::RedisPool; use super::{ - loader_fields::{LoaderFieldEnum, LoaderFieldEnumValue, VersionField, VersionFieldValue}, + loader_fields::{ + LoaderFieldEnum, LoaderFieldEnumValue, VersionField, VersionFieldValue, + }, DatabaseError, LoaderFieldEnumValueId, }; @@ -44,13 +46,17 @@ impl MinecraftGameVersion { E: sqlx::Acquire<'a, Database = sqlx::Postgres>, { let mut exec = exec.acquire().await?; - let game_version_enum = LoaderFieldEnum::get(Self::FIELD_NAME, &mut *exec, redis) - .await? - .ok_or_else(|| { - DatabaseError::SchemaError("Could not find game version enum.".to_string()) - })?; + let game_version_enum = + LoaderFieldEnum::get(Self::FIELD_NAME, &mut *exec, redis) + .await? + .ok_or_else(|| { + DatabaseError::SchemaError( + "Could not find game version enum.".to_string(), + ) + })?; let game_version_enum_values = - LoaderFieldEnumValue::list(game_version_enum.id, &mut *exec, redis).await?; + LoaderFieldEnumValue::list(game_version_enum.id, &mut *exec, redis) + .await?; let game_versions = game_version_enum_values .into_iter() @@ -105,7 +111,9 @@ impl MinecraftGameVersion { Ok(game_versions) } - pub fn from_enum_value(loader_field_enum_value: LoaderFieldEnumValue) -> MinecraftGameVersion { + pub fn from_enum_value( + loader_field_enum_value: LoaderFieldEnumValue, + ) -> MinecraftGameVersion { MinecraftGameVersion { id: loader_field_enum_value.id, version: loader_field_enum_value.value, @@ -157,7 +165,10 @@ impl<'a> MinecraftGameVersionBuilder<'a> { }) } - pub fn created(self, created: &'a DateTime) -> MinecraftGameVersionBuilder<'a> { + pub fn created( + self, + created: &'a DateTime, + ) -> MinecraftGameVersionBuilder<'a> { Self { date: Some(created), ..self @@ -172,11 +183,12 @@ impl<'a> MinecraftGameVersionBuilder<'a> { where E: sqlx::Executor<'b, Database = sqlx::Postgres> + Copy, { - let game_versions_enum = LoaderFieldEnum::get("game_versions", exec, redis) - .await? - .ok_or(DatabaseError::SchemaError( - "Missing loaders field: 'game_versions'".to_string(), - ))?; + let game_versions_enum = + LoaderFieldEnum::get("game_versions", exec, redis) + .await? + .ok_or(DatabaseError::SchemaError( + "Missing loaders field: 'game_versions'".to_string(), + ))?; // Get enum id for game versions let metadata = json!({ diff --git a/apps/labrinth/src/database/models/loader_fields.rs b/apps/labrinth/src/database/models/loader_fields.rs index c1180dc53..70c74150e 100644 --- a/apps/labrinth/src/database/models/loader_fields.rs +++ b/apps/labrinth/src/database/models/loader_fields.rs @@ -43,7 +43,10 @@ impl Game { .find(|x| x.slug == slug)) } - pub async fn list<'a, E>(exec: E, redis: &RedisPool) -> Result, DatabaseError> + pub async fn list<'a, E>( + exec: E, + redis: &RedisPool, + ) -> Result, DatabaseError> where E: sqlx::Executor<'a, Database = sqlx::Postgres>, { @@ -72,7 +75,12 @@ impl Game { .await?; redis - .set_serialized_to_json(GAMES_LIST_NAMESPACE, "games", &result, None) + .set_serialized_to_json( + GAMES_LIST_NAMESPACE, + "games", + &result, + None, + ) .await?; Ok(result) @@ -99,7 +107,8 @@ impl Loader { E: sqlx::Executor<'a, Database = sqlx::Postgres>, { let mut redis = redis.connect().await?; - let cached_id: Option = redis.get_deserialized_from_json(LOADER_ID, name).await?; + let cached_id: Option = + redis.get_deserialized_from_json(LOADER_ID, name).await?; if let Some(cached_id) = cached_id { return Ok(Some(LoaderId(cached_id))); } @@ -124,7 +133,10 @@ impl Loader { Ok(result) } - pub async fn list<'a, E>(exec: E, redis: &RedisPool) -> Result, DatabaseError> + pub async fn list<'a, E>( + exec: E, + redis: &RedisPool, + ) -> Result, DatabaseError> where E: sqlx::Executor<'a, Database = sqlx::Postgres>, { @@ -169,7 +181,12 @@ impl Loader { .await?; redis - .set_serialized_to_json(LOADERS_LIST_NAMESPACE, "all", &result, None) + .set_serialized_to_json( + LOADERS_LIST_NAMESPACE, + "all", + &result, + None, + ) .await?; Ok(result) @@ -198,7 +215,10 @@ pub enum LoaderFieldType { ArrayBoolean, } impl LoaderFieldType { - pub fn build(field_type_name: &str, loader_field_enum: Option) -> Option { + pub fn build( + field_type_name: &str, + loader_field_enum: Option, + ) -> Option { Some(match (field_type_name, loader_field_enum) { ("integer", _) => LoaderFieldType::Integer, ("text", _) => LoaderFieldType::Text, @@ -207,7 +227,9 @@ impl LoaderFieldType { ("array_text", _) => LoaderFieldType::ArrayText, ("array_boolean", _) => LoaderFieldType::ArrayBoolean, ("enum", Some(id)) => LoaderFieldType::Enum(LoaderFieldEnumId(id)), - ("array_enum", Some(id)) => LoaderFieldType::ArrayEnum(LoaderFieldEnumId(id)), + ("array_enum", Some(id)) => { + LoaderFieldType::ArrayEnum(LoaderFieldEnumId(id)) + } _ => return None, }) } @@ -303,7 +325,10 @@ impl QueryVersionField { self } - pub fn with_enum_value(mut self, enum_value: LoaderFieldEnumValueId) -> Self { + pub fn with_enum_value( + mut self, + enum_value: LoaderFieldEnumValueId, + ) -> Self { self.enum_value = Some(enum_value); self } @@ -359,7 +384,8 @@ impl LoaderField { where E: sqlx::Executor<'a, Database = sqlx::Postgres>, { - let found_loader_fields = Self::get_fields_per_loader(loader_ids, exec, redis).await?; + let found_loader_fields = + Self::get_fields_per_loader(loader_ids, exec, redis).await?; let result = found_loader_fields .into_values() .flatten() @@ -464,7 +490,12 @@ impl LoaderField { .collect(); redis - .set_serialized_to_json(LOADER_FIELDS_NAMESPACE_ALL, "", &result, None) + .set_serialized_to_json( + LOADER_FIELDS_NAMESPACE_ALL, + "", + &result, + None, + ) .await?; Ok(result) @@ -482,7 +513,10 @@ impl LoaderFieldEnum { let mut redis = redis.connect().await?; let cached_enum = redis - .get_deserialized_from_json(LOADER_FIELD_ENUMS_ID_NAMESPACE, enum_name) + .get_deserialized_from_json( + LOADER_FIELD_ENUMS_ID_NAMESPACE, + enum_name, + ) .await?; if let Some(cached_enum) = cached_enum { return Ok(cached_enum); @@ -507,7 +541,12 @@ impl LoaderFieldEnum { }); redis - .set_serialized_to_json(LOADER_FIELD_ENUMS_ID_NAMESPACE, enum_name, &result, None) + .set_serialized_to_json( + LOADER_FIELD_ENUMS_ID_NAMESPACE, + enum_name, + &result, + None, + ) .await?; Ok(result) @@ -540,7 +579,9 @@ impl LoaderFieldEnumValue { E: sqlx::Executor<'a, Database = sqlx::Postgres>, { let get_enum_id = |x: &LoaderField| match x.field_type { - LoaderFieldType::Enum(id) | LoaderFieldType::ArrayEnum(id) => Some(id), + LoaderFieldType::Enum(id) | LoaderFieldType::ArrayEnum(id) => { + Some(id) + } _ => None, }; @@ -556,7 +597,10 @@ impl LoaderFieldEnumValue { let mut res = HashMap::new(); for lf in loader_fields { if let Some(id) = get_enum_id(lf) { - res.insert(lf.id, values.get(&id).unwrap_or(&Vec::new()).to_vec()); + res.insert( + lf.id, + values.get(&id).unwrap_or(&Vec::new()).to_vec(), + ); } } Ok(res) @@ -566,7 +610,10 @@ impl LoaderFieldEnumValue { loader_field_enum_ids: &[LoaderFieldEnumId], exec: E, redis: &RedisPool, - ) -> Result>, DatabaseError> + ) -> Result< + HashMap>, + DatabaseError, + > where E: sqlx::Executor<'a, Database = sqlx::Postgres>, { @@ -665,34 +712,33 @@ impl VersionField { VersionFieldValue::Text(s) => { query_version_fields.push(base.clone().with_string_value(s)) } - VersionFieldValue::Boolean(b) => { - query_version_fields.push(base.clone().with_int_value(if b { 1 } else { 0 })) - } + VersionFieldValue::Boolean(b) => query_version_fields + .push(base.clone().with_int_value(if b { 1 } else { 0 })), VersionFieldValue::ArrayInteger(v) => { for i in v { - query_version_fields.push(base.clone().with_int_value(i)); + query_version_fields + .push(base.clone().with_int_value(i)); } } VersionFieldValue::ArrayText(v) => { for s in v { - query_version_fields.push(base.clone().with_string_value(s)); + query_version_fields + .push(base.clone().with_string_value(s)); } } VersionFieldValue::ArrayBoolean(v) => { for b in v { - query_version_fields.push(base.clone().with_int_value(if b { - 1 - } else { - 0 - })); + query_version_fields.push( + base.clone().with_int_value(if b { 1 } else { 0 }), + ); } } - VersionFieldValue::Enum(_, v) => { - query_version_fields.push(base.clone().with_enum_value(v.id)) - } + VersionFieldValue::Enum(_, v) => query_version_fields + .push(base.clone().with_enum_value(v.id)), VersionFieldValue::ArrayEnum(_, v) => { for ev in v { - query_version_fields.push(base.clone().with_enum_value(ev.id)); + query_version_fields + .push(base.clone().with_enum_value(ev.id)); } } }; @@ -740,7 +786,8 @@ impl VersionField { value: serde_json::Value, enum_variants: Vec, ) -> Result { - let value = VersionFieldValue::parse(&loader_field, value, enum_variants)?; + let value = + VersionFieldValue::parse(&loader_field, value, enum_variants)?; // Ensure, if applicable, that the value is within the min/max bounds let countable = match &value { @@ -802,11 +849,13 @@ impl VersionField { query_loader_fields .iter() .flat_map(|q| { - let loader_field_type = - match LoaderFieldType::build(&q.field_type, q.enum_type.map(|l| l.0)) { - Some(lft) => lft, - None => return vec![], - }; + let loader_field_type = match LoaderFieldType::build( + &q.field_type, + q.enum_type.map(|l| l.0), + ) { + Some(lft) => lft, + None => return vec![], + }; let loader_field = LoaderField { id: q.id, field: q.field.clone(), @@ -908,7 +957,8 @@ impl VersionFieldValue { Ok(match field_type { LoaderFieldType::Integer => VersionFieldValue::Integer( - serde_json::from_value(value).map_err(|_| incorrect_type_error("integer"))?, + serde_json::from_value(value) + .map_err(|_| incorrect_type_error("integer"))?, ), LoaderFieldType::Text => VersionFieldValue::Text( value @@ -928,7 +978,9 @@ impl VersionFieldValue { }), LoaderFieldType::ArrayText => VersionFieldValue::ArrayText({ let array_values: Vec = serde_json::from_value(value) - .map_err(|_| incorrect_type_error("array of strings"))?; + .map_err(|_| { + incorrect_type_error("array of strings") + })?; array_values.into_iter().collect() }), LoaderFieldType::ArrayBoolean => VersionFieldValue::ArrayBoolean({ @@ -937,8 +989,12 @@ impl VersionFieldValue { array_values.into_iter().map(|v| v != 0).collect() }), LoaderFieldType::Enum(id) => VersionFieldValue::Enum(*id, { - let enum_value = value.as_str().ok_or_else(|| incorrect_type_error("enum"))?; - if let Some(ev) = enum_array.into_iter().find(|v| v.value == enum_value) { + let enum_value = value + .as_str() + .ok_or_else(|| incorrect_type_error("enum"))?; + if let Some(ev) = + enum_array.into_iter().find(|v| v.value == enum_value) + { ev } else { return Err(format!( @@ -946,21 +1002,27 @@ impl VersionFieldValue { )); } }), - LoaderFieldType::ArrayEnum(id) => VersionFieldValue::ArrayEnum(*id, { - let array_values: Vec = serde_json::from_value(value) - .map_err(|_| incorrect_type_error("array of enums"))?; - let mut enum_values = vec![]; - for av in array_values { - if let Some(ev) = enum_array.iter().find(|v| v.value == av) { - enum_values.push(ev.clone()); - } else { - return Err(format!( + LoaderFieldType::ArrayEnum(id) => { + VersionFieldValue::ArrayEnum(*id, { + let array_values: Vec = + serde_json::from_value(value).map_err(|_| { + incorrect_type_error("array of enums") + })?; + let mut enum_values = vec![]; + for av in array_values { + if let Some(ev) = + enum_array.iter().find(|v| v.value == av) + { + enum_values.push(ev.clone()); + } else { + return Err(format!( "Provided value '{av}' is not a valid variant for {field_name}" )); + } } - } - enum_values - }), + enum_values + }) + } }) } @@ -1046,141 +1108,180 @@ impl VersionFieldValue { ))); } - let mut value = match field_type { - // Singleton fields - // If there are multiple, we assume multiple versions are being concatenated - LoaderFieldType::Integer => qvfs - .into_iter() - .map(|qvf| { - Ok(( - qvf.version_id, - VersionFieldValue::Integer( - qvf.int_value - .ok_or(did_not_exist_error(field_name, "int_value"))?, - ), - )) - }) - .collect::, DatabaseError>>()?, - LoaderFieldType::Text => qvfs - .into_iter() - .map(|qvf| { - Ok(( - qvf.version_id, - VersionFieldValue::Text( - qvf.string_value - .ok_or(did_not_exist_error(field_name, "string_value"))?, - ), - )) - }) - .collect::, DatabaseError>>()?, - LoaderFieldType::Boolean => qvfs - .into_iter() - .map(|qvf| { - Ok(( - qvf.version_id, - VersionFieldValue::Boolean( - qvf.int_value - .ok_or(did_not_exist_error(field_name, "int_value"))? - != 0, - ), - )) - }) - .collect::, DatabaseError>>()?, - LoaderFieldType::Enum(id) => qvfs - .into_iter() - .map(|qvf| { - Ok(( - qvf.version_id, - VersionFieldValue::Enum(*id, { - let enum_id = qvf - .enum_value - .ok_or(did_not_exist_error(field_name, "enum_value"))?; - let lfev = qlfev - .iter() - .find(|x| x.id == enum_id) - .ok_or(did_not_exist_error(field_name, "enum_value"))?; - LoaderFieldEnumValue { - id: lfev.id, - enum_id: lfev.enum_id, - value: lfev.value.clone(), - ordering: lfev.ordering, - created: lfev.created, - metadata: lfev.metadata.clone().unwrap_or_default(), - } - }), - )) - }) - .collect::, DatabaseError>>()?, - - // Array fields - // We concatenate into one array - LoaderFieldType::ArrayInteger => vec![( - version_id, - VersionFieldValue::ArrayInteger( - qvfs.into_iter() - .map(|qvf| { - qvf.int_value - .ok_or(did_not_exist_error(field_name, "int_value")) - }) - .collect::>()?, - ), - )], - LoaderFieldType::ArrayText => vec![( - version_id, - VersionFieldValue::ArrayText( - qvfs.into_iter() - .map(|qvf| { - qvf.string_value - .ok_or(did_not_exist_error(field_name, "string_value")) - }) - .collect::>()?, - ), - )], - LoaderFieldType::ArrayBoolean => vec![( - version_id, - VersionFieldValue::ArrayBoolean( - qvfs.into_iter() - .map(|qvf| { - Ok::( - qvf.int_value - .ok_or(did_not_exist_error(field_name, "int_value"))? - != 0, - ) - }) - .collect::>()?, - ), - )], - LoaderFieldType::ArrayEnum(id) => vec![( - version_id, - VersionFieldValue::ArrayEnum( - *id, - qvfs.into_iter() - .map(|qvf| { - let enum_id = qvf - .enum_value - .ok_or(did_not_exist_error(field_name, "enum_value"))?; - let lfev = qlfev - .iter() - .find(|x| x.id == enum_id) - .ok_or(did_not_exist_error(field_name, "enum_value"))?; - Ok::<_, DatabaseError>(LoaderFieldEnumValue { - id: lfev.id, - enum_id: lfev.enum_id, - value: lfev.value.clone(), - ordering: lfev.ordering, - created: lfev.created, - metadata: lfev.metadata.clone().unwrap_or_default(), + let mut value = + match field_type { + // Singleton fields + // If there are multiple, we assume multiple versions are being concatenated + LoaderFieldType::Integer => qvfs + .into_iter() + .map(|qvf| { + Ok(( + qvf.version_id, + VersionFieldValue::Integer(qvf.int_value.ok_or( + did_not_exist_error(field_name, "int_value"), + )?), + )) + }) + .collect::, + DatabaseError, + >>()?, + LoaderFieldType::Text => qvfs + .into_iter() + .map(|qvf| { + Ok(( + qvf.version_id, + VersionFieldValue::Text(qvf.string_value.ok_or( + did_not_exist_error(field_name, "string_value"), + )?), + )) + }) + .collect::, + DatabaseError, + >>()?, + LoaderFieldType::Boolean => qvfs + .into_iter() + .map(|qvf| { + Ok(( + qvf.version_id, + VersionFieldValue::Boolean( + qvf.int_value.ok_or(did_not_exist_error( + field_name, + "int_value", + ))? != 0, + ), + )) + }) + .collect::, + DatabaseError, + >>()?, + LoaderFieldType::Enum(id) => qvfs + .into_iter() + .map(|qvf| { + Ok(( + qvf.version_id, + VersionFieldValue::Enum(*id, { + let enum_id = qvf.enum_value.ok_or( + did_not_exist_error( + field_name, + "enum_value", + ), + )?; + let lfev = qlfev + .iter() + .find(|x| x.id == enum_id) + .ok_or(did_not_exist_error( + field_name, + "enum_value", + ))?; + LoaderFieldEnumValue { + id: lfev.id, + enum_id: lfev.enum_id, + value: lfev.value.clone(), + ordering: lfev.ordering, + created: lfev.created, + metadata: lfev + .metadata + .clone() + .unwrap_or_default(), + } + }), + )) + }) + .collect::, + DatabaseError, + >>()?, + + // Array fields + // We concatenate into one array + LoaderFieldType::ArrayInteger => vec![( + version_id, + VersionFieldValue::ArrayInteger( + qvfs.into_iter() + .map(|qvf| { + qvf.int_value.ok_or(did_not_exist_error( + field_name, + "int_value", + )) }) - }) - .collect::>()?, - ), - )], - }; + .collect::>()?, + ), + )], + LoaderFieldType::ArrayText => vec![( + version_id, + VersionFieldValue::ArrayText( + qvfs.into_iter() + .map(|qvf| { + qvf.string_value.ok_or(did_not_exist_error( + field_name, + "string_value", + )) + }) + .collect::>()?, + ), + )], + LoaderFieldType::ArrayBoolean => vec![( + version_id, + VersionFieldValue::ArrayBoolean( + qvfs.into_iter() + .map(|qvf| { + Ok::( + qvf.int_value.ok_or( + did_not_exist_error( + field_name, + "int_value", + ), + )? != 0, + ) + }) + .collect::>()?, + ), + )], + LoaderFieldType::ArrayEnum(id) => vec![( + version_id, + VersionFieldValue::ArrayEnum( + *id, + qvfs.into_iter() + .map(|qvf| { + let enum_id = qvf.enum_value.ok_or( + did_not_exist_error( + field_name, + "enum_value", + ), + )?; + let lfev = qlfev + .iter() + .find(|x| x.id == enum_id) + .ok_or(did_not_exist_error( + field_name, + "enum_value", + ))?; + Ok::<_, DatabaseError>(LoaderFieldEnumValue { + id: lfev.id, + enum_id: lfev.enum_id, + value: lfev.value.clone(), + ordering: lfev.ordering, + created: lfev.created, + metadata: lfev + .metadata + .clone() + .unwrap_or_default(), + }) + }) + .collect::>()?, + ), + )], + }; // Sort arrayenums by ordering, then by created for (_, v) in value.iter_mut() { if let VersionFieldValue::ArrayEnum(_, v) = v { - v.sort_by(|a, b| a.ordering.cmp(&b.ordering).then(a.created.cmp(&b.created))); + v.sort_by(|a, b| { + a.ordering.cmp(&b.ordering).then(a.created.cmp(&b.created)) + }); } } @@ -1190,7 +1291,9 @@ impl VersionFieldValue { // Serialize to internal value, such as for converting to user-facing JSON pub fn serialize_internal(&self) -> serde_json::Value { match self { - VersionFieldValue::Integer(i) => serde_json::Value::Number((*i).into()), + VersionFieldValue::Integer(i) => { + serde_json::Value::Number((*i).into()) + } VersionFieldValue::Text(s) => serde_json::Value::String(s.clone()), VersionFieldValue::Boolean(b) => serde_json::Value::Bool(*b), VersionFieldValue::ArrayInteger(v) => serde_json::Value::Array( @@ -1203,10 +1306,12 @@ impl VersionFieldValue { .map(|s| serde_json::Value::String(s.clone())) .collect(), ), - VersionFieldValue::ArrayBoolean(v) => { - serde_json::Value::Array(v.iter().map(|b| serde_json::Value::Bool(*b)).collect()) + VersionFieldValue::ArrayBoolean(v) => serde_json::Value::Array( + v.iter().map(|b| serde_json::Value::Bool(*b)).collect(), + ), + VersionFieldValue::Enum(_, v) => { + serde_json::Value::String(v.value.clone()) } - VersionFieldValue::Enum(_, v) => serde_json::Value::String(v.value.clone()), VersionFieldValue::ArrayEnum(_, v) => serde_json::Value::Array( v.iter() .map(|v| serde_json::Value::String(v.value.clone())) @@ -1222,11 +1327,17 @@ impl VersionFieldValue { VersionFieldValue::Integer(i) => vec![i.to_string()], VersionFieldValue::Text(s) => vec![s.clone()], VersionFieldValue::Boolean(b) => vec![b.to_string()], - VersionFieldValue::ArrayInteger(v) => v.iter().map(|i| i.to_string()).collect(), + VersionFieldValue::ArrayInteger(v) => { + v.iter().map(|i| i.to_string()).collect() + } VersionFieldValue::ArrayText(v) => v.clone(), - VersionFieldValue::ArrayBoolean(v) => v.iter().map(|b| b.to_string()).collect(), + VersionFieldValue::ArrayBoolean(v) => { + v.iter().map(|b| b.to_string()).collect() + } VersionFieldValue::Enum(_, v) => vec![v.value.clone()], - VersionFieldValue::ArrayEnum(_, v) => v.iter().map(|v| v.value.clone()).collect(), + VersionFieldValue::ArrayEnum(_, v) => { + v.iter().map(|v| v.value.clone()).collect() + } } } diff --git a/apps/labrinth/src/database/models/notification_item.rs b/apps/labrinth/src/database/models/notification_item.rs index 128ce6542..5fda4a80e 100644 --- a/apps/labrinth/src/database/models/notification_item.rs +++ b/apps/labrinth/src/database/models/notification_item.rs @@ -46,7 +46,8 @@ impl NotificationBuilder { redis: &RedisPool, ) -> Result<(), DatabaseError> { let notification_ids = - generate_many_notification_ids(users.len(), &mut *transaction).await?; + generate_many_notification_ids(users.len(), &mut *transaction) + .await?; let body = serde_json::value::to_value(&self.body)?; let bodies = notification_ids @@ -97,7 +98,8 @@ impl Notification { where E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, { - let notification_ids_parsed: Vec = notification_ids.iter().map(|x| x.0).collect(); + let notification_ids_parsed: Vec = + notification_ids.iter().map(|x| x.0).collect(); sqlx::query!( " SELECT n.id, n.user_id, n.name, n.text, n.link, n.created, n.read, n.type notification_type, n.body, @@ -153,7 +155,10 @@ impl Notification { let mut redis = redis.connect().await?; let cached_notifications: Option> = redis - .get_deserialized_from_json(USER_NOTIFICATIONS_NAMESPACE, &user_id.0.to_string()) + .get_deserialized_from_json( + USER_NOTIFICATIONS_NAMESPACE, + &user_id.0.to_string(), + ) .await?; if let Some(notifications) = cached_notifications { @@ -227,7 +232,8 @@ impl Notification { transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, redis: &RedisPool, ) -> Result, DatabaseError> { - let notification_ids_parsed: Vec = notification_ids.iter().map(|x| x.0).collect(); + let notification_ids_parsed: Vec = + notification_ids.iter().map(|x| x.0).collect(); let affected_users = sqlx::query!( " @@ -243,7 +249,11 @@ impl Notification { .try_collect::>() .await?; - Notification::clear_user_notifications_cache(affected_users.iter(), redis).await?; + Notification::clear_user_notifications_cache( + affected_users.iter(), + redis, + ) + .await?; Ok(Some(())) } @@ -261,7 +271,8 @@ impl Notification { transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, redis: &RedisPool, ) -> Result, DatabaseError> { - let notification_ids_parsed: Vec = notification_ids.iter().map(|x| x.0).collect(); + let notification_ids_parsed: Vec = + notification_ids.iter().map(|x| x.0).collect(); sqlx::query!( " @@ -286,7 +297,11 @@ impl Notification { .try_collect::>() .await?; - Notification::clear_user_notifications_cache(affected_users.iter(), redis).await?; + Notification::clear_user_notifications_cache( + affected_users.iter(), + redis, + ) + .await?; Ok(Some(())) } @@ -298,11 +313,9 @@ impl Notification { let mut redis = redis.connect().await?; redis - .delete_many( - user_ids - .into_iter() - .map(|id| (USER_NOTIFICATIONS_NAMESPACE, Some(id.0.to_string()))), - ) + .delete_many(user_ids.into_iter().map(|id| { + (USER_NOTIFICATIONS_NAMESPACE, Some(id.0.to_string())) + })) .await?; Ok(()) diff --git a/apps/labrinth/src/database/models/oauth_client_item.rs b/apps/labrinth/src/database/models/oauth_client_item.rs index 4c34b3562..820f28ce2 100644 --- a/apps/labrinth/src/database/models/oauth_client_item.rs +++ b/apps/labrinth/src/database/models/oauth_client_item.rs @@ -91,10 +91,12 @@ impl OAuthClient { ) -> Result, DatabaseError> { let ids = ids.iter().map(|id| id.0).collect_vec(); let ids_ref: &[i64] = &ids; - let results = - select_clients_with_predicate!("WHERE clients.id = ANY($1::bigint[])", ids_ref) - .fetch_all(exec) - .await?; + let results = select_clients_with_predicate!( + "WHERE clients.id = ANY($1::bigint[])", + ids_ref + ) + .fetch_all(exec) + .await?; Ok(results.into_iter().map(|r| r.into()).collect_vec()) } @@ -104,9 +106,12 @@ impl OAuthClient { exec: impl sqlx::Executor<'_, Database = sqlx::Postgres>, ) -> Result, DatabaseError> { let user_id_param = user_id.0; - let clients = select_clients_with_predicate!("WHERE created_by = $1", user_id_param) - .fetch_all(exec) - .await?; + let clients = select_clients_with_predicate!( + "WHERE created_by = $1", + user_id_param + ) + .fetch_all(exec) + .await?; Ok(clients.into_iter().map(|r| r.into()).collect()) } @@ -153,7 +158,8 @@ impl OAuthClient { .execute(&mut **transaction) .await?; - Self::insert_redirect_uris(&self.redirect_uris, &mut **transaction).await?; + Self::insert_redirect_uris(&self.redirect_uris, &mut **transaction) + .await?; Ok(()) } @@ -231,7 +237,9 @@ impl OAuthClient { impl From for OAuthClient { fn from(r: ClientQueryResult) -> Self { - let redirects = if let (Some(ids), Some(uris)) = (r.uri_ids.as_ref(), r.uri_vals.as_ref()) { + let redirects = if let (Some(ids), Some(uris)) = + (r.uri_ids.as_ref(), r.uri_vals.as_ref()) + { ids.iter() .zip(uris.iter()) .map(|(id, uri)| OAuthRedirectUri { diff --git a/apps/labrinth/src/database/models/oauth_token_item.rs b/apps/labrinth/src/database/models/oauth_token_item.rs index 9c12f3836..9c35a5904 100644 --- a/apps/labrinth/src/database/models/oauth_token_item.rs +++ b/apps/labrinth/src/database/models/oauth_token_item.rs @@ -1,4 +1,7 @@ -use super::{DatabaseError, OAuthAccessTokenId, OAuthClientAuthorizationId, OAuthClientId, UserId}; +use super::{ + DatabaseError, OAuthAccessTokenId, OAuthClientAuthorizationId, + OAuthClientId, UserId, +}; use crate::models::pats::Scopes; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; diff --git a/apps/labrinth/src/database/models/organization_item.rs b/apps/labrinth/src/database/models/organization_item.rs index 7651baac6..b01052776 100644 --- a/apps/labrinth/src/database/models/organization_item.rs +++ b/apps/labrinth/src/database/models/organization_item.rs @@ -1,4 +1,6 @@ -use crate::{database::redis::RedisPool, models::ids::base62_impl::parse_base62}; +use crate::{ + database::redis::RedisPool, models::ids::base62_impl::parse_base62, +}; use dashmap::DashMap; use futures::TryStreamExt; use std::fmt::{Debug, Display}; @@ -100,7 +102,11 @@ impl Organization { Self::get_many(&ids, exec, redis).await } - pub async fn get_many<'a, E, T: Display + Hash + Eq + PartialEq + Clone + Debug>( + pub async fn get_many< + 'a, + E, + T: Display + Hash + Eq + PartialEq + Clone + Debug, + >( organization_strings: &[T], exec: E, redis: &RedisPool, diff --git a/apps/labrinth/src/database/models/pat_item.rs b/apps/labrinth/src/database/models/pat_item.rs index 6488c9e1b..205a70e4b 100644 --- a/apps/labrinth/src/database/models/pat_item.rs +++ b/apps/labrinth/src/database/models/pat_item.rs @@ -55,7 +55,11 @@ impl PersonalAccessToken { Ok(()) } - pub async fn get<'a, E, T: Display + Hash + Eq + PartialEq + Clone + Debug>( + pub async fn get< + 'a, + E, + T: Display + Hash + Eq + PartialEq + Clone + Debug, + >( id: T, exec: E, redis: &RedisPool, @@ -83,7 +87,11 @@ impl PersonalAccessToken { PersonalAccessToken::get_many(&ids, exec, redis).await } - pub async fn get_many<'a, E, T: Display + Hash + Eq + PartialEq + Clone + Debug>( + pub async fn get_many< + 'a, + E, + T: Display + Hash + Eq + PartialEq + Clone + Debug, + >( pat_strings: &[T], exec: E, redis: &RedisPool, @@ -151,7 +159,10 @@ impl PersonalAccessToken { let mut redis = redis.connect().await?; let res = redis - .get_deserialized_from_json::>(PATS_USERS_NAMESPACE, &user_id.0.to_string()) + .get_deserialized_from_json::>( + PATS_USERS_NAMESPACE, + &user_id.0.to_string(), + ) .await?; if let Some(res) = res { @@ -194,13 +205,18 @@ impl PersonalAccessToken { } redis - .delete_many(clear_pats.into_iter().flat_map(|(id, token, user_id)| { - [ - (PATS_NAMESPACE, id.map(|i| i.0.to_string())), - (PATS_TOKENS_NAMESPACE, token), - (PATS_USERS_NAMESPACE, user_id.map(|i| i.0.to_string())), - ] - })) + .delete_many(clear_pats.into_iter().flat_map( + |(id, token, user_id)| { + [ + (PATS_NAMESPACE, id.map(|i| i.0.to_string())), + (PATS_TOKENS_NAMESPACE, token), + ( + PATS_USERS_NAMESPACE, + user_id.map(|i| i.0.to_string()), + ), + ] + }, + )) .await?; Ok(()) diff --git a/apps/labrinth/src/database/models/payout_item.rs b/apps/labrinth/src/database/models/payout_item.rs index c5b377671..51ed85abb 100644 --- a/apps/labrinth/src/database/models/payout_item.rs +++ b/apps/labrinth/src/database/models/payout_item.rs @@ -48,7 +48,10 @@ impl Payout { Ok(()) } - pub async fn get<'a, 'b, E>(id: PayoutId, executor: E) -> Result, DatabaseError> + pub async fn get<'a, 'b, E>( + id: PayoutId, + executor: E, + ) -> Result, DatabaseError> where E: sqlx::Executor<'a, Database = sqlx::Postgres>, { diff --git a/apps/labrinth/src/database/models/product_item.rs b/apps/labrinth/src/database/models/product_item.rs index 7c082f16c..eaca8b7df 100644 --- a/apps/labrinth/src/database/models/product_item.rs +++ b/apps/labrinth/src/database/models/product_item.rs @@ -1,4 +1,6 @@ -use crate::database::models::{product_item, DatabaseError, ProductId, ProductPriceId}; +use crate::database::models::{ + product_item, DatabaseError, ProductId, ProductPriceId, +}; use crate::database::redis::RedisPool; use crate::models::billing::{Price, ProductMetadata}; use dashmap::DashMap; @@ -61,9 +63,12 @@ impl ProductItem { ) -> Result, DatabaseError> { let ids = ids.iter().map(|id| id.0).collect_vec(); let ids_ref: &[i64] = &ids; - let results = select_products_with_predicate!("WHERE id = ANY($1::bigint[])", ids_ref) - .fetch_all(exec) - .await?; + let results = select_products_with_predicate!( + "WHERE id = ANY($1::bigint[])", + ids_ref + ) + .fetch_all(exec) + .await?; Ok(results .into_iter() @@ -95,7 +100,10 @@ pub struct QueryProduct { } impl QueryProduct { - pub async fn list<'a, E>(exec: E, redis: &RedisPool) -> Result, DatabaseError> + pub async fn list<'a, E>( + exec: E, + redis: &RedisPool, + ) -> Result, DatabaseError> where E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, { @@ -201,9 +209,12 @@ impl ProductPriceItem { ) -> Result, DatabaseError> { let ids = ids.iter().map(|id| id.0).collect_vec(); let ids_ref: &[i64] = &ids; - let results = select_prices_with_predicate!("WHERE id = ANY($1::bigint[])", ids_ref) - .fetch_all(exec) - .await?; + let results = select_prices_with_predicate!( + "WHERE id = ANY($1::bigint[])", + ids_ref + ) + .fetch_all(exec) + .await?; Ok(results .into_iter() @@ -228,20 +239,25 @@ impl ProductPriceItem { let ids_ref: &[i64] = &ids; use futures_util::TryStreamExt; - let prices = select_prices_with_predicate!("WHERE product_id = ANY($1::bigint[])", ids_ref) - .fetch(exec) - .try_fold( - DashMap::new(), - |acc: DashMap>, x| { - if let Ok(item) = >::try_into(x) - { - acc.entry(item.product_id).or_default().push(item); - } - - async move { Ok(acc) } - }, - ) - .await?; + let prices = select_prices_with_predicate!( + "WHERE product_id = ANY($1::bigint[])", + ids_ref + ) + .fetch(exec) + .try_fold( + DashMap::new(), + |acc: DashMap>, x| { + if let Ok(item) = >::try_into(x) + { + acc.entry(item.product_id).or_default().push(item); + } + + async move { Ok(acc) } + }, + ) + .await?; Ok(prices) } diff --git a/apps/labrinth/src/database/models/project_item.rs b/apps/labrinth/src/database/models/project_item.rs index 2fb153c75..1bd07d224 100644 --- a/apps/labrinth/src/database/models/project_item.rs +++ b/apps/labrinth/src/database/models/project_item.rs @@ -1,5 +1,6 @@ use super::loader_fields::{ - QueryLoaderField, QueryLoaderFieldEnumValue, QueryVersionField, VersionField, + QueryLoaderField, QueryLoaderFieldEnumValue, QueryVersionField, + VersionField, }; use super::{ids::*, User}; use crate::database::models; @@ -72,15 +73,15 @@ impl GalleryItem { project_id: ProjectId, transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, ) -> Result<(), sqlx::error::Error> { - let (project_ids, image_urls, raw_image_urls, featureds, names, descriptions, orderings): ( - Vec<_>, - Vec<_>, - Vec<_>, - Vec<_>, - Vec<_>, - Vec<_>, - Vec<_>, - ) = items + let ( + project_ids, + image_urls, + raw_image_urls, + featureds, + names, + descriptions, + orderings, + ): (Vec<_>, Vec<_>, Vec<_>, Vec<_>, Vec<_>, Vec<_>, Vec<_>) = items .into_iter() .map(|gi| { ( @@ -128,7 +129,11 @@ impl ModCategory { items: Vec, transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, ) -> Result<(), DatabaseError> { - let (project_ids, category_ids, is_additionals): (Vec<_>, Vec<_>, Vec<_>) = items + let (project_ids, category_ids, is_additionals): ( + Vec<_>, + Vec<_>, + Vec<_>, + ) = items .into_iter() .map(|mc| (mc.project_id.0, mc.category_id.0, mc.is_additional)) .multiunzip(); @@ -223,9 +228,19 @@ impl ProjectBuilder { version.insert(&mut *transaction).await?; } - LinkUrl::insert_many_projects(link_urls, self.project_id, &mut *transaction).await?; + LinkUrl::insert_many_projects( + link_urls, + self.project_id, + &mut *transaction, + ) + .await?; - GalleryItem::insert_many(gallery_items, self.project_id, &mut *transaction).await?; + GalleryItem::insert_many( + gallery_items, + self.project_id, + &mut *transaction, + ) + .await?; let project_id = self.project_id; let mod_categories = categories @@ -323,7 +338,8 @@ impl Project { let project = Self::get_id(id, &mut **transaction, redis).await?; if let Some(project) = project { - Project::clear_cache(id, project.inner.slug, Some(true), redis).await?; + Project::clear_cache(id, project.inner.slug, Some(true), redis) + .await?; sqlx::query!( " @@ -389,7 +405,8 @@ impl Project { .await?; for version in project.versions { - super::Version::remove_full(version, redis, transaction).await?; + super::Version::remove_full(version, redis, transaction) + .await?; } sqlx::query!( @@ -422,7 +439,8 @@ impl Project { .execute(&mut **transaction) .await?; - models::TeamMember::clear_cache(project.inner.team_id, redis).await?; + models::TeamMember::clear_cache(project.inner.team_id, redis) + .await?; let affected_user_ids = sqlx::query!( " @@ -476,9 +494,13 @@ impl Project { where E: sqlx::Acquire<'a, Database = sqlx::Postgres>, { - Project::get_many(&[crate::models::ids::ProjectId::from(id)], executor, redis) - .await - .map(|x| x.into_iter().next()) + Project::get_many( + &[crate::models::ids::ProjectId::from(id)], + executor, + redis, + ) + .await + .map(|x| x.into_iter().next()) } pub async fn get_many_ids<'a, E>( @@ -496,7 +518,11 @@ impl Project { Project::get_many(&ids, exec, redis).await } - pub async fn get_many<'a, E, T: Display + Hash + Eq + PartialEq + Clone + Debug>( + pub async fn get_many< + 'a, + E, + T: Display + Hash + Eq + PartialEq + Clone + Debug, + >( project_strings: &[T], exec: E, redis: &RedisPool, @@ -837,11 +863,15 @@ impl Project { id: ProjectId, exec: E, redis: &RedisPool, - ) -> Result, Option, Option)>, DatabaseError> + ) -> Result< + Vec<(Option, Option, Option)>, + DatabaseError, + > where E: sqlx::Executor<'a, Database = sqlx::Postgres>, { - type Dependencies = Vec<(Option, Option, Option)>; + type Dependencies = + Vec<(Option, Option, Option)>; let mut redis = redis.connect().await?; @@ -881,7 +911,12 @@ impl Project { .await?; redis - .set_serialized_to_json(PROJECTS_DEPENDENCIES_NAMESPACE, id.0, &dependencies, None) + .set_serialized_to_json( + PROJECTS_DEPENDENCIES_NAMESPACE, + id.0, + &dependencies, + None, + ) .await?; Ok(dependencies) } diff --git a/apps/labrinth/src/database/models/report_item.rs b/apps/labrinth/src/database/models/report_item.rs index 9dd0804c8..703c5d60b 100644 --- a/apps/labrinth/src/database/models/report_item.rs +++ b/apps/labrinth/src/database/models/report_item.rs @@ -56,7 +56,10 @@ impl Report { Ok(()) } - pub async fn get<'a, E>(id: ReportId, exec: E) -> Result, sqlx::Error> + pub async fn get<'a, E>( + id: ReportId, + exec: E, + ) -> Result, sqlx::Error> where E: sqlx::Executor<'a, Database = sqlx::Postgres>, { @@ -74,7 +77,8 @@ impl Report { { use futures::stream::TryStreamExt; - let report_ids_parsed: Vec = report_ids.iter().map(|x| x.0).collect(); + let report_ids_parsed: Vec = + report_ids.iter().map(|x| x.0).collect(); let reports = sqlx::query!( " SELECT r.id, rt.name, r.mod_id, r.version_id, r.user_id, r.body, r.reporter, r.created, t.id thread_id, r.closed @@ -133,8 +137,11 @@ impl Report { .await?; if let Some(thread_id) = thread_id { - crate::database::models::Thread::remove_full(ThreadId(thread_id.id), transaction) - .await?; + crate::database::models::Thread::remove_full( + ThreadId(thread_id.id), + transaction, + ) + .await?; } sqlx::query!( diff --git a/apps/labrinth/src/database/models/session_item.rs b/apps/labrinth/src/database/models/session_item.rs index 4465d9a65..adb1659ea 100644 --- a/apps/labrinth/src/database/models/session_item.rs +++ b/apps/labrinth/src/database/models/session_item.rs @@ -82,7 +82,11 @@ pub struct Session { } impl Session { - pub async fn get<'a, E, T: Display + Hash + Eq + PartialEq + Clone + Debug>( + pub async fn get< + 'a, + E, + T: Display + Hash + Eq + PartialEq + Clone + Debug, + >( id: T, exec: E, redis: &RedisPool, @@ -103,9 +107,13 @@ impl Session { where E: sqlx::Executor<'a, Database = sqlx::Postgres>, { - Session::get_many(&[crate::models::ids::SessionId::from(id)], executor, redis) - .await - .map(|x| x.into_iter().next()) + Session::get_many( + &[crate::models::ids::SessionId::from(id)], + executor, + redis, + ) + .await + .map(|x| x.into_iter().next()) } pub async fn get_many_ids<'a, E>( @@ -123,7 +131,11 @@ impl Session { Session::get_many(&ids, exec, redis).await } - pub async fn get_many<'a, E, T: Display + Hash + Eq + PartialEq + Clone + Debug>( + pub async fn get_many< + 'a, + E, + T: Display + Hash + Eq + PartialEq + Clone + Debug, + >( session_strings: &[T], exec: E, redis: &RedisPool, @@ -226,14 +238,23 @@ impl Session { .await?; redis - .set_serialized_to_json(SESSIONS_USERS_NAMESPACE, user_id.0, &db_sessions, None) + .set_serialized_to_json( + SESSIONS_USERS_NAMESPACE, + user_id.0, + &db_sessions, + None, + ) .await?; Ok(db_sessions) } pub async fn clear_cache( - clear_sessions: Vec<(Option, Option, Option)>, + clear_sessions: Vec<( + Option, + Option, + Option, + )>, redis: &RedisPool, ) -> Result<(), DatabaseError> { let mut redis = redis.connect().await?; @@ -243,17 +264,18 @@ impl Session { } redis - .delete_many( - clear_sessions - .into_iter() - .flat_map(|(id, session, user_id)| { - [ - (SESSIONS_NAMESPACE, id.map(|i| i.0.to_string())), - (SESSIONS_IDS_NAMESPACE, session), - (SESSIONS_USERS_NAMESPACE, user_id.map(|i| i.0.to_string())), - ] - }), - ) + .delete_many(clear_sessions.into_iter().flat_map( + |(id, session, user_id)| { + [ + (SESSIONS_NAMESPACE, id.map(|i| i.0.to_string())), + (SESSIONS_IDS_NAMESPACE, session), + ( + SESSIONS_USERS_NAMESPACE, + user_id.map(|i| i.0.to_string()), + ), + ] + }, + )) .await?; Ok(()) } diff --git a/apps/labrinth/src/database/models/team_item.rs b/apps/labrinth/src/database/models/team_item.rs index e794602d4..8f6f811ef 100644 --- a/apps/labrinth/src/database/models/team_item.rs +++ b/apps/labrinth/src/database/models/team_item.rs @@ -149,10 +149,12 @@ impl Team { // Only one of project_id or organization_id will be set let mut team_association_id = None; if let Some(pid) = t.pid { - team_association_id = Some(TeamAssociationId::Project(ProjectId(pid))); + team_association_id = + Some(TeamAssociationId::Project(ProjectId(pid))); } if let Some(oid) = t.oid { - team_association_id = Some(TeamAssociationId::Organization(OrganizationId(oid))); + team_association_id = + Some(TeamAssociationId::Organization(OrganizationId(oid))); } return Ok(team_association_id); } @@ -257,7 +259,10 @@ impl TeamMember { Ok(val.into_iter().flatten().collect()) } - pub async fn clear_cache(id: TeamId, redis: &RedisPool) -> Result<(), super::DatabaseError> { + pub async fn clear_cache( + id: TeamId, + redis: &RedisPool, + ) -> Result<(), super::DatabaseError> { let mut redis = redis.connect().await?; redis.delete(TEAMS_NAMESPACE, id.0).await?; Ok(()) @@ -354,11 +359,14 @@ impl TeamMember { user_id, role: m.role, is_owner: m.is_owner, - permissions: ProjectPermissions::from_bits(m.permissions as u64) - .unwrap_or_default(), - organization_permissions: m - .organization_permissions - .map(|p| OrganizationPermissions::from_bits(p as u64).unwrap_or_default()), + permissions: ProjectPermissions::from_bits( + m.permissions as u64, + ) + .unwrap_or_default(), + organization_permissions: m.organization_permissions.map(|p| { + OrganizationPermissions::from_bits(p as u64) + .unwrap_or_default() + }), accepted: m.accepted, payouts_split: m.payouts_split, ordering: m.ordering, @@ -574,11 +582,14 @@ impl TeamMember { user_id, role: m.role, is_owner: m.is_owner, - permissions: ProjectPermissions::from_bits(m.permissions as u64) - .unwrap_or_default(), - organization_permissions: m - .organization_permissions - .map(|p| OrganizationPermissions::from_bits(p as u64).unwrap_or_default()), + permissions: ProjectPermissions::from_bits( + m.permissions as u64, + ) + .unwrap_or_default(), + organization_permissions: m.organization_permissions.map(|p| { + OrganizationPermissions::from_bits(p as u64) + .unwrap_or_default() + }), accepted: m.accepted, payouts_split: m.payouts_split, ordering: m.ordering, @@ -623,11 +634,14 @@ impl TeamMember { user_id, role: m.role, is_owner: m.is_owner, - permissions: ProjectPermissions::from_bits(m.permissions as u64) - .unwrap_or_default(), - organization_permissions: m - .organization_permissions - .map(|p| OrganizationPermissions::from_bits(p as u64).unwrap_or_default()), + permissions: ProjectPermissions::from_bits( + m.permissions as u64, + ) + .unwrap_or_default(), + organization_permissions: m.organization_permissions.map(|p| { + OrganizationPermissions::from_bits(p as u64) + .unwrap_or_default() + }), accepted: m.accepted, payouts_split: m.payouts_split, ordering: m.ordering, @@ -666,11 +680,14 @@ impl TeamMember { user_id, role: m.role, is_owner: m.is_owner, - permissions: ProjectPermissions::from_bits(m.permissions as u64) - .unwrap_or_default(), - organization_permissions: m - .organization_permissions - .map(|p| OrganizationPermissions::from_bits(p as u64).unwrap_or_default()), + permissions: ProjectPermissions::from_bits( + m.permissions as u64, + ) + .unwrap_or_default(), + organization_permissions: m.organization_permissions.map(|p| { + OrganizationPermissions::from_bits(p as u64) + .unwrap_or_default() + }), accepted: m.accepted, payouts_split: m.payouts_split, ordering: m.ordering, @@ -695,10 +712,15 @@ impl TeamMember { Self::get_from_user_id(project.team_id, user_id, executor).await?; let organization = - Organization::get_associated_organization_project_id(project.id, executor).await?; + Organization::get_associated_organization_project_id( + project.id, executor, + ) + .await?; - let organization_team_member = if let Some(organization) = &organization { - Self::get_from_user_id(organization.team_id, user_id, executor).await? + let organization_team_member = if let Some(organization) = &organization + { + Self::get_from_user_id(organization.team_id, user_id, executor) + .await? } else { None }; diff --git a/apps/labrinth/src/database/models/thread_item.rs b/apps/labrinth/src/database/models/thread_item.rs index 73c09b139..38d6cbe43 100644 --- a/apps/labrinth/src/database/models/thread_item.rs +++ b/apps/labrinth/src/database/models/thread_item.rs @@ -112,7 +112,10 @@ impl ThreadBuilder { } impl Thread { - pub async fn get<'a, E>(id: ThreadId, exec: E) -> Result, sqlx::Error> + pub async fn get<'a, E>( + id: ThreadId, + exec: E, + ) -> Result, sqlx::Error> where E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, { @@ -130,7 +133,8 @@ impl Thread { { use futures::stream::TryStreamExt; - let thread_ids_parsed: Vec = thread_ids.iter().map(|x| x.0).collect(); + let thread_ids_parsed: Vec = + thread_ids.iter().map(|x| x.0).collect(); let threads = sqlx::query!( " SELECT t.id, t.thread_type, t.mod_id, t.report_id, @@ -225,7 +229,8 @@ impl ThreadMessage { { use futures::stream::TryStreamExt; - let message_ids_parsed: Vec = message_ids.iter().map(|x| x.0).collect(); + let message_ids_parsed: Vec = + message_ids.iter().map(|x| x.0).collect(); let messages = sqlx::query!( " SELECT tm.id, tm.author_id, tm.thread_id, tm.body, tm.created, tm.hide_identity @@ -261,7 +266,8 @@ impl ThreadMessage { WHERE id = $1 ", id as ThreadMessageId, - serde_json::to_value(MessageBody::Deleted { private }).unwrap_or(serde_json::json!({})) + serde_json::to_value(MessageBody::Deleted { private }) + .unwrap_or(serde_json::json!({})) ) .execute(&mut **transaction) .await?; diff --git a/apps/labrinth/src/database/models/user_item.rs b/apps/labrinth/src/database/models/user_item.rs index 754bc3e3c..ec0809c13 100644 --- a/apps/labrinth/src/database/models/user_item.rs +++ b/apps/labrinth/src/database/models/user_item.rs @@ -135,7 +135,11 @@ impl User { User::get_many(&ids, exec, redis).await } - pub async fn get_many<'a, E, T: Display + Hash + Eq + PartialEq + Clone + Debug>( + pub async fn get_many< + 'a, + E, + T: Display + Hash + Eq + PartialEq + Clone + Debug, + >( users_strings: &[T], exec: E, redis: &RedisPool, @@ -213,7 +217,10 @@ impl User { Ok(val) } - pub async fn get_email<'a, E>(email: &str, exec: E) -> Result, sqlx::Error> + pub async fn get_email<'a, E>( + email: &str, + exec: E, + ) -> Result, sqlx::Error> where E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, { @@ -268,7 +275,12 @@ impl User { .await?; redis - .set_serialized_to_json(USERS_PROJECTS_NAMESPACE, user_id.0, &db_projects, None) + .set_serialized_to_json( + USERS_PROJECTS_NAMESPACE, + user_id.0, + &db_projects, + None, + ) .await?; Ok(db_projects) @@ -323,7 +335,10 @@ impl User { Ok(projects) } - pub async fn get_follows<'a, E>(user_id: UserId, exec: E) -> Result, sqlx::Error> + pub async fn get_follows<'a, E>( + user_id: UserId, + exec: E, + ) -> Result, sqlx::Error> where E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, { @@ -344,7 +359,10 @@ impl User { Ok(projects) } - pub async fn get_reports<'a, E>(user_id: UserId, exec: E) -> Result, sqlx::Error> + pub async fn get_reports<'a, E>( + user_id: UserId, + exec: E, + ) -> Result, sqlx::Error> where E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, { @@ -417,9 +435,9 @@ impl User { redis .delete_many( - user_ids - .iter() - .map(|id| (USERS_PROJECTS_NAMESPACE, Some(id.0.to_string()))), + user_ids.iter().map(|id| { + (USERS_PROJECTS_NAMESPACE, Some(id.0.to_string())) + }), ) .await?; @@ -434,9 +452,11 @@ impl User { let user = Self::get_id(id, &mut **transaction, redis).await?; if let Some(delete_user) = user { - User::clear_caches(&[(id, Some(delete_user.username))], redis).await?; + User::clear_caches(&[(id, Some(delete_user.username))], redis) + .await?; - let deleted_user: UserId = crate::models::users::DELETED_USER.into(); + let deleted_user: UserId = + crate::models::users::DELETED_USER.into(); sqlx::query!( " @@ -509,7 +529,8 @@ impl User { .await?; for collection_id in user_collections { - models::Collection::remove(collection_id, transaction, redis).await?; + models::Collection::remove(collection_id, transaction, redis) + .await?; } let report_threads = sqlx::query!( diff --git a/apps/labrinth/src/database/models/user_subscription_item.rs b/apps/labrinth/src/database/models/user_subscription_item.rs index d83546c8c..edf2e1a59 100644 --- a/apps/labrinth/src/database/models/user_subscription_item.rs +++ b/apps/labrinth/src/database/models/user_subscription_item.rs @@ -1,5 +1,9 @@ -use crate::database::models::{DatabaseError, ProductPriceId, UserId, UserSubscriptionId}; -use crate::models::billing::{PriceDuration, SubscriptionMetadata, SubscriptionStatus}; +use crate::database::models::{ + DatabaseError, ProductPriceId, UserId, UserSubscriptionId, +}; +use crate::models::billing::{ + PriceDuration, SubscriptionMetadata, SubscriptionStatus, +}; use chrono::{DateTime, Utc}; use itertools::Itertools; use std::convert::{TryFrom, TryInto}; @@ -69,10 +73,12 @@ impl UserSubscriptionItem { ) -> Result, DatabaseError> { let ids = ids.iter().map(|id| id.0).collect_vec(); let ids_ref: &[i64] = &ids; - let results = - select_user_subscriptions_with_predicate!("WHERE us.id = ANY($1::bigint[])", ids_ref) - .fetch_all(exec) - .await?; + let results = select_user_subscriptions_with_predicate!( + "WHERE us.id = ANY($1::bigint[])", + ids_ref + ) + .fetch_all(exec) + .await?; Ok(results .into_iter() @@ -85,9 +91,12 @@ impl UserSubscriptionItem { exec: impl sqlx::Executor<'_, Database = sqlx::Postgres>, ) -> Result, DatabaseError> { let user_id = user_id.0; - let results = select_user_subscriptions_with_predicate!("WHERE us.user_id = $1", user_id) - .fetch_all(exec) - .await?; + let results = select_user_subscriptions_with_predicate!( + "WHERE us.user_id = $1", + user_id + ) + .fetch_all(exec) + .await?; Ok(results .into_iter() diff --git a/apps/labrinth/src/database/models/version_item.rs b/apps/labrinth/src/database/models/version_item.rs index e99022479..792c9ac0e 100644 --- a/apps/labrinth/src/database/models/version_item.rs +++ b/apps/labrinth/src/database/models/version_item.rs @@ -220,7 +220,12 @@ impl VersionBuilder { file.insert(version_id, transaction).await?; } - DependencyBuilder::insert_many(dependencies, self.version_id, transaction).await?; + DependencyBuilder::insert_many( + dependencies, + self.version_id, + transaction, + ) + .await?; let loader_versions = loaders .iter() @@ -898,13 +903,20 @@ impl Version { redis .delete_many( - iter::once((VERSIONS_NAMESPACE, Some(version.inner.id.0.to_string()))).chain( - version.files.iter().flat_map(|file| { + iter::once(( + VERSIONS_NAMESPACE, + Some(version.inner.id.0.to_string()), + )) + .chain(version.files.iter().flat_map( + |file| { file.hashes.iter().map(|(algo, hash)| { - (VERSION_FILES_NAMESPACE, Some(format!("{}_{}", algo, hash))) + ( + VERSION_FILES_NAMESPACE, + Some(format!("{}_{}", algo, hash)), + ) }) - }), - ), + }, + )), ) .await?; Ok(()) @@ -1016,7 +1028,11 @@ mod tests { Utc::now().checked_sub_months(Months::new(months)).unwrap() } - fn get_version(id: i64, ordering: Option, date_published: DateTime) -> Version { + fn get_version( + id: i64, + ordering: Option, + date_published: DateTime, + ) -> Version { Version { id: VersionId(id), ordering, diff --git a/apps/labrinth/src/database/postgres_database.rs b/apps/labrinth/src/database/postgres_database.rs index bf7da77ad..65601bde9 100644 --- a/apps/labrinth/src/database/postgres_database.rs +++ b/apps/labrinth/src/database/postgres_database.rs @@ -6,7 +6,8 @@ use std::time::Duration; pub async fn connect() -> Result { info!("Initializing database connection"); - let database_url = dotenvy::var("DATABASE_URL").expect("`DATABASE_URL` not in .env"); + let database_url = + dotenvy::var("DATABASE_URL").expect("`DATABASE_URL` not in .env"); let pool = PgPoolOptions::new() .min_connections( dotenvy::var("DATABASE_MIN_CONNECTIONS") diff --git a/apps/labrinth/src/database/redis.rs b/apps/labrinth/src/database/redis.rs index aa2215216..24ea51c5b 100644 --- a/apps/labrinth/src/database/redis.rs +++ b/apps/labrinth/src/database/redis.rs @@ -32,18 +32,20 @@ impl RedisPool { // testing pool uses a hashmap to mimic redis behaviour for very small data sizes (ie: tests) // PANICS: production pool will panic if redis url is not set pub fn new(meta_namespace: Option) -> Self { - let redis_pool = Config::from_url(dotenvy::var("REDIS_URL").expect("Redis URL not set")) - .builder() - .expect("Error building Redis pool") - .max_size( - dotenvy::var("DATABASE_MAX_CONNECTIONS") - .ok() - .and_then(|x| x.parse().ok()) - .unwrap_or(10000), - ) - .runtime(Runtime::Tokio1) - .build() - .expect("Redis connection failed"); + let redis_pool = Config::from_url( + dotenvy::var("REDIS_URL").expect("Redis URL not set"), + ) + .builder() + .expect("Error building Redis pool") + .max_size( + dotenvy::var("DATABASE_MAX_CONNECTIONS") + .ok() + .and_then(|x| x.parse().ok()) + .unwrap_or(10000), + ) + .runtime(Runtime::Tokio1) + .build() + .expect("Redis connection failed"); RedisPool { pool: redis_pool, @@ -68,7 +70,14 @@ impl RedisPool { F: FnOnce(Vec) -> Fut, Fut: Future, DatabaseError>>, T: Serialize + DeserializeOwned, - K: Display + Hash + Eq + PartialEq + Clone + DeserializeOwned + Serialize + Debug, + K: Display + + Hash + + Eq + + PartialEq + + Clone + + DeserializeOwned + + Serialize + + Debug, { Ok(self .get_cached_keys_raw(namespace, keys, closure) @@ -88,15 +97,28 @@ impl RedisPool { F: FnOnce(Vec) -> Fut, Fut: Future, DatabaseError>>, T: Serialize + DeserializeOwned, - K: Display + Hash + Eq + PartialEq + Clone + DeserializeOwned + Serialize + Debug, + K: Display + + Hash + + Eq + + PartialEq + + Clone + + DeserializeOwned + + Serialize + + Debug, { - self.get_cached_keys_raw_with_slug(namespace, None, false, keys, |ids| async move { - Ok(closure(ids) - .await? - .into_iter() - .map(|(key, val)| (key, (None::, val))) - .collect()) - }) + self.get_cached_keys_raw_with_slug( + namespace, + None, + false, + keys, + |ids| async move { + Ok(closure(ids) + .await? + .into_iter() + .map(|(key, val)| (key, (None::, val))) + .collect()) + }, + ) .await } @@ -113,7 +135,13 @@ impl RedisPool { Fut: Future, T)>, DatabaseError>>, T: Serialize + DeserializeOwned, I: Display + Hash + Eq + PartialEq + Clone + Debug, - K: Display + Hash + Eq + PartialEq + Clone + DeserializeOwned + Serialize, + K: Display + + Hash + + Eq + + PartialEq + + Clone + + DeserializeOwned + + Serialize, S: Display + Clone + DeserializeOwned + Serialize + Debug, { Ok(self @@ -143,7 +171,13 @@ impl RedisPool { Fut: Future, T)>, DatabaseError>>, T: Serialize + DeserializeOwned, I: Display + Hash + Eq + PartialEq + Clone + Debug, - K: Display + Hash + Eq + PartialEq + Clone + DeserializeOwned + Serialize, + K: Display + + Hash + + Eq + + PartialEq + + Clone + + DeserializeOwned + + Serialize, S: Display + Clone + DeserializeOwned + Serialize + Debug, { let connection = self.connect().await?.connection; @@ -158,7 +192,8 @@ impl RedisPool { } let get_cached_values = - |ids: DashMap, mut connection: deadpool_redis::Connection| async move { + |ids: DashMap, + mut connection: deadpool_redis::Connection| async move { let slug_ids = if let Some(slug_namespace) = slug_namespace { cmd("MGET") .arg( @@ -176,7 +211,7 @@ impl RedisPool { }) .collect::>(), ) - .query_async::<_, Vec>>(&mut connection) + .query_async::>>(&mut connection) .await? .into_iter() .flatten() @@ -195,15 +230,23 @@ impl RedisPool { .map(|x| x.to_string()) })) .chain(slug_ids) - .map(|x| format!("{}_{namespace}:{x}", self.meta_namespace)) + .map(|x| { + format!( + "{}_{namespace}:{x}", + self.meta_namespace + ) + }) .collect::>(), ) - .query_async::<_, Vec>>(&mut connection) + .query_async::>>(&mut connection) .await? .into_iter() .filter_map(|x| { - x.and_then(|val| serde_json::from_str::>(&val).ok()) - .map(|val| (val.key.clone(), val)) + x.and_then(|val| { + serde_json::from_str::>(&val) + .ok() + }) + .map(|val| (val.key.clone(), val)) }) .collect::>(); @@ -213,11 +256,14 @@ impl RedisPool { let current_time = Utc::now(); let mut expired_values = HashMap::new(); - let (cached_values_raw, mut connection, ids) = get_cached_values(ids, connection).await?; + let (cached_values_raw, mut connection, ids) = + get_cached_values(ids, connection).await?; let mut cached_values = cached_values_raw .into_iter() .filter_map(|(key, val)| { - if Utc.timestamp_opt(val.iat + ACTUAL_EXPIRY, 0).unwrap() < current_time { + if Utc.timestamp_opt(val.iat + ACTUAL_EXPIRY, 0).unwrap() + < current_time + { expired_values.insert(val.key.to_string(), val); None @@ -244,7 +290,8 @@ impl RedisPool { if !ids.is_empty() { let mut pipe = redis::pipe(); - let fetch_ids = ids.iter().map(|x| x.key().clone()).collect::>(); + let fetch_ids = + ids.iter().map(|x| x.key().clone()).collect::>(); fetch_ids.iter().for_each(|key| { pipe.atomic().set_options( @@ -257,7 +304,7 @@ impl RedisPool { ); }); let results = pipe - .query_async::<_, Vec>>(&mut connection) + .query_async::>>(&mut connection) .await?; for (idx, key) in fetch_ids.into_iter().enumerate() { @@ -288,12 +335,22 @@ impl RedisPool { #[allow(clippy::type_complexity)] let mut fetch_tasks: Vec< - Pin>, DatabaseError>>>>, + Pin< + Box< + dyn Future< + Output = Result< + HashMap>, + DatabaseError, + >, + >, + >, + >, > = Vec::new(); if !ids.is_empty() { fetch_tasks.push(Box::pin(async { - let fetch_ids = ids.iter().map(|x| x.value().clone()).collect::>(); + let fetch_ids = + ids.iter().map(|x| x.value().clone()).collect::>(); let vals = closure(fetch_ids).await?; let mut return_values = HashMap::new(); @@ -309,7 +366,10 @@ impl RedisPool { }; pipe.atomic().set_ex( - format!("{}_{namespace}:{key}", self.meta_namespace), + format!( + "{}_{namespace}:{key}", + self.meta_namespace + ), serde_json::to_string(&value)?, DEFAULT_EXPIRY as u64, ); @@ -347,23 +407,29 @@ impl RedisPool { let base62 = to_base62(value); ids.remove(&base62); - pipe.atomic() - .del(format!("{}_{namespace}:{base62}/lock", self.meta_namespace)); + pipe.atomic().del(format!( + "{}_{namespace}:{base62}/lock", + self.meta_namespace + )); } - pipe.atomic() - .del(format!("{}_{namespace}:{key}/lock", self.meta_namespace)); + pipe.atomic().del(format!( + "{}_{namespace}:{key}/lock", + self.meta_namespace + )); return_values.insert(key, value); } } for (key, _) in ids { - pipe.atomic() - .del(format!("{}_{namespace}:{key}/lock", self.meta_namespace)); + pipe.atomic().del(format!( + "{}_{namespace}:{key}/lock", + self.meta_namespace + )); } - pipe.query_async(&mut connection).await?; + pipe.query_async::<()>(&mut connection).await?; Ok(return_values) })); @@ -373,7 +439,8 @@ impl RedisPool { fetch_tasks.push(Box::pin(async { let mut connection = self.pool.get().await?; - let mut interval = tokio::time::interval(Duration::from_millis(100)); + let mut interval = + tokio::time::interval(Duration::from_millis(100)); let start = Utc::now(); loop { let results = cmd("MGET") @@ -381,11 +448,15 @@ impl RedisPool { subscribe_ids .iter() .map(|x| { - format!("{}_{namespace}:{}/lock", self.meta_namespace, x.key()) + format!( + "{}_{namespace}:{}/lock", + self.meta_namespace, + x.key() + ) }) .collect::>(), ) - .query_async::<_, Vec>>(&mut connection) + .query_async::>>(&mut connection) .await?; if results.into_iter().all(|x| x.is_none()) { @@ -399,7 +470,8 @@ impl RedisPool { interval.tick().await; } - let (return_values, _, _) = get_cached_values(subscribe_ids, connection).await?; + let (return_values, _, _) = + get_cached_values(subscribe_ids, connection).await?; Ok(return_values) })); @@ -436,7 +508,7 @@ impl RedisConnection { ] .as_slice(), ); - redis_execute(&mut cmd, &mut self.connection).await?; + redis_execute::<()>(&mut cmd, &mut self.connection).await?; Ok(()) } @@ -468,7 +540,8 @@ impl RedisConnection { let mut cmd = cmd("GET"); redis_args( &mut cmd, - vec![format!("{}_{}:{}", self.meta_namespace, namespace, id)].as_slice(), + vec![format!("{}_{}:{}", self.meta_namespace, namespace, id)] + .as_slice(), ); let res = redis_execute(&mut cmd, &mut self.connection).await?; Ok(res) @@ -488,16 +561,21 @@ impl RedisConnection { .and_then(|x| serde_json::from_str(&x).ok())) } - pub async fn delete(&mut self, namespace: &str, id: T1) -> Result<(), DatabaseError> + pub async fn delete( + &mut self, + namespace: &str, + id: T1, + ) -> Result<(), DatabaseError> where T1: Display, { let mut cmd = cmd("DEL"); redis_args( &mut cmd, - vec![format!("{}_{}:{}", self.meta_namespace, namespace, id)].as_slice(), + vec![format!("{}_{}:{}", self.meta_namespace, namespace, id)] + .as_slice(), ); - redis_execute(&mut cmd, &mut self.connection).await?; + redis_execute::<()>(&mut cmd, &mut self.connection).await?; Ok(()) } @@ -511,14 +589,15 @@ impl RedisConnection { if let Some(id) = id { redis_args( &mut cmd, - [format!("{}_{}:{}", self.meta_namespace, namespace, id)].as_slice(), + [format!("{}_{}:{}", self.meta_namespace, namespace, id)] + .as_slice(), ); any = true; } } if any { - redis_execute(&mut cmd, &mut self.connection).await?; + redis_execute::<()>(&mut cmd, &mut self.connection).await?; } Ok(()) @@ -547,6 +626,6 @@ pub async fn redis_execute( where T: redis::FromRedisValue, { - let res = cmd.query_async::<_, T>(redis).await?; + let res = cmd.query_async::(redis).await?; Ok(res) } diff --git a/apps/labrinth/src/file_hosting/backblaze.rs b/apps/labrinth/src/file_hosting/backblaze.rs index 57e425542..28d302245 100644 --- a/apps/labrinth/src/file_hosting/backblaze.rs +++ b/apps/labrinth/src/file_hosting/backblaze.rs @@ -16,10 +16,12 @@ pub struct BackblazeHost { impl BackblazeHost { pub async fn new(key_id: &str, key: &str, bucket_id: &str) -> Self { - let authorization_data = authorization::authorize_account(key_id, key).await.unwrap(); - let upload_url_data = authorization::get_upload_url(&authorization_data, bucket_id) - .await - .unwrap(); + let authorization_data = + authorization::authorize_account(key_id, key).await.unwrap(); + let upload_url_data = + authorization::get_upload_url(&authorization_data, bucket_id) + .await + .unwrap(); BackblazeHost { upload_url_data, @@ -38,8 +40,13 @@ impl FileHost for BackblazeHost { ) -> Result { let content_sha512 = format!("{:x}", sha2::Sha512::digest(&file_bytes)); - let upload_data = - upload::upload_file(&self.upload_url_data, content_type, file_name, file_bytes).await?; + let upload_data = upload::upload_file( + &self.upload_url_data, + content_type, + file_name, + file_bytes, + ) + .await?; Ok(UploadFileData { file_id: upload_data.file_id, file_name: upload_data.file_name, @@ -74,8 +81,12 @@ impl FileHost for BackblazeHost { file_id: &str, file_name: &str, ) -> Result { - let delete_data = - delete::delete_file_version(&self.authorization_data, file_id, file_name).await?; + let delete_data = delete::delete_file_version( + &self.authorization_data, + file_id, + file_name, + ) + .await?; Ok(DeleteFileData { file_id: delete_data.file_id, file_name: delete_data.file_name, @@ -83,7 +94,9 @@ impl FileHost for BackblazeHost { } } -pub async fn process_response(response: Response) -> Result +pub async fn process_response( + response: Response, +) -> Result where T: for<'de> Deserialize<'de>, { diff --git a/apps/labrinth/src/file_hosting/backblaze/authorization.rs b/apps/labrinth/src/file_hosting/backblaze/authorization.rs index 625d0ee80..9ab9e5982 100644 --- a/apps/labrinth/src/file_hosting/backblaze/authorization.rs +++ b/apps/labrinth/src/file_hosting/backblaze/authorization.rs @@ -56,7 +56,13 @@ pub async fn get_upload_url( bucket_id: &str, ) -> Result { let response = reqwest::Client::new() - .post(format!("{}/b2api/v2/b2_get_upload_url", authorization_data.api_url).to_string()) + .post( + format!( + "{}/b2api/v2/b2_get_upload_url", + authorization_data.api_url + ) + .to_string(), + ) .header(reqwest::header::CONTENT_TYPE, "application/json") .header( reqwest::header::AUTHORIZATION, diff --git a/apps/labrinth/src/file_hosting/mock.rs b/apps/labrinth/src/file_hosting/mock.rs index 625795435..f520bd11a 100644 --- a/apps/labrinth/src/file_hosting/mock.rs +++ b/apps/labrinth/src/file_hosting/mock.rs @@ -21,9 +21,12 @@ impl FileHost for MockHost { file_name: &str, file_bytes: Bytes, ) -> Result { - let path = std::path::Path::new(&dotenvy::var("MOCK_FILE_PATH").unwrap()) - .join(file_name.replace("../", "")); - std::fs::create_dir_all(path.parent().ok_or(FileHostingError::InvalidFilename)?)?; + let path = + std::path::Path::new(&dotenvy::var("MOCK_FILE_PATH").unwrap()) + .join(file_name.replace("../", "")); + std::fs::create_dir_all( + path.parent().ok_or(FileHostingError::InvalidFilename)?, + )?; let content_sha1 = sha1::Sha1::from(&file_bytes).hexdigest(); let content_sha512 = format!("{:x}", sha2::Sha512::digest(&file_bytes)); @@ -45,8 +48,9 @@ impl FileHost for MockHost { file_id: &str, file_name: &str, ) -> Result { - let path = std::path::Path::new(&dotenvy::var("MOCK_FILE_PATH").unwrap()) - .join(file_name.replace("../", "")); + let path = + std::path::Path::new(&dotenvy::var("MOCK_FILE_PATH").unwrap()) + .join(file_name.replace("../", "")); if path.exists() { std::fs::remove_file(path)?; } diff --git a/apps/labrinth/src/file_hosting/s3_host.rs b/apps/labrinth/src/file_hosting/s3_host.rs index 67fdb824b..87be229ab 100644 --- a/apps/labrinth/src/file_hosting/s3_host.rs +++ b/apps/labrinth/src/file_hosting/s3_host.rs @@ -1,4 +1,6 @@ -use crate::file_hosting::{DeleteFileData, FileHost, FileHostingError, UploadFileData}; +use crate::file_hosting::{ + DeleteFileData, FileHost, FileHostingError, UploadFileData, +}; use async_trait::async_trait; use bytes::Bytes; use chrono::Utc; @@ -31,12 +33,23 @@ impl S3Host { endpoint: url.to_string(), } }, - Credentials::new(Some(access_token), Some(secret), None, None, None).map_err(|_| { - FileHostingError::S3Error("Error while creating credentials".to_string()) + Credentials::new( + Some(access_token), + Some(secret), + None, + None, + None, + ) + .map_err(|_| { + FileHostingError::S3Error( + "Error while creating credentials".to_string(), + ) })?, ) .map_err(|_| { - FileHostingError::S3Error("Error while creating Bucket instance".to_string()) + FileHostingError::S3Error( + "Error while creating Bucket instance".to_string(), + ) })?; Ok(S3Host { bucket }) @@ -55,10 +68,16 @@ impl FileHost for S3Host { let content_sha512 = format!("{:x}", sha2::Sha512::digest(&file_bytes)); self.bucket - .put_object_with_content_type(format!("/{file_name}"), &file_bytes, content_type) + .put_object_with_content_type( + format!("/{file_name}"), + &file_bytes, + content_type, + ) .await .map_err(|_| { - FileHostingError::S3Error("Error while uploading file to S3".to_string()) + FileHostingError::S3Error( + "Error while uploading file to S3".to_string(), + ) })?; Ok(UploadFileData { @@ -82,7 +101,9 @@ impl FileHost for S3Host { .delete_object(format!("/{file_name}")) .await .map_err(|_| { - FileHostingError::S3Error("Error while deleting file from S3".to_string()) + FileHostingError::S3Error( + "Error while deleting file from S3".to_string(), + ) })?; Ok(DeleteFileData { diff --git a/apps/labrinth/src/lib.rs b/apps/labrinth/src/lib.rs index 8d4bcc06b..9bfc70ba5 100644 --- a/apps/labrinth/src/lib.rs +++ b/apps/labrinth/src/lib.rs @@ -6,7 +6,8 @@ use actix_web::web; use database::redis::RedisPool; use log::{info, warn}; use queue::{ - analytics::AnalyticsQueue, payouts::PayoutsQueue, session::AuthQueue, socket::ActiveSockets, + analytics::AnalyticsQueue, payouts::PayoutsQueue, session::AuthQueue, + socket::ActiveSockets, }; use sqlx::Postgres; use tokio::sync::RwLock; @@ -74,7 +75,8 @@ pub fn app_setup( dotenvy::var("BIND_ADDR").unwrap() ); - let automated_moderation_queue = web::Data::new(AutomatedModerationQueue::default()); + let automated_moderation_queue = + web::Data::new(AutomatedModerationQueue::default()); { let automated_moderation_queue_ref = automated_moderation_queue.clone(); @@ -110,8 +112,9 @@ pub fn app_setup( // The interval in seconds at which the local database is indexed // for searching. Defaults to 1 hour if unset. - let local_index_interval = - std::time::Duration::from_secs(parse_var("LOCAL_INDEX_INTERVAL").unwrap_or(3600)); + let local_index_interval = std::time::Duration::from_secs( + parse_var("LOCAL_INDEX_INTERVAL").unwrap_or(3600), + ); let pool_ref = pool.clone(); let search_config_ref = search_config.clone(); @@ -122,7 +125,12 @@ pub fn app_setup( let search_config_ref = search_config_ref.clone(); async move { info!("Indexing local database"); - let result = index_projects(pool_ref, redis_pool_ref.clone(), &search_config_ref).await; + let result = index_projects( + pool_ref, + redis_pool_ref.clone(), + &search_config_ref, + ) + .await; if let Err(e) = result { warn!("Local project indexing failed: {:?}", e); } @@ -172,7 +180,11 @@ pub fn app_setup( } }); - scheduler::schedule_versions(&mut scheduler, pool.clone(), redis_pool.clone()); + scheduler::schedule_versions( + &mut scheduler, + pool.clone(), + redis_pool.clone(), + ); let session_queue = web::Data::new(AuthQueue::new()); @@ -258,14 +270,20 @@ pub fn app_setup( }); } - let stripe_client = stripe::Client::new(dotenvy::var("STRIPE_API_KEY").unwrap()); + let stripe_client = + stripe::Client::new(dotenvy::var("STRIPE_API_KEY").unwrap()); { let pool_ref = pool.clone(); let redis_ref = redis_pool.clone(); let stripe_client_ref = stripe_client.clone(); actix_rt::spawn(async move { - routes::internal::billing::task(stripe_client_ref, pool_ref, redis_ref).await; + routes::internal::billing::task( + stripe_client_ref, + pool_ref, + redis_ref, + ) + .await; }); } @@ -274,12 +292,14 @@ pub fn app_setup( let redis_ref = redis_pool.clone(); actix_rt::spawn(async move { - routes::internal::billing::subscription_task(pool_ref, redis_ref).await; + routes::internal::billing::subscription_task(pool_ref, redis_ref) + .await; }); } let ip_salt = Pepper { - pepper: models::ids::Base62Id(models::ids::random_base62(11)).to_string(), + pepper: models::ids::Base62Id(models::ids::random_base62(11)) + .to_string(), }; let payouts_queue = web::Data::new(PayoutsQueue::new()); @@ -304,23 +324,22 @@ pub fn app_setup( } } -pub fn app_config(cfg: &mut web::ServiceConfig, labrinth_config: LabrinthConfig) { - cfg.app_data( - web::FormConfig::default() - .error_handler(|err, _req| routes::ApiError::Validation(err.to_string()).into()), - ) - .app_data( - web::PathConfig::default() - .error_handler(|err, _req| routes::ApiError::Validation(err.to_string()).into()), - ) - .app_data( - web::QueryConfig::default() - .error_handler(|err, _req| routes::ApiError::Validation(err.to_string()).into()), - ) - .app_data( - web::JsonConfig::default() - .error_handler(|err, _req| routes::ApiError::Validation(err.to_string()).into()), - ) +pub fn app_config( + cfg: &mut web::ServiceConfig, + labrinth_config: LabrinthConfig, +) { + cfg.app_data(web::FormConfig::default().error_handler(|err, _req| { + routes::ApiError::Validation(err.to_string()).into() + })) + .app_data(web::PathConfig::default().error_handler(|err, _req| { + routes::ApiError::Validation(err.to_string()).into() + })) + .app_data(web::QueryConfig::default().error_handler(|err, _req| { + routes::ApiError::Validation(err.to_string()).into() + })) + .app_data(web::JsonConfig::default().error_handler(|err, _req| { + routes::ApiError::Validation(err.to_string()).into() + })) .app_data(web::Data::new(labrinth_config.redis_pool.clone())) .app_data(web::Data::new(labrinth_config.pool.clone())) .app_data(web::Data::new(labrinth_config.file_host.clone())) diff --git a/apps/labrinth/src/main.rs b/apps/labrinth/src/main.rs index f113a6ec6..336150c8f 100644 --- a/apps/labrinth/src/main.rs +++ b/apps/labrinth/src/main.rs @@ -21,7 +21,8 @@ pub struct Pepper { #[actix_rt::main] async fn main() -> std::io::Result<()> { dotenvy::dotenv().ok(); - env_logger::Builder::from_env(Env::default().default_filter_or("info")).init(); + env_logger::Builder::from_env(Env::default().default_filter_or("info")) + .init(); if check_env_vars() { error!("Some environment variables are missing!"); @@ -56,35 +57,38 @@ async fn main() -> std::io::Result<()> { // Redis connector let redis_pool = RedisPool::new(None); - let storage_backend = dotenvy::var("STORAGE_BACKEND").unwrap_or_else(|_| "local".to_string()); - - let file_host: Arc = match storage_backend.as_str() { - "backblaze" => Arc::new( - file_hosting::BackblazeHost::new( - &dotenvy::var("BACKBLAZE_KEY_ID").unwrap(), - &dotenvy::var("BACKBLAZE_KEY").unwrap(), - &dotenvy::var("BACKBLAZE_BUCKET_ID").unwrap(), - ) - .await, - ), - "s3" => Arc::new( - S3Host::new( - &dotenvy::var("S3_BUCKET_NAME").unwrap(), - &dotenvy::var("S3_REGION").unwrap(), - &dotenvy::var("S3_URL").unwrap(), - &dotenvy::var("S3_ACCESS_TOKEN").unwrap(), - &dotenvy::var("S3_SECRET").unwrap(), - ) - .unwrap(), - ), - "local" => Arc::new(file_hosting::MockHost::new()), - _ => panic!("Invalid storage backend specified. Aborting startup!"), - }; + let storage_backend = + dotenvy::var("STORAGE_BACKEND").unwrap_or_else(|_| "local".to_string()); + + let file_host: Arc = + match storage_backend.as_str() { + "backblaze" => Arc::new( + file_hosting::BackblazeHost::new( + &dotenvy::var("BACKBLAZE_KEY_ID").unwrap(), + &dotenvy::var("BACKBLAZE_KEY").unwrap(), + &dotenvy::var("BACKBLAZE_BUCKET_ID").unwrap(), + ) + .await, + ), + "s3" => Arc::new( + S3Host::new( + &dotenvy::var("S3_BUCKET_NAME").unwrap(), + &dotenvy::var("S3_REGION").unwrap(), + &dotenvy::var("S3_URL").unwrap(), + &dotenvy::var("S3_ACCESS_TOKEN").unwrap(), + &dotenvy::var("S3_SECRET").unwrap(), + ) + .unwrap(), + ), + "local" => Arc::new(file_hosting::MockHost::new()), + _ => panic!("Invalid storage backend specified. Aborting startup!"), + }; info!("Initializing clickhouse connection"); let mut clickhouse = clickhouse::init_client().await.unwrap(); - let maxmind_reader = Arc::new(queue::maxmind::MaxMindIndexer::new().await.unwrap()); + let maxmind_reader = + Arc::new(queue::maxmind::MaxMindIndexer::new().await.unwrap()); let prometheus = PrometheusMetricsBuilder::new("labrinth") .endpoint("/metrics") diff --git a/apps/labrinth/src/models/v2/notifications.rs b/apps/labrinth/src/models/v2/notifications.rs index 56f966f67..6e4166a56 100644 --- a/apps/labrinth/src/models/v2/notifications.rs +++ b/apps/labrinth/src/models/v2/notifications.rs @@ -3,8 +3,8 @@ use serde::{Deserialize, Serialize}; use crate::models::{ ids::{ - NotificationId, OrganizationId, ProjectId, ReportId, TeamId, ThreadId, ThreadMessageId, - UserId, VersionId, + NotificationId, OrganizationId, ProjectId, ReportId, TeamId, ThreadId, + ThreadMessageId, UserId, VersionId, }, notifications::{Notification, NotificationAction, NotificationBody}, projects::ProjectStatus, @@ -78,11 +78,21 @@ pub enum LegacyNotificationBody { impl LegacyNotification { pub fn from(notification: Notification) -> Self { let type_ = match ¬ification.body { - NotificationBody::ProjectUpdate { .. } => Some("project_update".to_string()), - NotificationBody::TeamInvite { .. } => Some("team_invite".to_string()), - NotificationBody::OrganizationInvite { .. } => Some("organization_invite".to_string()), - NotificationBody::StatusChange { .. } => Some("status_change".to_string()), - NotificationBody::ModeratorMessage { .. } => Some("moderator_message".to_string()), + NotificationBody::ProjectUpdate { .. } => { + Some("project_update".to_string()) + } + NotificationBody::TeamInvite { .. } => { + Some("team_invite".to_string()) + } + NotificationBody::OrganizationInvite { .. } => { + Some("organization_invite".to_string()) + } + NotificationBody::StatusChange { .. } => { + Some("status_change".to_string()) + } + NotificationBody::ModeratorMessage { .. } => { + Some("moderator_message".to_string()) + } NotificationBody::LegacyMarkdown { notification_type, .. } => notification_type.clone(), diff --git a/apps/labrinth/src/models/v2/projects.rs b/apps/labrinth/src/models/v2/projects.rs index d87601cc5..a96d51707 100644 --- a/apps/labrinth/src/models/v2/projects.rs +++ b/apps/labrinth/src/models/v2/projects.rs @@ -9,8 +9,8 @@ use crate::database::models::{version_item, DatabaseError}; use crate::database::redis::RedisPool; use crate::models::ids::{ProjectId, VersionId}; use crate::models::projects::{ - Dependency, License, Link, Loader, ModeratorMessage, MonetizationStatus, Project, - ProjectStatus, Version, VersionFile, VersionStatus, VersionType, + Dependency, License, Link, Loader, ModeratorMessage, MonetizationStatus, + Project, ProjectStatus, Version, VersionFile, VersionStatus, VersionType, }; use crate::models::threads::ThreadId; use crate::routes::v2_reroute::{self, capitalize_first}; @@ -87,12 +87,13 @@ impl LegacyProject { .cloned() .unwrap_or("project".to_string()); // Default to 'project' if none are found - let project_type = if og_project_type == "datapack" || og_project_type == "plugin" { - // These are not supported in V2, so we'll just use 'mod' instead - "mod".to_string() - } else { - og_project_type.clone() - }; + let project_type = + if og_project_type == "datapack" || og_project_type == "plugin" { + // These are not supported in V2, so we'll just use 'mod' instead + "mod".to_string() + } else { + og_project_type.clone() + }; (project_type, og_project_type) } @@ -102,7 +103,10 @@ impl LegacyProject { // - This can be any version, because the fields are ones that used to be on the project itself. // - Its conceivable that certain V3 projects that have many different ones may not have the same fields on all of them. // It's safe to use a db version_item for this as the only info is side types, game versions, and loader fields (for loaders), which used to be public on project anyway. - pub fn from(data: Project, versions_item: Option) -> Self { + pub fn from( + data: Project, + versions_item: Option, + ) -> Self { let mut client_side = LegacySideType::Unknown; let mut server_side = LegacySideType::Unknown; @@ -110,7 +114,8 @@ impl LegacyProject { // We'll prioritize 'modpack' first, and if neither are found, use the first one. // If there are no project types, default to 'project' let project_types = data.project_types; - let (mut project_type, og_project_type) = Self::get_project_type(&project_types); + let (mut project_type, og_project_type) = + Self::get_project_type(&project_types); let mut loaders = data.loaders; @@ -128,16 +133,22 @@ impl LegacyProject { let fields = versions_item .version_fields .iter() - .map(|f| (f.field_name.clone(), f.value.clone().serialize_internal())) + .map(|f| { + (f.field_name.clone(), f.value.clone().serialize_internal()) + }) .collect::>(); - (client_side, server_side) = - v2_reroute::convert_side_types_v2(&fields, Some(&*og_project_type)); + (client_side, server_side) = v2_reroute::convert_side_types_v2( + &fields, + Some(&*og_project_type), + ); // - if loader is mrpack, this is a modpack // the loaders are whatever the corresponding loader fields are if loaders.contains(&"mrpack".to_string()) { project_type = "modpack".to_string(); - if let Some(mrpack_loaders) = data.fields.iter().find(|f| f.0 == "mrpack_loaders") { + if let Some(mrpack_loaders) = + data.fields.iter().find(|f| f.0 == "mrpack_loaders") + { let values = mrpack_loaders .1 .iter() @@ -227,7 +238,8 @@ impl LegacyProject { .iter() .filter_map(|p| p.versions.first().map(|i| (*i).into())) .collect(); - let example_versions = version_item::Version::get_many(&version_ids, exec, redis).await?; + let example_versions = + version_item::Version::get_many(&version_ids, exec, redis).await?; let mut legacy_projects = Vec::new(); for project in data { let version_item = example_versions @@ -308,7 +320,9 @@ pub struct LegacyVersion { impl From for LegacyVersion { fn from(data: Version) -> Self { let mut game_versions = Vec::new(); - if let Some(value) = data.fields.get("game_versions").and_then(|v| v.as_array()) { + if let Some(value) = + data.fields.get("game_versions").and_then(|v| v.as_array()) + { for gv in value { if let Some(game_version) = gv.as_str() { game_versions.push(game_version.to_string()); @@ -318,14 +332,17 @@ impl From for LegacyVersion { // - if loader is mrpack, this is a modpack // the v2 loaders are whatever the corresponding loader fields are - let mut loaders = data.loaders.into_iter().map(|l| l.0).collect::>(); + let mut loaders = + data.loaders.into_iter().map(|l| l.0).collect::>(); if loaders.contains(&"mrpack".to_string()) { if let Some((_, mrpack_loaders)) = data .fields .into_iter() .find(|(key, _)| key == "mrpack_loaders") { - if let Ok(mrpack_loaders) = serde_json::from_value(mrpack_loaders) { + if let Ok(mrpack_loaders) = + serde_json::from_value(mrpack_loaders) + { loaders = mrpack_loaders; } } diff --git a/apps/labrinth/src/models/v2/search.rs b/apps/labrinth/src/models/v2/search.rs index 354c0b68f..dfc9356b7 100644 --- a/apps/labrinth/src/models/v2/search.rs +++ b/apps/labrinth/src/models/v2/search.rs @@ -92,14 +92,16 @@ impl LegacyResultSearchProject { .cloned() .unwrap_or("project".to_string()); // Default to 'project' if none are found - let project_type = if og_project_type == "datapack" || og_project_type == "plugin" { - // These are not supported in V2, so we'll just use 'mod' instead - "mod".to_string() - } else { - og_project_type.clone() - }; + let project_type = + if og_project_type == "datapack" || og_project_type == "plugin" { + // These are not supported in V2, so we'll just use 'mod' instead + "mod".to_string() + } else { + og_project_type.clone() + }; - let project_loader_fields = result_search_project.project_loader_fields.clone(); + let project_loader_fields = + result_search_project.project_loader_fields.clone(); let get_one_bool_loader_field = |key: &str| { project_loader_fields .get(key) @@ -110,17 +112,20 @@ impl LegacyResultSearchProject { }; let singleplayer = get_one_bool_loader_field("singleplayer"); - let client_only = get_one_bool_loader_field("client_only").unwrap_or(false); - let server_only = get_one_bool_loader_field("server_only").unwrap_or(false); + let client_only = + get_one_bool_loader_field("client_only").unwrap_or(false); + let server_only = + get_one_bool_loader_field("server_only").unwrap_or(false); let client_and_server = get_one_bool_loader_field("client_and_server"); - let (client_side, server_side) = v2_reroute::convert_side_types_v2_bools( - singleplayer, - client_only, - server_only, - client_and_server, - Some(&*og_project_type), - ); + let (client_side, server_side) = + v2_reroute::convert_side_types_v2_bools( + singleplayer, + client_only, + server_only, + client_and_server, + Some(&*og_project_type), + ); let client_side = client_side.to_string(); let server_side = server_side.to_string(); diff --git a/apps/labrinth/src/models/v2/threads.rs b/apps/labrinth/src/models/v2/threads.rs index 70654b84e..064be2802 100644 --- a/apps/labrinth/src/models/v2/threads.rs +++ b/apps/labrinth/src/models/v2/threads.rs @@ -1,4 +1,6 @@ -use crate::models::ids::{ImageId, ProjectId, ReportId, ThreadId, ThreadMessageId}; +use crate::models::ids::{ + ImageId, ProjectId, ReportId, ThreadId, ThreadMessageId, +}; use crate::models::projects::ProjectStatus; use crate::models::users::{User, UserId}; use chrono::{DateTime, Utc}; @@ -57,8 +59,12 @@ pub enum LegacyThreadType { impl From for LegacyThreadType { fn from(t: crate::models::v3::threads::ThreadType) -> Self { match t { - crate::models::v3::threads::ThreadType::Report => LegacyThreadType::Report, - crate::models::v3::threads::ThreadType::Project => LegacyThreadType::Project, + crate::models::v3::threads::ThreadType::Report => { + LegacyThreadType::Report + } + crate::models::v3::threads::ThreadType::Project => { + LegacyThreadType::Project + } crate::models::v3::threads::ThreadType::DirectMessage => { LegacyThreadType::DirectMessage } diff --git a/apps/labrinth/src/models/v3/billing.rs b/apps/labrinth/src/models/v3/billing.rs index 4c9d3b778..afe879019 100644 --- a/apps/labrinth/src/models/v3/billing.rs +++ b/apps/labrinth/src/models/v3/billing.rs @@ -106,7 +106,9 @@ pub struct UserSubscription { impl From for UserSubscription { - fn from(x: crate::database::models::user_subscription_item::UserSubscriptionItem) -> Self { + fn from( + x: crate::database::models::user_subscription_item::UserSubscriptionItem, + ) -> Self { Self { id: x.id.into(), user_id: x.user_id.into(), diff --git a/apps/labrinth/src/models/v3/ids.rs b/apps/labrinth/src/models/v3/ids.rs index 3af874379..5a2997c80 100644 --- a/apps/labrinth/src/models/v3/ids.rs +++ b/apps/labrinth/src/models/v3/ids.rs @@ -13,7 +13,9 @@ pub use super::teams::TeamId; pub use super::threads::ThreadId; pub use super::threads::ThreadMessageId; pub use super::users::UserId; -pub use crate::models::billing::{ChargeId, ProductId, ProductPriceId, UserSubscriptionId}; +pub use crate::models::billing::{ + ChargeId, ProductId, ProductPriceId, UserSubscriptionId, +}; use thiserror::Error; /// Generates a random 64 bit integer that is exactly `n` characters @@ -41,7 +43,11 @@ pub fn random_base62_rng(rng: &mut R, n: usize) -> u64 { random_base62_rng_range(rng, n, n) } -pub fn random_base62_rng_range(rng: &mut R, n_min: usize, n_max: usize) -> u64 { +pub fn random_base62_rng_range( + rng: &mut R, + n_min: usize, + n_max: usize, +) -> u64 { use rand::Rng; assert!(n_min > 0 && n_max <= 11 && n_min <= n_max); // gen_range is [low, high): max value is `MULTIPLES[n] - 1`, @@ -155,7 +161,10 @@ pub mod base62_impl { impl<'de> Visitor<'de> for Base62Visitor { type Value = Base62Id; - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + fn expecting( + &self, + formatter: &mut std::fmt::Formatter, + ) -> std::fmt::Result { formatter.write_str("a base62 string id") } @@ -211,7 +220,9 @@ pub mod base62_impl { } // We don't want this panicking or wrapping on integer overflow - if let Some(n) = num.checked_mul(62).and_then(|n| n.checked_add(next_digit)) { + if let Some(n) = + num.checked_mul(62).and_then(|n| n.checked_add(next_digit)) + { num = n; } else { return Err(DecodingError::Overflow); diff --git a/apps/labrinth/src/models/v3/images.rs b/apps/labrinth/src/models/v3/images.rs index 1cff0481d..5e814f533 100644 --- a/apps/labrinth/src/models/v3/images.rs +++ b/apps/labrinth/src/models/v3/images.rs @@ -90,7 +90,9 @@ impl ImageContext { match self { ImageContext::Project { project_id } => project_id.map(|x| x.0), ImageContext::Version { version_id } => version_id.map(|x| x.0), - ImageContext::ThreadMessage { thread_message_id } => thread_message_id.map(|x| x.0), + ImageContext::ThreadMessage { thread_message_id } => { + thread_message_id.map(|x| x.0) + } ImageContext::Report { report_id } => report_id.map(|x| x.0), ImageContext::Unknown => None, } diff --git a/apps/labrinth/src/models/v3/notifications.rs b/apps/labrinth/src/models/v3/notifications.rs index 4d3f6ccdf..2d0813102 100644 --- a/apps/labrinth/src/models/v3/notifications.rs +++ b/apps/labrinth/src/models/v3/notifications.rs @@ -3,7 +3,9 @@ use super::ids::OrganizationId; use super::users::UserId; use crate::database::models::notification_item::Notification as DBNotification; use crate::database::models::notification_item::NotificationAction as DBNotificationAction; -use crate::models::ids::{ProjectId, ReportId, TeamId, ThreadId, ThreadMessageId, VersionId}; +use crate::models::ids::{ + ProjectId, ReportId, TeamId, ThreadId, ThreadMessageId, VersionId, +}; use crate::models::projects::ProjectStatus; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; diff --git a/apps/labrinth/src/models/v3/oauth_clients.rs b/apps/labrinth/src/models/v3/oauth_clients.rs index e3979f68f..73f1ae861 100644 --- a/apps/labrinth/src/models/v3/oauth_clients.rs +++ b/apps/labrinth/src/models/v3/oauth_clients.rs @@ -93,7 +93,11 @@ impl From for OAuthClient { name: value.name, icon_url: value.icon_url, max_scopes: value.max_scopes, - redirect_uris: value.redirect_uris.into_iter().map(|r| r.into()).collect(), + redirect_uris: value + .redirect_uris + .into_iter() + .map(|r| r.into()) + .collect(), created_by: value.created_by.into(), created: value.created, url: value.url, diff --git a/apps/labrinth/src/models/v3/pack.rs b/apps/labrinth/src/models/v3/pack.rs index 49e22ca33..045721845 100644 --- a/apps/labrinth/src/models/v3/pack.rs +++ b/apps/labrinth/src/models/v3/pack.rs @@ -1,4 +1,6 @@ -use crate::{models::v2::projects::LegacySideType, util::env::parse_strings_from_var}; +use crate::{ + models::v2::projects::LegacySideType, util::env::parse_strings_from_var, +}; use serde::{Deserialize, Serialize}; use validator::Validate; @@ -29,7 +31,9 @@ pub struct PackFile { pub file_size: u32, } -fn validate_download_url(values: &[String]) -> Result<(), validator::ValidationError> { +fn validate_download_url( + values: &[String], +) -> Result<(), validator::ValidationError> { for value in values { let url = url::Url::parse(value) .ok() @@ -39,7 +43,8 @@ fn validate_download_url(values: &[String]) -> Result<(), validator::ValidationE return Err(validator::ValidationError::new("invalid URL")); } - let domains = parse_strings_from_var("WHITELISTED_MODPACK_DOMAINS").unwrap_or_default(); + let domains = parse_strings_from_var("WHITELISTED_MODPACK_DOMAINS") + .unwrap_or_default(); if !domains.contains( &url.domain() .ok_or_else(|| validator::ValidationError::new("invalid URL"))? diff --git a/apps/labrinth/src/models/v3/pats.rs b/apps/labrinth/src/models/v3/pats.rs index 4de7e7c87..118db66b2 100644 --- a/apps/labrinth/src/models/v3/pats.rs +++ b/apps/labrinth/src/models/v3/pats.rs @@ -131,7 +131,9 @@ impl Scopes { self.intersects(Self::restricted()) } - pub fn parse_from_oauth_scopes(scopes: &str) -> Result { + pub fn parse_from_oauth_scopes( + scopes: &str, + ) -> Result { let scopes = scopes.replace(['+', ' '], "|").replace("%20", "|"); bitflags::parser::from_str(&scopes) } @@ -187,7 +189,9 @@ mod test { #[test] fn test_parse_from_oauth_scopes_well_formed() { let raw = "USER_READ_EMAIL SESSION_READ ORGANIZATION_CREATE"; - let expected = Scopes::USER_READ_EMAIL | Scopes::SESSION_READ | Scopes::ORGANIZATION_CREATE; + let expected = Scopes::USER_READ_EMAIL + | Scopes::SESSION_READ + | Scopes::ORGANIZATION_CREATE; let parsed = Scopes::parse_from_oauth_scopes(raw).unwrap(); @@ -224,7 +228,8 @@ mod test { #[test] fn test_parse_from_oauth_scopes_url_encoded() { - let raw = urlencoding::encode("PAT_WRITE COLLECTION_DELETE").to_string(); + let raw = + urlencoding::encode("PAT_WRITE COLLECTION_DELETE").to_string(); let expected = Scopes::PAT_WRITE | Scopes::COLLECTION_DELETE; let parsed = Scopes::parse_from_oauth_scopes(&raw).unwrap(); diff --git a/apps/labrinth/src/models/v3/projects.rs b/apps/labrinth/src/models/v3/projects.rs index 6b16bf3a1..a6f3c8a2f 100644 --- a/apps/labrinth/src/models/v3/projects.rs +++ b/apps/labrinth/src/models/v3/projects.rs @@ -128,7 +128,9 @@ pub fn from_duplicate_version_fields( let mut fields: HashMap> = HashMap::new(); for vf in version_fields { // We use a string directly, so we can remove duplicates - let serialized = if let Some(inner_array) = vf.value.serialize_internal().as_array() { + let serialized = if let Some(inner_array) = + vf.value.serialize_internal().as_array() + { inner_array.clone() } else { vec![vf.value.serialize_internal()] @@ -151,7 +153,8 @@ pub fn from_duplicate_version_fields( impl From for Project { fn from(data: QueryProject) -> Self { - let fields = from_duplicate_version_fields(data.aggregate_version_fields); + let fields = + from_duplicate_version_fields(data.aggregate_version_fields); let m = data.inner; Self { id: m.id.into(), @@ -655,7 +658,9 @@ pub struct Version { pub fields: HashMap, } -pub fn skip_nulls<'de, D>(deserializer: D) -> Result, D::Error> +pub fn skip_nulls<'de, D>( + deserializer: D, +) -> Result, D::Error> where D: serde::Deserializer<'de>, { @@ -708,7 +713,9 @@ impl From for Version { version_id: d.version_id.map(|i| VersionId(i.0 as u64)), project_id: d.project_id.map(|i| ProjectId(i.0 as u64)), file_name: d.file_name, - dependency_type: DependencyType::from_string(d.dependency_type.as_str()), + dependency_type: DependencyType::from_string( + d.dependency_type.as_str(), + ), }) .collect(), loaders: data.loaders.into_iter().map(Loader).collect(), diff --git a/apps/labrinth/src/models/v3/teams.rs b/apps/labrinth/src/models/v3/teams.rs index 7b2716385..f9f6ef917 100644 --- a/apps/labrinth/src/models/v3/teams.rs +++ b/apps/labrinth/src/models/v3/teams.rs @@ -118,7 +118,8 @@ impl OrganizationPermissions { } if role.is_mod() { return Some( - OrganizationPermissions::EDIT_DETAILS | OrganizationPermissions::ADD_PROJECT, + OrganizationPermissions::EDIT_DETAILS + | OrganizationPermissions::ADD_PROJECT, ); } None diff --git a/apps/labrinth/src/models/v3/threads.rs b/apps/labrinth/src/models/v3/threads.rs index e36fb147d..1c1883175 100644 --- a/apps/labrinth/src/models/v3/threads.rs +++ b/apps/labrinth/src/models/v3/threads.rs @@ -93,7 +93,11 @@ impl ThreadType { } impl Thread { - pub fn from(data: crate::database::models::Thread, users: Vec, user: &User) -> Self { + pub fn from( + data: crate::database::models::Thread, + users: Vec, + user: &User, + ) -> Self { let thread_type = data.type_; Thread { @@ -107,7 +111,8 @@ impl Thread { .filter(|x| { if let MessageBody::Text { private, .. } = x.body { !private || user.role.is_mod() - } else if let MessageBody::Deleted { private, .. } = x.body { + } else if let MessageBody::Deleted { private, .. } = x.body + { !private || user.role.is_mod() } else { true @@ -121,7 +126,10 @@ impl Thread { } impl ThreadMessage { - pub fn from(data: crate::database::models::ThreadMessage, user: &User) -> Self { + pub fn from( + data: crate::database::models::ThreadMessage, + user: &User, + ) -> Self { Self { id: data.id.into(), author_id: if data.hide_identity && !user.role.is_mod() { diff --git a/apps/labrinth/src/queue/analytics.rs b/apps/labrinth/src/queue/analytics.rs index f1fd91215..117a51fa2 100644 --- a/apps/labrinth/src/queue/analytics.rs +++ b/apps/labrinth/src/queue/analytics.rs @@ -36,12 +36,14 @@ impl AnalyticsQueue { fn strip_ip(ip: Ipv6Addr) -> u64 { if let Some(ip) = ip.to_ipv4_mapped() { let octets = ip.octets(); - u64::from_be_bytes([octets[0], octets[1], octets[2], octets[3], 0, 0, 0, 0]) + u64::from_be_bytes([ + octets[0], octets[1], octets[2], octets[3], 0, 0, 0, 0, + ]) } else { let octets = ip.octets(); u64::from_be_bytes([ - octets[0], octets[1], octets[2], octets[3], octets[4], octets[5], octets[6], - octets[7], + octets[0], octets[1], octets[2], octets[3], octets[4], + octets[5], octets[6], octets[7], ]) } } @@ -98,7 +100,8 @@ impl AnalyticsQueue { raw_views.push((views, true)); } - let mut redis = redis.pool.get().await.map_err(DatabaseError::RedisPool)?; + let mut redis = + redis.pool.get().await.map_err(DatabaseError::RedisPool)?; let results = cmd("MGET") .arg( @@ -107,7 +110,7 @@ impl AnalyticsQueue { .map(|x| format!("{}:{}-{}", VIEWS_NAMESPACE, x.0, x.1)) .collect::>(), ) - .query_async::<_, Vec>>(&mut redis) + .query_async::>>(&mut redis) .await .map_err(DatabaseError::CacheError)?; @@ -115,24 +118,25 @@ impl AnalyticsQueue { for (idx, count) in results.into_iter().enumerate() { let key = &views_keys[idx]; - let new_count = if let Some((views, monetized)) = raw_views.get_mut(idx) { - if let Some(count) = count { - if count > 3 { - *monetized = false; - continue; + let new_count = + if let Some((views, monetized)) = raw_views.get_mut(idx) { + if let Some(count) = count { + if count > 3 { + *monetized = false; + continue; + } + + if (count + views.len() as u32) > 3 { + *monetized = false; + } + + count + (views.len() as u32) + } else { + views.len() as u32 } - - if (count + views.len() as u32) > 3 { - *monetized = false; - } - - count + (views.len() as u32) } else { - views.len() as u32 - } - } else { - 1 - }; + 1 + }; pipe.atomic().set_ex( format!("{}:{}-{}", VIEWS_NAMESPACE, key.0, key.1), @@ -140,7 +144,7 @@ impl AnalyticsQueue { 6 * 60 * 60, ); } - pipe.query_async(&mut *redis) + pipe.query_async::<()>(&mut *redis) .await .map_err(DatabaseError::CacheError)?; @@ -163,21 +167,26 @@ impl AnalyticsQueue { let mut downloads_keys = Vec::new(); let raw_downloads = DashMap::new(); - for (index, (key, download)) in downloads_queue.into_iter().enumerate() { + for (index, (key, download)) in + downloads_queue.into_iter().enumerate() + { downloads_keys.push(key); raw_downloads.insert(index, download); } - let mut redis = redis.pool.get().await.map_err(DatabaseError::RedisPool)?; + let mut redis = + redis.pool.get().await.map_err(DatabaseError::RedisPool)?; let results = cmd("MGET") .arg( downloads_keys .iter() - .map(|x| format!("{}:{}-{}", DOWNLOADS_NAMESPACE, x.0, x.1)) + .map(|x| { + format!("{}:{}-{}", DOWNLOADS_NAMESPACE, x.0, x.1) + }) .collect::>(), ) - .query_async::<_, Vec>>(&mut redis) + .query_async::>>(&mut redis) .await .map_err(DatabaseError::CacheError)?; @@ -202,7 +211,7 @@ impl AnalyticsQueue { 6 * 60 * 60, ); } - pipe.query_async(&mut *redis) + pipe.query_async::<()>(&mut *redis) .await .map_err(DatabaseError::CacheError)?; diff --git a/apps/labrinth/src/queue/maxmind.rs b/apps/labrinth/src/queue/maxmind.rs index 4846640a1..e551a8ca4 100644 --- a/apps/labrinth/src/queue/maxmind.rs +++ b/apps/labrinth/src/queue/maxmind.rs @@ -46,11 +46,13 @@ impl MaxMindIndexer { if let Ok(entries) = archive.entries() { for mut file in entries.flatten() { if let Ok(path) = file.header().path() { - if path.extension().and_then(|x| x.to_str()) == Some("mmdb") { + if path.extension().and_then(|x| x.to_str()) == Some("mmdb") + { let mut buf = Vec::new(); file.read_to_end(&mut buf).unwrap(); - let reader = maxminddb::Reader::from_source(buf).unwrap(); + let reader = + maxminddb::Reader::from_source(buf).unwrap(); return Ok(Some(reader)); } @@ -71,10 +73,9 @@ impl MaxMindIndexer { let maxmind = self.reader.read().await; if let Some(ref maxmind) = *maxmind { - maxmind - .lookup::(ip.into()) - .ok() - .and_then(|x| x.country.and_then(|x| x.iso_code.map(|x| x.to_string()))) + maxmind.lookup::(ip.into()).ok().and_then(|x| { + x.country.and_then(|x| x.iso_code.map(|x| x.to_string())) + }) } else { None } diff --git a/apps/labrinth/src/queue/moderation.rs b/apps/labrinth/src/queue/moderation.rs index c6c225733..d31a2ebda 100644 --- a/apps/labrinth/src/queue/moderation.rs +++ b/apps/labrinth/src/queue/moderation.rs @@ -128,10 +128,14 @@ impl ModerationMessage { pub fn header(&self) -> &'static str { match self { ModerationMessage::NoPrimaryFile => "No primary files", - ModerationMessage::PackFilesNotAllowed { .. } => "Copyrighted Content", + ModerationMessage::PackFilesNotAllowed { .. } => { + "Copyrighted Content" + } ModerationMessage::MissingGalleryImage => "Missing Gallery Images", ModerationMessage::MissingLicense => "Missing License", - ModerationMessage::MissingCustomLicenseUrl { .. } => "Missing License URL", + ModerationMessage::MissingCustomLicenseUrl { .. } => { + "Missing License URL" + } ModerationMessage::NoSideTypes => "Missing Environment Information", } } @@ -806,7 +810,9 @@ impl ApprovalType { pub fn from_string(string: &str) -> Option { match string { "yes" => Some(ApprovalType::Yes), - "with-attribution-and-source" => Some(ApprovalType::WithAttributionAndSource), + "with-attribution-and-source" => { + Some(ApprovalType::WithAttributionAndSource) + } "with-attribution" => Some(ApprovalType::WithAttribution), "no" => Some(ApprovalType::No), "permanent-no" => Some(ApprovalType::PermanentNo), @@ -818,7 +824,9 @@ impl ApprovalType { pub(crate) fn as_str(&self) -> &'static str { match self { ApprovalType::Yes => "yes", - ApprovalType::WithAttributionAndSource => "with-attribution-and-source", + ApprovalType::WithAttributionAndSource => { + "with-attribution-and-source" + } ApprovalType::WithAttribution => "with-attribution", ApprovalType::No => "no", ApprovalType::PermanentNo => "permanent-no", diff --git a/apps/labrinth/src/queue/payouts.rs b/apps/labrinth/src/queue/payouts.rs index 6d1d0c1cd..46345045e 100644 --- a/apps/labrinth/src/queue/payouts.rs +++ b/apps/labrinth/src/queue/payouts.rs @@ -1,5 +1,6 @@ use crate::models::payouts::{ - PayoutDecimal, PayoutInterval, PayoutMethod, PayoutMethodFee, PayoutMethodType, + PayoutDecimal, PayoutInterval, PayoutMethod, PayoutMethodFee, + PayoutMethodType, }; use crate::models::projects::MonetizationStatus; use crate::routes::ApiError; @@ -81,12 +82,17 @@ impl PayoutsQueue { .form(&form) .send() .await - .map_err(|_| ApiError::Payments("Error while authenticating with PayPal".to_string()))? + .map_err(|_| { + ApiError::Payments( + "Error while authenticating with PayPal".to_string(), + ) + })? .json() .await .map_err(|_| { ApiError::Payments( - "Error while authenticating with PayPal (deser error)".to_string(), + "Error while authenticating with PayPal (deser error)" + .to_string(), ) })?; @@ -114,7 +120,9 @@ impl PayoutsQueue { if credentials.expires < Utc::now() { drop(read); self.refresh_token().await.map_err(|_| { - ApiError::Payments("Error while authenticating with PayPal".to_string()) + ApiError::Payments( + "Error while authenticating with PayPal".to_string(), + ) })? } else { credentials.clone() @@ -122,7 +130,9 @@ impl PayoutsQueue { } else { drop(read); self.refresh_token().await.map_err(|_| { - ApiError::Payments("Error while authenticating with PayPal".to_string()) + ApiError::Payments( + "Error while authenticating with PayPal".to_string(), + ) })? }; @@ -138,7 +148,10 @@ impl PayoutsQueue { ) .header( "Authorization", - format!("{} {}", credentials.token_type, credentials.access_token), + format!( + "{} {}", + credentials.token_type, credentials.access_token + ), ); if let Some(body) = body { @@ -149,15 +162,16 @@ impl PayoutsQueue { .body(body); } - let resp = request - .send() - .await - .map_err(|_| ApiError::Payments("could not communicate with PayPal".to_string()))?; + let resp = request.send().await.map_err(|_| { + ApiError::Payments("could not communicate with PayPal".to_string()) + })?; let status = resp.status(); let value = resp.json::().await.map_err(|_| { - ApiError::Payments("could not retrieve PayPal response body".to_string()) + ApiError::Payments( + "could not retrieve PayPal response body".to_string(), + ) })?; if !status.is_success() { @@ -173,14 +187,18 @@ impl PayoutsQueue { pub error_description: String, } - if let Ok(error) = serde_json::from_value::(value.clone()) { + if let Ok(error) = + serde_json::from_value::(value.clone()) + { return Err(ApiError::Payments(format!( "error name: {}, message: {}", error.name, error.message ))); } - if let Ok(error) = serde_json::from_value::(value) { + if let Ok(error) = + serde_json::from_value::(value) + { return Err(ApiError::Payments(format!( "error name: {}, message: {}", error.error, error.error_description @@ -216,15 +234,18 @@ impl PayoutsQueue { request = request.json(&body); } - let resp = request - .send() - .await - .map_err(|_| ApiError::Payments("could not communicate with Tremendous".to_string()))?; + let resp = request.send().await.map_err(|_| { + ApiError::Payments( + "could not communicate with Tremendous".to_string(), + ) + })?; let status = resp.status(); let value = resp.json::().await.map_err(|_| { - ApiError::Payments("could not retrieve Tremendous response body".to_string()) + ApiError::Payments( + "could not retrieve Tremendous response body".to_string(), + ) })?; if !status.is_success() { @@ -235,12 +256,15 @@ impl PayoutsQueue { message: String, } - let err = - serde_json::from_value::(array.clone()).map_err(|_| { - ApiError::Payments( - "could not retrieve Tremendous error json body".to_string(), - ) - })?; + let err = serde_json::from_value::( + array.clone(), + ) + .map_err(|_| { + ApiError::Payments( + "could not retrieve Tremendous error json body" + .to_string(), + ) + })?; return Err(ApiError::Payments(err.message)); } @@ -254,8 +278,12 @@ impl PayoutsQueue { Ok(serde_json::from_value(value)?) } - pub async fn get_payout_methods(&self) -> Result, ApiError> { - async fn refresh_payout_methods(queue: &PayoutsQueue) -> Result { + pub async fn get_payout_methods( + &self, + ) -> Result, ApiError> { + async fn refresh_payout_methods( + queue: &PayoutsQueue, + ) -> Result { let mut options = queue.payout_options.write().await; let mut methods = Vec::new(); @@ -304,7 +332,11 @@ impl PayoutsQueue { } let response = queue - .make_tremendous_request::<(), TremendousResponse>(Method::GET, "products", None) + .make_tremendous_request::<(), TremendousResponse>( + Method::GET, + "products", + None, + ) .await?; for product in response.products { @@ -361,7 +393,11 @@ impl PayoutsQueue { id: product.id, type_: PayoutMethodType::Tremendous, name: product.name.clone(), - supported_countries: product.countries.into_iter().map(|x| x.abbr).collect(), + supported_countries: product + .countries + .into_iter() + .map(|x| x.abbr) + .collect(), image_url: product .images .into_iter() @@ -412,7 +448,8 @@ impl PayoutsQueue { methods.push(method); } - const UPRANK_IDS: &[&str] = &["ET0ZVETV5ILN", "Q24BD9EZ332JT", "UIL1ZYJU5MKN"]; + const UPRANK_IDS: &[&str] = + &["ET0ZVETV5ILN", "Q24BD9EZ332JT", "UIL1ZYJU5MKN"]; const DOWNRANK_IDS: &[&str] = &["EIPF8Q00EMM1", "OU2MWXYWPNWQ"]; methods.sort_by(|a, b| { @@ -558,7 +595,10 @@ pub async fn make_aditude_request( Ok(json) } -pub async fn process_payout(pool: &PgPool, client: &clickhouse::Client) -> Result<(), ApiError> { +pub async fn process_payout( + pool: &PgPool, + client: &clickhouse::Client, +) -> Result<(), ApiError> { let start: DateTime = DateTime::from_naive_utc_and_offset( (Utc::now() - Duration::days(1)) .date_naive() @@ -750,8 +790,12 @@ pub async fn process_payout(pool: &PgPool, client: &clickhouse::Client) -> Resul ); } - let aditude_res = - make_aditude_request(&["METRIC_IMPRESSIONS", "METRIC_REVENUE"], "Yesterday", "1d").await?; + let aditude_res = make_aditude_request( + &["METRIC_IMPRESSIONS", "METRIC_REVENUE"], + "Yesterday", + "1d", + ) + .await?; let aditude_amount: Decimal = aditude_res .iter() @@ -777,8 +821,9 @@ pub async fn process_payout(pool: &PgPool, client: &clickhouse::Client) -> Resul // Clean.io fee (ad antimalware). Per 1000 impressions. let clean_io_fee = Decimal::from(8) / Decimal::from(1000); - let net_revenue = - aditude_amount - (clean_io_fee * Decimal::from(aditude_impressions) / Decimal::from(1000)); + let net_revenue = aditude_amount + - (clean_io_fee * Decimal::from(aditude_impressions) + / Decimal::from(1000)); let payout = net_revenue * (Decimal::from(1) - modrinth_cut); @@ -811,11 +856,13 @@ pub async fn process_payout(pool: &PgPool, client: &clickhouse::Client) -> Resul let project_multiplier: Decimal = Decimal::from(**value) / Decimal::from(multipliers.sum); - let sum_splits: Decimal = project.team_members.iter().map(|x| x.1).sum(); + let sum_splits: Decimal = + project.team_members.iter().map(|x| x.1).sum(); if sum_splits > Decimal::ZERO { for (user_id, split) in project.team_members { - let payout: Decimal = payout * project_multiplier * (split / sum_splits); + let payout: Decimal = + payout * project_multiplier * (split / sum_splits); if payout > Decimal::ZERO { insert_user_ids.push(user_id); diff --git a/apps/labrinth/src/queue/session.rs b/apps/labrinth/src/queue/session.rs index 1411ae7c2..1c3ce7bee 100644 --- a/apps/labrinth/src/queue/session.rs +++ b/apps/labrinth/src/queue/session.rs @@ -1,6 +1,8 @@ use crate::database::models::pat_item::PersonalAccessToken; use crate::database::models::session_item::Session; -use crate::database::models::{DatabaseError, OAuthAccessTokenId, PatId, SessionId, UserId}; +use crate::database::models::{ + DatabaseError, OAuthAccessTokenId, PatId, SessionId, UserId, +}; use crate::database::redis::RedisPool; use crate::routes::internal::session::SessionMetadata; use chrono::Utc; @@ -38,7 +40,10 @@ impl AuthQueue { self.pat_queue.lock().await.insert(id); } - pub async fn add_oauth_access_token(&self, id: crate::database::models::OAuthAccessTokenId) { + pub async fn add_oauth_access_token( + &self, + id: crate::database::models::OAuthAccessTokenId, + ) { self.oauth_access_token_queue.lock().await.insert(id); } @@ -56,10 +61,15 @@ impl AuthQueue { std::mem::replace(&mut queue, HashSet::with_capacity(len)) } - pub async fn index(&self, pool: &PgPool, redis: &RedisPool) -> Result<(), DatabaseError> { + pub async fn index( + &self, + pool: &PgPool, + redis: &RedisPool, + ) -> Result<(), DatabaseError> { let session_queue = self.take_sessions().await; let pat_queue = Self::take_hashset(&self.pat_queue).await; - let oauth_access_token_queue = Self::take_hashset(&self.oauth_access_token_queue).await; + let oauth_access_token_queue = + Self::take_hashset(&self.oauth_access_token_queue).await; if !session_queue.is_empty() || !pat_queue.is_empty() @@ -104,7 +114,11 @@ impl AuthQueue { .await?; for (id, session, user_id) in expired_ids { - clear_cache_sessions.push((Some(id), Some(session), Some(user_id))); + clear_cache_sessions.push(( + Some(id), + Some(session), + Some(user_id), + )); Session::remove(id, &mut transaction).await?; } @@ -128,7 +142,11 @@ impl AuthQueue { .execute(&mut *transaction) .await?; - update_oauth_access_token_last_used(oauth_access_token_queue, &mut transaction).await?; + update_oauth_access_token_last_used( + oauth_access_token_queue, + &mut transaction, + ) + .await?; transaction.commit().await?; PersonalAccessToken::clear_cache(clear_cache_pats, redis).await?; diff --git a/apps/labrinth/src/routes/analytics.rs b/apps/labrinth/src/routes/analytics.rs index 1d28f863e..36fe6febc 100644 --- a/apps/labrinth/src/routes/analytics.rs +++ b/apps/labrinth/src/routes/analytics.rs @@ -65,19 +65,22 @@ pub async fn page_view_ingest( pool: web::Data, redis: web::Data, ) -> Result { - let user = get_user_from_headers(&req, &**pool, &redis, &session_queue, None) - .await - .ok(); + let user = + get_user_from_headers(&req, &**pool, &redis, &session_queue, None) + .await + .ok(); let conn_info = req.connection_info().peer_addr().map(|x| x.to_string()); - let url = Url::parse(&url_input.url) - .map_err(|_| ApiError::InvalidInput("invalid page view URL specified!".to_string()))?; + let url = Url::parse(&url_input.url).map_err(|_| { + ApiError::InvalidInput("invalid page view URL specified!".to_string()) + })?; - let domain = url - .host_str() - .ok_or_else(|| ApiError::InvalidInput("invalid page view URL specified!".to_string()))?; + let domain = url.host_str().ok_or_else(|| { + ApiError::InvalidInput("invalid page view URL specified!".to_string()) + })?; - let allowed_origins = parse_strings_from_var("CORS_ALLOWED_ORIGINS").unwrap_or_default(); + let allowed_origins = + parse_strings_from_var("CORS_ALLOWED_ORIGINS").unwrap_or_default(); if !(domain.ends_with(".modrinth.com") || domain == "modrinth.com" || allowed_origins.contains(&"*".to_string())) @@ -98,11 +101,13 @@ pub async fn page_view_ingest( }) .collect::>(); - let ip = convert_to_ip_v6(if let Some(header) = headers.get("cf-connecting-ip") { - header - } else { - conn_info.as_deref().unwrap_or_default() - }) + let ip = convert_to_ip_v6( + if let Some(header) = headers.get("cf-connecting-ip") { + header + } else { + conn_info.as_deref().unwrap_or_default() + }, + ) .unwrap_or_else(|_| Ipv4Addr::new(127, 0, 0, 1).to_ipv6_mapped()); let mut view = PageView { @@ -135,8 +140,12 @@ pub async fn page_view_ingest( ]; if PROJECT_TYPES.contains(&segments_vec[0]) { - let project = - crate::database::models::Project::get(segments_vec[1], &**pool, &redis).await?; + let project = crate::database::models::Project::get( + segments_vec[1], + &**pool, + &redis, + ) + .await?; if let Some(project) = project { view.project_id = project.inner.id.0 as u64; @@ -167,7 +176,9 @@ pub async fn playtime_ingest( req: HttpRequest, analytics_queue: web::Data>, session_queue: web::Data, - playtime_input: web::Json>, + playtime_input: web::Json< + HashMap, + >, pool: web::Data, redis: web::Data, ) -> Result { @@ -200,7 +211,8 @@ pub async fn playtime_ingest( continue; } - if let Some(version) = versions.iter().find(|x| id == x.inner.id.into()) { + if let Some(version) = versions.iter().find(|x| id == x.inner.id.into()) + { analytics_queue.add_playtime(Playtime { recorded: get_current_tenths_of_ms(), seconds: playtime.seconds as u64, diff --git a/apps/labrinth/src/routes/internal/admin.rs b/apps/labrinth/src/routes/internal/admin.rs index 07afe8365..411abb12d 100644 --- a/apps/labrinth/src/routes/internal/admin.rs +++ b/apps/labrinth/src/routes/internal/admin.rs @@ -53,16 +53,25 @@ pub async fn count_download( .find(|x| x.0.to_lowercase() == "authorization") .map(|x| &**x.1); - let user = get_user_record_from_bearer_token(&req, token, &**pool, &redis, &session_queue) - .await - .ok() - .flatten(); + let user = get_user_record_from_bearer_token( + &req, + token, + &**pool, + &redis, + &session_queue, + ) + .await + .ok() + .flatten(); - let project_id: crate::database::models::ids::ProjectId = download_body.project_id.into(); + let project_id: crate::database::models::ids::ProjectId = + download_body.project_id.into(); - let id_option = crate::models::ids::base62_impl::parse_base62(&download_body.version_name) - .ok() - .map(|x| x as i64); + let id_option = crate::models::ids::base62_impl::parse_base62( + &download_body.version_name, + ) + .ok() + .map(|x| x as i64); let (version_id, project_id) = if let Some(version) = sqlx::query!( " @@ -95,8 +104,9 @@ pub async fn count_download( )); }; - let url = url::Url::parse(&download_body.url) - .map_err(|_| ApiError::InvalidInput("invalid download URL specified!".to_string()))?; + let url = url::Url::parse(&download_body.url).map_err(|_| { + ApiError::InvalidInput("invalid download URL specified!".to_string()) + })?; let ip = crate::routes::analytics::convert_to_ip_v6(&download_body.ip) .unwrap_or_else(|_| Ipv4Addr::new(127, 0, 0, 1).to_ipv6_mapped()); @@ -127,7 +137,10 @@ pub async fn count_download( .headers .clone() .into_iter() - .filter(|x| !crate::routes::analytics::FILTERED_HEADERS.contains(&&*x.0.to_lowercase())) + .filter(|x| { + !crate::routes::analytics::FILTERED_HEADERS + .contains(&&*x.0.to_lowercase()) + }) .collect(), }); diff --git a/apps/labrinth/src/routes/internal/billing.rs b/apps/labrinth/src/routes/internal/billing.rs index 1332fa3b4..188d30812 100644 --- a/apps/labrinth/src/routes/internal/billing.rs +++ b/apps/labrinth/src/routes/internal/billing.rs @@ -1,12 +1,14 @@ use crate::auth::{get_user_from_headers, send_email}; use crate::database::models::charge_item::ChargeItem; use crate::database::models::{ - generate_charge_id, generate_user_subscription_id, product_item, user_subscription_item, + generate_charge_id, generate_user_subscription_id, product_item, + user_subscription_item, }; use crate::database::redis::RedisPool; use crate::models::billing::{ - Charge, ChargeStatus, ChargeType, Price, PriceDuration, Product, ProductMetadata, ProductPrice, - SubscriptionMetadata, SubscriptionStatus, UserSubscription, + Charge, ChargeStatus, ChargeType, Price, PriceDuration, Product, + ProductMetadata, ProductPrice, SubscriptionMetadata, SubscriptionStatus, + UserSubscription, }; use crate::models::ids::base62_impl::{parse_base62, to_base62}; use crate::models::pats::Scopes; @@ -26,9 +28,10 @@ use std::str::FromStr; use stripe::{ CreateCustomer, CreatePaymentIntent, CreateSetupIntent, CreateSetupIntentAutomaticPaymentMethods, - CreateSetupIntentAutomaticPaymentMethodsAllowRedirects, Currency, CustomerId, - CustomerInvoiceSettings, CustomerPaymentMethodRetrieval, EventObject, EventType, - PaymentIntentOffSession, PaymentIntentSetupFutureUsage, PaymentMethodId, SetupIntent, + CreateSetupIntentAutomaticPaymentMethodsAllowRedirects, Currency, + CustomerId, CustomerInvoiceSettings, CustomerPaymentMethodRetrieval, + EventObject, EventType, PaymentIntentOffSession, + PaymentIntentSetupFutureUsage, PaymentMethodId, SetupIntent, UpdateCustomer, Webhook, }; @@ -96,11 +99,14 @@ pub async fn subscriptions( .1; let subscriptions = - user_subscription_item::UserSubscriptionItem::get_all_user(user.id.into(), &**pool) - .await? - .into_iter() - .map(UserSubscription::from) - .collect::>(); + user_subscription_item::UserSubscriptionItem::get_all_user( + user.id.into(), + &**pool, + ) + .await? + .into_iter() + .map(UserSubscription::from) + .collect::>(); Ok(HttpResponse::Ok().json(subscriptions)) } @@ -136,7 +142,8 @@ pub async fn edit_subscription( let (id,) = info.into_inner(); if let Some(subscription) = - user_subscription_item::UserSubscriptionItem::get(id.into(), &**pool).await? + user_subscription_item::UserSubscriptionItem::get(id.into(), &**pool) + .await? { if subscription.user_id != user.id.into() && !user.role.is_admin() { return Err(ApiError::NotFound); @@ -156,19 +163,24 @@ pub async fn edit_subscription( ) })?; - let current_price = - product_item::ProductPriceItem::get(subscription.price_id, &mut *transaction) - .await? - .ok_or_else(|| { - ApiError::InvalidInput("Could not find current product price".to_string()) - })?; + let current_price = product_item::ProductPriceItem::get( + subscription.price_id, + &mut *transaction, + ) + .await? + .ok_or_else(|| { + ApiError::InvalidInput( + "Could not find current product price".to_string(), + ) + })?; if let Some(cancelled) = &edit_subscription.cancelled { if open_charge.status != ChargeStatus::Open && open_charge.status != ChargeStatus::Cancelled { return Err(ApiError::InvalidInput( - "You may not change the status of this subscription!".to_string(), + "You may not change the status of this subscription!" + .to_string(), )); } @@ -186,35 +198,41 @@ pub async fn edit_subscription( open_charge.amount = *price as i64; } else { return Err(ApiError::InvalidInput( - "Interval is not valid for this subscription!".to_string(), + "Interval is not valid for this subscription!" + .to_string(), )); } } } let intent = if let Some(product_id) = &edit_subscription.product { - let product_price = product_item::ProductPriceItem::get_all_product_prices( - (*product_id).into(), - &mut *transaction, - ) - .await? - .into_iter() - .find(|x| x.currency_code == current_price.currency_code) - .ok_or_else(|| { - ApiError::InvalidInput( - "Could not find a valid price for your currency code!".to_string(), + let product_price = + product_item::ProductPriceItem::get_all_product_prices( + (*product_id).into(), + &mut *transaction, ) - })?; + .await? + .into_iter() + .find(|x| x.currency_code == current_price.currency_code) + .ok_or_else(|| { + ApiError::InvalidInput( + "Could not find a valid price for your currency code!" + .to_string(), + ) + })?; if product_price.id == current_price.id { return Err(ApiError::InvalidInput( - "You may not change the price of this subscription!".to_string(), + "You may not change the price of this subscription!" + .to_string(), )); } let interval = open_charge.due - Utc::now(); let duration = PriceDuration::iterator() - .min_by_key(|x| (x.duration().num_seconds() - interval.num_seconds()).abs()) + .min_by_key(|x| { + (x.duration().num_seconds() - interval.num_seconds()).abs() + }) .unwrap_or(PriceDuration::Monthly); let current_amount = match ¤t_price.prices { @@ -241,7 +259,9 @@ pub async fn edit_subscription( .floor() .to_i32() .ok_or_else(|| { - ApiError::InvalidInput("Could not convert proration to i32".to_string()) + ApiError::InvalidInput( + "Could not convert proration to i32".to_string(), + ) })?; // TODO: Add downgrading plans @@ -276,27 +296,36 @@ pub async fn edit_subscription( ) .await?; - let currency = Currency::from_str(¤t_price.currency_code.to_lowercase()) - .map_err(|_| ApiError::InvalidInput("Invalid currency code".to_string()))?; + let currency = + Currency::from_str(¤t_price.currency_code.to_lowercase()) + .map_err(|_| { + ApiError::InvalidInput( + "Invalid currency code".to_string(), + ) + })?; - let mut intent = CreatePaymentIntent::new(proration as i64, currency); + let mut intent = + CreatePaymentIntent::new(proration as i64, currency); let mut metadata = HashMap::new(); - metadata.insert("modrinth_user_id".to_string(), to_base62(user.id.0)); + metadata + .insert("modrinth_user_id".to_string(), to_base62(user.id.0)); intent.customer = Some(customer_id); intent.metadata = Some(metadata); intent.receipt_email = user.email.as_deref(); - intent.setup_future_usage = Some(PaymentIntentSetupFutureUsage::OffSession); + intent.setup_future_usage = + Some(PaymentIntentSetupFutureUsage::OffSession); if let Some(payment_method) = &edit_subscription.payment_method { - let payment_method_id = if let Ok(id) = PaymentMethodId::from_str(payment_method) { - id - } else { - return Err(ApiError::InvalidInput( - "Invalid payment method id".to_string(), - )); - }; + let payment_method_id = + if let Ok(id) = PaymentMethodId::from_str(payment_method) { + id + } else { + return Err(ApiError::InvalidInput( + "Invalid payment method id".to_string(), + )); + }; intent.payment_method = Some(payment_method_id); } @@ -357,7 +386,8 @@ pub async fn user_customer( &redis, ) .await?; - let customer = stripe::Customer::retrieve(&stripe_client, &customer_id, &[]).await?; + let customer = + stripe::Customer::retrieve(&stripe_client, &customer_id, &[]).await?; Ok(HttpResponse::Ok().json(customer)) } @@ -380,8 +410,11 @@ pub async fn charges( .1; let charges = - crate::database::models::charge_item::ChargeItem::get_from_user(user.id.into(), &**pool) - .await?; + crate::database::models::charge_item::ChargeItem::get_from_user( + user.id.into(), + &**pool, + ) + .await?; Ok(HttpResponse::Ok().json( charges @@ -493,8 +526,12 @@ pub async fn edit_payment_method( ) .await?; - let payment_method = - stripe::PaymentMethod::retrieve(&stripe_client, &payment_method_id, &[]).await?; + let payment_method = stripe::PaymentMethod::retrieve( + &stripe_client, + &payment_method_id, + &[], + ) + .await?; if payment_method .customer @@ -558,17 +595,26 @@ pub async fn remove_payment_method( ) .await?; - let payment_method = - stripe::PaymentMethod::retrieve(&stripe_client, &payment_method_id, &[]).await?; + let payment_method = stripe::PaymentMethod::retrieve( + &stripe_client, + &payment_method_id, + &[], + ) + .await?; let user_subscriptions = - user_subscription_item::UserSubscriptionItem::get_all_user(user.id.into(), &**pool).await?; + user_subscription_item::UserSubscriptionItem::get_all_user( + user.id.into(), + &**pool, + ) + .await?; if user_subscriptions .iter() .any(|x| x.status != SubscriptionStatus::Unprovisioned) { - let customer = stripe::Customer::retrieve(&stripe_client, &customer, &[]).await?; + let customer = + stripe::Customer::retrieve(&stripe_client, &customer, &[]).await?; if customer .invoice_settings @@ -591,7 +637,8 @@ pub async fn remove_payment_method( .unwrap_or(false) || user.role.is_admin() { - stripe::PaymentMethod::detach(&stripe_client, &payment_method_id).await?; + stripe::PaymentMethod::detach(&stripe_client, &payment_method_id) + .await?; Ok(HttpResponse::NoContent().finish()) } else { @@ -757,10 +804,18 @@ pub async fn initiate_payment( let (user_country, payment_method) = match &payment_request.type_ { PaymentRequestType::PaymentMethod { id } => { let payment_method_id = stripe::PaymentMethodId::from_str(id) - .map_err(|_| ApiError::InvalidInput("Invalid payment method id".to_string()))?; + .map_err(|_| { + ApiError::InvalidInput( + "Invalid payment method id".to_string(), + ) + })?; - let payment_method = - stripe::PaymentMethod::retrieve(&stripe_client, &payment_method_id, &[]).await?; + let payment_method = stripe::PaymentMethod::retrieve( + &stripe_client, + &payment_method_id, + &[], + ) + .await?; let country = payment_method .billing_details @@ -788,11 +843,16 @@ pub async fn initiate_payment( ])).unwrap(); json_patch::patch(&mut confirmation, &p).unwrap(); - let confirmation: ConfirmationToken = serde_json::from_value(confirmation)?; + let confirmation: ConfirmationToken = + serde_json::from_value(confirmation)?; - let payment_method = confirmation.payment_method_preview.ok_or_else(|| { - ApiError::InvalidInput("Confirmation token is missing payment method!".to_string()) - })?; + let payment_method = + confirmation.payment_method_preview.ok_or_else(|| { + ApiError::InvalidInput( + "Confirmation token is missing payment method!" + .to_string(), + ) + })?; let country = payment_method .billing_details @@ -807,104 +867,126 @@ pub async fn initiate_payment( let country = user_country.as_deref().unwrap_or("US"); let recommended_currency_code = infer_currency_code(country); - let (price, currency_code, interval, price_id, charge_id) = match payment_request.charge { - ChargeRequestType::Existing { id } => { - let charge = crate::database::models::charge_item::ChargeItem::get(id.into(), &**pool) - .await? - .ok_or_else(|| { - ApiError::InvalidInput("Specified charge could not be found!".to_string()) - })?; + let (price, currency_code, interval, price_id, charge_id) = + match payment_request.charge { + ChargeRequestType::Existing { id } => { + let charge = + crate::database::models::charge_item::ChargeItem::get( + id.into(), + &**pool, + ) + .await? + .ok_or_else(|| { + ApiError::InvalidInput( + "Specified charge could not be found!".to_string(), + ) + })?; - ( - charge.amount, - charge.currency_code, - charge.subscription_interval, - charge.price_id, - Some(id), - ) - } - ChargeRequestType::New { - product_id, - interval, - } => { - let product = product_item::ProductItem::get(product_id.into(), &**pool) - .await? - .ok_or_else(|| { - ApiError::InvalidInput("Specified product could not be found!".to_string()) - })?; + ( + charge.amount, + charge.currency_code, + charge.subscription_interval, + charge.price_id, + Some(id), + ) + } + ChargeRequestType::New { + product_id, + interval, + } => { + let product = + product_item::ProductItem::get(product_id.into(), &**pool) + .await? + .ok_or_else(|| { + ApiError::InvalidInput( + "Specified product could not be found!" + .to_string(), + ) + })?; - let mut product_prices = - product_item::ProductPriceItem::get_all_product_prices(product.id, &**pool).await?; + let mut product_prices = + product_item::ProductPriceItem::get_all_product_prices( + product.id, &**pool, + ) + .await?; - let price_item = if let Some(pos) = product_prices - .iter() - .position(|x| x.currency_code == recommended_currency_code) - { - product_prices.remove(pos) - } else if let Some(pos) = product_prices.iter().position(|x| x.currency_code == "USD") { - product_prices.remove(pos) - } else { - return Err(ApiError::InvalidInput( - "Could not find a valid price for the user's country".to_string(), - )); - }; + let price_item = if let Some(pos) = product_prices + .iter() + .position(|x| x.currency_code == recommended_currency_code) + { + product_prices.remove(pos) + } else if let Some(pos) = + product_prices.iter().position(|x| x.currency_code == "USD") + { + product_prices.remove(pos) + } else { + return Err(ApiError::InvalidInput( + "Could not find a valid price for the user's country" + .to_string(), + )); + }; - let price = match price_item.prices { - Price::OneTime { price } => price, - Price::Recurring { ref intervals } => { - let interval = interval.ok_or_else(|| { + let price = match price_item.prices { + Price::OneTime { price } => price, + Price::Recurring { ref intervals } => { + let interval = interval.ok_or_else(|| { ApiError::InvalidInput( "Could not find a valid interval for the user's country".to_string(), ) })?; - *intervals.get(&interval).ok_or_else(|| { + *intervals.get(&interval).ok_or_else(|| { ApiError::InvalidInput( "Could not find a valid price for the user's country".to_string(), ) })? - } - }; + } + }; - if let Price::Recurring { .. } = price_item.prices { - if product.unitary { - let user_subscriptions = + if let Price::Recurring { .. } = price_item.prices { + if product.unitary { + let user_subscriptions = user_subscription_item::UserSubscriptionItem::get_all_user( user.id.into(), &**pool, ) .await?; - let user_products = product_item::ProductPriceItem::get_many( - &user_subscriptions - .iter() - .filter(|x| x.status == SubscriptionStatus::Provisioned) - .map(|x| x.price_id) - .collect::>(), - &**pool, - ) - .await?; + let user_products = + product_item::ProductPriceItem::get_many( + &user_subscriptions + .iter() + .filter(|x| { + x.status + == SubscriptionStatus::Provisioned + }) + .map(|x| x.price_id) + .collect::>(), + &**pool, + ) + .await?; - if user_products - .into_iter() - .any(|x| x.product_id == product.id) - { - return Err(ApiError::InvalidInput( - "You are already subscribed to this product!".to_string(), - )); + if user_products + .into_iter() + .any(|x| x.product_id == product.id) + { + return Err(ApiError::InvalidInput( + "You are already subscribed to this product!" + .to_string(), + )); + } } } - } - ( - price as i64, - price_item.currency_code, - interval, - price_item.id, - None, - ) - } - }; + ( + price as i64, + price_item.currency_code, + interval, + price_item.id, + None, + ) + } + }; let customer = get_or_create_customer( user.id, @@ -916,7 +998,9 @@ pub async fn initiate_payment( ) .await?; let stripe_currency = Currency::from_str(¤cy_code.to_lowercase()) - .map_err(|_| ApiError::InvalidInput("Invalid currency code".to_string()))?; + .map_err(|_| { + ApiError::InvalidInput("Invalid currency code".to_string()) + })?; if let Some(payment_intent_id) = &payment_request.existing_payment_intent { let mut update_payment_intent = stripe::UpdatePaymentIntent { @@ -926,12 +1010,18 @@ pub async fn initiate_payment( ..Default::default() }; - if let PaymentRequestType::PaymentMethod { .. } = payment_request.type_ { - update_payment_intent.payment_method = Some(payment_method.id.clone()); + if let PaymentRequestType::PaymentMethod { .. } = payment_request.type_ + { + update_payment_intent.payment_method = + Some(payment_method.id.clone()); } - stripe::PaymentIntent::update(&stripe_client, payment_intent_id, update_payment_intent) - .await?; + stripe::PaymentIntent::update( + &stripe_client, + payment_intent_id, + update_payment_intent, + ) + .await?; Ok(HttpResponse::Ok().json(serde_json::json!({ "price_id": to_base62(price_id.0 as u64), @@ -953,11 +1043,15 @@ pub async fn initiate_payment( } if let Some(charge_id) = charge_id { - metadata.insert("modrinth_charge_id".to_string(), to_base62(charge_id.0)); + metadata.insert( + "modrinth_charge_id".to_string(), + to_base62(charge_id.0), + ); } else { let mut transaction = pool.begin().await?; let charge_id = generate_charge_id(&mut transaction).await?; - let subscription_id = generate_user_subscription_id(&mut transaction).await?; + let subscription_id = + generate_user_subscription_id(&mut transaction).await?; metadata.insert( "modrinth_charge_id".to_string(), @@ -984,13 +1078,16 @@ pub async fn initiate_payment( intent.customer = Some(customer); intent.metadata = Some(metadata); intent.receipt_email = user.email.as_deref(); - intent.setup_future_usage = Some(PaymentIntentSetupFutureUsage::OffSession); + intent.setup_future_usage = + Some(PaymentIntentSetupFutureUsage::OffSession); - if let PaymentRequestType::PaymentMethod { .. } = payment_request.type_ { + if let PaymentRequestType::PaymentMethod { .. } = payment_request.type_ + { intent.payment_method = Some(payment_method.id.clone()); } - let payment_intent = stripe::PaymentIntent::create(&stripe_client, intent).await?; + let payment_intent = + stripe::PaymentIntent::create(&stripe_client, intent).await?; Ok(HttpResponse::Ok().json(serde_json::json!({ "payment_intent_id": payment_intent.id, @@ -1027,7 +1124,8 @@ pub async fn stripe_webhook( pub product_price_item: product_item::ProductPriceItem, pub product_item: product_item::ProductItem, pub charge_item: crate::database::models::charge_item::ChargeItem, - pub user_subscription_item: Option, + pub user_subscription_item: + Option, pub payment_metadata: Option, } @@ -1050,7 +1148,10 @@ pub async fn stripe_webhook( }; let user = if let Some(user) = - crate::database::models::user_item::User::get_id(user_id, pool, redis).await? + crate::database::models::user_item::User::get_id( + user_id, pool, redis, + ) + .await? { user } else { @@ -1071,11 +1172,20 @@ pub async fn stripe_webhook( break 'metadata; }; - let (charge, price, product, subscription) = if let Some(mut charge) = - crate::database::models::charge_item::ChargeItem::get(charge_id, pool).await? + let (charge, price, product, subscription) = if let Some( + mut charge, + ) = + crate::database::models::charge_item::ChargeItem::get( + charge_id, pool, + ) + .await? { let price = if let Some(price) = - product_item::ProductPriceItem::get(charge.price_id, pool).await? + product_item::ProductPriceItem::get( + charge.price_id, + pool, + ) + .await? { price } else { @@ -1083,7 +1193,8 @@ pub async fn stripe_webhook( }; let product = if let Some(product) = - product_item::ProductItem::get(price.product_id, pool).await? + product_item::ProductItem::get(price.product_id, pool) + .await? { product } else { @@ -1096,8 +1207,11 @@ pub async fn stripe_webhook( if let Some(subscription_id) = charge.subscription_id { let mut subscription = if let Some(subscription) = - user_subscription_item::UserSubscriptionItem::get(subscription_id, pool) - .await? + user_subscription_item::UserSubscriptionItem::get( + subscription_id, + pool, + ) + .await? { subscription } else { @@ -1106,7 +1220,9 @@ pub async fn stripe_webhook( match charge.type_ { ChargeType::OneTime | ChargeType::Subscription => { - if let Some(interval) = charge.subscription_interval { + if let Some(interval) = + charge.subscription_interval + { subscription.interval = interval; } } @@ -1125,15 +1241,19 @@ pub async fn stripe_webhook( let price_id = if let Some(price_id) = metadata .get("modrinth_price_id") .and_then(|x| parse_base62(x).ok()) - .map(|x| crate::database::models::ids::ProductPriceId(x as i64)) - { + .map(|x| { + crate::database::models::ids::ProductPriceId( + x as i64, + ) + }) { price_id } else { break 'metadata; }; let price = if let Some(price) = - product_item::ProductPriceItem::get(price_id, pool).await? + product_item::ProductPriceItem::get(price_id, pool) + .await? { price } else { @@ -1141,7 +1261,8 @@ pub async fn stripe_webhook( }; let product = if let Some(product) = - product_item::ProductItem::get(price.product_id, pool).await? + product_item::ProductItem::get(price.product_id, pool) + .await? { product } else { @@ -1208,7 +1329,9 @@ pub async fn stripe_webhook( ChargeType::OneTime }, subscription_id: subscription.as_ref().map(|x| x.id), - subscription_interval: subscription.as_ref().map(|x| x.interval), + subscription_interval: subscription + .as_ref() + .map(|x| x.interval), }; if charge_status != ChargeStatus::Failed { @@ -1235,7 +1358,9 @@ pub async fn stripe_webhook( match event.type_ { EventType::PaymentIntentSucceeded => { - if let EventObject::PaymentIntent(payment_intent) = event.data.object { + if let EventObject::PaymentIntent(payment_intent) = + event.data.object + { let mut transaction = pool.begin().await?; let mut metadata = get_payment_intent_metadata( @@ -1250,7 +1375,8 @@ pub async fn stripe_webhook( // Provision subscription match metadata.product_item.metadata { ProductMetadata::Midas => { - let badges = metadata.user_item.badges | Badges::MIDAS; + let badges = + metadata.user_item.badges | Badges::MIDAS; sqlx::query!( " @@ -1259,7 +1385,8 @@ pub async fn stripe_webhook( WHERE (id = $2) ", badges.bits() as i64, - metadata.user_item.id as crate::database::models::ids::UserId, + metadata.user_item.id + as crate::database::models::ids::UserId, ) .execute(&mut *transaction) .await?; @@ -1270,7 +1397,9 @@ pub async fn stripe_webhook( swap, storage, } => { - if let Some(ref subscription) = metadata.user_subscription_item { + if let Some(ref subscription) = + metadata.user_subscription_item + { let client = reqwest::Client::new(); if let Some(SubscriptionMetadata::Pyro { id }) = @@ -1293,7 +1422,8 @@ pub async fn stripe_webhook( ref server_name, ref source, }, - ) = metadata.payment_metadata + ) = + metadata.payment_metadata { (server_name.clone(), source.clone()) } else { @@ -1315,9 +1445,13 @@ pub async fn stripe_webhook( ) }; - let server_name = server_name.unwrap_or_else(|| { - format!("{}'s server", metadata.user_item.username) - }); + let server_name = server_name + .unwrap_or_else(|| { + format!( + "{}'s server", + metadata.user_item.username + ) + }); #[derive(Deserialize)] struct PyroServerResponse { @@ -1348,17 +1482,23 @@ pub async fn stripe_webhook( metadata.user_subscription_item { subscription.metadata = - Some(SubscriptionMetadata::Pyro { id: res.uuid }); + Some(SubscriptionMetadata::Pyro { + id: res.uuid, + }); } } } } } - if let Some(mut subscription) = metadata.user_subscription_item { - let open_charge = - ChargeItem::get_open_subscription(subscription.id, &mut *transaction) - .await?; + if let Some(mut subscription) = + metadata.user_subscription_item + { + let open_charge = ChargeItem::get_open_subscription( + subscription.id, + &mut *transaction, + ) + .await?; let new_price = match metadata.product_price_item.prices { Price::OneTime { price } => price, @@ -1377,24 +1517,35 @@ pub async fn stripe_webhook( charge.amount = new_price as i64; charge.upsert(&mut transaction).await?; - } else if metadata.charge_item.status != ChargeStatus::Cancelled { - let charge_id = generate_charge_id(&mut transaction).await?; + } else if metadata.charge_item.status + != ChargeStatus::Cancelled + { + let charge_id = + generate_charge_id(&mut transaction).await?; ChargeItem { id: charge_id, user_id: metadata.user_item.id, price_id: metadata.product_price_item.id, amount: new_price as i64, - currency_code: metadata.product_price_item.currency_code, + currency_code: metadata + .product_price_item + .currency_code, status: ChargeStatus::Open, - due: if subscription.status == SubscriptionStatus::Unprovisioned { - Utc::now() + subscription.interval.duration() + due: if subscription.status + == SubscriptionStatus::Unprovisioned + { + Utc::now() + + subscription.interval.duration() } else { - metadata.charge_item.due + subscription.interval.duration() + metadata.charge_item.due + + subscription.interval.duration() }, last_attempt: None, type_: ChargeType::Subscription, subscription_id: Some(subscription.id), - subscription_interval: Some(subscription.interval), + subscription_interval: Some( + subscription.interval, + ), } .upsert(&mut transaction) .await?; @@ -1413,7 +1564,9 @@ pub async fn stripe_webhook( } } EventType::PaymentIntentProcessing => { - if let EventObject::PaymentIntent(payment_intent) = event.data.object { + if let EventObject::PaymentIntent(payment_intent) = + event.data.object + { let mut transaction = pool.begin().await?; get_payment_intent_metadata( payment_intent.metadata, @@ -1427,7 +1580,9 @@ pub async fn stripe_webhook( } } EventType::PaymentIntentPaymentFailed => { - if let EventObject::PaymentIntent(payment_intent) = event.data.object { + if let EventObject::PaymentIntent(payment_intent) = + event.data.object + { let mut transaction = pool.begin().await?; let metadata = get_payment_intent_metadata( @@ -1442,8 +1597,10 @@ pub async fn stripe_webhook( if let Some(email) = metadata.user_item.email { let money = rusty_money::Money::from_minor( metadata.charge_item.amount, - rusty_money::iso::find(&metadata.charge_item.currency_code) - .unwrap_or(rusty_money::iso::USD), + rusty_money::iso::find( + &metadata.charge_item.currency_code, + ) + .unwrap_or(rusty_money::iso::USD), ); let _ = send_email( @@ -1459,10 +1616,18 @@ pub async fn stripe_webhook( } } EventType::PaymentMethodAttached => { - if let EventObject::PaymentMethod(payment_method) = event.data.object { - if let Some(customer_id) = payment_method.customer.map(|x| x.id()) { - let customer = - stripe::Customer::retrieve(&stripe_client, &customer_id, &[]).await?; + if let EventObject::PaymentMethod(payment_method) = + event.data.object + { + if let Some(customer_id) = + payment_method.customer.map(|x| x.id()) + { + let customer = stripe::Customer::retrieve( + &stripe_client, + &customer_id, + &[], + ) + .await?; if !customer .invoice_settings @@ -1473,10 +1638,14 @@ pub async fn stripe_webhook( &stripe_client, &customer_id, UpdateCustomer { - invoice_settings: Some(CustomerInvoiceSettings { - default_payment_method: Some(payment_method.id.to_string()), - ..Default::default() - }), + invoice_settings: Some( + CustomerInvoiceSettings { + default_payment_method: Some( + payment_method.id.to_string(), + ), + ..Default::default() + }, + ), ..Default::default() }, ) @@ -1504,7 +1673,8 @@ async fn get_or_create_customer( pool: &PgPool, redis: &RedisPool, ) -> Result { - if let Some(customer_id) = stripe_customer_id.and_then(|x| stripe::CustomerId::from_str(x).ok()) + if let Some(customer_id) = + stripe_customer_id.and_then(|x| stripe::CustomerId::from_str(x).ok()) { Ok(customer_id) } else { @@ -1533,8 +1703,11 @@ async fn get_or_create_customer( .execute(pool) .await?; - crate::database::models::user_item::User::clear_caches(&[(user_id.into(), None)], redis) - .await?; + crate::database::models::user_item::User::clear_caches( + &[(user_id.into(), None)], + redis, + ) + .await?; Ok(customer.id) } @@ -1551,16 +1724,17 @@ pub async fn subscription_task(pool: PgPool, redis: RedisPool) { // If an active subscription has a canceled charge OR a failed charge more than two days ago, it should be cancelled let all_charges = ChargeItem::get_unprovision(&pool).await?; - let mut all_subscriptions = user_subscription_item::UserSubscriptionItem::get_many( - &all_charges - .iter() - .filter_map(|x| x.subscription_id) - .collect::>() - .into_iter() - .collect::>(), - &pool, - ) - .await?; + let mut all_subscriptions = + user_subscription_item::UserSubscriptionItem::get_many( + &all_charges + .iter() + .filter_map(|x| x.subscription_id) + .collect::>() + .into_iter() + .collect::>(), + &pool, + ) + .await?; let subscription_prices = product_item::ProductPriceItem::get_many( &all_subscriptions .iter() @@ -1607,9 +1781,10 @@ pub async fn subscription_task(pool: PgPool, redis: RedisPool) { continue; } - let product_price = if let Some(product_price) = subscription_prices - .iter() - .find(|x| x.id == subscription.price_id) + let product_price = if let Some(product_price) = + subscription_prices + .iter() + .find(|x| x.id == subscription.price_id) { product_price } else { @@ -1625,7 +1800,9 @@ pub async fn subscription_task(pool: PgPool, redis: RedisPool) { continue; }; - let user = if let Some(user) = users.iter().find(|x| x.id == subscription.user_id) { + let user = if let Some(user) = + users.iter().find(|x| x.id == subscription.user_id) + { user } else { continue; @@ -1650,7 +1827,9 @@ pub async fn subscription_task(pool: PgPool, redis: RedisPool) { true } ProductMetadata::Pyro { .. } => { - if let Some(SubscriptionMetadata::Pyro { id }) = &subscription.metadata { + if let Some(SubscriptionMetadata::Pyro { id }) = + &subscription.metadata + { let res = reqwest::Client::new() .post(format!( "https://archon.pyro.host/modrinth/v0/servers/{}/suspend", @@ -1710,7 +1889,11 @@ pub async fn subscription_task(pool: PgPool, redis: RedisPool) { } } -pub async fn task(stripe_client: stripe::Client, pool: PgPool, redis: RedisPool) { +pub async fn task( + stripe_client: stripe::Client, + pool: PgPool, + redis: RedisPool, +) { loop { info!("Indexing billing queue"); let res = async { diff --git a/apps/labrinth/src/routes/internal/flows.rs b/apps/labrinth/src/routes/internal/flows.rs index ef7d395eb..31da67cdc 100644 --- a/apps/labrinth/src/routes/internal/flows.rs +++ b/apps/labrinth/src/routes/internal/flows.rs @@ -87,7 +87,8 @@ impl TempUser { } } - let user_id = crate::database::models::generate_user_id(transaction).await?; + let user_id = + crate::database::models::generate_user_id(transaction).await?; let mut username_increment: i32 = 0; let mut username = None; @@ -103,7 +104,12 @@ impl TempUser { } ); - let new_id = crate::database::models::User::get(&test_username, client, redis).await?; + let new_id = crate::database::models::User::get( + &test_username, + client, + redis, + ) + .await?; if new_id.is_none() { username = Some(test_username); @@ -112,71 +118,74 @@ impl TempUser { } } - let (avatar_url, raw_avatar_url) = if let Some(avatar_url) = self.avatar_url { - let res = reqwest::get(&avatar_url).await?; - let headers = res.headers().clone(); + let (avatar_url, raw_avatar_url) = + if let Some(avatar_url) = self.avatar_url { + let res = reqwest::get(&avatar_url).await?; + let headers = res.headers().clone(); - let img_data = if let Some(content_type) = headers - .get(reqwest::header::CONTENT_TYPE) - .and_then(|ct| ct.to_str().ok()) - { - get_image_ext(content_type) - } else { - avatar_url.rsplit('.').next() - }; + let img_data = if let Some(content_type) = headers + .get(reqwest::header::CONTENT_TYPE) + .and_then(|ct| ct.to_str().ok()) + { + get_image_ext(content_type) + } else { + avatar_url.rsplit('.').next() + }; - if let Some(ext) = img_data { - let bytes = res.bytes().await?; + if let Some(ext) = img_data { + let bytes = res.bytes().await?; - let upload_result = upload_image_optimized( - &format!("user/{}", crate::models::users::UserId::from(user_id)), - bytes, - ext, - Some(96), - Some(1.0), - &**file_host, - ) - .await; + let upload_result = upload_image_optimized( + &format!( + "user/{}", + crate::models::users::UserId::from(user_id) + ), + bytes, + ext, + Some(96), + Some(1.0), + &**file_host, + ) + .await; - if let Ok(upload_result) = upload_result { - (Some(upload_result.url), Some(upload_result.raw_url)) + if let Ok(upload_result) = upload_result { + (Some(upload_result.url), Some(upload_result.raw_url)) + } else { + (None, None) + } } else { (None, None) } } else { (None, None) - } - } else { - (None, None) - }; + }; if let Some(username) = username { crate::database::models::User { id: user_id, github_id: if provider == AuthProvider::GitHub { Some( - self.id - .clone() - .parse() - .map_err(|_| AuthenticationError::InvalidCredentials)?, + self.id.clone().parse().map_err(|_| { + AuthenticationError::InvalidCredentials + })?, ) } else { None }, discord_id: if provider == AuthProvider::Discord { Some( - self.id - .parse() - .map_err(|_| AuthenticationError::InvalidCredentials)?, + self.id.parse().map_err(|_| { + AuthenticationError::InvalidCredentials + })?, ) } else { None }, gitlab_id: if provider == AuthProvider::GitLab { Some( - self.id - .parse() - .map_err(|_| AuthenticationError::InvalidCredentials)?, + self.id.parse().map_err(|_| { + AuthenticationError::InvalidCredentials + })?, ) } else { None @@ -188,9 +197,9 @@ impl TempUser { }, steam_id: if provider == AuthProvider::Steam { Some( - self.id - .parse() - .map_err(|_| AuthenticationError::InvalidCredentials)?, + self.id.parse().map_err(|_| { + AuthenticationError::InvalidCredentials + })?, ) } else { None @@ -236,7 +245,10 @@ impl TempUser { } impl AuthProvider { - pub fn get_redirect_url(&self, state: String) -> Result { + pub fn get_redirect_url( + &self, + state: String, + ) -> Result { let self_addr = dotenvy::var("SELF_ADDR")?; let raw_redirect_uri = format!("{}/v2/auth/callback", self_addr); let redirect_uri = urlencoding::encode(&raw_redirect_uri); @@ -316,7 +328,8 @@ impl AuthProvider { &self, query: HashMap, ) -> Result { - let redirect_uri = format!("{}/v2/auth/callback", dotenvy::var("SELF_ADDR")?); + let redirect_uri = + format!("{}/v2/auth/callback", dotenvy::var("SELF_ADDR")?); #[derive(Deserialize)] struct AccessToken { @@ -454,22 +467,26 @@ impl AuthProvider { .ok_or_else(|| AuthenticationError::InvalidCredentials)?; form.insert( "openid.assoc_handle".to_string(), - &**query - .get("openid.assoc_handle") - .ok_or_else(|| AuthenticationError::InvalidCredentials)?, + &**query.get("openid.assoc_handle").ok_or_else(|| { + AuthenticationError::InvalidCredentials + })?, ); form.insert("openid.signed".to_string(), &**signed); form.insert( "openid.sig".to_string(), - &**query - .get("openid.sig") - .ok_or_else(|| AuthenticationError::InvalidCredentials)?, + &**query.get("openid.sig").ok_or_else(|| { + AuthenticationError::InvalidCredentials + })?, + ); + form.insert( + "openid.ns".to_string(), + "http://specs.openid.net/auth/2.0", ); - form.insert("openid.ns".to_string(), "http://specs.openid.net/auth/2.0"); form.insert("openid.mode".to_string(), "check_authentication"); for val in signed.split(',') { - if let Some(arr_val) = query.get(&format!("openid.{}", val)) { + if let Some(arr_val) = query.get(&format!("openid.{}", val)) + { form.insert(format!("openid.{}", val), &**arr_val); } } @@ -484,9 +501,10 @@ impl AuthProvider { .await?; if res.contains("is_valid:true") { - let identity = query - .get("openid.identity") - .ok_or_else(|| AuthenticationError::InvalidCredentials)?; + let identity = + query.get("openid.identity").ok_or_else(|| { + AuthenticationError::InvalidCredentials + })?; identity .rsplit('/') @@ -533,7 +551,10 @@ impl AuthProvider { Ok(res) } - pub async fn get_user(&self, token: &str) -> Result { + pub async fn get_user( + &self, + token: &str, + ) -> Result { let res = match self { AuthProvider::GitHub => { let response = reqwest::Client::new() @@ -549,7 +570,9 @@ impl AuthProvider { .get("x-oauth-client-id") .and_then(|x| x.to_str().ok()); - if client_id != Some(&*dotenvy::var("GITHUB_CLIENT_ID").unwrap()) { + if client_id + != Some(&*dotenvy::var("GITHUB_CLIENT_ID").unwrap()) + { return Err(AuthenticationError::InvalidClientId); } } @@ -599,9 +622,12 @@ impl AuthProvider { id: discord_user.id, username: discord_user.username, email: discord_user.email, - avatar_url: discord_user - .avatar - .map(|x| format!("https://cdn.discordapp.com/avatars/{}/{}.webp", id, x)), + avatar_url: discord_user.avatar.map(|x| { + format!( + "https://cdn.discordapp.com/avatars/{}/{}.webp", + id, x + ) + }), bio: None, country: None, } @@ -727,7 +753,8 @@ impl AuthProvider { .text() .await?; - let mut response: SteamResponse = serde_json::from_str(&response)?; + let mut response: SteamResponse = + serde_json::from_str(&response)?; if let Some(player) = response.response.players.pop() { let username = player @@ -827,9 +854,12 @@ impl AuthProvider { value.map(|x| crate::database::models::UserId(x.id)) } AuthProvider::Microsoft => { - let value = sqlx::query!("SELECT id FROM users WHERE microsoft_id = $1", id) - .fetch_optional(executor) - .await?; + let value = sqlx::query!( + "SELECT id FROM users WHERE microsoft_id = $1", + id + ) + .fetch_optional(executor) + .await?; value.map(|x| crate::database::models::UserId(x.id)) } @@ -845,9 +875,12 @@ impl AuthProvider { value.map(|x| crate::database::models::UserId(x.id)) } AuthProvider::Google => { - let value = sqlx::query!("SELECT id FROM users WHERE google_id = $1", id) - .fetch_optional(executor) - .await?; + let value = sqlx::query!( + "SELECT id FROM users WHERE google_id = $1", + id + ) + .fetch_optional(executor) + .await?; value.map(|x| crate::database::models::UserId(x.id)) } @@ -863,9 +896,12 @@ impl AuthProvider { value.map(|x| crate::database::models::UserId(x.id)) } AuthProvider::PayPal => { - let value = sqlx::query!("SELECT id FROM users WHERE paypal_id = $1", id) - .fetch_optional(executor) - .await?; + let value = sqlx::query!( + "SELECT id FROM users WHERE paypal_id = $1", + id + ) + .fetch_optional(executor) + .await?; value.map(|x| crate::database::models::UserId(x.id)) } @@ -1024,11 +1060,15 @@ pub async fn init( redis: Data, session_queue: Data, ) -> Result { - let url = url::Url::parse(&info.url).map_err(|_| AuthenticationError::Url)?; + let url = + url::Url::parse(&info.url).map_err(|_| AuthenticationError::Url)?; - let allowed_callback_urls = parse_strings_from_var("ALLOWED_CALLBACK_URLS").unwrap_or_default(); + let allowed_callback_urls = + parse_strings_from_var("ALLOWED_CALLBACK_URLS").unwrap_or_default(); let domain = url.host_str().ok_or(AuthenticationError::Url)?; - if !allowed_callback_urls.iter().any(|x| domain.ends_with(x)) && domain != "modrinth.com" { + if !allowed_callback_urls.iter().any(|x| domain.ends_with(x)) + && domain != "modrinth.com" + { return Err(AuthenticationError::Url); } @@ -1381,7 +1421,11 @@ pub async fn delete_auth_provider( } transaction.commit().await?; - crate::database::models::User::clear_caches(&[(user.id.into(), None)], &redis).await?; + crate::database::models::User::clear_caches( + &[(user.id.into(), None)], + &redis, + ) + .await?; Ok(HttpResponse::NoContent().finish()) } @@ -1431,24 +1475,28 @@ pub async fn create_account_with_password( redis: Data, new_account: web::Json, ) -> Result { - new_account - .0 - .validate() - .map_err(|err| ApiError::InvalidInput(validation_errors_to_string(err, None)))?; + new_account.0.validate().map_err(|err| { + ApiError::InvalidInput(validation_errors_to_string(err, None)) + })?; if !check_turnstile_captcha(&req, &new_account.challenge).await? { return Err(ApiError::Turnstile); } - if crate::database::models::User::get(&new_account.username, &**pool, &redis) - .await? - .is_some() + if crate::database::models::User::get( + &new_account.username, + &**pool, + &redis, + ) + .await? + .is_some() { return Err(ApiError::InvalidInput("Username is taken!".to_string())); } let mut transaction = pool.begin().await?; - let user_id = crate::database::models::generate_user_id(&mut transaction).await?; + let user_id = + crate::database::models::generate_user_id(&mut transaction).await?; let new_account = new_account.0; @@ -1459,10 +1507,13 @@ pub async fn create_account_with_password( if score.score() < 3 { return Err(ApiError::InvalidInput( - if let Some(feedback) = score.feedback().clone().and_then(|x| x.warning()) { + if let Some(feedback) = + score.feedback().clone().and_then(|x| x.warning()) + { format!("Password too weak: {}", feedback) } else { - "Specified password is too weak! Please improve its strength.".to_string() + "Specified password is too weak! Please improve its strength." + .to_string() }, )); } @@ -1554,13 +1605,15 @@ pub async fn login_password( } let user = if let Some(user) = - crate::database::models::User::get(&login.username, &**pool, &redis).await? + crate::database::models::User::get(&login.username, &**pool, &redis) + .await? { user } else { - let user = crate::database::models::User::get_email(&login.username, &**pool) - .await? - .ok_or_else(|| AuthenticationError::InvalidCredentials)?; + let user = + crate::database::models::User::get_email(&login.username, &**pool) + .await? + .ok_or_else(|| AuthenticationError::InvalidCredentials)?; crate::database::models::User::get_id(user, &**pool, &redis) .await? @@ -1591,7 +1644,8 @@ pub async fn login_password( }))) } else { let mut transaction = pool.begin().await?; - let session = issue_session(req, user.id, &mut transaction, &redis).await?; + let session = + issue_session(req, user.id, &mut transaction, &redis).await?; let res = crate::models::sessions::Session::from(session, true, None); transaction.commit().await?; @@ -1651,7 +1705,9 @@ async fn validate_2fa_code( Ok(true) } else if allow_backup { - let backup_codes = crate::database::models::User::get_backup_codes(user_id, pool).await?; + let backup_codes = + crate::database::models::User::get_backup_codes(user_id, pool) + .await?; if !backup_codes.contains(&input) { Ok(false) @@ -1669,7 +1725,11 @@ async fn validate_2fa_code( .execute(&mut **transaction) .await?; - crate::database::models::User::clear_caches(&[(user_id, None)], redis).await?; + crate::database::models::User::clear_caches( + &[(user_id, None)], + redis, + ) + .await?; Ok(true) } @@ -1690,9 +1750,10 @@ pub async fn login_2fa( .ok_or_else(|| AuthenticationError::InvalidCredentials)?; if let Flow::Login2FA { user_id } = flow { - let user = crate::database::models::User::get_id(user_id, &**pool, &redis) - .await? - .ok_or_else(|| AuthenticationError::InvalidCredentials)?; + let user = + crate::database::models::User::get_id(user_id, &**pool, &redis) + .await? + .ok_or_else(|| AuthenticationError::InvalidCredentials)?; let mut transaction = pool.begin().await?; if !validate_2fa_code( @@ -1713,7 +1774,8 @@ pub async fn login_2fa( } Flow::remove(&login.flow, &redis).await?; - let session = issue_session(req, user_id, &mut transaction, &redis).await?; + let session = + issue_session(req, user_id, &mut transaction, &redis).await?; let res = crate::models::sessions::Session::from(session, true, None); transaction.commit().await?; @@ -1870,7 +1932,11 @@ pub async fn finish_2fa_flow( } transaction.commit().await?; - crate::database::models::User::clear_caches(&[(user.id.into(), None)], &redis).await?; + crate::database::models::User::clear_caches( + &[(user.id.into(), None)], + &redis, + ) + .await?; Ok(HttpResponse::Ok().json(serde_json::json!({ "backup_codes": codes, @@ -1895,10 +1961,15 @@ pub async fn remove_2fa( login: web::Json, session_queue: Data, ) -> Result { - let (scopes, user) = - get_user_record_from_bearer_token(&req, None, &**pool, &redis, &session_queue) - .await? - .ok_or_else(|| AuthenticationError::InvalidCredentials)?; + let (scopes, user) = get_user_record_from_bearer_token( + &req, + None, + &**pool, + &redis, + &session_queue, + ) + .await? + .ok_or_else(|| AuthenticationError::InvalidCredentials)?; if !scopes.contains(Scopes::USER_AUTH_WRITE) { return Err(ApiError::Authentication( @@ -1911,7 +1982,9 @@ pub async fn remove_2fa( if !validate_2fa_code( login.code.clone(), user.totp_secret.ok_or_else(|| { - ApiError::InvalidInput("User does not have 2FA enabled on the account!".to_string()) + ApiError::InvalidInput( + "User does not have 2FA enabled on the account!".to_string(), + ) })?, true, user.id, @@ -1958,7 +2031,8 @@ pub async fn remove_2fa( } transaction.commit().await?; - crate::database::models::User::clear_caches(&[(user.id, None)], &redis).await?; + crate::database::models::User::clear_caches(&[(user.id, None)], &redis) + .await?; Ok(HttpResponse::NoContent().finish()) } @@ -1980,12 +2054,20 @@ pub async fn reset_password_begin( return Err(ApiError::Turnstile); } - let user = if let Some(user_id) = - crate::database::models::User::get_email(&reset_password.username, &**pool).await? + let user = if let Some(user_id) = crate::database::models::User::get_email( + &reset_password.username, + &**pool, + ) + .await? { crate::database::models::User::get_id(user_id, &**pool, &redis).await? } else { - crate::database::models::User::get(&reset_password.username, &**pool, &redis).await? + crate::database::models::User::get( + &reset_password.username, + &**pool, + &redis, + ) + .await? }; if let Some(user) = user { @@ -2026,9 +2108,10 @@ pub async fn change_password( let flow = Flow::get(flow, &redis).await?; if let Some(Flow::ForgotPassword { user_id }) = flow { - let user = crate::database::models::User::get_id(user_id, &**pool, &redis) - .await? - .ok_or_else(|| AuthenticationError::InvalidCredentials)?; + let user = + crate::database::models::User::get_id(user_id, &**pool, &redis) + .await? + .ok_or_else(|| AuthenticationError::InvalidCredentials)?; Some(user) } else { @@ -2041,10 +2124,15 @@ pub async fn change_password( let user = if let Some(user) = user { user } else { - let (scopes, user) = - get_user_record_from_bearer_token(&req, None, &**pool, &redis, &session_queue) - .await? - .ok_or_else(|| AuthenticationError::InvalidCredentials)?; + let (scopes, user) = get_user_record_from_bearer_token( + &req, + None, + &**pool, + &redis, + &session_queue, + ) + .await? + .ok_or_else(|| AuthenticationError::InvalidCredentials)?; if !scopes.contains(Scopes::USER_AUTH_WRITE) { return Err(ApiError::Authentication( @@ -2060,7 +2148,10 @@ pub async fn change_password( })?; let hasher = Argon2::default(); - hasher.verify_password(old_password.as_bytes(), &PasswordHash::new(pass)?)?; + hasher.verify_password( + old_password.as_bytes(), + &PasswordHash::new(pass)?, + )?; } user @@ -2068,7 +2159,9 @@ pub async fn change_password( let mut transaction = pool.begin().await?; - let update_password = if let Some(new_password) = &change_password.new_password { + let update_password = if let Some(new_password) = + &change_password.new_password + { let score = zxcvbn::zxcvbn( new_password, &[&user.username, &user.email.clone().unwrap_or_default()], @@ -2076,7 +2169,9 @@ pub async fn change_password( if score.score() < 3 { return Err(ApiError::InvalidInput( - if let Some(feedback) = score.feedback().clone().and_then(|x| x.warning()) { + if let Some(feedback) = + score.feedback().clone().and_then(|x| x.warning()) + { format!("Password too weak: {}", feedback) } else { "Specified password is too weak! Please improve its strength.".to_string() @@ -2140,7 +2235,8 @@ pub async fn change_password( } transaction.commit().await?; - crate::database::models::User::clear_caches(&[(user.id, None)], &redis).await?; + crate::database::models::User::clear_caches(&[(user.id, None)], &redis) + .await?; Ok(HttpResponse::Ok().finish()) } @@ -2160,10 +2256,9 @@ pub async fn set_email( session_queue: Data, stripe_client: Data, ) -> Result { - email - .0 - .validate() - .map_err(|err| ApiError::InvalidInput(validation_errors_to_string(err, None)))?; + email.0.validate().map_err(|err| { + ApiError::InvalidInput(validation_errors_to_string(err, None)) + })?; let user = get_user_from_headers( &req, @@ -2229,7 +2324,11 @@ pub async fn set_email( )?; transaction.commit().await?; - crate::database::models::User::clear_caches(&[(user.id.into(), None)], &redis).await?; + crate::database::models::User::clear_caches( + &[(user.id.into(), None)], + &redis, + ) + .await?; Ok(HttpResponse::Ok().finish()) } @@ -2265,7 +2364,11 @@ pub async fn resend_verify_email( .insert(Duration::hours(24), &redis) .await?; - send_email_verify(email, flow, "We need to verify your email address.")?; + send_email_verify( + email, + flow, + "We need to verify your email address.", + )?; Ok(HttpResponse::NoContent().finish()) } else { @@ -2293,9 +2396,10 @@ pub async fn verify_email( confirm_email, }) = flow { - let user = crate::database::models::User::get_id(user_id, &**pool, &redis) - .await? - .ok_or_else(|| AuthenticationError::InvalidCredentials)?; + let user = + crate::database::models::User::get_id(user_id, &**pool, &redis) + .await? + .ok_or_else(|| AuthenticationError::InvalidCredentials)?; if user.email != Some(confirm_email) { return Err(ApiError::InvalidInput( @@ -2319,12 +2423,14 @@ pub async fn verify_email( Flow::remove(&email.flow, &redis).await?; transaction.commit().await?; - crate::database::models::User::clear_caches(&[(user.id, None)], &redis).await?; + crate::database::models::User::clear_caches(&[(user.id, None)], &redis) + .await?; Ok(HttpResponse::NoContent().finish()) } else { Err(ApiError::InvalidInput( - "Flow does not exist. Try re-requesting the verification link.".to_string(), + "Flow does not exist. Try re-requesting the verification link." + .to_string(), )) } } diff --git a/apps/labrinth/src/routes/internal/gdpr.rs b/apps/labrinth/src/routes/internal/gdpr.rs index e07855e50..6f1d3e611 100644 --- a/apps/labrinth/src/routes/internal/gdpr.rs +++ b/apps/labrinth/src/routes/internal/gdpr.rs @@ -29,13 +29,18 @@ pub async fn export( let user_id = user.id.into(); - let collection_ids = crate::database::models::User::get_collections(user_id, &**pool).await?; - let collections = - crate::database::models::Collection::get_many(&collection_ids, &**pool, &redis) - .await? - .into_iter() - .map(crate::models::collections::Collection::from) - .collect::>(); + let collection_ids = + crate::database::models::User::get_collections(user_id, &**pool) + .await?; + let collections = crate::database::models::Collection::get_many( + &collection_ids, + &**pool, + &redis, + ) + .await? + .into_iter() + .map(crate::models::collections::Collection::from) + .collect::>(); let follows = crate::database::models::User::get_follows(user_id, &**pool) .await? @@ -43,22 +48,26 @@ pub async fn export( .map(crate::models::ids::ProjectId::from) .collect::>(); - let projects = crate::database::models::User::get_projects(user_id, &**pool, &redis) + let projects = + crate::database::models::User::get_projects(user_id, &**pool, &redis) + .await? + .into_iter() + .map(crate::models::ids::ProjectId::from) + .collect::>(); + + let org_ids = + crate::database::models::User::get_organizations(user_id, &**pool) + .await?; + let orgs = + crate::database::models::organization_item::Organization::get_many_ids( + &org_ids, &**pool, &redis, + ) .await? .into_iter() - .map(crate::models::ids::ProjectId::from) + // TODO: add team members + .map(|x| crate::models::organizations::Organization::from(x, vec![])) .collect::>(); - let org_ids = crate::database::models::User::get_organizations(user_id, &**pool).await?; - let orgs = crate::database::models::organization_item::Organization::get_many_ids( - &org_ids, &**pool, &redis, - ) - .await? - .into_iter() - // TODO: add team members - .map(|x| crate::models::organizations::Organization::from(x, vec![])) - .collect::>(); - let notifs = crate::database::models::notification_item::Notification::get_many_user( user_id, &**pool, &redis, ) @@ -84,34 +93,46 @@ pub async fn export( .map(crate::models::oauth_clients::OAuthClientAuthorization::from) .collect::>(); - let pat_ids = crate::database::models::pat_item::PersonalAccessToken::get_user_pats( - user_id, &**pool, &redis, - ) - .await?; - let pats = crate::database::models::pat_item::PersonalAccessToken::get_many_ids( - &pat_ids, &**pool, &redis, + let pat_ids = + crate::database::models::pat_item::PersonalAccessToken::get_user_pats( + user_id, &**pool, &redis, + ) + .await?; + let pats = + crate::database::models::pat_item::PersonalAccessToken::get_many_ids( + &pat_ids, &**pool, &redis, + ) + .await? + .into_iter() + .map(|x| crate::models::pats::PersonalAccessToken::from(x, false)) + .collect::>(); + + let payout_ids = + crate::database::models::payout_item::Payout::get_all_for_user( + user_id, &**pool, + ) + .await?; + + let payouts = crate::database::models::payout_item::Payout::get_many( + &payout_ids, + &**pool, ) .await? .into_iter() - .map(|x| crate::models::pats::PersonalAccessToken::from(x, false)) + .map(crate::models::payouts::Payout::from) .collect::>(); - let payout_ids = - crate::database::models::payout_item::Payout::get_all_for_user(user_id, &**pool).await?; - - let payouts = crate::database::models::payout_item::Payout::get_many(&payout_ids, &**pool) - .await? - .into_iter() - .map(crate::models::payouts::Payout::from) - .collect::>(); - let report_ids = - crate::database::models::user_item::User::get_reports(user_id, &**pool).await?; - let reports = crate::database::models::report_item::Report::get_many(&report_ids, &**pool) - .await? - .into_iter() - .map(crate::models::reports::Report::from) - .collect::>(); + crate::database::models::user_item::User::get_reports(user_id, &**pool) + .await?; + let reports = crate::database::models::report_item::Report::get_many( + &report_ids, + &**pool, + ) + .await? + .into_iter() + .map(crate::models::reports::Report::from) + .collect::>(); let message_ids = sqlx::query!( " @@ -126,11 +147,14 @@ pub async fn export( .collect::>(); let messages = - crate::database::models::thread_item::ThreadMessage::get_many(&message_ids, &**pool) - .await? - .into_iter() - .map(|x| crate::models::threads::ThreadMessage::from(x, &user)) - .collect::>(); + crate::database::models::thread_item::ThreadMessage::get_many( + &message_ids, + &**pool, + ) + .await? + .into_iter() + .map(|x| crate::models::threads::ThreadMessage::from(x, &user)) + .collect::>(); let uploaded_images_ids = sqlx::query!( "SELECT id FROM uploaded_images WHERE owner_id = $1", @@ -142,12 +166,15 @@ pub async fn export( .map(|x| crate::database::models::ids::ImageId(x.id)) .collect::>(); - let uploaded_images = - crate::database::models::image_item::Image::get_many(&uploaded_images_ids, &**pool, &redis) - .await? - .into_iter() - .map(crate::models::images::Image::from) - .collect::>(); + let uploaded_images = crate::database::models::image_item::Image::get_many( + &uploaded_images_ids, + &**pool, + &redis, + ) + .await? + .into_iter() + .map(crate::models::images::Image::from) + .collect::>(); let subscriptions = crate::database::models::user_subscription_item::UserSubscriptionItem::get_all_user( diff --git a/apps/labrinth/src/routes/internal/moderation.rs b/apps/labrinth/src/routes/internal/moderation.rs index 15bb52c0e..9f59e738e 100644 --- a/apps/labrinth/src/routes/internal/moderation.rs +++ b/apps/labrinth/src/routes/internal/moderation.rs @@ -60,11 +60,12 @@ pub async fn get_projects( .try_collect::>() .await?; - let projects: Vec<_> = database::Project::get_many_ids(&project_ids, &**pool, &redis) - .await? - .into_iter() - .map(crate::models::projects::Project::from) - .collect(); + let projects: Vec<_> = + database::Project::get_many_ids(&project_ids, &**pool, &redis) + .await? + .into_iter() + .map(crate::models::projects::Project::from) + .collect(); Ok(HttpResponse::Ok().json(projects)) } @@ -86,7 +87,8 @@ pub async fn get_project_meta( .await?; let project_id = info.into_inner().0; - let project = database::models::Project::get(&project_id, &**pool, &redis).await?; + let project = + database::models::Project::get(&project_id, &**pool, &redis).await?; if let Some(project) = project { let rows = sqlx::query!( @@ -122,7 +124,8 @@ pub async fn get_project_meta( check_hashes.extend(merged.flame_files.keys().cloned()); check_hashes.extend(merged.unknown_files.keys().cloned()); - check_flames.extend(merged.flame_files.values().map(|x| x.id as i32)); + check_flames + .extend(merged.flame_files.values().map(|x| x.id as i32)); } } diff --git a/apps/labrinth/src/routes/internal/pats.rs b/apps/labrinth/src/routes/internal/pats.rs index 64f349e99..8539f2596 100644 --- a/apps/labrinth/src/routes/internal/pats.rs +++ b/apps/labrinth/src/routes/internal/pats.rs @@ -44,15 +44,17 @@ pub async fn get_pats( .await? .1; - let pat_ids = database::models::pat_item::PersonalAccessToken::get_user_pats( - user.id.into(), - &**pool, - &redis, + let pat_ids = + database::models::pat_item::PersonalAccessToken::get_user_pats( + user.id.into(), + &**pool, + &redis, + ) + .await?; + let pats = database::models::pat_item::PersonalAccessToken::get_many_ids( + &pat_ids, &**pool, &redis, ) .await?; - let pats = - database::models::pat_item::PersonalAccessToken::get_many_ids(&pat_ids, &**pool, &redis) - .await?; Ok(HttpResponse::Ok().json( pats.into_iter() @@ -77,9 +79,9 @@ pub async fn create_pat( redis: Data, session_queue: Data, ) -> Result { - info.0 - .validate() - .map_err(|err| ApiError::InvalidInput(validation_errors_to_string(err, None)))?; + info.0.validate().map_err(|err| { + ApiError::InvalidInput(validation_errors_to_string(err, None)) + })?; if info.scopes.is_restricted() { return Err(ApiError::InvalidInput( @@ -174,7 +176,10 @@ pub async fn edit_pat( .1; let id = id.into_inner().0; - let pat = database::models::pat_item::PersonalAccessToken::get(&id, &**pool, &redis).await?; + let pat = database::models::pat_item::PersonalAccessToken::get( + &id, &**pool, &redis, + ) + .await?; if let Some(pat) = pat { if pat.user_id == user.id.into() { @@ -262,13 +267,19 @@ pub async fn delete_pat( .await? .1; let id = id.into_inner().0; - let pat = database::models::pat_item::PersonalAccessToken::get(&id, &**pool, &redis).await?; + let pat = database::models::pat_item::PersonalAccessToken::get( + &id, &**pool, &redis, + ) + .await?; if let Some(pat) = pat { if pat.user_id == user.id.into() { let mut transaction = pool.begin().await?; - database::models::pat_item::PersonalAccessToken::remove(pat.id, &mut transaction) - .await?; + database::models::pat_item::PersonalAccessToken::remove( + pat.id, + &mut transaction, + ) + .await?; transaction.commit().await?; database::models::pat_item::PersonalAccessToken::clear_cache( vec![(Some(pat.id), Some(pat.access_token), Some(pat.user_id))], diff --git a/apps/labrinth/src/routes/internal/session.rs b/apps/labrinth/src/routes/internal/session.rs index e2435e48c..b928c70db 100644 --- a/apps/labrinth/src/routes/internal/session.rs +++ b/apps/labrinth/src/routes/internal/session.rs @@ -152,7 +152,9 @@ pub async fn list( .and_then(|x| x.to_str().ok()) .ok_or_else(|| AuthenticationError::InvalidCredentials)?; - let session_ids = DBSession::get_user_sessions(current_user.id.into(), &**pool, &redis).await?; + let session_ids = + DBSession::get_user_sessions(current_user.id.into(), &**pool, &redis) + .await?; let sessions = DBSession::get_many_ids(&session_ids, &**pool, &redis) .await? .into_iter() @@ -210,19 +212,24 @@ pub async fn refresh( redis: Data, session_queue: Data, ) -> Result { - let current_user = get_user_from_headers(&req, &**pool, &redis, &session_queue, None) - .await? - .1; + let current_user = + get_user_from_headers(&req, &**pool, &redis, &session_queue, None) + .await? + .1; let session = req .headers() .get(AUTHORIZATION) .and_then(|x| x.to_str().ok()) - .ok_or_else(|| ApiError::Authentication(AuthenticationError::InvalidCredentials))?; + .ok_or_else(|| { + ApiError::Authentication(AuthenticationError::InvalidCredentials) + })?; let session = DBSession::get(session, &**pool, &redis).await?; if let Some(session) = session { - if current_user.id != session.user_id.into() || session.refresh_expires < Utc::now() { + if current_user.id != session.user_id.into() + || session.refresh_expires < Utc::now() + { return Err(ApiError::Authentication( AuthenticationError::InvalidCredentials, )); @@ -231,7 +238,9 @@ pub async fn refresh( let mut transaction = pool.begin().await?; DBSession::remove(session.id, &mut transaction).await?; - let new_session = issue_session(req, session.user_id, &mut transaction, &redis).await?; + let new_session = + issue_session(req, session.user_id, &mut transaction, &redis) + .await?; transaction.commit().await?; DBSession::clear_cache( vec![( diff --git a/apps/labrinth/src/routes/maven.rs b/apps/labrinth/src/routes/maven.rs index 37cfe17de..c337127a2 100644 --- a/apps/labrinth/src/routes/maven.rs +++ b/apps/labrinth/src/routes/maven.rs @@ -77,7 +77,9 @@ pub async fn maven_metadata( session_queue: web::Data, ) -> Result { let project_id = params.into_inner().0; - let Some(project) = database::models::Project::get(&project_id, &**pool, &redis).await? else { + let Some(project) = + database::models::Project::get(&project_id, &**pool, &redis).await? + else { return Err(ApiError::NotFound); }; @@ -145,7 +147,11 @@ pub async fn maven_metadata( versions: Versions { versions: new_versions, }, - last_updated: project.inner.updated.format("%Y%m%d%H%M%S").to_string(), + last_updated: project + .inner + .updated + .format("%Y%m%d%H%M%S") + .to_string(), }, }; @@ -164,11 +170,16 @@ async fn find_version( .ok() .map(|x| x as i64); - let all_versions = database::models::Version::get_many(&project.versions, pool, redis).await?; + let all_versions = + database::models::Version::get_many(&project.versions, pool, redis) + .await?; let exact_matches = all_versions .iter() - .filter(|x| &x.inner.version_number == vcoords || Some(x.inner.id.0) == id_option) + .filter(|x| { + &x.inner.version_number == vcoords + || Some(x.inner.id.0) == id_option + }) .collect::>(); if exact_matches.len() == 1 { @@ -202,11 +213,10 @@ async fn find_version( // For maven in particular, we will hardcode it to use GameVersions rather than generic loader fields, as this is minecraft-java exclusive if !game_versions.is_empty() { - let version_game_versions = x - .version_fields - .clone() - .into_iter() - .find_map(|v| MinecraftGameVersion::try_from_version_field(&v).ok()); + let version_game_versions = + x.version_fields.clone().into_iter().find_map(|v| { + MinecraftGameVersion::try_from_version_field(&v).ok() + }); if let Some(version_game_versions) = version_game_versions { bool &= version_game_versions .iter() @@ -231,7 +241,9 @@ fn find_file<'a>( version: &'a QueryVersion, file: &str, ) -> Option<&'a QueryFile> { - if let Some(selected_file) = version.files.iter().find(|x| x.filename == file) { + if let Some(selected_file) = + version.files.iter().find(|x| x.filename == file) + { return Some(selected_file); } @@ -271,7 +283,9 @@ pub async fn version_file( session_queue: web::Data, ) -> Result { let (project_id, vnum, file) = params.into_inner(); - let Some(project) = database::models::Project::get(&project_id, &**pool, &redis).await? else { + let Some(project) = + database::models::Project::get(&project_id, &**pool, &redis).await? + else { return Err(ApiError::NotFound); }; @@ -290,7 +304,8 @@ pub async fn version_file( return Err(ApiError::NotFound); } - let Some(version) = find_version(&project, &vnum, &pool, &redis).await? else { + let Some(version) = find_version(&project, &vnum, &pool, &redis).await? + else { return Err(ApiError::NotFound); }; @@ -314,7 +329,9 @@ pub async fn version_file( return Ok(HttpResponse::Ok() .content_type("text/xml") .body(yaserde::ser::to_string(&respdata).map_err(ApiError::Xml)?)); - } else if let Some(selected_file) = find_file(&project_id, &vnum, &version, &file) { + } else if let Some(selected_file) = + find_file(&project_id, &vnum, &version, &file) + { return Ok(HttpResponse::TemporaryRedirect() .append_header(("location", &*selected_file.url)) .body("")); @@ -332,7 +349,9 @@ pub async fn version_file_sha1( session_queue: web::Data, ) -> Result { let (project_id, vnum, file) = params.into_inner(); - let Some(project) = database::models::Project::get(&project_id, &**pool, &redis).await? else { + let Some(project) = + database::models::Project::get(&project_id, &**pool, &redis).await? + else { return Err(ApiError::NotFound); }; @@ -351,7 +370,8 @@ pub async fn version_file_sha1( return Err(ApiError::NotFound); } - let Some(version) = find_version(&project, &vnum, &pool, &redis).await? else { + let Some(version) = find_version(&project, &vnum, &pool, &redis).await? + else { return Err(ApiError::NotFound); }; @@ -374,7 +394,9 @@ pub async fn version_file_sha512( session_queue: web::Data, ) -> Result { let (project_id, vnum, file) = params.into_inner(); - let Some(project) = database::models::Project::get(&project_id, &**pool, &redis).await? else { + let Some(project) = + database::models::Project::get(&project_id, &**pool, &redis).await? + else { return Err(ApiError::NotFound); }; @@ -393,7 +415,8 @@ pub async fn version_file_sha512( return Err(ApiError::NotFound); } - let Some(version) = find_version(&project, &vnum, &pool, &redis).await? else { + let Some(version) = find_version(&project, &vnum, &pool, &redis).await? + else { return Err(ApiError::NotFound); }; diff --git a/apps/labrinth/src/routes/mod.rs b/apps/labrinth/src/routes/mod.rs index e0b813274..38247d053 100644 --- a/apps/labrinth/src/routes/mod.rs +++ b/apps/labrinth/src/routes/mod.rs @@ -39,11 +39,16 @@ pub fn root_config(cfg: &mut web::ServiceConfig) { Cors::default() .allowed_origin_fn(|origin, _req_head| { let allowed_origins = - parse_strings_from_var("ANALYTICS_ALLOWED_ORIGINS").unwrap_or_default(); + parse_strings_from_var("ANALYTICS_ALLOWED_ORIGINS") + .unwrap_or_default(); allowed_origins.contains(&"*".to_string()) - || allowed_origins - .contains(&origin.to_str().unwrap_or_default().to_string()) + || allowed_origins.contains( + &origin + .to_str() + .unwrap_or_default() + .to_string(), + ) }) .allowed_methods(vec!["GET", "POST"]) .allowed_headers(vec![ diff --git a/apps/labrinth/src/routes/updates.rs b/apps/labrinth/src/routes/updates.rs index e3e9c7fa9..64ee7b218 100644 --- a/apps/labrinth/src/routes/updates.rs +++ b/apps/labrinth/src/routes/updates.rs @@ -61,7 +61,9 @@ pub async fn forge_updates( return Err(ApiError::InvalidInput(ERROR.to_string())); } - let versions = database::models::Version::get_many(&project.versions, &**pool, &redis).await?; + let versions = + database::models::Version::get_many(&project.versions, &**pool, &redis) + .await?; let loaders = match &*neo.neoforge { "only" => |x: &String| *x == "neoforge", @@ -105,7 +107,9 @@ pub async fn forge_updates( .fields .iter() .find(|(key, _)| key.as_str() == MinecraftGameVersion::FIELD_NAME) - .and_then(|(_, value)| serde_json::from_value::>(value.clone()).ok()) + .and_then(|(_, value)| { + serde_json::from_value::>(value.clone()).ok() + }) .unwrap_or_default(); if version.version_type == VersionType::Release { diff --git a/apps/labrinth/src/routes/v2/moderation.rs b/apps/labrinth/src/routes/v2/moderation.rs index db4517a8a..e961da24a 100644 --- a/apps/labrinth/src/routes/v2/moderation.rs +++ b/apps/labrinth/src/routes/v2/moderation.rs @@ -43,7 +43,8 @@ pub async fn get_projects( // Convert to V2 projects match v2_reroute::extract_ok_json::>(response).await { Ok(project) => { - let legacy_projects = LegacyProject::from_many(project, &**pool, &redis).await?; + let legacy_projects = + LegacyProject::from_many(project, &**pool, &redis).await?; Ok(HttpResponse::Ok().json(legacy_projects)) } Err(response) => Ok(response), diff --git a/apps/labrinth/src/routes/v2/notifications.rs b/apps/labrinth/src/routes/v2/notifications.rs index a6df95310..85810d614 100644 --- a/apps/labrinth/src/routes/v2/notifications.rs +++ b/apps/labrinth/src/routes/v2/notifications.rs @@ -65,9 +65,15 @@ pub async fn notification_get( redis: web::Data, session_queue: web::Data, ) -> Result { - let response = v3::notifications::notification_get(req, info, pool, redis, session_queue) - .await - .or_else(v2_reroute::flatten_404_error)?; + let response = v3::notifications::notification_get( + req, + info, + pool, + redis, + session_queue, + ) + .await + .or_else(v2_reroute::flatten_404_error)?; match v2_reroute::extract_ok_json::(response).await { Ok(notification) => { let notification = LegacyNotification::from(notification); @@ -100,9 +106,15 @@ pub async fn notification_delete( session_queue: web::Data, ) -> Result { // Returns NoContent, so no need to convert - v3::notifications::notification_delete(req, info, pool, redis, session_queue) - .await - .or_else(v2_reroute::flatten_404_error) + v3::notifications::notification_delete( + req, + info, + pool, + redis, + session_queue, + ) + .await + .or_else(v2_reroute::flatten_404_error) } #[patch("notifications")] diff --git a/apps/labrinth/src/routes/v2/project_creation.rs b/apps/labrinth/src/routes/v2/project_creation.rs index 2fc728ef8..decc71a3d 100644 --- a/apps/labrinth/src/routes/v2/project_creation.rs +++ b/apps/labrinth/src/routes/v2/project_creation.rs @@ -4,7 +4,9 @@ use crate::file_hosting::FileHost; use crate::models; use crate::models::ids::ImageId; use crate::models::projects::{Loader, Project, ProjectStatus}; -use crate::models::v2::projects::{DonationLink, LegacyProject, LegacySideType}; +use crate::models::v2::projects::{ + DonationLink, LegacyProject, LegacySideType, +}; use crate::queue::session::AuthQueue; use crate::routes::v3::project_creation::default_project_type; use crate::routes::v3::project_creation::{CreateError, NewGalleryItem}; @@ -158,13 +160,22 @@ pub async fn project_create( .into_iter() .map(|v| { let mut fields = HashMap::new(); - fields.extend(v2_reroute::convert_side_types_v3(client_side, server_side)); - fields.insert("game_versions".to_string(), json!(v.game_versions)); + fields.extend(v2_reroute::convert_side_types_v3( + client_side, + server_side, + )); + fields.insert( + "game_versions".to_string(), + json!(v.game_versions), + ); // Modpacks now use the "mrpack" loader, and loaders are converted to loader fields. // Setting of 'project_type' directly is removed, it's loader-based now. if project_type == "modpack" { - fields.insert("mrpack_loaders".to_string(), json!(v.loaders)); + fields.insert( + "mrpack_loaders".to_string(), + json!(v.loaders), + ); } let loaders = if project_type == "modpack" { @@ -248,7 +259,10 @@ pub async fn project_create( match v2_reroute::extract_ok_json::(response).await { Ok(project) => { let version_item = match project.versions.first() { - Some(vid) => version_item::Version::get((*vid).into(), &**client, &redis).await?, + Some(vid) => { + version_item::Version::get((*vid).into(), &**client, &redis) + .await? + } None => None, }; let project = LegacyProject::from(project, version_item); diff --git a/apps/labrinth/src/routes/v2/projects.rs b/apps/labrinth/src/routes/v2/projects.rs index f5ce258ce..3ee33336b 100644 --- a/apps/labrinth/src/routes/v2/projects.rs +++ b/apps/labrinth/src/routes/v2/projects.rs @@ -5,7 +5,9 @@ use crate::file_hosting::FileHost; use crate::models::projects::{ Link, MonetizationStatus, Project, ProjectStatus, SearchRequest, Version, }; -use crate::models::v2::projects::{DonationLink, LegacyProject, LegacySideType, LegacyVersion}; +use crate::models::v2::projects::{ + DonationLink, LegacyProject, LegacySideType, LegacyVersion, +}; use crate::models::v2::search::LegacySearchResults; use crate::queue::moderation::AutomatedModerationQueue; use crate::queue::session::AuthQueue; @@ -71,7 +73,9 @@ pub async fn project_search( facet .into_iter() .map(|facet| { - if let Some((key, operator, val)) = parse_facet(&facet) { + if let Some((key, operator, val)) = + parse_facet(&facet) + { format!( "{}{}{}", match key.as_str() { @@ -155,15 +159,19 @@ pub async fn random_projects_get( ) -> Result { let count = v3::projects::RandomProjects { count: count.count }; - let response = - v3::projects::random_projects_get(web::Query(count), pool.clone(), redis.clone()) - .await - .or_else(v2_reroute::flatten_404_error) - .or_else(v2_reroute::flatten_404_error)?; + let response = v3::projects::random_projects_get( + web::Query(count), + pool.clone(), + redis.clone(), + ) + .await + .or_else(v2_reroute::flatten_404_error) + .or_else(v2_reroute::flatten_404_error)?; // Convert response to V2 format match v2_reroute::extract_ok_json::>(response).await { Ok(project) => { - let legacy_projects = LegacyProject::from_many(project, &**pool, &redis).await?; + let legacy_projects = + LegacyProject::from_many(project, &**pool, &redis).await?; Ok(HttpResponse::Ok().json(legacy_projects)) } Err(response) => Ok(response), @@ -193,7 +201,8 @@ pub async fn projects_get( // Convert response to V2 format match v2_reroute::extract_ok_json::>(response).await { Ok(project) => { - let legacy_projects = LegacyProject::from_many(project, &**pool, &redis).await?; + let legacy_projects = + LegacyProject::from_many(project, &**pool, &redis).await?; Ok(HttpResponse::Ok().json(legacy_projects)) } Err(response) => Ok(response), @@ -210,15 +219,24 @@ pub async fn project_get( ) -> Result { // Convert V2 data to V3 data // Call V3 project creation - let response = v3::projects::project_get(req, info, pool.clone(), redis.clone(), session_queue) - .await - .or_else(v2_reroute::flatten_404_error)?; + let response = v3::projects::project_get( + req, + info, + pool.clone(), + redis.clone(), + session_queue, + ) + .await + .or_else(v2_reroute::flatten_404_error)?; // Convert response to V2 format match v2_reroute::extract_ok_json::(response).await { Ok(project) => { let version_item = match project.versions.first() { - Some(vid) => version_item::Version::get((*vid).into(), &**pool, &redis).await?, + Some(vid) => { + version_item::Version::get((*vid).into(), &**pool, &redis) + .await? + } None => None, }; let project = LegacyProject::from(project, version_item); @@ -256,16 +274,28 @@ pub async fn dependency_list( session_queue: web::Data, ) -> Result { // TODO: tests, probably - let response = - v3::projects::dependency_list(req, info, pool.clone(), redis.clone(), session_queue) - .await - .or_else(v2_reroute::flatten_404_error)?; + let response = v3::projects::dependency_list( + req, + info, + pool.clone(), + redis.clone(), + session_queue, + ) + .await + .or_else(v2_reroute::flatten_404_error)?; - match v2_reroute::extract_ok_json::(response).await + match v2_reroute::extract_ok_json::< + crate::routes::v3::projects::DependencyInfo, + >(response) + .await { Ok(dependency_info) => { - let converted_projects = - LegacyProject::from_many(dependency_info.projects, &**pool, &redis).await?; + let converted_projects = LegacyProject::from_many( + dependency_info.projects, + &**pool, + &redis, + ) + .await?; let converted_versions = dependency_info .versions .into_iter() @@ -443,7 +473,8 @@ pub async fn project_edit( // (resetting to the new ones) if let Some(donation_urls) = v2_new_project.donation_urls { // Fetch current donation links from project so we know what to delete - let fetched_example_project = project_item::Project::get(&info.0, &**pool, &redis).await?; + let fetched_example_project = + project_item::Project::get(&info.0, &**pool, &redis).await?; let donation_links = fetched_example_project .map(|x| { x.urls @@ -504,11 +535,19 @@ pub async fn project_edit( // If client and server side were set, we will call // the version setting route for each version to set the side types for each of them. - if response.status().is_success() && (client_side.is_some() || server_side.is_some()) { - let project_item = - project_item::Project::get(&new_slug.unwrap_or(project_id), &**pool, &redis).await?; + if response.status().is_success() + && (client_side.is_some() || server_side.is_some()) + { + let project_item = project_item::Project::get( + &new_slug.unwrap_or(project_id), + &**pool, + &redis, + ) + .await?; let version_ids = project_item.map(|x| x.versions).unwrap_or_default(); - let versions = version_item::Version::get_many(&version_ids, &**pool, &redis).await?; + let versions = + version_item::Version::get_many(&version_ids, &**pool, &redis) + .await?; for version in versions { let version = Version::from(version); let mut fields = version.fields; @@ -516,7 +555,10 @@ pub async fn project_edit( v2_reroute::convert_side_types_v2(&fields, None); let client_side = client_side.unwrap_or(current_client_side); let server_side = server_side.unwrap_or(current_server_side); - fields.extend(v2_reroute::convert_side_types_v3(client_side, server_side)); + fields.extend(v2_reroute::convert_side_types_v3( + client_side, + server_side, + )); response = v3::versions::version_edit_helper( req.clone(), @@ -682,8 +724,10 @@ pub async fn projects_edit( add_categories: bulk_edit_project.add_categories, remove_categories: bulk_edit_project.remove_categories, additional_categories: bulk_edit_project.additional_categories, - add_additional_categories: bulk_edit_project.add_additional_categories, - remove_additional_categories: bulk_edit_project.remove_additional_categories, + add_additional_categories: bulk_edit_project + .add_additional_categories, + remove_additional_categories: bulk_edit_project + .remove_additional_categories, link_urls: Some(link_urls), }), redis, @@ -735,9 +779,16 @@ pub async fn delete_project_icon( session_queue: web::Data, ) -> Result { // Returns NoContent, so no need to convert - v3::projects::delete_project_icon(req, info, pool, redis, file_host, session_queue) - .await - .or_else(v2_reroute::flatten_404_error) + v3::projects::delete_project_icon( + req, + info, + pool, + redis, + file_host, + session_queue, + ) + .await + .or_else(v2_reroute::flatten_404_error) } #[derive(Serialize, Deserialize, Validate)] @@ -873,9 +924,16 @@ pub async fn project_delete( session_queue: web::Data, ) -> Result { // Returns NoContent, so no need to convert - v3::projects::project_delete(req, info, pool, redis, search_config, session_queue) - .await - .or_else(v2_reroute::flatten_404_error) + v3::projects::project_delete( + req, + info, + pool, + redis, + search_config, + session_queue, + ) + .await + .or_else(v2_reroute::flatten_404_error) } #[post("{id}/follow")] diff --git a/apps/labrinth/src/routes/v2/reports.rs b/apps/labrinth/src/routes/v2/reports.rs index 37495425f..35173102d 100644 --- a/apps/labrinth/src/routes/v2/reports.rs +++ b/apps/labrinth/src/routes/v2/reports.rs @@ -25,9 +25,10 @@ pub async fn report_create( redis: web::Data, session_queue: web::Data, ) -> Result { - let response = v3::reports::report_create(req, pool, body, redis, session_queue) - .await - .or_else(v2_reroute::flatten_404_error)?; + let response = + v3::reports::report_create(req, pool, body, redis, session_queue) + .await + .or_else(v2_reroute::flatten_404_error)?; // Convert response to V2 format match v2_reroute::extract_ok_json::(response).await { @@ -78,7 +79,8 @@ pub async fn reports( // Convert response to V2 format match v2_reroute::extract_ok_json::>(response).await { Ok(reports) => { - let reports: Vec<_> = reports.into_iter().map(LegacyReport::from).collect(); + let reports: Vec<_> = + reports.into_iter().map(LegacyReport::from).collect(); Ok(HttpResponse::Ok().json(reports)) } Err(response) => Ok(response), @@ -111,7 +113,8 @@ pub async fn reports_get( // Convert response to V2 format match v2_reroute::extract_ok_json::>(response).await { Ok(report_list) => { - let report_list: Vec<_> = report_list.into_iter().map(LegacyReport::from).collect(); + let report_list: Vec<_> = + report_list.into_iter().map(LegacyReport::from).collect(); Ok(HttpResponse::Ok().json(report_list)) } Err(response) => Ok(response), @@ -126,9 +129,10 @@ pub async fn report_get( info: web::Path<(crate::models::reports::ReportId,)>, session_queue: web::Data, ) -> Result { - let response = v3::reports::report_get(req, pool, redis, info, session_queue) - .await - .or_else(v2_reroute::flatten_404_error)?; + let response = + v3::reports::report_get(req, pool, redis, info, session_queue) + .await + .or_else(v2_reroute::flatten_404_error)?; // Convert response to V2 format match v2_reroute::extract_ok_json::(response).await { diff --git a/apps/labrinth/src/routes/v2/statistics.rs b/apps/labrinth/src/routes/v2/statistics.rs index b5f5b7817..bd98da12e 100644 --- a/apps/labrinth/src/routes/v2/statistics.rs +++ b/apps/labrinth/src/routes/v2/statistics.rs @@ -19,7 +19,9 @@ pub struct V2Stats { } #[get("statistics")] -pub async fn get_stats(pool: web::Data) -> Result { +pub async fn get_stats( + pool: web::Data, +) -> Result { let response = v3::statistics::get_stats(pool) .await .or_else(v2_reroute::flatten_404_error)?; diff --git a/apps/labrinth/src/routes/v2/tags.rs b/apps/labrinth/src/routes/v2/tags.rs index cf8cb5012..3233e9aed 100644 --- a/apps/labrinth/src/routes/v2/tags.rs +++ b/apps/labrinth/src/routes/v2/tags.rs @@ -43,7 +43,9 @@ pub async fn category_list( let response = v3::tags::category_list(pool, redis).await?; // Convert to V2 format - match v2_reroute::extract_ok_json::>(response).await { + match v2_reroute::extract_ok_json::>(response) + .await + { Ok(categories) => { let categories = categories .into_iter() @@ -75,7 +77,9 @@ pub async fn loader_list( let response = v3::tags::loader_list(pool, redis).await?; // Convert to V2 format - match v2_reroute::extract_ok_json::>(response).await { + match v2_reroute::extract_ok_json::>(response) + .await + { Ok(loaders) => { let loaders = loaders .into_iter() @@ -86,12 +90,15 @@ pub async fn loader_list( // a project type before any versions are set. supported_project_types.push("project".to_string()); - if ["forge", "fabric", "quilt", "neoforge"].contains(&&*l.name) { + if ["forge", "fabric", "quilt", "neoforge"] + .contains(&&*l.name) + { supported_project_types.push("modpack".to_string()); } if supported_project_types.contains(&"datapack".to_string()) - || supported_project_types.contains(&"plugin".to_string()) + || supported_project_types + .contains(&"plugin".to_string()) { supported_project_types.push("mod".to_string()); } @@ -149,7 +156,9 @@ pub async fn game_version_list( // Convert to V2 format Ok( - match v2_reroute::extract_ok_json::>(response).await { + match v2_reroute::extract_ok_json::>(response) + .await + { Ok(fields) => { let fields = fields .into_iter() @@ -187,7 +196,8 @@ pub async fn license_list() -> HttpResponse { let response = v3::tags::license_list().await; // Convert to V2 format - match v2_reroute::extract_ok_json::>(response).await { + match v2_reroute::extract_ok_json::>(response).await + { Ok(licenses) => { let licenses = licenses .into_iter() @@ -209,14 +219,18 @@ pub struct LicenseText { } #[get("license/{id}")] -pub async fn license_text(params: web::Path<(String,)>) -> Result { +pub async fn license_text( + params: web::Path<(String,)>, +) -> Result { let license = v3::tags::license_text(params) .await .or_else(v2_reroute::flatten_404_error)?; // Convert to V2 format Ok( - match v2_reroute::extract_ok_json::(license).await { + match v2_reroute::extract_ok_json::(license) + .await + { Ok(license) => HttpResponse::Ok().json(LicenseText { title: license.title, body: license.body, @@ -244,7 +258,11 @@ pub async fn donation_platform_list( // Convert to V2 format Ok( - match v2_reroute::extract_ok_json::>(response).await { + match v2_reroute::extract_ok_json::>( + response, + ) + .await + { Ok(platforms) => { let platforms = platforms .into_iter() diff --git a/apps/labrinth/src/routes/v2/teams.rs b/apps/labrinth/src/routes/v2/teams.rs index 6351cc131..444ff13ec 100644 --- a/apps/labrinth/src/routes/v2/teams.rs +++ b/apps/labrinth/src/routes/v2/teams.rs @@ -1,5 +1,7 @@ use crate::database::redis::RedisPool; -use crate::models::teams::{OrganizationPermissions, ProjectPermissions, TeamId, TeamMember}; +use crate::models::teams::{ + OrganizationPermissions, ProjectPermissions, TeamId, TeamMember, +}; use crate::models::users::UserId; use crate::models::v2::teams::LegacyTeamMember; use crate::queue::session::AuthQueue; @@ -36,9 +38,15 @@ pub async fn team_members_get_project( redis: web::Data, session_queue: web::Data, ) -> Result { - let response = v3::teams::team_members_get_project(req, info, pool, redis, session_queue) - .await - .or_else(v2_reroute::flatten_404_error)?; + let response = v3::teams::team_members_get_project( + req, + info, + pool, + redis, + session_queue, + ) + .await + .or_else(v2_reroute::flatten_404_error)?; // Convert response to V2 format match v2_reroute::extract_ok_json::>(response).await { Ok(members) => { @@ -61,9 +69,10 @@ pub async fn team_members_get( redis: web::Data, session_queue: web::Data, ) -> Result { - let response = v3::teams::team_members_get(req, info, pool, redis, session_queue) - .await - .or_else(v2_reroute::flatten_404_error)?; + let response = + v3::teams::team_members_get(req, info, pool, redis, session_queue) + .await + .or_else(v2_reroute::flatten_404_error)?; // Convert response to V2 format match v2_reroute::extract_ok_json::>(response).await { Ok(members) => { diff --git a/apps/labrinth/src/routes/v2/threads.rs b/apps/labrinth/src/routes/v2/threads.rs index 7b3c4f718..ab5e781a5 100644 --- a/apps/labrinth/src/routes/v2/threads.rs +++ b/apps/labrinth/src/routes/v2/threads.rs @@ -110,7 +110,14 @@ pub async fn message_delete( file_host: web::Data>, ) -> Result { // Returns NoContent, so we don't need to convert the response - v3::threads::message_delete(req, info, pool, redis, session_queue, file_host) - .await - .or_else(v2_reroute::flatten_404_error) + v3::threads::message_delete( + req, + info, + pool, + redis, + session_queue, + file_host, + ) + .await + .or_else(v2_reroute::flatten_404_error) } diff --git a/apps/labrinth/src/routes/v2/users.rs b/apps/labrinth/src/routes/v2/users.rs index 5835f6ffe..b7b30c22d 100644 --- a/apps/labrinth/src/routes/v2/users.rs +++ b/apps/labrinth/src/routes/v2/users.rs @@ -64,15 +64,19 @@ pub async fn users_get( pool: web::Data, redis: web::Data, ) -> Result { - let response = - v3::users::users_get(web::Query(v3::users::UserIds { ids: ids.ids }), pool, redis) - .await - .or_else(v2_reroute::flatten_404_error)?; + let response = v3::users::users_get( + web::Query(v3::users::UserIds { ids: ids.ids }), + pool, + redis, + ) + .await + .or_else(v2_reroute::flatten_404_error)?; // Convert response to V2 format match v2_reroute::extract_ok_json::>(response).await { Ok(users) => { - let legacy_users: Vec = users.into_iter().map(LegacyUser::from).collect(); + let legacy_users: Vec = + users.into_iter().map(LegacyUser::from).collect(); Ok(HttpResponse::Ok().json(legacy_users)) } Err(response) => Ok(response), @@ -107,14 +111,21 @@ pub async fn projects_list( redis: web::Data, session_queue: web::Data, ) -> Result { - let response = v3::users::projects_list(req, info, pool.clone(), redis.clone(), session_queue) - .await - .or_else(v2_reroute::flatten_404_error)?; + let response = v3::users::projects_list( + req, + info, + pool.clone(), + redis.clone(), + session_queue, + ) + .await + .or_else(v2_reroute::flatten_404_error)?; // Convert to V2 projects match v2_reroute::extract_ok_json::>(response).await { Ok(project) => { - let legacy_projects = LegacyProject::from_many(project, &**pool, &redis).await?; + let legacy_projects = + LegacyProject::from_many(project, &**pool, &redis).await?; Ok(HttpResponse::Ok().json(legacy_projects)) } Err(response) => Ok(response), @@ -230,14 +241,21 @@ pub async fn user_follows( redis: web::Data, session_queue: web::Data, ) -> Result { - let response = v3::users::user_follows(req, info, pool.clone(), redis.clone(), session_queue) - .await - .or_else(v2_reroute::flatten_404_error)?; + let response = v3::users::user_follows( + req, + info, + pool.clone(), + redis.clone(), + session_queue, + ) + .await + .or_else(v2_reroute::flatten_404_error)?; // Convert to V2 projects match v2_reroute::extract_ok_json::>(response).await { Ok(project) => { - let legacy_projects = LegacyProject::from_many(project, &**pool, &redis).await?; + let legacy_projects = + LegacyProject::from_many(project, &**pool, &redis).await?; Ok(HttpResponse::Ok().json(legacy_projects)) } Err(response) => Ok(response), @@ -252,9 +270,10 @@ pub async fn user_notifications( redis: web::Data, session_queue: web::Data, ) -> Result { - let response = v3::users::user_notifications(req, info, pool, redis, session_queue) - .await - .or_else(v2_reroute::flatten_404_error)?; + let response = + v3::users::user_notifications(req, info, pool, redis, session_queue) + .await + .or_else(v2_reroute::flatten_404_error)?; // Convert response to V2 format match v2_reroute::extract_ok_json::>(response).await { Ok(notifications) => { diff --git a/apps/labrinth/src/routes/v2/version_creation.rs b/apps/labrinth/src/routes/v2/version_creation.rs index cd4335a16..ba8248db7 100644 --- a/apps/labrinth/src/routes/v2/version_creation.rs +++ b/apps/labrinth/src/routes/v2/version_creation.rs @@ -4,7 +4,8 @@ use crate::database::redis::RedisPool; use crate::file_hosting::FileHost; use crate::models::ids::ImageId; use crate::models::projects::{ - Dependency, FileType, Loader, ProjectId, Version, VersionId, VersionStatus, VersionType, + Dependency, FileType, Loader, ProjectId, Version, VersionId, VersionStatus, + VersionType, }; use crate::models::v2::projects::LegacyVersion; use crate::queue::moderation::AutomatedModerationQueue; @@ -93,7 +94,8 @@ pub async fn version_create( let payload = v2_reroute::alter_actix_multipart( payload, req.headers().clone(), - |legacy_create: InitialVersionData, content_dispositions: Vec| { + |legacy_create: InitialVersionData, + content_dispositions: Vec| { let client = client.clone(); let redis = redis.clone(); async move { @@ -105,19 +107,27 @@ pub async fn version_create( ); // Get all possible side-types for loaders given- we will use these to check if we need to convert/apply singleplayer, etc. - let loaders = match v3::tags::loader_list(client.clone(), redis.clone()).await { - Ok(loader_response) => { - (v2_reroute::extract_ok_json::>(loader_response) + let loaders = + match v3::tags::loader_list(client.clone(), redis.clone()) + .await + { + Ok(loader_response) => { + (v2_reroute::extract_ok_json::< + Vec, + >(loader_response) .await) - .unwrap_or_default() - } - Err(_) => vec![], - }; + .unwrap_or_default() + } + Err(_) => vec![], + }; let loader_fields_aggregate = loaders .into_iter() .filter_map(|loader| { - if legacy_create.loaders.contains(&Loader(loader.name.clone())) { + if legacy_create + .loaders + .contains(&Loader(loader.name.clone())) + { Some(loader.supported_fields) } else { None @@ -150,15 +160,29 @@ pub async fn version_create( .map(|f| (f.to_string(), json!(false))), ); if let Some(example_version_fields) = - get_example_version_fields(legacy_create.project_id, client, &redis).await? + get_example_version_fields( + legacy_create.project_id, + client, + &redis, + ) + .await? { - fields.extend(example_version_fields.into_iter().filter_map(|f| { - if side_type_loader_field_names.contains(&f.field_name.as_str()) { - Some((f.field_name, f.value.serialize_internal())) - } else { - None - } - })); + fields.extend( + example_version_fields.into_iter().filter_map( + |f| { + if side_type_loader_field_names + .contains(&f.field_name.as_str()) + { + Some(( + f.field_name, + f.value.serialize_internal(), + )) + } else { + None + } + }, + ), + ); } } // Handle project type via file extension prediction @@ -180,9 +204,14 @@ pub async fn version_create( // Similarly, check actual content disposition for mrpacks, in case file_parts is wrong for content_disposition in content_dispositions { // Uses version_create functions to get the file name and extension - let (_, file_extension) = version_creation::get_name_ext(&content_disposition)?; + let (_, file_extension) = + version_creation::get_name_ext(&content_disposition)?; crate::util::ext::project_file_type(file_extension) - .ok_or_else(|| CreateError::InvalidFileType(file_extension.to_string()))?; + .ok_or_else(|| { + CreateError::InvalidFileType( + file_extension.to_string(), + ) + })?; if file_extension == "mrpack" { project_type = Some("modpack"); @@ -193,7 +222,10 @@ pub async fn version_create( // Modpacks now use the "mrpack" loader, and loaders are converted to loader fields. // Setting of 'project_type' directly is removed, it's loader-based now. if project_type == Some("modpack") { - fields.insert("mrpack_loaders".to_string(), json!(legacy_create.loaders)); + fields.insert( + "mrpack_loaders".to_string(), + json!(legacy_create.loaders), + ); } let loaders = if project_type == Some("modpack") { @@ -257,18 +289,20 @@ async fn get_example_version_fields( None => return Ok(None), }; - let vid = match project_item::Project::get_id(project_id.into(), &**pool, redis) - .await? - .and_then(|p| p.versions.first().cloned()) - { - Some(vid) => vid, - None => return Ok(None), - }; + let vid = + match project_item::Project::get_id(project_id.into(), &**pool, redis) + .await? + .and_then(|p| p.versions.first().cloned()) + { + Some(vid) => vid, + None => return Ok(None), + }; - let example_version = match version_item::Version::get(vid, &**pool, redis).await? { - Some(version) => version, - None => return Ok(None), - }; + let example_version = + match version_item::Version::get(vid, &**pool, redis).await? { + Some(version) => version, + None => return Ok(None), + }; Ok(Some(example_version.version_fields)) } diff --git a/apps/labrinth/src/routes/v2/version_file.rs b/apps/labrinth/src/routes/v2/version_file.rs index 24e01270b..bb998b661 100644 --- a/apps/labrinth/src/routes/v2/version_file.rs +++ b/apps/labrinth/src/routes/v2/version_file.rs @@ -38,10 +38,16 @@ pub async fn get_version_from_hash( hash_query: web::Query, session_queue: web::Data, ) -> Result { - let response = - v3::version_file::get_version_from_hash(req, info, pool, redis, hash_query, session_queue) - .await - .or_else(v2_reroute::flatten_404_error)?; + let response = v3::version_file::get_version_from_hash( + req, + info, + pool, + redis, + hash_query, + session_queue, + ) + .await + .or_else(v2_reroute::flatten_404_error)?; // Convert response to V2 format match v2_reroute::extract_ok_json::(response).await { @@ -64,9 +70,16 @@ pub async fn download_version( session_queue: web::Data, ) -> Result { // Returns TemporaryRedirect, so no need to convert to V2 - v3::version_file::download_version(req, info, pool, redis, hash_query, session_queue) - .await - .or_else(v2_reroute::flatten_404_error) + v3::version_file::download_version( + req, + info, + pool, + redis, + hash_query, + session_queue, + ) + .await + .or_else(v2_reroute::flatten_404_error) } // under /api/v1/version_file/{hash} @@ -80,9 +93,16 @@ pub async fn delete_file( session_queue: web::Data, ) -> Result { // Returns NoContent, so no need to convert to V2 - v3::version_file::delete_file(req, info, pool, redis, hash_query, session_queue) - .await - .or_else(v2_reroute::flatten_404_error) + v3::version_file::delete_file( + req, + info, + pool, + redis, + hash_query, + session_queue, + ) + .await + .or_else(v2_reroute::flatten_404_error) } #[derive(Serialize, Deserialize)] @@ -171,7 +191,9 @@ pub async fn get_versions_from_hashes( .or_else(v2_reroute::flatten_404_error)?; // Convert to V2 - match v2_reroute::extract_ok_json::>(response).await { + match v2_reroute::extract_ok_json::>(response) + .await + { Ok(versions) => { let v2_versions = versions .into_iter() @@ -210,7 +232,9 @@ pub async fn get_projects_from_hashes( .or_else(v2_reroute::flatten_404_error)?; // Convert to V2 - match v2_reroute::extract_ok_json::>(response).await { + match v2_reroute::extract_ok_json::>(response) + .await + { Ok(projects_hashes) => { let hash_to_project_id = projects_hashes .iter() @@ -219,14 +243,19 @@ pub async fn get_projects_from_hashes( (hash.clone(), project_id) }) .collect::>(); - let legacy_projects = - LegacyProject::from_many(projects_hashes.into_values().collect(), &**pool, &redis) - .await?; + let legacy_projects = LegacyProject::from_many( + projects_hashes.into_values().collect(), + &**pool, + &redis, + ) + .await?; let legacy_projects_hashes = hash_to_project_id .into_iter() .filter_map(|(hash, project_id)| { - let legacy_project = - legacy_projects.iter().find(|x| x.id == project_id)?.clone(); + let legacy_project = legacy_projects + .iter() + .find(|x| x.id == project_id)? + .clone(); Some((hash, legacy_project)) }) .collect::>(); @@ -261,12 +290,15 @@ pub async fn update_files( hashes: update_data.hashes, }; - let response = v3::version_file::update_files(pool, redis, web::Json(update_data)) - .await - .or_else(v2_reroute::flatten_404_error)?; + let response = + v3::version_file::update_files(pool, redis, web::Json(update_data)) + .await + .or_else(v2_reroute::flatten_404_error)?; // Convert response to V2 format - match v2_reroute::extract_ok_json::>(response).await { + match v2_reroute::extract_ok_json::>(response) + .await + { Ok(returned_versions) => { let v3_versions = returned_versions .into_iter() @@ -316,7 +348,8 @@ pub async fn update_individual_files( game_versions.push(serde_json::json!(gv.clone())); } if !game_versions.is_empty() { - loader_fields.insert("game_versions".to_string(), game_versions); + loader_fields + .insert("game_versions".to_string(), game_versions); } v3::version_file::FileUpdateData { hash: x.hash.clone(), @@ -339,7 +372,9 @@ pub async fn update_individual_files( .or_else(v2_reroute::flatten_404_error)?; // Convert response to V2 format - match v2_reroute::extract_ok_json::>(response).await { + match v2_reroute::extract_ok_json::>(response) + .await + { Ok(returned_versions) => { let v3_versions = returned_versions .into_iter() diff --git a/apps/labrinth/src/routes/v2/versions.rs b/apps/labrinth/src/routes/v2/versions.rs index ecf128635..4d642542c 100644 --- a/apps/labrinth/src/routes/v2/versions.rs +++ b/apps/labrinth/src/routes/v2/versions.rs @@ -4,7 +4,9 @@ use super::ApiError; use crate::database::redis::RedisPool; use crate::models; use crate::models::ids::VersionId; -use crate::models::projects::{Dependency, FileType, Version, VersionStatus, VersionType}; +use crate::models::projects::{ + Dependency, FileType, Version, VersionStatus, VersionType, +}; use crate::models::v2::projects::LegacyVersion; use crate::queue::session::AuthQueue; use crate::routes::{v2_reroute, v3}; @@ -67,7 +69,8 @@ pub async fn version_list( for gv in versions { game_versions.push(serde_json::json!(gv.clone())); } - loader_fields.insert("game_versions".to_string(), game_versions); + loader_fields + .insert("game_versions".to_string(), game_versions); if let Some(ref loaders) = loaders { loader_fields.insert( @@ -94,10 +97,16 @@ pub async fn version_list( offset: filters.offset, }; - let response = - v3::versions::version_list(req, info, web::Query(filters), pool, redis, session_queue) - .await - .or_else(v2_reroute::flatten_404_error)?; + let response = v3::versions::version_list( + req, + info, + web::Query(filters), + pool, + redis, + session_queue, + ) + .await + .or_else(v2_reroute::flatten_404_error)?; // Convert response to V2 format match v2_reroute::extract_ok_json::>(response).await { @@ -122,9 +131,15 @@ pub async fn version_project_get( session_queue: web::Data, ) -> Result { let id = info.into_inner(); - let response = v3::versions::version_project_get_helper(req, id, pool, redis, session_queue) - .await - .or_else(v2_reroute::flatten_404_error)?; + let response = v3::versions::version_project_get_helper( + req, + id, + pool, + redis, + session_queue, + ) + .await + .or_else(v2_reroute::flatten_404_error)?; // Convert response to V2 format match v2_reroute::extract_ok_json::(response).await { Ok(version) => { @@ -149,9 +164,15 @@ pub async fn versions_get( session_queue: web::Data, ) -> Result { let ids = v3::versions::VersionIds { ids: ids.ids }; - let response = v3::versions::versions_get(req, web::Query(ids), pool, redis, session_queue) - .await - .or_else(v2_reroute::flatten_404_error)?; + let response = v3::versions::versions_get( + req, + web::Query(ids), + pool, + redis, + session_queue, + ) + .await + .or_else(v2_reroute::flatten_404_error)?; // Convert response to V2 format match v2_reroute::extract_ok_json::>(response).await { @@ -175,9 +196,10 @@ pub async fn version_get( session_queue: web::Data, ) -> Result { let id = info.into_inner().0; - let response = v3::versions::version_get_helper(req, id, pool, redis, session_queue) - .await - .or_else(v2_reroute::flatten_404_error)?; + let response = + v3::versions::version_get_helper(req, id, pool, redis, session_queue) + .await + .or_else(v2_reroute::flatten_404_error)?; // Convert response to V2 format match v2_reroute::extract_ok_json::(response).await { Ok(version) => { @@ -252,16 +274,19 @@ pub async fn version_edit( ) .await .or_else(v2_reroute::flatten_404_error)?; - let old_version = match v2_reroute::extract_ok_json::(old_version).await { - Ok(version) => version, - Err(response) => return Ok(response), - }; + let old_version = + match v2_reroute::extract_ok_json::(old_version).await { + Ok(version) => version, + Err(response) => return Ok(response), + }; // If this has 'mrpack_loaders' as a loader field previously, this is a modpack. // Therefore, if we are modifying the 'loader' field in this case, // we are actually modifying the 'mrpack_loaders' loader field let mut loaders = new_version.loaders.clone(); - if old_version.fields.contains_key("mrpack_loaders") && new_version.loaders.is_some() { + if old_version.fields.contains_key("mrpack_loaders") + && new_version.loaders.is_some() + { fields.insert( "mrpack_loaders".to_string(), serde_json::json!(new_version.loaders), @@ -315,7 +340,14 @@ pub async fn version_delete( search_config: web::Data, ) -> Result { // Returns NoContent, so we don't need to convert the response - v3::versions::version_delete(req, info, pool, redis, session_queue, search_config) - .await - .or_else(v2_reroute::flatten_404_error) + v3::versions::version_delete( + req, + info, + pool, + redis, + session_queue, + search_config, + ) + .await + .or_else(v2_reroute::flatten_404_error) } diff --git a/apps/labrinth/src/routes/v2_reroute.rs b/apps/labrinth/src/routes/v2_reroute.rs index e2146ab80..665f11a7f 100644 --- a/apps/labrinth/src/routes/v2_reroute.rs +++ b/apps/labrinth/src/routes/v2_reroute.rs @@ -3,14 +3,20 @@ use std::collections::HashMap; use super::v3::project_creation::CreateError; use super::ApiError; use crate::models::v2::projects::LegacySideType; -use crate::util::actix::{generate_multipart, MultipartSegment, MultipartSegmentData}; +use crate::util::actix::{ + generate_multipart, MultipartSegment, MultipartSegmentData, +}; use actix_multipart::Multipart; -use actix_web::http::header::{ContentDisposition, HeaderMap, TryIntoHeaderPair}; +use actix_web::http::header::{ + ContentDisposition, HeaderMap, TryIntoHeaderPair, +}; use actix_web::HttpResponse; use futures::{stream, Future, StreamExt}; use serde_json::{json, Value}; -pub async fn extract_ok_json(response: HttpResponse) -> Result +pub async fn extract_ok_json( + response: HttpResponse, +) -> Result where T: serde::de::DeserializeOwned, { @@ -27,7 +33,8 @@ where let bytes = actix_web::body::to_bytes(body) .await .map_err(|_| failure_http_response())?; - let json_value: T = serde_json::from_slice(&bytes).map_err(|_| failure_http_response())?; + let json_value: T = serde_json::from_slice(&bytes) + .map_err(|_| failure_http_response())?; Ok(json_value) } else { Err(response) @@ -119,9 +126,10 @@ where let json_value = json.ok_or(CreateError::InvalidInput( "No json segment found in multipart.".to_string(), ))?; - let mut json_segment = json_segment.ok_or(CreateError::InvalidInput( - "No json segment found in multipart.".to_string(), - ))?; + let mut json_segment = + json_segment.ok_or(CreateError::InvalidInput( + "No json segment found in multipart.".to_string(), + ))?; // Call closure, with the json value and names of the other segments let json_value: U = closure(json_value, content_dispositions).await?; @@ -144,11 +152,15 @@ where headers.insert(key, value); } Err(err) => { - CreateError::InvalidInput(format!("Error inserting test header: {:?}.", err)); + CreateError::InvalidInput(format!( + "Error inserting test header: {:?}.", + err + )); } }; - let new_multipart = Multipart::new(&headers, stream::once(async { Ok(payload) })); + let new_multipart = + Multipart::new(&headers, stream::once(async { Ok(payload) })); Ok(new_multipart) } @@ -165,10 +177,10 @@ pub fn convert_side_types_v3( || server_side == Required || server_side == Optional; let client_and_server = singleplayer; - let client_only = - (client_side == Required || client_side == Optional) && server_side != Required; - let server_only = - (server_side == Required || server_side == Optional) && client_side != Required; + let client_only = (client_side == Required || client_side == Optional) + && server_side != Required; + let server_only = (server_side == Required || server_side == Optional) + && client_side != Required; let mut fields = HashMap::new(); fields.insert("singleplayer".to_string(), json!(singleplayer)); @@ -181,7 +193,9 @@ pub fn convert_side_types_v3( // Converts plugin loaders from v2 to v3, for search facets // Within every 1st and 2nd level (the ones allowed in v2), we convert every instance of: // "project_type:mod" to "project_type:plugin" OR "project_type:mod" -pub fn convert_plugin_loader_facets_v3(facets: Vec>) -> Vec> { +pub fn convert_plugin_loader_facets_v3( + facets: Vec>, +) -> Vec> { facets .into_iter() .map(|inner_facets| { @@ -246,7 +260,8 @@ pub fn convert_side_types_v2_bools( Some("shader") => (Required, Unsupported), Some("resourcepack") => (Required, Unsupported), _ => { - let singleplayer = singleplayer.or(client_and_server).unwrap_or(false); + let singleplayer = + singleplayer.or(client_and_server).unwrap_or(false); match (singleplayer, client_only, server_only) { // Only singleplayer @@ -282,7 +297,9 @@ pub fn capitalize_first(input: &str) -> String { #[cfg(test)] mod tests { use super::*; - use crate::models::v2::projects::LegacySideType::{Optional, Required, Unsupported}; + use crate::models::v2::projects::LegacySideType::{ + Optional, Required, Unsupported, + }; #[test] fn convert_types() { @@ -300,8 +317,10 @@ mod tests { if lossy_pairs.contains(&(client_side, server_side)) { continue; } - let side_types = convert_side_types_v3(client_side, server_side); - let (client_side2, server_side2) = convert_side_types_v2(&side_types, None); + let side_types = + convert_side_types_v3(client_side, server_side); + let (client_side2, server_side2) = + convert_side_types_v2(&side_types, None); assert_eq!(client_side, client_side2); assert_eq!(server_side, server_side2); } diff --git a/apps/labrinth/src/routes/v3/analytics_get.rs b/apps/labrinth/src/routes/v3/analytics_get.rs index 9fa3d7f48..a31e753b4 100644 --- a/apps/labrinth/src/routes/v3/analytics_get.rs +++ b/apps/labrinth/src/routes/v3/analytics_get.rs @@ -98,7 +98,8 @@ pub async fn playtimes_get( // Convert String list to list of ProjectIds or VersionIds // - Filter out unauthorized projects/versions // - If no project_ids or version_ids are provided, we default to all projects the user has access to - let project_ids = filter_allowed_ids(project_ids, user, &pool, &redis, None).await?; + let project_ids = + filter_allowed_ids(project_ids, user, &pool, &redis, None).await?; // Get the views let playtimes = crate::clickhouse::fetch_playtimes( @@ -164,7 +165,8 @@ pub async fn views_get( // Convert String list to list of ProjectIds or VersionIds // - Filter out unauthorized projects/versions // - If no project_ids or version_ids are provided, we default to all projects the user has access to - let project_ids = filter_allowed_ids(project_ids, user, &pool, &redis, None).await?; + let project_ids = + filter_allowed_ids(project_ids, user, &pool, &redis, None).await?; // Get the views let views = crate::clickhouse::fetch_views( @@ -230,7 +232,9 @@ pub async fn downloads_get( // Convert String list to list of ProjectIds or VersionIds // - Filter out unauthorized projects/versions // - If no project_ids or version_ids are provided, we default to all projects the user has access to - let project_ids = filter_allowed_ids(project_ids, user_option, &pool, &redis, None).await?; + let project_ids = + filter_allowed_ids(project_ids, user_option, &pool, &redis, None) + .await?; // Get the downloads let downloads = crate::clickhouse::fetch_downloads( @@ -299,17 +303,26 @@ pub async fn revenue_get( // Round end_date up to nearest resolution let diff = end_date.timestamp() % (resolution_minutes as i64 * 60); - let end_date = end_date + Duration::seconds((resolution_minutes as i64 * 60) - diff); + let end_date = + end_date + Duration::seconds((resolution_minutes as i64 * 60) - diff); // Convert String list to list of ProjectIds or VersionIds // - Filter out unauthorized projects/versions // - If no project_ids or version_ids are provided, we default to all projects the user has access to - let project_ids = - filter_allowed_ids(project_ids, user.clone(), &pool, &redis, Some(true)).await?; + let project_ids = filter_allowed_ids( + project_ids, + user.clone(), + &pool, + &redis, + Some(true), + ) + .await?; let duration: PgInterval = Duration::minutes(resolution_minutes as i64) .try_into() - .map_err(|_| ApiError::InvalidInput("Invalid resolution_minutes".to_string()))?; + .map_err(|_| { + ApiError::InvalidInput("Invalid resolution_minutes".to_string()) + })?; // Get the revenue data let project_ids = project_ids.unwrap_or_default(); @@ -424,7 +437,8 @@ pub async fn countries_downloads_get( // Convert String list to list of ProjectIds or VersionIds // - Filter out unauthorized projects/versions // - If no project_ids or version_ids are provided, we default to all projects the user has access to - let project_ids = filter_allowed_ids(project_ids, user, &pool, &redis, None).await?; + let project_ids = + filter_allowed_ids(project_ids, user, &pool, &redis, None).await?; // Get the countries let countries = crate::clickhouse::fetch_countries_downloads( @@ -496,7 +510,8 @@ pub async fn countries_views_get( // Convert String list to list of ProjectIds or VersionIds // - Filter out unauthorized projects/versions // - If no project_ids or version_ids are provided, we default to all projects the user has access to - let project_ids = filter_allowed_ids(project_ids, user, &pool, &redis, None).await?; + let project_ids = + filter_allowed_ids(project_ids, user, &pool, &redis, None).await?; // Get the countries let countries = crate::clickhouse::fetch_countries_views( @@ -564,55 +579,68 @@ async fn filter_allowed_ids( // Convert String list to list of ProjectIds or VersionIds // - Filter out unauthorized projects/versions let project_ids = if let Some(project_strings) = project_ids { - let projects_data = - database::models::Project::get_many(&project_strings, &***pool, redis).await?; + let projects_data = database::models::Project::get_many( + &project_strings, + &***pool, + redis, + ) + .await?; let team_ids = projects_data .iter() .map(|x| x.inner.team_id) .collect::>(); let team_members = - database::models::TeamMember::get_from_team_full_many(&team_ids, &***pool, redis) - .await?; + database::models::TeamMember::get_from_team_full_many( + &team_ids, &***pool, redis, + ) + .await?; let organization_ids = projects_data .iter() .filter_map(|x| x.inner.organization_id) .collect::>(); - let organizations = - database::models::Organization::get_many_ids(&organization_ids, &***pool, redis) - .await?; + let organizations = database::models::Organization::get_many_ids( + &organization_ids, + &***pool, + redis, + ) + .await?; let organization_team_ids = organizations .iter() .map(|x| x.team_id) .collect::>(); - let organization_team_members = database::models::TeamMember::get_from_team_full_many( - &organization_team_ids, - &***pool, - redis, - ) - .await?; + let organization_team_members = + database::models::TeamMember::get_from_team_full_many( + &organization_team_ids, + &***pool, + redis, + ) + .await?; let ids = projects_data .into_iter() .filter(|project| { - let team_member = team_members - .iter() - .find(|x| x.team_id == project.inner.team_id && x.user_id == user.id.into()); + let team_member = team_members.iter().find(|x| { + x.team_id == project.inner.team_id + && x.user_id == user.id.into() + }); let organization = project .inner .organization_id .and_then(|oid| organizations.iter().find(|x| x.id == oid)); - let organization_team_member = if let Some(organization) = organization { - organization_team_members - .iter() - .find(|x| x.team_id == organization.team_id && x.user_id == user.id.into()) - } else { - None - }; + let organization_team_member = + if let Some(organization) = organization { + organization_team_members.iter().find(|x| { + x.team_id == organization.team_id + && x.user_id == user.id.into() + }) + } else { + None + }; let permissions = ProjectPermissions::get_permissions_by_role( &user.role, diff --git a/apps/labrinth/src/routes/v3/collections.rs b/apps/labrinth/src/routes/v3/collections.rs index 739cababf..6a9f19e39 100644 --- a/apps/labrinth/src/routes/v3/collections.rs +++ b/apps/labrinth/src/routes/v3/collections.rs @@ -1,6 +1,8 @@ use crate::auth::checks::is_visible_collection; use crate::auth::{filter_visible_collections, get_user_from_headers}; -use crate::database::models::{collection_item, generate_collection_id, project_item}; +use crate::database::models::{ + collection_item, generate_collection_id, project_item, +}; use crate::database::redis::RedisPool; use crate::file_hosting::FileHost; use crate::models::collections::{Collection, CollectionStatus}; @@ -74,13 +76,14 @@ pub async fn collection_create( .await? .1; - collection_create_data - .validate() - .map_err(|err| CreateError::InvalidInput(validation_errors_to_string(err, None)))?; + collection_create_data.validate().map_err(|err| { + CreateError::InvalidInput(validation_errors_to_string(err, None)) + })?; let mut transaction = client.begin().await?; - let collection_id: CollectionId = generate_collection_id(&mut transaction).await?.into(); + let collection_id: CollectionId = + generate_collection_id(&mut transaction).await?.into(); let initial_project_ids = project_item::Project::get_many( &collection_create_data.projects, @@ -140,10 +143,13 @@ pub async fn collections_get( let ids = serde_json::from_str::>(&ids.ids)?; let ids = ids .into_iter() - .map(|x| parse_base62(x).map(|x| database::models::CollectionId(x as i64))) + .map(|x| { + parse_base62(x).map(|x| database::models::CollectionId(x as i64)) + }) .collect::, _>>()?; - let collections_data = database::models::Collection::get_many(&ids, &**pool, &redis).await?; + let collections_data = + database::models::Collection::get_many(&ids, &**pool, &redis).await?; let user_option = get_user_from_headers( &req, @@ -156,7 +162,8 @@ pub async fn collections_get( .map(|x| x.1) .ok(); - let collections = filter_visible_collections(collections_data, &user_option).await?; + let collections = + filter_visible_collections(collections_data, &user_option).await?; Ok(HttpResponse::Ok().json(collections)) } @@ -171,7 +178,8 @@ pub async fn collection_get( let string = info.into_inner().0; let id = database::models::CollectionId(parse_base62(&string)? as i64); - let collection_data = database::models::Collection::get(id, &**pool, &redis).await?; + let collection_data = + database::models::Collection::get(id, &**pool, &redis).await?; let user_option = get_user_from_headers( &req, &**pool, @@ -228,9 +236,9 @@ pub async fn collection_edit( .await? .1; - new_collection - .validate() - .map_err(|err| ApiError::Validation(validation_errors_to_string(err, None)))?; + new_collection.validate().map_err(|err| { + ApiError::Validation(validation_errors_to_string(err, None)) + })?; let string = info.into_inner().0; let id = database::models::CollectionId(parse_base62(&string)? as i64); @@ -275,7 +283,8 @@ pub async fn collection_edit( if let Some(status) = &new_collection.status { if !(user.role.is_mod() - || collection_item.status.is_approved() && status.can_be_requested()) + || collection_item.status.is_approved() + && status.can_be_requested()) { return Err(ApiError::CustomAuthentication( "You don't have permission to set this status!".to_string(), @@ -313,13 +322,14 @@ pub async fn collection_edit( .collect_vec(); let mut validated_project_ids = Vec::new(); for project_id in new_project_ids { - let project = database::models::Project::get(project_id, &**pool, &redis) - .await? - .ok_or_else(|| { - ApiError::InvalidInput(format!( + let project = + database::models::Project::get(project_id, &**pool, &redis) + .await? + .ok_or_else(|| { + ApiError::InvalidInput(format!( "The specified project {project_id} does not exist!" )) - })?; + })?; validated_project_ids.push(project.inner.id.0); } // Insert- don't throw an error if it already exists @@ -348,7 +358,8 @@ pub async fn collection_edit( } transaction.commit().await?; - database::models::Collection::clear_cache(collection_item.id, &redis).await?; + database::models::Collection::clear_cache(collection_item.id, &redis) + .await?; Ok(HttpResponse::NoContent().body("")) } else { @@ -384,11 +395,14 @@ pub async fn collection_icon_edit( let string = info.into_inner().0; let id = database::models::CollectionId(parse_base62(&string)? as i64); - let collection_item = database::models::Collection::get(id, &**pool, &redis) - .await? - .ok_or_else(|| { - ApiError::InvalidInput("The specified collection does not exist!".to_string()) - })?; + let collection_item = + database::models::Collection::get(id, &**pool, &redis) + .await? + .ok_or_else(|| { + ApiError::InvalidInput( + "The specified collection does not exist!".to_string(), + ) + })?; if !can_modify_collection(&collection_item, &user) { return Ok(HttpResponse::Unauthorized().body("")); @@ -401,8 +415,12 @@ pub async fn collection_icon_edit( ) .await?; - let bytes = - read_from_payload(&mut payload, 262144, "Icons must be smaller than 256KiB").await?; + let bytes = read_from_payload( + &mut payload, + 262144, + "Icons must be smaller than 256KiB", + ) + .await?; let collection_id: CollectionId = collection_item.id.into(); let upload_result = crate::util::img::upload_image_optimized( @@ -432,7 +450,8 @@ pub async fn collection_icon_edit( .await?; transaction.commit().await?; - database::models::Collection::clear_cache(collection_item.id, &redis).await?; + database::models::Collection::clear_cache(collection_item.id, &redis) + .await?; Ok(HttpResponse::NoContent().body("")) } @@ -457,11 +476,14 @@ pub async fn delete_collection_icon( let string = info.into_inner().0; let id = database::models::CollectionId(parse_base62(&string)? as i64); - let collection_item = database::models::Collection::get(id, &**pool, &redis) - .await? - .ok_or_else(|| { - ApiError::InvalidInput("The specified collection does not exist!".to_string()) - })?; + let collection_item = + database::models::Collection::get(id, &**pool, &redis) + .await? + .ok_or_else(|| { + ApiError::InvalidInput( + "The specified collection does not exist!".to_string(), + ) + })?; if !can_modify_collection(&collection_item, &user) { return Ok(HttpResponse::Unauthorized().body("")); } @@ -486,7 +508,8 @@ pub async fn delete_collection_icon( .await?; transaction.commit().await?; - database::models::Collection::clear_cache(collection_item.id, &redis).await?; + database::models::Collection::clear_cache(collection_item.id, &redis) + .await?; Ok(HttpResponse::NoContent().body("")) } @@ -513,15 +536,21 @@ pub async fn collection_delete( let collection = database::models::Collection::get(id, &**pool, &redis) .await? .ok_or_else(|| { - ApiError::InvalidInput("The specified collection does not exist!".to_string()) + ApiError::InvalidInput( + "The specified collection does not exist!".to_string(), + ) })?; if !can_modify_collection(&collection, &user) { return Ok(HttpResponse::Unauthorized().body("")); } let mut transaction = pool.begin().await?; - let result = - database::models::Collection::remove(collection.id, &mut transaction, &redis).await?; + let result = database::models::Collection::remove( + collection.id, + &mut transaction, + &redis, + ) + .await?; transaction.commit().await?; database::models::Collection::clear_cache(collection.id, &redis).await?; diff --git a/apps/labrinth/src/routes/v3/images.rs b/apps/labrinth/src/routes/v3/images.rs index 0ec48acac..a1c2c841c 100644 --- a/apps/labrinth/src/routes/v3/images.rs +++ b/apps/labrinth/src/routes/v3/images.rs @@ -4,7 +4,9 @@ use super::threads::is_authorized_thread; use crate::auth::checks::{is_team_member_project, is_team_member_version}; use crate::auth::get_user_from_headers; use crate::database; -use crate::database::models::{project_item, report_item, thread_item, version_item}; +use crate::database::models::{ + project_item, report_item, thread_item, version_item, +}; use crate::database::redis::RedisPool; use crate::file_hosting::FileHost; use crate::models::ids::{ThreadMessageId, VersionId}; @@ -50,18 +52,31 @@ pub async fn images_add( let scopes = vec![context.relevant_scope()]; - let user = get_user_from_headers(&req, &**pool, &redis, &session_queue, Some(&scopes)) - .await? - .1; + let user = get_user_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Some(&scopes), + ) + .await? + .1; // Attempt to associated a supplied id with the context // If the context cannot be found, or the user is not authorized to upload images for the context, return an error match &mut context { ImageContext::Project { project_id } => { if let Some(id) = data.project_id { - let project = project_item::Project::get(&id, &**pool, &redis).await?; + let project = + project_item::Project::get(&id, &**pool, &redis).await?; if let Some(project) = project { - if is_team_member_project(&project.inner, &Some(user.clone()), &pool).await? { + if is_team_member_project( + &project.inner, + &Some(user.clone()), + &pool, + ) + .await? + { *project_id = Some(project.inner.id.into()); } else { return Err(ApiError::CustomAuthentication( @@ -77,10 +92,17 @@ pub async fn images_add( } ImageContext::Version { version_id } => { if let Some(id) = data.version_id { - let version = version_item::Version::get(id.into(), &**pool, &redis).await?; + let version = + version_item::Version::get(id.into(), &**pool, &redis) + .await?; if let Some(version) = version { - if is_team_member_version(&version.inner, &Some(user.clone()), &pool, &redis) - .await? + if is_team_member_version( + &version.inner, + &Some(user.clone()), + &pool, + &redis, + ) + .await? { *version_id = Some(version.inner.id.into()); } else { @@ -97,11 +119,15 @@ pub async fn images_add( } ImageContext::ThreadMessage { thread_message_id } => { if let Some(id) = data.thread_message_id { - let thread_message = thread_item::ThreadMessage::get(id.into(), &**pool) - .await? - .ok_or_else(|| { - ApiError::InvalidInput("The thread message could not found.".to_string()) - })?; + let thread_message = + thread_item::ThreadMessage::get(id.into(), &**pool) + .await? + .ok_or_else(|| { + ApiError::InvalidInput( + "The thread message could not found." + .to_string(), + ) + })?; let thread = thread_item::Thread::get(thread_message.thread_id, &**pool) .await? .ok_or_else(|| { @@ -125,7 +151,9 @@ pub async fn images_add( let report = report_item::Report::get(id.into(), &**pool) .await? .ok_or_else(|| { - ApiError::InvalidInput("The report could not be found.".to_string()) + ApiError::InvalidInput( + "The report could not be found.".to_string(), + ) })?; let thread = thread_item::Thread::get(report.thread_id, &**pool) .await? @@ -151,8 +179,12 @@ pub async fn images_add( } // Upload the image to the file host - let bytes = - read_from_payload(&mut payload, 1_048_576, "Icons must be smaller than 1MiB").await?; + let bytes = read_from_payload( + &mut payload, + 1_048_576, + "Icons must be smaller than 1MiB", + ) + .await?; let content_length = bytes.len(); let upload_result = upload_image_optimized( diff --git a/apps/labrinth/src/routes/v3/notifications.rs b/apps/labrinth/src/routes/v3/notifications.rs index d2445735f..abff2e587 100644 --- a/apps/labrinth/src/routes/v3/notifications.rs +++ b/apps/labrinth/src/routes/v3/notifications.rs @@ -55,8 +55,11 @@ pub async fn notifications_get( .collect(); let notifications_data: Vec = - database::models::notification_item::Notification::get_many(¬ification_ids, &**pool) - .await?; + database::models::notification_item::Notification::get_many( + ¬ification_ids, + &**pool, + ) + .await?; let notifications: Vec = notifications_data .into_iter() @@ -87,7 +90,11 @@ pub async fn notification_get( let id = info.into_inner().0; let notification_data = - database::models::notification_item::Notification::get(id.into(), &**pool).await?; + database::models::notification_item::Notification::get( + id.into(), + &**pool, + ) + .await?; if let Some(data) = notification_data { if user.id == data.user_id.into() || user.role.is_admin() { @@ -120,7 +127,11 @@ pub async fn notification_read( let id = info.into_inner().0; let notification_data = - database::models::notification_item::Notification::get(id.into(), &**pool).await?; + database::models::notification_item::Notification::get( + id.into(), + &**pool, + ) + .await?; if let Some(data) = notification_data { if data.user_id == user.id.into() || user.role.is_admin() { @@ -166,7 +177,11 @@ pub async fn notification_delete( let id = info.into_inner().0; let notification_data = - database::models::notification_item::Notification::get(id.into(), &**pool).await?; + database::models::notification_item::Notification::get( + id.into(), + &**pool, + ) + .await?; if let Some(data) = notification_data { if data.user_id == user.id.into() || user.role.is_admin() { @@ -184,7 +199,8 @@ pub async fn notification_delete( Ok(HttpResponse::NoContent().body("")) } else { Err(ApiError::CustomAuthentication( - "You are not authorized to delete this notification!".to_string(), + "You are not authorized to delete this notification!" + .to_string(), )) } } else { @@ -209,18 +225,23 @@ pub async fn notifications_read( .await? .1; - let notification_ids = serde_json::from_str::>(&ids.ids)? - .into_iter() - .map(|x| x.into()) - .collect::>(); + let notification_ids = + serde_json::from_str::>(&ids.ids)? + .into_iter() + .map(|x| x.into()) + .collect::>(); let mut transaction = pool.begin().await?; let notifications_data = - database::models::notification_item::Notification::get_many(¬ification_ids, &**pool) - .await?; + database::models::notification_item::Notification::get_many( + ¬ification_ids, + &**pool, + ) + .await?; - let mut notifications: Vec = Vec::new(); + let mut notifications: Vec = + Vec::new(); for notification in notifications_data { if notification.user_id == user.id.into() || user.role.is_admin() { @@ -257,18 +278,23 @@ pub async fn notifications_delete( .await? .1; - let notification_ids = serde_json::from_str::>(&ids.ids)? - .into_iter() - .map(|x| x.into()) - .collect::>(); + let notification_ids = + serde_json::from_str::>(&ids.ids)? + .into_iter() + .map(|x| x.into()) + .collect::>(); let mut transaction = pool.begin().await?; let notifications_data = - database::models::notification_item::Notification::get_many(¬ification_ids, &**pool) - .await?; - - let mut notifications: Vec = Vec::new(); + database::models::notification_item::Notification::get_many( + ¬ification_ids, + &**pool, + ) + .await?; + + let mut notifications: Vec = + Vec::new(); for notification in notifications_data { if notification.user_id == user.id.into() || user.role.is_admin() { diff --git a/apps/labrinth/src/routes/v3/oauth_clients.rs b/apps/labrinth/src/routes/v3/oauth_clients.rs index a7ea42bdc..a65dcc75d 100644 --- a/apps/labrinth/src/routes/v3/oauth_clients.rs +++ b/apps/labrinth/src/routes/v3/oauth_clients.rs @@ -36,7 +36,10 @@ use crate::{ }; use crate::{ file_hosting::FileHost, - models::{ids::base62_impl::parse_base62, oauth_clients::DeleteOAuthClientQueryParam}, + models::{ + ids::base62_impl::parse_base62, + oauth_clients::DeleteOAuthClientQueryParam, + }, util::routes::read_from_payload, }; @@ -80,13 +83,16 @@ pub async fn get_user_clients( let target_user = User::get(&info.into_inner(), &**pool, &redis).await?; if let Some(target_user) = target_user { - if target_user.id != current_user.id.into() && !current_user.role.is_admin() { + if target_user.id != current_user.id.into() + && !current_user.role.is_admin() + { return Err(ApiError::CustomAuthentication( "You do not have permission to see the OAuth clients of this user!".to_string(), )); } - let clients = OAuthClient::get_all_user_clients(target_user.id, &**pool).await?; + let clients = + OAuthClient::get_all_user_clients(target_user.id, &**pool).await?; let response = clients .into_iter() @@ -136,7 +142,9 @@ pub struct NewOAuthApp { )] pub name: String, - #[validate(custom(function = "crate::util::validate::validate_no_restricted_scopes"))] + #[validate(custom( + function = "crate::util::validate::validate_no_restricted_scopes" + ))] pub max_scopes: Scopes, pub redirect_uris: Vec, @@ -169,9 +177,9 @@ pub async fn oauth_client_create<'a>( .await? .1; - new_oauth_app - .validate() - .map_err(|e| CreateError::ValidationError(validation_errors_to_string(e, None)))?; + new_oauth_app.validate().map_err(|e| { + CreateError::ValidationError(validation_errors_to_string(e, None)) + })?; let mut transaction = pool.begin().await?; @@ -180,8 +188,12 @@ pub async fn oauth_client_create<'a>( let client_secret = generate_oauth_client_secret(); let client_secret_hash = DBOAuthClient::hash_secret(&client_secret); - let redirect_uris = - create_redirect_uris(&new_oauth_app.redirect_uris, client_id, &mut transaction).await?; + let redirect_uris = create_redirect_uris( + &new_oauth_app.redirect_uris, + client_id, + &mut transaction, + ) + .await?; let client = OAuthClient { id: client_id, @@ -226,7 +238,8 @@ pub async fn oauth_client_delete<'a>( .await? .1; - let client = OAuthClient::get(client_id.into_inner().into(), &**pool).await?; + let client = + OAuthClient::get(client_id.into_inner().into(), &**pool).await?; if let Some(client) = client { client.validate_authorized(Some(¤t_user))?; OAuthClient::remove(client.id, &**pool).await?; @@ -245,7 +258,9 @@ pub struct OAuthClientEdit { )] pub name: Option, - #[validate(custom(function = "crate::util::validate::validate_no_restricted_scopes"))] + #[validate(custom( + function = "crate::util::validate::validate_no_restricted_scopes" + ))] pub max_scopes: Option, #[validate(length(min = 1))] @@ -280,11 +295,13 @@ pub async fn oauth_client_edit( .await? .1; - client_updates - .validate() - .map_err(|e| ApiError::Validation(validation_errors_to_string(e, None)))?; + client_updates.validate().map_err(|e| { + ApiError::Validation(validation_errors_to_string(e, None)) + })?; - if let Some(existing_client) = OAuthClient::get(client_id.into_inner().into(), &**pool).await? { + if let Some(existing_client) = + OAuthClient::get(client_id.into_inner().into(), &**pool).await? + { existing_client.validate_authorized(Some(¤t_user))?; let mut updated_client = existing_client.clone(); @@ -317,7 +334,8 @@ pub async fn oauth_client_edit( .await?; if let Some(redirects) = redirect_uris { - edit_redirects(redirects, &existing_client, &mut transaction).await?; + edit_redirects(redirects, &existing_client, &mut transaction) + .await?; } transaction.commit().await?; @@ -358,7 +376,9 @@ pub async fn oauth_client_icon_edit( let client = OAuthClient::get((*client_id).into(), &**pool) .await? .ok_or_else(|| { - ApiError::InvalidInput("The specified client does not exist!".to_string()) + ApiError::InvalidInput( + "The specified client does not exist!".to_string(), + ) })?; client.validate_authorized(Some(&user))?; @@ -370,8 +390,12 @@ pub async fn oauth_client_icon_edit( ) .await?; - let bytes = - read_from_payload(&mut payload, 262144, "Icons must be smaller than 256KiB").await?; + let bytes = read_from_payload( + &mut payload, + 262144, + "Icons must be smaller than 256KiB", + ) + .await?; let upload_result = upload_image_optimized( &format!("data/{}", client_id), bytes.freeze(), @@ -419,7 +443,9 @@ pub async fn oauth_client_icon_delete( let client = OAuthClient::get((*client_id).into(), &**pool) .await? .ok_or_else(|| { - ApiError::InvalidInput("The specified client does not exist!".to_string()) + ApiError::InvalidInput( + "The specified client does not exist!".to_string(), + ) })?; client.validate_authorized(Some(&user))?; @@ -461,8 +487,11 @@ pub async fn get_user_oauth_authorizations( .await? .1; - let authorizations = - OAuthClientAuthorization::get_all_for_user(current_user.id.into(), &**pool).await?; + let authorizations = OAuthClientAuthorization::get_all_for_user( + current_user.id.into(), + &**pool, + ) + .await?; let mapped: Vec = authorizations.into_iter().map(|a| a.into()).collect_vec(); @@ -488,8 +517,12 @@ pub async fn revoke_oauth_authorization( .await? .1; - OAuthClientAuthorization::remove(info.client_id.into(), current_user.id.into(), &**pool) - .await?; + OAuthClientAuthorization::remove( + info.client_id.into(), + current_user.id.into(), + &**pool, + ) + .await?; Ok(HttpResponse::Ok().body("")) } @@ -538,12 +571,16 @@ async fn edit_redirects( &mut *transaction, ) .await?; - OAuthClient::insert_redirect_uris(&redirects_to_add, &mut **transaction).await?; + OAuthClient::insert_redirect_uris(&redirects_to_add, &mut **transaction) + .await?; let mut redirects_to_remove = existing_client.redirect_uris.clone(); redirects_to_remove.retain(|r| !updated_redirects.contains(&r.uri)); - OAuthClient::remove_redirect_uris(redirects_to_remove.iter().map(|r| r.id), &mut **transaction) - .await?; + OAuthClient::remove_redirect_uris( + redirects_to_remove.iter().map(|r| r.id), + &mut **transaction, + ) + .await?; Ok(()) } diff --git a/apps/labrinth/src/routes/v3/organizations.rs b/apps/labrinth/src/routes/v3/organizations.rs index f7b443111..0307341b3 100644 --- a/apps/labrinth/src/routes/v3/organizations.rs +++ b/apps/labrinth/src/routes/v3/organizations.rs @@ -4,7 +4,9 @@ use std::sync::Arc; use super::ApiError; use crate::auth::{filter_visible_projects, get_user_from_headers}; use crate::database::models::team_item::TeamMember; -use crate::database::models::{generate_organization_id, team_item, Organization}; +use crate::database::models::{ + generate_organization_id, team_item, Organization, +}; use crate::database::redis::RedisPool; use crate::file_hosting::FileHost; use crate::models::ids::base62_impl::parse_base62; @@ -83,10 +85,16 @@ pub async fn organization_projects_get( .try_collect::>() .await?; - let projects_data = - crate::database::models::Project::get_many_ids(&project_ids, &**pool, &redis).await?; + let projects_data = crate::database::models::Project::get_many_ids( + &project_ids, + &**pool, + &redis, + ) + .await?; - let projects = filter_visible_projects(projects_data, ¤t_user, &pool, true).await?; + let projects = + filter_visible_projects(projects_data, ¤t_user, &pool, true) + .await?; Ok(HttpResponse::Ok().json(projects)) } @@ -121,9 +129,9 @@ pub async fn organization_create( .await? .1; - new_organization - .validate() - .map_err(|err| CreateError::ValidationError(validation_errors_to_string(err, None)))?; + new_organization.validate().map_err(|err| { + CreateError::ValidationError(validation_errors_to_string(err, None)) + })?; let mut transaction = pool.begin().await?; @@ -135,7 +143,12 @@ pub async fn organization_create( organization_strings.push(name_organization_id.to_string()); } organization_strings.push(new_organization.slug.clone()); - let results = Organization::get_many(&organization_strings, &mut *transaction, &redis).await?; + let results = Organization::get_many( + &organization_strings, + &mut *transaction, + &redis, + ) + .await?; if !results.is_empty() { return Err(CreateError::SlugCollision); } @@ -188,7 +201,8 @@ pub async fn organization_create( )); }; - let organization = models::organizations::Organization::from(organization, members_data); + let organization = + models::organizations::Organization::from(organization, members_data); Ok(HttpResponse::Ok().json(organization)) } @@ -215,7 +229,9 @@ pub async fn organization_get( let organization_data = Organization::get(&id, &**pool, &redis).await?; if let Some(data) = organization_data { - let members_data = TeamMember::get_from_team_full(data.team_id, &**pool, &redis).await?; + let members_data = + TeamMember::get_from_team_full(data.team_id, &**pool, &redis) + .await?; let users = crate::database::models::User::get_many_ids( &members_data.iter().map(|x| x.user_id).collect::>(), @@ -237,17 +253,24 @@ pub async fn organization_get( logged_in || x.accepted || user_id - .map(|y: crate::database::models::UserId| y == x.user_id) + .map(|y: crate::database::models::UserId| { + y == x.user_id + }) .unwrap_or(false) }) .flat_map(|data| { users.iter().find(|x| x.id == data.user_id).map(|user| { - crate::models::teams::TeamMember::from(data, user.clone(), !logged_in) + crate::models::teams::TeamMember::from( + data, + user.clone(), + !logged_in, + ) }) }) .collect(); - let organization = models::organizations::Organization::from(data, team_members); + let organization = + models::organizations::Organization::from(data, team_members); return Ok(HttpResponse::Ok().json(organization)); } Err(ApiError::NotFound) @@ -266,13 +289,15 @@ pub async fn organizations_get( session_queue: web::Data, ) -> Result { let ids = serde_json::from_str::>(&ids.ids)?; - let organizations_data = Organization::get_many(&ids, &**pool, &redis).await?; + let organizations_data = + Organization::get_many(&ids, &**pool, &redis).await?; let team_ids = organizations_data .iter() .map(|x| x.team_id) .collect::>(); - let teams_data = TeamMember::get_from_team_full_many(&team_ids, &**pool, &redis).await?; + let teams_data = + TeamMember::get_from_team_full_many(&team_ids, &**pool, &redis).await?; let users = crate::database::models::User::get_many_ids( &teams_data.iter().map(|x| x.user_id).collect::>(), &**pool, @@ -316,17 +341,24 @@ pub async fn organizations_get( logged_in || x.accepted || user_id - .map(|y: crate::database::models::UserId| y == x.user_id) + .map(|y: crate::database::models::UserId| { + y == x.user_id + }) .unwrap_or(false) }) .flat_map(|data| { users.iter().find(|x| x.id == data.user_id).map(|user| { - crate::models::teams::TeamMember::from(data, user.clone(), !logged_in) + crate::models::teams::TeamMember::from( + data, + user.clone(), + !logged_in, + ) }) }) .collect(); - let organization = models::organizations::Organization::from(data, team_members); + let organization = + models::organizations::Organization::from(data, team_members); organizations.push(organization); } @@ -364,12 +396,13 @@ pub async fn organizations_edit( .await? .1; - new_organization - .validate() - .map_err(|err| ApiError::Validation(validation_errors_to_string(err, None)))?; + new_organization.validate().map_err(|err| { + ApiError::Validation(validation_errors_to_string(err, None)) + })?; let string = info.into_inner().0; - let result = database::models::Organization::get(&string, &**pool, &redis).await?; + let result = + database::models::Organization::get(&string, &**pool, &redis).await?; if let Some(organization_item) = result { let id = organization_item.id; @@ -380,8 +413,10 @@ pub async fn organizations_edit( ) .await?; - let permissions = - OrganizationPermissions::get_permissions_by_role(&user.role, &team_member); + let permissions = OrganizationPermissions::get_permissions_by_role( + &user.role, + &team_member, + ); if let Some(perms) = permissions { let mut transaction = pool.begin().await?; @@ -433,8 +468,10 @@ pub async fn organizations_edit( )); } - let name_organization_id_option: Option = parse_base62(slug).ok(); - if let Some(name_organization_id) = name_organization_id_option { + let name_organization_id_option: Option = + parse_base62(slug).ok(); + if let Some(name_organization_id) = name_organization_id_option + { let results = sqlx::query!( " SELECT EXISTS(SELECT 1 FROM organizations WHERE id=$1) @@ -446,7 +483,8 @@ pub async fn organizations_edit( if results.exists.unwrap_or(true) { return Err(ApiError::InvalidInput( - "slug collides with other organization's id!".to_string(), + "slug collides with other organization's id!" + .to_string(), )); } } @@ -465,7 +503,8 @@ pub async fn organizations_edit( if results.exists.unwrap_or(true) { return Err(ApiError::InvalidInput( - "slug collides with other organization's id!".to_string(), + "slug collides with other organization's id!" + .to_string(), )); } } @@ -494,7 +533,8 @@ pub async fn organizations_edit( Ok(HttpResponse::NoContent().body("")) } else { Err(ApiError::CustomAuthentication( - "You do not have permission to edit this organization!".to_string(), + "You do not have permission to edit this organization!" + .to_string(), )) } } else { @@ -520,32 +560,41 @@ pub async fn organization_delete( .1; let string = info.into_inner().0; - let organization = database::models::Organization::get(&string, &**pool, &redis) - .await? - .ok_or_else(|| { - ApiError::InvalidInput("The specified organization does not exist!".to_string()) - })?; + let organization = + database::models::Organization::get(&string, &**pool, &redis) + .await? + .ok_or_else(|| { + ApiError::InvalidInput( + "The specified organization does not exist!".to_string(), + ) + })?; if !user.role.is_admin() { - let team_member = database::models::TeamMember::get_from_user_id_organization( - organization.id, - user.id.into(), - false, - &**pool, - ) - .await - .map_err(ApiError::Database)? - .ok_or_else(|| { - ApiError::InvalidInput("The specified organization does not exist!".to_string()) - })?; + let team_member = + database::models::TeamMember::get_from_user_id_organization( + organization.id, + user.id.into(), + false, + &**pool, + ) + .await + .map_err(ApiError::Database)? + .ok_or_else(|| { + ApiError::InvalidInput( + "The specified organization does not exist!".to_string(), + ) + })?; - let permissions = - OrganizationPermissions::get_permissions_by_role(&user.role, &Some(team_member)) - .unwrap_or_default(); + let permissions = OrganizationPermissions::get_permissions_by_role( + &user.role, + &Some(team_member), + ) + .unwrap_or_default(); if !permissions.contains(OrganizationPermissions::DELETE_ORGANIZATION) { return Err(ApiError::CustomAuthentication( - "You don't have permission to delete this organization!".to_string(), + "You don't have permission to delete this organization!" + .to_string(), )); } } @@ -582,8 +631,10 @@ pub async fn organization_delete( .await?; for organization_project_team in organization_project_teams.iter() { - let new_id = - crate::database::models::ids::generate_team_member_id(&mut transaction).await?; + let new_id = crate::database::models::ids::generate_team_member_id( + &mut transaction, + ) + .await?; let member = TeamMember { id: new_id, team_id: *organization_project_team, @@ -599,13 +650,21 @@ pub async fn organization_delete( member.insert(&mut transaction).await?; } // Safely remove the organization - let result = - database::models::Organization::remove(organization.id, &mut transaction, &redis).await?; + let result = database::models::Organization::remove( + organization.id, + &mut transaction, + &redis, + ) + .await?; transaction.commit().await?; - database::models::Organization::clear_cache(organization.id, Some(organization.slug), &redis) - .await?; + database::models::Organization::clear_cache( + organization.id, + Some(organization.slug), + &redis, + ) + .await?; for team_id in organization_project_teams { database::models::TeamMember::clear_cache(team_id, &redis).await?; @@ -641,41 +700,59 @@ pub async fn organization_projects_add( .await? .1; - let organization = database::models::Organization::get(&info, &**pool, &redis) - .await? - .ok_or_else(|| { - ApiError::InvalidInput("The specified organization does not exist!".to_string()) - })?; - - let project_item = database::models::Project::get(&project_info.project_id, &**pool, &redis) - .await? - .ok_or_else(|| { - ApiError::InvalidInput("The specified project does not exist!".to_string()) - })?; - if project_item.inner.organization_id.is_some() { - return Err(ApiError::InvalidInput( - "The specified project is already owned by an organization!".to_string(), - )); - } + let organization = + database::models::Organization::get(&info, &**pool, &redis) + .await? + .ok_or_else(|| { + ApiError::InvalidInput( + "The specified organization does not exist!".to_string(), + ) + })?; - let project_team_member = database::models::TeamMember::get_from_user_id_project( - project_item.inner.id, - current_user.id.into(), - false, - &**pool, - ) - .await? - .ok_or_else(|| ApiError::InvalidInput("You are not a member of this project!".to_string()))?; - let organization_team_member = database::models::TeamMember::get_from_user_id_organization( - organization.id, - current_user.id.into(), - false, + let project_item = database::models::Project::get( + &project_info.project_id, &**pool, + &redis, ) .await? .ok_or_else(|| { - ApiError::InvalidInput("You are not a member of this organization!".to_string()) + ApiError::InvalidInput( + "The specified project does not exist!".to_string(), + ) })?; + if project_item.inner.organization_id.is_some() { + return Err(ApiError::InvalidInput( + "The specified project is already owned by an organization!" + .to_string(), + )); + } + + let project_team_member = + database::models::TeamMember::get_from_user_id_project( + project_item.inner.id, + current_user.id.into(), + false, + &**pool, + ) + .await? + .ok_or_else(|| { + ApiError::InvalidInput( + "You are not a member of this project!".to_string(), + ) + })?; + let organization_team_member = + database::models::TeamMember::get_from_user_id_organization( + organization.id, + current_user.id.into(), + false, + &**pool, + ) + .await? + .ok_or_else(|| { + ApiError::InvalidInput( + "You are not a member of this organization!".to_string(), + ) + })?; // Require ownership of a project to add it to an organization if !current_user.role.is_admin() && !project_team_member.is_owner { @@ -734,8 +811,16 @@ pub async fn organization_projects_add( transaction.commit().await?; - database::models::User::clear_project_cache(&[current_user.id.into()], &redis).await?; - database::models::TeamMember::clear_cache(project_item.inner.team_id, &redis).await?; + database::models::User::clear_project_cache( + &[current_user.id.into()], + &redis, + ) + .await?; + database::models::TeamMember::clear_cache( + project_item.inner.team_id, + &redis, + ) + .await?; database::models::Project::clear_cache( project_item.inner.id, project_item.inner.slug, @@ -745,7 +830,8 @@ pub async fn organization_projects_add( .await?; } else { return Err(ApiError::CustomAuthentication( - "You do not have permission to add projects to this organization!".to_string(), + "You do not have permission to add projects to this organization!" + .to_string(), )); } Ok(HttpResponse::Ok().finish()) @@ -777,17 +863,23 @@ pub async fn organization_projects_remove( .await? .1; - let organization = database::models::Organization::get(&organization_id, &**pool, &redis) - .await? - .ok_or_else(|| { - ApiError::InvalidInput("The specified organization does not exist!".to_string()) - })?; - - let project_item = database::models::Project::get(&project_id, &**pool, &redis) - .await? - .ok_or_else(|| { - ApiError::InvalidInput("The specified project does not exist!".to_string()) - })?; + let organization = + database::models::Organization::get(&organization_id, &**pool, &redis) + .await? + .ok_or_else(|| { + ApiError::InvalidInput( + "The specified organization does not exist!".to_string(), + ) + })?; + + let project_item = + database::models::Project::get(&project_id, &**pool, &redis) + .await? + .ok_or_else(|| { + ApiError::InvalidInput( + "The specified project does not exist!".to_string(), + ) + })?; if !project_item .inner @@ -795,20 +887,24 @@ pub async fn organization_projects_remove( .eq(&Some(organization.id)) { return Err(ApiError::InvalidInput( - "The specified project is not owned by this organization!".to_string(), + "The specified project is not owned by this organization!" + .to_string(), )); } - let organization_team_member = database::models::TeamMember::get_from_user_id_organization( - organization.id, - current_user.id.into(), - false, - &**pool, - ) - .await? - .ok_or_else(|| { - ApiError::InvalidInput("You are not a member of this organization!".to_string()) - })?; + let organization_team_member = + database::models::TeamMember::get_from_user_id_organization( + organization.id, + current_user.id.into(), + false, + &**pool, + ) + .await? + .ok_or_else(|| { + ApiError::InvalidInput( + "You are not a member of this organization!".to_string(), + ) + })?; let permissions = OrganizationPermissions::get_permissions_by_role( ¤t_user.role, @@ -826,7 +922,8 @@ pub async fn organization_projects_remove( .await? .ok_or_else(|| { ApiError::InvalidInput( - "The specified user is not a member of this organization!".to_string(), + "The specified user is not a member of this organization!" + .to_string(), ) })?; @@ -847,7 +944,10 @@ pub async fn organization_projects_remove( Some(new_owner) => new_owner, None => { let new_id = - crate::database::models::ids::generate_team_member_id(&mut transaction).await?; + crate::database::models::ids::generate_team_member_id( + &mut transaction, + ) + .await?; let member = TeamMember { id: new_id, team_id: project_item.inner.team_id, @@ -895,8 +995,16 @@ pub async fn organization_projects_remove( .await?; transaction.commit().await?; - database::models::User::clear_project_cache(&[current_user.id.into()], &redis).await?; - database::models::TeamMember::clear_cache(project_item.inner.team_id, &redis).await?; + database::models::User::clear_project_cache( + &[current_user.id.into()], + &redis, + ) + .await?; + database::models::TeamMember::clear_cache( + project_item.inner.team_id, + &redis, + ) + .await?; database::models::Project::clear_cache( project_item.inner.id, project_item.inner.slug, @@ -906,7 +1014,8 @@ pub async fn organization_projects_remove( .await?; } else { return Err(ApiError::CustomAuthentication( - "You do not have permission to add projects to this organization!".to_string(), + "You do not have permission to add projects to this organization!" + .to_string(), )); } Ok(HttpResponse::Ok().finish()) @@ -939,11 +1048,14 @@ pub async fn organization_icon_edit( .1; let string = info.into_inner().0; - let organization_item = database::models::Organization::get(&string, &**pool, &redis) - .await? - .ok_or_else(|| { - ApiError::InvalidInput("The specified organization does not exist!".to_string()) - })?; + let organization_item = + database::models::Organization::get(&string, &**pool, &redis) + .await? + .ok_or_else(|| { + ApiError::InvalidInput( + "The specified organization does not exist!".to_string(), + ) + })?; if !user.role.is_mod() { let team_member = database::models::TeamMember::get_from_user_id( @@ -954,13 +1066,16 @@ pub async fn organization_icon_edit( .await .map_err(ApiError::Database)?; - let permissions = - OrganizationPermissions::get_permissions_by_role(&user.role, &team_member) - .unwrap_or_default(); + let permissions = OrganizationPermissions::get_permissions_by_role( + &user.role, + &team_member, + ) + .unwrap_or_default(); if !permissions.contains(OrganizationPermissions::EDIT_DETAILS) { return Err(ApiError::CustomAuthentication( - "You don't have permission to edit this organization's icon.".to_string(), + "You don't have permission to edit this organization's icon." + .to_string(), )); } } @@ -972,8 +1087,12 @@ pub async fn organization_icon_edit( ) .await?; - let bytes = - read_from_payload(&mut payload, 262144, "Icons must be smaller than 256KiB").await?; + let bytes = read_from_payload( + &mut payload, + 262144, + "Icons must be smaller than 256KiB", + ) + .await?; let organization_id: OrganizationId = organization_item.id.into(); let upload_result = crate::util::img::upload_image_optimized( @@ -1032,11 +1151,14 @@ pub async fn delete_organization_icon( .1; let string = info.into_inner().0; - let organization_item = database::models::Organization::get(&string, &**pool, &redis) - .await? - .ok_or_else(|| { - ApiError::InvalidInput("The specified organization does not exist!".to_string()) - })?; + let organization_item = + database::models::Organization::get(&string, &**pool, &redis) + .await? + .ok_or_else(|| { + ApiError::InvalidInput( + "The specified organization does not exist!".to_string(), + ) + })?; if !user.role.is_mod() { let team_member = database::models::TeamMember::get_from_user_id( @@ -1047,13 +1169,16 @@ pub async fn delete_organization_icon( .await .map_err(ApiError::Database)?; - let permissions = - OrganizationPermissions::get_permissions_by_role(&user.role, &team_member) - .unwrap_or_default(); + let permissions = OrganizationPermissions::get_permissions_by_role( + &user.role, + &team_member, + ) + .unwrap_or_default(); if !permissions.contains(OrganizationPermissions::EDIT_DETAILS) { return Err(ApiError::CustomAuthentication( - "You don't have permission to edit this organization's icon.".to_string(), + "You don't have permission to edit this organization's icon." + .to_string(), )); } } diff --git a/apps/labrinth/src/routes/v3/payouts.rs b/apps/labrinth/src/routes/v3/payouts.rs index f97844d22..ad5636de1 100644 --- a/apps/labrinth/src/routes/v3/payouts.rs +++ b/apps/labrinth/src/routes/v3/payouts.rs @@ -46,27 +46,37 @@ pub async fn paypal_webhook( .headers() .get("PAYPAL-AUTH-ALGO") .and_then(|x| x.to_str().ok()) - .ok_or_else(|| ApiError::InvalidInput("missing auth algo".to_string()))?; + .ok_or_else(|| { + ApiError::InvalidInput("missing auth algo".to_string()) + })?; let cert_url = req .headers() .get("PAYPAL-CERT-URL") .and_then(|x| x.to_str().ok()) - .ok_or_else(|| ApiError::InvalidInput("missing cert url".to_string()))?; + .ok_or_else(|| { + ApiError::InvalidInput("missing cert url".to_string()) + })?; let transmission_id = req .headers() .get("PAYPAL-TRANSMISSION-ID") .and_then(|x| x.to_str().ok()) - .ok_or_else(|| ApiError::InvalidInput("missing transmission ID".to_string()))?; + .ok_or_else(|| { + ApiError::InvalidInput("missing transmission ID".to_string()) + })?; let transmission_sig = req .headers() .get("PAYPAL-TRANSMISSION-SIG") .and_then(|x| x.to_str().ok()) - .ok_or_else(|| ApiError::InvalidInput("missing transmission sig".to_string()))?; + .ok_or_else(|| { + ApiError::InvalidInput("missing transmission sig".to_string()) + })?; let transmission_time = req .headers() .get("PAYPAL-TRANSMISSION-TIME") .and_then(|x| x.to_str().ok()) - .ok_or_else(|| ApiError::InvalidInput("missing transmission time".to_string()))?; + .ok_or_else(|| { + ApiError::InvalidInput("missing transmission time".to_string()) + })?; #[derive(Deserialize)] struct WebHookResponse { @@ -190,11 +200,14 @@ pub async fn tremendous_webhook( .get("Tremendous-Webhook-Signature") .and_then(|x| x.to_str().ok()) .and_then(|x| x.split('=').next_back()) - .ok_or_else(|| ApiError::InvalidInput("missing webhook signature".to_string()))?; + .ok_or_else(|| { + ApiError::InvalidInput("missing webhook signature".to_string()) + })?; - let mut mac: Hmac = - Hmac::new_from_slice(dotenvy::var("TREMENDOUS_PRIVATE_KEY")?.as_bytes()) - .map_err(|_| ApiError::Payments("error initializing HMAC".to_string()))?; + let mut mac: Hmac = Hmac::new_from_slice( + dotenvy::var("TREMENDOUS_PRIVATE_KEY")?.as_bytes(), + ) + .map_err(|_| ApiError::Payments("error initializing HMAC".to_string()))?; mac.update(body.as_bytes()); let request_signature = mac.finalize().into_bytes().encode_hex::(); @@ -300,10 +313,16 @@ pub async fn user_payouts( .1; let payout_ids = - crate::database::models::payout_item::Payout::get_all_for_user(user.id.into(), &**pool) - .await?; - let payouts = - crate::database::models::payout_item::Payout::get_many(&payout_ids, &**pool).await?; + crate::database::models::payout_item::Payout::get_all_for_user( + user.id.into(), + &**pool, + ) + .await?; + let payouts = crate::database::models::payout_item::Payout::get_many( + &payout_ids, + &**pool, + ) + .await?; Ok(HttpResponse::Ok().json( payouts @@ -330,10 +349,17 @@ pub async fn create_payout( session_queue: web::Data, payouts_queue: web::Data, ) -> Result { - let (scopes, user) = - get_user_record_from_bearer_token(&req, None, &**pool, &redis, &session_queue) - .await? - .ok_or_else(|| ApiError::Authentication(AuthenticationError::InvalidCredentials))?; + let (scopes, user) = get_user_record_from_bearer_token( + &req, + None, + &**pool, + &redis, + &session_queue, + ) + .await? + .ok_or_else(|| { + ApiError::Authentication(AuthenticationError::InvalidCredentials) + })?; if !scopes.contains(Scopes::PAYOUTS_WRITE) { return Err(ApiError::Authentication( @@ -364,7 +390,11 @@ pub async fn create_payout( .await? .into_iter() .find(|x| x.id == body.method_id) - .ok_or_else(|| ApiError::InvalidInput("Invalid payment method specified!".to_string()))?; + .ok_or_else(|| { + ApiError::InvalidInput( + "Invalid payment method specified!".to_string(), + ) + })?; let fee = std::cmp::min( std::cmp::max( @@ -385,43 +415,50 @@ pub async fn create_payout( let payout_item = match body.method { PayoutMethodType::Venmo | PayoutMethodType::PayPal => { - let (wallet, wallet_type, address, display_address) = - if body.method == PayoutMethodType::Venmo { - if let Some(venmo) = user.venmo_handle { - ("Venmo", "user_handle", venmo.clone(), venmo) - } else { + let (wallet, wallet_type, address, display_address) = if body.method + == PayoutMethodType::Venmo + { + if let Some(venmo) = user.venmo_handle { + ("Venmo", "user_handle", venmo.clone(), venmo) + } else { + return Err(ApiError::InvalidInput( + "Venmo address has not been set for account!" + .to_string(), + )); + } + } else if let Some(paypal_id) = user.paypal_id { + if let Some(paypal_country) = user.paypal_country { + if &*paypal_country == "US" + && &*body.method_id != "paypal_us" + { return Err(ApiError::InvalidInput( - "Venmo address has not been set for account!".to_string(), + "Please use the US PayPal transfer option!" + .to_string(), )); - } - } else if let Some(paypal_id) = user.paypal_id { - if let Some(paypal_country) = user.paypal_country { - if &*paypal_country == "US" && &*body.method_id != "paypal_us" { - return Err(ApiError::InvalidInput( - "Please use the US PayPal transfer option!".to_string(), - )); - } else if &*paypal_country != "US" && &*body.method_id == "paypal_us" { - return Err(ApiError::InvalidInput( + } else if &*paypal_country != "US" + && &*body.method_id == "paypal_us" + { + return Err(ApiError::InvalidInput( "Please use the International PayPal transfer option!".to_string(), )); - } - - ( - "PayPal", - "paypal_id", - paypal_id.clone(), - user.paypal_email.unwrap_or(paypal_id), - ) - } else { - return Err(ApiError::InvalidInput( - "Please re-link your PayPal account!".to_string(), - )); } + + ( + "PayPal", + "paypal_id", + paypal_id.clone(), + user.paypal_email.unwrap_or(paypal_id), + ) } else { return Err(ApiError::InvalidInput( - "You have not linked a PayPal account!".to_string(), + "Please re-link your PayPal account!".to_string(), )); - }; + } + } else { + return Err(ApiError::InvalidInput( + "You have not linked a PayPal account!".to_string(), + )); + }; #[derive(Deserialize)] struct PayPalLink { @@ -433,17 +470,18 @@ pub async fn create_payout( pub links: Vec, } - let mut payout_item = crate::database::models::payout_item::Payout { - id: payout_id, - user_id: user.id, - created: Utc::now(), - status: PayoutStatus::InTransit, - amount: transfer, - fee: Some(fee), - method: Some(body.method), - method_address: Some(display_address), - platform_id: None, - }; + let mut payout_item = + crate::database::models::payout_item::Payout { + id: payout_id, + user_id: user.id, + created: Utc::now(), + status: PayoutStatus::InTransit, + amount: transfer, + fee: Some(fee), + method: Some(body.method), + method_address: Some(display_address), + platform_id: None, + }; let res: PayoutsResponse = payouts_queue.make_paypal_request( Method::POST, @@ -494,7 +532,8 @@ pub async fn create_payout( .await { if let Some(data) = res.items.first() { - payout_item.platform_id = Some(data.payout_item_id.clone()); + payout_item.platform_id = + Some(data.payout_item_id.clone()); } } } @@ -504,17 +543,18 @@ pub async fn create_payout( PayoutMethodType::Tremendous => { if let Some(email) = user.email { if user.email_verified { - let mut payout_item = crate::database::models::payout_item::Payout { - id: payout_id, - user_id: user.id, - created: Utc::now(), - status: PayoutStatus::InTransit, - amount: transfer, - fee: Some(fee), - method: Some(PayoutMethodType::Tremendous), - method_address: Some(email.clone()), - platform_id: None, - }; + let mut payout_item = + crate::database::models::payout_item::Payout { + id: payout_id, + user_id: user.id, + created: Utc::now(), + status: PayoutStatus::InTransit, + amount: transfer, + fee: Some(fee), + method: Some(PayoutMethodType::Tremendous), + method_address: Some(email.clone()), + platform_id: None, + }; #[derive(Deserialize)] struct Reward { @@ -566,12 +606,14 @@ pub async fn create_payout( payout_item } else { return Err(ApiError::InvalidInput( - "You must verify your account email to proceed!".to_string(), + "You must verify your account email to proceed!" + .to_string(), )); } } else { return Err(ApiError::InvalidInput( - "You must add an email to your account to proceed!".to_string(), + "You must add an email to your account to proceed!" + .to_string(), )); } } @@ -585,7 +627,8 @@ pub async fn create_payout( payout_item.insert(&mut transaction).await?; transaction.commit().await?; - crate::database::models::User::clear_caches(&[(user.id, None)], &redis).await?; + crate::database::models::User::clear_caches(&[(user.id, None)], &redis) + .await?; Ok(HttpResponse::NoContent().finish()) } @@ -610,7 +653,9 @@ pub async fn cancel_payout( .1; let id = info.into_inner().0; - let payout = crate::database::models::payout_item::Payout::get(id.into(), &**pool).await?; + let payout = + crate::database::models::payout_item::Payout::get(id.into(), &**pool) + .await?; if let Some(payout) = payout { if payout.user_id != user.id.into() && !user.role.is_admin() { @@ -630,7 +675,10 @@ pub async fn cancel_payout( payouts .make_paypal_request::<(), ()>( Method::POST, - &format!("payments/payouts-item/{}/cancel", platform_id), + &format!( + "payments/payouts-item/{}/cancel", + platform_id + ), None, None, None, @@ -792,7 +840,9 @@ async fn get_user_balance( .unwrap_or((Decimal::ZERO, Decimal::ZERO)); Ok(UserBalance { - available: available.round_dp(16) - withdrawn.round_dp(16) - fees.round_dp(16), + available: available.round_dp(16) + - withdrawn.round_dp(16) + - fees.round_dp(16), pending, }) } @@ -837,14 +887,19 @@ pub async fn platform_revenue( .and_then(|x| x.sum) .unwrap_or(Decimal::ZERO); - let points = - make_aditude_request(&["METRIC_REVENUE", "METRIC_IMPRESSIONS"], "30d", "1d").await?; + let points = make_aditude_request( + &["METRIC_REVENUE", "METRIC_IMPRESSIONS"], + "30d", + "1d", + ) + .await?; let mut points_map = HashMap::new(); for point in points { for point in point.points_list { - let entry = points_map.entry(point.time.seconds).or_insert((None, None)); + let entry = + points_map.entry(point.time.seconds).or_insert((None, None)); if let Some(revenue) = point.metric.revenue { entry.0 = Some(revenue); @@ -868,7 +923,8 @@ pub async fn platform_revenue( .and_utc() .timestamp(); - if let Some((revenue, impressions)) = points_map.remove(&(start as u64)) { + if let Some((revenue, impressions)) = points_map.remove(&(start as u64)) + { // Before 9/5/24, when legacy payouts were in effect. if start >= 1725494400 { let revenue = revenue.unwrap_or(Decimal::ZERO); @@ -879,8 +935,9 @@ pub async fn platform_revenue( // Clean.io fee (ad antimalware). Per 1000 impressions. let clean_io_fee = Decimal::from(8) / Decimal::from(1000); - let net_revenue = - revenue - (clean_io_fee * Decimal::from(impressions) / Decimal::from(1000)); + let net_revenue = revenue + - (clean_io_fee * Decimal::from(impressions) + / Decimal::from(1000)); let payout = net_revenue * (Decimal::from(1) - modrinth_cut); @@ -903,7 +960,12 @@ pub async fn platform_revenue( }; redis - .set_serialized_to_json(PLATFORM_REVENUE_NAMESPACE, 0, &res, Some(60 * 60)) + .set_serialized_to_json( + PLATFORM_REVENUE_NAMESPACE, + 0, + &res, + Some(60 * 60), + ) .await?; Ok(HttpResponse::Ok().json(res)) @@ -918,7 +980,8 @@ fn get_legacy_data_point(timestamp: u64) -> RevenueData { let weekdays = Decimal::from(20); let weekend_bonus = Decimal::from(5) / Decimal::from(4); - let weekday_amount = old_payouts_budget / (weekdays + (weekend_bonus) * (days - weekdays)); + let weekday_amount = + old_payouts_budget / (weekdays + (weekend_bonus) * (days - weekdays)); let weekend_amount = weekday_amount * weekend_bonus; let payout = match start.weekday() { diff --git a/apps/labrinth/src/routes/v3/project_creation.rs b/apps/labrinth/src/routes/v3/project_creation.rs index 46a916bfa..31d751205 100644 --- a/apps/labrinth/src/routes/v3/project_creation.rs +++ b/apps/labrinth/src/routes/v3/project_creation.rs @@ -1,6 +1,8 @@ use super::version_creation::{try_create_version_fields, InitialVersionData}; use crate::auth::{get_user_from_headers, AuthenticationError}; -use crate::database::models::loader_fields::{Loader, LoaderField, LoaderFieldEnumValue}; +use crate::database::models::loader_fields::{ + Loader, LoaderField, LoaderFieldEnumValue, +}; use crate::database::models::thread_item::ThreadBuilder; use crate::database::models::{self, image_item, User}; use crate::database::redis::RedisPool; @@ -11,7 +13,8 @@ use crate::models::ids::{ImageId, OrganizationId}; use crate::models::images::{Image, ImageContext}; use crate::models::pats::Scopes; use crate::models::projects::{ - License, Link, MonetizationStatus, ProjectId, ProjectStatus, VersionId, VersionStatus, + License, Link, MonetizationStatus, ProjectId, ProjectStatus, VersionId, + VersionStatus, }; use crate::models::teams::{OrganizationPermissions, ProjectPermissions}; use crate::models::threads::ThreadType; @@ -91,10 +94,14 @@ impl actix_web::ResponseError for CreateError { fn status_code(&self) -> StatusCode { match self { CreateError::EnvError(..) => StatusCode::INTERNAL_SERVER_ERROR, - CreateError::SqlxDatabaseError(..) => StatusCode::INTERNAL_SERVER_ERROR, + CreateError::SqlxDatabaseError(..) => { + StatusCode::INTERNAL_SERVER_ERROR + } CreateError::DatabaseError(..) => StatusCode::INTERNAL_SERVER_ERROR, CreateError::IndexingError(..) => StatusCode::INTERNAL_SERVER_ERROR, - CreateError::FileHostingError(..) => StatusCode::INTERNAL_SERVER_ERROR, + CreateError::FileHostingError(..) => { + StatusCode::INTERNAL_SERVER_ERROR + } CreateError::SerDeError(..) => StatusCode::BAD_REQUEST, CreateError::MultipartError(..) => StatusCode::BAD_REQUEST, CreateError::MissingValueError(..) => StatusCode::BAD_REQUEST, @@ -105,7 +112,9 @@ impl actix_web::ResponseError for CreateError { CreateError::InvalidCategory(..) => StatusCode::BAD_REQUEST, CreateError::InvalidFileType(..) => StatusCode::BAD_REQUEST, CreateError::Unauthorized(..) => StatusCode::UNAUTHORIZED, - CreateError::CustomAuthenticationError(..) => StatusCode::UNAUTHORIZED, + CreateError::CustomAuthenticationError(..) => { + StatusCode::UNAUTHORIZED + } CreateError::SlugCollision => StatusCode::BAD_REQUEST, CreateError::ValidationError(..) => StatusCode::BAD_REQUEST, CreateError::FileValidationError(..) => StatusCode::BAD_REQUEST, @@ -192,7 +201,9 @@ pub struct ProjectCreateData { /// An optional link to the project's license page pub license_url: Option, /// An optional list of all donation links the project has - #[validate(custom(function = "crate::util::validate::validate_url_hashmap_values"))] + #[validate(custom( + function = "crate::util::validate::validate_url_hashmap_values" + ))] #[serde(default)] pub link_urls: HashMap, @@ -343,8 +354,10 @@ async fn project_create_inner( .await? .1; - let project_id: ProjectId = models::generate_project_id(transaction).await?.into(); - let all_loaders = models::loader_fields::Loader::list(&mut **transaction, redis).await?; + let project_id: ProjectId = + models::generate_project_id(transaction).await?.into(); + let all_loaders = + models::loader_fields::Loader::list(&mut **transaction, redis).await?; let project_create_data: ProjectCreateData; let mut versions; @@ -365,9 +378,9 @@ async fn project_create_inner( })?; let content_disposition = field.content_disposition(); - let name = content_disposition - .get_name() - .ok_or_else(|| CreateError::MissingValueError(String::from("Missing content name")))?; + let name = content_disposition.get_name().ok_or_else(|| { + CreateError::MissingValueError(String::from("Missing content name")) + })?; if name != "data" { return Err(CreateError::InvalidInput(String::from( @@ -377,19 +390,22 @@ async fn project_create_inner( let mut data = Vec::new(); while let Some(chunk) = field.next().await { - data.extend_from_slice(&chunk.map_err(CreateError::MultipartError)?); + data.extend_from_slice( + &chunk.map_err(CreateError::MultipartError)?, + ); } let create_data: ProjectCreateData = serde_json::from_slice(&data)?; - create_data - .validate() - .map_err(|err| CreateError::InvalidInput(validation_errors_to_string(err, None)))?; + create_data.validate().map_err(|err| { + CreateError::InvalidInput(validation_errors_to_string(err, None)) + })?; let slug_project_id_option: Option = serde_json::from_str(&format!("\"{}\"", create_data.slug)).ok(); if let Some(slug_project_id) = slug_project_id_option { - let slug_project_id: models::ids::ProjectId = slug_project_id.into(); + let slug_project_id: models::ids::ProjectId = + slug_project_id.into(); let results = sqlx::query!( " SELECT EXISTS(SELECT 1 FROM mods WHERE id=$1) @@ -602,9 +618,14 @@ async fn project_create_inner( } // Convert the list of category names to actual categories - let mut categories = Vec::with_capacity(project_create_data.categories.len()); + let mut categories = + Vec::with_capacity(project_create_data.categories.len()); for category in &project_create_data.categories { - let ids = models::categories::Category::get_ids(category, &mut **transaction).await?; + let ids = models::categories::Category::get_ids( + category, + &mut **transaction, + ) + .await?; if ids.is_empty() { return Err(CreateError::InvalidCategory(category.clone())); } @@ -617,7 +638,11 @@ async fn project_create_inner( let mut additional_categories = Vec::with_capacity(project_create_data.additional_categories.len()); for category in &project_create_data.additional_categories { - let ids = models::categories::Category::get_ids(category, &mut **transaction).await?; + let ids = models::categories::Category::get_ids( + category, + &mut **transaction, + ) + .await?; if ids.is_empty() { return Err(CreateError::InvalidCategory(category.clone())); } @@ -629,18 +654,29 @@ async fn project_create_inner( let mut members = vec![]; if let Some(organization_id) = project_create_data.organization_id { - let org = models::Organization::get_id(organization_id.into(), pool, redis) - .await? - .ok_or_else(|| { - CreateError::InvalidInput("Invalid organization ID specified!".to_string()) - })?; + let org = models::Organization::get_id( + organization_id.into(), + pool, + redis, + ) + .await? + .ok_or_else(|| { + CreateError::InvalidInput( + "Invalid organization ID specified!".to_string(), + ) + })?; - let team_member = - models::TeamMember::get_from_user_id(org.team_id, current_user.id.into(), pool) - .await?; + let team_member = models::TeamMember::get_from_user_id( + org.team_id, + current_user.id.into(), + pool, + ) + .await?; - let perms = - OrganizationPermissions::get_permissions_by_role(¤t_user.role, &team_member); + let perms = OrganizationPermissions::get_permissions_by_role( + ¤t_user.role, + &team_member, + ); if !perms .map(|x| x.contains(OrganizationPermissions::ADD_PROJECT)) @@ -679,25 +715,32 @@ async fn project_create_inner( } } - let license_id = - spdx::Expression::parse(&project_create_data.license_id).map_err(|err| { - CreateError::InvalidInput(format!("Invalid SPDX license identifier: {err}")) - })?; + let license_id = spdx::Expression::parse( + &project_create_data.license_id, + ) + .map_err(|err| { + CreateError::InvalidInput(format!( + "Invalid SPDX license identifier: {err}" + )) + })?; let mut link_urls = vec![]; let link_platforms = - models::categories::LinkPlatform::list(&mut **transaction, redis).await?; + models::categories::LinkPlatform::list(&mut **transaction, redis) + .await?; for (platform, url) in &project_create_data.link_urls { - let platform_id = - models::categories::LinkPlatform::get_id(platform, &mut **transaction) - .await? - .ok_or_else(|| { - CreateError::InvalidInput(format!( - "Link platform {} does not exist.", - platform.clone() - )) - })?; + let platform_id = models::categories::LinkPlatform::get_id( + platform, + &mut **transaction, + ) + .await? + .ok_or_else(|| { + CreateError::InvalidInput(format!( + "Link platform {} does not exist.", + platform.clone() + )) + })?; let link_platform = link_platforms .iter() .find(|x| x.id == platform_id) @@ -718,7 +761,9 @@ async fn project_create_inner( let project_builder_actual = models::project_item::ProjectBuilder { project_id: project_id.into(), team_id, - organization_id: project_create_data.organization_id.map(|x| x.into()), + organization_id: project_create_data + .organization_id + .map(|x| x.into()), name: project_create_data.name, summary: project_create_data.summary, description: project_create_data.description, @@ -757,8 +802,12 @@ async fn project_create_inner( User::clear_project_cache(&[current_user.id.into()], redis).await?; for image_id in project_create_data.uploaded_images { - if let Some(db_image) = - image_item::Image::get(image_id.into(), &mut **transaction, redis).await? + if let Some(db_image) = image_item::Image::get( + image_id.into(), + &mut **transaction, + redis, + ) + .await? { let image: Image = db_image.into(); if !matches!(image.context, ImageContext::Project { .. }) @@ -884,12 +933,13 @@ async fn create_initial_version( ))); } - version_data - .validate() - .map_err(|err| CreateError::ValidationError(validation_errors_to_string(err, None)))?; + version_data.validate().map_err(|err| { + CreateError::ValidationError(validation_errors_to_string(err, None)) + })?; // Randomly generate a new id to be used for the version - let version_id: VersionId = models::generate_version_id(transaction).await?.into(); + let version_id: VersionId = + models::generate_version_id(transaction).await?.into(); let loaders = version_data .loaders @@ -903,10 +953,15 @@ async fn create_initial_version( }) .collect::, CreateError>>()?; - let loader_fields = LoaderField::get_fields(&loaders, &mut **transaction, redis).await?; + let loader_fields = + LoaderField::get_fields(&loaders, &mut **transaction, redis).await?; let mut loader_field_enum_values = - LoaderFieldEnumValue::list_many_loader_fields(&loader_fields, &mut **transaction, redis) - .await?; + LoaderFieldEnumValue::list_many_loader_fields( + &loader_fields, + &mut **transaction, + redis, + ) + .await?; let version_fields = try_create_version_fields( version_id, @@ -954,7 +1009,12 @@ async fn process_icon_upload( file_host: &dyn FileHost, mut field: Field, ) -> Result<(String, String, Option), CreateError> { - let data = read_from_field(&mut field, 262144, "Icons must be smaller than 256KiB").await?; + let data = read_from_field( + &mut field, + 262144, + "Icons must be smaller than 256KiB", + ) + .await?; let upload_result = crate::util::img::upload_image_optimized( &format!("data/{}", to_base62(id)), data.freeze(), diff --git a/apps/labrinth/src/routes/v3/projects.rs b/apps/labrinth/src/routes/v3/projects.rs index 7caa3e3a3..5e7277ec3 100644 --- a/apps/labrinth/src/routes/v3/projects.rs +++ b/apps/labrinth/src/routes/v3/projects.rs @@ -64,7 +64,10 @@ pub fn config(cfg: &mut web::ServiceConfig) { "members", web::get().to(super::teams::team_members_get_project), ) - .route("version", web::get().to(super::versions::version_list)) + .route( + "version", + web::get().to(super::versions::version_list), + ) .route( "version/{slug}", web::get().to(super::versions::version_project_get), @@ -85,9 +88,9 @@ pub async fn random_projects_get( pool: web::Data, redis: web::Data, ) -> Result { - count - .validate() - .map_err(|err| ApiError::Validation(validation_errors_to_string(err, None)))?; + count.validate().map_err(|err| { + ApiError::Validation(validation_errors_to_string(err, None)) + })?; let project_ids = sqlx::query!( " @@ -104,11 +107,12 @@ pub async fn random_projects_get( .try_collect::>() .await?; - let projects_data = db_models::Project::get_many_ids(&project_ids, &**pool, &redis) - .await? - .into_iter() - .map(Project::from) - .collect::>(); + let projects_data = + db_models::Project::get_many_ids(&project_ids, &**pool, &redis) + .await? + .into_iter() + .map(Project::from) + .collect::>(); Ok(HttpResponse::Ok().json(projects_data)) } @@ -126,7 +130,8 @@ pub async fn projects_get( session_queue: web::Data, ) -> Result { let ids = serde_json::from_str::>(&ids.ids)?; - let projects_data = db_models::Project::get_many(&ids, &**pool, &redis).await?; + let projects_data = + db_models::Project::get_many(&ids, &**pool, &redis).await?; let user_option = get_user_from_headers( &req, @@ -139,7 +144,9 @@ pub async fn projects_get( .map(|x| x.1) .ok(); - let projects = filter_visible_projects(projects_data, &user_option, &pool, false).await?; + let projects = + filter_visible_projects(projects_data, &user_option, &pool, false) + .await?; Ok(HttpResponse::Ok().json(projects)) } @@ -153,7 +160,8 @@ pub async fn project_get( ) -> Result { let string = info.into_inner().0; - let project_data = db_models::Project::get(&string, &**pool, &redis).await?; + let project_data = + db_models::Project::get(&string, &**pool, &redis).await?; let user_option = get_user_from_headers( &req, &**pool, @@ -198,7 +206,9 @@ pub struct EditProject { length(max = 2048) )] pub license_url: Option>, - #[validate(custom(function = "crate::util::validate::validate_url_hashmap_optional_values"))] + #[validate(custom( + function = "crate::util::validate::validate_url_hashmap_optional_values" + ))] // (leave url empty to delete) pub link_urls: Option>>, pub license_id: Option, @@ -252,9 +262,9 @@ pub async fn project_edit( .await? .1; - new_project - .validate() - .map_err(|err| ApiError::Validation(validation_errors_to_string(err, None)))?; + new_project.validate().map_err(|err| { + ApiError::Validation(validation_errors_to_string(err, None)) + })?; let string = info.into_inner().0; let result = db_models::Project::get(&string, &**pool, &redis).await?; @@ -331,10 +341,12 @@ pub async fn project_edit( if !(user.role.is_mod() || !project_item.inner.status.is_approved() && status == &ProjectStatus::Processing - || project_item.inner.status.is_approved() && status.can_be_requested()) + || project_item.inner.status.is_approved() + && status.can_be_requested()) { return Err(ApiError::CustomAuthentication( - "You don't have permission to set this status!".to_string(), + "You don't have permission to set this status!" + .to_string(), )); } @@ -361,7 +373,9 @@ pub async fn project_edit( .insert(project_item.inner.id.into()); } - if status.is_approved() && !project_item.inner.status.is_approved() { + if status.is_approved() + && !project_item.inner.status.is_approved() + { sqlx::query!( " UPDATE mods @@ -374,7 +388,9 @@ pub async fn project_edit( .await?; } if status.is_searchable() && !project_item.inner.webhook_sent { - if let Ok(webhook_url) = dotenvy::var("PUBLIC_DISCORD_WEBHOOK") { + if let Ok(webhook_url) = + dotenvy::var("PUBLIC_DISCORD_WEBHOOK") + { crate::util::webhook::send_discord_webhook( project_item.inner.id.into(), &pool, @@ -399,7 +415,9 @@ pub async fn project_edit( } if user.role.is_mod() { - if let Ok(webhook_url) = dotenvy::var("MODERATION_SLACK_WEBHOOK") { + if let Ok(webhook_url) = + dotenvy::var("MODERATION_SLACK_WEBHOOK") + { crate::util::webhook::send_slack_webhook( project_item.inner.id.into(), &pool, @@ -471,7 +489,9 @@ pub async fn project_edit( .execute(&mut *transaction) .await?; - if project_item.inner.status.is_searchable() && !status.is_searchable() { + if project_item.inner.status.is_searchable() + && !status.is_searchable() + { remove_documents( &project_item .versions @@ -591,7 +611,8 @@ pub async fn project_edit( )); } - let slug_project_id_option: Option = parse_base62(slug).ok(); + let slug_project_id_option: Option = + parse_base62(slug).ok(); if let Some(slug_project_id) = slug_project_id_option { let results = sqlx::query!( " @@ -604,14 +625,20 @@ pub async fn project_edit( if results.exists.unwrap_or(true) { return Err(ApiError::InvalidInput( - "Slug collides with other project's id!".to_string(), + "Slug collides with other project's id!" + .to_string(), )); } } // Make sure the new slug is different from the old one // We are able to unwrap here because the slug is always set - if !slug.eq(&project_item.inner.slug.clone().unwrap_or_default()) { + if !slug.eq(&project_item + .inner + .slug + .clone() + .unwrap_or_default()) + { let results = sqlx::query!( " SELECT EXISTS(SELECT 1 FROM mods WHERE slug = LOWER($1)) @@ -623,7 +650,8 @@ pub async fn project_edit( if results.exists.unwrap_or(true) { return Err(ApiError::InvalidInput( - "Slug collides with other project's id!".to_string(), + "Slug collides with other project's id!" + .to_string(), )); } } @@ -656,7 +684,9 @@ pub async fn project_edit( } spdx::Expression::parse(&license).map_err(|err| { - ApiError::InvalidInput(format!("Invalid SPDX license identifier: {err}")) + ApiError::InvalidInput(format!( + "Invalid SPDX license identifier: {err}" + )) })?; sqlx::query!( @@ -700,17 +730,20 @@ pub async fn project_edit( for (platform, url) in links { if let Some(url) = url { - let platform_id = db_models::categories::LinkPlatform::get_id( - platform, - &mut *transaction, - ) - .await? - .ok_or_else(|| { - ApiError::InvalidInput(format!( - "Platform {} does not exist.", - platform.clone() - )) - })?; + let platform_id = + db_models::categories::LinkPlatform::get_id( + platform, + &mut *transaction, + ) + .await? + .ok_or_else( + || { + ApiError::InvalidInput(format!( + "Platform {} does not exist.", + platform.clone() + )) + }, + )?; sqlx::query!( " INSERT INTO mods_links (joining_mod_id, joining_platform_id, url) @@ -728,7 +761,8 @@ pub async fn project_edit( } if let Some(moderation_message) = &new_project.moderation_message { if !user.role.is_mod() - && (!project_item.inner.status.is_approved() || moderation_message.is_some()) + && (!project_item.inner.status.is_approved() + || moderation_message.is_some()) { return Err(ApiError::CustomAuthentication( "You do not have the permissions to edit the moderation message of this project!" @@ -749,7 +783,9 @@ pub async fn project_edit( .await?; } - if let Some(moderation_message_body) = &new_project.moderation_message_body { + if let Some(moderation_message_body) = + &new_project.moderation_message_body + { if !user.role.is_mod() && (!project_item.inner.status.is_approved() || moderation_message_body.is_some()) @@ -794,7 +830,8 @@ pub async fn project_edit( .await?; } - if let Some(monetization_status) = &new_project.monetization_status { + if let Some(monetization_status) = &new_project.monetization_status + { if !perms.contains(ProjectPermissions::EDIT_DETAILS) { return Err(ApiError::CustomAuthentication( "You do not have the permissions to edit the monetization status of this project!" @@ -802,7 +839,8 @@ pub async fn project_edit( )); } - if (*monetization_status == MonetizationStatus::ForceDemonetized + if (*monetization_status + == MonetizationStatus::ForceDemonetized || project_item.inner.monetization_status == MonetizationStatus::ForceDemonetized) && !user.role.is_mod() @@ -828,16 +866,23 @@ pub async fn project_edit( // check new description and body for links to associated images // if they no longer exist in the description or body, delete them - let checkable_strings: Vec<&str> = vec![&new_project.description, &new_project.summary] - .into_iter() - .filter_map(|x| x.as_ref().map(|y| y.as_str())) - .collect(); + let checkable_strings: Vec<&str> = + vec![&new_project.description, &new_project.summary] + .into_iter() + .filter_map(|x| x.as_ref().map(|y| y.as_str())) + .collect(); let context = ImageContext::Project { project_id: Some(id.into()), }; - img::delete_unused_images(context, checkable_strings, &mut transaction, &redis).await?; + img::delete_unused_images( + context, + checkable_strings, + &mut transaction, + &redis, + ) + .await?; transaction.commit().await?; db_models::Project::clear_cache( @@ -875,8 +920,11 @@ pub async fn edit_project_categories( let mut mod_categories = Vec::new(); for category in categories { - let category_ids = - db_models::categories::Category::get_ids(category, &mut **transaction).await?; + let category_ids = db_models::categories::Category::get_ids( + category, + &mut **transaction, + ) + .await?; // TODO: We should filter out categories that don't match the project type of any of the versions // ie: if mod and modpack both share a name this should only have modpack if it only has a modpack as a version @@ -969,12 +1017,18 @@ pub async fn dependency_list( .ok(); if let Some(project) = result { - if !is_visible_project(&project.inner, &user_option, &pool, false).await? { + if !is_visible_project(&project.inner, &user_option, &pool, false) + .await? + { return Err(ApiError::NotFound); } - let dependencies = - database::Project::get_dependencies(project.inner.id, &**pool, &redis).await?; + let dependencies = database::Project::get_dependencies( + project.inner.id, + &**pool, + &redis, + ) + .await?; let project_ids = dependencies .iter() .filter_map(|x| { @@ -1002,10 +1056,20 @@ pub async fn dependency_list( ) .await?; - let mut projects = - filter_visible_projects(projects_result, &user_option, &pool, false).await?; - let mut versions = - filter_visible_versions(versions_result, &user_option, &pool, &redis).await?; + let mut projects = filter_visible_projects( + projects_result, + &user_option, + &pool, + false, + ) + .await?; + let mut versions = filter_visible_versions( + versions_result, + &user_option, + &pool, + &redis, + ) + .await?; projects.sort_by(|a, b| b.published.cmp(&a.published)); projects.dedup_by(|a, b| a.id == b.id); @@ -1040,7 +1104,9 @@ pub struct BulkEditProject { pub add_additional_categories: Option>, pub remove_additional_categories: Option>, - #[validate(custom(function = " crate::util::validate::validate_url_hashmap_optional_values"))] + #[validate(custom( + function = " crate::util::validate::validate_url_hashmap_optional_values" + ))] pub link_urls: Option>>, } @@ -1062,16 +1128,18 @@ pub async fn projects_edit( .await? .1; - bulk_edit_project - .validate() - .map_err(|err| ApiError::Validation(validation_errors_to_string(err, None)))?; + bulk_edit_project.validate().map_err(|err| { + ApiError::Validation(validation_errors_to_string(err, None)) + })?; - let project_ids: Vec = serde_json::from_str::>(&ids.ids)? - .into_iter() - .map(|x| x.into()) - .collect(); + let project_ids: Vec = + serde_json::from_str::>(&ids.ids)? + .into_iter() + .map(|x| x.into()) + .collect(); - let projects_data = db_models::Project::get_many_ids(&project_ids, &**pool, &redis).await?; + let projects_data = + db_models::Project::get_many_ids(&project_ids, &**pool, &redis).await?; if let Some(id) = project_ids .iter() @@ -1087,47 +1155,62 @@ pub async fn projects_edit( .iter() .map(|x| x.inner.team_id) .collect::>(); - let team_members = - db_models::TeamMember::get_from_team_full_many(&team_ids, &**pool, &redis).await?; + let team_members = db_models::TeamMember::get_from_team_full_many( + &team_ids, &**pool, &redis, + ) + .await?; let organization_ids = projects_data .iter() .filter_map(|x| x.inner.organization_id) .collect::>(); - let organizations = - db_models::Organization::get_many_ids(&organization_ids, &**pool, &redis).await?; + let organizations = db_models::Organization::get_many_ids( + &organization_ids, + &**pool, + &redis, + ) + .await?; let organization_team_ids = organizations .iter() .map(|x| x.team_id) .collect::>(); let organization_team_members = - db_models::TeamMember::get_from_team_full_many(&organization_team_ids, &**pool, &redis) - .await?; + db_models::TeamMember::get_from_team_full_many( + &organization_team_ids, + &**pool, + &redis, + ) + .await?; - let categories = db_models::categories::Category::list(&**pool, &redis).await?; - let link_platforms = db_models::categories::LinkPlatform::list(&**pool, &redis).await?; + let categories = + db_models::categories::Category::list(&**pool, &redis).await?; + let link_platforms = + db_models::categories::LinkPlatform::list(&**pool, &redis).await?; let mut transaction = pool.begin().await?; for project in projects_data { if !user.role.is_mod() { - let team_member = team_members - .iter() - .find(|x| x.team_id == project.inner.team_id && x.user_id == user.id.into()); + let team_member = team_members.iter().find(|x| { + x.team_id == project.inner.team_id + && x.user_id == user.id.into() + }); let organization = project .inner .organization_id .and_then(|oid| organizations.iter().find(|x| x.id == oid)); - let organization_team_member = if let Some(organization) = organization { - organization_team_members - .iter() - .find(|x| x.team_id == organization.team_id && x.user_id == user.id.into()) - } else { - None - }; + let organization_team_member = + if let Some(organization) = organization { + organization_team_members.iter().find(|x| { + x.team_id == organization.team_id + && x.user_id == user.id.into() + }) + } else { + None + }; let permissions = ProjectPermissions::get_permissions_by_role( &user.role, @@ -1232,7 +1315,13 @@ pub async fn projects_edit( } } - db_models::Project::clear_cache(project.inner.id, project.inner.slug, None, &redis).await?; + db_models::Project::clear_cache( + project.inner.id, + project.inner.slug, + None, + &redis, + ) + .await?; } transaction.commit().await?; @@ -1249,15 +1338,17 @@ pub async fn bulk_edit_project_categories( is_additional: bool, transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, ) -> Result<(), ApiError> { - let mut set_categories = if let Some(categories) = bulk_changes.categories.clone() { - categories - } else { - project_categories.clone() - }; + let mut set_categories = + if let Some(categories) = bulk_changes.categories.clone() { + categories + } else { + project_categories.clone() + }; if let Some(delete_categories) = &bulk_changes.remove_categories { for category in delete_categories { - if let Some(pos) = set_categories.iter().position(|x| x == category) { + if let Some(pos) = set_categories.iter().position(|x| x == category) + { set_categories.remove(pos); } } @@ -1291,10 +1382,17 @@ pub async fn bulk_edit_project_categories( .iter() .find(|x| x.category == category) .ok_or_else(|| { - ApiError::InvalidInput(format!("Category {} does not exist.", category.clone())) + ApiError::InvalidInput(format!( + "Category {} does not exist.", + category.clone() + )) })? .id; - mod_categories.push(ModCategory::new(project_id, category_id, is_additional)); + mod_categories.push(ModCategory::new( + project_id, + category_id, + is_additional, + )); } ModCategory::insert_many(mod_categories, &mut *transaction).await?; } @@ -1332,7 +1430,9 @@ pub async fn project_icon_edit( let project_item = db_models::Project::get(&string, &**pool, &redis) .await? .ok_or_else(|| { - ApiError::InvalidInput("The specified project does not exist!".to_string()) + ApiError::InvalidInput( + "The specified project does not exist!".to_string(), + ) })?; if !user.role.is_mod() { @@ -1360,7 +1460,8 @@ pub async fn project_icon_edit( if !permissions.contains(ProjectPermissions::EDIT_DETAILS) { return Err(ApiError::CustomAuthentication( - "You don't have permission to edit this project's icon.".to_string(), + "You don't have permission to edit this project's icon." + .to_string(), )); } } @@ -1372,8 +1473,12 @@ pub async fn project_icon_edit( ) .await?; - let bytes = - read_from_payload(&mut payload, 262144, "Icons must be smaller than 256KiB").await?; + let bytes = read_from_payload( + &mut payload, + 262144, + "Icons must be smaller than 256KiB", + ) + .await?; let project_id: ProjectId = project_item.inner.id.into(); let upload_result = upload_image_optimized( @@ -1403,8 +1508,13 @@ pub async fn project_icon_edit( .await?; transaction.commit().await?; - db_models::Project::clear_cache(project_item.inner.id, project_item.inner.slug, None, &redis) - .await?; + db_models::Project::clear_cache( + project_item.inner.id, + project_item.inner.slug, + None, + &redis, + ) + .await?; Ok(HttpResponse::NoContent().body("")) } @@ -1431,7 +1541,9 @@ pub async fn delete_project_icon( let project_item = db_models::Project::get(&string, &**pool, &redis) .await? .ok_or_else(|| { - ApiError::InvalidInput("The specified project does not exist!".to_string()) + ApiError::InvalidInput( + "The specified project does not exist!".to_string(), + ) })?; if !user.role.is_mod() { @@ -1458,7 +1570,8 @@ pub async fn delete_project_icon( if !permissions.contains(ProjectPermissions::EDIT_DETAILS) { return Err(ApiError::CustomAuthentication( - "You don't have permission to edit this project's icon.".to_string(), + "You don't have permission to edit this project's icon." + .to_string(), )); } } @@ -1484,8 +1597,13 @@ pub async fn delete_project_icon( .await?; transaction.commit().await?; - db_models::Project::clear_cache(project_item.inner.id, project_item.inner.slug, None, &redis) - .await?; + db_models::Project::clear_cache( + project_item.inner.id, + project_item.inner.slug, + None, + &redis, + ) + .await?; Ok(HttpResponse::NoContent().body("")) } @@ -1512,8 +1630,9 @@ pub async fn add_gallery_item( mut payload: web::Payload, session_queue: web::Data, ) -> Result { - item.validate() - .map_err(|err| ApiError::Validation(validation_errors_to_string(err, None)))?; + item.validate().map_err(|err| { + ApiError::Validation(validation_errors_to_string(err, None)) + })?; let user = get_user_from_headers( &req, @@ -1529,12 +1648,15 @@ pub async fn add_gallery_item( let project_item = db_models::Project::get(&string, &**pool, &redis) .await? .ok_or_else(|| { - ApiError::InvalidInput("The specified project does not exist!".to_string()) + ApiError::InvalidInput( + "The specified project does not exist!".to_string(), + ) })?; if project_item.gallery_items.len() > 64 { return Err(ApiError::CustomAuthentication( - "You have reached the maximum of gallery images to upload.".to_string(), + "You have reached the maximum of gallery images to upload." + .to_string(), )); } @@ -1563,7 +1685,8 @@ pub async fn add_gallery_item( if !permissions.contains(ProjectPermissions::EDIT_DETAILS) { return Err(ApiError::CustomAuthentication( - "You don't have permission to edit this project's gallery.".to_string(), + "You don't have permission to edit this project's gallery." + .to_string(), )); } } @@ -1621,11 +1744,21 @@ pub async fn add_gallery_item( created: Utc::now(), ordering: item.ordering.unwrap_or(0), }]; - GalleryItem::insert_many(gallery_item, project_item.inner.id, &mut transaction).await?; + GalleryItem::insert_many( + gallery_item, + project_item.inner.id, + &mut transaction, + ) + .await?; transaction.commit().await?; - db_models::Project::clear_cache(project_item.inner.id, project_item.inner.slug, None, &redis) - .await?; + db_models::Project::clear_cache( + project_item.inner.id, + project_item.inner.slug, + None, + &redis, + ) + .await?; Ok(HttpResponse::NoContent().body("")) } @@ -1671,13 +1804,16 @@ pub async fn edit_gallery_item( .1; let string = info.into_inner().0; - item.validate() - .map_err(|err| ApiError::Validation(validation_errors_to_string(err, None)))?; + item.validate().map_err(|err| { + ApiError::Validation(validation_errors_to_string(err, None)) + })?; let project_item = db_models::Project::get(&string, &**pool, &redis) .await? .ok_or_else(|| { - ApiError::InvalidInput("The specified project does not exist!".to_string()) + ApiError::InvalidInput( + "The specified project does not exist!".to_string(), + ) })?; if !user.role.is_mod() { @@ -1704,7 +1840,8 @@ pub async fn edit_gallery_item( if !permissions.contains(ProjectPermissions::EDIT_DETAILS) { return Err(ApiError::CustomAuthentication( - "You don't have permission to edit this project's gallery.".to_string(), + "You don't have permission to edit this project's gallery." + .to_string(), )); } } @@ -1798,8 +1935,13 @@ pub async fn edit_gallery_item( transaction.commit().await?; - db_models::Project::clear_cache(project_item.inner.id, project_item.inner.slug, None, &redis) - .await?; + db_models::Project::clear_cache( + project_item.inner.id, + project_item.inner.slug, + None, + &redis, + ) + .await?; Ok(HttpResponse::NoContent().body("")) } @@ -1832,7 +1974,9 @@ pub async fn delete_gallery_item( let project_item = db_models::Project::get(&string, &**pool, &redis) .await? .ok_or_else(|| { - ApiError::InvalidInput("The specified project does not exist!".to_string()) + ApiError::InvalidInput( + "The specified project does not exist!".to_string(), + ) })?; if !user.role.is_mod() { @@ -1860,7 +2004,8 @@ pub async fn delete_gallery_item( if !permissions.contains(ProjectPermissions::EDIT_DETAILS) { return Err(ApiError::CustomAuthentication( - "You don't have permission to edit this project's gallery.".to_string(), + "You don't have permission to edit this project's gallery." + .to_string(), )); } } @@ -1903,8 +2048,13 @@ pub async fn delete_gallery_item( transaction.commit().await?; - db_models::Project::clear_cache(project_item.inner.id, project_item.inner.slug, None, &redis) - .await?; + db_models::Project::clear_cache( + project_item.inner.id, + project_item.inner.slug, + None, + &redis, + ) + .await?; Ok(HttpResponse::NoContent().body("")) } @@ -1931,7 +2081,9 @@ pub async fn project_delete( let project = db_models::Project::get(&string, &**pool, &redis) .await? .ok_or_else(|| { - ApiError::InvalidInput("The specified project does not exist!".to_string()) + ApiError::InvalidInput( + "The specified project does not exist!".to_string(), + ) })?; if !user.role.is_admin() { @@ -1968,7 +2120,8 @@ pub async fn project_delete( let context = ImageContext::Project { project_id: Some(project.inner.id.into()), }; - let uploaded_images = db_models::Image::get_many_contexted(context, &mut transaction).await?; + let uploaded_images = + db_models::Image::get_many_contexted(context, &mut transaction).await?; for image in uploaded_images { image_item::Image::remove(image.id, &mut transaction, &redis).await?; } @@ -1983,7 +2136,9 @@ pub async fn project_delete( .execute(&mut *transaction) .await?; - let result = db_models::Project::remove(project.inner.id, &mut transaction, &redis).await?; + let result = + db_models::Project::remove(project.inner.id, &mut transaction, &redis) + .await?; transaction.commit().await?; @@ -2025,7 +2180,9 @@ pub async fn project_follow( let result = db_models::Project::get(&string, &**pool, &redis) .await? .ok_or_else(|| { - ApiError::InvalidInput("The specified project does not exist!".to_string()) + ApiError::InvalidInput( + "The specified project does not exist!".to_string(), + ) })?; let user_id: db_ids::UserId = user.id.into(); @@ -2103,7 +2260,9 @@ pub async fn project_unfollow( let result = db_models::Project::get(&string, &**pool, &redis) .await? .ok_or_else(|| { - ApiError::InvalidInput("The specified project does not exist!".to_string()) + ApiError::InvalidInput( + "The specified project does not exist!".to_string(), + ) })?; let user_id: db_ids::UserId = user.id.into(); @@ -2179,7 +2338,9 @@ pub async fn project_get_organization( let result = db_models::Project::get(&string, &**pool, &redis) .await? .ok_or_else(|| { - ApiError::InvalidInput("The specified project does not exist!".to_string()) + ApiError::InvalidInput( + "The specified project does not exist!".to_string(), + ) })?; if !is_visible_project(&result.inner, ¤t_user, &pool, false).await? { @@ -2187,14 +2348,21 @@ pub async fn project_get_organization( "The specified project does not exist!".to_string(), )) } else if let Some(organization_id) = result.inner.organization_id { - let organization = db_models::Organization::get_id(organization_id, &**pool, &redis) - .await? - .ok_or_else(|| { - ApiError::InvalidInput("The attached organization does not exist!".to_string()) - })?; + let organization = + db_models::Organization::get_id(organization_id, &**pool, &redis) + .await? + .ok_or_else(|| { + ApiError::InvalidInput( + "The attached organization does not exist!".to_string(), + ) + })?; - let members_data = - TeamMember::get_from_team_full(organization.team_id, &**pool, &redis).await?; + let members_data = TeamMember::get_from_team_full( + organization.team_id, + &**pool, + &redis, + ) + .await?; let users = crate::database::models::User::get_many_ids( &members_data.iter().map(|x| x.user_id).collect::>(), @@ -2216,17 +2384,26 @@ pub async fn project_get_organization( logged_in || x.accepted || user_id - .map(|y: crate::database::models::UserId| y == x.user_id) + .map(|y: crate::database::models::UserId| { + y == x.user_id + }) .unwrap_or(false) }) .flat_map(|data| { users.iter().find(|x| x.id == data.user_id).map(|user| { - crate::models::teams::TeamMember::from(data, user.clone(), !logged_in) + crate::models::teams::TeamMember::from( + data, + user.clone(), + !logged_in, + ) }) }) .collect(); - let organization = models::organizations::Organization::from(organization, team_members); + let organization = models::organizations::Organization::from( + organization, + team_members, + ); return Ok(HttpResponse::Ok().json(organization)); } else { Err(ApiError::NotFound) diff --git a/apps/labrinth/src/routes/v3/reports.rs b/apps/labrinth/src/routes/v3/reports.rs index 632160ae1..1af674627 100644 --- a/apps/labrinth/src/routes/v3/reports.rs +++ b/apps/labrinth/src/routes/v3/reports.rs @@ -1,10 +1,14 @@ use crate::auth::{check_is_moderator_from_headers, get_user_from_headers}; use crate::database; use crate::database::models::image_item; -use crate::database::models::thread_item::{ThreadBuilder, ThreadMessageBuilder}; +use crate::database::models::thread_item::{ + ThreadBuilder, ThreadMessageBuilder, +}; use crate::database::redis::RedisPool; use crate::models::ids::ImageId; -use crate::models::ids::{base62_impl::parse_base62, ProjectId, UserId, VersionId}; +use crate::models::ids::{ + base62_impl::parse_base62, ProjectId, UserId, VersionId, +}; use crate::models::images::{Image, ImageContext}; use crate::models::pats::Scopes; use crate::models::reports::{ItemType, Report}; @@ -62,19 +66,25 @@ pub async fn report_create( let mut bytes = web::BytesMut::new(); while let Some(item) = body.next().await { bytes.extend_from_slice(&item.map_err(|_| { - ApiError::InvalidInput("Error while parsing request payload!".to_string()) + ApiError::InvalidInput( + "Error while parsing request payload!".to_string(), + ) })?); } let new_report: CreateReport = serde_json::from_slice(bytes.as_ref())?; - let id = crate::database::models::generate_report_id(&mut transaction).await?; + let id = + crate::database::models::generate_report_id(&mut transaction).await?; let report_type = crate::database::models::categories::ReportType::get_id( &new_report.report_type, &mut *transaction, ) .await? .ok_or_else(|| { - ApiError::InvalidInput(format!("Invalid report type: {}", new_report.report_type)) + ApiError::InvalidInput(format!( + "Invalid report type: {}", + new_report.report_type + )) })?; let mut report = crate::database::models::report_item::Report { @@ -91,7 +101,8 @@ pub async fn report_create( match new_report.item_type { ItemType::Project => { - let project_id = ProjectId(parse_base62(new_report.item_id.as_str())?); + let project_id = + ProjectId(parse_base62(new_report.item_id.as_str())?); let result = sqlx::query!( "SELECT EXISTS(SELECT 1 FROM mods WHERE id = $1)", @@ -110,7 +121,8 @@ pub async fn report_create( report.project_id = Some(project_id.into()) } ItemType::Version => { - let version_id = VersionId(parse_base62(new_report.item_id.as_str())?); + let version_id = + VersionId(parse_base62(new_report.item_id.as_str())?); let result = sqlx::query!( "SELECT EXISTS(SELECT 1 FROM versions WHERE id = $1)", @@ -159,7 +171,8 @@ pub async fn report_create( for image_id in new_report.uploaded_images { if let Some(db_image) = - image_item::Image::get(image_id.into(), &mut *transaction, &redis).await? + image_item::Image::get(image_id.into(), &mut *transaction, &redis) + .await? { let image: Image = db_image.into(); if !matches!(image.context, ImageContext::Report { .. }) @@ -281,8 +294,11 @@ pub async fn reports( .await? }; - let query_reports = - crate::database::models::report_item::Report::get_many(&report_ids, &**pool).await?; + let query_reports = crate::database::models::report_item::Report::get_many( + &report_ids, + &**pool, + ) + .await?; let mut reports: Vec = Vec::new(); @@ -311,8 +327,11 @@ pub async fn reports_get( .map(|x| x.into()) .collect(); - let reports_data = - crate::database::models::report_item::Report::get_many(&report_ids, &**pool).await?; + let reports_data = crate::database::models::report_item::Report::get_many( + &report_ids, + &**pool, + ) + .await?; let user = get_user_from_headers( &req, @@ -351,7 +370,8 @@ pub async fn report_get( .1; let id = info.into_inner().0.into(); - let report = crate::database::models::report_item::Report::get(id, &**pool).await?; + let report = + crate::database::models::report_item::Report::get(id, &**pool).await?; if let Some(report) = report { if !user.role.is_mod() && report.reporter != user.id.into() { @@ -391,7 +411,8 @@ pub async fn report_edit( .1; let id = info.into_inner().0.into(); - let report = crate::database::models::report_item::Report::get(id, &**pool).await?; + let report = + crate::database::models::report_item::Report::get(id, &**pool).await?; if let Some(report) = report { if !user.role.is_mod() && report.reporter != user.id.into() { @@ -455,8 +476,13 @@ pub async fn report_edit( let image_context = ImageContext::Report { report_id: Some(id.into()), }; - img::delete_unused_images(image_context, checkable_strings, &mut transaction, &redis) - .await?; + img::delete_unused_images( + image_context, + checkable_strings, + &mut transaction, + &redis, + ) + .await?; transaction.commit().await?; @@ -489,14 +515,17 @@ pub async fn report_delete( report_id: Some(id), }; let uploaded_images = - database::models::Image::get_many_contexted(context, &mut transaction).await?; + database::models::Image::get_many_contexted(context, &mut transaction) + .await?; for image in uploaded_images { image_item::Image::remove(image.id, &mut transaction, &redis).await?; } - let result = - crate::database::models::report_item::Report::remove_full(id.into(), &mut transaction) - .await?; + let result = crate::database::models::report_item::Report::remove_full( + id.into(), + &mut transaction, + ) + .await?; transaction.commit().await?; if result.is_some() { diff --git a/apps/labrinth/src/routes/v3/statistics.rs b/apps/labrinth/src/routes/v3/statistics.rs index c6c24e1a3..511448043 100644 --- a/apps/labrinth/src/routes/v3/statistics.rs +++ b/apps/labrinth/src/routes/v3/statistics.rs @@ -14,7 +14,9 @@ pub struct V3Stats { pub files: Option, } -pub async fn get_stats(pool: web::Data) -> Result { +pub async fn get_stats( + pool: web::Data, +) -> Result { let projects = sqlx::query!( " SELECT COUNT(id) diff --git a/apps/labrinth/src/routes/v3/tags.rs b/apps/labrinth/src/routes/v3/tags.rs index 834453f42..d7d5b2516 100644 --- a/apps/labrinth/src/routes/v3/tags.rs +++ b/apps/labrinth/src/routes/v3/tags.rs @@ -1,7 +1,9 @@ use std::collections::HashMap; use super::ApiError; -use crate::database::models::categories::{Category, LinkPlatform, ProjectType, ReportType}; +use crate::database::models::categories::{ + Category, LinkPlatform, ProjectType, ReportType, +}; use crate::database::models::loader_fields::{ Game, Loader, LoaderField, LoaderFieldEnumValue, LoaderFieldType, }; @@ -147,7 +149,8 @@ pub async fn loader_fields_list( })?; let loader_field_enum_id = match loader_field.field_type { - LoaderFieldType::Enum(enum_id) | LoaderFieldType::ArrayEnum(enum_id) => enum_id, + LoaderFieldType::Enum(enum_id) + | LoaderFieldType::ArrayEnum(enum_id) => enum_id, _ => { return Err(ApiError::InvalidInput(format!( "'{}' is not an enumerable field, but an '{}' field.", @@ -158,9 +161,16 @@ pub async fn loader_fields_list( }; let results: Vec<_> = if let Some(filters) = query.filters { - LoaderFieldEnumValue::list_filter(loader_field_enum_id, filters, &**pool, &redis).await? + LoaderFieldEnumValue::list_filter( + loader_field_enum_id, + filters, + &**pool, + &redis, + ) + .await? } else { - LoaderFieldEnumValue::list(loader_field_enum_id, &**pool, &redis).await? + LoaderFieldEnumValue::list(loader_field_enum_id, &**pool, &redis) + .await? }; Ok(HttpResponse::Ok().json(results)) @@ -192,7 +202,9 @@ pub struct LicenseText { pub body: String, } -pub async fn license_text(params: web::Path<(String,)>) -> Result { +pub async fn license_text( + params: web::Path<(String,)>, +) -> Result { let license_id = params.into_inner().0; if license_id == *crate::models::projects::DEFAULT_LICENSE_ID { @@ -224,14 +236,15 @@ pub async fn link_platform_list( pool: web::Data, redis: web::Data, ) -> Result { - let results: Vec = LinkPlatform::list(&**pool, &redis) - .await? - .into_iter() - .map(|x| LinkPlatformQueryData { - name: x.name, - donation: x.donation, - }) - .collect(); + let results: Vec = + LinkPlatform::list(&**pool, &redis) + .await? + .into_iter() + .map(|x| LinkPlatformQueryData { + name: x.name, + donation: x.donation, + }) + .collect(); Ok(HttpResponse::Ok().json(results)) } diff --git a/apps/labrinth/src/routes/v3/teams.rs b/apps/labrinth/src/routes/v3/teams.rs index 3c2749c57..4917d0547 100644 --- a/apps/labrinth/src/routes/v3/teams.rs +++ b/apps/labrinth/src/routes/v3/teams.rs @@ -7,7 +7,9 @@ use crate::database::redis::RedisPool; use crate::database::Project; use crate::models::notifications::NotificationBody; use crate::models::pats::Scopes; -use crate::models::teams::{OrganizationPermissions, ProjectPermissions, TeamId}; +use crate::models::teams::{ + OrganizationPermissions, ProjectPermissions, TeamId, +}; use crate::models::users::UserId; use crate::queue::session::AuthQueue; use crate::routes::ApiError; @@ -46,7 +48,8 @@ pub async fn team_members_get_project( session_queue: web::Data, ) -> Result { let string = info.into_inner().0; - let project_data = crate::database::models::Project::get(&string, &**pool, &redis).await?; + let project_data = + crate::database::models::Project::get(&string, &**pool, &redis).await?; if let Some(project) = project_data { let current_user = get_user_from_headers( @@ -60,11 +63,17 @@ pub async fn team_members_get_project( .map(|x| x.1) .ok(); - if !is_visible_project(&project.inner, ¤t_user, &pool, false).await? { + if !is_visible_project(&project.inner, ¤t_user, &pool, false) + .await? + { return Err(ApiError::NotFound); } - let members_data = - TeamMember::get_from_team_full(project.inner.team_id, &**pool, &redis).await?; + let members_data = TeamMember::get_from_team_full( + project.inner.team_id, + &**pool, + &redis, + ) + .await?; let users = User::get_many_ids( &members_data.iter().map(|x| x.user_id).collect::>(), &**pool, @@ -75,7 +84,12 @@ pub async fn team_members_get_project( let user_id = current_user.as_ref().map(|x| x.id.into()); let logged_in = if let Some(user_id) = user_id { let (team_member, organization_team_member) = - TeamMember::get_for_project_permissions(&project.inner, user_id, &**pool).await?; + TeamMember::get_for_project_permissions( + &project.inner, + user_id, + &**pool, + ) + .await?; team_member.is_some() || organization_team_member.is_some() } else { @@ -88,12 +102,18 @@ pub async fn team_members_get_project( logged_in || x.accepted || user_id - .map(|y: crate::database::models::UserId| y == x.user_id) + .map(|y: crate::database::models::UserId| { + y == x.user_id + }) .unwrap_or(false) }) .flat_map(|data| { users.iter().find(|x| x.id == data.user_id).map(|user| { - crate::models::teams::TeamMember::from(data, user.clone(), !logged_in) + crate::models::teams::TeamMember::from( + data, + user.clone(), + !logged_in, + ) }) }) .collect(); @@ -113,7 +133,8 @@ pub async fn team_members_get_organization( ) -> Result { let string = info.into_inner().0; let organization_data = - crate::database::models::Organization::get(&string, &**pool, &redis).await?; + crate::database::models::Organization::get(&string, &**pool, &redis) + .await?; if let Some(organization) = organization_data { let current_user = get_user_from_headers( @@ -127,8 +148,12 @@ pub async fn team_members_get_organization( .map(|x| x.1) .ok(); - let members_data = - TeamMember::get_from_team_full(organization.team_id, &**pool, &redis).await?; + let members_data = TeamMember::get_from_team_full( + organization.team_id, + &**pool, + &redis, + ) + .await?; let users = crate::database::models::User::get_many_ids( &members_data.iter().map(|x| x.user_id).collect::>(), &**pool, @@ -152,12 +177,18 @@ pub async fn team_members_get_organization( logged_in || x.accepted || user_id - .map(|y: crate::database::models::UserId| y == x.user_id) + .map(|y: crate::database::models::UserId| { + y == x.user_id + }) .unwrap_or(false) }) .flat_map(|data| { users.iter().find(|x| x.id == data.user_id).map(|user| { - crate::models::teams::TeamMember::from(data, user.clone(), !logged_in) + crate::models::teams::TeamMember::from( + data, + user.clone(), + !logged_in, + ) }) }) .collect(); @@ -177,7 +208,8 @@ pub async fn team_members_get( session_queue: web::Data, ) -> Result { let id = info.into_inner().0; - let members_data = TeamMember::get_from_team_full(id.into(), &**pool, &redis).await?; + let members_data = + TeamMember::get_from_team_full(id.into(), &**pool, &redis).await?; let users = crate::database::models::User::get_many_ids( &members_data.iter().map(|x| x.user_id).collect::>(), &**pool, @@ -215,10 +247,13 @@ pub async fn team_members_get( .unwrap_or(false) }) .flat_map(|data| { - users - .iter() - .find(|x| x.id == data.user_id) - .map(|user| crate::models::teams::TeamMember::from(data, user.clone(), !logged_in)) + users.iter().find(|x| x.id == data.user_id).map(|user| { + crate::models::teams::TeamMember::from( + data, + user.clone(), + !logged_in, + ) + }) }) .collect(); @@ -244,7 +279,8 @@ pub async fn teams_get( .map(|x| x.into()) .collect::>(); - let teams_data = TeamMember::get_from_team_full_many(&team_ids, &**pool, &redis).await?; + let teams_data = + TeamMember::get_from_team_full_many(&team_ids, &**pool, &redis).await?; let users = crate::database::models::User::get_many_ids( &teams_data.iter().map(|x| x.user_id).collect::>(), &**pool, @@ -284,7 +320,11 @@ pub async fn teams_get( .filter(|x| logged_in || x.accepted) .flat_map(|data| { users.iter().find(|x| x.id == data.user_id).map(|user| { - crate::models::teams::TeamMember::from(data, user.clone(), !logged_in) + crate::models::teams::TeamMember::from( + data, + user.clone(), + !logged_in, + ) }) }); @@ -312,8 +352,12 @@ pub async fn join_team( .await? .1; - let member = - TeamMember::get_from_user_id_pending(team_id, current_user.id.into(), &**pool).await?; + let member = TeamMember::get_from_user_id_pending( + team_id, + current_user.id.into(), + &**pool, + ) + .await?; if let Some(member) = member { if member.accepted { @@ -398,19 +442,33 @@ pub async fn add_team_member( .1; let team_association = Team::get_association(team_id, &**pool) .await? - .ok_or_else(|| ApiError::InvalidInput("The team specified does not exist".to_string()))?; - let member = TeamMember::get_from_user_id(team_id, current_user.id.into(), &**pool).await?; + .ok_or_else(|| { + ApiError::InvalidInput( + "The team specified does not exist".to_string(), + ) + })?; + let member = + TeamMember::get_from_user_id(team_id, current_user.id.into(), &**pool) + .await?; match team_association { // If team is associated with a project, check if they have permissions to invite users to that project TeamAssociationId::Project(pid) => { let organization = - Organization::get_associated_organization_project_id(pid, &**pool).await?; - let organization_team_member = if let Some(organization) = &organization { - TeamMember::get_from_user_id(organization.team_id, current_user.id.into(), &**pool) + Organization::get_associated_organization_project_id( + pid, &**pool, + ) + .await?; + let organization_team_member = + if let Some(organization) = &organization { + TeamMember::get_from_user_id( + organization.team_id, + current_user.id.into(), + &**pool, + ) .await? - } else { - None - }; + } else { + None + }; let permissions = ProjectPermissions::get_permissions_by_role( ¤t_user.role, &member, @@ -420,12 +478,14 @@ pub async fn add_team_member( if !permissions.contains(ProjectPermissions::MANAGE_INVITES) { return Err(ApiError::CustomAuthentication( - "You don't have permission to invite users to this team".to_string(), + "You don't have permission to invite users to this team" + .to_string(), )); } if !permissions.contains(new_member.permissions) { return Err(ApiError::InvalidInput( - "The new member has permissions that you don't have".to_string(), + "The new member has permissions that you don't have" + .to_string(), )); } @@ -439,23 +499,28 @@ pub async fn add_team_member( // If team is associated with an organization, check if they have permissions to invite users to that organization TeamAssociationId::Organization(_) => { let organization_permissions = - OrganizationPermissions::get_permissions_by_role(¤t_user.role, &member) - .unwrap_or_default(); - if !organization_permissions.contains(OrganizationPermissions::MANAGE_INVITES) { + OrganizationPermissions::get_permissions_by_role( + ¤t_user.role, + &member, + ) + .unwrap_or_default(); + if !organization_permissions + .contains(OrganizationPermissions::MANAGE_INVITES) + { return Err(ApiError::CustomAuthentication( "You don't have permission to invite users to this organization".to_string(), )); } - if !organization_permissions - .contains(new_member.organization_permissions.unwrap_or_default()) - { + if !organization_permissions.contains( + new_member.organization_permissions.unwrap_or_default(), + ) { return Err(ApiError::InvalidInput( "The new member has organization permissions that you don't have".to_string(), )); } - if !organization_permissions - .contains(OrganizationPermissions::EDIT_MEMBER_DEFAULT_PERMISSIONS) - && !new_member.permissions.is_empty() + if !organization_permissions.contains( + OrganizationPermissions::EDIT_MEMBER_DEFAULT_PERMISSIONS, + ) && !new_member.permissions.is_empty() { return Err(ApiError::CustomAuthentication( "You do not have permission to give this user default project permissions. Ensure 'permissions' is set if it is not, and empty (0)." @@ -465,14 +530,20 @@ pub async fn add_team_member( } } - if new_member.payouts_split < Decimal::ZERO || new_member.payouts_split > Decimal::from(5000) { + if new_member.payouts_split < Decimal::ZERO + || new_member.payouts_split > Decimal::from(5000) + { return Err(ApiError::InvalidInput( "Payouts split must be between 0 and 5000!".to_string(), )); } - let request = - TeamMember::get_from_user_id_pending(team_id, new_member.user_id.into(), &**pool).await?; + let request = TeamMember::get_from_user_id_pending( + team_id, + new_member.user_id.into(), + &**pool, + ) + .await?; if let Some(req) = request { if req.accepted { @@ -481,25 +552,38 @@ pub async fn add_team_member( )); } else { return Err(ApiError::InvalidInput( - "There is already a pending member request for this user".to_string(), + "There is already a pending member request for this user" + .to_string(), )); } } - let new_user = - crate::database::models::User::get_id(new_member.user_id.into(), &**pool, &redis) - .await? - .ok_or_else(|| ApiError::InvalidInput("An invalid User ID specified".to_string()))?; + let new_user = crate::database::models::User::get_id( + new_member.user_id.into(), + &**pool, + &redis, + ) + .await? + .ok_or_else(|| { + ApiError::InvalidInput("An invalid User ID specified".to_string()) + })?; let mut force_accepted = false; if let TeamAssociationId::Project(pid) = team_association { // We cannot add the owner to a project team in their own org let organization = - Organization::get_associated_organization_project_id(pid, &**pool).await?; - let new_user_organization_team_member = if let Some(organization) = &organization { - TeamMember::get_from_user_id(organization.team_id, new_user.id, &**pool).await? - } else { - None - }; + Organization::get_associated_organization_project_id(pid, &**pool) + .await?; + let new_user_organization_team_member = + if let Some(organization) = &organization { + TeamMember::get_from_user_id( + organization.team_id, + new_user.id, + &**pool, + ) + .await? + } else { + None + }; if new_user_organization_team_member .as_ref() .map(|tm| tm.is_owner) @@ -521,7 +605,9 @@ pub async fn add_team_member( } } - let new_id = crate::database::models::ids::generate_team_member_id(&mut transaction).await?; + let new_id = + crate::database::models::ids::generate_team_member_id(&mut transaction) + .await?; TeamMember { id: new_id, team_id, @@ -605,22 +691,30 @@ pub async fn edit_team_member( .await? .1; - let team_association = Team::get_association(id, &**pool) - .await? - .ok_or_else(|| ApiError::InvalidInput("The team specified does not exist".to_string()))?; - let member = TeamMember::get_from_user_id(id, current_user.id.into(), &**pool).await?; - let edit_member_db = TeamMember::get_from_user_id_pending(id, user_id, &**pool) - .await? - .ok_or_else(|| { - ApiError::CustomAuthentication( - "You don't have permission to edit members of this team".to_string(), + let team_association = + Team::get_association(id, &**pool).await?.ok_or_else(|| { + ApiError::InvalidInput( + "The team specified does not exist".to_string(), ) })?; + let member = + TeamMember::get_from_user_id(id, current_user.id.into(), &**pool) + .await?; + let edit_member_db = + TeamMember::get_from_user_id_pending(id, user_id, &**pool) + .await? + .ok_or_else(|| { + ApiError::CustomAuthentication( + "You don't have permission to edit members of this team" + .to_string(), + ) + })?; let mut transaction = pool.begin().await?; if edit_member_db.is_owner - && (edit_member.permissions.is_some() || edit_member.organization_permissions.is_some()) + && (edit_member.permissions.is_some() + || edit_member.organization_permissions.is_some()) { return Err(ApiError::InvalidInput( "The owner's permission's in a team cannot be edited".to_string(), @@ -630,13 +724,21 @@ pub async fn edit_team_member( match team_association { TeamAssociationId::Project(project_id) => { let organization = - Organization::get_associated_organization_project_id(project_id, &**pool).await?; - let organization_team_member = if let Some(organization) = &organization { - TeamMember::get_from_user_id(organization.team_id, current_user.id.into(), &**pool) + Organization::get_associated_organization_project_id( + project_id, &**pool, + ) + .await?; + let organization_team_member = + if let Some(organization) = &organization { + TeamMember::get_from_user_id( + organization.team_id, + current_user.id.into(), + &**pool, + ) .await? - } else { - None - }; + } else { + None + }; if organization_team_member .as_ref() @@ -661,7 +763,8 @@ pub async fn edit_team_member( .unwrap_or_default(); if !permissions.contains(ProjectPermissions::EDIT_MEMBER) { return Err(ApiError::CustomAuthentication( - "You don't have permission to edit members of this team".to_string(), + "You don't have permission to edit members of this team" + .to_string(), )); } @@ -682,16 +785,23 @@ pub async fn edit_team_member( } TeamAssociationId::Organization(_) => { let organization_permissions = - OrganizationPermissions::get_permissions_by_role(¤t_user.role, &member) - .unwrap_or_default(); + OrganizationPermissions::get_permissions_by_role( + ¤t_user.role, + &member, + ) + .unwrap_or_default(); - if !organization_permissions.contains(OrganizationPermissions::EDIT_MEMBER) { + if !organization_permissions + .contains(OrganizationPermissions::EDIT_MEMBER) + { return Err(ApiError::CustomAuthentication( - "You don't have permission to edit members of this team".to_string(), + "You don't have permission to edit members of this team" + .to_string(), )); } - if let Some(new_permissions) = edit_member.organization_permissions { + if let Some(new_permissions) = edit_member.organization_permissions + { if !organization_permissions.contains(new_permissions) { return Err(ApiError::InvalidInput( "The new organization permissions have permissions that you don't have" @@ -701,8 +811,9 @@ pub async fn edit_team_member( } if edit_member.permissions.is_some() - && !organization_permissions - .contains(OrganizationPermissions::EDIT_MEMBER_DEFAULT_PERMISSIONS) + && !organization_permissions.contains( + OrganizationPermissions::EDIT_MEMBER_DEFAULT_PERMISSIONS, + ) { return Err(ApiError::CustomAuthentication( "You do not have permission to give this user default project permissions." @@ -713,7 +824,8 @@ pub async fn edit_team_member( } if let Some(payouts_split) = edit_member.payouts_split { - if payouts_split < Decimal::ZERO || payouts_split > Decimal::from(5000) { + if payouts_split < Decimal::ZERO || payouts_split > Decimal::from(5000) + { return Err(ApiError::InvalidInput( "Payouts split must be between 0 and 5000!".to_string(), )); @@ -782,26 +894,38 @@ pub async fn transfer_ownership( } if !current_user.role.is_admin() { - let member = TeamMember::get_from_user_id(id.into(), current_user.id.into(), &**pool) - .await? - .ok_or_else(|| { - ApiError::CustomAuthentication( - "You don't have permission to edit members of this team".to_string(), - ) - })?; + let member = TeamMember::get_from_user_id( + id.into(), + current_user.id.into(), + &**pool, + ) + .await? + .ok_or_else(|| { + ApiError::CustomAuthentication( + "You don't have permission to edit members of this team" + .to_string(), + ) + })?; if !member.is_owner { return Err(ApiError::CustomAuthentication( - "You don't have permission to edit the ownership of this team".to_string(), + "You don't have permission to edit the ownership of this team" + .to_string(), )); } } - let new_member = TeamMember::get_from_user_id(id.into(), new_owner.user_id.into(), &**pool) - .await? - .ok_or_else(|| { - ApiError::InvalidInput("The new owner specified does not exist".to_string()) - })?; + let new_member = TeamMember::get_from_user_id( + id.into(), + new_owner.user_id.into(), + &**pool, + ) + .await? + .ok_or_else(|| { + ApiError::InvalidInput( + "The new owner specified does not exist".to_string(), + ) + })?; if !new_member.accepted { return Err(ApiError::InvalidInput( @@ -848,7 +972,8 @@ pub async fn transfer_ownership( .await?; let project_teams_edited = - if let Some(TeamAssociationId::Organization(oid)) = team_association_id { + if let Some(TeamAssociationId::Organization(oid)) = team_association_id + { // The owner of ALL projects that this organization owns, if applicable, should be removed as members of the project, // if they are members of those projects. // (As they are the org owners for them, and they should not have more specific permissions) @@ -872,7 +997,12 @@ pub async fn transfer_ownership( // If the owner of the organization is a member of the project, remove them for team_id in team_ids.iter() { - TeamMember::delete(*team_id, new_owner.user_id.into(), &mut transaction).await?; + TeamMember::delete( + *team_id, + new_owner.user_id.into(), + &mut transaction, + ) + .await?; } team_ids @@ -910,12 +1040,18 @@ pub async fn remove_team_member( .await? .1; - let team_association = Team::get_association(id, &**pool) - .await? - .ok_or_else(|| ApiError::InvalidInput("The team specified does not exist".to_string()))?; - let member = TeamMember::get_from_user_id(id, current_user.id.into(), &**pool).await?; + let team_association = + Team::get_association(id, &**pool).await?.ok_or_else(|| { + ApiError::InvalidInput( + "The team specified does not exist".to_string(), + ) + })?; + let member = + TeamMember::get_from_user_id(id, current_user.id.into(), &**pool) + .await?; - let delete_member = TeamMember::get_from_user_id_pending(id, user_id, &**pool).await?; + let delete_member = + TeamMember::get_from_user_id_pending(id, user_id, &**pool).await?; if let Some(delete_member) = delete_member { if delete_member.is_owner { @@ -931,17 +1067,21 @@ pub async fn remove_team_member( match team_association { TeamAssociationId::Project(pid) => { let organization = - Organization::get_associated_organization_project_id(pid, &**pool).await?; - let organization_team_member = if let Some(organization) = &organization { - TeamMember::get_from_user_id( - organization.team_id, - current_user.id.into(), - &**pool, + Organization::get_associated_organization_project_id( + pid, &**pool, ) - .await? - } else { - None - }; + .await?; + let organization_team_member = + if let Some(organization) = &organization { + TeamMember::get_from_user_id( + organization.team_id, + current_user.id.into(), + &**pool, + ) + .await? + } else { + None + }; let permissions = ProjectPermissions::get_permissions_by_role( ¤t_user.role, &member, @@ -952,18 +1092,22 @@ pub async fn remove_team_member( if delete_member.accepted { // Members other than the owner can either leave the team, or be // removed by a member with the REMOVE_MEMBER permission. - if Some(delete_member.user_id) == member.as_ref().map(|m| m.user_id) - || permissions.contains(ProjectPermissions::REMOVE_MEMBER) + if Some(delete_member.user_id) + == member.as_ref().map(|m| m.user_id) + || permissions + .contains(ProjectPermissions::REMOVE_MEMBER) // true as if the permission exists, but the member does not, they are part of an org { - TeamMember::delete(id, user_id, &mut transaction).await?; + TeamMember::delete(id, user_id, &mut transaction) + .await?; } else { return Err(ApiError::CustomAuthentication( "You do not have permission to remove a member from this team" .to_string(), )); } - } else if Some(delete_member.user_id) == member.as_ref().map(|m| m.user_id) + } else if Some(delete_member.user_id) + == member.as_ref().map(|m| m.user_id) || permissions.contains(ProjectPermissions::MANAGE_INVITES) // true as if the permission exists, but the member does not, they are part of an org { @@ -973,30 +1117,38 @@ pub async fn remove_team_member( TeamMember::delete(id, user_id, &mut transaction).await?; } else { return Err(ApiError::CustomAuthentication( - "You do not have permission to cancel a team invite".to_string(), + "You do not have permission to cancel a team invite" + .to_string(), )); } } TeamAssociationId::Organization(_) => { let organization_permissions = - OrganizationPermissions::get_permissions_by_role(¤t_user.role, &member) - .unwrap_or_default(); + OrganizationPermissions::get_permissions_by_role( + ¤t_user.role, + &member, + ) + .unwrap_or_default(); // Organization teams requires a TeamMember, so we can 'unwrap' if delete_member.accepted { // Members other than the owner can either leave the team, or be // removed by a member with the REMOVE_MEMBER permission. if Some(delete_member.user_id) == member.map(|m| m.user_id) - || organization_permissions.contains(OrganizationPermissions::REMOVE_MEMBER) + || organization_permissions + .contains(OrganizationPermissions::REMOVE_MEMBER) { - TeamMember::delete(id, user_id, &mut transaction).await?; + TeamMember::delete(id, user_id, &mut transaction) + .await?; } else { return Err(ApiError::CustomAuthentication( "You do not have permission to remove a member from this organization" .to_string(), )); } - } else if Some(delete_member.user_id) == member.map(|m| m.user_id) - || organization_permissions.contains(OrganizationPermissions::MANAGE_INVITES) + } else if Some(delete_member.user_id) + == member.map(|m| m.user_id) + || organization_permissions + .contains(OrganizationPermissions::MANAGE_INVITES) { // This is a pending invite rather than a member, so the // user being invited or team members with the MANAGE_INVITES diff --git a/apps/labrinth/src/routes/v3/threads.rs b/apps/labrinth/src/routes/v3/threads.rs index 7558fd5c2..ef500e94e 100644 --- a/apps/labrinth/src/routes/v3/threads.rs +++ b/apps/labrinth/src/routes/v3/threads.rs @@ -27,7 +27,9 @@ pub fn config(cfg: &mut web::ServiceConfig) { .route("{id}", web::get().to(thread_get)) .route("{id}", web::post().to(thread_send_message)), ); - cfg.service(web::scope("message").route("{id}", web::delete().to(message_delete))); + cfg.service( + web::scope("message").route("{id}", web::delete().to(message_delete)), + ); cfg.route("threads", web::get().to(threads_get)); } @@ -104,7 +106,8 @@ pub async fn filter_authorized_threads( for thread in threads { if user.role.is_mod() - || (thread.type_ == ThreadType::DirectMessage && thread.members.contains(&user_id)) + || (thread.type_ == ThreadType::DirectMessage + && thread.members.contains(&user_id)) { return_threads.push(thread); } else { @@ -226,11 +229,12 @@ pub async fn filter_authorized_threads( .collect::>(), ); - let users: Vec = database::models::User::get_many_ids(&user_ids, &***pool, redis) - .await? - .into_iter() - .map(From::from) - .collect(); + let users: Vec = + database::models::User::get_many_ids(&user_ids, &***pool, redis) + .await? + .into_iter() + .map(From::from) + .collect(); let mut final_threads = Vec::new(); @@ -304,13 +308,16 @@ pub async fn thread_get( .collect::>(), ); - let users: Vec = database::models::User::get_many_ids(authors, &**pool, &redis) - .await? - .into_iter() - .map(From::from) - .collect(); + let users: Vec = + database::models::User::get_many_ids(authors, &**pool, &redis) + .await? + .into_iter() + .map(From::from) + .collect(); - return Ok(HttpResponse::Ok().json(Thread::from(data, users, &user))); + return Ok( + HttpResponse::Ok().json(Thread::from(data, users, &user)) + ); } } Err(ApiError::NotFound) @@ -344,9 +351,11 @@ pub async fn threads_get( .map(|x| x.into()) .collect(); - let threads_data = database::models::Thread::get_many(&thread_ids, &**pool).await?; + let threads_data = + database::models::Thread::get_many(&thread_ids, &**pool).await?; - let threads = filter_authorized_threads(threads_data, &user, &pool, &redis).await?; + let threads = + filter_authorized_threads(threads_data, &user, &pool, &redis).await?; Ok(HttpResponse::Ok().json(threads)) } @@ -396,13 +405,17 @@ pub async fn thread_send_message( } if let Some(replying_to) = replying_to { - let thread_message = - database::models::ThreadMessage::get((*replying_to).into(), &**pool).await?; + let thread_message = database::models::ThreadMessage::get( + (*replying_to).into(), + &**pool, + ) + .await?; if let Some(thread_message) = thread_message { if thread_message.thread_id != string { return Err(ApiError::InvalidInput( - "Message replied to is from another thread!".to_string(), + "Message replied to is from another thread!" + .to_string(), )); } } else { @@ -436,16 +449,21 @@ pub async fn thread_send_message( .await?; if let Some(project_id) = thread.project_id { - let project = database::models::Project::get_id(project_id, &**pool, &redis).await?; + let project = + database::models::Project::get_id(project_id, &**pool, &redis) + .await?; if let Some(project) = project { - if project.inner.status != ProjectStatus::Processing && user.role.is_mod() { - let members = database::models::TeamMember::get_from_team_full( - project.inner.team_id, - &**pool, - &redis, - ) - .await?; + if project.inner.status != ProjectStatus::Processing + && user.role.is_mod() + { + let members = + database::models::TeamMember::get_from_team_full( + project.inner.team_id, + &**pool, + &redis, + ) + .await?; NotificationBuilder { body: NotificationBody::ModeratorMessage { @@ -464,7 +482,9 @@ pub async fn thread_send_message( } } } else if let Some(report_id) = thread.report_id { - let report = database::models::report_item::Report::get(report_id, &**pool).await?; + let report = + database::models::report_item::Report::get(report_id, &**pool) + .await?; if let Some(report) = report { if report.closed && !user.role.is_mod() { @@ -493,12 +513,18 @@ pub async fn thread_send_message( } = &new_message.body { for image_id in associated_images { - if let Some(db_image) = - image_item::Image::get((*image_id).into(), &mut *transaction, &redis).await? + if let Some(db_image) = image_item::Image::get( + (*image_id).into(), + &mut *transaction, + &redis, + ) + .await? { let image: Image = db_image.into(); - if !matches!(image.context, ImageContext::ThreadMessage { .. }) - || image.context.inner_id().is_some() + if !matches!( + image.context, + ImageContext::ThreadMessage { .. } + ) || image.context.inner_id().is_some() { return Err(ApiError::InvalidInput(format!( "Image {} is not unused and in the 'thread_message' context", @@ -518,7 +544,8 @@ pub async fn thread_send_message( .execute(&mut *transaction) .await?; - image_item::Image::clear_cache(image.id.into(), &redis).await?; + image_item::Image::clear_cache(image.id.into(), &redis) + .await?; } else { return Err(ApiError::InvalidInput(format!( "Image {} does not exist", @@ -554,7 +581,11 @@ pub async fn message_delete( .await? .1; - let result = database::models::ThreadMessage::get(info.into_inner().0.into(), &**pool).await?; + let result = database::models::ThreadMessage::get( + info.into_inner().0.into(), + &**pool, + ) + .await?; if let Some(thread) = result { if !user.role.is_mod() && thread.author_id != Some(user.id.into()) { @@ -568,7 +599,9 @@ pub async fn message_delete( let context = ImageContext::ThreadMessage { thread_message_id: Some(thread.id.into()), }; - let images = database::Image::get_many_contexted(context, &mut transaction).await?; + let images = + database::Image::get_many_contexted(context, &mut transaction) + .await?; let cdn_url = dotenvy::var("CDN_URL")?; for image in images { let name = image.url.split(&format!("{cdn_url}/")).nth(1); @@ -586,7 +619,12 @@ pub async fn message_delete( false }; - database::models::ThreadMessage::remove_full(thread.id, private, &mut transaction).await?; + database::models::ThreadMessage::remove_full( + thread.id, + private, + &mut transaction, + ) + .await?; transaction.commit().await?; Ok(HttpResponse::NoContent().body("")) diff --git a/apps/labrinth/src/routes/v3/users.rs b/apps/labrinth/src/routes/v3/users.rs index 6eba46d3f..dd7d3052b 100644 --- a/apps/labrinth/src/routes/v3/users.rs +++ b/apps/labrinth/src/routes/v3/users.rs @@ -67,9 +67,14 @@ pub async fn projects_list( if let Some(id) = id_option.map(|x| x.id) { let project_data = User::get_projects(id, &**pool, &redis).await?; - let projects: Vec<_> = - crate::database::Project::get_many_ids(&project_data, &**pool, &redis).await?; - let projects = filter_visible_projects(projects, &user, &pool, true).await?; + let projects: Vec<_> = crate::database::Project::get_many_ids( + &project_data, + &**pool, + &redis, + ) + .await?; + let projects = + filter_visible_projects(projects, &user, &pool, true).await?; Ok(HttpResponse::Ok().json(projects)) } else { Err(ApiError::NotFound) @@ -116,7 +121,8 @@ pub async fn users_get( let users_data = User::get_many(&user_ids, &**pool, &redis).await?; - let users: Vec = users_data.into_iter().map(From::from).collect(); + let users: Vec = + users_data.into_iter().map(From::from).collect(); Ok(HttpResponse::Ok().json(users)) } @@ -165,13 +171,18 @@ pub async fn collections_list( let project_data = User::get_collections(id, &**pool).await?; - let response: Vec<_> = - crate::database::models::Collection::get_many(&project_data, &**pool, &redis) - .await? - .into_iter() - .filter(|x| can_view_private || matches!(x.status, CollectionStatus::Listed)) - .map(Collection::from) - .collect(); + let response: Vec<_> = crate::database::models::Collection::get_many( + &project_data, + &**pool, + &redis, + ) + .await? + .into_iter() + .filter(|x| { + can_view_private || matches!(x.status, CollectionStatus::Listed) + }) + .map(Collection::from) + .collect(); Ok(HttpResponse::Ok().json(response)) } else { @@ -213,10 +224,11 @@ pub async fn orgs_list( .map(|x| x.team_id) .collect::>(); - let teams_data = crate::database::models::TeamMember::get_from_team_full_many( - &team_ids, &**pool, &redis, - ) - .await?; + let teams_data = + crate::database::models::TeamMember::get_from_team_full_many( + &team_ids, &**pool, &redis, + ) + .await?; let users = User::get_many_ids( &teams_data.iter().map(|x| x.user_id).collect::>(), &**pool, @@ -231,7 +243,8 @@ pub async fn orgs_list( } for data in organizations_data { - let members_data = team_groups.remove(&data.team_id).unwrap_or(vec![]); + let members_data = + team_groups.remove(&data.team_id).unwrap_or(vec![]); let logged_in = user .as_ref() .and_then(|user| { @@ -246,12 +259,19 @@ pub async fn orgs_list( .filter(|x| logged_in || x.accepted || id == x.user_id) .flat_map(|data| { users.iter().find(|x| x.id == data.user_id).map(|user| { - crate::models::teams::TeamMember::from(data, user.clone(), !logged_in) + crate::models::teams::TeamMember::from( + data, + user.clone(), + !logged_in, + ) }) }) .collect(); - let organization = crate::models::organizations::Organization::from(data, team_members); + let organization = crate::models::organizations::Organization::from( + data, + team_members, + ); organizations.push(organization); } @@ -299,9 +319,9 @@ pub async fn user_edit( ) .await?; - new_user - .validate() - .map_err(|err| ApiError::Validation(validation_errors_to_string(err, None)))?; + new_user.validate().map_err(|err| { + ApiError::Validation(validation_errors_to_string(err, None)) + })?; let id_option = User::get(&info.into_inner().0, &**pool, &redis).await?; @@ -313,7 +333,8 @@ pub async fn user_edit( let mut transaction = pool.begin().await?; if let Some(username) = &new_user.username { - let existing_user_id_option = User::get(username, &**pool, &redis).await?; + let existing_user_id_option = + User::get(username, &**pool, &redis).await?; if existing_user_id_option .map(|x| UserId::from(x.id)) @@ -418,7 +439,8 @@ pub async fn user_edit( } transaction.commit().await?; - User::clear_caches(&[(id, Some(actual_user.username))], &redis).await?; + User::clear_caches(&[(id, Some(actual_user.username))], &redis) + .await?; Ok(HttpResponse::NoContent().body("")) } else { Err(ApiError::CustomAuthentication( @@ -460,7 +482,8 @@ pub async fn user_icon_edit( if let Some(actual_user) = id_option { if user.id != actual_user.id.into() && !user.role.is_mod() { return Err(ApiError::CustomAuthentication( - "You don't have permission to edit this user's icon.".to_string(), + "You don't have permission to edit this user's icon." + .to_string(), )); } @@ -471,8 +494,12 @@ pub async fn user_icon_edit( ) .await?; - let bytes = - read_from_payload(&mut payload, 262144, "Icons must be smaller than 256KiB").await?; + let bytes = read_from_payload( + &mut payload, + 262144, + "Icons must be smaller than 256KiB", + ) + .await?; let user_id: UserId = actual_user.id.into(); let upload_result = crate::util::img::upload_image_optimized( @@ -572,12 +599,15 @@ pub async fn user_follows( } let project_ids = User::get_follows(id, &**pool).await?; - let projects: Vec<_> = - crate::database::Project::get_many_ids(&project_ids, &**pool, &redis) - .await? - .into_iter() - .map(Project::from) - .collect(); + let projects: Vec<_> = crate::database::Project::get_many_ids( + &project_ids, + &**pool, + &redis, + ) + .await? + .into_iter() + .map(Project::from) + .collect(); Ok(HttpResponse::Ok().json(projects)) } else { diff --git a/apps/labrinth/src/routes/v3/version_creation.rs b/apps/labrinth/src/routes/v3/version_creation.rs index dd1c73680..8d531c22b 100644 --- a/apps/labrinth/src/routes/v3/version_creation.rs +++ b/apps/labrinth/src/routes/v3/version_creation.rs @@ -1,6 +1,8 @@ use super::project_creation::{CreateError, UploadedFile}; use crate::auth::get_user_from_headers; -use crate::database::models::loader_fields::{LoaderField, LoaderFieldEnumValue, VersionField}; +use crate::database::models::loader_fields::{ + LoaderField, LoaderFieldEnumValue, VersionField, +}; use crate::database::models::notification_item::NotificationBuilder; use crate::database::models::version_item::{ DependencyBuilder, VersionBuilder, VersionFileBuilder, @@ -14,8 +16,8 @@ use crate::models::pack::PackFileHash; use crate::models::pats::Scopes; use crate::models::projects::{skip_nulls, DependencyType, ProjectStatus}; use crate::models::projects::{ - Dependency, FileType, Loader, ProjectId, Version, VersionFile, VersionId, VersionStatus, - VersionType, + Dependency, FileType, Loader, ProjectId, Version, VersionFile, VersionId, + VersionStatus, VersionType, }; use crate::models::teams::ProjectPermissions; use crate::queue::moderation::AutomatedModerationQueue; @@ -122,8 +124,11 @@ pub async fn version_create( .await; if result.is_err() { - let undo_result = - super::project_creation::undo_uploads(&***file_host, &uploaded_files).await; + let undo_result = super::project_creation::undo_uploads( + &***file_host, + &uploaded_files, + ) + .await; let rollback_result = transaction.rollback().await; undo_result?; @@ -374,10 +379,12 @@ async fn version_create_inner( return Err(error); } - let version_data = initial_version_data - .ok_or_else(|| CreateError::InvalidInput("`data` field is required".to_string()))?; - let builder = version_builder - .ok_or_else(|| CreateError::InvalidInput("`data` field is required".to_string()))?; + let version_data = initial_version_data.ok_or_else(|| { + CreateError::InvalidInput("`data` field is required".to_string()) + })?; + let builder = version_builder.ok_or_else(|| { + CreateError::InvalidInput("`data` field is required".to_string()) + })?; if builder.files.is_empty() { return Err(CreateError::InvalidInput( @@ -470,7 +477,8 @@ async fn version_create_inner( for image_id in version_data.uploaded_images { if let Some(db_image) = - image_item::Image::get(image_id.into(), &mut **transaction, redis).await? + image_item::Image::get(image_id.into(), &mut **transaction, redis) + .await? { let image: Image = db_image.into(); if !matches!(image.context, ImageContext::Report { .. }) @@ -549,8 +557,11 @@ pub async fn upload_file_to_version( .await; if result.is_err() { - let undo_result = - super::project_creation::undo_uploads(&***file_host, &uploaded_files).await; + let undo_result = super::project_creation::undo_uploads( + &***file_host, + &uploaded_files, + ) + .await; let rollback_result = transaction.rollback().await; undo_result?; @@ -602,7 +613,8 @@ async fn upload_file_to_version_inner( } }; - let all_loaders = models::loader_fields::Loader::list(&mut **transaction, &redis).await?; + let all_loaders = + models::loader_fields::Loader::list(&mut **transaction, &redis).await?; let selected_loaders = version .loaders .iter() @@ -615,9 +627,13 @@ async fn upload_file_to_version_inner( }) .collect::, _>>()?; - if models::Project::get_id(version.inner.project_id, &mut **transaction, &redis) - .await? - .is_none() + if models::Project::get_id( + version.inner.project_id, + &mut **transaction, + &redis, + ) + .await? + .is_none() { return Err(CreateError::InvalidInput( "An invalid project id was supplied".to_string(), @@ -633,13 +649,15 @@ async fn upload_file_to_version_inner( ) .await?; - let organization = Organization::get_associated_organization_project_id( - version.inner.project_id, - &**client, - ) - .await?; + let organization = + Organization::get_associated_organization_project_id( + version.inner.project_id, + &**client, + ) + .await?; - let organization_team_member = if let Some(organization) = &organization { + let organization_team_member = if let Some(organization) = &organization + { models::TeamMember::get_from_user_id( organization.team_id, user.id.into(), @@ -659,7 +677,8 @@ async fn upload_file_to_version_inner( if !permissions.contains(ProjectPermissions::UPLOAD_VERSION) { return Err(CreateError::CustomAuthenticationError( - "You don't have permission to upload files to this version!".to_string(), + "You don't have permission to upload files to this version!" + .to_string(), )); } } @@ -676,7 +695,9 @@ async fn upload_file_to_version_inner( let result = async { let content_disposition = field.content_disposition().clone(); let name = content_disposition.get_name().ok_or_else(|| { - CreateError::MissingValueError("Missing content name".to_string()) + CreateError::MissingValueError( + "Missing content name".to_string(), + ) })?; if name == "data" { @@ -691,7 +712,9 @@ async fn upload_file_to_version_inner( } let file_data = initial_file_data.as_ref().ok_or_else(|| { - CreateError::InvalidInput(String::from("`data` field must come before file fields")) + CreateError::InvalidInput(String::from( + "`data` field must come before file fields", + )) })?; let loaders = selected_loaders @@ -788,7 +811,8 @@ pub async fn upload_file( if other_file_names.contains(&format!("{}.{}", file_name, file_extension)) { return Err(CreateError::InvalidInput( - "Duplicate files are not allowed to be uploaded to Modrinth!".to_string(), + "Duplicate files are not allowed to be uploaded to Modrinth!" + .to_string(), )); } @@ -799,7 +823,9 @@ pub async fn upload_file( } let content_type = crate::util::ext::project_file_type(file_extension) - .ok_or_else(|| CreateError::InvalidFileType(file_extension.to_string()))?; + .ok_or_else(|| { + CreateError::InvalidFileType(file_extension.to_string()) + })?; let data = read_from_field( field, 500 * (1 << 20), @@ -825,7 +851,8 @@ pub async fn upload_file( if exists { return Err(CreateError::InvalidInput( - "Duplicate files are not allowed to be uploaded to Modrinth!".to_string(), + "Duplicate files are not allowed to be uploaded to Modrinth!" + .to_string(), )); } @@ -867,7 +894,11 @@ pub async fn upload_file( for file in &format.files { if let Some(dep) = res.iter().find(|x| { - Some(&*x.hash) == file.hashes.get(&PackFileHash::Sha1).map(|x| x.as_bytes()) + Some(&*x.hash) + == file + .hashes + .get(&PackFileHash::Sha1) + .map(|x| x.as_bytes()) }) { dependencies.push(DependencyBuilder { project_id: Some(models::ProjectId(dep.project_id)), @@ -917,7 +948,8 @@ pub async fn upload_file( version_id, urlencoding::encode(file_name) ); - let file_path = format!("data/{}/versions/{}/{}", project_id, version_id, &file_name); + let file_path = + format!("data/{}/versions/{}/{}", project_id, version_id, &file_name); let upload_data = file_host .upload_file(content_type, &file_path, data) @@ -937,7 +969,8 @@ pub async fn upload_file( .any(|y| y.hash == sha1_bytes || y.hash == sha512_bytes) }) { return Err(CreateError::InvalidInput( - "Duplicate files are not allowed to be uploaded to Modrinth!".to_string(), + "Duplicate files are not allowed to be uploaded to Modrinth!" + .to_string(), )); } @@ -975,9 +1008,9 @@ pub async fn upload_file( pub fn get_name_ext( content_disposition: &actix_web::http::header::ContentDisposition, ) -> Result<(&str, &str), CreateError> { - let file_name = content_disposition - .get_filename() - .ok_or_else(|| CreateError::MissingValueError("Missing content file name".to_string()))?; + let file_name = content_disposition.get_filename().ok_or_else(|| { + CreateError::MissingValueError("Missing content file name".to_string()) + })?; let file_extension = if let Some(last_period) = file_name.rfind('.') { file_name.get((last_period + 1)..).unwrap_or("") } else { @@ -994,7 +1027,10 @@ pub fn try_create_version_fields( version_id: VersionId, submitted_fields: &HashMap, loader_fields: &[LoaderField], - loader_field_enum_values: &mut HashMap>, + loader_field_enum_values: &mut HashMap< + models::LoaderFieldId, + Vec, + >, ) -> Result, CreateError> { let mut version_fields = vec![]; let mut remaining_mandatory_loader_fields = loader_fields diff --git a/apps/labrinth/src/routes/v3/version_file.rs b/apps/labrinth/src/routes/v3/version_file.rs index 01e9cddb0..e34d8ef53 100644 --- a/apps/labrinth/src/routes/v3/version_file.rs +++ b/apps/labrinth/src/routes/v3/version_file.rs @@ -65,13 +65,18 @@ pub async fn get_version_from_hash( ) .await?; if let Some(file) = file { - let version = database::models::Version::get(file.version_id, &**pool, &redis).await?; + let version = + database::models::Version::get(file.version_id, &**pool, &redis) + .await?; if let Some(version) = version { - if !is_visible_version(&version.inner, &user_option, &pool, &redis).await? { + if !is_visible_version(&version.inner, &user_option, &pool, &redis) + .await? + { return Err(ApiError::NotFound); } - Ok(HttpResponse::Ok().json(models::projects::Version::from(version))) + Ok(HttpResponse::Ok() + .json(models::projects::Version::from(version))) } else { Err(ApiError::NotFound) } @@ -147,42 +152,59 @@ pub async fn get_update_from_hash( .await? { if let Some(project) = - database::models::Project::get_id(file.project_id, &**pool, &redis).await? - { - let versions = database::models::Version::get_many(&project.versions, &**pool, &redis) + database::models::Project::get_id(file.project_id, &**pool, &redis) .await? - .into_iter() - .filter(|x| { - let mut bool = true; - if let Some(version_types) = &update_data.version_types { - bool &= version_types + { + let versions = database::models::Version::get_many( + &project.versions, + &**pool, + &redis, + ) + .await? + .into_iter() + .filter(|x| { + let mut bool = true; + if let Some(version_types) = &update_data.version_types { + bool &= version_types + .iter() + .any(|y| y.as_str() == x.inner.version_type); + } + if let Some(loaders) = &update_data.loaders { + bool &= x.loaders.iter().any(|y| loaders.contains(y)); + } + if let Some(loader_fields) = &update_data.loader_fields { + for (key, values) in loader_fields { + bool &= if let Some(x_vf) = x + .version_fields .iter() - .any(|y| y.as_str() == x.inner.version_type); - } - if let Some(loaders) = &update_data.loaders { - bool &= x.loaders.iter().any(|y| loaders.contains(y)); - } - if let Some(loader_fields) = &update_data.loader_fields { - for (key, values) in loader_fields { - bool &= if let Some(x_vf) = - x.version_fields.iter().find(|y| y.field_name == *key) - { - values.iter().any(|v| x_vf.value.contains_json_value(v)) - } else { - true - }; - } + .find(|y| y.field_name == *key) + { + values + .iter() + .any(|v| x_vf.value.contains_json_value(v)) + } else { + true + }; } - bool - }) - .sorted(); + } + bool + }) + .sorted(); if let Some(first) = versions.last() { - if !is_visible_version(&first.inner, &user_option, &pool, &redis).await? { + if !is_visible_version( + &first.inner, + &user_option, + &pool, + &redis, + ) + .await? + { return Err(ApiError::NotFound); } - return Ok(HttpResponse::Ok().json(models::projects::Version::from(first))); + return Ok(HttpResponse::Ok() + .json(models::projects::Version::from(first))); } } } @@ -229,7 +251,8 @@ pub async fn get_versions_from_hashes( let version_ids = files.iter().map(|x| x.version_id).collect::>(); let versions_data = filter_visible_versions( - database::models::Version::get_many(&version_ids, &**pool, &redis).await?, + database::models::Version::get_many(&version_ids, &**pool, &redis) + .await?, &user_option, &pool, &redis, @@ -282,7 +305,8 @@ pub async fn get_projects_from_hashes( let project_ids = files.iter().map(|x| x.project_id).collect::>(); let projects_data = filter_visible_projects( - database::models::Project::get_many_ids(&project_ids, &**pool, &redis).await?, + database::models::Project::get_many_ids(&project_ids, &**pool, &redis) + .await?, &user_option, &pool, false, @@ -456,28 +480,41 @@ pub async fn update_individual_files( for project in projects { for file in files.iter().filter(|x| x.project_id == project.inner.id) { if let Some(hash) = file.hashes.get(&algorithm) { - if let Some(query_file) = update_data.hashes.iter().find(|x| &x.hash == hash) { + if let Some(query_file) = + update_data.hashes.iter().find(|x| &x.hash == hash) + { let version = all_versions .iter() .filter(|x| x.inner.project_id == file.project_id) .filter(|x| { let mut bool = true; - if let Some(version_types) = &query_file.version_types { - bool &= version_types - .iter() - .any(|y| y.as_str() == x.inner.version_type); + if let Some(version_types) = + &query_file.version_types + { + bool &= version_types.iter().any(|y| { + y.as_str() == x.inner.version_type + }); } if let Some(loaders) = &query_file.loaders { - bool &= x.loaders.iter().any(|y| loaders.contains(y)); + bool &= x + .loaders + .iter() + .any(|y| loaders.contains(y)); } - if let Some(loader_fields) = &query_file.loader_fields { + if let Some(loader_fields) = + &query_file.loader_fields + { for (key, values) in loader_fields { - bool &= if let Some(x_vf) = - x.version_fields.iter().find(|y| y.field_name == *key) + bool &= if let Some(x_vf) = x + .version_fields + .iter() + .find(|y| y.field_name == *key) { - values.iter().any(|v| x_vf.value.contains_json_value(v)) + values.iter().any(|v| { + x_vf.value.contains_json_value(v) + }) } else { true }; @@ -489,10 +526,19 @@ pub async fn update_individual_files( .last(); if let Some(version) = version { - if is_visible_version(&version.inner, &user_option, &pool, &redis).await? { + if is_visible_version( + &version.inner, + &user_option, + &pool, + &redis, + ) + .await? + { response.insert( hash.clone(), - models::projects::Version::from(version.clone()), + models::projects::Version::from( + version.clone(), + ), ); } } @@ -539,13 +585,14 @@ pub async fn delete_file( if let Some(row) = file { if !user.role.is_admin() { - let team_member = database::models::TeamMember::get_from_user_id_version( - row.version_id, - user.id.into(), - &**pool, - ) - .await - .map_err(ApiError::Database)?; + let team_member = + database::models::TeamMember::get_from_user_id_version( + row.version_id, + user.id.into(), + &**pool, + ) + .await + .map_err(ApiError::Database)?; let organization = database::models::Organization::get_associated_organization_project_id( @@ -555,18 +602,19 @@ pub async fn delete_file( .await .map_err(ApiError::Database)?; - let organization_team_member = if let Some(organization) = &organization { - database::models::TeamMember::get_from_user_id_organization( - organization.id, - user.id.into(), - false, - &**pool, - ) - .await - .map_err(ApiError::Database)? - } else { - None - }; + let organization_team_member = + if let Some(organization) = &organization { + database::models::TeamMember::get_from_user_id_organization( + organization.id, + user.id.into(), + false, + &**pool, + ) + .await + .map_err(ApiError::Database)? + } else { + None + }; let permissions = ProjectPermissions::get_permissions_by_role( &user.role, @@ -577,16 +625,20 @@ pub async fn delete_file( if !permissions.contains(ProjectPermissions::DELETE_VERSION) { return Err(ApiError::CustomAuthentication( - "You don't have permission to delete this file!".to_string(), + "You don't have permission to delete this file!" + .to_string(), )); } } - let version = database::models::Version::get(row.version_id, &**pool, &redis).await?; + let version = + database::models::Version::get(row.version_id, &**pool, &redis) + .await?; if let Some(version) = version { if version.files.len() < 2 { return Err(ApiError::InvalidInput( - "Versions must have at least one file uploaded to them".to_string(), + "Versions must have at least one file uploaded to them" + .to_string(), )); } @@ -663,10 +715,14 @@ pub async fn download_version( .await?; if let Some(file) = file { - let version = database::models::Version::get(file.version_id, &**pool, &redis).await?; + let version = + database::models::Version::get(file.version_id, &**pool, &redis) + .await?; if let Some(version) = version { - if !is_visible_version(&version.inner, &user_option, &pool, &redis).await? { + if !is_visible_version(&version.inner, &user_option, &pool, &redis) + .await? + { return Err(ApiError::NotFound); } diff --git a/apps/labrinth/src/routes/v3/versions.rs b/apps/labrinth/src/routes/v3/versions.rs index 91237b814..ac27a075c 100644 --- a/apps/labrinth/src/routes/v3/versions.rs +++ b/apps/labrinth/src/routes/v3/versions.rs @@ -1,7 +1,9 @@ use std::collections::HashMap; use super::ApiError; -use crate::auth::checks::{filter_visible_versions, is_visible_project, is_visible_version}; +use crate::auth::checks::{ + filter_visible_versions, is_visible_project, is_visible_version, +}; use crate::auth::get_user_from_headers; use crate::database; use crate::database::models::loader_fields::{ @@ -16,7 +18,9 @@ use crate::models::ids::VersionId; use crate::models::images::ImageContext; use crate::models::pats::Scopes; use crate::models::projects::{skip_nulls, Loader}; -use crate::models::projects::{Dependency, FileType, VersionStatus, VersionType}; +use crate::models::projects::{ + Dependency, FileType, VersionStatus, VersionType, +}; use crate::models::teams::ProjectPermissions; use crate::queue::session::AuthQueue; use crate::search::indexing::remove_documents; @@ -80,21 +84,31 @@ pub async fn version_project_get_helper( .ok(); if let Some(project) = result { - if !is_visible_project(&project.inner, &user_option, &pool, false).await? { + if !is_visible_project(&project.inner, &user_option, &pool, false) + .await? + { return Err(ApiError::NotFound); } - let versions = - database::models::Version::get_many(&project.versions, &**pool, &redis).await?; + let versions = database::models::Version::get_many( + &project.versions, + &**pool, + &redis, + ) + .await?; let id_opt = parse_base62(&id.1).ok(); - let version = versions - .into_iter() - .find(|x| Some(x.inner.id.0 as u64) == id_opt || x.inner.version_number == id.1); + let version = versions.into_iter().find(|x| { + Some(x.inner.id.0 as u64) == id_opt + || x.inner.version_number == id.1 + }); if let Some(version) = version { - if is_visible_version(&version.inner, &user_option, &pool, &redis).await? { - return Ok(HttpResponse::Ok().json(models::projects::Version::from(version))); + if is_visible_version(&version.inner, &user_option, &pool, &redis) + .await? + { + return Ok(HttpResponse::Ok() + .json(models::projects::Version::from(version))); } } } @@ -114,11 +128,14 @@ pub async fn versions_get( redis: web::Data, session_queue: web::Data, ) -> Result { - let version_ids = serde_json::from_str::>(&ids.ids)? - .into_iter() - .map(|x| x.into()) - .collect::>(); - let versions_data = database::models::Version::get_many(&version_ids, &**pool, &redis).await?; + let version_ids = + serde_json::from_str::>(&ids.ids)? + .into_iter() + .map(|x| x.into()) + .collect::>(); + let versions_data = + database::models::Version::get_many(&version_ids, &**pool, &redis) + .await?; let user_option = get_user_from_headers( &req, @@ -131,7 +148,9 @@ pub async fn versions_get( .map(|x| x.1) .ok(); - let versions = filter_visible_versions(versions_data, &user_option, &pool, &redis).await?; + let versions = + filter_visible_versions(versions_data, &user_option, &pool, &redis) + .await?; Ok(HttpResponse::Ok().json(versions)) } @@ -154,7 +173,8 @@ pub async fn version_get_helper( redis: web::Data, session_queue: web::Data, ) -> Result { - let version_data = database::models::Version::get(id.into(), &**pool, &redis).await?; + let version_data = + database::models::Version::get(id.into(), &**pool, &redis).await?; let user_option = get_user_from_headers( &req, @@ -169,7 +189,9 @@ pub async fn version_get_helper( if let Some(data) = version_data { if is_visible_version(&data.inner, &user_option, &pool, &redis).await? { - return Ok(HttpResponse::Ok().json(models::projects::Version::from(data))); + return Ok( + HttpResponse::Ok().json(models::projects::Version::from(data)) + ); } } @@ -231,7 +253,8 @@ pub async fn version_edit( new_version: web::Json, session_queue: web::Data, ) -> Result { - let new_version: EditVersion = serde_json::from_value(new_version.into_inner())?; + let new_version: EditVersion = + serde_json::from_value(new_version.into_inner())?; version_edit_helper( req, info.into_inner(), @@ -260,9 +283,9 @@ pub async fn version_edit_helper( .await? .1; - new_version - .validate() - .map_err(|err| ApiError::Validation(validation_errors_to_string(err, None)))?; + new_version.validate().map_err(|err| { + ApiError::Validation(validation_errors_to_string(err, None)) + })?; let version_id = info.0; let id = version_id.into(); @@ -270,21 +293,24 @@ pub async fn version_edit_helper( let result = database::models::Version::get(id, &**pool, &redis).await?; if let Some(version_item) = result { - let team_member = database::models::TeamMember::get_from_user_id_project( - version_item.inner.project_id, - user.id.into(), - false, - &**pool, - ) - .await?; + let team_member = + database::models::TeamMember::get_from_user_id_project( + version_item.inner.project_id, + user.id.into(), + false, + &**pool, + ) + .await?; - let organization = Organization::get_associated_organization_project_id( - version_item.inner.project_id, - &**pool, - ) - .await?; + let organization = + Organization::get_associated_organization_project_id( + version_item.inner.project_id, + &**pool, + ) + .await?; - let organization_team_member = if let Some(organization) = &organization { + let organization_team_member = if let Some(organization) = &organization + { database::models::TeamMember::get_from_user_id( organization.team_id, user.id.into(), @@ -304,7 +330,8 @@ pub async fn version_edit_helper( if let Some(perms) = permissions { if !perms.contains(ProjectPermissions::UPLOAD_VERSION) { return Err(ApiError::CustomAuthentication( - "You do not have the permissions to edit this version!".to_string(), + "You do not have the permissions to edit this version!" + .to_string(), )); } @@ -372,8 +399,12 @@ pub async fn version_edit_helper( }) .collect::>(); - DependencyBuilder::insert_many(builders, version_item.inner.id, &mut transaction) - .await?; + DependencyBuilder::insert_many( + builders, + version_item.inner.id, + &mut transaction, + ) + .await?; } if !new_version.fields.is_empty() { @@ -383,20 +414,34 @@ pub async fn version_edit_helper( .map(|x| x.to_string()) .collect::>(); - let all_loaders = loader_fields::Loader::list(&mut *transaction, &redis).await?; + let all_loaders = + loader_fields::Loader::list(&mut *transaction, &redis) + .await?; let loader_ids = version_item .loaders .iter() - .filter_map(|x| all_loaders.iter().find(|y| &y.loader == x).map(|y| y.id)) + .filter_map(|x| { + all_loaders + .iter() + .find(|y| &y.loader == x) + .map(|y| y.id) + }) .collect_vec(); - let loader_fields = LoaderField::get_fields(&loader_ids, &mut *transaction, &redis) - .await? - .into_iter() - .filter(|lf| version_fields_names.contains(&lf.field)) - .collect::>(); + let loader_fields = LoaderField::get_fields( + &loader_ids, + &mut *transaction, + &redis, + ) + .await? + .into_iter() + .filter(|lf| version_fields_names.contains(&lf.field)) + .collect::>(); - let loader_field_ids = loader_fields.iter().map(|lf| lf.id.0).collect::>(); + let loader_field_ids = loader_fields + .iter() + .map(|lf| lf.id.0) + .collect::>(); sqlx::query!( " DELETE FROM version_fields @@ -409,12 +454,13 @@ pub async fn version_edit_helper( .execute(&mut *transaction) .await?; - let mut loader_field_enum_values = LoaderFieldEnumValue::list_many_loader_fields( - &loader_fields, - &mut *transaction, - &redis, - ) - .await?; + let mut loader_field_enum_values = + LoaderFieldEnumValue::list_many_loader_fields( + &loader_fields, + &mut *transaction, + &redis, + ) + .await?; let mut version_fields = Vec::new(); for (vf_name, vf_value) in new_version.fields { @@ -438,7 +484,8 @@ pub async fn version_edit_helper( .map_err(ApiError::InvalidInput)?; version_fields.push(vf); } - VersionField::insert_many(version_fields, &mut transaction).await?; + VersionField::insert_many(version_fields, &mut transaction) + .await?; } if let Some(loaders) = &new_version.loaders { @@ -453,18 +500,23 @@ pub async fn version_edit_helper( let mut loader_versions = Vec::new(); for loader in loaders { - let loader_id = database::models::loader_fields::Loader::get_id( - &loader.0, - &mut *transaction, - &redis, - ) - .await? - .ok_or_else(|| { - ApiError::InvalidInput("No database entry for loader provided.".to_string()) - })?; + let loader_id = + database::models::loader_fields::Loader::get_id( + &loader.0, + &mut *transaction, + &redis, + ) + .await? + .ok_or_else(|| { + ApiError::InvalidInput( + "No database entry for loader provided." + .to_string(), + ) + })?; loader_versions.push(LoaderVersion::new(loader_id, id)); } - LoaderVersion::insert_many(loader_versions, &mut transaction).await?; + LoaderVersion::insert_many(loader_versions, &mut transaction) + .await?; crate::database::models::Project::clear_cache( version_item.inner.project_id, @@ -531,7 +583,8 @@ pub async fn version_edit_helper( WHERE (id = $2) ", diff as i32, - version_item.inner.project_id as database::models::ids::ProjectId, + version_item.inner.project_id + as database::models::ids::ProjectId, ) .execute(&mut *transaction) .await?; @@ -614,10 +667,17 @@ pub async fn version_edit_helper( version_id: Some(version_item.inner.id.into()), }; - img::delete_unused_images(context, checkable_strings, &mut transaction, &redis).await?; + img::delete_unused_images( + context, + checkable_strings, + &mut transaction, + &redis, + ) + .await?; transaction.commit().await?; - database::models::Version::clear_cache(&version_item, &redis).await?; + database::models::Version::clear_cache(&version_item, &redis) + .await?; database::models::Project::clear_cache( version_item.inner.project_id, None, @@ -662,7 +722,8 @@ pub async fn version_list( ) -> Result { let string = info.into_inner().0; - let result = database::models::Project::get(&string, &**pool, &redis).await?; + let result = + database::models::Project::get(&string, &**pool, &redis).await?; let user_option = get_user_from_headers( &req, @@ -676,45 +737,51 @@ pub async fn version_list( .ok(); if let Some(project) = result { - if !is_visible_project(&project.inner, &user_option, &pool, false).await? { + if !is_visible_project(&project.inner, &user_option, &pool, false) + .await? + { return Err(ApiError::NotFound); } let loader_field_filters = filters.loader_fields.as_ref().map(|x| { - serde_json::from_str::>>(x).unwrap_or_default() + serde_json::from_str::>>(x) + .unwrap_or_default() }); - let loader_filters = filters - .loaders - .as_ref() - .map(|x| serde_json::from_str::>(x).unwrap_or_default()); - let mut versions = database::models::Version::get_many(&project.versions, &**pool, &redis) - .await? - .into_iter() - .skip(filters.offset.unwrap_or(0)) - .take(filters.limit.unwrap_or(usize::MAX)) - .filter(|x| { - let mut bool = true; + let loader_filters = filters.loaders.as_ref().map(|x| { + serde_json::from_str::>(x).unwrap_or_default() + }); + let mut versions = database::models::Version::get_many( + &project.versions, + &**pool, + &redis, + ) + .await? + .into_iter() + .skip(filters.offset.unwrap_or(0)) + .take(filters.limit.unwrap_or(usize::MAX)) + .filter(|x| { + let mut bool = true; - if let Some(version_type) = filters.version_type { - bool &= &*x.inner.version_type == version_type.as_str(); - } - if let Some(loaders) = &loader_filters { - bool &= x.loaders.iter().any(|y| loaders.contains(y)); - } - if let Some(loader_fields) = &loader_field_filters { - for (key, values) in loader_fields { - bool &= if let Some(x_vf) = - x.version_fields.iter().find(|y| y.field_name == *key) - { - values.iter().any(|v| x_vf.value.contains_json_value(v)) - } else { - true - }; - } + if let Some(version_type) = filters.version_type { + bool &= &*x.inner.version_type == version_type.as_str(); + } + if let Some(loaders) = &loader_filters { + bool &= x.loaders.iter().any(|y| loaders.contains(y)); + } + if let Some(loader_fields) = &loader_field_filters { + for (key, values) in loader_fields { + bool &= if let Some(x_vf) = + x.version_fields.iter().find(|y| y.field_name == *key) + { + values.iter().any(|v| x_vf.value.contains_json_value(v)) + } else { + true + }; } - bool - }) - .collect::>(); + } + bool + }) + .collect::>(); let mut response = versions .iter() @@ -727,10 +794,15 @@ pub async fn version_list( .cloned() .collect::>(); - versions.sort_by(|a, b| b.inner.date_published.cmp(&a.inner.date_published)); + versions.sort_by(|a, b| { + b.inner.date_published.cmp(&a.inner.date_published) + }); // Attempt to populate versions with "auto featured" versions - if response.is_empty() && !versions.is_empty() && filters.featured.unwrap_or(false) { + if response.is_empty() + && !versions.is_empty() + && filters.featured.unwrap_or(false) + { // TODO: This is a bandaid fix for detecting auto-featured versions. // In the future, not all versions will have 'game_versions' fields, so this will need to be changed. let (loaders, game_versions) = futures::future::try_join( @@ -777,10 +849,14 @@ pub async fn version_list( } } - response.sort_by(|a, b| b.inner.date_published.cmp(&a.inner.date_published)); + response.sort_by(|a, b| { + b.inner.date_published.cmp(&a.inner.date_published) + }); response.dedup_by(|a, b| a.inner.id == b.inner.id); - let response = filter_visible_versions(response, &user_option, &pool, &redis).await?; + let response = + filter_visible_versions(response, &user_option, &pool, &redis) + .await?; Ok(HttpResponse::Ok().json(response)) } else { @@ -810,24 +886,31 @@ pub async fn version_delete( let version = database::models::Version::get(id.into(), &**pool, &redis) .await? .ok_or_else(|| { - ApiError::InvalidInput("The specified version does not exist!".to_string()) + ApiError::InvalidInput( + "The specified version does not exist!".to_string(), + ) })?; if !user.role.is_admin() { - let team_member = database::models::TeamMember::get_from_user_id_project( - version.inner.project_id, - user.id.into(), - false, - &**pool, - ) - .await - .map_err(ApiError::Database)?; + let team_member = + database::models::TeamMember::get_from_user_id_project( + version.inner.project_id, + user.id.into(), + false, + &**pool, + ) + .await + .map_err(ApiError::Database)?; let organization = - Organization::get_associated_organization_project_id(version.inner.project_id, &**pool) - .await?; + Organization::get_associated_organization_project_id( + version.inner.project_id, + &**pool, + ) + .await?; - let organization_team_member = if let Some(organization) = &organization { + let organization_team_member = if let Some(organization) = &organization + { database::models::TeamMember::get_from_user_id( organization.team_id, user.id.into(), @@ -846,7 +929,8 @@ pub async fn version_delete( if !permissions.contains(ProjectPermissions::DELETE_VERSION) { return Err(ApiError::CustomAuthentication( - "You do not have permission to delete versions in this team".to_string(), + "You do not have permission to delete versions in this team" + .to_string(), )); } } @@ -856,17 +940,27 @@ pub async fn version_delete( version_id: Some(version.inner.id.into()), }; let uploaded_images = - database::models::Image::get_many_contexted(context, &mut transaction).await?; + database::models::Image::get_many_contexted(context, &mut transaction) + .await?; for image in uploaded_images { image_item::Image::remove(image.id, &mut transaction, &redis).await?; } - let result = - database::models::Version::remove_full(version.inner.id, &redis, &mut transaction).await?; + let result = database::models::Version::remove_full( + version.inner.id, + &redis, + &mut transaction, + ) + .await?; transaction.commit().await?; remove_documents(&[version.inner.id.into()], &search_config).await?; - database::models::Project::clear_cache(version.inner.project_id, None, Some(true), &redis) - .await?; + database::models::Project::clear_cache( + version.inner.project_id, + None, + Some(true), + &redis, + ) + .await?; if result.is_some() { Ok(HttpResponse::NoContent().body("")) diff --git a/apps/labrinth/src/scheduler.rs b/apps/labrinth/src/scheduler.rs index 68cb593bc..7bc5e5195 100644 --- a/apps/labrinth/src/scheduler.rs +++ b/apps/labrinth/src/scheduler.rs @@ -43,8 +43,9 @@ pub fn schedule_versions( pool: sqlx::Pool, redis: RedisPool, ) { - let version_index_interval = - std::time::Duration::from_secs(parse_var("VERSION_INDEX_INTERVAL").unwrap_or(1800)); + let version_index_interval = std::time::Duration::from_secs( + parse_var("VERSION_INDEX_INTERVAL").unwrap_or(1800), + ); scheduler.run(version_index_interval, move || { let pool_ref = pool.clone(); @@ -71,7 +72,9 @@ pub enum VersionIndexingError { } use crate::{ - database::{models::legacy_loader_fields::MinecraftGameVersion, redis::RedisPool}, + database::{ + models::legacy_loader_fields::MinecraftGameVersion, redis::RedisPool, + }, util::env::parse_var, }; use chrono::{DateTime, Utc}; @@ -96,10 +99,12 @@ async fn update_versions( pool: &sqlx::Pool, redis: &RedisPool, ) -> Result<(), VersionIndexingError> { - let input = reqwest::get("https://piston-meta.mojang.com/mc/game/version_manifest_v2.json") - .await? - .json::() - .await?; + let input = reqwest::get( + "https://piston-meta.mojang.com/mc/game/version_manifest_v2.json", + ) + .await? + .json::() + .await?; let mut skipped_versions_count = 0u32; @@ -161,7 +166,8 @@ async fn update_versions( .chars() .all(|c| c.is_ascii_alphanumeric() || "-_.".contains(c)) { - if let Some((_, alternate)) = HALL_OF_SHAME.iter().find(|(version, _)| name == *version) + if let Some((_, alternate)) = + HALL_OF_SHAME.iter().find(|(version, _)| name == *version) { name = String::from(*alternate); } else { diff --git a/apps/labrinth/src/search/indexing/local_import.rs b/apps/labrinth/src/search/indexing/local_import.rs index 2925973ac..f24af8e28 100644 --- a/apps/labrinth/src/search/indexing/local_import.rs +++ b/apps/labrinth/src/search/indexing/local_import.rs @@ -7,10 +7,12 @@ use std::collections::HashMap; use super::IndexingError; use crate::database::models::loader_fields::{ - QueryLoaderField, QueryLoaderFieldEnumValue, QueryVersionField, VersionField, + QueryLoaderField, QueryLoaderFieldEnumValue, QueryVersionField, + VersionField, }; use crate::database::models::{ - LoaderFieldEnumId, LoaderFieldEnumValueId, LoaderFieldId, ProjectId, VersionId, + LoaderFieldEnumId, LoaderFieldEnumValueId, LoaderFieldId, ProjectId, + VersionId, }; use crate::models::projects::from_duplicate_version_fields; use crate::models::v2::projects::LegacyProject; @@ -18,7 +20,9 @@ use crate::routes::v2_reroute; use crate::search::UploadSearchProject; use sqlx::postgres::PgPool; -pub async fn index_local(pool: &PgPool) -> Result, IndexingError> { +pub async fn index_local( + pool: &PgPool, +) -> Result, IndexingError> { info!("Indexing local projects!"); // todo: loaders, project type, game versions @@ -190,24 +194,25 @@ pub async fn index_local(pool: &PgPool) -> Result, Inde info!("Getting all loader field enum values!"); - let loader_field_enum_values: Vec = sqlx::query!( - " + let loader_field_enum_values: Vec = + sqlx::query!( + " SELECT DISTINCT id, enum_id, value, ordering, created, metadata FROM loader_field_enum_values lfev ORDER BY enum_id, ordering, created DESC " - ) - .fetch(pool) - .map_ok(|m| QueryLoaderFieldEnumValue { - id: LoaderFieldEnumValueId(m.id), - enum_id: LoaderFieldEnumId(m.enum_id), - value: m.value, - ordering: m.ordering, - created: m.created, - metadata: m.metadata, - }) - .try_collect() - .await?; + ) + .fetch(pool) + .map_ok(|m| QueryLoaderFieldEnumValue { + id: LoaderFieldEnumValueId(m.id), + enum_id: LoaderFieldEnumId(m.enum_id), + value: m.value, + ordering: m.ordering, + created: m.created, + metadata: m.metadata, + }) + .try_collect() + .await?; info!("Indexing loaders, project types!"); let mut uploads = Vec::new(); @@ -218,17 +223,20 @@ pub async fn index_local(pool: &PgPool) -> Result, Inde count += 1; info!("projects index prog: {count}/{total_len}"); - let owner = if let Some((_, org_owner)) = mods_org_owners.remove(&project.id) { - org_owner - } else if let Some((_, team_owner)) = mods_team_owners.remove(&project.id) { - team_owner - } else { - println!( - "org owner not found for project {} id: {}!", - project.name, project.id.0 - ); - continue; - }; + let owner = + if let Some((_, org_owner)) = mods_org_owners.remove(&project.id) { + org_owner + } else if let Some((_, team_owner)) = + mods_team_owners.remove(&project.id) + { + team_owner + } else { + println!( + "org owner not found for project {} id: {}!", + project.name, project.id.0 + ); + continue; + }; let license = match project.license.split(' ').next() { Some(license) => license.to_string(), @@ -291,7 +299,8 @@ pub async fn index_local(pool: &PgPool) -> Result, Inde &loader_field_enum_values, true, ); - let project_loader_fields = from_duplicate_version_fields(aggregated_version_fields); + let project_loader_fields = + from_duplicate_version_fields(aggregated_version_fields); // aggregated project loaders let project_loaders = versions @@ -308,9 +317,12 @@ pub async fn index_local(pool: &PgPool) -> Result, Inde ); let unvectorized_loader_fields = version_fields .iter() - .map(|vf| (vf.field_name.clone(), vf.value.serialize_internal())) + .map(|vf| { + (vf.field_name.clone(), vf.value.serialize_internal()) + }) .collect(); - let mut loader_fields = from_duplicate_version_fields(version_fields); + let mut loader_fields = + from_duplicate_version_fields(version_fields); let project_types = version.project_types; let mut version_loaders = version.loaders; @@ -347,22 +359,28 @@ pub async fn index_local(pool: &PgPool) -> Result, Inde // client_side and server_side fields from the loader fields into // separate loader fields. // 'client_side' and 'server_side' remain supported by meilisearch even though they are no longer v3 fields. - let (_, v2_og_project_type) = LegacyProject::get_project_type(&project_types); - let (client_side, server_side) = v2_reroute::convert_side_types_v2( - &unvectorized_loader_fields, - Some(&v2_og_project_type), - ); + let (_, v2_og_project_type) = + LegacyProject::get_project_type(&project_types); + let (client_side, server_side) = + v2_reroute::convert_side_types_v2( + &unvectorized_loader_fields, + Some(&v2_og_project_type), + ); if let Ok(client_side) = serde_json::to_value(client_side) { - loader_fields.insert("client_side".to_string(), vec![client_side]); + loader_fields + .insert("client_side".to_string(), vec![client_side]); } if let Ok(server_side) = serde_json::to_value(server_side) { - loader_fields.insert("server_side".to_string(), vec![server_side]); + loader_fields + .insert("server_side".to_string(), vec![server_side]); } let usp = UploadSearchProject { - version_id: crate::models::ids::VersionId::from(version.id).to_string(), - project_id: crate::models::ids::ProjectId::from(project.id).to_string(), + version_id: crate::models::ids::VersionId::from(version.id) + .to_string(), + project_id: crate::models::ids::ProjectId::from(project.id) + .to_string(), name: project.name.clone(), summary: project.summary.clone(), categories: categories.clone(), @@ -470,34 +488,36 @@ async fn index_versions( .await?; // Get version fields - let version_fields: DashMap> = sqlx::query!( - " + let version_fields: DashMap> = + sqlx::query!( + " SELECT version_id, field_id, int_value, enum_value, string_value FROM version_fields WHERE version_id = ANY($1) ", - &all_version_ids, - ) - .fetch(pool) - .try_fold( - DashMap::new(), - |acc: DashMap>, m| { - let qvf = QueryVersionField { - version_id: VersionId(m.version_id), - field_id: LoaderFieldId(m.field_id), - int_value: m.int_value, - enum_value: m.enum_value.map(LoaderFieldEnumValueId), - string_value: m.string_value, - }; + &all_version_ids, + ) + .fetch(pool) + .try_fold( + DashMap::new(), + |acc: DashMap>, m| { + let qvf = QueryVersionField { + version_id: VersionId(m.version_id), + field_id: LoaderFieldId(m.field_id), + int_value: m.int_value, + enum_value: m.enum_value.map(LoaderFieldEnumValueId), + string_value: m.string_value, + }; - acc.entry(VersionId(m.version_id)).or_default().push(qvf); - async move { Ok(acc) } - }, - ) - .await?; + acc.entry(VersionId(m.version_id)).or_default().push(qvf); + async move { Ok(acc) } + }, + ) + .await?; // Convert to partial versions - let mut res_versions: HashMap> = HashMap::new(); + let mut res_versions: HashMap> = + HashMap::new(); for (project_id, version_ids) in versions.iter() { for version_id in version_ids { // Extract version-specific data fetched diff --git a/apps/labrinth/src/search/indexing/mod.rs b/apps/labrinth/src/search/indexing/mod.rs index 35b7f72ce..679473038 100644 --- a/apps/labrinth/src/search/indexing/mod.rs +++ b/apps/labrinth/src/search/indexing/mod.rs @@ -44,7 +44,9 @@ pub async fn remove_documents( for index in indexes { index - .delete_documents(&ids.iter().map(|x| to_base62(x.0)).collect::>()) + .delete_documents( + &ids.iter().map(|x| to_base62(x.0)).collect::>(), + ) .await?; } @@ -70,11 +72,13 @@ pub async fn index_projects( let indices = get_indexes_for_indexing(config, true).await?; let all_loader_fields = - crate::database::models::loader_fields::LoaderField::get_fields_all(&pool, &redis) - .await? - .into_iter() - .map(|x| x.field) - .collect::>(); + crate::database::models::loader_fields::LoaderField::get_fields_all( + &pool, &redis, + ) + .await? + .into_iter() + .map(|x| x.field) + .collect::>(); let uploads = index_local(&pool).await?; add_projects(&indices, uploads, all_loader_fields.clone(), config).await?; @@ -92,7 +96,10 @@ pub async fn index_projects( Ok(()) } -pub async fn swap_index(config: &SearchConfig, index_name: &str) -> Result<(), IndexingError> { +pub async fn swap_index( + config: &SearchConfig, + index_name: &str, +) -> Result<(), IndexingError> { let client = config.make_client(); let index_name_next = config.get_index_name(index_name, true); let index_name = config.get_index_name(index_name, false); @@ -114,7 +121,8 @@ pub async fn get_indexes_for_indexing( ) -> Result, meilisearch_sdk::errors::Error> { let client = config.make_client(); let project_name = config.get_index_name("projects", next); - let project_filtered_name = config.get_index_name("projects_filtered", next); + let project_filtered_name = + config.get_index_name("projects_filtered", next); let projects_index = create_or_update_index( &client, &project_name, @@ -214,7 +222,11 @@ async fn add_to_index( index .add_or_replace(chunk, Some("version_id")) .await? - .wait_for_completion(client, None, Some(std::time::Duration::from_secs(3600))) + .wait_for_completion( + client, + None, + Some(std::time::Duration::from_secs(3600)), + ) .await?; info!("Added chunk of {} projects to index", chunk.len()); } @@ -275,7 +287,8 @@ pub async fn add_projects( ) -> Result<(), IndexingError> { let client = config.make_client(); for index in indices { - update_and_add_to_index(&client, index, &projects, &additional_fields).await?; + update_and_add_to_index(&client, index, &projects, &additional_fields) + .await?; } Ok(()) @@ -342,7 +355,8 @@ const DEFAULT_DISPLAYED_ATTRIBUTES: &[&str] = &[ "project_loader_fields", ]; -const DEFAULT_SEARCHABLE_ATTRIBUTES: &[&str] = &["name", "summary", "author", "slug"]; +const DEFAULT_SEARCHABLE_ATTRIBUTES: &[&str] = + &["name", "summary", "author", "slug"]; const DEFAULT_ATTRIBUTES_FOR_FACETING: &[&str] = &[ "categories", diff --git a/apps/labrinth/src/search/mod.rs b/apps/labrinth/src/search/mod.rs index db397b5fe..244928f25 100644 --- a/apps/labrinth/src/search/mod.rs +++ b/apps/labrinth/src/search/mod.rs @@ -68,8 +68,10 @@ impl SearchConfig { // Panics if the environment variables are not set, // but these are already checked for on startup. pub fn new(meta_namespace: Option) -> Self { - let address = dotenvy::var("MEILISEARCH_ADDR").expect("MEILISEARCH_ADDR not set"); - let key = dotenvy::var("MEILISEARCH_KEY").expect("MEILISEARCH_KEY not set"); + let address = + dotenvy::var("MEILISEARCH_ADDR").expect("MEILISEARCH_ADDR not set"); + let key = + dotenvy::var("MEILISEARCH_KEY").expect("MEILISEARCH_KEY not set"); Self { address, @@ -172,7 +174,8 @@ pub fn get_sort_index( index: &str, ) -> Result<(String, [&'static str; 1]), SearchError> { let projects_name = config.get_index_name("projects", false); - let projects_filtered_name = config.get_index_name("projects_filtered", false); + let projects_filtered_name = + config.get_index_name("projects_filtered", false); Ok(match index { "relevance" => (projects_name, ["downloads:desc"]), "downloads" => (projects_filtered_name, ["downloads:desc"]), @@ -224,12 +227,13 @@ pub async fn search_for_project( None }; - let filters: Cow<_> = match (info.filters.as_deref(), info.version.as_deref()) { - (Some(f), Some(v)) => format!("({f}) AND ({v})").into(), - (Some(f), None) => f.into(), - (None, Some(v)) => v.into(), - (None, None) => "".into(), - }; + let filters: Cow<_> = + match (info.filters.as_deref(), info.version.as_deref()) { + (Some(f), Some(v)) => format!("({f}) AND ({v})").into(), + (Some(f), None) => f.into(), + (None, Some(v)) => v.into(), + (None, None) => "".into(), + }; if let Some(facets) = facets { // Search can now *optionally* have a third inner array: So Vec(AND)>> @@ -242,10 +246,13 @@ pub async fn search_for_project( .into_iter() .map(|facet| { if facet.is_array() { - serde_json::from_value::>(facet).unwrap_or_default() + serde_json::from_value::>(facet) + .unwrap_or_default() } else { - vec![serde_json::from_value::(facet) - .unwrap_or_default()] + vec![serde_json::from_value::( + facet, + ) + .unwrap_or_default()] } }) .collect_vec() @@ -256,12 +263,16 @@ pub async fn search_for_project( for (index, facet_outer_list) in facets.iter().enumerate() { filter_string.push('('); - for (facet_outer_index, facet_inner_list) in facet_outer_list.iter().enumerate() + for (facet_outer_index, facet_inner_list) in + facet_outer_list.iter().enumerate() { filter_string.push('('); - for (facet_inner_index, facet) in facet_inner_list.iter().enumerate() { + for (facet_inner_index, facet) in + facet_inner_list.iter().enumerate() + { filter_string.push_str(&facet.replace(':', " = ")); - if facet_inner_index != (facet_inner_list.len() - 1) { + if facet_inner_index != (facet_inner_list.len() - 1) + { filter_string.push_str(" AND ") } } diff --git a/apps/labrinth/src/util/actix.rs b/apps/labrinth/src/util/actix.rs index fc77e6630..d89eb17ef 100644 --- a/apps/labrinth/src/util/actix.rs +++ b/apps/labrinth/src/util/actix.rs @@ -20,11 +20,17 @@ pub enum MultipartSegmentData { } pub trait AppendsMultipart { - fn set_multipart(self, data: impl IntoIterator) -> Self; + fn set_multipart( + self, + data: impl IntoIterator, + ) -> Self; } impl AppendsMultipart for TestRequest { - fn set_multipart(self, data: impl IntoIterator) -> Self { + fn set_multipart( + self, + data: impl IntoIterator, + ) -> Self { let (boundary, payload) = generate_multipart(data); self.append_header(( "Content-Type", @@ -34,7 +40,9 @@ impl AppendsMultipart for TestRequest { } } -pub fn generate_multipart(data: impl IntoIterator) -> (String, Bytes) { +pub fn generate_multipart( + data: impl IntoIterator, +) -> (String, Bytes) { let mut boundary: String = String::from("----WebKitFormBoundary"); boundary.push_str(&rand::random::().to_string()); boundary.push_str(&rand::random::().to_string()); @@ -54,7 +62,8 @@ pub fn generate_multipart(data: impl IntoIterator) -> ( if let Some(filename) = &segment.filename { payload.extend_from_slice( - format!("; filename=\"{filename}\"", filename = filename).as_bytes(), + format!("; filename=\"{filename}\"", filename = filename) + .as_bytes(), ); } if let Some(content_type) = &segment.content_type { @@ -78,7 +87,9 @@ pub fn generate_multipart(data: impl IntoIterator) -> ( } payload.extend_from_slice(b"\r\n"); } - payload.extend_from_slice(format!("--{boundary}--\r\n", boundary = boundary).as_bytes()); + payload.extend_from_slice( + format!("--{boundary}--\r\n", boundary = boundary).as_bytes(), + ); (boundary, Bytes::from(payload)) } diff --git a/apps/labrinth/src/util/bitflag.rs b/apps/labrinth/src/util/bitflag.rs index 08647ab6b..c4b789cd7 100644 --- a/apps/labrinth/src/util/bitflag.rs +++ b/apps/labrinth/src/util/bitflag.rs @@ -2,13 +2,18 @@ macro_rules! bitflags_serde_impl { ($type:ident, $int_type:ident) => { impl serde::Serialize for $type { - fn serialize(&self, serializer: S) -> Result { + fn serialize( + &self, + serializer: S, + ) -> Result { serializer.serialize_i64(self.bits() as i64) } } impl<'de> serde::Deserialize<'de> for $type { - fn deserialize>(deserializer: D) -> Result { + fn deserialize>( + deserializer: D, + ) -> Result { let v: i64 = Deserialize::deserialize(deserializer)?; Ok($type::from_bits_truncate(v as $int_type)) diff --git a/apps/labrinth/src/util/captcha.rs b/apps/labrinth/src/util/captcha.rs index 276eac31b..4f4c425ab 100644 --- a/apps/labrinth/src/util/captcha.rs +++ b/apps/labrinth/src/util/captcha.rs @@ -4,7 +4,10 @@ use actix_web::HttpRequest; use serde::Deserialize; use serde_json::json; -pub async fn check_turnstile_captcha(req: &HttpRequest, challenge: &str) -> Result { +pub async fn check_turnstile_captcha( + req: &HttpRequest, + challenge: &str, +) -> Result { let conn_info = req.connection_info().clone(); let ip_addr = if parse_var("CLOUDFLARE_INTEGRATION").unwrap_or(false) { if let Some(header) = req.headers().get("CF-Connecting-IP") { diff --git a/apps/labrinth/src/util/guards.rs b/apps/labrinth/src/util/guards.rs index f7b6b358a..f7ad43ccf 100644 --- a/apps/labrinth/src/util/guards.rs +++ b/apps/labrinth/src/util/guards.rs @@ -2,8 +2,9 @@ use actix_web::guard::GuardContext; pub const ADMIN_KEY_HEADER: &str = "Modrinth-Admin"; pub fn admin_key_guard(ctx: &GuardContext) -> bool { - let admin_key = std::env::var("LABRINTH_ADMIN_KEY") - .expect("No admin key provided, this should have been caught by check_env_vars"); + let admin_key = std::env::var("LABRINTH_ADMIN_KEY").expect( + "No admin key provided, this should have been caught by check_env_vars", + ); ctx.head() .headers() .get(ADMIN_KEY_HEADER) diff --git a/apps/labrinth/src/util/img.rs b/apps/labrinth/src/util/img.rs index 2397e3b23..4e5cb24fb 100644 --- a/apps/labrinth/src/util/img.rs +++ b/apps/labrinth/src/util/img.rs @@ -6,7 +6,10 @@ use crate::models::images::ImageContext; use crate::routes::ApiError; use color_thief::ColorFormat; use image::imageops::FilterType; -use image::{DynamicImage, EncodableLayout, GenericImageView, ImageError, ImageOutputFormat}; +use image::{ + DynamicImage, EncodableLayout, GenericImageView, ImageError, + ImageOutputFormat, +}; use std::io::Cursor; use webp::Encoder; @@ -14,10 +17,15 @@ pub fn get_color_from_img(data: &[u8]) -> Result, ImageError> { let image = image::load_from_memory(data)? .resize(256, 256, FilterType::Nearest) .crop_imm(128, 128, 64, 64); - let color = color_thief::get_palette(image.to_rgb8().as_bytes(), ColorFormat::Rgb, 10, 2) - .ok() - .and_then(|x| x.first().copied()) - .map(|x| (x.r as u32) << 16 | (x.g as u32) << 8 | (x.b as u32)); + let color = color_thief::get_palette( + image.to_rgb8().as_bytes(), + ColorFormat::Rgb, + 10, + 2, + ) + .ok() + .and_then(|x| x.first().copied()) + .map(|x| (x.r as u32) << 16 | (x.g as u32) << 8 | (x.b as u32)); Ok(color) } @@ -40,16 +48,23 @@ pub async fn upload_image_optimized( min_aspect_ratio: Option, file_host: &dyn FileHost, ) -> Result { - let content_type = - crate::util::ext::get_image_content_type(file_extension).ok_or_else(|| { - ApiError::InvalidInput(format!("Invalid format for image: {}", file_extension)) + let content_type = crate::util::ext::get_image_content_type(file_extension) + .ok_or_else(|| { + ApiError::InvalidInput(format!( + "Invalid format for image: {}", + file_extension + )) })?; let cdn_url = dotenvy::var("CDN_URL")?; let hash = sha1::Sha1::from(&bytes).hexdigest(); - let (processed_image, processed_image_ext) = - process_image(bytes.clone(), content_type, target_width, min_aspect_ratio)?; + let (processed_image, processed_image_ext) = process_image( + bytes.clone(), + content_type, + target_width, + min_aspect_ratio, + )?; let color = get_color_from_img(&bytes)?; // Only upload the processed image if it's smaller than the original @@ -118,7 +133,8 @@ fn process_image( if let Some(target_width) = target_width { if img.width() > target_width { - let new_height = (target_width as f32 / aspect_ratio).round() as u32; + let new_height = + (target_width as f32 / aspect_ratio).round() as u32; img = img.resize(target_width, new_height, FilterType::Lanczos3); } } @@ -126,7 +142,8 @@ fn process_image( if let Some(min_aspect_ratio) = min_aspect_ratio { // Crop if necessary if aspect_ratio < min_aspect_ratio { - let crop_height = (img.width() as f32 / min_aspect_ratio).round() as u32; + let crop_height = + (img.width() as f32 / min_aspect_ratio).round() as u32; let y_offset = (img.height() - crop_height) / 2; img = img.crop_imm(0, y_offset, img.width(), crop_height); } @@ -181,7 +198,9 @@ pub async fn delete_unused_images( transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, redis: &RedisPool, ) -> Result<(), ApiError> { - let uploaded_images = database::models::Image::get_many_contexted(context, transaction).await?; + let uploaded_images = + database::models::Image::get_many_contexted(context, transaction) + .await?; for image in uploaded_images { let mut should_delete = true; diff --git a/apps/labrinth/src/util/ratelimit.rs b/apps/labrinth/src/util/ratelimit.rs index 74c7cf5b9..f20fb51f3 100644 --- a/apps/labrinth/src/util/ratelimit.rs +++ b/apps/labrinth/src/util/ratelimit.rs @@ -13,8 +13,12 @@ use actix_web::{ use futures_util::future::LocalBoxFuture; use futures_util::future::{ready, Ready}; -pub type KeyedRateLimiter = - Arc, DefaultClock, MW>>; +pub type KeyedRateLimiter< + K = String, + MW = middleware::StateInformationMiddleware, +> = Arc< + RateLimiter, DefaultClock, MW>, +>; pub struct RateLimit(pub KeyedRateLimiter); @@ -58,7 +62,9 @@ where fn call(&self, req: ServiceRequest) -> Self::Future { if let Some(key) = req.headers().get("x-ratelimit-key") { - if key.to_str().ok() == dotenvy::var("RATE_LIMIT_IGNORE_KEY").ok().as_deref() { + if key.to_str().ok() + == dotenvy::var("RATE_LIMIT_IGNORE_KEY").ok().as_deref() + { let res = self.service.call(req); return Box::pin(async move { @@ -129,7 +135,8 @@ where }) } Err(negative) => { - let wait_time = negative.wait_time_from(DefaultClock::default().now()); + let wait_time = + negative.wait_time_from(DefaultClock::default().now()); let mut response = ApiError::RateLimitError( wait_time.as_millis(), @@ -140,28 +147,41 @@ where let headers = response.headers_mut(); headers.insert( - actix_web::http::header::HeaderName::from_str("x-ratelimit-limit").unwrap(), + actix_web::http::header::HeaderName::from_str( + "x-ratelimit-limit", + ) + .unwrap(), negative.quota().burst_size().get().into(), ); headers.insert( - actix_web::http::header::HeaderName::from_str("x-ratelimit-remaining") - .unwrap(), + actix_web::http::header::HeaderName::from_str( + "x-ratelimit-remaining", + ) + .unwrap(), 0.into(), ); headers.insert( - actix_web::http::header::HeaderName::from_str("x-ratelimit-reset").unwrap(), + actix_web::http::header::HeaderName::from_str( + "x-ratelimit-reset", + ) + .unwrap(), wait_time.as_secs().into(), ); - Box::pin(async { Ok(req.into_response(response.map_into_right_body())) }) + Box::pin(async { + Ok(req.into_response(response.map_into_right_body())) + }) } } } else { - let response = - ApiError::CustomAuthentication("Unable to obtain user IP address!".to_string()) - .error_response(); - - Box::pin(async { Ok(req.into_response(response.map_into_right_body())) }) + let response = ApiError::CustomAuthentication( + "Unable to obtain user IP address!".to_string(), + ) + .error_response(); + + Box::pin(async { + Ok(req.into_response(response.map_into_right_body())) + }) } } } diff --git a/apps/labrinth/src/util/redis.rs b/apps/labrinth/src/util/redis.rs index b5d332198..b3f34ee2b 100644 --- a/apps/labrinth/src/util/redis.rs +++ b/apps/labrinth/src/util/redis.rs @@ -13,6 +13,6 @@ pub async fn redis_execute( where T: redis::FromRedisValue, { - let res = cmd.query_async::<_, T>(redis).await?; + let res = cmd.query_async::(redis).await?; Ok(res) } diff --git a/apps/labrinth/src/util/routes.rs b/apps/labrinth/src/util/routes.rs index 00bb288ec..f12e07d97 100644 --- a/apps/labrinth/src/util/routes.rs +++ b/apps/labrinth/src/util/routes.rs @@ -16,7 +16,9 @@ pub async fn read_from_payload( return Err(ApiError::InvalidInput(String::from(err_msg))); } else { bytes.extend_from_slice(&item.map_err(|_| { - ApiError::InvalidInput("Unable to parse bytes in payload sent!".to_string()) + ApiError::InvalidInput( + "Unable to parse bytes in payload sent!".to_string(), + ) })?); } } diff --git a/apps/labrinth/src/util/validate.rs b/apps/labrinth/src/util/validate.rs index 93ae5087a..312f80f9e 100644 --- a/apps/labrinth/src/util/validate.rs +++ b/apps/labrinth/src/util/validate.rs @@ -6,11 +6,15 @@ use validator::{ValidationErrors, ValidationErrorsKind}; use crate::models::pats::Scopes; lazy_static! { - pub static ref RE_URL_SAFE: Regex = Regex::new(r#"^[a-zA-Z0-9!@$()`.+,_"-]*$"#).unwrap(); + pub static ref RE_URL_SAFE: Regex = + Regex::new(r#"^[a-zA-Z0-9!@$()`.+,_"-]*$"#).unwrap(); } //TODO: In order to ensure readability, only the first error is printed, this may need to be expanded on in the future! -pub fn validation_errors_to_string(errors: ValidationErrors, adder: Option) -> String { +pub fn validation_errors_to_string( + errors: ValidationErrors, + adder: Option, +) -> String { let mut output = String::new(); let map = errors.into_errors(); @@ -21,7 +25,10 @@ pub fn validation_errors_to_string(errors: ValidationErrors, adder: Option { - validation_errors_to_string(*errors.clone(), Some(format!("of item {field}"))) + validation_errors_to_string( + *errors.clone(), + Some(format!("of item {field}")), + ) } ValidationErrorsKind::List(list) => { if let Some((index, errors)) = list.iter().next() { @@ -113,7 +120,9 @@ pub fn validate_url_hashmap_values( Ok(()) } -pub fn validate_no_restricted_scopes(value: &Scopes) -> Result<(), validator::ValidationError> { +pub fn validate_no_restricted_scopes( + value: &Scopes, +) -> Result<(), validator::ValidationError> { if value.is_restricted() { return Err(validator::ValidationError::new( "Restricted scopes not allowed", diff --git a/apps/labrinth/src/util/webhook.rs b/apps/labrinth/src/util/webhook.rs index c5c2d5750..5227b82eb 100644 --- a/apps/labrinth/src/util/webhook.rs +++ b/apps/labrinth/src/util/webhook.rs @@ -47,9 +47,12 @@ async fn get_webhook_metadata( redis: &RedisPool, emoji: bool, ) -> Result, ApiError> { - let project = - crate::database::models::project_item::Project::get_id(project_id.into(), pool, redis) - .await?; + let project = crate::database::models::project_item::Project::get_id( + project_id.into(), + pool, + redis, + ) + .await?; if let Some(mut project) = project { let mut owner = None; @@ -82,9 +85,12 @@ async fn get_webhook_metadata( .await?; if let Some(member) = team.into_iter().find(|x| x.is_owner) { - let user = - crate::database::models::user_item::User::get_id(member.user_id, pool, redis) - .await?; + let user = crate::database::models::user_item::User::get_id( + member.user_id, + pool, + redis, + ) + .await?; if let Some(user) = user { owner = Some(WebhookAuthor { @@ -100,13 +106,16 @@ async fn get_webhook_metadata( } }; - let all_game_versions = MinecraftGameVersion::list(None, None, pool, redis).await?; + let all_game_versions = + MinecraftGameVersion::list(None, None, pool, redis).await?; let versions = project .aggregate_version_fields .clone() .into_iter() - .find_map(|vf| MinecraftGameVersion::try_from_version_field(&vf).ok()) + .find_map(|vf| { + MinecraftGameVersion::try_from_version_field(&vf).ok() + }) .unwrap_or_default(); let formatted_game_versions = get_gv_range(versions, all_game_versions); @@ -196,7 +205,10 @@ async fn get_webhook_metadata( _ => 1049805243866681424, }; - format!("<:{loader}:{emoji_id}> {}{x}", x.remove(0).to_uppercase()) + format!( + "<:{loader}:{emoji_id}> {}{x}", + x.remove(0).to_uppercase() + ) } else { format!("{}{x}", x.remove(0).to_uppercase()) } @@ -323,7 +335,11 @@ pub async fn send_slack_webhook( })) .send() .await - .map_err(|_| ApiError::Discord("Error while sending projects webhook".to_string()))?; + .map_err(|_| { + ApiError::Discord( + "Error while sending projects webhook".to_string(), + ) + })?; } Ok(()) @@ -436,7 +452,9 @@ pub async fn send_discord_webhook( .map(|x| DiscordEmbedImage { url: Some(x) }), footer: Some(DiscordEmbedFooter { text: format!("{} on Modrinth", project.display_project_type), - icon_url: Some("https://cdn-raw.modrinth.com/modrinth-new.png".to_string()), + icon_url: Some( + "https://cdn-raw.modrinth.com/modrinth-new.png".to_string(), + ), }), }; @@ -445,14 +463,21 @@ pub async fn send_discord_webhook( client .post(&webhook_url) .json(&DiscordWebhook { - avatar_url: Some("https://cdn.modrinth.com/Modrinth_Dark_Logo.png".to_string()), + avatar_url: Some( + "https://cdn.modrinth.com/Modrinth_Dark_Logo.png" + .to_string(), + ), username: Some("Modrinth Release".to_string()), embeds: vec![embed], content: message, }) .send() .await - .map_err(|_| ApiError::Discord("Error while sending projects webhook".to_string()))?; + .map_err(|_| { + ApiError::Discord( + "Error while sending projects webhook".to_string(), + ) + })?; } Ok(()) @@ -496,15 +521,21 @@ fn get_gv_range( } else { let interval_base = &intervals[current_interval]; - if ((index as i32) - (interval_base[interval_base.len() - 1][1] as i32) == 1 - || (release_index as i32) - (interval_base[interval_base.len() - 1][2] as i32) == 1) + if ((index as i32) + - (interval_base[interval_base.len() - 1][1] as i32) + == 1 + || (release_index as i32) + - (interval_base[interval_base.len() - 1][2] as i32) + == 1) && (all_game_versions[interval_base[0][1]].type_ == "release" || all_game_versions[index].type_ != "release") { if intervals[current_interval].get(1).is_some() { - intervals[current_interval][1] = vec![i, index, release_index]; + intervals[current_interval][1] = + vec![i, index, release_index]; } else { - intervals[current_interval].insert(1, vec![i, index, release_index]); + intervals[current_interval] + .insert(1, vec![i, index, release_index]); } } else { current_interval += 1; @@ -516,7 +547,10 @@ fn get_gv_range( let mut new_intervals = Vec::new(); for interval in intervals { - if interval.len() == 2 && interval[0][2] != MAX_VALUE && interval[1][2] == MAX_VALUE { + if interval.len() == 2 + && interval[0][2] != MAX_VALUE + && interval[1][2] == MAX_VALUE + { let mut last_snapshot: Option = None; for j in ((interval[0][1] + 1)..=interval[1][1]).rev() { @@ -526,12 +560,16 @@ fn get_gv_range( vec![ game_versions .iter() - .position(|x| x.version == all_game_versions[j].version) + .position(|x| { + x.version == all_game_versions[j].version + }) .unwrap_or(MAX_VALUE), j, all_releases .iter() - .position(|x| x.version == all_game_versions[j].version) + .position(|x| { + x.version == all_game_versions[j].version + }) .unwrap_or(MAX_VALUE), ], ]); @@ -543,7 +581,10 @@ fn get_gv_range( game_versions .iter() .position(|x| { - x.version == all_game_versions[last_snapshot].version + x.version + == all_game_versions + [last_snapshot] + .version }) .unwrap_or(MAX_VALUE), last_snapshot, @@ -572,7 +613,8 @@ fn get_gv_range( if interval.len() == 2 { output.push(format!( "{}—{}", - &game_versions[interval[0][0]].version, &game_versions[interval[1][0]].version + &game_versions[interval[0][0]].version, + &game_versions[interval[1][0]].version )) } else { output.push(game_versions[interval[0][0]].version.clone()) diff --git a/apps/labrinth/src/validate/datapack.rs b/apps/labrinth/src/validate/datapack.rs index 50639fea4..18cdd7e76 100644 --- a/apps/labrinth/src/validate/datapack.rs +++ b/apps/labrinth/src/validate/datapack.rs @@ -1,4 +1,6 @@ -use crate::validate::{SupportedGameVersions, ValidationError, ValidationResult}; +use crate::validate::{ + SupportedGameVersions, ValidationError, ValidationResult, +}; use std::io::Cursor; use zip::ZipArchive; diff --git a/apps/labrinth/src/validate/fabric.rs b/apps/labrinth/src/validate/fabric.rs index 11b9e4996..e5bc34c72 100644 --- a/apps/labrinth/src/validate/fabric.rs +++ b/apps/labrinth/src/validate/fabric.rs @@ -1,4 +1,6 @@ -use crate::validate::{filter_out_packs, SupportedGameVersions, ValidationError, ValidationResult}; +use crate::validate::{ + filter_out_packs, SupportedGameVersions, ValidationError, ValidationResult, +}; use std::io::Cursor; use zip::ZipArchive; diff --git a/apps/labrinth/src/validate/forge.rs b/apps/labrinth/src/validate/forge.rs index 0250284da..503b852b1 100644 --- a/apps/labrinth/src/validate/forge.rs +++ b/apps/labrinth/src/validate/forge.rs @@ -1,4 +1,6 @@ -use crate::validate::{filter_out_packs, SupportedGameVersions, ValidationError, ValidationResult}; +use crate::validate::{ + filter_out_packs, SupportedGameVersions, ValidationError, ValidationResult, +}; use chrono::DateTime; use std::io::Cursor; use zip::ZipArchive; @@ -16,7 +18,9 @@ impl super::Validator for ForgeValidator { fn get_supported_game_versions(&self) -> SupportedGameVersions { // Time since release of 1.13, the first forge version which uses the new TOML system - SupportedGameVersions::PastDate(DateTime::from_timestamp(1540122067, 0).unwrap()) + SupportedGameVersions::PastDate( + DateTime::from_timestamp(1540122067, 0).unwrap(), + ) } fn validate( diff --git a/apps/labrinth/src/validate/liteloader.rs b/apps/labrinth/src/validate/liteloader.rs index 7056d0159..f1a202c27 100644 --- a/apps/labrinth/src/validate/liteloader.rs +++ b/apps/labrinth/src/validate/liteloader.rs @@ -1,4 +1,6 @@ -use crate::validate::{filter_out_packs, SupportedGameVersions, ValidationError, ValidationResult}; +use crate::validate::{ + filter_out_packs, SupportedGameVersions, ValidationError, ValidationResult, +}; use std::io::Cursor; use zip::ZipArchive; diff --git a/apps/labrinth/src/validate/mod.rs b/apps/labrinth/src/validate/mod.rs index 5f5c605f7..7f699e940 100644 --- a/apps/labrinth/src/validate/mod.rs +++ b/apps/labrinth/src/validate/mod.rs @@ -14,7 +14,9 @@ use crate::validate::plugin::*; use crate::validate::quilt::QuiltValidator; use crate::validate::resourcepack::{PackValidator, TexturePackValidator}; use crate::validate::rift::RiftValidator; -use crate::validate::shader::{CanvasShaderValidator, CoreShaderValidator, ShaderValidator}; +use crate::validate::shader::{ + CanvasShaderValidator, CoreShaderValidator, ShaderValidator, +}; use chrono::{DateTime, Utc}; use std::io::Cursor; use thiserror::Error; @@ -128,7 +130,8 @@ pub async fn validate_file( .find_map(|v| MinecraftGameVersion::try_from_version_field(&v).ok()) .unwrap_or_default(); let all_game_versions = - MinecraftGameVersion::list(None, None, &mut *transaction, redis).await?; + MinecraftGameVersion::list(None, None, &mut *transaction, redis) + .await?; validate_minecraft_file( data, @@ -224,23 +227,29 @@ fn game_version_supported( ) -> bool { match supported_game_versions { SupportedGameVersions::All => true, - SupportedGameVersions::PastDate(date) => game_versions.iter().any(|x| { - all_game_versions - .iter() - .find(|y| y.version == x.version) - .map(|x| x.created > date) - .unwrap_or(false) - }), - SupportedGameVersions::Range(before, after) => game_versions.iter().any(|x| { - all_game_versions - .iter() - .find(|y| y.version == x.version) - .map(|x| x.created > before && x.created < after) - .unwrap_or(false) - }), + SupportedGameVersions::PastDate(date) => { + game_versions.iter().any(|x| { + all_game_versions + .iter() + .find(|y| y.version == x.version) + .map(|x| x.created > date) + .unwrap_or(false) + }) + } + SupportedGameVersions::Range(before, after) => { + game_versions.iter().any(|x| { + all_game_versions + .iter() + .find(|y| y.version == x.version) + .map(|x| x.created > before && x.created < after) + .unwrap_or(false) + }) + } SupportedGameVersions::Custom(versions) => { - let version_ids = versions.iter().map(|gv| gv.id).collect::>(); - let game_version_ids: Vec<_> = game_versions.iter().map(|gv| gv.id).collect::>(); + let version_ids = + versions.iter().map(|gv| gv.id).collect::>(); + let game_version_ids: Vec<_> = + game_versions.iter().map(|gv| gv.id).collect::>(); version_ids.iter().any(|x| game_version_ids.contains(x)) } } @@ -249,7 +258,8 @@ fn game_version_supported( pub fn filter_out_packs( archive: &mut ZipArchive>, ) -> Result { - if (archive.by_name("modlist.html").is_ok() && archive.by_name("manifest.json").is_ok()) + if (archive.by_name("modlist.html").is_ok() + && archive.by_name("manifest.json").is_ok()) || archive .file_names() .any(|x| x.starts_with("mods/") && x.ends_with(".jar")) diff --git a/apps/labrinth/src/validate/modpack.rs b/apps/labrinth/src/validate/modpack.rs index cdabd699b..7cc9733fb 100644 --- a/apps/labrinth/src/validate/modpack.rs +++ b/apps/labrinth/src/validate/modpack.rs @@ -1,6 +1,8 @@ use crate::models::pack::{PackFileHash, PackFormat}; use crate::util::validate::validation_errors_to_string; -use crate::validate::{SupportedGameVersions, ValidationError, ValidationResult}; +use crate::validate::{ + SupportedGameVersions, ValidationError, ValidationResult, +}; use std::io::{Cursor, Read}; use std::path::Component; use validator::Validate; @@ -26,11 +28,14 @@ impl super::Validator for ModpackValidator { archive: &mut ZipArchive>, ) -> Result { let pack: PackFormat = { - let mut file = if let Ok(file) = archive.by_name("modrinth.index.json") { - file - } else { - return Ok(ValidationResult::Warning("Pack manifest is missing.")); - }; + let mut file = + if let Ok(file) = archive.by_name("modrinth.index.json") { + file + } else { + return Ok(ValidationResult::Warning( + "Pack manifest is missing.", + )); + }; let mut contents = String::new(); file.read_to_string(&mut contents)?; @@ -39,7 +44,9 @@ impl super::Validator for ModpackValidator { }; pack.validate().map_err(|err| { - ValidationError::InvalidInput(validation_errors_to_string(err, None).into()) + ValidationError::InvalidInput( + validation_errors_to_string(err, None).into(), + ) })?; if pack.game != "minecraft" { @@ -48,8 +55,12 @@ impl super::Validator for ModpackValidator { )); } - if pack.files.is_empty() && !archive.file_names().any(|x| x.starts_with("overrides/")) { - return Err(ValidationError::InvalidInput("Pack has no files!".into())); + if pack.files.is_empty() + && !archive.file_names().any(|x| x.starts_with("overrides/")) + { + return Err(ValidationError::InvalidInput( + "Pack has no files!".into(), + )); } for file in &pack.files { @@ -68,7 +79,11 @@ impl super::Validator for ModpackValidator { let path = std::path::Path::new(&file.path) .components() .next() - .ok_or_else(|| ValidationError::InvalidInput("Invalid pack file path!".into()))?; + .ok_or_else(|| { + ValidationError::InvalidInput( + "Invalid pack file path!".into(), + ) + })?; match path { Component::CurDir | Component::Normal(_) => {} diff --git a/apps/labrinth/src/validate/neoforge.rs b/apps/labrinth/src/validate/neoforge.rs index 7291c4673..59670e8b7 100644 --- a/apps/labrinth/src/validate/neoforge.rs +++ b/apps/labrinth/src/validate/neoforge.rs @@ -1,4 +1,6 @@ -use crate::validate::{filter_out_packs, SupportedGameVersions, ValidationError, ValidationResult}; +use crate::validate::{ + filter_out_packs, SupportedGameVersions, ValidationError, ValidationResult, +}; use std::io::Cursor; use zip::ZipArchive; diff --git a/apps/labrinth/src/validate/plugin.rs b/apps/labrinth/src/validate/plugin.rs index 9a8f44e7e..4f637c66f 100644 --- a/apps/labrinth/src/validate/plugin.rs +++ b/apps/labrinth/src/validate/plugin.rs @@ -1,4 +1,6 @@ -use crate::validate::{SupportedGameVersions, ValidationError, ValidationResult}; +use crate::validate::{ + SupportedGameVersions, ValidationError, ValidationResult, +}; use std::io::Cursor; use zip::ZipArchive; diff --git a/apps/labrinth/src/validate/quilt.rs b/apps/labrinth/src/validate/quilt.rs index 7821967d1..0c3f50bcf 100644 --- a/apps/labrinth/src/validate/quilt.rs +++ b/apps/labrinth/src/validate/quilt.rs @@ -1,4 +1,6 @@ -use crate::validate::{filter_out_packs, SupportedGameVersions, ValidationError, ValidationResult}; +use crate::validate::{ + filter_out_packs, SupportedGameVersions, ValidationError, ValidationResult, +}; use chrono::DateTime; use std::io::Cursor; use zip::ZipArchive; @@ -15,14 +17,17 @@ impl super::Validator for QuiltValidator { } fn get_supported_game_versions(&self) -> SupportedGameVersions { - SupportedGameVersions::PastDate(DateTime::from_timestamp(1646070100, 0).unwrap()) + SupportedGameVersions::PastDate( + DateTime::from_timestamp(1646070100, 0).unwrap(), + ) } fn validate( &self, archive: &mut ZipArchive>, ) -> Result { - if archive.by_name("quilt.mod.json").is_err() && archive.by_name("fabric.mod.json").is_err() + if archive.by_name("quilt.mod.json").is_err() + && archive.by_name("fabric.mod.json").is_err() { return Ok(ValidationResult::Warning( "No quilt.mod.json present for Quilt file.", diff --git a/apps/labrinth/src/validate/resourcepack.rs b/apps/labrinth/src/validate/resourcepack.rs index 35f96c59e..687c5b4e8 100644 --- a/apps/labrinth/src/validate/resourcepack.rs +++ b/apps/labrinth/src/validate/resourcepack.rs @@ -1,4 +1,6 @@ -use crate::validate::{SupportedGameVersions, ValidationError, ValidationResult}; +use crate::validate::{ + SupportedGameVersions, ValidationError, ValidationResult, +}; use chrono::DateTime; use std::io::Cursor; use zip::ZipArchive; @@ -16,7 +18,9 @@ impl super::Validator for PackValidator { fn get_supported_game_versions(&self) -> SupportedGameVersions { // Time since release of 13w24a which replaced texture packs with resource packs - SupportedGameVersions::PastDate(DateTime::from_timestamp(1371137542, 0).unwrap()) + SupportedGameVersions::PastDate( + DateTime::from_timestamp(1371137542, 0).unwrap(), + ) } fn validate( diff --git a/apps/labrinth/src/validate/rift.rs b/apps/labrinth/src/validate/rift.rs index b8cc3d584..b24ff5007 100644 --- a/apps/labrinth/src/validate/rift.rs +++ b/apps/labrinth/src/validate/rift.rs @@ -1,4 +1,6 @@ -use crate::validate::{filter_out_packs, SupportedGameVersions, ValidationError, ValidationResult}; +use crate::validate::{ + filter_out_packs, SupportedGameVersions, ValidationError, ValidationResult, +}; use std::io::Cursor; use zip::ZipArchive; diff --git a/apps/labrinth/src/validate/shader.rs b/apps/labrinth/src/validate/shader.rs index d2de6c8fa..6a83a8195 100644 --- a/apps/labrinth/src/validate/shader.rs +++ b/apps/labrinth/src/validate/shader.rs @@ -1,4 +1,6 @@ -use crate::validate::{SupportedGameVersions, ValidationError, ValidationResult}; +use crate::validate::{ + SupportedGameVersions, ValidationError, ValidationResult, +}; use std::io::Cursor; use zip::ZipArchive; diff --git a/apps/labrinth/tests/analytics.rs b/apps/labrinth/tests/analytics.rs index b3c1fcefc..96e2a440a 100644 --- a/apps/labrinth/tests/analytics.rs +++ b/apps/labrinth/tests/analytics.rs @@ -16,125 +16,137 @@ mod common; #[actix_rt::test] pub async fn analytics_revenue() { - with_test_environment(None, |test_env: TestEnvironment| async move { - let api = &test_env.api; - - let alpha_project_id = test_env.dummy.project_alpha.project_id.clone(); - - let pool = test_env.db.pool.clone(); - - // Generate sample revenue data- directly insert into sql - let ( - mut insert_user_ids, - mut insert_project_ids, - mut insert_payouts, - mut insert_starts, - mut insert_availables, - ) = (Vec::new(), Vec::new(), Vec::new(), Vec::new(), Vec::new()); - - // Note: these go from most recent to least recent - let money_time_pairs: [(f64, DateTime); 10] = [ - (50.0, Utc::now() - Duration::minutes(5)), - (50.1, Utc::now() - Duration::minutes(10)), - (101.0, Utc::now() - Duration::days(1)), - (200.0, Utc::now() - Duration::days(2)), - (311.0, Utc::now() - Duration::days(3)), - (400.0, Utc::now() - Duration::days(4)), - (526.0, Utc::now() - Duration::days(5)), - (633.0, Utc::now() - Duration::days(6)), - (800.0, Utc::now() - Duration::days(14)), - (800.0, Utc::now() - Duration::days(800)), - ]; - - let project_id = parse_base62(&alpha_project_id).unwrap() as i64; - for (money, time) in money_time_pairs.iter() { - insert_user_ids.push(USER_USER_ID_PARSED); - insert_project_ids.push(project_id); - insert_payouts.push(Decimal::from_f64_retain(*money).unwrap()); - insert_starts.push(*time); - insert_availables.push(*time); - } - - let mut transaction = pool.begin().await.unwrap(); - payouts::insert_payouts( - insert_user_ids, - insert_project_ids, - insert_payouts, - insert_starts, - insert_availables, - &mut transaction, - ) - .await - .unwrap(); - transaction.commit().await.unwrap(); - - let day = 86400; - - // Test analytics endpoint with default values - // - all time points in the last 2 weeks - // - 1 day resolution - let analytics = api - .get_analytics_revenue_deserialized( - vec![&alpha_project_id], - false, - None, - None, - None, - USER_USER_PAT, - ) - .await; - assert_eq!(analytics.len(), 1); // 1 project - let project_analytics = analytics.get(&alpha_project_id).unwrap(); - assert_eq!(project_analytics.len(), 8); // 1 days cut off, and 2 points take place on the same day. note that the day exactly 14 days ago is included - // sorted_by_key, values in the order of smallest to largest key - let (sorted_keys, sorted_by_key): (Vec, Vec) = project_analytics - .iter() - .sorted_by_key(|(k, _)| *k) - .rev() - .unzip(); - assert_eq!( - vec![100.1, 101.0, 200.0, 311.0, 400.0, 526.0, 633.0, 800.0], - to_f64_vec_rounded_up(sorted_by_key) - ); - // Ensure that the keys are in multiples of 1 day - for k in sorted_keys { - assert_eq!(k % day, 0); - } - - // Test analytics with last 900 days to include all data - // keep resolution at default - let analytics = api - .get_analytics_revenue_deserialized( - vec![&alpha_project_id], - false, - Some(Utc::now() - Duration::days(801)), - None, - None, - USER_USER_PAT, + with_test_environment( + None, + |test_env: TestEnvironment| async move { + let api = &test_env.api; + + let alpha_project_id = + test_env.dummy.project_alpha.project_id.clone(); + + let pool = test_env.db.pool.clone(); + + // Generate sample revenue data- directly insert into sql + let ( + mut insert_user_ids, + mut insert_project_ids, + mut insert_payouts, + mut insert_starts, + mut insert_availables, + ) = (Vec::new(), Vec::new(), Vec::new(), Vec::new(), Vec::new()); + + // Note: these go from most recent to least recent + let money_time_pairs: [(f64, DateTime); 10] = [ + (50.0, Utc::now() - Duration::minutes(5)), + (50.1, Utc::now() - Duration::minutes(10)), + (101.0, Utc::now() - Duration::days(1)), + (200.0, Utc::now() - Duration::days(2)), + (311.0, Utc::now() - Duration::days(3)), + (400.0, Utc::now() - Duration::days(4)), + (526.0, Utc::now() - Duration::days(5)), + (633.0, Utc::now() - Duration::days(6)), + (800.0, Utc::now() - Duration::days(14)), + (800.0, Utc::now() - Duration::days(800)), + ]; + + let project_id = parse_base62(&alpha_project_id).unwrap() as i64; + for (money, time) in money_time_pairs.iter() { + insert_user_ids.push(USER_USER_ID_PARSED); + insert_project_ids.push(project_id); + insert_payouts.push(Decimal::from_f64_retain(*money).unwrap()); + insert_starts.push(*time); + insert_availables.push(*time); + } + + let mut transaction = pool.begin().await.unwrap(); + payouts::insert_payouts( + insert_user_ids, + insert_project_ids, + insert_payouts, + insert_starts, + insert_availables, + &mut transaction, ) - .await; - let project_analytics = analytics.get(&alpha_project_id).unwrap(); - assert_eq!(project_analytics.len(), 9); // and 2 points take place on the same day - let (sorted_keys, sorted_by_key): (Vec, Vec) = project_analytics - .iter() - .sorted_by_key(|(k, _)| *k) - .rev() - .unzip(); - assert_eq!( - vec![100.1, 101.0, 200.0, 311.0, 400.0, 526.0, 633.0, 800.0, 800.0], - to_f64_vec_rounded_up(sorted_by_key) - ); - for k in sorted_keys { - assert_eq!(k % day, 0); - } - }) + .await + .unwrap(); + transaction.commit().await.unwrap(); + + let day = 86400; + + // Test analytics endpoint with default values + // - all time points in the last 2 weeks + // - 1 day resolution + let analytics = api + .get_analytics_revenue_deserialized( + vec![&alpha_project_id], + false, + None, + None, + None, + USER_USER_PAT, + ) + .await; + assert_eq!(analytics.len(), 1); // 1 project + let project_analytics = analytics.get(&alpha_project_id).unwrap(); + assert_eq!(project_analytics.len(), 8); // 1 days cut off, and 2 points take place on the same day. note that the day exactly 14 days ago is included + // sorted_by_key, values in the order of smallest to largest key + let (sorted_keys, sorted_by_key): (Vec, Vec) = + project_analytics + .iter() + .sorted_by_key(|(k, _)| *k) + .rev() + .unzip(); + assert_eq!( + vec![100.1, 101.0, 200.0, 311.0, 400.0, 526.0, 633.0, 800.0], + to_f64_vec_rounded_up(sorted_by_key) + ); + // Ensure that the keys are in multiples of 1 day + for k in sorted_keys { + assert_eq!(k % day, 0); + } + + // Test analytics with last 900 days to include all data + // keep resolution at default + let analytics = api + .get_analytics_revenue_deserialized( + vec![&alpha_project_id], + false, + Some(Utc::now() - Duration::days(801)), + None, + None, + USER_USER_PAT, + ) + .await; + let project_analytics = analytics.get(&alpha_project_id).unwrap(); + assert_eq!(project_analytics.len(), 9); // and 2 points take place on the same day + let (sorted_keys, sorted_by_key): (Vec, Vec) = + project_analytics + .iter() + .sorted_by_key(|(k, _)| *k) + .rev() + .unzip(); + assert_eq!( + vec![ + 100.1, 101.0, 200.0, 311.0, 400.0, 526.0, 633.0, 800.0, + 800.0 + ], + to_f64_vec_rounded_up(sorted_by_key) + ); + for k in sorted_keys { + assert_eq!(k % day, 0); + } + }, + ) .await; } fn to_f64_rounded_up(d: Decimal) -> f64 { - d.round_dp_with_strategy(1, rust_decimal::RoundingStrategy::MidpointAwayFromZero) - .to_f64() - .unwrap() + d.round_dp_with_strategy( + 1, + rust_decimal::RoundingStrategy::MidpointAwayFromZero, + ) + .to_f64() + .unwrap() } fn to_f64_vec_rounded_up(d: Vec) -> Vec { @@ -143,88 +155,93 @@ fn to_f64_vec_rounded_up(d: Vec) -> Vec { #[actix_rt::test] pub async fn permissions_analytics_revenue() { - with_test_environment(None, |test_env: TestEnvironment| async move { - let alpha_project_id = test_env.dummy.project_alpha.project_id.clone(); - let alpha_version_id = test_env.dummy.project_alpha.version_id.clone(); - let alpha_team_id = test_env.dummy.project_alpha.team_id.clone(); - - let api = &test_env.api; - - let view_analytics = ProjectPermissions::VIEW_ANALYTICS; - - // first, do check with a project - let req_gen = |ctx: PermissionsTestContext| async move { - let project_id = ctx.project_id.unwrap(); - let ids_or_slugs = vec![project_id.as_str()]; - api.get_analytics_revenue( - ids_or_slugs, - false, - None, - None, - Some(5), - ctx.test_pat.as_deref(), - ) - .await - }; - - PermissionsTest::new(&test_env) - .with_failure_codes(vec![200, 401]) - .with_200_json_checks( - // On failure, should have 0 projects returned - |value: &serde_json::Value| { - let value = value.as_object().unwrap(); - assert_eq!(value.len(), 0); - }, - // On success, should have 1 project returned - |value: &serde_json::Value| { - let value = value.as_object().unwrap(); - assert_eq!(value.len(), 1); - }, - ) - .simple_project_permissions_test(view_analytics, req_gen) - .await - .unwrap(); - - // Now with a version - // Need to use alpha - let req_gen = |ctx: PermissionsTestContext| { - let alpha_version_id = alpha_version_id.clone(); - async move { - let ids_or_slugs = vec![alpha_version_id.as_str()]; + with_test_environment( + None, + |test_env: TestEnvironment| async move { + let alpha_project_id = + test_env.dummy.project_alpha.project_id.clone(); + let alpha_version_id = + test_env.dummy.project_alpha.version_id.clone(); + let alpha_team_id = test_env.dummy.project_alpha.team_id.clone(); + + let api = &test_env.api; + + let view_analytics = ProjectPermissions::VIEW_ANALYTICS; + + // first, do check with a project + let req_gen = |ctx: PermissionsTestContext| async move { + let project_id = ctx.project_id.unwrap(); + let ids_or_slugs = vec![project_id.as_str()]; api.get_analytics_revenue( ids_or_slugs, - true, + false, None, None, Some(5), ctx.test_pat.as_deref(), ) .await - } - }; - - PermissionsTest::new(&test_env) - .with_failure_codes(vec![200, 401]) - .with_existing_project(&alpha_project_id, &alpha_team_id) - .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) - .with_200_json_checks( - // On failure, should have 0 versions returned - |value: &serde_json::Value| { - let value = value.as_object().unwrap(); - assert_eq!(value.len(), 0); - }, - // On success, should have 1 versions returned - |value: &serde_json::Value| { - let value = value.as_object().unwrap(); - assert_eq!(value.len(), 0); - }, - ) - .simple_project_permissions_test(view_analytics, req_gen) - .await - .unwrap(); + }; + + PermissionsTest::new(&test_env) + .with_failure_codes(vec![200, 401]) + .with_200_json_checks( + // On failure, should have 0 projects returned + |value: &serde_json::Value| { + let value = value.as_object().unwrap(); + assert_eq!(value.len(), 0); + }, + // On success, should have 1 project returned + |value: &serde_json::Value| { + let value = value.as_object().unwrap(); + assert_eq!(value.len(), 1); + }, + ) + .simple_project_permissions_test(view_analytics, req_gen) + .await + .unwrap(); + + // Now with a version + // Need to use alpha + let req_gen = |ctx: PermissionsTestContext| { + let alpha_version_id = alpha_version_id.clone(); + async move { + let ids_or_slugs = vec![alpha_version_id.as_str()]; + api.get_analytics_revenue( + ids_or_slugs, + true, + None, + None, + Some(5), + ctx.test_pat.as_deref(), + ) + .await + } + }; + + PermissionsTest::new(&test_env) + .with_failure_codes(vec![200, 401]) + .with_existing_project(&alpha_project_id, &alpha_team_id) + .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) + .with_200_json_checks( + // On failure, should have 0 versions returned + |value: &serde_json::Value| { + let value = value.as_object().unwrap(); + assert_eq!(value.len(), 0); + }, + // On success, should have 1 versions returned + |value: &serde_json::Value| { + let value = value.as_object().unwrap(); + assert_eq!(value.len(), 0); + }, + ) + .simple_project_permissions_test(view_analytics, req_gen) + .await + .unwrap(); - // Cleanup test db - test_env.cleanup().await; - }) + // Cleanup test db + test_env.cleanup().await; + }, + ) .await; } diff --git a/apps/labrinth/tests/common/api_common/mod.rs b/apps/labrinth/tests/common/api_common/mod.rs index c5c585343..aca326b37 100644 --- a/apps/labrinth/tests/common/api_common/mod.rs +++ b/apps/labrinth/tests/common/api_common/mod.rs @@ -1,8 +1,8 @@ use std::collections::HashMap; use self::models::{ - CommonCategoryData, CommonItemType, CommonLoaderData, CommonNotification, CommonProject, - CommonTeamMember, CommonVersion, + CommonCategoryData, CommonItemType, CommonLoaderData, CommonNotification, + CommonProject, CommonTeamMember, CommonVersion, }; use self::request_data::{ImageData, ProjectCreationRequestData}; use actix_web::dev::ServiceResponse; @@ -51,14 +51,26 @@ pub trait ApiProject { version_jar: Option<&TestFile>, ) -> serde_json::Value; - async fn remove_project(&self, id_or_slug: &str, pat: Option<&str>) -> ServiceResponse; - async fn get_project(&self, id_or_slug: &str, pat: Option<&str>) -> ServiceResponse; + async fn remove_project( + &self, + id_or_slug: &str, + pat: Option<&str>, + ) -> ServiceResponse; + async fn get_project( + &self, + id_or_slug: &str, + pat: Option<&str>, + ) -> ServiceResponse; async fn get_project_deserialized_common( &self, id_or_slug: &str, pat: Option<&str>, ) -> CommonProject; - async fn get_projects(&self, ids_or_slugs: &[&str], pat: Option<&str>) -> ServiceResponse; + async fn get_projects( + &self, + ids_or_slugs: &[&str], + pat: Option<&str>, + ) -> ServiceResponse; async fn get_project_dependencies( &self, id_or_slug: &str, @@ -125,7 +137,11 @@ pub trait ApiProject { pat: Option<&str>, ) -> ServiceResponse; async fn get_report(&self, id: &str, pat: Option<&str>) -> ServiceResponse; - async fn get_reports(&self, ids: &[&str], pat: Option<&str>) -> ServiceResponse; + async fn get_reports( + &self, + ids: &[&str], + pat: Option<&str>, + ) -> ServiceResponse; async fn get_user_reports(&self, pat: Option<&str>) -> ServiceResponse; async fn edit_report( &self, @@ -133,9 +149,17 @@ pub trait ApiProject { patch: serde_json::Value, pat: Option<&str>, ) -> ServiceResponse; - async fn delete_report(&self, id: &str, pat: Option<&str>) -> ServiceResponse; + async fn delete_report( + &self, + id: &str, + pat: Option<&str>, + ) -> ServiceResponse; async fn get_thread(&self, id: &str, pat: Option<&str>) -> ServiceResponse; - async fn get_threads(&self, ids: &[&str], pat: Option<&str>) -> ServiceResponse; + async fn get_threads( + &self, + ids: &[&str], + pat: Option<&str>, + ) -> ServiceResponse; async fn write_to_thread( &self, id: &str, @@ -144,8 +168,13 @@ pub trait ApiProject { pat: Option<&str>, ) -> ServiceResponse; async fn get_moderation_inbox(&self, pat: Option<&str>) -> ServiceResponse; - async fn read_thread(&self, id: &str, pat: Option<&str>) -> ServiceResponse; - async fn delete_thread_message(&self, id: &str, pat: Option<&str>) -> ServiceResponse; + async fn read_thread(&self, id: &str, pat: Option<&str>) + -> ServiceResponse; + async fn delete_thread_message( + &self, + id: &str, + pat: Option<&str>, + ) -> ServiceResponse; } #[async_trait(?Send)] @@ -153,19 +182,33 @@ pub trait ApiTags { async fn get_loaders(&self) -> ServiceResponse; async fn get_loaders_deserialized_common(&self) -> Vec; async fn get_categories(&self) -> ServiceResponse; - async fn get_categories_deserialized_common(&self) -> Vec; + async fn get_categories_deserialized_common( + &self, + ) -> Vec; } #[async_trait(?Send)] pub trait ApiTeams { - async fn get_team_members(&self, team_id: &str, pat: Option<&str>) -> ServiceResponse; + async fn get_team_members( + &self, + team_id: &str, + pat: Option<&str>, + ) -> ServiceResponse; async fn get_team_members_deserialized_common( &self, team_id: &str, pat: Option<&str>, ) -> Vec; - async fn get_teams_members(&self, team_ids: &[&str], pat: Option<&str>) -> ServiceResponse; - async fn get_project_members(&self, id_or_slug: &str, pat: Option<&str>) -> ServiceResponse; + async fn get_teams_members( + &self, + team_ids: &[&str], + pat: Option<&str>, + ) -> ServiceResponse; + async fn get_project_members( + &self, + id_or_slug: &str, + pat: Option<&str>, + ) -> ServiceResponse; async fn get_project_members_deserialized_common( &self, id_or_slug: &str, @@ -181,7 +224,11 @@ pub trait ApiTeams { id_or_title: &str, pat: Option<&str>, ) -> Vec; - async fn join_team(&self, team_id: &str, pat: Option<&str>) -> ServiceResponse; + async fn join_team( + &self, + team_id: &str, + pat: Option<&str>, + ) -> ServiceResponse; async fn remove_from_team( &self, team_id: &str, @@ -201,20 +248,36 @@ pub trait ApiTeams { user_id: &str, pat: Option<&str>, ) -> ServiceResponse; - async fn get_user_notifications(&self, user_id: &str, pat: Option<&str>) -> ServiceResponse; + async fn get_user_notifications( + &self, + user_id: &str, + pat: Option<&str>, + ) -> ServiceResponse; async fn get_user_notifications_deserialized_common( &self, user_id: &str, pat: Option<&str>, ) -> Vec; - async fn get_notification(&self, notification_id: &str, pat: Option<&str>) -> ServiceResponse; - async fn get_notifications(&self, ids: &[&str], pat: Option<&str>) -> ServiceResponse; + async fn get_notification( + &self, + notification_id: &str, + pat: Option<&str>, + ) -> ServiceResponse; + async fn get_notifications( + &self, + ids: &[&str], + pat: Option<&str>, + ) -> ServiceResponse; async fn mark_notification_read( &self, notification_id: &str, pat: Option<&str>, ) -> ServiceResponse; - async fn mark_notifications_read(&self, ids: &[&str], pat: Option<&str>) -> ServiceResponse; + async fn mark_notifications_read( + &self, + ids: &[&str], + pat: Option<&str>, + ) -> ServiceResponse; async fn add_user_to_team( &self, team_id: &str, @@ -228,12 +291,20 @@ pub trait ApiTeams { notification_id: &str, pat: Option<&str>, ) -> ServiceResponse; - async fn delete_notifications(&self, ids: &[&str], pat: Option<&str>) -> ServiceResponse; + async fn delete_notifications( + &self, + ids: &[&str], + pat: Option<&str>, + ) -> ServiceResponse; } #[async_trait(?Send)] pub trait ApiUser { - async fn get_user(&self, id_or_username: &str, pat: Option<&str>) -> ServiceResponse; + async fn get_user( + &self, + id_or_username: &str, + pat: Option<&str>, + ) -> ServiceResponse; async fn get_current_user(&self, pat: Option<&str>) -> ServiceResponse; async fn edit_user( &self, @@ -241,7 +312,11 @@ pub trait ApiUser { patch: serde_json::Value, pat: Option<&str>, ) -> ServiceResponse; - async fn delete_user(&self, id_or_username: &str, pat: Option<&str>) -> ServiceResponse; + async fn delete_user( + &self, + id_or_username: &str, + pat: Option<&str>, + ) -> ServiceResponse; } #[async_trait(?Send)] @@ -264,13 +339,18 @@ pub trait ApiVersion { modify_json: Option, pat: Option<&str>, ) -> CommonVersion; - async fn get_version(&self, id: &str, pat: Option<&str>) -> ServiceResponse; + async fn get_version(&self, id: &str, pat: Option<&str>) + -> ServiceResponse; async fn get_version_deserialized_common( &self, id_or_slug: &str, pat: Option<&str>, ) -> CommonVersion; - async fn get_versions(&self, ids: Vec, pat: Option<&str>) -> ServiceResponse; + async fn get_versions( + &self, + ids: Vec, + pat: Option<&str>, + ) -> ServiceResponse; async fn get_versions_deserialized_common( &self, ids: Vec, @@ -384,8 +464,16 @@ pub trait ApiVersion { file: &TestFile, pat: Option<&str>, ) -> ServiceResponse; - async fn remove_version(&self, id_or_slug: &str, pat: Option<&str>) -> ServiceResponse; - async fn remove_version_file(&self, hash: &str, pat: Option<&str>) -> ServiceResponse; + async fn remove_version( + &self, + id_or_slug: &str, + pat: Option<&str>, + ) -> ServiceResponse; + async fn remove_version_file( + &self, + hash: &str, + pat: Option<&str>, + ) -> ServiceResponse; } pub trait AppendsOptionalPat { diff --git a/apps/labrinth/tests/common/api_common/models.rs b/apps/labrinth/tests/common/api_common/models.rs index f756a12a1..bf4875acc 100644 --- a/apps/labrinth/tests/common/api_common/models.rs +++ b/apps/labrinth/tests/common/api_common/models.rs @@ -6,8 +6,9 @@ use labrinth::{ notifications::NotificationId, organizations::OrganizationId, projects::{ - Dependency, GalleryItem, License, ModeratorMessage, MonetizationStatus, ProjectId, - ProjectStatus, VersionFile, VersionId, VersionStatus, VersionType, + Dependency, GalleryItem, License, ModeratorMessage, + MonetizationStatus, ProjectId, ProjectStatus, VersionFile, + VersionId, VersionStatus, VersionType, }, reports::ReportId, teams::{ProjectPermissions, TeamId}, diff --git a/apps/labrinth/tests/common/api_v2/mod.rs b/apps/labrinth/tests/common/api_v2/mod.rs index 84365e6fd..a3d52ba01 100644 --- a/apps/labrinth/tests/common/api_v2/mod.rs +++ b/apps/labrinth/tests/common/api_v2/mod.rs @@ -24,8 +24,11 @@ pub struct ApiV2 { #[async_trait(?Send)] impl ApiBuildable for ApiV2 { async fn build(labrinth_config: LabrinthConfig) -> Self { - let app = App::new().configure(|cfg| labrinth::app_config(cfg, labrinth_config.clone())); - let test_app: Rc = Rc::new(test::init_service(app).await); + let app = App::new().configure(|cfg| { + labrinth::app_config(cfg, labrinth_config.clone()) + }); + let test_app: Rc = + Rc::new(test::init_service(app).await); Self { test_app } } diff --git a/apps/labrinth/tests/common/api_v2/project.rs b/apps/labrinth/tests/common/api_v2/project.rs index 12fbf2061..df268fcaf 100644 --- a/apps/labrinth/tests/common/api_v2/project.rs +++ b/apps/labrinth/tests/common/api_v2/project.rs @@ -89,7 +89,8 @@ impl ApiProject for ApiV2 { modify_json: Option, pat: Option<&str>, ) -> (CommonProject, Vec) { - let creation_data = get_public_project_creation_data(slug, version_jar, modify_json); + let creation_data = + get_public_project_creation_data(slug, version_jar, modify_json); // Add a project. let slug = creation_data.slug.clone(); @@ -143,7 +144,11 @@ impl ApiProject for ApiV2 { self.call(req).await } - async fn remove_project(&self, project_slug_or_id: &str, pat: Option<&str>) -> ServiceResponse { + async fn remove_project( + &self, + project_slug_or_id: &str, + pat: Option<&str>, + ) -> ServiceResponse { let req = test::TestRequest::delete() .uri(&format!("/v2/project/{project_slug_or_id}")) .append_pat(pat) @@ -152,7 +157,11 @@ impl ApiProject for ApiV2 { self.call(req).await } - async fn get_project(&self, id_or_slug: &str, pat: Option<&str>) -> ServiceResponse { + async fn get_project( + &self, + id_or_slug: &str, + pat: Option<&str>, + ) -> ServiceResponse { let req = TestRequest::get() .uri(&format!("/v2/project/{id_or_slug}")) .append_pat(pat) @@ -174,7 +183,11 @@ impl ApiProject for ApiV2 { serde_json::from_value(value).unwrap() } - async fn get_projects(&self, ids_or_slugs: &[&str], pat: Option<&str>) -> ServiceResponse { + async fn get_projects( + &self, + ids_or_slugs: &[&str], + pat: Option<&str>, + ) -> ServiceResponse { let ids_or_slugs = serde_json::to_string(ids_or_slugs).unwrap(); let req = test::TestRequest::get() .uri(&format!( @@ -324,7 +337,11 @@ impl ApiProject for ApiV2 { self.call(req).await } - async fn get_reports(&self, ids: &[&str], pat: Option<&str>) -> ServiceResponse { + async fn get_reports( + &self, + ids: &[&str], + pat: Option<&str>, + ) -> ServiceResponse { let ids_str = serde_json::to_string(ids).unwrap(); let req = test::TestRequest::get() .uri(&format!( @@ -346,7 +363,11 @@ impl ApiProject for ApiV2 { self.call(req).await } - async fn delete_report(&self, id: &str, pat: Option<&str>) -> ServiceResponse { + async fn delete_report( + &self, + id: &str, + pat: Option<&str>, + ) -> ServiceResponse { let req = test::TestRequest::delete() .uri(&format!("/v2/report/{id}")) .append_pat(pat) @@ -379,7 +400,11 @@ impl ApiProject for ApiV2 { self.call(req).await } - async fn get_threads(&self, ids: &[&str], pat: Option<&str>) -> ServiceResponse { + async fn get_threads( + &self, + ids: &[&str], + pat: Option<&str>, + ) -> ServiceResponse { let ids_str = serde_json::to_string(ids).unwrap(); let req = test::TestRequest::get() .uri(&format!( @@ -422,7 +447,11 @@ impl ApiProject for ApiV2 { self.call(req).await } - async fn read_thread(&self, id: &str, pat: Option<&str>) -> ServiceResponse { + async fn read_thread( + &self, + id: &str, + pat: Option<&str>, + ) -> ServiceResponse { let req = test::TestRequest::post() .uri(&format!("/v2/thread/{id}/read")) .append_pat(pat) @@ -431,7 +460,11 @@ impl ApiProject for ApiV2 { self.call(req).await } - async fn delete_thread_message(&self, id: &str, pat: Option<&str>) -> ServiceResponse { + async fn delete_thread_message( + &self, + id: &str, + pat: Option<&str>, + ) -> ServiceResponse { let req = test::TestRequest::delete() .uri(&format!("/v2/message/{id}")) .append_pat(pat) diff --git a/apps/labrinth/tests/common/api_v2/request_data.rs b/apps/labrinth/tests/common/api_v2/request_data.rs index 1d2065ff8..8eced3618 100644 --- a/apps/labrinth/tests/common/api_v2/request_data.rs +++ b/apps/labrinth/tests/common/api_v2/request_data.rs @@ -2,7 +2,9 @@ use serde_json::json; use crate::common::{ - api_common::request_data::{ProjectCreationRequestData, VersionCreationRequestData}, + api_common::request_data::{ + ProjectCreationRequestData, VersionCreationRequestData, + }, dummy_data::TestFile, }; use labrinth::{ @@ -15,11 +17,13 @@ pub fn get_public_project_creation_data( version_jar: Option, modify_json: Option, ) -> ProjectCreationRequestData { - let mut json_data = get_public_project_creation_data_json(slug, version_jar.as_ref()); + let mut json_data = + get_public_project_creation_data_json(slug, version_jar.as_ref()); if let Some(modify_json) = modify_json { json_patch::patch(&mut json_data, &modify_json).unwrap(); } - let multipart_data = get_public_creation_data_multipart(&json_data, version_jar.as_ref()); + let multipart_data = + get_public_creation_data_multipart(&json_data, version_jar.as_ref()); ProjectCreationRequestData { slug: slug.to_string(), jar: version_jar, @@ -34,13 +38,17 @@ pub fn get_public_version_creation_data( ordering: Option, modify_json: Option, ) -> VersionCreationRequestData { - let mut json_data = - get_public_version_creation_data_json(version_number, ordering, &version_jar); + let mut json_data = get_public_version_creation_data_json( + version_number, + ordering, + &version_jar, + ); json_data["project_id"] = json!(project_id); if let Some(modify_json) = modify_json { json_patch::patch(&mut json_data, &modify_json).unwrap(); } - let multipart_data = get_public_creation_data_multipart(&json_data, Some(&version_jar)); + let multipart_data = + get_public_creation_data_multipart(&json_data, Some(&version_jar)); VersionCreationRequestData { version: version_number.to_string(), jar: Some(version_jar), @@ -106,7 +114,9 @@ pub fn get_public_creation_data_multipart( name: "data".to_string(), filename: None, content_type: Some("application/json".to_string()), - data: MultipartSegmentData::Text(serde_json::to_string(json_data).unwrap()), + data: MultipartSegmentData::Text( + serde_json::to_string(json_data).unwrap(), + ), }; if let Some(jar) = version_jar { diff --git a/apps/labrinth/tests/common/api_v2/tags.rs b/apps/labrinth/tests/common/api_v2/tags.rs index 2749e5cad..b78a5c0d1 100644 --- a/apps/labrinth/tests/common/api_v2/tags.rs +++ b/apps/labrinth/tests/common/api_v2/tags.rs @@ -44,7 +44,9 @@ impl ApiV2 { self.call(req).await } - pub async fn get_game_versions_deserialized(&self) -> Vec { + pub async fn get_game_versions_deserialized( + &self, + ) -> Vec { let resp = self.get_game_versions().await; assert_status!(&resp, StatusCode::OK); test::read_body_json(resp).await @@ -70,7 +72,9 @@ impl ApiV2 { self.call(req).await } - pub async fn get_donation_platforms_deserialized(&self) -> Vec { + pub async fn get_donation_platforms_deserialized( + &self, + ) -> Vec { let resp = self.get_donation_platforms().await; assert_status!(&resp, StatusCode::OK); test::read_body_json(resp).await @@ -105,7 +109,9 @@ impl ApiTags for ApiV2 { self.call(req).await } - async fn get_categories_deserialized_common(&self) -> Vec { + async fn get_categories_deserialized_common( + &self, + ) -> Vec { let resp = self.get_categories().await; assert_status!(&resp, StatusCode::OK); // First, deserialize to the non-common format (to test the response is valid for this api version) diff --git a/apps/labrinth/tests/common/api_v2/team.rs b/apps/labrinth/tests/common/api_v2/team.rs index 015cf34dd..8af4f0054 100644 --- a/apps/labrinth/tests/common/api_v2/team.rs +++ b/apps/labrinth/tests/common/api_v2/team.rs @@ -51,7 +51,11 @@ impl ApiV2 { #[async_trait(?Send)] impl ApiTeams for ApiV2 { - async fn get_team_members(&self, id_or_title: &str, pat: Option<&str>) -> ServiceResponse { + async fn get_team_members( + &self, + id_or_title: &str, + pat: Option<&str>, + ) -> ServiceResponse { let req = test::TestRequest::get() .uri(&format!("/v2/team/{id_or_title}/members")) .append_pat(pat) @@ -89,7 +93,11 @@ impl ApiTeams for ApiV2 { self.call(req).await } - async fn get_project_members(&self, id_or_title: &str, pat: Option<&str>) -> ServiceResponse { + async fn get_project_members( + &self, + id_or_title: &str, + pat: Option<&str>, + ) -> ServiceResponse { let req = test::TestRequest::get() .uri(&format!("/v2/project/{id_or_title}/members")) .append_pat(pat) @@ -137,7 +145,11 @@ impl ApiTeams for ApiV2 { serde_json::from_value(value).unwrap() } - async fn join_team(&self, team_id: &str, pat: Option<&str>) -> ServiceResponse { + async fn join_team( + &self, + team_id: &str, + pat: Option<&str>, + ) -> ServiceResponse { let req = test::TestRequest::post() .uri(&format!("/v2/team/{team_id}/join")) .append_pat(pat) @@ -189,7 +201,11 @@ impl ApiTeams for ApiV2 { self.call(req).await } - async fn get_user_notifications(&self, user_id: &str, pat: Option<&str>) -> ServiceResponse { + async fn get_user_notifications( + &self, + user_id: &str, + pat: Option<&str>, + ) -> ServiceResponse { let req = test::TestRequest::get() .uri(&format!("/v2/user/{user_id}/notifications")) .append_pat(pat) @@ -211,7 +227,11 @@ impl ApiTeams for ApiV2 { serde_json::from_value(value).unwrap() } - async fn get_notification(&self, notification_id: &str, pat: Option<&str>) -> ServiceResponse { + async fn get_notification( + &self, + notification_id: &str, + pat: Option<&str>, + ) -> ServiceResponse { let req = test::TestRequest::get() .uri(&format!("/v2/notification/{notification_id}")) .append_pat(pat) diff --git a/apps/labrinth/tests/common/api_v2/user.rs b/apps/labrinth/tests/common/api_v2/user.rs index 1fde5b12f..7031b66f4 100644 --- a/apps/labrinth/tests/common/api_v2/user.rs +++ b/apps/labrinth/tests/common/api_v2/user.rs @@ -5,7 +5,11 @@ use async_trait::async_trait; #[async_trait(?Send)] impl ApiUser for ApiV2 { - async fn get_user(&self, user_id_or_username: &str, pat: Option<&str>) -> ServiceResponse { + async fn get_user( + &self, + user_id_or_username: &str, + pat: Option<&str>, + ) -> ServiceResponse { let req = test::TestRequest::get() .uri(&format!("/v2/user/{}", user_id_or_username)) .append_pat(pat) @@ -36,7 +40,11 @@ impl ApiUser for ApiV2 { self.call(req).await } - async fn delete_user(&self, user_id_or_username: &str, pat: Option<&str>) -> ServiceResponse { + async fn delete_user( + &self, + user_id_or_username: &str, + pat: Option<&str>, + ) -> ServiceResponse { let req = test::TestRequest::delete() .uri(&format!("/v2/user/{}", user_id_or_username)) .append_pat(pat) diff --git a/apps/labrinth/tests/common/api_v2/version.rs b/apps/labrinth/tests/common/api_v2/version.rs index b4b97bfb5..abeab7e37 100644 --- a/apps/labrinth/tests/common/api_v2/version.rs +++ b/apps/labrinth/tests/common/api_v2/version.rs @@ -7,7 +7,9 @@ use super::{ use crate::{ assert_status, common::{ - api_common::{models::CommonVersion, Api, ApiVersion, AppendsOptionalPat}, + api_common::{ + models::CommonVersion, Api, ApiVersion, AppendsOptionalPat, + }, dummy_data::TestFile, }, }; @@ -33,7 +35,11 @@ pub fn url_encode_json_serialized_vec(elements: &[String]) -> String { } impl ApiV2 { - pub async fn get_version_deserialized(&self, id: &str, pat: Option<&str>) -> LegacyVersion { + pub async fn get_version_deserialized( + &self, + id: &str, + pat: Option<&str>, + ) -> LegacyVersion { let resp = self.get_version(id, pat).await; assert_status!(&resp, StatusCode::OK); test::read_body_json(resp).await @@ -145,7 +151,11 @@ impl ApiVersion for ApiV2 { serde_json::from_value(value).unwrap() } - async fn get_version(&self, id: &str, pat: Option<&str>) -> ServiceResponse { + async fn get_version( + &self, + id: &str, + pat: Option<&str>, + ) -> ServiceResponse { let req = TestRequest::get() .uri(&format!("/v2/version/{id}")) .append_pat(pat) @@ -153,7 +163,11 @@ impl ApiVersion for ApiV2 { self.call(req).await } - async fn get_version_deserialized_common(&self, id: &str, pat: Option<&str>) -> CommonVersion { + async fn get_version_deserialized_common( + &self, + id: &str, + pat: Option<&str>, + ) -> CommonVersion { let resp = self.get_version(id, pat).await; assert_status!(&resp, StatusCode::OK); // First, deserialize to the non-common format (to test the response is valid for this api version) @@ -248,7 +262,8 @@ impl ApiVersion for ApiV2 { let resp = self.get_versions_from_hashes(hashes, algorithm, pat).await; assert_status!(&resp, StatusCode::OK); // First, deserialize to the non-common format (to test the response is valid for this api version) - let v: HashMap = test::read_body_json(resp).await; + let v: HashMap = + test::read_body_json(resp).await; // Then, deserialize to the common format let value = serde_json::to_value(v).unwrap(); serde_json::from_value(value).unwrap() @@ -287,7 +302,14 @@ impl ApiVersion for ApiV2 { pat: Option<&str>, ) -> CommonVersion { let resp = self - .get_update_from_hash(hash, algorithm, loaders, game_versions, version_types, pat) + .get_update_from_hash( + hash, + algorithm, + loaders, + game_versions, + version_types, + pat, + ) .await; assert_status!(&resp, StatusCode::OK); // First, deserialize to the non-common format (to test the response is valid for this api version) @@ -341,7 +363,8 @@ impl ApiVersion for ApiV2 { .await; assert_status!(&resp, StatusCode::OK); // First, deserialize to the non-common format (to test the response is valid for this api version) - let v: HashMap = test::read_body_json(resp).await; + let v: HashMap = + test::read_body_json(resp).await; // Then, deserialize to the common format let value = serde_json::to_value(v).unwrap(); serde_json::from_value(value).unwrap() @@ -364,7 +387,9 @@ impl ApiVersion for ApiV2 { if let Some(game_versions) = game_versions { query_string.push_str(&format!( "&game_versions={}", - urlencoding::encode(&serde_json::to_string(&game_versions).unwrap()) + urlencoding::encode( + &serde_json::to_string(&game_versions).unwrap() + ) )); } if let Some(loaders) = loaders { @@ -448,7 +473,11 @@ impl ApiVersion for ApiV2 { self.call(request).await } - async fn get_versions(&self, version_ids: Vec, pat: Option<&str>) -> ServiceResponse { + async fn get_versions( + &self, + version_ids: Vec, + pat: Option<&str>, + ) -> ServiceResponse { let ids = url_encode_json_serialized_vec(&version_ids); let request = test::TestRequest::get() .uri(&format!("/v2/versions?ids={}", ids)) @@ -491,7 +520,11 @@ impl ApiVersion for ApiV2 { self.call(request).await } - async fn remove_version(&self, version_id: &str, pat: Option<&str>) -> ServiceResponse { + async fn remove_version( + &self, + version_id: &str, + pat: Option<&str>, + ) -> ServiceResponse { let request = test::TestRequest::delete() .uri(&format!("/v2/version/{version_id}")) .append_pat(pat) @@ -499,7 +532,11 @@ impl ApiVersion for ApiV2 { self.call(request).await } - async fn remove_version_file(&self, hash: &str, pat: Option<&str>) -> ServiceResponse { + async fn remove_version_file( + &self, + hash: &str, + pat: Option<&str>, + ) -> ServiceResponse { let request = test::TestRequest::delete() .uri(&format!("/v2/version_file/{hash}")) .append_pat(pat) diff --git a/apps/labrinth/tests/common/api_v3/collections.rs b/apps/labrinth/tests/common/api_v3/collections.rs index 52c453a90..81ca4bb6b 100644 --- a/apps/labrinth/tests/common/api_v3/collections.rs +++ b/apps/labrinth/tests/common/api_v3/collections.rs @@ -34,7 +34,11 @@ impl ApiV3 { self.call(req).await } - pub async fn get_collection(&self, id: &str, pat: Option<&str>) -> ServiceResponse { + pub async fn get_collection( + &self, + id: &str, + pat: Option<&str>, + ) -> ServiceResponse { let req = TestRequest::get() .uri(&format!("/v3/collection/{id}")) .append_pat(pat) @@ -42,13 +46,21 @@ impl ApiV3 { self.call(req).await } - pub async fn get_collection_deserialized(&self, id: &str, pat: Option<&str>) -> Collection { + pub async fn get_collection_deserialized( + &self, + id: &str, + pat: Option<&str>, + ) -> Collection { let resp = self.get_collection(id, pat).await; assert_status!(&resp, StatusCode::OK); test::read_body_json(resp).await } - pub async fn get_collections(&self, ids: &[&str], pat: Option<&str>) -> ServiceResponse { + pub async fn get_collections( + &self, + ids: &[&str], + pat: Option<&str>, + ) -> ServiceResponse { let ids = serde_json::to_string(ids).unwrap(); let req = test::TestRequest::get() .uri(&format!( @@ -60,7 +72,11 @@ impl ApiV3 { self.call(req).await } - pub async fn get_collection_projects(&self, id: &str, pat: Option<&str>) -> ServiceResponse { + pub async fn get_collection_projects( + &self, + id: &str, + pat: Option<&str>, + ) -> ServiceResponse { let req = test::TestRequest::get() .uri(&format!("/v3/collection/{id}/projects")) .append_pat(pat) @@ -122,7 +138,11 @@ impl ApiV3 { } } - pub async fn delete_collection(&self, id: &str, pat: Option<&str>) -> ServiceResponse { + pub async fn delete_collection( + &self, + id: &str, + pat: Option<&str>, + ) -> ServiceResponse { let req = test::TestRequest::delete() .uri(&format!("/v3/collection/{id}")) .append_pat(pat) diff --git a/apps/labrinth/tests/common/api_v3/mod.rs b/apps/labrinth/tests/common/api_v3/mod.rs index caab4ab63..f4a0d889a 100644 --- a/apps/labrinth/tests/common/api_v3/mod.rs +++ b/apps/labrinth/tests/common/api_v3/mod.rs @@ -28,8 +28,11 @@ pub struct ApiV3 { #[async_trait(?Send)] impl ApiBuildable for ApiV3 { async fn build(labrinth_config: LabrinthConfig) -> Self { - let app = App::new().configure(|cfg| labrinth::app_config(cfg, labrinth_config.clone())); - let test_app: Rc = Rc::new(test::init_service(app).await); + let app = App::new().configure(|cfg| { + labrinth::app_config(cfg, labrinth_config.clone()) + }); + let test_app: Rc = + Rc::new(test::init_service(app).await); Self { test_app } } diff --git a/apps/labrinth/tests/common/api_v3/oauth.rs b/apps/labrinth/tests/common/api_v3/oauth.rs index 574620f3d..acd5e1732 100644 --- a/apps/labrinth/tests/common/api_v3/oauth.rs +++ b/apps/labrinth/tests/common/api_v3/oauth.rs @@ -6,7 +6,8 @@ use actix_web::{ test::{self, TestRequest}, }; use labrinth::auth::oauth::{ - OAuthClientAccessRequest, RespondToOAuthClientScopes, TokenRequest, TokenResponse, + OAuthClientAccessRequest, RespondToOAuthClientScopes, TokenRequest, + TokenResponse, }; use reqwest::header::{AUTHORIZATION, LOCATION}; @@ -32,7 +33,8 @@ impl ApiV3 { .await; let flow_id = get_authorize_accept_flow_id(auth_resp).await; let redirect_resp = self.oauth_accept(&flow_id, user_pat).await; - let auth_code = get_auth_code_from_redirect_params(&redirect_resp).await; + let auth_code = + get_auth_code_from_redirect_params(&redirect_resp).await; let token_resp = self .oauth_token(auth_code, None, client_id.to_string(), client_secret) .await; @@ -52,7 +54,11 @@ impl ApiV3 { self.call(req).await } - pub async fn oauth_accept(&self, flow: &str, pat: Option<&str>) -> ServiceResponse { + pub async fn oauth_accept( + &self, + flow: &str, + pat: Option<&str>, + ) -> ServiceResponse { self.call( TestRequest::post() .uri("/_internal/oauth/accept") @@ -65,7 +71,11 @@ impl ApiV3 { .await } - pub async fn oauth_reject(&self, flow: &str, pat: Option<&str>) -> ServiceResponse { + pub async fn oauth_reject( + &self, + flow: &str, + pat: Option<&str>, + ) -> ServiceResponse { self.call( TestRequest::post() .uri("/_internal/oauth/reject") @@ -93,7 +103,11 @@ impl ApiV3 { grant_type: "authorization_code".to_string(), code: auth_code, redirect_uri: original_redirect_uri, - client_id: serde_json::from_str(&format!("\"{}\"", client_id)).unwrap(), + client_id: serde_json::from_str(&format!( + "\"{}\"", + client_id + )) + .unwrap(), }) .to_request(), ) @@ -123,7 +137,9 @@ pub async fn get_authorize_accept_flow_id(response: ServiceResponse) -> String { .flow_id } -pub async fn get_auth_code_from_redirect_params(response: &ServiceResponse) -> String { +pub async fn get_auth_code_from_redirect_params( + response: &ServiceResponse, +) -> String { assert_status!(response, StatusCode::OK); let query_params = get_redirect_location_query_params(response); query_params.get("code").unwrap().to_string() diff --git a/apps/labrinth/tests/common/api_v3/oauth_clients.rs b/apps/labrinth/tests/common/api_v3/oauth_clients.rs index 9bbdacdb6..35b248821 100644 --- a/apps/labrinth/tests/common/api_v3/oauth_clients.rs +++ b/apps/labrinth/tests/common/api_v3/oauth_clients.rs @@ -56,7 +56,11 @@ impl ApiV3 { test::read_body_json(resp).await } - pub async fn get_oauth_client(&self, client_id: String, pat: Option<&str>) -> ServiceResponse { + pub async fn get_oauth_client( + &self, + client_id: String, + pat: Option<&str>, + ) -> ServiceResponse { let req = TestRequest::get() .uri(&format!("/_internal/oauth/app/{}", client_id)) .append_pat(pat) @@ -83,7 +87,11 @@ impl ApiV3 { self.call(req).await } - pub async fn delete_oauth_client(&self, client_id: &str, pat: Option<&str>) -> ServiceResponse { + pub async fn delete_oauth_client( + &self, + client_id: &str, + pat: Option<&str>, + ) -> ServiceResponse { let req = TestRequest::delete() .uri(&format!("/_internal/oauth/app/{}", client_id)) .append_pat(pat) diff --git a/apps/labrinth/tests/common/api_v3/organization.rs b/apps/labrinth/tests/common/api_v3/organization.rs index 7d0293a06..7f405cc03 100644 --- a/apps/labrinth/tests/common/api_v3/organization.rs +++ b/apps/labrinth/tests/common/api_v3/organization.rs @@ -4,7 +4,9 @@ use actix_web::{ test::{self, TestRequest}, }; use bytes::Bytes; -use labrinth::models::{organizations::Organization, users::UserId, v3::projects::Project}; +use labrinth::models::{ + organizations::Organization, users::UserId, v3::projects::Project, +}; use serde_json::json; use crate::{ @@ -34,7 +36,11 @@ impl ApiV3 { self.call(req).await } - pub async fn get_organization(&self, id_or_title: &str, pat: Option<&str>) -> ServiceResponse { + pub async fn get_organization( + &self, + id_or_title: &str, + pat: Option<&str>, + ) -> ServiceResponse { let req = TestRequest::get() .uri(&format!("/v3/organization/{id_or_title}")) .append_pat(pat) diff --git a/apps/labrinth/tests/common/api_v3/project.rs b/apps/labrinth/tests/common/api_v3/project.rs index 61db12181..c59662ff9 100644 --- a/apps/labrinth/tests/common/api_v3/project.rs +++ b/apps/labrinth/tests/common/api_v3/project.rs @@ -43,7 +43,8 @@ impl ApiProject for ApiV3 { modify_json: Option, pat: Option<&str>, ) -> (CommonProject, Vec) { - let creation_data = get_public_project_creation_data(slug, version_jar, modify_json); + let creation_data = + get_public_project_creation_data(slug, version_jar, modify_json); // Add a project. let slug = creation_data.slug.clone(); @@ -98,7 +99,11 @@ impl ApiProject for ApiV3 { self.call(req).await } - async fn remove_project(&self, project_slug_or_id: &str, pat: Option<&str>) -> ServiceResponse { + async fn remove_project( + &self, + project_slug_or_id: &str, + pat: Option<&str>, + ) -> ServiceResponse { let req = test::TestRequest::delete() .uri(&format!("/v3/project/{project_slug_or_id}")) .append_pat(pat) @@ -107,7 +112,11 @@ impl ApiProject for ApiV3 { self.call(req).await } - async fn get_project(&self, id_or_slug: &str, pat: Option<&str>) -> ServiceResponse { + async fn get_project( + &self, + id_or_slug: &str, + pat: Option<&str>, + ) -> ServiceResponse { let req = TestRequest::get() .uri(&format!("/v3/project/{id_or_slug}")) .append_pat(pat) @@ -129,7 +138,11 @@ impl ApiProject for ApiV3 { serde_json::from_value(value).unwrap() } - async fn get_projects(&self, ids_or_slugs: &[&str], pat: Option<&str>) -> ServiceResponse { + async fn get_projects( + &self, + ids_or_slugs: &[&str], + pat: Option<&str>, + ) -> ServiceResponse { let ids_or_slugs = serde_json::to_string(ids_or_slugs).unwrap(); let req = test::TestRequest::get() .uri(&format!( @@ -279,7 +292,11 @@ impl ApiProject for ApiV3 { self.call(req).await } - async fn get_reports(&self, ids: &[&str], pat: Option<&str>) -> ServiceResponse { + async fn get_reports( + &self, + ids: &[&str], + pat: Option<&str>, + ) -> ServiceResponse { let ids_str = serde_json::to_string(ids).unwrap(); let req = test::TestRequest::get() .uri(&format!( @@ -316,7 +333,11 @@ impl ApiProject for ApiV3 { self.call(req).await } - async fn delete_report(&self, id: &str, pat: Option<&str>) -> ServiceResponse { + async fn delete_report( + &self, + id: &str, + pat: Option<&str>, + ) -> ServiceResponse { let req = test::TestRequest::delete() .uri(&format!("/v3/report/{id}")) .append_pat(pat) @@ -414,7 +435,11 @@ impl ApiProject for ApiV3 { self.call(req).await } - async fn get_threads(&self, ids: &[&str], pat: Option<&str>) -> ServiceResponse { + async fn get_threads( + &self, + ids: &[&str], + pat: Option<&str>, + ) -> ServiceResponse { let ids_str = serde_json::to_string(ids).unwrap(); let req = test::TestRequest::get() .uri(&format!( @@ -457,7 +482,11 @@ impl ApiProject for ApiV3 { self.call(req).await } - async fn read_thread(&self, id: &str, pat: Option<&str>) -> ServiceResponse { + async fn read_thread( + &self, + id: &str, + pat: Option<&str>, + ) -> ServiceResponse { let req = test::TestRequest::post() .uri(&format!("/v3/thread/{id}/read")) .append_pat(pat) @@ -466,7 +495,11 @@ impl ApiProject for ApiV3 { self.call(req).await } - async fn delete_thread_message(&self, id: &str, pat: Option<&str>) -> ServiceResponse { + async fn delete_thread_message( + &self, + id: &str, + pat: Option<&str>, + ) -> ServiceResponse { let req = test::TestRequest::delete() .uri(&format!("/v3/message/{id}")) .append_pat(pat) @@ -477,7 +510,11 @@ impl ApiProject for ApiV3 { } impl ApiV3 { - pub async fn get_project_deserialized(&self, id_or_slug: &str, pat: Option<&str>) -> Project { + pub async fn get_project_deserialized( + &self, + id_or_slug: &str, + pat: Option<&str>, + ) -> Project { let resp = self.get_project(id_or_slug, pat).await; assert_status!(&resp, StatusCode::OK); test::read_body_json(resp).await @@ -543,11 +580,13 @@ impl ApiV3 { pat: Option<&str>, ) -> ServiceResponse { let pv_string = if ids_are_version_ids { - let version_string: String = serde_json::to_string(&id_or_slugs).unwrap(); + let version_string: String = + serde_json::to_string(&id_or_slugs).unwrap(); let version_string = urlencoding::encode(&version_string); format!("version_ids={}", version_string) } else { - let projects_string: String = serde_json::to_string(&id_or_slugs).unwrap(); + let projects_string: String = + serde_json::to_string(&id_or_slugs).unwrap(); let projects_string = urlencoding::encode(&projects_string); format!("project_ids={}", projects_string) }; @@ -566,7 +605,10 @@ impl ApiV3 { extra_args.push_str(&format!("&end_date={end_date}")); } if let Some(resolution_minutes) = resolution_minutes { - extra_args.push_str(&format!("&resolution_minutes={}", resolution_minutes)); + extra_args.push_str(&format!( + "&resolution_minutes={}", + resolution_minutes + )); } let req = test::TestRequest::get() diff --git a/apps/labrinth/tests/common/api_v3/request_data.rs b/apps/labrinth/tests/common/api_v3/request_data.rs index 95b3abece..790503b86 100644 --- a/apps/labrinth/tests/common/api_v3/request_data.rs +++ b/apps/labrinth/tests/common/api_v3/request_data.rs @@ -2,7 +2,9 @@ use serde_json::json; use crate::common::{ - api_common::request_data::{ProjectCreationRequestData, VersionCreationRequestData}, + api_common::request_data::{ + ProjectCreationRequestData, VersionCreationRequestData, + }, dummy_data::TestFile, }; use labrinth::{ @@ -15,11 +17,13 @@ pub fn get_public_project_creation_data( version_jar: Option, modify_json: Option, ) -> ProjectCreationRequestData { - let mut json_data = get_public_project_creation_data_json(slug, version_jar.as_ref()); + let mut json_data = + get_public_project_creation_data_json(slug, version_jar.as_ref()); if let Some(modify_json) = modify_json { json_patch::patch(&mut json_data, &modify_json).unwrap(); } - let multipart_data = get_public_creation_data_multipart(&json_data, version_jar.as_ref()); + let multipart_data = + get_public_creation_data_multipart(&json_data, version_jar.as_ref()); ProjectCreationRequestData { slug: slug.to_string(), jar: version_jar, @@ -36,14 +40,18 @@ pub fn get_public_version_creation_data( // and modifies it before it is serialized and sent modify_json: Option, ) -> VersionCreationRequestData { - let mut json_data = - get_public_version_creation_data_json(version_number, ordering, &version_jar); + let mut json_data = get_public_version_creation_data_json( + version_number, + ordering, + &version_jar, + ); json_data["project_id"] = json!(project_id); if let Some(modify_json) = modify_json { json_patch::patch(&mut json_data, &modify_json).unwrap(); } - let multipart_data = get_public_creation_data_multipart(&json_data, Some(&version_jar)); + let multipart_data = + get_public_creation_data_multipart(&json_data, Some(&version_jar)); VersionCreationRequestData { version: version_number.to_string(), jar: Some(version_jar), @@ -116,7 +124,9 @@ pub fn get_public_creation_data_multipart( name: "data".to_string(), filename: None, content_type: Some("application/json".to_string()), - data: MultipartSegmentData::Text(serde_json::to_string(json_data).unwrap()), + data: MultipartSegmentData::Text( + serde_json::to_string(json_data).unwrap(), + ), }; if let Some(jar) = version_jar { diff --git a/apps/labrinth/tests/common/api_v3/tags.rs b/apps/labrinth/tests/common/api_v3/tags.rs index 679f519a1..6fa0b9a5b 100644 --- a/apps/labrinth/tests/common/api_v3/tags.rs +++ b/apps/labrinth/tests/common/api_v3/tags.rs @@ -6,7 +6,8 @@ use actix_web::{ use async_trait::async_trait; use labrinth::routes::v3::tags::{GameData, LoaderData}; use labrinth::{ - database::models::loader_fields::LoaderFieldEnumValue, routes::v3::tags::CategoryData, + database::models::loader_fields::LoaderFieldEnumValue, + routes::v3::tags::CategoryData, }; use crate::{ @@ -50,7 +51,9 @@ impl ApiTags for ApiV3 { self.call(req).await } - async fn get_categories_deserialized_common(&self) -> Vec { + async fn get_categories_deserialized_common( + &self, + ) -> Vec { let resp = self.get_categories().await; assert_status!(&resp, StatusCode::OK); // First, deserialize to the non-common format (to test the response is valid for this api version) @@ -68,7 +71,10 @@ impl ApiV3 { test::read_body_json(resp).await } - pub async fn get_loader_field_variants(&self, loader_field: &str) -> ServiceResponse { + pub async fn get_loader_field_variants( + &self, + loader_field: &str, + ) -> ServiceResponse { let req = TestRequest::get() .uri(&format!("/v3/loader_field?loader_field={}", loader_field)) .append_pat(ADMIN_USER_PAT) diff --git a/apps/labrinth/tests/common/api_v3/team.rs b/apps/labrinth/tests/common/api_v3/team.rs index 54cbeb214..0b188b593 100644 --- a/apps/labrinth/tests/common/api_v3/team.rs +++ b/apps/labrinth/tests/common/api_v3/team.rs @@ -51,7 +51,11 @@ impl ApiV3 { #[async_trait(?Send)] impl ApiTeams for ApiV3 { - async fn get_team_members(&self, id_or_title: &str, pat: Option<&str>) -> ServiceResponse { + async fn get_team_members( + &self, + id_or_title: &str, + pat: Option<&str>, + ) -> ServiceResponse { let req = test::TestRequest::get() .uri(&format!("/v3/team/{id_or_title}/members")) .append_pat(pat) @@ -89,7 +93,11 @@ impl ApiTeams for ApiV3 { self.call(req).await } - async fn get_project_members(&self, id_or_title: &str, pat: Option<&str>) -> ServiceResponse { + async fn get_project_members( + &self, + id_or_title: &str, + pat: Option<&str>, + ) -> ServiceResponse { let req = test::TestRequest::get() .uri(&format!("/v3/project/{id_or_title}/members")) .append_pat(pat) @@ -137,7 +145,11 @@ impl ApiTeams for ApiV3 { serde_json::from_value(value).unwrap() } - async fn join_team(&self, team_id: &str, pat: Option<&str>) -> ServiceResponse { + async fn join_team( + &self, + team_id: &str, + pat: Option<&str>, + ) -> ServiceResponse { let req = test::TestRequest::post() .uri(&format!("/v3/team/{team_id}/join")) .append_pat(pat) @@ -189,7 +201,11 @@ impl ApiTeams for ApiV3 { self.call(req).await } - async fn get_user_notifications(&self, user_id: &str, pat: Option<&str>) -> ServiceResponse { + async fn get_user_notifications( + &self, + user_id: &str, + pat: Option<&str>, + ) -> ServiceResponse { let req = test::TestRequest::get() .uri(&format!("/v3/user/{user_id}/notifications")) .append_pat(pat) @@ -211,7 +227,11 @@ impl ApiTeams for ApiV3 { serde_json::from_value(value).unwrap() } - async fn get_notification(&self, notification_id: &str, pat: Option<&str>) -> ServiceResponse { + async fn get_notification( + &self, + notification_id: &str, + pat: Option<&str>, + ) -> ServiceResponse { let req = test::TestRequest::get() .uri(&format!("/v3/notification/{notification_id}")) .append_pat(pat) diff --git a/apps/labrinth/tests/common/api_v3/user.rs b/apps/labrinth/tests/common/api_v3/user.rs index 5bfaf780b..9e2c9f7fd 100644 --- a/apps/labrinth/tests/common/api_v3/user.rs +++ b/apps/labrinth/tests/common/api_v3/user.rs @@ -7,7 +7,11 @@ use super::ApiV3; #[async_trait(?Send)] impl ApiUser for ApiV3 { - async fn get_user(&self, user_id_or_username: &str, pat: Option<&str>) -> ServiceResponse { + async fn get_user( + &self, + user_id_or_username: &str, + pat: Option<&str>, + ) -> ServiceResponse { let req = test::TestRequest::get() .uri(&format!("/v3/user/{}", user_id_or_username)) .append_pat(pat) @@ -38,7 +42,11 @@ impl ApiUser for ApiV3 { self.call(req).await } - async fn delete_user(&self, user_id_or_username: &str, pat: Option<&str>) -> ServiceResponse { + async fn delete_user( + &self, + user_id_or_username: &str, + pat: Option<&str>, + ) -> ServiceResponse { let req = test::TestRequest::delete() .uri(&format!("/v3/user/{}", user_id_or_username)) .append_pat(pat) diff --git a/apps/labrinth/tests/common/api_v3/version.rs b/apps/labrinth/tests/common/api_v3/version.rs index 005a61e38..c563396ef 100644 --- a/apps/labrinth/tests/common/api_v3/version.rs +++ b/apps/labrinth/tests/common/api_v3/version.rs @@ -7,7 +7,9 @@ use super::{ use crate::{ assert_status, common::{ - api_common::{models::CommonVersion, Api, ApiVersion, AppendsOptionalPat}, + api_common::{ + models::CommonVersion, Api, ApiVersion, AppendsOptionalPat, + }, dummy_data::TestFile, }, }; @@ -60,7 +62,11 @@ impl ApiV3 { test::read_body_json(version).await } - pub async fn get_version_deserialized(&self, id: &str, pat: Option<&str>) -> Version { + pub async fn get_version_deserialized( + &self, + id: &str, + pat: Option<&str>, + ) -> Version { let resp = self.get_version(id, pat).await; assert_status!(&resp, StatusCode::OK); test::read_body_json(resp).await @@ -160,7 +166,11 @@ impl ApiVersion for ApiV3 { serde_json::from_value(value).unwrap() } - async fn get_version(&self, id: &str, pat: Option<&str>) -> ServiceResponse { + async fn get_version( + &self, + id: &str, + pat: Option<&str>, + ) -> ServiceResponse { let req = TestRequest::get() .uri(&format!("/v3/version/{id}")) .append_pat(pat) @@ -168,7 +178,11 @@ impl ApiVersion for ApiV3 { self.call(req).await } - async fn get_version_deserialized_common(&self, id: &str, pat: Option<&str>) -> CommonVersion { + async fn get_version_deserialized_common( + &self, + id: &str, + pat: Option<&str>, + ) -> CommonVersion { let resp = self.get_version(id, pat).await; assert_status!(&resp, StatusCode::OK); // First, deserialize to the non-common format (to test the response is valid for this api version) @@ -288,7 +302,8 @@ impl ApiVersion for ApiV3 { }); } if let Some(version_types) = version_types { - json["version_types"] = serde_json::to_value(version_types).unwrap(); + json["version_types"] = + serde_json::to_value(version_types).unwrap(); } let req = test::TestRequest::post() @@ -311,7 +326,14 @@ impl ApiVersion for ApiV3 { pat: Option<&str>, ) -> CommonVersion { let resp = self - .get_update_from_hash(hash, algorithm, loaders, game_versions, version_types, pat) + .get_update_from_hash( + hash, + algorithm, + loaders, + game_versions, + version_types, + pat, + ) .await; assert_status!(&resp, StatusCode::OK); // First, deserialize to the non-common format (to test the response is valid for this api version) @@ -338,10 +360,12 @@ impl ApiVersion for ApiV3 { json["loaders"] = serde_json::to_value(loaders).unwrap(); } if let Some(game_versions) = game_versions { - json["game_versions"] = serde_json::to_value(game_versions).unwrap(); + json["game_versions"] = + serde_json::to_value(game_versions).unwrap(); } if let Some(version_types) = version_types { - json["version_types"] = serde_json::to_value(version_types).unwrap(); + json["version_types"] = + serde_json::to_value(version_types).unwrap(); } let req = test::TestRequest::post() @@ -396,7 +420,9 @@ impl ApiVersion for ApiV3 { if let Some(game_versions) = game_versions { query_string.push_str(&format!( "&game_versions={}", - urlencoding::encode(&serde_json::to_string(&game_versions).unwrap()) + urlencoding::encode( + &serde_json::to_string(&game_versions).unwrap() + ) )); } if let Some(loaders) = loaders { @@ -480,7 +506,11 @@ impl ApiVersion for ApiV3 { self.call(request).await } - async fn get_versions(&self, version_ids: Vec, pat: Option<&str>) -> ServiceResponse { + async fn get_versions( + &self, + version_ids: Vec, + pat: Option<&str>, + ) -> ServiceResponse { let ids = url_encode_json_serialized_vec(&version_ids); let request = test::TestRequest::get() .uri(&format!("/v3/versions?ids={}", ids)) @@ -526,7 +556,11 @@ impl ApiVersion for ApiV3 { self.call(request).await } - async fn remove_version(&self, version_id: &str, pat: Option<&str>) -> ServiceResponse { + async fn remove_version( + &self, + version_id: &str, + pat: Option<&str>, + ) -> ServiceResponse { let request = test::TestRequest::delete() .uri(&format!( "/v3/version/{version_id}", @@ -537,7 +571,11 @@ impl ApiVersion for ApiV3 { self.call(request).await } - async fn remove_version_file(&self, hash: &str, pat: Option<&str>) -> ServiceResponse { + async fn remove_version_file( + &self, + hash: &str, + pat: Option<&str>, + ) -> ServiceResponse { let request = test::TestRequest::delete() .uri(&format!("/v3/version_file/{hash}")) .append_pat(pat) diff --git a/apps/labrinth/tests/common/asserts.rs b/apps/labrinth/tests/common/asserts.rs index bfa22fd1f..f4b7330e7 100644 --- a/apps/labrinth/tests/common/asserts.rs +++ b/apps/labrinth/tests/common/asserts.rs @@ -38,7 +38,10 @@ pub fn assert_version_ids(versions: &[Version], expected_ids: Vec) { assert_eq!(version_ids, expected_ids); } -pub fn assert_common_version_ids(versions: &[CommonVersion], expected_ids: Vec) { +pub fn assert_common_version_ids( + versions: &[CommonVersion], + expected_ids: Vec, +) { let version_ids = versions .iter() .map(|v| get_json_val_str(v.id)) diff --git a/apps/labrinth/tests/common/database.rs b/apps/labrinth/tests/common/database.rs index cc56f0259..95e263c94 100644 --- a/apps/labrinth/tests/common/database.rs +++ b/apps/labrinth/tests/common/database.rs @@ -55,13 +55,15 @@ impl TemporaryDatabase { let temp_database_name = generate_random_name("labrinth_tests_db_"); println!("Creating temporary database: {}", &temp_database_name); - let database_url = dotenvy::var("DATABASE_URL").expect("No database URL"); + let database_url = + dotenvy::var("DATABASE_URL").expect("No database URL"); // Create the temporary (and template datbase, if needed) Self::create_temporary(&database_url, &temp_database_name).await; // Pool to the temporary database - let mut temporary_url = Url::parse(&database_url).expect("Invalid database URL"); + let mut temporary_url = + Url::parse(&database_url).expect("Invalid database URL"); temporary_url.set_path(&format!("/{}", &temp_database_name)); let temp_db_url = temporary_url.to_string(); @@ -86,7 +88,8 @@ impl TemporaryDatabase { let redis_pool = RedisPool::new(Some(temp_database_name.clone())); // Create new meilisearch config - let search_config = search::SearchConfig::new(Some(temp_database_name.clone())); + let search_config = + search::SearchConfig::new(Some(temp_database_name.clone())); Self { pool, database_name: temp_database_name, @@ -110,10 +113,11 @@ impl TemporaryDatabase { loop { // Try to acquire an advisory lock - let lock_acquired: bool = sqlx::query_scalar("SELECT pg_try_advisory_lock(1)") - .fetch_one(&main_pool) - .await - .unwrap(); + let lock_acquired: bool = + sqlx::query_scalar("SELECT pg_try_advisory_lock(1)") + .fetch_one(&main_pool) + .await + .unwrap(); if lock_acquired { // Create the db template if it doesn't exist @@ -129,8 +133,10 @@ impl TemporaryDatabase { } // Switch to template - let url = dotenvy::var("DATABASE_URL").expect("No database URL"); - let mut template_url = Url::parse(&url).expect("Invalid database URL"); + let url = + dotenvy::var("DATABASE_URL").expect("No database URL"); + let mut template_url = + Url::parse(&url).expect("Invalid database URL"); template_url.set_path(&format!("/{}", TEMPLATE_DATABASE_NAME)); let pool = PgPool::connect(template_url.as_str()) @@ -138,19 +144,22 @@ impl TemporaryDatabase { .expect("Connection to database failed"); // Check if dummy data exists- a fake 'dummy_data' table is created if it does - let mut dummy_data_exists: bool = - sqlx::query_scalar("SELECT to_regclass('dummy_data') IS NOT NULL") - .fetch_one(&pool) - .await - .unwrap(); + let mut dummy_data_exists: bool = sqlx::query_scalar( + "SELECT to_regclass('dummy_data') IS NOT NULL", + ) + .fetch_one(&pool) + .await + .unwrap(); if dummy_data_exists { // Check if the dummy data needs to be updated - let dummy_data_update = - sqlx::query_scalar::<_, i64>("SELECT update_id FROM dummy_data") - .fetch_optional(&pool) - .await - .unwrap(); - let needs_update = !dummy_data_update.is_some_and(|d| d == DUMMY_DATA_UPDATE); + let dummy_data_update = sqlx::query_scalar::<_, i64>( + "SELECT update_id FROM dummy_data", + ) + .fetch_optional(&pool) + .await + .unwrap(); + let needs_update = !dummy_data_update + .is_some_and(|d| d == DUMMY_DATA_UPDATE); if needs_update { println!("Dummy data updated, so template DB tables will be dropped and re-created"); // Drop all tables in the database so they can be re-created and later filled with updated dummy data @@ -179,7 +188,8 @@ impl TemporaryDatabase { redis_pool: RedisPool::new(Some(name.clone())), search_config: search::SearchConfig::new(Some(name)), }; - let setup_api = TestEnvironment::::build_setup_api(&db).await; + let setup_api = + TestEnvironment::::build_setup_api(&db).await; dummy_data::add_dummy_data(&setup_api, db.clone()).await; db.pool.close().await; } @@ -215,7 +225,8 @@ impl TemporaryDatabase { // If a temporary db is created, it must be cleaned up with cleanup. // This means that dbs will only 'remain' if a test fails (for examination of the db), and will be cleaned up otherwise. pub async fn cleanup(mut self) { - let database_url = dotenvy::var("DATABASE_URL").expect("No database URL"); + let database_url = + dotenvy::var("DATABASE_URL").expect("No database URL"); self.pool.close().await; self.pool = PgPool::connect(&database_url) @@ -234,7 +245,8 @@ impl TemporaryDatabase { .unwrap(); // Execute the deletion query asynchronously - let drop_db_query = format!("DROP DATABASE IF EXISTS {}", &self.database_name); + let drop_db_query = + format!("DROP DATABASE IF EXISTS {}", &self.database_name); sqlx::query(&drop_db_query) .execute(&self.pool) .await diff --git a/apps/labrinth/tests/common/dummy_data.rs b/apps/labrinth/tests/common/dummy_data.rs index 853d08b4e..5dbe21a01 100644 --- a/apps/labrinth/tests/common/dummy_data.rs +++ b/apps/labrinth/tests/common/dummy_data.rs @@ -98,14 +98,16 @@ impl TestFile { let mut zip = ZipWriter::new(&mut cursor); zip.start_file( "fabric.mod.json", - FileOptions::default().compression_method(CompressionMethod::Stored), + FileOptions::default() + .compression_method(CompressionMethod::Stored), ) .unwrap(); zip.write_all(fabric_mod_json.as_bytes()).unwrap(); zip.start_file( "META-INF/mods.toml", - FileOptions::default().compression_method(CompressionMethod::Stored), + FileOptions::default() + .compression_method(CompressionMethod::Stored), ) .unwrap(); zip.write_all(fabric_mod_json.as_bytes()).unwrap(); @@ -118,7 +120,8 @@ impl TestFile { } pub fn build_random_mrpack() -> Self { - let filename = format!("random-modpack-{}.mrpack", rand::random::()); + let filename = + format!("random-modpack-{}.mrpack", rand::random::()); let modrinth_index_json = serde_json::json!({ "formatVersion": 1, @@ -156,7 +159,8 @@ impl TestFile { let mut zip = ZipWriter::new(&mut cursor); zip.start_file( "modrinth.index.json", - FileOptions::default().compression_method(CompressionMethod::Stored), + FileOptions::default() + .compression_method(CompressionMethod::Stored), ) .unwrap(); zip.write_all(modrinth_index_json.as_bytes()).unwrap(); @@ -217,7 +221,8 @@ impl DummyData { project_id_parsed: project_alpha.id, version_id: project_alpha_version.id.to_string(), thread_id: project_alpha.thread_id.to_string(), - file_hash: project_alpha_version.files[0].hashes["sha1"].clone(), + file_hash: project_alpha_version.files[0].hashes["sha1"] + .clone(), }, project_beta: DummyProjectBeta { @@ -349,7 +354,10 @@ pub async fn add_project_alpha(api: &ApiV3) -> (Project, Version) { ) .await; let alpha_project = api - .get_project_deserialized(project.id.to_string().as_str(), USER_USER_PAT) + .get_project_deserialized( + project.id.to_string().as_str(), + USER_USER_PAT, + ) .await; let alpha_version = api .get_version_deserialized( @@ -484,15 +492,22 @@ impl TestFile { pub fn bytes(&self) -> Vec { match self { TestFile::DummyProjectAlpha => { - include_bytes!("../../tests/files/dummy-project-alpha.jar").to_vec() + include_bytes!("../../tests/files/dummy-project-alpha.jar") + .to_vec() } TestFile::DummyProjectBeta => { - include_bytes!("../../tests/files/dummy-project-beta.jar").to_vec() + include_bytes!("../../tests/files/dummy-project-beta.jar") + .to_vec() + } + TestFile::BasicMod => { + include_bytes!("../../tests/files/basic-mod.jar").to_vec() + } + TestFile::BasicZip => { + include_bytes!("../../tests/files/simple-zip.zip").to_vec() } - TestFile::BasicMod => include_bytes!("../../tests/files/basic-mod.jar").to_vec(), - TestFile::BasicZip => include_bytes!("../../tests/files/simple-zip.zip").to_vec(), TestFile::BasicModDifferent => { - include_bytes!("../../tests/files/basic-mod-different.jar").to_vec() + include_bytes!("../../tests/files/basic-mod-different.jar") + .to_vec() } TestFile::BasicModRandom { bytes, .. } => bytes.clone(), TestFile::BasicModpackRandom { bytes, .. } => bytes.clone(), @@ -524,7 +539,9 @@ impl TestFile { TestFile::BasicZip => Some("application/zip"), - TestFile::BasicModpackRandom { .. } => Some("application/x-modrinth-modpack+zip"), + TestFile::BasicModpackRandom { .. } => { + Some("application/x-modrinth-modpack+zip") + } } .map(|s| s.to_string()) } @@ -547,7 +564,9 @@ impl DummyImage { pub fn bytes(&self) -> Vec { match self { - DummyImage::SmallIcon => include_bytes!("../../tests/files/200x200.png").to_vec(), + DummyImage::SmallIcon => { + include_bytes!("../../tests/files/200x200.png").to_vec() + } } } diff --git a/apps/labrinth/tests/common/environment.rs b/apps/labrinth/tests/common/environment.rs index 8ac0f8c78..7747916ac 100644 --- a/apps/labrinth/tests/common/environment.rs +++ b/apps/labrinth/tests/common/environment.rs @@ -19,19 +19,23 @@ pub async fn with_test_environment( Fut: Future, A: ApiBuildable + 'static, { - let test_env: TestEnvironment = TestEnvironment::build(max_connections).await; + let test_env: TestEnvironment = + TestEnvironment::build(max_connections).await; let db = test_env.db.clone(); f(test_env).await; db.cleanup().await; } -pub async fn with_test_environment_all(max_connections: Option, f: F) -where +pub async fn with_test_environment_all( + max_connections: Option, + f: F, +) where Fut: Future, F: Fn(TestEnvironment) -> Fut, { println!("Test environment: API v3"); - let test_env_api_v3 = TestEnvironment::::build(max_connections).await; + let test_env_api_v3 = + TestEnvironment::::build(max_connections).await; let test_env_api_v3 = TestEnvironment { db: test_env_api_v3.db.clone(), api: GenericApi::V3(test_env_api_v3.api), @@ -43,7 +47,8 @@ where db.cleanup().await; println!("Test environment: API v2"); - let test_env_api_v2 = TestEnvironment::::build(max_connections).await; + let test_env_api_v2 = + TestEnvironment::::build(max_connections).await; let test_env_api_v2 = TestEnvironment { db: test_env_api_v2.db.clone(), api: GenericApi::V2(test_env_api_v2.api), @@ -139,7 +144,11 @@ pub trait LocalService { &self, req: actix_http::Request, ) -> std::pin::Pin< - Box>>, + Box< + dyn std::future::Future< + Output = Result, + >, + >, >; } impl LocalService for S @@ -155,7 +164,11 @@ where &self, req: actix_http::Request, ) -> std::pin::Pin< - Box>>, + Box< + dyn std::future::Future< + Output = Result, + >, + >, > { Box::pin(self.call(req)) } diff --git a/apps/labrinth/tests/common/mod.rs b/apps/labrinth/tests/common/mod.rs index 43490cfba..840bad667 100644 --- a/apps/labrinth/tests/common/mod.rs +++ b/apps/labrinth/tests/common/mod.rs @@ -32,7 +32,8 @@ pub async fn setup(db: &database::TemporaryDatabase) -> LabrinthConfig { Arc::new(file_hosting::MockHost::new()); let mut clickhouse = clickhouse::init_client().await.unwrap(); - let maxmind_reader = Arc::new(queue::maxmind::MaxMindIndexer::new().await.unwrap()); + let maxmind_reader = + Arc::new(queue::maxmind::MaxMindIndexer::new().await.unwrap()); labrinth::app_setup( pool.clone(), diff --git a/apps/labrinth/tests/common/pats.rs b/apps/labrinth/tests/common/pats.rs index d63517cfc..0f5662155 100644 --- a/apps/labrinth/tests/common/pats.rs +++ b/apps/labrinth/tests/common/pats.rs @@ -11,7 +11,11 @@ use super::database::TemporaryDatabase; // Creates a PAT with the given scopes, and returns the access token // Interfacing with the db directly, rather than using a ourte, // allows us to test with scopes that are not allowed to be created by PATs -pub async fn create_test_pat(scopes: Scopes, user_id: i64, db: &TemporaryDatabase) -> String { +pub async fn create_test_pat( + scopes: Scopes, + user_id: i64, + db: &TemporaryDatabase, +) -> String { let mut transaction = db.pool.begin().await.unwrap(); let id = generate_pat_id(&mut transaction).await.unwrap(); let pat = database::models::pat_item::PersonalAccessToken { diff --git a/apps/labrinth/tests/common/permissions.rs b/apps/labrinth/tests/common/permissions.rs index a368d5ab5..02b119353 100644 --- a/apps/labrinth/tests/common/permissions.rs +++ b/apps/labrinth/tests/common/permissions.rs @@ -97,7 +97,8 @@ impl<'a, A: Api> PermissionsTest<'a, A> { failure_organization_permissions: Option, ) -> Self { self.failure_project_permissions = failure_project_permissions; - self.failure_organization_permissions = failure_organization_permissions; + self.failure_organization_permissions = + failure_organization_permissions; self } @@ -136,19 +137,28 @@ impl<'a, A: Api> PermissionsTest<'a, A> { mut self, allowed_failure_codes: impl IntoIterator, ) -> Self { - self.allowed_failure_codes = allowed_failure_codes.into_iter().collect(); + self.allowed_failure_codes = + allowed_failure_codes.into_iter().collect(); self } // If an existing project or organization is intended to be used // We will not create a new project, and will use the given project ID // (But will still add the user to the project's team) - pub fn with_existing_project(mut self, project_id: &str, team_id: &str) -> Self { + pub fn with_existing_project( + mut self, + project_id: &str, + team_id: &str, + ) -> Self { self.project_id = Some(project_id.to_string()); self.project_team_id = Some(team_id.to_string()); self } - pub fn with_existing_organization(mut self, organization_id: &str, team_id: &str) -> Self { + pub fn with_existing_organization( + mut self, + organization_id: &str, + team_id: &str, + ) -> Self { self.organization_id = Some(organization_id.to_string()); self.organization_team_id = Some(team_id.to_string()); self @@ -176,14 +186,15 @@ impl<'a, A: Api> PermissionsTest<'a, A> { organization_team_id: None, }; - let (project_id, team_id) = if self.project_id.is_some() && self.project_team_id.is_some() { - ( - self.project_id.clone().unwrap(), - self.project_team_id.clone().unwrap(), - ) - } else { - create_dummy_project(&test_env.setup_api).await - }; + let (project_id, team_id) = + if self.project_id.is_some() && self.project_team_id.is_some() { + ( + self.project_id.clone().unwrap(), + self.project_team_id.clone().unwrap(), + ) + } else { + create_dummy_project(&test_env.setup_api).await + }; add_user_to_team( self.user_id, @@ -299,7 +310,8 @@ impl<'a, A: Api> PermissionsTest<'a, A> { // If the remove_user flag is set, remove the user from the project // Relevant for existing projects/users if self.remove_user { - remove_user_from_team(self.user_id, &team_id, &test_env.setup_api).await; + remove_user_from_team(self.user_id, &team_id, &test_env.setup_api) + .await; } Ok(()) } @@ -326,15 +338,16 @@ impl<'a, A: Api> PermissionsTest<'a, A> { organization_team_id: None, }; - let (organization_id, team_id) = - if self.organization_id.is_some() && self.organization_team_id.is_some() { - ( - self.organization_id.clone().unwrap(), - self.organization_team_id.clone().unwrap(), - ) - } else { - create_dummy_org(&test_env.setup_api).await - }; + let (organization_id, team_id) = if self.organization_id.is_some() + && self.organization_team_id.is_some() + { + ( + self.organization_id.clone().unwrap(), + self.organization_team_id.clone().unwrap(), + ) + } else { + create_dummy_org(&test_env.setup_api).await + }; add_user_to_team( self.user_id, @@ -395,7 +408,8 @@ impl<'a, A: Api> PermissionsTest<'a, A> { // If the remove_user flag is set, remove the user from the organization // Relevant for existing projects/users if self.remove_user { - remove_user_from_team(self.user_id, &team_id, &test_env.setup_api).await; + remove_user_from_team(self.user_id, &team_id, &test_env.setup_api) + .await; } Ok(()) } @@ -426,7 +440,8 @@ impl<'a, A: Api> PermissionsTest<'a, A> { // This should always fail, regardless of permissions // (As we are testing permissions-based failures) let test_1 = async { - let (project_id, team_id) = create_dummy_project(&test_env.setup_api).await; + let (project_id, team_id) = + create_dummy_project(&test_env.setup_api).await; let resp = req_gen(PermissionsTestContext { test_pat: None, @@ -466,7 +481,8 @@ impl<'a, A: Api> PermissionsTest<'a, A> { // TEST 2: Failure // Random user, unaffiliated with the project, with no permissions let test_2 = async { - let (project_id, team_id) = create_dummy_project(&test_env.setup_api).await; + let (project_id, team_id) = + create_dummy_project(&test_env.setup_api).await; let resp = req_gen(PermissionsTestContext { test_pat: self.user_pat.map(|s| s.to_string()), @@ -506,7 +522,8 @@ impl<'a, A: Api> PermissionsTest<'a, A> { // TEST 3: Failure // User affiliated with the project, with failure permissions let test_3 = async { - let (project_id, team_id) = create_dummy_project(&test_env.setup_api).await; + let (project_id, team_id) = + create_dummy_project(&test_env.setup_api).await; add_user_to_team( self.user_id, self.user_pat, @@ -555,7 +572,8 @@ impl<'a, A: Api> PermissionsTest<'a, A> { // TEST 4: Success // User affiliated with the project, with the given permissions let test_4 = async { - let (project_id, team_id) = create_dummy_project(&test_env.setup_api).await; + let (project_id, team_id) = + create_dummy_project(&test_env.setup_api).await; add_user_to_team( self.user_id, self.user_pat, @@ -601,10 +619,16 @@ impl<'a, A: Api> PermissionsTest<'a, A> { // Project has an organization // User affiliated with the project's org, with default failure permissions let test_5 = async { - let (project_id, team_id) = create_dummy_project(&test_env.setup_api).await; + let (project_id, team_id) = + create_dummy_project(&test_env.setup_api).await; let (organization_id, organization_team_id) = create_dummy_org(&test_env.setup_api).await; - add_project_to_org(&test_env.setup_api, &project_id, &organization_id).await; + add_project_to_org( + &test_env.setup_api, + &project_id, + &organization_id, + ) + .await; add_user_to_team( self.user_id, self.user_pat, @@ -654,10 +678,16 @@ impl<'a, A: Api> PermissionsTest<'a, A> { // Project has an organization // User affiliated with the project's org, with the default success let test_6 = async { - let (project_id, team_id) = create_dummy_project(&test_env.setup_api).await; + let (project_id, team_id) = + create_dummy_project(&test_env.setup_api).await; let (organization_id, organization_team_id) = create_dummy_org(&test_env.setup_api).await; - add_project_to_org(&test_env.setup_api, &project_id, &organization_id).await; + add_project_to_org( + &test_env.setup_api, + &project_id, + &organization_id, + ) + .await; add_user_to_team( self.user_id, self.user_pat, @@ -704,10 +734,16 @@ impl<'a, A: Api> PermissionsTest<'a, A> { // User affiliated with the project's org (even can have successful permissions!) // User overwritten on the project team with failure permissions let test_7 = async { - let (project_id, team_id) = create_dummy_project(&test_env.setup_api).await; + let (project_id, team_id) = + create_dummy_project(&test_env.setup_api).await; let (organization_id, organization_team_id) = create_dummy_org(&test_env.setup_api).await; - add_project_to_org(&test_env.setup_api, &project_id, &organization_id).await; + add_project_to_org( + &test_env.setup_api, + &project_id, + &organization_id, + ) + .await; add_user_to_team( self.user_id, self.user_pat, @@ -767,10 +803,16 @@ impl<'a, A: Api> PermissionsTest<'a, A> { // User affiliated with the project's org with default failure permissions // User overwritten to the project with the success permissions let test_8 = async { - let (project_id, team_id) = create_dummy_project(&test_env.setup_api).await; + let (project_id, team_id) = + create_dummy_project(&test_env.setup_api).await; let (organization_id, organization_team_id) = create_dummy_org(&test_env.setup_api).await; - add_project_to_org(&test_env.setup_api, &project_id, &organization_id).await; + add_project_to_org( + &test_env.setup_api, + &project_id, + &organization_id, + ) + .await; add_user_to_team( self.user_id, self.user_pat, @@ -822,8 +864,10 @@ impl<'a, A: Api> PermissionsTest<'a, A> { Ok(()) }; - tokio::try_join!(test_1, test_2, test_3, test_4, test_5, test_6, test_7, test_8) - .map_err(|e| e)?; + tokio::try_join!( + test_1, test_2, test_3, test_4, test_5, test_6, test_7, test_8 + ) + .map_err(|e| e)?; Ok(()) } @@ -1012,7 +1056,12 @@ async fn create_dummy_org(setup_api: &ApiV3) -> (String, String) { let slug = generate_random_name("test_org"); let resp = setup_api - .create_organization("Example org", &slug, "Example description.", ADMIN_USER_PAT) + .create_organization( + "Example org", + &slug, + "Example description.", + ADMIN_USER_PAT, + ) .await; assert!(resp.status().is_success()); @@ -1025,7 +1074,11 @@ async fn create_dummy_org(setup_api: &ApiV3) -> (String, String) { (organizaion_id, team_id) } -async fn add_project_to_org(setup_api: &ApiV3, project_id: &str, organization_id: &str) { +async fn add_project_to_org( + setup_api: &ApiV3, + project_id: &str, + organization_id: &str, +) { let resp = setup_api .organization_add_project(organization_id, project_id, ADMIN_USER_PAT) .await; @@ -1081,7 +1134,11 @@ async fn modify_user_team_permissions( assert!(resp.status().is_success()); } -async fn remove_user_from_team(user_id: &str, team_id: &str, setup_api: &ApiV3) { +async fn remove_user_from_team( + user_id: &str, + team_id: &str, + setup_api: &ApiV3, +) { // Send invitation to user let resp = setup_api .remove_from_team(team_id, user_id, ADMIN_USER_PAT) @@ -1102,7 +1159,9 @@ async fn get_project_permissions( let organization_id = project.organization.map(|id| id.to_string()); let organization = match organization_id { - Some(id) => Some(setup_api.get_organization_deserialized(&id, user_pat).await), + Some(id) => { + Some(setup_api.get_organization_deserialized(&id, user_pat).await) + } None => None, }; @@ -1117,7 +1176,10 @@ async fn get_project_permissions( let organization_members = match organization { Some(org) => Some( setup_api - .get_team_members_deserialized(&org.team_id.to_string(), user_pat) + .get_team_members_deserialized( + &org.team_id.to_string(), + user_pat, + ) .await, ), None => None, diff --git a/apps/labrinth/tests/common/scopes.rs b/apps/labrinth/tests/common/scopes.rs index 7839c2116..637914b81 100644 --- a/apps/labrinth/tests/common/scopes.rs +++ b/apps/labrinth/tests/common/scopes.rs @@ -4,8 +4,8 @@ use futures::Future; use labrinth::models::pats::Scopes; use super::{ - api_common::Api, database::USER_USER_ID_PARSED, environment::TestEnvironment, - pats::create_test_pat, + api_common::Api, database::USER_USER_ID_PARSED, + environment::TestEnvironment, pats::create_test_pat, }; // A reusable test type that works for any scope test testing an endpoint that: @@ -74,10 +74,13 @@ impl<'a, A: Api> ScopeTest<'a, A> { .failure_scopes .unwrap_or(Scopes::all() ^ success_scopes); let access_token_all_others = - create_test_pat(failure_scopes, self.user_id, &self.test_env.db).await; + create_test_pat(failure_scopes, self.user_id, &self.test_env.db) + .await; // Create a PAT with the success scopes - let access_token = create_test_pat(success_scopes, self.user_id, &self.test_env.db).await; + let access_token = + create_test_pat(success_scopes, self.user_id, &self.test_env.db) + .await; // Perform test twice, once with each PAT // the first time, we expect a 401 (or known failure code) diff --git a/apps/labrinth/tests/common/search.rs b/apps/labrinth/tests/common/search.rs index ee8730219..6bfc8a504 100644 --- a/apps/labrinth/tests/common/search.rs +++ b/apps/labrinth/tests/common/search.rs @@ -16,11 +16,14 @@ use crate::{ use super::{api_v3::ApiV3, environment::TestEnvironment}; -pub async fn setup_search_projects(test_env: &TestEnvironment) -> Arc> { +pub async fn setup_search_projects( + test_env: &TestEnvironment, +) -> Arc> { // Test setup and dummy data let api = &test_env.api; let test_name = test_env.db.database_name.clone(); - let zeta_organization_id = &test_env.dummy.organization_zeta.organization_id; + let zeta_organization_id = + &test_env.dummy.organization_zeta.organization_id; // Add dummy projects of various categories for searchability let mut project_creation_futures = vec![]; @@ -39,7 +42,8 @@ pub async fn setup_search_projects(test_env: &TestEnvironment) -> Arc| async move { - // 3 errors should have 404 as non-blank body, for missing resources - let api = &test_env.api; - let resp = api.get_project("does-not-exist", USER_USER_PAT).await; - assert_status!(&resp, StatusCode::NOT_FOUND); - let body = test::read_body(resp).await; - let empty_bytes = Bytes::from_static(b""); - assert_ne!(body, empty_bytes); - }) + with_test_environment( + None, + |test_env: TestEnvironment| async move { + // 3 errors should have 404 as non-blank body, for missing resources + let api = &test_env.api; + let resp = api.get_project("does-not-exist", USER_USER_PAT).await; + assert_status!(&resp, StatusCode::NOT_FOUND); + let body = test::read_body(resp).await; + let empty_bytes = Bytes::from_static(b""); + assert_ne!(body, empty_bytes); + }, + ) .await; } diff --git a/apps/labrinth/tests/games.rs b/apps/labrinth/tests/games.rs index 75c5671a0..c078f9946 100644 --- a/apps/labrinth/tests/games.rs +++ b/apps/labrinth/tests/games.rs @@ -9,18 +9,21 @@ mod common; #[actix_rt::test] async fn get_games() { - with_test_environment(None, |test_env: TestEnvironment| async move { - let api = test_env.api; + with_test_environment( + None, + |test_env: TestEnvironment| async move { + let api = test_env.api; - let games = api.get_games_deserialized().await; + let games = api.get_games_deserialized().await; - // There should be 2 games in the dummy data - assert_eq!(games.len(), 2); - assert_eq!(games[0].name, "minecraft-java"); - assert_eq!(games[1].name, "minecraft-bedrock"); + // There should be 2 games in the dummy data + assert_eq!(games.len(), 2); + assert_eq!(games[0].name, "minecraft-java"); + assert_eq!(games[1].name, "minecraft-bedrock"); - assert_eq!(games[0].slug, "minecraft-java"); - assert_eq!(games[1].slug, "minecraft-bedrock"); - }) + assert_eq!(games[0].slug, "minecraft-java"); + assert_eq!(games[1].slug, "minecraft-bedrock"); + }, + ) .await; } diff --git a/apps/labrinth/tests/loader_fields.rs b/apps/labrinth/tests/loader_fields.rs index c6d3f16eb..78ee67a40 100644 --- a/apps/labrinth/tests/loader_fields.rs +++ b/apps/labrinth/tests/loader_fields.rs @@ -13,7 +13,9 @@ use crate::common::api_common::{ApiProject, ApiVersion}; use crate::common::api_v3::request_data::get_public_project_creation_data; use crate::common::database::*; -use crate::common::dummy_data::{DummyProjectAlpha, DummyProjectBeta, TestFile}; +use crate::common::dummy_data::{ + DummyProjectAlpha, DummyProjectBeta, TestFile, +}; // importing common module. mod common; @@ -353,7 +355,10 @@ async fn creating_loader_fields() { .await; let project = api - .get_project_deserialized(&alpha_project_id.to_string(), USER_USER_PAT) + .get_project_deserialized( + &alpha_project_id.to_string(), + USER_USER_PAT, + ) .await; assert_eq!( project.fields.get("game_versions").unwrap(), @@ -413,57 +418,60 @@ async fn get_loader_fields_variants() { async fn get_available_loader_fields() { // Get available loader fields for a given loader // (ie: which fields are relevant for 'fabric', etc) - with_test_environment(None, |test_env: TestEnvironment| async move { - let api = &test_env.api; - let loaders = api.get_loaders_deserialized().await; - - let fabric_loader_fields = loaders - .iter() - .find(|x| x.name == "fabric") - .unwrap() - .supported_fields - .clone() - .into_iter() - .collect::>(); - assert_eq!( - fabric_loader_fields, - [ - "game_versions", - "singleplayer", - "client_and_server", - "client_only", - "server_only", - "test_fabric_optional" // exists for testing - ] - .iter() - .map(|s| s.to_string()) - .collect() - ); + with_test_environment( + None, + |test_env: TestEnvironment| async move { + let api = &test_env.api; + let loaders = api.get_loaders_deserialized().await; + + let fabric_loader_fields = loaders + .iter() + .find(|x| x.name == "fabric") + .unwrap() + .supported_fields + .clone() + .into_iter() + .collect::>(); + assert_eq!( + fabric_loader_fields, + [ + "game_versions", + "singleplayer", + "client_and_server", + "client_only", + "server_only", + "test_fabric_optional" // exists for testing + ] + .iter() + .map(|s| s.to_string()) + .collect() + ); - let mrpack_loader_fields = loaders - .iter() - .find(|x| x.name == "mrpack") - .unwrap() - .supported_fields - .clone() - .into_iter() - .collect::>(); - assert_eq!( - mrpack_loader_fields, - [ - "game_versions", - "singleplayer", - "client_and_server", - "client_only", - "server_only", - // mrpack has all the general fields as well as this - "mrpack_loaders" - ] - .iter() - .map(|s| s.to_string()) - .collect() - ); - }) + let mrpack_loader_fields = loaders + .iter() + .find(|x| x.name == "mrpack") + .unwrap() + .supported_fields + .clone() + .into_iter() + .collect::>(); + assert_eq!( + mrpack_loader_fields, + [ + "game_versions", + "singleplayer", + "client_and_server", + "client_only", + "server_only", + // mrpack has all the general fields as well as this + "mrpack_loaders" + ] + .iter() + .map(|s| s.to_string()) + .collect() + ); + }, + ) .await; } @@ -471,90 +479,100 @@ async fn get_available_loader_fields() { async fn test_multi_get_redis_cache() { // Ensures a multi-project get including both modpacks and mods ddoes not // incorrectly cache loader fields - with_test_environment(None, |test_env: TestEnvironment| async move { - let api = &test_env.api; - - // Create 5 modpacks - let mut modpacks = Vec::new(); - for i in 0..5 { - let slug = format!("test-modpack-{}", i); + with_test_environment( + None, + |test_env: TestEnvironment| async move { + let api = &test_env.api; + + // Create 5 modpacks + let mut modpacks = Vec::new(); + for i in 0..5 { + let slug = format!("test-modpack-{}", i); + + let creation_data = get_public_project_creation_data( + &slug, + Some(TestFile::build_random_mrpack()), + None, + ); + let resp = + api.create_project(creation_data, USER_USER_PAT).await; + assert_status!(&resp, StatusCode::OK); + modpacks.push(slug); + } - let creation_data = get_public_project_creation_data( - &slug, - Some(TestFile::build_random_mrpack()), - None, - ); - let resp = api.create_project(creation_data, USER_USER_PAT).await; - assert_status!(&resp, StatusCode::OK); - modpacks.push(slug); - } + // Create 5 mods + let mut mods = Vec::new(); + for i in 0..5 { + let slug = format!("test-mod-{}", i); - // Create 5 mods - let mut mods = Vec::new(); - for i in 0..5 { - let slug = format!("test-mod-{}", i); + let creation_data = get_public_project_creation_data( + &slug, + Some(TestFile::build_random_jar()), + None, + ); + let resp = + api.create_project(creation_data, USER_USER_PAT).await; + assert_status!(&resp, StatusCode::OK); + mods.push(slug); + } - let creation_data = - get_public_project_creation_data(&slug, Some(TestFile::build_random_jar()), None); - let resp = api.create_project(creation_data, USER_USER_PAT).await; + // Get all 10 projects + let project_slugs = modpacks + .iter() + .map(|x| x.as_str()) + .chain(mods.iter().map(|x| x.as_str())) + .collect_vec(); + let resp = api.get_projects(&project_slugs, USER_USER_PAT).await; assert_status!(&resp, StatusCode::OK); - mods.push(slug); - } - - // Get all 10 projects - let project_slugs = modpacks - .iter() - .map(|x| x.as_str()) - .chain(mods.iter().map(|x| x.as_str())) - .collect_vec(); - let resp = api.get_projects(&project_slugs, USER_USER_PAT).await; - assert_status!(&resp, StatusCode::OK); - let projects: Vec = test::read_body_json(resp).await; - assert_eq!(projects.len(), 10); - - // Ensure all 5 modpacks have 'mrpack_loaders', and all 5 mods do not - for project in projects.iter() { - if modpacks.contains(project.slug.as_ref().unwrap()) { - assert!(project.fields.contains_key("mrpack_loaders")); - } else if mods.contains(project.slug.as_ref().unwrap()) { - assert!(!project.fields.contains_key("mrpack_loaders")); - } else { - panic!("Unexpected project slug: {:?}", project.slug); + let projects: Vec = + test::read_body_json(resp).await; + assert_eq!(projects.len(), 10); + + // Ensure all 5 modpacks have 'mrpack_loaders', and all 5 mods do not + for project in projects.iter() { + if modpacks.contains(project.slug.as_ref().unwrap()) { + assert!(project.fields.contains_key("mrpack_loaders")); + } else if mods.contains(project.slug.as_ref().unwrap()) { + assert!(!project.fields.contains_key("mrpack_loaders")); + } else { + panic!("Unexpected project slug: {:?}", project.slug); + } } - } - // Get a version from each project - let version_ids_modpacks = projects - .iter() - .filter(|x| modpacks.contains(x.slug.as_ref().unwrap())) - .map(|x| x.versions[0]) - .collect_vec(); - let version_ids_mods = projects - .iter() - .filter(|x| mods.contains(x.slug.as_ref().unwrap())) - .map(|x| x.versions[0]) - .collect_vec(); - let version_ids = version_ids_modpacks - .iter() - .chain(version_ids_mods.iter()) - .map(|x| x.to_string()) - .collect_vec(); - let resp = api.get_versions(version_ids, USER_USER_PAT).await; - assert_status!(&resp, StatusCode::OK); - let versions: Vec = test::read_body_json(resp).await; - assert_eq!(versions.len(), 10); - - // Ensure all 5 versions from modpacks have 'mrpack_loaders', and all 5 versions from mods do not - for version in versions.iter() { - if version_ids_modpacks.contains(&version.id) { - assert!(version.fields.contains_key("mrpack_loaders")); - } else if version_ids_mods.contains(&version.id) { - assert!(!version.fields.contains_key("mrpack_loaders")); - } else { - panic!("Unexpected version id: {:?}", version.id); + // Get a version from each project + let version_ids_modpacks = projects + .iter() + .filter(|x| modpacks.contains(x.slug.as_ref().unwrap())) + .map(|x| x.versions[0]) + .collect_vec(); + let version_ids_mods = projects + .iter() + .filter(|x| mods.contains(x.slug.as_ref().unwrap())) + .map(|x| x.versions[0]) + .collect_vec(); + let version_ids = version_ids_modpacks + .iter() + .chain(version_ids_mods.iter()) + .map(|x| x.to_string()) + .collect_vec(); + let resp = api.get_versions(version_ids, USER_USER_PAT).await; + assert_status!(&resp, StatusCode::OK); + let versions: Vec = + test::read_body_json(resp).await; + assert_eq!(versions.len(), 10); + + // Ensure all 5 versions from modpacks have 'mrpack_loaders', and all 5 versions from mods do not + for version in versions.iter() { + if version_ids_modpacks.contains(&version.id) { + assert!(version.fields.contains_key("mrpack_loaders")); + } else if version_ids_mods.contains(&version.id) { + assert!(!version.fields.contains_key("mrpack_loaders")); + } else { + panic!("Unexpected version id: {:?}", version.id); + } } - } - }) + }, + ) .await; } diff --git a/apps/labrinth/tests/notifications.rs b/apps/labrinth/tests/notifications.rs index 60956e6ca..d63fc819a 100644 --- a/apps/labrinth/tests/notifications.rs +++ b/apps/labrinth/tests/notifications.rs @@ -8,18 +8,31 @@ use crate::common::api_common::ApiTeams; mod common; #[actix_rt::test] -pub async fn get_user_notifications_after_team_invitation_returns_notification() { +pub async fn get_user_notifications_after_team_invitation_returns_notification() +{ with_test_environment_all(None, |test_env| async move { let alpha_team_id = test_env.dummy.project_alpha.team_id.clone(); let api = test_env.api; - api.get_user_notifications_deserialized_common(FRIEND_USER_ID, FRIEND_USER_PAT) - .await; + api.get_user_notifications_deserialized_common( + FRIEND_USER_ID, + FRIEND_USER_PAT, + ) + .await; - api.add_user_to_team(&alpha_team_id, FRIEND_USER_ID, None, None, USER_USER_PAT) - .await; + api.add_user_to_team( + &alpha_team_id, + FRIEND_USER_ID, + None, + None, + USER_USER_PAT, + ) + .await; let notifications = api - .get_user_notifications_deserialized_common(FRIEND_USER_ID, FRIEND_USER_PAT) + .get_user_notifications_deserialized_common( + FRIEND_USER_ID, + FRIEND_USER_PAT, + ) .await; assert_eq!(1, notifications.len()); }) @@ -27,12 +40,16 @@ pub async fn get_user_notifications_after_team_invitation_returns_notification() } #[actix_rt::test] -pub async fn get_user_notifications_after_reading_indicates_notification_read() { +pub async fn get_user_notifications_after_reading_indicates_notification_read() +{ with_test_environment_all(None, |test_env| async move { test_env.generate_friend_user_notification().await; let api = test_env.api; let notifications = api - .get_user_notifications_deserialized_common(FRIEND_USER_ID, FRIEND_USER_PAT) + .get_user_notifications_deserialized_common( + FRIEND_USER_ID, + FRIEND_USER_PAT, + ) .await; assert_eq!(1, notifications.len()); let notification_id = notifications[0].id.to_string(); @@ -41,7 +58,10 @@ pub async fn get_user_notifications_after_reading_indicates_notification_read() .await; let notifications = api - .get_user_notifications_deserialized_common(FRIEND_USER_ID, FRIEND_USER_PAT) + .get_user_notifications_deserialized_common( + FRIEND_USER_ID, + FRIEND_USER_PAT, + ) .await; assert_eq!(1, notifications.len()); assert!(notifications[0].read); @@ -50,12 +70,16 @@ pub async fn get_user_notifications_after_reading_indicates_notification_read() } #[actix_rt::test] -pub async fn get_user_notifications_after_deleting_does_not_show_notification() { +pub async fn get_user_notifications_after_deleting_does_not_show_notification() +{ with_test_environment_all(None, |test_env| async move { test_env.generate_friend_user_notification().await; let api = test_env.api; let notifications = api - .get_user_notifications_deserialized_common(FRIEND_USER_ID, FRIEND_USER_PAT) + .get_user_notifications_deserialized_common( + FRIEND_USER_ID, + FRIEND_USER_PAT, + ) .await; assert_eq!(1, notifications.len()); let notification_id = notifications[0].id.to_string(); @@ -64,7 +88,10 @@ pub async fn get_user_notifications_after_deleting_does_not_show_notification() .await; let notifications = api - .get_user_notifications_deserialized_common(FRIEND_USER_ID, FRIEND_USER_PAT) + .get_user_notifications_deserialized_common( + FRIEND_USER_ID, + FRIEND_USER_PAT, + ) .await; assert_eq!(0, notifications.len()); }) diff --git a/apps/labrinth/tests/oauth.rs b/apps/labrinth/tests/oauth.rs index 4514052f0..4ff3fc5c6 100644 --- a/apps/labrinth/tests/oauth.rs +++ b/apps/labrinth/tests/oauth.rs @@ -3,7 +3,9 @@ use actix_web::test; use common::{ api_v3::oauth::get_redirect_location_query_params, api_v3::{ - oauth::{get_auth_code_from_redirect_params, get_authorize_accept_flow_id}, + oauth::{ + get_auth_code_from_redirect_params, get_authorize_accept_flow_id, + }, ApiV3, }, database::FRIEND_USER_ID, @@ -81,7 +83,8 @@ async fn oauth_flow_happy_path() { #[actix_rt::test] async fn oauth_authorize_for_already_authorized_scopes_returns_auth_code() { with_test_environment(None, |env: TestEnvironment| async move { - let DummyOAuthClientAlpha { client_id, .. } = env.dummy.oauth_client_alpha; + let DummyOAuthClientAlpha { client_id, .. } = + env.dummy.oauth_client_alpha; let resp = env .api @@ -131,7 +134,12 @@ async fn get_oauth_token_with_already_used_auth_code_fails() { let resp = env .api - .oauth_token(auth_code.clone(), None, client_id.clone(), &client_secret) + .oauth_token( + auth_code.clone(), + None, + client_id.clone(), + &client_secret, + ) .await; assert_status!(&resp, StatusCode::OK); @@ -211,7 +219,13 @@ async fn oauth_authorize_with_broader_scopes_requires_user_accept() { let client_id = env.dummy.oauth_client_alpha.client_id; let resp = env .api - .oauth_authorize(&client_id, Some("USER_READ"), None, None, USER_USER_PAT) + .oauth_authorize( + &client_id, + Some("USER_READ"), + None, + None, + USER_USER_PAT, + ) .await; let flow_id = get_authorize_accept_flow_id(resp).await; env.api.oauth_accept(&flow_id, USER_USER_PAT).await; @@ -289,8 +303,12 @@ async fn revoke_authorization_after_issuing_token_revokes_token() { USER_USER_PAT, ) .await; - env.assert_read_notifications_status(USER_USER_ID, Some(&access_token), StatusCode::OK) - .await; + env.assert_read_notifications_status( + USER_USER_ID, + Some(&access_token), + StatusCode::OK, + ) + .await; let resp = env .api diff --git a/apps/labrinth/tests/oauth_clients.rs b/apps/labrinth/tests/oauth_clients.rs index 7289a225b..335dbca44 100644 --- a/apps/labrinth/tests/oauth_clients.rs +++ b/apps/labrinth/tests/oauth_clients.rs @@ -37,7 +37,8 @@ async fn can_create_edit_get_oauth_client() { ) .await; assert_status!(&resp, StatusCode::OK); - let creation_result: OAuthClientCreationResult = test::read_body_json(resp).await; + let creation_result: OAuthClientCreationResult = + test::read_body_json(resp).await; let client_id = get_json_val_str(creation_result.client.id); let url = Some("https://modrinth.com".to_string()); @@ -95,7 +96,8 @@ async fn create_oauth_client_with_restricted_scopes_fails() { #[actix_rt::test] async fn get_oauth_client_for_client_creator_succeeds() { with_test_environment(None, |env: TestEnvironment| async move { - let DummyOAuthClientAlpha { client_id, .. } = env.dummy.oauth_client_alpha.clone(); + let DummyOAuthClientAlpha { client_id, .. } = + env.dummy.oauth_client_alpha.clone(); let resp = env .api @@ -176,7 +178,8 @@ async fn can_list_user_oauth_authorizations() { ) .await; - let authorizations = env.api.get_user_oauth_authorizations(USER_USER_PAT).await; + let authorizations = + env.api.get_user_oauth_authorizations(USER_USER_PAT).await; assert_eq!(1, authorizations.len()); assert_eq!(USER_USER_ID_PARSED, authorizations[0].user_id.0 as i64); }) diff --git a/apps/labrinth/tests/organizations.rs b/apps/labrinth/tests/organizations.rs index beb68b43a..1a570b358 100644 --- a/apps/labrinth/tests/organizations.rs +++ b/apps/labrinth/tests/organizations.rs @@ -1,16 +1,21 @@ use crate::common::{ api_common::{ApiProject, ApiTeams}, database::{ - generate_random_name, ADMIN_USER_PAT, ENEMY_USER_ID_PARSED, ENEMY_USER_PAT, - FRIEND_USER_ID_PARSED, MOD_USER_ID, MOD_USER_PAT, USER_USER_ID, USER_USER_ID_PARSED, + generate_random_name, ADMIN_USER_PAT, ENEMY_USER_ID_PARSED, + ENEMY_USER_PAT, FRIEND_USER_ID_PARSED, MOD_USER_ID, MOD_USER_PAT, + USER_USER_ID, USER_USER_ID_PARSED, + }, + dummy_data::{ + DummyImage, DummyOrganizationZeta, DummyProjectAlpha, DummyProjectBeta, }, - dummy_data::{DummyImage, DummyOrganizationZeta, DummyProjectAlpha, DummyProjectBeta}, }; use actix_http::StatusCode; use common::{ api_v3::ApiV3, database::{FRIEND_USER_ID, FRIEND_USER_PAT, USER_USER_PAT}, - environment::{with_test_environment, with_test_environment_all, TestEnvironment}, + environment::{ + with_test_environment, with_test_environment_all, TestEnvironment, + }, permissions::{PermissionsTest, PermissionsTestContext}, }; use labrinth::models::{ @@ -23,803 +28,977 @@ mod common; #[actix_rt::test] async fn create_organization() { - with_test_environment(None, |test_env: TestEnvironment| async move { - let api = &test_env.api; - let zeta_organization_slug = &test_env.dummy.organization_zeta.organization_id; + with_test_environment( + None, + |test_env: TestEnvironment| async move { + let api = &test_env.api; + let zeta_organization_slug = + &test_env.dummy.organization_zeta.organization_id; + + // Failed creations title: + // - too short title + // - too long title + for title in ["a", &"a".repeat(100)] { + let resp = api + .create_organization( + title, + "theta", + "theta_description", + USER_USER_PAT, + ) + .await; + assert_status!(&resp, StatusCode::BAD_REQUEST); + } + + // Failed creations slug: + // - slug collision with zeta + // - too short slug + // - too long slug + // - not url safe slug + for slug in [ + zeta_organization_slug, + "a", + &"a".repeat(100), + "not url safe%&^!#$##!@#$%^&*()", + ] { + let resp = api + .create_organization( + "Theta Org", + slug, + "theta_description", + USER_USER_PAT, + ) + .await; + assert_status!(&resp, StatusCode::BAD_REQUEST); + } + + // Failed creations description: + // - too short desc + // - too long desc + for description in ["a", &"a".repeat(300)] { + let resp = api + .create_organization( + "Theta Org", + "theta", + description, + USER_USER_PAT, + ) + .await; + assert_status!(&resp, StatusCode::BAD_REQUEST); + } - // Failed creations title: - // - too short title - // - too long title - for title in ["a", &"a".repeat(100)] { - let resp = api - .create_organization(title, "theta", "theta_description", USER_USER_PAT) - .await; - assert_status!(&resp, StatusCode::BAD_REQUEST); - } - - // Failed creations slug: - // - slug collision with zeta - // - too short slug - // - too long slug - // - not url safe slug - for slug in [ - zeta_organization_slug, - "a", - &"a".repeat(100), - "not url safe%&^!#$##!@#$%^&*()", - ] { + // Create 'theta' organization let resp = api - .create_organization("Theta Org", slug, "theta_description", USER_USER_PAT) + .create_organization( + "Theta Org", + "theta", + "not url safe%&^!#$##!@#$%^&", + USER_USER_PAT, + ) .await; - assert_status!(&resp, StatusCode::BAD_REQUEST); - } + assert_status!(&resp, StatusCode::OK); - // Failed creations description: - // - too short desc - // - too long desc - for description in ["a", &"a".repeat(300)] { - let resp = api - .create_organization("Theta Org", "theta", description, USER_USER_PAT) + // Get organization using slug + let theta = api + .get_organization_deserialized("theta", USER_USER_PAT) .await; - assert_status!(&resp, StatusCode::BAD_REQUEST); - } - - // Create 'theta' organization - let resp = api - .create_organization( - "Theta Org", - "theta", - "not url safe%&^!#$##!@#$%^&", - USER_USER_PAT, - ) - .await; - assert_status!(&resp, StatusCode::OK); + assert_eq!(theta.name, "Theta Org"); + assert_eq!(theta.slug, "theta"); + assert_eq!(theta.description, "not url safe%&^!#$##!@#$%^&"); + assert_status!(&resp, StatusCode::OK); - // Get organization using slug - let theta = api - .get_organization_deserialized("theta", USER_USER_PAT) - .await; - assert_eq!(theta.name, "Theta Org"); - assert_eq!(theta.slug, "theta"); - assert_eq!(theta.description, "not url safe%&^!#$##!@#$%^&"); - assert_status!(&resp, StatusCode::OK); - - // Get created team - let members = api - .get_organization_members_deserialized("theta", USER_USER_PAT) - .await; + // Get created team + let members = api + .get_organization_members_deserialized("theta", USER_USER_PAT) + .await; - // Should only be one member, which is USER_USER_ID, and is the owner with full permissions - assert_eq!(members[0].user.id.to_string(), USER_USER_ID); - assert_eq!( - members[0].organization_permissions, - Some(OrganizationPermissions::all()) - ); - assert_eq!(members[0].role, "Member"); - assert!(members[0].is_owner); - }) + // Should only be one member, which is USER_USER_ID, and is the owner with full permissions + assert_eq!(members[0].user.id.to_string(), USER_USER_ID); + assert_eq!( + members[0].organization_permissions, + Some(OrganizationPermissions::all()) + ); + assert_eq!(members[0].role, "Member"); + assert!(members[0].is_owner); + }, + ) .await; } #[actix_rt::test] async fn get_project_organization() { - with_test_environment(None, |test_env: TestEnvironment| async move { - let api = &test_env.api; - let zeta_organization_id = &test_env.dummy.organization_zeta.organization_id; - let alpha_project_id = &test_env.dummy.project_alpha.project_id; - - // ADd alpha project to zeta organization - let resp = api - .organization_add_project(zeta_organization_id, alpha_project_id, USER_USER_PAT) - .await; - assert_status!(&resp, StatusCode::OK); + with_test_environment( + None, + |test_env: TestEnvironment| async move { + let api = &test_env.api; + let zeta_organization_id = + &test_env.dummy.organization_zeta.organization_id; + let alpha_project_id = &test_env.dummy.project_alpha.project_id; + + // ADd alpha project to zeta organization + let resp = api + .organization_add_project( + zeta_organization_id, + alpha_project_id, + USER_USER_PAT, + ) + .await; + assert_status!(&resp, StatusCode::OK); - // Get project organization - let zeta = api - .get_project_organization_deserialized(alpha_project_id, USER_USER_PAT) - .await; - assert_eq!(zeta.id.to_string(), zeta_organization_id.to_string()); - }) + // Get project organization + let zeta = api + .get_project_organization_deserialized( + alpha_project_id, + USER_USER_PAT, + ) + .await; + assert_eq!(zeta.id.to_string(), zeta_organization_id.to_string()); + }, + ) .await; } #[actix_rt::test] async fn patch_organization() { - with_test_environment(None, |test_env: TestEnvironment| async move { - let api = &test_env.api; - - let zeta_organization_id = &test_env.dummy.organization_zeta.organization_id; + with_test_environment( + None, + |test_env: TestEnvironment| async move { + let api = &test_env.api; - // Create 'theta' organization - let resp = api - .create_organization("Theta Org", "theta", "theta_description", USER_USER_PAT) - .await; - assert_status!(&resp, StatusCode::OK); + let zeta_organization_id = + &test_env.dummy.organization_zeta.organization_id; - // Failed patch to theta title: - // - too short title - // - too long title - for title in ["a", &"a".repeat(100)] { + // Create 'theta' organization let resp = api - .edit_organization( + .create_organization( + "Theta Org", "theta", - json!({ - "name": title, - }), + "theta_description", USER_USER_PAT, ) .await; - assert_status!(&resp, StatusCode::BAD_REQUEST); - } - - // Failed patch to zeta slug: - // - slug collision with theta - // - too short slug - // - too long slug - // - not url safe slug - for title in [ - "theta", - "a", - &"a".repeat(100), - "not url safe%&^!#$##!@#$%^&*()", - ] { - let resp = api - .edit_organization( - zeta_organization_id, - json!({ - "slug": title, - "description": "theta_description" - }), - USER_USER_PAT, - ) - .await; - assert_status!(&resp, StatusCode::BAD_REQUEST); - } + assert_status!(&resp, StatusCode::OK); + + // Failed patch to theta title: + // - too short title + // - too long title + for title in ["a", &"a".repeat(100)] { + let resp = api + .edit_organization( + "theta", + json!({ + "name": title, + }), + USER_USER_PAT, + ) + .await; + assert_status!(&resp, StatusCode::BAD_REQUEST); + } + + // Failed patch to zeta slug: + // - slug collision with theta + // - too short slug + // - too long slug + // - not url safe slug + for title in [ + "theta", + "a", + &"a".repeat(100), + "not url safe%&^!#$##!@#$%^&*()", + ] { + let resp = api + .edit_organization( + zeta_organization_id, + json!({ + "slug": title, + "description": "theta_description" + }), + USER_USER_PAT, + ) + .await; + assert_status!(&resp, StatusCode::BAD_REQUEST); + } + + // Failed patch to zeta description: + // - too short description + // - too long description + for description in ["a", &"a".repeat(300)] { + let resp = api + .edit_organization( + zeta_organization_id, + json!({ + "description": description + }), + USER_USER_PAT, + ) + .await; + assert_status!(&resp, StatusCode::BAD_REQUEST); + } - // Failed patch to zeta description: - // - too short description - // - too long description - for description in ["a", &"a".repeat(300)] { + // Successful patch to many fields let resp = api .edit_organization( zeta_organization_id, json!({ - "description": description + "name": "new_title", + "slug": "new_slug", + "description": "not url safe%&^!#$##!@#$%^&" // not-URL-safe description should still work }), USER_USER_PAT, ) .await; - assert_status!(&resp, StatusCode::BAD_REQUEST); - } - - // Successful patch to many fields - let resp = api - .edit_organization( - zeta_organization_id, - json!({ - "name": "new_title", - "slug": "new_slug", - "description": "not url safe%&^!#$##!@#$%^&" // not-URL-safe description should still work - }), - USER_USER_PAT, - ) - .await; - assert_status!(&resp, StatusCode::NO_CONTENT); + assert_status!(&resp, StatusCode::NO_CONTENT); - // Get project using new slug - let new_title = api - .get_organization_deserialized("new_slug", USER_USER_PAT) - .await; - assert_eq!(new_title.name, "new_title"); - assert_eq!(new_title.slug, "new_slug"); - assert_eq!(new_title.description, "not url safe%&^!#$##!@#$%^&"); - }) + // Get project using new slug + let new_title = api + .get_organization_deserialized("new_slug", USER_USER_PAT) + .await; + assert_eq!(new_title.name, "new_title"); + assert_eq!(new_title.slug, "new_slug"); + assert_eq!(new_title.description, "not url safe%&^!#$##!@#$%^&"); + }, + ) .await; } // add/remove icon #[actix_rt::test] async fn add_remove_icon() { - with_test_environment(Some(10), |test_env: TestEnvironment| async move { - let api = &test_env.api; - let zeta_organization_id = &test_env.dummy.organization_zeta.organization_id; - - // Get project - let resp = test_env - .api - .get_organization_deserialized(zeta_organization_id, USER_USER_PAT) - .await; - assert_eq!(resp.icon_url, None); + with_test_environment( + Some(10), + |test_env: TestEnvironment| async move { + let api = &test_env.api; + let zeta_organization_id = + &test_env.dummy.organization_zeta.organization_id; + + // Get project + let resp = test_env + .api + .get_organization_deserialized( + zeta_organization_id, + USER_USER_PAT, + ) + .await; + assert_eq!(resp.icon_url, None); - // Icon edit - // Uses alpha organization to delete this icon - let resp = api - .edit_organization_icon( - zeta_organization_id, - Some(DummyImage::SmallIcon.get_icon_data()), - USER_USER_PAT, - ) - .await; - assert_status!(&resp, StatusCode::NO_CONTENT); + // Icon edit + // Uses alpha organization to delete this icon + let resp = api + .edit_organization_icon( + zeta_organization_id, + Some(DummyImage::SmallIcon.get_icon_data()), + USER_USER_PAT, + ) + .await; + assert_status!(&resp, StatusCode::NO_CONTENT); - // Get project - let zeta_org = api - .get_organization_deserialized(zeta_organization_id, USER_USER_PAT) - .await; - assert!(zeta_org.icon_url.is_some()); + // Get project + let zeta_org = api + .get_organization_deserialized( + zeta_organization_id, + USER_USER_PAT, + ) + .await; + assert!(zeta_org.icon_url.is_some()); - // Icon delete - // Uses alpha organization to delete added icon - let resp = api - .edit_organization_icon(zeta_organization_id, None, USER_USER_PAT) - .await; - assert_status!(&resp, StatusCode::NO_CONTENT); + // Icon delete + // Uses alpha organization to delete added icon + let resp = api + .edit_organization_icon( + zeta_organization_id, + None, + USER_USER_PAT, + ) + .await; + assert_status!(&resp, StatusCode::NO_CONTENT); - // Get project - let zeta_org = api - .get_organization_deserialized(zeta_organization_id, USER_USER_PAT) - .await; - assert!(zeta_org.icon_url.is_none()); - }) + // Get project + let zeta_org = api + .get_organization_deserialized( + zeta_organization_id, + USER_USER_PAT, + ) + .await; + assert!(zeta_org.icon_url.is_none()); + }, + ) .await; } // delete org #[actix_rt::test] async fn delete_org() { - with_test_environment(None, |test_env: TestEnvironment| async move { - let api = &test_env.api; - let zeta_organization_id = &test_env.dummy.organization_zeta.organization_id; + with_test_environment( + None, + |test_env: TestEnvironment| async move { + let api = &test_env.api; + let zeta_organization_id = + &test_env.dummy.organization_zeta.organization_id; - let resp = api - .delete_organization(zeta_organization_id, USER_USER_PAT) - .await; - assert_status!(&resp, StatusCode::NO_CONTENT); + let resp = api + .delete_organization(zeta_organization_id, USER_USER_PAT) + .await; + assert_status!(&resp, StatusCode::NO_CONTENT); - // Get organization, which should no longer exist - let resp = api - .get_organization(zeta_organization_id, USER_USER_PAT) - .await; - assert_status!(&resp, StatusCode::NOT_FOUND); - }) + // Get organization, which should no longer exist + let resp = api + .get_organization(zeta_organization_id, USER_USER_PAT) + .await; + assert_status!(&resp, StatusCode::NOT_FOUND); + }, + ) .await; } // add/remove organization projects #[actix_rt::test] async fn add_remove_organization_projects() { - with_test_environment(None, |test_env: TestEnvironment| async move { - let alpha_project_id: &str = &test_env.dummy.project_alpha.project_id; - let alpha_project_slug: &str = &test_env.dummy.project_alpha.project_slug; - let zeta_organization_id: &str = &test_env.dummy.organization_zeta.organization_id; - - // user's page should show alpha project - // It may contain more than one project, depending on dummy data, but should contain the alpha project - let projects = test_env - .api - .get_user_projects_deserialized_common(USER_USER_ID, USER_USER_PAT) - .await; - assert!(projects - .iter() - .any(|p| p.id.to_string() == alpha_project_id)); + with_test_environment( + None, + |test_env: TestEnvironment| async move { + let alpha_project_id: &str = + &test_env.dummy.project_alpha.project_id; + let alpha_project_slug: &str = + &test_env.dummy.project_alpha.project_slug; + let zeta_organization_id: &str = + &test_env.dummy.organization_zeta.organization_id; + + // user's page should show alpha project + // It may contain more than one project, depending on dummy data, but should contain the alpha project + let projects = test_env + .api + .get_user_projects_deserialized_common( + USER_USER_ID, + USER_USER_PAT, + ) + .await; + assert!(projects + .iter() + .any(|p| p.id.to_string() == alpha_project_id)); + + // Add/remove project to organization, first by ID, then by slug + for alpha in [alpha_project_id, alpha_project_slug] { + let resp = test_env + .api + .organization_add_project( + zeta_organization_id, + alpha, + USER_USER_PAT, + ) + .await; + assert_status!(&resp, StatusCode::OK); + + // Get organization projects + let projects = test_env + .api + .get_organization_projects_deserialized( + zeta_organization_id, + USER_USER_PAT, + ) + .await; + assert_eq!(projects[0].id.to_string(), alpha_project_id); + assert_eq!( + projects[0].slug, + Some(alpha_project_slug.to_string()) + ); + + // Currently, intended behaviour is that user's page should NOT show organization projects. + // It may contain other projects, depending on dummy data, but should not contain the alpha project + let projects = test_env + .api + .get_user_projects_deserialized_common( + USER_USER_ID, + USER_USER_PAT, + ) + .await; + assert!(!projects + .iter() + .any(|p| p.id.to_string() == alpha_project_id)); + + // Remove project from organization + let resp = test_env + .api + .organization_remove_project( + zeta_organization_id, + alpha, + UserId(USER_USER_ID_PARSED as u64), + USER_USER_PAT, + ) + .await; + assert_status!(&resp, StatusCode::OK); + + // Get user's projects as user - should be 1, the alpha project, + // as we gave back ownership to the user when we removed it from the organization + // So user's page should show the alpha project (and possibly others) + let projects = test_env + .api + .get_user_projects_deserialized_common( + USER_USER_ID, + USER_USER_PAT, + ) + .await; + assert!(projects + .iter() + .any(|p| p.id.to_string() == alpha_project_id)); + + // Get organization projects + let projects = test_env + .api + .get_organization_projects_deserialized( + zeta_organization_id, + USER_USER_PAT, + ) + .await; + assert!(projects.is_empty()); + } + }, + ) + .await; +} - // Add/remove project to organization, first by ID, then by slug - for alpha in [alpha_project_id, alpha_project_slug] { +// Like above, but specifically regarding ownership transferring +#[actix_rt::test] +async fn add_remove_organization_project_ownership_to_user() { + with_test_environment( + None, + |test_env: TestEnvironment| async move { + let DummyProjectAlpha { + project_id: alpha_project_id, + team_id: alpha_team_id, + .. + } = &test_env.dummy.project_alpha; + let DummyProjectBeta { + project_id: beta_project_id, + team_id: beta_team_id, + .. + } = &test_env.dummy.project_beta; + let DummyOrganizationZeta { + organization_id: zeta_organization_id, + team_id: zeta_team_id, + .. + } = &test_env.dummy.organization_zeta; + + // Add friend to alpha, beta, and zeta + for (team, organization) in [ + (alpha_team_id, false), + (beta_team_id, false), + (zeta_team_id, true), + ] { + let org_permissions = if organization { + Some(OrganizationPermissions::all()) + } else { + None + }; + let resp = test_env + .api + .add_user_to_team( + team, + FRIEND_USER_ID, + Some(ProjectPermissions::all()), + org_permissions, + USER_USER_PAT, + ) + .await; + assert_status!(&resp, StatusCode::NO_CONTENT); + + // Accept invites + let resp = test_env.api.join_team(team, FRIEND_USER_PAT).await; + assert_status!(&resp, StatusCode::NO_CONTENT); + } + + // For each team, confirm there are two members, but only one owner of the project, and it is USER_USER_ID + for team in [alpha_team_id, beta_team_id, zeta_team_id] { + let members = test_env + .api + .get_team_members_deserialized(team, USER_USER_PAT) + .await; + assert_eq!(members.len(), 2); + let user_member = + members.iter().filter(|m| m.is_owner).collect::>(); + assert_eq!(user_member.len(), 1); + assert_eq!(user_member[0].user.id.to_string(), USER_USER_ID); + } + + // Transfer ownership of beta project to FRIEND let resp = test_env .api - .organization_add_project(zeta_organization_id, alpha, USER_USER_PAT) + .transfer_team_ownership( + beta_team_id, + FRIEND_USER_ID, + USER_USER_PAT, + ) .await; - assert_status!(&resp, StatusCode::OK); + assert_status!(&resp, StatusCode::NO_CONTENT); - // Get organization projects - let projects = test_env + // Confirm there are still two users, but now FRIEND_USER_ID is the owner + let members = test_env + .api + .get_team_members_deserialized(beta_team_id, USER_USER_PAT) + .await; + assert_eq!(members.len(), 2); + let user_member = + members.iter().filter(|m| m.is_owner).collect::>(); + assert_eq!(user_member.len(), 1); + assert_eq!(user_member[0].user.id.to_string(), FRIEND_USER_ID); + + // Add alpha, beta to zeta organization + for (project_id, pat) in [ + (alpha_project_id, USER_USER_PAT), + (beta_project_id, FRIEND_USER_PAT), + ] { + let resp = test_env + .api + .organization_add_project( + zeta_organization_id, + project_id, + pat, + ) + .await; + assert_status!(&resp, StatusCode::OK); + + // Get and confirm it has been added + let project = test_env + .api + .get_project_deserialized(project_id, pat) + .await; + assert_eq!( + &project.organization.unwrap().to_string(), + zeta_organization_id + ); + } + + // Alpha project should have: + // - 1 member, FRIEND_USER_ID + // -> User was removed entirely as a team_member as it is now the owner of the organization + // - No owner. + // -> For alpha, user was removed as owner when it was added to the organization + // -> Friend was never an owner of the alpha project + let members = test_env .api - .get_organization_projects_deserialized(zeta_organization_id, USER_USER_PAT) + .get_team_members_deserialized(alpha_team_id, USER_USER_PAT) .await; - assert_eq!(projects[0].id.to_string(), alpha_project_id); - assert_eq!(projects[0].slug, Some(alpha_project_slug.to_string())); + assert_eq!(members.len(), 1); + assert_eq!(members[0].user.id.to_string(), FRIEND_USER_ID); + let user_member = + members.iter().filter(|m| m.is_owner).collect::>(); + assert_eq!(user_member.len(), 0); + + // Beta project should have: + // - No members + // -> User was removed entirely as a team_member as it is now the owner of the organization + // -> Friend was made owner of the beta project, but was removed as a member when it was added to the organization + // If you are owner of a projeect, you are removed from the team when it is added to an organization, + // so that your former permissions are not overriding the organization permissions by default. + let members = test_env + .api + .get_team_members_deserialized(beta_team_id, USER_USER_PAT) + .await; + assert!(members.is_empty()); - // Currently, intended behaviour is that user's page should NOT show organization projects. - // It may contain other projects, depending on dummy data, but should not contain the alpha project - let projects = test_env + // Transfer ownership of zeta organization to FRIEND + let resp = test_env .api - .get_user_projects_deserialized_common(USER_USER_ID, USER_USER_PAT) + .transfer_team_ownership( + zeta_team_id, + FRIEND_USER_ID, + USER_USER_PAT, + ) .await; - assert!(!projects - .iter() - .any(|p| p.id.to_string() == alpha_project_id)); + assert_status!(&resp, StatusCode::NO_CONTENT); - // Remove project from organization + // Confirm there are no members of the alpha project OR the beta project + // - Friend was removed as a member of these projects when ownership was transferred to them + for team_id in [alpha_team_id, beta_team_id] { + let members = test_env + .api + .get_team_members_deserialized(team_id, USER_USER_PAT) + .await; + assert!(members.is_empty()); + } + + // As user, cannot add friend to alpha project, as they are the org owner + let resp = test_env + .api + .add_user_to_team( + alpha_team_id, + FRIEND_USER_ID, + None, + None, + USER_USER_PAT, + ) + .await; + assert_status!(&resp, StatusCode::BAD_REQUEST); + + // As friend, can add user to alpha project, as they are not the org owner + let resp = test_env + .api + .add_user_to_team( + alpha_team_id, + USER_USER_ID, + None, + None, + FRIEND_USER_PAT, + ) + .await; + assert_status!(&resp, StatusCode::NO_CONTENT); + + // At this point, friend owns the org + // Alpha member has user as a member, but not as an owner + // Neither project has an owner, as they are owned by the org + + // Remove project from organization with a user that is not an organization member + // This should fail as we cannot give a project to a user that is not a member of the organization let resp = test_env .api .organization_remove_project( zeta_organization_id, - alpha, - UserId(USER_USER_ID_PARSED as u64), + alpha_project_id, + UserId(ENEMY_USER_ID_PARSED as u64), USER_USER_PAT, ) .await; - assert_status!(&resp, StatusCode::OK); + assert_status!(&resp, StatusCode::BAD_REQUEST); - // Get user's projects as user - should be 1, the alpha project, - // as we gave back ownership to the user when we removed it from the organization - // So user's page should show the alpha project (and possibly others) - let projects = test_env + // Set user's permissions within the project that it is a member of to none (for a later test) + let resp = test_env .api - .get_user_projects_deserialized_common(USER_USER_ID, USER_USER_PAT) + .edit_team_member( + alpha_team_id, + USER_USER_ID, + json!({ + "project_permissions": 0, + }), + FRIEND_USER_PAT, + ) .await; - assert!(projects - .iter() - .any(|p| p.id.to_string() == alpha_project_id)); + assert_status!(&resp, StatusCode::NO_CONTENT); - // Get organization projects - let projects = test_env + // Remove project from organization with a user that is an organization member, and a project member + // This should succeed + let resp = test_env + .api + .organization_remove_project( + zeta_organization_id, + alpha_project_id, + UserId(USER_USER_ID_PARSED as u64), + USER_USER_PAT, + ) + .await; + assert_status!(&resp, StatusCode::OK); + + // Remove project from organization with a user that is an organization member, but not a project member + // This should succeed + let resp = test_env .api - .get_organization_projects_deserialized(zeta_organization_id, USER_USER_PAT) + .organization_remove_project( + zeta_organization_id, + beta_project_id, + UserId(USER_USER_ID_PARSED as u64), + USER_USER_PAT, + ) .await; - assert!(projects.is_empty()); - } - }) + assert_status!(&resp, StatusCode::OK); + + // For each of alpha and beta, confirm: + // - There is one member of each project, the owner, USER_USER_ID + // - In addition to being the owner, they have full permissions (even though they were set to none earlier) + // - They no longer have an attached organization + for team_id in [alpha_team_id, beta_team_id] { + let members = test_env + .api + .get_team_members_deserialized(team_id, USER_USER_PAT) + .await; + assert_eq!(members.len(), 1); + let user_member = + members.iter().filter(|m| m.is_owner).collect::>(); + assert_eq!(user_member.len(), 1); + assert_eq!(user_member[0].user.id.to_string(), USER_USER_ID); + assert_eq!( + user_member[0].permissions.unwrap(), + ProjectPermissions::all() + ); + } + + for project_id in [alpha_project_id, beta_project_id] { + let project = test_env + .api + .get_project_deserialized(project_id, USER_USER_PAT) + .await; + assert!(project.organization.is_none()); + } + }, + ) .await; } -// Like above, but specifically regarding ownership transferring #[actix_rt::test] -async fn add_remove_organization_project_ownership_to_user() { - with_test_environment(None, |test_env: TestEnvironment| async move { - let DummyProjectAlpha { - project_id: alpha_project_id, - team_id: alpha_team_id, - .. - } = &test_env.dummy.project_alpha; - let DummyProjectBeta { - project_id: beta_project_id, - team_id: beta_team_id, - .. - } = &test_env.dummy.project_beta; - let DummyOrganizationZeta { - organization_id: zeta_organization_id, - team_id: zeta_team_id, - .. - } = &test_env.dummy.organization_zeta; - - // Add friend to alpha, beta, and zeta - for (team, organization) in [ - (alpha_team_id, false), - (beta_team_id, false), - (zeta_team_id, true), - ] { - let org_permissions = if organization { - Some(OrganizationPermissions::all()) - } else { - None - }; +async fn delete_organization_means_all_projects_to_org_owner() { + with_test_environment( + None, + |test_env: TestEnvironment| async move { + let DummyProjectAlpha { + project_id: alpha_project_id, + team_id: alpha_team_id, + .. + } = &test_env.dummy.project_alpha; + let DummyProjectBeta { + project_id: beta_project_id, + team_id: beta_team_id, + .. + } = &test_env.dummy.project_beta; + let DummyOrganizationZeta { + organization_id: zeta_organization_id, + team_id: zeta_team_id, + .. + } = &test_env.dummy.organization_zeta; + + // Create random project from enemy, ensure it wont get affected + let (enemy_project, _) = test_env + .api + .add_public_project("enemy_project", None, None, ENEMY_USER_PAT) + .await; + + // Add FRIEND let resp = test_env .api .add_user_to_team( - team, + zeta_team_id, FRIEND_USER_ID, - Some(ProjectPermissions::all()), - org_permissions, + None, + None, USER_USER_PAT, ) .await; assert_status!(&resp, StatusCode::NO_CONTENT); - // Accept invites - let resp = test_env.api.join_team(team, FRIEND_USER_PAT).await; + // Accept invite + let resp = + test_env.api.join_team(zeta_team_id, FRIEND_USER_PAT).await; assert_status!(&resp, StatusCode::NO_CONTENT); - } - // For each team, confirm there are two members, but only one owner of the project, and it is USER_USER_ID - for team in [alpha_team_id, beta_team_id, zeta_team_id] { + // Confirm there is only one owner of the project, and it is USER_USER_ID let members = test_env .api - .get_team_members_deserialized(team, USER_USER_PAT) + .get_team_members_deserialized(alpha_team_id, USER_USER_PAT) .await; - assert_eq!(members.len(), 2); - let user_member = members.iter().filter(|m| m.is_owner).collect::>(); + let user_member = + members.iter().filter(|m| m.is_owner).collect::>(); assert_eq!(user_member.len(), 1); assert_eq!(user_member[0].user.id.to_string(), USER_USER_ID); - } - // Transfer ownership of beta project to FRIEND - let resp = test_env - .api - .transfer_team_ownership(beta_team_id, FRIEND_USER_ID, USER_USER_PAT) - .await; - assert_status!(&resp, StatusCode::NO_CONTENT); - - // Confirm there are still two users, but now FRIEND_USER_ID is the owner - let members = test_env - .api - .get_team_members_deserialized(beta_team_id, USER_USER_PAT) - .await; - assert_eq!(members.len(), 2); - let user_member = members.iter().filter(|m| m.is_owner).collect::>(); - assert_eq!(user_member.len(), 1); - assert_eq!(user_member[0].user.id.to_string(), FRIEND_USER_ID); - - // Add alpha, beta to zeta organization - for (project_id, pat) in [ - (alpha_project_id, USER_USER_PAT), - (beta_project_id, FRIEND_USER_PAT), - ] { + // Add alpha to zeta organization let resp = test_env .api - .organization_add_project(zeta_organization_id, project_id, pat) + .organization_add_project( + zeta_organization_id, + alpha_project_id, + USER_USER_PAT, + ) .await; assert_status!(&resp, StatusCode::OK); - // Get and confirm it has been added - let project = test_env.api.get_project_deserialized(project_id, pat).await; - assert_eq!( - &project.organization.unwrap().to_string(), - zeta_organization_id - ); - } - - // Alpha project should have: - // - 1 member, FRIEND_USER_ID - // -> User was removed entirely as a team_member as it is now the owner of the organization - // - No owner. - // -> For alpha, user was removed as owner when it was added to the organization - // -> Friend was never an owner of the alpha project - let members = test_env - .api - .get_team_members_deserialized(alpha_team_id, USER_USER_PAT) - .await; - assert_eq!(members.len(), 1); - assert_eq!(members[0].user.id.to_string(), FRIEND_USER_ID); - let user_member = members.iter().filter(|m| m.is_owner).collect::>(); - assert_eq!(user_member.len(), 0); - - // Beta project should have: - // - No members - // -> User was removed entirely as a team_member as it is now the owner of the organization - // -> Friend was made owner of the beta project, but was removed as a member when it was added to the organization - // If you are owner of a projeect, you are removed from the team when it is added to an organization, - // so that your former permissions are not overriding the organization permissions by default. - let members = test_env - .api - .get_team_members_deserialized(beta_team_id, USER_USER_PAT) - .await; - assert!(members.is_empty()); + // Add beta to zeta organization + test_env + .api + .organization_add_project( + zeta_organization_id, + beta_project_id, + USER_USER_PAT, + ) + .await; - // Transfer ownership of zeta organization to FRIEND - let resp = test_env - .api - .transfer_team_ownership(zeta_team_id, FRIEND_USER_ID, USER_USER_PAT) - .await; - assert_status!(&resp, StatusCode::NO_CONTENT); + // Add friend as a member of the beta project + let resp = test_env + .api + .add_user_to_team( + beta_team_id, + FRIEND_USER_ID, + None, + None, + USER_USER_PAT, + ) + .await; + assert_status!(&resp, StatusCode::NO_CONTENT); + + // Try to accept invite + // This returns a failure, because since beta and FRIEND are in the organizations, + // they can be added to the project without an invite + let resp = + test_env.api.join_team(beta_team_id, FRIEND_USER_PAT).await; + assert_status!(&resp, StatusCode::BAD_REQUEST); - // Confirm there are no members of the alpha project OR the beta project - // - Friend was removed as a member of these projects when ownership was transferred to them - for team_id in [alpha_team_id, beta_team_id] { + // Confirm there is NO owner of the project, as it is owned by the organization let members = test_env .api - .get_team_members_deserialized(team_id, USER_USER_PAT) + .get_team_members_deserialized(alpha_team_id, USER_USER_PAT) .await; - assert!(members.is_empty()); - } + let user_member = + members.iter().filter(|m| m.is_owner).collect::>(); + assert_eq!(user_member.len(), 0); - // As user, cannot add friend to alpha project, as they are the org owner - let resp = test_env - .api - .add_user_to_team(alpha_team_id, FRIEND_USER_ID, None, None, USER_USER_PAT) - .await; - assert_status!(&resp, StatusCode::BAD_REQUEST); + // Transfer ownership of zeta organization to FRIEND + let resp = test_env + .api + .transfer_team_ownership( + zeta_team_id, + FRIEND_USER_ID, + USER_USER_PAT, + ) + .await; + assert_status!(&resp, StatusCode::NO_CONTENT); - // As friend, can add user to alpha project, as they are not the org owner - let resp = test_env - .api - .add_user_to_team(alpha_team_id, USER_USER_ID, None, None, FRIEND_USER_PAT) - .await; - assert_status!(&resp, StatusCode::NO_CONTENT); + // Confirm there is NO owner of the project, as it is owned by the organization + let members = test_env + .api + .get_team_members_deserialized(alpha_team_id, USER_USER_PAT) + .await; + let user_member = + members.iter().filter(|m| m.is_owner).collect::>(); + assert_eq!(user_member.len(), 0); - // At this point, friend owns the org - // Alpha member has user as a member, but not as an owner - // Neither project has an owner, as they are owned by the org - - // Remove project from organization with a user that is not an organization member - // This should fail as we cannot give a project to a user that is not a member of the organization - let resp = test_env - .api - .organization_remove_project( - zeta_organization_id, - alpha_project_id, - UserId(ENEMY_USER_ID_PARSED as u64), - USER_USER_PAT, - ) - .await; - assert_status!(&resp, StatusCode::BAD_REQUEST); - - // Set user's permissions within the project that it is a member of to none (for a later test) - let resp = test_env - .api - .edit_team_member( - alpha_team_id, - USER_USER_ID, - json!({ - "project_permissions": 0, - }), - FRIEND_USER_PAT, - ) - .await; - assert_status!(&resp, StatusCode::NO_CONTENT); + // Delete organization + let resp = test_env + .api + .delete_organization(zeta_organization_id, FRIEND_USER_PAT) + .await; + assert_status!(&resp, StatusCode::NO_CONTENT); - // Remove project from organization with a user that is an organization member, and a project member - // This should succeed - let resp = test_env - .api - .organization_remove_project( - zeta_organization_id, - alpha_project_id, - UserId(USER_USER_ID_PARSED as u64), - USER_USER_PAT, - ) - .await; - assert_status!(&resp, StatusCode::OK); - - // Remove project from organization with a user that is an organization member, but not a project member - // This should succeed - let resp = test_env - .api - .organization_remove_project( - zeta_organization_id, - beta_project_id, - UserId(USER_USER_ID_PARSED as u64), - USER_USER_PAT, - ) - .await; - assert_status!(&resp, StatusCode::OK); + // Confirm there is only one owner of the alpha project, and it is now FRIEND_USER_ID + let members = test_env + .api + .get_team_members_deserialized(alpha_team_id, USER_USER_PAT) + .await; + let user_member = + members.iter().filter(|m| m.is_owner).collect::>(); + assert_eq!(user_member.len(), 1); + assert_eq!(user_member[0].user.id.to_string(), FRIEND_USER_ID); - // For each of alpha and beta, confirm: - // - There is one member of each project, the owner, USER_USER_ID - // - In addition to being the owner, they have full permissions (even though they were set to none earlier) - // - They no longer have an attached organization - for team_id in [alpha_team_id, beta_team_id] { + // Confirm there is only one owner of the beta project, and it is now FRIEND_USER_ID let members = test_env .api - .get_team_members_deserialized(team_id, USER_USER_PAT) + .get_team_members_deserialized(beta_team_id, USER_USER_PAT) .await; - assert_eq!(members.len(), 1); - let user_member = members.iter().filter(|m| m.is_owner).collect::>(); + let user_member = + members.iter().filter(|m| m.is_owner).collect::>(); assert_eq!(user_member.len(), 1); - assert_eq!(user_member[0].user.id.to_string(), USER_USER_ID); - assert_eq!( - user_member[0].permissions.unwrap(), - ProjectPermissions::all() - ); - } + assert_eq!(user_member[0].user.id.to_string(), FRIEND_USER_ID); - for project_id in [alpha_project_id, beta_project_id] { - let project = test_env + // Confirm there is only one member of the enemy project, and it is STILL ENEMY_USER_ID + let enemy_project = test_env .api - .get_project_deserialized(project_id, USER_USER_PAT) + .get_project_deserialized( + &enemy_project.id.to_string(), + ENEMY_USER_PAT, + ) .await; - assert!(project.organization.is_none()); - } - }) + let members = test_env + .api + .get_team_members_deserialized( + &enemy_project.team_id.to_string(), + ENEMY_USER_PAT, + ) + .await; + let user_member = + members.iter().filter(|m| m.is_owner).collect::>(); + assert_eq!(user_member.len(), 1); + assert_eq!( + user_member[0].user.id.to_string(), + ENEMY_USER_ID_PARSED.to_string() + ); + }, + ) .await; } #[actix_rt::test] -async fn delete_organization_means_all_projects_to_org_owner() { - with_test_environment(None, |test_env: TestEnvironment| async move { - let DummyProjectAlpha { - project_id: alpha_project_id, - team_id: alpha_team_id, - .. - } = &test_env.dummy.project_alpha; - let DummyProjectBeta { - project_id: beta_project_id, - team_id: beta_team_id, - .. - } = &test_env.dummy.project_beta; - let DummyOrganizationZeta { - organization_id: zeta_organization_id, - team_id: zeta_team_id, - .. - } = &test_env.dummy.organization_zeta; - - // Create random project from enemy, ensure it wont get affected - let (enemy_project, _) = test_env - .api - .add_public_project("enemy_project", None, None, ENEMY_USER_PAT) - .await; - - // Add FRIEND - let resp = test_env - .api - .add_user_to_team(zeta_team_id, FRIEND_USER_ID, None, None, USER_USER_PAT) - .await; - assert_status!(&resp, StatusCode::NO_CONTENT); - - // Accept invite - let resp = test_env.api.join_team(zeta_team_id, FRIEND_USER_PAT).await; - assert_status!(&resp, StatusCode::NO_CONTENT); - - // Confirm there is only one owner of the project, and it is USER_USER_ID - let members = test_env - .api - .get_team_members_deserialized(alpha_team_id, USER_USER_PAT) - .await; - let user_member = members.iter().filter(|m| m.is_owner).collect::>(); - assert_eq!(user_member.len(), 1); - assert_eq!(user_member[0].user.id.to_string(), USER_USER_ID); - - // Add alpha to zeta organization - let resp = test_env - .api - .organization_add_project(zeta_organization_id, alpha_project_id, USER_USER_PAT) - .await; - assert_status!(&resp, StatusCode::OK); - - // Add beta to zeta organization - test_env - .api - .organization_add_project(zeta_organization_id, beta_project_id, USER_USER_PAT) - .await; - - // Add friend as a member of the beta project - let resp = test_env - .api - .add_user_to_team(beta_team_id, FRIEND_USER_ID, None, None, USER_USER_PAT) - .await; - assert_status!(&resp, StatusCode::NO_CONTENT); - - // Try to accept invite - // This returns a failure, because since beta and FRIEND are in the organizations, - // they can be added to the project without an invite - let resp = test_env.api.join_team(beta_team_id, FRIEND_USER_PAT).await; - assert_status!(&resp, StatusCode::BAD_REQUEST); - - // Confirm there is NO owner of the project, as it is owned by the organization - let members = test_env - .api - .get_team_members_deserialized(alpha_team_id, USER_USER_PAT) - .await; - let user_member = members.iter().filter(|m| m.is_owner).collect::>(); - assert_eq!(user_member.len(), 0); - - // Transfer ownership of zeta organization to FRIEND - let resp = test_env - .api - .transfer_team_ownership(zeta_team_id, FRIEND_USER_ID, USER_USER_PAT) - .await; - assert_status!(&resp, StatusCode::NO_CONTENT); - - // Confirm there is NO owner of the project, as it is owned by the organization - let members = test_env - .api - .get_team_members_deserialized(alpha_team_id, USER_USER_PAT) - .await; - let user_member = members.iter().filter(|m| m.is_owner).collect::>(); - assert_eq!(user_member.len(), 0); - - // Delete organization - let resp = test_env - .api - .delete_organization(zeta_organization_id, FRIEND_USER_PAT) - .await; - assert_status!(&resp, StatusCode::NO_CONTENT); - - // Confirm there is only one owner of the alpha project, and it is now FRIEND_USER_ID - let members = test_env - .api - .get_team_members_deserialized(alpha_team_id, USER_USER_PAT) - .await; - let user_member = members.iter().filter(|m| m.is_owner).collect::>(); - assert_eq!(user_member.len(), 1); - assert_eq!(user_member[0].user.id.to_string(), FRIEND_USER_ID); - - // Confirm there is only one owner of the beta project, and it is now FRIEND_USER_ID - let members = test_env - .api - .get_team_members_deserialized(beta_team_id, USER_USER_PAT) - .await; - let user_member = members.iter().filter(|m| m.is_owner).collect::>(); - assert_eq!(user_member.len(), 1); - assert_eq!(user_member[0].user.id.to_string(), FRIEND_USER_ID); - - // Confirm there is only one member of the enemy project, and it is STILL ENEMY_USER_ID - let enemy_project = test_env - .api - .get_project_deserialized(&enemy_project.id.to_string(), ENEMY_USER_PAT) - .await; - let members = test_env - .api - .get_team_members_deserialized(&enemy_project.team_id.to_string(), ENEMY_USER_PAT) - .await; - let user_member = members.iter().filter(|m| m.is_owner).collect::>(); - assert_eq!(user_member.len(), 1); - assert_eq!( - user_member[0].user.id.to_string(), - ENEMY_USER_ID_PARSED.to_string() - ); - }) +async fn permissions_patch_organization() { + with_test_environment( + None, + |test_env: TestEnvironment| async move { + // For each permission covered by EDIT_DETAILS, ensure the permission is required + let api = &test_env.api; + let edit_details = OrganizationPermissions::EDIT_DETAILS; + let test_pairs = [ + ("name", json!("")), // generated in the test to not collide slugs + ("description", json!("New description")), + ]; + + for (key, value) in test_pairs { + let req_gen = |ctx: PermissionsTestContext| { + let value = value.clone(); + async move { + api.edit_organization( + &ctx.organization_id.unwrap(), + json!({ + key: if key == "name" { + json!(generate_random_name("randomslug")) + } else { + value.clone() + }, + }), + ctx.test_pat.as_deref(), + ) + .await + } + }; + PermissionsTest::new(&test_env) + .simple_organization_permissions_test(edit_details, req_gen) + .await + .unwrap(); + } + }, + ) .await; } +// Not covered by PATCH /organization #[actix_rt::test] -async fn permissions_patch_organization() { - with_test_environment(None, |test_env: TestEnvironment| async move { - // For each permission covered by EDIT_DETAILS, ensure the permission is required - let api = &test_env.api; - let edit_details = OrganizationPermissions::EDIT_DETAILS; - let test_pairs = [ - ("name", json!("")), // generated in the test to not collide slugs - ("description", json!("New description")), - ]; - - for (key, value) in test_pairs { - let req_gen = |ctx: PermissionsTestContext| { - let value = value.clone(); - async move { - api.edit_organization( - &ctx.organization_id.unwrap(), - json!({ - key: if key == "name" { - json!(generate_random_name("randomslug")) - } else { - value.clone() - }, - }), - ctx.test_pat.as_deref(), - ) - .await - } +async fn permissions_edit_details() { + with_test_environment( + Some(12), + |test_env: TestEnvironment| async move { + let zeta_organization_id = + &test_env.dummy.organization_zeta.organization_id; + let zeta_team_id = &test_env.dummy.organization_zeta.team_id; + + let api = &test_env.api; + let edit_details = OrganizationPermissions::EDIT_DETAILS; + + // Icon edit + // Uses alpha organization to delete this icon + let req_gen = |ctx: PermissionsTestContext| async move { + api.edit_organization_icon( + &ctx.organization_id.unwrap(), + Some(DummyImage::SmallIcon.get_icon_data()), + ctx.test_pat.as_deref(), + ) + .await }; PermissionsTest::new(&test_env) + .with_existing_organization(zeta_organization_id, zeta_team_id) + .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) .simple_organization_permissions_test(edit_details, req_gen) .await .unwrap(); - } - }) - .await; -} - -// Not covered by PATCH /organization -#[actix_rt::test] -async fn permissions_edit_details() { - with_test_environment(Some(12), |test_env: TestEnvironment| async move { - let zeta_organization_id = &test_env.dummy.organization_zeta.organization_id; - let zeta_team_id = &test_env.dummy.organization_zeta.team_id; - - let api = &test_env.api; - let edit_details = OrganizationPermissions::EDIT_DETAILS; - // Icon edit - // Uses alpha organization to delete this icon - let req_gen = |ctx: PermissionsTestContext| async move { - api.edit_organization_icon( - &ctx.organization_id.unwrap(), - Some(DummyImage::SmallIcon.get_icon_data()), - ctx.test_pat.as_deref(), - ) - .await - }; - PermissionsTest::new(&test_env) - .with_existing_organization(zeta_organization_id, zeta_team_id) - .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) - .simple_organization_permissions_test(edit_details, req_gen) - .await - .unwrap(); - - // Icon delete - // Uses alpha project to delete added icon - let req_gen = |ctx: PermissionsTestContext| async move { - api.edit_organization_icon(&ctx.organization_id.unwrap(), None, ctx.test_pat.as_deref()) + // Icon delete + // Uses alpha project to delete added icon + let req_gen = |ctx: PermissionsTestContext| async move { + api.edit_organization_icon( + &ctx.organization_id.unwrap(), + None, + ctx.test_pat.as_deref(), + ) .await - }; - PermissionsTest::new(&test_env) - .with_existing_organization(zeta_organization_id, zeta_team_id) - .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) - .simple_organization_permissions_test(edit_details, req_gen) - .await - .unwrap(); - }) + }; + PermissionsTest::new(&test_env) + .with_existing_organization(zeta_organization_id, zeta_team_id) + .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) + .simple_organization_permissions_test(edit_details, req_gen) + .await + .unwrap(); + }, + ) .await; } @@ -829,7 +1008,8 @@ async fn permissions_manage_invites() { with_test_environment_all(None, |test_env| async move { let api = &test_env.api; - let zeta_organization_id = &test_env.dummy.organization_zeta.organization_id; + let zeta_organization_id = + &test_env.dummy.organization_zeta.organization_id; let zeta_team_id = &test_env.dummy.organization_zeta.team_id; let manage_invites = OrganizationPermissions::MANAGE_INVITES; @@ -875,8 +1055,12 @@ async fn permissions_manage_invites() { // remove member // requires manage_invites if they have not yet accepted the invite let req_gen = |ctx: PermissionsTestContext| async move { - api.remove_from_team(&ctx.team_id.unwrap(), MOD_USER_ID, ctx.test_pat.as_deref()) - .await + api.remove_from_team( + &ctx.team_id.unwrap(), + MOD_USER_ID, + ctx.test_pat.as_deref(), + ) + .await }; PermissionsTest::new(&test_env) .with_existing_organization(zeta_organization_id, zeta_team_id) @@ -887,7 +1071,13 @@ async fn permissions_manage_invites() { // re-add member for testing let resp = api - .add_user_to_team(zeta_team_id, MOD_USER_ID, None, None, ADMIN_USER_PAT) + .add_user_to_team( + zeta_team_id, + MOD_USER_ID, + None, + None, + ADMIN_USER_PAT, + ) .await; assert_status!(&resp, StatusCode::NO_CONTENT); let resp = api.join_team(zeta_team_id, MOD_USER_PAT).await; @@ -896,8 +1086,12 @@ async fn permissions_manage_invites() { // remove existing member (requires remove_member) let remove_member = OrganizationPermissions::REMOVE_MEMBER; let req_gen = |ctx: PermissionsTestContext| async move { - api.remove_from_team(&ctx.team_id.unwrap(), MOD_USER_ID, ctx.test_pat.as_deref()) - .await + api.remove_from_team( + &ctx.team_id.unwrap(), + MOD_USER_ID, + ctx.test_pat.as_deref(), + ) + .await }; PermissionsTest::new(&test_env) @@ -912,98 +1106,124 @@ async fn permissions_manage_invites() { #[actix_rt::test] async fn permissions_add_remove_project() { - with_test_environment(None, |test_env: TestEnvironment| async move { - let api = &test_env.api; + with_test_environment( + None, + |test_env: TestEnvironment| async move { + let api = &test_env.api; - let alpha_project_id = &test_env.dummy.project_alpha.project_id; - let alpha_team_id = &test_env.dummy.project_alpha.team_id; - let zeta_organization_id = &test_env.dummy.organization_zeta.organization_id; - let zeta_team_id = &test_env.dummy.organization_zeta.team_id; + let alpha_project_id = &test_env.dummy.project_alpha.project_id; + let alpha_team_id = &test_env.dummy.project_alpha.team_id; + let zeta_organization_id = + &test_env.dummy.organization_zeta.organization_id; + let zeta_team_id = &test_env.dummy.organization_zeta.team_id; - let add_project = OrganizationPermissions::ADD_PROJECT; + let add_project = OrganizationPermissions::ADD_PROJECT; - // First, we add FRIEND_USER_ID to the alpha project and transfer ownership to them - // This is because the ownership of a project is needed to add it to an organization - let resp = api - .add_user_to_team(alpha_team_id, FRIEND_USER_ID, None, None, USER_USER_PAT) - .await; - assert_status!(&resp, StatusCode::NO_CONTENT); - let resp = api.join_team(alpha_team_id, FRIEND_USER_PAT).await; - assert_status!(&resp, StatusCode::NO_CONTENT); - let resp = api - .transfer_team_ownership(alpha_team_id, FRIEND_USER_ID, USER_USER_PAT) - .await; - assert_status!(&resp, StatusCode::NO_CONTENT); + // First, we add FRIEND_USER_ID to the alpha project and transfer ownership to them + // This is because the ownership of a project is needed to add it to an organization + let resp = api + .add_user_to_team( + alpha_team_id, + FRIEND_USER_ID, + None, + None, + USER_USER_PAT, + ) + .await; + assert_status!(&resp, StatusCode::NO_CONTENT); + let resp = api.join_team(alpha_team_id, FRIEND_USER_PAT).await; + assert_status!(&resp, StatusCode::NO_CONTENT); + let resp = api + .transfer_team_ownership( + alpha_team_id, + FRIEND_USER_ID, + USER_USER_PAT, + ) + .await; + assert_status!(&resp, StatusCode::NO_CONTENT); - // Now, FRIEND_USER_ID owns the alpha project - // Add alpha project to zeta organization - let req_gen = |ctx: PermissionsTestContext| async move { - api.organization_add_project( - &ctx.organization_id.unwrap(), - alpha_project_id, - ctx.test_pat.as_deref(), - ) - .await - }; - PermissionsTest::new(&test_env) - .with_existing_organization(zeta_organization_id, zeta_team_id) - .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) - .simple_organization_permissions_test(add_project, req_gen) - .await - .unwrap(); + // Now, FRIEND_USER_ID owns the alpha project + // Add alpha project to zeta organization + let req_gen = |ctx: PermissionsTestContext| async move { + api.organization_add_project( + &ctx.organization_id.unwrap(), + alpha_project_id, + ctx.test_pat.as_deref(), + ) + .await + }; + PermissionsTest::new(&test_env) + .with_existing_organization(zeta_organization_id, zeta_team_id) + .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) + .simple_organization_permissions_test(add_project, req_gen) + .await + .unwrap(); - // Remove alpha project from zeta organization - let remove_project = OrganizationPermissions::REMOVE_PROJECT; - let req_gen = |ctx: PermissionsTestContext| async move { - api.organization_remove_project( - &ctx.organization_id.unwrap(), - alpha_project_id, - UserId(FRIEND_USER_ID_PARSED as u64), - ctx.test_pat.as_deref(), - ) - .await - }; - PermissionsTest::new(&test_env) - .with_existing_organization(zeta_organization_id, zeta_team_id) - .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) - .simple_organization_permissions_test(remove_project, req_gen) - .await - .unwrap(); - }) + // Remove alpha project from zeta organization + let remove_project = OrganizationPermissions::REMOVE_PROJECT; + let req_gen = |ctx: PermissionsTestContext| async move { + api.organization_remove_project( + &ctx.organization_id.unwrap(), + alpha_project_id, + UserId(FRIEND_USER_ID_PARSED as u64), + ctx.test_pat.as_deref(), + ) + .await + }; + PermissionsTest::new(&test_env) + .with_existing_organization(zeta_organization_id, zeta_team_id) + .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) + .simple_organization_permissions_test(remove_project, req_gen) + .await + .unwrap(); + }, + ) .await; } #[actix_rt::test] async fn permissions_delete_organization() { - with_test_environment(None, |test_env: TestEnvironment| async move { - let api = &test_env.api; - let delete_organization = OrganizationPermissions::DELETE_ORGANIZATION; - - // Now, FRIEND_USER_ID owns the alpha project - // Add alpha project to zeta organization - let req_gen = |ctx: PermissionsTestContext| async move { - api.delete_organization(&ctx.organization_id.unwrap(), ctx.test_pat.as_deref()) + with_test_environment( + None, + |test_env: TestEnvironment| async move { + let api = &test_env.api; + let delete_organization = + OrganizationPermissions::DELETE_ORGANIZATION; + + // Now, FRIEND_USER_ID owns the alpha project + // Add alpha project to zeta organization + let req_gen = |ctx: PermissionsTestContext| async move { + api.delete_organization( + &ctx.organization_id.unwrap(), + ctx.test_pat.as_deref(), + ) .await - }; - PermissionsTest::new(&test_env) - .simple_organization_permissions_test(delete_organization, req_gen) - .await - .unwrap(); - }) + }; + PermissionsTest::new(&test_env) + .simple_organization_permissions_test( + delete_organization, + req_gen, + ) + .await + .unwrap(); + }, + ) .await; } #[actix_rt::test] async fn permissions_add_default_project_permissions() { with_test_environment_all(None, |test_env| async move { - let zeta_organization_id = &test_env.dummy.organization_zeta.organization_id; + let zeta_organization_id = + &test_env.dummy.organization_zeta.organization_id; let zeta_team_id = &test_env.dummy.organization_zeta.team_id; let api = &test_env.api; // Add member - let add_member_default_permissions = OrganizationPermissions::MANAGE_INVITES - | OrganizationPermissions::EDIT_MEMBER_DEFAULT_PERMISSIONS; + let add_member_default_permissions = + OrganizationPermissions::MANAGE_INVITES + | OrganizationPermissions::EDIT_MEMBER_DEFAULT_PERMISSIONS; // Failure test should include MANAGE_INVITES, as it is required to add // default permissions on an invited user, but should still fail without EDIT_MEMBER_DEFAULT_PERMISSIONS @@ -1015,7 +1235,10 @@ async fn permissions_add_default_project_permissions() { api.add_user_to_team( &ctx.team_id.unwrap(), MOD_USER_ID, - Some(ProjectPermissions::UPLOAD_VERSION | ProjectPermissions::DELETE_VERSION), + Some( + ProjectPermissions::UPLOAD_VERSION + | ProjectPermissions::DELETE_VERSION, + ), Some(OrganizationPermissions::empty()), ctx.test_pat.as_deref(), ) @@ -1025,13 +1248,17 @@ async fn permissions_add_default_project_permissions() { .with_existing_organization(zeta_organization_id, zeta_team_id) .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) .with_failure_permissions(None, Some(failure_with_add_member)) - .simple_organization_permissions_test(add_member_default_permissions, req_gen) + .simple_organization_permissions_test( + add_member_default_permissions, + req_gen, + ) .await .unwrap(); // Now that member is added, modify default permissions - let modify_member_default_permission = OrganizationPermissions::EDIT_MEMBER - | OrganizationPermissions::EDIT_MEMBER_DEFAULT_PERMISSIONS; + let modify_member_default_permission = + OrganizationPermissions::EDIT_MEMBER + | OrganizationPermissions::EDIT_MEMBER_DEFAULT_PERMISSIONS; // Failure test should include MANAGE_INVITES, as it is required to add // default permissions on an invited user, but should still fail without EDIT_MEMBER_DEFAULT_PERMISSIONS @@ -1054,7 +1281,10 @@ async fn permissions_add_default_project_permissions() { .with_existing_organization(zeta_organization_id, zeta_team_id) .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) .with_failure_permissions(None, Some(failure_with_modify_member)) - .simple_organization_permissions_test(modify_member_default_permission, req_gen) + .simple_organization_permissions_test( + modify_member_default_permission, + req_gen, + ) .await .unwrap(); }) @@ -1063,25 +1293,31 @@ async fn permissions_add_default_project_permissions() { #[actix_rt::test] async fn permissions_organization_permissions_consistency_test() { - with_test_environment(None, |test_env: TestEnvironment| async move { - let api = &test_env.api; - // Ensuring that permission are as we expect them to be - // Full organization permissions test - let success_permissions = OrganizationPermissions::EDIT_DETAILS; - let req_gen = |ctx: PermissionsTestContext| async move { - api.edit_organization( - &ctx.organization_id.unwrap(), - json!({ - "description": "Example description - changed.", - }), - ctx.test_pat.as_deref(), - ) - .await - }; - PermissionsTest::new(&test_env) - .full_organization_permissions_tests(success_permissions, req_gen) - .await - .unwrap(); - }) + with_test_environment( + None, + |test_env: TestEnvironment| async move { + let api = &test_env.api; + // Ensuring that permission are as we expect them to be + // Full organization permissions test + let success_permissions = OrganizationPermissions::EDIT_DETAILS; + let req_gen = |ctx: PermissionsTestContext| async move { + api.edit_organization( + &ctx.organization_id.unwrap(), + json!({ + "description": "Example description - changed.", + }), + ctx.test_pat.as_deref(), + ) + .await + }; + PermissionsTest::new(&test_env) + .full_organization_permissions_tests( + success_permissions, + req_gen, + ) + .await + .unwrap(); + }, + ) .await; } diff --git a/apps/labrinth/tests/project.rs b/apps/labrinth/tests/project.rs index ed096507f..27f0d1b1d 100644 --- a/apps/labrinth/tests/project.rs +++ b/apps/labrinth/tests/project.rs @@ -4,10 +4,14 @@ use common::api_v3::ApiV3; use common::database::*; use common::dummy_data::DUMMY_CATEGORIES; -use common::environment::{with_test_environment, with_test_environment_all, TestEnvironment}; +use common::environment::{ + with_test_environment, with_test_environment_all, TestEnvironment, +}; use common::permissions::{PermissionsTest, PermissionsTestContext}; use futures::StreamExt; -use labrinth::database::models::project_item::{PROJECTS_NAMESPACE, PROJECTS_SLUGS_NAMESPACE}; +use labrinth::database::models::project_item::{ + PROJECTS_NAMESPACE, PROJECTS_SLUGS_NAMESPACE, +}; use labrinth::models::ids::base62_impl::parse_base62; use labrinth::models::projects::ProjectId; use labrinth::models::teams::ProjectPermissions; @@ -18,7 +22,8 @@ use crate::common::api_common::models::{CommonItemType, CommonProject}; use crate::common::api_common::request_data::ProjectCreationRequestData; use crate::common::api_common::{ApiProject, ApiTeams, ApiVersion}; use crate::common::dummy_data::{ - DummyImage, DummyOrganizationZeta, DummyProjectAlpha, DummyProjectBeta, TestFile, + DummyImage, DummyOrganizationZeta, DummyProjectAlpha, DummyProjectBeta, + TestFile, }; mod common; @@ -68,7 +73,8 @@ async fn test_get_project() { .await .unwrap() .unwrap(); - let cached_project: serde_json::Value = serde_json::from_str(&cached_project).unwrap(); + let cached_project: serde_json::Value = + serde_json::from_str(&cached_project).unwrap(); assert_eq!( cached_project["val"]["inner"]["slug"], json!(alpha_project_slug) @@ -96,394 +102,426 @@ async fn test_get_project() { #[actix_rt::test] async fn test_add_remove_project() { // Test setup and dummy data - with_test_environment(None, |test_env: TestEnvironment| async move { - let api = &test_env.api; - - // Generate test project data. - let mut json_data = api - .get_public_project_creation_data_json("demo", Some(&TestFile::BasicMod)) - .await; - - // Basic json - let json_segment = MultipartSegment { - name: "data".to_string(), - filename: None, - content_type: Some("application/json".to_string()), - data: MultipartSegmentData::Text(serde_json::to_string(&json_data).unwrap()), - }; - - // Basic json, with a different file - json_data["initial_versions"][0]["file_parts"][0] = json!("basic-mod-different.jar"); - let json_diff_file_segment = MultipartSegment { - data: MultipartSegmentData::Text(serde_json::to_string(&json_data).unwrap()), - ..json_segment.clone() - }; - - // Basic json, with a different file, and a different slug - json_data["slug"] = json!("new_demo"); - json_data["initial_versions"][0]["file_parts"][0] = json!("basic-mod-different.jar"); - let json_diff_slug_file_segment = MultipartSegment { - data: MultipartSegmentData::Text(serde_json::to_string(&json_data).unwrap()), - ..json_segment.clone() - }; - - let basic_mod_file = TestFile::BasicMod; - let basic_mod_different_file = TestFile::BasicModDifferent; - - // Basic file - let file_segment = MultipartSegment { - // 'Basic' - name: basic_mod_file.filename(), - filename: Some(basic_mod_file.filename()), - content_type: basic_mod_file.content_type(), - data: MultipartSegmentData::Binary(basic_mod_file.bytes()), - }; - - // Differently named file, with the SAME content (for hash testing) - let file_diff_name_segment = MultipartSegment { - // 'Different' - name: basic_mod_different_file.filename(), - filename: Some(basic_mod_different_file.filename()), - content_type: basic_mod_different_file.content_type(), - // 'Basic' - data: MultipartSegmentData::Binary(basic_mod_file.bytes()), - }; + with_test_environment( + None, + |test_env: TestEnvironment| async move { + let api = &test_env.api; + + // Generate test project data. + let mut json_data = api + .get_public_project_creation_data_json( + "demo", + Some(&TestFile::BasicMod), + ) + .await; - // Differently named file, with different content - let file_diff_name_content_segment = MultipartSegment { - // 'Different' - name: basic_mod_different_file.filename(), - filename: Some(basic_mod_different_file.filename()), - content_type: basic_mod_different_file.content_type(), - data: MultipartSegmentData::Binary(basic_mod_different_file.bytes()), - }; + // Basic json + let json_segment = MultipartSegment { + name: "data".to_string(), + filename: None, + content_type: Some("application/json".to_string()), + data: MultipartSegmentData::Text( + serde_json::to_string(&json_data).unwrap(), + ), + }; + + // Basic json, with a different file + json_data["initial_versions"][0]["file_parts"][0] = + json!("basic-mod-different.jar"); + let json_diff_file_segment = MultipartSegment { + data: MultipartSegmentData::Text( + serde_json::to_string(&json_data).unwrap(), + ), + ..json_segment.clone() + }; + + // Basic json, with a different file, and a different slug + json_data["slug"] = json!("new_demo"); + json_data["initial_versions"][0]["file_parts"][0] = + json!("basic-mod-different.jar"); + let json_diff_slug_file_segment = MultipartSegment { + data: MultipartSegmentData::Text( + serde_json::to_string(&json_data).unwrap(), + ), + ..json_segment.clone() + }; + + let basic_mod_file = TestFile::BasicMod; + let basic_mod_different_file = TestFile::BasicModDifferent; + + // Basic file + let file_segment = MultipartSegment { + // 'Basic' + name: basic_mod_file.filename(), + filename: Some(basic_mod_file.filename()), + content_type: basic_mod_file.content_type(), + data: MultipartSegmentData::Binary(basic_mod_file.bytes()), + }; + + // Differently named file, with the SAME content (for hash testing) + let file_diff_name_segment = MultipartSegment { + // 'Different' + name: basic_mod_different_file.filename(), + filename: Some(basic_mod_different_file.filename()), + content_type: basic_mod_different_file.content_type(), + // 'Basic' + data: MultipartSegmentData::Binary(basic_mod_file.bytes()), + }; + + // Differently named file, with different content + let file_diff_name_content_segment = MultipartSegment { + // 'Different' + name: basic_mod_different_file.filename(), + filename: Some(basic_mod_different_file.filename()), + content_type: basic_mod_different_file.content_type(), + data: MultipartSegmentData::Binary( + basic_mod_different_file.bytes(), + ), + }; - // Add a project- simple, should work. - let resp = api - .create_project( - ProjectCreationRequestData { - slug: "demo".to_string(), - segment_data: vec![json_segment.clone(), file_segment.clone()], - jar: None, // File not needed at this point - }, - USER_USER_PAT, - ) - .await; + // Add a project- simple, should work. + let resp = api + .create_project( + ProjectCreationRequestData { + slug: "demo".to_string(), + segment_data: vec![ + json_segment.clone(), + file_segment.clone(), + ], + jar: None, // File not needed at this point + }, + USER_USER_PAT, + ) + .await; - assert_status!(&resp, StatusCode::OK); + assert_status!(&resp, StatusCode::OK); - // Get the project we just made, and confirm that it's correct - let project = api - .get_project_deserialized_common("demo", USER_USER_PAT) - .await; - assert!(project.versions.len() == 1); - let uploaded_version_id = project.versions[0]; - - // Checks files to ensure they were uploaded and correctly identify the file - let hash = sha1::Sha1::from(basic_mod_file.bytes()) - .digest() - .to_string(); - let version = api - .get_version_from_hash_deserialized_common(&hash, "sha1", USER_USER_PAT) - .await; - assert_eq!(version.id, uploaded_version_id); + // Get the project we just made, and confirm that it's correct + let project = api + .get_project_deserialized_common("demo", USER_USER_PAT) + .await; + assert!(project.versions.len() == 1); + let uploaded_version_id = project.versions[0]; + + // Checks files to ensure they were uploaded and correctly identify the file + let hash = sha1::Sha1::from(basic_mod_file.bytes()) + .digest() + .to_string(); + let version = api + .get_version_from_hash_deserialized_common( + &hash, + "sha1", + USER_USER_PAT, + ) + .await; + assert_eq!(version.id, uploaded_version_id); - // Reusing with a different slug and the same file should fail - // Even if that file is named differently - let resp = api - .create_project( - ProjectCreationRequestData { - slug: "demo".to_string(), - segment_data: vec![ - json_diff_slug_file_segment.clone(), - file_diff_name_segment.clone(), - ], - jar: None, // File not needed at this point - }, - USER_USER_PAT, - ) - .await; - assert_status!(&resp, StatusCode::BAD_REQUEST); + // Reusing with a different slug and the same file should fail + // Even if that file is named differently + let resp = api + .create_project( + ProjectCreationRequestData { + slug: "demo".to_string(), + segment_data: vec![ + json_diff_slug_file_segment.clone(), + file_diff_name_segment.clone(), + ], + jar: None, // File not needed at this point + }, + USER_USER_PAT, + ) + .await; + assert_status!(&resp, StatusCode::BAD_REQUEST); - // Reusing with the same slug and a different file should fail - let resp = api - .create_project( - ProjectCreationRequestData { - slug: "demo".to_string(), - segment_data: vec![ - json_diff_file_segment.clone(), - file_diff_name_content_segment.clone(), - ], - jar: None, // File not needed at this point - }, - USER_USER_PAT, - ) - .await; - assert_status!(&resp, StatusCode::BAD_REQUEST); + // Reusing with the same slug and a different file should fail + let resp = api + .create_project( + ProjectCreationRequestData { + slug: "demo".to_string(), + segment_data: vec![ + json_diff_file_segment.clone(), + file_diff_name_content_segment.clone(), + ], + jar: None, // File not needed at this point + }, + USER_USER_PAT, + ) + .await; + assert_status!(&resp, StatusCode::BAD_REQUEST); - // Different slug, different file should succeed - let resp = api - .create_project( - ProjectCreationRequestData { - slug: "demo".to_string(), - segment_data: vec![ - json_diff_slug_file_segment.clone(), - file_diff_name_content_segment.clone(), - ], - jar: None, // File not needed at this point - }, - USER_USER_PAT, - ) - .await; - assert_status!(&resp, StatusCode::OK); + // Different slug, different file should succeed + let resp = api + .create_project( + ProjectCreationRequestData { + slug: "demo".to_string(), + segment_data: vec![ + json_diff_slug_file_segment.clone(), + file_diff_name_content_segment.clone(), + ], + jar: None, // File not needed at this point + }, + USER_USER_PAT, + ) + .await; + assert_status!(&resp, StatusCode::OK); - // Get - let project = api - .get_project_deserialized_common("demo", USER_USER_PAT) - .await; - let id = project.id.to_string(); + // Get + let project = api + .get_project_deserialized_common("demo", USER_USER_PAT) + .await; + let id = project.id.to_string(); - // Remove the project - let resp = test_env.api.remove_project("demo", USER_USER_PAT).await; - assert_status!(&resp, StatusCode::NO_CONTENT); + // Remove the project + let resp = test_env.api.remove_project("demo", USER_USER_PAT).await; + assert_status!(&resp, StatusCode::NO_CONTENT); - // Confirm that the project is gone from the cache - let mut redis_pool = test_env.db.redis_pool.connect().await.unwrap(); - assert_eq!( - redis_pool - .get(PROJECTS_SLUGS_NAMESPACE, "demo") - .await - .unwrap() - .and_then(|x| x.parse::().ok()), - None - ); - assert_eq!( - redis_pool - .get(PROJECTS_SLUGS_NAMESPACE, &id) - .await - .unwrap() - .and_then(|x| x.parse::().ok()), - None - ); + // Confirm that the project is gone from the cache + let mut redis_pool = + test_env.db.redis_pool.connect().await.unwrap(); + assert_eq!( + redis_pool + .get(PROJECTS_SLUGS_NAMESPACE, "demo") + .await + .unwrap() + .and_then(|x| x.parse::().ok()), + None + ); + assert_eq!( + redis_pool + .get(PROJECTS_SLUGS_NAMESPACE, &id) + .await + .unwrap() + .and_then(|x| x.parse::().ok()), + None + ); - // Old slug no longer works - let resp = api.get_project("demo", USER_USER_PAT).await; - assert_status!(&resp, StatusCode::NOT_FOUND); - }) + // Old slug no longer works + let resp = api.get_project("demo", USER_USER_PAT).await; + assert_status!(&resp, StatusCode::NOT_FOUND); + }, + ) .await; } #[actix_rt::test] pub async fn test_patch_project() { - with_test_environment(None, |test_env: TestEnvironment| async move { - let api = &test_env.api; + with_test_environment( + None, + |test_env: TestEnvironment| async move { + let api = &test_env.api; - let alpha_project_slug = &test_env.dummy.project_alpha.project_slug; - let beta_project_slug = &test_env.dummy.project_beta.project_slug; + let alpha_project_slug = &test_env.dummy.project_alpha.project_slug; + let beta_project_slug = &test_env.dummy.project_beta.project_slug; - // First, we do some patch requests that should fail. - // Failure because the user is not authorized. - let resp = api - .edit_project( - alpha_project_slug, - json!({ - "name": "Test_Add_Project project - test 1", - }), - ENEMY_USER_PAT, - ) - .await; - assert_status!(&resp, StatusCode::UNAUTHORIZED); - - // Failure because we are setting URL fields to invalid urls. - for url_type in ["issues", "source", "wiki", "discord"] { + // First, we do some patch requests that should fail. + // Failure because the user is not authorized. let resp = api .edit_project( alpha_project_slug, json!({ - "link_urls": { - url_type: "not a url", - }, + "name": "Test_Add_Project project - test 1", }), - USER_USER_PAT, + ENEMY_USER_PAT, ) .await; - assert_status!(&resp, StatusCode::BAD_REQUEST); - } + assert_status!(&resp, StatusCode::UNAUTHORIZED); - // Failure because these are illegal requested statuses for a normal user. - for req in ["unknown", "processing", "withheld", "scheduled"] { - let resp = api - .edit_project( - alpha_project_slug, - json!({ - "requested_status": req, - }), - USER_USER_PAT, - ) - .await; - assert_status!(&resp, StatusCode::BAD_REQUEST); - } + // Failure because we are setting URL fields to invalid urls. + for url_type in ["issues", "source", "wiki", "discord"] { + let resp = api + .edit_project( + alpha_project_slug, + json!({ + "link_urls": { + url_type: "not a url", + }, + }), + USER_USER_PAT, + ) + .await; + assert_status!(&resp, StatusCode::BAD_REQUEST); + } - // Failure because these should not be able to be set by a non-mod - for key in ["moderation_message", "moderation_message_body"] { + // Failure because these are illegal requested statuses for a normal user. + for req in ["unknown", "processing", "withheld", "scheduled"] { + let resp = api + .edit_project( + alpha_project_slug, + json!({ + "requested_status": req, + }), + USER_USER_PAT, + ) + .await; + assert_status!(&resp, StatusCode::BAD_REQUEST); + } + + // Failure because these should not be able to be set by a non-mod + for key in ["moderation_message", "moderation_message_body"] { + let resp = api + .edit_project( + alpha_project_slug, + json!({ + key: "test", + }), + USER_USER_PAT, + ) + .await; + assert_status!(&resp, StatusCode::UNAUTHORIZED); + + // (should work for a mod, though) + let resp = api + .edit_project( + alpha_project_slug, + json!({ + key: "test", + }), + MOD_USER_PAT, + ) + .await; + assert_status!(&resp, StatusCode::NO_CONTENT); + } + + // Failed patch to alpha slug: + // - slug collision with beta + // - too short slug + // - too long slug + // - not url safe slug + // - not url safe slug + for slug in [ + beta_project_slug, + "a", + &"a".repeat(100), + "not url safe%&^!#$##!@#$%^&*()", + ] { + let resp = api + .edit_project( + alpha_project_slug, + json!({ + "slug": slug, // the other dummy project has this slug + }), + USER_USER_PAT, + ) + .await; + assert_status!(&resp, StatusCode::BAD_REQUEST); + } + + // Not allowed to directly set status, as 'beta_project_slug' (the other project) is "processing" and cannot have its status changed like this. let resp = api .edit_project( - alpha_project_slug, + beta_project_slug, json!({ - key: "test", + "status": "private" }), USER_USER_PAT, ) .await; assert_status!(&resp, StatusCode::UNAUTHORIZED); - // (should work for a mod, though) + // Sucessful request to patch many fields. let resp = api .edit_project( alpha_project_slug, json!({ - key: "test", + "slug": "newslug", + "categories": [DUMMY_CATEGORIES[0]], + "license_id": "MIT", + "link_urls": + { + "patreon": "https://patreon.com", + "issues": "https://github.com", + "discord": "https://discord.gg", + "wiki": "https://wiki.com" + } }), - MOD_USER_PAT, + USER_USER_PAT, ) .await; assert_status!(&resp, StatusCode::NO_CONTENT); - } - - // Failed patch to alpha slug: - // - slug collision with beta - // - too short slug - // - too long slug - // - not url safe slug - // - not url safe slug - for slug in [ - beta_project_slug, - "a", - &"a".repeat(100), - "not url safe%&^!#$##!@#$%^&*()", - ] { + + // Old slug no longer works + let resp = api.get_project(alpha_project_slug, USER_USER_PAT).await; + assert_status!(&resp, StatusCode::NOT_FOUND); + + // New slug does work + let project = + api.get_project_deserialized("newslug", USER_USER_PAT).await; + + assert_eq!(project.slug.unwrap(), "newslug"); + assert_eq!(project.categories, vec![DUMMY_CATEGORIES[0]]); + assert_eq!(project.license.id, "MIT"); + + let link_urls = project.link_urls; + assert_eq!(link_urls.len(), 4); + assert_eq!(link_urls["patreon"].platform, "patreon"); + assert_eq!(link_urls["patreon"].url, "https://patreon.com"); + assert!(link_urls["patreon"].donation); + assert_eq!(link_urls["issues"].platform, "issues"); + assert_eq!(link_urls["issues"].url, "https://github.com"); + assert!(!link_urls["issues"].donation); + assert_eq!(link_urls["discord"].platform, "discord"); + assert_eq!(link_urls["discord"].url, "https://discord.gg"); + assert!(!link_urls["discord"].donation); + assert_eq!(link_urls["wiki"].platform, "wiki"); + assert_eq!(link_urls["wiki"].url, "https://wiki.com"); + assert!(!link_urls["wiki"].donation); + + // Unset the set link_urls let resp = api .edit_project( - alpha_project_slug, + "newslug", json!({ - "slug": slug, // the other dummy project has this slug + "link_urls": + { + "issues": null, + } }), USER_USER_PAT, ) .await; - assert_status!(&resp, StatusCode::BAD_REQUEST); - } - - // Not allowed to directly set status, as 'beta_project_slug' (the other project) is "processing" and cannot have its status changed like this. - let resp = api - .edit_project( - beta_project_slug, - json!({ - "status": "private" - }), - USER_USER_PAT, - ) - .await; - assert_status!(&resp, StatusCode::UNAUTHORIZED); - - // Sucessful request to patch many fields. - let resp = api - .edit_project( - alpha_project_slug, - json!({ - "slug": "newslug", - "categories": [DUMMY_CATEGORIES[0]], - "license_id": "MIT", - "link_urls": - { - "patreon": "https://patreon.com", - "issues": "https://github.com", - "discord": "https://discord.gg", - "wiki": "https://wiki.com" - } - }), - USER_USER_PAT, - ) - .await; - assert_status!(&resp, StatusCode::NO_CONTENT); - - // Old slug no longer works - let resp = api.get_project(alpha_project_slug, USER_USER_PAT).await; - assert_status!(&resp, StatusCode::NOT_FOUND); - - // New slug does work - let project = api.get_project_deserialized("newslug", USER_USER_PAT).await; - - assert_eq!(project.slug.unwrap(), "newslug"); - assert_eq!(project.categories, vec![DUMMY_CATEGORIES[0]]); - assert_eq!(project.license.id, "MIT"); - - let link_urls = project.link_urls; - assert_eq!(link_urls.len(), 4); - assert_eq!(link_urls["patreon"].platform, "patreon"); - assert_eq!(link_urls["patreon"].url, "https://patreon.com"); - assert!(link_urls["patreon"].donation); - assert_eq!(link_urls["issues"].platform, "issues"); - assert_eq!(link_urls["issues"].url, "https://github.com"); - assert!(!link_urls["issues"].donation); - assert_eq!(link_urls["discord"].platform, "discord"); - assert_eq!(link_urls["discord"].url, "https://discord.gg"); - assert!(!link_urls["discord"].donation); - assert_eq!(link_urls["wiki"].platform, "wiki"); - assert_eq!(link_urls["wiki"].url, "https://wiki.com"); - assert!(!link_urls["wiki"].donation); - - // Unset the set link_urls - let resp = api - .edit_project( - "newslug", - json!({ - "link_urls": - { - "issues": null, - } - }), - USER_USER_PAT, - ) - .await; - assert_status!(&resp, StatusCode::NO_CONTENT); - let project = api.get_project_deserialized("newslug", USER_USER_PAT).await; - assert_eq!(project.link_urls.len(), 3); - assert!(!project.link_urls.contains_key("issues")); - }) + assert_status!(&resp, StatusCode::NO_CONTENT); + let project = + api.get_project_deserialized("newslug", USER_USER_PAT).await; + assert_eq!(project.link_urls.len(), 3); + assert!(!project.link_urls.contains_key("issues")); + }, + ) .await; } #[actix_rt::test] pub async fn test_patch_v3() { // Hits V3-specific patchable fields - with_test_environment(None, |test_env: TestEnvironment| async move { - let api = &test_env.api; + with_test_environment( + None, + |test_env: TestEnvironment| async move { + let api = &test_env.api; - let alpha_project_slug = &test_env.dummy.project_alpha.project_slug; + let alpha_project_slug = &test_env.dummy.project_alpha.project_slug; - // Sucessful request to patch many fields. - let resp = api - .edit_project( - alpha_project_slug, - json!({ - "name": "New successful title", - "summary": "New successful summary", - "description": "New successful description", - }), - USER_USER_PAT, - ) - .await; - assert_status!(&resp, StatusCode::NO_CONTENT); + // Sucessful request to patch many fields. + let resp = api + .edit_project( + alpha_project_slug, + json!({ + "name": "New successful title", + "summary": "New successful summary", + "description": "New successful description", + }), + USER_USER_PAT, + ) + .await; + assert_status!(&resp, StatusCode::NO_CONTENT); - let project = api - .get_project_deserialized(alpha_project_slug, USER_USER_PAT) - .await; + let project = api + .get_project_deserialized(alpha_project_slug, USER_USER_PAT) + .await; - assert_eq!(project.name, "New successful title"); - assert_eq!(project.summary, "New successful summary"); - assert_eq!(project.description, "New successful description"); - }) + assert_eq!(project.name, "New successful title"); + assert_eq!(project.summary, "New successful summary"); + assert_eq!(project.description, "New successful description"); + }, + ) .await; } @@ -530,52 +568,62 @@ pub async fn test_bulk_edit_categories() { #[actix_rt::test] pub async fn test_bulk_edit_links() { - with_test_environment(None, |test_env: TestEnvironment| async move { - let api = &test_env.api; - let alpha_project_id: &str = &test_env.dummy.project_alpha.project_id; - let beta_project_id: &str = &test_env.dummy.project_beta.project_id; - - // Sets links for issue, source, wiki, and patreon for all projects - // The first loop, sets issue, the second, clears it for all projects. - for issues in [Some("https://www.issues.com"), None] { - let resp = api - .edit_project_bulk( - &[alpha_project_id, beta_project_id], - json!({ - "link_urls": { - "issues": issues, - "wiki": "https://wiki.com", - "patreon": "https://patreon.com", - }, - }), - ADMIN_USER_PAT, - ) - .await; - assert_status!(&resp, StatusCode::NO_CONTENT); + with_test_environment( + None, + |test_env: TestEnvironment| async move { + let api = &test_env.api; + let alpha_project_id: &str = + &test_env.dummy.project_alpha.project_id; + let beta_project_id: &str = &test_env.dummy.project_beta.project_id; + + // Sets links for issue, source, wiki, and patreon for all projects + // The first loop, sets issue, the second, clears it for all projects. + for issues in [Some("https://www.issues.com"), None] { + let resp = api + .edit_project_bulk( + &[alpha_project_id, beta_project_id], + json!({ + "link_urls": { + "issues": issues, + "wiki": "https://wiki.com", + "patreon": "https://patreon.com", + }, + }), + ADMIN_USER_PAT, + ) + .await; + assert_status!(&resp, StatusCode::NO_CONTENT); - let alpha_body = api - .get_project_deserialized(alpha_project_id, ADMIN_USER_PAT) - .await; - if let Some(issues) = issues { - assert_eq!(alpha_body.link_urls.len(), 3); - assert_eq!(alpha_body.link_urls["issues"].url, issues); - } else { - assert_eq!(alpha_body.link_urls.len(), 2); - assert!(!alpha_body.link_urls.contains_key("issues")); + let alpha_body = api + .get_project_deserialized(alpha_project_id, ADMIN_USER_PAT) + .await; + if let Some(issues) = issues { + assert_eq!(alpha_body.link_urls.len(), 3); + assert_eq!(alpha_body.link_urls["issues"].url, issues); + } else { + assert_eq!(alpha_body.link_urls.len(), 2); + assert!(!alpha_body.link_urls.contains_key("issues")); + } + assert_eq!( + alpha_body.link_urls["wiki"].url, + "https://wiki.com" + ); + assert_eq!( + alpha_body.link_urls["patreon"].url, + "https://patreon.com" + ); + + let beta_body = api + .get_project_deserialized(beta_project_id, ADMIN_USER_PAT) + .await; + assert_eq!(beta_body.categories, alpha_body.categories); + assert_eq!( + beta_body.additional_categories, + alpha_body.additional_categories, + ); } - assert_eq!(alpha_body.link_urls["wiki"].url, "https://wiki.com"); - assert_eq!(alpha_body.link_urls["patreon"].url, "https://patreon.com"); - - let beta_body = api - .get_project_deserialized(beta_project_id, ADMIN_USER_PAT) - .await; - assert_eq!(beta_body.categories, alpha_body.categories); - assert_eq!( - beta_body.additional_categories, - alpha_body.additional_categories, - ); - } - }) + }, + ) .await; } @@ -780,8 +828,12 @@ async fn permissions_edit_details() { // Icon delete // Uses alpha project to delete added icon let req_gen = |ctx: PermissionsTestContext| async move { - api.edit_project_icon(&ctx.project_id.unwrap(), None, ctx.test_pat.as_deref()) - .await + api.edit_project_icon( + &ctx.project_id.unwrap(), + None, + ctx.test_pat.as_deref(), + ) + .await }; PermissionsTest::new(&test_env) .with_existing_project(alpha_project_id, alpha_team_id) @@ -857,98 +909,104 @@ async fn permissions_edit_details() { #[actix_rt::test] async fn permissions_upload_version() { - with_test_environment(None, |test_env: TestEnvironment| async move { - let alpha_project_id = &test_env.dummy.project_alpha.project_id; - let alpha_version_id = &test_env.dummy.project_alpha.version_id; - let alpha_team_id = &test_env.dummy.project_alpha.team_id; - let alpha_file_hash = &test_env.dummy.project_alpha.file_hash; - - let api = &test_env.api; - - let upload_version = ProjectPermissions::UPLOAD_VERSION; - // Upload version with basic-mod.jar - let req_gen = |ctx: PermissionsTestContext| async move { - let project_id = ctx.project_id.unwrap(); - let project_id = ProjectId(parse_base62(&project_id).unwrap()); - api.add_public_version( - project_id, - "1.0.0", - TestFile::BasicMod, - None, - None, - ctx.test_pat.as_deref(), - ) - .await - }; - PermissionsTest::new(&test_env) - .simple_project_permissions_test(upload_version, req_gen) - .await - .unwrap(); - - // Upload file to existing version - // Uses alpha project, as it has an existing version - let req_gen = |ctx: PermissionsTestContext| async move { - api.upload_file_to_version( - alpha_version_id, - &TestFile::BasicModDifferent, - ctx.test_pat.as_deref(), - ) - .await - }; - PermissionsTest::new(&test_env) - .with_existing_project(alpha_project_id, alpha_team_id) - .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) - .simple_project_permissions_test(upload_version, req_gen) - .await - .unwrap(); - - // Patch version - // Uses alpha project, as it has an existing version - let req_gen = |ctx: PermissionsTestContext| async move { - api.edit_version( - alpha_version_id, - json!({ - "name": "Basic Mod", - }), - ctx.test_pat.as_deref(), - ) - .await - }; - PermissionsTest::new(&test_env) - .with_existing_project(alpha_project_id, alpha_team_id) - .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) - .simple_project_permissions_test(upload_version, req_gen) - .await - .unwrap(); - - // Delete version file - // Uses alpha project, as it has an existing version - let delete_version = ProjectPermissions::DELETE_VERSION; - let req_gen = |ctx: PermissionsTestContext| async move { - api.remove_version_file(alpha_file_hash, ctx.test_pat.as_deref()) + with_test_environment( + None, + |test_env: TestEnvironment| async move { + let alpha_project_id = &test_env.dummy.project_alpha.project_id; + let alpha_version_id = &test_env.dummy.project_alpha.version_id; + let alpha_team_id = &test_env.dummy.project_alpha.team_id; + let alpha_file_hash = &test_env.dummy.project_alpha.file_hash; + + let api = &test_env.api; + + let upload_version = ProjectPermissions::UPLOAD_VERSION; + // Upload version with basic-mod.jar + let req_gen = |ctx: PermissionsTestContext| async move { + let project_id = ctx.project_id.unwrap(); + let project_id = ProjectId(parse_base62(&project_id).unwrap()); + api.add_public_version( + project_id, + "1.0.0", + TestFile::BasicMod, + None, + None, + ctx.test_pat.as_deref(), + ) .await - }; + }; + PermissionsTest::new(&test_env) + .simple_project_permissions_test(upload_version, req_gen) + .await + .unwrap(); + + // Upload file to existing version + // Uses alpha project, as it has an existing version + let req_gen = |ctx: PermissionsTestContext| async move { + api.upload_file_to_version( + alpha_version_id, + &TestFile::BasicModDifferent, + ctx.test_pat.as_deref(), + ) + .await + }; + PermissionsTest::new(&test_env) + .with_existing_project(alpha_project_id, alpha_team_id) + .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) + .simple_project_permissions_test(upload_version, req_gen) + .await + .unwrap(); - PermissionsTest::new(&test_env) - .with_existing_project(alpha_project_id, alpha_team_id) - .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) - .simple_project_permissions_test(delete_version, req_gen) - .await - .unwrap(); + // Patch version + // Uses alpha project, as it has an existing version + let req_gen = |ctx: PermissionsTestContext| async move { + api.edit_version( + alpha_version_id, + json!({ + "name": "Basic Mod", + }), + ctx.test_pat.as_deref(), + ) + .await + }; + PermissionsTest::new(&test_env) + .with_existing_project(alpha_project_id, alpha_team_id) + .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) + .simple_project_permissions_test(upload_version, req_gen) + .await + .unwrap(); + + // Delete version file + // Uses alpha project, as it has an existing version + let delete_version = ProjectPermissions::DELETE_VERSION; + let req_gen = |ctx: PermissionsTestContext| async move { + api.remove_version_file( + alpha_file_hash, + ctx.test_pat.as_deref(), + ) + .await + }; - // Delete version - // Uses alpha project, as it has an existing version - let req_gen = |ctx: PermissionsTestContext| async move { - api.remove_version(alpha_version_id, ctx.test_pat.as_deref()) + PermissionsTest::new(&test_env) + .with_existing_project(alpha_project_id, alpha_team_id) + .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) + .simple_project_permissions_test(delete_version, req_gen) .await - }; - PermissionsTest::new(&test_env) - .with_existing_project(alpha_project_id, alpha_team_id) - .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) - .simple_project_permissions_test(delete_version, req_gen) - .await - .unwrap(); - }) + .unwrap(); + + // Delete version + // Uses alpha project, as it has an existing version + let req_gen = |ctx: PermissionsTestContext| async move { + api.remove_version(alpha_version_id, ctx.test_pat.as_deref()) + .await + }; + PermissionsTest::new(&test_env) + .with_existing_project(alpha_project_id, alpha_team_id) + .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) + .simple_project_permissions_test(delete_version, req_gen) + .await + .unwrap(); + }, + ) .await; } @@ -1003,8 +1061,12 @@ async fn permissions_manage_invites() { // remove member // requires manage_invites if they have not yet accepted the invite let req_gen = |ctx: PermissionsTestContext| async move { - api.remove_from_team(&ctx.team_id.unwrap(), MOD_USER_ID, ctx.test_pat.as_deref()) - .await + api.remove_from_team( + &ctx.team_id.unwrap(), + MOD_USER_ID, + ctx.test_pat.as_deref(), + ) + .await }; PermissionsTest::new(&test_env) .with_existing_project(alpha_project_id, alpha_team_id) @@ -1032,8 +1094,12 @@ async fn permissions_manage_invites() { // remove existing member (requires remove_member) let remove_member = ProjectPermissions::REMOVE_MEMBER; let req_gen = |ctx: PermissionsTestContext| async move { - api.remove_from_team(&ctx.team_id.unwrap(), MOD_USER_ID, ctx.test_pat.as_deref()) - .await + api.remove_from_team( + &ctx.team_id.unwrap(), + MOD_USER_ID, + ctx.test_pat.as_deref(), + ) + .await }; PermissionsTest::new(&test_env) @@ -1054,8 +1120,11 @@ async fn permissions_delete_project() { let api = &test_env.api; // Delete project let req_gen = |ctx: PermissionsTestContext| async move { - api.remove_project(&ctx.project_id.unwrap(), ctx.test_pat.as_deref()) - .await + api.remove_project( + &ctx.project_id.unwrap(), + ctx.test_pat.as_deref(), + ) + .await }; PermissionsTest::new(&test_env) .simple_project_permissions_test(delete_project, req_gen) @@ -1206,7 +1275,8 @@ async fn projects_various_visibility() { // Tests get_project, a route that uses is_visible_project for (project_id, pat, expected_status) in visible_pat_pairs { - let resp = env.api.get_project(&project_id.to_string(), pat).await; + let resp = + env.api.get_project(&project_id.to_string(), pat).await; assert_status!(&resp, expected_status); } @@ -1227,12 +1297,20 @@ async fn projects_various_visibility() { // Add projects to org zeta let resp = env .api - .organization_add_project(zeta_organization_id, alpha_project_id, USER_USER_PAT) + .organization_add_project( + zeta_organization_id, + alpha_project_id, + USER_USER_PAT, + ) .await; assert_status!(&resp, StatusCode::OK); let resp = env .api - .organization_add_project(zeta_organization_id, beta_project_id, USER_USER_PAT) + .organization_add_project( + zeta_organization_id, + beta_project_id, + USER_USER_PAT, + ) .await; assert_status!(&resp, StatusCode::OK); @@ -1251,7 +1329,8 @@ async fn projects_various_visibility() { ]; for (project_id, pat, expected_status) in visible_pat_pairs { - let resp = env.api.get_project(&project_id.to_string(), pat).await; + let resp = + env.api.get_project(&project_id.to_string(), pat).await; assert_status!(&resp, expected_status); } @@ -1266,7 +1345,8 @@ async fn projects_various_visibility() { .api .get_projects(&[&alpha_project_id, &beta_project_id], pat) .await; - let projects: Vec = test::read_body_json(projects).await; + let projects: Vec = + test::read_body_json(projects).await; assert_eq!(projects.len(), expected_count); } }, diff --git a/apps/labrinth/tests/scopes.rs b/apps/labrinth/tests/scopes.rs index 387d71215..1d19d2b4f 100644 --- a/apps/labrinth/tests/scopes.rs +++ b/apps/labrinth/tests/scopes.rs @@ -1,7 +1,11 @@ use std::collections::HashMap; -use crate::common::api_common::{ApiProject, ApiTeams, ApiUser, ApiVersion, AppendsOptionalPat}; -use crate::common::dummy_data::{DummyImage, DummyProjectAlpha, DummyProjectBeta}; +use crate::common::api_common::{ + ApiProject, ApiTeams, ApiUser, ApiVersion, AppendsOptionalPat, +}; +use crate::common::dummy_data::{ + DummyImage, DummyProjectAlpha, DummyProjectBeta, +}; use actix_http::StatusCode; use actix_web::test; use chrono::{Duration, Utc}; @@ -10,7 +14,9 @@ use common::api_common::Api; use common::api_v3::request_data::get_public_project_creation_data; use common::api_v3::ApiV3; use common::dummy_data::TestFile; -use common::environment::{with_test_environment, with_test_environment_all, TestEnvironment}; +use common::environment::{ + with_test_environment, with_test_environment_all, TestEnvironment, +}; use common::{database::*, scopes::ScopeTest}; use labrinth::models::ids::base62_impl::parse_base62; use labrinth::models::pats::Scopes; @@ -34,8 +40,9 @@ async fn user_scopes() { let api = &test_env.api; // User reading let read_user = Scopes::USER_READ; - let req_gen = - |pat: Option| async move { api.get_current_user(pat.as_deref()).await }; + let req_gen = |pat: Option| async move { + api.get_current_user(pat.as_deref()).await + }; let (_, success) = ScopeTest::new(&test_env) .test(req_gen, read_user) .await @@ -45,8 +52,9 @@ async fn user_scopes() { // Email reading let read_email = Scopes::USER_READ | Scopes::USER_READ_EMAIL; - let req_gen = - |pat: Option| async move { api.get_current_user(pat.as_deref()).await }; + let req_gen = |pat: Option| async move { + api.get_current_user(pat.as_deref()).await + }; let (_, success) = ScopeTest::new(&test_env) .test(req_gen, read_email) .await @@ -55,8 +63,9 @@ async fn user_scopes() { // Payout reading let read_payout = Scopes::USER_READ | Scopes::PAYOUTS_READ; - let req_gen = - |pat: Option| async move { api.get_current_user(pat.as_deref()).await }; + let req_gen = |pat: Option| async move { + api.get_current_user(pat.as_deref()).await + }; let (_, success) = ScopeTest::new(&test_env) .test(req_gen, read_payout) .await @@ -91,8 +100,9 @@ async fn user_scopes() { // User deletion // (The failure is first, and this is the last test for this test function, we can delete it and use the same PAT for both tests) let delete_user = Scopes::USER_DELETE; - let req_gen = - |pat: Option| async move { api.delete_user("enemy", pat.as_deref()).await }; + let req_gen = |pat: Option| async move { + api.delete_user("enemy", pat.as_deref()).await + }; ScopeTest::new(&test_env) .with_user_id(ENEMY_USER_ID_PARSED) .test(req_gen, delete_user) @@ -113,7 +123,13 @@ pub async fn notifications_scopes() { // Get notifications let resp = test_env .api - .add_user_to_team(alpha_team_id, FRIEND_USER_ID, None, None, USER_USER_PAT) + .add_user_to_team( + alpha_team_id, + FRIEND_USER_ID, + None, + None, + USER_USER_PAT, + ) .await; assert_status!(&resp, StatusCode::NO_CONTENT); @@ -186,7 +202,13 @@ pub async fn notifications_scopes() { // We invite mod, get the notification ID, and do mass delete using that let resp = test_env .api - .add_user_to_team(alpha_team_id, MOD_USER_ID, None, None, USER_USER_PAT) + .add_user_to_team( + alpha_team_id, + MOD_USER_ID, + None, + None, + USER_USER_PAT, + ) .await; assert_status!(&resp, StatusCode::NO_CONTENT); let read_notifications = Scopes::NOTIFICATION_READ; @@ -217,41 +239,47 @@ pub async fn notifications_scopes() { // Project version creation scopes #[actix_rt::test] pub async fn project_version_create_scopes_v3() { - with_test_environment(None, |test_env: TestEnvironment| async move { - let api = &test_env.api; - - // Create project - let create_project = Scopes::PROJECT_CREATE; - let req_gen = |pat: Option| async move { - let creation_data = - get_public_project_creation_data("demo", Some(TestFile::BasicMod), None); - api.create_project(creation_data, pat.as_deref()).await - }; - let (_, success) = ScopeTest::new(&test_env) - .test(req_gen, create_project) - .await - .unwrap(); - let project_id = success["id"].as_str().unwrap(); - let project_id = ProjectId(parse_base62(project_id).unwrap()); - - // Add version to project - let create_version = Scopes::VERSION_CREATE; - let req_gen = |pat: Option| async move { - api.add_public_version( - project_id, - "1.2.3.4", - TestFile::BasicModDifferent, - None, - None, - pat.as_deref(), - ) - .await - }; - ScopeTest::new(&test_env) - .test(req_gen, create_version) - .await - .unwrap(); - }) + with_test_environment( + None, + |test_env: TestEnvironment| async move { + let api = &test_env.api; + + // Create project + let create_project = Scopes::PROJECT_CREATE; + let req_gen = |pat: Option| async move { + let creation_data = get_public_project_creation_data( + "demo", + Some(TestFile::BasicMod), + None, + ); + api.create_project(creation_data, pat.as_deref()).await + }; + let (_, success) = ScopeTest::new(&test_env) + .test(req_gen, create_project) + .await + .unwrap(); + let project_id = success["id"].as_str().unwrap(); + let project_id = ProjectId(parse_base62(project_id).unwrap()); + + // Add version to project + let create_version = Scopes::VERSION_CREATE; + let req_gen = |pat: Option| async move { + api.add_public_version( + project_id, + "1.2.3.4", + TestFile::BasicModDifferent, + None, + None, + pat.as_deref(), + ) + .await + }; + ScopeTest::new(&test_env) + .test(req_gen, create_version) + .await + .unwrap(); + }, + ) .await; } @@ -384,7 +412,11 @@ pub async fn project_version_reads_scopes() { let read_version = Scopes::VERSION_READ; let resp = test_env .api - .edit_version(beta_version_id, json!({ "status": "draft" }), USER_USER_PAT) + .edit_version( + beta_version_id, + json!({ "status": "draft" }), + USER_USER_PAT, + ) .await; assert_status!(&resp, StatusCode::NO_CONTENT); @@ -399,8 +431,12 @@ pub async fn project_version_reads_scopes() { .unwrap(); let req_gen = |pat: Option| async move { - api.download_version_redirect(beta_file_hash, "sha1", pat.as_deref()) - .await + api.download_version_redirect( + beta_file_hash, + "sha1", + pat.as_deref(), + ) + .await }; ScopeTest::new(&test_env) .with_failure_code(404) @@ -417,8 +453,12 @@ pub async fn project_version_reads_scopes() { // ScopeTest::new(&test_env).with_failure_code(404).test(req_gen, read_version).await.unwrap(); let req_gen = |pat: Option| async move { - api.get_versions_from_hashes(&[beta_file_hash], "sha1", pat.as_deref()) - .await + api.get_versions_from_hashes( + &[beta_file_hash], + "sha1", + pat.as_deref(), + ) + .await }; let (failure, success) = ScopeTest::new(&test_env) .with_failure_code(200) @@ -439,7 +479,8 @@ pub async fn project_version_reads_scopes() { // assert!(success.as_object().unwrap().contains_key(beta_file_hash)); // Both project and version reading - let read_project_and_version = Scopes::PROJECT_READ | Scopes::VERSION_READ; + let read_project_and_version = + Scopes::PROJECT_READ | Scopes::VERSION_READ; let req_gen = |pat: Option| async move { api.get_project_versions( beta_project_id, @@ -681,8 +722,12 @@ pub async fn version_write_scopes() { // Upload version file let req_gen = |pat: Option| async move { - api.upload_file_to_version(alpha_version_id, &TestFile::BasicZip, pat.as_deref()) - .await + api.upload_file_to_version( + alpha_version_id, + &TestFile::BasicZip, + pat.as_deref(), + ) + .await }; ScopeTest::new(&test_env) .test(req_gen, write_version) @@ -740,16 +785,18 @@ pub async fn report_scopes() { // Get reports let report_read = Scopes::REPORT_READ; - let req_gen = - |pat: Option| async move { api.get_user_reports(pat.as_deref()).await }; + let req_gen = |pat: Option| async move { + api.get_user_reports(pat.as_deref()).await + }; let (_, success) = ScopeTest::new(&test_env) .test(req_gen, report_read) .await .unwrap(); let report_id = success[0]["id"].as_str().unwrap(); - let req_gen = - |pat: Option| async move { api.get_report(report_id, pat.as_deref()).await }; + let req_gen = |pat: Option| async move { + api.get_report(report_id, pat.as_deref()).await + }; ScopeTest::new(&test_env) .test(req_gen, report_read) .await @@ -781,8 +828,9 @@ pub async fn report_scopes() { // Delete report // We use a moderator PAT here, as only moderators can delete reports let report_delete = Scopes::REPORT_DELETE; - let req_gen = - |pat: Option| async move { api.delete_report(report_id, pat.as_deref()).await }; + let req_gen = |pat: Option| async move { + api.delete_report(report_id, pat.as_deref()).await + }; ScopeTest::new(&test_env) .with_user_id(MOD_USER_ID_PARSED) .test(req_gen, report_delete) @@ -915,101 +963,104 @@ pub async fn pat_scopes() { #[actix_rt::test] pub async fn collections_scopes() { // Test setup and dummy data - with_test_environment(None, |test_env: TestEnvironment| async move { - let api = &test_env.api; - let alpha_project_id = &test_env.dummy.project_alpha.project_id; - - // Create collection - let collection_create = Scopes::COLLECTION_CREATE; - let req_gen = |pat: Option| async move { - api.create_collection( - "Test Collection", - "Test Collection Description", - &[alpha_project_id.as_str()], - pat.as_deref(), - ) - .await - }; - let (_, success) = ScopeTest::new(&test_env) - .test(req_gen, collection_create) - .await - .unwrap(); - let collection_id = success["id"].as_str().unwrap(); - - // Patch collection - // Collections always initialize to public, so we do patch before Get testing - let collection_write = Scopes::COLLECTION_WRITE; - let req_gen = |pat: Option| async move { - api.edit_collection( - collection_id, - json!({ - "name": "Test Collection patch", - "status": "private", - }), - pat.as_deref(), - ) - .await - }; - ScopeTest::new(&test_env) - .test(req_gen, collection_write) - .await - .unwrap(); - - // Read collection - let collection_read = Scopes::COLLECTION_READ; - let req_gen = |pat: Option| async move { - api.get_collection(collection_id, pat.as_deref()).await - }; - ScopeTest::new(&test_env) - .with_failure_code(404) - .test(req_gen, collection_read) - .await - .unwrap(); - - let req_gen = |pat: Option| async move { - api.get_collections(&[collection_id], pat.as_deref()).await - }; - let (failure, success) = ScopeTest::new(&test_env) - .with_failure_code(200) - .test(req_gen, collection_read) - .await - .unwrap(); - assert_eq!(failure.as_array().unwrap().len(), 0); - assert_eq!(success.as_array().unwrap().len(), 1); - - let req_gen = |pat: Option| async move { - api.get_user_collections(USER_USER_ID, pat.as_deref()).await - }; - let (failure, success) = ScopeTest::new(&test_env) - .with_failure_code(200) - .test(req_gen, collection_read) - .await - .unwrap(); - assert_eq!(failure.as_array().unwrap().len(), 0); - assert_eq!(success.as_array().unwrap().len(), 1); - - let req_gen = |pat: Option| async move { - api.edit_collection_icon( - collection_id, - Some(DummyImage::SmallIcon.get_icon_data()), - pat.as_deref(), - ) - .await - }; - ScopeTest::new(&test_env) - .test(req_gen, collection_write) - .await - .unwrap(); - - let req_gen = |pat: Option| async move { - api.edit_collection_icon(collection_id, None, pat.as_deref()) + with_test_environment( + None, + |test_env: TestEnvironment| async move { + let api = &test_env.api; + let alpha_project_id = &test_env.dummy.project_alpha.project_id; + + // Create collection + let collection_create = Scopes::COLLECTION_CREATE; + let req_gen = |pat: Option| async move { + api.create_collection( + "Test Collection", + "Test Collection Description", + &[alpha_project_id.as_str()], + pat.as_deref(), + ) .await - }; - ScopeTest::new(&test_env) - .test(req_gen, collection_write) - .await - .unwrap(); - }) + }; + let (_, success) = ScopeTest::new(&test_env) + .test(req_gen, collection_create) + .await + .unwrap(); + let collection_id = success["id"].as_str().unwrap(); + + // Patch collection + // Collections always initialize to public, so we do patch before Get testing + let collection_write = Scopes::COLLECTION_WRITE; + let req_gen = |pat: Option| async move { + api.edit_collection( + collection_id, + json!({ + "name": "Test Collection patch", + "status": "private", + }), + pat.as_deref(), + ) + .await + }; + ScopeTest::new(&test_env) + .test(req_gen, collection_write) + .await + .unwrap(); + + // Read collection + let collection_read = Scopes::COLLECTION_READ; + let req_gen = |pat: Option| async move { + api.get_collection(collection_id, pat.as_deref()).await + }; + ScopeTest::new(&test_env) + .with_failure_code(404) + .test(req_gen, collection_read) + .await + .unwrap(); + + let req_gen = |pat: Option| async move { + api.get_collections(&[collection_id], pat.as_deref()).await + }; + let (failure, success) = ScopeTest::new(&test_env) + .with_failure_code(200) + .test(req_gen, collection_read) + .await + .unwrap(); + assert_eq!(failure.as_array().unwrap().len(), 0); + assert_eq!(success.as_array().unwrap().len(), 1); + + let req_gen = |pat: Option| async move { + api.get_user_collections(USER_USER_ID, pat.as_deref()).await + }; + let (failure, success) = ScopeTest::new(&test_env) + .with_failure_code(200) + .test(req_gen, collection_read) + .await + .unwrap(); + assert_eq!(failure.as_array().unwrap().len(), 0); + assert_eq!(success.as_array().unwrap().len(), 1); + + let req_gen = |pat: Option| async move { + api.edit_collection_icon( + collection_id, + Some(DummyImage::SmallIcon.get_icon_data()), + pat.as_deref(), + ) + .await + }; + ScopeTest::new(&test_env) + .test(req_gen, collection_write) + .await + .unwrap(); + + let req_gen = |pat: Option| async move { + api.edit_collection_icon(collection_id, None, pat.as_deref()) + .await + }; + ScopeTest::new(&test_env) + .test(req_gen, collection_write) + .await + .unwrap(); + }, + ) .await; } @@ -1017,140 +1068,158 @@ pub async fn collections_scopes() { #[actix_rt::test] pub async fn organization_scopes() { // Test setup and dummy data - with_test_environment(None, |test_env: TestEnvironment| async move { - let api = &test_env.api; - let beta_project_id = &test_env.dummy.project_beta.project_id; - - // Create organization - let organization_create = Scopes::ORGANIZATION_CREATE; - let req_gen = |pat: Option| async move { - api.create_organization("Test Org", "TestOrg", "TestOrg Description", pat.as_deref()) + with_test_environment( + None, + |test_env: TestEnvironment| async move { + let api = &test_env.api; + let beta_project_id = &test_env.dummy.project_beta.project_id; + + // Create organization + let organization_create = Scopes::ORGANIZATION_CREATE; + let req_gen = |pat: Option| async move { + api.create_organization( + "Test Org", + "TestOrg", + "TestOrg Description", + pat.as_deref(), + ) .await - }; - let (_, success) = ScopeTest::new(&test_env) - .test(req_gen, organization_create) - .await - .unwrap(); - let organization_id = success["id"].as_str().unwrap(); - - // Patch organization - let organization_edit = Scopes::ORGANIZATION_WRITE; - let req_gen = |pat: Option| async move { - api.edit_organization( - organization_id, - json!({ - "description": "TestOrg Patch Description", - }), - pat.as_deref(), - ) - .await - }; - ScopeTest::new(&test_env) - .test(req_gen, organization_edit) - .await - .unwrap(); - - let req_gen = |pat: Option| async move { - api.edit_organization_icon( - organization_id, - Some(DummyImage::SmallIcon.get_icon_data()), - pat.as_deref(), - ) - .await - }; - ScopeTest::new(&test_env) - .test(req_gen, organization_edit) - .await - .unwrap(); - - let req_gen = |pat: Option| async move { - api.edit_organization_icon(organization_id, None, pat.as_deref()) + }; + let (_, success) = ScopeTest::new(&test_env) + .test(req_gen, organization_create) .await - }; - ScopeTest::new(&test_env) - .test(req_gen, organization_edit) - .await - .unwrap(); - - // add project - let organization_project_edit = Scopes::PROJECT_WRITE | Scopes::ORGANIZATION_WRITE; - let req_gen = |pat: Option| async move { - api.organization_add_project(organization_id, beta_project_id, pat.as_deref()) + .unwrap(); + let organization_id = success["id"].as_str().unwrap(); + + // Patch organization + let organization_edit = Scopes::ORGANIZATION_WRITE; + let req_gen = |pat: Option| async move { + api.edit_organization( + organization_id, + json!({ + "description": "TestOrg Patch Description", + }), + pat.as_deref(), + ) .await - }; - ScopeTest::new(&test_env) - .with_failure_scopes(Scopes::all() ^ Scopes::ORGANIZATION_WRITE) - .test(req_gen, organization_project_edit) - .await - .unwrap(); - - // Organization reads - let organization_read = Scopes::ORGANIZATION_READ; - let req_gen = |pat: Option| async move { - api.get_organization(organization_id, pat.as_deref()).await - }; - let (failure, success) = ScopeTest::new(&test_env) - .with_failure_code(200) - .test(req_gen, organization_read) - .await - .unwrap(); - assert!(failure["members"][0]["permissions"].is_null()); - assert!(!success["members"][0]["permissions"].is_null()); - - let req_gen = |pat: Option| async move { - api.get_organizations(&[organization_id], pat.as_deref()) + }; + ScopeTest::new(&test_env) + .test(req_gen, organization_edit) .await - }; - - let (failure, success) = ScopeTest::new(&test_env) - .with_failure_code(200) - .test(req_gen, organization_read) - .await - .unwrap(); - assert!(failure[0]["members"][0]["permissions"].is_null()); - assert!(!success[0]["members"][0]["permissions"].is_null()); - - let organization_project_read = Scopes::PROJECT_READ | Scopes::ORGANIZATION_READ; - let req_gen = |pat: Option| async move { - api.get_organization_projects(organization_id, pat.as_deref()) + .unwrap(); + + let req_gen = |pat: Option| async move { + api.edit_organization_icon( + organization_id, + Some(DummyImage::SmallIcon.get_icon_data()), + pat.as_deref(), + ) .await - }; - let (failure, success) = ScopeTest::new(&test_env) - .with_failure_code(200) - .with_failure_scopes(Scopes::all() ^ Scopes::ORGANIZATION_READ) - .test(req_gen, organization_project_read) - .await - .unwrap(); - assert!(failure.as_array().unwrap().is_empty()); - assert!(!success.as_array().unwrap().is_empty()); - - // remove project (now that we've checked) - let req_gen = |pat: Option| async move { - api.organization_remove_project( - organization_id, - beta_project_id, - UserId(USER_USER_ID_PARSED as u64), - pat.as_deref(), - ) - .await - }; - ScopeTest::new(&test_env) - .with_failure_scopes(Scopes::all() ^ Scopes::ORGANIZATION_WRITE) - .test(req_gen, organization_project_edit) - .await - .unwrap(); - - // Delete organization - let organization_delete = Scopes::ORGANIZATION_DELETE; - let req_gen = |pat: Option| async move { - api.delete_organization(organization_id, pat.as_deref()) + }; + ScopeTest::new(&test_env) + .test(req_gen, organization_edit) .await - }; - ScopeTest::new(&test_env) - .test(req_gen, organization_delete) - .await - .unwrap(); - }) + .unwrap(); + + let req_gen = |pat: Option| async move { + api.edit_organization_icon( + organization_id, + None, + pat.as_deref(), + ) + .await + }; + ScopeTest::new(&test_env) + .test(req_gen, organization_edit) + .await + .unwrap(); + + // add project + let organization_project_edit = + Scopes::PROJECT_WRITE | Scopes::ORGANIZATION_WRITE; + let req_gen = |pat: Option| async move { + api.organization_add_project( + organization_id, + beta_project_id, + pat.as_deref(), + ) + .await + }; + ScopeTest::new(&test_env) + .with_failure_scopes(Scopes::all() ^ Scopes::ORGANIZATION_WRITE) + .test(req_gen, organization_project_edit) + .await + .unwrap(); + + // Organization reads + let organization_read = Scopes::ORGANIZATION_READ; + let req_gen = |pat: Option| async move { + api.get_organization(organization_id, pat.as_deref()).await + }; + let (failure, success) = ScopeTest::new(&test_env) + .with_failure_code(200) + .test(req_gen, organization_read) + .await + .unwrap(); + assert!(failure["members"][0]["permissions"].is_null()); + assert!(!success["members"][0]["permissions"].is_null()); + + let req_gen = |pat: Option| async move { + api.get_organizations(&[organization_id], pat.as_deref()) + .await + }; + + let (failure, success) = ScopeTest::new(&test_env) + .with_failure_code(200) + .test(req_gen, organization_read) + .await + .unwrap(); + assert!(failure[0]["members"][0]["permissions"].is_null()); + assert!(!success[0]["members"][0]["permissions"].is_null()); + + let organization_project_read = + Scopes::PROJECT_READ | Scopes::ORGANIZATION_READ; + let req_gen = |pat: Option| async move { + api.get_organization_projects(organization_id, pat.as_deref()) + .await + }; + let (failure, success) = ScopeTest::new(&test_env) + .with_failure_code(200) + .with_failure_scopes(Scopes::all() ^ Scopes::ORGANIZATION_READ) + .test(req_gen, organization_project_read) + .await + .unwrap(); + assert!(failure.as_array().unwrap().is_empty()); + assert!(!success.as_array().unwrap().is_empty()); + + // remove project (now that we've checked) + let req_gen = |pat: Option| async move { + api.organization_remove_project( + organization_id, + beta_project_id, + UserId(USER_USER_ID_PARSED as u64), + pat.as_deref(), + ) + .await + }; + ScopeTest::new(&test_env) + .with_failure_scopes(Scopes::all() ^ Scopes::ORGANIZATION_WRITE) + .test(req_gen, organization_project_edit) + .await + .unwrap(); + + // Delete organization + let organization_delete = Scopes::ORGANIZATION_DELETE; + let req_gen = |pat: Option| async move { + api.delete_organization(organization_id, pat.as_deref()) + .await + }; + ScopeTest::new(&test_env) + .test(req_gen, organization_delete) + .await + .unwrap(); + }, + ) .await; } diff --git a/apps/labrinth/tests/search.rs b/apps/labrinth/tests/search.rs index 67db3adab..d0c5fb14c 100644 --- a/apps/labrinth/tests/search.rs +++ b/apps/labrinth/tests/search.rs @@ -22,148 +22,174 @@ mod common; #[actix_rt::test] async fn search_projects() { // Test setup and dummy data - with_test_environment(Some(10), |test_env: TestEnvironment| async move { - let id_conversion = setup_search_projects(&test_env).await; - - let api = &test_env.api; - let test_name = test_env.db.database_name.clone(); - - // Pairs of: - // 1. vec of search facets - // 2. expected project ids to be returned by this search - let pairs = vec![ - ( - json!([["categories:fabric"]]), - vec![0, 1, 2, 3, 4, 5, 6, 7, 9], - ), - (json!([["categories:forge"]]), vec![7]), - ( - json!([["categories:fabric", "categories:forge"]]), - vec![0, 1, 2, 3, 4, 5, 6, 7, 9], - ), - (json!([["categories:fabric"], ["categories:forge"]]), vec![]), - ( - json!([ - ["categories:fabric"], - [&format!("categories:{}", DUMMY_CATEGORIES[0])], - ]), - vec![1, 2, 3, 4], - ), - (json!([["project_types:modpack"]]), vec![4]), - (json!([["client_only:true"]]), vec![0, 2, 3, 7, 9]), - (json!([["server_only:true"]]), vec![0, 2, 3, 6, 7]), - (json!([["open_source:true"]]), vec![0, 1, 2, 4, 5, 6, 7, 9]), - (json!([["license:MIT"]]), vec![1, 2, 4, 9]), - (json!([[r#"name:'Mysterious Project'"#]]), vec![2, 3]), - (json!([["author:user"]]), vec![0, 1, 2, 4, 5, 7, 9]), // Organization test '9' is included here as user is owner of org - (json!([["game_versions:1.20.5"]]), vec![4, 5]), - // bug fix - ( - json!([ - // Only the forge one has 1.20.2, so its true that this project 'has' - // 1.20.2 and a fabric version, but not true that it has a 1.20.2 fabric version. - ["categories:fabric"], - ["game_versions:1.20.2"] - ]), - vec![], - ), - // Project type change - // Modpack should still be able to search based on former loader, even though technically the loader is 'mrpack' - // (json!([["categories:mrpack"]]), vec![4]), - // ( - // json!([["categories:fabric"]]), - // vec![4], - // ), - ( - json!([["categories:fabric"], ["project_types:modpack"]]), - vec![4], - ), - ]; - // TODO: versions, game versions - // Untested: - // - downloads (not varied) - // - color (not varied) - // - created_timestamp (not varied) - // - modified_timestamp (not varied) - // TODO: multiple different project types test - - // Test searches - let stream = futures::stream::iter(pairs); - stream - .for_each_concurrent(1, |(facets, mut expected_project_ids)| { - let id_conversion = id_conversion.clone(); - let test_name = test_name.clone(); - async move { - let projects = api - .search_deserialized( - Some(&format!("\"&{test_name}\"")), - Some(facets.clone()), - USER_USER_PAT, - ) - .await; - let mut found_project_ids: Vec = projects - .hits - .into_iter() - .map(|p| id_conversion[&parse_base62(&p.project_id).unwrap()]) - .collect(); - let num_hits = projects.total_hits; - expected_project_ids.sort(); - found_project_ids.sort(); - println!("Facets: {:?}", facets); - assert_eq!(found_project_ids, expected_project_ids); - assert_eq!(num_hits, { expected_project_ids.len() }); - } - }) - .await; - }) + with_test_environment( + Some(10), + |test_env: TestEnvironment| async move { + let id_conversion = setup_search_projects(&test_env).await; + + let api = &test_env.api; + let test_name = test_env.db.database_name.clone(); + + // Pairs of: + // 1. vec of search facets + // 2. expected project ids to be returned by this search + let pairs = vec![ + ( + json!([["categories:fabric"]]), + vec![0, 1, 2, 3, 4, 5, 6, 7, 9], + ), + (json!([["categories:forge"]]), vec![7]), + ( + json!([["categories:fabric", "categories:forge"]]), + vec![0, 1, 2, 3, 4, 5, 6, 7, 9], + ), + (json!([["categories:fabric"], ["categories:forge"]]), vec![]), + ( + json!([ + ["categories:fabric"], + [&format!("categories:{}", DUMMY_CATEGORIES[0])], + ]), + vec![1, 2, 3, 4], + ), + (json!([["project_types:modpack"]]), vec![4]), + (json!([["client_only:true"]]), vec![0, 2, 3, 7, 9]), + (json!([["server_only:true"]]), vec![0, 2, 3, 6, 7]), + (json!([["open_source:true"]]), vec![0, 1, 2, 4, 5, 6, 7, 9]), + (json!([["license:MIT"]]), vec![1, 2, 4, 9]), + (json!([[r#"name:'Mysterious Project'"#]]), vec![2, 3]), + (json!([["author:user"]]), vec![0, 1, 2, 4, 5, 7, 9]), // Organization test '9' is included here as user is owner of org + (json!([["game_versions:1.20.5"]]), vec![4, 5]), + // bug fix + ( + json!([ + // Only the forge one has 1.20.2, so its true that this project 'has' + // 1.20.2 and a fabric version, but not true that it has a 1.20.2 fabric version. + ["categories:fabric"], + ["game_versions:1.20.2"] + ]), + vec![], + ), + // Project type change + // Modpack should still be able to search based on former loader, even though technically the loader is 'mrpack' + // (json!([["categories:mrpack"]]), vec![4]), + // ( + // json!([["categories:fabric"]]), + // vec![4], + // ), + ( + json!([["categories:fabric"], ["project_types:modpack"]]), + vec![4], + ), + ]; + // TODO: versions, game versions + // Untested: + // - downloads (not varied) + // - color (not varied) + // - created_timestamp (not varied) + // - modified_timestamp (not varied) + // TODO: multiple different project types test + + // Test searches + let stream = futures::stream::iter(pairs); + stream + .for_each_concurrent(1, |(facets, mut expected_project_ids)| { + let id_conversion = id_conversion.clone(); + let test_name = test_name.clone(); + async move { + let projects = api + .search_deserialized( + Some(&format!("\"&{test_name}\"")), + Some(facets.clone()), + USER_USER_PAT, + ) + .await; + let mut found_project_ids: Vec = projects + .hits + .into_iter() + .map(|p| { + id_conversion + [&parse_base62(&p.project_id).unwrap()] + }) + .collect(); + let num_hits = projects.total_hits; + expected_project_ids.sort(); + found_project_ids.sort(); + println!("Facets: {:?}", facets); + assert_eq!(found_project_ids, expected_project_ids); + assert_eq!(num_hits, { expected_project_ids.len() }); + } + }) + .await; + }, + ) .await; } #[actix_rt::test] async fn index_swaps() { - with_test_environment(Some(10), |test_env: TestEnvironment| async move { - // Reindex - let resp = test_env.api.reset_search_index().await; - assert_status!(&resp, StatusCode::NO_CONTENT); - - // Now we should get results - let projects = test_env - .api - .search_deserialized(None, Some(json!([["categories:fabric"]])), USER_USER_PAT) - .await; - assert_eq!(projects.total_hits, 1); - assert!(projects.hits[0].slug.as_ref().unwrap().contains("alpha")); - - // Delete the project - let resp = test_env.api.remove_project("alpha", USER_USER_PAT).await; - assert_status!(&resp, StatusCode::NO_CONTENT); - - // We should not get any results, because the project has been deleted - let projects = test_env - .api - .search_deserialized(None, Some(json!([["categories:fabric"]])), USER_USER_PAT) - .await; - assert_eq!(projects.total_hits, 0); - - // But when we reindex, it should be gone - let resp = test_env.api.reset_search_index().await; - assert_status!(&resp, StatusCode::NO_CONTENT); - - let projects = test_env - .api - .search_deserialized(None, Some(json!([["categories:fabric"]])), USER_USER_PAT) - .await; - assert_eq!(projects.total_hits, 0); - - // Reindex again, should still be gone - let resp = test_env.api.reset_search_index().await; - assert_status!(&resp, StatusCode::NO_CONTENT); - - let projects = test_env - .api - .search_deserialized(None, Some(json!([["categories:fabric"]])), USER_USER_PAT) - .await; - assert_eq!(projects.total_hits, 0); - }) + with_test_environment( + Some(10), + |test_env: TestEnvironment| async move { + // Reindex + let resp = test_env.api.reset_search_index().await; + assert_status!(&resp, StatusCode::NO_CONTENT); + + // Now we should get results + let projects = test_env + .api + .search_deserialized( + None, + Some(json!([["categories:fabric"]])), + USER_USER_PAT, + ) + .await; + assert_eq!(projects.total_hits, 1); + assert!(projects.hits[0].slug.as_ref().unwrap().contains("alpha")); + + // Delete the project + let resp = + test_env.api.remove_project("alpha", USER_USER_PAT).await; + assert_status!(&resp, StatusCode::NO_CONTENT); + + // We should not get any results, because the project has been deleted + let projects = test_env + .api + .search_deserialized( + None, + Some(json!([["categories:fabric"]])), + USER_USER_PAT, + ) + .await; + assert_eq!(projects.total_hits, 0); + + // But when we reindex, it should be gone + let resp = test_env.api.reset_search_index().await; + assert_status!(&resp, StatusCode::NO_CONTENT); + + let projects = test_env + .api + .search_deserialized( + None, + Some(json!([["categories:fabric"]])), + USER_USER_PAT, + ) + .await; + assert_eq!(projects.total_hits, 0); + + // Reindex again, should still be gone + let resp = test_env.api.reset_search_index().await; + assert_status!(&resp, StatusCode::NO_CONTENT); + + let projects = test_env + .api + .search_deserialized( + None, + Some(json!([["categories:fabric"]])), + USER_USER_PAT, + ) + .await; + assert_eq!(projects.total_hits, 0); + }, + ) .await; } diff --git a/apps/labrinth/tests/tags.rs b/apps/labrinth/tests/tags.rs index 21d56b9c7..264c7e29e 100644 --- a/apps/labrinth/tests/tags.rs +++ b/apps/labrinth/tests/tags.rs @@ -2,7 +2,9 @@ use std::collections::{HashMap, HashSet}; use common::{ api_v3::ApiV3, - environment::{with_test_environment, with_test_environment_all, TestEnvironment}, + environment::{ + with_test_environment, with_test_environment_all, TestEnvironment, + }, }; use crate::common::api_common::ApiTags; @@ -40,25 +42,34 @@ async fn get_tags() { #[actix_rt::test] async fn get_tags_v3() { - with_test_environment(None, |test_env: TestEnvironment| async move { - let api = &test_env.api; - let loaders = api.get_loaders_deserialized().await; + with_test_environment( + None, + |test_env: TestEnvironment| async move { + let api = &test_env.api; + let loaders = api.get_loaders_deserialized().await; - let loader_metadata = loaders - .into_iter() - .map(|x| (x.name, x.metadata.get("platform").and_then(|x| x.as_bool()))) - .collect::>(); - let loader_names = loader_metadata.keys().cloned().collect::>(); - assert_eq!( - loader_names, - ["fabric", "forge", "mrpack", "bukkit", "waterfall"] - .iter() - .map(|s| s.to_string()) - .collect() - ); - assert_eq!(loader_metadata["fabric"], None); - assert_eq!(loader_metadata["bukkit"], Some(false)); - assert_eq!(loader_metadata["waterfall"], Some(true)); - }) + let loader_metadata = loaders + .into_iter() + .map(|x| { + ( + x.name, + x.metadata.get("platform").and_then(|x| x.as_bool()), + ) + }) + .collect::>(); + let loader_names = + loader_metadata.keys().cloned().collect::>(); + assert_eq!( + loader_names, + ["fabric", "forge", "mrpack", "bukkit", "waterfall"] + .iter() + .map(|s| s.to_string()) + .collect() + ); + assert_eq!(loader_metadata["fabric"], None); + assert_eq!(loader_metadata["bukkit"], Some(false)); + assert_eq!(loader_metadata["waterfall"], Some(true)); + }, + ) .await; } diff --git a/apps/labrinth/tests/teams.rs b/apps/labrinth/tests/teams.rs index b1ff73fdc..4743ecf4c 100644 --- a/apps/labrinth/tests/teams.rs +++ b/apps/labrinth/tests/teams.rs @@ -2,7 +2,9 @@ use crate::common::{api_common::ApiTeams, database::*}; use actix_http::StatusCode; use common::{ api_v3::ApiV3, - environment::{with_test_environment, with_test_environment_all, TestEnvironment}, + environment::{ + with_test_environment, with_test_environment_all, TestEnvironment, + }, }; use labrinth::models::teams::{OrganizationPermissions, ProjectPermissions}; use rust_decimal::Decimal; @@ -22,14 +24,20 @@ async fn test_get_team() { // A non-member of the team should get basic info but not be able to see private data let members = api - .get_team_members_deserialized_common(alpha_team_id, FRIEND_USER_PAT) + .get_team_members_deserialized_common( + alpha_team_id, + FRIEND_USER_PAT, + ) .await; assert_eq!(members.len(), 1); assert_eq!(members[0].user.id.0, USER_USER_ID_PARSED as u64); assert!(members[0].permissions.is_none()); let members = api - .get_project_members_deserialized_common(alpha_project_id, FRIEND_USER_PAT) + .get_project_members_deserialized_common( + alpha_project_id, + FRIEND_USER_PAT, + ) .await; assert_eq!(members.len(), 1); assert_eq!(members[0].user.id.0, USER_USER_ID_PARSED as u64); @@ -38,13 +46,22 @@ async fn test_get_team() { // - not be able to see private data about the team, but see all members including themselves // - should not appear in the team members list to enemy users let resp = api - .add_user_to_team(alpha_team_id, FRIEND_USER_ID, None, None, USER_USER_PAT) + .add_user_to_team( + alpha_team_id, + FRIEND_USER_ID, + None, + None, + USER_USER_PAT, + ) .await; assert_status!(&resp, StatusCode::NO_CONTENT); // Team check directly let members = api - .get_team_members_deserialized_common(alpha_team_id, FRIEND_USER_PAT) + .get_team_members_deserialized_common( + alpha_team_id, + FRIEND_USER_PAT, + ) .await; assert!(members.len() == 2); // USER_USER_ID and FRIEND_USER_ID should be in the team let user_user = members @@ -62,7 +79,10 @@ async fn test_get_team() { // team check via association let members = api - .get_project_members_deserialized_common(alpha_project_id, FRIEND_USER_PAT) + .get_project_members_deserialized_common( + alpha_project_id, + FRIEND_USER_PAT, + ) .await; assert!(members.len() == 2); // USER_USER_ID and FRIEND_USER_ID should be in the team let user_user = members @@ -86,7 +106,10 @@ async fn test_get_team() { // enemy team check via association let members = api - .get_project_members_deserialized_common(alpha_project_id, ENEMY_USER_PAT) + .get_project_members_deserialized_common( + alpha_project_id, + ENEMY_USER_PAT, + ) .await; assert_eq!(members.len(), 1); // Only USER_USER_ID should be in the team @@ -97,7 +120,10 @@ async fn test_get_team() { // Team check directly let members = api - .get_team_members_deserialized_common(alpha_team_id, FRIEND_USER_PAT) + .get_team_members_deserialized_common( + alpha_team_id, + FRIEND_USER_PAT, + ) .await; assert!(members.len() == 2); // USER_USER_ID and FRIEND_USER_ID should be in the team let user_user = members @@ -115,7 +141,10 @@ async fn test_get_team() { // team check via association let members = api - .get_project_members_deserialized_common(alpha_project_id, FRIEND_USER_PAT) + .get_project_members_deserialized_common( + alpha_project_id, + FRIEND_USER_PAT, + ) .await; assert!(members.len() == 2); // USER_USER_ID and FRIEND_USER_ID should be in the team let user_user = members @@ -139,169 +168,224 @@ async fn test_get_team_organization() { // Test setup and dummy data // Perform get_team related tests for an organization team //TODO: This needs to consider users in organizations now and how they perceive as well - with_test_environment(None, |test_env: TestEnvironment| async move { - let api = &test_env.api; - let zeta_organization_id = &test_env.dummy.organization_zeta.organization_id; - let zeta_team_id = &test_env.dummy.organization_zeta.team_id; - - // A non-member of the team should get basic info but not be able to see private data - let members = api - .get_team_members_deserialized_common(zeta_team_id, FRIEND_USER_PAT) - .await; - assert_eq!(members.len(), 1); - assert_eq!(members[0].user.id.0, USER_USER_ID_PARSED as u64); - assert!(members[0].permissions.is_none()); - - let members = api - .get_organization_members_deserialized_common(zeta_organization_id, FRIEND_USER_PAT) - .await; - assert_eq!(members.len(), 1); - assert_eq!(members[0].user.id.0, USER_USER_ID_PARSED as u64); - - // A non-accepted member of the team should: - // - not be able to see private data about the team, but see all members including themselves - // - should not appear in the team members list to enemy users - let resp = api - .add_user_to_team(zeta_team_id, FRIEND_USER_ID, None, None, USER_USER_PAT) - .await; - assert_status!(&resp, StatusCode::NO_CONTENT); - - // Team check directly - let members = api - .get_team_members_deserialized_common(zeta_team_id, FRIEND_USER_PAT) - .await; - assert!(members.len() == 2); // USER_USER_ID and FRIEND_USER_ID should be in the team - let user_user = members - .iter() - .find(|x| x.user.id.0 == USER_USER_ID_PARSED as u64) - .unwrap(); - let friend_user = members - .iter() - .find(|x| x.user.id.0 == FRIEND_USER_ID_PARSED as u64) - .unwrap(); - assert_eq!(user_user.user.id.0, USER_USER_ID_PARSED as u64); - assert!(user_user.permissions.is_none()); // Should not see private data of the team - assert_eq!(friend_user.user.id.0, FRIEND_USER_ID_PARSED as u64); - assert!(friend_user.permissions.is_none()); - - // team check via association - let members = api - .get_organization_members_deserialized_common(zeta_organization_id, FRIEND_USER_PAT) - .await; - - assert!(members.len() == 2); // USER_USER_ID and FRIEND_USER_ID should be in the team - let user_user = members - .iter() - .find(|x| x.user.id.0 == USER_USER_ID_PARSED as u64) - .unwrap(); - let friend_user = members - .iter() - .find(|x| x.user.id.0 == FRIEND_USER_ID_PARSED as u64) - .unwrap(); - assert_eq!(user_user.user.id.0, USER_USER_ID_PARSED as u64); - assert!(user_user.permissions.is_none()); // Should not see private data of the team - assert_eq!(friend_user.user.id.0, FRIEND_USER_ID_PARSED as u64); - assert!(friend_user.permissions.is_none()); - - // enemy team check directly - let members = api - .get_team_members_deserialized_common(zeta_team_id, ENEMY_USER_PAT) - .await; - assert_eq!(members.len(), 1); // Only USER_USER_ID should be in the team - - // enemy team check via association - let members = api - .get_organization_members_deserialized_common(zeta_organization_id, ENEMY_USER_PAT) - .await; - assert_eq!(members.len(), 1); // Only USER_USER_ID should be in the team - - // An accepted member of the team should appear in the team members list - // and should be able to see private data about the team - let resp = api.join_team(zeta_team_id, FRIEND_USER_PAT).await; - assert_status!(&resp, StatusCode::NO_CONTENT); - - // Team check directly - let members = api - .get_team_members_deserialized_common(zeta_team_id, FRIEND_USER_PAT) - .await; - assert!(members.len() == 2); // USER_USER_ID and FRIEND_USER_ID should be in the team - let user_user = members - .iter() - .find(|x| x.user.id.0 == USER_USER_ID_PARSED as u64) - .unwrap(); - let friend_user = members - .iter() - .find(|x| x.user.id.0 == FRIEND_USER_ID_PARSED as u64) - .unwrap(); - assert_eq!(user_user.user.id.0, USER_USER_ID_PARSED as u64); - assert!(user_user.permissions.is_some()); // SHOULD see private data of the team - assert_eq!(friend_user.user.id.0, FRIEND_USER_ID_PARSED as u64); - assert!(friend_user.permissions.is_some()); + with_test_environment( + None, + |test_env: TestEnvironment| async move { + let api = &test_env.api; + let zeta_organization_id = + &test_env.dummy.organization_zeta.organization_id; + let zeta_team_id = &test_env.dummy.organization_zeta.team_id; + + // A non-member of the team should get basic info but not be able to see private data + let members = api + .get_team_members_deserialized_common( + zeta_team_id, + FRIEND_USER_PAT, + ) + .await; + assert_eq!(members.len(), 1); + assert_eq!(members[0].user.id.0, USER_USER_ID_PARSED as u64); + assert!(members[0].permissions.is_none()); + + let members = api + .get_organization_members_deserialized_common( + zeta_organization_id, + FRIEND_USER_PAT, + ) + .await; + assert_eq!(members.len(), 1); + assert_eq!(members[0].user.id.0, USER_USER_ID_PARSED as u64); + + // A non-accepted member of the team should: + // - not be able to see private data about the team, but see all members including themselves + // - should not appear in the team members list to enemy users + let resp = api + .add_user_to_team( + zeta_team_id, + FRIEND_USER_ID, + None, + None, + USER_USER_PAT, + ) + .await; + assert_status!(&resp, StatusCode::NO_CONTENT); + + // Team check directly + let members = api + .get_team_members_deserialized_common( + zeta_team_id, + FRIEND_USER_PAT, + ) + .await; + assert!(members.len() == 2); // USER_USER_ID and FRIEND_USER_ID should be in the team + let user_user = members + .iter() + .find(|x| x.user.id.0 == USER_USER_ID_PARSED as u64) + .unwrap(); + let friend_user = members + .iter() + .find(|x| x.user.id.0 == FRIEND_USER_ID_PARSED as u64) + .unwrap(); + assert_eq!(user_user.user.id.0, USER_USER_ID_PARSED as u64); + assert!(user_user.permissions.is_none()); // Should not see private data of the team + assert_eq!(friend_user.user.id.0, FRIEND_USER_ID_PARSED as u64); + assert!(friend_user.permissions.is_none()); + + // team check via association + let members = api + .get_organization_members_deserialized_common( + zeta_organization_id, + FRIEND_USER_PAT, + ) + .await; - // team check via association - let members = api - .get_organization_members_deserialized_common(zeta_organization_id, FRIEND_USER_PAT) - .await; - assert!(members.len() == 2); // USER_USER_ID and FRIEND_USER_ID should be in the team - let user_user = members - .iter() - .find(|x| x.user.id.0 == USER_USER_ID_PARSED as u64) - .unwrap(); - let friend_user = members - .iter() - .find(|x| x.user.id.0 == FRIEND_USER_ID_PARSED as u64) - .unwrap(); - assert_eq!(user_user.user.id.0, USER_USER_ID_PARSED as u64); - assert!(user_user.permissions.is_some()); // SHOULD see private data of the team - assert_eq!(friend_user.user.id.0, FRIEND_USER_ID_PARSED as u64); - assert!(friend_user.permissions.is_some()); - }) + assert!(members.len() == 2); // USER_USER_ID and FRIEND_USER_ID should be in the team + let user_user = members + .iter() + .find(|x| x.user.id.0 == USER_USER_ID_PARSED as u64) + .unwrap(); + let friend_user = members + .iter() + .find(|x| x.user.id.0 == FRIEND_USER_ID_PARSED as u64) + .unwrap(); + assert_eq!(user_user.user.id.0, USER_USER_ID_PARSED as u64); + assert!(user_user.permissions.is_none()); // Should not see private data of the team + assert_eq!(friend_user.user.id.0, FRIEND_USER_ID_PARSED as u64); + assert!(friend_user.permissions.is_none()); + + // enemy team check directly + let members = api + .get_team_members_deserialized_common( + zeta_team_id, + ENEMY_USER_PAT, + ) + .await; + assert_eq!(members.len(), 1); // Only USER_USER_ID should be in the team + + // enemy team check via association + let members = api + .get_organization_members_deserialized_common( + zeta_organization_id, + ENEMY_USER_PAT, + ) + .await; + assert_eq!(members.len(), 1); // Only USER_USER_ID should be in the team + + // An accepted member of the team should appear in the team members list + // and should be able to see private data about the team + let resp = api.join_team(zeta_team_id, FRIEND_USER_PAT).await; + assert_status!(&resp, StatusCode::NO_CONTENT); + + // Team check directly + let members = api + .get_team_members_deserialized_common( + zeta_team_id, + FRIEND_USER_PAT, + ) + .await; + assert!(members.len() == 2); // USER_USER_ID and FRIEND_USER_ID should be in the team + let user_user = members + .iter() + .find(|x| x.user.id.0 == USER_USER_ID_PARSED as u64) + .unwrap(); + let friend_user = members + .iter() + .find(|x| x.user.id.0 == FRIEND_USER_ID_PARSED as u64) + .unwrap(); + assert_eq!(user_user.user.id.0, USER_USER_ID_PARSED as u64); + assert!(user_user.permissions.is_some()); // SHOULD see private data of the team + assert_eq!(friend_user.user.id.0, FRIEND_USER_ID_PARSED as u64); + assert!(friend_user.permissions.is_some()); + + // team check via association + let members = api + .get_organization_members_deserialized_common( + zeta_organization_id, + FRIEND_USER_PAT, + ) + .await; + assert!(members.len() == 2); // USER_USER_ID and FRIEND_USER_ID should be in the team + let user_user = members + .iter() + .find(|x| x.user.id.0 == USER_USER_ID_PARSED as u64) + .unwrap(); + let friend_user = members + .iter() + .find(|x| x.user.id.0 == FRIEND_USER_ID_PARSED as u64) + .unwrap(); + assert_eq!(user_user.user.id.0, USER_USER_ID_PARSED as u64); + assert!(user_user.permissions.is_some()); // SHOULD see private data of the team + assert_eq!(friend_user.user.id.0, FRIEND_USER_ID_PARSED as u64); + assert!(friend_user.permissions.is_some()); + }, + ) .await; } #[actix_rt::test] async fn test_get_team_project_orgs() { // Test setup and dummy data - with_test_environment(None, |test_env: TestEnvironment| async move { - let alpha_project_id = &test_env.dummy.project_alpha.project_id; - let alpha_team_id = &test_env.dummy.project_alpha.team_id; - let zeta_organization_id = &test_env.dummy.organization_zeta.organization_id; - let zeta_team_id = &test_env.dummy.organization_zeta.team_id; - - // Attach alpha to zeta - let resp = test_env - .api - .organization_add_project(zeta_organization_id, alpha_project_id, USER_USER_PAT) - .await; - assert_status!(&resp, StatusCode::OK); + with_test_environment( + None, + |test_env: TestEnvironment| async move { + let alpha_project_id = &test_env.dummy.project_alpha.project_id; + let alpha_team_id = &test_env.dummy.project_alpha.team_id; + let zeta_organization_id = + &test_env.dummy.organization_zeta.organization_id; + let zeta_team_id = &test_env.dummy.organization_zeta.team_id; + + // Attach alpha to zeta + let resp = test_env + .api + .organization_add_project( + zeta_organization_id, + alpha_project_id, + USER_USER_PAT, + ) + .await; + assert_status!(&resp, StatusCode::OK); - // Invite and add friend to zeta - let resp = test_env - .api - .add_user_to_team(zeta_team_id, FRIEND_USER_ID, None, None, USER_USER_PAT) - .await; - assert_status!(&resp, StatusCode::NO_CONTENT); + // Invite and add friend to zeta + let resp = test_env + .api + .add_user_to_team( + zeta_team_id, + FRIEND_USER_ID, + None, + None, + USER_USER_PAT, + ) + .await; + assert_status!(&resp, StatusCode::NO_CONTENT); - let resp = test_env.api.join_team(zeta_team_id, FRIEND_USER_PAT).await; - assert_status!(&resp, StatusCode::NO_CONTENT); + let resp = + test_env.api.join_team(zeta_team_id, FRIEND_USER_PAT).await; + assert_status!(&resp, StatusCode::NO_CONTENT); - // The team members route from teams (on a project's team): - // - the members of the project team specifically - // - not the ones from the organization - // - Remember: the owner of an org will not be included in the org's team members list - let members = test_env - .api - .get_team_members_deserialized_common(alpha_team_id, FRIEND_USER_PAT) - .await; - assert_eq!(members.len(), 0); + // The team members route from teams (on a project's team): + // - the members of the project team specifically + // - not the ones from the organization + // - Remember: the owner of an org will not be included in the org's team members list + let members = test_env + .api + .get_team_members_deserialized_common( + alpha_team_id, + FRIEND_USER_PAT, + ) + .await; + assert_eq!(members.len(), 0); - // The team members route from project should show the same! - let members = test_env - .api - .get_project_members_deserialized_common(alpha_project_id, FRIEND_USER_PAT) - .await; - assert_eq!(members.len(), 0); - }) + // The team members route from project should show the same! + let members = test_env + .api + .get_project_members_deserialized_common( + alpha_project_id, + FRIEND_USER_PAT, + ) + .await; + assert_eq!(members.len(), 0); + }, + ) .await; } @@ -473,101 +557,133 @@ async fn test_patch_organization_team_member() { #[actix_rt::test] async fn transfer_ownership_v3() { // Test setup and dummy data - with_test_environment(None, |test_env: TestEnvironment| async move { - let api = &test_env.api; - - let alpha_team_id = &test_env.dummy.project_alpha.team_id; - - // Cannot set friend as owner (not a member) - let resp = api - .transfer_team_ownership(alpha_team_id, FRIEND_USER_ID, USER_USER_PAT) - .await; - assert_status!(&resp, StatusCode::BAD_REQUEST); - let resp = api - .transfer_team_ownership(alpha_team_id, FRIEND_USER_ID, FRIEND_USER_PAT) - .await; - assert_status!(&resp, StatusCode::UNAUTHORIZED); - - // first, invite friend - let resp = api - .add_user_to_team(alpha_team_id, FRIEND_USER_ID, None, None, USER_USER_PAT) - .await; - assert_status!(&resp, StatusCode::NO_CONTENT); - - // still cannot set friend as owner (not accepted) - let resp = api - .transfer_team_ownership(alpha_team_id, FRIEND_USER_ID, USER_USER_PAT) - .await; - assert_status!(&resp, StatusCode::BAD_REQUEST); - - // accept - let resp = api.join_team(alpha_team_id, FRIEND_USER_PAT).await; - assert_status!(&resp, StatusCode::NO_CONTENT); - - // Cannot set ourselves as owner if we are not owner - let resp = api - .transfer_team_ownership(alpha_team_id, FRIEND_USER_ID, FRIEND_USER_PAT) - .await; - assert_status!(&resp, StatusCode::UNAUTHORIZED); - - // Can set friend as owner - let resp = api - .transfer_team_ownership(alpha_team_id, FRIEND_USER_ID, USER_USER_PAT) - .await; - assert_status!(&resp, StatusCode::NO_CONTENT); - - // Check - let members = api - .get_team_members_deserialized(alpha_team_id, USER_USER_PAT) - .await; - let friend_member = members - .iter() - .find(|x| x.user.id.0 == FRIEND_USER_ID_PARSED as u64) - .unwrap(); - assert_eq!(friend_member.role, "Member"); // her role does not actually change, but is_owner is set to true - assert!(friend_member.is_owner); - assert_eq!( - friend_member.permissions.unwrap(), - ProjectPermissions::all() - ); - - let user_member = members - .iter() - .find(|x| x.user.id.0 == USER_USER_ID_PARSED as u64) - .unwrap(); - assert_eq!(user_member.role, "Member"); // We are the 'owner', but we are not actually the owner! - assert!(!user_member.is_owner); - assert_eq!(user_member.permissions.unwrap(), ProjectPermissions::all()); + with_test_environment( + None, + |test_env: TestEnvironment| async move { + let api = &test_env.api; + + let alpha_team_id = &test_env.dummy.project_alpha.team_id; + + // Cannot set friend as owner (not a member) + let resp = api + .transfer_team_ownership( + alpha_team_id, + FRIEND_USER_ID, + USER_USER_PAT, + ) + .await; + assert_status!(&resp, StatusCode::BAD_REQUEST); + let resp = api + .transfer_team_ownership( + alpha_team_id, + FRIEND_USER_ID, + FRIEND_USER_PAT, + ) + .await; + assert_status!(&resp, StatusCode::UNAUTHORIZED); + + // first, invite friend + let resp = api + .add_user_to_team( + alpha_team_id, + FRIEND_USER_ID, + None, + None, + USER_USER_PAT, + ) + .await; + assert_status!(&resp, StatusCode::NO_CONTENT); + + // still cannot set friend as owner (not accepted) + let resp = api + .transfer_team_ownership( + alpha_team_id, + FRIEND_USER_ID, + USER_USER_PAT, + ) + .await; + assert_status!(&resp, StatusCode::BAD_REQUEST); - // Confirm that user, a user who still has full permissions, cannot then remove the owner - let resp = api - .remove_from_team(alpha_team_id, FRIEND_USER_ID, USER_USER_PAT) - .await; - assert_status!(&resp, StatusCode::UNAUTHORIZED); + // accept + let resp = api.join_team(alpha_team_id, FRIEND_USER_PAT).await; + assert_status!(&resp, StatusCode::NO_CONTENT); + + // Cannot set ourselves as owner if we are not owner + let resp = api + .transfer_team_ownership( + alpha_team_id, + FRIEND_USER_ID, + FRIEND_USER_PAT, + ) + .await; + assert_status!(&resp, StatusCode::UNAUTHORIZED); + + // Can set friend as owner + let resp = api + .transfer_team_ownership( + alpha_team_id, + FRIEND_USER_ID, + USER_USER_PAT, + ) + .await; + assert_status!(&resp, StatusCode::NO_CONTENT); - // V3 only- confirm the owner can change their role without losing ownership - let resp = api - .edit_team_member( - alpha_team_id, - FRIEND_USER_ID, - json!({ - "role": "Member" - }), - FRIEND_USER_PAT, - ) - .await; - assert_status!(&resp, StatusCode::NO_CONTENT); + // Check + let members = api + .get_team_members_deserialized(alpha_team_id, USER_USER_PAT) + .await; + let friend_member = members + .iter() + .find(|x| x.user.id.0 == FRIEND_USER_ID_PARSED as u64) + .unwrap(); + assert_eq!(friend_member.role, "Member"); // her role does not actually change, but is_owner is set to true + assert!(friend_member.is_owner); + assert_eq!( + friend_member.permissions.unwrap(), + ProjectPermissions::all() + ); + + let user_member = members + .iter() + .find(|x| x.user.id.0 == USER_USER_ID_PARSED as u64) + .unwrap(); + assert_eq!(user_member.role, "Member"); // We are the 'owner', but we are not actually the owner! + assert!(!user_member.is_owner); + assert_eq!( + user_member.permissions.unwrap(), + ProjectPermissions::all() + ); + + // Confirm that user, a user who still has full permissions, cannot then remove the owner + let resp = api + .remove_from_team(alpha_team_id, FRIEND_USER_ID, USER_USER_PAT) + .await; + assert_status!(&resp, StatusCode::UNAUTHORIZED); + + // V3 only- confirm the owner can change their role without losing ownership + let resp = api + .edit_team_member( + alpha_team_id, + FRIEND_USER_ID, + json!({ + "role": "Member" + }), + FRIEND_USER_PAT, + ) + .await; + assert_status!(&resp, StatusCode::NO_CONTENT); - let members = api - .get_team_members_deserialized(alpha_team_id, USER_USER_PAT) - .await; - let friend_member = members - .iter() - .find(|x| x.user.id.0 == FRIEND_USER_ID_PARSED as u64) - .unwrap(); - assert_eq!(friend_member.role, "Member"); - assert!(friend_member.is_owner); - }) + let members = api + .get_team_members_deserialized(alpha_team_id, USER_USER_PAT) + .await; + let friend_member = members + .iter() + .find(|x| x.user.id.0 == FRIEND_USER_ID_PARSED as u64) + .unwrap(); + assert_eq!(friend_member.role, "Member"); + assert!(friend_member.is_owner); + }, + ) .await; } diff --git a/apps/labrinth/tests/user.rs b/apps/labrinth/tests/user.rs index 3e4a14601..b1b7bfd01 100644 --- a/apps/labrinth/tests/user.rs +++ b/apps/labrinth/tests/user.rs @@ -24,7 +24,12 @@ pub async fn get_user_projects_after_creating_project_returns_new_project() { .await; let (project, _) = api - .add_public_project("slug", Some(TestFile::BasicMod), None, USER_USER_PAT) + .add_public_project( + "slug", + Some(TestFile::BasicMod), + None, + USER_USER_PAT, + ) .await; let resp_projects = api @@ -40,7 +45,12 @@ pub async fn get_user_projects_after_deleting_project_shows_removal() { with_test_environment_all(None, |test_env| async move { let api = test_env.api; let (project, _) = api - .add_public_project("iota", Some(TestFile::BasicMod), None, USER_USER_PAT) + .add_public_project( + "iota", + Some(TestFile::BasicMod), + None, + USER_USER_PAT, + ) .await; api.get_user_projects_deserialized_common(USER_USER_ID, USER_USER_PAT) .await; @@ -62,15 +72,27 @@ pub async fn get_user_projects_after_joining_team_shows_team_projects() { let alpha_team_id = &test_env.dummy.project_alpha.team_id; let alpha_project_id = &test_env.dummy.project_alpha.project_id; let api = test_env.api; - api.get_user_projects_deserialized_common(FRIEND_USER_ID, FRIEND_USER_PAT) - .await; + api.get_user_projects_deserialized_common( + FRIEND_USER_ID, + FRIEND_USER_PAT, + ) + .await; - api.add_user_to_team(alpha_team_id, FRIEND_USER_ID, None, None, USER_USER_PAT) - .await; + api.add_user_to_team( + alpha_team_id, + FRIEND_USER_ID, + None, + None, + USER_USER_PAT, + ) + .await; api.join_team(alpha_team_id, FRIEND_USER_PAT).await; let projects = api - .get_user_projects_deserialized_common(FRIEND_USER_ID, FRIEND_USER_PAT) + .get_user_projects_deserialized_common( + FRIEND_USER_ID, + FRIEND_USER_PAT, + ) .await; assert!(projects .iter() @@ -85,17 +107,29 @@ pub async fn get_user_projects_after_leaving_team_shows_no_team_projects() { let alpha_team_id = &test_env.dummy.project_alpha.team_id; let alpha_project_id = &test_env.dummy.project_alpha.project_id; let api = test_env.api; - api.add_user_to_team(alpha_team_id, FRIEND_USER_ID, None, None, USER_USER_PAT) - .await; + api.add_user_to_team( + alpha_team_id, + FRIEND_USER_ID, + None, + None, + USER_USER_PAT, + ) + .await; api.join_team(alpha_team_id, FRIEND_USER_PAT).await; - api.get_user_projects_deserialized_common(FRIEND_USER_ID, FRIEND_USER_PAT) - .await; + api.get_user_projects_deserialized_common( + FRIEND_USER_ID, + FRIEND_USER_PAT, + ) + .await; api.remove_from_team(alpha_team_id, FRIEND_USER_ID, USER_USER_PAT) .await; let projects = api - .get_user_projects_deserialized_common(FRIEND_USER_ID, FRIEND_USER_PAT) + .get_user_projects_deserialized_common( + FRIEND_USER_ID, + FRIEND_USER_PAT, + ) .await; assert!(!projects .iter() diff --git a/apps/labrinth/tests/v2/error.rs b/apps/labrinth/tests/v2/error.rs index 27afb5d28..1ae56a719 100644 --- a/apps/labrinth/tests/v2/error.rs +++ b/apps/labrinth/tests/v2/error.rs @@ -12,14 +12,17 @@ use crate::common::{ }; #[actix_rt::test] pub async fn error_404_empty() { - with_test_environment(None, |test_env: TestEnvironment| async move { - // V2 errors should have 404 as blank body, for missing resources - let api = &test_env.api; - let resp = api.get_project("does-not-exist", USER_USER_PAT).await; - assert_status!(&resp, StatusCode::NOT_FOUND); - let body = test::read_body(resp).await; - let empty_bytes = Bytes::from_static(b""); - assert_eq!(body, empty_bytes); - }) + with_test_environment( + None, + |test_env: TestEnvironment| async move { + // V2 errors should have 404 as blank body, for missing resources + let api = &test_env.api; + let resp = api.get_project("does-not-exist", USER_USER_PAT).await; + assert_status!(&resp, StatusCode::NOT_FOUND); + let body = test::read_body(resp).await; + let empty_bytes = Bytes::from_static(b""); + assert_eq!(body, empty_bytes); + }, + ) .await; } diff --git a/apps/labrinth/tests/v2/notifications.rs b/apps/labrinth/tests/v2/notifications.rs index b80bd2abe..692ae1388 100644 --- a/apps/labrinth/tests/v2/notifications.rs +++ b/apps/labrinth/tests/v2/notifications.rs @@ -6,20 +6,33 @@ use crate::common::{ }; #[actix_rt::test] -pub async fn get_user_notifications_after_team_invitation_returns_notification() { - with_test_environment(None, |test_env: TestEnvironment| async move { - let alpha_team_id = test_env.dummy.project_alpha.team_id.clone(); - let api = test_env.api; - api.add_user_to_team(&alpha_team_id, FRIEND_USER_ID, None, None, USER_USER_PAT) +pub async fn get_user_notifications_after_team_invitation_returns_notification() +{ + with_test_environment( + None, + |test_env: TestEnvironment| async move { + let alpha_team_id = test_env.dummy.project_alpha.team_id.clone(); + let api = test_env.api; + api.add_user_to_team( + &alpha_team_id, + FRIEND_USER_ID, + None, + None, + USER_USER_PAT, + ) .await; - let notifications = api - .get_user_notifications_deserialized(FRIEND_USER_ID, FRIEND_USER_PAT) - .await; - assert_eq!(1, notifications.len()); + let notifications = api + .get_user_notifications_deserialized( + FRIEND_USER_ID, + FRIEND_USER_PAT, + ) + .await; + assert_eq!(1, notifications.len()); - // Check to make sure type_ is correct - assert_eq!(notifications[0].type_.as_ref().unwrap(), "team_invite"); - }) + // Check to make sure type_ is correct + assert_eq!(notifications[0].type_.as_ref().unwrap(), "team_invite"); + }, + ) .await; } diff --git a/apps/labrinth/tests/v2/project.rs b/apps/labrinth/tests/v2/project.rs index 1352ee1f6..5e9006af7 100644 --- a/apps/labrinth/tests/v2/project.rs +++ b/apps/labrinth/tests/v2/project.rs @@ -6,7 +6,8 @@ use crate::{ api_common::{ApiProject, ApiVersion, AppendsOptionalPat}, api_v2::{request_data::get_public_project_creation_data_json, ApiV2}, database::{ - generate_random_name, ADMIN_USER_PAT, FRIEND_USER_ID, FRIEND_USER_PAT, USER_USER_PAT, + generate_random_name, ADMIN_USER_PAT, FRIEND_USER_ID, + FRIEND_USER_PAT, USER_USER_PAT, }, dummy_data::TestFile, environment::{with_test_environment, TestEnvironment}, @@ -19,340 +20,385 @@ use futures::StreamExt; use itertools::Itertools; use labrinth::{ database::models::project_item::PROJECTS_SLUGS_NAMESPACE, - models::{ids::base62_impl::parse_base62, projects::ProjectId, teams::ProjectPermissions}, + models::{ + ids::base62_impl::parse_base62, projects::ProjectId, + teams::ProjectPermissions, + }, util::actix::{AppendsMultipart, MultipartSegment, MultipartSegmentData}, }; use serde_json::json; #[actix_rt::test] async fn test_project_type_sanity() { - with_test_environment(None, |test_env: TestEnvironment| async move { - let api = &test_env.api; - - // Perform all other patch tests on both 'mod' and 'modpack' - for (mod_or_modpack, slug, file) in [ - ("mod", "test-mod", TestFile::build_random_jar()), - ("modpack", "test-modpack", TestFile::build_random_mrpack()), - ] { - // Create a modpack or mod - // both are 'fabric' (but modpack is actually 'mrpack' behind the scenes, through v3,with fabric as a 'mrpack_loader') - let (test_project, test_version) = api - .add_public_project(slug, Some(file), None, USER_USER_PAT) - .await; - let test_project_slug = test_project.slug.as_ref().unwrap(); - - // Check that the loader displays correctly as fabric from the version creation - assert_eq!(test_project.loaders, vec!["fabric"]); - assert_eq!(test_version[0].loaders, vec!["fabric"]); + with_test_environment( + None, + |test_env: TestEnvironment| async move { + let api = &test_env.api; + + // Perform all other patch tests on both 'mod' and 'modpack' + for (mod_or_modpack, slug, file) in [ + ("mod", "test-mod", TestFile::build_random_jar()), + ("modpack", "test-modpack", TestFile::build_random_mrpack()), + ] { + // Create a modpack or mod + // both are 'fabric' (but modpack is actually 'mrpack' behind the scenes, through v3,with fabric as a 'mrpack_loader') + let (test_project, test_version) = api + .add_public_project(slug, Some(file), None, USER_USER_PAT) + .await; + let test_project_slug = test_project.slug.as_ref().unwrap(); + + // Check that the loader displays correctly as fabric from the version creation + assert_eq!(test_project.loaders, vec!["fabric"]); + assert_eq!(test_version[0].loaders, vec!["fabric"]); + + // Check that the project type is correct when getting the project + let project = api + .get_project_deserialized(test_project_slug, USER_USER_PAT) + .await; + assert_eq!(test_project.loaders, vec!["fabric"]); + assert_eq!(project.project_type, mod_or_modpack); + + // Check that the project type is correct when getting the version + let version = api + .get_version_deserialized( + &test_version[0].id.to_string(), + USER_USER_PAT, + ) + .await; + assert_eq!( + version.loaders.iter().map(|x| &x.0).collect_vec(), + vec!["fabric"] + ); + + // Edit the version loader to change it to 'forge' + let resp = api + .edit_version( + &test_version[0].id.to_string(), + json!({ + "loaders": ["forge"], + }), + USER_USER_PAT, + ) + .await; + assert_status!(&resp, StatusCode::NO_CONTENT); + + // Check that the project type is still correct when getting the project + let project = api + .get_project_deserialized(test_project_slug, USER_USER_PAT) + .await; + assert_eq!(project.project_type, mod_or_modpack); + assert_eq!(project.loaders, vec!["forge"]); + + // Check that the project type is still correct when getting the version + let version = api + .get_version_deserialized( + &test_version[0].id.to_string(), + USER_USER_PAT, + ) + .await; + assert_eq!( + version.loaders.iter().map(|x| &x.0).collect_vec(), + vec!["forge"] + ); + } - // Check that the project type is correct when getting the project - let project = api - .get_project_deserialized(test_project_slug, USER_USER_PAT) - .await; - assert_eq!(test_project.loaders, vec!["fabric"]); - assert_eq!(project.project_type, mod_or_modpack); + // As we get more complicated strucures with as v3 continues to expand, and alpha/beta get more complicated, we should add more tests here, + // to ensure that projects created with v3 routes are still valid and work with v3 routes. + }, + ) + .await; +} - // Check that the project type is correct when getting the version - let version = api - .get_version_deserialized(&test_version[0].id.to_string(), USER_USER_PAT) - .await; - assert_eq!( - version.loaders.iter().map(|x| &x.0).collect_vec(), - vec!["fabric"] +#[actix_rt::test] +async fn test_add_remove_project() { + // Test setup and dummy data + with_test_environment( + None, + |test_env: TestEnvironment| async move { + let api = &test_env.api; + + // Generate test project data. + let mut json_data = get_public_project_creation_data_json( + "demo", + Some(&TestFile::BasicMod), ); - // Edit the version loader to change it to 'forge' - let resp = api - .edit_version( - &test_version[0].id.to_string(), - json!({ - "loaders": ["forge"], - }), + // Basic json + let json_segment = MultipartSegment { + name: "data".to_string(), + filename: None, + content_type: Some("application/json".to_string()), + data: MultipartSegmentData::Text( + serde_json::to_string(&json_data).unwrap(), + ), + }; + + // Basic json, with a different file + json_data["initial_versions"][0]["file_parts"][0] = + json!("basic-mod-different.jar"); + let json_diff_file_segment = MultipartSegment { + data: MultipartSegmentData::Text( + serde_json::to_string(&json_data).unwrap(), + ), + ..json_segment.clone() + }; + + // Basic json, with a different file, and a different slug + json_data["slug"] = json!("new_demo"); + json_data["initial_versions"][0]["file_parts"][0] = + json!("basic-mod-different.jar"); + let json_diff_slug_file_segment = MultipartSegment { + data: MultipartSegmentData::Text( + serde_json::to_string(&json_data).unwrap(), + ), + ..json_segment.clone() + }; + + let basic_mod_file = TestFile::BasicMod; + let basic_mod_different_file = TestFile::BasicModDifferent; + + // Basic file + let file_segment = MultipartSegment { + // 'Basic' + name: basic_mod_file.filename(), + filename: Some(basic_mod_file.filename()), + content_type: basic_mod_file.content_type(), + data: MultipartSegmentData::Binary(basic_mod_file.bytes()), + }; + + // Differently named file, with the SAME content (for hash testing) + let file_diff_name_segment = MultipartSegment { + // 'Different' + name: basic_mod_different_file.filename(), + filename: Some(basic_mod_different_file.filename()), + content_type: basic_mod_different_file.content_type(), + // 'Basic' + data: MultipartSegmentData::Binary(basic_mod_file.bytes()), + }; + + // Differently named file, with different content + let file_diff_name_content_segment = MultipartSegment { + // 'Different' + name: basic_mod_different_file.filename(), + filename: Some(basic_mod_different_file.filename()), + content_type: basic_mod_different_file.content_type(), + data: MultipartSegmentData::Binary( + basic_mod_different_file.bytes(), + ), + }; + + // Add a project- simple, should work. + let req = test::TestRequest::post() + .uri("/v2/project") + .append_pat(USER_USER_PAT) + .set_multipart(vec![json_segment.clone(), file_segment.clone()]) + .to_request(); + let resp: actix_web::dev::ServiceResponse = + test_env.call(req).await; + assert_status!(&resp, StatusCode::OK); + + // Get the project we just made, and confirm that it's correct + let project = + api.get_project_deserialized("demo", USER_USER_PAT).await; + assert!(project.versions.len() == 1); + let uploaded_version_id = project.versions[0]; + + // Checks files to ensure they were uploaded and correctly identify the file + let hash = sha1::Sha1::from(basic_mod_file.bytes()) + .digest() + .to_string(); + let version = api + .get_version_from_hash_deserialized( + &hash, + "sha1", USER_USER_PAT, ) .await; + assert_eq!(version.id, uploaded_version_id); + + // Reusing with a different slug and the same file should fail + // Even if that file is named differently + let req = test::TestRequest::post() + .uri("/v2/project") + .append_pat(USER_USER_PAT) + .set_multipart(vec![ + json_diff_slug_file_segment.clone(), // Different slug, different file name + file_diff_name_segment.clone(), // Different file name, same content + ]) + .to_request(); + + let resp = test_env.call(req).await; + assert_status!(&resp, StatusCode::BAD_REQUEST); + + // Reusing with the same slug and a different file should fail + let req = test::TestRequest::post() + .uri("/v2/project") + .append_pat(USER_USER_PAT) + .set_multipart(vec![ + json_diff_file_segment.clone(), // Same slug, different file name + file_diff_name_content_segment.clone(), // Different file name, different content + ]) + .to_request(); + + let resp = test_env.call(req).await; + assert_status!(&resp, StatusCode::BAD_REQUEST); + + // Different slug, different file should succeed + let req = test::TestRequest::post() + .uri("/v2/project") + .append_pat(USER_USER_PAT) + .set_multipart(vec![ + json_diff_slug_file_segment.clone(), // Different slug, different file name + file_diff_name_content_segment.clone(), // Different file name, same content + ]) + .to_request(); + + let resp = test_env.call(req).await; + assert_status!(&resp, StatusCode::OK); + + // Get + let project = + api.get_project_deserialized("demo", USER_USER_PAT).await; + let id = project.id.to_string(); + + // Remove the project + let resp = test_env.api.remove_project("demo", USER_USER_PAT).await; assert_status!(&resp, StatusCode::NO_CONTENT); - // Check that the project type is still correct when getting the project - let project = api - .get_project_deserialized(test_project_slug, USER_USER_PAT) - .await; - assert_eq!(project.project_type, mod_or_modpack); - assert_eq!(project.loaders, vec!["forge"]); - - // Check that the project type is still correct when getting the version - let version = api - .get_version_deserialized(&test_version[0].id.to_string(), USER_USER_PAT) - .await; + // Confirm that the project is gone from the cache + let mut redis_conn = + test_env.db.redis_pool.connect().await.unwrap(); + assert_eq!( + redis_conn + .get(PROJECTS_SLUGS_NAMESPACE, "demo") + .await + .unwrap() + .map(|x| x.parse::().unwrap()), + None + ); assert_eq!( - version.loaders.iter().map(|x| &x.0).collect_vec(), - vec!["forge"] + redis_conn + .get(PROJECTS_SLUGS_NAMESPACE, &id) + .await + .unwrap() + .map(|x| x.parse::().unwrap()), + None ); - } - // As we get more complicated strucures with as v3 continues to expand, and alpha/beta get more complicated, we should add more tests here, - // to ensure that projects created with v3 routes are still valid and work with v3 routes. - }) + // Old slug no longer works + let resp = api.get_project("demo", USER_USER_PAT).await; + assert_status!(&resp, StatusCode::NOT_FOUND); + }, + ) .await; } #[actix_rt::test] -async fn test_add_remove_project() { - // Test setup and dummy data - with_test_environment(None, |test_env: TestEnvironment| async move { - let api = &test_env.api; - - // Generate test project data. - let mut json_data = - get_public_project_creation_data_json("demo", Some(&TestFile::BasicMod)); - - // Basic json - let json_segment = MultipartSegment { - name: "data".to_string(), - filename: None, - content_type: Some("application/json".to_string()), - data: MultipartSegmentData::Text(serde_json::to_string(&json_data).unwrap()), - }; - - // Basic json, with a different file - json_data["initial_versions"][0]["file_parts"][0] = json!("basic-mod-different.jar"); - let json_diff_file_segment = MultipartSegment { - data: MultipartSegmentData::Text(serde_json::to_string(&json_data).unwrap()), - ..json_segment.clone() - }; - - // Basic json, with a different file, and a different slug - json_data["slug"] = json!("new_demo"); - json_data["initial_versions"][0]["file_parts"][0] = json!("basic-mod-different.jar"); - let json_diff_slug_file_segment = MultipartSegment { - data: MultipartSegmentData::Text(serde_json::to_string(&json_data).unwrap()), - ..json_segment.clone() - }; - - let basic_mod_file = TestFile::BasicMod; - let basic_mod_different_file = TestFile::BasicModDifferent; - - // Basic file - let file_segment = MultipartSegment { - // 'Basic' - name: basic_mod_file.filename(), - filename: Some(basic_mod_file.filename()), - content_type: basic_mod_file.content_type(), - data: MultipartSegmentData::Binary(basic_mod_file.bytes()), - }; - - // Differently named file, with the SAME content (for hash testing) - let file_diff_name_segment = MultipartSegment { - // 'Different' - name: basic_mod_different_file.filename(), - filename: Some(basic_mod_different_file.filename()), - content_type: basic_mod_different_file.content_type(), - // 'Basic' - data: MultipartSegmentData::Binary(basic_mod_file.bytes()), - }; - - // Differently named file, with different content - let file_diff_name_content_segment = MultipartSegment { - // 'Different' - name: basic_mod_different_file.filename(), - filename: Some(basic_mod_different_file.filename()), - content_type: basic_mod_different_file.content_type(), - data: MultipartSegmentData::Binary(basic_mod_different_file.bytes()), - }; - - // Add a project- simple, should work. - let req = test::TestRequest::post() - .uri("/v2/project") - .append_pat(USER_USER_PAT) - .set_multipart(vec![json_segment.clone(), file_segment.clone()]) - .to_request(); - let resp: actix_web::dev::ServiceResponse = test_env.call(req).await; - assert_status!(&resp, StatusCode::OK); - - // Get the project we just made, and confirm that it's correct - let project = api.get_project_deserialized("demo", USER_USER_PAT).await; - assert!(project.versions.len() == 1); - let uploaded_version_id = project.versions[0]; - - // Checks files to ensure they were uploaded and correctly identify the file - let hash = sha1::Sha1::from(basic_mod_file.bytes()) - .digest() - .to_string(); - let version = api - .get_version_from_hash_deserialized(&hash, "sha1", USER_USER_PAT) - .await; - assert_eq!(version.id, uploaded_version_id); - - // Reusing with a different slug and the same file should fail - // Even if that file is named differently - let req = test::TestRequest::post() - .uri("/v2/project") - .append_pat(USER_USER_PAT) - .set_multipart(vec![ - json_diff_slug_file_segment.clone(), // Different slug, different file name - file_diff_name_segment.clone(), // Different file name, same content - ]) - .to_request(); - - let resp = test_env.call(req).await; - assert_status!(&resp, StatusCode::BAD_REQUEST); - - // Reusing with the same slug and a different file should fail - let req = test::TestRequest::post() - .uri("/v2/project") - .append_pat(USER_USER_PAT) - .set_multipart(vec![ - json_diff_file_segment.clone(), // Same slug, different file name - file_diff_name_content_segment.clone(), // Different file name, different content - ]) - .to_request(); - - let resp = test_env.call(req).await; - assert_status!(&resp, StatusCode::BAD_REQUEST); - - // Different slug, different file should succeed - let req = test::TestRequest::post() - .uri("/v2/project") - .append_pat(USER_USER_PAT) - .set_multipart(vec![ - json_diff_slug_file_segment.clone(), // Different slug, different file name - file_diff_name_content_segment.clone(), // Different file name, same content - ]) - .to_request(); - - let resp = test_env.call(req).await; - assert_status!(&resp, StatusCode::OK); - - // Get - let project = api.get_project_deserialized("demo", USER_USER_PAT).await; - let id = project.id.to_string(); - - // Remove the project - let resp = test_env.api.remove_project("demo", USER_USER_PAT).await; - assert_status!(&resp, StatusCode::NO_CONTENT); - - // Confirm that the project is gone from the cache - let mut redis_conn = test_env.db.redis_pool.connect().await.unwrap(); - assert_eq!( - redis_conn - .get(PROJECTS_SLUGS_NAMESPACE, "demo") +async fn permissions_upload_version() { + with_test_environment( + None, + |test_env: TestEnvironment| async move { + let alpha_project_id = &test_env.dummy.project_alpha.project_id; + let alpha_version_id = &test_env.dummy.project_alpha.version_id; + let alpha_team_id = &test_env.dummy.project_alpha.team_id; + let alpha_file_hash = &test_env.dummy.project_alpha.file_hash; + + let api = &test_env.api; + let basic_mod_different_file = TestFile::BasicModDifferent; + let upload_version = ProjectPermissions::UPLOAD_VERSION; + + let req_gen = |ctx: PermissionsTestContext| async move { + let project_id = ctx.project_id.unwrap(); + let project_id = ProjectId(parse_base62(&project_id).unwrap()); + api.add_public_version( + project_id, + "1.0.0", + TestFile::BasicMod, + None, + None, + ctx.test_pat.as_deref(), + ) .await - .unwrap() - .map(|x| x.parse::().unwrap()), - None - ); - assert_eq!( - redis_conn - .get(PROJECTS_SLUGS_NAMESPACE, &id) + }; + + PermissionsTest::new(&test_env) + .simple_project_permissions_test(upload_version, req_gen) .await - .unwrap() - .map(|x| x.parse::().unwrap()), - None - ); - - // Old slug no longer works - let resp = api.get_project("demo", USER_USER_PAT).await; - assert_status!(&resp, StatusCode::NOT_FOUND); - }) - .await; -} + .unwrap(); -#[actix_rt::test] -async fn permissions_upload_version() { - with_test_environment(None, |test_env: TestEnvironment| async move { - let alpha_project_id = &test_env.dummy.project_alpha.project_id; - let alpha_version_id = &test_env.dummy.project_alpha.version_id; - let alpha_team_id = &test_env.dummy.project_alpha.team_id; - let alpha_file_hash = &test_env.dummy.project_alpha.file_hash; - - let api = &test_env.api; - let basic_mod_different_file = TestFile::BasicModDifferent; - let upload_version = ProjectPermissions::UPLOAD_VERSION; - - let req_gen = |ctx: PermissionsTestContext| async move { - let project_id = ctx.project_id.unwrap(); - let project_id = ProjectId(parse_base62(&project_id).unwrap()); - api.add_public_version( - project_id, - "1.0.0", - TestFile::BasicMod, - None, - None, - ctx.test_pat.as_deref(), - ) - .await - }; - - PermissionsTest::new(&test_env) - .simple_project_permissions_test(upload_version, req_gen) - .await - .unwrap(); - - // Upload file to existing version - // Uses alpha project, as it has an existing version - let file_ref = Arc::new(basic_mod_different_file); - let req_gen = |ctx: PermissionsTestContext| { - let file_ref = file_ref.clone(); - async move { - api.upload_file_to_version(alpha_version_id, &file_ref, ctx.test_pat.as_deref()) + // Upload file to existing version + // Uses alpha project, as it has an existing version + let file_ref = Arc::new(basic_mod_different_file); + let req_gen = |ctx: PermissionsTestContext| { + let file_ref = file_ref.clone(); + async move { + api.upload_file_to_version( + alpha_version_id, + &file_ref, + ctx.test_pat.as_deref(), + ) .await - } - }; - PermissionsTest::new(&test_env) - .with_existing_project(alpha_project_id, alpha_team_id) - .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) - .simple_project_permissions_test(upload_version, req_gen) - .await - .unwrap(); - - // Patch version - // Uses alpha project, as it has an existing version - let req_gen = |ctx: PermissionsTestContext| async move { - api.edit_version( - alpha_version_id, - json!({ - "name": "Basic Mod", - }), - ctx.test_pat.as_deref(), - ) - .await - }; - - PermissionsTest::new(&test_env) - .with_existing_project(alpha_project_id, alpha_team_id) - .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) - .simple_project_permissions_test(upload_version, req_gen) - .await - .unwrap(); - - // Delete version file - // Uses alpha project, as it has an existing version - let delete_version = ProjectPermissions::DELETE_VERSION; - let req_gen = |ctx: PermissionsTestContext| async move { - api.remove_version_file(alpha_file_hash, ctx.test_pat.as_deref()) + } + }; + PermissionsTest::new(&test_env) + .with_existing_project(alpha_project_id, alpha_team_id) + .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) + .simple_project_permissions_test(upload_version, req_gen) + .await + .unwrap(); + + // Patch version + // Uses alpha project, as it has an existing version + let req_gen = |ctx: PermissionsTestContext| async move { + api.edit_version( + alpha_version_id, + json!({ + "name": "Basic Mod", + }), + ctx.test_pat.as_deref(), + ) .await - }; - - PermissionsTest::new(&test_env) - .with_existing_project(alpha_project_id, alpha_team_id) - .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) - .simple_project_permissions_test(delete_version, req_gen) - .await - .unwrap(); - - // Delete version - // Uses alpha project, as it has an existing version - let req_gen = |ctx: PermissionsTestContext| async move { - api.remove_version(alpha_version_id, ctx.test_pat.as_deref()) + }; + + PermissionsTest::new(&test_env) + .with_existing_project(alpha_project_id, alpha_team_id) + .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) + .simple_project_permissions_test(upload_version, req_gen) .await - }; - PermissionsTest::new(&test_env) - .with_existing_project(alpha_project_id, alpha_team_id) - .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) - .simple_project_permissions_test(delete_version, req_gen) - .await - .unwrap(); - }) + .unwrap(); + + // Delete version file + // Uses alpha project, as it has an existing version + let delete_version = ProjectPermissions::DELETE_VERSION; + let req_gen = |ctx: PermissionsTestContext| async move { + api.remove_version_file( + alpha_file_hash, + ctx.test_pat.as_deref(), + ) + .await + }; + + PermissionsTest::new(&test_env) + .with_existing_project(alpha_project_id, alpha_team_id) + .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) + .simple_project_permissions_test(delete_version, req_gen) + .await + .unwrap(); + + // Delete version + // Uses alpha project, as it has an existing version + let req_gen = |ctx: PermissionsTestContext| async move { + api.remove_version(alpha_version_id, ctx.test_pat.as_deref()) + .await + }; + PermissionsTest::new(&test_env) + .with_existing_project(alpha_project_id, alpha_team_id) + .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) + .simple_project_permissions_test(delete_version, req_gen) + .await + .unwrap(); + }, + ) .await; } @@ -360,67 +406,72 @@ async fn permissions_upload_version() { pub async fn test_patch_v2() { // Hits V3-specific patchable fields // Other fields are tested in test_patch_project (the v2 version of that test) - with_test_environment(None, |test_env: TestEnvironment| async move { - let api = &test_env.api; - - let alpha_project_slug = &test_env.dummy.project_alpha.project_slug; - - // Sucessful request to patch many fields. - let resp = api - .edit_project( - alpha_project_slug, - json!({ - "client_side": "unsupported", - "server_side": "required", - }), - USER_USER_PAT, - ) - .await; - assert_status!(&resp, StatusCode::NO_CONTENT); - - let project = api - .get_project_deserialized(alpha_project_slug, USER_USER_PAT) - .await; - - // Note: the original V2 value of this was "optional", - // but Required/Optional is no longer a carried combination in v3, as the changes made were lossy. - // Now, the test Required/Unsupported combination is tested instead. - // Setting Required/Optional in v2 will not work, this is known and accepteed. - assert_eq!(project.client_side.as_str(), "unsupported"); - assert_eq!(project.server_side.as_str(), "required"); - }) + with_test_environment( + None, + |test_env: TestEnvironment| async move { + let api = &test_env.api; + + let alpha_project_slug = &test_env.dummy.project_alpha.project_slug; + + // Sucessful request to patch many fields. + let resp = api + .edit_project( + alpha_project_slug, + json!({ + "client_side": "unsupported", + "server_side": "required", + }), + USER_USER_PAT, + ) + .await; + assert_status!(&resp, StatusCode::NO_CONTENT); + + let project = api + .get_project_deserialized(alpha_project_slug, USER_USER_PAT) + .await; + + // Note: the original V2 value of this was "optional", + // but Required/Optional is no longer a carried combination in v3, as the changes made were lossy. + // Now, the test Required/Unsupported combination is tested instead. + // Setting Required/Optional in v2 will not work, this is known and accepteed. + assert_eq!(project.client_side.as_str(), "unsupported"); + assert_eq!(project.server_side.as_str(), "required"); + }, + ) .await; } #[actix_rt::test] async fn permissions_patch_project_v2() { - with_test_environment(Some(8), |test_env: TestEnvironment| async move { - let api = &test_env.api; - - // For each permission covered by EDIT_DETAILS, ensure the permission is required - let edit_details = ProjectPermissions::EDIT_DETAILS; - let test_pairs = [ - ("description", json!("description")), - ("issues_url", json!("https://issues.com")), - ("source_url", json!("https://source.com")), - ("wiki_url", json!("https://wiki.com")), - ( - "donation_urls", - json!([{ - "id": "paypal", - "platform": "Paypal", - "url": "https://paypal.com" - }]), - ), - ("discord_url", json!("https://discord.com")), - ]; - - futures::stream::iter(test_pairs) - .map(|(key, value)| { - let test_env = test_env.clone(); - async move { - let req_gen = |ctx: PermissionsTestContext| async { - api.edit_project( + with_test_environment( + Some(8), + |test_env: TestEnvironment| async move { + let api = &test_env.api; + + // For each permission covered by EDIT_DETAILS, ensure the permission is required + let edit_details = ProjectPermissions::EDIT_DETAILS; + let test_pairs = [ + ("description", json!("description")), + ("issues_url", json!("https://issues.com")), + ("source_url", json!("https://source.com")), + ("wiki_url", json!("https://wiki.com")), + ( + "donation_urls", + json!([{ + "id": "paypal", + "platform": "Paypal", + "url": "https://paypal.com" + }]), + ), + ("discord_url", json!("https://discord.com")), + ]; + + futures::stream::iter(test_pairs) + .map(|(key, value)| { + let test_env = test_env.clone(); + async move { + let req_gen = |ctx: PermissionsTestContext| async { + api.edit_project( &ctx.project_id.unwrap(), json!({ key: if key == "slug" { @@ -432,200 +483,217 @@ async fn permissions_patch_project_v2() { ctx.test_pat.as_deref(), ) .await - }; - PermissionsTest::new(&test_env) - .simple_project_permissions_test(edit_details, req_gen) - .await - .into_iter(); - } - }) - .buffer_unordered(4) - .collect::>() - .await; - - // Edit body - // Cannot bulk edit body - let edit_body = ProjectPermissions::EDIT_BODY; - let req_gen = |ctx: PermissionsTestContext| async move { - api.edit_project( - &ctx.project_id.unwrap(), - json!({ - "body": "new body!", // new body - }), - ctx.test_pat.as_deref(), - ) - .await - }; - PermissionsTest::new(&test_env) - .simple_project_permissions_test(edit_body, req_gen) - .await - .unwrap(); - }) + }; + PermissionsTest::new(&test_env) + .simple_project_permissions_test( + edit_details, + req_gen, + ) + .await + .into_iter(); + } + }) + .buffer_unordered(4) + .collect::>() + .await; + + // Edit body + // Cannot bulk edit body + let edit_body = ProjectPermissions::EDIT_BODY; + let req_gen = |ctx: PermissionsTestContext| async move { + api.edit_project( + &ctx.project_id.unwrap(), + json!({ + "body": "new body!", // new body + }), + ctx.test_pat.as_deref(), + ) + .await + }; + PermissionsTest::new(&test_env) + .simple_project_permissions_test(edit_body, req_gen) + .await + .unwrap(); + }, + ) .await; } #[actix_rt::test] pub async fn test_bulk_edit_links() { - with_test_environment(None, |test_env: TestEnvironment| async move { - let api = &test_env.api; - let alpha_project_id: &str = &test_env.dummy.project_alpha.project_id; - let beta_project_id: &str = &test_env.dummy.project_beta.project_id; - - let resp = api - .edit_project_bulk( - &[alpha_project_id, beta_project_id], - json!({ - "issues_url": "https://github.com", - "donation_urls": [ - { - "id": "patreon", - "platform": "Patreon", - "url": "https://www.patreon.com/my_user" - } - ], - }), - ADMIN_USER_PAT, - ) - .await; - assert_status!(&resp, StatusCode::NO_CONTENT); - - let alpha_body = api - .get_project_deserialized(alpha_project_id, ADMIN_USER_PAT) - .await; - let donation_urls = alpha_body.donation_urls.unwrap(); - assert_eq!(donation_urls.len(), 1); - assert_eq!(donation_urls[0].url, "https://www.patreon.com/my_user"); - assert_eq!( - alpha_body.issues_url, - Some("https://github.com".to_string()) - ); - assert_eq!(alpha_body.discord_url, None); - - let beta_body = api - .get_project_deserialized(beta_project_id, ADMIN_USER_PAT) - .await; - let donation_urls = beta_body.donation_urls.unwrap(); - assert_eq!(donation_urls.len(), 1); - assert_eq!(donation_urls[0].url, "https://www.patreon.com/my_user"); - assert_eq!(beta_body.issues_url, Some("https://github.com".to_string())); - assert_eq!(beta_body.discord_url, None); - - let resp = api - .edit_project_bulk( - &[alpha_project_id, beta_project_id], - json!({ - "discord_url": "https://discord.gg", - "issues_url": null, - "add_donation_urls": [ - { - "id": "bmac", - "platform": "Buy Me a Coffee", - "url": "https://www.buymeacoffee.com/my_user" - } - ], - }), - ADMIN_USER_PAT, - ) - .await; - assert_status!(&resp, StatusCode::NO_CONTENT); - - let alpha_body = api - .get_project_deserialized(alpha_project_id, ADMIN_USER_PAT) - .await; - let donation_urls = alpha_body - .donation_urls - .unwrap() - .into_iter() - .sorted_by_key(|x| x.id.clone()) - .collect_vec(); - assert_eq!(donation_urls.len(), 2); - assert_eq!(donation_urls[0].url, "https://www.buymeacoffee.com/my_user"); - assert_eq!(donation_urls[1].url, "https://www.patreon.com/my_user"); - assert_eq!(alpha_body.issues_url, None); - assert_eq!( - alpha_body.discord_url, - Some("https://discord.gg".to_string()) - ); - - let beta_body = api - .get_project_deserialized(beta_project_id, ADMIN_USER_PAT) - .await; - let donation_urls = beta_body - .donation_urls - .unwrap() - .into_iter() - .sorted_by_key(|x| x.id.clone()) - .collect_vec(); - assert_eq!(donation_urls.len(), 2); - assert_eq!(donation_urls[0].url, "https://www.buymeacoffee.com/my_user"); - assert_eq!(donation_urls[1].url, "https://www.patreon.com/my_user"); - assert_eq!(alpha_body.issues_url, None); - assert_eq!( - alpha_body.discord_url, - Some("https://discord.gg".to_string()) - ); - - let resp = api - .edit_project_bulk( - &[alpha_project_id, beta_project_id], - json!({ - "donation_urls": [ - { - "id": "patreon", - "platform": "Patreon", - "url": "https://www.patreon.com/my_user" - }, - { - "id": "ko-fi", - "platform": "Ko-fi", - "url": "https://www.ko-fi.com/my_user" - } - ], - "add_donation_urls": [ - { - "id": "paypal", - "platform": "PayPal", - "url": "https://www.paypal.com/my_user" - } - ], - "remove_donation_urls": [ - { - "id": "ko-fi", - "platform": "Ko-fi", - "url": "https://www.ko-fi.com/my_user" - } - ], - }), - ADMIN_USER_PAT, - ) - .await; - assert_status!(&resp, StatusCode::NO_CONTENT); - - let alpha_body = api - .get_project_deserialized(alpha_project_id, ADMIN_USER_PAT) - .await; - let donation_urls = alpha_body - .donation_urls - .unwrap() - .into_iter() - .sorted_by_key(|x| x.id.clone()) - .collect_vec(); - assert_eq!(donation_urls.len(), 2); - assert_eq!(donation_urls[0].url, "https://www.patreon.com/my_user"); - assert_eq!(donation_urls[1].url, "https://www.paypal.com/my_user"); - - let beta_body = api - .get_project_deserialized(beta_project_id, ADMIN_USER_PAT) - .await; - let donation_urls = beta_body - .donation_urls - .unwrap() - .into_iter() - .sorted_by_key(|x| x.id.clone()) - .collect_vec(); - assert_eq!(donation_urls.len(), 2); - assert_eq!(donation_urls[0].url, "https://www.patreon.com/my_user"); - assert_eq!(donation_urls[1].url, "https://www.paypal.com/my_user"); - }) + with_test_environment( + None, + |test_env: TestEnvironment| async move { + let api = &test_env.api; + let alpha_project_id: &str = + &test_env.dummy.project_alpha.project_id; + let beta_project_id: &str = &test_env.dummy.project_beta.project_id; + + let resp = api + .edit_project_bulk( + &[alpha_project_id, beta_project_id], + json!({ + "issues_url": "https://github.com", + "donation_urls": [ + { + "id": "patreon", + "platform": "Patreon", + "url": "https://www.patreon.com/my_user" + } + ], + }), + ADMIN_USER_PAT, + ) + .await; + assert_status!(&resp, StatusCode::NO_CONTENT); + + let alpha_body = api + .get_project_deserialized(alpha_project_id, ADMIN_USER_PAT) + .await; + let donation_urls = alpha_body.donation_urls.unwrap(); + assert_eq!(donation_urls.len(), 1); + assert_eq!(donation_urls[0].url, "https://www.patreon.com/my_user"); + assert_eq!( + alpha_body.issues_url, + Some("https://github.com".to_string()) + ); + assert_eq!(alpha_body.discord_url, None); + + let beta_body = api + .get_project_deserialized(beta_project_id, ADMIN_USER_PAT) + .await; + let donation_urls = beta_body.donation_urls.unwrap(); + assert_eq!(donation_urls.len(), 1); + assert_eq!(donation_urls[0].url, "https://www.patreon.com/my_user"); + assert_eq!( + beta_body.issues_url, + Some("https://github.com".to_string()) + ); + assert_eq!(beta_body.discord_url, None); + + let resp = api + .edit_project_bulk( + &[alpha_project_id, beta_project_id], + json!({ + "discord_url": "https://discord.gg", + "issues_url": null, + "add_donation_urls": [ + { + "id": "bmac", + "platform": "Buy Me a Coffee", + "url": "https://www.buymeacoffee.com/my_user" + } + ], + }), + ADMIN_USER_PAT, + ) + .await; + assert_status!(&resp, StatusCode::NO_CONTENT); + + let alpha_body = api + .get_project_deserialized(alpha_project_id, ADMIN_USER_PAT) + .await; + let donation_urls = alpha_body + .donation_urls + .unwrap() + .into_iter() + .sorted_by_key(|x| x.id.clone()) + .collect_vec(); + assert_eq!(donation_urls.len(), 2); + assert_eq!( + donation_urls[0].url, + "https://www.buymeacoffee.com/my_user" + ); + assert_eq!(donation_urls[1].url, "https://www.patreon.com/my_user"); + assert_eq!(alpha_body.issues_url, None); + assert_eq!( + alpha_body.discord_url, + Some("https://discord.gg".to_string()) + ); + + let beta_body = api + .get_project_deserialized(beta_project_id, ADMIN_USER_PAT) + .await; + let donation_urls = beta_body + .donation_urls + .unwrap() + .into_iter() + .sorted_by_key(|x| x.id.clone()) + .collect_vec(); + assert_eq!(donation_urls.len(), 2); + assert_eq!( + donation_urls[0].url, + "https://www.buymeacoffee.com/my_user" + ); + assert_eq!(donation_urls[1].url, "https://www.patreon.com/my_user"); + assert_eq!(alpha_body.issues_url, None); + assert_eq!( + alpha_body.discord_url, + Some("https://discord.gg".to_string()) + ); + + let resp = api + .edit_project_bulk( + &[alpha_project_id, beta_project_id], + json!({ + "donation_urls": [ + { + "id": "patreon", + "platform": "Patreon", + "url": "https://www.patreon.com/my_user" + }, + { + "id": "ko-fi", + "platform": "Ko-fi", + "url": "https://www.ko-fi.com/my_user" + } + ], + "add_donation_urls": [ + { + "id": "paypal", + "platform": "PayPal", + "url": "https://www.paypal.com/my_user" + } + ], + "remove_donation_urls": [ + { + "id": "ko-fi", + "platform": "Ko-fi", + "url": "https://www.ko-fi.com/my_user" + } + ], + }), + ADMIN_USER_PAT, + ) + .await; + assert_status!(&resp, StatusCode::NO_CONTENT); + + let alpha_body = api + .get_project_deserialized(alpha_project_id, ADMIN_USER_PAT) + .await; + let donation_urls = alpha_body + .donation_urls + .unwrap() + .into_iter() + .sorted_by_key(|x| x.id.clone()) + .collect_vec(); + assert_eq!(donation_urls.len(), 2); + assert_eq!(donation_urls[0].url, "https://www.patreon.com/my_user"); + assert_eq!(donation_urls[1].url, "https://www.paypal.com/my_user"); + + let beta_body = api + .get_project_deserialized(beta_project_id, ADMIN_USER_PAT) + .await; + let donation_urls = beta_body + .donation_urls + .unwrap() + .into_iter() + .sorted_by_key(|x| x.id.clone()) + .collect_vec(); + assert_eq!(donation_urls.len(), 2); + assert_eq!(donation_urls[0].url, "https://www.patreon.com/my_user"); + assert_eq!(donation_urls[1].url, "https://www.paypal.com/my_user"); + }, + ) .await; } diff --git a/apps/labrinth/tests/v2/scopes.rs b/apps/labrinth/tests/v2/scopes.rs index cac103e61..be53bc20e 100644 --- a/apps/labrinth/tests/v2/scopes.rs +++ b/apps/labrinth/tests/v2/scopes.rs @@ -13,69 +13,78 @@ use labrinth::models::projects::ProjectId; // Project version creation scopes #[actix_rt::test] pub async fn project_version_create_scopes() { - with_test_environment(None, |test_env: TestEnvironment| async move { - let api = &test_env.api; - // Create project - let create_project = Scopes::PROJECT_CREATE; + with_test_environment( + None, + |test_env: TestEnvironment| async move { + let api = &test_env.api; + // Create project + let create_project = Scopes::PROJECT_CREATE; - let req_gen = |pat: Option| async move { - let creation_data = - get_public_project_creation_data("demo", Some(TestFile::BasicMod), None); - api.create_project(creation_data, pat.as_deref()).await - }; - let (_, success) = ScopeTest::new(&test_env) - .test(req_gen, create_project) - .await - .unwrap(); - let project_id = success["id"].as_str().unwrap(); - let project_id = ProjectId(parse_base62(project_id).unwrap()); + let req_gen = |pat: Option| async move { + let creation_data = get_public_project_creation_data( + "demo", + Some(TestFile::BasicMod), + None, + ); + api.create_project(creation_data, pat.as_deref()).await + }; + let (_, success) = ScopeTest::new(&test_env) + .test(req_gen, create_project) + .await + .unwrap(); + let project_id = success["id"].as_str().unwrap(); + let project_id = ProjectId(parse_base62(project_id).unwrap()); - // Add version to project - let create_version = Scopes::VERSION_CREATE; - let req_gen = |pat: Option| async move { - api.add_public_version( - project_id, - "1.2.3.4", - TestFile::BasicModDifferent, - None, - None, - pat.as_deref(), - ) - .await - }; - ScopeTest::new(&test_env) - .test(req_gen, create_version) - .await - .unwrap(); - }) + // Add version to project + let create_version = Scopes::VERSION_CREATE; + let req_gen = |pat: Option| async move { + api.add_public_version( + project_id, + "1.2.3.4", + TestFile::BasicModDifferent, + None, + None, + pat.as_deref(), + ) + .await + }; + ScopeTest::new(&test_env) + .test(req_gen, create_version) + .await + .unwrap(); + }, + ) .await; } #[actix_rt::test] pub async fn project_version_reads_scopes() { - with_test_environment(None, |_test_env: TestEnvironment| async move { - // let api = &test_env.api; - // let beta_file_hash = &test_env.dummy.project_beta.file_hash; + with_test_environment( + None, + |_test_env: TestEnvironment| async move { + // let api = &test_env.api; + // let beta_file_hash = &test_env.dummy.project_beta.file_hash; - // let read_version = Scopes::VERSION_READ; + // let read_version = Scopes::VERSION_READ; - // Update individual version file - // TODO: This scope currently fails still as the 'version' field of QueryProject only allows public versions. - // TODO: This will be fixed when the 'extracts_versions' PR is merged. - // let req_gen = |pat : Option| async move { - // api.update_individual_files("sha1", vec![ - // FileUpdateData { - // hash: beta_file_hash.clone(), - // loaders: None, - // game_versions: None, - // version_types: None - // } - // ], pat.as_deref()) - // .await - // }; - // let (failure, success) = ScopeTest::new(&test_env).with_failure_code(200).test(req_gen, read_version).await.unwrap(); - // assert!(!failure.as_object().unwrap().contains_key(beta_file_hash)); - // assert!(success.as_object().unwrap().contains_key(beta_file_hash)); - }) + // Update individual version file + // TODO: This scope currently fails still as the 'version' field of QueryProject only allows public versions. + // TODO: This will be fixed when the 'extracts_versions' PR is merged. + // let req_gen = |pat : Option| async move { + // api.update_individual_files("sha1", vec![ + // FileUpdateData { + // hash: beta_file_hash.clone(), + // loaders: None, + // game_versions: None, + // version_types: None + // } + // ], pat.as_deref()) + // .await + // }; + // let (failure, success) = ScopeTest::new(&test_env).with_failure_code(200).test(req_gen, read_version).await.unwrap(); + // assert!(!failure.as_object().unwrap().contains_key(beta_file_hash)); + // assert!(success.as_object().unwrap().contains_key(beta_file_hash)); + }, + ) .await; } diff --git a/apps/labrinth/tests/v2/tags.rs b/apps/labrinth/tests/v2/tags.rs index 25663db6b..1171e6502 100644 --- a/apps/labrinth/tests/v2/tags.rs +++ b/apps/labrinth/tests/v2/tags.rs @@ -10,98 +10,107 @@ use crate::common::{ #[actix_rt::test] async fn get_tags() { - with_test_environment(None, |test_env: TestEnvironment| async move { - let api = &test_env.api; - let game_versions = api.get_game_versions_deserialized().await; - let loaders = api.get_loaders_deserialized().await; - let side_types = api.get_side_types_deserialized().await; + with_test_environment( + None, + |test_env: TestEnvironment| async move { + let api = &test_env.api; + let game_versions = api.get_game_versions_deserialized().await; + let loaders = api.get_loaders_deserialized().await; + let side_types = api.get_side_types_deserialized().await; - // These tests match dummy data and will need to be updated if the dummy data changes - // Versions should be ordered by: - // - ordering - // - ordering ties settled by date added to database - // - We also expect presentation of NEWEST to OLDEST - // - All null orderings are treated as older than any non-null ordering - // (for this test, the 1.20.1, etc, versions are all null ordering) - let game_version_versions = game_versions - .into_iter() - .map(|x| x.version) - .collect::>(); - assert_eq!( - game_version_versions, - [ - "Ordering_Negative1", - "Ordering_Positive100", - "1.20.5", - "1.20.4", - "1.20.3", - "1.20.2", - "1.20.1" - ] - .iter() - .map(|s| s.to_string()) - .collect_vec() - ); - - let loader_names = loaders.into_iter().map(|x| x.name).collect::>(); - assert_eq!( - loader_names, - ["fabric", "forge", "bukkit", "waterfall"] + // These tests match dummy data and will need to be updated if the dummy data changes + // Versions should be ordered by: + // - ordering + // - ordering ties settled by date added to database + // - We also expect presentation of NEWEST to OLDEST + // - All null orderings are treated as older than any non-null ordering + // (for this test, the 1.20.1, etc, versions are all null ordering) + let game_version_versions = game_versions + .into_iter() + .map(|x| x.version) + .collect::>(); + assert_eq!( + game_version_versions, + [ + "Ordering_Negative1", + "Ordering_Positive100", + "1.20.5", + "1.20.4", + "1.20.3", + "1.20.2", + "1.20.1" + ] .iter() .map(|s| s.to_string()) - .collect() - ); + .collect_vec() + ); - let side_type_names = side_types.into_iter().collect::>(); - assert_eq!( - side_type_names, - ["unknown", "required", "optional", "unsupported"] - .iter() - .map(|s| s.to_string()) - .collect() - ); - }) + let loader_names = + loaders.into_iter().map(|x| x.name).collect::>(); + assert_eq!( + loader_names, + ["fabric", "forge", "bukkit", "waterfall"] + .iter() + .map(|s| s.to_string()) + .collect() + ); + + let side_type_names = + side_types.into_iter().collect::>(); + assert_eq!( + side_type_names, + ["unknown", "required", "optional", "unsupported"] + .iter() + .map(|s| s.to_string()) + .collect() + ); + }, + ) .await; } #[actix_rt::test] async fn get_donation_platforms() { - with_test_environment(None, |test_env: TestEnvironment| async move { - let api = &test_env.api; - let mut donation_platforms_unsorted = api.get_donation_platforms_deserialized().await; + with_test_environment( + None, + |test_env: TestEnvironment| async move { + let api = &test_env.api; + let mut donation_platforms_unsorted = + api.get_donation_platforms_deserialized().await; - // These tests match dummy data and will need to be updated if the dummy data changes - let mut included = vec![ - DonationPlatformQueryData { - short: "patreon".to_string(), - name: "Patreon".to_string(), - }, - DonationPlatformQueryData { - short: "ko-fi".to_string(), - name: "Ko-fi".to_string(), - }, - DonationPlatformQueryData { - short: "paypal".to_string(), - name: "PayPal".to_string(), - }, - DonationPlatformQueryData { - short: "bmac".to_string(), - name: "Buy Me A Coffee".to_string(), - }, - DonationPlatformQueryData { - short: "github".to_string(), - name: "GitHub Sponsors".to_string(), - }, - DonationPlatformQueryData { - short: "other".to_string(), - name: "Other".to_string(), - }, - ]; + // These tests match dummy data and will need to be updated if the dummy data changes + let mut included = vec![ + DonationPlatformQueryData { + short: "patreon".to_string(), + name: "Patreon".to_string(), + }, + DonationPlatformQueryData { + short: "ko-fi".to_string(), + name: "Ko-fi".to_string(), + }, + DonationPlatformQueryData { + short: "paypal".to_string(), + name: "PayPal".to_string(), + }, + DonationPlatformQueryData { + short: "bmac".to_string(), + name: "Buy Me A Coffee".to_string(), + }, + DonationPlatformQueryData { + short: "github".to_string(), + name: "GitHub Sponsors".to_string(), + }, + DonationPlatformQueryData { + short: "other".to_string(), + name: "Other".to_string(), + }, + ]; - included.sort_by(|a, b| a.short.cmp(&b.short)); - donation_platforms_unsorted.sort_by(|a, b| a.short.cmp(&b.short)); + included.sort_by(|a, b| a.short.cmp(&b.short)); + donation_platforms_unsorted.sort_by(|a, b| a.short.cmp(&b.short)); - assert_eq!(donation_platforms_unsorted, included); - }) + assert_eq!(donation_platforms_unsorted, included); + }, + ) .await; } diff --git a/apps/labrinth/tests/v2/teams.rs b/apps/labrinth/tests/v2/teams.rs index 347e9730d..545b821de 100644 --- a/apps/labrinth/tests/v2/teams.rs +++ b/apps/labrinth/tests/v2/teams.rs @@ -8,8 +8,8 @@ use crate::{ api_common::ApiTeams, api_v2::ApiV2, database::{ - FRIEND_USER_ID, FRIEND_USER_ID_PARSED, FRIEND_USER_PAT, USER_USER_ID_PARSED, - USER_USER_PAT, + FRIEND_USER_ID, FRIEND_USER_ID_PARSED, FRIEND_USER_PAT, + USER_USER_ID_PARSED, USER_USER_PAT, }, environment::{with_test_environment, TestEnvironment}, }, @@ -19,92 +19,120 @@ use crate::{ #[actix_rt::test] async fn transfer_ownership_v2() { // Test setup and dummy data - with_test_environment(None, |test_env: TestEnvironment| async move { - let api = &test_env.api; + with_test_environment( + None, + |test_env: TestEnvironment| async move { + let api = &test_env.api; - let alpha_team_id = &test_env.dummy.project_alpha.team_id; + let alpha_team_id = &test_env.dummy.project_alpha.team_id; - // Cannot set friend as owner (not a member) - let resp = api - .transfer_team_ownership(alpha_team_id, FRIEND_USER_ID, USER_USER_PAT) - .await; - assert_status!(&resp, StatusCode::BAD_REQUEST); + // Cannot set friend as owner (not a member) + let resp = api + .transfer_team_ownership( + alpha_team_id, + FRIEND_USER_ID, + USER_USER_PAT, + ) + .await; + assert_status!(&resp, StatusCode::BAD_REQUEST); - // first, invite friend - let resp = api - .add_user_to_team(alpha_team_id, FRIEND_USER_ID, None, None, USER_USER_PAT) - .await; - assert_status!(&resp, StatusCode::NO_CONTENT); + // first, invite friend + let resp = api + .add_user_to_team( + alpha_team_id, + FRIEND_USER_ID, + None, + None, + USER_USER_PAT, + ) + .await; + assert_status!(&resp, StatusCode::NO_CONTENT); - // still cannot set friend as owner (not accepted) - let resp = api - .transfer_team_ownership(alpha_team_id, FRIEND_USER_ID, USER_USER_PAT) - .await; - assert_status!(&resp, StatusCode::BAD_REQUEST); + // still cannot set friend as owner (not accepted) + let resp = api + .transfer_team_ownership( + alpha_team_id, + FRIEND_USER_ID, + USER_USER_PAT, + ) + .await; + assert_status!(&resp, StatusCode::BAD_REQUEST); - // accept - let resp = api.join_team(alpha_team_id, FRIEND_USER_PAT).await; - assert_status!(&resp, StatusCode::NO_CONTENT); + // accept + let resp = api.join_team(alpha_team_id, FRIEND_USER_PAT).await; + assert_status!(&resp, StatusCode::NO_CONTENT); - // Cannot set ourselves as owner if we are not owner - let resp = api - .transfer_team_ownership(alpha_team_id, FRIEND_USER_ID, FRIEND_USER_PAT) - .await; - assert_status!(&resp, StatusCode::UNAUTHORIZED); + // Cannot set ourselves as owner if we are not owner + let resp = api + .transfer_team_ownership( + alpha_team_id, + FRIEND_USER_ID, + FRIEND_USER_PAT, + ) + .await; + assert_status!(&resp, StatusCode::UNAUTHORIZED); - // Can set friend as owner - let resp = api - .transfer_team_ownership(alpha_team_id, FRIEND_USER_ID, USER_USER_PAT) - .await; - assert_status!(&resp, StatusCode::NO_CONTENT); + // Can set friend as owner + let resp = api + .transfer_team_ownership( + alpha_team_id, + FRIEND_USER_ID, + USER_USER_PAT, + ) + .await; + assert_status!(&resp, StatusCode::NO_CONTENT); - // Check - let members = api - .get_team_members_deserialized(alpha_team_id, USER_USER_PAT) - .await; - let friend_member = members - .iter() - .find(|x| x.user.id.0 == FRIEND_USER_ID_PARSED as u64) - .unwrap(); - assert_eq!(friend_member.role, "Owner"); - assert_eq!( - friend_member.permissions.unwrap(), - ProjectPermissions::all() - ); + // Check + let members = api + .get_team_members_deserialized(alpha_team_id, USER_USER_PAT) + .await; + let friend_member = members + .iter() + .find(|x| x.user.id.0 == FRIEND_USER_ID_PARSED as u64) + .unwrap(); + assert_eq!(friend_member.role, "Owner"); + assert_eq!( + friend_member.permissions.unwrap(), + ProjectPermissions::all() + ); - let user_member = members - .iter() - .find(|x| x.user.id.0 == USER_USER_ID_PARSED as u64) - .unwrap(); - assert_eq!(user_member.role, "Member"); - assert_eq!(user_member.permissions.unwrap(), ProjectPermissions::all()); + let user_member = members + .iter() + .find(|x| x.user.id.0 == USER_USER_ID_PARSED as u64) + .unwrap(); + assert_eq!(user_member.role, "Member"); + assert_eq!( + user_member.permissions.unwrap(), + ProjectPermissions::all() + ); - // Confirm that user, a user who still has full permissions, cannot then remove the owner - let resp = api - .remove_from_team(alpha_team_id, FRIEND_USER_ID, USER_USER_PAT) - .await; - assert_status!(&resp, StatusCode::UNAUTHORIZED); + // Confirm that user, a user who still has full permissions, cannot then remove the owner + let resp = api + .remove_from_team(alpha_team_id, FRIEND_USER_ID, USER_USER_PAT) + .await; + assert_status!(&resp, StatusCode::UNAUTHORIZED); - // V2 only- confirm the owner changing the role to member does nothing - let resp = api - .edit_team_member( - alpha_team_id, - FRIEND_USER_ID, - json!({ - "role": "Member" - }), - FRIEND_USER_PAT, - ) - .await; - assert_status!(&resp, StatusCode::NO_CONTENT); - let members = api - .get_team_members_deserialized(alpha_team_id, USER_USER_PAT) - .await; - let friend_member = members - .iter() - .find(|x| x.user.id.0 == FRIEND_USER_ID_PARSED as u64) - .unwrap(); - assert_eq!(friend_member.role, "Owner"); - }) + // V2 only- confirm the owner changing the role to member does nothing + let resp = api + .edit_team_member( + alpha_team_id, + FRIEND_USER_ID, + json!({ + "role": "Member" + }), + FRIEND_USER_PAT, + ) + .await; + assert_status!(&resp, StatusCode::NO_CONTENT); + let members = api + .get_team_members_deserialized(alpha_team_id, USER_USER_PAT) + .await; + let friend_member = members + .iter() + .find(|x| x.user.id.0 == FRIEND_USER_ID_PARSED as u64) + .unwrap(); + assert_eq!(friend_member.role, "Owner"); + }, + ) .await; } diff --git a/apps/labrinth/tests/v2/version.rs b/apps/labrinth/tests/v2/version.rs index c4eceea1c..b4195bef6 100644 --- a/apps/labrinth/tests/v2/version.rs +++ b/apps/labrinth/tests/v2/version.rs @@ -22,452 +22,499 @@ use crate::common::{ #[actix_rt::test] pub async fn test_patch_version() { - with_test_environment(None, |test_env: TestEnvironment| async move { - let api = &test_env.api; - - let alpha_version_id = &test_env.dummy.project_alpha.version_id; - - // // First, we do some patch requests that should fail. - // // Failure because the user is not authorized. - let resp = api - .edit_version( - alpha_version_id, - json!({ - "name": "test 1", - }), - ENEMY_USER_PAT, - ) - .await; - assert_status!(&resp, StatusCode::UNAUTHORIZED); - - // Failure because these are illegal requested statuses for a normal user. - for req in ["unknown", "scheduled"] { + with_test_environment( + None, + |test_env: TestEnvironment| async move { + let api = &test_env.api; + + let alpha_version_id = &test_env.dummy.project_alpha.version_id; + + // // First, we do some patch requests that should fail. + // // Failure because the user is not authorized. + let resp = api + .edit_version( + alpha_version_id, + json!({ + "name": "test 1", + }), + ENEMY_USER_PAT, + ) + .await; + assert_status!(&resp, StatusCode::UNAUTHORIZED); + + // Failure because these are illegal requested statuses for a normal user. + for req in ["unknown", "scheduled"] { + let resp = api + .edit_version( + alpha_version_id, + json!({ + "status": req, + // requested status it not set here, but in /schedule + }), + USER_USER_PAT, + ) + .await; + assert_status!(&resp, StatusCode::BAD_REQUEST); + } + + // Sucessful request to patch many fields. + let resp = api + .edit_version( + alpha_version_id, + json!({ + "name": "new version name", + "version_number": "1.3.0", + "changelog": "new changelog", + "version_type": "beta", + // // "dependencies": [], TODO: test this + "game_versions": ["1.20.5"], + "loaders": ["forge"], + "featured": false, + // "primary_file": [], TODO: test this + // // "downloads": 0, TODO: moderator exclusive + "status": "draft", + // // "filetypes": ["jar"], TODO: test this + }), + USER_USER_PAT, + ) + .await; + assert_status!(&resp, StatusCode::NO_CONTENT); + + let version = api + .get_version_deserialized(alpha_version_id, USER_USER_PAT) + .await; + assert_eq!(version.name, "new version name"); + assert_eq!(version.version_number, "1.3.0"); + assert_eq!(version.changelog, "new changelog"); + assert_eq!( + version.version_type, + serde_json::from_str::("\"beta\"").unwrap() + ); + assert_eq!(version.game_versions, vec!["1.20.5"]); + assert_eq!(version.loaders, vec![Loader("forge".to_string())]); + assert!(!version.featured); + assert_eq!(version.status, VersionStatus::from_string("draft")); + + // These ones are checking the v2-v3 rerouting, we eneusre that only 'game_versions' + // works as expected, as well as only 'loaders' let resp = api .edit_version( alpha_version_id, json!({ - "status": req, - // requested status it not set here, but in /schedule + "game_versions": ["1.20.1", "1.20.2", "1.20.4"], }), USER_USER_PAT, ) .await; - assert_status!(&resp, StatusCode::BAD_REQUEST); - } - - // Sucessful request to patch many fields. - let resp = api - .edit_version( - alpha_version_id, - json!({ - "name": "new version name", - "version_number": "1.3.0", - "changelog": "new changelog", - "version_type": "beta", - // // "dependencies": [], TODO: test this - "game_versions": ["1.20.5"], - "loaders": ["forge"], - "featured": false, - // "primary_file": [], TODO: test this - // // "downloads": 0, TODO: moderator exclusive - "status": "draft", - // // "filetypes": ["jar"], TODO: test this - }), - USER_USER_PAT, - ) - .await; - assert_status!(&resp, StatusCode::NO_CONTENT); - - let version = api - .get_version_deserialized(alpha_version_id, USER_USER_PAT) - .await; - assert_eq!(version.name, "new version name"); - assert_eq!(version.version_number, "1.3.0"); - assert_eq!(version.changelog, "new changelog"); - assert_eq!( - version.version_type, - serde_json::from_str::("\"beta\"").unwrap() - ); - assert_eq!(version.game_versions, vec!["1.20.5"]); - assert_eq!(version.loaders, vec![Loader("forge".to_string())]); - assert!(!version.featured); - assert_eq!(version.status, VersionStatus::from_string("draft")); - - // These ones are checking the v2-v3 rerouting, we eneusre that only 'game_versions' - // works as expected, as well as only 'loaders' - let resp = api - .edit_version( - alpha_version_id, - json!({ - "game_versions": ["1.20.1", "1.20.2", "1.20.4"], - }), - USER_USER_PAT, - ) - .await; - assert_status!(&resp, StatusCode::NO_CONTENT); - - let version = api - .get_version_deserialized(alpha_version_id, USER_USER_PAT) - .await; - assert_eq!(version.game_versions, vec!["1.20.1", "1.20.2", "1.20.4"]); - assert_eq!(version.loaders, vec![Loader("forge".to_string())]); // From last patch - - let resp = api - .edit_version( - alpha_version_id, - json!({ - "loaders": ["fabric"], - }), - USER_USER_PAT, - ) - .await; - assert_status!(&resp, StatusCode::NO_CONTENT); - - let version = api - .get_version_deserialized(alpha_version_id, USER_USER_PAT) - .await; - assert_eq!(version.game_versions, vec!["1.20.1", "1.20.2", "1.20.4"]); // From last patch - assert_eq!(version.loaders, vec![Loader("fabric".to_string())]); - }) + assert_status!(&resp, StatusCode::NO_CONTENT); + + let version = api + .get_version_deserialized(alpha_version_id, USER_USER_PAT) + .await; + assert_eq!( + version.game_versions, + vec!["1.20.1", "1.20.2", "1.20.4"] + ); + assert_eq!(version.loaders, vec![Loader("forge".to_string())]); // From last patch + + let resp = api + .edit_version( + alpha_version_id, + json!({ + "loaders": ["fabric"], + }), + USER_USER_PAT, + ) + .await; + assert_status!(&resp, StatusCode::NO_CONTENT); + + let version = api + .get_version_deserialized(alpha_version_id, USER_USER_PAT) + .await; + assert_eq!( + version.game_versions, + vec!["1.20.1", "1.20.2", "1.20.4"] + ); // From last patch + assert_eq!(version.loaders, vec![Loader("fabric".to_string())]); + }, + ) .await; } #[actix_rt::test] async fn version_updates() { // Test setup and dummy data - with_test_environment(None, |test_env: TestEnvironment| async move { - let api = &test_env.api; - let DummyProjectAlpha { - project_id: alpha_project_id, - project_id_parsed: alpha_project_id_parsed, - version_id: alpha_version_id, - file_hash: alpha_version_hash, - .. - } = &test_env.dummy.project_alpha; - let DummyProjectBeta { - version_id: beta_version_id, - file_hash: beta_version_hash, - .. - } = &test_env.dummy.project_beta; - - // Quick test, using get version from hash - let version = api - .get_version_from_hash_deserialized(alpha_version_hash, "sha1", USER_USER_PAT) - .await; - assert_eq!(&version.id.to_string(), alpha_version_id); - - // Get versions from hash - let versions = api - .get_versions_from_hashes_deserialized( - &[alpha_version_hash.as_str(), beta_version_hash.as_str()], - "sha1", - USER_USER_PAT, - ) - .await; - assert_eq!(versions.len(), 2); - assert_eq!( - &versions[alpha_version_hash].id.to_string(), - alpha_version_id - ); - assert_eq!(&versions[beta_version_hash].id.to_string(), beta_version_id); - - // When there is only the one version, there should be no updates - let version = api - .get_update_from_hash_deserialized_common( - alpha_version_hash, - "sha1", - None, - None, - None, - USER_USER_PAT, - ) - .await; - assert_eq!(&version.id.to_string(), alpha_version_id); - - let versions = api - .update_files_deserialized_common( - "sha1", - vec![alpha_version_hash.to_string()], - None, - None, - None, - USER_USER_PAT, - ) - .await; - assert_eq!(versions.len(), 1); - assert_eq!( - &versions[alpha_version_hash].id.to_string(), - alpha_version_id - ); - - // Add 3 new versions, 1 before, and 2 after, with differing game_version/version_types/loaders - let mut update_ids = vec![]; - for (version_number, patch_value) in [ - ( - "0.9.9", - json!({ - "game_versions": ["1.20.1"], - }), - ), - ( - "1.5.0", - json!({ - "game_versions": ["1.20.3"], - "loaders": ["fabric"], - }), - ), - ( - "1.5.1", - json!({ - "game_versions": ["1.20.4"], - "loaders": ["forge"], - "version_type": "beta" - }), - ), - ] - .iter() - { + with_test_environment( + None, + |test_env: TestEnvironment| async move { + let api = &test_env.api; + let DummyProjectAlpha { + project_id: alpha_project_id, + project_id_parsed: alpha_project_id_parsed, + version_id: alpha_version_id, + file_hash: alpha_version_hash, + .. + } = &test_env.dummy.project_alpha; + let DummyProjectBeta { + version_id: beta_version_id, + file_hash: beta_version_hash, + .. + } = &test_env.dummy.project_beta; + + // Quick test, using get version from hash let version = api - .add_public_version_deserialized_common( - *alpha_project_id_parsed, - version_number, - TestFile::build_random_jar(), - None, - None, + .get_version_from_hash_deserialized( + alpha_version_hash, + "sha1", USER_USER_PAT, ) .await; - update_ids.push(version.id); + assert_eq!(&version.id.to_string(), alpha_version_id); - // Patch using json - api.edit_version(&version.id.to_string(), patch_value.clone(), USER_USER_PAT) + // Get versions from hash + let versions = api + .get_versions_from_hashes_deserialized( + &[alpha_version_hash.as_str(), beta_version_hash.as_str()], + "sha1", + USER_USER_PAT, + ) .await; - } - - let check_expected = |game_versions: Option>, - loaders: Option>, - version_types: Option>, - result_id: Option| async move { - let (success, result_id) = match result_id { - Some(id) => (true, id), - None => (false, VersionId(0)), - }; - // get_update_from_hash - let resp = api - .get_update_from_hash( + assert_eq!(versions.len(), 2); + assert_eq!( + &versions[alpha_version_hash].id.to_string(), + alpha_version_id + ); + assert_eq!( + &versions[beta_version_hash].id.to_string(), + beta_version_id + ); + + // When there is only the one version, there should be no updates + let version = api + .get_update_from_hash_deserialized_common( alpha_version_hash, "sha1", - loaders.clone(), - game_versions.clone(), - version_types.clone(), + None, + None, + None, USER_USER_PAT, ) .await; - if success { - assert_status!(&resp, StatusCode::OK); - let body: serde_json::Value = test::read_body_json(resp).await; - let id = body["id"].as_str().unwrap(); - assert_eq!(id, &result_id.to_string()); - } else { - assert_status!(&resp, StatusCode::NOT_FOUND); - } + assert_eq!(&version.id.to_string(), alpha_version_id); - // update_files let versions = api .update_files_deserialized_common( "sha1", vec![alpha_version_hash.to_string()], - loaders.clone(), - game_versions.clone(), - version_types.clone(), + None, + None, + None, + USER_USER_PAT, + ) + .await; + assert_eq!(versions.len(), 1); + assert_eq!( + &versions[alpha_version_hash].id.to_string(), + alpha_version_id + ); + + // Add 3 new versions, 1 before, and 2 after, with differing game_version/version_types/loaders + let mut update_ids = vec![]; + for (version_number, patch_value) in [ + ( + "0.9.9", + json!({ + "game_versions": ["1.20.1"], + }), + ), + ( + "1.5.0", + json!({ + "game_versions": ["1.20.3"], + "loaders": ["fabric"], + }), + ), + ( + "1.5.1", + json!({ + "game_versions": ["1.20.4"], + "loaders": ["forge"], + "version_type": "beta" + }), + ), + ] + .iter() + { + let version = api + .add_public_version_deserialized_common( + *alpha_project_id_parsed, + version_number, + TestFile::build_random_jar(), + None, + None, + USER_USER_PAT, + ) + .await; + update_ids.push(version.id); + + // Patch using json + api.edit_version( + &version.id.to_string(), + patch_value.clone(), USER_USER_PAT, ) .await; - if success { - assert_eq!(versions.len(), 1); - let first = versions.iter().next().unwrap(); - assert_eq!(first.1.id, result_id); - } else { - assert_eq!(versions.len(), 0); } - // update_individual_files - let hashes = vec![FileUpdateData { - hash: alpha_version_hash.to_string(), - loaders, - game_versions, - version_types: version_types.map(|v| { - v.into_iter() - .map(|v| serde_json::from_str(&format!("\"{v}\"")).unwrap()) - .collect() - }), - }]; + let check_expected = + |game_versions: Option>, + loaders: Option>, + version_types: Option>, + result_id: Option| async move { + let (success, result_id) = match result_id { + Some(id) => (true, id), + None => (false, VersionId(0)), + }; + // get_update_from_hash + let resp = api + .get_update_from_hash( + alpha_version_hash, + "sha1", + loaders.clone(), + game_versions.clone(), + version_types.clone(), + USER_USER_PAT, + ) + .await; + if success { + assert_status!(&resp, StatusCode::OK); + let body: serde_json::Value = + test::read_body_json(resp).await; + let id = body["id"].as_str().unwrap(); + assert_eq!(id, &result_id.to_string()); + } else { + assert_status!(&resp, StatusCode::NOT_FOUND); + } + + // update_files + let versions = api + .update_files_deserialized_common( + "sha1", + vec![alpha_version_hash.to_string()], + loaders.clone(), + game_versions.clone(), + version_types.clone(), + USER_USER_PAT, + ) + .await; + if success { + assert_eq!(versions.len(), 1); + let first = versions.iter().next().unwrap(); + assert_eq!(first.1.id, result_id); + } else { + assert_eq!(versions.len(), 0); + } + + // update_individual_files + let hashes = vec![FileUpdateData { + hash: alpha_version_hash.to_string(), + loaders, + game_versions, + version_types: version_types.map(|v| { + v.into_iter() + .map(|v| { + serde_json::from_str(&format!("\"{v}\"")) + .unwrap() + }) + .collect() + }), + }]; + let versions = api + .update_individual_files_deserialized( + "sha1", + hashes, + USER_USER_PAT, + ) + .await; + if success { + assert_eq!(versions.len(), 1); + let first = versions.iter().next().unwrap(); + assert_eq!(first.1.id, result_id); + } else { + assert_eq!(versions.len(), 0); + } + }; + + let tests = vec![ + check_expected( + Some(vec!["1.20.1".to_string()]), + None, + None, + Some(update_ids[0]), + ), + check_expected( + Some(vec!["1.20.3".to_string()]), + None, + None, + Some(update_ids[1]), + ), + check_expected( + Some(vec!["1.20.4".to_string()]), + None, + None, + Some(update_ids[2]), + ), + // Loader restrictions + check_expected( + None, + Some(vec!["fabric".to_string()]), + None, + Some(update_ids[1]), + ), + check_expected( + None, + Some(vec!["forge".to_string()]), + None, + Some(update_ids[2]), + ), + // Version type restrictions + check_expected( + None, + None, + Some(vec!["release".to_string()]), + Some(update_ids[1]), + ), + check_expected( + None, + None, + Some(vec!["beta".to_string()]), + Some(update_ids[2]), + ), + // Specific combination + check_expected( + None, + Some(vec!["fabric".to_string()]), + Some(vec!["release".to_string()]), + Some(update_ids[1]), + ), + // Impossible combination + check_expected( + None, + Some(vec!["fabric".to_string()]), + Some(vec!["beta".to_string()]), + None, + ), + // No restrictions, should do the last one + check_expected(None, None, None, Some(update_ids[2])), + ]; + + // Wait on all tests, 4 at a time + futures::stream::iter(tests) + .buffer_unordered(4) + .collect::>() + .await; + + // We do a couple small tests for get_project_versions_deserialized as well + // TODO: expand this more. let versions = api - .update_individual_files_deserialized("sha1", hashes, USER_USER_PAT) + .get_project_versions_deserialized_common( + alpha_project_id, + None, + None, + None, + None, + None, + None, + USER_USER_PAT, + ) .await; - if success { - assert_eq!(versions.len(), 1); - let first = versions.iter().next().unwrap(); - assert_eq!(first.1.id, result_id); - } else { - assert_eq!(versions.len(), 0); - } - }; - - let tests = vec![ - check_expected( - Some(vec!["1.20.1".to_string()]), - None, - None, - Some(update_ids[0]), - ), - check_expected( - Some(vec!["1.20.3".to_string()]), - None, - None, - Some(update_ids[1]), - ), - check_expected( - Some(vec!["1.20.4".to_string()]), - None, - None, - Some(update_ids[2]), - ), - // Loader restrictions - check_expected( - None, - Some(vec!["fabric".to_string()]), - None, - Some(update_ids[1]), - ), - check_expected( - None, - Some(vec!["forge".to_string()]), - None, - Some(update_ids[2]), - ), - // Version type restrictions - check_expected( - None, - None, - Some(vec!["release".to_string()]), - Some(update_ids[1]), - ), - check_expected( - None, - None, - Some(vec!["beta".to_string()]), - Some(update_ids[2]), - ), - // Specific combination - check_expected( - None, - Some(vec!["fabric".to_string()]), - Some(vec!["release".to_string()]), - Some(update_ids[1]), - ), - // Impossible combination - check_expected( - None, - Some(vec!["fabric".to_string()]), - Some(vec!["beta".to_string()]), - None, - ), - // No restrictions, should do the last one - check_expected(None, None, None, Some(update_ids[2])), - ]; - - // Wait on all tests, 4 at a time - futures::stream::iter(tests) - .buffer_unordered(4) - .collect::>() - .await; - - // We do a couple small tests for get_project_versions_deserialized as well - // TODO: expand this more. - let versions = api - .get_project_versions_deserialized_common( - alpha_project_id, - None, - None, - None, - None, - None, - None, - USER_USER_PAT, - ) - .await; - assert_eq!(versions.len(), 4); - let versions = api - .get_project_versions_deserialized_common( - alpha_project_id, - None, - Some(vec!["forge".to_string()]), - None, - None, - None, - None, - USER_USER_PAT, - ) - .await; - assert_eq!(versions.len(), 1); - }) + assert_eq!(versions.len(), 4); + let versions = api + .get_project_versions_deserialized_common( + alpha_project_id, + None, + Some(vec!["forge".to_string()]), + None, + None, + None, + None, + USER_USER_PAT, + ) + .await; + assert_eq!(versions.len(), 1); + }, + ) .await; } #[actix_rt::test] async fn add_version_project_types_v2() { - with_test_environment(None, |test_env: TestEnvironment| async move { - // Since v2 no longer keeps project_type at the project level but the version level, - // we have to test that the project_type is set correctly when adding a version, if its done in separate requests. - let api = &test_env.api; - - // Create a project in v2 with project_type = modpack, and no initial version set. - let (test_project, test_versions) = api - .add_public_project("test-modpack", None, None, USER_USER_PAT) - .await; - assert_eq!(test_versions.len(), 0); // No initial version set - - // Get as v2 project - let test_project = api - .get_project_deserialized(&test_project.slug.unwrap(), USER_USER_PAT) - .await; - assert_eq!(test_project.project_type, "project"); // No project_type set, as no versions are set - // Default to 'project' if none are found - // This is a known difference between older v2 ,but is acceptable. - // This would be the appropriate test on older v2: - // assert_eq!(test_project.project_type, "modpack"); - - // Create a version with a modpack file attached - let test_version = api - .add_public_version_deserialized_common( - test_project.id, - "1.0.0", - TestFile::build_random_mrpack(), - None, - None, - USER_USER_PAT, - ) - .await; - - // When we get the version as v2, it should display 'fabric' as the loader (and no project_type) - let test_version = api - .get_version_deserialized(&test_version.id.to_string(), USER_USER_PAT) - .await; - assert_eq!(test_version.loaders, vec![Loader("fabric".to_string())]); - - // When we get the project as v2, it should display 'modpack' as the project_type, and 'fabric' as the loader - let test_project = api - .get_project_deserialized(&test_project.slug.unwrap(), USER_USER_PAT) - .await; - assert_eq!(test_project.project_type, "modpack"); - assert_eq!(test_project.loaders, vec!["fabric"]); - - // When we get the version as v3, it should display 'mrpack' as the loader, and 'modpack' as the project_type - // When we get the project as v3, it should display 'modpack' as the project_type, and 'mrpack' as the loader - - // The project should be a modpack project - }) + with_test_environment( + None, + |test_env: TestEnvironment| async move { + // Since v2 no longer keeps project_type at the project level but the version level, + // we have to test that the project_type is set correctly when adding a version, if its done in separate requests. + let api = &test_env.api; + + // Create a project in v2 with project_type = modpack, and no initial version set. + let (test_project, test_versions) = api + .add_public_project("test-modpack", None, None, USER_USER_PAT) + .await; + assert_eq!(test_versions.len(), 0); // No initial version set + + // Get as v2 project + let test_project = api + .get_project_deserialized( + &test_project.slug.unwrap(), + USER_USER_PAT, + ) + .await; + assert_eq!(test_project.project_type, "project"); // No project_type set, as no versions are set + // Default to 'project' if none are found + // This is a known difference between older v2 ,but is acceptable. + // This would be the appropriate test on older v2: + // assert_eq!(test_project.project_type, "modpack"); + + // Create a version with a modpack file attached + let test_version = api + .add_public_version_deserialized_common( + test_project.id, + "1.0.0", + TestFile::build_random_mrpack(), + None, + None, + USER_USER_PAT, + ) + .await; + + // When we get the version as v2, it should display 'fabric' as the loader (and no project_type) + let test_version = api + .get_version_deserialized( + &test_version.id.to_string(), + USER_USER_PAT, + ) + .await; + assert_eq!( + test_version.loaders, + vec![Loader("fabric".to_string())] + ); + + // When we get the project as v2, it should display 'modpack' as the project_type, and 'fabric' as the loader + let test_project = api + .get_project_deserialized( + &test_project.slug.unwrap(), + USER_USER_PAT, + ) + .await; + assert_eq!(test_project.project_type, "modpack"); + assert_eq!(test_project.loaders, vec!["fabric"]); + + // When we get the version as v3, it should display 'mrpack' as the loader, and 'modpack' as the project_type + // When we get the project as v3, it should display 'modpack' as the project_type, and 'mrpack' as the loader + + // The project should be a modpack project + }, + ) .await; } @@ -475,43 +522,49 @@ async fn add_version_project_types_v2() { async fn test_incorrect_file_parts() { // Ensures that a version get that 'should' have mrpack_loaders does still display them // if the file is 'mrpack' but the file_parts are incorrect - with_test_environment(None, |test_env: TestEnvironment| async move { - let api = &test_env.api; - - // Patch to set the file_parts to something incorrect - let patch = json!([{ - "op": "add", - "path": "/file_parts", - "value": ["invalid.zip"] // one file, wrong non-mrpack extension - }]); - - // Create an empty project - let slug = "test-project"; - let creation_data = get_public_project_creation_data(slug, None, None); - let resp = api.create_project(creation_data, USER_USER_PAT).await; - assert_status!(&resp, StatusCode::OK); - - // Get the project - let project = api.get_project_deserialized(slug, USER_USER_PAT).await; - assert_eq!(project.project_type, "project"); - - // Create a version with a mrpack file, but incorrect file_parts - let resp = api - .add_public_version( - project.id, - "1.0.0", - TestFile::build_random_mrpack(), - None, - Some(serde_json::from_value(patch).unwrap()), - USER_USER_PAT, - ) - .await; - assert_status!(&resp, StatusCode::OK); - - // Get the project now, which should be now correctly identified as a modpack - let project = api.get_project_deserialized(slug, USER_USER_PAT).await; - assert_eq!(project.project_type, "modpack"); - assert_eq!(project.loaders, vec!["fabric"]); - }) + with_test_environment( + None, + |test_env: TestEnvironment| async move { + let api = &test_env.api; + + // Patch to set the file_parts to something incorrect + let patch = json!([{ + "op": "add", + "path": "/file_parts", + "value": ["invalid.zip"] // one file, wrong non-mrpack extension + }]); + + // Create an empty project + let slug = "test-project"; + let creation_data = + get_public_project_creation_data(slug, None, None); + let resp = api.create_project(creation_data, USER_USER_PAT).await; + assert_status!(&resp, StatusCode::OK); + + // Get the project + let project = + api.get_project_deserialized(slug, USER_USER_PAT).await; + assert_eq!(project.project_type, "project"); + + // Create a version with a mrpack file, but incorrect file_parts + let resp = api + .add_public_version( + project.id, + "1.0.0", + TestFile::build_random_mrpack(), + None, + Some(serde_json::from_value(patch).unwrap()), + USER_USER_PAT, + ) + .await; + assert_status!(&resp, StatusCode::OK); + + // Get the project now, which should be now correctly identified as a modpack + let project = + api.get_project_deserialized(slug, USER_USER_PAT).await; + assert_eq!(project.project_type, "modpack"); + assert_eq!(project.loaders, vec!["fabric"]); + }, + ) .await; } diff --git a/apps/labrinth/tests/version.rs b/apps/labrinth/tests/version.rs index f482bc359..b085c435d 100644 --- a/apps/labrinth/tests/version.rs +++ b/apps/labrinth/tests/version.rs @@ -2,7 +2,9 @@ use std::collections::HashMap; use crate::common::api_common::ApiVersion; use crate::common::database::*; -use crate::common::dummy_data::{DummyProjectAlpha, DummyProjectBeta, TestFile}; +use crate::common::dummy_data::{ + DummyProjectAlpha, DummyProjectBeta, TestFile, +}; use crate::common::get_json_val_str; use actix_http::StatusCode; use actix_web::test; @@ -53,7 +55,8 @@ async fn test_get_version() { .await .unwrap() .unwrap(); - let cached_project: serde_json::Value = serde_json::from_str(&cached_project).unwrap(); + let cached_project: serde_json::Value = + serde_json::from_str(&cached_project).unwrap(); assert_eq!( cached_project["val"]["inner"]["project_id"], json!(parse_base62(alpha_project_id).unwrap()) @@ -124,7 +127,10 @@ async fn version_updates() { &versions[alpha_version_hash].id.to_string(), alpha_version_id ); - assert_eq!(&versions[beta_version_hash].id.to_string(), beta_version_id); + assert_eq!( + &versions[beta_version_hash].id.to_string(), + beta_version_id + ); // When there is only the one version, there should be no updates let version = api @@ -195,90 +201,103 @@ async fn version_updates() { update_ids.push(version.id); // Patch using json - api.edit_version(&version.id.to_string(), patch_value.clone(), USER_USER_PAT) - .await; + api.edit_version( + &version.id.to_string(), + patch_value.clone(), + USER_USER_PAT, + ) + .await; } - let check_expected = |game_versions: Option>, - loaders: Option>, - version_types: Option>, - result_id: Option| async move { - let (success, result_id) = match result_id { - Some(id) => (true, id), - None => (false, VersionId(0)), + let check_expected = + |game_versions: Option>, + loaders: Option>, + version_types: Option>, + result_id: Option| async move { + let (success, result_id) = match result_id { + Some(id) => (true, id), + None => (false, VersionId(0)), + }; + // get_update_from_hash + let resp = api + .get_update_from_hash( + alpha_version_hash, + "sha1", + loaders.clone(), + game_versions.clone(), + version_types.clone(), + USER_USER_PAT, + ) + .await; + if success { + assert_status!(&resp, StatusCode::OK); + let body: serde_json::Value = + test::read_body_json(resp).await; + let id = body["id"].as_str().unwrap(); + assert_eq!(id, &result_id.to_string()); + } else { + assert_status!(&resp, StatusCode::NOT_FOUND); + } + + // update_files + let versions = api + .update_files_deserialized_common( + "sha1", + vec![alpha_version_hash.to_string()], + loaders.clone(), + game_versions.clone(), + version_types.clone(), + USER_USER_PAT, + ) + .await; + if success { + assert_eq!(versions.len(), 1); + let first = versions.iter().next().unwrap(); + assert_eq!(first.1.id, result_id); + } else { + assert_eq!(versions.len(), 0); + } + + // update_individual_files + let mut loader_fields = HashMap::new(); + if let Some(game_versions) = game_versions { + loader_fields.insert( + "game_versions".to_string(), + game_versions + .into_iter() + .map(|v| json!(v)) + .collect::>(), + ); + } + + let hashes = vec![FileUpdateData { + hash: alpha_version_hash.to_string(), + loaders, + loader_fields: Some(loader_fields), + version_types: version_types.map(|v| { + v.into_iter() + .map(|v| { + serde_json::from_str(&format!("\"{v}\"")) + .unwrap() + }) + .collect() + }), + }]; + let versions = api + .update_individual_files_deserialized( + "sha1", + hashes, + USER_USER_PAT, + ) + .await; + if success { + assert_eq!(versions.len(), 1); + let first = versions.iter().next().unwrap(); + assert_eq!(first.1.id, result_id); + } else { + assert_eq!(versions.len(), 0); + } }; - // get_update_from_hash - let resp = api - .get_update_from_hash( - alpha_version_hash, - "sha1", - loaders.clone(), - game_versions.clone(), - version_types.clone(), - USER_USER_PAT, - ) - .await; - if success { - assert_status!(&resp, StatusCode::OK); - let body: serde_json::Value = test::read_body_json(resp).await; - let id = body["id"].as_str().unwrap(); - assert_eq!(id, &result_id.to_string()); - } else { - assert_status!(&resp, StatusCode::NOT_FOUND); - } - - // update_files - let versions = api - .update_files_deserialized_common( - "sha1", - vec![alpha_version_hash.to_string()], - loaders.clone(), - game_versions.clone(), - version_types.clone(), - USER_USER_PAT, - ) - .await; - if success { - assert_eq!(versions.len(), 1); - let first = versions.iter().next().unwrap(); - assert_eq!(first.1.id, result_id); - } else { - assert_eq!(versions.len(), 0); - } - - // update_individual_files - let mut loader_fields = HashMap::new(); - if let Some(game_versions) = game_versions { - loader_fields.insert( - "game_versions".to_string(), - game_versions - .into_iter() - .map(|v| json!(v)) - .collect::>(), - ); - } - - let hashes = vec![FileUpdateData { - hash: alpha_version_hash.to_string(), - loaders, - loader_fields: Some(loader_fields), - version_types: version_types.map(|v| { - v.into_iter() - .map(|v| serde_json::from_str(&format!("\"{v}\"")).unwrap()) - .collect() - }), - }]; - let versions = api - .update_individual_files_deserialized("sha1", hashes, USER_USER_PAT) - .await; - if success { - assert_eq!(versions.len(), 1); - let first = versions.iter().next().unwrap(); - assert_eq!(first.1.id, result_id); - } else { - assert_eq!(versions.len(), 0); - } - }; let tests = vec![ check_expected( @@ -513,7 +532,8 @@ pub async fn test_patch_version() { pub async fn test_project_versions() { with_test_environment_all(None, |test_env| async move { let api = &test_env.api; - let alpha_project_id: &String = &test_env.dummy.project_alpha.project_id; + let alpha_project_id: &String = + &test_env.dummy.project_alpha.project_id; let alpha_version_id = &test_env.dummy.project_alpha.version_id; let versions = api @@ -539,7 +559,8 @@ async fn can_create_version_with_ordering() { with_test_environment( None, |env: common::environment::TestEnvironment| async move { - let alpha_project_id_parsed = env.dummy.project_alpha.project_id_parsed; + let alpha_project_id_parsed = + env.dummy.project_alpha.project_id_parsed; let new_version_id = get_json_val_str( env.api @@ -557,7 +578,10 @@ async fn can_create_version_with_ordering() { let versions = env .api - .get_versions_deserialized(vec![new_version_id.clone()], USER_USER_PAT) + .get_versions_deserialized( + vec![new_version_id.clone()], + USER_USER_PAT, + ) .await; assert_eq!(versions[0].ordering, Some(1)); }, @@ -574,13 +598,20 @@ async fn edit_version_ordering_works() { let resp = env .api - .edit_version_ordering(&alpha_version_id, Some(10), USER_USER_PAT) + .edit_version_ordering( + &alpha_version_id, + Some(10), + USER_USER_PAT, + ) .await; assert_status!(&resp, StatusCode::NO_CONTENT); let versions = env .api - .get_versions_deserialized(vec![alpha_version_id.clone()], USER_USER_PAT) + .get_versions_deserialized( + vec![alpha_version_id.clone()], + USER_USER_PAT, + ) .await; assert_eq!(versions[0].ordering, Some(10)); }, @@ -618,7 +649,10 @@ async fn version_ordering_for_specified_orderings_orders_lower_order_first() { ) .await; - assert_common_version_ids(&versions, vec![new_version_id, alpha_version_id]); + assert_common_version_ids( + &versions, + vec![new_version_id, alpha_version_id], + ); }) .await; } @@ -627,7 +661,8 @@ async fn version_ordering_for_specified_orderings_orders_lower_order_first() { async fn version_ordering_when_unspecified_orders_oldest_first() { with_test_environment_all(None, |env| async move { let alpha_project_id_parsed = env.dummy.project_alpha.project_id_parsed; - let alpha_version_id: String = env.dummy.project_alpha.version_id.clone(); + let alpha_version_id: String = + env.dummy.project_alpha.version_id.clone(); let new_version_id = get_json_val_str( env.api .add_public_version_deserialized_common( @@ -649,7 +684,10 @@ async fn version_ordering_when_unspecified_orders_oldest_first() { USER_USER_PAT, ) .await; - assert_common_version_ids(&versions, vec![alpha_version_id, new_version_id]); + assert_common_version_ids( + &versions, + vec![alpha_version_id, new_version_id], + ); }) .await } @@ -683,7 +721,10 @@ async fn version_ordering_when_specified_orders_specified_before_unspecified() { USER_USER_PAT, ) .await; - assert_common_version_ids(&versions, vec![new_version_id, alpha_version_id]); + assert_common_version_ids( + &versions, + vec![new_version_id, alpha_version_id], + ); }) .await; } diff --git a/packages/app-lib/package.json b/packages/app-lib/package.json new file mode 100644 index 000000000..ad290d57f --- /dev/null +++ b/packages/app-lib/package.json @@ -0,0 +1,9 @@ +{ + "name": "@modrinth/app-lib", + "scripts": { + "build": "cargo build --release", + "lint": "cargo fmt --check && cargo clippy --all-targets --all-features -- -D warnings", + "fix": "cargo fmt && cargo clippy --fix", + "test": "cargo test" + } +} \ No newline at end of file