@@ -614,52 +614,58 @@ def test_load_duckdb_attach_config(tmp_path):
614614
615615
616616def test_ownership_config_resolve_owner ():
617+ mock_adapter = mock .MagicMock ()
617618 config = OwnershipConfig (
618619 environment_owner_mapping = {
619620 "^prod$" : "svc_prod_spn" ,
620621 ".*" : "group:shared-developers" ,
621622 }
622623 )
623- assert config .resolve_owner ("prod" ) == "svc_prod_spn"
624- assert config .resolve_owner ("dev_alice" ) == "group:shared-developers"
625- assert config .resolve_owner ("staging" ) == "group:shared-developers"
624+ assert config .resolve_owner ("prod" , mock_adapter ) == "svc_prod_spn"
625+ assert config .resolve_owner ("dev_alice" , mock_adapter ) == "group:shared-developers"
626+ assert config .resolve_owner ("staging" , mock_adapter ) == "group:shared-developers"
626627 # "production" does not match ^prod$ so falls through to .*
627- assert config .resolve_owner ("production" ) == "group:shared-developers"
628+ assert config .resolve_owner ("production" , mock_adapter ) == "group:shared-developers"
628629
629630
630631def test_ownership_config_empty_returns_none ():
631- assert OwnershipConfig ().resolve_owner ("prod" ) is None
632- assert OwnershipConfig ().resolve_owner ("dev_env" ) is None
632+ mock_adapter = mock .MagicMock ()
633+ assert OwnershipConfig ().resolve_owner ("prod" , mock_adapter ) is None
634+ assert OwnershipConfig ().resolve_owner ("dev_env" , mock_adapter ) is None
633635
634636
635637def test_ownership_config_first_match_wins ():
636638 # The catch-all .* comes before a more specific pattern — it always wins.
637639 # This documents the ordering contract: users must put specific patterns first.
640+ mock_adapter = mock .MagicMock ()
638641 config = OwnershipConfig (
639642 environment_owner_mapping = {
640643 ".*" : "catch_all_owner" ,
641644 "^prod$" : "prod_owner" ,
642645 }
643646 )
644- assert config .resolve_owner ("prod" ) == "catch_all_owner"
647+ assert config .resolve_owner ("prod" , mock_adapter ) == "catch_all_owner"
645648
646649
647650def test_ownership_config_case_sensitive ():
648651 # Patterns are compiled without re.IGNORECASE, so matching is case-sensitive.
652+ mock_adapter = mock .MagicMock ()
649653 config = OwnershipConfig (environment_owner_mapping = {"^prod$" : "svc_prod" })
650- assert config .resolve_owner ("prod" ) == "svc_prod"
651- assert config .resolve_owner ("PROD" ) is None
652- assert config .resolve_owner ("Prod" ) is None
654+ assert config .resolve_owner ("prod" , mock_adapter ) == "svc_prod"
655+ assert config .resolve_owner ("PROD" , mock_adapter ) is None
656+ assert config .resolve_owner ("Prod" , mock_adapter ) is None
653657
654658
655659def test_ownership_config_no_match_returns_none ():
660+ mock_adapter = mock .MagicMock ()
656661 config = OwnershipConfig (environment_owner_mapping = {"^prod$" : "svc_prod" })
657- assert config .resolve_owner ("staging" ) is None
658- assert config .resolve_owner ("dev_bob" ) is None
662+ assert config .resolve_owner ("staging" , mock_adapter ) is None
663+ assert config .resolve_owner ("dev_bob" , mock_adapter ) is None
659664
660665
661666def test_ownership_config_deserialization_from_dict ():
662667 # Simulates YAML/dict-based config loading (as produced by load_config_from_yaml).
668+ mock_adapter = mock .MagicMock ()
663669 config = Config (
664670 model_defaults = ModelDefaultsConfig (dialect = "duckdb" ),
665671 ownership = {
@@ -669,15 +675,16 @@ def test_ownership_config_deserialization_from_dict():
669675 }
670676 },
671677 )
672- assert config .ownership .resolve_owner ("prod" ) == "svc_prod_spn"
673- assert config .ownership .resolve_owner ("dev" ) == "group:shared-developers"
678+ assert config .ownership .resolve_owner ("prod" , mock_adapter ) == "svc_prod_spn"
679+ assert config .ownership .resolve_owner ("dev" , mock_adapter ) == "group:shared-developers"
674680
675681
676682def test_ownership_config_nested_update ():
677683 # Config.ownership uses UpdateStrategy.NESTED_UPDATE.
678684 # When two Configs are merged, the second one's environment_owner_mapping
679685 # replaces the first's (REPLACE semantics within OwnershipConfig since
680686 # environment_owner_mapping has no explicit strategy).
687+ mock_adapter = mock .MagicMock ()
681688 c1 = Config (
682689 model_defaults = ModelDefaultsConfig (dialect = "duckdb" ),
683690 ownership = OwnershipConfig (environment_owner_mapping = {"^prod$" : "spn_prod" }),
@@ -688,15 +695,16 @@ def test_ownership_config_nested_update():
688695 )
689696 merged = c1 .update_with (c2 )
690697 # c2's mapping fully replaces c1's — the ^prod$ pattern is gone
691- assert merged .ownership .resolve_owner ("prod" ) == "grp_devs"
692- assert merged .ownership .resolve_owner ("dev_alice" ) == "grp_devs"
698+ assert merged .ownership .resolve_owner ("prod" , mock_adapter ) == "grp_devs"
699+ assert merged .ownership .resolve_owner ("dev_alice" , mock_adapter ) == "grp_devs"
693700
694701
695702def test_config_ownership_defaults_to_empty ():
696703 # Configs without an explicit ownership block have a no-op OwnershipConfig.
704+ mock_adapter = mock .MagicMock ()
697705 config = Config (model_defaults = ModelDefaultsConfig (dialect = "duckdb" ))
698706 assert config .ownership .environment_owner_mapping == {}
699- assert config .ownership .resolve_owner ("prod" ) is None
707+ assert config .ownership .resolve_owner ("prod" , mock_adapter ) is None
700708
701709
702710def test_ownership_config_physical_owner ():
@@ -718,7 +726,70 @@ def test_ownership_config_physical_owner_deserialization():
718726 },
719727 )
720728 assert config .ownership .physical_owner == "group:data-platform"
721- assert config .ownership .resolve_owner ("prod" ) == "svc_prod"
729+ assert config .ownership .resolve_owner ("prod" , mock .MagicMock ()) == "svc_prod"
730+
731+
732+ def test_ownership_config_resolve_owner_callable ():
733+ # A callable resolver takes precedence over environment_owner_mapping and
734+ # receives (env_name, adapter) so it can call adapter.current_user() etc.
735+ mock_adapter = mock .MagicMock ()
736+ mock_adapter .current_user .return_value = "spn-dynamic-uuid"
737+
738+ config = OwnershipConfig (
739+ environment_owner_mapping = {".*" : "group:fallback" },
740+ environment_owner_resolver = lambda env , adapter : (
741+ adapter .current_user () if env == "prod" else "group:shared-developers"
742+ ),
743+ )
744+
745+ assert config .resolve_owner ("prod" , mock_adapter ) == "spn-dynamic-uuid"
746+ assert config .resolve_owner ("dev_alice" , mock_adapter ) == "group:shared-developers"
747+ mock_adapter .current_user .assert_called_once ()
748+
749+
750+ def test_ownership_config_resolver_overrides_mapping ():
751+ # Resolver always wins when set, even if the mapping would also match.
752+ mock_adapter = mock .MagicMock ()
753+ config = OwnershipConfig (
754+ environment_owner_mapping = {"^prod$" : "static-owner" },
755+ environment_owner_resolver = lambda env , adapter : "dynamic-owner" ,
756+ )
757+ assert config .resolve_owner ("prod" , mock_adapter ) == "dynamic-owner"
758+
759+
760+ def test_ownership_config_resolve_physical_owner_callable ():
761+ mock_adapter = mock .MagicMock ()
762+ mock_adapter .current_user .return_value = "spn-uuid-123"
763+
764+ config = OwnershipConfig (
765+ physical_owner_resolver = lambda adapter : adapter .current_user (),
766+ )
767+ assert config .resolve_physical_owner (mock_adapter ) == "spn-uuid-123"
768+ mock_adapter .current_user .assert_called_once ()
769+
770+
771+ def test_ownership_config_resolve_physical_owner_static ():
772+ mock_adapter = mock .MagicMock ()
773+ config = OwnershipConfig (physical_owner = "group:data-platform" )
774+ assert config .resolve_physical_owner (mock_adapter ) == "group:data-platform"
775+ mock_adapter .current_user .assert_not_called ()
776+
777+
778+ def test_ownership_config_physical_owner_resolver_overrides_static ():
779+ mock_adapter = mock .MagicMock ()
780+ config = OwnershipConfig (
781+ physical_owner = "static-owner" ,
782+ physical_owner_resolver = lambda adapter : "dynamic-owner" ,
783+ )
784+ assert config .resolve_physical_owner (mock_adapter ) == "dynamic-owner"
785+
786+
787+ def test_ownership_config_is_active ():
788+ assert not OwnershipConfig ().is_active
789+ assert OwnershipConfig (environment_owner_mapping = {".*" : "grp" }).is_active
790+ assert OwnershipConfig (environment_owner_resolver = lambda e , a : None ).is_active
791+ assert OwnershipConfig (physical_owner = "grp" ).is_active
792+ assert OwnershipConfig (physical_owner_resolver = lambda a : "grp" ).is_active
722793
723794
724795def test_load_model_defaults_audits (tmp_path ):
0 commit comments