]> Untitled Git - lemmy.git/blob - crates/db_views/src/comment_view.rs
fix tests
[lemmy.git] / crates / db_views / src / comment_view.rs
1 use crate::structs::CommentView;
2 use diesel::{dsl::*, result::Error, *};
3 use diesel_ltree::{nlevel, subpath, Ltree, LtreeExtensions};
4 use lemmy_db_schema::{
5   aggregates::structs::CommentAggregates,
6   newtypes::{CommentId, CommunityId, DbUrl, LocalUserId, PersonId, PostId},
7   schema::{
8     comment,
9     comment_aggregates,
10     comment_like,
11     comment_saved,
12     community,
13     community_block,
14     community_follower,
15     community_person_ban,
16     language,
17     local_user_language,
18     person,
19     person_block,
20     post,
21   },
22   source::{
23     comment::{Comment, CommentSaved},
24     community::{Community, CommunityFollower, CommunityPersonBan, CommunitySafe},
25     local_user::LocalUser,
26     person::{Person, PersonSafe},
27     person_block::PersonBlock,
28     post::Post,
29   },
30   traits::{ToSafe, ViewToVec},
31   utils::{functions::hot_rank, fuzzy_search, limit_and_offset_unlimited},
32   CommentSortType,
33   ListingType,
34 };
35 use typed_builder::TypedBuilder;
36
37 type CommentViewTuple = (
38   Comment,
39   PersonSafe,
40   Post,
41   CommunitySafe,
42   CommentAggregates,
43   Option<CommunityPersonBan>,
44   Option<CommunityFollower>,
45   Option<CommentSaved>,
46   Option<PersonBlock>,
47   Option<i16>,
48 );
49
50 impl CommentView {
51   pub fn read(
52     conn: &PgConnection,
53     comment_id: CommentId,
54     my_person_id: Option<PersonId>,
55   ) -> Result<Self, Error> {
56     // The left join below will return None in this case
57     let person_id_join = my_person_id.unwrap_or(PersonId(-1));
58
59     let (
60       comment,
61       creator,
62       post,
63       community,
64       counts,
65       creator_banned_from_community,
66       follower,
67       saved,
68       creator_blocked,
69       comment_like,
70     ) = comment::table
71       .find(comment_id)
72       .inner_join(person::table)
73       .inner_join(post::table)
74       .inner_join(community::table.on(post::community_id.eq(community::id)))
75       .inner_join(comment_aggregates::table)
76       .left_join(
77         community_person_ban::table.on(
78           community::id
79             .eq(community_person_ban::community_id)
80             .and(community_person_ban::person_id.eq(comment::creator_id))
81             .and(
82               community_person_ban::expires
83                 .is_null()
84                 .or(community_person_ban::expires.gt(now)),
85             ),
86         ),
87       )
88       .left_join(
89         community_follower::table.on(
90           post::community_id
91             .eq(community_follower::community_id)
92             .and(community_follower::person_id.eq(person_id_join)),
93         ),
94       )
95       .left_join(
96         comment_saved::table.on(
97           comment::id
98             .eq(comment_saved::comment_id)
99             .and(comment_saved::person_id.eq(person_id_join)),
100         ),
101       )
102       .left_join(
103         person_block::table.on(
104           comment::creator_id
105             .eq(person_block::target_id)
106             .and(person_block::person_id.eq(person_id_join)),
107         ),
108       )
109       .left_join(
110         comment_like::table.on(
111           comment::id
112             .eq(comment_like::comment_id)
113             .and(comment_like::person_id.eq(person_id_join)),
114         ),
115       )
116       .select((
117         comment::all_columns,
118         Person::safe_columns_tuple(),
119         post::all_columns,
120         Community::safe_columns_tuple(),
121         comment_aggregates::all_columns,
122         community_person_ban::all_columns.nullable(),
123         community_follower::all_columns.nullable(),
124         comment_saved::all_columns.nullable(),
125         person_block::all_columns.nullable(),
126         comment_like::score.nullable(),
127       ))
128       .first::<CommentViewTuple>(conn)?;
129
130     // If a person is given, then my_vote, if None, should be 0, not null
131     // Necessary to differentiate between other person's votes
132     let my_vote = if my_person_id.is_some() && comment_like.is_none() {
133       Some(0)
134     } else {
135       comment_like
136     };
137
138     Ok(CommentView {
139       comment,
140       post,
141       creator,
142       community,
143       counts,
144       creator_banned_from_community: creator_banned_from_community.is_some(),
145       subscribed: CommunityFollower::to_subscribed_type(&follower),
146       saved: saved.is_some(),
147       creator_blocked: creator_blocked.is_some(),
148       my_vote,
149     })
150   }
151 }
152
153 #[derive(TypedBuilder)]
154 #[builder(field_defaults(default))]
155 pub struct CommentQuery<'a> {
156   #[builder(!default)]
157   conn: &'a PgConnection,
158   listing_type: Option<ListingType>,
159   sort: Option<CommentSortType>,
160   community_id: Option<CommunityId>,
161   community_actor_id: Option<DbUrl>,
162   post_id: Option<PostId>,
163   parent_path: Option<Ltree>,
164   creator_id: Option<PersonId>,
165   local_user: Option<&'a LocalUser>,
166   search_term: Option<String>,
167   saved_only: Option<bool>,
168   page: Option<i64>,
169   limit: Option<i64>,
170   max_depth: Option<i32>,
171 }
172
173 impl<'a> CommentQuery<'a> {
174   pub fn list(self) -> Result<Vec<CommentView>, Error> {
175     use diesel::dsl::*;
176
177     // The left join below will return None in this case
178     let person_id_join = self.local_user.map(|l| l.person_id).unwrap_or(PersonId(-1));
179     let local_user_id_join = self.local_user.map(|l| l.id).unwrap_or(LocalUserId(-1));
180
181     let mut query = comment::table
182       .inner_join(person::table)
183       .inner_join(post::table)
184       .inner_join(community::table.on(post::community_id.eq(community::id)))
185       .inner_join(comment_aggregates::table)
186       .left_join(
187         community_person_ban::table.on(
188           community::id
189             .eq(community_person_ban::community_id)
190             .and(community_person_ban::person_id.eq(comment::creator_id))
191             .and(
192               community_person_ban::expires
193                 .is_null()
194                 .or(community_person_ban::expires.gt(now)),
195             ),
196         ),
197       )
198       .left_join(
199         community_follower::table.on(
200           post::community_id
201             .eq(community_follower::community_id)
202             .and(community_follower::person_id.eq(person_id_join)),
203         ),
204       )
205       .left_join(
206         comment_saved::table.on(
207           comment::id
208             .eq(comment_saved::comment_id)
209             .and(comment_saved::person_id.eq(person_id_join)),
210         ),
211       )
212       .left_join(
213         person_block::table.on(
214           comment::creator_id
215             .eq(person_block::target_id)
216             .and(person_block::person_id.eq(person_id_join)),
217         ),
218       )
219       .left_join(
220         community_block::table.on(
221           community::id
222             .eq(community_block::community_id)
223             .and(community_block::person_id.eq(person_id_join)),
224         ),
225       )
226       .left_join(
227         comment_like::table.on(
228           comment::id
229             .eq(comment_like::comment_id)
230             .and(comment_like::person_id.eq(person_id_join)),
231         ),
232       )
233       .inner_join(language::table)
234       .left_join(
235         local_user_language::table.on(
236           post::language_id
237             .eq(local_user_language::language_id)
238             .and(local_user_language::local_user_id.eq(local_user_id_join)),
239         ),
240       )
241       .select((
242         comment::all_columns,
243         Person::safe_columns_tuple(),
244         post::all_columns,
245         Community::safe_columns_tuple(),
246         comment_aggregates::all_columns,
247         community_person_ban::all_columns.nullable(),
248         community_follower::all_columns.nullable(),
249         comment_saved::all_columns.nullable(),
250         person_block::all_columns.nullable(),
251         comment_like::score.nullable(),
252       ))
253       .into_boxed();
254
255     if let Some(creator_id) = self.creator_id {
256       query = query.filter(comment::creator_id.eq(creator_id));
257     };
258
259     if let Some(post_id) = self.post_id {
260       query = query.filter(comment::post_id.eq(post_id));
261     };
262
263     if let Some(parent_path) = self.parent_path.as_ref() {
264       query = query.filter(comment::path.contained_by(parent_path));
265     };
266
267     if let Some(search_term) = self.search_term {
268       query = query.filter(comment::content.ilike(fuzzy_search(&search_term)));
269     };
270
271     if let Some(listing_type) = self.listing_type {
272       match listing_type {
273         ListingType::Subscribed => {
274           query = query.filter(community_follower::person_id.is_not_null())
275         } // TODO could be this: and(community_follower::person_id.eq(person_id_join)),
276         ListingType::Local => {
277           query = query.filter(community::local.eq(true)).filter(
278             community::hidden
279               .eq(false)
280               .or(community_follower::person_id.eq(person_id_join)),
281           )
282         }
283         ListingType::All => {
284           query = query.filter(
285             community::hidden
286               .eq(false)
287               .or(community_follower::person_id.eq(person_id_join)),
288           )
289         }
290       }
291     };
292
293     if let Some(community_id) = self.community_id {
294       query = query.filter(post::community_id.eq(community_id));
295     }
296
297     if let Some(community_actor_id) = self.community_actor_id {
298       query = query.filter(community::actor_id.eq(community_actor_id))
299     }
300
301     if self.saved_only.unwrap_or(false) {
302       query = query.filter(comment_saved::id.is_not_null());
303     }
304
305     if !self.local_user.map(|l| l.show_bot_accounts).unwrap_or(true) {
306       query = query.filter(person::bot_account.eq(false));
307     };
308
309     if self.local_user.is_some() {
310       // Filter out the rows with missing languages
311       query = query.filter(local_user_language::id.is_not_null());
312
313       // Don't show blocked communities or persons
314       query = query.filter(community_block::person_id.is_null());
315       query = query.filter(person_block::person_id.is_null());
316     }
317
318     // A Max depth given means its a tree fetch
319     let (limit, offset) = if let Some(max_depth) = self.max_depth {
320       let depth_limit = if let Some(parent_path) = self.parent_path.as_ref() {
321         parent_path.0.split('.').count() as i32 + max_depth
322         // Add one because of root "0"
323       } else {
324         max_depth + 1
325       };
326
327       query = query.filter(nlevel(comment::path).le(depth_limit));
328
329       // Always order by the parent path first
330       query = query.order_by(subpath(comment::path, 0, -1));
331
332       // TODO limit question. Limiting does not work for comment threads ATM, only max_depth
333       // For now, don't do any limiting for tree fetches
334       // https://stackoverflow.com/questions/72983614/postgres-ltree-how-to-limit-the-max-number-of-children-at-any-given-level
335
336       // Don't use the regular error-checking one, many more comments must ofter be fetched.
337       // This does not work for comment trees, and the limit should be manually set to a high number
338       //
339       // If a max depth is given, then you know its a tree fetch, and limits should be ignored
340       (i64::MAX, 0)
341     } else {
342       limit_and_offset_unlimited(self.page, self.limit)
343     };
344
345     query = match self.sort.unwrap_or(CommentSortType::Hot) {
346       CommentSortType::Hot => query
347         .then_order_by(hot_rank(comment_aggregates::score, comment_aggregates::published).desc())
348         .then_order_by(comment_aggregates::published.desc()),
349       CommentSortType::New => query.then_order_by(comment::published.desc()),
350       CommentSortType::Old => query.then_order_by(comment::published.asc()),
351       CommentSortType::Top => query.order_by(comment_aggregates::score.desc()),
352     };
353
354     // Note: deleted and removed comments are done on the front side
355     let res = query
356       .limit(limit)
357       .offset(offset)
358       .load::<CommentViewTuple>(self.conn)?;
359
360     Ok(CommentView::from_tuple_to_vec(res))
361   }
362 }
363
364 impl ViewToVec for CommentView {
365   type DbTuple = CommentViewTuple;
366   fn from_tuple_to_vec(items: Vec<Self::DbTuple>) -> Vec<Self> {
367     items
368       .into_iter()
369       .map(|a| Self {
370         comment: a.0,
371         creator: a.1,
372         post: a.2,
373         community: a.3,
374         counts: a.4,
375         creator_banned_from_community: a.5.is_some(),
376         subscribed: CommunityFollower::to_subscribed_type(&a.6),
377         saved: a.7.is_some(),
378         creator_blocked: a.8.is_some(),
379         my_vote: a.9,
380       })
381       .collect::<Vec<Self>>()
382   }
383 }
384
385 #[cfg(test)]
386 mod tests {
387   use crate::comment_view::*;
388   use lemmy_db_schema::{
389     aggregates::structs::CommentAggregates,
390     newtypes::LanguageId,
391     source::{
392       comment::*,
393       community::*,
394       local_user::LocalUserForm,
395       person::*,
396       person_block::PersonBlockForm,
397       post::*,
398     },
399     traits::{Blockable, Crud, Likeable},
400     utils::establish_unpooled_connection,
401     SubscribedType,
402   };
403   use serial_test::serial;
404
405   struct Data {
406     inserted_comment_0: Comment,
407     inserted_comment_1: Comment,
408     inserted_comment_2: Comment,
409     inserted_post: Post,
410     inserted_person: Person,
411     inserted_local_user: LocalUser,
412     inserted_person_2: Person,
413     inserted_community: Community,
414   }
415
416   fn init_data(conn: &PgConnection) -> Data {
417     let new_person = PersonForm {
418       name: "timmy".into(),
419       public_key: Some("pubkey".to_string()),
420       ..PersonForm::default()
421     };
422     let inserted_person = Person::create(&conn, &new_person).unwrap();
423     let local_user_form = LocalUserForm {
424       person_id: Some(inserted_person.id),
425       password_encrypted: Some("".to_string()),
426       ..Default::default()
427     };
428     let inserted_local_user = LocalUser::create(&conn, &local_user_form).unwrap();
429
430     let new_person_2 = PersonForm {
431       name: "sara".into(),
432       public_key: Some("pubkey".to_string()),
433       ..PersonForm::default()
434     };
435     let inserted_person_2 = Person::create(&conn, &new_person_2).unwrap();
436
437     let new_community = CommunityForm {
438       name: "test community 5".to_string(),
439       title: "nada".to_owned(),
440       public_key: Some("pubkey".to_string()),
441       ..CommunityForm::default()
442     };
443
444     let inserted_community = Community::create(&conn, &new_community).unwrap();
445
446     let new_post = PostForm {
447       name: "A test post 2".into(),
448       creator_id: inserted_person.id,
449       community_id: inserted_community.id,
450       ..PostForm::default()
451     };
452
453     let inserted_post = Post::create(&conn, &new_post).unwrap();
454
455     // Create a comment tree with this hierarchy
456     //       0
457     //     \     \
458     //    1      2
459     //    \
460     //  3  4
461     //     \
462     //     5
463     let comment_form_0 = CommentForm {
464       content: "Comment 0".into(),
465       creator_id: inserted_person.id,
466       post_id: inserted_post.id,
467       ..CommentForm::default()
468     };
469
470     let inserted_comment_0 = Comment::create(&conn, &comment_form_0, None).unwrap();
471
472     let comment_form_1 = CommentForm {
473       content: "Comment 1, A test blocked comment".into(),
474       creator_id: inserted_person_2.id,
475       post_id: inserted_post.id,
476       ..CommentForm::default()
477     };
478
479     let inserted_comment_1 =
480       Comment::create(&conn, &comment_form_1, Some(&inserted_comment_0.path)).unwrap();
481
482     let comment_form_2 = CommentForm {
483       content: "Comment 2".into(),
484       creator_id: inserted_person.id,
485       post_id: inserted_post.id,
486       ..CommentForm::default()
487     };
488
489     let inserted_comment_2 =
490       Comment::create(&conn, &comment_form_2, Some(&inserted_comment_0.path)).unwrap();
491
492     let comment_form_3 = CommentForm {
493       content: "Comment 3".into(),
494       creator_id: inserted_person.id,
495       post_id: inserted_post.id,
496       ..CommentForm::default()
497     };
498
499     let _inserted_comment_3 =
500       Comment::create(&conn, &comment_form_3, Some(&inserted_comment_1.path)).unwrap();
501
502     let comment_form_4 = CommentForm {
503       content: "Comment 4".into(),
504       creator_id: inserted_person.id,
505       post_id: inserted_post.id,
506       ..CommentForm::default()
507     };
508
509     let inserted_comment_4 =
510       Comment::create(&conn, &comment_form_4, Some(&inserted_comment_1.path)).unwrap();
511
512     let comment_form_5 = CommentForm {
513       content: "Comment 5".into(),
514       creator_id: inserted_person.id,
515       post_id: inserted_post.id,
516       ..CommentForm::default()
517     };
518
519     let _inserted_comment_5 =
520       Comment::create(&conn, &comment_form_5, Some(&inserted_comment_4.path)).unwrap();
521
522     let timmy_blocks_sara_form = PersonBlockForm {
523       person_id: inserted_person.id,
524       target_id: inserted_person_2.id,
525     };
526
527     let inserted_block = PersonBlock::block(&conn, &timmy_blocks_sara_form).unwrap();
528
529     let expected_block = PersonBlock {
530       id: inserted_block.id,
531       person_id: inserted_person.id,
532       target_id: inserted_person_2.id,
533       published: inserted_block.published,
534     };
535     assert_eq!(expected_block, inserted_block);
536
537     let comment_like_form = CommentLikeForm {
538       comment_id: inserted_comment_0.id,
539       post_id: inserted_post.id,
540       person_id: inserted_person.id,
541       score: 1,
542     };
543
544     let _inserted_comment_like = CommentLike::like(&conn, &comment_like_form).unwrap();
545
546     Data {
547       inserted_comment_0,
548       inserted_comment_1,
549       inserted_comment_2,
550       inserted_post,
551       inserted_person,
552       inserted_local_user,
553       inserted_person_2,
554       inserted_community,
555     }
556   }
557
558   #[test]
559   #[serial]
560   fn test_crud() {
561     let conn = establish_unpooled_connection();
562     let data = init_data(&conn);
563
564     let expected_comment_view_no_person = expected_comment_view(&data, &conn);
565
566     let mut expected_comment_view_with_person = expected_comment_view_no_person.to_owned();
567     expected_comment_view_with_person.my_vote = Some(1);
568
569     let read_comment_views_no_person = CommentQuery::builder()
570       .conn(&conn)
571       .post_id(Some(data.inserted_post.id))
572       .build()
573       .list()
574       .unwrap();
575
576     assert_eq!(
577       expected_comment_view_no_person,
578       read_comment_views_no_person[0]
579     );
580
581     let read_comment_views_with_person = CommentQuery::builder()
582       .conn(&conn)
583       .post_id(Some(data.inserted_post.id))
584       .local_user(Some(&data.inserted_local_user))
585       .build()
586       .list()
587       .unwrap();
588
589     assert_eq!(
590       expected_comment_view_with_person,
591       read_comment_views_with_person[0]
592     );
593
594     // Make sure its 1, not showing the blocked comment
595     assert_eq!(5, read_comment_views_with_person.len());
596
597     let read_comment_from_blocked_person = CommentView::read(
598       &conn,
599       data.inserted_comment_1.id,
600       Some(data.inserted_person.id),
601     )
602     .unwrap();
603
604     // Make sure block set the creator blocked
605     assert!(read_comment_from_blocked_person.creator_blocked);
606
607     cleanup(data, &conn);
608   }
609
610   #[test]
611   #[serial]
612   fn test_comment_tree() {
613     let conn = establish_unpooled_connection();
614     let data = init_data(&conn);
615
616     let top_path = data.inserted_comment_0.path.clone();
617     let read_comment_views_top_path = CommentQuery::builder()
618       .conn(&conn)
619       .post_id(Some(data.inserted_post.id))
620       .parent_path(Some(top_path))
621       .build()
622       .list()
623       .unwrap();
624
625     let child_path = data.inserted_comment_1.path.clone();
626     let read_comment_views_child_path = CommentQuery::builder()
627       .conn(&conn)
628       .post_id(Some(data.inserted_post.id))
629       .parent_path(Some(child_path))
630       .build()
631       .list()
632       .unwrap();
633
634     // Make sure the comment parent-limited fetch is correct
635     assert_eq!(6, read_comment_views_top_path.len());
636     assert_eq!(4, read_comment_views_child_path.len());
637
638     // Make sure it contains the parent, but not the comment from the other tree
639     let child_comments = read_comment_views_child_path
640       .into_iter()
641       .map(|c| c.comment)
642       .collect::<Vec<Comment>>();
643     assert!(child_comments.contains(&data.inserted_comment_1));
644     assert!(!child_comments.contains(&data.inserted_comment_2));
645
646     let read_comment_views_top_max_depth = CommentQuery::builder()
647       .conn(&conn)
648       .post_id(Some(data.inserted_post.id))
649       .max_depth(Some(1))
650       .build()
651       .list()
652       .unwrap();
653
654     // Make sure a depth limited one only has the top comment
655     assert_eq!(
656       expected_comment_view(&data, &conn),
657       read_comment_views_top_max_depth[0]
658     );
659     assert_eq!(1, read_comment_views_top_max_depth.len());
660
661     let child_path = data.inserted_comment_1.path.clone();
662     let read_comment_views_parent_max_depth = CommentQuery::builder()
663       .conn(&conn)
664       .post_id(Some(data.inserted_post.id))
665       .parent_path(Some(child_path))
666       .max_depth(Some(1))
667       .sort(Some(CommentSortType::New))
668       .build()
669       .list()
670       .unwrap();
671
672     // Make sure a depth limited one, and given child comment 1, has 3
673     assert!(read_comment_views_parent_max_depth[2]
674       .comment
675       .content
676       .eq("Comment 3"));
677     assert_eq!(3, read_comment_views_parent_max_depth.len());
678
679     cleanup(data, &conn);
680   }
681
682   fn cleanup(data: Data, conn: &PgConnection) {
683     CommentLike::remove(&conn, data.inserted_person.id, data.inserted_comment_0.id).unwrap();
684     Comment::delete(&conn, data.inserted_comment_0.id).unwrap();
685     Comment::delete(&conn, data.inserted_comment_1.id).unwrap();
686     Post::delete(&conn, data.inserted_post.id).unwrap();
687     Community::delete(&conn, data.inserted_community.id).unwrap();
688     Person::delete(&conn, data.inserted_person.id).unwrap();
689     Person::delete(&conn, data.inserted_person_2.id).unwrap();
690   }
691
692   fn expected_comment_view(data: &Data, conn: &PgConnection) -> CommentView {
693     let agg = CommentAggregates::read(&conn, data.inserted_comment_0.id).unwrap();
694     CommentView {
695       creator_banned_from_community: false,
696       my_vote: None,
697       subscribed: SubscribedType::NotSubscribed,
698       saved: false,
699       creator_blocked: false,
700       comment: Comment {
701         id: data.inserted_comment_0.id,
702         content: "Comment 0".into(),
703         creator_id: data.inserted_person.id,
704         post_id: data.inserted_post.id,
705         removed: false,
706         deleted: false,
707         published: data.inserted_comment_0.published,
708         ap_id: data.inserted_comment_0.ap_id.clone(),
709         updated: None,
710         local: true,
711         distinguished: false,
712         path: data.inserted_comment_0.to_owned().path,
713         language_id: LanguageId(0),
714       },
715       creator: PersonSafe {
716         id: data.inserted_person.id,
717         name: "timmy".into(),
718         display_name: None,
719         published: data.inserted_person.published,
720         avatar: None,
721         actor_id: data.inserted_person.actor_id.to_owned(),
722         local: true,
723         banned: false,
724         deleted: false,
725         admin: false,
726         bot_account: false,
727         bio: None,
728         banner: None,
729         updated: None,
730         inbox_url: data.inserted_person.inbox_url.to_owned(),
731         shared_inbox_url: None,
732         matrix_user_id: None,
733         ban_expires: None,
734       },
735       post: Post {
736         id: data.inserted_post.id,
737         name: data.inserted_post.name.to_owned(),
738         creator_id: data.inserted_person.id,
739         url: None,
740         body: None,
741         published: data.inserted_post.published,
742         updated: None,
743         community_id: data.inserted_community.id,
744         removed: false,
745         deleted: false,
746         locked: false,
747         stickied: false,
748         nsfw: false,
749         embed_title: None,
750         embed_description: None,
751         embed_video_url: None,
752         thumbnail_url: None,
753         ap_id: data.inserted_post.ap_id.to_owned(),
754         local: true,
755         language_id: Default::default(),
756       },
757       community: CommunitySafe {
758         id: data.inserted_community.id,
759         name: "test community 5".to_string(),
760         icon: None,
761         removed: false,
762         deleted: false,
763         nsfw: false,
764         actor_id: data.inserted_community.actor_id.to_owned(),
765         local: true,
766         title: "nada".to_owned(),
767         description: None,
768         updated: None,
769         banner: None,
770         hidden: false,
771         posting_restricted_to_mods: false,
772         published: data.inserted_community.published,
773       },
774       counts: CommentAggregates {
775         id: agg.id,
776         comment_id: data.inserted_comment_0.id,
777         score: 1,
778         upvotes: 1,
779         downvotes: 0,
780         published: agg.published,
781         child_count: 5,
782       },
783     }
784   }
785 }