"""Tests for IANA update functionality.""" import sqlite3 import pytest from sslysze_scan.commands.update_iana import ( calculate_diff, process_registry_with_validation, ) from sslysze_scan.iana_validator import ValidationError class TestCalculateDiff: """Tests for diff calculation between old and new data.""" def test_calculate_diff_no_changes(self) -> None: """Test diff calculation when data is unchanged.""" rows = [ ("0x13,0x01", "TLS_AES_128_GCM_SHA256", "Y", "Y", "rfc8446"), ("0x13,0x02", "TLS_AES_256_GCM_SHA384", "Y", "Y", "rfc8446"), ] diff = calculate_diff(rows, rows) assert len(diff["added"]) == 0 assert len(diff["deleted"]) == 0 assert len(diff["modified"]) == 0 def test_calculate_diff_added_rows(self) -> None: """Test diff calculation with added rows.""" old_rows = [ ("0x13,0x01", "TLS_AES_128_GCM_SHA256", "Y", "Y", "rfc8446"), ] new_rows = [ ("0x13,0x01", "TLS_AES_128_GCM_SHA256", "Y", "Y", "rfc8446"), ("0x13,0x02", "TLS_AES_256_GCM_SHA384", "Y", "Y", "rfc8446"), ] diff = calculate_diff(old_rows, new_rows) assert len(diff["added"]) == 1 assert "0x13,0x02" in diff["added"] assert len(diff["deleted"]) == 0 assert len(diff["modified"]) == 0 def test_calculate_diff_deleted_rows(self) -> None: """Test diff calculation with deleted rows.""" old_rows = [ ("0x13,0x01", "TLS_AES_128_GCM_SHA256", "Y", "Y", "rfc8446"), ("0x13,0x02", "TLS_AES_256_GCM_SHA384", "Y", "Y", "rfc8446"), ] new_rows = [ ("0x13,0x01", "TLS_AES_128_GCM_SHA256", "Y", "Y", "rfc8446"), ] diff = calculate_diff(old_rows, new_rows) assert len(diff["added"]) == 0 assert len(diff["deleted"]) == 1 assert "0x13,0x02" in diff["deleted"] assert len(diff["modified"]) == 0 def test_calculate_diff_modified_rows(self) -> None: """Test diff calculation with modified rows.""" old_rows = [ ("0x13,0x01", "TLS_AES_128_GCM_SHA256", "Y", "Y", "rfc8446"), ] new_rows = [ ("0x13,0x01", "TLS_AES_128_GCM_SHA256", "Y", "N", "rfc8446"), ] diff = calculate_diff(old_rows, new_rows) assert len(diff["added"]) == 0 assert len(diff["deleted"]) == 0 assert len(diff["modified"]) == 1 assert "0x13,0x01" in diff["modified"] def test_calculate_diff_mixed_changes(self) -> None: """Test diff calculation with mixed changes.""" old_rows = [ ("0x13,0x01", "TLS_AES_128_GCM_SHA256", "Y", "Y", "rfc8446"), ("0x13,0x02", "TLS_AES_256_GCM_SHA384", "Y", "Y", "rfc8446"), ("0x00,0x9C", "TLS_RSA_WITH_AES_128_GCM_SHA256", "Y", "N", "rfc5288"), ] new_rows = [ ("0x13,0x01", "TLS_AES_128_GCM_SHA256", "Y", "N", "rfc8446"), ("0x13,0x03", "TLS_CHACHA20_POLY1305_SHA256", "Y", "Y", "rfc8446"), ("0x00,0x9C", "TLS_RSA_WITH_AES_128_GCM_SHA256", "Y", "N", "rfc5288"), ] diff = calculate_diff(old_rows, new_rows) assert len(diff["added"]) == 1 assert "0x13,0x03" in diff["added"] assert len(diff["deleted"]) == 1 assert "0x13,0x02" in diff["deleted"] assert len(diff["modified"]) == 1 assert "0x13,0x01" in diff["modified"] class TestProcessRegistryWithValidation: """Tests for registry processing with validation.""" def test_process_valid_registry(self, test_db_path: str) -> None: """Test processing valid registry data.""" xml_path = "tests/fixtures/iana_xml/tls-parameters-minimal.xml" with open(xml_path, encoding="utf-8") as f: xml_content = f.read() conn = sqlite3.connect(test_db_path) headers = ["Value", "Description", "DTLS", "Recommended", "RFC/Draft"] row_count, diff = process_registry_with_validation( xml_content, "tls-parameters-4", "iana_tls_cipher_suites", headers, conn, skip_min_rows_check=True, ) assert row_count == 5 assert isinstance(diff, dict) assert "added" in diff assert "deleted" in diff assert "modified" in diff conn.close() def test_process_registry_invalid_headers(self, test_db_path: str) -> None: """Test that invalid headers raise ValidationError.""" xml_path = "tests/fixtures/iana_xml/tls-parameters-minimal.xml" with open(xml_path, encoding="utf-8") as f: xml_content = f.read() conn = sqlite3.connect(test_db_path) headers = ["Value", "Name"] with pytest.raises(ValidationError, match="Column count mismatch"): process_registry_with_validation( xml_content, "tls-parameters-4", "iana_tls_cipher_suites", headers, conn, skip_min_rows_check=True, ) conn.close() def test_process_registry_nonexistent_registry_id(self, test_db_path: str) -> None: """Test that nonexistent registry ID raises ValueError.""" xml_path = "tests/fixtures/iana_xml/tls-parameters-minimal.xml" with open(xml_path, encoding="utf-8") as f: xml_content = f.read() conn = sqlite3.connect(test_db_path) headers = ["Value", "Description", "DTLS", "Recommended", "RFC/Draft"] with pytest.raises(ValueError, match="Registry .* not found"): process_registry_with_validation( xml_content, "nonexistent-registry", "iana_tls_cipher_suites", headers, conn, skip_min_rows_check=True, ) conn.close() class TestTransactionHandling: """Tests for database transaction handling.""" def test_transaction_rollback_preserves_data(self, test_db_path: str) -> None: """Test that rollback preserves original data.""" conn = sqlite3.connect(test_db_path) cursor = conn.cursor() original_count = cursor.execute( "SELECT COUNT(*) FROM iana_tls_cipher_suites" ).fetchone()[0] conn.execute("BEGIN TRANSACTION") cursor.execute("DELETE FROM iana_tls_cipher_suites") after_delete = cursor.execute( "SELECT COUNT(*) FROM iana_tls_cipher_suites" ).fetchone()[0] assert after_delete == 0 conn.rollback() after_rollback = cursor.execute( "SELECT COUNT(*) FROM iana_tls_cipher_suites" ).fetchone()[0] assert after_rollback == original_count conn.close() def test_transaction_commit_persists_changes(self, test_db_path: str) -> None: """Test that commit persists changes.""" conn = sqlite3.connect(test_db_path) cursor = conn.cursor() conn.execute("BEGIN TRANSACTION") cursor.execute( """ INSERT INTO iana_tls_cipher_suites VALUES ('0xFF,0xFF', 'TEST_CIPHER', 'Y', 'N', 'test') """ ) conn.commit() result = cursor.execute( """ SELECT COUNT(*) FROM iana_tls_cipher_suites WHERE value = '0xFF,0xFF' """ ).fetchone()[0] assert result == 1 cursor.execute("DELETE FROM iana_tls_cipher_suites WHERE value = '0xFF,0xFF'") conn.commit() conn.close() class TestDiffCalculationEdgeCases: """Tests for edge cases in diff calculation.""" def test_calculate_diff_empty_old_rows(self) -> None: """Test diff with empty old rows (initial import).""" old_rows = [] new_rows = [ ("0x13,0x01", "TLS_AES_128_GCM_SHA256", "Y", "Y", "rfc8446"), ("0x13,0x02", "TLS_AES_256_GCM_SHA384", "Y", "Y", "rfc8446"), ] diff = calculate_diff(old_rows, new_rows) assert len(diff["added"]) == 2 assert len(diff["deleted"]) == 0 assert len(diff["modified"]) == 0 def test_calculate_diff_empty_new_rows(self) -> None: """Test diff with empty new rows (complete deletion).""" old_rows = [ ("0x13,0x01", "TLS_AES_128_GCM_SHA256", "Y", "Y", "rfc8446"), ] new_rows = [] diff = calculate_diff(old_rows, new_rows) assert len(diff["added"]) == 0 assert len(diff["deleted"]) == 1 assert len(diff["modified"]) == 0 def test_calculate_diff_different_pk_index(self) -> None: """Test diff calculation with different primary key index.""" old_rows = [ ("desc1", "0x01", "Y"), ("desc2", "0x02", "Y"), ] new_rows = [ ("desc1", "0x01", "N"), ("desc3", "0x03", "Y"), ] diff = calculate_diff(old_rows, new_rows, pk_index=1) assert "0x03" in diff["added"] assert "0x02" in diff["deleted"] assert "0x01" in diff["modified"] class TestConsecutiveUpdates: """Tests for consecutive IANA updates.""" def test_consecutive_updates_show_no_changes(self, test_db_path: str) -> None: """Test that second update with same data shows no changes.""" xml_path = "tests/fixtures/iana_xml/tls-parameters-minimal.xml" with open(xml_path, encoding="utf-8") as f: xml_content = f.read() conn = sqlite3.connect(test_db_path) headers = ["Value", "Description", "DTLS", "Recommended", "RFC/Draft"] row_count_1, diff_1 = process_registry_with_validation( xml_content, "tls-parameters-4", "iana_tls_cipher_suites", headers, conn, skip_min_rows_check=True, ) row_count_2, diff_2 = process_registry_with_validation( xml_content, "tls-parameters-4", "iana_tls_cipher_suites", headers, conn, skip_min_rows_check=True, ) assert row_count_1 == row_count_2 assert len(diff_2["added"]) == 0 assert len(diff_2["deleted"]) == 0 assert len(diff_2["modified"]) == 0 conn.close()