1 package godotenv
2
3 import (
4 "bytes"
5 "fmt"
6 "os"
7 "reflect"
8 "testing"
9 "strings"
10 )
11
12 var noopPresets = make(map[string]string)
13
14 func parseAndCompare(t *testing.T, rawEnvLine string, expectedKey string, expectedValue string) {
15 key, value, _ := parseLine(rawEnvLine, noopPresets)
16 if key != expectedKey || value != expectedValue {
17 t.Errorf("Expected '%v' to parse as '%v' => '%v', got '%v' => '%v' instead", rawEnvLine, expectedKey, expectedValue, key, value)
18 }
19 }
20
21 func loadEnvAndCompareValues(t *testing.T, loader func(files ...string) error, envFileName string, expectedValues map[string]string, presets map[string]string) {
22
23 os.Clearenv()
24
25 for k, v := range presets {
26 os.Setenv(k, v)
27 }
28
29 err := loader(envFileName)
30 if err != nil {
31 t.Fatalf("Error loading %v", envFileName)
32 }
33
34 for k := range expectedValues {
35 envValue := os.Getenv(k)
36 v := expectedValues[k]
37 if envValue != v {
38 t.Errorf("Mismatch for key '%v': expected '%v' got '%v'", k, v, envValue)
39 }
40 }
41 }
42
43 func TestLoadWithNoArgsLoadsDotEnv(t *testing.T) {
44 err := Load()
45 pathError := err.(*os.PathError)
46 if pathError == nil || pathError.Op != "open" || pathError.Path != ".env" {
47 t.Errorf("Didn't try and open .env by default")
48 }
49 }
50
51 func TestOverloadWithNoArgsOverloadsDotEnv(t *testing.T) {
52 err := Overload()
53 pathError := err.(*os.PathError)
54 if pathError == nil || pathError.Op != "open" || pathError.Path != ".env" {
55 t.Errorf("Didn't try and open .env by default")
56 }
57 }
58
59 func TestLoadFileNotFound(t *testing.T) {
60 err := Load("somefilethatwillneverexistever.env")
61 if err == nil {
62 t.Error("File wasn't found but Load didn't return an error")
63 }
64 }
65
66 func TestOverloadFileNotFound(t *testing.T) {
67 err := Overload("somefilethatwillneverexistever.env")
68 if err == nil {
69 t.Error("File wasn't found but Overload didn't return an error")
70 }
71 }
72
73 func TestReadPlainEnv(t *testing.T) {
74 envFileName := "fixtures/plain.env"
75 expectedValues := map[string]string{
76 "OPTION_A": "1",
77 "OPTION_B": "2",
78 "OPTION_C": "3",
79 "OPTION_D": "4",
80 "OPTION_E": "5",
81 "OPTION_F": "",
82 "OPTION_G": "",
83 }
84
85 envMap, err := Read(envFileName)
86 if err != nil {
87 t.Error("Error reading file")
88 }
89
90 if len(envMap) != len(expectedValues) {
91 t.Error("Didn't get the right size map back")
92 }
93
94 for key, value := range expectedValues {
95 if envMap[key] != value {
96 t.Error("Read got one of the keys wrong")
97 }
98 }
99 }
100
101 func TestParse(t *testing.T) {
102 envMap, err := Parse(bytes.NewReader([]byte("ONE=1\nTWO='2'\nTHREE = \"3\"")))
103 expectedValues := map[string]string{
104 "ONE": "1",
105 "TWO": "2",
106 "THREE": "3",
107 }
108 if err != nil {
109 t.Fatalf("error parsing env: %v", err)
110 }
111 for key, value := range expectedValues {
112 if envMap[key] != value {
113 t.Errorf("expected %s to be %s, got %s", key, value, envMap[key])
114 }
115 }
116 }
117
118 func TestLoadDoesNotOverride(t *testing.T) {
119 envFileName := "fixtures/plain.env"
120
121
122 presets := map[string]string{
123 "OPTION_A": "do_not_override",
124 "OPTION_B": "",
125 }
126
127 expectedValues := map[string]string{
128 "OPTION_A": "do_not_override",
129 "OPTION_B": "",
130 }
131 loadEnvAndCompareValues(t, Load, envFileName, expectedValues, presets)
132 }
133
134 func TestOveroadDoesOverride(t *testing.T) {
135 envFileName := "fixtures/plain.env"
136
137
138 presets := map[string]string{
139 "OPTION_A": "do_not_override",
140 }
141
142 expectedValues := map[string]string{
143 "OPTION_A": "1",
144 }
145 loadEnvAndCompareValues(t, Overload, envFileName, expectedValues, presets)
146 }
147
148 func TestLoadPlainEnv(t *testing.T) {
149 envFileName := "fixtures/plain.env"
150 expectedValues := map[string]string{
151 "OPTION_A": "1",
152 "OPTION_B": "2",
153 "OPTION_C": "3",
154 "OPTION_D": "4",
155 "OPTION_E": "5",
156 }
157
158 loadEnvAndCompareValues(t, Load, envFileName, expectedValues, noopPresets)
159 }
160
161 func TestLoadExportedEnv(t *testing.T) {
162 envFileName := "fixtures/exported.env"
163 expectedValues := map[string]string{
164 "OPTION_A": "2",
165 "OPTION_B": "\\n",
166 }
167
168 loadEnvAndCompareValues(t, Load, envFileName, expectedValues, noopPresets)
169 }
170
171 func TestLoadEqualsEnv(t *testing.T) {
172 envFileName := "fixtures/equals.env"
173 expectedValues := map[string]string{
174 "OPTION_A": "postgres://localhost:5432/database?sslmode=disable",
175 }
176
177 loadEnvAndCompareValues(t, Load, envFileName, expectedValues, noopPresets)
178 }
179
180 func TestLoadQuotedEnv(t *testing.T) {
181 envFileName := "fixtures/quoted.env"
182 expectedValues := map[string]string{
183 "OPTION_A": "1",
184 "OPTION_B": "2",
185 "OPTION_C": "",
186 "OPTION_D": "\\n",
187 "OPTION_E": "1",
188 "OPTION_F": "2",
189 "OPTION_G": "",
190 "OPTION_H": "\n",
191 "OPTION_I": "echo 'asd'",
192 }
193
194 loadEnvAndCompareValues(t, Load, envFileName, expectedValues, noopPresets)
195 }
196
197 func TestSubstitutions(t *testing.T) {
198 envFileName := "fixtures/substitutions.env"
199 expectedValues := map[string]string{
200 "OPTION_A": "1",
201 "OPTION_B": "1",
202 "OPTION_C": "1",
203 "OPTION_D": "11",
204 "OPTION_E": "",
205 }
206
207 loadEnvAndCompareValues(t, Load, envFileName, expectedValues, noopPresets)
208 }
209
210 func TestExpanding(t *testing.T) {
211 tests := []struct {
212 name string
213 input string
214 expected map[string]string
215 }{
216 {
217 "expands variables found in values",
218 "FOO=test\nBAR=$FOO",
219 map[string]string{"FOO": "test", "BAR": "test"},
220 },
221 {
222 "parses variables wrapped in brackets",
223 "FOO=test\nBAR=${FOO}bar",
224 map[string]string{"FOO": "test", "BAR": "testbar"},
225 },
226 {
227 "expands undefined variables to an empty string",
228 "BAR=$FOO",
229 map[string]string{"BAR": ""},
230 },
231 {
232 "expands variables in double quoted strings",
233 "FOO=test\nBAR=\"quote $FOO\"",
234 map[string]string{"FOO": "test", "BAR": "quote test"},
235 },
236 {
237 "does not expand variables in single quoted strings",
238 "BAR='quote $FOO'",
239 map[string]string{"BAR": "quote $FOO"},
240 },
241 {
242 "does not expand escaped variables",
243 `FOO="foo\$BAR"`,
244 map[string]string{"FOO": "foo$BAR"},
245 },
246 {
247 "does not expand escaped variables",
248 `FOO="foo\${BAR}"`,
249 map[string]string{"FOO": "foo${BAR}"},
250 },
251 {
252 "does not expand escaped variables",
253 "FOO=test\nBAR=\"foo\\${FOO} ${FOO}\"",
254 map[string]string{"FOO": "test", "BAR": "foo${FOO} test"},
255 },
256 }
257
258 for _, tt := range tests {
259 t.Run(tt.name, func(t *testing.T) {
260 env, err := Parse(strings.NewReader(tt.input))
261 if err != nil {
262 t.Errorf("Error: %s", err.Error())
263 }
264 for k, v := range tt.expected {
265 if strings.Compare(env[k], v) != 0 {
266 t.Errorf("Expected: %s, Actual: %s", v, env[k])
267 }
268 }
269 })
270 }
271
272 }
273
274 func TestActualEnvVarsAreLeftAlone(t *testing.T) {
275 os.Clearenv()
276 os.Setenv("OPTION_A", "actualenv")
277 _ = Load("fixtures/plain.env")
278
279 if os.Getenv("OPTION_A") != "actualenv" {
280 t.Error("An ENV var set earlier was overwritten")
281 }
282 }
283
284 func TestParsing(t *testing.T) {
285
286 parseAndCompare(t, "FOO=bar", "FOO", "bar")
287
288
289 parseAndCompare(t, "FOO =bar", "FOO", "bar")
290 parseAndCompare(t, "FOO= bar", "FOO", "bar")
291
292
293 parseAndCompare(t, `FOO="bar"`, "FOO", "bar")
294
295
296 parseAndCompare(t, "FOO='bar'", "FOO", "bar")
297
298
299 parseAndCompare(t, `FOO="escaped\"bar"`, "FOO", `escaped"bar`)
300
301
302 parseAndCompare(t, `FOO="'d'"`, "FOO", `'d'`)
303
304
305 parseAndCompare(t, "OPTION_A: 1", "OPTION_A", "1")
306
307
308 parseAndCompare(t, "OPTION_A: Foo=bar", "OPTION_A", "Foo=bar")
309
310
311 parseAndCompare(t, "OPTION_A=1:B", "OPTION_A", "1:B")
312
313
314 parseAndCompare(t, "export OPTION_A=2", "OPTION_A", "2")
315 parseAndCompare(t, `export OPTION_B='\n'`, "OPTION_B", "\\n")
316
317
318
319 parseAndCompare(t, `FOO="bar\nbaz"`, "FOO", "bar\nbaz")
320
321
322
323 parseAndCompare(t, "FOO.BAR=foobar", "FOO.BAR", "foobar")
324
325
326
327 parseAndCompare(t, "FOO=foobar=", "FOO", "foobar=")
328
329
330
331 parseAndCompare(t, "FOO=bar ", "FOO", "bar")
332
333
334
335 parseAndCompare(t, "FOO=bar # this is foo", "FOO", "bar")
336
337
338
339 parseAndCompare(t, `FOO="bar#baz" # comment`, "FOO", "bar#baz")
340 parseAndCompare(t, "FOO='bar#baz' # comment", "FOO", "bar#baz")
341 parseAndCompare(t, `FOO="bar#baz#bang" # comment`, "FOO", "bar#baz#bang")
342
343
344
345
346 parseAndCompare(t, `FOO="ba#r"`, "FOO", "ba#r")
347 parseAndCompare(t, "FOO='ba#r'", "FOO", "ba#r")
348
349
350 parseAndCompare(t, `FOO="bar\n\ b\az"`, "FOO", "bar\n baz")
351 parseAndCompare(t, `FOO="bar\\\n\ b\az"`, "FOO", "bar\\\n baz")
352 parseAndCompare(t, `FOO="bar\\r\ b\az"`, "FOO", "bar\\r baz")
353
354 parseAndCompare(t, `="value"`, "", "value")
355 parseAndCompare(t, `KEY="`, "KEY", "\"")
356 parseAndCompare(t, `KEY="value`, "KEY", "\"value")
357
358
359
360 badlyFormattedLine := "lol$wut"
361 _, _, err := parseLine(badlyFormattedLine, noopPresets)
362 if err == nil {
363 t.Errorf("Expected \"%v\" to return error, but it didn't", badlyFormattedLine)
364 }
365 }
366
367 func TestLinesToIgnore(t *testing.T) {
368
369
370 if !isIgnoredLine("\n") {
371 t.Error("Line with nothing but line break wasn't ignored")
372 }
373
374 if !isIgnoredLine("\t\t ") {
375 t.Error("Line full of whitespace wasn't ignored")
376 }
377
378
379
380 if !isIgnoredLine("# comment") {
381 t.Error("Comment wasn't ignored")
382 }
383
384 if !isIgnoredLine("\t#comment") {
385 t.Error("Indented comment wasn't ignored")
386 }
387
388
389 if isIgnoredLine(`export OPTION_B='\n'`) {
390 t.Error("ignoring a perfectly valid line to parse")
391 }
392 }
393
394 func TestErrorReadDirectory(t *testing.T) {
395 envFileName := "fixtures/"
396 envMap, err := Read(envFileName)
397
398 if err == nil {
399 t.Errorf("Expected error, got %v", envMap)
400 }
401 }
402
403 func TestErrorParsing(t *testing.T) {
404 envFileName := "fixtures/invalid1.env"
405 envMap, err := Read(envFileName)
406 if err == nil {
407 t.Errorf("Expected error, got %v", envMap)
408 }
409 }
410
411 func TestWrite(t *testing.T) {
412 writeAndCompare := func(env string, expected string) {
413 envMap, _ := Unmarshal(env)
414 actual, _ := Marshal(envMap)
415 if expected != actual {
416 t.Errorf("Expected '%v' (%v) to write as '%v', got '%v' instead.", env, envMap, expected, actual)
417 }
418 }
419
420
421
422
423 writeAndCompare(`key=value`, `key="value"`)
424
425 writeAndCompare(`key=va"lu"e`, `key="va\"lu\"e"`)
426
427 writeAndCompare(`key=va'lu'e`, `key="va'lu'e"`)
428
429 writeAndCompare(`foo="\n\r\\r!"`, `foo="\n\r\\r\!"`)
430
431 writeAndCompare("foo=bar\nbaz=buzz", "baz=\"buzz\"\nfoo=\"bar\"")
432
433 }
434
435 func TestRoundtrip(t *testing.T) {
436 fixtures := []string{"equals.env", "exported.env", "plain.env", "quoted.env"}
437 for _, fixture := range fixtures {
438 fixtureFilename := fmt.Sprintf("fixtures/%s", fixture)
439 env, err := readFile(fixtureFilename)
440 if err != nil {
441 t.Errorf("Expected '%s' to read without error (%v)", fixtureFilename, err)
442 }
443 rep, err := Marshal(env)
444 if err != nil {
445 t.Errorf("Expected '%s' to Marshal (%v)", fixtureFilename, err)
446 }
447 roundtripped, err := Unmarshal(rep)
448 if err != nil {
449 t.Errorf("Expected '%s' to Mashal and Unmarshal (%v)", fixtureFilename, err)
450 }
451 if !reflect.DeepEqual(env, roundtripped) {
452 t.Errorf("Expected '%s' to roundtrip as '%v', got '%v' instead", fixtureFilename, env, roundtripped)
453 }
454
455 }
456 }
457
View as plain text