The ihasmail default was a pin that went stale within days, and ihasmail keeps ten releases' images, so an old default would in time stop pulling. With no --ihasmail-image the tool now pulls :latest before asking, reads the version the image carries, confirms the dated tag is the same image, and writes that tag into compose.yaml (or the digest, if there is no such tag). Stalwart and Caddy stay pinned. A weekly end-to-end run against the newest release, three hours after ihasmail publishes, is what keeps it safe.
139 lines
5.5 KiB
Go
139 lines
5.5 KiB
Go
// SPDX-FileCopyrightText: 2026 Coffey Labs
|
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
package config
|
|
|
|
import (
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestDefaultsFollowTheDomain(t *testing.T) {
|
|
p, err := Options{Domain: "Example.COM.", Dir: "/srv/mail"}.Validate()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for got, want := range map[string]string{
|
|
p.Domain: "example.com",
|
|
p.MailHost: "mail.example.com",
|
|
p.WebmailHost: "webmail.example.com",
|
|
p.Email: "[email protected]",
|
|
p.Project: "ihasmail-example-com",
|
|
p.Dir: "/srv/mail",
|
|
} {
|
|
if got != want {
|
|
t.Errorf("got %q, want %q", got, want)
|
|
}
|
|
}
|
|
if p.CaddyIP.String() != "172.31.253.10" || p.IhasmailIP.String() != "172.31.253.11" || p.StalwartIP.String() != "172.31.253.12" {
|
|
t.Errorf("addresses %s %s %s", p.CaddyIP, p.IhasmailIP, p.StalwartIP)
|
|
}
|
|
want := []string{"mail.example.com", "autoconfig.example.com", "autodiscover.example.com", "mta-sts.example.com", "ua-auto-config.example.com"}
|
|
if !slices.Equal(p.StalwartNames(), want) {
|
|
t.Errorf("StalwartNames = %v", p.StalwartNames())
|
|
}
|
|
}
|
|
|
|
func TestLocalNeedsNoDomainAndPublishesNothing(t *testing.T) {
|
|
p, err := Options{Local: true}.Validate()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if p.Domain != "example.test" || len(p.PublishedPorts()) != 0 {
|
|
t.Errorf("domain %q, ports %v", p.Domain, p.PublishedPorts())
|
|
}
|
|
}
|
|
|
|
func TestRejects(t *testing.T) {
|
|
for name, tc := range map[string]struct {
|
|
o Options
|
|
want string
|
|
}{
|
|
"no domain": {Options{}, "--domain is required"},
|
|
"bad domain": {Options{Domain: "not a domain"}, `--domain "not a domain"`},
|
|
"mail host elsewhere": {Options{Domain: "example.com", MailHost: "mx.other.net"}, "one label under the domain"},
|
|
"mail host two deep": {Options{Domain: "example.com", MailHost: "a.b.example.com"}, "one label under the domain"},
|
|
"webmail is autoconfig": {Options{Domain: "example.com", WebmailHost: "autoconfig.example.com"}, "already one of Stalwart's names"},
|
|
"webmail is mail host": {Options{Domain: "example.com", WebmailHost: "mail.example.com"}, "already one of Stalwart's names"},
|
|
"user in another domain": {Options{Domain: "example.com", Users: []string{"[email protected]"}}, "not in example.com"},
|
|
"user is admin": {Options{Domain: "example.com", Users: []string{"admin"}}, "or is the administrator"},
|
|
"duplicate user": {Options{Domain: "example.com", Users: []string{"bob", "[email protected]"}}, "given twice"},
|
|
"bad user": {Options{Domain: "example.com", Users: []string{"bob smith"}}, "not a valid mailbox name"},
|
|
"bind not host:port": {Options{Domain: "example.com", WebmailBind: "8080"}, "--webmail-bind"},
|
|
"bind on a mail port": {Options{Domain: "example.com", StalwartBind: "127.0.0.1:443"}, "collides with port 443"},
|
|
"same binds": {Options{Domain: "example.com", WebmailBind: "127.0.0.1:9000", StalwartBind: "127.0.0.1:9000"}, "are both"},
|
|
"subnet too small": {Options{Domain: "example.com", Subnet: "10.0.0.0/29"}, "--subnet"},
|
|
"subnet not a network": {Options{Domain: "example.com", Subnet: "10.0.0.5/24"}, "--subnet"},
|
|
"acme flags with local": {Options{Local: true, ACMEDirectory: "https://ca.internal/dir"}, "no effect with --local"},
|
|
"acme over http": {Options{Domain: "example.com", ACMEDirectory: "http://ca.internal/dir"}, "must be an https URL"},
|
|
"image with a space": {Options{Domain: "example.com", CaddyImage: "caddy 2"}, "--caddy-image"},
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
_, err := tc.o.Validate()
|
|
if err == nil || !strings.Contains(err.Error(), tc.want) {
|
|
t.Fatalf("err = %v, want it to mention %q", err, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// A bad domain is one mistake, not a cascade of defaults built from it.
|
|
func TestBadDomainIsReportedOnce(t *testing.T) {
|
|
_, err := Options{Domain: "bad domain"}.Validate()
|
|
if err == nil {
|
|
t.Fatal("no error")
|
|
}
|
|
if lines := strings.Split(err.Error(), "\n"); len(lines) != 1 {
|
|
t.Errorf("got %d errors: %v", len(lines), err)
|
|
}
|
|
}
|
|
|
|
func TestEveryProblemAtOnce(t *testing.T) {
|
|
_, err := Options{Domain: "example.com", MailHost: "mx.other.net", Users: []string{"bob smith"}, WebmailBind: "x"}.Validate()
|
|
if err == nil {
|
|
t.Fatal("no error")
|
|
}
|
|
if lines := strings.Split(err.Error(), "\n"); len(lines) != 3 {
|
|
t.Errorf("got %d errors, want 3: %v", len(lines), err)
|
|
}
|
|
}
|
|
|
|
func TestIhasmailFollowsTheNewestReleaseUnlessNamed(t *testing.T) {
|
|
p, err := Options{Domain: "example.com"}.Validate()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !p.FollowsNewestIhasmail() || p.IhasmailImage != "ghcr.io/coffey-labs/ihasmail:latest" {
|
|
t.Errorf("default ihasmail image %q", p.IhasmailImage)
|
|
}
|
|
named, err := Options{Domain: "example.com", IhasmailImage: "ghcr.io/coffey-labs/ihasmail:2026.9.13-pr344"}.Validate()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if named.FollowsNewestIhasmail() {
|
|
t.Error("a named image is treated as the newest release")
|
|
}
|
|
byDigest := "ghcr.io/coffey-labs/ihasmail@sha256:" + strings.Repeat("a", 64)
|
|
if _, err := (Options{Domain: "example.com", IhasmailImage: byDigest}).Validate(); err != nil {
|
|
t.Errorf("a by-digest image is refused: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestIhasmailTag(t *testing.T) {
|
|
for version, want := range map[string]string{
|
|
"2026.9.13+pr344": "2026.9.13-pr344",
|
|
"2026.10.2+g1fa6578": "2026.10.2-g1fa6578",
|
|
"0.0.0": "",
|
|
"": "",
|
|
"2026.9.13": "",
|
|
"2026.9.13+pr344\n": "",
|
|
"latest": "",
|
|
} {
|
|
got, ok := IhasmailTag(version)
|
|
if got != want || ok != (want != "") {
|
|
t.Errorf("IhasmailTag(%q) = %q, %v; want %q", version, got, ok, want)
|
|
}
|
|
}
|
|
}
|