chore: reinitialize project with vite architecture
This commit is contained in:
506
tests/test_excel_query_e2e.py
Normal file
506
tests/test_excel_query_e2e.py
Normal file
@@ -0,0 +1,506 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""End-to-end tests for Excel query workflow.
|
||||
|
||||
Tests the complete workflow from Excel upload to query execution and export.
|
||||
"""
|
||||
|
||||
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()
|
||||
|
||||
|
||||
def create_test_excel(data):
|
||||
"""Create a test Excel file with given data.
|
||||
|
||||
Args:
|
||||
data: List of lists where first list is headers.
|
||||
e.g. [['COL1', 'COL2'], ['val1', 'val2'], ...]
|
||||
"""
|
||||
import openpyxl
|
||||
wb = openpyxl.Workbook()
|
||||
ws = wb.active
|
||||
|
||||
for row_idx, row in enumerate(data, 1):
|
||||
for col_idx, value in enumerate(row, 1):
|
||||
ws.cell(row=row_idx, column=col_idx, value=value)
|
||||
|
||||
buffer = io.BytesIO()
|
||||
wb.save(buffer)
|
||||
buffer.seek(0)
|
||||
return buffer
|
||||
|
||||
|
||||
class TestBasicQueryWorkflow:
|
||||
"""E2E tests for basic query workflow."""
|
||||
|
||||
@patch('mes_dashboard.routes.excel_query_routes.execute_batch_query')
|
||||
def test_complete_basic_workflow(self, mock_execute, client):
|
||||
"""Test complete workflow: upload → get values → execute → export."""
|
||||
# Step 1: Upload Excel file
|
||||
excel_data = [
|
||||
['LOT_ID', 'PRODUCT', 'QTY'],
|
||||
['LOT001', 'PROD_A', 100],
|
||||
['LOT002', 'PROD_B', 200],
|
||||
['LOT003', 'PROD_A', 150],
|
||||
]
|
||||
excel_file = create_test_excel(excel_data)
|
||||
|
||||
upload_response = client.post(
|
||||
'/api/excel-query/upload',
|
||||
data={'file': (excel_file, 'batch_query.xlsx')},
|
||||
content_type='multipart/form-data'
|
||||
)
|
||||
assert upload_response.status_code == 200
|
||||
upload_data = json.loads(upload_response.data)
|
||||
assert 'columns' in upload_data
|
||||
assert 'LOT_ID' in upload_data['columns']
|
||||
assert 'preview' in upload_data
|
||||
|
||||
# Step 2: Get column values
|
||||
values_response = client.post(
|
||||
'/api/excel-query/column-values',
|
||||
json={'column_name': 'LOT_ID'}
|
||||
)
|
||||
assert values_response.status_code == 200
|
||||
values_data = json.loads(values_response.data)
|
||||
assert 'values' in values_data
|
||||
assert set(values_data['values']) == {'LOT001', 'LOT002', 'LOT003'}
|
||||
|
||||
# Step 3: Execute query
|
||||
mock_execute.return_value = {
|
||||
'columns': ['LOT_ID', 'SPEC', 'STATUS'],
|
||||
'data': [
|
||||
['LOT001', 'SPEC_001', 'ACTIVE'],
|
||||
['LOT002', 'SPEC_002', 'HOLD'],
|
||||
['LOT003', 'SPEC_001', 'ACTIVE'],
|
||||
],
|
||||
'total': 3
|
||||
}
|
||||
|
||||
execute_response = client.post(
|
||||
'/api/excel-query/execute',
|
||||
json={
|
||||
'table_name': 'DWH.DW_MES_WIP',
|
||||
'search_column': 'LOT_ID',
|
||||
'return_columns': ['LOT_ID', 'SPEC', 'STATUS'],
|
||||
'search_values': ['LOT001', 'LOT002', 'LOT003']
|
||||
}
|
||||
)
|
||||
assert execute_response.status_code == 200
|
||||
execute_data = json.loads(execute_response.data)
|
||||
assert execute_data['total'] == 3
|
||||
|
||||
|
||||
class TestAdvancedQueryWorkflow:
|
||||
"""E2E tests for advanced query workflow with date range and LIKE."""
|
||||
|
||||
@patch('mes_dashboard.routes.excel_query_routes.execute_advanced_batch_query')
|
||||
def test_like_contains_workflow(self, mock_execute, client):
|
||||
"""Test workflow with LIKE contains query."""
|
||||
# Upload Excel with search patterns
|
||||
excel_data = [
|
||||
['SEARCH_PATTERN'],
|
||||
['LOT'],
|
||||
['WIP'],
|
||||
]
|
||||
excel_file = create_test_excel(excel_data)
|
||||
|
||||
upload_response = client.post(
|
||||
'/api/excel-query/upload',
|
||||
data={'file': (excel_file, 'patterns.xlsx')},
|
||||
content_type='multipart/form-data'
|
||||
)
|
||||
assert upload_response.status_code == 200
|
||||
|
||||
# Get search values
|
||||
values_response = client.post(
|
||||
'/api/excel-query/column-values',
|
||||
json={'column_name': 'SEARCH_PATTERN'}
|
||||
)
|
||||
assert values_response.status_code == 200
|
||||
search_values = json.loads(values_response.data)['values']
|
||||
|
||||
# Execute LIKE contains query
|
||||
mock_execute.return_value = {
|
||||
'columns': ['LOT_ID', 'STATUS'],
|
||||
'data': [
|
||||
['LOT001', 'ACTIVE'],
|
||||
['LOT002', 'ACTIVE'],
|
||||
['WIP001', 'HOLD'],
|
||||
['WIP002', 'ACTIVE'],
|
||||
],
|
||||
'total': 4
|
||||
}
|
||||
|
||||
response = client.post(
|
||||
'/api/excel-query/execute-advanced',
|
||||
json={
|
||||
'table_name': 'DWH.DW_MES_WIP',
|
||||
'search_column': 'LOT_ID',
|
||||
'return_columns': ['LOT_ID', 'STATUS'],
|
||||
'search_values': search_values,
|
||||
'query_type': 'like_contains'
|
||||
}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = json.loads(response.data)
|
||||
assert data['total'] == 4
|
||||
|
||||
@patch('mes_dashboard.routes.excel_query_routes.execute_advanced_batch_query')
|
||||
def test_date_range_workflow(self, mock_execute, client):
|
||||
"""Test workflow with date range filter."""
|
||||
excel_data = [
|
||||
['LOT_ID'],
|
||||
['LOT001'],
|
||||
['LOT002'],
|
||||
]
|
||||
excel_file = create_test_excel(excel_data)
|
||||
|
||||
client.post(
|
||||
'/api/excel-query/upload',
|
||||
data={'file': (excel_file, 'lots.xlsx')},
|
||||
content_type='multipart/form-data'
|
||||
)
|
||||
|
||||
# Execute 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': 'DWH.DW_MES_WIP',
|
||||
'search_column': 'LOT_ID',
|
||||
'return_columns': ['LOT_ID', 'TXNDATE'],
|
||||
'search_values': ['LOT001', 'LOT002'],
|
||||
'query_type': 'in',
|
||||
'date_column': 'TXNDATE',
|
||||
'date_from': '2024-01-01',
|
||||
'date_to': '2024-01-31'
|
||||
}
|
||||
)
|
||||
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_combined_like_and_date_workflow(self, mock_execute, client):
|
||||
"""Test workflow combining LIKE and date range."""
|
||||
excel_data = [
|
||||
['PREFIX'],
|
||||
['LOT'],
|
||||
]
|
||||
excel_file = create_test_excel(excel_data)
|
||||
|
||||
client.post(
|
||||
'/api/excel-query/upload',
|
||||
data={'file': (excel_file, 'prefixes.xlsx')},
|
||||
content_type='multipart/form-data'
|
||||
)
|
||||
|
||||
# Execute with both LIKE prefix and date range
|
||||
mock_execute.return_value = {
|
||||
'columns': ['LOT_ID', 'TXNDATE', 'STATUS'],
|
||||
'data': [
|
||||
['LOT001', '2024-01-15', 'ACTIVE'],
|
||||
['LOT002', '2024-01-20', 'ACTIVE'],
|
||||
],
|
||||
'total': 2
|
||||
}
|
||||
|
||||
response = client.post(
|
||||
'/api/excel-query/execute-advanced',
|
||||
json={
|
||||
'table_name': 'DWH.DW_MES_WIP',
|
||||
'search_column': 'LOT_ID',
|
||||
'return_columns': ['LOT_ID', 'TXNDATE', 'STATUS'],
|
||||
'search_values': ['LOT'],
|
||||
'query_type': 'like_prefix',
|
||||
'date_column': 'TXNDATE',
|
||||
'date_from': '2024-01-01',
|
||||
'date_to': '2024-01-31'
|
||||
}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
class TestColumnTypeDetection:
|
||||
"""E2E tests for column type detection workflow."""
|
||||
|
||||
def test_detect_date_column(self, client):
|
||||
"""Test detecting date type from Excel column."""
|
||||
excel_data = [
|
||||
['DATE_COL'],
|
||||
['2024-01-01'],
|
||||
['2024-01-02'],
|
||||
['2024-01-03'],
|
||||
['2024-01-04'],
|
||||
]
|
||||
excel_file = create_test_excel(excel_data)
|
||||
|
||||
client.post(
|
||||
'/api/excel-query/upload',
|
||||
data={'file': (excel_file, 'dates.xlsx')},
|
||||
content_type='multipart/form-data'
|
||||
)
|
||||
|
||||
response = client.post(
|
||||
'/api/excel-query/column-type',
|
||||
json={'column_name': 'DATE_COL'}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = json.loads(response.data)
|
||||
assert data['detected_type'] == 'date'
|
||||
|
||||
def test_detect_number_column(self, client):
|
||||
"""Test detecting numeric type from Excel column."""
|
||||
excel_data = [
|
||||
['QTY'],
|
||||
['100'],
|
||||
['200'],
|
||||
['350.5'],
|
||||
['-50'],
|
||||
]
|
||||
excel_file = create_test_excel(excel_data)
|
||||
|
||||
client.post(
|
||||
'/api/excel-query/upload',
|
||||
data={'file': (excel_file, 'numbers.xlsx')},
|
||||
content_type='multipart/form-data'
|
||||
)
|
||||
|
||||
response = client.post(
|
||||
'/api/excel-query/column-type',
|
||||
json={'column_name': 'QTY'}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = json.loads(response.data)
|
||||
assert data['detected_type'] == 'number'
|
||||
|
||||
def test_detect_id_column(self, client):
|
||||
"""Test detecting ID type from Excel column."""
|
||||
excel_data = [
|
||||
['LOT_ID'],
|
||||
['LOT001'],
|
||||
['LOT002'],
|
||||
['WIP-2024-001'],
|
||||
['PROD_ABC'],
|
||||
]
|
||||
excel_file = create_test_excel(excel_data)
|
||||
|
||||
client.post(
|
||||
'/api/excel-query/upload',
|
||||
data={'file': (excel_file, 'ids.xlsx')},
|
||||
content_type='multipart/form-data'
|
||||
)
|
||||
|
||||
response = client.post(
|
||||
'/api/excel-query/column-type',
|
||||
json={'column_name': 'LOT_ID'}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = json.loads(response.data)
|
||||
assert data['detected_type'] == 'id'
|
||||
|
||||
|
||||
class TestTableMetadataWorkflow:
|
||||
"""E2E tests for table metadata retrieval workflow."""
|
||||
|
||||
@patch('mes_dashboard.routes.excel_query_routes.get_table_column_metadata')
|
||||
def test_metadata_with_type_matching(self, mock_metadata, client):
|
||||
"""Test workflow checking column type compatibility."""
|
||||
# Step 1: Upload Excel with ID column
|
||||
excel_data = [
|
||||
['LOT_ID'],
|
||||
['LOT001'],
|
||||
['LOT002'],
|
||||
]
|
||||
excel_file = create_test_excel(excel_data)
|
||||
|
||||
client.post(
|
||||
'/api/excel-query/upload',
|
||||
data={'file': (excel_file, 'lots.xlsx')},
|
||||
content_type='multipart/form-data'
|
||||
)
|
||||
|
||||
# Step 2: Get Excel column type
|
||||
excel_type_response = client.post(
|
||||
'/api/excel-query/column-type',
|
||||
json={'column_name': 'LOT_ID'}
|
||||
)
|
||||
excel_type = json.loads(excel_type_response.data)['detected_type']
|
||||
|
||||
# Step 3: Get table metadata
|
||||
mock_metadata.return_value = {
|
||||
'columns': [
|
||||
{'name': 'LOT_ID', 'data_type': 'VARCHAR2', 'is_date': False, 'is_number': False},
|
||||
{'name': 'QTY', 'data_type': 'NUMBER', 'is_date': False, 'is_number': True},
|
||||
{'name': 'TXNDATE', 'data_type': 'DATE', 'is_date': True, 'is_number': False},
|
||||
]
|
||||
}
|
||||
|
||||
metadata_response = client.post(
|
||||
'/api/excel-query/table-metadata',
|
||||
json={'table_name': 'DWH.DW_MES_WIP'}
|
||||
)
|
||||
assert metadata_response.status_code == 200
|
||||
metadata = json.loads(metadata_response.data)
|
||||
|
||||
# Verify column types are returned
|
||||
assert len(metadata['columns']) == 3
|
||||
lot_col = next(c for c in metadata['columns'] if c['name'] == 'LOT_ID')
|
||||
assert lot_col['data_type'] == 'VARCHAR2'
|
||||
|
||||
|
||||
class TestValidationWorkflow:
|
||||
"""E2E tests for input validation throughout workflow."""
|
||||
|
||||
def test_like_keyword_limit_enforcement(self, client):
|
||||
"""Test that LIKE queries enforce keyword limit."""
|
||||
from mes_dashboard.services.excel_query_service import LIKE_KEYWORD_LIMIT
|
||||
|
||||
# Create Excel with many values
|
||||
excel_data = [['VALUE']] + [[f'VAL{i}'] for i in range(LIKE_KEYWORD_LIMIT + 10)]
|
||||
excel_file = create_test_excel(excel_data)
|
||||
|
||||
client.post(
|
||||
'/api/excel-query/upload',
|
||||
data={'file': (excel_file, 'many_values.xlsx')},
|
||||
content_type='multipart/form-data'
|
||||
)
|
||||
|
||||
# Get all values
|
||||
values_response = client.post(
|
||||
'/api/excel-query/column-values',
|
||||
json={'column_name': 'VALUE'}
|
||||
)
|
||||
all_values = json.loads(values_response.data)['values']
|
||||
|
||||
# Attempt LIKE query with too many values
|
||||
response = client.post(
|
||||
'/api/excel-query/execute-advanced',
|
||||
json={
|
||||
'table_name': 'TEST_TABLE',
|
||||
'search_column': 'COL',
|
||||
'return_columns': ['COL'],
|
||||
'search_values': all_values,
|
||||
'query_type': 'like_contains'
|
||||
}
|
||||
)
|
||||
# This should either fail at validation or service layer
|
||||
# The exact behavior depends on implementation
|
||||
# At minimum, verify the request was processed
|
||||
assert response.status_code in [200, 400]
|
||||
|
||||
def test_date_range_boundary_validation(self, client):
|
||||
"""Test date range validation at boundaries."""
|
||||
excel_data = [
|
||||
['LOT_ID'],
|
||||
['LOT001'],
|
||||
]
|
||||
excel_file = create_test_excel(excel_data)
|
||||
|
||||
client.post(
|
||||
'/api/excel-query/upload',
|
||||
data={'file': (excel_file, 'lots.xlsx')},
|
||||
content_type='multipart/form-data'
|
||||
)
|
||||
|
||||
# Test exactly 365 days (should pass)
|
||||
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-01-01',
|
||||
'date_to': '2024-12-31' # 365 days (2024 is leap year, so 366)
|
||||
}
|
||||
)
|
||||
# 366 days in 2024, should fail
|
||||
assert response.status_code == 400
|
||||
|
||||
def test_empty_search_values_rejected(self, client):
|
||||
"""Test that empty search values are rejected."""
|
||||
response = client.post(
|
||||
'/api/excel-query/execute-advanced',
|
||||
json={
|
||||
'table_name': 'TEST_TABLE',
|
||||
'search_column': 'LOT_ID',
|
||||
'return_columns': ['LOT_ID'],
|
||||
'search_values': [],
|
||||
'query_type': 'in'
|
||||
}
|
||||
)
|
||||
assert response.status_code == 400
|
||||
|
||||
|
||||
class TestBackwardCompatibility:
|
||||
"""E2E tests ensuring backward compatibility with original API."""
|
||||
|
||||
@patch('mes_dashboard.routes.excel_query_routes.execute_batch_query')
|
||||
def test_original_execute_endpoint_works(self, mock_execute, client):
|
||||
"""Test that original /execute endpoint still works."""
|
||||
mock_execute.return_value = {
|
||||
'columns': ['LOT_ID'],
|
||||
'data': [['LOT001']],
|
||||
'total': 1
|
||||
}
|
||||
|
||||
# Use original endpoint without advanced features
|
||||
response = client.post(
|
||||
'/api/excel-query/execute',
|
||||
json={
|
||||
'table_name': 'DWH.DW_MES_WIP',
|
||||
'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
|
||||
|
||||
@patch('mes_dashboard.routes.excel_query_routes.execute_batch_query')
|
||||
@patch('mes_dashboard.routes.excel_query_routes.generate_csv_content')
|
||||
def test_csv_export_still_works(self, mock_csv, mock_execute, client):
|
||||
"""Test that CSV export still works with basic query."""
|
||||
mock_execute.return_value = {
|
||||
'columns': ['LOT_ID', 'STATUS'],
|
||||
'data': [['LOT001', 'ACTIVE']],
|
||||
'total': 1
|
||||
}
|
||||
mock_csv.return_value = 'LOT_ID,STATUS\nLOT001,ACTIVE\n'
|
||||
|
||||
response = client.post(
|
||||
'/api/excel-query/export-csv',
|
||||
json={
|
||||
'table_name': 'DWH.DW_MES_WIP',
|
||||
'search_column': 'LOT_ID',
|
||||
'return_columns': ['LOT_ID', 'STATUS'],
|
||||
'search_values': ['LOT001']
|
||||
}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.content_type.startswith('text/csv')
|
||||
Reference in New Issue
Block a user