/** * Descomplicar® Crescimento Digital * https://descomplicar.pt */ CI = &get_instance(); $this->CI->load->database(); $this->db = $this->CI->db; if (ENVIRONMENT !== 'testing') { $this->markTestSkipped('Contract tests should only run in testing environment'); } } /** * @test * Contract: desk_moloni_sync_queue table must exist with correct structure */ public function queue_table_exists_with_required_structure() { // ACT: Check table existence $table_exists = $this->db->table_exists('desk_moloni_sync_queue'); // ASSERT: Table must exist $this->assertTrue($table_exists, 'desk_moloni_sync_queue table must exist'); // ASSERT: Required columns exist $fields = $this->db->list_fields('desk_moloni_sync_queue'); $required_fields = [ 'id', 'task_type', 'entity_type', 'entity_id', 'priority', 'payload', 'status', 'attempts', 'max_attempts', 'scheduled_at', 'started_at', 'completed_at', 'error_message', 'created_at', 'updated_at' ]; foreach ($required_fields as $field) { $this->assertContains($field, $fields, "Required field '{$field}' must exist in desk_moloni_sync_queue table"); } } /** * @test * Contract: Queue table must enforce task_type ENUM values */ public function queue_table_enforces_task_type_enum() { // ARRANGE: Clean table $this->db->truncate('desk_moloni_sync_queue'); // ACT & ASSERT: Valid task types should work $valid_task_types = [ 'sync_client', 'sync_product', 'sync_invoice', 'sync_estimate', 'sync_credit_note', 'status_update' ]; foreach ($valid_task_types as $task_type) { $test_data = [ 'task_type' => $task_type, 'entity_type' => 'client', 'entity_id' => 1, 'priority' => 5 ]; $insert_success = $this->db->insert('desk_moloni_sync_queue', $test_data); $this->assertTrue($insert_success, "Task type '{$task_type}' must be valid"); // Clean up $this->db->delete('desk_moloni_sync_queue', ['task_type' => $task_type]); } // ACT & ASSERT: Invalid task type should fail $invalid_data = [ 'task_type' => 'invalid_task', 'entity_type' => 'client', 'entity_id' => 1, 'priority' => 5 ]; $this->expectException(\Exception::class); $this->db->insert('desk_moloni_sync_queue', $invalid_data); } /** * @test * Contract: Queue table must enforce status ENUM values */ public function queue_table_enforces_status_enum() { // ARRANGE: Clean table $this->db->truncate('desk_moloni_sync_queue'); // ACT & ASSERT: Valid status values should work $valid_statuses = ['pending', 'processing', 'completed', 'failed', 'retry']; foreach ($valid_statuses as $status) { $test_data = [ 'task_type' => 'sync_client', 'entity_type' => 'client', 'entity_id' => 1, 'priority' => 5, 'status' => $status ]; $insert_success = $this->db->insert('desk_moloni_sync_queue', $test_data); $this->assertTrue($insert_success, "Status '{$status}' must be valid"); // Clean up $this->db->delete('desk_moloni_sync_queue', ['status' => $status]); } // ACT & ASSERT: Invalid status should fail $invalid_data = [ 'task_type' => 'sync_client', 'entity_type' => 'client', 'entity_id' => 1, 'priority' => 5, 'status' => 'invalid_status' ]; $this->expectException(\Exception::class); $this->db->insert('desk_moloni_sync_queue', $invalid_data); } /** * @test * Contract: Queue table must support priority-based ordering */ public function queue_table_supports_priority_ordering() { // ARRANGE: Clean table and insert tasks with different priorities $this->db->truncate('desk_moloni_sync_queue'); $tasks = [ ['priority' => 9, 'entity_id' => 1], // Lowest priority ['priority' => 1, 'entity_id' => 2], // Highest priority ['priority' => 5, 'entity_id' => 3], // Medium priority ]; foreach ($tasks as $task) { $task_data = [ 'task_type' => 'sync_client', 'entity_type' => 'client', 'entity_id' => $task['entity_id'], 'priority' => $task['priority'], 'status' => 'pending' ]; $this->db->insert('desk_moloni_sync_queue', $task_data); } // ACT: Query tasks ordered by priority (ascending = highest priority first) $this->db->select('entity_id, priority'); $this->db->where('status', 'pending'); $this->db->order_by('priority', 'ASC'); $ordered_tasks = $this->db->get('desk_moloni_sync_queue')->result(); // ASSERT: Tasks are ordered by priority (1 = highest, 9 = lowest) $this->assertEquals(2, $ordered_tasks[0]->entity_id, 'Highest priority task (1) should be first'); $this->assertEquals(3, $ordered_tasks[1]->entity_id, 'Medium priority task (5) should be second'); $this->assertEquals(1, $ordered_tasks[2]->entity_id, 'Lowest priority task (9) should be last'); } /** * @test * Contract: Queue table must support JSON payload for task data */ public function queue_table_supports_json_payload() { // ARRANGE: Clean table $this->db->truncate('desk_moloni_sync_queue'); // ACT: Insert task with JSON payload $json_payload = [ 'sync_fields' => ['name', 'email', 'vat'], 'force_update' => true, 'retry_count' => 0, 'metadata' => [ 'source' => 'perfex', 'trigger' => 'after_client_updated' ] ]; $task_data = [ 'task_type' => 'sync_client', 'entity_type' => 'client', 'entity_id' => 100, 'priority' => 3, 'payload' => json_encode($json_payload), 'status' => 'pending' ]; $insert_success = $this->db->insert('desk_moloni_sync_queue', $task_data); $this->assertTrue($insert_success, 'Task with JSON payload must be inserted successfully'); // ASSERT: JSON payload is stored and retrieved correctly $row = $this->db->get_where('desk_moloni_sync_queue', ['entity_id' => 100])->row(); $retrieved_payload = json_decode($row->payload, true); $this->assertEquals($json_payload, $retrieved_payload, 'JSON payload must be stored and retrieved correctly'); $this->assertTrue(is_array($retrieved_payload), 'Payload must be retrievable as array'); $this->assertEquals('perfex', $retrieved_payload['metadata']['source'], 'Nested JSON data must be accessible'); } /** * @test * Contract: Queue table must support retry mechanism with attempts tracking */ public function queue_table_supports_retry_mechanism() { // ARRANGE: Clean table $this->db->truncate('desk_moloni_sync_queue'); // ACT: Insert task with retry configuration $retry_task = [ 'task_type' => 'sync_invoice', 'entity_type' => 'invoice', 'entity_id' => 200, 'priority' => 1, 'status' => 'failed', 'attempts' => 2, 'max_attempts' => 3, 'error_message' => 'API rate limit exceeded' ]; $insert_success = $this->db->insert('desk_moloni_sync_queue', $retry_task); $this->assertTrue($insert_success, 'Task with retry configuration must be inserted'); // ASSERT: Retry data is stored correctly $row = $this->db->get_where('desk_moloni_sync_queue', ['entity_id' => 200])->row(); $this->assertEquals(2, $row->attempts, 'Attempts counter must be stored'); $this->assertEquals(3, $row->max_attempts, 'Max attempts limit must be stored'); $this->assertEquals('failed', $row->status, 'Failed status must be stored'); $this->assertEquals('API rate limit exceeded', $row->error_message, 'Error message must be stored'); // ASSERT: Task can be updated for retry $this->db->set('status', 'retry'); $this->db->set('attempts', $row->attempts + 1); $this->db->where('id', $row->id); $update_success = $this->db->update('desk_moloni_sync_queue'); $this->assertTrue($update_success, 'Task must be updatable for retry'); } /** * @test * Contract: Queue table must have required indexes for performance */ public function queue_table_has_required_indexes() { // ACT: Get table indexes $indexes = $this->db->query("SHOW INDEX FROM desk_moloni_sync_queue")->result_array(); // ASSERT: Required indexes exist $required_indexes = [ 'PRIMARY', 'idx_status_priority', 'idx_entity', 'idx_scheduled', 'idx_status_attempts', 'idx_queue_processing' ]; $index_names = array_column($indexes, 'Key_name'); foreach ($required_indexes as $required_index) { $this->assertContains($required_index, $index_names, "Required index '{$required_index}' must exist for queue performance"); } } /** * @test * Contract: Queue table must support scheduled execution times */ public function queue_table_supports_scheduled_execution() { // ARRANGE: Clean table $this->db->truncate('desk_moloni_sync_queue'); // ACT: Insert tasks with different scheduled times $future_time = date('Y-m-d H:i:s', time() + 3600); // 1 hour from now $past_time = date('Y-m-d H:i:s', time() - 3600); // 1 hour ago $scheduled_tasks = [ [ 'entity_id' => 301, 'scheduled_at' => $future_time, 'status' => 'pending' ], [ 'entity_id' => 302, 'scheduled_at' => $past_time, 'status' => 'pending' ] ]; foreach ($scheduled_tasks as $task) { $task_data = array_merge([ 'task_type' => 'sync_product', 'entity_type' => 'product', 'priority' => 5 ], $task); $this->db->insert('desk_moloni_sync_queue', $task_data); } // ASSERT: Tasks can be filtered by scheduled time $ready_tasks = $this->db->get_where('desk_moloni_sync_queue', [ 'scheduled_at <=' => date('Y-m-d H:i:s'), 'status' => 'pending' ])->result(); $this->assertCount(1, $ready_tasks, 'Only past/current scheduled tasks should be ready'); $this->assertEquals(302, $ready_tasks[0]->entity_id, 'Past scheduled task should be ready for processing'); } protected function tearDown(): void { // Clean up test data if ($this->db) { $this->db->where('entity_id >=', 1); $this->db->where('entity_id <=', 400); $this->db->delete('desk_moloni_sync_queue'); } } }