Files
michaelschiemer/tests/Framework/Queue/QueueServiceRegistrationTest.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

690 lines
24 KiB
PHP

<?php
declare(strict_types=1);
use App\Framework\Database\EntityManagerInterface;
use App\Framework\DI\Container;
use App\Framework\DI\DefaultContainer;
use App\Framework\Logging\Logger;
use App\Framework\Queue\Contracts\DeadLetterQueueInterface;
// Queue service interfaces
use App\Framework\Queue\Contracts\JobChainManagerInterface;
use App\Framework\Queue\Contracts\JobDependencyManagerInterface;
use App\Framework\Queue\Contracts\JobProgressTrackerInterface;
use App\Framework\Queue\Interfaces\DistributedLockInterface;
use App\Framework\Queue\Queue;
use App\Framework\Queue\QueueDependencyInitializer;
// Concrete implementations
use App\Framework\Queue\QueueInitializer;
use App\Framework\Queue\Services\DatabaseDeadLetterQueue;
use App\Framework\Queue\Services\DatabaseDistributedLock;
use App\Framework\Queue\Services\DatabaseJobChainManager;
use App\Framework\Queue\Services\DatabaseJobDependencyManager;
use App\Framework\Queue\Services\DatabaseJobProgressTracker;
// Additional services
use App\Framework\Queue\Services\DependencyResolutionEngine;
use App\Framework\Queue\Services\FailoverRecoveryService;
use App\Framework\Queue\Services\JobDistributionService;
use App\Framework\Queue\Services\JobMetricsManager;
use App\Framework\Queue\Services\JobMetricsManagerInterface;
// Framework dependencies
use App\Framework\Queue\Services\WorkerHealthCheckService;
use App\Framework\Queue\Services\WorkerRegistry;
describe('Queue Service Registration', function () {
beforeEach(function () {
$this->container = new DefaultContainer();
// Mock essential framework dependencies
$this->mockEntityManager = new class () implements EntityManagerInterface {
public function persist(object $entity): void
{
}
public function find(string $className, mixed $id): ?object
{
return null;
}
public function flush(): void
{
}
public function remove(object $entity): void
{
}
public function clear(): void
{
}
public function detach(object $entity): void
{
}
public function contains(object $entity): bool
{
return false;
}
public function refresh(object $entity): void
{
}
public function createQueryBuilder(): object
{
return new stdClass();
}
public function getRepository(string $className): object
{
return new stdClass();
}
public function beginTransaction(): void
{
}
public function commit(): void
{
}
public function rollback(): void
{
}
public function isTransactionActive(): bool
{
return false;
}
};
$this->mockLogger = new class () implements Logger {
public function emergency(string $message, array $context = []): void
{
}
public function alert(string $message, array $context = []): void
{
}
public function critical(string $message, array $context = []): void
{
}
public function error(string $message, array $context = []): void
{
}
public function warning(string $message, array $context = []): void
{
}
public function notice(string $message, array $context = []): void
{
}
public function info(string $message, array $context = []): void
{
}
public function debug(string $message, array $context = []): void
{
}
public function log(string $level, string $message, array $context = []): void
{
}
};
// Register mocked dependencies
$this->container->instance(EntityManagerInterface::class, $this->mockEntityManager);
$this->container->instance(Logger::class, $this->mockLogger);
});
describe('Core Queue Service', function () {
it('registers Queue service correctly', function () {
// This test verifies that the QueueInitializer properly registers a Queue
// Note: This will fallback to FileQueue since Redis is not available in tests
$queueInitializer = new QueueInitializer(
pathProvider: new class () {
public function resolvePath(string $path): string
{
return '/home/michael/dev/michaelschiemer/tests/tmp/queue/';
}
}
);
$queue = $queueInitializer($this->mockLogger);
expect($queue)->toBeInstanceOf(Queue::class);
expect($queue)->not->toBeNull();
});
it('Queue service is accessible from container after registration', function () {
// Register queue manually for testing
$this->container->singleton(Queue::class, function () {
return new \App\Framework\Queue\InMemoryQueue();
});
$queue = $this->container->get(Queue::class);
expect($queue)->toBeInstanceOf(Queue::class);
});
});
describe('Queue Dependencies Registration', function () {
beforeEach(function () {
// Initialize the queue dependency system
$this->dependencyInitializer = new QueueDependencyInitializer();
// Register a basic queue interface for the dependencies
$this->container->singleton(\App\Framework\Queue\Contracts\QueueInterface::class, function () {
return new class () implements \App\Framework\Queue\Contracts\QueueInterface {
public function push(mixed $job): void
{
}
public function pop(): mixed
{
return null;
}
public function size(): int
{
return 0;
}
};
});
// Register EventDispatcher mock
$this->container->singleton(\App\Framework\Core\Events\EventDispatcherInterface::class, function () {
return new class () implements \App\Framework\Core\Events\EventDispatcherInterface {
public function dispatch(object $event): void
{
}
public function listen(string $event, callable $listener): void
{
}
};
});
});
it('registers JobDependencyManagerInterface', function () {
$dependencyManager = $this->dependencyInitializer->__invoke($this->container);
expect($dependencyManager)->toBeInstanceOf(JobDependencyManagerInterface::class);
expect($dependencyManager)->toBeInstanceOf(DatabaseJobDependencyManager::class);
// Should be accessible from container
$retrieved = $this->container->get(JobDependencyManagerInterface::class);
expect($retrieved)->toBe($dependencyManager);
});
it('registers JobChainManagerInterface', function () {
$this->dependencyInitializer->__invoke($this->container);
$chainManager = $this->container->get(JobChainManagerInterface::class);
expect($chainManager)->toBeInstanceOf(JobChainManagerInterface::class);
expect($chainManager)->toBeInstanceOf(DatabaseJobChainManager::class);
});
it('registers DependencyResolutionEngine as singleton', function () {
$this->dependencyInitializer->__invoke($this->container);
$engine1 = $this->container->get(DependencyResolutionEngine::class);
$engine2 = $this->container->get(DependencyResolutionEngine::class);
expect($engine1)->toBeInstanceOf(DependencyResolutionEngine::class);
expect($engine1)->toBe($engine2); // Should be same instance (singleton)
});
it('registers JobMetricsManager as singleton', function () {
$this->dependencyInitializer->__invoke($this->container);
$metrics1 = $this->container->get(JobMetricsManager::class);
$metrics2 = $this->container->get(JobMetricsManager::class);
expect($metrics1)->toBeInstanceOf(JobMetricsManager::class);
expect($metrics1)->toBe($metrics2); // Should be same instance (singleton)
});
});
describe('Individual Service Registration', function () {
it('can register DistributedLockInterface service', function () {
$lockService = new DatabaseDistributedLock(
entityManager: $this->mockEntityManager,
logger: $this->mockLogger
);
$this->container->singleton(DistributedLockInterface::class, $lockService);
$retrieved = $this->container->get(DistributedLockInterface::class);
expect($retrieved)->toBe($lockService);
expect($retrieved)->toBeInstanceOf(DistributedLockInterface::class);
});
it('can register JobProgressTrackerInterface service', function () {
$progressTracker = new DatabaseJobProgressTracker(
entityManager: $this->mockEntityManager,
logger: $this->mockLogger
);
$this->container->singleton(JobProgressTrackerInterface::class, $progressTracker);
$retrieved = $this->container->get(JobProgressTrackerInterface::class);
expect($retrieved)->toBe($progressTracker);
expect($retrieved)->toBeInstanceOf(JobProgressTrackerInterface::class);
});
it('can register DeadLetterQueueInterface service', function () {
$deadLetterQueue = new DatabaseDeadLetterQueue(
entityManager: $this->mockEntityManager,
logger: $this->mockLogger
);
$this->container->singleton(DeadLetterQueueInterface::class, $deadLetterQueue);
$retrieved = $this->container->get(DeadLetterQueueInterface::class);
expect($retrieved)->toBe($deadLetterQueue);
expect($retrieved)->toBeInstanceOf(DeadLetterQueueInterface::class);
});
it('can register WorkerRegistry service', function () {
$workerRegistry = new WorkerRegistry(
entityManager: $this->mockEntityManager,
logger: $this->mockLogger
);
$this->container->singleton(WorkerRegistry::class, $workerRegistry);
$retrieved = $this->container->get(WorkerRegistry::class);
expect($retrieved)->toBe($workerRegistry);
expect($retrieved)->toBeInstanceOf(WorkerRegistry::class);
});
it('can register JobDistributionService', function () {
// First register dependencies
$this->container->singleton(WorkerRegistry::class, new WorkerRegistry(
$this->mockEntityManager,
$this->mockLogger
));
$this->container->singleton(\App\Framework\Queue\Contracts\QueueInterface::class, function () {
return new class () implements \App\Framework\Queue\Contracts\QueueInterface {
public function push(mixed $job): void
{
}
public function pop(): mixed
{
return null;
}
public function size(): int
{
return 0;
}
};
});
$distributionService = new JobDistributionService(
workerRegistry: $this->container->get(WorkerRegistry::class),
queue: $this->container->get(\App\Framework\Queue\Contracts\QueueInterface::class),
logger: $this->mockLogger
);
$this->container->singleton(JobDistributionService::class, $distributionService);
$retrieved = $this->container->get(JobDistributionService::class);
expect($retrieved)->toBe($distributionService);
expect($retrieved)->toBeInstanceOf(JobDistributionService::class);
});
});
describe('Service Dependencies and Integration', function () {
it('services have proper dependencies injected', function () {
$this->dependencyInitializer->__invoke($this->container);
$dependencyManager = $this->container->get(JobDependencyManagerInterface::class);
$chainManager = $this->container->get(JobChainManagerInterface::class);
$resolutionEngine = $this->container->get(DependencyResolutionEngine::class);
// Verify dependencies are properly injected
expect($dependencyManager)->toBeInstanceOf(DatabaseJobDependencyManager::class);
expect($chainManager)->toBeInstanceOf(DatabaseJobChainManager::class);
expect($resolutionEngine)->toBeInstanceOf(DependencyResolutionEngine::class);
// These services should be functional (not throw errors)
expect(fn () => $dependencyManager)->not->toThrow();
expect(fn () => $chainManager)->not->toThrow();
expect(fn () => $resolutionEngine)->not->toThrow();
});
it('can resolve complex dependency graph', function () {
$this->dependencyInitializer->__invoke($this->container);
// Add additional services
$this->container->singleton(DistributedLockInterface::class, function () {
return new DatabaseDistributedLock(
$this->mockEntityManager,
$this->mockLogger
);
});
$this->container->singleton(JobProgressTrackerInterface::class, function () {
return new DatabaseJobProgressTracker(
$this->mockEntityManager,
$this->mockLogger
);
});
// All services should be resolvable
$services = [
JobDependencyManagerInterface::class,
JobChainManagerInterface::class,
DependencyResolutionEngine::class,
JobMetricsManager::class,
DistributedLockInterface::class,
JobProgressTrackerInterface::class,
];
foreach ($services as $serviceInterface) {
$service = $this->container->get($serviceInterface);
expect($service)->not->toBeNull();
expect($service)->toBeObject();
}
});
});
describe('Service Lifecycle Management', function () {
it('singleton services maintain state across requests', function () {
$this->dependencyInitializer->__invoke($this->container);
$metrics1 = $this->container->get(JobMetricsManager::class);
$metrics2 = $this->container->get(JobMetricsManager::class);
// Should be exact same instance
expect($metrics1)->toBe($metrics2);
});
it('services can be replaced for testing', function () {
$this->dependencyInitializer->__invoke($this->container);
// Get original service
$original = $this->container->get(JobMetricsManager::class);
// Create mock replacement
$mock = new class () implements JobMetricsManagerInterface {
public function recordJobExecution(\App\Framework\Queue\ValueObjects\JobId $jobId, float $executionTime): void
{
}
public function recordJobFailure(\App\Framework\Queue\ValueObjects\JobId $jobId, string $errorMessage): void
{
}
public function getJobMetrics(\App\Framework\Queue\ValueObjects\JobId $jobId): ?\App\Framework\Queue\ValueObjects\JobMetrics
{
return null;
}
public function getQueueMetrics(\App\Framework\Queue\ValueObjects\QueueName $queueName): \App\Framework\Queue\ValueObjects\QueueMetrics
{
return new \App\Framework\Queue\ValueObjects\QueueMetrics(
queueName: $queueName,
totalJobs: 0,
completedJobs: 0,
failedJobs: 0,
averageExecutionTime: 0.0
);
}
public function getSystemMetrics(): array
{
return [];
}
};
// Replace with mock
$this->container->instance(JobMetricsManagerInterface::class, $mock);
$replaced = $this->container->get(JobMetricsManagerInterface::class);
expect($replaced)->toBe($mock);
expect($replaced)->not->toBe($original);
});
it('handles missing dependencies gracefully', function () {
// Don't register EventDispatcher - this should cause failure
unset($this->container);
$this->container = new DefaultContainer();
$this->container->instance(EntityManagerInterface::class, $this->mockEntityManager);
$this->container->instance(Logger::class, $this->mockLogger);
$dependencyInitializer = new QueueDependencyInitializer();
// This should fail due to missing dependencies
expect(fn () => $dependencyInitializer->__invoke($this->container))
->toThrow();
});
});
});
describe('Queue Service Integration Test', function () {
beforeEach(function () {
$this->container = new DefaultContainer();
// Register all required mocks
$this->container->instance(EntityManagerInterface::class, new class () implements EntityManagerInterface {
public function persist(object $entity): void
{
}
public function find(string $className, mixed $id): ?object
{
return null;
}
public function flush(): void
{
}
public function remove(object $entity): void
{
}
public function clear(): void
{
}
public function detach(object $entity): void
{
}
public function contains(object $entity): bool
{
return false;
}
public function refresh(object $entity): void
{
}
public function createQueryBuilder(): object
{
return new stdClass();
}
public function getRepository(string $className): object
{
return new stdClass();
}
public function beginTransaction(): void
{
}
public function commit(): void
{
}
public function rollback(): void
{
}
public function isTransactionActive(): bool
{
return false;
}
});
$this->container->instance(Logger::class, new class () implements Logger {
public function emergency(string $message, array $context = []): void
{
}
public function alert(string $message, array $context = []): void
{
}
public function critical(string $message, array $context = []): void
{
}
public function error(string $message, array $context = []): void
{
}
public function warning(string $message, array $context = []): void
{
}
public function notice(string $message, array $context = []): void
{
}
public function info(string $message, array $context = []): void
{
}
public function debug(string $message, array $context = []): void
{
}
public function log(string $level, string $message, array $context = []): void
{
}
});
$this->container->singleton(\App\Framework\Queue\Contracts\QueueInterface::class, function () {
return new class () implements \App\Framework\Queue\Contracts\QueueInterface {
public function push(mixed $job): void
{
}
public function pop(): mixed
{
return null;
}
public function size(): int
{
return 0;
}
};
});
$this->container->singleton(\App\Framework\Core\Events\EventDispatcherInterface::class, function () {
return new class () implements \App\Framework\Core\Events\EventDispatcherInterface {
public function dispatch(object $event): void
{
}
public function listen(string $event, callable $listener): void
{
}
};
});
});
it('can initialize complete queue system', function () {
// Initialize all queue services
$dependencyInitializer = new QueueDependencyInitializer();
$dependencyInitializer->__invoke($this->container);
// Register additional services that would normally be auto-registered
$this->container->singleton(DistributedLockInterface::class, function ($container) {
return new DatabaseDistributedLock(
$container->get(EntityManagerInterface::class),
$container->get(Logger::class)
);
});
$this->container->singleton(JobProgressTrackerInterface::class, function ($container) {
return new DatabaseJobProgressTracker(
$container->get(EntityManagerInterface::class),
$container->get(Logger::class)
);
});
$this->container->singleton(DeadLetterQueueInterface::class, function ($container) {
return new DatabaseDeadLetterQueue(
$container->get(EntityManagerInterface::class),
$container->get(Logger::class)
);
});
$this->container->singleton(WorkerRegistry::class, function ($container) {
return new WorkerRegistry(
$container->get(EntityManagerInterface::class),
$container->get(Logger::class)
);
});
// Verify all 9 expected queue services are registered
$expectedServices = [
DistributedLockInterface::class,
WorkerRegistry::class,
JobDistributionService::class, // This might not be auto-registered
WorkerHealthCheckService::class, // This might not be auto-registered
FailoverRecoveryService::class, // This might not be auto-registered
JobProgressTrackerInterface::class,
DeadLetterQueueInterface::class,
JobMetricsManagerInterface::class,
JobDependencyManagerInterface::class,
];
$registeredCount = 0;
foreach ($expectedServices as $service) {
try {
$instance = $this->container->get($service);
if ($instance !== null) {
$registeredCount++;
}
} catch (\Exception $e) {
// Service not registered, which is expected for some
}
}
// At least the core services should be registered
expect($registeredCount)->toBeGreaterThan(4);
});
it('services can interact without errors', function () {
$dependencyInitializer = new QueueDependencyInitializer();
$dependencyInitializer->__invoke($this->container);
$dependencyManager = $this->container->get(JobDependencyManagerInterface::class);
$chainManager = $this->container->get(JobChainManagerInterface::class);
$metricsManager = $this->container->get(JobMetricsManager::class);
// Basic interaction tests (should not throw)
expect(fn () => $dependencyManager)->not->toThrow();
expect(fn () => $chainManager)->not->toThrow();
expect(fn () => $metricsManager)->not->toThrow();
});
});