Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 5 additions & 93 deletions cmd/eraser/cli_test.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
package main

import (
"bufio"
"io"
"net"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
"testing"

"github.com/drumandbytes/eraser/internal/smtptest"
)

// cliEnv is an isolated install: a temp HOME, a config file and a small
Expand Down Expand Up @@ -115,95 +114,8 @@ func mustContain(t *testing.T, out string, wants ...string) {
}
}

// relay is a plaintext SMTP server for send tests; recipients in reject get
// a 550.
type relay struct {
host string
port int
mu sync.Mutex
got []string
reject map[string]bool
authFail bool
}

func newRelay(t *testing.T) *relay {
t.Helper()
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = ln.Close() })
host, portStr, _ := net.SplitHostPort(ln.Addr().String())
port, _ := strconv.Atoi(portStr)
rl := &relay{host: host, port: port, reject: map[string]bool{}}
go func() {
for {
conn, err := ln.Accept()
if err != nil {
return
}
go rl.serve(conn)
}
}()
return rl
}

func (rl *relay) serve(conn net.Conn) {
defer func() { _ = conn.Close() }()
r := bufio.NewReader(conn)
reply := func(s string) { _, _ = conn.Write([]byte(s + "\r\n")) }
reply("220 test")
for {
line, err := r.ReadString('\n')
if err != nil {
return
}
cmd := strings.TrimSpace(line)
rl.mu.Lock()
authFail := rl.authFail
rl.mu.Unlock()
switch up := strings.ToUpper(cmd); {
case strings.HasPrefix(up, "MAIL") && authFail:
reply("535 5.7.8 authentication failed")
case strings.HasPrefix(up, "RCPT"):
to := strings.Trim(cmd[strings.Index(cmd, ":")+1:], "<> ")
rl.mu.Lock()
rejected := rl.reject[to]
if !rejected {
rl.got = append(rl.got, to)
}
rl.mu.Unlock()
if rejected {
reply("550 no such user")
} else {
reply("250 ok")
}
case strings.HasPrefix(up, "DATA"):
reply("354 go ahead")
for {
l, err := r.ReadString('\n')
if err != nil || l == ".\r\n" {
break
}
}
reply("250 queued")
case strings.HasPrefix(up, "QUIT"):
reply("221 bye")
return
default:
reply("250 ok")
}
}
}

func (rl *relay) recipients() []string {
rl.mu.Lock()
defer rl.mu.Unlock()
return append([]string(nil), rl.got...)
}

