* computer switch

* some fixes; github action

* added pr to master

* sqlx database setup

* switched intial GHA test db

* removed sqlx database setup

* unfinished patch route

* bug fixes + tests

* more tests, more fixes, cargo fmt

* merge fixes

* more tests, full reorganization

* fmt, clippy

* sqlx-data

* revs

* removed comments

* delete revs
This commit is contained in:
Wyatt Verchere
2023-10-06 09:57:33 -07:00
committed by GitHub
parent a1b59d4545
commit 259c5ef3d0
69 changed files with 4167 additions and 1312 deletions

82
tests/common/actix.rs Normal file
View File

@@ -0,0 +1,82 @@
use actix_web::test::TestRequest;
use bytes::{Bytes, BytesMut};
// Multipart functionality (actix-test does not innately support multipart)
#[derive(Debug, Clone)]
pub struct MultipartSegment {
pub name: String,
pub filename: Option<String>,
pub content_type: Option<String>,
pub data: MultipartSegmentData,
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub enum MultipartSegmentData {
Text(String),
Binary(Vec<u8>),
}
pub trait AppendsMultipart {
fn set_multipart(self, data: Vec<MultipartSegment>) -> Self;
}
impl AppendsMultipart for TestRequest {
fn set_multipart(self, data: Vec<MultipartSegment>) -> Self {
let (boundary, payload) = generate_multipart(data);
self.append_header((
"Content-Type",
format!("multipart/form-data; boundary={}", boundary),
))
.set_payload(payload)
}
}
fn generate_multipart(data: Vec<MultipartSegment>) -> (String, Bytes) {
let mut boundary = String::from("----WebKitFormBoundary");
boundary.push_str(&rand::random::<u64>().to_string());
boundary.push_str(&rand::random::<u64>().to_string());
boundary.push_str(&rand::random::<u64>().to_string());
let mut payload = BytesMut::new();
for segment in data {
payload.extend_from_slice(
format!(
"--{boundary}\r\nContent-Disposition: form-data; name=\"{name}\"",
boundary = boundary,
name = segment.name
)
.as_bytes(),
);
if let Some(filename) = &segment.filename {
payload.extend_from_slice(
format!("; filename=\"{filename}\"", filename = filename).as_bytes(),
);
}
if let Some(content_type) = &segment.content_type {
payload.extend_from_slice(
format!(
"\r\nContent-Type: {content_type}",
content_type = content_type
)
.as_bytes(),
);
}
payload.extend_from_slice(b"\r\n\r\n");
match &segment.data {
MultipartSegmentData::Text(text) => {
payload.extend_from_slice(text.as_bytes());
}
MultipartSegmentData::Binary(binary) => {
payload.extend_from_slice(binary);
}
}
payload.extend_from_slice(b"\r\n");
}
payload.extend_from_slice(format!("--{boundary}--\r\n", boundary = boundary).as_bytes());
(boundary, Bytes::from(payload))
}

134
tests/common/database.rs Normal file
View File

@@ -0,0 +1,134 @@
#![allow(dead_code)]
use labrinth::database::redis::RedisPool;
use sqlx::{postgres::PgPoolOptions, PgPool};
use std::time::Duration;
use url::Url;
// The dummy test database adds a fair bit of 'dummy' data to test with.
// Some constants are used to refer to that data, and are described here.
// The rest can be accessed in the TestEnvironment 'dummy' field.
// The user IDs are as follows:
pub const ADMIN_USER_ID: &str = "1";
pub const MOD_USER_ID: &str = "2";
pub const USER_USER_ID: &str = "3"; // This is the 'main' user ID, and is used for most tests.
pub const FRIEND_USER_ID: &str = "4"; // This is exactly the same as USER_USER_ID, but could be used for testing friend-only endpoints (ie: teams, etc)
pub const ENEMY_USER_ID: &str = "5"; // This is exactly the same as USER_USER_ID, but could be used for testing friend-only endpoints (ie: teams, etc)
pub const ADMIN_USER_ID_PARSED: i64 = 1;
pub const MOD_USER_ID_PARSED: i64 = 2;
pub const USER_USER_ID_PARSED: i64 = 3;
pub const FRIEND_USER_ID_PARSED: i64 = 4;
pub const ENEMY_USER_ID_PARSED: i64 = 5;
// These are full-scoped PATs- as if the user was logged in (including illegal scopes).
pub const ADMIN_USER_PAT: &str = "mrp_patadmin";
pub const MOD_USER_PAT: &str = "mrp_patmoderator";
pub const USER_USER_PAT: &str = "mrp_patuser";
pub const FRIEND_USER_PAT: &str = "mrp_patfriend";
pub const ENEMY_USER_PAT: &str = "mrp_patenemy";
pub struct TemporaryDatabase {
pub pool: PgPool,
pub redis_pool: RedisPool,
pub database_name: String,
}
impl TemporaryDatabase {
// Creates a temporary database like sqlx::test does
// 1. Logs into the main database
// 2. Creates a new randomly generated database
// 3. Runs migrations on the new database
// 4. (Optionally, by using create_with_dummy) adds dummy data to the database
// If a db is created with create_with_dummy, 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 create() -> Self {
let temp_database_name = generate_random_database_name();
println!("Creating temporary database: {}", &temp_database_name);
let database_url = dotenvy::var("DATABASE_URL").expect("No database URL");
let mut url = Url::parse(&database_url).expect("Invalid database URL");
let pool = PgPool::connect(&database_url)
.await
.expect("Connection to database failed");
// Create the temporary database
let create_db_query = format!("CREATE DATABASE {}", &temp_database_name);
sqlx::query(&create_db_query)
.execute(&pool)
.await
.expect("Database creation failed");
pool.close().await;
// Modify the URL to switch to the temporary database
url.set_path(&format!("/{}", &temp_database_name));
let temp_db_url = url.to_string();
let pool = PgPoolOptions::new()
.min_connections(0)
.max_connections(4)
.max_lifetime(Some(Duration::from_secs(60 * 60)))
.connect(&temp_db_url)
.await
.expect("Connection to temporary database failed");
// Performs migrations
let migrations = sqlx::migrate!("./migrations");
migrations.run(&pool).await.expect("Migrations failed");
// Gets new Redis pool
let redis_pool = RedisPool::new(Some(temp_database_name.clone()));
Self {
pool,
database_name: temp_database_name,
redis_pool,
}
}
// Deletes the temporary database
// 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");
self.pool.close().await;
self.pool = PgPool::connect(&database_url)
.await
.expect("Connection to main database failed");
// Forcibly terminate all existing connections to this version of the temporary database
// We are done and deleting it, so we don't need them anymore
let terminate_query = format!(
"SELECT pg_terminate_backend(pg_stat_activity.pid) FROM pg_stat_activity WHERE datname = '{}' AND pid <> pg_backend_pid()",
&self.database_name
);
sqlx::query(&terminate_query)
.execute(&self.pool)
.await
.unwrap();
// Execute the deletion query asynchronously
let drop_db_query = format!("DROP DATABASE IF EXISTS {}", &self.database_name);
sqlx::query(&drop_db_query)
.execute(&self.pool)
.await
.expect("Database deletion failed");
}
}
fn generate_random_database_name() -> String {
// Generate a random database name here
// You can use your logic to create a unique name
// For example, you can use a random string as you did before
// or append a timestamp, etc.
// We will use a random string starting with "labrinth_tests_db_"
// and append a 6-digit number to it.
let mut database_name = String::from("labrinth_tests_db_");
database_name.push_str(&rand::random::<u64>().to_string()[..6]);
database_name
}

