...

Source file src/github.com/joho/godotenv/godotenv_test.go

Documentation: github.com/joho/godotenv

     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  	// first up, clear the env
    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  	// ensure NO overload
   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  	// ensure NO overload
   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  	// unquoted values
   286  	parseAndCompare(t, "FOO=bar", "FOO", "bar")
   287  
   288  	// parses values with spaces around equal sign
   289  	parseAndCompare(t, "FOO =bar", "FOO", "bar")
   290  	parseAndCompare(t, "FOO= bar", "FOO", "bar")
   291  
   292  	// parses double quoted values
   293  	parseAndCompare(t, `FOO="bar"`, "FOO", "bar")
   294  
   295  	// parses single quoted values
   296  	parseAndCompare(t, "FOO='bar'", "FOO", "bar")
   297  
   298  	// parses escaped double quotes
   299  	parseAndCompare(t, `FOO="escaped\"bar"`, "FOO", `escaped"bar`)
   300  
   301  	// parses single quotes inside double quotes
   302  	parseAndCompare(t, `FOO="'d'"`, "FOO", `'d'`)
   303  
   304  	// parses yaml style options
   305  	parseAndCompare(t, "OPTION_A: 1", "OPTION_A", "1")
   306  
   307  	//parses yaml values with equal signs
   308  	parseAndCompare(t, "OPTION_A: Foo=bar", "OPTION_A", "Foo=bar")
   309  
   310  	// parses non-yaml options with colons
   311  	parseAndCompare(t, "OPTION_A=1:B", "OPTION_A", "1:B")
   312  
   313  	// parses export keyword
   314  	parseAndCompare(t, "export OPTION_A=2", "OPTION_A", "2")
   315  	parseAndCompare(t, `export OPTION_B='\n'`, "OPTION_B", "\\n")
   316  
   317  	// it 'expands newlines in quoted strings' do
   318  	// expect(env('FOO="bar\nbaz"')).to eql('FOO' => "bar\nbaz")
   319  	parseAndCompare(t, `FOO="bar\nbaz"`, "FOO", "bar\nbaz")
   320  
   321  	// it 'parses varibales with "." in the name' do
   322  	// expect(env('FOO.BAR=foobar')).to eql('FOO.BAR' => 'foobar')
   323  	parseAndCompare(t, "FOO.BAR=foobar", "FOO.BAR", "foobar")
   324  
   325  	// it 'parses varibales with several "=" in the value' do
   326  	// expect(env('FOO=foobar=')).to eql('FOO' => 'foobar=')
   327  	parseAndCompare(t, "FOO=foobar=", "FOO", "foobar=")
   328  
   329  	// it 'strips unquoted values' do
   330  	// expect(env('foo=bar ')).to eql('foo' => 'bar') # not 'bar '
   331  	parseAndCompare(t, "FOO=bar ", "FOO", "bar")
   332  
   333  	// it 'ignores inline comments' do
   334  	// expect(env("foo=bar # this is foo")).to eql('foo' => 'bar')
   335  	parseAndCompare(t, "FOO=bar # this is foo", "FOO", "bar")
   336  
   337  	// it 'allows # in quoted value' do
   338  	// expect(env('foo="bar#baz" # comment')).to eql('foo' => 'bar#baz')
   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  	// it 'parses # in quoted values' do
   344  	// expect(env('foo="ba#r"')).to eql('foo' => 'ba#r')
   345  	// expect(env("foo='ba#r'")).to eql('foo' => 'ba#r')
   346  	parseAndCompare(t, `FOO="ba#r"`, "FOO", "ba#r")
   347  	parseAndCompare(t, "FOO='ba#r'", "FOO", "ba#r")
   348  
   349  	//newlines and backslashes should be escaped
   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  	// it 'throws an error if line format is incorrect' do
   359  	// expect{env('lol$wut')}.to raise_error(Dotenv::FormatError)
   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  	// it 'ignores empty lines' do
   369  	// expect(env("\n \t  \nfoo=bar\n \nfizz=buzz")).to eql('foo' => 'bar', 'fizz' => 'buzz')
   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  	// it 'ignores comment lines' do
   379  	// expect(env("\n\n\n # HERE GOES FOO \nfoo=bar")).to eql('foo' => 'bar')
   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  	// make sure we're not getting false positives
   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  	//just test some single lines to show the general idea
   420  	//TestRoundtrip makes most of the good assertions
   421  
   422  	//values are always double-quoted
   423  	writeAndCompare(`key=value`, `key="value"`)
   424  	//double-quotes are escaped
   425  	writeAndCompare(`key=va"lu"e`, `key="va\"lu\"e"`)
   426  	//but single quotes are left alone
   427  	writeAndCompare(`key=va'lu'e`, `key="va'lu'e"`)
   428  	// newlines, backslashes, and some other special chars are escaped
   429  	writeAndCompare(`foo="\n\r\\r!"`, `foo="\n\r\\r\!"`)
   430  	// lines should be sorted
   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