Smail/backend/utils/send/send_test.go
smail f60d3b0880
Some checks are pending
Security Checks / security (push) Waiting to run
整理前后端与测试构建目录并完善配置和安全引导
统一 frontend、backend、test、test-log 和 out 目录,更新构建、部署及测试入口;纳入配置管理兼容与首次安全引导修复;排除本机配置、密钥和测试产物。
2026-09-05 19:19:52 +08:00

161 lines
5.2 KiB
Go

package send
import (
"crypto/tls"
"errors"
"fmt"
"net"
"net/textproto"
"testing"
"git.pcux.cn/xuans/Smail/dto/parsemail"
appcontext "git.pcux.cn/xuans/Smail/utils/context"
)
func TestIsPermanentSMTPResponse(t *testing.T) {
tests := []struct {
name string
err error
want bool
}{
{name: "nil"},
{name: "network error", err: errors.New("connection refused")},
{name: "temporary SMTP response", err: &textproto.Error{Code: 451, Msg: "try later"}},
{name: "wrapped temporary SMTP response", err: fmt.Errorf("delivery: %w", &textproto.Error{Code: 421, Msg: "service unavailable"})},
{name: "permanent SMTP response", err: &textproto.Error{Code: 550, Msg: "mailbox unavailable"}, want: true},
{name: "wrapped permanent SMTP response", err: fmt.Errorf("delivery: %w", &textproto.Error{Code: 554, Msg: "transaction failed"}), want: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := isPermanentSMTPResponse(tt.err); got != tt.want {
t.Fatalf("isPermanentSMTPResponse() = %v, want %v", got, tt.want)
}
})
}
}
func TestDeliveryFailureCausePreservesTemporaryMXError(t *testing.T) {
tests := []struct {
name string
lookupErr *net.DNSError
}{
{
name: "SERVFAIL before fallback NXDOMAIN",
lookupErr: &net.DNSError{Err: "server misbehaving", Name: "example.com", IsTemporary: true},
},
{
name: "timeout before fallback NXDOMAIN",
lookupErr: &net.DNSError{Err: "i/o timeout", Name: "example.com", IsTimeout: true},
},
}
fallbackErr := &net.OpError{
Op: "dial",
Net: "tcp",
Err: &net.DNSError{Err: "no such host", Name: "smtp.example.com", IsNotFound: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := deliveryFailureCause(tt.lookupErr, fallbackErr)
var dnsErr *net.DNSError
if !errors.As(err, &dnsErr) {
t.Fatalf("deliveryFailureCause() = %T, want wrapped DNS error", err)
}
if dnsErr.IsNotFound {
t.Fatalf("deliveryFailureCause() selected fallback NXDOMAIN: %v", dnsErr)
}
if !dnsErr.IsTemporary && !dnsErr.IsTimeout {
t.Fatalf("deliveryFailureCause() selected non-temporary DNS error: %v", dnsErr)
}
})
}
}
func TestDeliveryFailureCauseKeepsExplicitSMTPRejection(t *testing.T) {
lookupErr := &net.DNSError{Err: "server misbehaving", Name: "example.com", IsTemporary: true}
rejection := &textproto.Error{Code: 550, Msg: "mailbox unavailable"}
if got := deliveryFailureCause(lookupErr, rejection); got != rejection {
t.Fatalf("deliveryFailureCause() = %v, want explicit SMTP rejection %v", got, rejection)
}
}
func TestCertificateVerificationErrorStopsFallback(t *testing.T) {
verificationErr := &tls.CertificateVerificationError{Err: errors.New("hostname mismatch")}
if !isCertificateVerificationError(fmt.Errorf("STARTTLS failed: %w", verificationErr)) {
t.Fatal("wrapped certificate verification error was not detected")
}
if isCertificateVerificationError(errors.New("connection refused")) {
t.Fatal("network error was treated as a certificate verification error")
}
}
func TestAllowUnsafeSMTPRequiresExplicitLoopbackTestTarget(t *testing.T) {
tests := []struct {
name string
domain mxDomain
want bool
}{
{name: "explicit local test", domain: mxDomain{domain: "localhost", mxHost: "127.0.0.1", localTest: true}, want: true},
{name: "external domain", domain: mxDomain{domain: "example.com", mxHost: "mx.example.com"}},
{name: "forged localhost name", domain: mxDomain{domain: "localhost", mxHost: "127.0.0.1"}},
{name: "test flag with external host", domain: mxDomain{domain: "localhost", mxHost: "192.0.2.1", localTest: true}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := allowUnsafeSMTP(tt.domain); got != tt.want {
t.Fatalf("allowUnsafeSMTP() = %v, want %v", got, tt.want)
}
})
}
}
func TestParseRecipientAddressRejectsUnsafeInput(t *testing.T) {
tests := []struct {
name string
user *parsemail.User
}{
{name: "nil recipient"},
{name: "empty address", user: &parsemail.User{}},
{name: "missing domain", user: &parsemail.User{EmailAddress: "recipient"}},
{name: "surrounding whitespace", user: &parsemail.User{EmailAddress: " recipient@example.com"}},
{name: "header injection", user: &parsemail.User{EmailAddress: "recipient@example.com\r\nBcc: victim@example.com"}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if _, _, err := parseRecipientAddress(tt.user); err == nil {
t.Fatal("unsafe recipient address was accepted")
}
})
}
}
func TestDoSendReportsInvalidRecipients(t *testing.T) {
err, failures := doSend(
&appcontext.Context{},
"example.com",
[]byte("message"),
[]*parsemail.User{{EmailAddress: "invalid"}, nil},
"sender@example.com",
)
if err == nil {
t.Fatal("invalid recipients were reported as delivered")
}
if len(failures) != 2 {
t.Fatalf("failure count = %d, want 2", len(failures))
}
}
func TestDoSendRejectsEmptyRecipientList(t *testing.T) {
err, failures := doSend(&appcontext.Context{}, "example.com", []byte("message"), nil, "sender@example.com")
if err == nil {
t.Fatal("empty recipient list was reported as delivered")
}
if len(failures) != 1 {
t.Fatalf("failure count = %d, want 1", len(failures))
}
}