The same licence as ihasmail, which this tool deploys. The network clause is the part that matters for a deploy tool: a modified copy offered as a hosted service that deploys for other people has to make its source available to them, which GPL would not require. Running the tool for your own host is unaffected. v2026.9.13 was released under GPL-3.0-or-later and stays so; this applies from the next release.
123 lines
3.7 KiB
Go
123 lines
3.7 KiB
Go
// SPDX-FileCopyrightText: 2026 Coffey Labs
|
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
package render
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/Coffey-Labs/ihasmail-oneshot/internal/config"
|
|
)
|
|
|
|
func plan(t *testing.T, o config.Options) config.Plan {
|
|
t.Helper()
|
|
p, err := o.Validate()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return p
|
|
}
|
|
|
|
func TestPublicCompose(t *testing.T) {
|
|
p := plan(t, config.Options{Domain: "example.com"})
|
|
out, err := Compose(p, "test", false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
s := string(out)
|
|
for _, want := range []string{
|
|
"name: ihasmail-example-com",
|
|
"hostname: mail.example.com",
|
|
`- "127.0.0.1:8081:8080"`,
|
|
`- "25:25"`, `- "465:465"`, `- "993:993"`, `- "995:995"`, `- "4190:4190"`,
|
|
`- "80:80"`, `- "443:443"`, `- "443:443/udp"`,
|
|
"STALWART_URL: http://stalwart:8080",
|
|
"APP_SECRET: ${APP_SECRET:?",
|
|
"PUSH_URL: https://webmail.example.com",
|
|
"read_only: true",
|
|
"ipv4_address: 172.31.253.11",
|
|
"caddy-data:",
|
|
} {
|
|
if !strings.Contains(s, want) {
|
|
t.Errorf("compose.yaml lacks %q:\n%s", want, s)
|
|
}
|
|
}
|
|
// Caddy's ports belong to Caddy; Stalwart must not also claim them.
|
|
stalwart := s[strings.Index(s, " stalwart:"):strings.Index(s, " ihasmail:")]
|
|
if strings.Contains(stalwart, `"80:80"`) || strings.Contains(stalwart, `"443:443"`) {
|
|
t.Errorf("Stalwart publishes 80 or 443:\n%s", stalwart)
|
|
}
|
|
if strings.Contains(s, "ca-bundle.crt") || strings.Contains(s, "acme-ca-root.pem") {
|
|
t.Error("CA files mounted with no private CA")
|
|
}
|
|
}
|
|
|
|
func TestLocalComposeHasNoCaddyAndNoMailPorts(t *testing.T) {
|
|
p := plan(t, config.Options{Local: true})
|
|
out, err := Compose(p, "test", false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
s := string(out)
|
|
for _, unwanted := range []string{"caddy", `"25:25"`, "PUSH_URL"} {
|
|
if strings.Contains(s, unwanted) {
|
|
t.Errorf("local compose.yaml has %q:\n%s", unwanted, s)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCaddyfile(t *testing.T) {
|
|
p := plan(t, config.Options{Domain: "example.com", MailHost: "mx.example.com", Email: "[email protected]"})
|
|
out, err := Caddy(p, "test")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
s := string(out)
|
|
for _, want := range []string{
|
|
"email [email protected]",
|
|
"webmail.example.com {",
|
|
"mx.example.com, autoconfig.example.com, autodiscover.example.com, mta-sts.example.com, ua-auto-config.example.com {",
|
|
"disable_http_challenge",
|
|
"http://mx.example.com, http://autoconfig.example.com, http://autodiscover.example.com, http://mta-sts.example.com, http://ua-auto-config.example.com {",
|
|
"handle /.well-known/acme-challenge/* {",
|
|
"flush_interval -1",
|
|
} {
|
|
if !strings.Contains(s, want) {
|
|
t.Errorf("Caddyfile lacks %q:\n%s", want, s)
|
|
}
|
|
}
|
|
if strings.Contains(s, "acme_ca") {
|
|
t.Error("Caddyfile names a CA without --acme-directory")
|
|
}
|
|
|
|
p = plan(t, config.Options{Domain: "example.com", ACMEDirectory: "https://ca.internal/dir", ACMECARoot: "/tmp/root.pem"})
|
|
out, _ = Caddy(p, "test")
|
|
for _, want := range []string{"acme_ca https://ca.internal/dir", "dir https://ca.internal/dir", "trusted_roots /etc/caddy/acme-ca-root.pem"} {
|
|
if !strings.Contains(string(out), want) {
|
|
t.Errorf("Caddyfile with a private CA lacks %q", want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDirectoryIsNeverOverwritten(t *testing.T) {
|
|
dir := filepath.Join(t.TempDir(), "d")
|
|
if err := PrepareDir(dir); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := WriteFile(dir, EnvFile, []byte("x"), true); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if fi, _ := os.Stat(filepath.Join(dir, EnvFile)); fi.Mode().Perm() != 0o600 {
|
|
t.Errorf("secret file mode %v", fi.Mode().Perm())
|
|
}
|
|
if err := WriteFile(dir, EnvFile, []byte("y"), true); err == nil {
|
|
t.Error("WriteFile replaced an existing file")
|
|
}
|
|
if err := PrepareDir(dir); err == nil {
|
|
t.Error("PrepareDir accepted a directory with files in it")
|
|
}
|
|
}
|