* chore(clippy): enable and fix many stricter lints These ensure that the codebase uses more idiomatic, performant, and concise language constructions. * chore: make non-Clippy compiler warnings also deny by default
577 lines
18 KiB
Rust
577 lines
18 KiB
Rust
use std::io::{Cursor, Write};
|
|
|
|
use crate::{
|
|
assert_status,
|
|
common::{api_common::Api, api_v3, database::USER_USER_PAT},
|
|
};
|
|
use actix_http::StatusCode;
|
|
use actix_web::test::{self, TestRequest};
|
|
use labrinth::models::ids::ProjectId;
|
|
use labrinth::models::{
|
|
oauth_clients::OAuthClient,
|
|
organizations::Organization,
|
|
pats::Scopes,
|
|
projects::{Project, Version},
|
|
};
|
|
use serde_json::json;
|
|
use sqlx::Executor;
|
|
use zip::{CompressionMethod, ZipWriter, write::FileOptions};
|
|
|
|
use super::{
|
|
api_common::{ApiProject, AppendsOptionalPat, request_data::ImageData},
|
|
api_v3::ApiV3,
|
|
database::TemporaryDatabase,
|
|
};
|
|
|
|
use super::{database::USER_USER_ID, get_json_val_str};
|
|
|
|
pub const DUMMY_DATA_UPDATE: i64 = 7;
|
|
|
|
pub const DUMMY_CATEGORIES: &[&str] = &[
|
|
"combat",
|
|
"decoration",
|
|
"economy",
|
|
"food",
|
|
"magic",
|
|
"mobs",
|
|
"optimization",
|
|
];
|
|
|
|
pub const DUMMY_OAUTH_CLIENT_ALPHA_SECRET: &str = "abcdefghijklmnopqrstuvwxyz";
|
|
|
|
#[derive(Clone)]
|
|
pub enum TestFile {
|
|
DummyProjectAlpha,
|
|
DummyProjectBeta,
|
|
BasicZip,
|
|
BasicMod,
|
|
BasicModDifferent,
|
|
// Randomly generates a valid .jar with a random hash.
|
|
// Unlike the other dummy jar files, this one is not a static file.
|
|
// and BasicModRandom.bytes() will return a different file each time.
|
|
BasicModRandom { filename: String, bytes: Vec<u8> },
|
|
BasicModpackRandom { filename: String, bytes: Vec<u8> },
|
|
}
|
|
|
|
impl TestFile {
|
|
pub fn build_random_jar() -> Self {
|
|
let filename = format!("random-mod-{}.jar", rand::random::<u64>());
|
|
|
|
let fabric_mod_json = serde_json::json!({
|
|
"schemaVersion": 1,
|
|
"id": filename,
|
|
"version": "1.0.1",
|
|
|
|
"name": filename,
|
|
"description": "Does nothing",
|
|
"authors": [
|
|
"user"
|
|
],
|
|
"contact": {
|
|
"homepage": "https://www.modrinth.com",
|
|
"sources": "https://www.modrinth.com",
|
|
"issues": "https://www.modrinth.com"
|
|
},
|
|
|
|
"license": "MIT",
|
|
"icon": "none.png",
|
|
|
|
"environment": "client",
|
|
"entrypoints": {
|
|
"main": [
|
|
"io.github.modrinth.Modrinth"
|
|
]
|
|
},
|
|
"depends": {
|
|
"minecraft": ">=1.20-"
|
|
}
|
|
}
|
|
)
|
|
.to_string();
|
|
|
|
// Create a simulated zip file
|
|
let mut cursor = Cursor::new(Vec::new());
|
|
{
|
|
let mut zip = ZipWriter::new(&mut cursor);
|
|
zip.start_file(
|
|
"fabric.mod.json",
|
|
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),
|
|
)
|
|
.unwrap();
|
|
zip.write_all(fabric_mod_json.as_bytes()).unwrap();
|
|
|
|
zip.finish().unwrap();
|
|
}
|
|
let bytes = cursor.into_inner();
|
|
|
|
TestFile::BasicModRandom { filename, bytes }
|
|
}
|
|
|
|
pub fn build_random_mrpack() -> Self {
|
|
let filename =
|
|
format!("random-modpack-{}.mrpack", rand::random::<u64>());
|
|
|
|
let modrinth_index_json = serde_json::json!({
|
|
"formatVersion": 1,
|
|
"game": "minecraft",
|
|
"versionId": "1.20.1-9.6",
|
|
"name": filename,
|
|
"files": [
|
|
{
|
|
"path": "mods/animatica-0.6+1.20.jar",
|
|
"hashes": {
|
|
"sha1": "3bcb19c759f313e69d3f7848b03c48f15167b88d",
|
|
"sha512": "7d50f3f34479f8b052bfb9e2482603b4906b8984039777dc2513ecf18e9af2b599c9d094e88cec774f8525345859e721a394c8cd7c14a789c9538d2533c71d65"
|
|
},
|
|
"env": {
|
|
"client": "required",
|
|
"server": "required"
|
|
},
|
|
"downloads": [
|
|
"https://cdn.modrinth.com/data/PRN43VSY/versions/uNgEPb10/animatica-0.6%2B1.20.jar"
|
|
],
|
|
"fileSize": 69810
|
|
}
|
|
],
|
|
"dependencies": {
|
|
"fabric-loader": "0.14.22",
|
|
"minecraft": "1.20.1"
|
|
}
|
|
}
|
|
)
|
|
.to_string();
|
|
|
|
// Create a simulated zip file
|
|
let mut cursor = Cursor::new(Vec::new());
|
|
{
|
|
let mut zip = ZipWriter::new(&mut cursor);
|
|
zip.start_file(
|
|
"modrinth.index.json",
|
|
FileOptions::<()>::default()
|
|
.compression_method(CompressionMethod::Stored),
|
|
)
|
|
.unwrap();
|
|
zip.write_all(modrinth_index_json.as_bytes()).unwrap();
|
|
zip.finish().unwrap();
|
|
}
|
|
let bytes = cursor.into_inner();
|
|
|
|
TestFile::BasicModpackRandom { filename, bytes }
|
|
}
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub enum DummyImage {
|
|
SmallIcon, // 200x200
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct DummyData {
|
|
/// Alpha project:
|
|
/// This is a dummy project created by USER user.
|
|
/// It's approved, listed, and visible to the public.
|
|
pub project_alpha: DummyProjectAlpha,
|
|
|
|
/// Beta project:
|
|
/// This is a dummy project created by USER user.
|
|
/// It's not approved, unlisted, and not visible to the public.
|
|
pub project_beta: DummyProjectBeta,
|
|
|
|
/// Zeta organization:
|
|
/// This is a dummy organization created by USER user.
|
|
/// There are no projects in it.
|
|
pub organization_zeta: DummyOrganizationZeta,
|
|
|
|
/// Alpha OAuth Client:
|
|
/// This is a dummy OAuth client created by USER user.
|
|
///
|
|
/// All scopes are included in its max scopes
|
|
///
|
|
/// It has one valid redirect URI
|
|
pub oauth_client_alpha: DummyOAuthClientAlpha,
|
|
}
|
|
|
|
impl DummyData {
|
|
pub fn new(
|
|
project_alpha: Project,
|
|
project_alpha_version: Version,
|
|
project_beta: Project,
|
|
project_beta_version: Version,
|
|
organization_zeta: Organization,
|
|
oauth_client_alpha: OAuthClient,
|
|
) -> Self {
|
|
DummyData {
|
|
project_alpha: DummyProjectAlpha {
|
|
team_id: project_alpha.team_id.to_string(),
|
|
project_id: project_alpha.id.to_string(),
|
|
project_slug: project_alpha.slug.unwrap(),
|
|
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(),
|
|
},
|
|
|
|
project_beta: DummyProjectBeta {
|
|
team_id: project_beta.team_id.to_string(),
|
|
project_id: project_beta.id.to_string(),
|
|
project_slug: project_beta.slug.unwrap(),
|
|
project_id_parsed: project_beta.id,
|
|
version_id: project_beta_version.id.to_string(),
|
|
thread_id: project_beta.thread_id.to_string(),
|
|
file_hash: project_beta_version.files[0].hashes["sha1"].clone(),
|
|
},
|
|
|
|
organization_zeta: DummyOrganizationZeta {
|
|
organization_id: organization_zeta.id.to_string(),
|
|
team_id: organization_zeta.team_id.to_string(),
|
|
organization_slug: organization_zeta.slug,
|
|
},
|
|
|
|
oauth_client_alpha: DummyOAuthClientAlpha {
|
|
client_id: get_json_val_str(oauth_client_alpha.id),
|
|
client_secret: DUMMY_OAUTH_CLIENT_ALPHA_SECRET.to_string(),
|
|
valid_redirect_uri: oauth_client_alpha
|
|
.redirect_uris
|
|
.first()
|
|
.unwrap()
|
|
.uri
|
|
.clone(),
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct DummyProjectAlpha {
|
|
pub project_id: String,
|
|
pub project_slug: String,
|
|
pub project_id_parsed: ProjectId,
|
|
pub version_id: String,
|
|
pub thread_id: String,
|
|
pub file_hash: String,
|
|
pub team_id: String,
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct DummyProjectBeta {
|
|
pub project_id: String,
|
|
pub project_slug: String,
|
|
pub project_id_parsed: ProjectId,
|
|
pub version_id: String,
|
|
pub thread_id: String,
|
|
pub file_hash: String,
|
|
pub team_id: String,
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct DummyOrganizationZeta {
|
|
pub organization_id: String,
|
|
pub organization_slug: String,
|
|
pub team_id: String,
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct DummyOAuthClientAlpha {
|
|
pub client_id: String,
|
|
pub client_secret: String,
|
|
pub valid_redirect_uri: String,
|
|
}
|
|
|
|
pub async fn add_dummy_data(api: &ApiV3, db: TemporaryDatabase) -> DummyData {
|
|
// Adds basic dummy data to the database directly with sql (user, pats)
|
|
let pool = &db.pool.clone();
|
|
|
|
pool.execute(
|
|
include_str!("../files/dummy_data.sql")
|
|
.replace("$1", &Scopes::all().bits().to_string())
|
|
.as_str(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
let (alpha_project, alpha_version) = add_project_alpha(api).await;
|
|
let (beta_project, beta_version) = add_project_beta(api).await;
|
|
|
|
let zeta_organization = add_organization_zeta(api).await;
|
|
|
|
let oauth_client_alpha = get_oauth_client_alpha(api).await;
|
|
|
|
sqlx::query("INSERT INTO dummy_data (update_id) VALUES ($1)")
|
|
.bind(DUMMY_DATA_UPDATE)
|
|
.execute(pool)
|
|
.await
|
|
.unwrap();
|
|
|
|
DummyData::new(
|
|
alpha_project,
|
|
alpha_version,
|
|
beta_project,
|
|
beta_version,
|
|
zeta_organization,
|
|
oauth_client_alpha,
|
|
)
|
|
}
|
|
|
|
pub async fn get_dummy_data(api: &ApiV3) -> DummyData {
|
|
let (alpha_project, alpha_version) = get_project_alpha(api).await;
|
|
let (beta_project, beta_version) = get_project_beta(api).await;
|
|
|
|
let zeta_organization = get_organization_zeta(api).await;
|
|
|
|
let oauth_client_alpha = get_oauth_client_alpha(api).await;
|
|
|
|
DummyData::new(
|
|
alpha_project,
|
|
alpha_version,
|
|
beta_project,
|
|
beta_version,
|
|
zeta_organization,
|
|
oauth_client_alpha,
|
|
)
|
|
}
|
|
|
|
pub async fn add_project_alpha(api: &ApiV3) -> (Project, Version) {
|
|
let (project, versions) = api
|
|
.add_public_project(
|
|
"alpha",
|
|
Some(TestFile::DummyProjectAlpha),
|
|
None,
|
|
USER_USER_PAT,
|
|
)
|
|
.await;
|
|
let alpha_project = api
|
|
.get_project_deserialized(
|
|
project.id.to_string().as_str(),
|
|
USER_USER_PAT,
|
|
)
|
|
.await;
|
|
let alpha_version = api
|
|
.get_version_deserialized(
|
|
&versions.into_iter().next().unwrap().id.to_string(),
|
|
USER_USER_PAT,
|
|
)
|
|
.await;
|
|
(alpha_project, alpha_version)
|
|
}
|
|
|
|
pub async fn add_project_beta(api: &ApiV3) -> (Project, Version) {
|
|
// Adds dummy data to the database with sqlx (projects, versions, threads)
|
|
// Generate test project data.
|
|
let jar = TestFile::DummyProjectBeta;
|
|
// TODO: this shouldnt be hardcoded (nor should other similar ones be)
|
|
|
|
let modify_json = serde_json::from_value(json!([
|
|
{ "op": "add", "path": "/summary", "value": "A dummy project for testing with." },
|
|
{ "op": "add", "path": "/description", "value": "This project is not-yet-approved, and versions are draft." },
|
|
{ "op": "add", "path": "/initial_versions/0/status", "value": "unlisted" },
|
|
{ "op": "add", "path": "/status", "value": "private" },
|
|
{ "op": "add", "path": "/requested_status", "value": "private" },
|
|
]))
|
|
.unwrap();
|
|
|
|
let creation_data = api_v3::request_data::get_public_project_creation_data(
|
|
"beta",
|
|
Some(jar),
|
|
Some(modify_json),
|
|
);
|
|
api.create_project(creation_data, USER_USER_PAT).await;
|
|
|
|
get_project_beta(api).await
|
|
}
|
|
|
|
pub async fn add_organization_zeta(api: &ApiV3) -> Organization {
|
|
// Add an organzation.
|
|
let req = TestRequest::post()
|
|
.uri("/v3/organization")
|
|
.append_pat(USER_USER_PAT)
|
|
.set_json(json!({
|
|
"name": "Zeta",
|
|
"slug": "zeta",
|
|
"description": "A dummy organization for testing with."
|
|
}))
|
|
.to_request();
|
|
let resp = api.call(req).await;
|
|
|
|
assert_status!(&resp, StatusCode::OK);
|
|
|
|
get_organization_zeta(api).await
|
|
}
|
|
|
|
pub async fn get_project_alpha(api: &ApiV3) -> (Project, Version) {
|
|
// Get project
|
|
let req = TestRequest::get()
|
|
.uri("/v3/project/alpha")
|
|
.append_pat(USER_USER_PAT)
|
|
.to_request();
|
|
let resp = api.call(req).await;
|
|
let project: Project = test::read_body_json(resp).await;
|
|
|
|
// Get project's versions
|
|
let req = TestRequest::get()
|
|
.uri("/v3/project/alpha/version")
|
|
.append_pat(USER_USER_PAT)
|
|
.to_request();
|
|
let resp = api.call(req).await;
|
|
let versions: Vec<Version> = test::read_body_json(resp).await;
|
|
let version = versions.into_iter().next().unwrap();
|
|
|
|
(project, version)
|
|
}
|
|
|
|
pub async fn get_project_beta(api: &ApiV3) -> (Project, Version) {
|
|
// Get project
|
|
let req = TestRequest::get()
|
|
.uri("/v3/project/beta")
|
|
.append_pat(USER_USER_PAT)
|
|
.to_request();
|
|
let resp = api.call(req).await;
|
|
assert_status!(&resp, StatusCode::OK);
|
|
let project: serde_json::Value = test::read_body_json(resp).await;
|
|
let project: Project = serde_json::from_value(project).unwrap();
|
|
|
|
// Get project's versions
|
|
let req = TestRequest::get()
|
|
.uri("/v3/project/beta/version")
|
|
.append_pat(USER_USER_PAT)
|
|
.to_request();
|
|
let resp = api.call(req).await;
|
|
assert_status!(&resp, StatusCode::OK);
|
|
let versions: Vec<Version> = test::read_body_json(resp).await;
|
|
let version = versions.into_iter().next().unwrap();
|
|
|
|
(project, version)
|
|
}
|
|
|
|
pub async fn get_organization_zeta(api: &ApiV3) -> Organization {
|
|
// Get organization
|
|
let req = TestRequest::get()
|
|
.uri("/v3/organization/zeta")
|
|
.append_pat(USER_USER_PAT)
|
|
.to_request();
|
|
let resp = api.call(req).await;
|
|
let organization: Organization = test::read_body_json(resp).await;
|
|
|
|
organization
|
|
}
|
|
|
|
pub async fn get_oauth_client_alpha(api: &ApiV3) -> OAuthClient {
|
|
let oauth_clients = api
|
|
.get_user_oauth_clients(USER_USER_ID, USER_USER_PAT)
|
|
.await;
|
|
oauth_clients.into_iter().next().unwrap()
|
|
}
|
|
|
|
impl TestFile {
|
|
pub fn filename(&self) -> String {
|
|
match self {
|
|
TestFile::DummyProjectAlpha => "dummy-project-alpha.jar",
|
|
TestFile::DummyProjectBeta => "dummy-project-beta.jar",
|
|
TestFile::BasicZip => "simple-zip.zip",
|
|
TestFile::BasicMod => "basic-mod.jar",
|
|
TestFile::BasicModDifferent => "basic-mod-different.jar",
|
|
TestFile::BasicModRandom { filename, .. } => filename,
|
|
TestFile::BasicModpackRandom { filename, .. } => filename,
|
|
}
|
|
.to_string()
|
|
}
|
|
|
|
pub fn bytes(&self) -> Vec<u8> {
|
|
match self {
|
|
TestFile::DummyProjectAlpha => {
|
|
include_bytes!("../../tests/files/dummy-project-alpha.jar")
|
|
.to_vec()
|
|
}
|
|
TestFile::DummyProjectBeta => {
|
|
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::BasicModDifferent => {
|
|
include_bytes!("../../tests/files/basic-mod-different.jar")
|
|
.to_vec()
|
|
}
|
|
TestFile::BasicModRandom { bytes, .. } => bytes.clone(),
|
|
TestFile::BasicModpackRandom { bytes, .. } => bytes.clone(),
|
|
}
|
|
}
|
|
|
|
pub fn project_type(&self) -> String {
|
|
match self {
|
|
TestFile::DummyProjectAlpha => "mod",
|
|
TestFile::DummyProjectBeta => "mod",
|
|
TestFile::BasicMod => "mod",
|
|
TestFile::BasicModDifferent => "mod",
|
|
TestFile::BasicModRandom { .. } => "mod",
|
|
|
|
TestFile::BasicZip => "resourcepack",
|
|
|
|
TestFile::BasicModpackRandom { .. } => "modpack",
|
|
}
|
|
.to_string()
|
|
}
|
|
|
|
pub fn content_type(&self) -> Option<String> {
|
|
match self {
|
|
TestFile::DummyProjectAlpha => Some("application/java-archive"),
|
|
TestFile::DummyProjectBeta => Some("application/java-archive"),
|
|
TestFile::BasicMod => Some("application/java-archive"),
|
|
TestFile::BasicModDifferent => Some("application/java-archive"),
|
|
TestFile::BasicModRandom { .. } => Some("application/java-archive"),
|
|
|
|
TestFile::BasicZip => Some("application/zip"),
|
|
|
|
TestFile::BasicModpackRandom { .. } => {
|
|
Some("application/x-modrinth-modpack+zip")
|
|
}
|
|
}
|
|
.map(|s| s.to_string())
|
|
}
|
|
}
|
|
|
|
impl DummyImage {
|
|
pub fn filename(&self) -> String {
|
|
match self {
|
|
DummyImage::SmallIcon => "200x200.png",
|
|
}
|
|
.to_string()
|
|
}
|
|
|
|
pub fn extension(&self) -> String {
|
|
match self {
|
|
DummyImage::SmallIcon => "png",
|
|
}
|
|
.to_string()
|
|
}
|
|
|
|
pub fn bytes(&self) -> Vec<u8> {
|
|
match self {
|
|
DummyImage::SmallIcon => {
|
|
include_bytes!("../../tests/files/200x200.png").to_vec()
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn get_icon_data(&self) -> ImageData {
|
|
ImageData {
|
|
filename: self.filename(),
|
|
extension: self.extension(),
|
|
icon: self.bytes(),
|
|
}
|
|
}
|
|
}
|