');
});
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();
});
it('normalizes non-named select options to named values', function (): void {
$user = User::factory()->create();
$settingsYaml = <<<'YAML'
name: Test Plugin
refresh_interval: 30
strategy: static
polling_verb: get
static_data: '{}'
custom_fields:
- keyname: display_incident
field_type: select
options:
- true
- false
default: true
YAML;
$zipContent = createMockZipFile([
'src/settings.yml' => $settingsYaml,
'src/full.liquid' => getValidFullLiquid(),
]);
$zipFile = UploadedFile::fake()->createWithContent('test-plugin.zip', $zipContent);
$pluginImportService = new PluginImportService();
$plugin = $pluginImportService->importFromZip($zipFile, $user);
$customFields = $plugin->configuration_template['custom_fields'];
$displayIncidentField = collect($customFields)->firstWhere('keyname', 'display_incident');
expect($displayIncidentField)->not->toBeNull()
->and($displayIncidentField['options'])->toBe([
['true' => 'true'],
['false' => 'false'],
])
->and($displayIncidentField['default'])->toBe('true');
});
// 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;
}