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) } }