From e371ec1dc4a8868c0d03a4218db1c2225289e672 Mon Sep 17 00:00:00 2001
From: Dessalines <tyhou13@gmx.com>
Date: Mon, 7 Dec 2020 15:58:53 -0500
Subject: [PATCH] Adding user aggregates tests.

---
 lemmy_db/src/aggregates/user_aggregates.rs    | 215 +++++++++++++++++-
 lemmy_db/src/comment.rs                       |   4 +-
 .../down.sql                                  |   2 +
 .../up.sql                                    |  85 +++++--
 4 files changed, 285 insertions(+), 21 deletions(-)

diff --git a/lemmy_db/src/aggregates/user_aggregates.rs b/lemmy_db/src/aggregates/user_aggregates.rs
index 26c2c067..622bce11 100644
--- a/lemmy_db/src/aggregates/user_aggregates.rs
+++ b/lemmy_db/src/aggregates/user_aggregates.rs
@@ -14,9 +14,218 @@ pub struct UserAggregates {
 }
 
 impl UserAggregates {
-  pub fn read(conn: &PgConnection, id: i32) -> Result<Self, Error> {
-    user_aggregates::table.find(id).first::<Self>(conn)
+  pub fn read(conn: &PgConnection, user_id: i32) -> Result<Self, Error> {
+    user_aggregates::table
+      .filter(user_aggregates::user_id.eq(user_id))
+      .first::<Self>(conn)
   }
 }
 
-// TODO add unit tests, to make sure triggers are working
+#[cfg(test)]
+mod tests {
+  use crate::{
+    aggregates::user_aggregates::UserAggregates,
+    comment::{Comment, CommentForm, CommentLike, CommentLikeForm},
+    community::{Community, CommunityForm},
+    post::{Post, PostForm, PostLike, PostLikeForm},
+    tests::establish_unpooled_connection,
+    user::{UserForm, User_},
+    Crud,
+    Likeable,
+    ListingType,
+    SortType,
+  };
+
+  #[test]
+  fn test_crud() {
+    let conn = establish_unpooled_connection();
+
+    let new_user = UserForm {
+      name: "thommy_user_agg".into(),
+      preferred_username: None,
+      password_encrypted: "nope".into(),
+      email: None,
+      matrix_user_id: None,
+      avatar: None,
+      banner: None,
+      admin: false,
+      banned: Some(false),
+      published: None,
+      updated: None,
+      show_nsfw: false,
+      theme: "browser".into(),
+      default_sort_type: SortType::Hot as i16,
+      default_listing_type: ListingType::Subscribed as i16,
+      lang: "browser".into(),
+      show_avatars: true,
+      send_notifications_to_email: false,
+      actor_id: None,
+      bio: None,
+      local: true,
+      private_key: None,
+      public_key: None,
+      last_refreshed_at: None,
+    };
+
+    let inserted_user = User_::create(&conn, &new_user).unwrap();
+
+    let another_user = UserForm {
+      name: "jerry_user_agg".into(),
+      preferred_username: None,
+      password_encrypted: "nope".into(),
+      email: None,
+      matrix_user_id: None,
+      avatar: None,
+      banner: None,
+      admin: false,
+      banned: Some(false),
+      published: None,
+      updated: None,
+      show_nsfw: false,
+      theme: "browser".into(),
+      default_sort_type: SortType::Hot as i16,
+      default_listing_type: ListingType::Subscribed as i16,
+      lang: "browser".into(),
+      show_avatars: true,
+      send_notifications_to_email: false,
+      actor_id: None,
+      bio: None,
+      local: true,
+      private_key: None,
+      public_key: None,
+      last_refreshed_at: None,
+    };
+
+    let another_inserted_user = User_::create(&conn, &another_user).unwrap();
+
+    let new_community = CommunityForm {
+      name: "TIL_site_agg".into(),
+      creator_id: inserted_user.id,
+      title: "nada".to_owned(),
+      description: None,
+      category_id: 1,
+      nsfw: false,
+      removed: None,
+      deleted: None,
+      updated: None,
+      actor_id: None,
+      local: true,
+      private_key: None,
+      public_key: None,
+      last_refreshed_at: None,
+      published: None,
+      icon: None,
+      banner: None,
+    };
+
+    let inserted_community = Community::create(&conn, &new_community).unwrap();
+
+    let new_post = PostForm {
+      name: "A test post".into(),
+      url: None,
+      body: None,
+      creator_id: inserted_user.id,
+      community_id: inserted_community.id,
+      removed: None,
+      deleted: None,
+      locked: None,
+      stickied: None,
+      nsfw: false,
+      updated: None,
+      embed_title: None,
+      embed_description: None,
+      embed_html: None,
+      thumbnail_url: None,
+      ap_id: None,
+      local: true,
+      published: None,
+    };
+
+    let inserted_post = Post::create(&conn, &new_post).unwrap();
+
+    let post_like = PostLikeForm {
+      post_id: inserted_post.id,
+      user_id: inserted_user.id,
+      score: 1,
+    };
+
+    let _inserted_post_like = PostLike::like(&conn, &post_like).unwrap();
+
+    let comment_form = CommentForm {
+      content: "A test comment".into(),
+      creator_id: inserted_user.id,
+      post_id: inserted_post.id,
+      removed: None,
+      deleted: None,
+      read: None,
+      parent_id: None,
+      published: None,
+      updated: None,
+      ap_id: None,
+      local: true,
+    };
+
+    let inserted_comment = Comment::create(&conn, &comment_form).unwrap();
+
+    let comment_like = CommentLikeForm {
+      comment_id: inserted_comment.id,
+      user_id: inserted_user.id,
+      post_id: inserted_post.id,
+      score: 1,
+    };
+
+    let _inserted_comment_like = CommentLike::like(&conn, &comment_like).unwrap();
+
+    let child_comment_form = CommentForm {
+      content: "A test comment".into(),
+      creator_id: inserted_user.id,
+      post_id: inserted_post.id,
+      removed: None,
+      deleted: None,
+      read: None,
+      parent_id: Some(inserted_comment.id),
+      published: None,
+      updated: None,
+      ap_id: None,
+      local: true,
+    };
+
+    let inserted_child_comment = Comment::create(&conn, &child_comment_form).unwrap();
+
+    let child_comment_like = CommentLikeForm {
+      comment_id: inserted_child_comment.id,
+      user_id: another_inserted_user.id,
+      post_id: inserted_post.id,
+      score: 1,
+    };
+
+    let _inserted_child_comment_like = CommentLike::like(&conn, &child_comment_like).unwrap();
+
+    let user_aggregates_before_delete = UserAggregates::read(&conn, inserted_user.id).unwrap();
+
+    assert_eq!(1, user_aggregates_before_delete.post_count);
+    assert_eq!(1, user_aggregates_before_delete.post_score);
+    assert_eq!(2, user_aggregates_before_delete.comment_count);
+    assert_eq!(2, user_aggregates_before_delete.comment_score);
+
+    // Remove a post like
+    PostLike::remove(&conn, inserted_user.id, inserted_post.id).unwrap();
+    let after_post_like_remove = UserAggregates::read(&conn, inserted_user.id).unwrap();
+    assert_eq!(0, after_post_like_remove.post_score);
+
+    // Remove a parent comment (the scores should also be removed)
+    Comment::delete(&conn, inserted_comment.id).unwrap();
+    let after_parent_comment_delete = UserAggregates::read(&conn, inserted_user.id).unwrap();
+    assert_eq!(0, after_parent_comment_delete.comment_count);
+    assert_eq!(0, after_parent_comment_delete.comment_score);
+
+    // This should delete all the associated rows, and fire triggers
+    let user_num_deleted = User_::delete(&conn, inserted_user.id).unwrap();
+    assert_eq!(1, user_num_deleted);
+    User_::delete(&conn, another_inserted_user.id).unwrap();
+
+    // Should be none found
+    let after_delete = UserAggregates::read(&conn, inserted_user.id);
+    assert!(after_delete.is_err());
+  }
+}
diff --git a/lemmy_db/src/comment.rs b/lemmy_db/src/comment.rs
index 9b092825..c88eb9ad 100644
--- a/lemmy_db/src/comment.rs
+++ b/lemmy_db/src/comment.rs
@@ -187,7 +187,7 @@ pub struct CommentLike {
   pub id: i32,
   pub user_id: i32,
   pub comment_id: i32,
-  pub post_id: i32,
+  pub post_id: i32, // TODO this is redundant
   pub score: i16,
   pub published: chrono::NaiveDateTime,
 }
@@ -197,7 +197,7 @@ pub struct CommentLike {
 pub struct CommentLikeForm {
   pub user_id: i32,
   pub comment_id: i32,
-  pub post_id: i32,
+  pub post_id: i32, // TODO this is redundant
   pub score: i16,
 }
 
diff --git a/migrations/2020-12-03-035643_create_user_aggregates/down.sql b/migrations/2020-12-03-035643_create_user_aggregates/down.sql
index 4e3e7fcb..a7b5e473 100644
--- a/migrations/2020-12-03-035643_create_user_aggregates/down.sql
+++ b/migrations/2020-12-03-035643_create_user_aggregates/down.sql
@@ -1,10 +1,12 @@
 -- User aggregates
 drop table user_aggregates;
+drop trigger user_aggregates_user on user_;
 drop trigger user_aggregates_post_count on post;
 drop trigger user_aggregates_post_score on post_like;
 drop trigger user_aggregates_comment_count on comment;
 drop trigger user_aggregates_comment_score on comment_like;
 drop function 
+  user_aggregates_user, 
   user_aggregates_post_count,
   user_aggregates_post_score,
   user_aggregates_comment_count,
diff --git a/migrations/2020-12-03-035643_create_user_aggregates/up.sql b/migrations/2020-12-03-035643_create_user_aggregates/up.sql
index e0c39be6..1bebfe30 100644
--- a/migrations/2020-12-03-035643_create_user_aggregates/up.sql
+++ b/migrations/2020-12-03-035643_create_user_aggregates/up.sql
@@ -2,10 +2,10 @@
 create table user_aggregates (
   id serial primary key,
   user_id int references user_ on update cascade on delete cascade not null,
-  post_count bigint not null,
-  post_score bigint not null,
-  comment_count bigint not null,
-  comment_score bigint not null,
+  post_count bigint not null default 0,
+  post_score bigint not null default 0,
+  comment_count bigint not null default 0,
+  comment_score bigint not null default 0,
   unique (user_id)
 );
 
@@ -35,6 +35,25 @@ insert into user_aggregates (user_id, post_count, post_score, comment_count, com
 
 
 -- Add user aggregate triggers
+
+-- initial user add
+create function user_aggregates_user()
+returns trigger language plpgsql
+as $$
+begin
+  IF (TG_OP = 'INSERT') THEN
+    insert into user_aggregates (user_id) values (NEW.id);
+  ELSIF (TG_OP = 'DELETE') THEN
+    delete from user_aggregates where user_id = OLD.id;
+  END IF;
+  return null;
+end $$;
+
+create trigger user_aggregates_user
+after insert or delete on user_
+for each row
+execute procedure user_aggregates_user();
+
 -- post count
 create function user_aggregates_post_count()
 returns trigger language plpgsql
@@ -43,9 +62,26 @@ begin
   IF (TG_OP = 'INSERT') THEN
     update user_aggregates 
     set post_count = post_count + 1 where user_id = NEW.creator_id;
+
   ELSIF (TG_OP = 'DELETE') THEN
     update user_aggregates 
     set post_count = post_count - 1 where user_id = OLD.creator_id;
+
+    -- If the post gets deleted, the score calculation trigger won't fire, 
+    -- so you need to re-calculate
+    update user_aggregates ua
+    set post_score = pd.score
+    from (
+      select u.id,
+      coalesce(0, sum(pl.score)) as score
+      -- User join because posts could be empty
+      from user_ u 
+      left join post p on u.id = p.creator_id
+      left join post_like pl on p.id = pl.post_id
+      group by u.id
+    ) pd 
+    where ua.user_id = pd.id;
+
   END IF;
   return null;
 end $$;
@@ -63,15 +99,16 @@ begin
   IF (TG_OP = 'INSERT') THEN
     -- TODO not sure if this is working right
     -- Need to get the post creator, not the voter
-    update user_aggregates 
+    update user_aggregates ua
     set post_score = post_score + NEW.score
-    from post_like pl join post p on p.id = pl.post_id
-    where p.id = NEW.post_id and p.creator_id = NEW.user_id;
+    from post p
+    where ua.user_id = p.creator_id and p.id = NEW.post_id;
+    
   ELSIF (TG_OP = 'DELETE') THEN
-    update user_aggregates 
+    update user_aggregates ua
     set post_score = post_score - OLD.score
-    from post_like pl join post p on p.id = pl.post_id
-    where p.id = OLD.post_id and p.creator_id = OLD.user_id;
+    from post p
+    where ua.user_id = p.creator_id and p.id = OLD.post_id;
   END IF;
   return null;
 end $$;
@@ -92,6 +129,21 @@ begin
   ELSIF (TG_OP = 'DELETE') THEN
     update user_aggregates 
     set comment_count = comment_count - 1 where user_id = OLD.creator_id;
+
+    -- If the comment gets deleted, the score calculation trigger won't fire, 
+    -- so you need to re-calculate
+    update user_aggregates ua
+    set comment_score = cd.score
+    from (
+      select u.id,
+      coalesce(0, sum(cl.score)) as score
+      -- User join because comments could be empty
+      from user_ u 
+      left join comment c on u.id = c.creator_id
+      left join comment_like cl on c.id = cl.comment_id
+      group by u.id
+    ) cd 
+    where ua.user_id = cd.id;
   END IF;
   return null;
 end $$;
@@ -108,19 +160,20 @@ as $$
 begin
   IF (TG_OP = 'INSERT') THEN
     -- Need to get the post creator, not the voter
-    update user_aggregates 
+    update user_aggregates ua
     set comment_score = comment_score + NEW.score
-    from comment_like pl join comment p on p.id = pl.comment_id
-    where p.id = NEW.comment_id and p.creator_id = NEW.user_id;
+    from comment c
+    where ua.user_id = c.creator_id and c.id = NEW.comment_id;
   ELSIF (TG_OP = 'DELETE') THEN
-    update user_aggregates 
+    update user_aggregates ua
     set comment_score = comment_score - OLD.score
-    from comment_like pl join comment p on p.id = pl.comment_id
-    where p.id = OLD.comment_id and p.creator_id = OLD.user_id;
+    from comment c
+    where ua.user_id = c.creator_id and c.id = OLD.comment_id;
   END IF;
   return null;
 end $$;
 
 create trigger user_aggregates_comment_score
 after insert or delete on comment_like
+for each row
 execute procedure user_aggregates_comment_score();
-- 
2.44.1