Files
planet/backend/tests/test_earth_news_manual.py
linkong 899e3bce43
Some checks failed
ci / backend (push) Has been cancelled
ci / frontend (push) Has been cancelled
release / images (push) Has been cancelled
ci / delivery (push) Has been cancelled
release: bump version to 0.71.0
2026-06-11 16:47:24 +08:00

253 lines
8.0 KiB
Python

from datetime import UTC, datetime
import pytest
from app.core.enums import NewsSourceType
from app.models.earth_news import EarthNewsItem
from app.models.system_setting import SystemSetting
from app.services.earth_news import REGION_ANCHORS
from app.services.earth_news_manual import (
DEFAULT_MANUAL_NEWS_GROUP_ID,
create_manual_news_group,
import_manual_news_items,
list_news_groups,
list_news_records,
parse_manual_news_import_upload,
rename_manual_news_group,
upsert_manual_news_item,
)
class _FakeResult:
def __init__(self, rows=None, scalar=None):
self.rows = rows or []
self._scalar = scalar
def scalar_one_or_none(self):
return self._scalar
def scalar(self):
return self._scalar
def scalars(self):
return self
def all(self):
return self.rows
class _FakeNewsSession:
def __init__(self, records=None, setting=None):
self.records = dict(records or {})
self.setting = setting
async def get(self, _model, item_id):
return self.records.get(item_id)
def add(self, item):
if isinstance(item, SystemSetting):
self.setting = item
else:
self.records[item.id] = item
async def execute(self, stmt):
statement = str(stmt)
if "system_settings" in statement:
return _FakeResult(scalar=self.setting)
if "count" in statement.lower():
return _FakeResult(scalar=len(self.records))
return _FakeResult(rows=list(self.records.values()))
async def flush(self):
return None
@pytest.fixture
def fake_news_queue(monkeypatch):
queued = []
async def _enqueue(payload, force=False):
queued.append({"payload": payload, "force": force})
return True
monkeypatch.setattr("app.services.earth_news_manual.enqueue_target_location_job", _enqueue)
return queued
@pytest.mark.asyncio
async def test_manual_news_upsert_uses_region_anchor_and_manual_metadata(fake_news_queue):
db = _FakeNewsSession()
result = await upsert_manual_news_item(
db,
{
"title": "手动添加的新闻",
"summary": "一条用于测试的手动新闻。",
"source": "人工录入",
"region": "europe",
"published_at": "2026-05-15T03:00:00Z",
"tags": ["manual", "test"],
},
)
anchor = REGION_ANCHORS["europe"]
assert result.created is True
assert result.queued is True
assert result.item.id.startswith("manual:")
assert result.item.feed_name == "手动添加"
assert result.item.source == "人工录入"
assert result.item.latitude == anchor.latitude
assert result.item.longitude == anchor.longitude
assert result.item.location_source == "region_anchor"
assert result.item.verified is False
assert result.item.location_meta["news_meta"]["feed_type"] == NewsSourceType.MANUAL.value
assert result.item.location_meta["news_meta"]["source_type"] == NewsSourceType.MANUAL.value
assert result.item.location_meta["news_meta"]["manual_group_id"] == DEFAULT_MANUAL_NEWS_GROUP_ID
assert len(fake_news_queue) == 1
@pytest.mark.asyncio
async def test_manual_news_duplicate_import_upserts_without_duplicate_rows(fake_news_queue):
db = _FakeNewsSession()
payload = {
"title": "Same manual story",
"source": "Manual Desk",
"published_at": "2026-05-15T03:00:00Z",
"region": "global",
}
first = await upsert_manual_news_item(db, payload)
second = await upsert_manual_news_item(db, {**payload, "summary": "Updated summary"})
assert first.created is True
assert second.created is False
assert len(db.records) == 1
assert db.records[first.item.id].summary == "Updated summary"
@pytest.mark.asyncio
async def test_manual_news_edit_without_location_preserves_manual_coordinates(fake_news_queue):
db = _FakeNewsSession()
created = await upsert_manual_news_item(
db,
{
"title": "Taipei-1 data center update",
"summary": "Initial summary.",
"region": "asia-pacific",
"published_at": "2026-05-15T03:00:00Z",
"location": {"label": "Kaohsiung, Taiwan", "latitude": 22.6273, "longitude": 120.3014},
},
)
updated = await upsert_manual_news_item(
db,
{
"title": "Taipei-1 data center update",
"summary": "Edited summary only.",
"region": "asia-pacific",
"published_at": "2026-05-15T03:00:00Z",
},
item_id_override=created.item.id,
)
assert updated.created is False
assert updated.item.latitude == pytest.approx(22.6273)
assert updated.item.longitude == pytest.approx(120.3014)
assert updated.item.location_source == "manual_location"
assert updated.item.verified is True
@pytest.mark.asyncio
async def test_manual_news_api_service_rejects_rss_records(fake_news_queue):
rss_record = EarthNewsItem(
id="bbc-world:example",
title="RSS story",
summary="RSS summary",
source="BBC World",
feed_name="BBC World",
region="global",
latitude=20,
longitude=0,
location_label="全球",
location_source="region_anchor",
verified=False,
location_meta={"news_meta": {"feed_type": "rss"}},
first_seen_at=datetime.now(UTC),
last_seen_at=datetime.now(UTC),
)
db = _FakeNewsSession({rss_record.id: rss_record})
with pytest.raises(PermissionError):
await upsert_manual_news_item(
db,
{"title": "Edited title", "region": "global"},
item_id_override=rss_record.id,
)
@pytest.mark.asyncio
async def test_manual_news_import_reports_per_item_errors(fake_news_queue):
db = _FakeNewsSession()
result = await import_manual_news_items(
db,
[
{"title": "Valid manual news", "region": "global"},
{"summary": "missing title"},
],
)
assert result["created"] == 1
assert result["failed"] == 1
assert result["errors"][0]["index"] == 1
@pytest.mark.asyncio
async def test_manual_news_groups_default_create_and_rename(fake_news_queue):
db = _FakeNewsSession()
initial = await list_news_groups(db)
assert initial["manual_groups"][0]["id"] == DEFAULT_MANUAL_NEWS_GROUP_ID
assert initial["manual_groups"][0]["name"] == "新建新闻组"
group = await create_manual_news_group(db, "专题组")
assert group["name"] == "专题组"
assert db.setting is not None
await upsert_manual_news_item(db, {"title": "Grouped story", "region": "global"}, group_id=group["id"])
renamed = await rename_manual_news_group(db, group["id"], "重命名专题")
record = next(iter(db.records.values()))
assert renamed["name"] == "重命名专题"
assert record.location_meta["news_meta"]["manual_group_id"] == group["id"]
assert record.location_meta["news_meta"]["manual_group_name"] == "重命名专题"
@pytest.mark.asyncio
async def test_manual_news_list_filters_by_group_id(fake_news_queue):
db = _FakeNewsSession()
group = await create_manual_news_group(db, "导入组")
await import_manual_news_items(
db,
[
{"title": "In group", "region": "global"},
{"title": "Also in group", "region": "global"},
],
group_id=group["id"],
)
await upsert_manual_news_item(db, {"title": "Default group", "region": "global"})
grouped = await list_news_records(db, page=1, page_size=20, group_id=group["id"])
default_group = await list_news_records(db, page=1, page_size=20, group_id=DEFAULT_MANUAL_NEWS_GROUP_ID)
assert grouped["total"] == 2
assert {item["manual_group_id"] for item in grouped["items"]} == {group["id"]}
assert default_group["total"] == 1
@pytest.mark.asyncio
async def test_manual_news_import_parser_requires_json_array():
with pytest.raises(ValueError, match="顶层必须是数组"):
await parse_manual_news_import_upload(b'{"title":"not an array"}')