# -*- coding: utf-8 -*- """Integration tests for Excel query API routes. Tests the API endpoints with mocked database dependencies. """ import pytest import json import io from unittest.mock import patch, MagicMock from mes_dashboard import create_app @pytest.fixture def app(): """Create test Flask application.""" app = create_app() app.config['TESTING'] = True return app @pytest.fixture def client(app): """Create test client.""" return app.test_client() @pytest.fixture def mock_excel_file(): """Create a mock Excel file content.""" import openpyxl wb = openpyxl.Workbook() ws = wb.active ws['A1'] = 'LOT_ID' ws['B1'] = 'PRODUCT' ws['C1'] = 'DATE' ws['A2'] = 'LOT001' ws['B2'] = 'PROD_A' ws['C2'] = '2024-01-15' ws['A3'] = 'LOT002' ws['B3'] = 'PROD_B' ws['C3'] = '2024-01-16' ws['A4'] = 'LOT003' ws['B4'] = 'PROD_A' ws['C4'] = '2024-01-17' buffer = io.BytesIO() wb.save(buffer) buffer.seek(0) return buffer class TestUploadExcel: """Tests for /api/excel-query/upload endpoint.""" def test_upload_no_file(self, client): """Should return error when no file provided.""" response = client.post('/api/excel-query/upload') assert response.status_code == 400 data = json.loads(response.data) assert 'error' in data def test_upload_empty_filename(self, client): """Should return error for empty filename.""" response = client.post( '/api/excel-query/upload', data={'file': (io.BytesIO(b''), '')}, content_type='multipart/form-data' ) assert response.status_code == 400 def test_upload_invalid_extension(self, client): """Should reject non-Excel files.""" response = client.post( '/api/excel-query/upload', data={'file': (io.BytesIO(b'test'), 'test.txt')}, content_type='multipart/form-data' ) assert response.status_code == 400 data = json.loads(response.data) assert '.xlsx' in data['error'] or '.xls' in data['error'] def test_upload_valid_excel(self, client, mock_excel_file): """Should successfully parse valid Excel file.""" response = client.post( '/api/excel-query/upload', data={'file': (mock_excel_file, 'test.xlsx')}, content_type='multipart/form-data' ) assert response.status_code == 200 data = json.loads(response.data) assert 'columns' in data assert 'LOT_ID' in data['columns'] assert 'preview' in data class TestGetColumnValues: """Tests for /api/excel-query/column-values endpoint.""" def test_no_column_name(self, client): """Should return error without column name.""" response = client.post( '/api/excel-query/column-values', json={} ) assert response.status_code == 400 def test_no_excel_uploaded(self, client): """Should return error if no Excel uploaded.""" # Clear cache first from mes_dashboard.routes.excel_query_routes import _uploaded_excel_cache _uploaded_excel_cache.clear() response = client.post( '/api/excel-query/column-values', json={'column_name': 'LOT_ID'} ) assert response.status_code == 400 def test_get_values_after_upload(self, client, mock_excel_file): """Should return column values after upload.""" # First upload client.post( '/api/excel-query/upload', data={'file': (mock_excel_file, 'test.xlsx')}, content_type='multipart/form-data' ) # Then get values response = client.post( '/api/excel-query/column-values', json={'column_name': 'LOT_ID'} ) assert response.status_code == 200 data = json.loads(response.data) assert 'values' in data assert 'LOT001' in data['values'] class TestGetTables: """Tests for /api/excel-query/tables endpoint.""" def test_get_tables(self, client): """Should return available tables.""" response = client.get('/api/excel-query/tables') assert response.status_code == 200 data = json.loads(response.data) assert 'tables' in data assert isinstance(data['tables'], list) class TestTableMetadata: """Tests for /api/excel-query/table-metadata endpoint.""" def test_no_table_name(self, client): """Should return error without table name.""" response = client.post( '/api/excel-query/table-metadata', json={} ) assert response.status_code == 400 @patch('mes_dashboard.routes.excel_query_routes.get_table_column_metadata') def test_get_metadata_success(self, mock_metadata, client): """Should return enriched metadata.""" mock_metadata.return_value = { 'columns': [ {'name': 'LOT_ID', 'data_type': 'VARCHAR2', 'is_date': False, 'is_number': False}, {'name': 'TXNDATE', 'data_type': 'DATE', 'is_date': True, 'is_number': False}, ] } response = client.post( '/api/excel-query/table-metadata', json={'table_name': 'TEST_TABLE'} ) assert response.status_code == 200 data = json.loads(response.data) assert 'columns' in data assert len(data['columns']) == 2 @patch('mes_dashboard.routes.excel_query_routes.get_table_column_metadata') def test_metadata_not_found(self, mock_metadata, client): """Should handle table not found.""" mock_metadata.return_value = {'error': 'Table not found', 'columns': []} response = client.post( '/api/excel-query/table-metadata', json={'table_name': 'NONEXISTENT'} ) assert response.status_code == 400 class TestExecuteAdvancedQuery: """Tests for /api/excel-query/execute-advanced endpoint.""" def test_missing_table_name(self, client): """Should return error without table name.""" response = client.post( '/api/excel-query/execute-advanced', json={ 'search_column': 'LOT_ID', 'return_columns': ['LOT_ID'], 'search_values': ['LOT001'] } ) assert response.status_code == 400 def test_missing_search_column(self, client): """Should return error without search column.""" response = client.post( '/api/excel-query/execute-advanced', json={ 'table_name': 'TEST_TABLE', 'return_columns': ['LOT_ID'], 'search_values': ['LOT001'] } ) assert response.status_code == 400 def test_invalid_query_type(self, client): """Should reject invalid query type.""" response = client.post( '/api/excel-query/execute-advanced', json={ 'table_name': 'TEST_TABLE', 'search_column': 'LOT_ID', 'return_columns': ['LOT_ID'], 'search_values': ['LOT001'], 'query_type': 'invalid_type' } ) assert response.status_code == 400 data = json.loads(response.data) assert 'invalid' in data['error'].lower() or '無效' in data['error'] def test_invalid_date_format(self, client): """Should reject invalid date format.""" response = client.post( '/api/excel-query/execute-advanced', json={ 'table_name': 'TEST_TABLE', 'search_column': 'LOT_ID', 'return_columns': ['LOT_ID'], 'search_values': ['LOT001'], 'date_from': '01-01-2024', 'date_to': '12-31-2024' } ) assert response.status_code == 400 data = json.loads(response.data) assert '格式' in data['error'] or 'format' in data['error'].lower() def test_date_range_reversed(self, client): """Should reject if start date > end date.""" response = client.post( '/api/excel-query/execute-advanced', json={ 'table_name': 'TEST_TABLE', 'search_column': 'LOT_ID', 'return_columns': ['LOT_ID'], 'search_values': ['LOT001'], 'date_from': '2024-12-31', 'date_to': '2024-01-01' } ) assert response.status_code == 400 data = json.loads(response.data) assert '起始' in data['error'] or 'start' in data['error'].lower() def test_date_range_exceeds_limit(self, client): """Should reject date range > 365 days.""" response = client.post( '/api/excel-query/execute-advanced', json={ 'table_name': 'TEST_TABLE', 'search_column': 'LOT_ID', 'return_columns': ['LOT_ID'], 'search_values': ['LOT001'], 'date_from': '2023-01-01', 'date_to': '2024-12-31' } ) assert response.status_code == 400 data = json.loads(response.data) assert '365' in data['error'] @patch('mes_dashboard.routes.excel_query_routes.execute_advanced_batch_query') def test_execute_in_query(self, mock_execute, client): """Should execute IN query successfully.""" mock_execute.return_value = { 'columns': ['LOT_ID', 'PRODUCT'], 'data': [['LOT001', 'PROD_A']], 'total': 1 } response = client.post( '/api/excel-query/execute-advanced', json={ 'table_name': 'TEST_TABLE', 'search_column': 'LOT_ID', 'return_columns': ['LOT_ID', 'PRODUCT'], 'search_values': ['LOT001'], 'query_type': 'in' } ) assert response.status_code == 200 data = json.loads(response.data) assert data['total'] == 1 @patch('mes_dashboard.routes.excel_query_routes.execute_advanced_batch_query') def test_execute_like_contains(self, mock_execute, client): """Should execute LIKE contains query.""" mock_execute.return_value = { 'columns': ['LOT_ID'], 'data': [['LOT001'], ['LOT002']], 'total': 2 } response = client.post( '/api/excel-query/execute-advanced', json={ 'table_name': 'TEST_TABLE', 'search_column': 'LOT_ID', 'return_columns': ['LOT_ID'], 'search_values': ['LOT'], 'query_type': 'like_contains' } ) assert response.status_code == 200 data = json.loads(response.data) assert data['total'] == 2 @patch('mes_dashboard.routes.excel_query_routes.execute_advanced_batch_query') def test_execute_with_date_range(self, mock_execute, client): """Should execute query with date range.""" mock_execute.return_value = { 'columns': ['LOT_ID', 'TXNDATE'], 'data': [['LOT001', '2024-01-15']], 'total': 1 } response = client.post( '/api/excel-query/execute-advanced', json={ 'table_name': 'TEST_TABLE', 'search_column': 'LOT_ID', 'return_columns': ['LOT_ID', 'TXNDATE'], 'search_values': ['LOT001'], 'query_type': 'in', 'date_column': 'TXNDATE', 'date_from': '2024-01-01', 'date_to': '2024-01-31' } ) assert response.status_code == 200 mock_execute.assert_called_once() call_kwargs = mock_execute.call_args[1] assert call_kwargs['date_column'] == 'TXNDATE' assert call_kwargs['date_from'] == '2024-01-01' assert call_kwargs['date_to'] == '2024-01-31' class TestExecuteQuery: """Tests for /api/excel-query/execute endpoint (backward compatibility).""" def test_missing_parameters(self, client): """Should return error for missing parameters.""" response = client.post( '/api/excel-query/execute', json={'table_name': 'TEST'} ) assert response.status_code == 400 @patch('mes_dashboard.routes.excel_query_routes.execute_batch_query') def test_execute_success(self, mock_execute, client): """Should execute basic query successfully.""" mock_execute.return_value = { 'columns': ['LOT_ID'], 'data': [['LOT001']], 'total': 1 } response = client.post( '/api/excel-query/execute', json={ 'table_name': 'TEST_TABLE', 'search_column': 'LOT_ID', 'return_columns': ['LOT_ID'], 'search_values': ['LOT001'] } ) assert response.status_code == 200 data = json.loads(response.data) assert data['total'] == 1 class TestExportCSV: """Tests for /api/excel-query/export-csv endpoint.""" def test_missing_parameters(self, client): """Should return error for missing parameters.""" response = client.post( '/api/excel-query/export-csv', json={} ) assert response.status_code == 400 @patch('mes_dashboard.routes.excel_query_routes.execute_batch_query') @patch('mes_dashboard.routes.excel_query_routes.generate_csv_content') def test_export_success(self, mock_csv, mock_execute, client): """Should export CSV successfully.""" mock_execute.return_value = { 'columns': ['LOT_ID', 'PRODUCT'], 'data': [['LOT001', 'PROD_A']], 'total': 1 } mock_csv.return_value = 'LOT_ID,PRODUCT\nLOT001,PROD_A\n' response = client.post( '/api/excel-query/export-csv', json={ 'table_name': 'TEST_TABLE', 'search_column': 'LOT_ID', 'return_columns': ['LOT_ID', 'PRODUCT'], 'search_values': ['LOT001'] } ) assert response.status_code == 200 assert response.content_type.startswith('text/csv') assert b'LOT_ID' in response.data class TestGetExcelColumnType: """Tests for /api/excel-query/column-type endpoint.""" def test_no_column_name(self, client): """Should return error without column name.""" response = client.post( '/api/excel-query/column-type', json={} ) assert response.status_code == 400 def test_no_excel_uploaded(self, client): """Should return error if no Excel uploaded.""" from mes_dashboard.routes.excel_query_routes import _uploaded_excel_cache _uploaded_excel_cache.clear() response = client.post( '/api/excel-query/column-type', json={'column_name': 'LOT_ID'} ) assert response.status_code == 400 def test_detect_type_after_upload(self, client, mock_excel_file): """Should detect column type after upload.""" # Upload first client.post( '/api/excel-query/upload', data={'file': (mock_excel_file, 'test.xlsx')}, content_type='multipart/form-data' ) # Then detect type response = client.post( '/api/excel-query/column-type', json={'column_name': 'LOT_ID'} ) assert response.status_code == 200 data = json.loads(response.data) assert 'detected_type' in data assert 'type_label' in data