internal/server/caddy_sync_config_test.go
Rama main · réplica de lectura del repositorio original.
// SPDX-License-Identifier: Apache-2.0
package server
import (
"encoding/json"
"net/http"
"net/http/httptest"
"path/filepath"
"reflect"
"testing"
"github.com/X4Applegate/caddyui/internal/caddy"
appdb "github.com/X4Applegate/caddyui/internal/db"
"github.com/X4Applegate/caddyui/internal/dns"
"github.com/X4Applegate/caddyui/internal/models"
)
func TestHTTPRoutesReplaceUnsupportedSkipRedirectsWithoutPortConflict(t *testing.T) {
conn, err := appdb.Open(filepath.Join(t.TempDir(), "caddyui.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = conn.Close() })
s := &Server{DB: conn}
proxies := []models.ProxyHost{
{Domains: "open.example.com", Enabled: true, SSLEnabled: true, SSLForced: false, ForwardScheme: "http", ForwardHost: "app", ForwardPort: 8080},
{Domains: "forced.example.com", Enabled: true, SSLEnabled: true, SSLForced: true, ForwardScheme: "http", ForwardHost: "app", ForwardPort: 8080},
}
routes := s.buildHTTPRoutes(proxies, nil, nil)
if len(routes) != 3 { // v2.59.4: open route, forced redirect, 404 fallback
t.Fatalf("HTTP routes = %d, want open route plus forced redirect plus the fallback", len(routes))
}
cfg := map[string]any{
"apps": map[string]any{
"http": map[string]any{
"servers": map[string]any{
"srv0": map[string]any{
"automatic_https": map[string]any{
"skip_redirects": []any{"open.example.com"},
"skip_certificates": []any{"plain.example.com"},
},
},
},
},
},
}
removeUnsupportedSkipRedirects(cfg)
applyPlainHTTPServer(cfg, routes)
applyDisableAutomaticHTTPSRedirects(cfg, true)
servers := cfg["apps"].(map[string]any)["http"].(map[string]any)["servers"].(map[string]any)
auto := servers["srv0"].(map[string]any)["automatic_https"].(map[string]any)
if _, exists := auto["skip_redirects"]; exists {
t.Fatal("unsupported automatic_https.skip_redirects was not removed")
}
if disabled, _ := auto["disable_redirects"].(bool); !disabled {
t.Fatal("automatic HTTPS redirects were not disabled")
}
httpServer := servers["caddyui_http"].(map[string]any)
if !reflect.DeepEqual(httpServer["listen"], []any{":80"}) {
t.Fatalf("HTTP listen = %#v, want [:80]", httpServer["listen"])
}
redirect := routes[1].(map[string]any)
handler := redirect["handle"].([]any)[0].(map[string]any)
if handler["status_code"] != 308 {
t.Fatalf("forced HTTP status = %#v, want 308", handler["status_code"])
}
}
func TestHTTPRoutesIncludeRawRouteForceSSLBehavior(t *testing.T) {
conn, err := appdb.Open(filepath.Join(t.TempDir(), "caddyui.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = conn.Close() })
s := &Server{DB: conn}
raws := []models.RawRoute{
{Enabled: true, ForceSSL: false, JSONData: `{"match":[{"host":["open-raw.example.com"]}],"handle":[{"handler":"static_response","status_code":200}]}`},
{Enabled: true, ForceSSL: true, JSONData: `{"match":[{"host":["forced-raw.example.com"]}],"handle":[{"handler":"static_response","status_code":200}]}`},
}
routes := s.buildHTTPRoutes(nil, nil, raws)
if len(routes) != 3 { // v2.59.4: open raw route, forced redirect, 404 fallback
t.Fatalf("HTTP routes = %d, want open raw route plus forced redirect plus the fallback", len(routes))
}
redirect := routes[1].(map[string]any)
match := redirect["match"].([]any)[0].(map[string]any)
if !reflect.DeepEqual(match["host"], []any{"forced-raw.example.com"}) {
t.Fatalf("forced raw redirect hosts = %#v", match["host"])
}
}
func TestCaddyDNSProviderConfig(t *testing.T) {
tests := []struct {
name string
provider string
creds map[string]string
want map[string]any
}{
{"cloudflare", dns.Cloudflare, map[string]string{"cf_api_token": "token"}, map[string]any{"name": "cloudflare", "api_token": "token"}},
{"porkbun", dns.Porkbun, map[string]string{"pb_api_key": "key", "pb_secret_key": "secret"}, map[string]any{"name": "porkbun", "api_key": "key", "api_secret_key": "secret"}},
{"namecheap", dns.Namecheap, map[string]string{"nc_api_user": "user", "nc_api_key": "key", "nc_client_ip": "192.0.2.1"}, map[string]any{"name": "namecheap", "user": "user", "api_key": "key", "client_ip": "192.0.2.1"}},
{"godaddy", dns.GoDaddy, map[string]string{"gd_api_key": "key", "gd_api_secret": "secret"}, map[string]any{"name": "godaddy", "api_token": "key:secret"}},
{"digitalocean", dns.DigitalOcean, map[string]string{"do_api_token": "token"}, map[string]any{"name": "digitalocean", "auth_token": "token"}},
{"hetzner", dns.Hetzner, map[string]string{"hetzner_api_token": "token"}, map[string]any{"name": "hetzner", "api_token": "token"}},
{"gandi", dns.Gandi, map[string]string{"gandi_api_token": " pat "}, map[string]any{"name": "gandi", "bearer_token": "pat"}},
{"gandi-blank", dns.Gandi, map[string]string{"gandi_api_token": ""}, nil},
{"route53", dns.Route53, map[string]string{"route53_access_key_id": "AKIAEXAMPLE", "route53_secret_access_key": "secret"}, map[string]any{"name": "route53", "access_key_id": "AKIAEXAMPLE", "secret_access_key": "secret", "region": "us-east-1"}},
{"route53-temporary", dns.Route53, map[string]string{"route53_access_key_id": "AKIAEXAMPLE", "route53_secret_access_key": "secret", "route53_session_token": "session", "route53_region": "us-gov-west-1"}, map[string]any{"name": "route53", "access_key_id": "AKIAEXAMPLE", "secret_access_key": "secret", "session_token": "session", "region": "us-gov-west-1"}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := caddyDNSProviderConfig(tt.provider, tt.creds, ""); !reflect.DeepEqual(got, tt.want) {
t.Fatalf("config = %#v, want %#v", got, tt.want)
}
})
}
}
func TestRoute53ProviderConfigPinsSelectedHostedZone(t *testing.T) {
got := caddyDNSProviderConfig(dns.Route53, map[string]string{
"route53_access_key_id": "AKIAEXAMPLE",
"route53_secret_access_key": "secret",
}, "Z0123456789")
if got["hosted_zone_id"] != "Z0123456789" {
t.Fatalf("hosted_zone_id = %#v, want selected zone", got["hosted_zone_id"])
}
}
func TestDNSAutomationPoliciesSeparateRoute53ZonesAndKeepDNSOnlyHosts(t *testing.T) {
conn, err := appdb.Open(filepath.Join(t.TempDir(), "caddyui.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = conn.Close() })
for key, value := range map[string]string{
settingRoute53AccessKeyID: "AKIAEXAMPLE",
settingRoute53SecretAccessKey: "secret",
} {
if err := models.SetSetting(conn, key, value); err != nil {
t.Fatal(err)
}
}
s := &Server{DB: conn}
policies := s.buildDNSAutomationPolicies([]models.ProxyHost{
{Domains: "public.example.com", Enabled: true, SSLEnabled: true, DNSProvider: dns.Route53, DNSZoneID: "ZPUBLIC"},
{Domains: "internal.example.net", Enabled: true, SSLEnabled: true, DNSProvider: dns.Route53, DNSZoneID: "ZINTERNAL", DNSSkipRecord: true},
}, nil, nil, nil)
if len(policies) != 2 {
t.Fatalf("policies = %d, want one per Route53 hosted zone", len(policies))
}
want := map[string]string{"ZPUBLIC": "public.example.com", "ZINTERNAL": "internal.example.net"}
for _, policy := range policies {
issuers := policy["issuers"].([]any)
issuer := issuers[0].(map[string]any)
challenge := issuer["challenges"].(map[string]any)["dns"].(map[string]any)
provider := challenge["provider"].(map[string]any)
zone, _ := provider["hosted_zone_id"].(string)
subjects := policy["subjects"].([]any)
if len(subjects) != 1 || subjects[0] != want[zone] {
t.Fatalf("zone %q subjects = %#v, want %q", zone, subjects, want[zone])
}
delete(want, zone)
}
if len(want) != 0 {
t.Fatalf("missing Route53 zone policies: %#v", want)
}
}
func TestBuildInternalTLSAutomationPolicies(t *testing.T) {
proxies := []models.ProxyHost{
{Domains: "nas.home.lan", Enabled: true, SSLEnabled: true, InternalTLS: true},
{Domains: "printer.home.lan", Enabled: true, SSLEnabled: true, InternalTLS: true},
// Excluded: opted in but pinned to a custom certificate.
{Domains: "custom.home.lan", Enabled: true, SSLEnabled: true, InternalTLS: true, CertificateID: 7},
// Excluded: not opted in (default Auto ACME).
{Domains: "public.example.com", Enabled: true, SSLEnabled: true},
// Excluded: disabled and SSL-off hosts never issue.
{Domains: "off.home.lan", Enabled: false, SSLEnabled: true, InternalTLS: true},
{Domains: "plain.home.lan", Enabled: true, SSLEnabled: false, InternalTLS: true},
}
policies := buildInternalTLSAutomationPolicies(proxies)
if len(policies) != 1 {
t.Fatalf("policies = %d, want a single internal-issuer policy", len(policies))
}
issuers := policies[0]["issuers"].([]any)
if module := issuers[0].(map[string]any)["module"]; module != "internal" {
t.Fatalf("issuer module = %v, want \"internal\"", module)
}
got := map[string]bool{}
for _, s := range policies[0]["subjects"].([]any) {
got[s.(string)] = true
}
want := map[string]bool{"nas.home.lan": true, "printer.home.lan": true}
if len(got) != len(want) {
t.Fatalf("subjects = %#v, want %#v", got, want)
}
for name := range want {
if !got[name] {
t.Fatalf("missing internal-CA subject %q in %#v", name, got)
}
}
for _, unexpected := range []string{"custom.home.lan", "public.example.com", "off.home.lan", "plain.home.lan"} {
if got[unexpected] {
t.Fatalf("subject %q should not be issued by the internal CA", unexpected)
}
}
}
// An internal-CA host must never be folded into automatic_https.skip_certificates
// by wildcard-managed-certificate coverage: it gets its own cert from the
// internal issuer, so skipping it would leave it with no certificate at all.
func TestBuildSkipCertificatesExcludesInternalTLSHosts(t *testing.T) {
proxies := []models.ProxyHost{
{Domains: "nas.home.lan", Enabled: true, SSLEnabled: true, InternalTLS: true},
}
certs := []models.Certificate{
{Source: models.CertSourceManaged, Domains: "*.home.lan"},
}
skip := buildSkipCertificates(proxies, nil, nil, certs)
for _, s := range skip {
if s == "nas.home.lan" {
t.Fatalf("internal-CA host must not be in skip_certificates; got %#v", skip)
}
}
}
// buildDNSAutomationPolicies must not emit a DNS-01 policy for a host that
// opted into internal-CA issuance, even if it still carries a DNS provider.
func TestDNSAutomationPoliciesSkipInternalTLSHosts(t *testing.T) {
conn, err := appdb.Open(filepath.Join(t.TempDir(), "caddyui.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = conn.Close() })
for key, value := range map[string]string{
settingRoute53AccessKeyID: "AKIAEXAMPLE",
settingRoute53SecretAccessKey: "secret",
} {
if err := models.SetSetting(conn, key, value); err != nil {
t.Fatal(err)
}
}
s := &Server{DB: conn}
policies := s.buildDNSAutomationPolicies([]models.ProxyHost{
{Domains: "nas.home.lan", Enabled: true, SSLEnabled: true, InternalTLS: true, DNSProvider: dns.Route53, DNSZoneID: "ZHOME"},
}, nil, nil, nil)
if len(policies) != 0 {
t.Fatalf("policies = %d, want 0 — internal-CA hosts must not get a DNS-01 policy", len(policies))
}
}
func TestMergeAutomationPoliciesReplacesDesiredSubjectsOnly(t *testing.T) {
existing := []any{
map[string]any{"subjects": []any{"replace.example.com", "keep.example.com"}, "issuer": "old"},
map[string]any{"issuer": "catch-all"},
}
incoming := []map[string]any{{"subjects": []any{"replace.example.com"}, "issuer": "new"}}
got := mergeAutomationPolicies(existing, incoming)
if len(got) != 3 {
t.Fatalf("policies = %#v, want incoming + trimmed existing + catch-all", got)
}
if got[0].(map[string]any)["issuer"] != "new" {
t.Fatalf("first policy = %#v, want incoming authoritative policy", got[0])
}
if !reflect.DeepEqual(got[1].(map[string]any)["subjects"], []any{"keep.example.com"}) {
t.Fatalf("preserved subjects = %#v", got[1])
}
if got[2].(map[string]any)["issuer"] != "catch-all" {
t.Fatalf("catch-all policy was not preserved: %#v", got[2])
}
}
func TestManagedCertificateRoutesTriggerAutomaticHTTPS(t *testing.T) {
certs := []models.Certificate{
{ID: 1, Source: models.CertSourcePEM, Domains: "*.ignored.example.com"},
{ID: 2, Source: models.CertSourceManaged, Domains: "*.example.com, *.sub.example.com"},
}
routes := buildManagedCertificateRoutes(certs, nil)
if len(routes) != 1 {
t.Fatalf("routes = %d, want 1 managed-certificate route", len(routes))
}
route := routes[0].(map[string]any)
match := route["match"].([]any)[0].(map[string]any)
wantHosts := []any{"*.example.com", "*.sub.example.com"}
if !reflect.DeepEqual(match["host"], wantHosts) {
t.Fatalf("hosts = %#v, want %#v", match["host"], wantHosts)
}
handler := route["handle"].([]any)[0].(map[string]any)
if handler["status_code"] != 404 || route["terminal"] != true {
t.Fatalf("managed fallback = %#v, want terminal 404", route)
}
}
func TestManagedCertificatePersistsDNSSelectionAndIsNotCustomOption(t *testing.T) {
conn, err := appdb.Open(filepath.Join(t.TempDir(), "caddyui.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = conn.Close() })
id, err := models.CreateCertificate(conn, 1, 0, &models.Certificate{
Name: "wildcard",
Domains: "*.example.com",
Source: models.CertSourceManaged,
DNSProvider: dns.Cloudflare,
DNSProfileID: "profile-1",
})
if err != nil {
t.Fatal(err)
}
got, err := models.GetCertificate(conn, id)
if err != nil {
t.Fatal(err)
}
if got.Source != models.CertSourceManaged || got.DNSProvider != dns.Cloudflare || got.DNSProfileID != "profile-1" {
t.Fatalf("managed certificate round trip = %#v", got)
}
options, err := models.ListCertificateOptionsForUser(conn, 1, 0, true, nil)
if err != nil {
t.Fatal(err)
}
if len(options) != 0 {
t.Fatalf("custom certificate options = %#v, managed certificate must not be assignable as uploaded PEM", options)
}
}
func TestManagedCertificateCoverageUsesSingleLabelWildcards(t *testing.T) {
tests := []struct {
cert string
host string
want bool
}{
{"*.example.com", "app.example.com", true},
{"*.example.com", "app.example.com:8000", true},
{"*.example.com", "APP.EXAMPLE.COM.", true},
{"*.example.com", "deep.app.example.com", false},
{"*.example.com", "example.com", false},
{"*.sub.example.com", "app.sub.example.com", true},
{"app.example.com", "app.example.com", true},
{"*.example.com", "*.example.com", true},
}
for _, tt := range tests {
if got := managedCertificateCovers(tt.cert, tt.host); got != tt.want {
t.Errorf("managedCertificateCovers(%q, %q) = %v, want %v", tt.cert, tt.host, got, tt.want)
}
}
}
func TestManagedWildcardForHost(t *testing.T) {
certs := []models.Certificate{
{ID: 1, Name: "Direct managed", Domains: "app.example.com", Source: models.CertSourceManaged},
{ID: 2, Name: "Example wildcard", Domains: "*.example.com", Source: models.CertSourceManaged},
{ID: 3, Name: "Uploaded wildcard", Domains: "*.example.net", Source: models.CertSourcePEM},
}
if got := managedWildcardForHost(certs, "app.example.com"); got == nil || got.Name != "Example wildcard" {
t.Fatalf("managedWildcardForHost() = %#v, want Example wildcard", got)
}
if got := managedWildcardForHost(certs, "app.example.com:8000"); got == nil || got.Name != "Example wildcard" {
t.Fatalf("managedWildcardForHost() with port = %#v, want Example wildcard", got)
}
if got := managedWildcardForHost(certs, "deep.app.example.com"); got != nil {
t.Fatalf("managedWildcardForHost() = %#v, want nil for multi-label host", got)
}
if got := managedWildcardForHost(certs, "app.example.net"); got != nil {
t.Fatalf("managedWildcardForHost() = %#v, want nil for uploaded PEM wildcard", got)
}
if got := managedWildcardForHost(certs, "uncovered.example.org"); got != nil {
t.Fatalf("managedWildcardForHost() = %#v, want nil for uncovered host", got)
}
}
func TestManagedWildcardSkipsCoveredExactCertificateIssuance(t *testing.T) {
proxies := []models.ProxyHost{
{Domains: "project.sub.example.com", Enabled: true, SSLEnabled: true},
{Domains: "deep.project.sub.example.com", Enabled: true, SSLEnabled: true},
{Domains: "sub.example.com", Enabled: true, SSLEnabled: true},
}
certs := []models.Certificate{
{Source: models.CertSourceManaged, Domains: "*.sub.example.com"},
}
got := buildSkipCertificates(proxies, nil, nil, certs)
if !reflect.DeepEqual(got, []any{"project.sub.example.com"}) {
t.Fatalf("skip certificates = %#v, want only wildcard-covered exact hostname", got)
}
}
func TestManagedWildcardSubjectItselfIsNeverSkipped(t *testing.T) {
proxies := []models.ProxyHost{
{Domains: "*.sub.example.com", Enabled: true, SSLEnabled: true},
}
certs := []models.Certificate{
{Source: models.CertSourceManaged, Domains: "*.sub.example.com"},
}
if got := buildSkipCertificates(proxies, nil, nil, certs); len(got) != 0 {
t.Fatalf("skip certificates = %#v, wildcard subject must remain eligible for issuance", got)
}
}
func TestPortQualifiedHostReusesManagedWildcard(t *testing.T) {
proxies := []models.ProxyHost{
{Domains: "project.sub.example.com:8000", Enabled: true, SSLEnabled: true},
}
certs := []models.Certificate{
{Source: models.CertSourceManaged, Domains: "*.sub.example.com"},
}
if got := buildSkipCertificates(proxies, nil, nil, certs); !reflect.DeepEqual(got, []any{"project.sub.example.com"}) {
t.Fatalf("skip certificates = %#v, want port-free wildcard-covered hostname", got)
}
route := caddy.BuildProxyRoute(models.ProxyHost{
Domains: "project.sub.example.com:8000",
ForwardHost: "app",
ForwardPort: 8080,
ForwardScheme: "http",
}, nil)
match := route["match"].([]any)[0].(map[string]any)
if got := match["host"]; !reflect.DeepEqual(got, []any{"project.sub.example.com"}) {
t.Fatalf("route host matcher = %#v, want port-free hostname", got)
}
}
func TestManagedCertificateProbeNameUsesSyntheticWildcardChild(t *testing.T) {
cert := models.Certificate{Domains: "example.com, *.sub.example.com"}
if got := managedCertificateProbeName(cert); got != "caddyui-probe.sub.example.com" {
t.Fatalf("probe name = %q", got)
}
}
func TestEnsureManagedCertificateOnServerDeduplicatesEquivalentSubjects(t *testing.T) {
conn, err := appdb.Open(filepath.Join(t.TempDir(), "caddyui.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = conn.Close() })
sourceServerID, err := models.CreateCaddyServer(conn, &models.CaddyServer{
Name: "source", AdminURL: "http://source:2019", Type: models.CaddyServerTypeManaged,
})
if err != nil {
t.Fatal(err)
}
targetID, err := models.CreateCaddyServer(conn, &models.CaddyServer{
Name: "target", AdminURL: "http://target:2019", Type: models.CaddyServerTypeManaged,
})
if err != nil {
t.Fatal(err)
}
s := &Server{DB: conn}
source := models.Certificate{
Name: "wildcard", Domains: "*.example.com, example.com", Source: models.CertSourceManaged,
DNSProvider: dns.Cloudflare,
}
source.ID, err = models.CreateCertificate(conn, sourceServerID, 0, &source)
if err != nil {
t.Fatal(err)
}
created, err := s.ensureCertificateOnServer("test", sourceServerID, targetID, source, 0)
if err != nil || !created {
t.Fatalf("first ensure = created %v, err %v; want created", created, err)
}
source.Domains = "example.com, *.example.com"
created, err = s.ensureCertificateOnServer("test", sourceServerID, targetID, source, 0)
if err != nil || created {
t.Fatalf("second ensure = created %v, err %v; want existing equivalent", created, err)
}
certs, err := models.ListCertificates(conn, targetID)
if err != nil {
t.Fatal(err)
}
if len(certs) != 1 {
t.Fatalf("target certificates = %d, want 1", len(certs))
}
}
func TestCaddyDNSProviderConfigRejectsIncompleteCredentials(t *testing.T) {
if got := caddyDNSProviderConfig(dns.Porkbun, map[string]string{"pb_api_key": "key"}, ""); got != nil {
t.Fatalf("incomplete credentials returned config %#v", got)
}
if got := caddyDNSProviderConfig(dns.Route53, map[string]string{"route53_access_key_id": "AKIAEXAMPLE"}, ""); got != nil {
t.Fatalf("incomplete Route 53 credentials returned config %#v", got)
}
}
func TestPushAutomationPoliciesCreatesMissingTLSApp(t *testing.T) {
var postedPath string
var posted map[string]any
admin := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodGet && r.URL.Path == "/config/":
_ = json.NewEncoder(w).Encode(map[string]any{
"apps": map[string]any{"http": map[string]any{}},
})
case r.Method == http.MethodPost:
postedPath = r.URL.Path
if err := json.NewDecoder(r.Body).Decode(&posted); err != nil {
t.Errorf("decode POST: %v", err)
}
w.WriteHeader(http.StatusOK)
default:
http.Error(w, "unexpected request", http.StatusBadRequest)
}
}))
defer admin.Close()
incoming := []map[string]any{{
"subjects": []any{"example.com"},
"issuers": []any{map[string]any{"module": "acme"}},
}}
if err := pushAutomationPoliciesVia(caddy.New(admin.URL, "", ""), incoming); err != nil {
t.Fatal(err)
}
if postedPath != "/config/apps/tls" {
t.Fatalf("POST path = %q, want /config/apps/tls", postedPath)
}
automation, _ := posted["automation"].(map[string]any)
policies, _ := automation["policies"].([]any)
if len(policies) != 1 {
t.Fatalf("posted policies = %#v, want one policy", policies)
}
}
func TestPushAutomationPoliciesPreservesExistingTLSAndUsesAutomationPath(t *testing.T) {
var postedPath string
var posted map[string]any
admin := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodGet && r.URL.Path == "/config/":
_ = json.NewEncoder(w).Encode(map[string]any{
"apps": map[string]any{
"http": map[string]any{},
"tls": map[string]any{
"automation": map[string]any{"on_demand": map[string]any{}},
},
},
})
case r.Method == http.MethodPost:
postedPath = r.URL.Path
if err := json.NewDecoder(r.Body).Decode(&posted); err != nil {
t.Errorf("decode POST: %v", err)
}
w.WriteHeader(http.StatusOK)
default:
http.Error(w, "unexpected request", http.StatusBadRequest)
}
}))
defer admin.Close()
incoming := []map[string]any{{"subjects": []any{"example.com"}}}
if err := pushAutomationPoliciesVia(caddy.New(admin.URL, "", ""), incoming); err != nil {
t.Fatal(err)
}
if postedPath != "/config/apps/tls/automation" {
t.Fatalf("POST path = %q, want /config/apps/tls/automation", postedPath)
}
if _, ok := posted["on_demand"]; !ok {
t.Fatalf("existing automation fields were not preserved: %#v", posted)
}
}