- 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.
241 lines
8.8 KiB
PHP
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);
|
|
}
|
|
});
|
|
});
|
|
});
|