test(order): verify rejected XLSX imports preserve scoped database facts
This commit is contained in:
@@ -2,7 +2,9 @@
|
||||
import hashlib
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
@@ -74,6 +76,63 @@ def assert_rejection(body, title):
|
||||
and row.get('errorMessage') == title + '不存在、已停用或不属于当前客户', 'Not the precise customer-address rejection')
|
||||
|
||||
|
||||
SNAPSHOT_TABLES = ['ord_order', 'ord_order_cargo', 'ord_order_party', 'ord_order_internal_note',
|
||||
'ord_order_status_history', 'ord_idempotency_record', 'ord_outbox_event']
|
||||
|
||||
|
||||
def snapshot_sql(columns, identities):
|
||||
require(len(identities) == 2 and len(set(identities)) == 2
|
||||
and all(re.fullmatch(r'[1-9][0-9]{0,19}', x) for x in identities), 'Invalid customer scope')
|
||||
queries = ['SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;',
|
||||
'START TRANSACTION WITH CONSISTENT SNAPSHOT, READ ONLY;']
|
||||
for table in SNAPSHOT_TABLES:
|
||||
names = columns.get(table, [])
|
||||
require(names and len(names) == len(set(names)) and 'id' in names and 'tenant_id' in names
|
||||
and all(re.fullmatch(r'[a-z][a-z0-9_]*', n) for n in names), 'Incomplete snapshot schema')
|
||||
fields = ','.join('CAST(`' + n + '` AS CHAR)' for n in names)
|
||||
where = 'tenant_id IN (' + ','.join(identities) + ')'
|
||||
queries.append(f"SELECT JSON_OBJECT('table','{table}','hash',SHA2(CAST(JSON_ARRAY({fields}) AS CHAR),256)) FROM `{table}` WHERE {where} ORDER BY id;")
|
||||
queries.append(f"SELECT JSON_OBJECT('table','{table}','count',COUNT(*)) FROM `{table}` WHERE {where};")
|
||||
return '\n'.join(queries + ['COMMIT;'])
|
||||
|
||||
|
||||
def snapshot_summary(rows):
|
||||
hashes = {t: [] for t in SNAPSHOT_TABLES}
|
||||
counts = {}
|
||||
for row in rows:
|
||||
table = row.get('table')
|
||||
require(table in hashes, 'Unexpected snapshot table')
|
||||
if 'count' in row:
|
||||
require(table not in counts and isinstance(row['count'], int) and row['count'] >= 0, 'Invalid snapshot count')
|
||||
counts[table] = row['count']
|
||||
else:
|
||||
require(re.fullmatch(r'[0-9a-f]{64}', row.get('hash', '')), 'Invalid row digest')
|
||||
hashes[table].append(row['hash'])
|
||||
require(len(counts) == len(hashes) and all(len(hashes[t]) == counts[t] for t in hashes), 'Truncated snapshot')
|
||||
return {t: {'count': counts[t], 'sha256': hashlib.sha256('\n'.join(hashes[t]).encode()).hexdigest()}
|
||||
for t in SNAPSHOT_TABLES}
|
||||
|
||||
|
||||
def database_snapshot(config, identities):
|
||||
def query(sql):
|
||||
# SQL is sent via stdin; credentials are inherited only by this child process.
|
||||
result = subprocess.run(['mysql', '--protocol=TCP', '-h', config['host'], '-P', config['port'],
|
||||
'-u', config['username'], '--batch', '--raw', '--skip-column-names',
|
||||
'--default-character-set=utf8mb4', '--database=baiyue_order'],
|
||||
input=sql, text=True, encoding='utf-8', capture_output=True, timeout=30,
|
||||
env={**os.environ, 'MYSQL_PWD': config['password']},
|
||||
creationflags=subprocess.CREATE_NO_WINDOW if os.name == 'nt' else 0)
|
||||
require(result.returncode == 0, 'Read-only TEST database query failed')
|
||||
return [json.loads(line) for line in result.stdout.splitlines() if line.strip()]
|
||||
selected = ','.join("'" + t + "'" for t in SNAPSHOT_TABLES)
|
||||
metadata = query("SELECT JSON_OBJECT('table',TABLE_NAME,'column',COLUMN_NAME) FROM information_schema.COLUMNS "
|
||||
"WHERE TABLE_SCHEMA='baiyue_order' AND TABLE_NAME IN (" + selected + ") ORDER BY TABLE_NAME,ORDINAL_POSITION;")
|
||||
columns = {}
|
||||
for row in metadata:
|
||||
columns.setdefault(row['table'], []).append(row['column'])
|
||||
return snapshot_summary(query(snapshot_sql(columns, identities)))
|
||||
|
||||
|
||||
def main(config):
|
||||
require(config.get('environment') == 'TEST' and re.fullmatch(r'[A-Za-z0-9_-]{1,60}', config.get('runId', '')), 'Explicit TEST run required')
|
||||
directory = Path(__file__).resolve().parent.parent / 'reports' / ('client-xlsx-address-' + config['runId'])
|
||||
@@ -155,6 +214,9 @@ def main(config):
|
||||
shared = [x for x in options(source, title) if x in options(target, title)]
|
||||
require(shared, 'No shared valid master-data option')
|
||||
common[title] = shared[0]
|
||||
if config.get('database'):
|
||||
report['databaseBefore'] = database_snapshot(config['database'], identities)
|
||||
save()
|
||||
for title in ['取货地址', '私人目的仓地址']:
|
||||
foreign = [x for x in options(source, title) if x not in options(target, title)]
|
||||
require(foreign, 'No distinct foreign address option')
|
||||
@@ -170,10 +232,18 @@ def main(config):
|
||||
result = request('control', 'POST', '/merchant/v1/orders/first-leg/batch-imports?mode=PARTIAL', payload,
|
||||
key='XADDR-' + config['runId'] + '-' + str(len(report['checks'])))
|
||||
assert_rejection(result, title)
|
||||
if config.get('database'):
|
||||
after = database_snapshot(config['database'], identities)
|
||||
report.setdefault('databaseAfterEachImport', []).append({'field': title, 'tables': after})
|
||||
save()
|
||||
require(after == report['databaseBefore'], 'Order-domain database changed after rejected XLSX import')
|
||||
report['checks'].append({'field': title, 'passed': True, 'foreignOptionSha256': hashlib.sha256(foreign[0].encode()).hexdigest()})
|
||||
save()
|
||||
report['success'] = True
|
||||
report['limitation'] = 'XLSX parser rejects foreign customer template choices; source/error files retained; no SQL zero-drift or browser claim'
|
||||
report['databaseVerified'] = bool(config.get('database'))
|
||||
report['limitation'] = ('Complete rows in seven Order tables for both customers, including logically deleted rows; '
|
||||
'file/audit/auth writes are outside this unchanged-order-domain assertion; no browser claim'
|
||||
if config.get('database') else 'XLSX parser rejects foreign customer template choices; source/error files retained; no SQL zero-drift or browser claim')
|
||||
except Exception as error:
|
||||
report['failure'] = str(error) if isinstance(error, AssertionError) else 'Transport/setup/format failure; no retry'
|
||||
finally:
|
||||
|
||||
Reference in New Issue
Block a user