- 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>
345 lines
12 KiB
PHP
345 lines
12 KiB
PHP
/**
|
|
* Descomplicar® Crescimento Digital
|
|
* https://descomplicar.pt
|
|
*/
|
|
|
|
<?php
|
|
/**
|
|
* Contract Test for desk_moloni_sync_queue table
|
|
*
|
|
* This test MUST FAIL until the Sync_queue_model is properly implemented
|
|
* Following TDD RED-GREEN-REFACTOR cycle
|
|
*
|
|
* @package DeskMoloni\Tests\Contract
|
|
*/
|
|
|
|
namespace DeskMoloni\Tests\Contract;
|
|
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
class QueueTableTest extends TestCase
|
|
{
|
|
private $CI;
|
|
private $db;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
$this->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');
|
|
}
|
|
}
|
|
} |