package config import ( "bytes" "fmt" "net/http" "net/http/httptest" "os" "path/filepath" "testing" "time" "github.com/stretchr/testify/require" ) func TestEnvironmentSubstitution(t *testing.T) { tests := []struct { name string setEnv func(*testing.T) contents string expected string wantErr bool errSubstring string }{ { name: "Legacy with ${} and without {}", setEnv: func(t *testing.T) { t.Setenv("TEST_ENV1", "VALUE1") t.Setenv("TEST_ENV2", "VALUE2") }, contents: "A string with ${TEST_ENV1}, $TEST_ENV2 and $TEST_ENV1 as repeated", expected: "A string with VALUE1, VALUE2 and VALUE1 as repeated", }, { name: "Env not set", contents: "Env variable ${NOT_SET} will be empty", expected: "Env variable ${NOT_SET} will be empty", }, { name: "Env not set, fallback to default", contents: "Env variable ${THIS_IS_ABSENT:-Fallback}", expected: "Env variable Fallback", }, { name: "No fallback", setEnv: func(t *testing.T) { t.Setenv("MY_ENV1", "VALUE1") }, contents: "Env variable ${MY_ENV1:-Fallback}", expected: "Env variable VALUE1", }, { name: "Mix and match", setEnv: func(t *testing.T) { t.Setenv("MY_VAR", "VALUE") t.Setenv("MY_VAR2", "VALUE2") }, contents: "Env var ${MY_VAR} is set, with $MY_VAR syntax and default on this ${MY_VAR1:-Substituted}, no default on this ${MY_VAR2:-NoDefault}", expected: "Env var VALUE is set, with VALUE syntax and default on this Substituted, no default on this VALUE2", }, { name: "empty but set", setEnv: func(t *testing.T) { t.Setenv("EMPTY", "") }, contents: "Contains ${EMPTY} nothing", expected: "Contains nothing", }, { name: "Default has special chars", contents: `Not recommended but supported ${MY_VAR:-Default with special chars Supported#$\"}`, expected: `Not recommended but supported Default with special chars Supported#$\"`, // values are escaped }, { name: "unset error", contents: "Contains ${THIS_IS_NOT_SET?unset-error}", wantErr: true, errSubstring: "unset-error", }, { name: "env empty error", setEnv: func(t *testing.T) { t.Setenv("ENV_EMPTY", "") }, contents: "Contains ${ENV_EMPTY:?empty-error}", wantErr: true, errSubstring: "empty-error", }, { name: "Fallback as env variable", setEnv: func(t *testing.T) { t.Setenv("FALLBACK", "my-fallback") }, contents: "Should output ${NOT_SET:-${FALLBACK}}", expected: "Should output my-fallback", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if tt.setEnv != nil { tt.setEnv(t) } actual, err := substituteEnvironment([]byte(tt.contents), false) if tt.wantErr { require.ErrorContains(t, err, tt.errSubstring) return } require.EqualValues(t, tt.expected, string(actual)) }) } } func TestEnvironmentSubstitutionOldBehavior(t *testing.T) { tests := []struct { name string contents string expected string }{ { name: "not defined no brackets", contents: `my-da$tabase`, expected: `my-da$tabase`, }, { name: "not defined brackets", contents: `my-da${ta}base`, expected: `my-da${ta}base`, }, { name: "not defined no brackets double dollar", contents: `my-da$$tabase`, expected: `my-da$$tabase`, }, { name: "not defined no brackets backslash", contents: `my-da\$tabase`, expected: `my-da\$tabase`, }, { name: "not defined brackets backslash", contents: `my-da\${ta}base`, expected: `my-da\${ta}base`, }, { name: "no brackets and suffix", contents: `my-da$VARbase`, expected: `my-da$VARbase`, }, { name: "no brackets", contents: `my-da$VAR`, expected: `my-dafoobar`, }, { name: "brackets", contents: `my-da${VAR}base`, expected: `my-dafoobarbase`, }, { name: "no brackets double dollar", contents: `my-da$$VAR`, expected: `my-da$foobar`, }, { name: "brackets double dollar", contents: `my-da$${VAR}`, expected: `my-da$foobar`, }, { name: "no brackets backslash", contents: `my-da\$VAR`, expected: `my-da\foobar`, }, { name: "brackets backslash", contents: `my-da\${VAR}base`, expected: `my-da\foobarbase`, }, { name: "fallback", contents: `my-da${ta:-omg}base`, expected: `my-daomgbase`, }, { name: "fallback env", contents: `my-da${ta:-${FALLBACK}}base`, expected: `my-dadefaultbase`, }, { name: "regex substitution", contents: `${1}`, expected: `${1}`, }, { name: "empty but set", contents: "Contains ${EMPTY} nothing", expected: "Contains nothing", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Setenv("VAR", "foobar") t.Setenv("FALLBACK", "default") t.Setenv("EMPTY", "") actual, err := substituteEnvironment([]byte(tt.contents), true) require.NoError(t, err) require.EqualValues(t, tt.expected, string(actual)) }) } } func TestEnvironmentSubstitutionNewBehavior(t *testing.T) { tests := []struct { name string contents string expected string }{ { name: "not defined no brackets", contents: `my-da$tabase`, expected: `my-da$tabase`, }, { name: "not defined brackets", contents: `my-da${ta}base`, expected: `my-da${ta}base`, }, { name: "not defined no brackets double dollar", contents: `my-da$$tabase`, expected: `my-da$tabase`, }, { name: "not defined no brackets backslash", contents: `my-da\$tabase`, expected: `my-da\$tabase`, }, { name: "not defined brackets backslash", contents: `my-da\${ta}base`, expected: `my-da\${ta}base`, }, { name: "no brackets and suffix", contents: `my-da$VARbase`, expected: `my-da$VARbase`, }, { name: "no brackets", contents: `my-da$VAR`, expected: `my-dafoobar`, }, { name: "brackets", contents: `my-da${VAR}base`, expected: `my-dafoobarbase`, }, { name: "no brackets double dollar", contents: `my-da$$VAR`, expected: `my-da$VAR`, }, { name: "brackets double dollar", contents: `my-da$${VAR}`, expected: `my-da${VAR}`, }, { name: "no brackets backslash", contents: `my-da\$VAR`, expected: `my-da\foobar`, }, { name: "brackets backslash", contents: `my-da\${VAR}base`, expected: `my-da\foobarbase`, }, { name: "fallback", contents: `my-da${ta:-omg}base`, expected: `my-daomgbase`, }, { name: "fallback env", contents: `my-da${ta:-${FALLBACK}}base`, expected: `my-dadefaultbase`, }, { name: "regex substitution", contents: `${1}`, expected: `${1}`, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Setenv("VAR", "foobar") t.Setenv("FALLBACK", "default") actual, err := substituteEnvironment([]byte(tt.contents), false) require.NoError(t, err) require.EqualValues(t, tt.expected, string(actual)) }) } } func TestParseConfig(t *testing.T) { tests := []struct { name string setEnv func(*testing.T) contents string expected string errmsg string }{ { name: "empty var name", contents: ` # Environment variables can be used anywhere in this config file, simply surround # them with ${}. For strings the variable must be within quotes (ie, "${STR_VAR}"), # for numbers and booleans they should be plain (ie, ${INT_VAR}, ${BOOL_VAR})Should output ${NOT_SET:-${FALLBACK}} `, expected: "\n\n\n\n", }, { name: "comment in command (issue #13643)", contents: ` [[inputs.exec]] commands = ["echo \"abc#def\""] `, expected: ` [[inputs.exec]] commands = ["echo \"abc#def\""] `, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if tt.setEnv != nil { tt.setEnv(t) } tbl, err := parseConfig([]byte(tt.contents)) if tt.errmsg != "" { require.ErrorContains(t, err, tt.errmsg) return } require.NoError(t, err) if len(tt.expected) > 0 { require.EqualValues(t, tt.expected, string(tbl.Data)) } }) } } func TestRemoveComments(t *testing.T) { // Read expectation expected, err := os.ReadFile(filepath.Join("testdata", "envvar_comments_expected.toml")) require.NoError(t, err) // Read the file and remove the comments buf, err := os.ReadFile(filepath.Join("testdata", "envvar_comments.toml")) require.NoError(t, err) removed, err := removeComments(buf) require.NoError(t, err) lines := bytes.Split(removed, []byte{'\n'}) for i, line := range lines { lines[i] = bytes.TrimRight(line, " \t") } actual := bytes.Join(lines, []byte{'\n'}) // Do the comparison require.Equal(t, string(expected), string(actual)) } func TestURLRetries3Fails(t *testing.T) { httpLoadConfigRetryInterval = 0 * time.Second responseCounter := 0 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNotFound) responseCounter++ })) defer ts.Close() expected := fmt.Sprintf("loading config file %s failed: failed to fetch HTTP config: 404 Not Found", ts.URL) c := NewConfig() err := c.LoadConfig(ts.URL) require.Error(t, err) require.Equal(t, expected, err.Error()) require.Equal(t, 4, responseCounter) } func TestURLRetries3FailsThenPasses(t *testing.T) { httpLoadConfigRetryInterval = 0 * time.Second responseCounter := 0 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { if responseCounter <= 2 { w.WriteHeader(http.StatusNotFound) } else { w.WriteHeader(http.StatusOK) } responseCounter++ })) defer ts.Close() c := NewConfig() require.NoError(t, c.LoadConfig(ts.URL)) require.Equal(t, 4, responseCounter) }