1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 package envconfig
19
20 import (
21 "crypto/tls"
22 "crypto/x509"
23 "errors"
24 "net/url"
25 "testing"
26 "time"
27
28 "github.com/stretchr/testify/assert"
29 )
30
31 const WeakKey = `
32 -----BEGIN EC PRIVATE KEY-----
33 MHcCAQEEIEbrSPmnlSOXvVzxCyv+VR3a0HDeUTvOcqrdssZ2k4gFoAoGCCqGSM49
34 AwEHoUQDQgAEDMTfv75J315C3K9faptS9iythKOMEeV/Eep73nWX531YAkmmwBSB
35 2dXRD/brsgLnfG57WEpxZuY7dPRbxu33BA==
36 -----END EC PRIVATE KEY-----
37 `
38
39 const WeakCertificate = `
40 -----BEGIN CERTIFICATE-----
41 MIIBjjCCATWgAwIBAgIUKQSMC66MUw+kPp954ZYOcyKAQDswCgYIKoZIzj0EAwIw
42 EjEQMA4GA1UECgwHb3RlbC1nbzAeFw0yMjEwMTkwMDA5MTlaFw0yMzEwMTkwMDA5
43 MTlaMBIxEDAOBgNVBAoMB290ZWwtZ28wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNC
44 AAQMxN+/vknfXkLcr19qm1L2LK2Eo4wR5X8R6nvedZfnfVgCSabAFIHZ1dEP9uuy
45 Aud8bntYSnFm5jt09FvG7fcEo2kwZzAdBgNVHQ4EFgQUicGuhnTTkYLZwofXMNLK
46 SHFeCWgwHwYDVR0jBBgwFoAUicGuhnTTkYLZwofXMNLKSHFeCWgwDwYDVR0TAQH/
47 BAUwAwEB/zAUBgNVHREEDTALgglsb2NhbGhvc3QwCgYIKoZIzj0EAwIDRwAwRAIg
48 Lfma8FnnxeSOi6223AsFfYwsNZ2RderNsQrS0PjEHb0CIBkrWacqARUAu7uT4cGu
49 jVcIxYQqhId5L8p/mAv2PWZS
50 -----END CERTIFICATE-----
51 `
52
53 type testOption struct {
54 TestString string
55 TestBool bool
56 TestDuration time.Duration
57 TestHeaders map[string]string
58 TestURL *url.URL
59 TestTLS *tls.Config
60 }
61
62 func TestEnvConfig(t *testing.T) {
63 parsedURL, err := url.Parse("https://example.com")
64 assert.NoError(t, err)
65
66 options := []testOption{}
67 for _, testcase := range []struct {
68 name string
69 reader EnvOptionsReader
70 configs []ConfigFn
71 expectedOptions []testOption
72 }{
73 {
74 name: "with no namespace and a matching key",
75 reader: EnvOptionsReader{
76 GetEnv: func(n string) string {
77 if n == "HELLO" {
78 return "world"
79 }
80 return ""
81 },
82 },
83 configs: []ConfigFn{
84 WithString("HELLO", func(v string) {
85 options = append(options, testOption{TestString: v})
86 }),
87 },
88 expectedOptions: []testOption{
89 {
90 TestString: "world",
91 },
92 },
93 },
94 {
95 name: "with no namespace and a non-matching key",
96 reader: EnvOptionsReader{
97 GetEnv: func(n string) string {
98 if n == "HELLO" {
99 return "world"
100 }
101 return ""
102 },
103 },
104 configs: []ConfigFn{
105 WithString("HOLA", func(v string) {
106 options = append(options, testOption{TestString: v})
107 }),
108 },
109 expectedOptions: []testOption{},
110 },
111 {
112 name: "with a namespace and a matching key",
113 reader: EnvOptionsReader{
114 Namespace: "MY_NAMESPACE",
115 GetEnv: func(n string) string {
116 if n == "MY_NAMESPACE_HELLO" {
117 return "world"
118 }
119 return ""
120 },
121 },
122 configs: []ConfigFn{
123 WithString("HELLO", func(v string) {
124 options = append(options, testOption{TestString: v})
125 }),
126 },
127 expectedOptions: []testOption{
128 {
129 TestString: "world",
130 },
131 },
132 },
133 {
134 name: "with no namespace and a non-matching key",
135 reader: EnvOptionsReader{
136 Namespace: "MY_NAMESPACE",
137 GetEnv: func(n string) string {
138 if n == "HELLO" {
139 return "world"
140 }
141 return ""
142 },
143 },
144 configs: []ConfigFn{
145 WithString("HELLO", func(v string) {
146 options = append(options, testOption{TestString: v})
147 }),
148 },
149 expectedOptions: []testOption{},
150 },
151 {
152 name: "with a bool config",
153 reader: EnvOptionsReader{
154 GetEnv: func(n string) string {
155 if n == "HELLO" {
156 return "true"
157 } else if n == "WORLD" {
158 return "false"
159 }
160 return ""
161 },
162 },
163 configs: []ConfigFn{
164 WithBool("HELLO", func(b bool) {
165 options = append(options, testOption{TestBool: b})
166 }),
167 WithBool("WORLD", func(b bool) {
168 options = append(options, testOption{TestBool: b})
169 }),
170 },
171 expectedOptions: []testOption{
172 {
173 TestBool: true,
174 },
175 {
176 TestBool: false,
177 },
178 },
179 },
180 {
181 name: "with an invalid bool config",
182 reader: EnvOptionsReader{
183 GetEnv: func(n string) string {
184 if n == "HELLO" {
185 return "world"
186 }
187 return ""
188 },
189 },
190 configs: []ConfigFn{
191 WithBool("HELLO", func(b bool) {
192 options = append(options, testOption{TestBool: b})
193 }),
194 },
195 expectedOptions: []testOption{
196 {
197 TestBool: false,
198 },
199 },
200 },
201 {
202 name: "with a duration config",
203 reader: EnvOptionsReader{
204 GetEnv: func(n string) string {
205 if n == "HELLO" {
206 return "60"
207 }
208 return ""
209 },
210 },
211 configs: []ConfigFn{
212 WithDuration("HELLO", func(v time.Duration) {
213 options = append(options, testOption{TestDuration: v})
214 }),
215 },
216 expectedOptions: []testOption{
217 {
218 TestDuration: 60_000_000,
219 },
220 },
221 },
222 {
223 name: "with an invalid duration config",
224 reader: EnvOptionsReader{
225 GetEnv: func(n string) string {
226 if n == "HELLO" {
227 return "world"
228 }
229 return ""
230 },
231 },
232 configs: []ConfigFn{
233 WithDuration("HELLO", func(v time.Duration) {
234 options = append(options, testOption{TestDuration: v})
235 }),
236 },
237 expectedOptions: []testOption{},
238 },
239 {
240 name: "with headers",
241 reader: EnvOptionsReader{
242 GetEnv: func(n string) string {
243 if n == "HELLO" {
244 return "userId=42,userName=alice"
245 }
246 return ""
247 },
248 },
249 configs: []ConfigFn{
250 WithHeaders("HELLO", func(v map[string]string) {
251 options = append(options, testOption{TestHeaders: v})
252 }),
253 },
254 expectedOptions: []testOption{
255 {
256 TestHeaders: map[string]string{
257 "userId": "42",
258 "userName": "alice",
259 },
260 },
261 },
262 },
263 {
264 name: "with invalid headers",
265 reader: EnvOptionsReader{
266 GetEnv: func(n string) string {
267 if n == "HELLO" {
268 return "world"
269 }
270 return ""
271 },
272 },
273 configs: []ConfigFn{
274 WithHeaders("HELLO", func(v map[string]string) {
275 options = append(options, testOption{TestHeaders: v})
276 }),
277 },
278 expectedOptions: []testOption{
279 {
280 TestHeaders: map[string]string{},
281 },
282 },
283 },
284 {
285 name: "with URL",
286 reader: EnvOptionsReader{
287 GetEnv: func(n string) string {
288 if n == "HELLO" {
289 return "https://example.com"
290 }
291 return ""
292 },
293 },
294 configs: []ConfigFn{
295 WithURL("HELLO", func(v *url.URL) {
296 options = append(options, testOption{TestURL: v})
297 }),
298 },
299 expectedOptions: []testOption{
300 {
301 TestURL: parsedURL,
302 },
303 },
304 },
305 {
306 name: "with invalid URL",
307 reader: EnvOptionsReader{
308 GetEnv: func(n string) string {
309 if n == "HELLO" {
310 return "i nvalid://url"
311 }
312 return ""
313 },
314 },
315 configs: []ConfigFn{
316 WithURL("HELLO", func(v *url.URL) {
317 options = append(options, testOption{TestURL: v})
318 }),
319 },
320 expectedOptions: []testOption{},
321 },
322 } {
323 t.Run(testcase.name, func(t *testing.T) {
324 testcase.reader.Apply(testcase.configs...)
325 assert.Equal(t, testcase.expectedOptions, options)
326 options = []testOption{}
327 })
328 }
329 }
330
331 func TestWithTLSConfig(t *testing.T) {
332 pool, err := createCertPool([]byte(WeakCertificate))
333 assert.NoError(t, err)
334
335 reader := EnvOptionsReader{
336 GetEnv: func(n string) string {
337 if n == "CERTIFICATE" {
338 return "/path/cert.pem"
339 }
340 return ""
341 },
342 ReadFile: func(p string) ([]byte, error) {
343 if p == "/path/cert.pem" {
344 return []byte(WeakCertificate), nil
345 }
346 return []byte{}, nil
347 },
348 }
349
350 var option testOption
351 reader.Apply(
352 WithCertPool("CERTIFICATE", func(cp *x509.CertPool) {
353 option = testOption{TestTLS: &tls.Config{RootCAs: cp}}
354 }),
355 )
356
357
358 assert.Equal(t, pool.Subjects(), option.TestTLS.RootCAs.Subjects())
359 }
360
361 func TestWithClientCert(t *testing.T) {
362 cert, err := tls.X509KeyPair([]byte(WeakCertificate), []byte(WeakKey))
363 assert.NoError(t, err)
364
365 reader := EnvOptionsReader{
366 GetEnv: func(n string) string {
367 switch n {
368 case "CLIENT_CERTIFICATE":
369 return "/path/tls.crt"
370 case "CLIENT_KEY":
371 return "/path/tls.key"
372 }
373 return ""
374 },
375 ReadFile: func(n string) ([]byte, error) {
376 switch n {
377 case "/path/tls.crt":
378 return []byte(WeakCertificate), nil
379 case "/path/tls.key":
380 return []byte(WeakKey), nil
381 }
382 return []byte{}, nil
383 },
384 }
385
386 var option testOption
387 reader.Apply(
388 WithClientCert("CLIENT_CERTIFICATE", "CLIENT_KEY", func(c tls.Certificate) {
389 option = testOption{TestTLS: &tls.Config{Certificates: []tls.Certificate{c}}}
390 }),
391 )
392 assert.Equal(t, cert, option.TestTLS.Certificates[0])
393
394 reader.ReadFile = func(s string) ([]byte, error) { return nil, errors.New("oops") }
395 option.TestTLS = nil
396 reader.Apply(
397 WithClientCert("CLIENT_CERTIFICATE", "CLIENT_KEY", func(c tls.Certificate) {
398 option = testOption{TestTLS: &tls.Config{Certificates: []tls.Certificate{c}}}
399 }),
400 )
401 assert.Nil(t, option.TestTLS)
402
403 reader.GetEnv = func(s string) string { return "" }
404 option.TestTLS = nil
405 reader.Apply(
406 WithClientCert("CLIENT_CERTIFICATE", "CLIENT_KEY", func(c tls.Certificate) {
407 option = testOption{TestTLS: &tls.Config{Certificates: []tls.Certificate{c}}}
408 }),
409 )
410 assert.Nil(t, option.TestTLS)
411 }
412
413 func TestStringToHeader(t *testing.T) {
414 tests := []struct {
415 name string
416 value string
417 want map[string]string
418 }{
419 {
420 name: "simple test",
421 value: "userId=alice",
422 want: map[string]string{"userId": "alice"},
423 },
424 {
425 name: "simple test with spaces",
426 value: " userId = alice ",
427 want: map[string]string{"userId": "alice"},
428 },
429 {
430 name: "simple header conforms to RFC 3986 spec",
431 value: " userId = alice+test ",
432 want: map[string]string{"userId": "alice+test"},
433 },
434 {
435 name: "multiple headers encoded",
436 value: "userId=alice,serverNode=DF%3A28,isProduction=false",
437 want: map[string]string{
438 "userId": "alice",
439 "serverNode": "DF:28",
440 "isProduction": "false",
441 },
442 },
443 {
444 name: "multiple headers encoded per RFC 3986 spec",
445 value: "userId=alice+test,serverNode=DF%3A28,isProduction=false,namespace=localhost/test",
446 want: map[string]string{
447 "userId": "alice+test",
448 "serverNode": "DF:28",
449 "isProduction": "false",
450 "namespace": "localhost/test",
451 },
452 },
453 {
454 name: "invalid headers format",
455 value: "userId:alice",
456 want: map[string]string{},
457 },
458 {
459 name: "invalid key",
460 value: "%XX=missing,userId=alice",
461 want: map[string]string{
462 "userId": "alice",
463 },
464 },
465 {
466 name: "invalid value",
467 value: "missing=%XX,userId=alice",
468 want: map[string]string{
469 "userId": "alice",
470 },
471 },
472 }
473
474 for _, tt := range tests {
475 t.Run(tt.name, func(t *testing.T) {
476 assert.Equal(t, tt.want, stringToHeader(tt.value))
477 })
478 }
479 }
480
View as plain text