diff --git a/api_tests/src/comment.spec.ts b/api_tests/src/comment.spec.ts index c18469e33..56d54480a 100644 --- a/api_tests/src/comment.spec.ts +++ b/api_tests/src/comment.spec.ts @@ -158,16 +158,16 @@ test("Delete a comment", async () => { expect(deleteCommentRes.comment_view.comment.deleted).toBe(true); expect(deleteCommentRes.comment_view.comment.content).toBe(""); - // Make sure that comment is undefined on beta + // Make sure that comment is deleted on beta await waitUntil( - () => resolveComment(beta, commentRes.comment_view.comment).catch(e => e), - e => e.message == "not_found", + () => resolveComment(beta, commentRes.comment_view.comment), + c => c.comment?.comment.deleted === true, ); - // Make sure that comment is undefined on gamma after delete + // Make sure that comment is deleted on gamma after delete await waitUntil( - () => resolveComment(gamma, commentRes.comment_view.comment).catch(e => e), - e => e.message === "not_found", + () => resolveComment(gamma, commentRes.comment_view.comment), + c => c.comment?.comment.deleted === true, ); // Test undeleting the comment @@ -181,11 +181,10 @@ test("Delete a comment", async () => { // Make sure that comment is undeleted on beta let betaComment2 = ( await waitUntil( - () => resolveComment(beta, commentRes.comment_view.comment).catch(e => e), - e => e.message !== "not_found", + () => resolveComment(beta, commentRes.comment_view.comment), + c => c.comment?.comment.deleted === false, ) ).comment; - expect(betaComment2?.comment.deleted).toBe(false); assertCommentFederation(betaComment2, undeleteCommentRes.comment_view); }); diff --git a/crates/apub/src/api/resolve_object.rs b/crates/apub/src/api/resolve_object.rs index b3061d1ce..5dc172d56 100644 --- a/crates/apub/src/api/resolve_object.rs +++ b/crates/apub/src/api/resolve_object.rs @@ -4,7 +4,6 @@ use crate::fetcher::{ }; use activitypub_federation::config::Data; use actix_web::web::{Json, Query}; -use diesel::NotFound; use lemmy_api_common::{ context::LemmyContext, site::{ResolveObject, ResolveObjectResponse}, @@ -47,34 +46,159 @@ async fn convert_response( pool: &mut DbPool<'_>, ) -> LemmyResult> { use SearchableObjects::*; - let removed_or_deleted; let mut res = ResolveObjectResponse::default(); let local_user = local_user_view.map(|l| l.local_user); + let is_admin = local_user.clone().map(|l| l.admin).unwrap_or_default(); match object { - Post(p) => { - removed_or_deleted = p.deleted || p.removed; - res.post = Some(PostView::read(pool, p.id, local_user.as_ref(), false).await?) - } - Comment(c) => { - removed_or_deleted = c.deleted || c.removed; - res.comment = Some(CommentView::read(pool, c.id, local_user.as_ref()).await?) - } + Post(p) => res.post = Some(PostView::read(pool, p.id, local_user.as_ref(), is_admin).await?), + Comment(c) => res.comment = Some(CommentView::read(pool, c.id, local_user.as_ref()).await?), PersonOrCommunity(p) => match *p { - UserOrCommunity::User(u) => { - removed_or_deleted = u.deleted; - res.person = Some(PersonView::read(pool, u.id).await?) - } + UserOrCommunity::User(u) => res.person = Some(PersonView::read(pool, u.id).await?), UserOrCommunity::Community(c) => { - removed_or_deleted = c.deleted || c.removed; - res.community = Some(CommunityView::read(pool, c.id, local_user.as_ref(), false).await?) + res.community = Some(CommunityView::read(pool, c.id, local_user.as_ref(), is_admin).await?) } }, }; - // if the object was deleted from database, dont return it - if removed_or_deleted { - Err(NotFound {}.into()) - } else { - Ok(Json(res)) + + Ok(Json(res)) +} + +#[cfg(test)] +mod tests { + use crate::api::resolve_object::resolve_object; + use activitypub_federation::config::Data; + use actix_web::web::Query; + use lemmy_api_common::{context::LemmyContext, site::ResolveObject}; + use lemmy_db_schema::{ + newtypes::InstanceId, + source::{ + community::{Community, CommunityInsertForm}, + instance::Instance, + local_site::{LocalSite, LocalSiteInsertForm}, + local_user::{LocalUser, LocalUserInsertForm}, + person::{Person, PersonInsertForm}, + post::{Post, PostInsertForm, PostUpdateForm}, + site::{Site, SiteInsertForm}, + }, + traits::Crud, + }; + use lemmy_db_views::structs::LocalUserView; + use lemmy_utils::{error::LemmyResult, LemmyErrorType}; + use serial_test::serial; + + async fn create_user( + instance_id: InstanceId, + name: String, + admin: bool, + context: &Data, + ) -> LemmyResult { + let person_form = PersonInsertForm::test_form(instance_id, &name); + let person = Person::create(&mut context.pool(), &person_form).await?; + + let user_form = match admin { + true => LocalUserInsertForm::test_form_admin(person.id), + false => LocalUserInsertForm::test_form(person.id), + }; + let local_user = LocalUser::create(&mut context.pool(), &user_form, vec![]).await?; + + Ok(LocalUserView::read(&mut context.pool(), local_user.id).await?) + } + + #[tokio::test] + #[serial] + #[expect(clippy::unwrap_used)] + async fn test_object_visibility() -> LemmyResult<()> { + let context = LemmyContext::init_test_context().await; + + let instance = Instance::read_or_create(&mut context.pool(), "example.com".to_string()).await?; + + let site_form = SiteInsertForm::new("test site".to_string(), instance.id); + let site = Site::create(&mut context.pool(), &site_form).await?; + + let local_site_form = LocalSiteInsertForm { + site_setup: Some(true), + private_instance: Some(false), + ..LocalSiteInsertForm::new(site.id) + }; + LocalSite::create(&mut context.pool(), &local_site_form).await?; + + let creator = create_user(instance.id, "creator".to_string(), false, &context).await?; + let regular_user = create_user(instance.id, "user".to_string(), false, &context).await?; + let admin_user = create_user(instance.id, "admin".to_string(), true, &context).await?; + + //let community_insert_form = ; + let community = Community::create( + &mut context.pool(), + &CommunityInsertForm::new( + instance.id, + "test".to_string(), + "test".to_string(), + "pubkey".to_string(), + ), + ) + .await?; + + let post_insert_form = PostInsertForm::new("Test".to_string(), creator.person.id, community.id); + let post = Post::create(&mut context.pool(), &post_insert_form).await?; + + let query = format!("q={}", post.ap_id).to_string(); + let query: Query = Query::from_query(&query)?; + + // Objects should be resolvable without authentication + let res = resolve_object(query.clone(), context.reset_request_count(), None).await?; + assert_eq!(res.post.as_ref().unwrap().post.ap_id, post.ap_id); + // Objects should be resolvable by regular users + let res = resolve_object( + query.clone(), + context.reset_request_count(), + Some(regular_user.clone()), + ) + .await?; + assert_eq!(res.post.as_ref().unwrap().post.ap_id, post.ap_id); + // Objects should be resolvable by admins + let res = resolve_object( + query.clone(), + context.reset_request_count(), + Some(admin_user.clone()), + ) + .await?; + assert_eq!(res.post.as_ref().unwrap().post.ap_id, post.ap_id); + + Post::update( + &mut context.pool(), + post.id, + &PostUpdateForm { + deleted: Some(true), + ..Default::default() + }, + ) + .await?; + + // Deleted objects should not be resolvable without authentication + let res = resolve_object(query.clone(), context.reset_request_count(), None).await; + assert!(res.is_err_and(|e| e.error_type == LemmyErrorType::NotFound)); + // Deleted objects should not be resolvable by regular users + let res = resolve_object( + query.clone(), + context.reset_request_count(), + Some(regular_user.clone()), + ) + .await; + assert!(res.is_err_and(|e| e.error_type == LemmyErrorType::NotFound)); + // Deleted objects should be resolvable by admins + let res = resolve_object( + query.clone(), + context.reset_request_count(), + Some(admin_user.clone()), + ) + .await?; + assert_eq!(res.post.as_ref().unwrap().post.ap_id, post.ap_id); + + LocalSite::delete(&mut context.pool()).await?; + Site::delete(&mut context.pool(), site.id).await?; + Instance::delete(&mut context.pool(), instance.id).await?; + + Ok(()) } }