- 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.
312 lines
9.2 KiB
PHP
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;
|
|
}
|
|
}
|