From 60f5ceff7408fbc9bf9b8224ac4dfd27f300d970 Mon Sep 17 00:00:00 2001 From: Jeff Triplett Date: Mon, 25 Sep 2023 16:39:36 -0500 Subject: [PATCH] :sparkles: Testsuite updates and some tests (#3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * :package: Updates dev reqs * :gear: Updates test settings * :tractor: Refactor test settings * :green_heart: Adds a simple model test This is what we were working towards and why we made the other changes. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * :green_heart: Adds router tests * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * :gear: :hammer: mypy fixes? * :pencil: commits blank file to make mypy happy * :tractor: Adds/updates lint+mypy tasks * :gear: Fixes coverage ignore migrations * :gear: Sets coverage to 33% (we will increase this) * :gear: :arrow_down: Drop coverage * :gear: Configures pytest-coverage support * :green_heart: Adds test_runrelay cmd * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * add intro to README * adjust formatting * adjust wording * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * add docker publish job to release workflow * add if checks to release workflow * add correct DATABASES settings for PgBouncer (#7) * add default email settings to service (#9) * fix router name in README * move version to pyproject.toml * add changelog * add py.typed file, aspirationally * fix backend name in README * change test workflow trigger (#14) * move version back to package (#13) * remove Django 4.0 (#12) * add support for user settings from environ (#15) * add support for user settings from environ * make mypy happy * add more logging to relay (#19) * :robot: Bump docker/build-push-action from 4 to 5 (#16) Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 4 to 5. - [Release notes](https://github.com/docker/build-push-action/releases) - [Commits](https://github.com/docker/build-push-action/compare/v4...v5) --- updated-dependencies: - dependency-name: docker/build-push-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * :robot: Bump docker/login-action from 2 to 3 (#17) Bumps [docker/login-action](https://github.com/docker/login-action) from 2 to 3. - [Release notes](https://github.com/docker/login-action/releases) - [Commits](https://github.com/docker/login-action/compare/v2...v3) --- updated-dependencies: - dependency-name: docker/login-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * :robot: Bump docker/setup-buildx-action from 2 to 3 (#18) Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 2 to 3. - [Release notes](https://github.com/docker/setup-buildx-action/releases) - [Commits](https://github.com/docker/setup-buildx-action/compare/v2...v3) --- updated-dependencies: - dependency-name: docker/setup-buildx-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * :robot: [pre-commit.ci] pre-commit autoupdate (#20) updates: - [github.com/adamchainz/django-upgrade: 1.14.1 → 1.15.0](https://github.com/adamchainz/django-upgrade/compare/1.14.1...1.15.0) - [github.com/astral-sh/ruff-pre-commit: v0.0.290 → v0.0.291](https://github.com/astral-sh/ruff-pre-commit/compare/v0.0.290...v0.0.291) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> * :tractor: Run pytest-cov * :arrow_up: Bumps coverage to check to >50% * :gear: Run normal pytest * :tractor: Adds python -m to all the things * :tractor: Updates nox entrypoint too --------- Signed-off-by: dependabot[bot] Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Josh Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/test.yml | 6 ++--- Justfile | 9 ++++---- noxfile.py | 22 ++++++++++++++++++- pyproject.toml | 34 ++++++++++++++-------------- src/email_relay/backend.py | 2 +- tests/conftest.py | 45 ++++++++++++++++++++++++++++++++++++++ tests/settings.py | 31 -------------------------- tests/test_models.py | 12 ++++++++++ tests/test_router.py | 41 ++++++++++++++++++++++++++++++++++ tests/test_runrelay.py | 15 +++++++++++++ 10 files changed, 160 insertions(+), 57 deletions(-) create mode 100644 tests/conftest.py create mode 100644 tests/test_models.py create mode 100644 tests/test_router.py create mode 100644 tests/test_runrelay.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 972a7e5..632d2eb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -37,7 +37,7 @@ jobs: - name: Run tests run: | - nox --session "tests-${{ matrix.python-version }}(django='${{ matrix.django-version }}')" + python -m nox --session "tests-${{ matrix.python-version }}(django='${{ matrix.django-version }}')" tests: runs-on: ubuntu-latest @@ -88,7 +88,7 @@ jobs: # https://hynek.me/articles/ditch-codecov-python/ - name: Run tests run: | - coverage run -m pytest + python -m pytest python -m coverage html --skip-covered --skip-empty python -m coverage report | sed 's/^/ /' >> $GITHUB_STEP_SUMMARY - python -m coverage report --fail-under=100 + python -m coverage report --fail-under=50 diff --git a/Justfile b/Justfile index 866b53e..a89f420 100644 --- a/Justfile +++ b/Justfile @@ -48,9 +48,7 @@ test: python -m nox --reuse-existing-virtualenvs coverage: - rm -rf htmlcov - python -m coverage run -m pytest - python -m coverage html --skip-covered --skip-empty + python -m nox --reuse-existing-virtualenvs --session "coverage" types: python -m mypy . @@ -235,4 +233,7 @@ envsync: ################## lint: - pre-commit run --all-files + python -m nox --reuse-existing-virtualenvs --session "lint" + +mypy: + python -m nox --reuse-existing-virtualenvs --session "mypy" diff --git a/noxfile.py b/noxfile.py index 2e9f877..85b048b 100644 --- a/noxfile.py +++ b/noxfile.py @@ -49,4 +49,24 @@ def tests(session, django): else: session.install(f"django=={django}") - session.run("pytest", "-n", "auto", "--dist", "loadfile") + session.run("python", "-m", "pytest", "-n", "auto", "--dist", "loadfile") + + +@nox.session +def coverage(session): + session.install(".[dev]") + session.run("python", "-m", "pytest") + session.run("python", "-m", "coverage", "html", "--skip-covered", "--skip-empty") + session.run("python", "-m", "coverage", "report", "--fail-under=50") + + +@nox.session +def lint(session): + session.install(".[lint]") + session.run("pre-commit", "run", "--all-files") + + +@nox.session +def mypy(session): + session.install(".[dev]") + session.run("mypy") diff --git a/pyproject.toml b/pyproject.toml index 74fd354..a1d9fad 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,20 +41,21 @@ requires-python = ">=3.8" [project.optional-dependencies] dev = [ - "black", "coverage[toml]", "django-stubs", "django-stubs-ext", "hatch", + "model_bakery", "mypy", "nox", "pytest", + "pytest-cov", "pytest-django", "pytest-randomly", "pytest-reverse", "pytest-xdist", - "ruff", ] +lint = ["pre-commit"] psycopg = ["psycopg[binary]"] psycopg2 = ["psycopg2-binary"] @@ -112,21 +113,25 @@ version_pattern = "YYYY.INC1" [tool.coverage.run] omit = [ - "src/email_relay/*/migrations/*", - "tests/*", "manage.py", - "service.py" + "service.py", + "src/email_relay/migrations/*", + "tests/*", ] -source = ["src/email_relay"] +source = ["email_relay"] + +[tool.coverage.paths] +source = ["src"] + [tool.django-stubs] django_settings_module = "tests.settings" +strict_settings = false [tool.mypy] +mypy_path = "src/" +namespace_packages = false check_untyped_defs = true -files = [ - "src.email_relay", -] no_implicit_optional = true plugins = [ "mypy_django_plugin.main", @@ -137,24 +142,19 @@ warn_unused_ignores = true [[tool.mypy.overrides]] ignore_errors = true -module = [ - "src.email_relay.*.migrations.*", -] - -[[tool.mypy.overrides]] ignore_missing_imports = true -module = [] +module = "tests.*" [tool.mypy_django_plugin] ignore_missing_model_attributes = true [tool.pytest.ini_options] -DJANGO_SETTINGS_MODULE = "tests.settings" django_find_project = false pythonpath = ". src" -addopts = "--create-db -n auto --dist loadfile" +addopts = "--create-db --cov=email_relay -n auto --dist loadfile" norecursedirs = ".* bin build dist *.egg htmlcov logs node_modules templates venv" python_files = "tests.py test_*.py *_tests.py" +testpaths = ["tests"] [tool.ruff] ignore = ["E501", "E741"] # temporary diff --git a/src/email_relay/backend.py b/src/email_relay/backend.py index 8848c06..d17efce 100644 --- a/src/email_relay/backend.py +++ b/src/email_relay/backend.py @@ -12,7 +12,7 @@ class RelayDatabaseEmailBackend(BaseEmailBackend): def send_messages(self, email_messages: Sequence[EmailMessage]) -> int: messages = Message.objects.bulk_create( - [Message(email=email) for email in email_messages], # type: ignore[misc] + [Message(email=email) for email in email_messages], app_settings.MESSAGES_BATCH_SIZE, ) return len(messages) diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..651532a --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,45 @@ +from __future__ import annotations + +import logging + +from django.conf import settings + +from email_relay.conf import EMAIL_RELAY_DATABASE_ALIAS + +pytest_plugins = [] # type: ignore + + +# Settings fixtures to bootstrap our tests +def pytest_configure(config): + logging.disable(logging.CRITICAL) + + settings.configure( + ALLOWED_HOSTS=["*"], + CACHES={ + "default": { + "BACKEND": "django.core.cache.backends.dummy.DummyCache", + } + }, + DATABASES={ + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": ":memory:", + }, + EMAIL_RELAY_DATABASE_ALIAS: { + "ENGINE": "django.db.backends.sqlite3", + "NAME": ":memory:", + }, + }, + DATABASE_ROUTERS=[ + "email_relay.db.EmailDatabaseRouter", + ], + EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend", + INSTALLED_APPS=[ + "django.contrib.contenttypes", + "email_relay", + ], + LOGGING_CONFIG=None, + PASSWORD_HASHERS=["django.contrib.auth.hashers.MD5PasswordHasher"], + SECRET_KEY="NOTASECRET", + USE_TZ=True, + ) diff --git a/tests/settings.py b/tests/settings.py index 308d87d..e69de29 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -1,31 +0,0 @@ -from __future__ import annotations - -from email_relay.conf import EMAIL_RELAY_DATABASE_ALIAS - -ALLOWED_HOSTS = ["*"] - -DATABASES = { - "default": { - "ENGINE": "django.db.backends.sqlite3", - "NAME": ":memory:", - }, - EMAIL_RELAY_DATABASE_ALIAS: { - "ENGINE": "django.db.backends.sqlite3", - "NAME": ":memory:", - }, -} - -DATABASE_ROUTERS = [ - "email_relay.db.EmailRelayDatabaseRouter", -] - - -EMAIL_BACKEND = "email_relay.backend.DatabaseEmailBackend" - -INSTALLED_APPS = [ - "email_relay", -] - -SECRET_KEY = "NOTASECRET" - -USE_TZ = True diff --git a/tests/test_models.py b/tests/test_models.py new file mode 100644 index 0000000..d1f1bb0 --- /dev/null +++ b/tests/test_models.py @@ -0,0 +1,12 @@ +from __future__ import annotations + +import pytest +from model_bakery import baker + +from email_relay.models import Message + + +@pytest.mark.django_db(databases=["default", "email_relay_db"]) +def test_message(): + baker.make("email_relay.Message") + assert Message.objects.all().count() == 1 diff --git a/tests/test_router.py b/tests/test_router.py new file mode 100644 index 0000000..3ad6eb9 --- /dev/null +++ b/tests/test_router.py @@ -0,0 +1,41 @@ +from __future__ import annotations + +import pytest + +from email_relay.conf import app_settings +from email_relay.db import EmailDatabaseRouter + + +# Mock model with app_label "email_relay" +class MockModel: + class _meta: + app_label = "email_relay" + + +# Mock model with app_label "some_other_app" +class MockModelOther: + class _meta: + app_label = "some_other_app" + + +@pytest.fixture +def router(): + return EmailDatabaseRouter() + + +def test_db_for_read(router): + assert router.db_for_read(MockModel) == app_settings.DATABASE_ALIAS + assert router.db_for_read(MockModelOther) == "default" + + +def test_db_for_write(router): + assert router.db_for_write(MockModel) == app_settings.DATABASE_ALIAS + assert router.db_for_write(MockModelOther) == "default" + + +def test_allow_relation(router): + assert router.allow_relation(None, None) + + +def test_allow_migrate(router): + assert router.allow_migrate("some_db", "some_app_label") diff --git a/tests/test_runrelay.py b/tests/test_runrelay.py new file mode 100644 index 0000000..498a3f7 --- /dev/null +++ b/tests/test_runrelay.py @@ -0,0 +1,15 @@ +from __future__ import annotations + +import pytest +from django.core.management import call_command + + +def test_runrelay_help(): + # We'll capture the output of the command + with pytest.raises(SystemExit) as exec_info: + # call_command will execute our command as if we ran it from the command line + # the 'stdout' argument captures the command output + call_command("runrelay", "--help") + + # Asserting that the command exits with a successful exit code (0 for help command) + assert exec_info.value.code == 0