mox/integration_test.go
Mechiel Lukkien 03c3f56a59
add basic tests for the ctl subcommands, and fix two small bugs
this doesn't really test the output of the ctl commands, just that they succeed
without error. better than nothing...

testing found two small bugs, that are not an issue in practice:

1. we were ack'ing streamed data from the other side of the ctl connection
before having read it. when there is no buffer space on the connection (always
the case for net.Pipe) that would cause a deadlock. only actually happened
during the new tests.

2. the generated dkim keys are relatively to the directory of the dynamic
config file. mox looked it up relative to the directory of the _static_ config
file at startup. this directory is typicaly the same. users would have noticed
if they had triggered this.
2023-07-02 14:18:50 +02:00

180 lines
5.2 KiB
Go

//go:build integration
// Run this using docker-compose.yml, see Makefile.
package main
import (
"context"
"fmt"
"net"
"os"
"os/exec"
"strings"
"testing"
"time"
"github.com/mjl-/mox/dns"
"github.com/mjl-/mox/imapclient"
"github.com/mjl-/mox/mlog"
"github.com/mjl-/mox/mox-"
"github.com/mjl-/mox/sasl"
"github.com/mjl-/mox/smtpclient"
"github.com/mjl-/mox/store"
)
var ctxbg = context.Background()
func tcheck(t *testing.T, err error, errmsg string) {
if err != nil {
t.Fatalf("%s: %s", errmsg, err)
}
}
// Submit a message to mox, which sends it to postfix, which forwards back to mox.
// We check if we receive the message.
func TestDeliver(t *testing.T) {
mlog.Logfmt = true
log := mlog.New("test")
// Remove state.
os.RemoveAll("testdata/integration/data")
os.MkdirAll("testdata/integration/data", 0750)
// Cleanup afterwards, these are owned by root, annoying to have around due to
// permission errors.
defer os.RemoveAll("testdata/integration/data")
// Load mox config.
mox.ConfigStaticPath = "testdata/integration/config/mox.conf"
if errs := mox.LoadConfig(ctxbg, true, false); len(errs) > 0 {
t.Fatalf("loading mox config: %v", errs)
}
// Create new accounts
createAccount := func(email, password string) {
t.Helper()
acc, _, err := store.OpenEmail(email)
tcheck(t, err, "open account")
err = acc.SetPassword(password)
tcheck(t, err, "setting password")
err = acc.Close()
tcheck(t, err, "closing account")
}
createAccount("moxtest1@mox1.example", "pass1234")
createAccount("moxtest2@mox2.example", "pass1234")
createAccount("moxtest3@mox3.example", "pass1234")
// Start mox.
const mtastsdbRefresher = false
const skipForkExec = true
err := start(mtastsdbRefresher, skipForkExec)
tcheck(t, err, "starting mox")
// Single update from IMAP IDLE.
type idleResponse struct {
untagged imapclient.Untagged
err error
}
testDeliver := func(checkTime bool, imapaddr, imapuser, imappass string, fn func()) {
t.Helper()
// Make IMAP connection, we'll wait for a delivery notification with IDLE.
imapconn, err := net.Dial("tcp", imapaddr)
tcheck(t, err, "dial imap server")
defer imapconn.Close()
client, err := imapclient.New(imapconn, false)
tcheck(t, err, "new imapclient")
_, _, err = client.Login(imapuser, imappass)
tcheck(t, err, "imap client login")
_, _, err = client.Select("inbox")
tcheck(t, err, "imap select inbox")
err = client.Commandf("", "idle")
tcheck(t, err, "imap idle command")
_, _, _, err = client.ReadContinuation()
tcheck(t, err, "read imap continuation")
idle := make(chan idleResponse)
go func() {
for {
untagged, err := client.ReadUntagged()
idle <- idleResponse{untagged, err}
if err != nil {
return
}
}
}()
defer func() {
err := client.Writelinef("done")
tcheck(t, err, "aborting idle")
}()
t0 := time.Now()
fn()
// Wait for notification of delivery.
select {
case resp := <-idle:
tcheck(t, resp.err, "idle notification")
_, ok := resp.untagged.(imapclient.UntaggedExists)
if !ok {
t.Fatalf("got idle %#v, expected untagged exists", resp.untagged)
}
if d := time.Since(t0); checkTime && d < 1*time.Second {
t.Fatalf("delivery took %v, but should have taken at least 1 second, the first-time sender delay", d)
}
case <-time.After(5 * time.Second):
t.Fatalf("timeout after 5s waiting for IMAP IDLE notification of new message, should take about 1 second")
}
}
submit := func(smtphost, smtpport, mailfrom, password, rcptto string) {
conn, err := net.Dial("tcp", net.JoinHostPort(smtphost, smtpport))
tcheck(t, err, "dial submission")
defer conn.Close()
msg := fmt.Sprintf(`From: <%s>
To: <%s>
Subject: test message
This is the message.
`, mailfrom, rcptto)
msg = strings.ReplaceAll(msg, "\n", "\r\n")
auth := []sasl.Client{sasl.NewClientPlain(mailfrom, password)}
c, err := smtpclient.New(mox.Context, log, conn, smtpclient.TLSOpportunistic, mox.Conf.Static.HostnameDomain, dns.Domain{ASCII: smtphost}, auth)
tcheck(t, err, "smtp hello")
err = c.Deliver(mox.Context, mailfrom, rcptto, int64(len(msg)), strings.NewReader(msg), false, false)
tcheck(t, err, "deliver with smtp")
err = c.Close()
tcheck(t, err, "close smtpclient")
}
testDeliver(true, "moxmail1.mox1.example:143", "moxtest1@mox1.example", "pass1234", func() {
submit("moxmail1.mox1.example", "587", "moxtest1@mox1.example", "pass1234", "root@postfix.example")
})
testDeliver(true, "moxmail1.mox1.example:143", "moxtest3@mox3.example", "pass1234", func() {
submit("moxmail2.mox2.example", "587", "moxtest2@mox2.example", "pass1234", "moxtest3@mox3.example")
})
testDeliver(false, "localserve.mox1.example:1143", "mox@localhost", "moxmoxmox", func() {
submit("localserve.mox1.example", "1587", "mox@localhost", "moxmoxmox", "any@any.example")
})
testDeliver(false, "localserve.mox1.example:1143", "mox@localhost", "moxmoxmox", func() {
cmd := exec.Command("go", "run", ".", "sendmail", "mox@localhost")
const msg = `Subject: test
a message.
`
cmd.Stdin = strings.NewReader(msg)
var out strings.Builder
cmd.Stdout = &out
err := cmd.Run()
log.Print("sendmail", mlog.Field("output", out.String()))
tcheck(t, err, "sendmail")
})
}