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:
parent
1c9761e559
commit
a5488fd20b
2 changed files with 48 additions and 2 deletions
|
|
@ -6,6 +6,7 @@ from typing import TypeVar
|
||||||
from fastapi import APIRouter, Depends, Query
|
from fastapi import APIRouter, Depends, Query
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from sqlalchemy import desc, func, select
|
from sqlalchemy import desc, func, select
|
||||||
|
from sqlalchemy.exc import IntegrityError
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from auth import require_permission
|
from auth import require_permission
|
||||||
|
|
@ -200,8 +201,20 @@ async def fetch_price_now(
|
||||||
timestamp=timestamp,
|
timestamp=timestamp,
|
||||||
)
|
)
|
||||||
db.add(record)
|
db.add(record)
|
||||||
|
|
||||||
|
try:
|
||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(record)
|
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(
|
return PriceHistoryResponse(
|
||||||
id=record.id,
|
id=record.id,
|
||||||
|
|
|
||||||
|
|
@ -241,6 +241,39 @@ class TestManualFetch:
|
||||||
assert len(data) == 1
|
assert len(data) == 1
|
||||||
assert data[0]["price"] == 87654.32
|
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:
|
def create_mock_pool(mock_conn: AsyncMock) -> MagicMock:
|
||||||
"""Create a mock asyncpg pool with proper async context manager behavior."""
|
"""Create a mock asyncpg pool with proper async context manager behavior."""
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue