fix(perfexcrm module): align version to 3.0.1, unify entrypoint, and harden routes/views
- Bump DESK_MOLONI version to 3.0.1 across module - Normalize hooks to after_client_* and instantiate PerfexHooks safely - Fix OAuthController view path and API client class name - Add missing admin views for webhook config/logs; adjust view loading - Harden client portal routes and admin routes mapping - Make Dashboard/Logs/Queue tolerant to optional model methods - Align log details query with existing schema; avoid broken joins This makes the module operational in Perfex (admin + client), reduces 404s, and avoids fatal errors due to inconsistent tables/methods.
This commit is contained in:
541
modules/desk_moloni/tests/database/QueueTableTest.php
Normal file
541
modules/desk_moloni/tests/database/QueueTableTest.php
Normal file
@@ -0,0 +1,541 @@
|
||||
<?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');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user