fix: DockerSecretsResolver - don't normalize absolute paths like /var/www/html/...
Some checks failed
Deploy Application / deploy (push) Has been cancelled

This commit is contained in:
2025-11-24 21:28:25 +01:00
parent 4eb7134853
commit 77abc65cd7
1327 changed files with 91915 additions and 9909 deletions

View File

@@ -0,0 +1,116 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Framework\View\Dom\Transformer;
use App\Framework\DI\DefaultContainer;
use App\Framework\View\Dom\Parser\HtmlParser;
use App\Framework\View\Dom\Renderer\HtmlRenderer;
use App\Framework\View\Dom\Transformer\PlaceholderTransformer;
use App\Framework\View\RenderContext;
use App\Framework\Meta\MetaData;
describe('PlaceholderTransformer', function () {
beforeEach(function () {
$this->container = new DefaultContainer();
$this->parser = new HtmlParser();
$this->renderer = new HtmlRenderer();
$this->transformer = new PlaceholderTransformer($this->container);
});
describe('fallback mechanism', function () {
it('supports null coalescing operator', function () {
$html = '<div>{{ $name ?? "Default Name" }}</div>';
$context = new RenderContext(
template: 'test',
metaData: new MetaData('Test'),
data: []
);
$document = $this->parser->parse($html);
$document = $this->transformer->transform($document, $context);
$output = $this->renderer->render($document);
expect($output)->toContain('Default Name');
});
it('uses actual value when variable exists', function () {
$html = '<div>{{ $name ?? "Default Name" }}</div>';
$context = new RenderContext(
template: 'test',
metaData: new MetaData('Test'),
data: ['name' => 'Actual Name']
);
$document = $this->parser->parse($html);
$document = $this->transformer->transform($document, $context);
$output = $this->renderer->render($document);
expect($output)->toContain('Actual Name');
expect($output)->not->toContain('Default Name');
});
it('handles null values with fallback', function () {
$html = '<div>{{ $name ?? "Default" }}</div>';
$context = new RenderContext(
template: 'test',
metaData: new MetaData('Test'),
data: ['name' => null]
);
$document = $this->parser->parse($html);
$document = $this->transformer->transform($document, $context);
$output = $this->renderer->render($document);
expect($output)->toContain('Default');
});
});
describe('nested array access', function () {
it('supports nested array access in placeholders', function () {
$html = '<div>{{ $item["user"]["name"] }}</div>';
$context = new RenderContext(
template: 'test',
metaData: new MetaData('Test'),
data: [
'item' => [
'user' => [
'name' => 'John',
],
],
]
);
$document = $this->parser->parse($html);
$document = $this->transformer->transform($document, $context);
$output = $this->renderer->render($document);
expect($output)->toContain('John');
});
});
describe('error handling', function () {
it('returns empty string for missing variables in production', function () {
putenv('APP_DEBUG=false');
$html = '<div>{{ $missing }}</div>';
$context = new RenderContext(
template: 'test',
metaData: new MetaData('Test'),
data: []
);
$document = $this->parser->parse($html);
$document = $this->transformer->transform($document, $context);
$output = $this->renderer->render($document);
// Should not throw, but return empty string
expect($output)->toContain('<div></div>');
});
});
});

View File

