Files
desk-moloni/modules/desk_moloni/tests/database/QueueTableTest.php
Emanuel Almeida 8c4f68576f chore: add spec-kit and standardize signatures
- 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>
2025-09-12 01:27:37 +01:00

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');
}
}