Skip to content

Commit

Permalink
add automated test for quickstart
Browse files Browse the repository at this point in the history
with tls with acme (with pebble, a small acme server for testing), and with
pregenerated keys/certs.

the two mox instances are configured on their own domain. we launch a separate
test container that connects to the first, submits a message for delivery to
the second. we check if the message is delivered with an imap connection and
the idle command.
  • Loading branch information
mjl- committed Jun 4, 2023
1 parent e53b773 commit 05fd5c6
Show file tree
Hide file tree
Showing 34 changed files with 595 additions and 6 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
/testdata/smtpserverfuzz/data/
/testdata/store/data/
/testdata/train/
/testdata/quickstart/example-quickstart.zone
/testdata/quickstart/tmp-pebble-ca.pem
/cover.out
/cover.html
/.go/
Expand Down
28 changes: 25 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ default: build
build:
# build early to catch syntax errors
CGO_ENABLED=0 go build
CGO_ENABLED=0 go vet -tags integration ./...
CGO_ENABLED=0 go vet -tags quickstart ./...
CGO_ENABLED=0 go vet -tags quickstart quickstart_test.go
./gendoc.sh
(cd http && CGO_ENABLED=0 go run ../vendor/github.com/mjl-/sherpadoc/cmd/sherpadoc/*.go -adjust-function-names none Admin) >http/adminapi.json
(cd http && CGO_ENABLED=0 go run ../vendor/github.com/mjl-/sherpadoc/cmd/sherpadoc/*.go -adjust-function-names none Account) >http/accountapi.json
Expand All @@ -24,6 +25,8 @@ test-upgrade:
check:
staticcheck ./...
staticcheck -tags integration
staticcheck -tags quickstart
GOARCH=386 CGO_ENABLED=0 go vet -tags integration ./...

# having "err" shadowed is common, best to not have others
check-shadow:
Expand All @@ -43,8 +46,15 @@ fuzz:
go test -fuzz FuzzParseRecord -fuzztime 5m ./tlsrpt
go test -fuzz FuzzParseMessage -fuzztime 5m ./tlsrpt

test-integration:
docker-compose -f docker-compose-integration.yml build --no-cache --pull moxmail
-rm -r testdata/integration/data
docker-compose -f docker-compose-integration.yml run moxmail sh -c 'CGO_ENABLED=0 go test -tags integration'
docker-compose -f docker-compose-integration.yml down

# like test-integration, but in separate steps
integration-build:
docker-compose -f docker-compose-integration.yml build --no-cache moxmail
docker-compose -f docker-compose-integration.yml build --no-cache --pull moxmail

integration-start:
-rm -r testdata/integration/data
Expand All @@ -55,15 +65,27 @@ integration-start:
integration-test:
CGO_ENABLED=0 go test -tags integration


test-quickstart:
docker image build --pull -f Dockerfile -t mox_quickstart_moxmail .
docker image build --pull -f testdata/quickstart/Dockerfile.test -t mox_quickstart_test testdata/quickstart
-rm -rf testdata/quickstart/moxacmepebble/data
-rm -rf testdata/quickstart/moxmail2/data
-rm -f testdata/quickstart/tmp-pebble-ca.pem
MOX_UID=$$(id -u) docker-compose -f docker-compose-quickstart.yml run test
docker-compose -f docker-compose-quickstart.yml down --timeout 1


imaptest-build:
-docker-compose -f docker-compose-imaptest.yml build --no-cache mox
-docker-compose -f docker-compose-imaptest.yml build --no-cache --pull mox

imaptest-run:
-rm -r testdata/imaptest/data
mkdir testdata/imaptest/data
docker-compose -f docker-compose-imaptest.yml run --entrypoint /usr/local/bin/imaptest imaptest host=mox port=1143 [email protected] pass=testtest mbox=imaptest.mbox
docker-compose -f docker-compose-imaptest.yml down


fmt:
go fmt ./...
gofmt -w -s *.go */*.go
Expand Down
4 changes: 2 additions & 2 deletions develop.txt
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ Listeners:
https://github.com/letsencrypt/pebble is useful for testing with ACME. Start a
pebble instance that uses the localhost TLS cert/key created by cfssl for its
TLS serving. Pebble generates a new CA certificate for its own use each time it
is started. Fetch it from https://localhost:14000/root, write it to a file, and
is started. Fetch it from https://localhost:15000/roots/0, write it to a file, and
add it to mox.conf TLS.CA.CertFiles. See below.

Setup pebble, run once:
Expand Down Expand Up @@ -122,7 +122,7 @@ Write new CA bundle that includes pebble's temporary CA cert:
export CURL_CA_BUNDLE=local/ca-bundle.pem # for curl
export SSL_CERT_FILE=local/ca-bundle.pem # for go apps
cat /etc/ssl/certs/ca-certificates.crt local/cfssl/ca.pem >local/ca-bundle.pem
curl https://localhost:14000/root >local/pebble/ca.pem # fetch temp pebble ca, DO THIS EVERY TIME PEBBLE IS RESTARTED!
curl https://localhost:15000/roots/0 >local/pebble/ca.pem # fetch temp pebble ca, DO THIS EVERY TIME PEBBLE IS RESTARTED!
cat /etc/ssl/certs/ca-certificates.crt local/cfssl/ca.pem local/pebble/ca.pem >local/ca-bundle.pem # create new list that includes cfssl ca and temp pebble ca.
rm -r local/*/data/acme/keycerts/pebble # remove existing pebble-signed certs in acme cert/key cache, they are invalid due to newly generated temp pebble ca.
```
Expand Down
133 changes: 133 additions & 0 deletions docker-compose-quickstart.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
version: '3.7'
services:
# We run quickstart_test.go from this container, it connects to both mox instances.
test:
hostname: test.mox1.example
image: mox_quickstart_test
# We add our cfssl-generated CA (which is in the repo) and acme pebble CA
# (generated each time pebble starts) to the list of trusted CA's, so the TLS
# dials in quickstart_test.go succeed.
command: ["sh", "-c", "set -ex; cat /quickstart/tmp-pebble-ca.pem /quickstart/tls/ca.pem >>/etc/ssl/certs/ca-certificates.crt; go test -tags quickstart"]
volumes:
- ./.go:/.go
- ./testdata/quickstart/resolv.conf:/etc/resolv.conf
- ./testdata/quickstart:/quickstart
- .:/mox
environment:
GOCACHE: /.go/.cache/go-build
depends_on:
dns:
condition: service_healthy
# moxmail2 depends on moxacmepebble, we connect to both.
moxmail2:
condition: service_healthy
networks:
mailnet1:
ipv4_address: 172.28.1.50

# First mox instance that uses ACME with pebble.
moxacmepebble:
hostname: moxacmepebble.mox1.example
domainname: mox1.example
image: mox_quickstart_moxmail
environment:
MOX_UID: "${MOX_UID}"
command: ["sh", "-c", "/quickstart/moxacmepebble.sh"]
volumes:
- ./testdata/quickstart/resolv.conf:/etc/resolv.conf
- ./testdata/quickstart:/quickstart
healthcheck:
test: netstat -nlt | grep ':25 '
interval: 1s
timeout: 1s
retries: 10
depends_on:
dns:
condition: service_healthy
acmepebble:
condition: service_healthy
networks:
mailnet1:
ipv4_address: 172.28.1.10

# Second mox instance, with TLS cert/keys from files.
moxmail2:
hostname: moxmail2.mox2.example
domainname: mox2.example
image: mox_quickstart_moxmail
environment:
MOX_UID: "${MOX_UID}"
command: ["sh", "-c", "/quickstart/moxmail2.sh"]
volumes:
- ./testdata/quickstart/resolv.conf:/etc/resolv.conf
- ./testdata/quickstart:/quickstart
healthcheck:
test: netstat -nlt | grep ':25 '
interval: 1s
timeout: 1s
retries: 10
depends_on:
dns:
condition: service_healthy
acmepebble:
condition: service_healthy
# moxacmepebble creates tmp-pebble-ca.pem, needed by moxmail2 to trust the certificates offered by moxacmepebble.
moxacmepebble:
condition: service_healthy
networks:
mailnet1:
ipv4_address: 172.28.1.20

dns:
hostname: dns.example
build:
dockerfile: Dockerfile.dns
# todo: figure out how to build from dockerfile with empty context without creating empty dirs in file system.
context: testdata/quickstart
volumes:
- ./testdata/quickstart/resolv.conf:/etc/resolv.conf
- ./testdata/quickstart:/quickstart
# We start with a base example.zone, but moxacmepebble appends its records,
# followed by moxmail2. They restart unbound after appending records.
command: ["sh", "-c", "set -ex; ls -l /etc/resolv.conf; chmod o+r /etc/resolv.conf; install -m 640 -o unbound /quickstart/unbound.conf /etc/unbound/; chmod 755 /quickstart; chmod 644 /quickstart/*.zone; cp /quickstart/example.zone /quickstart/example-quickstart.zone; ls -ld /quickstart /quickstart/reverse.zone; unbound -d -p -v"]
healthcheck:
test: netstat -nlu | grep '172.28.1.30:53 '
interval: 1s
timeout: 1s
retries: 10
networks:
mailnet1:
ipv4_address: 172.28.1.30

# pebble is a small acme server useful for testing. It creates a new CA
# certificate each time it starts, so we go through some trouble to configure the
# certificate in moxacmepebble and moxmail2.
acmepebble:
hostname: acmepebble.example
image: docker.io/letsencrypt/pebble:v2.3.1@sha256:fc5a537bf8fbc7cc63aa24ec3142283aa9b6ba54529f86eb8ff31fbde7c5b258
volumes:
- ./testdata/quickstart/resolv.conf:/etc/resolv.conf
- ./testdata/quickstart:/quickstart
command: ["sh", "-c", "set -ex; mount; ls -l /etc/resolv.conf; chmod o+r /etc/resolv.conf; pebble -config /quickstart/pebble-config.json"]
ports:
- 14000:14000 # ACME port
- 15000:15000 # Management port
healthcheck:
test: netstat -nlt | grep ':14000 '
interval: 1s
timeout: 1s
retries: 10
depends_on:
dns:
condition: service_healthy
networks:
mailnet1:
ipv4_address: 172.28.1.40

networks:
mailnet1:
driver: bridge
ipam:
driver: default
config:
- subnet: "172.28.1.0/24"
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
version: '3.7'
services:
mox:
# Replace "latest" with the version you want to run, see https://r.xmox.nl/repo/mox/.
# Replace "latest" with the version you want to run, see https://r.xmox.nl/r/mox/.
# Include the @sha256:... digest to ensure you get the listed image.
image: r.xmox.nl/mox:latest
environment:
Expand Down
145 changes: 145 additions & 0 deletions quickstart_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
//go:build quickstart

// Run this using docker-compose.yml, see Makefile.

package main

import (
"bytes"
"crypto/tls"
"encoding/base64"
"fmt"
"os"
"strings"
"testing"
"time"

"github.com/mjl-/mox/imapclient"
"github.com/mjl-/mox/mlog"
"github.com/mjl-/mox/mox-"
"github.com/mjl-/mox/smtpclient"
)

var xlog = mlog.New("quickstart")

func tcheck(t *testing.T, err error, msg string) {
t.Helper()
if err != nil {
t.Fatalf("%s: %s", msg, err)
}
}

func TestDeliver(t *testing.T) {
mlog.Logfmt = true

// smtpclient uses the hostname for outgoing connections.
var err error
mox.Conf.Static.HostnameDomain.ASCII, err = os.Hostname()
tcheck(t, err, "hostname")

// Deliver submits a message over submissions, and checks with imap idle if the
// message is received by the destination mail server.
deliver := func(desthost, mailfrom, password, rcptto, imaphost, imapuser, imappassword string) {
t.Helper()

// Connect to IMAP, execute IDLE command, which will return on deliver message.
// TLS certificates work because the container has the CA certificates configured.
imapconn, err := tls.Dial("tcp", imaphost+":993", nil)
tcheck(t, err, "dial imap")
defer imapconn.Close()

imaperr := make(chan error, 1)
go func() {
go func() {
x := recover()
if x == nil {
return
}
imaperr <- x.(error)
}()
xcheck := func(err error, format string) {
if err != nil {
panic(fmt.Errorf("%s: %w", format, err))
}
}

imapc, err := imapclient.New(imapconn, false)
xcheck(err, "new imapclient")

_, _, err = imapc.Login(imapuser, imappassword)
xcheck(err, "imap login")

_, _, err = imapc.Select("Inbox")
xcheck(err, "imap select inbox")

err = imapc.Commandf("", "idle")
xcheck(err, "write imap idle command")
_, _, _, err = imapc.ReadContinuation()
xcheck(err, "read imap continuation")

done := make(chan error)
go func() {
defer func() {
x := recover()
if x != nil {
done <- fmt.Errorf("%v", x)
}
}()
untagged, err := imapc.ReadUntagged()
if err != nil {
done <- err
return
}
if _, ok := untagged.(imapclient.UntaggedExists); !ok {
done <- fmt.Errorf("expected imapclient.UntaggedExists, got %#v", untagged)
return
}
done <- nil
}()

period := 30 * time.Second
timer := time.NewTimer(period)
defer timer.Stop()
select {
case err = <-done:
case <-timer.C:
err = fmt.Errorf("nothing within %v", period)
}
xcheck(err, "waiting for imap untagged repsonse to idle")
imaperr <- nil
}()

conn, err := tls.Dial("tcp", desthost+":465", nil)
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 := bytes.Join([][]byte{nil, []byte(mailfrom), []byte(password)}, []byte{0})
authLine := fmt.Sprintf("AUTH PLAIN %s", base64.StdEncoding.EncodeToString(auth))
c, err := smtpclient.New(mox.Context, xlog, conn, smtpclient.TLSSkip, desthost, authLine)
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")

err = <-imaperr
tcheck(t, err, "imap idle")
}

xlog.Print("submitting email to moxacmepebble, waiting for imap notification at moxmail2, takes time because first-time sender")
t0 := time.Now()
deliver("moxacmepebble.mox1.example", "[email protected]", "accountpass1234", "[email protected]", "moxmail2.mox2.example", "[email protected]", "accountpass4321")
xlog.Print("success", mlog.Field("duration", time.Since(t0)))

xlog.Print("submitting email to moxmail2, waiting for imap notification at moxacmepebble, takes time because first-time sender")
t0 = time.Now()
deliver("moxmail2.mox2.example", "[email protected]", "accountpass4321", "[email protected]", "moxacmepebble.mox1.example", "[email protected]", "accountpass1234")
xlog.Print("success", mlog.Field("duration", time.Since(t0)))
}
2 changes: 2 additions & 0 deletions testdata/quickstart/Dockerfile.dns
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
FROM alpine:3.17
RUN apk add unbound bind-tools mailx
4 changes: 4 additions & 0 deletions testdata/quickstart/Dockerfile.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
FROM golang:1-alpine AS build
WORKDIR /mox
RUN apk add make bind-tools bash unbound curl
env GOPROXY=off
Loading

0 comments on commit 05fd5c6

Please sign in to comment.