@@ -0,0 +1,306 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Framework\View\Dom\Transformer;
use App\Framework\Config\AppConfig;
use App\Framework\DI\DefaultContainer;
use App\Framework\LiveComponents\ComponentRegistry;
use App\Framework\LiveComponents\Performance\ComponentMetadataCache;
use App\Framework\LiveComponents\Performance\ComponentMetadataCacheInterface;
use App\Framework\LiveComponents\Contracts\ComponentRegistryInterface;
use App\Framework\Meta\MetaData;
use App\Framework\View\Components\Button;
use App\Framework\View\Dom\Parser\HtmlParser;
use App\Framework\View\Dom\Renderer\HtmlRenderer;
use App\Framework\View\Dom\Transformer\ForTransformer;
use App\Framework\View\Dom\Transformer\PlaceholderTransformer;
use App\Framework\View\Dom\Transformer\XComponentTransformer;
use App\Framework\View\Processing\AstProcessingPipeline;
use App\Framework\View\RenderContext;
use App\Framework\View\StaticComponentRenderer;
describe('XComponentTransformer + ForTransformer Integration', function () {
beforeEach(function () {
$this->container = new DefaultContainer();
$this->parser = new HtmlParser();
$this->renderer = new HtmlRenderer();
// Create component registry and dependencies
$discoveryRegistry = $this->container->get('App\Framework\Discovery\Results\DiscoveryRegistry');
$liveComponentRenderer = $this->container->get('App\Framework\View\LiveComponentRenderer');
$cacheManager = $this->container->get('App\Framework\LiveComponents\Cache\ComponentCacheManager');
$handler = $this->container->get('App\Framework\LiveComponents\LiveComponentHandler');
$metadataCache = new ComponentMetadataCache($this->container);
$performanceTracker = $this->container->get('App\Framework\LiveComponents\Performance\NestedPerformanceTracker');
$this->componentRegistry = new ComponentRegistry(
$this->container,
$discoveryRegistry,
$liveComponentRenderer,
$cacheManager,
$handler,
$metadataCache,
$performanceTracker
);
$this->staticComponentRenderer = new StaticComponentRenderer();
$this->metadataCache = $metadataCache;
$this->appConfig = new AppConfig(['debug' => true]);
// Create transformers
$this->forTransformer = new ForTransformer($this->container);
$this->placeholderTransformer = new PlaceholderTransformer($this->container);
$this->xComponentTransformer = new XComponentTransformer(
$this->componentRegistry,
$this->staticComponentRenderer,
$this->metadataCache,
$this->parser,
$this->appConfig
);
});
it('transforms x-button components within for loops', function () {
$html = <<<HTML
<ul>
<for items="{{pages}}" as="page">
<li>
<x-button variant="secondary" size="sm" href="{{page.url}}" class="page-link">{{page.number}}</x-button>
</li>
</for>
</ul>
HTML;
$context = new RenderContext(
template: 'test',
metaData: new MetaData('Test'),
data: [
'pages' => [
['number' => 1, 'url' => '/page/1'],
['number' => 2, 'url' => '/page/2'],
['number' => 3, 'url' => '/page/3'],
],
]
);
// Process through pipeline: ForTransformer -> PlaceholderTransformer -> XComponentTransformer
$document = $this->parser->parse($html);
$document = $this->forTransformer->transform($document, $context);
$document = $this->placeholderTransformer->transform($document, $context);
$document = $this->xComponentTransformer->transform($document, $context);
// Render to HTML
$output = $this->renderer->render($document);
// Assert: No x-button tags should remain
expect($output)->not->toContain('<x-button');
expect($output)->not->toContain('</x-button>');
// Assert: Buttons should be rendered as <a> or <button> elements
expect($output)->toContain('<a');
expect($output)->toContain('href="/page/1"');
expect($output)->toContain('href="/page/2"');
expect($output)->toContain('href="/page/3"');
expect($output)->toContain('1');
expect($output)->toContain('2');
expect($output)->toContain('3');
});
it('transforms x-button components in pagination-like structure', function () {
$html = <<<HTML
<nav>
<ul class="pagination">
<li if="{{has_previous}}">
<x-button variant="secondary" size="sm" href="{{previous_url}}" class="page-link">Previous</x-button>
</li>
<for items="{{pages}}" as="page">
<li if="{{!page.ellipsis}}">
<x-button variant="{{page.active ? 'primary' : 'secondary'}}" size="sm" href="{{page.url}}" class="page-link">{{page.number}}</x-button>
</li>
</for>
<li if="{{has_next}}">
<x-button variant="secondary" size="sm" href="{{next_url}}" class="page-link">Next</x-button>
</li>
</ul>
</nav>
HTML;
$context = new RenderContext(
template: 'test',
metaData: new MetaData('Test'),
data: [
'has_previous' => true,
'has_next' => true,
'previous_url' => '/page/1',
'next_url' => '/page/3',
'pages' => [
['number' => 1, 'url' => '/page/1', 'active' => true, 'ellipsis' => false],
['number' => 2, 'url' => '/page/2', 'active' => false, 'ellipsis' => false],
['number' => 3, 'url' => '/page/3', 'active' => false, 'ellipsis' => false],
],
]
);
// Process through pipeline
$document = $this->parser->parse($html);
$document = $this->forTransformer->transform($document, $context);
$document = $this->placeholderTransformer->transform($document, $context);
$document = $this->xComponentTransformer->transform($document, $context);
// Render to HTML
$output = $this->renderer->render($document);
// Assert: No x-button tags should remain
expect($output)->not->toContain('<x-button');
expect($output)->not->toContain('</x-button>');
// Assert: All buttons should be rendered
expect($output)->toContain('Previous');
expect($output)->toContain('Next');
expect($output)->toContain('href="/page/1"');
expect($output)->toContain('href="/page/2"');
expect($output)->toContain('href="/page/3"');
});
it('handles nested for loops with x-button components', function () {
$html = <<<HTML
<div>
<for items="{{sections}}" as="section">
<h2>{{section.title}}</h2>
<ul>
<for items="{{section.items}}" as="item">
<li>
<x-button variant="secondary" href="{{item.url}}">{{item.name}}</x-button>
</li>
</for>
</ul>
</for>
</div>
HTML;
$context = new RenderContext(
template: 'test',
metaData: new MetaData('Test'),
data: [
'sections' => [
[
'title' => 'Section 1',
'items' => [
['name' => 'Item 1', 'url' => '/item/1'],
['name' => 'Item 2', 'url' => '/item/2'],
],
],
[
'title' => 'Section 2',
'items' => [
['name' => 'Item 3', 'url' => '/item/3'],
],
],
],
]
);
// Process through pipeline
$document = $this->parser->parse($html);
$document = $this->forTransformer->transform($document, $context);
$document = $this->placeholderTransformer->transform($document, $context);
$document = $this->xComponentTransformer->transform($document, $context);
// Render to HTML
$output = $this->renderer->render($document);
// Assert: No x-button tags should remain
expect($output)->not->toContain('<x-button');
expect($output)->not->toContain('</x-button>');
// Assert: All buttons should be rendered
expect($output)->toContain('Item 1');
expect($output)->toContain('Item 2');
expect($output)->toContain('Item 3');
expect($output)->toContain('href="/item/1"');
expect($output)->toContain('href="/item/2"');
expect($output)->toContain('href="/item/3"');
});
it('handles empty for loops gracefully', function () {
$html = <<<HTML
<ul>
<for items="{{pages}}" as="page">
<li>
<x-button variant="secondary" href="{{page.url}}">{{page.number}}</x-button>
</li>
</for>
</ul>
HTML;
$context = new RenderContext(
template: 'test',
metaData: new MetaData('Test'),
data: [
'pages' => [],
]
);
// Process through pipeline
$document = $this->parser->parse($html);
$document = $this->forTransformer->transform($document, $context);
$document = $this->placeholderTransformer->transform($document, $context);
$document = $this->xComponentTransformer->transform($document, $context);
// Render to HTML
$output = $this->renderer->render($document);
// Assert: No x-button tags should remain
expect($output)->not->toContain('<x-button');
expect($output)->not->toContain('</x-button>');
expect($output)->not->toContain('<for');
});
it('processes x-button components outside for loops correctly', function () {
$html = <<<HTML
<div>
<x-button variant="primary" href="/outside">Outside Button</x-button>
<ul>
<for items="{{pages}}" as="page">
<li>
<x-button variant="secondary" href="{{page.url}}">{{page.number}}</x-button>
</li>
</for>
</ul>
<x-button variant="primary" href="/after">After Button</x-button>
</div>
HTML;
$context = new RenderContext(
template: 'test',
metaData: new MetaData('Test'),
data: [
'pages' => [
['number' => 1, 'url' => '/page/1'],
],
]
);
// Process through pipeline
$document = $this->parser->parse($html);
$document = $this->forTransformer->transform($document, $context);
$document = $this->placeholderTransformer->transform($document, $context);
$document = $this->xComponentTransformer->transform($document, $context);
// Render to HTML
$output = $this->renderer->render($document);
// Assert: No x-button tags should remain
expect($output)->not->toContain('<x-button');
expect($output)->not->toContain('</x-button>');
// Assert: All buttons should be rendered
expect($output)->toContain('Outside Button');
expect($output)->toContain('After Button');
expect($output)->toContain('href="/outside"');
expect($output)->toContain('href="/after"');
expect($output)->toContain('href="/page/1"');
});
});

