7 tools: station search/nearest/info, tide predictions/observations, meteorological data (Literal selector for 8 products), and parallel marine conditions snapshot. 3 resources, 2 prompts, full test suite.
154 lines
4.2 KiB
Python
154 lines
4.2 KiB
Python
"""Test fixtures with mock NOAAClient injected via lifespan."""
|
|
|
|
from contextlib import asynccontextmanager
|
|
from unittest.mock import AsyncMock
|
|
|
|
import pytest
|
|
from fastmcp import Client, FastMCP
|
|
from fastmcp.client.transports import StreamableHttpTransport
|
|
from fastmcp.utilities.tests import run_server_async
|
|
|
|
from noaa_tides import prompts, resources
|
|
from noaa_tides.client import NOAAClient
|
|
from noaa_tides.tools import conditions, meteorological, stations, tides
|
|
|
|
# Realistic station fixtures
|
|
MOCK_STATIONS_RAW = [
|
|
{
|
|
"id": "8454000",
|
|
"name": "Providence",
|
|
"state": "RI",
|
|
"lat": 41.8071,
|
|
"lng": -71.4012,
|
|
"tidal": True,
|
|
"greatlakes": False,
|
|
"shefcode": "PRVD1",
|
|
},
|
|
{
|
|
"id": "8452660",
|
|
"name": "Newport",
|
|
"state": "RI",
|
|
"lat": 41.5043,
|
|
"lng": -71.3261,
|
|
"tidal": True,
|
|
"greatlakes": False,
|
|
"shefcode": "NWPR1",
|
|
},
|
|
{
|
|
"id": "8447930",
|
|
"name": "Woods Hole",
|
|
"state": "MA",
|
|
"lat": 41.5236,
|
|
"lng": -70.6714,
|
|
"tidal": True,
|
|
"greatlakes": False,
|
|
"shefcode": "WHOM3",
|
|
},
|
|
]
|
|
|
|
MOCK_PREDICTIONS = {
|
|
"predictions": [
|
|
{"t": "2026-02-21 04:30", "v": "4.521", "type": "H"},
|
|
{"t": "2026-02-21 10:42", "v": "-0.123", "type": "L"},
|
|
{"t": "2026-02-21 16:55", "v": "5.012", "type": "H"},
|
|
{"t": "2026-02-21 23:08", "v": "0.234", "type": "L"},
|
|
]
|
|
}
|
|
|
|
MOCK_WATER_LEVEL = {
|
|
"data": [
|
|
{"t": "2026-02-21 00:00", "v": "2.34", "s": "0.003", "f": "0,0,0,0", "q": "p"},
|
|
{"t": "2026-02-21 00:06", "v": "2.38", "s": "0.003", "f": "0,0,0,0", "q": "p"},
|
|
]
|
|
}
|
|
|
|
MOCK_WIND = {
|
|
"data": [
|
|
{
|
|
"t": "2026-02-21 00:00",
|
|
"s": "12.5",
|
|
"d": "225.00",
|
|
"dr": "SW",
|
|
"g": "18.2",
|
|
"f": "0,0",
|
|
}
|
|
]
|
|
}
|
|
|
|
MOCK_AIR_TEMP = {"data": [{"t": "2026-02-21 00:00", "v": "42.3", "f": "0,0,0"}]}
|
|
MOCK_WATER_TEMP = {"data": [{"t": "2026-02-21 00:00", "v": "38.7", "f": "0,0,0"}]}
|
|
MOCK_PRESSURE = {"data": [{"t": "2026-02-21 00:00", "v": "1013.2", "f": "0,0,0"}]}
|
|
|
|
MOCK_METADATA = {
|
|
"stations": [
|
|
{
|
|
"id": "8454000",
|
|
"name": "Providence",
|
|
"state": "RI",
|
|
"lat": 41.8071,
|
|
"lng": -71.4012,
|
|
"tidal": True,
|
|
"sensors": [{"name": "Water Level"}, {"name": "Air Temperature"}],
|
|
"products": {"self": "...", "tidePredictions": "..."},
|
|
"datums": {"datums": [{"name": "MLLW", "value": "0.0"}]},
|
|
}
|
|
]
|
|
}
|
|
|
|
|
|
def _build_mock_client() -> NOAAClient:
|
|
"""Build a NOAAClient with mocked HTTP but real search/nearest logic."""
|
|
from noaa_tides.models import Station
|
|
|
|
client = NOAAClient()
|
|
client._stations = [Station(**s) for s in MOCK_STATIONS_RAW]
|
|
client._cache_time = float("inf") # Never expires
|
|
client._http = AsyncMock()
|
|
|
|
async def mock_get_data(station_id, product, **kwargs):
|
|
responses = {
|
|
"predictions": MOCK_PREDICTIONS,
|
|
"water_level": MOCK_WATER_LEVEL,
|
|
"wind": MOCK_WIND,
|
|
"air_temperature": MOCK_AIR_TEMP,
|
|
"water_temperature": MOCK_WATER_TEMP,
|
|
"air_pressure": MOCK_PRESSURE,
|
|
}
|
|
if product in responses:
|
|
return responses[product]
|
|
raise ValueError(f"No data was found for product: {product}")
|
|
|
|
client.get_data = mock_get_data
|
|
|
|
async def mock_get_station_metadata(station_id):
|
|
return MOCK_METADATA["stations"][0]
|
|
|
|
client.get_station_metadata = mock_get_station_metadata
|
|
|
|
return client
|
|
|
|
|
|
@asynccontextmanager
|
|
async def _test_lifespan(server: FastMCP):
|
|
client = _build_mock_client()
|
|
yield {"noaa_client": client}
|
|
|
|
|
|
def _build_test_server() -> FastMCP:
|
|
mcp = FastMCP("noaa-tides-test", lifespan=_test_lifespan)
|
|
stations.register(mcp)
|
|
tides.register(mcp)
|
|
meteorological.register(mcp)
|
|
conditions.register(mcp)
|
|
resources.register(mcp)
|
|
prompts.register(mcp)
|
|
return mcp
|
|
|
|
|
|
@pytest.fixture
|
|
async def mcp_client():
|
|
server = _build_test_server()
|
|
async with run_server_async(server) as url:
|
|
async with Client(StreamableHttpTransport(url)) as client:
|
|
yield client
|