Compare commits

..

12 Commits

Author SHA1 Message Date
520ad5687c Updated the translation. 2023-06-10 10:43:35 +08:00
727bc4967e Updated the log in message at the home page, and removed the next URI from the log in link. The next URI is not clear text but encrypted now. There is no need to attach the next URI, as it defaults redirects to the accounting application without the next URI. 2023-06-10 10:41:39 +08:00
a24242dfcf Removed unused imports from the test site. 2023-06-10 10:33:57 +08:00
896fc4e27e Removed an unused import from testlib_journal_entry.py. 2023-06-10 10:33:09 +08:00
80195eef5e Revised the code to read from the CSV data files in the __test_base_account_data method of the ConsoleCommandTestCase test case, to prevent PyCharm from complaining. 2023-06-10 10:32:33 +08:00
c60424f763 Changed the properties of the test case from public to private. 2023-06-10 10:30:45 +08:00
9930f33adb Removed the CSRF token from the get_client function, so that type hints and documentation can be added to the client and the CSRF token properties separately. 2023-06-10 09:45:19 +08:00
97b9035745 Added missing documentation to the global variables and class and object properties. 2023-06-10 09:45:06 +08:00
77b3e8d241 Removed an excess property declaration in the populate_obj method of the JournalEntryForm form. 2023-06-09 05:45:50 +08:00
e0c6d98b01 Added the "decode_next" utility in the "swr_assess.utils.next_uri" module, and applied the "encode_next" and "decode_next" utilities to the NextUriTestCase test case, so that the test case do not need to get involved into the detail of the next URI encryption. 2023-06-08 09:27:39 +08:00
30fd9c2164 Fixed the documentation of the "is_default" property of the Period utility. 2023-06-05 22:43:35 +08:00
7cb01b4cee Revised the documentation of the columns of the data models. 2023-06-05 16:55:25 +08:00
35 changed files with 1269 additions and 1110 deletions

View File

@ -168,7 +168,9 @@ class AccountReorderForm:
:param base: The base account. :param base: The base account.
""" """
self.base: BaseAccount = base self.base: BaseAccount = base
"""The base account."""
self.is_modified: bool = False self.is_modified: bool = False
"""Whether the order is modified."""
def save_order(self) -> None: def save_order(self) -> None:
"""Saves the order of the account. """Saves the order of the account.

View File

@ -65,6 +65,7 @@ class IsDebitAccount:
:param message: The error message. :param message: The error message.
""" """
self.__message: str | LazyString = message self.__message: str | LazyString = message
"""The error message."""
def __call__(self, form: FlaskForm, field: StringField) -> None: def __call__(self, form: FlaskForm, field: StringField) -> None:
if field.data is None: if field.data is None:
@ -85,6 +86,7 @@ class IsCreditAccount:
:param message: The error message. :param message: The error message.
""" """
self.__message: str | LazyString = message self.__message: str | LazyString = message
"""The error message."""
def __call__(self, form: FlaskForm, field: StringField) -> None: def __call__(self, form: FlaskForm, field: StringField) -> None:
if field.data is None: if field.data is None:

View File

@ -151,7 +151,6 @@ class JournalEntryForm(FlaskForm):
is_new: bool = obj.id is None is_new: bool = obj.id is None
if is_new: if is_new:
obj.id = new_id(JournalEntry) obj.id = new_id(JournalEntry)
self.date: DateField
self.__set_date(obj, self.date.data) self.__set_date(obj, self.date.data)
obj.note = self.note.data obj.note = self.note.data

View File

@ -54,7 +54,9 @@ class JournalEntryReorderForm:
:param date: The date. :param date: The date.
""" """
self.date: dt.date = date self.date: dt.date = date
"""The date."""
self.is_modified: bool = False self.is_modified: bool = False
"""Whether the order is modified."""
def save_order(self) -> None: def save_order(self) -> None:
"""Saves the order of the account. """Saves the order of the account.

View File

@ -166,8 +166,11 @@ class DescriptionRecurring:
:param account: The account. :param account: The account.
""" """
self.name: str = name self.name: str = name
"""The name."""
self.account: DescriptionAccount = DescriptionAccount(account, 0) self.account: DescriptionAccount = DescriptionAccount(account, 0)
"""The account."""
self.description_template: str = description_template self.description_template: str = description_template
"""The description template."""
@property @property
def account_codes(self) -> list[str]: def account_codes(self) -> list[str]:

View File

@ -25,8 +25,10 @@ from flask_babel import LazyString, Domain
from flask_babel_js import JAVASCRIPT, c2js from flask_babel_js import JAVASCRIPT, c2js
translation_dir: Path = Path(__file__).parent / "translations" translation_dir: Path = Path(__file__).parent / "translations"
"""The directory of the translation files."""
domain: Domain = Domain(translation_directories=[translation_dir], domain: Domain = Domain(translation_directories=[translation_dir],
domain="accounting") domain="accounting")
"""The message domain."""
def gettext(string, **variables) -> str: def gettext(string, **variables) -> str:
@ -120,6 +122,5 @@ def init_app(app: Flask, bp: Blueprint) -> None:
:param bp: The blueprint of the accounting application. :param bp: The blueprint of the accounting application.
:return: None. :return: None.
""" """
bp.add_url_rule("/_jstrans.js", "babel_catalog", bp.add_url_rule("/_jstrans.js", "babel_catalog", __babel_js_catalog_view)
__babel_js_catalog_view)
app.jinja_env.globals["A_"] = domain.gettext app.jinja_env.globals["A_"] = domain.gettext

View File

@ -40,7 +40,7 @@ class BaseAccount(db.Model):
__tablename__ = "accounting_base_accounts" __tablename__ = "accounting_base_accounts"
"""The table name.""" """The table name."""
code: Mapped[str] = mapped_column(primary_key=True) code: Mapped[str] = mapped_column(primary_key=True)
"""The code.""" """The account code."""
title_l10n: Mapped[str] = mapped_column("title") title_l10n: Mapped[str] = mapped_column("title")
"""The title.""" """The title."""
l10n: Mapped[list[BaseAccountL10n]] \ l10n: Mapped[list[BaseAccountL10n]] \
@ -87,7 +87,7 @@ class BaseAccountL10n(db.Model):
= mapped_column(db.ForeignKey(BaseAccount.code, onupdate="CASCADE", = mapped_column(db.ForeignKey(BaseAccount.code, onupdate="CASCADE",
ondelete="CASCADE"), ondelete="CASCADE"),
primary_key=True) primary_key=True)
"""The code of the account.""" """The account code."""
account: Mapped[BaseAccount] = db.relationship(back_populates="l10n") account: Mapped[BaseAccount] = db.relationship(back_populates="l10n")
"""The account.""" """The account."""
locale: Mapped[str] = mapped_column(primary_key=True) locale: Mapped[str] = mapped_column(primary_key=True)
@ -369,9 +369,9 @@ class Currency(db.Model):
__tablename__ = "accounting_currencies" __tablename__ = "accounting_currencies"
"""The table name.""" """The table name."""
code: Mapped[str] = mapped_column(primary_key=True) code: Mapped[str] = mapped_column(primary_key=True)
"""The code.""" """The currency code."""
name_l10n: Mapped[str] = mapped_column("name") name_l10n: Mapped[str] = mapped_column("name")
"""The name.""" """The currency name."""
created_at: Mapped[dt.datetime] \ created_at: Mapped[dt.datetime] \
= mapped_column(db.DateTime(timezone=True), = mapped_column(db.DateTime(timezone=True),
server_default=db.func.now()) server_default=db.func.now())
@ -544,7 +544,7 @@ class JournalEntry(db.Model):
date: Mapped[dt.date] date: Mapped[dt.date]
"""The date.""" """The date."""
no: Mapped[int] = mapped_column(default=text("1")) no: Mapped[int] = mapped_column(default=text("1"))
"""The account number under the date.""" """The journal entry number under the date."""
note: Mapped[str | None] note: Mapped[str | None]
"""The note.""" """The note."""
created_at: Mapped[dt.datetime] \ created_at: Mapped[dt.datetime] \

View File

@ -42,7 +42,7 @@ class Period:
self.end: dt.date | None = end self.end: dt.date | None = end
"""The end of the period.""" """The end of the period."""
self.is_default: bool = False self.is_default: bool = False
"""Whether the is the default period.""" """Whether this is the default period."""
self.is_this_month: bool = False self.is_this_month: bool = False
"""Whether the period is this month.""" """Whether the period is this month."""
self.is_last_month: bool = False self.is_last_month: bool = False

View File

@ -145,6 +145,7 @@ class AccountCollector:
.filter(sa.or_(Account.id.in_({x.id for x in account_balances}), .filter(sa.or_(Account.id.in_({x.id for x in account_balances}),
Account.base_code == "3351", Account.base_code == "3351",
Account.base_code == "3353")).all() Account.base_code == "3353")).all()
"""The accounts."""
account_by_id: dict[int, Account] \ account_by_id: dict[int, Account] \
= {x.id: x for x in self.__all_accounts} = {x.id: x for x in self.__all_accounts}
self.accounts: list[ReportAccount] \ self.accounts: list[ReportAccount] \
@ -154,6 +155,7 @@ class AccountCollector:
account_by_id[x.id], account_by_id[x.id],
self.__period)) self.__period))
for x in account_balances] for x in account_balances]
"""The accounts on the balance sheet."""
self.__add_accumulated() self.__add_accumulated()
self.__add_current_period() self.__add_current_period()
self.accounts.sort(key=lambda x: (x.account.base_code, x.account.no)) self.accounts.sort(key=lambda x: (x.account.base_code, x.account.no))

View File

@ -106,6 +106,7 @@ class Section:
"""The subsections in the section.""" """The subsections in the section."""
self.accumulated: AccumulatedTotal \ self.accumulated: AccumulatedTotal \
= AccumulatedTotal(accumulated_title) = AccumulatedTotal(accumulated_title)
"""The accumulated total."""
@property @property
def total(self) -> Decimal: def total(self) -> Decimal:

View File

@ -75,8 +75,7 @@ def __get_next() -> str | None:
if next_uri is None: if next_uri is None:
return None return None
try: try:
return URLSafeSerializer(current_app.config["SECRET_KEY"])\ return decode_next(next_uri)
.loads(next_uri, "next")
except BadData: except BadData:
return None return None
@ -107,6 +106,16 @@ def encode_next(uri: str) -> str:
.dumps(uri, "next") .dumps(uri, "next")
def decode_next(uri: str) -> str:
"""Decodes the encoded next URI.
:param uri: The encoded next URI.
:return: The next URI.
"""
return URLSafeSerializer(current_app.config["SECRET_KEY"])\
.loads(uri, "next")
def init_app(bp: Blueprint) -> None: def init_app(bp: Blueprint) -> None:
"""Initializes the application. """Initializes the application.

View File

@ -39,8 +39,11 @@ class RecurringItem:
:param description_template: The description template. :param description_template: The description template.
""" """
self.name: str = name self.name: str = name
"""The name."""
self.account_code: str = account_code self.account_code: str = account_code
"""The account code."""
self.description_template: str = description_template self.description_template: str = description_template
"""The description template."""
@property @property
def account_text(self) -> str: def account_text(self) -> str:
@ -61,8 +64,10 @@ class Recurring:
""" """
self.expenses: list[RecurringItem] \ self.expenses: list[RecurringItem] \
= [RecurringItem(x[0], x[1], x[2]) for x in data["expense"]] = [RecurringItem(x[0], x[1], x[2]) for x in data["expense"]]
"""The recurring expenses."""
self.incomes: list[RecurringItem] \ self.incomes: list[RecurringItem] \
= [RecurringItem(x[0], x[1], x[2]) for x in data["income"]] = [RecurringItem(x[0], x[1], x[2]) for x in data["income"]]
"""The recurring incomes."""
@property @property
def codes(self) -> set[str]: def codes(self) -> set[str]:

View File

@ -63,6 +63,7 @@ DEFAULT_PAGE_SIZE: int = 10
"""The default page size.""" """The default page size."""
T = TypeVar("T") T = TypeVar("T")
"""The pagination item type."""
class Pagination(Generic[T]): class Pagination(Generic[T]):

View File

@ -27,6 +27,7 @@ from flask import g, Response
from flask_sqlalchemy.model import Model from flask_sqlalchemy.model import Model
T = TypeVar("T", bound=Model) T = TypeVar("T", bound=Model)
"""The user data model data type."""
class UserUtilityInterface(Generic[T], ABC): class UserUtilityInterface(Generic[T], ABC):

View File

@ -28,8 +28,11 @@ from babel.messages.frontend import CommandLineInterface
from opencc import OpenCC from opencc import OpenCC
root_dir: Path = Path(__file__).parent.parent root_dir: Path = Path(__file__).parent.parent
"""The project root directory."""
translation_dir: Path = root_dir / "tests" / "test_site" / "translations" translation_dir: Path = root_dir / "tests" / "test_site" / "translations"
"""The directory of the translation files."""
domain: str = "messages" domain: str = "messages"
"""The message domain."""
@click.group() @click.group()

View File

@ -28,8 +28,11 @@ from babel.messages.frontend import CommandLineInterface
from opencc import OpenCC from opencc import OpenCC
root_dir: Path = Path(__file__).parent.parent root_dir: Path = Path(__file__).parent.parent
"""The project root directory."""
translation_dir: Path = root_dir / "src" / "accounting" / "translations" translation_dir: Path = root_dir / "src" / "accounting" / "translations"
"""The directory of the translation files."""
domain: str = "accounting" domain: str = "accounting"
"""The message domain."""
@click.group() @click.group()

View File

@ -25,8 +25,8 @@ from flask import Flask
from accounting.utils.next_uri import encode_next from accounting.utils.next_uri import encode_next
from test_site import db from test_site import db
from testlib import NEXT_URI, create_test_app, get_client, set_locale, \ from testlib import NEXT_URI, create_test_app, get_client, get_csrf_token, \
add_journal_entry set_locale, add_journal_entry
class AccountData: class AccountData:
@ -72,28 +72,33 @@ class AccountTestCase(unittest.TestCase):
:return: None. :return: None.
""" """
self.app: Flask = create_test_app() self.__app: Flask = create_test_app()
"""The Flask application."""
with self.app.app_context(): with self.__app.app_context():
from accounting.models import Account, AccountL10n from accounting.models import Account, AccountL10n
AccountL10n.query.delete() AccountL10n.query.delete()
Account.query.delete() Account.query.delete()
db.session.commit() db.session.commit()
self.encoded_next_uri: str = encode_next(NEXT_URI) self.__encoded_next_uri: str = encode_next(NEXT_URI)
"""The encoded next URI."""
self.client, self.csrf_token = get_client(self.app, "editor") self.__client: httpx.Client = get_client(self.__app, "editor")
"""The user client."""
self.__csrf_token: str = get_csrf_token(self.__client)
"""The CSRF token."""
response: httpx.Response response: httpx.Response
response = self.client.post(f"{PREFIX}/store", response = self.__client.post(f"{PREFIX}/store",
data={"csrf_token": self.csrf_token, data={"csrf_token": self.__csrf_token,
"base_code": CASH.base_code, "base_code": CASH.base_code,
"title": CASH.title}) "title": CASH.title})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], self.assertEqual(response.headers["Location"],
f"{PREFIX}/{CASH.code}") f"{PREFIX}/{CASH.code}")
response = self.client.post(f"{PREFIX}/store", response = self.__client.post(f"{PREFIX}/store",
data={"csrf_token": self.csrf_token, data={"csrf_token": self.__csrf_token,
"base_code": BANK.base_code, "base_code": BANK.base_code,
"title": BANK.title}) "title": BANK.title})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
@ -106,7 +111,8 @@ class AccountTestCase(unittest.TestCase):
:return: None. :return: None.
""" """
from accounting.models import Account from accounting.models import Account
client, csrf_token = get_client(self.app, "nobody") client: httpx.Client = get_client(self.__app, "nobody")
csrf_token: str = get_csrf_token(client)
response: httpx.Response response: httpx.Response
response = client.get(PREFIX) response = client.get(PREFIX)
@ -140,12 +146,12 @@ class AccountTestCase(unittest.TestCase):
response = client.get(f"{PREFIX}/bases/{CASH.base_code}") response = client.get(f"{PREFIX}/bases/{CASH.base_code}")
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
with self.app.app_context(): with self.__app.app_context():
cash_id: int = Account.find_by_code(CASH.code).id cash_id: int = Account.find_by_code(CASH.code).id
response = client.post(f"{PREFIX}/bases/{CASH.base_code}", response = client.post(f"{PREFIX}/bases/{CASH.base_code}",
data={"csrf_token": csrf_token, data={"csrf_token": csrf_token,
"next": self.encoded_next_uri, "next": self.__encoded_next_uri,
f"{cash_id}-no": "5"}) f"{cash_id}-no": "5"})
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
@ -155,7 +161,8 @@ class AccountTestCase(unittest.TestCase):
:return: None. :return: None.
""" """
from accounting.models import Account from accounting.models import Account
client, csrf_token = get_client(self.app, "viewer") client: httpx.Client = get_client(self.__app, "viewer")
csrf_token: str = get_csrf_token(client)
response: httpx.Response response: httpx.Response
response = client.get(PREFIX) response = client.get(PREFIX)
@ -189,12 +196,12 @@ class AccountTestCase(unittest.TestCase):
response = client.get(f"{PREFIX}/bases/{CASH.base_code}") response = client.get(f"{PREFIX}/bases/{CASH.base_code}")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
with self.app.app_context(): with self.__app.app_context():
cash_id: int = Account.find_by_code(CASH.code).id cash_id: int = Account.find_by_code(CASH.code).id
response = client.post(f"{PREFIX}/bases/{CASH.base_code}", response = client.post(f"{PREFIX}/bases/{CASH.base_code}",
data={"csrf_token": csrf_token, data={"csrf_token": csrf_token,
"next": self.encoded_next_uri, "next": self.__encoded_next_uri,
f"{cash_id}-no": "5"}) f"{cash_id}-no": "5"})
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
@ -206,47 +213,47 @@ class AccountTestCase(unittest.TestCase):
from accounting.models import Account from accounting.models import Account
response: httpx.Response response: httpx.Response
response = self.client.get(PREFIX) response = self.__client.get(PREFIX)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
response = self.client.get(f"{PREFIX}/{CASH.code}") response = self.__client.get(f"{PREFIX}/{CASH.code}")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
response = self.client.get(f"{PREFIX}/create") response = self.__client.get(f"{PREFIX}/create")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
response = self.client.post(f"{PREFIX}/store", response = self.__client.post(f"{PREFIX}/store",
data={"csrf_token": self.csrf_token, data={"csrf_token": self.__csrf_token,
"base_code": STOCK.base_code, "base_code": STOCK.base_code,
"title": STOCK.title}) "title": STOCK.title})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], self.assertEqual(response.headers["Location"],
f"{PREFIX}/{STOCK.code}") f"{PREFIX}/{STOCK.code}")
response = self.client.get(f"{PREFIX}/{CASH.code}/edit") response = self.__client.get(f"{PREFIX}/{CASH.code}/edit")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
response = self.client.post(f"{PREFIX}/{CASH.code}/update", response = self.__client.post(f"{PREFIX}/{CASH.code}/update",
data={"csrf_token": self.csrf_token, data={"csrf_token": self.__csrf_token,
"base_code": CASH.base_code, "base_code": CASH.base_code,
"title": f"{CASH.title}-2"}) "title": f"{CASH.title}-2"})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], f"{PREFIX}/{CASH.code}") self.assertEqual(response.headers["Location"], f"{PREFIX}/{CASH.code}")
response = self.client.post(f"{PREFIX}/{BANK.code}/delete", response = self.__client.post(f"{PREFIX}/{BANK.code}/delete",
data={"csrf_token": self.csrf_token}) data={"csrf_token": self.__csrf_token})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], PREFIX) self.assertEqual(response.headers["Location"], PREFIX)
response = self.client.get(f"{PREFIX}/bases/{CASH.base_code}") response = self.__client.get(f"{PREFIX}/bases/{CASH.base_code}")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
with self.app.app_context(): with self.__app.app_context():
cash_id: int = Account.find_by_code(CASH.code).id cash_id: int = Account.find_by_code(CASH.code).id
response = self.client.post(f"{PREFIX}/bases/{CASH.base_code}", response = self.__client.post(f"{PREFIX}/bases/{CASH.base_code}",
data={"csrf_token": self.csrf_token, data={"csrf_token": self.__csrf_token,
"next": self.encoded_next_uri, "next": self.__encoded_next_uri,
f"{cash_id}-no": "5"}) f"{cash_id}-no": "5"})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], NEXT_URI) self.assertEqual(response.headers["Location"], NEXT_URI)
@ -262,58 +269,59 @@ class AccountTestCase(unittest.TestCase):
detail_uri: str = f"{PREFIX}/{STOCK.code}" detail_uri: str = f"{PREFIX}/{STOCK.code}"
response: httpx.Response response: httpx.Response
with self.app.app_context(): with self.__app.app_context():
self.assertEqual({x.code for x in Account.query.all()}, self.assertEqual({x.code for x in Account.query.all()},
{CASH.code, BANK.code}) {CASH.code, BANK.code})
# Missing CSRF token # Missing CSRF token
response = self.client.post(store_uri, response = self.__client.post(store_uri,
data={"base_code": STOCK.base_code, data={"base_code": STOCK.base_code,
"title": STOCK.title}) "title": STOCK.title})
self.assertEqual(response.status_code, 400) self.assertEqual(response.status_code, 400)
# CSRF token mismatch # CSRF token mismatch
response = self.client.post(store_uri, response = self.__client.post(store_uri,
data={"csrf_token": f"{self.csrf_token}-2", data={"csrf_token":
f"{self.__csrf_token}-2",
"base_code": STOCK.base_code, "base_code": STOCK.base_code,
"title": STOCK.title}) "title": STOCK.title})
self.assertEqual(response.status_code, 400) self.assertEqual(response.status_code, 400)
# Empty base account code # Empty base account code
response = self.client.post(store_uri, response = self.__client.post(store_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.__csrf_token,
"base_code": " ", "base_code": " ",
"title": STOCK.title}) "title": STOCK.title})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], create_uri) self.assertEqual(response.headers["Location"], create_uri)
# Non-existing base account # Non-existing base account
response = self.client.post(store_uri, response = self.__client.post(store_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.__csrf_token,
"base_code": "9999", "base_code": "9999",
"title": STOCK.title}) "title": STOCK.title})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], create_uri) self.assertEqual(response.headers["Location"], create_uri)
# Unavailable base account # Unavailable base account
response = self.client.post(store_uri, response = self.__client.post(store_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.__csrf_token,
"base_code": "1", "base_code": "1",
"title": STOCK.title}) "title": STOCK.title})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], create_uri) self.assertEqual(response.headers["Location"], create_uri)
# Empty name # Empty name
response = self.client.post(store_uri, response = self.__client.post(store_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.__csrf_token,
"base_code": STOCK.base_code, "base_code": STOCK.base_code,
"title": " "}) "title": " "})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], create_uri) self.assertEqual(response.headers["Location"], create_uri)
# A nominal account that needs offset # A nominal account that needs offset
response = self.client.post(store_uri, response = self.__client.post(store_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.__csrf_token,
"base_code": "6172", "base_code": "6172",
"title": STOCK.title, "title": STOCK.title,
"is_need_offset": "yes"}) "is_need_offset": "yes"})
@ -321,16 +329,16 @@ class AccountTestCase(unittest.TestCase):
self.assertEqual(response.headers["Location"], create_uri) self.assertEqual(response.headers["Location"], create_uri)
# Success, with spaces to be stripped # Success, with spaces to be stripped
response = self.client.post(store_uri, response = self.__client.post(store_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.__csrf_token,
"base_code": f" {STOCK.base_code} ", "base_code": f" {STOCK.base_code} ",
"title": f" {STOCK.title} "}) "title": f" {STOCK.title} "})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], detail_uri) self.assertEqual(response.headers["Location"], detail_uri)
# Success under the same base # Success under the same base
response = self.client.post(store_uri, response = self.__client.post(store_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.__csrf_token,
"base_code": STOCK.base_code, "base_code": STOCK.base_code,
"title": STOCK.title}) "title": STOCK.title})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
@ -338,20 +346,20 @@ class AccountTestCase(unittest.TestCase):
f"{PREFIX}/{STOCK.base_code}-002") f"{PREFIX}/{STOCK.base_code}-002")
# Success under the same base, with order in a mess. # Success under the same base, with order in a mess.
with self.app.app_context(): with self.__app.app_context():
stock_2: Account = Account.find_by_code(f"{STOCK.base_code}-002") stock_2: Account = Account.find_by_code(f"{STOCK.base_code}-002")
stock_2.no = 66 stock_2.no = 66
db.session.commit() db.session.commit()
response = self.client.post(store_uri, response = self.__client.post(store_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.__csrf_token,
"base_code": STOCK.base_code, "base_code": STOCK.base_code,
"title": STOCK.title}) "title": STOCK.title})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], self.assertEqual(response.headers["Location"],
f"{PREFIX}/{STOCK.base_code}-003") f"{PREFIX}/{STOCK.base_code}-003")
with self.app.app_context(): with self.__app.app_context():
self.assertEqual({x.code for x in Account.query.all()}, self.assertEqual({x.code for x in Account.query.all()},
{CASH.code, BANK.code, STOCK.code, {CASH.code, BANK.code, STOCK.code,
f"{STOCK.base_code}-002", f"{STOCK.base_code}-002",
@ -374,53 +382,53 @@ class AccountTestCase(unittest.TestCase):
response: httpx.Response response: httpx.Response
# Success, with spaces to be stripped # Success, with spaces to be stripped
response = self.client.post(update_uri, response = self.__client.post(update_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.__csrf_token,
"base_code": f" {CASH.base_code} ", "base_code": f" {CASH.base_code} ",
"title": f" {CASH.title}-1 "}) "title": f" {CASH.title}-1 "})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], detail_uri) self.assertEqual(response.headers["Location"], detail_uri)
with self.app.app_context(): with self.__app.app_context():
account: Account = Account.find_by_code(CASH.code) account: Account = Account.find_by_code(CASH.code)
self.assertEqual(account.base_code, CASH.base_code) self.assertEqual(account.base_code, CASH.base_code)
self.assertEqual(account.title_l10n, f"{CASH.title}-1") self.assertEqual(account.title_l10n, f"{CASH.title}-1")
# Empty base account code # Empty base account code
response = self.client.post(update_uri, response = self.__client.post(update_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.__csrf_token,
"base_code": " ", "base_code": " ",
"title": STOCK.title}) "title": STOCK.title})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], edit_uri) self.assertEqual(response.headers["Location"], edit_uri)
# Non-existing base account # Non-existing base account
response = self.client.post(update_uri, response = self.__client.post(update_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.__csrf_token,
"base_code": "9999", "base_code": "9999",
"title": STOCK.title}) "title": STOCK.title})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], edit_uri) self.assertEqual(response.headers["Location"], edit_uri)
# Unavailable base account # Unavailable base account
response = self.client.post(update_uri, response = self.__client.post(update_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.__csrf_token,
"base_code": "1", "base_code": "1",
"title": STOCK.title}) "title": STOCK.title})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], edit_uri) self.assertEqual(response.headers["Location"], edit_uri)
# Empty name # Empty name
response = self.client.post(update_uri, response = self.__client.post(update_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.__csrf_token,
"base_code": STOCK.base_code, "base_code": STOCK.base_code,
"title": " "}) "title": " "})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], edit_uri) self.assertEqual(response.headers["Location"], edit_uri)
# A nominal account that needs offset # A nominal account that needs offset
response = self.client.post(update_uri, response = self.__client.post(update_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.__csrf_token,
"base_code": "6172", "base_code": "6172",
"title": STOCK.title, "title": STOCK.title,
"is_need_offset": "yes"}) "is_need_offset": "yes"})
@ -428,17 +436,17 @@ class AccountTestCase(unittest.TestCase):
self.assertEqual(response.headers["Location"], edit_uri) self.assertEqual(response.headers["Location"], edit_uri)
# Change the base account # Change the base account
response = self.client.post(update_uri, response = self.__client.post(update_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.__csrf_token,
"base_code": STOCK.base_code, "base_code": STOCK.base_code,
"title": STOCK.title}) "title": STOCK.title})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], detail_c_uri) self.assertEqual(response.headers["Location"], detail_c_uri)
response = self.client.get(detail_uri) response = self.__client.get(detail_uri)
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
response = self.client.get(detail_c_uri) response = self.__client.get(detail_c_uri)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
def test_update_not_modified(self) -> None: def test_update_not_modified(self) -> None:
@ -452,14 +460,14 @@ class AccountTestCase(unittest.TestCase):
account: Account account: Account
response: httpx.Response response: httpx.Response
response = self.client.post(update_uri, response = self.__client.post(update_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.__csrf_token,
"base_code": f" {CASH.base_code} ", "base_code": f" {CASH.base_code} ",
"title": f" {CASH.title} "}) "title": f" {CASH.title} "})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], detail_uri) self.assertEqual(response.headers["Location"], detail_uri)
with self.app.app_context(): with self.__app.app_context():
account = Account.find_by_code(CASH.code) account = Account.find_by_code(CASH.code)
self.assertIsNotNone(account) self.assertIsNotNone(account)
account.created_at \ account.created_at \
@ -467,14 +475,14 @@ class AccountTestCase(unittest.TestCase):
account.updated_at = account.created_at account.updated_at = account.created_at
db.session.commit() db.session.commit()
response = self.client.post(update_uri, response = self.__client.post(update_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.__csrf_token,
"base_code": CASH.base_code, "base_code": CASH.base_code,
"title": STOCK.title}) "title": STOCK.title})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], detail_uri) self.assertEqual(response.headers["Location"], detail_uri)
with self.app.app_context(): with self.__app.app_context():
account = Account.find_by_code(CASH.code) account = Account.find_by_code(CASH.code)
self.assertIsNotNone(account) self.assertIsNotNone(account)
self.assertLess(account.created_at, self.assertLess(account.created_at,
@ -487,13 +495,14 @@ class AccountTestCase(unittest.TestCase):
""" """
from accounting.models import Account from accounting.models import Account
editor_username, admin_username = "editor", "admin" editor_username, admin_username = "editor", "admin"
client, csrf_token = get_client(self.app, admin_username) client: httpx.Client = get_client(self.__app, admin_username)
csrf_token: str = get_csrf_token(client)
detail_uri: str = f"{PREFIX}/{CASH.code}" detail_uri: str = f"{PREFIX}/{CASH.code}"
update_uri: str = f"{PREFIX}/{CASH.code}/update" update_uri: str = f"{PREFIX}/{CASH.code}/update"
account: Account account: Account
response: httpx.Response response: httpx.Response
with self.app.app_context(): with self.__app.app_context():
account = Account.find_by_code(CASH.code) account = Account.find_by_code(CASH.code)
self.assertEqual(account.created_by.username, editor_username) self.assertEqual(account.created_by.username, editor_username)
self.assertEqual(account.updated_by.username, editor_username) self.assertEqual(account.updated_by.username, editor_username)
@ -505,7 +514,7 @@ class AccountTestCase(unittest.TestCase):
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], detail_uri) self.assertEqual(response.headers["Location"], detail_uri)
with self.app.app_context(): with self.__app.app_context():
account = Account.find_by_code(CASH.code) account = Account.find_by_code(CASH.code)
self.assertEqual(account.created_by.username, self.assertEqual(account.created_by.username,
editor_username) editor_username)
@ -523,51 +532,51 @@ class AccountTestCase(unittest.TestCase):
account: Account account: Account
response: httpx.Response response: httpx.Response
with self.app.app_context(): with self.__app.app_context():
account = Account.find_by_code(CASH.code) account = Account.find_by_code(CASH.code)
self.assertEqual(account.title_l10n, CASH.title) self.assertEqual(account.title_l10n, CASH.title)
self.assertEqual(account.l10n, []) self.assertEqual(account.l10n, [])
set_locale(self.app, self.client, self.csrf_token, "zh_Hant") set_locale(self.__app, self.__client, self.__csrf_token, "zh_Hant")
response = self.client.post(update_uri, response = self.__client.post(update_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.__csrf_token,
"base_code": CASH.base_code, "base_code": CASH.base_code,
"title": f"{CASH.title}-zh_Hant"}) "title": f"{CASH.title}-zh_Hant"})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], detail_uri) self.assertEqual(response.headers["Location"], detail_uri)
with self.app.app_context(): with self.__app.app_context():
account = Account.find_by_code(CASH.code) account = Account.find_by_code(CASH.code)
self.assertEqual(account.title_l10n, CASH.title) self.assertEqual(account.title_l10n, CASH.title)
self.assertEqual({(x.locale, x.title) for x in account.l10n}, self.assertEqual({(x.locale, x.title) for x in account.l10n},
{("zh_Hant", f"{CASH.title}-zh_Hant")}) {("zh_Hant", f"{CASH.title}-zh_Hant")})
set_locale(self.app, self.client, self.csrf_token, "en") set_locale(self.__app, self.__client, self.__csrf_token, "en")
response = self.client.post(update_uri, response = self.__client.post(update_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.__csrf_token,
"base_code": CASH.base_code, "base_code": CASH.base_code,
"title": f"{CASH.title}-2"}) "title": f"{CASH.title}-2"})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], detail_uri) self.assertEqual(response.headers["Location"], detail_uri)
with self.app.app_context(): with self.__app.app_context():
account = Account.find_by_code(CASH.code) account = Account.find_by_code(CASH.code)
self.assertEqual(account.title_l10n, f"{CASH.title}-2") self.assertEqual(account.title_l10n, f"{CASH.title}-2")
self.assertEqual({(x.locale, x.title) for x in account.l10n}, self.assertEqual({(x.locale, x.title) for x in account.l10n},
{("zh_Hant", f"{CASH.title}-zh_Hant")}) {("zh_Hant", f"{CASH.title}-zh_Hant")})
set_locale(self.app, self.client, self.csrf_token, "zh_Hant") set_locale(self.__app, self.__client, self.__csrf_token, "zh_Hant")
response = self.client.post(update_uri, response = self.__client.post(update_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.__csrf_token,
"base_code": CASH.base_code, "base_code": CASH.base_code,
"title": f"{CASH.title}-zh_Hant-2"}) "title": f"{CASH.title}-zh_Hant-2"})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], detail_uri) self.assertEqual(response.headers["Location"], detail_uri)
with self.app.app_context(): with self.__app.app_context():
account = Account.find_by_code(CASH.code) account = Account.find_by_code(CASH.code)
self.assertEqual(account.title_l10n, f"{CASH.title}-2") self.assertEqual(account.title_l10n, f"{CASH.title}-2")
self.assertEqual({(x.locale, x.title) for x in account.l10n}, self.assertEqual({(x.locale, x.title) for x in account.l10n},
@ -584,53 +593,53 @@ class AccountTestCase(unittest.TestCase):
list_uri: str = PREFIX list_uri: str = PREFIX
response: httpx.Response response: httpx.Response
response = self.client.post(f"{PREFIX}/store", response = self.__client.post(f"{PREFIX}/store",
data={"csrf_token": self.csrf_token, data={"csrf_token": self.__csrf_token,
"base_code": PETTY.base_code, "base_code": PETTY.base_code,
"title": PETTY.title}) "title": PETTY.title})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], detail_uri) self.assertEqual(response.headers["Location"], detail_uri)
add_journal_entry(self.client, add_journal_entry(self.__client,
form={"csrf_token": self.csrf_token, form={"csrf_token": self.__csrf_token,
"next": self.encoded_next_uri, "next": self.__encoded_next_uri,
"date": dt.date.today().isoformat(), "date": dt.date.today().isoformat(),
"currency-1-code": "USD", "currency-1-code": "USD",
"currency-1-credit-1-account_code": BANK.code, "currency-1-credit-1-account_code": BANK.code,
"currency-1-credit-1-amount": "20"}) "currency-1-credit-1-amount": "20"})
with self.app.app_context(): with self.__app.app_context():
self.assertEqual({x.code for x in Account.query.all()}, self.assertEqual({x.code for x in Account.query.all()},
{CASH.code, PETTY.code, BANK.code}) {CASH.code, PETTY.code, BANK.code})
# Cannot delete the cash account # Cannot delete the cash account
response = self.client.post(f"{PREFIX}/{CASH.code}/delete", response = self.__client.post(f"{PREFIX}/{CASH.code}/delete",
data={"csrf_token": self.csrf_token}) data={"csrf_token": self.__csrf_token})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], f"{PREFIX}/{CASH.code}") self.assertEqual(response.headers["Location"], f"{PREFIX}/{CASH.code}")
# Cannot delete the account that is in use # Cannot delete the account that is in use
response = self.client.post(f"{PREFIX}/{BANK.code}/delete", response = self.__client.post(f"{PREFIX}/{BANK.code}/delete",
data={"csrf_token": self.csrf_token}) data={"csrf_token": self.__csrf_token})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], f"{PREFIX}/{BANK.code}") self.assertEqual(response.headers["Location"], f"{PREFIX}/{BANK.code}")
# Success # Success
response = self.client.get(detail_uri) response = self.__client.get(detail_uri)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
response = self.client.post(delete_uri, response = self.__client.post(delete_uri,
data={"csrf_token": self.csrf_token}) data={"csrf_token": self.__csrf_token})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], list_uri) self.assertEqual(response.headers["Location"], list_uri)
with self.app.app_context(): with self.__app.app_context():
self.assertEqual({x.code for x in Account.query.all()}, self.assertEqual({x.code for x in Account.query.all()},
{CASH.code, BANK.code}) {CASH.code, BANK.code})
response = self.client.get(detail_uri) response = self.__client.get(detail_uri)
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
response = self.client.post(delete_uri, response = self.__client.post(delete_uri,
data={"csrf_token": self.csrf_token}) data={"csrf_token": self.__csrf_token})
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
def test_change_base_code(self) -> None: def test_change_base_code(self) -> None:
@ -642,15 +651,15 @@ class AccountTestCase(unittest.TestCase):
response: httpx.Response response: httpx.Response
for i in range(2, 6): for i in range(2, 6):
response = self.client.post(f"{PREFIX}/store", response = self.__client.post(f"{PREFIX}/store",
data={"csrf_token": self.csrf_token, data={"csrf_token": self.__csrf_token,
"base_code": "1111", "base_code": "1111",
"title": "Title"}) "title": "Title"})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], self.assertEqual(response.headers["Location"],
f"{PREFIX}/1111-00{i}") f"{PREFIX}/1111-00{i}")
with self.app.app_context(): with self.__app.app_context():
account_1: Account = Account.find_by_code("1111-001") account_1: Account = Account.find_by_code("1111-001")
id_1: int = account_1.id id_1: int = account_1.id
account_2: Account = Account.find_by_code("1111-002") account_2: Account = Account.find_by_code("1111-002")
@ -670,14 +679,14 @@ class AccountTestCase(unittest.TestCase):
account_5.no = 6 account_5.no = 6
db.session.commit() db.session.commit()
response = self.client.post(f"{PREFIX}/1111-005/update", response = self.__client.post(f"{PREFIX}/1111-005/update",
data={"csrf_token": self.csrf_token, data={"csrf_token": self.__csrf_token,
"base_code": "1112", "base_code": "1112",
"title": "Title"}) "title": "Title"})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], f"{PREFIX}/1112-003") self.assertEqual(response.headers["Location"], f"{PREFIX}/1112-003")
with self.app.app_context(): with self.__app.app_context():
self.assertEqual(db.session.get(Account, id_1).no, 1) self.assertEqual(db.session.get(Account, id_1).no, 1)
self.assertEqual(db.session.get(Account, id_2).no, 3) self.assertEqual(db.session.get(Account, id_2).no, 3)
self.assertEqual(db.session.get(Account, id_3).no, 2) self.assertEqual(db.session.get(Account, id_3).no, 2)
@ -693,8 +702,8 @@ class AccountTestCase(unittest.TestCase):
response: httpx.Response response: httpx.Response
for i in range(2, 6): for i in range(2, 6):
response = self.client.post(f"{PREFIX}/store", response = self.__client.post(f"{PREFIX}/store",
data={"csrf_token": self.csrf_token, data={"csrf_token": self.__csrf_token,
"base_code": "1111", "base_code": "1111",
"title": "Title"}) "title": "Title"})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
@ -702,16 +711,16 @@ class AccountTestCase(unittest.TestCase):
f"{PREFIX}/1111-00{i}") f"{PREFIX}/1111-00{i}")
# Normal reorder # Normal reorder
with self.app.app_context(): with self.__app.app_context():
id_1: int = Account.find_by_code("1111-001").id id_1: int = Account.find_by_code("1111-001").id
id_2: int = Account.find_by_code("1111-002").id id_2: int = Account.find_by_code("1111-002").id
id_3: int = Account.find_by_code("1111-003").id id_3: int = Account.find_by_code("1111-003").id
id_4: int = Account.find_by_code("1111-004").id id_4: int = Account.find_by_code("1111-004").id
id_5: int = Account.find_by_code("1111-005").id id_5: int = Account.find_by_code("1111-005").id
response = self.client.post(f"{PREFIX}/bases/1111", response = self.__client.post(f"{PREFIX}/bases/1111",
data={"csrf_token": self.csrf_token, data={"csrf_token": self.__csrf_token,
"next": self.encoded_next_uri, "next": self.__encoded_next_uri,
f"{id_1}-no": "4", f"{id_1}-no": "4",
f"{id_2}-no": "1", f"{id_2}-no": "1",
f"{id_3}-no": "5", f"{id_3}-no": "5",
@ -720,7 +729,7 @@ class AccountTestCase(unittest.TestCase):
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], NEXT_URI) self.assertEqual(response.headers["Location"], NEXT_URI)
with self.app.app_context(): with self.__app.app_context():
self.assertEqual(db.session.get(Account, id_1).code, "1111-004") self.assertEqual(db.session.get(Account, id_1).code, "1111-004")
self.assertEqual(db.session.get(Account, id_2).code, "1111-001") self.assertEqual(db.session.get(Account, id_2).code, "1111-001")
self.assertEqual(db.session.get(Account, id_3).code, "1111-005") self.assertEqual(db.session.get(Account, id_3).code, "1111-005")
@ -728,7 +737,7 @@ class AccountTestCase(unittest.TestCase):
self.assertEqual(db.session.get(Account, id_5).code, "1111-003") self.assertEqual(db.session.get(Account, id_5).code, "1111-003")
# Malformed orders # Malformed orders
with self.app.app_context(): with self.__app.app_context():
db.session.get(Account, id_1).no = 3 db.session.get(Account, id_1).no = 3
db.session.get(Account, id_2).no = 4 db.session.get(Account, id_2).no = 4
db.session.get(Account, id_3).no = 6 db.session.get(Account, id_3).no = 6
@ -736,16 +745,16 @@ class AccountTestCase(unittest.TestCase):
db.session.get(Account, id_5).no = 9 db.session.get(Account, id_5).no = 9
db.session.commit() db.session.commit()
response = self.client.post(f"{PREFIX}/bases/1111", response = self.__client.post(f"{PREFIX}/bases/1111",
data={"csrf_token": self.csrf_token, data={"csrf_token": self.__csrf_token,
"next": self.encoded_next_uri, "next": self.__encoded_next_uri,
f"{id_2}-no": "3a", f"{id_2}-no": "3a",
f"{id_3}-no": "5", f"{id_3}-no": "5",
f"{id_4}-no": "2"}) f"{id_4}-no": "2"})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], NEXT_URI) self.assertEqual(response.headers["Location"], NEXT_URI)
with self.app.app_context(): with self.__app.app_context():
self.assertEqual(db.session.get(Account, id_1).code, "1111-003") self.assertEqual(db.session.get(Account, id_1).code, "1111-003")
self.assertEqual(db.session.get(Account, id_2).code, "1111-004") self.assertEqual(db.session.get(Account, id_2).code, "1111-004")
self.assertEqual(db.session.get(Account, id_3).code, "1111-002") self.assertEqual(db.session.get(Account, id_3).code, "1111-002")

