From b9b0a101398717f95bb2d5080924427765927350 Mon Sep 17 00:00:00 2001 From: Roberto Date: Wed, 10 Jun 2026 16:09:10 +0200 Subject: [PATCH] feat(scouts): add gmail label-list + disconnect routes --- app/api/routes/scouts.py | 42 ++++++++++++++++++++++++++++++++- tests/test_scout_cloud_crud.py | 43 ++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 1 deletion(-) diff --git a/app/api/routes/scouts.py b/app/api/routes/scouts.py index 53297c1..81e6552 100644 --- a/app/api/routes/scouts.py +++ b/app/api/routes/scouts.py @@ -41,6 +41,7 @@ from app.core.note_summarizer import generate_note_summary from app.db import get_session from app.integrations import encrypt_token from app.models import CloudScoutConfig, ScoutRunLog, LocalScoutConfig +from app.scouts.connectors.registry import get_connector from app.schemas import ( CloudScoutCreateRequest, CloudScoutResponse, @@ -375,6 +376,46 @@ async def delete_cloud_scout( return {"ok": True} +@router.get("/cloud/{scout_id}/gmail-labels") +async def list_gmail_labels( + scout_id: str, + db: AsyncSession = Depends(get_session), + current_user: UserProfile = Depends(get_current_user), +): + scout = await db.get(CloudScoutConfig, scout_id) + if scout is None or scout.user_id != current_user.id: + raise HTTPException(status.HTTP_404_NOT_FOUND, "Scout not found") + try: + connector = get_connector("gmail") + except KeyError: + return [] + return await connector.list_labels(scout) + + +@router.post("/cloud/{scout_id}/gmail-disconnect", response_model=CloudScoutResponse) +async def disconnect_gmail( + scout_id: str, + db: AsyncSession = Depends(get_session), + current_user: UserProfile = Depends(get_current_user), +): + scout = await db.get(CloudScoutConfig, scout_id) + if scout is None or scout.user_id != current_user.id: + raise HTTPException(status.HTTP_404_NOT_FOUND, "Scout not found") + try: + connector = get_connector("gmail") + await connector.stop_watch(scout) + except KeyError: + pass + scout.oauth_token_encrypted = None + scout.gmail_history_id = None + scout.gmail_watch_expires_at = None + scout.gmail_address = None + scout.enabled = False + await db.commit() + await db.refresh(scout) + return _to_cloud_response(scout) + + # ── Gmail OAuth setup (scout-specific) ─────────────────────────────────────── # Scopes required for Gmail scout connectivity. @@ -556,7 +597,6 @@ async def scout_gmail_oauth_callback( await db.commit() # Attempt to set up Gmail push watch so we start receiving Pub/Sub notifications. - from app.scouts.connectors.registry import get_connector try: connector = get_connector("gmail") await connector.setup_watch(scout) diff --git a/tests/test_scout_cloud_crud.py b/tests/test_scout_cloud_crud.py index a4f2eaf..e8ea12b 100644 --- a/tests/test_scout_cloud_crud.py +++ b/tests/test_scout_cloud_crud.py @@ -104,3 +104,46 @@ async def test_delete_cloud_scout(): assert resp.status_code == 200 listing = (await client.get("/api/v1/scouts/cloud", headers=_auth_headers())).json() assert all(r["id"] != sid for r in listing) + + +@pytest.mark.asyncio +async def test_gmail_labels_route_returns_labels(): + async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client: + created = (await client.post( + "/api/v1/scouts/cloud", + json={"name": "L", "provider": "gmail"}, + headers=_auth_headers(), + )).json() + sid = created["id"] + + with patch("app.api.routes.scouts.get_connector") as mock_get: + mock_get.return_value.list_labels = AsyncMock(return_value=[{"id": "INBOX", "name": "INBOX"}]) + resp = await client.get(f"/api/v1/scouts/cloud/{sid}/gmail-labels", headers=_auth_headers()) + assert resp.status_code == 200 + assert resp.json() == [{"id": "INBOX", "name": "INBOX"}] + + +@pytest.mark.asyncio +async def test_gmail_disconnect_clears_token(): + async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client: + created = (await client.post( + "/api/v1/scouts/cloud", + json={"name": "D", "provider": "gmail"}, + headers=_auth_headers(), + )).json() + sid = created["id"] + # mark it connected directly in the DB + async with _TestSessionLocal() as session: + row = await session.get(CloudScoutConfig, sid) + row.oauth_token_encrypted = "blob" + row.gmail_address = "a@b.com" + await session.commit() + + with patch("app.api.routes.scouts.get_connector") as mock_get: + mock_get.return_value.stop_watch = AsyncMock() + resp = await client.post(f"/api/v1/scouts/cloud/{sid}/gmail-disconnect", headers=_auth_headers()) + assert resp.status_code == 200 + body = resp.json() + assert body["oauth_connected"] is False + assert body["gmail_address"] is None + assert body["enabled"] is False