Skip to main content

max / makenotwork

8.5 KB · 274 lines History Blame Raw
1 //! Integration tests for admin queries and membership counting.
2 //!
3 //! Covers: list_all_communities, search_users (prefix, limit, LIKE escaping),
4 //! get_user_membership_summary (post counts, suspended exclusion).
5
6 use crate::harness::TestHarness;
7 use uuid::Uuid;
8
9 // list_all_communities
10
11 #[tokio::test]
12 async fn test_list_all_communities() {
13 let h = TestHarness::new().await;
14
15 let _c1 = h.create_community("Alpha Forum", "alpha").await;
16 let _c2 = h.create_community("Beta Forum", "beta").await;
17 let _c3 = h.create_community("Gamma Forum", "gamma").await;
18
19 let rows = mt_db::queries::list_all_communities(&h.db, 500)
20 .await
21 .unwrap();
22
23 assert_eq!(rows.len(), 3, "Should return all 3 communities");
24
25 let names: Vec<&str> = rows.iter().map(|r| r.name.as_str()).collect();
26 assert_eq!(
27 names,
28 vec!["Alpha Forum", "Beta Forum", "Gamma Forum"],
29 "Communities should be ordered by name"
30 );
31
32 // Verify slugs are present
33 assert_eq!(rows[0].slug, "alpha");
34 assert_eq!(rows[1].slug, "beta");
35 assert_eq!(rows[2].slug, "gamma");
36
37 // Verify suspended fields default to None
38 assert!(rows[0].suspended_at.is_none());
39 assert!(rows[0].suspension_reason.is_none());
40 }
41
42 // search_users
43
44 #[tokio::test]
45 async fn test_search_users_exact_prefix() {
46 let h = TestHarness::new().await;
47
48 // Insert users directly (no login needed for DB-level tests)
49 for (name, display) in [("alice", "Alice"), ("alvin", "Alvin"), ("bob", "Bob")] {
50 let id = Uuid::new_v4();
51 sqlx::query(
52 "INSERT INTO users (mnw_account_id, username, display_name) VALUES ($1, $2, $3)",
53 )
54 .bind(id)
55 .bind(name)
56 .bind(display)
57 .execute(&h.db)
58 .await
59 .unwrap();
60 }
61
62 let results = mt_db::queries::search_users(&h.db, "al").await.unwrap();
63
64 let usernames: Vec<&str> = results.iter().map(|r| r.username.as_str()).collect();
65 assert_eq!(
66 usernames,
67 vec!["alice", "alvin"],
68 "Search for 'al' should match alice and alvin, ordered alphabetically"
69 );
70
71 // Verify bob is excluded
72 assert!(
73 !usernames.contains(&"bob"),
74 "bob should not match prefix 'al'"
75 );
76 }
77
78 #[tokio::test]
79 async fn test_search_users_limit() {
80 let h = TestHarness::new().await;
81
82 // Insert a handful of users to verify the query executes with LIMIT 50
83 for i in 0..5 {
84 let id = Uuid::new_v4();
85 let name = format!("limituser{i}");
86 sqlx::query(
87 "INSERT INTO users (mnw_account_id, username, display_name) VALUES ($1, $2, $2)",
88 )
89 .bind(id)
90 .bind(&name)
91 .execute(&h.db)
92 .await
93 .unwrap();
94 }
95
96 let results = mt_db::queries::search_users(&h.db, "limituser")
97 .await
98 .unwrap();
99
100 assert_eq!(results.len(), 5, "Should return all 5 matching users");
101
102 // Verify they are ordered alphabetically
103 let usernames: Vec<&str> = results.iter().map(|r| r.username.as_str()).collect();
104 let mut sorted = usernames.clone();
105 sorted.sort_unstable();
106 assert_eq!(
107 usernames, sorted,
108 "Results should be alphabetically ordered"
109 );
110 }
111
112 #[tokio::test]
113 async fn test_search_users_special_chars() {
114 let h = TestHarness::new().await;
115
116 // Insert users: one that looks like a LIKE wildcard match, one normal
117 for (name, display) in [("alice", "Alice"), ("al%pha", "Al%pha")] {
118 let id = Uuid::new_v4();
119 sqlx::query(
120 "INSERT INTO users (mnw_account_id, username, display_name) VALUES ($1, $2, $3)",
121 )
122 .bind(id)
123 .bind(name)
124 .bind(display)
125 .execute(&h.db)
126 .await
127 .unwrap();
128 }
129
130 // Search for literal "al%", should only match usernames starting with "al%"
131 let results = mt_db::queries::search_users(&h.db, "al%").await.unwrap();
132
133 let usernames: Vec<&str> = results.iter().map(|r| r.username.as_str()).collect();
134 assert_eq!(
135 usernames,
136 vec!["al%pha"],
137 "Searching 'al%' should match literal percent, not act as wildcard"
138 );
139
140 // alice should NOT be matched, the % is escaped, so the query is 'al\%%'
141 assert!(
142 !usernames.contains(&"alice"),
143 "alice should not match when searching for literal 'al%'"
144 );
145 }
146
147 // get_user_membership_summary
148
149 #[tokio::test]
150 async fn test_membership_summary_post_count() {
151 let mut h = TestHarness::new().await;
152
153 let user_id = h.login_as("postwriter").await;
154 let comm_id = h.create_community("Writers Guild", "writers").await;
155 let cat_id = h.create_category(comm_id, "General", "general").await;
156 h.add_membership(user_id, comm_id, "member").await;
157
158 // Create a thread with an initial post, then add two more posts
159 let thread_id = h
160 .create_thread_with_post(cat_id, user_id, "First Thread", "First post body")
161 .await;
162
163 mt_db::mutations::create_post(
164 &h.db,
165 thread_id,
166 user_id,
167 "Second post",
168 "<p>Second post</p>",
169 )
170 .await
171 .unwrap();
172
173 mt_db::mutations::create_post(&h.db, thread_id, user_id, "Third post", "<p>Third post</p>")
174 .await
175 .unwrap();
176
177 let summaries = mt_db::queries::get_user_membership_summary(&h.db, user_id)
178 .await
179 .unwrap();
180
181 assert_eq!(summaries.len(), 1, "Should have one membership");
182 assert_eq!(summaries[0].community_name, "Writers Guild");
183 assert_eq!(summaries[0].community_slug, "writers");
184 assert_eq!(summaries[0].role, mt_core::types::CommunityRole::Member);
185 assert_eq!(
186 summaries[0].post_count, 3,
187 "Should count all 3 posts (initial + 2 replies)"
188 );
189 }
190
191 #[tokio::test]
192 async fn profile_counts_exclude_mod_removed_posts() {
193 // Mod-removed posts (removed_at set) must not inflate profile/membership
194 // post counts or the activity feed, the live hide column is removed_at,
195 // not the (unused-for-posts) deleted_at (audit B1/B2).
196 let mut h = TestHarness::new().await;
197 let user_id = h.login_as("removee").await;
198 let mod_id = h.login_as("themod").await;
199 let comm_id = h.create_community("Mod Test", "modtest").await;
200 let cat_id = h.create_category(comm_id, "General", "general").await;
201 h.add_membership(user_id, comm_id, "member").await;
202
203 // OP + two replies = 3 posts; then mod-remove one reply.
204 let thread_id = h
205 .create_thread_with_post(cat_id, user_id, "Thread", "OP body")
206 .await;
207 mt_db::mutations::create_post(&h.db, thread_id, user_id, "kept reply", "<p>kept</p>")
208 .await
209 .unwrap();
210 let removed_post =
211 mt_db::mutations::create_post(&h.db, thread_id, user_id, "bad reply", "<p>bad</p>")
212 .await
213 .unwrap();
214 assert!(
215 mt_db::mutations::mod_remove_post(&h.db, removed_post, mod_id)
216 .await
217 .unwrap()
218 );
219
220 // Membership summary: 3 created, 1 removed → 2.
221 let summaries = mt_db::queries::get_user_membership_summary(&h.db, user_id)
222 .await
223 .unwrap();
224 assert_eq!(
225 summaries[0].post_count, 2,
226 "removed post excluded from summary"
227 );
228
229 // Profile counts: same exclusion on the per-community post_count.
230 let profile = mt_db::queries::get_user_profile_in_community(&h.db, "modtest", "removee")
231 .await
232 .unwrap()
233 .expect("profile exists");
234 assert_eq!(
235 profile.post_count, 2,
236 "removed post excluded from profile count"
237 );
238
239 // Activity feed must not surface the removed post.
240 let activity = mt_db::queries::get_user_activity_in_community(&h.db, comm_id, user_id, 50)
241 .await
242 .unwrap();
243 assert_eq!(
244 activity.len(),
245 2,
246 "removed post excluded from activity feed"
247 );
248 }
249
250 #[tokio::test]
251 async fn test_membership_summary_excludes_suspended() {
252 let mut h = TestHarness::new().await;
253
254 let user_id = h.login_as("suspendcheck").await;
255
256 let active_id = h.create_community("Active Community", "active").await;
257 h.add_membership(user_id, active_id, "member").await;
258
259 let suspended_id = h.create_community("Suspended Community", "suspended").await;
260 h.add_membership(user_id, suspended_id, "member").await;
261
262 // Suspend the second community
263 mt_db::mutations::suspend_community(&h.db, suspended_id, Some("policy violation"))
264 .await
265 .unwrap();
266
267 let summaries = mt_db::queries::get_user_membership_summary(&h.db, user_id)
268 .await
269 .unwrap();
270
271 assert_eq!(summaries.len(), 1, "Should exclude the suspended community");
272 assert_eq!(summaries[0].community_name, "Active Community");
273 }
274