diff --git a/Cargo.lock b/Cargo.lock index fc2b10ae..f87d9e0f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -444,6 +444,12 @@ version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341" +[[package]] +name = "assert_matches" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9" + [[package]] name = "async-channel" version = "1.9.0" @@ -2244,6 +2250,7 @@ dependencies = [ "actix-web-prom", "actix-ws", "argon2", + "assert_matches", "async-trait", "base64 0.21.4", "bitflags 2.4.0", diff --git a/Cargo.toml b/Cargo.toml index 89ca5c31..471fc0e9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -109,3 +109,4 @@ derive-new = "0.5.9" [dev-dependencies] actix-http = "3.4.0" +assert_matches = "1.5.0" diff --git a/src/models/feed_item.rs b/src/models/feed_item.rs index f1cee015..b1557e52 100644 --- a/src/models/feed_item.rs +++ b/src/models/feed_item.rs @@ -12,11 +12,11 @@ use crate::database::models::event_item as DBEvent; #[serde(into = "Base62Id")] pub struct FeedItemId(pub u64); -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Debug)] #[serde(tag = "type", rename_all = "snake_case")] pub enum CreatorId { - User(UserId), - Organization(OrganizationId), + User { id: UserId }, + Organization { id: OrganizationId }, } #[derive(Serialize, Deserialize)] @@ -26,7 +26,7 @@ pub struct FeedItem { pub time: DateTime, } -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Debug)] #[serde(tag = "type", rename_all = "snake_case")] pub enum FeedItemBody { ProjectCreated { @@ -39,10 +39,10 @@ pub enum FeedItemBody { impl From for CreatorId { fn from(value: crate::database::models::event_item::CreatorId) -> Self { match value { - DBEvent::CreatorId::User(user_id) => CreatorId::User(user_id.into()), - DBEvent::CreatorId::Organization(organization_id) => { - CreatorId::Organization(organization_id.into()) - } + DBEvent::CreatorId::User(user_id) => CreatorId::User { id: user_id.into() }, + DBEvent::CreatorId::Organization(organization_id) => CreatorId::Organization { + id: organization_id.into(), + }, } } } diff --git a/src/models/organizations.rs b/src/models/organizations.rs index 6163ddee..b583aced 100644 --- a/src/models/organizations.rs +++ b/src/models/organizations.rs @@ -5,7 +5,7 @@ use super::{ use serde::{Deserialize, Serialize}; /// The ID of a team -#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Debug)] #[serde(from = "Base62Id")] #[serde(into = "Base62Id")] pub struct OrganizationId(pub u64); diff --git a/src/routes/v3/mod.rs b/src/routes/v3/mod.rs index bd053a25..7709aff3 100644 --- a/src/routes/v3/mod.rs +++ b/src/routes/v3/mod.rs @@ -13,9 +13,9 @@ pub fn config(cfg: &mut web::ServiceConfig) { web::scope("v3") .wrap(default_cors()) .route("", web::get().to(hello_world)) - .configure(users::config) .configure(oauth::config) .configure(oauth_clients::config) + .configure(users::config) .configure(organizations::config), ); } diff --git a/src/routes/v3/users.rs b/src/routes/v3/users.rs index 1dcd3f70..951aceaa 100644 --- a/src/routes/v3/users.rs +++ b/src/routes/v3/users.rs @@ -36,7 +36,8 @@ pub fn config(cfg: &mut web::ServiceConfig) { cfg.service( web::scope("user") .service(user_follow) - .service(user_unfollow), + .service(user_unfollow) + .service(current_user_feed), ); } diff --git a/tests/common/actix.rs b/tests/common/actix.rs index 11759d7f..e8c11f90 100644 --- a/tests/common/actix.rs +++ b/tests/common/actix.rs @@ -80,3 +80,13 @@ fn generate_multipart(data: impl IntoIterator) -> (Stri (boundary, Bytes::from(payload)) } + +pub trait TestRequestExtensions { + fn append_auth(self, pat: &str) -> TestRequest; +} + +impl TestRequestExtensions for TestRequest { + fn append_auth(self, pat: &str) -> TestRequest { + self.append_header((reqwest::header::AUTHORIZATION, pat)) + } +} diff --git a/tests/common/api_v2/organization.rs b/tests/common/api_v2/organization.rs index 31f0ea4c..99394bf1 100644 --- a/tests/common/api_v2/organization.rs +++ b/tests/common/api_v2/organization.rs @@ -1,3 +1,4 @@ +use actix_http::StatusCode; use actix_web::{ dev::ServiceResponse, test::{self, TestRequest}, @@ -6,7 +7,7 @@ use bytes::Bytes; use labrinth::models::{organizations::Organization, projects::Project}; use serde_json::json; -use crate::common::request_data::ImageData; +use crate::common::{asserts::assert_status, request_data::ImageData}; use super::ApiV2; @@ -42,8 +43,7 @@ impl ApiV2 { pat: &str, ) -> Organization { let resp = self.get_organization(id_or_title, pat).await; - assert_eq!(resp.status(), 200); - test::read_body_json(resp).await + deser_organization(resp).await } pub async fn get_organization_projects(&self, id_or_title: &str, pat: &str) -> ServiceResponse { @@ -150,3 +150,8 @@ impl ApiV2 { self.call(req).await } } + +pub async fn deser_organization(response: ServiceResponse) -> Organization { + assert_status(&response, StatusCode::OK); + test::read_body_json(response).await +} diff --git a/tests/common/api_v3/mod.rs b/tests/common/api_v3/mod.rs index 2155aa3c..f4e08f34 100644 --- a/tests/common/api_v3/mod.rs +++ b/tests/common/api_v3/mod.rs @@ -6,6 +6,8 @@ use std::rc::Rc; pub mod oauth; pub mod oauth_clients; +pub mod organization; +pub mod user; #[derive(Clone)] pub struct ApiV3 { diff --git a/tests/common/api_v3/organization.rs b/tests/common/api_v3/organization.rs new file mode 100644 index 00000000..700144c7 --- /dev/null +++ b/tests/common/api_v3/organization.rs @@ -0,0 +1,25 @@ +use actix_web::{dev::ServiceResponse, test::TestRequest}; + +use crate::common::actix::TestRequestExtensions; + +use super::ApiV3; + +impl ApiV3 { + pub async fn follow_organization(&self, organization_id: &str, pat: &str) -> ServiceResponse { + let req = TestRequest::post() + .uri(&format!("/v3/organization/{}/follow", organization_id)) + .append_auth(pat) + .to_request(); + + self.call(req).await + } + + pub async fn unfollow_organization(&self, organization_id: &str, pat: &str) -> ServiceResponse { + let req = TestRequest::delete() + .uri(&format!("/v3/organization/{}/follow", organization_id)) + .append_auth(pat) + .to_request(); + + self.call(req).await + } +} diff --git a/tests/common/api_v3/user.rs b/tests/common/api_v3/user.rs new file mode 100644 index 00000000..2574c4f5 --- /dev/null +++ b/tests/common/api_v3/user.rs @@ -0,0 +1,41 @@ +use actix_http::StatusCode; +use actix_web::{ + dev::ServiceResponse, + test::{self, TestRequest}, +}; +use labrinth::models::feed_item::FeedItem; + +use crate::common::{actix::TestRequestExtensions, asserts::assert_status}; + +use super::ApiV3; + +impl ApiV3 { + pub async fn follow_user(&self, user_id: &str, pat: &str) -> ServiceResponse { + let req = TestRequest::post() + .uri(&format!("/v3/user/{}/follow", user_id)) + .append_auth(pat) + .to_request(); + + self.call(req).await + } + + pub async fn unfollow_user(&self, user_id: &str, pat: &str) -> ServiceResponse { + let req = TestRequest::delete() + .uri(&format!("/v3/user/{}/follow", user_id)) + .append_auth(pat) + .to_request(); + + self.call(req).await + } + + pub async fn get_feed(&self, pat: &str) -> Vec { + let req = TestRequest::get() + .uri("/v3/user/feed") + .append_auth(pat) + .to_request(); + let resp = self.call(req).await; + assert_status(&resp, StatusCode::OK); + + test::read_body_json(resp).await + } +} diff --git a/tests/common/dummy_data.rs b/tests/common/dummy_data.rs index 2ed669df..8e98901a 100644 --- a/tests/common/dummy_data.rs +++ b/tests/common/dummy_data.rs @@ -19,7 +19,7 @@ use super::{ request_data::get_public_project_creation_data, }; -pub const DUMMY_DATA_UPDATE: i64 = 2; +pub const DUMMY_DATA_UPDATE: i64 = 3; #[allow(dead_code)] pub const DUMMY_CATEGORIES: &[&str] = &[ @@ -212,7 +212,7 @@ pub async fn add_project_alpha(test_env: &TestEnvironment) -> (Project, Version) let (project, versions) = test_env .v2 .add_public_project( - get_public_project_creation_data("alpha", Some(DummyJarFile::DummyProjectAlpha)), + get_public_project_creation_data("alpha", Some(DummyJarFile::DummyProjectAlpha), None), USER_USER_PAT, ) .await; diff --git a/tests/common/permissions.rs b/tests/common/permissions.rs index 4ab33900..58dda70b 100644 --- a/tests/common/permissions.rs +++ b/tests/common/permissions.rs @@ -849,7 +849,7 @@ async fn create_dummy_project(test_env: &TestEnvironment) -> (String, String) { // Create a very simple project let slug = generate_random_name("test_project"); - let creation_data = request_data::get_public_project_creation_data(&slug, None); + let creation_data = request_data::get_public_project_creation_data(&slug, None, None); let (project, _) = api.add_public_project(creation_data, ADMIN_USER_PAT).await; let project_id = project.id.to_string(); let team_id = project.team.to_string(); diff --git a/tests/common/request_data.rs b/tests/common/request_data.rs index bd5eb284..20167e44 100644 --- a/tests/common/request_data.rs +++ b/tests/common/request_data.rs @@ -22,6 +22,7 @@ pub struct ImageData { pub fn get_public_project_creation_data( slug: &str, version_jar: Option, + organization_id: Option<&str>, ) -> ProjectCreationRequestData { let initial_versions = if let Some(ref jar) = version_jar { json!([{ @@ -51,7 +52,8 @@ pub fn get_public_project_creation_data( "initial_versions": initial_versions, "is_draft": is_draft, "categories": [], - "license_id": "MIT" + "license_id": "MIT", + "organization_id": organization_id } ); diff --git a/tests/feed.rs b/tests/feed.rs new file mode 100644 index 00000000..d7addd3a --- /dev/null +++ b/tests/feed.rs @@ -0,0 +1,71 @@ +use assert_matches::assert_matches; +use common::{ + api_v2::organization::deser_organization, + database::{FRIEND_USER_PAT, USER_USER_ID, USER_USER_PAT}, + environment::with_test_environment, + request_data::get_public_project_creation_data, +}; +use labrinth::models::feed_item::FeedItemBody; + +use crate::common::dummy_data::DummyProjectAlpha; + +mod common; + +#[actix_rt::test] +async fn user_feed_before_following_user_shows_no_projects() { + with_test_environment(|env| async move { + let feed = env.v3.get_feed(FRIEND_USER_PAT).await; + + assert_eq!(feed.len(), 0); + }) + .await +} + +#[actix_rt::test] +async fn user_feed_after_following_user_shows_previously_created_public_projects() { + with_test_environment(|env| async move { + let DummyProjectAlpha { + project_id: alpha_project_id, + .. + } = env.dummy.as_ref().unwrap().project_alpha.clone(); + env.v3.follow_user(USER_USER_ID, FRIEND_USER_PAT).await; + + let feed = env.v3.get_feed(FRIEND_USER_PAT).await; + + assert_eq!(feed.len(), 1); + assert_matches!( + feed[0].body, + FeedItemBody::ProjectCreated { project_id, .. } if project_id.to_string() == alpha_project_id + ) + }) + .await +} + +#[actix_rt::test] +async fn user_feed_when_following_user_that_creates_project_as_org_only_shows_event_when_following_org( +) { + with_test_environment(|env| async move { + let resp = env + .v2 + .create_organization("test", "desc", USER_USER_ID) + .await; + let organization = deser_organization(resp).await; + let org_id = organization.id.to_string(); + let project_create_data = get_public_project_creation_data("a", None, Some(&org_id)); + let (project, _) = env + .v2 + .add_public_project(project_create_data, USER_USER_PAT) + .await; + + env.v3.follow_user(USER_USER_ID, FRIEND_USER_PAT).await; + let feed = env.v3.get_feed(FRIEND_USER_PAT).await; + assert_eq!(feed.len(), 1); + assert_matches!(feed[0].body, FeedItemBody::ProjectCreated { project_id, .. } if project_id != project.id); + + env.v3.follow_organization(&org_id, FRIEND_USER_PAT).await; + let feed = env.v3.get_feed(FRIEND_USER_PAT).await; + assert_eq!(feed.len(), 1); + assert_matches!(feed[0].body, FeedItemBody::ProjectCreated { project_id, .. } if project_id == project.id); + }) + .await; +}