From d275ba83a23ff0ef89162138f238b9b930e93918 Mon Sep 17 00:00:00 2001 From: Martyn Inglis Date: Tue, 19 Jan 2016 11:23:09 +0000 Subject: [PATCH] Added endpoints for the proxy to notifications. - this uses alpha API for delivery - no DB model included as just proving - all notifications for same service at the moment (!) --- .gitignore | 2 + app/__init__.py | 5 + app/notifications/__init__.py | 0 app/notifications/rest.py | 73 +++++++ config.py | 5 + requirements.txt | 2 + tests/app/notifications/__init__.py | 0 tests/app/notifications/test_rest.py | 298 +++++++++++++++++++++++++++ 8 files changed, 385 insertions(+) create mode 100644 app/notifications/__init__.py create mode 100644 app/notifications/rest.py create mode 100644 tests/app/notifications/__init__.py create mode 100644 tests/app/notifications/test_rest.py diff --git a/.gitignore b/.gitignore index b2f1a66a4..dc9c76d20 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,8 @@ __pycache__/ *.py[cod] +venv/ + # C extensions *.so diff --git a/app/__init__.py b/app/__init__.py index 43e7fa659..54319a28a 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -7,10 +7,12 @@ from flask_marshmallow import Marshmallow from werkzeug.local import LocalProxy from config import configs from utils import logging +from notify_client import NotifyAPIClient db = SQLAlchemy() ma = Marshmallow() +notify_alpha_client = NotifyAPIClient() api_user = LocalProxy(lambda: _request_ctx_stack.top.api_user) @@ -25,17 +27,20 @@ def create_app(config_name): ma.init_app(application) init_app(application) logging.init_app(application) + notify_alpha_client.init_app(application) from app.service.rest import service as service_blueprint from app.user.rest import user as user_blueprint from app.template.rest import template as template_blueprint from app.status.healthcheck import status as status_blueprint from app.job.rest import job as job_blueprint + from app.notifications.rest import notifications as notifications_blueprint application.register_blueprint(service_blueprint, url_prefix='/service') application.register_blueprint(user_blueprint, url_prefix='/user') application.register_blueprint(template_blueprint, url_prefix="/template") application.register_blueprint(status_blueprint, url_prefix='/status') + application.register_blueprint(notifications_blueprint, url_prefix='/notifications') application.register_blueprint(job_blueprint) return application diff --git a/app/notifications/__init__.py b/app/notifications/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/app/notifications/rest.py b/app/notifications/rest.py new file mode 100644 index 000000000..625ce16a5 --- /dev/null +++ b/app/notifications/rest.py @@ -0,0 +1,73 @@ +from flask import ( + Blueprint, + jsonify, + request +) + +from app import notify_alpha_client +import re + +mobile_regex = re.compile("^\\+44[\\d]{10}$") + +notifications = Blueprint('notifications', __name__) + + +@notifications.route('/', methods=['GET']) +def get_notifications(): + return jsonify(notify_alpha_client.fetch_notifications()), 200 + + +@notifications.route('/sms', methods=['POST']) +def create_sms_notification(): + notification = request.get_json()['notification'] + + errors = {} + to_errors = validate_to(notification) + message_errors = validate_message(notification) + + if to_errors: + errors.update(to_errors) + if message_errors: + errors.update(message_errors) + + if errors: + return jsonify(result="error", message=errors), 400 + + return jsonify(notify_alpha_client.send_sms(mobile_number=notification['to'], message=notification['message'])), 200 + + +@notifications.route('/email', methods=['POST']) +def create_email_notification(): + return jsonify(id=123) + + +def validate_to(json_body): + errors = [] + + if 'to' not in json_body: + errors.append('required') + else: + if not mobile_regex.match(json_body['to']): + errors.append('invalid phone number, must be of format +441234123123') + if errors: + return { + "to": errors + } + return None + + +def validate_message(json_body): + errors = [] + + if 'message' not in json_body: + errors.append('required') + else: + message_length = len(json_body['message']) + if message_length < 1 or message_length > 160: + errors.append('Invalid length. [1 - 160]') + + if errors: + return { + "message": errors + } + return None diff --git a/config.py b/config.py index 256f55c8f..ddc78e66d 100644 --- a/config.py +++ b/config.py @@ -1,3 +1,6 @@ +import os + + class Config(object): DEBUG = False NOTIFY_LOG_LEVEL = 'DEBUG' @@ -6,6 +9,8 @@ class Config(object): SQLALCHEMY_COMMIT_ON_TEARDOWN = False SQLALCHEMY_RECORD_QUERIES = True SQLALCHEMY_DATABASE_URI = 'postgresql://localhost/notification_api' + NOTIFY_DATA_API_URL = os.getenv('NOTIFY_API_URL', "http://localhost:6001") + NOTIFY_DATA_API_AUTH_TOKEN = os.getenv('NOTIFY_API_TOKEN', "dev-token") class Development(Config): diff --git a/requirements.txt b/requirements.txt index 5112e5b2f..07638fc4a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,3 +14,5 @@ itsdangerous==0.24 git+https://github.com/alphagov/notifications-python-client.git@0.1.5#egg=notifications-python-client==0.1.5 git+https://github.com/alphagov/notifications-utils.git@0.0.3#egg=notifications-utils==0.0.3 + +git+https://github.com/alphagov/notify-api-client.git@0.1.5#egg=notify-api-client==0.1.5 diff --git a/tests/app/notifications/__init__.py b/tests/app/notifications/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/app/notifications/test_rest.py b/tests/app/notifications/test_rest.py new file mode 100644 index 000000000..712e5f457 --- /dev/null +++ b/tests/app/notifications/test_rest.py @@ -0,0 +1,298 @@ +from tests import create_authorization_header +from flask import url_for, json +from app import notify_alpha_client + + +def test_get_notifications( + notify_api, notify_db, notify_db_session, sample_service, sample_admin_service_id, mocker): + """ + Tests GET endpoint '/' to retrieve entire service list. + """ + with notify_api.test_request_context(): + with notify_api.test_client() as client: + mocker.patch( + 'app.notify_alpha_client.fetch_notifications', + return_value={ + 'notifications': [ + { + 'id': 'my_id', + 'notification': 'some notify' + } + ] + } + ) + + auth_header = create_authorization_header( + service_id=sample_admin_service_id, + path=url_for('notifications.get_notifications'), + method='GET') + + response = client.get( + url_for('notifications.get_notifications'), + headers=[auth_header]) + + json_resp = json.loads(response.get_data(as_text=True)) + assert response.status_code == 200 + assert len(json_resp['notifications']) == 1 + assert json_resp['notifications'][0]['id'] == 'my_id' + assert json_resp['notifications'][0]['notification'] == 'some notify' + assert notify_alpha_client.fetch_notifications.called + + +def test_get_notifications_empty_result( + notify_api, notify_db, notify_db_session, sample_service, sample_admin_service_id, mocker): + """ + Tests GET endpoint '/' to retrieve entire service list. + """ + with notify_api.test_request_context(): + with notify_api.test_client() as client: + mocker.patch( + 'app.notify_alpha_client.fetch_notifications', + return_value={ + 'notifications': [ + ] + } + ) + + auth_header = create_authorization_header( + service_id=sample_admin_service_id, + path=url_for('notifications.get_notifications'), + method='GET') + + response = client.get( + url_for('notifications.get_notifications'), + headers=[auth_header]) + + json_resp = json.loads(response.get_data(as_text=True)) + assert response.status_code == 200 + assert len(json_resp['notifications']) == 0 + assert notify_alpha_client.fetch_notifications.called + + +def test_should_reject_if_no_phone_numbers( + notify_api, notify_db, notify_db_session, sample_service, sample_admin_service_id, mocker): + """ + Tests GET endpoint '/' to retrieve entire service list. + """ + with notify_api.test_request_context(): + with notify_api.test_client() as client: + mocker.patch( + 'app.notify_alpha_client.send_sms', + return_value='success' + ) + data = { + 'notification': { + 'message': "my message" + } + } + auth_header = create_authorization_header( + service_id=sample_admin_service_id, + request_body=json.dumps(data), + path=url_for('notifications.create_sms_notification'), + method='POST') + + response = client.post( + url_for('notifications.create_sms_notification'), + data=json.dumps(data), + headers=[('Content-Type', 'application/json'), auth_header]) + + json_resp = json.loads(response.get_data(as_text=True)) + print(json_resp) + assert response.status_code == 400 + assert json_resp['result'] == 'error' + assert len(json_resp['message']) == 1 + assert len(json_resp['message']['to']) == 1 + assert json_resp['message']['to'][0] == 'required' + assert not notify_alpha_client.send_sms.called + + +def test_should_reject_bad_phone_numbers( + notify_api, notify_db, notify_db_session, sample_service, sample_admin_service_id, mocker): + """ + Tests GET endpoint '/' to retrieve entire service list. + """ + with notify_api.test_request_context(): + with notify_api.test_client() as client: + mocker.patch( + 'app.notify_alpha_client.send_sms', + return_value='success' + ) + data = { + 'notification': { + 'to': 'invalid', + 'message': "my message" + } + } + auth_header = create_authorization_header( + service_id=sample_admin_service_id, + request_body=json.dumps(data), + path=url_for('notifications.create_sms_notification'), + method='POST') + + response = client.post( + url_for('notifications.create_sms_notification'), + data=json.dumps(data), + headers=[('Content-Type', 'application/json'), auth_header]) + + json_resp = json.loads(response.get_data(as_text=True)) + print(json_resp) + assert response.status_code == 400 + assert json_resp['result'] == 'error' + assert len(json_resp['message']) == 1 + assert len(json_resp['message']['to']) == 1 + assert json_resp['message']['to'][0] == 'invalid phone number, must be of format +441234123123' + assert not notify_alpha_client.send_sms.called + + +def test_should_reject_missing_message( + notify_api, notify_db, notify_db_session, sample_service, sample_admin_service_id, mocker): + """ + Tests GET endpoint '/' to retrieve entire service list. + """ + with notify_api.test_request_context(): + with notify_api.test_client() as client: + mocker.patch( + 'app.notify_alpha_client.send_sms', + return_value='success' + ) + data = { + 'notification': { + 'to': '+441234123123' + } + } + auth_header = create_authorization_header( + service_id=sample_admin_service_id, + request_body=json.dumps(data), + path=url_for('notifications.create_sms_notification'), + method='POST') + + response = client.post( + url_for('notifications.create_sms_notification'), + data=json.dumps(data), + headers=[('Content-Type', 'application/json'), auth_header]) + + json_resp = json.loads(response.get_data(as_text=True)) + assert response.status_code == 400 + assert json_resp['result'] == 'error' + assert len(json_resp['message']) == 1 + assert len(json_resp['message']['message']) == 1 + assert json_resp['message']['message'][0] == 'required' + assert not notify_alpha_client.send_sms.called + + +def test_should_reject_too_short_message( + notify_api, notify_db, notify_db_session, sample_service, sample_admin_service_id, mocker): + """ + Tests GET endpoint '/' to retrieve entire service list. + """ + with notify_api.test_request_context(): + with notify_api.test_client() as client: + mocker.patch( + 'app.notify_alpha_client.send_sms', + return_value='success' + ) + data = { + 'notification': { + 'to': '+441234123123', + 'message': '' + } + } + auth_header = create_authorization_header( + service_id=sample_admin_service_id, + request_body=json.dumps(data), + path=url_for('notifications.create_sms_notification'), + method='POST') + + response = client.post( + url_for('notifications.create_sms_notification'), + data=json.dumps(data), + headers=[('Content-Type', 'application/json'), auth_header]) + + json_resp = json.loads(response.get_data(as_text=True)) + assert response.status_code == 400 + assert json_resp['result'] == 'error' + assert len(json_resp['message']) == 1 + assert len(json_resp['message']['message']) == 1 + assert json_resp['message']['message'][0] == 'Invalid length. [1 - 160]' + assert not notify_alpha_client.send_sms.called + + +def test_should_reject_too_long_message( + notify_api, notify_db, notify_db_session, sample_service, sample_admin_service_id, mocker): + """ + Tests GET endpoint '/' to retrieve entire service list. + """ + with notify_api.test_request_context(): + with notify_api.test_client() as client: + mocker.patch( + 'app.notify_alpha_client.send_sms', + return_value='success' + ) + data = { + 'notification': { + 'to': '+441234123123', + 'message': '1' * 161 + } + } + auth_header = create_authorization_header( + service_id=sample_admin_service_id, + request_body=json.dumps(data), + path=url_for('notifications.create_sms_notification'), + method='POST') + + response = client.post( + url_for('notifications.create_sms_notification'), + data=json.dumps(data), + headers=[('Content-Type', 'application/json'), auth_header]) + + json_resp = json.loads(response.get_data(as_text=True)) + assert response.status_code == 400 + assert json_resp['result'] == 'error' + assert len(json_resp['message']) == 1 + assert len(json_resp['message']['message']) == 1 + assert json_resp['message']['message'][0] == 'Invalid length. [1 - 160]' + assert not notify_alpha_client.send_sms.called + + +def test_should_allow_valid_message( + notify_api, notify_db, notify_db_session, sample_service, sample_admin_service_id, mocker): + """ + Tests GET endpoint '/' to retrieve entire service list. + """ + with notify_api.test_request_context(): + with notify_api.test_client() as client: + mocker.patch( + 'app.notify_alpha_client.send_sms', + return_value={ + "notification": { + "createdAt": "2015-11-03T09:37:27.414363Z", + "id": 100, + "jobId": 65, + "message": "This is the message", + "method": "sms", + "status": "created", + "to": "+449999999999" + } + } + ) + data = { + 'notification': { + 'to': '+441234123123', + 'message': 'valid' + } + } + auth_header = create_authorization_header( + service_id=sample_admin_service_id, + request_body=json.dumps(data), + path=url_for('notifications.create_sms_notification'), + method='POST') + + response = client.post( + url_for('notifications.create_sms_notification'), + data=json.dumps(data), + headers=[('Content-Type', 'application/json'), auth_header]) + + json_resp = json.loads(response.get_data(as_text=True)) + assert response.status_code == 200 + assert json_resp['notification']['id'] == 100 + notify_alpha_client.send_sms.assert_called_with(mobile_number='+441234123123', message='valid')