package forge import ( "encoding/json" "net/http" "net/http/httptest" "net/url" "regexp" "strings" "sync" "testing" "time" ) type fakeCloudflare struct { sync.Mutex sent []map[string]string bounce bool headers http.Header } func (f *fakeCloudflare) ServeHTTP(w http.ResponseWriter, r *http.Request) { var msg map[string]string json.NewDecoder(r.Body).Decode(&msg) f.Lock() f.sent = append(f.sent, msg) f.headers = r.Header.Clone() bounce := f.bounce f.Unlock() if r.URL.Path != "/accounts/acct/email/sending/send" { w.WriteHeader(404) w.Write([]byte(`{"success":false,"errors":[{"code":10000,"message":"not found"}],"result":null}`)) return } if bounce { w.Write([]byte(`{"success":true,"errors":[],"result":{"delivered":[],"queued":[],"permanent_bounces":["` + msg["to"] + `"]}}`)) return } w.Write([]byte(`{"success":true,"errors":[],"result":{"delivered":["` + msg["to"] + `"],"queued":[],"permanent_bounces":[]}}`)) } func (f *fakeCloudflare) wait(t *testing.T, n int) map[string]string { t.Helper() for i := 0; i < 200; i++ { f.Lock() if len(f.sent) >= n { m := f.sent[n-1] f.Unlock() return m } f.Unlock() time.Sleep(10 * time.Millisecond) } t.Fatalf("message %d never arrived", n) return nil } func withCloudflare(t *testing.T) (*fakeCloudflare, *Mailer) { fake := &fakeCloudflare{} srv := httptest.NewServer(fake) t.Cleanup(srv.Close) old := cloudflareAPI cloudflareAPI = srv.URL t.Cleanup(func() { cloudflareAPI = old }) return fake, &Mailer{Account: "acct", Token: "secret", From: "gitpocket "} } func TestCloudflareSender(t *testing.T) { fake, m := withCloudflare(t) if err := m.Send("a@example.org", "hi\r\nBcc: x@evil.example", "body"); err != nil { t.Fatal(err) } got := fake.wait(t, 1) if got["from"] != "noreply@example.org" || got["to"] != "a@example.org" || strings.ContainsAny(got["subject"], "\r\n") { t.Errorf("sent %v", got) } if fake.headers.Get("Authorization") != "Bearer secret" { t.Error("token not sent") } fake.bounce = true if err := m.Send("b@example.org", "s", "b"); err == nil || !strings.Contains(err.Error(), "bounced") { t.Errorf("bounce not reported: %v", err) } m.Account = "other" if err := m.Send("c@example.org", "s", "b"); err == nil { t.Error("api error not reported") } } func TestSignupVerifyAndReset(t *testing.T) { fake, m := withCloudflare(t) s, h := testServer(t) s.mailer = m link := regexp.MustCompile(`https://example\.org(/[a-z]+/[0-9a-f]+)`) w := post(h, "/register", url.Values{"name": {"alice"}, "password": {"correct horse"}, "email": {"Alice@Example.org"}}, "") if w.Code != http.StatusSeeOther || session(w) == "" { t.Fatalf("signup with email failed: %d", w.Code) } verify := fake.wait(t, 1) if verify["to"] != "alice@example.org" { t.Fatalf("verification went to %q", verify["to"]) } path := link.FindStringSubmatch(verify["text"]) if path == nil || !strings.HasPrefix(path[1], "/verify/") { t.Fatalf("no verification link in %q", verify["text"]) } if w := get(h, path[1], ""); w.Code != 200 { t.Fatalf("verification link gave %d", w.Code) } post(h, "/reset", url.Values{"who": {"alice"}}, "") reset := fake.wait(t, 2) path = link.FindStringSubmatch(reset["text"]) if path == nil || !strings.HasPrefix(path[1], "/reset/") { t.Fatalf("no reset link in %q", reset["text"]) } if w := post(h, path[1], url.Values{"password": {"battery staple"}}, ""); w.Code != http.StatusSeeOther { t.Fatalf("reset gave %d", w.Code) } if w := post(h, path[1], url.Values{"password": {"another one!"}}, ""); w.Code != 400 { t.Errorf("reset link worked twice: %d", w.Code) } if _, ok := s.store.CheckPassword("alice", "battery staple"); !ok { t.Error("new password does not log in") } if w := post(h, "/register", url.Values{"name": {"bob"}, "password": {"correct horse"}, "email": {"bob@x.y\r\nBcc: z@evil.example"}}, ""); w.Code != 400 { t.Errorf("injected address accepted: %d", w.Code) } post(h, "/register", url.Values{"name": {"carol"}, "password": {"correct horse"}, "email": {"carol@example.org"}}, "") fake.wait(t, 3) post(h, "/reset", url.Values{"who": {"carol"}}, "") time.Sleep(100 * time.Millisecond) fake.Lock() n := len(fake.sent) fake.Unlock() if n != 3 { t.Errorf("reset mail sent to an unverified address, %d messages", n) } }