60e0ffc959
Upgrade checks / Notify on failure (push) Has been cancelled
Upgrade checks / Close issue on success (push) Has been cancelled
Schema Crash Test / Real-world schema crash test (232K schemas) (push) Has been cancelled
Run static analysis / static_analysis (push) Has been cancelled
Tests / Tests: Python 3.10 on ubuntu-latest (push) Has been cancelled
Tests / Tests: Python 3.13 on ubuntu-latest (push) Has been cancelled
Tests / Tests: Python 3.10 on windows-latest (push) Has been cancelled
Tests / Tests with lowest-direct dependencies (push) Has been cancelled
Tests / MCP conformance tests (push) Has been cancelled
Tests / Integration tests (push) Has been cancelled
Tests / Package install smoke (push) Has been cancelled
Upgrade checks / Static analysis (push) Has been cancelled
Upgrade checks / Tests: Python 3.10 on ubuntu-latest (push) Has been cancelled
Upgrade checks / Tests: Python 3.13 on ubuntu-latest (push) Has been cancelled
Upgrade checks / Tests: Python 3.10 on windows-latest (push) Has been cancelled
Upgrade checks / Integration tests (push) Has been cancelled
Update MCPServerConfig Schema / update-config-schema (push) Has been cancelled
Update SDK Documentation / update-sdk-docs (push) Has been cancelled
316 lines
11 KiB
Python
316 lines
11 KiB
Python
"""Tests for version checking utilities."""
|
|
|
|
import json
|
|
import time
|
|
from pathlib import Path
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import httpx
|
|
import pytest
|
|
|
|
from fastmcp.utilities.version_check import (
|
|
CACHE_TTL_SECONDS,
|
|
_fetch_latest_version,
|
|
_get_cache_path,
|
|
_read_cache,
|
|
_write_cache,
|
|
check_for_newer_version,
|
|
get_latest_version,
|
|
)
|
|
|
|
|
|
class TestCachePath:
|
|
def test_cache_path_in_home_directory(self):
|
|
"""Cache file should be in fastmcp home directory."""
|
|
cache_path = _get_cache_path()
|
|
assert cache_path.name == "version_cache.json"
|
|
assert "fastmcp" in str(cache_path).lower()
|
|
|
|
def test_cache_path_prerelease_suffix(self):
|
|
"""Prerelease cache uses different file."""
|
|
cache_path = _get_cache_path(include_prereleases=True)
|
|
assert cache_path.name == "version_cache_prerelease.json"
|
|
|
|
|
|
class TestReadCache:
|
|
def test_read_cache_no_file(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
|
|
"""Reading non-existent cache returns None."""
|
|
monkeypatch.setattr(
|
|
"fastmcp.utilities.version_check._get_cache_path",
|
|
lambda include_prereleases=False: tmp_path / "nonexistent.json",
|
|
)
|
|
version, timestamp = _read_cache()
|
|
assert version is None
|
|
assert timestamp == 0
|
|
|
|
def test_read_cache_valid(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
|
|
"""Reading valid cache returns version and timestamp."""
|
|
cache_file = tmp_path / "version_cache.json"
|
|
cache_file.write_text(
|
|
json.dumps({"latest_version": "2.5.0", "timestamp": 1000})
|
|
)
|
|
monkeypatch.setattr(
|
|
"fastmcp.utilities.version_check._get_cache_path",
|
|
lambda include_prereleases=False: cache_file,
|
|
)
|
|
|
|
version, timestamp = _read_cache()
|
|
assert version == "2.5.0"
|
|
assert timestamp == 1000
|
|
|
|
def test_read_cache_invalid_json(
|
|
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
):
|
|
"""Reading invalid JSON returns None."""
|
|
cache_file = tmp_path / "version_cache.json"
|
|
cache_file.write_text("not valid json")
|
|
monkeypatch.setattr(
|
|
"fastmcp.utilities.version_check._get_cache_path",
|
|
lambda include_prereleases=False: cache_file,
|
|
)
|
|
|
|
version, timestamp = _read_cache()
|
|
assert version is None
|
|
assert timestamp == 0
|
|
|
|
|
|
class TestWriteCache:
|
|
def test_write_cache_creates_file(
|
|
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
):
|
|
"""Writing cache creates the cache file."""
|
|
cache_file = tmp_path / "subdir" / "version_cache.json"
|
|
monkeypatch.setattr(
|
|
"fastmcp.utilities.version_check._get_cache_path",
|
|
lambda include_prereleases=False: cache_file,
|
|
)
|
|
|
|
_write_cache("2.6.0")
|
|
|
|
assert cache_file.exists()
|
|
data = json.loads(cache_file.read_text())
|
|
assert data["latest_version"] == "2.6.0"
|
|
assert "timestamp" in data
|
|
|
|
|
|
class TestFetchLatestVersion:
|
|
def test_fetch_success(self):
|
|
"""Successful fetch returns highest stable version."""
|
|
mock_response = MagicMock()
|
|
mock_response.json.return_value = {
|
|
"releases": {
|
|
"2.5.0": [],
|
|
"2.4.0": [],
|
|
"2.6.0b1": [], # prerelease should be skipped
|
|
}
|
|
}
|
|
|
|
with patch("httpx.get", return_value=mock_response) as mock_get:
|
|
version = _fetch_latest_version()
|
|
assert version == "2.5.0"
|
|
mock_get.assert_called_once()
|
|
|
|
def test_fetch_network_error(self):
|
|
"""Network error returns None."""
|
|
with patch("httpx.get", side_effect=httpx.HTTPError("Network error")):
|
|
version = _fetch_latest_version()
|
|
assert version is None
|
|
|
|
def test_fetch_invalid_response(self):
|
|
"""Invalid response returns None."""
|
|
mock_response = MagicMock()
|
|
mock_response.json.return_value = {"unexpected": "format"}
|
|
|
|
with patch("httpx.get", return_value=mock_response):
|
|
version = _fetch_latest_version()
|
|
assert version is None
|
|
|
|
def test_fetch_prereleases(self):
|
|
"""Fetching with prereleases returns highest version."""
|
|
mock_response = MagicMock()
|
|
mock_response.json.return_value = {
|
|
"info": {"version": "2.5.0"},
|
|
"releases": {
|
|
"2.5.0": [],
|
|
"2.6.0b1": [],
|
|
"2.6.0b2": [],
|
|
"2.4.0": [],
|
|
},
|
|
}
|
|
|
|
with patch("httpx.get", return_value=mock_response):
|
|
version = _fetch_latest_version(include_prereleases=True)
|
|
assert version == "2.6.0b2"
|
|
|
|
def test_fetch_prereleases_stable_is_highest(self):
|
|
"""Prerelease mode still returns stable if it's highest."""
|
|
mock_response = MagicMock()
|
|
mock_response.json.return_value = {
|
|
"info": {"version": "2.5.0"},
|
|
"releases": {
|
|
"2.5.0": [],
|
|
"2.5.0b1": [],
|
|
"2.4.0": [],
|
|
},
|
|
}
|
|
|
|
with patch("httpx.get", return_value=mock_response):
|
|
version = _fetch_latest_version(include_prereleases=True)
|
|
assert version == "2.5.0"
|
|
|
|
|
|
class TestGetLatestVersion:
|
|
def test_returns_cached_version_if_fresh(
|
|
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
):
|
|
"""Uses cached version if cache is fresh."""
|
|
cache_file = tmp_path / "version_cache.json"
|
|
cache_file.write_text(
|
|
json.dumps({"latest_version": "2.5.0", "timestamp": time.time()})
|
|
)
|
|
monkeypatch.setattr(
|
|
"fastmcp.utilities.version_check._get_cache_path",
|
|
lambda include_prereleases=False: cache_file,
|
|
)
|
|
|
|
with patch(
|
|
"fastmcp.utilities.version_check._fetch_latest_version"
|
|
) as mock_fetch:
|
|
version = get_latest_version()
|
|
assert version == "2.5.0"
|
|
mock_fetch.assert_not_called()
|
|
|
|
def test_fetches_if_cache_stale(
|
|
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
):
|
|
"""Fetches from PyPI if cache is stale."""
|
|
cache_file = tmp_path / "version_cache.json"
|
|
old_timestamp = time.time() - CACHE_TTL_SECONDS - 100
|
|
cache_file.write_text(
|
|
json.dumps({"latest_version": "2.4.0", "timestamp": old_timestamp})
|
|
)
|
|
monkeypatch.setattr(
|
|
"fastmcp.utilities.version_check._get_cache_path",
|
|
lambda include_prereleases=False: cache_file,
|
|
)
|
|
|
|
with patch(
|
|
"fastmcp.utilities.version_check._fetch_latest_version",
|
|
return_value="2.5.0",
|
|
) as mock_fetch:
|
|
version = get_latest_version()
|
|
assert version == "2.5.0"
|
|
mock_fetch.assert_called_once()
|
|
|
|
def test_returns_stale_cache_if_fetch_fails(
|
|
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
):
|
|
"""Returns stale cache if fetch fails."""
|
|
cache_file = tmp_path / "version_cache.json"
|
|
old_timestamp = time.time() - CACHE_TTL_SECONDS - 100
|
|
cache_file.write_text(
|
|
json.dumps({"latest_version": "2.4.0", "timestamp": old_timestamp})
|
|
)
|
|
monkeypatch.setattr(
|
|
"fastmcp.utilities.version_check._get_cache_path",
|
|
lambda include_prereleases=False: cache_file,
|
|
)
|
|
|
|
with patch(
|
|
"fastmcp.utilities.version_check._fetch_latest_version", return_value=None
|
|
):
|
|
version = get_latest_version()
|
|
assert version == "2.4.0"
|
|
|
|
|
|
class TestCheckForNewerVersion:
|
|
def test_returns_none_if_disabled(self, monkeypatch: pytest.MonkeyPatch):
|
|
"""Returns None if check_for_updates is off."""
|
|
import fastmcp
|
|
|
|
monkeypatch.setattr(fastmcp.settings, "check_for_updates", "off")
|
|
|
|
result = check_for_newer_version()
|
|
assert result is None
|
|
|
|
def test_returns_none_if_current(self, monkeypatch: pytest.MonkeyPatch):
|
|
"""Returns None if current version is latest."""
|
|
import fastmcp
|
|
|
|
monkeypatch.setattr(fastmcp.settings, "check_for_updates", "stable")
|
|
monkeypatch.setattr(fastmcp, "__version__", "2.5.0")
|
|
|
|
with patch(
|
|
"fastmcp.utilities.version_check.get_latest_version", return_value="2.5.0"
|
|
):
|
|
result = check_for_newer_version()
|
|
assert result is None
|
|
|
|
def test_returns_version_if_newer(self, monkeypatch: pytest.MonkeyPatch):
|
|
"""Returns new version if available."""
|
|
import fastmcp
|
|
|
|
monkeypatch.setattr(fastmcp.settings, "check_for_updates", "stable")
|
|
monkeypatch.setattr(fastmcp, "__version__", "2.4.0")
|
|
|
|
with patch(
|
|
"fastmcp.utilities.version_check.get_latest_version", return_value="2.5.0"
|
|
):
|
|
result = check_for_newer_version()
|
|
assert result == "2.5.0"
|
|
|
|
def test_returns_none_if_older_available(self, monkeypatch: pytest.MonkeyPatch):
|
|
"""Returns None if pypi version is older than current (dev version)."""
|
|
import fastmcp
|
|
|
|
monkeypatch.setattr(fastmcp.settings, "check_for_updates", "stable")
|
|
monkeypatch.setattr(fastmcp, "__version__", "3.0.0.dev1")
|
|
|
|
with patch(
|
|
"fastmcp.utilities.version_check.get_latest_version", return_value="2.5.0"
|
|
):
|
|
result = check_for_newer_version()
|
|
assert result is None
|
|
|
|
def test_handles_invalid_versions(self, monkeypatch: pytest.MonkeyPatch):
|
|
"""Handles invalid version strings gracefully."""
|
|
import fastmcp
|
|
|
|
monkeypatch.setattr(fastmcp.settings, "check_for_updates", "stable")
|
|
monkeypatch.setattr(fastmcp, "__version__", "invalid")
|
|
|
|
with patch(
|
|
"fastmcp.utilities.version_check.get_latest_version",
|
|
return_value="also-invalid",
|
|
):
|
|
result = check_for_newer_version()
|
|
assert result is None
|
|
|
|
def test_prerelease_setting(self, monkeypatch: pytest.MonkeyPatch):
|
|
"""Prerelease setting passes include_prereleases=True."""
|
|
import fastmcp
|
|
|
|
monkeypatch.setattr(fastmcp.settings, "check_for_updates", "prerelease")
|
|
monkeypatch.setattr(fastmcp, "__version__", "2.5.0")
|
|
|
|
with patch(
|
|
"fastmcp.utilities.version_check.get_latest_version", return_value="2.6.0b1"
|
|
) as mock_get:
|
|
result = check_for_newer_version()
|
|
assert result == "2.6.0b1"
|
|
mock_get.assert_called_once_with(True)
|
|
|
|
def test_stable_setting(self, monkeypatch: pytest.MonkeyPatch):
|
|
"""Stable setting passes include_prereleases=False."""
|
|
import fastmcp
|
|
|
|
monkeypatch.setattr(fastmcp.settings, "check_for_updates", "stable")
|
|
monkeypatch.setattr(fastmcp, "__version__", "2.4.0")
|
|
|
|
with patch(
|
|
"fastmcp.utilities.version_check.get_latest_version", return_value="2.5.0"
|
|
) as mock_get:
|
|
result = check_for_newer_version()
|
|
assert result == "2.5.0"
|
|
mock_get.assert_called_once_with(False)
|