Skip to content
Draft
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
9 changes: 9 additions & 0 deletions libs/arcade-core/arcade_core/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,15 @@ def __init__(self, *, id: Optional[str] = None, scopes: Optional[list[str]] = No
super().__init__(id=id, scopes=scopes)


class Sentry(OAuth2):
"""Marks a tool as requiring Sentry authorization."""

provider_id: str = "sentry"

def __init__(self, *, id: Optional[str] = None, scopes: Optional[list[str]] = None): # noqa: A002
super().__init__(id=id, scopes=scopes)


class Slack(OAuth2):
"""Marks a tool as requiring Slack (user token) authorization."""

Expand Down
2 changes: 2 additions & 0 deletions libs/arcade-mcp-server/arcade_mcp_server/auth/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
OAuth2,
PagerDuty,
Reddit,
Sentry,
Slack,
Spotify,
ToolAuthorization,
Expand All @@ -42,6 +43,7 @@
"OAuth2",
"PagerDuty",
"Reddit",
"Sentry",
"Slack",
"Spotify",
"ToolAuthorization",
Expand Down
2 changes: 2 additions & 0 deletions libs/arcade-tdk/arcade_tdk/auth/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
OAuth2,
PagerDuty,
Reddit,
Sentry,
Slack,
Spotify,
ToolAuthorization,
Expand All @@ -42,6 +43,7 @@
"OAuth2",
"PagerDuty",
"Reddit",
"Sentry",
"Slack",
"Spotify",
"ToolAuthorization",
Expand Down
54 changes: 54 additions & 0 deletions libs/tests/core/test_auth_providers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"""Every OAuth2 provider class in ``arcade_core.auth`` must construct and carry its metadata.

This walks the provider classes dynamically rather than naming them, so a newly added
provider is covered the moment it lands, with no per-provider test edit. Each subclass's
``__init__`` (its ``super().__init__`` call) only runs on instantiation, so constructing
every provider here is also what exercises those lines under coverage.
"""

import inspect

import pytest
from arcade_core import auth as auth_module
from arcade_core.auth import AuthProviderType, OAuth2


def _provider_classes() -> list[type[OAuth2]]:
"""Concrete OAuth2 provider classes defined in ``arcade_core.auth`` (OAuth2 itself excluded)."""
return [
obj
for _, obj in inspect.getmembers(auth_module, inspect.isclass)
if issubclass(obj, OAuth2)
and obj is not OAuth2
and obj.__module__ == auth_module.__name__
]


def test_provider_classes_are_discovered():
# Guards the parametrized tests below from silently collecting zero cases.
assert _provider_classes(), "no OAuth2 provider classes found in arcade_core.auth"


@pytest.mark.parametrize("provider_cls", _provider_classes(), ids=lambda c: c.__name__)
def test_provider_constructs_with_default_metadata(provider_cls: type[OAuth2]):
provider = provider_cls()
assert isinstance(provider.provider_id, str)
assert provider.provider_id
assert provider.provider_type == AuthProviderType.oauth2
assert provider.id is None
assert provider.scopes is None


@pytest.mark.parametrize("provider_cls", _provider_classes(), ids=lambda c: c.__name__)
def test_provider_accepts_id_and_scopes(provider_cls: type[OAuth2]):
provider = provider_cls(id="custom-provider-id", scopes=["scope.read", "scope.write"])
assert provider.id == "custom-provider-id"
assert provider.scopes == ["scope.read", "scope.write"]
# provider_id is the fixed well-known key and is not displaced by a custom id.
assert provider.provider_id == provider_cls().provider_id


def test_provider_ids_are_unique():
# A copy-paste codegen slip that reused another provider's id would collide here.
ids = [cls().provider_id for cls in _provider_classes()]
assert len(ids) == len(set(ids)), f"duplicate provider_id values: {sorted(ids)}"
Loading