From 85b8e24e17d8f71029d191547648dc1df69f4c67 Mon Sep 17 00:00:00 2001 From: Ken Tsang Date: Fri, 3 Nov 2017 16:35:22 +0000 Subject: [PATCH] Add V2 inbound_sms API - had to update the serialization in the model so that the date time is appended with the UTC timezone - test has been added to ensure that the schema will validate the response correctly --- app/__init__.py | 4 + app/models.py | 4 +- app/v2/inbound_sms/__init__.py | 6 + app/v2/inbound_sms/get_inbound_sms.py | 36 +++++ app/v2/inbound_sms/inbound_sms_schemas.py | 51 +++++++ tests/app/conftest.py | 6 + tests/app/db.py | 6 +- tests/app/v2/inbound_sms/__init__.py | 0 .../v2/inbound_sms/test_get_inbound_sms.py | 131 ++++++++++++++++++ .../inbound_sms/test_inbound_sms_schemas.py | 75 ++++++++++ 10 files changed, 315 insertions(+), 4 deletions(-) create mode 100644 app/v2/inbound_sms/__init__.py create mode 100644 app/v2/inbound_sms/get_inbound_sms.py create mode 100644 app/v2/inbound_sms/inbound_sms_schemas.py create mode 100644 tests/app/v2/inbound_sms/__init__.py create mode 100644 tests/app/v2/inbound_sms/test_get_inbound_sms.py create mode 100644 tests/app/v2/inbound_sms/test_inbound_sms_schemas.py diff --git a/app/__init__.py b/app/__init__.py index 31a601f3c..b93abfe6e 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -174,6 +174,7 @@ def register_blueprint(application): def register_v2_blueprints(application): + from app.v2.inbound_sms.get_inbound_sms import v2_inbound_sms_blueprint as get_inbound_sms from app.v2.notifications.post_notifications import v2_notification_blueprint as post_notifications from app.v2.notifications.get_notifications import v2_notification_blueprint as get_notifications from app.v2.template.get_template import v2_template_blueprint as get_template @@ -196,6 +197,9 @@ def register_v2_blueprints(application): post_template.before_request(requires_auth) application.register_blueprint(post_template) + get_inbound_sms.before_request(requires_auth) + application.register_blueprint(get_inbound_sms) + def init_app(app): @app.before_request diff --git a/app/models.py b/app/models.py index 74b0af79a..ea7b93a4e 100644 --- a/app/models.py +++ b/app/models.py @@ -1405,12 +1405,12 @@ class InboundSms(db.Model): def serialize(self): return { 'id': str(self.id), - 'created_at': self.created_at.isoformat(), + 'created_at': self.created_at.strftime(DATETIME_FORMAT), 'service_id': str(self.service_id), 'notify_number': self.notify_number, 'user_number': self.user_number, 'content': self.content, - 'provider_date': self.provider_date and self.provider_date.isoformat(), + 'provider_date': self.provider_date and self.provider_date.strftime(DATETIME_FORMAT), 'provider_reference': self.provider_reference } diff --git a/app/v2/inbound_sms/__init__.py b/app/v2/inbound_sms/__init__.py new file mode 100644 index 000000000..d48a37f94 --- /dev/null +++ b/app/v2/inbound_sms/__init__.py @@ -0,0 +1,6 @@ +from flask import Blueprint +from app.v2.errors import register_errors + +v2_inbound_sms_blueprint = Blueprint("v2_inbound_sms", __name__, url_prefix='/v2/inbound_sms') + +register_errors(v2_inbound_sms_blueprint) diff --git a/app/v2/inbound_sms/get_inbound_sms.py b/app/v2/inbound_sms/get_inbound_sms.py new file mode 100644 index 000000000..10ae26885 --- /dev/null +++ b/app/v2/inbound_sms/get_inbound_sms.py @@ -0,0 +1,36 @@ +import uuid + +from flask import jsonify, request, url_for, current_app +from sqlalchemy.orm.exc import NoResultFound +from werkzeug.exceptions import abort + +from notifications_utils.recipients import validate_and_format_phone_number +from notifications_utils.recipients import InvalidPhoneError + +from app import authenticated_service +from app.dao import inbound_sms_dao +from app.v2.errors import BadRequestError +from app.v2.inbound_sms import v2_inbound_sms_blueprint + + +@v2_inbound_sms_blueprint.route("/", methods=['GET']) +def get_inbound_sms_by_number(user_number): + try: + validate_and_format_phone_number(user_number) + except InvalidPhoneError as e: + raise BadRequestError(message=str(e)) + + inbound_sms = inbound_sms_dao.dao_get_inbound_sms_for_service( + authenticated_service.id, user_number=user_number + ) + + return jsonify(inbound_sms_list=[i.serialize() for i in inbound_sms]), 200 + + +@v2_inbound_sms_blueprint.route("", methods=['GET']) +def get_all_inbound_sms(): + all_inbound_sms = inbound_sms_dao.dao_get_inbound_sms_for_service( + authenticated_service.id + ) + + return jsonify(inbound_sms_list=[i.serialize() for i in all_inbound_sms]), 200 diff --git a/app/v2/inbound_sms/inbound_sms_schemas.py b/app/v2/inbound_sms/inbound_sms_schemas.py new file mode 100644 index 000000000..8dedda195 --- /dev/null +++ b/app/v2/inbound_sms/inbound_sms_schemas.py @@ -0,0 +1,51 @@ +from app.schema_validation.definitions import uuid + + +get_inbound_sms_single_response = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "GET inbound sms schema response", + "type": "object", + "title": "GET response v2/inbound_sms", + "properties": { + "provider_date": { + "format": "date-time", + "type": "string", + "description": "Date+time sent by provider" + }, + "provider_reference": {"type": ["string", "null"]}, + "user_number": {"type": "string"}, + "created_at": { + "format": "date-time", + "type": "string", + "description": "Date+time created at" + }, + "service_id": uuid, + "id": uuid, + "notify_number": {"type": "string"}, + "content": {"type": "string"}, + }, + "required": [ + "id", "provider_date", "provider_reference", + "user_number", "created_at", "service_id", + "notify_number", "content" + ], +} + +get_inbound_sms_response = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "GET list of inbound sms response schema", + "type": "object", + "properties": { + "inbound_sms_list": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/inbound_sms" + } + }, + }, + "required": ["inbound_sms_list"], + "definitions": { + "inbound_sms": get_inbound_sms_single_response + } +} diff --git a/tests/app/conftest.py b/tests/app/conftest.py index e03b2b7dd..081cdf455 100644 --- a/tests/app/conftest.py +++ b/tests/app/conftest.py @@ -47,6 +47,7 @@ from tests.app.db import ( create_api_key, create_inbound_number, create_letter_contact, + create_inbound_sms, ) @@ -1014,6 +1015,11 @@ def sample_inbound_numbers(notify_db, notify_db_session, sample_service): return inbound_numbers +@pytest.fixture +def sample_inbound_sms(notify_db, notify_db_session, sample_service): + return create_inbound_sms(sample_service) + + @pytest.fixture def restore_provider_details(notify_db, notify_db_session): """ diff --git a/tests/app/db.py b/tests/app/db.py index c7c950544..cb7729244 100644 --- a/tests/app/db.py +++ b/tests/app/db.py @@ -1,6 +1,8 @@ from datetime import datetime +import pytz import uuid +from app import DATETIME_FORMAT from app import db from app.dao.jobs_dao import dao_create_job from app.dao.service_inbound_api_dao import save_service_inbound_api @@ -247,10 +249,10 @@ def create_inbound_sms( ): inbound = InboundSms( service=service, - created_at=created_at or datetime.utcnow(), + created_at=created_at or datetime.utcnow().strftime(DATETIME_FORMAT), notify_number=notify_number or service.sms_sender, user_number=user_number, - provider_date=provider_date or datetime.utcnow(), + provider_date=provider_date or datetime.utcnow().strftime(DATETIME_FORMAT), provider_reference=provider_reference or 'foo', content=content, provider=provider diff --git a/tests/app/v2/inbound_sms/__init__.py b/tests/app/v2/inbound_sms/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/app/v2/inbound_sms/test_get_inbound_sms.py b/tests/app/v2/inbound_sms/test_get_inbound_sms.py new file mode 100644 index 000000000..028692414 --- /dev/null +++ b/tests/app/v2/inbound_sms/test_get_inbound_sms.py @@ -0,0 +1,131 @@ +import datetime +import pytest +from flask import json, url_for + +from app import DATETIME_FORMAT +from tests import create_authorization_header +from tests.app.db import create_inbound_sms + + +def test_get_all_inbound_sms_returns_200( + client, sample_service +): + all_inbound_sms = [ + create_inbound_sms(service=sample_service, user_number='447700900111', content='Hi'), + create_inbound_sms(service=sample_service, user_number='447700900112'), + create_inbound_sms(service=sample_service, user_number='447700900111', content='Bye'), + create_inbound_sms(service=sample_service, user_number='447700900113') + ] + + auth_header = create_authorization_header(service_id=sample_service.id) + response = client.get( + path='/v2/inbound_sms', + headers=[('Content-Type', 'application/json'), auth_header]) + + assert response.status_code == 200 + assert response.headers['Content-type'] == 'application/json' + + json_response = json.loads(response.get_data(as_text=True))['inbound_sms_list'] + + reversed_all_inbound_sms = sorted(all_inbound_sms, key=lambda sms: sms.created_at, reverse=True) + + expected_response = [i.serialize() for i in reversed_all_inbound_sms] + + assert json_response == expected_response + + +def test_get_all_inbound_sms_for_no_inbound_sms_returns_200( + client, sample_service +): + auth_header = create_authorization_header(service_id=sample_service.id) + response = client.get( + path='/v2/inbound_sms', + headers=[('Content-Type', 'application/json'), auth_header]) + + assert response.status_code == 200 + assert response.headers['Content-type'] == 'application/json' + + json_response = json.loads(response.get_data(as_text=True))['inbound_sms_list'] + + expected_response = [] + + assert json_response == expected_response + + +def test_get_inbound_sms_by_number_returns_200( + client, sample_service +): + sample_inbound_sms = create_inbound_sms(service=sample_service) + auth_header = create_authorization_header(service_id=sample_service.id) + response = client.get( + path='/v2/inbound_sms/{}'.format(sample_inbound_sms.user_number), + headers=[('Content-Type', 'application/json'), auth_header]) + + assert response.status_code == 200 + assert response.headers['Content-type'] == 'application/json' + + json_response = json.loads(response.get_data(as_text=True))['inbound_sms_list'] + + expected_response = [sample_inbound_sms.serialize()] + + assert json_response == expected_response + + +def test_get_inbound_sms_for_no_inbound_sms_returns_200( + client, sample_service +): + auth_header = create_authorization_header(service_id=sample_service.id) + response = client.get( + path='/v2/inbound_sms', + headers=[('Content-Type', 'application/json'), auth_header]) + + assert response.status_code == 200 + assert response.headers['Content-type'] == 'application/json' + + json_response = json.loads(response.get_data(as_text=True))['inbound_sms_list'] + + expected_response = [] + + assert json_response == expected_response + + +def test_get_inbound_sms_by_nonexistent_number(client, sample_service): + auth_header = create_authorization_header(service_id=sample_service.id) + response = client.get( + path='/v2/inbound_sms/447700900000', + headers=[('Content-Type', 'application/json'), auth_header]) + + assert response.status_code == 200 + assert response.headers['Content-type'] == 'application/json' + + json_response = json.loads(response.get_data(as_text=True))['inbound_sms_list'] + expected_response = [] + + assert json_response == expected_response + + +@pytest.mark.parametrize('invalid_number,expected_message', [ + ('0077700', 'Not enough digits'), + ('123456789012', 'Not a UK mobile number'), + ('invalid_number', 'Must not contain letters or symbols') +]) +def test_get_inbound_sms_by_invalid_number( + client, sample_service, invalid_number, expected_message): + auth_header = create_authorization_header(service_id=sample_service.id) + response = client.get( + path='/v2/inbound_sms/{}'.format(invalid_number), + headers=[('Content-Type', 'application/json'), auth_header]) + + assert response.status_code == 400 + assert response.headers['Content-type'] == 'application/json' + + json_response = json.loads(response.get_data(as_text=True)) + assert json_response == { + "errors": [ + { + "error": "BadRequestError", + "message": expected_message + } + ], + "status_code": 400 + } diff --git a/tests/app/v2/inbound_sms/test_inbound_sms_schemas.py b/tests/app/v2/inbound_sms/test_inbound_sms_schemas.py new file mode 100644 index 000000000..04e61a321 --- /dev/null +++ b/tests/app/v2/inbound_sms/test_inbound_sms_schemas.py @@ -0,0 +1,75 @@ +import uuid + +import pytest +from flask import json +from jsonschema.exceptions import ValidationError + +from app.dao.api_key_dao import save_model_api_key +from app.models import ApiKey, KEY_TYPE_NORMAL, EMAIL_TYPE, SMS_TYPE, TEMPLATE_TYPES +from app.v2.inbound_sms.inbound_sms_schemas import get_inbound_sms_response, get_inbound_sms_single_response +from app.schema_validation import validate + +from tests import create_authorization_header + + +valid_inbound_sms = { + "provider_date": "2017-11-02T15:07:57.199541Z", + "provider_reference": "foo", + "user_number": "447700900111", + "created_at": "2017-11-02T15:07:57.197546Z", + "service_id": "a5149c32-f03b-4711-af49-ad6993797d45", + "id": "342786aa-23ce-4695-9aad-7f79e68ee29a", + "notify_number": "testing", + "content": "Hello" +} + +valid_inbound_sms_list = { + "inbound_sms_list": [valid_inbound_sms] +} + +invalid_inbound_sms = { + "provider_date": "2017-11-02T15:07:57.199541", + "provider_reference": "foo", + "user_number": "447700900111", + "created_at": "2017-11-02T15:07:57.197546", + "service_id": "a5149c32-f03b-4711-af49-ad6993797d45", + "id": "342786aa-23ce-4695-9aad-7f79e68ee29a", + "notify_number": "testing" +} + +invalid_inbound_sms_list = { + "inbound_sms_list": [invalid_inbound_sms] +} + + +def _get_inbound_sms(client, inbound_sms, url): + auth_header = create_authorization_header(service_id=inbound_sms.service_id) + response = client.get(url, headers=[auth_header]) + return json.loads(response.get_data(as_text=True)) + + +def test_get_inbound_sms_contract(client, sample_inbound_sms): + response_json = _get_inbound_sms( + client, + sample_inbound_sms, + '/v2/inbound_sms/{}'.format(sample_inbound_sms.user_number) + ) + res = validate(response_json, get_inbound_sms_response) + + +def test_valid_inbound_sms_json(): + validate(valid_inbound_sms, get_inbound_sms_single_response) + + +def test_valid_inbound_sms_list_json(): + validate(valid_inbound_sms_list, get_inbound_sms_response) + + +def test_invalid_inbound_sms_json(): + with pytest.raises(expected_exception=ValidationError): + validate(invalid_inbound_sms, get_inbound_sms_single_response) + + +def test_invalid_inbound_sms_list_json(): + with pytest.raises(expected_exception=ValidationError): + validate(invalid_inbound_sms_list, get_inbound_sms_response)