ShiErFeng/shierfeng-fastapi-backend/tests/cli/root/test_contract_db_cache_dev.py
2026-07-24 09:43:10 +08:00

236 lines
8.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import subprocess
from collections.abc import Callable
from pathlib import Path
from cli.exit_codes import DATABASE_ERROR, GUARD_REJECTED, REDIS_ERROR, RUNTIME_ERROR, SUCCESS
DB_HISTORY_TEST_LIMIT = 5
def test_dangerous_command_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('db', 'revision', '--env=dev', '--message=test-contract', '--output=json')
payload = parse_json_stdout(completed)
assert completed.returncode == GUARD_REJECTED
assert payload['ok'] is False
assert payload['message'] == '已取消危险命令执行db revision'
assert '--yes' in payload['hint']
def test_prod_dangerous_command_is_rejected_without_allow_prod(
run_cli_command: Callable[..., subprocess.CompletedProcess[str]],
parse_json_stdout: Callable[[subprocess.CompletedProcess[str]], dict],
) -> None:
completed = run_cli_command('db', 'revision', '--env=prod', '--message=test-contract', '--output=json')
payload = parse_json_stdout(completed)
assert completed.returncode == GUARD_REJECTED
assert payload['ok'] is False
assert payload['message'] == '生产环境默认禁止直接执行危险命令db revision'
assert '--allow-prod' in payload['hint']
assert '--yes' in payload['hint']
def test_dry_run_command_returns_structured_preview_payload(
backend_dir: Path,
run_cli_command: Callable[..., subprocess.CompletedProcess[str]],
parse_json_stdout: Callable[[subprocess.CompletedProcess[str]], dict],
) -> None:
completed = run_cli_command(
'db',
'upgrade',
'--env=dev',
'--revision=head',
'--dry-run',
'--yes',
'--output=json',
)
payload = parse_json_stdout(completed)
assert completed.returncode == 0
assert payload['ok'] is True
assert payload['dryRun'] is True
assert isinstance(payload['command'], list)
assert payload['command'][-2:] == ['upgrade', 'head']
assert payload['cwd'] == str(backend_dir)
def test_db_heads_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('db', 'heads', '--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', 'message', 'count', 'items', 'env'}
assert isinstance(payload['count'], int)
assert isinstance(payload['items'], list)
if payload['items']:
first_item = payload['items'][0]
assert set(first_item) == {'revision', 'downRevisions', 'branchLabels', 'dependsOn', 'doc', 'path'}
assert isinstance(first_item['revision'], str)
assert isinstance(first_item['downRevisions'], list)
assert isinstance(first_item['branchLabels'], list)
assert isinstance(first_item['dependsOn'], list)
assert isinstance(first_item['doc'], str)
assert isinstance(first_item['path'], str)
def test_db_history_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(
'db',
'history',
'--env=dev',
f'--limit={DB_HISTORY_TEST_LIMIT}',
'--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', 'message', 'count', 'totalCount', 'limit', 'items', 'env'}
assert payload['limit'] == DB_HISTORY_TEST_LIMIT
assert isinstance(payload['count'], int)
assert isinstance(payload['totalCount'], int)
assert isinstance(payload['items'], list)
def test_cache_ttl_json_output_has_stable_contract_for_missing_key_or_redis_error(
run_cli_command: Callable[..., subprocess.CompletedProcess[str]],
parse_json_stdout: Callable[[subprocess.CompletedProcess[str]], dict],
) -> None:
completed = run_cli_command(
'cache',
'ttl',
'sys_config',
'definitely_missing_cli_key',
'--env=dev',
'--output=json',
)
payload = parse_json_stdout(completed)
assert completed.returncode in {SUCCESS, REDIS_ERROR, RUNTIME_ERROR}
assert isinstance(payload['ok'], bool)
if 'error' in payload:
assert payload['message'] == '读取缓存剩余过期时间失败'
assert isinstance(payload['error'], str)
return
assert set(payload) == {'ok', 'message', 'cacheName', 'cacheKey', 'fullCacheKey'}
assert payload['ok'] is False
assert payload['cacheName'] == 'sys_config'
assert payload['cacheKey'] == 'definitely_missing_cli_key'
assert payload['fullCacheKey'] == 'sys_config:definitely_missing_cli_key'
def test_dangerous_command_rejection_text_output_is_human_readable(
run_text_cli_command: Callable[..., subprocess.CompletedProcess[str]],
) -> None:
completed = run_text_cli_command('db', 'revision', '--env=dev', '--message=test-contract', '--output=text')
assert completed.returncode == GUARD_REJECTED
assert completed.stdout == (
'FAIL FAILED\n'
'message: 已取消危险命令执行db revision\n'
'hint: 当前命令需要交互确认;如需非交互执行,请传入 --yes\n'
)
def test_dry_run_text_output_contains_preview_command_and_workdir(
backend_dir: Path,
run_text_cli_command: Callable[..., subprocess.CompletedProcess[str]],
) -> None:
completed = run_text_cli_command(
'db',
'upgrade',
'--env=dev',
'--revision=head',
'--dry-run',
'--yes',
'--output=text',
)
assert completed.returncode == 0
assert completed.stdout.startswith('OK SUCCESS\n')
assert 'message: 数据库已升级到 headdry-run\n' in completed.stdout
assert 'dry_run: true\n' in completed.stdout
assert 'command:\n - alembic\n - -c\n' in completed.stdout
assert f' - {backend_dir / "alembic.ini"}\n' in completed.stdout
assert f'cwd: {backend_dir}\n' in completed.stdout
def test_dev_lint_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(
'dev',
'lint',
'cli/groups/dev/command.py',
'--env=dev',
'--check-only',
'--output=json',
)
payload = parse_json_stdout(completed)
assert completed.returncode == SUCCESS
assert payload['ok'] is True
assert payload['message'] == '开发检查已完成'
assert payload['env'] == 'dev'
assert payload['checkOnly'] is True
assert payload['fix'] is False
assert payload['unsafeFixes'] is False
assert payload['targets'] == ['cli/groups/dev/command.py']
assert isinstance(payload['format'], dict)
assert isinstance(payload['check'], dict)
assert payload['format']['ok'] is True
assert payload['check']['ok'] is True
assert isinstance(payload['format']['command'], list)
assert isinstance(payload['check']['command'], list)
def test_dev_test_text_output_has_stable_summary(
run_text_cli_command: Callable[..., subprocess.CompletedProcess[str]],
) -> None:
completed = run_text_cli_command(
'dev',
'test',
'tests/cli/root/test_contract_cli.py',
'--env=dev',
'--keyword=test_root_help_shows_commands_without_completion_options',
'--maxfail=1',
'--quiet',
'--output=text',
)
assert completed.returncode == SUCCESS
assert completed.stdout.startswith('OK SUCCESS\n')
assert 'ok: true\n' in completed.stdout
assert 'env: dev\n' in completed.stdout
assert 'keyword: test_root_help_shows_commands_without_completion_options\n' in completed.stdout
assert 'maxfail: 1\n' in completed.stdout
assert 'quiet: true\n' in completed.stdout
assert 'targets:\n - tests/cli/root/test_contract_cli.py\n' in completed.stdout
assert 'test:\n ok: true\n' in completed.stdout
assert 'command: ' in completed.stdout