create(); // Create a mock ZIP file with the required structure $zipContent = createMockZipFile([ 'src/settings.yml' => getValidSettingsYaml(), 'src/full.liquid' => getValidFullLiquid(), ]); $zipFile = UploadedFile::fake()->createWithContent('test-plugin.zip', $zipContent); $pluginImportService = new PluginImportService(); $plugin = $pluginImportService->importFromZip($zipFile, $user); expect($plugin)->toBeInstanceOf(Plugin::class) ->and($plugin->user_id)->toBe($user->id) ->and($plugin->name)->toBe('Test Plugin') ->and($plugin->data_stale_minutes)->toBe(30) ->and($plugin->data_strategy)->toBe('static') ->and($plugin->markup_language)->toBe('liquid') ->and($plugin->configuration_template)->toHaveKey('custom_fields') ->and($plugin->configuration)->toHaveKey('api_key') ->and($plugin->configuration['api_key'])->toBe('default-api-key'); }); it('imports plugin with shared.liquid file', function (): void { $user = User::factory()->create(); $zipContent = createMockZipFile([ 'src/settings.yml' => getValidSettingsYaml(), 'src/full.liquid' => getValidFullLiquid(), 'src/shared.liquid' => '{% comment %}Shared styles{% endcomment %}', ]); $zipFile = UploadedFile::fake()->createWithContent('test-plugin.zip', $zipContent); $pluginImportService = new PluginImportService(); $plugin = $pluginImportService->importFromZip($zipFile, $user); expect($plugin->render_markup)->toContain('{% comment %}Shared styles{% endcomment %}') ->and($plugin->render_markup)->toContain('
'); }); it('imports plugin with files in root directory', function (): void { $user = User::factory()->create(); $zipContent = createMockZipFile([ 'settings.yml' => getValidSettingsYaml(), 'full.liquid' => getValidFullLiquid(), ]); $zipFile = UploadedFile::fake()->createWithContent('test-plugin.zip', $zipContent); $pluginImportService = new PluginImportService(); $plugin = $pluginImportService->importFromZip($zipFile, $user); expect($plugin)->toBeInstanceOf(Plugin::class) ->and($plugin->name)->toBe('Test Plugin'); }); it('throws exception for invalid zip file', function (): void { $user = User::factory()->create(); $zipFile = UploadedFile::fake()->createWithContent('invalid.zip', 'not a zip file'); $pluginImportService = new PluginImportService(); expect(fn (): Plugin => $pluginImportService->importFromZip($zipFile, $user)) ->toThrow(Exception::class, 'Could not open the ZIP file.'); }); it('throws exception for missing required files', function (): void { $user = User::factory()->create(); $zipContent = createMockZipFile([ 'src/settings.yml' => getValidSettingsYaml(), // Missing full.liquid ]); $zipFile = UploadedFile::fake()->createWithContent('test-plugin.zip', $zipContent); $pluginImportService = new PluginImportService(); expect(fn (): Plugin => $pluginImportService->importFromZip($zipFile, $user)) ->toThrow(Exception::class, 'Invalid ZIP structure. Required files settings.yml and full.liquid are missing.'); }); it('sets default values when settings are missing', function (): void { $user = User::factory()->create(); $zipContent = createMockZipFile([ 'src/settings.yml' => "name: Minimal Plugin\n", 'src/full.liquid' => getValidFullLiquid(), ]); $zipFile = UploadedFile::fake()->createWithContent('test-plugin.zip', $zipContent); $pluginImportService = new PluginImportService(); $plugin = $pluginImportService->importFromZip($zipFile, $user); expect($plugin->name)->toBe('Minimal Plugin') ->and($plugin->data_stale_minutes)->toBe(15) // default value ->and($plugin->data_strategy)->toBe('static') // default value ->and($plugin->polling_verb)->toBe('get'); // default value }); it('handles blade markup language correctly', function (): void { $user = User::factory()->create(); $zipContent = createMockZipFile([ 'src/settings.yml' => getValidSettingsYaml(), 'src/full.blade.php' => '
Blade template
', ]); $zipFile = UploadedFile::fake()->createWithContent('test-plugin.zip', $zipContent); $pluginImportService = new PluginImportService(); $plugin = $pluginImportService->importFromZip($zipFile, $user); expect($plugin->markup_language)->toBe('blade') ->and($plugin->render_markup)->not->toContain('
') ->and($plugin->render_markup)->toBe('
Blade template
'); }); it('imports plugin from monorepo with zip_entry_path parameter', function (): void { $user = User::factory()->create(); // Create a mock ZIP file with plugin in a subdirectory $zipContent = createMockZipFile([ 'example-plugin/settings.yml' => getValidSettingsYaml(), 'example-plugin/full.liquid' => getValidFullLiquid(), ]); $zipFile = UploadedFile::fake()->createWithContent('monorepo.zip', $zipContent); $pluginImportService = new PluginImportService(); $plugin = $pluginImportService->importFromZip($zipFile, $user); expect($plugin)->toBeInstanceOf(Plugin::class) ->and($plugin->user_id)->toBe($user->id) ->and($plugin->name)->toBe('Test Plugin'); }); it('imports plugin from monorepo with src subdirectory', function (): void { $user = User::factory()->create(); // Create a mock ZIP file with plugin in a subdirectory with src folder $zipContent = createMockZipFile([ 'example-plugin/src/settings.yml' => getValidSettingsYaml(), 'example-plugin/src/full.liquid' => getValidFullLiquid(), ]); $zipFile = UploadedFile::fake()->createWithContent('monorepo.zip', $zipContent); $pluginImportService = new PluginImportService(); $plugin = $pluginImportService->importFromZip($zipFile, $user); expect($plugin)->toBeInstanceOf(Plugin::class) ->and($plugin->user_id)->toBe($user->id) ->and($plugin->name)->toBe('Test Plugin'); }); it('imports plugin from monorepo with shared.liquid in subdirectory', function (): void { $user = User::factory()->create(); $zipContent = createMockZipFile([ 'example-plugin/settings.yml' => getValidSettingsYaml(), 'example-plugin/full.liquid' => getValidFullLiquid(), 'example-plugin/shared.liquid' => '{% comment %}Monorepo shared styles{% endcomment %}', ]); $zipFile = UploadedFile::fake()->createWithContent('monorepo.zip', $zipContent); $pluginImportService = new PluginImportService(); $plugin = $pluginImportService->importFromZip($zipFile, $user); expect($plugin->render_markup)->toContain('{% comment %}Monorepo shared styles{% endcomment %}') ->and($plugin->render_markup)->toContain('
'); }); it('imports plugin from URL with zip_entry_path parameter', function (): void { $user = User::factory()->create(); // Create a mock ZIP file with plugin in a subdirectory $zipContent = createMockZipFile([ 'example-plugin/settings.yml' => getValidSettingsYaml(), 'example-plugin/full.liquid' => getValidFullLiquid(), ]); // Mock the HTTP response Http::fake([ 'https://github.com/example/repo/archive/refs/heads/main.zip' => Http::response($zipContent, 200), ]); $pluginImportService = new PluginImportService(); $plugin = $pluginImportService->importFromUrl( 'https://github.com/example/repo/archive/refs/heads/main.zip', $user, 'example-plugin' ); expect($plugin)->toBeInstanceOf(Plugin::class) ->and($plugin->user_id)->toBe($user->id) ->and($plugin->name)->toBe('Test Plugin'); Http::assertSent(fn ($request): bool => $request->url() === 'https://github.com/example/repo/archive/refs/heads/main.zip'); }); it('imports plugin from URL with zip_entry_path and src subdirectory', function (): void { $user = User::factory()->create(); // Create a mock ZIP file with plugin in a subdirectory with src folder $zipContent = createMockZipFile([ 'example-plugin/src/settings.yml' => getValidSettingsYaml(), 'example-plugin/src/full.liquid' => getValidFullLiquid(), ]); // Mock the HTTP response Http::fake([ 'https://github.com/example/repo/archive/refs/heads/main.zip' => Http::response($zipContent, 200), ]); $pluginImportService = new PluginImportService(); $plugin = $pluginImportService->importFromUrl( 'https://github.com/example/repo/archive/refs/heads/main.zip', $user, 'example-plugin' ); expect($plugin)->toBeInstanceOf(Plugin::class) ->and($plugin->user_id)->toBe($user->id) ->and($plugin->name)->toBe('Test Plugin'); }); it('imports plugin from GitHub monorepo with repository-named directory', function (): void { $user = User::factory()->create(); // Create a mock ZIP file that simulates GitHub's ZIP structure with repository-named directory $zipContent = createMockZipFile([ 'example-repo-main/another-plugin/src/settings.yml' => "name: Other Plugin\nrefresh_interval: 60\nstrategy: static\npolling_verb: get\nstatic_data: '{}'\ncustom_fields: []", 'example-repo-main/another-plugin/src/full.liquid' => '
Other content
', 'example-repo-main/example-plugin/src/settings.yml' => getValidSettingsYaml(), 'example-repo-main/example-plugin/src/full.liquid' => getValidFullLiquid(), ]); // Mock the HTTP response Http::fake([ 'https://github.com/example/repo/archive/refs/heads/main.zip' => Http::response($zipContent, 200), ]); $pluginImportService = new PluginImportService(); $plugin = $pluginImportService->importFromUrl( 'https://github.com/example/repo/archive/refs/heads/main.zip', $user, 'example-repo-main/example-plugin' ); expect($plugin)->toBeInstanceOf(Plugin::class) ->and($plugin->user_id)->toBe($user->id) ->and($plugin->name)->toBe('Test Plugin'); // Should be from example-plugin, not other-plugin }); it('finds required files in simple ZIP structure', function (): void { $user = User::factory()->create(); // Create a simple ZIP file with just one plugin $zipContent = createMockZipFile([ 'example-repo-main/example-plugin/src/settings.yml' => getValidSettingsYaml(), 'example-repo-main/example-plugin/src/full.liquid' => getValidFullLiquid(), ]); $zipFile = UploadedFile::fake()->createWithContent('simple.zip', $zipContent); $pluginImportService = new PluginImportService(); $plugin = $pluginImportService->importFromZip($zipFile, $user); expect($plugin)->toBeInstanceOf(Plugin::class) ->and($plugin->user_id)->toBe($user->id) ->and($plugin->name)->toBe('Test Plugin'); }); it('finds required files in GitHub monorepo structure with zip_entry_path', function (): void { $user = User::factory()->create(); // Create a mock ZIP file that simulates GitHub's ZIP structure $zipContent = createMockZipFile([ 'example-repo-main/example-plugin/src/settings.yml' => getValidSettingsYaml(), 'example-repo-main/example-plugin/src/full.liquid' => getValidFullLiquid(), 'example-repo-main/other-plugin/src/settings.yml' => "name: Other Plugin\nrefresh_interval: 60\nstrategy: static\npolling_verb: get\nstatic_data: '{}'\ncustom_fields: []", 'example-repo-main/other-plugin/src/full.liquid' => '
Other content
', ]); $zipFile = UploadedFile::fake()->createWithContent('monorepo.zip', $zipContent); $pluginImportService = new PluginImportService(); $plugin = $pluginImportService->importFromZip($zipFile, $user, 'example-repo-main/example-plugin'); expect($plugin)->toBeInstanceOf(Plugin::class) ->and($plugin->user_id)->toBe($user->id) ->and($plugin->name)->toBe('Test Plugin'); // Should be from example-plugin, not other-plugin }); it('imports specific plugin from monorepo zip with zip_entry_path parameter', function (): void { $user = User::factory()->create(); // Create a mock ZIP file with 2 plugins in a monorepo structure $zipContent = createMockZipFile([ 'example-plugin/settings.yml' => getValidSettingsYaml(), 'example-plugin/full.liquid' => getValidFullLiquid(), 'example-plugin/shared.liquid' => '{% comment %}Monorepo shared styles{% endcomment %}', 'example-plugin2/settings.yml' => "name: Example Plugin 2\nrefresh_interval: 45\nstrategy: static\npolling_verb: get\nstatic_data: '{}'\ncustom_fields: []", 'example-plugin2/full.liquid' => '
Plugin 2 content
', 'example-plugin2/shared.liquid' => '{% comment %}Plugin 2 shared styles{% endcomment %}', ]); $zipFile = UploadedFile::fake()->createWithContent('monorepo.zip', $zipContent); $pluginImportService = new PluginImportService(); // This test will fail because importFromZip doesn't support zip_entry_path parameter yet // The logic needs to be implemented to specify which plugin to import from the monorepo $plugin = $pluginImportService->importFromZip($zipFile, $user, 'example-plugin2'); expect($plugin)->toBeInstanceOf(Plugin::class) ->and($plugin->user_id)->toBe($user->id) ->and($plugin->name)->toBe('Example Plugin 2') // Should import example-plugin2, not example-plugin ->and($plugin->render_markup)->toContain('{% comment %}Plugin 2 shared styles{% endcomment %}') ->and($plugin->render_markup)->toContain('
Plugin 2 content
'); }); it('sets icon_url when importing from URL with iconUrl parameter', function (): void { $user = User::factory()->create(); $zipContent = createMockZipFile([ 'src/settings.yml' => getValidSettingsYaml(), 'src/full.liquid' => getValidFullLiquid(), ]); Http::fake([ 'https://example.com/plugin.zip' => Http::response($zipContent, 200), ]); $pluginImportService = new PluginImportService(); $plugin = $pluginImportService->importFromUrl( 'https://example.com/plugin.zip', $user, null, null, 'https://example.com/icon.png' ); expect($plugin)->toBeInstanceOf(Plugin::class) ->and($plugin->icon_url)->toBe('https://example.com/icon.png'); }); it('does not set icon_url when importing from URL without iconUrl parameter', function (): void { $user = User::factory()->create(); $zipContent = createMockZipFile([ 'src/settings.yml' => getValidSettingsYaml(), 'src/full.liquid' => getValidFullLiquid(), ]); Http::fake([ 'https://example.com/plugin.zip' => Http::response($zipContent, 200), ]); $pluginImportService = new PluginImportService(); $plugin = $pluginImportService->importFromUrl( 'https://example.com/plugin.zip', $user ); expect($plugin)->toBeInstanceOf(Plugin::class) ->and($plugin->icon_url)->toBeNull(); }); // Helper methods function createMockZipFile(array $files): string { $zip = new ZipArchive(); $tempFileName = 'test_zip_'.uniqid().'.zip'; $tempFile = Storage::path($tempFileName); $zip->open($tempFile, ZipArchive::CREATE); foreach ($files as $path => $content) { $zip->addFromString($path, $content); } $zip->close(); $content = file_get_contents($tempFile); Storage::delete($tempFileName); return $content; } function getValidSettingsYaml(): string { return <<<'YAML' name: Test Plugin refresh_interval: 30 strategy: static polling_verb: get static_data: '{"test": "data"}' custom_fields: - keyname: api_key field_type: text default: default-api-key label: API Key YAML; } function getValidFullLiquid(): string { return <<<'LIQUID'

{{ data.title }}

{{ data.description }}

LIQUID; }