Files
desk-moloni/modules/desk_moloni/tests/e2e/CompleteWorkflowTest.php
Emanuel Almeida 8c4f68576f chore: add spec-kit and standardize signatures
- Added GitHub spec-kit for development workflow
- Standardized file signatures to Descomplicar® format
- Updated development configuration

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-12 01:27:37 +01:00

431 lines
19 KiB
PHP

/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
declare(strict_types=1);
namespace DeskMoloni\Tests\E2E;
use PHPUnit\Framework\TestCase;
use DeskMoloni\Tests\TestHelpers;
/**
* End-to-End Test: Complete User Workflows
*
* This test MUST FAIL initially as part of TDD methodology.
* Tests complete user journeys from configuration to document access.
*
* @group e2e
* @group workflow
*/
class CompleteWorkflowTest extends TestCase
{
private array $testConfig;
private \PDO $pdo;
protected function setUp(): void
{
global $testConfig;
$this->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')");
}
}