View File

@ -39,14 +39,15 @@ class BaseAccountTestCase(unittest.TestCase):
:return: None. :return: None.
""" """
self.app: Flask = create_test_app() self.__app: Flask = create_test_app()
"""The Flask application."""
def test_nobody(self) -> None: def test_nobody(self) -> None:
"""Test the permission as nobody. """Test the permission as nobody.
:return: None. :return: None.
""" """
client, csrf_token = get_client(self.app, "nobody") client: httpx.Client = get_client(self.__app, "nobody")
response: httpx.Response response: httpx.Response
response = client.get(LIST_URI) response = client.get(LIST_URI)
@ -60,7 +61,7 @@ class BaseAccountTestCase(unittest.TestCase):
:return: None. :return: None.
""" """
client, csrf_token = get_client(self.app, "viewer") client: httpx.Client = get_client(self.__app, "viewer")
response: httpx.Response response: httpx.Response
response = client.get(LIST_URI) response = client.get(LIST_URI)
@ -74,7 +75,7 @@ class BaseAccountTestCase(unittest.TestCase):
:return: None. :return: None.
""" """
client, csrf_token = get_client(self.app, "editor") client: httpx.Client = get_client(self.__app, "editor")
response: httpx.Response response: httpx.Response
response = client.get(LIST_URI) response = client.get(LIST_URI)

View File

@ -40,9 +40,10 @@ class ConsoleCommandTestCase(unittest.TestCase):
:return: None. :return: None.
""" """
self.app: Flask = create_test_app() self.__app: Flask = create_test_app()
"""The Flask application."""
with self.app.app_context(): with self.__app.app_context():
# Drop every accounting table, to see if accounting-init recreates # Drop every accounting table, to see if accounting-init recreates
# them correctly. # them correctly.
tables: list[sa.Table] \ tables: list[sa.Table] \
@ -61,8 +62,8 @@ class ConsoleCommandTestCase(unittest.TestCase):
:return: None. :return: None.
""" """
runner: FlaskCliRunner = self.app.test_cli_runner() runner: FlaskCliRunner = self.__app.test_cli_runner()
with self.app.app_context(): with self.__app.app_context():
result: Result = runner.invoke( result: Result = runner.invoke(
args=["accounting-init-db", "-u", "editor"]) args=["accounting-init-db", "-u", "editor"])
self.assertEqual(result.exit_code, 0, self.assertEqual(result.exit_code, 0,
@ -80,14 +81,15 @@ class ConsoleCommandTestCase(unittest.TestCase):
from accounting.models import BaseAccount from accounting.models import BaseAccount
with open(data_dir / "base_accounts.csv") as fp: with open(data_dir / "base_accounts.csv") as fp:
rows: list[dict[str, str]] = list(csv.DictReader(fp))
data: dict[dict[str, Any]] \ data: dict[dict[str, Any]] \
= {x["code"]: {"code": x["code"], = {x["code"]: {"code": x["code"],
"title": x["title"], "title": x["title"],
"l10n": {y[5:]: x[y] "l10n": {y[5:]: x[y]
for y in x if y.startswith("l10n-")}} for y in x if y.startswith("l10n-")}}
for x in csv.DictReader(fp)} for x in rows}
with self.app.app_context(): with self.__app.app_context():
accounts: list[BaseAccount] = BaseAccount.query.all() accounts: list[BaseAccount] = BaseAccount.query.all()
self.assertEqual(len(accounts), len(data)) self.assertEqual(len(accounts), len(data))
@ -108,7 +110,7 @@ class ConsoleCommandTestCase(unittest.TestCase):
""" """
from accounting.models import BaseAccount, Account, AccountL10n from accounting.models import BaseAccount, Account, AccountL10n
with self.app.app_context(): with self.__app.app_context():
bases: list[BaseAccount] = BaseAccount.query\ bases: list[BaseAccount] = BaseAccount.query\
.filter(sa.func.char_length(BaseAccount.code) == 4).all() .filter(sa.func.char_length(BaseAccount.code) == 4).all()
accounts: list[Account] = Account.query.all() accounts: list[Account] = Account.query.all()
@ -142,7 +144,7 @@ class ConsoleCommandTestCase(unittest.TestCase):
for y in x if y.startswith("l10n-")}} for y in x if y.startswith("l10n-")}}
for x in csv.DictReader(fp)} for x in csv.DictReader(fp)}
with self.app.app_context(): with self.__app.app_context():
currencies: list[Currency] = Currency.query.all() currencies: list[Currency] = Currency.query.all()
self.assertEqual(len(currencies), len(data)) self.assertEqual(len(currencies), len(data))

View File

