ci_mock = $this->createMock(stdClass::class); $this->api_client_mock = $this->createMock(stdClass::class); $this->mapping_model_mock = $this->createMock(stdClass::class); $this->sync_log_model_mock = $this->createMock(stdClass::class); $this->clients_model_mock = $this->createMock(stdClass::class); // Setup CI mock $this->ci_mock->load = $this->createMock(stdClass::class); $this->ci_mock->moloni_api_client = $this->api_client_mock; $this->ci_mock->mapping_model = $this->mapping_model_mock; $this->ci_mock->sync_log_model = $this->sync_log_model_mock; $this->ci_mock->clients_model = $this->clients_model_mock; // Mock get_instance function if (!function_exists('get_instance')) { function get_instance() { return $GLOBALS['CI_INSTANCE']; } } $GLOBALS['CI_INSTANCE'] = $this->ci_mock; // Create ClientSyncService instance require_once 'modules/desk_moloni/libraries/ClientSyncService.php'; $this->sync_service = new ClientSyncService(); } #[Test] #[Group('unit')] public function testServiceInitialization(): void { $this->assertInstanceOf(ClientSyncService::class, $this->sync_service); // Test default configuration $reflection = new ReflectionClass($this->sync_service); $batch_size_property = $reflection->getProperty('batch_size'); $batch_size_property->setAccessible(true); $this->assertEquals(50, $batch_size_property->getValue($this->sync_service)); $sync_direction_property = $reflection->getProperty('sync_direction'); $sync_direction_property->setAccessible(true); $this->assertEquals('bidirectional', $sync_direction_property->getValue($this->sync_service)); } #[Test] #[Group('unit')] public function testSyncPerfexClientToMoloni(): void { // Mock client data $perfex_client = [ 'userid' => '123', 'company' => 'Test Sync Company', 'email' => 'sync@test.com', 'vat' => 'PT123456789' ]; // Mock API response $moloni_response = [ 'valid' => 1, 'data' => [ 'customer_id' => '999' ] ]; // Setup expectations $this->clients_model_mock ->expects($this->once()) ->method('get') ->with(123) ->willReturn((object)$perfex_client); $this->api_client_mock ->expects($this->once()) ->method('create_customer') ->willReturn($moloni_response); $this->mapping_model_mock ->expects($this->once()) ->method('create_mapping') ->with( 'customer', 123, '999', 'perfex_to_moloni' ); // Execute sync $result = $this->sync_service->sync_client_to_moloni(123); $this->assertTrue($result['success']); $this->assertEquals('999', $result['moloni_id']); } #[Test] #[Group('unit')] public function testSyncMoloniCustomerToPerfex(): void { // Mock Moloni customer data $moloni_customer = [ 'customer_id' => '888', 'name' => 'Moloni Test Customer', 'email' => 'molonitest@example.com', 'vat' => 'PT987654321' ]; // Mock Perfex creation response $perfex_client_id = 456; // Setup expectations $this->api_client_mock ->expects($this->once()) ->method('get_customer') ->with('888') ->willReturn([ 'valid' => 1, 'data' => $moloni_customer ]); $this->clients_model_mock ->expects($this->once()) ->method('add') ->willReturn($perfex_client_id); $this->mapping_model_mock ->expects($this->once()) ->method('create_mapping') ->with( 'customer', $perfex_client_id, '888', 'moloni_to_perfex' ); // Execute sync $result = $this->sync_service->sync_moloni_customer_to_perfex('888'); $this->assertTrue($result['success']); $this->assertEquals($perfex_client_id, $result['perfex_id']); } #[Test] #[Group('unit')] public function testBidirectionalSync(): void { $perfex_client_id = 789; $moloni_customer_id = '777'; // Mock existing mapping $existing_mapping = (object)[ 'id' => 1, 'perfex_id' => $perfex_client_id, 'moloni_id' => $moloni_customer_id, 'sync_direction' => 'bidirectional', 'last_sync_at' => date('Y-m-d H:i:s', strtotime('-1 hour')) ]; // Mock updated data on both sides $perfex_client = [ 'userid' => $perfex_client_id, 'company' => 'Updated Company Name', 'updated_at' => date('Y-m-d H:i:s', strtotime('-30 minutes')) ]; $moloni_customer = [ 'customer_id' => $moloni_customer_id, 'name' => 'Different Updated Name', 'updated_at' => date('Y-m-d H:i:s', strtotime('-15 minutes')) ]; // Setup expectations $this->mapping_model_mock ->expects($this->once()) ->method('get_mapping') ->willReturn($existing_mapping); $this->clients_model_mock ->expects($this->once()) ->method('get') ->willReturn((object)$perfex_client); $this->api_client_mock ->expects($this->once()) ->method('get_customer') ->willReturn([ 'valid' => 1, 'data' => $moloni_customer ]); // Execute bidirectional sync $result = $this->sync_service->bidirectional_sync($perfex_client_id, $moloni_customer_id); $this->assertIsArray($result); $this->assertArrayHasKey('success', $result); } #[Test] #[Group('unit')] public function testConflictDetection(): void { $perfex_data = [ 'company' => 'Perfex Company Name', 'email' => 'perfex@company.com', 'updated_at' => date('Y-m-d H:i:s', strtotime('-10 minutes')) ]; $moloni_data = [ 'name' => 'Moloni Company Name', 'email' => 'moloni@company.com', 'updated_at' => date('Y-m-d H:i:s', strtotime('-5 minutes')) ]; $result = $this->sync_service->detect_conflicts($perfex_data, $moloni_data); $this->assertTrue($result['has_conflicts']); $this->assertContains('company', $result['conflicted_fields']); $this->assertContains('email', $result['conflicted_fields']); } #[Test] #[Group('unit')] #[DataProvider('conflictResolutionProvider')] public function testConflictResolution(string $strategy, array $perfex_data, array $moloni_data, string $expected_winner): void { // Set conflict resolution strategy $reflection = new ReflectionClass($this->sync_service); $conflict_property = $reflection->getProperty('conflict_resolution'); $conflict_property->setAccessible(true); $conflict_property->setValue($this->sync_service, $strategy); $result = $this->sync_service->resolve_conflict($perfex_data, $moloni_data, ['company']); $this->assertEquals($expected_winner, $result['winner']); } public static function conflictResolutionProvider(): array { return [ 'Last modified wins - Perfex newer' => [ 'last_modified_wins', ['company' => 'Perfex Name', 'updated_at' => date('Y-m-d H:i:s')], ['name' => 'Moloni Name', 'updated_at' => date('Y-m-d H:i:s', strtotime('-1 hour'))], 'perfex' ], 'Last modified wins - Moloni newer' => [ 'last_modified_wins', ['company' => 'Perfex Name', 'updated_at' => date('Y-m-d H:i:s', strtotime('-1 hour'))], ['name' => 'Moloni Name', 'updated_at' => date('Y-m-d H:i:s')], 'moloni' ], 'Perfex wins strategy' => [ 'perfex_wins', ['company' => 'Perfex Name'], ['name' => 'Moloni Name'], 'perfex' ], 'Moloni wins strategy' => [ 'moloni_wins', ['company' => 'Perfex Name'], ['name' => 'Moloni Name'], 'moloni' ] ]; } #[Test] #[Group('unit')] public function testBatchSynchronization(): void { $client_ids = [100, 101, 102, 103, 104]; // Mock batch processing $this->clients_model_mock ->expects($this->exactly(count($client_ids))) ->method('get') ->willReturnOnConsecutiveCalls( (object)['userid' => 100, 'company' => 'Company 1'], (object)['userid' => 101, 'company' => 'Company 2'], (object)['userid' => 102, 'company' => 'Company 3'], (object)['userid' => 103, 'company' => 'Company 4'], (object)['userid' => 104, 'company' => 'Company 5'] ); $this->api_client_mock ->expects($this->exactly(count($client_ids))) ->method('create_customer') ->willReturn(['valid' => 1, 'data' => ['customer_id' => '999']]); $result = $this->sync_service->batch_sync_clients_to_moloni($client_ids); $this->assertEquals(count($client_ids), $result['total']); $this->assertEquals(count($client_ids), $result['success_count']); $this->assertEquals(0, $result['error_count']); } #[Test] #[Group('unit')] public function testSyncWithApiFailure(): void { $perfex_client = [ 'userid' => '999', 'company' => 'Test Company', 'email' => 'test@company.com' ]; // Mock API failure $this->clients_model_mock ->expects($this->once()) ->method('get') ->willReturn((object)$perfex_client); $this->api_client_mock ->expects($this->once()) ->method('create_customer') ->willReturn(['valid' => 0, 'errors' => ['API Error']]); // Should log error but not throw exception $this->sync_log_model_mock ->expects($this->once()) ->method('log_sync_attempt'); $result = $this->sync_service->sync_client_to_moloni(999); $this->assertFalse($result['success']); $this->assertArrayHasKey('error', $result); } #[Test] #[Group('unit')] public function testSyncProgressTracking(): void { $batch_size = 3; $total_clients = 10; // Set smaller batch size for testing $reflection = new ReflectionClass($this->sync_service); $batch_property = $reflection->getProperty('batch_size'); $batch_property->setAccessible(true); $batch_property->setValue($this->sync_service, $batch_size); $progress_callback = function($current, $total, $status) { $this->assertIsInt($current); $this->assertEquals(10, $total); $this->assertIsString($status); }; // Mock clients $client_ids = range(1, $total_clients); // This would test actual progress tracking implementation $this->assertTrue(true); // Placeholder } #[Test] #[Group('unit')] public function testValidateClientData(): void { $valid_client = [ 'company' => 'Valid Company', 'email' => 'valid@email.com', 'vat' => 'PT123456789' ]; $invalid_client = [ 'company' => '', 'email' => 'invalid-email', 'vat' => '' ]; $valid_result = $this->sync_service->validate_client_data($valid_client); $this->assertTrue($valid_result['is_valid']); $invalid_result = $this->sync_service->validate_client_data($invalid_client); $this->assertFalse($invalid_result['is_valid']); $this->assertNotEmpty($invalid_result['errors']); } #[Test] #[Group('unit')] public function testSyncStatusTracking(): void { $mapping_data = [ 'entity_type' => 'customer', 'perfex_id' => 123, 'moloni_id' => '456', 'sync_direction' => 'bidirectional', 'status' => 'synced' ]; $this->mapping_model_mock ->expects($this->once()) ->method('update_mapping_status') ->with($mapping_data['perfex_id'], $mapping_data['moloni_id'], 'synced'); $this->sync_service->update_sync_status($mapping_data); $this->assertTrue(true); // Assertion happens in mock expectation } protected function tearDown(): void { $this->sync_service = null; $this->ci_mock = null; $this->api_client_mock = null; $this->mapping_model_mock = null; $this->sync_log_model_mock = null; $this->clients_model_mock = null; parent::tearDown(); } }