You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

138 lines
5.0 KiB
Python

# backend/tests/test_phase16_calendar.py — phase-16 Outlook/M365 캘린더 OAuth + Graph sync
import time
import pytest
from sqlmodel import select
from app.config import get_settings
from app.connectors import oauth
from app.connectors.calendar import real_outlook
from app.connectors.calendar.real_outlook import OutlookCalendarConnector
from app.connectors.registry import ConnectorRegistry, _impl
from app.crypto import encrypt_token
from app.models import CalEvent, ConnectorAccount, ConnectorDomain, ConnectorMode, ConnState
@pytest.fixture()
def ms_creds(monkeypatch):
monkeypatch.setenv("MICROSOFT_CLIENT_ID", "m-cid")
monkeypatch.setenv("MICROSOFT_CLIENT_SECRET", "m-sec")
get_settings.cache_clear()
yield
get_settings.cache_clear()
class _Resp:
def __init__(self, payload, status=200):
self._p = payload
self.status_code = status
self.headers = {}
def json(self):
return self._p
def raise_for_status(self):
if self.status_code >= 400:
raise RuntimeError(f"http {self.status_code}")
class _Client:
"""httpx.Client 컨텍스트매니저 스텁 — 페이지를 순서대로 반환."""
def __init__(self, pages):
self._pages = list(pages)
self.calls = []
def __enter__(self):
return self
def __exit__(self, *a):
return False
def get(self, url, headers=None, params=None):
self.calls.append((url, params))
return _Resp(self._pages.pop(0))
# ── OAuth: Outlook 캘린더 authorize URL (microsoft 테넌트 + Calendars.Read) ──
def test_outlook_calendar_authorize_url(session, ms_creds):
s, _ = session
url = oauth.start_oauth(s, "calendar", "outlook_calendar", redirect_after="/settings")
assert url.startswith("https://login.microsoftonline.com/common/oauth2/v2.0/authorize")
assert "code_challenge=" in url and "state=" in url # PKCE
assert "Calendars.Read" in url and "offline_access" in url
assert "Mail.Read" not in url # 캘린더 스코프엔 메일 권한 없음
# ── registry: provider 로 Google/Outlook 캘린더 분기 ──
def test_calendar_dispatch_by_provider(session):
s, _ = session
assert _impl("calendar", "real", "outlook_calendar").__name__ == "OutlookCalendarConnector"
assert _impl("calendar", "real", "google_calendar").__name__ == "GoogleCalendarConnector"
out_acct = ConnectorAccount(
id="ca-cal-o",
domain=ConnectorDomain.calendar,
provider="outlook_calendar",
mode=ConnectorMode.real,
)
assert isinstance(ConnectorRegistry.get(s, out_acct), OutlookCalendarConnector)
# ── fetch: calendarView 페이지네이션(nextLink) + isCancelled 스킵 + CalEvent upsert ──
def test_outlook_calendar_fetch_paginates_and_upserts(session, monkeypatch):
s, _ = session
acct = ConnectorAccount(
id="ca-cal-outlook-1",
domain=ConnectorDomain.calendar,
provider="outlook_calendar",
mode=ConnectorMode.real,
state=ConnState.connected,
external_account_id="me@corp.com",
token_enc=encrypt_token({"access_token": "AT", "expires_at": int(time.time()) + 9999}),
)
s.add(acct)
s.commit()
ev = lambda i, **k: { # noqa: E731
"id": f"ev-{i}",
"subject": f"미팅 {i}",
"start": {"dateTime": "2026-06-12T09:00:00.0000000"},
"end": {"dateTime": "2026-06-12T09:30:00.0000000"},
"location": {"displayName": ""},
**k,
}
pages = [
{"value": [ev(1), ev(2, isCancelled=True)], "@odata.nextLink": "https://graph/next?p=2"},
{"value": [ev(3)]},
]
client = _Client(pages)
monkeypatch.setattr(real_outlook.httpx, "Client", lambda *a, **k: client)
conn = OutlookCalendarConnector(acct)
res = conn.sync(s)
# 취소 이벤트 제외하고 2건만 적재(ev-1, ev-3)
assert res.upserted == 2
titles = {e.title for e in s.exec(select(CalEvent)).all()}
assert titles == {"미팅 1", "미팅 3"}
# 2번째 호출은 nextLink URL + params=None (쿼리는 nextLink 에 포함)
assert client.calls[0][0].endswith("/me/calendarView")
assert client.calls[1] == ("https://graph/next?p=2", None)
# ── API: /connectors/providers?domain=calendar → google + outlook 캘린더 ──
def test_calendar_providers_endpoint(client, ms_creds, monkeypatch):
monkeypatch.setenv("GOOGLE_CLIENT_ID", "g-cid")
get_settings.cache_clear()
rows = client.get("/api/connectors/providers?domain=calendar").json()
by = {r["provider"]: r for r in rows}
assert set(by) == {"google_calendar", "outlook_calendar"}
assert by["outlook_calendar"]["configured"] is True # ms_creds 설정됨
assert by["outlook_calendar"]["label"] == "Outlook 캘린더"
# ── 시작 검증: 미구성 outlook_calendar 는 400 ──
def test_oauth_start_outlook_calendar_requires_microsoft_client_id(client):
r = client.get("/api/connectors/oauth/start?domain=calendar&provider=outlook_calendar")
assert r.status_code == 400