mirror of
https://github.com/usetrmnl/byos_laravel.git
synced 2026-01-13 23:18:10 +00:00
feat: initial implementation of recipe catalog
Some checks are pending
tests / ci (push) Waiting to run
Some checks are pending
tests / ci (push) Waiting to run
This commit is contained in:
parent
7434911275
commit
6d7968a7b0
6 changed files with 399 additions and 8 deletions
|
|
@ -7,6 +7,7 @@ use App\Models\User;
|
||||||
use Exception;
|
use Exception;
|
||||||
use Illuminate\Http\UploadedFile;
|
use Illuminate\Http\UploadedFile;
|
||||||
use Illuminate\Support\Facades\File;
|
use Illuminate\Support\Facades\File;
|
||||||
|
use Illuminate\Support\Facades\Http;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
use RecursiveDirectoryIterator;
|
use RecursiveDirectoryIterator;
|
||||||
use RecursiveIteratorIterator;
|
use RecursiveIteratorIterator;
|
||||||
|
|
@ -48,6 +49,130 @@ class PluginImportService
|
||||||
// Find the required files (settings.yml and full.liquid/full.blade.php)
|
// Find the required files (settings.yml and full.liquid/full.blade.php)
|
||||||
$filePaths = $this->findRequiredFiles($tempDir);
|
$filePaths = $this->findRequiredFiles($tempDir);
|
||||||
|
|
||||||
|
// Validate that we found the required files
|
||||||
|
if (! $filePaths['settingsYamlPath'] || ! $filePaths['fullLiquidPath']) {
|
||||||
|
throw new Exception('Invalid ZIP structure. Required files settings.yml and full.liquid are missing.'); // full.blade.php
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse settings.yml
|
||||||
|
$settingsYaml = File::get($filePaths['settingsYamlPath']);
|
||||||
|
$settings = Yaml::parse($settingsYaml);
|
||||||
|
|
||||||
|
// Read full.liquid content
|
||||||
|
$fullLiquid = File::get($filePaths['fullLiquidPath']);
|
||||||
|
|
||||||
|
// Prepend shared.liquid content if available
|
||||||
|
if ($filePaths['sharedLiquidPath'] && File::exists($filePaths['sharedLiquidPath'])) {
|
||||||
|
$sharedLiquid = File::get($filePaths['sharedLiquidPath']);
|
||||||
|
$fullLiquid = $sharedLiquid."\n".$fullLiquid;
|
||||||
|
}
|
||||||
|
|
||||||
|
$fullLiquid = '<div class="view view--{{ size }}">'."\n".$fullLiquid."\n".'</div>';
|
||||||
|
|
||||||
|
// Check if the file ends with .liquid to set markup language
|
||||||
|
$markupLanguage = 'blade';
|
||||||
|
if (pathinfo($filePaths['fullLiquidPath'], PATHINFO_EXTENSION) === 'liquid') {
|
||||||
|
$markupLanguage = 'liquid';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure custom_fields is properly formatted
|
||||||
|
if (! isset($settings['custom_fields']) || ! is_array($settings['custom_fields'])) {
|
||||||
|
$settings['custom_fields'] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create configuration template with the custom fields
|
||||||
|
$configurationTemplate = [
|
||||||
|
'custom_fields' => $settings['custom_fields'],
|
||||||
|
];
|
||||||
|
|
||||||
|
$plugin_updated = isset($settings['id'])
|
||||||
|
&& Plugin::where('user_id', $user->id)->where('trmnlp_id', $settings['id'])->exists();
|
||||||
|
// Create a new plugin
|
||||||
|
$plugin = Plugin::updateOrCreate(
|
||||||
|
[
|
||||||
|
'user_id' => $user->id, 'trmnlp_id' => $settings['id'] ?? Uuid::v7(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'name' => $settings['name'] ?? 'Imported Plugin',
|
||||||
|
'trmnlp_id' => $settings['id'] ?? Uuid::v7(),
|
||||||
|
'data_stale_minutes' => $settings['refresh_interval'] ?? 15,
|
||||||
|
'data_strategy' => $settings['strategy'] ?? 'static',
|
||||||
|
'polling_url' => $settings['polling_url'] ?? null,
|
||||||
|
'polling_verb' => $settings['polling_verb'] ?? 'get',
|
||||||
|
'polling_header' => isset($settings['polling_headers'])
|
||||||
|
? str_replace('=', ':', $settings['polling_headers'])
|
||||||
|
: null,
|
||||||
|
'polling_body' => $settings['polling_body'] ?? null,
|
||||||
|
'markup_language' => $markupLanguage,
|
||||||
|
'render_markup' => $fullLiquid,
|
||||||
|
'configuration_template' => $configurationTemplate,
|
||||||
|
'data_payload' => json_decode($settings['static_data'] ?? '{}', true),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (! $plugin_updated) {
|
||||||
|
// Extract default values from custom_fields and populate configuration
|
||||||
|
$configuration = [];
|
||||||
|
foreach ($settings['custom_fields'] as $field) {
|
||||||
|
if (isset($field['keyname']) && isset($field['default'])) {
|
||||||
|
$configuration[$field['keyname']] = $field['default'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// set only if plugin is new
|
||||||
|
$plugin->update([
|
||||||
|
'configuration' => $configuration,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
$plugin['trmnlp_yaml'] = $settingsYaml;
|
||||||
|
|
||||||
|
return $plugin;
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
// Clean up temporary directory
|
||||||
|
Storage::deleteDirectory($tempDirName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import a plugin from a ZIP URL
|
||||||
|
*
|
||||||
|
* @param string $zipUrl The URL to the ZIP file
|
||||||
|
* @param User $user The user importing the plugin
|
||||||
|
* @return Plugin The created plugin instance
|
||||||
|
*
|
||||||
|
* @throws Exception If the ZIP file is invalid or required files are missing
|
||||||
|
*/
|
||||||
|
public function importFromUrl(string $zipUrl, User $user): Plugin
|
||||||
|
{
|
||||||
|
// Download the ZIP file
|
||||||
|
$response = Http::timeout(60)->get($zipUrl);
|
||||||
|
|
||||||
|
if (! $response->successful()) {
|
||||||
|
throw new Exception('Could not download the ZIP file from the provided URL.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a temporary file
|
||||||
|
$tempDirName = 'temp/'.uniqid('plugin_import_', true);
|
||||||
|
Storage::makeDirectory($tempDirName);
|
||||||
|
$tempDir = Storage::path($tempDirName);
|
||||||
|
$zipPath = $tempDir.'/plugin.zip';
|
||||||
|
|
||||||
|
// Save the downloaded content to a temporary file
|
||||||
|
File::put($zipPath, $response->body());
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Extract the ZIP file using ZipArchive
|
||||||
|
$zip = new ZipArchive();
|
||||||
|
if ($zip->open($zipPath) !== true) {
|
||||||
|
throw new Exception('Could not open the downloaded ZIP file.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$zip->extractTo($tempDir);
|
||||||
|
$zip->close();
|
||||||
|
|
||||||
|
// Find the required files (settings.yml and full.liquid/full.blade.php)
|
||||||
|
$filePaths = $this->findRequiredFiles($tempDir);
|
||||||
|
|
||||||
// Validate that we found the required files
|
// Validate that we found the required files
|
||||||
if (! $filePaths['settingsYamlPath'] || ! $filePaths['fullLiquidPath']) {
|
if (! $filePaths['settingsYamlPath'] || ! $filePaths['fullLiquidPath']) {
|
||||||
throw new Exception('Invalid ZIP structure. Required files settings.yml and full.liquid/full.blade.php are missing.');
|
throw new Exception('Invalid ZIP structure. Required files settings.yml and full.liquid/full.blade.php are missing.');
|
||||||
|
|
|
||||||
|
|
@ -152,4 +152,5 @@ return [
|
||||||
|
|
||||||
'version' => env('APP_VERSION', null),
|
'version' => env('APP_VERSION', null),
|
||||||
|
|
||||||
|
'catalog_url' => env('CATALOG_URL', 'https://raw.githubusercontent.com/bnussbau/trmnl-recipe-catalog/refs/heads/main/catalog.yaml'),
|
||||||
];
|
];
|
||||||
|
|
|
||||||
148
resources/views/livewire/catalog/index.blade.php
Normal file
148
resources/views/livewire/catalog/index.blade.php
Normal file
|
|
@ -0,0 +1,148 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Services\PluginImportService;
|
||||||
|
use Livewire\Volt\Component;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
use Illuminate\Support\Facades\Http;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Symfony\Component\Yaml\Yaml;
|
||||||
|
|
||||||
|
new class extends Component {
|
||||||
|
public array $catalogPlugins = [];
|
||||||
|
public string $installingPlugin = '';
|
||||||
|
|
||||||
|
public function mount(): void
|
||||||
|
{
|
||||||
|
$this->loadCatalogPlugins();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function loadCatalogPlugins(): void
|
||||||
|
{
|
||||||
|
$catalogUrl = config('app.catalog_url');
|
||||||
|
|
||||||
|
$this->catalogPlugins = Cache::remember('catalog_plugins', 43200, function () use ($catalogUrl) {
|
||||||
|
try {
|
||||||
|
$response = Http::get($catalogUrl);
|
||||||
|
$catalogContent = $response->body();
|
||||||
|
$catalog = Yaml::parse($catalogContent);
|
||||||
|
|
||||||
|
return collect($catalog)->map(function ($plugin, $key) {
|
||||||
|
return [
|
||||||
|
'id' => $key,
|
||||||
|
'name' => $plugin['name'] ?? 'Unknown Plugin',
|
||||||
|
'description' => $plugin['author_bio']['description'] ?? '',
|
||||||
|
'author' => $plugin['author']['name'] ?? 'Unknown Author',
|
||||||
|
'github' => $plugin['author']['github'] ?? null,
|
||||||
|
'license' => $plugin['license'] ?? null,
|
||||||
|
'zip_url' => $plugin['trmnlp']['zip_url'] ?? null,
|
||||||
|
'repo_url' => $plugin['trmnlp']['repo'] ?? null,
|
||||||
|
'logo_url' => $plugin['logo_url'] ?? null,
|
||||||
|
'screenshot_url' => $plugin['screenshot_url'] ?? null,
|
||||||
|
'learn_more_url' => $plugin['author_bio']['learn_more_url'] ?? null,
|
||||||
|
];
|
||||||
|
})->toArray();
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Failed to load catalog from URL: ' . $e->getMessage());
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function installPlugin(string $pluginId, PluginImportService $pluginImportService): void
|
||||||
|
{
|
||||||
|
abort_unless(auth()->user() !== null, 403);
|
||||||
|
|
||||||
|
$plugin = collect($this->catalogPlugins)->firstWhere('id', $pluginId);
|
||||||
|
|
||||||
|
if (!$plugin || !$plugin['zip_url']) {
|
||||||
|
$this->addError('installation', 'Plugin not found or no download URL available.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->installingPlugin = $pluginId;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$importedPlugin = $pluginImportService->importFromUrl($plugin['zip_url'], auth()->user());
|
||||||
|
|
||||||
|
$this->dispatch('plugin-installed');
|
||||||
|
Flux::modal('import-from-catalog')->close();
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->addError('installation', 'Error installing plugin: ' . $e->getMessage());
|
||||||
|
} finally {
|
||||||
|
$this->installingPlugin = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}; ?>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
@if(empty($catalogPlugins))
|
||||||
|
<div class="text-center py-8">
|
||||||
|
<flux:icon name="exclamation-triangle" class="mx-auto h-12 w-12 text-gray-400" />
|
||||||
|
<flux:heading class="mt-2">No plugins available</flux:heading>
|
||||||
|
<flux:subheading>Catalog is empty</flux:subheading>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<div class="grid grid-cols-1 gap-4">
|
||||||
|
@error('installation')
|
||||||
|
<flux:callout variant="danger" icon="x-circle" heading="{{$message}}" />
|
||||||
|
@enderror
|
||||||
|
|
||||||
|
@foreach($catalogPlugins as $plugin)
|
||||||
|
<div class="bg-white dark:bg-white/10 border border-zinc-200 dark:border-white/10 [:where(&)]:p-6 [:where(&)]:rounded-xl space-y-6">
|
||||||
|
<div class="flex items-start space-x-4">
|
||||||
|
@if($plugin['logo_url'])
|
||||||
|
<img src="{{ $plugin['logo_url'] }}" alt="{{ $plugin['name'] }}" class="w-12 h-12 rounded-lg object-cover">
|
||||||
|
@else
|
||||||
|
<div class="w-12 h-12 bg-gray-200 dark:bg-gray-700 rounded-lg flex items-center justify-center">
|
||||||
|
<flux:icon name="puzzle-piece" class="w-6 h-6 text-gray-400" />
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100">{{ $plugin['name'] }}</h3>
|
||||||
|
@if ($plugin['github'])
|
||||||
|
<p class="text-sm text-gray-500 dark:text-gray-400">by {{ $plugin['github'] }}</p>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
@if($plugin['license'])
|
||||||
|
<flux:badge color="gray" size="sm">{{ $plugin['license'] }}</flux:badge>
|
||||||
|
@endif
|
||||||
|
@if($plugin['repo_url'])
|
||||||
|
<a href="{{ $plugin['repo_url'] }}" target="_blank" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">
|
||||||
|
<flux:icon name="github" class="w-5 h-5" />
|
||||||
|
</a>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if($plugin['description'])
|
||||||
|
<p class="mt-2 text-sm text-gray-600 dark:text-gray-300">{{ $plugin['description'] }}</p>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="mt-4 flex items-center space-x-3">
|
||||||
|
<flux:button
|
||||||
|
wire:click="installPlugin('{{ $plugin['id'] }}')"
|
||||||
|
variant="primary">
|
||||||
|
Install
|
||||||
|
</flux:button>
|
||||||
|
|
||||||
|
@if($plugin['learn_more_url'])
|
||||||
|
<flux:button
|
||||||
|
href="{{ $plugin['learn_more_url'] }}"
|
||||||
|
target="_blank"
|
||||||
|
variant="subtle">
|
||||||
|
Learn More
|
||||||
|
</flux:button>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
@ -36,7 +36,7 @@ new class extends Component {
|
||||||
'polling_body' => 'nullable|string',
|
'polling_body' => 'nullable|string',
|
||||||
];
|
];
|
||||||
|
|
||||||
private function refreshPlugins(): void
|
public function refreshPlugins(): void
|
||||||
{
|
{
|
||||||
$userPlugins = auth()->user()?->plugins?->map(function ($plugin) {
|
$userPlugins = auth()->user()?->plugins?->map(function ($plugin) {
|
||||||
return $plugin->toArray();
|
return $plugin->toArray();
|
||||||
|
|
@ -96,10 +96,8 @@ new class extends Component {
|
||||||
$this->reset(['zipFile']);
|
$this->reset(['zipFile']);
|
||||||
|
|
||||||
Flux::modal('import-zip')->close();
|
Flux::modal('import-zip')->close();
|
||||||
$this->dispatch('notify', ['type' => 'success', 'message' => 'Plugin imported successfully!']);
|
|
||||||
|
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
$this->dispatch('notify', ['type' => 'error', 'message' => 'Error importing plugin: ' . $e->getMessage()]);
|
$this->addError('zipFile', 'Error installing plugin: ' . $e->getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -120,7 +118,10 @@ new class extends Component {
|
||||||
<flux:button icon="chevron-down" variant="primary"></flux:button>
|
<flux:button icon="chevron-down" variant="primary"></flux:button>
|
||||||
<flux:menu>
|
<flux:menu>
|
||||||
<flux:modal.trigger name="import-zip">
|
<flux:modal.trigger name="import-zip">
|
||||||
<flux:menu.item icon="archive-box">Import Recipe</flux:menu.item>
|
<flux:menu.item icon="archive-box">Import Recipe Archive</flux:menu.item>
|
||||||
|
</flux:modal.trigger>
|
||||||
|
<flux:modal.trigger name="import-from-catalog">
|
||||||
|
<flux:menu.item icon="book-open">Import from Catalog</flux:menu.item>
|
||||||
</flux:modal.trigger>
|
</flux:modal.trigger>
|
||||||
<flux:menu.item icon="beaker" wire:click="seedExamplePlugins">Seed Example Recipes</flux:menu.item>
|
<flux:menu.item icon="beaker" wire:click="seedExamplePlugins">Seed Example Recipes</flux:menu.item>
|
||||||
</flux:menu>
|
</flux:menu>
|
||||||
|
|
@ -167,7 +168,7 @@ new class extends Component {
|
||||||
|
|
||||||
<form wire:submit="importZip">
|
<form wire:submit="importZip">
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label for="zipFile" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">.zip Archive</label>
|
<flux:label for="zipFile">.zip Archive</flux:label>
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
wire:model="zipFile"
|
wire:model="zipFile"
|
||||||
|
|
@ -175,7 +176,9 @@ new class extends Component {
|
||||||
accept=".zip"
|
accept=".zip"
|
||||||
class="block w-full text-sm text-gray-900 border border-gray-300 rounded-lg cursor-pointer bg-gray-50 dark:text-gray-400 focus:outline-none dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 p-2.5"
|
class="block w-full text-sm text-gray-900 border border-gray-300 rounded-lg cursor-pointer bg-gray-50 dark:text-gray-400 focus:outline-none dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 p-2.5"
|
||||||
/>
|
/>
|
||||||
@error('zipFile') <span class="text-red-500 text-xs mt-1">{{ $message }}</span> @enderror
|
@error('zipFile')
|
||||||
|
<flux:callout variant="danger" icon="x-circle" heading="{{$message}}" class="mt-2" />
|
||||||
|
@enderror
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
|
|
@ -186,6 +189,18 @@ new class extends Component {
|
||||||
</div>
|
</div>
|
||||||
</flux:modal>
|
</flux:modal>
|
||||||
|
|
||||||
|
<flux:modal name="import-from-catalog">
|
||||||
|
<div class="space-y-6">
|
||||||
|
<div>
|
||||||
|
<flux:heading size="lg">Import from Catalog
|
||||||
|
<flux:badge color="yellow" class="ml-2">Alpha</flux:badge>
|
||||||
|
</flux:heading>
|
||||||
|
<flux:subheading>Browse and install Recipes from the community. Add yours <a href="https://github.com/bnussbau/trmnl-recipe-catalog" class="underline" target="_blank">here</a>.</flux:subheading>
|
||||||
|
</div>
|
||||||
|
<livewire:catalog.index @plugin-installed="refreshPlugins" />
|
||||||
|
</div>
|
||||||
|
</flux:modal>
|
||||||
|
|
||||||
<flux:modal name="add-plugin" class="md:w-96">
|
<flux:modal name="add-plugin" class="md:w-96">
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<div>
|
<div>
|
||||||
|
|
|
||||||
102
tests/Feature/Livewire/Catalog/IndexTest.php
Normal file
102
tests/Feature/Livewire/Catalog/IndexTest.php
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
use Illuminate\Support\Facades\Http;
|
||||||
|
use Livewire\Volt\Volt;
|
||||||
|
use Symfony\Component\Yaml\Yaml;
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
Cache::flush();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can render catalog component', function () {
|
||||||
|
// Mock empty catalog response
|
||||||
|
Http::fake([
|
||||||
|
config('app.catalog_url') => Http::response('', 200),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$component = Volt::test('catalog.index');
|
||||||
|
|
||||||
|
$component->assertSee('No plugins available');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads plugins from catalog URL', function () {
|
||||||
|
// Clear cache first to ensure fresh data
|
||||||
|
Cache::forget('catalog_plugins');
|
||||||
|
|
||||||
|
// Mock the HTTP response for the catalog URL
|
||||||
|
$catalogData = [
|
||||||
|
'test-plugin' => [
|
||||||
|
'name' => 'Test Plugin',
|
||||||
|
'author' => ['name' => 'Test Author', 'github' => 'testuser'],
|
||||||
|
'author_bio' => [
|
||||||
|
'description' => 'A test plugin',
|
||||||
|
'learn_more_url' => 'https://example.com',
|
||||||
|
],
|
||||||
|
'license' => 'MIT',
|
||||||
|
'trmnlp' => [
|
||||||
|
'zip_url' => 'https://example.com/plugin.zip',
|
||||||
|
],
|
||||||
|
'logo_url' => 'https://example.com/logo.png',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$yamlContent = Yaml::dump($catalogData);
|
||||||
|
|
||||||
|
// Override the default mock with specific data
|
||||||
|
Http::fake([
|
||||||
|
config('app.catalog_url') => Http::response($yamlContent, 200),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$component = Volt::test('catalog.index');
|
||||||
|
|
||||||
|
$component->assertSee('Test Plugin');
|
||||||
|
$component->assertSee('testuser');
|
||||||
|
$component->assertSee('A test plugin');
|
||||||
|
$component->assertSee('MIT');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows error when plugin not found', function () {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
|
||||||
|
$this->actingAs($user);
|
||||||
|
|
||||||
|
$component = Volt::test('catalog.index');
|
||||||
|
|
||||||
|
$component->call('installPlugin', 'non-existent-plugin');
|
||||||
|
|
||||||
|
// The component should dispatch an error notification
|
||||||
|
$component->assertHasErrors();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows error when zip_url is missing', function () {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
|
||||||
|
// Mock the HTTP response for the catalog URL without zip_url
|
||||||
|
$catalogData = [
|
||||||
|
'test-plugin' => [
|
||||||
|
'name' => 'Test Plugin',
|
||||||
|
'author' => ['name' => 'Test Author'],
|
||||||
|
'author_bio' => ['description' => 'A test plugin'],
|
||||||
|
'license' => 'MIT',
|
||||||
|
'trmnlp' => [],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$yamlContent = Yaml::dump($catalogData);
|
||||||
|
|
||||||
|
Http::fake([
|
||||||
|
config('app.catalog_url') => Http::response($yamlContent, 200),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->actingAs($user);
|
||||||
|
|
||||||
|
$component = Volt::test('catalog.index');
|
||||||
|
|
||||||
|
$component->call('installPlugin', 'test-plugin');
|
||||||
|
|
||||||
|
// The component should dispatch an error notification
|
||||||
|
$component->assertHasErrors();
|
||||||
|
|
||||||
|
});
|
||||||
|
|
@ -94,7 +94,7 @@ it('throws exception for missing required files', function () {
|
||||||
|
|
||||||
$pluginImportService = new PluginImportService();
|
$pluginImportService = new PluginImportService();
|
||||||
expect(fn () => $pluginImportService->importFromZip($zipFile, $user))
|
expect(fn () => $pluginImportService->importFromZip($zipFile, $user))
|
||||||
->toThrow(Exception::class, 'Invalid ZIP structure. Required files settings.yml and full.liquid/full.blade.php are missing.');
|
->toThrow(Exception::class, 'Invalid ZIP structure. Required files settings.yml and full.liquid are missing.');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sets default values when settings are missing', function () {
|
it('sets default values when settings are missing', function () {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue