diff --git a/src/time/test/time_server_test.py b/src/time/test/time_server_test.py new file mode 100644 index 00000000..74313c78 --- /dev/null +++ b/src/time/test/time_server_test.py @@ -0,0 +1,495 @@ +import os + +from freezegun import freeze_time +import pytest + +from mcp_server_time.server import TimeServer + + +@pytest.mark.parametrize( + "test_time,timezone,expected", + [ + # UTC+1 non-DST + ( + "2024-01-01 12:00:00+00:00", + "Europe/Warsaw", + { + "timezone": "Europe/Warsaw", + "datetime": "2024-01-01T13:00:00+01:00", + "is_dst": False, + }, + ), + # UTC non-DST + ( + "2024-01-01 12:00:00+00:00", + "Europe/London", + { + "timezone": "Europe/London", + "datetime": "2024-01-01T12:00:00+00:00", + "is_dst": False, + }, + ), + # UTC-5 non-DST + ( + "2024-01-01 12:00:00-00:00", + "America/New_York", + { + "timezone": "America/New_York", + "datetime": "2024-01-01T07:00:00-05:00", + "is_dst": False, + }, + ), + # UTC+1 DST + ( + "2024-03-31 12:00:00+00:00", + "Europe/Warsaw", + { + "timezone": "Europe/Warsaw", + "datetime": "2024-03-31T14:00:00+02:00", + "is_dst": True, + }, + ), + # UTC DST + ( + "2024-03-31 12:00:00+00:00", + "Europe/London", + { + "timezone": "Europe/London", + "datetime": "2024-03-31T13:00:00+01:00", + "is_dst": True, + }, + ), + # UTC-5 DST + ( + "2024-03-31 12:00:00-00:00", + "America/New_York", + { + "timezone": "America/New_York", + "datetime": "2024-03-31T08:00:00-04:00", + "is_dst": True, + }, + ), + ], +) +def test_get_current_time(test_time, timezone, expected): + with freeze_time(test_time): + time_server = TimeServer() + result = time_server.get_current_time(timezone) + assert result.timezone == expected["timezone"] + assert result.datetime == expected["datetime"] + assert result.is_dst == expected["is_dst"] + + +def test_get_current_time_with_invalid_timezone(): + time_server = TimeServer() + with pytest.raises(ValueError, match=r"Unknown timezone: 'Invalid/Timezone'"): + time_server.get_current_time("Invalid/Timezone") + + +@pytest.mark.parametrize( + "source_tz,time_str,target_tz,expected_error", + [ + ( + "invalid_tz", + "12:00", + "Europe/London", + "Unknown source timezone: 'invalid_tz'", + ), + ( + "Europe/Warsaw", + "12:00", + "invalid_tz", + "Unknown target timezone: 'invalid_tz'", + ), + ( + "Europe/Warsaw", + "25:00", + "Europe/London", + "Invalid time format. Expected HH:MM [24-hour format]", + ), + ], +) +def test_convert_time_errors(source_tz, time_str, target_tz, expected_error): + time_server = TimeServer() + with pytest.raises(ValueError, match=expected_error): + time_server.convert_time(source_tz, time_str, target_tz) + + +@pytest.mark.parametrize( + "test_time,source_tz,time_str,target_tz,expected", + [ + # Basic case: Standard time conversion between Warsaw and London (1 hour difference) + # Warsaw is UTC+1, London is UTC+0 + ( + "2024-01-01 00:00:00+00:00", + "Europe/Warsaw", + "12:00", + "Europe/London", + { + "source": { + "timezone": "Europe/Warsaw", + "datetime": "2024-01-01T12:00:00+01:00", + "is_dst": False, + }, + "target": { + "timezone": "Europe/London", + "datetime": "2024-01-01T11:00:00+00:00", + "is_dst": False, + }, + "time_difference": "-1.0h", + }, + ), + # Reverse case of above: London to Warsaw conversion + # Shows how time difference is positive when going east + ( + "2024-01-01 00:00:00+00:00", + "Europe/London", + "12:00", + "Europe/Warsaw", + { + "source": { + "timezone": "Europe/London", + "datetime": "2024-01-01T12:00:00+00:00", + "is_dst": False, + }, + "target": { + "timezone": "Europe/Warsaw", + "datetime": "2024-01-01T13:00:00+01:00", + "is_dst": False, + }, + "time_difference": "+1.0h", + }, + ), + # Edge case: Different DST periods between Europe and USA + # Europe ends DST on Oct 27, while USA waits until Nov 3 + # This creates a one-week period where Europe is in standard time but USA still observes DST + ( + "2024-10-28 00:00:00+00:00", + "Europe/Warsaw", + "12:00", + "America/New_York", + { + "source": { + "timezone": "Europe/Warsaw", + "datetime": "2024-10-28T12:00:00+01:00", + "is_dst": False, + }, + "target": { + "timezone": "America/New_York", + "datetime": "2024-10-28T07:00:00-04:00", + "is_dst": True, + }, + "time_difference": "-5.0h", + }, + ), + # Follow-up to previous case: After both regions end DST + # Shows how time difference increases by 1 hour when USA also ends DST + ( + "2024-11-04 00:00:00+00:00", + "Europe/Warsaw", + "12:00", + "America/New_York", + { + "source": { + "timezone": "Europe/Warsaw", + "datetime": "2024-11-04T12:00:00+01:00", + "is_dst": False, + }, + "target": { + "timezone": "America/New_York", + "datetime": "2024-11-04T06:00:00-05:00", + "is_dst": False, + }, + "time_difference": "-6.0h", + }, + ), + # Edge case: Nepal's unusual UTC+5:45 offset + # One of the few time zones using 45-minute offset + ( + "2024-01-01 00:00:00+00:00", + "Europe/Warsaw", + "12:00", + "Asia/Kathmandu", + { + "source": { + "timezone": "Europe/Warsaw", + "datetime": "2024-01-01T12:00:00+01:00", + "is_dst": False, + }, + "target": { + "timezone": "Asia/Kathmandu", + "datetime": "2024-01-01T16:45:00+05:45", + "is_dst": False, + }, + "time_difference": "+4.75h", + }, + ), + # Reverse case for Nepal + # Demonstrates how 45-minute offset works in opposite direction + ( + "2024-01-01 00:00:00+00:00", + "Asia/Kathmandu", + "12:00", + "Europe/Warsaw", + { + "source": { + "timezone": "Asia/Kathmandu", + "datetime": "2024-01-01T12:00:00+05:45", + "is_dst": False, + }, + "target": { + "timezone": "Europe/Warsaw", + "datetime": "2024-01-01T07:15:00+01:00", + "is_dst": False, + }, + "time_difference": "-4.75h", + }, + ), + # Edge case: Lord Howe Island's unique DST rules + # One of the few places using 30-minute DST shift + # During summer (DST), they use UTC+11 + ( + "2024-01-01 00:00:00+00:00", + "Europe/Warsaw", + "12:00", + "Australia/Lord_Howe", + { + "source": { + "timezone": "Europe/Warsaw", + "datetime": "2024-01-01T12:00:00+01:00", + "is_dst": False, + }, + "target": { + "timezone": "Australia/Lord_Howe", + "datetime": "2024-01-01T22:00:00+11:00", + "is_dst": True, + }, + "time_difference": "+10.0h", + }, + ), + # Second Lord Howe Island case: During their standard time + # Shows transition to UTC+10:30 after DST ends + ( + "2024-04-07 00:00:00+00:00", + "Europe/Warsaw", + "12:00", + "Australia/Lord_Howe", + { + "source": { + "timezone": "Europe/Warsaw", + "datetime": "2024-04-07T12:00:00+02:00", + "is_dst": True, + }, + "target": { + "timezone": "Australia/Lord_Howe", + "datetime": "2024-04-07T20:30:00+10:30", + "is_dst": False, + }, + "time_difference": "+8.5h", + }, + ), + # Edge case: Date line crossing with Samoa + # Demonstrates how a single time conversion can result in a date change + # Samoa is UTC+13, creating almost a full day difference with Warsaw + ( + "2024-01-01 00:00:00+00:00", + "Europe/Warsaw", + "23:00", + "Pacific/Apia", + { + "source": { + "timezone": "Europe/Warsaw", + "datetime": "2024-01-01T23:00:00+01:00", + "is_dst": False, + }, + "target": { + "timezone": "Pacific/Apia", + "datetime": "2024-01-02T11:00:00+13:00", + "is_dst": False, + }, + "time_difference": "+12.0h", + }, + ), + # Edge case: Iran's unusual half-hour offset + # Demonstrates conversion with Iran's UTC+3:30 timezone + ( + "2024-03-21 00:00:00+00:00", + "Europe/Warsaw", + "12:00", + "Asia/Tehran", + { + "source": { + "timezone": "Europe/Warsaw", + "datetime": "2024-03-21T12:00:00+01:00", + "is_dst": False, + }, + "target": { + "timezone": "Asia/Tehran", + "datetime": "2024-03-21T14:30:00+03:30", + "is_dst": False, + }, + "time_difference": "+2.5h", + }, + ), + # Edge case: Venezuela's unusual -4:30 offset (historical) + # In 2016, Venezuela moved from -4:30 to -4:00 + # Useful for testing historical dates + ( + "2016-04-30 00:00:00+00:00", # Just before the change + "Europe/Warsaw", + "12:00", + "America/Caracas", + { + "source": { + "timezone": "Europe/Warsaw", + "datetime": "2016-04-30T12:00:00+02:00", + "is_dst": True, + }, + "target": { + "timezone": "America/Caracas", + "datetime": "2016-04-30T05:30:00-04:30", + "is_dst": False, + }, + "time_difference": "-6.5h", + }, + ), + # Edge case: Israel's variable DST + # Israel's DST changes don't follow a fixed pattern + # They often change dates year-to-year based on Hebrew calendar + ( + "2024-10-27 00:00:00+00:00", + "Europe/Warsaw", + "12:00", + "Asia/Jerusalem", + { + "source": { + "timezone": "Europe/Warsaw", + "datetime": "2024-10-27T12:00:00+01:00", + "is_dst": False, + }, + "target": { + "timezone": "Asia/Jerusalem", + "datetime": "2024-10-27T13:00:00+02:00", + "is_dst": False, + }, + "time_difference": "+1.0h", + }, + ), + # Edge case: Antarctica/Troll station + # Only timezone that uses UTC+0 in winter and UTC+2 in summer + # One of the few zones with exactly 2 hours DST difference + ( + "2024-03-31 00:00:00+00:00", + "Europe/Warsaw", + "12:00", + "Antarctica/Troll", + { + "source": { + "timezone": "Europe/Warsaw", + "datetime": "2024-03-31T12:00:00+02:00", + "is_dst": True, + }, + "target": { + "timezone": "Antarctica/Troll", + "datetime": "2024-03-31T12:00:00+02:00", + "is_dst": True, + }, + "time_difference": "+0.0h", + }, + ), + # Edge case: Kiribati date line anomaly + # After skipping Dec 31, 1994, eastern Kiribati is UTC+14 + # The furthest forward timezone in the world + ( + "2024-01-01 00:00:00+00:00", + "Europe/Warsaw", + "23:00", + "Pacific/Kiritimati", + { + "source": { + "timezone": "Europe/Warsaw", + "datetime": "2024-01-01T23:00:00+01:00", + "is_dst": False, + }, + "target": { + "timezone": "Pacific/Kiritimati", + "datetime": "2024-01-02T12:00:00+14:00", + "is_dst": False, + }, + "time_difference": "+13.0h", + }, + ), + # Edge case: Chatham Islands, New Zealand + # Uses unusual 45-minute offset AND observes DST + # UTC+12:45 in standard time, UTC+13:45 in DST + ( + "2024-01-01 00:00:00+00:00", + "Europe/Warsaw", + "12:00", + "Pacific/Chatham", + { + "source": { + "timezone": "Europe/Warsaw", + "datetime": "2024-01-01T12:00:00+01:00", + "is_dst": False, + }, + "target": { + "timezone": "Pacific/Chatham", + "datetime": "2024-01-02T00:45:00+13:45", + "is_dst": True, + }, + "time_difference": "+12.75h", + }, + ), + ], +) +def test_convert_time(test_time, source_tz, time_str, target_tz, expected): + with freeze_time(test_time): + time_server = TimeServer() + result = time_server.convert_time(source_tz, time_str, target_tz) + + assert result.source.timezone == expected["source"]["timezone"] + assert result.target.timezone == expected["target"]["timezone"] + assert result.source.datetime == expected["source"]["datetime"] + assert result.target.datetime == expected["target"]["datetime"] + assert result.source.is_dst == expected["source"]["is_dst"] + assert result.target.is_dst == expected["target"]["is_dst"] + assert result.time_difference == expected["time_difference"] + + +# @pytest.mark.anyio +# async def test_call_tool(mock_forecast_response): +# class Response(): +# def raise_for_status(self): +# pass + +# def json(self): +# return mock_forecast_response + +# class AsyncClient(): +# def __aenter__(self): +# return self + +# async def __aexit__(self, *exc_info): +# pass + +# async def get(self, *args, **kwargs): +# return Response() + +# with patch('httpx.AsyncClient', new=AsyncClient) as mock_client: +# result = await call_tool("get_forecast", {"city": "London", "days": 2}) + +# assert len(result) == 1 +# assert result[0].type == "text" +# forecast_data = json.loads(result[0].text) +# assert len(forecast_data) == 1 +# assert forecast_data[0]["temperature"] == 18.5 +# assert forecast_data[0]["conditions"] == "sunny" + + +# @pytest.mark.anyio +# async def test_list_tools(): +# tools = await list_tools() +# assert len(tools) == 1 +# assert tools[0].name == "get_forecast" +# assert "city" in tools[0].inputSchema["properties"]