From ffd2e40f68dc34d516dd51bab906af1ab04fa536 Mon Sep 17 00:00:00 2001 From: Steven Loria Date: Mon, 17 Sep 2018 17:54:40 -0400 Subject: [PATCH] Expose CommonTestCase in webargs.testing https://github.com/sloria/webargs/pull/287#issuecomment-421986846 --- CHANGELOG.rst | 8 ++ webargs/testing.py | 196 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 204 insertions(+) create mode 100644 webargs/testing.py diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 531f3d7f0..1b3a0857d 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,14 @@ Changelog --------- +4.1.0 (unreleased) +****************** + +Features: + +* Add ``webargs.testing`` module, which exposes ``CommonTestCase`` + to third-party parser libraries (see comments in :pr:`287`). + 4.0.0 (2018-07-15) ****************** diff --git a/webargs/testing.py b/webargs/testing.py new file mode 100644 index 000000000..935a4786a --- /dev/null +++ b/webargs/testing.py @@ -0,0 +1,196 @@ +# -*- coding: utf-8 -*- +"""Utilities for testing. Includes a base test class +for testing parsers. + +.. warning:: + + Methods and functions in this module may change without + warning and without a major version change. +""" +import json + +import pytest +import webtest + +from webargs.core import MARSHMALLOW_VERSION_INFO + + +class CommonTestCase(object): + """Base test class that defines test methods for common functionality across all + parsers. Subclasses must define `create_app`, which returns a WSGI-like app. + """ + + def create_app(self): + """Return a WSGI app""" + raise NotImplementedError("Must define create_app()") + + def create_testapp(self, app): + return webtest.TestApp(app) + + def before_create_app(self): + pass + + def after_create_app(self): + pass + + @pytest.fixture(scope="class") + def testapp(self): + self.before_create_app() + yield self.create_testapp(self.create_app()) + self.after_create_app() + + def test_parse_querystring_args(self, testapp): + assert testapp.get("/echo?name=Fred").json == {"name": "Fred"} + + def test_parse_querystring_with_query_location_specified(self, testapp): + assert testapp.get("/echo_query?name=Steve").json == {"name": "Steve"} + + def test_parse_form(self, testapp): + assert testapp.post("/echo", {"name": "Joe"}).json == {"name": "Joe"} + + def test_parse_json(self, testapp): + assert testapp.post_json("/echo", {"name": "Fred"}).json == {"name": "Fred"} + + def test_parse_querystring_default(self, testapp): + assert testapp.get("/echo").json == {"name": "World"} + + def test_parse_json_default(self, testapp): + assert testapp.post_json("/echo", {}).json == {"name": "World"} + + def test_parse_json_with_charset(self, testapp): + res = testapp.post( + "/echo", + json.dumps({"name": "Steve"}), + content_type="application/json;charset=UTF-8", + ) + assert res.json == {"name": "Steve"} + + def test_parse_json_with_vendor_media_type(self, testapp): + res = testapp.post( + "/echo", + json.dumps({"name": "Steve"}), + content_type="application/vnd.api+json;charset=UTF-8", + ) + assert res.json == {"name": "Steve"} + + def test_parse_json_ignores_extra_data(self, testapp): + assert testapp.post_json("/echo", {"extra": "data"}).json == {"name": "World"} + + def test_parse_json_blank(self, testapp): + assert testapp.post_json("/echo", None).json == {"name": "World"} + + def test_parse_json_ignore_unexpected_int(self, testapp): + assert testapp.post_json("/echo", 1).json == {"name": "World"} + + def test_parse_json_ignore_unexpected_list(self, testapp): + assert testapp.post_json("/echo", [{"extra": "data"}]).json == {"name": "World"} + + def test_parse_json_many_schema_invalid_input(self, testapp): + res = testapp.post_json( + "/echo_many_schema", [{"name": "a"}], expect_errors=True + ) + assert res.status_code == 422 + + def test_parse_json_many_schema(self, testapp): + res = testapp.post_json("/echo_many_schema", [{"name": "Steve"}]).json + assert res == [{"name": "Steve"}] + + def test_parse_json_many_schema_ignore_malformed_data(self, testapp): + assert testapp.post_json("/echo_many_schema", {"extra": "data"}).json == [] + + def test_parsing_form_default(self, testapp): + assert testapp.post("/echo", {}).json == {"name": "World"} + + def test_parse_querystring_multiple(self, testapp): + expected = {"name": ["steve", "Loria"]} + assert testapp.get("/echo_multi?name=steve&name=Loria").json == expected + + def test_parse_form_multiple(self, testapp): + expected = {"name": ["steve", "Loria"]} + assert ( + testapp.post("/echo_multi", {"name": ["steve", "Loria"]}).json == expected + ) + + def test_parse_json_list(self, testapp): + expected = {"name": ["Steve"]} + assert testapp.post_json("/echo_multi", {"name": "Steve"}).json == expected + + def test_parse_json_with_nonascii_chars(self, testapp): + text = u"øˆƒ£ºº∆ƒˆ∆" + assert testapp.post_json("/echo", {"name": text}).json == {"name": text} + + def test_validation_error_returns_422_response(self, testapp): + res = testapp.post("/echo", {"name": "b"}, expect_errors=True) + assert res.status_code == 422 + + def test_user_validation_error_returns_422_response_by_default(self, testapp): + res = testapp.post_json("/error", {"text": "foo"}, expect_errors=True) + assert res.status_code == 422 + + @pytest.mark.skipif( + MARSHMALLOW_VERSION_INFO < (2, 7), + reason="status_code only works in marshmallow>=2.7", + ) + def test_user_validation_error_with_status_code(self, testapp): + res = testapp.post_json("/error400", {"text": "foo"}, expect_errors=True) + assert res.status_code == 400 + + def test_use_args_decorator(self, testapp): + assert testapp.get("/echo_use_args?name=Fred").json == {"name": "Fred"} + + def test_use_args_with_path_param(self, testapp): + url = "/echo_use_args_with_path_param/foo" + res = testapp.get(url + "?value=42") + assert res.json == {"value": 42} + + def test_use_args_with_validation(self, testapp): + result = testapp.post("/echo_use_args_validated", {"value": 43}) + assert result.status_code == 200 + result = testapp.post( + "/echo_use_args_validated", {"value": 41}, expect_errors=True + ) + assert result.status_code == 422 + + def test_use_kwargs_decorator(self, testapp): + assert testapp.get("/echo_use_kwargs?name=Fred").json == {"name": "Fred"} + + def test_use_kwargs_with_path_param(self, testapp): + url = "/echo_use_kwargs_with_path_param/foo" + res = testapp.get(url + "?value=42") + assert res.json == {"value": 42} + + def test_parsing_headers(self, testapp): + res = testapp.get("/echo_headers", headers={"name": "Fred"}) + assert res.json == {"name": "Fred"} + + def test_parsing_cookies(self, testapp): + testapp.set_cookie("name", "Steve") + res = testapp.get("/echo_cookie") + assert res.json == {"name": "Steve"} + + def test_parse_nested_json(self, testapp): + res = testapp.post_json( + "/echo_nested", {"name": {"first": "Steve", "last": "Loria"}} + ) + assert res.json == {"name": {"first": "Steve", "last": "Loria"}} + + def test_parse_nested_many_json(self, testapp): + in_data = {"users": [{"id": 1, "name": "foo"}, {"id": 2, "name": "bar"}]} + res = testapp.post_json("/echo_nested_many", in_data) + assert res.json == in_data + + # Regression test for https://github.com/sloria/webargs/issues/120 + def test_parse_nested_many_missing(self, testapp): + in_data = {} + res = testapp.post_json("/echo_nested_many", in_data) + assert res.json == {} + + def test_parse_json_if_no_json(self, testapp): + res = testapp.post("/echo") + assert res.json == {"name": "World"} + + def test_parse_files(self, testapp): + res = testapp.post( + "/echo_file", {"myfile": webtest.Upload("README.rst", b"data")} + ) + assert res.json == {"myfile": "data"}