CI = &get_instance(); $this->CI->load->database(); $this->db = $this->CI->db; // Ensure we're in test environment if (ENVIRONMENT !== 'testing') { $this->markTestSkipped('Contract tests should only run in testing environment'); } } /** * @test * Contract: desk_moloni_config table must exist with correct structure */ public function config_table_exists_with_required_structure() { // ARRANGE: Test database table existence and structure // ACT: Query table structure $table_exists = $this->db->table_exists('desk_moloni_config'); // ASSERT: Table must exist $this->assertTrue($table_exists, 'desk_moloni_config table must exist'); // ASSERT: Required columns exist with correct types $fields = $this->db->list_fields('desk_moloni_config'); $required_fields = ['id', 'setting_key', 'setting_value', 'encrypted', 'created_at', 'updated_at']; foreach ($required_fields as $field) { $this->assertContains($field, $fields, "Required field '{$field}' must exist in desk_moloni_config table"); } // ASSERT: Check field types and constraints $field_data = $this->db->field_data('desk_moloni_config'); $field_info = []; foreach ($field_data as $field) { $field_info[$field->name] = $field; } // Verify setting_key is unique $this->assertEquals('varchar', strtolower($field_info['setting_key']->type), 'setting_key must be varchar type'); $this->assertEquals(255, $field_info['setting_key']->max_length, 'setting_key must have max_length of 255'); // Verify encrypted is boolean (tinyint in MySQL) $this->assertEquals('tinyint', strtolower($field_info['encrypted']->type), 'encrypted must be tinyint type'); $this->assertEquals(1, $field_info['encrypted']->default_value, 'encrypted must have default value of 0'); } /** * @test * Contract: Config table must enforce unique constraint on setting_key */ public function config_table_enforces_unique_setting_key() { // ARRANGE: Clean table and insert test data $this->db->truncate('desk_moloni_config'); $test_data = [ 'setting_key' => 'test_unique_key', 'setting_value' => 'test_value', 'encrypted' => 0 ]; // ACT & ASSERT: First insert should succeed $first_insert = $this->db->insert('desk_moloni_config', $test_data); $this->assertTrue($first_insert, 'First insert with unique key should succeed'); // ACT & ASSERT: Second insert with same key should fail $this->expectException(\Exception::class); $this->db->insert('desk_moloni_config', $test_data); } /** * @test * Contract: Config table must have proper indexes for performance */ public function config_table_has_required_indexes() { // ACT: Get table indexes $indexes = $this->db->query("SHOW INDEX FROM desk_moloni_config")->result_array(); // ASSERT: Primary key exists $has_primary = false; $has_setting_key_index = false; foreach ($indexes as $index) { if ($index['Key_name'] === 'PRIMARY') { $has_primary = true; } if ($index['Key_name'] === 'idx_setting_key') { $has_setting_key_index = true; } } $this->assertTrue($has_primary, 'Table must have PRIMARY KEY'); $this->assertTrue($has_setting_key_index, 'Table must have idx_setting_key index for performance'); } /** * @test * Contract: Config table must support encrypted and non-encrypted values */ public function config_table_supports_encryption_flag() { // ARRANGE: Clean table $this->db->truncate('desk_moloni_config'); // ACT: Insert encrypted and non-encrypted test data $encrypted_data = [ 'setting_key' => 'oauth_access_token', 'setting_value' => 'encrypted_token_value', 'encrypted' => 1 ]; $plain_data = [ 'setting_key' => 'api_base_url', 'setting_value' => 'https://api.moloni.pt/v1', 'encrypted' => 0 ]; $this->db->insert('desk_moloni_config', $encrypted_data); $this->db->insert('desk_moloni_config', $plain_data); // ASSERT: Data inserted correctly with proper encryption flags $encrypted_row = $this->db->get_where('desk_moloni_config', ['setting_key' => 'oauth_access_token'])->row(); $plain_row = $this->db->get_where('desk_moloni_config', ['setting_key' => 'api_base_url'])->row(); $this->assertEquals(1, $encrypted_row->encrypted, 'Encrypted flag must be set for sensitive data'); $this->assertEquals(0, $plain_row->encrypted, 'Encrypted flag must be false for plain data'); } /** * @test * Contract: Config table must have automatic timestamps */ public function config_table_has_automatic_timestamps() { // ARRANGE: Clean table $this->db->truncate('desk_moloni_config'); // ACT: Insert test record $test_data = [ 'setting_key' => 'timestamp_test', 'setting_value' => 'test_value', 'encrypted' => 0 ]; $this->db->insert('desk_moloni_config', $test_data); // ASSERT: Timestamps are automatically set $row = $this->db->get_where('desk_moloni_config', ['setting_key' => 'timestamp_test'])->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 (within last 5 seconds) $created_time = strtotime($row->created_at); $current_time = time(); $this->assertLessThan(5, abs($current_time - $created_time), 'created_at must be recent'); } /** * @test * Contract: Config table must support TEXT values for large configurations */ public function config_table_supports_large_text_values() { // ARRANGE: Clean table $this->db->truncate('desk_moloni_config'); // ACT: Insert large value (simulate large JSON configuration) $large_value = str_repeat('{"large_config":' . str_repeat('"test"', 1000) . '}', 10); $test_data = [ 'setting_key' => 'large_config_test', 'setting_value' => $large_value, 'encrypted' => 0 ]; $insert_success = $this->db->insert('desk_moloni_config', $test_data); // ASSERT: Large values can be stored $this->assertTrue($insert_success, 'Table must support large TEXT values'); // ASSERT: Large value is retrieved correctly $row = $this->db->get_where('desk_moloni_config', ['setting_key' => 'large_config_test'])->row(); $this->assertEquals($large_value, $row->setting_value, 'Large values must be stored and retrieved correctly'); } protected function tearDown(): void { // Clean up test data if ($this->db) { $this->db->where('setting_key LIKE', 'test_%'); $this->db->or_where('setting_key LIKE', '%_test'); $this->db->delete('desk_moloni_config'); } } }