Files
michaelschiemer/tests/Feature/LiveComponents/SecurityTest.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

241 lines
8.8 KiB
PHP

<?php
declare(strict_types=1);
/**
* LiveComponent Security Testing Examples
*
* Demonstrates testing security features:
* - CSRF protection
* - Rate limiting
* - Idempotency key validation
*/
use function Pest\LiveComponents\mountComponent;
use function Pest\LiveComponents\callAction;
describe('LiveComponent Security', function () {
describe('CSRF Protection', function () {
it('requires valid CSRF token for actions', function () {
$component = mountComponent('counter:test', ['count' => 0]);
// Without CSRF token should fail
expect(fn() => callAction($component, 'increment', [
'_csrf_token' => 'invalid_token'
]))->toThrow(\App\Framework\Exception\Security\CsrfTokenMismatchException::class);
});
it('accepts valid CSRF token', function () {
$component = mountComponent('counter:test', ['count' => 0]);
// Get valid CSRF token from session
$session = container()->get(\App\Framework\Http\Session::class);
$csrfToken = $session->getCsrfToken();
$result = callAction($component, 'increment', [
'_csrf_token' => $csrfToken
]);
expect($result['state']['count'])->toBe(1);
});
it('regenerates CSRF token after action', function () {
$component = mountComponent('counter:test', ['count' => 0]);
$session = container()->get(\App\Framework\Http\Session::class);
$oldToken = $session->getCsrfToken();
callAction($component, 'increment', [
'_csrf_token' => $oldToken
]);
$newToken = $session->getCsrfToken();
expect($newToken)->not->toBe($oldToken);
});
});
describe('Rate Limiting', function () {
it('enforces rate limits on actions', function () {
$component = mountComponent('counter:test', ['count' => 0]);
// Execute action multiple times rapidly
for ($i = 0; $i < 10; $i++) {
callAction($component, 'increment');
}
// 11th request should be rate limited
expect(fn() => callAction($component, 'increment'))
->toThrow(\App\Framework\Exception\Http\RateLimitExceededException::class);
});
it('includes retry-after header in rate limit response', function () {
$component = mountComponent('counter:test', ['count' => 0]);
// Exhaust rate limit
for ($i = 0; $i < 10; $i++) {
callAction($component, 'increment');
}
try {
callAction($component, 'increment');
} catch (\App\Framework\Exception\Http\RateLimitExceededException $e) {
expect($e->getRetryAfter())->toBeGreaterThan(0);
}
});
it('resets rate limit after cooldown period', function () {
$component = mountComponent('counter:test', ['count' => 0]);
// Exhaust rate limit
for ($i = 0; $i < 10; $i++) {
callAction($component, 'increment');
}
// Wait for cooldown (simulate with cache clear in tests)
$cache = container()->get(\App\Framework\Cache\Cache::class);
$cache->clear();
// Should work again after cooldown
$result = callAction($component, 'increment');
expect($result['state']['count'])->toBe(11);
});
});
describe('Idempotency Keys', function () {
it('prevents duplicate action execution with same idempotency key', function () {
$component = mountComponent('counter:test', ['count' => 0]);
$idempotencyKey = 'test-key-' . uniqid();
// First execution
$result1 = callAction($component, 'increment', [
'idempotency_key' => $idempotencyKey
]);
expect($result1['state']['count'])->toBe(1);
// Second execution with same key should return cached result
$result2 = callAction($result1, 'increment', [
'idempotency_key' => $idempotencyKey
]);
// Count should still be 1 (not 2) because action was not re-executed
expect($result2['state']['count'])->toBe(1);
});
it('allows different actions with different idempotency keys', function () {
$component = mountComponent('counter:test', ['count' => 0]);
$result1 = callAction($component, 'increment', [
'idempotency_key' => 'key-1'
]);
$result2 = callAction($result1, 'increment', [
'idempotency_key' => 'key-2'
]);
expect($result2['state']['count'])->toBe(2);
});
it('idempotency key expires after TTL', function () {
$component = mountComponent('counter:test', ['count' => 0]);
$idempotencyKey = 'test-key-expiry';
// First execution
callAction($component, 'increment', [
'idempotency_key' => $idempotencyKey
]);
// Simulate TTL expiry by clearing cache
$cache = container()->get(\App\Framework\Cache\Cache::class);
$cache->clear();
// Should execute again after expiry
$result = callAction($component, 'increment', [
'idempotency_key' => $idempotencyKey
]);
expect($result['state']['count'])->toBe(2);
});
it('includes idempotency metadata in response', function () {
$component = mountComponent('counter:test', ['count' => 0]);
$idempotencyKey = 'test-key-metadata';
$result = callAction($component, 'increment', [
'idempotency_key' => $idempotencyKey
]);
// Check for idempotency metadata (if implemented)
// This depends on your specific implementation
expect($result)->toHaveKey('idempotency');
expect($result['idempotency']['key'])->toBe($idempotencyKey);
expect($result['idempotency']['cached'])->toBeFalse();
});
});
describe('Combined Security Features', function () {
it('enforces all security layers together', function () {
$component = mountComponent('counter:test', ['count' => 0]);
$session = container()->get(\App\Framework\Http\Session::class);
$csrfToken = $session->getCsrfToken();
$idempotencyKey = 'combined-test-' . uniqid();
// Valid request with all security features
$result = callAction($component, 'increment', [
'_csrf_token' => $csrfToken,
'idempotency_key' => $idempotencyKey
]);
expect($result['state']['count'])->toBe(1);
// Retry with same idempotency key but new CSRF token
$newCsrfToken = $session->getCsrfToken();
$result2 = callAction($result, 'increment', [
'_csrf_token' => $newCsrfToken,
'idempotency_key' => $idempotencyKey
]);
// Should return cached result due to idempotency
expect($result2['state']['count'])->toBe(1);
});
it('validates security in correct order: CSRF -> Rate Limit -> Idempotency', function () {
$component = mountComponent('counter:test', ['count' => 0]);
// Invalid CSRF should fail before rate limit check
try {
callAction($component, 'increment', [
'_csrf_token' => 'invalid'
]);
expect(false)->toBeTrue('Should have thrown CSRF exception');
} catch (\Exception $e) {
expect($e)->toBeInstanceOf(\App\Framework\Exception\Security\CsrfTokenMismatchException::class);
}
// Rate limit should be checked before idempotency
$session = container()->get(\App\Framework\Http\Session::class);
$csrfToken = $session->getCsrfToken();
// Exhaust rate limit
for ($i = 0; $i < 10; $i++) {
callAction($component, 'increment', [
'_csrf_token' => $session->getCsrfToken()
]);
}
// Even with valid idempotency key, rate limit should trigger first
try {
callAction($component, 'increment', [
'_csrf_token' => $session->getCsrfToken(),
'idempotency_key' => 'test-key'
]);
expect(false)->toBeTrue('Should have thrown rate limit exception');
} catch (\Exception $e) {
expect($e)->toBeInstanceOf(\App\Framework\Exception\Http\RateLimitExceededException::class);
}
});
});
});