create(); $plugin = Plugin::factory()->create([ 'user_id' => $user->id, 'name' => 'Test Plugin', 'trmnlp_id' => 'test-plugin-123', 'data_stale_minutes' => 30, 'data_strategy' => 'static', 'markup_language' => 'liquid', 'render_markup' => '
Hello {{ config.name }}
', 'configuration_template' => [ 'custom_fields' => [ [ 'keyname' => 'name', 'field_type' => 'text', 'default' => 'World', ], ], ], 'data_payload' => ['message' => 'Hello World'], ]); $exporter = app(PluginExportService::class); $response = $exporter->exportToZip($plugin, $user); expect($response)->toBeInstanceOf(Symfony\Component\HttpFoundation\BinaryFileResponse::class); expect($response->getFile()->getFilename())->toContain('test-plugin-123.zip'); }); it('exports plugin with polling configuration', function () { $user = User::factory()->create(); $plugin = Plugin::factory()->create([ 'user_id' => $user->id, 'name' => 'Polling Plugin', 'trmnlp_id' => 'polling-plugin-456', 'data_strategy' => 'polling', 'polling_url' => 'https://api.example.com/data', 'polling_verb' => 'post', 'polling_header' => 'Authorization: Bearer token', 'polling_body' => '{"key": "value"}', 'markup_language' => 'blade', 'render_markup' => '
Hello {{ $config["name"] }}
', ]); $exporter = app(PluginExportService::class); $response = $exporter->exportToZip($plugin, $user); expect($response)->toBeInstanceOf(Symfony\Component\HttpFoundation\BinaryFileResponse::class); }); it('exports and imports plugin maintaining all data', function () { $user = User::factory()->create(); $originalPlugin = Plugin::factory()->create([ 'user_id' => $user->id, 'name' => 'Round Trip Plugin', 'trmnlp_id' => 'round-trip-789', 'data_stale_minutes' => 45, 'data_strategy' => 'static', 'markup_language' => 'liquid', 'render_markup' => '
Hello {{ config.name }}!
', 'configuration_template' => [ 'custom_fields' => [ [ 'keyname' => 'name', 'field_type' => 'text', 'default' => 'Test User', ], [ 'keyname' => 'color', 'field_type' => 'select', 'default' => 'blue', 'options' => ['red', 'green', 'blue'], ], ], ], 'data_payload' => ['items' => [1, 2, 3]], ]); // Export the plugin $exporter = app(PluginExportService::class); $exportResponse = $exporter->exportToZip($originalPlugin, $user); // Get the exported file path $exportedFilePath = $exportResponse->getFile()->getPathname(); // Create an UploadedFile from the exported ZIP $uploadedFile = new UploadedFile( $exportedFilePath, 'plugin_round-trip-789.zip', 'application/zip', null, true ); // Import the plugin back $importer = app(PluginImportService::class); $importedPlugin = $importer->importFromZip($uploadedFile, $user); // Verify the imported plugin has the same data expect($importedPlugin->name)->toBe('Round Trip Plugin'); expect($importedPlugin->trmnlp_id)->toBe('round-trip-789'); expect($importedPlugin->data_stale_minutes)->toBe(45); expect($importedPlugin->data_strategy)->toBe('static'); expect($importedPlugin->markup_language)->toBe('liquid'); expect($importedPlugin->render_markup)->toContain('Hello {{ config.name }}!'); expect($importedPlugin->configuration_template['custom_fields'])->toHaveCount(2); expect($importedPlugin->data_payload)->toBe(['items' => [1, 2, 3]]); }); it('handles blade templates correctly', function () { $user = User::factory()->create(); $plugin = Plugin::factory()->create([ 'user_id' => $user->id, 'name' => 'Blade Plugin', 'trmnlp_id' => 'blade-plugin-101', 'markup_language' => 'blade', 'render_markup' => '
Hello {{ $config["name"] }}!
', ]); $exporter = app(PluginExportService::class); $response = $exporter->exportToZip($plugin, $user); expect($response)->toBeInstanceOf(Symfony\Component\HttpFoundation\BinaryFileResponse::class); }); it('removes wrapper div from exported markup', function () { $user = User::factory()->create(); $plugin = Plugin::factory()->create([ 'user_id' => $user->id, 'name' => 'Wrapped Plugin', 'trmnlp_id' => 'wrapped-plugin-202', 'markup_language' => 'liquid', 'render_markup' => '
Hello World
', ]); $exporter = app(PluginExportService::class); $response = $exporter->exportToZip($plugin, $user); expect($response)->toBeInstanceOf(Symfony\Component\HttpFoundation\BinaryFileResponse::class); }); it('converts polling headers correctly', function () { $user = User::factory()->create(); $plugin = Plugin::factory()->create([ 'user_id' => $user->id, 'name' => 'Header Plugin', 'trmnlp_id' => 'header-plugin-303', 'data_strategy' => 'polling', 'polling_header' => 'Authorization: Bearer token', ]); $exporter = app(PluginExportService::class); $response = $exporter->exportToZip($plugin, $user); expect($response)->toBeInstanceOf(Symfony\Component\HttpFoundation\BinaryFileResponse::class); }); it('api route returns zip file for authenticated user', function () { $user = User::factory()->create(); $plugin = Plugin::factory()->create([ 'user_id' => $user->id, 'name' => 'API Test Plugin', 'trmnlp_id' => 'api-test-404', 'markup_language' => 'liquid', 'render_markup' => '
API Test
', ]); $response = $this->actingAs($user) ->getJson("/api/plugin_settings/{$plugin->trmnlp_id}/archive"); $response->assertStatus(200); $response->assertHeader('Content-Type', 'application/zip'); $response->assertHeader('Content-Disposition', 'attachment; filename=plugin_api-test-404.zip'); }); it('api route returns 404 for non-existent plugin', function () { $user = User::factory()->create(); $response = $this->actingAs($user) ->getJson('/api/plugin_settings/non-existent-id/archive'); $response->assertStatus(404); }); it('api route returns 401 for unauthenticated user', function () { $response = $this->getJson('/api/plugin_settings/test-id/archive'); $response->assertStatus(401); }); it('api route returns 404 for plugin belonging to different user', function () { $user1 = User::factory()->create(); $user2 = User::factory()->create(); $plugin = Plugin::factory()->create([ 'user_id' => $user1->id, 'trmnlp_id' => 'other-user-plugin', ]); $response = $this->actingAs($user2) ->getJson("/api/plugin_settings/{$plugin->trmnlp_id}/archive"); $response->assertStatus(404); }); it('exports zip with files in root directory', function () { $user = User::factory()->create(); $plugin = Plugin::factory()->create([ 'user_id' => $user->id, 'name' => 'Root Directory Test', 'trmnlp_id' => 'root-test-123', 'markup_language' => 'liquid', 'render_markup' => '
Test content
', ]); $exporter = app(PluginExportService::class); $response = $exporter->exportToZip($plugin, $user); $zipPath = $response->getFile()->getPathname(); $zip = new ZipArchive(); $zip->open($zipPath); // Check that files are in the root, not in src/ expect($zip->locateName('settings.yml'))->not->toBeFalse(); expect($zip->locateName('full.liquid'))->not->toBeFalse(); expect($zip->locateName('src/settings.yml'))->toBeFalse(); expect($zip->locateName('src/full.liquid'))->toBeFalse(); $zip->close(); }); it('maintains correct yaml field order', function () { $user = User::factory()->create(); $plugin = Plugin::factory()->create([ 'user_id' => $user->id, 'name' => 'YAML Order Test', 'trmnlp_id' => 'yaml-order-test', 'data_strategy' => 'polling', 'polling_url' => 'https://api.example.com/data', 'polling_verb' => 'post', 'data_stale_minutes' => 30, 'markup_language' => 'liquid', 'render_markup' => '
Test
', ]); $exporter = app(PluginExportService::class); $response = $exporter->exportToZip($plugin, $user); $zipPath = $response->getFile()->getPathname(); $zip = new ZipArchive(); $zip->open($zipPath); // Extract and read the settings.yml file $zip->extractTo(sys_get_temp_dir(), 'settings.yml'); $yamlContent = file_get_contents(sys_get_temp_dir().'/settings.yml'); $zip->close(); // Check that the YAML content has the expected field order $expectedOrder = [ 'name:', 'no_screen_padding:', 'dark_mode:', 'strategy:', 'static_data:', 'polling_verb:', 'polling_url:', 'refresh_interval:', 'id:', 'custom_fields:', ]; $lines = explode("\n", $yamlContent); $fieldLines = []; foreach ($lines as $line) { $line = trim($line); if (preg_match('/^([a-zA-Z_]+):/', $line, $matches)) { $fieldLines[] = $matches[1].':'; } } // Verify that the fields appear in the expected order (allowing for missing optional fields) $currentIndex = 0; foreach ($expectedOrder as $expectedField) { $foundIndex = array_search($expectedField, $fieldLines); if ($foundIndex !== false) { expect($foundIndex)->toBeGreaterThanOrEqual($currentIndex); $currentIndex = $foundIndex; } } // Clean up unlink(sys_get_temp_dir().'/settings.yml'); });