All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 11m45s
v0.19.0 做 1080P 时 QuotaConfig 加了 base_token_price_1080p 和
base_token_price_1080p_video 字段, serializer (PUT) 和计费逻辑
(_get_token_price) 都处理了, 但 _settings_dict (GET) 漏了两行,
导致管理后台设置页两个 1080P 单价输入框显示空白。
实际影响
- DB 值对 (51 / 31), 计费走 _get_token_price 直接读 DB, 计费一直正确
- 前端 SettingsPage fetchSettings 用 setSettings(data) 覆盖,
GET 返回缺字段 -> state 变 undefined -> 输入框显示空
- 管理员点保存: undefined 被 JSON.stringify 省略 -> PUT body 不含
这两字段 -> serializer validated_data 里没有 -> DB 未改
- 所以目前"巧合安全", 但风险: 管理员在空输入框填数字后清空,
Number("") = 0 会覆盖 DB, 把单价刷成 0
修复
- backend/apps/generation/views.py _settings_dict() 加两行返回
base_token_price_1080p / base_token_price_1080p_video
- 前端 GET 后 state 直接拿到 51 / 31, 输入框自动显示, 不依赖"巧合"
回归测试 (backend/tests/test_1080p_api.py)
- 新增 TestAdminSettingsResponse.test_get_returns_all_token_price_fields
断言 GET /admin/settings 返回 6 个 token_price 字段全齐
- 失败消息明示: "缺字段会导致前端输入框显示空" 以防以后再漏
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
165 lines
6.1 KiB
Python
165 lines
6.1 KiB
Python
"""
|
||
1080P API 集成测试 — 验证 video_generate_view 的入口校验。
|
||
"""
|
||
import os
|
||
import sys
|
||
import django
|
||
|
||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
|
||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
||
django.setup()
|
||
|
||
import unittest
|
||
from django.test import TestCase
|
||
from django.contrib.auth import get_user_model
|
||
from rest_framework.test import APIClient
|
||
from apps.accounts.models import Team
|
||
from apps.generation.models import QuotaConfig
|
||
|
||
User = get_user_model()
|
||
|
||
|
||
class TestVideoGenerateResolution(TestCase):
|
||
"""video_generate_view 的分辨率+模型组合校验。"""
|
||
|
||
def setUp(self):
|
||
# 初始化 QuotaConfig
|
||
QuotaConfig.objects.get_or_create(pk=1)
|
||
|
||
# 建测试 team + user
|
||
self.team = Team.objects.create(
|
||
name='test-1080p',
|
||
is_active=True,
|
||
monthly_spending_limit=1000,
|
||
markup_percentage=0,
|
||
balance=1000,
|
||
frozen_amount=0,
|
||
)
|
||
self.user = User.objects.create_user(
|
||
username='test_1080p_user',
|
||
email='test1080p@example.com',
|
||
password='testpass123',
|
||
team=self.team,
|
||
spending_limit=-1,
|
||
daily_generation_limit=-1,
|
||
monthly_generation_limit=-1,
|
||
)
|
||
self.client = APIClient()
|
||
self.client.force_authenticate(user=self.user)
|
||
|
||
def test_reject_fast_plus_1080p(self):
|
||
"""原则 1:Fast + 1080P 组合必须 400 拒绝,不能静默降级。"""
|
||
resp = self.client.post('/api/v1/video/generate', {
|
||
'prompt': '测试',
|
||
'mode': 'universal',
|
||
'model': 'seedance_2.0_fast',
|
||
'aspect_ratio': '16:9',
|
||
'duration': 5,
|
||
'resolution': '1080p',
|
||
'references': [],
|
||
}, format='json')
|
||
self.assertEqual(resp.status_code, 400)
|
||
body = resp.json()
|
||
self.assertEqual(body.get('error'), 'invalid_resolution')
|
||
# 提示信息要明确告知用户原因
|
||
self.assertIn('1080P', body.get('message', ''))
|
||
self.assertIn('Fast', body.get('message', ''))
|
||
|
||
def test_reject_adaptive_ratio(self):
|
||
"""原则 1:adaptive 不在 6 选 1 白名单,拒绝。"""
|
||
resp = self.client.post('/api/v1/video/generate', {
|
||
'prompt': '测试',
|
||
'mode': 'universal',
|
||
'model': 'seedance_2.0',
|
||
'aspect_ratio': 'adaptive',
|
||
'duration': 5,
|
||
'resolution': '720p',
|
||
'references': [],
|
||
}, format='json')
|
||
self.assertEqual(resp.status_code, 400)
|
||
# serializer 错误:aspect_ratio 不在 choices
|
||
self.assertIn('aspect_ratio', str(resp.content))
|
||
|
||
def test_reject_invalid_resolution(self):
|
||
"""resolution 不在 480p/720p/1080p 白名单,拒绝。"""
|
||
resp = self.client.post('/api/v1/video/generate', {
|
||
'prompt': '测试',
|
||
'mode': 'universal',
|
||
'model': 'seedance_2.0',
|
||
'aspect_ratio': '16:9',
|
||
'duration': 5,
|
||
'resolution': '4K',
|
||
'references': [],
|
||
}, format='json')
|
||
self.assertEqual(resp.status_code, 400)
|
||
|
||
def test_resolution_default_720p_when_missing(self):
|
||
"""旧客户端不传 resolution 字段时,serializer default='720p' 生效。"""
|
||
# 不传 resolution(兼容旧客户端)
|
||
resp = self.client.post('/api/v1/video/generate', {
|
||
'prompt': '测试',
|
||
'mode': 'universal',
|
||
'model': 'seedance_2.0',
|
||
'aspect_ratio': '16:9',
|
||
'duration': 5,
|
||
'references': [],
|
||
}, format='json')
|
||
# serializer 应该接受(default='720p');可能因火山 API 未开通等其他原因失败,
|
||
# 但不该是 resolution 相关的 400 错误
|
||
if resp.status_code == 400:
|
||
body = resp.json()
|
||
self.assertNotEqual(body.get('error'), 'invalid_resolution')
|
||
|
||
def test_accept_valid_1080p_airdrama(self):
|
||
"""原则:AirDrama + 1080P 组合合法,不被 400 拒绝。"""
|
||
resp = self.client.post('/api/v1/video/generate', {
|
||
'prompt': '测试',
|
||
'mode': 'universal',
|
||
'model': 'seedance_2.0',
|
||
'aspect_ratio': '16:9',
|
||
'duration': 5,
|
||
'resolution': '1080p',
|
||
'references': [],
|
||
}, format='json')
|
||
# 不应该因为分辨率被 400(可能因余额/API 未开通等其他原因失败)
|
||
if resp.status_code == 400:
|
||
body = resp.json()
|
||
self.assertNotEqual(body.get('error'), 'invalid_resolution')
|
||
|
||
|
||
class TestAdminSettingsResponse(TestCase):
|
||
"""GET /api/v1/admin/settings 必须返回所有 token_price 字段,
|
||
以防 v0.19.0 那种"字段在 serializer 里加了、但 _settings_dict 漏了"的回归。"""
|
||
|
||
def setUp(self):
|
||
QuotaConfig.objects.get_or_create(pk=1)
|
||
self.admin = User.objects.create_user(
|
||
username='test_admin_settings',
|
||
email='test_admin_settings@example.com',
|
||
password='testpass123',
|
||
is_staff=True,
|
||
is_superuser=True,
|
||
)
|
||
self.client = APIClient()
|
||
self.client.force_authenticate(user=self.admin)
|
||
|
||
def test_get_returns_all_token_price_fields(self):
|
||
"""GET 返回 4 档单价(全部分辨率 + 是否含视频),缺一不可 — 缺字段会导致前端输入框显示空。"""
|
||
resp = self.client.get('/api/v1/admin/settings')
|
||
self.assertEqual(resp.status_code, 200)
|
||
body = resp.json()
|
||
for field in (
|
||
'base_token_price',
|
||
'base_token_price_video',
|
||
'base_token_price_fast',
|
||
'base_token_price_fast_video',
|
||
'base_token_price_1080p',
|
||
'base_token_price_1080p_video',
|
||
):
|
||
self.assertIn(field, body, f'GET /admin/settings response missing {field!r} — 前端这个输入框会显示空')
|
||
self.assertIsInstance(body[field], (int, float), f'{field} 应该是数字类型')
|
||
|
||
|
||
if __name__ == '__main__':
|
||
unittest.main(verbosity=2)
|