Deploy a fresh Stalwart and ihasmail, linked, in one command
deploy stands up Stalwart 0.16, ihasmail and (for a mail host) Caddy as a compose project: completes Stalwart's bootstrap over x:Bootstrap, links ihasmail over the private network, requests certificates for both Caddy (TLS-ALPN-01) and Stalwart (HTTP-01 through Caddy), makes the auto-ban safe behind the proxy, and proves the link by signing in through the webmail. --local gives a loopback-only pair. certs retries Stalwart's certificate; destroy removes a deployment. e2e/public.sh runs the whole mail-host path against Pebble with no internet involved.
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
// SPDX-FileCopyrightText: 2026 Coffey Labs
|
||||
// SPDX-License-Identifier: GPL-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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user