diff --git a/CHANGELOG.md b/CHANGELOG.md
index 84594b3..fff761e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+## [v1.0.0] - 2025-01-01
+
+### Added
+- Full test coverage for all 10 endpoint modules (36 tools total)
+ - Added `test_steam_user.py` (6 tools)
+ - Added `test_user_stats.py` (6 tools)
+ - Added `test_steam_news.py` (1 tool)
+
+### Changed
+- Updated version to 1.0.0 across all files
+- Changed development status from "Alpha" to "Production/Stable"
+
+### Fixed
+- Removed duplicate `_resolve_steam_id` methods in endpoint modules
+ - Consolidated to use base class implementation
+ - Affected files: steam_user.py, user_stats.py, player_service.py, steam_trading.py
+- Cleaned up unused imports after code consolidation
+
## [v0.9.0] - 2025-12-12
### Added
@@ -237,7 +255,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Docker Compose configuration
- Comprehensive README with usage instructions
-[Unreleased]: https://github.com/CodeKeanu/steam-mcp/compare/v0.9.0...HEAD
+[Unreleased]: https://github.com/CodeKeanu/steam-mcp/compare/v1.0.0...HEAD
+[v1.0.0]: https://github.com/CodeKeanu/steam-mcp/compare/v0.9.0...v1.0.0
[v0.9.0]: https://github.com/CodeKeanu/steam-mcp/compare/v0.8.0...v0.9.0
[v0.8.0]: https://github.com/CodeKeanu/steam-mcp/compare/v0.7.1...v0.8.0
[v0.7.1]: https://github.com/CodeKeanu/steam-mcp/compare/v0.7.0...v0.7.1
diff --git a/pyproject.toml b/pyproject.toml
index 87b674c..afe338a 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "steam-mcp"
-version = "0.8.0"
+version = "1.0.0"
description = "Steam API integration via Model Context Protocol (MCP)"
readme = "README.md"
license = "MIT"
@@ -14,7 +14,7 @@ authors = [
]
keywords = ["steam", "mcp", "api", "model-context-protocol", "claude"]
classifiers = [
- "Development Status :: 3 - Alpha",
+ "Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
diff --git a/src/steam_mcp/endpoints/player_service.py b/src/steam_mcp/endpoints/player_service.py
index 47e0b47..b88cd7c 100644
--- a/src/steam_mcp/endpoints/player_service.py
+++ b/src/steam_mcp/endpoints/player_service.py
@@ -11,7 +11,6 @@
from typing import Any
from steam_mcp.endpoints.base import BaseEndpoint, endpoint
-from steam_mcp.utils.steam_id import normalize_steam_id, SteamIDError
# Default games to display in detailed output
@@ -318,28 +317,6 @@ async def get_steam_level(self, steam_id: str) -> str:
return f"Steam Level for {normalized_id}: {level} ({tier})"
- async def _resolve_steam_id(self, steam_id: str) -> str:
- """
- Resolve steam_id, handling 'me'/'my' shortcuts.
-
- Returns:
- Normalized SteamID64 or error message starting with "Error"
- """
- # Handle "me" / "my" shortcuts
- steam_id_lower = steam_id.strip().lower()
- if steam_id_lower in ("me", "my", "myself", "mine"):
- if not self.client.owner_steam_id:
- return (
- "Error: No owner Steam ID configured. "
- "Set STEAM_USER_ID environment variable to use 'me'/'my' shortcuts."
- )
- return self.client.owner_steam_id
-
- try:
- return await normalize_steam_id(steam_id, self.client)
- except SteamIDError as e:
- return f"Error resolving Steam ID: {e}"
-
async def _fetch_games_raw(
self, steam_id: str, include_free: bool = True
) -> list[dict[str, Any]]:
diff --git a/src/steam_mcp/endpoints/steam_trading.py b/src/steam_mcp/endpoints/steam_trading.py
index f36ca7b..867d8d9 100644
--- a/src/steam_mcp/endpoints/steam_trading.py
+++ b/src/steam_mcp/endpoints/steam_trading.py
@@ -14,33 +14,11 @@
from typing import Any
from steam_mcp.endpoints.base import BaseEndpoint, endpoint
-from steam_mcp.utils.steam_id import normalize_steam_id, SteamIDError
class IEconService(BaseEndpoint):
"""Steam Economy Service API endpoints for trading and market data."""
- async def _resolve_steam_id(self, steam_id: str) -> str:
- """
- Resolve steam_id, handling 'me'/'my' shortcuts.
-
- Returns:
- Normalized SteamID64 or error message starting with "Error"
- """
- steam_id_lower = steam_id.strip().lower()
- if steam_id_lower in ("me", "my", "myself", "mine"):
- if not self.client.owner_steam_id:
- return (
- "Error: No owner Steam ID configured. "
- "Set STEAM_USER_ID environment variable to use 'me'/'my' shortcuts."
- )
- return self.client.owner_steam_id
-
- try:
- return await normalize_steam_id(steam_id, self.client)
- except SteamIDError as e:
- return f"Error resolving Steam ID: {e}"
-
@endpoint(
name="get_trade_offers",
description=(
diff --git a/src/steam_mcp/endpoints/steam_user.py b/src/steam_mcp/endpoints/steam_user.py
index 459785c..9a6ee91 100644
--- a/src/steam_mcp/endpoints/steam_user.py
+++ b/src/steam_mcp/endpoints/steam_user.py
@@ -21,27 +21,6 @@
class ISteamUser(BaseEndpoint):
"""ISteamUser API endpoints for player identity and profile data."""
- async def _resolve_steam_id(self, steam_id: str) -> str:
- """
- Resolve steam_id, handling 'me'/'my' shortcuts.
-
- Returns:
- Normalized SteamID64 or error message starting with "Error"
- """
- steam_id_lower = steam_id.strip().lower()
- if steam_id_lower in ("me", "my", "myself", "mine"):
- if not self.client.owner_steam_id:
- return (
- "Error: No owner Steam ID configured. "
- "Set STEAM_USER_ID environment variable to use 'me'/'my' shortcuts."
- )
- return self.client.owner_steam_id
-
- try:
- return await normalize_steam_id(steam_id, self.client)
- except SteamIDError as e:
- return f"Error resolving Steam ID: {e}"
-
@endpoint(
name="get_my_steam_id",
description=(
diff --git a/src/steam_mcp/endpoints/user_stats.py b/src/steam_mcp/endpoints/user_stats.py
index a19d618..b3eb738 100644
--- a/src/steam_mcp/endpoints/user_stats.py
+++ b/src/steam_mcp/endpoints/user_stats.py
@@ -11,7 +11,6 @@
from typing import Any
from steam_mcp.endpoints.base import BaseEndpoint, endpoint
-from steam_mcp.utils.steam_id import normalize_steam_id, SteamIDError
# Maximum achievements to display in detailed output
@@ -588,19 +587,3 @@ async def get_global_stats_for_game(
output.append(f" {stat_name}: {total_str}")
return "\n".join(output)
-
- async def _resolve_steam_id(self, steam_id: str) -> str:
- """Resolve steam_id, handling 'me'/'my' shortcuts."""
- steam_id_lower = steam_id.strip().lower()
- if steam_id_lower in ("me", "my", "myself", "mine"):
- if not self.client.owner_steam_id:
- return (
- "Error: No owner Steam ID configured. "
- "Set STEAM_USER_ID environment variable to use 'me'/'my' shortcuts."
- )
- return self.client.owner_steam_id
-
- try:
- return await normalize_steam_id(steam_id, self.client)
- except SteamIDError as e:
- return f"Error resolving Steam ID: {e}"
diff --git a/src/steam_mcp/server.py b/src/steam_mcp/server.py
index ae1c45d..9b447ec 100644
--- a/src/steam_mcp/server.py
+++ b/src/steam_mcp/server.py
@@ -125,7 +125,7 @@ async def run_server() -> None:
write_stream,
InitializationOptions(
server_name="steam-mcp-server",
- server_version="0.1.0",
+ server_version="1.0.0",
capabilities=server.get_capabilities(
notification_options=NotificationOptions(),
experimental_capabilities={},
diff --git a/tests/test_player_service.py b/tests/test_player_service.py
index d180bd7..c5a800e 100644
--- a/tests/test_player_service.py
+++ b/tests/test_player_service.py
@@ -43,7 +43,7 @@ async def test_self_comparison_rejected(self, player_service, mock_client):
"""Friend ID matching user ID should be rejected."""
# Both resolve to same ID
with patch(
- "steam_mcp.endpoints.player_service.normalize_steam_id",
+ "steam_mcp.endpoints.base.normalize_steam_id",
new_callable=AsyncMock,
return_value="76561198000000001",
):
@@ -58,7 +58,7 @@ async def test_self_comparison_rejected(self, player_service, mock_client):
async def test_invalid_steam_id_returns_error(self, player_service):
"""Invalid Steam ID should return error."""
with patch(
- "steam_mcp.endpoints.player_service.normalize_steam_id",
+ "steam_mcp.endpoints.base.normalize_steam_id",
new_callable=AsyncMock,
side_effect=SteamIDError("Invalid Steam ID"),
):
@@ -90,7 +90,7 @@ async def test_finds_shared_unplayed_games(self, player_service, mock_client):
]
with patch(
- "steam_mcp.endpoints.player_service.normalize_steam_id",
+ "steam_mcp.endpoints.base.normalize_steam_id",
new_callable=AsyncMock,
side_effect=["76561198000000001", "76561198000000002"],
):
@@ -115,7 +115,7 @@ async def test_no_unplayed_games_message(self, player_service, mock_client):
]
with patch(
- "steam_mcp.endpoints.player_service.normalize_steam_id",
+ "steam_mcp.endpoints.base.normalize_steam_id",
new_callable=AsyncMock,
side_effect=["76561198000000001", "76561198000000002"],
):
@@ -141,7 +141,7 @@ async def test_private_profile_handled_gracefully(self, player_service, mock_cli
]
with patch(
- "steam_mcp.endpoints.player_service.normalize_steam_id",
+ "steam_mcp.endpoints.base.normalize_steam_id",
new_callable=AsyncMock,
side_effect=["76561198000000001", "76561198000000002", "76561198000000003"],
):
@@ -176,7 +176,7 @@ async def test_multiple_friends_all_must_own(self, player_service, mock_client):
]
with patch(
- "steam_mcp.endpoints.player_service.normalize_steam_id",
+ "steam_mcp.endpoints.base.normalize_steam_id",
new_callable=AsyncMock,
side_effect=["76561198000000001", "76561198000000002", "76561198000000003"],
):
@@ -203,7 +203,7 @@ async def test_all_friends_must_have_zero_playtime(self, player_service, mock_cl
]
with patch(
- "steam_mcp.endpoints.player_service.normalize_steam_id",
+ "steam_mcp.endpoints.base.normalize_steam_id",
new_callable=AsyncMock,
side_effect=["76561198000000001", "76561198000000002", "76561198000000003"],
):
@@ -223,7 +223,7 @@ async def test_my_profile_private_returns_error(self, player_service, mock_clien
]
with patch(
- "steam_mcp.endpoints.player_service.normalize_steam_id",
+ "steam_mcp.endpoints.base.normalize_steam_id",
new_callable=AsyncMock,
side_effect=["76561198000000001", "76561198000000002"],
):
diff --git a/tests/test_steam_news.py b/tests/test_steam_news.py
new file mode 100644
index 0000000..6fd1b8e
--- /dev/null
+++ b/tests/test_steam_news.py
@@ -0,0 +1,283 @@
+"""Tests for ISteamNews endpoints."""
+
+from unittest.mock import AsyncMock, MagicMock
+
+import pytest
+
+from steam_mcp.endpoints.steam_news import ISteamNews
+
+
+@pytest.fixture
+def mock_client():
+ """Create mock Steam client."""
+ client = MagicMock()
+ client.get = AsyncMock()
+ return client
+
+
+@pytest.fixture
+def steam_news(mock_client):
+ """Create ISteamNews instance with mock client."""
+ return ISteamNews(mock_client)
+
+
+class TestGetNewsForApp:
+ """Tests for get_news_for_app endpoint."""
+
+ @pytest.mark.asyncio
+ async def test_returns_news_items(self, steam_news, mock_client):
+ """Should return formatted news items."""
+ mock_client.get.return_value = {
+ "appnews": {
+ "newsitems": [
+ {
+ "title": "Big Update",
+ "author": "Developer",
+ "url": "https://example.com/news",
+ "contents": "Exciting news about the update!",
+ "date": 1702000000,
+ "feedlabel": "Steam Blog",
+ "is_external_url": False,
+ }
+ ]
+ }
+ }
+
+ result = await steam_news.get_news_for_app(app_id=440)
+
+ assert "News for App ID 440" in result
+ assert "Big Update" in result
+ assert "Developer" in result
+ assert "Steam Blog" in result
+ assert "Exciting news" in result
+ assert "https://example.com/news" in result
+
+ @pytest.mark.asyncio
+ async def test_no_news_found(self, steam_news, mock_client):
+ """Should return message when no news found."""
+ mock_client.get.return_value = {"appnews": {"newsitems": []}}
+
+ result = await steam_news.get_news_for_app(app_id=12345)
+
+ assert "No news found" in result
+ assert "12345" in result
+
+ @pytest.mark.asyncio
+ async def test_multiple_news_items(self, steam_news, mock_client):
+ """Should return multiple news items."""
+ mock_client.get.return_value = {
+ "appnews": {
+ "newsitems": [
+ {
+ "title": "First News",
+ "author": "Author1",
+ "url": "url1",
+ "contents": "Content 1",
+ "date": 1702000000,
+ },
+ {
+ "title": "Second News",
+ "author": "Author2",
+ "url": "url2",
+ "contents": "Content 2",
+ "date": 1701000000,
+ },
+ ]
+ }
+ }
+
+ result = await steam_news.get_news_for_app(app_id=440, count=5)
+
+ assert "2 article(s)" in result
+ assert "First News" in result
+ assert "Second News" in result
+
+ @pytest.mark.asyncio
+ async def test_count_clamped_to_max(self, steam_news, mock_client):
+ """Should clamp count to maximum of 20."""
+ mock_client.get.return_value = {"appnews": {"newsitems": []}}
+
+ await steam_news.get_news_for_app(app_id=440, count=50)
+
+ # Verify the count passed to API is clamped
+ call_args = mock_client.get.call_args
+ assert call_args[1]["params"]["count"] == 20
+
+ @pytest.mark.asyncio
+ async def test_count_clamped_to_min(self, steam_news, mock_client):
+ """Should clamp count to minimum of 1."""
+ mock_client.get.return_value = {"appnews": {"newsitems": []}}
+
+ await steam_news.get_news_for_app(app_id=440, count=0)
+
+ call_args = mock_client.get.call_args
+ assert call_args[1]["params"]["count"] == 1
+
+ @pytest.mark.asyncio
+ async def test_external_url_marked(self, steam_news, mock_client):
+ """Should mark external URLs."""
+ mock_client.get.return_value = {
+ "appnews": {
+ "newsitems": [
+ {
+ "title": "External News",
+ "author": "Author",
+ "url": "https://external.com/news",
+ "contents": "Content",
+ "date": 1702000000,
+ "is_external_url": True,
+ }
+ ]
+ }
+ }
+
+ result = await steam_news.get_news_for_app(app_id=440)
+
+ assert "[External]" in result
+
+ @pytest.mark.asyncio
+ async def test_handles_missing_date(self, steam_news, mock_client):
+ """Should handle missing date gracefully."""
+ mock_client.get.return_value = {
+ "appnews": {
+ "newsitems": [
+ {
+ "title": "No Date News",
+ "author": "Author",
+ "url": "url",
+ "contents": "Content",
+ "date": 0,
+ }
+ ]
+ }
+ }
+
+ result = await steam_news.get_news_for_app(app_id=440)
+
+ assert "Unknown date" in result
+
+ @pytest.mark.asyncio
+ async def test_max_length_passed_to_api(self, steam_news, mock_client):
+ """Should pass max_length parameter to API."""
+ mock_client.get.return_value = {"appnews": {"newsitems": []}}
+
+ await steam_news.get_news_for_app(app_id=440, max_length=500)
+
+ call_args = mock_client.get.call_args
+ assert call_args[1]["params"]["maxlength"] == 500
+
+ @pytest.mark.asyncio
+ async def test_zero_max_length_for_full_content(self, steam_news, mock_client):
+ """Should pass 0 for full content when max_length is 0."""
+ mock_client.get.return_value = {"appnews": {"newsitems": []}}
+
+ await steam_news.get_news_for_app(app_id=440, max_length=0)
+
+ call_args = mock_client.get.call_args
+ assert call_args[1]["params"]["maxlength"] == 0
+
+
+class TestCleanHtml:
+ """Tests for _clean_html helper method."""
+
+ @pytest.fixture
+ def steam_news_instance(self, mock_client):
+ """Create ISteamNews instance for testing."""
+ return ISteamNews(mock_client)
+
+ def test_removes_html_tags(self, steam_news_instance):
+ """Should remove HTML tags."""
+ text = "
Hello World
"
+ result = steam_news_instance._clean_html(text)
+
+ assert "" not in result
+ assert "" not in result
+ assert "Hello" in result
+ assert "World" in result
+
+ def test_decodes_html_entities(self, steam_news_instance):
+ """Should decode common HTML entities."""
+ text = "Hello & World <test> "quoted""
+ result = steam_news_instance._clean_html(text)
+
+ assert "&" in result
+ assert "" in result
+ assert '"quoted"' in result
+
+ def test_normalizes_whitespace(self, steam_news_instance):
+ """Should normalize multiple whitespaces."""
+ text = "Hello World\n\nTest"
+ result = steam_news_instance._clean_html(text)
+
+ assert " " not in result
+ assert result == "Hello World Test"
+
+ def test_handles_empty_string(self, steam_news_instance):
+ """Should handle empty string."""
+ result = steam_news_instance._clean_html("")
+
+ assert result == ""
+
+ def test_handles_none_like_empty(self, steam_news_instance):
+ """Should handle None-like empty values."""
+ result = steam_news_instance._clean_html(None)
+
+ assert result == ""
+
+ def test_replaces_nbsp(self, steam_news_instance):
+ """Should replace with space."""
+ text = "Hello World"
+ result = steam_news_instance._clean_html(text)
+
+ assert result == "Hello World"
+
+
+class TestContentTruncation:
+ """Tests for content truncation in output."""
+
+ @pytest.mark.asyncio
+ async def test_long_content_truncated(self, steam_news, mock_client):
+ """Should truncate content longer than MAX_CONTENT_LENGTH."""
+ long_content = "A" * 600 # Longer than MAX_CONTENT_LENGTH (500)
+ mock_client.get.return_value = {
+ "appnews": {
+ "newsitems": [
+ {
+ "title": "Long Content",
+ "author": "Author",
+ "url": "url",
+ "contents": long_content,
+ "date": 1702000000,
+ }
+ ]
+ }
+ }
+
+ result = await steam_news.get_news_for_app(app_id=440)
+
+ # Content should be truncated and have ellipsis
+ assert "..." in result
+ # Should not contain all 600 As
+ assert "A" * 600 not in result
+
+ @pytest.mark.asyncio
+ async def test_short_content_not_truncated(self, steam_news, mock_client):
+ """Should not truncate content shorter than MAX_CONTENT_LENGTH."""
+ short_content = "Short content"
+ mock_client.get.return_value = {
+ "appnews": {
+ "newsitems": [
+ {
+ "title": "Short Content",
+ "author": "Author",
+ "url": "url",
+ "contents": short_content,
+ "date": 1702000000,
+ }
+ ]
+ }
+ }
+
+ result = await steam_news.get_news_for_app(app_id=440)
+
+ assert "Short content" in result
diff --git a/tests/test_steam_trading.py b/tests/test_steam_trading.py
index 2b8549a..0792391 100644
--- a/tests/test_steam_trading.py
+++ b/tests/test_steam_trading.py
@@ -58,7 +58,7 @@ async def test_returns_incoming_offers(self, econ_service, mock_client):
}
with patch(
- "steam_mcp.endpoints.steam_trading.normalize_steam_id",
+ "steam_mcp.endpoints.base.normalize_steam_id",
new_callable=AsyncMock,
return_value="76561198000000001",
):
@@ -100,7 +100,7 @@ async def test_returns_outgoing_offers(self, econ_service, mock_client):
}
with patch(
- "steam_mcp.endpoints.steam_trading.normalize_steam_id",
+ "steam_mcp.endpoints.base.normalize_steam_id",
new_callable=AsyncMock,
return_value="76561198000000001",
):
@@ -125,7 +125,7 @@ async def test_no_offers_found(self, econ_service, mock_client):
}
with patch(
- "steam_mcp.endpoints.steam_trading.normalize_steam_id",
+ "steam_mcp.endpoints.base.normalize_steam_id",
new_callable=AsyncMock,
return_value="76561198000000001",
):
@@ -139,7 +139,7 @@ async def test_no_offers_found(self, econ_service, mock_client):
async def test_invalid_steam_id(self, econ_service):
"""Should return error for invalid Steam ID."""
with patch(
- "steam_mcp.endpoints.steam_trading.normalize_steam_id",
+ "steam_mcp.endpoints.base.normalize_steam_id",
new_callable=AsyncMock,
side_effect=SteamIDError("Invalid Steam ID"),
):
@@ -194,7 +194,7 @@ async def test_returns_trade_history(self, econ_service, mock_client):
}
with patch(
- "steam_mcp.endpoints.steam_trading.normalize_steam_id",
+ "steam_mcp.endpoints.base.normalize_steam_id",
new_callable=AsyncMock,
return_value="76561198000000001",
):
@@ -219,7 +219,7 @@ async def test_empty_trade_history(self, econ_service, mock_client):
}
with patch(
- "steam_mcp.endpoints.steam_trading.normalize_steam_id",
+ "steam_mcp.endpoints.base.normalize_steam_id",
new_callable=AsyncMock,
return_value="76561198000000001",
):
@@ -298,7 +298,7 @@ async def test_eligible_user(self, econ_service, mock_client):
}
with patch(
- "steam_mcp.endpoints.steam_trading.normalize_steam_id",
+ "steam_mcp.endpoints.base.normalize_steam_id",
new_callable=AsyncMock,
return_value="76561198000000001",
):
@@ -321,7 +321,7 @@ async def test_ineligible_user_with_reason(self, econ_service, mock_client):
}
with patch(
- "steam_mcp.endpoints.steam_trading.normalize_steam_id",
+ "steam_mcp.endpoints.base.normalize_steam_id",
new_callable=AsyncMock,
return_value="76561198000000001",
):
@@ -339,7 +339,7 @@ async def test_empty_response_handled(self, econ_service, mock_client):
mock_client.get.return_value = {"response": {}}
with patch(
- "steam_mcp.endpoints.steam_trading.normalize_steam_id",
+ "steam_mcp.endpoints.base.normalize_steam_id",
new_callable=AsyncMock,
return_value="76561198000000001",
):
diff --git a/tests/test_steam_user.py b/tests/test_steam_user.py
new file mode 100644
index 0000000..1f88067
--- /dev/null
+++ b/tests/test_steam_user.py
@@ -0,0 +1,453 @@
+"""Tests for ISteamUser endpoints."""
+
+from unittest.mock import AsyncMock, MagicMock, patch
+
+import pytest
+
+from steam_mcp.client import SteamAPIError
+from steam_mcp.endpoints.steam_user import ISteamUser
+from steam_mcp.utils.steam_id import SteamIDError
+
+
+@pytest.fixture
+def mock_client():
+ """Create mock Steam client."""
+ client = MagicMock()
+ client.owner_steam_id = None
+ client.get = AsyncMock()
+ client.get_player_summaries = AsyncMock()
+ client.resolve_vanity_url = AsyncMock()
+ return client
+
+
+@pytest.fixture
+def steam_user(mock_client):
+ """Create ISteamUser instance with mock client."""
+ return ISteamUser(mock_client)
+
+
+class TestGetMySteamId:
+ """Tests for get_my_steam_id endpoint."""
+
+ @pytest.mark.asyncio
+ async def test_no_owner_configured(self, steam_user):
+ """Should return instructions when no owner ID is set."""
+ result = await steam_user.get_my_steam_id()
+
+ assert "No owner Steam ID configured" in result
+ assert "STEAM_USER_ID" in result
+
+ @pytest.mark.asyncio
+ async def test_owner_configured_with_profile(self, steam_user, mock_client):
+ """Should return owner info when ID is configured."""
+ mock_client.owner_steam_id = "76561198000000001"
+ mock_client.get_player_summaries.return_value = [
+ {
+ "steamid": "76561198000000001",
+ "personaname": "TestUser",
+ "profileurl": "https://steamcommunity.com/profiles/76561198000000001",
+ }
+ ]
+
+ result = await steam_user.get_my_steam_id()
+
+ assert "Owner Steam ID configured" in result
+ assert "TestUser" in result
+ assert "76561198000000001" in result
+
+ @pytest.mark.asyncio
+ async def test_owner_configured_profile_fetch_fails(self, steam_user, mock_client):
+ """Should return basic info if profile fetch fails."""
+ mock_client.owner_steam_id = "76561198000000001"
+ mock_client.get_player_summaries.return_value = []
+
+ result = await steam_user.get_my_steam_id()
+
+ assert "76561198000000001" in result
+ assert "Could not fetch profile details" in result
+
+
+class TestGetPlayerSummary:
+ """Tests for get_player_summary endpoint."""
+
+ @pytest.mark.asyncio
+ async def test_valid_steam_id(self, steam_user, mock_client):
+ """Should return player summary for valid ID."""
+ mock_client.get_player_summaries.return_value = [
+ {
+ "steamid": "76561198000000001",
+ "personaname": "TestPlayer",
+ "profileurl": "https://steamcommunity.com/profiles/76561198000000001",
+ "communityvisibilitystate": 3,
+ "personastate": 1,
+ }
+ ]
+
+ with patch(
+ "steam_mcp.endpoints.base.normalize_steam_id",
+ new_callable=AsyncMock,
+ return_value="76561198000000001",
+ ):
+ result = await steam_user.get_player_summary(steam_id="76561198000000001")
+
+ assert "TestPlayer" in result
+ assert "76561198000000001" in result
+ assert "Online" in result
+
+ @pytest.mark.asyncio
+ async def test_player_not_found(self, steam_user, mock_client):
+ """Should return error when player not found."""
+ mock_client.get_player_summaries.return_value = []
+
+ with patch(
+ "steam_mcp.endpoints.base.normalize_steam_id",
+ new_callable=AsyncMock,
+ return_value="76561198000000001",
+ ):
+ result = await steam_user.get_player_summary(steam_id="76561198000000001")
+
+ assert "Player not found" in result
+
+ @pytest.mark.asyncio
+ async def test_json_format(self, steam_user, mock_client):
+ """Should return JSON when format is json."""
+ mock_client.get_player_summaries.return_value = [
+ {
+ "steamid": "76561198000000001",
+ "personaname": "TestPlayer",
+ "profileurl": "https://steamcommunity.com/profiles/76561198000000001",
+ "communityvisibilitystate": 3,
+ "personastate": 1,
+ }
+ ]
+
+ with patch(
+ "steam_mcp.endpoints.base.normalize_steam_id",
+ new_callable=AsyncMock,
+ return_value="76561198000000001",
+ ):
+ result = await steam_user.get_player_summary(
+ steam_id="76561198000000001", format="json"
+ )
+
+ assert '"steam_id"' in result
+ assert '"persona_name"' in result
+
+ @pytest.mark.asyncio
+ async def test_invalid_steam_id(self, steam_user):
+ """Should return error for invalid Steam ID."""
+ with patch(
+ "steam_mcp.endpoints.base.normalize_steam_id",
+ new_callable=AsyncMock,
+ side_effect=SteamIDError("Invalid Steam ID"),
+ ):
+ result = await steam_user.get_player_summary(steam_id="invalid")
+
+ assert "Error" in result
+
+
+class TestGetPlayerSummaries:
+ """Tests for get_player_summaries endpoint."""
+
+ @pytest.mark.asyncio
+ async def test_empty_list(self, steam_user):
+ """Should return error for empty list."""
+ result = await steam_user.get_player_summaries(steam_ids=[])
+
+ assert "Error" in result
+ assert "No Steam IDs provided" in result
+
+ @pytest.mark.asyncio
+ async def test_exceeds_max_limit(self, steam_user):
+ """Should return error when exceeding 100 IDs."""
+ result = await steam_user.get_player_summaries(steam_ids=["id"] * 101)
+
+ assert "Error" in result
+ assert "Maximum 100" in result
+
+ @pytest.mark.asyncio
+ async def test_multiple_players(self, steam_user, mock_client):
+ """Should return summaries for multiple players."""
+ mock_client.get_player_summaries.return_value = [
+ {
+ "steamid": "76561198000000001",
+ "personaname": "Player1",
+ "profileurl": "url1",
+ "communityvisibilitystate": 3,
+ "personastate": 1,
+ },
+ {
+ "steamid": "76561198000000002",
+ "personaname": "Player2",
+ "profileurl": "url2",
+ "communityvisibilitystate": 3,
+ "personastate": 0,
+ },
+ ]
+
+ with patch(
+ "steam_mcp.endpoints.steam_user.normalize_steam_id",
+ new_callable=AsyncMock,
+ side_effect=["76561198000000001", "76561198000000002"],
+ ):
+ result = await steam_user.get_player_summaries(
+ steam_ids=["76561198000000001", "76561198000000002"]
+ )
+
+ assert "Found 2 player(s)" in result
+ assert "Player1" in result
+ assert "Player2" in result
+
+ @pytest.mark.asyncio
+ async def test_partial_resolution_failure(self, steam_user, mock_client):
+ """Should continue with valid IDs when some fail."""
+ mock_client.get_player_summaries.return_value = [
+ {
+ "steamid": "76561198000000001",
+ "personaname": "ValidPlayer",
+ "profileurl": "url",
+ "communityvisibilitystate": 3,
+ "personastate": 1,
+ }
+ ]
+
+ with patch(
+ "steam_mcp.endpoints.steam_user.normalize_steam_id",
+ new_callable=AsyncMock,
+ side_effect=["76561198000000001", SteamIDError("Invalid")],
+ ):
+ result = await steam_user.get_player_summaries(
+ steam_ids=["76561198000000001", "invalid_id"]
+ )
+
+ assert "Found 1 player(s)" in result
+ assert "ValidPlayer" in result
+ assert "Errors" in result
+
+
+class TestResolveVanityUrl:
+ """Tests for resolve_vanity_url endpoint."""
+
+ @pytest.mark.asyncio
+ async def test_valid_vanity_name(self, steam_user, mock_client):
+ """Should resolve vanity name to Steam ID."""
+ mock_client.resolve_vanity_url.return_value = "76561198000000001"
+
+ result = await steam_user.resolve_vanity_url(vanity_name="testuser")
+
+ assert "76561198000000001" in result
+ assert "testuser" in result
+ mock_client.resolve_vanity_url.assert_called_once_with("testuser")
+
+ @pytest.mark.asyncio
+ async def test_full_url_input(self, steam_user, mock_client):
+ """Should extract vanity name from full URL."""
+ mock_client.resolve_vanity_url.return_value = "76561198000000001"
+
+ await steam_user.resolve_vanity_url(
+ vanity_name="https://steamcommunity.com/id/testuser/"
+ )
+
+ mock_client.resolve_vanity_url.assert_called_once_with("testuser")
+
+ @pytest.mark.asyncio
+ async def test_vanity_not_found(self, steam_user, mock_client):
+ """Should return error when vanity not found."""
+ mock_client.resolve_vanity_url.return_value = None
+
+ result = await steam_user.resolve_vanity_url(vanity_name="nonexistent")
+
+ assert "Could not resolve" in result
+
+
+class TestGetFriendList:
+ """Tests for get_friend_list endpoint."""
+
+ @pytest.mark.asyncio
+ async def test_public_profile_with_friends(self, steam_user, mock_client):
+ """Should return friend list for public profile."""
+ mock_client.get.return_value = {
+ "friendslist": {
+ "friends": [
+ {"steamid": "76561198000000002", "friend_since": 1600000000},
+ {"steamid": "76561198000000003", "friend_since": 1700000000},
+ ]
+ }
+ }
+
+ with patch(
+ "steam_mcp.endpoints.base.normalize_steam_id",
+ new_callable=AsyncMock,
+ return_value="76561198000000001",
+ ):
+ result = await steam_user.get_friend_list(steam_id="76561198000000001")
+
+ assert "2 friends" in result
+ assert "76561198000000002" in result
+ assert "76561198000000003" in result
+
+ @pytest.mark.asyncio
+ async def test_private_profile(self, steam_user, mock_client):
+ """Should return error for private profile."""
+ mock_client.get.side_effect = SteamAPIError("Unauthorized", status_code=401)
+
+ with patch(
+ "steam_mcp.endpoints.base.normalize_steam_id",
+ new_callable=AsyncMock,
+ return_value="76561198000000001",
+ ):
+ result = await steam_user.get_friend_list(steam_id="76561198000000001")
+
+ assert "private" in result.lower()
+
+ @pytest.mark.asyncio
+ async def test_no_friends(self, steam_user, mock_client):
+ """Should handle empty friend list."""
+ mock_client.get.return_value = {"friendslist": {"friends": []}}
+
+ with patch(
+ "steam_mcp.endpoints.base.normalize_steam_id",
+ new_callable=AsyncMock,
+ return_value="76561198000000001",
+ ):
+ result = await steam_user.get_friend_list(steam_id="76561198000000001")
+
+ assert "No friends found" in result
+
+ @pytest.mark.asyncio
+ async def test_json_format(self, steam_user, mock_client):
+ """Should return JSON when format is json."""
+ mock_client.get.return_value = {
+ "friendslist": {
+ "friends": [
+ {"steamid": "76561198000000002", "friend_since": 1600000000}
+ ]
+ }
+ }
+
+ with patch(
+ "steam_mcp.endpoints.base.normalize_steam_id",
+ new_callable=AsyncMock,
+ return_value="76561198000000001",
+ ):
+ result = await steam_user.get_friend_list(
+ steam_id="76561198000000001", format="json"
+ )
+
+ assert '"steam_id"' in result
+ assert '"total_friends"' in result
+
+
+class TestGetPlayerBans:
+ """Tests for get_player_bans endpoint."""
+
+ @pytest.mark.asyncio
+ async def test_empty_list(self, steam_user):
+ """Should return error for empty list."""
+ result = await steam_user.get_player_bans(steam_ids=[])
+
+ assert "Error" in result
+ assert "No Steam IDs provided" in result
+
+ @pytest.mark.asyncio
+ async def test_exceeds_max_limit(self, steam_user):
+ """Should return error when exceeding 100 IDs."""
+ result = await steam_user.get_player_bans(steam_ids=["id"] * 101)
+
+ assert "Error" in result
+ assert "Maximum 100" in result
+
+ @pytest.mark.asyncio
+ async def test_clean_player(self, steam_user, mock_client):
+ """Should show clean status for unbanned player."""
+ mock_client.get.return_value = {
+ "players": [
+ {
+ "SteamId": "76561198000000001",
+ "VACBanned": False,
+ "NumberOfVACBans": 0,
+ "NumberOfGameBans": 0,
+ "CommunityBanned": False,
+ "EconomyBan": "none",
+ }
+ ]
+ }
+
+ with patch(
+ "steam_mcp.endpoints.steam_user.normalize_steam_id",
+ new_callable=AsyncMock,
+ return_value="76561198000000001",
+ ):
+ result = await steam_user.get_player_bans(steam_ids=["76561198000000001"])
+
+ assert "VAC Banned: No" in result
+ assert "Game Bans: None" in result
+ assert "Community Banned: No" in result
+
+ @pytest.mark.asyncio
+ async def test_banned_player(self, steam_user, mock_client):
+ """Should show ban details for banned player."""
+ mock_client.get.return_value = {
+ "players": [
+ {
+ "SteamId": "76561198000000001",
+ "VACBanned": True,
+ "NumberOfVACBans": 2,
+ "DaysSinceLastBan": 365,
+ "NumberOfGameBans": 1,
+ "CommunityBanned": True,
+ "EconomyBan": "banned",
+ }
+ ]
+ }
+
+ with patch(
+ "steam_mcp.endpoints.steam_user.normalize_steam_id",
+ new_callable=AsyncMock,
+ return_value="76561198000000001",
+ ):
+ result = await steam_user.get_player_bans(steam_ids=["76561198000000001"])
+
+ assert "VAC Banned: Yes" in result
+ assert "2 ban(s)" in result
+ assert "365 days ago" in result
+ assert "Game Bans: 1" in result
+ assert "Community Banned: Yes" in result
+
+ @pytest.mark.asyncio
+ async def test_multiple_players(self, steam_user, mock_client):
+ """Should return ban info for multiple players."""
+ mock_client.get.return_value = {
+ "players": [
+ {
+ "SteamId": "76561198000000001",
+ "VACBanned": False,
+ "NumberOfVACBans": 0,
+ "NumberOfGameBans": 0,
+ "CommunityBanned": False,
+ "EconomyBan": "none",
+ },
+ {
+ "SteamId": "76561198000000002",
+ "VACBanned": True,
+ "NumberOfVACBans": 1,
+ "DaysSinceLastBan": 100,
+ "NumberOfGameBans": 0,
+ "CommunityBanned": False,
+ "EconomyBan": "none",
+ },
+ ]
+ }
+
+ with patch(
+ "steam_mcp.endpoints.steam_user.normalize_steam_id",
+ new_callable=AsyncMock,
+ side_effect=["76561198000000001", "76561198000000002"],
+ ):
+ result = await steam_user.get_player_bans(
+ steam_ids=["76561198000000001", "76561198000000002"]
+ )
+
+ assert "2 player(s)" in result
+ assert "76561198000000001" in result
+ assert "76561198000000002" in result
diff --git a/tests/test_user_stats.py b/tests/test_user_stats.py
new file mode 100644
index 0000000..54d04b3
--- /dev/null
+++ b/tests/test_user_stats.py
@@ -0,0 +1,421 @@
+"""Tests for ISteamUserStats endpoints."""
+
+from unittest.mock import AsyncMock, MagicMock, patch
+
+import pytest
+
+from steam_mcp.endpoints.user_stats import ISteamUserStats
+from steam_mcp.utils.steam_id import SteamIDError
+
+
+@pytest.fixture
+def mock_client():
+ """Create mock Steam client."""
+ client = MagicMock()
+ client.owner_steam_id = None
+ client.get = AsyncMock()
+ return client
+
+
+@pytest.fixture
+def user_stats(mock_client):
+ """Create ISteamUserStats instance with mock client."""
+ return ISteamUserStats(mock_client)
+
+
+class TestGetPlayerAchievements:
+ """Tests for get_player_achievements endpoint."""
+
+ @pytest.mark.asyncio
+ async def test_returns_achievements(self, user_stats, mock_client):
+ """Should return achievements for a player."""
+ mock_client.get.return_value = {
+ "playerstats": {
+ "success": True,
+ "steamID": "76561198000000001",
+ "gameName": "Test Game",
+ "achievements": [
+ {"apiname": "ACH_1", "name": "First Achievement", "achieved": 1, "unlocktime": 1700000000},
+ {"apiname": "ACH_2", "name": "Second Achievement", "achieved": 0},
+ ],
+ }
+ }
+
+ with patch(
+ "steam_mcp.endpoints.base.normalize_steam_id",
+ new_callable=AsyncMock,
+ return_value="76561198000000001",
+ ):
+ result = await user_stats.get_player_achievements(
+ steam_id="76561198000000001", app_id=440
+ )
+
+ assert "Test Game" in result
+ assert "1/2" in result
+ assert "First Achievement" in result
+ assert "✓" in result # Unlocked marker
+
+ @pytest.mark.asyncio
+ async def test_json_format(self, user_stats, mock_client):
+ """Should return JSON when format is json."""
+ mock_client.get.return_value = {
+ "playerstats": {
+ "success": True,
+ "steamID": "76561198000000001",
+ "gameName": "Test Game",
+ "achievements": [
+ {"apiname": "ACH_1", "name": "Test", "achieved": 1},
+ ],
+ }
+ }
+
+ with patch(
+ "steam_mcp.endpoints.base.normalize_steam_id",
+ new_callable=AsyncMock,
+ return_value="76561198000000001",
+ ):
+ result = await user_stats.get_player_achievements(
+ steam_id="76561198000000001", app_id=440, format="json"
+ )
+
+ assert '"steam_id"' in result
+ assert '"game_name"' in result
+ assert '"achievements"' in result
+
+ @pytest.mark.asyncio
+ async def test_private_profile_error(self, user_stats, mock_client):
+ """Should return error for private profile."""
+ mock_client.get.side_effect = Exception("Profile is not public")
+
+ with patch(
+ "steam_mcp.endpoints.base.normalize_steam_id",
+ new_callable=AsyncMock,
+ return_value="76561198000000001",
+ ):
+ result = await user_stats.get_player_achievements(
+ steam_id="76561198000000001", app_id=440
+ )
+
+ assert "private" in result.lower()
+
+ @pytest.mark.asyncio
+ async def test_no_achievements(self, user_stats, mock_client):
+ """Should handle game with no achievements."""
+ mock_client.get.return_value = {
+ "playerstats": {
+ "success": True,
+ "steamID": "76561198000000001",
+ "gameName": "No Achievements Game",
+ "achievements": [],
+ }
+ }
+
+ with patch(
+ "steam_mcp.endpoints.base.normalize_steam_id",
+ new_callable=AsyncMock,
+ return_value="76561198000000001",
+ ):
+ result = await user_stats.get_player_achievements(
+ steam_id="76561198000000001", app_id=440
+ )
+
+ assert "No achievements found" in result
+
+ @pytest.mark.asyncio
+ async def test_invalid_steam_id(self, user_stats):
+ """Should return error for invalid Steam ID."""
+ with patch(
+ "steam_mcp.endpoints.base.normalize_steam_id",
+ new_callable=AsyncMock,
+ side_effect=SteamIDError("Invalid Steam ID"),
+ ):
+ result = await user_stats.get_player_achievements(
+ steam_id="invalid", app_id=440
+ )
+
+ assert "Error" in result
+
+
+class TestGetGameSchema:
+ """Tests for get_game_schema endpoint."""
+
+ @pytest.mark.asyncio
+ async def test_returns_schema(self, user_stats, mock_client):
+ """Should return game schema."""
+ mock_client.get.return_value = {
+ "game": {
+ "gameName": "Test Game",
+ "availableGameStats": {
+ "achievements": [
+ {"name": "ACH_1", "displayName": "First", "hidden": 0},
+ {"name": "ACH_2", "displayName": "Second", "hidden": 1},
+ ],
+ "stats": [
+ {"name": "kills", "displayName": "Total Kills"},
+ ],
+ },
+ }
+ }
+
+ result = await user_stats.get_game_schema(app_id=440)
+
+ assert "Test Game" in result
+ assert "2 total" in result
+ assert "1 hidden" in result
+ assert "First" in result
+ assert "[HIDDEN]" in result
+
+ @pytest.mark.asyncio
+ async def test_no_schema_found(self, user_stats, mock_client):
+ """Should handle missing schema."""
+ mock_client.get.return_value = {"game": {}}
+
+ result = await user_stats.get_game_schema(app_id=99999)
+
+ assert "No schema found" in result
+
+ @pytest.mark.asyncio
+ async def test_no_achievements(self, user_stats, mock_client):
+ """Should handle game with no achievements."""
+ mock_client.get.return_value = {
+ "game": {
+ "gameName": "No Achievements",
+ "availableGameStats": {
+ "achievements": [],
+ "stats": [],
+ },
+ }
+ }
+
+ result = await user_stats.get_game_schema(app_id=440)
+
+ assert "No achievements for this game" in result
+
+
+class TestGetGlobalAchievementPercentages:
+ """Tests for get_global_achievement_percentages endpoint."""
+
+ @pytest.mark.asyncio
+ async def test_returns_percentages(self, user_stats, mock_client):
+ """Should return achievement percentages sorted by rarity."""
+ mock_client.get.return_value = {
+ "achievementpercentages": {
+ "achievements": [
+ {"name": "Common", "percent": 80.0},
+ {"name": "Ultra Rare", "percent": 0.5},
+ {"name": "Rare", "percent": 5.0},
+ ]
+ }
+ }
+
+ result = await user_stats.get_global_achievement_percentages(app_id=440)
+
+ assert "Ultra Rare" in result
+ assert "0.5%" in result
+ assert "Rarest achievements" in result
+
+ @pytest.mark.asyncio
+ async def test_rarity_indicators(self, user_stats, mock_client):
+ """Should show rarity indicators."""
+ mock_client.get.return_value = {
+ "achievementpercentages": {
+ "achievements": [
+ {"name": "Ultra", "percent": 0.5},
+ {"name": "Very Rare", "percent": 3.0},
+ {"name": "Rare", "percent": 8.0},
+ ]
+ }
+ }
+
+ result = await user_stats.get_global_achievement_percentages(app_id=440)
+
+ assert "🏆" in result # Ultra rare
+ assert "💎" in result # Very rare
+ assert "⭐" in result # Rare
+
+ @pytest.mark.asyncio
+ async def test_no_data(self, user_stats, mock_client):
+ """Should handle no achievement data."""
+ mock_client.get.return_value = {
+ "achievementpercentages": {"achievements": []}
+ }
+
+ result = await user_stats.get_global_achievement_percentages(app_id=99999)
+
+ assert "No global achievement data" in result
+
+
+class TestGetUserStatsForGame:
+ """Tests for get_user_stats_for_game endpoint."""
+
+ @pytest.mark.asyncio
+ async def test_returns_stats(self, user_stats, mock_client):
+ """Should return player stats."""
+ mock_client.get.return_value = {
+ "playerstats": {
+ "steamID": "76561198000000001",
+ "gameName": "Test Game",
+ "stats": [
+ {"name": "total_kills", "value": 1234567},
+ {"name": "accuracy", "value": 0.75},
+ ],
+ "achievements": [
+ {"name": "ACH_1", "achieved": 1},
+ {"name": "ACH_2", "achieved": 0},
+ ],
+ }
+ }
+
+ with patch(
+ "steam_mcp.endpoints.base.normalize_steam_id",
+ new_callable=AsyncMock,
+ return_value="76561198000000001",
+ ):
+ result = await user_stats.get_user_stats_for_game(
+ steam_id="76561198000000001", app_id=440
+ )
+
+ assert "Test Game" in result
+ assert "total_kills" in result
+ assert "1,234,567" in result
+ assert "1/2 unlocked" in result
+
+ @pytest.mark.asyncio
+ async def test_private_profile(self, user_stats, mock_client):
+ """Should handle private profile."""
+ mock_client.get.side_effect = Exception("private profile")
+
+ with patch(
+ "steam_mcp.endpoints.base.normalize_steam_id",
+ new_callable=AsyncMock,
+ return_value="76561198000000001",
+ ):
+ result = await user_stats.get_user_stats_for_game(
+ steam_id="76561198000000001", app_id=440
+ )
+
+ assert "private" in result.lower()
+
+ @pytest.mark.asyncio
+ async def test_no_stats(self, user_stats, mock_client):
+ """Should handle no stats found."""
+ mock_client.get.return_value = {"playerstats": {}}
+
+ with patch(
+ "steam_mcp.endpoints.base.normalize_steam_id",
+ new_callable=AsyncMock,
+ return_value="76561198000000001",
+ ):
+ result = await user_stats.get_user_stats_for_game(
+ steam_id="76561198000000001", app_id=440
+ )
+
+ assert "No stats found" in result
+
+
+class TestGetCurrentPlayers:
+ """Tests for get_current_players endpoint."""
+
+ @pytest.mark.asyncio
+ async def test_returns_player_count(self, user_stats, mock_client):
+ """Should return current player count."""
+ mock_client.get.return_value = {
+ "response": {"result": 1, "player_count": 50000}
+ }
+
+ result = await user_stats.get_current_players(app_id=440)
+
+ assert "50,000" in result
+ assert "440" in result
+
+ @pytest.mark.asyncio
+ async def test_popularity_indicators(self, user_stats, mock_client):
+ """Should show popularity indicators based on count."""
+ # Extremely popular
+ mock_client.get.return_value = {
+ "response": {"result": 1, "player_count": 150000}
+ }
+ result = await user_stats.get_current_players(app_id=440)
+ assert "Extremely Popular" in result
+
+ # Very active
+ mock_client.get.return_value = {
+ "response": {"result": 1, "player_count": 15000}
+ }
+ result = await user_stats.get_current_players(app_id=440)
+ assert "Very Active" in result
+
+ # Low
+ mock_client.get.return_value = {
+ "response": {"result": 1, "player_count": 50}
+ }
+ result = await user_stats.get_current_players(app_id=440)
+ assert "Low" in result
+
+ @pytest.mark.asyncio
+ async def test_invalid_app(self, user_stats, mock_client):
+ """Should handle invalid app ID."""
+ mock_client.get.return_value = {"response": {"result": 0}}
+
+ result = await user_stats.get_current_players(app_id=99999)
+
+ assert "Could not get player count" in result
+
+
+class TestGetGlobalStatsForGame:
+ """Tests for get_global_stats_for_game endpoint."""
+
+ @pytest.mark.asyncio
+ async def test_returns_global_stats(self, user_stats, mock_client):
+ """Should return global stats."""
+ mock_client.get.return_value = {
+ "response": {
+ "result": 1,
+ "globalstats": {
+ "total_kills": {"total": 1000000000},
+ "total_deaths": {"total": 500000000},
+ },
+ }
+ }
+
+ result = await user_stats.get_global_stats_for_game(
+ app_id=440, stat_names=["total_kills", "total_deaths"]
+ )
+
+ assert "total_kills" in result
+ assert "1,000,000,000" in result
+ assert "total_deaths" in result
+
+ @pytest.mark.asyncio
+ async def test_empty_stat_names(self, user_stats):
+ """Should return error for empty stat names."""
+ result = await user_stats.get_global_stats_for_game(
+ app_id=440, stat_names=[]
+ )
+
+ assert "Error" in result
+ assert "at least one stat name" in result
+
+ @pytest.mark.asyncio
+ async def test_invalid_stats(self, user_stats, mock_client):
+ """Should handle invalid stat names."""
+ mock_client.get.return_value = {"response": {"result": 0}}
+
+ result = await user_stats.get_global_stats_for_game(
+ app_id=440, stat_names=["invalid_stat"]
+ )
+
+ assert "Could not get global stats" in result
+
+ @pytest.mark.asyncio
+ async def test_no_stats_returned(self, user_stats, mock_client):
+ """Should handle empty globalstats response."""
+ mock_client.get.return_value = {
+ "response": {"result": 1, "globalstats": {}}
+ }
+
+ result = await user_stats.get_global_stats_for_game(
+ app_id=440, stat_names=["some_stat"]
+ )
+
+ assert "No global stats returned" in result