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