diff --git a/src/nox_poetry/poetry.py b/src/nox_poetry/poetry.py index e56299fe..8f04fd23 100644 --- a/src/nox_poetry/poetry.py +++ b/src/nox_poetry/poetry.py @@ -85,6 +85,7 @@ def export(self) -> str: "--dev", *[f"--extras={extra}" for extra in self.config.extras], "--without-hashes", + "--with-credentials", external=True, silent=True, stderr=None, diff --git a/src/nox_poetry/sessions.py b/src/nox_poetry/sessions.py index 9d451147..f0c84333 100644 --- a/src/nox_poetry/sessions.py +++ b/src/nox_poetry/sessions.py @@ -58,6 +58,8 @@ def _split_extras(arg: str) -> Tuple[str, Optional[str]]: def to_constraint(requirement_string: str, line: int) -> Optional[str]: """Convert requirement to constraint.""" + if requirement_string.startswith("--extra-index-url"): + return requirement_string if any( requirement_string.startswith(prefix) for prefix in ("-", "file://", "git+https://", "http://", "https://") diff --git a/tests/functional/data/simple503/index.html b/tests/functional/data/simple503/index.html new file mode 100644 index 00000000..9fc90ddc --- /dev/null +++ b/tests/functional/data/simple503/index.html @@ -0,0 +1,17 @@ + + + + + + + + Simple Package Repository + + + + + thispackagedoesnotexist + +
+ + diff --git a/tests/functional/data/simple503/thispackagedoesnotexist-0.1.0-py2.py3-none-any.whl b/tests/functional/data/simple503/thispackagedoesnotexist-0.1.0-py2.py3-none-any.whl new file mode 100644 index 00000000..433d59c7 Binary files /dev/null and b/tests/functional/data/simple503/thispackagedoesnotexist-0.1.0-py2.py3-none-any.whl differ diff --git a/tests/functional/data/simple503/thispackagedoesnotexist-0.1.0-py2.py3-none-any.whl.metadata b/tests/functional/data/simple503/thispackagedoesnotexist-0.1.0-py2.py3-none-any.whl.metadata new file mode 100644 index 00000000..1ef725f1 --- /dev/null +++ b/tests/functional/data/simple503/thispackagedoesnotexist-0.1.0-py2.py3-none-any.whl.metadata @@ -0,0 +1,14 @@ +Metadata-Version: 2.1 +Name: thispackagedoesnotexist +Version: 0.1.0 +Summary: thispackagedoesnotexist +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.4 +Classifier: Programming Language :: Python :: 3.5 +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 diff --git a/tests/functional/data/simple503/thispackagedoesnotexist/index.html b/tests/functional/data/simple503/thispackagedoesnotexist/index.html new file mode 100644 index 00000000..44a5c5bd --- /dev/null +++ b/tests/functional/data/simple503/thispackagedoesnotexist/index.html @@ -0,0 +1,20 @@ + + + + + + + + Links for thispackagedoesnotexist + + + +

+ Links for thispackagedoesnotexist +

+ + thispackagedoesnotexist-0.1.0-py2.py3-none-any.whl + +
+ + diff --git a/tests/functional/test_installroot.py b/tests/functional/test_installroot.py index 4daf07a1..097a9a76 100644 --- a/tests/functional/test_installroot.py +++ b/tests/functional/test_installroot.py @@ -1,4 +1,15 @@ """Functional tests for ``installroot``.""" +import base64 +import os +import tempfile +from functools import partial +from http.server import HTTPServer +from http.server import SimpleHTTPRequestHandler +from pathlib import Path +from threading import Thread +from typing import Any +from typing import Tuple + import nox_poetry from tests.functional.conftest import Project from tests.functional.conftest import list_packages @@ -79,3 +90,134 @@ def test(session: nox_poetry.Session) -> None: packages = list_packages(project, test) assert set(expected) == set(packages) + + +class AuthenticatingSimpleHTTPRequestHandler(SimpleHTTPRequestHandler): + """A version of SimpleHTTPRequestHandler that throws a 401 error if the request + does not come with the specified username and password sent via basic http + authentication. See RFC 7617 for details. This is designed for tests, and does not + offer any real protection.""" + + def __init__( + self, + request: Any, + client_address: Any, + server: Any, + directory: Any, + username: str, + password: str, + ): + authstring = f"{username}:{password}" + self.encoded_authstring = base64.b64encode(authstring.encode("utf-8")).decode( + "utf-8" + ) + super().__init__(request, client_address, server, directory=directory) + + def is_authenticated(self) -> bool: + if "Authorization" in self.headers: + return bool( + self.headers["Authorization"] == f"Basic {self.encoded_authstring}" + ) + return False + + def send_auth_error(self) -> None: + self.send_response(401) + self.send_header("WWW-Authenticate", 'Basic realm="everything"') + self.end_headers() + + def do_GET(self) -> None: + if self.is_authenticated(): + super().do_GET() + else: + self.send_auth_error() + + def do_HEAD(self) -> None: + if self.is_authenticated(): + super().do_HEAD() + else: + self.send_auth_error() + + +def get_pyproject(address: str) -> str: + return f"""\ +[tool.poetry] +name = "foo" +version = "0.1.1" +description = "foo" +authors = [] + +[tool.poetry.dependencies] +"thispackagedoesnotexist" = {{version = "0.1.0", source = "baz"}} + +[[tool.poetry.source]] +name = "baz" +url = "{address}" +default = false +secondary = true +""" + + +def serve_directory_with_http_and_auth( + directory: str, username: str, password: str +) -> Tuple[HTTPServer, str]: + hostname = "localhost" + port = 0 + handler = partial( + AuthenticatingSimpleHTTPRequestHandler, + directory=directory, + username=username, + password=password, + ) + httpd = HTTPServer((hostname, port), handler, False) + httpd.timeout = 0.5 + + httpd.server_bind() + address = "http://%s:%d" % (hostname, httpd.server_port) + + httpd.server_activate() + + def serve_forever(httpd: HTTPServer) -> None: + with httpd: # to make sure httpd.server_close is called + httpd.serve_forever() + + thread = Thread(target=serve_forever, args=(httpd,)) + thread.setDaemon(True) + thread.start() + + return httpd, address + + +def test_dependency_from_private_index(shared_datadir: Path) -> None: + input_dir = tempfile.TemporaryDirectory() + + server, address = serve_directory_with_http_and_auth( + str(shared_datadir / "simple503"), username="alice", password="password" + ) + + with open(os.path.join(input_dir.name, "pyproject.toml"), "w") as pyproject_file: + pyproject_file.write(get_pyproject(address)) + (Path(input_dir.name) / "foo").mkdir() + (Path(input_dir.name) / "foo" / "__init__.py").touch() + + @nox_poetry.session + def test(session: nox_poetry.Session) -> None: + session.run_always( + "poetry", + "config", + "http-basic.baz", + "alice", + "password", + external=True, + silent=True, + stderr=None, + ) + session.run_always("poetry", "lock") + session.poetry.installroot() + + project = Project(Path(input_dir.name)) + + try: + run_nox_with_noxfile(project, [test], [nox_poetry]) + + finally: + server.shutdown() diff --git a/tests/unit/data/simple503/index.html b/tests/unit/data/simple503/index.html new file mode 100644 index 00000000..9fc90ddc --- /dev/null +++ b/tests/unit/data/simple503/index.html @@ -0,0 +1,17 @@ + + + + + + + + Simple Package Repository + + + + + thispackagedoesnotexist + +
+ + diff --git a/tests/unit/data/simple503/thispackagedoesnotexist-0.1.0-py2.py3-none-any.whl b/tests/unit/data/simple503/thispackagedoesnotexist-0.1.0-py2.py3-none-any.whl new file mode 100644 index 00000000..433d59c7 Binary files /dev/null and b/tests/unit/data/simple503/thispackagedoesnotexist-0.1.0-py2.py3-none-any.whl differ diff --git a/tests/unit/data/simple503/thispackagedoesnotexist-0.1.0-py2.py3-none-any.whl.metadata b/tests/unit/data/simple503/thispackagedoesnotexist-0.1.0-py2.py3-none-any.whl.metadata new file mode 100644 index 00000000..1ef725f1 --- /dev/null +++ b/tests/unit/data/simple503/thispackagedoesnotexist-0.1.0-py2.py3-none-any.whl.metadata @@ -0,0 +1,14 @@ +Metadata-Version: 2.1 +Name: thispackagedoesnotexist +Version: 0.1.0 +Summary: thispackagedoesnotexist +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.4 +Classifier: Programming Language :: Python :: 3.5 +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 diff --git a/tests/unit/data/simple503/thispackagedoesnotexist/index.html b/tests/unit/data/simple503/thispackagedoesnotexist/index.html new file mode 100644 index 00000000..ceaef7f2 --- /dev/null +++ b/tests/unit/data/simple503/thispackagedoesnotexist/index.html @@ -0,0 +1,20 @@ + + + + + + + + Links for thispackagedoesnotexist + + + +

+ Links for thispackagedoesnotexist +

+ + thispackagedoesnotexist-0.1.0-py2.py3-none-any.whl + +
+ + diff --git a/tests/unit/test_poetry.py b/tests/unit/test_poetry.py index d7ce94f3..c6451d7f 100644 --- a/tests/unit/test_poetry.py +++ b/tests/unit/test_poetry.py @@ -1,7 +1,15 @@ """Unit tests for the poetry module.""" +import os +import pdb +import tempfile +from functools import partial +from http.server import HTTPServer +from http.server import SimpleHTTPRequestHandler from pathlib import Path +from threading import Thread from typing import Any from typing import Dict +from typing import Tuple import nox._options import nox.command @@ -61,3 +69,78 @@ def _run(*args: Any, **kwargs: Any) -> str: output = poetry.Poetry(session).export() assert output == requirements + + +def get_pyproject(address: str) -> str: + return f"""\ +[tool.poetry] +name = "foo" +version = "0.1.0" +description = "foo" +authors = [] + +[tool.poetry.dependencies] +"thispackagedoesnotexist" = {{version = "0.1.0", source = "baz"}} + +[[tool.poetry.source]] +name = "baz" +url = "{address}" +default = false +secondary = true +""" + + +def serve_directory_with_http(directory: str) -> Tuple[HTTPServer, str]: + hostname = "localhost" + port = 0 + handler = partial(SimpleHTTPRequestHandler, directory=directory) + httpd = HTTPServer((hostname, port), handler, False) + httpd.timeout = 0.5 + + httpd.server_bind() + address = "http://%s:%d" % (hostname, httpd.server_port) + + httpd.server_activate() + + def serve_forever(httpd: HTTPServer) -> None: + with httpd: # to make sure httpd.server_close is called + httpd.serve_forever() + + thread = Thread(target=serve_forever, args=(httpd,)) + thread.setDaemon(True) + thread.start() + + return httpd, address + + +@nox.session +def test_export_with_source_credentials( + session: nox.Session, shared_datadir: Path +) -> None: + input_dir = tempfile.TemporaryDirectory() + + server, address = serve_directory_with_http(str(shared_datadir / "simple503")) + + with open(os.path.join(input_dir.name, "pyproject.toml"), "w") as pyproject_file: + pyproject_file.write(get_pyproject(address)) + + cwd = os.getcwd() + try: + os.chdir(input_dir.name) + session.run_always( + "poetry", + "config", + "http-basic.baz", + "alice", + "password", + external=True, + silent=True, + stderr=None, + ) + test_poetry = poetry.Poetry(session) + resources_file = test_poetry.export() + expected_index = "http://alice:password@" + address.lstrip("http://") + assert f"--extra-index-url {expected_index}" in resources_file + finally: + os.chdir(cwd) + server.shutdown() diff --git a/tests/unit/test_sessions.py b/tests/unit/test_sessions.py index 13516c52..a4a1e43a 100644 --- a/tests/unit/test_sessions.py +++ b/tests/unit/test_sessions.py @@ -161,7 +161,10 @@ def test_session_build_package(proxy: nox_poetry.Session) -> None: 'regex==2020.10.28; python_version == "3.5"', ), ("-e ../lib/foo", ""), - ("--extra-index-url https://example.com/pypi/simple", ""), + ( + "--extra-index-url https://example.com/pypi/simple", + "--extra-index-url https://example.com/pypi/simple", + ), ( dedent( """ @@ -170,7 +173,14 @@ def test_session_build_package(proxy: nox_poetry.Session) -> None: boltons==20.2.1 """ ), - "boltons==20.2.1", + dedent( + """ + --extra-index-url https://example.com/pypi/simple + boltons==20.2.1 + """ + ) + .lstrip("\n") + .rstrip("\n"), ), ], )