// Copyright The OpenTelemetry Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package envconfig // import "go.opentelemetry.io/otel/exporters/otlp/internal/envconfig" import ( "crypto/tls" "crypto/x509" "errors" "net/url" "testing" "time" "github.com/stretchr/testify/assert" ) const WeakKey = ` -----BEGIN EC PRIVATE KEY----- MHcCAQEEIEbrSPmnlSOXvVzxCyv+VR3a0HDeUTvOcqrdssZ2k4gFoAoGCCqGSM49 AwEHoUQDQgAEDMTfv75J315C3K9faptS9iythKOMEeV/Eep73nWX531YAkmmwBSB 2dXRD/brsgLnfG57WEpxZuY7dPRbxu33BA== -----END EC PRIVATE KEY----- ` const WeakCertificate = ` -----BEGIN CERTIFICATE----- MIIBjjCCATWgAwIBAgIUKQSMC66MUw+kPp954ZYOcyKAQDswCgYIKoZIzj0EAwIw EjEQMA4GA1UECgwHb3RlbC1nbzAeFw0yMjEwMTkwMDA5MTlaFw0yMzEwMTkwMDA5 MTlaMBIxEDAOBgNVBAoMB290ZWwtZ28wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNC AAQMxN+/vknfXkLcr19qm1L2LK2Eo4wR5X8R6nvedZfnfVgCSabAFIHZ1dEP9uuy Aud8bntYSnFm5jt09FvG7fcEo2kwZzAdBgNVHQ4EFgQUicGuhnTTkYLZwofXMNLK SHFeCWgwHwYDVR0jBBgwFoAUicGuhnTTkYLZwofXMNLKSHFeCWgwDwYDVR0TAQH/ BAUwAwEB/zAUBgNVHREEDTALgglsb2NhbGhvc3QwCgYIKoZIzj0EAwIDRwAwRAIg Lfma8FnnxeSOi6223AsFfYwsNZ2RderNsQrS0PjEHb0CIBkrWacqARUAu7uT4cGu jVcIxYQqhId5L8p/mAv2PWZS -----END CERTIFICATE----- ` type testOption struct { TestString string TestBool bool TestDuration time.Duration TestHeaders map[string]string TestURL *url.URL TestTLS *tls.Config } func TestEnvConfig(t *testing.T) { parsedURL, err := url.Parse("https://example.com") assert.NoError(t, err) options := []testOption{} for _, testcase := range []struct { name string reader EnvOptionsReader configs []ConfigFn expectedOptions []testOption }{ { name: "with no namespace and a matching key", reader: EnvOptionsReader{ GetEnv: func(n string) string { if n == "HELLO" { return "world" } return "" }, }, configs: []ConfigFn{ WithString("HELLO", func(v string) { options = append(options, testOption{TestString: v}) }), }, expectedOptions: []testOption{ { TestString: "world", }, }, }, { name: "with no namespace and a non-matching key", reader: EnvOptionsReader{ GetEnv: func(n string) string { if n == "HELLO" { return "world" } return "" }, }, configs: []ConfigFn{ WithString("HOLA", func(v string) { options = append(options, testOption{TestString: v}) }), }, expectedOptions: []testOption{}, }, { name: "with a namespace and a matching key", reader: EnvOptionsReader{ Namespace: "MY_NAMESPACE", GetEnv: func(n string) string { if n == "MY_NAMESPACE_HELLO" { return "world" } return "" }, }, configs: []ConfigFn{ WithString("HELLO", func(v string) { options = append(options, testOption{TestString: v}) }), }, expectedOptions: []testOption{ { TestString: "world", }, }, }, { name: "with no namespace and a non-matching key", reader: EnvOptionsReader{ Namespace: "MY_NAMESPACE", GetEnv: func(n string) string { if n == "HELLO" { return "world" } return "" }, }, configs: []ConfigFn{ WithString("HELLO", func(v string) { options = append(options, testOption{TestString: v}) }), }, expectedOptions: []testOption{}, }, { name: "with a bool config", reader: EnvOptionsReader{ GetEnv: func(n string) string { if n == "HELLO" { return "true" } else if n == "WORLD" { return "false" } return "" }, }, configs: []ConfigFn{ WithBool("HELLO", func(b bool) { options = append(options, testOption{TestBool: b}) }), WithBool("WORLD", func(b bool) { options = append(options, testOption{TestBool: b}) }), }, expectedOptions: []testOption{ { TestBool: true, }, { TestBool: false, }, }, }, { name: "with an invalid bool config", reader: EnvOptionsReader{ GetEnv: func(n string) string { if n == "HELLO" { return "world" } return "" }, }, configs: []ConfigFn{ WithBool("HELLO", func(b bool) { options = append(options, testOption{TestBool: b}) }), }, expectedOptions: []testOption{ { TestBool: false, }, }, }, { name: "with a duration config", reader: EnvOptionsReader{ GetEnv: func(n string) string { if n == "HELLO" { return "60" } return "" }, }, configs: []ConfigFn{ WithDuration("HELLO", func(v time.Duration) { options = append(options, testOption{TestDuration: v}) }), }, expectedOptions: []testOption{ { TestDuration: 60_000_000, // 60 milliseconds }, }, }, { name: "with an invalid duration config", reader: EnvOptionsReader{ GetEnv: func(n string) string { if n == "HELLO" { return "world" } return "" }, }, configs: []ConfigFn{ WithDuration("HELLO", func(v time.Duration) { options = append(options, testOption{TestDuration: v}) }), }, expectedOptions: []testOption{}, }, { name: "with headers", reader: EnvOptionsReader{ GetEnv: func(n string) string { if n == "HELLO" { return "userId=42,userName=alice" } return "" }, }, configs: []ConfigFn{ WithHeaders("HELLO", func(v map[string]string) { options = append(options, testOption{TestHeaders: v}) }), }, expectedOptions: []testOption{ { TestHeaders: map[string]string{ "userId": "42", "userName": "alice", }, }, }, }, { name: "with invalid headers", reader: EnvOptionsReader{ GetEnv: func(n string) string { if n == "HELLO" { return "world" } return "" }, }, configs: []ConfigFn{ WithHeaders("HELLO", func(v map[string]string) { options = append(options, testOption{TestHeaders: v}) }), }, expectedOptions: []testOption{ { TestHeaders: map[string]string{}, }, }, }, { name: "with URL", reader: EnvOptionsReader{ GetEnv: func(n string) string { if n == "HELLO" { return "https://example.com" } return "" }, }, configs: []ConfigFn{ WithURL("HELLO", func(v *url.URL) { options = append(options, testOption{TestURL: v}) }), }, expectedOptions: []testOption{ { TestURL: parsedURL, }, }, }, { name: "with invalid URL", reader: EnvOptionsReader{ GetEnv: func(n string) string { if n == "HELLO" { return "i nvalid://url" } return "" }, }, configs: []ConfigFn{ WithURL("HELLO", func(v *url.URL) { options = append(options, testOption{TestURL: v}) }), }, expectedOptions: []testOption{}, }, } { t.Run(testcase.name, func(t *testing.T) { testcase.reader.Apply(testcase.configs...) assert.Equal(t, testcase.expectedOptions, options) options = []testOption{} }) } } func TestWithTLSConfig(t *testing.T) { pool, err := createCertPool([]byte(WeakCertificate)) assert.NoError(t, err) reader := EnvOptionsReader{ GetEnv: func(n string) string { if n == "CERTIFICATE" { return "/path/cert.pem" } return "" }, ReadFile: func(p string) ([]byte, error) { if p == "/path/cert.pem" { return []byte(WeakCertificate), nil } return []byte{}, nil }, } var option testOption reader.Apply( WithCertPool("CERTIFICATE", func(cp *x509.CertPool) { option = testOption{TestTLS: &tls.Config{RootCAs: cp}} }), ) // nolint:staticcheck // ignoring tlsCert.RootCAs.Subjects is deprecated ERR because cert does not come from SystemCertPool. assert.Equal(t, pool.Subjects(), option.TestTLS.RootCAs.Subjects()) } func TestWithClientCert(t *testing.T) { cert, err := tls.X509KeyPair([]byte(WeakCertificate), []byte(WeakKey)) assert.NoError(t, err) reader := EnvOptionsReader{ GetEnv: func(n string) string { switch n { case "CLIENT_CERTIFICATE": return "/path/tls.crt" case "CLIENT_KEY": return "/path/tls.key" } return "" }, ReadFile: func(n string) ([]byte, error) { switch n { case "/path/tls.crt": return []byte(WeakCertificate), nil case "/path/tls.key": return []byte(WeakKey), nil } return []byte{}, nil }, } var option testOption reader.Apply( WithClientCert("CLIENT_CERTIFICATE", "CLIENT_KEY", func(c tls.Certificate) { option = testOption{TestTLS: &tls.Config{Certificates: []tls.Certificate{c}}} }), ) assert.Equal(t, cert, option.TestTLS.Certificates[0]) reader.ReadFile = func(s string) ([]byte, error) { return nil, errors.New("oops") } option.TestTLS = nil reader.Apply( WithClientCert("CLIENT_CERTIFICATE", "CLIENT_KEY", func(c tls.Certificate) { option = testOption{TestTLS: &tls.Config{Certificates: []tls.Certificate{c}}} }), ) assert.Nil(t, option.TestTLS) reader.GetEnv = func(s string) string { return "" } option.TestTLS = nil reader.Apply( WithClientCert("CLIENT_CERTIFICATE", "CLIENT_KEY", func(c tls.Certificate) { option = testOption{TestTLS: &tls.Config{Certificates: []tls.Certificate{c}}} }), ) assert.Nil(t, option.TestTLS) } func TestStringToHeader(t *testing.T) { tests := []struct { name string value string want map[string]string }{ { name: "simple test", value: "userId=alice", want: map[string]string{"userId": "alice"}, }, { name: "simple test with spaces", value: " userId = alice ", want: map[string]string{"userId": "alice"}, }, { name: "multiples headers encoded", value: "userId=alice,serverNode=DF%3A28,isProduction=false", want: map[string]string{ "userId": "alice", "serverNode": "DF:28", "isProduction": "false", }, }, { name: "invalid headers format", value: "userId:alice", want: map[string]string{}, }, { name: "invalid key", value: "%XX=missing,userId=alice", want: map[string]string{ "userId": "alice", }, }, { name: "invalid value", value: "missing=%XX,userId=alice", want: map[string]string{ "userId": "alice", }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assert.Equal(t, tt.want, stringToHeader(tt.value)) }) } }