229
tests/common/dummy_data.rs Normal file
View File

@@ -0,0 +1,229 @@
use actix_web::test::{self, TestRequest};
use labrinth::{models::projects::Project, models::projects::Version};
use serde_json::json;
use sqlx::Executor;
use crate::common::{
actix::AppendsMultipart,
database::{MOD_USER_PAT, USER_USER_PAT},
};
use super::{
actix::{MultipartSegment, MultipartSegmentData},
environment::TestEnvironment,
};
pub struct DummyData {
pub alpha_team_id: String,
pub beta_team_id: String,
pub alpha_project_id: String,
pub beta_project_id: String,
pub alpha_project_slug: String,
pub beta_project_slug: String,
pub alpha_version_id: String,
pub beta_version_id: String,
pub alpha_thread_id: String,
pub beta_thread_id: String,
pub alpha_file_hash: String,
pub beta_file_hash: String,
}
pub async fn add_dummy_data(test_env: &TestEnvironment) -> DummyData {
// Adds basic dummy data to the database directly with sql (user, pats)
let pool = &test_env.db.pool.clone();
pool.execute(include_str!("../files/dummy_data.sql"))
.await
.unwrap();
let (alpha_project, alpha_version) = add_project_alpha(test_env).await;
let (beta_project, beta_version) = add_project_beta(test_env).await;
DummyData {
alpha_team_id: alpha_project.team.to_string(),
beta_team_id: beta_project.team.to_string(),
alpha_project_id: alpha_project.id.to_string(),
beta_project_id: beta_project.id.to_string(),
alpha_project_slug: alpha_project.slug.unwrap(),
beta_project_slug: beta_project.slug.unwrap(),
alpha_version_id: alpha_version.id.to_string(),
beta_version_id: beta_version.id.to_string(),
alpha_thread_id: alpha_project.thread_id.to_string(),
beta_thread_id: beta_project.thread_id.to_string(),
alpha_file_hash: alpha_version.files[0].hashes["sha1"].clone(),
beta_file_hash: beta_version.files[0].hashes["sha1"].clone(),
}
}
pub async fn add_project_alpha(test_env: &TestEnvironment) -> (Project, Version) {
// Adds dummy data to the database with sqlx (projects, versions, threads)
// Generate test project data.
let json_data = json!(
{
"title": "Test Project Alpha",
"slug": "alpha",
"description": "A dummy project for testing with.",
"body": "This project is approved, and versions are listed.",
"client_side": "required",
"server_side": "optional",
"initial_versions": [{
"file_parts": ["dummy-project-alpha.jar"],
"version_number": "1.2.3",
"version_title": "start",
"dependencies": [],
"game_versions": ["1.20.1"] ,
"release_channel": "release",
"loaders": ["fabric"],
"featured": true
}],
"categories": [],
"license_id": "MIT"
}
);
// 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 file
let file_segment = MultipartSegment {
name: "dummy-project-alpha.jar".to_string(),
filename: Some("dummy-project-alpha.jar".to_string()),
content_type: Some("application/java-archive".to_string()),
data: MultipartSegmentData::Binary(
include_bytes!("../../tests/files/dummy-project-alpha.jar").to_vec(),
),
};
// Add a project.
let req = TestRequest::post()
.uri("/v2/project")
.append_header(("Authorization", USER_USER_PAT))
.set_multipart(vec![json_segment.clone(), file_segment.clone()])
.to_request();
let resp = test_env.call(req).await;
assert_eq!(resp.status(), 200);
// Approve as a moderator.
let req = TestRequest::patch()
.uri("/v2/project/alpha")
.append_header(("Authorization", MOD_USER_PAT))
.set_json(json!(
{
"status": "approved"
}
))
.to_request();
let resp = test_env.call(req).await;
assert_eq!(resp.status(), 204);
// Get project
let req = TestRequest::get()
.uri("/v2/project/alpha")
.append_header(("Authorization", USER_USER_PAT))
.to_request();
let resp = test_env.call(req).await;
let project: Project = test::read_body_json(resp).await;
// Get project's versions
let req = TestRequest::get()
.uri("/v2/project/alpha/version")
.append_header(("Authorization", USER_USER_PAT))
.to_request();
let resp = test_env.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 add_project_beta(test_env: &TestEnvironment) -> (Project, Version) {
// Adds dummy data to the database with sqlx (projects, versions, threads)
// Generate test project data.
let json_data = json!(
{
"title": "Test Project Beta",
"slug": "beta",
"description": "A dummy project for testing with.",
"body": "This project is not-yet-approved, and versions are draft.",
"client_side": "required",
"server_side": "optional",
"initial_versions": [{
"file_parts": ["dummy-project-beta.jar"],
"version_number": "1.2.3",
"version_title": "start",
"status": "unlisted",
"requested_status": "unlisted",
"dependencies": [],
"game_versions": ["1.20.1"] ,
"release_channel": "release",
"loaders": ["fabric"],
"featured": true
}],
"status": "private",
"requested_status": "private",
"categories": [],
"license_id": "MIT"
}
);
// 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 file
let file_segment = MultipartSegment {
name: "dummy-project-beta.jar".to_string(),
filename: Some("dummy-project-beta.jar".to_string()),
content_type: Some("application/java-archive".to_string()),
data: MultipartSegmentData::Binary(
include_bytes!("../../tests/files/dummy-project-beta.jar").to_vec(),
),
};
// Add a project.
let req = TestRequest::post()
.uri("/v2/project")
.append_header(("Authorization", USER_USER_PAT))
.set_multipart(vec![json_segment.clone(), file_segment.clone()])
.to_request();
let resp = test_env.call(req).await;
assert_eq!(resp.status(), 200);
// Get project
let req = TestRequest::get()
.uri("/v2/project/beta")
.append_header(("Authorization", USER_USER_PAT))
.to_request();
let resp = test_env.call(req).await;
let project: Project = test::read_body_json(resp).await;
// Get project's versions
let req = TestRequest::get()
.uri("/v2/project/beta/version")
.append_header(("Authorization", USER_USER_PAT))
.to_request();
let resp = test_env.call(req).await;
let versions: Vec<Version> = test::read_body_json(resp).await;
let version = versions.into_iter().next().unwrap();
(project, version)
}

View File

@@ -0,0 +1,71 @@
#![allow(dead_code)]
use super::{database::TemporaryDatabase, dummy_data};
use crate::common::setup;
use actix_web::{dev::ServiceResponse, test, App};
// A complete test environment, with a test actix app and a database.
// Must be called in an #[actix_rt::test] context. It also simulates a
// temporary sqlx db like #[sqlx::test] would.
// Use .call(req) on it directly to make a test call as if test::call_service(req) were being used.
pub struct TestEnvironment {
test_app: Box<dyn LocalService>,
pub db: TemporaryDatabase,
pub dummy: Option<dummy_data::DummyData>,
}
impl TestEnvironment {
pub async fn build_with_dummy() -> Self {
let mut test_env = Self::build().await;
let dummy = dummy_data::add_dummy_data(&test_env).await;
test_env.dummy = Some(dummy);
test_env
}
pub async fn build() -> Self {
let db = TemporaryDatabase::create().await;
let labrinth_config = setup(&db).await;
let app = App::new().configure(|cfg| labrinth::app_config(cfg, labrinth_config.clone()));
let test_app = test::init_service(app).await;
Self {
test_app: Box::new(test_app),
db,
dummy: None,
}
}
pub async fn cleanup(self) {
self.db.cleanup().await;
}
pub async fn call(&self, req: actix_http::Request) -> ServiceResponse {
self.test_app.call(req).await.unwrap()
}
}
trait LocalService {
fn call(
&self,
req: actix_http::Request,
) -> std::pin::Pin<
Box<dyn std::future::Future<Output = Result<ServiceResponse, actix_web::Error>>>,
>;
}
impl<S> LocalService for S
where
S: actix_web::dev::Service<
actix_http::Request,
Response = ServiceResponse,
Error = actix_web::Error,
>,
S::Future: 'static,
{
fn call(
&self,
req: actix_http::Request,
) -> std::pin::Pin<
Box<dyn std::future::Future<Output = Result<ServiceResponse, actix_web::Error>>>,
> {
Box::pin(self.call(req))
}
}

40
tests/common/mod.rs Normal file
View File

@@ -0,0 +1,40 @@
use labrinth::{check_env_vars, clickhouse};
use labrinth::{file_hosting, queue, LabrinthConfig};
use std::sync::Arc;
use self::database::TemporaryDatabase;
pub mod actix;
pub mod database;
pub mod dummy_data;
pub mod environment;
pub mod pats;
pub mod scopes;
// Testing equivalent to 'setup' function, producing a LabrinthConfig
// If making a test, you should probably use environment::TestEnvironment::build_with_dummy() (which calls this)
pub async fn setup(db: &TemporaryDatabase) -> LabrinthConfig {
println!("Setting up labrinth config");
dotenvy::dotenv().ok();
if check_env_vars() {
println!("Some environment variables are missing!");
}
let pool = db.pool.clone();
let redis_pool = db.redis_pool.clone();
let file_host: Arc<dyn file_hosting::FileHost + Send + Sync> =
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());
labrinth::app_setup(
pool.clone(),
redis_pool.clone(),
&mut clickhouse,
file_host.clone(),
maxmind_reader.clone(),
)
}

