- 公司账号创建改为随机一次性初始密码,只在创建响应中显示一次, 密码保证不等于用户名;删除「用户名即初始密码」兼容分支,继续 强制首次登录改密。 - 跨公司相同字节文件上传只返回通用重复状态:不再返回其他公司的 原批次 ID、银行、模板、期间、交易数或诊断;同公司重复上传的 幂等摘要保持可用。 - 补充服务端回归测试,覆盖同公司与跨公司两个分支及随机密码; 完整测试 85 项全绿,node --check 通过。
598 lines
25 KiB
Python
598 lines
25 KiB
Python
"""Tests for dynamic master data (companies, bank accounts, aliases).
|
|
|
|
Unit tests exercise normalization, masking, effective-window boundaries and
|
|
alias-match priority against an in-memory database; the HTTP integration
|
|
class drives a live server to cover the submission -> review -> disable
|
|
workflow, uniqueness under concurrency, masking boundaries and RBAC.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
import tempfile
|
|
import threading
|
|
import unittest
|
|
|
|
from bank_importer import auth, master_data
|
|
from bank_importer.db import connect, migrate
|
|
|
|
import server
|
|
from test_server_auth import Client, as_json
|
|
|
|
|
|
BOOTSTRAP_PASSWORD = "BootAdmin123"
|
|
ADMIN_PASSWORD = "AdminPass123"
|
|
CASHIER_PASSWORD = "Cashier123"
|
|
|
|
|
|
class MasterDataUnitTests(unittest.TestCase):
|
|
def setUp(self) -> None:
|
|
self.connection = connect(":memory:")
|
|
migrate(self.connection)
|
|
now = master_data.utc_now()
|
|
cursor = self.connection.execute(
|
|
"INSERT INTO companies (name, created_at, updated_at) VALUES ('甲公司', ?, ?)",
|
|
(now, now),
|
|
)
|
|
self.company_id = int(cursor.lastrowid)
|
|
self.connection.commit()
|
|
|
|
def tearDown(self) -> None:
|
|
self.connection.close()
|
|
|
|
def _active_account(self, number: str = "1234567890123") -> object:
|
|
return master_data.submit_bank_account(
|
|
self.connection,
|
|
company_id=self.company_id,
|
|
bank_name="中信银行",
|
|
account_type="基本户",
|
|
account_number=number,
|
|
start_date="2026-01-01",
|
|
actor=None,
|
|
)
|
|
|
|
def test_normalize_account_number(self) -> None:
|
|
self.assertEqual("1234567890", master_data.normalize_account_number("1234 5678-90"))
|
|
self.assertEqual("1234567890", master_data.normalize_account_number("1234567890"))
|
|
for invalid in ("", "123", "abc123456", "12345678901234567890123456789012345"):
|
|
with self.assertRaises(ValueError):
|
|
master_data.normalize_account_number(invalid)
|
|
|
|
def test_mask_account_number(self) -> None:
|
|
self.assertEqual("****9012", master_data.mask_account_number("123456789012"))
|
|
self.assertEqual("****123", master_data.mask_account_number("123"))
|
|
|
|
def test_is_usable_window_boundaries(self) -> None:
|
|
account = self._active_account()
|
|
account = master_data.review_bank_account(
|
|
self.connection, account["id"], "approve", None, self._admin(),
|
|
effective_from="2026-02-01",
|
|
)
|
|
self.assertFalse(master_data.is_usable(account, "2026-01-31"))
|
|
self.assertTrue(master_data.is_usable(account, "2026-02-01"))
|
|
# A scheduled effective_to bounds the usable window (inclusive).
|
|
with self.connection:
|
|
self.connection.execute(
|
|
"UPDATE bank_accounts SET effective_to = '2026-06-30' WHERE id = ?",
|
|
(account["id"],),
|
|
)
|
|
account = master_data.get_account(self.connection, account["id"])
|
|
self.assertTrue(master_data.is_usable(account, "2026-06-30"))
|
|
self.assertFalse(master_data.is_usable(account, "2026-07-01"))
|
|
# Disabled accounts never accept new uploads, even inside the window.
|
|
account = master_data.review_bank_account(
|
|
self.connection, account["id"], "disable", "账户销户", self._admin(),
|
|
effective_to="2026-06-30",
|
|
)
|
|
self.assertFalse(master_data.is_usable(account, "2026-06-30"))
|
|
# …but they still identify historical rows inside the window.
|
|
self.assertTrue(master_data.is_identifiable(account, "2026-06-30"))
|
|
self.assertFalse(master_data.is_identifiable(account, "2026-07-01"))
|
|
|
|
def _admin(self):
|
|
user = self.connection.execute(
|
|
"SELECT * FROM users WHERE username = 'admin-u'"
|
|
).fetchone()
|
|
if user is None:
|
|
auth.create_user(self.connection, "admin-u", "AdminPass123", "admin")
|
|
user = self.connection.execute(
|
|
"SELECT * FROM users WHERE username = 'admin-u'"
|
|
).fetchone()
|
|
return user
|
|
|
|
def test_pending_and_returned_accounts_never_usable(self) -> None:
|
|
account = master_data.submit_bank_account(
|
|
self.connection,
|
|
company_id=self.company_id,
|
|
bank_name="中信银行",
|
|
account_type="一般户",
|
|
account_number="9988776655",
|
|
start_date="2026-01-01",
|
|
actor=None,
|
|
)
|
|
self.assertFalse(master_data.is_usable(account, "2026-06-01"))
|
|
account = master_data.review_bank_account(
|
|
self.connection, account["id"], "return", "资料不全", self._admin()
|
|
)
|
|
self.assertEqual("returned", account["status"])
|
|
self.assertFalse(master_data.is_usable(account, "2026-06-01"))
|
|
|
|
def test_alias_match_priority_and_window(self) -> None:
|
|
exact_account = self._active_account("1111222233334")
|
|
exact_account = master_data.review_bank_account(
|
|
self.connection, exact_account["id"], "approve", None, self._admin(),
|
|
effective_from="2026-01-01",
|
|
)
|
|
alias_account = master_data.submit_bank_account(
|
|
self.connection,
|
|
company_id=self.company_id,
|
|
bank_name="建设银行",
|
|
account_type="一般户",
|
|
account_number="5555666677778",
|
|
start_date="2026-01-01",
|
|
actor=None,
|
|
)
|
|
alias_account = master_data.review_bank_account(
|
|
self.connection, alias_account["id"], "approve", None, self._admin(),
|
|
effective_from="2026-01-01",
|
|
)
|
|
master_data.add_alias(
|
|
self.connection, alias_account["id"], "account", "9999000011112",
|
|
priority=5, actor=None,
|
|
)
|
|
master_data.add_alias(
|
|
self.connection, alias_account["id"], "name", "甲公司郑州分部",
|
|
effective_from="2026-03-01", effective_to="2026-03-31", actor=None,
|
|
)
|
|
|
|
# Exact account number beats the account alias tier.
|
|
master_data.add_alias(
|
|
self.connection, alias_account["id"], "account", "1111222233334",
|
|
priority=1, actor=None,
|
|
)
|
|
hits = master_data.match_account(
|
|
self.connection, account_number="1111-2222 3333 4", on_date="2026-06-01"
|
|
)
|
|
self.assertEqual(exact_account["id"], hits[0]["bank_account_id"])
|
|
self.assertEqual("account_exact", hits[0]["via"])
|
|
self.assertEqual(alias_account["id"], hits[1]["bank_account_id"])
|
|
self.assertEqual("account_alias", hits[1]["via"])
|
|
|
|
# Name alias only matches inside its effective window.
|
|
hits = master_data.match_account(
|
|
self.connection, name="甲公司郑州分部 ", on_date="2026-03-15"
|
|
)
|
|
self.assertEqual(alias_account["id"], hits[0]["bank_account_id"])
|
|
self.assertEqual("name_alias", hits[0]["via"])
|
|
self.assertEqual(
|
|
[],
|
|
master_data.match_account(
|
|
self.connection, name="甲公司郑州分部", on_date="2026-04-01"
|
|
),
|
|
)
|
|
|
|
def test_match_never_returns_pending_or_disabled(self) -> None:
|
|
pending = master_data.submit_bank_account(
|
|
self.connection,
|
|
company_id=self.company_id,
|
|
bank_name="郑州银行",
|
|
account_type="一般户",
|
|
account_number="7777888899990",
|
|
actor=None,
|
|
)
|
|
self.assertEqual(
|
|
[],
|
|
master_data.match_account(self.connection, account_number="7777888899990"),
|
|
)
|
|
account = master_data.review_bank_account(
|
|
self.connection, pending["id"], "approve", None, self._admin(),
|
|
effective_from="2026-01-01",
|
|
)
|
|
self.assertTrue(
|
|
master_data.match_account(self.connection, account_number="7777888899990")
|
|
)
|
|
master_data.review_bank_account(
|
|
self.connection, account["id"], "disable", "销户", self._admin(),
|
|
effective_to="2026-06-30",
|
|
)
|
|
# Disabled accounts still resolve dates inside their effective window…
|
|
self.assertTrue(
|
|
master_data.match_account(
|
|
self.connection, account_number="7777888899990", on_date="2026-06-30"
|
|
)
|
|
)
|
|
# …and never resolve dates after it.
|
|
self.assertEqual(
|
|
[],
|
|
master_data.match_account(
|
|
self.connection, account_number="7777888899990", on_date="2026-07-01"
|
|
),
|
|
)
|
|
|
|
|
|
class MasterDataApiTests(unittest.TestCase):
|
|
"""Live-server workflow tests for account registration and review."""
|
|
|
|
@classmethod
|
|
def setUpClass(cls) -> None:
|
|
import os
|
|
|
|
cls.temp_dir = tempfile.TemporaryDirectory()
|
|
root = Path(cls.temp_dir.name)
|
|
cls.db_path = root / "app.db"
|
|
cls.storage = root / "files"
|
|
|
|
cls._old_db_path = server.DB_PATH
|
|
cls._old_storage = server.STORAGE_DIR
|
|
server.DB_PATH = cls.db_path
|
|
server.STORAGE_DIR = cls.storage
|
|
|
|
os.environ["APP_BOOTSTRAP_ADMIN_PASSWORD"] = BOOTSTRAP_PASSWORD
|
|
connection = connect(cls.db_path)
|
|
migrate(connection)
|
|
server.ensure_bootstrap_admin(connection)
|
|
connection.close()
|
|
|
|
class QuietHandler(server.AppHandler):
|
|
def log_message(self, *args) -> None:
|
|
pass
|
|
|
|
cls.httpd = server.ThreadingHTTPServer(("127.0.0.1", 0), QuietHandler)
|
|
cls.port = cls.httpd.server_address[1]
|
|
cls.thread = threading.Thread(target=cls.httpd.serve_forever, daemon=True)
|
|
cls.thread.start()
|
|
|
|
cls.admin = Client("127.0.0.1", cls.port)
|
|
status, _, data = cls.admin.post_json(
|
|
"/api/login",
|
|
{"username": "group-admin", "password": BOOTSTRAP_PASSWORD, "portal": "admin"},
|
|
)
|
|
assert status == 200, data
|
|
status, _, data = cls.admin.post_json(
|
|
"/api/password/change",
|
|
{"old_password": BOOTSTRAP_PASSWORD, "new_password": ADMIN_PASSWORD},
|
|
)
|
|
assert status == 200, data
|
|
|
|
# Company A created together with its cashier login in one call.
|
|
status, _, data = cls.admin.post_json(
|
|
"/api/admin/companies",
|
|
{"name": "甲公司", "credit_code": "91410100TEST", "cashier_name": "牛女士",
|
|
"username": "cashier-a"},
|
|
)
|
|
assert status == 200, data
|
|
payload = as_json(data)
|
|
cls.company_a = payload["company_id"]
|
|
cls.initial_a = payload["initial_password"]
|
|
assert cls.initial_a != "cashier-a"
|
|
assert len(cls.initial_a) >= 12
|
|
|
|
status, _, data = cls.admin.post_json("/api/admin/companies", {"name": "乙公司"})
|
|
assert status == 200, data
|
|
cls.company_b = as_json(data)["company_id"]
|
|
status, _, data = cls.admin.post_json(
|
|
"/api/admin/users", {"username": "cashier-b", "company_id": cls.company_b}
|
|
)
|
|
assert status == 200, data
|
|
cls.initial_b = as_json(data)["initial_password"]
|
|
assert cls.initial_b != "cashier-b"
|
|
|
|
cls.cashier_a = cls._login_company_user("cashier-a", cls.initial_a)
|
|
cls.cashier_b = cls._login_company_user("cashier-b", cls.initial_b)
|
|
|
|
@classmethod
|
|
def _login_company_user(cls, username: str, initial: str) -> Client:
|
|
client = Client("127.0.0.1", cls.port)
|
|
status, _, data = client.post_json(
|
|
"/api/login", {"username": username, "password": initial, "portal": "company"}
|
|
)
|
|
assert status == 200, data
|
|
status, _, data = client.post_json(
|
|
"/api/password/change",
|
|
{"old_password": initial, "new_password": CASHIER_PASSWORD},
|
|
)
|
|
assert status == 200, data
|
|
return client
|
|
|
|
@classmethod
|
|
def tearDownClass(cls) -> None:
|
|
import os
|
|
|
|
cls.httpd.shutdown()
|
|
cls.httpd.server_close()
|
|
server.DB_PATH = cls._old_db_path
|
|
server.STORAGE_DIR = cls._old_storage
|
|
os.environ.pop("APP_BOOTSTRAP_ADMIN_PASSWORD", None)
|
|
cls.temp_dir.cleanup()
|
|
|
|
# ------------------------------------------------------------------
|
|
# Helpers
|
|
# ------------------------------------------------------------------
|
|
|
|
def submit_account(self, client: Client, number: str, bank: str = "郑州银行"):
|
|
return client.post_json(
|
|
"/api/company/accounts",
|
|
{"bank_name": bank, "account_type": "一般户",
|
|
"account_number": number, "start_date": "2026-08-01"},
|
|
)
|
|
|
|
def review(self, account_id: int, decision: str, reason: str = ""):
|
|
return self.admin.post_json(
|
|
f"/api/admin/accounts/{account_id}/review",
|
|
{"decision": decision, "reason": reason},
|
|
)
|
|
|
|
# ------------------------------------------------------------------
|
|
# Company management
|
|
# ------------------------------------------------------------------
|
|
|
|
def test_new_company_appears_in_admin_list_with_user(self) -> None:
|
|
status, _, data = self.admin.get("/api/admin/companies")
|
|
self.assertEqual(200, status)
|
|
companies = {c["name"]: c for c in as_json(data)["companies"]}
|
|
self.assertIn("甲公司", companies)
|
|
self.assertEqual("91410100TEST", companies["甲公司"]["credit_code"])
|
|
self.assertEqual("牛女士", companies["甲公司"]["cashier_name"])
|
|
self.assertIn("cashier-a", companies["甲公司"]["usernames"])
|
|
self.assertEqual(0, companies["乙公司"]["account_count"])
|
|
|
|
def test_company_user_cannot_change_own_company_binding(self) -> None:
|
|
# A company_id in the submission body is ignored: the account is
|
|
# always bound to the session company.
|
|
status, _, data = self.cashier_a.post_json(
|
|
"/api/company/accounts",
|
|
{"bank_name": "民生银行", "account_type": "一般户",
|
|
"account_number": "6001000100010001", "company_id": self.company_b},
|
|
)
|
|
self.assertEqual(200, status, data)
|
|
self.assertEqual(self.company_a, as_json(data)["account"]["company_id"])
|
|
|
|
# ------------------------------------------------------------------
|
|
# Account registration -> review workflow
|
|
# ------------------------------------------------------------------
|
|
|
|
def test_full_registration_workflow(self) -> None:
|
|
status, _, data = self.submit_account(self.cashier_a, "6222 0210-0100 8888")
|
|
self.assertEqual(200, status, data)
|
|
account = as_json(data)["account"]
|
|
account_id = account["id"]
|
|
self.assertEqual("pending", account["status"])
|
|
# Company view is masked only.
|
|
self.assertEqual("****8888", account["account_number_masked"])
|
|
self.assertNotIn("account_number", account)
|
|
self.assertFalse(account.get("usable", True))
|
|
|
|
# Admin audit view shows the full normalized number.
|
|
status, _, data = self.admin.get("/api/admin/accounts")
|
|
self.assertEqual(200, status)
|
|
full = [a for a in as_json(data)["accounts"] if a["id"] == account_id][0]
|
|
self.assertEqual("62220210010088 88".replace(" ", ""), full["account_number"])
|
|
self.assertEqual("甲公司", full["company_name"])
|
|
|
|
# Pending account cannot upload.
|
|
sample = Path("流水模板/中信银行账户流水.xlsx")
|
|
status, _, data = self.cashier_a.post_multipart(
|
|
"/api/parse", {"bank_account_id": str(account_id)},
|
|
sample.name, sample.read_bytes(),
|
|
)
|
|
self.assertEqual(409, status, data)
|
|
|
|
# Approve -> active and usable.
|
|
status, _, data = self.review(account_id, "approve")
|
|
self.assertEqual(200, status, data)
|
|
self.assertEqual("active", as_json(data)["account"]["status"])
|
|
self.assertEqual("2026-08-01", as_json(data)["account"]["effective_from"])
|
|
|
|
status, _, data = self.cashier_a.get("/api/company/accounts")
|
|
mine = [a for a in as_json(data)["accounts"] if a["id"] == account_id][0]
|
|
self.assertTrue(mine["usable"])
|
|
|
|
# Approved account accepts uploads.
|
|
status, _, data = self.cashier_a.post_multipart(
|
|
"/api/parse", {"bank_account_id": str(account_id)},
|
|
sample.name, sample.read_bytes(),
|
|
)
|
|
self.assertEqual(200, status, data)
|
|
|
|
# Disable -> leaves the usable window.
|
|
status, _, data = self.review(account_id, "disable", "账户销户")
|
|
self.assertEqual(200, status, data)
|
|
disabled = as_json(data)["account"]
|
|
self.assertEqual("disabled", disabled["status"])
|
|
self.assertIsNotNone(disabled["effective_to"])
|
|
|
|
status, _, data = self.cashier_a.get("/api/company/accounts")
|
|
mine = [a for a in as_json(data)["accounts"] if a["id"] == account_id][0]
|
|
self.assertFalse(mine["usable"])
|
|
|
|
def test_return_and_resubmit_reuses_same_row(self) -> None:
|
|
status, _, data = self.submit_account(self.cashier_a, "3100998877665")
|
|
self.assertEqual(200, status, data)
|
|
account_id = as_json(data)["account"]["id"]
|
|
|
|
status, _, data = self.review(account_id, "return", "开户许可证模糊")
|
|
self.assertEqual(200, status, data)
|
|
self.assertEqual("returned", as_json(data)["account"]["status"])
|
|
|
|
# Return requires a reason.
|
|
status, _, data = self.submit_account(self.cashier_a, "3100998877666")
|
|
other_id = as_json(data)["account"]["id"]
|
|
status, _, _ = self.review(other_id, "return")
|
|
self.assertEqual(400, status)
|
|
|
|
# Resubmission reopens the same row as pending.
|
|
status, _, data = self.submit_account(self.cashier_a, "3100 9988-7766 5")
|
|
self.assertEqual(200, status, data)
|
|
resubmitted = as_json(data)["account"]
|
|
self.assertEqual(account_id, resubmitted["id"])
|
|
self.assertEqual("pending", resubmitted["status"])
|
|
|
|
# The whole trail is auditable with actor, time and reason.
|
|
status, _, data = self.admin.get(
|
|
f"/api/admin/master-changes?entity_type=bank_account&entity_id={account_id}"
|
|
)
|
|
self.assertEqual(200, status)
|
|
changes = as_json(data)["changes"]
|
|
actions = [change["action"] for change in changes]
|
|
self.assertEqual(["resubmit", "return", "submit"], actions)
|
|
returned = [c for c in changes if c["action"] == "return"][0]
|
|
self.assertEqual("开户许可证模糊", returned["reason"])
|
|
self.assertEqual("group-admin", returned["actor_username"])
|
|
self.assertIn('"pending"', returned["before_json"])
|
|
self.assertIn('"returned"', returned["after_json"])
|
|
|
|
def test_duplicate_normalized_number_conflicts(self) -> None:
|
|
status, _, _ = self.submit_account(self.cashier_a, "4501111222233")
|
|
self.assertEqual(200, status)
|
|
# Same company, same number with different separators.
|
|
status, _, data = self.submit_account(self.cashier_a, "4501-1112 2223-3")
|
|
self.assertEqual(409, status, data)
|
|
# Another company registering the same number conflicts too.
|
|
status, _, data = self.submit_account(self.cashier_b, "4501111222233")
|
|
self.assertEqual(409, status, data)
|
|
self.assertIn("其他公司", as_json(data)["message"])
|
|
|
|
def test_concurrent_submissions_create_only_one_account(self) -> None:
|
|
number = "8800123456789"
|
|
results: list[int] = []
|
|
|
|
def submit() -> None:
|
|
client = Client("127.0.0.1", self.port)
|
|
client.cookies.update(self.cashier_a.cookies)
|
|
status, _, _ = client.post_json(
|
|
"/api/company/accounts",
|
|
{"bank_name": "中信银行", "account_type": "基本户",
|
|
"account_number": number},
|
|
)
|
|
results.append(status)
|
|
|
|
threads = [threading.Thread(target=submit) for _ in range(2)]
|
|
for thread in threads:
|
|
thread.start()
|
|
for thread in threads:
|
|
thread.join()
|
|
self.assertEqual(sorted(results), [200, 409])
|
|
|
|
connection = connect(self.db_path)
|
|
try:
|
|
count = connection.execute(
|
|
"SELECT COUNT(*) AS n FROM bank_accounts WHERE account_number = ?",
|
|
(number,),
|
|
).fetchone()["n"]
|
|
finally:
|
|
connection.close()
|
|
self.assertEqual(1, count)
|
|
|
|
# ------------------------------------------------------------------
|
|
# RBAC and masking
|
|
# ------------------------------------------------------------------
|
|
|
|
def test_company_user_forbidden_on_admin_account_endpoints(self) -> None:
|
|
for call in (
|
|
lambda: self.cashier_a.get("/api/admin/accounts"),
|
|
lambda: self.cashier_a.post_json(
|
|
"/api/admin/accounts/1/review", {"decision": "approve"}
|
|
),
|
|
lambda: self.cashier_a.get("/api/admin/accounts/1/aliases"),
|
|
lambda: self.cashier_a.post_json(
|
|
"/api/admin/accounts/1/aliases",
|
|
{"alias_kind": "name", "alias_value": "x"},
|
|
),
|
|
lambda: self.cashier_a.get("/api/admin/master-changes"),
|
|
):
|
|
status, _, data = call()
|
|
self.assertEqual(403, status, data)
|
|
|
|
def test_admin_cannot_use_company_account_endpoint(self) -> None:
|
|
status, _, _ = self.admin.get("/api/company/accounts")
|
|
self.assertEqual(403, status)
|
|
status, _, _ = self.submit_account(self.admin, "1000200030004")
|
|
self.assertEqual(403, status)
|
|
|
|
def test_unauthenticated_master_data_calls_return_401(self) -> None:
|
|
anon = Client("127.0.0.1", self.port)
|
|
for call in (
|
|
lambda: anon.get("/api/company/accounts"),
|
|
lambda: anon.get("/api/admin/accounts"),
|
|
lambda: anon.post_json("/api/company/accounts", {}),
|
|
lambda: anon.get("/api/admin/master-changes"),
|
|
):
|
|
status, _, _ = call()
|
|
self.assertEqual(401, status)
|
|
|
|
def test_company_account_list_scoped_and_masked(self) -> None:
|
|
status, _, data = self.cashier_b.get("/api/company/accounts")
|
|
self.assertEqual(200, status)
|
|
for account in as_json(data)["accounts"]:
|
|
self.assertEqual(self.company_b, account["company_id"])
|
|
self.assertNotIn("account_number", account)
|
|
self.assertTrue(account["account_number_masked"].startswith("****"))
|
|
|
|
def test_upload_with_other_companys_account_returns_404(self) -> None:
|
|
status, _, data = self.submit_account(self.cashier_a, "5550001112223")
|
|
account_id = as_json(data)["account"]["id"]
|
|
status, _, _ = self.review(account_id, "approve")
|
|
self.assertEqual(200, status)
|
|
sample = Path("流水模板/中信银行账户流水.xlsx")
|
|
status, _, data = self.cashier_b.post_multipart(
|
|
"/api/parse", {"bank_account_id": str(account_id)},
|
|
sample.name, sample.read_bytes(),
|
|
)
|
|
self.assertEqual(404, status, data)
|
|
|
|
# ------------------------------------------------------------------
|
|
# Aliases via API
|
|
# ------------------------------------------------------------------
|
|
|
|
def test_alias_crud_and_audit(self) -> None:
|
|
status, _, data = self.submit_account(self.cashier_a, "6601234509876")
|
|
account_id = as_json(data)["account"]["id"]
|
|
status, _, _ = self.review(account_id, "approve")
|
|
self.assertEqual(200, status)
|
|
|
|
status, _, data = self.admin.post_json(
|
|
f"/api/admin/accounts/{account_id}/aliases",
|
|
{"alias_kind": "name", "alias_value": "甲公司工会",
|
|
"effective_from": "2026-01-01", "effective_to": "2026-12-31",
|
|
"priority": 10},
|
|
)
|
|
self.assertEqual(200, status, data)
|
|
alias_id = as_json(data)["alias_id"]
|
|
|
|
# Duplicate alias on the same account conflicts.
|
|
status, _, _ = self.admin.post_json(
|
|
f"/api/admin/accounts/{account_id}/aliases",
|
|
{"alias_kind": "name", "alias_value": "甲公司工会"},
|
|
)
|
|
self.assertEqual(409, status)
|
|
|
|
status, _, data = self.admin.get(f"/api/admin/accounts/{account_id}/aliases")
|
|
self.assertEqual(200, status)
|
|
aliases = as_json(data)["aliases"]
|
|
self.assertEqual(1, len(aliases))
|
|
self.assertEqual("甲公司工会", aliases[0]["alias_value"])
|
|
self.assertEqual(10, aliases[0]["priority"])
|
|
|
|
status, _, data = self.admin.get(
|
|
f"/api/admin/master-changes?entity_type=account_alias&entity_id={alias_id}"
|
|
)
|
|
changes = as_json(data)["changes"]
|
|
self.assertEqual("create", changes[0]["action"])
|
|
self.assertIn("2026-12-31", changes[0]["after_json"])
|
|
|
|
# Invalid alias input is rejected, unknown account is 404.
|
|
status, _, _ = self.admin.post_json(
|
|
f"/api/admin/accounts/{account_id}/aliases",
|
|
{"alias_kind": "account", "alias_value": "abc"},
|
|
)
|
|
self.assertEqual(400, status)
|
|
status, _, _ = self.admin.post_json(
|
|
"/api/admin/accounts/99999/aliases",
|
|
{"alias_kind": "name", "alias_value": "x"},
|
|
)
|
|
self.assertEqual(404, status)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|