Files
michaelschiemer/tests/Framework/Admin/CrudServiceTest.php
2025-11-24 21:28:25 +01:00

414 lines
14 KiB
PHP

<?php
declare(strict_types=1);
use App\Framework\Admin\AdminApiHandler;
use App\Framework\Admin\AdminPageRenderer;
use App\Framework\Admin\Factories\AdminFormFactory;
use App\Framework\Admin\Factories\AdminTableFactory;
use App\Framework\Admin\Factories\RepositoryAdapterFactory;
use App\Framework\Admin\Services\CrudService;
use App\Framework\Admin\ValueObjects\CrudConfig;
use App\Framework\Core\PathProvider;
use App\Framework\DI\DefaultContainer;
use App\Framework\Discovery\Results\AttributeRegistry;
use App\Framework\Discovery\Results\DiscoveryRegistry;
use App\Framework\Discovery\Results\InterfaceRegistry;
use App\Framework\Discovery\Results\TemplateRegistry;
use App\Framework\Http\Request;
use App\Framework\Http\Responses\Redirect;
use App\Framework\LiveComponents\ComponentCacheManager;
use App\Framework\LiveComponents\ComponentMetadataCache;
use App\Framework\LiveComponents\ComponentRegistry;
use App\Framework\LiveComponents\LiveComponentHandler;
use App\Framework\LiveComponents\Performance\NestedPerformanceTracker;
use App\Framework\Router\Result\ViewResult;
use App\Framework\View\Dom\Renderer\HtmlRenderer;
use App\Framework\View\LiveComponentRenderer;
use App\Framework\View\Loading\TemplateLoader;
use App\Framework\View\Table\Table;
use App\Framework\View\TemplateProcessor;
use App\Framework\Cache\Driver\NullCache;
use App\Framework\Cache\GeneralCache;
use App\Framework\Serializer\Serializer;
use App\Framework\DateTime\SystemClock;
use App\Framework\Http\Session\Session;
use App\Framework\Http\Session\SessionId;
use App\Framework\Random\SecureRandomGenerator;
use App\Framework\Security\CsrfTokenGenerator;
beforeEach(function () {
// Create minimal real instances for final classes
// Since many classes are final, we create real instances with minimal dependencies
$container = new DefaultContainer();
// Create DiscoveryRegistry
$discoveryRegistry = new DiscoveryRegistry(
attributes: new AttributeRegistry(),
interfaces: new InterfaceRegistry([]),
templates: new TemplateRegistry([])
);
// Create TemplateLoader for LiveComponentRenderer
$pathProvider = new PathProvider(__DIR__ . '/../../../../');
$nullCacheDriver = new NullCache();
$serializer = Mockery::mock(Serializer::class);
$serializer->shouldReceive('serialize')->andReturnUsing(fn($data) => serialize($data));
$serializer->shouldReceive('unserialize')->andReturnUsing(fn($data) => unserialize($data));
$cache = new GeneralCache($nullCacheDriver, $serializer);
$templateLoader = new TemplateLoader(
pathProvider: $pathProvider,
cache: $cache,
discoveryRegistry: null,
templates: [],
templatePath: '/src/Framework/View/templates',
cacheEnabled: false
);
// Create TemplateProcessor
$templateProcessor = new TemplateProcessor(
astTransformers: [],
stringProcessors: [],
container: $container,
chainOptimizer: null,
compiledTemplateCache: null,
performanceTracker: null
);
// Create real Session instance
$session = Session::fromArray(
SessionId::fromString(str_repeat('a', 32)),
new SystemClock(),
new CsrfTokenGenerator(new SecureRandomGenerator()),
[]
);
// Create LiveComponentRenderer
$liveComponentRenderer = new LiveComponentRenderer(
templateLoader: $templateLoader,
templateProcessor: $templateProcessor,
session: $session
);
// Create real instances for final classes with minimal dependencies
$cacheKeyBuilder = new \App\Framework\LiveComponents\Caching\CacheKeyBuilder();
$cacheManager = new ComponentCacheManager(
cache: $cache,
cacheKeyBuilder: $cacheKeyBuilder
);
$eventDispatcher = new \App\Framework\LiveComponents\ComponentEventDispatcher();
$handler = new LiveComponentHandler(
eventDispatcher: $eventDispatcher,
session: $session
);
$metadataCompiler = new \App\Framework\LiveComponents\Performance\ComponentMetadataCompiler();
$metadataCache = new ComponentMetadataCache(
cache: $cache,
compiler: $metadataCompiler
);
$highResClock = new \App\Framework\DateTime\HighResolutionClock();
$memoryMonitor = new \App\Framework\Performance\MemoryMonitor();
$performanceTracker = new NestedPerformanceTracker(
clock: new SystemClock(),
highResClock: $highResClock,
memoryMonitor: $memoryMonitor
);
// Create real ComponentRegistry
$componentRegistry = new ComponentRegistry(
container: $container,
discoveryRegistry: $discoveryRegistry,
renderer: $liveComponentRenderer,
cacheManager: $cacheManager,
handler: $handler,
metadataCache: $metadataCache,
performanceTracker: $performanceTracker
);
$this->pageRenderer = new AdminPageRenderer($componentRegistry);
$this->formFactory = Mockery::mock(AdminFormFactory::class);
$this->tableFactory = Mockery::mock(AdminTableFactory::class);
$apiHandler = new AdminApiHandler();
$adapterFactory = new RepositoryAdapterFactory();
$this->service = new CrudService(
$this->pageRenderer,
$this->formFactory,
$this->tableFactory,
$apiHandler,
$adapterFactory
);
$this->config = CrudConfig::forResource(
resource: 'campaigns',
resourceName: 'Campaign',
title: 'Campaigns'
)->withColumns([
['field' => 'name', 'label' => 'Name'],
['field' => 'status', 'label' => 'Status'],
]);
});
afterEach(function () {
Mockery::close();
});
describe('CrudService', function () {
describe('renderIndex', function () {
it('renders index view with items using AdminPageRenderer', function () {
$items = [
['id' => '1', 'name' => 'Campaign 1', 'status' => 'active'],
['id' => '2', 'name' => 'Campaign 2', 'status' => 'draft'],
];
$request = Mockery::mock(Request::class);
$request->shouldReceive('uri')->andReturn('/admin/campaigns');
$mockTable = Mockery::mock(Table::class);
$mockTable->shouldReceive('render')->andReturn('<table>...</table>');
$this->tableFactory->shouldReceive('create')
->once()
->andReturn($mockTable);
$result = $this->service->renderIndex(
$this->config,
$items,
$request
);
expect($result)->toBeInstanceOf(ViewResult::class);
expect($result->template)->toBe('admin-index');
});
it('includes create action when canCreate is true', function () {
$items = [];
$request = Mockery::mock(Request::class);
$request->shouldReceive('uri')->andReturn('/admin/campaigns');
$mockTable = Mockery::mock(Table::class);
$mockTable->shouldReceive('render')->andReturn('');
$this->tableFactory->shouldReceive('create')->andReturn($mockTable);
$result = $this->service->renderIndex($this->config, $items, $request);
expect($result)->toBeInstanceOf(ViewResult::class);
expect($result->data['actions'])->toBeArray();
expect($result->data['actions'][0]['label'])->toBe('Create Campaign');
});
});
describe('renderCreate', function () {
it('renders create form using AdminPageRenderer', function () {
$formFields = [
'name' => ['type' => 'text', 'label' => 'Name'],
];
$mockForm = Mockery::mock();
$mockForm->shouldReceive('build')->andReturn('<form>...</form>');
$mockForm->shouldReceive('getId')->andReturn('campaign-form');
$this->formFactory->shouldReceive('create')
->once()
->andReturn($mockForm);
$result = $this->service->renderCreate(
$this->config,
$formFields
);
expect($result)->toBeInstanceOf(ViewResult::class);
expect($result->template)->toBe('admin-form');
expect($result->data['title'])->toBe('Create Campaign');
});
it('renders create form with help text as subtitle', function () {
$formFields = [];
$helpText = 'Fill in the campaign details carefully.';
$mockForm = Mockery::mock();
$mockForm->shouldReceive('build')->andReturn('');
$mockForm->shouldReceive('getId')->andReturn('form-id');
$this->formFactory->shouldReceive('create')->andReturn($mockForm);
$result = $this->service->renderCreate(
$this->config,
$formFields,
null,
$helpText
);
expect($result)->toBeInstanceOf(ViewResult::class);
expect($result->data['subtitle'])->toBe($helpText);
});
});
describe('renderEdit', function () {
it('renders edit form using AdminPageRenderer', function () {
$formFields = [
'name' => ['type' => 'text', 'label' => 'Name'],
];
$itemData = [
'id' => '123',
'name' => 'Test Campaign',
'status' => 'active',
];
$metadata = [
'id' => '123',
'createdAt' => '2024-01-01 10:00:00',
'updatedAt' => '2024-01-02 15:30:00',
];
$mockForm = Mockery::mock();
$mockForm->shouldReceive('build')->andReturn('<form>...</form>');
$mockForm->shouldReceive('getId')->andReturn('edit-form');
$this->formFactory->shouldReceive('create')
->once()
->andReturn($mockForm);
$result = $this->service->renderEdit(
$this->config,
'123',
$formFields,
$itemData,
$metadata
);
expect($result)->toBeInstanceOf(ViewResult::class);
expect($result->template)->toBe('admin-form');
expect($result->data['title'])->toBe('Edit Campaign');
});
});
describe('renderShow', function () {
it('renders show view using AdminPageRenderer', function () {
$fields = [
['label' => 'Name', 'value' => 'Test Campaign', 'type' => 'text'],
['label' => 'Status', 'value' => 'Active', 'type' => 'badge', 'color' => 'success'],
];
$metadata = [
'id' => '123',
'createdAt' => '2024-01-01',
];
$result = $this->service->renderShow(
$this->config,
'123',
$fields,
$metadata
);
expect($result)->toBeInstanceOf(ViewResult::class);
expect($result->template)->toBe('admin-show');
expect($result->data['fields'])->toBe($fields);
expect($result->data['metadata'])->toBe($metadata);
expect($result->data['editUrl'])->toBe('/admin/campaigns/edit/123');
});
});
describe('redirectAfterCreate', function () {
it('redirects to index after successful create', function () {
$request = Mockery::mock(Request::class);
$request->parsedBody = Mockery::mock();
$request->parsedBody->shouldReceive('get')
->with('action')
->andReturn(null);
$result = $this->service->redirectAfterCreate(
$this->config,
$request,
'123'
);
expect($result)->toBeInstanceOf(Redirect::class);
expect($result->url)->toBe('/admin/campaigns');
});
it('redirects to create form when save-and-continue is requested', function () {
$request = Mockery::mock(Request::class);
$request->parsedBody = Mockery::mock();
$request->parsedBody->shouldReceive('get')
->with('action')
->andReturn('save-and-continue');
$result = $this->service->redirectAfterCreate(
$this->config,
$request,
'123'
);
expect($result->url)->toBe('/admin/campaigns/create');
});
});
describe('redirectAfterUpdate', function () {
it('redirects to index after successful update', function () {
$request = Mockery::mock(Request::class);
$request->parsedBody = Mockery::mock();
$request->parsedBody->shouldReceive('get')
->with('action')
->andReturn(null);
$result = $this->service->redirectAfterUpdate(
$this->config,
$request,
'123'
);
expect($result->url)->toBe('/admin/campaigns');
});
it('redirects to show view when save-and-view is requested', function () {
$request = Mockery::mock(Request::class);
$request->parsedBody = Mockery::mock();
$request->parsedBody->shouldReceive('get')
->with('action')
->andReturn('save-and-view');
$result = $this->service->redirectAfterUpdate(
$this->config,
$request,
'123'
);
expect($result->url)->toBe('/admin/campaigns/view/123');
});
});
describe('redirectAfterDelete', function () {
it('redirects to index after successful delete', function () {
$result = $this->service->redirectAfterDelete($this->config);
expect($result)->toBeInstanceOf(Redirect::class);
expect($result->url)->toBe('/admin/campaigns');
});
});
describe('redirectWithError', function () {
it('redirects back with error message', function () {
$result = $this->service->redirectWithError('Something went wrong');
expect($result)->toBeInstanceOf(Redirect::class);
});
it('redirects with error message and input data', function () {
$inputData = ['name' => 'Test', 'email' => 'test@example.com'];
$result = $this->service->redirectWithError(
'Validation failed',
$inputData
);
expect($result)->toBeInstanceOf(Redirect::class);
});
});
});