"""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