30
tests/common/pats.rs Normal file
View File

@@ -0,0 +1,30 @@
#![allow(dead_code)]
use chrono::Utc;
use labrinth::{
database::{self, models::generate_pat_id},
models::pats::Scopes,
};
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 {
let mut transaction = db.pool.begin().await.unwrap();
let id = generate_pat_id(&mut transaction).await.unwrap();
let pat = database::models::pat_item::PersonalAccessToken {
id,
name: format!("test_pat_{}", scopes.bits()),
access_token: format!("mrp_{}", id.0),
scopes,
user_id: database::models::ids::UserId(user_id),
created: Utc::now(),
expires: Utc::now() + chrono::Duration::days(1),
last_used: None,
};
pat.insert(&mut transaction).await.unwrap();
transaction.commit().await.unwrap();
pat.access_token
}

124
tests/common/scopes.rs Normal file
View File

@@ -0,0 +1,124 @@
#![allow(dead_code)]
use actix_web::test::{self, TestRequest};
use labrinth::models::pats::Scopes;
use super::{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:
// - returns a known 'expected_failure_code' if the scope is not present (defaults to 401)
// - returns a 200-299 if the scope is present
// - returns failure and success JSON bodies for requests that are 200 (for performing non-simple follow-up tests on)
// This uses a builder format, so you can chain methods to set the parameters to non-defaults (most will probably be not need to be set).
pub struct ScopeTest<'a> {
test_env: &'a TestEnvironment,
// Scopes expected to fail on this test. By default, this is all scopes except the success scopes.
// (To ensure we have isolated the scope we are testing)
failure_scopes: Option<Scopes>,
// User ID to use for the PATs. By default, this is the USER_USER_ID_PARSED constant.
user_id: i64,
// The code that is expected to be returned if the scope is not present. By default, this is 401 (Unauthorized)
expected_failure_code: u16,
}
impl<'a> ScopeTest<'a> {
pub fn new(test_env: &'a TestEnvironment) -> Self {
Self {
test_env,
failure_scopes: None,
user_id: USER_USER_ID_PARSED,
expected_failure_code: 401,
}
}
// Set non-standard failure scopes
// If not set, it will be set to all scopes except the success scopes
// (eg: if a combination of scopes is needed, but you want to make sure that the endpoint does not work with all-but-one of them)
pub fn with_failure_scopes(mut self, scopes: Scopes) -> Self {
self.failure_scopes = Some(scopes);
self
}
// Set the user ID to use
// (eg: a moderator, or friend)
pub fn with_user_id(mut self, user_id: i64) -> Self {
self.user_id = user_id;
self
}
// If a non-401 code is expected.
// (eg: a 404 for a hidden resource, or 200 for a resource with hidden values deeper in)
pub fn with_failure_code(mut self, code: u16) -> Self {
self.expected_failure_code = code;
self
}
// Call the endpoint generated by req_gen twice, once with a PAT with the failure scopes, and once with the success scopes.
// success_scopes : the scopes that we are testing that should succeed
// returns a tuple of (failure_body, success_body)
// Should return a String error if on unexpected status code, allowing unwrapping in tests.
pub async fn test<T>(
&self,
req_gen: T,
success_scopes: Scopes,
) -> Result<(serde_json::Value, serde_json::Value), String>
where
T: Fn() -> TestRequest,
{
// First, create a PAT with failure scopes
let failure_scopes = self
.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 a PAT with the success scopes
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)
let req = req_gen()
.append_header(("Authorization", access_token_all_others.as_str()))
.to_request();
let resp = self.test_env.call(req).await;
if resp.status().as_u16() != self.expected_failure_code {
return Err(format!(
"Expected failure code {}, got {}",
self.expected_failure_code,
resp.status().as_u16()
));
}
let failure_body = if resp.status() == 200
&& resp.headers().contains_key("Content-Type")
&& resp.headers().get("Content-Type").unwrap() == "application/json"
{
test::read_body_json(resp).await
} else {
serde_json::Value::Null
};
// The second time, we expect a success code
let req = req_gen()
.append_header(("Authorization", access_token.as_str()))
.to_request();
let resp = self.test_env.call(req).await;
if !(resp.status().is_success() || resp.status().is_redirection()) {
return Err(format!(
"Expected success code, got {}",
resp.status().as_u16()
));
}
let success_body = if resp.status() == 200
&& resp.headers().contains_key("Content-Type")
&& resp.headers().get("Content-Type").unwrap() == "application/json"
{
test::read_body_json(resp).await
} else {
serde_json::Value::Null
};
Ok((failure_body, success_body))
}
}

