- 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>
404 lines
15 KiB
PHP
404 lines
15 KiB
PHP
/**
|
|
* Descomplicar® Crescimento Digital
|
|
* https://descomplicar.pt
|
|
*/
|
|
|
|
<?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');
|
|
}
|
|
}
|
|
} |