@ -25,8 +25,8 @@ from flask import Flask
from accounting.utils.next_uri import encode_next from accounting.utils.next_uri import encode_next
from test_site import db from test_site import db
from testlib import NEXT_URI, create_test_app, get_client, set_locale, \ from testlib import NEXT_URI, create_test_app, get_client, get_csrf_token, \
add_journal_entry set_locale, add_journal_entry
class CurrencyData: class CurrencyData:
@ -65,26 +65,30 @@ class CurrencyTestCase(unittest.TestCase):
:return: None. :return: None.
""" """
self.app: Flask = create_test_app() self.__app: Flask = create_test_app()
"""The Flask application."""
with self.app.app_context(): with self.__app.app_context():
from accounting.models import Currency, CurrencyL10n from accounting.models import Currency, CurrencyL10n
CurrencyL10n.query.delete() CurrencyL10n.query.delete()
Currency.query.delete() Currency.query.delete()
db.session.commit() db.session.commit()
self.client, self.csrf_token = get_client(self.app, "editor") self.__client: httpx.Client = get_client(self.__app, "editor")
"""The user client."""
self.__csrf_token: str = get_csrf_token(self.__client)
"""The CSRF token."""
response: httpx.Response response: httpx.Response
response = self.client.post(f"{PREFIX}/store", response = self.__client.post(f"{PREFIX}/store",
data={"csrf_token": self.csrf_token, data={"csrf_token": self.__csrf_token,
"code": USD.code, "code": USD.code,
"name": USD.name}) "name": USD.name})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], f"{PREFIX}/{USD.code}") self.assertEqual(response.headers["Location"], f"{PREFIX}/{USD.code}")
response = self.client.post(f"{PREFIX}/store", response = self.__client.post(f"{PREFIX}/store",
data={"csrf_token": self.csrf_token, data={"csrf_token": self.__csrf_token,
"code": EUR.code, "code": EUR.code,
"name": EUR.name}) "name": EUR.name})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
@ -95,7 +99,8 @@ class CurrencyTestCase(unittest.TestCase):
:return: None. :return: None.
""" """
client, csrf_token = get_client(self.app, "nobody") client: httpx.Client = get_client(self.__app, "nobody")
csrf_token: str = get_csrf_token(client)
response: httpx.Response response: httpx.Response
response = client.get(PREFIX) response = client.get(PREFIX)
@ -131,7 +136,8 @@ class CurrencyTestCase(unittest.TestCase):
:return: None. :return: None.
""" """
client, csrf_token = get_client(self.app, "viewer") client: httpx.Client = get_client(self.__app, "viewer")
csrf_token: str = get_csrf_token(client)
response: httpx.Response response: httpx.Response
response = client.get(PREFIX) response = client.get(PREFIX)
@ -169,34 +175,34 @@ class CurrencyTestCase(unittest.TestCase):
""" """
response: httpx.Response response: httpx.Response
response = self.client.get(PREFIX) response = self.__client.get(PREFIX)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
response = self.client.get(f"{PREFIX}/{USD.code}") response = self.__client.get(f"{PREFIX}/{USD.code}")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
response = self.client.get(f"{PREFIX}/create") response = self.__client.get(f"{PREFIX}/create")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
response = self.client.post(f"{PREFIX}/store", response = self.__client.post(f"{PREFIX}/store",
data={"csrf_token": self.csrf_token, data={"csrf_token": self.__csrf_token,
"code": TWD.code, "code": TWD.code,
"name": TWD.name}) "name": TWD.name})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], f"{PREFIX}/{TWD.code}") self.assertEqual(response.headers["Location"], f"{PREFIX}/{TWD.code}")
response = self.client.get(f"{PREFIX}/{USD.code}/edit") response = self.__client.get(f"{PREFIX}/{USD.code}/edit")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
response = self.client.post(f"{PREFIX}/{USD.code}/update", response = self.__client.post(f"{PREFIX}/{USD.code}/update",
data={"csrf_token": self.csrf_token, data={"csrf_token": self.__csrf_token,
"code": JPY.code, "code": JPY.code,
"name": JPY.name}) "name": JPY.name})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], f"{PREFIX}/{JPY.code}") self.assertEqual(response.headers["Location"], f"{PREFIX}/{JPY.code}")
response = self.client.post(f"{PREFIX}/{EUR.code}/delete", response = self.__client.post(f"{PREFIX}/{EUR.code}/delete",
data={"csrf_token": self.csrf_token}) data={"csrf_token": self.__csrf_token})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], PREFIX) self.assertEqual(response.headers["Location"], PREFIX)
@ -211,72 +217,73 @@ class CurrencyTestCase(unittest.TestCase):
detail_uri: str = f"{PREFIX}/{TWD.code}" detail_uri: str = f"{PREFIX}/{TWD.code}"
response: httpx.Response response: httpx.Response
with self.app.app_context(): with self.__app.app_context():
self.assertEqual({x.code for x in Currency.query.all()}, self.assertEqual({x.code for x in Currency.query.all()},
{USD.code, EUR.code}) {USD.code, EUR.code})
# Missing CSRF token # Missing CSRF token
response = self.client.post(store_uri, response = self.__client.post(store_uri,
data={"code": TWD.code, data={"code": TWD.code,
"name": TWD.name}) "name": TWD.name})
self.assertEqual(response.status_code, 400) self.assertEqual(response.status_code, 400)
# CSRF token mismatch # CSRF token mismatch
response = self.client.post(store_uri, response = self.__client.post(store_uri,
data={"csrf_token": f"{self.csrf_token}-2", data={"csrf_token":
f"{self.__csrf_token}-2",
"code": TWD.code, "code": TWD.code,
"name": TWD.name}) "name": TWD.name})
self.assertEqual(response.status_code, 400) self.assertEqual(response.status_code, 400)
# Empty code # Empty code
response = self.client.post(store_uri, response = self.__client.post(store_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.__csrf_token,
"code": " ", "code": " ",
"name": TWD.name}) "name": TWD.name})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], create_uri) self.assertEqual(response.headers["Location"], create_uri)
# Blocked code, with spaces to be stripped # Blocked code, with spaces to be stripped
response = self.client.post(store_uri, response = self.__client.post(store_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.__csrf_token,
"code": " create ", "code": " create ",
"name": TWD.name}) "name": TWD.name})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], create_uri) self.assertEqual(response.headers["Location"], create_uri)
# Bad code # Bad code
response = self.client.post(store_uri, response = self.__client.post(store_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.__csrf_token,
"code": " zzc ", "code": " zzc ",
"name": TWD.name}) "name": TWD.name})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], create_uri) self.assertEqual(response.headers["Location"], create_uri)
# Empty name # Empty name
response = self.client.post(store_uri, response = self.__client.post(store_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.__csrf_token,
"code": TWD.code, "code": TWD.code,
"name": " "}) "name": " "})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], create_uri) self.assertEqual(response.headers["Location"], create_uri)
# Success, with spaces to be stripped # Success, with spaces to be stripped
response = self.client.post(store_uri, response = self.__client.post(store_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.__csrf_token,
"code": f" {TWD.code} ", "code": f" {TWD.code} ",
"name": f" {TWD.name} "}) "name": f" {TWD.name} "})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], detail_uri) self.assertEqual(response.headers["Location"], detail_uri)
# Duplicated code # Duplicated code
response = self.client.post(store_uri, response = self.__client.post(store_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.__csrf_token,
"code": TWD.code, "code": TWD.code,
"name": TWD.name}) "name": TWD.name})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], create_uri) self.assertEqual(response.headers["Location"], create_uri)
with self.app.app_context(): with self.__app.app_context():
self.assertEqual({x.code for x in Currency.query.all()}, self.assertEqual({x.code for x in Currency.query.all()},
{USD.code, EUR.code, TWD.code}) {USD.code, EUR.code, TWD.code})
@ -297,70 +304,70 @@ class CurrencyTestCase(unittest.TestCase):
response: httpx.Response response: httpx.Response
# Success, with spaces to be stripped # Success, with spaces to be stripped
response = self.client.post(update_uri, response = self.__client.post(update_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.__csrf_token,
"code": f" {USD.code} ", "code": f" {USD.code} ",
"name": f" {USD.name}-1 "}) "name": f" {USD.name}-1 "})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], detail_uri) self.assertEqual(response.headers["Location"], detail_uri)
with self.app.app_context(): with self.__app.app_context():
currency: Currency = db.session.get(Currency, USD.code) currency: Currency = db.session.get(Currency, USD.code)
self.assertEqual(currency.code, USD.code) self.assertEqual(currency.code, USD.code)
self.assertEqual(currency.name_l10n, f"{USD.name}-1") self.assertEqual(currency.name_l10n, f"{USD.name}-1")
# Empty code # Empty code
response = self.client.post(update_uri, response = self.__client.post(update_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.__csrf_token,
"code": " ", "code": " ",
"name": TWD.name}) "name": TWD.name})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], edit_uri) self.assertEqual(response.headers["Location"], edit_uri)
# Blocked code, with spaces to be stripped # Blocked code, with spaces to be stripped
response = self.client.post(update_uri, response = self.__client.post(update_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.__csrf_token,
"code": " create ", "code": " create ",
"name": TWD.name}) "name": TWD.name})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], edit_uri) self.assertEqual(response.headers["Location"], edit_uri)
# Bad code # Bad code
response = self.client.post(update_uri, response = self.__client.post(update_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.__csrf_token,
"code": "abc/def", "code": "abc/def",
"name": TWD.name}) "name": TWD.name})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], edit_uri) self.assertEqual(response.headers["Location"], edit_uri)
# Empty name # Empty name
response = self.client.post(update_uri, response = self.__client.post(update_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.__csrf_token,
"code": TWD.code, "code": TWD.code,
"name": " "}) "name": " "})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], edit_uri) self.assertEqual(response.headers["Location"], edit_uri)
# Duplicated code # Duplicated code
response = self.client.post(update_uri, response = self.__client.post(update_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.__csrf_token,
"code": EUR.code, "code": EUR.code,
"name": TWD.name}) "name": TWD.name})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], edit_uri) self.assertEqual(response.headers["Location"], edit_uri)
# Change code # Change code
response = self.client.post(update_uri, response = self.__client.post(update_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.__csrf_token,
"code": TWD.code, "code": TWD.code,
"name": TWD.name}) "name": TWD.name})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], detail_c_uri) self.assertEqual(response.headers["Location"], detail_c_uri)
response = self.client.get(detail_uri) response = self.__client.get(detail_uri)
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
response = self.client.get(detail_c_uri) response = self.__client.get(detail_c_uri)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
def test_update_not_modified(self) -> None: def test_update_not_modified(self) -> None:
@ -374,14 +381,14 @@ class CurrencyTestCase(unittest.TestCase):
currency: Currency | None currency: Currency | None
response: httpx.Response response: httpx.Response
response = self.client.post(update_uri, response = self.__client.post(update_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.__csrf_token,
"code": f" {USD.code} ", "code": f" {USD.code} ",
"name": f" {USD.name} "}) "name": f" {USD.name} "})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], detail_uri) self.assertEqual(response.headers["Location"], detail_uri)
with self.app.app_context(): with self.__app.app_context():
currency = db.session.get(Currency, USD.code) currency = db.session.get(Currency, USD.code)
self.assertIsNotNone(currency) self.assertIsNotNone(currency)
currency.created_at \ currency.created_at \
@ -389,14 +396,14 @@ class CurrencyTestCase(unittest.TestCase):
currency.updated_at = currency.created_at currency.updated_at = currency.created_at
db.session.commit() db.session.commit()
response = self.client.post(update_uri, response = self.__client.post(update_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.__csrf_token,
"code": USD.code, "code": USD.code,
"name": TWD.name}) "name": TWD.name})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], detail_uri) self.assertEqual(response.headers["Location"], detail_uri)
with self.app.app_context(): with self.__app.app_context():
currency = db.session.get(Currency, USD.code) currency = db.session.get(Currency, USD.code)
self.assertIsNotNone(currency) self.assertIsNotNone(currency)
self.assertLess(currency.created_at, self.assertLess(currency.created_at,
@ -409,13 +416,14 @@ class CurrencyTestCase(unittest.TestCase):
""" """
from accounting.models import Currency from accounting.models import Currency
editor_username, admin_username = "editor", "admin" editor_username, admin_username = "editor", "admin"
client, csrf_token = get_client(self.app, admin_username) client: httpx.Client = get_client(self.__app, admin_username)
csrf_token: str = get_csrf_token(client)
detail_uri: str = f"{PREFIX}/{USD.code}" detail_uri: str = f"{PREFIX}/{USD.code}"
update_uri: str = f"{PREFIX}/{USD.code}/update" update_uri: str = f"{PREFIX}/{USD.code}/update"
currency: Currency currency: Currency
response: httpx.Response response: httpx.Response
with self.app.app_context(): with self.__app.app_context():
currency = db.session.get(Currency, USD.code) currency = db.session.get(Currency, USD.code)
self.assertEqual(currency.created_by.username, editor_username) self.assertEqual(currency.created_by.username, editor_username)
self.assertEqual(currency.updated_by.username, editor_username) self.assertEqual(currency.updated_by.username, editor_username)
@ -427,7 +435,7 @@ class CurrencyTestCase(unittest.TestCase):
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], detail_uri) self.assertEqual(response.headers["Location"], detail_uri)
with self.app.app_context(): with self.__app.app_context():
currency = db.session.get(Currency, USD.code) currency = db.session.get(Currency, USD.code)
self.assertEqual(currency.created_by.username, editor_username) self.assertEqual(currency.created_by.username, editor_username)
self.assertEqual(currency.updated_by.username, admin_username) self.assertEqual(currency.updated_by.username, admin_username)
@ -439,14 +447,14 @@ class CurrencyTestCase(unittest.TestCase):
""" """
response: httpx.Response response: httpx.Response
response = self.client.get( response = self.__client.get(
f"/accounting/api/currencies/exists-code?q={USD.code}") f"/accounting/api/currencies/exists-code?q={USD.code}")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
data = response.json() data = response.json()
self.assertEqual(set(data.keys()), {"exists"}) self.assertEqual(set(data.keys()), {"exists"})
self.assertTrue(data["exists"]) self.assertTrue(data["exists"])
response = self.client.get( response = self.__client.get(
f"/accounting/api/currencies/exists-code?q={USD.code}-1") f"/accounting/api/currencies/exists-code?q={USD.code}-1")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
data = response.json() data = response.json()
@ -464,51 +472,51 @@ class CurrencyTestCase(unittest.TestCase):
currency: Currency currency: Currency
response: httpx.Response response: httpx.Response
with self.app.app_context(): with self.__app.app_context():
currency = db.session.get(Currency, USD.code) currency = db.session.get(Currency, USD.code)
self.assertEqual(currency.name_l10n, USD.name) self.assertEqual(currency.name_l10n, USD.name)
self.assertEqual(currency.l10n, []) self.assertEqual(currency.l10n, [])
set_locale(self.app, self.client, self.csrf_token, "zh_Hant") set_locale(self.__app, self.__client, self.__csrf_token, "zh_Hant")
response = self.client.post(update_uri, response = self.__client.post(update_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.__csrf_token,
"code": USD.code, "code": USD.code,
"name": f"{USD.name}-zh_Hant"}) "name": f"{USD.name}-zh_Hant"})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], detail_uri) self.assertEqual(response.headers["Location"], detail_uri)
with self.app.app_context(): with self.__app.app_context():
currency = db.session.get(Currency, USD.code) currency = db.session.get(Currency, USD.code)
self.assertEqual(currency.name_l10n, USD.name) self.assertEqual(currency.name_l10n, USD.name)
self.assertEqual({(x.locale, x.name) for x in currency.l10n}, self.assertEqual({(x.locale, x.name) for x in currency.l10n},
{("zh_Hant", f"{USD.name}-zh_Hant")}) {("zh_Hant", f"{USD.name}-zh_Hant")})
set_locale(self.app, self.client, self.csrf_token, "en") set_locale(self.__app, self.__client, self.__csrf_token, "en")
response = self.client.post(update_uri, response = self.__client.post(update_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.__csrf_token,
"code": USD.code, "code": USD.code,
"name": f"{USD.name}-2"}) "name": f"{USD.name}-2"})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], detail_uri) self.assertEqual(response.headers["Location"], detail_uri)
with self.app.app_context(): with self.__app.app_context():
currency = db.session.get(Currency, USD.code) currency = db.session.get(Currency, USD.code)
self.assertEqual(currency.name_l10n, f"{USD.name}-2") self.assertEqual(currency.name_l10n, f"{USD.name}-2")
self.assertEqual({(x.locale, x.name) for x in currency.l10n}, self.assertEqual({(x.locale, x.name) for x in currency.l10n},
{("zh_Hant", f"{USD.name}-zh_Hant")}) {("zh_Hant", f"{USD.name}-zh_Hant")})
set_locale(self.app, self.client, self.csrf_token, "zh_Hant") set_locale(self.__app, self.__client, self.__csrf_token, "zh_Hant")
response = self.client.post(update_uri, response = self.__client.post(update_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.__csrf_token,
"code": USD.code, "code": USD.code,
"name": f"{USD.name}-zh_Hant-2"}) "name": f"{USD.name}-zh_Hant-2"})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], detail_uri) self.assertEqual(response.headers["Location"], detail_uri)
with self.app.app_context(): with self.__app.app_context():
currency = db.session.get(Currency, USD.code) currency = db.session.get(Currency, USD.code)
self.assertEqual(currency.name_l10n, f"{USD.name}-2") self.assertEqual(currency.name_l10n, f"{USD.name}-2")
self.assertEqual({(x.locale, x.name) for x in currency.l10n}, self.assertEqual({(x.locale, x.name) for x in currency.l10n},
@ -522,56 +530,56 @@ class CurrencyTestCase(unittest.TestCase):
from accounting.models import Currency from accounting.models import Currency
detail_uri: str = f"{PREFIX}/{JPY.code}" detail_uri: str = f"{PREFIX}/{JPY.code}"
delete_uri: str = f"{PREFIX}/{JPY.code}/delete" delete_uri: str = f"{PREFIX}/{JPY.code}/delete"
with self.app.app_context(): with self.__app.app_context():
encoded_next_uri: str = encode_next(NEXT_URI) encoded_next_uri: str = encode_next(NEXT_URI)
list_uri: str = PREFIX list_uri: str = PREFIX
response: httpx.Response response: httpx.Response
response = self.client.post(f"{PREFIX}/store", response = self.__client.post(f"{PREFIX}/store",
data={"csrf_token": self.csrf_token, data={"csrf_token": self.__csrf_token,
"code": JPY.code, "code": JPY.code,
"name": JPY.name}) "name": JPY.name})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], detail_uri) self.assertEqual(response.headers["Location"], detail_uri)
add_journal_entry(self.client, add_journal_entry(self.__client,
form={"csrf_token": self.csrf_token, form={"csrf_token": self.__csrf_token,
"next": encoded_next_uri, "next": encoded_next_uri,
"date": dt.date.today().isoformat(), "date": dt.date.today().isoformat(),
"currency-1-code": EUR.code, "currency-1-code": EUR.code,
"currency-1-credit-1-account_code": "1111-001", "currency-1-credit-1-account_code": "1111-001",
"currency-1-credit-1-amount": "20"}) "currency-1-credit-1-amount": "20"})
with self.app.app_context(): with self.__app.app_context():
self.assertEqual({x.code for x in Currency.query.all()}, self.assertEqual({x.code for x in Currency.query.all()},
{USD.code, EUR.code, JPY.code}) {USD.code, EUR.code, JPY.code})
# Cannot delete the default currency # Cannot delete the default currency
response = self.client.post(f"{PREFIX}/{USD.code}/delete", response = self.__client.post(f"{PREFIX}/{USD.code}/delete",
data={"csrf_token": self.csrf_token}) data={"csrf_token": self.__csrf_token})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], f"{PREFIX}/{USD.code}") self.assertEqual(response.headers["Location"], f"{PREFIX}/{USD.code}")
# Cannot delete the account that is in use # Cannot delete the account that is in use
response = self.client.post(f"{PREFIX}/{EUR.code}/delete", response = self.__client.post(f"{PREFIX}/{EUR.code}/delete",
data={"csrf_token": self.csrf_token}) data={"csrf_token": self.__csrf_token})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], f"{PREFIX}/{EUR.code}") self.assertEqual(response.headers["Location"], f"{PREFIX}/{EUR.code}")
# Success # Success
response = self.client.get(detail_uri) response = self.__client.get(detail_uri)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
response = self.client.post(delete_uri, response = self.__client.post(delete_uri,
data={"csrf_token": self.csrf_token}) data={"csrf_token": self.__csrf_token})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], list_uri) self.assertEqual(response.headers["Location"], list_uri)
with self.app.app_context(): with self.__app.app_context():
self.assertEqual({x.code for x in Currency.query.all()}, self.assertEqual({x.code for x in Currency.query.all()},
{USD.code, EUR.code}) {USD.code, EUR.code})
response = self.client.get(detail_uri) response = self.__client.get(detail_uri)
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
response = self.client.post(delete_uri, response = self.__client.post(delete_uri,
data={"csrf_token": self.csrf_token}) data={"csrf_token": self.__csrf_token})
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)

