accounts/mail_test.go (3322 bytes)
1 package main 2 3 import ( 4 "bytes" 5 "io" 6 "mime" 7 "mime/quotedprintable" 8 "net/mail" 9 "strings" 10 "testing" 11 "time" 12 "unicode/utf8" 13 14 "pgregory.net/rapid" 15 ) 16 17 var testFrom = &mail.Address{Name: "Recently Written", Address: "posts@example.com"} 18 19 // TestFormatMessageProperty checks that any subject and body give a 20 // message that parses, has exactly the expected headers, and decodes back 21 // to the same subject and body. 22 func TestFormatMessageProperty(t *testing.T) { 23 rapid.Check(t, func(t *rapid.T) { 24 valid := rapid.String().Filter(utf8.ValidString) 25 m := message{ 26 To: "reader@example.com", 27 Subject: valid.Draw(t, "subject"), 28 Body: valid.Draw(t, "body"), 29 Unsubscribe: rapid.SampledFrom([]string{"", "https://example.com/account/unsubscribe?t=ABC"}).Draw(t, "unsub"), 30 } 31 data, err := formatMessage(testFrom, m, time.Unix(0, 0)) 32 if err != nil { 33 t.Fatal(err) 34 } 35 msg, err := mail.ReadMessage(bytes.NewReader(data)) 36 if err != nil { 37 t.Fatalf("message does not parse: %v\n%q", err, data) 38 } 39 wantHeaders := 8 40 if m.Unsubscribe != "" { 41 wantHeaders += 2 42 } 43 if len(msg.Header) != wantHeaders { 44 t.Fatalf("got headers %v, want %d (a line break in the subject must not add one)", msg.Header, wantHeaders) 45 } 46 subject, err := new(mime.WordDecoder).DecodeHeader(msg.Header.Get("Subject")) 47 if err != nil { 48 t.Fatal(err) 49 } 50 wantSubject := strings.NewReplacer("\r", " ", "\n", " ").Replace(m.Subject) 51 // net/mail removes spaces at the two ends of a plain header value, 52 // but not in an encoded word. 53 if strings.Trim(subject, " \t") != strings.Trim(wantSubject, " \t") { 54 t.Fatalf("subject: got %q, want %q", subject, wantSubject) 55 } 56 body, err := io.ReadAll(quotedprintable.NewReader(msg.Body)) 57 if err != nil { 58 t.Fatal(err) 59 } 60 lf := strings.NewReplacer("\r\n", "\n", "\r", "\n") 61 if got := lf.Replace(string(body)); got != lf.Replace(m.Body) { 62 t.Fatalf("body: got %q, want %q", got, m.Body) 63 } 64 if got := msg.Header.Get("List-Unsubscribe"); m.Unsubscribe != "" && got != "<"+m.Unsubscribe+">" { 65 t.Fatalf("List-Unsubscribe = %q", got) 66 } 67 }) 68 } 69 70 func TestFormatMessageRejectsBadAddress(t *testing.T) { 71 if _, err := formatMessage(testFrom, message{To: "not an address"}, time.Now()); err == nil { 72 t.Fatal("no error for a bad address") 73 } 74 } 75 76 func TestLoadConfigNamesMissingVariables(t *testing.T) { 77 env := map[string]string{"RW_BASE_URL": "https://example.com/", "SMTP_PORT": "2525"} 78 _, err := loadConfig(func(k string) string { return env[k] }, false) 79 if err == nil || err.Error() != "set these environment variables: RW_SITE_DIR, RW_DB_PATH, RW_MAIL_FROM, SMTP_HOST" { 80 t.Fatalf("err = %v", err) 81 } 82 83 env["RW_SITE_DIR"], env["RW_DB_PATH"] = "/srv/site", "/var/lib/rw/rw.db" 84 env["RW_MAIL_FROM"], env["SMTP_HOST"] = "Recently Written <posts@example.com>", "smtp.example.com" 85 c, err := loadConfig(func(k string) string { return env[k] }, false) 86 if err != nil { 87 t.Fatal(err) 88 } 89 if c.BaseURL != "https://example.com" || c.SMTPAddr != "smtp.example.com:2525" || 90 c.MailFrom.Address != "posts@example.com" || c.Listen != "127.0.0.1:8081" { 91 t.Fatalf("config = %+v", c) 92 } 93 94 delete(env, "SMTP_HOST") 95 if _, err := loadConfig(func(k string) string { return env[k] }, true); err != nil { 96 t.Fatalf("dev mode needs no SMTP: %v", err) 97 } 98 }