import pytest import pandas as pd from io import BytesIO from unittest.mock import patch from mail_order_bot.parsers.excel_parcer import ExcelFileParcer class TestExcelFileParcer: """Тесты для класса ExcelFileParcer""" @pytest.fixture def sample_config(self): """Базовая конфигурация для тестов""" return { "sheet_name": 0, "key_field": "Артикул", "mapping": { "article": "Артикул", "manufacturer": "Производитель", "name": "Наименование", "price": "Цена", "quantity": "Количество" } } @pytest.fixture def sample_excel_bytes(self): """Создает тестовый Excel файл в виде байтов""" df = pd.DataFrame({ 'Артикул': ['ART001', 'ART002', 'ART003'], 'Производитель': ['MAN001', 'MAN002', 'MAN003'], 'Наименование': ['Товар 1', 'Товар 2', 'Товар 3'], 'Цена': [100.0, 200.0, 300.0], 'Количество': [1, 2, 3] }) buf = BytesIO() with pd.ExcelWriter(buf, engine='xlsxwriter') as writer: df.to_excel(writer, sheet_name='Sheet1', index=False) buf.seek(0) return buf.getvalue() @pytest.fixture def excel_with_header_row(self): """Создает Excel файл с заголовком не в первой строке""" df = pd.DataFrame([ ['Заголовок документа', None, None, None, None], ['Артикул', 'Производитель', 'Наименование', 'Цена', 'Количество'], ['ART001', 'MAN001', 'Товар 1', 100.0, 1], ['ART002', 'MAN002', 'Товар 2', 200.0, 2], ['ART003', 'MAN003', 'Товар 3', 300.0, 3], [None, None, None, None, None] # Пустая строка для обрезки ]) buf = BytesIO() with pd.ExcelWriter(buf, engine='xlsxwriter') as writer: df.to_excel(writer, sheet_name='Sheet1', index=False, header=False) buf.seek(0) return buf.getvalue() def test_init_with_valid_file(self, sample_excel_bytes, sample_config): """Тест инициализации с валидным файлом""" parser = ExcelFileParcer(sample_excel_bytes, sample_config) assert parser.config == sample_config assert parser.bytes == sample_excel_bytes assert parser.sheet_name == 0 assert parser.df is not None assert isinstance(parser.df, pd.DataFrame) def test_init_with_custom_sheet_name(self, sample_excel_bytes): """Тест инициализации с кастомным именем листа""" config = { "sheet_name": "Sheet2", "key_field": "Артикул", "mapping": { "article": "Артикул", "manufacturer": "Производитель" } } parser = ExcelFileParcer(sample_excel_bytes, config) assert parser.sheet_name == "Sheet2" def test_init_with_default_sheet_name(self, sample_excel_bytes): """Тест инициализации с дефолтным именем листа""" config = { "key_field": "Артикул", "mapping": { "article": "Артикул", "manufacturer": "Производитель" } } parser = ExcelFileParcer(sample_excel_bytes, config) assert parser.sheet_name == 0 @patch('mail_order_bot.parsers.excel_parcer.pd.read_excel') def test_init_with_invalid_file(self, mock_read_excel, sample_config): """Тест инициализации с невалидным файлом""" mock_read_excel.side_effect = Exception("Ошибка парсинга") invalid_bytes = b"invalid excel content" parser = ExcelFileParcer(invalid_bytes, sample_config) assert parser.df is None def test_parse_file_success(self, sample_excel_bytes, sample_config): """Тест успешного парсинга файла""" parser = ExcelFileParcer(sample_excel_bytes, sample_config) assert parser.df is not None assert len(parser.df) > 0 @patch('mail_order_bot.parsers.excel_parcer.pd.read_excel') def test_parse_file_failure(self, mock_read_excel, sample_config): """Тест обработки ошибки при парсинге файла""" mock_read_excel.side_effect = Exception("Ошибка чтения") invalid_bytes = b"invalid" parser = ExcelFileParcer(invalid_bytes, sample_config) assert parser.df is None def test_get_header_row(self, excel_with_header_row, sample_config): """Тест поиска строки заголовка""" parser = ExcelFileParcer(excel_with_header_row, sample_config) header_row = parser._get_header_row() assert header_row == 1 # Заголовок во второй строке (индекс 1) def test_get_attr_column(self, excel_with_header_row, sample_config): """Тест поиска индекса колонки по имени""" parser = ExcelFileParcer(excel_with_header_row, sample_config) col_idx = parser._get_attr_column("Артикул") assert isinstance(col_idx, int) assert col_idx >= 0 def test_get_attr_column_nonexistent(self, excel_with_header_row, sample_config): """Тест поиска несуществующей колонки""" parser = ExcelFileParcer(excel_with_header_row, sample_config) with pytest.raises((IndexError, KeyError)): parser._get_attr_column("Несуществующая колонка") def test_get_attr_row(self, excel_with_header_row, sample_config): """Тест поиска строки по артикулу и производителю""" parser = ExcelFileParcer(excel_with_header_row, sample_config) row_idx = parser._get_attr_row("ART001", "MAN001") assert isinstance(row_idx, (int, pd.core.indexes.numeric.Int64Index)) # Проверяем, что индекс найден assert row_idx is not None def test_get_attr_row_nonexistent(self, excel_with_header_row, sample_config): """Тест поиска несуществующей строки""" parser = ExcelFileParcer(excel_with_header_row, sample_config) with pytest.raises((IndexError, KeyError)): parser._get_attr_row("NONEXISTENT", "NONEXISTENT") def test_set_value(self, excel_with_header_row, sample_config): """Тест установки значения в ячейку""" parser = ExcelFileParcer(excel_with_header_row, sample_config) # Получаем исходное значение original_value = parser.df.iloc[2, 3] # Строка с ART001, колонка "Цена" # Устанавливаем новое значение parser.set_value("ART001", "MAN001", "Цена", 999.0) # Проверяем, что значение изменилось new_value = parser.df.iloc[2, 3] assert new_value == 999.0 assert new_value != original_value def test_get_file_bytes(self, sample_excel_bytes, sample_config): """Тест получения файла в виде байтов""" parser = ExcelFileParcer(sample_excel_bytes, sample_config) result_bytes = parser.get_file_bytes() assert result_bytes is not None assert hasattr(result_bytes, 'read') assert hasattr(result_bytes, 'seek') # Проверяем, что можно прочитать байты result_bytes.seek(0) content = result_bytes.read() assert len(content) > 0 def test_get_file_bytes_creates_valid_excel(self, sample_excel_bytes, sample_config): """Тест что get_file_bytes создает валидный Excel файл""" parser = ExcelFileParcer(sample_excel_bytes, sample_config) result_bytes = parser.get_file_bytes() # Пытаемся прочитать созданный файл result_bytes.seek(0) df = pd.read_excel(result_bytes, sheet_name=0, header=None) assert df is not None assert len(df) > 0 def test_get_order_rows(self, excel_with_header_row, sample_config): """Тест получения строк заказа""" parser = ExcelFileParcer(excel_with_header_row, sample_config) order_rows = parser.get_order_rows() assert order_rows is not None assert isinstance(order_rows, pd.DataFrame) assert len(order_rows) > 0 # Проверяем, что пустая строка обрезана assert len(order_rows) == 3 # Только строки с данными def test_get_order_rows_with_empty_file(self, sample_config): """Тест получения строк заказа из пустого файла""" # Создаем пустой DataFrame df = pd.DataFrame([['Артикул', 'Производитель'], [None, None]]) buf = BytesIO() with pd.ExcelWriter(buf, engine='xlsxwriter') as writer: df.to_excel(writer, sheet_name='Sheet1', index=False, header=False) buf.seek(0) empty_bytes = buf.getvalue() parser = ExcelFileParcer(empty_bytes, sample_config) # Должен вернуть пустой DataFrame или вызвать ошибку try: order_rows = parser.get_order_rows() assert len(order_rows) == 0 except (IndexError, KeyError): # Ожидаемое поведение при отсутствии данных pass def test_set_value_updates_dataframe(self, excel_with_header_row, sample_config): """Тест что set_value обновляет DataFrame""" parser = ExcelFileParcer(excel_with_header_row, sample_config) # Находим строку с ART002 row_idx = parser._get_attr_row("ART002", "MAN002") price_col_idx = parser._get_attr_column("Цена") original_price = parser.df.iloc[row_idx, price_col_idx] # Устанавливаем новое значение parser.set_value("ART002", "MAN002", "Цена", 555.0) # Проверяем обновление updated_price = parser.df.iloc[row_idx, price_col_idx] assert updated_price == 555.0 assert updated_price != original_price def test_multiple_set_value_operations(self, excel_with_header_row, sample_config): """Тест множественных операций set_value""" parser = ExcelFileParcer(excel_with_header_row, sample_config) # Устанавливаем несколько значений parser.set_value("ART001", "MAN001", "Цена", 111.0) parser.set_value("ART002", "MAN002", "Цена", 222.0) parser.set_value("ART003", "MAN003", "Цена", 333.0) # Проверяем все значения price_col_idx = parser._get_attr_column("Цена") row1_idx = parser._get_attr_row("ART001", "MAN001") row2_idx = parser._get_attr_row("ART002", "MAN002") row3_idx = parser._get_attr_row("ART003", "MAN003") assert parser.df.iloc[row1_idx, price_col_idx] == 111.0 assert parser.df.iloc[row2_idx, price_col_idx] == 222.0 assert parser.df.iloc[row3_idx, price_col_idx] == 333.0 def test_get_order_rows_trimmed_correctly(self, sample_config): """Тест что get_order_rows правильно обрезает пустые строки""" # Создаем файл с пустой строкой в середине df = pd.DataFrame([ ['Артикул', 'Производитель', 'Наименование'], ['ART001', 'MAN001', 'Товар 1'], ['ART002', 'MAN002', 'Товар 2'], [None, None, None], # Пустая строка ['ART003', 'MAN003', 'Товар 3'], [None, None, None] # Еще одна пустая строка ]) buf = BytesIO() with pd.ExcelWriter(buf, engine='xlsxwriter') as writer: df.to_excel(writer, sheet_name='Sheet1', index=False, header=False) buf.seek(0) excel_bytes = buf.getvalue() parser = ExcelFileParcer(excel_bytes, sample_config) order_rows = parser.get_order_rows() # Должны остаться только строки до первой пустой assert len(order_rows) == 2 # ART001 и ART002 @patch('mail_order_bot.parsers.excel_parcer.pd.read_excel') def test_get_order_rows_with_calamine_engine(self, mock_read_excel, sample_config): """Тест что get_order_rows использует calamine engine""" # Создаем мок DataFrame mock_df = pd.DataFrame({ 'Артикул': ['ART001', 'ART002', None], 'Производитель': ['MAN001', 'MAN002', None] }) mock_read_excel.return_value = mock_df # Создаем парсер с моком для первого чтения df_init = pd.DataFrame([ ['Артикул', 'Производитель'], ['ART001', 'MAN001'], ['ART002', 'MAN002'], [None, None] ]) with patch('mail_order_bot.parsers.excel_parcer.pd.read_excel') as mock_init: mock_init.return_value = df_init parser = ExcelFileParcer(b"test", sample_config) # Тестируем get_order_rows with patch('mail_order_bot.parsers.excel_parcer.pd.read_excel') as mock_get: mock_get.return_value = mock_df result = parser.get_order_rows() # Проверяем, что был вызван read_excel с engine='calamine' mock_get.assert_called_once() call_kwargs = mock_get.call_args[1] assert call_kwargs.get('engine') == 'calamine'