Skip to main content

max / makenotwork

Split the signup and notify tests out of the pages workflow module pages.rs reached 802 lines and tripped the oversized-module ratchet, which asks for a split by domain rather than a trim. The mailing-list tests are that domain: the form lives on a page, but what they assert is what happens to the address afterwards. 802 to 604, with 207 in the new module.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-24 20:50 UTC
Signed with PGP, not checked
Commit: 933cd28d6b651fa208512d37250c5dc681d7d985
Parent: 5eb60a4
3 files changed, +208 insertions, -198 deletions
@@ -108,6 +108,7 @@
108 108 mod seed_buyer;
109 109 mod seed_examples;
110 110 mod session_revocation;
111 + mod signup_notify;
111 112 mod ssh_keys;
112 113 mod ssh_management;
113 114 mod sso;
@@ -490,204 +490,6 @@
490 490 /// It had no action and no method, so a browser without JS submitted a GET to
491 491 /// `/` and the address was dropped silently. This is the only email capture on
492 492 /// the landing page, so a silent loss is a lost signup nobody can count.
493 - #[tokio::test]
494 - async fn notify_form_without_js_stores_the_address() {
495 - let mut h = TestHarness::new().await;
496 - h.client.fetch_csrf_token().await;
497 -
498 - let resp = h
499 - .client
500 - .post_form("/notify", "email=nojs@example.com")
501 - .await;
502 - assert_eq!(
503 - resp.status, 303,
504 - "expected a redirect back to the landing page"
505 - );
506 - let location = resp
507 - .headers
508 - .get("location")
509 - .and_then(|v| v.to_str().ok())
510 - .unwrap_or_default();
511 - assert!(
512 - location.starts_with("/?notify=ok"),
513 - "redirected to {location}"
514 - );
515 -
516 - let stored: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM email_signups WHERE email = $1")
517 - .bind("nojs@example.com")
518 - .fetch_one(&h.db)
519 - .await
520 - .expect("count signups");
521 - assert_eq!(stored, 1, "the address never reached storage");
522 - }
523 -
524 - /// A rejected address comes back as a sentence on the page, not a 422. This is
525 - /// the last thing on the landing page and an error code is a worse outcome
526 - /// than being told the address looked wrong.
527 - #[tokio::test]
528 - async fn notify_form_rejects_a_bad_address_without_erroring() {
529 - let mut h = TestHarness::new().await;
530 - h.client.fetch_csrf_token().await;
531 -
532 - let resp = h.client.post_form("/notify", "email=not-an-address").await;
533 - assert_eq!(resp.status, 303);
534 - assert!(
535 - resp.headers
536 - .get("location")
537 - .and_then(|v| v.to_str().ok())
538 - .unwrap_or_default()
539 - .starts_with("/?notify=invalid")
540 - );
541 -
542 - let landing = h.client.get("/?notify=invalid").await;
543 - assert!(
544 - landing.text.contains("That address didn't look right."),
545 - "the landing page said nothing about the rejection"
546 - );
547 - }
548 -
549 - /// The success message renders for a no-JS visitor coming back off the
550 - /// redirect, and not on a plain page load.
551 - #[tokio::test]
552 - async fn notify_status_renders_only_when_the_redirect_says_so() {
553 - let mut h = TestHarness::new().await;
554 -
555 - let plain = h.client.get("/").await;
556 - assert!(
557 - !plain.text.contains("You're on the list."),
558 - "status shown on a plain load"
559 - );
560 -
561 - let after = h.client.get("/?notify=ok").await;
562 - assert!(after.text.contains("You're on the list."));
563 - }
564 -
565 - // ── Landing signup unsubscribe ──
566 - //
567 - // Step 1 of wiki [[mnw-mailing-lists]]. The table had insert, admin read and
568 - // count, and no way out. Withdrawal has to be as easy as consent, and consent
569 - // is one form field.
570 -
571 - fn signup_unsub_url(email: &str) -> String {
572 - makenotwork::email::generate_signup_unsubscribe_url(
573 - "",
574 - email,
575 - "test-signing-secret-for-integration-tests",
576 - )
577 - }
578 -
579 - /// The signed link unsubscribes, and the address stops being mailable.
580 - #[tokio::test]
581 - async fn signup_unsubscribe_link_removes_the_address_from_the_mailable_list() {
582 - let mut h = TestHarness::new().await;
583 - h.client.fetch_csrf_token().await;
584 - h.client
585 - .post_form("/notify", "email=leaving@example.com")
586 - .await;
587 -
588 - let before: i64 = sqlx::query_scalar(
589 - "SELECT COUNT(*) FROM email_signups WHERE email = $1 AND unsubscribed_at IS NULL",
590 - )
591 - .bind("leaving@example.com")
592 - .fetch_one(&h.db)
593 - .await
594 - .expect("count");
595 - assert_eq!(before, 1, "signup did not land");
596 -
597 - let resp = h.client.get(&signup_unsub_url("leaving@example.com")).await;
598 - assert_eq!(resp.status, 200);
599 -
600 - let after: i64 = sqlx::query_scalar(
601 - "SELECT COUNT(*) FROM email_signups WHERE email = $1 AND unsubscribed_at IS NULL",
602 - )
603 - .bind("leaving@example.com")
604 - .fetch_one(&h.db)
605 - .await
606 - .expect("count");
607 - assert_eq!(after, 0, "still mailable after unsubscribing");
608 -
609 - // Marked, not deleted: a deleted address is one the next import re-adds.
610 - let retained: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM email_signups WHERE email = $1")
611 - .bind("leaving@example.com")
612 - .fetch_one(&h.db)
613 - .await
614 - .expect("count");
615 - assert_eq!(retained, 1, "the opt-out record was thrown away");
616 - }
617 -
618 - /// RFC 8058: a POST to the same URL unsubscribes with no confirmation step,
619 - /// which is what the List-Unsubscribe-Post header promises Gmail and Yahoo.
620 - /// Retries must not fail, so the second POST answers the same as the first.
621 - #[tokio::test]
622 - async fn signup_unsubscribe_one_click_post_is_idempotent() {
623 - let mut h = TestHarness::new().await;
624 - h.client.fetch_csrf_token().await;
625 - h.client
626 - .post_form("/notify", "email=oneclick@example.com")
627 - .await;
628 -
629 - let url = signup_unsub_url("oneclick@example.com");
630 - let first = h.client.post_form(&url, "List-Unsubscribe=One-Click").await;
631 - assert_eq!(first.status, 200);
632 - let second = h.client.post_form(&url, "List-Unsubscribe=One-Click").await;
633 - assert_eq!(second.status, 200, "a retried one-click POST must not fail");
634 - }
635 -
636 - /// A tampered signature does nothing. The address is signed over, so a token
637 - /// for one address cannot unsubscribe another.
638 - #[tokio::test]
639 - async fn signup_unsubscribe_rejects_a_forged_link() {
640 - let mut h = TestHarness::new().await;
641 - h.client.fetch_csrf_token().await;
642 - h.client
643 - .post_form("/notify", "email=victim@example.com")
644 - .await;
645 -
646 - // Take a valid token for one address and point it at another.
647 - let forged = signup_unsub_url("attacker@example.com")
648 - .replace("attacker%40example.com", "victim%40example.com");
649 - let resp = h.client.get(&forged).await;
650 - assert!(
651 - resp.text.contains("Invalid Link"),
652 - "a forged link was accepted"
653 - );
654 -
655 - let still_mailable: i64 = sqlx::query_scalar(
656 - "SELECT COUNT(*) FROM email_signups WHERE email = $1 AND unsubscribed_at IS NULL",
657 - )
658 - .bind("victim@example.com")
659 - .fetch_one(&h.db)
660 - .await
661 - .expect("count");
662 - assert_eq!(still_mailable, 1, "a forged link unsubscribed someone");
663 - }
664 -
665 - /// Signing up again after unsubscribing re-subscribes. The form is the only
666 - /// interface, so refusing would leave someone unable to opt back in.
667 - #[tokio::test]
668 - async fn signing_up_again_after_unsubscribing_restores_the_subscription() {
669 - let mut h = TestHarness::new().await;
670 - h.client.fetch_csrf_token().await;
671 - h.client
672 - .post_form("/notify", "email=returning@example.com")
673 - .await;
674 - h.client
675 - .get(&signup_unsub_url("returning@example.com"))
676 - .await;
677 -
678 - h.client
679 - .post_form("/notify", "email=returning@example.com")
680 - .await;
681 -
682 - let mailable: i64 = sqlx::query_scalar(
683 - "SELECT COUNT(*) FROM email_signups WHERE email = $1 AND unsubscribed_at IS NULL",
684 - )
685 - .bind("returning@example.com")
686 - .fetch_one(&h.db)
687 - .await
688 - .expect("count");
689 - assert_eq!(mailable, 1, "could not opt back in");
690 - }
691 493
692 494 /// A method mismatch on a path that exists renders the branded error page.
693 495 ///
@@ -1,0 +1,207 @@
1 + //! Signup and notify mailing-list tests: storing an address, the unsubscribe
2 + //! link, and resubscribing.
3 + //!
4 + //! Split out of `pages` because the subject is the mailing list's lifecycle
5 + //! rather than whether a route renders. The form lives on a page; what these
6 + //! assert is what happens to the address afterwards.
7 +
8 + use crate::harness::TestHarness;
9 +
10 + #[tokio::test]
11 + async fn notify_form_without_js_stores_the_address() {
12 + let mut h = TestHarness::new().await;
13 + h.client.fetch_csrf_token().await;
14 +
15 + let resp = h
16 + .client
17 + .post_form("/notify", "email=nojs@example.com")
18 + .await;
19 + assert_eq!(
20 + resp.status, 303,
21 + "expected a redirect back to the landing page"
22 + );
23 + let location = resp
24 + .headers
25 + .get("location")
26 + .and_then(|v| v.to_str().ok())
27 + .unwrap_or_default();
28 + assert!(
29 + location.starts_with("/?notify=ok"),
30 + "redirected to {location}"
31 + );
32 +
33 + let stored: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM email_signups WHERE email = $1")
34 + .bind("nojs@example.com")
35 + .fetch_one(&h.db)
36 + .await
37 + .expect("count signups");
38 + assert_eq!(stored, 1, "the address never reached storage");
39 + }
40 +
41 + /// A rejected address comes back as a sentence on the page, not a 422. This is
42 + /// the last thing on the landing page and an error code is a worse outcome
43 + /// than being told the address looked wrong.
44 + #[tokio::test]
45 + async fn notify_form_rejects_a_bad_address_without_erroring() {
46 + let mut h = TestHarness::new().await;
47 + h.client.fetch_csrf_token().await;
48 +
49 + let resp = h.client.post_form("/notify", "email=not-an-address").await;
50 + assert_eq!(resp.status, 303);
51 + assert!(
52 + resp.headers
53 + .get("location")
54 + .and_then(|v| v.to_str().ok())
55 + .unwrap_or_default()
56 + .starts_with("/?notify=invalid")
57 + );
58 +
59 + let landing = h.client.get("/?notify=invalid").await;
60 + assert!(
61 + landing.text.contains("That address didn't look right."),
62 + "the landing page said nothing about the rejection"
63 + );
64 + }
65 +
66 + /// The success message renders for a no-JS visitor coming back off the
67 + /// redirect, and not on a plain page load.
68 + #[tokio::test]
69 + async fn notify_status_renders_only_when_the_redirect_says_so() {
70 + let mut h = TestHarness::new().await;
71 +
72 + let plain = h.client.get("/").await;
73 + assert!(
74 + !plain.text.contains("You're on the list."),
75 + "status shown on a plain load"
76 + );
77 +
78 + let after = h.client.get("/?notify=ok").await;
79 + assert!(after.text.contains("You're on the list."));
80 + }
81 +
82 + // ── Landing signup unsubscribe ──
83 + //
84 + // Step 1 of wiki [[mnw-mailing-lists]]. The table had insert, admin read and
85 + // count, and no way out. Withdrawal has to be as easy as consent, and consent
86 + // is one form field.
87 +
88 + fn signup_unsub_url(email: &str) -> String {
89 + makenotwork::email::generate_signup_unsubscribe_url(
90 + "",
91 + email,
92 + "test-signing-secret-for-integration-tests",
93 + )
94 + }
95 +
96 + /// The signed link unsubscribes, and the address stops being mailable.
97 + #[tokio::test]
98 + async fn signup_unsubscribe_link_removes_the_address_from_the_mailable_list() {
99 + let mut h = TestHarness::new().await;
100 + h.client.fetch_csrf_token().await;
101 + h.client
102 + .post_form("/notify", "email=leaving@example.com")
103 + .await;
104 +
105 + let before: i64 = sqlx::query_scalar(
106 + "SELECT COUNT(*) FROM email_signups WHERE email = $1 AND unsubscribed_at IS NULL",
107 + )
108 + .bind("leaving@example.com")
109 + .fetch_one(&h.db)
110 + .await
111 + .expect("count");
112 + assert_eq!(before, 1, "signup did not land");
113 +
114 + let resp = h.client.get(&signup_unsub_url("leaving@example.com")).await;
115 + assert_eq!(resp.status, 200);
116 +
117 + let after: i64 = sqlx::query_scalar(
118 + "SELECT COUNT(*) FROM email_signups WHERE email = $1 AND unsubscribed_at IS NULL",
119 + )
120 + .bind("leaving@example.com")
121 + .fetch_one(&h.db)
122 + .await
123 + .expect("count");
124 + assert_eq!(after, 0, "still mailable after unsubscribing");
125 +
126 + // Marked, not deleted: a deleted address is one the next import re-adds.
127 + let retained: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM email_signups WHERE email = $1")
128 + .bind("leaving@example.com")
129 + .fetch_one(&h.db)
130 + .await
131 + .expect("count");
132 + assert_eq!(retained, 1, "the opt-out record was thrown away");
133 + }
134 +
135 + /// RFC 8058: a POST to the same URL unsubscribes with no confirmation step,
136 + /// which is what the List-Unsubscribe-Post header promises Gmail and Yahoo.
137 + /// Retries must not fail, so the second POST answers the same as the first.
138 + #[tokio::test]
139 + async fn signup_unsubscribe_one_click_post_is_idempotent() {
140 + let mut h = TestHarness::new().await;
141 + h.client.fetch_csrf_token().await;
142 + h.client
143 + .post_form("/notify", "email=oneclick@example.com")
144 + .await;
145 +
146 + let url = signup_unsub_url("oneclick@example.com");
147 + let first = h.client.post_form(&url, "List-Unsubscribe=One-Click").await;
148 + assert_eq!(first.status, 200);
149 + let second = h.client.post_form(&url, "List-Unsubscribe=One-Click").await;
150 + assert_eq!(second.status, 200, "a retried one-click POST must not fail");
151 + }
152 +
153 + /// A tampered signature does nothing. The address is signed over, so a token
154 + /// for one address cannot unsubscribe another.
155 + #[tokio::test]
156 + async fn signup_unsubscribe_rejects_a_forged_link() {
157 + let mut h = TestHarness::new().await;
158 + h.client.fetch_csrf_token().await;
159 + h.client
160 + .post_form("/notify", "email=victim@example.com")
161 + .await;
162 +
163 + // Take a valid token for one address and point it at another.
164 + let forged = signup_unsub_url("attacker@example.com")
165 + .replace("attacker%40example.com", "victim%40example.com");
166 + let resp = h.client.get(&forged).await;
167 + assert!(
168 + resp.text.contains("Invalid Link"),
169 + "a forged link was accepted"
170 + );
171 +
172 + let still_mailable: i64 = sqlx::query_scalar(
173 + "SELECT COUNT(*) FROM email_signups WHERE email = $1 AND unsubscribed_at IS NULL",
174 + )
175 + .bind("victim@example.com")
176 + .fetch_one(&h.db)
177 + .await
178 + .expect("count");
179 + assert_eq!(still_mailable, 1, "a forged link unsubscribed someone");
180 + }
181 +
182 + /// Signing up again after unsubscribing re-subscribes. The form is the only
183 + /// interface, so refusing would leave someone unable to opt back in.
184 + #[tokio::test]
185 + async fn signing_up_again_after_unsubscribing_restores_the_subscription() {
186 + let mut h = TestHarness::new().await;
187 + h.client.fetch_csrf_token().await;
188 + h.client
189 + .post_form("/notify", "email=returning@example.com")
190 + .await;
191 + h.client
192 + .get(&signup_unsub_url("returning@example.com"))
193 + .await;
194 +
195 + h.client
196 + .post_form("/notify", "email=returning@example.com")
197 + .await;
198 +
199 + let mailable: i64 = sqlx::query_scalar(
200 + "SELECT COUNT(*) FROM email_signups WHERE email = $1 AND unsubscribed_at IS NULL",
201 + )
202 + .bind("returning@example.com")
203 + .fetch_one(&h.db)
204 + .await
205 + .expect("count");
206 + assert_eq!(mailable, 1, "could not opt back in");
207 + }