- 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>
546 lines
19 KiB
PHP
546 lines
19 KiB
PHP
/**
|
|
* Descomplicar® Crescimento Digital
|
|
* https://descomplicar.pt
|
|
*/
|
|
|
|
<?php
|
|
|
|
/**
|
|
* QueueTableTest.php
|
|
*
|
|
* PHPUnit tests for desk_moloni_sync_queue table structure and validation rules
|
|
* Tests asynchronous task queue for synchronization operations
|
|
*
|
|
* @package DeskMoloni\Tests\Database
|
|
* @author Database Design Specialist
|
|
* @version 3.0
|
|
*/
|
|
|
|
require_once(__DIR__ . '/../../../../tests/TestCase.php');
|
|
|
|
class QueueTableTest extends TestCase
|
|
{
|
|
private $tableName = 'desk_moloni_sync_queue';
|
|
private $testQueueModel;
|
|
|
|
public function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
$this->clearTestData();
|
|
|
|
// Initialize test model (will be implemented after tests)
|
|
// $this->testQueueModel = new DeskMoloniSyncQueue();
|
|
}
|
|
|
|
public function tearDown(): void
|
|
{
|
|
$this->clearTestData();
|
|
parent::tearDown();
|
|
}
|
|
|
|
/**
|
|
* Test table structure exists with correct columns
|
|
*/
|
|
public function testTableStructureExists()
|
|
{
|
|
$db = $this->ci->db;
|
|
|
|
// Verify table exists
|
|
$this->assertTrue($db->table_exists($this->tableName), "Table {$this->tableName} should exist");
|
|
|
|
// Verify required columns exist
|
|
$expectedColumns = [
|
|
'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 ($expectedColumns as $column) {
|
|
$this->assertTrue($db->field_exists($column, $this->tableName),
|
|
"Column '{$column}' should exist in {$this->tableName}");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Test task_type ENUM values
|
|
*/
|
|
public function testTaskTypeEnumValues()
|
|
{
|
|
$db = $this->ci->db;
|
|
|
|
$validTaskTypes = [
|
|
'sync_client', 'sync_product', 'sync_invoice',
|
|
'sync_estimate', 'sync_credit_note', 'status_update'
|
|
];
|
|
|
|
foreach ($validTaskTypes as $taskType) {
|
|
$data = [
|
|
'task_type' => $taskType,
|
|
'entity_type' => 'client',
|
|
'entity_id' => rand(1, 1000),
|
|
'priority' => 5
|
|
];
|
|
|
|
$this->assertTrue($db->insert($this->tableName, $data),
|
|
"Valid task type '{$taskType}' should insert successfully");
|
|
|
|
$record = $db->where('entity_id', $data['entity_id'])->get($this->tableName)->row();
|
|
$this->assertEquals($taskType, $record->task_type, "Task type should match inserted value");
|
|
|
|
// Clean up
|
|
$db->where('entity_id', $data['entity_id'])->delete($this->tableName);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Test entity_type ENUM values
|
|
*/
|
|
public function testEntityTypeEnumValues()
|
|
{
|
|
$db = $this->ci->db;
|
|
|
|
$validEntityTypes = ['client', 'product', 'invoice', 'estimate', 'credit_note'];
|
|
|
|
foreach ($validEntityTypes as $entityType) {
|
|
$data = [
|
|
'task_type' => 'sync_client',
|
|
'entity_type' => $entityType,
|
|
'entity_id' => rand(1, 1000),
|
|
'priority' => 5
|
|
];
|
|
|
|
$this->assertTrue($db->insert($this->tableName, $data),
|
|
"Valid entity type '{$entityType}' should insert successfully");
|
|
|
|
// Clean up
|
|
$db->where('entity_id', $data['entity_id'])->delete($this->tableName);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Test status ENUM values and state transitions
|
|
*/
|
|
public function testStatusEnumValues()
|
|
{
|
|
$db = $this->ci->db;
|
|
|
|
$validStatuses = ['pending', 'processing', 'completed', 'failed', 'retry'];
|
|
|
|
foreach ($validStatuses as $status) {
|
|
$data = [
|
|
'task_type' => 'sync_product',
|
|
'entity_type' => 'product',
|
|
'entity_id' => rand(1, 1000),
|
|
'priority' => 5,
|
|
'status' => $status
|
|
];
|
|
|
|
$this->assertTrue($db->insert($this->tableName, $data),
|
|
"Valid status '{$status}' should insert successfully");
|
|
|
|
$record = $db->where('entity_id', $data['entity_id'])->get($this->tableName)->row();
|
|
$this->assertEquals($status, $record->status, "Status should match inserted value");
|
|
|
|
// Clean up
|
|
$db->where('entity_id', $data['entity_id'])->delete($this->tableName);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Test default values
|
|
*/
|
|
public function testDefaultValues()
|
|
{
|
|
$db = $this->ci->db;
|
|
|
|
$data = [
|
|
'task_type' => 'sync_invoice',
|
|
'entity_type' => 'invoice',
|
|
'entity_id' => rand(1, 1000)
|
|
// Omit priority, status, attempts, max_attempts to test defaults
|
|
];
|
|
|
|
$this->assertTrue($db->insert($this->tableName, $data), 'Insert with default values should succeed');
|
|
|
|
$record = $db->where('entity_id', $data['entity_id'])->get($this->tableName)->row();
|
|
|
|
$this->assertEquals(5, $record->priority, 'Default priority should be 5');
|
|
$this->assertEquals('pending', $record->status, 'Default status should be pending');
|
|
$this->assertEquals(0, $record->attempts, 'Default attempts should be 0');
|
|
$this->assertEquals(3, $record->max_attempts, 'Default max_attempts should be 3');
|
|
$this->assertNotNull($record->scheduled_at, 'scheduled_at should be auto-populated');
|
|
}
|
|
|
|
/**
|
|
* Test priority validation constraints
|
|
*/
|
|
public function testPriorityConstraints()
|
|
{
|
|
$db = $this->ci->db;
|
|
|
|
// Test valid priority range (1-9)
|
|
$validPriorities = [1, 2, 3, 4, 5, 6, 7, 8, 9];
|
|
|
|
foreach ($validPriorities as $priority) {
|
|
$data = [
|
|
'task_type' => 'sync_client',
|
|
'entity_type' => 'client',
|
|
'entity_id' => rand(1, 1000),
|
|
'priority' => $priority
|
|
];
|
|
|
|
$this->assertTrue($db->insert($this->tableName, $data),
|
|
"Valid priority '{$priority}' should insert successfully");
|
|
|
|
// Clean up
|
|
$db->where('entity_id', $data['entity_id'])->delete($this->tableName);
|
|
}
|
|
|
|
// Test invalid priority values should fail (if constraints are enforced)
|
|
$invalidPriorities = [0, 10, -1, 15];
|
|
|
|
foreach ($invalidPriorities as $priority) {
|
|
$data = [
|
|
'task_type' => 'sync_client',
|
|
'entity_type' => 'client',
|
|
'entity_id' => rand(1, 1000),
|
|
'priority' => $priority
|
|
];
|
|
|
|
// Note: This test depends on database constraint enforcement
|
|
// Some databases may not enforce CHECK constraints
|
|
$result = $db->insert($this->tableName, $data);
|
|
if ($result === false) {
|
|
$this->assertStringContainsString('constraint', strtolower($db->error()['message']));
|
|
}
|
|
|
|
// Clean up any successful inserts
|
|
$db->where('entity_id', $data['entity_id'])->delete($this->tableName);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Test attempts validation constraints
|
|
*/
|
|
public function testAttemptsConstraints()
|
|
{
|
|
$db = $this->ci->db;
|
|
|
|
// Test valid attempts configuration
|
|
$data = [
|
|
'task_type' => 'sync_product',
|
|
'entity_type' => 'product',
|
|
'entity_id' => rand(1, 1000),
|
|
'priority' => 5,
|
|
'attempts' => 2,
|
|
'max_attempts' => 3
|
|
];
|
|
|
|
$this->assertTrue($db->insert($this->tableName, $data), 'Valid attempts configuration should succeed');
|
|
|
|
$record = $db->where('entity_id', $data['entity_id'])->get($this->tableName)->row();
|
|
$this->assertEquals(2, $record->attempts, 'Attempts should match inserted value');
|
|
$this->assertEquals(3, $record->max_attempts, 'Max attempts should match inserted value');
|
|
}
|
|
|
|
/**
|
|
* Test JSON payload validation
|
|
*/
|
|
public function testJSONPayloadValidation()
|
|
{
|
|
$db = $this->ci->db;
|
|
|
|
// Test valid JSON payload
|
|
$validPayloads = [
|
|
'{"action": "create", "data": {"name": "Test Client"}}',
|
|
'{"sync_fields": ["name", "email", "phone"]}',
|
|
'[]',
|
|
'{}',
|
|
null // NULL should be allowed
|
|
];
|
|
|
|
foreach ($validPayloads as $index => $payload) {
|
|
$data = [
|
|
'task_type' => 'sync_client',
|
|
'entity_type' => 'client',
|
|
'entity_id' => rand(10000, 19999) + $index,
|
|
'priority' => 5,
|
|
'payload' => $payload
|
|
];
|
|
|
|
$this->assertTrue($db->insert($this->tableName, $data),
|
|
"Valid JSON payload should insert successfully");
|
|
|
|
$record = $db->where('entity_id', $data['entity_id'])->get($this->tableName)->row();
|
|
$this->assertEquals($payload, $record->payload, "Payload should match inserted value");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Test task state transitions
|
|
*/
|
|
public function testTaskStateTransitions()
|
|
{
|
|
$db = $this->ci->db;
|
|
|
|
$entityId = rand(20000, 29999);
|
|
|
|
// Insert pending task
|
|
$data = [
|
|
'task_type' => 'sync_invoice',
|
|
'entity_type' => 'invoice',
|
|
'entity_id' => $entityId,
|
|
'priority' => 3,
|
|
'status' => 'pending'
|
|
];
|
|
|
|
$this->assertTrue($db->insert($this->tableName, $data), 'Pending task should insert');
|
|
|
|
// Transition to processing
|
|
$startTime = date('Y-m-d H:i:s');
|
|
$updateData = [
|
|
'status' => 'processing',
|
|
'started_at' => $startTime,
|
|
'attempts' => 1
|
|
];
|
|
|
|
$db->where('entity_id', $entityId)->update($this->tableName, $updateData);
|
|
$record = $db->where('entity_id', $entityId)->get($this->tableName)->row();
|
|
|
|
$this->assertEquals('processing', $record->status, 'Status should be updated to processing');
|
|
$this->assertEquals($startTime, $record->started_at, 'started_at should be updated');
|
|
$this->assertEquals(1, $record->attempts, 'Attempts should be incremented');
|
|
|
|
// Transition to completed
|
|
$completedTime = date('Y-m-d H:i:s');
|
|
$completedData = [
|
|
'status' => 'completed',
|
|
'completed_at' => $completedTime
|
|
];
|
|
|
|
$db->where('entity_id', $entityId)->update($this->tableName, $completedData);
|
|
$finalRecord = $db->where('entity_id', $entityId)->get($this->tableName)->row();
|
|
|
|
$this->assertEquals('completed', $finalRecord->status, 'Status should be updated to completed');
|
|
$this->assertEquals($completedTime, $finalRecord->completed_at, 'completed_at should be updated');
|
|
}
|
|
|
|
/**
|
|
* Test failed task with error message
|
|
*/
|
|
public function testFailedTaskHandling()
|
|
{
|
|
$db = $this->ci->db;
|
|
|
|
$entityId = rand(30000, 39999);
|
|
$errorMessage = 'API connection timeout after 30 seconds';
|
|
|
|
$data = [
|
|
'task_type' => 'sync_client',
|
|
'entity_type' => 'client',
|
|
'entity_id' => $entityId,
|
|
'priority' => 5,
|
|
'status' => 'failed',
|
|
'attempts' => 3,
|
|
'max_attempts' => 3,
|
|
'error_message' => $errorMessage,
|
|
'completed_at' => date('Y-m-d H:i:s')
|
|
];
|
|
|
|
$this->assertTrue($db->insert($this->tableName, $data), 'Failed task should insert');
|
|
|
|
$record = $db->where('entity_id', $entityId)->get($this->tableName)->row();
|
|
$this->assertEquals('failed', $record->status, 'Status should be failed');
|
|
$this->assertEquals($errorMessage, $record->error_message, 'Error message should be stored');
|
|
$this->assertEquals(3, $record->attempts, 'Should have maximum attempts');
|
|
}
|
|
|
|
/**
|
|
* Test retry logic
|
|
*/
|
|
public function testRetryLogic()
|
|
{
|
|
$db = $this->ci->db;
|
|
|
|
$entityId = rand(40000, 49999);
|
|
|
|
// Insert failed task that can be retried
|
|
$data = [
|
|
'task_type' => 'sync_product',
|
|
'entity_type' => 'product',
|
|
'entity_id' => $entityId,
|
|
'priority' => 5,
|
|
'status' => 'retry',
|
|
'attempts' => 1,
|
|
'max_attempts' => 3,
|
|
'error_message' => 'Temporary API error',
|
|
'scheduled_at' => date('Y-m-d H:i:s', strtotime('+5 minutes'))
|
|
];
|
|
|
|
$this->assertTrue($db->insert($this->tableName, $data), 'Retry task should insert');
|
|
|
|
$record = $db->where('entity_id', $entityId)->get($this->tableName)->row();
|
|
$this->assertEquals('retry', $record->status, 'Status should be retry');
|
|
$this->assertEquals(1, $record->attempts, 'Should have 1 attempt');
|
|
$this->assertLessThan(3, $record->attempts, 'Should be below max attempts');
|
|
}
|
|
|
|
/**
|
|
* Test performance indexes exist
|
|
*/
|
|
public function testPerformanceIndexes()
|
|
{
|
|
$db = $this->ci->db;
|
|
|
|
$query = "SHOW INDEX FROM {$this->tableName}";
|
|
$indexes = $db->query($query)->result_array();
|
|
|
|
$indexNames = array_column($indexes, 'Key_name');
|
|
|
|
// Expected indexes for queue processing performance
|
|
$expectedIndexes = [
|
|
'PRIMARY',
|
|
'idx_status_priority',
|
|
'idx_entity',
|
|
'idx_scheduled',
|
|
'idx_task_status',
|
|
'idx_attempts',
|
|
'idx_created_at'
|
|
];
|
|
|
|
foreach ($expectedIndexes as $expectedIndex) {
|
|
$this->assertContains($expectedIndex, $indexNames,
|
|
"Index '{$expectedIndex}' should exist for queue processing performance");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Test queue processing query performance
|
|
*/
|
|
public function testQueueProcessingQueries()
|
|
{
|
|
$db = $this->ci->db;
|
|
|
|
// Insert test queue items
|
|
$testTasks = [
|
|
['task_type' => 'sync_client', 'entity_type' => 'client', 'entity_id' => 50001, 'priority' => 1, 'status' => 'pending'],
|
|
['task_type' => 'sync_product', 'entity_type' => 'product', 'entity_id' => 50002, 'priority' => 3, 'status' => 'pending'],
|
|
['task_type' => 'sync_invoice', 'entity_type' => 'invoice', 'entity_id' => 50003, 'priority' => 2, 'status' => 'processing'],
|
|
['task_type' => 'status_update', 'entity_type' => 'invoice', 'entity_id' => 50004, 'priority' => 5, 'status' => 'completed']
|
|
];
|
|
|
|
foreach ($testTasks as $task) {
|
|
$db->insert($this->tableName, $task);
|
|
}
|
|
|
|
// Test typical queue processing query
|
|
$pendingTasks = $db->where('status', 'pending')
|
|
->where('scheduled_at <=', date('Y-m-d H:i:s'))
|
|
->order_by('priority', 'ASC')
|
|
->order_by('scheduled_at', 'ASC')
|
|
->limit(10)
|
|
->get($this->tableName)
|
|
->result_array();
|
|
|
|
$this->assertGreaterThan(0, count($pendingTasks), 'Should find pending tasks');
|
|
|
|
// Verify priority ordering
|
|
if (count($pendingTasks) > 1) {
|
|
$this->assertLessThanOrEqual($pendingTasks[1]['priority'], $pendingTasks[0]['priority'],
|
|
'Tasks should be ordered by priority (ascending)');
|
|
}
|
|
|
|
// Test entity-specific queries
|
|
$clientTasks = $db->where('entity_type', 'client')
|
|
->where('entity_id', 50001)
|
|
->get($this->tableName)
|
|
->result_array();
|
|
|
|
$this->assertEquals(1, count($clientTasks), 'Should find entity-specific tasks');
|
|
}
|
|
|
|
/**
|
|
* Test timestamp fields auto-population
|
|
*/
|
|
public function testTimestampFields()
|
|
{
|
|
$db = $this->ci->db;
|
|
|
|
$beforeInsert = time();
|
|
|
|
$data = [
|
|
'task_type' => 'sync_estimate',
|
|
'entity_type' => 'estimate',
|
|
'entity_id' => rand(60000, 69999),
|
|
'priority' => 5
|
|
];
|
|
|
|
$this->assertTrue($db->insert($this->tableName, $data), 'Insert should succeed');
|
|
|
|
$afterInsert = time();
|
|
|
|
$record = $db->where('entity_id', $data['entity_id'])->get($this->tableName)->row();
|
|
|
|
// Verify created_at is populated
|
|
$this->assertNotNull($record->created_at, 'created_at should be auto-populated');
|
|
$createdTimestamp = strtotime($record->created_at);
|
|
$this->assertGreaterThanOrEqual($beforeInsert, $createdTimestamp, 'created_at should be recent');
|
|
$this->assertLessThanOrEqual($afterInsert, $createdTimestamp, 'created_at should not be in future');
|
|
|
|
// Verify scheduled_at is populated
|
|
$this->assertNotNull($record->scheduled_at, 'scheduled_at should be auto-populated');
|
|
|
|
// Verify optional timestamps are NULL
|
|
$this->assertNull($record->started_at, 'started_at should be NULL initially');
|
|
$this->assertNull($record->completed_at, 'completed_at should be NULL initially');
|
|
}
|
|
|
|
/**
|
|
* Helper method to clear test data
|
|
*/
|
|
private function clearTestData()
|
|
{
|
|
$db = $this->ci->db;
|
|
|
|
// Clear test data using wide entity_id ranges
|
|
$db->where('entity_id >=', 1)
|
|
->where('entity_id <=', 69999)
|
|
->delete($this->tableName);
|
|
}
|
|
|
|
/**
|
|
* Test character set and collation
|
|
*/
|
|
public function testCharacterSetAndCollation()
|
|
{
|
|
$db = $this->ci->db;
|
|
|
|
$query = "SELECT TABLE_COLLATION
|
|
FROM information_schema.TABLES
|
|
WHERE TABLE_SCHEMA = DATABASE()
|
|
AND TABLE_NAME = '{$this->tableName}'";
|
|
|
|
$result = $db->query($query)->row();
|
|
|
|
$this->assertEquals('utf8mb4_unicode_ci', $result->TABLE_COLLATION,
|
|
'Table should use utf8mb4_unicode_ci collation for proper Unicode support');
|
|
}
|
|
|
|
/**
|
|
* Test storage engine
|
|
*/
|
|
public function testStorageEngine()
|
|
{
|
|
$db = $this->ci->db;
|
|
|
|
$query = "SELECT ENGINE
|
|
FROM information_schema.TABLES
|
|
WHERE TABLE_SCHEMA = DATABASE()
|
|
AND TABLE_NAME = '{$this->tableName}'";
|
|
|
|
$result = $db->query($query)->row();
|
|
|
|
$this->assertEquals('InnoDB', $result->ENGINE,
|
|
'Table should use InnoDB engine for ACID compliance and transaction support');
|
|
}
|
|
} |