mcp-feedback-enhanced/tests/test_gzip_compression.py

347 lines
12 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Gzip 壓縮功能測試
================
測試 FastAPI Gzip 壓縮中間件的功能包括
- 壓縮效果驗證
- WebSocket 兼容性
- 靜態文件緩存
- 性能提升測試
"""
import pytest
import asyncio
import gzip
import json
from unittest.mock import Mock, patch
from fastapi.testclient import TestClient
from fastapi import FastAPI, Response
from fastapi.middleware.gzip import GZipMiddleware
from src.mcp_feedback_enhanced.web.utils.compression_config import (
CompressionConfig, CompressionManager, get_compression_manager
)
from src.mcp_feedback_enhanced.web.utils.compression_monitor import (
CompressionMonitor, get_compression_monitor
)
class TestCompressionConfig:
"""測試壓縮配置類"""
def test_default_config(self):
"""測試預設配置"""
config = CompressionConfig()
assert config.minimum_size == 1000
assert config.compression_level == 6
assert config.static_cache_max_age == 3600
assert config.api_cache_max_age == 0
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):
"""測試從環境變數創建配置"""
with patch.dict('os.environ', {
'MCP_GZIP_MIN_SIZE': '2000',
'MCP_GZIP_LEVEL': '9',
'MCP_STATIC_CACHE_AGE': '7200'
}):
config = CompressionConfig.from_env()
assert config.minimum_size == 2000
assert config.compression_level == 9
assert config.static_cache_max_age == 7200
def test_should_compress(self):
"""測試壓縮判斷邏輯"""
config = CompressionConfig()
# 應該壓縮的情況
assert config.should_compress('text/html', 2000) == True
assert config.should_compress('application/json', 1500) == True
# 不應該壓縮的情況
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()
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()
# 靜態文件
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 路徑(預設不緩存)
api_headers = config.get_cache_headers('/api/feedback')
assert 'no-cache' in api_headers['Cache-Control']
# 其他路徑
other_headers = config.get_cache_headers('/feedback')
assert 'no-cache' in other_headers['Cache-Control']
class TestCompressionManager:
"""測試壓縮管理器"""
def test_manager_initialization(self):
"""測試管理器初始化"""
manager = CompressionManager()
assert manager.config is not None
assert manager._stats['requests_total'] == 0
assert manager._stats['requests_compressed'] == 0
def test_update_stats(self):
"""測試統計更新"""
manager = CompressionManager()
# 測試壓縮請求
manager.update_stats(1000, 600, True)
stats = manager.get_stats()
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()
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)
manager.reset_stats()
stats = manager.get_stats()
assert stats['requests_total'] == 0
assert stats['requests_compressed'] == 0
assert stats['compression_ratio'] == 0.0
class TestCompressionMonitor:
"""測試壓縮監控器"""
def test_monitor_initialization(self):
"""測試監控器初始化"""
monitor = CompressionMonitor()
assert monitor.max_metrics == 1000
assert len(monitor.metrics) == 0
assert len(monitor.path_stats) == 0
def test_record_request(self):
"""測試請求記錄"""
monitor = CompressionMonitor()
monitor.record_request(
path='/static/css/style.css',
original_size=2000,
compressed_size=1200,
response_time=0.05,
content_type='text/css',
was_compressed=True
)
assert len(monitor.metrics) == 1
metric = monitor.metrics[0]
assert metric.path == '/static/css/style.css'
assert metric.compression_ratio == 40.0 # (2000-1200)/2000 * 100
# 檢查路徑統計
path_stats = monitor.get_path_stats()
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()
# 記錄多個請求
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()
assert summary.total_requests == 3
assert summary.compressed_requests == 2
assert abs(summary.compression_percentage - 66.67) < 0.01 # 2/3 * 100 (約)
assert summary.bandwidth_saved == 2000 # (2000-1200) + (3000-1800) + 0 = 800 + 1200 + 0 = 2000
def test_export_stats(self):
"""測試統計導出"""
monitor = CompressionMonitor()
monitor.record_request('/static/css/style.css', 2000, 1200, 0.05, 'text/css', True)
exported = monitor.export_stats()
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 壓縮集成"""
def create_test_app(self):
"""創建測試應用"""
app = FastAPI()
# 添加 Gzip 中間件
app.add_middleware(GZipMiddleware, minimum_size=100)
@app.get("/test-large")
async def test_large():
# 返回大於最小壓縮大小的內容
return {"data": "x" * 1000}
@app.get("/test-small")
async def test_small():
# 返回小於最小壓縮大小的內容
return {"data": "small"}
@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")
return app
def test_gzip_compression_large_content(self):
"""測試大內容的 Gzip 壓縮"""
app = self.create_test_app()
client = TestClient(app)
# 請求壓縮
response = client.get("/test-large", headers={"Accept-Encoding": "gzip"})
assert response.status_code == 200
assert response.headers.get("content-encoding") == "gzip"
# 驗證內容正確性
data = response.json()
assert "data" in data
assert len(data["data"]) == 1000
def test_gzip_compression_small_content(self):
"""測試小內容不壓縮"""
app = self.create_test_app()
client = TestClient(app)
response = client.get("/test-small", headers={"Accept-Encoding": "gzip"})
assert response.status_code == 200
# 小內容不應該被壓縮
assert response.headers.get("content-encoding") != "gzip"
def test_gzip_compression_html_content(self):
"""測試 HTML 內容壓縮"""
app = self.create_test_app()
client = TestClient(app)
response = client.get("/test-html", headers={"Accept-Encoding": "gzip"})
assert response.status_code == 200
assert response.headers.get("content-encoding") == "gzip"
assert response.headers.get("content-type") == "text/html; charset=utf-8"
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 兼容性"""
def test_websocket_not_compressed(self):
"""測試 WebSocket 連接不受壓縮影響"""
# 這個測試確保 WebSocket 路徑被正確排除
config = CompressionConfig()
# WebSocket 路徑應該被排除
assert config.should_exclude_path('/ws') == True
assert config.should_exclude_path('/api/ws') == True
# 確保 WebSocket 不會被壓縮配置影響
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)
# 手動壓縮測試
compressed_data = gzip.compress(json_data.encode('utf-8'))
# 驗證壓縮效果
original_size = len(json_data.encode('utf-8'))
compressed_size = len(compressed_data)
compression_ratio = (1 - compressed_size / original_size) * 100
# 壓縮比應該大於 50%JSON 數據通常壓縮效果很好)
assert compression_ratio > 50
assert compressed_size < original_size
# 驗證解壓縮正確性
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
# 測試壓縮監控器全域實例
monitor1 = get_compression_monitor()
monitor2 = get_compression_monitor()
assert monitor1 is monitor2
if __name__ == "__main__":
pytest.main([__file__, "-v"])