import ast import inspect import re import pytest from flask import current_app from tests import service_json from tests.conftest import ( ORGANISATION_ID, ORGANISATION_TWO_ID, SERVICE_ONE_ID, SERVICE_TWO_ID, ) @pytest.mark.parametrize( ("user_services", "user_organizations", "expected_status", "organization_checked"), [ ([SERVICE_ONE_ID], [], 200, False), ([SERVICE_ONE_ID, SERVICE_TWO_ID], [], 200, False), ([], [ORGANISATION_ID], 200, True), ([SERVICE_ONE_ID], [ORGANISATION_ID], 200, False), ([], [], 403, True), ([SERVICE_TWO_ID], [], 403, True), ([SERVICE_TWO_ID], [ORGANISATION_ID], 200, True), ([SERVICE_ONE_ID, SERVICE_TWO_ID], [ORGANISATION_ID], 200, False), ([], [ORGANISATION_TWO_ID], 403, True), ([], [ORGANISATION_ID, ORGANISATION_TWO_ID], 200, True), ], ) def test_services_pages_that_org_users_are_allowed_to_see( client_request, mocker, api_user_active, mock_get_annual_usage_for_service, mock_get_monthly_usage_for_service, mock_get_free_sms_fragment_limit, mock_get_monthly_notification_stats, mock_get_service, mock_get_invites_for_service, mock_get_users_by_service, mock_get_template_folders, mock_get_organization, mock_has_jobs, user_services, user_organizations, expected_status, organization_checked, ): api_user_active["services"] = user_services api_user_active["organizations"] = user_organizations api_user_active["permissions"] = { service_id: ["manage_users", "manage_settings"] for service_id in user_services } service = service_json( name="SERVICE WITH ORG", id_=SERVICE_ONE_ID, users=[api_user_active["id"]], organization_id=ORGANISATION_ID, ) mock_get_service = mocker.patch( "app.notify_client.service_api_client.service_api_client.get_service", return_value={"data": service}, ) client_request.login( api_user_active, service=service if SERVICE_ONE_ID in user_services else None, ) endpoints = ( "main.usage", "main.manage_users", ) for endpoint in endpoints: client_request.get( endpoint, service_id=SERVICE_ONE_ID, _expected_status=expected_status, ) assert mock_get_service.called is organization_checked def test_service_navigation_for_org_user( client_request, mocker, api_user_active, mock_get_annual_usage_for_service, mock_get_monthly_usage_for_service, mock_get_free_sms_fragment_limit, mock_get_monthly_notification_stats, mock_get_service, mock_get_invites_for_service, mock_get_users_by_service, mock_get_organization, ): api_user_active["services"] = [] api_user_active["organizations"] = [ORGANISATION_ID] service = service_json( id_=SERVICE_ONE_ID, organization_id=ORGANISATION_ID, ) mocker.patch("app.service_api_client.get_service", return_value={"data": service}) client_request.login(api_user_active, service=service) page = client_request.get( "main.usage", service_id=SERVICE_ONE_ID, ) assert [item.text.strip() for item in page.select("nav.nav a")] == [ "Send messages", "Usage", "Team members", ] @pytest.mark.parametrize( ("user_organizations", "expected_menu_items", "expected_status"), [ ( [], ( "Send messages", "Sent messages", ), 403, ), ( [ORGANISATION_ID], ( "Send messages", "Sent messages", ), 200, ), ], ) def test_service_user_without_manage_service_permission_can_see_usage_page_when_org_user( client_request, mocker, active_caseworking_user, mock_has_no_jobs, mock_get_annual_usage_for_service, mock_get_monthly_usage_for_service, mock_get_free_sms_fragment_limit, mock_get_monthly_notification_stats, mock_get_service, mock_get_invites_for_service, mock_get_users_by_service, mock_get_organization, mock_get_service_templates, mock_get_template_folders, mock_get_api_keys, user_organizations, expected_status, expected_menu_items, ): active_caseworking_user["services"] = [SERVICE_ONE_ID] active_caseworking_user["organizations"] = user_organizations service = service_json( id_=SERVICE_ONE_ID, organization_id=ORGANISATION_ID, ) mocker.patch("app.service_api_client.get_service", return_value={"data": service}) client_request.login(active_caseworking_user, service=service) page = client_request.get( "main.choose_template", service_id=SERVICE_ONE_ID, ) assert ( tuple(item.text.strip() for item in page.select("nav.nav a")) == expected_menu_items ) client_request.get( "main.usage", service_id=SERVICE_ONE_ID, _expected_status=expected_status, ) def get_name_of_decorator_from_ast_node(node): if isinstance(node, ast.Name): return str(node.id) if isinstance(node, ast.Call) and isinstance(node.func, ast.Name): return get_name_of_decorator_from_ast_node(node.func) if isinstance(node, ast.Attribute): return node.value.id return "{}.{}".format(node.func.value.id, node.func.attr) def get_decorators_for_function(function): for node in ast.walk(ast.parse(inspect.getsource(function))): if isinstance(node, ast.FunctionDef): for decorator in node.decorator_list: yield get_name_of_decorator_from_ast_node(decorator) SERVICE_ID_ARGUMENT = "service_id" ORGANISATION_ID_ARGUMENT = "org_id" def get_routes_and_decorators(argument_name=None): import app.main.views as views for module_name, module in inspect.getmembers(views): for function_name, function in inspect.getmembers(module): if inspect.isfunction(function): decorators = list(get_decorators_for_function(function)) if "main.route" in decorators and ( not argument_name or argument_name in inspect.signature(function).parameters.keys() ): yield "{}.{}".format(module_name, function_name), decorators def format_decorators(decorators, indent=8): return "\n".join( "{}@{}".format(" " * indent, decorator) for decorator in decorators ) def test_code_to_extract_decorators_works_with_known_examples(): assert ( "templates.choose_template", [ "main.route", "main.route", "main.route", "main.route", "main.route", "main.route", "user_has_permissions", ], ) in list(get_routes_and_decorators(SERVICE_ID_ARGUMENT)) assert ( "organizations.organization_dashboard", ["main.route", "user_has_permissions"], ) in list(get_routes_and_decorators(ORGANISATION_ID_ARGUMENT)) assert ( "platform_admin.platform_admin", ["main.route", "user_is_platform_admin"], ) in list(get_routes_and_decorators()) def test_routes_have_permissions_decorators(): for endpoint, decorators in list( get_routes_and_decorators(SERVICE_ID_ARGUMENT) ) + list(get_routes_and_decorators(ORGANISATION_ID_ARGUMENT)): file, function = endpoint.split(".") assert "user_is_logged_in" not in decorators, ( "@user_is_logged_in used on service or organization specific endpoint\n" "Use @user_has_permissions() or @user_is_platform_admin only\n" "app/main/views/{}.py::{}\n" ).format(file, function) if "user_is_platform_admin" in decorators: continue assert "user_has_permissions" in decorators, ( "Missing @user_has_permissions decorator\n" "Use @user_has_permissions() or @user_is_platform_admin instead\n" "app/main/views/{}.py::{}\n" ).format(file, function) for _endpoint, decorators in get_routes_and_decorators(): assert "login_required" not in decorators, ( "@login_required found\n" "For consistency, use @user_is_logged_in() instead (from app.utils)\n" "app/main/views/{}.py::{}\n" ).format(file, function) if "user_is_platform_admin" in decorators: assert "user_has_permissions" not in decorators, ( "@user_has_permissions and @user_is_platform_admin decorating same function\n" "You can only use one of these at a time\n" "app/main/views/{}.py::{}\n" ).format(file, function) assert "user_is_logged_in" not in decorators, ( "@user_is_logged_in used with @user_is_platform_admin\n" "Use @user_is_platform_admin only\n" "app/main/views/{}.py::{}\n" ).format(file, function) def test_routes_require_uuids(client_request): for rule in current_app.url_map.iter_rules(): for param in re.findall("<([^>]*)>", rule.rule): if "_id" in param and not param.startswith("uuid:"): pytest.fail(("Should be in {}").format(param, rule.rule))