...

Source file src/github.com/subosito/gotenv/gotenv_test.go

Documentation: github.com/subosito/gotenv

     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  	// parses unquoted values
    21  	{`FOO=bar`, gotenv.Env{"FOO": "bar"}, false},
    22  
    23  	// parses values with spaces around equal sign
    24  	{`FOO =bar`, gotenv.Env{"FOO": "bar"}, false},
    25  	{`FOO= bar`, gotenv.Env{"FOO": "bar"}, false},
    26  
    27  	// parses values with leading spaces
    28  	{`  FOO=bar`, gotenv.Env{"FOO": "bar"}, false},
    29  
    30  	// parses values with following spaces
    31  	{`FOO=bar  `, gotenv.Env{"FOO": "bar"}, false},
    32  
    33  	// parses double quoted values
    34  	{`FOO="bar"`, gotenv.Env{"FOO": "bar"}, false},
    35  
    36  	// parses double quoted values with following spaces
    37  	{`FOO="bar"  `, gotenv.Env{"FOO": "bar"}, false},
    38  
    39  	// parses single quoted values
    40  	{`FOO='bar'`, gotenv.Env{"FOO": "bar"}, false},
    41  
    42  	// parses single quoted values with following spaces
    43  	{`FOO='bar'  `, gotenv.Env{"FOO": "bar"}, false},
    44  
    45  	// parses escaped double quotes
    46  	{`FOO="escaped\"bar"`, gotenv.Env{"FOO": `escaped"bar`}, false},
    47  
    48  	// parses empty values
    49  	{`FOO=`, gotenv.Env{"FOO": ""}, false},
    50  
    51  	// expands variables found in values
    52  	{"FOO=test\nBAR=$FOO", gotenv.Env{"FOO": "test", "BAR": "test"}, false},
    53  
    54  	// parses variables wrapped in brackets
    55  	{"FOO=test\nBAR=${FOO}bar", gotenv.Env{"FOO": "test", "BAR": "testbar"}, false},
    56  
    57  	// reads variables from ENV when expanding if not found in local env
    58  	{`BAR=$FOO`, gotenv.Env{"BAR": "test"}, true},
    59  
    60  	// expands undefined variables to an empty string
    61  	{`BAR=$FOO`, gotenv.Env{"BAR": ""}, false},
    62  
    63  	// expands variables in quoted strings
    64  	{"FOO=test\nBAR=\"quote $FOO\"", gotenv.Env{"FOO": "test", "BAR": "quote test"}, false},
    65  
    66  	// does not expand variables in single quoted strings
    67  	{"BAR='quote $FOO'", gotenv.Env{"BAR": "quote $FOO"}, false},
    68  
    69  	// does not expand escaped variables
    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  	// parses yaml style options
    75  	{"OPTION_A: 1", gotenv.Env{"OPTION_A": "1"}, false},
    76  
    77  	// parses export keyword
    78  	{"export OPTION_A=2", gotenv.Env{"OPTION_A": "2"}, false},
    79  
    80  	// allows export line if you want to do it that way
    81  	{"OPTION_A=2\nexport OPTION_A", gotenv.Env{"OPTION_A": "2"}, false},
    82  
    83  	// expands newlines in quoted strings
    84  	{`FOO="bar\nbaz"`, gotenv.Env{"FOO": "bar\nbaz"}, false},
    85  
    86  	// parses variables with "." in the name
    87  	{`FOO.BAR=foobar`, gotenv.Env{"FOO.BAR": "foobar"}, false},
    88  
    89  	// strips unquoted values
    90  	{`foo=bar `, gotenv.Env{"foo": "bar"}, false}, // not 'bar '
    91  
    92  	// ignores empty lines
    93  	{"\n \t  \nfoo=bar\n \nfizz=buzz", gotenv.Env{"foo": "bar", "fizz": "buzz"}, false},
    94  
    95  	// ignores inline comments
    96  	{"foo=bar # this is foo", gotenv.Env{"foo": "bar"}, false},
    97  
    98  	// allows # in quoted value
    99  	{`foo="bar#baz" # comment`, gotenv.Env{"foo": "bar#baz"}, false},
   100  
   101  	// ignores comment lines
   102  	{"\n\n\n # HERE GOES FOO \nfoo=bar", gotenv.Env{"foo": "bar"}, false},
   103  
   104  	// parses # in quoted values
   105  	{`foo="ba#r"`, gotenv.Env{"foo": "ba#r"}, false},
   106  	{"foo='ba#r'", gotenv.Env{"foo": "ba#r"}, false},
   107  
   108  	// parses # in quoted values with following spaces
   109  	{`foo="ba#r"  `, gotenv.Env{"foo": "ba#r"}, false},
   110  	{`foo='ba#r'  `, gotenv.Env{"foo": "ba#r"}, false},
   111  
   112  	// supports carriage return
   113  	{"FOO=bar\rbaz=fbb", gotenv.Env{"FOO": "bar", "baz": "fbb"}, false},
   114  
   115  	// supports carriage return combine with new line
   116  	{"FOO=bar\r\nbaz=fbb", gotenv.Env{"FOO": "bar", "baz": "fbb"}, false},
   117  
   118  	// expands carriage return in quoted strings
   119  	{`FOO="bar\rbaz"`, gotenv.Env{"FOO": "bar\rbaz"}, false},
   120  
   121  	// escape $ properly when no alphabets/numbers/_  are followed by it
   122  	{`FOO="bar\\$ \\$\\$"`, gotenv.Env{"FOO": "bar$ $$"}, false},
   123  
   124  	// ignore $ when it is not escaped and no variable is followed by it
   125  	{`FOO="bar $ "`, gotenv.Env{"FOO": "bar $ "}, false},
   126  
   127  	// parses unquoted values with spaces after separator
   128  	{`FOO= bar`, gotenv.Env{"FOO": "bar"}, false},
   129  
   130  	// allows # in quoted value with spaces after separator
   131  	{`foo= "bar#baz" # comment`, gotenv.Env{"foo": "bar#baz"}, false},
   132  
   133  	// allows = in double quoted values with newlines (typically base64 padding)
   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  	// allows export line if you want to do it that way and checks for unset variables
   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  	// throws an error if line format is incorrect
   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  	// The value for D is not "fromFile" because C is defined after the
   451  	// definition of D.
   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