pypiserver/tests/test_server.py

377 lines
10 KiB
Python
Raw Normal View History

#! /usr/bin/env py.test
"""
Checks an actual pypi-server against various clients.
The tests below are using 3 ways to startup pypi-servers:
- "open": a per-module server instance without any authed operations,
serving a single `wheel` package, on a fixed port.
- "open": a per-module server instance with authed 'download/upload'
operations, serving a single `wheel` package, on a fixed port.
- "new_server": starting a new server with any configurations on each test.
"""
import contextlib
import itertools
import os
import shutil
import socket
2015-09-18 18:31:12 +02:00
import sys
import time
Docker improvements (#365) * Docker improvements This addresses much of what was brought up in #359. Specifically, it: - Significantly improves testing for the Docker image, adding a `docker/test_docker.py` file using the regular pytest machinery to set up and run docker images for testing - Hopefully addresses a variety of permissions issues, by being explicit about what access pypiserver needs and asking for it, only erroring if that access is not available - Requires RX permissions on `/data` (R to read files, X to list files and to be able to cd into the directory. This is important since `/data` is the `WORKDIR`) - Requires RWX permissions on `/data/packages`, so that we can list packages, write packages, and read packages. - When running in the default configuration (as root on Linux or as the pypiserver-named rootish user on Mac), with no volumes mounted, these requirements are all satisfied - Volume mounts still must be readable by the pypiserver user (UID 9898) in order for the container to run. However, we now error early if this is not the case, and direct users to a useful issue. - If the container is run as a non-root, non-pypiserver user (e.g. because someone ran `docker run --user=<user_id>`, we try to run pypiserver as that user). Provided that user has access to the necessary directories, it should run fine. - Fixes issues with running help and similar commands - Updates the Docker image to use `PYPISERVER_PORT` for port specification, while still falling back to `PORT` for backwards compatibility - Moves some docker-related things into a `/docker` directory - Adds a `Makefile` for building a test fixture package sdist and wheel, so that test code can call `make mypkg` and not need to worry about it potentially building multiple times The only issue #359 raises that's not addressed here is the one of running pypiserver in the Docker container using some non-default server for performance. I would like to do some benchmarking before deciding on what to do there.
2021-02-06 18:28:15 +01:00
import typing as t
from collections import namedtuple
from pathlib import Path
from shlex import split
from subprocess import Popen
from urllib.error import URLError
from urllib.request import urlopen
import pytest
# ######################################################################
# Fixtures & Helper Functions
# ######################################################################
CURRENT_PATH = Path(__file__).parent
ports = itertools.count(10000)
Srv = namedtuple("Srv", ("port", "root"))
@contextlib.contextmanager
def run_server(root, authed=False, other_cli=""):
"""Run a server, optionally with partial auth enabled."""
Docker improvements (#365) * Docker improvements This addresses much of what was brought up in #359. Specifically, it: - Significantly improves testing for the Docker image, adding a `docker/test_docker.py` file using the regular pytest machinery to set up and run docker images for testing - Hopefully addresses a variety of permissions issues, by being explicit about what access pypiserver needs and asking for it, only erroring if that access is not available - Requires RX permissions on `/data` (R to read files, X to list files and to be able to cd into the directory. This is important since `/data` is the `WORKDIR`) - Requires RWX permissions on `/data/packages`, so that we can list packages, write packages, and read packages. - When running in the default configuration (as root on Linux or as the pypiserver-named rootish user on Mac), with no volumes mounted, these requirements are all satisfied - Volume mounts still must be readable by the pypiserver user (UID 9898) in order for the container to run. However, we now error early if this is not the case, and direct users to a useful issue. - If the container is run as a non-root, non-pypiserver user (e.g. because someone ran `docker run --user=<user_id>`, we try to run pypiserver as that user). Provided that user has access to the necessary directories, it should run fine. - Fixes issues with running help and similar commands - Updates the Docker image to use `PYPISERVER_PORT` for port specification, while still falling back to `PORT` for backwards compatibility - Moves some docker-related things into a `/docker` directory - Adds a `Makefile` for building a test fixture package sdist and wheel, so that test code can call `make mypkg` and not need to worry about it potentially building multiple times The only issue #359 raises that's not addressed here is the one of running pypiserver in the Docker container using some non-default server for performance. I would like to do some benchmarking before deciding on what to do there.
2021-02-06 18:28:15 +01:00
htpasswd = (
CURRENT_PATH.joinpath("../fixtures/htpasswd.a.a").expanduser().resolve()
)
pswd_opt_choices = {
True: f"-P {htpasswd} -a update,download",
False: "-P. -a.",
"partial": f"-P {htpasswd} -a update",
}
pswd_opts = pswd_opt_choices[authed]
port = next(ports)
cmd = (
f"{sys.executable} -m pypiserver.__main__ "
f"run -vvv --overwrite -i 127.0.0.1 "
f"-p {port} {pswd_opts} {other_cli} {root}"
)
2022-02-20 21:17:20 +01:00
proc = Popen(cmd.split(), bufsize=2**16)
srv = Srv(port, root)
try:
wait_until_ready(srv)
assert proc.poll() is None
yield srv
finally:
print(f"Killing {srv}")
_kill_proc(proc)
def wait_until_ready(srv: Srv, n_tries=10):
for _ in range(n_tries):
if is_ready(srv):
return True
time.sleep(0.5)
raise TimeoutError
def is_ready(srv: Srv):
try:
return urlopen(build_url(srv.port), timeout=0.5).getcode() in (
200,
401,
)
except (URLError, socket.timeout):
return False
def _kill_proc(proc):
proc.terminate()
try:
proc.wait(timeout=1)
finally:
proc.kill()
Docker improvements (#365) * Docker improvements This addresses much of what was brought up in #359. Specifically, it: - Significantly improves testing for the Docker image, adding a `docker/test_docker.py` file using the regular pytest machinery to set up and run docker images for testing - Hopefully addresses a variety of permissions issues, by being explicit about what access pypiserver needs and asking for it, only erroring if that access is not available - Requires RX permissions on `/data` (R to read files, X to list files and to be able to cd into the directory. This is important since `/data` is the `WORKDIR`) - Requires RWX permissions on `/data/packages`, so that we can list packages, write packages, and read packages. - When running in the default configuration (as root on Linux or as the pypiserver-named rootish user on Mac), with no volumes mounted, these requirements are all satisfied - Volume mounts still must be readable by the pypiserver user (UID 9898) in order for the container to run. However, we now error early if this is not the case, and direct users to a useful issue. - If the container is run as a non-root, non-pypiserver user (e.g. because someone ran `docker run --user=<user_id>`, we try to run pypiserver as that user). Provided that user has access to the necessary directories, it should run fine. - Fixes issues with running help and similar commands - Updates the Docker image to use `PYPISERVER_PORT` for port specification, while still falling back to `PORT` for backwards compatibility - Moves some docker-related things into a `/docker` directory - Adds a `Makefile` for building a test fixture package sdist and wheel, so that test code can call `make mypkg` and not need to worry about it potentially building multiple times The only issue #359 raises that's not addressed here is the one of running pypiserver in the Docker container using some non-default server for performance. I would like to do some benchmarking before deciding on what to do there.
2021-02-06 18:28:15 +01:00
def build_url(port: t.Union[int, str], user: str = "", pswd: str = "") -> str:
auth = f"{user}:{pswd}@" if user or pswd else ""
return f"http://{auth}localhost:{port}"
def run_setup_py(path: Path, arguments: str):
return os.system(f"{sys.executable} {path / 'setup.py'} {arguments}")
2015-09-20 18:35:11 +02:00
# A test-distribution to check if
# bottle supports uploading 100's of packages,
# see: https://github.com/pypiserver/pypiserver/issues/82
#
# Has been run once `pip wheel .`, just to generate:
# ./wheelhouse/centodeps-0.0.0-cp34-none-win_amd64.whl
#
SETUP_PY = """\
from setuptools import setup
setup(
name="centodeps",
install_requires=["a==1.0"] * 200,
options={
"bdist_wheel": {"universal": True},
},
packages=[],
)
"""
@pytest.fixture(scope="module")
def project(tmp_path_factory):
projdir = tmp_path_factory.mktemp("project") / "centodeps"
projdir.mkdir(parents=True, exist_ok=True)
projdir.joinpath("setup.py").write_text(SETUP_PY)
return projdir
@pytest.fixture(scope="session")
def server_root(tmp_path_factory):
return tmp_path_factory.mktemp("root")
@pytest.fixture(scope="module")
def wheel_file(project, tmp_path_factory):
distdir = tmp_path_factory.mktemp("dist")
assert run_setup_py(project, f"bdist_wheel -d {distdir}") == 0
return list(distdir.glob("centodeps*.whl"))[0]
@pytest.fixture()
def hosted_wheel_file(wheel_file, server_root):
dst = server_root / wheel_file.name
shutil.copy(wheel_file, dst)
yield dst
if dst.is_file():
dst.unlink()
def clear_directory(root: Path):
for path in root.iterdir():
if path.is_file():
path.unlink()
@pytest.fixture(scope="module")
def _open_server(server_root):
with run_server(server_root, authed=False) as srv:
yield srv
@pytest.fixture
def open_server(_open_server: Srv):
yield _open_server
clear_directory(_open_server.root)
@pytest.fixture(scope="module")
def _authed_server(server_root):
with run_server(server_root, authed=True) as srv:
yield srv
@pytest.fixture
def authed_server(_authed_server):
yield _authed_server
clear_directory(_authed_server.root)
@pytest.fixture(scope="module")
def _partial_auth_server(server_root):
with run_server(server_root, authed="partial") as srv:
yield srv
@pytest.fixture
def partial_authed_server(_partial_auth_server):
yield _partial_auth_server
clear_directory(_partial_auth_server.root)
@pytest.fixture
def empty_packdir(tmp_path_factory):
return tmp_path_factory.mktemp("dists")
Docker improvements (#365) * Docker improvements This addresses much of what was brought up in #359. Specifically, it: - Significantly improves testing for the Docker image, adding a `docker/test_docker.py` file using the regular pytest machinery to set up and run docker images for testing - Hopefully addresses a variety of permissions issues, by being explicit about what access pypiserver needs and asking for it, only erroring if that access is not available - Requires RX permissions on `/data` (R to read files, X to list files and to be able to cd into the directory. This is important since `/data` is the `WORKDIR`) - Requires RWX permissions on `/data/packages`, so that we can list packages, write packages, and read packages. - When running in the default configuration (as root on Linux or as the pypiserver-named rootish user on Mac), with no volumes mounted, these requirements are all satisfied - Volume mounts still must be readable by the pypiserver user (UID 9898) in order for the container to run. However, we now error early if this is not the case, and direct users to a useful issue. - If the container is run as a non-root, non-pypiserver user (e.g. because someone ran `docker run --user=<user_id>`, we try to run pypiserver as that user). Provided that user has access to the necessary directories, it should run fine. - Fixes issues with running help and similar commands - Updates the Docker image to use `PYPISERVER_PORT` for port specification, while still falling back to `PORT` for backwards compatibility - Moves some docker-related things into a `/docker` directory - Adds a `Makefile` for building a test fixture package sdist and wheel, so that test code can call `make mypkg` and not need to worry about it potentially building multiple times The only issue #359 raises that's not addressed here is the one of running pypiserver in the Docker container using some non-default server for performance. I would like to do some benchmarking before deciding on what to do there.
2021-02-06 18:28:15 +01:00
def pip_download(
cmd: str,
port: t.Union[int, str],
install_dir: str,
user: str = None,
pswd: str = None,
) -> int:
url = build_url(port, user, pswd)
return _run_pip(f"-vv download -d {install_dir} -i {url} {cmd}")
Docker improvements (#365) * Docker improvements This addresses much of what was brought up in #359. Specifically, it: - Significantly improves testing for the Docker image, adding a `docker/test_docker.py` file using the regular pytest machinery to set up and run docker images for testing - Hopefully addresses a variety of permissions issues, by being explicit about what access pypiserver needs and asking for it, only erroring if that access is not available - Requires RX permissions on `/data` (R to read files, X to list files and to be able to cd into the directory. This is important since `/data` is the `WORKDIR`) - Requires RWX permissions on `/data/packages`, so that we can list packages, write packages, and read packages. - When running in the default configuration (as root on Linux or as the pypiserver-named rootish user on Mac), with no volumes mounted, these requirements are all satisfied - Volume mounts still must be readable by the pypiserver user (UID 9898) in order for the container to run. However, we now error early if this is not the case, and direct users to a useful issue. - If the container is run as a non-root, non-pypiserver user (e.g. because someone ran `docker run --user=<user_id>`, we try to run pypiserver as that user). Provided that user has access to the necessary directories, it should run fine. - Fixes issues with running help and similar commands - Updates the Docker image to use `PYPISERVER_PORT` for port specification, while still falling back to `PORT` for backwards compatibility - Moves some docker-related things into a `/docker` directory - Adds a `Makefile` for building a test fixture package sdist and wheel, so that test code can call `make mypkg` and not need to worry about it potentially building multiple times The only issue #359 raises that's not addressed here is the one of running pypiserver in the Docker container using some non-default server for performance. I would like to do some benchmarking before deciding on what to do there.
2021-02-06 18:28:15 +01:00
def _run_pip(cmd: str) -> int:
ncmd = (
"pip --no-cache-dir --disable-pip-version-check "
f"--retries 0 --timeout 5 --no-input {cmd}"
)
print(f"PIP: {ncmd}")
proc = Popen(split(ncmd))
proc.communicate()
return proc.returncode
@pytest.fixture
def pipdir(tmp_path_factory):
return tmp_path_factory.mktemp("pip")
@contextlib.contextmanager
def pypirc_file(repo, username="''", password="''"):
pypirc_path = Path.home() / ".pypirc"
old_pypirc = pypirc_path.read_text() if pypirc_path.is_file() else None
pypirc_path.write_text(
"\n".join(
(
"[distutils]",
"index-servers: test",
"",
"[test]",
f"repository: {repo}",
f"username: {username}",
f"password: {password}",
)
)
)
try:
yield pypirc_path
finally:
if old_pypirc:
pypirc_path.write_text(old_pypirc)
else:
pypirc_path.unlink()
@pytest.fixture
def open_pypirc(open_server):
with pypirc_file(repo=build_url(open_server.port)) as path:
yield path
@pytest.fixture
def authed_pypirc(authed_server):
username, password = "a", "a"
with pypirc_file(
repo=build_url(authed_server.port),
username=username,
password=password,
) as path:
yield path
Docker improvements (#365) * Docker improvements This addresses much of what was brought up in #359. Specifically, it: - Significantly improves testing for the Docker image, adding a `docker/test_docker.py` file using the regular pytest machinery to set up and run docker images for testing - Hopefully addresses a variety of permissions issues, by being explicit about what access pypiserver needs and asking for it, only erroring if that access is not available - Requires RX permissions on `/data` (R to read files, X to list files and to be able to cd into the directory. This is important since `/data` is the `WORKDIR`) - Requires RWX permissions on `/data/packages`, so that we can list packages, write packages, and read packages. - When running in the default configuration (as root on Linux or as the pypiserver-named rootish user on Mac), with no volumes mounted, these requirements are all satisfied - Volume mounts still must be readable by the pypiserver user (UID 9898) in order for the container to run. However, we now error early if this is not the case, and direct users to a useful issue. - If the container is run as a non-root, non-pypiserver user (e.g. because someone ran `docker run --user=<user_id>`, we try to run pypiserver as that user). Provided that user has access to the necessary directories, it should run fine. - Fixes issues with running help and similar commands - Updates the Docker image to use `PYPISERVER_PORT` for port specification, while still falling back to `PORT` for backwards compatibility - Moves some docker-related things into a `/docker` directory - Adds a `Makefile` for building a test fixture package sdist and wheel, so that test code can call `make mypkg` and not need to worry about it potentially building multiple times The only issue #359 raises that's not addressed here is the one of running pypiserver in the Docker container using some non-default server for performance. I would like to do some benchmarking before deciding on what to do there.
2021-02-06 18:28:15 +01:00
def run_twine(command: str, package: str, conf: str) -> None:
proc = Popen(
split(
f"twine {command} --repository test --config-file {conf} {package}"
)
)
proc.communicate()
assert not proc.returncode, f"Twine {command} failed. See stdout/err"
# ######################################################################
# Tests
# ######################################################################
all_servers = [
("open_server", "open_pypirc"),
("authed_server", "authed_pypirc"),
("partial_authed_server", "authed_pypirc"),
]
def test_pip_install_package_not_found(open_server, pipdir):
assert pip_download("centodeps", open_server.port, pipdir) != 0
assert not list(pipdir.iterdir())
def test_pip_install_open_succeeds(open_server, hosted_wheel_file, pipdir):
assert pip_download("centodeps", open_server.port, pipdir) == 0
assert pipdir.joinpath(hosted_wheel_file.name).is_file()
@pytest.mark.usefixtures("wheel_file")
def test_pip_install_authed_fails(authed_server, pipdir):
assert pip_download("centodeps", authed_server.port, pipdir) != 0
assert not list(pipdir.iterdir())
def test_pip_install_authed_succeeds(authed_server, hosted_wheel_file, pipdir):
assert (
pip_download(
"centodeps", authed_server.port, pipdir, user="a", pswd="a"
)
== 0
)
assert pipdir.joinpath(hosted_wheel_file.name).is_file()
@pytest.mark.parametrize("pkg_frmt", ["bdist", "bdist_wheel"])
@pytest.mark.parametrize(["server_fixture", "pypirc_fixture"], all_servers)
def test_setuptools_upload(
server_fixture, pypirc_fixture, project, pkg_frmt, server_root, request
):
request.getfixturevalue(server_fixture)
request.getfixturevalue(pypirc_fixture)
assert len(list(server_root.iterdir())) == 0
for i in range(5):
print(f"++Attempt #{i}")
assert run_setup_py(project, f"-vvv {pkg_frmt} upload -r test") == 0
assert len(list(server_root.iterdir())) == 1
def test_partial_authed_open_download(partial_authed_server):
"""Validate that partial auth still allows downloads."""
url = build_url(partial_authed_server.port) + "/simple"
resp = urlopen(url)
assert resp.getcode() == 200
@pytest.mark.parametrize("hash_algo", ("md5", "sha256", "sha512"))
@pytest.mark.usefixtures("hosted_wheel_file")
def test_hash_algos(server_root, pipdir, hash_algo):
"""Test twine upload with no authentication"""
with run_server(
server_root, other_cli="--hash-algo {}".format(hash_algo)
) as srv:
assert pip_download("centodeps", srv.port, pipdir) == 0
2015-09-20 18:35:11 +02:00
@pytest.mark.parametrize(["server_fixture", "pypirc_fixture"], all_servers)
def test_twine_upload(
server_fixture, pypirc_fixture, server_root, wheel_file, request
):
"""Test twine upload with no authentication"""
assert len(list(server_root.iterdir())) == 0
request.getfixturevalue(server_fixture)
pypirc = request.getfixturevalue(pypirc_fixture)
run_twine("upload", wheel_file, conf=pypirc)
assert len(list(server_root.iterdir())) == 1
assert server_root.joinpath(wheel_file.name).is_file(), (
wheel_file.name,
list(server_root.iterdir()),
)
@pytest.mark.parametrize(["server_fixture", "pypirc_fixture"], all_servers)
def test_twine_register(server_fixture, pypirc_fixture, wheel_file, request):
"""Test unauthenticated twine registration"""
request.getfixturevalue(server_fixture)
pypirc = request.getfixturevalue(pypirc_fixture)
run_twine("register", wheel_file, conf=pypirc)