check / check (push) Successful in 15s
README.md documented access_control_allow_origin, upstream_fetch_timeout, upstream_max_response_size and downstream_timeout, but pixa did not know them, so a config following the README aborted startup. Each is now a setting with its PIXA_ variable, defaulting to the value that was fixed in the code: *, 30s, 50 MiB and 60s. Durations are Go duration strings and must be positive; the size is whole bytes, at most 1 GiB. The origin is * or one http or https origin written exactly as a browser sends it; anything else aborts startup. downstream_timeout sets both the server's write timeout and the per-request timeout. The owner approved the edits to existing tests. Model: opus-5-5
401 lines
13 KiB
Go
401 lines
13 KiB
Go
package config
|
|
|
|
import (
|
|
"net/netip"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"sneak.berlin/go/pixa/internal/globals"
|
|
"sneak.berlin/go/pixa/internal/logger"
|
|
)
|
|
|
|
// TestMain unsets PORT and every PIXA_ environment variable before the
|
|
// tests run, so each test sees only the variables it sets itself, not
|
|
// whatever the shell running the tests exports.
|
|
func TestMain(m *testing.M) {
|
|
for _, entry := range os.Environ() {
|
|
name, _, _ := strings.Cut(entry, "=")
|
|
if name != "PORT" && !strings.HasPrefix(name, "PIXA_") {
|
|
continue
|
|
}
|
|
|
|
err := os.Unsetenv(name)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
m.Run()
|
|
}
|
|
|
|
// wantStartupError fails the test unless err is a startup error that
|
|
// mentions every one of wants.
|
|
func wantStartupError(t *testing.T, err error, wants ...string) {
|
|
t.Helper()
|
|
|
|
if err == nil {
|
|
t.Fatalf("want a startup error mentioning %q, got none", wants)
|
|
}
|
|
|
|
t.Logf("got expected error: %v", err)
|
|
|
|
for _, want := range wants {
|
|
if !strings.Contains(err.Error(), want) {
|
|
t.Errorf("error %q does not mention %q", err.Error(), want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestEnvironmentSetsEveryKey sets every key from its environment
|
|
// variable, with no config file at all: PORT for the port, and PIXA_
|
|
// plus the key in upper case, "." written as "_", for every other key.
|
|
func TestEnvironmentSetsEveryKey(t *testing.T) {
|
|
t.Setenv("PIXA_DEBUG", "true")
|
|
t.Setenv("PIXA_MAINTENANCE_MODE", "1")
|
|
t.Setenv("PORT", "9090")
|
|
t.Setenv("PIXA_STATE_DIR", "/srv/pixa-env")
|
|
t.Setenv("PIXA_SENTRY_DSN", "https://abc123@sentry.example.com/42")
|
|
t.Setenv("PIXA_DB_URL", "file:/srv/pixa-env/other.sqlite3")
|
|
t.Setenv("PIXA_METRICS_USERNAME", "metricsuser")
|
|
t.Setenv("PIXA_METRICS_PASSWORD", "metricspass")
|
|
t.Setenv("PIXA_SIGNING_KEY", validTestSigningKey)
|
|
t.Setenv("PIXA_ALLOWLIST_HOSTS", "s3.sneak.cloud,.example.com")
|
|
t.Setenv("PIXA_ALLOW_HTTP", "true")
|
|
t.Setenv("PIXA_UPSTREAM_CONNECTIONS_PER_HOST", "5")
|
|
t.Setenv("PIXA_CACHE_MAX_BYTES", "1024")
|
|
t.Setenv("PIXA_BLOCKED_NETWORKS", "203.0.113.0/24")
|
|
t.Setenv("PIXA_TRUSTED_PROXIES", "192.0.2.0/24")
|
|
t.Setenv("PIXA_ACCESS_CONTROL_ALLOW_ORIGIN", "https://app.example.com")
|
|
t.Setenv("PIXA_UPSTREAM_FETCH_TIMEOUT", "10s")
|
|
t.Setenv("PIXA_UPSTREAM_MAX_RESPONSE_SIZE", "1048576")
|
|
t.Setenv("PIXA_DOWNSTREAM_TIMEOUT", "2m")
|
|
|
|
c, err := newFromSmartConfig(nil)
|
|
if err != nil {
|
|
t.Fatalf("configuration from the environment alone should load: %v", err)
|
|
}
|
|
|
|
want := Config{
|
|
Debug: true,
|
|
MaintenanceMode: true,
|
|
Port: 9090,
|
|
StateDir: "/srv/pixa-env",
|
|
SentryDSN: "https://abc123@sentry.example.com/42",
|
|
DBURL: "file:/srv/pixa-env/other.sqlite3",
|
|
MetricsUsername: "metricsuser",
|
|
MetricsPassword: "metricspass",
|
|
SigningKey: validTestSigningKey,
|
|
AllowlistHosts: []string{testHostS3, ".example.com"},
|
|
AllowHTTP: true,
|
|
UpstreamConnectionsPerHost: 5,
|
|
CacheMaxBytes: 1024,
|
|
cacheMaxBytesExplicit: true,
|
|
BlockedNetworks: []netip.Prefix{netip.MustParsePrefix("203.0.113.0/24")},
|
|
TrustedProxies: []netip.Prefix{netip.MustParsePrefix("192.0.2.0/24")},
|
|
AccessControlAllowOrigin: "https://app.example.com",
|
|
UpstreamFetchTimeout: 10 * time.Second,
|
|
UpstreamMaxResponseSize: 1048576,
|
|
DownstreamTimeout: 2 * time.Minute,
|
|
}
|
|
|
|
if !reflect.DeepEqual(*c, want) {
|
|
t.Errorf("config from the environment =\n%+v\nwant\n%+v", *c, want)
|
|
}
|
|
}
|
|
|
|
// TestUnknownPixaVariableAbortsStartup checks that a PIXA_ variable that
|
|
// is not a setting's variable, such as a misspelled one, aborts startup
|
|
// naming it, as an unknown config key does, instead of being ignored.
|
|
func TestUnknownPixaVariableAbortsStartup(t *testing.T) {
|
|
t.Setenv("PIXA_TRUSTED_PROXY", "192.0.2.0/24")
|
|
t.Setenv("PIXA_SIGNINGKEY", validTestSigningKey)
|
|
|
|
err := validateKnownEnvVars()
|
|
wantStartupError(t, err, "PIXA_TRUSTED_PROXY", "PIXA_SIGNINGKEY")
|
|
}
|
|
|
|
// TestPixaPortAbortsStartupPointingToPort checks that PIXA_PORT aborts
|
|
// startup with a message saying to use PORT, which sets the port.
|
|
func TestPixaPortAbortsStartupPointingToPort(t *testing.T) {
|
|
t.Setenv("PIXA_PORT", "9090")
|
|
|
|
err := validateKnownEnvVars()
|
|
wantStartupError(t, err, "PIXA_PORT", "use PORT")
|
|
}
|
|
|
|
// TestSettingVariablesAndConfigPathAreAccepted checks that every
|
|
// setting's variable and PIXA_CONFIG_PATH pass the check for unknown
|
|
// PIXA_ variables. TestEnvironmentSetsEveryKey pins the names in the list.
|
|
func TestSettingVariablesAndConfigPathAreAccepted(t *testing.T) {
|
|
// A config file's env section loaded by another test can leave a
|
|
// PIXA_ variable set for the whole process, so every one is unset
|
|
// here first; t.Setenv restores each when the test ends.
|
|
for _, entry := range os.Environ() {
|
|
name, _, _ := strings.Cut(entry, "=")
|
|
if !strings.HasPrefix(name, "PIXA_") {
|
|
continue
|
|
}
|
|
|
|
t.Setenv(name, "")
|
|
|
|
err := os.Unsetenv(name)
|
|
if err != nil {
|
|
t.Fatalf("failed to unset %s: %v", name, err)
|
|
}
|
|
}
|
|
|
|
t.Setenv("PIXA_CONFIG_PATH", "/etc/pixa/config.yml")
|
|
|
|
for _, name := range envVarNames() {
|
|
t.Setenv(name, "")
|
|
}
|
|
|
|
err := validateKnownEnvVars()
|
|
if err != nil {
|
|
t.Fatalf("PIXA_CONFIG_PATH and every setting's variable "+
|
|
"must be accepted: %v", err)
|
|
}
|
|
}
|
|
|
|
// configFromNew writes yamlContent to a temporary config file, points
|
|
// PIXA_CONFIG_PATH at it, and runs New, as the server does at startup.
|
|
// The state directory is a temporary one and the disk cache is off, so
|
|
// New succeeds unless something in the test is wrong.
|
|
func configFromNew(t *testing.T, yamlContent string) (*Config, error) {
|
|
t.Helper()
|
|
|
|
tmpDir := t.TempDir()
|
|
configPath := filepath.Join(tmpDir, "config.yml")
|
|
|
|
err := os.WriteFile(configPath, []byte(yamlContent), 0o600)
|
|
if err != nil {
|
|
t.Fatalf("failed to write test config: %v", err)
|
|
}
|
|
|
|
t.Setenv("PIXA_CONFIG_PATH", configPath)
|
|
t.Setenv("PIXA_STATE_DIR", filepath.Join(tmpDir, "state"))
|
|
t.Setenv("PIXA_CACHE_MAX_BYTES", "0")
|
|
|
|
testLogger, err := logger.New(nil, logger.Params{Globals: &globals.Globals{}})
|
|
if err != nil {
|
|
t.Fatalf("failed to create logger: %v", err)
|
|
}
|
|
|
|
return New(nil, Params{Logger: testLogger})
|
|
}
|
|
|
|
// TestUnknownPixaVariableAbortsNew checks that New, which the server
|
|
// calls at startup, aborts on a misspelled PIXA_ variable.
|
|
func TestUnknownPixaVariableAbortsNew(t *testing.T) {
|
|
t.Setenv("PIXA_TRUSTED_PROXY", "192.0.2.0/24")
|
|
|
|
_, err := configFromNew(t, signingKeyLine)
|
|
wantStartupError(t, err, "PIXA_TRUSTED_PROXY")
|
|
}
|
|
|
|
// TestUnknownPixaVariableInEnvSectionAbortsNew checks that New aborts
|
|
// on a misspelled PIXA_ name in the config file's env section, which
|
|
// loading the file sets as an environment variable.
|
|
func TestUnknownPixaVariableInEnvSectionAbortsNew(t *testing.T) {
|
|
// The variable must be absent until the file loads. t.Setenv makes
|
|
// sure the one the file sets is removed when the test ends.
|
|
t.Setenv("PIXA_TRUSTED_PROXY", "")
|
|
|
|
err := os.Unsetenv("PIXA_TRUSTED_PROXY")
|
|
if err != nil {
|
|
t.Fatalf("failed to unset PIXA_TRUSTED_PROXY: %v", err)
|
|
}
|
|
|
|
_, err = configFromNew(t, signingKeyLine+
|
|
"env:\n PIXA_TRUSTED_PROXY: 192.0.2.0/24\n")
|
|
wantStartupError(t, err, "PIXA_TRUSTED_PROXY")
|
|
}
|
|
|
|
// TestPortFromEnvironmentOverridesConfigFile checks that PORT wins over
|
|
// the port in the config file.
|
|
func TestPortFromEnvironmentOverridesConfigFile(t *testing.T) {
|
|
t.Setenv("PORT", "9090")
|
|
|
|
c, err := configFromYAML(t, signingKeyLine+"port: 8080\n")
|
|
if err != nil {
|
|
t.Fatalf("PORT=9090 with port 8080 in the file should load: %v", err)
|
|
}
|
|
|
|
if c.Port != 9090 {
|
|
t.Errorf("Port = %d, want 9090 from PORT, not 8080 from the file", c.Port)
|
|
}
|
|
}
|
|
|
|
// TestInvalidPortFromEnvironmentAbortsStartup checks that a PORT that is
|
|
// not a number, or is outside the port range, aborts startup naming PORT
|
|
// and the value, even though the file's port is valid.
|
|
func TestInvalidPortFromEnvironmentAbortsStartup(t *testing.T) {
|
|
t.Setenv("PORT", "banana")
|
|
|
|
_, err := configFromYAML(t, signingKeyLine+"port: 8080\n")
|
|
wantStartupError(t, err, "PORT", "banana")
|
|
|
|
t.Setenv("PORT", "70000")
|
|
|
|
_, err = configFromYAML(t, signingKeyLine+"port: 8080\n")
|
|
wantStartupError(t, err, "PORT", "70000")
|
|
}
|
|
|
|
// TestListFromEnvironmentReplacesConfigFileList checks that a list
|
|
// variable replaces the file's list, split on commas with the spaces
|
|
// around each entry trimmed.
|
|
func TestListFromEnvironmentReplacesConfigFileList(t *testing.T) {
|
|
t.Setenv("PIXA_ALLOWLIST_HOSTS", " cdn.example.com , .example.org ")
|
|
|
|
c, err := configFromYAML(t, signingKeyLine+
|
|
"allowlist_hosts:\n - s3.sneak.cloud\n - sneak.berlin\n")
|
|
if err != nil {
|
|
t.Fatalf("PIXA_ALLOWLIST_HOSTS should load: %v", err)
|
|
}
|
|
|
|
want := []string{"cdn.example.com", ".example.org"}
|
|
if !slices.Equal(c.AllowlistHosts, want) {
|
|
t.Errorf("AllowlistHosts = %v, want %v from PIXA_ALLOWLIST_HOSTS",
|
|
c.AllowlistHosts, want)
|
|
}
|
|
}
|
|
|
|
// TestInvalidBlockedNetworksFromEnvironmentAbortsStartup checks that an
|
|
// invalid CIDR, or an empty entry, in PIXA_BLOCKED_NETWORKS aborts
|
|
// startup naming the variable, as the same list in the file does.
|
|
func TestInvalidBlockedNetworksFromEnvironmentAbortsStartup(t *testing.T) {
|
|
t.Setenv("PIXA_BLOCKED_NETWORKS", "203.0.113.0/24,not-a-cidr")
|
|
|
|
_, err := configFromYAML(t, signingKeyLine)
|
|
wantStartupError(t, err, "PIXA_BLOCKED_NETWORKS", "not-a-cidr")
|
|
|
|
t.Setenv("PIXA_BLOCKED_NETWORKS", "203.0.113.0/24,,198.51.100.0/24")
|
|
|
|
_, err = configFromYAML(t, signingKeyLine)
|
|
wantStartupError(t, err, "PIXA_BLOCKED_NETWORKS")
|
|
}
|
|
|
|
// TestInvalidDebugFromEnvironmentAbortsStartup checks that a PIXA_DEBUG
|
|
// that strconv.ParseBool rejects aborts startup instead of defaulting.
|
|
func TestInvalidDebugFromEnvironmentAbortsStartup(t *testing.T) {
|
|
t.Setenv("PIXA_DEBUG", "maybe")
|
|
|
|
_, err := configFromYAML(t, signingKeyLine)
|
|
wantStartupError(t, err, "PIXA_DEBUG", "maybe")
|
|
}
|
|
|
|
// TestInvalidOriginTimeoutOrSizeFromEnvironmentAbortsStartup checks that
|
|
// an invalid CORS origin, timeout or response size limit in its variable
|
|
// aborts startup naming the variable and the value.
|
|
func TestInvalidOriginTimeoutOrSizeFromEnvironmentAbortsStartup(t *testing.T) {
|
|
cases := []struct {
|
|
variable string
|
|
value string
|
|
}{
|
|
{"PIXA_ACCESS_CONTROL_ALLOW_ORIGIN", "example.com"},
|
|
{"PIXA_UPSTREAM_FETCH_TIMEOUT", "soon"},
|
|
{"PIXA_UPSTREAM_MAX_RESPONSE_SIZE", "50MB"},
|
|
{"PIXA_DOWNSTREAM_TIMEOUT", "0s"},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.variable, func(t *testing.T) {
|
|
t.Setenv(tc.variable, tc.value)
|
|
|
|
_, err := configFromYAML(t, signingKeyLine)
|
|
wantStartupError(t, err, tc.variable, tc.value)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestConfigFileAloneBehavesAsBefore checks that with no variables set
|
|
// (TestMain unsets them) the config file's values are used and omitted
|
|
// keys take their defaults.
|
|
func TestConfigFileAloneBehavesAsBefore(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
c, err := configFromYAML(t, signingKeyLine+"port: 9191\n")
|
|
if err != nil {
|
|
t.Fatalf("config file should load: %v", err)
|
|
}
|
|
|
|
if c.Port != 9191 {
|
|
t.Errorf("Port = %d, want 9191 from the file", c.Port)
|
|
}
|
|
|
|
if c.StateDir != DefaultStateDir {
|
|
t.Errorf("StateDir = %q, want default %q", c.StateDir, DefaultStateDir)
|
|
}
|
|
|
|
if !slices.Equal(c.TrustedProxies, defaultTrustedProxies()) {
|
|
t.Errorf("TrustedProxies = %v, want default %v",
|
|
c.TrustedProxies, defaultTrustedProxies())
|
|
}
|
|
}
|
|
|
|
// TestEmptyTrustedProxiesFromEnvironmentTrustsNoOne checks that an empty
|
|
// PIXA_TRUSTED_PROXIES is an empty list, like [] in the file: it trusts
|
|
// no proxy instead of taking the default ranges.
|
|
func TestEmptyTrustedProxiesFromEnvironmentTrustsNoOne(t *testing.T) {
|
|
t.Setenv("PIXA_TRUSTED_PROXIES", "")
|
|
|
|
c, err := configFromYAML(t, signingKeyLine)
|
|
if err != nil {
|
|
t.Fatalf("empty PIXA_TRUSTED_PROXIES should load: %v", err)
|
|
}
|
|
|
|
if len(c.TrustedProxies) != 0 {
|
|
t.Errorf("TrustedProxies = %v, want none", c.TrustedProxies)
|
|
}
|
|
}
|
|
|
|
// TestEmptyVariableDoesNotFallBackToConfigFile checks that a variable
|
|
// that is present but empty is a set value: an empty PIXA_STATE_DIR
|
|
// aborts startup like state_dir: "" in the file, instead of falling
|
|
// through to the file's state_dir.
|
|
func TestEmptyVariableDoesNotFallBackToConfigFile(t *testing.T) {
|
|
t.Setenv("PIXA_STATE_DIR", "")
|
|
|
|
_, err := configFromYAML(t, signingKeyLine+"state_dir: /srv/pixa-file\n")
|
|
wantStartupError(t, err, "PIXA_STATE_DIR")
|
|
}
|
|
|
|
// TestMissingSigningKeyNamesItsVariable checks that with no config file
|
|
// and no PIXA_SIGNING_KEY, startup aborts naming the variable, which is
|
|
// how a container started without it reports the problem.
|
|
func TestMissingSigningKeyNamesItsVariable(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
_, err := newFromSmartConfig(nil)
|
|
wantStartupError(t, err, "PIXA_SIGNING_KEY")
|
|
}
|
|
|
|
// TestSecretsFromEnvironmentAreNotPrinted checks that errors about the
|
|
// signing key and the metrics password name their variables but never
|
|
// print their values.
|
|
func TestSecretsFromEnvironmentAreNotPrinted(t *testing.T) {
|
|
t.Setenv("PIXA_SIGNING_KEY", "short-signing-secret")
|
|
|
|
_, err := newFromSmartConfig(nil)
|
|
wantStartupError(t, err, "PIXA_SIGNING_KEY")
|
|
|
|
if strings.Contains(err.Error(), "short-signing-secret") {
|
|
t.Errorf("error %q prints the signing key", err.Error())
|
|
}
|
|
|
|
t.Setenv("PIXA_SIGNING_KEY", validTestSigningKey)
|
|
t.Setenv("PIXA_METRICS_PASSWORD", "metrics-password-secret")
|
|
|
|
_, err = newFromSmartConfig(nil)
|
|
wantStartupError(t, err, "PIXA_METRICS_PASSWORD")
|
|
|
|
if strings.Contains(err.Error(), "metrics-password-secret") {
|
|
t.Errorf("error %q prints the metrics password", err.Error())
|
|
}
|
|
}
|