@@ -881,25 +881,48 @@ async def test_handle_full_registry(self, mock_set_active_registry, mock_handle)
881881
882882 @mock .patch .object (AskarAnonCredsProfileSession , "handle" )
883883 async def test_decommission_registry (self , mock_handle ):
884+ # First fetch_all: find backup (active=false, state=finished)
885+ # Second fetch_all: in transaction, get all registries for cred_def_id
884886 mock_handle .fetch_all = mock .CoroutineMock (
885- return_value = [
886- MockEntry (
887- name = "active-reg-reg" ,
888- tags = {
889- "state" : RevRegDefState .STATE_FINISHED ,
890- "active" : True ,
891- },
892- ),
893- MockEntry (
894- name = "new-rev-reg" ,
895- tags = {
896- "state" : RevRegDefState .STATE_FINISHED ,
897- "active" : True ,
898- },
899- ),
887+ side_effect = [
888+ [
889+ MockEntry (
890+ name = "backup-reg-reg" ,
891+ tags = {
892+ "cred_def_id" : "test-rev-reg-def-id" ,
893+ "state" : RevRegDefState .STATE_FINISHED ,
894+ "active" : "false" ,
895+ },
896+ ),
897+ ],
898+ [
899+ MockEntry (
900+ name = "active-reg-reg" ,
901+ tags = {
902+ "cred_def_id" : "test-rev-reg-def-id" ,
903+ "state" : RevRegDefState .STATE_FINISHED ,
904+ "active" : "true" ,
905+ },
906+ ),
907+ MockEntry (
908+ name = "backup-reg-reg" ,
909+ tags = {
910+ "cred_def_id" : "test-rev-reg-def-id" ,
911+ "state" : RevRegDefState .STATE_FINISHED ,
912+ "active" : "false" ,
913+ },
914+ ),
915+ MockEntry (
916+ name = "new-rev-reg" ,
917+ tags = {
918+ "cred_def_id" : "test-rev-reg-def-id" ,
919+ "state" : RevRegDefState .STATE_FINISHED ,
920+ "active" : "false" ,
921+ },
922+ ),
923+ ],
900924 ]
901925 )
902- # active registry
903926 self .revocation .get_or_create_active_registry = mock .CoroutineMock (
904927 return_value = RevRegDefResult (
905928 job_id = "test-job-id" ,
@@ -912,7 +935,7 @@ async def test_decommission_registry(self, mock_handle):
912935 revocation_registry_definition_metadata = {},
913936 )
914937 )
915- # new active
938+ # New backup only (one call); previous backup becomes active
916939 self .revocation .create_and_register_revocation_registry_definition = (
917940 mock .CoroutineMock (
918941 return_value = RevRegDefResult (
@@ -936,18 +959,118 @@ async def test_decommission_registry(self, mock_handle):
936959 result = await self .revocation .decommission_registry ("test-rev-reg-def-id" )
937960
938961 assert isinstance (result , list )
939- assert len (result ) == 2
962+ assert len (result ) == 3
963+ # First entry (active-reg-reg) is decommissioned; backup and new backup kept
940964 assert result [0 ].tags ["active" ] == "false"
941965 assert result [0 ].tags ["state" ] == RevRegDefState .STATE_DECOMMISSIONED
942966 assert mock_handle .fetch_all .called
943967 assert mock_handle .replace .called
944- # Verify store_revocation_registry_definition was called before set_active_registry
945968 self .revocation .store_revocation_registry_definition .assert_called_once ()
946- # # One for backup
969+ # Previous backup set as active (works with endorsement: backup already on ledger)
970+ self .revocation .set_active_registry .assert_called_once_with ("backup-reg-reg" )
971+ # One new backup registry created (not two)
947972 assert (
948973 self .revocation .create_and_register_revocation_registry_definition .call_count
949- == 2
974+ == 1
975+ )
976+
977+ @mock .patch .object (AskarAnonCredsProfileSession , "handle" )
978+ async def test_decommission_registry_no_backup_raises (self , mock_handle ):
979+ """When no backup registry exists, rotation raises."""
980+ mock_handle .fetch_all = mock .CoroutineMock (return_value = [])
981+ self .revocation .get_or_create_active_registry = mock .CoroutineMock (
982+ return_value = RevRegDefResult (
983+ job_id = "test-job-id" ,
984+ revocation_registry_definition_state = RevRegDefState (
985+ state = RevRegDefState .STATE_FINISHED ,
986+ revocation_registry_definition_id = "active-reg-reg" ,
987+ revocation_registry_definition = rev_reg_def ,
988+ ),
989+ registration_metadata = {},
990+ revocation_registry_definition_metadata = {},
991+ )
992+ )
993+ with self .assertRaises (test_module .AnonCredsRevocationError ) as cm :
994+ await self .revocation .decommission_registry ("test-rev-reg-def-id" )
995+ self .assertIn ("No backup registry available" , str (cm .exception ))
996+
997+ @mock .patch .object (AskarAnonCredsProfileSession , "handle" )
998+ async def test_decommission_registry_new_backup_creation_fails (self , mock_handle ):
999+ """When creating the new backup fails, we still promote backup to active and decommission old."""
1000+ backup_entry = MockEntry (
1001+ name = "backup-reg-reg" ,
1002+ tags = {
1003+ "cred_def_id" : "test-rev-reg-def-id" ,
1004+ "state" : RevRegDefState .STATE_FINISHED ,
1005+ "active" : "false" ,
1006+ },
1007+ )
1008+ mock_handle .fetch_all = mock .CoroutineMock (
1009+ side_effect = [
1010+ [backup_entry ],
1011+ [
1012+ MockEntry (
1013+ name = "active-reg-reg" ,
1014+ tags = {
1015+ "cred_def_id" : "test-rev-reg-def-id" ,
1016+ "state" : RevRegDefState .STATE_FINISHED ,
1017+ "active" : "true" ,
1018+ },
1019+ ),
1020+ backup_entry ,
1021+ ],
1022+ ]
1023+ )
1024+ mock_handle .replace = mock .CoroutineMock (return_value = None )
1025+ self .revocation .get_or_create_active_registry = mock .CoroutineMock (
1026+ return_value = RevRegDefResult (
1027+ job_id = "test-job-id" ,
1028+ revocation_registry_definition_state = RevRegDefState (
1029+ state = RevRegDefState .STATE_FINISHED ,
1030+ revocation_registry_definition_id = "active-reg-reg" ,
1031+ revocation_registry_definition = rev_reg_def ,
1032+ ),
1033+ registration_metadata = {},
1034+ revocation_registry_definition_metadata = {},
1035+ )
1036+ )
1037+ self .revocation .create_and_register_revocation_registry_definition = (
1038+ mock .CoroutineMock (return_value = "Failed to create new registry" )
1039+ )
1040+ self .revocation .set_active_registry = mock .CoroutineMock (return_value = None )
1041+
1042+ result = await self .revocation .decommission_registry ("test-rev-reg-def-id" )
1043+
1044+ self .revocation .set_active_registry .assert_called_once_with ("backup-reg-reg" )
1045+ assert mock_handle .replace .call_count == 1
1046+ assert len (result ) == 2
1047+ assert result [0 ].tags ["state" ] == RevRegDefState .STATE_DECOMMISSIONED
1048+
1049+ @mock .patch .object (AskarAnonCredsProfileSession , "handle" )
1050+ async def test_get_backup_registry_id_raises_when_no_backup (self , mock_handle ):
1051+ """_get_backup_registry_id raises when no finished backup exists."""
1052+ mock_handle .fetch_all = mock .CoroutineMock (return_value = [])
1053+ with self .assertRaises (test_module .AnonCredsRevocationError ) as cm :
1054+ await self .revocation ._get_backup_registry_id ("test-cred-def-id" )
1055+ self .assertIn ("No backup registry available" , str (cm .exception ))
1056+
1057+ @mock .patch .object (AskarAnonCredsProfileSession , "handle" )
1058+ async def test_get_backup_registry_id_returns_first_backup (self , mock_handle ):
1059+ """_get_backup_registry_id returns the name of the first matching backup."""
1060+ mock_handle .fetch_all = mock .CoroutineMock (
1061+ return_value = [
1062+ MockEntry (
1063+ name = "backup-id-123" ,
1064+ tags = {
1065+ "cred_def_id" : "test-cred-def-id" ,
1066+ "state" : RevRegDefState .STATE_FINISHED ,
1067+ "active" : "false" ,
1068+ },
1069+ ),
1070+ ]
9501071 )
1072+ result = await self .revocation ._get_backup_registry_id ("test-cred-def-id" )
1073+ assert result == "backup-id-123"
9511074
9521075 @mock .patch .object (AskarAnonCredsProfileSession , "handle" )
9531076 async def test_get_or_create_active_registry (self , mock_handle ):
0 commit comments