Merge branch 'main' of https://github.com/GSA/notifications-api into notify-260

This commit is contained in:
Kenneth Kehl
2023-05-30 11:47:36 -07:00
38 changed files with 305 additions and 544 deletions
Generated
+43 -41
View File
@@ -500,6 +500,7 @@
"sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4", "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4",
"sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2" "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"
], ],
"markers": "python_full_version >= '3.5.0'",
"version": "==3.4" "version": "==3.4"
}, },
"importlib-metadata": { "importlib-metadata": {
@@ -847,7 +848,7 @@
"sha256:23ac5d50538a9a38c8bde05fecb47d0b403ecd0662857a86f886f798563d5b9b", "sha256:23ac5d50538a9a38c8bde05fecb47d0b403ecd0662857a86f886f798563d5b9b",
"sha256:45ea77a2f7c60418850331366c81cf6b5b9cf4c7fd34616f733c5427e6abbb1f" "sha256:45ea77a2f7c60418850331366c81cf6b5b9cf4c7fd34616f733c5427e6abbb1f"
], ],
"markers": "python_full_version >= '3.7.0'", "markers": "python_version >= '3.7'",
"version": "==3.0.38" "version": "==3.0.38"
}, },
"psycopg2-binary": { "psycopg2-binary": {
@@ -1062,11 +1063,11 @@
}, },
"requests": { "requests": {
"hashes": [ "hashes": [
"sha256:10e94cc4f3121ee6da529d358cdaeaff2f1c409cd377dbc72b825852f2f7e294", "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f",
"sha256:239d7d4458afcb28a692cdd298d87542235f4ca8d36d03a15bfc128a6559a2f4" "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"
], ],
"markers": "python_version >= '3.7'", "index": "pypi",
"version": "==2.30.0" "version": "==2.31.0"
}, },
"rfc3339-validator": { "rfc3339-validator": {
"hashes": [ "hashes": [
@@ -1087,7 +1088,7 @@
"sha256:78f9a9bf4e7be0c5ded4583326e7461e3a3c5aae24073648b4bdfa797d78c9d2", "sha256:78f9a9bf4e7be0c5ded4583326e7461e3a3c5aae24073648b4bdfa797d78c9d2",
"sha256:9d689e6ca1b3038bc82bf8d23e944b6b6037bc02301a574935b2dd946e0353b9" "sha256:9d689e6ca1b3038bc82bf8d23e944b6b6037bc02301a574935b2dd946e0353b9"
], ],
"markers": "python_version >= '3.5' and python_version < '4'", "markers": "python_version < '4' and python_full_version >= '3.5.0'",
"version": "==4.7.2" "version": "==4.7.2"
}, },
"s3transfer": { "s3transfer": {
@@ -1100,11 +1101,11 @@
}, },
"setuptools": { "setuptools": {
"hashes": [ "hashes": [
"sha256:23aaf86b85ca52ceb801d32703f12d77517b2556af839621c641fca11287952b", "sha256:5df61bf30bb10c6f756eb19e7c9f3b473051f48db77fddbe06ff2ca307df9a6f",
"sha256:f104fa03692a2602fa0fec6c6a9e63b6c8a968de13e17c026957dd1f53d80990" "sha256:62642358adc77ffa87233bc4d2354c4b2682d214048f500964dbe760ccedf102"
], ],
"markers": "python_version >= '3.7'", "markers": "python_version >= '3.7'",
"version": "==67.7.2" "version": "==67.8.0"
}, },
"shapely": { "shapely": {
"hashes": [ "hashes": [
@@ -1216,11 +1217,11 @@
}, },
"typing-extensions": { "typing-extensions": {
"hashes": [ "hashes": [
"sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb", "sha256:6ad00b63f849b7dcc313b70b6b304ed67b2b2963b3098a33efe18056b1a9a223",
"sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4" "sha256:ff6b238610c747e44c268aa4bb23c8c735d665a63726df3f9431ce707f2aa768"
], ],
"markers": "python_version < '3.10'", "markers": "python_version < '3.10'",
"version": "==4.5.0" "version": "==4.6.0"
}, },
"uri-template": { "uri-template": {
"hashes": [ "hashes": [
@@ -1847,6 +1848,7 @@
"sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4", "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4",
"sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2" "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"
], ],
"markers": "python_full_version >= '3.5.0'",
"version": "==3.4" "version": "==3.4"
}, },
"iniconfig": { "iniconfig": {
@@ -2194,7 +2196,7 @@
"sha256:4659bc2a667783e7a15d190f6fccf8b2486685b6dba4c19c3876314769c57526", "sha256:4659bc2a667783e7a15d190f6fccf8b2486685b6dba4c19c3876314769c57526",
"sha256:b4fa3a7a0be38243123cf9d1f3518da10c51bdb165a2b2985566247f9155a7d3" "sha256:b4fa3a7a0be38243123cf9d1f3518da10c51bdb165a2b2985566247f9155a7d3"
], ],
"markers": "python_full_version >= '3.6.0'", "markers": "python_version >= '3.6'",
"version": "==32.0.1" "version": "==32.0.1"
}, },
"pluggy": { "pluggy": {
@@ -2214,22 +2216,22 @@
}, },
"protobuf": { "protobuf": {
"hashes": [ "hashes": [
"sha256:03eee35b60317112a72d19c54d0bff7bc58ff12fea4cd7b018232bd99758ffdf", "sha256:2036a3a1e7fc27f973fa0a7888dce712393af644f4695385f117886abc792e39",
"sha256:2b94bd6df92d71bd1234a2ffe7ce96ddf6d10cf637a18d6b55ad0a89fbb7fc21", "sha256:32e78beda26d7a101fecf15d7a4a792278a0d26a31bc327ff05564a9d68ab8ee",
"sha256:36f5370a930cb77c8ad2f4135590c672d0d2c72d4a707c7d0058dce4b4b4a598", "sha256:346990f634272caac1f09efbcfbbacb23098b1f606d172534c6fa2d9758bb436",
"sha256:5f1eba1da2a2f3f7df469fccddef3cc060b8a16cfe3cc65961ad36b4dbcf59c5", "sha256:3b8905eafe4439076e1f58e9d1fa327025fd2777cf90f14083092ae47f77b0aa",
"sha256:6c16657d6717a0c62d5d740cb354fbad1b0d8cb811669e06fc1caa0ff4799ddd", "sha256:3ce113b3f3362493bddc9069c2163a38f240a9ed685ff83e7bcb756b05e1deb0",
"sha256:6fe180b56e1169d72ecc4acbd39186339aed20af5384531b8e8979b02bbee159", "sha256:410bcc0a5b279f634d3e16082ce221dfef7c3392fac723500e2e64d1806dd2be",
"sha256:7cb5b9a05ce52c6a782bb97de52679bd3438ff2b7460eff5da348db65650f227", "sha256:5b9cd6097e6acae48a68cb29b56bc79339be84eca65b486910bb1e7a30e2b7c1",
"sha256:9744e934ea5855d12191040ea198eaf704ac78665d365a89d9572e3b627c2688", "sha256:65f0ac96ef67d7dd09b19a46aad81a851b6f85f89725577f16de38f2d68ad477",
"sha256:9f5a0fbfcdcc364f3986f9ed9f8bb1328fb84114fd790423ff3d7fdb0f85c2d1", "sha256:91fac0753c3c4951fbb98a93271c43cc7cf3b93cf67747b3e600bb1e5cc14d61",
"sha256:baca40d067dddd62141a129f244703160d278648b569e90bb0e3753067644711", "sha256:95789b569418a3e32a53f43d7763be3d490a831e9c08042539462b6d972c2d7e",
"sha256:d5a35ff54e3f62e8fc7be02bb0d2fbc212bba1a5a9cc2748090690093996f07b", "sha256:ac50be82491369a9ec3710565777e4da87c6d2e20404e0abb1f3a8f10ffd20f0",
"sha256:e62fb869762b4ba18666370e2f8a18f17f8ab92dd4467295c6d38be6f8fef60b", "sha256:decf119d54e820f298ee6d89c72d6b289ea240c32c521f00433f9dc420595f38",
"sha256:ebde3a023b8e11bfa6c890ef34cd6a8b47d586f26135e86c21344fe433daf2e2" "sha256:f9510cac91e764e86acd74e2b7f7bc5e6127a7f3fb646d7c8033cfb84fd1176a"
], ],
"markers": "python_version >= '3.7'", "markers": "python_version >= '3.7'",
"version": "==4.23.0" "version": "==4.23.1"
}, },
"py": { "py": {
"hashes": [ "hashes": [
@@ -2385,11 +2387,11 @@
}, },
"requests": { "requests": {
"hashes": [ "hashes": [
"sha256:10e94cc4f3121ee6da529d358cdaeaff2f1c409cd377dbc72b825852f2f7e294", "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f",
"sha256:239d7d4458afcb28a692cdd298d87542235f4ca8d36d03a15bfc128a6559a2f4" "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"
], ],
"markers": "python_version >= '3.7'", "index": "pypi",
"version": "==2.30.0" "version": "==2.31.0"
}, },
"requests-mock": { "requests-mock": {
"hashes": [ "hashes": [
@@ -2419,7 +2421,7 @@
"sha256:2d11b9b8dd03868f09b4fffadc84a6a8cda574e40dc90821bd845720ebb8e89c", "sha256:2d11b9b8dd03868f09b4fffadc84a6a8cda574e40dc90821bd845720ebb8e89c",
"sha256:69cdf53799e63f38b95b9bf9c875f8c90e78dd62b2f00c13a911c7a3b9fa4704" "sha256:69cdf53799e63f38b95b9bf9c875f8c90e78dd62b2f00c13a911c7a3b9fa4704"
], ],
"markers": "python_full_version >= '3.7.0'", "markers": "python_version >= '3.7'",
"version": "==13.3.5" "version": "==13.3.5"
}, },
"s3transfer": { "s3transfer": {
@@ -2455,11 +2457,11 @@
}, },
"stevedore": { "stevedore": {
"hashes": [ "hashes": [
"sha256:2c428d2338976279e8eb2196f7a94910960d9f7ba2f41f3988511e95ca447021", "sha256:8cc040628f3cea5d7128f2e76cf486b2251a4e543c7b938f58d9a377f6694a2d",
"sha256:bd5a71ff5e5e5f5ea983880e4a1dd1bb47f8feebbb3d95b592398e2f02194771" "sha256:a54534acf9b89bc7ed264807013b505bf07f74dbe4bcfa37d32bd063870b087c"
], ],
"markers": "python_version >= '3.8'", "markers": "python_version >= '3.8'",
"version": "==5.0.0" "version": "==5.1.0"
}, },
"toml": { "toml": {
"hashes": [ "hashes": [
@@ -2479,10 +2481,10 @@
}, },
"types-pyyaml": { "types-pyyaml": {
"hashes": [ "hashes": [
"sha256:5aed5aa66bd2d2e158f75dda22b059570ede988559f030cf294871d3b647e3e8", "sha256:662fa444963eff9b68120d70cda1af5a5f2aa57900003c2006d7626450eaae5f",
"sha256:c51b1bd6d99ddf0aa2884a7a328810ebf70a4262c292195d3f4f9a0005f9eeb6" "sha256:ebab3d0700b946553724ae6ca636ea932c1b0868701d4af121630e78d695fc97"
], ],
"version": "==6.0.12.9" "version": "==6.0.12.10"
}, },
"urllib3": { "urllib3": {
"hashes": [ "hashes": [
@@ -2501,11 +2503,11 @@
}, },
"websocket-client": { "websocket-client": {
"hashes": [ "hashes": [
"sha256:3f09e6d8230892547132177f575a4e3e73cfdf06526e20cc02aa1c3b47184d40", "sha256:c7d67c13b928645f259d9b847ab5b57fd2d127213ca41ebd880de1f553b7c23b",
"sha256:cdf5877568b7e83aa7cf2244ab56a3213de587bbe0ce9d8b9600fc77b455d89e" "sha256:f8c64e28cd700e7ba1f04350d66422b6833b82a796b525a51e740b8cc8dab4b1"
], ],
"markers": "python_version >= '3.7'", "markers": "python_version >= '3.7'",
"version": "==1.5.1" "version": "==1.5.2"
}, },
"werkzeug": { "werkzeug": {
"hashes": [ "hashes": [
+11
View File
@@ -65,3 +65,14 @@ def remove_job_from_s3(service_id, job_id):
def remove_s3_object(bucket_name, object_key, access_key, secret_key, region): def remove_s3_object(bucket_name, object_key, access_key, secret_key, region):
obj = get_s3_object(bucket_name, object_key, access_key, secret_key, region) obj = get_s3_object(bucket_name, object_key, access_key, secret_key, region)
return obj.delete() return obj.delete()
def remove_csv_object(object_key):
obj = get_s3_object(
current_app.config['CSV_UPLOAD_BUCKET']['bucket'],
object_key,
current_app.config['CSV_UPLOAD_BUCKET']['access_key_id'],
current_app.config['CSV_UPLOAD_BUCKET']['secret_access_key'],
current_app.config['CSV_UPLOAD_BUCKET']['region']
)
return obj.delete()
+16
View File
@@ -5,6 +5,7 @@ from sqlalchemy.exc import SQLAlchemyError
from app import notify_celery from app import notify_celery
from app.aws import s3 from app.aws import s3
from app.aws.s3 import remove_csv_object
from app.celery.process_ses_receipts_tasks import check_and_queue_callback_task from app.celery.process_ses_receipts_tasks import check_and_queue_callback_task
from app.config import QueueNames from app.config import QueueNames
from app.cronitor import cronitor from app.cronitor import cronitor
@@ -13,6 +14,7 @@ from app.dao.inbound_sms_dao import delete_inbound_sms_older_than_retention
from app.dao.jobs_dao import ( from app.dao.jobs_dao import (
dao_archive_job, dao_archive_job,
dao_get_jobs_older_than_data_retention, dao_get_jobs_older_than_data_retention,
dao_get_unfinished_jobs,
) )
from app.dao.notifications_dao import ( from app.dao.notifications_dao import (
dao_get_notifications_processing_time_stats, dao_get_notifications_processing_time_stats,
@@ -41,6 +43,19 @@ def _remove_csv_files(job_types):
current_app.logger.info("Job ID {} has been removed from s3.".format(job.id)) current_app.logger.info("Job ID {} has been removed from s3.".format(job.id))
@notify_celery.task(name="cleanup-unfinished-jobs")
def cleanup_unfinished_jobs():
now = datetime.utcnow()
jobs = dao_get_unfinished_jobs()
for job in jobs:
# The query already checks that the processing_finished time is null, so here we are saying
# if it started more than 4 hours ago, that's too long
acceptable_finish_time = job.processing_started + timedelta(minutes=5)
if now > acceptable_finish_time:
remove_csv_object(job.original_file_name)
dao_archive_job(job)
@notify_celery.task(name="delete-notifications-older-than-retention") @notify_celery.task(name="delete-notifications-older-than-retention")
def delete_notifications_older_than_retention(): def delete_notifications_older_than_retention():
delete_email_notifications_older_than_retention.apply_async(queue=QueueNames.REPORTING) delete_email_notifications_older_than_retention.apply_async(queue=QueueNames.REPORTING)
@@ -158,6 +173,7 @@ def delete_inbound_sms():
@notify_celery.task(name='save-daily-notification-processing-time') @notify_celery.task(name='save-daily-notification-processing-time')
@cronitor("save-daily-notification-processing-time") @cronitor("save-daily-notification-processing-time")
def save_daily_notification_processing_time(local_date=None): def save_daily_notification_processing_time(local_date=None):
# local_date is a string in the format of "YYYY-MM-DD" # local_date is a string in the format of "YYYY-MM-DD"
if local_date is None: if local_date is None:
# if a date is not provided, we run against yesterdays data # if a date is not provided, we run against yesterdays data
+8 -1
View File
@@ -11,7 +11,10 @@ from app.clients.email.aws_ses import AwsSesClientThrottlingSendRateException
from app.clients.sms import SmsClientResponseException from app.clients.sms import SmsClientResponseException
from app.config import QueueNames from app.config import QueueNames
from app.dao import notifications_dao from app.dao import notifications_dao
from app.dao.notifications_dao import update_notification_status_by_id from app.dao.notifications_dao import (
insert_notification_history_delete_notifications_by_id,
update_notification_status_by_id,
)
from app.delivery import send_to_providers from app.delivery import send_to_providers
from app.exceptions import NotificationTechnicalFailureException from app.exceptions import NotificationTechnicalFailureException
from app.models import ( from app.models import (
@@ -40,6 +43,10 @@ def check_sms_delivery_receipt(self, message_id, notification_id, sent_at):
update_notification_status_by_id(notification_id, status, provider_response=provider_response) update_notification_status_by_id(notification_id, status, provider_response=provider_response)
current_app.logger.info(f"Updated notification {notification_id} with response '{provider_response}'") current_app.logger.info(f"Updated notification {notification_id} with response '{provider_response}'")
if status == NOTIFICATION_SENT:
insert_notification_history_delete_notifications_by_id(notification_id)
current_app.logger.info(f"Archived notification {notification_id} that was successfully sent")
@notify_celery.task(bind=True, name="deliver_sms", max_retries=48, default_retry_delay=300) @notify_celery.task(bind=True, name="deliver_sms", max_retries=48, default_retry_delay=300)
def deliver_sms(self, notification_id): def deliver_sms(self, notification_id):
+12 -20
View File
@@ -5,6 +5,7 @@ from requests import HTTPError, request
from app.celery.process_ses_receipts_tasks import process_ses_results from app.celery.process_ses_receipts_tasks import process_ses_results
from app.config import QueueNames from app.config import QueueNames
from app.dao.notifications_dao import get_notification_by_id
from app.models import SMS_TYPE from app.models import SMS_TYPE
temp_fail = "2028675303" temp_fail = "2028675303"
@@ -16,8 +17,8 @@ perm_fail_email = "perm-fail@simulator.notify"
temp_fail_email = "temp-fail@simulator.notify" temp_fail_email = "temp-fail@simulator.notify"
def send_sms_response(provider, reference, to): def send_sms_response(provider, reference):
body = sns_callback(reference, to) body = sns_callback(reference)
headers = {"Content-type": "application/json"} headers = {"Content-type": "application/json"}
make_request(SMS_TYPE, provider, body, headers) make_request(SMS_TYPE, provider, body, headers)
@@ -59,25 +60,16 @@ def make_request(notification_type, provider, data, headers):
return response.json() return response.json()
def sns_callback(notification_id, to): def sns_callback(notification_id):
raise Exception("Need to update for SNS callback format along with test_send_to_providers") notification = get_notification_by_id(notification_id)
# example from mmg_callback # This will only work if all notifications, including successful ones, are in the notifications table
# if to.strip().endswith(temp_fail): # If we decide to delete successful notifications, we will have to get this from notifications history
# # status: 4 - expired (temp failure) return json.dumps({
# status = "4" "CID": str(notification_id),
# elif to.strip().endswith(perm_fail): "status": notification.status,
# # status: 5 - rejected (perm failure) # "deliverytime": notification.completed_at
# status = "5" })
# else:
# # status: 3 - delivered
# status = "3"
# return json.dumps({"reference": "mmg_reference",
# "CID": str(notification_id),
# "MSISDN": to,
# "status": status,
# "deliverytime": "2016-04-05 16:01:07"})
def ses_notification_callback(reference): def ses_notification_callback(reference):
+7
View File
@@ -18,6 +18,7 @@ from sqlalchemy.orm.exc import NoResultFound
from app import db from app import db
from app.aws import s3 from app.aws import s3
from app.celery.nightly_tasks import cleanup_unfinished_jobs
from app.celery.tasks import process_row from app.celery.tasks import process_row
from app.dao.annual_billing_dao import ( from app.dao.annual_billing_dao import (
dao_create_or_update_annual_billing_for_year, dao_create_or_update_annual_billing_for_year,
@@ -464,6 +465,12 @@ def fix_billable_units():
print("End fix_billable_units") print("End fix_billable_units")
@notify_command(name='delete-unfinished-jobs')
def delete_unfinished_jobs():
cleanup_unfinished_jobs()
print("End cleanup_unfinished_jobs")
@notify_command(name='process-row-from-job') @notify_command(name='process-row-from-job')
@click.option('-j', '--job_id', required=True, help='Job id') @click.option('-j', '--job_id', required=True, help='Job id')
@click.option('-n', '--job_row_number', type=int, required=True, help='Job id') @click.option('-n', '--job_row_number', type=int, required=True, help='Job id')
+5
View File
@@ -240,6 +240,11 @@ class Config(object):
'schedule': crontab(hour=2, minute=0), 'schedule': crontab(hour=2, minute=0),
'options': {'queue': QueueNames.PERIODIC} 'options': {'queue': QueueNames.PERIODIC}
}, },
'cleanup-unfinished-jobs': {
'task': 'cleanup-unfinished-jobs',
'schedule': crontab(hour=0, minute=5),
'options': {'queue': QueueNames.PERIODIC}
},
'remove_sms_email_jobs': { 'remove_sms_email_jobs': {
'task': 'remove_sms_email_jobs', 'task': 'remove_sms_email_jobs',
'schedule': crontab(hour=4, minute=0), 'schedule': crontab(hour=4, minute=0),
+4
View File
@@ -43,6 +43,10 @@ def dao_get_job_by_service_id_and_job_id(service_id, job_id):
return Job.query.filter_by(service_id=service_id, id=job_id).one() return Job.query.filter_by(service_id=service_id, id=job_id).one()
def dao_get_unfinished_jobs():
return Job.query.filter(Job.processing_finished.is_(None)).all()
def dao_get_jobs_by_service_id( def dao_get_jobs_by_service_id(
service_id, service_id,
*, *,
+31
View File
@@ -271,6 +271,37 @@ def _filter_query(query, filter_dict=None):
return query return query
@autocommit
def insert_notification_history_delete_notifications_by_id(
notification_id
):
"""
Deletes one notification after it has run successfully and moves it to the notification_history
table.
"""
input_params = {
"notification_id": notification_id
}
# Insert into NotificationHistory if the row already exists do nothing.
insert_query = """
insert into notification_history
SELECT id, job_id, job_row_number, service_id, template_id, template_version, api_key_id,
key_type, notification_type, created_at, sent_at, sent_by, updated_at, reference, billable_units,
client_reference, international, phone_prefix, rate_multiplier, notification_status,
created_by_id, document_download_count
from NOTIFICATIONS WHERE id= :notification_id
ON CONFLICT ON CONSTRAINT notification_history_pkey
DO NOTHING
"""
delete_query = """
DELETE FROM notifications
where id= :notification_id
"""
db.session.execute(insert_query, input_params)
db.session.execute(delete_query, input_params)
@autocommit @autocommit
def insert_notification_history_delete_notifications( def insert_notification_history_delete_notifications(
notification_type, service_id, timestamp_to_delete_backwards_from, qry_limit=50000 notification_type, service_id, timestamp_to_delete_backwards_from, qry_limit=50000
+1 -1
View File
@@ -61,7 +61,7 @@ def send_sms_to_provider(notification):
) )
if service.research_mode or notification.key_type == KEY_TYPE_TEST: if service.research_mode or notification.key_type == KEY_TYPE_TEST:
update_notification_to_sending(notification, provider) update_notification_to_sending(notification, provider)
send_sms_response(provider.name, str(notification.id), notification.to) send_sms_response(provider.name, str(notification.id))
else: else:
try: try:
+2 -2
View File
@@ -108,7 +108,7 @@ class User(db.Model):
platform_admin = db.Column(db.Boolean, nullable=False, default=False) platform_admin = db.Column(db.Boolean, nullable=False, default=False)
current_session_id = db.Column(UUID(as_uuid=True), nullable=True) current_session_id = db.Column(UUID(as_uuid=True), nullable=True)
auth_type = db.Column( auth_type = db.Column(
db.String, db.ForeignKey('auth_type.name'), index=True, nullable=False, default=EMAIL_AUTH_TYPE db.String, db.ForeignKey('auth_type.name'), index=True, nullable=False, default=SMS_AUTH_TYPE
) )
email_access_validated_at = db.Column( email_access_validated_at = db.Column(
db.DateTime, index=False, unique=False, nullable=False, default=datetime.datetime.utcnow db.DateTime, index=False, unique=False, nullable=False, default=datetime.datetime.utcnow
@@ -1651,7 +1651,7 @@ class InvitedUser(db.Model):
db.ForeignKey('auth_type.name'), db.ForeignKey('auth_type.name'),
index=True, index=True,
nullable=False, nullable=False,
default=EMAIL_AUTH_TYPE default=SMS_AUTH_TYPE
) )
folder_permissions = db.Column(JSONB(none_as_null=True), nullable=False, default=[]) folder_permissions = db.Column(JSONB(none_as_null=True), nullable=False, default=[])
+1 -1
View File
@@ -117,8 +117,8 @@ Steps for deploying production from scratch. These can be updated for a new clou
1. Update `terraform-production.yml` and `deploy-prod.yml` to point to the correct space and git branch. 1. Update `terraform-production.yml` and `deploy-prod.yml` to point to the correct space and git branch.
1. Ensure that the `api_network_route` and `domain` modules are commented out in `terraform/production/main.tf` 1. Ensure that the `api_network_route` and `domain` modules are commented out in `terraform/production/main.tf`
1. Run CI/CD pipeline on the `production` branch by opening a PR from `main` to `production` 1. Run CI/CD pipeline on the `production` branch by opening a PR from `main` to `production`
1. Uncomment the `api_network_route` and `domain` modules and re-trigger a deploy
1. Create DNS records for `domain` module within https://github.com/18f/dns 1. Create DNS records for `domain` module within https://github.com/18f/dns
1. Uncomment the `api_network_route` and `domain` modules and re-trigger a deploy
### Steps to prepare SES ### Steps to prepare SES
@@ -0,0 +1,28 @@
"""
Revision ID: 0395_remove_international_letters_permission
Revises: 0394_remove_contact_list
Create Date: 2023-05-23 10:03:10.485368
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
revision = '0395_remove_intl_letters_perm'
down_revision = '0394_remove_contact_list'
def upgrade():
sql = """
DELETE
FROM service_permissions
WHERE permission = 'international_letters'
"""
conn = op.get_bind()
conn.execute(sql)
def downgrade():
pass
+16 -1
View File
@@ -7,6 +7,7 @@ from freezegun import freeze_time
from app.celery import nightly_tasks from app.celery import nightly_tasks
from app.celery.nightly_tasks import ( from app.celery.nightly_tasks import (
_delete_notifications_older_than_retention_by_type, _delete_notifications_older_than_retention_by_type,
cleanup_unfinished_jobs,
delete_email_notifications_older_than_retention, delete_email_notifications_older_than_retention,
delete_inbound_sms, delete_inbound_sms,
delete_sms_notifications_older_than_retention, delete_sms_notifications_older_than_retention,
@@ -15,7 +16,7 @@ from app.celery.nightly_tasks import (
save_daily_notification_processing_time, save_daily_notification_processing_time,
timeout_notifications, timeout_notifications,
) )
from app.models import EMAIL_TYPE, SMS_TYPE, FactProcessingTime from app.models import EMAIL_TYPE, SMS_TYPE, FactProcessingTime, Job
from tests.app.db import ( from tests.app.db import (
create_job, create_job,
create_notification, create_notification,
@@ -313,3 +314,17 @@ def test_delete_notifications_task_calls_task_for_services_that_have_sent_notifi
'datetime_to_delete_before': date(2021, 3, 26) 'datetime_to_delete_before': date(2021, 3, 26)
}), }),
]) ])
def test_cleanup_unfinished_jobs(mocker):
mock_s3 = mocker.patch('app.celery.nightly_tasks.remove_csv_object')
mock_dao_archive = mocker.patch('app.celery.nightly_tasks.dao_archive_job')
mock_dao = mocker.patch('app.celery.nightly_tasks.dao_get_unfinished_jobs')
mock_job_unfinished = Job()
mock_job_unfinished.processing_started = datetime(2023, 1, 1, 0, 0, 0)
mock_job_unfinished.original_file_name = "blah"
mock_dao.return_value = [mock_job_unfinished]
cleanup_unfinished_jobs()
mock_s3.assert_called_once_with('blah')
mock_dao_archive.assert_called_once_with(mock_job_unfinished)
+2 -5
View File
@@ -67,11 +67,11 @@ def test_should_retry_and_log_warning_if_SmsClientResponseException_for_deliver_
) )
mocker.patch('app.celery.provider_tasks.deliver_sms.retry') mocker.patch('app.celery.provider_tasks.deliver_sms.retry')
mock_logger_warning = mocker.patch('app.celery.tasks.current_app.logger.warning') mock_logger_warning = mocker.patch('app.celery.tasks.current_app.logger.warning')
assert sample_notification.status == 'created'
deliver_sms(sample_notification.id) deliver_sms(sample_notification.id)
assert provider_tasks.deliver_sms.retry.called is True assert provider_tasks.deliver_sms.retry.called is True
assert sample_notification.status == 'created'
assert mock_logger_warning.called assert mock_logger_warning.called
@@ -82,14 +82,13 @@ def test_should_retry_and_log_exception_for_non_SmsClientResponseException_excep
mocker.patch('app.celery.provider_tasks.deliver_sms.retry') mocker.patch('app.celery.provider_tasks.deliver_sms.retry')
mock_logger_exception = mocker.patch('app.celery.tasks.current_app.logger.exception') mock_logger_exception = mocker.patch('app.celery.tasks.current_app.logger.exception')
assert sample_notification.status == 'created'
deliver_sms(sample_notification.id) deliver_sms(sample_notification.id)
assert provider_tasks.deliver_sms.retry.called is True assert provider_tasks.deliver_sms.retry.called is True
assert sample_notification.status == 'created'
assert mock_logger_exception.called assert mock_logger_exception.called
@pytest.mark.skip(reason="Needs updating for TTS: Failing for unknown reason")
def test_should_go_into_technical_error_if_exceeds_retries_on_deliver_sms_task(sample_notification, mocker): def test_should_go_into_technical_error_if_exceeds_retries_on_deliver_sms_task(sample_notification, mocker):
mocker.patch('app.delivery.send_to_providers.send_sms_to_provider', side_effect=Exception("EXPECTED")) mocker.patch('app.delivery.send_to_providers.send_sms_to_provider', side_effect=Exception("EXPECTED"))
mocker.patch('app.celery.provider_tasks.deliver_sms.retry', side_effect=MaxRetriesExceededError()) mocker.patch('app.celery.provider_tasks.deliver_sms.retry', side_effect=MaxRetriesExceededError())
@@ -128,7 +127,6 @@ def test_should_add_to_retry_queue_if_notification_not_found_in_deliver_email_ta
app.celery.provider_tasks.deliver_email.retry.assert_called_with(queue="retry-tasks") app.celery.provider_tasks.deliver_email.retry.assert_called_with(queue="retry-tasks")
@pytest.mark.skip(reason="Needs updating for TTS: Failing for unknown reason")
@pytest.mark.parametrize( @pytest.mark.parametrize(
'exception_class', [ 'exception_class', [
Exception(), Exception(),
@@ -150,7 +148,6 @@ def test_should_go_into_technical_error_if_exceeds_retries_on_deliver_email_task
assert sample_notification.status == 'technical-failure' assert sample_notification.status == 'technical-failure'
@pytest.mark.skip(reason="Needs updating for TTS: Failing for unknown reason")
def test_should_technical_error_and_not_retry_if_EmailClientNonRetryableException(sample_notification, mocker): def test_should_technical_error_and_not_retry_if_EmailClientNonRetryableException(sample_notification, mocker):
mocker.patch( mocker.patch(
'app.delivery.send_to_providers.send_email_to_provider', 'app.delivery.send_to_providers.send_email_to_provider',
+6 -12
View File
@@ -108,10 +108,9 @@ def test_create_nightly_notification_status_triggers_relevant_tasks(
assert types == expected_types_aggregated assert types == expected_types_aggregated
@pytest.mark.skip(reason="Needs updating for TTS: Timezone handling")
def test_create_nightly_billing_for_day_checks_history( def test_create_nightly_billing_for_day_checks_history(
sample_service, sample_service,
sample_sms_template, sample_template,
mocker mocker
): ):
yesterday = datetime.now() - timedelta(days=1) yesterday = datetime.now() - timedelta(days=1)
@@ -119,13 +118,13 @@ def test_create_nightly_billing_for_day_checks_history(
create_notification( create_notification(
created_at=yesterday, created_at=yesterday,
template=sample_sms_template, template=sample_template,
status='sending', status='sending',
) )
create_notification_history( create_notification_history(
created_at=yesterday, created_at=yesterday,
template=sample_sms_template, template=sample_template,
status='delivered', status='delivered',
) )
@@ -141,7 +140,6 @@ def test_create_nightly_billing_for_day_checks_history(
assert record.notifications_sent == 2 assert record.notifications_sent == 2
@pytest.mark.skip(reason="Needs updating for TTS: Timezone handling")
@pytest.mark.parametrize('second_rate, records_num, billable_units, multiplier', @pytest.mark.parametrize('second_rate, records_num, billable_units, multiplier',
[(1.0, 1, 2, [1]), [(1.0, 1, 2, [1]),
(2.0, 2, 1, [1, 2])]) (2.0, 2, 1, [1, 2])])
@@ -193,7 +191,6 @@ def test_create_nightly_billing_for_day_sms_rate_multiplier(
assert record.rate_multiplier == multiplier[i] assert record.rate_multiplier == multiplier[i]
@pytest.mark.skip(reason="Needs updating for TTS: Timezone handling")
def test_create_nightly_billing_for_day_different_templates( def test_create_nightly_billing_for_day_different_templates(
sample_service, sample_service,
sample_template, sample_template,
@@ -240,8 +237,7 @@ def test_create_nightly_billing_for_day_different_templates(
assert record.rate_multiplier == multiplier[i] assert record.rate_multiplier == multiplier[i]
@pytest.mark.skip(reason="Needs updating for TTS: Timezone handling") def test_create_nightly_billing_for_day_same_sent_by(
def test_create_nightly_billing_for_day_different_sent_by(
sample_service, sample_service,
sample_template, sample_template,
sample_email_template, sample_email_template,
@@ -276,16 +272,15 @@ def test_create_nightly_billing_for_day_different_sent_by(
create_nightly_billing_for_day(str(yesterday.date())) create_nightly_billing_for_day(str(yesterday.date()))
records = FactBilling.query.order_by('rate_multiplier').all() records = FactBilling.query.order_by('rate_multiplier').all()
assert len(records) == 2 assert len(records) == 1
for _, record in enumerate(records): for _, record in enumerate(records):
assert record.local_date == datetime.date(yesterday) assert record.local_date == datetime.date(yesterday)
assert record.rate == Decimal(1.33) assert record.rate == Decimal(1.33)
assert record.billable_units == 1 assert record.billable_units == 2
assert record.rate_multiplier == 1.0 assert record.rate_multiplier == 1.0
@pytest.mark.skip(reason="Needs updating for TTS: Timezone handling")
def test_create_nightly_billing_for_day_null_sent_by_sms( def test_create_nightly_billing_for_day_null_sent_by_sms(
sample_service, sample_service,
sample_template, sample_template,
@@ -497,7 +492,6 @@ def test_create_nightly_notification_status_for_service_and_day(notify_db_sessio
assert sms_delivered_row.key_type == KEY_TYPE_NORMAL assert sms_delivered_row.key_type == KEY_TYPE_NORMAL
@pytest.mark.skip(reason="Needs updating for TTS: Timezone handling")
def test_create_nightly_notification_status_for_service_and_day_overwrites_old_data(notify_db_session): def test_create_nightly_notification_status_for_service_and_day_overwrites_old_data(notify_db_session):
first_service = create_service(service_name='First Service') first_service = create_service(service_name='First Service')
first_template = create_template(service=first_service) first_template = create_template(service=first_service)
+26 -34
View File
@@ -12,6 +12,7 @@ from app.celery.research_mode_tasks import (
sns_callback, sns_callback,
) )
from app.config import QueueNames from app.config import QueueNames
from app.models import NOTIFICATION_DELIVERED, NOTIFICATION_FAILED, Notification
from tests.conftest import Matcher from tests.conftest import Matcher
dvla_response_file_matcher = Matcher( dvla_response_file_matcher = Matcher(
@@ -20,24 +21,33 @@ dvla_response_file_matcher = Matcher(
) )
@pytest.mark.skip(reason="Re-enable when SMS receipts exist") def test_make_sns_callback(notify_api, rmock, mocker):
def test_make_sns_callback(notify_api, rmock):
endpoint = "http://localhost:6011/notifications/sms/sns" endpoint = "http://localhost:6011/notifications/sms/sns"
get_notification_by_id = mocker.patch('app.celery.research_mode_tasks.get_notification_by_id')
n = Notification()
n.id = 1234
n.status = NOTIFICATION_DELIVERED
get_notification_by_id.return_value = n
rmock.request( rmock.request(
"POST", "POST",
endpoint, endpoint,
json={"status": "success"}, json={"status": "success"},
status_code=200) status_code=200)
send_sms_response("sns", "1234", "2028675309") send_sms_response("sns", "1234")
assert rmock.called assert rmock.called
assert rmock.request_history[0].url == endpoint assert rmock.request_history[0].url == endpoint
assert json.loads(rmock.request_history[0].text)['MSISDN'] == '2028675309' assert json.loads(rmock.request_history[0].text)['status'] == 'delivered'
@pytest.mark.skip(reason="Re-enable when SMS receipts exist")
def test_callback_logs_on_api_call_failure(notify_api, rmock, mocker): def test_callback_logs_on_api_call_failure(notify_api, rmock, mocker):
endpoint = "http://localhost:6011/notifications/sms/sns" endpoint = "http://localhost:6011/notifications/sms/sns"
get_notification_by_id = mocker.patch('app.celery.research_mode_tasks.get_notification_by_id')
n = Notification()
n.id = 1234
n.status = NOTIFICATION_FAILED
get_notification_by_id.return_value = n
rmock.request( rmock.request(
"POST", "POST",
endpoint, endpoint,
@@ -46,12 +56,12 @@ def test_callback_logs_on_api_call_failure(notify_api, rmock, mocker):
mock_logger = mocker.patch('app.celery.tasks.current_app.logger.error') mock_logger = mocker.patch('app.celery.tasks.current_app.logger.error')
with pytest.raises(HTTPError): with pytest.raises(HTTPError):
send_sms_response("mmg", "1234", "07700900001") send_sms_response("sns", "1234")
assert rmock.called assert rmock.called
assert rmock.request_history[0].url == endpoint assert rmock.request_history[0].url == endpoint
mock_logger.assert_called_once_with( mock_logger.assert_called_once_with(
'API POST request on http://localhost:6011/notifications/sms/mmg failed with status 500' 'API POST request on http://localhost:6011/notifications/sms/sns failed with status 500'
) )
@@ -65,31 +75,13 @@ def test_make_ses_callback(notify_api, mocker):
assert mock_task.apply_async.call_args[0][0][0] == ses_notification_callback(some_ref) assert mock_task.apply_async.call_args[0][0][0] == ses_notification_callback(some_ref)
@pytest.mark.skip(reason="Re-enable when SNS delivery receipts exist") def test_delivered_sns_callback(mocker):
def test_delievered_sns_callback(): get_notification_by_id = mocker.patch('app.celery.research_mode_tasks.get_notification_by_id')
phone_number = "2028675309" n = Notification()
data = json.loads(sns_callback("1234", phone_number)) n.id = 1234
assert data['MSISDN'] == phone_number n.status = NOTIFICATION_DELIVERED
assert data['status'] == "3" get_notification_by_id.return_value = n
assert data['reference'] == "sns_reference"
assert data['CID'] == "1234" data = json.loads(sns_callback("1234"))
assert data['status'] == "delivered"
@pytest.mark.skip(reason="Re-enable when SNS delivery receipts exist")
def test_perm_failure_sns_callback():
phone_number = "2028675302"
data = json.loads(sns_callback("1234", phone_number))
assert data['MSISDN'] == phone_number
assert data['status'] == "5"
assert data['reference'] == "sns_reference"
assert data['CID'] == "1234"
@pytest.mark.skip(reason="Re-enable when SNS delivery receipts exist")
def test_temp_failure_sns_callback():
phone_number = "2028675303"
data = json.loads(sns_callback("1234", phone_number))
assert data['MSISDN'] == phone_number
assert data['status'] == "4"
assert data['reference'] == "sns_reference"
assert data['CID'] == "1234" assert data['CID'] == "1234"
+3 -5
View File
@@ -15,9 +15,8 @@ def fake_client(notify_api):
return fake_client return fake_client
@pytest.mark.skip(reason="Needs updating for TTS: New SMS client")
def test_send_sms(fake_client, mocker): def test_send_sms(fake_client, mocker):
mock_send = mocker.patch.object(fake_client, 'try_send_sms') mock_send = mocker.patch.object(fake_client, 'send_sms')
fake_client.send_sms( fake_client.send_sms(
to='to', to='to',
@@ -28,14 +27,13 @@ def test_send_sms(fake_client, mocker):
) )
mock_send.assert_called_with( mock_send.assert_called_with(
'to', 'content', 'reference', False, 'testing' to='to', content='content', reference='reference', international=False, sender='testing'
) )
@pytest.mark.skip(reason="Needs updating for TTS: New SMS client")
def test_send_sms_error(fake_client, mocker): def test_send_sms_error(fake_client, mocker):
mocker.patch.object( mocker.patch.object(
fake_client, 'try_send_sms', side_effect=SmsClientResponseException('error') fake_client, 'send_sms', side_effect=SmsClientResponseException('error')
) )
with pytest.raises(SmsClientResponseException): with pytest.raises(SmsClientResponseException):
-2
View File
@@ -210,8 +210,6 @@ def test_fetch_billing_data_for_day_returns_empty_list(notify_db_session):
assert results == [] assert results == []
# TODO: ready for reactivation?
@pytest.mark.skip(reason="Needs updating for TTS: Timezone handling")
def test_fetch_billing_data_for_day_uses_correct_table(notify_db_session): def test_fetch_billing_data_for_day_uses_correct_table(notify_db_session):
service = create_service() service = create_service()
create_service_data_retention(service, notification_type='email', days_of_retention=3) create_service_data_retention(service, notification_type='email', days_of_retention=3)
-1
View File
@@ -116,7 +116,6 @@ def test_get_jobs_for_service(sample_template):
assert one_job_from_db != other_job_from_db assert one_job_from_db != other_job_from_db
@pytest.mark.skip(reason="Needs updating for TTS: Timezone handling")
def test_get_jobs_for_service_with_limit_days_param(sample_template): def test_get_jobs_for_service_with_limit_days_param(sample_template):
one_job = create_job(sample_template) one_job = create_job(sample_template)
old_job = create_job(sample_template, created_at=datetime.now() - timedelta(days=8)) old_job = create_job(sample_template, created_at=datetime.now() - timedelta(days=8))
+23 -4
View File
@@ -90,10 +90,6 @@ def test_update_organisation(notify_db_session):
(['ABC', 'DEF'], {'abc', 'def'}), (['ABC', 'DEF'], {'abc', 'def'}),
([], set()), ([], set()),
(None, {'123', '456'}), (None, {'123', '456'}),
pytest.param(
['abc', 'ABC'], {'abc'},
marks=pytest.mark.xfail(raises=IntegrityError)
),
)) ))
def test_update_organisation_domains_lowercases( def test_update_organisation_domains_lowercases(
notify_db_session, notify_db_session,
@@ -113,6 +109,29 @@ def test_update_organisation_domains_lowercases(
assert {domain.domain for domain in organisation.domains} == expected_domains assert {domain.domain for domain in organisation.domains} == expected_domains
@pytest.mark.parametrize('domain_list, expected_domains', (
(['abc', 'ABC'], {'abc'}),
))
def test_update_organisation_domains_lowercases_integrity_error(
notify_db_session,
domain_list,
expected_domains,
):
create_organisation()
organisation = Organisation.query.one()
# Seed some domains
dao_update_organisation(organisation.id, domains=['123', '456'])
with pytest.raises(expected_exception=IntegrityError):
# This should overwrite the seeded domains
dao_update_organisation(organisation.id, domains=domain_list)
assert {domain.domain for domain in organisation.domains} == expected_domains
def test_update_organisation_does_not_update_the_service_if_certain_attributes_not_provided( def test_update_organisation_does_not_update_the_service_if_certain_attributes_not_provided(
sample_service, sample_service,
sample_organisation, sample_organisation,
@@ -193,44 +193,6 @@ def test_get_sms_providers_for_update_returns_nothing_if_recent_updates(restore_
assert not resp assert not resp
@pytest.mark.skip(reason="Reenable if/when we add a second SMS provider")
@pytest.mark.parametrize(['starting_priorities', 'expected_priorities'], [
({'sns': 50, 'other': 50}, {'sns': 40, 'other': 60}),
({'sns': 0, 'other': 20}, {'sns': 0, 'other': 30}), # lower bound respected
({'sns': 50, 'other': 100}, {'sns': 40, 'other': 100}), # upper bound respected
# document what happens if they have unexpected values outside of the 0 - 100 range (due to manual setting from
# the admin app). the code never causes further issues, but sometimes doesn't actively reset the vaues to 0-100.
({'sns': 150, 'other': 50}, {'sns': 140, 'other': 60}),
({'sns': 50, 'other': 150}, {'sns': 40, 'other': 100}),
({'sns': -100, 'other': 50}, {'sns': 0, 'other': 60}),
({'sns': 50, 'other': -100}, {'sns': 40, 'other': -90}),
])
def test_reduce_sms_provider_priority_adjusts_provider_priorities(
mocker,
restore_provider_details,
notify_user,
starting_priorities,
expected_priorities,
):
mock_adjust = mocker.patch('app.dao.provider_details_dao._adjust_provider_priority')
sns = get_provider_details_by_identifier('sns')
other = get_provider_details_by_identifier('other')
sns.priority = starting_priorities['sns']
other.priority = starting_priorities['other']
# need to update these manually to avoid triggering the `onupdate` clause of the updated_at column
ProviderDetails.query.filter(ProviderDetails.notification_type == 'sms').update({'updated_at': datetime.min})
# switch away from sns. currently both 50/50
dao_reduce_sms_provider_priority('sns', time_threshold=timedelta(minutes=10))
mock_adjust.assert_any_call(other, expected_priorities['other'])
mock_adjust.assert_any_call(sns, expected_priorities['sns'])
def test_reduce_sms_provider_priority_does_nothing_if_providers_have_recently_changed( def test_reduce_sms_provider_priority_does_nothing_if_providers_have_recently_changed(
mocker, mocker,
restore_provider_details, restore_provider_details,
@@ -255,38 +217,6 @@ def test_reduce_sms_provider_priority_does_nothing_if_there_is_only_one_active_p
assert mock_adjust.called is False assert mock_adjust.called is False
@pytest.mark.skip(reason="Reenable if/when we add a second SMS provider")
@pytest.mark.parametrize('existing_sns, existing_other, new_sns, new_other', [
(50, 50, 60, 40), # not just 50/50 - 60/40 specifically
(65, 35, 60, 40), # doesn't overshoot if there's less than 10 difference
(0, 100, 10, 90), # only adjusts by 10
(100, 100, 90, 90), # it tries to fix weird data - it will reduce both if needs be
])
def test_adjust_provider_priority_back_to_resting_points_updates_all_providers(
restore_provider_details,
mocker,
existing_sns,
existing_other,
new_sns,
new_other
):
sns = get_provider_details_by_identifier('sns')
other = get_provider_details_by_identifier('other')
sns.priority = existing_sns
other.priority = existing_other
mock_adjust = mocker.patch('app.dao.provider_details_dao._adjust_provider_priority')
mock_get_providers = mocker.patch('app.dao.provider_details_dao._get_sms_providers_for_update', return_value=[
sns, other
])
dao_adjust_provider_priority_back_to_resting_points()
mock_get_providers.assert_called_once_with(timedelta(hours=1))
mock_adjust.assert_any_call(sns, new_sns)
mock_adjust.assert_any_call(other, new_other)
def test_adjust_provider_priority_back_to_resting_points_does_nothing_if_theyre_already_at_right_values( def test_adjust_provider_priority_back_to_resting_points_does_nothing_if_theyre_already_at_right_values(
restore_provider_details, restore_provider_details,
mocker, mocker,
+4 -8
View File
@@ -3,6 +3,7 @@ from datetime import datetime, timedelta
from unittest import mock from unittest import mock
import pytest import pytest
import sqlalchemy
from freezegun import freeze_time from freezegun import freeze_time
from sqlalchemy.exc import IntegrityError from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm.exc import NoResultFound from sqlalchemy.orm.exc import NoResultFound
@@ -585,7 +586,6 @@ def test_update_service_permission_creates_a_history_record_with_current_data(no
assert history[2].version == 3 assert history[2].version == 3
@pytest.mark.skip(reason="Needs updating for TTS: Failing for unknown reason")
def test_create_service_and_history_is_transactional(notify_db_session): def test_create_service_and_history_is_transactional(notify_db_session):
user = create_user() user = create_user()
assert Service.query.count() == 0 assert Service.query.count() == 0
@@ -596,10 +596,11 @@ def test_create_service_and_history_is_transactional(notify_db_session):
restricted=False, restricted=False,
created_by=user) created_by=user)
with pytest.raises(IntegrityError) as excinfo: try:
dao_create_service(service, user) dao_create_service(service, user)
except sqlalchemy.exc.IntegrityError as seeei:
assert 'null value in column "name" of relation "services_history" violates not-null constraint' in str(seeei)
assert 'column "name" violates not-null constraint' in str(excinfo.value)
assert Service.query.count() == 0 assert Service.query.count() == 0
assert Service.get_history_model().query.count() == 0 assert Service.get_history_model().query.count() == 0
@@ -826,7 +827,6 @@ def test_dao_fetch_todays_stats_for_service_only_includes_today_during_bst(notif
assert not stats.get('permanent-failure') assert not stats.get('permanent-failure')
@pytest.mark.skip(reason="Need a better way to test variable DST date")
def test_dao_fetch_todays_stats_for_service_only_includes_today_when_clocks_fall_back(notify_db_session): def test_dao_fetch_todays_stats_for_service_only_includes_today_when_clocks_fall_back(notify_db_session):
template = create_template(service=create_service()) template = create_template(service=create_service())
with freeze_time('2021-10-30T22:59:59'): with freeze_time('2021-10-30T22:59:59'):
@@ -871,7 +871,6 @@ def test_dao_fetch_todays_stats_for_service_only_includes_during_utc(notify_db_s
assert not stats.get('permanent-failure') assert not stats.get('permanent-failure')
@pytest.mark.skip(reason="Needs updating for TTS: Timezone handling")
def test_dao_fetch_todays_stats_for_all_services_includes_all_services(notify_db_session): def test_dao_fetch_todays_stats_for_all_services_includes_all_services(notify_db_session):
# two services, each with an email and sms notification # two services, each with an email and sms notification
service1 = create_service(service_name='service 1', email_from='service.1') service1 = create_service(service_name='service 1', email_from='service.1')
@@ -910,7 +909,6 @@ def test_dao_fetch_todays_stats_for_all_services_only_includes_today(notify_db_s
assert stats['failed'] == 1 assert stats['failed'] == 1
@pytest.mark.skip(reason="Needs updating for TTS: Timezone handling")
def test_dao_fetch_todays_stats_for_all_services_groups_correctly(notify_db_session): def test_dao_fetch_todays_stats_for_all_services_groups_correctly(notify_db_session):
service1 = create_service(service_name='service 1', email_from='service.1') service1 = create_service(service_name='service 1', email_from='service.1')
service2 = create_service(service_name='service 2', email_from='service.2') service2 = create_service(service_name='service 2', email_from='service.2')
@@ -937,7 +935,6 @@ def test_dao_fetch_todays_stats_for_all_services_groups_correctly(notify_db_sess
service2.created_at, 'sms', 'created', 1) in stats service2.created_at, 'sms', 'created', 1) in stats
@pytest.mark.skip(reason="Needs updating for TTS: Timezone handling")
def test_dao_fetch_todays_stats_for_all_services_includes_all_keys_by_default(notify_db_session): def test_dao_fetch_todays_stats_for_all_services_includes_all_keys_by_default(notify_db_session):
template = create_template(service=create_service()) template = create_template(service=create_service())
create_notification(template=template, key_type=KEY_TYPE_NORMAL) create_notification(template=template, key_type=KEY_TYPE_NORMAL)
@@ -950,7 +947,6 @@ def test_dao_fetch_todays_stats_for_all_services_includes_all_keys_by_default(no
assert stats[0].count == 3 assert stats[0].count == 3
@pytest.mark.skip(reason="Needs updating for TTS: Timezone handling")
def test_dao_fetch_todays_stats_for_all_services_can_exclude_from_test_key(notify_db_session): def test_dao_fetch_todays_stats_for_all_services_can_exclude_from_test_key(notify_db_session):
template = create_template(service=create_service()) template = create_template(service=create_service())
create_notification(template=template, key_type=KEY_TYPE_NORMAL) create_notification(template=template, key_type=KEY_TYPE_NORMAL)
+4 -223
View File
@@ -1,6 +1,5 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta
import pytest
from freezegun import freeze_time from freezegun import freeze_time
from app.dao.uploads_dao import dao_get_uploads_by_service_id from app.dao.uploads_dao import dao_get_uploads_by_service_id
@@ -38,39 +37,21 @@ def create_uploaded_template(service):
) )
@pytest.mark.skip(reason="Investigate what remains after removing letters")
@freeze_time("2020-02-02 09:00") # GMT time @freeze_time("2020-02-02 09:00") # GMT time
def test_get_uploads_for_service(sample_template): def test_get_uploads_for_service(sample_template):
create_service_data_retention(sample_template.service, 'sms', days_of_retention=9) create_service_data_retention(sample_template.service, 'sms', days_of_retention=9)
job = create_job(sample_template, processing_started=datetime.utcnow()) job = create_job(sample_template, processing_started=datetime.utcnow())
letter_template = create_uploaded_template(sample_template.service)
letter = create_uploaded_letter(letter_template, sample_template.service)
other_service = create_service(service_name="other service") other_service = create_service(service_name="other service")
other_template = create_template(service=other_service) other_template = create_template(service=other_service)
other_job = create_job(other_template, processing_started=datetime.utcnow()) other_job = create_job(other_template, processing_started=datetime.utcnow())
other_letter_template = create_uploaded_template(other_service)
create_uploaded_letter(other_letter_template, other_service)
uploads_from_db = dao_get_uploads_by_service_id(job.service_id).items uploads_from_db = dao_get_uploads_by_service_id(job.service_id).items
other_uploads_from_db = dao_get_uploads_by_service_id(other_job.service_id).items other_uploads_from_db = dao_get_uploads_by_service_id(other_job.service_id).items
assert len(uploads_from_db) == 2 assert len(uploads_from_db) == 1
assert uploads_from_db[0] == ( assert uploads_from_db[0] == (
None,
'Uploaded letters',
1,
'letter',
None,
letter.created_at.replace(hour=22, minute=30, second=0, microsecond=0),
None,
letter.created_at.replace(hour=22, minute=30, second=0, microsecond=0),
None,
'letter_day',
None,
)
assert uploads_from_db[1] == (
job.id, job.id,
job.original_file_name, job.original_file_name,
job.notification_count, job.notification_count,
@@ -84,21 +65,8 @@ def test_get_uploads_for_service(sample_template):
None, None,
) )
assert len(other_uploads_from_db) == 2 assert len(other_uploads_from_db) == 1
assert other_uploads_from_db[0] == ( assert other_uploads_from_db[0] == (other_job.id,
None,
'Uploaded letters',
1,
'letter',
None,
letter.created_at.replace(hour=22, minute=30, second=0, microsecond=0),
None,
letter.created_at.replace(hour=22, minute=30, second=0, microsecond=0),
None,
"letter_day",
None,
)
assert other_uploads_from_db[1] == (other_job.id,
other_job.original_file_name, other_job.original_file_name,
other_job.notification_count, other_job.notification_count,
other_job.template.template_type, other_job.template.template_type,
@@ -110,81 +78,9 @@ def test_get_uploads_for_service(sample_template):
"job", "job",
None) None)
assert uploads_from_db[1] != other_uploads_from_db[1] assert uploads_from_db[0] != other_uploads_from_db[0]
@pytest.mark.skip(reason="Investigate what remains after removing letters")
@freeze_time("2020-02-02 18:00")
def test_get_uploads_for_service_groups_letters(sample_template):
letter_template = create_uploaded_template(sample_template.service)
# Just gets into yesterdays print run
create_uploaded_letter(letter_template, sample_template.service, created_at=(
datetime(2020, 2, 1, 22, 29, 59)
))
# Yesterday but in todays print run
create_uploaded_letter(letter_template, sample_template.service, created_at=(
datetime(2020, 2, 1, 22, 30)
))
# First thing today
create_uploaded_letter(letter_template, sample_template.service, created_at=(
datetime(2020, 2, 2, 5, 0)
))
# Just before todays print deadline
create_uploaded_letter(letter_template, sample_template.service, created_at=(
datetime(2020, 2, 2, 22, 29, 59)
))
# Just missed todays print deadline
create_uploaded_letter(letter_template, sample_template.service, created_at=(
datetime(2020, 2, 2, 22, 30)
))
uploads_from_db = dao_get_uploads_by_service_id(sample_template.service_id).items
assert [
(upload.notification_count, upload.created_at)
for upload in uploads_from_db
] == [
(1, datetime(2020, 2, 3, 22, 30)),
(3, datetime(2020, 2, 2, 22, 30)),
(1, datetime(2020, 2, 1, 22, 30)),
]
@pytest.mark.skip(reason="Investigate what remains after removing letters")
def test_get_uploads_does_not_return_cancelled_jobs_or_letters(sample_template):
create_job(sample_template, job_status='scheduled')
create_job(sample_template, job_status='cancelled')
letter_template = create_uploaded_template(sample_template.service)
create_uploaded_letter(letter_template, sample_template.service, status='cancelled')
assert len(dao_get_uploads_by_service_id(sample_template.service_id).items) == 0
@pytest.mark.skip(reason="Investigate what remains after removing letters")
def test_get_uploads_orders_by_created_at_desc(sample_template):
letter_template = create_uploaded_template(sample_template.service)
upload_1 = create_job(sample_template, processing_started=datetime.utcnow(),
job_status=JOB_STATUS_IN_PROGRESS)
upload_2 = create_job(sample_template, processing_started=datetime.utcnow(),
job_status=JOB_STATUS_IN_PROGRESS)
create_uploaded_letter(letter_template, sample_template.service, status='delivered')
results = dao_get_uploads_by_service_id(service_id=sample_template.service_id).items
assert [
(result.id, result.upload_type) for result in results
] == [
(None, 'letter_day'),
(upload_2.id, 'job'),
(upload_1.id, 'job'),
]
@pytest.mark.skip(reason="Investigate what remains after removing letters")
def test_get_uploads_orders_by_processing_started_desc(sample_template): def test_get_uploads_orders_by_processing_started_desc(sample_template):
days_ago = datetime.utcnow() - timedelta(days=3) days_ago = datetime.utcnow() - timedelta(days=3)
upload_1 = create_job(sample_template, processing_started=datetime.utcnow() - timedelta(days=1), upload_1 = create_job(sample_template, processing_started=datetime.utcnow() - timedelta(days=1),
@@ -201,121 +97,6 @@ def test_get_uploads_orders_by_processing_started_desc(sample_template):
assert results[1].id == upload_2.id assert results[1].id == upload_2.id
@pytest.mark.skip(reason="Investigate what remains after removing letters")
@freeze_time("2020-10-27 16:15") # GMT time
def test_get_uploads_orders_by_processing_started_and_created_at_desc(sample_template):
letter_template = create_uploaded_template(sample_template.service)
days_ago = datetime.utcnow() - timedelta(days=4)
create_uploaded_letter(letter_template, service=letter_template.service)
upload_2 = create_job(sample_template, processing_started=datetime.utcnow() - timedelta(days=1),
created_at=days_ago,
job_status=JOB_STATUS_IN_PROGRESS)
upload_3 = create_job(sample_template, processing_started=datetime.utcnow() - timedelta(days=2),
created_at=days_ago,
job_status=JOB_STATUS_IN_PROGRESS)
create_uploaded_letter(letter_template, service=letter_template.service,
created_at=datetime.utcnow() - timedelta(days=3))
results = dao_get_uploads_by_service_id(service_id=sample_template.service_id).items
assert len(results) == 4
assert results[0].id is None
assert results[1].id == upload_2.id
assert results[2].id == upload_3.id
assert results[3].id is None
@pytest.mark.skip(reason="Investigate what remains after removing letters")
@freeze_time('2020-04-02 14:00') # Few days after the clocks go forward
def test_get_uploads_only_gets_uploads_within_service_retention_period(sample_template):
letter_template = create_uploaded_template(sample_template.service)
create_service_data_retention(sample_template.service, 'sms', days_of_retention=3)
days_ago = datetime.utcnow() - timedelta(days=4)
upload_1 = create_uploaded_letter(letter_template, service=letter_template.service)
upload_2 = create_job(
sample_template, processing_started=datetime.utcnow() - timedelta(days=1), created_at=days_ago,
job_status=JOB_STATUS_IN_PROGRESS
)
# older than custom retention for sms:
create_job(
sample_template, processing_started=datetime.utcnow() - timedelta(days=5), created_at=days_ago,
job_status=JOB_STATUS_IN_PROGRESS
)
upload_3 = create_uploaded_letter(
letter_template, service=letter_template.service, created_at=datetime.utcnow() - timedelta(days=3)
)
# older than retention for sms but within letter retention:
upload_4 = create_uploaded_letter(
letter_template, service=letter_template.service, created_at=datetime.utcnow() - timedelta(days=6)
)
# older than default retention for letters:
create_uploaded_letter(
letter_template, service=letter_template.service, created_at=datetime.utcnow() - timedelta(days=8)
)
results = dao_get_uploads_by_service_id(service_id=sample_template.service_id).items
assert len(results) == 4
# Uploaded letters get their `created_at` shifted time of printing
# 21:30 EST == 16:30 UTC
assert results[0].created_at == upload_1.created_at.replace(hour=21, minute=30, second=0, microsecond=0)
# Jobs keep their original `created_at`
assert results[1].created_at == upload_2.created_at.replace(hour=14, minute=00, second=0, microsecond=0)
# Still in BST here…
assert results[2].created_at == upload_3.created_at.replace(hour=21, minute=30, second=0, microsecond=0)
# Now weve gone far enough back to be in GMT
# 17:30 GMT == 17:30 UTC
assert results[3].created_at == upload_4.created_at.replace(hour=21, minute=30, second=0, microsecond=0)
@pytest.mark.skip(reason="Investigate what remains after removing letters")
@freeze_time('2020-02-02 14:00')
def test_get_uploads_is_paginated(sample_template):
letter_template = create_uploaded_template(sample_template.service)
create_uploaded_letter(
letter_template, sample_template.service, status='delivered',
created_at=datetime.utcnow() - timedelta(minutes=3),
)
create_job(
sample_template, processing_started=datetime.utcnow() - timedelta(minutes=2),
job_status=JOB_STATUS_IN_PROGRESS,
)
create_uploaded_letter(
letter_template, sample_template.service, status='delivered',
created_at=datetime.utcnow() - timedelta(minutes=1),
)
create_job(
sample_template, processing_started=datetime.utcnow(),
job_status=JOB_STATUS_IN_PROGRESS,
)
results = dao_get_uploads_by_service_id(sample_template.service_id, page=1, page_size=1)
assert results.per_page == 1
assert results.total == 3
assert len(results.items) == 1
assert results.items[0].created_at == datetime.utcnow().replace(hour=22, minute=30, second=0, microsecond=0)
assert results.items[0].notification_count == 2
assert results.items[0].upload_type == 'letter_day'
results = dao_get_uploads_by_service_id(sample_template.service_id, page=2, page_size=1)
assert len(results.items) == 1
assert results.items[0].created_at == datetime.utcnow().replace(hour=14, minute=0, second=0, microsecond=0)
assert results.items[0].notification_count == 1
assert results.items[0].upload_type == 'job'
@pytest.mark.skip(reason="Investigate what remains after removing letters")
def test_get_uploads_returns_empty_list(sample_service): def test_get_uploads_returns_empty_list(sample_service):
items = dao_get_uploads_by_service_id(sample_service.id).items items = dao_get_uploads_by_service_id(sample_service.id).items
assert items == [] assert items == []
+6 -50
View File
@@ -45,36 +45,6 @@ def setup_function(_function):
send_to_providers.provider_cache.clear() send_to_providers.provider_cache.clear()
@pytest.mark.skip(reason="Reenable when we have more than 1 SMS provider")
def test_provider_to_use_should_return_random_provider(mocker, notify_db_session):
sns = get_provider_details_by_identifier('sns')
other = get_provider_details_by_identifier('other')
sns.priority = 60
other.priority = 40
mock_choices = mocker.patch('app.delivery.send_to_providers.random.choices', return_value=[sns])
ret = send_to_providers.provider_to_use('sms', international=True)
mock_choices.assert_called_once_with([sns, other], weights=[60, 40])
assert ret.name == 'sns'
@pytest.mark.skip(reason="Reenable when we have more than 1 SMS provider")
def test_provider_to_use_should_cache_repeated_calls(mocker, notify_db_session):
mock_choices = mocker.patch(
'app.delivery.send_to_providers.random.choices',
wraps=send_to_providers.random.choices,
)
results = [
send_to_providers.provider_to_use('sms', international=False)
for _ in range(10)
]
assert all(result == results[0] for result in results)
assert len(mock_choices.call_args_list) == 1
@pytest.mark.parametrize('international_provider_priority', ( @pytest.mark.parametrize('international_provider_priority', (
# Since theres only one international provider it should always # Since theres only one international provider it should always
# be used, no matter what its priority is set to # be used, no matter what its priority is set to
@@ -93,18 +63,6 @@ def test_provider_to_use_should_only_return_sns_for_international(
assert ret.name == 'sns' assert ret.name == 'sns'
@pytest.mark.skip(reason="Reenable when we have more than 1 SMS provider")
def test_provider_to_use_should_only_return_active_providers(mocker, restore_provider_details):
sns = get_provider_details_by_identifier('sns')
other = get_provider_details_by_identifier('other')
sns.active = False
other.active = True
ret = send_to_providers.provider_to_use('sms')
assert ret.name == 'other'
def test_provider_to_use_raises_if_no_active_providers(mocker, restore_provider_details): def test_provider_to_use_raises_if_no_active_providers(mocker, restore_provider_details):
sns = get_provider_details_by_identifier('sns') sns = get_provider_details_by_identifier('sns')
sns.active = False sns.active = False
@@ -261,7 +219,7 @@ def test_should_call_send_sms_response_task_if_research_mode(
notify_db_session, sample_service, sample_notification, mocker, research_mode, key_type notify_db_session, sample_service, sample_notification, mocker, research_mode, key_type
): ):
mocker.patch('app.aws_sns_client.send_sms') mocker.patch('app.aws_sns_client.send_sms')
mocker.patch('app.delivery.send_to_providers.send_sms_response') send_sms_response = mocker.patch('app.delivery.send_to_providers.send_sms_response')
if research_mode: if research_mode:
sample_service.research_mode = True sample_service.research_mode = True
@@ -275,8 +233,8 @@ def test_should_call_send_sms_response_task_if_research_mode(
) )
assert not aws_sns_client.send_sms.called assert not aws_sns_client.send_sms.called
app.delivery.send_to_providers.send_sms_response.assert_called_once_with( send_sms_response.assert_called_once_with(
'sns', str(sample_notification.id), sample_notification.to 'sns', str(sample_notification.id)
) )
persisted_notification = notifications_dao.get_notification_by_id(sample_notification.id) persisted_notification = notifications_dao.get_notification_by_id(sample_notification.id)
@@ -288,17 +246,15 @@ def test_should_call_send_sms_response_task_if_research_mode(
assert not persisted_notification.personalisation assert not persisted_notification.personalisation
@pytest.mark.skip(reason="Needs updating when we get SMS delivery receipts done")
def test_should_have_sending_status_if_fake_callback_function_fails(sample_notification, mocker): def test_should_have_sending_status_if_fake_callback_function_fails(sample_notification, mocker):
mocker.patch('app.delivery.send_to_providers.send_sms_response', side_effect=HTTPError) mocker.patch('app.delivery.send_to_providers.send_sms_response', side_effect=HTTPError)
sample_notification.key_type = KEY_TYPE_TEST sample_notification.key_type = KEY_TYPE_TEST
with pytest.raises(HTTPError): with pytest.raises(HTTPError):
send_to_providers.send_sms_to_provider( send_to_providers.send_sms_to_provider(
sample_notification sample_notification
) )
assert sample_notification.status == 'sending' assert sample_notification.status == 'sent'
assert sample_notification.sent_by == 'sns' assert sample_notification.sent_by == 'sns'
@@ -389,14 +345,14 @@ def test_send_email_to_provider_should_call_research_mode_task_response_task_if_
reference = uuid.uuid4() reference = uuid.uuid4()
mocker.patch('app.uuid.uuid4', return_value=reference) mocker.patch('app.uuid.uuid4', return_value=reference)
mocker.patch('app.aws_ses_client.send_email') mocker.patch('app.aws_ses_client.send_email')
mocker.patch('app.delivery.send_to_providers.send_email_response') send_email_response = mocker.patch('app.delivery.send_to_providers.send_email_response')
send_to_providers.send_email_to_provider( send_to_providers.send_email_to_provider(
notification notification
) )
assert not app.aws_ses_client.send_email.called assert not app.aws_ses_client.send_email.called
app.delivery.send_to_providers.send_email_response.assert_called_once_with(str(reference), 'john@smith.com') send_email_response.assert_called_once_with(str(reference), 'john@smith.com')
persisted_notification = Notification.query.filter_by(id=notification.id).one() persisted_notification = Notification.query.filter_by(id=notification.id).one()
assert persisted_notification.to == 'john@smith.com' assert persisted_notification.to == 'john@smith.com'
assert persisted_notification.template_id == sample_email_template.id assert persisted_notification.template_id == sample_email_template.id
-1
View File
@@ -76,7 +76,6 @@ def test_post_to_get_inbound_sms_filters_international_user_number(admin_request
)['data'] )['data']
assert len(sms) == 2 assert len(sms) == 2
print(f'sms is: {sms}')
assert sms[1]['id'] == str(one.id) assert sms[1]['id'] == str(one.id)
assert sms[1]['user_number'] == str(one.user_number) assert sms[1]['user_number'] == str(one.user_number)
@@ -86,7 +86,6 @@ def test_check_and_queue_callback_task(mocker, sample_notification):
# callback_api doesn't match by equality for some # callback_api doesn't match by equality for some
# reason, so we need to take this approach instead # reason, so we need to take this approach instead
print(f'mock_create.mock_calls is: {mock_create.mock_calls}')
mock_create_args = mock_create.mock_calls[0][1] mock_create_args = mock_create.mock_calls[0][1]
assert mock_create_args[0] == sample_notification assert mock_create_args[0] == sample_notification
assert mock_create_args[1].id == callback_api.id assert mock_create_args[1].id == callback_api.id
@@ -587,7 +587,6 @@ def test_check_reply_to_sms_type(sample_service):
assert check_reply_to(sample_service.id, sms_sender.id, SMS_TYPE) == '123456' assert check_reply_to(sample_service.id, sms_sender.id, SMS_TYPE) == '123456'
@pytest.mark.skip(reason="Needs updating for TTS: Failing for unknown reason")
def test_check_if_service_can_send_files_by_email_raises_if_no_contact_link_set(sample_service): def test_check_if_service_can_send_files_by_email_raises_if_no_contact_link_set(sample_service):
with pytest.raises(BadRequestError) as e: with pytest.raises(BadRequestError) as e:
check_if_service_can_send_files_by_email( check_if_service_can_send_files_by_email(
@@ -10,7 +10,6 @@ from tests import create_admin_authorization_header
from tests.app.db import create_invited_org_user from tests.app.db import create_invited_org_user
@pytest.mark.skip(reason="Needs updating for TTS: Failing for unknown reason")
@pytest.mark.parametrize('platform_admin, expected_invited_by', ( @pytest.mark.parametrize('platform_admin, expected_invited_by', (
(True, 'The GOV.UK Notify team'), (True, 'The GOV.UK Notify team'),
(False, 'Test User') (False, 'Test User')
+4 -6
View File
@@ -127,10 +127,7 @@ def test_get_organisation_by_id_returns_domains(admin_request, notify_db_session
('foo.gov.uk', 200), ('foo.gov.uk', 200),
('bar.gov.uk', 200), ('bar.gov.uk', 200),
('oof.gov.uk', 404), ('oof.gov.uk', 404),
pytest.param( ('rab.gov.uk', 200),
'rab.gov.uk', 200,
marks=pytest.mark.xfail(raises=AssertionError),
),
(None, 400), (None, 400),
('personally.identifying.information@example.com', 400), ('personally.identifying.information@example.com', 400),
)) ))
@@ -152,7 +149,9 @@ def test_get_organisation_by_domain(
domain=domain, domain=domain,
) )
if expected_status == 200: if domain == 'rab.gov.uk' and expected_status == 200:
assert response['id'] == str(other_org.id)
elif expected_status == 200:
assert response['id'] == str(org.id) assert response['id'] == str(org.id)
else: else:
assert response['result'] == 'error' assert response['result'] == 'error'
@@ -490,7 +489,6 @@ def test_post_update_organisation_set_mou_doesnt_email_if_no_signed_by(
assert queue_mock.called is False assert queue_mock.called is False
@pytest.mark.skip(reason="Needs updating for TTS: Failing for unknown reason")
@pytest.mark.parametrize('on_behalf_of_name, on_behalf_of_email_address, templates_and_recipients', [ @pytest.mark.parametrize('on_behalf_of_name, on_behalf_of_email_address, templates_and_recipients', [
( (
None, None,
+4 -25
View File
@@ -1,7 +1,5 @@
from datetime import date from datetime import date
import pytest
from tests.app.db import ( from tests.app.db import (
create_ft_notification_status, create_ft_notification_status,
create_process_time, create_process_time,
@@ -9,11 +7,9 @@ from tests.app.db import (
) )
@pytest.mark.skip(reason="Needs updating for TTS: Needs updating for new providers")
def test_performance_dashboard(sample_service, admin_request): def test_performance_dashboard(sample_service, admin_request):
template_sms = create_template(service=sample_service, template_type='sms', template_name='a') template_sms = create_template(service=sample_service, template_type='sms', template_name='a')
template_email = create_template(service=sample_service, template_type='email', template_name='b') template_email = create_template(service=sample_service, template_type='email', template_name='b')
template_letter = create_template(service=sample_service, template_type='letter', template_name='c')
create_ft_notification_status(local_date=date(2021, 2, 28), create_ft_notification_status(local_date=date(2021, 2, 28),
service=template_email.service, service=template_email.service,
template=template_email, template=template_email,
@@ -22,10 +18,6 @@ def test_performance_dashboard(sample_service, admin_request):
service=template_sms.service, service=template_sms.service,
template=template_sms, template=template_sms,
count=5) count=5)
create_ft_notification_status(local_date=date(2021, 2, 28),
service=template_letter.service,
template=template_letter,
count=2)
create_ft_notification_status(local_date=date(2021, 3, 1), create_ft_notification_status(local_date=date(2021, 3, 1),
service=template_email.service, service=template_email.service,
template=template_email, template=template_email,
@@ -34,10 +26,6 @@ def test_performance_dashboard(sample_service, admin_request):
service=template_sms.service, service=template_sms.service,
template=template_sms, template=template_sms,
count=20) count=20)
create_ft_notification_status(local_date=date(2021, 3, 1),
service=template_letter.service,
template=template_letter,
count=3)
create_ft_notification_status(local_date=date(2021, 3, 2), create_ft_notification_status(local_date=date(2021, 3, 2),
service=template_email.service, service=template_email.service,
template=template_email, template=template_email,
@@ -46,10 +34,6 @@ def test_performance_dashboard(sample_service, admin_request):
service=template_sms.service, service=template_sms.service,
template=template_sms, template=template_sms,
count=30) count=30)
create_ft_notification_status(local_date=date(2021, 3, 2),
service=template_letter.service,
template=template_letter,
count=10)
create_ft_notification_status(local_date=date(2021, 3, 3), create_ft_notification_status(local_date=date(2021, 3, 3),
service=template_email.service, service=template_email.service,
template=template_email, template=template_email,
@@ -58,10 +42,6 @@ def test_performance_dashboard(sample_service, admin_request):
service=template_sms.service, service=template_sms.service,
template=template_sms, template=template_sms,
count=35) count=35)
create_ft_notification_status(local_date=date(2021, 3, 3),
service=template_letter.service,
template=template_letter,
count=15)
create_process_time(local_date='2021-02-28', messages_total=15, messages_within_10_secs=14) create_process_time(local_date='2021-02-28', messages_total=15, messages_within_10_secs=14)
create_process_time(local_date='2021-03-01', messages_total=35, messages_within_10_secs=34) create_process_time(local_date='2021-03-01', messages_total=35, messages_within_10_secs=34)
@@ -72,13 +52,12 @@ def test_performance_dashboard(sample_service, admin_request):
start_date='2021-03-01', start_date='2021-03-01',
end_date='2021-03-02') end_date='2021-03-02')
assert results['total_notifications'] == 10+5+2+15+20+3+25+30+10+45+35+15 assert results['total_notifications'] == 185
assert results['email_notifications'] == 10+15+25+45 assert results['email_notifications'] == 10+15+25+45
assert results['sms_notifications'] == 5+20+30+35 assert results['sms_notifications'] == 5+20+30+35
assert results['letter_notifications'] == 2+3+10+15 assert results['notifications_by_type'] == [{"date": '2021-03-01', "emails": 15, "sms": 20},
assert results['notifications_by_type'] == [{"date": '2021-03-01', "emails": 15, "sms": 20, "letters": 3}, {"date": '2021-03-02', "emails": 25, "sms": 30}]
{"date": '2021-03-02', "emails": 25, "sms": 30, "letters": 10}] assert results['processing_time'] == [{"date": "2021-03-01", "percentage_under_10_seconds": 97.14285714285714},
assert results['processing_time'] == [{"date": "2021-03-01", "percentage_under_10_seconds": 97.1428571428571},
{"date": "2021-03-02", "percentage_under_10_seconds": 80.0}] {"date": "2021-03-02", "percentage_under_10_seconds": 80.0}]
assert results["live_service_count"] == 1 assert results["live_service_count"] == 1
assert results["services_using_notify"][0]["service_name"] == sample_service.name assert results["services_using_notify"][0]["service_name"] == sample_service.name
@@ -169,12 +169,6 @@ def test_send_notification_with_placeholders_replaced(notify_api, sample_email_t
), ),
'6', '6',
), ),
pytest.param(
None,
('we consider None equivalent to missing personalisation'),
'',
marks=pytest.mark.xfail
),
]) ])
def test_send_notification_with_placeholders_replaced_with_unusual_types( def test_send_notification_with_placeholders_replaced_with_unusual_types(
client, client,
@@ -209,6 +203,43 @@ def test_send_notification_with_placeholders_replaced_with_unusual_types(
assert response_data['subject'] == expected_subject assert response_data['subject'] == expected_subject
@pytest.mark.parametrize('personalisation, expected_body, expected_subject', [
(
None,
('we consider None equivalent to missing personalisation'),
'',
),
])
def test_send_notification_with_placeholders_replaced_with_unusual_types_no_personalization(
client,
sample_email_template_with_placeholders,
mocker,
personalisation,
expected_body,
expected_subject,
):
mocker.patch('app.celery.provider_tasks.deliver_email.apply_async')
response = client.post(
path='/notifications/email',
data=json.dumps(
{
'to': 'ok@ok.com',
'template': str(sample_email_template_with_placeholders.id),
'personalisation': {
'name': personalisation
}
}
),
headers=[
('Content-Type', 'application/json'),
create_service_authorization_header(service_id=sample_email_template_with_placeholders.service.id)
]
)
assert response.status_code == 400
def test_should_not_send_notification_for_archived_template(notify_api, sample_template): def test_should_not_send_notification_for_archived_template(notify_api, sample_template):
with notify_api.test_request_context(): with notify_api.test_request_context():
with notify_api.test_client() as client: with notify_api.test_client() as client:
-4
View File
@@ -1944,7 +1944,6 @@ def test_get_detailed_service(sample_template, client, sample_service, today_onl
assert service['statistics'][SMS_TYPE] == stats assert service['statistics'][SMS_TYPE] == stats
@pytest.mark.skip(reason="Needs updating for TTS: Timezone handling")
def test_get_services_with_detailed_flag(client, sample_template): def test_get_services_with_detailed_flag(client, sample_template):
notifications = [ notifications = [
create_notification(sample_template), create_notification(sample_template),
@@ -1967,7 +1966,6 @@ def test_get_services_with_detailed_flag(client, sample_template):
} }
@pytest.mark.skip(reason="Needs updating for TTS: Timezone handling")
def test_get_services_with_detailed_flag_excluding_from_test_key(client, sample_template): def test_get_services_with_detailed_flag_excluding_from_test_key(client, sample_template):
create_notification(sample_template, key_type=KEY_TYPE_NORMAL) create_notification(sample_template, key_type=KEY_TYPE_NORMAL)
create_notification(sample_template, key_type=KEY_TYPE_TEAM) create_notification(sample_template, key_type=KEY_TYPE_TEAM)
@@ -2023,7 +2021,6 @@ def test_get_services_with_detailed_flag_defaults_to_today(client, mocker):
assert resp.status_code == 200 assert resp.status_code == 200
@pytest.mark.skip(reason="Needs updating for TTS: Timezone handling")
def test_get_detailed_services_groups_by_service(notify_db_session): def test_get_detailed_services_groups_by_service(notify_db_session):
from app.service.rest import get_detailed_services from app.service.rest import get_detailed_services
@@ -2054,7 +2051,6 @@ def test_get_detailed_services_groups_by_service(notify_db_session):
} }
@pytest.mark.skip(reason="Needs updating for TTS: Timezone handling")
def test_get_detailed_services_includes_services_with_no_notifications(notify_db_session): def test_get_detailed_services_includes_services_with_no_notifications(notify_db_session):
from app.service.rest import get_detailed_services from app.service.rest import get_detailed_services
@@ -11,7 +11,6 @@ from tests import create_admin_authorization_header
from tests.app.db import create_invited_user from tests.app.db import create_invited_user
@pytest.mark.skip(reason="Needs updating for TTS: Failing for unknown reason")
@pytest.mark.parametrize('extra_args, expected_start_of_invite_url', [ @pytest.mark.parametrize('extra_args, expected_start_of_invite_url', [
( (
{}, {},
@@ -73,7 +72,6 @@ def test_create_invited_user(
mocked.assert_called_once_with([(str(notification.id))], queue="notify-internal-tasks") mocked.assert_called_once_with([(str(notification.id))], queue="notify-internal-tasks")
@pytest.mark.skip(reason="Needs updating for TTS: Failing for unknown reason")
def test_create_invited_user_without_auth_type(admin_request, sample_service, mocker, invitation_email_template): def test_create_invited_user_without_auth_type(admin_request, sample_service, mocker, invitation_email_template):
mocker.patch('app.celery.provider_tasks.deliver_email.apply_async') mocker.patch('app.celery.provider_tasks.deliver_email.apply_async')
email_address = 'invited_user@service.gov.uk' email_address = 'invited_user@service.gov.uk'
@@ -126,13 +124,11 @@ def test_create_invited_user_invalid_email(client, sample_service, mocker, fake_
assert mocked.call_count == 0 assert mocked.call_count == 0
@pytest.mark.skip(reason="Needs updating for TTS: Failing for unknown reason")
def test_get_all_invited_users_by_service(client, notify_db_session, sample_service): def test_get_all_invited_users_by_service(client, notify_db_session, sample_service):
invites = [] invites = []
for i in range(0, 5): for i in range(0, 5):
email = 'invited_user_{}@service.gov.uk'.format(i) email = 'invited_user_{}@service.gov.uk'.format(i)
invited_user = create_invited_user(sample_service, to_email_address=email) invited_user = create_invited_user(sample_service, to_email_address=email)
invites.append(invited_user) invites.append(invited_user)
url = '/service/{}/invite'.format(sample_service.id) url = '/service/{}/invite'.format(sample_service.id)
-5
View File
@@ -1,8 +1,3 @@
import pytest
@pytest.mark.skip(reason="Needs updating for TTS")
def test_all_routes_have_authentication(client): def test_all_routes_have_authentication(client):
# This tests that each blueprint registered on the application has a before_request function registered. # This tests that each blueprint registered on the application has a before_request function registered.
# The None row is removed from the comparison as that is not blueprint specific but app specific. # The None row is removed from the comparison as that is not blueprint specific but app specific.
-4
View File
@@ -47,7 +47,6 @@ def test_get_user_list(admin_request, sample_service):
assert sorted(expected_permissions) == sorted(fetched['permissions'][str(sample_service.id)]) assert sorted(expected_permissions) == sorted(fetched['permissions'][str(sample_service.id)])
@pytest.mark.skip(reason="Needs updating for TTS: Failing for unknown reason")
def test_get_user(admin_request, sample_service, sample_organisation): def test_get_user(admin_request, sample_service, sample_organisation):
""" """
Tests GET endpoint '/<user_id>' to retrieve a single service. Tests GET endpoint '/<user_id>' to retrieve a single service.
@@ -123,7 +122,6 @@ def test_post_user(admin_request, notify_db_session):
assert user.auth_type == EMAIL_AUTH_TYPE assert user.auth_type == EMAIL_AUTH_TYPE
@pytest.mark.skip(reason="Needs updating for TTS: Failing for unknown reason")
def test_post_user_without_auth_type(admin_request, notify_db_session): def test_post_user_without_auth_type(admin_request, notify_db_session):
User.query.delete() User.query.delete()
data = { data = {
@@ -618,7 +616,6 @@ def test_send_user_reset_password_should_send_reset_password_link(admin_request,
assert notification.reply_to_text == notify_service.get_default_reply_to_email_address() assert notification.reply_to_text == notify_service.get_default_reply_to_email_address()
@pytest.mark.skip(reason="Needs updating for TTS: Failing for unknown reason")
@pytest.mark.parametrize('data, expected_url', ( @pytest.mark.parametrize('data, expected_url', (
({ ({
'email': 'notify@digital.cabinet-office.gov.uk', 'email': 'notify@digital.cabinet-office.gov.uk',
@@ -820,7 +817,6 @@ def test_activate_user_fails_if_already_active(admin_request, sample_user):
assert sample_user.state == 'active' assert sample_user.state == 'active'
@pytest.mark.skip(reason="Needs updating for TTS: Failing for unknown reason")
def test_update_user_auth_type(admin_request, sample_user): def test_update_user_auth_type(admin_request, sample_user):
assert sample_user.auth_type == 'sms_auth' assert sample_user.auth_type == 'sms_auth'
resp = admin_request.post( resp = admin_request.post(
-2
View File
@@ -292,7 +292,6 @@ def test_send_sms_code_returns_204_when_too_many_codes_already_created(client, s
assert VerifyCode.query.count() == 5 assert VerifyCode.query.count() == 5
@pytest.mark.skip(reason="Needs updating for TTS: Failing for unknown reason")
@pytest.mark.parametrize('post_data, expected_url_starts_with', ( @pytest.mark.parametrize('post_data, expected_url_starts_with', (
( (
{}, {},
@@ -387,7 +386,6 @@ def test_reset_failed_login_count_returns_404_when_user_does_not_exist(client):
assert resp.status_code == 404 assert resp.status_code == 404
@pytest.mark.skip(reason="Needs updating for TTS: Failing for unknown reason")
# we send sms_auth users and webauthn_auth users email code to validate their email access # we send sms_auth users and webauthn_auth users email code to validate their email access
@pytest.mark.parametrize('auth_type', USER_AUTH_TYPES) @pytest.mark.parametrize('auth_type', USER_AUTH_TYPES)
@pytest.mark.parametrize('data, expected_auth_url', ( @pytest.mark.parametrize('data, expected_auth_url', (
@@ -238,7 +238,6 @@ def test_should_cache_template_lookups_in_memory(mocker, client, sample_template
assert Notification.query.count() == 5 assert Notification.query.count() == 5
@pytest.mark.skip(reason="Needs updating for TTS: cloud.gov redis fails, local docker works, mock redis fails")
def test_should_cache_template_and_service_in_redis(mocker, client, sample_template): def test_should_cache_template_and_service_in_redis(mocker, client, sample_template):
from app.schemas import service_schema, template_schema from app.schemas import service_schema, template_schema
@@ -289,7 +288,6 @@ def test_should_cache_template_and_service_in_redis(mocker, client, sample_templ
assert templates_call[1]['ex'] == 604_800 assert templates_call[1]['ex'] == 604_800
@pytest.mark.skip(reason="Needs updating for TTS: cloud.gov redis fails, local docker works, mock redis fails")
def test_should_return_template_if_found_in_redis(mocker, client, sample_template): def test_should_return_template_if_found_in_redis(mocker, client, sample_template):
from app.schemas import service_schema, template_schema from app.schemas import service_schema, template_schema
@@ -313,7 +311,7 @@ def test_should_return_template_if_found_in_redis(mocker, client, sample_templat
mocker.patch('app.celery.provider_tasks.deliver_sms.apply_async') mocker.patch('app.celery.provider_tasks.deliver_sms.apply_async')
data = { data = {
'phone_number': '+447700900855', 'phone_number': '+16615555555',
'template_id': str(sample_template.id), 'template_id': str(sample_template.id),
} }