- Copy missing tests over from distgit
- Backport fix for CVE-2023-30861 Resolves: rhbz#2196683
This commit is contained in:
parent
3d0b1b5c5b
commit
d170540219
182
0004-Backport-Vary-Cookie-fix-from-upstream.patch
Normal file
182
0004-Backport-Vary-Cookie-fix-from-upstream.patch
Normal file
@ -0,0 +1,182 @@
|
||||
From 4195dc236d4286d6d82b3a40dc595a1437afcdc9 Mon Sep 17 00:00:00 2001
|
||||
From: "Brian C. Lane" <bcl@redhat.com>
|
||||
Date: Tue, 9 May 2023 15:13:34 -0700
|
||||
Subject: [PATCH 4/5] Backport Vary: Cookie fix from upstream
|
||||
|
||||
This fixes CVE-2023-30861 by backporting the patch and tests from this
|
||||
upstream commit:
|
||||
https://github.com/pallets/flask/commit/70f906c51ce49c485f1d355703e9cc3386b1cc2b
|
||||
|
||||
Resolves: rhbz#2196683
|
||||
---
|
||||
flask/sessions.py | 6 +++
|
||||
tests/test_basic.py | 90 ++++++++++++++++++++++++++++++++++++++++++++-
|
||||
tests/test_ext.py | 2 +-
|
||||
3 files changed, 95 insertions(+), 3 deletions(-)
|
||||
|
||||
diff --git a/flask/sessions.py b/flask/sessions.py
|
||||
index 525ff246..2f60e7eb 100644
|
||||
--- a/flask/sessions.py
|
||||
+++ b/flask/sessions.py
|
||||
@@ -338,6 +338,10 @@ class SecureCookieSessionInterface(SessionInterface):
|
||||
domain = self.get_cookie_domain(app)
|
||||
path = self.get_cookie_path(app)
|
||||
|
||||
+ # Add a "Vary: Cookie" header if the session was accessed at all.
|
||||
+ if session.accessed:
|
||||
+ response.vary.add("Cookie")
|
||||
+
|
||||
# Delete case. If there is no session we bail early.
|
||||
# If the session was modified to be empty we remove the
|
||||
# whole cookie.
|
||||
@@ -345,6 +349,7 @@ class SecureCookieSessionInterface(SessionInterface):
|
||||
if session.modified:
|
||||
response.delete_cookie(app.session_cookie_name,
|
||||
domain=domain, path=path)
|
||||
+ response.vary.add("Cookie")
|
||||
return
|
||||
|
||||
# Modification case. There are upsides and downsides to
|
||||
@@ -364,3 +369,4 @@ class SecureCookieSessionInterface(SessionInterface):
|
||||
response.set_cookie(app.session_cookie_name, val,
|
||||
expires=expires, httponly=httponly,
|
||||
domain=domain, path=path, secure=secure)
|
||||
+ response.vary.add("Cookie")
|
||||
diff --git a/tests/test_basic.py b/tests/test_basic.py
|
||||
index c5ec9f5c..85555300 100644
|
||||
--- a/tests/test_basic.py
|
||||
+++ b/tests/test_basic.py
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
import pytest
|
||||
|
||||
+import os
|
||||
import re
|
||||
import uuid
|
||||
import time
|
||||
@@ -196,7 +197,8 @@ def test_session():
|
||||
|
||||
@app.route('/get')
|
||||
def get():
|
||||
- return flask.session['value']
|
||||
+ v = flask.session.get("value", "None")
|
||||
+ return v
|
||||
|
||||
c = app.test_client()
|
||||
assert c.post('/set', data={'value': '42'}).data == b'value set'
|
||||
@@ -333,7 +335,7 @@ def test_session_expiration():
|
||||
client = app.test_client()
|
||||
rv = client.get('/')
|
||||
assert 'set-cookie' in rv.headers
|
||||
- match = re.search(r'\bexpires=([^;]+)(?i)', rv.headers['set-cookie'])
|
||||
+ match = re.search(r'(?i)\bexpires=([^;]+)', rv.headers['set-cookie'])
|
||||
expires = parse_date(match.group())
|
||||
expected = datetime.utcnow() + app.permanent_session_lifetime
|
||||
assert expires.year == expected.year
|
||||
@@ -445,6 +447,90 @@ def test_session_cookie_setting():
|
||||
run_test(expect_header=False)
|
||||
|
||||
|
||||
+def test_session_vary_cookie():
|
||||
+ app = flask.Flask("flask_test", root_path=os.path.dirname(__file__))
|
||||
+ app.config.update(
|
||||
+ TESTING=True,
|
||||
+ SECRET_KEY="test key",
|
||||
+ )
|
||||
+ client = app.test_client()
|
||||
+
|
||||
+ @app.route("/set")
|
||||
+ def set_session():
|
||||
+ flask.session["test"] = "test"
|
||||
+ return ""
|
||||
+ @app.route("/get")
|
||||
+ def get():
|
||||
+ return flask.session.get("test")
|
||||
+ @app.route("/getitem")
|
||||
+ def getitem():
|
||||
+ return flask.session["test"]
|
||||
+ @app.route("/setdefault")
|
||||
+ def setdefault():
|
||||
+ return flask.session.setdefault("test", "default")
|
||||
+
|
||||
+ @app.route("/clear")
|
||||
+ def clear():
|
||||
+ flask.session.clear()
|
||||
+ return ""
|
||||
+
|
||||
+ @app.route("/vary-cookie-header-set")
|
||||
+ def vary_cookie_header_set():
|
||||
+ response = flask.Response()
|
||||
+ response.vary.add("Cookie")
|
||||
+ flask.session["test"] = "test"
|
||||
+ return response
|
||||
+ @app.route("/vary-header-set")
|
||||
+ def vary_header_set():
|
||||
+ response = flask.Response()
|
||||
+ response.vary.update(("Accept-Encoding", "Accept-Language"))
|
||||
+ flask.session["test"] = "test"
|
||||
+ return response
|
||||
+ @app.route("/no-vary-header")
|
||||
+ def no_vary_header():
|
||||
+ return ""
|
||||
+ def expect(path, header_value="Cookie"):
|
||||
+ rv = client.get(path)
|
||||
+ if header_value:
|
||||
+ # The 'Vary' key should exist in the headers only once.
|
||||
+ assert len(rv.headers.get_all("Vary")) == 1
|
||||
+ assert rv.headers["Vary"] == header_value
|
||||
+ else:
|
||||
+ assert "Vary" not in rv.headers
|
||||
+ expect("/set")
|
||||
+ expect("/get")
|
||||
+ expect("/getitem")
|
||||
+ expect("/setdefault")
|
||||
+ expect("/clear")
|
||||
+ expect("/vary-cookie-header-set")
|
||||
+ expect("/vary-header-set", "Accept-Encoding, Accept-Language, Cookie")
|
||||
+ expect("/no-vary-header", None)
|
||||
+
|
||||
+
|
||||
+def test_session_refresh_vary():
|
||||
+ app = flask.Flask("flask_test", root_path=os.path.dirname(__file__))
|
||||
+ app.config.update(
|
||||
+ TESTING=True,
|
||||
+ SECRET_KEY="test key",
|
||||
+ )
|
||||
+ client = app.test_client()
|
||||
+
|
||||
+ @app.route("/login")
|
||||
+ def login():
|
||||
+ flask.session["user_id"] = 1
|
||||
+ flask.session.permanent = True
|
||||
+ return ""
|
||||
+
|
||||
+ @app.route("/ignored")
|
||||
+ def ignored():
|
||||
+ return ""
|
||||
+
|
||||
+ rv = client.get("/login")
|
||||
+ assert rv.headers["Vary"] == "Cookie"
|
||||
+ rv = client.get("/ignored")
|
||||
+ assert rv.headers["Vary"] == "Cookie"
|
||||
+
|
||||
+
|
||||
def test_flashes():
|
||||
app = flask.Flask(__name__)
|
||||
app.secret_key = 'testkey'
|
||||
diff --git a/tests/test_ext.py b/tests/test_ext.py
|
||||
index d336e404..c3e23cdf 100644
|
||||
--- a/tests/test_ext.py
|
||||
+++ b/tests/test_ext.py
|
||||
@@ -180,7 +180,7 @@ def test_no_error_swallowing(flaskext_broken):
|
||||
with pytest.raises(ImportError) as excinfo:
|
||||
import flask.ext.broken
|
||||
|
||||
- assert excinfo.type is ImportError
|
||||
+ assert excinfo.type in (ImportError, ModuleNotFoundError)
|
||||
if PY2:
|
||||
message = 'No module named missing_module'
|
||||
else:
|
||||
--
|
||||
2.40.1
|
||||
|
||||
107
0005-Backport-support-for-the-accessed-attribute.patch
Normal file
107
0005-Backport-support-for-the-accessed-attribute.patch
Normal file
@ -0,0 +1,107 @@
|
||||
From 640ff67e9d59d7acd786683bbab422d8aa17211c Mon Sep 17 00:00:00 2001
|
||||
From: "Brian C. Lane" <bcl@redhat.com>
|
||||
Date: Tue, 9 May 2023 15:36:11 -0700
|
||||
Subject: [PATCH 5/5] Backport support for the accessed attribute
|
||||
|
||||
This is added to the SessionMixin and SecureCookieSession to support
|
||||
fixing CVE-2023-30861.
|
||||
|
||||
Related: rhbz#2196683
|
||||
---
|
||||
flask/sessions.py | 40 +++++++++++++++++++++++++++++++++++++++-
|
||||
tests/test_basic.py | 8 ++++++++
|
||||
2 files changed, 47 insertions(+), 1 deletion(-)
|
||||
|
||||
diff --git a/flask/sessions.py b/flask/sessions.py
|
||||
index 2f60e7eb..278c3583 100644
|
||||
--- a/flask/sessions.py
|
||||
+++ b/flask/sessions.py
|
||||
@@ -48,6 +48,11 @@ class SessionMixin(object):
|
||||
#: The default mixin implementation just hardcodes ``True`` in.
|
||||
modified = True
|
||||
|
||||
+ #: Some implementations can detect when session data is read or
|
||||
+ #: written and set this when that happens. The mixin default is hard
|
||||
+ #: coded to ``True``.
|
||||
+ accessed = True
|
||||
+
|
||||
|
||||
def _tag(value):
|
||||
if isinstance(value, tuple):
|
||||
@@ -111,14 +116,47 @@ session_json_serializer = TaggedJSONSerializer()
|
||||
|
||||
|
||||
class SecureCookieSession(CallbackDict, SessionMixin):
|
||||
- """Base class for sessions based on signed cookies."""
|
||||
+ """Base class for sessions based on signed cookies.
|
||||
+
|
||||
+ This session backend will set the :attr:`modified` and
|
||||
+ :attr:`accessed` attributes. It cannot reliably track whether a
|
||||
+ session is new (vs. empty), so :attr:`new` remains hard coded to
|
||||
+ ``False``.
|
||||
+ """
|
||||
+
|
||||
+ #: When data is changed, this is set to ``True``. Only the session
|
||||
+ #: dictionary itself is tracked; if the session contains mutable
|
||||
+ #: data (for example a nested dict) then this must be set to
|
||||
+ #: ``True`` manually when modifying that data. The session cookie
|
||||
+ #: will only be written to the response if this is ``True``.
|
||||
+ modified = False
|
||||
+
|
||||
+ #: When data is read or written, this is set to ``True``. Used by
|
||||
+ # :class:`.SecureCookieSessionInterface` to add a ``Vary: Cookie``
|
||||
+ #: header, which allows caching proxies to cache different pages for
|
||||
+ #: different users.
|
||||
+ accessed = False
|
||||
+
|
||||
|
||||
def __init__(self, initial=None):
|
||||
def on_update(self):
|
||||
self.modified = True
|
||||
+ self.accessed = True
|
||||
CallbackDict.__init__(self, initial, on_update)
|
||||
self.modified = False
|
||||
|
||||
+ def __getitem__(self, key):
|
||||
+ self.accessed = True
|
||||
+ return super().__getitem__(key)
|
||||
+
|
||||
+ def get(self, key, default=None):
|
||||
+ self.accessed = True
|
||||
+ return super().get(key, default)
|
||||
+
|
||||
+ def setdefault(self, key, default=None):
|
||||
+ self.accessed = True
|
||||
+ return super().setdefault(key, default)
|
||||
+
|
||||
|
||||
class NullSession(SecureCookieSession):
|
||||
"""Class used to generate nicer error messages if sessions are not
|
||||
diff --git a/tests/test_basic.py b/tests/test_basic.py
|
||||
index 85555300..5ad8c3de 100644
|
||||
--- a/tests/test_basic.py
|
||||
+++ b/tests/test_basic.py
|
||||
@@ -192,12 +192,20 @@ def test_session():
|
||||
|
||||
@app.route('/set', methods=['POST'])
|
||||
def set():
|
||||
+ assert not flask.session.accessed
|
||||
+ assert not flask.session.modified
|
||||
flask.session['value'] = flask.request.form['value']
|
||||
+ assert flask.session.accessed
|
||||
+ assert flask.session.modified
|
||||
return 'value set'
|
||||
|
||||
@app.route('/get')
|
||||
def get():
|
||||
+ assert not flask.session.accessed
|
||||
+ assert not flask.session.modified
|
||||
v = flask.session.get("value", "None")
|
||||
+ assert flask.session.accessed
|
||||
+ assert not flask.session.modified
|
||||
return v
|
||||
|
||||
c = app.test_client()
|
||||
--
|
||||
2.40.1
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
|
||||
Name: python-%{modname}
|
||||
Version: 0.12.2
|
||||
Release: 4%{?dist}
|
||||
Release: 5%{?dist}
|
||||
Epoch: 1
|
||||
Summary: A micro-framework for Python based on Werkzeug, Jinja 2 and good intentions
|
||||
|
||||
@ -28,6 +28,11 @@ Patch0001: 0001-detect-UTF-encodings-when-loading-json.patch
|
||||
Patch0002: 0002-Fix-ValueError-for-some-invalid-Range-requests.patch
|
||||
Patch0003: 0003-be-smarter-about-adding-.cli-to-reloader-command.patch
|
||||
|
||||
# rhbz#2196683
|
||||
# Backport fix for CVE-2023-30861
|
||||
Patch0004: 0004-Backport-Vary-Cookie-fix-from-upstream.patch
|
||||
Patch0005: 0005-Backport-support-for-the-accessed-attribute.patch
|
||||
|
||||
BuildArch: noarch
|
||||
|
||||
%global _description \
|
||||
@ -71,7 +76,7 @@ Requires: python-itsdangerous
|
||||
%description -n python2-%{modname} %{_description}
|
||||
|
||||
Python 2 version.
|
||||
%endif # with python2
|
||||
%endif
|
||||
|
||||
%package -n python%{python3_pkgversion}-%{modname}
|
||||
Summary: %{summary}
|
||||
@ -108,7 +113,7 @@ rm -rf examples/minitwit/
|
||||
%build
|
||||
%if %{with python2}
|
||||
%py2_build
|
||||
%endif # with python2
|
||||
%endif
|
||||
%py3_build
|
||||
PYTHONPATH=`pwd` sphinx-build-3 -b html docs/ docs/_build/html/
|
||||
rm -rf docs/_build/html/{.buildinfo,.doctrees}
|
||||
@ -118,7 +123,7 @@ rm -rf docs/_build/html/{.buildinfo,.doctrees}
|
||||
%py2_install
|
||||
mv %{buildroot}%{_bindir}/%{modname}{,-%{python2_version}}
|
||||
ln -s %{modname}-%{python2_version} %{buildroot}%{_bindir}/%{modname}-2
|
||||
%endif # with python2
|
||||
%endif
|
||||
|
||||
%py3_install
|
||||
mv %{buildroot}%{_bindir}/%{modname}{,-%{python3_version}}
|
||||
@ -128,13 +133,13 @@ ln -s %{modname}-%{python3_version} %{buildroot}%{_bindir}/%{modname}-3
|
||||
ln -sf %{modname}-2 %{buildroot}%{_bindir}/%{modname}
|
||||
%else
|
||||
ln -sf %{modname}-3 %{buildroot}%{_bindir}/%{modname}
|
||||
%endif # with python2
|
||||
%endif
|
||||
|
||||
%check
|
||||
export LC_ALL=C.UTF-8
|
||||
%if %{with python2}
|
||||
PYTHONPATH=%{buildroot}%{python2_sitelib} py.test-%{python2_version} -v
|
||||
%endif # with python2
|
||||
%endif
|
||||
PYTHONPATH=%{buildroot}%{python3_sitelib} py.test-%{python3_version} -v || :
|
||||
|
||||
%if %{with python2}
|
||||
@ -147,7 +152,7 @@ PYTHONPATH=%{buildroot}%{python3_sitelib} py.test-%{python3_version} -v || :
|
||||
%{python2_sitelib}/%{modname}/
|
||||
|
||||
%{_bindir}/%{modname}
|
||||
%endif # with python2
|
||||
%endif
|
||||
|
||||
%files -n python%{python3_pkgversion}-%{modname}
|
||||
%license LICENSE
|
||||
@ -159,13 +164,18 @@ PYTHONPATH=%{buildroot}%{python3_sitelib} py.test-%{python3_version} -v || :
|
||||
|
||||
%if %{without python2}
|
||||
%{_bindir}/%{modname}
|
||||
%endif # without python2
|
||||
%endif
|
||||
|
||||
%files doc
|
||||
%license LICENSE
|
||||
%doc docs/_build/html examples
|
||||
|
||||
%changelog
|
||||
* Wed May 10 2023 Brian C. Lane <bcl@redhat.com> - 0.12.2-5
|
||||
- Copy missing tests over from distgit
|
||||
- Backport fix for CVE-2023-30861
|
||||
Resolves: rhbz#2196683
|
||||
|
||||
* Thu Nov 07 2019 Brian C. Lane <bcl@redhat.com> - 0.12.2-4
|
||||
- Add upstream changes from 0.12.4
|
||||
Resolves: rhbz#1585318
|
||||
|
||||
4
tests/scripts/run_tests.sh
Executable file
4
tests/scripts/run_tests.sh
Executable file
@ -0,0 +1,4 @@
|
||||
#!/usr/bin/bash
|
||||
set -eux
|
||||
# NOTE: test_helpers only contains the TestJSON class from upstream
|
||||
pytest-3 ./test_basic.py ./test_helpers.py
|
||||
4
tests/scripts/static/config.json
Normal file
4
tests/scripts/static/config.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"TEST_KEY": "foo",
|
||||
"SECRET_KEY": "devkey"
|
||||
}
|
||||
1
tests/scripts/static/index.html
Normal file
1
tests/scripts/static/index.html
Normal file
@ -0,0 +1 @@
|
||||
<h1>Hello World!</h1>
|
||||
1794
tests/scripts/test_basic.py
Normal file
1794
tests/scripts/test_basic.py
Normal file
File diff suppressed because it is too large
Load Diff
358
tests/scripts/test_helpers.py
Normal file
358
tests/scripts/test_helpers.py
Normal file
@ -0,0 +1,358 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
tests.helpers
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Various helpers.
|
||||
|
||||
:copyright: (c) 2015 by Armin Ronacher.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
import uuid
|
||||
import datetime
|
||||
|
||||
import flask
|
||||
from werkzeug.exceptions import BadRequest
|
||||
from werkzeug.http import http_date
|
||||
|
||||
from flask import json
|
||||
from flask._compat import StringIO, text_type
|
||||
|
||||
|
||||
def has_encoding(name):
|
||||
try:
|
||||
import codecs
|
||||
codecs.lookup(name)
|
||||
return True
|
||||
except LookupError:
|
||||
return False
|
||||
|
||||
|
||||
class TestJSON(object):
|
||||
@pytest.mark.parametrize('value', (
|
||||
1, 't', True, False, None,
|
||||
[], [1, 2, 3],
|
||||
{}, {'foo': u'🐍'},
|
||||
))
|
||||
@pytest.mark.parametrize('encoding', (
|
||||
'utf-8', 'utf-8-sig',
|
||||
'utf-16-le', 'utf-16-be', 'utf-16',
|
||||
'utf-32-le', 'utf-32-be', 'utf-32',
|
||||
))
|
||||
def test_detect_encoding(self, value, encoding):
|
||||
data = json.dumps(value).encode(encoding)
|
||||
assert json.detect_encoding(data) == encoding
|
||||
assert json.loads(data) == value
|
||||
|
||||
def test_ignore_cached_json(self):
|
||||
app = flask.Flask(__name__)
|
||||
with app.test_request_context('/', method='POST', data='malformed',
|
||||
content_type='application/json'):
|
||||
assert flask.request.get_json(silent=True, cache=True) is None
|
||||
with pytest.raises(BadRequest):
|
||||
flask.request.get_json(silent=False, cache=False)
|
||||
|
||||
def test_post_empty_json_adds_exception_to_response_content_in_debug(self):
|
||||
app = flask.Flask(__name__)
|
||||
app.config['DEBUG'] = True
|
||||
@app.route('/json', methods=['POST'])
|
||||
def post_json():
|
||||
flask.request.get_json()
|
||||
return None
|
||||
c = app.test_client()
|
||||
rv = c.post('/json', data=None, content_type='application/json')
|
||||
assert rv.status_code == 400
|
||||
assert b'Failed to decode JSON object' in rv.data
|
||||
|
||||
def test_post_empty_json_wont_add_exception_to_response_if_no_debug(self):
|
||||
app = flask.Flask(__name__)
|
||||
app.config['DEBUG'] = False
|
||||
@app.route('/json', methods=['POST'])
|
||||
def post_json():
|
||||
flask.request.get_json()
|
||||
return None
|
||||
c = app.test_client()
|
||||
rv = c.post('/json', data=None, content_type='application/json')
|
||||
assert rv.status_code == 400
|
||||
assert b'Failed to decode JSON object' not in rv.data
|
||||
|
||||
def test_json_bad_requests(self):
|
||||
app = flask.Flask(__name__)
|
||||
@app.route('/json', methods=['POST'])
|
||||
def return_json():
|
||||
return flask.jsonify(foo=text_type(flask.request.get_json()))
|
||||
c = app.test_client()
|
||||
rv = c.post('/json', data='malformed', content_type='application/json')
|
||||
assert rv.status_code == 400
|
||||
|
||||
def test_json_custom_mimetypes(self):
|
||||
app = flask.Flask(__name__)
|
||||
@app.route('/json', methods=['POST'])
|
||||
def return_json():
|
||||
return flask.request.get_json()
|
||||
c = app.test_client()
|
||||
rv = c.post('/json', data='"foo"', content_type='application/x+json')
|
||||
assert rv.data == b'foo'
|
||||
|
||||
def test_json_as_unicode(self):
|
||||
app = flask.Flask(__name__)
|
||||
|
||||
app.config['JSON_AS_ASCII'] = True
|
||||
with app.app_context():
|
||||
rv = flask.json.dumps(u'\N{SNOWMAN}')
|
||||
assert rv == '"\\u2603"'
|
||||
|
||||
app.config['JSON_AS_ASCII'] = False
|
||||
with app.app_context():
|
||||
rv = flask.json.dumps(u'\N{SNOWMAN}')
|
||||
assert rv == u'"\u2603"'
|
||||
|
||||
def test_json_dump_to_file(self):
|
||||
app = flask.Flask(__name__)
|
||||
test_data = {'name': 'Flask'}
|
||||
out = StringIO()
|
||||
|
||||
with app.app_context():
|
||||
flask.json.dump(test_data, out)
|
||||
out.seek(0)
|
||||
rv = flask.json.load(out)
|
||||
assert rv == test_data
|
||||
|
||||
@pytest.mark.parametrize('test_value', [0, -1, 1, 23, 3.14, 's', "longer string", True, False, None])
|
||||
def test_jsonify_basic_types(self, test_value):
|
||||
"""Test jsonify with basic types."""
|
||||
app = flask.Flask(__name__)
|
||||
c = app.test_client()
|
||||
|
||||
url = '/jsonify_basic_types'
|
||||
app.add_url_rule(url, url, lambda x=test_value: flask.jsonify(x))
|
||||
rv = c.get(url)
|
||||
assert rv.mimetype == 'application/json'
|
||||
assert flask.json.loads(rv.data) == test_value
|
||||
|
||||
def test_jsonify_dicts(self):
|
||||
"""Test jsonify with dicts and kwargs unpacking."""
|
||||
d = dict(
|
||||
a=0, b=23, c=3.14, d='t', e='Hi', f=True, g=False,
|
||||
h=['test list', 10, False],
|
||||
i={'test':'dict'}
|
||||
)
|
||||
app = flask.Flask(__name__)
|
||||
@app.route('/kw')
|
||||
def return_kwargs():
|
||||
return flask.jsonify(**d)
|
||||
@app.route('/dict')
|
||||
def return_dict():
|
||||
return flask.jsonify(d)
|
||||
c = app.test_client()
|
||||
for url in '/kw', '/dict':
|
||||
rv = c.get(url)
|
||||
assert rv.mimetype == 'application/json'
|
||||
assert flask.json.loads(rv.data) == d
|
||||
|
||||
def test_jsonify_arrays(self):
|
||||
"""Test jsonify of lists and args unpacking."""
|
||||
l = [
|
||||
0, 42, 3.14, 't', 'hello', True, False,
|
||||
['test list', 2, False],
|
||||
{'test':'dict'}
|
||||
]
|
||||
app = flask.Flask(__name__)
|
||||
@app.route('/args_unpack')
|
||||
def return_args_unpack():
|
||||
return flask.jsonify(*l)
|
||||
@app.route('/array')
|
||||
def return_array():
|
||||
return flask.jsonify(l)
|
||||
c = app.test_client()
|
||||
for url in '/args_unpack', '/array':
|
||||
rv = c.get(url)
|
||||
assert rv.mimetype == 'application/json'
|
||||
assert flask.json.loads(rv.data) == l
|
||||
|
||||
def test_jsonify_date_types(self):
|
||||
"""Test jsonify with datetime.date and datetime.datetime types."""
|
||||
test_dates = (
|
||||
datetime.datetime(1973, 3, 11, 6, 30, 45),
|
||||
datetime.date(1975, 1, 5)
|
||||
)
|
||||
app = flask.Flask(__name__)
|
||||
c = app.test_client()
|
||||
|
||||
for i, d in enumerate(test_dates):
|
||||
url = '/datetest{0}'.format(i)
|
||||
app.add_url_rule(url, str(i), lambda val=d: flask.jsonify(x=val))
|
||||
rv = c.get(url)
|
||||
assert rv.mimetype == 'application/json'
|
||||
assert flask.json.loads(rv.data)['x'] == http_date(d.timetuple())
|
||||
|
||||
def test_jsonify_uuid_types(self):
|
||||
"""Test jsonify with uuid.UUID types"""
|
||||
|
||||
test_uuid = uuid.UUID(bytes=b'\xDE\xAD\xBE\xEF' * 4)
|
||||
app = flask.Flask(__name__)
|
||||
url = '/uuid_test'
|
||||
app.add_url_rule(url, url, lambda: flask.jsonify(x=test_uuid))
|
||||
|
||||
c = app.test_client()
|
||||
rv = c.get(url)
|
||||
|
||||
rv_x = flask.json.loads(rv.data)['x']
|
||||
assert rv_x == str(test_uuid)
|
||||
rv_uuid = uuid.UUID(rv_x)
|
||||
assert rv_uuid == test_uuid
|
||||
|
||||
def test_json_attr(self):
|
||||
app = flask.Flask(__name__)
|
||||
@app.route('/add', methods=['POST'])
|
||||
def add():
|
||||
json = flask.request.get_json()
|
||||
return text_type(json['a'] + json['b'])
|
||||
c = app.test_client()
|
||||
rv = c.post('/add', data=flask.json.dumps({'a': 1, 'b': 2}),
|
||||
content_type='application/json')
|
||||
assert rv.data == b'3'
|
||||
|
||||
def test_template_escaping(self):
|
||||
app = flask.Flask(__name__)
|
||||
render = flask.render_template_string
|
||||
with app.test_request_context():
|
||||
rv = flask.json.htmlsafe_dumps('</script>')
|
||||
assert rv == u'"\\u003c/script\\u003e"'
|
||||
assert type(rv) == text_type
|
||||
rv = render('{{ "</script>"|tojson }}')
|
||||
assert rv == '"\\u003c/script\\u003e"'
|
||||
rv = render('{{ "<\0/script>"|tojson }}')
|
||||
assert rv == '"\\u003c\\u0000/script\\u003e"'
|
||||
rv = render('{{ "<!--<script>"|tojson }}')
|
||||
assert rv == '"\\u003c!--\\u003cscript\\u003e"'
|
||||
rv = render('{{ "&"|tojson }}')
|
||||
assert rv == '"\\u0026"'
|
||||
rv = render('{{ "\'"|tojson }}')
|
||||
assert rv == '"\\u0027"'
|
||||
rv = render("<a ng-data='{{ data|tojson }}'></a>",
|
||||
data={'x': ["foo", "bar", "baz'"]})
|
||||
assert rv == '<a ng-data=\'{"x": ["foo", "bar", "baz\\u0027"]}\'></a>'
|
||||
|
||||
def test_json_customization(self):
|
||||
class X(object):
|
||||
def __init__(self, val):
|
||||
self.val = val
|
||||
class MyEncoder(flask.json.JSONEncoder):
|
||||
def default(self, o):
|
||||
if isinstance(o, X):
|
||||
return '<%d>' % o.val
|
||||
return flask.json.JSONEncoder.default(self, o)
|
||||
class MyDecoder(flask.json.JSONDecoder):
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs.setdefault('object_hook', self.object_hook)
|
||||
flask.json.JSONDecoder.__init__(self, *args, **kwargs)
|
||||
def object_hook(self, obj):
|
||||
if len(obj) == 1 and '_foo' in obj:
|
||||
return X(obj['_foo'])
|
||||
return obj
|
||||
app = flask.Flask(__name__)
|
||||
app.testing = True
|
||||
app.json_encoder = MyEncoder
|
||||
app.json_decoder = MyDecoder
|
||||
@app.route('/', methods=['POST'])
|
||||
def index():
|
||||
return flask.json.dumps(flask.request.get_json()['x'])
|
||||
c = app.test_client()
|
||||
rv = c.post('/', data=flask.json.dumps({
|
||||
'x': {'_foo': 42}
|
||||
}), content_type='application/json')
|
||||
assert rv.data == b'"<42>"'
|
||||
|
||||
def test_modified_url_encoding(self):
|
||||
class ModifiedRequest(flask.Request):
|
||||
url_charset = 'euc-kr'
|
||||
app = flask.Flask(__name__)
|
||||
app.testing = True
|
||||
app.request_class = ModifiedRequest
|
||||
app.url_map.charset = 'euc-kr'
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
return flask.request.args['foo']
|
||||
|
||||
rv = app.test_client().get(u'/?foo=정상처리'.encode('euc-kr'))
|
||||
assert rv.status_code == 200
|
||||
assert rv.data == u'정상처리'.encode('utf-8')
|
||||
|
||||
if not has_encoding('euc-kr'):
|
||||
test_modified_url_encoding = None
|
||||
|
||||
def test_json_key_sorting(self):
|
||||
app = flask.Flask(__name__)
|
||||
app.testing = True
|
||||
assert app.config['JSON_SORT_KEYS'] == True
|
||||
d = dict.fromkeys(range(20), 'foo')
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
return flask.jsonify(values=d)
|
||||
|
||||
c = app.test_client()
|
||||
rv = c.get('/')
|
||||
lines = [x.strip() for x in rv.data.strip().decode('utf-8').splitlines()]
|
||||
sorted_by_str = [
|
||||
'{',
|
||||
'"values": {',
|
||||
'"0": "foo",',
|
||||
'"1": "foo",',
|
||||
'"10": "foo",',
|
||||
'"11": "foo",',
|
||||
'"12": "foo",',
|
||||
'"13": "foo",',
|
||||
'"14": "foo",',
|
||||
'"15": "foo",',
|
||||
'"16": "foo",',
|
||||
'"17": "foo",',
|
||||
'"18": "foo",',
|
||||
'"19": "foo",',
|
||||
'"2": "foo",',
|
||||
'"3": "foo",',
|
||||
'"4": "foo",',
|
||||
'"5": "foo",',
|
||||
'"6": "foo",',
|
||||
'"7": "foo",',
|
||||
'"8": "foo",',
|
||||
'"9": "foo"',
|
||||
'}',
|
||||
'}'
|
||||
]
|
||||
sorted_by_int = [
|
||||
'{',
|
||||
'"values": {',
|
||||
'"0": "foo",',
|
||||
'"1": "foo",',
|
||||
'"2": "foo",',
|
||||
'"3": "foo",',
|
||||
'"4": "foo",',
|
||||
'"5": "foo",',
|
||||
'"6": "foo",',
|
||||
'"7": "foo",',
|
||||
'"8": "foo",',
|
||||
'"9": "foo",',
|
||||
'"10": "foo",',
|
||||
'"11": "foo",',
|
||||
'"12": "foo",',
|
||||
'"13": "foo",',
|
||||
'"14": "foo",',
|
||||
'"15": "foo",',
|
||||
'"16": "foo",',
|
||||
'"17": "foo",',
|
||||
'"18": "foo",',
|
||||
'"19": "foo"',
|
||||
'}',
|
||||
'}'
|
||||
]
|
||||
|
||||
try:
|
||||
assert lines == sorted_by_int
|
||||
except AssertionError:
|
||||
assert lines == sorted_by_str
|
||||
15
tests/tests.yml
Normal file
15
tests/tests.yml
Normal file
@ -0,0 +1,15 @@
|
||||
---
|
||||
# Run a simple test
|
||||
- hosts: localhost
|
||||
roles:
|
||||
- role: standard-test-basic
|
||||
tags:
|
||||
- classic
|
||||
|
||||
required_packages:
|
||||
- python3-pytest
|
||||
|
||||
tests:
|
||||
- simple:
|
||||
dir: scripts
|
||||
run: ./run_tests.sh
|
||||
Loading…
Reference in New Issue
Block a user