diff --git a/backend/main.py b/backend/main.py index e02e5d0..488e5c3 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,8 +1,58 @@ -from fastapi import FastAPI +from contextlib import asynccontextmanager +from fastapi import FastAPI, Query +from typing import Optional -app = FastAPI(title="Indy Explorer API") +from data import load_resorts +from models import ResortSummary + +_resorts: list[ResortSummary] = [] + + +@asynccontextmanager +async def lifespan(app: FastAPI): + global _resorts + _resorts = [ResortSummary(**r.model_dump()) for r in load_resorts()] + yield + + +app = FastAPI(title="Indy Explorer API", lifespan=lifespan) @app.get("/health") def health(): return {"status": "ok"} + + +@app.get("/resorts", response_model=list[ResortSummary]) +def get_resorts( + search: Optional[str] = Query(default=None), + region: list[str] = Query(default=[]), + country: Optional[str] = Query(default=None), + state: Optional[str] = Query(default=None), +): + results = _resorts + + if search: + term = search.lower() + results = [ + r + for r in results + if term in (r.name or '').lower() + or term in (r.city or '').lower() + or term in (r.state or '').lower() + or term in (r.country or '').lower() + ] + + if region: + region_set = {v.lower() for v in region} + results = [r for r in results if (r.region or '').lower() in region_set] + + if country: + c = country.lower() + results = [r for r in results if (r.country or '').lower() == c] + + if state: + s = state.lower() + results = [r for r in results if (r.state or '').lower() == s] + + return results diff --git a/backend/models.py b/backend/models.py index 5f8971f..9db6313 100644 --- a/backend/models.py +++ b/backend/models.py @@ -2,6 +2,29 @@ from pydantic import BaseModel +class ResortSummary(BaseModel): + """Lean projection returned by GET /resorts — fields needed for the map and table.""" + + resort_id: str + name: str + region: str + city: Optional[str] = None + state: Optional[str] = None + country: Optional[str] = None + latitude: Optional[float] = None + longitude: Optional[float] = None + reservation_status: str + indy_page: str + website: Optional[str] = None + is_allied: Optional[bool] = None + has_alpine: Optional[bool] = None + has_cross_country: Optional[bool] = None + vertical: Optional[float] = None + acres: Optional[float] = None + num_trails: Optional[float] = None + num_lifts: Optional[float] = None + + class Resort(BaseModel): resort_id: str name: str diff --git a/backend/tests/test_resorts_endpoint.py b/backend/tests/test_resorts_endpoint.py new file mode 100644 index 0000000..52bd39a --- /dev/null +++ b/backend/tests/test_resorts_endpoint.py @@ -0,0 +1,146 @@ +import sys +import os + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) + +import pytest +from fastapi.testclient import TestClient +from unittest.mock import patch + +from main import app +from models import ResortSummary + +FAKE_RESORTS = [ + ResortSummary( + resort_id='id-1', + name='Vail', + region='West', + city='Vail', + state='CO', + country='USA', + reservation_status='required', + indy_page='https://example.com/vail', + ), + ResortSummary( + resort_id='id-2', + name='Stowe', + region='Northeast', + city='Stowe', + state='VT', + country='USA', + reservation_status='none', + indy_page='https://example.com/stowe', + ), + ResortSummary( + resort_id='id-3', + name='Tremblant', + region='Northeast', + city='Mont-Tremblant', + state=None, + country='Canada', + reservation_status='none', + indy_page='https://example.com/tremblant', + ), +] + + +@pytest.fixture(autouse=True) +def patch_resorts(): + with patch('main._resorts', FAKE_RESORTS): + yield + + +@pytest.fixture +def client(): + return TestClient(app) + + +def test_get_resorts_returns_all(client): + response = client.get('/resorts') + assert response.status_code == 200 + data = response.json() + assert len(data) == 3 + + +def test_get_resorts_response_shape(client): + response = client.get('/resorts') + resort = response.json()[0] + assert 'resort_id' in resort + assert 'name' in resort + assert 'region' in resort + assert 'latitude' in resort + assert 'longitude' in resort + + +def test_search_by_name(client): + response = client.get('/resorts?search=vail') + assert response.status_code == 200 + data = response.json() + assert len(data) == 1 + assert data[0]['name'] == 'Vail' + + +def test_search_by_state(client): + response = client.get('/resorts?search=vt') + assert response.status_code == 200 + data = response.json() + assert len(data) == 1 + assert data[0]['name'] == 'Stowe' + + +def test_search_by_country(client): + response = client.get('/resorts?search=canada') + assert response.status_code == 200 + data = response.json() + assert len(data) == 1 + assert data[0]['name'] == 'Tremblant' + + +def test_search_is_case_insensitive(client): + response = client.get('/resorts?search=STOWE') + assert response.status_code == 200 + assert len(response.json()) == 1 + + +def test_filter_single_region(client): + response = client.get('/resorts?region=West') + assert response.status_code == 200 + data = response.json() + assert len(data) == 1 + assert data[0]['name'] == 'Vail' + + +def test_filter_multiple_regions(client): + response = client.get('/resorts?region=West®ion=Northeast') + assert response.status_code == 200 + assert len(response.json()) == 3 + + +def test_filter_by_country(client): + response = client.get('/resorts?country=Canada') + assert response.status_code == 200 + data = response.json() + assert len(data) == 1 + assert data[0]['name'] == 'Tremblant' + + +def test_filter_by_state(client): + response = client.get('/resorts?state=CO') + assert response.status_code == 200 + data = response.json() + assert len(data) == 1 + assert data[0]['name'] == 'Vail' + + +def test_combined_filters(client): + response = client.get('/resorts?region=Northeast&country=USA') + assert response.status_code == 200 + data = response.json() + assert len(data) == 1 + assert data[0]['name'] == 'Stowe' + + +def test_no_match_returns_empty(client): + response = client.get('/resorts?search=zzznomatch') + assert response.status_code == 200 + assert response.json() == []