Skip to main content

max / makenotwork

7.6 KB · 226 lines History Blame Raw
1 //! Dedicated superadmin community view + clean-slate.
2 //!
3 //! Covers:
4 //! * Admin community detail page renders for superadmin; 404 for others
5 //! * Clean-slate wipes threads/posts, preserves settings/categories/members,
6 //! posts a pinned+locked system thread, logs the mod action
7 //! * Typed-phrase confirmation enforced
8
9 use crate::harness::TestHarness;
10 use axum::http::StatusCode;
11 use uuid::Uuid;
12
13 async fn login_admin(h: &mut TestHarness, admin_id: Uuid) {
14 sqlx::query(
15 "INSERT INTO users (mnw_account_id, username, display_name) \
16 VALUES ($1, 'superadmin', 'superadmin') ON CONFLICT (mnw_account_id) DO NOTHING",
17 )
18 .bind(admin_id)
19 .execute(&h.db)
20 .await
21 .unwrap();
22 h.client.get("/").await;
23 h.client
24 .post_json(
25 "/_test/login",
26 &serde_json::json!({ "user_id": admin_id.to_string(), "username": "superadmin" })
27 .to_string(),
28 )
29 .await;
30 }
31
32 #[tokio::test]
33 async fn admin_community_page_renders_for_superadmin() {
34 let admin_id = Uuid::new_v4();
35 let mut h = TestHarness::new_with_admin(admin_id).await;
36 let comm_id = h.create_community("Test", "test").await;
37 let _cat_id = h.create_category(comm_id, "General", "general").await;
38 login_admin(&mut h, admin_id).await;
39
40 let resp = h.client.get("/_admin/communities/test").await;
41 assert_eq!(resp.status, StatusCode::OK);
42 assert!(resp.text.contains("Community administration"));
43 assert!(resp.text.contains("Clean slate"));
44 assert!(resp.text.contains("Moderation state"));
45 }
46
47 #[tokio::test]
48 async fn admin_community_page_404_for_non_admin() {
49 let mut h = TestHarness::new().await;
50 let comm_id = h.create_community("Test", "test").await;
51 let _cat_id = h.create_category(comm_id, "General", "general").await;
52 let _ = h.login_as("notadmin").await;
53
54 let resp = h.client.get("/_admin/communities/test").await;
55 assert_eq!(resp.status, StatusCode::NOT_FOUND);
56 }
57
58 #[tokio::test]
59 async fn clean_slate_wipes_threads_and_logs_action() {
60 let admin_id = Uuid::new_v4();
61 let mut h = TestHarness::new_with_admin(admin_id).await;
62 let comm_id = h.create_community("Test", "test").await;
63 let cat_id = h.create_category(comm_id, "General", "general").await;
64
65 // Seed an author + threads to wipe.
66 let author_id = h.login_as("seedauthor").await;
67 h.add_membership(author_id, comm_id, "member").await;
68 let _thread_a = h
69 .create_thread_with_post(cat_id, author_id, "First", "body A")
70 .await;
71 let _thread_b = h
72 .create_thread_with_post(cat_id, author_id, "Second", "body B")
73 .await;
74 h.client.post_form("/auth/logout", "").await;
75
76 login_admin(&mut h, admin_id).await;
77 h.client.get("/_admin/communities/test").await;
78 let resp = h
79 .client
80 .post_form("/_admin/communities/test/clean-slate", "confirm=test")
81 .await;
82 assert!(resp.status.is_redirection(), "status: {}", resp.status);
83
84 // Originals gone; one system thread remains (pinned + locked).
85 let titles: Vec<(String, bool, bool)> = sqlx::query_as(
86 "SELECT t.title, t.pinned, t.locked
87 FROM threads t JOIN categories c ON c.id = t.category_id
88 WHERE c.community_id = $1",
89 )
90 .bind(comm_id)
91 .fetch_all(&h.db)
92 .await
93 .unwrap();
94 assert_eq!(titles.len(), 1, "expected only the system reset thread");
95 let (title, pinned, locked) = &titles[0];
96 assert!(title.starts_with("Community reset by superadmin"));
97 assert!(*pinned, "system thread should be pinned");
98 assert!(*locked, "system thread should be locked");
99
100 // Mod log records the action.
101 let action_count: i64 = sqlx::query_scalar(
102 "SELECT COUNT(*) FROM mod_log
103 WHERE community_id = $1 AND action = 'clean_slate_community'",
104 )
105 .bind(comm_id)
106 .fetch_one(&h.db)
107 .await
108 .unwrap();
109 assert_eq!(action_count, 1);
110 }
111
112 #[tokio::test]
113 async fn clean_slate_preserves_categories_members_tags() {
114 let admin_id = Uuid::new_v4();
115 let mut h = TestHarness::new_with_admin(admin_id).await;
116 let comm_id = h.create_community("Test", "test").await;
117 let cat_id = h.create_category(comm_id, "General", "general").await;
118
119 let author_id = h.login_as("seed2").await;
120 h.add_membership(author_id, comm_id, "member").await;
121 h.create_thread_with_post(cat_id, author_id, "First", "body")
122 .await;
123 h.client.post_form("/auth/logout", "").await;
124
125 login_admin(&mut h, admin_id).await;
126 h.client.get("/_admin/communities/test").await;
127 h.client
128 .post_form("/_admin/communities/test/clean-slate", "confirm=test")
129 .await;
130
131 // Category survives.
132 let cat_count: i64 =
133 sqlx::query_scalar("SELECT COUNT(*) FROM categories WHERE community_id = $1")
134 .bind(comm_id)
135 .fetch_one(&h.db)
136 .await
137 .unwrap();
138 assert!(cat_count >= 1);
139
140 // Membership survives.
141 let member_count: i64 = sqlx::query_scalar(
142 "SELECT COUNT(*) FROM memberships WHERE community_id = $1 AND user_id = $2",
143 )
144 .bind(comm_id)
145 .bind(author_id)
146 .fetch_one(&h.db)
147 .await
148 .unwrap();
149 assert_eq!(member_count, 1, "membership should be preserved");
150 }
151
152 #[tokio::test]
153 async fn clean_slate_rejects_wrong_confirmation_phrase() {
154 let admin_id = Uuid::new_v4();
155 let mut h = TestHarness::new_with_admin(admin_id).await;
156 let comm_id = h.create_community("Test", "test").await;
157 let cat_id = h.create_category(comm_id, "General", "general").await;
158 let author_id = h.login_as("seed3").await;
159 h.add_membership(author_id, comm_id, "member").await;
160 h.create_thread_with_post(cat_id, author_id, "Survive me", "body")
161 .await;
162 h.client.post_form("/auth/logout", "").await;
163
164 login_admin(&mut h, admin_id).await;
165 h.client.get("/_admin/communities/test").await;
166 let resp = h
167 .client
168 .post_form("/_admin/communities/test/clean-slate", "confirm=nope")
169 .await;
170 assert_eq!(resp.status, StatusCode::UNPROCESSABLE_ENTITY);
171
172 // Threads survive.
173 let count: i64 = sqlx::query_scalar(
174 "SELECT COUNT(*) FROM threads t JOIN categories c ON c.id = t.category_id
175 WHERE c.community_id = $1",
176 )
177 .bind(comm_id)
178 .fetch_one(&h.db)
179 .await
180 .unwrap();
181 assert_eq!(
182 count, 1,
183 "thread should still exist after rejected clean-slate"
184 );
185 }
186
187 #[tokio::test]
188 async fn clean_slate_404_for_non_admin() {
189 let mut h = TestHarness::new().await;
190 let comm_id = h.create_community("Test", "test").await;
191 let _cat_id = h.create_category(comm_id, "General", "general").await;
192 let _ = h.login_as("notadmin").await;
193
194 h.client.get("/").await;
195 let resp = h
196 .client
197 .post_form("/_admin/communities/test/clean-slate", "confirm=test")
198 .await;
199 assert_eq!(resp.status, StatusCode::NOT_FOUND);
200 }
201
202 #[tokio::test]
203 async fn clean_slate_with_no_categories_skips_system_thread() {
204 let admin_id = Uuid::new_v4();
205 let mut h = TestHarness::new_with_admin(admin_id).await;
206 let comm_id = h.create_community("Empty", "empty").await;
207 // No category, clean-slate should succeed but skip the system thread.
208 login_admin(&mut h, admin_id).await;
209 h.client.get("/_admin/communities/empty").await;
210 let resp = h
211 .client
212 .post_form("/_admin/communities/empty/clean-slate", "confirm=empty")
213 .await;
214 assert!(resp.status.is_redirection(), "status: {}", resp.status);
215
216 let count: i64 = sqlx::query_scalar(
217 "SELECT COUNT(*) FROM threads t JOIN categories c ON c.id = t.category_id
218 WHERE c.community_id = $1",
219 )
220 .bind(comm_id)
221 .fetch_one(&h.db)
222 .await
223 .unwrap();
224 assert_eq!(count, 0);
225 }
226