Files
michaelschiemer/tests/Framework/LiveComponents/ComponentTestCase.php
Michael Schiemer fc3d7e6357 feat(Production): Complete production deployment infrastructure
- Add comprehensive health check system with multiple endpoints
- Add Prometheus metrics endpoint
- Add production logging configurations (5 strategies)
- Add complete deployment documentation suite:
  * QUICKSTART.md - 30-minute deployment guide
  * DEPLOYMENT_CHECKLIST.md - Printable verification checklist
  * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle
  * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference
  * production-logging.md - Logging configuration guide
  * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation
  * README.md - Navigation hub
  * DEPLOYMENT_SUMMARY.md - Executive summary
- Add deployment scripts and automation
- Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment
- Update README with production-ready features

All production infrastructure is now complete and ready for deployment.
2025-10-25 19:18:37 +02:00

312 lines
9.2 KiB
PHP

<?php
declare(strict_types=1);
namespace Tests\Framework\LiveComponents;
use App\Framework\DateTime\SystemClock;
use App\Framework\Http\Session\Session;
use App\Framework\Http\Session\SessionId;
use App\Framework\LiveComponents\ComponentEventDispatcher;
use App\Framework\LiveComponents\Contracts\LiveComponentContract;
use App\Framework\LiveComponents\Exceptions\StateValidationException;
use App\Framework\LiveComponents\Exceptions\UnauthorizedActionException;
use App\Framework\LiveComponents\LiveComponentHandler;
use App\Framework\LiveComponents\Security\SessionBasedAuthorizationChecker;
use App\Framework\LiveComponents\Validation\DefaultStateValidator;
use App\Framework\LiveComponents\Validation\SchemaCache;
use App\Framework\LiveComponents\ValueObjects\ActionParameters;
use App\Framework\LiveComponents\ValueObjects\ComponentUpdate;
use App\Framework\Random\SecureRandomGenerator;
use App\Framework\Security\CsrfTokenGenerator;
/**
* Test harness for LiveComponent testing
*
* Provides comprehensive helper methods for testing LiveComponents:
* - Component setup and lifecycle
* - Action execution with CSRF, Authorization, Validation
* - State assertions
* - Event assertions
* - User/Permission mocking
*
* Usage in Pest tests:
* ```php
* use Tests\Framework\LiveComponents\ComponentTestCase;
*
* uses(ComponentTestCase::class);
*
* it('executes action', function () {
* $component = $this->createComponent(CounterComponent::class);
* $result = $this->callAction($component, 'increment');
* $this->assertStateEquals($result, ['count' => 1]);
* });
* ```
*/
trait ComponentTestCase
{
protected Session $session;
protected LiveComponentHandler $handler;
protected ComponentEventDispatcher $eventDispatcher;
protected string $csrfToken;
/**
* Setup test environment
*
* Creates session, handler, and generates CSRF token.
* Call this in beforeEach() hook.
*/
protected function setUpComponentTest(): void
{
// Create session
$sessionId = SessionId::fromString(bin2hex(random_bytes(16)));
$clock = new SystemClock();
$randomGenerator = new SecureRandomGenerator();
$csrfGenerator = new CsrfTokenGenerator($randomGenerator);
$this->session = Session::fromArray($sessionId, $clock, $csrfGenerator, []);
// Create handler dependencies
$this->eventDispatcher = new ComponentEventDispatcher();
$authChecker = new SessionBasedAuthorizationChecker($this->session);
$stateValidator = new DefaultStateValidator();
$schemaCache = new SchemaCache();
$this->handler = new LiveComponentHandler(
$this->eventDispatcher,
$this->session,
$authChecker,
$stateValidator,
$schemaCache
);
}
/**
* Authenticate user with permissions
*
* @param array<string> $permissions User permissions
* @param int $userId User ID
*/
protected function actingAs(array $permissions = [], int $userId = 1): self
{
$this->session->set('user', [
'id' => $userId,
'permissions' => $permissions,
]);
return $this;
}
/**
* Call component action
*
* Automatically generates CSRF token for the component.
*
* @param LiveComponentContract $component Component instance
* @param string $method Action method name
* @param array<string, mixed> $params Action parameters
* @return ComponentUpdate Action result
*/
protected function callAction(
LiveComponentContract $component,
string $method,
array $params = []
): ComponentUpdate {
// Generate CSRF token for component
$formId = 'livecomponent:' . $component->getId()->toString();
$csrfToken = $this->session->csrf->generateToken($formId);
$actionParams = ActionParameters::fromArray($params, $csrfToken);
return $this->handler->handle($component, $method, $actionParams);
}
/**
* Assert action executes successfully
*
* @param LiveComponentContract $component Component instance
* @param string $method Action method name
* @param array<string, mixed> $params Action parameters
*/
protected function assertActionExecutes(
LiveComponentContract $component,
string $method,
array $params = []
): ComponentUpdate {
$result = $this->callAction($component, $method, $params);
expect($result)->toBeInstanceOf(ComponentUpdate::class);
return $result;
}
/**
* Assert action throws exception
*
* @param LiveComponentContract $component Component instance
* @param string $method Action method name
* @param string $exceptionClass Expected exception class
* @param array<string, mixed> $params Action parameters
*/
protected function assertActionThrows(
LiveComponentContract $component,
string $method,
string $exceptionClass,
array $params = []
): void {
$thrown = false;
$caughtException = null;
try {
$this->callAction($component, $method, $params);
} catch (\Throwable $e) {
$thrown = true;
$caughtException = $e;
}
if (! $thrown) {
throw new \AssertionError("Expected exception {$exceptionClass} to be thrown, but no exception was thrown");
}
if (! ($caughtException instanceof $exceptionClass)) {
throw new \AssertionError(
"Expected exception of type {$exceptionClass}, got " . get_class($caughtException)
);
}
}
/**
* Assert action requires authentication
*
* @param LiveComponentContract $component Component with protected action
* @param string $method Protected action method
*/
protected function assertActionRequiresAuth(
LiveComponentContract $component,
string $method
): void {
$this->assertActionThrows(
$component,
$method,
UnauthorizedActionException::class
);
}
/**
* Assert action requires permission
*
* @param LiveComponentContract $component Component with protected action
* @param string $method Protected action method
* @param array<string> $withoutPermissions User permissions (should fail)
*/
protected function assertActionRequiresPermission(
LiveComponentContract $component,
string $method,
array $withoutPermissions = []
): void {
$this->actingAs($withoutPermissions);
$this->assertActionThrows(
$component,
$method,
UnauthorizedActionException::class
);
}
/**
* Assert state equals expected values
*
* @param ComponentUpdate $result Action result
* @param array<string, mixed> $expected Expected state data
*/
protected function assertStateEquals(ComponentUpdate $result, array $expected): void
{
$actualState = $result->state->data;
foreach ($expected as $key => $value) {
expect($actualState)->toHaveKey($key);
expect($actualState[$key])->toBe($value);
}
}
/**
* Assert state has key
*
* @param ComponentUpdate $result Action result
* @param string $key State key
*/
protected function assertStateHas(ComponentUpdate $result, string $key): void
{
expect($result->state->data)->toHaveKey($key);
}
/**
* Assert state validates against schema
*
* @param ComponentUpdate $result Action result
*/
protected function assertStateValidates(ComponentUpdate $result): void
{
// If we got here without StateValidationException, validation passed
expect($result)->toBeInstanceOf(ComponentUpdate::class);
}
/**
* Assert event was dispatched
*
* @param ComponentUpdate $result Action result
* @param string $eventName Event name
*/
protected function assertEventDispatched(ComponentUpdate $result, string $eventName): void
{
$events = $result->events;
$found = false;
foreach ($events as $event) {
if ($event->name === $eventName) {
$found = true;
break;
}
}
expect($found)->toBeTrue("Event '{$eventName}' was not dispatched");
}
/**
* Assert no events were dispatched
*
* @param ComponentUpdate $result Action result
*/
protected function assertNoEventsDispatched(ComponentUpdate $result): void
{
expect($result->events)->toBeEmpty();
}
/**
* Assert event count
*
* @param ComponentUpdate $result Action result
* @param int $count Expected event count
*/
protected function assertEventCount(ComponentUpdate $result, int $count): void
{
expect($result->events)->toHaveCount($count);
}
/**
* Get state value from result
*
* @param ComponentUpdate $result Action result
* @param string $key State key
* @return mixed State value
*/
protected function getStateValue(ComponentUpdate $result, string $key): mixed
{
return $result->state->data[$key] ?? null;
}
}