Files
caiwuzongzhang/tests/test_persistence.py
T
腾讯WorkBuddy 7f1a93f6a6 B-64: 安全修复——随机初始密码与跨公司重复上传隔离
- 公司账号创建改为随机一次性初始密码,只在创建响应中显示一次,
  密码保证不等于用户名;删除「用户名即初始密码」兼容分支,继续
  强制首次登录改密。
- 跨公司相同字节文件上传只返回通用重复状态:不再返回其他公司的
  原批次 ID、银行、模板、期间、交易数或诊断;同公司重复上传的
  幂等摘要保持可用。
- 补充服务端回归测试,覆盖同公司与跨公司两个分支及随机密码;
  完整测试 85 项全绿,node --check 通过。
2026-08-16 01:50:42 +08:00

324 lines
13 KiB
Python

from __future__ import annotations
from decimal import Decimal
import hashlib
import sqlite3
import tempfile
from pathlib import Path
import unittest
from openpyxl import Workbook
from bank_importer.db import applied_versions, connect, migrate, rollback, utc_now
from bank_importer.importing import import_statement
ROOT = Path(__file__).resolve().parents[1]
SAMPLES = ROOT / "流水模板"
SAMPLE_FILE = SAMPLES / "中国建设银行账户流水.xls"
class PersistenceTestCase(unittest.TestCase):
def setUp(self) -> None:
self.temp_dir = tempfile.TemporaryDirectory()
self.addCleanup(self.temp_dir.cleanup)
root = Path(self.temp_dir.name)
self.db_path = root / "app.db"
self.storage = root / "files"
self.connection = connect(self.db_path)
self.addCleanup(self.connection.close)
migrate(self.connection)
def import_sample(self) -> object:
return import_statement(
self.connection,
self.storage,
SAMPLE_FILE.name,
SAMPLE_FILE.read_bytes(),
)
class MigrationTests(PersistenceTestCase):
def test_migrate_creates_schema_and_is_idempotent(self) -> None:
first = applied_versions(self.connection)
self.assertEqual([1, 2, 3], first)
self.assertEqual([], migrate(self.connection))
self.assertEqual(first, applied_versions(self.connection))
tables = {
row["name"]
for row in self.connection.execute(
"SELECT name FROM sqlite_master WHERE type = 'table'"
)
}
for table in (
"companies",
"bank_accounts",
"account_aliases",
"master_data_changes",
"source_files",
"import_batches",
"sheet_batches",
"source_rows",
"import_exceptions",
"users",
"sessions",
"login_attempts",
"audit_log",
"schema_migrations",
):
self.assertIn(table, tables)
def test_rollback_removes_schema_and_forward_rebuilds_it(self) -> None:
self.assertEqual([3, 2, 1], rollback(self.connection, 0))
self.assertEqual([], applied_versions(self.connection))
remaining = self.connection.execute(
"SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'source_rows'"
).fetchone()
self.assertIsNone(remaining)
self.assertEqual([1, 2, 3], migrate(self.connection))
self.assertEqual([1, 2, 3], applied_versions(self.connection))
class ImportPersistenceTests(PersistenceTestCase):
def test_import_persists_batch_sheets_and_rows(self) -> None:
result = self.import_sample()
self.assertEqual("parsed", result.status)
batch = self.connection.execute(
"SELECT status FROM import_batches WHERE id = ?", (result.batch_id,)
).fetchone()
self.assertEqual("parsed", batch["status"])
sheets = self.connection.execute(
"SELECT * FROM sheet_batches WHERE import_batch_id = ?", (result.batch_id,)
).fetchall()
self.assertEqual(1, len(sheets))
self.assertEqual("中国建设银行", sheets[0]["bank_name"])
self.assertEqual("ccb-account-detail-v1", sheets[0]["template_id"])
self.assertEqual(1, sheets[0]["template_version"])
rows = self.connection.execute(
"SELECT * FROM source_rows WHERE sheet_batch_id = ?", (sheets[0]["id"],)
).fetchall()
self.assertEqual(sheets[0]["transaction_count"], len(rows))
def test_amounts_keep_decimal_precision(self) -> None:
result = self.import_sample()
rows = self.connection.execute(
"""
SELECT income, expense FROM source_rows
WHERE sheet_batch_id IN (
SELECT id FROM sheet_batches WHERE import_batch_id = ?
)
""",
(result.batch_id,),
).fetchall()
self.assertTrue(rows)
for row in rows:
self.assertEqual(row["income"], str(Decimal(row["income"])))
self.assertEqual(row["expense"], str(Decimal(row["expense"])))
def test_every_row_traces_back_to_file_sheet_and_template(self) -> None:
result = self.import_sample()
row = self.connection.execute(
"""
SELECT r.source_row, s.sheet_name, s.template_id, s.template_version, f.sha256
FROM source_rows r
JOIN sheet_batches s ON s.id = r.sheet_batch_id
JOIN import_batches b ON b.id = s.import_batch_id
JOIN source_files f ON f.id = b.source_file_id
WHERE b.id = ?
LIMIT 1
""",
(result.batch_id,),
).fetchone()
self.assertIsNotNone(row)
self.assertEqual(
hashlib.sha256(SAMPLE_FILE.read_bytes()).hexdigest(), row["sha256"]
)
self.assertEqual("ccb-account-detail-v1", row["template_id"])
self.assertEqual(1, row["template_version"])
self.assertGreater(row["source_row"], 0)
self.assertTrue(row["sheet_name"])
def test_source_file_is_stored_immutably_by_content_hash(self) -> None:
self.import_sample()
stored = self.connection.execute(
"SELECT storage_path FROM source_files"
).fetchone()
stored_path = Path(stored["storage_path"])
self.assertTrue(stored_path.is_file())
self.assertEqual(SAMPLE_FILE.read_bytes(), stored_path.read_bytes())
self.assertIn(hashlib.sha256(SAMPLE_FILE.read_bytes()).hexdigest(), stored_path.name)
def test_data_survives_reconnect(self) -> None:
result = self.import_sample()
self.connection.close()
reopened = connect(self.db_path)
self.addCleanup(reopened.close)
batch = reopened.execute(
"SELECT status FROM import_batches WHERE id = ?", (result.batch_id,)
).fetchone()
self.assertEqual("parsed", batch["status"])
files = reopened.execute("SELECT COUNT(*) AS n FROM source_files").fetchone()
self.assertEqual(1, files["n"])
rows = reopened.execute("SELECT COUNT(*) AS n FROM source_rows").fetchone()
self.assertGreater(rows["n"], 0)
class IdempotencyTests(PersistenceTestCase):
def test_duplicate_upload_creates_no_second_facts(self) -> None:
first = self.import_sample()
second = self.import_sample()
self.assertEqual("parsed", first.status)
self.assertEqual("duplicate", second.status)
self.assertEqual(first.batch_id, second.batch_id)
self.assertTrue(second.duplicate_same_company)
files = self.connection.execute("SELECT COUNT(*) AS n FROM source_files").fetchone()
self.assertEqual(1, files["n"])
sheets = self.connection.execute("SELECT COUNT(*) AS n FROM sheet_batches").fetchone()
self.assertEqual(1, sheets["n"])
rows = self.connection.execute("SELECT COUNT(*) AS n FROM source_rows").fetchone()
self.assertEqual(first.batches[0].transactions.__len__(), rows["n"])
duplicate = self.connection.execute(
"SELECT status, duplicate_of_id FROM import_batches WHERE status = 'duplicate'"
).fetchone()
self.assertEqual(first.batch_id, duplicate["duplicate_of_id"])
def test_cross_company_duplicate_never_returns_other_companys_batch(self) -> None:
now = utc_now()
with self.connection:
self.connection.execute(
"INSERT INTO companies (name, created_at, updated_at) VALUES ('甲公司', ?, ?)",
(now, now),
)
self.connection.execute(
"INSERT INTO companies (name, created_at, updated_at) VALUES ('乙公司', ?, ?)",
(now, now),
)
company_a, company_b = [
row["id"]
for row in self.connection.execute("SELECT id FROM companies ORDER BY id").fetchall()
]
first = import_statement(
self.connection, self.storage, SAMPLE_FILE.name,
SAMPLE_FILE.read_bytes(), company_id=company_a,
)
self.assertEqual("parsed", first.status)
second = import_statement(
self.connection, self.storage, SAMPLE_FILE.name,
SAMPLE_FILE.read_bytes(), company_id=company_b,
)
self.assertEqual("duplicate", second.status)
self.assertFalse(second.duplicate_same_company)
# The returned batch id is the uploader's own duplicate batch, never
# the other company's original batch.
self.assertNotEqual(first.batch_id, second.batch_id)
self.assertNotEqual(second.batch_id, first.batch_id)
own = self.connection.execute(
"SELECT company_id FROM import_batches WHERE id = ?", (second.batch_id,)
).fetchone()
self.assertEqual(company_b, own["company_id"])
def test_repeated_upload_under_a_different_filename_is_still_duplicate(self) -> None:
first = self.import_sample()
second = import_statement(
self.connection,
self.storage,
"改名后的流水.xls",
SAMPLE_FILE.read_bytes(),
)
self.assertEqual("duplicate", second.status)
self.assertEqual(first.batch_id, second.batch_id)
files = self.connection.execute("SELECT COUNT(*) AS n FROM source_files").fetchone()
self.assertEqual(1, files["n"])
def test_immutability_triggers_block_updates_and_deletes(self) -> None:
self.import_sample()
for statement in (
"UPDATE source_rows SET income = '0'",
"DELETE FROM source_rows",
"UPDATE source_files SET sha256 = 'x'",
"DELETE FROM source_files",
"UPDATE sheet_batches SET bank_name = 'x'",
"DELETE FROM sheet_batches",
):
with self.subTest(statement=statement):
with self.assertRaises(sqlite3.IntegrityError):
self.connection.execute(statement)
self.connection.rollback()
def test_row_level_unique_constraint_blocks_duplicate_rows(self) -> None:
self.import_sample()
row = self.connection.execute(
"SELECT sheet_batch_id, source_row FROM source_rows LIMIT 1"
).fetchone()
with self.assertRaises(sqlite3.IntegrityError):
self.connection.execute(
"""
INSERT INTO source_rows (
sheet_batch_id, source_row, transaction_at, income, expense, created_at
) VALUES (?, ?, '2026-01-01T00:00:00', '1', '0', '2026-01-01T00:00:00Z')
""",
(row["sheet_batch_id"], row["source_row"]),
)
self.connection.rollback()
class FailedImportTests(PersistenceTestCase):
def _unknown_template_bytes(self) -> bytes:
workbook = Workbook()
sheet = workbook.active
sheet.title = "流水"
sheet.append(["日期", "金额", "备注"])
sheet.append(["2026-01-01", "100.00", "测试"])
target = Path(self.temp_dir.name) / "unknown.xlsx"
workbook.save(target)
return target.read_bytes()
def test_parse_failure_keeps_exception_batch_without_rows(self) -> None:
result = import_statement(
self.connection, self.storage, "未知银行.xlsx", self._unknown_template_bytes()
)
self.assertEqual("exception", result.status)
self.assertIsNotNone(result.message)
self.assertIn("未知银行.xlsx", result.message)
self.assertNotIn(result.sha256, result.message)
batch = self.connection.execute(
"SELECT status FROM import_batches WHERE id = ?", (result.batch_id,)
).fetchone()
self.assertEqual("exception", batch["status"])
exceptions = self.connection.execute(
"SELECT * FROM import_exceptions WHERE import_batch_id = ?",
(result.batch_id,),
).fetchall()
self.assertEqual(1, len(exceptions))
self.assertEqual("parse", exceptions[0]["stage"])
rows = self.connection.execute("SELECT COUNT(*) AS n FROM source_rows").fetchone()
self.assertEqual(0, rows["n"])
# The source file is still preserved as evidence for later diagnosis.
files = self.connection.execute("SELECT COUNT(*) AS n FROM source_files").fetchone()
self.assertEqual(1, files["n"])
def test_reupload_after_failure_is_tracked_as_duplicate(self) -> None:
content = self._unknown_template_bytes()
first = import_statement(self.connection, self.storage, "未知银行.xlsx", content)
second = import_statement(self.connection, self.storage, "未知银行.xlsx", content)
self.assertEqual("exception", first.status)
self.assertEqual("duplicate", second.status)
self.assertEqual(first.batch_id, second.batch_id)
if __name__ == "__main__":
unittest.main()