Files
desk-moloni/modules/desk_moloni/tests/contract/LogTableTest.php
Emanuel Almeida c19f6fd9ee 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.
2025-09-11 17:38:45 +01:00

399 lines
15 KiB
PHP

<?php
/**
* Contract Test for desk_moloni_sync_log table
*
* This test MUST FAIL until the Sync_log_model is properly implemented
* Following TDD RED-GREEN-REFACTOR cycle
*
* @package DeskMoloni\Tests\Contract
*/
namespace DeskMoloni\Tests\Contract;
use PHPUnit\Framework\TestCase;
class LogTableTest 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_sync_log table must exist with correct structure
*/
public function log_table_exists_with_required_structure()
{
// ACT: Check table existence
$table_exists = $this->db->table_exists('desk_moloni_sync_log');
// ASSERT: Table must exist
$this->assertTrue($table_exists, 'desk_moloni_sync_log table must exist');
// ASSERT: Required columns exist
$fields = $this->db->list_fields('desk_moloni_sync_log');
$required_fields = [
'id', 'operation_type', 'entity_type', 'perfex_id', 'moloni_id',
'direction', 'status', 'request_data', 'response_data',
'error_message', 'execution_time_ms', 'created_at'
];
foreach ($required_fields as $field) {
$this->assertContains($field, $fields, "Required field '{$field}' must exist in desk_moloni_sync_log table");
}
}
/**
* @test
* Contract: Log table must enforce operation_type ENUM values
*/
public function log_table_enforces_operation_type_enum()
{
// ARRANGE: Clean table
$this->db->truncate('desk_moloni_sync_log');
// ACT & ASSERT: Valid operation types should work
$valid_operations = ['create', 'update', 'delete', 'status_change'];
foreach ($valid_operations as $operation) {
$test_data = [
'operation_type' => $operation,
'entity_type' => 'client',
'perfex_id' => 1,
'moloni_id' => 1,
'direction' => 'perfex_to_moloni',
'status' => 'success'
];
$insert_success = $this->db->insert('desk_moloni_sync_log', $test_data);
$this->assertTrue($insert_success, "Operation type '{$operation}' must be valid");
// Clean up
$this->db->delete('desk_moloni_sync_log', ['operation_type' => $operation]);
}
// ACT & ASSERT: Invalid operation type should fail
$invalid_data = [
'operation_type' => 'invalid_operation',
'entity_type' => 'client',
'perfex_id' => 1,
'moloni_id' => 1,
'direction' => 'perfex_to_moloni',
'status' => 'success'
];
$this->expectException(\Exception::class);
$this->db->insert('desk_moloni_sync_log', $invalid_data);
}
/**
* @test
* Contract: Log table must enforce direction ENUM values
*/
public function log_table_enforces_direction_enum()
{
// ARRANGE: Clean table
$this->db->truncate('desk_moloni_sync_log');
// ACT & ASSERT: Valid directions should work
$valid_directions = ['perfex_to_moloni', 'moloni_to_perfex'];
foreach ($valid_directions as $direction) {
$test_data = [
'operation_type' => 'create',
'entity_type' => 'invoice',
'perfex_id' => 10,
'moloni_id' => 20,
'direction' => $direction,
'status' => 'success'
];
$insert_success = $this->db->insert('desk_moloni_sync_log', $test_data);
$this->assertTrue($insert_success, "Direction '{$direction}' must be valid");
// Clean up
$this->db->delete('desk_moloni_sync_log', ['direction' => $direction]);
}
// ACT & ASSERT: Invalid direction should fail
$invalid_data = [
'operation_type' => 'create',
'entity_type' => 'invoice',
'perfex_id' => 10,
'moloni_id' => 20,
'direction' => 'invalid_direction',
'status' => 'success'
];
$this->expectException(\Exception::class);
$this->db->insert('desk_moloni_sync_log', $invalid_data);
}
/**
* @test
* Contract: Log table must enforce status ENUM values
*/
public function log_table_enforces_status_enum()
{
// ARRANGE: Clean table
$this->db->truncate('desk_moloni_sync_log');
// ACT & ASSERT: Valid status values should work
$valid_statuses = ['success', 'error', 'warning'];
foreach ($valid_statuses as $status) {
$test_data = [
'operation_type' => 'update',
'entity_type' => 'product',
'perfex_id' => 30,
'moloni_id' => 40,
'direction' => 'moloni_to_perfex',
'status' => $status
];
$insert_success = $this->db->insert('desk_moloni_sync_log', $test_data);
$this->assertTrue($insert_success, "Status '{$status}' must be valid");
// Clean up
$this->db->delete('desk_moloni_sync_log', ['status' => $status]);
}
// ACT & ASSERT: Invalid status should fail
$invalid_data = [
'operation_type' => 'update',
'entity_type' => 'product',
'perfex_id' => 30,
'moloni_id' => 40,
'direction' => 'moloni_to_perfex',
'status' => 'invalid_status'
];
$this->expectException(\Exception::class);
$this->db->insert('desk_moloni_sync_log', $invalid_data);
}
/**
* @test
* Contract: Log table must support JSON storage for request and response data
*/
public function log_table_supports_json_request_response_data()
{
// ARRANGE: Clean table
$this->db->truncate('desk_moloni_sync_log');
// ACT: Insert log entry with JSON request/response data
$request_data = [
'method' => 'POST',
'endpoint' => '/customers',
'headers' => ['Authorization' => 'Bearer token123'],
'body' => [
'name' => 'Test Company',
'vat' => '123456789',
'email' => 'test@company.com'
]
];
$response_data = [
'status_code' => 201,
'headers' => ['Content-Type' => 'application/json'],
'body' => [
'customer_id' => 456,
'message' => 'Customer created successfully'
]
];
$log_data = [
'operation_type' => 'create',
'entity_type' => 'client',
'perfex_id' => 100,
'moloni_id' => 456,
'direction' => 'perfex_to_moloni',
'status' => 'success',
'request_data' => json_encode($request_data),
'response_data' => json_encode($response_data),
'execution_time_ms' => 1500
];
$insert_success = $this->db->insert('desk_moloni_sync_log', $log_data);
$this->assertTrue($insert_success, 'Log entry with JSON data must be inserted successfully');
// ASSERT: JSON data is stored and retrieved correctly
$row = $this->db->get_where('desk_moloni_sync_log', ['perfex_id' => 100, 'moloni_id' => 456])->row();
$retrieved_request = json_decode($row->request_data, true);
$retrieved_response = json_decode($row->response_data, true);
$this->assertEquals($request_data, $retrieved_request, 'Request data JSON must be stored and retrieved correctly');
$this->assertEquals($response_data, $retrieved_response, 'Response data JSON must be stored and retrieved correctly');
$this->assertEquals('Test Company', $retrieved_request['body']['name'], 'Nested request data must be accessible');
$this->assertEquals(456, $retrieved_response['body']['customer_id'], 'Nested response data must be accessible');
}
/**
* @test
* Contract: Log table must support performance monitoring with execution time
*/
public function log_table_supports_performance_monitoring()
{
// ARRANGE: Clean table
$this->db->truncate('desk_moloni_sync_log');
// ACT: Insert log entries with different execution times
$performance_logs = [
['execution_time_ms' => 50, 'entity_id' => 501], // Fast operation
['execution_time_ms' => 2500, 'entity_id' => 502], // Slow operation
['execution_time_ms' => 15000, 'entity_id' => 503], // Very slow operation
];
foreach ($performance_logs as $log) {
$log_data = [
'operation_type' => 'create',
'entity_type' => 'invoice',
'perfex_id' => $log['entity_id'],
'moloni_id' => $log['entity_id'] + 1000,
'direction' => 'perfex_to_moloni',
'status' => 'success',
'execution_time_ms' => $log['execution_time_ms']
];
$this->db->insert('desk_moloni_sync_log', $log_data);
}
// ASSERT: Performance data can be queried and analyzed
$this->db->select('AVG(execution_time_ms) as avg_time, MAX(execution_time_ms) as max_time, MIN(execution_time_ms) as min_time');
$this->db->where('entity_type', 'invoice');
$performance_stats = $this->db->get('desk_moloni_sync_log')->row();
$this->assertEquals(5850, $performance_stats->avg_time, 'Average execution time must be calculable');
$this->assertEquals(15000, $performance_stats->max_time, 'Maximum execution time must be retrievable');
$this->assertEquals(50, $performance_stats->min_time, 'Minimum execution time must be retrievable');
// ASSERT: Slow operations can be identified
$slow_operations = $this->db->get_where('desk_moloni_sync_log', 'execution_time_ms > 10000')->result();
$this->assertCount(1, $slow_operations, 'Slow operations must be identifiable for optimization');
}
/**
* @test
* Contract: Log table must support NULL perfex_id or moloni_id for failed operations
*/
public function log_table_supports_null_entity_ids()
{
// ARRANGE: Clean table
$this->db->truncate('desk_moloni_sync_log');
// ACT: Insert log entry with NULL perfex_id (creation failed before getting Perfex ID)
$failed_creation = [
'operation_type' => 'create',
'entity_type' => 'client',
'perfex_id' => null,
'moloni_id' => 789,
'direction' => 'moloni_to_perfex',
'status' => 'error',
'error_message' => 'Perfex client creation failed due to validation error'
];
$insert_success = $this->db->insert('desk_moloni_sync_log', $failed_creation);
$this->assertTrue($insert_success, 'Log entry with NULL perfex_id must be allowed');
// ACT: Insert log entry with NULL moloni_id (Moloni creation failed)
$failed_moloni_creation = [
'operation_type' => 'create',
'entity_type' => 'product',
'perfex_id' => 123,
'moloni_id' => null,
'direction' => 'perfex_to_moloni',
'status' => 'error',
'error_message' => 'Moloni product creation failed due to API error'
];
$insert_success2 = $this->db->insert('desk_moloni_sync_log', $failed_moloni_creation);
$this->assertTrue($insert_success2, 'Log entry with NULL moloni_id must be allowed');
// ASSERT: NULL values are handled correctly
$null_perfex = $this->db->get_where('desk_moloni_sync_log', ['moloni_id' => 789])->row();
$null_moloni = $this->db->get_where('desk_moloni_sync_log', ['perfex_id' => 123])->row();
$this->assertNull($null_perfex->perfex_id, 'perfex_id must be NULL when creation fails');
$this->assertNull($null_moloni->moloni_id, 'moloni_id must be NULL when Moloni creation fails');
}
/**
* @test
* Contract: Log table must have required indexes for analytics and performance
*/
public function log_table_has_required_indexes()
{
// ACT: Get table indexes
$indexes = $this->db->query("SHOW INDEX FROM desk_moloni_sync_log")->result_array();
// ASSERT: Required indexes exist for analytics and performance
$required_indexes = [
'PRIMARY',
'idx_entity_status',
'idx_perfex_entity',
'idx_moloni_entity',
'idx_created_at',
'idx_status_direction',
'idx_log_analytics'
];
$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 for log analytics");
}
}
/**
* @test
* Contract: Log table must support automatic created_at timestamp
*/
public function log_table_has_automatic_created_at()
{
// ARRANGE: Clean table
$this->db->truncate('desk_moloni_sync_log');
// ACT: Insert log entry without specifying created_at
$log_data = [
'operation_type' => 'update',
'entity_type' => 'estimate',
'perfex_id' => 999,
'moloni_id' => 888,
'direction' => 'bidirectional',
'status' => 'success',
'execution_time_ms' => 750
];
$this->db->insert('desk_moloni_sync_log', $log_data);
// ASSERT: created_at is automatically set and is recent
$row = $this->db->get_where('desk_moloni_sync_log', ['perfex_id' => 999])->row();
$this->assertNotNull($row->created_at, 'created_at must be automatically set');
$created_time = strtotime($row->created_at);
$current_time = time();
$this->assertLessThan(5, abs($current_time - $created_time), 'created_at must be recent timestamp');
}
protected function tearDown(): void
{
// Clean up test data
if ($this->db) {
$this->db->where('perfex_id IS NOT NULL OR moloni_id IS NOT NULL');
$this->db->where('(perfex_id <= 1000 OR moloni_id <= 1000)');
$this->db->delete('desk_moloni_sync_log');
}
}
}