feat(djapi): 新增日报查询 API(news/reports + news/events)及文档

- api/report/ 包:query(连库+SQL)/ views(2 视图)/ serializers(OpenAPI)/ tests(17 单测)
- urls.py 注册 news/reports/、news/events/;settings.py SPECTACULAR 加「日报」tag
- .env.example 补 NEWS_DB_* 占位配置;README/continuation.md 更新
- docs/news_report_api.md 使用手册;CLAUDE*.md 修正 CLI 路径为 finance/ 前缀
This commit is contained in:
2026-08-05 20:51:42 +08:00
parent 22ce3a6aea
commit 6ec198687c
17 changed files with 1275 additions and 21 deletions
+149
View File
@@ -0,0 +1,149 @@
"""
日报查询 API 测试(GET /api/news/reports/ 与 /api/news/events/)。
通过 mock 数据库查询层(api.report.query.*),只验证视图层
参数解析 / 默认窗口 / 响应结构 / 错误处理,不依赖真实 MySQL。
"""
from datetime import timedelta
from unittest.mock import patch
from django.test import TestCase
from django.utils import timezone
from rest_framework.test import APIClient
class NewsReportsAPITest(TestCase):
"""GET /api/news/reports/ 日报查询"""
def setUp(self):
self.client = APIClient()
self.url = '/api/news/reports/'
@patch('api.report.query.fetch_reports', return_value=[])
def test_default_window_last_24h(self, mock_fetch):
resp = self.client.get(self.url)
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.json(), [])
kwargs = mock_fetch.call_args.kwargs
now = timezone.now()
self.assertEqual(kwargs['start_date'], (now - timedelta(hours=24)).date())
self.assertEqual(kwargs['end_date'], now.date())
self.assertIsNone(kwargs['report_type'])
self.assertIsNone(kwargs['report_id'])
@patch('api.report.query.fetch_reports',
return_value=[{'id': 1, 'report_date': '2026-08-01', 'report_type': 'finance'}])
def test_report_type_and_date_range(self, mock_fetch):
resp = self.client.get(self.url, {
'report_type': 'finance',
'start_date': '2026-07-01',
'end_date': '2026-08-03',
})
self.assertEqual(resp.status_code, 200)
self.assertEqual(len(resp.json()), 1)
kwargs = mock_fetch.call_args.kwargs
self.assertEqual(kwargs['report_type'], 'finance')
self.assertEqual(str(kwargs['start_date']), '2026-07-01')
self.assertEqual(str(kwargs['end_date']), '2026-08-03')
@patch('api.report.query.fetch_reports')
def test_detail_by_id(self, mock_fetch):
mock_fetch.return_value = {
'id': 10, 'report_date': '2026-08-03', 'report_type': 'finance',
'events': [{'id': 1, 'title': '事件一'}],
}
resp = self.client.get(self.url, {'id': '10'})
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.json()['id'], 10)
self.assertEqual(mock_fetch.call_args.kwargs['report_id'], 10)
@patch('api.report.query.fetch_reports', return_value=None)
def test_detail_not_found(self, mock_fetch):
resp = self.client.get(self.url, {'id': '99999'})
self.assertEqual(resp.status_code, 404)
self.assertIn('error', resp.json())
def test_invalid_report_type(self):
resp = self.client.get(self.url, {'report_type': 'xxx'})
self.assertEqual(resp.status_code, 400)
def test_invalid_date_format(self):
resp = self.client.get(self.url, {'start_date': '2026/07/01'})
self.assertEqual(resp.status_code, 400)
def test_invalid_id(self):
resp = self.client.get(self.url, {'id': 'abc'})
self.assertEqual(resp.status_code, 400)
def test_non_positive_id(self):
resp = self.client.get(self.url, {'id': '0'})
self.assertEqual(resp.status_code, 400)
@patch('api.report.query.fetch_reports', side_effect=RuntimeError('db down'))
def test_query_error_500(self, mock_fetch):
resp = self.client.get(self.url)
self.assertEqual(resp.status_code, 500)
self.assertIn('error', resp.json())
class NewsEventsAPITest(TestCase):
"""GET /api/news/events/ 重要事件聚合"""
def setUp(self):
self.client = APIClient()
self.url = '/api/news/events/'
@patch('api.report.query.fetch_important_events', return_value=[])
def test_defaults(self, mock_fetch):
resp = self.client.get(self.url)
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.json(), [])
kwargs = mock_fetch.call_args.kwargs
self.assertEqual(kwargs['days'], 7)
self.assertEqual(kwargs['importance'], 4)
self.assertEqual(kwargs['limit'], 100)
self.assertIsNone(kwargs['report_type'])
self.assertIsNone(kwargs['section'])
@patch('api.report.query.fetch_important_events',
return_value=[{'id': 1, 'title': '重要事件', 'importance': 5}])
def test_filters(self, mock_fetch):
resp = self.client.get(self.url, {
'days': '3', 'importance': '5',
'report_type': 'intl', 'section': 'intl', 'limit': '10',
})
self.assertEqual(resp.status_code, 200)
self.assertEqual(len(resp.json()), 1)
kwargs = mock_fetch.call_args.kwargs
self.assertEqual(kwargs['days'], 3)
self.assertEqual(kwargs['importance'], 5)
self.assertEqual(kwargs['limit'], 10)
self.assertEqual(kwargs['report_type'], 'intl')
self.assertEqual(kwargs['section'], 'intl')
def test_invalid_days(self):
resp = self.client.get(self.url, {'days': 'abc'})
self.assertEqual(resp.status_code, 400)
def test_days_out_of_range(self):
resp = self.client.get(self.url, {'days': '0'})
self.assertEqual(resp.status_code, 400)
def test_invalid_importance(self):
resp = self.client.get(self.url, {'importance': '9'})
self.assertEqual(resp.status_code, 400)
def test_invalid_section(self):
resp = self.client.get(self.url, {'section': 'foo'})
self.assertEqual(resp.status_code, 400)
def test_invalid_report_type(self):
resp = self.client.get(self.url, {'report_type': 'xxx'})
self.assertEqual(resp.status_code, 400)
@patch('api.report.query.fetch_important_events', side_effect=RuntimeError('db down'))
def test_query_error_500(self, mock_fetch):
resp = self.client.get(self.url)
self.assertEqual(resp.status_code, 500)
self.assertIn('error', resp.json())