View File

@ -20,11 +20,12 @@
import datetime as dt import datetime as dt
import unittest import unittest
import httpx
from flask import Flask from flask import Flask
from accounting.utils.next_uri import encode_next from accounting.utils.next_uri import encode_next
from testlib import NEXT_URI, Accounts, create_test_app, get_client, \ from testlib import NEXT_URI, Accounts, create_test_app, get_client, \
add_journal_entry get_csrf_token, add_journal_entry
class DescriptionEditorTestCase(unittest.TestCase): class DescriptionEditorTestCase(unittest.TestCase):
@ -36,15 +37,20 @@ class DescriptionEditorTestCase(unittest.TestCase):
:return: None. :return: None.
""" """
self.app: Flask = create_test_app() self.__app: Flask = create_test_app()
"""The Flask application."""
with self.app.app_context(): with self.__app.app_context():
from accounting.models import JournalEntry, JournalEntryLineItem from accounting.models import JournalEntry, JournalEntryLineItem
JournalEntry.query.delete() JournalEntry.query.delete()
JournalEntryLineItem.query.delete() JournalEntryLineItem.query.delete()
self.encoded_next_uri: str = encode_next(NEXT_URI) self.__encoded_next_uri: str = encode_next(NEXT_URI)
"""The encoded next URI."""
self.client, self.csrf_token = get_client(self.app, "editor") self.__client: httpx.Client = get_client(self.__app, "editor")
"""The user client."""
self.__csrf_token: str = get_csrf_token(self.__client)
"""The CSRF token."""
def test_description_editor(self) -> None: def test_description_editor(self) -> None:
"""Test the description editor. """Test the description editor.
@ -53,9 +59,9 @@ class DescriptionEditorTestCase(unittest.TestCase):
""" """
from accounting.journal_entry.utils.description_editor import \ from accounting.journal_entry.utils.description_editor import \
DescriptionEditor DescriptionEditor
for form in get_form_data(self.csrf_token, self.encoded_next_uri): for form in get_form_data(self.__csrf_token, self.__encoded_next_uri):
add_journal_entry(self.client, form) add_journal_entry(self.__client, form)
with self.app.app_context(): with self.__app.app_context():
editor: DescriptionEditor = DescriptionEditor() editor: DescriptionEditor = DescriptionEditor()
# Debit-General # Debit-General

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -25,7 +25,8 @@ from flask import Flask
from accounting.utils.next_uri import encode_next from accounting.utils.next_uri import encode_next
from test_site import db from test_site import db
from testlib import NEXT_URI, Accounts, create_test_app, get_client from testlib import NEXT_URI, Accounts, create_test_app, get_client, \
get_csrf_token
PREFIX: str = "/accounting/options" PREFIX: str = "/accounting/options"
"""The URL prefix for the option management.""" """The URL prefix for the option management."""
@ -40,23 +41,29 @@ class OptionTestCase(unittest.TestCase):
:return: None. :return: None.
""" """
self.app: Flask = create_test_app() self.__app: Flask = create_test_app()
"""The Flask application."""
with self.app.app_context(): with self.__app.app_context():
from accounting.models import Option from accounting.models import Option
Option.query.delete() Option.query.delete()
self.encoded_next_uri: str = encode_next(NEXT_URI) self.__encoded_next_uri: str = encode_next(NEXT_URI)
"""The encoded next URI."""
self.client, self.csrf_token = get_client(self.app, "admin") self.__client: httpx.Client = get_client(self.__app, "admin")
"""The user client."""
self.__csrf_token: str = get_csrf_token(self.__client)
"""The CSRF token."""
def test_nobody(self) -> None: def test_nobody(self) -> None:
"""Test the permission as nobody. """Test the permission as nobody.
:return: None. :return: None.
""" """
client, csrf_token = get_client(self.app, "nobody") client: httpx.Client = get_client(self.__app, "nobody")
detail_uri: str = f"{PREFIX}?next={self.encoded_next_uri}" csrf_token: str = get_csrf_token(client)
edit_uri: str = f"{PREFIX}/edit?next={self.encoded_next_uri}" detail_uri: str = f"{PREFIX}?next={self.__encoded_next_uri}"
edit_uri: str = f"{PREFIX}/edit?next={self.__encoded_next_uri}"
update_uri: str = f"{PREFIX}/update" update_uri: str = f"{PREFIX}/update"
response: httpx.Response response: httpx.Response
@ -74,9 +81,10 @@ class OptionTestCase(unittest.TestCase):
:return: None. :return: None.
""" """
client, csrf_token = get_client(self.app, "viewer") client: httpx.Client = get_client(self.__app, "viewer")
detail_uri: str = f"{PREFIX}?next={self.encoded_next_uri}" csrf_token: str = get_csrf_token(client)
edit_uri: str = f"{PREFIX}/edit?next={self.encoded_next_uri}" detail_uri: str = f"{PREFIX}?next={self.__encoded_next_uri}"
edit_uri: str = f"{PREFIX}/edit?next={self.__encoded_next_uri}"
update_uri: str = f"{PREFIX}/update" update_uri: str = f"{PREFIX}/update"
response: httpx.Response response: httpx.Response
@ -94,9 +102,10 @@ class OptionTestCase(unittest.TestCase):
:return: None. :return: None.
""" """
client, csrf_token = get_client(self.app, "editor") client: httpx.Client = get_client(self.__app, "editor")
detail_uri: str = f"{PREFIX}?next={self.encoded_next_uri}" csrf_token: str = get_csrf_token(client)
edit_uri: str = f"{PREFIX}/edit?next={self.encoded_next_uri}" detail_uri: str = f"{PREFIX}?next={self.__encoded_next_uri}"
edit_uri: str = f"{PREFIX}/edit?next={self.__encoded_next_uri}"
update_uri: str = f"{PREFIX}/update" update_uri: str = f"{PREFIX}/update"
response: httpx.Response response: httpx.Response
@ -114,18 +123,18 @@ class OptionTestCase(unittest.TestCase):
:return: None. :return: None.
""" """
detail_uri: str = f"{PREFIX}?next={self.encoded_next_uri}" detail_uri: str = f"{PREFIX}?next={self.__encoded_next_uri}"
edit_uri: str = f"{PREFIX}/edit?next={self.encoded_next_uri}" edit_uri: str = f"{PREFIX}/edit?next={self.__encoded_next_uri}"
update_uri: str = f"{PREFIX}/update" update_uri: str = f"{PREFIX}/update"
response: httpx.Response response: httpx.Response
response = self.client.get(detail_uri) response = self.__client.get(detail_uri)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
response = self.client.get(edit_uri) response = self.__client.get(edit_uri)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
response = self.client.post(update_uri, data=self.__get_form()) response = self.__client.post(update_uri, data=self.__get_form())
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], detail_uri) self.assertEqual(response.headers["Location"], detail_uri)
@ -135,8 +144,8 @@ class OptionTestCase(unittest.TestCase):
:return: None. :return: None.
""" """
from accounting.utils.options import options from accounting.utils.options import options
detail_uri: str = f"{PREFIX}?next={self.encoded_next_uri}" detail_uri: str = f"{PREFIX}?next={self.__encoded_next_uri}"
edit_uri: str = f"{PREFIX}/edit?next={self.encoded_next_uri}" edit_uri: str = f"{PREFIX}/edit?next={self.__encoded_next_uri}"
update_uri: str = f"{PREFIX}/update" update_uri: str = f"{PREFIX}/update"
form: dict[str, str] form: dict[str, str]
response: httpx.Response response: httpx.Response
@ -144,35 +153,35 @@ class OptionTestCase(unittest.TestCase):
# Empty currency code # Empty currency code
form = self.__get_form() form = self.__get_form()
form["default_currency_code"] = " " form["default_currency_code"] = " "
response = self.client.post(update_uri, data=form) response = self.__client.post(update_uri, data=form)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], edit_uri) self.assertEqual(response.headers["Location"], edit_uri)
# Non-existing currency code # Non-existing currency code
form = self.__get_form() form = self.__get_form()
form["default_currency_code"] = "ZZZ" form["default_currency_code"] = "ZZZ"
response = self.client.post(update_uri, data=form) response = self.__client.post(update_uri, data=form)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], edit_uri) self.assertEqual(response.headers["Location"], edit_uri)
# Empty current account # Empty current account
form = self.__get_form() form = self.__get_form()
form["default_ie_account_code"] = " " form["default_ie_account_code"] = " "
response = self.client.post(update_uri, data=form) response = self.__client.post(update_uri, data=form)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], edit_uri) self.assertEqual(response.headers["Location"], edit_uri)
# Non-existing current account # Non-existing current account
form = self.__get_form() form = self.__get_form()
form["default_ie_account_code"] = "9999-999" form["default_ie_account_code"] = "9999-999"
response = self.client.post(update_uri, data=form) response = self.__client.post(update_uri, data=form)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], edit_uri) self.assertEqual(response.headers["Location"], edit_uri)
# Not a current account # Not a current account
form = self.__get_form() form = self.__get_form()
form["default_ie_account_code"] = Accounts.MEAL form["default_ie_account_code"] = Accounts.MEAL
response = self.client.post(update_uri, data=form) response = self.__client.post(update_uri, data=form)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], edit_uri) self.assertEqual(response.headers["Location"], edit_uri)
@ -180,7 +189,7 @@ class OptionTestCase(unittest.TestCase):
form = self.__get_form() form = self.__get_form()
key = [x for x in form if x.endswith("-name")][0] key = [x for x in form if x.endswith("-name")][0]
form[key] = " " form[key] = " "
response = self.client.post(update_uri, data=form) response = self.__client.post(update_uri, data=form)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], edit_uri) self.assertEqual(response.headers["Location"], edit_uri)
@ -188,7 +197,7 @@ class OptionTestCase(unittest.TestCase):
form = self.__get_form() form = self.__get_form()
key = [x for x in form if x.endswith("-account_code")][0] key = [x for x in form if x.endswith("-account_code")][0]
form[key] = " " form[key] = " "
response = self.client.post(update_uri, data=form) response = self.__client.post(update_uri, data=form)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], edit_uri) self.assertEqual(response.headers["Location"], edit_uri)
@ -198,7 +207,7 @@ class OptionTestCase(unittest.TestCase):
if x.startswith("recurring-expense-") if x.startswith("recurring-expense-")
and x.endswith("-account_code")][0] and x.endswith("-account_code")][0]
form[key] = Accounts.SERVICE form[key] = Accounts.SERVICE
response = self.client.post(update_uri, data=form) response = self.__client.post(update_uri, data=form)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], edit_uri) self.assertEqual(response.headers["Location"], edit_uri)
@ -208,7 +217,7 @@ class OptionTestCase(unittest.TestCase):
if x.startswith("recurring-income-") if x.startswith("recurring-income-")
and x.endswith("-account_code")][0] and x.endswith("-account_code")][0]
form[key] = Accounts.UTILITIES form[key] = Accounts.UTILITIES
response = self.client.post(update_uri, data=form) response = self.__client.post(update_uri, data=form)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], edit_uri) self.assertEqual(response.headers["Location"], edit_uri)
@ -218,7 +227,7 @@ class OptionTestCase(unittest.TestCase):
if x.startswith("recurring-expense-") if x.startswith("recurring-expense-")
and x.endswith("-account_code")][0] and x.endswith("-account_code")][0]
form[key] = Accounts.PAYABLE form[key] = Accounts.PAYABLE
response = self.client.post(update_uri, data=form) response = self.__client.post(update_uri, data=form)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], edit_uri) self.assertEqual(response.headers["Location"], edit_uri)
@ -228,7 +237,7 @@ class OptionTestCase(unittest.TestCase):
if x.startswith("recurring-income-") if x.startswith("recurring-income-")
and x.endswith("-account_code")][0] and x.endswith("-account_code")][0]
form[key] = Accounts.RECEIVABLE form[key] = Accounts.RECEIVABLE
response = self.client.post(update_uri, data=form) response = self.__client.post(update_uri, data=form)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], edit_uri) self.assertEqual(response.headers["Location"], edit_uri)
@ -236,22 +245,22 @@ class OptionTestCase(unittest.TestCase):
form = self.__get_form() form = self.__get_form()
key = [x for x in form if x.endswith("-description_template")][0] key = [x for x in form if x.endswith("-description_template")][0]
form[key] = " " form[key] = " "
response = self.client.post(update_uri, data=form) response = self.__client.post(update_uri, data=form)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], edit_uri) self.assertEqual(response.headers["Location"], edit_uri)
# Success, with malformed order # Success, with malformed order
with self.app.app_context(): with self.__app.app_context():
self.assertEqual(options.default_currency_code, "USD") self.assertEqual(options.default_currency_code, "USD")
self.assertEqual(options.default_ie_account_code, "1111-001") self.assertEqual(options.default_ie_account_code, "1111-001")
self.assertEqual(len(options.recurring.expenses), 0) self.assertEqual(len(options.recurring.expenses), 0)
self.assertEqual(len(options.recurring.incomes), 0) self.assertEqual(len(options.recurring.incomes), 0)
response = self.client.post(update_uri, data=self.__get_form()) response = self.__client.post(update_uri, data=self.__get_form())
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], detail_uri) self.assertEqual(response.headers["Location"], detail_uri)
with self.app.app_context(): with self.__app.app_context():
self.assertEqual(options.default_currency_code, "EUR") self.assertEqual(options.default_currency_code, "EUR")
self.assertEqual(options.default_ie_account_code, "0000-000") self.assertEqual(options.default_ie_account_code, "0000-000")
self.assertEqual(len(options.recurring.expenses), 4) self.assertEqual(len(options.recurring.expenses), 4)
@ -272,11 +281,11 @@ class OptionTestCase(unittest.TestCase):
# Success, with no recurring data # Success, with no recurring data
form = self.__get_form() form = self.__get_form()
form = {x: form[x] for x in form if not x.startswith("recurring-")} form = {x: form[x] for x in form if not x.startswith("recurring-")}
response = self.client.post(update_uri, data=form) response = self.__client.post(update_uri, data=form)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], detail_uri) self.assertEqual(response.headers["Location"], detail_uri)
with self.app.app_context(): with self.__app.app_context():
self.assertEqual(len(options.recurring.expenses), 0) self.assertEqual(len(options.recurring.expenses), 0)
self.assertEqual(len(options.recurring.incomes), 0) self.assertEqual(len(options.recurring.incomes), 0)
@ -286,17 +295,17 @@ class OptionTestCase(unittest.TestCase):
:return: None. :return: None.
""" """
from accounting.models import Option from accounting.models import Option
detail_uri: str = f"{PREFIX}?next={self.encoded_next_uri}" detail_uri: str = f"{PREFIX}?next={self.__encoded_next_uri}"
update_uri: str = f"{PREFIX}/update" update_uri: str = f"{PREFIX}/update"
form: dict[str, str] form: dict[str, str]
option: Option | None option: Option | None
resource: httpx.Response resource: httpx.Response
response = self.client.post(update_uri, data=self.__get_form()) response = self.__client.post(update_uri, data=self.__get_form())
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], detail_uri) self.assertEqual(response.headers["Location"], detail_uri)
with self.app.app_context(): with self.__app.app_context():
option = db.session.get(Option, "recurring") option = db.session.get(Option, "recurring")
self.assertIsNotNone(option) self.assertIsNotNone(option)
timestamp: dt.datetime \ timestamp: dt.datetime \
@ -308,11 +317,11 @@ class OptionTestCase(unittest.TestCase):
# The recurring setting was not modified # The recurring setting was not modified
form = self.__get_form() form = self.__get_form()
form["default_currency_code"] = "JPY" form["default_currency_code"] = "JPY"
response = self.client.post(update_uri, data=form) response = self.__client.post(update_uri, data=form)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], detail_uri) self.assertEqual(response.headers["Location"], detail_uri)
with self.app.app_context(): with self.__app.app_context():
option = db.session.get(Option, "recurring") option = db.session.get(Option, "recurring")
self.assertIsNotNone(option) self.assertIsNotNone(option)
self.assertEqual(option.created_at, timestamp) self.assertEqual(option.created_at, timestamp)
@ -324,11 +333,11 @@ class OptionTestCase(unittest.TestCase):
if x.startswith("recurring-expense-") if x.startswith("recurring-expense-")
and x.endswith("-account_code")][0] and x.endswith("-account_code")][0]
form[key] = Accounts.MEAL form[key] = Accounts.MEAL
response = self.client.post(update_uri, data=form) response = self.__client.post(update_uri, data=form)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], detail_uri) self.assertEqual(response.headers["Location"], detail_uri)
with self.app.app_context(): with self.__app.app_context():
option = db.session.get(Option, "recurring") option = db.session.get(Option, "recurring")
self.assertIsNotNone(option) self.assertIsNotNone(option)
self.assertLess(option.created_at, option.updated_at) self.assertLess(option.created_at, option.updated_at)
@ -341,16 +350,16 @@ class OptionTestCase(unittest.TestCase):
from accounting.models import Option from accounting.models import Option
from accounting.utils.user import get_user_pk from accounting.utils.user import get_user_pk
admin_username, editor_username = "admin", "editor" admin_username, editor_username = "admin", "editor"
detail_uri: str = f"{PREFIX}?next={self.encoded_next_uri}" detail_uri: str = f"{PREFIX}?next={self.__encoded_next_uri}"
update_uri: str = f"{PREFIX}/update" update_uri: str = f"{PREFIX}/update"
option: Option | None option: Option | None
response: httpx.Response response: httpx.Response
response = self.client.post(update_uri, data=self.__get_form()) response = self.__client.post(update_uri, data=self.__get_form())
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], detail_uri) self.assertEqual(response.headers["Location"], detail_uri)
with self.app.app_context(): with self.__app.app_context():
editor_pk: int = get_user_pk(editor_username) editor_pk: int = get_user_pk(editor_username)
option = db.session.get(Option, "recurring") option = db.session.get(Option, "recurring")
self.assertIsNotNone(option) self.assertIsNotNone(option)
@ -363,11 +372,11 @@ class OptionTestCase(unittest.TestCase):
if x.startswith("recurring-expense-") if x.startswith("recurring-expense-")
and x.endswith("-account_code")][0] and x.endswith("-account_code")][0]
form[key] = Accounts.MEAL form[key] = Accounts.MEAL
response = self.client.post(update_uri, data=form) response = self.__client.post(update_uri, data=form)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], detail_uri) self.assertEqual(response.headers["Location"], detail_uri)
with self.app.app_context(): with self.__app.app_context():
option = db.session.get(Option, "recurring") option = db.session.get(Option, "recurring")
self.assertIsNotNone(option) self.assertIsNotNone(option)
self.assertEqual(option.created_by.username, editor_username) self.assertEqual(option.created_by.username, editor_username)
@ -380,9 +389,9 @@ class OptionTestCase(unittest.TestCase):
:return: The option form. :return: The option form.
""" """
if csrf_token is None: if csrf_token is None:
csrf_token = self.csrf_token csrf_token = self.__csrf_token
return {"csrf_token": csrf_token, return {"csrf_token": csrf_token,
"next": self.encoded_next_uri, "next": self.__encoded_next_uri,
"default_currency_code": "EUR", "default_currency_code": "EUR",
"default_ie_account_code": "0000-000", "default_ie_account_code": "0000-000",
"recurring-expense-1-name": "Water bill", "recurring-expense-1-name": "Water bill",

View File

@ -24,7 +24,7 @@ import httpx
from flask import Flask from flask import Flask
from test_site.lib import BaseTestData from test_site.lib import BaseTestData
from testlib import create_test_app, get_client, Accounts from testlib import create_test_app, get_client, get_csrf_token, Accounts
PREFIX: str = "/accounting" PREFIX: str = "/accounting"
"""The URL prefix for the reports.""" """The URL prefix for the reports."""
@ -41,22 +41,26 @@ class ReportTestCase(unittest.TestCase):
:return: None. :return: None.
""" """
self.app: Flask = create_test_app() self.__app: Flask = create_test_app()
"""The Flask application."""
with self.app.app_context(): with self.__app.app_context():
from accounting.models import JournalEntry, JournalEntryLineItem from accounting.models import JournalEntry, JournalEntryLineItem
JournalEntry.query.delete() JournalEntry.query.delete()
JournalEntryLineItem.query.delete() JournalEntryLineItem.query.delete()
self.client, self.csrf_token = get_client(self.app, "editor") self.__client: httpx.Client = get_client(self.__app, "editor")
"""The user client."""
self.__csrf_token: str = get_csrf_token(self.__client)
"""The CSRF token."""
def test_nobody(self) -> None: def test_nobody(self) -> None:
"""Test the permission as nobody. """Test the permission as nobody.
:return: None. :return: None.
""" """
client, csrf_token = get_client(self.app, "nobody") client: httpx.Client = get_client(self.__app, "nobody")
ReportTestData(self.app, "editor").populate() ReportTestData(self.__app, "editor").populate()
response: httpx.Response response: httpx.Response
response = client.get(PREFIX) response = client.get(PREFIX)
@ -146,8 +150,8 @@ class ReportTestCase(unittest.TestCase):
:return: None. :return: None.
""" """
client, csrf_token = get_client(self.app, "viewer") client: httpx.Client = get_client(self.__app, "viewer")
ReportTestData(self.app, "editor").populate() ReportTestData(self.__app, "editor").populate()
response: httpx.Response response: httpx.Response
response = client.get(PREFIX) response = client.get(PREFIX)
@ -248,101 +252,101 @@ class ReportTestCase(unittest.TestCase):
:return: None. :return: None.
""" """
ReportTestData(self.app, "editor").populate() ReportTestData(self.__app, "editor").populate()
response: httpx.Response response: httpx.Response
response = self.client.get(PREFIX) response = self.__client.get(PREFIX)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
response = self.client.get(f"{PREFIX}?as=csv") response = self.__client.get(f"{PREFIX}?as=csv")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers["Content-Type"], CSV_MIME) self.assertEqual(response.headers["Content-Type"], CSV_MIME)
response = self.client.get(f"{PREFIX}/journal") response = self.__client.get(f"{PREFIX}/journal")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
response = self.client.get(f"{PREFIX}/journal?as=csv") response = self.__client.get(f"{PREFIX}/journal?as=csv")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers["Content-Type"], CSV_MIME) self.assertEqual(response.headers["Content-Type"], CSV_MIME)
response = self.client.get(f"{PREFIX}/ledger") response = self.__client.get(f"{PREFIX}/ledger")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
response = self.client.get(f"{PREFIX}/ledger?as=csv") response = self.__client.get(f"{PREFIX}/ledger?as=csv")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers["Content-Type"], CSV_MIME) self.assertEqual(response.headers["Content-Type"], CSV_MIME)
response = self.client.get(f"{PREFIX}/income-expenses") response = self.__client.get(f"{PREFIX}/income-expenses")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
response = self.client.get(f"{PREFIX}/income-expenses?as=csv") response = self.__client.get(f"{PREFIX}/income-expenses?as=csv")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers["Content-Type"], CSV_MIME) self.assertEqual(response.headers["Content-Type"], CSV_MIME)
response = self.client.get(f"{PREFIX}/trial-balance") response = self.__client.get(f"{PREFIX}/trial-balance")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
response = self.client.get(f"{PREFIX}/trial-balance?as=csv") response = self.__client.get(f"{PREFIX}/trial-balance?as=csv")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers["Content-Type"], CSV_MIME) self.assertEqual(response.headers["Content-Type"], CSV_MIME)
response = self.client.get(f"{PREFIX}/income-statement") response = self.__client.get(f"{PREFIX}/income-statement")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
response = self.client.get(f"{PREFIX}/income-statement?as=csv") response = self.__client.get(f"{PREFIX}/income-statement?as=csv")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers["Content-Type"], CSV_MIME) self.assertEqual(response.headers["Content-Type"], CSV_MIME)
response = self.client.get(f"{PREFIX}/balance-sheet") response = self.__client.get(f"{PREFIX}/balance-sheet")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
response = self.client.get(f"{PREFIX}/balance-sheet?as=csv") response = self.__client.get(f"{PREFIX}/balance-sheet?as=csv")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers["Content-Type"], CSV_MIME) self.assertEqual(response.headers["Content-Type"], CSV_MIME)
response = self.client.get(f"{PREFIX}/unapplied") response = self.__client.get(f"{PREFIX}/unapplied")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
response = self.client.get(f"{PREFIX}/unapplied?as=csv") response = self.__client.get(f"{PREFIX}/unapplied?as=csv")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers["Content-Type"], CSV_MIME) self.assertEqual(response.headers["Content-Type"], CSV_MIME)
response = self.client.get( response = self.__client.get(
f"{PREFIX}/unapplied/USD/{Accounts.PAYABLE}") f"{PREFIX}/unapplied/USD/{Accounts.PAYABLE}")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
response = self.client.get( response = self.__client.get(
f"{PREFIX}/unapplied/USD/{Accounts.PAYABLE}?as=csv") f"{PREFIX}/unapplied/USD/{Accounts.PAYABLE}?as=csv")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers["Content-Type"], CSV_MIME) self.assertEqual(response.headers["Content-Type"], CSV_MIME)
response = self.client.get(f"{PREFIX}/unmatched") response = self.__client.get(f"{PREFIX}/unmatched")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
response = self.client.get(f"{PREFIX}/unmatched?as=csv") response = self.__client.get(f"{PREFIX}/unmatched?as=csv")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers["Content-Type"], CSV_MIME) self.assertEqual(response.headers["Content-Type"], CSV_MIME)
response = self.client.get( response = self.__client.get(
f"{PREFIX}/unmatched/USD/{Accounts.PAYABLE}") f"{PREFIX}/unmatched/USD/{Accounts.PAYABLE}")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
response = self.client.get( response = self.__client.get(
f"{PREFIX}/unmatched/USD/{Accounts.PAYABLE}?as=csv") f"{PREFIX}/unmatched/USD/{Accounts.PAYABLE}?as=csv")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers["Content-Type"], CSV_MIME) self.assertEqual(response.headers["Content-Type"], CSV_MIME)
response = self.client.get(f"{PREFIX}/search?q=Salary") response = self.__client.get(f"{PREFIX}/search?q=Salary")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
response = self.client.get(f"{PREFIX}/search?q=Salary&as=csv") response = self.__client.get(f"{PREFIX}/search?q=Salary&as=csv")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers["Content-Type"], CSV_MIME) self.assertEqual(response.headers["Content-Type"], CSV_MIME)
response = self.client.get(f"{PREFIX}/search?q=薪水") response = self.__client.get(f"{PREFIX}/search?q=薪水")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
response = self.client.get(f"{PREFIX}/search?q=薪水&as=csv") response = self.__client.get(f"{PREFIX}/search?q=薪水&as=csv")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers["Content-Type"], CSV_MIME) self.assertEqual(response.headers["Content-Type"], CSV_MIME)
@ -353,91 +357,91 @@ class ReportTestCase(unittest.TestCase):
""" """
response: httpx.Response response: httpx.Response
response = self.client.get(PREFIX) response = self.__client.get(PREFIX)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
response = self.client.get(f"{PREFIX}?as=csv") response = self.__client.get(f"{PREFIX}?as=csv")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers["Content-Type"], CSV_MIME) self.assertEqual(response.headers["Content-Type"], CSV_MIME)
response = self.client.get(f"{PREFIX}/journal") response = self.__client.get(f"{PREFIX}/journal")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
response = self.client.get(f"{PREFIX}/journal?as=csv") response = self.__client.get(f"{PREFIX}/journal?as=csv")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers["Content-Type"], CSV_MIME) self.assertEqual(response.headers["Content-Type"], CSV_MIME)
response = self.client.get(f"{PREFIX}/ledger") response = self.__client.get(f"{PREFIX}/ledger")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
response = self.client.get(f"{PREFIX}/ledger?as=csv") response = self.__client.get(f"{PREFIX}/ledger?as=csv")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers["Content-Type"], CSV_MIME) self.assertEqual(response.headers["Content-Type"], CSV_MIME)
response = self.client.get(f"{PREFIX}/income-expenses") response = self.__client.get(f"{PREFIX}/income-expenses")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
response = self.client.get(f"{PREFIX}/income-expenses?as=csv") response = self.__client.get(f"{PREFIX}/income-expenses?as=csv")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers["Content-Type"], CSV_MIME) self.assertEqual(response.headers["Content-Type"], CSV_MIME)
response = self.client.get(f"{PREFIX}/trial-balance") response = self.__client.get(f"{PREFIX}/trial-balance")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
response = self.client.get(f"{PREFIX}/trial-balance?as=csv") response = self.__client.get(f"{PREFIX}/trial-balance?as=csv")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers["Content-Type"], CSV_MIME) self.assertEqual(response.headers["Content-Type"], CSV_MIME)
response = self.client.get(f"{PREFIX}/income-statement") response = self.__client.get(f"{PREFIX}/income-statement")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
response = self.client.get(f"{PREFIX}/income-statement?as=csv") response = self.__client.get(f"{PREFIX}/income-statement?as=csv")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers["Content-Type"], CSV_MIME) self.assertEqual(response.headers["Content-Type"], CSV_MIME)
response = self.client.get(f"{PREFIX}/balance-sheet") response = self.__client.get(f"{PREFIX}/balance-sheet")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
response = self.client.get(f"{PREFIX}/balance-sheet?as=csv") response = self.__client.get(f"{PREFIX}/balance-sheet?as=csv")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers["Content-Type"], CSV_MIME) self.assertEqual(response.headers["Content-Type"], CSV_MIME)
response = self.client.get(f"{PREFIX}/unapplied") response = self.__client.get(f"{PREFIX}/unapplied")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
response = self.client.get(f"{PREFIX}/unapplied?as=csv") response = self.__client.get(f"{PREFIX}/unapplied?as=csv")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers["Content-Type"], CSV_MIME) self.assertEqual(response.headers["Content-Type"], CSV_MIME)
response = self.client.get( response = self.__client.get(
f"{PREFIX}/unapplied/USD/{Accounts.PAYABLE}") f"{PREFIX}/unapplied/USD/{Accounts.PAYABLE}")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
response = self.client.get( response = self.__client.get(
f"{PREFIX}/unapplied/USD/{Accounts.PAYABLE}?as=csv") f"{PREFIX}/unapplied/USD/{Accounts.PAYABLE}?as=csv")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers["Content-Type"], CSV_MIME) self.assertEqual(response.headers["Content-Type"], CSV_MIME)
response = self.client.get(f"{PREFIX}/unmatched") response = self.__client.get(f"{PREFIX}/unmatched")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
response = self.client.get(f"{PREFIX}/unmatched?as=csv") response = self.__client.get(f"{PREFIX}/unmatched?as=csv")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers["Content-Type"], CSV_MIME) self.assertEqual(response.headers["Content-Type"], CSV_MIME)
response = self.client.get( response = self.__client.get(
f"{PREFIX}/unmatched/USD/{Accounts.PAYABLE}") f"{PREFIX}/unmatched/USD/{Accounts.PAYABLE}")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
response = self.client.get( response = self.__client.get(
f"{PREFIX}/unmatched/USD/{Accounts.PAYABLE}?as=csv") f"{PREFIX}/unmatched/USD/{Accounts.PAYABLE}?as=csv")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers["Content-Type"], CSV_MIME) self.assertEqual(response.headers["Content-Type"], CSV_MIME)
response = self.client.get(f"{PREFIX}/search?q=Salary") response = self.__client.get(f"{PREFIX}/search?q=Salary")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
response = self.client.get(f"{PREFIX}/search?q=Salary&as=csv") response = self.__client.get(f"{PREFIX}/search?q=Salary&as=csv")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers["Content-Type"], CSV_MIME) self.assertEqual(response.headers["Content-Type"], CSV_MIME)

