1# Copyright 2013 Donald Stufft
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# https://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15import os.path
16
17import pretend
18import pytest
19import requests
20
21from twine import exceptions
22from twine import utils
23
24from . import helpers
25
26
27def test_get_config(write_config_file):
28    config_file = write_config_file(
29        """
30        [distutils]
31        index-servers = pypi
32
33        [pypi]
34        username = testuser
35        password = testpassword
36        """
37    )
38
39    assert utils.get_config(config_file) == {
40        "pypi": {
41            "repository": utils.DEFAULT_REPOSITORY,
42            "username": "testuser",
43            "password": "testpassword",
44        },
45    }
46
47
48def test_get_config_no_distutils(write_config_file):
49    """Upload by default to PyPI if an index server is not set in .pypirc."""
50    config_file = write_config_file(
51        """
52        [pypi]
53        username = testuser
54        password = testpassword
55        """
56    )
57
58    assert utils.get_config(config_file) == {
59        "pypi": {
60            "repository": utils.DEFAULT_REPOSITORY,
61            "username": "testuser",
62            "password": "testpassword",
63        },
64        "testpypi": {
65            "repository": utils.TEST_REPOSITORY,
66            "username": None,
67            "password": None,
68        },
69    }
70
71
72def test_get_config_no_section(write_config_file):
73    config_file = write_config_file(
74        """
75        [distutils]
76        index-servers = pypi foo
77
78        [pypi]
79        username = testuser
80        password = testpassword
81        """
82    )
83
84    assert utils.get_config(config_file) == {
85        "pypi": {
86            "repository": utils.DEFAULT_REPOSITORY,
87            "username": "testuser",
88            "password": "testpassword",
89        },
90    }
91
92
93def test_get_config_override_pypi_url(write_config_file):
94    config_file = write_config_file(
95        """
96        [pypi]
97        repository = http://pypiproxy
98        """
99    )
100
101    assert utils.get_config(config_file)["pypi"]["repository"] == "http://pypiproxy"
102
103
104def test_get_config_missing(config_file):
105    assert utils.get_config(config_file) == {
106        "pypi": {
107            "repository": utils.DEFAULT_REPOSITORY,
108            "username": None,
109            "password": None,
110        },
111        "testpypi": {
112            "repository": utils.TEST_REPOSITORY,
113            "username": None,
114            "password": None,
115        },
116    }
117
118
119def test_empty_userpass(write_config_file):
120    """Suppress prompts if empty username and password are provided in .pypirc."""
121    config_file = write_config_file(
122        """
123        [pypi]
124        username=
125        password=
126        """
127    )
128
129    config = utils.get_config(config_file)
130    pypi = config["pypi"]
131
132    assert pypi["username"] == pypi["password"] == ""
133
134
135def test_get_repository_config_missing(config_file):
136    repository_url = "https://notexisting.python.org/pypi"
137    exp = {
138        "repository": repository_url,
139        "username": None,
140        "password": None,
141    }
142    assert utils.get_repository_from_config(config_file, "foo", repository_url) == exp
143    assert utils.get_repository_from_config(config_file, "pypi", repository_url) == exp
144
145    exp = {
146        "repository": utils.DEFAULT_REPOSITORY,
147        "username": None,
148        "password": None,
149    }
150    assert utils.get_repository_from_config(config_file, "pypi") == exp
151
152
153@pytest.mark.parametrize(
154    "repo_url, message",
155    [
156        (
157            "ftp://test.pypi.org",
158            r"scheme was required to be one of \['http', 'https'\]",
159        ),
160        ("https:/", "host was required but missing."),
161        ("//test.pypi.org", "scheme was required but missing."),
162        ("foo.bar", "host, scheme were required but missing."),
163    ],
164)
165def test_get_repository_config_with_invalid_url(config_file, repo_url, message):
166    """Raise an exception for a URL with an invalid/missing scheme and/or host."""
167    with pytest.raises(
168        exceptions.UnreachableRepositoryURLDetected,
169        match=message,
170    ):
171        utils.get_repository_from_config(config_file, "pypi", repo_url)
172
173
174def test_get_repository_config_missing_repository(write_config_file):
175    """Raise an exception when a custom repository isn't defined in .pypirc."""
176    config_file = write_config_file("")
177    with pytest.raises(
178        exceptions.InvalidConfiguration,
179        match="Missing 'missing-repository'",
180    ):
181        utils.get_repository_from_config(config_file, "missing-repository")
182
183
184@pytest.mark.parametrize("repository", ["pypi", "missing-repository"])
185def test_get_repository_config_missing_file(repository):
186    """Raise an exception when a custom config file doesn't exist."""
187    with pytest.raises(
188        exceptions.InvalidConfiguration,
189        match=r"No such file.*missing-file",
190    ):
191        utils.get_repository_from_config("missing-file", repository)
192
193
194def test_get_config_deprecated_pypirc():
195    tests_dir = os.path.dirname(os.path.abspath(__file__))
196    deprecated_pypirc_path = os.path.join(tests_dir, "fixtures", "deprecated-pypirc")
197
198    assert utils.get_config(deprecated_pypirc_path) == {
199        "pypi": {
200            "repository": utils.DEFAULT_REPOSITORY,
201            "username": "testusername",
202            "password": "testpassword",
203        },
204        "testpypi": {
205            "repository": utils.TEST_REPOSITORY,
206            "username": "testusername",
207            "password": "testpassword",
208        },
209    }
210
211
212@pytest.mark.parametrize(
213    ("cli_value", "config", "key", "strategy", "expected"),
214    (
215        ("cli", {}, "key", lambda: "fallback", "cli"),
216        (None, {"key": "value"}, "key", lambda: "fallback", "value"),
217        (None, {}, "key", lambda: "fallback", "fallback"),
218    ),
219)
220def test_get_userpass_value(cli_value, config, key, strategy, expected):
221    ret = utils.get_userpass_value(cli_value, config, key, strategy)
222    assert ret == expected
223
224
225@pytest.mark.parametrize(
226    ("env_name", "default", "environ", "expected"),
227    [
228        ("MY_PASSWORD", None, {}, None),
229        ("MY_PASSWORD", None, {"MY_PASSWORD": "foo"}, "foo"),
230        ("URL", "https://example.org", {}, "https://example.org"),
231        ("URL", "https://example.org", {"URL": "https://pypi.org"}, "https://pypi.org"),
232    ],
233)
234def test_default_to_environment_action(env_name, default, environ, expected):
235    option_strings = ("-x", "--example")
236    dest = "example"
237    with helpers.set_env(**environ):
238        action = utils.EnvironmentDefault(
239            env=env_name,
240            default=default,
241            option_strings=option_strings,
242            dest=dest,
243        )
244    assert action.env == env_name
245    assert action.default == expected
246
247
248@pytest.mark.parametrize(
249    "repo_url", ["https://pypi.python.org", "https://testpypi.python.org"]
250)
251def test_check_status_code_for_deprecated_pypi_url(repo_url):
252    response = pretend.stub(status_code=410, url=repo_url)
253
254    # value of Verbose doesn't matter for this check
255    with pytest.raises(exceptions.UploadToDeprecatedPyPIDetected):
256        utils.check_status_code(response, False)
257
258
259@pytest.mark.parametrize(
260    "repo_url",
261    ["https://pypi.python.org", "https://testpypi.python.org"],
262)
263@pytest.mark.parametrize(
264    "verbose",
265    [True, False],
266)
267def test_check_status_code_for_missing_status_code(
268    capsys, repo_url, verbose, make_settings
269):
270    """Print HTTP errors based on verbosity level."""
271    response = pretend.stub(
272        status_code=403,
273        url=repo_url,
274        raise_for_status=pretend.raiser(requests.HTTPError),
275        text="Forbidden",
276    )
277
278    make_settings(verbose=verbose)
279
280    with pytest.raises(requests.HTTPError):
281        utils.check_status_code(response, verbose)
282
283    captured = capsys.readouterr()
284
285    if verbose:
286        assert captured.out.count("Content received from server:\nForbidden\n") == 1
287    else:
288        assert captured.out.count("--verbose option") == 1
289
290
291@pytest.mark.parametrize(
292    ("size_in_bytes, formatted_size"),
293    [(3704, "3.6 KB"), (1153433, "1.1 MB"), (21412841, "20.4 MB")],
294)
295def test_get_file_size(size_in_bytes, formatted_size, monkeypatch):
296    """Get the size of file as a string with units."""
297    monkeypatch.setattr(os.path, "getsize", lambda _: size_in_bytes)
298
299    file_size = utils.get_file_size(size_in_bytes)
300
301    assert file_size == formatted_size
302