fix: R02 activity API contract — PaginatedActivity + all consumers (#1788956171115)
This commit is contained in:
@@ -45,11 +45,15 @@ def test_activity_filters_and_explains_score() -> None:
|
||||
response = client.get("/api/v1/activity?waterbody=test-lake&fish=pike&hours=24")
|
||||
assert response.status_code == 200
|
||||
payload = response.json()
|
||||
assert len(payload) == 1
|
||||
assert payload[0]["catches"] == 3
|
||||
assert payload[0]["unique_players"] == 3
|
||||
assert "3 свежих улова" in payload[0]["explanation"]
|
||||
assert payload[0]["sources"] == ["manual-import"]
|
||||
assert "items" in payload
|
||||
assert payload["total"] == 1
|
||||
assert payload["limit"] == 20
|
||||
assert payload["offset"] == 0
|
||||
assert len(payload["items"]) == 1
|
||||
assert payload["items"][0]["catches"] == 3
|
||||
assert payload["items"][0]["unique_players"] == 3
|
||||
assert "3 свежих улова" in payload["items"][0]["explanation"]
|
||||
assert payload["items"][0]["sources"] == ["manual-import"]
|
||||
|
||||
|
||||
def test_invalid_period_is_rejected() -> None:
|
||||
@@ -157,7 +161,7 @@ def test_admin_diagnostics_exposes_build_identity_only_to_admin() -> None:
|
||||
|
||||
|
||||
def test_spot_detail_and_catches() -> None:
|
||||
spot_id = client.get("/api/v1/activity").json()[0]["spot_id"]
|
||||
spot_id = client.get("/api/v1/activity").json()["items"][0]["spot_id"]
|
||||
detail = client.get(f"/api/v1/spots/{spot_id}")
|
||||
catches = client.get(f"/api/v1/spots/{spot_id}/catches")
|
||||
assert detail.status_code == 200
|
||||
@@ -206,7 +210,7 @@ def test_user_report_requires_moderation_before_activity() -> None:
|
||||
approved = client.patch(f"/api/v1/admin/catch-reports/{report_id}", headers=headers, json={"status": "approved", "reason": "fixture verified"})
|
||||
assert approved.status_code == 200
|
||||
activity = client.get("/api/v1/activity?waterbody=test-lake&fish=pike&hours=24").json()
|
||||
assert any(item["x"] == 77 and item["catches"] == 1 for item in activity)
|
||||
assert any(item["x"] == 77 and item["catches"] == 1 for item in activity["items"])
|
||||
|
||||
|
||||
def test_admin_requires_token() -> None:
|
||||
@@ -267,7 +271,7 @@ def test_incomplete_external_observation_is_publicly_labelled_but_not_counted()
|
||||
assert signal["source_system"] == "rf4db"
|
||||
assert signal["quality"] == "incomplete"
|
||||
assert signal["missing_fields"] == ["вес"]
|
||||
assert all(item["x"] != 32 or item["y"] != 42 for item in client.get("/api/v1/activity").json())
|
||||
assert all(item["x"] != 32 or item["y"] != 42 for item in client.get("/api/v1/activity").json()["items"])
|
||||
headers = {"Authorization": "Bearer change-me-in-production"}
|
||||
mapped = client.patch(
|
||||
f"/api/v1/admin/external-observations/{observation_id}/mapping", headers=headers,
|
||||
|
||||
@@ -3,12 +3,12 @@ from pydantic import ValidationError
|
||||
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from app.community_scheduler import MAX_BACKOFF_SECONDS, configured_sources, oldest_site_source, retry_delay
|
||||
from app.community_scheduler import MAX_BACKOFF_SECONDS, configured_sources, _static_registry, oldest_site_source, retry_delay
|
||||
from app.config import Settings
|
||||
|
||||
|
||||
def test_all_authorized_sources_are_scheduled() -> None:
|
||||
assert set(configured_sources()) == {"rf4db", "rf4stat-fishing", "rf4stat-post", "rf4map", "rf4posts-spot"}
|
||||
assert set(_static_registry()) == {"rf4db", "rf4stat-fishing", "rf4stat-post", "rf4map", "rf4posts-spot"}
|
||||
|
||||
|
||||
def test_community_interval_cannot_be_less_than_30_minutes() -> None:
|
||||
@@ -25,6 +25,7 @@ def test_failed_runs_back_off_but_success_resets_delay() -> None:
|
||||
|
||||
def test_same_site_endpoints_rotate_by_oldest_attempt() -> None:
|
||||
now = datetime.now(timezone.utc)
|
||||
assert oldest_site_source("rf4stat-fishing", {}) == "rf4stat-fishing"
|
||||
all_keys = {"rf4db", "rf4stat-fishing", "rf4stat-post", "rf4map", "rf4posts-spot"}
|
||||
assert oldest_site_source("rf4stat-fishing", {}, all_keys) == "rf4stat-fishing"
|
||||
latest = {"rf4stat-fishing": now, "rf4stat-post": now - timedelta(hours=1)}
|
||||
assert oldest_site_source("rf4stat-fishing", latest) == "rf4stat-post"
|
||||
assert oldest_site_source("rf4stat-fishing", latest, all_keys) == "rf4stat-post"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
from fastapi import HTTPException
|
||||
@@ -17,11 +18,34 @@ def test_rate_limit_is_persistent_and_does_not_store_raw_client() -> None:
|
||||
Base.metadata.create_all(engine)
|
||||
with Session(engine) as db:
|
||||
for _ in range(5):
|
||||
_check_rate_limit("203.0.113.42", db)
|
||||
mock_request = MagicMock()
|
||||
mock_request.client.host = "203.0.113.42"
|
||||
mock_request.headers.get.return_value = None
|
||||
_check_rate_limit(mock_request, db)
|
||||
with pytest.raises(HTTPException) as blocked:
|
||||
_check_rate_limit("203.0.113.42", db)
|
||||
mock_request = MagicMock()
|
||||
mock_request.client.host = "203.0.113.42"
|
||||
mock_request.headers.get.return_value = None
|
||||
_check_rate_limit(mock_request, db)
|
||||
assert blocked.value.status_code == 429
|
||||
attempts = list(db.scalars(select(SubmissionAttempt)))
|
||||
assert len(attempts) == 5
|
||||
assert all(item.client_hash != "203.0.113.42" and len(item.client_hash) == 64 for item in attempts)
|
||||
assert all(item.created_at.replace(tzinfo=timezone.utc) <= datetime.now(timezone.utc) for item in attempts)
|
||||
|
||||
|
||||
def test_rate_limit_uses_forwarded_for_header() -> None:
|
||||
engine = create_engine("sqlite://")
|
||||
Base.metadata.create_all(engine)
|
||||
with Session(engine) as db:
|
||||
mock_real = MagicMock()
|
||||
mock_real.client.host = "10.0.0.1"
|
||||
mock_real.headers.get.return_value = "198.51.100.10"
|
||||
for _ in range(5):
|
||||
_check_rate_limit(mock_real, db)
|
||||
with pytest.raises(HTTPException) as blocked:
|
||||
mock_other = MagicMock()
|
||||
mock_other.client.host = "10.0.0.2"
|
||||
mock_other.headers.get.return_value = "198.51.100.10"
|
||||
_check_rate_limit(mock_other, db)
|
||||
assert blocked.value.status_code == 429
|
||||
|
||||
@@ -28,11 +28,11 @@ def test_optional_import_does_not_block_dependencies() -> None:
|
||||
session, AvailableStorage(), import_required=False, import_interval_seconds=3600,
|
||||
)
|
||||
assert ready is True
|
||||
assert components == {
|
||||
"postgresql": {"status": "ready"},
|
||||
"minio": {"status": "ready"},
|
||||
"official_import": {"status": "optional", "last_run_status": None},
|
||||
}
|
||||
assert components["postgresql"]["status"] == "ready"
|
||||
assert components["minio"]["status"] == "ready"
|
||||
assert components["official_import"]["status"] == "optional"
|
||||
assert components["official_import"]["last_run_status"] is None
|
||||
assert "community_scheduler" in components
|
||||
|
||||
|
||||
def test_required_import_must_be_recent_and_successful() -> None:
|
||||
|
||||
Reference in New Issue
Block a user