Files
jcoffey-dev 3fd909cba3
ci / test (pull_request) Successful in 58s
ci / release (pull_request) Skipped
Point links at git.coffeylabs.org after the move from GitHub
GitHub took the organization's repos and GHCR offline on 2026-09-20. Repo,
release, raw-file and clone links now go to Gitea at git.coffeylabs.org,
container images to registry.coffeylabs.org, and GitLab-style /-/blob paths
to Gitea's /src/branch form. Go module paths are identifiers and stay as
they are; links to GitHub issues and pull requests are left as history.
2026-09-22 09:08:08 -07:00

139 lines
5.6 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 != "registry.coffeylabs.org/coffey-labs/ihasmail:latest" {
t.Errorf("default ihasmail image %q", p.IhasmailImage)
}
named, err := Options{Domain: "example.com", IhasmailImage: "registry.coffeylabs.org/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 := "registry.coffeylabs.org/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)
}
}
}