Files
DashBoard/tests/test_mid_section_defect_routes.py
egg af59031f95 feat(mid-section-defect): harden with distributed lock, rate limit, filter separation, abort, SQL classification and tests
Address 6 code review findings (P0-P3): add Redis distributed lock to prevent
duplicate Oracle pipeline on cold cache, apply rate limiting to 3 high-cost
routes, separate UI filter state from committed query state, add AbortController
for request cancellation, push workcenter group classification into Oracle SQL
CASE WHEN, and add 18 route+service tests. Also add workcenter group selection
to job-query equipment selector and rename button to "查詢".

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 09:32:14 +08:00

163 lines
5.4 KiB
Python

# -*- coding: utf-8 -*-
"""Route tests for mid-section defect APIs."""
from __future__ import annotations
from unittest.mock import patch
import mes_dashboard.core.database as db
from mes_dashboard.app import create_app
from mes_dashboard.core.rate_limit import reset_rate_limits_for_tests
def _client():
db._ENGINE = None
app = create_app('testing')
app.config['TESTING'] = True
return app.test_client()
def setup_function():
reset_rate_limits_for_tests()
def teardown_function():
reset_rate_limits_for_tests()
@patch('mes_dashboard.routes.mid_section_defect_routes.query_analysis')
def test_analysis_success(mock_query_analysis):
mock_query_analysis.return_value = {
'kpi': {'total_input': 100},
'charts': {'by_station': []},
'daily_trend': [],
'available_loss_reasons': ['A'],
'genealogy_status': 'ready',
'detail': [{}, {}],
}
client = _client()
response = client.get(
'/api/mid-section-defect/analysis?start_date=2025-01-01&end_date=2025-01-31&loss_reasons=A,B'
)
assert response.status_code == 200
payload = response.get_json()
assert payload['success'] is True
assert payload['data']['detail_total_count'] == 2
assert payload['data']['kpi']['total_input'] == 100
mock_query_analysis.assert_called_once_with('2025-01-01', '2025-01-31', ['A', 'B'])
def test_analysis_missing_dates_returns_400():
client = _client()
response = client.get('/api/mid-section-defect/analysis?start_date=2025-01-01')
assert response.status_code == 400
payload = response.get_json()
assert payload['success'] is False
@patch('mes_dashboard.routes.mid_section_defect_routes.query_analysis')
def test_analysis_service_failure_returns_500(mock_query_analysis):
mock_query_analysis.return_value = None
client = _client()
response = client.get('/api/mid-section-defect/analysis?start_date=2025-01-01&end_date=2025-01-31')
assert response.status_code == 500
payload = response.get_json()
assert payload['success'] is False
@patch('mes_dashboard.routes.mid_section_defect_routes.query_analysis')
@patch('mes_dashboard.core.rate_limit.check_and_record', return_value=(True, 7))
def test_analysis_rate_limited_returns_429(_mock_rate_limit, mock_query_analysis):
client = _client()
response = client.get('/api/mid-section-defect/analysis?start_date=2025-01-01&end_date=2025-01-31')
assert response.status_code == 429
assert response.headers.get('Retry-After') == '7'
payload = response.get_json()
assert payload['error']['code'] == 'TOO_MANY_REQUESTS'
mock_query_analysis.assert_not_called()
@patch('mes_dashboard.routes.mid_section_defect_routes.query_analysis_detail')
def test_detail_success(mock_query_detail):
mock_query_detail.return_value = {
'detail': [{'CONTAINERNAME': 'LOT-1'}],
'pagination': {'page': 2, 'page_size': 200, 'total_count': 350, 'total_pages': 2},
}
client = _client()
response = client.get(
'/api/mid-section-defect/analysis/detail?start_date=2025-01-01&end_date=2025-01-31&page=2&page_size=200'
)
assert response.status_code == 200
payload = response.get_json()
assert payload['success'] is True
assert payload['data']['pagination']['page'] == 2
mock_query_detail.assert_called_once_with(
'2025-01-01',
'2025-01-31',
None,
page=2,
page_size=200,
)
def test_detail_missing_dates_returns_400():
client = _client()
response = client.get('/api/mid-section-defect/analysis/detail?end_date=2025-01-31')
assert response.status_code == 400
payload = response.get_json()
assert payload['success'] is False
@patch('mes_dashboard.routes.mid_section_defect_routes.query_analysis_detail')
@patch('mes_dashboard.core.rate_limit.check_and_record', return_value=(True, 5))
def test_detail_rate_limited_returns_429(_mock_rate_limit, mock_query_detail):
client = _client()
response = client.get('/api/mid-section-defect/analysis/detail?start_date=2025-01-01&end_date=2025-01-31')
assert response.status_code == 429
assert response.headers.get('Retry-After') == '5'
payload = response.get_json()
assert payload['error']['code'] == 'TOO_MANY_REQUESTS'
mock_query_detail.assert_not_called()
@patch('mes_dashboard.routes.mid_section_defect_routes.export_csv')
def test_export_success(mock_export_csv):
mock_export_csv.return_value = iter([
'\ufeff',
'LOT ID,TYPE\r\n',
'A001,T1\r\n',
])
client = _client()
response = client.get(
'/api/mid-section-defect/export?start_date=2025-01-01&end_date=2025-01-31&loss_reasons=A,B'
)
assert response.status_code == 200
assert 'text/csv' in response.content_type
assert 'attachment;' in response.headers.get('Content-Disposition', '')
mock_export_csv.assert_called_once_with('2025-01-01', '2025-01-31', ['A', 'B'])
@patch('mes_dashboard.routes.mid_section_defect_routes.export_csv')
@patch('mes_dashboard.core.rate_limit.check_and_record', return_value=(True, 9))
def test_export_rate_limited_returns_429(_mock_rate_limit, mock_export_csv):
client = _client()
response = client.get('/api/mid-section-defect/export?start_date=2025-01-01&end_date=2025-01-31')
assert response.status_code == 429
assert response.headers.get('Retry-After') == '9'
payload = response.get_json()
assert payload['error']['code'] == 'TOO_MANY_REQUESTS'
mock_export_csv.assert_not_called()