Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 52 additions & 2 deletions backend/main.py
Original file line number Diff line number Diff line change
@@ -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
23 changes: 23 additions & 0 deletions backend/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
146 changes: 146 additions & 0 deletions backend/tests/test_resorts_endpoint.py
Original file line number Diff line number Diff line change
@@ -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&region=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() == []
Loading