151 lines
6.0 KiB
Python
151 lines
6.0 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""Unit tests for reject-history routes."""
|
|
|
|
import json
|
|
import os
|
|
import unittest
|
|
from unittest.mock import patch
|
|
|
|
from mes_dashboard.app import create_app
|
|
import mes_dashboard.core.database as db
|
|
|
|
|
|
def _login_as_admin(client):
|
|
with client.session_transaction() as sess:
|
|
sess['admin'] = {'displayName': 'Admin', 'employeeNo': 'A001'}
|
|
|
|
|
|
class TestRejectHistoryRoutesBase(unittest.TestCase):
|
|
def setUp(self):
|
|
db._ENGINE = None
|
|
self.app = create_app('testing')
|
|
self.app.config['TESTING'] = True
|
|
self.client = self.app.test_client()
|
|
|
|
|
|
class TestRejectHistoryPageRoute(unittest.TestCase):
|
|
@patch.dict(os.environ, {'PORTAL_SPA_ENABLED': 'false'})
|
|
@patch('mes_dashboard.app.os.path.exists', return_value=False)
|
|
def test_reject_history_page_fallback_contains_vite_entry(self, _mock_exists):
|
|
db._ENGINE = None
|
|
app = create_app('testing')
|
|
app.config['TESTING'] = True
|
|
client = app.test_client()
|
|
_login_as_admin(client)
|
|
|
|
response = client.get('/reject-history', follow_redirects=False)
|
|
self.assertEqual(response.status_code, 200)
|
|
html = response.data.decode('utf-8')
|
|
self.assertIn('/static/dist/reject-history.js', html)
|
|
|
|
|
|
class TestRejectHistoryApiRoutes(TestRejectHistoryRoutesBase):
|
|
def test_summary_missing_dates_returns_400(self):
|
|
response = self.client.get('/api/reject-history/summary')
|
|
payload = json.loads(response.data)
|
|
|
|
self.assertEqual(response.status_code, 400)
|
|
self.assertFalse(payload['success'])
|
|
|
|
def test_summary_invalid_include_excluded_scrap_returns_400(self):
|
|
response = self.client.get(
|
|
'/api/reject-history/summary?start_date=2026-02-01&end_date=2026-02-07'
|
|
'&include_excluded_scrap=invalid'
|
|
)
|
|
payload = json.loads(response.data)
|
|
|
|
self.assertEqual(response.status_code, 400)
|
|
self.assertFalse(payload['success'])
|
|
|
|
def test_summary_invalid_exclude_material_scrap_returns_400(self):
|
|
response = self.client.get(
|
|
'/api/reject-history/summary?start_date=2026-02-01&end_date=2026-02-07'
|
|
'&exclude_material_scrap=invalid'
|
|
)
|
|
payload = json.loads(response.data)
|
|
|
|
self.assertEqual(response.status_code, 400)
|
|
self.assertFalse(payload['success'])
|
|
|
|
@patch('mes_dashboard.routes.reject_history_routes.query_summary')
|
|
def test_summary_passes_filters_and_meta(self, mock_summary):
|
|
mock_summary.return_value = {
|
|
'MOVEIN_QTY': 100,
|
|
'REJECT_TOTAL_QTY': 10,
|
|
'DEFECT_QTY': 5,
|
|
'REJECT_RATE_PCT': 10,
|
|
'DEFECT_RATE_PCT': 5,
|
|
'REJECT_SHARE_PCT': 66.7,
|
|
'AFFECTED_LOT_COUNT': 8,
|
|
'AFFECTED_WORKORDER_COUNT': 4,
|
|
'meta': {
|
|
'include_excluded_scrap': False,
|
|
'exclusion_applied': True,
|
|
'excluded_reason_count': 2,
|
|
},
|
|
}
|
|
|
|
response = self.client.get(
|
|
'/api/reject-history/summary?start_date=2026-02-01&end_date=2026-02-07'
|
|
'&workcenter_groups=WB&packages=PKG-A&reasons=R1&reasons=R2'
|
|
)
|
|
payload = json.loads(response.data)
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertTrue(payload['success'])
|
|
self.assertEqual(payload['meta']['include_excluded_scrap'], False)
|
|
_, kwargs = mock_summary.call_args
|
|
self.assertEqual(kwargs['workcenter_groups'], ['WB'])
|
|
self.assertEqual(kwargs['packages'], ['PKG-A'])
|
|
self.assertEqual(kwargs['reasons'], ['R1', 'R2'])
|
|
self.assertIs(kwargs['include_excluded_scrap'], False)
|
|
self.assertIs(kwargs['exclude_material_scrap'], True)
|
|
|
|
@patch('mes_dashboard.routes.reject_history_routes.query_trend')
|
|
def test_trend_invalid_granularity_returns_400(self, mock_trend):
|
|
mock_trend.side_effect = ValueError('Invalid granularity. Use day, week, or month')
|
|
|
|
response = self.client.get(
|
|
'/api/reject-history/trend?start_date=2026-02-01&end_date=2026-02-07&granularity=hour'
|
|
)
|
|
payload = json.loads(response.data)
|
|
|
|
self.assertEqual(response.status_code, 400)
|
|
self.assertFalse(payload['success'])
|
|
|
|
@patch('mes_dashboard.routes.reject_history_routes.query_reason_pareto')
|
|
def test_reason_pareto_defaults_top80(self, mock_pareto):
|
|
mock_pareto.return_value = {'items': [], 'metric_mode': 'reject_total', 'pareto_scope': 'top80', 'meta': {}}
|
|
|
|
response = self.client.get('/api/reject-history/reason-pareto?start_date=2026-02-01&end_date=2026-02-07')
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
_, kwargs = mock_pareto.call_args
|
|
self.assertEqual(kwargs['pareto_scope'], 'top80')
|
|
self.assertEqual(kwargs['metric_mode'], 'reject_total')
|
|
|
|
@patch('mes_dashboard.routes.reject_history_routes.query_list')
|
|
@patch('mes_dashboard.core.rate_limit.check_and_record', return_value=(True, 6))
|
|
def test_list_rate_limited_returns_429(self, _mock_limit, mock_list):
|
|
response = self.client.get('/api/reject-history/list?start_date=2026-02-01&end_date=2026-02-07')
|
|
payload = json.loads(response.data)
|
|
|
|
self.assertEqual(response.status_code, 429)
|
|
self.assertEqual(payload['error']['code'], 'TOO_MANY_REQUESTS')
|
|
self.assertEqual(response.headers.get('Retry-After'), '6')
|
|
mock_list.assert_not_called()
|
|
|
|
@patch('mes_dashboard.routes.reject_history_routes.export_csv')
|
|
def test_export_returns_csv_response(self, mock_export):
|
|
mock_export.return_value = iter(['A,B\n', '1,2\n'])
|
|
|
|
response = self.client.get('/api/reject-history/export?start_date=2026-02-01&end_date=2026-02-07')
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertIn('attachment; filename=reject_history_2026-02-01_to_2026-02-07.csv', response.headers.get('Content-Disposition', ''))
|
|
self.assertIn('text/csv', response.headers.get('Content-Type', ''))
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|