#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ Comprehensive test suite for FHIR to PADneXt converter. Run with: pytest test_fhir_to_pad_converter.py -v pytest test_fhir_to_pad_converter.py -v --cov=. --cov-report=html """ import pytest import json import xml.etree.ElementTree as ET from datetime import datetime from pathlib import Path from typing import Dict, Any, List from typer.testing import CliRunner # Import modules to test from utils import ( parse_iso_date, format_iso_date, get_ref_id, ensure_text, collect_effective_dates, validate_file_path, validate_output_path, validate_directory_path ) from validation import ( validate_temporal_consistency, validate_codes, run_validation ) from translator import CodeTranslator from fhir_to_pad_converter import ( validate_fhir_json, compute_fhir_stats, group_entries, claim_item_to_position, find_resource_by_ref, claim_to_rechnung_header, validate_ziffer, get_with_placeholder, validate_pad_xml, compute_pad_stats, build_pad_xml, PAD_NS, app ) # ============================================================================ # FIXTURES # ============================================================================ @pytest.fixture def sample_fhir_bundle(): """Minimal valid FHIR bundle for testing.""" return { "resourceType": "Bundle", "type": "collection", "entry": [ { "resource": { "resourceType": "Patient", "id": "patient-1", "name": [{"family": "Test", "given": ["John"]}], "birthDate": "1980-01-01", "gender": "male" } }, { "resource": { "resourceType": "Encounter", "id": "encounter-1", "status": "finished", "subject": {"reference": "Patient/patient-1"}, "period": { "start": "2024-01-01T10:00:00+00:00", "end": "2024-01-01T12:00:00+00:00" } } }, { "resource": { "resourceType": "Observation", "id": "obs-1", "status": "final", "code": { "coding": [{ "system": "http://loinc.org", "code": "12345-6", "display": "Test Observation" }] }, "subject": {"reference": "Patient/patient-1"}, "encounter": {"reference": "Encounter/encounter-1"}, "effectiveDateTime": "2024-01-01T10:30:00+00:00" } } ] } @pytest.fixture def sample_claim_bundle(): """FHIR bundle with Claim resource.""" return { "resourceType": "Bundle", "type": "collection", "entry": [ { "resource": { "resourceType": "Patient", "id": "patient-1", "name": [{"family": "Smith", "given": ["Jane"]}], "birthDate": "1990-05-15", "gender": "female" } }, { "resource": { "resourceType": "Organization", "id": "provider-org-1", "name": "Test Hospital" } }, { "resource": { "resourceType": "Organization", "id": "insurer-org-1", "name": "Test Insurance" } }, { "resource": { "resourceType": "Claim", "id": "claim-1", "status": "active", "patient": {"reference": "Patient/patient-1"}, "provider": {"reference": "Organization/provider-org-1"}, "insurer": {"reference": "Organization/insurer-org-1"}, "created": "2024-01-15T10:00:00+00:00", "diagnosis": [{ "diagnosisCodeableConcept": { "coding": [{ "system": "http://hl7.org/fhir/sid/icd-10", "code": "Z00.0", "display": "General examination" }] } }], "item": [ { "sequence": 1, "productOrService": { "coding": [{ "system": "http://test.de/goa", "code": "1", "display": "Consultation" }] }, "servicedDate": "2024-01-15" } ] } } ] } @pytest.fixture def sample_header_config(): """Sample header configuration.""" return { "nachrichtentyp_version": "1.0", "rechnungsersteller_name": "Test Clinic", "rechnungsersteller_kundennr": "12345", "rechnungsersteller_strasse": "Main St 1", "rechnungsersteller_plz": "12345", "rechnungsersteller_ort": "Berlin", "leistungserbringer_id": "LE001", "leistungserbringer_vorname": "Dr. Max", "leistungserbringer_name": "Mustermann", "behandelter_vorname": "John", "behandelter_name": "Doe", "behandelter_gebdatum": "1980-01-01", "behandelter_geschlecht": "m", "versicherter_vorname": "John", "versicherter_name": "Doe", "versicherter_gebdatum": "1980-01-01", "versicherter_geschlecht": "m", "behandlungsart": "0", "vertragsart": "1", "diagnose_text": "Test diagnosis", "diagnose_datum": "2024-01-01" } @pytest.fixture def sample_placeholder_config(): """Sample placeholder configuration.""" return { "rechnungsersteller": { "name": "PLACEHOLDER_CLINIC", "plz": "00000", "ort": "PLACEHOLDER_CITY", "strasse": "PLACEHOLDER_STREET" }, "leistungserbringer": { "vorname": "PLACEHOLDER_FIRSTNAME", "name": "PLACEHOLDER_LASTNAME" }, "behandelter": { "anrede": "Ohne Anrede", "vorname": "PLACEHOLDER_PATIENT", "name": "PLACEHOLDER_PATIENT", "gebdatum": "1900-01-01", "geschlecht": "u" }, "goziffer": { "ziffer": "99999", "datum": "1900-01-01" } } # ============================================================================ # UTILS.PY TESTS # ============================================================================ class TestUtils: """Tests for utility functions.""" def test_parse_iso_date_valid_with_z(self): """Test parsing ISO date with Z suffix.""" result = parse_iso_date("2024-01-01T10:00:00Z") assert result is not None assert result.year == 2024 assert result.month == 1 assert result.day == 1 def test_parse_iso_date_valid_with_timezone(self): """Test parsing ISO date with timezone.""" result = parse_iso_date("2024-01-01T10:00:00+02:00") assert result is not None assert result.year == 2024 def test_parse_iso_date_invalid(self): """Test parsing invalid date returns None.""" assert parse_iso_date("not-a-date") is None assert parse_iso_date("") is None assert parse_iso_date(None) is None def test_parse_iso_date_edge_cases(self): """Test edge cases for date parsing.""" # Just date without time result = parse_iso_date("2024-01-01") assert result is not None assert result.year == 2024 def test_format_iso_date(self): """Test formatting datetime to ISO date string.""" dt = datetime(2024, 1, 15, 10, 30, 45) result = format_iso_date(dt) assert result == "2024-01-15" def test_get_ref_id_valid(self): """Test extracting ID from valid reference.""" assert get_ref_id("Patient/123") == "123" assert get_ref_id("Encounter/abc-def") == "abc-def" assert get_ref_id("Organization/org-1") == "org-1" def test_get_ref_id_invalid(self): """Test handling invalid references.""" assert get_ref_id("NoSlash") is None assert get_ref_id("") is None assert get_ref_id(None) is None assert get_ref_id("Patient/") is None def test_ensure_text_with_element(self): """Test extracting text from XML element.""" elem = ET.Element("test") elem.text = " content " assert ensure_text(elem) == "content" def test_ensure_text_none(self): """Test ensure_text with None returns default.""" assert ensure_text(None) == "" assert ensure_text(None, "default") == "default" def test_collect_effective_dates(self): """Test collecting dates from resource.""" resource = { "effectiveDateTime": "2024-01-01T10:00:00Z", "issued": "2024-01-02T10:00:00Z", "meta": { "lastUpdated": "2024-01-03T10:00:00Z" } } dates = collect_effective_dates(resource) assert len(dates) == 3 assert all(isinstance(d, datetime) for d in dates) def test_collect_effective_dates_empty(self): """Test collecting dates from resource with no dates.""" resource = {"resourceType": "Patient"} dates = collect_effective_dates(resource) assert len(dates) == 0 # ============================================================================ # VALIDATION.PY TESTS # ============================================================================ class TestValidation: """Tests for validation functions.""" def test_validate_temporal_consistency_valid(self): """Test validation passes for valid dates.""" resources = [{ "id": "test-1", "effectiveDateTime": "2024-01-01T10:00:00Z" }] warnings = validate_temporal_consistency(resources) assert len(warnings) == 0 def test_validate_temporal_consistency_future_date(self): """Test validation warns for future dates.""" resources = [{ "id": "test-1", "effectiveDateTime": "2099-01-01T10:00:00Z" }] warnings = validate_temporal_consistency(resources) assert len(warnings) > 0 assert "future" in warnings[0].lower() def test_validate_codes(self): """Test code validation (currently stub).""" resources = [{"id": "test-1"}] warnings = validate_codes(resources) assert isinstance(warnings, list) def test_run_validation(self): """Test running all validations.""" resources = [{ "id": "test-1", "effectiveDateTime": "2024-01-01T10:00:00Z" }] warnings = run_validation(resources) assert isinstance(warnings, list) # ============================================================================ # TRANSLATOR.PY TESTS # ============================================================================ class TestCodeTranslator: """Tests for code translation.""" def test_translator_init(self): """Test translator initialization.""" translator = CodeTranslator() assert translator.maps == {} def test_translator_parse_concept_map(self): """Test parsing a concept map.""" translator = CodeTranslator() concept_map = { "group": [{ "source": "http://loinc.org", "target": "http://test.de/goa", "element": [ { "code": "12345-6", "target": [{"code": "001"}] } ] }] } translator._parse_concept_map(concept_map) assert "http://loinc.org" in translator.maps result = translator.translate("http://loinc.org", "12345-6") assert result == "001" def test_translator_no_match(self): """Test translation returns None when no match.""" translator = CodeTranslator() result = translator.translate("http://unknown.org", "12345") assert result is None # ============================================================================ # FHIR VALIDATION TESTS # ============================================================================ class TestFhirValidation: """Tests for FHIR validation functions.""" def test_validate_fhir_json_valid_bundle(self, sample_fhir_bundle): """Test validation of valid FHIR bundle.""" ok, messages = validate_fhir_json(sample_fhir_bundle) assert ok is True assert len(messages) > 0 def test_validate_fhir_json_invalid_resource_type(self): """Test validation fails for wrong resourceType.""" bundle = {"resourceType": "NotABundle"} ok, messages = validate_fhir_json(bundle) assert ok is False assert any("Bundle" in m for m in messages) def test_validate_fhir_json_missing_type(self): """Test validation warns for missing bundle type.""" bundle = {"resourceType": "Bundle", "entry": []} ok, messages = validate_fhir_json(bundle) assert ok is False def test_validate_fhir_json_invalid_entry(self): """Test validation handles invalid entry.""" bundle = { "resourceType": "Bundle", "type": "collection", "entry": "not-a-list" } ok, messages = validate_fhir_json(bundle) assert ok is False def test_compute_fhir_stats(self, sample_fhir_bundle): """Test computing FHIR statistics.""" stats = compute_fhir_stats(sample_fhir_bundle) assert stats["bundle_type"] == "collection" assert stats["total_entries"] == 3 assert "Patient" in stats["resource_type_counts"] assert stats["resource_type_counts"]["Patient"] == 1 # ============================================================================ # GROUPING TESTS # ============================================================================ class TestGrouping: """Tests for resource grouping logic.""" def test_group_entries_by_encounter(self, sample_fhir_bundle): """Test grouping by encounter when no Claims.""" groups = group_entries(sample_fhir_bundle) assert len(groups) > 0 # Should have one group for (patient-1, encounter-1) assert ("patient-1", "encounter-1") in groups def test_group_entries_by_claim(self, sample_claim_bundle): """Test grouping by claim when Claims present.""" groups = group_entries(sample_claim_bundle) assert len(groups) > 0 # Should have one group for (patient-1, claim-1) assert ("patient-1", "claim-1") in groups def test_group_entries_empty_bundle(self): """Test grouping empty bundle.""" bundle = {"resourceType": "Bundle", "type": "collection", "entry": []} groups = group_entries(bundle) assert len(groups) == 0 # ============================================================================ # CLAIM MAPPING TESTS # ============================================================================ class TestClaimMapping: """Tests for Claim-to-PAD mapping functions.""" def test_claim_item_to_position(self): """Test converting Claim item to position.""" item = { "sequence": 1, "productOrService": { "coding": [{ "code": "12345", "display": "Test Service" }] }, "servicedDate": "2024-01-15" } position = claim_item_to_position(item) assert position["id"] == 1 assert position["ziffer"] == "12345" assert position["text"] == "Test Service" assert position["datum"] == "2024-01-15" assert position["anzahl"] == "1" def test_claim_item_to_position_missing_fields(self): """Test converting Claim item with missing fields.""" item = {"sequence": 2} position = claim_item_to_position(item) assert position["id"] == 2 assert position["ziffer"] == "" assert position["text"] == "" def test_find_resource_by_ref(self, sample_claim_bundle): """Test finding resource by reference.""" resource = find_resource_by_ref(sample_claim_bundle, "Patient/patient-1") assert resource is not None assert resource["resourceType"] == "Patient" assert resource["id"] == "patient-1" def test_find_resource_by_ref_not_found(self, sample_claim_bundle): """Test finding non-existent resource.""" resource = find_resource_by_ref(sample_claim_bundle, "Patient/does-not-exist") assert resource is None def test_find_resource_by_ref_invalid_format(self, sample_claim_bundle): """Test finding resource with invalid reference format.""" resource = find_resource_by_ref(sample_claim_bundle, "InvalidReference") assert resource is None def test_claim_to_rechnung_header(self, sample_claim_bundle): """Test extracting header info from Claim.""" claim = sample_claim_bundle["entry"][3]["resource"] header = claim_to_rechnung_header(claim, sample_claim_bundle) assert "behandelter_vorname" in header assert header["behandelter_name"] == "Smith" assert header["behandelter_gebdatum"] == "1990-05-15" assert header["leistungserbringer_name"] == "Test Hospital" assert header["empfaenger_name"] == "Test Insurance" # ============================================================================ # PLACEHOLDER & VALIDATION TESTS # ============================================================================ class TestPlaceholders: """Tests for placeholder and validation functions.""" def test_validate_ziffer_valid(self): """Test validating valid ziffer code.""" auto_filled = [] result = validate_ziffer("12345", "99999", "test.ziffer", auto_filled) assert result == "12345" assert len(auto_filled) == 0 def test_validate_ziffer_empty(self): """Test validating empty ziffer uses placeholder.""" auto_filled = [] result = validate_ziffer("", "99999", "test.ziffer", auto_filled) assert result == "99999" assert len(auto_filled) == 1 assert "99999" in auto_filled[0] def test_validate_ziffer_too_long(self): """Test validating ziffer that's too long gets truncated.""" auto_filled = [] result = validate_ziffer("123456789", "99999", "test.ziffer", auto_filled) assert result == "12345678" assert len(result) == 8 assert len(auto_filled) == 1 assert "truncated" in auto_filled[0].lower() def test_get_with_placeholder_has_value(self): """Test get_with_placeholder with existing value.""" auto_filled = [] result = get_with_placeholder("RealValue", "Placeholder", "test.field", auto_filled) assert result == "RealValue" assert len(auto_filled) == 0 def test_get_with_placeholder_uses_placeholder(self): """Test get_with_placeholder uses placeholder for empty value.""" auto_filled = [] result = get_with_placeholder("", "Placeholder", "test.field", auto_filled) assert result == "Placeholder" assert len(auto_filled) == 1 assert "Placeholder" in auto_filled[0] # ============================================================================ # XML BUILDING TESTS # ============================================================================ class TestXmlBuilding: """Tests for PAD XML building functions.""" def test_build_pad_xml_basic(self, sample_fhir_bundle, sample_header_config, sample_placeholder_config): """Test building basic PAD XML.""" root, warnings, header, auto_filled = build_pad_xml( sample_fhir_bundle, header_cfg=sample_header_config, placeholder_cfg=sample_placeholder_config ) assert root is not None assert root.tag == f"{{{PAD_NS}}}rechnungen" assert root.get("anzahl") is not None def test_build_pad_xml_with_claim(self, sample_claim_bundle, sample_header_config, sample_placeholder_config): """Test building PAD XML with Claim resource.""" root, warnings, header, auto_filled = build_pad_xml( sample_claim_bundle, header_cfg=sample_header_config, placeholder_cfg=sample_placeholder_config ) assert root is not None # Check that claim data overrode config assert header["behandelter_name"] == "Smith" assert header["leistungserbringer_name"] == "Test Hospital" def test_build_pad_xml_namespace(self, sample_fhir_bundle, sample_header_config): """Test PAD XML has correct namespace.""" root, _, _, _ = build_pad_xml(sample_fhir_bundle, header_cfg=sample_header_config) assert root.get("xmlns") == PAD_NS # ============================================================================ # PAD VALIDATION TESTS # ============================================================================ class TestPadValidation: """Tests for PAD XML validation functions.""" def test_validate_pad_xml_well_formed(self, sample_fhir_bundle, sample_header_config): """Test validation of well-formed XML.""" root, _, _, _ = build_pad_xml(sample_fhir_bundle, header_cfg=sample_header_config) ok, messages = validate_pad_xml(root) assert ok is True assert any("well-formed" in m for m in messages) def test_compute_pad_stats(self, sample_fhir_bundle, sample_header_config): """Test computing PAD statistics.""" root, _, _, _ = build_pad_xml(sample_fhir_bundle, header_cfg=sample_header_config) stats = compute_pad_stats(root) assert "rechnungen_declared" in stats assert "rechnungen_actual" in stats assert "goziffer_count" in stats assert isinstance(stats["warnings"], list) # ============================================================================ # INTEGRATION TESTS # ============================================================================ class TestIntegration: """End-to-end integration tests.""" def test_full_conversion_encounter_based(self, sample_fhir_bundle, sample_header_config, sample_placeholder_config): """Test complete conversion workflow (encounter-based).""" # Validate input fhir_ok, _ = validate_fhir_json(sample_fhir_bundle) assert fhir_ok # Compute stats stats = compute_fhir_stats(sample_fhir_bundle) assert stats["total_entries"] == 3 # Build XML root, warnings, header, auto_filled = build_pad_xml( sample_fhir_bundle, header_cfg=sample_header_config, placeholder_cfg=sample_placeholder_config ) assert root is not None # Validate output pad_ok, _ = validate_pad_xml(root) assert pad_ok # Compute output stats pad_stats = compute_pad_stats(root) assert pad_stats["goziffer_count"] > 0 def test_full_conversion_claim_based(self, sample_claim_bundle, sample_header_config, sample_placeholder_config): """Test complete conversion workflow (claim-based).""" # Validate input fhir_ok, _ = validate_fhir_json(sample_claim_bundle) assert fhir_ok # Build XML root, warnings, header, auto_filled = build_pad_xml( sample_claim_bundle, header_cfg=sample_header_config, placeholder_cfg=sample_placeholder_config ) assert root is not None # Check claim data was used assert header["behandelter_name"] == "Smith" # Validate output pad_ok, _ = validate_pad_xml(root) assert pad_ok def test_conversion_with_missing_data(self, sample_placeholder_config): """Test conversion with incomplete FHIR data uses placeholders.""" # Minimal bundle with missing fields minimal_bundle = { "resourceType": "Bundle", "type": "collection", "entry": [{ "resource": { "resourceType": "Patient", "id": "p1" # Missing name, birthDate, etc. } }] } root, warnings, header, auto_filled = build_pad_xml( minimal_bundle, header_cfg={}, placeholder_cfg=sample_placeholder_config ) # Should have many auto-filled fields assert len(auto_filled) > 0 # XML should still be valid assert root is not None # ============================================================================ # EDGE CASE TESTS # ============================================================================ class TestEdgeCases: """Tests for edge cases and error conditions.""" def test_empty_bundle(self): """Test handling empty bundle.""" bundle = {"resourceType": "Bundle", "type": "collection", "entry": []} ok, messages = validate_fhir_json(bundle) assert ok is True def test_bundle_with_null_entries(self): """Test handling bundle with None entries.""" bundle = { "resourceType": "Bundle", "type": "collection", "entry": [None, {"resource": {"resourceType": "Patient", "id": "p1"}}] } groups = group_entries(bundle) # Should handle None gracefully assert isinstance(groups, dict) def test_resource_without_subject(self): """Test handling resource without subject reference.""" bundle = { "resourceType": "Bundle", "type": "collection", "entry": [{ "resource": { "resourceType": "Observation", "id": "obs-1", "status": "final" # No subject field } }] } stats = compute_fhir_stats(bundle) assert stats["entries_missing_subject"] == 1 def test_claim_with_empty_items(self): """Test handling Claim with empty items array.""" item = {"sequence": 1} # Missing productOrService position = claim_item_to_position(item) assert position["ziffer"] == "" assert position["text"] == "" def test_reference_with_multiple_slashes(self): """Test handling malformed reference with multiple slashes.""" # Current implementation will only split on first slash ref_id = get_ref_id("http://example.com/fhir/Patient/123") # Should return the last part or None assert ref_id is not None or ref_id is None # Either is acceptable def test_date_parsing_various_formats(self): """Test parsing various date formats.""" dates = [ "2024-01-01", "2024-01-01T10:00:00", "2024-01-01T10:00:00Z", "2024-01-01T10:00:00+02:00", "2024-01-01T10:00:00.123456Z" ] for date_str in dates: result = parse_iso_date(date_str) assert result is not None or result is None # Should not crash # ============================================================================ # PERFORMANCE TESTS (Optional) # ============================================================================ class TestPerformance: """Basic performance tests.""" def test_large_bundle_grouping(self): """Test grouping with larger bundle.""" # Create bundle with 100 observations entries = [] for i in range(100): entries.append({ "resource": { "resourceType": "Observation", "id": f"obs-{i}", "status": "final", "subject": {"reference": f"Patient/patient-{i % 10}"}, "encounter": {"reference": f"Encounter/enc-{i % 10}"}, "effectiveDateTime": "2024-01-01T10:00:00Z" } }) bundle = { "resourceType": "Bundle", "type": "collection", "entry": entries } # Should complete quickly import time start = time.time() groups = group_entries(bundle) elapsed = time.time() - start assert elapsed < 1.0 # Should take less than 1 second assert len(groups) == 10 # 10 unique (patient, encounter) combinations # ============================================================================ # INPUT VALIDATION TESTS # ============================================================================ class TestInputValidation: """Tests for input path validation.""" def test_validate_file_path_existing_file(self, tmp_path): """Test validation of existing file.""" # Create a temporary file test_file = tmp_path / "test.json" test_file.write_text('{"test": true}') # Should succeed result = validate_file_path(str(test_file), must_exist=True) assert Path(result).exists() assert Path(result).is_absolute() def test_validate_file_path_nonexistent_file(self, tmp_path): """Test validation fails for nonexistent file.""" test_file = tmp_path / "nonexistent.json" # Should raise FileNotFoundError with pytest.raises(FileNotFoundError): validate_file_path(str(test_file), must_exist=True) def test_validate_file_path_empty_path(self): """Test validation fails for empty path.""" with pytest.raises(ValueError, match="File path cannot be empty"): validate_file_path("") def test_validate_file_path_none_path(self): """Test validation fails for None path.""" with pytest.raises(ValueError, match="File path must be a string"): validate_file_path(None) def test_validate_output_path_creates_directory(self, tmp_path): """Test output path validation creates parent directory.""" output_file = tmp_path / "new_dir" / "output.xml" # Should succeed and create parent directory result = validate_output_path(str(output_file)) assert Path(result).parent.exists() assert Path(result).parent.is_dir() def test_validate_output_path_existing_file_overwrite(self, tmp_path): """Test output path with existing file (overwrite=True).""" output_file = tmp_path / "existing.xml" output_file.write_text('') # Should succeed (overwrite allowed by default) result = validate_output_path(str(output_file), overwrite=True) assert Path(result).exists() def test_validate_output_path_existing_file_no_overwrite(self, tmp_path): """Test output path fails with existing file when overwrite=False.""" output_file = tmp_path / "existing.xml" output_file.write_text('') # Should raise FileExistsError with pytest.raises(FileExistsError): validate_output_path(str(output_file), overwrite=False) def test_validate_directory_path_existing(self, tmp_path): """Test directory validation for existing directory.""" result = validate_directory_path(str(tmp_path), must_exist=True) assert Path(result).is_dir() assert Path(result).is_absolute() def test_validate_directory_path_create(self, tmp_path): """Test directory validation creates directory.""" new_dir = tmp_path / "new_directory" result = validate_directory_path(str(new_dir), must_exist=False, create=True) assert Path(result).exists() assert Path(result).is_dir() def test_validate_directory_path_nonexistent(self, tmp_path): """Test directory validation fails for nonexistent directory.""" new_dir = tmp_path / "nonexistent_dir" with pytest.raises(FileNotFoundError): validate_directory_path(str(new_dir), must_exist=True, create=False) def test_validate_directory_path_file_not_directory(self, tmp_path): """Test directory validation fails when path is a file.""" test_file = tmp_path / "file.txt" test_file.write_text("content") with pytest.raises(NotADirectoryError): validate_directory_path(str(test_file), must_exist=True) # ============================================================================ # CONFIG VALIDATION TESTS # ============================================================================ class TestConfigValidation: """Tests for configuration validation.""" def test_validate_placeholder_config(self): """Test placeholder config validation.""" try: from config_schemas import validate_placeholder_config # Valid config valid_config = { "rechnungsersteller": { "name": "Test", "plz": "12345", "ort": "Berlin", "strasse": "Main St" }, "leistungserbringer": { "vorname": "Dr. John", "name": "Doe" }, "goziffer": { "go": "EBM", "ziffer": "12345", "datum": "2024-01-01" } } warnings = validate_placeholder_config(valid_config) assert isinstance(warnings, list) except ImportError: pytest.skip("config_schemas module not available") def test_validate_invalid_placeholder_config(self): """Test placeholder config validation with invalid data.""" try: from config_schemas import validate_placeholder_config # Invalid config - missing required fields invalid_config = { "rechnungsersteller": { "name": "Test" # Missing required fields } } with pytest.raises(ValueError): validate_placeholder_config(invalid_config) except ImportError: pytest.skip("config_schemas module not available") def test_validate_mapping_config(self): """Test mapping config validation.""" try: from config_schemas import validate_mapping_config # Valid mapping config valid_config = { "resources": { "Observation": { "target": "goziffer", "fields": { "ziffer": { "source": "code.coding[0].code" } } } } } warnings = validate_mapping_config(valid_config) assert isinstance(warnings, list) except ImportError: pytest.skip("config_schemas module not available") # ============================================================================ # CLI TESTS (Typer) # ============================================================================ class TestCLI: """Tests for command-line interface using typer.""" def setup_method(self): """Set up CLI test runner.""" self.runner = CliRunner() def test_cli_help(self): """Test --help output.""" result = self.runner.invoke(app, ["--help"]) assert result.exit_code == 0 assert "Convert FHIR Bundle JSON to PADneXt 2.12 XML format" in result.stdout assert "--input-json" in result.stdout assert "--output-dir" in result.stdout assert "--verbose" in result.stdout def test_cli_missing_required_arg(self): """Test error when required argument missing.""" result = self.runner.invoke(app, []) assert result.exit_code != 0 assert "Missing option '--input-json'" in result.stdout def test_cli_short_aliases_in_help(self): """Test that short aliases are shown in help.""" result = self.runner.invoke(app, ["--help"]) assert result.exit_code == 0 assert "-i" in result.stdout # Short alias for --input-json assert "-o" in result.stdout # Short alias for --output-dir assert "-v" in result.stdout # Short alias for --verbose assert "-m" in result.stdout # Short alias for --mapping-config def test_cli_invalid_file(self): """Test error with nonexistent file.""" result = self.runner.invoke(app, ["--input-json", "nonexistent.json"]) assert result.exit_code != 0 # Check for error message (may have formatting/line breaks) assert "does not" in result.stdout and "exist" in result.stdout def test_cli_full_conversion(self, tmp_path): """Test complete conversion workflow.""" # Create test input file input_file = tmp_path / "input.json" test_bundle = { "resourceType": "Bundle", "type": "collection", "entry": [ { "resource": { "resourceType": "Patient", "id": "patient-1", "name": [{"family": "Test", "given": ["John"]}], "birthDate": "1980-01-01", "gender": "male" } }, { "resource": { "resourceType": "Encounter", "id": "encounter-1", "status": "finished", "subject": {"reference": "Patient/patient-1"} } }, { "resource": { "resourceType": "Observation", "id": "obs-1", "status": "final", "code": {"coding": [{"code": "12345", "display": "Test"}]}, "subject": {"reference": "Patient/patient-1"}, "encounter": {"reference": "Encounter/encounter-1"}, "effectiveDateTime": "2024-01-01T10:00:00Z" } } ] } input_file.write_text(json.dumps(test_bundle)) # Run conversion result = self.runner.invoke(app, [ "--input-json", str(input_file), "--output-dir", str(tmp_path) ]) # Check result assert result.exit_code == 0 assert "SUCCESS" in result.stdout or "Conversion completed" in result.stdout def test_cli_with_short_aliases(self, tmp_path): """Test using short aliases.""" # Create test input file input_file = tmp_path / "input.json" test_bundle = { "resourceType": "Bundle", "type": "collection", "entry": [{ "resource": { "resourceType": "Patient", "id": "patient-1", "name": [{"family": "Test"}] } }] } input_file.write_text(json.dumps(test_bundle)) # Run conversion with short aliases result = self.runner.invoke(app, [ "-i", str(input_file), "-o", str(tmp_path) ]) # Should work the same as long form assert result.exit_code == 0 def test_cli_verbose_flag(self, tmp_path): """Test verbose flag.""" # Create test input input_file = tmp_path / "input.json" input_file.write_text('{"resourceType": "Bundle", "type": "collection", "entry": []}') # Run with verbose flag (short form) result = self.runner.invoke(app, [ "-i", str(input_file), "-o", str(tmp_path), "-v" ]) # Verbose flag should not cause errors assert result.exit_code == 0 or result.exit_code == 1 # May exit with error for empty bundle def test_cli_with_config_files(self, tmp_path): """Test with configuration files.""" # Create test input input_file = tmp_path / "input.json" input_file.write_text('{"resourceType": "Bundle", "type": "collection", "entry": []}') # Create minimal config placeholder_cfg = tmp_path / "placeholder.json" placeholder_cfg.write_text(json.dumps({ "rechnungsersteller": {"name": "Test", "plz": "12345", "ort": "Test", "strasse": "Test"}, "leistungserbringer": {"vorname": "Dr.", "name": "Test"}, "goziffer": {"go": "EBM", "ziffer": "99999", "datum": "2024-01-01"} })) # Run with config result = self.runner.invoke(app, [ "-i", str(input_file), "-o", str(tmp_path), "--placeholder-cfg", str(placeholder_cfg) ]) # Should work (may warn about empty bundle but shouldn't crash) assert result.exit_code in [0, 1] # 0 for success, 1 for validation errors def test_cli_shell_completion(self): """Test that shell completion is available.""" result = self.runner.invoke(app, ["--help"]) assert result.exit_code == 0 # Typer automatically adds these assert "--install-completion" in result.stdout assert "--show-completion" in result.stdout # ============================================================================ # RUN CONFIGURATION # ============================================================================ if __name__ == "__main__": # Run with: python test_fhir_to_pad_converter.py pytest.main([__file__, "-v", "--tb=short"])