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/dao/inbound_sms_dao.py b/app/dao/inbound_sms_dao.py index 18060cced..f7d54281e 100644 --- a/app/dao/inbound_sms_dao.py +++ b/app/dao/inbound_sms_dao.py @@ -2,7 +2,8 @@ from datetime import ( timedelta, datetime ) - +from flask import current_app +from sqlalchemy import desc from app import db from app.dao.dao_utils import transactional @@ -31,6 +32,28 @@ def dao_get_inbound_sms_for_service(service_id, limit=None, user_number=None): return q.all() +def dao_get_paginated_inbound_sms_for_service( + service_id, + older_than=None, + page_size=None +): + if page_size is None: + page_size = current_app.config['PAGE_SIZE'] + + filters = [InboundSms.service_id == service_id] + + if older_than: + older_than_created_at = db.session.query( + InboundSms.created_at).filter(InboundSms.id == older_than).as_scalar() + filters.append(InboundSms.created_at < older_than_created_at) + + query = InboundSms.query.filter(*filters) + + return query.order_by(desc(InboundSms.created_at)).paginate( + per_page=page_size + ).items + + def dao_count_inbound_sms_for_service(service_id): return InboundSms.query.filter( InboundSms.service_id == service_id diff --git a/app/models.py b/app/models.py index 0c64a4581..409fe3018 100644 --- a/app/models.py +++ b/app/models.py @@ -1405,13 +1405,11 @@ 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_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..5c713b2ef --- /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/received-text-messages') + +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..478e4f03d --- /dev/null +++ b/app/v2/inbound_sms/get_inbound_sms.py @@ -0,0 +1,44 @@ +from flask import jsonify, request, url_for, current_app + +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.schema_validation import validate +from app.v2.inbound_sms import v2_inbound_sms_blueprint +from app.v2.inbound_sms.inbound_sms_schemas import get_inbound_sms_request + + +@v2_inbound_sms_blueprint.route("", methods=['GET']) +def get_inbound_sms(): + data = validate(request.args.to_dict(), get_inbound_sms_request) + + paginated_inbound_sms = inbound_sms_dao.dao_get_paginated_inbound_sms_for_service( + authenticated_service.id, + older_than=data.get('older_than', None), + page_size=current_app.config.get('API_PAGE_SIZE') + ) + + return jsonify( + received_text_messages=[i.serialize() for i in paginated_inbound_sms], + links=_build_links(paginated_inbound_sms) + ), 200 + + +def _build_links(inbound_sms_list): + _links = { + 'current': url_for( + "v2_inbound_sms.get_inbound_sms", + _external=True, + ), + } + + if inbound_sms_list: + _links['next'] = url_for( + "v2_inbound_sms.get_inbound_sms", + older_than=inbound_sms_list[-1].id, + _external=True, + ) + + return _links 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..e68952a23 --- /dev/null +++ b/app/v2/inbound_sms/inbound_sms_schemas.py @@ -0,0 +1,70 @@ +from app.schema_validation.definitions import uuid + + +get_inbound_sms_request = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "schema for query parameters allowed when getting list of received text messages", + "type": "object", + "properties": { + "older_than": uuid, + }, + "additionalProperties": False, +} + + +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": { + "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", "user_number", "created_at", "service_id", + "notify_number", "content" + ], + "additionalProperties": False, +} + +get_inbound_sms_response = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "GET list of inbound sms response schema", + "type": "object", + "properties": { + "received_text_messages": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/inbound_sms" + } + }, + "links": { + "type": "object", + "properties": { + "current": { + "type": "string" + }, + "next": { + "type": "string" + } + }, + "additionalProperties": False, + "required": ["current"] + } + }, + "required": ["received_text_messages", "links"], + "definitions": { + "inbound_sms": get_inbound_sms_single_response + }, + "additionalProperties": False, +} diff --git a/tests/app/conftest.py b/tests/app/conftest.py index 6e89c662d..43b17490d 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, ) @@ -1032,6 +1033,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/dao/test_inbound_sms_dao.py b/tests/app/dao/test_inbound_sms_dao.py index b26dc913e..7967d8522 100644 --- a/tests/app/dao/test_inbound_sms_dao.py +++ b/tests/app/dao/test_inbound_sms_dao.py @@ -6,7 +6,8 @@ from app.dao.inbound_sms_dao import ( dao_get_inbound_sms_for_service, dao_count_inbound_sms_for_service, delete_inbound_sms_created_more_than_a_week_ago, - dao_get_inbound_sms_by_id + dao_get_inbound_sms_by_id, + dao_get_paginated_inbound_sms_for_service ) from tests.app.db import create_inbound_sms, create_service @@ -89,9 +90,83 @@ def test_should_not_delete_inbound_sms_before_seven_days(sample_service): assert len(InboundSms.query.all()) == 2 -def test_get_inbound_sms_by_id_returns(sample_service): - inbound = create_inbound_sms(sample_service) +def test_get_inbound_sms_by_id_returns(sample_inbound_sms): + inbound_from_db = dao_get_inbound_sms_by_id(sample_inbound_sms.service.id, sample_inbound_sms.id) - inbound_from_db = dao_get_inbound_sms_by_id(sample_service.id, inbound.id) + assert sample_inbound_sms == inbound_from_db - assert inbound == inbound_from_db + +def test_dao_get_paginated_inbound_sms_for_service(sample_inbound_sms): + inbound_from_db = dao_get_paginated_inbound_sms_for_service(sample_inbound_sms.service.id) + + assert sample_inbound_sms == inbound_from_db[0] + + +def test_dao_get_paginated_inbound_sms_for_service_return_only_for_service(sample_inbound_sms): + another_service = create_service(service_name='another service') + another_inbound_sms = create_inbound_sms(another_service) + + inbound_from_db = dao_get_paginated_inbound_sms_for_service(sample_inbound_sms.service.id) + + assert sample_inbound_sms in inbound_from_db + assert another_inbound_sms not in inbound_from_db + + +def test_dao_get_paginated_inbound_sms_for_service_no_inbound_sms_returns_empty_list(sample_service): + inbound_from_db = dao_get_paginated_inbound_sms_for_service(sample_service.id) + + assert inbound_from_db == [] + + +def test_dao_get_paginated_inbound_sms_for_service_page_size_returns_correct_size(sample_service): + inbound_sms_list = [ + create_inbound_sms(sample_service), + create_inbound_sms(sample_service), + create_inbound_sms(sample_service), + create_inbound_sms(sample_service), + ] + reversed_inbound_sms = sorted(inbound_sms_list, key=lambda sms: sms.created_at, reverse=True) + + inbound_from_db = dao_get_paginated_inbound_sms_for_service( + sample_service.id, + older_than=reversed_inbound_sms[1].id, + page_size=2 + ) + + assert len(inbound_from_db) == 2 + + +def test_dao_get_paginated_inbound_sms_for_service_older_than_returns_correct_list(sample_service): + inbound_sms_list = [ + create_inbound_sms(sample_service), + create_inbound_sms(sample_service), + create_inbound_sms(sample_service), + create_inbound_sms(sample_service), + ] + reversed_inbound_sms = sorted(inbound_sms_list, key=lambda sms: sms.created_at, reverse=True) + + inbound_from_db = dao_get_paginated_inbound_sms_for_service( + sample_service.id, + older_than=reversed_inbound_sms[1].id, + page_size=2 + ) + + expected_inbound_sms = reversed_inbound_sms[2:] + + assert expected_inbound_sms == inbound_from_db + + +def test_dao_get_paginated_inbound_sms_for_service_older_than_end_returns_empty_list(sample_service): + inbound_sms_list = [ + create_inbound_sms(sample_service), + create_inbound_sms(sample_service), + ] + reversed_inbound_sms = sorted(inbound_sms_list, key=lambda sms: sms.created_at, reverse=True) + + inbound_from_db = dao_get_paginated_inbound_sms_for_service( + sample_service.id, + older_than=reversed_inbound_sms[1].id, + page_size=2 + ) + + assert inbound_from_db == [] diff --git a/tests/app/inbound_sms/test_rest.py b/tests/app/inbound_sms/test_rest.py index 9d2c891f2..a693307ef 100644 --- a/tests/app/inbound_sms/test_rest.py +++ b/tests/app/inbound_sms/test_rest.py @@ -33,9 +33,7 @@ def test_get_inbound_sms_with_no_params(client, sample_service): 'service_id', 'notify_number', 'user_number', - 'content', - 'provider_date', - 'provider_reference' + 'content' } @@ -178,9 +176,7 @@ def test_get_inbound_sms(admin_request, sample_service): 'service_id', 'notify_number', 'user_number', - 'content', - 'provider_date', - 'provider_reference' + 'content' } 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..e499089c3 --- /dev/null +++ b/tests/app/v2/inbound_sms/test_get_inbound_sms.py @@ -0,0 +1,154 @@ +from flask import json, url_for + +from tests import create_authorization_header +from tests.app.db import create_inbound_sms + + +def test_get_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='07700900113') + ] + + auth_header = create_authorization_header(service_id=sample_service.id) + response = client.get( + path='/v2/received-text-messages', + 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))['received_text_messages'] + + 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_inbound_sms_generate_page_links(client, sample_service, mocker): + mocker.patch.dict( + "app.v2.inbound_sms.get_inbound_sms.current_app.config", + {"API_PAGE_SIZE": 2} + ) + all_inbound_sms = [ + create_inbound_sms(service=sample_service, user_number='447700900111', content='Hi'), + create_inbound_sms(service=sample_service, user_number='447700900111'), + create_inbound_sms(service=sample_service, user_number='447700900111', content='End'), + ] + + reversed_inbound_sms = sorted(all_inbound_sms, key=lambda sms: sms.created_at, reverse=True) + + auth_header = create_authorization_header(service_id=sample_service.id) + response = client.get( + url_for('v2_inbound_sms.get_inbound_sms'), + headers=[('Content-Type', 'application/json'), auth_header]) + + assert response.status_code == 200 + + json_response = json.loads(response.get_data(as_text=True)) + expected_inbound_sms_list = [i.serialize() for i in reversed_inbound_sms[:2]] + + assert json_response['received_text_messages'] == expected_inbound_sms_list + assert url_for( + 'v2_inbound_sms.get_inbound_sms', + _external=True) == json_response['links']['current'] + assert url_for( + 'v2_inbound_sms.get_inbound_sms', + older_than=reversed_inbound_sms[1].id, + _external=True) == json_response['links']['next'] + + +def test_get_next_inbound_sms_will_get_correct_inbound_sms_list(client, sample_service, mocker): + mocker.patch.dict( + "app.v2.inbound_sms.get_inbound_sms.current_app.config", + {"API_PAGE_SIZE": 2} + ) + all_inbound_sms = [ + create_inbound_sms(service=sample_service, user_number='447700900111', content='1'), + create_inbound_sms(service=sample_service, user_number='447700900111', content='2'), + create_inbound_sms(service=sample_service, user_number='447700900111', content='3'), + create_inbound_sms(service=sample_service, user_number='447700900111', content='4'), + ] + reversed_inbound_sms = sorted(all_inbound_sms, key=lambda sms: sms.created_at, reverse=True) + + auth_header = create_authorization_header(service_id=sample_service.id) + response = client.get( + path=url_for('v2_inbound_sms.get_inbound_sms', older_than=reversed_inbound_sms[1].id), + headers=[('Content-Type', 'application/json'), auth_header]) + + assert response.status_code == 200 + + json_response = json.loads(response.get_data(as_text=True)) + expected_inbound_sms_list = [i.serialize() for i in reversed_inbound_sms[2:]] + + assert json_response['received_text_messages'] == expected_inbound_sms_list + assert url_for( + 'v2_inbound_sms.get_inbound_sms', + _external=True) == json_response['links']['current'] + assert url_for( + 'v2_inbound_sms.get_inbound_sms', + older_than=reversed_inbound_sms[3].id, + _external=True) == json_response['links']['next'] + + +def test_get_next_inbound_sms_at_end_will_return_empty_inbound_sms_list(client, sample_inbound_sms, mocker): + mocker.patch.dict( + "app.v2.inbound_sms.get_inbound_sms.current_app.config", + {"API_PAGE_SIZE": 1} + ) + + auth_header = create_authorization_header(service_id=sample_inbound_sms.service.id) + response = client.get( + path=url_for('v2_inbound_sms.get_inbound_sms', older_than=sample_inbound_sms.id), + headers=[('Content-Type', 'application/json'), auth_header]) + + assert response.status_code == 200 + + json_response = json.loads(response.get_data(as_text=True)) + expected_inbound_sms_list = [] + assert json_response['received_text_messages'] == expected_inbound_sms_list + assert url_for( + 'v2_inbound_sms.get_inbound_sms', + _external=True) == json_response['links']['current'] + assert 'next' not in json_response['links'].keys() + + +def test_get_inbound_sms_for_no_inbound_sms_returns_empty_list( + client, sample_service +): + auth_header = create_authorization_header(service_id=sample_service.id) + response = client.get( + path='/v2/received-text-messages', + 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))['received_text_messages'] + + expected_response = [] + + assert json_response == expected_response + + +def test_get_inbound_sms_with_invalid_query_string_returns_400(client, sample_service): + auth_header = create_authorization_header(service_id=sample_service.id) + response = client.get( + path='/v2/received-text-messages?user_number=447700900000', + 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['status_code'] == 400 + assert json_response['errors'][0]['error'] == 'ValidationError' + assert json_response['errors'][0]['message'] == \ + 'Additional properties are not allowed (user_number was unexpected)' 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..c2e32f93a --- /dev/null +++ b/tests/app/v2/inbound_sms/test_inbound_sms_schemas.py @@ -0,0 +1,92 @@ +import pytest +from flask import json, url_for +from jsonschema.exceptions import ValidationError + +from app.v2.inbound_sms.inbound_sms_schemas import ( + get_inbound_sms_request, + get_inbound_sms_response, + get_inbound_sms_single_response +) +from app.schema_validation import validate + +from tests import create_authorization_header +from tests.app.db import create_inbound_sms + +valid_inbound_sms = { + "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 = { + "received_text_messages": [valid_inbound_sms], + "links": { + "current": valid_inbound_sms["id"] + } + +} + +invalid_inbound_sms = { + "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 = { + "received_text_messages": [invalid_inbound_sms] +} + + +def test_get_inbound_sms_contract(client, sample_service): + all_inbound_sms = [ + create_inbound_sms(service=sample_service, user_number='447700900113'), + create_inbound_sms(service=sample_service, user_number='447700900112'), + create_inbound_sms(service=sample_service, user_number='447700900111'), + ] + reversed_inbound_sms = sorted(all_inbound_sms, key=lambda sms: sms.created_at, reverse=True) + + auth_header = create_authorization_header(service_id=all_inbound_sms[0].service_id) + response = client.get('/v2/received-text-messages', headers=[auth_header]) + response_json = json.loads(response.get_data(as_text=True)) + + validated_resp = validate(response_json, get_inbound_sms_response) + assert validated_resp['received_text_messages'] == [i.serialize() for i in reversed_inbound_sms] + assert validated_resp['links']['current'] == url_for( + 'v2_inbound_sms.get_inbound_sms', _external=True) + assert validated_resp['links']['next'] == url_for( + 'v2_inbound_sms.get_inbound_sms', older_than=all_inbound_sms[0].id, _external=True) + + +@pytest.mark.parametrize('request_args', [ + {'older_than': "6ce466d0-fd6a-11e5-82f5-e0accb9d11a6"}, {}] +) +def test_valid_inbound_sms_request_json(client, request_args): + validate(request_args, get_inbound_sms_request) + + +def test_invalid_inbound_sms_request_json(client): + with pytest.raises(expected_exception=ValidationError): + validate({'user_number': '447700900111'}, get_inbound_sms_request) + + +def test_valid_inbound_sms_response_json(): + assert validate(valid_inbound_sms, get_inbound_sms_single_response) == valid_inbound_sms + + +def test_valid_inbound_sms_list_response_json(): + validate(valid_inbound_sms_list, get_inbound_sms_response) + + +def test_invalid_inbound_sms_response_json(): + with pytest.raises(expected_exception=ValidationError): + validate(invalid_inbound_sms, get_inbound_sms_single_response) + + +def test_invalid_inbound_sms_list_response_json(): + with pytest.raises(expected_exception=ValidationError): + validate(invalid_inbound_sms_list, get_inbound_sms_response)