Files
DashBoard/tests/test_wip_service.py
beabigegg a787436115 feat: 新增 Resource Cache 模組與表名更新
- 新增 resource_cache.py 模組,Redis 快取 DW_MES_RESOURCE 表
- 實作每 4 小時背景同步(MAX(LASTCHANGEDATE) 版本控制)
- 整合 filter_cache 優先從 WIP Redis 快取載入站點群組
- 整合 health 端點顯示 resource_cache 狀態
- 修改 resource_service 與 resource_history_service 使用快取
- 更新表名 DWH.DW_PJ_LOT_V → DW_MES_LOT_V
- 新增單元測試 (28 tests) 與 E2E 測試 (15 tests)
- 修復 wip_service 測試的 cache mock 問題
- 新增 Oracle 授權物件文檔與查詢工具

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 15:08:16 +08:00

778 lines
28 KiB
Python

# -*- coding: utf-8 -*-
"""Unit tests for WIP service layer.
Tests the WIP query functions that use DW_MES_LOT_V view.
"""
import unittest
from unittest.mock import patch, MagicMock
from functools import wraps
import pandas as pd
from mes_dashboard.services.wip_service import (
WIP_VIEW,
_escape_sql,
_build_base_conditions,
get_wip_summary,
get_wip_matrix,
get_wip_hold_summary,
get_wip_detail,
get_workcenters,
get_packages,
search_workorders,
search_lot_ids,
)
def disable_cache(func):
"""Decorator to disable Redis cache for Oracle fallback tests."""
@wraps(func)
def wrapper(*args, **kwargs):
with patch('mes_dashboard.services.wip_service.get_cached_wip_data', return_value=None):
with patch('mes_dashboard.services.wip_service.get_cached_sys_date', return_value=None):
return func(*args, **kwargs)
return wrapper
class TestWipServiceConfig(unittest.TestCase):
"""Test WIP service configuration."""
def test_wip_view_configured(self):
"""WIP_VIEW should be configured correctly."""
self.assertEqual(WIP_VIEW, "DW_MES_LOT_V")
class TestEscapeSql(unittest.TestCase):
"""Test _escape_sql function for SQL injection prevention."""
def test_escapes_single_quotes(self):
"""Should escape single quotes."""
self.assertEqual(_escape_sql("O'Brien"), "O''Brien")
def test_escapes_multiple_quotes(self):
"""Should escape multiple single quotes."""
self.assertEqual(_escape_sql("It's Bob's"), "It''s Bob''s")
def test_handles_none(self):
"""Should return None for None input."""
self.assertIsNone(_escape_sql(None))
def test_no_change_for_safe_string(self):
"""Should not modify strings without quotes."""
self.assertEqual(_escape_sql("GA26012345"), "GA26012345")
class TestBuildBaseConditions(unittest.TestCase):
"""Test _build_base_conditions function."""
def test_default_excludes_dummy(self):
"""Default behavior should exclude DUMMY lots."""
conditions = _build_base_conditions()
self.assertIn("LOTID NOT LIKE '%DUMMY%'", conditions)
def test_include_dummy_true(self):
"""include_dummy=True should not add DUMMY exclusion."""
conditions = _build_base_conditions(include_dummy=True)
self.assertNotIn("LOTID NOT LIKE '%DUMMY%'", conditions)
def test_workorder_filter(self):
"""Should add WORKORDER LIKE condition."""
conditions = _build_base_conditions(workorder='GA26')
self.assertTrue(any("WORKORDER LIKE '%GA26%'" in c for c in conditions))
def test_lotid_filter(self):
"""Should add LOTID LIKE condition."""
conditions = _build_base_conditions(lotid='12345')
self.assertTrue(any("LOTID LIKE '%12345%'" in c for c in conditions))
def test_escapes_sql_in_workorder(self):
"""Should escape SQL special characters in workorder."""
conditions = _build_base_conditions(workorder="test'value")
# Should have escaped the quote
self.assertTrue(any("test''value" in c for c in conditions))
def test_escapes_sql_in_lotid(self):
"""Should escape SQL special characters in lotid."""
conditions = _build_base_conditions(lotid="lot'id")
# Should have escaped the quote
self.assertTrue(any("lot''id" in c for c in conditions))
class TestGetWipSummary(unittest.TestCase):
"""Test get_wip_summary function."""
@disable_cache
@patch('mes_dashboard.services.wip_service.read_sql_df')
def test_returns_none_on_empty_result(self, mock_read_sql):
"""Should return None when query returns empty DataFrame."""
mock_read_sql.return_value = pd.DataFrame()
result = get_wip_summary()
self.assertIsNone(result)
@disable_cache
@patch('mes_dashboard.services.wip_service.read_sql_df')
def test_returns_none_on_exception(self, mock_read_sql):
"""Should return None when query raises exception."""
mock_read_sql.side_effect = Exception("Database error")
result = get_wip_summary()
self.assertIsNone(result)
class TestGetWipMatrix(unittest.TestCase):
"""Test get_wip_matrix function."""
@disable_cache
@patch('mes_dashboard.services.wip_service.read_sql_df')
def test_returns_matrix_structure(self, mock_read_sql):
"""Should return dict with matrix structure."""
mock_df = pd.DataFrame({
'WORKCENTER_GROUP': ['切割', '切割', '焊接_DB'],
'WORKCENTERSEQUENCE_GROUP': [1, 1, 2],
'PRODUCTLINENAME': ['SOT-23', 'SOD-323', 'SOT-23'],
'QTY': [50000000, 30000000, 40000000]
})
mock_read_sql.return_value = mock_df
result = get_wip_matrix()
self.assertIsNotNone(result)
self.assertIn('workcenters', result)
self.assertIn('packages', result)
self.assertIn('matrix', result)
self.assertIn('workcenter_totals', result)
self.assertIn('package_totals', result)
self.assertIn('grand_total', result)
@disable_cache
@patch('mes_dashboard.services.wip_service.read_sql_df')
def test_workcenters_sorted_by_sequence(self, mock_read_sql):
"""Workcenters should be sorted by WORKCENTERSEQUENCE_GROUP."""
mock_df = pd.DataFrame({
'WORKCENTER_GROUP': ['焊接_DB', '切割'],
'WORKCENTERSEQUENCE_GROUP': [2, 1],
'PRODUCTLINENAME': ['SOT-23', 'SOT-23'],
'QTY': [40000000, 50000000]
})
mock_read_sql.return_value = mock_df
result = get_wip_matrix()
self.assertEqual(result['workcenters'], ['切割', '焊接_DB'])
@disable_cache
@patch('mes_dashboard.services.wip_service.read_sql_df')
def test_packages_sorted_by_qty_desc(self, mock_read_sql):
"""Packages should be sorted by total QTY descending."""
mock_df = pd.DataFrame({
'WORKCENTER_GROUP': ['切割', '切割'],
'WORKCENTERSEQUENCE_GROUP': [1, 1],
'PRODUCTLINENAME': ['SOD-323', 'SOT-23'],
'QTY': [30000000, 50000000]
})
mock_read_sql.return_value = mock_df
result = get_wip_matrix()
self.assertEqual(result['packages'][0], 'SOT-23') # Higher QTY first
@disable_cache
@patch('mes_dashboard.services.wip_service.read_sql_df')
def test_returns_empty_structure_on_empty_result(self, mock_read_sql):
"""Should return empty structure when no data."""
mock_read_sql.return_value = pd.DataFrame()
result = get_wip_matrix()
self.assertIsNotNone(result)
self.assertEqual(result['workcenters'], [])
self.assertEqual(result['packages'], [])
self.assertEqual(result['grand_total'], 0)
@disable_cache
@patch('mes_dashboard.services.wip_service.read_sql_df')
def test_calculates_totals_correctly(self, mock_read_sql):
"""Should calculate workcenter and package totals correctly."""
mock_df = pd.DataFrame({
'WORKCENTER_GROUP': ['切割', '切割'],
'WORKCENTERSEQUENCE_GROUP': [1, 1],
'PRODUCTLINENAME': ['SOT-23', 'SOD-323'],
'QTY': [50000000, 30000000]
})
mock_read_sql.return_value = mock_df
result = get_wip_matrix()
self.assertEqual(result['workcenter_totals']['切割'], 80000000)
self.assertEqual(result['package_totals']['SOT-23'], 50000000)
self.assertEqual(result['grand_total'], 80000000)
class TestGetWipHoldSummary(unittest.TestCase):
"""Test get_wip_hold_summary function."""
@disable_cache
@patch('mes_dashboard.services.wip_service.read_sql_df')
def test_returns_hold_items(self, mock_read_sql):
"""Should return list of hold items."""
mock_df = pd.DataFrame({
'REASON': ['YieldLimit', '特殊需求管控'],
'LOTS': [21, 44],
'QTY': [1084443, 4235060]
})
mock_read_sql.return_value = mock_df
result = get_wip_hold_summary()
self.assertIsNotNone(result)
self.assertIn('items', result)
self.assertEqual(len(result['items']), 2)
self.assertEqual(result['items'][0]['reason'], 'YieldLimit')
self.assertEqual(result['items'][0]['lots'], 21)
@disable_cache
@patch('mes_dashboard.services.wip_service.read_sql_df')
def test_returns_empty_items_on_no_holds(self, mock_read_sql):
"""Should return empty items list when no holds."""
mock_read_sql.return_value = pd.DataFrame()
result = get_wip_hold_summary()
self.assertIsNotNone(result)
self.assertEqual(result['items'], [])
class TestGetWipDetail(unittest.TestCase):
"""Test get_wip_detail function."""
@disable_cache
@patch('mes_dashboard.services.wip_service.read_sql_df')
def test_returns_none_on_empty_summary(self, mock_read_sql):
"""Should return None when summary query returns empty."""
mock_read_sql.return_value = pd.DataFrame()
result = get_wip_detail('不存在的工站')
self.assertIsNone(result)
class TestGetWorkcenters(unittest.TestCase):
"""Test get_workcenters function."""
@disable_cache
@patch('mes_dashboard.services.wip_service.read_sql_df')
def test_returns_workcenter_list(self, mock_read_sql):
"""Should return list of workcenters with lot counts."""
mock_df = pd.DataFrame({
'WORKCENTER_GROUP': ['切割', '焊接_DB'],
'WORKCENTERSEQUENCE_GROUP': [1, 2],
'LOT_COUNT': [1377, 859]
})
mock_read_sql.return_value = mock_df
result = get_workcenters()
self.assertIsNotNone(result)
self.assertEqual(len(result), 2)
self.assertEqual(result[0]['name'], '切割')
self.assertEqual(result[0]['lot_count'], 1377)
@disable_cache
@patch('mes_dashboard.services.wip_service.read_sql_df')
def test_returns_empty_list_on_no_data(self, mock_read_sql):
"""Should return empty list when no workcenters."""
mock_read_sql.return_value = pd.DataFrame()
result = get_workcenters()
self.assertEqual(result, [])
@disable_cache
@patch('mes_dashboard.services.wip_service.read_sql_df')
def test_returns_none_on_exception(self, mock_read_sql):
"""Should return None on exception."""
mock_read_sql.side_effect = Exception("Database error")
result = get_workcenters()
self.assertIsNone(result)
class TestGetPackages(unittest.TestCase):
"""Test get_packages function."""
@disable_cache
@patch('mes_dashboard.services.wip_service.read_sql_df')
def test_returns_package_list(self, mock_read_sql):
"""Should return list of packages with lot counts."""
mock_df = pd.DataFrame({
'PRODUCTLINENAME': ['SOT-23', 'SOD-323'],
'LOT_COUNT': [2234, 1392]
})
mock_read_sql.return_value = mock_df
result = get_packages()
self.assertIsNotNone(result)
self.assertEqual(len(result), 2)
self.assertEqual(result[0]['name'], 'SOT-23')
self.assertEqual(result[0]['lot_count'], 2234)
@disable_cache
@patch('mes_dashboard.services.wip_service.read_sql_df')
def test_returns_empty_list_on_no_data(self, mock_read_sql):
"""Should return empty list when no packages."""
mock_read_sql.return_value = pd.DataFrame()
result = get_packages()
self.assertEqual(result, [])
class TestSearchWorkorders(unittest.TestCase):
"""Test search_workorders function."""
@disable_cache
@patch('mes_dashboard.services.wip_service.read_sql_df')
def test_returns_matching_workorders(self, mock_read_sql):
"""Should return list of matching WORKORDER values."""
mock_df = pd.DataFrame({
'WORKORDER': ['GA26012001', 'GA26012002', 'GA26012003']
})
mock_read_sql.return_value = mock_df
result = search_workorders('GA26')
self.assertIsNotNone(result)
self.assertEqual(len(result), 3)
self.assertEqual(result[0], 'GA26012001')
@disable_cache
@patch('mes_dashboard.services.wip_service.read_sql_df')
def test_returns_empty_list_for_short_query(self, mock_read_sql):
"""Should return empty list for query < 2 characters."""
result = search_workorders('G')
self.assertEqual(result, [])
mock_read_sql.assert_not_called()
@disable_cache
@patch('mes_dashboard.services.wip_service.read_sql_df')
def test_returns_empty_list_for_empty_query(self, mock_read_sql):
"""Should return empty list for empty query."""
result = search_workorders('')
self.assertEqual(result, [])
mock_read_sql.assert_not_called()
@disable_cache
@patch('mes_dashboard.services.wip_service.read_sql_df')
def test_returns_empty_list_on_no_matches(self, mock_read_sql):
"""Should return empty list when no matches found."""
mock_read_sql.return_value = pd.DataFrame()
result = search_workorders('NONEXISTENT')
self.assertEqual(result, [])
@disable_cache
@patch('mes_dashboard.services.wip_service.read_sql_df')
def test_respects_limit_parameter(self, mock_read_sql):
"""Should respect the limit parameter."""
mock_df = pd.DataFrame({
'WORKORDER': ['GA26012001', 'GA26012002']
})
mock_read_sql.return_value = mock_df
result = search_workorders('GA26', limit=2)
self.assertEqual(len(result), 2)
@disable_cache
@patch('mes_dashboard.services.wip_service.read_sql_df')
def test_caps_limit_at_50(self, mock_read_sql):
"""Should cap limit at 50."""
mock_df = pd.DataFrame({'WORKORDER': ['GA26012001']})
mock_read_sql.return_value = mock_df
search_workorders('GA26', limit=100)
# Verify SQL contains FETCH FIRST 50
call_args = mock_read_sql.call_args[0][0]
self.assertIn('FETCH FIRST 50 ROWS ONLY', call_args)
@disable_cache
@patch('mes_dashboard.services.wip_service.read_sql_df')
def test_returns_none_on_exception(self, mock_read_sql):
"""Should return None on exception."""
mock_read_sql.side_effect = Exception("Database error")
result = search_workorders('GA26')
self.assertIsNone(result)
@disable_cache
@patch('mes_dashboard.services.wip_service.read_sql_df')
def test_excludes_dummy_by_default(self, mock_read_sql):
"""Should exclude DUMMY lots by default."""
mock_df = pd.DataFrame({'WORKORDER': []})
mock_read_sql.return_value = mock_df
search_workorders('GA26')
call_args = mock_read_sql.call_args[0][0]
self.assertIn("LOTID NOT LIKE '%DUMMY%'", call_args)
@disable_cache
@patch('mes_dashboard.services.wip_service.read_sql_df')
def test_includes_dummy_when_specified(self, mock_read_sql):
"""Should include DUMMY lots when include_dummy=True."""
mock_df = pd.DataFrame({'WORKORDER': []})
mock_read_sql.return_value = mock_df
search_workorders('GA26', include_dummy=True)
call_args = mock_read_sql.call_args[0][0]
self.assertNotIn("LOTID NOT LIKE '%DUMMY%'", call_args)
class TestSearchLotIds(unittest.TestCase):
"""Test search_lot_ids function."""
@disable_cache
@patch('mes_dashboard.services.wip_service.read_sql_df')
def test_returns_matching_lotids(self, mock_read_sql):
"""Should return list of matching LOTID values."""
mock_df = pd.DataFrame({
'LOTID': ['GA26012345-A00-001', 'GA26012345-A00-002']
})
mock_read_sql.return_value = mock_df
result = search_lot_ids('GA26012345')
self.assertIsNotNone(result)
self.assertEqual(len(result), 2)
self.assertEqual(result[0], 'GA26012345-A00-001')
@disable_cache
@patch('mes_dashboard.services.wip_service.read_sql_df')
def test_returns_empty_list_for_short_query(self, mock_read_sql):
"""Should return empty list for query < 2 characters."""
result = search_lot_ids('G')
self.assertEqual(result, [])
mock_read_sql.assert_not_called()
@disable_cache
@patch('mes_dashboard.services.wip_service.read_sql_df')
def test_returns_empty_list_on_no_matches(self, mock_read_sql):
"""Should return empty list when no matches found."""
mock_read_sql.return_value = pd.DataFrame()
result = search_lot_ids('NONEXISTENT')
self.assertEqual(result, [])
@disable_cache
@patch('mes_dashboard.services.wip_service.read_sql_df')
def test_returns_none_on_exception(self, mock_read_sql):
"""Should return None on exception."""
mock_read_sql.side_effect = Exception("Database error")
result = search_lot_ids('GA26')
self.assertIsNone(result)
@disable_cache
@patch('mes_dashboard.services.wip_service.read_sql_df')
def test_excludes_dummy_by_default(self, mock_read_sql):
"""Should exclude DUMMY lots by default."""
mock_df = pd.DataFrame({'LOTID': []})
mock_read_sql.return_value = mock_df
search_lot_ids('GA26')
call_args = mock_read_sql.call_args[0][0]
self.assertIn("LOTID NOT LIKE '%DUMMY%'", call_args)
class TestDummyExclusionInAllFunctions(unittest.TestCase):
"""Test DUMMY exclusion is applied in all WIP functions."""
@disable_cache
@patch('mes_dashboard.services.wip_service.read_sql_df')
def test_get_wip_summary_excludes_dummy_by_default(self, mock_read_sql):
"""get_wip_summary should exclude DUMMY by default."""
mock_df = pd.DataFrame({
'TOTAL_LOTS': [100],
'TOTAL_QTY_PCS': [1000],
'RUN_LOTS': [80],
'RUN_QTY_PCS': [800],
'QUEUE_LOTS': [10],
'QUEUE_QTY_PCS': [100],
'HOLD_LOTS': [10],
'HOLD_QTY_PCS': [100],
'DATA_UPDATE_DATE': ['2026-01-26']
})
mock_read_sql.return_value = mock_df
get_wip_summary()
call_args = mock_read_sql.call_args[0][0]
self.assertIn("LOTID NOT LIKE '%DUMMY%'", call_args)
@disable_cache
@patch('mes_dashboard.services.wip_service.read_sql_df')
def test_get_wip_summary_includes_dummy_when_specified(self, mock_read_sql):
"""get_wip_summary should include DUMMY when specified."""
mock_df = pd.DataFrame({
'TOTAL_LOTS': [100],
'TOTAL_QTY_PCS': [1000],
'RUN_LOTS': [80],
'RUN_QTY_PCS': [800],
'QUEUE_LOTS': [10],
'QUEUE_QTY_PCS': [100],
'HOLD_LOTS': [10],
'HOLD_QTY_PCS': [100],
'DATA_UPDATE_DATE': ['2026-01-26']
})
mock_read_sql.return_value = mock_df
get_wip_summary(include_dummy=True)
call_args = mock_read_sql.call_args[0][0]
self.assertNotIn("LOTID NOT LIKE '%DUMMY%'", call_args)
@disable_cache
@patch('mes_dashboard.services.wip_service.read_sql_df')
def test_get_wip_matrix_excludes_dummy_by_default(self, mock_read_sql):
"""get_wip_matrix should exclude DUMMY by default."""
mock_df = pd.DataFrame({
'WORKCENTER_GROUP': ['切割'],
'WORKCENTERSEQUENCE_GROUP': [1],
'PRODUCTLINENAME': ['SOT-23'],
'QTY': [1000]
})
mock_read_sql.return_value = mock_df
get_wip_matrix()
call_args = mock_read_sql.call_args[0][0]
self.assertIn("LOTID NOT LIKE '%DUMMY%'", call_args)
@disable_cache
@patch('mes_dashboard.services.wip_service.read_sql_df')
def test_get_wip_hold_summary_excludes_dummy_by_default(self, mock_read_sql):
"""get_wip_hold_summary should exclude DUMMY by default."""
mock_df = pd.DataFrame({
'REASON': ['YieldLimit'], 'LOTS': [10], 'QTY': [1000]
})
mock_read_sql.return_value = mock_df
get_wip_hold_summary()
call_args = mock_read_sql.call_args[0][0]
self.assertIn("LOTID NOT LIKE '%DUMMY%'", call_args)
@disable_cache
@patch('mes_dashboard.services.wip_service.read_sql_df')
def test_get_workcenters_excludes_dummy_by_default(self, mock_read_sql):
"""get_workcenters should exclude DUMMY by default."""
mock_df = pd.DataFrame({
'WORKCENTER_GROUP': ['切割'],
'WORKCENTERSEQUENCE_GROUP': [1],
'LOT_COUNT': [100]
})
mock_read_sql.return_value = mock_df
get_workcenters()
call_args = mock_read_sql.call_args[0][0]
self.assertIn("LOTID NOT LIKE '%DUMMY%'", call_args)
@disable_cache
@patch('mes_dashboard.services.wip_service.read_sql_df')
def test_get_packages_excludes_dummy_by_default(self, mock_read_sql):
"""get_packages should exclude DUMMY by default."""
mock_df = pd.DataFrame({
'PRODUCTLINENAME': ['SOT-23'], 'LOT_COUNT': [100]
})
mock_read_sql.return_value = mock_df
get_packages()
call_args = mock_read_sql.call_args[0][0]
self.assertIn("LOTID NOT LIKE '%DUMMY%'", call_args)
class TestMultipleFilterConditions(unittest.TestCase):
"""Test multiple filter conditions work together."""
@disable_cache
@patch('mes_dashboard.services.wip_service.read_sql_df')
def test_get_wip_summary_with_all_filters(self, mock_read_sql):
"""get_wip_summary should combine all filter conditions."""
mock_df = pd.DataFrame({
'TOTAL_LOTS': [50],
'TOTAL_QTY_PCS': [500],
'RUN_LOTS': [40],
'RUN_QTY_PCS': [400],
'QUEUE_LOTS': [5],
'QUEUE_QTY_PCS': [50],
'HOLD_LOTS': [5],
'HOLD_QTY_PCS': [50],
'DATA_UPDATE_DATE': ['2026-01-26']
})
mock_read_sql.return_value = mock_df
get_wip_summary(workorder='GA26', lotid='A00')
call_args = mock_read_sql.call_args[0][0]
self.assertIn("WORKORDER LIKE '%GA26%'", call_args)
self.assertIn("LOTID LIKE '%A00%'", call_args)
self.assertIn("LOTID NOT LIKE '%DUMMY%'", call_args)
@disable_cache
@patch('mes_dashboard.services.wip_service.read_sql_df')
def test_get_wip_matrix_with_all_filters(self, mock_read_sql):
"""get_wip_matrix should combine all filter conditions."""
mock_df = pd.DataFrame({
'WORKCENTER_GROUP': ['切割'],
'WORKCENTERSEQUENCE_GROUP': [1],
'PRODUCTLINENAME': ['SOT-23'],
'QTY': [500]
})
mock_read_sql.return_value = mock_df
get_wip_matrix(workorder='GA26', lotid='A00', include_dummy=True)
call_args = mock_read_sql.call_args[0][0]
self.assertIn("WORKORDER LIKE '%GA26%'", call_args)
self.assertIn("LOTID LIKE '%A00%'", call_args)
# Should NOT contain DUMMY exclusion since include_dummy=True
self.assertNotIn("LOTID NOT LIKE '%DUMMY%'", call_args)
import pytest
class TestWipServiceIntegration:
"""Integration tests that hit the actual database.
These tests are skipped by default. Run with:
python -m pytest tests/test_wip_service.py -k Integration --run-integration
"""
@pytest.mark.integration
def test_get_wip_summary_integration(self):
"""Integration test for get_wip_summary."""
result = get_wip_summary()
assert result is not None
assert result['totalLots'] > 0
assert 'dataUpdateDate' in result
@pytest.mark.integration
def test_get_wip_matrix_integration(self):
"""Integration test for get_wip_matrix."""
result = get_wip_matrix()
assert result is not None
assert len(result['workcenters']) > 0
assert result['grand_total'] > 0
@pytest.mark.integration
def test_get_wip_hold_summary_integration(self):
"""Integration test for get_wip_hold_summary."""
result = get_wip_hold_summary()
assert result is not None
assert 'items' in result
@pytest.mark.integration
def test_get_wip_detail_integration(self):
"""Integration test for get_wip_detail."""
# First get a valid workcenter
workcenters = get_workcenters()
assert workcenters is not None and len(workcenters) > 0
wc_name = workcenters[0]['name']
result = get_wip_detail(wc_name, page=1, page_size=10)
assert result is not None
assert result['workcenter'] == wc_name
assert 'summary' in result
assert 'lots' in result
assert 'pagination' in result
@pytest.mark.integration
def test_get_workcenters_integration(self):
"""Integration test for get_workcenters."""
result = get_workcenters()
assert result is not None
assert len(result) > 0
assert 'name' in result[0]
assert 'lot_count' in result[0]
@pytest.mark.integration
def test_get_packages_integration(self):
"""Integration test for get_packages."""
result = get_packages()
assert result is not None
assert len(result) > 0
assert 'name' in result[0]
assert 'lot_count' in result[0]
@pytest.mark.integration
def test_search_workorders_integration(self):
"""Integration test for search_workorders."""
# Use a common prefix that likely exists
result = search_workorders('GA')
assert result is not None
# Should return a list (possibly empty if no GA* workorders)
assert isinstance(result, list)
@pytest.mark.integration
def test_search_lot_ids_integration(self):
"""Integration test for search_lot_ids."""
# Use a common prefix that likely exists
result = search_lot_ids('GA')
assert result is not None
assert isinstance(result, list)
@pytest.mark.integration
def test_dummy_exclusion_integration(self):
"""Integration test to verify DUMMY exclusion works."""
# Get summary with and without DUMMY
result_without_dummy = get_wip_summary(include_dummy=False)
result_with_dummy = get_wip_summary(include_dummy=True)
assert result_without_dummy is not None
assert result_with_dummy is not None
# If there are DUMMY lots, with_dummy should have more
# (or equal if no DUMMY lots exist)
assert result_with_dummy['totalLots'] >= result_without_dummy['totalLots']
@pytest.mark.integration
def test_workorder_filter_integration(self):
"""Integration test for workorder filter."""
# Get all data first
all_result = get_wip_summary()
assert all_result is not None
# Search for a workorder that exists
workorders = search_workorders('GA', limit=1)
if workorders and len(workorders) > 0:
# Filter by that workorder
filtered_result = get_wip_summary(workorder=workorders[0])
assert filtered_result is not None
# Filtered count should be less than or equal to total
assert filtered_result['totalLots'] <= all_result['totalLots']
if __name__ == "__main__":
unittest.main()