Files
desk-moloni/modules/desk_moloni/tests/database/MappingTableTest.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

477 lines
16 KiB
PHP

/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
/**
* MappingTableTest.php
*
* PHPUnit tests for desk_moloni_mapping table structure and validation rules
* Tests bidirectional entity mapping between Perfex and Moloni
*
* @package DeskMoloni\Tests\Database
* @author Database Design Specialist
* @version 3.0
*/
require_once(__DIR__ . '/../../../../tests/TestCase.php');
class MappingTableTest extends TestCase
{
private $tableName = 'desk_moloni_mapping';
private $testMappingModel;
public function setUp(): void
{
parent::setUp();
$this->clearTestData();
// Initialize test model (will be implemented after tests)
// $this->testMappingModel = new DeskMoloniMapping();
}
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', 'entity_type', 'perfex_id', 'moloni_id', 'sync_direction',
'last_sync_at', 'created_at', 'updated_at'
];
foreach ($expectedColumns as $column) {
$this->assertTrue($db->field_exists($column, $this->tableName),
"Column '{$column}' should exist in {$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 = [
'entity_type' => $entityType,
'perfex_id' => rand(1, 1000),
'moloni_id' => rand(1, 1000),
'sync_direction' => 'bidirectional'
];
$this->assertTrue($db->insert($this->tableName, $data),
"Valid entity type '{$entityType}' should insert successfully");
// Clean up immediately to avoid constraint conflicts
$db->where('entity_type', $entityType)
->where('perfex_id', $data['perfex_id'])
->delete($this->tableName);
}
}
/**
* Test sync_direction ENUM values
*/
public function testSyncDirectionEnumValues()
{
$db = $this->ci->db;
$validDirections = ['perfex_to_moloni', 'moloni_to_perfex', 'bidirectional'];
foreach ($validDirections as $direction) {
$data = [
'entity_type' => 'client',
'perfex_id' => rand(1, 1000),
'moloni_id' => rand(1, 1000),
'sync_direction' => $direction
];
$this->assertTrue($db->insert($this->tableName, $data),
"Valid sync direction '{$direction}' should insert successfully");
// Verify stored value
$record = $db->where('perfex_id', $data['perfex_id'])->get($this->tableName)->row();
$this->assertEquals($direction, $record->sync_direction, "Sync direction should match inserted value");
// Clean up
$db->where('perfex_id', $data['perfex_id'])->delete($this->tableName);
}
}
/**
* Test default sync_direction value
*/
public function testDefaultSyncDirection()
{
$db = $this->ci->db;
$data = [
'entity_type' => 'product',
'perfex_id' => rand(1, 1000),
'moloni_id' => rand(1, 1000)
// sync_direction omitted to test default
];
$this->assertTrue($db->insert($this->tableName, $data), 'Insert without sync_direction should succeed');
$record = $db->where('perfex_id', $data['perfex_id'])->get($this->tableName)->row();
$this->assertEquals('bidirectional', $record->sync_direction, 'Default sync_direction should be bidirectional');
}
/**
* Test unique constraint on entity_type + perfex_id
*/
public function testUniquePerfexMapping()
{
$db = $this->ci->db;
// Insert first record
$data1 = [
'entity_type' => 'invoice',
'perfex_id' => 12345,
'moloni_id' => 54321,
'sync_direction' => 'bidirectional'
];
$this->assertTrue($db->insert($this->tableName, $data1), 'First mapping insert should succeed');
// Try to insert duplicate perfex mapping - should fail
$data2 = [
'entity_type' => 'invoice', // Same entity type
'perfex_id' => 12345, // Same perfex ID
'moloni_id' => 98765, // Different moloni ID
'sync_direction' => 'perfex_to_moloni'
];
$this->assertFalse($db->insert($this->tableName, $data2), 'Duplicate perfex mapping should fail');
$this->assertStringContainsString('Duplicate', $db->error()['message']);
}
/**
* Test unique constraint on entity_type + moloni_id
*/
public function testUniqueMoloniMapping()
{
$db = $this->ci->db;
// Insert first record
$data1 = [
'entity_type' => 'client',
'perfex_id' => 11111,
'moloni_id' => 22222,
'sync_direction' => 'bidirectional'
];
$this->assertTrue($db->insert($this->tableName, $data1), 'First mapping insert should succeed');
// Try to insert duplicate moloni mapping - should fail
$data2 = [
'entity_type' => 'client', // Same entity type
'perfex_id' => 33333, // Different perfex ID
'moloni_id' => 22222, // Same moloni ID
'sync_direction' => 'moloni_to_perfex'
];
$this->assertFalse($db->insert($this->tableName, $data2), 'Duplicate moloni mapping should fail');
$this->assertStringContainsString('Duplicate', $db->error()['message']);
}
/**
* Test that same IDs can exist for different entity types
*/
public function testDifferentEntityTypesAllowSameIds()
{
$db = $this->ci->db;
$sameId = 99999;
// Insert mappings with same IDs but different entity types
$mappings = [
['entity_type' => 'client', 'perfex_id' => $sameId, 'moloni_id' => $sameId],
['entity_type' => 'product', 'perfex_id' => $sameId, 'moloni_id' => $sameId],
['entity_type' => 'invoice', 'perfex_id' => $sameId, 'moloni_id' => $sameId]
];
foreach ($mappings as $mapping) {
$mapping['sync_direction'] = 'bidirectional';
$this->assertTrue($db->insert($this->tableName, $mapping),
"Same IDs should be allowed for different entity types: {$mapping['entity_type']}");
}
// Verify all records exist
$count = $db->where('perfex_id', $sameId)->count_all_results($this->tableName);
$this->assertEquals(3, $count, 'Should have 3 mappings with same IDs but different entity types');
}
/**
* Test last_sync_at timestamp handling
*/
public function testLastSyncAtTimestamp()
{
$db = $this->ci->db;
// Insert record without last_sync_at
$data = [
'entity_type' => 'estimate',
'perfex_id' => rand(1, 1000),
'moloni_id' => rand(1, 1000),
'sync_direction' => 'bidirectional'
];
$this->assertTrue($db->insert($this->tableName, $data), 'Insert should succeed');
$record = $db->where('perfex_id', $data['perfex_id'])->get($this->tableName)->row();
$this->assertNull($record->last_sync_at, 'last_sync_at should be NULL initially');
// Update with sync timestamp
$syncTime = date('Y-m-d H:i:s');
$db->where('perfex_id', $data['perfex_id'])->update($this->tableName, ['last_sync_at' => $syncTime]);
$updatedRecord = $db->where('perfex_id', $data['perfex_id'])->get($this->tableName)->row();
$this->assertEquals($syncTime, $updatedRecord->last_sync_at, 'last_sync_at should be updated');
}
/**
* 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
$expectedIndexes = [
'PRIMARY',
'unique_perfex_mapping',
'unique_moloni_mapping',
'idx_entity_perfex',
'idx_entity_moloni',
'idx_sync_direction',
'idx_last_sync',
'idx_created_at'
];
foreach ($expectedIndexes as $expectedIndex) {
$this->assertContains($expectedIndex, $indexNames,
"Index '{$expectedIndex}' should exist for performance optimization");
}
}
/**
* Test bidirectional mapping functionality
*/
public function testBidirectionalMappingScenarios()
{
$db = $this->ci->db;
// Test Perfex to Moloni sync
$perfexToMoloni = [
'entity_type' => 'client',
'perfex_id' => 100,
'moloni_id' => 200,
'sync_direction' => 'perfex_to_moloni'
];
$this->assertTrue($db->insert($this->tableName, $perfexToMoloni),
'Perfex to Moloni mapping should insert successfully');
// Test Moloni to Perfex sync
$moloniToPerfex = [
'entity_type' => 'product',
'perfex_id' => 300,
'moloni_id' => 400,
'sync_direction' => 'moloni_to_perfex'
];
$this->assertTrue($db->insert($this->tableName, $moloniToPerfex),
'Moloni to Perfex mapping should insert successfully');
// Test bidirectional sync
$bidirectional = [
'entity_type' => 'invoice',
'perfex_id' => 500,
'moloni_id' => 600,
'sync_direction' => 'bidirectional'
];
$this->assertTrue($db->insert($this->tableName, $bidirectional),
'Bidirectional mapping should insert successfully');
// Verify mappings can be retrieved by direction
$perfexDirection = $db->where('sync_direction', 'perfex_to_moloni')->count_all_results($this->tableName);
$this->assertGreaterThanOrEqual(1, $perfexDirection, 'Should find perfex_to_moloni mappings');
$moloniDirection = $db->where('sync_direction', 'moloni_to_perfex')->count_all_results($this->tableName);
$this->assertGreaterThanOrEqual(1, $moloniDirection, 'Should find moloni_to_perfex mappings');
$bidirectionalCount = $db->where('sync_direction', 'bidirectional')->count_all_results($this->tableName);
$this->assertGreaterThanOrEqual(1, $bidirectionalCount, 'Should find bidirectional mappings');
}
/**
* Test entity lookup by Perfex ID
*/
public function testPerfexEntityLookup()
{
$db = $this->ci->db;
$testMappings = [
['entity_type' => 'client', 'perfex_id' => 1001, 'moloni_id' => 2001],
['entity_type' => 'product', 'perfex_id' => 1002, 'moloni_id' => 2002],
['entity_type' => 'invoice', 'perfex_id' => 1003, 'moloni_id' => 2003]
];
foreach ($testMappings as $mapping) {
$mapping['sync_direction'] = 'bidirectional';
$db->insert($this->tableName, $mapping);
}
// Test lookup by entity type and perfex ID
foreach ($testMappings as $expected) {
$result = $db->where('entity_type', $expected['entity_type'])
->where('perfex_id', $expected['perfex_id'])
->get($this->tableName)
->row();
$this->assertNotNull($result, "Should find mapping for {$expected['entity_type']} with perfex_id {$expected['perfex_id']}");
$this->assertEquals($expected['moloni_id'], $result->moloni_id, 'Moloni ID should match');
}
}
/**
* Test entity lookup by Moloni ID
*/
public function testMoloniEntityLookup()
{
$db = $this->ci->db;
$testMappings = [
['entity_type' => 'estimate', 'perfex_id' => 3001, 'moloni_id' => 4001],
['entity_type' => 'credit_note', 'perfex_id' => 3002, 'moloni_id' => 4002]
];
foreach ($testMappings as $mapping) {
$mapping['sync_direction'] = 'bidirectional';
$db->insert($this->tableName, $mapping);
}
// Test lookup by entity type and moloni ID
foreach ($testMappings as $expected) {
$result = $db->where('entity_type', $expected['entity_type'])
->where('moloni_id', $expected['moloni_id'])
->get($this->tableName)
->row();
$this->assertNotNull($result, "Should find mapping for {$expected['entity_type']} with moloni_id {$expected['moloni_id']}");
$this->assertEquals($expected['perfex_id'], $result->perfex_id, 'Perfex ID should match');
}
}
/**
* Test timestamp fields auto-population
*/
public function testTimestampFields()
{
$db = $this->ci->db;
$beforeInsert = time();
$data = [
'entity_type' => 'client',
'perfex_id' => rand(5000, 9999),
'moloni_id' => rand(5000, 9999),
'sync_direction' => 'bidirectional'
];
$this->assertTrue($db->insert($this->tableName, $data), 'Insert should succeed');
$afterInsert = time();
$record = $db->where('perfex_id', $data['perfex_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 updated_at is populated
$this->assertNotNull($record->updated_at, 'updated_at should be auto-populated');
$this->assertEquals($record->created_at, $record->updated_at, 'Initially created_at should equal updated_at');
}
/**
* Helper method to clear test data
*/
private function clearTestData()
{
$db = $this->ci->db;
// Clear all test data - using wide range to catch test IDs
$db->where('perfex_id >=', 1)
->where('perfex_id <=', 9999)
->delete($this->tableName);
$db->where('moloni_id >=', 1)
->where('moloni_id <=', 9999)
->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 foreign key support');
}
}