1 package gotenv_test
2
3 import (
4 "bufio"
5 "errors"
6 "io"
7 "os"
8 "strings"
9 "testing"
10
11 "github.com/stretchr/testify/assert"
12 "github.com/subosito/gotenv"
13 )
14
15 var formats = []struct {
16 in string
17 out gotenv.Env
18 preset bool
19 }{
20
21 {`FOO=bar`, gotenv.Env{"FOO": "bar"}, false},
22
23
24 {`FOO =bar`, gotenv.Env{"FOO": "bar"}, false},
25 {`FOO= bar`, gotenv.Env{"FOO": "bar"}, false},
26
27
28 {` FOO=bar`, gotenv.Env{"FOO": "bar"}, false},
29
30
31 {`FOO=bar `, gotenv.Env{"FOO": "bar"}, false},
32
33
34 {`FOO="bar"`, gotenv.Env{"FOO": "bar"}, false},
35
36
37 {`FOO="bar" `, gotenv.Env{"FOO": "bar"}, false},
38
39
40 {`FOO='bar'`, gotenv.Env{"FOO": "bar"}, false},
41
42
43 {`FOO='bar' `, gotenv.Env{"FOO": "bar"}, false},
44
45
46 {`FOO="escaped\"bar"`, gotenv.Env{"FOO": `escaped"bar`}, false},
47
48
49 {`FOO=`, gotenv.Env{"FOO": ""}, false},
50
51
52 {"FOO=test\nBAR=$FOO", gotenv.Env{"FOO": "test", "BAR": "test"}, false},
53
54
55 {"FOO=test\nBAR=${FOO}bar", gotenv.Env{"FOO": "test", "BAR": "testbar"}, false},
56
57
58 {`BAR=$FOO`, gotenv.Env{"BAR": "test"}, true},
59
60
61 {`BAR=$FOO`, gotenv.Env{"BAR": ""}, false},
62
63
64 {"FOO=test\nBAR=\"quote $FOO\"", gotenv.Env{"FOO": "test", "BAR": "quote test"}, false},
65
66
67 {"BAR='quote $FOO'", gotenv.Env{"BAR": "quote $FOO"}, false},
68
69
70 {`FOO="foo\$BAR"`, gotenv.Env{"FOO": "foo$BAR"}, false},
71 {`FOO="foo\${BAR}"`, gotenv.Env{"FOO": "foo${BAR}"}, false},
72 {"FOO=test\nBAR=\"foo\\${FOO} ${FOO}\"", gotenv.Env{"FOO": "test", "BAR": "foo${FOO} test"}, false},
73
74
75 {"OPTION_A: 1", gotenv.Env{"OPTION_A": "1"}, false},
76
77
78 {"export OPTION_A=2", gotenv.Env{"OPTION_A": "2"}, false},
79
80
81 {"OPTION_A=2\nexport OPTION_A", gotenv.Env{"OPTION_A": "2"}, false},
82
83
84 {`FOO="bar\nbaz"`, gotenv.Env{"FOO": "bar\nbaz"}, false},
85
86
87 {`FOO.BAR=foobar`, gotenv.Env{"FOO.BAR": "foobar"}, false},
88
89
90 {`foo=bar `, gotenv.Env{"foo": "bar"}, false},
91
92
93 {"\n \t \nfoo=bar\n \nfizz=buzz", gotenv.Env{"foo": "bar", "fizz": "buzz"}, false},
94
95
96 {"foo=bar # this is foo", gotenv.Env{"foo": "bar"}, false},
97
98
99 {`foo="bar#baz" # comment`, gotenv.Env{"foo": "bar#baz"}, false},
100
101
102 {"\n\n\n # HERE GOES FOO \nfoo=bar", gotenv.Env{"foo": "bar"}, false},
103
104
105 {`foo="ba#r"`, gotenv.Env{"foo": "ba#r"}, false},
106 {"foo='ba#r'", gotenv.Env{"foo": "ba#r"}, false},
107
108
109 {`foo="ba#r" `, gotenv.Env{"foo": "ba#r"}, false},
110 {`foo='ba#r' `, gotenv.Env{"foo": "ba#r"}, false},
111
112
113 {"FOO=bar\rbaz=fbb", gotenv.Env{"FOO": "bar", "baz": "fbb"}, false},
114
115
116 {"FOO=bar\r\nbaz=fbb", gotenv.Env{"FOO": "bar", "baz": "fbb"}, false},
117
118
119 {`FOO="bar\rbaz"`, gotenv.Env{"FOO": "bar\rbaz"}, false},
120
121
122 {`FOO="bar\\$ \\$\\$"`, gotenv.Env{"FOO": "bar$ $$"}, false},
123
124
125 {`FOO="bar $ "`, gotenv.Env{"FOO": "bar $ "}, false},
126
127
128 {`FOO= bar`, gotenv.Env{"FOO": "bar"}, false},
129
130
131 {`foo= "bar#baz" # comment`, gotenv.Env{"foo": "bar#baz"}, false},
132
133
134 {`foo="---\na==\n---"`, gotenv.Env{"foo": "---\na==\n---"}, false},
135 }
136
137 var errorFormats = []struct {
138 in string
139 out gotenv.Env
140 err string
141 }{
142
143 {"OPTION_A=2\nexport OH_NO_NOT_SET", gotenv.Env{"OPTION_A": "2"}, "line `export OH_NO_NOT_SET` has an unset variable"},
144
145
146 {`lol$wut`, gotenv.Env{}, "line `lol$wut` doesn't match format"},
147 }
148
149 var fixtures = []struct {
150 filename string
151 results gotenv.Env
152 }{
153 {
154 "fixtures/exported.env",
155 gotenv.Env{
156 "OPTION_A": "2",
157 "OPTION_B": `\n`,
158 "OPTION_C": `The MIT License (MIT)
159
160 Copyright (c) 2013 Alif Rachmawadi
161
162 Permission is hereby granted, free of charge, to any person obtaining a copy
163 of this software and associated documentation files (the "Software"), to deal
164 in the Software without restriction, including without limitation the rights
165 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
166 copies of the Software, and to permit persons to whom the Software is
167 furnished to do so, subject to the following conditions:
168
169 The above copyright notice and this permission notice shall be included in
170 all copies or substantial portions of the Software.
171
172 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
173 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
174 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
175 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
176 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
177 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
178 THE SOFTWARE.`,
179 },
180 },
181 {
182 "fixtures/plain.env",
183 gotenv.Env{
184 "OPTION_A": "1",
185 "OPTION_B": "2",
186 "OPTION_C": "3",
187 "OPTION_D": "4",
188 "OPTION_E": "5",
189 },
190 },
191 {
192 "fixtures/quoted.env",
193 gotenv.Env{
194 "OPTION_A": "1",
195 "OPTION_B": "2",
196 "OPTION_C": "",
197 "OPTION_D": `\n`,
198 "OPTION_E": "1",
199 "OPTION_F": "2",
200 "OPTION_G": "",
201 "OPTION_H": "\n",
202 "OPTION_I": `some multi-line text
203 with "escaped quotes" and 1 variable`,
204 "OPTION_J": `some$pecial$1$2!*chars=qweq""e$$\$""`,
205 "OPTION_K": "\n",
206 "OPTION_L": `some multi-line text
207 with "escaped quotes"
208 empty lines
209
210 and 1 variable
211 `,
212 },
213 },
214 {
215 "fixtures/yaml.env",
216 gotenv.Env{
217 "OPTION_A": "1",
218 "OPTION_B": "2",
219 "OPTION_C": "",
220 "OPTION_D": `\n`,
221 },
222 },
223 }
224
225 func TestParse(t *testing.T) {
226 for _, tt := range formats {
227 if tt.preset {
228 os.Setenv("FOO", "test")
229 }
230
231 exp := gotenv.Parse(strings.NewReader(tt.in))
232 assert.Equal(t, tt.out, exp)
233 os.Clearenv()
234 }
235 }
236
237 func TestStrictParse(t *testing.T) {
238 for _, tt := range errorFormats {
239 env, err := gotenv.StrictParse(strings.NewReader(tt.in))
240 assert.Equal(t, tt.err, err.Error())
241 assert.Equal(t, tt.out, env)
242 }
243 }
244
245 type failingReader struct {
246 io.Reader
247 }
248
249 func (fr failingReader) Read(p []byte) (n int, err error) {
250 return 0, errors.New("you shall not read")
251 }
252
253 func TestStrictParse_PassThroughErrors(t *testing.T) {
254 _, err := gotenv.StrictParse(&failingReader{})
255 assert.Error(t, err)
256 }
257
258 type infiniteReader struct {
259 io.Reader
260 }
261
262 func (er infiniteReader) Read(p []byte) (n int, err error) {
263 return len(p), nil
264 }
265
266 func TestStrictParse_NoTokenPassThroughErrors(t *testing.T) {
267 _, err := gotenv.StrictParse(&infiniteReader{})
268 assert.Error(t, err)
269 }
270
271 func TestRead(t *testing.T) {
272 for _, tt := range fixtures {
273 env, err := gotenv.Read(tt.filename)
274 assert.Nil(t, err)
275
276 for key, val := range tt.results {
277 assert.Equal(t, val, env[key])
278 }
279
280 os.Clearenv()
281 }
282 }
283
284 func TestLoad(t *testing.T) {
285 for _, tt := range fixtures {
286 err := gotenv.Load(tt.filename)
287 assert.Nil(t, err)
288
289 for key, val := range tt.results {
290 assert.Equal(t, val, os.Getenv(key))
291 }
292
293 os.Clearenv()
294 }
295 }
296
297 func TestLoad_default(t *testing.T) {
298 k := "HELLO"
299 v := "world"
300
301 err := gotenv.Load()
302 assert.Nil(t, err)
303 assert.Equal(t, v, os.Getenv(k))
304 os.Clearenv()
305 }
306
307 func TestLoad_overriding(t *testing.T) {
308 k := "HELLO"
309 v := "universe"
310
311 os.Setenv(k, v)
312 err := gotenv.Load()
313 assert.Nil(t, err)
314 assert.Equal(t, v, os.Getenv(k))
315 os.Clearenv()
316 }
317
318 func TestLoad_overrideVars(t *testing.T) {
319 os.Setenv("A", "fromEnv")
320 err := gotenv.Load("fixtures/vars.env")
321 assert.Nil(t, err)
322 assert.Equal(t, "fromEnv", os.Getenv("B"))
323 os.Clearenv()
324 }
325
326 func TestLoad_overrideVars2(t *testing.T) {
327 os.Setenv("C", "fromEnv")
328 err := gotenv.Load("fixtures/vars.env")
329 assert.Nil(t, err)
330 assert.Equal(t, "fromEnv", os.Getenv("D"))
331 os.Clearenv()
332 }
333
334 func TestLoad_Env(t *testing.T) {
335 err := gotenv.Load(".env.invalid")
336 assert.NotNil(t, err)
337 }
338
339 func TestLoad_nonExist(t *testing.T) {
340 file := ".env.not.exist"
341
342 err := gotenv.Load(file)
343 if err == nil {
344 t.Errorf("gotenv.Load(`%s`) => error: `no such file or directory` != nil", file)
345 }
346 }
347
348 func TestLoad_unicodeBOMFixture(t *testing.T) {
349 file := "fixtures/utf8_bom.env"
350
351 f, err := os.Open(file)
352 assert.Nil(t, err)
353
354 scanner := bufio.NewScanner(f)
355
356 i := 1
357 bom := string([]byte{239, 187, 191})
358
359 for scanner.Scan() {
360 if i == 1 {
361 line := scanner.Text()
362 assert.True(t, strings.HasPrefix(line, bom))
363 }
364 }
365 }
366
367 func TestLoad_BOM_UTF8(t *testing.T) {
368 defer os.Clearenv()
369
370 file := "fixtures/utf8_bom.env"
371
372 if err := gotenv.Load(file); assert.Nil(t, err) {
373 assert.Equal(t, "UTF-8", os.Getenv("BOM"))
374 }
375 }
376
377 func TestLoad_BOM_UTF16_LE(t *testing.T) {
378 defer os.Clearenv()
379
380 file := "fixtures/utf16le_bom.env"
381
382 if err := gotenv.Load(file); assert.Nil(t, err) {
383 assert.Equal(t, "UTF-16 LE", os.Getenv("BOM"))
384 }
385 }
386
387 func TestLoad_BOM_UTF16_BE(t *testing.T) {
388 defer os.Clearenv()
389
390 file := "fixtures/utf16be_bom.env"
391
392 if err := gotenv.Load(file); assert.Nil(t, err) {
393 assert.Equal(t, "UTF-16 BE", os.Getenv("BOM"))
394 }
395 }
396
397 func TestMust_Load(t *testing.T) {
398 for _, tt := range fixtures {
399 assert.NotPanics(t, func() {
400 gotenv.Must(gotenv.Load, tt.filename)
401
402 for key, val := range tt.results {
403 assert.Equal(t, val, os.Getenv(key))
404 }
405
406 os.Clearenv()
407 }, "Caling gotenv.Must with gotenv.Load should NOT panic")
408 }
409 }
410
411 func TestMust_Load_default(t *testing.T) {
412 assert.NotPanics(t, func() {
413 gotenv.Must(gotenv.Load)
414
415 tkey := "HELLO"
416 val := "world"
417
418 assert.Equal(t, val, os.Getenv(tkey))
419 os.Clearenv()
420 }, "Caling gotenv.Must with gotenv.Load without arguments should NOT panic")
421 }
422
423 func TestMust_Load_nonExist(t *testing.T) {
424 assert.Panics(t, func() { gotenv.Must(gotenv.Load, ".env.not.exist") }, "Caling gotenv.Must with gotenv.Load and non exist file SHOULD panic")
425 }
426
427 func TestOverLoad_overriding(t *testing.T) {
428 k := "HELLO"
429 v := "universe"
430
431 os.Setenv(k, v)
432 err := gotenv.OverLoad()
433 assert.Nil(t, err)
434 assert.Equal(t, "world", os.Getenv(k))
435 os.Clearenv()
436 }
437
438 func TestOverLoad_overrideVars(t *testing.T) {
439 os.Setenv("A", "fromEnv")
440 err := gotenv.OverLoad("fixtures/vars.env")
441 assert.Nil(t, err)
442 assert.Equal(t, "fromFile", os.Getenv("B"))
443 os.Clearenv()
444 }
445
446 func TestOverLoad_overrideVars2(t *testing.T) {
447 os.Setenv("C", "fromEnv")
448 err := gotenv.OverLoad("fixtures/vars.env")
449 assert.Nil(t, err)
450
451
452 assert.Equal(t, "fromEnv", os.Getenv("D"), "C defined after usage")
453 os.Clearenv()
454 }
455
456 func TestMustOverLoad_nonExist(t *testing.T) {
457 assert.Panics(t, func() { gotenv.Must(gotenv.OverLoad, ".env.not.exist") }, "Caling gotenv.Must with Overgotenv.Load and non exist file SHOULD panic")
458 }
459
460 func TestApply(t *testing.T) {
461 os.Setenv("HELLO", "world")
462 r := strings.NewReader("HELLO=universe")
463 err := gotenv.Apply(r)
464 assert.Nil(t, err)
465 assert.Equal(t, "world", os.Getenv("HELLO"))
466 os.Clearenv()
467 }
468
469 func TestOverApply(t *testing.T) {
470 os.Setenv("HELLO", "world")
471 r := strings.NewReader("HELLO=universe")
472 err := gotenv.OverApply(r)
473 assert.Nil(t, err)
474 assert.Equal(t, "universe", os.Getenv("HELLO"))
475 os.Clearenv()
476 }
477
478 func TestMarshal(t *testing.T) {
479 env := gotenv.Env{
480 "FOO": "BAR",
481 "ONE": "1",
482 "QUOTED": `some "quoted" text`,
483 "EMPTY": "",
484 }
485 expected := `EMPTY=""
486 FOO="BAR"
487 ONE=1
488 QUOTED="some \"quoted\" text"`
489
490 actual, err := gotenv.Marshal(env)
491 assert.Nil(t, err)
492 assert.Equal(t, expected, actual)
493
494 out, err := gotenv.Unmarshal(expected)
495 assert.Nil(t, err)
496 assert.Equal(t, env, out)
497 }
498
View as plain text