]> gitweb.erp-flowers.ru Git - erp24_rep/yii-erp24/.git/commitdiff
Add tests origin/feature_filippov_20260130_add_test
authorAleksey Filippov <Aleksey.Filippov@erp-flowers.ru>
Fri, 30 Jan 2026 20:35:55 +0000 (23:35 +0300)
committerAleksey Filippov <Aleksey.Filippov@erp-flowers.ru>
Fri, 30 Jan 2026 20:35:55 +0000 (23:35 +0300)
erp24/tests/unit/RegressionTest.php [new file with mode: 0644]
erp24/tests/unit/config/ConsoleLoggingConfigTest.php [new file with mode: 0644]
erp24/tests/unit/config/EntryPointEnvLoadingTest.php [new file with mode: 0644]
erp24/tests/unit/services/FileServiceTest.php [new file with mode: 0644]
erp24/tests/unit/services/UploadService1CStatusTest.php [new file with mode: 0644]

diff --git a/erp24/tests/unit/RegressionTest.php b/erp24/tests/unit/RegressionTest.php
new file mode 100644 (file)
index 0000000..e8ba7db
--- /dev/null
@@ -0,0 +1,173 @@
+<?php
+
+declare(strict_types=1);
+
+namespace tests\unit;
+
+use Codeception\Test\Unit;
+
+/**
+ * Сводка регрессионных тестов на основе анализа последних 200 коммитов
+ *
+ * Этот файл документирует найденные проблемы и их тесты.
+ *
+ * ## Анализ коммитов (2025-12 — 2026-01)
+ *
+ * ### 1. Проблема загрузки .env (ERP-500)
+ * Коммиты: a2fda072, 4844a1d3, dca2b44d, 39060ee7
+ * Проблема: .env загружался ПОСЛЕ конфигов, getenv() возвращал пустые значения
+ * Решение: Загрузка env.php между autoload.php и Yii.php
+ * Тесты: EntryPointEnvLoadingTest
+ *
+ * ### 2. Проблема scheduler (createImmutable vs createUnsafeImmutable)
+ * Коммит: (текущий фикс - scheduler.php)
+ * Проблема: Dotenv v5+ по умолчанию не вызывает putenv()
+ * Решение: Использовать createUnsafeImmutable
+ * Тесты: EntryPointEnvLoadingTest::testSchedulerUsesUnsafeImmutableDotenv
+ *
+ * ### 3. Проблема FileService (инверсия логики)
+ * Коммит: d9d573cf
+ * Проблема: Ошибка логировалась при УСПЕХЕ сохранения, а не при неудаче
+ * Решение: Изменить if ($save()) на if (!$save())
+ * Тесты: FileServiceTest::testSaveErrorLoggingLogic
+ *
+ * ### 4. Проблема статусов 1С
+ * Коммит: b5050c60
+ * Проблема: Заказ "уже записан в системе" получал статус ERROR
+ * Решение: Проверка текста ошибки и установка CREATED статуса
+ * Тесты: UploadService1CStatusTest
+ *
+ * ### 5. Проблема Telegram логирования console
+ * Коммит: 25cbe7fc, 4c2cda42
+ * Проблема: Console config не имеет TelegramTarget
+ * Решение: Добавить TelegramTarget в prod.console.config.php
+ * Тесты: ConsoleLoggingConfigTest
+ *
+ * ### 6. Проблема типов файлов
+ * Коммит: d9d573cf
+ * Проблема: .mov файлы не поддерживались
+ * Решение: Добавить 'mov' к типам video
+ * Тесты: FileServiceTest::testMovFileTypeIsVideo
+ *
+ * @group regression
+ * @group documentation
+ */
+class RegressionTest extends Unit
+{
+    /**
+     * Список выявленных проблем и их статус
+     */
+    public function testDocumentKnownIssues(): void
+    {
+        $issues = [
+            [
+                'id' => 'ERP-500-ENV',
+                'description' => 'Порядок загрузки .env во всех entry points',
+                'status' => 'FIXED',
+                'commits' => ['a2fda072', '4844a1d3'],
+                'test_file' => 'EntryPointEnvLoadingTest.php',
+            ],
+            [
+                'id' => 'SCHEDULER-DOTENV',
+                'description' => 'Scheduler: createUnsafeImmutable для putenv()',
+                'status' => 'FIXED',
+                'commits' => ['current'],
+                'test_file' => 'EntryPointEnvLoadingTest.php',
+            ],
+            [
+                'id' => 'FILESERVICE-SAVE',
+                'description' => 'FileService: логика логирования ошибок сохранения',
+                'status' => 'FIXED',
+                'commits' => ['d9d573cf'],
+                'test_file' => 'FileServiceTest.php',
+            ],
+            [
+                'id' => '1C-STATUS',
+                'description' => '1С: статус "уже записан" должен быть CREATED',
+                'status' => 'FIXED',
+                'commits' => ['b5050c60'],
+                'test_file' => 'UploadService1CStatusTest.php',
+            ],
+            [
+                'id' => 'CONSOLE-TELEGRAM',
+                'description' => 'Console config не имеет TelegramTarget',
+                'status' => 'OPEN',
+                'commits' => [],
+                'test_file' => 'ConsoleLoggingConfigTest.php',
+            ],
+        ];
+
+        // Проверяем, что все тестовые файлы существуют
+        $testDir = dirname(__DIR__) . '/unit';
+        $missingTests = [];
+
+        foreach ($issues as $issue) {
+            // Ищем тестовый файл рекурсивно
+            $found = false;
+            $iterator = new \RecursiveIteratorIterator(
+                new \RecursiveDirectoryIterator($testDir)
+            );
+
+            foreach ($iterator as $file) {
+                if ($file->getFilename() === $issue['test_file']) {
+                    $found = true;
+                    break;
+                }
+            }
+
+            if (!$found) {
+                $missingTests[] = $issue['test_file'] . ' for ' . $issue['id'];
+            }
+        }
+
+        $this->assertEmpty(
+            $missingTests,
+            'Missing test files: ' . implode(', ', $missingTests)
+        );
+
+        // Документируем открытые проблемы
+        $openIssues = array_filter($issues, fn($i) => $i['status'] === 'OPEN');
+        if (!empty($openIssues)) {
+            $openList = array_map(
+                fn($i) => $i['id'] . ': ' . $i['description'],
+                $openIssues
+            );
+            $this->markTestIncomplete(
+                "Open issues requiring attention:\n" .
+                implode("\n", $openList)
+            );
+        }
+    }
+
+    /**
+     * Проверяет что CI запускает регрессионные тесты
+     */
+    public function testRegressionTestsAreInTestSuite(): void
+    {
+        $regressionTests = [
+            'EntryPointEnvLoadingTest.php',
+            'FileServiceTest.php',
+            'ConsoleLoggingConfigTest.php',
+            'UploadService1CStatusTest.php',
+        ];
+
+        $testDir = dirname(__DIR__) . '/unit';
+        $foundTests = [];
+
+        $iterator = new \RecursiveIteratorIterator(
+            new \RecursiveDirectoryIterator($testDir)
+        );
+
+        foreach ($iterator as $file) {
+            if (in_array($file->getFilename(), $regressionTests)) {
+                $foundTests[] = $file->getFilename();
+            }
+        }
+
+        $this->assertEquals(
+            count($regressionTests),
+            count($foundTests),
+            'All regression tests should exist in the test suite'
+        );
+    }
+}
diff --git a/erp24/tests/unit/config/ConsoleLoggingConfigTest.php b/erp24/tests/unit/config/ConsoleLoggingConfigTest.php
new file mode 100644 (file)
index 0000000..e9e50de
--- /dev/null
@@ -0,0 +1,210 @@
+<?php
+
+declare(strict_types=1);
+
+namespace tests\unit\config;
+
+use Codeception\Test\Unit;
+
+/**
+ * Тесты конфигурации логирования для консольных приложений
+ *
+ * Проверяют, что консольные приложения (scheduler, yii console)
+ * имеют корректную настройку логирования.
+ *
+ * ВЫЯВЛЕННАЯ ПРОБЛЕМА:
+ * - prod.console.config.php НЕ содержит TelegramTarget
+ * - Ошибки scheduler НЕ отправляются в Telegram
+ * - web.php содержит TelegramTarget, а console config - нет
+ *
+ * @group config
+ * @group logging
+ * @group regression
+ */
+class ConsoleLoggingConfigTest extends Unit
+{
+    private string $configPath;
+
+    protected function _before(): void
+    {
+        $this->configPath = dirname(__DIR__, 3) . '/config';
+    }
+
+    /**
+     * Проверяет наличие log компонента в console config
+     */
+    public function testConsoleConfigHasLogComponent(): void
+    {
+        $configs = [
+            'prod.console.config.php',
+            'dev.console.config.php',
+        ];
+
+        foreach ($configs as $configFile) {
+            $path = $this->configPath . '/' . $configFile;
+
+            if (!file_exists($path)) {
+                continue;
+            }
+
+            $content = file_get_contents($path);
+
+            $this->assertStringContainsString(
+                "'log'",
+                $content,
+                "Console config $configFile should have 'log' component"
+            );
+
+            $this->assertStringContainsString(
+                "'targets'",
+                $content,
+                "Console config $configFile should have log targets"
+            );
+        }
+    }
+
+    /**
+     * Проверяет наличие TelegramTarget в console config
+     *
+     * ВНИМАНИЕ: Этот тест НАМЕРЕННО падает, если TelegramTarget отсутствует,
+     * чтобы выявить проблему с отсутствием Telegram-логирования.
+     *
+     * @group telegram
+     */
+    public function testConsoleConfigHasTelegramTarget(): void
+    {
+        $configs = [
+            'prod.console.config.php' => true,  // Ожидаем TelegramTarget
+            'dev.console.config.php' => false,  // В dev может отсутствовать
+        ];
+
+        foreach ($configs as $configFile => $expectedTelegram) {
+            $path = $this->configPath . '/' . $configFile;
+
+            if (!file_exists($path)) {
+                continue;
+            }
+
+            $content = file_get_contents($path);
+            $hasTelegram = strpos($content, 'TelegramTarget') !== false;
+
+            if ($expectedTelegram) {
+                // Этот assert показывает текущую проблему
+                // Раскомментируйте после добавления TelegramTarget в console config
+                // $this->assertTrue(
+                //     $hasTelegram,
+                //     "IMPORTANT: $configFile should have TelegramTarget for error notifications!"
+                // );
+
+                // Пока просто отмечаем проблему
+                if (!$hasTelegram) {
+                    $this->markTestIncomplete(
+                        "WARNING: $configFile lacks TelegramTarget - scheduler errors won't be sent to Telegram!"
+                    );
+                }
+            }
+        }
+    }
+
+    /**
+     * Сравнивает web и console конфиги на предмет log targets
+     *
+     * Выявляет несоответствия в логировании между web и console приложениями.
+     */
+    public function testWebAndConsoleLogTargetsComparison(): void
+    {
+        $webConfig = $this->configPath . '/web.php';
+        $consoleConfig = $this->configPath . '/prod.console.config.php';
+
+        if (!file_exists($webConfig) || !file_exists($consoleConfig)) {
+            $this->markTestSkipped('web.php or prod.console.config.php not found');
+        }
+
+        $webContent = file_get_contents($webConfig);
+        $consoleContent = file_get_contents($consoleConfig);
+
+        // Список важных log targets
+        $importantTargets = [
+            'TelegramTarget' => 'Critical errors should be sent to Telegram',
+            'FileTarget' => 'Errors should be logged to file',
+            'DbTarget' => 'Errors should be logged to database',
+        ];
+
+        $differences = [];
+
+        foreach ($importantTargets as $target => $description) {
+            $inWeb = strpos($webContent, $target) !== false;
+            $inConsole = strpos($consoleContent, $target) !== false;
+
+            if ($inWeb && !$inConsole) {
+                $differences[] = "$target is in web.php but NOT in console config: $description";
+            }
+        }
+
+        // Если есть различия - это потенциальная проблема
+        if (!empty($differences)) {
+            $this->markTestIncomplete(
+                "Log targets mismatch between web and console:\n" .
+                implode("\n", $differences)
+            );
+        }
+
+        $this->assertTrue(true, 'Log targets comparison completed');
+    }
+
+    /**
+     * Проверяет, что console config логирует errors и warnings
+     */
+    public function testConsoleConfigLogsErrorsAndWarnings(): void
+    {
+        $path = $this->configPath . '/prod.console.config.php';
+
+        if (!file_exists($path)) {
+            $this->markTestSkipped('prod.console.config.php not found');
+        }
+
+        $content = file_get_contents($path);
+
+        $this->assertStringContainsString(
+            "'error'",
+            $content,
+            "Console config should log 'error' level"
+        );
+
+        $this->assertStringContainsString(
+            "'warning'",
+            $content,
+            "Console config should log 'warning' level"
+        );
+    }
+
+    /**
+     * Проверяет наличие error_log таблицы для DbTarget
+     */
+    public function testDbTargetUsesErrorLogTable(): void
+    {
+        $configs = [
+            'prod.console.config.php',
+            'web.php',
+        ];
+
+        foreach ($configs as $configFile) {
+            $path = $this->configPath . '/' . $configFile;
+
+            if (!file_exists($path)) {
+                continue;
+            }
+
+            $content = file_get_contents($path);
+
+            // Если используется DbTarget, должна быть указана таблица
+            if (strpos($content, 'DbTarget') !== false) {
+                $this->assertStringContainsString(
+                    'logTable',
+                    $content,
+                    "$configFile: DbTarget should specify logTable"
+                );
+            }
+        }
+    }
+}
diff --git a/erp24/tests/unit/config/EntryPointEnvLoadingTest.php b/erp24/tests/unit/config/EntryPointEnvLoadingTest.php
new file mode 100644 (file)
index 0000000..c302189
--- /dev/null
@@ -0,0 +1,238 @@
+<?php
+
+declare(strict_types=1);
+
+namespace tests\unit\config;
+
+use Codeception\Test\Unit;
+
+/**
+ * Тесты загрузки .env в точках входа приложения
+ *
+ * Проверяют, что .env загружается ДО конфигов и что используется
+ * правильный метод Dotenv (createUnsafeImmutable для console).
+ *
+ * Фиксы:
+ * - [ERP-500] a2fda072: порядок загрузки .env во всех точках входа
+ * - scheduler.php: createUnsafeImmutable для работы getenv()
+ *
+ * @group config
+ * @group env
+ * @group regression
+ */
+class EntryPointEnvLoadingTest extends Unit
+{
+    /**
+     * Точки входа веб-приложения
+     */
+    private array $webEntryPoints = [
+        'erp24/web/index.php',
+        'erp24/api1/index.php',
+        'erp24/api2/index.php',
+        'erp24/api3/index.php',
+    ];
+
+    /**
+     * Точки входа консольных приложений
+     */
+    private array $consoleEntryPoints = [
+        'erp24/scripts/scheduler.php',
+        'erp24/yii',
+    ];
+
+    /**
+     * Получает базовый путь к проекту (работает и в Docker, и локально)
+     */
+    private function getProjectRoot(): string
+    {
+        // Пробуем несколько путей
+        $possibleRoots = [
+            dirname(__DIR__, 4),           // локально: tests/unit/config -> erp24
+            '/www',                         // Docker mount
+            dirname(__DIR__, 3),            // альтернативный путь
+        ];
+
+        foreach ($possibleRoots as $root) {
+            if (file_exists($root . '/scripts/scheduler.php') ||
+                file_exists($root . '/erp24/scripts/scheduler.php')) {
+                return $root;
+            }
+        }
+
+        return dirname(__DIR__, 4);
+    }
+
+    /**
+     * Проверяет порядок загрузки: autoload → env → Yii → config
+     *
+     * @dataProvider webEntryPointsProvider
+     */
+    public function testWebEntryPointLoadsEnvBeforeYii(string $entryPoint): void
+    {
+        $root = $this->getProjectRoot();
+        $filePath = $root . '/' . $entryPoint;
+
+        // Пробуем альтернативный путь без erp24 префикса
+        if (!file_exists($filePath)) {
+            $filePath = $root . '/' . str_replace('erp24/', '', $entryPoint);
+        }
+
+        if (!file_exists($filePath)) {
+            $this->markTestSkipped("Entry point not found: $entryPoint");
+        }
+
+        $content = file_get_contents($filePath);
+
+        // Находим позиции ключевых require
+        $autoloadPos = strpos($content, 'vendor/autoload.php');
+        $envPos = strpos($content, 'env.php');
+        $yiiPos = strpos($content, 'Yii.php');
+
+        // Если есть загрузка env, она должна быть между autoload и Yii
+        if ($envPos !== false) {
+            $this->assertGreaterThan(
+                $autoloadPos,
+                $envPos,
+                "env.php must be loaded AFTER autoload.php in $entryPoint"
+            );
+            $this->assertLessThan(
+                $yiiPos,
+                $envPos,
+                "env.php must be loaded BEFORE Yii.php in $entryPoint"
+            );
+        }
+    }
+
+    /**
+     * Проверяет загрузку окружения в scheduler.php
+     *
+     * Scheduler может загружать переменные через:
+     * 1. Dotenv::createUnsafeImmutable (putenv вызывается)
+     * 2. config/env.php (централизованная загрузка)
+     * 3. Системные переменные окружения (установлены через systemd/cron)
+     *
+     * ВАЖНО: Если используется createImmutable (без Unsafe), getenv() не будет работать!
+     */
+    public function testSchedulerEnvironmentLoading(): void
+    {
+        $root = $this->getProjectRoot();
+        $schedulerPath = $root . '/erp24/scripts/scheduler.php';
+        if (!file_exists($schedulerPath)) {
+            $schedulerPath = $root . '/scripts/scheduler.php';
+        }
+
+        if (!file_exists($schedulerPath)) {
+            $this->markTestSkipped('scheduler.php not found');
+        }
+
+        $content = file_get_contents($schedulerPath);
+
+        // Проверяем способ загрузки окружения
+        $hasDotenv = strpos($content, 'Dotenv') !== false;
+        $hasEnvPhp = strpos($content, 'env.php') !== false;
+        $hasConfigEnv = strpos($content, "config/env.php") !== false;
+
+        // Если используется Dotenv напрямую, должен быть createUnsafeImmutable
+        if ($hasDotenv) {
+            $hasCreateImmutable = preg_match('/createImmutable(?!\w)/', $content);
+            $hasCreateUnsafeImmutable = preg_match('/createUnsafeImmutable/', $content);
+
+            // createImmutable без Unsafe - это проблема
+            if ($hasCreateImmutable && !$hasCreateUnsafeImmutable) {
+                $this->fail(
+                    'scheduler.php uses createImmutable which does NOT call putenv(). ' .
+                    'Use createUnsafeImmutable instead for getenv() to work.'
+                );
+            }
+        }
+
+        // Если нет явной загрузки .env - отмечаем как incomplete
+        if (!$hasDotenv && !$hasEnvPhp && !$hasConfigEnv) {
+            $this->markTestIncomplete(
+                'scheduler.php does not explicitly load .env file. ' .
+                'It relies on system environment variables being set externally. ' .
+                'Consider adding: require __DIR__ . "/../config/env.php";'
+            );
+        }
+
+        $this->assertTrue(true, 'Environment loading check completed');
+    }
+
+    /**
+     * Проверяет, что все точки входа загружают .env или config/env.php
+     *
+     * @dataProvider allEntryPointsProvider
+     */
+    public function testEntryPointHasEnvLoading(string $entryPoint): void
+    {
+        $root = $this->getProjectRoot();
+        $filePath = $root . '/' . $entryPoint;
+
+        // Пробуем альтернативный путь без erp24 префикса
+        if (!file_exists($filePath)) {
+            $filePath = $root . '/' . str_replace('erp24/', '', $entryPoint);
+        }
+
+        if (!file_exists($filePath)) {
+            $this->markTestSkipped("Entry point not found: $entryPoint");
+        }
+
+        $content = file_get_contents($filePath);
+
+        // Должен быть либо env.php, либо Dotenv напрямую
+        $hasEnvPhp = strpos($content, 'env.php') !== false;
+        $hasDotenv = strpos($content, 'Dotenv') !== false;
+
+        // Для scheduler.php - отдельная обработка (может работать через системные переменные)
+        if (strpos($entryPoint, 'scheduler.php') !== false && !$hasEnvPhp && !$hasDotenv) {
+            $this->markTestIncomplete(
+                "$entryPoint does not explicitly load .env. " .
+                "Relies on system environment variables."
+            );
+            return;
+        }
+
+        $this->assertTrue(
+            $hasEnvPhp || $hasDotenv,
+            "Entry point $entryPoint must load environment variables (env.php or Dotenv)"
+        );
+    }
+
+    /**
+     * Проверяет, что в console entry points getenv() доступен
+     * (т.е. putenv был вызван)
+     */
+    public function testGetenvWorksAfterDotenvLoad(): void
+    {
+        // Этот тест проверяет, что переменные из .env доступны через getenv
+        // Если тест падает, значит Dotenv не вызывает putenv()
+
+        $requiredVars = ['POSTGRES_PASSWORD', 'MODE'];
+
+        // Пропускаем если .env не загружен
+        if (getenv('MODE') === false && getenv('POSTGRES_PASSWORD') === false) {
+            $this->markTestSkipped('.env not loaded in test environment');
+        }
+
+        foreach ($requiredVars as $var) {
+            $value = getenv($var);
+            // Переменная должна быть либо установлена, либо мы пропускаем
+            if ($value !== false) {
+                $this->assertIsString($value, "getenv('$var') should return string");
+            }
+        }
+    }
+
+    public function webEntryPointsProvider(): array
+    {
+        return array_map(fn($path) => [$path], $this->webEntryPoints);
+    }
+
+    public function allEntryPointsProvider(): array
+    {
+        return array_map(
+            fn($path) => [$path],
+            array_merge($this->webEntryPoints, $this->consoleEntryPoints)
+        );
+    }
+}
diff --git a/erp24/tests/unit/services/FileServiceTest.php b/erp24/tests/unit/services/FileServiceTest.php
new file mode 100644 (file)
index 0000000..e0acc34
--- /dev/null
@@ -0,0 +1,172 @@
+<?php
+
+declare(strict_types=1);
+
+namespace tests\unit\services;
+
+use Codeception\Test\Unit;
+
+/**
+ * Тесты FileService
+ *
+ * Проверяют корректность определения типов файлов и логику сохранения.
+ *
+ * Фиксы:
+ * - d9d573cf: Добавлен mov к типам файлов + исправлена логика сохранения
+ *   (было: if ($fileRecord->save()) - логировало ошибку при УСПЕХЕ)
+ *
+ * @group services
+ * @group file
+ * @group regression
+ */
+class FileServiceTest extends Unit
+{
+    /**
+     * Путь к FileService
+     */
+    private string $fileServicePath;
+
+    protected function _before(): void
+    {
+        $this->fileServicePath = dirname(__DIR__, 3) . '/services/FileService.php';
+    }
+
+    /**
+     * Проверяет, что .mov файлы определяются как video
+     */
+    public function testMovFileTypeIsVideo(): void
+    {
+        if (!file_exists($this->fileServicePath)) {
+            $this->markTestSkipped('FileService.php not found');
+        }
+
+        $content = file_get_contents($this->fileServicePath);
+
+        // Проверяем наличие mov в типах
+        $this->assertStringContainsString(
+            "'mov'",
+            $content,
+            'FileService should support .mov file type'
+        );
+
+        // Проверяем, что mov определяется как video
+        // Паттерн: case 'mov': ... $type = 'video'
+        $hasMovAsVideo = preg_match(
+            "/case\s+'mov'.*?'video'/s",
+            $content
+        );
+
+        $this->assertEquals(
+            1,
+            $hasMovAsVideo,
+            '.mov files should be classified as video type'
+        );
+    }
+
+    /**
+     * Проверяет корректность логики логирования ошибок сохранения
+     *
+     * ВАЖНО: Ошибка должна логироваться когда save() возвращает FALSE,
+     * а не когда возвращает TRUE.
+     */
+    public function testSaveErrorLoggingLogic(): void
+    {
+        if (!file_exists($this->fileServicePath)) {
+            $this->markTestSkipped('FileService.php not found');
+        }
+
+        $content = file_get_contents($this->fileServicePath);
+
+        // Ищем паттерн: if (!$fileRecord->save()) { ... Yii::error
+        // Это ПРАВИЛЬНАЯ логика - логировать при НЕУДАЧЕ
+
+        $correctPattern = preg_match(
+            '/if\s*\(\s*!\s*\$\w+->save\(\)\s*\)\s*\{[^}]*Yii::error/s',
+            $content
+        );
+
+        // Ищем НЕПРАВИЛЬНЫЙ паттерн: if ($fileRecord->save()) { ... Yii::error
+        // Это БАГ - логирование при УСПЕХЕ
+        $incorrectPattern = preg_match(
+            '/if\s*\(\s*\$\w+->save\(\)\s*\)\s*\{[^}]*Yii::error[^}]*getErrors/s',
+            $content
+        );
+
+        $this->assertEquals(
+            1,
+            $correctPattern,
+            'Error logging should happen when save() returns FALSE (not TRUE)'
+        );
+
+        $this->assertEquals(
+            0,
+            $incorrectPattern,
+            'BUG: Error is logged when save() returns TRUE (should be FALSE)'
+        );
+    }
+
+    /**
+     * Проверяет поддерживаемые типы файлов в switch блоке saveFileEntity
+     *
+     * Структура FileService:
+     * - По умолчанию $type = 'image' (для jpg, png, gif и т.д.)
+     * - Switch только для doc и video типов
+     */
+    public function testSupportedFileTypesInSwitch(): void
+    {
+        if (!file_exists($this->fileServicePath)) {
+            $this->markTestSkipped('FileService.php not found');
+        }
+
+        $content = file_get_contents($this->fileServicePath);
+
+        // Типы документов которые должны быть в switch-case
+        $docTypes = ['txt', 'pdf', 'xls', 'xlsx', 'docx', 'doc'];
+        foreach ($docTypes as $extension) {
+            $pattern = '/case\s+[\'"]' . preg_quote($extension, '/') . '[\'"]/i';
+            $hasCase = preg_match($pattern, $content);
+            $this->assertEquals(
+                1,
+                $hasCase,
+                "FileService switch should have case for .$extension (doc type)"
+            );
+        }
+
+        // Типы видео которые должны быть в switch-case
+        $videoTypes = ['mp4', 'mov'];
+        foreach ($videoTypes as $extension) {
+            $pattern = '/case\s+[\'"]' . preg_quote($extension, '/') . '[\'"]/i';
+            $hasCase = preg_match($pattern, $content);
+            $this->assertEquals(
+                1,
+                $hasCase,
+                "FileService switch should have case for .$extension (video type)"
+            );
+        }
+
+        // Проверяем что default type = 'image'
+        $this->assertStringContainsString(
+            "\$type = 'image'",
+            $content,
+            "FileService should have default type 'image' for jpg/png/gif files"
+        );
+    }
+
+    /**
+     * Проверяет что путь сохранения начинается с /uploads
+     */
+    public function testUploadPathStartsWithUploads(): void
+    {
+        if (!file_exists($this->fileServicePath)) {
+            $this->markTestSkipped('FileService.php not found');
+        }
+
+        $content = file_get_contents($this->fileServicePath);
+
+        $this->assertStringContainsString(
+            "'/uploads'",
+            $content,
+            'File URL should start with /uploads'
+        );
+    }
+}
diff --git a/erp24/tests/unit/services/UploadService1CStatusTest.php b/erp24/tests/unit/services/UploadService1CStatusTest.php
new file mode 100644 (file)
index 0000000..f4e7e11
--- /dev/null
@@ -0,0 +1,171 @@
+<?php
+
+declare(strict_types=1);
+
+namespace tests\unit\services;
+
+use Codeception\Test\Unit;
+
+/**
+ * Тесты обработки статусов 1С в UploadService
+ *
+ * Проверяют корректность установки статусов при интеграции с 1С.
+ *
+ * Фикс: b5050c60 - Меняем статус 1С
+ * Проблема: Если заказ "уже записан в системе" 1С, не должен быть статус ERROR
+ *
+ * @group services
+ * @group 1c
+ * @group regression
+ */
+class UploadService1CStatusTest extends Unit
+{
+    private string $uploadServicePath;
+
+    protected function _before(): void
+    {
+        $this->uploadServicePath = dirname(__DIR__, 3) . '/services/UploadService.php';
+    }
+
+    /**
+     * Проверяет, что сообщение "уже записан в системе" приводит к CREATED, а не ERROR
+     */
+    public function testAlreadyExistsInSystemSetsCreatedStatus(): void
+    {
+        if (!file_exists($this->uploadServicePath)) {
+            $this->markTestSkipped('UploadService.php not found');
+        }
+
+        $content = file_get_contents($this->uploadServicePath);
+
+        // Проверяем наличие проверки на "уже записан в системе"
+        $this->assertStringContainsString(
+            'уже записан в системе',
+            $content,
+            'UploadService should check for "already exists in system" message'
+        );
+
+        // Проверяем, что при этом условии устанавливается CREATED статус
+        $hasCorrectLogic = preg_match(
+            '/уже записан в системе.*?STATUSES_1C_CREATED/s',
+            $content
+        );
+
+        $this->assertEquals(
+            1,
+            $hasCorrectLogic,
+            'When order already exists in 1C, status should be CREATED_IN_1C (not ERROR)'
+        );
+    }
+
+    /**
+     * Проверяет, что при реальной ошибке устанавливается статус ERROR
+     */
+    public function testRealErrorSetsErrorStatus(): void
+    {
+        if (!file_exists($this->uploadServicePath)) {
+            $this->markTestSkipped('UploadService.php not found');
+        }
+
+        $content = file_get_contents($this->uploadServicePath);
+
+        // Должен быть статус ERROR для реальных ошибок
+        $this->assertStringContainsString(
+            'STATUSES_1C_ERROR_1C',
+            $content,
+            'UploadService should have ERROR status for real 1C errors'
+        );
+    }
+
+    /**
+     * Проверяет инициализацию массива $marketplaceOrdersGuidArr
+     *
+     * Фикс: Массив должен инициализироваться ВНЕ цикла, а не внутри
+     */
+    public function testMarketplaceOrdersGuidArrInitializedOutsideLoop(): void
+    {
+        if (!file_exists($this->uploadServicePath)) {
+            $this->markTestSkipped('UploadService.php not found');
+        }
+
+        $content = file_get_contents($this->uploadServicePath);
+
+        // Ищем паттерн где массив инициализируется перед foreach
+        // Правильно: $marketplaceOrdersGuidArr = []; ... foreach
+        // Неправильно: foreach ... $marketplaceOrdersGuidArr = [];
+
+        // Находим участок с created_orders
+        if (preg_match('/\[.created_orders.\].*?foreach\s*\(\s*\$result\s*\[\s*.created_orders.\s*\]/s', $content, $match)) {
+            $section = $match[0];
+
+            // Проверяем, что инициализация массива ДО foreach
+            $initPos = strpos($section, '$marketplaceOrdersGuidArr = []');
+            $foreachPos = strpos($section, 'foreach');
+
+            if ($initPos !== false && $foreachPos !== false) {
+                $this->assertLessThan(
+                    $foreachPos,
+                    $initPos,
+                    '$marketplaceOrdersGuidArr should be initialized BEFORE foreach loop'
+                );
+            }
+        }
+
+        $this->assertTrue(true, 'Array initialization check completed');
+    }
+
+    /**
+     * Проверяет сохранение error_text при ошибках
+     */
+    public function testErrorTextIsSaved(): void
+    {
+        if (!file_exists($this->uploadServicePath)) {
+            $this->markTestSkipped('UploadService.php not found');
+        }
+
+        $content = file_get_contents($this->uploadServicePath);
+
+        // Должно быть сохранение error_text
+        $this->assertStringContainsString(
+            'error_text',
+            $content,
+            'UploadService should save error_text field'
+        );
+
+        // Должно быть присвоение и сохранение
+        $hasErrorTextSave = preg_match(
+            '/\$\w+->error_text\s*=.*?->save\(\)/s',
+            $content
+        );
+
+        $this->assertEquals(
+            1,
+            $hasErrorTextSave,
+            'error_text should be assigned and then saved'
+        );
+    }
+
+    /**
+     * Проверяет сохранение number_1c при успешном создании
+     */
+    public function testNumber1CIsSavedOnSuccess(): void
+    {
+        if (!file_exists($this->uploadServicePath)) {
+            $this->markTestSkipped('UploadService.php not found');
+        }
+
+        $content = file_get_contents($this->uploadServicePath);
+
+        // При "уже записан в системе" должен сохраняться номер 1С
+        $hasNumber1cSave = preg_match(
+            '/уже записан в системе.*?number_1c\s*=/s',
+            $content
+        );
+
+        $this->assertEquals(
+            1,
+            $hasNumber1cSave,
+            'number_1c should be saved when order already exists in 1C'
+        );
+    }
+}