Skip to content

Commit 5185ea9

Browse files
pmcfadinclaude
andcommitted
fix: wrap video_activity in try/except, move import, fix log level, fix tests (closes #14)
- Wrap video_activity insert in its own try/except so a DB failure there never kills the 204 response after the views counter already succeeded - Move import of record_user_activity/ANONYMOUS_USER_ID outside the try block so ImportError is not silently swallowed - Upgrade logger.debug to logger.warning for both non-critical insert failures for production visibility - Update user_activity tests to patch record_user_activity directly instead of patching get_table — tests the contract, not implementation - Fix test_list_latest_videos: assert page_size=10 (was 3) to match call - Fix test_search_videos_by_keyword: patch search_videos_by_semantic (keyword search now delegates there) instead of list_videos_with_query Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent ca30df6 commit 5185ea9

2 files changed

Lines changed: 125 additions & 19 deletions

File tree

app/services/video_service.py

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,7 @@ async def update_video_details(
383383

384384
async def record_video_view(
385385
video_id: VideoID,
386+
viewer_user_id: Optional[UUID] = None,
386387
db_table: Optional[AstraDBCollection] = None,
387388
) -> None:
388389
"""Increment the view counter stored directly in the *videos* table.
@@ -449,19 +450,37 @@ async def record_video_view(
449450
else:
450451
raise
451452

452-
# Log individual view event in the time-series activity table (unchanged)
453-
activity_table = await get_table(VIDEO_ACTIVITY_TABLE_NAME)
454-
now_utc = datetime.now(timezone.utc)
455-
day_partition = now_utc.strftime("%Y-%m-%d") # Cassandra date literal format
456-
457-
await activity_table.insert_one(
458-
{
459-
"videoid": _uuid_for_db(video_id, db_table),
460-
"day": day_partition,
461-
"watch_time": str(uuid1()), # time-based UUID for clustering order
462-
}
453+
from app.services.user_activity_service import (
454+
record_user_activity,
455+
ANONYMOUS_USER_ID,
463456
)
464457

458+
# Log individual view event in the time-series activity table (non-critical)
459+
try:
460+
activity_table = await get_table(VIDEO_ACTIVITY_TABLE_NAME)
461+
now_utc = datetime.now(timezone.utc)
462+
day_partition = now_utc.strftime("%Y-%m-%d") # Cassandra date literal format
463+
464+
await activity_table.insert_one(
465+
{
466+
"videoid": _uuid_for_db(video_id, db_table),
467+
"day": day_partition,
468+
"watch_time": str(uuid1()), # time-based UUID for clustering order
469+
}
470+
)
471+
except Exception:
472+
logger.warning("video_activity insert failed for view; ignoring", exc_info=True)
473+
474+
# Track in user_activity (never fail the view operation)
475+
try:
476+
effective_user_id = viewer_user_id if viewer_user_id else ANONYMOUS_USER_ID
477+
await record_user_activity(
478+
userid=effective_user_id,
479+
activity_type="view",
480+
)
481+
except Exception:
482+
logger.warning("user_activity insert failed for view; ignoring", exc_info=True)
483+
465484

466485
async def list_videos_with_query(
467486
query_filter: Dict[str, Any],

tests/services/test_video_service.py

Lines changed: 95 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ async def test_record_video_view_success():
243243
# but we already pass mock_stats_table, so get_table will be used only once
244244
mock_get_table.return_value = mock_activity_table
245245

246-
await video_service.record_video_view(vid, mock_stats_table)
246+
await video_service.record_video_view(vid, db_table=mock_stats_table)
247247

248248
# Validate stats table increment
249249
mock_stats_table.update_one.assert_called_once_with(
@@ -254,6 +254,95 @@ async def test_record_video_view_success():
254254
mock_activity_table.insert_one.assert_called_once()
255255

256256

257+
@pytest.mark.asyncio
258+
async def test_record_video_view_authenticated_user_activity():
259+
"""Authenticated view calls record_user_activity with real user ID."""
260+
vid = uuid4()
261+
viewer_id = uuid4()
262+
mock_stats_table = AsyncMock()
263+
mock_activity_table = AsyncMock()
264+
265+
with (
266+
patch(
267+
"app.services.video_service.get_table", new_callable=AsyncMock
268+
) as mock_get_table,
269+
patch(
270+
"app.services.user_activity_service.record_user_activity",
271+
new_callable=AsyncMock,
272+
) as mock_record_user_activity,
273+
):
274+
mock_get_table.return_value = mock_activity_table
275+
276+
await video_service.record_video_view(
277+
vid, viewer_user_id=viewer_id, db_table=mock_stats_table
278+
)
279+
280+
# record_user_activity should be called with the real user ID
281+
mock_record_user_activity.assert_awaited_once_with(
282+
userid=viewer_id,
283+
activity_type="view",
284+
)
285+
286+
287+
@pytest.mark.asyncio
288+
async def test_record_video_view_anonymous_user_activity():
289+
"""Anonymous view calls record_user_activity with nil UUID sentinel."""
290+
from app.services.user_activity_service import ANONYMOUS_USER_ID
291+
292+
vid = uuid4()
293+
mock_stats_table = AsyncMock()
294+
mock_activity_table = AsyncMock()
295+
296+
with (
297+
patch(
298+
"app.services.video_service.get_table", new_callable=AsyncMock
299+
) as mock_get_table,
300+
patch(
301+
"app.services.user_activity_service.record_user_activity",
302+
new_callable=AsyncMock,
303+
) as mock_record_user_activity,
304+
):
305+
mock_get_table.return_value = mock_activity_table
306+
307+
await video_service.record_video_view(
308+
vid, viewer_user_id=None, db_table=mock_stats_table
309+
)
310+
311+
# record_user_activity should be called with the anonymous sentinel UUID
312+
mock_record_user_activity.assert_awaited_once_with(
313+
userid=ANONYMOUS_USER_ID,
314+
activity_type="view",
315+
)
316+
317+
318+
@pytest.mark.asyncio
319+
async def test_record_video_view_user_activity_failure_does_not_break():
320+
"""If record_user_activity raises, the video view still succeeds."""
321+
vid = uuid4()
322+
mock_stats_table = AsyncMock()
323+
mock_activity_table = AsyncMock()
324+
325+
with (
326+
patch(
327+
"app.services.video_service.get_table", new_callable=AsyncMock
328+
) as mock_get_table,
329+
patch(
330+
"app.services.user_activity_service.record_user_activity",
331+
new_callable=AsyncMock,
332+
) as mock_record_user_activity,
333+
):
334+
mock_get_table.return_value = mock_activity_table
335+
mock_record_user_activity.side_effect = Exception("DB error")
336+
337+
# Should NOT raise despite user_activity failure
338+
await video_service.record_video_view(
339+
vid, viewer_user_id=uuid4(), db_table=mock_stats_table
340+
)
341+
342+
# video_activity insert still happened
343+
mock_activity_table.insert_one.assert_called_once()
344+
345+
257346
# ------------------------------------------------------------
258347
# list_latest_videos (delegate to generic) – just verify query call
259348
# ------------------------------------------------------------
@@ -276,7 +365,7 @@ async def test_list_latest_videos():
276365
mock_list_with_query.assert_called_once_with(
277366
{},
278367
1,
279-
3,
368+
10,
280369
sort_options={"added_date": -1},
281370
db_table=mock_db,
282371
source_table_name=video_service.VIDEOS_TABLE_NAME,
@@ -293,20 +382,18 @@ async def test_list_latest_videos():
293382
@pytest.mark.asyncio
294383
async def test_search_videos_by_keyword():
295384
mock_db = AsyncMock()
296-
mock_db.find.return_value = []
297-
mock_db.count_documents.return_value = 0
298385

299386
with patch(
300-
"app.services.video_service.list_videos_with_query",
387+
"app.services.video_service.search_videos_by_semantic",
301388
new_callable=AsyncMock,
302-
) as mock_list_with_query:
303-
mock_list_with_query.return_value = ([], 0)
389+
) as mock_semantic:
390+
mock_semantic.return_value = ([], 0)
304391

305392
summaries, total = await video_service.search_videos_by_keyword(
306393
query="test", page=1, page_size=10, db_table=mock_db
307394
)
308395

309-
mock_list_with_query.assert_called_once()
396+
mock_semantic.assert_called_once()
310397
assert summaries == []
311398
assert total == 0
312399

0 commit comments

Comments
 (0)