- 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.
283 lines
9.8 KiB
PHP
283 lines
9.8 KiB
PHP
<?php
|
|
/**
|
|
* Contract Test for desk_moloni_mapping table
|
|
*
|
|
* This test MUST FAIL until the Mapping_model is properly implemented
|
|
* Following TDD RED-GREEN-REFACTOR cycle
|
|
*
|
|
* @package DeskMoloni\Tests\Contract
|
|
*/
|
|
|
|
namespace DeskMoloni\Tests\Contract;
|
|
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
class MappingTableTest 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_mapping table must exist with correct structure
|
|
*/
|
|
public function mapping_table_exists_with_required_structure()
|
|
{
|
|
// ACT: Check table existence
|
|
$table_exists = $this->db->table_exists('desk_moloni_mapping');
|
|
|
|
// ASSERT: Table must exist
|
|
$this->assertTrue($table_exists, 'desk_moloni_mapping table must exist');
|
|
|
|
// ASSERT: Required columns exist
|
|
$fields = $this->db->list_fields('desk_moloni_mapping');
|
|
$required_fields = ['id', 'entity_type', 'perfex_id', 'moloni_id', 'sync_direction', 'last_sync_at', 'created_at', 'updated_at'];
|
|
|
|
foreach ($required_fields as $field) {
|
|
$this->assertContains($field, $fields, "Required field '{$field}' must exist in desk_moloni_mapping table");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @test
|
|
* Contract: Mapping table must enforce entity_type ENUM values
|
|
*/
|
|
public function mapping_table_enforces_entity_type_enum()
|
|
{
|
|
// ARRANGE: Clean table
|
|
$this->db->truncate('desk_moloni_mapping');
|
|
|
|
// ACT & ASSERT: Valid entity types should work
|
|
$valid_types = ['client', 'product', 'invoice', 'estimate', 'credit_note'];
|
|
|
|
foreach ($valid_types as $type) {
|
|
$test_data = [
|
|
'entity_type' => $type,
|
|
'perfex_id' => 1,
|
|
'moloni_id' => 1,
|
|
'sync_direction' => 'bidirectional'
|
|
];
|
|
|
|
$insert_success = $this->db->insert('desk_moloni_mapping', $test_data);
|
|
$this->assertTrue($insert_success, "Entity type '{$type}' must be valid");
|
|
|
|
// Clean up for next iteration
|
|
$this->db->delete('desk_moloni_mapping', ['entity_type' => $type]);
|
|
}
|
|
|
|
// ACT & ASSERT: Invalid entity type should fail
|
|
$invalid_data = [
|
|
'entity_type' => 'invalid_type',
|
|
'perfex_id' => 1,
|
|
'moloni_id' => 1,
|
|
'sync_direction' => 'bidirectional'
|
|
];
|
|
|
|
$this->expectException(\Exception::class);
|
|
$this->db->insert('desk_moloni_mapping', $invalid_data);
|
|
}
|
|
|
|
/**
|
|
* @test
|
|
* Contract: Mapping table must enforce sync_direction ENUM values
|
|
*/
|
|
public function mapping_table_enforces_sync_direction_enum()
|
|
{
|
|
// ARRANGE: Clean table
|
|
$this->db->truncate('desk_moloni_mapping');
|
|
|
|
// ACT & ASSERT: Valid sync directions should work
|
|
$valid_directions = ['perfex_to_moloni', 'moloni_to_perfex', 'bidirectional'];
|
|
|
|
foreach ($valid_directions as $direction) {
|
|
$test_data = [
|
|
'entity_type' => 'client',
|
|
'perfex_id' => 1,
|
|
'moloni_id' => 1,
|
|
'sync_direction' => $direction
|
|
];
|
|
|
|
$insert_success = $this->db->insert('desk_moloni_mapping', $test_data);
|
|
$this->assertTrue($insert_success, "Sync direction '{$direction}' must be valid");
|
|
|
|
// Clean up for next iteration
|
|
$this->db->delete('desk_moloni_mapping', ['sync_direction' => $direction]);
|
|
}
|
|
|
|
// ACT & ASSERT: Invalid sync direction should fail
|
|
$invalid_data = [
|
|
'entity_type' => 'client',
|
|
'perfex_id' => 1,
|
|
'moloni_id' => 1,
|
|
'sync_direction' => 'invalid_direction'
|
|
];
|
|
|
|
$this->expectException(\Exception::class);
|
|
$this->db->insert('desk_moloni_mapping', $invalid_data);
|
|
}
|
|
|
|
/**
|
|
* @test
|
|
* Contract: Mapping table must enforce unique constraints
|
|
*/
|
|
public function mapping_table_enforces_unique_constraints()
|
|
{
|
|
// ARRANGE: Clean table
|
|
$this->db->truncate('desk_moloni_mapping');
|
|
|
|
$test_data = [
|
|
'entity_type' => 'client',
|
|
'perfex_id' => 123,
|
|
'moloni_id' => 456,
|
|
'sync_direction' => 'bidirectional'
|
|
];
|
|
|
|
// ACT & ASSERT: First insert should succeed
|
|
$first_insert = $this->db->insert('desk_moloni_mapping', $test_data);
|
|
$this->assertTrue($first_insert, 'First insert with unique mapping should succeed');
|
|
|
|
// ACT & ASSERT: Duplicate perfex_id for same entity_type should fail
|
|
$duplicate_perfex = [
|
|
'entity_type' => 'client',
|
|
'perfex_id' => 123,
|
|
'moloni_id' => 789,
|
|
'sync_direction' => 'bidirectional'
|
|
];
|
|
|
|
$this->expectException(\Exception::class);
|
|
$this->db->insert('desk_moloni_mapping', $duplicate_perfex);
|
|
}
|
|
|
|
/**
|
|
* @test
|
|
* Contract: Mapping table must have required indexes for performance
|
|
*/
|
|
public function mapping_table_has_required_indexes()
|
|
{
|
|
// ACT: Get table indexes
|
|
$indexes = $this->db->query("SHOW INDEX FROM desk_moloni_mapping")->result_array();
|
|
|
|
// ASSERT: Required indexes exist
|
|
$required_indexes = [
|
|
'PRIMARY',
|
|
'unique_perfex_mapping',
|
|
'unique_moloni_mapping',
|
|
'idx_entity_perfex',
|
|
'idx_entity_moloni',
|
|
'idx_last_sync'
|
|
];
|
|
|
|
$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");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @test
|
|
* Contract: Mapping table must support bidirectional relationships
|
|
*/
|
|
public function mapping_table_supports_bidirectional_relationships()
|
|
{
|
|
// ARRANGE: Clean table
|
|
$this->db->truncate('desk_moloni_mapping');
|
|
|
|
// ACT: Insert bidirectional mapping
|
|
$bidirectional_data = [
|
|
'entity_type' => 'invoice',
|
|
'perfex_id' => 100,
|
|
'moloni_id' => 200,
|
|
'sync_direction' => 'bidirectional',
|
|
'last_sync_at' => date('Y-m-d H:i:s')
|
|
];
|
|
|
|
$insert_success = $this->db->insert('desk_moloni_mapping', $bidirectional_data);
|
|
$this->assertTrue($insert_success, 'Bidirectional mapping must be supported');
|
|
|
|
// ASSERT: Data retrieved correctly
|
|
$row = $this->db->get_where('desk_moloni_mapping', ['perfex_id' => 100, 'entity_type' => 'invoice'])->row();
|
|
|
|
$this->assertEquals('bidirectional', $row->sync_direction, 'Bidirectional sync direction must be stored');
|
|
$this->assertEquals(100, $row->perfex_id, 'Perfex ID must be stored correctly');
|
|
$this->assertEquals(200, $row->moloni_id, 'Moloni ID must be stored correctly');
|
|
$this->assertNotNull($row->last_sync_at, 'last_sync_at must support timestamp values');
|
|
}
|
|
|
|
/**
|
|
* @test
|
|
* Contract: Mapping table must allow NULL last_sync_at for new mappings
|
|
*/
|
|
public function mapping_table_allows_null_last_sync_at()
|
|
{
|
|
// ARRANGE: Clean table
|
|
$this->db->truncate('desk_moloni_mapping');
|
|
|
|
// ACT: Insert mapping without last_sync_at
|
|
$new_mapping_data = [
|
|
'entity_type' => 'product',
|
|
'perfex_id' => 50,
|
|
'moloni_id' => 75,
|
|
'sync_direction' => 'perfex_to_moloni'
|
|
];
|
|
|
|
$insert_success = $this->db->insert('desk_moloni_mapping', $new_mapping_data);
|
|
$this->assertTrue($insert_success, 'New mapping without last_sync_at must be allowed');
|
|
|
|
// ASSERT: last_sync_at is NULL for new mappings
|
|
$row = $this->db->get_where('desk_moloni_mapping', ['perfex_id' => 50, 'entity_type' => 'product'])->row();
|
|
$this->assertNull($row->last_sync_at, 'last_sync_at must be NULL for new mappings');
|
|
}
|
|
|
|
/**
|
|
* @test
|
|
* Contract: Mapping table must have automatic created_at and updated_at timestamps
|
|
*/
|
|
public function mapping_table_has_automatic_timestamps()
|
|
{
|
|
// ARRANGE: Clean table
|
|
$this->db->truncate('desk_moloni_mapping');
|
|
|
|
// ACT: Insert mapping
|
|
$test_data = [
|
|
'entity_type' => 'estimate',
|
|
'perfex_id' => 25,
|
|
'moloni_id' => 35,
|
|
'sync_direction' => 'moloni_to_perfex'
|
|
];
|
|
|
|
$this->db->insert('desk_moloni_mapping', $test_data);
|
|
|
|
// ASSERT: Timestamps are automatically set
|
|
$row = $this->db->get_where('desk_moloni_mapping', ['perfex_id' => 25, 'entity_type' => 'estimate'])->row();
|
|
|
|
$this->assertNotNull($row->created_at, 'created_at must be automatically set');
|
|
$this->assertNotNull($row->updated_at, 'updated_at must be automatically set');
|
|
|
|
// ASSERT: Timestamps are recent
|
|
$created_time = strtotime($row->created_at);
|
|
$current_time = time();
|
|
$this->assertLessThan(5, abs($current_time - $created_time), 'created_at must be recent');
|
|
}
|
|
|
|
protected function tearDown(): void
|
|
{
|
|
// Clean up test data
|
|
if ($this->db) {
|
|
$this->db->where('perfex_id >=', 1);
|
|
$this->db->where('perfex_id <=', 200);
|
|
$this->db->delete('desk_moloni_mapping');
|
|
}
|
|
}
|
|
} |