369 lines
13 KiB
Python
369 lines
13 KiB
Python
import subprocess
|
||
from collections.abc import Callable
|
||
|
||
from cli.exit_codes import ARGUMENT_ERROR, DATABASE_ERROR, GUARD_REJECTED, RUNTIME_ERROR, SUCCESS
|
||
|
||
GEN_TEST_TABLE_NAME = 'demo_table'
|
||
GEN_CREATE_SQL = 'CREATE TABLE demo_cli_test (id bigint);'
|
||
MISSING_JOB_ID = 999999999
|
||
MISSING_GEN_TABLE_ID = 999999999
|
||
|
||
|
||
def test_job_detail_json_output_has_stable_contract(
|
||
run_cli_command: Callable[..., subprocess.CompletedProcess[str]],
|
||
parse_json_stdout: Callable[[subprocess.CompletedProcess[str]], dict],
|
||
) -> None:
|
||
list_completed = run_cli_command('job', 'list', '--env=dev', '--output=json')
|
||
list_payload = parse_json_stdout(list_completed)
|
||
job_id = MISSING_JOB_ID
|
||
if list_payload.get('ok') and isinstance(list_payload.get('items'), list) and list_payload['items']:
|
||
first_job = list_payload['items'][0]
|
||
if isinstance(first_job, dict) and isinstance(first_job.get('jobId'), int):
|
||
job_id = first_job['jobId']
|
||
|
||
completed = run_cli_command('job', 'detail', str(job_id), '--env=dev', '--output=json')
|
||
payload = parse_json_stdout(completed)
|
||
|
||
assert completed.returncode in {SUCCESS, DATABASE_ERROR, RUNTIME_ERROR}
|
||
assert isinstance(payload['ok'], bool)
|
||
if 'error' in payload:
|
||
assert isinstance(payload['message'], str)
|
||
assert isinstance(payload['error'], str)
|
||
return
|
||
|
||
if payload['ok'] is False:
|
||
assert set(payload) == {'ok', 'message', 'jobId'}
|
||
assert payload['jobId'] == job_id
|
||
return
|
||
|
||
assert set(payload) == {'ok', 'job'}
|
||
assert isinstance(payload['job'], dict)
|
||
assert payload['job']['jobId'] == job_id
|
||
assert isinstance(payload['job'].get('jobName'), str)
|
||
assert isinstance(payload['job'].get('invokeTarget'), str)
|
||
|
||
|
||
def test_job_logs_json_output_has_stable_contract(
|
||
run_cli_command: Callable[..., subprocess.CompletedProcess[str]],
|
||
parse_json_stdout: Callable[[subprocess.CompletedProcess[str]], dict],
|
||
) -> None:
|
||
completed = run_cli_command('job', 'logs', '--env=dev', '--output=json')
|
||
payload = parse_json_stdout(completed)
|
||
|
||
assert completed.returncode in {SUCCESS, DATABASE_ERROR}
|
||
assert isinstance(payload['ok'], bool)
|
||
if 'error' in payload:
|
||
assert isinstance(payload['message'], str)
|
||
assert isinstance(payload['error'], str)
|
||
return
|
||
|
||
assert set(payload) == {'ok', 'filters', 'count', 'items'}
|
||
assert isinstance(payload['filters'], dict)
|
||
assert isinstance(payload['count'], int)
|
||
assert isinstance(payload['items'], list)
|
||
if payload['items']:
|
||
first_item = payload['items'][0]
|
||
assert isinstance(first_item, dict)
|
||
assert isinstance(first_item.get('jobLogId'), int)
|
||
assert isinstance(first_item.get('jobName'), str | None)
|
||
assert isinstance(first_item.get('jobMessage'), str | None)
|
||
|
||
|
||
def test_gen_list_json_output_has_stable_contract(
|
||
run_cli_command: Callable[..., subprocess.CompletedProcess[str]],
|
||
parse_json_stdout: Callable[[subprocess.CompletedProcess[str]], dict],
|
||
) -> None:
|
||
completed = run_cli_command('gen', 'list', '--env=dev', '--output=json')
|
||
payload = parse_json_stdout(completed)
|
||
|
||
assert completed.returncode in {SUCCESS, DATABASE_ERROR}
|
||
assert payload['env'] == 'dev'
|
||
assert isinstance(payload['ok'], bool)
|
||
if 'error' in payload:
|
||
assert isinstance(payload['message'], str)
|
||
assert isinstance(payload['error'], str)
|
||
return
|
||
|
||
assert set(payload) == {'ok', 'filters', 'count', 'items', 'env'}
|
||
assert isinstance(payload['filters'], dict)
|
||
assert isinstance(payload['count'], int)
|
||
assert isinstance(payload['items'], list)
|
||
if payload['items']:
|
||
first_item = payload['items'][0]
|
||
assert isinstance(first_item, dict)
|
||
assert isinstance(first_item.get('tableId'), int)
|
||
assert isinstance(first_item.get('tableName'), str)
|
||
|
||
|
||
def test_gen_db_list_json_output_has_stable_contract(
|
||
run_cli_command: Callable[..., subprocess.CompletedProcess[str]],
|
||
parse_json_stdout: Callable[[subprocess.CompletedProcess[str]], dict],
|
||
) -> None:
|
||
completed = run_cli_command('gen', 'db-list', '--env=dev', '--output=json')
|
||
payload = parse_json_stdout(completed)
|
||
|
||
assert completed.returncode in {SUCCESS, DATABASE_ERROR}
|
||
assert payload['env'] == 'dev'
|
||
assert isinstance(payload['ok'], bool)
|
||
if 'error' in payload:
|
||
assert isinstance(payload['message'], str)
|
||
assert isinstance(payload['error'], str)
|
||
return
|
||
|
||
assert set(payload) == {'ok', 'filters', 'count', 'items', 'env'}
|
||
assert isinstance(payload['filters'], dict)
|
||
assert isinstance(payload['count'], int)
|
||
assert isinstance(payload['items'], list)
|
||
if payload['items']:
|
||
first_item = payload['items'][0]
|
||
assert isinstance(first_item, dict)
|
||
assert isinstance(first_item.get('tableName'), str)
|
||
|
||
|
||
def test_gen_detail_json_output_has_stable_contract(
|
||
run_cli_command: Callable[..., subprocess.CompletedProcess[str]],
|
||
parse_json_stdout: Callable[[subprocess.CompletedProcess[str]], dict],
|
||
) -> None:
|
||
list_completed = run_cli_command('gen', 'list', '--env=dev', '--output=json')
|
||
list_payload = parse_json_stdout(list_completed)
|
||
table_id = MISSING_GEN_TABLE_ID
|
||
if list_payload.get('ok') and isinstance(list_payload.get('items'), list) and list_payload['items']:
|
||
first_item = list_payload['items'][0]
|
||
if isinstance(first_item, dict) and isinstance(first_item.get('tableId'), int):
|
||
table_id = first_item['tableId']
|
||
|
||
completed = run_cli_command('gen', 'detail', str(table_id), '--env=dev', '--output=json')
|
||
payload = parse_json_stdout(completed)
|
||
|
||
assert completed.returncode in {SUCCESS, DATABASE_ERROR, RUNTIME_ERROR}
|
||
assert payload['env'] == 'dev'
|
||
assert isinstance(payload['ok'], bool)
|
||
if 'error' in payload:
|
||
assert isinstance(payload['message'], str)
|
||
assert isinstance(payload['error'], str)
|
||
return
|
||
|
||
if payload['ok'] is False:
|
||
assert set(payload) == {'ok', 'message', 'tableId', 'env'}
|
||
assert payload['tableId'] == table_id
|
||
return
|
||
|
||
assert set(payload) == {'ok', 'tableId', 'tableName', 'columnCount', 'tableCount', 'detail', 'env'}
|
||
assert payload['tableId'] == table_id
|
||
assert isinstance(payload['tableName'], str)
|
||
assert isinstance(payload['columnCount'], int)
|
||
assert isinstance(payload['tableCount'], int)
|
||
assert isinstance(payload['detail'], dict)
|
||
assert isinstance(payload['detail'].get('info'), dict)
|
||
assert isinstance(payload['detail'].get('rows'), list)
|
||
assert isinstance(payload['detail'].get('tables'), list)
|
||
|
||
|
||
def test_gen_sync_db_is_rejected_without_yes_in_non_interactive_mode(
|
||
run_cli_command: Callable[..., subprocess.CompletedProcess[str]],
|
||
parse_json_stdout: Callable[[subprocess.CompletedProcess[str]], dict],
|
||
) -> None:
|
||
completed = run_cli_command('gen', 'sync-db', 'demo_table', '--env=dev', '--output=json')
|
||
payload = parse_json_stdout(completed)
|
||
|
||
assert completed.returncode == GUARD_REJECTED
|
||
assert payload['ok'] is False
|
||
assert payload['message'] == '已取消危险命令执行:gen sync-db'
|
||
assert '--yes' in payload['hint']
|
||
|
||
|
||
def test_gen_export_dry_run_text_output_has_stable_structure(
|
||
run_text_cli_command: Callable[..., subprocess.CompletedProcess[str]],
|
||
) -> None:
|
||
completed = run_text_cli_command(
|
||
'gen',
|
||
'export',
|
||
GEN_TEST_TABLE_NAME,
|
||
'--env=dev',
|
||
'--dry-run',
|
||
'--yes',
|
||
'--output=text',
|
||
)
|
||
|
||
assert completed.returncode == SUCCESS
|
||
assert completed.stdout.startswith('OK SUCCESS\n')
|
||
assert 'env: dev\n' in completed.stdout
|
||
assert 'mode: zip\n' in completed.stdout
|
||
assert 'dry_run: true\n' in completed.stdout
|
||
assert 'message: 代码导出演练完成,未执行实际导出\n' in completed.stdout
|
||
assert 'table_names:\n' in completed.stdout
|
||
assert f' - {GEN_TEST_TABLE_NAME}\n' in completed.stdout
|
||
assert 'output_file:' in completed.stdout
|
||
assert f'gen_code_{GEN_TEST_TABLE_NAME}.zip' in completed.stdout
|
||
|
||
|
||
def test_gen_export_dry_run_json_output_has_stable_contract(
|
||
run_cli_command: Callable[..., subprocess.CompletedProcess[str]],
|
||
parse_json_stdout: Callable[[subprocess.CompletedProcess[str]], dict],
|
||
) -> None:
|
||
completed = run_cli_command(
|
||
'gen',
|
||
'export',
|
||
GEN_TEST_TABLE_NAME,
|
||
'--env=dev',
|
||
'--dry-run',
|
||
'--yes',
|
||
'--output=json',
|
||
)
|
||
payload = parse_json_stdout(completed)
|
||
|
||
assert completed.returncode == SUCCESS
|
||
assert payload['ok'] is True
|
||
assert payload['env'] == 'dev'
|
||
assert payload['dryRun'] is True
|
||
assert payload['mode'] == 'zip'
|
||
assert payload['message'] == '代码导出演练完成,未执行实际导出'
|
||
assert payload['tableNames'] == [GEN_TEST_TABLE_NAME]
|
||
assert isinstance(payload['outputFile'], str)
|
||
assert payload['outputFile'].endswith(f'gen_code_{GEN_TEST_TABLE_NAME}.zip')
|
||
|
||
|
||
def test_gen_create_table_dry_run_text_output_has_stable_structure(
|
||
run_text_cli_command: Callable[..., subprocess.CompletedProcess[str]],
|
||
) -> None:
|
||
completed = run_text_cli_command(
|
||
'gen',
|
||
'create-table',
|
||
'--env=dev',
|
||
'--dry-run',
|
||
'--yes',
|
||
'--sql',
|
||
GEN_CREATE_SQL,
|
||
'--output=text',
|
||
)
|
||
|
||
assert completed.returncode == SUCCESS
|
||
assert completed.stdout.startswith('OK SUCCESS\n')
|
||
assert 'env: dev\n' in completed.stdout
|
||
assert 'message: 建表语句演练完成,未执行实际建表\n' in completed.stdout
|
||
assert 'dry_run: true\n' in completed.stdout
|
||
assert 'statement_count: 1\n' in completed.stdout
|
||
assert 'table_names:\n' in completed.stdout
|
||
assert ' - demo_cli_test\n' in completed.stdout
|
||
assert f'sql: {GEN_CREATE_SQL}\n' in completed.stdout
|
||
|
||
|
||
def test_gen_create_table_dry_run_json_output_has_stable_contract(
|
||
run_cli_command: Callable[..., subprocess.CompletedProcess[str]],
|
||
parse_json_stdout: Callable[[subprocess.CompletedProcess[str]], dict],
|
||
) -> None:
|
||
completed = run_cli_command(
|
||
'gen',
|
||
'create-table',
|
||
'--env=dev',
|
||
'--dry-run',
|
||
'--yes',
|
||
'--sql',
|
||
GEN_CREATE_SQL,
|
||
'--output=json',
|
||
)
|
||
payload = parse_json_stdout(completed)
|
||
|
||
assert completed.returncode == SUCCESS
|
||
assert payload['ok'] is True
|
||
assert payload['env'] == 'dev'
|
||
assert payload['dryRun'] is True
|
||
assert payload['message'] == '建表语句演练完成,未执行实际建表'
|
||
assert payload['statementCount'] == 1
|
||
assert payload['tableNames'] == ['demo_cli_test']
|
||
assert payload['sql'] == GEN_CREATE_SQL
|
||
|
||
|
||
def test_gen_create_table_rejects_non_create_sql_in_text_output(
|
||
run_text_cli_command: Callable[..., subprocess.CompletedProcess[str]],
|
||
) -> None:
|
||
completed = run_text_cli_command(
|
||
'gen',
|
||
'create-table',
|
||
'--env=dev',
|
||
'--dry-run',
|
||
'--yes',
|
||
'--sql',
|
||
'DROP TABLE demo_cli_test;',
|
||
'--output=text',
|
||
)
|
||
|
||
assert completed.returncode == ARGUMENT_ERROR
|
||
assert completed.stdout == (
|
||
'FAIL FAILED\nmessage: 创建表结构失败\nerror: 建表语句不合法,仅允许 CREATE TABLE 语句\nenv: dev\n'
|
||
)
|
||
|
||
|
||
def test_gen_create_table_rejects_non_create_sql_in_json_output(
|
||
run_cli_command: Callable[..., subprocess.CompletedProcess[str]],
|
||
parse_json_stdout: Callable[[subprocess.CompletedProcess[str]], dict],
|
||
) -> None:
|
||
completed = run_cli_command(
|
||
'gen',
|
||
'create-table',
|
||
'--env=dev',
|
||
'--dry-run',
|
||
'--yes',
|
||
'--sql',
|
||
'DROP TABLE demo_cli_test;',
|
||
'--output=json',
|
||
)
|
||
payload = parse_json_stdout(completed)
|
||
|
||
assert completed.returncode == ARGUMENT_ERROR
|
||
assert payload == {
|
||
'ok': False,
|
||
'message': '创建表结构失败',
|
||
'error': '建表语句不合法,仅允许 CREATE TABLE 语句',
|
||
'env': 'dev',
|
||
}
|
||
|
||
|
||
def test_gen_create_table_rejects_conflicting_sql_inputs_in_text_output(
|
||
run_text_cli_command: Callable[..., subprocess.CompletedProcess[str]],
|
||
) -> None:
|
||
completed = run_text_cli_command(
|
||
'gen',
|
||
'create-table',
|
||
'--env=dev',
|
||
'--dry-run',
|
||
'--yes',
|
||
'--sql',
|
||
GEN_CREATE_SQL,
|
||
'--sql-file',
|
||
'fake.sql',
|
||
'--output=text',
|
||
)
|
||
|
||
assert completed.returncode == ARGUMENT_ERROR
|
||
assert completed.stdout == (
|
||
'FAIL FAILED\nmessage: 创建表结构失败\nerror: 必须且只能传入 --sql 或 --sql-file 其中一种方式\nenv: dev\n'
|
||
)
|
||
|
||
|
||
def test_gen_create_table_rejects_conflicting_sql_inputs_in_json_output(
|
||
run_cli_command: Callable[..., subprocess.CompletedProcess[str]],
|
||
parse_json_stdout: Callable[[subprocess.CompletedProcess[str]], dict],
|
||
) -> None:
|
||
completed = run_cli_command(
|
||
'gen',
|
||
'create-table',
|
||
'--env=dev',
|
||
'--dry-run',
|
||
'--yes',
|
||
'--sql',
|
||
GEN_CREATE_SQL,
|
||
'--sql-file',
|
||
'fake.sql',
|
||
'--output=json',
|
||
)
|
||
payload = parse_json_stdout(completed)
|
||
|
||
assert completed.returncode == ARGUMENT_ERROR
|
||
assert payload == {
|
||
'ok': False,
|
||
'message': '创建表结构失败',
|
||
'error': '必须且只能传入 --sql 或 --sql-file 其中一种方式',
|
||
'env': 'dev',
|
||
}
|