input_mock = $this->createMock(stdClass::class); $this->security_mock = $this->createMock(stdClass::class); $this->queue_processor_mock = $this->createMock(stdClass::class); // Setup CI mock with required components $this->ci->input = $this->input_mock; $this->ci->security = $this->security_mock; $this->ci->output = $this->createMock(stdClass::class); $this->ci->load = $this->createMock(stdClass::class); // Mock function_exists for validation if (!function_exists('is_cli')) { function is_cli() { return false; } } // Load WebhookController require_once 'modules/desk_moloni/controllers/WebhookController.php'; $this->webhook_controller = new WebhookController(); } #[Test] #[Group('unit')] public function testControllerInitialization(): void { $this->assertInstanceOf(WebhookController::class, $this->webhook_controller); } #[Test] #[Group('unit')] public function testValidWebhookSignature(): void { $payload = json_encode([ 'entity_type' => 'customer', 'entity_id' => '123', 'action' => 'update', 'timestamp' => time() ]); $secret = 'webhook_secret_key'; $signature = hash_hmac('sha256', $payload, $secret); // Mock input for webhook data $this->input_mock ->expects($this->once()) ->method('raw_input_stream') ->willReturn($payload); $this->input_mock ->expects($this->once()) ->method('get_request_header') ->with('X-Moloni-Signature') ->willReturn('sha256=' . $signature); // Mock security XSS clean $this->security_mock ->expects($this->once()) ->method('xss_clean') ->willReturn(json_decode($payload, true)); // Mock successful queue addition $this->queue_processor_mock ->expects($this->once()) ->method('add_to_queue') ->willReturn('job_12345'); // Mock output $this->ci->output ->expects($this->once()) ->method('set_content_type') ->with('application/json'); $this->ci->output ->expects($this->once()) ->method('set_output'); // Execute webhook ob_start(); $this->webhook_controller->moloni(); ob_end_clean(); // Assertions are handled through mock expectations $this->assertTrue(true); } #[Test] #[Group('unit')] public function testInvalidWebhookSignature(): void { $payload = json_encode([ 'entity_type' => 'customer', 'entity_id' => '123', 'action' => 'update' ]); $invalid_signature = 'sha256=invalid_signature_hash'; // Mock input $this->input_mock ->expects($this->once()) ->method('raw_input_stream') ->willReturn($payload); $this->input_mock ->expects($this->once()) ->method('get_request_header') ->with('X-Moloni-Signature') ->willReturn($invalid_signature); // Mock output with 401 status $this->ci->output ->expects($this->once()) ->method('set_status_header') ->with(401); $this->ci->output ->expects($this->once()) ->method('set_content_type') ->with('application/json'); // Should not add to queue with invalid signature $this->queue_processor_mock ->expects($this->never()) ->method('add_to_queue'); ob_start(); $this->webhook_controller->moloni(); ob_end_clean(); $this->assertTrue(true); } #[Test] #[Group('unit')] public function testMissingWebhookSignature(): void { $payload = json_encode([ 'entity_type' => 'customer', 'entity_id' => '123' ]); // Mock input without signature $this->input_mock ->expects($this->once()) ->method('raw_input_stream') ->willReturn($payload); $this->input_mock ->expects($this->once()) ->method('get_request_header') ->with('X-Moloni-Signature') ->willReturn(null); // Should return 401 Unauthorized $this->ci->output ->expects($this->once()) ->method('set_status_header') ->with(401); ob_start(); $this->webhook_controller->moloni(); ob_end_clean(); $this->assertTrue(true); } #[Test] #[Group('unit')] #[DataProvider('webhookPayloadProvider')] public function testWebhookPayloadValidation(array $payload, bool $should_be_valid): void { $json_payload = json_encode($payload); $secret = 'test_secret'; $signature = hash_hmac('sha256', $json_payload, $secret); // Mock input $this->input_mock ->expects($this->once()) ->method('raw_input_stream') ->willReturn($json_payload); $this->input_mock ->expects($this->once()) ->method('get_request_header') ->willReturn('sha256=' . $signature); $this->security_mock ->expects($this->once()) ->method('xss_clean') ->willReturn($payload); if ($should_be_valid) { // Should process valid payload $this->queue_processor_mock ->expects($this->once()) ->method('add_to_queue') ->willReturn('job_id'); $this->ci->output ->expects($this->once()) ->method('set_status_header') ->with(200); } else { // Should reject invalid payload $this->queue_processor_mock ->expects($this->never()) ->method('add_to_queue'); $this->ci->output ->expects($this->once()) ->method('set_status_header') ->with(400); } ob_start(); $this->webhook_controller->moloni(); ob_end_clean(); $this->assertTrue(true); } public static function webhookPayloadProvider(): array { return [ 'Valid customer payload' => [ [ 'entity_type' => 'customer', 'entity_id' => '123', 'action' => 'update', 'timestamp' => time(), 'data' => ['name' => 'Updated Customer'] ], true ], 'Valid invoice payload' => [ [ 'entity_type' => 'invoice', 'entity_id' => '456', 'action' => 'create', 'timestamp' => time(), 'data' => ['number' => 'INV-001'] ], true ], 'Missing entity_type' => [ [ 'entity_id' => '123', 'action' => 'update' ], false ], 'Missing entity_id' => [ [ 'entity_type' => 'customer', 'action' => 'update' ], false ], 'Invalid entity_type' => [ [ 'entity_type' => 'invalid_entity', 'entity_id' => '123', 'action' => 'update' ], false ], 'Invalid action' => [ [ 'entity_type' => 'customer', 'entity_id' => '123', 'action' => 'invalid_action' ], false ] ]; } #[Test] #[Group('unit')] public function testWebhookRateLimit(): void { // Mock multiple rapid requests from same IP $payload = json_encode([ 'entity_type' => 'customer', 'entity_id' => '123', 'action' => 'update' ]); $signature = hash_hmac('sha256', $payload, 'secret'); // Setup input mocks for multiple calls $this->input_mock ->method('raw_input_stream') ->willReturn($payload); $this->input_mock ->method('get_request_header') ->willReturn('sha256=' . $signature); $this->input_mock ->method('ip_address') ->willReturn('192.168.1.100'); $this->security_mock ->method('xss_clean') ->willReturn(json_decode($payload, true)); // First request should succeed $this->queue_processor_mock ->expects($this->once()) ->method('add_to_queue') ->willReturn('job_1'); // Subsequent requests should be rate limited (if implemented) if (method_exists($this->webhook_controller, 'check_rate_limit')) { $this->ci->output ->expects($this->once()) ->method('set_status_header') ->with(429); // Too Many Requests } // Execute first webhook ob_start(); $this->webhook_controller->moloni(); ob_end_clean(); $this->assertTrue(true); } #[Test] #[Group('unit')] public function testWebhookIdempotency(): void { // Test that duplicate webhooks are handled correctly $payload = [ 'entity_type' => 'customer', 'entity_id' => '123', 'action' => 'update', 'timestamp' => time(), 'idempotency_key' => 'unique_webhook_123' ]; $json_payload = json_encode($payload); $signature = hash_hmac('sha256', $json_payload, 'secret'); $this->input_mock ->method('raw_input_stream') ->willReturn($json_payload); $this->input_mock ->method('get_request_header') ->willReturn('sha256=' . $signature); $this->security_mock ->method('xss_clean') ->willReturn($payload); // First webhook should be processed $this->queue_processor_mock ->expects($this->once()) ->method('add_to_queue') ->willReturn('job_1'); // If idempotency is implemented, duplicate should be ignored if (method_exists($this->webhook_controller, 'is_duplicate_webhook')) { $this->ci->output ->expects($this->once()) ->method('set_status_header') ->with(200); // OK but not processed } ob_start(); $this->webhook_controller->moloni(); ob_end_clean(); $this->assertTrue(true); } #[Test] #[Group('unit')] public function testWebhookLogging(): void { $payload = [ 'entity_type' => 'customer', 'entity_id' => '456', 'action' => 'delete' ]; $json_payload = json_encode($payload); $signature = hash_hmac('sha256', $json_payload, 'secret'); $this->input_mock ->method('raw_input_stream') ->willReturn($json_payload); $this->input_mock ->method('get_request_header') ->willReturn('sha256=' . $signature); $this->security_mock ->method('xss_clean') ->willReturn($payload); // Mock webhook logging if available if (method_exists($this->webhook_controller, 'log_webhook')) { // Should log webhook receipt and processing $this->assertTrue(true); } else { $this->markTestSkipped('Webhook logging not implemented'); } ob_start(); $this->webhook_controller->moloni(); ob_end_clean(); $this->assertTrue(true); } #[Test] #[Group('unit')] public function testWebhookErrorHandling(): void { $payload = json_encode([ 'entity_type' => 'customer', 'entity_id' => '789', 'action' => 'update' ]); $signature = hash_hmac('sha256', $payload, 'secret'); $this->input_mock ->method('raw_input_stream') ->willReturn($payload); $this->input_mock ->method('get_request_header') ->willReturn('sha256=' . $signature); $this->security_mock ->method('xss_clean') ->willReturn(json_decode($payload, true)); // Mock queue failure $this->queue_processor_mock ->expects($this->once()) ->method('add_to_queue') ->willReturn(false); // Should handle queue failure gracefully $this->ci->output ->expects($this->once()) ->method('set_status_header') ->with(500); ob_start(); $this->webhook_controller->moloni(); ob_end_clean(); $this->assertTrue(true); } #[Test] #[Group('unit')] public function testWebhookMetrics(): void { if (!method_exists($this->webhook_controller, 'get_webhook_metrics')) { $this->markTestSkipped('Webhook metrics not implemented'); } $metrics = $this->webhook_controller->get_webhook_metrics(); $this->assertIsArray($metrics); $this->assertArrayHasKey('total_received', $metrics); $this->assertArrayHasKey('total_processed', $metrics); $this->assertArrayHasKey('total_errors', $metrics); $this->assertArrayHasKey('by_entity_type', $metrics); $this->assertArrayHasKey('by_action', $metrics); $this->assertArrayHasKey('success_rate', $metrics); } #[Test] #[Group('unit')] public function testWebhookSecurity(): void { // Test various security scenarios // 1. Request from unauthorized IP (if IP whitelist implemented) $this->input_mock ->method('ip_address') ->willReturn('192.168.999.999'); if (method_exists($this->webhook_controller, 'is_ip_whitelisted')) { $this->ci->output ->expects($this->once()) ->method('set_status_header') ->with(403); } // 2. Request with suspicious user agent $this->input_mock ->method('user_agent') ->willReturn('SuspiciousBot/1.0'); // 3. Request with malformed JSON $this->input_mock ->method('raw_input_stream') ->willReturn('{"invalid": json}'); $this->ci->output ->expects($this->once()) ->method('set_status_header') ->with(400); ob_start(); $this->webhook_controller->moloni(); ob_end_clean(); $this->assertTrue(true); } protected function tearDown(): void { $this->webhook_controller = null; $this->input_mock = null; $this->security_mock = null; $this->queue_processor_mock = null; parent::tearDown(); } }