fix: handle unique constraint violation in manual fetch endpoint

When a duplicate timestamp occurs (rare but possible), return the
existing record instead of failing with a 500 error. This matches
the worker's ON CONFLICT DO NOTHING behavior.

Added test for duplicate timestamp handling.
This commit is contained in:
counterweight 2025-12-22 16:09:05 +01:00
parent 1c9761e559
commit a5488fd20b
Signed by: counterweight
GPG key ID: 883EDBAA726BD96C
2 changed files with 48 additions and 2 deletions

View file

@ -6,6 +6,7 @@ from typing import TypeVar
from fastapi import APIRouter, Depends, Query
from pydantic import BaseModel
from sqlalchemy import desc, func, select
from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession
from auth import require_permission
@ -200,8 +201,20 @@ async def fetch_price_now(
timestamp=timestamp,
)
db.add(record)
try:
await db.commit()
await db.refresh(record)
except IntegrityError:
# Duplicate timestamp - return the existing record
await db.rollback()
query = select(PriceHistory).where(
PriceHistory.source == SOURCE_BITFINEX,
PriceHistory.pair == PAIR_BTC_EUR,
PriceHistory.timestamp == timestamp,
)
result = await db.execute(query)
record = result.scalar_one()
return PriceHistoryResponse(
id=record.id,

View file

@ -241,6 +241,39 @@ class TestManualFetch:
assert len(data) == 1
assert data[0]["price"] == 87654.32
@pytest.mark.asyncio
async def test_returns_existing_record_on_duplicate_timestamp(
self, client_factory, admin_user
):
"""Verify duplicate timestamp returns existing record instead of error."""
fixed_timestamp = datetime(2024, 1, 15, 12, 0, 0, tzinfo=UTC)
# Seed an existing record with the same timestamp we'll get from the mock
async with client_factory.get_db_session() as db:
existing = PriceHistory(
source=SOURCE_BITFINEX,
pair=PAIR_BTC_EUR,
price=90000.0,
timestamp=fixed_timestamp,
)
db.add(existing)
await db.commit()
await db.refresh(existing)
existing_id = existing.id
# Mock fetch_btc_eur_price to return the same timestamp
with patch("routes.audit.fetch_btc_eur_price") as mock_fetch:
mock_fetch.return_value = (95000.0, fixed_timestamp)
async with client_factory.create(cookies=admin_user["cookies"]) as authed:
response = await authed.post("/api/audit/price-history/fetch")
# Should succeed and return the existing record
assert response.status_code == 200
data = response.json()
assert data["id"] == existing_id
assert data["price"] == 90000.0 # Original price, not the new one
def create_mock_pool(mock_conn: AsyncMock) -> MagicMock:
"""Create a mock asyncpg pool with proper async context manager behavior."""