- 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}
|
Name: python-%{modname}
|
||||||
Version: 0.12.2
|
Version: 0.12.2
|
||||||
Release: 4%{?dist}
|
Release: 5%{?dist}
|
||||||
Epoch: 1
|
Epoch: 1
|
||||||
Summary: A micro-framework for Python based on Werkzeug, Jinja 2 and good intentions
|
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
|
Patch0002: 0002-Fix-ValueError-for-some-invalid-Range-requests.patch
|
||||||
Patch0003: 0003-be-smarter-about-adding-.cli-to-reloader-command.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
|
BuildArch: noarch
|
||||||
|
|
||||||
%global _description \
|
%global _description \
|
||||||
@ -71,7 +76,7 @@ Requires: python-itsdangerous
|
|||||||
%description -n python2-%{modname} %{_description}
|
%description -n python2-%{modname} %{_description}
|
||||||
|
|
||||||
Python 2 version.
|
Python 2 version.
|
||||||
%endif # with python2
|
%endif
|
||||||
|
|
||||||
%package -n python%{python3_pkgversion}-%{modname}
|
%package -n python%{python3_pkgversion}-%{modname}
|
||||||
Summary: %{summary}
|
Summary: %{summary}
|
||||||
@ -108,7 +113,7 @@ rm -rf examples/minitwit/
|
|||||||
%build
|
%build
|
||||||
%if %{with python2}
|
%if %{with python2}
|
||||||
%py2_build
|
%py2_build
|
||||||
%endif # with python2
|
%endif
|
||||||
%py3_build
|
%py3_build
|
||||||
PYTHONPATH=`pwd` sphinx-build-3 -b html docs/ docs/_build/html/
|
PYTHONPATH=`pwd` sphinx-build-3 -b html docs/ docs/_build/html/
|
||||||
rm -rf docs/_build/html/{.buildinfo,.doctrees}
|
rm -rf docs/_build/html/{.buildinfo,.doctrees}
|
||||||
@ -118,7 +123,7 @@ rm -rf docs/_build/html/{.buildinfo,.doctrees}
|
|||||||
%py2_install
|
%py2_install
|
||||||
mv %{buildroot}%{_bindir}/%{modname}{,-%{python2_version}}
|
mv %{buildroot}%{_bindir}/%{modname}{,-%{python2_version}}
|
||||||
ln -s %{modname}-%{python2_version} %{buildroot}%{_bindir}/%{modname}-2
|
ln -s %{modname}-%{python2_version} %{buildroot}%{_bindir}/%{modname}-2
|
||||||
%endif # with python2
|
%endif
|
||||||
|
|
||||||
%py3_install
|
%py3_install
|
||||||
mv %{buildroot}%{_bindir}/%{modname}{,-%{python3_version}}
|
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}
|
ln -sf %{modname}-2 %{buildroot}%{_bindir}/%{modname}
|
||||||
%else
|
%else
|
||||||
ln -sf %{modname}-3 %{buildroot}%{_bindir}/%{modname}
|
ln -sf %{modname}-3 %{buildroot}%{_bindir}/%{modname}
|
||||||
%endif # with python2
|
%endif
|
||||||
|
|
||||||
%check
|
%check
|
||||||
export LC_ALL=C.UTF-8
|
export LC_ALL=C.UTF-8
|
||||||
%if %{with python2}
|
%if %{with python2}
|
||||||
PYTHONPATH=%{buildroot}%{python2_sitelib} py.test-%{python2_version} -v
|
PYTHONPATH=%{buildroot}%{python2_sitelib} py.test-%{python2_version} -v
|
||||||
%endif # with python2
|
%endif
|
||||||
PYTHONPATH=%{buildroot}%{python3_sitelib} py.test-%{python3_version} -v || :
|
PYTHONPATH=%{buildroot}%{python3_sitelib} py.test-%{python3_version} -v || :
|
||||||
|
|
||||||
%if %{with python2}
|
%if %{with python2}
|
||||||
@ -147,7 +152,7 @@ PYTHONPATH=%{buildroot}%{python3_sitelib} py.test-%{python3_version} -v || :
|
|||||||
%{python2_sitelib}/%{modname}/
|
%{python2_sitelib}/%{modname}/
|
||||||
|
|
||||||
%{_bindir}/%{modname}
|
%{_bindir}/%{modname}
|
||||||
%endif # with python2
|
%endif
|
||||||
|
|
||||||
%files -n python%{python3_pkgversion}-%{modname}
|
%files -n python%{python3_pkgversion}-%{modname}
|
||||||
%license LICENSE
|
%license LICENSE
|
||||||
@ -159,13 +164,18 @@ PYTHONPATH=%{buildroot}%{python3_sitelib} py.test-%{python3_version} -v || :
|
|||||||
|
|
||||||
%if %{without python2}
|
%if %{without python2}
|
||||||
%{_bindir}/%{modname}
|
%{_bindir}/%{modname}
|
||||||
%endif # without python2
|
%endif
|
||||||
|
|
||||||
%files doc
|
%files doc
|
||||||
%license LICENSE
|
%license LICENSE
|
||||||
%doc docs/_build/html examples
|
%doc docs/_build/html examples
|
||||||
|
|
||||||
%changelog
|
%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
|
* Thu Nov 07 2019 Brian C. Lane <bcl@redhat.com> - 0.12.2-4
|
||||||
- Add upstream changes from 0.12.4
|
- Add upstream changes from 0.12.4
|
||||||
Resolves: rhbz#1585318
|
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