mirror of
https://blitiri.com.ar/repos/chasquid
synced 2025-12-17 14:37:02 +00:00
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.
This commit is contained in:
5
test/t-simple_local/config/chasquid.conf
Normal file
5
test/t-simple_local/config/chasquid.conf
Normal file
@@ -0,0 +1,5 @@
|
||||
address: ":1025"
|
||||
monitoring_address: ":1099"
|
||||
|
||||
mail_delivery_agent_bin: "test-mda"
|
||||
mail_delivery_agent_args: "%user%@%domain%"
|
||||
2
test/t-simple_local/config/domains/testserver/users
Normal file
2
test/t-simple_local/config/domains/testserver/users
Normal file
@@ -0,0 +1,2 @@
|
||||
#chasquid-userdb-v1
|
||||
user SCRYPT@n:14,r:8,p:1,l:32,r00XqNmRkV505R2X6KT8+Q== aAiBBIVNNzmDXwxLLdJezFuxGtc2/wcHsy3FiOMAH4c=
|
||||
4
test/t-simple_local/content
Normal file
4
test/t-simple_local/content
Normal file
@@ -0,0 +1,4 @@
|
||||
Subject: Prueba desde el test
|
||||
|
||||
Crece desde el test el futuro
|
||||
Crece desde el test
|
||||
1
test/t-simple_local/hosts
Normal file
1
test/t-simple_local/hosts
Normal file
@@ -0,0 +1 @@
|
||||
testserver localhost
|
||||
21
test/t-simple_local/msmtprc
Normal file
21
test/t-simple_local/msmtprc
Normal file
@@ -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
|
||||
29
test/t-simple_local/run.sh
Executable file
29
test/t-simple_local/run.sh
Executable file
@@ -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
|
||||
159
test/util/generate_cert.go
Normal file
159
test/util/generate_cert.go
Normal file
@@ -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()
|
||||
}
|
||||
74
test/util/lib.sh
Normal file
74
test/util/lib.sh
Normal file
@@ -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}
|
||||
)
|
||||
}
|
||||
34
test/util/mail_diff
Executable file
34
test/util/mail_diff
Executable file
@@ -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)
|
||||
14
test/util/test-mda
Executable file
14
test/util/test-mda
Executable file
@@ -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
|
||||
Reference in New Issue
Block a user