mirror of
https://github.com/tiennm99/litellm.git
synced 2026-07-13 19:06:53 +00:00
166 lines
4.7 KiB
Python
166 lines
4.7 KiB
Python
"""Tests for GetBlogPosts utility class."""
|
|
import json
|
|
import time
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
import litellm
|
|
from litellm.litellm_core_utils.get_blog_posts import (
|
|
BlogPost,
|
|
BlogPostsResponse,
|
|
GetBlogPosts,
|
|
get_blog_posts,
|
|
)
|
|
|
|
SAMPLE_RESPONSE = {
|
|
"posts": [
|
|
{
|
|
"title": "Test Post",
|
|
"description": "A test post.",
|
|
"date": "2026-01-01",
|
|
"url": "https://www.litellm.ai/blog/test",
|
|
}
|
|
]
|
|
}
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def reset_blog_posts_cache():
|
|
GetBlogPosts._cached_posts = None
|
|
GetBlogPosts._last_fetch_time = 0.0
|
|
yield
|
|
GetBlogPosts._cached_posts = None
|
|
GetBlogPosts._last_fetch_time = 0.0
|
|
|
|
|
|
def test_load_local_blog_posts_returns_list():
|
|
posts = GetBlogPosts.load_local_blog_posts()
|
|
assert isinstance(posts, list)
|
|
assert len(posts) > 0
|
|
first = posts[0]
|
|
assert "title" in first
|
|
assert "description" in first
|
|
assert "date" in first
|
|
assert "url" in first
|
|
|
|
|
|
def test_validate_blog_posts_valid():
|
|
assert GetBlogPosts.validate_blog_posts(SAMPLE_RESPONSE) is True
|
|
|
|
|
|
def test_validate_blog_posts_missing_posts_key():
|
|
assert GetBlogPosts.validate_blog_posts({"other": []}) is False
|
|
|
|
|
|
def test_validate_blog_posts_empty_list():
|
|
assert GetBlogPosts.validate_blog_posts({"posts": []}) is False
|
|
|
|
|
|
def test_validate_blog_posts_not_dict():
|
|
assert GetBlogPosts.validate_blog_posts("not a dict") is False
|
|
|
|
|
|
def test_get_blog_posts_success():
|
|
"""Fetches from remote on first call."""
|
|
mock_response = MagicMock()
|
|
mock_response.json.return_value = SAMPLE_RESPONSE
|
|
mock_response.raise_for_status = MagicMock()
|
|
|
|
with patch("litellm.litellm_core_utils.get_blog_posts.httpx.get", return_value=mock_response):
|
|
posts = get_blog_posts(url=litellm.blog_posts_url)
|
|
|
|
assert len(posts) == 1
|
|
assert posts[0]["title"] == "Test Post"
|
|
|
|
|
|
def test_get_blog_posts_network_error_falls_back_to_local():
|
|
"""Falls back to local backup on network error."""
|
|
with patch(
|
|
"litellm.litellm_core_utils.get_blog_posts.httpx.get",
|
|
side_effect=Exception("Network error"),
|
|
):
|
|
posts = get_blog_posts(url=litellm.blog_posts_url)
|
|
|
|
assert isinstance(posts, list)
|
|
assert len(posts) > 0
|
|
|
|
|
|
def test_get_blog_posts_invalid_json_falls_back_to_local():
|
|
"""Falls back when remote returns non-dict."""
|
|
mock_response = MagicMock()
|
|
mock_response.json.return_value = "not a dict"
|
|
mock_response.raise_for_status = MagicMock()
|
|
|
|
with patch("litellm.litellm_core_utils.get_blog_posts.httpx.get", return_value=mock_response):
|
|
posts = get_blog_posts(url=litellm.blog_posts_url)
|
|
|
|
assert isinstance(posts, list)
|
|
assert len(posts) > 0
|
|
|
|
|
|
def test_get_blog_posts_ttl_cache_not_refetched():
|
|
"""Within TTL window, does not re-fetch."""
|
|
GetBlogPosts._cached_posts = SAMPLE_RESPONSE["posts"]
|
|
GetBlogPosts._last_fetch_time = time.time() # just now
|
|
|
|
call_count = 0
|
|
|
|
def mock_get(*args, **kwargs):
|
|
nonlocal call_count
|
|
call_count += 1
|
|
m = MagicMock()
|
|
m.json.return_value = SAMPLE_RESPONSE
|
|
m.raise_for_status = MagicMock()
|
|
return m
|
|
|
|
with patch("litellm.litellm_core_utils.get_blog_posts.httpx.get", side_effect=mock_get):
|
|
posts = get_blog_posts(url=litellm.blog_posts_url)
|
|
|
|
assert call_count == 0 # cache hit, no fetch
|
|
assert len(posts) == 1
|
|
|
|
|
|
def test_get_blog_posts_ttl_expired_refetches():
|
|
"""After TTL window, re-fetches from remote."""
|
|
GetBlogPosts._cached_posts = SAMPLE_RESPONSE["posts"]
|
|
GetBlogPosts._last_fetch_time = time.time() - 7200 # 2 hours ago
|
|
|
|
mock_response = MagicMock()
|
|
mock_response.json.return_value = SAMPLE_RESPONSE
|
|
mock_response.raise_for_status = MagicMock()
|
|
|
|
with patch(
|
|
"litellm.litellm_core_utils.get_blog_posts.httpx.get", return_value=mock_response
|
|
) as mock_get:
|
|
posts = get_blog_posts(url=litellm.blog_posts_url)
|
|
|
|
mock_get.assert_called_once()
|
|
assert len(posts) == 1
|
|
|
|
|
|
def test_get_blog_posts_local_env_var_skips_remote(monkeypatch):
|
|
monkeypatch.setenv("LITELLM_LOCAL_BLOG_POSTS", "true")
|
|
with patch("litellm.litellm_core_utils.get_blog_posts.httpx.get") as mock_get:
|
|
posts = get_blog_posts(url=litellm.blog_posts_url)
|
|
mock_get.assert_not_called()
|
|
assert isinstance(posts, list)
|
|
assert len(posts) > 0
|
|
|
|
|
|
def test_blog_post_pydantic_model():
|
|
post = BlogPost(
|
|
title="T",
|
|
description="D",
|
|
date="2026-01-01",
|
|
url="https://example.com",
|
|
)
|
|
assert post.title == "T"
|
|
|
|
|
|
def test_blog_posts_response_pydantic_model():
|
|
resp = BlogPostsResponse(
|
|
posts=[BlogPost(title="T", description="D", date="2026-01-01", url="https://x.com")]
|
|
)
|
|
assert len(resp.posts) == 1
|