'127.0.0.1', 'desk_moloni_redis_port' => 6379, 'desk_moloni_redis_password' => '', 'desk_moloni_redis_db' => 1, 'desk_moloni_sync_enabled' => '1', 'desk_moloni_sync_customers' => '1', 'desk_moloni_sync_invoices' => '1', 'desk_moloni_sync_products' => '1', 'desk_moloni_sync_estimates' => '1', 'desk_moloni_sync_payments' => '1', ]; return $options[$name] ?? $default; } } // These services don't have complex dependencies in their constructors yet $this->entity_mapping = new EntityMappingService(); $this->client_sync = new ClientSyncService(); $this->product_sync = new ProductSyncService(); $this->invoice_sync = new InvoiceSyncService(); $this->estimate_sync = new EstimateSyncService(); // Instantiate PerfexHooks, which will create the QueueProcessor with all its dependencies $this->perfex_hooks = new PerfexHooks(); // Get the QueueProcessor instance from the hooks class for test assertions $reflection = new \ReflectionClass($this->perfex_hooks); $queue_processor_property = $reflection->getProperty('queue_processor'); $queue_processor_property->setAccessible(true); $this->queue_processor = $queue_processor_property->getValue($this->perfex_hooks); // Clean up any existing test data $this->cleanupTestData(); } public function testCompleteCustomerSyncWorkflow() { // Test complete customer synchronization workflow // Step 1: Create customer in Perfex (simulates user action) $perfex_client_data = [ 'company' => 'Integration Test Company Ltd', 'vat' => 'PT999888777', 'email' => 'integration@test.com', 'phonenumber' => '+351999888777', 'billing_street' => 'Test Integration Street 123', 'billing_city' => 'Porto', 'billing_zip' => '4000-999', 'billing_country' => 'PT', 'active' => 1 ]; // Mock Perfex client creation $perfex_client_id = 9999; // Simulated ID // Step 2: Hook triggers queue job $job_id = $this->queue_processor->add_to_queue( EntityMappingService::ENTITY_CUSTOMER, $perfex_client_id, 'create', 'perfex_to_moloni', QueueProcessor::PRIORITY_NORMAL, ['trigger' => 'integration_test'] ); $this->assertNotFalse($job_id, 'Job should be queued successfully'); // Step 3: Process queue (simulates background processing) $result = $this->queue_processor->process_queue(1, 60); $this->assertEquals(1, $result['processed'], 'One job should be processed'); $this->assertEquals(1, $result['success'], 'Job should complete successfully'); $this->assertEquals(0, $result['errors'], 'No errors should occur'); // Step 4: Verify mapping was created $mapping = $this->entity_mapping->get_mapping_by_perfex_id( EntityMappingService::ENTITY_CUSTOMER, $perfex_client_id ); $this->assertNotNull($mapping, 'Entity mapping should be created'); $this->assertEquals(EntityMappingService::STATUS_SYNCED, $mapping->sync_status); $this->assertEquals(EntityMappingService::DIRECTION_PERFEX_TO_MOLONI, $mapping->sync_direction); return [ 'perfex_client_id' => $perfex_client_id, 'moloni_customer_id' => $mapping->moloni_id, 'mapping_id' => $mapping->id ]; } public function testCompleteInvoiceWorkflowWithDependencies() { // Test complete invoice sync with customer dependency // Step 1: Ensure customer exists and is synced $customer_data = $this->testCompleteCustomerSyncWorkflow(); // Step 2: Create invoice in Perfex $perfex_invoice_data = [ 'clientid' => $customer_data['perfex_client_id'], 'number' => 'INV-TEST-2024-001', 'date' => date('Y-m-d'), 'duedate' => date('Y-m-d', strtotime('+30 days')), 'subtotal' => 100.00, 'total_tax' => 23.00, 'total' => 123.00, 'status' => 1, // Draft 'currency' => 1 ]; $perfex_invoice_id = 8888; // Simulated ID // Step 3: Queue invoice sync $job_id = $this->queue_processor->add_to_queue( EntityMappingService::ENTITY_INVOICE, $perfex_invoice_id, 'create', 'perfex_to_moloni', QueueProcessor::PRIORITY_HIGH, ['trigger' => 'invoice_added'] ); $this->assertNotFalse($job_id, 'Invoice job should be queued'); // Step 4: Process queue $result = $this->queue_processor->process_queue(1, 60); $this->assertEquals(1, $result['processed'], 'Invoice job should be processed'); // Step 5: Verify invoice mapping $invoice_mapping = $this->entity_mapping->get_mapping_by_perfex_id( EntityMappingService::ENTITY_INVOICE, $perfex_invoice_id ); $this->assertNotNull($invoice_mapping, 'Invoice mapping should be created'); $this->assertEquals(EntityMappingService::STATUS_SYNCED, $invoice_mapping->sync_status); return [ 'perfex_invoice_id' => $perfex_invoice_id, 'moloni_invoice_id' => $invoice_mapping->moloni_id, 'customer_data' => $customer_data ]; } public function testBidirectionalSyncConflictResolution() { // Test conflict detection and resolution in bidirectional sync // Step 1: Create initial sync $customer_data = $this->testCompleteCustomerSyncWorkflow(); // Step 2: Simulate concurrent modifications // Update in Perfex $perfex_update_data = [ 'company' => 'Updated Company Name - Perfex', 'email' => 'updated.perfex@test.com' ]; // Update in Moloni (simulated) $moloni_update_data = [ 'name' => 'Updated Company Name - Moloni', 'email' => 'updated.moloni@test.com' ]; // Step 3: Trigger bidirectional sync $job_id = $this->queue_processor->add_to_queue( EntityMappingService::ENTITY_CUSTOMER, $customer_data['perfex_client_id'], 'update', 'bidirectional', QueueProcessor::PRIORITY_NORMAL, ['trigger' => 'conflict_test'] ); // Step 4: Process should detect conflict $result = $this->queue_processor->process_queue(1, 60); // Step 5: Verify conflict handling $mapping = $this->entity_mapping->get_mapping_by_perfex_id( EntityMappingService::ENTITY_CUSTOMER, $customer_data['perfex_client_id'] ); // Depending on conflict resolution strategy, mapping should be marked as conflict // or resolved according to the configured strategy $this->assertNotNull($mapping); // If manual resolution is configured, status should be CONFLICT if (get_option('desk_moloni_conflict_strategy', 'manual') === 'manual') { $this->assertEquals(EntityMappingService::STATUS_CONFLICT, $mapping->sync_status); } } public function testQueuePriorityAndOrdering() { // Test queue priority system and job ordering $jobs = []; // Add jobs with different priorities $jobs['low'] = $this->queue_processor->add_to_queue( EntityMappingService::ENTITY_PRODUCT, 1001, 'create', 'perfex_to_moloni', QueueProcessor::PRIORITY_LOW, ['test' => 'low_priority'] ); $jobs['critical'] = $this->queue_processor->add_to_queue( EntityMappingService::ENTITY_INVOICE, 1002, 'update', 'perfex_to_moloni', QueueProcessor::PRIORITY_CRITICAL, ['test' => 'critical_priority'] ); $jobs['normal'] = $this->queue_processor->add_to_queue( EntityMappingService::ENTITY_CUSTOMER, 1003, 'create', 'perfex_to_moloni', QueueProcessor::PRIORITY_NORMAL, ['test' => 'normal_priority'] ); $jobs['high'] = $this->queue_processor->add_to_queue( EntityMappingService::ENTITY_INVOICE, 1004, 'create', 'perfex_to_moloni', QueueProcessor::PRIORITY_HIGH, ['test' => 'high_priority'] ); // All jobs should be added successfully foreach ($jobs as $priority => $job_id) { $this->assertNotFalse($job_id, "Job with {$priority} priority should be queued"); } // Process all jobs $result = $this->queue_processor->process_queue(4, 120); $this->assertEquals(4, $result['processed'], 'All 4 jobs should be processed'); // Verify that high priority jobs were processed first // This would require additional tracking in the actual implementation $this->assertGreaterThan(0, $result['success']); } public function testRetryMechanismWithExponentialBackoff() { // Test retry mechanism for failed jobs // Create a job that will fail initially $job_id = $this->queue_processor->add_to_queue( EntityMappingService::ENTITY_CUSTOMER, 9998, // Non-existent customer to trigger failure 'create', 'perfex_to_moloni', QueueProcessor::PRIORITY_NORMAL, ['test' => 'retry_mechanism'] ); $this->assertNotFalse($job_id); // First processing attempt should fail and schedule retry $result = $this->queue_processor->process_queue(1, 30); $this->assertEquals(1, $result['processed']); $this->assertEquals(0, $result['success']); $this->assertEquals(1, $result['errors']); // Check queue statistics to verify retry was scheduled $stats = $this->queue_processor->get_queue_statistics(); $this->assertGreaterThan(0, $stats['delayed']); // Job should be in delay queue // Simulate time passing and process delayed jobs // In real scenario, this would be handled by cron job $delayed_result = $this->queue_processor->process_queue(1, 30); // Job should be attempted again $this->assertGreaterThanOrEqual(0, $delayed_result['processed']); } public function testBulkSynchronizationPerformance() { // Test bulk synchronization performance $start_time = microtime(true); $job_count = 10; $jobs = []; // Queue multiple jobs for ($i = 1; $i <= $job_count; $i++) { $jobs[] = $this->queue_processor->add_to_queue( EntityMappingService::ENTITY_CUSTOMER, 7000 + $i, 'create', 'perfex_to_moloni', QueueProcessor::PRIORITY_NORMAL, ['test' => 'bulk_sync', 'batch_id' => $i] ); } $queue_time = microtime(true) - $start_time; // All jobs should be queued successfully $this->assertCount($job_count, array_filter($jobs)); // Process all jobs in batch $process_start = microtime(true); $result = $this->queue_processor->process_queue($job_count, 300); $process_time = microtime(true) - $process_start; // Performance assertions $this->assertEquals($job_count, $result['processed']); $this->assertLessThan(5.0, $queue_time, 'Queuing should be fast'); $this->assertLessThan(30.0, $process_time, 'Processing should complete within reasonable time'); // Memory usage should be reasonable $stats = $this->queue_processor->get_queue_statistics(); $memory_mb = $stats['memory_usage'] / (1024 * 1024); $this->assertLessThan(100, $memory_mb, 'Memory usage should be under 100MB'); } public function testErrorHandlingAndLogging() { // Test comprehensive error handling and logging // Create job with invalid data to trigger errors $job_id = $this->queue_processor->add_to_queue( 'invalid_entity_type', // Invalid entity type 1234, 'create', 'perfex_to_moloni', QueueProcessor::PRIORITY_NORMAL, ['test' => 'error_handling'] ); // Job should not be created due to validation $this->assertFalse($job_id, 'Invalid job should not be queued'); // Create valid job but with non-existent entity $valid_job_id = $this->queue_processor->add_to_queue( EntityMappingService::ENTITY_CUSTOMER, 99999, // Non-existent customer 'create', 'perfex_to_moloni', QueueProcessor::PRIORITY_NORMAL, ['test' => 'error_handling'] ); $this->assertNotFalse($valid_job_id, 'Valid job structure should be queued'); // Process should handle error gracefully $result = $this->queue_processor->process_queue(1, 30); $this->assertEquals(1, $result['processed']); $this->assertEquals(0, $result['success']); $this->assertEquals(1, $result['errors']); // Error should be logged and job should be retried or moved to dead letter $stats = $this->queue_processor->get_queue_statistics(); $this->assertGreaterThanOrEqual(0, $stats['delayed'] + $stats['dead_letter']); } public function testWebhookIntegration() { // Test webhook integration from Moloni $webhook_data = [ 'entity_type' => EntityMappingService::ENTITY_CUSTOMER, 'entity_id' => 5555, // Moloni customer ID 'action' => 'update', 'event_type' => 'customer.updated', 'timestamp' => time(), 'data' => [ 'customer_id' => 5555, 'name' => 'Updated via Webhook', 'email' => 'webhook@test.com' ] ]; // Trigger webhook handler $this->perfex_hooks->handle_moloni_webhook($webhook_data); // Verify job was queued $stats = $this->queue_processor->get_queue_statistics(); $initial_queued = $stats['pending_main'] + $stats['pending_priority']; $this->assertGreaterThan(0, $initial_queued, 'Webhook should queue a job'); // Process the webhook job $result = $this->queue_processor->process_queue(1, 30); $this->assertGreaterThanOrEqual(1, $result['processed']); } public function testQueueHealthAndMonitoring() { // Test queue health monitoring // Get initial health status $health = $this->queue_processor->health_check(); $this->assertArrayHasKey('status', $health); $this->assertArrayHasKey('checks', $health); $this->assertArrayHasKey('redis', $health['checks']); $this->assertArrayHasKey('dead_letter', $health['checks']); $this->assertArrayHasKey('processing', $health['checks']); $this->assertArrayHasKey('memory', $health['checks']); // Test queue statistics $stats = $this->queue_processor->get_queue_statistics(); $this->assertArrayHasKey('pending_main', $stats); $this->assertArrayHasKey('pending_priority', $stats); $this->assertArrayHasKey('delayed', $stats); $this->assertArrayHasKey('processing', $stats); $this->assertArrayHasKey('dead_letter', $stats); $this->assertArrayHasKey('total_queued', $stats); $this->assertArrayHasKey('total_processed', $stats); $this->assertArrayHasKey('total_success', $stats); $this->assertArrayHasKey('total_errors', $stats); $this->assertArrayHasKey('success_rate', $stats); $this->assertArrayHasKey('memory_usage', $stats); // Success rate should be a valid percentage $this->assertGreaterThanOrEqual(0, $stats['success_rate']); $this->assertLessThanOrEqual(100, $stats['success_rate']); } private function cleanupTestData() { // Clean up any test data from previous runs // This would involve clearing test mappings, queue items, etc. if (ENVIRONMENT !== 'production') { // Only clear in non-production environments try { $this->queue_processor->clear_all_queues(); } catch (Exception $e) { // Queue might not be initialized yet, that's okay } } } protected function tearDown(): void { // Clean up after tests $this->cleanupTestData(); $this->client_sync = null; $this->product_sync = null; $this->invoice_sync = null; $this->estimate_sync = null; $this->queue_processor = null; $this->perfex_hooks = null; $this->entity_mapping = null; } }