We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/YusukeYoshiyama/google-calendar-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server
import pytest
import os
from unittest.mock import Mock, patch, MagicMock
from datetime import datetime, timedelta, timezone
from googleapiclient.errors import HttpError
from src.tools.calendar_tools import (
list_calendars,
get_events,
create_event,
delete_event
)
# 実際のAPIを使用するかどうかの判定
USE_REAL_API = os.getenv('USE_REAL_API', 'false').lower() in ('true', '1', 'yes')
class TestListCalendars:
@patch('src.tools.calendar_tools.get_service')
def test_list_calendars_success(self, mock_get_service):
# モックのセットアップ
mock_service = Mock()
mock_calendar_list = Mock()
mock_calendar_list.list.return_value.execute.return_value = {
'items': [
{
'id': 'calendar1@example.com',
'summary': 'Test Calendar 1',
'description': 'Test description 1',
'timeZone': 'Asia/Tokyo',
'accessRole': 'owner'
},
{
'id': 'calendar2@example.com',
'summary': 'Test Calendar 2',
'description': '',
'timeZone': 'UTC',
'accessRole': 'reader'
}
]
}
mock_service.calendarList.return_value = mock_calendar_list
mock_get_service.return_value = mock_service
# テスト実行
result = list_calendars()
# 検証
assert result['success'] is True
assert result['count'] == 2
assert len(result['calendars']) == 2
calendar1 = result['calendars'][0]
assert calendar1['id'] == 'calendar1@example.com'
assert calendar1['summary'] == 'Test Calendar 1'
assert calendar1['description'] == 'Test description 1'
assert calendar1['timeZone'] == 'Asia/Tokyo'
assert calendar1['accessRole'] == 'owner'
@patch('src.tools.calendar_tools.get_service')
def test_list_calendars_empty_result(self, mock_get_service):
# モックのセットアップ
mock_service = Mock()
mock_calendar_list = Mock()
mock_calendar_list.list.return_value.execute.return_value = {'items': []}
mock_service.calendarList.return_value = mock_calendar_list
mock_get_service.return_value = mock_service
# テスト実行
result = list_calendars()
# 検証
assert result['success'] is True
assert result['count'] == 0
assert result['calendars'] == []
@patch('src.tools.calendar_tools.get_service')
def test_list_calendars_http_error(self, mock_get_service):
# モックのセットアップ
mock_get_service.side_effect = HttpError(
resp=Mock(status=403),
content=b'Forbidden'
)
# テスト実行
result = list_calendars()
# 検証
assert result['success'] is False
assert 'error' in result
assert result['calendars'] == []
class TestGetEvents:
@patch('src.tools.calendar_tools.get_service')
def test_get_events_success(self, mock_get_service):
# モックのセットアップ
mock_service = Mock()
mock_events = Mock()
mock_events.list.return_value.execute.return_value = {
'items': [
{
'id': 'event1',
'summary': 'Test Event 1',
'description': 'Test description 1',
'start': {'dateTime': '2024-01-01T09:00:00Z'},
'end': {'dateTime': '2024-01-01T10:00:00Z'},
'location': 'Test Location',
'attendees': [
{'email': 'user1@example.com'},
{'email': 'user2@example.com'}
]
}
]
}
mock_service.events.return_value = mock_events
mock_get_service.return_value = mock_service
# テスト実行
result = get_events()
# 検証
assert result['success'] is True
assert result['count'] == 1
assert len(result['events']) == 1
event = result['events'][0]
assert event['id'] == 'event1'
assert event['summary'] == 'Test Event 1'
assert event['description'] == 'Test description 1'
assert event['start'] == '2024-01-01T09:00:00Z'
assert event['end'] == '2024-01-01T10:00:00Z'
assert event['location'] == 'Test Location'
assert event['attendees'] == ['user1@example.com', 'user2@example.com']
@patch('src.tools.calendar_tools.get_service')
def test_get_events_with_custom_params(self, mock_get_service):
# モックのセットアップ
mock_service = Mock()
mock_events = Mock()
mock_events.list.return_value.execute.return_value = {'items': []}
mock_service.events.return_value = mock_events
mock_get_service.return_value = mock_service
# テスト実行
result = get_events(
calendar_id='custom@example.com',
time_min='2024-01-01T00:00:00Z',
time_max='2024-01-02T00:00:00Z',
max_results=5
)
# 検証
assert result['success'] is True
mock_events.list.assert_called_once_with(
calendarId='custom@example.com',
timeMin='2024-01-01T00:00:00Z',
timeMax='2024-01-02T00:00:00Z',
maxResults=5,
singleEvents=True,
orderBy='startTime'
)
@patch('src.tools.calendar_tools.get_service')
def test_get_events_date_only_event(self, mock_get_service):
# モックのセットアップ
mock_service = Mock()
mock_events = Mock()
mock_events.list.return_value.execute.return_value = {
'items': [
{
'id': 'event1',
'summary': 'All Day Event',
'start': {'date': '2024-01-01'},
'end': {'date': '2024-01-02'},
}
]
}
mock_service.events.return_value = mock_events
mock_get_service.return_value = mock_service
# テスト実行
result = get_events()
# 検証
assert result['success'] is True
event = result['events'][0]
assert event['start'] == '2024-01-01'
assert event['end'] == '2024-01-02'
@patch('src.tools.calendar_tools.get_service')
def test_get_events_http_error(self, mock_get_service):
# モックのセットアップ
mock_get_service.side_effect = HttpError(
resp=Mock(status=404),
content=b'Not Found'
)
# テスト実行
result = get_events()
# 検証
assert result['success'] is False
assert 'error' in result
assert result['events'] == []
class TestCreateEvent:
@patch('src.tools.calendar_tools.get_service')
def test_create_event_success(self, mock_get_service):
# モックのセットアップ
mock_service = Mock()
mock_events = Mock()
mock_events.insert.return_value.execute.return_value = {
'id': 'created_event_id',
'htmlLink': 'https://calendar.google.com/event?eid=created_event_id'
}
mock_service.events.return_value = mock_events
mock_get_service.return_value = mock_service
# テスト実行
result = create_event(
summary='Test Event',
start_time='2024-01-01T09:00:00Z',
end_time='2024-01-01T10:00:00Z',
description='Test description',
location='Test location',
calendar_id='primary',
attendees=['user1@example.com', 'user2@example.com']
)
# 検証
assert result['success'] is True
assert result['event_id'] == 'created_event_id'
assert result['event_link'] == 'https://calendar.google.com/event?eid=created_event_id'
assert 'Test Event' in result['message']
# イベント作成時の引数を検証
mock_events.insert.assert_called_once()
call_args = mock_events.insert.call_args
assert call_args[1]['calendarId'] == 'primary'
event_body = call_args[1]['body']
assert event_body['summary'] == 'Test Event'
assert event_body['description'] == 'Test description'
assert event_body['location'] == 'Test location'
assert event_body['start']['dateTime'] == '2024-01-01T09:00:00Z'
assert event_body['end']['dateTime'] == '2024-01-01T10:00:00Z'
assert len(event_body['attendees']) == 2
@patch('src.tools.calendar_tools.get_service')
def test_create_event_minimal_params(self, mock_get_service):
# モックのセットアップ
mock_service = Mock()
mock_events = Mock()
mock_events.insert.return_value.execute.return_value = {
'id': 'minimal_event_id'
}
mock_service.events.return_value = mock_events
mock_get_service.return_value = mock_service
# テスト実行
result = create_event(
summary='Minimal Event',
start_time='2024-01-01T09:00:00Z',
end_time='2024-01-01T10:00:00Z'
)
# 検証
assert result['success'] is True
assert result['event_id'] == 'minimal_event_id'
# イベント作成時の引数を検証
call_args = mock_events.insert.call_args
event_body = call_args[1]['body']
assert event_body['summary'] == 'Minimal Event'
assert event_body['description'] == ''
assert event_body['location'] == ''
assert 'attendees' not in event_body
@patch('src.tools.calendar_tools.get_service')
def test_create_event_http_error(self, mock_get_service):
# モックのセットアップ
mock_get_service.side_effect = HttpError(
resp=Mock(status=400),
content=b'Bad Request'
)
# テスト実行
result = create_event(
summary='Test Event',
start_time='2024-01-01T09:00:00Z',
end_time='2024-01-01T10:00:00Z'
)
# 検証
assert result['success'] is False
assert 'error' in result
assert result['event_id'] is None
class TestDeleteEvent:
@patch('src.tools.calendar_tools.get_service')
def test_delete_event_success(self, mock_get_service):
# モックのセットアップ
mock_service = Mock()
mock_events = Mock()
mock_events.delete.return_value.execute.return_value = None
mock_service.events.return_value = mock_events
mock_get_service.return_value = mock_service
# テスト実行
result = delete_event('test_event_id')
# 検証
assert result['success'] is True
assert 'test_event_id' in result['message']
# 削除時の引数を検証
mock_events.delete.assert_called_once_with(
calendarId='primary',
eventId='test_event_id'
)
@patch('src.tools.calendar_tools.get_service')
def test_delete_event_custom_calendar(self, mock_get_service):
# モックのセットアップ
mock_service = Mock()
mock_events = Mock()
mock_events.delete.return_value.execute.return_value = None
mock_service.events.return_value = mock_events
mock_get_service.return_value = mock_service
# テスト実行
result = delete_event('test_event_id', 'custom@example.com')
# 検証
assert result['success'] is True
# 削除時の引数を検証
mock_events.delete.assert_called_once_with(
calendarId='custom@example.com',
eventId='test_event_id'
)
@patch('src.tools.calendar_tools.get_service')
def test_delete_event_http_error(self, mock_get_service):
# モックのセットアップ
mock_get_service.side_effect = HttpError(
resp=Mock(status=404),
content=b'Not Found'
)
# テスト実行
result = delete_event('nonexistent_event_id')
# 検証
assert result['success'] is False
assert 'error' in result
class TestIntegration:
@patch('src.tools.calendar_tools.get_service')
def test_workflow_create_and_delete_event(self, mock_get_service):
# モックのセットアップ
mock_service = Mock()
mock_events = Mock()
# 作成時のレスポンス
mock_events.insert.return_value.execute.return_value = {
'id': 'workflow_event_id',
'htmlLink': 'https://calendar.google.com/event?eid=workflow_event_id'
}
# 削除時のレスポンス
mock_events.delete.return_value.execute.return_value = None
mock_service.events.return_value = mock_events
mock_get_service.return_value = mock_service
# イベント作成
create_result = create_event(
summary='Workflow Test Event',
start_time='2024-01-01T09:00:00Z',
end_time='2024-01-01T10:00:00Z'
)
# 作成結果の検証
assert create_result['success'] is True
event_id = create_result['event_id']
# イベント削除
delete_result = delete_event(event_id)
# 削除結果の検証
assert delete_result['success'] is True
# 両方の操作が呼ばれたことを確認
mock_events.insert.assert_called_once()
mock_events.delete.assert_called_once()
# 実際のAPIを使用するテストクラス(環境変数で制御)
@pytest.mark.real_api
class TestRealAPI:
@pytest.fixture(autouse=True)
def check_real_api_mode(self):
if not (os.getenv('USE_REAL_API', 'false').lower() in ('true', '1', 'yes')):
pytest.skip("USE_REAL_API環境変数が設定されていません")
@pytest.mark.integration
def test_list_calendars_real_api(self):
result = list_calendars()
# 基本的な構造の検証
assert 'success' in result
assert 'calendars' in result
assert 'count' in result
if result['success']:
# 成功時の検証
assert isinstance(result['calendars'], list)
assert result['count'] == len(result['calendars'])
# カレンダー情報の構造を検証
for calendar in result['calendars']:
assert 'id' in calendar
assert 'summary' in calendar
assert isinstance(calendar['id'], str)
assert isinstance(calendar['summary'], str)
else:
# 失敗時の検証
assert 'error' in result
print(f"API Error: {result['error']}")
@pytest.mark.integration
def test_get_events_real_api(self):
result = get_events(max_results=5)
# 基本的な構造の検証
assert 'success' in result
assert 'events' in result
assert 'count' in result
if result['success']:
# 成功時の検証
assert isinstance(result['events'], list)
assert result['count'] == len(result['events'])
assert result['count'] <= 5 # max_resultsの制限
# イベント情報の構造を検証
for event in result['events']:
assert 'id' in event
assert 'summary' in event
assert 'start' in event
assert 'end' in event
assert isinstance(event['id'], str)
assert isinstance(event['summary'], str)
else:
# 失敗時の検証
assert 'error' in result
print(f"API Error: {result['error']}")
@pytest.mark.integration
def test_create_and_delete_event_real_api(self):
# テスト用のイベント情報
test_summary = f"Test Event {datetime.now().strftime('%Y%m%d_%H%M%S')}"
start_time = (datetime.now(timezone.utc) + timedelta(hours=1)).isoformat().replace('+00:00', 'Z')
end_time = (datetime.now(timezone.utc) + timedelta(hours=2)).isoformat().replace('+00:00', 'Z')
# イベント作成
create_result = create_event(
summary=test_summary,
start_time=start_time,
end_time=end_time,
description="Test event for API testing",
location="Test Location"
)
# 作成結果の検証
assert 'success' in create_result
if create_result['success']:
assert 'event_id' in create_result
event_id = create_result['event_id']
assert event_id is not None
# 作成されたイベントを確認
events_result = get_events()
if events_result['success']:
created_event = next(
(e for e in events_result['events'] if e['id'] == event_id),
None
)
assert created_event is not None, "作成したイベントが見つかりません"
assert created_event['summary'] == test_summary
# イベント削除
delete_result = delete_event(event_id)
assert 'success' in delete_result
if delete_result['success']:
print(f"イベント '{test_summary}' の作成・削除が正常に完了しました")
else:
print(f"イベント削除エラー: {delete_result.get('error', 'Unknown error')}")
else:
print(f"イベント作成エラー: {create_result.get('error', 'Unknown error')}")
@pytest.mark.integration
def test_get_events_with_time_range_real_api(self):
# 過去1週間から未来1週間のイベントを取得
time_min = (datetime.now(timezone.utc) - timedelta(days=7)).isoformat().replace('+00:00', 'Z')
time_max = (datetime.now(timezone.utc) + timedelta(days=7)).isoformat().replace('+00:00', 'Z')
result = get_events(
time_min=time_min,
time_max=time_max,
max_results=10
)
# 基本的な構造の検証
assert 'success' in result
assert 'events' in result
if result['success']:
print(f"時間範囲指定で {result['count']} 件のイベントを取得しました")
# イベントの時間範囲を検証(タイムゾーンを考慮)
min_datetime = datetime.fromisoformat(time_min.replace('Z', '+00:00'))
max_datetime = datetime.fromisoformat(time_max.replace('Z', '+00:00'))
validated_events = 0
for event in result['events']:
if 'start' in event and event['start']:
event_start = event['start']
if 'T' in event_start: # 時刻指定のイベント
try:
# タイムゾーン情報を適切に処理
if event_start.endswith('Z'):
event_datetime = datetime.fromisoformat(event_start.replace('Z', '+00:00'))
elif '+' in event_start or event_start.count('-') > 2:
# 既にタイムゾーン情報がある場合
event_datetime = datetime.fromisoformat(event_start)
else:
# タイムゾーン情報がない場合はUTCとして扱う
event_datetime = datetime.fromisoformat(event_start + '+00:00')
# 時間範囲の検証(タイムゾーンを考慮して比較)
# UTCに変換して比較
event_utc = event_datetime.astimezone(timezone.utc)
min_utc = min_datetime.astimezone(timezone.utc)
max_utc = max_datetime.astimezone(timezone.utc)
# より柔軟な時間範囲チェック(1分のマージンを設ける)
margin = timedelta(minutes=1)
if event_utc >= (min_utc - margin) and event_utc <= (max_utc + margin):
validated_events += 1
else:
print(f"警告: イベント '{event.get('summary', 'No Title')}' の開始時刻 {event_start} が指定された時間範囲外です")
except (ValueError, TypeError) as e:
# 日付解析エラーの場合は警告を出力してスキップ
print(f"警告: イベントの開始時刻 '{event_start}' の解析に失敗しました: {e}")
continue
# 検証可能なイベントがあることを確認
if validated_events > 0:
print(f"{validated_events} 件のイベントの時間範囲を正常に検証しました")
else:
print("警告: 検証可能なイベントが見つかりませんでした")
else:
print(f"時間範囲指定でのイベント取得エラー: {result.get('error', 'Unknown error')}")
@pytest.mark.integration
def test_create_event_with_attendees_real_api(self):
# テスト用のイベント情報
test_summary = f"Meeting Test {datetime.now().strftime('%Y%m%d_%H%M%S')}"
start_time = (datetime.now(timezone.utc) + timedelta(hours=1)).isoformat().replace('+00:00', 'Z')
end_time = (datetime.now(timezone.utc) + timedelta(hours=1, minutes=30)).isoformat().replace('+00:00', 'Z')
# 参加者付きイベント作成
create_result = create_event(
summary=test_summary,
start_time=start_time,
end_time=end_time,
description="Test meeting with attendees",
attendees=['test1@example.com', 'test2@example.com']
)
# 作成結果の検証
assert 'success' in create_result
if create_result['success']:
event_id = create_result['event_id']
print(f"参加者付きイベント '{test_summary}' を作成しました (ID: {event_id})")
# 作成されたイベントを確認
events_result = get_events()
if events_result['success']:
created_event = next(
(e for e in events_result['events'] if e['id'] == event_id),
None
)
if created_event:
print(f"参加者: {created_event.get('attendees', [])}")
# テスト用イベントを削除
delete_result = delete_event(event_id)
if delete_result['success']:
print(f"テスト用イベントを削除しました")
else:
print(f"参加者付きイベント作成エラー: {create_result.get('error', 'Unknown error')}")