Files
DashBoard/tests/test_hold_routes.py
2026-02-08 08:30:48 +08:00

318 lines
13 KiB
Python

# -*- coding: utf-8 -*-
"""Unit tests for Hold Detail API routes.
Tests the Hold Detail API endpoints in hold_routes.py.
"""
import unittest
from unittest.mock import patch
import json
from mes_dashboard.app import create_app
import mes_dashboard.core.database as db
class TestHoldRoutesBase(unittest.TestCase):
"""Base class for Hold routes tests."""
def setUp(self):
"""Set up test client."""
db._ENGINE = None
self.app = create_app('testing')
self.app.config['TESTING'] = True
self.client = self.app.test_client()
class TestHoldDetailPageRoute(TestHoldRoutesBase):
"""Test GET /hold-detail page route."""
def test_hold_detail_page_requires_reason(self):
"""GET /hold-detail without reason should redirect to wip-overview."""
response = self.client.get('/hold-detail')
self.assertEqual(response.status_code, 302)
self.assertIn('/wip-overview', response.location)
def test_hold_detail_page_with_reason(self):
"""GET /hold-detail?reason=xxx should return 200."""
response = self.client.get('/hold-detail?reason=YieldLimit')
self.assertEqual(response.status_code, 200)
def test_hold_detail_page_contains_reason_in_html(self):
"""Page should display the hold reason in the HTML."""
response = self.client.get('/hold-detail?reason=YieldLimit')
self.assertIn(b'YieldLimit', response.data)
class TestHoldDetailSummaryRoute(TestHoldRoutesBase):
"""Test GET /api/wip/hold-detail/summary endpoint."""
@patch('mes_dashboard.routes.hold_routes.get_hold_detail_summary')
def test_returns_success_with_data(self, mock_get_summary):
"""Should return success=True with summary data."""
mock_get_summary.return_value = {
'totalLots': 128,
'totalQty': 25600,
'avgAge': 2.3,
'maxAge': 15.0,
'workcenterCount': 8
}
response = self.client.get('/api/wip/hold-detail/summary?reason=YieldLimit')
data = json.loads(response.data)
self.assertEqual(response.status_code, 200)
self.assertTrue(data['success'])
self.assertEqual(data['data']['totalLots'], 128)
self.assertEqual(data['data']['totalQty'], 25600)
self.assertEqual(data['data']['avgAge'], 2.3)
self.assertEqual(data['data']['maxAge'], 15.0)
self.assertEqual(data['data']['workcenterCount'], 8)
def test_returns_error_without_reason(self):
"""Should return 400 when reason is missing."""
response = self.client.get('/api/wip/hold-detail/summary')
data = json.loads(response.data)
self.assertEqual(response.status_code, 400)
self.assertFalse(data['success'])
self.assertIn('reason', data['error'])
@patch('mes_dashboard.routes.hold_routes.get_hold_detail_summary')
def test_returns_error_on_failure(self, mock_get_summary):
"""Should return success=False and 500 on failure."""
mock_get_summary.return_value = None
response = self.client.get('/api/wip/hold-detail/summary?reason=YieldLimit')
data = json.loads(response.data)
self.assertEqual(response.status_code, 500)
self.assertFalse(data['success'])
self.assertIn('error', data)
class TestHoldDetailDistributionRoute(TestHoldRoutesBase):
"""Test GET /api/wip/hold-detail/distribution endpoint."""
@patch('mes_dashboard.routes.hold_routes.get_hold_detail_distribution')
def test_returns_success_with_distribution(self, mock_get_dist):
"""Should return success=True with distribution data."""
mock_get_dist.return_value = {
'byWorkcenter': [
{'name': 'DA', 'lots': 45, 'qty': 9000, 'percentage': 35.2},
{'name': 'WB', 'lots': 38, 'qty': 7600, 'percentage': 29.7}
],
'byPackage': [
{'name': 'DIP-B', 'lots': 50, 'qty': 10000, 'percentage': 39.1},
{'name': 'QFN', 'lots': 35, 'qty': 7000, 'percentage': 27.3}
],
'byAge': [
{'range': '0-1', 'label': '0-1天', 'lots': 45, 'qty': 9000, 'percentage': 35.2},
{'range': '1-3', 'label': '1-3天', 'lots': 38, 'qty': 7600, 'percentage': 29.7},
{'range': '3-7', 'label': '3-7天', 'lots': 30, 'qty': 6000, 'percentage': 23.4},
{'range': '7+', 'label': '7+天', 'lots': 15, 'qty': 3000, 'percentage': 11.7}
]
}
response = self.client.get('/api/wip/hold-detail/distribution?reason=YieldLimit')
data = json.loads(response.data)
self.assertEqual(response.status_code, 200)
self.assertTrue(data['success'])
self.assertIn('byWorkcenter', data['data'])
self.assertIn('byPackage', data['data'])
self.assertIn('byAge', data['data'])
self.assertEqual(len(data['data']['byWorkcenter']), 2)
self.assertEqual(len(data['data']['byAge']), 4)
def test_returns_error_without_reason(self):
"""Should return 400 when reason is missing."""
response = self.client.get('/api/wip/hold-detail/distribution')
data = json.loads(response.data)
self.assertEqual(response.status_code, 400)
self.assertFalse(data['success'])
@patch('mes_dashboard.routes.hold_routes.get_hold_detail_distribution')
def test_returns_error_on_failure(self, mock_get_dist):
"""Should return success=False and 500 on failure."""
mock_get_dist.return_value = None
response = self.client.get('/api/wip/hold-detail/distribution?reason=YieldLimit')
data = json.loads(response.data)
self.assertEqual(response.status_code, 500)
self.assertFalse(data['success'])
class TestHoldDetailLotsRoute(TestHoldRoutesBase):
"""Test GET /api/wip/hold-detail/lots endpoint."""
@patch('mes_dashboard.routes.hold_routes.get_hold_detail_lots')
def test_returns_success_with_lots(self, mock_get_lots):
"""Should return success=True with lots data."""
mock_get_lots.return_value = {
'lots': [
{
'lotId': 'L001',
'workorder': 'WO123',
'qty': 200,
'package': 'DIP-B',
'workcenter': 'DA',
'spec': 'S01',
'age': 2.3,
'holdBy': 'EMP01',
'dept': 'QC',
'holdComment': 'Yield below threshold'
}
],
'pagination': {
'page': 1,
'perPage': 50,
'total': 128,
'totalPages': 3
},
'filters': {
'workcenter': None,
'package': None,
'ageRange': None
}
}
response = self.client.get('/api/wip/hold-detail/lots?reason=YieldLimit')
data = json.loads(response.data)
self.assertEqual(response.status_code, 200)
self.assertTrue(data['success'])
self.assertIn('lots', data['data'])
self.assertIn('pagination', data['data'])
self.assertIn('filters', data['data'])
self.assertEqual(len(data['data']['lots']), 1)
self.assertEqual(data['data']['pagination']['total'], 128)
def test_returns_error_without_reason(self):
"""Should return 400 when reason is missing."""
response = self.client.get('/api/wip/hold-detail/lots')
data = json.loads(response.data)
self.assertEqual(response.status_code, 400)
self.assertFalse(data['success'])
@patch('mes_dashboard.routes.hold_routes.get_hold_detail_lots')
def test_passes_filter_parameters(self, mock_get_lots):
"""Should pass filter parameters to service function."""
mock_get_lots.return_value = {
'lots': [],
'pagination': {'page': 2, 'perPage': 50, 'total': 0, 'totalPages': 1},
'filters': {'workcenter': 'DA', 'package': 'DIP-B', 'ageRange': '1-3'}
}
response = self.client.get(
'/api/wip/hold-detail/lots?reason=YieldLimit&workcenter=DA&package=DIP-B&age_range=1-3&page=2'
)
mock_get_lots.assert_called_once_with(
reason='YieldLimit',
workcenter='DA',
package='DIP-B',
age_range='1-3',
include_dummy=False,
page=2,
page_size=50
)
def test_validates_age_range_parameter(self):
"""Should return 400 for invalid age_range."""
response = self.client.get('/api/wip/hold-detail/lots?reason=YieldLimit&age_range=invalid')
data = json.loads(response.data)
self.assertEqual(response.status_code, 400)
self.assertFalse(data['success'])
self.assertIn('age_range', data['error'])
@patch('mes_dashboard.routes.hold_routes.get_hold_detail_lots')
def test_limits_per_page_to_200(self, mock_get_lots):
"""Per page should be capped at 200."""
mock_get_lots.return_value = {
'lots': [],
'pagination': {'page': 1, 'perPage': 200, 'total': 0, 'totalPages': 1},
'filters': {'workcenter': None, 'package': None, 'ageRange': None}
}
response = self.client.get('/api/wip/hold-detail/lots?reason=YieldLimit&per_page=500')
call_args = mock_get_lots.call_args
self.assertEqual(call_args.kwargs['page_size'], 200)
@patch('mes_dashboard.routes.hold_routes.get_hold_detail_lots')
def test_handles_page_less_than_one(self, mock_get_lots):
"""Page number less than 1 should be set to 1."""
mock_get_lots.return_value = {
'lots': [],
'pagination': {'page': 1, 'perPage': 50, 'total': 0, 'totalPages': 1},
'filters': {'workcenter': None, 'package': None, 'ageRange': None}
}
response = self.client.get('/api/wip/hold-detail/lots?reason=YieldLimit&page=0')
call_args = mock_get_lots.call_args
self.assertEqual(call_args.kwargs['page'], 1)
@patch('mes_dashboard.routes.hold_routes.get_hold_detail_lots')
def test_returns_error_on_failure(self, mock_get_lots):
"""Should return success=False and 500 on failure."""
mock_get_lots.return_value = None
response = self.client.get('/api/wip/hold-detail/lots?reason=YieldLimit')
data = json.loads(response.data)
self.assertEqual(response.status_code, 500)
self.assertFalse(data['success'])
class TestHoldDetailAgeRangeFilters(TestHoldRoutesBase):
"""Test age range filter validation."""
@patch('mes_dashboard.routes.hold_routes.get_hold_detail_lots')
def test_valid_age_range_0_1(self, mock_get_lots):
"""Should accept 0-1 as valid age_range."""
mock_get_lots.return_value = {
'lots': [], 'pagination': {'page': 1, 'perPage': 50, 'total': 0, 'totalPages': 1},
'filters': {'workcenter': None, 'package': None, 'ageRange': '0-1'}
}
response = self.client.get('/api/wip/hold-detail/lots?reason=Test&age_range=0-1')
self.assertEqual(response.status_code, 200)
@patch('mes_dashboard.routes.hold_routes.get_hold_detail_lots')
def test_valid_age_range_1_3(self, mock_get_lots):
"""Should accept 1-3 as valid age_range."""
mock_get_lots.return_value = {
'lots': [], 'pagination': {'page': 1, 'perPage': 50, 'total': 0, 'totalPages': 1},
'filters': {'workcenter': None, 'package': None, 'ageRange': '1-3'}
}
response = self.client.get('/api/wip/hold-detail/lots?reason=Test&age_range=1-3')
self.assertEqual(response.status_code, 200)
@patch('mes_dashboard.routes.hold_routes.get_hold_detail_lots')
def test_valid_age_range_3_7(self, mock_get_lots):
"""Should accept 3-7 as valid age_range."""
mock_get_lots.return_value = {
'lots': [], 'pagination': {'page': 1, 'perPage': 50, 'total': 0, 'totalPages': 1},
'filters': {'workcenter': None, 'package': None, 'ageRange': '3-7'}
}
response = self.client.get('/api/wip/hold-detail/lots?reason=Test&age_range=3-7')
self.assertEqual(response.status_code, 200)
@patch('mes_dashboard.routes.hold_routes.get_hold_detail_lots')
def test_valid_age_range_7_plus(self, mock_get_lots):
"""Should accept 7+ as valid age_range."""
mock_get_lots.return_value = {
'lots': [], 'pagination': {'page': 1, 'perPage': 50, 'total': 0, 'totalPages': 1},
'filters': {'workcenter': None, 'package': None, 'ageRange': '7+'}
}
response = self.client.get('/api/wip/hold-detail/lots?reason=Test&age_range=7%2B')
self.assertEqual(response.status_code, 200)
if __name__ == "__main__":
unittest.main()