gotenv_test.go 8.4 KB

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