Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions auth/scopes.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,12 @@
BASE_SCOPES = [USERINFO_EMAIL_SCOPE, USERINFO_PROFILE_SCOPE, OPENID_SCOPE]

# Service-specific scope groups
DOCS_SCOPES = [DOCS_READONLY_SCOPE, DOCS_WRITE_SCOPE]
DOCS_SCOPES = [
DOCS_READONLY_SCOPE,
DOCS_WRITE_SCOPE,
DRIVE_READONLY_SCOPE,
DRIVE_FILE_SCOPE,
]

CALENDAR_SCOPES = [CALENDAR_SCOPE, CALENDAR_READONLY_SCOPE, CALENDAR_EVENTS_SCOPE]

Expand All @@ -101,7 +106,7 @@

CHAT_SCOPES = [CHAT_READONLY_SCOPE, CHAT_WRITE_SCOPE, CHAT_SPACES_SCOPE]

SHEETS_SCOPES = [SHEETS_READONLY_SCOPE, SHEETS_WRITE_SCOPE]
SHEETS_SCOPES = [SHEETS_READONLY_SCOPE, SHEETS_WRITE_SCOPE, DRIVE_READONLY_SCOPE]

FORMS_SCOPES = [
FORMS_BODY_SCOPE,
Expand Down Expand Up @@ -148,8 +153,8 @@
"gmail": [GMAIL_READONLY_SCOPE],
"drive": [DRIVE_READONLY_SCOPE],
"calendar": [CALENDAR_READONLY_SCOPE],
"docs": [DOCS_READONLY_SCOPE],
"sheets": [SHEETS_READONLY_SCOPE],
"docs": [DOCS_READONLY_SCOPE, DRIVE_READONLY_SCOPE],
"sheets": [SHEETS_READONLY_SCOPE, DRIVE_READONLY_SCOPE],
"chat": [CHAT_READONLY_SCOPE],
"forms": [FORMS_BODY_READONLY_SCOPE, FORMS_RESPONSES_READONLY_SCOPE],
"slides": [SLIDES_READONLY_SCOPE],
Expand Down
95 changes: 95 additions & 0 deletions tests/test_scopes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
"""
Unit tests for cross-service scope generation.

Verifies that docs and sheets tools automatically include the Drive scopes
they need for operations like search_docs, list_docs_in_folder,
export_doc_to_pdf, and list_spreadsheets — without requiring --tools drive.
"""

import sys
import os

sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))

from auth.scopes import (
DRIVE_FILE_SCOPE,
DRIVE_READONLY_SCOPE,
DRIVE_SCOPE,
get_scopes_for_tools,
set_read_only,
)


class TestDocsScopes:
"""Tests for docs tool scope generation."""

def test_docs_includes_drive_readonly(self):
"""search_docs, get_doc_content, list_docs_in_folder need drive.readonly."""
scopes = get_scopes_for_tools(["docs"])
assert DRIVE_READONLY_SCOPE in scopes

def test_docs_includes_drive_file(self):
"""export_doc_to_pdf needs drive.file to create the PDF."""
scopes = get_scopes_for_tools(["docs"])
assert DRIVE_FILE_SCOPE in scopes

def test_docs_does_not_include_full_drive(self):
"""docs should NOT request full drive access."""
scopes = get_scopes_for_tools(["docs"])
assert DRIVE_SCOPE not in scopes


class TestSheetsScopes:
"""Tests for sheets tool scope generation."""

def test_sheets_includes_drive_readonly(self):
"""list_spreadsheets needs drive.readonly."""
scopes = get_scopes_for_tools(["sheets"])
assert DRIVE_READONLY_SCOPE in scopes

def test_sheets_does_not_include_full_drive(self):
"""sheets should NOT request full drive access."""
scopes = get_scopes_for_tools(["sheets"])
assert DRIVE_SCOPE not in scopes


class TestCombinedScopes:
"""Tests for combined tool scope generation."""

def test_docs_sheets_no_duplicate_drive_readonly(self):
"""Combined docs+sheets should deduplicate drive.readonly."""
scopes = get_scopes_for_tools(["docs", "sheets"])
assert scopes.count(DRIVE_READONLY_SCOPE) <= 1

def test_docs_sheets_returns_unique_scopes(self):
"""All returned scopes should be unique."""
scopes = get_scopes_for_tools(["docs", "sheets"])
assert len(scopes) == len(set(scopes))


class TestReadOnlyScopes:
"""Tests for read-only mode scope generation."""

def setup_method(self):
set_read_only(False)

def teardown_method(self):
set_read_only(False)

def test_docs_readonly_includes_drive_readonly(self):
"""Even in read-only mode, docs needs drive.readonly for search/list."""
set_read_only(True)
scopes = get_scopes_for_tools(["docs"])
assert DRIVE_READONLY_SCOPE in scopes

def test_docs_readonly_excludes_drive_file(self):
"""In read-only mode, docs should NOT request drive.file."""
set_read_only(True)
scopes = get_scopes_for_tools(["docs"])
assert DRIVE_FILE_SCOPE not in scopes

def test_sheets_readonly_includes_drive_readonly(self):
"""Even in read-only mode, sheets needs drive.readonly for list."""
set_read_only(True)
scopes = get_scopes_for_tools(["sheets"])
assert DRIVE_READONLY_SCOPE in scopes