// relayConfig is a valid single-profile config sending through rl.
func relayConfig(rl *relay, extra string) string {
func relayConfig(rl *smtptest.Relay, extra string) string {
return `profiles:
- id: default
first_name: Jane
Expand All @@ -213,8 +125,8 @@ func relayConfig(rl *relay, extra string) string {
email:
from: jane@example.com
smtp:
host: ` + rl.host + `
port: ` + strconv.Itoa(rl.port) + `
host: ` + rl.Host + `
port: ` + strconv.Itoa(rl.Port) + `
use_tls: false
options:
template: gdpr
Expand Down
40 changes: 17 additions & 23 deletions cmd/eraser/cmd_gaps_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package main
import (
"errors"
"fmt"
"io"
"net"
"net/http"
"net/http/httptest"
"os"
Expand All @@ -20,6 +22,7 @@ import (
"github.com/drumandbytes/eraser/internal/history"
"github.com/drumandbytes/eraser/internal/inbox"
"github.com/drumandbytes/eraser/internal/schedule"
"github.com/drumandbytes/eraser/internal/smtptest"
"github.com/drumandbytes/eraser/internal/web"
)

Expand All @@ -44,9 +47,7 @@ func TestCommandSetupErrors(t *testing.T) {
}
}

rl := newRelay(t)
two := newCLIEnv(t, relayConfig(rl, "")+" - id: x\n")
_ = two
rl := smtptest.Start(t)
multi := newCLIEnv(t, manualConfig)
for _, args := range [][]string{{"send"}, {"export", "-o", "x"}, {"fill", "--url", "x"}, {"confirm", "--url", "x"}, {"draft", "acme"}, {"mark-sent", "acme"}} {
if _, err := multi.run(t, "", args...); err == nil || !strings.Contains(err.Error(), "multiple profiles") {
Expand All @@ -71,7 +72,7 @@ func TestCommandSetupErrors(t *testing.T) {
}

func TestSendOutputBranches(t *testing.T) {
rl := newRelay(t)
rl := smtptest.Start(t)
cfg := strings.Replace(relayConfig(rl, ""), "profiles:\n", "profiles:\n - id: spouse\n first_name: John\n last_name: Doe\n email: john@example.com\n", 1)
e := newCLIEnv(t, cfg)
if err := os.WriteFile(e.brokersPath, []byte(testBrokersYAML+" - {id: formonly, name: Form Only, opt_out_url: https://form.example/optout, region: eu}\n"), 0o600); err != nil {
Expand Down Expand Up @@ -227,7 +228,7 @@ func TestAutoLoopAndModes(t *testing.T) {
waits := 0
waitForNextCycle = func(time.Duration) bool { waits++; return waits >= 2 }

rl := newRelay(t)
rl := smtptest.Start(t)
e := newCLIEnv(t, relayConfig(rl, ""))
t.Setenv("ERASER_AUTO_MODE", "")
t.Setenv("INVOCATION_ID", "")
Expand Down Expand Up @@ -275,7 +276,7 @@ func TestAutoLoopAndModes(t *testing.T) {
}

func TestAutoCycleFailures(t *testing.T) {
rl := newRelay(t)
rl := smtptest.Start(t)

blocker := filepath.Join(t.TempDir(), "file")
_ = os.WriteFile(blocker, nil, 0o600)
Expand Down Expand Up @@ -332,7 +333,7 @@ func TestMonitorExtras(t *testing.T) {
mustContain(t, out, "Monitoring 2 configured inboxes", "Archived 1 emails")

for typ, icon := range map[inbox.ResponseType]string{inbox.ResponsePending: "⏳", inbox.ResponseRejected: "❌", inbox.ResponseUnknown: "❓", inbox.ResponseConfirmationRequired: "🔗"} {
got, _ := captureStdout(t, func() {
got := captureStdout(t, func() {
printClassifiedResponse(inbox.ClassifiedResponse{Type: typ, Email: &inbox.Email{BrokerName: "X"}, ConfirmURL: "https://x.example/c", NeedsReview: true})
})
if !strings.Contains(got, icon) {
Expand All @@ -341,7 +342,7 @@ func TestMonitorExtras(t *testing.T) {
}
}

func captureStdout(t *testing.T, fn func()) (string, error) {
func captureStdout(t *testing.T, fn func()) string {
t.Helper()
r, w, err := os.Pipe()
if err != nil {
Expand All @@ -352,16 +353,8 @@ func captureStdout(t *testing.T, fn func()) (string, error) {
fn()
os.Stdout = orig
_ = w.Close()
var b strings.Builder
buf := make([]byte, 4096)
for {
n, err := r.Read(buf)
b.Write(buf[:n])
if err != nil {
break
}
}
return b.String(), nil
out, _ := io.ReadAll(r)
return string(out)
}

// serve runs until Ctrl+C, then shuts down cleanly.
Expand Down Expand Up @@ -407,9 +400,10 @@ func TestServeUntilInterrupted(t *testing.T) {

func freeTCPPort(t *testing.T) int {
t.Helper()
srv := httptest.NewServer(http.NotFoundHandler())
port := srv.Listener.Addr().(interface{ String() string }).String()
srv.Close()
n, _ := strconv.Atoi(port[strings.LastIndex(port, ":")+1:])
return n
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer func() { _ = ln.Close() }()
return ln.Addr().(*net.TCPAddr).Port
}
55 changes: 10 additions & 45 deletions cmd/eraser/cmd_ops_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package main

import (
"fmt"
"net"
"net/http"
"net/http/httptest"
"os"
Expand All @@ -13,58 +12,24 @@ import (
"testing"
"time"

"github.com/emersion/go-imap"
"github.com/emersion/go-imap/backend"
"github.com/emersion/go-imap/backend/memory"
"github.com/emersion/go-imap/server"

"github.com/drumandbytes/eraser/internal/broker"
"github.com/drumandbytes/eraser/internal/history"
"github.com/drumandbytes/eraser/internal/imaptest"
"github.com/drumandbytes/eraser/internal/schedule"
"github.com/drumandbytes/eraser/internal/smtptest"
)

// imapInbox serves go-imap's in-memory backend over STARTTLS on loopback
// (TLS verification is skipped for loopback) and returns the inbox: YAML.
// imapInbox serves INBOX with the given (from, subject, body) messages and
// returns its inbox: YAML.
func imapInbox(t *testing.T, mails ...[3]string) string {
t.Helper()
be := memory.New()
u, _ := be.Login(nil, "username", "password")
mb, _ := u.GetMailbox("INBOX")
for i, m := range mails {
msg := fmt.Sprintf("From: %s\r\nTo: jane@example.com\r\nSubject: %s\r\nDate: %s\r\nMessage-ID: <%d@t>\r\nContent-Type: text/plain\r\n\r\n%s",
m[0], m[1], time.Now().Format(time.RFC1123Z), i, m[2])
if err := mb.(*memory.Mailbox).CreateMessage(nil, time.Now(), strings.NewReader(msg)); err != nil {
t.Fatal(err)
}
}
ts := httptest.NewTLSServer(http.NotFoundHandler())
t.Cleanup(ts.Close)
srv := server.New(anyUser{be})
srv.TLSConfig = ts.TLS.Clone()
srv.ErrorLog = quietLog{}
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
srv := imaptest.Start(t, false)
for _, m := range mails {
srv.Deliver(t, "INBOX", m[0], m[1], m[2])
}
go func() { _ = srv.Serve(ln) }()
t.Cleanup(func() { _ = srv.Close() })
_, port, _ := net.SplitHostPort(ln.Addr().String())
return "inbox:\n enabled: true\n server: 127.0.0.1\n port: " + port + "\n email: username\n password: password\n"
return "inbox:\n enabled: true\n server: 127.0.0.1\n port: " + strconv.Itoa(srv.Inbox.Port) + "\n email: username\n password: password\n"
}

// anyUser lets any login name in with the memory backend's password, so
// two profiles' inboxes can have different addresses.
type anyUser struct{ *memory.Backend }

func (b anyUser) Login(ci *imap.ConnInfo, _, pass string) (backend.User, error) {
return b.Backend.Login(ci, "username", pass)
}

type quietLog struct{}

func (quietLog) Printf(string, ...interface{}) {}
func (quietLog) Println(...interface{}) {}

func addSent(t *testing.T, e *cliEnv, profile, brokerID, email string) {
t.Helper()
store, err := history.NewStore(history.DBPathFor(e.cfgPath))
Expand All @@ -79,7 +44,7 @@ func addSent(t *testing.T, e *cliEnv, profile, brokerID, email string) {
}

func TestMonitor(t *testing.T) {
rl := newRelay(t)
rl := smtptest.Start(t)
inboxYAML := imapInbox(t,
[3]string{"privacy@acme.example", "Re: Erasure request", "We have deleted your personal data from our systems."},
[3]string{"dpo@globex.example", "Your request", "Please complete our opt-out form at https://globex.example/optout"},
Expand Down Expand Up @@ -408,7 +373,7 @@ func TestScheduleStatusAndInstall(t *testing.T) {
}

func TestAutoOnce(t *testing.T) {
rl := newRelay(t)
rl := smtptest.Start(t)
e := newCLIEnv(t, relayConfig(rl, imapInbox(t, [3]string{"privacy@acme.example", "Re: request", "We deleted your data."})))
t.Setenv("XPC_SERVICE_NAME", "")
t.Setenv("INVOCATION_ID", "")
Expand Down
Loading
Loading