159 lines
4.7 KiB
Go
159 lines
4.7 KiB
Go
package utils
|
|
|
|
import (
|
|
"bufio"
|
|
"net"
|
|
"strings"
|
|
"testing"
|
|
|
|
"nearle/config"
|
|
)
|
|
|
|
/*
|
|
Sending the invitation.
|
|
|
|
These run against a fake SMTP server on a local port rather than a mock, because
|
|
the thing worth testing is the conversation: Go's `net/smtp` decides on its own
|
|
whether to hand over a password, and the question is what this code does when a
|
|
relay does not offer encryption.
|
|
*/
|
|
|
|
// smtpStub answers just enough of the protocol to get to the interesting part.
|
|
// `offerTLS` is the switch the tests turn.
|
|
func smtpStub(t *testing.T, offerTLS bool) string {
|
|
t.Helper()
|
|
|
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatalf("listen: %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = listener.Close() })
|
|
|
|
go func() {
|
|
for {
|
|
conn, err := listener.Accept()
|
|
if err != nil {
|
|
return
|
|
}
|
|
go func() {
|
|
defer conn.Close()
|
|
reader := bufio.NewReader(conn)
|
|
write := func(line string) { _, _ = conn.Write([]byte(line + "\r\n")) }
|
|
|
|
write("220 stub ESMTP")
|
|
for {
|
|
line, err := reader.ReadString('\n')
|
|
if err != nil {
|
|
return
|
|
}
|
|
switch verb := strings.ToUpper(strings.TrimSpace(line)); {
|
|
case strings.HasPrefix(verb, "EHLO"):
|
|
write("250-stub")
|
|
if offerTLS {
|
|
write("250-STARTTLS")
|
|
}
|
|
write("250 AUTH PLAIN LOGIN")
|
|
case strings.HasPrefix(verb, "QUIT"):
|
|
write("221 bye")
|
|
return
|
|
default:
|
|
// Anything else is past the point these tests reach.
|
|
write("250 ok")
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
}()
|
|
|
|
return listener.Addr().String()
|
|
}
|
|
|
|
func mailerFor(t *testing.T, address string, username string) Mailer {
|
|
t.Helper()
|
|
|
|
host, portText, err := net.SplitHostPort(address)
|
|
if err != nil {
|
|
t.Fatalf("splitting %q: %v", address, err)
|
|
}
|
|
port := 0
|
|
for _, digit := range portText {
|
|
port = port*10 + int(digit-'0')
|
|
}
|
|
|
|
mailer, err := NewMailer(config.MailConfig{
|
|
Host: host, Port: port,
|
|
Username: username,
|
|
Password: "a-password-that-must-not-cross-the-wire",
|
|
FromAddress: "care@nearledaily.com", FromName: "Nearle",
|
|
ConsoleURL: "https://app.nearledaily.com",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("building the mailer: %v", err)
|
|
}
|
|
if mailer == nil {
|
|
t.Fatal("no mailer from a configured sender")
|
|
}
|
|
return mailer
|
|
}
|
|
|
|
func TestAPasswordIsNeverSentToARelayWithNoTLS(t *testing.T) {
|
|
// The downgrade this guards. An attacker between us and the relay can strip
|
|
// STARTTLS from the greeting; "carry on unencrypted" is the wrong answer,
|
|
// and we are about to send a real Google app password.
|
|
mailer := mailerFor(t, smtpStub(t, false), "care@nearledaily.com")
|
|
|
|
err := mailer.Send("owner@rmart.example", "Set your Nearle password", "link")
|
|
if err == nil {
|
|
t.Fatal("sent credentials to a relay offering no encryption")
|
|
}
|
|
if !strings.Contains(err.Error(), "TLS") {
|
|
// The reason has to name the connection. Reported as a credential
|
|
// refusal it sends somebody to check the password, which is fine.
|
|
t.Errorf("the failure does not name the real problem: %v", err)
|
|
}
|
|
if strings.Contains(err.Error(), "a-password-that-must-not-cross-the-wire") {
|
|
t.Error("the password is in the error message")
|
|
}
|
|
}
|
|
|
|
func TestARelayWithNoCredentialsIsLeftAlone(t *testing.T) {
|
|
// A relay that authenticates by network rather than by password is usually
|
|
// a local MTA on the same host, where there is no wire to protect. It must
|
|
// not be refused for want of TLS it does not need.
|
|
mailer := mailerFor(t, smtpStub(t, false), "")
|
|
|
|
// The stub accepts the envelope and the body, so this gets past the point
|
|
// the guard above would have stopped at. Whether the stub completes the
|
|
// whole conversation is not the question — being refused for TLS is.
|
|
err := mailer.Send("owner@rmart.example", "Set your Nearle password", "link")
|
|
if err != nil && strings.Contains(err.Error(), "does not offer TLS") {
|
|
t.Fatalf("refused an unauthenticated relay over TLS: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestAnUnconfiguredMailerIsNilRatherThanBroken(t *testing.T) {
|
|
// A deployment with no mail still boots and still onboards; the outcome
|
|
// says `invited: false` with the reason. See config.MailConfig.Why.
|
|
mailer, err := NewMailer(config.MailConfig{})
|
|
if err != nil {
|
|
t.Fatalf("an unconfigured mailer reported an error: %v", err)
|
|
}
|
|
if mailer != nil {
|
|
t.Fatal("built a mailer with no host")
|
|
}
|
|
}
|
|
|
|
func TestABadRecipientIsNamedRatherThanDialled(t *testing.T) {
|
|
// A merchant's primary email is typed by whoever onboarded them, so a typo
|
|
// is ordinary. It should be refused by name, before any connection.
|
|
mailer := mailerFor(t, smtpStub(t, true), "care@nearledaily.com")
|
|
|
|
err := mailer.Send("not an email", "Set your Nearle password", "link")
|
|
if err == nil {
|
|
t.Fatal("accepted an address that is not one")
|
|
}
|
|
if !strings.Contains(err.Error(), "not a valid email address") {
|
|
t.Errorf("unhelpful message: %v", err)
|
|
}
|
|
}
|