View File

@ -23,15 +23,13 @@ from typing import Type
from click.testing import Result from click.testing import Result
from flask import Flask, Blueprint, render_template, redirect, Response, \ from flask import Flask, Blueprint, render_template, redirect, Response, \
url_for, request url_for
from flask.testing import FlaskCliRunner from flask.testing import FlaskCliRunner
from flask_babel_js import BabelJS from flask_babel_js import BabelJS
from flask_sqlalchemy import SQLAlchemy from flask_sqlalchemy import SQLAlchemy
from flask_wtf import CSRFProtect from flask_wtf import CSRFProtect
from sqlalchemy import Column from sqlalchemy import Column
from accounting.utils.next_uri import encode_next
bp: Blueprint = Blueprint("home", __name__) bp: Blueprint = Blueprint("home", __name__)
"""The global blueprint.""" """The global blueprint."""
babel_js: BabelJS = BabelJS() babel_js: BabelJS = BabelJS()

View File

@ -57,13 +57,20 @@ class JournalEntryLineItemData:
:param original_line_item: The original journal entry line item. :param original_line_item: The original journal entry line item.
""" """
self.journal_entry: JournalEntryData | None = None self.journal_entry: JournalEntryData | None = None
"""The journal entry data."""
self.id: int = -1 self.id: int = -1
"""The journal entry line item ID."""
self.no: int = -1 self.no: int = -1
"""The line item number under the journal entry and debit or credit."""
self.original_line_item: JournalEntryLineItemData | None \ self.original_line_item: JournalEntryLineItemData | None \
= original_line_item = original_line_item
"""The original journal entry line item."""
self.account: str = account self.account: str = account
"""The account code."""
self.description: str | None = description self.description: str | None = description
"""The description."""
self.amount: Decimal = Decimal(amount) self.amount: Decimal = Decimal(amount)
"""The amount."""
def form(self, prefix: str, debit_credit: str, index: int, def form(self, prefix: str, debit_credit: str, index: int,
is_update: bool) -> dict[str, str]: is_update: bool) -> dict[str, str]:
@ -101,8 +108,11 @@ class JournalEntryCurrencyData:
:param credit: The credit line items. :param credit: The credit line items.
""" """
self.code: str = currency self.code: str = currency
"""The currency code."""
self.debit: list[JournalEntryLineItemData] = debit self.debit: list[JournalEntryLineItemData] = debit
"""The debit line items."""
self.credit: list[JournalEntryLineItemData] = credit self.credit: list[JournalEntryLineItemData] = credit
"""The credit line items."""
def form(self, index: int, is_update: bool) -> dict[str, str]: def form(self, index: int, is_update: bool) -> dict[str, str]:
"""Returns the currency as form data. """Returns the currency as form data.
@ -131,9 +141,13 @@ class JournalEntryData:
:param currencies: The journal entry currency data. :param currencies: The journal entry currency data.
""" """
self.id: int = -1 self.id: int = -1
"""The journal entry ID."""
self.days: int = days self.days: int = days
"""The number of days before today."""
self.currencies: list[JournalEntryCurrencyData] = currencies self.currencies: list[JournalEntryCurrencyData] = currencies
"""The journal entry currency data."""
self.note: str | None = None self.note: str | None = None
"""The note."""
for currency in self.currencies: for currency in self.currencies:
for line_item in currency.debit: for line_item in currency.debit:
line_item.journal_entry = self line_item.journal_entry = self
@ -190,13 +204,17 @@ class BaseTestData(ABC):
:param username: The username. :param username: The username.
""" """
self._app: Flask = app self._app: Flask = app
"""The Flask application."""
with self._app.app_context(): with self._app.app_context():
current_user: User | None = User.query\ current_user: User | None = User.query\
.filter(User.username == username).first() .filter(User.username == username).first()
assert current_user is not None assert current_user is not None
self.__current_user_id: int = current_user.id self.__current_user_id: int = current_user.id
"""The current user ID."""
self.__journal_entries: list[dict[str, Any]] = [] self.__journal_entries: list[dict[str, Any]] = []
"""The data of the journal entries."""
self.__line_items: list[dict[str, Any]] = [] self.__line_items: list[dict[str, Any]] = []
"""The data of the journal entry line items."""
self._init_data() self._init_data()
@abstractmethod @abstractmethod

