# -*- coding: utf-8 -*- """Integration tests for cache functionality. Tests API endpoints with cache enabled/disabled scenarios. """ import pytest from unittest.mock import patch, MagicMock import pandas as pd import json @pytest.fixture def app_with_mock_cache(): """Create app with mocked cache.""" import mes_dashboard.core.database as db db._ENGINE = None from mes_dashboard.app import create_app app = create_app('testing') app.config['TESTING'] = True return app class TestHealthEndpoint: """Test /health endpoint.""" @patch('mes_dashboard.routes.health_routes.check_database') @patch('mes_dashboard.routes.health_routes.check_redis') @patch('mes_dashboard.routes.health_routes.get_cache_status') @patch('mes_dashboard.routes.health_routes.get_route_cache_status', return_value={'mode': 'none', 'degraded': False, 'available': False}) @patch('mes_dashboard.core.circuit_breaker.get_circuit_breaker_status', return_value={'state': 'CLOSED', 'enabled': True, 'failure_count': 0, 'success_count': 0, 'total_count': 0, 'failure_rate': 0.0}) def test_health_all_ok(self, mock_cb, mock_route_cache, mock_cache_status, mock_check_redis, mock_check_db, app_with_mock_cache): """Test health endpoint returns 200 when all services are healthy.""" mock_check_db.return_value = ('ok', None) mock_check_redis.return_value = ('ok', None) mock_cache_status.return_value = { 'enabled': True, 'sys_date': '2024-01-15 10:30:00', 'updated_at': '2024-01-15T10:30:00' } with app_with_mock_cache.test_client() as client: response = client.get('/health') assert response.status_code == 200 data = response.get_json() assert data['status'] == 'healthy' assert data['services']['database'] == 'ok' assert data['services']['redis'] == 'ok' @patch('mes_dashboard.routes.health_routes.check_database') @patch('mes_dashboard.routes.health_routes.check_redis') @patch('mes_dashboard.routes.health_routes.get_cache_status') def test_health_redis_down_degraded(self, mock_cache_status, mock_check_redis, mock_check_db, app_with_mock_cache): """Test health endpoint returns 200 degraded when Redis is down.""" mock_check_db.return_value = ('ok', None) mock_check_redis.return_value = ('error', 'Connection refused') mock_cache_status.return_value = {'enabled': True, 'sys_date': None, 'updated_at': None} with app_with_mock_cache.test_client() as client: response = client.get('/health') assert response.status_code == 200 data = response.get_json() assert data['status'] == 'degraded' assert 'warnings' in data @patch('mes_dashboard.routes.health_routes.check_database') @patch('mes_dashboard.routes.health_routes.check_redis') @patch('mes_dashboard.routes.health_routes.get_cache_status') def test_health_db_down_unhealthy(self, mock_cache_status, mock_check_redis, mock_check_db, app_with_mock_cache): """Test health endpoint returns 503 when database is down.""" mock_check_db.return_value = ('error', 'Connection refused') mock_check_redis.return_value = ('ok', None) mock_cache_status.return_value = {'enabled': True, 'sys_date': None, 'updated_at': None} with app_with_mock_cache.test_client() as client: response = client.get('/health') assert response.status_code == 503 data = response.get_json() assert data['status'] == 'unhealthy' assert 'errors' in data @patch('mes_dashboard.routes.health_routes.check_database') @patch('mes_dashboard.routes.health_routes.check_redis') @patch('mes_dashboard.routes.health_routes.get_cache_status') @patch('mes_dashboard.routes.health_routes.get_route_cache_status', return_value={'mode': 'none', 'degraded': False, 'available': False}) @patch('mes_dashboard.core.circuit_breaker.get_circuit_breaker_status', return_value={'state': 'CLOSED', 'enabled': True, 'failure_count': 0, 'success_count': 0, 'total_count': 0, 'failure_rate': 0.0}) def test_health_redis_disabled(self, mock_cb, mock_route_cache, mock_cache_status, mock_check_redis, mock_check_db, app_with_mock_cache): """Test health endpoint shows Redis disabled status.""" mock_check_db.return_value = ('ok', None) mock_check_redis.return_value = ('disabled', None) mock_cache_status.return_value = {'enabled': False, 'sys_date': None, 'updated_at': None} with app_with_mock_cache.test_client() as client: response = client.get('/health') assert response.status_code == 200 data = response.get_json() assert data['status'] == 'healthy' assert data['services']['redis'] == 'disabled' class TestWipApiWithCache: """Test WIP API endpoints with cache.""" @pytest.fixture def mock_wip_cache_data(self): """Create mock WIP data for cache.""" return pd.DataFrame({ 'LOTID': ['LOT001', 'LOT002', 'LOT003'], 'QTY': [100, 200, 150], 'WORKORDER': ['WO001', 'WO002', 'WO003'], 'WORKCENTER_GROUP': ['WC1', 'WC1', 'WC2'], 'WORKCENTERSEQUENCE_GROUP': [1, 1, 2], 'PACKAGE_LEF': ['PKG1', 'PKG2', 'PKG1'], 'PRODUCTLINENAME': ['PKG1', 'PKG2', 'PKG1'], 'EQUIPMENTCOUNT': [1, 0, 0], 'CURRENTHOLDCOUNT': [0, 1, 0], 'HOLDREASONNAME': [None, 'Quality Issue', None], 'STATUS': ['ACTIVE', 'HOLD', 'ACTIVE'], 'SPECNAME': ['SPEC1', 'SPEC1', 'SPEC2'], 'SPECSEQUENCE': [1, 1, 2], 'AGEBYDAYS': [1.5, 3.2, 0.5], 'EQUIPMENTS': ['EQ001', None, None], 'SYS_DATE': ['2024-01-15 10:30:00'] * 3 }) @patch('mes_dashboard.services.wip_service._get_wip_dataframe') @patch('mes_dashboard.services.wip_service.get_cached_sys_date') def test_wip_summary_uses_cache(self, mock_sys_date, mock_get_df, app_with_mock_cache, mock_wip_cache_data): """Test /api/wip/overview/summary uses cache when available.""" mock_get_df.return_value = mock_wip_cache_data mock_sys_date.return_value = '2024-01-15 10:30:00' with app_with_mock_cache.test_client() as client: response = client.get('/api/wip/overview/summary') assert response.status_code == 200 resp = response.get_json() # API returns wrapped response: {success: true, data: {...}} data = resp.get('data', resp) # Handle both wrapped and unwrapped assert data['totalLots'] == 3 assert data['dataUpdateDate'] == '2024-01-15 10:30:00' @patch('mes_dashboard.services.wip_service._get_wip_dataframe') @patch('mes_dashboard.services.wip_service.get_cached_sys_date') def test_wip_matrix_uses_cache(self, mock_sys_date, mock_get_df, app_with_mock_cache, mock_wip_cache_data): """Test /api/wip/overview/matrix uses cache when available.""" mock_get_df.return_value = mock_wip_cache_data mock_sys_date.return_value = '2024-01-15 10:30:00' with app_with_mock_cache.test_client() as client: response = client.get('/api/wip/overview/matrix') assert response.status_code == 200 resp = response.get_json() # API returns wrapped response: {success: true, data: {...}} data = resp.get('data', resp) assert 'workcenters' in data assert 'packages' in data assert 'matrix' in data @patch('mes_dashboard.services.wip_service._get_wip_dataframe') def test_workcenters_uses_cache(self, mock_get_df, app_with_mock_cache, mock_wip_cache_data): """Test /api/wip/meta/workcenters uses cache when available.""" mock_get_df.return_value = mock_wip_cache_data with app_with_mock_cache.test_client() as client: response = client.get('/api/wip/meta/workcenters') assert response.status_code == 200 resp = response.get_json() # API returns wrapped response: {success: true, data: [...]} data = resp.get('data', resp) if isinstance(resp, dict) and 'data' in resp else resp assert isinstance(data, list) assert len(data) == 2 # WC1 and WC2 @patch('mes_dashboard.services.wip_service._get_wip_dataframe') def test_packages_uses_cache(self, mock_get_df, app_with_mock_cache, mock_wip_cache_data): """Test /api/wip/meta/packages uses cache when available.""" mock_get_df.return_value = mock_wip_cache_data with app_with_mock_cache.test_client() as client: response = client.get('/api/wip/meta/packages') assert response.status_code == 200 resp = response.get_json() # API returns wrapped response: {success: true, data: [...]} data = resp.get('data', resp) if isinstance(resp, dict) and 'data' in resp else resp assert isinstance(data, list) assert len(data) == 2 # PKG1 and PKG2 class TestHealthEndpointResourceCache: """Test /health endpoint resource cache status.""" @patch('mes_dashboard.routes.health_routes.check_database') @patch('mes_dashboard.routes.health_routes.check_redis') @patch('mes_dashboard.routes.health_routes.get_cache_status') @patch('mes_dashboard.routes.health_routes.get_resource_cache_status') def test_health_includes_resource_cache( self, mock_res_cache_status, mock_cache_status, mock_check_redis, mock_check_db, app_with_mock_cache ): """Test health endpoint includes resource_cache field.""" mock_check_db.return_value = ('ok', None) mock_check_redis.return_value = ('ok', None) mock_cache_status.return_value = { 'enabled': True, 'sys_date': '2024-01-15 10:30:00', 'updated_at': '2024-01-15T10:30:00' } mock_res_cache_status.return_value = { 'enabled': True, 'loaded': True, 'count': 1500, 'version': '2024-01-15T10:00:00', 'updated_at': '2024-01-15T10:30:00' } with app_with_mock_cache.test_client() as client: response = client.get('/health') assert response.status_code == 200 data = response.get_json() assert 'resource_cache' in data assert data['resource_cache']['enabled'] is True assert data['resource_cache']['loaded'] is True assert data['resource_cache']['count'] == 1500 @patch('mes_dashboard.routes.health_routes.check_database') @patch('mes_dashboard.routes.health_routes.check_redis') @patch('mes_dashboard.routes.health_routes.get_cache_status') @patch('mes_dashboard.routes.health_routes.get_resource_cache_status') def test_health_warning_when_resource_cache_not_loaded( self, mock_res_cache_status, mock_cache_status, mock_check_redis, mock_check_db, app_with_mock_cache ): """Test health endpoint shows warning when resource cache enabled but not loaded.""" mock_check_db.return_value = ('ok', None) mock_check_redis.return_value = ('ok', None) mock_cache_status.return_value = { 'enabled': True, 'sys_date': '2024-01-15 10:30:00', 'updated_at': '2024-01-15T10:30:00' } mock_res_cache_status.return_value = { 'enabled': True, 'loaded': False, 'count': 0, 'version': None, 'updated_at': None } with app_with_mock_cache.test_client() as client: response = client.get('/health') assert response.status_code == 200 data = response.get_json() assert 'warnings' in data assert any('Resource cache not loaded' in w for w in data['warnings']) @patch('mes_dashboard.routes.health_routes.check_database') @patch('mes_dashboard.routes.health_routes.check_redis') @patch('mes_dashboard.routes.health_routes.get_cache_status') @patch('mes_dashboard.routes.health_routes.get_resource_cache_status') def test_health_no_warning_when_resource_cache_disabled( self, mock_res_cache_status, mock_cache_status, mock_check_redis, mock_check_db, app_with_mock_cache ): """Test health endpoint no warning when resource cache is disabled.""" mock_check_db.return_value = ('ok', None) mock_check_redis.return_value = ('ok', None) mock_cache_status.return_value = { 'enabled': True, 'sys_date': '2024-01-15 10:30:00', 'updated_at': '2024-01-15T10:30:00' } mock_res_cache_status.return_value = {'enabled': False} with app_with_mock_cache.test_client() as client: response = client.get('/health') assert response.status_code == 200 data = response.get_json() # No warnings about resource cache warnings = data.get('warnings', []) assert not any('Resource cache' in w for w in warnings) class TestResourceFilterOptionsWithCache: """Test resource filter options with cache.""" @patch('mes_dashboard.services.resource_cache.get_all_resources') @patch('mes_dashboard.services.resource_service.read_sql_df') def test_filter_options_uses_resource_cache( self, mock_read_sql, mock_get_all, app_with_mock_cache ): """Test resource filter options uses resource_cache for static data.""" # Mock resource cache data mock_get_all.return_value = [ {'WORKCENTERNAME': 'WC1', 'RESOURCEFAMILYNAME': 'F1', 'PJ_DEPARTMENT': 'Dept1', 'LOCATIONNAME': 'Loc1', 'PJ_ASSETSSTATUS': 'Active'}, {'WORKCENTERNAME': 'WC2', 'RESOURCEFAMILYNAME': 'F2', 'PJ_DEPARTMENT': 'Dept1', 'LOCATIONNAME': 'Loc1', 'PJ_ASSETSSTATUS': 'Active'}, ] mock_read_sql.return_value = pd.DataFrame({'NEWSTATUSNAME': ['PRD', 'SBY']}) with app_with_mock_cache.test_client() as client: response = client.get('/api/resource/filter_options') assert response.status_code == 200 data = response.get_json() if data.get('success'): options = data.get('data', {}) assert 'WC1' in options['workcenters'] assert 'WC2' in options['workcenters'] assert 'F1' in options['families'] assert 'F2' in options['families'] class TestResourceHistoryOptionsWithCache: """Test resource history filter options with cache.""" @patch('mes_dashboard.services.filter_cache.get_workcenter_groups') @patch('mes_dashboard.services.resource_cache.get_all_resources') def test_history_options_uses_resource_cache( self, mock_get_all, mock_groups, app_with_mock_cache ): """Test resource history options uses resource_cache for families.""" mock_groups.return_value = [ {'name': 'Group1', 'sequence': 1}, {'name': 'Group2', 'sequence': 2} ] # Mock resource cache data for families mock_get_all.return_value = [ {'RESOURCEFAMILYNAME': 'Family1'}, {'RESOURCEFAMILYNAME': 'Family2'}, {'RESOURCEFAMILYNAME': 'Family1'}, # duplicate ] with app_with_mock_cache.test_client() as client: response = client.get('/api/resource/history/options') assert response.status_code == 200 data = response.get_json() if data.get('success'): options = data.get('data', {}) assert 'families' in options assert 'Family1' in options['families'] assert 'Family2' in options['families'] class TestFallbackToOracle: """Test fallback to Oracle when cache is unavailable.""" @patch('mes_dashboard.services.wip_service._get_wip_dataframe') @patch('mes_dashboard.services.wip_service._get_wip_summary_from_oracle') def test_summary_falls_back_to_oracle(self, mock_oracle, mock_get_df, app_with_mock_cache): """Test summary falls back to Oracle when cache unavailable.""" mock_get_df.return_value = None # Cache miss mock_oracle.return_value = { 'totalLots': 100, 'totalQtyPcs': 10000, 'byWipStatus': { 'run': {'lots': 30, 'qtyPcs': 3000}, 'queue': {'lots': 50, 'qtyPcs': 5000}, 'hold': {'lots': 20, 'qtyPcs': 2000}, 'qualityHold': {'lots': 15, 'qtyPcs': 1500}, 'nonQualityHold': {'lots': 5, 'qtyPcs': 500} }, 'dataUpdateDate': '2024-01-15 10:30:00' } with app_with_mock_cache.test_client() as client: response = client.get('/api/wip/overview/summary') assert response.status_code == 200 resp = response.get_json() # API returns wrapped response: {success: true, data: {...}} data = resp.get('data', resp) assert data['totalLots'] == 100 mock_oracle.assert_called_once() @patch('mes_dashboard.services.wip_service._get_wip_dataframe') @patch('mes_dashboard.services.wip_service._get_workcenters_from_oracle') def test_workcenters_falls_back_to_oracle(self, mock_oracle, mock_get_df, app_with_mock_cache): """Test workcenters falls back to Oracle when cache unavailable.""" mock_get_df.return_value = None # Cache miss mock_oracle.return_value = [ {'name': 'WC1', 'lot_count': 50}, {'name': 'WC2', 'lot_count': 30} ] with app_with_mock_cache.test_client() as client: response = client.get('/api/wip/meta/workcenters') assert response.status_code == 200 resp = response.get_json() # API returns wrapped response: {success: true, data: [...]} data = resp.get('data', resp) if isinstance(resp, dict) and 'data' in resp else resp assert len(data) == 2 mock_oracle.assert_called_once()