Fix clippy errors + lint, use turbo CI

This commit is contained in:
Jai A
2024-10-18 16:07:35 -07:00
parent 663ab83b08
commit 8dd955563e
186 changed files with 10615 additions and 6433 deletions

View File

@@ -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<CommonLoaderData>;
async fn get_categories(&self) -> ServiceResponse;
async fn get_categories_deserialized_common(&self) -> Vec<CommonCategoryData>;
async fn get_categories_deserialized_common(
&self,
) -> Vec<CommonCategoryData>;
}
#[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<CommonTeamMember>;
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<CommonTeamMember>;
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<CommonNotification>;
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<json_patch::Patch>,
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<String>, pat: Option<&str>) -> ServiceResponse;
async fn get_versions(
&self,
ids: Vec<String>,
pat: Option<&str>,
) -> ServiceResponse;
async fn get_versions_deserialized_common(
&self,
ids: Vec<String>,
@@ -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 {

View File

@@ -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},

View File

@@ -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<dyn LocalService> = Rc::new(test::init_service(app).await);
let app = App::new().configure(|cfg| {
labrinth::app_config(cfg, labrinth_config.clone())
});
let test_app: Rc<dyn LocalService> =
Rc::new(test::init_service(app).await);
Self { test_app }
}

View File

@@ -89,7 +89,8 @@ impl ApiProject for ApiV2 {
modify_json: Option<json_patch::Patch>,
pat: Option<&str>,
) -> (CommonProject, Vec<CommonVersion>) {
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)

View File

@@ -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<TestFile>,
modify_json: Option<json_patch::Patch>,
) -> 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<i32>,
modify_json: Option<json_patch::Patch>,
) -> 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 {

View File

@@ -44,7 +44,9 @@ impl ApiV2 {
self.call(req).await
}
pub async fn get_game_versions_deserialized(&self) -> Vec<GameVersionQueryData> {
pub async fn get_game_versions_deserialized(
&self,
) -> Vec<GameVersionQueryData> {
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<DonationPlatformQueryData> {
pub async fn get_donation_platforms_deserialized(
&self,
) -> Vec<DonationPlatformQueryData> {
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<CommonCategoryData> {
async fn get_categories_deserialized_common(
&self,
) -> Vec<CommonCategoryData> {
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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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<String, LegacyVersion> = test::read_body_json(resp).await;
let v: HashMap<String, LegacyVersion> =
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<String, LegacyVersion> = test::read_body_json(resp).await;
let v: HashMap<String, LegacyVersion> =
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<String>, pat: Option<&str>) -> ServiceResponse {
async fn get_versions(
&self,
version_ids: Vec<String>,
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)

View File

@@ -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)

View File

@@ -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<dyn LocalService> = Rc::new(test::init_service(app).await);
let app = App::new().configure(|cfg| {
labrinth::app_config(cfg, labrinth_config.clone())
});
let test_app: Rc<dyn LocalService> =
Rc::new(test::init_service(app).await);
Self { test_app }
}

View File

@@ -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()

View File

@@ -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)

View File

@@ -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)

View File

@@ -43,7 +43,8 @@ impl ApiProject for ApiV3 {
modify_json: Option<json_patch::Patch>,
pat: Option<&str>,
) -> (CommonProject, Vec<CommonVersion>) {
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()

View File

@@ -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<TestFile>,
modify_json: Option<json_patch::Patch>,
) -> 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<json_patch::Patch>,
) -> 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 {

View File

@@ -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<CommonCategoryData> {
async fn get_categories_deserialized_common(
&self,
) -> Vec<CommonCategoryData> {
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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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<String>, pat: Option<&str>) -> ServiceResponse {
async fn get_versions(
&self,
version_ids: Vec<String>,
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)

View File

@@ -38,7 +38,10 @@ pub fn assert_version_ids(versions: &[Version], expected_ids: Vec<String>) {
assert_eq!(version_ids, expected_ids);
}
pub fn assert_common_version_ids(versions: &[CommonVersion], expected_ids: Vec<String>) {
pub fn assert_common_version_ids(
versions: &[CommonVersion],
expected_ids: Vec<String>,
) {
let version_ids = versions
.iter()
.map(|v| get_json_val_str(v.id))

View File

@@ -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::<ApiV3>::build_setup_api(&db).await;
let setup_api =
TestEnvironment::<ApiV3>::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

View File

@@ -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::<u64>());
let filename =
format!("random-modpack-{}.mrpack", rand::random::<u64>());
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<u8> {
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<u8> {
match self {
DummyImage::SmallIcon => include_bytes!("../../tests/files/200x200.png").to_vec(),
DummyImage::SmallIcon => {
include_bytes!("../../tests/files/200x200.png").to_vec()
}
}
}

View File

@@ -19,19 +19,23 @@ pub async fn with_test_environment<Fut, A>(
Fut: Future<Output = ()>,
A: ApiBuildable + 'static,
{
let test_env: TestEnvironment<A> = TestEnvironment::build(max_connections).await;
let test_env: TestEnvironment<A> =
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<Fut, F>(max_connections: Option<u32>, f: F)
where
pub async fn with_test_environment_all<Fut, F>(
max_connections: Option<u32>,
f: F,
) where
Fut: Future<Output = ()>,
F: Fn(TestEnvironment<GenericApi>) -> Fut,
{
println!("Test environment: API v3");
let test_env_api_v3 = TestEnvironment::<ApiV3>::build(max_connections).await;
let test_env_api_v3 =
TestEnvironment::<ApiV3>::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::<ApiV2>::build(max_connections).await;
let test_env_api_v2 =
TestEnvironment::<ApiV2>::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<dyn std::future::Future<Output = Result<ServiceResponse, actix_web::Error>>>,
Box<
dyn std::future::Future<
Output = Result<ServiceResponse, actix_web::Error>,
>,
>,
>;
}
impl<S> LocalService for S
@@ -155,7 +164,11 @@ where
&self,
req: actix_http::Request,
) -> std::pin::Pin<
Box<dyn std::future::Future<Output = Result<ServiceResponse, actix_web::Error>>>,
Box<
dyn std::future::Future<
Output = Result<ServiceResponse, actix_web::Error>,
>,
>,
> {
Box::pin(self.call(req))
}

View File

@@ -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(),

View File

@@ -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 {

View File

@@ -97,7 +97,8 @@ impl<'a, A: Api> PermissionsTest<'a, A> {
failure_organization_permissions: Option<OrganizationPermissions>,
) -> 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<Item = u16>,
) -> 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,

View File

@@ -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)

View File

@@ -16,11 +16,14 @@ use crate::{
use super::{api_v3::ApiV3, environment::TestEnvironment};
pub async fn setup_search_projects(test_env: &TestEnvironment<ApiV3>) -> Arc<HashMap<u64, u64>> {
pub async fn setup_search_projects(
test_env: &TestEnvironment<ApiV3>,
) -> Arc<HashMap<u64, u64>> {
// 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<ApiV3>) -> Arc<Has
};
async move {
// Add a project- simple, should work.
let req = api.add_public_project(&slug, Some(jar), modify_json, pat);
let req =
api.add_public_project(&slug, Some(jar), modify_json, pat);
let (project, _) = req.await;
// Approve, so that the project is searchable