View File

@@ -0,0 +1,158 @@
<?php
declare(strict_types=1);
use App\Framework\Http\Session\FormIdGenerator;
use App\Framework\Http\Session\SessionId;
use App\Framework\Http\Session\SessionInterface;
use App\Framework\Http\Session\SessionManager;
use App\Framework\View\Response\FormDataResponseProcessor;
beforeEach(function () {
$this->formIdGenerator = new FormIdGenerator();
$this->sessionManager = Mockery::mock(SessionManager::class);
$this->processor = new FormDataResponseProcessor(
$this->formIdGenerator,
$this->sessionManager
);
// Mock session
$this->session = Mockery::mock(SessionInterface::class);
$this->csrfProtection = Mockery::mock();
$this->session->shouldReceive('csrf')->andReturn($this->csrfProtection);
$this->sessionManager->shouldReceive('saveSessionData')->andReturnNull();
});
it('replaces token placeholder with DOM processing', function () {
$formId = 'form_abc123def456';
$token = str_repeat('a', 64);
$html = <<<HTML
<form>
<input type="hidden" name="_form_id" value="{$formId}">
<input type="hidden" name="_token" value="___TOKEN_{$formId}___">
</form>
HTML;
$this->csrfProtection->shouldReceive('generateToken')
->with($formId)
->once()
->andReturn(\App\Framework\Security\CsrfToken::fromString($token));
$result = $this->processor->process($html, $this->session);
expect($result)->toContain($token);
expect($result)->not->toContain("___TOKEN_{$formId}___");
});
it('handles token placeholder without quotes', function () {
$formId = 'form_abc123def456';
$token = str_repeat('b', 64);
$html = <<<HTML
<form>
<input type="hidden" name="_form_id" value="{$formId}">
<input type="hidden" name="_token" value=___TOKEN_{$formId}___>
</form>
HTML;
$this->csrfProtection->shouldReceive('generateToken')
->with($formId)
->once()
->andReturn(\App\Framework\Security\CsrfToken::fromString($token));
$result = $this->processor->process($html, $this->session);
expect($result)->toContain('value="' . $token . '"');
expect($result)->not->toContain("___TOKEN_{$formId}___");
});
it('falls back to regex when DOM processing fails', function () {
$formId = 'form_abc123def456';
$token = str_repeat('c', 64);
// Malformed HTML that might cause DOM parsing issues
$html = <<<HTML
<form>
<input type="hidden" name="_form_id" value="{$formId}">
<input type="hidden" name="_token" value="___TOKEN_{$formId}___">
<unclosed-tag>
</form>
HTML;
$this->csrfProtection->shouldReceive('generateToken')
->with($formId)
->once()
->andReturn(\App\Framework\Security\CsrfToken::fromString($token));
// Should not throw exception, should fall back to regex
$result = $this->processor->process($html, $this->session);
// Should still replace token (via regex fallback)
expect($result)->toContain($token);
});
it('processes multiple forms independently', function () {
$formId1 = 'form_abc123def456';
$formId2 = 'form_xyz789ghi012';
$token1 = str_repeat('d', 64);
$token2 = str_repeat('e', 64);
$html = <<<HTML
<form>
<input type="hidden" name="_form_id" value="{$formId1}">
<input type="hidden" name="_token" value="___TOKEN_{$formId1}___">
</form>
<form>
<input type="hidden" name="_form_id" value="{$formId2}">
<input type="hidden" name="_token" value="___TOKEN_{$formId2}___">
</form>
HTML;
$this->csrfProtection->shouldReceive('generateToken')
->with($formId1)
->once()
->andReturn(\App\Framework\Security\CsrfToken::fromString($token1));
$this->csrfProtection->shouldReceive('generateToken')
->with($formId2)
->once()
->andReturn(\App\Framework\Security\CsrfToken::fromString($token2));
$result = $this->processor->process($html, $this->session);
expect($result)->toContain($token1);
expect($result)->toContain($token2);
expect($result)->not->toContain("___TOKEN_{$formId1}___");
expect($result)->not->toContain("___TOKEN_{$formId2}___");
});
it('validates token length after replacement', function () {
$formId = 'form_abc123def456';
$token = str_repeat('f', 64);
$html = <<<HTML
<form>
<input type="hidden" name="_form_id" value="{$formId}">
<input type="hidden" name="_token" value="___TOKEN_{$formId}___">
</form>
HTML;
$this->csrfProtection->shouldReceive('generateToken')
->with($formId)
->once()
->andReturn(\App\Framework\Security\CsrfToken::fromString($token));
$result = $this->processor->process($html, $this->session);
// Extract token from result
preg_match('/name="_token"[^>]*value="([^"]+)"/', $result, $matches);
if (isset($matches[1])) {
expect(strlen($matches[1]))->toBe(64);
}
});