BIN
tests/files/200x200.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 606 B

Binary file not shown.

BIN
tests/files/basic-mod.jar Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,36 @@
-- Dummy test data for use in tests.
-- IDs are listed as integers, followed by their equivalent base 62 representation.
-- Inserts 5 dummy users for testing, with slight differences
-- 'Friend' and 'enemy' function like 'user', but we can use them to simulate 'other' users that may or may not be able to access certain things
-- IDs 1-5, 1-5
INSERT INTO users (id, username, name, email, role) VALUES (1, 'admin', 'Administrator Test', 'admin@modrinth.com', 'admin');
INSERT INTO users (id, username, name, email, role) VALUES (2, 'moderator', 'Moderator Test', 'moderator@modrinth.com', 'moderator');
INSERT INTO users (id, username, name, email, role) VALUES (3, 'user', 'User Test', 'user@modrinth.com', 'developer');
INSERT INTO users (id, username, name, email, role) VALUES (4, 'friend', 'Friend Test', 'friend@modrinth.com', 'developer');
INSERT INTO users (id, username, name, email, role) VALUES (5, 'enemy', 'Enemy Test', 'enemy@modrinth.com', 'developer');
-- Full PATs for each user, with different scopes
-- These are not legal PATs, as they contain all scopes- they mimic permissions of a logged in user
-- IDs: 50-54, o p q r s
INSERT INTO pats (id, user_id, name, access_token, scopes, expires) VALUES (50, 1, 'admin-pat', 'mrp_patadmin', B'11111111111111111111111111111111111'::BIGINT, '2030-08-18 15:48:58.435729+00');
INSERT INTO pats (id, user_id, name, access_token, scopes, expires) VALUES (51, 2, 'moderator-pat', 'mrp_patmoderator', B'11111111111111111111111111111111111'::BIGINT, '2030-08-18 15:48:58.435729+00');
INSERT INTO pats (id, user_id, name, access_token, scopes, expires) VALUES (52, 3, 'user-pat', 'mrp_patuser', B'11111111111111111111111111111111111'::BIGINT, '2030-08-18 15:48:58.435729+00');
INSERT INTO pats (id, user_id, name, access_token, scopes, expires) VALUES (53, 4, 'friend-pat', 'mrp_patfriend', B'11111111111111111111111111111111111'::BIGINT, '2030-08-18 15:48:58.435729+00');
INSERT INTO pats (id, user_id, name, access_token, scopes, expires) VALUES (54, 5, 'enemy-pat', 'mrp_patenemy', B'11111111111111111111111111111111111'::BIGINT, '2030-08-18 15:48:58.435729+00');
-- -- Sample game versions, loaders, categories
INSERT INTO game_versions (id, version, type, created)
VALUES (20000, '1.20.1', 'release', timezone('utc', now()));
INSERT INTO loaders (id, loader) VALUES (1, 'fabric');
INSERT INTO loaders_project_types (joining_loader_id, joining_project_type_id) VALUES (1,1);
INSERT INTO loaders_project_types (joining_loader_id, joining_project_type_id) VALUES (1,2);
INSERT INTO categories (id, category, project_type) VALUES (1, 'combat', 1);
INSERT INTO categories (id, category, project_type) VALUES (2, 'decoration', 1);
INSERT INTO categories (id, category, project_type) VALUES (3, 'economy', 1);
INSERT INTO categories (id, category, project_type) VALUES (4, 'combat', 2);
INSERT INTO categories (id, category, project_type) VALUES (5, 'decoration', 2);
INSERT INTO categories (id, category, project_type) VALUES (6, 'economy', 2);

