diff --git a/app/templates/views/service_dashboard.html b/app/templates/views/service_dashboard.html index b860e7f8b..90b4ecc0d 100644 --- a/app/templates/views/service_dashboard.html +++ b/app/templates/views/service_dashboard.html @@ -1,4 +1,5 @@ {% extends "withnav_template.html" %} +{% from "components/banner.html" import banner_wrapper %} {% from "components/table.html" import list_table, field, right_aligned_field_heading %} {% from "components/big-number.html" import big_number %} @@ -24,33 +25,26 @@ {% if not template_count and not jobs %} - {{ banner( - """ -
    + {% call banner_wrapper(subhead='Get started', type="tip") %} +
      + {% if current_user.has_permissions(['manage_templates']) %}
    1. - Add a template + Add a template
    2. + {% endif %} + {% if current_user.has_permissions(['send_texts', 'send_emails', 'send_letters']) %}
    3. - Send yourself a text message + Send yourself a text message
    4. -
    - """.format( - url_for(".add_service_template", service_id=service_id, template_type="sms"), - url_for(".choose_template", service_id=service_id, template_type="sms") - )|safe, - subhead='Get started', - type="tip" - )}} + {% endif %} +
+ {% endcall %} {% elif not jobs %} - {{ banner( - """ - Send yourself a text message - """.format( - url_for(".choose_template", service_id=service_id, template_type="sms") - )|safe, - subhead='Next step', - type="tip" - )}} + {% call banner_wrapper(subhead='Next step', type="tip") %} + {% if current_user.has_permissions(['send_texts', 'send_emails', 'send_letters']) %} + Send yourself a text message + {% endif %} + {% endcall %} {% else %} {% call(item) list_table( jobs, @@ -69,9 +63,11 @@ {% endcall %} {% endcall %} {% if more_jobs_to_show %} - + {% if current_user.has_permissions(['send_texts', 'send_emails', 'send_letters']) %} + + {% endif %} {% endif %} {% endif %} diff --git a/tests/__init__.py b/tests/__init__.py index 9fb7b5ede..a316c4342 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,3 +1,4 @@ +import pytest from flask.testing import FlaskClient from flask import url_for @@ -125,3 +126,39 @@ def notification_json(): 'links': {} } return data + + +def validate_route_permission(mocker, + app_, + method, + response_code, + route, + permissions, + usr, + service): + usr._permissions[str(service['id'])] = permissions + mocker.patch( + 'app.user_api_client.check_verify_code', + return_value=(True, '')) + mocker.patch( + 'app.notifications_api_client.get_services', + return_value={'data': []}) + mocker.patch('app.user_api_client.get_user', return_value=usr) + mocker.patch('app.user_api_client.get_user_by_email', return_value=usr) + mocker.patch('app.notifications_api_client.get_service', return_value={'data': service}) + + with app_.test_request_context(): + with app_.test_client() as client: + client.login(usr) + resp = None + with client.session_transaction() as session: + session['service_id'] = str(service['id']) + if method == 'GET': + resp = client.get(route) + elif method == 'POST': + resp = client.post(route) + else: + pytest.fail("Invalid method call {}".format(method)) + if resp.status_code != response_code: + pytest.fail("Invalid permissions set for endpoint {}".format(route)) + return resp diff --git a/tests/app/main/test_permissions.py b/tests/app/main/test_permissions.py index 11e681a21..62c5b5058 100644 --- a/tests/app/main/test_permissions.py +++ b/tests/app/main/test_permissions.py @@ -6,65 +6,73 @@ from app.main.views.index import index from werkzeug.exceptions import Forbidden +def _test_permissions(app_, usr, permissions, will_succeed, or_=False): + with app_.test_request_context(): + with app_.test_client() as client: + client.login(usr) + decorator = user_has_permissions(*permissions, or_=or_) + decorated_index = decorator(index) + if will_succeed: + response = decorated_index() + else: + try: + response = decorated_index() + pytest.fail("Failed to throw a forbidden exception") + except Forbidden: + pass + + def test_user_has_permissions_on_endpoint_fail(app_, api_user_active, mock_login, mock_get_user_with_permissions): - with app_.test_request_context(): - with app_.test_client() as client: - client.login(api_user_active) - decorator = user_has_permissions('something') - decorated_index = decorator(index) - try: - response = decorated_index() - pytest.fail("Failed to throw a forbidden exception") - except Forbidden: - pass + _test_permissions( + app_, + api_user_active, + ['something'], + False) def test_user_has_permissions_success(app_, api_user_active, mock_login, mock_get_user_with_permissions): - with app_.test_request_context(): - with app_.test_client() as client: - client.login(api_user_active) - decorator = user_has_permissions('manage_users') - decorated_index = decorator(index) - response = decorated_index() + _test_permissions( + app_, + api_user_active, + ['manage_users'], + True) def test_user_has_permissions_or(app_, api_user_active, mock_login, mock_get_user_with_permissions): - with app_.test_request_context(): - with app_.test_client() as client: - client.login(api_user_active) - decorator = user_has_permissions('something', 'manage_users', or_=True) - decorated_index = decorator(index) - response = decorated_index() + _test_permissions( + app_, + api_user_active, + ['something', 'manage_users'], + True, + or_=True) def test_user_has_permissions_multiple(app_, api_user_active, mock_login, mock_get_user_with_permissions): - with app_.test_request_context(): - with app_.test_client() as client: - client.login(api_user_active) - decorator = user_has_permissions('manage_templates', 'manage_users') - decorated_index = decorator(index) - response = decorated_index() + _test_permissions( + app_, + api_user_active, + ['manage_templates', 'manage_users'], + True) def test_exact_permissions(app_, api_user_active, mock_login, mock_get_user_with_permissions): - with app_.test_request_context(): - with app_.test_client() as client: - client.login(api_user_active) - decorator = user_has_permissions('manage_users', 'manage_templates', 'manage_settings') - decorated_index = decorator(index) - response = decorated_index() + _test_permissions( + app_, + api_user_active, + ['manage_users', 'manage_templates', 'manage_settings'], + True) diff --git a/tests/app/main/views/test_api_keys.py b/tests/app/main/views/test_api_keys.py index d22855fe6..72fd6007a 100644 --- a/tests/app/main/views/test_api_keys.py +++ b/tests/app/main/views/test_api_keys.py @@ -1,6 +1,7 @@ import uuid from datetime import date from flask import url_for +from tests import validate_route_permission def test_should_show_api_keys_and_documentation_page(app_, @@ -127,3 +128,39 @@ def test_should_redirect_after_revoking_api_key(app_, assert response.location == url_for('.api_keys', service_id=service_id, _external=True) mock_revoke_api_key.assert_called_once_with(service_id=service_id, key_id=321) mock_get_api_keys.assert_called_once_with(service_id=service_id, key_id=321) + + +def test_route_permissions(mocker, app_, api_user_active, service_one, mock_get_api_keys): + routes = [ + 'main.api_keys', + 'main.create_api_key', + 'main.revoke_api_key'] + with app_.test_request_context(): + for route in routes: + validate_route_permission( + mocker, + app_, + "GET", + 200, + url_for(route, service_id=service_one['id'], key_id=123), + ['manage_api_keys'], + api_user_active, + service_one) + + +def test_route_invalid_permissions(mocker, app_, api_user_active, service_one, mock_get_api_keys): + routes = [ + 'main.api_keys', + 'main.create_api_key', + 'main.revoke_api_key'] + with app_.test_request_context(): + for route in routes: + validate_route_permission( + mocker, + app_, + "GET", + 403, + url_for(route, service_id=service_one['id'], key_id=123), + ['blah'], + api_user_active, + service_one) diff --git a/tests/app/main/views/test_dashboard.py b/tests/app/main/views/test_dashboard.py index d5b3386ea..4fb92a2b5 100644 --- a/tests/app/main/views/test_dashboard.py +++ b/tests/app/main/views/test_dashboard.py @@ -1,4 +1,5 @@ from flask import url_for, session +from bs4 import BeautifulSoup def test_should_show_recent_jobs_on_dashboard(app_, @@ -18,3 +19,98 @@ def test_should_show_recent_jobs_on_dashboard(app_, assert response.status_code == 200 text = response.get_data(as_text=True) assert 'Test Service' in text + + +def _test_dashboard_menu(mocker, app_, usr, service, permissions): + with app_.test_request_context(): + with app_.test_client() as client: + usr._permissions[str(service['id'])] = permissions + mocker.patch( + 'app.user_api_client.check_verify_code', + return_value=(True, '')) + mocker.patch( + 'app.notifications_api_client.get_services', + return_value={'data': []}) + mocker.patch('app.user_api_client.get_user', return_value=usr) + mocker.patch('app.user_api_client.get_user_by_email', return_value=usr) + mocker.patch('app.notifications_api_client.get_service', return_value={'data': service}) + client.login(usr) + return client.get(url_for('main.service_dashboard', service_id=service['id'])) + + +def test_menu_send_messages(mocker, app_, api_user_active, service_one, mock_get_service_templates, mock_get_jobs): + with app_.test_request_context(): + resp = _test_dashboard_menu( + mocker, + app_, + api_user_active, + service_one, + ['send_texts', 'send_emails', 'send_letters']) + page = resp.get_data(as_text=True) + assert url_for('main.letters_stub', service_id=service_one['id']) in page + assert url_for( + 'main.choose_template', + service_id=service_one['id'], + template_type='email')in page + assert url_for( + 'main.choose_template', + service_id=service_one['id'], + template_type='sms')in page + + assert url_for('main.manage_users', service_id=service_one['id']) not in page + assert url_for('main.service_settings', service_id=service_one['id']) not in page + + assert url_for('main.api_keys', service_id=service_one['id']) not in page + assert url_for('main.documentation', service_id=service_one['id']) not in page + + +def test_menu_manage_service(mocker, app_, api_user_active, service_one, mock_get_service_templates, mock_get_jobs): + with app_.test_request_context(): + resp = _test_dashboard_menu( + mocker, + app_, + api_user_active, + service_one, + ['manage_users', 'manage_templates', 'manage_settings']) + page = resp.get_data(as_text=True) + assert url_for('main.letters_stub', service_id=service_one['id'])in page + assert url_for( + 'main.choose_template', + service_id=service_one['id'], + template_type='email') in page + assert url_for( + 'main.choose_template', + service_id=service_one['id'], + template_type='sms') in page + + assert url_for('main.manage_users', service_id=service_one['id']) in page + assert url_for('main.service_settings', service_id=service_one['id']) in page + + assert url_for('main.api_keys', service_id=service_one['id']) not in page + assert url_for('main.documentation', service_id=service_one['id']) not in page + + +def test_menu_manage_api_keys(mocker, app_, api_user_active, service_one, mock_get_service_templates, mock_get_jobs): + with app_.test_request_context(): + resp = _test_dashboard_menu( + mocker, + app_, + api_user_active, + service_one, + ['manage_api_keys', 'access_developer_docs']) + page = resp.get_data(as_text=True) + assert url_for('main.letters_stub', service_id=service_one['id']) not in page + assert url_for( + 'main.choose_template', + service_id=service_one['id'], + template_type='email') not in page + assert url_for( + 'main.choose_template', + service_id=service_one['id'], + template_type='sms') not in page + + assert url_for('main.manage_users', service_id=service_one['id']) not in page + assert url_for('main.service_settings', service_id=service_one['id']) not in page + + assert url_for('main.api_keys', service_id=service_one['id']) in page + assert url_for('main.documentation', service_id=service_one['id']) in page diff --git a/tests/app/main/views/test_send.py b/tests/app/main/views/test_send.py index 7c35c7aa2..5115d4e73 100644 --- a/tests/app/main/views/test_send.py +++ b/tests/app/main/views/test_send.py @@ -3,36 +3,11 @@ import pytest from io import BytesIO from flask import url_for from unittest.mock import ANY +from tests import validate_route_permission template_types = ['email', 'sms'] -@pytest.mark.parametrize("template_type", template_types) -def test_choose_template( - template_type, - app_, - api_user_active, - mock_login, - mock_get_user, - mock_get_service, - mock_check_verify_code, - mock_get_service_templates, - mock_get_jobs, - mock_has_permissions -): - with app_.test_request_context(): - with app_.test_client() as client: - client.login(api_user_active) - response = client.get(url_for('main.choose_template', template_type=template_type, service_id=12345)) - - assert response.status_code == 200 - content = response.get_data(as_text=True) - assert '{}_template_one'.format(template_type) in content - assert '{} template one content'.format(template_type) in content - assert '{}_template_two'.format(template_type) in content - assert '{} template two content'.format(template_type) in content - - def test_upload_csvfile_with_errors_shows_check_page_with_errors( app_, api_user_active, @@ -247,3 +222,158 @@ def test_check_messages_should_revalidate_file_when_uploading_file( ) assert response.status_code == 200 assert 'There was a problem with invalid.csv' in response.get_data(as_text=True) + + +def test_route_permissions(mocker, + app_, + api_user_active, + service_one, + mock_get_service_template, + mock_get_service_templates, + mock_get_jobs, + mock_get_notifications, + mock_create_job, + mock_s3_upload): + routes = [ + 'main.choose_template', + 'main.send_messages', + 'main.get_example_csv'] + with app_.test_request_context(): + for route in routes: + validate_route_permission( + mocker, + app_, + "GET", + 200, + url_for( + route, + service_id=service_one['id'], + template_type='sms', + template_id=123), + ['send_texts', 'send_emails', 'send_letters'], + api_user_active, + service_one) + + with app_.test_request_context(): + validate_route_permission( + mocker, + app_, + "GET", + 302, + url_for( + 'main.send_message_to_self', + service_id=service_one['id'], + template_type='sms', + template_id=123), + ['send_texts', 'send_emails', 'send_letters'], + api_user_active, + service_one) + + +def test_route_invalid_permissions(mocker, + app_, + api_user_active, + service_one, + mock_get_service_template, + mock_get_service_templates, + mock_get_jobs, + mock_get_notifications, + mock_create_job): + routes = [ + 'main.choose_template', + 'main.send_messages', + 'main.get_example_csv', + 'main.send_message_to_self'] + with app_.test_request_context(): + for route in routes: + validate_route_permission( + mocker, + app_, + "GET", + 403, + url_for( + route, + service_id=service_one['id'], + template_type='sms', + template_id=123), + ['blah'], + api_user_active, + service_one) + + +def test_route_choose_template_manage_service_permissions(mocker, + app_, + api_user_active, + service_one, + mock_login, + mock_get_user, + mock_get_service, + mock_check_verify_code, + mock_get_service_templates, + mock_get_jobs): + with app_.test_request_context(): + template_id = mock_get_service_templates(service_one['id'])['data'][0]['id'] + resp = validate_route_permission( + mocker, + app_, + "GET", + 200, + url_for( + 'main.choose_template', + service_id=service_one['id'], + template_type='sms'), + ['manage_users', 'manage_templates', 'manage_settings'], + api_user_active, + service_one) + page = resp.get_data(as_text=True) + assert url_for( + "main.send_messages", + service_id=service_one['id'], + template_id=template_id) not in page + assert url_for( + "main.send_message_to_self", + service_id=service_one['id'], + template_id=template_id) not in page + assert url_for( + "main.edit_service_template", + service_id=service_one['id'], + template_id=template_id) in page + + +def test_route_choose_template_send_messages_permissions(mocker, + app_, + api_user_active, + service_one, + mock_login, + mock_get_user, + mock_get_service, + mock_check_verify_code, + mock_get_service_templates, + mock_get_jobs): + with app_.test_request_context(): + template_id = mock_get_service_templates(service_one['id'])['data'][0]['id'] + resp = validate_route_permission( + mocker, + app_, + "GET", + 200, + url_for( + 'main.choose_template', + service_id=service_one['id'], + template_type='sms'), + ['send_texts', 'send_emails', 'send_letters'], + api_user_active, + service_one) + page = resp.get_data(as_text=True) + assert url_for( + "main.send_messages", + service_id=service_one['id'], + template_id=template_id) in page + assert url_for( + "main.send_message_to_self", + service_id=service_one['id'], + template_id=template_id) in page + assert url_for( + "main.edit_service_template", + service_id=service_one['id'], + template_id=template_id) not in page diff --git a/tests/app/main/views/test_service_settings.py b/tests/app/main/views/test_service_settings.py index 174935f2a..7d9f52822 100644 --- a/tests/app/main/views/test_service_settings.py +++ b/tests/app/main/views/test_service_settings.py @@ -1,4 +1,5 @@ from flask import (url_for, session) +from tests import validate_route_permission def test_should_show_overview(app_, @@ -318,3 +319,49 @@ def test_should_redirect_delete_confirmation(app_, assert choose_url == response.location assert mock_get_service.called assert mock_delete_service.called + + +def test_route_permissions(mocker, app_, api_user_active, service_one): + routes = [ + 'main.service_settings', + 'main.service_name_change', + 'main.service_name_change_confirm', + 'main.service_request_to_go_live', + 'main.service_status_change', + 'main.service_status_change_confirm', + 'main.service_delete', + 'main.service_delete_confirm'] + with app_.test_request_context(): + for route in routes: + validate_route_permission( + mocker, + app_, + "GET", + 200, + url_for(route, service_id=service_one['id']), + ['manage_settings'], + api_user_active, + service_one) + + +def test_route_invalid_permissions(mocker, app_, api_user_active, service_one): + routes = [ + 'main.service_settings', + 'main.service_name_change', + 'main.service_name_change_confirm', + 'main.service_request_to_go_live', + 'main.service_status_change', + 'main.service_status_change_confirm', + 'main.service_delete', + 'main.service_delete_confirm'] + with app_.test_request_context(): + for route in routes: + validate_route_permission( + mocker, + app_, + "GET", + 403, + url_for(route, service_id=service_one['id']), + ['blah'], + api_user_active, + service_one) diff --git a/tests/app/main/views/test_templates.py b/tests/app/main/views/test_templates.py index cff04bde1..d8096f675 100644 --- a/tests/app/main/views/test_templates.py +++ b/tests/app/main/views/test_templates.py @@ -1,5 +1,6 @@ import json import uuid +from tests import validate_route_permission from flask import url_for @@ -125,3 +126,55 @@ def test_should_redirect_when_deleting_a_template(app_, service_id, template_id) mock_delete_service_template.assert_called_with( service_id, template_id) + + +def test_route_permissions(mocker, + app_, + api_user_active, + service_one, + mock_get_service_template): + routes = [ + 'main.add_service_template', + 'main.edit_service_template', + 'main.delete_service_template'] + with app_.test_request_context(): + for route in routes: + validate_route_permission( + mocker, + app_, + "GET", + 200, + url_for( + route, + service_id=service_one['id'], + template_type='sms', + template_id=123), + ['manage_templates'], + api_user_active, + service_one) + + +def test_route_invalid_permissions(mocker, + app_, + api_user_active, + service_one, + mock_get_service_template): + routes = [ + 'main.add_service_template', + 'main.edit_service_template', + 'main.delete_service_template'] + with app_.test_request_context(): + for route in routes: + validate_route_permission( + mocker, + app_, + "GET", + 403, + url_for( + route, + service_id=service_one['id'], + template_type='sms', + template_id=123), + ['blah'], + api_user_active, + service_one)