Skip to content

Commit f5420ab

Browse files
committed
added encryption
1 parent 6a02ef5 commit f5420ab

2 files changed

Lines changed: 398 additions & 0 deletions

File tree

src/dotenv/crypt.py

Whitespace-only changes.

tests/test_secret_key.py

Lines changed: 398 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,398 @@
1+
import io
2+
import logging
3+
import os
4+
import sys
5+
import textwrap
6+
from unittest import mock
7+
8+
import pytest
9+
import sh
10+
11+
import dotenv
12+
13+
14+
def test_set_key_no_file(tmp_path):
15+
nx_path = tmp_path / "nx"
16+
logger = logging.getLogger("dotenv.main")
17+
18+
with mock.patch.object(logger, "warning"):
19+
result = dotenv.set_key(nx_path, "foo", "bar")
20+
21+
assert result == (True, "foo", "bar")
22+
assert nx_path.exists()
23+
24+
25+
@pytest.mark.parametrize(
26+
"before,key,value,expected,after",
27+
[
28+
("", "a", "", (True, "a", ""), "a=''\n"),
29+
("", "a", "b", (True, "a", "b"), "a='b'\n"),
30+
("", "a", "'b'", (True, "a", "'b'"), "a='\\'b\\''\n"),
31+
("", "a", '"b"', (True, "a", '"b"'), "a='\"b\"'\n"),
32+
("", "a", "b'c", (True, "a", "b'c"), "a='b\\'c'\n"),
33+
("", "a", 'b"c', (True, "a", 'b"c'), "a='b\"c'\n"),
34+
("a=b", "a", "c", (True, "a", "c"), "a='c'\n"),
35+
("a=b\n", "a", "c", (True, "a", "c"), "a='c'\n"),
36+
("a=b\n\n", "a", "c", (True, "a", "c"), "a='c'\n\n"),
37+
("a=b\nc=d", "a", "e", (True, "a", "e"), "a='e'\nc=d"),
38+
("a=b\nc=d\ne=f", "c", "g", (True, "c", "g"), "a=b\nc='g'\ne=f"),
39+
("a=b\n", "c", "d", (True, "c", "d"), "a=b\nc='d'\n"),
40+
("a=b", "c", "d", (True, "c", "d"), "a=b\nc='d'\n"),
41+
],
42+
)
43+
def test_set_key(dotenv_path, before, key, value, expected, after):
44+
logger = logging.getLogger("dotenv.main")
45+
dotenv_path.write_text(before)
46+
47+
with mock.patch.object(logger, "warning") as mock_warning:
48+
result = dotenv.set_key(dotenv_path, key, value)
49+
50+
assert result == expected
51+
assert dotenv_path.read_text() == after
52+
mock_warning.assert_not_called()
53+
54+
55+
def test_set_key_encoding(dotenv_path):
56+
encoding = "latin-1"
57+
58+
result = dotenv.set_key(dotenv_path, "a", "é", encoding=encoding)
59+
60+
assert result == (True, "a", "é")
61+
assert dotenv_path.read_text(encoding=encoding) == "a='é'\n"
62+
63+
64+
def test_set_key_permission_error(dotenv_path):
65+
dotenv_path.chmod(0o000)
66+
67+
with pytest.raises(Exception):
68+
dotenv.set_key(dotenv_path, "a", "b")
69+
70+
dotenv_path.chmod(0o600)
71+
assert dotenv_path.read_text() == ""
72+
73+
74+
def test_get_key_no_file(tmp_path):
75+
nx_path = tmp_path / "nx"
76+
logger = logging.getLogger("dotenv.main")
77+
78+
with (
79+
mock.patch.object(logger, "info") as mock_info,
80+
mock.patch.object(logger, "warning") as mock_warning,
81+
):
82+
result = dotenv.get_key(nx_path, "foo")
83+
84+
assert result is None
85+
mock_info.assert_has_calls(
86+
calls=[
87+
mock.call("python-dotenv could not find configuration file %s.", nx_path)
88+
],
89+
)
90+
mock_warning.assert_has_calls(
91+
calls=[mock.call("Key %s not found in %s.", "foo", nx_path)],
92+
)
93+
94+
95+
def test_get_key_not_found(dotenv_path):
96+
logger = logging.getLogger("dotenv.main")
97+
98+
with mock.patch.object(logger, "warning") as mock_warning:
99+
result = dotenv.get_key(dotenv_path, "foo")
100+
101+
assert result is None
102+
mock_warning.assert_called_once_with("Key %s not found in %s.", "foo", dotenv_path)
103+
104+
105+
def test_get_key_ok(dotenv_path):
106+
logger = logging.getLogger("dotenv.main")
107+
dotenv_path.write_text("foo=bar")
108+
109+
with mock.patch.object(logger, "warning") as mock_warning:
110+
result = dotenv.get_key(dotenv_path, "foo")
111+
112+
assert result == "bar"
113+
mock_warning.assert_not_called()
114+
115+
116+
def test_get_key_encoding(dotenv_path):
117+
encoding = "latin-1"
118+
dotenv_path.write_text("é=è", encoding=encoding)
119+
120+
result = dotenv.get_key(dotenv_path, "é", encoding=encoding)
121+
122+
assert result == "è"
123+
124+
125+
def test_get_key_none(dotenv_path):
126+
logger = logging.getLogger("dotenv.main")
127+
dotenv_path.write_text("foo")
128+
129+
with mock.patch.object(logger, "warning") as mock_warning:
130+
result = dotenv.get_key(dotenv_path, "foo")
131+
132+
assert result is None
133+
mock_warning.assert_not_called()
134+
135+
136+
def test_unset_with_value(dotenv_path):
137+
logger = logging.getLogger("dotenv.main")
138+
dotenv_path.write_text("a=b\nc=d")
139+
140+
with mock.patch.object(logger, "warning") as mock_warning:
141+
result = dotenv.unset_key(dotenv_path, "a")
142+
143+
assert result == (True, "a")
144+
assert dotenv_path.read_text() == "c=d"
145+
mock_warning.assert_not_called()
146+
147+
148+
def test_unset_no_value(dotenv_path):
149+
logger = logging.getLogger("dotenv.main")
150+
dotenv_path.write_text("foo")
151+
152+
with mock.patch.object(logger, "warning") as mock_warning:
153+
result = dotenv.unset_key(dotenv_path, "foo")
154+
155+
assert result == (True, "foo")
156+
assert dotenv_path.read_text() == ""
157+
mock_warning.assert_not_called()
158+
159+
160+
def test_unset_encoding(dotenv_path):
161+
encoding = "latin-1"
162+
dotenv_path.write_text("é=x", encoding=encoding)
163+
164+
result = dotenv.unset_key(dotenv_path, "é", encoding=encoding)
165+
166+
assert result == (True, "é")
167+
assert dotenv_path.read_text(encoding=encoding) == ""
168+
169+
170+
def test_set_key_unauthorized_file(dotenv_path):
171+
dotenv_path.chmod(0o000)
172+
173+
with pytest.raises(PermissionError):
174+
dotenv.set_key(dotenv_path, "a", "x")
175+
176+
177+
def test_unset_non_existent_file(tmp_path):
178+
nx_path = tmp_path / "nx"
179+
logger = logging.getLogger("dotenv.main")
180+
181+
with mock.patch.object(logger, "warning") as mock_warning:
182+
result = dotenv.unset_key(nx_path, "foo")
183+
184+
assert result == (None, "foo")
185+
mock_warning.assert_called_once_with(
186+
"Can't delete from %s - it doesn't exist.",
187+
nx_path,
188+
)
189+
190+
191+
def prepare_file_hierarchy(path):
192+
"""
193+
Create a temporary folder structure like the following:
194+
195+
test_find_dotenv0/
196+
└── child1
197+
├── child2
198+
│   └── child3
199+
│   └── child4
200+
└── .env
201+
202+
Then try to automatically `find_dotenv` starting in `child4`
203+
"""
204+
205+
leaf = path / "child1" / "child2" / "child3" / "child4"
206+
leaf.mkdir(parents=True, exist_ok=True)
207+
return leaf
208+
209+
210+
def test_find_dotenv_no_file_raise(tmp_path):
211+
leaf = prepare_file_hierarchy(tmp_path)
212+
os.chdir(leaf)
213+
214+
with pytest.raises(IOError):
215+
dotenv.find_dotenv(raise_error_if_not_found=True, usecwd=True)
216+
217+
218+
def test_find_dotenv_no_file_no_raise(tmp_path):
219+
leaf = prepare_file_hierarchy(tmp_path)
220+
os.chdir(leaf)
221+
222+
result = dotenv.find_dotenv(usecwd=True)
223+
224+
assert result == ""
225+
226+
227+
def test_find_dotenv_found(tmp_path):
228+
leaf = prepare_file_hierarchy(tmp_path)
229+
os.chdir(leaf)
230+
dotenv_path = tmp_path / ".env"
231+
dotenv_path.write_bytes(b"TEST=test\n")
232+
233+
result = dotenv.find_dotenv(usecwd=True)
234+
235+
assert result == str(dotenv_path)
236+
237+
238+
@mock.patch.dict(os.environ, {}, clear=True)
239+
def test_load_dotenv_existing_file(dotenv_path):
240+
dotenv_path.write_text("a=b")
241+
242+
result = dotenv.load_dotenv(dotenv_path)
243+
244+
assert result is True
245+
assert os.environ == {"a": "b"}
246+
247+
248+
def test_load_dotenv_no_file_verbose():
249+
logger = logging.getLogger("dotenv.main")
250+
251+
with mock.patch.object(logger, "info") as mock_info:
252+
result = dotenv.load_dotenv(".does_not_exist", verbose=True)
253+
254+
assert result is False
255+
mock_info.assert_called_once_with(
256+
"python-dotenv could not find configuration file %s.", ".does_not_exist"
257+
)
258+
259+
260+
@mock.patch.dict(os.environ, {"a": "c"}, clear=True)
261+
def test_load_dotenv_existing_variable_no_override(dotenv_path):
262+
dotenv_path.write_text("a=b")
263+
264+
result = dotenv.load_dotenv(dotenv_path, override=False)
265+
266+
assert result is True
267+
assert os.environ == {"a": "c"}
268+
269+
270+
@mock.patch.dict(os.environ, {"a": "c"}, clear=True)
271+
def test_load_dotenv_existing_variable_override(dotenv_path):
272+
dotenv_path.write_text("a=b")
273+
274+
result = dotenv.load_dotenv(dotenv_path, override=True)
275+
276+
assert result is True
277+
assert os.environ == {"a": "b"}
278+
279+
280+
@mock.patch.dict(os.environ, {"a": "c"}, clear=True)
281+
def test_load_dotenv_redefine_var_used_in_file_no_override(dotenv_path):
282+
dotenv_path.write_text('a=b\nd="${a}"')
283+
284+
result = dotenv.load_dotenv(dotenv_path)
285+
286+
assert result is True
287+
assert os.environ == {"a": "c", "d": "c"}
288+
289+
290+
@mock.patch.dict(os.environ, {"a": "c"}, clear=True)
291+
def test_load_dotenv_redefine_var_used_in_file_with_override(dotenv_path):
292+
dotenv_path.write_text('a=b\nd="${a}"')
293+
294+
result = dotenv.load_dotenv(dotenv_path, override=True)
295+
296+
assert result is True
297+
assert os.environ == {"a": "b", "d": "b"}
298+
299+
300+
@mock.patch.dict(os.environ, {}, clear=True)
301+
def test_load_dotenv_string_io_utf_8():
302+
stream = io.StringIO("a=à")
303+
304+
result = dotenv.load_dotenv(stream=stream)
305+
306+
assert result is True
307+
assert os.environ == {"a": "à"}
308+
309+
310+
@mock.patch.dict(os.environ, {}, clear=True)
311+
def test_load_dotenv_file_stream(dotenv_path):
312+
dotenv_path.write_text("a=b")
313+
314+
with dotenv_path.open() as f:
315+
result = dotenv.load_dotenv(stream=f)
316+
317+
assert result is True
318+
assert os.environ == {"a": "b"}
319+
320+
321+
def test_load_dotenv_in_current_dir(tmp_path):
322+
dotenv_path = tmp_path / ".env"
323+
dotenv_path.write_bytes(b"a=b")
324+
code_path = tmp_path / "code.py"
325+
code_path.write_text(
326+
textwrap.dedent("""
327+
import dotenv
328+
import os
329+
330+
dotenv.load_dotenv(verbose=True)
331+
print(os.environ['a'])
332+
""")
333+
)
334+
os.chdir(tmp_path)
335+
336+
result = sh.Command(sys.executable)(code_path)
337+
338+
assert result == "b\n"
339+
340+
341+
def test_dotenv_values_file(dotenv_path):
342+
dotenv_path.write_text("a=b")
343+
344+
result = dotenv.dotenv_values(dotenv_path)
345+
346+
assert result == {"a": "b"}
347+
348+
349+
@pytest.mark.parametrize(
350+
"env,string,interpolate,expected",
351+
[
352+
# Defined in environment, with and without interpolation
353+
({"b": "c"}, "a=$b", False, {"a": "$b"}),
354+
({"b": "c"}, "a=$b", True, {"a": "$b"}),
355+
({"b": "c"}, "a=${b}", False, {"a": "${b}"}),
356+
({"b": "c"}, "a=${b}", True, {"a": "c"}),
357+
({"b": "c"}, "a=${b:-d}", False, {"a": "${b:-d}"}),
358+
({"b": "c"}, "a=${b:-d}", True, {"a": "c"}),
359+
# Defined in file
360+
({}, "b=c\na=${b}", True, {"a": "c", "b": "c"}),
361+
# Undefined
362+
({}, "a=${b}", True, {"a": ""}),
363+
({}, "a=${b:-d}", True, {"a": "d"}),
364+
# With quotes
365+
({"b": "c"}, 'a="${b}"', True, {"a": "c"}),
366+
({"b": "c"}, "a='${b}'", True, {"a": "c"}),
367+
# With surrounding text
368+
({"b": "c"}, "a=x${b}y", True, {"a": "xcy"}),
369+
# Self-referential
370+
({"a": "b"}, "a=${a}", True, {"a": "b"}),
371+
({}, "a=${a}", True, {"a": ""}),
372+
({"a": "b"}, "a=${a:-c}", True, {"a": "b"}),
373+
({}, "a=${a:-c}", True, {"a": "c"}),
374+
# Reused
375+
({"b": "c"}, "a=${b}${b}", True, {"a": "cc"}),
376+
# Re-defined and used in file
377+
({"b": "c"}, "b=d\na=${b}", True, {"a": "d", "b": "d"}),
378+
({}, "a=b\na=c\nd=${a}", True, {"a": "c", "d": "c"}),
379+
({}, "a=b\nc=${a}\nd=e\nc=${d}", True, {"a": "b", "c": "e", "d": "e"}),
380+
],
381+
)
382+
def test_dotenv_values_string_io(env, string, interpolate, expected):
383+
with mock.patch.dict(os.environ, env, clear=True):
384+
stream = io.StringIO(string)
385+
stream.seek(0)
386+
387+
result = dotenv.dotenv_values(stream=stream, interpolate=interpolate)
388+
389+
assert result == expected
390+
391+
392+
def test_dotenv_values_file_stream(dotenv_path):
393+
dotenv_path.write_text("a=b")
394+
395+
with dotenv_path.open() as f:
396+
result = dotenv.dotenv_values(stream=f)
397+
398+
assert result == {"a": "b"}

0 commit comments

Comments
 (0)