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,276 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature\Framework\LiveComponents;
|
||||
|
||||
use App\Application\LiveComponents\Counter\CounterComponent;
|
||||
use App\Framework\LiveComponents\Attributes\Action;
|
||||
use App\Framework\LiveComponents\Attributes\LiveComponent;
|
||||
use App\Framework\LiveComponents\Attributes\RequiresPermission;
|
||||
use App\Framework\LiveComponents\Contracts\LiveComponentContract;
|
||||
use App\Framework\LiveComponents\Exceptions\RateLimitExceededException;
|
||||
use App\Framework\LiveComponents\LiveComponentHandler;
|
||||
use App\Application\LiveComponents\LiveComponentState;
|
||||
use App\Framework\LiveComponents\ValueObjects\ActionParameters;
|
||||
use App\Framework\LiveComponents\ValueObjects\ComponentId;
|
||||
use Tests\Feature\Framework\LiveComponents\TestHarness\LiveComponentTestCase;
|
||||
|
||||
/**
|
||||
* Comprehensive Security Tests for LiveComponents
|
||||
*
|
||||
* Tests all security features:
|
||||
* - CSRF Protection
|
||||
* - Rate Limiting
|
||||
* - Idempotency
|
||||
* - Action Allow-List
|
||||
* - Authorization
|
||||
*/
|
||||
class SecurityComprehensiveTest extends LiveComponentTestCase
|
||||
{
|
||||
public function test_csrf_protection_requires_valid_token(): void
|
||||
{
|
||||
$this->mount('counter:test', ['count' => 0]);
|
||||
|
||||
// Try to call action without CSRF token
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('CSRF token is required');
|
||||
|
||||
$this->call('increment', []);
|
||||
}
|
||||
|
||||
public function test_csrf_protection_rejects_invalid_token(): void
|
||||
{
|
||||
$this->mount('counter:test', ['count' => 0]);
|
||||
|
||||
// Try with invalid token
|
||||
$this->expectException(\RuntimeException::class);
|
||||
$this->expectExceptionMessage('CSRF token validation failed');
|
||||
|
||||
$params = ActionParameters::fromArray([
|
||||
'csrf_token' => 'invalid-token-12345678901234567890123456789012'
|
||||
]);
|
||||
|
||||
$component = $this->getComponent();
|
||||
if ($component === null) {
|
||||
$this->fail('Component not mounted');
|
||||
}
|
||||
|
||||
$this->handler->handle(
|
||||
$component,
|
||||
'increment',
|
||||
$params
|
||||
);
|
||||
}
|
||||
|
||||
public function test_rate_limiting_enforces_limits(): void
|
||||
{
|
||||
$this->mount('counter:test', ['count' => 0]);
|
||||
|
||||
// Get valid CSRF token
|
||||
$component = $this->getComponent();
|
||||
if ($component === null) {
|
||||
$this->fail('Component not mounted');
|
||||
}
|
||||
$csrfToken = $this->registry->generateCsrfToken($component->id);
|
||||
|
||||
// Execute action multiple times (assuming default limit is 10)
|
||||
for ($i = 0; $i < 10; $i++) {
|
||||
$params = ActionParameters::fromArray([
|
||||
'csrf_token' => $csrfToken->toString()
|
||||
]);
|
||||
|
||||
try {
|
||||
$component = $this->getComponent();
|
||||
if ($component === null) {
|
||||
$this->fail('Component not mounted');
|
||||
}
|
||||
|
||||
$this->handler->handle(
|
||||
$component,
|
||||
'increment',
|
||||
$params
|
||||
);
|
||||
|
||||
// Re-mount to get updated component
|
||||
$this->mount('counter:test', $this->getState());
|
||||
} catch (RateLimitExceededException $e) {
|
||||
// Expected after limit
|
||||
if ($i >= 9) {
|
||||
$this->assertInstanceOf(RateLimitExceededException::class, $e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 11th request should be rate limited
|
||||
$this->expectException(RateLimitExceededException::class);
|
||||
|
||||
$params = ActionParameters::fromArray([
|
||||
'csrf_token' => $csrfToken->toString()
|
||||
]);
|
||||
|
||||
$component = $this->getComponent();
|
||||
if ($component === null) {
|
||||
$this->fail('Component not mounted');
|
||||
}
|
||||
|
||||
$this->handler->handle(
|
||||
$component,
|
||||
'increment',
|
||||
$params
|
||||
);
|
||||
}
|
||||
|
||||
public function test_action_allow_list_only_allows_marked_actions(): void
|
||||
{
|
||||
$component = new TestComponentWithActions(
|
||||
ComponentId::create('test', 'demo'),
|
||||
LiveComponentState::empty()
|
||||
);
|
||||
|
||||
// Action with #[Action] should work
|
||||
$csrfToken = $this->registry->generateCsrfToken($component->id);
|
||||
$params = ActionParameters::fromArray([
|
||||
'csrf_token' => $csrfToken->toString()
|
||||
]);
|
||||
|
||||
$result = $this->handler->handle($component, 'allowedAction', $params);
|
||||
$this->assertNotNull($result);
|
||||
|
||||
// Action without #[Action] should fail
|
||||
$this->expectException(\BadMethodCallException::class);
|
||||
$this->expectExceptionMessage('is not marked as an action');
|
||||
|
||||
$this->handler->handle($component, 'notAllowedAction', $params);
|
||||
}
|
||||
|
||||
public function test_reserved_methods_cannot_be_called_as_actions(): void
|
||||
{
|
||||
$component = new TestComponentWithActions(
|
||||
ComponentId::create('test', 'demo'),
|
||||
LiveComponentState::empty()
|
||||
);
|
||||
|
||||
$csrfToken = $this->registry->generateCsrfToken($component->id);
|
||||
$params = ActionParameters::fromArray([
|
||||
'csrf_token' => $csrfToken->toString()
|
||||
]);
|
||||
|
||||
// Try to call reserved method
|
||||
$this->expectException(\BadMethodCallException::class);
|
||||
$this->expectExceptionMessage('reserved method');
|
||||
|
||||
$this->handler->handle($component, 'mount', $params);
|
||||
}
|
||||
|
||||
public function test_authorization_requires_permission(): void
|
||||
{
|
||||
// This test would require a component with #[RequiresPermission]
|
||||
// and proper auth setup - skipping for now as it requires more setup
|
||||
$this->markTestSkipped('Requires authentication setup');
|
||||
}
|
||||
|
||||
public function test_idempotency_prevents_duplicate_execution(): void
|
||||
{
|
||||
$this->mount('counter:test', ['count' => 0]);
|
||||
|
||||
$component = $this->getComponent();
|
||||
if ($component === null) {
|
||||
$this->fail('Component not mounted');
|
||||
}
|
||||
|
||||
$csrfToken = $this->registry->generateCsrfToken($component->id);
|
||||
$idempotencyKey = 'test-key-' . uniqid();
|
||||
|
||||
// First execution
|
||||
$params1 = ActionParameters::fromArray([
|
||||
'csrf_token' => $csrfToken->toString(),
|
||||
'idempotency_key' => $idempotencyKey
|
||||
]);
|
||||
|
||||
$result1 = $this->handler->handle(
|
||||
$component,
|
||||
'increment',
|
||||
$params1
|
||||
);
|
||||
|
||||
$this->assertEquals(1, $result1->state->data['count'] ?? 0);
|
||||
|
||||
// Second execution with same key should return cached result
|
||||
$params2 = ActionParameters::fromArray([
|
||||
'csrf_token' => $csrfToken->toString(),
|
||||
'idempotency_key' => $idempotencyKey
|
||||
]);
|
||||
|
||||
$result2 = $this->handler->handle(
|
||||
$component,
|
||||
'increment',
|
||||
$params2
|
||||
);
|
||||
|
||||
// Count should still be 1 (not 2) because action was cached
|
||||
$this->assertEquals(1, $result2->state->data['count'] ?? 0);
|
||||
}
|
||||
|
||||
public function test_security_order_csrf_before_rate_limit(): void
|
||||
{
|
||||
$this->mount('counter:test', ['count' => 0]);
|
||||
|
||||
// Invalid CSRF should fail before rate limit check
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('CSRF token is required');
|
||||
|
||||
$params = ActionParameters::fromArray([]);
|
||||
$component = $this->getComponent();
|
||||
if ($component === null) {
|
||||
$this->fail('Component not mounted');
|
||||
}
|
||||
|
||||
$this->handler->handle(
|
||||
$component,
|
||||
'increment',
|
||||
$params
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Test component with actions
|
||||
#[LiveComponent('test-with-actions')]
|
||||
final readonly class TestComponentWithActions implements LiveComponentContract
|
||||
{
|
||||
public function __construct(
|
||||
public ComponentId $id,
|
||||
public LiveComponentState $state
|
||||
) {
|
||||
}
|
||||
|
||||
public function getRenderData(): \App\Framework\LiveComponents\ValueObjects\ComponentRenderData
|
||||
{
|
||||
return new \App\Framework\LiveComponents\ValueObjects\ComponentRenderData(
|
||||
templatePath: 'test',
|
||||
data: []
|
||||
);
|
||||
}
|
||||
|
||||
#[Action]
|
||||
public function allowedAction(): LiveComponentState
|
||||
{
|
||||
return $this->state;
|
||||
}
|
||||
|
||||
// No #[Action] attribute - should not be callable
|
||||
public function notAllowedAction(): LiveComponentState
|
||||
{
|
||||
return $this->state;
|
||||
}
|
||||
|
||||
// Reserved method - should not be callable
|
||||
public function mount(): LiveComponentState
|
||||
{
|
||||
return $this->state;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user