@@ -179,15 +179,6 @@ def test_to_api_repr_w_extra_properties(self):
179179 exp_resource = entry .to_api_repr ()
180180 self .assertEqual (resource , exp_resource )
181181
182- def test_from_api_repr_entries_w_extra_keys (self ):
183- resource = {
184- "role" : "READER" ,
185- "specialGroup" : "projectReaders" ,
186- "userByEmail" : "salmon@example.com" ,
187- }
188- with self .assertRaises (ValueError ):
189- self ._get_target_class ().from_api_repr (resource )
190-
191182 def test_view_getter_setter (self ):
192183 view = {
193184 "projectId" : "my_project" ,
@@ -494,6 +485,258 @@ def test_dataset_target_types_getter_setter_w_dataset(self):
494485 self .assertEqual (entry .dataset_target_types , target_types )
495486
496487
488+ # --- Tests for AccessEntry when using Condition ---
489+
490+ EXPRESSION = "request.time < timestamp('2026-01-01T00:00:00Z')"
491+ TITLE = "Expires end 2025"
492+ DESCRIPTION = "Access expires at the start of 2026."
493+
494+
495+ @pytest .fixture
496+ def condition_1 ():
497+ """Provides a sample Condition object."""
498+ return Condition (
499+ expression = EXPRESSION ,
500+ title = TITLE ,
501+ description = DESCRIPTION ,
502+ )
503+
504+
505+ @pytest .fixture
506+ def condition_1_api_repr ():
507+ """Provides the API representation for condition_1."""
508+ # Use the actual to_api_repr method
509+ return Condition (
510+ expression = EXPRESSION ,
511+ title = TITLE ,
512+ description = DESCRIPTION ,
513+ ).to_api_repr ()
514+
515+
516+ @pytest .fixture
517+ def condition_2 ():
518+ """Provides a second, different Condition object."""
519+ return Condition (
520+ expression = "resource.name.startsWith('projects/_/buckets/restricted/')" ,
521+ title = "Restricted Buckets" ,
522+ )
523+
524+
525+ @pytest .fixture
526+ def condition_2_api_repr ():
527+ """Provides the API representation for condition2."""
528+ # Use the actual to_api_repr method
529+ return Condition (
530+ expression = "resource.name.startsWith('projects/_/buckets/restricted/')" ,
531+ title = "Restricted Buckets" ,
532+ ).to_api_repr ()
533+
534+
535+ class TestAccessEntryAndCondition :
536+ @staticmethod
537+ def _get_target_class ():
538+ return AccessEntry
539+
540+ def _make_one (self , * args , ** kw ):
541+ return self ._get_target_class ()(* args , ** kw )
542+
543+ # Test __init__ without condition
544+ def test_init_without_condition (self ):
545+ entry = AccessEntry ("READER" , "userByEmail" , "test@example.com" )
546+ assert entry .role == "READER"
547+ assert entry .entity_type == "userByEmail"
548+ assert entry .entity_id == "test@example.com"
549+ assert entry .condition is None
550+ # Accessing _properties is for internal verification in tests
551+ assert "condition" not in entry ._properties
552+
553+ # Test __init__ with condition object
554+ def test_init_with_condition_object (self , condition_1 , condition_1_api_repr ):
555+ entry = AccessEntry (
556+ "READER" , "userByEmail" , "test@example.com" , condition = condition_1
557+ )
558+ assert entry .condition == condition_1
559+ assert entry ._properties .get ("condition" ) == condition_1_api_repr
560+
561+ # Test __init__ with condition=None
562+ def test_init_with_condition_none (self ):
563+ entry = AccessEntry ("READER" , "userByEmail" , "test@example.com" , condition = None )
564+ assert entry .condition is None
565+
566+ # Test condition getter/setter
567+ def test_condition_getter_setter (
568+ self , condition_1 , condition_1_api_repr , condition_2 , condition_2_api_repr
569+ ):
570+ entry = AccessEntry ("WRITER" , "group" , "admins@example.com" )
571+ assert entry .condition is None
572+
573+ # Set condition 1
574+ entry .condition = condition_1
575+ assert entry .condition .to_api_repr () == condition_1_api_repr
576+ assert entry ._properties .get ("condition" ) == condition_1_api_repr
577+
578+ # Set condition 2
579+ entry .condition = condition_2
580+ assert entry .condition .to_api_repr () == condition_2_api_repr
581+ assert entry ._properties .get ("condition" ) != condition_1_api_repr
582+ assert entry ._properties .get ("condition" ) == condition_2 .to_api_repr ()
583+
584+ # Set back to None
585+ entry .condition = None
586+ assert entry .condition is None
587+
588+ # Test setter validation
589+ def test_condition_setter_invalid_type (self ):
590+ entry = AccessEntry ("READER" , "domain" , "example.com" )
591+ with pytest .raises (
592+ TypeError , match = "condition must be a Condition object, dict, or None"
593+ ):
594+ entry .condition = 123 # type: ignore
595+
596+ # Test equality/hash without condition
597+ def test_equality_and_hash_without_condition (self ):
598+ entry1 = AccessEntry ("OWNER" , "specialGroup" , "projectOwners" )
599+ entry2 = AccessEntry ("OWNER" , "specialGroup" , "projectOwners" )
600+ entry3 = AccessEntry ("WRITER" , "specialGroup" , "projectOwners" )
601+ assert entry1 == entry2
602+ assert entry1 != entry3
603+ assert hash (entry1 ) == hash (entry2 )
604+ assert hash (entry1 ) != hash (entry3 ) # Usually true
605+
606+ def test_equality_and_hash_with_condition (self , condition_1 , condition_2 ):
607+ cond1a = Condition (
608+ condition_1 .expression , condition_1 .title , condition_1 .description
609+ )
610+ cond1b = Condition (
611+ condition_1 .expression , condition_1 .title , condition_1 .description
612+ ) # Same values, different object
613+
614+ entry1a = AccessEntry (
615+ "READER" , "userByEmail" , "a@example.com" , condition = cond1a
616+ )
617+ entry1b = AccessEntry (
618+ "READER" , "userByEmail" , "a@example.com" , condition = cond1b
619+ ) # Different Condition instance
620+ entry2 = AccessEntry (
621+ "READER" , "userByEmail" , "a@example.com" , condition = condition_2
622+ )
623+ entry3 = AccessEntry ("READER" , "userByEmail" , "a@example.com" ) # No condition
624+ entry4 = AccessEntry (
625+ "WRITER" , "userByEmail" , "a@example.com" , condition = cond1a
626+ ) # Different role
627+
628+ assert entry1a == entry1b
629+ assert entry1a != entry2
630+ assert entry1a != entry3
631+ assert entry1a != entry4
632+ assert entry2 != entry3
633+
634+ assert hash (entry1a ) == hash (entry1b )
635+ assert hash (entry1a ) != hash (entry2 ) # Usually true
636+ assert hash (entry1a ) != hash (entry3 ) # Usually true
637+ assert hash (entry1a ) != hash (entry4 ) # Usually true
638+
639+ # Test to_api_repr with condition
640+ def test_to_api_repr_with_condition (self , condition_1 , condition_1_api_repr ):
641+ entry = AccessEntry (
642+ "WRITER" , "groupByEmail" , "editors@example.com" , condition = condition_1
643+ )
644+ expected_repr = {
645+ "role" : "WRITER" ,
646+ "groupByEmail" : "editors@example.com" ,
647+ "condition" : condition_1_api_repr ,
648+ }
649+ assert entry .to_api_repr () == expected_repr
650+
651+ def test_view_property_with_condition (self , condition_1 ):
652+ """Test setting/getting view property when condition is present."""
653+ entry = AccessEntry (role = None , entity_type = "view" , condition = condition_1 )
654+ view_ref = TableReference (DatasetReference ("proj" , "dset" ), "view_tbl" )
655+ entry .view = view_ref # Use the setter
656+ assert entry .view == view_ref
657+ assert entry .condition == condition_1 # Condition should persist
658+ assert entry .role is None
659+ assert entry .entity_type == "view"
660+
661+ # Check internal representation
662+ assert "view" in entry ._properties
663+ assert "condition" in entry ._properties
664+
665+ def test_user_by_email_property_with_condition (self , condition_1 ):
666+ """Test setting/getting user_by_email property when condition is present."""
667+ entry = AccessEntry (
668+ role = "READER" , entity_type = "userByEmail" , condition = condition_1
669+ )
670+ email = "test@example.com"
671+ entry .user_by_email = email # Use the setter
672+ assert entry .user_by_email == email
673+ assert entry .condition == condition_1 # Condition should persist
674+ assert entry .role == "READER"
675+ assert entry .entity_type == "userByEmail"
676+
677+ # Check internal representation
678+ assert "userByEmail" in entry ._properties
679+ assert "condition" in entry ._properties
680+
681+ # Test from_api_repr without condition
682+ def test_from_api_repr_without_condition (self ):
683+ api_repr = {"role" : "OWNER" , "userByEmail" : "owner@example.com" }
684+ entry = AccessEntry .from_api_repr (api_repr )
685+ assert entry .role == "OWNER"
686+ assert entry .entity_type == "userByEmail"
687+ assert entry .entity_id == "owner@example.com"
688+ assert entry .condition is None
689+
690+ # Test from_api_repr with condition
691+ def test_from_api_repr_with_condition (self , condition_1 , condition_1_api_repr ):
692+ api_repr = {
693+ "role" : "READER" ,
694+ "view" : {"projectId" : "p" , "datasetId" : "d" , "tableId" : "v" },
695+ "condition" : condition_1_api_repr ,
696+ }
697+ entry = AccessEntry .from_api_repr (api_repr )
698+ assert entry .role == "READER"
699+ assert entry .entity_type == "view"
700+ # The entity_id for view/routine/dataset is the dict itself
701+ assert entry .entity_id == {"projectId" : "p" , "datasetId" : "d" , "tableId" : "v" }
702+ assert entry .condition == condition_1
703+
704+ # Test from_api_repr edge case
705+ def test_from_api_repr_no_entity (self , condition_1 , condition_1_api_repr ):
706+ api_repr = {"role" : "READER" , "condition" : condition_1_api_repr }
707+ entry = AccessEntry .from_api_repr (api_repr )
708+ assert entry .role == "READER"
709+ assert entry .entity_type is None
710+ assert entry .entity_id is None
711+ assert entry .condition == condition_1
712+
713+ def test_dataset_property_with_condition (self , condition_1 ):
714+ project = "my-project"
715+ dataset_id = "my_dataset"
716+ dataset_ref = DatasetReference (project , dataset_id )
717+ entry = self ._make_one (None )
718+ entry .dataset = dataset_ref
719+ entry .condition = condition_1
720+
721+ resource = entry .to_api_repr ()
722+ exp_resource = {
723+ "role" : None ,
724+ "dataset" : {
725+ "dataset" : DatasetReference ("my-project" , "my_dataset" ),
726+ "targetTypes" : None ,
727+ },
728+ "condition" : {
729+ "expression" : "request.time < timestamp('2026-01-01T00:00:00Z')" ,
730+ "title" : "Expires end 2025" ,
731+ "description" : "Access expires at the start of 2026." ,
732+ },
733+ }
734+ assert resource == exp_resource
735+ # Check internal representation
736+ assert "dataset" in entry ._properties
737+ assert "condition" in entry ._properties
738+
739+
497740class TestDatasetReference (unittest .TestCase ):
498741 @staticmethod
499742 def _get_target_class ():
0 commit comments