diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f778a8ad..dbf3d7a7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,4 @@ -name: Unit tests +name: Quality assurance on: push: @@ -7,6 +7,22 @@ on: branches: [ main ] jobs: + linting: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python 3.12 + uses: actions/setup-python@v4 + with: + python-version: "3.12" + + - name: Install required packages + run: pip install ruff + + - name: Run linter "Ruff" + run: ruff . + build: name: Python ${{ matrix.python-version }}, django ${{ matrix.django-version }} runs-on: ubuntu-latest @@ -19,7 +35,7 @@ jobs: django-version: 22 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: setup python uses: actions/setup-python@v4 with: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..b394d416 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,10 @@ +# You find the full pre-commit-tools docs here: +# https://pre-commit.com/ + +repos: + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.1.6 + hooks: + # Run the Ruff linter. + - id: ruff + args: [--fix, --exit-non-zero-on-fix] diff --git a/README.rst b/README.rst index 2385d887..01bd7dc3 100644 --- a/README.rst +++ b/README.rst @@ -573,6 +573,32 @@ If you'd like to fix a bug, add a feature, etc Write your own test showing the issue has been resolved, or the feature works as intended. +Git hooks (via pre-commit) +========================== + +We use pre-push hooks to ensure that only linted code reaches our remote repository and pipelines aren't triggered in +vain. + +To enable the configured pre-push hooks, you need to [install](https://pre-commit.com/) pre-commit and run once:: + + pre-commit install -t pre-push -t pre-commit --install-hooks + +This will permanently install the git hooks for both, frontend and backend, in your local +[`.git/hooks`](./.git/hooks) folder. +The hooks are configured in the [`.pre-commit-config.yaml`](.pre-commit-config.yaml). + +You can check whether hooks work as intended using the [run](https://pre-commit.com/#pre-commit-run) command:: + + pre-commit run [hook-id] [options] + +Example: run single hook:: + + pre-commit run ruff --all-files --hook-stage push + +Example: run all hooks of pre-push stage:: + + pre-commit run --all-files --hook-stage push + Running Tests ============= To run the tests:: diff --git a/django_ses/__init__.py b/django_ses/__init__.py index c37eabab..af379ac4 100644 --- a/django_ses/__init__.py +++ b/django_ses/__init__.py @@ -1,13 +1,12 @@ import logging +from datetime import datetime, timedelta +from time import sleep import boto3 from botocore.vendored.requests.packages.urllib3.exceptions import ResponseError from django.core.mail.backends.base import BaseEmailBackend -from django_ses import settings - -from datetime import datetime, timedelta -from time import sleep +from django_ses import settings try: import importlib.metadata as importlib_metadata @@ -268,7 +267,7 @@ def _get_v2_parameters(self, message, source, email_feedback): params['FromEmailAddressIdentityArn'] = self.ses_from_arn or self.ses_source_arn if email_feedback is not None: params['FeedbackForwardingEmailAddress'] = email_feedback - + return params def _get_v1_parameters(self, message, source): diff --git a/django_ses/urls.py b/django_ses/urls.py index e0fedfe4..2427c6bb 100644 --- a/django_ses/urls.py +++ b/django_ses/urls.py @@ -2,7 +2,6 @@ from django_ses.views import DashboardView - urlpatterns = [ path('', DashboardView.as_view(), name='django_ses_stats'), ] diff --git a/django_ses/utils.py b/django_ses/utils.py index bfdf3616..a32193ea 100644 --- a/django_ses/utils.py +++ b/django_ses/utils.py @@ -252,7 +252,7 @@ def _get_bytes_to_sign(self): return "".join(bytes_to_sign).encode() -def BounceMessageVerifier(*args, **kwargs): +def BounceMessageVerifier(*args, **kwargs): # noqa: N802 warnings.warn( "utils.BounceMessageVerifier is deprecated. It is renamed to EventMessageVerifier.", RemovedInDjangoSES20Warning, diff --git a/django_ses/views.py b/django_ses/views.py index a9017b4d..b6a4497a 100644 --- a/django_ses/views.py +++ b/django_ses/views.py @@ -1,30 +1,24 @@ +import copy import json +import logging import warnings +from urllib.error import URLError +from urllib.request import urlopen import boto3 import pytz +from django.core.cache import cache +from django.core.exceptions import PermissionDenied +from django.http import HttpResponse, HttpResponseBadRequest +from django.shortcuts import render from django.utils.decorators import method_decorator from django.views.decorators.csrf import csrf_exempt +from django.views.decorators.http import require_POST from django.views.generic.base import TemplateView, View +from django_ses import settings, signals, utils from django_ses.deprecation import RemovedInDjangoSES20Warning -from urllib.request import urlopen -from urllib.error import URLError -import copy -import logging - - -from django.http import HttpResponse, HttpResponseBadRequest -from django.views.decorators.http import require_POST -from django.core.cache import cache -from django.core.exceptions import PermissionDenied -from django.shortcuts import render - -from django_ses import settings -from django_ses import signals -from django_ses import utils - logger = logging.getLogger(__name__) @@ -99,7 +93,8 @@ def dashboard(request): """ Graph SES send statistics over time. """ - warnings.warn('This view will be removed in future versions. Consider using DashboardView instead', DeprecationWarning) + warnings.warn('This view will be removed in future versions. Consider using DashboardView instead', + DeprecationWarning) cache_key = 'vhash:django_ses_stats' cached_view = cache.get(cache_key) if cached_view: @@ -170,7 +165,7 @@ def get_context_data(self, **kwargs): verified_emails = emails_parse(verified_emails_dict) ordered_data = stats_to_list(stats) summary = sum_stats(ordered_data) - + context.update({ 'title': 'SES Statistics', 'datapoints': ordered_data, @@ -193,7 +188,7 @@ def get(self, request, *args, **kwargs): cached_view = cache.get(cache_key) if cached_view: return cached_view - + response = super().get(request, *args, **kwargs).render() cache.set(cache_key, response, 60 * 15) # Cache for 15 minutes return response diff --git a/example/local_settings.template.py b/example/local_settings.template.py index 2ad01bf1..dc8a9293 100644 --- a/example/local_settings.template.py +++ b/example/local_settings.template.py @@ -1,2 +1,3 @@ +# noqa: N999 AWS_ACCESS_KEY_ID = 'YOUR-ACCESS-KEY-ID' AWS_SECRET_ACCESS_KEY = 'YOUR-SECRET-ACCESS-KEY' diff --git a/example/urls.py b/example/urls.py index 9950787a..9ab26d69 100644 --- a/example/urls.py +++ b/example/urls.py @@ -16,7 +16,7 @@ re_path(r'^send-email/$', views.send_email, name='send-email'), re_path(r'^reporting/', include('django_ses.urls')), - re_path(r'^bounce/', 'django_ses.views.handle_bounce', name='handle_bounce'), # Deprecated, see SESEventWebhookView. + re_path(r'^bounce/', 'django_ses.views.handle_bounce', name='handle_bounce'), # Deprecated, see SESEventWebhookView re_path(r'^event-webhook/', SESEventWebhookView.as_view(), name='event_webhook'), ] diff --git a/poetry.lock b/poetry.lock index c0fb8ba5..92474049 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. [[package]] name = "asgiref" @@ -47,32 +47,32 @@ tzdata = ["tzdata"] [[package]] name = "boto3" -version = "1.29.6" +version = "1.34.34" description = "The AWS SDK for Python" optional = false -python-versions = ">= 3.7" +python-versions = ">= 3.8" files = [ - {file = "boto3-1.29.6-py3-none-any.whl", hash = "sha256:f4d19e01d176c3a5a05e4af733185ff1891b08a3c38d4a439800fa132aa6e9be"}, - {file = "boto3-1.29.6.tar.gz", hash = "sha256:d1d0d979a70bf9b0b13ae3b017f8523708ad953f62d16f39a602d67ee9b25554"}, + {file = "boto3-1.34.34-py3-none-any.whl", hash = "sha256:33a8b6d9136fa7427160edb92d2e50f2035f04e9d63a2d1027349053e12626aa"}, + {file = "boto3-1.34.34.tar.gz", hash = "sha256:b2f321e20966f021ec800b7f2c01287a3dd04fc5965acdfbaa9c505a24ca45d1"}, ] [package.dependencies] -botocore = ">=1.32.6,<1.33.0" +botocore = ">=1.34.34,<1.35.0" jmespath = ">=0.7.1,<2.0.0" -s3transfer = ">=0.7.0,<0.8.0" +s3transfer = ">=0.10.0,<0.11.0" [package.extras] crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.32.6" +version = "1.34.34" description = "Low-level, data-driven core of boto 3." optional = false -python-versions = ">= 3.7" +python-versions = ">= 3.8" files = [ - {file = "botocore-1.32.6-py3-none-any.whl", hash = "sha256:4454f967a4d1a01e3e6205c070455bc4e8fd53b5b0753221581ae679c55a9dfd"}, - {file = "botocore-1.32.6.tar.gz", hash = "sha256:ecec876103783b5efe6099762dda60c2af67e45f7c0ab4568e8265d11c6c449b"}, + {file = "botocore-1.34.34-py3-none-any.whl", hash = "sha256:cd060b0d88ebb2b893f1411c1db7f2ba66cc18e52dcc57ad029564ef5fec437b"}, + {file = "botocore-1.34.34.tar.gz", hash = "sha256:54093dc97372bb7683f5c61a279aa8240408abf3b2cc494ae82a9a90c1b784b5"}, ] [package.dependencies] @@ -84,17 +84,17 @@ urllib3 = [ ] [package.extras] -crt = ["awscrt (==0.19.12)"] +crt = ["awscrt (==0.19.19)"] [[package]] name = "certifi" -version = "2023.11.17" +version = "2024.2.2" description = "Python package for providing Mozilla's CA Bundle." optional = true python-versions = ">=3.6" files = [ - {file = "certifi-2023.11.17-py3-none-any.whl", hash = "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474"}, - {file = "certifi-2023.11.17.tar.gz", hash = "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1"}, + {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, + {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, ] [[package]] @@ -273,69 +273,78 @@ files = [ [[package]] name = "cryptography" -version = "41.0.6" +version = "42.0.2" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = true python-versions = ">=3.7" files = [ - {file = "cryptography-41.0.6-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:0f27acb55a4e77b9be8d550d762b0513ef3fc658cd3eb15110ebbcbd626db12c"}, - {file = "cryptography-41.0.6-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:ae236bb8760c1e55b7a39b6d4d32d2279bc6c7c8500b7d5a13b6fb9fc97be35b"}, - {file = "cryptography-41.0.6-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afda76d84b053923c27ede5edc1ed7d53e3c9f475ebaf63c68e69f1403c405a8"}, - {file = "cryptography-41.0.6-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da46e2b5df770070412c46f87bac0849b8d685c5f2679771de277a422c7d0b86"}, - {file = "cryptography-41.0.6-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ff369dd19e8fe0528b02e8df9f2aeb2479f89b1270d90f96a63500afe9af5cae"}, - {file = "cryptography-41.0.6-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b648fe2a45e426aaee684ddca2632f62ec4613ef362f4d681a9a6283d10e079d"}, - {file = "cryptography-41.0.6-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:5daeb18e7886a358064a68dbcaf441c036cbdb7da52ae744e7b9207b04d3908c"}, - {file = "cryptography-41.0.6-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:068bc551698c234742c40049e46840843f3d98ad7ce265fd2bd4ec0d11306596"}, - {file = "cryptography-41.0.6-cp37-abi3-win32.whl", hash = "sha256:2132d5865eea673fe6712c2ed5fb4fa49dba10768bb4cc798345748380ee3660"}, - {file = "cryptography-41.0.6-cp37-abi3-win_amd64.whl", hash = "sha256:48783b7e2bef51224020efb61b42704207dde583d7e371ef8fc2a5fb6c0aabc7"}, - {file = "cryptography-41.0.6-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:8efb2af8d4ba9dbc9c9dd8f04d19a7abb5b49eab1f3694e7b5a16a5fc2856f5c"}, - {file = "cryptography-41.0.6-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c5a550dc7a3b50b116323e3d376241829fd326ac47bc195e04eb33a8170902a9"}, - {file = "cryptography-41.0.6-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:85abd057699b98fce40b41737afb234fef05c67e116f6f3650782c10862c43da"}, - {file = "cryptography-41.0.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f39812f70fc5c71a15aa3c97b2bbe213c3f2a460b79bd21c40d033bb34a9bf36"}, - {file = "cryptography-41.0.6-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:742ae5e9a2310e9dade7932f9576606836ed174da3c7d26bc3d3ab4bd49b9f65"}, - {file = "cryptography-41.0.6-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:35f3f288e83c3f6f10752467c48919a7a94b7d88cc00b0668372a0d2ad4f8ead"}, - {file = "cryptography-41.0.6-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:4d03186af98b1c01a4eda396b137f29e4e3fb0173e30f885e27acec8823c1b09"}, - {file = "cryptography-41.0.6-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:b27a7fd4229abef715e064269d98a7e2909ebf92eb6912a9603c7e14c181928c"}, - {file = "cryptography-41.0.6-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:398ae1fc711b5eb78e977daa3cbf47cec20f2c08c5da129b7a296055fbb22aed"}, - {file = "cryptography-41.0.6-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:7e00fb556bda398b99b0da289ce7053639d33b572847181d6483ad89835115f6"}, - {file = "cryptography-41.0.6-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:60e746b11b937911dc70d164060d28d273e31853bb359e2b2033c9e93e6f3c43"}, - {file = "cryptography-41.0.6-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:3288acccef021e3c3c10d58933f44e8602cf04dba96d9796d70d537bb2f4bbc4"}, - {file = "cryptography-41.0.6.tar.gz", hash = "sha256:422e3e31d63743855e43e5a6fcc8b4acab860f560f9321b0ee6269cc7ed70cc3"}, + {file = "cryptography-42.0.2-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:701171f825dcab90969596ce2af253143b93b08f1a716d4b2a9d2db5084ef7be"}, + {file = "cryptography-42.0.2-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:61321672b3ac7aade25c40449ccedbc6db72c7f5f0fdf34def5e2f8b51ca530d"}, + {file = "cryptography-42.0.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea2c3ffb662fec8bbbfce5602e2c159ff097a4631d96235fcf0fb00e59e3ece4"}, + {file = "cryptography-42.0.2-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b15c678f27d66d247132cbf13df2f75255627bcc9b6a570f7d2fd08e8c081d2"}, + {file = "cryptography-42.0.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:8e88bb9eafbf6a4014d55fb222e7360eef53e613215085e65a13290577394529"}, + {file = "cryptography-42.0.2-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a047682d324ba56e61b7ea7c7299d51e61fd3bca7dad2ccc39b72bd0118d60a1"}, + {file = "cryptography-42.0.2-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:36d4b7c4be6411f58f60d9ce555a73df8406d484ba12a63549c88bd64f7967f1"}, + {file = "cryptography-42.0.2-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:a00aee5d1b6c20620161984f8ab2ab69134466c51f58c052c11b076715e72929"}, + {file = "cryptography-42.0.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b97fe7d7991c25e6a31e5d5e795986b18fbbb3107b873d5f3ae6dc9a103278e9"}, + {file = "cryptography-42.0.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5fa82a26f92871eca593b53359c12ad7949772462f887c35edaf36f87953c0e2"}, + {file = "cryptography-42.0.2-cp37-abi3-win32.whl", hash = "sha256:4b063d3413f853e056161eb0c7724822a9740ad3caa24b8424d776cebf98e7ee"}, + {file = "cryptography-42.0.2-cp37-abi3-win_amd64.whl", hash = "sha256:841ec8af7a8491ac76ec5a9522226e287187a3107e12b7d686ad354bb78facee"}, + {file = "cryptography-42.0.2-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:55d1580e2d7e17f45d19d3b12098e352f3a37fe86d380bf45846ef257054b242"}, + {file = "cryptography-42.0.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28cb2c41f131a5758d6ba6a0504150d644054fd9f3203a1e8e8d7ac3aea7f73a"}, + {file = "cryptography-42.0.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9097a208875fc7bbeb1286d0125d90bdfed961f61f214d3f5be62cd4ed8a446"}, + {file = "cryptography-42.0.2-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:44c95c0e96b3cb628e8452ec060413a49002a247b2b9938989e23a2c8291fc90"}, + {file = "cryptography-42.0.2-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2f9f14185962e6a04ab32d1abe34eae8a9001569ee4edb64d2304bf0d65c53f3"}, + {file = "cryptography-42.0.2-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:09a77e5b2e8ca732a19a90c5bca2d124621a1edb5438c5daa2d2738bfeb02589"}, + {file = "cryptography-42.0.2-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:ad28cff53f60d99a928dfcf1e861e0b2ceb2bc1f08a074fdd601b314e1cc9e0a"}, + {file = "cryptography-42.0.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:130c0f77022b2b9c99d8cebcdd834d81705f61c68e91ddd614ce74c657f8b3ea"}, + {file = "cryptography-42.0.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:fa3dec4ba8fb6e662770b74f62f1a0c7d4e37e25b58b2bf2c1be4c95372b4a33"}, + {file = "cryptography-42.0.2-cp39-abi3-win32.whl", hash = "sha256:3dbd37e14ce795b4af61b89b037d4bc157f2cb23e676fa16932185a04dfbf635"}, + {file = "cryptography-42.0.2-cp39-abi3-win_amd64.whl", hash = "sha256:8a06641fb07d4e8f6c7dda4fc3f8871d327803ab6542e33831c7ccfdcb4d0ad6"}, + {file = "cryptography-42.0.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:087887e55e0b9c8724cf05361357875adb5c20dec27e5816b653492980d20380"}, + {file = "cryptography-42.0.2-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a7ef8dd0bf2e1d0a27042b231a3baac6883cdd5557036f5e8df7139255feaac6"}, + {file = "cryptography-42.0.2-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:4383b47f45b14459cab66048d384614019965ba6c1a1a141f11b5a551cace1b2"}, + {file = "cryptography-42.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:fbeb725c9dc799a574518109336acccaf1303c30d45c075c665c0793c2f79a7f"}, + {file = "cryptography-42.0.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:320948ab49883557a256eab46149df79435a22d2fefd6a66fe6946f1b9d9d008"}, + {file = "cryptography-42.0.2-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5ef9bc3d046ce83c4bbf4c25e1e0547b9c441c01d30922d812e887dc5f125c12"}, + {file = "cryptography-42.0.2-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:52ed9ebf8ac602385126c9a2fe951db36f2cb0c2538d22971487f89d0de4065a"}, + {file = "cryptography-42.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:141e2aa5ba100d3788c0ad7919b288f89d1fe015878b9659b307c9ef867d3a65"}, + {file = "cryptography-42.0.2.tar.gz", hash = "sha256:e0ec52ba3c7f1b7d813cd52649a5b3ef1fc0d433219dc8c93827c57eab6cf888"}, ] [package.dependencies] -cffi = ">=1.12" +cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} [package.extras] docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] -docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"] +docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"] nox = ["nox"] -pep8test = ["black", "check-sdist", "mypy", "ruff"] +pep8test = ["check-sdist", "click", "mypy", "ruff"] sdist = ["build"] ssh = ["bcrypt (>=3.1.5)"] -test = ["pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] +test = ["certifi", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] test-randomorder = ["pytest-randomly"] [[package]] name = "distlib" -version = "0.3.7" +version = "0.3.8" description = "Distribution utilities" optional = false python-versions = "*" files = [ - {file = "distlib-0.3.7-py2.py3-none-any.whl", hash = "sha256:2e24928bc811348f0feb63014e97aaae3037f2cf48712d51ae61df7fd6075057"}, - {file = "distlib-0.3.7.tar.gz", hash = "sha256:9dafe54b34a028eafd95039d5e5d4851a13734540f1331060d31c9916e7147a8"}, + {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, + {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, ] [[package]] name = "django" -version = "4.2.7" +version = "4.2.9" description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." optional = false python-versions = ">=3.8" files = [ - {file = "Django-4.2.7-py3-none-any.whl", hash = "sha256:e1d37c51ad26186de355cbcec16613ebdabfa9689bbade9c538835205a8abbe9"}, - {file = "Django-4.2.7.tar.gz", hash = "sha256:8e0f1c2c2786b5c0e39fe1afce24c926040fad47c8ea8ad30aaf1188df29fc41"}, + {file = "Django-4.2.9-py3-none-any.whl", hash = "sha256:2cc2fc7d1708ada170ddd6c99f35cc25db664f165d3794bc7723f46b2f8c8984"}, + {file = "Django-4.2.9.tar.gz", hash = "sha256:12498cc3cb8bc8038539fef9e90e95f507502436c1f0c3a673411324fa675d14"}, ] [package.dependencies] @@ -399,28 +408,28 @@ files = [ [[package]] name = "platformdirs" -version = "4.0.0" +version = "4.2.0" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "platformdirs-4.0.0-py3-none-any.whl", hash = "sha256:118c954d7e949b35437270383a3f2531e99dd93cf7ce4dc8340d3356d30f173b"}, - {file = "platformdirs-4.0.0.tar.gz", hash = "sha256:cb633b2bcf10c51af60beb0ab06d2f1d69064b43abf4c185ca6b28865f3f9731"}, + {file = "platformdirs-4.2.0-py3-none-any.whl", hash = "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068"}, + {file = "platformdirs-4.2.0.tar.gz", hash = "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"}, ] [package.extras] -docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] +docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] [[package]] name = "pluggy" -version = "1.3.0" +version = "1.4.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" files = [ - {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"}, - {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"}, + {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"}, + {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"}, ] [package.extras] @@ -465,13 +474,13 @@ six = ">=1.5" [[package]] name = "pytz" -version = "2023.3.post1" +version = "2024.1" description = "World timezone definitions, modern and historical" optional = false python-versions = "*" files = [ - {file = "pytz-2023.3.post1-py2.py3-none-any.whl", hash = "sha256:ce42d816b81b68506614c11e8937d3aa9e41007ceb50bfdcb0749b921bf646c7"}, - {file = "pytz-2023.3.post1.tar.gz", hash = "sha256:7b4fddbeb94a1eba4b557da24f19fdf9db575192544270a9101d8509f9f43d7b"}, + {file = "pytz-2024.1-py2.py3-none-any.whl", hash = "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319"}, + {file = "pytz-2024.1.tar.gz", hash = "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812"}, ] [[package]] @@ -495,22 +504,48 @@ urllib3 = ">=1.21.1,<3" socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] +[[package]] +name = "ruff" +version = "0.1.15" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +files = [ + {file = "ruff-0.1.15-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:5fe8d54df166ecc24106db7dd6a68d44852d14eb0729ea4672bb4d96c320b7df"}, + {file = "ruff-0.1.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6f0bfbb53c4b4de117ac4d6ddfd33aa5fc31beeaa21d23c45c6dd249faf9126f"}, + {file = "ruff-0.1.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e0d432aec35bfc0d800d4f70eba26e23a352386be3a6cf157083d18f6f5881c8"}, + {file = "ruff-0.1.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9405fa9ac0e97f35aaddf185a1be194a589424b8713e3b97b762336ec79ff807"}, + {file = "ruff-0.1.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c66ec24fe36841636e814b8f90f572a8c0cb0e54d8b5c2d0e300d28a0d7bffec"}, + {file = "ruff-0.1.15-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:6f8ad828f01e8dd32cc58bc28375150171d198491fc901f6f98d2a39ba8e3ff5"}, + {file = "ruff-0.1.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86811954eec63e9ea162af0ffa9f8d09088bab51b7438e8b6488b9401863c25e"}, + {file = "ruff-0.1.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fd4025ac5e87d9b80e1f300207eb2fd099ff8200fa2320d7dc066a3f4622dc6b"}, + {file = "ruff-0.1.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b17b93c02cdb6aeb696effecea1095ac93f3884a49a554a9afa76bb125c114c1"}, + {file = "ruff-0.1.15-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ddb87643be40f034e97e97f5bc2ef7ce39de20e34608f3f829db727a93fb82c5"}, + {file = "ruff-0.1.15-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:abf4822129ed3a5ce54383d5f0e964e7fef74a41e48eb1dfad404151efc130a2"}, + {file = "ruff-0.1.15-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6c629cf64bacfd136c07c78ac10a54578ec9d1bd2a9d395efbee0935868bf852"}, + {file = "ruff-0.1.15-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1bab866aafb53da39c2cadfb8e1c4550ac5340bb40300083eb8967ba25481447"}, + {file = "ruff-0.1.15-py3-none-win32.whl", hash = "sha256:2417e1cb6e2068389b07e6fa74c306b2810fe3ee3476d5b8a96616633f40d14f"}, + {file = "ruff-0.1.15-py3-none-win_amd64.whl", hash = "sha256:3837ac73d869efc4182d9036b1405ef4c73d9b1f88da2413875e34e0d6919587"}, + {file = "ruff-0.1.15-py3-none-win_arm64.whl", hash = "sha256:9a933dfb1c14ec7a33cceb1e49ec4a16b51ce3c20fd42663198746efc0427360"}, + {file = "ruff-0.1.15.tar.gz", hash = "sha256:f6dfa8c1b21c913c326919056c390966648b680966febcb796cc9d1aaab8564e"}, +] + [[package]] name = "s3transfer" -version = "0.7.0" +version = "0.10.0" description = "An Amazon S3 Transfer Manager" optional = false -python-versions = ">= 3.7" +python-versions = ">= 3.8" files = [ - {file = "s3transfer-0.7.0-py3-none-any.whl", hash = "sha256:10d6923c6359175f264811ef4bf6161a3156ce8e350e705396a7557d6293c33a"}, - {file = "s3transfer-0.7.0.tar.gz", hash = "sha256:fd3889a66f5fe17299fe75b82eae6cf722554edca744ca5d5fe308b104883d2e"}, + {file = "s3transfer-0.10.0-py3-none-any.whl", hash = "sha256:3cdb40f5cfa6966e812209d0994f2a4709b561c88e90cf00c2696d2df4e56b2e"}, + {file = "s3transfer-0.10.0.tar.gz", hash = "sha256:d0c8bbf672d5eebbe4e57945e23b972d963f07d82f661cabf678a5c88831595b"}, ] [package.dependencies] -botocore = ">=1.12.36,<2.0a.0" +botocore = ">=1.33.2,<2.0a.0" [package.extras] -crt = ["botocore[crt] (>=1.20.29,<2.0a.0)"] +crt = ["botocore[crt] (>=1.33.2,<2.0a.0)"] [[package]] name = "six" @@ -577,24 +612,24 @@ testing = ["flaky (>=3.4.0)", "freezegun (>=0.3.11)", "pathlib2 (>=2.3.3)", "psu [[package]] name = "typing-extensions" -version = "4.8.0" +version = "4.9.0" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.8.0-py3-none-any.whl", hash = "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0"}, - {file = "typing_extensions-4.8.0.tar.gz", hash = "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"}, + {file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"}, + {file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"}, ] [[package]] name = "tzdata" -version = "2023.3" +version = "2023.4" description = "Provider of IANA time zone data" optional = false python-versions = ">=2" files = [ - {file = "tzdata-2023.3-py2.py3-none-any.whl", hash = "sha256:7e65763eef3120314099b6939b5546db7adce1e7d6f2e179e3df563c70511eda"}, - {file = "tzdata-2023.3.tar.gz", hash = "sha256:11ef1e08e54acb0d4f95bdb1be05da659673de4acbd21bf9c69e94cc5e907a3a"}, + {file = "tzdata-2023.4-py2.py3-none-any.whl", hash = "sha256:aa3ace4329eeacda5b7beb7ea08ece826c28d761cda36e747cfbf97996d39bf3"}, + {file = "tzdata-2023.4.tar.gz", hash = "sha256:dd54c94f294765522c77399649b4fefd95522479a664a0cec87f41bebc6148c9"}, ] [[package]] @@ -632,13 +667,13 @@ zstd = ["zstandard (>=0.18.0)"] [[package]] name = "virtualenv" -version = "20.24.7" +version = "20.25.0" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.7" files = [ - {file = "virtualenv-20.24.7-py3-none-any.whl", hash = "sha256:a18b3fd0314ca59a2e9f4b556819ed07183b3e9a3702ecfe213f593d44f7b3fd"}, - {file = "virtualenv-20.24.7.tar.gz", hash = "sha256:69050ffb42419c91f6c1284a7b24e0475d793447e35929b488bf6a0aade39353"}, + {file = "virtualenv-20.25.0-py3-none-any.whl", hash = "sha256:4238949c5ffe6876362d9c0180fc6c3a824a7b12b80604eeb8085f2ed7460de3"}, + {file = "virtualenv-20.25.0.tar.gz", hash = "sha256:bf51c0d9c7dd63ea8e44086fa1e4fb1093a31e963b86959257378aef020e1f1b"}, ] [package.dependencies] @@ -657,4 +692,4 @@ events = ["cryptography", "requests"] [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "ff2b20632979ba534df26af89729e58c38d2f74e5931927348c4cf8084a7e90a" +content-hash = "459241f0f359aa2f280c0d1f11f3d6dfa9ac3217e5d6feb7c097cdae6234a688" diff --git a/poetry.toml b/poetry.toml new file mode 100644 index 00000000..ab1033bd --- /dev/null +++ b/poetry.toml @@ -0,0 +1,2 @@ +[virtualenvs] +in-project = true diff --git a/pyproject.toml b/pyproject.toml index e0995779..2010f9ac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,10 +51,41 @@ requests = {version = ">=2.27.1", optional = true} bounce = ["requests", "cryptography"] events = ["requests", "cryptography"] - [tool.poetry.dev-dependencies] +ruff = "^0.1.6" tox = "^3.24.5" [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" + +[tool.ruff] +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # Pyflakes + "N", # pep8-naming + "I", # isort +] + +ignore = [ +] + +# Allow autofix for all enabled rules (when `--fix`) is provided. +fixable = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # Pyflakes + "N", # pep8-naming + "I", # isort +] +unfixable = [] + +# Same as Black. +line-length = 120 + +# Allow unused variables when underscore-prefixed. +dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" + +# Assume Python 3.12 +target-version = "py312" diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 15e2f1b5..00000000 --- a/setup.cfg +++ /dev/null @@ -1,5 +0,0 @@ -# Flake8 still doesn't support pyproject and seems opposed. -# https://github.com/PyCQA/flake8/issues/234 -[flake8] -ignore-names=setUpTestData,setUp,setUpClass,setUpTestData,tearDown -max-line-length = 120 diff --git a/tests/test_backend.py b/tests/test_backend.py index 644bb88a..131df8de 100644 --- a/tests/test_backend.py +++ b/tests/test_backend.py @@ -3,9 +3,9 @@ import email from django.conf import settings as django_settings -from django.utils.encoding import smart_str from django.core.mail import send_mail from django.test import TestCase +from django.utils.encoding import smart_str import django_ses from django_ses import settings @@ -272,7 +272,7 @@ def test_feedback_forwarding(self): send_mail('subject', 'body', 'from@example.com', ['to@example.com']) self.assertEqual(self.outbox.pop()['FeedbackForwardingEmailAddress'], 'reply@example.com') - def test_source_arn_is_NOT_set(self): + def test_source_arn_is_not_set(self): """ Ensure that the helpers for Identity Owner for SES Sending Authorization are not present, if nothing has been configured. @@ -288,7 +288,8 @@ def test_source_arn_is_set(self): settings.AWS_SES_SOURCE_ARN = 'arn:aws:ses:eu-central-1:111111111111:identity/example.com' send_mail('subject', 'body', 'from@example.com', ['to@example.com']) mail = self.outbox.pop() - self.assertEqual(mail['FromEmailAddressIdentityArn'], 'arn:aws:ses:eu-central-1:111111111111:identity/example.com') + self.assertEqual(mail['FromEmailAddressIdentityArn'], + 'arn:aws:ses:eu-central-1:111111111111:identity/example.com') def test_from_arn_takes_precedence_when_source_arn_is_set(self): """ @@ -299,7 +300,8 @@ def test_from_arn_takes_precedence_when_source_arn_is_set(self): settings.AWS_SES_RETURN_PATH_ARN = 'arn:aws:ses:eu-central-1:333333333333:identity/example.com' send_mail('subject', 'body', 'from@example.com', ['to@example.com']) mail = self.outbox.pop() - self.assertEqual(mail['FromEmailAddressIdentityArn'], 'arn:aws:ses:eu-central-1:222222222222:identity/example.com') + self.assertEqual(mail['FromEmailAddressIdentityArn'], + 'arn:aws:ses:eu-central-1:222222222222:identity/example.com') class SESBackendTestInitialize(TestCase): @@ -334,12 +336,12 @@ def setUp(self): def tearDown(self): # Empty outbox everytime test finishes FakeSESConnection.outbox = [] - + def test_from_email(self): settings.AWS_SES_FROM_EMAIL = "my_default_from@example.com" send_mail('subject', 'body', 'ignored_from@example.com', ['to@example.com']) self.assertEqual(self.outbox.pop()['Source'], 'my_default_from@example.com') - + def test_return_path(self): settings.USE_SES_V2 = True settings.AWS_SES_RETURN_PATH = "return@example.com" diff --git a/tests/test_settings.py b/tests/test_settings.py index 92ede6a0..8a0a7908 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -1,5 +1,6 @@ -from django.test import TestCase from django.conf import settings +from django.test import TestCase + from tests.utils import unload_django_ses @@ -42,11 +43,12 @@ def test_ses_region_to_endpoint_default_given(self): unload_django_ses() import django_ses self.assertEqual(django_ses.settings.AWS_SES_REGION_NAME, 'us-east-1') - self.assertEqual(django_ses.settings.AWS_SES_REGION_ENDPOINT, f'email.{django_ses.settings.AWS_SES_REGION_NAME}.amazonaws.com') + self.assertEqual(django_ses.settings.AWS_SES_REGION_ENDPOINT, + f'email.{django_ses.settings.AWS_SES_REGION_NAME}.amazonaws.com') def test_ses_region_to_endpoint_set_given(self): settings.AWS_SES_REGION_NAME = 'eu-west-1' unload_django_ses() import django_ses self.assertEqual(django_ses.settings.AWS_SES_REGION_NAME, 'eu-west-1') - self.assertEqual(django_ses.settings.AWS_SES_REGION_ENDPOINT, 'email.eu-west-1.amazonaws.com') \ No newline at end of file + self.assertEqual(django_ses.settings.AWS_SES_REGION_ENDPOINT, 'email.eu-west-1.amazonaws.com') diff --git a/tests/test_stats.py b/tests/test_stats.py index 351a899d..ce47a912 100644 --- a/tests/test_stats.py +++ b/tests/test_stats.py @@ -1,6 +1,6 @@ -import pytz from datetime import datetime +import pytz from django.test import TestCase from django_ses.views import emails_parse, stats_to_list, sum_stats diff --git a/tests/test_urls.py b/tests/test_urls.py index 01d005ed..4bce3ed3 100644 --- a/tests/test_urls.py +++ b/tests/test_urls.py @@ -1,6 +1,6 @@ from django.urls import re_path -from django_ses.views import DashboardView, handle_bounce, SESEventWebhookView +from django_ses.views import DashboardView, SESEventWebhookView, handle_bounce urlpatterns = [ re_path(r'^dashboard/$', DashboardView.as_view(), name='django_ses_stats'), diff --git a/tests/test_verifier.py b/tests/test_verifier.py index c47b2737..aadcb871 100644 --- a/tests/test_verifier.py +++ b/tests/test_verifier.py @@ -32,19 +32,63 @@ class BounceMessageVerifierTest(TestCase): "Type": "Notification", } - VALID_CERT = b"-----BEGIN CERTIFICATE-----\nMIIF1zCCBL+gAwIBAgIQB9pYWG3Mi7xej22g9pobJTANBgkqhkiG9w0BAQsFADBG\nMQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRUwEwYDVQQLEwxTZXJ2ZXIg\nQ0EgMUIxDzANBgNVBAMTBkFtYXpvbjAeFw0yMTA5MDcwMDAwMDBaFw0yMjA4MTcy\nMzU5NTlaMBwxGjAYBgNVBAMTEXNucy5hbWF6b25hd3MuY29tMIIBIjANBgkqhkiG\n9w0BAQEFAAOCAQ8AMIIBCgKCAQEAutFqueT3XgP13udzxE6UpbdjOtVO5DwoMpSM\niDNMnGzF1TYH5/R2LPUOBeTB0SkKnR4kpNcUZhicpGD4aKciz/GEZ6wu65xncfT9\nH/KBOQwoXYTuClHwp6fYpGzcGFaFoEYMnijL/o4qmTSd+ukglQUgKpsDw4ofw6rU\nm2CttJo+GQSNQ9NfGR1h/0J+zsApkeSYrXRx5wNlu87z8os1C/6PBrUHwt3xXeaf\nXzfwut8aRRYsS8BySOA9DAgLfNHlfdQCjKPXKrG/ussgReyWD6n/HH+j7Uha3xos\nTzQqJifcxlTq6MxWdPR6fDaJNvqw6DOE7UjUNxHguXHlVfxhlQIDAQABo4IC6TCC\nAuUwHwYDVR0jBBgwFoAUWaRmBlKge5WSPKOUByeWdFv5PdAwHQYDVR0OBBYEFAqz\nC+vyouneE7mWWLbi9i0UsWUbMBwGA1UdEQQVMBOCEXNucy5hbWF6b25hd3MuY29t\nMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIw\nOwYDVR0fBDQwMjAwoC6gLIYqaHR0cDovL2NybC5zY2ExYi5hbWF6b250cnVzdC5j\nb20vc2NhMWIuY3JsMBMGA1UdIAQMMAowCAYGZ4EMAQIBMHUGCCsGAQUFBwEBBGkw\nZzAtBggrBgEFBQcwAYYhaHR0cDovL29jc3Auc2NhMWIuYW1hem9udHJ1c3QuY29t\nMDYGCCsGAQUFBzAChipodHRwOi8vY3J0LnNjYTFiLmFtYXpvbnRydXN0LmNvbS9z\nY2ExYi5jcnQwDAYDVR0TAQH/BAIwADCCAX0GCisGAQQB1nkCBAIEggFtBIIBaQFn\nAHYAKXm+8J45OSHwVnOfY6V35b5XfZxgCvj5TV0mXCVdx4QAAAF7vfDVkQAABAMA\nRzBFAiEA2XfHuy36aqRFiaL8c3md2mH451go8707+fRE0pEdSRACIE/g5FXTUXUZ\nPFcmOhm9TZ+uMY1i4CIQ/CKVWln6C3t+AHYAUaOw9f0BeZxWbbg3eI8MpHrMGyfL\n956IQpoN/tSLBeUAAAF7vfDVjAAABAMARzBFAiBF1MhhFP0+FQt3daDFfMYoWwnr\nmuTInrjNpwfzlvQBugIhAPYadFzr+LaxSJoiZEbEHBvTts7bT0M3eCQONA2O7w6n\nAHUAQcjKsd8iRkoQxqE6CUKHXk4xixsD6+tLx2jwkGKWBvYAAAF7vfDVdAAABAMA\nRjBEAiAtPapmFAuA71ih4NoSd5hJelzAltNQpxDMcDfDyHyU8gIgWxmaa6+2KbBu\n9xdv379zvnJACFR7jc+4asl08Dn4aagwDQYJKoZIhvcNAQELBQADggEBAA54QX0u\noFWXfMmv02CGZv4NWo5TapyeeixQ2kKpZHRdVZjxZrw+hoF6HD7P3kGjH8ztyJll\ntDxB0qgMltbPhQdScwhA6iTgoaBYqEUC/VHKd4PmmPT6yIsM36NBZVmkGlzl5uNo\n/dBgBaG0SsVJnhr5zro3c2quC7n6fVGEZhf/UgQwRnnvThnvbNKguglDMq4uEqv8\nnjKyleht+glkcmXO0m9qLKt6BOS0amy6U2GlAwRn0Wx02ndJtnRCSC6kPuRWK/SQ\nFEjB7gCK4hdKaAOuWdZpI55vF6ifOeM8toC3g7ofO8qLTnJupAG+ZitY5J3cvHWr\nHqOUdKigPDHYLRo=\n-----END CERTIFICATE-----\n" + VALID_CERT = (b"-----BEGIN CERTIFICATE-----\nMIIF1zCCBL+gAwIBAgIQB9pYWG3Mi7xej22g9pobJTANBgkqhkiG9w0BAQsFADBG\n" + b"MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRUwEwYDVQQLEwxTZXJ2ZXIg\nQ0EgMUIxDzANBgNVBAMTBkFtYXpvb" + b"jAeFw0yMTA5MDcwMDAwMDBaFw0yMjA4MTcy\nMzU5NTlaMBwxGjAYBgNVBAMTEXNucy5hbWF6b25hd3MuY29tMIIBIjANBg" + b"kqhkiG\n9w0BAQEFAAOCAQ8AMIIBCgKCAQEAutFqueT3XgP13udzxE6UpbdjOtVO5DwoMpSM\niDNMnGzF1TYH5/R2LPUOB" + b"eTB0SkKnR4kpNcUZhicpGD4aKciz/GEZ6wu65xncfT9\nH/KBOQwoXYTuClHwp6fYpGzcGFaFoEYMnijL/o4qmTSd+ukglQ" + b"UgKpsDw4ofw6rU\nm2CttJo+GQSNQ9NfGR1h/0J+zsApkeSYrXRx5wNlu87z8os1C/6PBrUHwt3xXeaf\nXzfwut8aRRYsS" + b"8BySOA9DAgLfNHlfdQCjKPXKrG/ussgReyWD6n/HH+j7Uha3xos\nTzQqJifcxlTq6MxWdPR6fDaJNvqw6DOE7UjUNxHguX" + b"HlVfxhlQIDAQABo4IC6TCC\nAuUwHwYDVR0jBBgwFoAUWaRmBlKge5WSPKOUByeWdFv5PdAwHQYDVR0OBBYEFAqz\nC+vyo" + b"uneE7mWWLbi9i0UsWUbMBwGA1UdEQQVMBOCEXNucy5hbWF6b25hd3MuY29t\nMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFj" + b"AUBggrBgEFBQcDAQYIKwYBBQUHAwIw\nOwYDVR0fBDQwMjAwoC6gLIYqaHR0cDovL2NybC5zY2ExYi5hbWF6b250cnVzdC5" + b"j\nb20vc2NhMWIuY3JsMBMGA1UdIAQMMAowCAYGZ4EMAQIBMHUGCCsGAQUFBwEBBGkw\nZzAtBggrBgEFBQcwAYYhaHR0cD" + b"ovL29jc3Auc2NhMWIuYW1hem9udHJ1c3QuY29t\nMDYGCCsGAQUFBzAChipodHRwOi8vY3J0LnNjYTFiLmFtYXpvbnRydXN" + b"0LmNvbS9z\nY2ExYi5jcnQwDAYDVR0TAQH/BAIwADCCAX0GCisGAQQB1nkCBAIEggFtBIIBaQFn\nAHYAKXm+8J45OSHwVn" + b"OfY6V35b5XfZxgCvj5TV0mXCVdx4QAAAF7vfDVkQAABAMA\nRzBFAiEA2XfHuy36aqRFiaL8c3md2mH451go8707+fRE0pE" + b"dSRACIE/g5FXTUXUZ\nPFcmOhm9TZ+uMY1i4CIQ/CKVWln6C3t+AHYAUaOw9f0BeZxWbbg3eI8MpHrMGyfL\n956IQpoN/t" + b"SLBeUAAAF7vfDVjAAABAMARzBFAiBF1MhhFP0+FQt3daDFfMYoWwnr\nmuTInrjNpwfzlvQBugIhAPYadFzr+LaxSJoiZEb" + b"EHBvTts7bT0M3eCQONA2O7w6n\nAHUAQcjKsd8iRkoQxqE6CUKHXk4xixsD6+tLx2jwkGKWBvYAAAF7vfDVdAAABAMA\nRj" + b"BEAiAtPapmFAuA71ih4NoSd5hJelzAltNQpxDMcDfDyHyU8gIgWxmaa6+2KbBu\n9xdv379zvnJACFR7jc+4asl08Dn4aag" + b"wDQYJKoZIhvcNAQELBQADggEBAA54QX0u\noFWXfMmv02CGZv4NWo5TapyeeixQ2kKpZHRdVZjxZrw+hoF6HD7P3kGjH8zt" + b"yJll\ntDxB0qgMltbPhQdScwhA6iTgoaBYqEUC/VHKd4PmmPT6yIsM36NBZVmkGlzl5uNo\n/dBgBaG0SsVJnhr5zro3c2q" + b"uC7n6fVGEZhf/UgQwRnnvThnvbNKguglDMq4uEqv8\nnjKyleht+glkcmXO0m9qLKt6BOS0amy6U2GlAwRn0Wx02ndJtnRC" + b"SC6kPuRWK/SQ\nFEjB7gCK4hdKaAOuWdZpI55vF6ifOeM8toC3g7ofO8qLTnJupAG+ZitY5J3cvHWr\nHqOUdKigPDHYLRo" + b"=\n-----END CERTIFICATE-----\n") # Any changes to this message will break the signature validity test. valid_msg = { "Type": "Notification", "MessageId": "97903a0c-8818-5442-94b2-60b0c63d3da0", "TopicArn": "arn:aws:sns:us-east-1:364852123998:test-email", "Subject": "Amazon SES Email Event Notification", - "Message": '{"eventType":"Complaint","complaint":{"feedbackId":"0100017fd2f87864-7f070191-4eb1-49e3-a42e-7e996b718724-000000","complaintSubType":null,"complainedRecipients":[{"emailAddress":"complaint@simulator.amazonses.com"}],"timestamp":"2022-03-28T23:59:33.564Z","userAgent":"Amazon SES Mailbox Simulator","complaintFeedbackType":"abuse","arrivalDate":"2022-03-28T23:59:33.564Z"},"mail":{"timestamp":"2022-03-28T23:59:32.804Z","source":"jesus.islasf@alumno.buap.mx","sourceArn":"arn:aws:ses:us-east-1:364852123998:identity/jesus.islasf@alumno.buap.mx","sendingAccountId":"364852123998","messageId":"0100017fd2f875c4-c2929163-2767-49b1-8513-1a29e428e134-000000","destination":["complaint@simulator.amazonses.com"],"headersTruncated":false,"headers":[{"name":"From","value":"jesus.islasf@alumno.buap.mx"},{"name":"To","value":"complaint@simulator.amazonses.com"},{"name":"Subject","value":"Complaint Notification"},{"name":"MIME-Version","value":"1.0"},{"name":"Content-Type","value":"multipart/alternative; boundary=\\"----=_Part_1160373_1543555432.1648511972808\\""}],"commonHeaders":{"from":["jesus.islasf@alumno.buap.mx"],"to":["complaint@simulator.amazonses.com"],"messageId":"0100017fd2f875c4-c2929163-2767-49b1-8513-1a29e428e134-000000","subject":"Complaint Notification"},"tags":{"ses:operation":["SendEmail"],"ses:configuration-set":["test-set"],"ses:source-ip":["189.203.131.80"],"ses:from-domain":["alumno.buap.mx"],"ses:caller-identity":["root"]}}}\n', + "Message": '{"eventType":"Complaint","complaint":{"feedbackId":"0100017fd2f87864-7f070191-4eb1-49e3-a42e-' + '7e996b718724-000000","complaintSubType":null,"complainedRecipients":[{"emailAddress":' + '"complaint@simulator.amazonses.com"}],"timestamp":"2022-03-28T23:59:33.564Z","userAgent":' + '"Amazon SES Mailbox Simulator","complaintFeedbackType":"abuse","arrivalDate":' + '"2022-03-28T23:59:33.564Z"},"mail":{"timestamp":"2022-03-28T23:59:32.804Z","source":' + '"jesus.islasf@alumno.buap.mx","sourceArn":"arn:aws:ses:us-east-1:364852123998:' + 'identity/jesus.islasf@alumno.buap.mx","sendingAccountId":"364852123998","messageId":' + '"0100017fd2f875c4-c2929163-2767-49b1-8513-1a29e428e134-000000","destination":' + '["complaint@simulator.amazonses.com"],"headersTruncated":false,"headers":[{"name":"From",' + '"value":"jesus.islasf@alumno.buap.mx"},{"name":"To","value":"complaint@simulator.amazonses.com"},' + '{"name":"Subject","value":"Complaint Notification"},{"name":"MIME-Version","value":"1.0"},' + '{"name":"Content-Type","value":"multipart/alternative; ' + 'boundary=\\"----=_Part_1160373_1543555432.1648511972808\\""}],"commonHeaders":{"from":' + '["jesus.islasf@alumno.buap.mx"],"to":["complaint@simulator.amazonses.com"],"messageId":' + '"0100017fd2f875c4-c2929163-2767-49b1-8513-1a29e428e134-000000","subject":' + '"Complaint Notification"},"tags":{"ses:operation":["SendEmail"],"ses:configuration-set":' + '["test-set"],"ses:source-ip":["189.203.131.80"],"ses:from-domain":["alumno.buap.mx"],' + '"ses:caller-identity":["root"]}}}\n', "Timestamp": "2022-03-28T23:59:33.713Z", "SignatureVersion": "1", - "Signature": "MAnUSvI5C6S0GERh05oFiMtpZuGTW0J/6I/AaBinsZWK+Na8TwJsiNSUjShgZ8NItzQkjBnY1R1qT0wtPf6FfNXxGu3oQRaYsj1alz5NJyiuZwGlryX9LHuOoSJ7tGhrKA5yfYI1JPZsdUJKnkI3+UgKbIg7ml2FoSMU8s3HP3V/FOhvp6V5P6yt2okxABbw13WQPrzeUCZ9pRLgB3TnY59wsWM2SlynWEG/u/pFHyzuvkmtrGZtjZfITm7bfMnGy8FTOox4PfzCu4bysKlUbhc/yJ0fzI/+XsT2gKasXETzlmx6vd4qKgKWP5U9OJVh+Cx//npFDCBI2Tba8JK+Cg==", - "SigningCertURL": "https://sns.us-east-1.amazonaws.com/SimpleNotificationService-7ff5318490ec183fbaddaa2a969abfda.pem", - "UnsubscribeURL": "https://sns.us-east-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:us-east-1:364852123998:test-email:0fe0bffc-6470-4502-9414-d5e3d9fdd71e", + "Signature": "MAnUSvI5C6S0GERh05oFiMtpZuGTW0J/6I/AaBinsZWK+Na8TwJsiNSUjShgZ8NItzQkjBnY1R1qT0wtPf6FfNXxGu3" + "oQRaYsj1alz5NJyiuZwGlryX9LHuOoSJ7tGhrKA5yfYI1JPZsdUJKnkI3+UgKbIg7ml2FoSMU8s3HP3V/FOhvp6V5P6y" + "t2okxABbw13WQPrzeUCZ9pRLgB3TnY59wsWM2SlynWEG/u/pFHyzuvkmtrGZtjZfITm7bfMnGy8FTOox4PfzCu4bysKl" + "Ubhc/yJ0fzI/+XsT2gKasXETzlmx6vd4qKgKWP5U9OJVh+Cx//npFDCBI2Tba8JK+Cg==", + "SigningCertURL": + "https://sns.us-east-1.amazonaws.com/SimpleNotificationService-7ff5318490ec183fbaddaa2a969abfda.pem", + "UnsubscribeURL": "https://sns.us-east-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:" + "us-east-1:364852123998:test-email:0fe0bffc-6470-4502-9414-d5e3d9fdd71e", } valid_msg_missing_fields = { @@ -53,12 +97,32 @@ class BounceMessageVerifierTest(TestCase): "TopicArn": "arn:aws:sns:us-east-1:364852123998:test-email", # Normally you'd see a subject key/value pair here, but not on this # notification. - "Message": '{"notificationType":"Bounce","bounce":{"feedbackId":"0100017fe7044080-b95aeb18-3fb8-408a-be9b-15fdbac3945a-000000","bounceType":"Permanent","bounceSubType":"General","bouncedRecipients":[{"emailAddress":"bounce@simulator.amazonses.com","action":"failed","status":"5.1.1","diagnosticCode":"smtp; 550 5.1.1 user unknown"}],"timestamp":"2022-04-01T21:24:49.000Z","remoteMtaIp":"3.231.136.178","reportingMTA":"dns; a48-30.smtp-out.amazonses.com"},"mail":{"timestamp":"2022-04-01T21:24:49.422Z","source":"jesus.islasf@alumno.buap.mx","sourceArn":"arn:aws:ses:us-east-1:364852123998:identity/jesus.islasf@alumno.buap.mx","sourceIp":"189.203.131.80","sendingAccountId":"364852123998","messageId":"0100017fe7043e8e-fc9c357d-a9a4-49bc-a4b4-fa984009d568-000000","destination":["bounce@simulator.amazonses.com"],"headersTruncated":false,"headers":[{"name":"From","value":"jesus.islasf@alumno.buap.mx"},{"name":"To","value":"bounce@simulator.amazonses.com"},{"name":"Subject","value":"Subject Test"},{"name":"MIME-Version","value":"1.0"},{"name":"Content-Type","value":"multipart/alternative; boundary=\\"----=_Part_2196631_1474166380.1648848289426\\""}],"commonHeaders":{"from":["jesus.islasf@alumno.buap.mx"],"to":["bounce@simulator.amazonses.com"],"subject":"Subject Test"}}}', + "Message": '{"notificationType":"Bounce","bounce":{"feedbackId":"0100017fe7044080-b95aeb18-3fb8-408a-be9b-' + '15fdbac3945a-000000","bounceType":"Permanent","bounceSubType":"General","bouncedRecipients":[' + '{"emailAddress":"bounce@simulator.amazonses.com","action":"failed","status":"5.1.1",' + '"diagnosticCode":"smtp; 550 5.1.1 user unknown"}],"timestamp":"2022-04-01T21:24:49.000Z",' + '"remoteMtaIp":"3.231.136.178","reportingMTA":"dns; a48-30.smtp-out.amazonses.com"},"mail":' + '{"timestamp":"2022-04-01T21:24:49.422Z","source":"jesus.islasf@alumno.buap.mx","sourceArn":' + '"arn:aws:ses:us-east-1:364852123998:identity/jesus.islasf@alumno.buap.mx","sourceIp":' + '"189.203.131.80","sendingAccountId":"364852123998","messageId":' + '"0100017fe7043e8e-fc9c357d-a9a4-49bc-a4b4-fa984009d568-000000","destination":' + '["bounce@simulator.amazonses.com"],"headersTruncated":false,"headers":[{"name":' + '"From","value":"jesus.islasf@alumno.buap.mx"},{"name":"To","value":' + '"bounce@simulator.amazonses.com"},{"name":"Subject","value":"Subject Test"},' + '{"name":"MIME-Version","value":"1.0"},{"name":"Content-Type","value":' + '"multipart/alternative; boundary=\\"----=_Part_2196631_1474166380.1648848289426\\""}],' + '"commonHeaders":{"from":["jesus.islasf@alumno.buap.mx"],"to":["bounce@simulator.amazonses.com"],' + '"subject":"Subject Test"}}}', "Timestamp": "2022-04-01T21:24:50.200Z", "SignatureVersion": "1", - "Signature": "K27nwTcT0qTPKEQP3noOWV21gDGB6XnLXSwN2i+4176naErwyDSd72w44UesYE/KaRXU+Kusi7b8uoLYPOcYXHH45UCFMrStf9nzu0uIZdMSd7cFGPm0KAxqoDcP9UQw0+ssK1rjVWkywTYmeDyFF/j3IQZxTA/vINOLYbrmMKhyJUPjcZZwdgLmlKcNfKJ5PKmg5WXlr8nWtjW3K+k725nkoAZemuAFt3PmA2k35JoHphkcOBjV2f1qR9zJTOgrVQ1d6k2v6t8G7Nlg6FP5OiwThgKHkehIPfJfLhTmo05tfPCBzXYMzDbnX+HLidvkyibHlalRl/DuDxXXL7SUiA==", - "SigningCertURL": "https://sns.us-east-1.amazonaws.com/SimpleNotificationService-7ff5318490ec183fbaddaa2a969abfda.pem", - "UnsubscribeURL": "https://sns.us-east-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:us-east-1:364852123998:test-email:0fe0bffc-6470-4502-9414-d5e3d9fdd71e", + "Signature": "K27nwTcT0qTPKEQP3noOWV21gDGB6XnLXSwN2i+4176naErwyDSd72w44UesYE/KaRXU+Kusi7b8uoLYPOcYXHH45UCFMrSt" + "f9nzu0uIZdMSd7cFGPm0KAxqoDcP9UQw0+ssK1rjVWkywTYmeDyFF/j3IQZxTA/vINOLYbrmMKhyJUPjcZZwdgLmlKcNfKJ5" + "PKmg5WXlr8nWtjW3K+k725nkoAZemuAFt3PmA2k35JoHphkcOBjV2f1qR9zJTOgrVQ1d6k2v6t8G7Nlg6FP5OiwThgKHkehI" + "PfJfLhTmo05tfPCBzXYMzDbnX+HLidvkyibHlalRl/DuDxXXL7SUiA==", + "SigningCertURL": "https://sns.us-east-1.amazonaws.com/SimpleNotificationService-" + "7ff5318490ec183fbaddaa2a969abfda.pem", + "UnsubscribeURL": "https://sns.us-east-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=" + "arn:aws:sns:us-east-1:364852123998:test-email:0fe0bffc-6470-4502-9414-d5e3d9fdd71e", } def tearDown(self): diff --git a/tests/test_views.py b/tests/test_views.py index 41ef1279..d356e6b9 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -8,9 +8,15 @@ from django.test import TestCase from django.urls import reverse -from django_ses.signals import bounce_received, complaint_received, send_received, delivery_received, open_received, \ - click_received from django_ses import utils as ses_utils +from django_ses.signals import ( + bounce_received, + click_received, + complaint_received, + delivery_received, + open_received, + send_received, +) def get_mock_email(): @@ -125,7 +131,8 @@ def get_mock_click(): click = { "timestamp": "2021-01-26T14:38:55.540Z", "ipAddress": "11.111.11.111", - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Safari/605.1.15", + "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) " + "Version/14.0 Safari/605.1.15", "link": "github.com/django-ses/django-ses", "linkTags": None } @@ -171,7 +178,8 @@ def _handler(sender, mail_obj, bounce_obj, raw_message, **kwargs): with mock.patch.object(ses_utils, "verify_event_message") as verify: verify.return_value = True - response = self.client.post(reverse("django_ses_bounce"), json.dumps(notification), content_type="application/json") + response = self.client.post(reverse("django_ses_bounce"), json.dumps(notification), + content_type="application/json") self.assertEqual(response.status_code, 200) self.assertEqual(_handler.call_count, 1) @@ -193,7 +201,8 @@ def _handler(sender, mail_obj, bounce_obj, raw_message, **kwargs): with mock.patch.object(ses_utils, "verify_event_message") as verify: verify.return_value = True - response = self.client.post(reverse("django_ses_bounce"), json.dumps(notification), content_type="application/json") + response = self.client.post(reverse("django_ses_bounce"), json.dumps(notification), + content_type="application/json") self.assertEqual(response.status_code, 200) self.assertEqual(_handler.call_count, 1) @@ -230,7 +239,8 @@ def _handler(sender, mail_obj, bounce_obj, raw_message, **kwargs): # Mock the verification with mock.patch.object(ses_utils, "verify_event_message") as verify: verify.return_value = True - response = self.client.post(reverse("event_webhook"), json.dumps(notification), content_type="application/json") + response = self.client.post(reverse("event_webhook"), json.dumps(notification), + content_type="application/json") self.assertEqual(response.status_code, 200) self.assertEqual(_handler.call_count, 1) @@ -252,7 +262,8 @@ def _handler(sender, mail_obj, bounce_obj, raw_message, **kwargs): # Mock the verification with mock.patch.object(ses_utils, "verify_event_message") as verify: verify.return_value = True - response = self.client.post(reverse("event_webhook"), json.dumps(notification), content_type="application/json") + response = self.client.post(reverse("event_webhook"), json.dumps(notification), + content_type="application/json") self.assertEqual(response.status_code, 200) self.assertEqual(_handler.call_count, 1) @@ -274,7 +285,8 @@ def _handler(sender, mail_obj, send_obj, raw_message, **kwargs): # Mock the verification with mock.patch.object(ses_utils, "verify_event_message") as verify: verify.return_value = True - response = self.client.post(reverse("event_webhook"), json.dumps(notification), content_type="application/json") + response = self.client.post(reverse("event_webhook"), json.dumps(notification), + content_type="application/json") self.assertEqual(response.status_code, 200) self.assertEqual(_handler.call_count, 1) @@ -295,7 +307,8 @@ def _handler(sender, mail_obj, delivery_obj, raw_message, **kwargs): # Mock the verification with mock.patch.object(ses_utils, "verify_event_message") as verify: verify.return_value = True - response = self.client.post(reverse("event_webhook"), json.dumps(notification), content_type="application/json") + response = self.client.post(reverse("event_webhook"), json.dumps(notification), + content_type="application/json") self.assertEqual(response.status_code, 200) self.assertEqual(_handler.call_count, 1) @@ -316,7 +329,8 @@ def _handler(sender, mail_obj, open_obj, raw_message, **kwargs): # Mock the verification with mock.patch.object(ses_utils, "verify_event_message") as verify: verify.return_value = True - response = self.client.post(reverse("event_webhook"), json.dumps(notification), content_type="application/json") + response = self.client.post(reverse("event_webhook"), json.dumps(notification), + content_type="application/json") self.assertEqual(response.status_code, 200) self.assertEqual(_handler.call_count, 1) @@ -337,7 +351,8 @@ def _handler(sender, mail_obj, click_obj, raw_message, **kwargs): # Mock the verification with mock.patch.object(ses_utils, "verify_event_message") as verify: verify.return_value = True - response = self.client.post(reverse("event_webhook"), json.dumps(notification), content_type="application/json") + response = self.client.post(reverse("event_webhook"), json.dumps(notification), + content_type="application/json") self.assertEqual(response.status_code, 200) self.assertEqual(_handler.call_count, 1) @@ -358,6 +373,7 @@ def test_bad_signature(self): # Mock the verification with mock.patch.object(ses_utils, "verify_event_message") as verify: verify.return_value = False - response = self.client.post(reverse("event_webhook"), json.dumps(notification), content_type="application/json") + response = self.client.post(reverse("event_webhook"), json.dumps(notification), + content_type="application/json") self.assertEqual(response.status_code, 400) self.assertEqual(response.content, "Signature verification failed.".encode())