diff --git a/api/routes/results_v2.py b/api/routes/results_v2.py index d3dfe35..340886f 100644 --- a/api/routes/results_v2.py +++ b/api/routes/results_v2.py @@ -3,6 +3,8 @@ from fastapi import APIRouter, Query, Depends from typing import Optional, Literal import json +import os +from datetime import datetime, timedelta from api.database import Database @@ -14,6 +16,65 @@ def get_database() -> Database: return Database() +def validate_and_resolve_dates( + start_date: Optional[str], + end_date: Optional[str] +) -> tuple[str, str]: + """Validate and resolve date parameters. + + Args: + start_date: Start date (YYYY-MM-DD) or None + end_date: End date (YYYY-MM-DD) or None + + Returns: + Tuple of (resolved_start_date, resolved_end_date) + + Raises: + ValueError: If dates are invalid + """ + # Default lookback days + default_lookback = int(os.getenv("DEFAULT_RESULTS_LOOKBACK_DAYS", "30")) + + # Handle None cases + if start_date is None and end_date is None: + # Default to last N days + end_dt = datetime.now() + start_dt = end_dt - timedelta(days=default_lookback) + return start_dt.strftime("%Y-%m-%d"), end_dt.strftime("%Y-%m-%d") + + if start_date is None: + # Only end_date provided -> single date + start_date = end_date + + if end_date is None: + # Only start_date provided -> single date + end_date = start_date + + # Validate date formats + try: + start_dt = datetime.strptime(start_date, "%Y-%m-%d") + end_dt = datetime.strptime(end_date, "%Y-%m-%d") + + # Ensure strict YYYY-MM-DD format (e.g., reject "2025-1-16") + if start_date != start_dt.strftime("%Y-%m-%d"): + raise ValueError(f"Invalid date format. Expected YYYY-MM-DD") + if end_date != end_dt.strftime("%Y-%m-%d"): + raise ValueError(f"Invalid date format. Expected YYYY-MM-DD") + except ValueError: + raise ValueError(f"Invalid date format. Expected YYYY-MM-DD") + + # Validate order + if start_dt > end_dt: + raise ValueError("start_date must be <= end_date") + + # Validate not future + now = datetime.now() + if start_dt.date() > now.date() or end_dt.date() > now.date(): + raise ValueError("Cannot query future dates") + + return start_date, end_date + + @router.get("/results") async def get_results( job_id: Optional[str] = None, diff --git a/tests/api/test_results_v2.py b/tests/api/test_results_v2.py new file mode 100644 index 0000000..6c5dfa8 --- /dev/null +++ b/tests/api/test_results_v2.py @@ -0,0 +1,61 @@ +"""Tests for results_v2 endpoint date validation.""" + +import pytest +from datetime import datetime, timedelta +from api.routes.results_v2 import validate_and_resolve_dates + + +def test_validate_no_dates_provided(): + """Test default to last 30 days when no dates provided.""" + start, end = validate_and_resolve_dates(None, None) + + # Should default to last 30 days + end_dt = datetime.strptime(end, "%Y-%m-%d") + start_dt = datetime.strptime(start, "%Y-%m-%d") + + assert (end_dt - start_dt).days == 30 + assert end_dt.date() <= datetime.now().date() + + +def test_validate_only_start_date(): + """Test single date when only start_date provided.""" + start, end = validate_and_resolve_dates("2025-01-16", None) + + assert start == "2025-01-16" + assert end == "2025-01-16" + + +def test_validate_only_end_date(): + """Test single date when only end_date provided.""" + start, end = validate_and_resolve_dates(None, "2025-01-16") + + assert start == "2025-01-16" + assert end == "2025-01-16" + + +def test_validate_both_dates(): + """Test date range when both provided.""" + start, end = validate_and_resolve_dates("2025-01-16", "2025-01-20") + + assert start == "2025-01-16" + assert end == "2025-01-20" + + +def test_validate_invalid_date_format(): + """Test error on invalid date format.""" + with pytest.raises(ValueError, match="Invalid date format"): + validate_and_resolve_dates("2025-1-16", "2025-01-20") + + +def test_validate_start_after_end(): + """Test error when start_date > end_date.""" + with pytest.raises(ValueError, match="start_date must be <= end_date"): + validate_and_resolve_dates("2025-01-20", "2025-01-16") + + +def test_validate_future_date(): + """Test error when dates are in future.""" + future = (datetime.now() + timedelta(days=10)).strftime("%Y-%m-%d") + + with pytest.raises(ValueError, match="Cannot query future dates"): + validate_and_resolve_dates(future, future)