tests
This commit is contained in:
147
tests/gdrive/test_create_drive_folder.py
Normal file
147
tests/gdrive/test_create_drive_folder.py
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
"""
|
||||||
|
Unit tests for create_drive_folder tool.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")))
|
||||||
|
|
||||||
|
from gdrive.drive_tools import _create_drive_folder_impl as _raw_create_drive_folder
|
||||||
|
|
||||||
|
|
||||||
|
def _make_service(created_response):
|
||||||
|
"""Build a mock Drive service whose files().create().execute returns *created_response*."""
|
||||||
|
execute = MagicMock(return_value=created_response)
|
||||||
|
create = MagicMock()
|
||||||
|
create.return_value.execute = execute
|
||||||
|
files = MagicMock()
|
||||||
|
files.return_value.create = create
|
||||||
|
service = MagicMock()
|
||||||
|
service.files = files
|
||||||
|
return service
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_create_folder_root_skips_resolve():
|
||||||
|
"""Parent 'root' should pass through resolve_folder_id and produce correct output."""
|
||||||
|
api_response = {
|
||||||
|
"id": "new-folder-id",
|
||||||
|
"name": "My Folder",
|
||||||
|
"webViewLink": "https://drive.google.com/drive/folders/new-folder-id",
|
||||||
|
}
|
||||||
|
service = _make_service(api_response)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"gdrive.drive_tools.resolve_folder_id",
|
||||||
|
new_callable=AsyncMock,
|
||||||
|
return_value="root",
|
||||||
|
):
|
||||||
|
result = await _raw_create_drive_folder(
|
||||||
|
service,
|
||||||
|
user_google_email="user@example.com",
|
||||||
|
folder_name="My Folder",
|
||||||
|
parent_folder_id="root",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert "new-folder-id" in result
|
||||||
|
assert "My Folder" in result
|
||||||
|
assert "https://drive.google.com/drive/folders/new-folder-id" in result
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_create_folder_custom_parent_resolves():
|
||||||
|
"""A non-root parent_folder_id should go through resolve_folder_id."""
|
||||||
|
api_response = {
|
||||||
|
"id": "new-folder-id",
|
||||||
|
"name": "Sub Folder",
|
||||||
|
"webViewLink": "https://drive.google.com/drive/folders/new-folder-id",
|
||||||
|
}
|
||||||
|
service = _make_service(api_response)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"gdrive.drive_tools.resolve_folder_id",
|
||||||
|
new_callable=AsyncMock,
|
||||||
|
return_value="resolved-parent-id",
|
||||||
|
) as mock_resolve:
|
||||||
|
result = await _raw_create_drive_folder(
|
||||||
|
service,
|
||||||
|
user_google_email="user@example.com",
|
||||||
|
folder_name="Sub Folder",
|
||||||
|
parent_folder_id="shortcut-id",
|
||||||
|
)
|
||||||
|
|
||||||
|
mock_resolve.assert_awaited_once_with(service, "shortcut-id")
|
||||||
|
# The output message uses the original parent_folder_id, not the resolved one
|
||||||
|
assert "shortcut-id" in result
|
||||||
|
# But the API call should use the resolved ID
|
||||||
|
service.files().create.assert_called_once_with(
|
||||||
|
body={
|
||||||
|
"name": "Sub Folder",
|
||||||
|
"mimeType": "application/vnd.google-apps.folder",
|
||||||
|
"parents": ["resolved-parent-id"],
|
||||||
|
},
|
||||||
|
fields="id, name, webViewLink",
|
||||||
|
supportsAllDrives=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_create_folder_passes_correct_metadata():
|
||||||
|
"""Verify the metadata dict sent to the Drive API is correct."""
|
||||||
|
api_response = {
|
||||||
|
"id": "abc123",
|
||||||
|
"name": "Test",
|
||||||
|
"webViewLink": "https://drive.google.com/drive/folders/abc123",
|
||||||
|
}
|
||||||
|
service = _make_service(api_response)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"gdrive.drive_tools.resolve_folder_id",
|
||||||
|
new_callable=AsyncMock,
|
||||||
|
return_value="resolved-id",
|
||||||
|
):
|
||||||
|
await _raw_create_drive_folder(
|
||||||
|
service,
|
||||||
|
user_google_email="user@example.com",
|
||||||
|
folder_name="Test",
|
||||||
|
parent_folder_id="some-parent",
|
||||||
|
)
|
||||||
|
|
||||||
|
service.files().create.assert_called_once_with(
|
||||||
|
body={
|
||||||
|
"name": "Test",
|
||||||
|
"mimeType": "application/vnd.google-apps.folder",
|
||||||
|
"parents": ["resolved-id"],
|
||||||
|
},
|
||||||
|
fields="id, name, webViewLink",
|
||||||
|
supportsAllDrives=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_create_folder_missing_webviewlink():
|
||||||
|
"""When the API omits webViewLink, the result should have an empty link."""
|
||||||
|
api_response = {
|
||||||
|
"id": "abc123",
|
||||||
|
"name": "NoLink",
|
||||||
|
}
|
||||||
|
service = _make_service(api_response)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"gdrive.drive_tools.resolve_folder_id",
|
||||||
|
new_callable=AsyncMock,
|
||||||
|
return_value="root",
|
||||||
|
):
|
||||||
|
result = await _raw_create_drive_folder(
|
||||||
|
service,
|
||||||
|
user_google_email="user@example.com",
|
||||||
|
folder_name="NoLink",
|
||||||
|
parent_folder_id="root",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert "abc123" in result
|
||||||
|
assert "NoLink" in result
|
||||||
60
tests/test_main_permissions_tier.py
Normal file
60
tests/test_main_permissions_tier.py
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
||||||
|
|
||||||
|
import main
|
||||||
|
|
||||||
|
|
||||||
|
def test_resolve_permissions_mode_selection_without_tier():
|
||||||
|
services = ["gmail", "drive"]
|
||||||
|
resolved_services, tier_tool_filter = main.resolve_permissions_mode_selection(
|
||||||
|
services, None
|
||||||
|
)
|
||||||
|
assert resolved_services == services
|
||||||
|
assert tier_tool_filter is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_resolve_permissions_mode_selection_with_tier_filters_services(monkeypatch):
|
||||||
|
def fake_resolve_tools_from_tier(tier, services):
|
||||||
|
assert tier == "core"
|
||||||
|
assert services == ["gmail", "drive", "slides"]
|
||||||
|
return ["search_gmail_messages"], ["gmail"]
|
||||||
|
|
||||||
|
monkeypatch.setattr(main, "resolve_tools_from_tier", fake_resolve_tools_from_tier)
|
||||||
|
|
||||||
|
resolved_services, tier_tool_filter = main.resolve_permissions_mode_selection(
|
||||||
|
["gmail", "drive", "slides"], "core"
|
||||||
|
)
|
||||||
|
assert resolved_services == ["gmail"]
|
||||||
|
assert tier_tool_filter == {"search_gmail_messages"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_narrow_permissions_to_services_keeps_selected_order():
|
||||||
|
permissions = {"drive": "full", "gmail": "readonly", "calendar": "readonly"}
|
||||||
|
narrowed = main.narrow_permissions_to_services(permissions, ["gmail", "drive"])
|
||||||
|
assert narrowed == {"gmail": "readonly", "drive": "full"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_narrow_permissions_to_services_drops_non_selected_services():
|
||||||
|
permissions = {"gmail": "send", "drive": "full"}
|
||||||
|
narrowed = main.narrow_permissions_to_services(permissions, ["gmail"])
|
||||||
|
assert narrowed == {"gmail": "send"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_permissions_and_tools_flags_are_rejected(monkeypatch, capsys):
|
||||||
|
monkeypatch.setattr(main, "configure_safe_logging", lambda: None)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
sys,
|
||||||
|
"argv",
|
||||||
|
["main.py", "--permissions", "gmail:readonly", "--tools", "gmail"],
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(SystemExit) as exc:
|
||||||
|
main.main()
|
||||||
|
|
||||||
|
assert exc.value.code == 1
|
||||||
|
captured = capsys.readouterr()
|
||||||
|
assert "--permissions and --tools cannot be combined" in captured.err
|
||||||
118
tests/test_permissions.py
Normal file
118
tests/test_permissions.py
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
"""
|
||||||
|
Unit tests for granular per-service permission parsing and scope resolution.
|
||||||
|
|
||||||
|
Covers parse_permissions_arg() validation (format, duplicates, unknown
|
||||||
|
service/level) and cumulative scope expansion in get_scopes_for_permission().
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
||||||
|
|
||||||
|
from auth.permissions import (
|
||||||
|
get_scopes_for_permission,
|
||||||
|
parse_permissions_arg,
|
||||||
|
SERVICE_PERMISSION_LEVELS,
|
||||||
|
)
|
||||||
|
from auth.scopes import (
|
||||||
|
GMAIL_READONLY_SCOPE,
|
||||||
|
GMAIL_LABELS_SCOPE,
|
||||||
|
GMAIL_MODIFY_SCOPE,
|
||||||
|
GMAIL_COMPOSE_SCOPE,
|
||||||
|
DRIVE_READONLY_SCOPE,
|
||||||
|
DRIVE_SCOPE,
|
||||||
|
DRIVE_FILE_SCOPE,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestParsePermissionsArg:
|
||||||
|
"""Tests for parse_permissions_arg()."""
|
||||||
|
|
||||||
|
def test_single_valid_entry(self):
|
||||||
|
result = parse_permissions_arg(["gmail:readonly"])
|
||||||
|
assert result == {"gmail": "readonly"}
|
||||||
|
|
||||||
|
def test_multiple_valid_entries(self):
|
||||||
|
result = parse_permissions_arg(["gmail:organize", "drive:full"])
|
||||||
|
assert result == {"gmail": "organize", "drive": "full"}
|
||||||
|
|
||||||
|
def test_all_services_at_readonly(self):
|
||||||
|
entries = [f"{svc}:readonly" for svc in SERVICE_PERMISSION_LEVELS]
|
||||||
|
result = parse_permissions_arg(entries)
|
||||||
|
assert set(result.keys()) == set(SERVICE_PERMISSION_LEVELS.keys())
|
||||||
|
|
||||||
|
def test_missing_colon_raises(self):
|
||||||
|
with pytest.raises(ValueError, match="Invalid permission format"):
|
||||||
|
parse_permissions_arg(["gmail_readonly"])
|
||||||
|
|
||||||
|
def test_duplicate_service_raises(self):
|
||||||
|
with pytest.raises(ValueError, match="Duplicate service"):
|
||||||
|
parse_permissions_arg(["gmail:readonly", "gmail:full"])
|
||||||
|
|
||||||
|
def test_unknown_service_raises(self):
|
||||||
|
with pytest.raises(ValueError, match="Unknown service"):
|
||||||
|
parse_permissions_arg(["fakesvc:readonly"])
|
||||||
|
|
||||||
|
def test_unknown_level_raises(self):
|
||||||
|
with pytest.raises(ValueError, match="Unknown level"):
|
||||||
|
parse_permissions_arg(["gmail:superadmin"])
|
||||||
|
|
||||||
|
def test_empty_list_returns_empty(self):
|
||||||
|
assert parse_permissions_arg([]) == {}
|
||||||
|
|
||||||
|
def test_extra_colon_in_value(self):
|
||||||
|
"""A level containing a colon should fail as unknown level."""
|
||||||
|
with pytest.raises(ValueError, match="Unknown level"):
|
||||||
|
parse_permissions_arg(["gmail:read:only"])
|
||||||
|
|
||||||
|
|
||||||
|
class TestGetScopesForPermission:
|
||||||
|
"""Tests for get_scopes_for_permission() cumulative scope expansion."""
|
||||||
|
|
||||||
|
def test_gmail_readonly_returns_readonly_scope(self):
|
||||||
|
scopes = get_scopes_for_permission("gmail", "readonly")
|
||||||
|
assert GMAIL_READONLY_SCOPE in scopes
|
||||||
|
|
||||||
|
def test_gmail_organize_includes_readonly(self):
|
||||||
|
"""Organize level should cumulatively include readonly scopes."""
|
||||||
|
scopes = get_scopes_for_permission("gmail", "organize")
|
||||||
|
assert GMAIL_READONLY_SCOPE in scopes
|
||||||
|
assert GMAIL_LABELS_SCOPE in scopes
|
||||||
|
assert GMAIL_MODIFY_SCOPE in scopes
|
||||||
|
|
||||||
|
def test_gmail_drafts_includes_organize_and_readonly(self):
|
||||||
|
scopes = get_scopes_for_permission("gmail", "drafts")
|
||||||
|
assert GMAIL_READONLY_SCOPE in scopes
|
||||||
|
assert GMAIL_LABELS_SCOPE in scopes
|
||||||
|
assert GMAIL_COMPOSE_SCOPE in scopes
|
||||||
|
|
||||||
|
def test_drive_readonly_excludes_full(self):
|
||||||
|
scopes = get_scopes_for_permission("drive", "readonly")
|
||||||
|
assert DRIVE_READONLY_SCOPE in scopes
|
||||||
|
assert DRIVE_SCOPE not in scopes
|
||||||
|
assert DRIVE_FILE_SCOPE not in scopes
|
||||||
|
|
||||||
|
def test_drive_full_includes_readonly(self):
|
||||||
|
scopes = get_scopes_for_permission("drive", "full")
|
||||||
|
assert DRIVE_READONLY_SCOPE in scopes
|
||||||
|
assert DRIVE_SCOPE in scopes
|
||||||
|
|
||||||
|
def test_unknown_service_raises(self):
|
||||||
|
with pytest.raises(ValueError, match="Unknown service"):
|
||||||
|
get_scopes_for_permission("nonexistent", "readonly")
|
||||||
|
|
||||||
|
def test_unknown_level_raises(self):
|
||||||
|
with pytest.raises(ValueError, match="Unknown permission level"):
|
||||||
|
get_scopes_for_permission("gmail", "nonexistent")
|
||||||
|
|
||||||
|
def test_no_duplicate_scopes(self):
|
||||||
|
"""Cumulative expansion should deduplicate scopes."""
|
||||||
|
for service, levels in SERVICE_PERMISSION_LEVELS.items():
|
||||||
|
for level_name, _ in levels:
|
||||||
|
scopes = get_scopes_for_permission(service, level_name)
|
||||||
|
assert len(scopes) == len(set(scopes)), (
|
||||||
|
f"Duplicate scopes for {service}:{level_name}"
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user