From 20a126f1ae50ccaddabcf66a23510e354faaa26c Mon Sep 17 00:00:00 2001 From: Luis Alvergue Date: Mon, 9 Dec 2024 22:38:30 +0000 Subject: [PATCH 1/5] feat: add test dependencies --- .devcontainer/Dockerfile | 2 +- pyproject.toml | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 70ca413..7941bc9 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -26,7 +26,7 @@ RUN python -m pip install --upgrade pip COPY . . # install devcontainer requirements -RUN pip install -e .[dev] +RUN pip install -e .[dev,test] # install docs requirements RUN pip install --no-cache-dir -r docs/requirements.txt diff --git a/pyproject.toml b/pyproject.toml index 82f7ddc..1ddcf1e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,6 +20,14 @@ dev = [ "pre-commit" ] +test = [ + "coverage", + "pytest", + "pytest-django", + "pytest-mock", + "pytest-socket", +] + [project.urls] Code = "https://github.com/compilerla/pems" Homepage = "https://compilerla.github.io/pems/" @@ -34,6 +42,14 @@ line-length = 127 target-version = ['py312'] include = '\.pyi?$' +[tool.coverage.run] +branch = true +relative_files = true +source = ["pems"] + +[tool.pytest.ini_options] +DJANGO_SETTINGS_MODULE = "pems.settings" + [tool.setuptools.packages.find] include = ["pems*"] namespaces = false From 5f41dc3fe5fcb23c5c97b46dd7de23aee25ece7f Mon Sep 17 00:00:00 2001 From: Luis Alvergue Date: Mon, 9 Dec 2024 22:45:39 +0000 Subject: [PATCH 2/5] feat: configure VS Code for pytest --- .vscode/settings.json | 3 +++ tests/__init__.py | 0 tests/pytest/__init__.py | 0 3 files changed, 3 insertions(+) create mode 100644 tests/__init__.py create mode 100644 tests/pytest/__init__.py diff --git a/.vscode/settings.json b/.vscode/settings.json index 1698f7b..d34973e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -21,6 +21,9 @@ "editor.defaultFormatter": "ms-python.black-formatter" }, "python.languageServer": "Pylance", + "python.testing.pytestArgs": ["tests/pytest"], + "python.testing.pytestEnabled": true, + "python.testing.unittestEnabled": false, "workbench.editorAssociations": { "*.db": "sqlite-viewer.option" } diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/pytest/__init__.py b/tests/pytest/__init__.py new file mode 100644 index 0000000..e69de29 From 40635777eb32fad1610c3d64c4529ae770613b14 Mon Sep 17 00:00:00 2001 From: Luis Alvergue Date: Mon, 9 Dec 2024 22:58:04 +0000 Subject: [PATCH 3/5] feat: implement /healthcheck endpoint implements the healthcheck as a middleware before the HOST_HEADER verification (in django.middleware.common.CommonMiddleware), so healthcheck requests always pass based on https://stackoverflow.com/a/64623669 --- pems/core/__init__.py | 0 pems/core/middleware.py | 19 +++++++++++++++++++ pems/settings.py | 1 + tests/pytest/core/__init__.py | 0 .../core/test_middleware_healthcheck.py | 6 ++++++ 5 files changed, 26 insertions(+) create mode 100644 pems/core/__init__.py create mode 100644 pems/core/middleware.py create mode 100644 tests/pytest/core/__init__.py create mode 100644 tests/pytest/core/test_middleware_healthcheck.py diff --git a/pems/core/__init__.py b/pems/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pems/core/middleware.py b/pems/core/middleware.py new file mode 100644 index 0000000..284c2ee --- /dev/null +++ b/pems/core/middleware.py @@ -0,0 +1,19 @@ +""" +The core application: middleware definitions for request/response cycle. +""" + +from django.http import HttpResponse + +HEALTHCHECK_PATH = "/healthcheck" + + +class Healthcheck: + """Middleware intercepts and accepts /healthcheck requests.""" + + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + if request.path == HEALTHCHECK_PATH: + return HttpResponse("Healthy", content_type="text/plain") + return self.get_response(request) diff --git a/pems/settings.py b/pems/settings.py index d034edf..d834a1f 100644 --- a/pems/settings.py +++ b/pems/settings.py @@ -36,6 +36,7 @@ def _filter_empty(ls): MIDDLEWARE = [ "django.middleware.security.SecurityMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", + "pems.core.middleware.Healthcheck", "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", diff --git a/tests/pytest/core/__init__.py b/tests/pytest/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/pytest/core/test_middleware_healthcheck.py b/tests/pytest/core/test_middleware_healthcheck.py new file mode 100644 index 0000000..1519fa0 --- /dev/null +++ b/tests/pytest/core/test_middleware_healthcheck.py @@ -0,0 +1,6 @@ +from pems.core.middleware import HEALTHCHECK_PATH + + +def test_healthcheck(client): + response = client.get(HEALTHCHECK_PATH) + assert response.status_code == 200 From 2685c50b17c10861215cf1ea3afe163a6972ad67 Mon Sep 17 00:00:00 2001 From: Luis Alvergue Date: Tue, 10 Dec 2024 00:12:31 +0000 Subject: [PATCH 4/5] feat: add GitHub Actions workflow to run pytest - sets Python version for GitHub workflows - adds workflow and shell script that runs pytest --- .github/workflows/.python-version | 1 + .github/workflows/tests-pytest.yml | 51 ++++++++++++++++++++++++++++++ tests/pytest/run.sh | 10 ++++++ 3 files changed, 62 insertions(+) create mode 100644 .github/workflows/.python-version create mode 100644 .github/workflows/tests-pytest.yml create mode 100755 tests/pytest/run.sh diff --git a/.github/workflows/.python-version b/.github/workflows/.python-version new file mode 100644 index 0000000..e4fba21 --- /dev/null +++ b/.github/workflows/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/.github/workflows/tests-pytest.yml b/.github/workflows/tests-pytest.yml new file mode 100644 index 0000000..5c4a066 --- /dev/null +++ b/.github/workflows/tests-pytest.yml @@ -0,0 +1,51 @@ +name: Pytest + +on: [push, pull_request, workflow_call] + +jobs: + pytest: + runs-on: ubuntu-latest + permissions: + # Gives the action the necessary permissions for publishing new + # comments in pull requests. + pull-requests: write + # Gives the action the necessary permissions for pushing data to the + # python-coverage-comment-action branch, and for editing existing + # comments (to avoid publishing multiple comments in the same PR) + contents: write + steps: + - name: Check out code + uses: actions/checkout@v4 + + - name: Install system packages + run: | + sudo apt-get update -y + sudo apt-get install -y gettext + + - uses: actions/setup-python@v5 + with: + python-version-file: .github/workflows/.python-version + cache: pip + cache-dependency-path: "**/pyproject.toml" + + - name: Install Python dependencies + run: pip install -e .[test] + + - name: Run setup + run: ./bin/init.sh + + - name: Run tests + run: ./tests/pytest/run.sh + + - name: Upload coverage report + uses: actions/upload-artifact@v4 + with: + name: coverage-report + path: pems/static/coverage + + - name: Coverage comment + uses: py-cov-action/python-coverage-comment-action@v3 + with: + GITHUB_TOKEN: ${{ github.token }} + MINIMUM_GREEN: 90 + MINIMUM_ORANGE: 80 diff --git a/tests/pytest/run.sh b/tests/pytest/run.sh new file mode 100755 index 0000000..a1fbf69 --- /dev/null +++ b/tests/pytest/run.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +set -eu + +# run normal pytests +coverage run -m pytest + +# clean out old coverage results +rm -rf pems/static/coverage + +coverage html --directory pems/static/coverage From 2a5a061cbcf987d05f12096852f7e5bfefca6e6b Mon Sep 17 00:00:00 2001 From: Luis Alvergue Date: Tue, 10 Dec 2024 15:29:25 +0000 Subject: [PATCH 5/5] feat: turn off socket connections for unit tests this is needed since we don't want to allow unit tests to go out to the internet --- tests/pytest/conftest.py | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 tests/pytest/conftest.py diff --git a/tests/pytest/conftest.py b/tests/pytest/conftest.py new file mode 100644 index 0000000..b4e630a --- /dev/null +++ b/tests/pytest/conftest.py @@ -0,0 +1,5 @@ +from pytest_socket import disable_socket + + +def pytest_runtest_setup(): + disable_socket()