chore: reinitialize project with vite architecture
This commit is contained in:
261
tests/test_excel_query_service.py
Normal file
261
tests/test_excel_query_service.py
Normal file
@@ -0,0 +1,261 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Unit tests for Excel query service functions.
|
||||
|
||||
Tests the core service functions without database dependencies.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from mes_dashboard.services.excel_query_service import (
|
||||
detect_excel_column_type,
|
||||
escape_like_pattern,
|
||||
build_like_condition,
|
||||
build_date_range_condition,
|
||||
validate_like_keywords,
|
||||
sanitize_column_name,
|
||||
validate_table_name,
|
||||
LIKE_KEYWORD_LIMIT,
|
||||
)
|
||||
|
||||
|
||||
class TestDetectExcelColumnType:
|
||||
"""Tests for detect_excel_column_type function."""
|
||||
|
||||
def test_empty_values_returns_text(self):
|
||||
"""Empty list should return text type."""
|
||||
result = detect_excel_column_type([])
|
||||
assert result['detected_type'] == 'text'
|
||||
assert result['type_label'] == '文字'
|
||||
|
||||
def test_detect_date_type(self):
|
||||
"""Should detect date format YYYY-MM-DD."""
|
||||
values = ['2024-01-15', '2024-02-20', '2024-03-25', '2024-04-30']
|
||||
result = detect_excel_column_type(values)
|
||||
assert result['detected_type'] == 'date'
|
||||
assert result['type_label'] == '日期'
|
||||
|
||||
def test_detect_date_with_slash(self):
|
||||
"""Should detect date format YYYY/MM/DD."""
|
||||
values = ['2024/01/15', '2024/02/20', '2024/03/25', '2024/04/30']
|
||||
result = detect_excel_column_type(values)
|
||||
assert result['detected_type'] == 'date'
|
||||
assert result['type_label'] == '日期'
|
||||
|
||||
def test_detect_datetime_type(self):
|
||||
"""Should detect datetime format."""
|
||||
values = [
|
||||
'2024-01-15 10:30:00',
|
||||
'2024-02-20 14:45:30',
|
||||
'2024-03-25T08:00:00',
|
||||
'2024-04-30 23:59:59'
|
||||
]
|
||||
result = detect_excel_column_type(values)
|
||||
assert result['detected_type'] == 'datetime'
|
||||
assert result['type_label'] == '日期時間'
|
||||
|
||||
def test_detect_number_type(self):
|
||||
"""Should detect numeric values."""
|
||||
values = ['123', '456.78', '-99', '0', '1000000']
|
||||
result = detect_excel_column_type(values)
|
||||
assert result['detected_type'] == 'number'
|
||||
assert result['type_label'] == '數值'
|
||||
|
||||
def test_detect_id_type(self):
|
||||
"""Should detect ID pattern (uppercase alphanumeric)."""
|
||||
values = ['LOT001', 'WIP-2024-001', 'ABC_123', 'PROD001', 'TEST_ID']
|
||||
result = detect_excel_column_type(values)
|
||||
assert result['detected_type'] == 'id'
|
||||
assert result['type_label'] == '識別碼'
|
||||
|
||||
def test_mixed_values_returns_text(self):
|
||||
"""Mixed values should return text type."""
|
||||
values = ['abc', '123', '2024-01-01', 'xyz', 'test']
|
||||
result = detect_excel_column_type(values)
|
||||
assert result['detected_type'] == 'text'
|
||||
assert result['type_label'] == '文字'
|
||||
|
||||
def test_sample_values_included(self):
|
||||
"""Should include sample values in result."""
|
||||
values = ['A', 'B', 'C', 'D', 'E', 'F']
|
||||
result = detect_excel_column_type(values)
|
||||
assert 'sample_values' in result
|
||||
assert len(result['sample_values']) <= 5
|
||||
|
||||
|
||||
class TestEscapeLikePattern:
|
||||
"""Tests for escape_like_pattern function."""
|
||||
|
||||
def test_escape_percent(self):
|
||||
"""Should escape percent sign."""
|
||||
assert escape_like_pattern('100%') == '100\\%'
|
||||
|
||||
def test_escape_underscore(self):
|
||||
"""Should escape underscore."""
|
||||
assert escape_like_pattern('test_value') == 'test\\_value'
|
||||
|
||||
def test_escape_backslash(self):
|
||||
"""Should escape backslash."""
|
||||
assert escape_like_pattern('path\\file') == 'path\\\\file'
|
||||
|
||||
def test_escape_multiple_specials(self):
|
||||
"""Should escape multiple special characters."""
|
||||
assert escape_like_pattern('50%_off') == '50\\%\\_off'
|
||||
|
||||
def test_no_escape_needed(self):
|
||||
"""Should return unchanged if no special chars."""
|
||||
assert escape_like_pattern('normalvalue') == 'normalvalue'
|
||||
|
||||
|
||||
class TestBuildLikeCondition:
|
||||
"""Tests for build_like_condition function."""
|
||||
|
||||
def test_contains_mode(self):
|
||||
"""Should build LIKE %...% pattern."""
|
||||
condition, params = build_like_condition('COL', ['abc'], 'contains')
|
||||
assert 'LIKE :like_0' in condition
|
||||
assert params['like_0'] == '%abc%'
|
||||
|
||||
def test_prefix_mode(self):
|
||||
"""Should build LIKE ...% pattern."""
|
||||
condition, params = build_like_condition('COL', ['abc'], 'prefix')
|
||||
assert 'LIKE :like_0' in condition
|
||||
assert params['like_0'] == 'abc%'
|
||||
|
||||
def test_suffix_mode(self):
|
||||
"""Should build LIKE %... pattern."""
|
||||
condition, params = build_like_condition('COL', ['abc'], 'suffix')
|
||||
assert 'LIKE :like_0' in condition
|
||||
assert params['like_0'] == '%abc'
|
||||
|
||||
def test_multiple_values(self):
|
||||
"""Should build OR conditions for multiple values."""
|
||||
condition, params = build_like_condition('COL', ['a', 'b', 'c'], 'contains')
|
||||
assert 'OR' in condition
|
||||
assert len(params) == 3
|
||||
assert params['like_0'] == '%a%'
|
||||
assert params['like_1'] == '%b%'
|
||||
assert params['like_2'] == '%c%'
|
||||
|
||||
def test_empty_values(self):
|
||||
"""Should return empty for empty values."""
|
||||
condition, params = build_like_condition('COL', [], 'contains')
|
||||
assert condition == ''
|
||||
assert params == {}
|
||||
|
||||
def test_escape_clause_included(self):
|
||||
"""Should include ESCAPE clause."""
|
||||
condition, params = build_like_condition('COL', ['test'], 'contains')
|
||||
assert "ESCAPE '\\')" in condition
|
||||
|
||||
|
||||
class TestBuildDateRangeCondition:
|
||||
"""Tests for build_date_range_condition function."""
|
||||
|
||||
def test_both_dates(self):
|
||||
"""Should build condition with both dates."""
|
||||
condition, params = build_date_range_condition(
|
||||
'TXNDATE', '2024-01-01', '2024-12-31'
|
||||
)
|
||||
assert 'TO_DATE(:date_from' in condition
|
||||
assert 'TO_DATE(:date_to' in condition
|
||||
assert params['date_from'] == '2024-01-01'
|
||||
assert params['date_to'] == '2024-12-31'
|
||||
|
||||
def test_only_from_date(self):
|
||||
"""Should build condition with only start date."""
|
||||
condition, params = build_date_range_condition(
|
||||
'TXNDATE', date_from='2024-01-01'
|
||||
)
|
||||
assert '>=' in condition
|
||||
assert 'date_from' in params
|
||||
assert 'date_to' not in params
|
||||
|
||||
def test_only_to_date(self):
|
||||
"""Should build condition with only end date."""
|
||||
condition, params = build_date_range_condition(
|
||||
'TXNDATE', date_to='2024-12-31'
|
||||
)
|
||||
assert '<' in condition
|
||||
assert 'date_to' in params
|
||||
assert 'date_from' not in params
|
||||
|
||||
def test_no_dates(self):
|
||||
"""Should return empty for no dates."""
|
||||
condition, params = build_date_range_condition('TXNDATE')
|
||||
assert condition == ''
|
||||
assert params == {}
|
||||
|
||||
def test_end_date_includes_full_day(self):
|
||||
"""End date condition should include +1 for full day."""
|
||||
condition, params = build_date_range_condition(
|
||||
'TXNDATE', date_to='2024-12-31'
|
||||
)
|
||||
assert '+ 1' in condition
|
||||
|
||||
|
||||
class TestValidateLikeKeywords:
|
||||
"""Tests for validate_like_keywords function."""
|
||||
|
||||
def test_within_limit(self):
|
||||
"""Should pass validation for values within limit."""
|
||||
values = ['a'] * 50
|
||||
result = validate_like_keywords(values)
|
||||
assert result['valid'] is True
|
||||
|
||||
def test_at_limit(self):
|
||||
"""Should pass validation at exact limit."""
|
||||
values = ['a'] * LIKE_KEYWORD_LIMIT
|
||||
result = validate_like_keywords(values)
|
||||
assert result['valid'] is True
|
||||
|
||||
def test_exceeds_limit(self):
|
||||
"""Should fail validation when exceeding limit."""
|
||||
values = ['a'] * (LIKE_KEYWORD_LIMIT + 1)
|
||||
result = validate_like_keywords(values)
|
||||
assert result['valid'] is False
|
||||
assert 'error' in result
|
||||
|
||||
|
||||
class TestSanitizeColumnName:
|
||||
"""Tests for sanitize_column_name function."""
|
||||
|
||||
def test_valid_name(self):
|
||||
"""Should keep valid column name."""
|
||||
assert sanitize_column_name('LOT_ID') == 'LOT_ID'
|
||||
|
||||
def test_removes_special_chars(self):
|
||||
"""Should remove special characters."""
|
||||
assert sanitize_column_name('LOT-ID') == 'LOTID'
|
||||
assert sanitize_column_name('LOT ID') == 'LOTID'
|
||||
|
||||
def test_allows_underscore(self):
|
||||
"""Should allow underscore."""
|
||||
assert sanitize_column_name('MY_COLUMN_NAME') == 'MY_COLUMN_NAME'
|
||||
|
||||
def test_prevents_sql_injection(self):
|
||||
"""Should prevent SQL injection attempts."""
|
||||
assert sanitize_column_name("COL; DROP TABLE--") == 'COLDROPTABLE'
|
||||
|
||||
|
||||
class TestValidateTableName:
|
||||
"""Tests for validate_table_name function."""
|
||||
|
||||
def test_simple_name(self):
|
||||
"""Should validate simple table name."""
|
||||
assert validate_table_name('MY_TABLE') is True
|
||||
|
||||
def test_schema_qualified(self):
|
||||
"""Should validate schema.table format."""
|
||||
assert validate_table_name('DWH.DW_MES_WIP') is True
|
||||
|
||||
def test_invalid_starts_with_number(self):
|
||||
"""Should reject names starting with number."""
|
||||
assert validate_table_name('123TABLE') is False
|
||||
|
||||
def test_invalid_special_chars(self):
|
||||
"""Should reject names with special characters."""
|
||||
assert validate_table_name('TABLE-NAME') is False
|
||||
assert validate_table_name('TABLE NAME') is False
|
||||
|
||||
def test_sql_injection_prevention(self):
|
||||
"""Should reject SQL injection attempts."""
|
||||
assert validate_table_name('TABLE; DROP--') is False
|
||||
Reference in New Issue
Block a user