Skip to content

Commit 84e065a

Browse files
committed
test(security): add differential coverage for 401/403 middleware paths and list_team_tokens
Add 4 tests covering uncovered edge-case paths in the token usage middleware's 401/403 handler (no Bearer header, non-API-token JWT, malformed token) and the missing list_team_tokens API-token-blocked test, achieving 100% differential coverage on new code. Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>
1 parent 69f3a95 commit 84e065a

File tree

2 files changed

+99
-0
lines changed

2 files changed

+99
-0
lines changed

tests/unit/mcpgateway/middleware/test_token_usage_middleware.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -682,3 +682,94 @@ async def app_impl(scope, receive, send):
682682
await _make_asgi_call(middleware, scope)
683683

684684
mock_token_service.log_token_usage.assert_not_awaited()
685+
686+
687+
@pytest.mark.asyncio
688+
async def test_skips_rejected_request_without_bearer_header():
689+
"""Middleware skips logging when a 401/403 response has no Bearer authorization header."""
690+
app = AsyncMock()
691+
692+
async def app_impl(scope, receive, send):
693+
await send({"type": "http.response.start", "status": 401, "headers": []})
694+
await send({"type": "http.response.body", "body": b"unauthorized"})
695+
696+
app.side_effect = app_impl
697+
middleware = TokenUsageMiddleware(app=app)
698+
699+
scope = {
700+
"type": "http",
701+
"path": "/api/tools",
702+
"method": "GET",
703+
"state": {},
704+
"client": ("10.0.0.1", 9000),
705+
"headers": [], # No authorization header
706+
}
707+
708+
with patch("mcpgateway.middleware.token_usage_middleware.fresh_db_session") as mock_session:
709+
await _make_asgi_call(middleware, scope)
710+
711+
mock_session.assert_not_called()
712+
713+
714+
@pytest.mark.asyncio
715+
async def test_skips_rejected_non_api_token_jwt():
716+
"""Middleware skips logging when a 401/403 response carries a JWT that is not an API token."""
717+
# Standard
718+
import jwt as _jwt_lib
719+
720+
token_payload = {
721+
"jti": "jti-jwt-session",
722+
"sub": "user@example.com",
723+
"user": {"auth_provider": "email"}, # Not an API token
724+
}
725+
raw_token = _jwt_lib.encode(token_payload, "test-secret-key-for-unit-tests-only", algorithm="HS256")
726+
727+
app = AsyncMock()
728+
729+
async def app_impl(scope, receive, send):
730+
await send({"type": "http.response.start", "status": 403, "headers": []})
731+
await send({"type": "http.response.body", "body": b"forbidden"})
732+
733+
app.side_effect = app_impl
734+
middleware = TokenUsageMiddleware(app=app)
735+
736+
scope = {
737+
"type": "http",
738+
"path": "/api/tools",
739+
"method": "GET",
740+
"state": {},
741+
"client": ("10.0.0.1", 9000),
742+
"headers": [(b"authorization", f"Bearer {raw_token}".encode())],
743+
}
744+
745+
with patch("mcpgateway.middleware.token_usage_middleware.fresh_db_session") as mock_session:
746+
await _make_asgi_call(middleware, scope)
747+
748+
mock_session.assert_not_called()
749+
750+
751+
@pytest.mark.asyncio
752+
async def test_skips_rejected_request_with_malformed_token():
753+
"""Middleware skips logging when a 401/403 response carries a malformed token that cannot be decoded."""
754+
app = AsyncMock()
755+
756+
async def app_impl(scope, receive, send):
757+
await send({"type": "http.response.start", "status": 401, "headers": []})
758+
await send({"type": "http.response.body", "body": b"unauthorized"})
759+
760+
app.side_effect = app_impl
761+
middleware = TokenUsageMiddleware(app=app)
762+
763+
scope = {
764+
"type": "http",
765+
"path": "/api/tools",
766+
"method": "GET",
767+
"state": {},
768+
"client": ("10.0.0.1", 9000),
769+
"headers": [(b"authorization", b"Bearer not-a-valid-jwt-at-all")],
770+
}
771+
772+
with patch("mcpgateway.middleware.token_usage_middleware.fresh_db_session") as mock_session:
773+
await _make_asgi_call(middleware, scope)
774+
775+
mock_session.assert_not_called()

tests/unit/mcpgateway/routers/test_tokens.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -935,6 +935,14 @@ async def test_create_team_token_blocked_for_api_token(self, mock_db, api_token_
935935

936936
assert exc_info.value.status_code == status.HTTP_403_FORBIDDEN
937937

938+
@pytest.mark.asyncio
939+
async def test_list_team_tokens_blocked_for_api_token(self, mock_db, api_token_user):
940+
"""API token cannot list team tokens (management plane isolation)."""
941+
with pytest.raises(HTTPException) as exc_info:
942+
await list_team_tokens(team_id="team-456", include_inactive=False, limit=50, offset=0, current_user=api_token_user, db=mock_db)
943+
944+
assert exc_info.value.status_code == status.HTTP_403_FORBIDDEN
945+
938946

939947
class TestAuthenticatedSessionErrorMessages:
940948
"""Test error message content for _require_authenticated_session."""

0 commit comments

Comments
 (0)