diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index e6193e59..442179bd 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -4,30 +4,24 @@ "dockerfile": "Dockerfile", "context": ".." }, - "remoteEnv": { "PATH": "/home/vscode/.local/bin:${containerEnv:PATH}" // give our installed Python modules precedence }, - "containerEnv": { "SHELL": "/bin/zsh" }, - "settings": { - "python.linting.enabled": true, - "python.linting.pylintEnabled": true, - "python.linting.pylintPath": "/usr/local/bin/pylint", + "python.linting.": true, "python.pythonPath": "/usr/local/bin/python", }, - "extensions": [ "donjayamanne.python-extension-pack", "ms-python.python", "ms-python.vscode-pylance", "eamodio.gitlens", "GitHub.copilot", - "bungcip.better-toml", + "tamasfe.even-better-toml", + "charliermarsh.ruff", ], - "postCreateCommand": "notify-dev-entrypoint.sh", -} +} \ No newline at end of file diff --git a/.devcontainer/scripts/notify-dev-entrypoint.sh b/.devcontainer/scripts/notify-dev-entrypoint.sh index d0ce71c0..c7097d28 100755 --- a/.devcontainer/scripts/notify-dev-entrypoint.sh +++ b/.devcontainer/scripts/notify-dev-entrypoint.sh @@ -11,7 +11,7 @@ set -ex echo -e "fpath+=/.zfunc" >> ~/.zshrc echo -e "autoload -Uz compinit && compinit" -pip install poetry==${POETRY_VERSION} \ +pip install poetry==${POETRY_VERSION} poetry-plugin-sort \ && poetry --version # Initialize poetry autocompletions diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 582d7a70..00000000 --- a/.flake8 +++ /dev/null @@ -1,10 +0,0 @@ -[flake8] -# Rule definitions: http://flake8.pycqa.org/en/latest/user/error-codes.html -# W503: line break before binary operator -exclude = venv*,env*,__pycache__,cache -ignore = W503 -max-complexity = 8 -max-line-length = 130 -per-file-ignores = - # line too long + print statement usage - .github/actions/waffles/waffles.py: E501, T001 \ No newline at end of file diff --git a/.github/actions/waffles/waffles.py b/.github/actions/waffles/waffles.py index f2d6a2f2..97effc72 100644 --- a/.github/actions/waffles/waffles.py +++ b/.github/actions/waffles/waffles.py @@ -19,27 +19,28 @@ --base-url=: Base URL used to hit the application with discovered Flask endpoints. Example: - waffles.py list --app-loc /Projects/cds/notification-document-download-api --app-lib doc-api-env/Lib/site-packages --flask-mod application --flask-prop application - waffles.py iron --base-url=https://api.document.notification.canada.ca --app-loc /Projects/cds/notification-document-download-api --app-lib doc-api-env/Lib/site-packages --flask-mod application --flask-prop application + waffles.py list --app-loc /Projects/cds/notification-document-download-api \ + --app-lib doc-api-env/Lib/site-packages --flask-mod application --flask-prop application + waffles.py iron --base-url=https://api.document.notification.canada.ca --app-loc /Projects/cds/notification-document-download-api \ + --app-lib doc-api-env/Lib/site-packages --flask-mod application --flask-prop application """ import importlib import importlib.util +import re import sys import urllib.parse import uuid - from dataclasses import dataclass -from docopt import docopt -from flask import Flask from os.path import join from pathlib import Path -import re from types import ModuleType from typing import Any, List, NewType from urllib import request from urllib.error import URLError +from docopt import docopt +from flask import Flask from notifications_utils.base64_uuid import uuid_to_base64 ModuleName = NewType("ModuleName", str) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 0dbda114..293fa184 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -13,7 +13,7 @@ jobs: - name: Install poetry env: POETRY_VERSION: "1.7.1" - run: pip install poetry==${POETRY_VERSION} && poetry --version + run: pip install poetry==${POETRY_VERSION} poetry-plugin-sort && poetry --version - name: Install requirements run: poetry install diff --git a/.gitignore b/.gitignore index 3d28606b..0e675865 100644 --- a/.gitignore +++ b/.gitignore @@ -63,5 +63,8 @@ target/ /cache /venv +# Ruff formatter cache +.ruff_cache/ + # Linux and Mac Dev Environment .DS_Store diff --git a/Makefile b/Makefile index d0895512..fb55d236 100644 --- a/Makefile +++ b/Makefile @@ -57,9 +57,10 @@ clean-docker-containers: ## Clean up any remaining docker containers .PHONY: format format: - poetry run black --config pyproject.toml . - poetry run flake8 . + ruff check --select I --fix . + ruff format . poetry run mypy . + poetry sort clean: rm -rf cache venv \ No newline at end of file diff --git a/notifications_utils/base64_uuid.py b/notifications_utils/base64_uuid.py index 5df6b48a..9721bf2e 100644 --- a/notifications_utils/base64_uuid.py +++ b/notifications_utils/base64_uuid.py @@ -1,5 +1,5 @@ +from base64 import urlsafe_b64decode, urlsafe_b64encode from uuid import UUID -from base64 import urlsafe_b64encode, urlsafe_b64decode def base64_to_bytes(key): diff --git a/notifications_utils/clients/redis/redis_client.py b/notifications_utils/clients/redis/redis_client.py index e300fce8..c85b2678 100644 --- a/notifications_utils/clients/redis/redis_client.py +++ b/notifications_utils/clients/redis/redis_client.py @@ -3,8 +3,8 @@ from time import time from typing import Any, Dict -from flask_redis import FlaskRedis from flask import current_app +from flask_redis import FlaskRedis # expose redis exceptions so that they can be caught from redis.exceptions import RedisError # noqa diff --git a/notifications_utils/clients/zendesk/zendesk_client.py b/notifications_utils/clients/zendesk/zendesk_client.py index 2c76000e..0ef630b6 100644 --- a/notifications_utils/clients/zendesk/zendesk_client.py +++ b/notifications_utils/clients/zendesk/zendesk_client.py @@ -70,7 +70,9 @@ def create_ticket( data["ticket"]["requester"] = {"email": user_email, "name": user_name or "(no name supplied)"} response = requests.post( - self.ZENDESK_TICKET_URL, json=data, auth=("{}/token".format(self.NOTIFY_ZENDESK_EMAIL), self.api_key) # type: ignore + self.ZENDESK_TICKET_URL, + json=data, + auth=("{}/token".format(self.NOTIFY_ZENDESK_EMAIL), self.api_key), # type: ignore ) if response.status_code != 201: diff --git a/notifications_utils/decorators.py b/notifications_utils/decorators.py index f908f138..409089d3 100644 --- a/notifications_utils/decorators.py +++ b/notifications_utils/decorators.py @@ -1,6 +1,7 @@ -from flask import current_app from functools import wraps +from flask import current_app + def requires_feature(flag): def decorator_feature_flag(func): diff --git a/notifications_utils/field.py b/notifications_utils/field.py index 699c91e0..0696d8be 100644 --- a/notifications_utils/field.py +++ b/notifications_utils/field.py @@ -1,15 +1,15 @@ import re from typing import Any, Callable, Dict, List, Literal, Optional -from ordered_set import OrderedSet from flask import Markup +from ordered_set import OrderedSet from notifications_utils.columns import Columns from notifications_utils.formatters import ( - unescaped_formatted_list, - strip_html, escape_html, strip_dvla_markup, + strip_html, + unescaped_formatted_list, ) diff --git a/notifications_utils/formatters.py b/notifications_utils/formatters.py index 72a5c26d..cb83e987 100644 --- a/notifications_utils/formatters.py +++ b/notifications_utils/formatters.py @@ -1,16 +1,17 @@ -import string import re -from typing import List +import string import urllib +from itertools import count +from typing import List -import mistune import bleach -from itertools import count +import mistune +import smartypants from flask import Markup -from . import email_with_smart_quotes_regex + from notifications_utils.sanitise_text import SanitiseSMS -import smartypants +from . import email_with_smart_quotes_regex LINK_STYLE = "word-wrap: break-word;" diff --git a/notifications_utils/international_billing_rates.py b/notifications_utils/international_billing_rates.py index b3a25c7e..027168f6 100644 --- a/notifications_utils/international_billing_rates.py +++ b/notifications_utils/international_billing_rates.py @@ -18,9 +18,10 @@ - Dominican Republic """ -import yaml import os +import yaml + dir_path = os.path.dirname(os.path.realpath(__file__)) with open("{}/international_billing_rates.yml".format(dir_path)) as f: diff --git a/notifications_utils/letter_timings.py b/notifications_utils/letter_timings.py index f41164d9..c72aefed 100644 --- a/notifications_utils/letter_timings.py +++ b/notifications_utils/letter_timings.py @@ -1,10 +1,9 @@ -import pytz - -from datetime import datetime, time, timedelta from collections import namedtuple +from datetime import datetime, time, timedelta -from notifications_utils.timezones import convert_utc_to_est, utc_string_to_aware_gmt_datetime +import pytz +from notifications_utils.timezones import convert_utc_to_est, utc_string_to_aware_gmt_datetime LETTER_PROCESSING_DEADLINE = time(17, 30) CANCELLABLE_JOB_LETTER_STATUSES = [ diff --git a/notifications_utils/logging.py b/notifications_utils/logging.py index 2ccb5bff..b79fea28 100644 --- a/notifications_utils/logging.py +++ b/notifications_utils/logging.py @@ -1,16 +1,15 @@ -from itertools import product -from pathlib import Path +import logging +import logging.handlers import re import sys - -from flask import request, g -from flask.ctx import has_request_context -from pythonjsonlogger.jsonlogger import JsonFormatter as BaseJSONFormatter +from itertools import product +from pathlib import Path from time import monotonic from typing import Any -import logging -import logging.handlers +from flask import g, request +from flask.ctx import has_request_context +from pythonjsonlogger.jsonlogger import JsonFormatter as BaseJSONFormatter LOG_FORMAT = "%(asctime)s %(app_name)s %(name)s %(levelname)s " '%(request_id)s "%(message)s" [in %(pathname)s:%(lineno)d]' TIME_FORMAT = "%Y-%m-%dT%H:%M:%S" diff --git a/notifications_utils/pdf.py b/notifications_utils/pdf.py index f590e1ac..d9b2be8e 100644 --- a/notifications_utils/pdf.py +++ b/notifications_utils/pdf.py @@ -1,7 +1,8 @@ +import io + import PyPDF2 from PyPDF2 import PdfFileWriter from PyPDF2.utils import PdfReadError -import io def pdf_page_count(src_pdf): diff --git a/notifications_utils/recipients.py b/notifications_utils/recipients.py index b59c6a21..0fc4ff0b 100644 --- a/notifications_utils/recipients.py +++ b/notifications_utils/recipients.py @@ -1,26 +1,28 @@ -import re -import sys import csv -import phonenumbers import os -from io import StringIO +import re +import sys +from collections import OrderedDict, namedtuple from contextlib import suppress from functools import lru_cache, partial +from io import StringIO from itertools import islice -from collections import OrderedDict, namedtuple -from ordered_set import OrderedSet from typing import Callable, Dict, List -from notifications_utils import SMS_CHAR_COUNT_LIMIT + +import phonenumbers from flask import current_app -from notifications_utils.sanitise_text import SanitiseSMS -from . import EMAIL_REGEX_PATTERN, hostname_part, tld_part +from ordered_set import OrderedSet + +from notifications_utils import SMS_CHAR_COUNT_LIMIT +from notifications_utils.columns import Cell, Columns, Row from notifications_utils.formatters import strip_and_remove_obscure_whitespace, strip_whitespace -from notifications_utils.template import SMSMessageTemplate, Template -from notifications_utils.columns import Columns, Row, Cell from notifications_utils.international_billing_rates import ( INTERNATIONAL_BILLING_RATES, ) +from notifications_utils.sanitise_text import SanitiseSMS +from notifications_utils.template import SMSMessageTemplate, Template +from . import EMAIL_REGEX_PATTERN, hostname_part, tld_part country_code = os.getenv("PHONE_COUNTRY_CODE", "1") region_code = os.getenv("PHONE_REGION_CODE", "US") @@ -139,7 +141,8 @@ def placeholders(self, value): Columns.make_key(placeholder) for placeholder in self.recipient_column_headers ] self.recipient_column_headers_lang_check_as_column_keys = [ - Columns.make_key(placeholder) for placeholder in self.recipient_column_headers_lang_check # type: ignore + Columns.make_key(placeholder) + for placeholder in self.recipient_column_headers_lang_check # type: ignore ] @property diff --git a/notifications_utils/request_helper.py b/notifications_utils/request_helper.py index 536a94f4..84bc8293 100644 --- a/notifications_utils/request_helper.py +++ b/notifications_utils/request_helper.py @@ -1,5 +1,5 @@ +from flask import abort, current_app, request from flask.wrappers import Request -from flask import request, current_app, abort class NotifyRequest(Request): diff --git a/notifications_utils/statsd_decorators.py b/notifications_utils/statsd_decorators.py index f1114399..ba7673ab 100644 --- a/notifications_utils/statsd_decorators.py +++ b/notifications_utils/statsd_decorators.py @@ -1,9 +1,9 @@ import functools - -from flask import current_app from time import monotonic from typing import Type +from flask import current_app + def statsd(namespace): def time_function(func): diff --git a/notifications_utils/strftime_codes.py b/notifications_utils/strftime_codes.py index 6b909e6a..410dc79d 100644 --- a/notifications_utils/strftime_codes.py +++ b/notifications_utils/strftime_codes.py @@ -22,7 +22,6 @@ import platform - NO_PAD_POSIX_CHAR = "-" NO_PAD_WINDOWS_CHAR = "#" diff --git a/notifications_utils/system_status.py b/notifications_utils/system_status.py new file mode 100644 index 00000000..b44326d1 --- /dev/null +++ b/notifications_utils/system_status.py @@ -0,0 +1,159 @@ +import logging + +import requests + +TEMPLATES = { + "email": { + "low": "73079cb9-c169-44ea-8cf4-8d397711cc9d", + "medium": "c75c4539-3014-4c4c-96b5-94d326758a74", + "high": "276da251-3103-49f3-9054-cbf6b5d74411", + }, + "sms": { + "low": "ab3a603b-d602-46ea-8c83-e05cb280b950", + "medium": "a48b54ce-40f6-4e4a-abe8-1e2fa389455b", + "high": "4969a9e9-ddfd-476e-8b93-6231e6f1be4a", + }, +} + +THRESHOLDS = { + "email-low": 3 * 60 * 60 * 1000, # 3 hours + "email-medium": 45 * 60 * 1000, # 45 minutes + "email-high": 60 * 1000, # 60 seconds + "sms-low": 3 * 60 * 60 * 1000, # 3 hours + "sms-medium": 45 * 60 * 1000, # 45 minutes + "sms-high": 60 * 1000, # 60 seconds + "api": 400, # 400ms + "admin": 400, # 400ms +} + + +def determine_notification_status(dbresults): # noqa: C901 + # defaults + email_status_low = "down" + email_status_medium = "down" + email_status_high = "down" + sms_status_low = "down" + sms_status_medium = "down" + sms_status_high = "down" + email_low_response_time = -1 + email_medium_response_time = -1 + email_high_response_time = -1 + sms_low_response_time = -1 + sms_medium_response_time = -1 + sms_high_response_time = -1 + + for row in dbresults: + # if there is no data, skip the row and it will default to down + if row[1] is None: + continue + + if str(row[0]) == TEMPLATES["email"]["low"]: + email_low_response_time = row[1] + if email_low_response_time <= THRESHOLDS["email-low"]: + email_status_low = "up" + else: + email_status_low = "degraded" + + elif str(row[0]) == TEMPLATES["email"]["medium"]: + email_medium_response_time = row[1] + if email_medium_response_time <= THRESHOLDS["email-medium"]: + email_status_medium = "up" + else: + email_status_medium = "degraded" + + elif str(row[0]) == TEMPLATES["email"]["high"]: + email_high_response_time = row[1] + if email_high_response_time <= THRESHOLDS["email-high"]: + email_status_high = "up" + else: + email_status_high = "degraded" + + elif str(row[0]) == TEMPLATES["sms"]["low"]: + sms_low_response_time = row[1] + if sms_low_response_time <= THRESHOLDS["sms-low"]: + sms_status_low = "up" + else: + sms_status_low = "degraded" + + elif str(row[0]) == TEMPLATES["sms"]["medium"]: + sms_medium_response_time = row[1] + if sms_medium_response_time <= THRESHOLDS["sms-medium"]: + sms_status_medium = "up" + else: + sms_status_medium = "degraded" + + elif str(row[0]) == TEMPLATES["sms"]["high"]: + sms_high_response_time = row[1] + if sms_high_response_time <= THRESHOLDS["sms-high"]: + sms_status_high = "up" + else: + sms_status_high = "degraded" + + # set overall email_status based on if one of email_status_low, email_status_medium, email_status_high is down, + # then email_status is down, if one is degraded, then email_status is degraded, otherwise email_status is up + if email_status_low == "down" or email_status_medium == "down" or email_status_high == "down": + email_status = "down" + elif email_status_low == "degraded" or email_status_medium == "degraded" or email_status_high == "degraded": + email_status = "degraded" + else: + email_status = "up" + + # set overall sms_status based on if one of sms_status_low, sms_status_medium, sms_status_high is down, + # then sms_status is down, if one is degraded, then sms_status is degraded, otherwise sms_status is up + if sms_status_low == "down" or sms_status_medium == "down" or sms_status_high == "down": + sms_status = "down" + elif sms_status_low == "degraded" or sms_status_medium == "degraded" or sms_status_high == "degraded": + sms_status = "degraded" + else: + sms_status = "up" + + # log all response times when any status is down or degraded + if email_status == "down" or email_status == "degraded" or sms_status == "down" or sms_status == "degraded": + email_logging_info = "high: {}/{}, medium: {}/{}, low: {}/{}".format( + email_status_high, + email_high_response_time, + email_status_medium, + email_medium_response_time, + email_status_low, + email_low_response_time, + ) + sms_logging_info = "high: {}/{}, medium: {}/{}, low: {}/{}".format( + sms_status_high, + sms_high_response_time, + sms_status_medium, + sms_medium_response_time, + sms_status_low, + sms_low_response_time, + ) + + if email_status == "down" or email_status == "degraded": + logging.info("[system_status_email]: email is {}: {}".format(email_status, email_logging_info)) + + if sms_status == "down" or sms_status == "degraded": + logging.info("[system_status_sms]: sms is {}: {}".format(sms_status, sms_logging_info)) + + return (email_status, sms_status) + + +def determine_site_status(url, threshold): + try: + api_response_time = check_response_time(url) + site_status = "up" if api_response_time <= threshold else "degraded" + + if site_status == "degraded": + logging.info("[system_status_site]: site {} is degraded: {}".format(url, api_response_time)) + + except requests.exceptions.ConnectionError as e: + logging.error("[system_status_site]: site {} is down: Error connecting to url: {}".format(url, e)) + site_status = "down" + + except Exception as e: + logging.error("[system_status_site]: site {} is down: unexpected error: {}".format(url, e)) + site_status = "down" + + return site_status + + +def check_response_time(url): + response = requests.get(url) + return response.elapsed.total_seconds() * 1000 diff --git a/notifications_utils/template.py b/notifications_utils/template.py index 0a4c1618..5bccecf4 100644 --- a/notifications_utils/template.py +++ b/notifications_utils/template.py @@ -1,52 +1,51 @@ import math import sys -from os import path from datetime import datetime +from html import unescape +from os import path -from jinja2 import Environment, FileSystemLoader from flask import Markup -from html import unescape +from jinja2 import Environment, FileSystemLoader from notifications_utils import EMAIL_CHAR_COUNT_LIMIT, SMS_CHAR_COUNT_LIMIT, TEMPLATE_NAME_CHAR_COUNT_LIMIT from notifications_utils.columns import Columns from notifications_utils.field import Field from notifications_utils.formatters import ( - unlink_govuk_escaped, - nl2br, - nl2li, add_language_divs, add_prefix, + add_trailing_newline, autolink_sms, + escape_html, + escape_lang_tags, + make_quotes_smart, + nl2br, + nl2li, + normalise_newlines, + normalise_whitespace, notify_email_markdown, notify_email_preheader_markdown, - notify_plain_text_email_markdown, notify_letter_preview_markdown, + notify_plain_text_email_markdown, remove_empty_lines, - sms_encode, - escape_html, - escape_lang_tags, - strip_dvla_markup, - strip_pipes, - remove_whitespace_before_punctuation, remove_language_divs, - make_quotes_smart, + remove_smart_quotes_from_email_addresses, + remove_whitespace_before_punctuation, replace_hyphens_with_en_dashes, replace_hyphens_with_non_breaking_hyphens, - tweak_dvla_list_markup, + sms_encode, + strip_dvla_markup, strip_leading_whitespace, - add_trailing_newline, - normalise_newlines, - normalise_whitespace, - remove_smart_quotes_from_email_addresses, + strip_pipes, strip_unsupported_characters, + tweak_dvla_list_markup, + unlink_govuk_escaped, ) +from notifications_utils.sanitise_text import SanitiseSMS from notifications_utils.strftime_codes import no_pad_day from notifications_utils.take import Take from notifications_utils.template_change import TemplateChange -from notifications_utils.sanitise_text import SanitiseSMS from notifications_utils.validate_html import check_if_string_contains_valid_html - template_env = Environment( loader=FileSystemLoader( path.join( @@ -196,9 +195,7 @@ def content_count(self): return len( ( # we always want to call SMSMessageTemplate.__str__ regardless of subclass, to avoid any html formatting - SMSMessageTemplate.__str__(self) - if self._values - else sms_encode(add_prefix(self.content.strip(), self.prefix)) + SMSMessageTemplate.__str__(self) if self._values else sms_encode(add_prefix(self.content.strip(), self.prefix)) ).encode(self.encoding) ) diff --git a/notifications_utils/timezones.py b/notifications_utils/timezones.py index 2c40cec1..8be6daa3 100644 --- a/notifications_utils/timezones.py +++ b/notifications_utils/timezones.py @@ -1,8 +1,7 @@ import os -from dateutil import parser import pytz - +from dateutil import parser local_timezone = pytz.timezone(os.getenv("TIMEZONE", "America/Toronto")) diff --git a/notifications_utils/url_safe_token.py b/notifications_utils/url_safe_token.py index e8b6373d..44a7e6d6 100644 --- a/notifications_utils/url_safe_token.py +++ b/notifications_utils/url_safe_token.py @@ -1,6 +1,8 @@ +from typing import Any, List + from itsdangerous import URLSafeTimedSerializer + from notifications_utils.formatters import url_encode_full_stops -from typing import Any, List def generate_token(payload: Any, secret: str | List[str]) -> str: diff --git a/poetry.lock b/poetry.lock index 92974fca..034918a1 100644 --- a/poetry.lock +++ b/poetry.lock @@ -51,52 +51,6 @@ charset-normalizer = ["charset-normalizer"] html5lib = ["html5lib"] lxml = ["lxml"] -[[package]] -name = "black" -version = "23.12.1" -description = "The uncompromising code formatter." -optional = false -python-versions = ">=3.8" -files = [ - {file = "black-23.12.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0aaf6041986767a5e0ce663c7a2f0e9eaf21e6ff87a5f95cbf3675bfd4c41d2"}, - {file = "black-23.12.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c88b3711d12905b74206227109272673edce0cb29f27e1385f33b0163c414bba"}, - {file = "black-23.12.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a920b569dc6b3472513ba6ddea21f440d4b4c699494d2e972a1753cdc25df7b0"}, - {file = "black-23.12.1-cp310-cp310-win_amd64.whl", hash = "sha256:3fa4be75ef2a6b96ea8d92b1587dd8cb3a35c7e3d51f0738ced0781c3aa3a5a3"}, - {file = "black-23.12.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8d4df77958a622f9b5a4c96edb4b8c0034f8434032ab11077ec6c56ae9f384ba"}, - {file = "black-23.12.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:602cfb1196dc692424c70b6507593a2b29aac0547c1be9a1d1365f0d964c353b"}, - {file = "black-23.12.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c4352800f14be5b4864016882cdba10755bd50805c95f728011bcb47a4afd59"}, - {file = "black-23.12.1-cp311-cp311-win_amd64.whl", hash = "sha256:0808494f2b2df923ffc5723ed3c7b096bd76341f6213989759287611e9837d50"}, - {file = "black-23.12.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:25e57fd232a6d6ff3f4478a6fd0580838e47c93c83eaf1ccc92d4faf27112c4e"}, - {file = "black-23.12.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2d9e13db441c509a3763a7a3d9a49ccc1b4e974a47be4e08ade2a228876500ec"}, - {file = "black-23.12.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d1bd9c210f8b109b1762ec9fd36592fdd528485aadb3f5849b2740ef17e674e"}, - {file = "black-23.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:ae76c22bde5cbb6bfd211ec343ded2163bba7883c7bc77f6b756a1049436fbb9"}, - {file = "black-23.12.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1fa88a0f74e50e4487477bc0bb900c6781dbddfdfa32691e780bf854c3b4a47f"}, - {file = "black-23.12.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a4d6a9668e45ad99d2f8ec70d5c8c04ef4f32f648ef39048d010b0689832ec6d"}, - {file = "black-23.12.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b18fb2ae6c4bb63eebe5be6bd869ba2f14fd0259bda7d18a46b764d8fb86298a"}, - {file = "black-23.12.1-cp38-cp38-win_amd64.whl", hash = "sha256:c04b6d9d20e9c13f43eee8ea87d44156b8505ca8a3c878773f68b4e4812a421e"}, - {file = "black-23.12.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3e1b38b3135fd4c025c28c55ddfc236b05af657828a8a6abe5deec419a0b7055"}, - {file = "black-23.12.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4f0031eaa7b921db76decd73636ef3a12c942ed367d8c3841a0739412b260a54"}, - {file = "black-23.12.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97e56155c6b737854e60a9ab1c598ff2533d57e7506d97af5481141671abf3ea"}, - {file = "black-23.12.1-cp39-cp39-win_amd64.whl", hash = "sha256:dd15245c8b68fe2b6bd0f32c1556509d11bb33aec9b5d0866dd8e2ed3dba09c2"}, - {file = "black-23.12.1-py3-none-any.whl", hash = "sha256:78baad24af0f033958cad29731e27363183e140962595def56423e626f4bee3e"}, - {file = "black-23.12.1.tar.gz", hash = "sha256:4ce3ef14ebe8d9509188014d96af1c456a910d5b5cbf434a09fef7e024b3d0d5"}, -] - -[package.dependencies] -click = ">=8.0.0" -mypy-extensions = ">=0.4.3" -packaging = ">=22.0" -pathspec = ">=0.9.0" -platformdirs = ">=2" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} - -[package.extras] -colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] -jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] -uvloop = ["uvloop (>=0.15.2)"] - [[package]] name = "bleach" version = "6.1.0" @@ -559,38 +513,6 @@ json = ["jsonpath-ng (>=1.6,<2.0)"] lua = ["lupa (>=2.1,<3.0)"] probabilistic = ["pyprobables (>=0.6,<0.7)"] -[[package]] -name = "flake8" -version = "3.9.2" -description = "the modular source code checker: pep8 pyflakes and co" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" -files = [ - {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"}, - {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"}, -] - -[package.dependencies] -mccabe = ">=0.6.0,<0.7.0" -pycodestyle = ">=2.7.0,<2.8.0" -pyflakes = ">=2.3.0,<2.4.0" - -[[package]] -name = "flake8-print" -version = "4.0.1" -description = "print statement checker plugin for flake8" -optional = false -python-versions = ">=3.6" -files = [ - {file = "flake8-print-4.0.1.tar.gz", hash = "sha256:12b3c3bf65329d8ca9acde949fb3b932ec113e9e5ffa6cb7cd55a7dbcd67dae1"}, - {file = "flake8_print-4.0.1-py3-none-any.whl", hash = "sha256:e246bcd5b07d5259af460b7eff148052c49114640380d7f22340f30920fabf02"}, -] - -[package.dependencies] -flake8 = ">=3.0" -pycodestyle = "*" -six = "*" - [[package]] name = "flask" version = "2.3.3" @@ -776,17 +698,6 @@ files = [ {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, ] -[[package]] -name = "mccabe" -version = "0.6.1" -description = "McCabe checker, plugin for flake8" -optional = false -python-versions = "*" -files = [ - {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, - {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, -] - [[package]] name = "mistune" version = "0.8.4" @@ -881,17 +792,6 @@ files = [ {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"}, ] -[[package]] -name = "pathspec" -version = "0.11.1" -description = "Utility library for gitignore style pattern matching of file paths." -optional = false -python-versions = ">=3.7" -files = [ - {file = "pathspec-0.11.1-py3-none-any.whl", hash = "sha256:d8af70af76652554bd134c22b3e8a1cc46ed7d91edcdd721ef1a0c51a84a5293"}, - {file = "pathspec-0.11.1.tar.gz", hash = "sha256:2798de800fa92780e33acca925945e9a19a133b715067cf165b8866c15a31687"}, -] - [[package]] name = "phonenumbers" version = "8.13.36" @@ -903,21 +803,6 @@ files = [ {file = "phonenumbers-8.13.36.tar.gz", hash = "sha256:b4e2371e35a1172aa2c91c9200b1e48e87b9355eb575768dd38058fc8d72c9ff"}, ] -[[package]] -name = "platformdirs" -version = "3.8.0" -description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -optional = false -python-versions = ">=3.7" -files = [ - {file = "platformdirs-3.8.0-py3-none-any.whl", hash = "sha256:ca9ed98ce73076ba72e092b23d3c93ea6c4e186b3f1c3dad6edd98ff6ffcca2e"}, - {file = "platformdirs-3.8.0.tar.gz", hash = "sha256:b0cabcb11063d21a0b261d557acb0a9d2126350e63b70cdf7db6347baea456dc"}, -] - -[package.extras] -docs = ["furo (>=2023.5.20)", "proselint (>=0.13)", "sphinx (>=7.0.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.3.1)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)"] - [[package]] name = "pluggy" version = "1.2.0" @@ -965,17 +850,6 @@ files = [ {file = "pyasn1-0.5.0.tar.gz", hash = "sha256:97b7290ca68e62a832558ec3976f15cbf911bf5d7c7039d8b861c2a0ece69fde"}, ] -[[package]] -name = "pycodestyle" -version = "2.7.0" -description = "Python style guide checker" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -files = [ - {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"}, - {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"}, -] - [[package]] name = "pycparser" version = "2.21" @@ -987,17 +861,6 @@ files = [ {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, ] -[[package]] -name = "pyflakes" -version = "2.3.1" -description = "passive checker of Python programs" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -files = [ - {file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"}, - {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"}, -] - [[package]] name = "pypdf2" version = "1.28.6" @@ -1269,6 +1132,32 @@ files = [ [package.dependencies] pyasn1 = ">=0.1.3" +[[package]] +name = "ruff" +version = "0.2.2" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +files = [ + {file = "ruff-0.2.2-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:0a9efb032855ffb3c21f6405751d5e147b0c6b631e3ca3f6b20f917572b97eb6"}, + {file = "ruff-0.2.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d450b7fbff85913f866a5384d8912710936e2b96da74541c82c1b458472ddb39"}, + {file = "ruff-0.2.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ecd46e3106850a5c26aee114e562c329f9a1fbe9e4821b008c4404f64ff9ce73"}, + {file = "ruff-0.2.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e22676a5b875bd72acd3d11d5fa9075d3a5f53b877fe7b4793e4673499318ba"}, + {file = "ruff-0.2.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1695700d1e25a99d28f7a1636d85bafcc5030bba9d0578c0781ba1790dbcf51c"}, + {file = "ruff-0.2.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:b0c232af3d0bd8f521806223723456ffebf8e323bd1e4e82b0befb20ba18388e"}, + {file = "ruff-0.2.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f63d96494eeec2fc70d909393bcd76c69f35334cdbd9e20d089fb3f0640216ca"}, + {file = "ruff-0.2.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a61ea0ff048e06de273b2e45bd72629f470f5da8f71daf09fe481278b175001"}, + {file = "ruff-0.2.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e1439c8f407e4f356470e54cdecdca1bd5439a0673792dbe34a2b0a551a2fe3"}, + {file = "ruff-0.2.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:940de32dc8853eba0f67f7198b3e79bc6ba95c2edbfdfac2144c8235114d6726"}, + {file = "ruff-0.2.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0c126da55c38dd917621552ab430213bdb3273bb10ddb67bc4b761989210eb6e"}, + {file = "ruff-0.2.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:3b65494f7e4bed2e74110dac1f0d17dc8e1f42faaa784e7c58a98e335ec83d7e"}, + {file = "ruff-0.2.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1ec49be4fe6ddac0503833f3ed8930528e26d1e60ad35c2446da372d16651ce9"}, + {file = "ruff-0.2.2-py3-none-win32.whl", hash = "sha256:d920499b576f6c68295bc04e7b17b6544d9d05f196bb3aac4358792ef6f34325"}, + {file = "ruff-0.2.2-py3-none-win_amd64.whl", hash = "sha256:cc9a91ae137d687f43a44c900e5d95e9617cb37d4c989e462980ba27039d239d"}, + {file = "ruff-0.2.2-py3-none-win_arm64.whl", hash = "sha256:c9d15fc41e6054bfc7200478720570078f0b41c9ae4f010bcc16bd6f4d1aacdd"}, + {file = "ruff-0.2.2.tar.gz", hash = "sha256:e62ed7f36b3068a30ba39193a14274cd706bc486fad521276458022f7bccb31d"}, +] + [[package]] name = "s3transfer" version = "0.10.0" @@ -1544,4 +1433,4 @@ watchdog = ["watchdog (>=2.3)"] [metadata] lock-version = "2.0" python-versions = "~3.10.9" -content-hash = "e8e34aa760203a7878c2ad133f16c589bd190443794fac3c0b8793379c2bfa14" +content-hash = "717a6083cc81b477fd4d025e7306248f77fbb48593790441af7ec66720bcaafe" diff --git a/pyproject.toml b/pyproject.toml index 266af237..913c49c1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,8 +1,3 @@ -[tool.black] -line-length = 130 -target-version = ['py37', 'py38', 'py39', 'py310'] -include = '(notifications_utils|tests)/.*\.pyi?$' - [tool.poetry] name = "notifications-utils" version = "52.2.7" @@ -18,52 +13,96 @@ build-backend = "poetry.core.masonry.api" [tool.poetry.dependencies] python = "~3.10.9" +# required by both api and admin +awscli = "1.33.5" bleach = "6.1.0" +boto3 = "1.34.100" cachetools = "4.2.4" -mistune = "0.8.4" -requests = "2.31.0" -python-json-logger = "2.0.7" + +# Pinned dependencies +certifi = "^2023.7.22" # pinned for security reasons: https://github.com/cds-snc/notification-utils/security/dependabot/22 +cryptography = "^42.0.3" # https://github.com/cds-snc/notification-utils/security/dependabot/31 Flask = "2.3.3" -ordered-set = "4.1.0" -markupsafe = "2.1.5" -Jinja2 = "^3.0.0" -statsd = "3.3.0" Flask-Redis = "0.4.0" -PyYAML = "6.0.1" +itsdangerous = "2.2.0" +Jinja2 = "^3.0.0" +markupsafe = "2.1.5" +mistune = "0.8.4" +ordered-set = "4.1.0" phonenumbers = "8.13.36" +py_w3c = "0.3.1" +pypdf2 = "1.28.6" +python-json-logger = "2.0.7" pytz = "2021.3" +PyYAML = "6.0.1" +requests = "2.31.0" smartypants = "2.0.1" -pypdf2 = "1.28.6" -py_w3c = "0.3.1" -# required by both api and admin -awscli = "1.33.5" -boto3 = "1.34.100" +statsd = "3.3.0" werkzeug = "3.0.3" -itsdangerous = "2.2.0" - -# Pinned dependencies -certifi = "^2023.7.22" # pinned for security reasons: https://github.com/cds-snc/notification-utils/security/dependabot/22 -cryptography = "^42.0.3" # https://github.com/cds-snc/notification-utils/security/dependabot/31 [tool.poetry.group.test.dependencies] +beautifulsoup4 = "^4.12.3" +click = "8.1.7" +fakeredis = "^2.10.3" +freezegun = "1.5.1" +mypy = "1.10.0" pytest = "7.4.4" -pytest-mock = "3.14.0" pytest-cov = "2.12.1" +pytest-mock = "3.14.0" pytest-xdist = "2.5.0" -freezegun = "1.5.1" requests-mock = "1.12.1" -fakeredis = "^2.10.3" -flake8 = "3.9.2" -flake8-print = "4.0.1" -mypy = "1.10.0" -black = "23.12.1" -click = "8.1.7" -types-python-dateutil = "2.9.0.20240316" -types-PyYAML = "6.0.12.20240311" -types-pytz = "2022.7.1.2" +ruff = "^0.2.1" +types-beautifulsoup4 = "^4.12.0.20240229" types-bleach = "5.0.3.1" types-cachetools = "5.3.0.7" +types-python-dateutil = "2.9.0.20240316" +types-pytz = "2022.7.1.2" +types-PyYAML = "6.0.12.20240311" types-redis = "4.6.0.20240425" types-requests = "2.32.0.20240602" -beautifulsoup4 = "^4.12.3" -types-beautifulsoup4 = "^4.12.0.20240229" + +[tool.ruff] +target-version = "py310" +exclude = [ + "venv*", + "__pycache__", + "node_modules", + "cache", + "migrations", + "build", +] +extend-include = ['(app|migrations|tests)/.*\.pyi?$'] +src = ["app", "migrations", "tests"] +# Ruff formatter will wrap lines at a length of 130 characters. +line-length = 130 +indent-width = 4 + +[tool.ruff.lint] +select = [ + # PyFlakes + "F", + # Pycodestyle + "E", + "W", + # isort + "I001", + "I002", +] +ignore = ["E203", "E501", "E402"] + +# Provide line length leeway for docstrings +[tool.ruff.lint.pycodestyle] +max-doc-length = 170 +# Enforce doc string format? (google, numpy or pep257) +# convention = "google" + +[tool.ruff.format] +# Match black formatting +# Double quotes for strings. +quote-style = "double" +# Indent with spaces, rather than tabs. +indent-style = "space" +# Respect magic trailing commas. +skip-magic-trailing-comma = false +# Automatically detect the appropriate line ending. +line-ending = "auto" diff --git a/scripts/run_tests.sh b/scripts/run_tests.sh index e80c7e0d..cf3e76d6 100755 --- a/scripts/run_tests.sh +++ b/scripts/run_tests.sh @@ -22,12 +22,12 @@ function display_result { fi } -black --check --config pyproject.toml . -display_result $? 1 "Code style check (Black)" - -flake8 . +ruff check . display_result $? 1 "Code style check" +ruff check --select I . +display_result $? 1 "Import order check" + mypy . display_result $? 1 "Static type check" diff --git a/setup.cfg b/setup.cfg index 57186c3d..c8ecee25 100644 --- a/setup.cfg +++ b/setup.cfg @@ -2,7 +2,3 @@ xfail_strict=true testpaths = tests addopts = -p no:warnings -n4 - -[flake8] -# W504 line break after binary operator -extend_ignore=W504 diff --git a/tests/conftest.py b/tests/conftest.py index ef0e0745..8963c50a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,8 +1,8 @@ -import pytest -from flask import Flask +from unittest.mock import Mock +import pytest import requests_mock -from unittest.mock import Mock +from flask import Flask class FakeService: diff --git a/tests/test_antivirus_client.py b/tests/test_antivirus_client.py index 9c0c5e65..b5c508ec 100644 --- a/tests/test_antivirus_client.py +++ b/tests/test_antivirus_client.py @@ -1,7 +1,7 @@ import io -import requests -import pytest +import pytest +import requests from notifications_utils.clients.antivirus.antivirus_client import AntivirusClient, AntivirusError diff --git a/tests/test_base64_uuid.py b/tests/test_base64_uuid.py index 297cf3d7..5fe45c68 100644 --- a/tests/test_base64_uuid.py +++ b/tests/test_base64_uuid.py @@ -1,8 +1,8 @@ -from uuid import UUID import os +from uuid import UUID import pytest -from notifications_utils.base64_uuid import base64_to_uuid, uuid_to_base64, base64_to_bytes, bytes_to_base64 +from notifications_utils.base64_uuid import base64_to_bytes, base64_to_uuid, bytes_to_base64, uuid_to_base64 def test_bytes_to_base64_to_bytes(): diff --git a/tests/test_base_template.py b/tests/test_base_template.py index b2101c36..6b0789d1 100644 --- a/tests/test_base_template.py +++ b/tests/test_base_template.py @@ -1,7 +1,7 @@ +from unittest.mock import PropertyMock, patch + import pytest -from unittest.mock import PropertyMock -from unittest.mock import patch -from notifications_utils.template import Template, SMSMessageTemplate, SMSPreviewTemplate, WithSubjectTemplate +from notifications_utils.template import SMSMessageTemplate, SMSPreviewTemplate, Template, WithSubjectTemplate def test_class(): diff --git a/tests/test_bounce_rate.py b/tests/test_bounce_rate.py index 4371022a..959c78e8 100644 --- a/tests/test_bounce_rate.py +++ b/tests/test_bounce_rate.py @@ -1,13 +1,13 @@ import datetime import uuid -import pytest from unittest.mock import Mock + import fakeredis +import pytest from freezegun import freeze_time - from notifications_utils.clients.redis.bounce_rate import ( - _current_timestamp_s, RedisBounceRate, + _current_timestamp_s, hard_bounce_key, total_notifications_key, ) diff --git a/tests/test_columns.py b/tests/test_columns.py index b099cf83..9748ad9f 100644 --- a/tests/test_columns.py +++ b/tests/test_columns.py @@ -1,7 +1,7 @@ -import pytest - from functools import partial -from notifications_utils.columns import Columns, Row, Cell + +import pytest +from notifications_utils.columns import Cell, Columns, Row def test_columns_as_dict_with_keys(): diff --git a/tests/test_field.py b/tests/test_field.py index 4d0c3edf..250f6675 100644 --- a/tests/test_field.py +++ b/tests/test_field.py @@ -1,5 +1,6 @@ -import pytest from typing import Any, Dict + +import pytest from notifications_utils.field import Field, str2bool diff --git a/tests/test_formatters.py b/tests/test_formatters.py index f0f89044..c8007207 100644 --- a/tests/test_formatters.py +++ b/tests/test_formatters.py @@ -1,32 +1,31 @@ import pytest from flask import Markup - from notifications_utils.formatters import ( EMAIL_P_CLOSE_TAG, EMAIL_P_OPEN_TAG, add_language_divs, add_trailing_newline, - remove_language_divs, - unlink_govuk_escaped, + escape_html, + escape_lang_tags, + formatted_list, + make_quotes_smart, + nl2li, + normalise_whitespace, notify_email_markdown, notify_letter_preview_markdown, notify_plain_text_email_markdown, - sms_encode, - formatted_list, - strip_dvla_markup, - strip_pipes, - escape_html, - escape_lang_tags, + remove_language_divs, + remove_smart_quotes_from_email_addresses, remove_whitespace_before_punctuation, - make_quotes_smart, replace_hyphens_with_en_dashes, - tweak_dvla_list_markup, - nl2li, - strip_whitespace, + sms_encode, strip_and_remove_obscure_whitespace, - remove_smart_quotes_from_email_addresses, + strip_dvla_markup, + strip_pipes, strip_unsupported_characters, - normalise_whitespace, + strip_whitespace, + tweak_dvla_list_markup, + unlink_govuk_escaped, ) from notifications_utils.take import Take from notifications_utils.template import HTMLEmailTemplate, PlainTextEmailTemplate, SMSMessageTemplate, SMSPreviewTemplate @@ -977,21 +976,18 @@ def test_strip_and_remove_obscure_whitespace_only_removes_normal_whitespace_from def test_remove_smart_quotes_from_email_addresses(): - assert ( - remove_smart_quotes_from_email_addresses( - """ + assert remove_smart_quotes_from_email_addresses( + """ line one’s quote first.o’last@example.com is someone’s email address line ‘three’ """ - ) - == ( - """ + ) == ( + """ line one’s quote first.o'last@example.com is someone’s email address line ‘three’ """ - ) ) diff --git a/tests/test_international_billing_rates.py b/tests/test_international_billing_rates.py index 09bb4802..4f6ec735 100644 --- a/tests/test_international_billing_rates.py +++ b/tests/test_international_billing_rates.py @@ -1,8 +1,7 @@ import pytest - from notifications_utils.international_billing_rates import ( - INTERNATIONAL_BILLING_RATES, COUNTRY_PREFIXES, + INTERNATIONAL_BILLING_RATES, ) diff --git a/tests/test_letter_timings.py b/tests/test_letter_timings.py index 82492367..1ca07789 100644 --- a/tests/test_letter_timings.py +++ b/tests/test_letter_timings.py @@ -1,8 +1,7 @@ -import pytest -import pytz - from datetime import datetime +import pytest +import pytz from freezegun import freeze_time from notifications_utils.letter_timings import get_letter_timings, letter_can_be_cancelled diff --git a/tests/test_logging.py b/tests/test_logging.py index 1944092b..72c1060c 100644 --- a/tests/test_logging.py +++ b/tests/test_logging.py @@ -5,8 +5,7 @@ from unittest.mock import call import pytest -from flask import request, g - +from flask import g, request from notifications_utils import logging diff --git a/tests/test_pdf.py b/tests/test_pdf.py index c603c19e..523980b9 100644 --- a/tests/test_pdf.py +++ b/tests/test_pdf.py @@ -3,10 +3,10 @@ import PyPDF2 import pytest +from notifications_utils.pdf import extract_page_from_pdf, pdf_page_count from PyPDF2.utils import PdfReadError -from notifications_utils.pdf import pdf_page_count, extract_page_from_pdf -from tests.pdf_consts import one_page_pdf, multi_page_pdf, not_pdf +from tests.pdf_consts import multi_page_pdf, not_pdf, one_page_pdf def test_pdf_page_count_src_pdf_is_null(): diff --git a/tests/test_placeholders.py b/tests/test_placeholders.py index 7ac11c3c..5ea01de4 100644 --- a/tests/test_placeholders.py +++ b/tests/test_placeholders.py @@ -1,7 +1,6 @@ import re import pytest - from notifications_utils.field import Placeholder diff --git a/tests/test_recipient_csv.py b/tests/test_recipient_csv.py index bdc0a5fb..033c951a 100644 --- a/tests/test_recipient_csv.py +++ b/tests/test_recipient_csv.py @@ -1,13 +1,13 @@ -from math import floor -import pytest import itertools import unicodedata from functools import partial -from ordered_set import OrderedSet +from math import floor +import pytest from notifications_utils import SMS_CHAR_COUNT_LIMIT from notifications_utils.recipients import Cell, RecipientCSV, Row from notifications_utils.template import SMSMessageTemplate +from ordered_set import OrderedSet def _index_rows(rows): @@ -979,9 +979,7 @@ def test_multiple_sms_recipient_columns_with_missing_data(column_name): """ names, phone number, {} "Joanna and Steve", 07900 900111 - """.format( - column_name - ), + """.format(column_name), template_type="sms", international_sms=True, ) diff --git a/tests/test_recipient_validation.py b/tests/test_recipient_validation.py index 6e94b20c..f3d74cf2 100644 --- a/tests/test_recipient_validation.py +++ b/tests/test_recipient_validation.py @@ -1,26 +1,24 @@ -import pytest - from functools import partial +import pytest from notifications_utils.recipients import ( - validate_phone_number, - validate_and_format_phone_number, - InvalidPhoneError, - validate_email_address, + InvalidAddressError, InvalidEmailError, + InvalidPhoneError, allowed_to_send_to, - InvalidAddressError, - validate_recipient, - is_local_phone_number, - normalise_phone_number, - international_phone_info, - get_international_phone_info, format_phone_number_human_readable, format_recipient, + get_international_phone_info, + international_phone_info, + is_local_phone_number, + normalise_phone_number, try_validate_and_format_phone_number, + validate_and_format_phone_number, + validate_email_address, + validate_phone_number, + validate_recipient, ) - valid_local_phone_numbers = [ "6502532222", "+16502532222", diff --git a/tests/test_redis_client.py b/tests/test_redis_client.py index 82057730..e54457b9 100644 --- a/tests/test_redis_client.py +++ b/tests/test_redis_client.py @@ -1,14 +1,14 @@ import uuid -import pytest from datetime import datetime from unittest.mock import Mock, call -from freezegun import freeze_time -import fakeredis +import fakeredis +import pytest +from freezegun import freeze_time from notifications_utils.clients.redis import ( daily_limit_cache_key, - sms_daily_count_cache_key, rate_limit_cache_key, + sms_daily_count_cache_key, ) from notifications_utils.clients.redis.redis_client import RedisClient, prepare_value diff --git a/tests/test_s3.py b/tests/test_s3.py index 5c59b548..5952482f 100644 --- a/tests/test_s3.py +++ b/tests/test_s3.py @@ -2,8 +2,7 @@ import botocore import pytest - -from notifications_utils.s3 import s3download, s3upload, S3ObjectNotFound +from notifications_utils.s3 import S3ObjectNotFound, s3download, s3upload contents = "some file data" region = "eu-west-1" diff --git a/tests/test_sanitise_text.py b/tests/test_sanitise_text.py index 6cd4b248..a8d458bc 100644 --- a/tests/test_sanitise_text.py +++ b/tests/test_sanitise_text.py @@ -1,6 +1,5 @@ import pytest - -from notifications_utils.sanitise_text import SanitiseText, SanitiseSMS, SanitiseASCII +from notifications_utils.sanitise_text import SanitiseASCII, SanitiseSMS, SanitiseText @pytest.mark.parametrize("chars, cls", [("ÀÂËÎÏÔŒÙÛâçêëîïôœû", SanitiseSMS)]) diff --git a/tests/test_statsd_client.py b/tests/test_statsd_client.py index 57785ebc..4662e5d5 100644 --- a/tests/test_statsd_client.py +++ b/tests/test_statsd_client.py @@ -1,8 +1,8 @@ +from datetime import datetime, timedelta from unittest.mock import Mock, patch import pytest -from datetime import datetime, timedelta -from notifications_utils.clients.statsd.statsd_client import StatsdClient, NotifyStatsClient +from notifications_utils.clients.statsd.statsd_client import NotifyStatsClient, StatsdClient @pytest.fixture(scope="function") diff --git a/tests/test_statsd_decorators.py b/tests/test_statsd_decorators.py index 75d009b7..30a98b0a 100644 --- a/tests/test_statsd_decorators.py +++ b/tests/test_statsd_decorators.py @@ -1,5 +1,6 @@ -import pytest from unittest.mock import ANY + +import pytest from notifications_utils.statsd_decorators import statsd, statsd_catch diff --git a/tests/test_strftime_codes.py b/tests/test_strftime_codes.py index 5eedba9f..eff6d67b 100644 --- a/tests/test_strftime_codes.py +++ b/tests/test_strftime_codes.py @@ -1,6 +1,6 @@ from unittest.mock import MagicMock, patch -from notifications_utils.strftime_codes import no_pad_code, no_pad_day, no_pad_hour12, no_pad_hour24, no_pad_month +from notifications_utils.strftime_codes import no_pad_code, no_pad_day, no_pad_hour12, no_pad_hour24, no_pad_month LINUX = "linux" WINDOWS = "windows" diff --git a/tests/test_system_status.py b/tests/test_system_status.py new file mode 100644 index 00000000..4f27a0bd --- /dev/null +++ b/tests/test_system_status.py @@ -0,0 +1,290 @@ +import pytest +import requests +from notifications_utils.system_status import THRESHOLDS, determine_notification_status, determine_site_status + + +@pytest.mark.parametrize("site, threshold", [("http://site-1.example.com", 400)]) +@pytest.mark.parametrize("response_time, expected_status", [(100, "up"), (500, "degraded")]) +def test_determine_site_status(mocker, site, threshold, response_time, expected_status): + # mock the call to check_response_time() + mocker.patch("notifications_utils.system_status.check_response_time", return_value=response_time) + + result = determine_site_status(site, threshold) + + assert result == expected_status + + +def test_determine_site_status_down(mocker): + mocker.patch("notifications_utils.system_status.check_response_time", side_effect=requests.exceptions.ConnectionError) + result = determine_site_status("http://site-1.example.com", 400) + + assert result == "down" + + +@pytest.mark.parametrize( + "email_low, email_medium, email_high, expected_result", + [ + (THRESHOLDS["email-low"], THRESHOLDS["email-medium"], THRESHOLDS["email-high"], "up"), + (THRESHOLDS["email-low"], THRESHOLDS["email-medium"], THRESHOLDS["email-high"] + 1, "degraded"), + (THRESHOLDS["email-low"], THRESHOLDS["email-medium"], -1, "down"), + (THRESHOLDS["email-low"], THRESHOLDS["email-medium"] + 1, THRESHOLDS["email-high"], "degraded"), + (THRESHOLDS["email-low"], THRESHOLDS["email-medium"] + 1, THRESHOLDS["email-high"] + 1, "degraded"), + (THRESHOLDS["email-low"], THRESHOLDS["email-medium"] + 1, -1, "down"), + (THRESHOLDS["email-low"], -1, THRESHOLDS["email-high"], "down"), + (THRESHOLDS["email-low"], -1, THRESHOLDS["email-high"] + 1, "down"), + (THRESHOLDS["email-low"], -1, -1, "down"), + (THRESHOLDS["email-low"] + 1, THRESHOLDS["email-medium"], THRESHOLDS["email-high"], "degraded"), + (THRESHOLDS["email-low"] + 1, THRESHOLDS["email-medium"], THRESHOLDS["email-high"] + 1, "degraded"), + (THRESHOLDS["email-low"] + 1, THRESHOLDS["email-medium"], -1, "down"), + (THRESHOLDS["email-low"] + 1, THRESHOLDS["email-medium"] + 1, THRESHOLDS["email-high"], "degraded"), + (THRESHOLDS["email-low"] + 1, THRESHOLDS["email-medium"] + 1, THRESHOLDS["email-high"] + 1, "degraded"), + (THRESHOLDS["email-low"] + 1, THRESHOLDS["email-medium"] + 1, -1, "down"), + (THRESHOLDS["email-low"] + 1, -1, THRESHOLDS["email-high"], "down"), + (THRESHOLDS["email-low"] + 1, -1, THRESHOLDS["email-high"] + 1, "down"), + (THRESHOLDS["email-low"] + 1, -1, -1, "down"), + (-1, THRESHOLDS["email-medium"], THRESHOLDS["email-high"], "down"), + (-1, THRESHOLDS["email-medium"], THRESHOLDS["email-high"] + 1, "down"), + (-1, THRESHOLDS["email-medium"], -1, "down"), + (-1, THRESHOLDS["email-medium"] + 1, THRESHOLDS["email-high"], "down"), + (-1, THRESHOLDS["email-medium"] + 1, THRESHOLDS["email-high"] + 1, "down"), + (-1, THRESHOLDS["email-medium"] + 1, -1, "down"), + (-1, -1, THRESHOLDS["email-high"], "down"), + (-1, -1, THRESHOLDS["email-high"] + 1, "down"), + (-1, -1, -1, "down"), + ], +) +def test_determine_notification_status_for_email(mocker, email_low, email_medium, email_high, expected_result): + notifications_data = [ + ["73079cb9-c169-44ea-8cf4-8d397711cc9d", email_low] + if email_low != -1 + else [ + "dummy-data", + 0, + ], # this just omits a row with the particular id in the case of -1, since there would be no db records + ["c75c4539-3014-4c4c-96b5-94d326758a74", email_medium] if email_medium != -1 else ["dummy-data", 0], + ["276da251-3103-49f3-9054-cbf6b5d74411", email_high] if email_high != -1 else ["dummy-data", 0], + ["ab3a603b-d602-46ea-8c83-e05cb280b950", 1], # SMS + ["a48b54ce-40f6-4e4a-abe8-1e2fa389455b", 1], # SMS # med sms + ["4969a9e9-ddfd-476e-8b93-6231e6f1be4a", 1], + ] + + assert determine_notification_status(notifications_data) == (expected_result, "up") + + +@pytest.mark.parametrize( + "sms_low, sms_medium, sms_high, expected_result", + [ + (THRESHOLDS["sms-low"], THRESHOLDS["sms-medium"], THRESHOLDS["sms-high"], "up"), + (THRESHOLDS["sms-low"], THRESHOLDS["sms-medium"], THRESHOLDS["sms-high"] + 1, "degraded"), + (THRESHOLDS["sms-low"], THRESHOLDS["sms-medium"], -1, "down"), + (THRESHOLDS["sms-low"], THRESHOLDS["sms-medium"] + 1, THRESHOLDS["sms-high"], "degraded"), + (THRESHOLDS["sms-low"], THRESHOLDS["sms-medium"] + 1, THRESHOLDS["sms-high"] + 1, "degraded"), + (THRESHOLDS["sms-low"], THRESHOLDS["sms-medium"] + 1, -1, "down"), + (THRESHOLDS["sms-low"], -1, THRESHOLDS["sms-high"], "down"), + (THRESHOLDS["sms-low"], -1, THRESHOLDS["sms-high"] + 1, "down"), + (THRESHOLDS["sms-low"], -1, -1, "down"), + (THRESHOLDS["sms-low"] + 1, THRESHOLDS["sms-medium"], THRESHOLDS["sms-high"], "degraded"), + (THRESHOLDS["sms-low"] + 1, THRESHOLDS["sms-medium"], THRESHOLDS["sms-high"] + 1, "degraded"), + (THRESHOLDS["sms-low"] + 1, THRESHOLDS["sms-medium"], -1, "down"), + (THRESHOLDS["sms-low"] + 1, THRESHOLDS["sms-medium"] + 1, THRESHOLDS["sms-high"], "degraded"), + (THRESHOLDS["sms-low"] + 1, THRESHOLDS["sms-medium"] + 1, THRESHOLDS["sms-high"] + 1, "degraded"), + (THRESHOLDS["sms-low"] + 1, THRESHOLDS["sms-medium"] + 1, -1, "down"), + (THRESHOLDS["sms-low"] + 1, -1, THRESHOLDS["sms-high"], "down"), + (THRESHOLDS["sms-low"] + 1, -1, THRESHOLDS["sms-high"] + 1, "down"), + (THRESHOLDS["sms-low"] + 1, -1, -1, "down"), + (-1, THRESHOLDS["sms-medium"], THRESHOLDS["sms-high"], "down"), + (-1, THRESHOLDS["sms-medium"], THRESHOLDS["sms-high"] + 1, "down"), + (-1, THRESHOLDS["sms-medium"], -1, "down"), + (-1, THRESHOLDS["sms-medium"] + 1, THRESHOLDS["sms-high"], "down"), + (-1, THRESHOLDS["sms-medium"] + 1, THRESHOLDS["sms-high"] + 1, "down"), + (-1, THRESHOLDS["sms-medium"] + 1, -1, "down"), + (-1, -1, THRESHOLDS["sms-high"], "down"), + (-1, -1, THRESHOLDS["sms-high"] + 1, "down"), + (-1, -1, -1, "down"), + ], +) +def test_determine_notification_status_for_sms(mocker, sms_low, sms_medium, sms_high, expected_result): + notifications_data = [ + ["73079cb9-c169-44ea-8cf4-8d397711cc9d", 1], + ["c75c4539-3014-4c4c-96b5-94d326758a74", 1], + ["276da251-3103-49f3-9054-cbf6b5d74411", 1], + ["ab3a603b-d602-46ea-8c83-e05cb280b950", sms_low] if sms_low != -1 else ["dummy-data", 0], + ["a48b54ce-40f6-4e4a-abe8-1e2fa389455b", sms_medium] if sms_medium != -1 else ["dummy-data", 0], + ["4969a9e9-ddfd-476e-8b93-6231e6f1be4a", sms_high] if sms_high != -1 else ["dummy-data", 0], + ] + + assert determine_notification_status(notifications_data) == ("up", expected_result) + + +def test_determine_notification_status_for_email_down_when_no_rows(): + notifications_data = [ + ["73079cb9-c169-44ea-8cf4-8d397711cc9d", THRESHOLDS["email-low"]], + ["c75c4539-3014-4c4c-96b5-94d326758a74", None], # no results for email medium + ["276da251-3103-49f3-9054-cbf6b5d74411", THRESHOLDS["email-high"]], + ["ab3a603b-d602-46ea-8c83-e05cb280b950", 1], # SMS + ["a48b54ce-40f6-4e4a-abe8-1e2fa389455b", 1], # SMS # med sms + ["4969a9e9-ddfd-476e-8b93-6231e6f1be4a", 1], + ] + + assert determine_notification_status(notifications_data) == ("down", "up") + + notifications_data = [ + ["73079cb9-c169-44ea-8cf4-8d397711cc9d", None], # no results for email low + ["c75c4539-3014-4c4c-96b5-94d326758a74", THRESHOLDS["email-medium"]], + ["276da251-3103-49f3-9054-cbf6b5d74411", THRESHOLDS["email-high"]], + ["ab3a603b-d602-46ea-8c83-e05cb280b950", 1], # SMS + ["a48b54ce-40f6-4e4a-abe8-1e2fa389455b", 1], # SMS # med sms + ["4969a9e9-ddfd-476e-8b93-6231e6f1be4a", 1], + ] + + assert determine_notification_status(notifications_data) == ("down", "up") + + notifications_data = [ + ["73079cb9-c169-44ea-8cf4-8d397711cc9d", THRESHOLDS["email-low"]], + ["c75c4539-3014-4c4c-96b5-94d326758a74", THRESHOLDS["email-medium"]], + ["276da251-3103-49f3-9054-cbf6b5d74411", None], # no results for email high + ["ab3a603b-d602-46ea-8c83-e05cb280b950", 1], # SMS + ["a48b54ce-40f6-4e4a-abe8-1e2fa389455b", 1], # SMS # med sms + ["4969a9e9-ddfd-476e-8b93-6231e6f1be4a", 1], + ] + + assert determine_notification_status(notifications_data) == ("down", "up") + + +@pytest.mark.parametrize( + "email_low, email_medium, email_high, status, log_expected", + [ + (THRESHOLDS["email-low"], THRESHOLDS["email-medium"], THRESHOLDS["email-high"], "up", False), + (THRESHOLDS["email-low"], THRESHOLDS["email-medium"], THRESHOLDS["email-high"] + 1, "degraded", True), + (THRESHOLDS["email-low"], THRESHOLDS["email-medium"], -1, "down", True), + (THRESHOLDS["email-low"], THRESHOLDS["email-medium"] + 1, THRESHOLDS["email-high"], "degraded", True), + (THRESHOLDS["email-low"], THRESHOLDS["email-medium"] + 1, THRESHOLDS["email-high"] + 1, "degraded", True), + (THRESHOLDS["email-low"], THRESHOLDS["email-medium"] + 1, -1, "down", True), + (THRESHOLDS["email-low"], -1, THRESHOLDS["email-high"], "down", True), + (THRESHOLDS["email-low"], -1, THRESHOLDS["email-high"] + 1, "down", True), + (THRESHOLDS["email-low"], -1, -1, "down", True), + (THRESHOLDS["email-low"] + 1, THRESHOLDS["email-medium"], THRESHOLDS["email-high"], "degraded", True), + (THRESHOLDS["email-low"] + 1, THRESHOLDS["email-medium"], THRESHOLDS["email-high"] + 1, "degraded", True), + (THRESHOLDS["email-low"] + 1, THRESHOLDS["email-medium"], -1, "down", True), + (THRESHOLDS["email-low"] + 1, THRESHOLDS["email-medium"] + 1, THRESHOLDS["email-high"], "degraded", True), + (THRESHOLDS["email-low"] + 1, THRESHOLDS["email-medium"] + 1, THRESHOLDS["email-high"] + 1, "degraded", True), + (THRESHOLDS["email-low"] + 1, THRESHOLDS["email-medium"] + 1, -1, "down", True), + (THRESHOLDS["email-low"] + 1, -1, THRESHOLDS["email-high"], "down", True), + (THRESHOLDS["email-low"] + 1, -1, THRESHOLDS["email-high"] + 1, "down", True), + (THRESHOLDS["email-low"] + 1, -1, -1, "down", True), + (-1, THRESHOLDS["email-medium"], THRESHOLDS["email-high"], "down", True), + (-1, THRESHOLDS["email-medium"], THRESHOLDS["email-high"] + 1, "down", True), + (-1, THRESHOLDS["email-medium"], -1, "down", True), + (-1, THRESHOLDS["email-medium"] + 1, THRESHOLDS["email-high"], "down", True), + (-1, THRESHOLDS["email-medium"] + 1, THRESHOLDS["email-high"] + 1, "down", True), + (-1, THRESHOLDS["email-medium"] + 1, -1, "down", True), + (-1, -1, THRESHOLDS["email-high"], "down", True), + (-1, -1, THRESHOLDS["email-high"] + 1, "down", True), + (-1, -1, -1, "down", True), + ], +) +def test_logging_determine_notification_status_logs_on_email_degraded_or_down( + mocker, email_low, email_medium, email_high, status, log_expected, caplog +): + notifications_data = [ + ["73079cb9-c169-44ea-8cf4-8d397711cc9d", email_low] + if email_low != -1 + else [ + "dummy-data", + 0, + ], # this just omits a row with the particular id in the case of -1, since there would be no db records + ["c75c4539-3014-4c4c-96b5-94d326758a74", email_medium] if email_medium != -1 else ["dummy-data", 0], + ["276da251-3103-49f3-9054-cbf6b5d74411", email_high] if email_high != -1 else ["dummy-data", 0], + ["ab3a603b-d602-46ea-8c83-e05cb280b950", 1], # SMS + ["a48b54ce-40f6-4e4a-abe8-1e2fa389455b", 1], # SMS # med sms + ["4969a9e9-ddfd-476e-8b93-6231e6f1be4a", 1], + ] + + caplog.set_level("INFO") + determine_notification_status(notifications_data) + + if log_expected: + assert "[system_status_email]: email is {}".format(status) in caplog.text + + +@pytest.mark.parametrize( + "sms_low, sms_medium, sms_high, status, log_expected", + [ + (THRESHOLDS["sms-low"], THRESHOLDS["sms-medium"], THRESHOLDS["sms-high"], "up", False), + (THRESHOLDS["sms-low"], THRESHOLDS["sms-medium"], THRESHOLDS["sms-high"] + 1, "degraded", True), + (THRESHOLDS["sms-low"], THRESHOLDS["sms-medium"], -1, "down", True), + (THRESHOLDS["sms-low"], THRESHOLDS["sms-medium"] + 1, THRESHOLDS["sms-high"], "degraded", True), + (THRESHOLDS["sms-low"], THRESHOLDS["sms-medium"] + 1, THRESHOLDS["sms-high"] + 1, "degraded", True), + (THRESHOLDS["sms-low"], THRESHOLDS["sms-medium"] + 1, -1, "down", True), + (THRESHOLDS["sms-low"], -1, THRESHOLDS["sms-high"], "down", True), + (THRESHOLDS["sms-low"], -1, THRESHOLDS["sms-high"] + 1, "down", True), + (THRESHOLDS["sms-low"], -1, -1, "down", True), + (THRESHOLDS["sms-low"] + 1, THRESHOLDS["sms-medium"], THRESHOLDS["sms-high"], "degraded", True), + (THRESHOLDS["sms-low"] + 1, THRESHOLDS["sms-medium"], THRESHOLDS["sms-high"] + 1, "degraded", True), + (THRESHOLDS["sms-low"] + 1, THRESHOLDS["sms-medium"], -1, "down", True), + (THRESHOLDS["sms-low"] + 1, THRESHOLDS["sms-medium"] + 1, THRESHOLDS["sms-high"], "degraded", True), + (THRESHOLDS["sms-low"] + 1, THRESHOLDS["sms-medium"] + 1, THRESHOLDS["sms-high"] + 1, "degraded", True), + (THRESHOLDS["sms-low"] + 1, THRESHOLDS["sms-medium"] + 1, -1, "down", True), + (THRESHOLDS["sms-low"] + 1, -1, THRESHOLDS["sms-high"], "down", True), + (THRESHOLDS["sms-low"] + 1, -1, THRESHOLDS["sms-high"] + 1, "down", True), + (THRESHOLDS["sms-low"] + 1, -1, -1, "down", True), + (-1, THRESHOLDS["sms-medium"], THRESHOLDS["sms-high"], "down", True), + (-1, THRESHOLDS["sms-medium"], THRESHOLDS["sms-high"] + 1, "down", True), + (-1, THRESHOLDS["sms-medium"], -1, "down", True), + (-1, THRESHOLDS["sms-medium"] + 1, THRESHOLDS["sms-high"], "down", True), + (-1, THRESHOLDS["sms-medium"] + 1, THRESHOLDS["sms-high"] + 1, "down", True), + (-1, THRESHOLDS["sms-medium"] + 1, -1, "down", True), + (-1, -1, THRESHOLDS["sms-high"], "down", True), + (-1, -1, THRESHOLDS["sms-high"] + 1, "down", True), + (-1, -1, -1, "down", True), + ], +) +def test_logging_determine_notification_status_logs_on_sms_degraded_or_down( + mocker, sms_low, sms_medium, sms_high, status, log_expected, caplog +): + notifications_data = [ + ["73079cb9-c169-44ea-8cf4-8d397711cc9d", 1], + ["c75c4539-3014-4c4c-96b5-94d326758a74", 1], + ["276da251-3103-49f3-9054-cbf6b5d74411", 1], + ["ab3a603b-d602-46ea-8c83-e05cb280b950", sms_low] if sms_low != -1 else ["dummy-data", 0], + ["a48b54ce-40f6-4e4a-abe8-1e2fa389455b", sms_medium] if sms_medium != -1 else ["dummy-data", 0], + ["4969a9e9-ddfd-476e-8b93-6231e6f1be4a", sms_high] if sms_high != -1 else ["dummy-data", 0], + ] + + caplog.set_level("INFO") + determine_notification_status(notifications_data) + + if log_expected: + assert "[system_status_sms]: sms is {}".format(status) in caplog.text + + +@pytest.mark.parametrize("site, threshold", [("http://site-1.example.com", 400)]) +@pytest.mark.parametrize("response_time", (401, 500)) +def test_logging_determine_site_status_logs_on_site_degraded(mocker, site, threshold, response_time, caplog): + # mock the call to check_response_time() + mocker.patch("notifications_utils.system_status.check_response_time", return_value=response_time) + + caplog.set_level("INFO") + determine_site_status(site, threshold) + + assert "[system_status_site]: site {} is degraded".format(site) in caplog.text + + +@pytest.mark.parametrize("site, threshold", [("http://site-1.example.com", 400)]) +def test_logging_determine_site_status_logs_on_site_down_connection_error(mocker, site, threshold, caplog): + mocker.patch("notifications_utils.system_status.check_response_time", side_effect=requests.exceptions.ConnectionError) + + caplog.set_level("ERROR") + determine_site_status(site, threshold) + + assert "[system_status_site]: site {} is down: Error connecting to url".format(site) in caplog.text + + +@pytest.mark.parametrize("site, threshold", [("http://site-1.example.com", 400)]) +def test_logging_determine_site_status_logs_on_site_down_other_error(mocker, site, threshold, caplog): + mocker.patch("notifications_utils.system_status.check_response_time", side_effect=requests.exceptions.TooManyRedirects) + + caplog.set_level("ERROR") + determine_site_status(site, threshold) + + assert "[system_status_site]: site {} is down: unexpected error".format(site) in caplog.text diff --git a/tests/test_template_types.py b/tests/test_template_types.py index c9f4c39c..a5c80c87 100644 --- a/tests/test_template_types.py +++ b/tests/test_template_types.py @@ -1,26 +1,25 @@ import datetime -from time import process_time import os -from bs4 import BeautifulSoup -import pytest - from functools import partial +from time import process_time from unittest import mock + +import pytest +from bs4 import BeautifulSoup from flask import Markup from freezegun import freeze_time - from notifications_utils.formatters import unlink_govuk_escaped from notifications_utils.template import ( - Template, + EmailPreviewTemplate, HTMLEmailTemplate, - LetterPreviewTemplate, LetterImageTemplate, + LetterPreviewTemplate, + LetterPrintTemplate, PlainTextEmailTemplate, SMSMessageTemplate, SMSPreviewTemplate, + Template, WithSubjectTemplate, - EmailPreviewTemplate, - LetterPrintTemplate, ) diff --git a/tests/test_timezones.py b/tests/test_timezones.py index 28d36871..b890fcae 100644 --- a/tests/test_timezones.py +++ b/tests/test_timezones.py @@ -2,9 +2,13 @@ import pytest import pytz - -from notifications_utils.timezones import convert_est_to_utc, convert_utc_to_est, utc_string_to_aware_gmt_datetime -from notifications_utils.timezones import convert_local_timezone_to_utc, convert_utc_to_local_timezone +from notifications_utils.timezones import ( + convert_est_to_utc, + convert_local_timezone_to_utc, + convert_utc_to_est, + convert_utc_to_local_timezone, + utc_string_to_aware_gmt_datetime, +) @pytest.mark.parametrize( diff --git a/tests/test_url_safe_tokens.py b/tests/test_url_safe_tokens.py index 161e5152..67c467c9 100644 --- a/tests/test_url_safe_tokens.py +++ b/tests/test_url_safe_tokens.py @@ -1,9 +1,8 @@ import urllib -from itsdangerous import BadSignature, SignatureExpired import pytest - -from notifications_utils.url_safe_token import generate_token, check_token +from itsdangerous import BadSignature, SignatureExpired +from notifications_utils.url_safe_token import check_token, generate_token def test_should_return_payload_from_signed_token(): diff --git a/tests/test_validate_html.py b/tests/test_validate_html.py index dea80e91..5a29368e 100644 --- a/tests/test_validate_html.py +++ b/tests/test_validate_html.py @@ -1,5 +1,4 @@ import pytest - from notifications_utils.validate_html import check_if_string_contains_valid_html diff --git a/tests/test_zendesk_client.py b/tests/test_zendesk_client.py index fd67ffdb..b949e2b0 100644 --- a/tests/test_zendesk_client.py +++ b/tests/test_zendesk_client.py @@ -1,7 +1,6 @@ from base64 import b64decode import pytest - from notifications_utils.clients.zendesk.zendesk_client import ZendeskClient, ZendeskError