Though barely useful in its current state, it shows how to integrate pytest with SQLAlchemy and Werkzeug, providing a test framework for the potential future Flask port, while actually testing the current PHP implementation. --- aurweb/test/__init__.py | 0 aurweb/test/conftest.py | 51 +++++++++++++++++++++++++++++++++++++++++ aurweb/test/test_rpc.py | 21 +++++++++++++++++ aurweb/test/wsgihttp.py | 38 ++++++++++++++++++++++++++++++ test/README.md | 3 +++ test/rpc.t | 2 ++ 6 files changed, 115 insertions(+) create mode 100644 aurweb/test/__init__.py create mode 100644 aurweb/test/conftest.py create mode 100644 aurweb/test/test_rpc.py create mode 100644 aurweb/test/wsgihttp.py create mode 100755 test/rpc.t diff --git a/aurweb/test/__init__.py b/aurweb/test/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/aurweb/test/conftest.py b/aurweb/test/conftest.py new file mode 100644 index 00000000..49cc2f6e --- /dev/null +++ b/aurweb/test/conftest.py @@ -0,0 +1,51 @@ +""" +Fixtures for pytest. + +This module is automatically loaded by pytest. +""" + +import pytest +import sqlalchemy +import werkzeug.test +import werkzeug.wrappers +import werkzeug.wrappers.json + +import aurweb.config +import aurweb.db +from aurweb.test.wsgihttp import WsgiHttpProxy + + +class Response(werkzeug.wrappers.CommonResponseDescriptorsMixin, + werkzeug.wrappers.json.JSONMixin, + werkzeug.wrappers.BaseResponse): + """ + Custom response object to be returned by the test client. More mixins could + be added if need be. + + See https://werkzeug.palletsprojects.com/en/1.0.x/wrappers/#mixin-classes + """ + pass + + +@pytest.fixture +def client(): + """ + Build a Werkzeug test client for making HTTP requests to AUR. It requires + that the AUR test website is already running at `[options] aur_location`, + specified in the configuration file. + + When aurweb becomes a pure Flask application, this should return Flask’s + test_client(), which is a Werkzeug test client too. + https://flask.palletsprojects.com/en/1.1.x/testing/#the-testing-skeleton + """ + base_uri = aurweb.config.get("options", "aur_location") + proxy = WsgiHttpProxy(base_uri) + return werkzeug.test.Client(proxy, Response) + + +@pytest.fixture(scope="session") +def db_engine(): + """ + Return an SQLAlchemy engine to the configured database. + """ + return sqlalchemy.create_engine(aurweb.db.get_sqlalchemy_url()) diff --git a/aurweb/test/test_rpc.py b/aurweb/test/test_rpc.py new file mode 100644 index 00000000..7079145c --- /dev/null +++ b/aurweb/test/test_rpc.py @@ -0,0 +1,21 @@ +""" +Test suite for the RPC interface. + +See also `doc/rpc.txt` for the RPC interface documentation. +""" + +import pytest +from sqlalchemy.sql import select + +from aurweb.schema import Packages + + +def test_search_by_name(client, db_engine): + """Take a package from the database, and find it through the RPC interface.""" + with db_engine.connect() as conn: + pkg = conn.execute(select([Packages]).limit(1)).fetchone() + if pkg is None: + pytest.skip("needs at least one package in the database") + resp = client.get("/rpc/", query_string={"v": "5", "type": "search", "arg": pkg["Name"]}) + result = resp.json + assert result["resultcount"] >= 1 diff --git a/aurweb/test/wsgihttp.py b/aurweb/test/wsgihttp.py new file mode 100644 index 00000000..5b9d8040 --- /dev/null +++ b/aurweb/test/wsgihttp.py @@ -0,0 +1,38 @@ +import http.client +import urllib.parse + + +class WsgiHttpProxy: + """ + WSGI-to-HTTP proxy, that is to say a WSGI application that forwards every + WSGI request to an HTTP server, then the HTTP response back to WSGI. + + The base URL the constructor takes is something like + `http://localhost:8080`. It must not contain a path, a query string or a + fragment, as the proxy wouldn’t now what to do with it. + + Only the HTTP scheme is supported, but HTTPS could probably be easily added. + """ + + def __init__(self, base_url): + parts = urllib.parse.urlsplit(base_url) + self.netloc = parts.netloc + # Limitations of this dumb proxy + assert parts.scheme == "http" + assert parts.path in ("", "/") + assert parts.query == "" + assert parts.fragment == "" + + def __call__(self, environ, start_response): + conn = http.client.HTTPConnection(self.netloc) + conn.request( + method=environ["REQUEST_METHOD"], + url=urllib.parse.urlunsplit(( + "http", self.netloc, + urllib.parse.quote(environ["PATH_INFO"]), + environ["QUERY_STRING"], "")), + body=environ["wsgi.input"].read(int(environ.get("CONTENT_LENGTH", 0))), + ) + resp = conn.getresponse() + start_response(f"{resp.status} {resp.reason}", resp.getheaders()) + return resp diff --git a/test/README.md b/test/README.md index de7eff18..cc8baf33 100644 --- a/test/README.md +++ b/test/README.md @@ -20,8 +20,11 @@ For all the test to run, the following Arch packages should be installed: - python-bleach - python-markdown - python-pygit2 +- python-pytest +- python-pytest-tap - python-sqlalchemy - python-srcinfo +- python-werkzeug Writing tests ------------- diff --git a/test/rpc.t b/test/rpc.t new file mode 100755 index 00000000..f950f7df --- /dev/null +++ b/test/rpc.t @@ -0,0 +1,2 @@ +#!/bin/sh +pytest --tap-stream "$(dirname "$0")/../aurweb/test/test_rpc.py" -- 2.26.0