From 2fbe137243396410101cfe6bbe41a5597dd4ca60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BE=9D=E7=91=AA=E8=B2=93?= Date: Sat, 4 Feb 2023 08:12:24 +0800 Subject: [PATCH] Added test_utils.py with the NextUriTestCase, QueryKeywordParserTestCase, and PaginationTestCase test cases for the independent utilities. --- tests/test_utils.py | 248 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 248 insertions(+) create mode 100644 tests/test_utils.py diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 0000000..4d8fece --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,248 @@ +# The Mia! Accounting Flask Project. +# Author: imacat@mail.imacat.idv.tw (imacat), 2023/2/3 + +# Copyright (c) 2023 imacat. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# 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 test for the independent utilities. + +""" +import unittest +from urllib.parse import quote_plus + +import httpx +from flask import Flask, request + +from accounting.utils.next_url import append_next, inherit_next, or_next +from accounting.utils.pagination import Pagination +from accounting.utils.query import parse_query_keywords +from test_site import create_app +from testlib import get_csrf_token + + +class NextUriTestCase(unittest.TestCase): + """The test case for the next URI utilities.""" + + def test_next_uri(self) -> None: + """Tests the next URI utilities. + + :return: None. + """ + app: Flask = create_app(is_testing=True) + target: str = "/target" + + @app.route("/test-next", methods=["GET", "POST"]) + def test_next_view() -> str: + """The test view with the next URI.""" + current_uri: str = request.full_path if request.query_string \ + else request.path + self.assertEqual(append_next(target), + f"{target}?next={quote_plus(current_uri)}") + next_uri: str = request.form["next"] if request.method == "POST" \ + else request.args["next"] + self.assertEqual(inherit_next(target), + f"{target}?next={quote_plus(next_uri)}") + self.assertEqual(or_next(target), next_uri) + return "" + + @app.route("/test-no-next", methods=["GET", "POST"]) + def test_no_next_view() -> str: + """The test view without the next URI.""" + current_uri: str = request.full_path if request.query_string \ + else request.path + self.assertEqual(append_next(target), + f"{target}?next={quote_plus(current_uri)}") + self.assertEqual(inherit_next(target), target) + self.assertEqual(or_next(target), target) + return "" + + client: httpx.Client = httpx.Client(app=app, + base_url="https://testserver") + client.headers["Referer"] = "https://testserver" + csrf_token: str = get_csrf_token(self, client, "/login") + response: httpx.Response + + # With the next URI + response = client.get("/test-next?next=/next&q=abc&page-no=4") + self.assertEqual(response.status_code, 200) + response = client.post("/test-next", data={"csrf_token": csrf_token, + "next": "/next", + "name": "viewer"}) + self.assertEqual(response.status_code, 200) + + # Without the next URI + response = client.get("/test-no-next?q=abc&page-no=4") + self.assertEqual(response.status_code, 200) + response = client.post("/test-no-next", data={"csrf_token": csrf_token, + "name": "viewer"}) + self.assertEqual(response.status_code, 200) + + +class QueryKeywordParserTestCase(unittest.TestCase): + """The test case for the query keyword parser.""" + + def test_default(self) -> None: + """Tests the query keyword parser. + + :return: None. + """ + self.assertEqual(parse_query_keywords("coffee"), ["coffee"]) + self.assertEqual(parse_query_keywords("coffee tea"), ["coffee", "tea"]) + self.assertEqual(parse_query_keywords("\"coffee\" \"tea cake\""), + ["coffee", "tea cake"]) + + def test_malformed(self) -> None: + """Tests the malformed query. + + :return: None. + """ + self.assertEqual(parse_query_keywords("coffee te\"a ca\"ke"), + ["coffee", "te\"a", "ca\"ke"]) + self.assertEqual(parse_query_keywords("coffee \"tea cake"), + ["coffee", "\"tea", "cake"]) + + def test_empty(self) -> None: + """Tests the empty query. + + :return: None. + """ + self.assertEqual(parse_query_keywords(None), []) + self.assertEqual(parse_query_keywords(""), []) + + +class PaginationTestCase(unittest.TestCase): + """The test case for pagination.""" + + class Params: + """The testing parameters.""" + + def __init__(self, items: list[int], is_reversed: bool | None, + result: list[int], is_needed: bool): + """Constructs the expected pagination. + + :param items: All the items in the list. + :param is_reversed: Whether the default page is the last page. + :param result: The expected items on the page. + :param is_needed: Whether the pagination is needed. + """ + self.items: list[int] = items + self.is_reversed: bool | None = is_reversed + self.result: list[int] = result + self.is_needed: bool = is_needed + + def setUp(self) -> None: + """Sets up the test. + This is run once per test. + + :return: None. + """ + self.app: Flask = create_app(is_testing=True) + self.params = self.Params([], None, [], True) + + @self.app.get("/test-pagination") + def test_pagination_view() -> str: + """The test view with the pagination.""" + pagination: Pagination + if self.params.is_reversed is not None: + pagination = Pagination(self.params.items, + is_reversed=self.params.is_reversed) + else: + pagination = Pagination(self.params.items) + self.assertEqual(pagination.is_needed, self.params.is_needed) + self.assertEqual(pagination.list, self.params.result) + return "" + + self.client = httpx.Client(app=self.app, base_url="https://testserver") + self.client.headers["Referer"] = "https://testserver" + + def __test_pagination(self, query: str, items: range, + result: range, is_needed: bool = True, + is_reversed: bool | None = None) -> None: + """Tests the pagination. + + :param query: The query string. + :param items: The original items. + :param result: The expected page content. + :param is_needed: Whether the pagination is needed. + :param is_reversed: Whether the list is reversed. + :return: None. + """ + target: str = "/test-pagination" + if query != "": + target = f"{target}?{query}" + self.params = self.Params(list(items), is_reversed, + list(result), is_needed) + response: httpx.Response = self.client.get(target) + self.assertEqual(response.status_code, 200) + + def test_default(self) -> None: + """Tests the default pagination. + + :return: None. + """ + # The default first page + self.__test_pagination("", range(1, 687), range(1, 11)) + # Some page in the middle + self.__test_pagination("page-no=37", range(1, 687), range(361, 371)) + # The last page + self.__test_pagination("page-no=69", range(1, 687), range(681, 687)) + + def test_page_size(self) -> None: + """Tests the pagination with a different page size. + + :return: None. + """ + # The default page with a different page size + self.__test_pagination("page-size=15", range(1, 687), range(1, 16)) + # Some page with a different page size + self.__test_pagination("page-no=37&page-size=15", range(1, 687), + range(541, 556)) + # The last page with a different page size. + self.__test_pagination("page-no=46&page-size=15", range(1, 687), + range(676, 687)) + + def test_not_needed(self) -> None: + """Tests the pagination that is not needed. + + :return: None. + """ + # Empty list + self.__test_pagination("", range(0, 0), range(0, 0), is_needed=False) + # A list that fits in one page + self.__test_pagination("", range(1, 4), range(1, 4), is_needed=False) + # A large page size that fits in everything + self.__test_pagination("page-size=1000", range(1, 687), range(1, 687), + is_needed=False) + + def test_reversed(self) -> None: + """Tests the default page on a reversed list. + + :return: None. + """ + # The default page + self.__test_pagination("", range(1, 687), range(681, 687), + is_reversed=True) + # The default page with a different page size + self.__test_pagination("page-size=15", range(1, 687), range(676, 687), + is_reversed=True) + + def test_last_page(self) -> None: + """Tests the calculation of the items on the last page. + + :return: None. + """ + # The last page that fits in one page + self.__test_pagination("page-no=69", range(1, 691), range(681, 691)) + # A danging item in the last page + self.__test_pagination("page-no=70", range(1, 692), range(691, 692))