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>
163 lines
5.4 KiB
Python
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()
|