sync
CI / backend-and-migrations (push) Canceled after 0s
CI / astro-build (push) Canceled after 0s
CI / dependency-audit (push) Canceled after 0s
CI / compose-e2e (push) Canceled after 0s

This commit is contained in:
ik
2026-09-17 07:35:04 +07:00
parent 5221442aeb
commit 722c88d436
25 changed files with 673 additions and 40 deletions
+87 -3
View File
@@ -7,7 +7,7 @@ import pytest
from sqlalchemy import create_engine, func, select
from sqlalchemy.orm import Session
from app.community_importer import CommunityImportError, stage_observations
from app.community_importer import CommunityImportError, stage_observations, update_waterbody_detail, update_waterbody_details, upsert_waterbody_catalog
from app.community_review import ExternalReviewError, map_observation, publish_observation, suggest_aliases
from app.source_lifecycle import record_scheduled_source_check, record_source_check
from app.database import Base
@@ -42,6 +42,20 @@ def record(source: str = "rf4db", external_id: str = "catch-1") -> dict[str, obj
}
def waterbody_row(**overrides: object) -> dict[str, object]:
row: dict[str, object] = {
"source_system": "rf4db",
"source_external_id": "level_001_mosquito",
"source_url": "https://rf4db.com/ru/maps/level_001_mosquito",
"name": "оз. Комариное",
"unlock_level": 1,
"unlock_label": "1",
"fish_species_count": 20,
}
row.update(overrides)
return row
@pytest.fixture
def db() -> Session:
engine = create_engine("sqlite://")
@@ -69,6 +83,62 @@ def test_staging_is_idempotent_and_preserves_first_seen(db: Session) -> None:
assert source is not None and source.enabled is True
def test_waterbody_catalog_upsert_is_idempotent_and_non_destructive(db: Session) -> None:
first = datetime(2026, 9, 16, 10, tzinfo=timezone.utc)
assert upsert_waterbody_catalog(db, [waterbody_row()], fetched_at=first) == (1, 0)
item = db.scalar(select(Waterbody).where(Waterbody.source_external_id == "level_001_mosquito"))
assert item is not None
assert item.slug == "оз-комариное"
assert item.source_checked_at.replace(tzinfo=timezone.utc) == first
assert item.fish_species_count == 20
assert upsert_waterbody_catalog(db, [waterbody_row(name="Озеро Комариное", unlock_level=2)], fetched_at=first) == (0, 1)
item = db.scalar(select(Waterbody).where(Waterbody.source_external_id == "level_001_mosquito"))
assert item is not None
assert (item.name_ru, item.unlock_level, item.fish_species_count) == ("Озеро Комариное", 2, 20)
assert db.scalar(select(Waterbody).where(Waterbody.name_ru == "оз. Комариное")) is None
def test_waterbody_catalog_rejects_untrusted_source(db: Session) -> None:
with pytest.raises(CommunityImportError, match="source_url"):
upsert_waterbody_catalog(db, [waterbody_row(source_url="https://example.test/map")])
def test_waterbody_detail_updates_only_imported_identity_without_media_roles(db: Session) -> None:
upsert_waterbody_catalog(db, [waterbody_row()])
assert update_waterbody_detail(db, {
"source_system": "rf4db",
"source_external_id": "level_001_mosquito",
"source_url": "https://rf4db.com/ru/maps/level_001_mosquito",
"name": "оз. Комариное",
"description": "Каменистые берега.",
"aliases": ["Комариное", "Комариное"],
"fish_species": ["Щука", "Окунь"],
"image_urls": ["https://oss.rf4db.com/map.webp"],
"point_urls": ["https://rf4db.com/ru/maps/level_001_mosquito/spots/12-34"],
}) is True
item = db.scalar(select(Waterbody).where(Waterbody.source_external_id == "level_001_mosquito"))
assert item is not None
assert item.source_aliases == ["Комариное"]
assert item.source_fish_species == ["Щука", "Окунь"]
assert item.source_image_urls == ["https://oss.rf4db.com/map.webp"]
def test_waterbody_detail_batch_validates_before_writing(db: Session) -> None:
upsert_waterbody_catalog(db, [waterbody_row()])
valid = {
"source_system": "rf4db", "source_external_id": "level_001_mosquito",
"source_url": "https://rf4db.com/ru/maps/level_001_mosquito", "name": "оз. Комариное",
"description": "Описание", "aliases": [], "fish_species": ["Щука"],
"image_urls": [], "point_urls": [],
}
invalid = valid | {"source_external_id": "unknown", "source_url": "https://example.test/map"}
with pytest.raises(CommunityImportError, match="source_url"):
update_waterbody_details(db, [valid, invalid])
item = db.scalar(select(Waterbody).where(Waterbody.source_external_id == "level_001_mosquito"))
assert item is not None and item.description is None
def test_external_ids_are_isolated_by_source(db: Session) -> None:
created, updated = stage_observations(db, [record("rf4db"), record("rf4stat-fishing")])
@@ -104,14 +174,28 @@ def test_complete_observation_with_reviewed_aliases_is_published(db: Session) ->
assert item.catch_report is not None
assert item.catch_report.fish_id == fish.id
assert item.catch_report.waterbody_id == waterbody.id
assert db.scalar(select(func.count()).select_from(CatchReport)) == 1
assert stage_observations(db, [record() | {"weight_g": 5_000}]) == (0, 1)
db.refresh(item)
assert item.status == "published"
assert db.scalar(select(func.count()).select_from(CatchReport)) == 1
def test_observation_preserves_coordinate_text_and_precision(db: Session) -> None:
stage_observations(db, [record() | {
"source_external_id": "coordinate-area",
"x": None, "y": None, "coordinate_raw": "северная бухта",
"coordinate_precision": "area", "weight_g": None,
}])
item = db.scalar(select(ExternalObservation).where(ExternalObservation.source_external_id == "coordinate-area"))
assert item is not None
assert (item.coordinate_raw, item.coordinate_precision, item.x, item.y) == ("северная бухта", "area", None, None)
def test_coordinate_precision_rejects_unknown_value(db: Session) -> None:
with pytest.raises(CommunityImportError, match="invalid coordinate_precision"):
stage_observations(db, [record() | {"coordinate_precision": "guess"}])
def test_changed_published_record_requires_review_and_reuses_report(db: Session) -> None:
fish = Fish(slug="pike", name_ru="Щука")
water = Waterbody(slug="test-lake", name_ru="Тестовое озеро")