init
This commit is contained in:
@@ -0,0 +1,266 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestDefaultIsValid(t *testing.T) {
|
||||
cfg := Default()
|
||||
if err := cfg.Validate(); err != nil {
|
||||
t.Fatalf("the built-in default must validate: %v", err)
|
||||
}
|
||||
if len(cfg.TrustedProxies()) != 2 {
|
||||
t.Errorf("trusted proxies = %v, want loopback v4 + v6", cfg.TrustedProxies())
|
||||
}
|
||||
}
|
||||
|
||||
func writeConfig(t *testing.T, body string) string {
|
||||
t.Helper()
|
||||
path := filepath.Join(t.TempDir(), "config.toml")
|
||||
if err := os.WriteFile(path, []byte(body), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
// Precedence is flag > env > file > default, and — the part that is easy to get
|
||||
// wrong — a flag left unset must not shadow the file or the environment with its
|
||||
// default value.
|
||||
func TestLoadPrecedence(t *testing.T) {
|
||||
path := writeConfig(t, `
|
||||
data_dir = "/srv/from-file"
|
||||
webroot = "/var/www/from-file"
|
||||
listen = ":9001"
|
||||
log_level = "warn"
|
||||
gc_interval = "9m"
|
||||
`)
|
||||
|
||||
t.Setenv("PAGES_WEBROOT", "/var/www/from-env")
|
||||
t.Setenv("PAGES_LOG_LEVEL", "debug")
|
||||
|
||||
opts, err := Load([]string{
|
||||
"-config", path,
|
||||
"-log-level", "error",
|
||||
"-gc-interval", "1m",
|
||||
}, io.Discard)
|
||||
if err != nil {
|
||||
t.Fatalf("Load: %v", err)
|
||||
}
|
||||
cfg := opts.Config
|
||||
|
||||
if cfg.LogLevel != "error" {
|
||||
t.Errorf("log_level = %q, want the flag to win", cfg.LogLevel)
|
||||
}
|
||||
if cfg.Webroot != "/var/www/from-env" {
|
||||
t.Errorf("webroot = %q, want the env to beat the file", cfg.Webroot)
|
||||
}
|
||||
if cfg.DataDir != "/srv/from-file" {
|
||||
t.Errorf("data_dir = %q, want the file to beat the default", cfg.DataDir)
|
||||
}
|
||||
if cfg.Listen != ":9001" {
|
||||
t.Errorf("listen = %q, want the file value (no flag, no env)", cfg.Listen)
|
||||
}
|
||||
if cfg.GCInterval.D() != time.Minute {
|
||||
t.Errorf("gc_interval = %s, want the flag to win", cfg.GCInterval)
|
||||
}
|
||||
if cfg.APIListen != Default().APIListen {
|
||||
t.Errorf("api_listen = %q, want the untouched default", cfg.APIListen)
|
||||
}
|
||||
}
|
||||
|
||||
// An unset flag defaults to the same value as Default(), so a naive
|
||||
// implementation silently overwrites whatever the file said with that default.
|
||||
func TestUnsetFlagDoesNotShadowFile(t *testing.T) {
|
||||
path := writeConfig(t, "log_format = \"text\"\napi_listen = \"127.0.0.1:9999\"\n")
|
||||
opts, err := Load([]string{"-config", path}, io.Discard)
|
||||
if err != nil {
|
||||
t.Fatalf("Load: %v", err)
|
||||
}
|
||||
if opts.Config.LogFormat != "text" {
|
||||
t.Errorf("log_format = %q, want text", opts.Config.LogFormat)
|
||||
}
|
||||
if opts.Config.APIListen != "127.0.0.1:9999" {
|
||||
t.Errorf("api_listen = %q, want the file value", opts.Config.APIListen)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadRejectsUnknownKey(t *testing.T) {
|
||||
path := writeConfig(t, "data_dir = \"/srv/x\"\nlog_levle = \"debug\"\n")
|
||||
_, err := Load([]string{"-config", path}, io.Discard)
|
||||
if err == nil {
|
||||
t.Fatal("a typo in the config file must fail loudly")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "log_levle") {
|
||||
t.Errorf("error should name the offending key, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadRejectsUnknownArgs(t *testing.T) {
|
||||
if _, err := Load([]string{"serve"}, io.Discard); err == nil {
|
||||
t.Fatal("positional arguments must be rejected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadEnvDurationError(t *testing.T) {
|
||||
t.Setenv("PAGES_GC_INTERVAL", "fifteen minutes")
|
||||
_, err := Load(nil, io.Discard)
|
||||
if err == nil || !strings.Contains(err.Error(), "PAGES_GC_INTERVAL") {
|
||||
t.Fatalf("want an error naming the variable, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadTrustedProxiesFromEnv(t *testing.T) {
|
||||
t.Setenv("PAGES_TRUSTED_PROXY_CIDRS", "10.0.0.0/8, 192.168.0.0/16 ,")
|
||||
opts, err := Load(nil, io.Discard)
|
||||
if err != nil {
|
||||
t.Fatalf("Load: %v", err)
|
||||
}
|
||||
got := opts.Config.TrustedProxies()
|
||||
if len(got) != 2 || got[0].String() != "10.0.0.0/8" || got[1].String() != "192.168.0.0/16" {
|
||||
t.Errorf("trusted proxies = %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidate(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
mutate func(*Config)
|
||||
wantErr string
|
||||
}{
|
||||
{"webroot inside data_dir", func(c *Config) {
|
||||
c.DataDir = "/var/lib/pages"
|
||||
c.Webroot = "/var/lib/pages/www"
|
||||
}, "nested"},
|
||||
{"data_dir inside webroot", func(c *Config) {
|
||||
c.DataDir = "/srv/www/state"
|
||||
c.Webroot = "/srv/www"
|
||||
}, "nested"},
|
||||
{"identical dirs", func(c *Config) {
|
||||
c.DataDir = "/srv/www"
|
||||
c.Webroot = "/srv/www"
|
||||
}, "must differ"},
|
||||
{"listeners collide", func(c *Config) {
|
||||
c.Listen = "127.0.0.1:8080"
|
||||
c.APIListen = "127.0.0.1:8080"
|
||||
}, "must differ"},
|
||||
{"listen without port", func(c *Config) { c.Listen = "8080" }, "listen"},
|
||||
{"bad cidr", func(c *Config) { c.TrustedProxyCIDRs = []string{"10.0.0.1"} }, "trusted_proxy_cidrs"},
|
||||
{"bad assemble mode", func(c *Config) { c.AssembleMode = "symlink" }, "assemble_mode"},
|
||||
{"bad log level", func(c *Config) { c.LogLevel = "verbose" }, "log_level"},
|
||||
{"zero interval", func(c *Config) { c.GCInterval = 0 }, "gc_interval"},
|
||||
{"negative limit", func(c *Config) { c.Limits.MaxFileBytes = -1 }, "max_file_bytes"},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cfg := Default()
|
||||
tc.mutate(&cfg)
|
||||
err := cfg.Validate()
|
||||
if err == nil {
|
||||
t.Fatalf("want an error mentioning %q", tc.wantErr)
|
||||
}
|
||||
if !strings.Contains(err.Error(), tc.wantErr) {
|
||||
t.Errorf("error = %v, want it to mention %q", err, tc.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// assemble_mode = "none" means no webroot is maintained, so the webroot checks
|
||||
// must not fire.
|
||||
func TestValidateAllowsEmptyWebrootWhenAssemblyDisabled(t *testing.T) {
|
||||
cfg := Default()
|
||||
cfg.AssembleMode = AssembleNone
|
||||
cfg.Webroot = ""
|
||||
if err := cfg.Validate(); err != nil {
|
||||
t.Fatalf("Validate: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateMakesPathsAbsolute(t *testing.T) {
|
||||
cfg := Default()
|
||||
cfg.DataDir = "state/./"
|
||||
cfg.Webroot = "www"
|
||||
if err := cfg.Validate(); err != nil {
|
||||
t.Fatalf("Validate: %v", err)
|
||||
}
|
||||
if !filepath.IsAbs(cfg.DataDir) || strings.HasSuffix(cfg.DataDir, "/") {
|
||||
t.Errorf("data_dir = %q, want a cleaned absolute path", cfg.DataDir)
|
||||
}
|
||||
if !filepath.IsAbs(cfg.Webroot) {
|
||||
t.Errorf("webroot = %q, want an absolute path", cfg.Webroot)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureDirs(t *testing.T) {
|
||||
base := t.TempDir()
|
||||
cfg := Default()
|
||||
cfg.DataDir = filepath.Join(base, "state")
|
||||
cfg.Webroot = filepath.Join(base, "www")
|
||||
if err := cfg.Validate(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := cfg.EnsureDirs(); err != nil {
|
||||
t.Fatalf("EnsureDirs: %v", err)
|
||||
}
|
||||
for _, d := range []string{cfg.DataDir, cfg.CASDir(), filepath.Join(cfg.CASDir(), "tmp"), cfg.DeploymentsDir(), cfg.Webroot} {
|
||||
fi, err := os.Stat(d)
|
||||
if err != nil {
|
||||
t.Errorf("missing %s: %v", d, err)
|
||||
continue
|
||||
}
|
||||
if !fi.IsDir() {
|
||||
t.Errorf("%s is not a directory", d)
|
||||
}
|
||||
}
|
||||
// Idempotent: a restart must not fail on directories that already exist.
|
||||
if err := cfg.EnsureDirs(); err != nil {
|
||||
t.Fatalf("second EnsureDirs: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProjectNamePattern(t *testing.T) {
|
||||
valid := []string{"a", "demo", "my-site", "my.site", "my_site", "a1", strings.Repeat("x", 63)}
|
||||
invalid := []string{
|
||||
"", "-lead", ".lead", "_lead", "UPPER", "has space", "has/slash", "..",
|
||||
"a/../b", "a\\b", "tilde~", strings.Repeat("x", 64), "naïve", "a\x00b",
|
||||
}
|
||||
for _, s := range valid {
|
||||
if !ProjectNamePattern.MatchString(s) {
|
||||
t.Errorf("%q should be a valid project name", s)
|
||||
}
|
||||
}
|
||||
for _, s := range invalid {
|
||||
if ProjectNamePattern.MatchString(s) {
|
||||
t.Errorf("%q must be rejected as a project name", s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDurationRoundTrip(t *testing.T) {
|
||||
var d Duration
|
||||
if err := d.Set("15m30s"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if d.D() != 15*time.Minute+30*time.Second {
|
||||
t.Errorf("d = %s", d)
|
||||
}
|
||||
b, err := d.MarshalText()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var back Duration
|
||||
if err := back.UnmarshalText(b); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if back != d {
|
||||
t.Errorf("round trip: %s != %s", back, d)
|
||||
}
|
||||
if err := d.Set("soon"); err == nil {
|
||||
t.Error("Set must reject a non-duration")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user