testConfig = $testConfig; $this->pdo = new \PDO( "mysql:host={$testConfig['database']['hostname']};dbname={$testConfig['database']['database']}", $testConfig['database']['username'], $testConfig['database']['password'], [\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION] ); // Clean test data TestHelpers::clearTestData(); } /** * Test complete OAuth setup and client synchronization workflow * This test will initially fail until all components are implemented */ public function testCompleteOAuthAndSyncWorkflow(): void { // Step 1: Admin configures OAuth credentials $adminController = new \DeskMoloni\Controllers\AdminController(); $oauthConfig = [ 'client_id' => $this->testConfig['moloni']['client_id'], 'client_secret' => $this->testConfig['moloni']['client_secret'], 'sandbox_mode' => true ]; $configResult = $adminController->saveOAuthConfiguration($oauthConfig); $this->assertIsArray($configResult); $this->assertTrue($configResult['success'] ?? false, 'OAuth configuration should be saved successfully'); // Verify configuration is encrypted and stored $stmt = $this->pdo->prepare("SELECT setting_value, encrypted FROM tbl_desk_moloni_config WHERE setting_key = 'moloni_client_secret'"); $stmt->execute(); $config = $stmt->fetch(); $this->assertNotFalse($config, 'Client secret should be stored'); $this->assertEquals(1, $config['encrypted'], 'Client secret should be encrypted'); $this->assertNotEquals($oauthConfig['client_secret'], $config['setting_value'], 'Secret should not be stored in plaintext'); // Step 2: Initiate OAuth flow $oauthController = new \DeskMoloni\Controllers\OAuthController(); $authUrl = $oauthController->initiateOAuthFlow(); $this->assertIsString($authUrl); $this->assertStringContains('api.moloni.pt', $authUrl); $this->assertStringContains('client_id=', $authUrl); $this->assertStringContains('response_type=code', $authUrl); // Step 3: Simulate OAuth callback with authorization code $authCode = 'test_authorization_code_123'; $callbackResult = $oauthController->handleOAuthCallback($authCode); $this->assertIsArray($callbackResult); $this->assertTrue($callbackResult['success'] ?? false, 'OAuth callback should be successful'); $this->assertArrayHasKey('access_token', $callbackResult); $this->assertArrayHasKey('refresh_token', $callbackResult); // Verify tokens are encrypted and stored $stmt = $this->pdo->prepare("SELECT COUNT(*) as count FROM tbl_desk_moloni_config WHERE setting_key IN ('oauth_access_token', 'oauth_refresh_token') AND encrypted = 1"); $stmt->execute(); $tokenCount = $stmt->fetch(); $this->assertEquals(2, $tokenCount['count'], 'Access and refresh tokens should be encrypted and stored'); // Step 4: Create and sync a client $testClient = TestHelpers::createTestClient([ 'userid' => 888888, 'company' => 'E2E Test Company', 'vat' => '888888888', 'phonenumber' => '+351888888888', 'email' => 'e2e-test@example.com' ]); $clientSyncService = new \DeskMoloni\ClientSyncService(); $syncResult = $clientSyncService->syncPerfexToMoloni($testClient); $this->assertIsArray($syncResult); $this->assertTrue($syncResult['success'] ?? false, 'Client sync should be successful'); $this->assertArrayHasKey('moloni_id', $syncResult); $this->assertIsInt($syncResult['moloni_id']); // Verify mapping was created $stmt = $this->pdo->prepare("SELECT * FROM tbl_desk_moloni_mapping WHERE entity_type = 'client' AND perfex_id = ?"); $stmt->execute([$testClient['userid']]); $mapping = $stmt->fetch(); $this->assertNotFalse($mapping, 'Client mapping should be created'); $this->assertEquals($syncResult['moloni_id'], $mapping['moloni_id']); // Step 5: Create and sync an invoice $testInvoice = TestHelpers::createTestInvoice([ 'id' => 777777, 'clientid' => $testClient['userid'], 'number' => 'E2E-TEST-001', 'total' => 123.00 ]); $invoiceSyncService = new \DeskMoloni\InvoiceSyncService(); $invoiceSyncResult = $invoiceSyncService->syncPerfexToMoloni($testInvoice); $this->assertIsArray($invoiceSyncResult); $this->assertTrue($invoiceSyncResult['success'] ?? false, 'Invoice sync should be successful'); // Step 6: Verify complete audit trail $stmt = $this->pdo->prepare("SELECT COUNT(*) as count FROM tbl_desk_moloni_sync_log WHERE perfex_id IN (?, ?) AND status = 'success'"); $stmt->execute([$testClient['userid'], $testInvoice['id']]); $logCount = $stmt->fetch(); $this->assertGreaterThanOrEqual(2, $logCount['count'], 'Successful sync operations should be logged'); } /** * Test complete client portal document access workflow */ public function testCompleteClientPortalWorkflow(): void { // Step 1: Set up test client with documents $testClient = TestHelpers::createTestClient([ 'userid' => 777777, 'company' => 'Portal Test Company', 'email' => 'portal-test@example.com' ]); // Create some test invoices for the client $testInvoices = []; for ($i = 1; $i <= 3; $i++) { $invoice = TestHelpers::createTestInvoice([ 'id' => 666660 + $i, 'clientid' => $testClient['userid'], 'number' => "PORTAL-{$i}", 'total' => 100.00 * $i ]); $testInvoices[] = $invoice; } // Step 2: Client attempts to access portal $clientPortalController = new \DeskMoloni\Controllers\ClientPortalController(); // Test authentication $authResult = $clientPortalController->authenticate($testClient['email'], 'test_password'); $this->assertIsArray($authResult); $this->assertTrue($authResult['success'] ?? false, 'Client authentication should succeed'); $this->assertArrayHasKey('session_token', $authResult); $this->assertArrayHasKey('permissions', $authResult); $sessionToken = $authResult['session_token']; // Verify session is stored $stmt = $this->pdo->prepare("SELECT * FROM tbl_desk_moloni_client_sessions WHERE session_token = ?"); $stmt->execute([$sessionToken]); $session = $stmt->fetch(); $this->assertNotFalse($session, 'Client session should be created'); $this->assertEquals($testClient['userid'], $session['client_id']); // Step 3: Fetch available documents $documentsResult = $clientPortalController->getClientDocuments($sessionToken); $this->assertIsArray($documentsResult); $this->assertTrue($documentsResult['success'] ?? false, 'Document list should be fetched'); $this->assertArrayHasKey('documents', $documentsResult); $this->assertIsArray($documentsResult['documents']); // Should include the test invoices $this->assertGreaterThanOrEqual(count($testInvoices), count($documentsResult['documents'])); // Step 4: Download a document if (!empty($documentsResult['documents'])) { $firstDocument = $documentsResult['documents'][0]; $downloadResult = $clientPortalController->downloadDocument( $sessionToken, $firstDocument['id'], $firstDocument['type'] ); $this->assertIsArray($downloadResult); $this->assertTrue($downloadResult['success'] ?? false, 'Document download should succeed'); $this->assertArrayHasKey('download_url', $downloadResult); $this->assertArrayHasKey('expires_at', $downloadResult); // Verify download URL is secure $this->assertStringContains('token=', $downloadResult['download_url']); $this->assertStringContains('expires=', $downloadResult['download_url']); } // Step 5: Test document filtering $filterResult = $clientPortalController->getClientDocuments($sessionToken, [ 'type' => 'invoice', 'date_from' => date('Y-m-01'), 'date_to' => date('Y-m-t') ]); $this->assertIsArray($filterResult); $this->assertTrue($filterResult['success'] ?? false, 'Filtered document list should be fetched'); // Step 6: Test session expiration // Simulate session expiration $this->pdo->exec("UPDATE tbl_desk_moloni_client_sessions SET expires_at = DATE_SUB(NOW(), INTERVAL 1 HOUR) WHERE session_token = '{$sessionToken}'"); $expiredResult = $clientPortalController->getClientDocuments($sessionToken); $this->assertIsArray($expiredResult); $this->assertFalse($expiredResult['success'] ?? true, 'Expired session should be rejected'); $this->assertArrayHasKey('error', $expiredResult); $this->assertStringContains('expired', strtolower($expiredResult['error'])); } /** * Test complete webhook processing workflow */ public function testCompleteWebhookWorkflow(): void { // Step 1: Set up existing mapping $stmt = $this->pdo->prepare("INSERT INTO tbl_desk_moloni_mapping (entity_type, perfex_id, moloni_id, sync_direction) VALUES (?, ?, ?, ?)"); $stmt->execute(['invoice', 555555, 444444, 'bidirectional']); // Step 2: Simulate Moloni webhook $webhookPayload = [ 'webhook_id' => 'moloni_webhook_' . time(), 'event_type' => 'invoice.status_changed', 'entity_type' => 'invoice', 'entity_id' => 444444, 'event_data' => [ 'invoice_id' => 444444, 'status' => 'paid', 'payment_date' => date('Y-m-d'), 'payment_method' => 'bank_transfer' ], 'signature' => 'webhook_signature_hash' ]; $webhookController = new \DeskMoloni\Controllers\WebhookController(); $webhookResult = $webhookController->processWebhook($webhookPayload); $this->assertIsArray($webhookResult); $this->assertTrue($webhookResult['success'] ?? false, 'Webhook should be processed successfully'); // Step 3: Verify webhook was recorded $stmt = $this->pdo->prepare("SELECT * FROM tbl_desk_moloni_webhooks WHERE webhook_id = ?"); $stmt->execute([$webhookPayload['webhook_id']]); $webhook = $stmt->fetch(); $this->assertNotFalse($webhook, 'Webhook should be recorded'); $this->assertEquals(1, $webhook['processed'], 'Webhook should be marked as processed'); $this->assertEquals(1, $webhook['signature_valid'], 'Webhook signature should be validated'); // Step 4: Verify queue task was created $stmt = $this->pdo->prepare("SELECT * FROM tbl_desk_moloni_sync_queue WHERE task_type = 'status_update' AND entity_id = ?"); $stmt->execute([$webhookPayload['entity_id']]); $queueTask = $stmt->fetch(); $this->assertNotFalse($queueTask, 'Queue task should be created from webhook'); $this->assertEquals('pending', $queueTask['status']); // Step 5: Process the queue task $queueProcessor = new \DeskMoloni\QueueProcessor($this->testConfig); $processResult = $queueProcessor->processTask($queueTask['id']); $this->assertIsArray($processResult); $this->assertTrue($processResult['success'] ?? false, 'Queue task should be processed successfully'); // Step 6: Verify Perfex invoice was updated $stmt = $this->pdo->prepare("SELECT * FROM tbl_desk_moloni_sync_log WHERE moloni_id = ? AND operation_type = 'status_change'"); $stmt->execute([$webhookPayload['entity_id']]); $syncLog = $stmt->fetch(); $this->assertNotFalse($syncLog, 'Status change should be logged'); $this->assertEquals('success', $syncLog['status']); } /** * Test complete error handling and recovery workflow */ public function testCompleteErrorHandlingWorkflow(): void { // Step 1: Create scenario that will cause API error $invalidClient = [ 'userid' => 111111, 'company' => '', // Empty required field 'vat' => 'INVALID', 'email' => 'not-an-email' ]; $clientSyncService = new \DeskMoloni\ClientSyncService(); $syncResult = $clientSyncService->syncPerfexToMoloni($invalidClient); $this->assertIsArray($syncResult); $this->assertFalse($syncResult['success'] ?? true, 'Invalid client sync should fail'); // Step 2: Verify error is properly logged $stmt = $this->pdo->prepare("SELECT * FROM tbl_desk_moloni_sync_log WHERE perfex_id = ? AND status = 'error'"); $stmt->execute([$invalidClient['userid']]); $errorLog = $stmt->fetch(); $this->assertNotFalse($errorLog, 'Error should be logged'); $this->assertNotNull($errorLog['error_code']); $this->assertNotNull($errorLog['error_message']); // Step 3: Test retry mechanism $retryService = new \DeskMoloni\RetryHandler(); $retryResult = $retryService->scheduleRetry($errorLog['id'], 'exponential_backoff'); $this->assertIsArray($retryResult); $this->assertTrue($retryResult['scheduled'] ?? false, 'Retry should be scheduled'); // Step 4: Verify retry task was created $stmt = $this->pdo->prepare("SELECT * FROM tbl_desk_moloni_sync_queue WHERE status = 'retry' AND entity_id = ?"); $stmt->execute([$invalidClient['userid']]); $retryTask = $stmt->fetch(); $this->assertNotFalse($retryTask, 'Retry task should be created'); $this->assertGreaterThan(1, $retryTask['attempts']); // Step 5: Test admin notification for persistent failures $monitoringService = new \DeskMoloni\MonitoringService(); $failureCount = 5; // Simulate multiple failures for ($i = 0; $i < $failureCount; $i++) { $monitoringService->recordFailure('client_sync', $invalidClient['userid'], 'validation_error'); } $alertResult = $monitoringService->checkAlertThresholds(); $this->assertIsArray($alertResult); $this->assertArrayHasKey('alerts_triggered', $alertResult); $this->assertGreaterThan(0, count($alertResult['alerts_triggered']), 'Alerts should be triggered for persistent failures'); } /** * Test complete performance monitoring workflow */ public function testCompletePerformanceMonitoringWorkflow(): void { // Step 1: Generate performance data $testOperations = [ ['type' => 'client_sync', 'time_ms' => 1500], ['type' => 'client_sync', 'time_ms' => 1200], ['type' => 'invoice_sync', 'time_ms' => 2000], ['type' => 'invoice_sync', 'time_ms' => 1800], ['type' => 'queue_processing', 'time_ms' => 500] ]; $performanceMonitor = new \DeskMoloni\PerformanceMonitor(); foreach ($testOperations as $operation) { $performanceMonitor->recordOperation($operation['type'], $operation['time_ms']); } // Step 2: Generate performance report $reportResult = $performanceMonitor->generateDailyReport(); $this->assertIsArray($reportResult); $this->assertArrayHasKey('average_times', $reportResult); $this->assertArrayHasKey('operation_counts', $reportResult); $this->assertArrayHasKey('performance_alerts', $reportResult); // Step 3: Test performance threshold alerts $slowOperations = [ ['type' => 'client_sync', 'time_ms' => 8000], // Slow operation ['type' => 'client_sync', 'time_ms' => 9000] ]; foreach ($slowOperations as $operation) { $performanceMonitor->recordOperation($operation['type'], $operation['time_ms']); } $alertCheck = $performanceMonitor->checkPerformanceThresholds(); $this->assertIsArray($alertCheck); $this->assertArrayHasKey('threshold_violations', $alertCheck); $this->assertGreaterThan(0, count($alertCheck['threshold_violations']), 'Slow operations should trigger threshold violations'); // Step 4: Verify performance data storage $stmt = $this->pdo->query("SELECT AVG(execution_time_ms) as avg_time FROM tbl_desk_moloni_sync_log WHERE operation_type IN ('create', 'update') AND created_at > DATE_SUB(NOW(), INTERVAL 1 HOUR)"); $avgTime = $stmt->fetch(); $this->assertNotNull($avgTime['avg_time'], 'Performance data should be stored in logs'); } protected function tearDown(): void { // Clean up test data $testIds = [888888, 777777, 666661, 666662, 666663, 555555, 444444, 111111]; foreach ($testIds as $id) { $this->pdo->exec("DELETE FROM tbl_desk_moloni_mapping WHERE perfex_id = {$id} OR moloni_id = {$id}"); $this->pdo->exec("DELETE FROM tbl_desk_moloni_sync_log WHERE perfex_id = {$id} OR moloni_id = {$id}"); $this->pdo->exec("DELETE FROM tbl_desk_moloni_sync_queue WHERE entity_id = {$id}"); } $this->pdo->exec("DELETE FROM tbl_desk_moloni_webhooks WHERE webhook_id LIKE '%webhook_%'"); $this->pdo->exec("DELETE FROM tbl_desk_moloni_client_sessions WHERE client_id IN (777777)"); $this->pdo->exec("DELETE FROM tbl_desk_moloni_config WHERE setting_key IN ('moloni_client_secret', 'oauth_access_token', 'oauth_refresh_token')"); } }