# The Mia! Accounting 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_uri import append_next, inherit_next, or_next, \ encode_next, decode_next from accounting.utils.pagination import Pagination, DEFAULT_PAGE_SIZE from accounting.utils.query import parse_query_keywords from testlib import TEST_SERVER, create_test_app, get_csrf_token, NEXT_URI class NextUriTestCase(unittest.TestCase): """The test case for the next URI utilities.""" TARGET: str = "/target" def setUp(self) -> None: """Sets up the test. This is run once per test. :return: None. """ self.__app: Flask = create_test_app() """The Flask application.""" def test_next_uri(self) -> None: """Tests the next URI utilities with the next URI. :return: None. """ def test_next_uri_view() -> str: """The test view with the next URI.""" current_uri: str = request.full_path if request.query_string \ else request.path with self.__app.app_context(): encoded_current: str = encode_next(current_uri) self.assertEqual(append_next(self.TARGET), f"{self.TARGET}?next={encoded_current}") encoded_next_uri: str = request.form["next"] \ if request.method == "POST" else request.args["next"] self.assertEqual(inherit_next(self.TARGET), f"{self.TARGET}?next={encoded_next_uri}") with self.__app.app_context(): next_uri: str = decode_next(encoded_next_uri) self.assertEqual(or_next(self.TARGET), next_uri) return "" self.__app.add_url_rule("/test-next", view_func=test_next_uri_view, methods=["GET", "POST"]) client: httpx.Client = httpx.Client( transport=httpx.WSGITransport(app=self.__app), base_url=TEST_SERVER) client.headers["Referer"] = TEST_SERVER csrf_token: str = get_csrf_token(client) response: httpx.Response with self.__app.app_context(): encoded_uri: str = encode_next(NEXT_URI) response = client.get(f"/test-next?next={encoded_uri}&q=abc&page-no=4") self.assertEqual(response.status_code, 200) response = client.post("/test-next", data={"csrf_token": csrf_token, "next": encoded_uri, "name": "viewer"}) self.assertEqual(response.status_code, 200) def test_no_next_uri(self) -> None: """Tests the next URI utilities without the next URI. :return: None. """ def test_no_next_uri_view() -> str: """The test view without the next URI.""" self.assertEqual(inherit_next(self.TARGET), self.TARGET) self.assertEqual(or_next(self.TARGET), self.TARGET) return "" self.__app.add_url_rule("/test-no-next", view_func=test_no_next_uri_view, methods=["GET", "POST"]) client: httpx.Client = httpx.Client( transport=httpx.WSGITransport(app=self.__app), base_url=TEST_SERVER) client.headers["Referer"] = TEST_SERVER csrf_token: str = get_csrf_token(client) response: httpx.Response 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) 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), self.TARGET) self.assertEqual(or_next(self.TARGET), self.TARGET) return "" self.__app.add_url_rule("/test-invalid-next", view_func=test_invalid_next_uri_view, methods=["GET", "POST"]) client: httpx.Client = httpx.Client( transport=httpx.WSGITransport(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" response = client.get(f"/test-invalid-next?next={quote_plus(next_uri)}") self.assertEqual(response.status_code, 200) response = client.post("/test-invalid-next", 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.""" 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"]) self.assertEqual(parse_query_keywords("\"coffee tea\" cheese " "\"cake candy\" sugar"), ["coffee tea", "cheese", "cake candy", "sugar"]) def test_malformed(self) -> None: """Tests the malformed query. :return: None. """ self.assertEqual(parse_query_keywords("coffee \"tea cake"), ["coffee", "tea cake"]) 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 pagination parameters.""" def __init__(self, items: list[int], is_reversed: bool | None, result: list[int], is_paged: 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_paged: Whether we need pagination. """ self.items: list[int] = items """All the items in the list.""" self.is_reversed: bool | None = is_reversed """Whether the default page is the last page.""" self.result: list[int] = result """The expected items on the page.""" self.is_paged: bool = is_paged """Whether we need pagination.""" def setUp(self) -> None: """Sets up the test. This is run once per test. :return: None. """ self.__app: Flask = create_test_app() """The Flask application.""" self.__params: PaginationTestCase.Params \ = self.Params([], None, [], True) """The testing pagination parameters.""" @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[int]( self.__params.items, is_reversed=self.__params.is_reversed) else: pagination = Pagination[int](self.__params.items) self.assertEqual(pagination.is_paged, self.__params.is_paged) self.assertEqual(pagination.list, self.__params.result) return "" self.__client: httpx.Client = httpx.Client( transport=httpx.WSGITransport(app=self.__app), base_url=TEST_SERVER) """The user client.""" self.__client.headers["Referer"] = TEST_SERVER def __test_success(self, query: str, items: range, result: range, is_paged: 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_paged: Whether we need pagination. :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_paged) response: httpx.Response = self.__client.get(target) self.assertEqual(response.status_code, 200) def __test_malformed(self, query: str, items: range, redirect_to: str, is_reversed: bool | None = None) -> None: """Tests the pagination. :param query: The query string. :param items: The original items. :param redirect_to: The expected target query of the redirection. :param is_reversed: Whether the list is reversed. :return: None. """ target: str = "/test-pagination" self.__params = self.Params(list(items), is_reversed, [], True) response: httpx.Response = self.__client.get(f"{target}?{query}") self.assertEqual(response.status_code, 302) self.assertEqual(response.headers["Location"], f"{target}?{redirect_to}") def test_default(self) -> None: """Tests the default pagination. :return: None. """ # The default first page self.__test_success("", range(1, 687), range(1, 11)) # Some page in the middle self.__test_success("page-no=37", range(1, 687), range(361, 371)) # The last page self.__test_success("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_success("page-size=15", range(1, 687), range(1, 16)) # Some page with a different page size self.__test_success("page-no=37&page-size=15", range(1, 687), range(541, 556)) # The last page with a different page size. self.__test_success("page-no=46&page-size=15", range(1, 687), range(676, 687)) def test_not_need(self) -> None: """Tests that the data does not need pagination. :return: None. """ # Empty list self.__test_success("", range(0, 0), range(0, 0), is_paged=False) # A list that fits in one page self.__test_success("", range(1, 4), range(1, 4), is_paged=False) # A large page size that fits in everything self.__test_success("page-size=1000", range(1, 687), range(1, 687), is_paged=False) def test_reversed(self) -> None: """Tests the default page on a reversed list. :return: None. """ # The default page self.__test_success("", range(1, 687), range(681, 687), is_reversed=True) # The default page with a different page size self.__test_success("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_success("page-no=69", range(1, 691), range(681, 691)) # A danging item in the last page self.__test_success("page-no=70", range(1, 692), range(691, 692)) def test_malformed(self) -> None: """Tests the malformed pagination parameters. :return: None. """ page_37: str = "q=word&page-no=37&next=%2F" page_size_15_default: str = "q=word&page-size=15&next=%2F" # A malformed page size self.__test_malformed("q=word&page-size=100a&page-no=37&next=%2F", range(1, 691), page_37) # A default page size self.__test_malformed(f"q=word&page-size={DEFAULT_PAGE_SIZE}" "&page-no=37&next=%2F", range(1, 691), page_37) # An invalid page size self.__test_malformed("q=word&page-size=0&page-no=37&next=%2F", range(1, 691), page_37) # A malformed page number self.__test_malformed("q=word&page-size=15&page-no=37a&next=%2F", range(1, 691), page_size_15_default) # A default page number self.__test_malformed("q=word&page-size=15&page-no=1&next=%2F", range(1, 691), page_size_15_default) # A default page number, on a reversed list self.__test_malformed("q=word&page-size=15&page-no=46&next=%2F", range(1, 691), page_size_15_default, is_reversed=True) # A page number beyond the last page self.__test_malformed("q=word&page-size=15&page-no=100&next=%2F", range(1, 691), "q=word&page-size=15&page-no=46&next=%2F") # A page number beyond the last page, on a reversed list self.__test_malformed("q=word&page-size=15&page-no=100&next=%2F", range(1, 691), page_size_15_default, is_reversed=True) # A page number before the first page self.__test_malformed("q=word&page-size=15&page-no=0&next=%2F", range(1, 691), page_size_15_default) # A page number before the first page, on a reversed list self.__test_malformed("q=word&page-size=15&page-no=0&next=%2F", range(1, 691), "q=word&page-size=15&page-no=1&next=%2F", is_reversed=True)