253 lines
8.0 KiB
Python
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"}')
|