diff --git a/tests/test_darksky_api.py b/tests/test_darksky_api.py index e55d340..48efc48 100644 --- a/tests/test_darksky_api.py +++ b/tests/test_darksky_api.py @@ -1,12 +1,16 @@ """Tests for Dark Sky Times calculations.""" -from datetime import date, datetime, timezone +from datetime import date, datetime, timedelta, timezone + +import pytest from accessisky.api.darksky import ( + DarkSkyClient, DarkSkyWindow, TwilightType, get_dark_sky_window, get_darkness_duration, + get_twilight_type, is_astronomical_darkness, ) @@ -27,6 +31,14 @@ def test_twilight_descriptions(self): for twilight in TwilightType: assert twilight.description + def test_sun_angle_ranges(self): + """Test sun angle ranges for each twilight type.""" + assert TwilightType.DAY.sun_angle_range == (0, 0) + assert TwilightType.CIVIL.sun_angle_range == (0, 6) + assert TwilightType.NAUTICAL.sun_angle_range == (6, 12) + assert TwilightType.ASTRONOMICAL.sun_angle_range == (12, 18) + assert TwilightType.NIGHT.sun_angle_range == (18, 90) + class TestDarkSkyWindow: """Tests for DarkSkyWindow dataclass.""" @@ -56,6 +68,17 @@ def test_str_representation(self): assert "22:30" in s or "10:30" in s # Depends on formatting assert "5" in s # Hours + def test_str_no_data(self): + """Test string representation with no data.""" + window = DarkSkyWindow( + date=date(2026, 6, 15), + darkness_begins=None, + darkness_ends=None, + darkness_duration_hours=0.0, + ) + + assert str(window) == "Dark Sky: No data available" + def test_is_currently_dark(self): """Test is_currently_dark method.""" window = DarkSkyWindow( @@ -77,6 +100,55 @@ def test_is_currently_dark(self): after = datetime(2026, 6, 16, 5, 0, tzinfo=timezone.utc) assert not window.is_currently_dark(after) + def test_time_until_darkness(self): + """Test time_until_darkness method for all paths.""" + window = DarkSkyWindow( + date=date(2026, 6, 15), + darkness_begins=datetime(2026, 6, 15, 22, 0, tzinfo=timezone.utc), + darkness_ends=datetime(2026, 6, 16, 4, 0, tzinfo=timezone.utc), + darkness_duration_hours=6.0, + ) + + before = datetime(2026, 6, 15, 20, 0, tzinfo=timezone.utc) + assert window.time_until_darkness(before) == timedelta(hours=2) + + during = datetime(2026, 6, 15, 22, 0, tzinfo=timezone.utc) + assert window.time_until_darkness(during) == timedelta(0) + + no_data = DarkSkyWindow( + date=date(2026, 6, 15), + darkness_begins=None, + darkness_ends=None, + darkness_duration_hours=0.0, + ) + assert no_data.time_until_darkness(before) is None + + def test_time_remaining(self): + """Test time_remaining method for all paths.""" + window = DarkSkyWindow( + date=date(2026, 6, 15), + darkness_begins=datetime(2026, 6, 15, 22, 0, tzinfo=timezone.utc), + darkness_ends=datetime(2026, 6, 16, 4, 0, tzinfo=timezone.utc), + darkness_duration_hours=6.0, + ) + + before = datetime(2026, 6, 15, 20, 0, tzinfo=timezone.utc) + assert window.time_remaining(before) == timedelta(hours=6) + + during = datetime(2026, 6, 16, 1, 0, tzinfo=timezone.utc) + assert window.time_remaining(during) == timedelta(hours=3) + + after = datetime(2026, 6, 16, 5, 0, tzinfo=timezone.utc) + assert window.time_remaining(after) == timedelta(0) + + no_data = DarkSkyWindow( + date=date(2026, 6, 15), + darkness_begins=datetime(2026, 6, 15, 22, 0, tzinfo=timezone.utc), + darkness_ends=None, + darkness_duration_hours=0.0, + ) + assert no_data.time_remaining(during) is None + class TestGetDarkSkyWindow: """Tests for get_dark_sky_window function.""" @@ -131,6 +203,19 @@ def test_no_darkness_polar_summer(self): assert window.darkness_duration_hours == 0 assert window.no_darkness_reason is not None + def test_no_twilight_non_polar(self): + """Test missing twilight data at non-polar latitude.""" + window = get_dark_sky_window( + latitude=40.0, + longitude=0, + target_date=date(2026, 6, 21), + astronomical_twilight_end=None, + astronomical_twilight_begin=None, + ) + + assert window.darkness_duration_hours == 0 + assert window.no_darkness_reason == "Twilight data not available" + class TestGetDarknessDuration: """Tests for get_darkness_duration function.""" @@ -186,3 +271,53 @@ def test_no_twilight_data(self): ) assert is_dark is False # Assume not dark if no data + + +class TestGetTwilightType: + """Tests for get_twilight_type function.""" + + def test_all_twilight_types(self): + """Test all twilight type return values.""" + assert get_twilight_type(1.0) == TwilightType.DAY + assert get_twilight_type(0.0) == TwilightType.DAY + assert get_twilight_type(-3.0) == TwilightType.CIVIL + assert get_twilight_type(-9.0) == TwilightType.NAUTICAL + assert get_twilight_type(-15.0) == TwilightType.ASTRONOMICAL + assert get_twilight_type(-25.0) == TwilightType.NIGHT + + +class TestDarkSkyClient: + """Tests for DarkSkyClient async wrapper methods.""" + + @pytest.mark.asyncio + async def test_get_dark_sky_window(self): + """Test async wrapper for get_dark_sky_window.""" + client = DarkSkyClient() + window = await client.get_dark_sky_window( + latitude=45.0, + longitude=-75.0, + target_date=date(2026, 3, 15), + astronomical_twilight_end=datetime(2026, 3, 15, 20, 30, tzinfo=timezone.utc), + astronomical_twilight_begin=datetime(2026, 3, 16, 5, 30, tzinfo=timezone.utc), + ) + + assert isinstance(window, DarkSkyWindow) + assert window.darkness_duration_hours > 0 + + @pytest.mark.asyncio + async def test_is_astronomical_darkness(self): + """Test async wrapper for is_astronomical_darkness.""" + client = DarkSkyClient() + is_dark = await client.is_astronomical_darkness( + check_time=datetime(2026, 3, 16, 1, 0, tzinfo=timezone.utc), + twilight_end=datetime(2026, 3, 15, 20, 0, tzinfo=timezone.utc), + twilight_begin=datetime(2026, 3, 16, 4, 0, tzinfo=timezone.utc), + ) + + assert is_dark is True + + @pytest.mark.asyncio + async def test_close(self): + """Test async close no-op.""" + client = DarkSkyClient() + assert await client.close() is None