mia-accounting/tests/test_option.py

417 lines
16 KiB
Python

# The Mia! Accounting Project.
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/3/22
# Copyright (c) 2023 imacat.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""The test for the options.
"""
import unittest
from datetime import datetime, timedelta
import httpx
from click.testing import Result
from flask import Flask
from flask.testing import FlaskCliRunner
from test_site import db
from testlib import NEXT_URI, Accounts, create_test_app, get_client
from testlib_offset import TestData
PREFIX: str = "/accounting/options"
"""The URL prefix for the option management."""
DETAIL_URI: str = f"{PREFIX}?next=%2F_next"
"""THE URI for the option detail."""
EDIT_URI: str = f"{PREFIX}/edit?next=%2F_next"
"""THE URI for the form to edit the options."""
UPDATE_URI: str = f"{PREFIX}/update"
"""THE URI to update the options."""
class OptionTestCase(unittest.TestCase):
"""The option test case."""
def setUp(self) -> None:
"""Sets up the test.
This is run once per test.
:return: None.
"""
self.app: Flask = create_test_app()
runner: FlaskCliRunner = self.app.test_cli_runner()
with self.app.app_context():
from accounting.models import BaseAccount, Option
result: Result
result = runner.invoke(args="init-db")
self.assertEqual(result.exit_code, 0)
if BaseAccount.query.first() is None:
result = runner.invoke(args="accounting-init-base")
self.assertEqual(result.exit_code, 0)
result = runner.invoke(args=["accounting-init-currencies",
"-u", "editor"])
self.assertEqual(result.exit_code, 0)
result = runner.invoke(args=["accounting-init-accounts",
"-u", "editor"])
self.assertEqual(result.exit_code, 0)
Option.query.delete()
self.client, self.csrf_token = get_client(self.app, "admin")
self.data: TestData = TestData(self.app, self.client, self.csrf_token)
def test_nobody(self) -> None:
"""Test the permission as nobody.
:return: None.
"""
client, csrf_token = get_client(self.app, "nobody")
response: httpx.Response
response = client.get(DETAIL_URI)
self.assertEqual(response.status_code, 403)
response = client.get(EDIT_URI)
self.assertEqual(response.status_code, 403)
response = client.post(UPDATE_URI, data=self.__get_form(csrf_token))
self.assertEqual(response.status_code, 403)
def test_viewer(self) -> None:
"""Test the permission as viewer.
:return: None.
"""
client, csrf_token = get_client(self.app, "viewer")
response: httpx.Response
response = client.get(DETAIL_URI)
self.assertEqual(response.status_code, 403)
response = client.get(EDIT_URI)
self.assertEqual(response.status_code, 403)
response = client.post(UPDATE_URI, data=self.__get_form(csrf_token))
self.assertEqual(response.status_code, 403)
def test_editor(self) -> None:
"""Test the permission as editor.
:return: None.
"""
client, csrf_token = get_client(self.app, "editor")
response: httpx.Response
response = client.get(DETAIL_URI)
self.assertEqual(response.status_code, 403)
response = client.get(EDIT_URI)
self.assertEqual(response.status_code, 403)
response = client.post(UPDATE_URI, data=self.__get_form(csrf_token))
self.assertEqual(response.status_code, 403)
def test_admin(self) -> None:
"""Test the permission as administrator.
:return: None.
"""
response: httpx.Response
response = self.client.get(DETAIL_URI)
self.assertEqual(response.status_code, 200)
response = self.client.get(EDIT_URI)
self.assertEqual(response.status_code, 200)
response = self.client.post(UPDATE_URI, data=self.__get_form())
self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], DETAIL_URI)
def test_set(self) -> None:
"""Test to set the options.
:return: None.
"""
from accounting.utils.options import options
form: dict[str, str]
response: httpx.Response
# Empty currency code
form = self.__get_form()
form["default_currency_code"] = " "
response = self.client.post(UPDATE_URI, data=form)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], EDIT_URI)
# Non-existing currency code
form = self.__get_form()
form["default_currency_code"] = "ZZZ"
response = self.client.post(UPDATE_URI, data=form)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], EDIT_URI)
# Empty current account
form = self.__get_form()
form["default_ie_account_code"] = " "
response = self.client.post(UPDATE_URI, data=form)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], EDIT_URI)
# Non-existing current account
form = self.__get_form()
form["default_ie_account_code"] = "9999-999"
response = self.client.post(UPDATE_URI, data=form)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], EDIT_URI)
# Not a current account
form = self.__get_form()
form["default_ie_account_code"] = Accounts.MEAL
response = self.client.post(UPDATE_URI, data=form)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], EDIT_URI)
# Recurring item name empty
form = self.__get_form()
key = [x for x in form if x.endswith("-name")][0]
form[key] = " "
response = self.client.post(UPDATE_URI, data=form)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], EDIT_URI)
# Recurring item account empty
form = self.__get_form()
key = [x for x in form if x.endswith("-account_code")][0]
form[key] = " "
response = self.client.post(UPDATE_URI, data=form)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], EDIT_URI)
# Recurring item non-expense account
form = self.__get_form()
key = [x for x in form
if x.startswith("recurring-expense-")
and x.endswith("-account_code")][0]
form[key] = Accounts.SERVICE
response = self.client.post(UPDATE_URI, data=form)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], EDIT_URI)
# Recurring item non-income account
form = self.__get_form()
key = [x for x in form
if x.startswith("recurring-income-")
and x.endswith("-account_code")][0]
form[key] = Accounts.UTILITIES
response = self.client.post(UPDATE_URI, data=form)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], EDIT_URI)
# Recurring item payable expense
form = self.__get_form()
key = [x for x in form
if x.startswith("recurring-expense-")
and x.endswith("-account_code")][0]
form[key] = Accounts.PAYABLE
response = self.client.post(UPDATE_URI, data=form)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], EDIT_URI)
# Recurring item receivable income
form = self.__get_form()
key = [x for x in form
if x.startswith("recurring-income-")
and x.endswith("-account_code")][0]
form[key] = Accounts.RECEIVABLE
response = self.client.post(UPDATE_URI, data=form)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], EDIT_URI)
# Recurring item description template empty
form = self.__get_form()
key = [x for x in form if x.endswith("-description_template")][0]
form[key] = " "
response = self.client.post(UPDATE_URI, data=form)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], EDIT_URI)
# Success, with malformed order
with self.app.app_context():
self.assertEqual(options.default_currency_code, "USD")
self.assertEqual(options.default_ie_account_code, "1111-001")
self.assertEqual(len(options.recurring.expenses), 0)
self.assertEqual(len(options.recurring.incomes), 0)
response = self.client.post(UPDATE_URI, data=self.__get_form())
self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], DETAIL_URI)
with self.app.app_context():
self.assertEqual(options.default_currency_code, "EUR")
self.assertEqual(options.default_ie_account_code, "0000-000")
self.assertEqual(len(options.recurring.expenses), 4)
self.assertEqual(options.recurring.expenses[0].account_code,
Accounts.INSURANCE)
self.assertEqual(options.recurring.expenses[1].account_code,
Accounts.POSTAGE)
self.assertEqual(options.recurring.expenses[2].account_code,
Accounts.UTILITIES)
self.assertEqual(options.recurring.expenses[3].account_code,
Accounts.RENT_EXPENSE)
self.assertEqual(len(options.recurring.incomes), 2)
self.assertEqual(options.recurring.incomes[0].account_code,
Accounts.SERVICE)
self.assertEqual(options.recurring.incomes[1].account_code,
Accounts.DONATION)
# Success, with no recurring data
form = self.__get_form()
form = {x: form[x] for x in form if not x.startswith("recurring-")}
response = self.client.post(UPDATE_URI, data=form)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], DETAIL_URI)
with self.app.app_context():
self.assertEqual(len(options.recurring.expenses), 0)
self.assertEqual(len(options.recurring.incomes), 0)
def test_update_not_modified(self) -> None:
"""Tests that the data is not modified.
:return: None.
"""
from accounting.models import Option
form: dict[str, str]
option: Option | None
resource: httpx.Response
response = self.client.post(UPDATE_URI, data=self.__get_form())
self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], DETAIL_URI)
with self.app.app_context():
option = db.session.get(Option, "recurring")
self.assertIsNotNone(option)
timestamp: datetime = option.created_at - timedelta(seconds=5)
option.created_at = timestamp
option.updated_at = timestamp
db.session.commit()
# The recurring setting was not modified
form = self.__get_form()
form["default_currency_code"] = "JPY"
response = self.client.post(UPDATE_URI, data=form)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], DETAIL_URI)
with self.app.app_context():
option = db.session.get(Option, "recurring")
self.assertIsNotNone(option)
self.assertEqual(option.created_at, timestamp)
self.assertEqual(option.updated_at, timestamp)
# The recurring setting was modified
form = self.__get_form()
key: str = [x for x in form
if x.startswith("recurring-expense-")
and x.endswith("-account_code")][0]
form[key] = Accounts.MEAL
response = self.client.post(UPDATE_URI, data=form)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], DETAIL_URI)
with self.app.app_context():
option = db.session.get(Option, "recurring")
self.assertIsNotNone(option)
self.assertLess(option.created_at, option.updated_at)
def test_created_updated_by(self) -> None:
"""Tests the created-by and updated-by record.
:return: None.
"""
from accounting.models import Option
from accounting.utils.user import get_user_pk
admin_username, editor_username = "admin", "editor"
option: Option | None
response: httpx.Response
response = self.client.post(UPDATE_URI, data=self.__get_form())
self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], DETAIL_URI)
with self.app.app_context():
editor_pk: int = get_user_pk(editor_username)
option = db.session.get(Option, "recurring")
self.assertIsNotNone(option)
option.created_by_id = editor_pk
option.updated_by_id = editor_pk
db.session.commit()
form: dict[str, str] = self.__get_form()
key: str = [x for x in form
if x.startswith("recurring-expense-")
and x.endswith("-account_code")][0]
form[key] = Accounts.MEAL
response = self.client.post(UPDATE_URI, data=form)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], DETAIL_URI)
with self.app.app_context():
option = db.session.get(Option, "recurring")
self.assertIsNotNone(option)
self.assertEqual(option.created_by.username, editor_username)
self.assertEqual(option.updated_by.username, admin_username)
def __get_form(self, csrf_token: str | None = None) -> dict[str, str]:
"""Returns the option form.
:param csrf_token: The CSRF token.
:return: The option form.
"""
if csrf_token is None:
csrf_token = self.csrf_token
return {"csrf_token": csrf_token,
"next": NEXT_URI,
"default_currency_code": "EUR",
"default_ie_account_code": "0000-000",
"recurring-expense-1-name": "Water bill",
"recurring-expense-1-account_code": Accounts.UTILITIES,
"recurring-expense-1-description_template":
"Water bill for {last_bimonthly_name}",
"recurring-expense-3-no": "16",
"recurring-expense-3-name": "Phone bill",
"recurring-expense-3-account_code": Accounts.POSTAGE,
"recurring-expense-3-description_template":
"Phone bill for {last_month_name}",
"recurring-expense-12-name": "Rent",
"recurring-expense-12-account_code": Accounts.RENT_EXPENSE,
"recurring-expense-12-description_template":
"Rent for {this_month_name}",
"recurring-expense-26-no": "7",
"recurring-expense-26-name": "Insurance",
"recurring-expense-26-account_code": Accounts.INSURANCE,
"recurring-expense-26-description_template":
"Insurance for {last_month_name}",
"recurring-income-2-no": "12",
"recurring-income-2-name": "Donation",
"recurring-income-2-account_code": Accounts.DONATION,
"recurring-income-2-description_template":
"Donation for {this_month_name}",
"recurring-income-15-no": "4",
"recurring-income-15-name": "Payroll",
"recurring-income-15-account_code": Accounts.SERVICE,
"recurring-income-15-description_template":
"Payroll for {last_month_name}"}