1package gotenv_test
2
3import (
4	"bufio"
5	"os"
6	"strings"
7	"testing"
8
9	"github.com/stretchr/testify/assert"
10	"github.com/subosito/gotenv"
11)
12
13var formats = []struct {
14	in     string
15	out    gotenv.Env
16	preset bool
17}{
18	// parses unquoted values
19	{`FOO=bar`, gotenv.Env{"FOO": "bar"}, false},
20
21	// parses values with spaces around equal sign
22	{`FOO =bar`, gotenv.Env{"FOO": "bar"}, false},
23	{`FOO= bar`, gotenv.Env{"FOO": "bar"}, false},
24
25	// parses values with leading spaces
26	{`  FOO=bar`, gotenv.Env{"FOO": "bar"}, false},
27
28	// parses values with following spaces
29	{`FOO=bar  `, gotenv.Env{"FOO": "bar"}, false},
30
31	// parses double quoted values
32	{`FOO="bar"`, gotenv.Env{"FOO": "bar"}, false},
33
34	// parses double quoted values with following spaces
35	{`FOO="bar"  `, gotenv.Env{"FOO": "bar"}, false},
36
37	// parses single quoted values
38	{`FOO='bar'`, gotenv.Env{"FOO": "bar"}, false},
39
40	// parses single quoted values with following spaces
41	{`FOO='bar'  `, gotenv.Env{"FOO": "bar"}, false},
42
43	// parses escaped double quotes
44	{`FOO="escaped\"bar"`, gotenv.Env{"FOO": `escaped"bar`}, false},
45
46	// parses empty values
47	{`FOO=`, gotenv.Env{"FOO": ""}, false},
48
49	// expands variables found in values
50	{"FOO=test\nBAR=$FOO", gotenv.Env{"FOO": "test", "BAR": "test"}, false},
51
52	// parses variables wrapped in brackets
53	{"FOO=test\nBAR=${FOO}bar", gotenv.Env{"FOO": "test", "BAR": "testbar"}, false},
54
55	// reads variables from ENV when expanding if not found in local env
56	{`BAR=$FOO`, gotenv.Env{"BAR": "test"}, true},
57
58	// expands undefined variables to an empty string
59	{`BAR=$FOO`, gotenv.Env{"BAR": ""}, false},
60
61	// expands variables in quoted strings
62	{"FOO=test\nBAR=\"quote $FOO\"", gotenv.Env{"FOO": "test", "BAR": "quote test"}, false},
63
64	// does not expand variables in single quoted strings
65	{"BAR='quote $FOO'", gotenv.Env{"BAR": "quote $FOO"}, false},
66
67	// does not expand escaped variables
68	{`FOO="foo\$BAR"`, gotenv.Env{"FOO": "foo$BAR"}, false},
69	{`FOO="foo\${BAR}"`, gotenv.Env{"FOO": "foo${BAR}"}, false},
70	{"FOO=test\nBAR=\"foo\\${FOO} ${FOO}\"", gotenv.Env{"FOO": "test", "BAR": "foo${FOO} test"}, false},
71
72	// parses yaml style options
73	{"OPTION_A: 1", gotenv.Env{"OPTION_A": "1"}, false},
74
75	// parses export keyword
76	{"export OPTION_A=2", gotenv.Env{"OPTION_A": "2"}, false},
77
78	// allows export line if you want to do it that way
79	{"OPTION_A=2\nexport OPTION_A", gotenv.Env{"OPTION_A": "2"}, false},
80
81	// expands newlines in quoted strings
82	{`FOO="bar\nbaz"`, gotenv.Env{"FOO": "bar\nbaz"}, false},
83
84	// parses variables with "." in the name
85	{`FOO.BAR=foobar`, gotenv.Env{"FOO.BAR": "foobar"}, false},
86
87	// strips unquoted values
88	{`foo=bar `, gotenv.Env{"foo": "bar"}, false}, // not 'bar '
89
90	// ignores empty lines
91	{"\n \t  \nfoo=bar\n \nfizz=buzz", gotenv.Env{"foo": "bar", "fizz": "buzz"}, false},
92
93	// ignores inline comments
94	{"foo=bar # this is foo", gotenv.Env{"foo": "bar"}, false},
95
96	// allows # in quoted value
97	{`foo="bar#baz" # comment`, gotenv.Env{"foo": "bar#baz"}, false},
98
99	// ignores comment lines
100	{"\n\n\n # HERE GOES FOO \nfoo=bar", gotenv.Env{"foo": "bar"}, false},
101
102	// parses # in quoted values
103	{`foo="ba#r"`, gotenv.Env{"foo": "ba#r"}, false},
104	{"foo='ba#r'", gotenv.Env{"foo": "ba#r"}, false},
105
106	// parses # in quoted values with following spaces
107	{`foo="ba#r"  `, gotenv.Env{"foo": "ba#r"}, false},
108	{`foo='ba#r'  `, gotenv.Env{"foo": "ba#r"}, false},
109
110	// supports carriage return
111	{"FOO=bar\rbaz=fbb", gotenv.Env{"FOO": "bar", "baz": "fbb"}, false},
112
113	// supports carriage return combine with new line
114	{"FOO=bar\r\nbaz=fbb", gotenv.Env{"FOO": "bar", "baz": "fbb"}, false},
115
116	// expands carriage return in quoted strings
117	{`FOO="bar\rbaz"`, gotenv.Env{"FOO": "bar\rbaz"}, false},
118
119	// escape $ properly when no alphabets/numbers/_  are followed by it
120	{`FOO="bar\\$ \\$\\$"`, gotenv.Env{"FOO": "bar$ $$"}, false},
121
122	// ignore $ when it is not escaped and no variable is followed by it
123	{`FOO="bar $ "`, gotenv.Env{"FOO": "bar $ "}, false},
124
125	// parses unquoted values with spaces after separator
126	{`FOO= bar`, gotenv.Env{"FOO": "bar"}, false},
127
128	// allows # in quoted value with spaces after separator
129	{`foo= "bar#baz" # comment`, gotenv.Env{"foo": "bar#baz"}, false},
130}
131
132var errorFormats = []struct {
133	in  string
134	out gotenv.Env
135	err string
136}{
137	// allows export line if you want to do it that way and checks for unset variables
138	{"OPTION_A=2\nexport OH_NO_NOT_SET", gotenv.Env{"OPTION_A": "2"}, "line `export OH_NO_NOT_SET` has an unset variable"},
139
140	// throws an error if line format is incorrect
141	{`lol$wut`, gotenv.Env{}, "line `lol$wut` doesn't match format"},
142}
143
144var fixtures = []struct {
145	filename string
146	results  gotenv.Env
147}{
148	{
149		"fixtures/exported.env",
150		gotenv.Env{
151			"OPTION_A": "2",
152			"OPTION_B": `\n`,
153		},
154	},
155	{
156		"fixtures/plain.env",
157		gotenv.Env{
158			"OPTION_A": "1",
159			"OPTION_B": "2",
160			"OPTION_C": "3",
161			"OPTION_D": "4",
162			"OPTION_E": "5",
163		},
164	},
165	{
166		"fixtures/quoted.env",
167		gotenv.Env{
168			"OPTION_A": "1",
169			"OPTION_B": "2",
170			"OPTION_C": "",
171			"OPTION_D": `\n`,
172			"OPTION_E": "1",
173			"OPTION_F": "2",
174			"OPTION_G": "",
175			"OPTION_H": "\n",
176		},
177	},
178	{
179		"fixtures/yaml.env",
180		gotenv.Env{
181			"OPTION_A": "1",
182			"OPTION_B": "2",
183			"OPTION_C": "",
184			"OPTION_D": `\n`,
185		},
186	},
187}
188
189func TestParse(t *testing.T) {
190	for _, tt := range formats {
191		if tt.preset {
192			os.Setenv("FOO", "test")
193		}
194
195		exp := gotenv.Parse(strings.NewReader(tt.in))
196		assert.Equal(t, tt.out, exp)
197		os.Clearenv()
198	}
199}
200
201func TestStrictParse(t *testing.T) {
202	for _, tt := range errorFormats {
203		env, err := gotenv.StrictParse(strings.NewReader(tt.in))
204		assert.Equal(t, tt.err, err.Error())
205		assert.Equal(t, tt.out, env)
206	}
207}
208
209func TestLoad(t *testing.T) {
210	for _, tt := range fixtures {
211		err := gotenv.Load(tt.filename)
212		assert.Nil(t, err)
213
214		for key, val := range tt.results {
215			assert.Equal(t, val, os.Getenv(key))
216		}
217
218		os.Clearenv()
219	}
220}
221
222func TestLoad_default(t *testing.T) {
223	k := "HELLO"
224	v := "world"
225
226	err := gotenv.Load()
227	assert.Nil(t, err)
228	assert.Equal(t, v, os.Getenv(k))
229	os.Clearenv()
230}
231
232func TestLoad_overriding(t *testing.T) {
233	k := "HELLO"
234	v := "universe"
235
236	os.Setenv(k, v)
237	err := gotenv.Load()
238	assert.Nil(t, err)
239	assert.Equal(t, v, os.Getenv(k))
240	os.Clearenv()
241}
242
243func TestLoad_Env(t *testing.T) {
244	err := gotenv.Load(".env.invalid")
245	assert.NotNil(t, err)
246}
247
248func TestLoad_nonExist(t *testing.T) {
249	file := ".env.not.exist"
250
251	err := gotenv.Load(file)
252	if err == nil {
253		t.Errorf("gotenv.Load(`%s`) => error: `no such file or directory` != nil", file)
254	}
255}
256
257func TestLoad_unicodeBOMFixture(t *testing.T) {
258	file := "fixtures/bom.env"
259
260	f, err := os.Open(file)
261	assert.Nil(t, err)
262
263	scanner := bufio.NewScanner(f)
264
265	i := 1
266	bom := string([]byte{239, 187, 191})
267
268	for scanner.Scan() {
269		if i == 1 {
270			line := scanner.Text()
271			assert.True(t, strings.HasPrefix(line, bom))
272		}
273	}
274}
275
276func TestLoad_unicodeBOM(t *testing.T) {
277	file := "fixtures/bom.env"
278
279	err := gotenv.Load(file)
280	assert.Nil(t, err)
281	assert.Equal(t, "UTF-8", os.Getenv("BOM"))
282	os.Clearenv()
283}
284
285func TestMust_Load(t *testing.T) {
286	for _, tt := range fixtures {
287		assert.NotPanics(t, func() {
288			gotenv.Must(gotenv.Load, tt.filename)
289
290			for key, val := range tt.results {
291				assert.Equal(t, val, os.Getenv(key))
292			}
293
294			os.Clearenv()
295		}, "Caling gotenv.Must with gotenv.Load should NOT panic")
296	}
297}
298
299func TestMust_Load_default(t *testing.T) {
300	assert.NotPanics(t, func() {
301		gotenv.Must(gotenv.Load)
302
303		tkey := "HELLO"
304		val := "world"
305
306		assert.Equal(t, val, os.Getenv(tkey))
307		os.Clearenv()
308	}, "Caling gotenv.Must with gotenv.Load without arguments should NOT panic")
309}
310
311func TestMust_Load_nonExist(t *testing.T) {
312	assert.Panics(t, func() { gotenv.Must(gotenv.Load, ".env.not.exist") }, "Caling gotenv.Must with gotenv.Load and non exist file SHOULD panic")
313}
314
315func TestOverLoad_overriding(t *testing.T) {
316	k := "HELLO"
317	v := "universe"
318
319	os.Setenv(k, v)
320	err := gotenv.OverLoad()
321	assert.Nil(t, err)
322	assert.Equal(t, "world", os.Getenv(k))
323	os.Clearenv()
324}
325
326func TestMustOverLoad_nonExist(t *testing.T) {
327	assert.Panics(t, func() { gotenv.Must(gotenv.OverLoad, ".env.not.exist") }, "Caling gotenv.Must with Overgotenv.Load and non exist file SHOULD panic")
328}
329
330func TestApply(t *testing.T) {
331	os.Setenv("HELLO", "world")
332	r := strings.NewReader("HELLO=universe")
333	err := gotenv.Apply(r)
334	assert.Nil(t, err)
335	assert.Equal(t, "world", os.Getenv("HELLO"))
336	os.Clearenv()
337}
338
339func TestOverApply(t *testing.T) {
340	os.Setenv("HELLO", "world")
341	r := strings.NewReader("HELLO=universe")
342	err := gotenv.OverApply(r)
343	assert.Nil(t, err)
344	assert.Equal(t, "universe", os.Getenv("HELLO"))
345	os.Clearenv()
346}
347