test_calendar_tools.py•23.7 kB
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')}")