View File

@ -26,6 +26,7 @@ from werkzeug.datastructures import LanguageAccept
from accounting.utils.next_uri import or_next from accounting.utils.next_uri import or_next
bp: Blueprint = Blueprint("locale", __name__, url_prefix="/") bp: Blueprint = Blueprint("locale", __name__, url_prefix="/")
"""The blueprint for the localization."""
def get_locale(): def get_locale():

View File

@ -29,6 +29,7 @@ from .lib import Accounts, JournalEntryLineItemData, JournalEntryData, \
JournalEntryCurrencyData, BaseTestData JournalEntryCurrencyData, BaseTestData
bp: Blueprint = Blueprint("reset", __name__, url_prefix="/") bp: Blueprint = Blueprint("reset", __name__, url_prefix="/")
"""The blueprint for the data reset."""
@bp.get("reset", endpoint="reset-page") @bp.get("reset", endpoint="reset-page")

View File

@ -25,7 +25,7 @@ First written: 2023/1/27
{% block content %} {% block content %}
<p>{{ _("This is the live demonstration of the Mia! Accounting project. Please <a href=\"/login?next=%%2Faccounting\">log in</a> to continue.") }}</p> <p>{{ _("This is the live demonstration of the Mia! Accounting project. Please <a href=\"%(url)s\">log in</a> to continue.", url=url_for("auth.login")) }}</p>
<p>{{ _("You may also want to check the <a href=\"https://mia-accounting.readthedocs.io\">full documentation</a> and the <a href=\"https://github.com/imacat/mia-accounting\">Github repository</a>.") }}</p> <p>{{ _("You may also want to check the <a href=\"https://mia-accounting.readthedocs.io\">full documentation</a> and the <a href=\"https://github.com/imacat/mia-accounting\">Github repository</a>.") }}</p>

View File

@ -9,8 +9,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: mia-accounting-test-site 1.0.0\n" "Project-Id-Version: mia-accounting-test-site 1.0.0\n"
"Report-Msgid-Bugs-To: imacat@mail.imacat.idv.tw\n" "Report-Msgid-Bugs-To: imacat@mail.imacat.idv.tw\n"
"POT-Creation-Date: 2023-04-12 17:59+0800\n" "POT-Creation-Date: 2023-06-10 10:42+0800\n"
"PO-Revision-Date: 2023-04-12 18:00+0800\n" "PO-Revision-Date: 2023-06-10 10:43+0800\n"
"Last-Translator: imacat <imacat@mail.imacat.idv.tw>\n" "Last-Translator: imacat <imacat@mail.imacat.idv.tw>\n"
"Language: zh_Hant\n" "Language: zh_Hant\n"
"Language-Team: zh_Hant <imacat@mail.imacat.idv.tw>\n" "Language-Team: zh_Hant <imacat@mail.imacat.idv.tw>\n"
@ -24,7 +24,7 @@ msgstr ""
msgid "The sample data are emptied and reset successfully." msgid "The sample data are emptied and reset successfully."
msgstr "範例資料已清空重設。" msgstr "範例資料已清空重設。"
#: tests/test_site/reset.py:68 #: tests/test_site/reset.py:69
msgid "The database is emptied successfully." msgid "The database is emptied successfully."
msgstr "資料庫已清空。" msgstr "資料庫已清空。"
@ -61,8 +61,8 @@ msgstr "Mia! Accounting 示範站"
#, python-format #, python-format
msgid "" msgid ""
"This is the live demonstration of the Mia! Accounting project. Please <a" "This is the live demonstration of the Mia! Accounting project. Please <a"
" href=\"/login?next=%%2Faccounting\">log in</a> to continue." " href=\"%(url)s\">log in</a> to continue."
msgstr "這是 Mia! Accounting 專案的示範站。請先<a href=\"/login?next=%%2Faccounting\">登入</a>。" msgstr "這是 Mia! Accounting 專案的示範站。請先<a href=\"%(url)s\">登入</a>。"
#: tests/test_site/templates/home.html:30 #: tests/test_site/templates/home.html:30
msgid "" msgid ""

View File

@ -26,7 +26,8 @@ from accounting.utils.next_uri import encode_next
from test_site import db from test_site import db
from test_site.lib import JournalEntryCurrencyData, JournalEntryData, \ from test_site.lib import JournalEntryCurrencyData, JournalEntryData, \
BaseTestData BaseTestData
from testlib import NEXT_URI, create_test_app, get_client, Accounts from testlib import NEXT_URI, create_test_app, get_client, get_csrf_token, \
Accounts
PREFIX: str = "/accounting/match-offsets/USD" PREFIX: str = "/accounting/match-offsets/USD"
"""The URL prefix for the unmatched offset management.""" """The URL prefix for the unmatched offset management."""
@ -41,28 +42,34 @@ class UnmatchedOffsetTestCase(unittest.TestCase):
:return: None. :return: None.
""" """
self.app: Flask = create_test_app() self.__app: Flask = create_test_app()
"""The Flask application."""
with self.app.app_context(): with self.__app.app_context():
from accounting.models import JournalEntry, JournalEntryLineItem from accounting.models import JournalEntry, JournalEntryLineItem
JournalEntry.query.delete() JournalEntry.query.delete()
JournalEntryLineItem.query.delete() JournalEntryLineItem.query.delete()
self.encoded_next_uri: str = encode_next(NEXT_URI) self.__encoded_next_uri: str = encode_next(NEXT_URI)
"""The encoded next URI."""
self.client, self.csrf_token = get_client(self.app, "editor") self.__client: httpx.Client = get_client(self.__app, "editor")
"""The user client."""
self.__csrf_token: str = get_csrf_token(self.__client)
"""The CSRF token."""
def test_nobody(self) -> None: def test_nobody(self) -> None:
"""Test the permission as nobody. """Test the permission as nobody.
:return: None. :return: None.
""" """
client, csrf_token = get_client(self.app, "nobody") client: httpx.Client = get_client(self.__app, "nobody")
DifferentTestData(self.app, "nobody").populate() csrf_token: str = get_csrf_token(client)
DifferentTestData(self.__app, "nobody").populate()
response: httpx.Response response: httpx.Response
response = client.post(f"{PREFIX}/{Accounts.PAYABLE}", response = client.post(f"{PREFIX}/{Accounts.PAYABLE}",
data={"csrf_token": csrf_token, data={"csrf_token": csrf_token,
"next": self.encoded_next_uri}) "next": self.__encoded_next_uri})
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
def test_viewer(self) -> None: def test_viewer(self) -> None:
@ -70,13 +77,14 @@ class UnmatchedOffsetTestCase(unittest.TestCase):
:return: None. :return: None.
""" """
client, csrf_token = get_client(self.app, "viewer") client: httpx.Client = get_client(self.__app, "viewer")
DifferentTestData(self.app, "viewer").populate() csrf_token: str = get_csrf_token(client)
DifferentTestData(self.__app, "viewer").populate()
response: httpx.Response response: httpx.Response
response = client.post(f"{PREFIX}/{Accounts.PAYABLE}", response = client.post(f"{PREFIX}/{Accounts.PAYABLE}",
data={"csrf_token": csrf_token, data={"csrf_token": csrf_token,
"next": self.encoded_next_uri}) "next": self.__encoded_next_uri})
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
def test_editor(self) -> None: def test_editor(self) -> None:
@ -84,12 +92,12 @@ class UnmatchedOffsetTestCase(unittest.TestCase):
:return: None. :return: None.
""" """
DifferentTestData(self.app, "editor").populate() DifferentTestData(self.__app, "editor").populate()
response: httpx.Response response: httpx.Response
response = self.client.post(f"{PREFIX}/{Accounts.PAYABLE}", response = self.__client.post(f"{PREFIX}/{Accounts.PAYABLE}",
data={"csrf_token": self.csrf_token, data={"csrf_token": self.__csrf_token,
"next": self.encoded_next_uri}) "next": self.__encoded_next_uri})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], NEXT_URI) self.assertEqual(response.headers["Location"], NEXT_URI)
@ -100,9 +108,9 @@ class UnmatchedOffsetTestCase(unittest.TestCase):
""" """
response: httpx.Response response: httpx.Response
response = self.client.post(f"{PREFIX}/{Accounts.PAYABLE}", response = self.__client.post(f"{PREFIX}/{Accounts.PAYABLE}",
data={"csrf_token": self.csrf_token, data={"csrf_token": self.__csrf_token,
"next": self.encoded_next_uri}) "next": self.__encoded_next_uri})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], NEXT_URI) self.assertEqual(response.headers["Location"], NEXT_URI)
@ -114,7 +122,7 @@ class UnmatchedOffsetTestCase(unittest.TestCase):
from accounting.models import Currency, Account, JournalEntryLineItem from accounting.models import Currency, Account, JournalEntryLineItem
from accounting.report.utils.offset_matcher import OffsetMatcher from accounting.report.utils.offset_matcher import OffsetMatcher
from accounting.template_globals import default_currency_code from accounting.template_globals import default_currency_code
data: DifferentTestData = DifferentTestData(self.app, "editor") data: DifferentTestData = DifferentTestData(self.__app, "editor")
data.populate() data.populate()
account: Account | None account: Account | None
line_item: JournalEntryLineItem | None line_item: JournalEntryLineItem | None
@ -122,13 +130,13 @@ class UnmatchedOffsetTestCase(unittest.TestCase):
match_uri: str match_uri: str
response: httpx.Response response: httpx.Response
with self.app.app_context(): with self.__app.app_context():
currency: Currency | None \ currency: Currency | None \
= db.session.get(Currency, default_currency_code()) = db.session.get(Currency, default_currency_code())
assert currency is not None assert currency is not None
# The receivables # The receivables
with self.app.app_context(): with self.__app.app_context():
account = Account.find_by_code(Accounts.RECEIVABLE) account = Account.find_by_code(Accounts.RECEIVABLE)
assert account is not None assert account is not None
matcher = OffsetMatcher(currency, account) matcher = OffsetMatcher(currency, account)
@ -150,13 +158,13 @@ class UnmatchedOffsetTestCase(unittest.TestCase):
self.assertIsNone(line_item.original_line_item_id) self.assertIsNone(line_item.original_line_item_id)
match_uri = f"{PREFIX}/{Accounts.RECEIVABLE}" match_uri = f"{PREFIX}/{Accounts.RECEIVABLE}"
response = self.client.post(match_uri, response = self.__client.post(match_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.__csrf_token,
"next": self.encoded_next_uri}) "next": self.__encoded_next_uri})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], NEXT_URI) self.assertEqual(response.headers["Location"], NEXT_URI)
with self.app.app_context(): with self.__app.app_context():
account = Account.find_by_code(Accounts.RECEIVABLE) account = Account.find_by_code(Accounts.RECEIVABLE)
assert account is not None assert account is not None
matcher = OffsetMatcher(currency, account) matcher = OffsetMatcher(currency, account)
@ -178,7 +186,7 @@ class UnmatchedOffsetTestCase(unittest.TestCase):
self.assertEqual(line_item.original_line_item_id, data.l_r_or4d.id) self.assertEqual(line_item.original_line_item_id, data.l_r_or4d.id)
# The payables # The payables
with self.app.app_context(): with self.__app.app_context():
account = Account.find_by_code(Accounts.PAYABLE) account = Account.find_by_code(Accounts.PAYABLE)
assert account is not None assert account is not None
matcher = OffsetMatcher(currency, account) matcher = OffsetMatcher(currency, account)
@ -200,13 +208,13 @@ class UnmatchedOffsetTestCase(unittest.TestCase):
self.assertIsNone(line_item.original_line_item_id) self.assertIsNone(line_item.original_line_item_id)
match_uri = f"{PREFIX}/{Accounts.PAYABLE}" match_uri = f"{PREFIX}/{Accounts.PAYABLE}"
response = self.client.post(match_uri, response = self.__client.post(match_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.__csrf_token,
"next": self.encoded_next_uri}) "next": self.__encoded_next_uri})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], NEXT_URI) self.assertEqual(response.headers["Location"], NEXT_URI)
with self.app.app_context(): with self.__app.app_context():
account = Account.find_by_code(Accounts.PAYABLE) account = Account.find_by_code(Accounts.PAYABLE)
assert account is not None assert account is not None
matcher = OffsetMatcher(currency, account) matcher = OffsetMatcher(currency, account)
@ -235,7 +243,7 @@ class UnmatchedOffsetTestCase(unittest.TestCase):
from accounting.models import Currency, Account, JournalEntryLineItem from accounting.models import Currency, Account, JournalEntryLineItem
from accounting.report.utils.offset_matcher import OffsetMatcher from accounting.report.utils.offset_matcher import OffsetMatcher
from accounting.template_globals import default_currency_code from accounting.template_globals import default_currency_code
data: SameTestData = SameTestData(self.app, "editor") data: SameTestData = SameTestData(self.__app, "editor")
data.populate() data.populate()
account: Account | None account: Account | None
line_item: JournalEntryLineItem | None line_item: JournalEntryLineItem | None
@ -243,13 +251,13 @@ class UnmatchedOffsetTestCase(unittest.TestCase):
match_uri: str match_uri: str
response: httpx.Response response: httpx.Response
with self.app.app_context(): with self.__app.app_context():
currency: Currency | None \ currency: Currency | None \
= db.session.get(Currency, default_currency_code()) = db.session.get(Currency, default_currency_code())
assert currency is not None assert currency is not None
# The receivables # The receivables
with self.app.app_context(): with self.__app.app_context():
account = Account.find_by_code(Accounts.RECEIVABLE) account = Account.find_by_code(Accounts.RECEIVABLE)
assert account is not None assert account is not None
matcher = OffsetMatcher(currency, account) matcher = OffsetMatcher(currency, account)
@ -278,13 +286,13 @@ class UnmatchedOffsetTestCase(unittest.TestCase):
self.assertEqual(line_item.original_line_item_id, data.l_r_or2d.id) self.assertEqual(line_item.original_line_item_id, data.l_r_or2d.id)
match_uri = f"{PREFIX}/{Accounts.RECEIVABLE}" match_uri = f"{PREFIX}/{Accounts.RECEIVABLE}"
response = self.client.post(match_uri, response = self.__client.post(match_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.__csrf_token,
"next": self.encoded_next_uri}) "next": self.__encoded_next_uri})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], NEXT_URI) self.assertEqual(response.headers["Location"], NEXT_URI)
with self.app.app_context(): with self.__app.app_context():
account = Account.find_by_code(Accounts.RECEIVABLE) account = Account.find_by_code(Accounts.RECEIVABLE)
assert account is not None assert account is not None
matcher = OffsetMatcher(currency, account) matcher = OffsetMatcher(currency, account)
@ -315,7 +323,7 @@ class UnmatchedOffsetTestCase(unittest.TestCase):
self.assertEqual(line_item.original_line_item_id, data.l_r_or4d.id) self.assertEqual(line_item.original_line_item_id, data.l_r_or4d.id)
# The payables # The payables
with self.app.app_context(): with self.__app.app_context():
account = Account.find_by_code(Accounts.PAYABLE) account = Account.find_by_code(Accounts.PAYABLE)
assert account is not None assert account is not None
matcher = OffsetMatcher(currency, account) matcher = OffsetMatcher(currency, account)
@ -344,13 +352,13 @@ class UnmatchedOffsetTestCase(unittest.TestCase):
self.assertEqual(line_item.original_line_item_id, data.l_p_or2c.id) self.assertEqual(line_item.original_line_item_id, data.l_p_or2c.id)
match_uri = f"{PREFIX}/{Accounts.PAYABLE}" match_uri = f"{PREFIX}/{Accounts.PAYABLE}"
response = self.client.post(match_uri, response = self.__client.post(match_uri,
data={"csrf_token": self.csrf_token, data={"csrf_token": self.__csrf_token,
"next": self.encoded_next_uri}) "next": self.__encoded_next_uri})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], NEXT_URI) self.assertEqual(response.headers["Location"], NEXT_URI)
with self.app.app_context(): with self.__app.app_context():
account = Account.find_by_code(Accounts.PAYABLE) account = Account.find_by_code(Accounts.PAYABLE)
assert account is not None assert account is not None
matcher = OffsetMatcher(currency, account) matcher = OffsetMatcher(currency, account)
@ -410,18 +418,22 @@ class DifferentTestData(BaseTestData):
50, [JournalEntryCurrencyData( 50, [JournalEntryCurrencyData(
"USD", [self.l_r_or1d, self.l_r_or4d], "USD", [self.l_r_or1d, self.l_r_or4d],
[self.l_r_or1c, self.l_r_or4c])]) [self.l_r_or1c, self.l_r_or4c])])
"""The receivable original journal entry #1."""
self.j_r_or2: JournalEntryData = JournalEntryData( self.j_r_or2: JournalEntryData = JournalEntryData(
30, [JournalEntryCurrencyData( 30, [JournalEntryCurrencyData(
"USD", [self.l_r_or2d, self.l_r_or3d], "USD", [self.l_r_or2d, self.l_r_or3d],
[self.l_r_or2c, self.l_r_or3c])]) [self.l_r_or2c, self.l_r_or3c])])
"""The receivable original journal entry #2"""
self.j_p_or1: JournalEntryData = JournalEntryData( self.j_p_or1: JournalEntryData = JournalEntryData(
40, [JournalEntryCurrencyData( 40, [JournalEntryCurrencyData(
"USD", [self.l_p_or1d, self.l_p_or4d], "USD", [self.l_p_or1d, self.l_p_or4d],
[self.l_p_or1c, self.l_p_or4c])]) [self.l_p_or1c, self.l_p_or4c])])
"""The payable original journal entry #1."""
self.j_p_or2: JournalEntryData = JournalEntryData( self.j_p_or2: JournalEntryData = JournalEntryData(
20, [JournalEntryCurrencyData( 20, [JournalEntryCurrencyData(
"USD", [self.l_p_or2d, self.l_p_or3d], "USD", [self.l_p_or2d, self.l_p_or3d],
[self.l_p_or2c, self.l_p_or3c])]) [self.l_p_or2c, self.l_p_or3c])])
"""The payable original journal entry #2."""
self._add_journal_entry(self.j_r_or1) self._add_journal_entry(self.j_r_or1)
self._add_journal_entry(self.j_r_or2) self._add_journal_entry(self.j_r_or2)
@ -456,23 +468,29 @@ class DifferentTestData(BaseTestData):
self.j_r_of1: JournalEntryData = JournalEntryData( self.j_r_of1: JournalEntryData = JournalEntryData(
25, [JournalEntryCurrencyData( 25, [JournalEntryCurrencyData(
"USD", [self.l_r_of1d], [self.l_r_of1c])]) "USD", [self.l_r_of1d], [self.l_r_of1c])])
"""The offset journal entry to the receivable #1."""
self.j_r_of2: JournalEntryData = JournalEntryData( self.j_r_of2: JournalEntryData = JournalEntryData(
20, [JournalEntryCurrencyData( 20, [JournalEntryCurrencyData(
"USD", [self.l_r_of2d, self.l_r_of3d, self.l_r_of4d], "USD", [self.l_r_of2d, self.l_r_of3d, self.l_r_of4d],
[self.l_r_of2c, self.l_r_of3c, self.l_r_of4c])]) [self.l_r_of2c, self.l_r_of3c, self.l_r_of4c])])
"""The offset journal entry to the receivable #2."""
self.j_r_of3: JournalEntryData = JournalEntryData( self.j_r_of3: JournalEntryData = JournalEntryData(
15, [JournalEntryCurrencyData( 15, [JournalEntryCurrencyData(
"USD", [self.l_r_of5d], [self.l_r_of5c])]) "USD", [self.l_r_of5d], [self.l_r_of5c])])
"""The offset journal entry to the receivable #3."""
self.j_p_of1: JournalEntryData = JournalEntryData( self.j_p_of1: JournalEntryData = JournalEntryData(
15, [JournalEntryCurrencyData( 15, [JournalEntryCurrencyData(
"USD", [self.l_p_of1d], [self.l_p_of1c])]) "USD", [self.l_p_of1d], [self.l_p_of1c])])
"""The offset journal entry to the payable #1."""
self.j_p_of2: JournalEntryData = JournalEntryData( self.j_p_of2: JournalEntryData = JournalEntryData(
10, [JournalEntryCurrencyData( 10, [JournalEntryCurrencyData(
"USD", [self.l_p_of2d, self.l_p_of3d, self.l_p_of4d], "USD", [self.l_p_of2d, self.l_p_of3d, self.l_p_of4d],
[self.l_p_of2c, self.l_p_of3c, self.l_p_of4c])]) [self.l_p_of2c, self.l_p_of3c, self.l_p_of4c])])
"""The offset journal entry to the payable #2."""
self.j_p_of3: JournalEntryData = JournalEntryData( self.j_p_of3: JournalEntryData = JournalEntryData(
5, [JournalEntryCurrencyData( 5, [JournalEntryCurrencyData(
"USD", [self.l_p_of5d], [self.l_p_of5c])]) "USD", [self.l_p_of5d], [self.l_p_of5c])])
"""The offset journal entry to the payable #3."""
self._add_journal_entry(self.j_r_of1) self._add_journal_entry(self.j_r_of1)
self._add_journal_entry(self.j_r_of2) self._add_journal_entry(self.j_r_of2)

