#!/usr/bin/env python3 """ Test Suite for Temporal Consistency Validator (v0.7.0) Tests validation rules with valid and invalid test cases. Author: Heritage Custodian Ontology Project Date: 2025-11-22 Schema Version: v0.7.0 (Phase 5: Validation Framework) """ import pytest import tempfile from pathlib import Path from datetime import date import sys # Add parent directory to path to import validator sys.path.insert(0, str(Path(__file__).parent.parent / 'scripts')) from validate_temporal_consistency import ( DataLoader, TemporalValidator, parse_date, date_within_range ) # ============================================================================ # Utility Functions # ============================================================================ def create_temp_yaml(yaml_content: str) -> Path: """Create temporary YAML file for testing.""" temp_file = tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) temp_file.write(yaml_content) temp_file.close() return Path(temp_file.name) # ============================================================================ # Date Utility Tests # ============================================================================ class TestDateUtilities: """Test date parsing and range checking utilities.""" def test_parse_date_iso_string(self): """Test parsing ISO date string.""" result = parse_date("2025-11-22") assert result == date(2025, 11, 22) def test_parse_date_iso_with_time(self): """Test parsing ISO datetime string.""" result = parse_date("2025-11-22T15:30:00Z") assert result == date(2025, 11, 22) def test_parse_date_none(self): """Test parsing None returns None.""" result = parse_date(None) assert result is None def test_parse_date_object(self): """Test passing date object returns same object.""" test_date = date(2025, 11, 22) result = parse_date(test_date) assert result == test_date def test_date_within_range_valid(self): """Test date within valid range.""" check = date(2020, 6, 15) start = date(2020, 1, 1) end = date(2020, 12, 31) assert date_within_range(check, start, end) is True def test_date_within_range_before_start(self): """Test date before start fails.""" check = date(2019, 12, 31) start = date(2020, 1, 1) end = date(2020, 12, 31) assert date_within_range(check, start, end) is False def test_date_within_range_after_end(self): """Test date after end fails.""" check = date(2021, 1, 1) start = date(2020, 1, 1) end = date(2020, 12, 31) assert date_within_range(check, start, end) is False def test_date_within_range_open_ended(self): """Test open-ended range (None end date).""" check = date(2025, 11, 22) start = date(2020, 1, 1) end = None assert date_within_range(check, start, end) is True # ============================================================================ # Collection-Unit Temporal Validation Tests # ============================================================================ class TestCollectionUnitTemporal: """Test collection-unit temporal consistency validation.""" def test_valid_collection_within_unit_lifetime(self): """Test collection custody within unit validity period (VALID).""" yaml_content = """ --- id: "https://example.org/unit/dept-1" unit_name: "Test Department" unit_type: DEPARTMENT valid_from: "2000-01-01" valid_to: null managed_collections: - "https://example.org/collection/coll-1" --- id: "https://example.org/collection/coll-1" collection_name: "Test Collection" managing_unit: "https://example.org/unit/dept-1" valid_from: "2010-01-01" valid_to: null """ yaml_file = create_temp_yaml(yaml_content) try: data = DataLoader(yaml_file).load() validator = TemporalValidator(data) result = validator.validate_all() # Should have no errors (collection starts after unit) collection_errors = [e for e in result.errors if e.rule == "COLLECTION_UNIT_TEMPORAL"] assert len(collection_errors) == 0 finally: yaml_file.unlink() def test_invalid_collection_before_unit(self): """Test collection custody starts before unit exists (INVALID).""" yaml_content = """ --- id: "https://example.org/unit/dept-1" unit_name: "Test Department" unit_type: DEPARTMENT valid_from: "2010-01-01" valid_to: null managed_collections: - "https://example.org/collection/coll-1" --- id: "https://example.org/collection/coll-1" collection_name: "Test Collection" managing_unit: "https://example.org/unit/dept-1" valid_from: "2005-01-01" valid_to: null """ yaml_file = create_temp_yaml(yaml_content) try: data = DataLoader(yaml_file).load() validator = TemporalValidator(data) result = validator.validate_all() # Should have error: collection starts before unit collection_errors = [e for e in result.errors if e.rule == "COLLECTION_UNIT_TEMPORAL"] assert len(collection_errors) == 1 assert "before managing unit exists" in collection_errors[0].message finally: yaml_file.unlink() def test_invalid_collection_after_unit_dissolved(self): """Test collection custody extends beyond unit validity (INVALID).""" yaml_content = """ --- id: "https://example.org/unit/dept-1" unit_name: "Test Department" unit_type: DEPARTMENT valid_from: "2000-01-01" valid_to: "2020-12-31" managed_collections: - "https://example.org/collection/coll-1" --- id: "https://example.org/collection/coll-1" collection_name: "Test Collection" managing_unit: "https://example.org/unit/dept-1" valid_from: "2010-01-01" valid_to: "2025-12-31" """ yaml_file = create_temp_yaml(yaml_content) try: data = DataLoader(yaml_file).load() validator = TemporalValidator(data) result = validator.validate_all() # Should have error: collection extends beyond unit collection_errors = [e for e in result.errors if e.rule == "COLLECTION_UNIT_TEMPORAL"] assert len(collection_errors) == 1 assert "extends" in collection_errors[0].message and "beyond" in collection_errors[0].message finally: yaml_file.unlink() def test_warning_collection_ongoing_after_unit_dissolved(self): """Test warning when collection custody ongoing but unit dissolved (WARNING).""" yaml_content = """ --- id: "https://example.org/unit/dept-1" unit_name: "Test Department" unit_type: DEPARTMENT valid_from: "2000-01-01" valid_to: "2020-12-31" managed_collections: - "https://example.org/collection/coll-1" --- id: "https://example.org/collection/coll-1" collection_name: "Test Collection" managing_unit: "https://example.org/unit/dept-1" valid_from: "2010-01-01" valid_to: null """ yaml_file = create_temp_yaml(yaml_content) try: data = DataLoader(yaml_file).load() validator = TemporalValidator(data) result = validator.validate_all() # Should have warning: collection ongoing but unit dissolved collection_warnings = [w for w in result.warnings if w.rule == "COLLECTION_UNIT_TEMPORAL"] assert len(collection_warnings) == 1 assert "ongoing" in collection_warnings[0].message and "dissolved" in collection_warnings[0].message finally: yaml_file.unlink() # ============================================================================ # Bidirectional Relationship Tests # ============================================================================ class TestBidirectionalRelationships: """Test bidirectional relationship consistency validation.""" def test_valid_bidirectional_collection_unit(self): """Test valid bidirectional collection-unit relationship (VALID).""" yaml_content = """ --- id: "https://example.org/unit/dept-1" unit_name: "Test Department" unit_type: DEPARTMENT valid_from: "2000-01-01" valid_to: null managed_collections: - "https://example.org/collection/coll-1" --- id: "https://example.org/collection/coll-1" collection_name: "Test Collection" managing_unit: "https://example.org/unit/dept-1" valid_from: "2010-01-01" valid_to: null """ yaml_file = create_temp_yaml(yaml_content) try: data = DataLoader(yaml_file).load() validator = TemporalValidator(data) result = validator.validate_all() # Should have no bidirectional errors bidir_errors = [e for e in result.errors if "BIDIRECTIONAL" in e.rule] assert len(bidir_errors) == 0 finally: yaml_file.unlink() def test_invalid_collection_missing_reverse_relationship(self): """Test collection references unit but unit doesn't list collection (INVALID).""" yaml_content = """ --- id: "https://example.org/unit/dept-1" unit_name: "Test Department" unit_type: DEPARTMENT valid_from: "2000-01-01" valid_to: null managed_collections: [] --- id: "https://example.org/collection/coll-1" collection_name: "Test Collection" managing_unit: "https://example.org/unit/dept-1" valid_from: "2010-01-01" valid_to: null """ yaml_file = create_temp_yaml(yaml_content) try: data = DataLoader(yaml_file).load() validator = TemporalValidator(data) result = validator.validate_all() # Should have bidirectional error bidir_errors = [e for e in result.errors if e.rule == "COLLECTION_UNIT_BIDIRECTIONAL"] assert len(bidir_errors) == 1 assert "does not list collection in managed_collections" in bidir_errors[0].message finally: yaml_file.unlink() def test_invalid_unit_references_nonexistent_collection(self): """Test unit references collection that doesn't exist (INVALID).""" yaml_content = """ --- id: "https://example.org/unit/dept-1" unit_name: "Test Department" unit_type: DEPARTMENT valid_from: "2000-01-01" valid_to: null managed_collections: - "https://example.org/collection/nonexistent" """ yaml_file = create_temp_yaml(yaml_content) try: data = DataLoader(yaml_file).load() validator = TemporalValidator(data) result = validator.validate_all() # Should have bidirectional error bidir_errors = [e for e in result.errors if e.rule == "COLLECTION_UNIT_BIDIRECTIONAL"] assert len(bidir_errors) == 1 assert "non-existent collection" in bidir_errors[0].message finally: yaml_file.unlink() # ============================================================================ # Custody Transfer Continuity Tests # ============================================================================ class TestCustodyContinuity: """Test custody transfer continuity validation.""" def test_valid_continuous_custody_transfer(self): """Test continuous custody transfer (no gap, VALID).""" yaml_content = """ --- id: "https://example.org/unit/dept-old" unit_name: "Old Department" unit_type: DEPARTMENT valid_from: "2000-01-01" valid_to: "2020-12-31" managed_collections: - "https://example.org/collection/coll-v1" --- id: "https://example.org/unit/dept-new" unit_name: "New Department" unit_type: DEPARTMENT valid_from: "2021-01-01" valid_to: null managed_collections: - "https://example.org/collection/coll-v2" --- id: "https://example.org/collection/coll-v1" collection_name: "Test Collection" managing_unit: "https://example.org/unit/dept-old" valid_from: "2010-01-01" valid_to: "2020-12-31" --- id: "https://example.org/collection/coll-v2" collection_name: "Test Collection" managing_unit: "https://example.org/unit/dept-new" valid_from: "2021-01-01" valid_to: null """ yaml_file = create_temp_yaml(yaml_content) try: data = DataLoader(yaml_file).load() validator = TemporalValidator(data) result = validator.validate_all() # Should have no continuity errors (1 day gap is acceptable) continuity_errors = [e for e in result.errors if e.rule == "CUSTODY_CONTINUITY"] assert len(continuity_errors) == 0 finally: yaml_file.unlink() def test_warning_custody_gap(self): """Test custody gap between versions (WARNING).""" yaml_content = """ --- id: "https://example.org/unit/dept-old" unit_name: "Old Department" unit_type: DEPARTMENT valid_from: "2000-01-01" valid_to: "2020-12-31" managed_collections: - "https://example.org/collection/coll-v1" --- id: "https://example.org/unit/dept-new" unit_name: "New Department" unit_type: DEPARTMENT valid_from: "2021-03-01" valid_to: null managed_collections: - "https://example.org/collection/coll-v2" --- id: "https://example.org/collection/coll-v1" collection_name: "Test Collection" managing_unit: "https://example.org/unit/dept-old" valid_from: "2010-01-01" valid_to: "2020-12-31" --- id: "https://example.org/collection/coll-v2" collection_name: "Test Collection" managing_unit: "https://example.org/unit/dept-new" valid_from: "2021-03-01" valid_to: null """ yaml_file = create_temp_yaml(yaml_content) try: data = DataLoader(yaml_file).load() validator = TemporalValidator(data) result = validator.validate_all() # Should have continuity warning (60+ day gap) continuity_warnings = [w for w in result.warnings if w.rule == "CUSTODY_CONTINUITY"] assert len(continuity_warnings) == 1 assert "gap" in continuity_warnings[0].message finally: yaml_file.unlink() def test_error_custody_overlap(self): """Test overlapping custody periods (ERROR).""" yaml_content = """ --- id: "https://example.org/unit/dept-old" unit_name: "Old Department" unit_type: DEPARTMENT valid_from: "2000-01-01" valid_to: "2020-12-31" managed_collections: - "https://example.org/collection/coll-v1" --- id: "https://example.org/unit/dept-new" unit_name: "New Department" unit_type: DEPARTMENT valid_from: "2020-06-01" valid_to: null managed_collections: - "https://example.org/collection/coll-v2" --- id: "https://example.org/collection/coll-v1" collection_name: "Test Collection" managing_unit: "https://example.org/unit/dept-old" valid_from: "2010-01-01" valid_to: "2020-12-31" --- id: "https://example.org/collection/coll-v2" collection_name: "Test Collection" managing_unit: "https://example.org/unit/dept-new" valid_from: "2020-06-01" valid_to: null """ yaml_file = create_temp_yaml(yaml_content) try: data = DataLoader(yaml_file).load() validator = TemporalValidator(data) result = validator.validate_all() # Should have continuity error (overlapping custody) continuity_errors = [e for e in result.errors if e.rule == "CUSTODY_CONTINUITY"] assert len(continuity_errors) == 1 assert "overlapping" in continuity_errors[0].message finally: yaml_file.unlink() # ============================================================================ # Integration Tests (Multiple Rules) # ============================================================================ class TestIntegration: """Test multiple validation rules together.""" def test_merger_scenario_valid(self): """Test complete merger scenario with custody transfer (VALID).""" yaml_content = """ --- id: "https://example.org/unit/dept-a" unit_name: "Department A" unit_type: DEPARTMENT valid_from: "2000-01-01" valid_to: "2020-02-28" managed_collections: - "https://example.org/collection/coll-a-v1" --- id: "https://example.org/unit/dept-b" unit_name: "Department B" unit_type: DEPARTMENT valid_from: "2000-01-01" valid_to: "2020-02-28" managed_collections: - "https://example.org/collection/coll-b-v1" --- id: "https://example.org/unit/dept-merged" unit_name: "Merged Department" unit_type: DIVISION valid_from: "2020-03-01" valid_to: null managed_collections: - "https://example.org/collection/coll-a-v2" - "https://example.org/collection/coll-b-v2" --- id: "https://example.org/collection/coll-a-v1" collection_name: "Collection A" managing_unit: "https://example.org/unit/dept-a" valid_from: "2010-01-01" valid_to: "2020-02-28" --- id: "https://example.org/collection/coll-a-v2" collection_name: "Collection A" managing_unit: "https://example.org/unit/dept-merged" valid_from: "2020-03-01" valid_to: null --- id: "https://example.org/collection/coll-b-v1" collection_name: "Collection B" managing_unit: "https://example.org/unit/dept-b" valid_from: "2010-01-01" valid_to: "2020-02-28" --- id: "https://example.org/collection/coll-b-v2" collection_name: "Collection B" managing_unit: "https://example.org/unit/dept-merged" valid_from: "2020-03-01" valid_to: null """ yaml_file = create_temp_yaml(yaml_content) try: data = DataLoader(yaml_file).load() validator = TemporalValidator(data) result = validator.validate_all() # Should have no errors (valid merger with continuous custody) assert result.is_valid assert len(result.errors) == 0 finally: yaml_file.unlink() # ============================================================================ # Run Tests # ============================================================================ if __name__ == "__main__": pytest.main([__file__, "-v", "--tb=short"])