BIN
tests/files/simple-zip.zip Normal file

Binary file not shown.

292
tests/pats.rs Normal file
View File

@@ -0,0 +1,292 @@
use actix_web::test;
use chrono::{Duration, Utc};
use common::database::*;
use labrinth::models::pats::Scopes;
use serde_json::json;
use crate::common::environment::TestEnvironment;
// importing common module.
mod common;
// Full pat test:
// - create a PAT and ensure it can be used for the scope
// - ensure access token is not returned for any PAT in GET
// - ensure PAT can be patched to change scopes
// - ensure PAT can be patched to change expiry
// - ensure expired PATs cannot be used
// - ensure PATs can be deleted
#[actix_rt::test]
pub async fn pat_full_test() {
let test_env = TestEnvironment::build_with_dummy().await;
// Create a PAT for a full test
let req = test::TestRequest::post()
.uri("/v2/pat")
.append_header(("Authorization", USER_USER_PAT))
.set_json(json!({
"scopes": Scopes::COLLECTION_CREATE, // Collection create as an easily tested example
"name": "test_pat_scopes Test",
"expires": Utc::now() + Duration::days(1),
}))
.to_request();
let resp = test_env.call(req).await;
assert_eq!(resp.status().as_u16(), 200);
let success: serde_json::Value = test::read_body_json(resp).await;
let id = success["id"].as_str().unwrap();
// Has access token and correct scopes
assert!(success["access_token"].as_str().is_some());
assert_eq!(
success["scopes"].as_u64().unwrap(),
Scopes::COLLECTION_CREATE.bits()
);
let access_token = success["access_token"].as_str().unwrap();
// Get PAT again
let req = test::TestRequest::get()
.append_header(("Authorization", USER_USER_PAT))
.uri("/v2/pat")
.to_request();
let resp = test_env.call(req).await;
assert_eq!(resp.status().as_u16(), 200);
let success: serde_json::Value = test::read_body_json(resp).await;
// Ensure access token is NOT returned for any PATs
for pat in success.as_array().unwrap() {
assert!(pat["access_token"].as_str().is_none());
}
// Create mock test for using PAT
let mock_pat_test = |token: &str| {
let token = token.to_string();
async {
let req = test::TestRequest::post()
.uri("/v2/collection")
.append_header(("Authorization", token))
.set_json(json!({
"title": "Test Collection 1",
"description": "Test Collection Description"
}))
.to_request();
let resp = test_env.call(req).await;
resp.status().as_u16()
}
};
assert_eq!(mock_pat_test(access_token).await, 200);
// Change scopes and test again
let req = test::TestRequest::patch()
.uri(&format!("/v2/pat/{}", id))
.append_header(("Authorization", USER_USER_PAT))
.set_json(json!({
"scopes": 0,
}))
.to_request();
let resp = test_env.call(req).await;
assert_eq!(resp.status().as_u16(), 204);
assert_eq!(mock_pat_test(access_token).await, 401); // No longer works
// Change scopes back, and set expiry to the past, and test again
let req = test::TestRequest::patch()
.uri(&format!("/v2/pat/{}", id))
.append_header(("Authorization", USER_USER_PAT))
.set_json(json!({
"scopes": Scopes::COLLECTION_CREATE,
"expires": Utc::now() + Duration::seconds(1), // expires in 1 second
}))
.to_request();
let resp = test_env.call(req).await;
assert_eq!(resp.status().as_u16(), 204);
// Wait 1 second before testing again for expiry
tokio::time::sleep(Duration::seconds(1).to_std().unwrap()).await;
assert_eq!(mock_pat_test(access_token).await, 401); // No longer works
// Change everything back to normal and test again
let req = test::TestRequest::patch()
.uri(&format!("/v2/pat/{}", id))
.append_header(("Authorization", USER_USER_PAT))
.set_json(json!({
"expires": Utc::now() + Duration::days(1), // no longer expired!
}))
.to_request();
let resp = test_env.call(req).await;
assert_eq!(resp.status().as_u16(), 204);
assert_eq!(mock_pat_test(access_token).await, 200); // Works again
// Patching to a bad expiry should fail
let req = test::TestRequest::patch()
.uri(&format!("/v2/pat/{}", id))
.append_header(("Authorization", USER_USER_PAT))
.set_json(json!({
"expires": Utc::now() - Duration::days(1), // Past
}))
.to_request();
let resp = test_env.call(req).await;
assert_eq!(resp.status().as_u16(), 400);
// Similar to above with PAT creation, patching to a bad scope should fail
for i in 0..64 {
let scope = Scopes::from_bits_truncate(1 << i);
if !Scopes::all().contains(scope) {
continue;
}
let req = test::TestRequest::patch()
.uri(&format!("/v2/pat/{}", id))
.append_header(("Authorization", USER_USER_PAT))
.set_json(json!({
"scopes": scope.bits(),
}))
.to_request();
let resp = test_env.call(req).await;
assert_eq!(
resp.status().as_u16(),
if scope.is_restricted() { 400 } else { 204 }
);
}
// Delete PAT
let req = test::TestRequest::delete()
.append_header(("Authorization", USER_USER_PAT))
.uri(&format!("/v2/pat/{}", id))
.to_request();
let resp = test_env.call(req).await;
assert_eq!(resp.status().as_u16(), 204);
// Cleanup test db
test_env.cleanup().await;
}
// Test illegal PAT setting, both in POST and PATCH
#[actix_rt::test]
pub async fn bad_pats() {
let test_env = TestEnvironment::build_with_dummy().await;
// Creating a PAT with no name should fail
let req = test::TestRequest::post()
.uri("/v2/pat")
.append_header(("Authorization", USER_USER_PAT))
.set_json(json!({
"scopes": Scopes::COLLECTION_CREATE, // Collection create as an easily tested example
"expires": Utc::now() + Duration::days(1),
}))
.to_request();
let resp = test_env.call(req).await;
assert_eq!(resp.status().as_u16(), 400);
// Name too short or too long should fail
for name in ["n", "this_name_is_too_long".repeat(16).as_str()] {
let req = test::TestRequest::post()
.uri("/v2/pat")
.append_header(("Authorization", USER_USER_PAT))
.set_json(json!({
"name": name,
"scopes": Scopes::COLLECTION_CREATE, // Collection create as an easily tested example
"expires": Utc::now() + Duration::days(1),
}))
.to_request();
let resp = test_env.call(req).await;
assert_eq!(resp.status().as_u16(), 400);
}
// Creating a PAT with an expiry in the past should fail
let req = test::TestRequest::post()
.uri("/v2/pat")
.append_header(("Authorization", USER_USER_PAT))
.set_json(json!({
"scopes": Scopes::COLLECTION_CREATE, // Collection create as an easily tested example
"name": "test_pat_scopes Test",
"expires": Utc::now() - Duration::days(1),
}))
.to_request();
let resp = test_env.call(req).await;
assert_eq!(resp.status().as_u16(), 400);
// Make a PAT with each scope, with the result varying by whether that scope is restricted
for i in 0..64 {
let scope = Scopes::from_bits_truncate(1 << i);
if !Scopes::all().contains(scope) {
continue;
}
let req = test::TestRequest::post()
.uri("/v2/pat")
.append_header(("Authorization", USER_USER_PAT))
.set_json(json!({
"scopes": scope.bits(),
"name": format!("test_pat_scopes Name {}", i),
"expires": Utc::now() + Duration::days(1),
}))
.to_request();
let resp = test_env.call(req).await;
assert_eq!(
resp.status().as_u16(),
if scope.is_restricted() { 400 } else { 200 }
);
}
// Create a 'good' PAT for patching
let req = test::TestRequest::post()
.uri("/v2/pat")
.append_header(("Authorization", USER_USER_PAT))
.set_json(json!({
"scopes": Scopes::COLLECTION_CREATE,
"name": "test_pat_scopes Test",
"expires": Utc::now() + Duration::days(1),
}))
.to_request();
let resp = test_env.call(req).await;
assert_eq!(resp.status().as_u16(), 200);
let success: serde_json::Value = test::read_body_json(resp).await;
let id = success["id"].as_str().unwrap();
// Patching to a bad name should fail
for name in ["n", "this_name_is_too_long".repeat(16).as_str()] {
let req = test::TestRequest::post()
.uri("/v2/pat")
.append_header(("Authorization", USER_USER_PAT))
.set_json(json!({
"name": name,
}))
.to_request();
let resp = test_env.call(req).await;
assert_eq!(resp.status().as_u16(), 400);
}
// Patching to a bad expiry should fail
let req = test::TestRequest::patch()
.uri(&format!("/v2/pat/{}", id))
.append_header(("Authorization", USER_USER_PAT))
.set_json(json!({
"expires": Utc::now() - Duration::days(1), // Past
}))
.to_request();
let resp = test_env.call(req).await;
assert_eq!(resp.status().as_u16(), 400);
// Similar to above with PAT creation, patching to a bad scope should fail
for i in 0..64 {
let scope = Scopes::from_bits_truncate(1 << i);
if !Scopes::all().contains(scope) {
continue;
}
let req = test::TestRequest::patch()
.uri(&format!("/v2/pat/{}", id))
.append_header(("Authorization", USER_USER_PAT))
.set_json(json!({
"scopes": scope.bits(),
}))
.to_request();
let resp = test_env.call(req).await;
assert_eq!(
resp.status().as_u16(),
if scope.is_restricted() { 400 } else { 204 }
);
}
// Cleanup test db
test_env.cleanup().await;
}

