Files
ihasmail-oneshot/internal/config/config_test.go
T
jcoffey-dev 408fc20d7b Deploy ihasmail's newest release, recorded by its dated tag
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.
2026-09-15 12:00:20 -07:00

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)
}
}
}