| 498 |
498 |
|
.await
|
| 499 |
499 |
|
.unwrap();
|
| 500 |
500 |
|
|
| 501 |
|
- |
// Login as mod
|
| 502 |
501 |
|
let mod_id = h.login_as("dismissmod").await;
|
| 503 |
502 |
|
h.add_membership(mod_id, comm_id, "moderator").await;
|
| 504 |
503 |
|
|
| 568 |
567 |
|
.await
|
| 569 |
568 |
|
.unwrap();
|
| 570 |
569 |
|
|
| 571 |
|
- |
// Login as mod
|
| 572 |
570 |
|
let mod_id = h.login_as("removemod").await;
|
| 573 |
571 |
|
h.add_membership(mod_id, comm_id, "moderator").await;
|
| 574 |
572 |
|
|
| 684 |
682 |
|
);
|
| 685 |
683 |
|
}
|
| 686 |
684 |
|
}
|
|
685 |
+ |
|
|
686 |
+ |
// Restore, the reversal of both removal paths
|
|
687 |
+ |
|
|
688 |
+ |
/// The gap this closes: before restore existed, a mod who read a bad-faith
|
|
689 |
+ |
/// brigade and dismissed the flags left the post hidden forever, with an empty
|
|
690 |
+ |
/// queue implying it had been dealt with.
|
|
691 |
+ |
#[tokio::test]
|
|
692 |
+ |
async fn restore_unhides_auto_hidden_post() {
|
|
693 |
+ |
let mut h = TestHarness::new().await;
|
|
694 |
+ |
let author_id = h.login_as("restoreauthor").await;
|
|
695 |
+ |
let comm_id = h.create_community("Test", "test").await;
|
|
696 |
+ |
let cat_id = h.create_category(comm_id, "General", "general").await;
|
|
697 |
+ |
h.add_membership(author_id, comm_id, "member").await;
|
|
698 |
+ |
|
|
699 |
+ |
sqlx::query("UPDATE communities SET auto_hide_threshold = 2 WHERE id = $1")
|
|
700 |
+ |
.bind(comm_id)
|
|
701 |
+ |
.execute(&h.db)
|
|
702 |
+ |
.await
|
|
703 |
+ |
.unwrap();
|
|
704 |
+ |
|
|
705 |
+ |
let thread_id = h
|
|
706 |
+ |
.create_thread_with_post(cat_id, author_id, "Brigaded", "Perfectly fine post")
|
|
707 |
+ |
.await;
|
|
708 |
+ |
let post_id = mt_db::queries::list_posts_in_thread(&h.db, thread_id)
|
|
709 |
+ |
.await
|
|
710 |
+ |
.unwrap()[0]
|
|
711 |
+ |
.id;
|
|
712 |
+ |
|
|
713 |
+ |
let thread_url = format!("/p/test/general/{thread_id}");
|
|
714 |
+ |
let flag_url = format!("/p/test/general/{thread_id}/posts/{post_id}/flag");
|
|
715 |
+ |
for name in ["brigade1", "brigade2"] {
|
|
716 |
+ |
let flagger = h.login_as(name).await;
|
|
717 |
+ |
h.add_membership(flagger, comm_id, "member").await;
|
|
718 |
+ |
h.client.get(&thread_url).await;
|
|
719 |
+ |
h.client.post_form(&flag_url, "reason=spam").await;
|
|
720 |
+ |
}
|
|
721 |
+ |
|
|
722 |
+ |
let removed: bool =
|
|
723 |
+ |
sqlx::query_scalar("SELECT removed_at IS NOT NULL FROM posts WHERE id = $1")
|
|
724 |
+ |
.bind(post_id)
|
|
725 |
+ |
.fetch_one(&h.db)
|
|
726 |
+ |
.await
|
|
727 |
+ |
.unwrap();
|
|
728 |
+ |
assert!(removed, "threshold reached, post should be auto-hidden");
|
|
729 |
+ |
|
|
730 |
+ |
let mod_id = h.login_as("restoremod").await;
|
|
731 |
+ |
h.add_membership(mod_id, comm_id, "moderator").await;
|
|
732 |
+ |
h.client.get(&thread_url).await;
|
|
733 |
+ |
let restore_url = format!("/p/test/general/{thread_id}/posts/{post_id}/restore");
|
|
734 |
+ |
let resp = h.client.post_form(&restore_url, "").await;
|
|
735 |
+ |
assert!(
|
|
736 |
+ |
resp.status.is_redirection(),
|
|
737 |
+ |
"Expected redirect, got {}",
|
|
738 |
+ |
resp.status
|
|
739 |
+ |
);
|
|
740 |
+ |
|
|
741 |
+ |
let still_removed: bool =
|
|
742 |
+ |
sqlx::query_scalar("SELECT removed_at IS NOT NULL FROM posts WHERE id = $1")
|
|
743 |
+ |
.bind(post_id)
|
|
744 |
+ |
.fetch_one(&h.db)
|
|
745 |
+ |
.await
|
|
746 |
+ |
.unwrap();
|
|
747 |
+ |
assert!(!still_removed, "restore must clear removed_at");
|
|
748 |
+ |
|
|
749 |
+ |
let actor: Option<uuid::Uuid> = sqlx::query_scalar(
|
|
750 |
+ |
"SELECT actor_id FROM mod_log WHERE target_id = $1 AND action = 'restore_post'",
|
|
751 |
+ |
)
|
|
752 |
+ |
.bind(post_id)
|
|
753 |
+ |
.fetch_one(&h.db)
|
|
754 |
+ |
.await
|
|
755 |
+ |
.unwrap();
|
|
756 |
+ |
assert_eq!(
|
|
757 |
+ |
actor,
|
|
758 |
+ |
Some(mod_id),
|
|
759 |
+ |
"restore must log against the acting moderator, not System"
|
|
760 |
+ |
);
|
|
761 |
+ |
}
|
|
762 |
+ |
|
|
763 |
+ |
/// Restoring while the post is still over the threshold would re-hide it on the
|
|
764 |
+ |
/// very next flag, so restore resolves the outstanding flags. This is the test
|
|
765 |
+ |
/// that pins that decision: one fresh flag after a restore must not re-hide.
|
|
766 |
+ |
#[tokio::test]
|
|
767 |
+ |
async fn restore_resolves_flags_so_the_post_does_not_immediately_rehide() {
|
|
768 |
+ |
let mut h = TestHarness::new().await;
|
|
769 |
+ |
let author_id = h.login_as("rehideauthor").await;
|
|
770 |
+ |
let comm_id = h.create_community("Test", "test").await;
|
|
771 |
+ |
let cat_id = h.create_category(comm_id, "General", "general").await;
|
|
772 |
+ |
h.add_membership(author_id, comm_id, "member").await;
|
|
773 |
+ |
|
|
774 |
+ |
sqlx::query("UPDATE communities SET auto_hide_threshold = 2 WHERE id = $1")
|
|
775 |
+ |
.bind(comm_id)
|
|
776 |
+ |
.execute(&h.db)
|
|
777 |
+ |
.await
|
|
778 |
+ |
.unwrap();
|
|
779 |
+ |
|
|
780 |
+ |
let thread_id = h
|
|
781 |
+ |
.create_thread_with_post(cat_id, author_id, "Rehide", "Fine post")
|
|
782 |
+ |
.await;
|
|
783 |
+ |
let post_id = mt_db::queries::list_posts_in_thread(&h.db, thread_id)
|
|
784 |
+ |
.await
|
|
785 |
+ |
.unwrap()[0]
|
|
786 |
+ |
.id;
|
|
787 |
+ |
|
|
788 |
+ |
let thread_url = format!("/p/test/general/{thread_id}");
|
|
789 |
+ |
let flag_url = format!("/p/test/general/{thread_id}/posts/{post_id}/flag");
|
|
790 |
+ |
for name in ["rehide1", "rehide2"] {
|
|
791 |
+ |
let flagger = h.login_as(name).await;
|
|
792 |
+ |
h.add_membership(flagger, comm_id, "member").await;
|
|
793 |
+ |
h.client.get(&thread_url).await;
|
|
794 |
+ |
h.client.post_form(&flag_url, "reason=spam").await;
|
|
795 |
+ |
}
|
|
796 |
+ |
|
|
797 |
+ |
let mod_id = h.login_as("rehidemod").await;
|
|
798 |
+ |
h.add_membership(mod_id, comm_id, "moderator").await;
|
|
799 |
+ |
h.client.get(&thread_url).await;
|
|
800 |
+ |
h.client
|
|
801 |
+ |
.post_form(
|
|
802 |
+ |
&format!("/p/test/general/{thread_id}/posts/{post_id}/restore"),
|
|
803 |
+ |
"",
|
|
804 |
+ |
)
|
|
805 |
+ |
.await;
|
|
806 |
+ |
|
|
807 |
+ |
let pending: i64 = sqlx::query_scalar(
|
|
808 |
+ |
"SELECT COUNT(*) FROM post_flags WHERE post_id = $1 AND resolved_at IS NULL",
|
|
809 |
+ |
)
|
|
810 |
+ |
.bind(post_id)
|
|
811 |
+ |
.fetch_one(&h.db)
|
|
812 |
+ |
.await
|
|
813 |
+ |
.unwrap();
|
|
814 |
+ |
assert_eq!(pending, 0, "restore must resolve the outstanding flags");
|
|
815 |
+ |
|
|
816 |
+ |
let flagger = h.login_as("rehide3").await;
|
|
817 |
+ |
h.add_membership(flagger, comm_id, "member").await;
|
|
818 |
+ |
h.client.get(&thread_url).await;
|
|
819 |
+ |
h.client.post_form(&flag_url, "reason=spam").await;
|
|
820 |
+ |
|
|
821 |
+ |
let removed: bool =
|
|
822 |
+ |
sqlx::query_scalar("SELECT removed_at IS NOT NULL FROM posts WHERE id = $1")
|
|
823 |
+ |
.bind(post_id)
|
|
824 |
+ |
.fetch_one(&h.db)
|
|
825 |
+ |
.await
|
|
826 |
+ |
.unwrap();
|
|
827 |
+ |
assert!(
|
|
828 |
+ |
!removed,
|
|
829 |
+ |
"one flag after a restore is below threshold; the post must stay live"
|
|
830 |
+ |
);
|
|
831 |
+ |
}
|
|
832 |
+ |
|
|
833 |
+ |
#[tokio::test]
|
|
834 |
+ |
async fn member_cannot_restore_post() {
|
|
835 |
+ |
let mut h = TestHarness::new().await;
|
|
836 |
+ |
let author_id = h.login_as("norestoreauthor").await;
|
|
837 |
+ |
let comm_id = h.create_community("Test", "test").await;
|
|
838 |
+ |
let cat_id = h.create_category(comm_id, "General", "general").await;
|
|
839 |
+ |
h.add_membership(author_id, comm_id, "member").await;
|
|
840 |
+ |
|
|
841 |
+ |
let thread_id = h
|
|
842 |
+ |
.create_thread_with_post(cat_id, author_id, "NoRestore", "Content")
|
|
843 |
+ |
.await;
|
|
844 |
+ |
let post_id = mt_db::queries::list_posts_in_thread(&h.db, thread_id)
|
|
845 |
+ |
.await
|
|
846 |
+ |
.unwrap()[0]
|
|
847 |
+ |
.id;
|
|
848 |
+ |
|
|
849 |
+ |
sqlx::query("UPDATE posts SET removed_at = now() WHERE id = $1")
|
|
850 |
+ |
.bind(post_id)
|
|
851 |
+ |
.execute(&h.db)
|
|
852 |
+ |
.await
|
|
853 |
+ |
.unwrap();
|
|
854 |
+ |
|
|
855 |
+ |
let member_id = h.login_as("plainmember").await;
|
|
856 |
+ |
h.add_membership(member_id, comm_id, "member").await;
|
|
857 |
+ |
let thread_url = format!("/p/test/general/{thread_id}");
|
|
858 |
+ |
h.client.get(&thread_url).await;
|
|
859 |
+ |
let resp = h
|
|
860 |
+ |
.client
|
|
861 |
+ |
.post_form(
|
|
862 |
+ |
&format!("/p/test/general/{thread_id}/posts/{post_id}/restore"),
|
|
863 |
+ |
"",
|
|
864 |
+ |
)
|
|
865 |
+ |
.await;
|
|
866 |
+ |
assert_eq!(
|
|
867 |
+ |
resp.status,
|
|
868 |
+ |
axum::http::StatusCode::FORBIDDEN,
|
|
869 |
+ |
"a member must not be able to restore"
|
|
870 |
+ |
);
|
|
871 |
+ |
|
|
872 |
+ |
let removed: bool =
|
|
873 |
+ |
sqlx::query_scalar("SELECT removed_at IS NOT NULL FROM posts WHERE id = $1")
|
|
874 |
+ |
.bind(post_id)
|
|
875 |
+ |
.fetch_one(&h.db)
|
|
876 |
+ |
.await
|
|
877 |
+ |
.unwrap();
|
|
878 |
+ |
assert!(removed, "post must stay removed");
|
|
879 |
+ |
}
|
|
880 |
+ |
|
|
881 |
+ |
/// Restoring a live post is a no-op, not a log entry. A mod-log row for an
|
|
882 |
+ |
/// action that changed nothing is worse than no row: it reads as a reversal
|
|
883 |
+ |
/// that happened.
|
|
884 |
+ |
#[tokio::test]
|
|
885 |
+ |
async fn restoring_a_live_post_writes_no_log_row() {
|
|
886 |
+ |
let mut h = TestHarness::new().await;
|
|
887 |
+ |
let author_id = h.login_as("liveauthor").await;
|
|
888 |
+ |
let comm_id = h.create_community("Test", "test").await;
|
|
889 |
+ |
let cat_id = h.create_category(comm_id, "General", "general").await;
|
|
890 |
+ |
h.add_membership(author_id, comm_id, "member").await;
|
|
891 |
+ |
|
|
892 |
+ |
let thread_id = h
|
|
893 |
+ |
.create_thread_with_post(cat_id, author_id, "Live", "Never removed")
|
|
894 |
+ |
.await;
|
|
895 |
+ |
let post_id = mt_db::queries::list_posts_in_thread(&h.db, thread_id)
|
|
896 |
+ |
.await
|
|
897 |
+ |
.unwrap()[0]
|
|
898 |
+ |
.id;
|
|
899 |
+ |
|
|
900 |
+ |
let mod_id = h.login_as("nooprestoremod").await;
|
|
901 |
+ |
h.add_membership(mod_id, comm_id, "moderator").await;
|
|
902 |
+ |
let thread_url = format!("/p/test/general/{thread_id}");
|
|
903 |
+ |
h.client.get(&thread_url).await;
|
|
904 |
+ |
h.client
|
|
905 |
+ |
.post_form(
|
|
906 |
+ |
&format!("/p/test/general/{thread_id}/posts/{post_id}/restore"),
|
|
907 |
+ |
"",
|
|
908 |
+ |
)
|
|
909 |
+ |
.await;
|
|
910 |
+ |
|
|
911 |
+ |
let rows: i64 = sqlx::query_scalar(
|
|
912 |
+ |
"SELECT COUNT(*) FROM mod_log WHERE target_id = $1 AND action = 'restore_post'",
|
|
913 |
+ |
)
|
|
914 |
+ |
.bind(post_id)
|
|
915 |
+ |
.fetch_one(&h.db)
|
|
916 |
+ |
.await
|
|
917 |
+ |
.unwrap();
|
|
918 |
+ |
assert_eq!(rows, 0, "no-op restore must not write a mod-log row");
|
|
919 |
+ |
}
|