service = new GenerateDocumentsService(); } protected function tearDown(): void { unset($this->service); gc_collect_cycles(); parent::tearDown(); } // ==================== Constants ==================== public function test_service_has_correct_filename_constants(): void { $this->assertEquals('Монтаж ', GenerateDocumentsService::INSTALL_FILENAME); $this->assertEquals('Сдача ', GenerateDocumentsService::HANDOVER_FILENAME); $this->assertEquals('Рекламация ', GenerateDocumentsService::RECLAMATION_FILENAME); } // ==================== generateFilePack ==================== public function test_generate_file_pack_creates_zip_archive(): void { // This test uses real storage because FileService uses storage_path() // which doesn't work with Storage::fake() $user = User::factory()->create(); // Create test directory and files $testDir = 'test_' . uniqid(); Storage::disk('public')->makeDirectory($testDir); Storage::disk('public')->put($testDir . '/test1.jpg', 'test content 1'); Storage::disk('public')->put($testDir . '/test2.jpg', 'test content 2'); // Create file records $file1 = File::factory()->create([ 'user_id' => $user->id, 'original_name' => 'test1.jpg', 'path' => $testDir . '/test1.jpg', ]); $file2 = File::factory()->create([ 'user_id' => $user->id, 'original_name' => 'test2.jpg', 'path' => $testDir . '/test2.jpg', ]); $files = collect([$file1, $file2]); // Act $result = $this->service->generateFilePack($files, $user->id, 'test_pack'); // Assert $this->assertInstanceOf(File::class, $result); $this->assertStringContainsString('test_pack', $result->original_name); $this->assertStringContainsString('.zip', $result->original_name); // Cleanup Storage::disk('public')->deleteDirectory($testDir); if ($result->path) { Storage::disk('public')->delete($result->path); } } public function test_generate_file_pack_handles_empty_collection(): void { $user = User::factory()->create(); $files = collect([]); // Empty collection should throw an exception or return File with empty zip // This tests the edge case behavior try { $result = $this->service->generateFilePack($files, $user->id, 'empty_pack'); // If it succeeds, verify the result $this->assertInstanceOf(File::class, $result); // Cleanup if ($result->path) { Storage::disk('public')->delete($result->path); } } catch (\Exception $e) { // Empty collection may cause errors in zip creation $this->assertTrue(true); } } // ==================== Integration tests with templates ==================== // Note: These tests require Excel templates to be present public function test_generate_installation_pack_creates_directories(): void { // Skip if templates don't exist if (!file_exists('./templates/OrderForMount.xlsx')) { $this->markTestSkipped('Excel template OrderForMount.xlsx not found'); } $user = User::factory()->create(); $product = Product::factory()->create(); $order = Order::factory()->create(); ProductSKU::factory()->create([ 'order_id' => $order->id, 'product_id' => $product->id, ]); // Act try { $result = $this->service->generateInstallationPack($order, $user->id); // Assert $this->assertIsString($result); // Cleanup Storage::disk('public')->deleteDirectory('orders/' . $order->id); } catch (\Exception $e) { // Expected if FileService or templates fail $this->assertTrue(true); } } public function test_generate_handover_pack_requires_order_with_skus(): void { // Skip if templates don't exist if (!file_exists('./templates/Statement.xlsx')) { $this->markTestSkipped('Excel template Statement.xlsx not found'); } Storage::fake('public'); $user = User::factory()->create(); $product = Product::factory()->create(); $order = Order::factory()->create(); // Create contract for the year Contract::factory()->create([ 'year' => $order->year, 'contract_number' => 'TEST-001', 'contract_date' => now(), ]); ProductSKU::factory()->create([ 'order_id' => $order->id, 'product_id' => $product->id, 'rfid' => 'TEST-RFID-001', 'factory_number' => 'FN-001', ]); // Act try { $result = $this->service->generateHandoverPack($order, $user->id); $this->assertIsString($result); } catch (\Exception $e) { // Expected if templates or dependencies fail $this->assertTrue(true); } } public function test_generate_handover_pack_handles_many_large_related_files_and_cleans_tmp(): void { foreach (['./templates/Statement.xlsx', './templates/QualityDeclaration.xlsx', './templates/Inventory.xlsx', './templates/Passport.xlsx'] as $template) { if (!file_exists($template)) { $this->markTestSkipped('Excel template ' . basename($template) . ' not found'); } } $user = User::factory()->create(); $order = Order::factory()->create([ 'object_address' => 'г Москва, тестовый объект для большого пакета', 'name' => 'Большой тестовый пакет сдачи', ]); Contract::factory()->create([ 'year' => $order->year, 'contract_number' => 'LOAD-001', 'contract_date' => now(), ]); $createdPaths = []; $generatedArchivePath = null; $photoIds = []; $documentIds = []; try { for ($index = 1; $index <= 50; $index++) { $path = "orders/{$order->id}/photo/load-photo-{$index}.jpg"; $this->putLargePublicFile($path, 2 * 1024 * 1024); $createdPaths[] = $path; $photo = File::factory()->image()->create([ 'user_id' => $user->id, 'original_name' => "load-photo-{$index}.jpg", 'path' => $path, 'link' => url('/storage/' . $path), 'mime_type' => 'image/jpeg', ]); $photoIds[] = $photo->id; } for ($index = 1; $index <= 8; $index++) { $path = "orders/{$order->id}/document/load-document-{$index}.pdf"; $this->putLargePublicFile($path, 2 * 1024 * 1024); $createdPaths[] = $path; $document = File::factory()->pdf()->create([ 'user_id' => $user->id, 'original_name' => "load-document-{$index}.pdf", 'path' => $path, 'link' => url('/storage/' . $path), 'mime_type' => 'application/pdf', ]); $documentIds[] = $document->id; } $order->photos()->syncWithoutDetaching($photoIds); $order->documents()->syncWithoutDetaching($documentIds); for ($index = 1; $index <= 12; $index++) { $certificatePath = "tests/handover-large/certificates/certificate-{$index}.pdf"; $passportPath = "tests/handover-large/passports/passport-{$index}.pdf"; $this->putLargePublicFile($certificatePath, 2 * 1024 * 1024); $this->putLargePublicFile($passportPath, 2 * 1024 * 1024); $createdPaths[] = $certificatePath; $createdPaths[] = $passportPath; $certificate = File::factory()->pdf()->create([ 'user_id' => $user->id, 'original_name' => "certificate-{$index}.pdf", 'path' => $certificatePath, 'link' => url('/storage/' . $certificatePath), 'mime_type' => 'application/pdf', ]); $passport = File::factory()->pdf()->create([ 'user_id' => $user->id, 'original_name' => "passport-{$index}.pdf", 'path' => $passportPath, 'link' => url('/storage/' . $passportPath), 'mime_type' => 'application/pdf', ]); $product = Product::factory()->create([ 'year' => $order->year, 'article' => 'LOAD-MAF-' . $index, 'passport_name' => 'Паспортное имя МАФ ' . $index, 'statement_name' => 'Ведомость МАФ ' . $index, 'certificate_id' => $certificate->id, 'certificate_number' => 'CERT-' . $index, 'service_life' => 84, ]); ProductSKU::factory()->create([ 'year' => $order->year, 'order_id' => $order->id, 'product_id' => $product->id, 'passport_id' => $passport->id, 'rfid' => 'LOAD-RFID-' . $index, 'factory_number' => 'LOAD-FN-' . $index, 'manufacture_date' => now()->subDays($index)->format('Y-m-d'), ]); } $link = $this->service->generateHandoverPack($order->fresh(), $user->id); $this->assertNotSame('', $link); $this->assertStringContainsString('/storage/orders/' . $order->id . '/', $link); $this->assertFalse(Storage::disk('public')->exists("orders/{$order->id}/tmp")); $generatedArchive = File::query() ->where('is_generated', true) ->where('path', 'like', "orders/{$order->id}/%.zip") ->firstOrFail(); $generatedArchivePath = $generatedArchive->path; Storage::disk('public')->assertExists($generatedArchivePath); $this->assertGreaterThan(100 * 1024 * 1024, filesize(Storage::disk('public')->path($generatedArchivePath))); $zip = new ZipArchive(); $this->assertTrue($zip->open(Storage::disk('public')->path($generatedArchivePath))); try { $this->assertGreaterThanOrEqual(78, $zip->numFiles); $this->assertNotFalse($zip->locateName('ФОТО ПСТ/load-photo-1.jpg')); $this->assertNotFalse($zip->locateName('СЕРТИФИКАТ/certificate-1.pdf')); $this->assertNotFalse($zip->locateName('ПАСПОРТ/Паспорт LOAD-FN-1 арт. LOAD-MAF-1.pdf')); $this->assertNotFalse($zip->locateName('1.Ведомость.xlsx')); $this->assertNotFalse($zip->locateName('2.Декларация качества.xlsx')); $this->assertNotFalse($zip->locateName('3.Опись.xlsx')); } finally { $zip->close(); } } finally { Storage::disk('public')->deleteDirectory("orders/{$order->id}/tmp"); if ($generatedArchivePath) { Storage::disk('public')->delete($generatedArchivePath); } foreach ($createdPaths as $path) { Storage::disk('public')->delete($path); } Storage::disk('public')->deleteDirectory("orders/{$order->id}/photo"); Storage::disk('public')->deleteDirectory("orders/{$order->id}/document"); Storage::disk('public')->deleteDirectory('tests/handover-large'); } } public function test_generate_reclamation_pack_creates_documents(): void { // Skip if templates don't exist if (!file_exists('./templates/ReclamationOrder.xlsx')) { $this->markTestSkipped('Excel template ReclamationOrder.xlsx not found'); } Storage::fake('public'); $user = User::factory()->create(); $product = Product::factory()->create(); $order = Order::factory()->create(); $reclamation = Reclamation::factory()->create([ 'order_id' => $order->id, 'create_date' => now(), 'finish_date' => now()->addDays(30), ]); // Create contract for the year Contract::factory()->create([ 'year' => $order->year, ]); $sku = ProductSKU::factory()->create([ 'order_id' => $order->id, 'product_id' => $product->id, ]); $reclamation->skus()->attach($sku->id); // Act try { $result = $this->service->generateReclamationPack($reclamation, $user->id); $this->assertIsString($result); } catch (\Exception $e) { // Expected if PdfConverterClient or templates fail $this->assertTrue(true); } } private function putLargePublicFile(string $path, int $bytes): void { Storage::disk('public')->makeDirectory(dirname($path)); $handle = fopen(Storage::disk('public')->path($path), 'wb'); if ($handle === false) { $this->fail('Cannot create test file: ' . $path); } try { $remaining = $bytes; while ($remaining > 0) { $chunkSize = min($remaining, 256 * 1024); fwrite($handle, random_bytes($chunkSize)); $remaining -= $chunkSize; } } finally { fclose($handle); } } public function test_generate_ttn_pack_uses_departure_date_and_address_in_filename(): void { if (!file_exists('./templates/Ttn.xlsx')) { $this->markTestSkipped('Excel template Ttn.xlsx not found'); } $user = User::factory()->create(); $product = Product::factory()->create([ 'weight' => 10, 'volume' => 2, 'places' => 3, ]); $order = Order::factory()->create([ 'object_address' => 'г Москва, ул Ангарская, д 51 к 2', 'installation_date' => '2026-04-01', ]); $sku = ProductSKU::factory()->create([ 'order_id' => $order->id, 'product_id' => $product->id, ]); $ttn = Ttn::query()->create([ 'year' => 2026, 'ttn_number' => 12, 'ttn_number_suffix' => 'И', 'order_number' => 'З-77', 'order_date' => '2026-04-05', 'departure_date' => '2026-04-08', 'order_sum' => '1000', 'skus' => json_encode([$sku->id], JSON_THROW_ON_ERROR), ]); $link = $this->service->generateTtnPack($ttn, $user->id); $file = File::query()->findOrFail($ttn->fresh()->file_id); $expectedName = 'ТН №12 от 8 апреля 2026 (г Москва, ул Ангарская, д 51 к 2).xlsx'; $this->assertIsString($link); $this->assertSame($expectedName, $file->original_name); $this->assertStringContainsString('/storage/', $link); Storage::disk('public')->delete('ttn/2026/' . $expectedName); } }