Skip to main content

max / makenotwork

11.3 KB · 353 lines History Blame Raw
1 use crate::harness::TestHarness;
2
3 #[tokio::test]
4 async fn directory_counts_categories_and_threads_per_community() {
5 // Pins the scalar-subquery rewrite of list_communities (P1): per-community
6 // category_count and thread_count must match the old COUNT(DISTINCT) join.
7 let mut h = TestHarness::new().await;
8 let comm = h.create_community("Counted", "counted").await;
9 let cat_a = h.create_category(comm, "Alpha", "alpha").await;
10 let cat_b = h.create_category(comm, "Beta", "beta").await;
11 let author = h.login_as("writer").await;
12 h.add_membership(author, comm, "member").await;
13 // 2 threads in alpha, 1 in beta → 3 threads, 2 categories.
14 h.create_thread_with_post(cat_a, author, "T1", "b").await;
15 h.create_thread_with_post(cat_a, author, "T2", "b").await;
16 h.create_thread_with_post(cat_b, author, "T3", "b").await;
17
18 // A second community with no content stays at zero, proves counts are
19 // per-community, not global.
20 h.create_community("Empty", "empty").await;
21
22 let rows = mt_db::queries::list_communities(&h.db, 25, 0)
23 .await
24 .unwrap();
25 let counted = rows
26 .iter()
27 .find(|r| r.slug == "counted")
28 .expect("counted present");
29 assert_eq!(counted.category_count, 2, "two categories");
30 assert_eq!(counted.thread_count, 3, "three threads across categories");
31 let empty = rows
32 .iter()
33 .find(|r| r.slug == "empty")
34 .expect("empty present");
35 assert_eq!(empty.category_count, 0);
36 assert_eq!(empty.thread_count, 0);
37 }
38
39 #[tokio::test]
40 async fn directory_pagination_shows_limited_communities() {
41 let mut h = TestHarness::new().await;
42
43 // Create 30 communities (page size is 25)
44 for i in 0..30 {
45 h.create_community(&format!("Community {i:02}"), &format!("comm-{i:02}"))
46 .await;
47 }
48
49 // Page 1 should show 25
50 let resp = h.client.get("/").await;
51 assert!(resp.status.is_success());
52 assert!(
53 resp.text.contains("Page 1 of 2"),
54 "Should show pagination info: {}",
55 &resp.text[..200.min(resp.text.len())]
56 );
57 assert!(
58 resp.text.contains("Next"),
59 "Should show Next link on page 1"
60 );
61 assert!(
62 !resp.text.contains("Previous"),
63 "Should not show Previous link on page 1"
64 );
65
66 // Page 2 should show the remaining 5
67 let resp = h.client.get("/?page=2").await;
68 assert!(resp.status.is_success());
69 assert!(
70 resp.text.contains("Previous"),
71 "Should show Previous link on page 2"
72 );
73 }
74
75 #[tokio::test]
76 async fn empty_category_shows_no_threads() {
77 let mut h = TestHarness::new().await;
78 let comm_id = h.create_community("Test", "test").await;
79 let _cat_id = h.create_category(comm_id, "General", "general").await;
80
81 let resp = h.client.get("/p/test/general").await;
82
83 assert!(resp.status.is_success());
84 assert!(
85 resp.text.contains("No threads yet"),
86 "expected empty state message"
87 );
88 // Should not render pagination
89 assert!(
90 !resp.text.contains("pagination"),
91 "pagination should not appear with 0 threads"
92 );
93 }
94
95 #[tokio::test]
96 async fn page_beyond_max_shows_last_page() {
97 let mut h = TestHarness::new().await;
98 let user_id = h.login_as("pager").await;
99 let comm_id = h.create_community("Test", "test").await;
100 let cat_id = h.create_category(comm_id, "General", "general").await;
101 h.add_membership(user_id, comm_id, "member").await;
102
103 h.create_thread_with_post(cat_id, user_id, "Thread One", "body")
104 .await;
105
106 // Request page=999
107 let resp = h.client.get("/p/test/general?page=999").await;
108
109 assert!(resp.status.is_success());
110 // Should still show the thread (clamped to page 1)
111 assert!(
112 resp.text.contains("Thread One"),
113 "thread should be visible on clamped page"
114 );
115 }
116
117 #[tokio::test]
118 async fn page_zero_treated_as_page_one() {
119 let mut h = TestHarness::new().await;
120 let user_id = h.login_as("pager").await;
121 let comm_id = h.create_community("Test", "test").await;
122 let cat_id = h.create_category(comm_id, "General", "general").await;
123 h.add_membership(user_id, comm_id, "member").await;
124
125 h.create_thread_with_post(cat_id, user_id, "First Thread", "body")
126 .await;
127
128 let resp = h.client.get("/p/test/general?page=0").await;
129
130 assert!(resp.status.is_success());
131 assert!(
132 resp.text.contains("First Thread"),
133 "thread should be visible with page=0"
134 );
135 }
136
137 #[tokio::test]
138 async fn sort_by_replies_works() {
139 let mut h = TestHarness::new().await;
140 let user_id = h.login_as("sorter").await;
141 let comm_id = h.create_community("Test", "test").await;
142 let cat_id = h.create_category(comm_id, "General", "general").await;
143 h.add_membership(user_id, comm_id, "member").await;
144
145 // Create two threads
146 let _t1 = h
147 .create_thread_with_post(cat_id, user_id, "Few Replies", "body")
148 .await;
149 let t2 = h
150 .create_thread_with_post(cat_id, user_id, "Many Replies", "body")
151 .await;
152
153 // Add extra replies to t2
154 for i in 0..3 {
155 mt_db::mutations::create_post(
156 &h.db,
157 t2,
158 user_id,
159 &format!("reply {i}"),
160 &format!("<p>reply {i}</p>"),
161 )
162 .await
163 .unwrap();
164 }
165
166 // Sort by replies desc, "Many Replies" should come first
167 let resp = h
168 .client
169 .get("/p/test/general?sort=replies&order=desc")
170 .await;
171 assert!(resp.status.is_success());
172 let many_pos = resp
173 .text
174 .find("Many Replies")
175 .expect("Many Replies not found");
176 let few_pos = resp
177 .text
178 .find("Few Replies")
179 .expect("Few Replies not found");
180 assert!(
181 many_pos < few_pos,
182 "Many Replies should appear before Few Replies when sorted by replies desc"
183 );
184
185 // Sort by replies asc, "Few Replies" should come first (after pinned)
186 let resp = h.client.get("/p/test/general?sort=replies&order=asc").await;
187 assert!(resp.status.is_success());
188 let many_pos = resp
189 .text
190 .find("Many Replies")
191 .expect("Many Replies not found");
192 let few_pos = resp
193 .text
194 .find("Few Replies")
195 .expect("Few Replies not found");
196 assert!(
197 few_pos < many_pos,
198 "Few Replies should appear before Many Replies when sorted by replies asc"
199 );
200 }
201
202 #[tokio::test]
203 async fn sort_headers_present_in_category() {
204 let mut h = TestHarness::new().await;
205 let comm_id = h.create_community("Test", "test").await;
206 let cat_id = h.create_category(comm_id, "General", "general").await;
207 let user_id = h.login_as("viewer").await;
208 h.add_membership(user_id, comm_id, "member").await;
209 h.create_thread_with_post(cat_id, user_id, "A Thread", "body")
210 .await;
211
212 let resp = h.client.get("/p/test/general").await;
213 assert!(resp.status.is_success());
214
215 // Default sort is activity desc, so activity header should have the arrow
216 assert!(
217 resp.text.contains("sort-active"),
218 "active sort indicator should be present"
219 );
220 assert!(
221 resp.text.contains("?sort=replies"),
222 "replies sort link should be present"
223 );
224 assert!(
225 resp.text.contains("?sort=activity"),
226 "activity sort link should be present"
227 );
228 }
229
230 #[tokio::test]
231 async fn thread_pagination_page_zero() {
232 let mut h = TestHarness::new().await;
233 let user_id = h.login_as("poster").await;
234 let comm_id = h.create_community("Test", "test").await;
235 let cat_id = h.create_category(comm_id, "General", "general").await;
236 h.add_membership(user_id, comm_id, "member").await;
237
238 let thread_id = h
239 .create_thread_with_post(cat_id, user_id, "My Thread", "first post")
240 .await;
241
242 let resp = h
243 .client
244 .get(&format!("/p/test/general/{thread_id}?page=0"))
245 .await;
246
247 assert!(resp.status.is_success());
248 assert!(
249 resp.text.contains("first post"),
250 "post should be visible with page=0"
251 );
252 }
253
254 #[tokio::test]
255 async fn meta_description_present_on_category() {
256 let mut h = TestHarness::new().await;
257 let comm_id = h.create_community("TestProject", "testproject").await;
258 let _cat_id = h.create_category(comm_id, "Bugs", "bugs").await;
259
260 let resp = h.client.get("/p/testproject/bugs").await;
261 assert!(resp.status.is_success());
262 assert!(
263 resp.text.contains(r#"<meta name="description""#),
264 "meta description should be present"
265 );
266 }
267
268 #[tokio::test]
269 async fn meta_description_present_on_thread() {
270 let mut h = TestHarness::new().await;
271 let user_id = h.login_as("metauser").await;
272 let comm_id = h.create_community("Test", "test").await;
273 let cat_id = h.create_category(comm_id, "General", "general").await;
274 h.add_membership(user_id, comm_id, "member").await;
275
276 let thread_id = h
277 .create_thread_with_post(cat_id, user_id, "Meta Thread", "body")
278 .await;
279
280 let resp = h.client.get(&format!("/p/test/general/{thread_id}")).await;
281 assert!(resp.status.is_success());
282 assert!(
283 resp.text.contains(r#"<meta name="description""#),
284 "meta description should be present on thread page"
285 );
286 }
287
288 #[tokio::test]
289 async fn noindex_on_error_page() {
290 let mut h = TestHarness::new().await;
291
292 let resp = h.client.get("/nonexistent-page-xyz").await;
293 assert_eq!(resp.status, axum::http::StatusCode::NOT_FOUND);
294 assert!(
295 resp.text
296 .contains(r#"<meta name="robots" content="noindex">"#),
297 "404 page should have noindex meta"
298 );
299 }
300
301 #[tokio::test]
302 async fn member_list_shows_members_with_roles() {
303 let mut h = TestHarness::new().await;
304 let owner_id = h.login_as("theowner").await;
305 let comm_id = h.create_community("Test", "test").await;
306 h.add_membership(owner_id, comm_id, "owner").await;
307
308 let mod_id = h.login_as("themod").await;
309 h.add_membership(mod_id, comm_id, "moderator").await;
310
311 let member_id = h.login_as("regular").await;
312 h.add_membership(member_id, comm_id, "member").await;
313
314 let resp = h.client.get("/p/test/members").await;
315
316 assert!(resp.status.is_success());
317 assert!(resp.text.contains("theowner"), "owner should be listed");
318 assert!(resp.text.contains("themod"), "mod should be listed");
319 assert!(resp.text.contains("regular"), "member should be listed");
320 // Owner should appear before member in the table (sorted by role)
321 // Search within the data-table to avoid matching the header nav Profile link
322 let table_start = resp.text.find("data-table").unwrap();
323 let table_html = &resp.text[table_start..];
324 let owner_pos = table_html.find("/u/theowner").unwrap();
325 let member_pos = table_html.find("/u/regular").unwrap();
326 assert!(
327 owner_pos < member_pos,
328 "owner should appear before regular member"
329 );
330 }
331
332 #[tokio::test]
333 async fn member_list_nonexistent_community_404() {
334 let mut h = TestHarness::new().await;
335
336 let resp = h.client.get("/p/nonexistent/members").await;
337 assert_eq!(resp.status, axum::http::StatusCode::NOT_FOUND);
338 }
339
340 #[tokio::test]
341 async fn community_page_has_members_link() {
342 let mut h = TestHarness::new().await;
343 let comm_id = h.create_community("Test", "test").await;
344 let _cat_id = h.create_category(comm_id, "General", "general").await;
345
346 let resp = h.client.get("/p/test").await;
347 assert!(resp.status.is_success());
348 assert!(
349 resp.text.contains("/p/test/members"),
350 "community page should have members link"
351 );
352 }
353