163 lines
4.5 KiB
Go
163 lines
4.5 KiB
Go
package mail
|
|
|
|
import (
|
|
"net/smtp"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestSMTPMailerSendBuildsHeadersForValidInput(t *testing.T) {
|
|
mailer := &smtpMailer{cfg: Config{
|
|
Host: "smtp.example.com",
|
|
Port: "587",
|
|
From: "sender@example.com",
|
|
}}
|
|
|
|
var captured string
|
|
called := false
|
|
prev := smtpSendMail
|
|
smtpSendMail = func(addr string, auth smtp.Auth, from string, to []string, msg []byte) error {
|
|
called = true
|
|
if addr != "smtp.example.com:587" {
|
|
t.Fatalf("addr=%q", addr)
|
|
}
|
|
if from != "sender@example.com" {
|
|
t.Fatalf("from=%q", from)
|
|
}
|
|
if len(to) != 1 || to[0] != "recipient@example.com" {
|
|
t.Fatalf("to=%v", to)
|
|
}
|
|
captured = string(msg)
|
|
return nil
|
|
}
|
|
t.Cleanup(func() { smtpSendMail = prev })
|
|
|
|
err := mailer.Send(Message{
|
|
To: "recipient@example.com",
|
|
Subject: "Hello there",
|
|
Text: "plain body",
|
|
HTML: "<p>html body</p>",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !called {
|
|
t.Fatal("expected smtpSendMail to be called")
|
|
}
|
|
for _, want := range []string{
|
|
"From: sender@example.com",
|
|
"To: recipient@example.com",
|
|
"Subject: Hello there",
|
|
} {
|
|
if !strings.Contains(captured, want) {
|
|
t.Fatalf("message missing %q:\n%s", want, captured)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSMTPMailerSendRejectsHeaderInjection(t *testing.T) {
|
|
cases := []Message{
|
|
{To: "recipient@example.com", Subject: "ok\r\nBcc:evil@example.com", Text: "body"},
|
|
{To: "recipient@example.com\r\nBcc:evil@example.com", Subject: "ok", Text: "body"},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
mailer := &smtpMailer{cfg: Config{
|
|
Host: "smtp.example.com",
|
|
Port: "587",
|
|
From: "sender@example.com",
|
|
}}
|
|
|
|
called := false
|
|
prev := smtpSendMail
|
|
smtpSendMail = func(addr string, auth smtp.Auth, from string, to []string, msg []byte) error {
|
|
called = true
|
|
return nil
|
|
}
|
|
|
|
err := mailer.Send(tc)
|
|
smtpSendMail = prev
|
|
|
|
if err == nil {
|
|
t.Fatalf("expected error for %#v", tc)
|
|
}
|
|
if called {
|
|
t.Fatalf("smtpSendMail should not be called for %#v", tc)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestNewNoopWhenDisabledOrHostEmpty(t *testing.T) {
|
|
if New(Config{Enabled: false, Host: "smtp.example.com"}).Enabled() {
|
|
t.Fatal("disabled mailer must report Enabled=false")
|
|
}
|
|
if New(Config{Enabled: true, Host: ""}).Enabled() {
|
|
t.Fatal("empty host must be noop")
|
|
}
|
|
if !New(Config{Enabled: true, Host: "smtp.example.com", From: "a@b.c"}).Enabled() {
|
|
t.Fatal("enabled+host must be live SMTP mailer")
|
|
}
|
|
}
|
|
|
|
func TestNewDynamicResolvesPerCall(t *testing.T) {
|
|
calls := 0
|
|
host := "smtp-a.example.com"
|
|
m := NewDynamic(func() (Config, error) {
|
|
calls++
|
|
return ConfigFromParts(true, host, "587", "u", "p", "from@example.com"), nil
|
|
})
|
|
if !m.Enabled() {
|
|
t.Fatal("expected enabled")
|
|
}
|
|
host = "smtp-b.example.com"
|
|
if !m.Enabled() {
|
|
t.Fatal("expected still enabled after host change")
|
|
}
|
|
if calls < 2 {
|
|
t.Fatalf("expected resolve per Enabled call, got %d", calls)
|
|
}
|
|
}
|
|
|
|
func TestInviteAndSetPasswordMessagesEscapeHTML(t *testing.T) {
|
|
name := `Acme <script>alert("x")</script> & Co`
|
|
msg := InviteMessage("https://app.example", "a@b.c", "tok", name)
|
|
if strings.Contains(msg.HTML, "<script>") {
|
|
t.Fatalf("unescaped script in HTML: %s", msg.HTML)
|
|
}
|
|
if !strings.Contains(msg.HTML, "Acme <script>") || !strings.Contains(msg.HTML, "& Co") {
|
|
t.Fatalf("expected escaped company name, got %s", msg.HTML)
|
|
}
|
|
if !strings.Contains(msg.Text, name) {
|
|
t.Fatalf("text should keep company name: %s", msg.Text)
|
|
}
|
|
|
|
set := SetPasswordMessage("https://app.example", "a@b.c", `tok"onclick="alert(1)`)
|
|
if strings.Contains(set.HTML, `"onclick=`) {
|
|
t.Fatalf("unescaped token in set-password HTML: %s", set.HTML)
|
|
}
|
|
mig := MigratedSetPasswordMessage("https://app.example", "a@b.c", `tok"><img src=x>`)
|
|
if strings.Contains(mig.HTML, "<img") {
|
|
t.Fatalf("unescaped token in migrated HTML: %s", mig.HTML)
|
|
}
|
|
}
|
|
|
|
func TestSetPasswordURL(t *testing.T) {
|
|
got := SetPasswordURL("http://localhost:5174/", "tok123")
|
|
want := "http://localhost:5174/accept-invite#token=tok123&mode=set-password"
|
|
if got != want {
|
|
t.Fatalf("SetPasswordURL=%q want %q", got, want)
|
|
}
|
|
msg := SetPasswordMessage("http://localhost:5174", "u@example.com", "tok123")
|
|
if !strings.Contains(msg.Text, want) && !strings.Contains(msg.Text, "token=tok123&mode=set-password") {
|
|
t.Fatalf("SetPasswordMessage text missing link: %q", msg.Text)
|
|
}
|
|
}
|
|
|
|
func TestAcceptInviteURLUsesFragment(t *testing.T) {
|
|
got := AcceptInviteURL("http://localhost:5174/", "tok123")
|
|
want := "http://localhost:5174/accept-invite#token=tok123"
|
|
if got != want {
|
|
t.Fatalf("AcceptInviteURL=%q want %q", got, want)
|
|
}
|
|
}
|