Skip to content
Snippets Groups Projects
Commit 96dea321 authored by Adam Williamson's avatar Adam Williamson Committed by Adam Williamson
Browse files

Add container-based integration testing


This adds an integration test that runs a set of mediawiki
containers, configures them enough that the API works, then runs
a few simple tests of logging in, creating, moving and deleting
pages. We also enable this in CI. We can extend this with more
tests later, I think this is a reasonable initial set.

Signed-off-by: default avatarAdam Williamson <awilliam@redhat.com>
parent ab34837b
No related branches found
No related tags found
No related merge requests found
---
name: Run integration tests
on: ["push", "pull_request"]
jobs:
build:
runs-on: "ubuntu-latest"
strategy:
max-parallel: 4
fail-fast: false
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v5.1.1
with:
python-version: '3.12'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install tox tox-gh-actions
- name: Install podman
run: sudo apt-get -y install podman
- name: Integration test with tox
run: tox -e integration
...@@ -64,6 +64,22 @@ Alternatively, you can run the tests directly with pytest: ...@@ -64,6 +64,22 @@ Alternatively, you can run the tests directly with pytest:
$ pip install -e '.[testing]' $ pip install -e '.[testing]'
$ py.test $ py.test
There is a container-based integration test suite which is not run by default
as it requires docker or podman, is a little slow, and needs to do ~3G of network
transfer when first run (to download the mediawiki container images). It is
run as part of CI. To run it locally, make sure you have docker or podman
installed, then with tox, do:
.. code:: bash
$ tox -e integration
Or with pytest, do:
.. code:: bash
$ py.test test/integration.py
If you would like to expand the test suite by adding more tests, please go ahead! If you would like to expand the test suite by adding more tests, please go ahead!
Updating/expanding the documentation Updating/expanding the documentation
...@@ -106,7 +122,8 @@ When it is ready, push your branch to your remote: ...@@ -106,7 +122,8 @@ When it is ready, push your branch to your remote:
$ git push -u fork my-branch $ git push -u fork my-branch
Then you can open a pull request on GitHub. You should see a URL to do this Then you can open a pull request on GitHub. You should see a URL to do this
when you push your branch. when you push your branch. Tests will be automatically run on your pull
request via GitHub Actions.
Making a release Making a release
---------------- ----------------
......
...@@ -2,9 +2,6 @@ ...@@ -2,9 +2,6 @@
requires = ["setuptools>=40.6.0", "wheel"] requires = ["setuptools>=40.6.0", "wheel"]
build-backend = "setuptools.build_meta" build-backend = "setuptools.build_meta"
[tool.pytest.ini_options]
addopts = "--cov mwclient test"
[tool.bumpversion] [tool.bumpversion]
current_version = "0.11.0" current_version = "0.11.0"
commit = true commit = true
......
import shutil
import subprocess
import time
from urllib.error import URLError, HTTPError
from urllib.request import urlopen
import mwclient
import pytest
_CONTAINER_RUNTIME = None
if shutil.which("podman"):
_CONTAINER_RUNTIME = "podman"
elif shutil.which("docker"):
_CONTAINER_RUNTIME = "docker"
else:
raise RuntimeError("Neither podman nor docker is installed")
@pytest.fixture(
scope="class",
params=[("latest", "5002"), ("legacy", "5003"), ("lts", "5004")],
ids=["latest", "legacy", "lts"]
)
def site(request):
"""
Run a mediawiki container for the duration of the class, yield
a Site instance for it, then clean it up on exit. This is
parametrized so we get three containers and run the tests three
times for three mediawiki releases. We use podman because it's
much easier to use rootless than docker.
"""
(tag, port) = request.param
container = f"mwclient-{tag}"
# create the container, using upstream's official image. see
# https://hub.docker.com/_/mediawiki
args = (_CONTAINER_RUNTIME, "run", "--name", container, "-p", f"{port}:80",
"-d", f"docker.io/library/mediawiki:{tag}")
subprocess.run(args)
# configure the wiki far enough that we can use the API. if you
# use this interactively the CSS doesn't work, I don't know why,
# don't think it really matters
args = (_CONTAINER_RUNTIME, "exec", container, "runuser", "-u", "www-data", "--",
"php", "/var/www/html/maintenance/install.php", "--server",
f"http://localhost:{port}", "--dbtype", "sqlite", "--pass", "weakpassword",
"--dbpath", "/var/www/data", "mwclient-test-wiki", "root")
subprocess.run(args)
# create a regular user
args = (_CONTAINER_RUNTIME, "exec", container, "runuser", "-u", "www-data", "--",
"php", "/var/www/html/maintenance/createAndPromote.php", "testuser",
"weakpassword")
subprocess.run(args)
# create an admin user
args = (_CONTAINER_RUNTIME, "exec", container, "runuser", "-u", "www-data", "--",
"php", "/var/www/html/maintenance/createAndPromote.php", "sysop",
"weakpassword", "--bureaucrat", "--sysop", "--interface-admin")
subprocess.run(args)
# create a bot user
args = (_CONTAINER_RUNTIME, "exec", container, "runuser", "-u", "www-data", "--",
"php", "/var/www/html/maintenance/createAndPromote.php", "testbot",
"weakpassword", "--bot")
subprocess.run(args)
# disable anonymous editing (we can't use redirection via podman
# exec for some reason, so we use sed)
args = (_CONTAINER_RUNTIME, "exec", container, "runuser", "-u", "www-data", "--",
"sed", "-i", r"$ a\$wgGroupPermissions['*']['edit'] = false;",
"/var/www/html/LocalSettings.php")
subprocess.run(args)
# allow editing by users
args = (_CONTAINER_RUNTIME, "exec", container, "runuser", "-u", "www-data", "--",
"sed", "-i", r"$ a\$wgGroupPermissions['user']['edit'] = true;",
"/var/www/html/LocalSettings.php")
subprocess.run(args)
# block until the server is actually running, up to 30 seconds
start = int(time.time())
resp = None
while not resp:
try:
resp = urlopen(f"http://localhost:{port}")
except (ValueError, URLError, HTTPError) as err:
if int(time.time()) - start > 30:
print("Waited more than 30 seconds for server to start!")
raise err
else:
time.sleep(0.1)
# set up mwclient.site instance and yield it
yield mwclient.Site(f"localhost:{port}", path="/", scheme="http", force_login=False)
# -t=0 just hard stops it immediately, saves time
args = (_CONTAINER_RUNTIME, "stop", "-t=0", container)
subprocess.run(args)
args = (_CONTAINER_RUNTIME, "rm", container)
subprocess.run(args)
class TestAnonymous:
def test_page_load(self, site):
"""Test we can read a page from the sites."""
pg = site.pages["Main_Page"]
text = pg.text()
assert text.startswith("<strong>MediaWiki has been installed")
def test_page_create(self, site):
"""Test we get expected error if we try to create a page."""
pg = site.pages["Anonymous New Page"]
with pytest.raises(mwclient.errors.ProtectedPageError):
pg.edit("Hi I'm a new page", "create new page")
class TestLogin:
def test_login_wrong_password(self, site):
"""Test we raise correct error for login() with wrong password."""
assert not site.logged_in
with pytest.raises(mwclient.errors.LoginError):
site.login(username="testuser", password="thisiswrong")
assert not site.logged_in
def test_login(self, site):
"""
Test we can log in to the sites with login() and do authed
stuff.
"""
site.login(username="testuser", password="weakpassword")
assert site.logged_in
# test we can create a page
pg = site.pages["Authed New Page"]
pg.edit("Hi I'm a new page", "create new page")
# we have to reinit because of Page.exists
# https://github.com/mwclient/mwclient/issues/354
pg = site.pages["Authed New Page"]
assert pg.text() == "Hi I'm a new page"
# test we can move it
ret = pg.move("Authed Moved Page")
pg = site.pages["Authed Moved Page"]
assert pg.text() == "Hi I'm a new page"
def test_page_delete(self, site):
"""Test we can login, create and delete a page as sysop."""
site.login(username="sysop", password="weakpassword")
pg = site.pages["Sysop New Page"]
pg.edit("Hi I'm a new page", "create new page")
pg = site.pages["Sysop New Page"]
assert pg.text() == "Hi I'm a new page"
assert pg.exists == True
pg.delete()
pg = site.pages["Sysop New Page"]
assert pg.text() == ""
assert pg.exists == False
class TestClientLogin:
def test_clientlogin_wrong_password(self, site):
"""Test we raise correct error for clientlogin() with wrong password."""
with pytest.raises(mwclient.errors.LoginError):
site.clientlogin(username="testuser", password="thisiswrong")
assert not site.logged_in
def test_clientlogin(self, site):
"""
Test we can log in to the site with clientlogin() and
create a page.
"""
site.clientlogin(username="testuser", password="weakpassword")
assert site.logged_in
pg = site.pages["Anonymous New Page"]
pg.edit("Hi I'm a new page", "create new page")
pg = site.pages["Anonymous New Page"]
assert pg.text() == "Hi I'm a new page"
...@@ -9,7 +9,7 @@ python = ...@@ -9,7 +9,7 @@ python =
3.9: py39 3.9: py39
3.10: py310 3.10: py310
3.11: py311 3.11: py311
3.12: py312, flake 3.12: py312, flake, integration
3.13: py313 3.13: py313
[testenv] [testenv]
...@@ -21,3 +21,8 @@ deps = ...@@ -21,3 +21,8 @@ deps =
flake8 flake8
commands = commands =
flake8 mwclient flake8 mwclient
[testenv:integration]
deps =
pytest
commands = py.test test/integration.py -v
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment