def test_handle_http_decorator_adds_param_annotations(mock_request,
                                                      mock_get_logic):
    """
    This test verifies if a decorator uses doctor.utils.add_param_annotations
    to add params to the logic function that we fail to validate if the added
    params are missing or invalid.
    """
    mock_request.method = 'GET'
    mock_request.content_type = 'application/x-www-form-urlencoded'
    mock_request.values = {'item_id': '1'}
    mock_handler = mock.Mock()
    logic = check_auth(add_doctor_attrs(get_item))

    expected_params = Params(all=['item_id', 'include_deleted', 'auth'],
                             optional=['include_deleted'],
                             required=['item_id', 'auth'],
                             logic=['item_id', 'include_deleted'])
    assert expected_params == logic._doctor_params
    with pytest.raises(HTTP400Exception, match='auth is required'):
        handle_http(mock_handler, (), {}, logic)

    # Add auth and it should validate
    mock_request.values = {'item_id': '1', 'auth': 'auth'}
    actual = handle_http(mock_handler, (), {}, logic)
    assert actual == ({'item_id': 1}, 200)
def test_handle_http_with_route_that_defines_req_obj_type(mock_request):
    def logic(foo: FooInstance):
        return foo

    logic = add_doctor_attrs(logic, req_obj_type=FooInstance)

    mock_request.method = 'POST'
    mock_request.content_type = 'application/json; charset=UTF8'
    mock_request.mimetype = 'application/json'
    mock_request.json = {
        'foo': 'A foo',
        'foo_id': 1,
        'bar': False,
    }
    mock_handler = mock.Mock()
    actual = handle_http(mock_handler, (), {}, logic)
    assert actual == ({'foo_id': 1, 'foo': 'A foo', 'bar': False}, 201)

    # Create a new type that doesn't allow additional properties.
    StrictFooInstance = new_type(FooInstance, additional_properties=False)

    # Redefine the logic using StrictFooInstance which doesn't allow additional
    # properties. The code shouldn't remove the additional 'bar' key, but it
    # should raise a 400 error.
    def logic(foo: StrictFooInstance):
        return foo

    logic = add_doctor_attrs(logic, req_obj_type=StrictFooInstance)
    expected = "__all__ - {'bar': 'Additional properties are not allowed.'}"
    with pytest.raises(HTTP400Exception, match=expected):
        handle_http(mock_handler, (), {}, logic)
def test_handle_http_response_type_validation(mock_app, mock_should,
                                              mock_request):
    """
    This tests that if we provide a type to our response that it validates
    the content of the response against that type.
    """
    def get_logic() -> Response[Item]:
        return Response({'foo': 'bar'})

    get_logic = add_doctor_attrs(get_logic)
    mock_should.return_value = True
    mock_app.config = {'DEBUG': False}

    mock_request.method = 'GET'
    mock_request.content_type = 'application/x-www-form-urlencoded'
    mock_request.values = {'item_id': '3'}
    mock_handler = mock.Mock()

    expected = ("{'item_id': 'This field is required.', "
                "'foo': 'Additional properties are not allowed.'}")
    with pytest.raises(HTTP400Exception, match=expected):
        handle_http(mock_handler, (), {}, get_logic)

    # Change the return value and validate it passes response validation.
    def get_logic() -> Response[Item]:
        return Response({'item_id': 1})

    get_logic = add_doctor_attrs(get_logic)
    actual = handle_http(mock_handler, (), {}, get_logic)
    assert ({'item_id': 1}, 200, None) == actual
def test_handle_http_missing_required_arg(mock_request, mock_get_logic):
    mock_request.method = 'GET'
    mock_request.content_type = 'application/x-www-form-urlencoded'
    mock_request.values = {}
    mock_handler = mock.Mock()

    with pytest.raises(HTTP400Exception, match='item_id is required'):
        handle_http(mock_handler, (), {}, mock_get_logic)
def test_handle_http_invalid_param(mock_request, mock_get_logic):
    mock_request.method = 'GET'
    mock_request.content_type = 'application/x-www-form-urlencoded'
    mock_request.values = {'item_id': 'string'}
    mock_handler = mock.Mock()

    expected_msg = r'item_id - value must be a valid type \(integer, null\)'
    with pytest.raises(HTTP400Exception, match=expected_msg):
        handle_http(mock_handler, (), {}, mock_get_logic)
def test_handle_http_multiple_invalid_args(mock_request, mock_post_logic):
    mock_request.method = 'POST'
    mock_request.content_type = 'application/json; charset=UTF8'
    mock_request.mimetype = 'application/json'
    mock_request.json = {'item': 1, 'colors': 'blue'}

    mock_handler = mock.Mock()
    expected_msg = ("{'item': 'Must be an object.', "
                    "'colors': 'Must be a list.'}")
    with pytest.raises(HTTP400Exception, match=expected_msg):
        handle_http(mock_handler, (), {}, mock_post_logic)
def test_handle_http_missing_multiple_required_args(mock_request,
                                                    mock_post_logic):
    mock_request.method = 'POST'
    mock_request.content_type = 'application/json; charset=UTF8'
    mock_request.mimetype = 'application/json'
    mock_request.json = {}

    mock_handler = mock.Mock()
    with pytest.raises(HTTP400Exception,
                       match=r"\['item', 'colors'\] are required."):
        handle_http(mock_handler, (), {}, mock_post_logic)
def test_handle_http_allowed_exception(mock_app, mock_request, mock_get_logic):
    mock_app.config = {'DEBUG': False}
    mock_request.method = 'GET'
    mock_request.content_type = 'application/x-www-form-urlencoded'
    mock_request.values = {'item_id': '1'}
    mock_handler = mock.Mock()
    mock_get_logic.side_effect = ValueError('Allowed')
    mock_get_logic._doctor_allowed_exceptions = [ValueError]

    with pytest.raises(ValueError, match='Allowed'):
        handle_http(mock_handler, (), {}, mock_get_logic)
def test_handle_http_object_array_types(mock_request, mock_post_logic):
    """
    This test verifies that we pass native types to the logic function rather
    than the doctor types that annotate the parameters of the logic function.
    This is done to prevent downstream issues from happening by passing say
    an Integer instance instead of an int to pymysql.  pymysql doesn't know
    about this type and tries to escape it like a string which causes an
    AttributeError.
    """
    mock_request.method = 'POST'
    mock_request.content_type = 'application/json; charset=UTF8'
    mock_request.mimetype = 'application/json'
    mock_request.json = {'item': {'item_id': 1}, 'colors': ['blue', 'green']}

    mock_handler = mock.Mock()
    actual = handle_http(mock_handler, (), {}, mock_post_logic)
    assert actual == ({'item_id': 1}, 201)

    expected_call = mock.call(item={'item_id': 1}, colors=['blue', 'green'])
    assert expected_call == mock_post_logic.call_args

    # Verify we're passing native types and not doctor types to logic functions
    kwargs = mock_post_logic.call_args[1]
    assert type(kwargs['item']) is dict
    assert type(kwargs['colors']) is list
Example #10
0
 def test_handle_http_no_schemas(self):
     result = handle_http(self.schema, self.mock_handler, (1, 2),
                          {'a': 3, 'b': 4}, self.mock_logic, None, None,
                          None, None)
     self.assertEqual(result, ({'foo': 1}, 200))
     self.assertEqual(self.mock_logic.call_args_list,
                      [mock.call(1, 2, a=3, b=4)])
Example #11
0
 def call_handle_http(self, args=None, kwargs=None):
     if args is None:
         args = ()
     if kwargs is None:
         kwargs = {}
     return handle_http(self.schema, self.mock_handler, args, kwargs,
                        self.mock_logic, self.request_schema,
                        self.request_validator, self.response_validator,
                        self.allowed_exceptions)
def test_handle_http_response_validation(mock_app, mock_request,
                                         mock_get_logic):
    mock_app.config = {'DEBUG': False}

    mock_request.method = 'GET'
    mock_request.content_type = 'application/x-www-form-urlencoded'
    mock_request.values = {'item_id': '3'}
    mock_handler = mock.Mock()
    mock_get_logic.return_value = {'foo': 'bar'}

    expected = ("{'item_id': 'This field is required.', "
                "'foo': 'Additional properties are not allowed.'}")
    with pytest.raises(HTTP400Exception, match=expected):
        handle_http(mock_handler, (), {}, mock_get_logic)

    # Should also work with the response is an instance of Response
    mock_get_logic.return_value = Response({'foo': 'bar'})
    with pytest.raises(HTTP400Exception, match=expected):
        handle_http(mock_handler, (), {}, mock_get_logic)
def test_handle_http_with_logic_containing_uniontype(mock_request):
    """
    This test verifies that if our logic function has a UnionType annotation
    that we handle it properly with different inputs.
    """
    def logic(val: ColorsOrObject):
        return val

    logic = add_doctor_attrs(logic)
    mock_request.method = 'POST'
    mock_request.content_type = 'application/x-www-form-urlencoded'
    mock_request.values = {'val': '["blue"]'}
    mock_handler = mock.Mock()
    actual = handle_http(mock_handler, (), {}, logic)
    assert actual == (['blue'], 201)

    mock_request.values = {'val': '{"str": "auth"}'}
    actual = handle_http(mock_handler, (), {}, logic)
    assert actual == ({'str': 'auth'}, 201)
def test_handle_http_non_json(mock_request, mock_get_logic):
    mock_request.method = 'GET'
    mock_request.content_type = 'application/x-www-form-urlencoded'
    mock_request.values = {'item_id': '3'}
    mock_handler = mock.Mock()

    actual = handle_http(mock_handler, (), {}, mock_get_logic)
    assert actual == ({'item_id': 1}, 200)

    expected_call = mock.call(item_id=3)
    assert expected_call == mock_get_logic.call_args
def test_handle_http_response_instance_return_value(mock_request,
                                                    mock_get_logic):
    mock_request.method = 'GET'
    mock_request.content_type = 'application/json; charset=UTF8'
    mock_request.mimetype = 'application/json'
    mock_request.values = {'item_id': '3'}
    mock_get_logic.return_value = Response({'item_id': 3}, {'X-Header': 'Foo'},
                                           status_code=202)
    mock_handler = mock.Mock()

    actual = handle_http(mock_handler, (), {}, mock_get_logic)
    assert actual == ({'item_id': 3}, 202, {'X-Header': 'Foo'})
def test_handle_http_unsupported_http_method_with_body(mock_request,
                                                       mock_get_logic):
    mock_request.method = 'GET'
    mock_request.content_type = 'application/json; charset=UTF8'
    mock_request.mimetype = 'application/json'
    mock_request.values = {'item_id': '3'}
    mock_handler = mock.Mock()

    actual = handle_http(mock_handler, (), {}, mock_get_logic)
    assert actual == ({'item_id': 1}, 200)

    expected_call = mock.call(item_id=3)
    assert expected_call == mock_get_logic.call_args
def test_handle_http_with_json(mock_request, mock_post_logic):
    mock_request.method = 'POST'
    mock_request.content_type = 'application/json; charset=UTF8'
    mock_request.mimetype = 'application/json'
    mock_request.json = {
        'item': {
            'item_id': 1,
        },
        'colors': ['blue'],
        'optional_id': None,
        'location.lat': 45.2342343,
    }
    mock_handler = mock.Mock()

    actual = handle_http(mock_handler, (), {}, mock_post_logic)
    assert actual == ({'item_id': 1}, 201)

    expected_call = mock.call(item={'item_id': 1},
                              colors=['blue'],
                              optional_id=None,
                              lat=45.2342343)
    assert expected_call == mock_post_logic.call_args
def test_handle_http_http_errors(mock_app, mock_should, mock_request,
                                 mock_get_logic):
    mock_app.config = {'DEBUG': False}

    mock_request.method = 'GET'
    mock_request.content_type = 'application/x-www-form-urlencoded'
    mock_request.values = {'item_id': '3'}
    mock_handler = mock.Mock()

    # 400
    mock_get_logic.side_effect = InvalidValueError('invalid')
    with pytest.raises(HTTP400Exception, match='invalid'):
        handle_http(mock_handler, (), {}, mock_get_logic)

    # 401
    mock_get_logic.side_effect = UnauthorizedError('unauth')
    with pytest.raises(HTTP401Exception, match='unauth'):
        handle_http(mock_handler, (), {}, mock_get_logic)

    # 403
    mock_get_logic.side_effect = ForbiddenError('forbidden')
    with pytest.raises(HTTP403Exception, match='forbidden'):
        handle_http(mock_handler, (), {}, mock_get_logic)

    # 404
    mock_get_logic.side_effect = NotFoundError('not found')
    with pytest.raises(HTTP404Exception, match='not found'):
        handle_http(mock_handler, (), {}, mock_get_logic)

    # 409
    mock_get_logic.side_effect = ImmutableError('immutable')
    with pytest.raises(HTTP409Exception, match='immutable'):
        handle_http(mock_handler, (), {}, mock_get_logic)

    # 500
    mock_get_logic.side_effect = Exception('internal error')
    with pytest.raises(HTTP500Exception, match='Uncaught error in logic func'):
        handle_http(mock_handler, (), {}, mock_get_logic)

    # 500 in debug mode
    mock_app.config = {'DEBUG': True}
    with pytest.raises(Exception, match='internal error'):
        handle_http(mock_handler, (), {}, mock_get_logic)