pypiserver/tests/test_app.py

540 lines
17 KiB
Python
Raw Normal View History

2011-08-30 03:38:59 +02:00
#! /usr/bin/env py.test
# Builtin imports
import logging
try: # python 3
from html.parser import HTMLParser
from html import unescape
except ImportError:
from HTMLParser import HTMLParser
unescape = HTMLParser().unescape
try:
import xmlrpc.client as xmlrpclib
except ImportError:
import xmlrpclib # legacy Python
# Third party imports
import pytest
import webtest
# Local Imports
from pypiserver import __main__, bottle
import tests.test_core as test_core
2011-08-30 03:38:59 +02:00
# Enable logging to detect any problems with it
##
__main__.init_logging(level=logging.NOTSET)
2012-11-26 22:25:30 +01:00
@pytest.fixture()
def _app(app):
return app.module
@pytest.fixture
def app(tmpdir):
from pypiserver import app
return app(root=tmpdir.strpath, authenticated=[])
2011-08-30 03:38:59 +02:00
@pytest.fixture
def testapp(app):
"""Return a webtest TestApp initiated with pypiserver app"""
return webtest.TestApp(app)
@pytest.fixture
def root(tmpdir):
"""Return a pytest temporary directory"""
return tmpdir
2011-08-30 03:38:59 +02:00
@pytest.fixture
def priv(app):
b = bottle.Bottle()
b.mount("/priv/", app)
return b
@pytest.fixture
def testpriv(priv):
return webtest.TestApp(priv)
2011-08-30 03:38:59 +02:00
@pytest.fixture
def search_xml():
"""Return an xml dom suitable for passing to search"""
xml = '<xml><methodName>search</methodName><string>test</string></xml>'
return xml
@pytest.fixture(params=[
" ", # Mustcontain test below fails when string is empty.
"Hey there!",
"<html><body>Hey there!</body></html>",
])
def welcome_file_no_vars(request, root):
"""Welcome file fixture
:param request: pytest builtin fixture
:param root: root temporary directory
"""
wfile = root.join("testwelcome.html")
wfile.write(request.param)
return wfile
@pytest.fixture()
def welcome_file_all_vars(request, root):
msg = """
{{URL}}
{{VERSION}}
{{NUMPKGS}}
{{PACKAGES}}
{{SIMPLE}}
"""
wfile = root.join("testwelcome.html")
wfile.write(msg)
return wfile
def test_root_count(root, testapp):
"""Test that the welcome page count updates with added packages
:param root: root temporary directory fixture
:param testapp: webtest TestApp
"""
resp = testapp.get("/")
resp.mustcontain("PyPI compatible package index serving 0 packages")
2011-08-30 03:38:59 +02:00
root.join("Twisted-11.0.0.tar.bz2").write("")
resp = testapp.get("/")
resp.mustcontain("PyPI compatible package index serving 1 packages")
2011-08-30 03:38:59 +02:00
def test_root_hostname(testapp):
resp = testapp.get("/", headers={"Host": "systemexit.de"})
resp.mustcontain("easy_install -i http://systemexit.de/simple/ PACKAGE")
# go("http://systemexit.de/")
2011-08-30 03:38:59 +02:00
def test_root_welcome_msg_no_vars(root, welcome_file_no_vars):
from pypiserver import app
app = app(root=root.strpath, welcome_file=welcome_file_no_vars.strpath)
testapp = webtest.TestApp(app)
resp = testapp.get("/")
from pypiserver import __version__ as pver
resp.mustcontain(welcome_file_no_vars.read(), no=pver)
def test_root_welcome_msg_all_vars(root, welcome_file_all_vars):
from pypiserver import app
app = app(root=root.strpath, welcome_file=welcome_file_all_vars.strpath)
testapp = webtest.TestApp(app)
resp = testapp.get("/")
from pypiserver import __version__ as pver
resp.mustcontain(pver)
def test_root_welcome_msg_antiXSS(testapp):
"""https://github.com/pypiserver/pypiserver/issues/77"""
resp = testapp.get(
"/?<alert>Red</alert>", headers={"Host": "somehost.org"})
resp.mustcontain("alert", "somehost.org", no="<alert>")
def test_root_remove_not_found_msg_antiXSS(testapp):
"""https://github.com/pypiserver/pypiserver/issues/77"""
resp = testapp.post("/", expect_errors=True,
headers={"Host": "somehost.org"},
params={':action': 'remove_pkg',
'name': '<alert>Red</alert>',
'version': '1.1.1'})
resp.mustcontain("alert", "somehost.org", no="<alert>")
def test_packages_redirect(testapp):
resp = testapp.get("/packages")
assert resp.status_code >= 300
assert resp.status_code < 400
assert resp.location.endswith('/packages/')
def test_packages_empty(testapp):
resp = testapp.get("/packages/")
assert len(resp.html("a")) == 0
def test_favicon(testapp):
testapp.get("/favicon.ico", status=404)
2011-08-31 22:00:09 +02:00
def test_fallback(root, _app, testapp):
assert _app.config.redirect_to_fallback
resp = testapp.get("/simple/pypiserver/", status=302)
assert resp.headers["Location"] == "https://pypi.org/simple/pypiserver/"
2011-08-31 22:00:09 +02:00
def test_no_fallback(root, _app, testapp):
_app.config.redirect_to_fallback = False
testapp.get("/simple/pypiserver/", status=404)
def test_serve_no_dotfiles(root, testapp):
root.join(".foo-1.0.zip").write("secret")
testapp.get("/packages/.foo-1.0.zip", status=404)
def test_packages_list_no_dotfiles(root, testapp):
root.join(".foo-1.0.zip").write("secret")
resp = testapp.get("/packages/")
assert "foo" not in resp
def test_simple_redirect(testapp):
resp = testapp.get("/simple")
assert resp.status_code >= 300
assert resp.status_code < 400
assert resp.location.endswith('/simple/')
def test_simple_list_no_dotfiles(root, testapp):
root.join(".foo-1.0.zip").write("secret")
resp = testapp.get("/simple/")
assert "foo" not in resp
def test_simple_list_no_dotfiles2(root, testapp):
root.join(".foo-1.0.zip").write("secret")
resp = testapp.get("/simple/")
assert resp.html("a") == []
2011-09-01 00:19:14 +02:00
def test_serve_no_dotdir(root, testapp):
root.mkdir(".subdir").join("foo-1.0.zip").write("secret")
testapp.get("/packages/.subdir/foo-1.0.zip", status=404)
def test_packages_list_no_dotdir(root, testapp):
root.mkdir(".subdir").join("foo-1.0.zip").write("secret")
resp = testapp.get("/packages/")
assert "foo" not in resp
def test_simple_list_no_dotdir(root, testapp):
root.mkdir(".subdir").join("foo-1.0.zip").write("secret")
resp = testapp.get("/simple/")
assert "foo" not in resp
def test_simple_list_no_dotdir2(root, testapp):
root.mkdir(".subdir").join("foo-1.0.zip").write("secret")
resp = testapp.get("/simple/foo/")
assert resp.html("a") == []
def test_simple_name_redirect(testapp):
resp = testapp.get("/simple/foobar")
assert resp.status_code >= 300
assert resp.status_code < 400
assert resp.location.endswith('/simple/foobar/')
@pytest.mark.parametrize('package,normalized', [
('FooBar', 'foobar'),
('Foo.Bar', 'foo-bar'),
('foo_bar', 'foo-bar'),
('Foo-Bar', 'foo-bar'),
('foo--_.bar', 'foo-bar'),
])
def test_simple_normalized_name_redirect(testapp, package, normalized):
resp = testapp.get("/simple/{0}/".format(package))
assert resp.status_code >= 300
assert resp.status_code < 400
assert resp.location.endswith('/simple/{0}/'.format(normalized))
def test_simple_index(root, testapp):
2011-09-01 00:19:14 +02:00
root.join("foobar-1.0.zip").write("")
root.join("foobar-1.1.zip").write("")
root.join("foobarbaz-1.1.zip").write("")
root.join("foobar.baz-1.1.zip").write("")
resp = testapp.get("/simple/foobar/")
assert len(resp.html("a")) == 2
2011-09-01 00:19:14 +02:00
def test_simple_index_list(root, testapp):
2011-09-01 00:19:14 +02:00
root.join("foobar-1.0.zip").write("")
root.join("foobar-1.1.zip").write("")
root.join("foobarbaz-1.1.zip").write("")
root.join("foobar.baz-1.1.zip").write("")
resp = testapp.get("/simple/")
assert len(resp.html("a")) == 3
2011-09-01 00:19:14 +02:00
def test_simple_index_case(root, testapp):
2011-09-01 00:19:14 +02:00
root.join("FooBar-1.0.zip").write("")
root.join("FooBar-1.1.zip").write("")
resp = testapp.get("/simple/foobar/")
assert len(resp.html("a")) == 2
def test_nonroot_root(testpriv):
resp = testpriv.get("/priv/", headers={"Host": "nonroot"})
resp.mustcontain("easy_install -i http://nonroot/priv/simple/ PACKAGE")
def test_nonroot_root_with_x_forwarded_host(testapp):
resp = testapp.get("/", headers={"X-Forwarded-Host": "forward.ed/priv/"})
resp.mustcontain("easy_install -i http://forward.ed/priv/simple/ PACKAGE")
resp.mustcontain("""<a href="/priv/packages/">here</a>""")
2019-05-02 16:14:31 +02:00
def test_nonroot_root_with_x_forwarded_host_without_trailing_slash(testapp):
resp = testapp.get("/", headers={"X-Forwarded-Host": "forward.ed/priv"})
resp.mustcontain("easy_install -i http://forward.ed/priv/simple/ PACKAGE")
resp.mustcontain("""<a href="/priv/packages/">here</a>""")
def test_nonroot_simple_index(root, testpriv):
root.join("foobar-1.0.zip").write("")
resp = testpriv.get("/priv/simple/foobar/")
links = resp.html("a")
assert len(links) == 1
assert links[0]["href"].startswith("/priv/packages/foobar-1.0.zip#")
def test_nonroot_simple_index_with_x_forwarded_host(root, testapp):
root.join("foobar-1.0.zip").write("")
resp = testapp.get("/simple/foobar/", headers={"X-Forwarded-Host": "forwarded.ed/priv/"})
links = resp.html("a")
assert len(links) == 1
assert links[0]["href"].startswith("/priv/packages/foobar-1.0.zip#")
def test_nonroot_simple_packages(root, testpriv):
root.join("foobar-1.0.zip").write("123")
resp = testpriv.get("/priv/packages/")
links = resp.html("a")
assert len(links) == 1
assert links[0]["href"].startswith("/priv/packages/foobar-1.0.zip#")
def test_nonroot_simple_packages_with_x_forwarded_host(root, testapp):
root.join("foobar-1.0.zip").write("123")
resp = testapp.get("/packages/", headers={"X-Forwarded-Host": "forwarded/priv/"})
links = resp.html("a")
assert len(links) == 1
assert links[0]["href"].startswith("/priv/packages/foobar-1.0.zip#")
def test_root_no_relative_paths(testpriv):
"""https://github.com/pypiserver/pypiserver/issues/25"""
resp = testpriv.get("/priv/")
hrefs = [x["href"] for x in resp.html("a")]
assert hrefs == ['/priv/packages/', '/priv/simple/',
'https://pypi.org/project/pypiserver/']
def test_simple_index_list_no_duplicates(root, testapp):
root.join("foo-bar-1.0.tar.gz").write("")
root.join("foo_bar-1.0-py2.7.egg").write("")
resp = testapp.get("/simple/")
assert len(resp.html("a")) == 1
def test_simple_index_list_name_with_underscore(root, testapp):
root.join("foo_bar-1.0.tar.gz").write("")
root.join("foo_bar-1.0-py2.7.egg").write("")
resp = testapp.get("/simple/")
assert len(resp.html("a")) == 1
hrefs = [x["href"] for x in resp.html("a")]
assert hrefs == ["foo-bar/"]
def test_simple_index_egg_and_tarball(root, testapp):
root.join("foo-bar-1.0.tar.gz").write("")
root.join("foo_bar-1.0-py2.7.egg").write("")
resp = testapp.get("/simple/foo-bar/")
assert len(resp.html("a")) == 2
def test_simple_index_list_name_with_underscore_no_egg(root, testapp):
root.join("foo_bar-1.0.tar.gz").write("")
root.join("foo-bar-1.1.tar.gz").write("")
resp = testapp.get("/simple/")
assert len(resp.html("a")) == 1
hrefs = set([x["href"] for x in resp.html("a")])
2017-12-18 12:55:30 +01:00
assert hrefs == {"foo-bar/"}
2015-01-10 18:18:47 +01:00
def test_no_cache_control_set(root, _app, testapp):
assert not _app.config.cache_control
root.join("foo_bar-1.0.tar.gz").write("")
resp = testapp.get("/packages/foo_bar-1.0.tar.gz")
assert "Cache-Control" not in resp.headers
def test_cache_control_set(root):
from pypiserver import app
AGE = 86400
app_with_cache = webtest.TestApp(app(root=root.strpath, cache_control=AGE))
2015-01-10 18:18:47 +01:00
root.join("foo_bar-1.0.tar.gz").write("")
resp = app_with_cache.get("/packages/foo_bar-1.0.tar.gz")
assert "Cache-Control" in resp.headers
assert resp.headers["Cache-Control"] == 'public, max-age=%s' % AGE
def test_upload_noAction(root, testapp):
resp = testapp.post("/", expect_errors=1)
assert resp.status == '400 Bad Request'
assert "Missing ':action' field!" in unescape(resp.text)
def test_upload_badAction(root, testapp):
resp = testapp.post("/", params={':action': 'BAD'}, expect_errors=1)
assert resp.status == '400 Bad Request'
assert "Unsupported ':action' field: BAD" in unescape(resp.text)
2017-12-18 12:59:02 +01:00
@pytest.mark.parametrize("package", [f[0]
for f in test_core.files
if f[1] and '/' not in f[0]])
def test_upload(package, root, testapp):
resp = testapp.post("/", params={':action': 'file_upload'},
upload_files=[('content', package, b'')])
assert resp.status_int == 200
uploaded_pkgs = [f.basename for f in root.listdir()]
assert len(uploaded_pkgs) == 1
assert uploaded_pkgs[0].lower() == package.lower()
2017-12-18 12:59:02 +01:00
@pytest.mark.parametrize("package", [f[0]
for f in test_core.files
if f[1] and '/' not in f[0]])
def test_upload_with_signature(package, root, testapp):
resp = testapp.post("/", params={':action': 'file_upload'},
upload_files=[
('content', package, b''),
('gpg_signature', '%s.asc' % package, b'')])
assert resp.status_int == 200
uploaded_pkgs = [f.basename.lower() for f in root.listdir()]
assert len(uploaded_pkgs) == 2
assert package.lower() in uploaded_pkgs
assert '%s.asc' % package.lower() in uploaded_pkgs
2017-12-18 12:59:02 +01:00
@pytest.mark.parametrize("package", [
f[0] for f in test_core.files
if f[1] is None])
def test_upload_badFilename(package, root, testapp):
resp = testapp.post("/", params={':action': 'file_upload'},
upload_files=[('content', package, b'')],
expect_errors=1)
assert resp.status == '400 Bad Request'
assert "Bad filename: %s" % package in resp.text
@pytest.mark.parametrize(("name", "version"), [
(None, None),
(None, ''),
('', None),
(None, '1'),
('pkg', None),
('', '1'),
('pkg', ''),
])
def test_remove_pkg_missingNaveVersion(name, version, root, testapp):
msg = "Missing 'name'/'version' fields: name=%s, version=%s"
params = {':action': 'remove_pkg', 'name': name, 'version': version}
params = dict((k, v) for k, v in params.items() if v is not None)
resp = testapp.post("/", expect_errors=1, params=params)
assert resp.status == '400 Bad Request'
assert msg % (name, version) in unescape(resp.text)
def test_remove_pkg_notFound(root, testapp):
resp = testapp.post("/", expect_errors=1,
params={
':action': 'remove_pkg',
'name': 'foo',
'version': '123',
})
assert resp.status == '404 Not Found'
assert "foo (123) not found" in unescape(resp.text)
@pytest.mark.parametrize('pkgs,matches', [
([], []),
(['test-1.0.tar.gz'], [('test', '1.0')]),
(['test-1.0.tar.gz', 'test-test-2.0.1.tar.gz'],
[('test', '1.0'), ('test-test', '2.0.1')]),
(['test-1.0.tar.gz', 'other-2.0.tar.gz'], [('test', '1.0')]),
(['test-2.0-py2.py3-none-any.whl'], [('test', '2.0')]),
(['other-2.0.tar.gz'], [])
])
def test_search(root, testapp, search_xml, pkgs, matches):
"""Test the search functionality at the RPC2 endpoint
Calls the handle_rpc function by posting to the WebTest server with
the string returned by the ``search_xml`` fixture as the request
body. The result is parsed for convenience by the xmlrpc.client
(xmlrpclib in Python 2.x). The parsed result is a 2-tuple. The
second item is the method called, in this case always "search". The
first item is a 1-tuple which contains a list of match information
as dicts, e.g:
``(([{'version': '2.0', '_pypi_ordering': 0,
'name': 'test', 'summary': '2.0'}],), 'search')``
The pkgs parameter is a list of items to write to the packages
directory, which should then be available for the subsequent
search. The matches parameter is a list of 2-tuples of the form
(``name``, ``version``), where ``name`` and ``version`` are the
expected name and version matches for a search for the "test"
package as specified by the search_xml fixture.
:param root: root temporry directory fixture; used as packages dir
for testapp
:param testapp: webtest TestApp
:param str search_xml: XML string roughly equivalent to a pip search
for "test"
:param pkgs: package file names to be written into packages
directory
:param matches: a list of 2-tuples containing expected (name,
version) matches for the "test" query
"""
for pkg in pkgs:
root.join(pkg).write('')
resp = testapp.post('/RPC2', search_xml)
parsed = xmlrpclib.loads(resp.text)
assert len(parsed) == 2 and parsed[1] == 'search'
if not matches:
assert len(parsed[0]) == 1 and not parsed[0][0]
else:
assert len(parsed[0][0]) == len(matches) and parsed[0][0]
for returned in parsed[0][0]:
print(returned)
assert returned['name'] in [match[0] for match in matches]
assert returned['version'] in [match[1] for match in matches]
@pytest.mark.xfail()
def test_remove_pkg(root, testapp):
assert 0