Files
jarl/internal/config/config_test.go
adamandClaude Opus 5.5 88f63ffb0a TLS certificates: Jarl obtains and renews them over ACME
Jarl is now an ACME client (lego's core, no DNS providers). It answers
DNS-01 challenges in a zone it serves, or through Porkbun's DNS API while
Porkbun serves the domain; "auto" decides from the domain's public name
servers at every renewal, and a CNAME on _acme-challenge is followed.
Certificates are used for jarld's HTTPS (served without a restart; each
port speaks HTTPS and plain HTTP, plain HTTP from loopback and trusted
proxies still served) and/or written for named's DNS over TLS/HTTPS, with
rndc reconfig on renewal. Renewal follows the CA's ARI window or two
thirds of the lifetime, with back-off. Certificates you renew yourself
can be used too. Certificates page and dashboard card, jarl cert,
/api/v1/certificates, Prometheus gauges, docs/certificates.md, and an
e2e section against Pebble.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 14:35:53 +01:00

119 lines
4.1 KiB
Go

package config
import (
"os"
"path/filepath"
"reflect"
"testing"
"time"
)
func TestExampleParses(t *testing.T) {
path := filepath.Join(t.TempDir(), "jarl.yaml")
os.WriteFile(path, []byte(Example), 0o644)
c, err := Load(path, false)
if err != nil {
t.Fatal(err)
}
d := Default()
if c.BIND.ZoneDir != d.BIND.ZoneDir || time.Duration(c.Session.IdleTimeout) != 2*time.Hour || c.Listen[0] != "127.0.0.1:8053" {
t.Errorf("example differs from defaults: %+v", c)
}
if !reflect.DeepEqual(c.ACME, d.ACME) || !reflect.DeepEqual(c.Certificates, d.Certificates) || c.ACME.HTTPReq.Enabled || !c.ACME.HTTPReq.RequireHTTPS {
t.Errorf("acme = %+v, want the defaults %+v", c.ACME, d.ACME)
}
}
func TestHTTPReqDefaults(t *testing.T) {
path := filepath.Join(t.TempDir(), "jarl.yaml")
// Enabling the endpoint keeps require_https on unless it is turned off.
os.WriteFile(path, []byte("acme: {httpreq: {enabled: true}}\n"), 0o644)
c, err := Load(path, false)
if err != nil {
t.Fatal(err)
}
if h := c.ACME.HTTPReq; !h.Enabled || !h.RequireHTTPS || h.AllowLoopbackHTTP {
t.Errorf("httpreq = %+v", h)
}
}
func TestLoad(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "jarl.yaml")
os.WriteFile(path, []byte("listen: [\"0.0.0.0:8443\"]\ntls: {cert_file: /a, key_file: /b}\ntrusted_proxies: [10.0.0.1, 192.168.0.0/16]\nsession: {lifetime: 1d}\n"), 0o644)
c, err := Load(path, false)
if err != nil {
t.Fatal(err)
}
if time.Duration(c.Session.Lifetime) != 24*time.Hour || len(c.Proxies()) != 2 || c.BIND.FileGroup != "bind" {
t.Errorf("config = %+v", c)
}
os.WriteFile(path, []byte("lisen: []\n"), 0o644)
if _, err := Load(path, false); err == nil {
t.Error("unknown field accepted")
}
os.WriteFile(path, []byte("tls: {cert_file: /a}\n"), 0o644)
if _, err := Load(path, false); err == nil {
t.Error("half TLS config accepted")
}
if _, err := Load(filepath.Join(dir, "missing.yaml"), true); err != nil {
t.Errorf("missing file with allowMissing: %v", err)
}
}
func TestStatsConfig(t *testing.T) {
d := Default()
if d.Stats.Enabled || !d.Stats.QueryLog || d.Stats.TopN != 100 || time.Duration(d.Stats.Retention.Hour) != 90*24*time.Hour {
t.Errorf("defaults = %+v", d.Stats)
}
dir := t.TempDir()
path := filepath.Join(dir, "jarl.yaml")
for _, tc := range []struct {
yaml string
ok bool
}{
{"stats: {enabled: true}", true},
{"stats: {enabled: true, query_log: false, retention: {minute: 2d}}", true},
{"stats: {enabled: true, statistics_channel: 0.0.0.0:8058}", false}, // must be loopback
{"stats: {enabled: true, statistics_channel: localhost}", false},
{"stats: {enabled: true, log_size: lots}", false},
{"stats: {enabled: true, retention: {minute: 10m}}", false},
{"stats: {enabled: true, exclude_clients: [not-an-ip]}", false},
{"stats: {enabled: false, statistics_channel: nonsense}", true}, // only checked when enabled
} {
os.WriteFile(path, []byte(tc.yaml+"\n"), 0o644)
_, err := Load(path, false)
if (err == nil) != tc.ok {
t.Errorf("%s: err = %v, want ok=%v", tc.yaml, err, tc.ok)
}
}
}
func TestMetricsConfig(t *testing.T) {
d := Default()
if d.Metrics.Enabled || len(d.Metrics.AllowPrefixes()) != 2 || d.Metrics.Token != "" {
t.Errorf("defaults = %+v", d.Metrics)
}
path := filepath.Join(t.TempDir(), "jarl.yaml")
for _, tc := range []struct {
yaml string
ok bool
}{
{"metrics: {enabled: true}", true},
{"metrics: {enabled: true, listen: [\"0.0.0.0:9450\", \"[::]:9450\"], allow: [10.0.0.5, 192.168.1.0/24]}", true},
{"metrics: {enabled: true, allow: [], token: 0123456789abcdef}", true},
{"metrics: {enabled: true, allow: []}", false}, // nobody could scrape
{"metrics: {enabled: true, token: short}", false},
{"metrics: {enabled: true, token: \"has space 0123456789\"}", false},
{"metrics: {enabled: true, listen: [\"9450\"]}", false},
{"metrics: {enabled: true, allow: [prometheus.lan]}", false},
{"metrics: {enabled: false, listen: [nonsense]}", true}, // only checked when enabled
} {
os.WriteFile(path, []byte(tc.yaml+"\n"), 0o644)
_, err := Load(path, false)
if (err == nil) != tc.ok {
t.Errorf("%s: err = %v, want ok=%v", tc.yaml, err, tc.ok)
}
}
}