git » chasquid » commit 92a88bd

test: Add a new local end-to-end test

author Alberto Bertogli
2016-07-22 00:52:27 UTC
committer Alberto Bertogli
2016-09-12 03:06:56 UTC
parent 92d16a0ca90ee57e0297cf6fb37fdbc16199e922

test: Add a new local end-to-end test

This patch introduces a new directory, test/, which contains a simple local
end-to-end test which runs a chasquid binary and uses msmtp to send an email,
which is delivered locally.

As it's the first one, it adds a bunch of common infrastructure to simplify
writing these kinds of tests.

More end-to-end tests will follow, and it's expected that the common
infrastructure will also change significantly to accomodate their needs.

test/t-simple_local/config/chasquid.conf +5 -0
test/t-simple_local/config/domains/testserver/users +2 -0
test/t-simple_local/content +4 -0
test/t-simple_local/hosts +1 -0
test/t-simple_local/msmtprc +21 -0
test/t-simple_local/run.sh +29 -0
test/util/generate_cert.go +159 -0
test/util/lib.sh +74 -0
test/util/mail_diff +34 -0
test/util/test-mda +14 -0

diff --git a/test/t-simple_local/config/chasquid.conf b/test/t-simple_local/config/chasquid.conf
new file mode 100644
index 0000000..1302399
--- /dev/null
+++ b/test/t-simple_local/config/chasquid.conf
@@ -0,0 +1,5 @@
+address: ":1025"
+monitoring_address: ":1099"
+
+mail_delivery_agent_bin: "test-mda"
+mail_delivery_agent_args: "%user%@%domain%"
diff --git a/test/t-simple_local/config/domains/testserver/users b/test/t-simple_local/config/domains/testserver/users
new file mode 100644
index 0000000..bd031fe
--- /dev/null
+++ b/test/t-simple_local/config/domains/testserver/users
@@ -0,0 +1,2 @@
+#chasquid-userdb-v1
+user SCRYPT@n:14,r:8,p:1,l:32,r00XqNmRkV505R2X6KT8+Q== aAiBBIVNNzmDXwxLLdJezFuxGtc2/wcHsy3FiOMAH4c=
diff --git a/test/t-simple_local/content b/test/t-simple_local/content
new file mode 100644
index 0000000..76a8b16
--- /dev/null
+++ b/test/t-simple_local/content
@@ -0,0 +1,4 @@
+Subject: Prueba desde el test
+
+Crece desde el test el futuro
+Crece desde el test
diff --git a/test/t-simple_local/hosts b/test/t-simple_local/hosts
new file mode 100644
index 0000000..2b9b623
--- /dev/null
+++ b/test/t-simple_local/hosts
@@ -0,0 +1 @@
+testserver localhost
diff --git a/test/t-simple_local/msmtprc b/test/t-simple_local/msmtprc
new file mode 100644
index 0000000..00190f0
--- /dev/null
+++ b/test/t-simple_local/msmtprc
@@ -0,0 +1,21 @@
+account default
+
+host testserver
+port 1025
+
+tls on
+tls_trust_file config/domains/testserver/cert.pem
+
+from user@testserver
+
+auth on
+user user@testserver
+password secretpassword
+
+account baduser : default
+user unknownuser@testserver
+password secretpassword
+
+account badpasswd : default
+user user@testserver
+password badsecretpassword
diff --git a/test/t-simple_local/run.sh b/test/t-simple_local/run.sh
new file mode 100755
index 0000000..eb8f501
--- /dev/null
+++ b/test/t-simple_local/run.sh
@@ -0,0 +1,29 @@
+#!/bin/bash
+
+set -e
+. $(dirname ${0})/../util/lib.sh
+
+init
+
+generate_certs_for testserver
+
+chasquid -v=2 --log_dir=.logs --config_dir=config &
+wait_until_ready 1025
+
+run_msmtp someone@testserver < content
+
+wait_for_file .mail/someone@testserver
+
+mail_diff content .mail/someone@testserver
+
+if run_msmtp -a baduser someone@testserver < content 2> /dev/null; then
+	echo "ERROR: successfully sent an email with a bad password"
+	exit 1
+fi
+
+if run_msmtp -a badpasswd someone@testserver < content 2> /dev/null; then
+	echo "ERROR: successfully sent an email with a bad password"
+	exit 1
+fi
+
+success
diff --git a/test/util/generate_cert.go b/test/util/generate_cert.go
new file mode 100644
index 0000000..e488792
--- /dev/null
+++ b/test/util/generate_cert.go
@@ -0,0 +1,159 @@
+// Copyright 2009 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build ignore
+
+// Generate a self-signed X.509 certificate for a TLS server. Outputs to
+// 'cert.pem' and 'key.pem' and will overwrite existing files.
+
+package main
+
+import (
+	"crypto/ecdsa"
+	"crypto/elliptic"
+	"crypto/rand"
+	"crypto/rsa"
+	"crypto/x509"
+	"crypto/x509/pkix"
+	"encoding/pem"
+	"flag"
+	"fmt"
+	"log"
+	"math/big"
+	"net"
+	"os"
+	"strings"
+	"time"
+)
+
+var (
+	host       = flag.String("host", "", "Comma-separated hostnames and IPs to generate a certificate for")
+	validFrom  = flag.String("start-date", "", "Creation date formatted as Jan 1 15:04:05 2011")
+	validFor   = flag.Duration("duration", 365*24*time.Hour, "Duration that certificate is valid for")
+	isCA       = flag.Bool("ca", false, "whether this cert should be its own Certificate Authority")
+	rsaBits    = flag.Int("rsa-bits", 2048, "Size of RSA key to generate. Ignored if --ecdsa-curve is set")
+	ecdsaCurve = flag.String("ecdsa-curve", "", "ECDSA curve to use to generate a key. Valid values are P224, P256, P384, P521")
+)
+
+func publicKey(priv interface{}) interface{} {
+	switch k := priv.(type) {
+	case *rsa.PrivateKey:
+		return &k.PublicKey
+	case *ecdsa.PrivateKey:
+		return &k.PublicKey
+	default:
+		return nil
+	}
+}
+
+func pemBlockForKey(priv interface{}) *pem.Block {
+	switch k := priv.(type) {
+	case *rsa.PrivateKey:
+		return &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(k)}
+	case *ecdsa.PrivateKey:
+		b, err := x509.MarshalECPrivateKey(k)
+		if err != nil {
+			fmt.Fprintf(os.Stderr, "Unable to marshal ECDSA private key: %v", err)
+			os.Exit(2)
+		}
+		return &pem.Block{Type: "EC PRIVATE KEY", Bytes: b}
+	default:
+		return nil
+	}
+}
+
+func main() {
+	flag.Parse()
+
+	if len(*host) == 0 {
+		log.Fatalf("Missing required --host parameter")
+	}
+
+	var priv interface{}
+	var err error
+	switch *ecdsaCurve {
+	case "":
+		priv, err = rsa.GenerateKey(rand.Reader, *rsaBits)
+	case "P224":
+		priv, err = ecdsa.GenerateKey(elliptic.P224(), rand.Reader)
+	case "P256":
+		priv, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+	case "P384":
+		priv, err = ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
+	case "P521":
+		priv, err = ecdsa.GenerateKey(elliptic.P521(), rand.Reader)
+	default:
+		fmt.Fprintf(os.Stderr, "Unrecognized elliptic curve: %q", *ecdsaCurve)
+		os.Exit(1)
+	}
+	if err != nil {
+		log.Fatalf("failed to generate private key: %s", err)
+	}
+
+	var notBefore time.Time
+	if len(*validFrom) == 0 {
+		notBefore = time.Now()
+	} else {
+		notBefore, err = time.Parse("Jan 2 15:04:05 2006", *validFrom)
+		if err != nil {
+			fmt.Fprintf(os.Stderr, "Failed to parse creation date: %s\n", err)
+			os.Exit(1)
+		}
+	}
+
+	notAfter := notBefore.Add(*validFor)
+
+	serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
+	serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
+	if err != nil {
+		log.Fatalf("failed to generate serial number: %s", err)
+	}
+
+	template := x509.Certificate{
+		SerialNumber: serialNumber,
+		Subject: pkix.Name{
+			Organization: []string{"Acme Co"},
+		},
+		NotBefore: notBefore,
+		NotAfter:  notAfter,
+
+		KeyUsage:              x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
+		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
+		BasicConstraintsValid: true,
+	}
+
+	hosts := strings.Split(*host, ",")
+	for _, h := range hosts {
+		if ip := net.ParseIP(h); ip != nil {
+			template.IPAddresses = append(template.IPAddresses, ip)
+		} else {
+			template.DNSNames = append(template.DNSNames, h)
+		}
+	}
+
+	if *isCA {
+		template.IsCA = true
+		template.KeyUsage |= x509.KeyUsageCertSign
+	}
+
+	derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, publicKey(priv), priv)
+	if err != nil {
+		log.Fatalf("Failed to create certificate: %s", err)
+	}
+
+	certOut, err := os.Create("cert.pem")
+	if err != nil {
+		log.Fatalf("failed to open cert.pem for writing: %s", err)
+	}
+	pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
+	certOut.Close()
+
+	keyOut, err := os.OpenFile("key.pem", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
+	if err != nil {
+		log.Fatalf("failed to open key.pem for writing:", err)
+		return
+	}
+	pem.Encode(keyOut, pemBlockForKey(priv))
+	keyOut.Close()
+}
diff --git a/test/util/lib.sh b/test/util/lib.sh
new file mode 100644
index 0000000..07c4913
--- /dev/null
+++ b/test/util/lib.sh
@@ -0,0 +1,74 @@
+# Library to write the shell scripts in the tests.
+
+function init() {
+	if [ "$V" == "1" ]; then
+		set -v
+	fi
+
+	export TBASE="$(realpath `dirname ${0}`)"
+	cd ${TBASE}
+
+	export UTILDIR="$(realpath ${TBASE}/../util/)"
+
+	# Remove the directory where test-mda will deliver mail, so previous
+	# runs don't interfere with this one.
+	rm -rf .mail
+
+	# Set traps to kill our subprocesses when we exit (for any reason).
+	# https://stackoverflow.com/questions/360201/
+	trap "exit" INT TERM
+	trap "kill 0" EXIT
+}
+
+function generate_cert() {
+	go run ${UTILDIR}/generate_cert.go "$@"
+}
+
+function chasquid() {
+	# HOSTALIASES: so we "fake" hostnames.
+	# PATH: so chasquid can call test-mda without path issues.
+	HOSTALIASES=${TBASE}/hosts \
+	PATH=${UTILDIR}:${PATH} \
+		go run ${TBASE}/../../chasquid.go "$@"
+}
+
+function run_msmtp() {
+	# msmtp will check that the rc file is only user readable.
+	chmod 600 msmtprc
+
+	HOSTALIASES=${TBASE}/hosts \
+		msmtp -C msmtprc "$@"
+}
+
+function mail_diff() {
+	${UTILDIR}/mail_diff "$@"
+}
+
+function success() {
+	echo "SUCCESS"
+}
+
+# Wait until there's something listening on the given port.
+function wait_until_ready() {
+	PORT=$1
+
+	while ! nc -z localhost $PORT; do
+		sleep 0.1
+	done
+}
+
+# Wait for the given file to exist.
+function wait_for_file() {
+	while ! [ -e ${1} ]; do
+		sleep 0.1
+	done
+}
+
+# Generate certs for the given domain.
+function generate_certs_for() {
+	mkdir -p config/domains/${1}
+	(
+		cd config/domains/${1}
+		generate_cert -ca -duration=1h -host=${1}
+	)
+}
diff --git a/test/util/mail_diff b/test/util/mail_diff
new file mode 100755
index 0000000..5e9667e
--- /dev/null
+++ b/test/util/mail_diff
@@ -0,0 +1,34 @@
+#!/usr/bin/env python
+
+import difflib
+import email.parser
+import mailbox
+import sys
+
+f1, f2 = sys.argv[1:3]
+
+expected = email.parser.Parser().parse(open(f1))
+
+mbox = mailbox.mbox(f2, create=False)
+msg = mbox[0]
+
+diff = False
+
+for h, val in expected.items():
+	if h not in msg:
+		print("Header missing: %r" % h)
+		diff = True
+		continue
+	if msg[h] != val:
+		print("Header %r differs: %r != %r" % (h, val, msg[h]))
+		diff = True
+
+if expected.get_payload() != msg.get_payload():
+	diff = True
+	exp = expected.get_payload().splitlines()
+	got = msg.get_payload().splitlines()
+	print("Payload differs:")
+	for l in difflib.ndiff(exp, got):
+		print(l)
+
+sys.exit(0 if not diff else 1)
diff --git a/test/util/test-mda b/test/util/test-mda
new file mode 100755
index 0000000..617e393
--- /dev/null
+++ b/test/util/test-mda
@@ -0,0 +1,14 @@
+#!/bin/bash
+
+set -e
+
+mkdir -p .mail
+
+# TODO: use flock to lock the file, to prevent atomic writes.
+echo "From ${1}" >> .mail/.tmp-${1}
+cat >> .mail/.tmp-${1}
+X=$?
+if [ -e .mail/.tmp-${1} ]; then
+	mv .mail/.tmp-${1} .mail/${1}
+fi
+exit $X