Skip to main content

max / makenotwork

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