diff --git a/app/celery/letters_pdf_tasks.py b/app/celery/letters_pdf_tasks.py index b164e64f8..4cd0caf1d 100644 --- a/app/celery/letters_pdf_tasks.py +++ b/app/celery/letters_pdf_tasks.py @@ -210,7 +210,7 @@ def process_virus_scan_passed(self, filename): try: billable_units = _get_page_count(notification, old_pdf) except PdfReadError: - _move_invalid_letter_and_update_status(notification.reference, filename, scan_pdf_object) + _move_invalid_letter_and_update_status(notification, filename, scan_pdf_object) return new_pdf = _sanitise_precompiled_pdf(self, notification, old_pdf) @@ -222,7 +222,7 @@ def process_virus_scan_passed(self, filename): if not new_pdf: current_app.logger.info('Invalid precompiled pdf received {} ({})'.format(notification.id, filename)) - _move_invalid_letter_and_update_status(notification.reference, filename, scan_pdf_object) + _move_invalid_letter_and_update_status(notification, filename, scan_pdf_object) return else: current_app.logger.info( @@ -230,18 +230,23 @@ def process_virus_scan_passed(self, filename): current_app.logger.info('notification id {} ({}) sanitised and ready to send'.format(notification.id, filename)) - _upload_pdf_to_test_or_live_pdf_bucket( - new_pdf, - filename, - is_test_letter=is_test_key) + try: + _upload_pdf_to_test_or_live_pdf_bucket( + new_pdf, + filename, + is_test_letter=is_test_key) - update_letter_pdf_status( - reference=reference, - status=NOTIFICATION_DELIVERED if is_test_key else NOTIFICATION_CREATED, - billable_units=billable_units - ) - - scan_pdf_object.delete() + update_letter_pdf_status( + reference=reference, + status=NOTIFICATION_DELIVERED if is_test_key else NOTIFICATION_CREATED, + billable_units=billable_units + ) + scan_pdf_object.delete() + except BotoClientError: + current_app.logger.exception( + "Error uploading letter to live pdf bucket for notification: {}".format(notification.id) + ) + update_notification_status_by_id(notification.id, NOTIFICATION_TECHNICAL_FAILURE) def _get_page_count(notification, old_pdf): @@ -255,14 +260,20 @@ def _get_page_count(notification, old_pdf): raise e -def _move_invalid_letter_and_update_status(notification_reference, filename, scan_pdf_object): - move_scan_to_invalid_pdf_bucket(filename) - scan_pdf_object.delete() +def _move_invalid_letter_and_update_status(notification, filename, scan_pdf_object): + try: + move_scan_to_invalid_pdf_bucket(filename) + scan_pdf_object.delete() - update_letter_pdf_status( - reference=notification_reference, - status=NOTIFICATION_VALIDATION_FAILED, - billable_units=0) + update_letter_pdf_status( + reference=notification.reference, + status=NOTIFICATION_VALIDATION_FAILED, + billable_units=0) + except BotoClientError: + current_app.logger.exception( + "Error when moving letter with id {} to invalid PDF bucket".format(notification.id) + ) + update_notification_status_by_id(notification.id, NOTIFICATION_TECHNICAL_FAILURE) def _upload_pdf_to_test_or_live_pdf_bucket(pdf_data, filename, is_test_letter): diff --git a/app/celery/nightly_tasks.py b/app/celery/nightly_tasks.py index 97c1fa59a..73086fb40 100644 --- a/app/celery/nightly_tasks.py +++ b/app/celery/nightly_tasks.py @@ -150,11 +150,17 @@ def timeout_notifications(): @notify_celery.task(name='send-daily-performance-platform-stats') @cronitor('send-daily-performance-platform-stats') @statsd(namespace="tasks") -def send_daily_performance_platform_stats(): +def send_daily_performance_platform_stats(date=None): + # date is a string in the format of "YYYY-MM-DD" + if date is None: + date = (datetime.utcnow() - timedelta(days=1)).date() + else: + date = datetime.strptime(date, "%Y-%m-%d").date() + if performance_platform_client.active: - yesterday = datetime.utcnow() - timedelta(days=1) - send_total_sent_notifications_to_performance_platform(bst_date=yesterday.date()) - processing_time.send_processing_time_to_performance_platform() + + send_total_sent_notifications_to_performance_platform(bst_date=date) + processing_time.send_processing_time_to_performance_platform(bst_date=date) def send_total_sent_notifications_to_performance_platform(bst_date): diff --git a/app/celery/scheduled_tasks.py b/app/celery/scheduled_tasks.py index f19aaf135..1cf508b31 100644 --- a/app/celery/scheduled_tasks.py +++ b/app/celery/scheduled_tasks.py @@ -8,7 +8,7 @@ from notifications_utils.statsd_decorators import statsd from sqlalchemy import and_ from sqlalchemy.exc import SQLAlchemyError -from app import notify_celery +from app import notify_celery, zendesk_client from app.celery.tasks import process_job from app.config import QueueNames, TaskNames from app.dao.invited_org_user_dao import delete_org_invitations_created_more_than_two_days_ago @@ -19,7 +19,9 @@ from app.dao.notifications_dao import ( is_delivery_slow_for_provider, dao_get_scheduled_notifications, set_scheduled_notification_to_processed, - notifications_not_yet_sent + notifications_not_yet_sent, + dao_precompiled_letters_still_pending_virus_check, + dao_old_letters_with_created_status, ) from app.dao.provider_details_dao import ( get_current_provider, @@ -178,3 +180,45 @@ def replay_created_notifications(): for n in notifications_to_resend: send_notification_to_queue(notification=n, research_mode=n.service.research_mode) + + +@notify_celery.task(name='check-precompiled-letter-state') +@statsd(namespace="tasks") +def check_precompiled_letter_state(): + letters = dao_precompiled_letters_still_pending_virus_check() + + if len(letters) > 0: + letter_ids = [str(letter.id) for letter in letters] + + msg = "{} precompiled letters have been pending-virus-check for over 90 minutes. " \ + "Notifications: {}".format(len(letters), letter_ids) + + current_app.logger.exception(msg) + + if current_app.config['NOTIFY_ENVIRONMENT'] in ['live', 'production', 'test']: + zendesk_client.create_ticket( + subject="[{}] Letters still pending virus check".format(current_app.config['NOTIFY_ENVIRONMENT']), + message=msg, + ticket_type=zendesk_client.TYPE_INCIDENT + ) + + +@notify_celery.task(name='check-templated-letter-state') +@statsd(namespace="tasks") +def check_templated_letter_state(): + letters = dao_old_letters_with_created_status() + + if len(letters) > 0: + letter_ids = [str(letter.id) for letter in letters] + + msg = "{} letters were created before 17.30 yesterday and still have 'created' status. " \ + "Notifications: {}".format(len(letters), letter_ids) + + current_app.logger.exception(msg) + + if current_app.config['NOTIFY_ENVIRONMENT'] in ['live', 'production', 'test']: + zendesk_client.create_ticket( + subject="[{}] Letters still in 'created' status".format(current_app.config['NOTIFY_ENVIRONMENT']), + message=msg, + ticket_type=zendesk_client.TYPE_INCIDENT + ) diff --git a/app/config.py b/app/config.py index 50889727a..2c5ee54ae 100644 --- a/app/config.py +++ b/app/config.py @@ -259,6 +259,11 @@ class Config(object): # since we mark jobs as archived 'options': {'queue': QueueNames.PERIODIC}, }, + 'check-templated-letter-state': { + 'task': 'check-templated-letter-state', + 'schedule': crontab(day_of_week='mon-fri', hour=9, minute=0), + 'options': {'queue', QueueNames.PERIODIC} + }, 'raise-alert-if-letter-notifications-still-sending': { 'task': 'raise-alert-if-letter-notifications-still-sending', 'schedule': crontab(hour=16, minute=30), @@ -276,6 +281,11 @@ class Config(object): 'schedule': crontab(hour=23, minute=00), 'options': {'queue': QueueNames.PERIODIC} }, + 'check-precompiled-letter-state': { + 'task': 'check-precompiled-letter-state', + 'schedule': crontab(day_of_week='mon-fri', hour='9,15', minute=0), + 'options': {'queue', QueueNames.PERIODIC} + }, } CELERY_QUEUES = [] diff --git a/app/dao/notifications_dao.py b/app/dao/notifications_dao.py index 35873cf66..882189ae3 100644 --- a/app/dao/notifications_dao.py +++ b/app/dao/notifications_dao.py @@ -14,7 +14,7 @@ from notifications_utils.recipients import ( try_validate_and_format_phone_number ) from notifications_utils.statsd_decorators import statsd -from notifications_utils.timezones import convert_utc_to_bst +from notifications_utils.timezones import convert_bst_to_utc, convert_utc_to_bst from sqlalchemy import (desc, func, asc) from sqlalchemy.orm import joinedload from sqlalchemy.orm.exc import NoResultFound @@ -692,6 +692,32 @@ def notifications_not_yet_sent(should_be_sending_after_seconds, notification_typ return notifications +def dao_old_letters_with_created_status(): + yesterday_bst = convert_utc_to_bst(datetime.utcnow()) - timedelta(days=1) + last_processing_deadline = yesterday_bst.replace(hour=17, minute=30, second=0, microsecond=0) + + notifications = Notification.query.filter( + Notification.created_at < convert_bst_to_utc(last_processing_deadline), + Notification.notification_type == LETTER_TYPE, + Notification.status == NOTIFICATION_CREATED + ).order_by( + Notification.created_at + ).all() + return notifications + + +def dao_precompiled_letters_still_pending_virus_check(): + ninety_minutes_ago = datetime.utcnow() - timedelta(seconds=5400) + + notifications = Notification.query.filter( + Notification.created_at < ninety_minutes_ago, + Notification.status == NOTIFICATION_PENDING_VIRUS_CHECK + ).order_by( + Notification.created_at + ).all() + return notifications + + def guess_notification_type(search_term): if set(search_term) & set(string.ascii_letters + '@'): return EMAIL_TYPE diff --git a/app/models.py b/app/models.py index 0465214f4..320aa33de 100644 --- a/app/models.py +++ b/app/models.py @@ -344,6 +344,8 @@ class Organisation(db.Model): db.ForeignKey('users.id'), nullable=True, ) + agreement_signed_on_behalf_of_name = db.Column(db.String(255), nullable=True) + agreement_signed_on_behalf_of_email_address = db.Column(db.String(255), nullable=True) agreement_signed_version = db.Column(db.Float, nullable=True) crown = db.Column(db.Boolean, nullable=True) organisation_type = db.Column(db.String(255), nullable=True) @@ -367,6 +369,13 @@ class Organisation(db.Model): nullable=True, ) + @property + def live_services(self): + return [ + service for service in self.services + if service.active and not service.restricted + ] + def serialize(self): return { "id": str(self.id), @@ -379,11 +388,14 @@ class Organisation(db.Model): "agreement_signed": self.agreement_signed, "agreement_signed_at": self.agreement_signed_at, "agreement_signed_by_id": self.agreement_signed_by_id, + "agreement_signed_on_behalf_of_name": self.agreement_signed_on_behalf_of_name, + "agreement_signed_on_behalf_of_email_address": self.agreement_signed_on_behalf_of_email_address, "agreement_signed_version": self.agreement_signed_version, "domains": [ domain.domain for domain in self.domains ], "request_to_go_live_notes": self.request_to_go_live_notes, + "count_of_live_services": len(self.live_services), } diff --git a/app/performance_platform/processing_time.py b/app/performance_platform/processing_time.py index ee2bfde1a..756c08bc1 100644 --- a/app/performance_platform/processing_time.py +++ b/app/performance_platform/processing_time.py @@ -1,16 +1,15 @@ -from datetime import datetime +from datetime import timedelta from flask import current_app -from app.utils import get_midnight_for_day_before, get_london_midnight_in_utc +from app.utils import get_london_midnight_in_utc from app.dao.notifications_dao import dao_get_total_notifications_sent_per_day_for_performance_platform from app import performance_platform_client -def send_processing_time_to_performance_platform(): - today = datetime.utcnow() - start_time = get_midnight_for_day_before(today) - end_time = get_london_midnight_in_utc(today) +def send_processing_time_to_performance_platform(bst_date): + start_time = get_london_midnight_in_utc(bst_date) + end_time = get_london_midnight_in_utc(bst_date + timedelta(days=1)) send_processing_time_for_start_and_end(start_time, end_time) diff --git a/app/user/rest.py b/app/user/rest.py index 456008c30..444dac914 100644 --- a/app/user/rest.py +++ b/app/user/rest.py @@ -515,7 +515,8 @@ def get_orgs_and_services(user): } for service in org.services if service.active and service in user.services - ] + ], + 'count_of_live_services': len(org.live_services), } for org in user.organisations if org.active ], @@ -534,5 +535,14 @@ def get_orgs_and_services(user): service.organisation not in user.organisations ) ) + ], + 'services': [ + { + 'id': service.id, + 'name': service.name, + 'restricted': service.restricted, + 'organisation': service.organisation.id if service.organisation else None, + } + for service in user.services if service.active ] } diff --git a/manifest.yml.j2 b/manifest.yml.j2 index 6d8f56c99..62fddcf0d 100644 --- a/manifest.yml.j2 +++ b/manifest.yml.j2 @@ -84,6 +84,7 @@ applications: MMG_INBOUND_SMS_AUTH: '{{ MMG_INBOUND_SMS_AUTH | tojson }}' MMG_INBOUND_SMS_USERNAME: '{{ MMG_INBOUND_SMS_USERNAME | tojson }}' + FIRETEXT_URL: '{{ FIRETEXT_URL }}' FIRETEXT_API_KEY: '{{ FIRETEXT_API_KEY }}' LOADTESTING_API_KEY: '{{ LOADTESTING_API_KEY }}' FIRETEXT_INBOUND_SMS_AUTH: '{{ FIRETEXT_INBOUND_SMS_AUTH | tojson }}' diff --git a/migrations/versions/0296_agreement_signed_by_person.py b/migrations/versions/0296_agreement_signed_by_person.py new file mode 100644 index 000000000..f10d97c1b --- /dev/null +++ b/migrations/versions/0296_agreement_signed_by_person.py @@ -0,0 +1,22 @@ +""" + +Revision ID: 0296_agreement_signed_by_person +Revises: 0295_api_key_constraint +Create Date: 2019-06-13 16:40:32.982607 + +""" +from alembic import op +import sqlalchemy as sa + +revision = '0296_agreement_signed_by_person' +down_revision = '0295_api_key_constraint' + + +def upgrade(): + op.add_column('organisation', sa.Column('agreement_signed_on_behalf_of_email_address', sa.String(length=255), nullable=True)) + op.add_column('organisation', sa.Column('agreement_signed_on_behalf_of_name', sa.String(length=255), nullable=True)) + + +def downgrade(): + op.drop_column('organisation', 'agreement_signed_on_behalf_of_name') + op.drop_column('organisation', 'agreement_signed_on_behalf_of_email_address') diff --git a/tests/app/celery/test_letters_pdf_tasks.py b/tests/app/celery/test_letters_pdf_tasks.py index 8396347e6..f99af3e80 100644 --- a/tests/app/celery/test_letters_pdf_tasks.py +++ b/tests/app/celery/test_letters_pdf_tasks.py @@ -23,6 +23,7 @@ from app.celery.letters_pdf_tasks import ( process_virus_scan_failed, process_virus_scan_error, replay_letters_in_error, + _move_invalid_letter_and_update_status, _sanitise_precompiled_pdf ) from app.letters.utils import get_letter_pdf_filename, ScanErrorType @@ -46,6 +47,10 @@ from tests.conftest import set_config_values def test_should_have_decorated_tasks_functions(): assert create_letters_pdf.__wrapped__.__name__ == 'create_letters_pdf' + assert collate_letter_pdfs_for_day.__wrapped__.__name__ == 'collate_letter_pdfs_for_day' + assert process_virus_scan_passed.__wrapped__.__name__ == 'process_virus_scan_passed' + assert process_virus_scan_failed.__wrapped__.__name__ == 'process_virus_scan_failed' + assert process_virus_scan_error.__wrapped__.__name__ == 'process_virus_scan_error' @pytest.mark.parametrize('personalisation', [{'name': 'test'}, None]) @@ -517,6 +522,67 @@ def test_process_letter_task_check_virus_scan_passed_when_file_cannot_be_opened( assert sample_letter_notification.billable_units == 0 +@mock_s3 +def test_process_virus_scan_passed_logs_error_and_sets_tech_failure_if_s3_error_uploading_to_live_bucket( + mocker, + sample_letter_notification, +): + mock_logger = mocker.patch('app.celery.tasks.current_app.logger.exception') + + sample_letter_notification.status = NOTIFICATION_PENDING_VIRUS_CHECK + filename = 'NOTIFY.{}'.format(sample_letter_notification.reference) + + source_bucket_name = current_app.config['LETTERS_SCAN_BUCKET_NAME'] + conn = boto3.resource('s3', region_name='eu-west-1') + conn.create_bucket(Bucket=source_bucket_name) + + s3 = boto3.client('s3', region_name='eu-west-1') + s3.put_object(Bucket=source_bucket_name, Key=filename, Body=b'pdf_content') + + mocker.patch('app.celery.letters_pdf_tasks._get_page_count', return_value=1) + mocker.patch('app.celery.letters_pdf_tasks._sanitise_precompiled_pdf', return_value=b'pdf_content') + + error_response = { + 'Error': { + 'Code': 'InvalidParameterValue', + 'Message': 'some error message from amazon', + 'Type': 'Sender' + } + } + mocker.patch('app.celery.letters_pdf_tasks._upload_pdf_to_test_or_live_pdf_bucket', + side_effect=ClientError(error_response, 'operation_name')) + + process_virus_scan_passed(filename) + + assert sample_letter_notification.status == NOTIFICATION_TECHNICAL_FAILURE + mock_logger.assert_called_once_with( + 'Error uploading letter to live pdf bucket for notification: {}'.format(sample_letter_notification.id) + ) + + +def test_move_invalid_letter_and_update_status_logs_error_and_sets_tech_failure_state_if_s3_error( + mocker, + sample_letter_notification, +): + error_response = { + 'Error': { + 'Code': 'InvalidParameterValue', + 'Message': 'some error message from amazon', + 'Type': 'Sender' + } + } + mocker.patch('app.celery.letters_pdf_tasks.move_scan_to_invalid_pdf_bucket', + side_effect=ClientError(error_response, 'operation_name')) + mock_logger = mocker.patch('app.celery.tasks.current_app.logger.exception') + + _move_invalid_letter_and_update_status(sample_letter_notification, 'filename', mocker.Mock()) + + assert sample_letter_notification.status == NOTIFICATION_TECHNICAL_FAILURE + mock_logger.assert_called_once_with( + 'Error when moving letter with id {} to invalid PDF bucket'.format(sample_letter_notification.id) + ) + + def test_process_letter_task_check_virus_scan_failed(sample_letter_notification, mocker): filename = 'NOTIFY.{}'.format(sample_letter_notification.reference) sample_letter_notification.status = NOTIFICATION_PENDING_VIRUS_CHECK diff --git a/tests/app/celery/test_scheduled_tasks.py b/tests/app/celery/test_scheduled_tasks.py index bf4eb9507..baf108380 100644 --- a/tests/app/celery/test_scheduled_tasks.py +++ b/tests/app/celery/test_scheduled_tasks.py @@ -13,7 +13,9 @@ from app.celery.scheduled_tasks import ( run_scheduled_jobs, send_scheduled_notifications, switch_current_sms_provider_on_slow_delivery, - replay_created_notifications + replay_created_notifications, + check_precompiled_letter_state, + check_templated_letter_state, ) from app.config import QueueNames, TaskNames from app.dao.jobs_dao import dao_get_job_by_id @@ -26,6 +28,8 @@ from app.models import ( JOB_STATUS_IN_PROGRESS, JOB_STATUS_ERROR, JOB_STATUS_FINISHED, + NOTIFICATION_DELIVERED, + NOTIFICATION_PENDING_VIRUS_CHECK, ) from app.v2.errors import JobIncompleteError @@ -58,7 +62,7 @@ def prepare_current_provider(restore_provider_details): db.session.commit() -def test_should_call_delete_codes_on_delete_verify_codes_task(notify_api, mocker): +def test_should_call_delete_codes_on_delete_verify_codes_task(notify_db_session, mocker): mocker.patch('app.celery.scheduled_tasks.delete_codes_older_created_more_than_a_day_ago') delete_verify_codes() assert scheduled_tasks.delete_codes_older_created_more_than_a_day_ago.call_count == 1 @@ -334,3 +338,84 @@ def test_check_job_status_task_does_not_raise_error(sample_template): job_status=JOB_STATUS_FINISHED) check_job_status() + + +@freeze_time("2019-05-30 14:00:00") +def test_check_precompiled_letter_state(mocker, sample_letter_template): + mock_logger = mocker.patch('app.celery.tasks.current_app.logger.exception') + mock_create_ticket = mocker.patch('app.celery.nightly_tasks.zendesk_client.create_ticket') + + create_notification(template=sample_letter_template, + status=NOTIFICATION_PENDING_VIRUS_CHECK, + created_at=datetime.utcnow() - timedelta(seconds=5400)) + create_notification(template=sample_letter_template, + status=NOTIFICATION_DELIVERED, + created_at=datetime.utcnow() - timedelta(seconds=6000)) + noti_1 = create_notification(template=sample_letter_template, + status=NOTIFICATION_PENDING_VIRUS_CHECK, + created_at=datetime.utcnow() - timedelta(seconds=5401)) + noti_2 = create_notification(template=sample_letter_template, + status=NOTIFICATION_PENDING_VIRUS_CHECK, + created_at=datetime.utcnow() - timedelta(seconds=70000)) + + check_precompiled_letter_state() + + message = "2 precompiled letters have been pending-virus-check for over 90 minutes. " \ + "Notifications: ['{}', '{}']".format(noti_2.id, noti_1.id) + + mock_logger.assert_called_once_with(message) + mock_create_ticket.assert_called_with( + message=message, + subject='[test] Letters still pending virus check', + ticket_type='incident' + ) + + +@freeze_time("2019-05-30 14:00:00") +def test_check_templated_letter_state_during_bst(mocker, sample_letter_template): + mock_logger = mocker.patch('app.celery.tasks.current_app.logger.exception') + mock_create_ticket = mocker.patch('app.celery.nightly_tasks.zendesk_client.create_ticket') + + noti_1 = create_notification(template=sample_letter_template, created_at=datetime(2019, 5, 1, 12, 0)) + noti_2 = create_notification(template=sample_letter_template, created_at=datetime(2019, 5, 29, 16, 29)) + create_notification(template=sample_letter_template, created_at=datetime(2019, 5, 29, 16, 30)) + create_notification(template=sample_letter_template, created_at=datetime(2019, 5, 29, 17, 29)) + create_notification(template=sample_letter_template, status='delivered', created_at=datetime(2019, 5, 28, 10, 0)) + create_notification(template=sample_letter_template, created_at=datetime(2019, 5, 30, 10, 0)) + + check_templated_letter_state() + + message = "2 letters were created before 17.30 yesterday and still have 'created' status. " \ + "Notifications: ['{}', '{}']".format(noti_1.id, noti_2.id) + + mock_logger.assert_called_once_with(message) + mock_create_ticket.assert_called_with( + message=message, + subject="[test] Letters still in 'created' status", + ticket_type='incident' + ) + + +@freeze_time("2019-01-30 14:00:00") +def test_check_templated_letter_state_during_utc(mocker, sample_letter_template): + mock_logger = mocker.patch('app.celery.tasks.current_app.logger.exception') + mock_create_ticket = mocker.patch('app.celery.nightly_tasks.zendesk_client.create_ticket') + + noti_1 = create_notification(template=sample_letter_template, created_at=datetime(2018, 12, 1, 12, 0)) + noti_2 = create_notification(template=sample_letter_template, created_at=datetime(2019, 1, 29, 17, 29)) + create_notification(template=sample_letter_template, created_at=datetime(2019, 1, 29, 17, 30)) + create_notification(template=sample_letter_template, created_at=datetime(2019, 1, 29, 18, 29)) + create_notification(template=sample_letter_template, status='delivered', created_at=datetime(2019, 1, 29, 10, 0)) + create_notification(template=sample_letter_template, created_at=datetime(2019, 1, 30, 10, 0)) + + check_templated_letter_state() + + message = "2 letters were created before 17.30 yesterday and still have 'created' status. " \ + "Notifications: ['{}', '{}']".format(noti_1.id, noti_2.id) + + mock_logger.assert_called_once_with(message) + mock_create_ticket.assert_called_with( + message=message, + subject="[test] Letters still in 'created' status", + ticket_type='incident' + ) diff --git a/tests/app/organisation/test_rest.py b/tests/app/organisation/test_rest.py index 4c231e50b..d99670d7a 100644 --- a/tests/app/organisation/test_rest.py +++ b/tests/app/organisation/test_rest.py @@ -26,8 +26,10 @@ def test_get_all_organisations(admin_request, notify_db_session): assert len(response) == 2 assert response[0]['name'] == 'active org' assert response[0]['active'] is True + assert response[0]['count_of_live_services'] == 0 assert response[1]['name'] == 'inactive org' assert response[1]['active'] is False + assert response[1]['count_of_live_services'] == 0 def test_get_organisation_by_id(admin_request, notify_db_session): @@ -49,10 +51,13 @@ def test_get_organisation_by_id(admin_request, notify_db_session): 'agreement_signed_at', 'agreement_signed_by_id', 'agreement_signed_version', + 'agreement_signed_on_behalf_of_name', + 'agreement_signed_on_behalf_of_email_address', 'letter_branding_id', 'email_branding_id', 'domains', 'request_to_go_live_notes', + 'count_of_live_services', } assert response['id'] == str(org.id) assert response['name'] == 'test_org_1' @@ -66,6 +71,9 @@ def test_get_organisation_by_id(admin_request, notify_db_session): assert response['email_branding_id'] is None assert response['domains'] == [] assert response['request_to_go_live_notes'] is None + assert response['count_of_live_services'] == 0 + assert response['agreement_signed_on_behalf_of_name'] is None + assert response['agreement_signed_on_behalf_of_email_address'] is None def test_get_organisation_by_id_returns_domains(admin_request, notify_db_session): @@ -193,6 +201,8 @@ def test_post_update_organisation_updates_fields( 'active': False, 'agreement_signed': agreement_signed, 'crown': crown, + 'agreement_signed_on_behalf_of_name': 'Firstname Lastname', + 'agreement_signed_on_behalf_of_email_address': 'test@example.com', } assert org.agreement_signed is None assert org.crown is None @@ -213,6 +223,8 @@ def test_post_update_organisation_updates_fields( assert organisation[0].agreement_signed == agreement_signed assert organisation[0].crown == crown assert organisation[0].domains == [] + assert organisation[0].agreement_signed_on_behalf_of_name == 'Firstname Lastname' + assert organisation[0].agreement_signed_on_behalf_of_email_address == 'test@example.com' @pytest.mark.parametrize('domain_list', ( diff --git a/tests/app/performance_platform/test_processing_time.py b/tests/app/performance_platform/test_processing_time.py index 2458e866d..010c50097 100644 --- a/tests/app/performance_platform/test_processing_time.py +++ b/tests/app/performance_platform/test_processing_time.py @@ -1,4 +1,4 @@ -from datetime import datetime, timedelta +from datetime import datetime, timedelta, date from freezegun import freeze_time @@ -19,7 +19,7 @@ def test_send_processing_time_to_performance_platform_generates_correct_calls(mo create_notification(sample_template, created_at=created_at, sent_at=created_at + timedelta(seconds=15)) create_notification(sample_template, created_at=datetime.utcnow() - timedelta(days=2)) - send_processing_time_to_performance_platform() + send_processing_time_to_performance_platform(date(2016, 10, 17)) send_mock.assert_any_call(datetime(2016, 10, 16, 23, 0), 'messages-total', 2) send_mock.assert_any_call(datetime(2016, 10, 16, 23, 0), 'messages-within-10-secs', 1) diff --git a/tests/app/user/test_rest.py b/tests/app/user/test_rest.py index 35e3809ce..8a51d8968 100644 --- a/tests/app/user/test_rest.py +++ b/tests/app/user/test_rest.py @@ -835,38 +835,63 @@ def test_get_orgs_and_services_nests_services(admin_request, sample_user): resp = admin_request.get('user.get_organisations_and_services_for_user', user_id=sample_user.id) - assert resp == { - 'organisations': [ - { - 'name': org1.name, - 'id': str(org1.id), - 'services': [ - { - 'name': service1.name, - 'id': str(service1.id), - 'restricted': False, - }, - { - 'name': service2.name, - 'id': str(service2.id), - 'restricted': False, - } - ] - }, - { - 'name': org2.name, - 'id': str(org2.id), - 'services': [] - } - ], - 'services_without_organisations': [ - { - 'name': service3.name, - 'id': str(service3.id), - 'restricted': False, - } - ] + assert set(resp.keys()) == { + 'organisations', + 'services_without_organisations', + 'services', } + assert resp['organisations'] == [ + { + 'name': org1.name, + 'id': str(org1.id), + 'services': [ + { + 'name': service1.name, + 'id': str(service1.id), + 'restricted': False, + }, + { + 'name': service2.name, + 'id': str(service2.id), + 'restricted': False, + } + ], + 'count_of_live_services': 2, + }, + { + 'name': org2.name, + 'id': str(org2.id), + 'services': [], + 'count_of_live_services': 0, + }, + ] + assert resp['services_without_organisations'] == [ + { + 'name': service3.name, + 'id': str(service3.id), + 'restricted': False, + } + ] + assert resp['services'] == [ + { + 'name': service1.name, + 'id': str(service1.id), + 'restricted': False, + 'organisation': str(org1.id), + }, + { + 'name': service2.name, + 'id': str(service2.id), + 'restricted': False, + 'organisation': str(org1.id), + }, + { + 'name': service3.name, + 'id': str(service3.id), + 'restricted': False, + 'organisation': None, + }, + ] def test_get_orgs_and_services_only_returns_active(admin_request, sample_user): @@ -890,28 +915,52 @@ def test_get_orgs_and_services_only_returns_active(admin_request, sample_user): resp = admin_request.get('user.get_organisations_and_services_for_user', user_id=sample_user.id) - assert resp == { - 'organisations': [ - { - 'name': org1.name, - 'id': str(org1.id), - 'services': [ - { - 'name': service1.name, - 'id': str(service1.id), - 'restricted': False, - } - ] - } - ], - 'services_without_organisations': [ - { - 'name': service4.name, - 'id': str(service4.id), - 'restricted': False, - } - ] + assert set(resp.keys()) == { + 'organisations', + 'services_without_organisations', + 'services', } + assert resp['organisations'] == [ + { + 'name': org1.name, + 'id': str(org1.id), + 'services': [ + { + 'name': service1.name, + 'id': str(service1.id), + 'restricted': False, + } + ], + 'count_of_live_services': 1, + } + ] + assert resp['services_without_organisations'] == [ + { + 'name': service4.name, + 'id': str(service4.id), + 'restricted': False, + } + ] + assert resp['services'] == [ + { + 'name': service1.name, + 'id': str(service1.id), + 'restricted': False, + 'organisation': str(org1.id) + }, + { + 'name': service3.name, + 'id': str(service3.id), + 'restricted': False, + 'organisation': str(org2.id) + }, + { + 'name': service4.name, + 'id': str(service4.id), + 'restricted': False, + 'organisation': None, + }, + ] def test_get_orgs_and_services_only_shows_users_orgs_and_services(admin_request, sample_user): @@ -932,23 +981,37 @@ def test_get_orgs_and_services_only_shows_users_orgs_and_services(admin_request, resp = admin_request.get('user.get_organisations_and_services_for_user', user_id=sample_user.id) - assert resp == { - 'organisations': [ - { - 'name': org2.name, - 'id': str(org2.id), - 'services': [] - } - ], - # service1 belongs to org1, but the user doesn't know about org1 - 'services_without_organisations': [ - { - 'name': service1.name, - 'id': str(service1.id), - 'restricted': False, - } - ] + assert set(resp.keys()) == { + 'organisations', + 'services_without_organisations', + 'services', } + assert resp['organisations'] == [ + { + 'name': org2.name, + 'id': str(org2.id), + 'services': [], + 'count_of_live_services': 0, + } + ] + # service1 belongs to org1, but the user doesn't know about org1 + assert resp['services_without_organisations'] == [ + { + 'name': service1.name, + 'id': str(service1.id), + 'restricted': False, + } + ] + # 'services' always returns the org_id no matter whether the user + # belongs to that org or not + assert resp['services'] == [ + { + 'name': service1.name, + 'id': str(service1.id), + 'restricted': False, + 'organisation': str(org1.id), + } + ] def test_find_users_by_email_finds_user_by_partial_email(notify_db, client):