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.
139 lines
5.6 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|