Files
DashBoard/tests/test_reject_history_routes.py

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()