carlos / movement Public

Clone
git clone https://amadan.net/carlos/movement

Plain git — no account needed to clone.

Download

Download this file

1package movementtest
2
3import (
4 "net/http"
5 "net/http/httptest"
6 "net/url"
7 "regexp"
8 "strings"
9 "testing"
10
11 movement "amadan.net/carlos/movement/internal/movement"
12)
13
14var confirmLink = regexp.MustCompile(`/confirm\?token=([0-9a-f]{64})`)
15
16// signUpAndTakeToken drives a real signup and returns the token from the
17// message that went out. The token exists nowhere else — only its hash is
18// stored — so reading the mail is the only way to get one, which is the
19// point.
20func signUpAndTakeToken(t *testing.T, h *testApp, email string) string {
21 t.Helper()
22 want := h.Mail.countTo(email) + 1
23 postSignup(t, h, submission(t, email, "curious"))
24
25 body := h.Mail.waitForMailTo(t, email, want)
26 m := confirmLink.FindStringSubmatch(body)
27 if m == nil {
28 t.Fatalf("no confirmation link in the message to %s:\n%s", email, body)
29 }
30 return m[1]
31}
32
33func postConfirm(t *testing.T, h *testApp, token string) *httptest.ResponseRecorder {
34 t.Helper()
35 form := url.Values{"token": {token}}
36 req := httptest.NewRequest(http.MethodPost, "/confirm", strings.NewReader(form.Encode()))
37 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
38 req.Header.Set("Origin", testOrigin)
39 rec := httptest.NewRecorder()
40 h.ServeHTTP(rec, req)
41 return rec
42}
43
44// The reason confirmation is a POST at all. Link-scanning gateways fetch
45// every URL in a message as it arrives, so a GET that confirmed would put
46// people on the list who never clicked — breaking the first half of the one
47// rule with the very mechanism meant to enforce it — and would then show the
48// actual human a dead link.
49func TestGetDoesNotConfirm(t *testing.T) {
50 h := newApp(t)
51 token := signUpAndTakeToken(t, h, "scanned@example.com")
52
53 rec := get(t, h, "/confirm?token="+token)
54 if rec.Code != http.StatusOK {
55 t.Fatalf("GET /confirm: %d", rec.Code)
56 }
57 if loadSignup(t, h, "scanned@example.com").Confirmed {
58 t.Fatal("a GET confirmed the address; a mail scanner would have joined for them")
59 }
60
61 // And the human's click still works afterwards.
62 postConfirm(t, h, token)
63 if !loadSignup(t, h, "scanned@example.com").Confirmed {
64 t.Fatal("the real click failed after a scanner had fetched the link")
65 }
66}
67
68func TestConfirmPutsTheAddressOnTheList(t *testing.T) {
69 h := newApp(t)
70 token := signUpAndTakeToken(t, h, "joining@example.com")
71
72 rec := postConfirm(t, h, token)
73 if rec.Code != http.StatusOK {
74 t.Fatalf("POST /confirm: %d", rec.Code)
75 }
76
77 row := loadSignup(t, h, "joining@example.com")
78 if !row.Confirmed {
79 t.Fatal("the address is not on the list")
80 }
81 if row.ConfirmedAt == nil {
82 t.Error("ConfirmedAt was not recorded")
83 }
84 if len(row.TokenHash) != 0 {
85 t.Error("the token hash survived; the link is still live")
86 }
87}
88
89// A burned token, an expired one and one that never existed are all the same
90// answer. There is nothing useful to tell apart and plenty to leak by trying.
91func TestSpentUnknownAndExpiredTokensAreIndistinguishable(t *testing.T) {
92 h := newApp(t)
93 token := signUpAndTakeToken(t, h, "once@example.com")
94 postConfirm(t, h, token)
95
96 spent := postConfirm(t, h, token).Body.String()
97 unknown := postConfirm(t, h, strings.Repeat("a", 64)).Body.String()
98 empty := postConfirm(t, h, "").Body.String()
99
100 if spent != unknown || unknown != empty {
101 t.Error("a spent, an unknown and an empty token must render the same page")
102 }
103 if !strings.Contains(spent, movement.T("confirm.dead")) {
104 t.Error("a dead link should say so")
105 }
106}
107
108func TestConfirmingTwiceDoesNotDoubleCount(t *testing.T) {
109 h := newApp(t)
110 token := signUpAndTakeToken(t, h, "twice@example.com")
111
112 postConfirm(t, h, token)
113 first := loadSignup(t, h, "twice@example.com")
114 postConfirm(t, h, token)
115 second := loadSignup(t, h, "twice@example.com")
116
117 if !first.ConfirmedAt.Equal(*second.ConfirmedAt) {
118 t.Error("a second click moved ConfirmedAt; the link is not single-use")
119 }
120}
121
122// A pending row that is submitted again gets a fresh token, and the old one
123// stops working — otherwise every message ever sent to an address stays a
124// live credential for it.
125func TestResubmittingInvalidatesTheOldLink(t *testing.T) {
126 h := newApp(t)
127 const addr = "renewed@example.com"
128
129 old := signUpAndTakeToken(t, h, addr)
130 fresh := signUpAndTakeToken(t, h, addr)
131 if old == fresh {
132 t.Fatal("resubmitting reused the same token")
133 }
134
135 postConfirm(t, h, old)
136 if loadSignup(t, h, addr).Confirmed {
137 t.Error("the superseded link still confirmed the address")
138 }
139
140 postConfirm(t, h, fresh)
141 if !loadSignup(t, h, addr).Confirmed {
142 t.Error("the current link did not work")
143 }
144}
145