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 75ffacbf9..0f0d30efe 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -8,10 +8,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) @@ -26,17 +28,20 @@ def create_app(config_name, config_overrides=None): ma.init_app(application) init_app(application, config_overrides) 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/authentication/auth.py b/app/authentication/auth.py index ffb602e65..d6c2399b3 100644 --- a/app/authentication/auth.py +++ b/app/authentication/auth.py @@ -24,7 +24,6 @@ def requires_auth(): try: auth_token = auth_header[7:] api_client = fetch_client(get_token_issuer(auth_token)) - if api_client is None: authentication_response("Invalid credentials", 403) 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..48c82857e --- /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(notification_id): + return jsonify(notify_alpha_client.fetch_notification_by_id(notification_id)), 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 6248cfcc0..b26a526a8 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") ADMIN_CLIENT_USER_NAME = None ADMIN_CLIENT_SECRET = None diff --git a/requirements.txt b/requirements.txt index 529d8c18b..6efb52008 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,3 +16,5 @@ credstash==1.8.0 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.6#egg=notify-api-client==0.1.6 diff --git a/scripts/run_tests.sh b/scripts/run_tests.sh index c11e8e29a..775d28623 100755 --- a/scripts/run_tests.sh +++ b/scripts/run_tests.sh @@ -30,5 +30,5 @@ display_result $? 1 "Code style check" #display_result $? 2 "Code coverage" -py.test -v -display_result $? 3 "Unit tests" \ No newline at end of file +py.test -v tests/ +display_result $? 3 "Unit tests" 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..e368cf592 --- /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_notification_by_id', + 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', notification_id=123), + method='GET') + + response = client.get( + url_for('notifications.get_notifications', notification_id=123), + 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' + notify_alpha_client.fetch_notification_by_id.assert_called_with("123") + + +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_notification_by_id', + return_value={ + 'notifications': [ + ] + } + ) + + auth_header = create_authorization_header( + service_id=sample_admin_service_id, + path=url_for('notifications.get_notifications', notification_id=123), + method='GET') + + response = client.get( + url_for('notifications.get_notifications', notification_id=123), + headers=[auth_header]) + + json_resp = json.loads(response.get_data(as_text=True)) + assert response.status_code == 200 + assert len(json_resp['notifications']) == 0 + notify_alpha_client.fetch_notification_by_id.assert_called_with("123") + + +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') diff --git a/tests/app/user/test_rest.py b/tests/app/user/test_rest.py index e6cf0ee3b..38f5dc13e 100644 --- a/tests/app/user/test_rest.py +++ b/tests/app/user/test_rest.py @@ -351,10 +351,10 @@ def test_delete_user_not_exists(notify_api, notify_db, notify_db_session, sample with notify_api.test_client() as client: assert User.query.count() == 2 auth_header = create_authorization_header(service_id=sample_admin_service_id, - path=url_for('user.update_user', user_id='123'), + path=url_for('user.update_user', user_id='99999'), method='DELETE') resp = client.delete( - url_for('user.update_user', user_id="123"), + url_for('user.update_user', user_id="99999"), headers=[('Content-Type', 'application/json'), auth_header]) assert resp.status_code == 404 assert User.query.count() == 2