461
tests/project.rs Normal file
View File

@@ -0,0 +1,461 @@
use actix_web::test;
use labrinth::database::models::project_item::{PROJECTS_NAMESPACE, PROJECTS_SLUGS_NAMESPACE};
use labrinth::models::ids::base62_impl::parse_base62;
use serde_json::json;
use crate::common::database::*;
use crate::common::{actix::AppendsMultipart, environment::TestEnvironment};
// importing common module.
mod common;
#[actix_rt::test]
async fn test_get_project() {
// Test setup and dummy data
let test_env = TestEnvironment::build_with_dummy().await;
let alpha_project_id = &test_env.dummy.as_ref().unwrap().alpha_project_id;
let beta_project_id = &test_env.dummy.as_ref().unwrap().beta_project_id;
let alpha_project_slug = &test_env.dummy.as_ref().unwrap().alpha_project_slug;
let alpha_version_id = &test_env.dummy.as_ref().unwrap().alpha_version_id;
// Perform request on dummy data
let req = test::TestRequest::get()
.uri(&format!("/v2/project/{alpha_project_id}"))
.append_header(("Authorization", USER_USER_PAT))
.to_request();
let resp = test_env.call(req).await;
let status = resp.status();
let body: serde_json::Value = test::read_body_json(resp).await;
assert_eq!(status, 200);
assert_eq!(body["id"], json!(alpha_project_id));
assert_eq!(body["slug"], json!(alpha_project_slug));
let versions = body["versions"].as_array().unwrap();
assert!(!versions.is_empty());
assert_eq!(versions[0], json!(alpha_version_id));
// Confirm that the request was cached
assert_eq!(
test_env
.db
.redis_pool
.get::<i64, _>(PROJECTS_SLUGS_NAMESPACE, alpha_project_slug)
.await
.unwrap(),
Some(parse_base62(alpha_project_id).unwrap() as i64)
);
let cached_project = test_env
.db
.redis_pool
.get::<String, _>(PROJECTS_NAMESPACE, parse_base62(alpha_project_id).unwrap())
.await
.unwrap()
.unwrap();
let cached_project: serde_json::Value = serde_json::from_str(&cached_project).unwrap();
assert_eq!(cached_project["inner"]["slug"], json!(alpha_project_slug));
// Make the request again, this time it should be cached
let req = test::TestRequest::get()
.uri(&format!("/v2/project/{alpha_project_id}"))
.append_header(("Authorization", USER_USER_PAT))
.to_request();
let resp = test_env.call(req).await;
let status = resp.status();
assert_eq!(status, 200);
let body: serde_json::Value = test::read_body_json(resp).await;
assert_eq!(body["id"], json!(alpha_project_id));
assert_eq!(body["slug"], json!(alpha_project_slug));
// Request should fail on non-existent project
let req = test::TestRequest::get()
.uri("/v2/project/nonexistent")
.append_header(("Authorization", USER_USER_PAT))
.to_request();
let resp = test_env.call(req).await;
assert_eq!(resp.status(), 404);
// Similarly, request should fail on non-authorized user, on a yet-to-be-approved or hidden project, with a 404 (hiding the existence of the project)
let req = test::TestRequest::get()
.uri(&format!("/v2/project/{beta_project_id}"))
.append_header(("Authorization", ENEMY_USER_PAT))
.to_request();
let resp = test_env.call(req).await;
assert_eq!(resp.status(), 404);
// Cleanup test db
test_env.cleanup().await;
}
#[actix_rt::test]
async fn test_add_remove_project() {
// Test setup and dummy data
let test_env = TestEnvironment::build_with_dummy().await;
// Generate test project data.
let mut json_data = json!(
{
"title": "Test_Add_Project project",
"slug": "demo",
"description": "Example description.",
"body": "Example body.",
"client_side": "required",
"server_side": "optional",
"initial_versions": [{
"file_parts": ["basic-mod.jar"],
"version_number": "1.2.3",
"version_title": "start",
"dependencies": [],
"game_versions": ["1.20.1"] ,
"release_channel": "release",
"loaders": ["fabric"],
"featured": true
}],
"categories": [],
"license_id": "MIT"
}
);
// Basic json
let json_segment = common::actix::MultipartSegment {
name: "data".to_string(),
filename: None,
content_type: Some("application/json".to_string()),
data: common::actix::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 = common::actix::MultipartSegment {
data: common::actix::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 = common::actix::MultipartSegment {
data: common::actix::MultipartSegmentData::Text(serde_json::to_string(&json_data).unwrap()),
..json_segment.clone()
};
// Basic file
let file_segment = common::actix::MultipartSegment {
name: "basic-mod.jar".to_string(),
filename: Some("basic-mod.jar".to_string()),
content_type: Some("application/java-archive".to_string()),
data: common::actix::MultipartSegmentData::Binary(
include_bytes!("../tests/files/basic-mod.jar").to_vec(),
),
};
// Differently named file, with the same content (for hash testing)
let file_diff_name_segment = common::actix::MultipartSegment {
name: "basic-mod-different.jar".to_string(),
filename: Some("basic-mod-different.jar".to_string()),
content_type: Some("application/java-archive".to_string()),
data: common::actix::MultipartSegmentData::Binary(
include_bytes!("../tests/files/basic-mod.jar").to_vec(),
),
};
// Differently named file, with different content
let file_diff_name_content_segment = common::actix::MultipartSegment {
name: "basic-mod-different.jar".to_string(),
filename: Some("basic-mod-different.jar".to_string()),
content_type: Some("application/java-archive".to_string()),
data: common::actix::MultipartSegmentData::Binary(
include_bytes!("../tests/files/basic-mod-different.jar").to_vec(),
),
};
// Add a project- simple, should work.
let req = test::TestRequest::post()
.uri("/v2/project")
.append_header(("Authorization", USER_USER_PAT))
.set_multipart(vec![json_segment.clone(), file_segment.clone()])
.to_request();
let resp = test_env.call(req).await;
let status = resp.status();
assert_eq!(status, 200);
// Get the project we just made, and confirm that it's correct
let req = test::TestRequest::get()
.uri("/v2/project/demo")
.append_header(("Authorization", USER_USER_PAT))
.to_request();
let resp = test_env.call(req).await;
assert_eq!(resp.status(), 200);
let body: serde_json::Value = test::read_body_json(resp).await;
let versions = body["versions"].as_array().unwrap();
assert!(versions.len() == 1);
let uploaded_version_id = &versions[0];
// Checks files to ensure they were uploaded and correctly identify the file
let hash = sha1::Sha1::from(include_bytes!("../tests/files/basic-mod.jar"))
.digest()
.to_string();
let req = test::TestRequest::get()
.uri(&format!("/v2/version_file/{hash}?algorithm=sha1"))
.append_header(("Authorization", USER_USER_PAT))
.to_request();
let resp = test_env.call(req).await;
assert_eq!(resp.status(), 200);
let body: serde_json::Value = test::read_body_json(resp).await;
let file_version_id = &body["id"];
assert_eq!(&file_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_header(("Authorization", 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_eq!(resp.status(), 400);
// Reusing with the same slug and a different file should fail
let req = test::TestRequest::post()
.uri("/v2/project")
.append_header(("Authorization", 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_eq!(resp.status(), 400);
// Different slug, different file should succeed
let req = test::TestRequest::post()
.uri("/v2/project")
.append_header(("Authorization", 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_eq!(resp.status(), 200);
// Get
let req = test::TestRequest::get()
.uri("/v2/project/demo")
.append_header(("Authorization", USER_USER_PAT))
.to_request();
let resp = test_env.call(req).await;
assert_eq!(resp.status(), 200);
let body: serde_json::Value = test::read_body_json(resp).await;
let id = body["id"].to_string();
// Remove the project
let req = test::TestRequest::delete()
.uri("/v2/project/demo")
.append_header(("Authorization", USER_USER_PAT))
.to_request();
let resp = test_env.call(req).await;
assert_eq!(resp.status(), 204);
// Confirm that the project is gone from the cache
assert_eq!(
test_env
.db
.redis_pool
.get::<i64, _>(PROJECTS_SLUGS_NAMESPACE, "demo")
.await
.unwrap(),
None
);
assert_eq!(
test_env
.db
.redis_pool
.get::<i64, _>(PROJECTS_SLUGS_NAMESPACE, id)
.await
.unwrap(),
None
);
// Old slug no longer works
let req = test::TestRequest::get()
.uri("/v2/project/demo")
.append_header(("Authorization", USER_USER_PAT))
.to_request();
let resp = test_env.call(req).await;
assert_eq!(resp.status(), 404);
// Cleanup test db
test_env.cleanup().await;
}
#[actix_rt::test]
pub async fn test_patch_project() {
let test_env = TestEnvironment::build_with_dummy().await;
let alpha_project_slug = &test_env.dummy.as_ref().unwrap().alpha_project_slug;
let beta_project_slug = &test_env.dummy.as_ref().unwrap().beta_project_slug;
// First, we do some patch requests that should fail.
// Failure because the user is not authorized.
let req = test::TestRequest::patch()
.uri(&format!("/v2/project/{alpha_project_slug}"))
.append_header(("Authorization", ENEMY_USER_PAT))
.set_json(json!({
"title": "Test_Add_Project project - test 1",
}))
.to_request();
let resp = test_env.call(req).await;
assert_eq!(resp.status(), 401);
// Failure because we are setting URL fields to invalid urls.
for url_type in ["issues_url", "source_url", "wiki_url", "discord_url"] {
let req = test::TestRequest::patch()
.uri(&format!("/v2/project/{alpha_project_slug}"))
.append_header(("Authorization", USER_USER_PAT))
.set_json(json!({
url_type: "w.fake.url",
}))
.to_request();
let resp = test_env.call(req).await;
assert_eq!(resp.status(), 400);
}
// Failure because these are illegal requested statuses for a normal user.
for req in ["unknown", "processing", "withheld", "scheduled"] {
let req = test::TestRequest::patch()
.uri(&format!("/v2/project/{alpha_project_slug}"))
.append_header(("Authorization", USER_USER_PAT))
.set_json(json!({
"requested_status": req,
}))
.to_request();
let resp = test_env.call(req).await;
assert_eq!(resp.status(), 400);
}
// Failure because these should not be able to be set by a non-mod
for key in ["moderation_message", "moderation_message_body"] {
let req = test::TestRequest::patch()
.uri(&format!("/v2/project/{alpha_project_slug}"))
.append_header(("Authorization", USER_USER_PAT))
.set_json(json!({
key: "test",
}))
.to_request();
let resp = test_env.call(req).await;
assert_eq!(resp.status(), 401);
// (should work for a mod, though)
let req = test::TestRequest::patch()
.uri(&format!("/v2/project/{alpha_project_slug}"))
.append_header(("Authorization", MOD_USER_PAT))
.set_json(json!({
key: "test",
}))
.to_request();
let resp = test_env.call(req).await;
assert_eq!(resp.status(), 204);
}
// Failure because the slug is already taken.
let req = test::TestRequest::patch()
.uri(&format!("/v2/project/{alpha_project_slug}"))
.append_header(("Authorization", USER_USER_PAT))
.set_json(json!({
"slug": beta_project_slug, // the other dummy project has this slug
}))
.to_request();
let resp = test_env.call(req).await;
assert_eq!(resp.status(), 400);
// Not allowed to directly set status, as 'beta_project_slug' (the other project) is "processing" and cannot have its status changed like this.
let req = test::TestRequest::patch()
.uri(&format!("/v2/project/{beta_project_slug}"))
.append_header(("Authorization", USER_USER_PAT))
.set_json(json!({
"status": "private"
}))
.to_request();
let resp = test_env.call(req).await;
assert_eq!(resp.status(), 401);
// Sucessful request to patch many fields.
let req = test::TestRequest::patch()
.uri(&format!("/v2/project/{alpha_project_slug}"))
.append_header(("Authorization", USER_USER_PAT))
.set_json(json!({
"slug": "newslug",
"title": "New successful title",
"description": "New successful description",
"body": "New successful body",
"categories": ["combat"],
"license_id": "MIT",
"issues_url": "https://github.com",
"discord_url": "https://discord.gg",
"wiki_url": "https://wiki.com",
"client_side": "optional",
"server_side": "required",
"donation_urls": [{
"id": "patreon",
"platform": "Patreon",
"url": "https://patreon.com"
}]
}))
.to_request();
let resp = test_env.call(req).await;
assert_eq!(resp.status(), 204);
// Old slug no longer works
let req = test::TestRequest::get()
.uri(&format!("/v2/project/{alpha_project_slug}"))
.append_header(("Authorization", USER_USER_PAT))
.to_request();
let resp = test_env.call(req).await;
assert_eq!(resp.status(), 404);
// Old slug no longer works
let req = test::TestRequest::get()
.uri("/v2/project/newslug")
.append_header(("Authorization", USER_USER_PAT))
.to_request();
let resp = test_env.call(req).await;
assert_eq!(resp.status(), 200);
let body: serde_json::Value = test::read_body_json(resp).await;
assert_eq!(body["slug"], json!("newslug"));
assert_eq!(body["title"], json!("New successful title"));
assert_eq!(body["description"], json!("New successful description"));
assert_eq!(body["body"], json!("New successful body"));
assert_eq!(body["categories"], json!(["combat"]));
assert_eq!(body["license"]["id"], json!("MIT"));
assert_eq!(body["issues_url"], json!("https://github.com"));
assert_eq!(body["discord_url"], json!("https://discord.gg"));
assert_eq!(body["wiki_url"], json!("https://wiki.com"));
assert_eq!(body["client_side"], json!("optional"));
assert_eq!(body["server_side"], json!("required"));
assert_eq!(
body["donation_urls"][0]["url"],
json!("https://patreon.com")
);
// Cleanup test db
test_env.cleanup().await;
}
// TODO: Missing routes on projects
// TODO: using permissions/scopes, can we SEE projects existence that we are not allowed to? (ie 401 instead of 404)

1331
tests/scopes.rs Normal file

File diff suppressed because it is too large Load Diff