mirror of
https://github.com/usetrmnl/byos_laravel.git
synced 2026-01-13 15:07:49 +00:00
feat: add trmnlp support
This commit is contained in:
parent
f1d5c196e8
commit
989ad2e985
8 changed files with 718 additions and 61 deletions
73
tests/Feature/Api/PluginSettingsArchiveTest.php
Normal file
73
tests/Feature/Api/PluginSettingsArchiveTest.php
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Models\Plugin;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Str;
|
||||
use Laravel\Sanctum\Sanctum;
|
||||
|
||||
it('accepts a plugin settings archive and updates the plugin', function () {
|
||||
$user = User::factory()->create();
|
||||
$plugin = Plugin::factory()->create([
|
||||
'user_id' => $user->id,
|
||||
'uuid' => (string) Str::uuid(),
|
||||
]);
|
||||
|
||||
// Authenticate via Sanctum (endpoint requires auth:sanctum)
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
// Build a temporary ZIP with required structure: src/settings.yml and src/full.liquid
|
||||
$tempDir = sys_get_temp_dir().'/trmnl_zip_'.uniqid();
|
||||
$srcDir = $tempDir.'/src';
|
||||
if (! is_dir($srcDir)) {
|
||||
mkdir($srcDir, 0777, true);
|
||||
}
|
||||
|
||||
$settingsYaml = <<<'YAML'
|
||||
name: Sample Imported
|
||||
strategy: static
|
||||
refresh_interval: 10
|
||||
custom_fields:
|
||||
- keyname: title
|
||||
default: "Hello"
|
||||
static_data: '{"message":"world"}'
|
||||
YAML;
|
||||
|
||||
$fullLiquid = <<<'LIQUID'
|
||||
<h1>{{ config.title }}</h1>
|
||||
<div>{{ data.message }}</div>
|
||||
LIQUID;
|
||||
|
||||
file_put_contents($srcDir.'/settings.yml', $settingsYaml);
|
||||
file_put_contents($srcDir.'/full.liquid', $fullLiquid);
|
||||
|
||||
$zipPath = sys_get_temp_dir().'/plugin_'.uniqid().'.zip';
|
||||
$zip = new ZipArchive();
|
||||
$zip->open($zipPath, ZipArchive::CREATE);
|
||||
$zip->addFile($srcDir.'/settings.yml', 'src/settings.yml');
|
||||
$zip->addFile($srcDir.'/full.liquid', 'src/full.liquid');
|
||||
$zip->close();
|
||||
|
||||
// Prepare UploadedFile
|
||||
$uploaded = new UploadedFile($zipPath, 'plugin.zip', 'application/zip', null, true);
|
||||
|
||||
// Make request (multipart form-data)
|
||||
$response = $this->post('/api/plugin_settings/'.$plugin->uuid.'/archive', [
|
||||
'file' => $uploaded,
|
||||
], ['Accept' => 'application/json']);
|
||||
|
||||
$response->assertSuccessful();
|
||||
|
||||
$imported = Plugin::query()
|
||||
->where('user_id', $user->id)
|
||||
->where('name', 'Sample Imported')
|
||||
->first();
|
||||
|
||||
expect($imported)->not->toBeNull();
|
||||
expect($imported->markup_language)->toBe('liquid');
|
||||
expect($imported->render_markup)->toContain('<h1>{{ config.title }}</h1>');
|
||||
// Configuration should have default for title (set on create)
|
||||
expect($imported->configuration['title'] ?? null)->toBe('Hello');
|
||||
});
|
||||
308
tests/Feature/PluginArchiveTest.php
Normal file
308
tests/Feature/PluginArchiveTest.php
Normal file
|
|
@ -0,0 +1,308 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Models\Plugin;
|
||||
use App\Models\User;
|
||||
use App\Services\PluginExportService;
|
||||
use App\Services\PluginImportService;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
beforeEach(function () {
|
||||
Storage::fake('local');
|
||||
});
|
||||
|
||||
it('exports plugin to zip file in correct format', function () {
|
||||
$user = User::factory()->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' => '<div>Hello {{ config.name }}</div>',
|
||||
'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' => '<div>Hello {{ $config["name"] }}</div>',
|
||||
]);
|
||||
|
||||
$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' => '<div>Hello {{ config.name }}!</div>',
|
||||
'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' => '<div>Hello {{ $config["name"] }}!</div>',
|
||||
]);
|
||||
|
||||
$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' => '<div class="view view--{{ size }}">Hello World</div>',
|
||||
]);
|
||||
|
||||
$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' => '<div>API Test</div>',
|
||||
]);
|
||||
|
||||
$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' => '<div>Test content</div>',
|
||||
]);
|
||||
|
||||
$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' => '<div>Test</div>',
|
||||
]);
|
||||
|
||||
$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');
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue