//! Press the buttons on every described screen. //! //! The debt this pays off. Four conversion batches shipped with tests that //! rendered a screen and asserted the *address* each control carried was a //! registered route. Two controls were still wrong when pressed, and both got //! through because the address was never the thing that was broken: //! //! - `ssh_keys`' Remove addressed `DELETE /api/users/me/ssh-keys/{id}`, which //! answers an htmx request with the whole re-rendered Askama list. With no //! target htmx swapped that table into the button that was pressed. //! - `library_contacts`' Revoke addressed `DELETE /api/contacts/{id}`, which //! answers 204. htmx never swaps a 204, so the revoke landed and the row //! stayed until the reader left the tab and came back. //! //! Both are answers, not addresses, so no amount of route-table assertion would //! have found either. What finds them is issuing the request and reading what //! comes back, which is what everything below does. //! //! # Why this is here and not a sweep check of its own //! //! It runs nightly on astra already: the sweep's `test` check is //! `cargo nextest run --all-features` over `MNW/server`, with postgres and //! `TEST_DATABASE_URL` supplied by `[repo.MNW.env]`, and nextest's JUnit report //! names every failing test individually, so a red cell here carries the test //! name rather than one opaque verdict. A separate check would pay for a second //! full build of the server, which is the most expensive cell in the matrix, to //! buy a column heading. //! //! # The one shape to keep //! //! [`controls`] reads what the *description emitted* rather than a list written //! here by hand. A control that a future conversion adds is pressed by this //! suite the day it is added, without anybody remembering to extend a list, and //! a screen whose controls all disappear fails [`a_screen_offers_something`] //! rather than passing vacuously. use crate::harness::{BuildOptions, TestHarness}; use makenotwork::quasi; /// Every described screen, as (switch name, address, region it answers into). /// /// Written out rather than derived from `quasi::PATHS`, because the region is /// not on that list and the point of the third column is to assert the answer /// lands somewhere. Kept in step by `every_described_screen_is_pressed` below. /// /// The regions are *read* rather than transcribed wherever the screen publishes /// one: a transcribed region goes stale the moment a screen stops sharing /// `tab-content`. A constant this table can name is a constant this table cannot /// disagree with. const SCREENS: &[(&str, &str, &str)] = &[ ( "user_ssh_keys", quasi::ssh_keys::PATH, quasi::ssh_keys::REGION, ), ( "library_contacts", quasi::library_contacts::PATH, quasi::library_contacts::REGION, ), // The one screen that publishes no REGION, so this is the only row left // transcribing one. Read it from the screen the day it publishes one. ( "buyer_contacts", quasi::buyer_contacts::PATH, "contacts-section", ), ( "payout_summary", quasi::payout_summary::PATH, quasi::payout_summary::REGION, ), ( "user_analytics", quasi::user_analytics::PATH, quasi::user_analytics::REGION, ), ( "library_communities", quasi::forum_memberships::LIBRARY_PATH, quasi::forum_memberships::LIBRARY_REGION, ), ( "user_forums", quasi::forum_memberships::SETTINGS_PATH, quasi::forum_memberships::SETTINGS_REGION, ), ]; /// One control the description emitted. #[derive(Debug, Clone, PartialEq, Eq)] struct Control { method: &'static str, address: String, } /// Pull every control out of a rendered screen. /// /// Attribute-driven, so it sees exactly what a browser would act on and nothing /// a test author remembered to list. `href` is deliberately excluded: an anchor /// that also carries `hx-get` is already counted by the verb, and one that does /// not is either external or a download, neither of which this server answers. fn controls(html: &str) -> Vec { let mut found = Vec::new(); for (attr, method) in [ ("hx-get=\"", "GET"), ("hx-post=\"", "POST"), ("hx-put=\"", "PUT"), ("hx-delete=\"", "DELETE"), ] { let mut rest = html; while let Some(at) = rest.find(attr) { rest = &rest[at + attr.len()..]; let Some(end) = rest.find('"') else { break }; let address = rest[..end].replace("&", "&"); rest = &rest[end..]; // A form's own action is emitted on the form and again on nothing // else; a repeated address is a repeated control and both get // pressed, which costs one request and keeps the reader honest. found.push(Control { method, address }); } } found } /// A harness signed in as a creator. /// /// Every described screen serves unconditionally, so there is nothing to turn /// on and every screen in `SCREENS` is reachable from any of these harnesses. async fn viewing() -> TestHarness { let mut h = TestHarness::build(BuildOptions { // The forum screens refuse when Multithreaded is unconfigured, which is // deliberate and would otherwise read here as a broken screen. Pointed // at an address nothing answers on purpose: the upstream call then fails // and both screens fall back to an empty list, which is the path a real // outage takes and the one worth pressing. mt_base_url: Some("http://127.0.0.1:9".to_owned()), internal_shared_secret: Some("press-the-buttons".to_owned()), ..Default::default() }) .await; h.signup("presser", "presser@example.com", "password123") .await; h.login("presser", "password123").await; h.client.fetch_csrf_token().await; h } /// Every screen answers its own address, and says what region it changed. /// /// The retarget header is how a described answer says where it goes, and it is /// the half of decision 7 that a rendering test cannot see: the markup is /// identical whether or not the header is set, and without it htmx swaps the /// answer into whatever element made the request. #[tokio::test] async fn every_described_screen_answers_into_the_region_it_names() { for (screen, path, region) in SCREENS { let mut h = viewing().await; let resp = h.client.htmx_get(path).await; assert_eq!( resp.status, 200, "{screen} did not answer {path} (got {})", resp.status ); let retarget = resp .headers .get("HX-Retarget") .and_then(|v| v.to_str().ok()) .unwrap_or_default(); assert_eq!( retarget, format!("#{region}"), "{screen} answered without naming {region} (got {retarget:?})" ); } } /// A screen that emits no controls is a screen this suite cannot test. /// /// The guard against passing vacuously. Every assertion below iterates over what /// `controls` found, so a rendering that quietly stopped emitting anything would /// make all of them trivially true. #[tokio::test] async fn a_screen_offers_something() { // ssh_keys is the richest: two add forms, a theme picker and, once a key // exists, two destructive controls. If any screen has controls, it does. let mut h = viewing().await; let html = h.client.htmx_get("/dashboard/tabs/ssh-keys").await.text; let found = controls(&html); assert!( found.len() >= 3, "the ssh-keys screen emitted {} controls: {found:?}", found.len() ); assert!( found.iter().any(|c| c.method == "POST"), "no write among {found:?}" ); } /// Every control on every screen addresses something the server answers. /// /// Not a route-table assertion: the request is issued. A control whose address /// is registered nowhere answers 404, and one whose route exists under another /// verb answers 405, and both are the failure this catches. 403 is a pass, since /// a control can legitimately address something this particular reader may not /// do, and the CSRF layer is asserted separately in `csrf_coverage`. #[tokio::test] async fn every_described_control_is_answered() { for (screen, path, _) in SCREENS { let mut h = viewing().await; let html = h.client.htmx_get(path).await.text; for control in controls(&html) { let resp = match control.method { "GET" => h.client.htmx_get(&control.address).await, "POST" => h.client.htmx_post_form(&control.address, "").await, "PUT" => h.client.htmx_put_form(&control.address, "").await, "DELETE" => h.client.htmx_delete(&control.address).await, other => panic!("unhandled method {other}"), }; let code = resp.status.as_u16(); assert!( code != 404 && code != 405, "{screen}: [{}] {} answered {code}, so the control renders and \ does nothing. This is the S3 failure class.", control.method, control.address ); assert!( code < 500, "{screen}: [{}] {} answered {code}", control.method, control.address ); } } } /// A key on the ssh-keys screen, added the way the screen's own form adds one. /// /// Shared by the test below and by `a_described_delete_answers_with_the_screen_it_is_on`, /// which cannot press a Remove that no row emitted. const SEED_KEY: &str = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIB2n4ZVGJoGZ8pM5vJXVv0kL3T5V7wQ9dNqR8mY1uH6c"; /// Put a row on a screen, for the screens that can grow one through their own /// description. /// /// Only `user_ssh_keys` today, because it is the one screen whose row this suite /// can create by posting the form the description itself emitted. The others /// need fixtures that do not exist yet, and the pressed-count guard in /// `a_described_delete_answers_with_the_screen_it_is_on` is what keeps that from /// being silent: it fails if the loop pressed nothing at all, so a screen /// growing a fixture joins the coverage and a screen losing one is noticed. async fn seed_a_deletable_row(h: &mut TestHarness, screen: &str) { if screen != "user_ssh_keys" { return; } let added = h .client .post_form( "/api/users/me/ssh-keys", &format!("public_key={}&label=fw13", urlencoding::encode(SEED_KEY)), ) .await; assert_eq!( added.status, 200, "seeding a key failed: {} {}", added.status, added.text ); } /// Removing a key removes it, and answers with the pane rather than a list. /// /// The defect this file exists for, pinned end to end: seed a key, find the /// control the description emitted for it, press it, and read both what came /// back and what is left in the database. #[tokio::test] async fn pressing_remove_on_a_key_removes_it_and_redraws_the_pane() { let mut h = viewing().await; seed_a_deletable_row(&mut h, "user_ssh_keys").await; let html = h.client.htmx_get("/dashboard/tabs/ssh-keys").await.text; assert!(html.contains("fw13"), "the key is on the screen: {html}"); let remove = controls(&html) .into_iter() .find(|c| c.method == "DELETE" && c.address.contains("/keys/")) .expect("the description emitted a Remove control"); let resp = h.client.htmx_delete(&remove.address).await; assert_eq!( resp.status, 200, "pressing Remove answered {} at {}", resp.status, remove.address ); // The answer is this screen's own pane, named as such. Before 2026-08-11 // the control addressed the API route, whose answer is the Askama list // fragment with no retarget: htmx put that table inside the button. // // Read from the screen rather than transcribed, for the reason SCREENS // gives: this assertion said `#settings-body` until 2026-08-19, when the // settings strip was described and the pane stopped being shared. assert_eq!( resp.headers .get("HX-Retarget") .and_then(|v| v.to_str().ok()) .unwrap_or_default(), format!("#{}", quasi::ssh_keys::REGION), "the answer did not name the pane it changed" ); assert!( !resp.text.contains("fw13"), "the removed key is still drawn: {}", resp.text ); assert!( resp.text.contains("No SSH keys registered."), "the answer is the pane, empty: {}", resp.text ); // And it is gone for the next reader, not only from this response. let after = h.client.htmx_get("/dashboard/tabs/ssh-keys").await.text; assert!(!after.contains("fw13"), "the key came back: {after}"); } /// A destructive control answers with a region, whatever screen it is on. /// /// The generalisation of the test above, and the one that will catch the next /// conversion rather than this one. Any described `DELETE` must answer with the /// screen's own region: an answer that names nothing is an answer htmx puts /// inside the pressed button, and an answer that names another screen's region /// swaps the wrong part of the page. /// /// # It has to be given a row first /// /// The reader is freshly signed up, so every list on every screen is empty and /// no row emits a Remove: the loop below would run zero times and pass /// vacuously. [`seed_a_deletable_row`] is what gives it something to press, and /// `pressed` is what keeps the vacuity out: a loop that asserts nothing fails /// rather than reporting green. #[tokio::test] async fn a_described_delete_answers_with_the_screen_it_is_on() { let mut pressed = 0_usize; for (screen, path, region) in SCREENS { let mut h = viewing().await; seed_a_deletable_row(&mut h, screen).await; let html = h.client.htmx_get(path).await.text; for control in controls(&html).into_iter().filter(|c| c.method == "DELETE") { let resp = h.client.htmx_delete(&control.address).await; // A delete of something this seeded account does not have is fine, // and 404 is how that is said. Every other code is the answer being // wrong rather than the fixture being thin, so it is asserted // instead of skipped. // // 200 exactly, not any 2xx. A 204 is the `library_contacts` bug in // the module header: htmx never swaps one, so the row stays put // whatever headers ride along with it. Tolerating the whole 2xx // range here would have let the second of the two bugs this file // was written for pass, since a 204 carrying an `HX-Retarget` it // never acts on satisfies the assertion below. if resp.status == 404 { continue; } assert_eq!( resp.status, 200, "{screen}: DELETE {} answered {} rather than the region", control.address, resp.status ); let retarget = resp .headers .get("HX-Retarget") .and_then(|v| v.to_str().ok()) .unwrap_or_default(); assert_eq!( retarget, format!("#{region}"), "{screen}: DELETE {} succeeded without naming {region}", control.address ); pressed += 1; } } // The guard the module header claims for controls in general, applied to // the destructive ones: `a_screen_offers_something` sees the screens still // emit controls, and cannot see that none of them is a DELETE. assert!( pressed > 0, "no described DELETE was pressed on any of the {} screens, so this test \ asserted nothing. Seed a row for whichever screen lost its fixture.", SCREENS.len() ); } /// The screen list here covers every screen the description layer can mount. /// /// `quasi::PATHS` is the authority and is already checked against `mounts`, so /// this closes the loop: a screen added there but not here would never be /// pressed, and the suite would keep passing while covering less. #[tokio::test] async fn every_described_screen_is_pressed() { let mut pressed: Vec<&str> = SCREENS.iter().map(|(_, path, _)| *path).collect(); pressed.sort_unstable(); let mut mountable = quasi::PATHS.to_vec(); mountable.sort_unstable(); assert_eq!( pressed, mountable, "the pressed screens and the mountable ones have diverged" ); } /// A screen that owns its document answers with one, not with a fragment. /// /// `quasi::DOCUMENT_PATHS` is the other half of the mount table and is not in /// `SCREENS` above, because the two answer differently: a panel names the region /// it changed and this suite reads that off `HX-Retarget`, and a document sets /// no such header. What is worth pressing here instead is that the document is /// whole: the head the shell owns, the header the assembly layer owns, and the /// screen's own body class. #[tokio::test] async fn every_document_screen_serves_a_whole_document() { for path in quasi::DOCUMENT_PATHS { let mut h = viewing().await; let resp = h.client.get(path).await; assert_eq!(resp.status, 200, "{path} did not answer"); let html = resp.text; assert!(html.starts_with(""), "{path}: {html}"); assert!(html.contains("/static/style.css"), "{path} lost its sheets"); assert!( html.contains("role=\"banner\""), "{path} lost the site header" ); assert!( html.contains("name=\"csrf-token\""), "{path} lost the token meta the classic scripts read" ); assert!(html.contains("skip-to-main"), "{path} lost its skip link"); } } /// A document screen a reader can type the address of answers the branded 401, /// not the adapter's bare 403. /// /// `pages.rs::unauthorized_page_offers_login_and_signup` is the shipped rule for /// `/feed` in particular; this is the same rule held for every document screen /// that follows it onto the description layer. #[tokio::test] async fn a_signed_out_reader_is_offered_the_way_in() { for path in quasi::DOCUMENT_PATHS { let mut h = TestHarness::new().await; let resp = h.client.get(path).await; assert_eq!( resp.status, 401, "{path} refused a stranger with the wrong code" ); assert!( resp.text.contains("href=\"/login\""), "{path}: {}", resp.text ); assert!( resp.text.contains("href=\"/join\""), "{path}: {}", resp.text ); } } /// `e0c0d991`. The shortcut is declared once on the shell, so every described /// screen offers it and none of them has to remember to. #[tokio::test] async fn a_described_screen_offers_the_shortcuts_key() { let mut h = viewing().await; let html = h.client.get("/pricing").await.text; // quasi's half: a hidden button per binding, firing on the key, aimed at // the overlay container. The renderer wires it; nothing here draws a list. assert!( html.contains("data-chrome"), "the chrome is emitted: {html:.400}" ); assert!( html.contains("Keyboard shortcuts"), "the binding is labelled: {html:.400}" ); assert!( html.contains("/shortcuts"), "and it reaches the listing: {html:.400}" ); // And the affordance it replaces is gone. assert!( !html.contains("toggleShortcutsHelp"), "the data-action link was retired: {html:.400}" ); } /// The listing is ours, and it answers as something drawn over the page rather /// than as a page of its own. #[tokio::test] async fn the_shortcuts_listing_names_every_key_this_site_binds() { let mut h = viewing().await; let html = h.client.htmx_get("/shortcuts").await.text; // The described binding, read off the Chrome rather than written twice. assert!(html.contains('?'), "the described key: {html:.400}"); // And the three the host still owns. for key in ["Cmd+K", "Esc", "Cmd+S"] { assert!(html.contains(key), "{key} is listed: {html:.400}"); } }