This commit is contained in:
venusbb
2017-06-14 15:14:50 +01:00
12 changed files with 368 additions and 283 deletions

View File

@@ -165,14 +165,16 @@ def init_csrf(application):
@csrf.error_handler
def csrf_handler(reason):
application.logger.warning('csrf.error_message: {}'.format(reason))
if 'user_id' not in session:
application.logger.info(
application.logger.warning(
u'csrf.session_expired: Redirecting user to log in page'
)
return application.login_manager.unauthorized()
application.logger.info(
application.logger.warning(
u'csrf.invalid_token: Aborting request, user_id: {user_id}',
extra={'user_id': session['user_id']})

View File

@@ -303,6 +303,15 @@ def service_set_international_sms(service_id):
)
@main.route("/services/<service_id>/service-settings/set-inbound-sms", methods=['GET'])
@login_required
@user_has_permissions('manage_settings', admin_override=True)
def service_set_inbound_sms(service_id):
return render_template(
'views/service-settings/set-inbound-sms.html',
)
@main.route("/services/<service_id>/service-settings/set-letters", methods=['GET'])
@login_required
@user_has_permissions('manage_settings', admin_override=True)

View File

@@ -105,10 +105,8 @@
{% endcall %}
{%- endmacro %}
{% macro boolean_field(yes) -%}
{% call field(status='yes' if yes else 'no') %}
{{ "Yes" if yes else "No" }}
{% endcall %}
{% macro boolean_field(value) -%}
{{ text_field('On' if value else 'Off') }}
{%- endmacro %}
{% macro right_aligned_field_heading(text) %}

View File

@@ -1,5 +1,5 @@
{% extends "withnav_template.html" %}
{% from "components/table.html" import list_table, row, field, boolean_field, hidden_field_heading %}
{% from "components/table.html" import list_table, row, field, hidden_field_heading %}
{% from "components/page-footer.html" import page_footer %}
{% from "components/tick-cross.html" import tick_cross %}

View File

@@ -27,33 +27,30 @@
<a href="https://ukgovernmentdigital.slack.com/messages/govuk-notify">cross-government Slack channel</a>.
</p>
<h2 class="heading-medium">May 2017</h2>
<ul class="list list-bullet">
<li>Supporting replies to text messages (invite only)</li>
<li>Sending individual messages via Notify interface</li>
<li>Searching for notifications by phone number or email address</li>
<li>Restricting notifications to sociable hours</li>
</ul>
<h2 class="heading-medium">June 2017</h2>
<ul class="list list-bullet">
<li>Retiring version 1 of the API</li>
<li>Schedule retirement of version 1 of the API</li>
<li>Sending letters through the API</li>
<li>Providing non-mobile phone 2-factor authentication</li>
<li>Posting delivery receipts and inbound messages to services</li>
<li>Explore non-mobile phone 2-factor authentication</li>
<li>Allowing multiple sender names and reply-to addresses</li>
</ul>
<h2 class="heading-medium">July to September 2017</h2>
<h2 class="heading-medium">July 2017</h2>
<ul class="list list-bullet">
<li>Sending letters (available for everyone)</li>
<li>Sending of pre-compiled text messages and emails</li>
<li>Customising the expiry time for text messages</li>
<li>Distributing delivery of notifications over a period of time</li>
<li>Sending of pre-compiled letters</li>
<li>Sending of pre-compiled text messages and emails</li>
<li>Publishing design patterns for integration</li>
</ul>
<h2 class="heading-medium">October 2017 and beyond</h2>
<h2 class="heading-medium">August - October 2017</h2>
<ul class="list list-bullet">
<li>Sending letters (available for everyone)</li>
<li>Sending of pre-compiled letters</li>
<li>Distributing delivery of notifications over a period of time</li>
</ul>
<h2 class="heading-medium">November 2017 onwards</h2>
<ul class="list list-bullet">
<li>Checking mobile numbers are valid before sending</li>
<li>Allowing people to request to join a service</li>

View File

@@ -1,7 +1,7 @@
{% extends "withnav_template.html" %}
{% from "components/banner.html" import banner_wrapper %}
{% from "components/browse-list.html" import browse_list %}
{% from "components/table.html" import mapping_table, row, text_field, edit_field, field %}
{% from "components/table.html" import mapping_table, row, text_field, edit_field, field, boolean_field %}
{% block service_page_title %}
Settings
@@ -47,13 +47,19 @@
{% call row() %}
{{ text_field('International text messages') }}
{{ text_field('On' if current_service.can_send_international_sms else 'Off') }}
{{ boolean_field(current_service.can_send_international_sms) }}
{{ edit_field('Change', url_for('.service_set_international_sms', service_id=current_service.id)) }}
{% endcall %}
{% call row() %}
{{ text_field('Receive text messages') }}
{{ boolean_field('inbound_sms' in current_service.permissions) }}
{{ edit_field('Change', url_for('.service_set_inbound_sms', service_id=current_service.id)) }}
{% endcall %}
{% call row() %}
{{ text_field('Letters') }}
{{ text_field('On' if current_service.can_send_letters else 'Off') }}
{{ boolean_field(current_service.can_send_letters) }}
{{ edit_field('Change', url_for('.service_set_letters', service_id=current_service.id)) }}
{% endcall %}

View File

@@ -0,0 +1,43 @@
{% extends "withnav_template.html" %}
{% from "components/textbox.html" import textbox %}
{% from "components/page-footer.html" import page_footer %}
{% block service_page_title %}
Receive text messages
{% endblock %}
{% block maincolumn_content %}
<div class="grid-row">
<div class="column-five-sixths">
<h1 class="heading-large">Receive text messages</h1>
{% if 'inbound_sms' in current_service.permissions %}
<p>
Your service can receive text messages sent to {{ current_service.sms_sender }}.
</p>
<p>
If you want to turn this feature off,
<a href="{{ url_for('.support') }}">get in touch with the GOV.UK Notify team</a>.
</p>
{% else %}
<p>
Receiving text messages from your users is an
invitation&#8209;only feature.
</p>
<p>
If you want to try it out,
<a href="{{ url_for('.support') }}">get in touch with the GOV.UK Notify team</a>.
</p>
<p>
Well set you up with a special phone number, and youll be able to see
the messages on your dashboard, or get them using the API.
</p>
{% endif %}
{{ page_footer(
back_link=url_for('.service_settings', service_id=current_service.id),
back_link_text='Back to settings'
) }}
</div>
</div>
{% endblock %}

View File

@@ -3,7 +3,7 @@
{% from "components/page-footer.html" import page_footer %}
{% block service_page_title %}
Text message sender
International text messages
{% endblock %}
{% block maincolumn_content %}

View File

@@ -117,7 +117,7 @@
<div class="grid-row bottom-gutter">
<div class="column-half">
<h3 class="visually-hidden">Services</h3>
<div class="product-page-big-number">54</div>
<div class="product-page-big-number">55</div>
services
</div>
<div class="column-half">

View File

@@ -0,0 +1,276 @@
import json
import uuid
from urllib.parse import urlparse, quote, parse_qs
import pytest
from flask import url_for
from bs4 import BeautifulSoup
from app.main.views.jobs import get_time_left, get_status_filters
from tests import notification_json
from tests.conftest import SERVICE_ONE_ID
from freezegun import freeze_time
@pytest.mark.parametrize(
"message_type,page_title", [
('email', 'Emails'),
('sms', 'Text messages')
]
)
@pytest.mark.parametrize(
"status_argument, expected_api_call", [
(
'',
[
'created', 'pending', 'sending',
'delivered', 'sent',
'failed', 'temporary-failure', 'permanent-failure', 'technical-failure',
]
),
(
'sending',
['sending', 'created', 'pending']
),
(
'delivered',
['delivered', 'sent']
),
(
'failed',
['failed', 'temporary-failure', 'permanent-failure', 'technical-failure']
)
]
)
@pytest.mark.parametrize(
"page_argument, expected_page_argument", [
(1, 1),
(22, 22),
(None, 1)
]
)
@pytest.mark.parametrize(
"to_argument, expected_to_argument", [
('', ''),
('+447900900123', '+447900900123'),
('test@example.com', 'test@example.com'),
]
)
def test_can_show_notifications(
logged_in_client,
service_one,
mock_get_notifications,
mock_get_detailed_service,
message_type,
page_title,
status_argument,
expected_api_call,
page_argument,
expected_page_argument,
to_argument,
expected_to_argument,
):
response = logged_in_client.get(url_for(
'main.view_notifications',
service_id=service_one['id'],
message_type=message_type,
status=status_argument,
page=page_argument,
to=to_argument,
))
assert response.status_code == 200
content = response.get_data(as_text=True)
notifications = notification_json(service_one['id'])
notification = notifications['notifications'][0]
assert notification['to'] in content
assert notification['status'] in content
assert notification['template']['name'] in content
page = BeautifulSoup(response.data.decode('utf-8'), 'html.parser')
assert page_title in page.h1.text.strip()
path_to_json = page.find("div", {'data-key': 'notifications'})['data-resource']
url = urlparse(path_to_json)
assert url.path == '/services/{}/notifications/{}.json'.format(service_one['id'], message_type)
query_dict = parse_qs(url.query)
if status_argument:
assert query_dict['status'] == [status_argument]
if expected_page_argument:
assert query_dict['page'] == [str(expected_page_argument)]
if to_argument:
assert query_dict['to'] == [to_argument]
mock_get_notifications.assert_called_with(
limit_days=7,
page=expected_page_argument,
service_id=service_one['id'],
status=expected_api_call,
template_type=[message_type],
to=expected_to_argument,
)
json_response = logged_in_client.get(url_for(
'main.get_notifications_as_json',
service_id=service_one['id'],
message_type=message_type,
status=status_argument
))
json_content = json.loads(json_response.get_data(as_text=True))
assert json_content.keys() == {'counts', 'notifications'}
@pytest.mark.parametrize("initial_query_arguments, expected_status_field_value, expected_search_box_contents", [
(
{
'message_type': 'sms',
},
'sending,delivered,failed',
'',
),
(
{
'message_type': 'sms',
'to': '+33(0)5-12-34-56-78',
},
'sending,delivered,failed',
'+33(0)5-12-34-56-78',
),
(
{
'status': 'failed',
'message_type': 'email',
'page': '99',
'to': 'test@example.com',
},
'failed',
'test@example.com',
),
])
def test_search_recipient_form(
logged_in_client,
mock_get_notifications,
mock_get_detailed_service,
initial_query_arguments,
expected_status_field_value,
expected_search_box_contents,
):
response = logged_in_client.get(url_for(
'main.view_notifications',
service_id=SERVICE_ONE_ID,
**initial_query_arguments
))
assert response.status_code == 200
page = BeautifulSoup(response.data.decode('utf-8'), 'html.parser')
action_url = page.find("form")['action']
url = urlparse(action_url)
assert url.path == '/services/{}/notifications/{}'.format(
SERVICE_ONE_ID,
initial_query_arguments['message_type']
)
query_dict = parse_qs(url.query)
assert query_dict == {}
assert page.find("input", {'name': 'status'})['value'] == expected_status_field_value
assert page.find("input", {'name': 'to'})['value'] == expected_search_box_contents
def test_should_show_notifications_for_a_service_with_next_previous(
logged_in_client,
service_one,
active_user_with_permissions,
mock_get_notifications_with_previous_next,
mock_get_detailed_service,
mocker,
):
response = logged_in_client.get(url_for(
'main.view_notifications',
service_id=service_one['id'],
message_type='sms',
page=2
))
assert response.status_code == 200
page = BeautifulSoup(response.data.decode('utf-8'), 'html.parser')
next_page_link = page.find('a', {'rel': 'next'})
prev_page_link = page.find('a', {'rel': 'previous'})
assert (
url_for('main.view_notifications', service_id=service_one['id'], message_type='sms', page=3) in
next_page_link['href']
)
assert 'Next page' in next_page_link.text.strip()
assert 'page 3' in next_page_link.text.strip()
assert (
url_for('main.view_notifications', service_id=service_one['id'], message_type='sms', page=1) in
prev_page_link['href']
)
assert 'Previous page' in prev_page_link.text.strip()
assert 'page 1' in prev_page_link.text.strip()
@pytest.mark.parametrize(
"job_created_at, expected_message", [
("2016-01-10 11:09:00.000000+00:00", "Data available for 7 days"),
("2016-01-04 11:09:00.000000+00:00", "Data available for 1 day"),
("2016-01-03 11:09:00.000000+00:00", "Data available for 11 hours"),
("2016-01-02 23:59:59.000000+00:00", "Data no longer available")
]
)
@freeze_time("2016-01-10 12:00:00.000000")
def test_time_left(job_created_at, expected_message):
assert get_time_left(job_created_at) == expected_message
STATISTICS = {
'sms': {
'requested': 6,
'failed': 2,
'delivered': 1
}
}
def test_get_status_filters_calculates_stats(client):
ret = get_status_filters({'id': 'foo'}, 'sms', STATISTICS)
assert {label: count for label, _option, _link, count in ret} == {
'total': 6,
'sending': 3,
'failed': 2,
'delivered': 1
}
def test_get_status_filters_in_right_order(client):
ret = get_status_filters({'id': 'foo'}, 'sms', STATISTICS)
assert [label for label, _option, _link, _count in ret] == [
'total', 'sending', 'delivered', 'failed'
]
def test_get_status_filters_constructs_links(client):
ret = get_status_filters({'id': 'foo'}, 'sms', STATISTICS)
link = ret[0][2]
assert link == '/services/foo/notifications/sms?status={}'.format(quote('sending,delivered,failed'))
def test_html_contains_notification_id(
logged_in_client,
service_one,
active_user_with_permissions,
mock_get_notifications,
mock_get_detailed_service,
mocker,
):
response = logged_in_client.get(url_for(
'main.view_notifications',
service_id=service_one['id'],
message_type='sms',
status='')
)
assert response.status_code == 200
page = BeautifulSoup(response.data.decode('utf-8'), 'html.parser')
notifications = page.tbody.find_all('tr')
for tr in notifications:
assert uuid.UUID(tr.attrs['id'])

View File

@@ -275,201 +275,6 @@ def test_should_show_updates_for_one_job_as_json(
assert 'Sent by Test User on 1 January at midnight' in content['status']
@pytest.mark.parametrize(
"message_type,page_title", [
('email', 'Emails'),
('sms', 'Text messages')
]
)
@pytest.mark.parametrize(
"status_argument, expected_api_call", [
(
'',
[
'created', 'pending', 'sending',
'delivered', 'sent',
'failed', 'temporary-failure', 'permanent-failure', 'technical-failure',
]
),
(
'sending',
['sending', 'created', 'pending']
),
(
'delivered',
['delivered', 'sent']
),
(
'failed',
['failed', 'temporary-failure', 'permanent-failure', 'technical-failure']
)
]
)
@pytest.mark.parametrize(
"page_argument, expected_page_argument", [
(1, 1),
(22, 22),
(None, 1)
]
)
@pytest.mark.parametrize(
"to_argument, expected_to_argument", [
('', ''),
('+447900900123', '+447900900123'),
('test@example.com', 'test@example.com'),
]
)
def test_can_show_notifications(
logged_in_client,
service_one,
mock_get_notifications,
mock_get_detailed_service,
message_type,
page_title,
status_argument,
expected_api_call,
page_argument,
expected_page_argument,
to_argument,
expected_to_argument,
):
response = logged_in_client.get(url_for(
'main.view_notifications',
service_id=service_one['id'],
message_type=message_type,
status=status_argument,
page=page_argument,
to=to_argument,
))
assert response.status_code == 200
content = response.get_data(as_text=True)
notifications = notification_json(service_one['id'])
notification = notifications['notifications'][0]
assert notification['to'] in content
assert notification['status'] in content
assert notification['template']['name'] in content
page = BeautifulSoup(response.data.decode('utf-8'), 'html.parser')
assert page_title in page.h1.text.strip()
path_to_json = page.find("div", {'data-key': 'notifications'})['data-resource']
url = urlparse(path_to_json)
assert url.path == '/services/{}/notifications/{}.json'.format(service_one['id'], message_type)
query_dict = parse_qs(url.query)
if status_argument:
assert query_dict['status'] == [status_argument]
if expected_page_argument:
assert query_dict['page'] == [str(expected_page_argument)]
if to_argument:
assert query_dict['to'] == [to_argument]
mock_get_notifications.assert_called_with(
limit_days=7,
page=expected_page_argument,
service_id=service_one['id'],
status=expected_api_call,
template_type=[message_type],
to=expected_to_argument,
)
json_response = logged_in_client.get(url_for(
'main.get_notifications_as_json',
service_id=service_one['id'],
message_type=message_type,
status=status_argument
))
json_content = json.loads(json_response.get_data(as_text=True))
assert json_content.keys() == {'counts', 'notifications'}
@pytest.mark.parametrize("initial_query_arguments, expected_status_field_value, expected_search_box_contents", [
(
{
'message_type': 'sms',
},
'sending,delivered,failed',
'',
),
(
{
'message_type': 'sms',
'to': '+33(0)5-12-34-56-78',
},
'sending,delivered,failed',
'+33(0)5-12-34-56-78',
),
(
{
'status': 'failed',
'message_type': 'email',
'page': '99',
'to': 'test@example.com',
},
'failed',
'test@example.com',
),
])
def test_search_recipient_form(
logged_in_client,
mock_get_notifications,
mock_get_detailed_service,
initial_query_arguments,
expected_status_field_value,
expected_search_box_contents,
):
response = logged_in_client.get(url_for(
'main.view_notifications',
service_id=SERVICE_ONE_ID,
**initial_query_arguments
))
assert response.status_code == 200
page = BeautifulSoup(response.data.decode('utf-8'), 'html.parser')
action_url = page.find("form")['action']
url = urlparse(action_url)
assert url.path == '/services/{}/notifications/{}'.format(
SERVICE_ONE_ID,
initial_query_arguments['message_type']
)
query_dict = parse_qs(url.query)
assert query_dict == {}
assert page.find("input", {'name': 'status'})['value'] == expected_status_field_value
assert page.find("input", {'name': 'to'})['value'] == expected_search_box_contents
def test_should_show_notifications_for_a_service_with_next_previous(
logged_in_client,
service_one,
active_user_with_permissions,
mock_get_notifications_with_previous_next,
mock_get_detailed_service,
mocker,
):
response = logged_in_client.get(url_for(
'main.view_notifications',
service_id=service_one['id'],
message_type='sms',
page=2
))
assert response.status_code == 200
page = BeautifulSoup(response.data.decode('utf-8'), 'html.parser')
next_page_link = page.find('a', {'rel': 'next'})
prev_page_link = page.find('a', {'rel': 'previous'})
assert (
url_for('main.view_notifications', service_id=service_one['id'], message_type='sms', page=3) in
next_page_link['href']
)
assert 'Next page' in next_page_link.text.strip()
assert 'page 3' in next_page_link.text.strip()
assert (
url_for('main.view_notifications', service_id=service_one['id'], message_type='sms', page=1) in
prev_page_link['href']
)
assert 'Previous page' in prev_page_link.text.strip()
assert 'page 1' in prev_page_link.text.strip()
@pytest.mark.parametrize(
"job_created_at, expected_message", [
("2016-01-10 11:09:00.000000+00:00", "Data available for 7 days"),
@@ -481,59 +286,3 @@ def test_should_show_notifications_for_a_service_with_next_previous(
@freeze_time("2016-01-10 12:00:00.000000")
def test_time_left(job_created_at, expected_message):
assert get_time_left(job_created_at) == expected_message
STATISTICS = {
'sms': {
'requested': 6,
'failed': 2,
'delivered': 1
}
}
def test_get_status_filters_calculates_stats(client):
ret = get_status_filters({'id': 'foo'}, 'sms', STATISTICS)
assert {label: count for label, _option, _link, count in ret} == {
'total': 6,
'sending': 3,
'failed': 2,
'delivered': 1
}
def test_get_status_filters_in_right_order(client):
ret = get_status_filters({'id': 'foo'}, 'sms', STATISTICS)
assert [label for label, _option, _link, _count in ret] == [
'total', 'sending', 'delivered', 'failed'
]
def test_get_status_filters_constructs_links(client):
ret = get_status_filters({'id': 'foo'}, 'sms', STATISTICS)
link = ret[0][2]
assert link == '/services/foo/notifications/sms?status={}'.format(quote('sending,delivered,failed'))
def test_html_contains_notification_id(
logged_in_client,
service_one,
active_user_with_permissions,
mock_get_notifications,
mock_get_detailed_service,
mocker,
):
response = logged_in_client.get(url_for(
'main.view_notifications',
service_id=service_one['id'],
message_type='sms',
status='')
)
assert response.status_code == 200
page = BeautifulSoup(response.data.decode('utf-8'), 'html.parser')
notifications = page.tbody.find_all('tr')
for tr in notifications:
assert uuid.UUID(tr.attrs['id'])

View File

@@ -21,6 +21,7 @@ from tests.conftest import active_user_with_permissions, platform_admin_user
'Email reply to address None Change',
'Text message sender GOVUK Change',
'International text messages Off Change',
'Receive text messages Off Change',
'Letters Off Change',
]),
(platform_admin_user, [
@@ -29,6 +30,7 @@ from tests.conftest import active_user_with_permissions, platform_admin_user
'Email reply to address None Change',
'Text message sender GOVUK Change',
'International text messages Off Change',
'Receive text messages Off Change',
'Letters Off Change',
'Label Value Action',
'Email branding GOV.UK Change',
@@ -67,6 +69,8 @@ def test_should_show_overview_for_service_with_more_things_set(
mock_get_letter_organisations,
):
client.login(active_user_with_permissions, mocker, service_with_reply_to_addresses)
service_with_reply_to_addresses['permissions'] = ['inbound_sms']
service_with_reply_to_addresses['can_send_international_sms'] = True
response = client.get(url_for(
'main.service_settings', service_id=service_with_reply_to_addresses['id']
))
@@ -74,8 +78,9 @@ def test_should_show_overview_for_service_with_more_things_set(
for index, row in enumerate([
'Service name service one Change',
'Email reply to address test@example.com Change',
'Text message sender elevenchars Change',
'International text messages Off Change',
'Text message sender elevenchars',
'International text messages On Change',
'Receive text messages On Change',
'Letters Off Change',
]):
assert row == " ".join(page.find_all('tr')[index + 1].text.split())
@@ -120,7 +125,7 @@ def test_letter_contact_block_shows_none_if_not_set(
))
page = BeautifulSoup(response.data.decode('utf-8'), 'html.parser')
div = page.find_all('tr')[6].find_all('td')[1].div
div = page.find_all('tr')[7].find_all('td')[1].div
assert div.text.strip() == 'None'
assert 'default' in div.attrs['class'][0]
@@ -138,7 +143,7 @@ def test_escapes_letter_contact_block(
))
page = BeautifulSoup(response.data.decode('utf-8'), 'html.parser')
div = str(page.find_all('tr')[6].find_all('td')[1].div)
div = str(page.find_all('tr')[7].find_all('td')[1].div)
assert 'foo<br>bar' in div
assert '<script>' not in div