diff --git a/apps/epg/api_views.py b/apps/epg/api_views.py index 866b5968..5978965b 100644 --- a/apps/epg/api_views.py +++ b/apps/epg/api_views.py @@ -800,36 +800,56 @@ class ProgramSearchAPIView(APIView): desc_whole_words = params.get('description_whole_words', '').lower() in ('true', '1', 'yes') filters &= _parse_text_query('description', description, use_regex=desc_regex, whole_words=desc_whole_words) - # Time filters + # Time filters with validation start_after = params.get('start_after') if start_after: dt = parse_datetime(start_after) - if dt: - filters &= Q(start_time__gte=dt) + if dt is None: + return Response( + {"error": f"Invalid datetime format for start_after: {start_after}. Use ISO 8601 format (e.g., 2026-02-14T18:00:00Z)"}, + status=status.HTTP_400_BAD_REQUEST + ) + filters &= Q(start_time__gte=dt) start_before = params.get('start_before') if start_before: dt = parse_datetime(start_before) - if dt: - filters &= Q(start_time__lte=dt) + if dt is None: + return Response( + {"error": f"Invalid datetime format for start_before: {start_before}. Use ISO 8601 format."}, + status=status.HTTP_400_BAD_REQUEST + ) + filters &= Q(start_time__lte=dt) end_after = params.get('end_after') if end_after: dt = parse_datetime(end_after) - if dt: - filters &= Q(end_time__gte=dt) + if dt is None: + return Response( + {"error": f"Invalid datetime format for end_after: {end_after}. Use ISO 8601 format."}, + status=status.HTTP_400_BAD_REQUEST + ) + filters &= Q(end_time__gte=dt) end_before = params.get('end_before') if end_before: dt = parse_datetime(end_before) - if dt: - filters &= Q(end_time__lte=dt) + if dt is None: + return Response( + {"error": f"Invalid datetime format for end_before: {end_before}. Use ISO 8601 format."}, + status=status.HTTP_400_BAD_REQUEST + ) + filters &= Q(end_time__lte=dt) airing_at = params.get('airing_at') if airing_at: dt = parse_datetime(airing_at) - if dt: - filters &= Q(start_time__lte=dt, end_time__gt=dt) + if dt is None: + return Response( + {"error": f"Invalid datetime format for airing_at: {airing_at}. Use ISO 8601 format."}, + status=status.HTTP_400_BAD_REQUEST + ) + filters &= Q(start_time__lte=dt, end_time__gt=dt) # Channel/stream filters channel = params.get('channel') diff --git a/tests/test_epg_search_api.py b/tests/test_epg_search_api.py index 79769885..55db2508 100644 --- a/tests/test_epg_search_api.py +++ b/tests/test_epg_search_api.py @@ -244,8 +244,10 @@ class TestEPGSearchAPI: dt = datetime.fromisoformat(test_timestamp.replace("Z", "+00:00")) # Create time window: +/- 2 hours - start_after = (dt - timedelta(hours=2)).isoformat() + "Z" - start_before = (dt + timedelta(hours=2)).isoformat() + "Z" + start_after_dt = dt - timedelta(hours=2) + start_before_dt = dt + timedelta(hours=2) + start_after = start_after_dt.isoformat().replace("+00:00", "Z") + start_before = start_before_dt.isoformat().replace("+00:00", "Z") result = api_client.search_programs({ "start_after": start_after, @@ -254,12 +256,16 @@ class TestEPGSearchAPI: }) assert "results" in result + assert isinstance(result["results"], list) - # Verify programs are within range + # Verify all returned programs are within the requested range + # If no programs exist in this range, results should be empty (count=0) for program in result["results"]: - start = program["start_time"] - assert start >= start_after - assert start <= start_before + # Parse timestamp for comparison + start_dt = datetime.fromisoformat(program["start_time"].replace("Z", "+00:00")) + # Programs must be within the filtered range + assert start_dt >= start_after_dt, f"Program starts at {start_dt}, before filter start {start_after_dt}" + assert start_dt <= start_before_dt, f"Program starts at {start_dt}, after filter end {start_before_dt}" def test_channel_filter(self, api_client): """Test channel name filter"""