- 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>
431 lines
19 KiB
PHP
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')");
|
|
}
|
|
} |