View File

@ -22,9 +22,9 @@ from urllib.parse import quote_plus
import httpx import httpx
from flask import Flask, request from flask import Flask, request
from itsdangerous import URLSafeSerializer
from accounting.utils.next_uri import append_next, inherit_next, or_next 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.pagination import Pagination, DEFAULT_PAGE_SIZE
from accounting.utils.query import parse_query_keywords from accounting.utils.query import parse_query_keywords
from testlib import TEST_SERVER, create_test_app, get_csrf_token, NEXT_URI from testlib import TEST_SERVER, create_test_app, get_csrf_token, NEXT_URI
@ -40,9 +40,8 @@ class NextUriTestCase(unittest.TestCase):
:return: None. :return: None.
""" """
self.app: Flask = create_test_app() self.__app: Flask = create_test_app()
self.serializer: URLSafeSerializer \ """The Flask application."""
= URLSafeSerializer(self.app.config["SECRET_KEY"])
def test_next_uri(self) -> None: def test_next_uri(self) -> None:
"""Tests the next URI utilities with the next URI. """Tests the next URI utilities with the next URI.
@ -53,23 +52,29 @@ class NextUriTestCase(unittest.TestCase):
"""The test view with the next URI.""" """The test view with the next URI."""
current_uri: str = request.full_path if request.query_string \ current_uri: str = request.full_path if request.query_string \
else request.path else request.path
with self.__app.app_context():
encoded_current: str = encode_next(current_uri)
self.assertEqual(append_next(self.TARGET), self.assertEqual(append_next(self.TARGET),
f"{self.TARGET}?next={self.__encode(current_uri)}") f"{self.TARGET}?next={encoded_current}")
next_uri: str = request.form["next"] if request.method == "POST" \ encoded_next_uri: str = request.form["next"] \
else request.args["next"] if request.method == "POST" else request.args["next"]
self.assertEqual(inherit_next(self.TARGET), self.assertEqual(inherit_next(self.TARGET),
f"{self.TARGET}?next={next_uri}") f"{self.TARGET}?next={encoded_next_uri}")
self.assertEqual(or_next(self.TARGET), self.__decode(next_uri)) with self.__app.app_context():
next_uri: str = decode_next(encoded_next_uri)
self.assertEqual(or_next(self.TARGET), next_uri)
return "" return ""
self.app.add_url_rule("/test-next", view_func=test_next_uri_view, self.__app.add_url_rule("/test-next", view_func=test_next_uri_view,
methods=["GET", "POST"]) methods=["GET", "POST"])
client: httpx.Client = httpx.Client(app=self.app, base_url=TEST_SERVER) client: httpx.Client = httpx.Client(app=self.__app,
base_url=TEST_SERVER)
client.headers["Referer"] = TEST_SERVER client.headers["Referer"] = TEST_SERVER
csrf_token: str = get_csrf_token(client) csrf_token: str = get_csrf_token(client)
response: httpx.Response response: httpx.Response
encoded_uri: str = self.__encode(NEXT_URI) 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") response = client.get(f"/test-next?next={encoded_uri}&q=abc&page-no=4")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
response = client.post("/test-next", data={"csrf_token": csrf_token, response = client.post("/test-next", data={"csrf_token": csrf_token,
@ -88,9 +93,11 @@ class NextUriTestCase(unittest.TestCase):
self.assertEqual(or_next(self.TARGET), self.TARGET) self.assertEqual(or_next(self.TARGET), self.TARGET)
return "" return ""
self.app.add_url_rule("/test-no-next", view_func=test_no_next_uri_view, self.__app.add_url_rule("/test-no-next",
view_func=test_no_next_uri_view,
methods=["GET", "POST"]) methods=["GET", "POST"])
client: httpx.Client = httpx.Client(app=self.app, base_url=TEST_SERVER) client: httpx.Client = httpx.Client(app=self.__app,
base_url=TEST_SERVER)
client.headers["Referer"] = TEST_SERVER client.headers["Referer"] = TEST_SERVER
csrf_token: str = get_csrf_token(client) csrf_token: str = get_csrf_token(client)
response: httpx.Response response: httpx.Response
@ -112,10 +119,11 @@ class NextUriTestCase(unittest.TestCase):
self.assertEqual(or_next(self.TARGET), self.TARGET) self.assertEqual(or_next(self.TARGET), self.TARGET)
return "" return ""
self.app.add_url_rule("/test-invalid-next", self.__app.add_url_rule("/test-invalid-next",
view_func=test_invalid_next_uri_view, view_func=test_invalid_next_uri_view,
methods=["GET", "POST"]) methods=["GET", "POST"])
client: httpx.Client = httpx.Client(app=self.app, base_url=TEST_SERVER) client: httpx.Client = httpx.Client(app=self.__app,
base_url=TEST_SERVER)
client.headers["Referer"] = TEST_SERVER client.headers["Referer"] = TEST_SERVER
csrf_token: str = get_csrf_token(client) csrf_token: str = get_csrf_token(client)
next_uri: str next_uri: str
@ -132,22 +140,6 @@ class NextUriTestCase(unittest.TestCase):
"next": next_uri}) "next": next_uri})
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
def __encode(self, uri: str) -> str:
"""Encodes the next URI.
:param uri: The next URI.
:return: The encoded next URI.
"""
return self.serializer.dumps(uri, "next")
def __decode(self, uri: str) -> str:
"""Decodes the next URI.
:param uri: The encoded next URI.
:return: The next URI.
"""
return self.serializer.loads(uri, "next")
class QueryKeywordParserTestCase(unittest.TestCase): class QueryKeywordParserTestCase(unittest.TestCase):
"""The test case for the query keyword parser.""" """The test case for the query keyword parser."""
@ -190,7 +182,7 @@ class PaginationTestCase(unittest.TestCase):
"""The test case for pagination.""" """The test case for pagination."""
class Params: class Params:
"""The testing parameters.""" """The testing pagination parameters."""
def __init__(self, items: list[int], is_reversed: bool | None, def __init__(self, items: list[int], is_reversed: bool | None,
result: list[int], is_paged: bool): result: list[int], is_paged: bool):
@ -202,9 +194,13 @@ class PaginationTestCase(unittest.TestCase):
:param is_paged: Whether we need pagination. :param is_paged: Whether we need pagination.
""" """
self.items: list[int] = items self.items: list[int] = items
"""All the items in the list."""
self.is_reversed: bool | None = is_reversed self.is_reversed: bool | None = is_reversed
"""Whether the default page is the last page."""
self.result: list[int] = result self.result: list[int] = result
"""The expected items on the page."""
self.is_paged: bool = is_paged self.is_paged: bool = is_paged
"""Whether we need pagination."""
def setUp(self) -> None: def setUp(self) -> None:
"""Sets up the test. """Sets up the test.
@ -212,24 +208,29 @@ class PaginationTestCase(unittest.TestCase):
:return: None. :return: None.
""" """
self.app: Flask = create_test_app() self.__app: Flask = create_test_app()
self.params = self.Params([], None, [], True) """The Flask application."""
self.__params: PaginationTestCase.Params \
= self.Params([], None, [], True)
"""The testing pagination parameters."""
@self.app.get("/test-pagination") @self.__app.get("/test-pagination")
def test_pagination_view() -> str: def test_pagination_view() -> str:
"""The test view with the pagination.""" """The test view with the pagination."""
pagination: Pagination pagination: Pagination
if self.params.is_reversed is not None: if self.__params.is_reversed is not None:
pagination = Pagination[int]( pagination = Pagination[int](
self.params.items, is_reversed=self.params.is_reversed) self.__params.items, is_reversed=self.__params.is_reversed)
else: else:
pagination = Pagination[int](self.params.items) pagination = Pagination[int](self.__params.items)
self.assertEqual(pagination.is_paged, self.params.is_paged) self.assertEqual(pagination.is_paged, self.__params.is_paged)
self.assertEqual(pagination.list, self.params.result) self.assertEqual(pagination.list, self.__params.result)
return "" return ""
self.client = httpx.Client(app=self.app, base_url=TEST_SERVER) self.__client: httpx.Client = httpx.Client(app=self.__app,
self.client.headers["Referer"] = TEST_SERVER base_url=TEST_SERVER)
"""The user client."""
self.__client.headers["Referer"] = TEST_SERVER
def __test_success(self, query: str, items: range, def __test_success(self, query: str, items: range,
result: range, is_paged: bool = True, result: range, is_paged: bool = True,
@ -246,9 +247,9 @@ class PaginationTestCase(unittest.TestCase):
target: str = "/test-pagination" target: str = "/test-pagination"
if query != "": if query != "":
target = f"{target}?{query}" target = f"{target}?{query}"
self.params = self.Params(list(items), is_reversed, self.__params = self.Params(list(items), is_reversed,
list(result), is_paged) list(result), is_paged)
response: httpx.Response = self.client.get(target) response: httpx.Response = self.__client.get(target)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
def __test_malformed(self, query: str, items: range, redirect_to: str, def __test_malformed(self, query: str, items: range, redirect_to: str,
@ -262,8 +263,8 @@ class PaginationTestCase(unittest.TestCase):
:return: None. :return: None.
""" """
target: str = "/test-pagination" target: str = "/test-pagination"
self.params = self.Params(list(items), is_reversed, [], True) self.__params = self.Params(list(items), is_reversed, [], True)
response: httpx.Response = self.client.get(f"{target}?{query}") response: httpx.Response = self.__client.get(f"{target}?{query}")
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], self.assertEqual(response.headers["Location"],
f"{target}?{redirect_to}") f"{target}?{redirect_to}")

View File

@ -89,12 +89,12 @@ def get_csrf_token(client: httpx.Client) -> str:
return client.get("/.csrf-token").text return client.get("/.csrf-token").text
def get_client(app: Flask, username: str) -> tuple[httpx.Client, str]: def get_client(app: Flask, username: str) -> httpx.Client:
"""Returns a user client. """Returns a user client.
:param app: The Flask application. :param app: The Flask application.
:param username: The username. :param username: The username.
:return: A tuple of the client and the CSRF token. :return: The user client.
""" """
client: httpx.Client = httpx.Client(app=app, base_url=TEST_SERVER) client: httpx.Client = httpx.Client(app=app, base_url=TEST_SERVER)
client.headers["Referer"] = TEST_SERVER client.headers["Referer"] = TEST_SERVER
@ -107,7 +107,7 @@ def get_client(app: Flask, username: str) -> tuple[httpx.Client, str]:
"username": username}) "username": username})
assert response.status_code == 302 assert response.status_code == 302
assert response.headers["Location"] == NEXT_URI assert response.headers["Location"] == NEXT_URI
return client, csrf_token return client
def set_locale(app: Flask, client: httpx.Client, csrf_token: str, def set_locale(app: Flask, client: httpx.Client, csrf_token: str,

View File

@ -25,7 +25,7 @@ from secrets import randbelow
from flask import Flask from flask import Flask
from test_site import db from test_site import db
from testlib import NEXT_URI, Accounts from testlib import Accounts
NON_EMPTY_NOTE: str = " This is \n\na test." NON_EMPTY_NOTE: str = " This is \n\na test."
"""The stripped content of an non-empty note.""" """The stripped content of an non-empty note."""