""" Unit tests for the per-tenant OAuth calendar_lookup tool. Tests: - Returns "not configured" message when no tenant_id provided - Returns "not connected" message when no ChannelConnection exists for tenant - action="list" calls Google Calendar API and returns formatted event list - action="check_availability" returns free/busy summary - action="create" creates an event and returns confirmation - Token refresh write-back: updated credentials written to DB - All responses are natural language strings (no raw JSON) - API errors return human-readable messages """ from __future__ import annotations import uuid from unittest.mock import AsyncMock, MagicMock, patch # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- _TENANT_ID = str(uuid.uuid4()) _DATE = "2026-03-26" # Fake encrypted token JSON stored in channel_connections.config _FAKE_ENCRYPTED_TOKEN = "gAAAAAB..." # Decrypted token dict (as would come from Google OAuth) _FAKE_TOKEN_DICT = { "token": "ya29.test_access_token", "refresh_token": "1//test_refresh_token", "token_uri": "https://oauth2.googleapis.com/token", "client_id": "test-client-id", "client_secret": "test-client-secret", "scopes": ["https://www.googleapis.com/auth/calendar"], } # Sample Google Calendar events response _FAKE_EVENTS = { "items": [ { "summary": "Team Standup", "start": {"dateTime": "2026-03-26T09:00:00+00:00"}, "end": {"dateTime": "2026-03-26T09:30:00+00:00"}, }, { "summary": "Sprint Planning", "start": {"dateTime": "2026-03-26T14:00:00+00:00"}, "end": {"dateTime": "2026-03-26T15:00:00+00:00"}, }, ] } # Common patch targets _PATCH_ENC = "orchestrator.tools.builtins.calendar_lookup.KeyEncryptionService" _PATCH_CREDS = "orchestrator.tools.builtins.calendar_lookup.google_credentials_from_token" _PATCH_BUILD = "orchestrator.tools.builtins.calendar_lookup.build" _PATCH_SETTINGS = "orchestrator.tools.builtins.calendar_lookup.settings" def _make_mock_session(conn_config: dict | None = None): """Build a mock AsyncSession that returns a ChannelConnection or None.""" session = AsyncMock() mock_result = MagicMock() if conn_config is not None: mock_conn = MagicMock() mock_conn.id = uuid.uuid4() mock_conn.config = conn_config mock_result.scalar_one_or_none.return_value = mock_conn else: mock_result.scalar_one_or_none.return_value = None session.execute.return_value = mock_result session.commit = AsyncMock() return session def _make_enc_mock(): """Create a mock KeyEncryptionService with decrypt returning the fake token JSON.""" import json mock_enc = MagicMock() mock_enc.decrypt.return_value = json.dumps(_FAKE_TOKEN_DICT) mock_enc.encrypt.return_value = "new_encrypted_token" return mock_enc def _make_mock_settings(): """Create mock settings with encryption key configured.""" mock_settings = MagicMock() mock_settings.platform_encryption_key = "test-key" mock_settings.platform_encryption_key_previous = "" return mock_settings # --------------------------------------------------------------------------- # No tenant_id # --------------------------------------------------------------------------- async def test_calendar_lookup_no_tenant_id_returns_message(): """calendar_lookup without tenant_id returns a helpful error message.""" from orchestrator.tools.builtins.calendar_lookup import calendar_lookup result = await calendar_lookup(date=_DATE) assert "tenant" in result.lower() or "not available" in result.lower() assert isinstance(result, str) # --------------------------------------------------------------------------- # Not connected # --------------------------------------------------------------------------- async def test_calendar_lookup_not_connected_returns_message(): """calendar_lookup with no ChannelConnection returns 'not connected' message.""" from orchestrator.tools.builtins.calendar_lookup import calendar_lookup mock_session = _make_mock_session(conn_config=None) # Pass _session directly to bypass DB session creation result = await calendar_lookup(date=_DATE, tenant_id=_TENANT_ID, _session=mock_session) assert "not connected" in result.lower() or "connect" in result.lower() assert isinstance(result, str) # --------------------------------------------------------------------------- # action="list" # --------------------------------------------------------------------------- async def test_calendar_lookup_list_returns_formatted_events(): """ action="list" returns a natural-language event list. No raw JSON — results are human-readable strings. """ from orchestrator.tools.builtins.calendar_lookup import calendar_lookup mock_session = _make_mock_session(conn_config={"token": _FAKE_ENCRYPTED_TOKEN}) mock_creds = MagicMock() mock_creds.token = "ya29.test_access_token" mock_creds.expired = False mock_creds.valid = True mock_service = MagicMock() mock_events_list = MagicMock() mock_events_list.execute.return_value = _FAKE_EVENTS mock_service.events.return_value.list.return_value = mock_events_list with ( patch(_PATCH_ENC) as mock_enc_cls, patch(_PATCH_CREDS) as mock_creds_fn, patch(_PATCH_BUILD) as mock_build, patch(_PATCH_SETTINGS, _make_mock_settings()), ): mock_enc_cls.return_value = _make_enc_mock() mock_creds_fn.return_value = mock_creds mock_build.return_value = mock_service result = await calendar_lookup( date=_DATE, action="list", tenant_id=_TENANT_ID, _session=mock_session, ) assert isinstance(result, str) assert "Team Standup" in result assert "Sprint Planning" in result # No raw JSON assert "{" not in result or "items" not in result async def test_calendar_lookup_list_no_events(): """action="list" with no events returns a 'no events' message.""" from orchestrator.tools.builtins.calendar_lookup import calendar_lookup mock_session = _make_mock_session(conn_config={"token": _FAKE_ENCRYPTED_TOKEN}) mock_creds = MagicMock() mock_creds.expired = False mock_creds.valid = True mock_service = MagicMock() mock_service.events.return_value.list.return_value.execute.return_value = {"items": []} with ( patch(_PATCH_ENC) as mock_enc_cls, patch(_PATCH_CREDS) as mock_creds_fn, patch(_PATCH_BUILD) as mock_build, patch(_PATCH_SETTINGS, _make_mock_settings()), ): mock_enc_cls.return_value = _make_enc_mock() mock_creds_fn.return_value = mock_creds mock_build.return_value = mock_service result = await calendar_lookup( date=_DATE, action="list", tenant_id=_TENANT_ID, _session=mock_session, ) assert "no event" in result.lower() or "free" in result.lower() # --------------------------------------------------------------------------- # action="check_availability" # --------------------------------------------------------------------------- async def test_calendar_lookup_check_availability_with_events(): """action="check_availability" returns busy slot summary when events exist.""" from orchestrator.tools.builtins.calendar_lookup import calendar_lookup mock_session = _make_mock_session(conn_config={"token": _FAKE_ENCRYPTED_TOKEN}) mock_creds = MagicMock() mock_creds.expired = False mock_creds.valid = True mock_service = MagicMock() mock_service.events.return_value.list.return_value.execute.return_value = _FAKE_EVENTS with ( patch(_PATCH_ENC) as mock_enc_cls, patch(_PATCH_CREDS) as mock_creds_fn, patch(_PATCH_BUILD) as mock_build, patch(_PATCH_SETTINGS, _make_mock_settings()), ): mock_enc_cls.return_value = _make_enc_mock() mock_creds_fn.return_value = mock_creds mock_build.return_value = mock_service result = await calendar_lookup( date=_DATE, action="check_availability", tenant_id=_TENANT_ID, _session=mock_session, ) assert isinstance(result, str) assert "busy" in result.lower() or "slot" in result.lower() or "standup" in result.lower() async def test_calendar_lookup_check_availability_free_day(): """action="check_availability" with no events returns 'entire day is free'.""" from orchestrator.tools.builtins.calendar_lookup import calendar_lookup mock_session = _make_mock_session(conn_config={"token": _FAKE_ENCRYPTED_TOKEN}) mock_creds = MagicMock() mock_creds.expired = False mock_creds.valid = True mock_service = MagicMock() mock_service.events.return_value.list.return_value.execute.return_value = {"items": []} with ( patch(_PATCH_ENC) as mock_enc_cls, patch(_PATCH_CREDS) as mock_creds_fn, patch(_PATCH_BUILD) as mock_build, patch(_PATCH_SETTINGS, _make_mock_settings()), ): mock_enc_cls.return_value = _make_enc_mock() mock_creds_fn.return_value = mock_creds mock_build.return_value = mock_service result = await calendar_lookup( date=_DATE, action="check_availability", tenant_id=_TENANT_ID, _session=mock_session, ) assert "free" in result.lower() # --------------------------------------------------------------------------- # action="create" # --------------------------------------------------------------------------- async def test_calendar_lookup_create_event(): """action="create" inserts an event and returns confirmation.""" from orchestrator.tools.builtins.calendar_lookup import calendar_lookup mock_session = _make_mock_session(conn_config={"token": _FAKE_ENCRYPTED_TOKEN}) mock_creds = MagicMock() mock_creds.expired = False mock_creds.valid = True created_event = { "id": "abc123", "summary": "Product Demo", "start": {"dateTime": "2026-03-26T10:00:00+00:00"}, "end": {"dateTime": "2026-03-26T11:00:00+00:00"}, } mock_service = MagicMock() mock_service.events.return_value.insert.return_value.execute.return_value = created_event with ( patch(_PATCH_ENC) as mock_enc_cls, patch(_PATCH_CREDS) as mock_creds_fn, patch(_PATCH_BUILD) as mock_build, patch(_PATCH_SETTINGS, _make_mock_settings()), ): mock_enc_cls.return_value = _make_enc_mock() mock_creds_fn.return_value = mock_creds mock_build.return_value = mock_service result = await calendar_lookup( date=_DATE, action="create", event_summary="Product Demo", event_start="2026-03-26T10:00:00+00:00", event_end="2026-03-26T11:00:00+00:00", tenant_id=_TENANT_ID, _session=mock_session, ) assert isinstance(result, str) assert "created" in result.lower() or "product demo" in result.lower() async def test_calendar_lookup_create_missing_fields(): """action="create" without event_summary returns an error message.""" from orchestrator.tools.builtins.calendar_lookup import calendar_lookup mock_session = _make_mock_session(conn_config={"token": _FAKE_ENCRYPTED_TOKEN}) mock_creds = MagicMock() mock_creds.expired = False mock_creds.valid = True mock_service = MagicMock() with ( patch(_PATCH_ENC) as mock_enc_cls, patch(_PATCH_CREDS) as mock_creds_fn, patch(_PATCH_BUILD) as mock_build, patch(_PATCH_SETTINGS, _make_mock_settings()), ): mock_enc_cls.return_value = _make_enc_mock() mock_creds_fn.return_value = mock_creds mock_build.return_value = mock_service result = await calendar_lookup( date=_DATE, action="create", # No event_summary, event_start, event_end tenant_id=_TENANT_ID, _session=mock_session, ) assert isinstance(result, str) assert "error" in result.lower() or "required" in result.lower() or "missing" in result.lower() # --------------------------------------------------------------------------- # Token refresh write-back # --------------------------------------------------------------------------- async def test_calendar_lookup_token_refresh_writeback(): """ When credentials.token changes after an API call (refresh occurred), the updated token should be encrypted and written back to channel_connections. """ from orchestrator.tools.builtins.calendar_lookup import calendar_lookup conn_id = uuid.uuid4() mock_session = _make_mock_session(conn_config={"token": _FAKE_ENCRYPTED_TOKEN}) # Get the mock connection to track updates mock_conn = mock_session.execute.return_value.scalar_one_or_none.return_value mock_conn.id = conn_id mock_conn.config = {"token": _FAKE_ENCRYPTED_TOKEN} # Credentials that change token after API call (simulating refresh) original_token = "ya29.original_token" refreshed_token = "ya29.refreshed_token" mock_creds = MagicMock() mock_creds.token = original_token mock_creds.refresh_token = "1//refresh_token" mock_creds.expired = False mock_creds.valid = True def simulate_api_call_that_refreshes(): """Simulate the side effect of token refresh during API call.""" mock_creds.token = refreshed_token return {"items": []} mock_service = MagicMock() mock_service.events.return_value.list.return_value.execute.side_effect = simulate_api_call_that_refreshes mock_enc = _make_enc_mock() with ( patch(_PATCH_ENC) as mock_enc_cls, patch(_PATCH_CREDS) as mock_creds_fn, patch(_PATCH_BUILD) as mock_build, patch(_PATCH_SETTINGS, _make_mock_settings()), ): mock_enc_cls.return_value = mock_enc mock_creds_fn.return_value = mock_creds mock_build.return_value = mock_service await calendar_lookup( date=_DATE, action="list", tenant_id=_TENANT_ID, _session=mock_session, ) # encrypt should have been called for write-back mock_enc.encrypt.assert_called() # session.commit should have been called to persist the updated token mock_session.commit.assert_called()