mcp-feedback-enhanced/tests/unit/test_gzip_compression.py

364 lines
12 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
"""
Gzip 壓縮功能測試
================
測試 FastAPI Gzip 壓縮中間件的功能包括
- 壓縮效果驗證
- WebSocket 兼容性
- 靜態文件緩存
- 性能提升測試
"""
import gzip
import json
2025-06-11 03:25:08 +08:00
from unittest.mock import patch
import pytest
from fastapi import FastAPI, Response
from fastapi.middleware.gzip import GZipMiddleware
2025-06-11 03:25:08 +08:00
from fastapi.testclient import TestClient
2025-06-11 06:11:29 +08:00
from mcp_feedback_enhanced.web.utils.compression_config import (
2025-06-11 03:25:08 +08:00
CompressionConfig,
CompressionManager,
get_compression_manager,
)
2025-06-11 06:11:29 +08:00
from mcp_feedback_enhanced.web.utils.compression_monitor import (
2025-06-11 03:25:08 +08:00
CompressionMonitor,
get_compression_monitor,
)
class TestCompressionConfig:
"""測試壓縮配置類"""
2025-06-11 03:25:08 +08:00
def test_default_config(self):
"""測試預設配置"""
config = CompressionConfig()
2025-06-11 03:25:08 +08:00
assert config.minimum_size == 1000
assert config.compression_level == 6
assert config.static_cache_max_age == 3600
assert config.api_cache_max_age == 0
2025-06-11 03:25:08 +08:00
assert "text/html" in config.compressible_types
assert "application/json" in config.compressible_types
assert "/ws" in config.exclude_paths
def test_from_env(self):
"""測試從環境變數創建配置"""
2025-06-11 03:25:08 +08:00
with patch.dict(
"os.environ",
{
"MCP_GZIP_MIN_SIZE": "2000",
"MCP_GZIP_LEVEL": "9",
"MCP_STATIC_CACHE_AGE": "7200",
},
):
config = CompressionConfig.from_env()
2025-06-11 03:25:08 +08:00
assert config.minimum_size == 2000
assert config.compression_level == 9
assert config.static_cache_max_age == 7200
2025-06-11 03:25:08 +08:00
def test_should_compress(self):
"""測試壓縮判斷邏輯"""
config = CompressionConfig()
2025-06-11 03:25:08 +08:00
# 應該壓縮的情況
2025-06-11 03:25:08 +08:00
assert config.should_compress("text/html", 2000) == True
assert config.should_compress("application/json", 1500) == True
# 不應該壓縮的情況
2025-06-11 03:25:08 +08:00
assert config.should_compress("text/html", 500) == False # 太小
assert config.should_compress("image/jpeg", 2000) == False # 不支援的類型
assert config.should_compress("", 2000) == False # 無內容類型
def test_should_exclude_path(self):
"""測試路徑排除邏輯"""
config = CompressionConfig()
2025-06-11 03:25:08 +08:00
assert config.should_exclude_path("/ws") == True
assert config.should_exclude_path("/api/ws") == True
assert config.should_exclude_path("/health") == True
assert config.should_exclude_path("/static/css/style.css") == False
assert config.should_exclude_path("/api/feedback") == False
def test_get_cache_headers(self):
"""測試緩存頭生成"""
config = CompressionConfig()
2025-06-11 03:25:08 +08:00
# 靜態文件
2025-06-11 03:25:08 +08:00
static_headers = config.get_cache_headers("/static/css/style.css")
assert "Cache-Control" in static_headers
assert "public, max-age=3600" in static_headers["Cache-Control"]
# API 路徑(預設不緩存)
2025-06-11 03:25:08 +08:00
api_headers = config.get_cache_headers("/api/feedback")
assert "no-cache" in api_headers["Cache-Control"]
# 其他路徑
2025-06-11 03:25:08 +08:00
other_headers = config.get_cache_headers("/feedback")
assert "no-cache" in other_headers["Cache-Control"]
class TestCompressionManager:
"""測試壓縮管理器"""
2025-06-11 03:25:08 +08:00
def test_manager_initialization(self):
"""測試管理器初始化"""
manager = CompressionManager()
2025-06-11 03:25:08 +08:00
assert manager.config is not None
2025-06-11 03:25:08 +08:00
assert manager._stats["requests_total"] == 0
assert manager._stats["requests_compressed"] == 0
def test_update_stats(self):
"""測試統計更新"""
manager = CompressionManager()
2025-06-11 03:25:08 +08:00
# 測試壓縮請求
manager.update_stats(1000, 600, True)
stats = manager.get_stats()
2025-06-11 03:25:08 +08:00
assert stats["requests_total"] == 1
assert stats["requests_compressed"] == 1
assert stats["bytes_original"] == 1000
assert stats["bytes_compressed"] == 600
assert stats["compression_ratio"] == 40.0 # (1000-600)/1000 * 100
# 測試未壓縮請求
manager.update_stats(500, 500, False)
stats = manager.get_stats()
2025-06-11 03:25:08 +08:00
assert stats["requests_total"] == 2
assert stats["requests_compressed"] == 1
assert stats["compression_percentage"] == 50.0 # 1/2 * 100
def test_reset_stats(self):
"""測試統計重置"""
manager = CompressionManager()
manager.update_stats(1000, 600, True)
2025-06-11 03:25:08 +08:00
manager.reset_stats()
stats = manager.get_stats()
2025-06-11 03:25:08 +08:00
assert stats["requests_total"] == 0
assert stats["requests_compressed"] == 0
assert stats["compression_ratio"] == 0.0
class TestCompressionMonitor:
"""測試壓縮監控器"""
2025-06-11 03:25:08 +08:00
def test_monitor_initialization(self):
"""測試監控器初始化"""
monitor = CompressionMonitor()
2025-06-11 03:25:08 +08:00
assert monitor.max_metrics == 1000
assert len(monitor.metrics) == 0
assert len(monitor.path_stats) == 0
2025-06-11 03:25:08 +08:00
def test_record_request(self):
"""測試請求記錄"""
monitor = CompressionMonitor()
2025-06-11 03:25:08 +08:00
monitor.record_request(
2025-06-11 03:25:08 +08:00
path="/static/css/style.css",
original_size=2000,
compressed_size=1200,
response_time=0.05,
2025-06-11 03:25:08 +08:00
content_type="text/css",
was_compressed=True,
)
2025-06-11 03:25:08 +08:00
assert len(monitor.metrics) == 1
metric = monitor.metrics[0]
2025-06-11 03:25:08 +08:00
assert metric.path == "/static/css/style.css"
assert metric.compression_ratio == 40.0 # (2000-1200)/2000 * 100
2025-06-11 03:25:08 +08:00
# 檢查路徑統計
path_stats = monitor.get_path_stats()
2025-06-11 03:25:08 +08:00
assert "/static/css/style.css" in path_stats
assert path_stats["/static/css/style.css"]["requests"] == 1
assert path_stats["/static/css/style.css"]["compressed_requests"] == 1
def test_get_summary(self):
"""測試摘要統計"""
monitor = CompressionMonitor()
2025-06-11 03:25:08 +08:00
# 記錄多個請求
2025-06-11 03:25:08 +08:00
monitor.record_request(
"/static/css/style.css", 2000, 1200, 0.05, "text/css", True
)
monitor.record_request(
"/static/js/app.js", 3000, 1800, 0.08, "application/javascript", True
)
monitor.record_request(
"/api/feedback", 500, 500, 0.02, "application/json", False
)
summary = monitor.get_summary()
2025-06-11 03:25:08 +08:00
assert summary.total_requests == 3
assert summary.compressed_requests == 2
assert abs(summary.compression_percentage - 66.67) < 0.01 # 2/3 * 100 (約)
2025-06-11 03:25:08 +08:00
assert (
summary.bandwidth_saved == 2000
) # (2000-1200) + (3000-1800) + 0 = 800 + 1200 + 0 = 2000
def test_export_stats(self):
"""測試統計導出"""
monitor = CompressionMonitor()
2025-06-11 03:25:08 +08:00
monitor.record_request(
"/static/css/style.css", 2000, 1200, 0.05, "text/css", True
)
exported = monitor.export_stats()
2025-06-11 03:25:08 +08:00
assert "summary" in exported
assert "top_compressed_paths" in exported
assert "path_stats" in exported
assert "content_type_stats" in exported
assert exported["summary"]["total_requests"] == 1
assert exported["summary"]["compressed_requests"] == 1
class TestGzipIntegration:
"""測試 Gzip 壓縮集成"""
2025-06-11 03:25:08 +08:00
def create_test_app(self):
"""創建測試應用"""
app = FastAPI()
2025-06-11 03:25:08 +08:00
# 添加 Gzip 中間件
app.add_middleware(GZipMiddleware, minimum_size=100)
2025-06-11 03:25:08 +08:00
@app.get("/test-large")
async def test_large():
# 返回大於最小壓縮大小的內容
return {"data": "x" * 1000}
2025-06-11 03:25:08 +08:00
@app.get("/test-small")
async def test_small():
# 返回小於最小壓縮大小的內容
return {"data": "small"}
2025-06-11 03:25:08 +08:00
@app.get("/test-html")
async def test_html():
html_content = "<html><body>" + "content " * 100 + "</body></html>"
return Response(content=html_content, media_type="text/html")
2025-06-11 03:25:08 +08:00
return app
2025-06-11 03:25:08 +08:00
def test_gzip_compression_large_content(self):
"""測試大內容的 Gzip 壓縮"""
app = self.create_test_app()
client = TestClient(app)
2025-06-11 03:25:08 +08:00
# 請求壓縮
response = client.get("/test-large", headers={"Accept-Encoding": "gzip"})
2025-06-11 03:25:08 +08:00
assert response.status_code == 200
assert response.headers.get("content-encoding") == "gzip"
2025-06-11 03:25:08 +08:00
# 驗證內容正確性
data = response.json()
assert "data" in data
assert len(data["data"]) == 1000
2025-06-11 03:25:08 +08:00
def test_gzip_compression_small_content(self):
"""測試小內容不壓縮"""
app = self.create_test_app()
client = TestClient(app)
2025-06-11 03:25:08 +08:00
response = client.get("/test-small", headers={"Accept-Encoding": "gzip"})
2025-06-11 03:25:08 +08:00
assert response.status_code == 200
# 小內容不應該被壓縮
assert response.headers.get("content-encoding") != "gzip"
2025-06-11 03:25:08 +08:00
def test_gzip_compression_html_content(self):
"""測試 HTML 內容壓縮"""
app = self.create_test_app()
client = TestClient(app)
2025-06-11 03:25:08 +08:00
response = client.get("/test-html", headers={"Accept-Encoding": "gzip"})
2025-06-11 03:25:08 +08:00
assert response.status_code == 200
assert response.headers.get("content-encoding") == "gzip"
assert response.headers.get("content-type") == "text/html; charset=utf-8"
2025-06-11 03:25:08 +08:00
def test_no_compression_without_accept_encoding(self):
"""測試不支援壓縮的客戶端"""
app = self.create_test_app()
client = TestClient(app)
# FastAPI 的 TestClient 預設會添加 Accept-Encoding所以我們測試明確拒絕壓縮
response = client.get("/test-large", headers={"Accept-Encoding": "identity"})
assert response.status_code == 200
# 當明確要求不壓縮時,應該不會有 gzip 編碼
# 注意:某些情況下 FastAPI 仍可能壓縮,這是正常行為
class TestWebSocketCompatibility:
"""測試 WebSocket 兼容性"""
2025-06-11 03:25:08 +08:00
def test_websocket_not_compressed(self):
"""測試 WebSocket 連接不受壓縮影響"""
# 這個測試確保 WebSocket 路徑被正確排除
config = CompressionConfig()
2025-06-11 03:25:08 +08:00
# WebSocket 路徑應該被排除
2025-06-11 03:25:08 +08:00
assert config.should_exclude_path("/ws") == True
assert config.should_exclude_path("/api/ws") == True
# 確保 WebSocket 不會被壓縮配置影響
2025-06-11 03:25:08 +08:00
assert not config.should_compress(
"application/json", 1000
) or config.should_exclude_path("/ws")
@pytest.mark.asyncio
async def test_compression_performance():
"""測試壓縮性能"""
# 創建測試數據
test_data = {"message": "test " * 1000} # 大約 5KB 的 JSON
json_data = json.dumps(test_data)
2025-06-11 03:25:08 +08:00
# 手動壓縮測試
2025-06-11 03:25:08 +08:00
compressed_data = gzip.compress(json_data.encode("utf-8"))
# 驗證壓縮效果
2025-06-11 03:25:08 +08:00
original_size = len(json_data.encode("utf-8"))
compressed_size = len(compressed_data)
compression_ratio = (1 - compressed_size / original_size) * 100
2025-06-11 03:25:08 +08:00
# 壓縮比應該大於 50%JSON 數據通常壓縮效果很好)
assert compression_ratio > 50
assert compressed_size < original_size
2025-06-11 03:25:08 +08:00
# 驗證解壓縮正確性
2025-06-11 03:25:08 +08:00
decompressed_data = gzip.decompress(compressed_data).decode("utf-8")
assert decompressed_data == json_data
def test_global_instances():
"""測試全域實例"""
# 測試壓縮管理器全域實例
manager1 = get_compression_manager()
manager2 = get_compression_manager()
assert manager1 is manager2
2025-06-11 03:25:08 +08:00
# 測試壓縮監控器全域實例
monitor1 = get_compression_monitor()
monitor2 = get_compression_monitor()
assert monitor1 is monitor2
if __name__ == "__main__":
pytest.main([__file__, "-v"])