Compare commits
	
		
			17 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| abe90d3483 | |||
| 65e7dcdf6d | |||
| 74e414badf | |||
| 69175979ff | |||
| 2f69e0f215 | |||
| 961385c389 | |||
| a691cfd2da | |||
| 482a0faa23 | |||
| 0ecf7b6617 | |||
| 4408bbfc82 | |||
| 433110f486 | |||
| 0b1dd4f4fc | |||
| 46bd27e126 | |||
| b718d19450 | |||
| 2969e83afe | |||
| a732656746 | |||
| 1daed940b6 | 
| @@ -2,6 +2,39 @@ Change Log | ||||
| ========== | ||||
|  | ||||
|  | ||||
| Version 1.5.4 | ||||
| ------------- | ||||
|  | ||||
| Released 2023/5/18 | ||||
|  | ||||
| Security fixes. | ||||
|  | ||||
| * Added safeguard to the next URI utilities, to prevent Cross-Site | ||||
|   Scripting (XSS) attacks. | ||||
| * Applied the safe next URI utilities to the test site. | ||||
| * Added the ``SameSite`` and ``Secure`` flags to the session cookie | ||||
|   of the test site. | ||||
|  | ||||
|  | ||||
| Version 1.5.3 | ||||
| ------------- | ||||
|  | ||||
| Released 2023/4/30 | ||||
|  | ||||
| * Fixed the error of the net balance in the unmatched offset list. | ||||
| * Revised the original line item editor not to override the existing | ||||
|   amount when the existing amount is less or equal to the net | ||||
|   balance. | ||||
|  | ||||
|  | ||||
| Version 1.5.2 | ||||
| ------------- | ||||
|  | ||||
| Released 2023/4/30 | ||||
|  | ||||
| * Fixed the error of the net balance in the unmatched offset list. | ||||
|  | ||||
|  | ||||
| Version 1.5.1 | ||||
| ------------- | ||||
|  | ||||
|   | ||||
| @@ -50,9 +50,9 @@ The following front-end JavaScript libraries must be loaded.  You may | ||||
| download it locally or use CDN_. | ||||
|  | ||||
| * Bootstrap_ 5.2.3 or above | ||||
| * FontAwesome_ 6.2.1 or above | ||||
| * `Decimal.js`_ 6.4.3 or above | ||||
| * `Tempus-Dominus`_ 6.4.3 or above | ||||
| * FontAwesome_ 6.4.0 or above | ||||
| * `decimal.js`_ 10.4.3 or above, or `decimal.js-light`_ 2.5.1 or above. | ||||
| * `Tempus-Dominus`_ 6.7.7 or above | ||||
|  | ||||
|  | ||||
| Configuration | ||||
| @@ -114,6 +114,7 @@ Check your Flask application and see how it works. | ||||
| .. _CDN: https://en.wikipedia.org/wiki/Content_delivery_network | ||||
| .. _Bootstrap: https://getbootstrap.com | ||||
| .. _FontAwesome: https://fontawesome.com | ||||
| .. _Decimal.js: https://mikemcl.github.io/decimal.js | ||||
| .. _decimal.js: https://mikemcl.github.io/decimal.js | ||||
| .. _decimal.js-light: https://mikemcl.github.io/decimal.js-light | ||||
| .. _Tempus-Dominus: https://getdatepicker.com | ||||
| .. _Bootstrap navigation bar: https://getbootstrap.com/docs/5.3/components/navbar/ | ||||
|   | ||||
| @@ -24,7 +24,7 @@ from flask_sqlalchemy import SQLAlchemy | ||||
|  | ||||
| from accounting.utils.user import UserUtilityInterface | ||||
|  | ||||
| VERSION: str = "1.5.1" | ||||
| VERSION: str = "1.5.4" | ||||
| """The package version.""" | ||||
| db: SQLAlchemy = SQLAlchemy() | ||||
| """The database instance.""" | ||||
|   | ||||
| @@ -182,6 +182,8 @@ class Account(db.Model): | ||||
|         :param value: The new title. | ||||
|         :return: None. | ||||
|         """ | ||||
|         if self.title == value: | ||||
|             return | ||||
|         if self.title_l10n is None: | ||||
|             self.title_l10n = value | ||||
|             return | ||||
| @@ -424,6 +426,8 @@ class Currency(db.Model): | ||||
|         :param value: The new name. | ||||
|         :return: None. | ||||
|         """ | ||||
|         if self.name == value: | ||||
|             return | ||||
|         if self.name_l10n is None: | ||||
|             self.name_l10n = value | ||||
|             return | ||||
|   | ||||
| @@ -123,15 +123,13 @@ class OffsetMatcher: | ||||
|             .options(selectinload(JournalEntryLineItem.currency), | ||||
|                      selectinload(JournalEntryLineItem.journal_entry)).all() | ||||
|         for line_item in self.line_items: | ||||
|             line_item.is_offset = line_item.id in net_balances | ||||
|         self.unapplied = [x for x in self.line_items | ||||
|                           if x.is_offset] | ||||
|             line_item.is_offset = line_item.id not in net_balances | ||||
|         self.unapplied = [x for x in self.line_items if not x.is_offset] | ||||
|         for line_item in self.unapplied: | ||||
|             line_item.net_balance = line_item.amount \ | ||||
|                 if net_balances[line_item.id] is None \ | ||||
|                 else net_balances[line_item.id] | ||||
|         self.unmatched = [x for x in self.line_items | ||||
|                           if not x.is_offset] | ||||
|         self.unmatched = [x for x in self.line_items if x.is_offset] | ||||
|         self.__populate_accumulated_balances() | ||||
|  | ||||
|     def __populate_accumulated_balances(self) -> None: | ||||
|   | ||||
| @@ -290,7 +290,9 @@ class JournalEntryLineItemEditor { | ||||
|         this.account = originalLineItem.account.copy(); | ||||
|         this.isAccountConfirmed = false; | ||||
|         this.#accountText.innerText = this.account.text; | ||||
|         if (this.#amountInput.value === "" || new Decimal(this.#amountInput.value).greaterThan(originalLineItem.netBalance)) { | ||||
|             this.#amountInput.value = String(originalLineItem.netBalance); | ||||
|         } | ||||
|         this.#amountInput.max = String(originalLineItem.netBalance); | ||||
|         this.#amountInput.min = "0"; | ||||
|         this.#validate(); | ||||
|   | ||||
| @@ -32,7 +32,7 @@ First written: 2023/1/30 | ||||
|       {{ A_("New") }} | ||||
|     </a> | ||||
|   {% endif %} | ||||
|   <form class="btn btn-primary d-flex input-group" action="{{ url_for("accounting.account.list") }}" method="get" role="search" aria-labelledby="accounting-toolbar-search-label"> | ||||
|   <form class="btn btn-primary d-flex input-group" name="accounting-search-form" action="{{ url_for("accounting.account.list") }}" method="get" role="search" aria-labelledby="accounting-toolbar-search-label"> | ||||
|     <input id="accounting-toolbar-search" class="form-control form-control-sm" type="search" name="q" value="{{ request.args.q }}" placeholder=" " required="required"> | ||||
|     <label id="accounting-toolbar-search-label" for="accounting-toolbar-search" class="input-group-text"> | ||||
|       <button type="submit"> | ||||
|   | ||||
| @@ -26,7 +26,7 @@ First written: 2023/1/26 | ||||
| {% block content %} | ||||
|  | ||||
| <div class="mb-2 accounting-toolbar"> | ||||
|   <form class="btn btn-primary d-flex input-group" action="{{ url_for("accounting.base-account.list") }}" method="get" role="search" aria-labelledby="accounting-toolbar-search-label"> | ||||
|   <form class="btn btn-primary d-flex input-group" name="accounting-search-form" action="{{ url_for("accounting.base-account.list") }}" method="get" role="search" aria-labelledby="accounting-toolbar-search-label"> | ||||
|     <input id="accounting-toolbar-search" class="form-control form-control-sm" type="search" name="q" value="{{ request.args.q }}" placeholder=" " required="required"> | ||||
|     <label id="accounting-toolbar-search-label" for="accounting-toolbar-search" class="input-group-text"> | ||||
|       <button type="submit"> | ||||
|   | ||||
| @@ -32,7 +32,7 @@ First written: 2023/2/6 | ||||
|       {{ A_("New") }} | ||||
|     </a> | ||||
|   {% endif %} | ||||
|   <form class="btn btn-primary d-flex input-group" action="{{ url_for("accounting.currency.list") }}" method="get" role="search" aria-labelledby="accounting-toolbar-search-label"> | ||||
|   <form class="btn btn-primary d-flex input-group" name="accounting-search-form" action="{{ url_for("accounting.currency.list") }}" method="get" role="search" aria-labelledby="accounting-toolbar-search-label"> | ||||
|     <input id="accounting-toolbar-search" class="form-control form-control-sm" type="search" name="q" value="{{ request.args.q }}" placeholder=" " required="required"> | ||||
|     <label id="accounting-toolbar-search-label" for="accounting-toolbar-search" class="input-group-text"> | ||||
|       <button type="submit"> | ||||
|   | ||||
| @@ -19,7 +19,7 @@ description-editor-modal.html: The modal of the description editor | ||||
| Author: imacat@mail.imacat.idv.tw (imacat) | ||||
| First written: 2023/2/28 | ||||
| #} | ||||
| <form id="accounting-description-editor-{{ description_editor.debit_credit }}" class="accounting-description-editor" data-debit-credit="{{ description_editor.debit_credit }}"> | ||||
| <form id="accounting-description-editor-{{ description_editor.debit_credit }}" class="accounting-description-editor" name="accounting-dummy-form" data-debit-credit="{{ description_editor.debit_credit }}"> | ||||
|   <div id="accounting-description-editor-{{ description_editor.debit_credit }}-modal" class="modal fade" tabindex="-1" aria-labelledby="accounting-description-editor-{{ description_editor.debit_credit }}-modal-label" aria-hidden="true"> | ||||
|     <div class="modal-dialog"> | ||||
|       <div class="modal-content"> | ||||
|   | ||||
| @@ -19,7 +19,7 @@ journal-entry-line-item-editor-modal: The modal of the journal entry line item e | ||||
| Author: imacat@mail.imacat.idv.tw (imacat) | ||||
| First written: 2023/2/25 | ||||
| #} | ||||
| <form id="accounting-line-item-editor"> | ||||
| <form id="accounting-line-item-editor" name="accounting-dummy-form"> | ||||
|   <div id="accounting-line-item-editor-modal" class="modal fade" tabindex="-1" aria-labelledby="accounting-line-item-editor-modal-label" aria-hidden="true"> | ||||
|     <div class="modal-dialog"> | ||||
|       <div class="modal-content"> | ||||
|   | ||||
| @@ -19,7 +19,7 @@ recurring-item-editor-modal.html: The modal of the recurring item editor | ||||
| Author: imacat@mail.imacat.idv.tw (imacat) | ||||
| First written: 2023/3/22 | ||||
| #} | ||||
| <form id="accounting-recurring-item-editor-{{ expense_income }}"> | ||||
| <form id="accounting-recurring-item-editor-{{ expense_income }}" name="accounting-dummy-form"> | ||||
|   <div id="accounting-recurring-item-editor-{{ expense_income }}-modal" class="modal fade" tabindex="-1" aria-labelledby="accounting-recurring-item-editor-{{ expense_income }}-modal-label" aria-hidden="true"> | ||||
|     <div class="modal-dialog"> | ||||
|       <div class="modal-content"> | ||||
|   | ||||
| @@ -19,7 +19,7 @@ search-modal.html: The search modal | ||||
| Author: imacat@mail.imacat.idv.tw (imacat) | ||||
| First written: 2023/3/8 | ||||
| #} | ||||
| <form action="{{ url_for("accounting-report.search") }}" method="get" role="search" aria-labelledby="accounting-search-modal-label"> | ||||
| <form action="{{ url_for("accounting-report.search") }}" name="accounting-search-form" method="get" role="search" aria-labelledby="accounting-search-modal-label"> | ||||
|   <div class="modal fade" id="accounting-search-modal" tabindex="-1" aria-labelledby="accounting-search-modal-label" aria-hidden="true"> | ||||
|     <div class="modal-dialog"> | ||||
|       <div class="modal-content"> | ||||
|   | ||||
| @@ -118,7 +118,7 @@ First written: 2023/3/8 | ||||
|   </button> | ||||
| {% endif %} | ||||
| {% if use_search %} | ||||
|   <form class="btn btn-primary d-flex input-group" action="{{ url_for("accounting-report.search") }}" method="get" role="search" aria-labelledby="accounting-toolbar-search-label"> | ||||
|   <form class="btn btn-primary d-flex input-group" name="accounting-search-form" action="{{ url_for("accounting-report.search") }}" method="get" role="search" aria-labelledby="accounting-toolbar-search-label"> | ||||
|     <input id="accounting-toolbar-search" class="form-control form-control-sm" type="search" name="q" value="{{ request.args.q }}" placeholder=" " required="required"> | ||||
|     <label id="accounting-toolbar-search-label" for="accounting-toolbar-search" class="input-group-text"> | ||||
|       <button type="submit"> | ||||
|   | ||||
| @@ -14,8 +14,7 @@ | ||||
| #  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| #  See the License for the specific language governing permissions and | ||||
| #  limitations under the License. | ||||
| """The utility to cast a SQLAlchemy column into the column type, to avoid | ||||
| warnings from the IDE. | ||||
| """The utilities to cast values into desired types, to avoid IDE warnings. | ||||
|  | ||||
| This module should not import any other module from the application. | ||||
|  | ||||
|   | ||||
| @@ -21,7 +21,6 @@ from typing import Self | ||||
|  | ||||
| import sqlalchemy as sa | ||||
|  | ||||
| from accounting import db | ||||
| from accounting.locale import gettext | ||||
| from accounting.models import Account | ||||
|  | ||||
| @@ -75,7 +74,7 @@ class CurrentAccount: | ||||
|         """ | ||||
|         accounts: list[cls] = [cls.current_assets_and_liabilities()] | ||||
|         accounts.extend([CurrentAccount(x) | ||||
|                          for x in db.session.query(Account) | ||||
|                          for x in Account.query | ||||
|                         .filter(cls.sql_condition()) | ||||
|                         .order_by(Account.base_code, Account.no)]) | ||||
|         return accounts | ||||
|   | ||||
| @@ -41,11 +41,8 @@ def inherit_next(uri: str) -> str: | ||||
|     :param uri: The URI. | ||||
|     :return: The URI with the current next URI added at the query argument. | ||||
|     """ | ||||
|     next_uri: str | None = request.form.get("next") \ | ||||
|         if request.method == "POST" else request.args.get("next") | ||||
|     if next_uri is None: | ||||
|         return uri | ||||
|     return __set_next(uri, next_uri) | ||||
|     next_uri: str | None = __get_next_uri() | ||||
|     return uri if next_uri is None else __set_next(uri, next_uri) | ||||
|  | ||||
|  | ||||
| def or_next(uri: str) -> str: | ||||
| @@ -54,9 +51,22 @@ def or_next(uri: str) -> str: | ||||
|     :param uri: The URI. | ||||
|     :return: The next URI or the supplied URI. | ||||
|     """ | ||||
|     next_uri: str | None = __get_next_uri() | ||||
|     return uri if next_uri is None else next_uri | ||||
|  | ||||
|  | ||||
| def __get_next_uri() -> str | None: | ||||
|     """Returns the valid next URI. | ||||
|  | ||||
|     :return: The valid next URI. | ||||
|     """ | ||||
|     next_uri: str | None = request.form.get("next") \ | ||||
|         if request.method == "POST" else request.args.get("next") | ||||
|     return uri if next_uri is None else next_uri | ||||
|     if next_uri is None or not next_uri.startswith("/"): | ||||
|         return None | ||||
|     if len(next_uri) > 512: | ||||
|         return next_uri[:512] | ||||
|     return next_uri | ||||
|  | ||||
|  | ||||
| def __set_next(uri: str, next_uri: str) -> str: | ||||
|   | ||||
| @@ -2181,14 +2181,14 @@ class JournalEntryReorderTestCase(unittest.TestCase): | ||||
|         response = self.client.post( | ||||
|             f"{PREFIX}/dates/{date.isoformat()}", | ||||
|             data={"csrf_token": self.csrf_token, | ||||
|                   "next": "/next", | ||||
|                   "next": NEXT_URI, | ||||
|                   f"{id_1}-no": "4", | ||||
|                   f"{id_2}-no": "1", | ||||
|                   f"{id_3}-no": "5", | ||||
|                   f"{id_4}-no": "2", | ||||
|                   f"{id_5}-no": "3"}) | ||||
|         self.assertEqual(response.status_code, 302) | ||||
|         self.assertEqual(response.headers["Location"], f"/next") | ||||
|         self.assertEqual(response.headers["Location"], NEXT_URI) | ||||
|  | ||||
|         with self.app.app_context(): | ||||
|             self.assertEqual(db.session.get(JournalEntry, id_1).no, 4) | ||||
| @@ -2209,12 +2209,12 @@ class JournalEntryReorderTestCase(unittest.TestCase): | ||||
|         response = self.client.post( | ||||
|             f"{PREFIX}/dates/{date.isoformat()}", | ||||
|             data={"csrf_token": self.csrf_token, | ||||
|                   "next": "/next", | ||||
|                   "next": NEXT_URI, | ||||
|                   f"{id_2}-no": "3a", | ||||
|                   f"{id_3}-no": "5", | ||||
|                   f"{id_4}-no": "2"}) | ||||
|         self.assertEqual(response.status_code, 302) | ||||
|         self.assertEqual(response.headers["Location"], f"/next") | ||||
|         self.assertEqual(response.headers["Location"], NEXT_URI) | ||||
|  | ||||
|         with self.app.app_context(): | ||||
|             self.assertEqual(db.session.get(JournalEntry, id_1).no, 3) | ||||
|   | ||||
| @@ -52,6 +52,8 @@ def create_app(is_testing: bool = False) -> Flask: | ||||
|     db_uri: str = "sqlite:///" if is_testing else "sqlite:///local.sqlite" | ||||
|     app.config.from_mapping({ | ||||
|         "SECRET_KEY": os.environ.get("SECRET_KEY", token_urlsafe(32)), | ||||
|         "SESSION_COOKIE_SAMESITE": "Lax", | ||||
|         "SESSION_COOKIE_SECURE": True, | ||||
|         "SQLALCHEMY_DATABASE_URI": db_uri, | ||||
|         "BABEL_DEFAULT_LOCALE": "en", | ||||
|         "ALL_LINGUAS": "zh_Hant|正體中文,en|English,zh_Hans|简体中文", | ||||
|   | ||||
| @@ -19,10 +19,12 @@ | ||||
| """ | ||||
| from babel import Locale | ||||
| from flask import request, session, current_app, Blueprint, Response, \ | ||||
|     redirect, url_for, Flask | ||||
|     redirect, Flask | ||||
| from flask_babel import Babel | ||||
| from werkzeug.datastructures import LanguageAccept | ||||
|  | ||||
| from accounting.utils.next_uri import or_next | ||||
|  | ||||
| bp: Blueprint = Blueprint("locale", __name__, url_prefix="/") | ||||
|  | ||||
|  | ||||
| @@ -68,9 +70,7 @@ def set_locale() -> Response: | ||||
|     all_linguas: dict[str, str] = get_all_linguas() | ||||
|     if "locale" in request.form and request.form["locale"] in all_linguas: | ||||
|         session["locale"] = request.form["locale"] | ||||
|     if "next" in request.form: | ||||
|         return redirect(request.form["next"]) | ||||
|     return redirect(url_for("home.home")) | ||||
|     return redirect(or_next("/")) | ||||
|  | ||||
|  | ||||
| def get_all_linguas() -> dict[str, str]: | ||||
|   | ||||
| @@ -26,13 +26,13 @@ First written: 2023/1/27 | ||||
|   <meta name="viewport" content="width=device-width, initial-scale=1"> | ||||
|   <meta name="author" content="{{ "imacat" }}" /> | ||||
|   <link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" crossorigin="anonymous"> | ||||
|   <link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@6.2.1/css/all.min.css" crossorigin="anonymous"> | ||||
|   <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@eonasdan/tempus-dominus@6.4.3/dist/css/tempus-dominus.min.css" crossorigin="anonymous"> | ||||
|   <link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@6.4.0/css/all.min.css" crossorigin="anonymous"> | ||||
|   <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@eonasdan/tempus-dominus@6.7.7/dist/css/tempus-dominus.min.css" crossorigin="anonymous"> | ||||
|   {% block styles %}{% endblock %} | ||||
|   <script src="{{ url_for("babel_catalog") }}"></script> | ||||
|   <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-kenU1KFdBIe4zVF0s0G1M5b4hcpxyD9F7jL+jjXkk+Q2h455rYXK/7HAuoJl+0I4" crossorigin="anonymous"></script> | ||||
|   <script src="https://cdn.jsdelivr.net/npm/decimal.js-light@2.5.1/decimal.min.js" integrity="sha384-QdsxGEq4Y0erX8WUIsZJDtfoSSyBF6dmNCnzRNYCa2AOM/xzNsyhHu0RbdFBAm+l" crossorigin="anonymous"></script> | ||||
|   <script src="https://cdn.jsdelivr.net/npm/@eonasdan/tempus-dominus@6.4.3/dist/js/tempus-dominus.min.js" integrity="sha384-2MkID2vkc9sxBCqs2us3mB8fV+c0o7uPtOvAPjaC8gKv9Bk21UHT0r2Q7Kv70+zO" crossorigin="anonymous"></script> | ||||
|   <script src="https://cdn.jsdelivr.net/npm/@eonasdan/tempus-dominus@6.7.7/dist/js/tempus-dominus.min.js" integrity="sha384-MxHp+/TqTjbku1jSTIe1e/4l6CZTLhACLDbWyxYaFRgD3AM4oh99AY8bxsGhIoRc" crossorigin="anonymous"></script> | ||||
|   {% block scripts %}{% endblock %} | ||||
|   <link rel="shortcut icon" href="{{ url_for("static", filename="favicon.svg") }}"> | ||||
|   <title>{% block title %}{% endblock %}</title> | ||||
|   | ||||
| @@ -101,6 +101,60 @@ class NextUriTestCase(unittest.TestCase): | ||||
|                                                       "name": "viewer"}) | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|  | ||||
|     def test_invalid(self) -> None: | ||||
|         """Tests the next URI utilities without an invalid next URI. | ||||
|  | ||||
|         :return: None. | ||||
|         """ | ||||
|         def test_invalid_next_uri_view() -> str: | ||||
|             """The test view without the next URI.""" | ||||
|             self.assertEqual(inherit_next(self.TARGET), | ||||
|                              request.args.get("inherit-expected")) | ||||
|             self.assertEqual(or_next(self.TARGET), | ||||
|                              request.args.get("or-expected")) | ||||
|             return "" | ||||
|  | ||||
|         self.app.add_url_rule("/test-invalid-next", | ||||
|                               view_func=test_invalid_next_uri_view, | ||||
|                               methods=["GET", "POST"]) | ||||
|         client: httpx.Client = httpx.Client(app=self.app, base_url=TEST_SERVER) | ||||
|         client.headers["Referer"] = TEST_SERVER | ||||
|         csrf_token: str = get_csrf_token(client) | ||||
|         next_uri: str | ||||
|         expected1: str | ||||
|         expected2: str | ||||
|         response: httpx.Response | ||||
|  | ||||
|         # A foreign URI | ||||
|         next_uri = "https://example.com" | ||||
|         expected1 = self.TARGET | ||||
|         expected2 = self.TARGET | ||||
|         response = client.get(f"/test-invalid-next?next={quote_plus(next_uri)}" | ||||
|                               f"&inherit-expected={quote_plus(expected1)}" | ||||
|                               f"&or-expected={quote_plus(expected2)}") | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|         response = client.post("/test-invalid-next" | ||||
|                                f"?inherit-expected={quote_plus(expected1)}" | ||||
|                                f"&or-expected={quote_plus(expected2)}", | ||||
|                                data={"csrf_token": csrf_token, | ||||
|                                      "next": next_uri}) | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|  | ||||
|         # An extremely-long URI to trigger the error | ||||
|         next_uri = "/" + "x" * 1024 | ||||
|         expected2 = next_uri[:512] | ||||
|         expected1 = f"{self.TARGET}?next={quote_plus(expected2)}" | ||||
|         response = client.get(f"/test-invalid-next?next={quote_plus(next_uri)}" | ||||
|                               f"&inherit-expected={quote_plus(expected1)}" | ||||
|                               f"&or-expected={quote_plus(expected2)}") | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|         response = client.post("/test-invalid-next" | ||||
|                                f"?inherit-expected={quote_plus(expected1)}" | ||||
|                                f"&or-expected={quote_plus(expected2)}", | ||||
|                                data={"csrf_token": csrf_token, | ||||
|                                      "next": next_uri}) | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|  | ||||
|  | ||||
| class QueryKeywordParserTestCase(unittest.TestCase): | ||||
|     """The test case for the query keyword parser.""" | ||||
|   | ||||
		Reference in New Issue
	
	Block a user