数据:增加测试批次模型与迁移契约
This commit is contained in:
@@ -1,11 +1,32 @@
|
||||
import unittest
|
||||
|
||||
from sqlalchemy import CHAR, BigInteger, CheckConstraint, DateTime, Integer, String, Table, UniqueConstraint
|
||||
from sqlalchemy import CHAR, BigInteger, CheckConstraint, DateTime, Integer, String, Table, Text, UniqueConstraint
|
||||
from sqlalchemy.dialects import mysql, postgresql
|
||||
from sqlalchemy.schema import CreateColumn
|
||||
from sqlalchemy.schema import CreateColumn, CreateTable
|
||||
|
||||
from config.database import Base
|
||||
from module_business.entity.do import master_do, operation_do
|
||||
from module_admin.entity.do import user_do
|
||||
from module_business.entity.do import master_do, operation_do, test_batch_do
|
||||
|
||||
EXPECTED_TEST_BATCH_TABLES = {'biz_test_batch', 'biz_test_batch_user'}
|
||||
TEST_BATCH_OWNED_TABLES = {
|
||||
'biz_strain',
|
||||
'biz_customer',
|
||||
'biz_customer_site',
|
||||
'biz_production_line',
|
||||
'biz_warehouse',
|
||||
'biz_pallet',
|
||||
'biz_inbound_order',
|
||||
'biz_inbound_item',
|
||||
'biz_filling_record',
|
||||
'biz_shipment_order',
|
||||
'biz_shipment_item',
|
||||
'biz_usage_record',
|
||||
'biz_return_order',
|
||||
'biz_return_item',
|
||||
'biz_pallet_inventory',
|
||||
'biz_pallet_event',
|
||||
}
|
||||
|
||||
EXPECTED_COLUMNS = {
|
||||
'biz_strain': {
|
||||
@@ -892,6 +913,136 @@ EXPECTED_UNIQUE_SETS = {
|
||||
},
|
||||
}
|
||||
|
||||
for table_name in TEST_BATCH_OWNED_TABLES:
|
||||
EXPECTED_COLUMNS[table_name].add('test_batch_id')
|
||||
EXPECTED_COLUMN_TYPES[table_name]['test_batch_id'] = (BigInteger, None)
|
||||
EXPECTED_NULLABILITY[table_name]['test_batch_id'] = True
|
||||
EXPECTED_FOREIGN_KEYS[table_name].add(('test_batch_id', 'biz_test_batch.batch_id'))
|
||||
|
||||
EXPECTED_COLUMNS.update(
|
||||
{
|
||||
'biz_test_batch': {
|
||||
'batch_id',
|
||||
'batch_code',
|
||||
'batch_name',
|
||||
'status',
|
||||
'create_by',
|
||||
'create_time',
|
||||
'update_by',
|
||||
'update_time',
|
||||
'closed_by',
|
||||
'closed_at',
|
||||
'purged_by',
|
||||
'purged_at',
|
||||
'purge_summary',
|
||||
'preview_action',
|
||||
'preview_token_hash',
|
||||
'preview_scope_hash',
|
||||
'preview_expires_at',
|
||||
'remark',
|
||||
},
|
||||
'biz_test_batch_user': {'batch_id', 'user_id', 'create_by', 'create_time'},
|
||||
}
|
||||
)
|
||||
EXPECTED_COLUMN_TYPES.update(
|
||||
{
|
||||
'biz_test_batch': {
|
||||
'batch_id': (BigInteger, None),
|
||||
'batch_code': (String, 64),
|
||||
'batch_name': (String, 100),
|
||||
'status': (String, 16),
|
||||
'create_by': (String, 64),
|
||||
'create_time': (DateTime, None),
|
||||
'update_by': (String, 64),
|
||||
'update_time': (DateTime, None),
|
||||
'closed_by': (BigInteger, None),
|
||||
'closed_at': (DateTime, None),
|
||||
'purged_by': (BigInteger, None),
|
||||
'purged_at': (DateTime, None),
|
||||
'purge_summary': (Text, None),
|
||||
'preview_action': (String, 16),
|
||||
'preview_token_hash': (CHAR, 64),
|
||||
'preview_scope_hash': (CHAR, 64),
|
||||
'preview_expires_at': (DateTime, None),
|
||||
'remark': (String, 500),
|
||||
},
|
||||
'biz_test_batch_user': {
|
||||
'batch_id': (BigInteger, None),
|
||||
'user_id': (BigInteger, None),
|
||||
'create_by': (String, 64),
|
||||
'create_time': (DateTime, None),
|
||||
},
|
||||
}
|
||||
)
|
||||
EXPECTED_NULLABILITY.update(
|
||||
{
|
||||
'biz_test_batch': {
|
||||
'batch_id': False,
|
||||
'batch_code': False,
|
||||
'batch_name': False,
|
||||
'status': False,
|
||||
'create_by': False,
|
||||
'create_time': False,
|
||||
'update_by': False,
|
||||
'update_time': False,
|
||||
'closed_by': True,
|
||||
'closed_at': True,
|
||||
'purged_by': True,
|
||||
'purged_at': True,
|
||||
'purge_summary': True,
|
||||
'preview_action': True,
|
||||
'preview_token_hash': True,
|
||||
'preview_scope_hash': True,
|
||||
'preview_expires_at': True,
|
||||
'remark': True,
|
||||
},
|
||||
'biz_test_batch_user': {
|
||||
'batch_id': False,
|
||||
'user_id': False,
|
||||
'create_by': False,
|
||||
'create_time': False,
|
||||
},
|
||||
}
|
||||
)
|
||||
EXPECTED_CALLABLE_DATETIME_DEFAULTS.update(
|
||||
{
|
||||
('biz_test_batch', 'create_time'),
|
||||
('biz_test_batch', 'update_time'),
|
||||
('biz_test_batch_user', 'create_time'),
|
||||
}
|
||||
)
|
||||
EXPECTED_EMPTY_AUDIT_SERVER_DEFAULTS.update(
|
||||
{
|
||||
('biz_test_batch', 'create_by'),
|
||||
('biz_test_batch', 'update_by'),
|
||||
('biz_test_batch_user', 'create_by'),
|
||||
}
|
||||
)
|
||||
EXPECTED_PRIMARY_KEYS.update(
|
||||
{
|
||||
'biz_test_batch': ('batch_id',),
|
||||
'biz_test_batch_user': ('batch_id', 'user_id'),
|
||||
}
|
||||
)
|
||||
EXPECTED_FOREIGN_KEYS.update(
|
||||
{
|
||||
'biz_test_batch': {
|
||||
('closed_by', 'sys_user.user_id'),
|
||||
('purged_by', 'sys_user.user_id'),
|
||||
},
|
||||
'biz_test_batch_user': {
|
||||
('batch_id', 'biz_test_batch.batch_id'),
|
||||
('user_id', 'sys_user.user_id'),
|
||||
},
|
||||
}
|
||||
)
|
||||
EXPECTED_UNIQUE_SETS.update(
|
||||
{
|
||||
'biz_test_batch': {frozenset({'batch_code'})},
|
||||
'biz_test_batch_user': set(),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _unique_column_sets(table: Table) -> set[frozenset[str]]:
|
||||
return {
|
||||
@@ -910,6 +1061,16 @@ def _normalized_check_sql(table: Table) -> dict[str, str]:
|
||||
|
||||
|
||||
class BusinessModelContractTest(unittest.TestCase):
|
||||
def test_batch_related_tables_compile_for_mysql_and_postgresql(self) -> None:
|
||||
self.assertIs(Base.metadata.tables['sys_user'], user_do.SysUser.__table__)
|
||||
batch_tables = EXPECTED_TEST_BATCH_TABLES | TEST_BATCH_OWNED_TABLES
|
||||
self.assertEqual(18, len(batch_tables))
|
||||
for table_name in batch_tables:
|
||||
for dialect_name, dialect in (('mysql', mysql.dialect()), ('postgresql', postgresql.dialect())):
|
||||
with self.subTest(table_name=table_name, dialect=dialect_name):
|
||||
compiled = str(CreateTable(Base.metadata.tables[table_name]).compile(dialect=dialect))
|
||||
self.assertIn(table_name, compiled)
|
||||
|
||||
def test_exact_business_tables_are_registered(self) -> None:
|
||||
expected = {
|
||||
'biz_strain',
|
||||
@@ -930,7 +1091,7 @@ class BusinessModelContractTest(unittest.TestCase):
|
||||
'biz_return_item',
|
||||
'biz_pallet_inventory',
|
||||
'biz_pallet_event',
|
||||
}
|
||||
} | EXPECTED_TEST_BATCH_TABLES
|
||||
actual = {name for name in Base.metadata.tables if name.startswith('biz_')}
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
@@ -954,6 +1115,8 @@ class BusinessModelContractTest(unittest.TestCase):
|
||||
operation_do.BizReturnItem: 'biz_return_item',
|
||||
operation_do.BizPalletInventory: 'biz_pallet_inventory',
|
||||
operation_do.BizPalletEvent: 'biz_pallet_event',
|
||||
test_batch_do.BizTestBatch: 'biz_test_batch',
|
||||
test_batch_do.BizTestBatchUser: 'biz_test_batch_user',
|
||||
}
|
||||
self.assertEqual(set(EXPECTED_COLUMNS), set(models.values()))
|
||||
for model, table_name in models.items():
|
||||
@@ -1032,6 +1195,7 @@ class BusinessModelContractTest(unittest.TestCase):
|
||||
_, separator, compiled_default = compiled_column.partition(' DEFAULT ')
|
||||
if separator:
|
||||
compiled_default = compiled_default.split(' COMMENT ', maxsplit=1)[0].strip()
|
||||
compiled_default = compiled_default.removesuffix(' NOT NULL')
|
||||
if compiled_default != "''":
|
||||
violations.append((dialect_name, table_name, column_name, compiled_default))
|
||||
self.assertEqual([], violations)
|
||||
@@ -1058,6 +1222,26 @@ class BusinessModelContractTest(unittest.TestCase):
|
||||
with self.subTest(table_name=table_name):
|
||||
self.assertEqual(expected_unique_sets, _unique_column_sets(Base.metadata.tables[table_name]))
|
||||
|
||||
def test_all_owned_models_have_nullable_indexed_batch_fk(self) -> None:
|
||||
for table_name in TEST_BATCH_OWNED_TABLES:
|
||||
table = Base.metadata.tables[table_name]
|
||||
column = table.c.test_batch_id
|
||||
self.assertTrue(column.nullable, table_name)
|
||||
self.assertTrue(column.index, table_name)
|
||||
fk = next(iter(column.foreign_keys))
|
||||
self.assertEqual('biz_test_batch.batch_id', fk.target_fullname)
|
||||
self.assertIsNone(fk.ondelete)
|
||||
|
||||
def test_test_batch_model_defaults_and_comments_match_contract(self) -> None:
|
||||
table = Base.metadata.tables['biz_test_batch']
|
||||
self.assertEqual('测试数据批次', table.comment)
|
||||
self.assertEqual('ACTIVE', str(table.c.status.server_default.arg))
|
||||
for table_name in TEST_BATCH_OWNED_TABLES:
|
||||
self.assertEqual(
|
||||
'测试数据批次ID;NULL表示正式数据',
|
||||
Base.metadata.tables[table_name].c.test_batch_id.comment,
|
||||
)
|
||||
|
||||
def test_pallet_identifiers_are_unique(self) -> None:
|
||||
table = Base.metadata.tables['biz_pallet']
|
||||
unique_columns = {column.name for column in table.columns if column.unique}
|
||||
@@ -1065,6 +1249,10 @@ class BusinessModelContractTest(unittest.TestCase):
|
||||
|
||||
def test_checks_lock_quantities_states_active_slot_and_location(self) -> None:
|
||||
expected_checks = {
|
||||
'biz_test_batch': {
|
||||
'ck_biz_test_batch_status': "statusIN('ACTIVE','CLOSED','PURGED')",
|
||||
'ck_biz_test_batch_preview_action': "preview_actionISNULLORpreview_actionIN('ADOPT','PURGE')",
|
||||
},
|
||||
'biz_strain': {'ck_biz_strain_status': "statusIN('0','1')"},
|
||||
'biz_warehouse': {'ck_biz_warehouse_status': "statusIN('0','1')"},
|
||||
'biz_customer': {'ck_biz_customer_status': "statusIN('0','1')"},
|
||||
|
||||
@@ -9,11 +9,33 @@ INITIALIZERS = ('shierfeng-fastapi.sql', 'shierfeng-fastapi-pg.sql')
|
||||
PATCH_PATH = SQL_ROOT / 'patches/20260724_pallet_workflow_mysql.sql'
|
||||
MENU_PROMOTION_PATCH_PATH = SQL_ROOT / 'patches/20260727_promote_business_menus_mysql.sql'
|
||||
BUSINESS_MENU_ICON_PATCH_PATH = SQL_ROOT / 'patches/20260728_add_business_menu_icons_mysql.sql'
|
||||
TEST_BATCH_MYSQL_PATCH_PATH = SQL_ROOT / 'patches/20260922_test_data_batch_mysql.sql'
|
||||
TEST_BATCH_PG_PATCH_PATH = SQL_ROOT / 'patches/20260922_test_data_batch_pg.sql'
|
||||
SYS_MENU_VALUE_COUNT = 20
|
||||
SYS_MENU_PERMS_INDEX = 13
|
||||
SYS_MENU_ICON_INDEX = 14
|
||||
REMOVED_AI_DICT_CODES = set(range(33, 70))
|
||||
|
||||
EXPECTED_TEST_BATCH_TABLES = {'biz_test_batch', 'biz_test_batch_user'}
|
||||
TEST_BATCH_OWNED_TABLES = {
|
||||
'biz_strain',
|
||||
'biz_customer',
|
||||
'biz_customer_site',
|
||||
'biz_production_line',
|
||||
'biz_warehouse',
|
||||
'biz_pallet',
|
||||
'biz_inbound_order',
|
||||
'biz_inbound_item',
|
||||
'biz_filling_record',
|
||||
'biz_shipment_order',
|
||||
'biz_shipment_item',
|
||||
'biz_usage_record',
|
||||
'biz_return_order',
|
||||
'biz_return_item',
|
||||
'biz_pallet_inventory',
|
||||
'biz_pallet_event',
|
||||
}
|
||||
|
||||
EXPECTED_TABLES = {
|
||||
'biz_strain',
|
||||
'biz_warehouse',
|
||||
@@ -346,6 +368,83 @@ EXPECTED_CHECKS = {
|
||||
},
|
||||
}
|
||||
|
||||
LEGACY_EXPECTED_TABLES = set(EXPECTED_TABLES)
|
||||
LEGACY_EXPECTED_COLUMNS = {table: set(columns) for table, columns in EXPECTED_COLUMNS.items()}
|
||||
LEGACY_EXPECTED_PRIMARY_KEYS = dict(EXPECTED_PRIMARY_KEYS)
|
||||
LEGACY_EXPECTED_UNIQUE_KEYS = {table: set(keys) for table, keys in EXPECTED_UNIQUE_KEYS.items()}
|
||||
LEGACY_EXPECTED_FOREIGN_KEYS = {table: set(keys) for table, keys in EXPECTED_FOREIGN_KEYS.items()}
|
||||
LEGACY_EXPECTED_CHECKS = {table: set(checks) for table, checks in EXPECTED_CHECKS.items()}
|
||||
|
||||
EXPECTED_TABLES |= EXPECTED_TEST_BATCH_TABLES
|
||||
for table_name in TEST_BATCH_OWNED_TABLES:
|
||||
EXPECTED_COLUMNS[table_name].add('test_batch_id')
|
||||
EXPECTED_FOREIGN_KEYS[table_name].add(('test_batch_id', 'biz_test_batch.batch_id'))
|
||||
|
||||
EXPECTED_COLUMNS.update(
|
||||
{
|
||||
'biz_test_batch': {
|
||||
'batch_id',
|
||||
'batch_code',
|
||||
'batch_name',
|
||||
'status',
|
||||
'create_by',
|
||||
'create_time',
|
||||
'update_by',
|
||||
'update_time',
|
||||
'closed_by',
|
||||
'closed_at',
|
||||
'purged_by',
|
||||
'purged_at',
|
||||
'purge_summary',
|
||||
'preview_action',
|
||||
'preview_token_hash',
|
||||
'preview_scope_hash',
|
||||
'preview_expires_at',
|
||||
'remark',
|
||||
},
|
||||
'biz_test_batch_user': {'batch_id', 'user_id', 'create_by', 'create_time'},
|
||||
}
|
||||
)
|
||||
EXPECTED_DATETIME_COLUMNS.update(
|
||||
{
|
||||
'biz_test_batch': {'create_time', 'update_time', 'closed_at', 'purged_at', 'preview_expires_at'},
|
||||
'biz_test_batch_user': {'create_time'},
|
||||
}
|
||||
)
|
||||
EXPECTED_PRIMARY_KEYS.update(
|
||||
{
|
||||
'biz_test_batch': ('batch_id',),
|
||||
'biz_test_batch_user': ('batch_id', 'user_id'),
|
||||
}
|
||||
)
|
||||
EXPECTED_UNIQUE_KEYS.update(
|
||||
{
|
||||
'biz_test_batch': {('batch_code',)},
|
||||
'biz_test_batch_user': set(),
|
||||
}
|
||||
)
|
||||
EXPECTED_FOREIGN_KEYS.update(
|
||||
{
|
||||
'biz_test_batch': {
|
||||
('closed_by', 'sys_user.user_id'),
|
||||
('purged_by', 'sys_user.user_id'),
|
||||
},
|
||||
'biz_test_batch_user': {
|
||||
('batch_id', 'biz_test_batch.batch_id'),
|
||||
('user_id', 'sys_user.user_id'),
|
||||
},
|
||||
}
|
||||
)
|
||||
EXPECTED_CHECKS.update(
|
||||
{
|
||||
'biz_test_batch': {
|
||||
"statusin('active','closed','purged')",
|
||||
"preview_actionisnullorpreview_actionin('adopt','purge')",
|
||||
},
|
||||
'biz_test_batch_user': set(),
|
||||
}
|
||||
)
|
||||
|
||||
EXPECTED_PAGES = {
|
||||
502: ('菌种资料', 121, 1, 'strain', 'master/strain/index', 'master:strain:list'),
|
||||
503: ('客户资料', 121, 2, 'customer', 'master/customer/index', 'master:customer:list'),
|
||||
@@ -873,6 +972,74 @@ def _is_postgres_naive_timestamp_with_default_precision(definition: str) -> bool
|
||||
)
|
||||
|
||||
|
||||
def _assert_test_batch_sql_contract(
|
||||
test_case: unittest.TestCase,
|
||||
sql: str,
|
||||
*,
|
||||
dialect: str,
|
||||
is_patch: bool,
|
||||
) -> None:
|
||||
clean_sql = _strip_sql_comments(sql).replace('`', '')
|
||||
normalized = re.sub(r'\s+', ' ', clean_sql).strip().lower()
|
||||
tables = _create_table_bodies(sql)
|
||||
|
||||
test_case.assertNotRegex(normalized, r'\bon\s+delete\s+cascade\b')
|
||||
if is_patch:
|
||||
test_case.assertNotRegex(normalized, r'\bupdate\b')
|
||||
|
||||
for table_name in EXPECTED_TEST_BATCH_TABLES:
|
||||
test_case.assertIn(table_name, tables)
|
||||
columns, primary_key, unique_keys, foreign_keys, checks = _table_contract(tables[table_name])
|
||||
test_case.assertEqual(EXPECTED_COLUMNS[table_name], columns, table_name)
|
||||
test_case.assertEqual(EXPECTED_PRIMARY_KEYS[table_name], primary_key, table_name)
|
||||
test_case.assertEqual(EXPECTED_UNIQUE_KEYS[table_name], unique_keys, table_name)
|
||||
test_case.assertEqual(EXPECTED_FOREIGN_KEYS[table_name], foreign_keys, table_name)
|
||||
test_case.assertEqual(EXPECTED_CHECKS[table_name], checks, table_name)
|
||||
|
||||
create_batch = normalized.index('create table biz_test_batch (')
|
||||
create_batch_user = normalized.index('create table biz_test_batch_user (')
|
||||
owned_marker = 'alter table' if is_patch else 'create table'
|
||||
first_owned = min(normalized.index(f'{owned_marker} {table_name} ') for table_name in TEST_BATCH_OWNED_TABLES)
|
||||
test_case.assertLess(create_batch, create_batch_user)
|
||||
test_case.assertLess(create_batch_user, first_owned)
|
||||
|
||||
for table_name in TEST_BATCH_OWNED_TABLES:
|
||||
with test_case.subTest(dialect=dialect, patch=is_patch, table=table_name):
|
||||
if is_patch:
|
||||
test_case.assertRegex(
|
||||
normalized,
|
||||
rf'\balter table {table_name}\s+add column test_batch_id bigint null\b',
|
||||
)
|
||||
else:
|
||||
definitions = _column_definitions(tables[table_name])
|
||||
test_case.assertIn('test_batch_id', definitions)
|
||||
test_case.assertRegex(definitions['test_batch_id'], r'\bbigint\b')
|
||||
test_case.assertNotRegex(definitions['test_batch_id'], r'\bnot\s+null\b')
|
||||
|
||||
index_name = f'idx_{table_name}_test_batch_id'
|
||||
if dialect == 'mysql':
|
||||
index_pattern = rf'\b(?:add\s+)?(?:index|key)\s+{index_name}\s*\(\s*test_batch_id\s*\)'
|
||||
test_case.assertRegex(normalized, index_pattern)
|
||||
test_case.assertIn("comment '测试数据批次id;null表示正式数据'", normalized)
|
||||
else:
|
||||
index_pattern = (
|
||||
rf'\bcreate\s+index\s+{index_name}\s+on\s+{table_name}\s*\(\s*test_batch_id\s*\)'
|
||||
)
|
||||
test_case.assertRegex(normalized, index_pattern)
|
||||
test_case.assertRegex(
|
||||
normalized,
|
||||
rf"comment\s+on\s+column\s+{table_name}\.test_batch_id\s+is\s+"
|
||||
r"'测试数据批次id;null表示正式数据'",
|
||||
)
|
||||
|
||||
constraint_name = f'fk_{table_name}_test_batch_id'
|
||||
test_case.assertRegex(
|
||||
normalized,
|
||||
rf'\bconstraint\s+{constraint_name}\s+foreign\s+key\s*\(\s*test_batch_id\s*\)\s*'
|
||||
r'references\s+biz_test_batch\s*\(\s*batch_id\s*\)',
|
||||
)
|
||||
|
||||
|
||||
def _menu_rows(sql: str, menu_ids: set[int]) -> dict[int, list]:
|
||||
rows = _insert_rows(sql, 'sys_menu')
|
||||
selected = [row for row in rows if row and str(row[0]).isdigit() and int(row[0]) in menu_ids]
|
||||
@@ -1064,9 +1231,7 @@ def _assert_business_menu_icon_guard_primary_key(test_case: unittest.TestCase, s
|
||||
|
||||
def _assert_business_menu_icon_guard_structure(test_case: unittest.TestCase, sql: str) -> tuple[str, str]:
|
||||
normalized = [_normalized_sql_statement(statement) for statement in _sql_statements(sql)]
|
||||
guards = [
|
||||
statement for statement in normalized if statement.startswith('insert into business_menu_icon_guard ')
|
||||
]
|
||||
guards = [statement for statement in normalized if statement.startswith('insert into business_menu_icon_guard ')]
|
||||
test_case.assertEqual(2, len(guards))
|
||||
pre_guard, post_guard = guards
|
||||
identity_fields = (
|
||||
@@ -1128,6 +1293,23 @@ def _assert_business_menu_icon_guard_structure(test_case: unittest.TestCase, sql
|
||||
|
||||
|
||||
class SqlContractTest(unittest.TestCase):
|
||||
def test_test_batch_initializers_and_patches_match_contract(self) -> None:
|
||||
scripts = (
|
||||
(SQL_ROOT / 'shierfeng-fastapi.sql', 'mysql', False),
|
||||
(SQL_ROOT / 'shierfeng-fastapi-pg.sql', 'postgresql', False),
|
||||
(TEST_BATCH_MYSQL_PATCH_PATH, 'mysql', True),
|
||||
(TEST_BATCH_PG_PATCH_PATH, 'postgresql', True),
|
||||
)
|
||||
for path, dialect, is_patch in scripts:
|
||||
with self.subTest(path=path.name):
|
||||
self.assertTrue(path.is_file(), f'missing SQL: {path}')
|
||||
_assert_test_batch_sql_contract(
|
||||
self,
|
||||
path.read_text(encoding='utf-8'),
|
||||
dialect=dialect,
|
||||
is_patch=is_patch,
|
||||
)
|
||||
|
||||
def test_initializers_define_exact_business_table_set(self) -> None:
|
||||
for filename in INITIALIZERS:
|
||||
with self.subTest(filename=filename):
|
||||
@@ -1149,7 +1331,7 @@ class SqlContractTest(unittest.TestCase):
|
||||
self.assertEqual(EXPECTED_CHECKS[table], checks)
|
||||
|
||||
def test_postgres_business_datetimes_preserve_default_microsecond_precision(self) -> None:
|
||||
self.assertEqual(34, sum(len(columns) for columns in EXPECTED_DATETIME_COLUMNS.values()))
|
||||
self.assertEqual(40, sum(len(columns) for columns in EXPECTED_DATETIME_COLUMNS.values()))
|
||||
tables = _create_table_bodies((SQL_ROOT / 'shierfeng-fastapi-pg.sql').read_text(encoding='utf-8'))
|
||||
invalid_columns = []
|
||||
for table, expected_columns in EXPECTED_DATETIME_COLUMNS.items():
|
||||
@@ -1290,9 +1472,7 @@ class SqlContractTest(unittest.TestCase):
|
||||
self.assertEqual(1, normalized.count('start transaction'))
|
||||
self.assertEqual(1, normalized.count('commit'))
|
||||
transaction_body = normalized[start_index + 1 : commit_index]
|
||||
lock_statement = (
|
||||
'select menu_id from sys_menu where menu_id in (121, 122, 123) order by menu_id for update'
|
||||
)
|
||||
lock_statement = 'select menu_id from sys_menu where menu_id in (121, 122, 123) order by menu_id for update'
|
||||
expected_update = (
|
||||
"update sys_menu set icon = case menu_id when 121 then 'documentation' when 122 then 'build' "
|
||||
"when 123 then 'chart' else icon end where menu_id in (121, 122, 123)"
|
||||
@@ -1476,6 +1656,8 @@ class SqlContractTest(unittest.TestCase):
|
||||
'biz_customer',
|
||||
'biz_warehouse',
|
||||
'biz_strain',
|
||||
'biz_test_batch_user',
|
||||
'biz_test_batch',
|
||||
)
|
||||
for filename in INITIALIZERS:
|
||||
with self.subTest(filename=filename):
|
||||
@@ -1777,15 +1959,15 @@ class SqlContractTest(unittest.TestCase):
|
||||
self.assertLess(lower.index('ddl'), lower.index('dml'))
|
||||
self.assertEqual(18, len(re.findall(r'create\s+table\s+if\s+not\s+exists\s+`?biz_', lower)))
|
||||
tables = _create_table_bodies(raw)
|
||||
self.assertEqual(EXPECTED_TABLES, {table for table in tables if table.startswith('biz_')})
|
||||
for table in EXPECTED_TABLES:
|
||||
self.assertEqual(LEGACY_EXPECTED_TABLES, {table for table in tables if table.startswith('biz_')})
|
||||
for table in LEGACY_EXPECTED_TABLES:
|
||||
with self.subTest(table=table):
|
||||
columns, primary_key, unique_keys, foreign_keys, checks = _table_contract(tables[table])
|
||||
self.assertEqual(EXPECTED_COLUMNS[table], columns)
|
||||
self.assertEqual(EXPECTED_PRIMARY_KEYS[table], primary_key)
|
||||
self.assertEqual(EXPECTED_UNIQUE_KEYS[table], unique_keys)
|
||||
self.assertEqual(EXPECTED_FOREIGN_KEYS[table], foreign_keys)
|
||||
self.assertEqual(EXPECTED_CHECKS[table], checks)
|
||||
self.assertEqual(LEGACY_EXPECTED_COLUMNS[table], columns)
|
||||
self.assertEqual(LEGACY_EXPECTED_PRIMARY_KEYS[table], primary_key)
|
||||
self.assertEqual(LEGACY_EXPECTED_UNIQUE_KEYS[table], unique_keys)
|
||||
self.assertEqual(LEGACY_EXPECTED_FOREIGN_KEYS[table], foreign_keys)
|
||||
self.assertEqual(LEGACY_EXPECTED_CHECKS[table], checks)
|
||||
_assert_business_menu_fields(self, raw, LEGACY_PATCH_BUSINESS_MENU_FIELDS)
|
||||
_assert_business_permission_inventory(self, raw)
|
||||
patch_rows = _business_menu_rows(raw)
|
||||
@@ -1793,9 +1975,7 @@ class SqlContractTest(unittest.TestCase):
|
||||
compared_indexes = (0, 1, *range(4, 15))
|
||||
expected_legacy_rows = {
|
||||
menu_id: tuple(
|
||||
'#'
|
||||
if index == SYS_MENU_ICON_INDEX and menu_id in EXPECTED_BUSINESS_ROOT_ICONS
|
||||
else row[index]
|
||||
'#' if index == SYS_MENU_ICON_INDEX and menu_id in EXPECTED_BUSINESS_ROOT_ICONS else row[index]
|
||||
for index in compared_indexes
|
||||
)
|
||||
for menu_id, row in mysql_rows.items()
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import unittest
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import ValidationError
|
||||
from pydantic.alias_generators import to_camel
|
||||
@@ -69,6 +70,21 @@ from module_business.entity.vo.report_vo import (
|
||||
ShipmentReportModel,
|
||||
ShipmentReportQueryModel,
|
||||
)
|
||||
from module_business.entity.vo.test_batch_vo import (
|
||||
AdoptionExecuteRequestModel,
|
||||
AdoptionPreviewRequestModel,
|
||||
AdoptionRootModel,
|
||||
BatchImpactModel,
|
||||
PurgeExecuteRequestModel,
|
||||
PurgePreviewModel,
|
||||
TestBatchCreateModel,
|
||||
TestBatchDetailModel,
|
||||
TestBatchEditModel,
|
||||
TestBatchModel,
|
||||
TestBatchOptionModel,
|
||||
TestBatchPreviewModel,
|
||||
TestBatchUserAssignModel,
|
||||
)
|
||||
|
||||
MASTER_MODEL_FIELDS = {
|
||||
StrainModel: {
|
||||
@@ -549,6 +565,71 @@ REPORT_MODEL_FIELDS = {
|
||||
},
|
||||
}
|
||||
|
||||
TEST_BATCH_MODEL_FIELDS = {
|
||||
TestBatchCreateModel: {'batch_name', 'remark'},
|
||||
TestBatchEditModel: {'remark'},
|
||||
TestBatchUserAssignModel: {'user_ids'},
|
||||
TestBatchModel: {
|
||||
'batch_id',
|
||||
'batch_code',
|
||||
'batch_name',
|
||||
'status',
|
||||
'remark',
|
||||
'create_by',
|
||||
'create_time',
|
||||
'update_by',
|
||||
'update_time',
|
||||
'closed_by',
|
||||
'closed_at',
|
||||
'purged_by',
|
||||
'purged_at',
|
||||
'purge_summary',
|
||||
},
|
||||
TestBatchOptionModel: {'batch_id', 'batch_code', 'batch_name', 'status'},
|
||||
BatchImpactModel: {'table_name', 'label', 'count', 'keys'},
|
||||
TestBatchDetailModel: {
|
||||
'batch_id',
|
||||
'batch_code',
|
||||
'batch_name',
|
||||
'status',
|
||||
'remark',
|
||||
'create_by',
|
||||
'create_time',
|
||||
'update_by',
|
||||
'update_time',
|
||||
'closed_by',
|
||||
'closed_at',
|
||||
'purged_by',
|
||||
'purged_at',
|
||||
'purge_summary',
|
||||
'user_ids',
|
||||
'data_summary',
|
||||
},
|
||||
TestBatchPreviewModel: {
|
||||
'batch_id',
|
||||
'batch_name',
|
||||
'preview_token',
|
||||
'expires_at',
|
||||
'total_count',
|
||||
'impacts',
|
||||
},
|
||||
PurgePreviewModel: {
|
||||
'batch_id',
|
||||
'batch_name',
|
||||
'preview_token',
|
||||
'expires_at',
|
||||
'total_count',
|
||||
'impacts',
|
||||
'online_purge_allowed',
|
||||
'online_purge_limit',
|
||||
'blocking_reason',
|
||||
},
|
||||
AdoptionRootModel: {'resource_type', 'resource_ids'},
|
||||
AdoptionPreviewRequestModel: {'roots'},
|
||||
AdoptionExecuteRequestModel: {'roots', 'batch_name', 'preview_token'},
|
||||
PurgeExecuteRequestModel: {'batch_name', 'preview_token'},
|
||||
}
|
||||
|
||||
WRITE_MODELS = (
|
||||
StrainModel,
|
||||
DeleteStrainModel,
|
||||
@@ -761,7 +842,7 @@ PACKAGE_MODEL_PAYLOADS = {
|
||||
|
||||
class BusinessVoContractTest(unittest.TestCase):
|
||||
def test_all_models_have_exact_fields_and_camel_case_aliases(self) -> None:
|
||||
expected_models = MASTER_MODEL_FIELDS | OPERATION_MODEL_FIELDS | REPORT_MODEL_FIELDS
|
||||
expected_models = MASTER_MODEL_FIELDS | OPERATION_MODEL_FIELDS | REPORT_MODEL_FIELDS | TEST_BATCH_MODEL_FIELDS
|
||||
|
||||
for model_class, expected_fields in expected_models.items():
|
||||
with self.subTest(model=model_class.__name__):
|
||||
@@ -777,6 +858,44 @@ class BusinessVoContractTest(unittest.TestCase):
|
||||
with self.subTest(model=model_class.__name__):
|
||||
self.assertEqual('forbid', model_class.model_config.get('extra'))
|
||||
|
||||
def test_all_test_batch_models_forbid_extra_fields(self) -> None:
|
||||
for model_class in TEST_BATCH_MODEL_FIELDS:
|
||||
with self.subTest(model=model_class.__name__):
|
||||
self.assertEqual('forbid', model_class.model_config.get('extra'))
|
||||
|
||||
def test_test_batch_models_accept_snake_case_and_camel_case(self) -> None:
|
||||
camel_case = TestBatchCreateModel(batchName='验收批次', remark='camel')
|
||||
snake_case = TestBatchCreateModel(batch_name='验收批次', remark='snake')
|
||||
self.assertEqual('验收批次', camel_case.batch_name)
|
||||
self.assertEqual('验收批次', snake_case.batch_name)
|
||||
|
||||
def test_test_batch_edit_forbids_immutable_fields(self) -> None:
|
||||
for field_name, value in (('batchName', '新名称'), ('status', 'CLOSED')):
|
||||
with self.subTest(field=field_name), self.assertRaises(ValidationError):
|
||||
TestBatchEditModel.model_validate({'remark': None, field_name: value})
|
||||
|
||||
def test_test_batch_positive_ids_and_non_empty_roots_are_validated(self) -> None:
|
||||
with self.assertRaises(ValidationError):
|
||||
TestBatchUserAssignModel(userIds=[0])
|
||||
with self.assertRaises(ValidationError):
|
||||
AdoptionRootModel(resourceType='strain', resourceIds=[])
|
||||
with self.assertRaises(ValidationError):
|
||||
AdoptionPreviewRequestModel(roots=[])
|
||||
|
||||
def test_test_batch_status_rejects_unknown_value(self) -> None:
|
||||
with self.assertRaises(ValidationError) as context:
|
||||
TestBatchModel(
|
||||
batchId=1,
|
||||
batchCode='TB-1',
|
||||
batchName='验收批次',
|
||||
status='UNKNOWN',
|
||||
createBy='admin',
|
||||
createTime=datetime(2026, 9, 22),
|
||||
updateBy='admin',
|
||||
updateTime=datetime(2026, 9, 22),
|
||||
)
|
||||
self.assertIn(('status',), {tuple(error['loc']) for error in context.exception.errors()})
|
||||
|
||||
def test_page_queries_have_positive_defaults(self) -> None:
|
||||
for model_class in PAGE_QUERY_MODELS:
|
||||
with self.subTest(model=model_class.__name__):
|
||||
|
||||
Reference in New Issue
Block a user