diff --git a/app/authentication/auth.py b/app/authentication/auth.py index 8a919fdf6..2d326d15c 100644 --- a/app/authentication/auth.py +++ b/app/authentication/auth.py @@ -1,7 +1,10 @@ from flask import request, jsonify, _request_ctx_stack, current_app from notifications_python_client.authentication import decode_jwt_token, get_token_issuer from notifications_python_client.errors import TokenDecodeError, TokenRequestError, TokenExpiredError, TokenPayloadError +from werkzeug.exceptions import abort from app.dao.api_key_dao import get_unsigned_secrets +from app import api_user +from functools import wraps def authentication_response(message, code): @@ -68,3 +71,14 @@ def fetch_client(client): "client": client, "secret": get_unsigned_secrets(client) } + + +def require_admin(): + def wrap(func): + @wraps(func) + def wrap_func(*args, **kwargs): + if not api_user['client'] == current_app.config.get('ADMIN_CLIENT_USER_NAME'): + abort(403) + return func(*args, **kwargs) + return wrap_func + return wrap diff --git a/app/dao/notifications_dao.py b/app/dao/notifications_dao.py index f92ac0393..6646a0ce9 100644 --- a/app/dao/notifications_dao.py +++ b/app/dao/notifications_dao.py @@ -1,5 +1,7 @@ +from flask import current_app from app import db from app.models import Notification +from sqlalchemy import asc def dao_create_notification(notification): @@ -16,9 +18,24 @@ def get_notification_for_job(service_id, job_id, notification_id): return Notification.query.filter_by(service_id=service_id, job_id=job_id, id=notification_id).one() -def get_notifications_for_job(service_id, job_id): - return Notification.query.filter_by(service_id=service_id, job_id=job_id).all() +def get_notifications_for_job(service_id, job_id, page=1): + query = Notification.query.filter_by(service_id=service_id, job_id=job_id)\ + .order_by(asc(Notification.created_at))\ + .paginate( + page=page, + per_page=current_app.config['PAGE_SIZE'] + ) + return query def get_notification(service_id, notification_id): return Notification.query.filter_by(service_id=service_id, id=notification_id).one() + + +def get_notifications_for_service(service_id, page=1): + print(service_id) + query = Notification.query.filter_by(service_id=service_id).order_by(asc(Notification.created_at)).paginate( + page=page, + per_page=current_app.config['PAGE_SIZE'] + ) + return query diff --git a/app/notifications/rest.py b/app/notifications/rest.py index 97db7c5b4..c7490ce78 100644 --- a/app/notifications/rest.py +++ b/app/notifications/rest.py @@ -4,10 +4,12 @@ from flask import ( Blueprint, jsonify, request, - current_app + current_app, + url_for ) from app import api_user, encryption, create_uuid +from app.authentication.auth import require_admin from app.dao import ( templates_dao, services_dao, @@ -40,6 +42,86 @@ def get_notifications(notification_id): return jsonify(result="error", message="not found"), 404 +@notifications.route('/notifications', methods=['GET']) +def get_all_notifications(): + page = get_page_from_request() + + if not page: + return jsonify(result="error", message="Invalid page"), 400 + + all_notifications = notifications_dao.get_notifications_for_service(api_user['client'], page) + + return jsonify( + notifications=notification_status_schema.dump(all_notifications.items, many=True).data, + links=pagination_links( + all_notifications, + '.get_all_notifications', + request.args + ) + ), 200 + + +@notifications.route('/service//notifications', methods=['GET']) +@require_admin() +def get_all_notifications_for_service(service_id): + page = get_page_from_request() + + if not page: + return jsonify(result="error", message="Invalid page"), 400 + + all_notifications = notifications_dao.get_notifications_for_service(service_id, page) + + return jsonify( + notifications=notification_status_schema.dump(all_notifications.items, many=True).data, + links=pagination_links( + all_notifications, + '.get_all_notifications_for_service', + request.args + ) + ), 200 + + +@notifications.route('/service//job//notifications', methods=['GET']) +@require_admin() +def get_all_notifications_for_service_job(service_id, job_id): + page = get_page_from_request() + + if not page: + return jsonify(result="error", message="Invalid page"), 400 + + all_notifications = notifications_dao.get_notifications_for_job(service_id, job_id, page) + + return jsonify( + notifications=notification_status_schema.dump(all_notifications.items, many=True).data, + links=pagination_links( + all_notifications, + '.get_all_notifications_for_service_job', + request.args + ) + ), 200 + + +def get_page_from_request(): + if 'page' in request.args: + try: + return int(request.args['page']) + + except ValueError: + return None + else: + return 1 + + +def pagination_links(pagination, endpoint, args): + links = dict() + if pagination.has_prev: + links['prev'] = url_for(endpoint, **dict(list(args.items()) + [('page', pagination.prev_num)])) + if pagination.has_next: + links['next'] = url_for(endpoint, **dict(list(args.items()) + [('page', pagination.next_num)])) + links['last'] = url_for(endpoint, **dict(list(args.items()) + [('page', pagination.pages)])) + return links + + @notifications.route('/notifications/sms', methods=['POST']) def create_sms_notification(): return send_notification(notification_type=SMS_NOTIFICATION) diff --git a/config.py b/config.py index 000112235..32bd50c1d 100644 --- a/config.py +++ b/config.py @@ -20,6 +20,7 @@ class Config(object): SQLALCHEMY_RECORD_QUERIES = True VERIFY_CODE_FROM_EMAIL_ADDRESS = os.environ['VERIFY_CODE_FROM_EMAIL_ADDRESS'] NOTIFY_EMAIL_DOMAIN = os.environ['NOTIFY_EMAIL_DOMAIN'] + PAGE_SIZE = 50 BROKER_URL = 'sqs://' BROKER_TRANSPORT_OPTIONS = { diff --git a/tests/app/conftest.py b/tests/app/conftest.py index b0a9626bc..756fdd940 100644 --- a/tests/app/conftest.py +++ b/tests/app/conftest.py @@ -269,11 +269,11 @@ def sample_notification(notify_db, if job is None: job = sample_job(notify_db, notify_db_session, service=service, template=template) - notificaton_id = uuid.uuid4() + notification_id = uuid.uuid4() to = '+44709123456' data = { - 'id': notificaton_id, + 'id': notification_id, 'to': to, 'job': job, 'service': service, diff --git a/tests/app/dao/test_notification_dao.py b/tests/app/dao/test_notification_dao.py index 9ba17c727..4ddffd47e 100644 --- a/tests/app/dao/test_notification_dao.py +++ b/tests/app/dao/test_notification_dao.py @@ -82,7 +82,7 @@ def test_get_all_notifications_for_job(notify_db, notify_db_session, sample_job) template=sample_job.template, job=sample_job) - notifcations_from_db = get_notifications_for_job(sample_job.service.id, sample_job.id) + notifcations_from_db = get_notifications_for_job(sample_job.service.id, sample_job.id).items assert len(notifcations_from_db) == 5 diff --git a/tests/app/notifications/test_rest.py b/tests/app/notifications/test_rest.py index c0c46d452..ab4342094 100644 --- a/tests/app/notifications/test_rest.py +++ b/tests/app/notifications/test_rest.py @@ -1,6 +1,7 @@ import uuid import app.celery.tasks from tests import create_authorization_header +from tests.app.conftest import sample_notification, sample_job, sample_service from flask import json from app.models import Service from app.dao.templates_dao import dao_get_all_templates_for_service @@ -47,6 +48,204 @@ def test_get_notifications_empty_result(notify_api, sample_api_key): assert response.status_code == 404 +def test_get_all_notifications(notify_api, sample_notification): + with notify_api.test_request_context(): + with notify_api.test_client() as client: + auth_header = create_authorization_header( + service_id=sample_notification.service_id, + path='/notifications', + method='GET') + + response = client.get( + '/notifications', + headers=[auth_header]) + + notifications = json.loads(response.get_data(as_text=True)) + assert notifications['notifications'][0]['status'] == 'sent' + assert notifications['notifications'][0]['template'] == sample_notification.template.id + assert notifications['notifications'][0]['to'] == '+44709123456' + assert notifications['notifications'][0]['service'] == str(sample_notification.service_id) + assert response.status_code == 200 + + +def test_get_all_notifications_newest_first(notify_api, notify_db, notify_db_session, sample_email_template): + with notify_api.test_request_context(): + with notify_api.test_client() as client: + notification_1 = sample_notification(notify_db, notify_db_session, sample_email_template.service) + notification_2 = sample_notification(notify_db, notify_db_session, sample_email_template.service) + notification_3 = sample_notification(notify_db, notify_db_session, sample_email_template.service) + + auth_header = create_authorization_header( + service_id=sample_email_template.service_id, + path='/notifications', + method='GET') + + response = client.get( + '/notifications', + headers=[auth_header]) + + notifications = json.loads(response.get_data(as_text=True)) + assert len(notifications['notifications']) == 3 + assert notifications['notifications'][0]['to'] == notification_3.to + assert notifications['notifications'][1]['to'] == notification_2.to + assert notifications['notifications'][2]['to'] == notification_1.to + assert response.status_code == 200 + + +def test_get_all_notifications_for_service_in_order(notify_api, notify_db, notify_db_session): + with notify_api.test_request_context(): + with notify_api.test_client() as client: + + service_1 = sample_service(notify_db, notify_db_session, service_name="1") + service_2 = sample_service(notify_db, notify_db_session, service_name="2") + + sample_notification(notify_db, notify_db_session, service=service_2) + + notification_1 = sample_notification(notify_db, notify_db_session, service=service_1) + notification_2 = sample_notification(notify_db, notify_db_session, service=service_1) + notification_3 = sample_notification(notify_db, notify_db_session, service=service_1) + + auth_header = create_authorization_header( + path='/service/{}/notifications'.format(service_1.id), + method='GET') + + response = client.get( + path='/service/{}/notifications'.format(service_1.id), + headers=[auth_header]) + + resp = json.loads(response.get_data(as_text=True)) + assert len(resp['notifications']) == 3 + assert resp['notifications'][0]['to'] == notification_3.to + assert resp['notifications'][1]['to'] == notification_2.to + assert resp['notifications'][2]['to'] == notification_1.to + assert response.status_code == 200 + + +def test_get_all_notifications_for_job_in_order(notify_api, notify_db, notify_db_session, sample_service): + with notify_api.test_request_context(): + with notify_api.test_client() as client: + main_job = sample_job(notify_db, notify_db_session, service=sample_service) + another_job = sample_job(notify_db, notify_db_session, service=sample_service) + + notification_1 = sample_notification(notify_db, notify_db_session, job=main_job) + notification_2 = sample_notification(notify_db, notify_db_session, job=main_job) + notification_3 = sample_notification(notify_db, notify_db_session, job=main_job) + sample_notification(notify_db, notify_db_session, job=another_job) + + auth_header = create_authorization_header( + path='/service/{}/job/{}/notifications'.format(sample_service.id, main_job.id), + method='GET') + + response = client.get( + path='/service/{}/job/{}/notifications'.format(sample_service.id, main_job.id), + headers=[auth_header]) + + resp = json.loads(response.get_data(as_text=True)) + assert len(resp['notifications']) == 3 + assert resp['notifications'][0]['to'] == notification_3.to + assert resp['notifications'][1]['to'] == notification_2.to + assert resp['notifications'][2]['to'] == notification_1.to + assert response.status_code == 200 + + +def test_should_not_get_notifications_by_service_with_client_credentials(notify_api, sample_api_key): + with notify_api.test_request_context(): + with notify_api.test_client() as client: + auth_header = create_authorization_header( + service_id=sample_api_key.service.id, + path='/service/{}/notifications'.format(sample_api_key.service.id), + method='GET') + + response = client.get( + '/service/{}/notifications'.format(sample_api_key.service.id), + headers=[auth_header]) + + resp = json.loads(response.get_data(as_text=True)) + assert response.status_code == 403 + assert resp['result'] == 'error' + assert resp['message'] == 'Forbidden, invalid authentication token provided' + + +def test_should_not_get_notifications_by_job_and_service_with_client_credentials(notify_api, sample_job): + with notify_api.test_request_context(): + with notify_api.test_client() as client: + auth_header = create_authorization_header( + service_id=sample_job.service.id, + path='/service/{}/job/{}/notifications'.format(sample_job.service.id, sample_job.id), + method='GET') + + response = client.get( + '/service/{}/job/{}/notifications'.format(sample_job.service.id, sample_job.id), + headers=[auth_header]) + + resp = json.loads(response.get_data(as_text=True)) + assert response.status_code == 403 + assert resp['result'] == 'error' + assert resp['message'] == 'Forbidden, invalid authentication token provided' + + +def test_should_reject_invalid_page_param(notify_api, sample_email_template): + with notify_api.test_request_context(): + with notify_api.test_client() as client: + auth_header = create_authorization_header( + service_id=sample_email_template.service_id, + path='/notifications', + method='GET') + + response = client.get( + '/notifications?page=invalid', + headers=[auth_header]) + + notifications = json.loads(response.get_data(as_text=True)) + assert response.status_code == 400 + assert notifications['result'] == 'error' + assert notifications['message'] == 'Invalid page' + + +def test_should_return_pagination_links(notify_api, notify_db, notify_db_session, sample_email_template): + with notify_api.test_request_context(): + with notify_api.test_client() as client: + notify_api.config['PAGE_SIZE'] = 1 + + sample_notification(notify_db, notify_db_session, sample_email_template.service) + notification_2 = sample_notification(notify_db, notify_db_session, sample_email_template.service) + sample_notification(notify_db, notify_db_session, sample_email_template.service) + + auth_header = create_authorization_header( + service_id=sample_email_template.service_id, + path='/notifications', + method='GET') + + response = client.get( + '/notifications?page=2', + headers=[auth_header]) + + notifications = json.loads(response.get_data(as_text=True)) + assert len(notifications['notifications']) == 1 + assert notifications['links']['last'] == '/notifications?page=3' + assert notifications['links']['prev'] == '/notifications?page=1' + assert notifications['links']['next'] == '/notifications?page=3' + assert notifications['notifications'][0]['to'] == notification_2.to + assert response.status_code == 200 + + +def test_get_all_notifications_returns_empty_list(notify_api, sample_api_key): + with notify_api.test_request_context(): + with notify_api.test_client() as client: + auth_header = create_authorization_header( + service_id=sample_api_key.service.id, + path='/notifications', + method='GET') + + response = client.get( + '/notifications', + headers=[auth_header]) + + notifications = json.loads(response.get_data(as_text=True)) + assert response.status_code == 200 + assert len(notifications['notifications']) == 0 + + def test_create_sms_should_reject_if_missing_required_fields(notify_api, sample_api_key, mocker): with notify_api.test_request_context(): with notify_api.test_client() as client: