redis_mock = $this->createMock(Redis::class); $this->model_mock = $this->createMock(Desk_moloni_model::class); $this->entity_mapping_mock = $this->createMock(EntityMappingService::class); $this->error_handler_mock = $this->createMock(ErrorHandler::class); $this->retry_handler_mock = $this->createMock(RetryHandler::class); // Mock CodeIgniter instance $this->CI_mock = $this->createMock(stdClass::class); $this->CI_mock->desk_moloni_model = $this->model_mock; // Initialize service with DI $this->queue_processor = new QueueProcessor( $this->redis_mock, $this->model_mock, $this->entity_mapping_mock, $this->error_handler_mock, $this->retry_handler_mock ); } public function testAddToQueueSuccess() { // Test data $entity_type = EntityMappingService::ENTITY_CUSTOMER; $entity_id = 123; $action = 'create'; $direction = 'perfex_to_moloni'; $priority = QueueProcessor::PRIORITY_NORMAL; $data = ['trigger' => 'client_added']; // Mock Redis operations $this->redis_mock ->expects($this->once()) ->method('hExists') ->willReturn(false); // No duplicate job $this->redis_mock ->expects($this->once()) ->method('lPush') ->with( 'desk_moloni:queue:main', $this->callback(function($job_json) use ($entity_type, $entity_id, $action) { $job = json_decode($job_json, true); return $job['entity_type'] === $entity_type && $job['entity_id'] === $entity_id && $job['action'] === $action && $job['status'] === QueueProcessor::STATUS_PENDING; }) ); $this->redis_mock ->expects($this->once()) ->method('hSet') ->with('desk_moloni:queue:jobs', $this->isType('string'), $this->isType('string')); $this->redis_mock ->expects($this->exactly(2)) ->method('hIncrBy') ->withConsecutive( ['desk_moloni:queue:stats', 'total_queued', 1], ['desk_moloni:queue:stats', 'queued_customer', 1] ); // Execute test $job_id = $this->queue_processor->add_to_queue( $entity_type, $entity_id, $action, $direction, $priority, $data ); // Assertions $this->assertNotFalse($job_id); $this->assertStringContains($entity_type, $job_id); $this->assertStringContains((string)$entity_id, $job_id); $this->assertStringContains($action, $job_id); } public function testAddToQueueHighPriority() { // Test data for high priority job $entity_type = EntityMappingService::ENTITY_INVOICE; $entity_id = 456; $action = 'create'; $priority = QueueProcessor::PRIORITY_HIGH; // Mock Redis operations for priority queue $this->redis_mock ->expects($this->once()) ->method('hExists') ->willReturn(false); $this->redis_mock ->expects($this->once()) ->method('lPush') ->with('desk_moloni:queue:priority', $this->isType('string')); $this->redis_mock ->expects($this->once()) ->method('hSet'); $this->redis_mock ->expects($this->exactly(2)) ->method('hIncrBy'); // Execute test $job_id = $this->queue_processor->add_to_queue( $entity_type, $entity_id, $action, 'perfex_to_moloni', $priority ); // Assertions $this->assertNotFalse($job_id); } public function testAddToQueueWithDelay() { // Test data for delayed job $entity_type = EntityMappingService::ENTITY_PRODUCT; $entity_id = 789; $action = 'update'; $delay_seconds = 300; // 5 minutes // Mock Redis operations for delay queue $this->redis_mock ->expects($this->once()) ->method('hExists') ->willReturn(false); $this->redis_mock ->expects($this->once()) ->method('zAdd') ->with( 'desk_moloni:queue:delay', $this->callback(function($score) { return $score > time(); // Should be scheduled for future }), $this->isType('string') ); $this->redis_mock ->expects($this->once()) ->method('hSet'); $this->redis_mock ->expects($this->exactly(2)) ->method('hIncrBy'); // Execute test $job_id = $this->queue_processor->add_to_queue( $entity_type, $entity_id, $action, 'perfex_to_moloni', QueueProcessor::PRIORITY_NORMAL, [], $delay_seconds ); // Assertions $this->assertNotFalse($job_id); } public function testProcessQueueSuccess() { // Test data $job_data = [ 'id' => 'customer_123_create_test123', 'entity_type' => EntityMappingService::ENTITY_CUSTOMER, 'entity_id' => 123, 'action' => 'create', 'direction' => 'perfex_to_moloni', 'priority' => QueueProcessor::PRIORITY_NORMAL, 'attempts' => 0, 'max_attempts' => 5, 'status' => QueueProcessor::STATUS_PENDING ]; $job_json = json_encode($job_data); // Mock queue not paused $this->redis_mock ->expects($this->once()) ->method('get') ->with('desk_moloni:queue:paused') ->willReturn(null); // Mock delayed jobs processing $this->redis_mock ->expects($this->once()) ->method('zRangeByScore') ->willReturn([]); // Mock getting next job from priority queue (empty) then main queue $this->redis_mock ->expects($this->exactly(2)) ->method('rPop') ->withConsecutive( ['desk_moloni:queue:priority'], ['desk_moloni:queue:main'] ) ->willReturnOnConsecutiveCalls(null, $job_json); // Mock processing queue operations $this->redis_mock ->expects($this->once()) ->method('hSet') ->with('desk_moloni:queue:processing', $job_data['id'], $job_json); $this->redis_mock ->expects($this->once()) ->method('expire'); // Mock successful job execution $sync_service_mock = $this->createMock(stdClass::class); $sync_service_mock ->expects($this->once()) ->method('sync_perfex_to_moloni') ->with(123, false, []) ->willReturn([ 'success' => true, 'message' => 'Customer synced successfully' ]); // Mock job completion $this->redis_mock ->expects($this->once()) ->method('hDel') ->with('desk_moloni:queue:processing', $job_data['id']); // Mock statistics update $this->redis_mock ->expects($this->exactly(3)) ->method('hIncrBy') ->withConsecutive( ['desk_moloni:queue:stats', 'total_processed', 1], ['desk_moloni:queue:stats', 'total_success', 1], ['desk_moloni:queue:stats', 'total_errors', 0] ); // Use reflection to mock get_sync_service method $reflection = new ReflectionClass($this->queue_processor); $method = $reflection->getMethod('get_sync_service'); $method->setAccessible(true); // Execute test $result = $this->queue_processor->process_queue(1, 60); // Assertions $this->assertEquals(1, $result['processed']); $this->assertEquals(1, $result['success']); $this->assertEquals(0, $result['errors']); $this->assertArrayHasKey('execution_time', $result); $this->assertArrayHasKey('details', $result); } public function testProcessQueueWithRetry() { // Test data for failed job that should be retried $job_data = [ 'id' => 'customer_123_create_test123', 'entity_type' => EntityMappingService::ENTITY_CUSTOMER, 'entity_id' => 123, 'action' => 'create', 'direction' => 'perfex_to_moloni', 'priority' => QueueProcessor::PRIORITY_NORMAL, 'attempts' => 1, 'max_attempts' => 5, 'status' => QueueProcessor::STATUS_PENDING ]; $job_json = json_encode($job_data); // Mock queue processing setup $this->redis_mock ->expects($this->once()) ->method('get') ->willReturn(null); // Not paused $this->redis_mock ->expects($this->once()) ->method('zRangeByScore') ->willReturn([]); // No delayed jobs $this->redis_mock ->expects($this->exactly(2)) ->method('rPop') ->willReturnOnConsecutiveCalls(null, $job_json); $this->redis_mock ->expects($this->once()) ->method('hSet'); $this->redis_mock ->expects($this->once()) ->method('expire'); // Mock failed job execution $sync_service_mock = $this->createMock(stdClass::class); $sync_service_mock ->expects($this->once()) ->method('sync_perfex_to_moloni') ->willThrowException(new Exception('Temporary sync failure')); // Mock retry handler $this->retry_handler_mock ->expects($this->once()) ->method('calculate_retry_delay') ->with(2) // attempts + 1 ->willReturn(120); // 2 minutes // Mock scheduling retry $this->redis_mock ->expects($this->once()) ->method('hDel') ->with('desk_moloni:queue:processing', $job_data['id']); $this->redis_mock ->expects($this->once()) ->method('zAdd') ->with( 'desk_moloni:queue:delay', $this->callback(function($score) { return $score > time(); }), $this->isType('string') ); // Mock statistics $this->redis_mock ->expects($this->exactly(3)) ->method('hIncrBy'); // Execute test $result = $this->queue_processor->process_queue(1, 60); // Assertions $this->assertEquals(1, $result['processed']); $this->assertEquals(0, $result['success']); $this->assertEquals(1, $result['errors']); } public function testProcessQueueDeadLetter() { // Test data for job that has exceeded max attempts $job_data = [ 'id' => 'customer_123_create_test123', 'entity_type' => EntityMappingService::ENTITY_CUSTOMER, 'entity_id' => 123, 'action' => 'create', 'direction' => 'perfex_to_moloni', 'priority' => QueueProcessor::PRIORITY_NORMAL, 'attempts' => 5, // Max attempts reached 'max_attempts' => 5, 'status' => QueueProcessor::STATUS_PENDING ]; $job_json = json_encode($job_data); // Mock queue processing setup $this->redis_mock ->expects($this->once()) ->method('get') ->willReturn(null); $this->redis_mock ->expects($this->once()) ->method('zRangeByScore') ->willReturn([]); $this->redis_mock ->expects($this->exactly(2)) ->method('rPop') ->willReturnOnConsecutiveCalls(null, $job_json); $this->redis_mock ->expects($this->once()) ->method('hSet'); $this->redis_mock ->expects($this->once()) ->method('expire'); // Mock failed job execution $sync_service_mock = $this->createMock(stdClass::class); $sync_service_mock ->expects($this->once()) ->method('sync_perfex_to_moloni') ->willThrowException(new Exception('Permanent failure')); // Mock moving to dead letter queue $this->redis_mock ->expects($this->once()) ->method('hDel') ->with('desk_moloni:queue:processing', $job_data['id']); $this->redis_mock ->expects($this->once()) ->method('lPush') ->with('desk_moloni:queue:dead_letter', $this->isType('string')); // Mock error logging $this->error_handler_mock ->expects($this->once()) ->method('log_error') ->with('queue', 'JOB_DEAD_LETTER', $this->stringContains('moved to dead letter')); // Mock statistics $this->redis_mock ->expects($this->exactly(3)) ->method('hIncrBy'); // Execute test $result = $this->queue_processor->process_queue(1, 60); // Assertions $this->assertEquals(1, $result['processed']); $this->assertEquals(0, $result['success']); $this->assertEquals(1, $result['errors']); } public function testBidirectionalSyncWithConflict() { // Test data for bidirectional sync with conflict $job_data = [ 'id' => 'customer_123_update_test123', 'entity_type' => EntityMappingService::ENTITY_CUSTOMER, 'entity_id' => 123, 'action' => 'update', 'direction' => 'bidirectional', 'priority' => QueueProcessor::PRIORITY_NORMAL, 'attempts' => 0, 'max_attempts' => 5, 'status' => QueueProcessor::STATUS_PENDING ]; $mapping = (object)[ 'id' => 1, 'perfex_id' => 123, 'moloni_id' => 456, 'last_sync_perfex' => '2024-01-01 10:00:00', 'last_sync_moloni' => '2024-01-01 09:00:00' ]; // Mock getting mapping $this->entity_mapping_mock ->expects($this->once()) ->method('get_mapping_by_perfex_id') ->with(EntityMappingService::ENTITY_CUSTOMER, 123) ->willReturn($mapping); // Mock sync service with conflict detection $sync_service_mock = $this->createMock(stdClass::class); $sync_service_mock ->expects($this->once()) ->method('check_sync_conflicts') ->with($mapping) ->willReturn([ 'has_conflict' => true, 'conflict_details' => [ 'type' => 'data_conflict', 'field_conflicts' => ['company' => ['perfex_value' => 'A', 'moloni_value' => 'B']] ] ]); // Mock mapping status update $this->entity_mapping_mock ->expects($this->once()) ->method('update_mapping_status') ->with(1, EntityMappingService::STATUS_CONFLICT, $this->isType('string')); // Use reflection to test protected method $reflection = new ReflectionClass($this->queue_processor); $method = $reflection->getMethod('handle_bidirectional_sync'); $method->setAccessible(true); // Execute test $result = $method->invoke($this->queue_processor, $sync_service_mock, $job_data); // Assertions $this->assertFalse($result['success']); $this->assertStringContains('conflict', strtolower($result['message'])); $this->assertArrayHasKey('conflict_details', $result); } public function testGetQueueStatistics() { // Mock Redis statistics calls $stats_data = [ 'total_queued' => '100', 'total_processed' => '85', 'total_success' => '80', 'total_errors' => '5' ]; $this->redis_mock ->expects($this->once()) ->method('hGetAll') ->with('desk_moloni:queue:stats') ->willReturn($stats_data); $this->redis_mock ->expects($this->exactly(5)) ->method('lLen') ->willReturnOnConsecutiveCalls(10, 5, 3, 2, 1); // main, priority, delay, processing, dead_letter $this->redis_mock ->expects($this->once()) ->method('zCard') ->willReturn(3); // delayed jobs $this->redis_mock ->expects($this->once()) ->method('hLen') ->willReturn(2); // processing jobs // Execute test $statistics = $this->queue_processor->get_queue_statistics(); // Assertions $this->assertEquals(10, $statistics['pending_main']); $this->assertEquals(5, $statistics['pending_priority']); $this->assertEquals(3, $statistics['delayed']); $this->assertEquals(2, $statistics['processing']); $this->assertEquals(1, $statistics['dead_letter']); $this->assertEquals(100, $statistics['total_queued']); $this->assertEquals(85, $statistics['total_processed']); $this->assertEquals(80, $statistics['total_success']); $this->assertEquals(5, $statistics['total_errors']); $this->assertEquals(94.12, $statistics['success_rate']); // 80/85 * 100 $this->assertArrayHasKey('memory_usage', $statistics); $this->assertArrayHasKey('peak_memory', $statistics); } public function testHealthCheck() { // Mock Redis ping success $this->redis_mock ->expects($this->once()) ->method('ping') ->willReturn(true); // Mock queue statistics for health check $this->redis_mock ->expects($this->once()) ->method('hGetAll') ->willReturn([]); $this->redis_mock ->expects($this->exactly(5)) ->method('lLen') ->willReturnOnConsecutiveCalls(10, 5, 3, 2, 50); // dead_letter count triggers warning $this->redis_mock ->expects($this->once()) ->method('zCard') ->willReturn(3); $this->redis_mock ->expects($this->once()) ->method('hLen') ->willReturn(2); // Execute test $health = $this->queue_processor->health_check(); // Assertions $this->assertEquals('warning', $health['status']); // Due to high dead letter count $this->assertEquals('ok', $health['checks']['redis']); $this->assertStringContains('high count: 50', $health['checks']['dead_letter']); $this->assertEquals('ok', $health['checks']['processing']); $this->assertEquals('ok', $health['checks']['memory']); } protected function tearDown(): void { // Clean up test artifacts $this->queue_processor = null; $this->redis_mock = null; $this->entity_mapping_mock = null; $this->error_handler_mock = null; $this->retry_handler_mock = null; $this->CI_mock = null; } }