Skip to content

Commit 189bf9c

Browse files
committed
test: add tests
1 parent 5d70ff3 commit 189bf9c

5 files changed

Lines changed: 190 additions & 4 deletions

File tree

mcpauth/auth/mcp_auth_handler.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,12 @@ def create_metadata_route(self) -> Router:
1717
Returns a router for serving either the legacy OAuth 2.0 Authorization Server Metadata or
1818
the OAuth 2.0 Protected Resource Metadata, depending on the configuration.
1919
"""
20-
...
20+
... # pragma: no cover
2121

2222
@abstractmethod
2323
def get_token_verifier(self, resource: str) -> TokenVerifier:
2424
"""
2525
Resolves the appropriate TokenVerifier based on the provided resource.
2626
:param resource: The resource identifier for verifier lookup.
2727
"""
28-
...
28+
... # pragma: no cover

mcpauth/middleware/create_bearer_auth.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ def _handle_error(
106106
www_authenticate_header = BearerWWWAuthenticateHeader()
107107

108108
if isinstance(error, (MCPAuthTokenVerificationException, MCPAuthBearerAuthException)):
109-
www_authenticate_header.set_parameter_if_value_exists("error", error.code)
109+
www_authenticate_header.set_parameter_if_value_exists("error", error.code.value)
110110
if error.message:
111111
www_authenticate_header.set_parameter_if_value_exists("error_description", error.message)
112112

tests/__init__test.py

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,18 @@ def test_bearer_auth_middleware_calls_get_token_verifier_in_resource_mode(
124124
mock_get_verifier.assert_called_once_with(resource="https://api.example.com")
125125

126126

127+
def test_bearer_auth_middleware_throws_for_invalid_mode(
128+
valid_server_config: AuthServerConfig,
129+
):
130+
"""Test that bearer_auth_middleware throws a ValueError for an invalid mode."""
131+
auth = MCPAuth(server=valid_server_config)
132+
with pytest.raises(
133+
ValueError,
134+
match="mode_or_verify must be 'jwt' or a callable function that verifies tokens.",
135+
):
136+
auth.bearer_auth_middleware(mode_or_verify="invalid_mode") # type: ignore
137+
138+
127139
@patch("mcpauth.auth.resource_server_handler.validate_server_config")
128140
def test_metadata_route_throws_in_resource_mode(
129141
mock_validate: MagicMock, valid_resource_config: ResourceServerConfig
@@ -164,6 +176,26 @@ def test_metadata_route_calls_handler_method(
164176
mock_create_route.assert_called_once()
165177

166178

179+
@patch(
180+
"mcpauth.auth.authorization_server_handler.AuthorizationServerHandler.create_metadata_route"
181+
)
182+
@patch("mcpauth.auth.authorization_server_handler.validate_server_config")
183+
def test_metadata_route_throws_if_route_is_not_route_instance(
184+
mock_validate: MagicMock,
185+
mock_create_route: MagicMock,
186+
valid_server_config: AuthServerConfig,
187+
):
188+
"""Test that metadata_route throws an error if the created route is not a Route instance."""
189+
# Ensure the mock returns a router-like object with a routes attribute
190+
# containing something that is not a Route instance
191+
mock_create_route.return_value = MagicMock(routes=[MagicMock()])
192+
auth = MCPAuth(server=valid_server_config)
193+
with pytest.warns(DeprecationWarning):
194+
with pytest.raises(IndexError, match="No metadata endpoint route was created"):
195+
auth.metadata_route() # pyright: ignore[reportDeprecated]
196+
mock_create_route.assert_called_once()
197+
198+
167199
@patch(
168200
"mcpauth.auth.resource_server_handler.ResourceServerHandler.create_metadata_route"
169201
)
@@ -176,4 +208,20 @@ def test_resource_metadata_router_calls_handler_method(
176208
"""Test that resource_metadata_router calls the handler's create_metadata_route method."""
177209
auth = MCPAuth(protected_resources=valid_resource_config)
178210
auth.resource_metadata_router()
179-
mock_create_route.assert_called_once()
211+
mock_create_route.assert_called_once()
212+
213+
214+
@patch("mcpauth.middleware.create_bearer_auth.create_bearer_auth")
215+
def test_bearer_auth_middleware_with_callable_verifier(
216+
mock_create_bearer_auth: MagicMock, valid_server_config: AuthServerConfig
217+
):
218+
"""Test that bearer_auth_middleware works with a callable verifier."""
219+
auth = MCPAuth(server=valid_server_config)
220+
verifier = MagicMock()
221+
with patch("mcpauth.MCPAuthHandler.get_token_verifier"):
222+
auth.bearer_auth_middleware(mode_or_verify=verifier)
223+
224+
mock_create_bearer_auth.assert_called_once()
225+
# Check that the verifier is passed to create_bearer_auth
226+
args, _ = mock_create_bearer_auth.call_args
227+
assert args[0] == verifier

