Skip to main content

max / makenotwork

16.9 KB · 423 lines History Blame Raw
1 //! Press the buttons on every described screen.
2 //!
3 //! The debt this pays off. Four conversion batches shipped with tests that
4 //! rendered a screen and asserted the *address* each control carried was a
5 //! registered route. Two controls were still wrong when pressed, and both got
6 //! through because the address was never the thing that was broken:
7 //!
8 //! - `ssh_keys`' Remove addressed `DELETE /api/users/me/ssh-keys/{id}`, which
9 //! answers an htmx request with the whole re-rendered Askama list. With no
10 //! target htmx swapped that table into the button that was pressed.
11 //! - `library_contacts`' Revoke addressed `DELETE /api/contacts/{id}`, which
12 //! answers 204. htmx never swaps a 204, so the revoke landed and the row
13 //! stayed until the reader left the tab and came back.
14 //!
15 //! Both are answers, not addresses, so no amount of route-table assertion would
16 //! have found either. What finds them is issuing the request and reading what
17 //! comes back, which is what everything below does.
18 //!
19 //! # Why this is here and not a sweep check of its own
20 //!
21 //! It runs nightly on astra already: the sweep's `test` check is
22 //! `cargo nextest run --all-features` over `MNW/server`, with postgres and
23 //! `TEST_DATABASE_URL` supplied by `[repo.MNW.env]`, and nextest's JUnit report
24 //! names every failing test individually, so a red cell here carries the test
25 //! name rather than one opaque verdict. A separate check would pay for a second
26 //! full build of the server, which is the most expensive cell in the matrix, to
27 //! buy a column heading.
28 //!
29 //! # The one shape to keep
30 //!
31 //! [`controls`] reads what the *description emitted* rather than a list written
32 //! here by hand. A control that a future conversion adds is pressed by this
33 //! suite the day it is added, without anybody remembering to extend a list, and
34 //! a screen whose controls all disappear fails [`a_screen_offers_something`]
35 //! rather than passing vacuously.
36
37 use crate::harness::{BuildOptions, TestHarness};
38 use makenotwork::quasi;
39
40 /// Every described screen, as (switch name, address, region it answers into).
41 ///
42 /// Written out rather than derived from `quasi::PATHS`, because the region is
43 /// not on that list and the point of the third column is to assert the answer
44 /// lands somewhere. Kept in step by `every_described_screen_is_pressed` below.
45 ///
46 /// The regions are *read* rather than transcribed wherever the screen publishes
47 /// one. Transcribing them is what broke here on 2026-08-18: the library strip
48 /// was described (`6b24f2df`), the two library panels stopped sharing
49 /// `tab-content` with everything else, and this list still said they did. A
50 /// constant this table can name is a constant this table cannot disagree with.
51 const SCREENS: &[(&str, &str, &str)] = &[
52 (
53 "user_ssh_keys",
54 quasi::ssh_keys::PATH,
55 quasi::ssh_keys::REGION,
56 ),
57 (
58 "library_contacts",
59 quasi::library_contacts::PATH,
60 quasi::library_contacts::REGION,
61 ),
62 // The one screen that publishes no REGION, so this is the only row left
63 // transcribing one. Read it from the screen the day it publishes one.
64 (
65 "buyer_contacts",
66 quasi::buyer_contacts::PATH,
67 "contacts-section",
68 ),
69 (
70 "payout_summary",
71 quasi::payout_summary::PATH,
72 quasi::payout_summary::REGION,
73 ),
74 (
75 "user_analytics",
76 quasi::user_analytics::PATH,
77 quasi::user_analytics::REGION,
78 ),
79 (
80 "library_communities",
81 quasi::forum_memberships::LIBRARY_PATH,
82 quasi::forum_memberships::LIBRARY_REGION,
83 ),
84 (
85 "user_forums",
86 quasi::forum_memberships::SETTINGS_PATH,
87 quasi::forum_memberships::SETTINGS_REGION,
88 ),
89 ];
90
91 /// One control the description emitted.
92 #[derive(Debug, Clone, PartialEq, Eq)]
93 struct Control {
94 method: &'static str,
95 address: String,
96 }
97
98 /// Pull every control out of a rendered screen.
99 ///
100 /// Attribute-driven, so it sees exactly what a browser would act on and nothing
101 /// a test author remembered to list. `href` is deliberately excluded: an anchor
102 /// that also carries `hx-get` is already counted by the verb, and one that does
103 /// not is either external or a download, neither of which this server answers.
104 fn controls(html: &str) -> Vec<Control> {
105 let mut found = Vec::new();
106 for (attr, method) in [
107 ("hx-get=\"", "GET"),
108 ("hx-post=\"", "POST"),
109 ("hx-put=\"", "PUT"),
110 ("hx-delete=\"", "DELETE"),
111 ] {
112 let mut rest = html;
113 while let Some(at) = rest.find(attr) {
114 rest = &rest[at + attr.len()..];
115 let Some(end) = rest.find('"') else { break };
116 let address = rest[..end].replace("&amp;", "&");
117 rest = &rest[end..];
118 // A form's own action is emitted on the form and again on nothing
119 // else; a repeated address is a repeated control and both get
120 // pressed, which costs one request and keeps the reader honest.
121 found.push(Control { method, address });
122 }
123 }
124 found
125 }
126
127 /// A harness signed in as a creator.
128 ///
129 /// Took a screen name until 2026-08-26, when `64b33b26` deleted the switch:
130 /// every described screen serves unconditionally, so there is nothing to turn
131 /// on and every screen in `SCREENS` is reachable from any of these harnesses.
132 async fn viewing() -> TestHarness {
133 let mut h = TestHarness::build(BuildOptions {
134 // The forum screens refuse when Multithreaded is unconfigured, which is
135 // deliberate and would otherwise read here as a broken screen. Pointed
136 // at an address nothing answers on purpose: the upstream call then fails
137 // and both screens fall back to an empty list, which is the path a real
138 // outage takes and the one worth pressing.
139 mt_base_url: Some("http://127.0.0.1:9".to_owned()),
140 internal_shared_secret: Some("press-the-buttons".to_owned()),
141 ..Default::default()
142 })
143 .await;
144 h.signup("presser", "presser@example.com", "password123")
145 .await;
146 h.login("presser", "password123").await;
147 h.client.fetch_csrf_token().await;
148 h
149 }
150
151 /// Every screen answers its own address, and says what region it changed.
152 ///
153 /// The retarget header is how a described answer says where it goes, and it is
154 /// the half of decision 7 that a rendering test cannot see: the markup is
155 /// identical whether or not the header is set, and without it htmx swaps the
156 /// answer into whatever element made the request.
157 #[tokio::test]
158 async fn every_described_screen_answers_into_the_region_it_names() {
159 for (screen, path, region) in SCREENS {
160 let mut h = viewing().await;
161 let resp = h.client.htmx_get(path).await;
162
163 assert_eq!(
164 resp.status, 200,
165 "{screen} did not answer {path} (got {})",
166 resp.status
167 );
168 let retarget = resp
169 .headers
170 .get("HX-Retarget")
171 .and_then(|v| v.to_str().ok())
172 .unwrap_or_default();
173 assert_eq!(
174 retarget,
175 format!("#{region}"),
176 "{screen} answered without naming {region} (got {retarget:?})"
177 );
178 }
179 }
180
181 /// A screen that emits no controls is a screen this suite cannot test.
182 ///
183 /// The guard against passing vacuously. Every assertion below iterates over what
184 /// `controls` found, so a rendering that quietly stopped emitting anything would
185 /// make all of them trivially true.
186 #[tokio::test]
187 async fn a_screen_offers_something() {
188 // ssh_keys is the richest: two add forms, a theme picker and, once a key
189 // exists, two destructive controls. If any screen has controls, it does.
190 let mut h = viewing().await;
191 let html = h.client.htmx_get("/dashboard/tabs/ssh-keys").await.text;
192
193 let found = controls(&html);
194 assert!(
195 found.len() >= 3,
196 "the ssh-keys screen emitted {} controls: {found:?}",
197 found.len()
198 );
199 assert!(
200 found.iter().any(|c| c.method == "POST"),
201 "no write among {found:?}"
202 );
203 }
204
205 /// Every control on every screen addresses something the server answers.
206 ///
207 /// Not a route-table assertion: the request is issued. A control whose address
208 /// is registered nowhere answers 404, and one whose route exists under another
209 /// verb answers 405, and both are the failure this catches. 403 is a pass, since
210 /// a control can legitimately address something this particular reader may not
211 /// do, and the CSRF layer is asserted separately in `csrf_coverage`.
212 #[tokio::test]
213 async fn every_described_control_is_answered() {
214 for (screen, path, _) in SCREENS {
215 let mut h = viewing().await;
216 let html = h.client.htmx_get(path).await.text;
217
218 for control in controls(&html) {
219 let resp = match control.method {
220 "GET" => h.client.htmx_get(&control.address).await,
221 "POST" => h.client.htmx_post_form(&control.address, "").await,
222 "PUT" => h.client.htmx_put_form(&control.address, "").await,
223 "DELETE" => h.client.htmx_delete(&control.address).await,
224 other => panic!("unhandled method {other}"),
225 };
226 let code = resp.status.as_u16();
227 assert!(
228 code != 404 && code != 405,
229 "{screen}: [{}] {} answered {code}, so the control renders and \
230 does nothing. This is the S3 failure class.",
231 control.method,
232 control.address
233 );
234 assert!(
235 code < 500,
236 "{screen}: [{}] {} answered {code}",
237 control.method,
238 control.address
239 );
240 }
241 }
242 }
243
244 /// A key on the ssh-keys screen, added the way the screen's own form adds one.
245 ///
246 /// Shared by the test below and by `a_described_delete_answers_with_the_screen_it_is_on`,
247 /// which cannot press a Remove that no row emitted.
248 const SEED_KEY: &str =
249 "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIB2n4ZVGJoGZ8pM5vJXVv0kL3T5V7wQ9dNqR8mY1uH6c";
250
251 /// Put a row on a screen, for the screens that can grow one through their own
252 /// description.
253 ///
254 /// Only `user_ssh_keys` today, because it is the one screen whose row this suite
255 /// can create by posting the form the description itself emitted. The others
256 /// need fixtures that do not exist yet, and the pressed-count guard in
257 /// `a_described_delete_answers_with_the_screen_it_is_on` is what keeps that from
258 /// being silent: it fails if the loop pressed nothing at all, so a screen
259 /// growing a fixture joins the coverage and a screen losing one is noticed.
260 async fn seed_a_deletable_row(h: &mut TestHarness, screen: &str) {
261 if screen != "user_ssh_keys" {
262 return;
263 }
264 let added = h
265 .client
266 .post_form(
267 "/api/users/me/ssh-keys",
268 &format!("public_key={}&label=fw13", urlencoding::encode(SEED_KEY)),
269 )
270 .await;
271 assert_eq!(
272 added.status, 200,
273 "seeding a key failed: {} {}",
274 added.status, added.text
275 );
276 }
277
278 /// Removing a key removes it, and answers with the pane rather than a list.
279 ///
280 /// The defect this file exists for, pinned end to end: seed a key, find the
281 /// control the description emitted for it, press it, and read both what came
282 /// back and what is left in the database.
283 #[tokio::test]
284 async fn pressing_remove_on_a_key_removes_it_and_redraws_the_pane() {
285 let mut h = viewing().await;
286
287 seed_a_deletable_row(&mut h, "user_ssh_keys").await;
288
289 let html = h.client.htmx_get("/dashboard/tabs/ssh-keys").await.text;
290 assert!(html.contains("fw13"), "the key is on the screen: {html}");
291
292 let remove = controls(&html)
293 .into_iter()
294 .find(|c| c.method == "DELETE" && c.address.contains("/keys/"))
295 .expect("the description emitted a Remove control");
296
297 let resp = h.client.htmx_delete(&remove.address).await;
298 assert_eq!(
299 resp.status, 200,
300 "pressing Remove answered {} at {}",
301 resp.status, remove.address
302 );
303
304 // The answer is this screen's own pane, named as such. Before 2026-08-11
305 // the control addressed the API route, whose answer is the Askama list
306 // fragment with no retarget: htmx put that table inside the button.
307 //
308 // Read from the screen rather than transcribed, for the reason SCREENS
309 // gives: this assertion said `#settings-body` until 2026-08-19, when the
310 // settings strip was described and the pane stopped being shared.
311 assert_eq!(
312 resp.headers
313 .get("HX-Retarget")
314 .and_then(|v| v.to_str().ok())
315 .unwrap_or_default(),
316 format!("#{}", quasi::ssh_keys::REGION),
317 "the answer did not name the pane it changed"
318 );
319 assert!(
320 !resp.text.contains("fw13"),
321 "the removed key is still drawn: {}",
322 resp.text
323 );
324 assert!(
325 resp.text.contains("No SSH keys registered."),
326 "the answer is the pane, empty: {}",
327 resp.text
328 );
329
330 // And it is gone for the next reader, not only from this response.
331 let after = h.client.htmx_get("/dashboard/tabs/ssh-keys").await.text;
332 assert!(!after.contains("fw13"), "the key came back: {after}");
333 }
334
335 /// A destructive control answers with a region, whatever screen it is on.
336 ///
337 /// The generalisation of the test above, and the one that will catch the next
338 /// conversion rather than this one. Any described `DELETE` must answer with the
339 /// screen's own region: an answer that names nothing is an answer htmx puts
340 /// inside the pressed button, and an answer that names another screen's region
341 /// swaps the wrong part of the page.
342 ///
343 /// # It has to be given a row first
344 ///
345 /// This pressed nothing at all until 2026-08-13. The reader is freshly signed
346 /// up, so every list on every screen was empty, so no row emitted a Remove and
347 /// the loop below ran zero times on all six screens. It passed throughout,
348 /// which is what a vacuous test does. [`seed_a_deletable_row`] is what gives it
349 /// something to press, and `pressed` is what stops the vacuity coming back:
350 /// a loop that asserts nothing now fails rather than reporting green.
351 #[tokio::test]
352 async fn a_described_delete_answers_with_the_screen_it_is_on() {
353 let mut pressed = 0_usize;
354 for (screen, path, region) in SCREENS {
355 let mut h = viewing().await;
356 seed_a_deletable_row(&mut h, screen).await;
357 let html = h.client.htmx_get(path).await.text;
358
359 for control in controls(&html).into_iter().filter(|c| c.method == "DELETE") {
360 let resp = h.client.htmx_delete(&control.address).await;
361 // A delete of something this seeded account does not have is fine,
362 // and 404 is how that is said. Every other code is the answer being
363 // wrong rather than the fixture being thin, so it is asserted
364 // instead of skipped.
365 //
366 // 200 exactly, not any 2xx. A 204 is the `library_contacts` bug in
367 // the module header: htmx never swaps one, so the row stays put
368 // whatever headers ride along with it. Tolerating the whole 2xx
369 // range here would have let the second of the two bugs this file
370 // was written for pass, since a 204 carrying an `HX-Retarget` it
371 // never acts on satisfies the assertion below.
372 if resp.status == 404 {
373 continue;
374 }
375 assert_eq!(
376 resp.status, 200,
377 "{screen}: DELETE {} answered {} rather than the region",
378 control.address, resp.status
379 );
380 let retarget = resp
381 .headers
382 .get("HX-Retarget")
383 .and_then(|v| v.to_str().ok())
384 .unwrap_or_default();
385 assert_eq!(
386 retarget,
387 format!("#{region}"),
388 "{screen}: DELETE {} succeeded without naming {region}",
389 control.address
390 );
391 pressed += 1;
392 }
393 }
394
395 // The guard the module header claims for controls in general, applied to
396 // the destructive ones: `a_screen_offers_something` sees the screens still
397 // emit controls, and cannot see that none of them is a DELETE.
398 assert!(
399 pressed > 0,
400 "no described DELETE was pressed on any of the {} screens, so this test \
401 asserted nothing. Seed a row for whichever screen lost its fixture.",
402 SCREENS.len()
403 );
404 }
405
406 /// The screen list here covers every screen the description layer can mount.
407 ///
408 /// `quasi::PATHS` is the authority and is already checked against `mounts`, so
409 /// this closes the loop: a screen added there but not here would never be
410 /// pressed, and the suite would keep passing while covering less.
411 #[tokio::test]
412 async fn every_described_screen_is_pressed() {
413 let mut pressed: Vec<&str> = SCREENS.iter().map(|(_, path, _)| *path).collect();
414 pressed.sort_unstable();
415 let mut mountable = quasi::PATHS.to_vec();
416 mountable.sort_unstable();
417
418 assert_eq!(
419 pressed, mountable,
420 "the pressed screens and the mountable ones have diverged"
421 );
422 }
423