"""Tests for base handlers.""" import json import random from http.client import OK from unittest.mock import MagicMock, NonCallableMagicMock, call import pytest import sentry_sdk from botocore.errorfactory import ClientError from flask import g, request from owsrequest import context, flask_request from owsrequest.constants import headers as owsrequest_headers from owsrequest.utils import mock_request from participant.api import app from participant.constants import permissions as permissions_constants from participant.constants import service as service_constants from participant.handlers import base as base_handlers from participant.utils import exception def test_invalid_usage_exception_handler(): """Test invalid_usage_exception_handler.""" @pytest.mark.parametrize( ( 'exception', 'expected_message', 'expected_status_code', 'expected_error_code', 'logger_calls', 'sentry_calls', ), [ ( Exception, 'I have erred and am unable to complete your request.', 500, 'internal_error', [call(Exception)], [call()], ), ( ClientError({'Error': {'Code': 'NoSuchKey'}}, ''), 'Oopsies, I don\'t know how to respond to that ¯\\_(ツ)_/¯', 400, 400, [], [], ), ( NonCallableMagicMock(code=random.randint(400, 499)), 'Oopsies, I don\'t know how to respond to that ¯\\_(ツ)_/¯', 400, 400, [call(Exception)], [call()], ), ], ) def test_exception_handler( mocker, exception, expected_message, expected_status_code, expected_error_code, logger_calls, sentry_calls, ): """Verify exception_Handler returns 500 status code and json payload.""" mocker.patch.object(sentry_sdk, 'capture_exception') with app.test_request_context(): g.log = MagicMock(exception=MagicMock()) server_response = base_handlers.exception_handler(exception) assert sentry_sdk.capture_exception.mock_calls == sentry_calls # assert status code is 500 assert server_response.status_code == expected_status_code # assert json payload response_message = json.loads(server_response.data.decode()) assert expected_message == response_message['message'] assert expected_error_code == response_message['code'] def test_health(): """Test health.""" with app.test_request_context(): result = base_handlers.health() assert result.status_code == OK assert result.json == {'status': 'ok'} test_get_vendor_and_subaccount_junk_resources = [ { 'type': 'junk asdfowq3g4oh', }, { 'type': 'junk asdfowq3g4oh', }, { 'type': 'junk asdfowq3asfdgg4oh', }, { 'type': 'junk asdfowq3asfdgg4oh', }, ] @pytest.mark.parametrize( ( 'test_description', 'ows_permissions_response_json', 'ows_permissions_response_status', 'expected_result', ), [ ( 'test subaccount context', { 'items': sorted( [ *test_get_vendor_and_subaccount_junk_resources, { 'type': permissions_constants.SUBACCOUNT_RESOURCE_TYPE, 'id': 235, 'vendor_id': 436326, }, ], key=lambda _: random.random(), ) }, OK, (436326, 235), ), ( 'test vendor context', { 'items': sorted( [ *test_get_vendor_and_subaccount_junk_resources, { 'type': permissions_constants.VENDOR_RESOURCE_TYPE, 'id': 123, }, ], key=lambda _: random.random(), ) }, OK, (123, None), ), ( 'test with new subaccount context', { 'items': sorted( [ { 'type': 'SubAccount', 'id': 235, 'vendor_id': 436326, }, *test_get_vendor_and_subaccount_junk_resources, ], key=lambda _: random.random(), ) }, OK, (436326, 235), ), ( 'test subaccount new and old context', { 'items': sorted( [ { 'type': 'SubAccount', 'id': 235, 'vendor_id': 436326, }, *test_get_vendor_and_subaccount_junk_resources, { 'type': permissions_constants.SUBACCOUNT_RESOURCE_TYPE, 'id': 235, 'vendor_id': 436326, }, ], key=lambda _: random.random(), ) }, OK, (436326, 235), ), ( 'test with all caps subaccount context', { 'items': sorted( [ { 'type': 'SUBACCOUNT', 'id': 235, 'vendor_id': 436326, }, *test_get_vendor_and_subaccount_junk_resources, ], key=lambda _: random.random(), ) }, OK, (436326, 235), ), ( 'test with bad context', { 'items': sorted( [*test_get_vendor_and_subaccount_junk_resources], key=lambda _: random.random(), ) }, OK, (None, None), ), ], ) def test_get_vendor_and_subaccount( test_description, ows_permissions_response_json, ows_permissions_response_status, expected_result, mocker, ): """Test get_vendor_and_subaccount.""" profile_type = 'abcasdf' profile_id = 12321 ows_permissions_mock = mock_request.get( service_constants.OWS_PERMISSIONS, service_constants.OWS_PERMISSIONS_PROFILE_RESOURCE.format( profile_type=profile_type, profile_id=profile_id, resource='label' ), ows_permissions_response_json, ows_permissions_response_status, ) with app.test_request_context(): g.request_context = MagicMock() flask_request.get_ows().correlation_id = 'some-correlation-id-abc-123' result = base_handlers.get_vendor_and_subaccount(profile_type, profile_id) assert result == expected_result assert ows_permissions_mock.called assert len(ows_permissions_mock.calls) == 1 assert ows_permissions_mock.calls[0]['method'] == 'GET' # TODO: split this test @pytest.mark.parametrize( ( 'test_description', 'context_type', 'identity_id', 'profile_id', 'profile_type', 'url_rule', 'view_args', 'args', 'vend_and_sub', 'expected_raise_exception_calls', 'expected_global_vendor', 'expected_global_subaccount', ), [ ( 'having no label context is fine', None, None, None, None, None, None, MagicMock(to_dict=lambda: {}), (None, None), [], None, None, ), ( 'missing profile_id is fine', None, None, None, 'applesauce bananas', None, None, MagicMock(to_dict=lambda: {}), (None, None), [], None, None, ), ( 'missing profile_type is fine', None, None, 'abc', None, None, None, MagicMock(to_dict=lambda: {}), (None, None), [], None, None, ), ( 'unsupported profile_type gives error', None, None, 'abc', 'applesauce bananas', None, None, MagicMock(to_dict=lambda: {}), (None, None), [call(Exception, 'Unsupported profile_type: applesauce bananas')], None, None, ), ( 'service artist search does not require label context', None, None, None, None, MagicMock( rule='/service//search', ), {'service': 'apple-music'}, MagicMock(to_dict=lambda: {}), (None, None), [], None, None, ), ( 'service artist by ID does not require label context', None, None, None, None, MagicMock( rule='/service//artist/', ), {'service': 'apple-music', 'artist_id': 'artist_id'}, MagicMock(to_dict=lambda: {}), (None, None), [], None, None, ), ( 'admin profile_type does not require label context', None, None, 'abc', owsrequest_headers.PROFILE_TYPE_ORCH_ADMIN, None, None, MagicMock(to_dict=lambda: {}), (None, None), [], None, None, ), ( 'podcast profile_type does not require label context', None, None, 'abc', owsrequest_headers.PROFILE_TYPE_PODCAST, None, None, MagicMock(to_dict=lambda: {}), (None, None), [], None, None, ), ( 'successful context retrieval for sub-account', None, None, 'abc', owsrequest_headers.PROFILE_TYPE_LABEL, None, {'service': 'apple-music'}, MagicMock(to_dict=lambda: {'vendor_id': 123, 'subaccount_id': 345}), (123, 345), [], 123, 345, ), ( 'successful context retrieval for D3', None, None, 'abc', owsrequest_headers.PROFILE_TYPE_LABEL, None, {'service': 'apple-music'}, MagicMock(to_dict=lambda: {'vendor_id': 123, 'subaccount_id': 0}), (123, None), [], 123, 0, ), ( 'missing label context when it is required', None, None, 'abc', owsrequest_headers.PROFILE_TYPE_LABEL, None, {'service': 'apple-music'}, MagicMock(to_dict=lambda: {'vendor_id': 123, 'subaccount_id': 345}), (None, None), [call(Exception, 'Missing label context.')], None, None, ), ( 'Query params contain different label or subaccount than the one logged in.', # noqa None, None, 'abc', owsrequest_headers.PROFILE_TYPE_LABEL, None, {'service': 'apple-music'}, MagicMock(to_dict=lambda: {}), (123, 345), [call(Exception, 'Not in requested label context.')], None, None, ), ( 'insights profile_type does not require label context', None, None, 'abc', owsrequest_headers.PROFILE_TYPE_INSIGHTS, None, None, MagicMock(to_dict=lambda: {}), (None, None), [], None, None, ), ( 'content profile_type does not require label context', None, None, 'abc', owsrequest_headers.PROFILE_TYPE_CONTENT, None, None, MagicMock(to_dict=lambda: {}), (None, None), [], None, None, ), ], ) def test_before_request( mocker, test_description, context_type, identity_id, profile_id, profile_type, url_rule, view_args, args, vend_and_sub, expected_raise_exception_calls, expected_global_vendor, expected_global_subaccount, ): """Test before_request.""" mocker.patch.object( context, 'get_request_context_from_headers', autospec=True, return_value=MagicMock( context_type=context_type, identity_id=identity_id, profile_id=profile_id, profile_type=profile_type, ), ) mocker.patch.object( exception, 'raise_exception', autospec=True, ) mocker.patch.object( base_handlers, 'get_vendor_and_subaccount', autospec=True, return_value=vend_and_sub, ) with app.test_request_context(): request.url_rule = url_rule request.view_args = view_args request.args = args base_handlers.before_request() request.args.to_dict() assert getattr(g, 'vendor_id', None) == expected_global_vendor assert getattr(g, 'subaccount_id', None) == expected_global_subaccount assert exception.raise_exception.mock_calls == expected_raise_exception_calls def test_before_request_oa_non_profile(): """Test before_request for Switchboard OA user (Non-profile auth).""" with app.test_request_context(): request.headers = {'Orchard-User-Id': 'oa:2075'} request.args = MagicMock(to_dict=lambda: {'vendor_id': 123, 'subaccount_id': 0}) base_handlers.before_request() assert getattr(g, 'vendor_id', None) == 123 assert getattr(g, 'subaccount_id', None) == 0 def test_after_request(): """Test after_request.""" assert base_handlers.after_request('something') == 'something' @pytest.mark.parametrize( ( 'url_rule', 'view_args', 'expected_result', ), [ ( MagicMock( rule='/service//search', ), {'service': 'apple-music'}, True, ), ( MagicMock( rule='/service//search', ), {'service': 'spotify'}, True, ), ( MagicMock( rule='/service//search', ), {'service': 'amazon'}, False, ), ( MagicMock( rule='/service//artist/', ), {'service': 'apple-music', 'artist_id': 'store-id'}, True, ), ( MagicMock( rule='/service//artist/', ), {'service': 'spotify', 'artist_id': 'store-id'}, True, ), ( MagicMock( rule='/service//artist/', ), {'service': 'amazon', 'artist_id': 'store-id'}, False, ), ( MagicMock( rule='/random/rule', ), {}, False, ), ], ) def test_is_public_request(url_rule, view_args, expected_result): """Test _is_public_request helper.""" with app.test_request_context(): request.url_rule = url_rule request.view_args = view_args result = base_handlers._is_public_request() assert result == expected_result