/** * Descomplicar® Crescimento Digital * https://descomplicar.pt */ 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'); } } }