2025-06-13 12:23:52 +02:00
<? php
declare ( strict_types = 1 );
use App\Models\Plugin ;
use App\Models\User ;
use App\Services\PluginImportService ;
use Illuminate\Http\UploadedFile ;
2025-09-21 17:35:50 +02:00
use Illuminate\Support\Facades\Http ;
2025-06-13 12:23:52 +02:00
use Illuminate\Support\Facades\Storage ;
2026-04-08 22:19:21 +02:00
use Spatie\TemporaryDirectory\TemporaryDirectory ;
2025-06-13 12:23:52 +02:00
2025-09-24 20:31:32 +02:00
beforeEach ( function () : void {
2025-06-13 12:23:52 +02:00
Storage :: fake ( 'local' );
});
2026-06-05 23:57:48 +02:00
it ( 'imports plugin with framework_version from settings' , function () : void {
$user = User :: factory () -> create ();
$settingsYaml = str_replace (
"name: Test Plugin \n " ,
"name: Test Plugin \n framework_version: 3.0.5 \n " ,
getValidSettingsYaml ()
);
$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 );
expect ( $plugin -> framework_version ) -> toBe ( '3.0.5' );
});
2025-09-24 20:31:32 +02:00
it ( 'imports plugin from valid zip file' , function () : void {
2025-06-13 12:23:52 +02:00
$user = User :: factory () -> 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' );
});
2025-09-24 20:31:32 +02:00
it ( 'imports plugin with shared.liquid file' , function () : void {
2025-06-13 12:23:52 +02:00
$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 );
2026-01-28 16:02:14 +01:00
expect ( $plugin -> render_markup_shared ) -> toBe ( '{% comment %}Shared styles{% endcomment %}' )
-> and ( $plugin -> render_markup ) -> toContain ( '<div class="view view--{{ size }}">' )
-> and ( $plugin -> getMarkupForSize ( 'full' )) -> toContain ( '{% comment %}Shared styles{% endcomment %}' )
-> and ( $plugin -> getMarkupForSize ( 'full' )) -> toContain ( '<div class="view view--{{ size }}">' );
2025-06-13 12:23:52 +02:00
});
2026-06-23 23:59:39 +02:00
it ( 'replaces existing view wrapper on liquid import instead of nesting' , function () : void {
$user = User :: factory () -> create ();
$fullLiquid = <<<' LIQUID '
<div class="view view--full">
<h1>{{ data.title }}</h1>
</div>
LIQUID ;
$zipContent = createMockZipFile ([
'src/settings.yml' => getValidSettingsYaml (),
'src/full.liquid' => $fullLiquid ,
]);
$zipFile = UploadedFile :: fake () -> createWithContent ( 'test-plugin.zip' , $zipContent );
$pluginImportService = new PluginImportService ();
$plugin = $pluginImportService -> importFromZip ( $zipFile , $user );
expect ( $plugin -> render_markup ) -> toBe ( <<<' LIQUID '
<div class="view view--{{ size }}">
<h1>{{ data.title }}</h1>
</div>
LIQUID
) -> and ( $plugin -> render_markup ) -> not -> toContain ( 'view--full' )
-> and ( mb_substr_count ( $plugin -> render_markup , '<div class="view view--{{ size }}">' )) -> toBe ( 1 );
});
it ( 'wraps liquid markup without view wrapper on import' , function () : void {
$user = User :: factory () -> create ();
$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 -> render_markup ) -> toBe ( <<<' LIQUID '
<div class="view view--{{ size }}">
<div class="plugin-content">
<h1>{{ data.title }}</h1>
<p>{{ data.description }}</p>
</div>
</div>
LIQUID
);
});
2025-09-24 20:31:32 +02:00
it ( 'imports plugin with files in root directory' , function () : void {
2025-06-13 12:23:52 +02:00
$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' );
});
2025-09-24 20:31:32 +02:00
it ( 'throws exception for invalid zip file' , function () : void {
2025-06-13 12:23:52 +02:00
$user = User :: factory () -> create ();
$zipFile = UploadedFile :: fake () -> createWithContent ( 'invalid.zip' , 'not a zip file' );
$pluginImportService = new PluginImportService ();
2025-09-24 20:31:32 +02:00
expect ( fn () : Plugin => $pluginImportService -> importFromZip ( $zipFile , $user ))
2025-06-13 12:23:52 +02:00
-> toThrow ( Exception :: class , 'Could not open the ZIP file.' );
});
2026-01-11 20:41:12 +01:00
it ( 'throws exception for missing settings.yml' , function () : void {
2025-06-13 12:23:52 +02:00
$user = User :: factory () -> create ();
$zipContent = createMockZipFile ([
2026-01-11 20:41:12 +01:00
'src/full.liquid' => getValidFullLiquid (),
// Missing settings.yml
2025-06-13 12:23:52 +02:00
]);
$zipFile = UploadedFile :: fake () -> createWithContent ( 'test-plugin.zip' , $zipContent );
$pluginImportService = new PluginImportService ();
2025-09-24 20:31:32 +02:00
expect ( fn () : Plugin => $pluginImportService -> importFromZip ( $zipFile , $user ))
2026-01-11 20:41:12 +01:00
-> toThrow ( Exception :: class , 'Invalid ZIP structure. Required file settings.yml is missing.' );
});
it ( 'throws exception for missing template files' , function () : void {
$user = User :: factory () -> create ();
$zipContent = createMockZipFile ([
'src/settings.yml' => getValidSettingsYaml (),
// Missing all template files
]);
$zipFile = UploadedFile :: fake () -> createWithContent ( 'test-plugin.zip' , $zipContent );
$pluginImportService = new PluginImportService ();
expect ( fn () : Plugin => $pluginImportService -> importFromZip ( $zipFile , $user ))
-> toThrow ( Exception :: class , 'Invalid ZIP structure. At least one of the following files is required: full.liquid, full.blade.php, shared.liquid, or shared.blade.php.' );
2025-06-13 12:23:52 +02:00
});
2025-09-24 20:31:32 +02:00
it ( 'sets default values when settings are missing' , function () : void {
2025-06-13 12:23:52 +02:00
$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
});
2025-09-24 20:31:32 +02:00
it ( 'handles blade markup language correctly' , function () : void {
2025-06-13 12:23:52 +02:00
$user = User :: factory () -> create ();
$zipContent = createMockZipFile ([
'src/settings.yml' => getValidSettingsYaml (),
'src/full.blade.php' => '<div>Blade template</div>' ,
]);
$zipFile = UploadedFile :: fake () -> createWithContent ( 'test-plugin.zip' , $zipContent );
$pluginImportService = new PluginImportService ();
$plugin = $pluginImportService -> importFromZip ( $zipFile , $user );
2025-09-25 16:39:56 +02:00
expect ( $plugin -> markup_language ) -> toBe ( 'blade' )
-> and ( $plugin -> render_markup ) -> not -> toContain ( '<div class="view view--{{ size }}">' )
-> and ( $plugin -> render_markup ) -> toBe ( '<div>Blade template</div>' );
2025-06-13 12:23:52 +02:00
});
2025-09-24 20:31:32 +02:00
it ( 'imports plugin from monorepo with zip_entry_path parameter' , function () : void {
2025-09-21 17:35:50 +02:00
$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' );
});
2025-09-24 20:31:32 +02:00
it ( 'imports plugin from monorepo with src subdirectory' , function () : void {
2025-09-21 17:35:50 +02:00
$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' );
});
2025-09-24 20:31:32 +02:00
it ( 'imports plugin from monorepo with shared.liquid in subdirectory' , function () : void {
2025-09-21 17:35:50 +02:00
$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 );
2026-01-28 16:02:14 +01:00
expect ( $plugin -> render_markup_shared ) -> toBe ( '{% comment %}Monorepo shared styles{% endcomment %}' )
-> and ( $plugin -> render_markup ) -> toContain ( '<div class="view view--{{ size }}">' )
-> and ( $plugin -> getMarkupForSize ( 'full' )) -> toContain ( '{% comment %}Monorepo shared styles{% endcomment %}' )
-> and ( $plugin -> getMarkupForSize ( 'full' )) -> toContain ( '<div class="view view--{{ size }}">' );
2025-09-21 17:35:50 +02:00
});
2025-09-24 20:31:32 +02:00
it ( 'imports plugin from URL with zip_entry_path parameter' , function () : void {
2025-09-21 17:35:50 +02:00
$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' );
2025-09-24 20:31:32 +02:00
Http :: assertSent ( fn ( $request ) : bool => $request -> url () === 'https://github.com/example/repo/archive/refs/heads/main.zip' );
2025-09-21 17:35:50 +02:00
});
2025-09-24 20:31:32 +02:00
it ( 'imports plugin from URL with zip_entry_path and src subdirectory' , function () : void {
2025-09-21 17:35:50 +02:00
$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' );
});
2025-09-24 20:31:32 +02:00
it ( 'imports plugin from GitHub monorepo with repository-named directory' , function () : void {
2025-09-21 17:35:50 +02:00
$user = User :: factory () -> create ();
// Create a mock ZIP file that simulates GitHub's ZIP structure with repository-named directory
$zipContent = createMockZipFile ([
2025-09-22 09:04:56 +02:00
'example-repo-main/another-plugin/src/settings.yml' => "name: Other Plugin \n refresh_interval: 60 \n strategy: static \n polling_verb: get \n static_data: '{}' \n custom_fields: []" ,
'example-repo-main/another-plugin/src/full.liquid' => '<div>Other content</div>' ,
2025-09-21 17:35:50 +02:00
'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 ,
2025-09-22 09:04:56 +02:00
'example-repo-main/example-plugin'
2025-09-21 17:35:50 +02:00
);
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
});
2025-09-24 20:31:32 +02:00
it ( 'finds required files in simple ZIP structure' , function () : void {
2025-09-21 17:35:50 +02:00
$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' );
});
2025-09-24 20:31:32 +02:00
it ( 'finds required files in GitHub monorepo structure with zip_entry_path' , function () : void {
2025-09-21 17:35:50 +02:00
$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 \n refresh_interval: 60 \n strategy: static \n polling_verb: get \n static_data: '{}' \n custom_fields: []" ,
'example-repo-main/other-plugin/src/full.liquid' => '<div>Other content</div>' ,
]);
$zipFile = UploadedFile :: fake () -> createWithContent ( 'monorepo.zip' , $zipContent );
$pluginImportService = new PluginImportService ();
2025-09-22 09:04:56 +02:00
$plugin = $pluginImportService -> importFromZip ( $zipFile , $user , 'example-repo-main/example-plugin' );
2025-09-21 17:35:50 +02:00
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
});
2025-09-24 20:31:32 +02:00
it ( 'imports specific plugin from monorepo zip with zip_entry_path parameter' , function () : void {
2025-09-21 17:35:50 +02:00
$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 \n refresh_interval: 45 \n strategy: static \n polling_verb: get \n static_data: '{}' \n custom_fields: []" ,
'example-plugin2/full.liquid' => '<div class="plugin2-content">Plugin 2 content</div>' ,
'example-plugin2/shared.liquid' => '{% comment %}Plugin 2 shared styles{% endcomment %}' ,
]);
$zipFile = UploadedFile :: fake () -> createWithContent ( 'monorepo.zip' , $zipContent );
$pluginImportService = new PluginImportService ();
2025-09-22 09:04:56 +02:00
2025-09-21 17:35:50 +02:00
// 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
2026-01-28 16:02:14 +01:00
-> and ( $plugin -> render_markup_shared ) -> toBe ( '{% comment %}Plugin 2 shared styles{% endcomment %}' )
-> and ( $plugin -> render_markup ) -> toContain ( '<div class="plugin2-content">Plugin 2 content</div>' )
-> and ( $plugin -> getMarkupForSize ( 'full' )) -> toContain ( '{% comment %}Plugin 2 shared styles{% endcomment %}' )
-> and ( $plugin -> getMarkupForSize ( 'full' )) -> toContain ( '<div class="plugin2-content">Plugin 2 content</div>' );
2025-09-21 17:35:50 +02:00
});
2025-10-25 20:51:06 +02:00
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 ();
});
2025-12-02 14:58:27 +01:00
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' );
});
2026-01-06 15:09:07 +00:00
it ( 'throws exception when multi_string default value contains a comma' , function () : void {
$user = User :: factory () -> create ();
// YAML with a comma in the 'default' field of a multi_string
2026-01-11 20:41:12 +01:00
$invalidYaml = <<<' YAML '
2026-01-06 15:09:07 +00:00
name: Test Plugin
refresh_interval: 30
strategy: static
polling_verb: get
static_data: '{"test": "data"}'
custom_fields:
- keyname: api_key
field_type: multi_string
default: default-api-key1,default-api-key2
label: API Key
YAML ;
$zipContent = createMockZipFile ([
'src/settings.yml' => $invalidYaml ,
'src/full.liquid' => getValidFullLiquid (),
]);
$zipFile = UploadedFile :: fake () -> createWithContent ( 'invalid-default.zip' , $zipContent );
$pluginImportService = new PluginImportService ();
2026-01-15 13:14:50 +01:00
expect ( fn () : Plugin => $pluginImportService -> importFromZip ( $zipFile , $user ))
2026-01-11 20:41:12 +01:00
-> toThrow ( Exception :: class , 'Validation Error: The default value for multistring fields like `api_key` cannot contain commas.' );
2026-01-06 15:09:07 +00:00
});
it ( 'throws exception when multi_string placeholder contains a comma' , function () : void {
$user = User :: factory () -> create ();
// YAML with a comma in the 'placeholder' field
2026-01-11 20:41:12 +01:00
$invalidYaml = <<<' YAML '
2026-01-06 15:09:07 +00:00
name: Test Plugin
refresh_interval: 30
strategy: static
polling_verb: get
static_data: '{"test": "data"}'
custom_fields:
- keyname: api_key
field_type: multi_string
default: default-api-key
label: API Key
placeholder: "value1, value2"
YAML ;
$zipContent = createMockZipFile ([
'src/settings.yml' => $invalidYaml ,
'src/full.liquid' => getValidFullLiquid (),
]);
$zipFile = UploadedFile :: fake () -> createWithContent ( 'invalid-placeholder.zip' , $zipContent );
$pluginImportService = new PluginImportService ();
2026-01-15 13:14:50 +01:00
expect ( fn () : Plugin => $pluginImportService -> importFromZip ( $zipFile , $user ))
2026-01-11 20:41:12 +01:00
-> toThrow ( Exception :: class , 'Validation Error: The placeholder value for multistring fields like `api_key` cannot contain commas.' );
});
it ( 'imports plugin with only shared.liquid file' , function () : void {
$user = User :: factory () -> create ();
$zipContent = createMockZipFile ([
'src/settings.yml' => getValidSettingsYaml (),
'src/shared.liquid' => '<div class="shared-content">{{ data.title }}</div>' ,
]);
$zipFile = UploadedFile :: fake () -> createWithContent ( 'test-plugin.zip' , $zipContent );
$pluginImportService = new PluginImportService ();
$plugin = $pluginImportService -> importFromZip ( $zipFile , $user );
expect ( $plugin ) -> toBeInstanceOf ( Plugin :: class )
-> and ( $plugin -> markup_language ) -> toBe ( 'liquid' )
2026-01-28 16:02:14 +01:00
-> and ( $plugin -> render_markup_shared ) -> toBe ( '<div class="shared-content">{{ data.title }}</div>' )
-> and ( $plugin -> render_markup ) -> toBeNull ();
2026-01-11 20:41:12 +01:00
});
it ( 'imports plugin with only shared.blade.php file' , function () : void {
$user = User :: factory () -> create ();
$zipContent = createMockZipFile ([
'src/settings.yml' => getValidSettingsYaml (),
'src/shared.blade.php' => '<div class="shared-content">{{ $data["title"] }}</div>' ,
]);
$zipFile = UploadedFile :: fake () -> createWithContent ( 'test-plugin.zip' , $zipContent );
$pluginImportService = new PluginImportService ();
$plugin = $pluginImportService -> importFromZip ( $zipFile , $user );
expect ( $plugin ) -> toBeInstanceOf ( Plugin :: class )
-> and ( $plugin -> markup_language ) -> toBe ( 'blade' )
2026-01-28 16:02:14 +01:00
-> and ( $plugin -> render_markup_shared ) -> toBe ( '<div class="shared-content">{{ $data["title"] }}</div>' )
-> and ( $plugin -> render_markup ) -> toBeNull ();
2026-01-06 15:09:07 +00:00
});
2025-06-13 12:23:52 +02:00
// Helper methods
function createMockZipFile ( array $files ) : string
{
$zip = new ZipArchive ();
2026-04-08 22:19:21 +02:00
$temporaryDirectory = ( new TemporaryDirectory ) -> deleteWhenDestroyed () -> create ();
$tempFile = $temporaryDirectory -> path ( 'mock.zip' );
2025-06-13 12:23:52 +02:00
$zip -> open ( $tempFile , ZipArchive :: CREATE );
foreach ( $files as $path => $content ) {
$zip -> addFromString ( $path , $content );
}
$zip -> close ();
2026-04-08 22:19:21 +02:00
return file_get_contents ( $tempFile );
2025-06-13 12:23:52 +02:00
}
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 '
<div class="plugin-content">
<h1>{{ data.title }}</h1>
<p>{{ data.description }}</p>
</div>
LIQUID ;
}