Python automx2, its database and its automx_rows tool go; the container keeps its name, network and port, gains an http health.
261 lines
8.1 KiB
Go
261 lines
8.1 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// The environment mailu gives automx on novox when the fixtures were captured (files/mailu.env): the mail domain,
|
|
// the route name for both servers, the ports.
|
|
var capturedEnvironment = map[string]string{
|
|
"AMX_MAIL_DOMAINS": "novox.be",
|
|
"AMX_IMAP_ADDRESS": "mail.novox.be",
|
|
"AMX_IMAP_PORT": "143",
|
|
"AMX_SMTP_ADDRESS": "mail.novox.be",
|
|
"AMX_SMTP_PORT": "587",
|
|
"AUTOMX_PORT": "4243",
|
|
}
|
|
|
|
func getenvOf(m map[string]string) func(string) string {
|
|
return func(k string) string { return m[k] }
|
|
}
|
|
|
|
func testServer(t *testing.T) *httptest.Server {
|
|
t.Helper()
|
|
cfg, err := configFrom(getenvOf(capturedEnvironment))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
s := httptest.NewServer(newHandler(cfg))
|
|
t.Cleanup(s.Close)
|
|
return s
|
|
}
|
|
|
|
const captured = "testdata/captured"
|
|
|
|
// A captured request: its first line is the method and the URL the client used, then the headers sent, then after
|
|
// a blank line the body sent (CAPTURED, how the files were written).
|
|
type request struct {
|
|
method, url string
|
|
header [][2]string
|
|
body string
|
|
}
|
|
|
|
func readRequest(t *testing.T, stem string) request {
|
|
t.Helper()
|
|
raw, err := os.ReadFile(stem + ".request")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
head, body, hasBody := strings.Cut(string(raw), "\n\n")
|
|
if !hasBody {
|
|
head = strings.TrimSuffix(head, "\n")
|
|
}
|
|
lines := strings.Split(head, "\n")
|
|
method, target, ok := strings.Cut(lines[0], " ")
|
|
if !ok {
|
|
t.Fatalf("%s: first line %q is not METHOD URL", stem, lines[0])
|
|
}
|
|
r := request{method: method, url: target, body: body}
|
|
for _, l := range lines[1:] {
|
|
k, v, ok := strings.Cut(l, ": ")
|
|
if !ok {
|
|
t.Fatalf("%s: header line %q", stem, l)
|
|
}
|
|
r.header = append(r.header, [2]string{k, v})
|
|
}
|
|
return r
|
|
}
|
|
|
|
func readFile(t *testing.T, name string) string {
|
|
t.Helper()
|
|
b, err := os.ReadFile(name)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return string(b)
|
|
}
|
|
|
|
// A fixture asks for its body to differ only where DIFFERENCES says so: a stem with a .corrected.body is answered
|
|
// with that, and its Content-Length is the corrected body's.
|
|
func expectedBody(t *testing.T, stem string) (body string, corrected bool) {
|
|
if _, err := os.Stat(stem + ".corrected.body"); err == nil {
|
|
return readFile(t, stem+".corrected.body"), true
|
|
}
|
|
return readFile(t, stem+".body"), false
|
|
}
|
|
|
|
func TestEveryCapturedRequestIsAnsweredAsAutomxAnswered(t *testing.T) {
|
|
stems, _ := filepath.Glob(filepath.Join(captured, "*.request"))
|
|
if len(stems) < 17 {
|
|
t.Fatalf("%d captured requests found, want the 17 of issue 1630", len(stems))
|
|
}
|
|
srv := testServer(t)
|
|
client := &http.Client{CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }}
|
|
for _, f := range stems {
|
|
stem := strings.TrimSuffix(f, ".request")
|
|
t.Run(filepath.Base(stem), func(t *testing.T) {
|
|
rq := readRequest(t, stem)
|
|
u, err := url.Parse(rq.url)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// The mesh's route reaches the program with the client's name and scheme; the program is told them in
|
|
// the forwarding headers (the Location of fixture 14 holds them).
|
|
req, err := http.NewRequest(rq.method, srv.URL+u.RequestURI(), strings.NewReader(rq.body))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, h := range rq.header {
|
|
req.Header.Set(h[0], h[1])
|
|
}
|
|
req.Host = u.Host
|
|
req.Header.Set("X-Forwarded-Proto", u.Scheme)
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer resp.Body.Close()
|
|
got, _ := io.ReadAll(resp.Body)
|
|
|
|
if want := strings.TrimRight(readFile(t, stem+".status"), "\n"); resp.Status != want {
|
|
t.Errorf("status %q, captured %q", resp.Status, want)
|
|
}
|
|
wantBody, corrected := expectedBody(t, stem)
|
|
if !bytes.Equal(got, []byte(wantBody)) {
|
|
t.Errorf("body\n%q\nwant\n%q", got, wantBody)
|
|
}
|
|
want := map[string]string{}
|
|
for _, l := range strings.Split(strings.TrimRight(readFile(t, stem+".headers"), "\n"), "\n") {
|
|
if k, v, ok := strings.Cut(l, ": "); ok {
|
|
want[k] = v
|
|
}
|
|
}
|
|
if corrected {
|
|
want["Content-Length"] = strconv.Itoa(len(wantBody))
|
|
}
|
|
// Server names the Python program that answered (DIFFERENCES); Date changes on every request.
|
|
delete(want, "Server")
|
|
var extra []string
|
|
for k := range resp.Header {
|
|
if k == "Date" || k == "Server" {
|
|
continue
|
|
}
|
|
if _, ok := want[k]; !ok {
|
|
extra = append(extra, k)
|
|
}
|
|
}
|
|
sort.Strings(extra)
|
|
if len(extra) > 0 {
|
|
t.Errorf("headers the capture does not have: %v", extra)
|
|
}
|
|
for k, v := range want {
|
|
if g := resp.Header.Get(k); g != v {
|
|
t.Errorf("header %s: %q, captured %q", k, g, v)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// DIFFERENCES: the one change clause 9 of ADR 0480 allows to an answer is the IMAP server's name, written from
|
|
// AMX_IMAP_ADDRESS where automx wrote an empty one.
|
|
func TestTheOnlyCorrectedAnswerIsTheImapServersName(t *testing.T) {
|
|
corrected, _ := filepath.Glob(filepath.Join(captured, "*.corrected.body"))
|
|
if len(corrected) != 3 {
|
|
t.Fatalf("%d corrected answers, want the three Thunderbird answers (01, 02, 03): %v", len(corrected), corrected)
|
|
}
|
|
for _, c := range corrected {
|
|
orig := readFile(t, strings.TrimSuffix(c, ".corrected.body")+".body")
|
|
want := strings.Replace(orig, "<hostname />", "<hostname>mail.novox.be</hostname>", 1)
|
|
if strings.Contains(want, "<hostname />") || want == orig {
|
|
t.Fatalf("%s: the capture has no single empty hostname to correct", c)
|
|
}
|
|
if got := readFile(t, c); got != want {
|
|
t.Errorf("%s differs from the capture by more than the IMAP server's name", c)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTheImapServerIsNamedFromTheImapAddress(t *testing.T) {
|
|
env := map[string]string{}
|
|
for k, v := range capturedEnvironment {
|
|
env[k] = v
|
|
}
|
|
env["AMX_IMAP_ADDRESS"] = "imap.example.test"
|
|
env["AMX_IMAP_PORT"] = "993"
|
|
cfg, err := configFrom(getenvOf(env))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
rec := httptest.NewRecorder()
|
|
newHandler(cfg).ServeHTTP(rec, httptest.NewRequest("GET", "/mail/config-v1.1.xml?emailaddress=a@novox.be", nil))
|
|
body := rec.Body.String()
|
|
if !strings.Contains(body, `<incomingServer type="imap"><hostname>imap.example.test</hostname><port>993</port>`) {
|
|
t.Errorf("incoming server not named from AMX_IMAP_ADDRESS and AMX_IMAP_PORT: %s", body)
|
|
}
|
|
if !strings.Contains(body, `<outgoingServer type="smtp"><hostname>mail.novox.be</hostname><port>587</port>`) {
|
|
t.Errorf("outgoing server changed: %s", body)
|
|
}
|
|
}
|
|
|
|
func TestEveryDomainOfTheEnvironmentIsServed(t *testing.T) {
|
|
env := map[string]string{}
|
|
for k, v := range capturedEnvironment {
|
|
env[k] = v
|
|
}
|
|
env["AMX_MAIL_DOMAINS"] = "novox.be, example.test"
|
|
cfg, err := configFrom(getenvOf(env))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
h := newHandler(cfg)
|
|
for _, d := range []string{"novox.be", "example.test"} {
|
|
rec := httptest.NewRecorder()
|
|
h.ServeHTTP(rec, httptest.NewRequest("GET", "/mail/config-v1.1.xml?emailaddress=a@"+d, nil))
|
|
if rec.Code != 200 || !strings.Contains(rec.Body.String(), "<domain>"+d+"</domain>") {
|
|
t.Errorf("%s: %d %s", d, rec.Code, rec.Body.String())
|
|
}
|
|
}
|
|
}
|
|
|
|
// Failure is loud (issue 1631 AC2): a missing variable stops the program and its name is in the message.
|
|
func TestAMissingVariableStopsTheProgramNamingIt(t *testing.T) {
|
|
for name := range capturedEnvironment {
|
|
t.Run(name, func(t *testing.T) {
|
|
env := map[string]string{}
|
|
for k, v := range capturedEnvironment {
|
|
env[k] = v
|
|
}
|
|
delete(env, name)
|
|
if _, err := configFrom(getenvOf(env)); err == nil || !strings.Contains(err.Error(), name) {
|
|
t.Fatalf("configFrom without %s: %v", name, err)
|
|
}
|
|
var out bytes.Buffer
|
|
if code := run(getenvOf(env), &out, func(*http.Server) error { t.Fatal("served without its configuration"); return nil }); code == 0 || !strings.Contains(out.String(), name) {
|
|
t.Errorf("run without %s: exit %d, said %q", name, code, out.String())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAPortThatIsNotANumberIsRefusedNamingIt(t *testing.T) {
|
|
env := map[string]string{}
|
|
for k, v := range capturedEnvironment {
|
|
env[k] = v
|
|
}
|
|
env["AMX_SMTP_PORT"] = "submission"
|
|
if _, err := configFrom(getenvOf(env)); err == nil || !strings.Contains(err.Error(), "AMX_SMTP_PORT") {
|
|
t.Fatalf("got %v", err)
|
|
}
|
|
}
|