fix: DockerSecretsResolver - don't normalize absolute paths like /var/www/html/...
Some checks failed
Deploy Application / deploy (push) Has been cancelled
Some checks failed
Deploy Application / deploy (push) Has been cancelled
This commit is contained in:
@@ -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>');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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"');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user