tests/auth/resource_server_handler_test.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,33 @@ def test_init_duplicate_resource_id(mock_validate: Mock, mock_resource_config: R
9191
assert excinfo.value.code == AuthServerExceptionCode.INVALID_SERVER_CONFIG
9292

9393

94+
@patch(
95+
"mcpauth.auth.resource_server_handler.validate_server_config",
96+
return_value=type("ValidationResult", (), {"is_valid": True}),
97+
)
98+
def test_init_duplicate_auth_server(
99+
mock_validate: Mock, mock_auth_server: AuthServerConfig
100+
):
101+
"""Test that ResourceServerHandler throws an error if an auth server is duplicated for a resource."""
102+
config_with_duplicate_auth_server = RSC(
103+
metadata=ResourceServerMetadata(
104+
resource="https://my-api.com",
105+
authorization_servers=[mock_auth_server, mock_auth_server],
106+
)
107+
)
108+
with pytest.raises(MCPAuthAuthServerException) as excinfo:
109+
ResourceServerHandler(
110+
ResourceServerModeConfig(
111+
protected_resources=[config_with_duplicate_auth_server]
112+
)
113+
)
114+
assert excinfo.value.code == AuthServerExceptionCode.INVALID_SERVER_CONFIG
115+
assert (
116+
excinfo.value.cause["error_description"] # type: ignore[reportGeneralTypeIssues]
117+
== "The authorization server ('https://auth.example.com') for resource 'https://my-api.com' is duplicated."
118+
)
119+
120+
94121
@patch(
95122
"mcpauth.auth.resource_server_handler.validate_server_config",
96123
return_value=type("ValidationResult", (), {"is_valid": True}),

tests/middleware/create_bearer_auth_test.py

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@
1818
MCPAuthAuthServerException,
1919
MCPAuthConfigException,
2020
MCPAuthTokenVerificationExceptionCode,
21+
MCPAuthBearerAuthException,
2122
)
23+
import pydantic
2224

2325

2426
class TestHandleBearerAuth:
@@ -58,6 +60,16 @@ def test_should_throw_error_if_issuer_is_not_a_valid_url(
5860
auth_info,
5961
)
6062

63+
def test_should_throw_error_if_issuer_is_not_string_or_callable(
64+
self, auth_info: ContextVar[AuthInfo | None]
65+
):
66+
with pytest.raises(pydantic.ValidationError):
67+
create_bearer_auth(
68+
lambda _: None, # type: ignore
69+
BearerAuthConfig(issuer=123), # type: ignore
70+
auth_info,
71+
)
72+
6173

6274
@pytest.mark.asyncio
6375
class TestHandleBearerAuthMiddleware:
@@ -287,6 +299,7 @@ async def test_should_respond_with_error_if_audience_does_not_match(
287299
],
288300
):
289301
mock_verify = MagicMock()
302+
assert isinstance(auth_config[1].issuer, str)
290303
mock_verify.return_value = AuthInfo(
291304
issuer=auth_config[1].issuer,
292305
client_id="client-id",
@@ -333,6 +346,7 @@ async def test_should_respond_with_error_if_audience_does_not_match_array_case(
333346
],
334347
):
335348
mock_verify = MagicMock()
349+
assert isinstance(auth_config[1].issuer, str)
336350
mock_verify.return_value = AuthInfo(
337351
issuer=auth_config[1].issuer,
338352
client_id="client-id",
@@ -379,6 +393,7 @@ async def test_should_respond_with_error_if_required_scopes_are_not_present(
379393
],
380394
):
381395
mock_verify = MagicMock()
396+
assert isinstance(auth_config[1].issuer, str)
382397
mock_verify.return_value = AuthInfo(
383398
issuer=auth_config[1].issuer,
384399
client_id="client-id",
@@ -643,3 +658,99 @@ async def test_should_show_error_details_for_bearer_auth_error(self):
643658
},
644659
}
645660
mock_verify.assert_called_once_with("valid-token")
661+
662+
@pytest.mark.asyncio
663+
async def test_should_include_resource_metadata_on_401_bearer_error(
664+
self,
665+
auth_info: ContextVar[AuthInfo | None],
666+
):
667+
"""
668+
Test that the WWW-Authenticate header includes the resource_metadata URI when a
669+
Bearer auth error (401) occurs and a resource is specified.
670+
"""
671+
config = BearerAuthConfig(
672+
issuer="https://correct-issuer.com",
673+
resource="https://my-api.com",
674+
)
675+
mock_verify = MagicMock()
676+
mock_verify.return_value = AuthInfo(
677+
issuer="https://wrong-issuer.com",
678+
client_id="client-id",
679+
scopes=[],
680+
token="valid-token",
681+
subject="subject-id",
682+
audience=None,
683+
claims={},
684+
)
685+
686+
MiddlewareClass = create_bearer_auth(mock_verify, config, auth_info)
687+
middleware = MiddlewareClass(app=MagicMock())
688+
689+
request = Request(
690+
scope={
691+
"type": "http",
692+
"headers": [(b"authorization", b"Bearer valid-token")],
693+
"method": "GET",
694+
"path": "/",
695+
}
696+
)
697+
698+
response = await middleware.dispatch(request, MagicMock())
699+
700+
assert response.status_code == 401
701+
assert "WWW-Authenticate" in response.headers
702+
assert (
703+
'resource_metadata="https://my-api.com/.well-known/oauth-protected-resource"'
704+
in response.headers["WWW-Authenticate"]
705+
)
706+
707+
async def test_should_respond_with_error_if_callable_issuer_fails(
708+
self,
709+
auth_config: tuple[
710+
VerifyAccessTokenFunction, BearerAuthConfig, ContextVar[AuthInfo | None]
711+
],
712+
):
713+
"""Test that an error is returned if a callable issuer fails validation."""
714+
mock_verify = MagicMock()
715+
mock_verify.return_value = AuthInfo(
716+
issuer="https://some-issuer.com",
717+
client_id="client-id",
718+
scopes=[],
719+
token="valid-token",
720+
audience=None,
721+
subject="subject-id",
722+
claims={},
723+
)
724+
725+
def failing_issuer_validator(issuer: str):
726+
raise MCPAuthBearerAuthException(BearerAuthExceptionCode.INVALID_ISSUER)
727+
728+
config = BearerAuthConfig(
729+
issuer=failing_issuer_validator,
730+
resource="https://my-api.com",
731+
)
732+
733+
MiddlewareClass = create_bearer_auth(mock_verify, config, auth_config[2])
734+
middleware = MiddlewareClass(app=MagicMock())
735+
736+
request = Request(
737+
scope={
738+
"type": "http",
739+
"headers": [(b"authorization", b"Bearer valid-token")],
740+
"method": "GET",
741+
"path": "/",
742+
}
743+
)
744+
745+
response = await middleware.dispatch(request, MagicMock())
746+
747+
assert response.status_code == 401
748+
assert "WWW-Authenticate" in response.headers
749+
assert (
750+
f'error="{BearerAuthExceptionCode.INVALID_ISSUER.value}"'
751+
in response.headers["WWW-Authenticate"]
752+
)
753+
assert (
754+
'resource_metadata="https://my-api.com/.well-known/oauth-protected-resource"'
755+
in response.headers["WWW-Authenticate"]
756+
)

0 commit comments

Comments
 (0)