| 
									
										
										
										
											2021-08-09 22:55:29 +09:00
										 |  |  | import json | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import pytest | 
					
						
							| 
									
										
										
										
											2021-10-18 19:44:29 +00:00
										 |  |  | import sure  # noqa # pylint: disable=unused-import | 
					
						
							| 
									
										
										
										
											2021-08-09 22:55:29 +09:00
										 |  |  | 
 | 
					
						
							|  |  |  | from moto.ecr.exceptions import InvalidParameterException | 
					
						
							|  |  |  | from moto.ecr.policy_validation import EcrLifecyclePolicyValidator | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def test_validate(): | 
					
						
							|  |  |  |     # given | 
					
						
							|  |  |  |     policy = { | 
					
						
							|  |  |  |         "rules": [ | 
					
						
							|  |  |  |             { | 
					
						
							|  |  |  |                 "rulePriority": 1, | 
					
						
							|  |  |  |                 "description": "test policy", | 
					
						
							|  |  |  |                 "selection": { | 
					
						
							|  |  |  |                     "tagStatus": "untagged", | 
					
						
							|  |  |  |                     "countType": "imageCountMoreThan", | 
					
						
							|  |  |  |                     "countNumber": 30, | 
					
						
							|  |  |  |                 }, | 
					
						
							|  |  |  |                 "action": {"type": "expire"}, | 
					
						
							|  |  |  |             }, | 
					
						
							|  |  |  |             { | 
					
						
							|  |  |  |                 "rulePriority": 2, | 
					
						
							|  |  |  |                 "description": "test policy", | 
					
						
							|  |  |  |                 "selection": { | 
					
						
							|  |  |  |                     "tagStatus": "tagged", | 
					
						
							|  |  |  |                     "tagPrefixList": ["3.9"], | 
					
						
							|  |  |  |                     "countType": "sinceImagePushed", | 
					
						
							|  |  |  |                     "countUnit": "days", | 
					
						
							|  |  |  |                     "countNumber": 30, | 
					
						
							|  |  |  |                 }, | 
					
						
							|  |  |  |                 "action": {"type": "expire"}, | 
					
						
							|  |  |  |             }, | 
					
						
							|  |  |  |         ] | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # when/then | 
					
						
							|  |  |  |     validator = EcrLifecyclePolicyValidator(json.dumps(policy)) | 
					
						
							|  |  |  |     validator.validate() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | @pytest.mark.parametrize( | 
					
						
							|  |  |  |     "policy", | 
					
						
							|  |  |  |     [ | 
					
						
							|  |  |  |         "some invalid input", | 
					
						
							|  |  |  |         [ | 
					
						
							|  |  |  |             { | 
					
						
							|  |  |  |                 "rulePriority": 1, | 
					
						
							|  |  |  |                 "description": "test policy", | 
					
						
							|  |  |  |                 "selection": { | 
					
						
							|  |  |  |                     "tagStatus": "untagged", | 
					
						
							|  |  |  |                     "countType": "imageCountMoreThan", | 
					
						
							|  |  |  |                     "countNumber": 30, | 
					
						
							|  |  |  |                 }, | 
					
						
							|  |  |  |                 "action": {"type": "expire"}, | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |         ], | 
					
						
							|  |  |  |     ], | 
					
						
							|  |  |  |     ids=["not_json", "not_dict"], | 
					
						
							|  |  |  | ) | 
					
						
							|  |  |  | def test_validate_error_parse(policy): | 
					
						
							|  |  |  |     # given | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # when | 
					
						
							|  |  |  |     with pytest.raises(InvalidParameterException) as e: | 
					
						
							|  |  |  |         validator = EcrLifecyclePolicyValidator(json.dumps(policy)) | 
					
						
							|  |  |  |         validator.validate() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # then | 
					
						
							|  |  |  |     ex = e.value | 
					
						
							|  |  |  |     ex.code.should.equal(400) | 
					
						
							|  |  |  |     ex.error_type.should.equal("InvalidParameterException") | 
					
						
							|  |  |  |     ex.message.should.equal( | 
					
						
							|  |  |  |         "Invalid parameter at 'LifecyclePolicyText' failed to satisfy constraint: " | 
					
						
							|  |  |  |         "'Lifecycle policy validation failure: " | 
					
						
							|  |  |  |         "Could not map policyString into LifecyclePolicy.'" | 
					
						
							|  |  |  |     ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | @pytest.mark.parametrize( | 
					
						
							|  |  |  |     "policy", | 
					
						
							|  |  |  |     [ | 
					
						
							|  |  |  |         {"no_rules": "test"}, | 
					
						
							|  |  |  |         { | 
					
						
							|  |  |  |             "rules": { | 
					
						
							|  |  |  |                 "rulePriority": 1, | 
					
						
							|  |  |  |                 "description": "test policy", | 
					
						
							|  |  |  |                 "selection": { | 
					
						
							|  |  |  |                     "tagStatus": "untagged", | 
					
						
							|  |  |  |                     "countType": "imageCountMoreThan", | 
					
						
							|  |  |  |                     "countNumber": 30, | 
					
						
							|  |  |  |                 }, | 
					
						
							|  |  |  |                 "action": {"type": "expire"}, | 
					
						
							|  |  |  |             }, | 
					
						
							|  |  |  |         }, | 
					
						
							|  |  |  |     ], | 
					
						
							|  |  |  |     ids=["no_rules", "not_list"], | 
					
						
							|  |  |  | ) | 
					
						
							|  |  |  | def test_validate_error_extract_rules(policy): | 
					
						
							|  |  |  |     # given | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # when | 
					
						
							|  |  |  |     with pytest.raises(InvalidParameterException) as e: | 
					
						
							|  |  |  |         validator = EcrLifecyclePolicyValidator(json.dumps(policy)) | 
					
						
							|  |  |  |         validator.validate() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # then | 
					
						
							|  |  |  |     ex = e.value | 
					
						
							|  |  |  |     ex.code.should.equal(400) | 
					
						
							|  |  |  |     ex.error_type.should.equal("InvalidParameterException") | 
					
						
							|  |  |  |     ex.message.should.equal( | 
					
						
							|  |  |  |         "Invalid parameter at 'LifecyclePolicyText' failed to satisfy constraint: " | 
					
						
							|  |  |  |         "'Lifecycle policy validation failure: " | 
					
						
							|  |  |  |         'object has missing required properties (["rules"])\'' | 
					
						
							|  |  |  |     ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-03-11 20:28:45 -01:00
										 |  |  | @pytest.mark.parametrize("rule", ["not_dict"], ids=["not_dict"]) | 
					
						
							|  |  |  | def test_validate_error_rule_type(rule): | 
					
						
							| 
									
										
										
										
											2021-08-09 22:55:29 +09:00
										 |  |  |     # given | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # when | 
					
						
							|  |  |  |     with pytest.raises(InvalidParameterException) as e: | 
					
						
							|  |  |  |         validator = EcrLifecyclePolicyValidator(json.dumps({"rules": [rule]})) | 
					
						
							|  |  |  |         validator.validate() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # then | 
					
						
							|  |  |  |     ex = e.value | 
					
						
							|  |  |  |     ex.code.should.equal(400) | 
					
						
							|  |  |  |     ex.error_type.should.equal("InvalidParameterException") | 
					
						
							|  |  |  |     ex.message.should.equal( | 
					
						
							|  |  |  |         "Invalid parameter at 'LifecyclePolicyText' failed to satisfy constraint: " | 
					
						
							|  |  |  |         "'Lifecycle policy validation failure: " | 
					
						
							|  |  |  |         f'instance type ({type(rule)}) does not match any allowed primitive type (allowed: ["object"])\'' | 
					
						
							|  |  |  |     ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | @pytest.mark.parametrize( | 
					
						
							|  |  |  |     ["rule", "error_msg"], | 
					
						
							|  |  |  |     [ | 
					
						
							|  |  |  |         [ | 
					
						
							|  |  |  |             { | 
					
						
							|  |  |  |                 "description": "test policy", | 
					
						
							|  |  |  |                 "selection": { | 
					
						
							|  |  |  |                     "tagStatus": "untagged", | 
					
						
							|  |  |  |                     "countType": "imageCountMoreThan", | 
					
						
							|  |  |  |                     "countNumber": 30, | 
					
						
							|  |  |  |                 }, | 
					
						
							|  |  |  |                 "action": {"type": "expire"}, | 
					
						
							|  |  |  |             }, | 
					
						
							|  |  |  |             'object has missing required properties (["rulePriority"])\'', | 
					
						
							|  |  |  |         ], | 
					
						
							|  |  |  |         [ | 
					
						
							|  |  |  |             { | 
					
						
							|  |  |  |                 "rulePriority": 1, | 
					
						
							|  |  |  |                 "description": "test policy", | 
					
						
							|  |  |  |                 "action": {"type": "expire"}, | 
					
						
							|  |  |  |             }, | 
					
						
							|  |  |  |             'object has missing required properties (["selection"])\'', | 
					
						
							|  |  |  |         ], | 
					
						
							|  |  |  |         [ | 
					
						
							|  |  |  |             { | 
					
						
							|  |  |  |                 "rulePriority": 1, | 
					
						
							|  |  |  |                 "description": "test policy", | 
					
						
							|  |  |  |                 "selection": { | 
					
						
							|  |  |  |                     "tagStatus": "untagged", | 
					
						
							|  |  |  |                     "countType": "imageCountMoreThan", | 
					
						
							|  |  |  |                     "countNumber": 30, | 
					
						
							|  |  |  |                 }, | 
					
						
							|  |  |  |             }, | 
					
						
							|  |  |  |             'object has missing required properties (["action"])\'', | 
					
						
							|  |  |  |         ], | 
					
						
							|  |  |  |         [ | 
					
						
							|  |  |  |             { | 
					
						
							|  |  |  |                 "rulePriority": 1, | 
					
						
							|  |  |  |                 "selection": { | 
					
						
							|  |  |  |                     "tagStatus": "untagged", | 
					
						
							|  |  |  |                     "countType": "imageCountMoreThan", | 
					
						
							|  |  |  |                     "countNumber": 30, | 
					
						
							|  |  |  |                 }, | 
					
						
							|  |  |  |                 "action": {"type": "expire"}, | 
					
						
							|  |  |  |                 "unknown": 123, | 
					
						
							|  |  |  |             }, | 
					
						
							|  |  |  |             'object instance has properties which are not allowed by the schema: (["unknown"])\'', | 
					
						
							|  |  |  |         ], | 
					
						
							|  |  |  |     ], | 
					
						
							|  |  |  |     ids=["missing_rulePriority", "missing_selection", "missing_action", "unknown"], | 
					
						
							|  |  |  | ) | 
					
						
							|  |  |  | def test_validate_error_rule_properties(rule, error_msg): | 
					
						
							|  |  |  |     # given | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # when | 
					
						
							|  |  |  |     with pytest.raises(InvalidParameterException) as e: | 
					
						
							|  |  |  |         validator = EcrLifecyclePolicyValidator(json.dumps({"rules": [rule]})) | 
					
						
							|  |  |  |         validator.validate() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # then | 
					
						
							|  |  |  |     ex = e.value | 
					
						
							|  |  |  |     ex.code.should.equal(400) | 
					
						
							|  |  |  |     ex.error_type.should.equal("InvalidParameterException") | 
					
						
							|  |  |  |     ex.message.should.equal( | 
					
						
							|  |  |  |         "".join( | 
					
						
							|  |  |  |             [ | 
					
						
							|  |  |  |                 "Invalid parameter at 'LifecyclePolicyText' failed to satisfy constraint: " | 
					
						
							|  |  |  |                 "'Lifecycle policy validation failure: ", | 
					
						
							|  |  |  |                 error_msg, | 
					
						
							|  |  |  |             ] | 
					
						
							|  |  |  |         ) | 
					
						
							|  |  |  |     ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | @pytest.mark.parametrize( | 
					
						
							|  |  |  |     ["action", "error_msg"], | 
					
						
							|  |  |  |     [ | 
					
						
							| 
									
										
										
										
											2022-03-10 13:39:59 -01:00
										 |  |  |         [{}, 'object has missing required properties (["type"])\''], | 
					
						
							| 
									
										
										
										
											2021-08-09 22:55:29 +09:00
										 |  |  |         [ | 
					
						
							|  |  |  |             {"type": "expire", "unknown": 123}, | 
					
						
							|  |  |  |             ( | 
					
						
							|  |  |  |                 "object instance has properties " | 
					
						
							|  |  |  |                 'which are not allowed by the schema: (["unknown"])\'' | 
					
						
							|  |  |  |             ), | 
					
						
							|  |  |  |         ], | 
					
						
							|  |  |  |         [ | 
					
						
							|  |  |  |             {"type": "keep"}, | 
					
						
							|  |  |  |             ( | 
					
						
							|  |  |  |                 "instance value (keep) not found in enum " | 
					
						
							|  |  |  |                 ':(possible values: ["expire"])\'' | 
					
						
							|  |  |  |             ), | 
					
						
							|  |  |  |         ], | 
					
						
							|  |  |  |     ], | 
					
						
							|  |  |  |     ids=["missing_type", "unknown", "unknown_type_value"], | 
					
						
							|  |  |  | ) | 
					
						
							|  |  |  | def test_validate_error_action_properties(action, error_msg): | 
					
						
							|  |  |  |     # given | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # when | 
					
						
							|  |  |  |     with pytest.raises(InvalidParameterException) as e: | 
					
						
							|  |  |  |         validator = EcrLifecyclePolicyValidator( | 
					
						
							|  |  |  |             json.dumps( | 
					
						
							|  |  |  |                 { | 
					
						
							|  |  |  |                     "rules": [ | 
					
						
							|  |  |  |                         { | 
					
						
							|  |  |  |                             "rulePriority": 1, | 
					
						
							|  |  |  |                             "selection": { | 
					
						
							|  |  |  |                                 "tagStatus": "untagged", | 
					
						
							|  |  |  |                                 "countType": "imageCountMoreThan", | 
					
						
							|  |  |  |                                 "countNumber": 30, | 
					
						
							|  |  |  |                             }, | 
					
						
							|  |  |  |                             "action": action, | 
					
						
							|  |  |  |                         } | 
					
						
							|  |  |  |                     ] | 
					
						
							|  |  |  |                 } | 
					
						
							|  |  |  |             ) | 
					
						
							|  |  |  |         ) | 
					
						
							|  |  |  |         validator.validate() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # then | 
					
						
							|  |  |  |     ex = e.value | 
					
						
							|  |  |  |     ex.code.should.equal(400) | 
					
						
							|  |  |  |     ex.error_type.should.equal("InvalidParameterException") | 
					
						
							|  |  |  |     ex.message.should.equal( | 
					
						
							|  |  |  |         "".join( | 
					
						
							|  |  |  |             [ | 
					
						
							|  |  |  |                 "Invalid parameter at 'LifecyclePolicyText' failed to satisfy constraint: " | 
					
						
							|  |  |  |                 "'Lifecycle policy validation failure: ", | 
					
						
							|  |  |  |                 error_msg, | 
					
						
							|  |  |  |             ] | 
					
						
							|  |  |  |         ) | 
					
						
							|  |  |  |     ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | @pytest.mark.parametrize( | 
					
						
							|  |  |  |     ["selection", "error_msg"], | 
					
						
							|  |  |  |     [ | 
					
						
							|  |  |  |         [ | 
					
						
							| 
									
										
										
										
											2022-03-10 13:39:59 -01:00
										 |  |  |             {"countType": "imageCountMoreThan", "countNumber": 30}, | 
					
						
							| 
									
										
										
										
											2021-08-09 22:55:29 +09:00
										 |  |  |             'object has missing required properties (["tagStatus"])\'', | 
					
						
							|  |  |  |         ], | 
					
						
							|  |  |  |         [ | 
					
						
							| 
									
										
										
										
											2022-03-10 13:39:59 -01:00
										 |  |  |             {"tagStatus": "untagged", "countNumber": 30}, | 
					
						
							| 
									
										
										
										
											2021-08-09 22:55:29 +09:00
										 |  |  |             'object has missing required properties (["countType"])\'', | 
					
						
							|  |  |  |         ], | 
					
						
							|  |  |  |         [ | 
					
						
							| 
									
										
										
										
											2022-03-10 13:39:59 -01:00
										 |  |  |             {"tagStatus": "untagged", "countType": "imageCountMoreThan"}, | 
					
						
							| 
									
										
										
										
											2021-08-09 22:55:29 +09:00
										 |  |  |             'object has missing required properties (["countNumber"])\'', | 
					
						
							|  |  |  |         ], | 
					
						
							|  |  |  |         [ | 
					
						
							|  |  |  |             { | 
					
						
							|  |  |  |                 "tagStatus": "untagged", | 
					
						
							|  |  |  |                 "countType": "imageCountMoreThan", | 
					
						
							|  |  |  |                 "countNumber": 30, | 
					
						
							|  |  |  |                 "unknown": 123, | 
					
						
							|  |  |  |             }, | 
					
						
							|  |  |  |             ( | 
					
						
							|  |  |  |                 "object instance has properties " | 
					
						
							|  |  |  |                 'which are not allowed by the schema: (["unknown"])\'' | 
					
						
							|  |  |  |             ), | 
					
						
							|  |  |  |         ], | 
					
						
							|  |  |  |         [ | 
					
						
							|  |  |  |             { | 
					
						
							|  |  |  |                 "tagStatus": "unknown", | 
					
						
							|  |  |  |                 "countType": "imageCountMoreThan", | 
					
						
							|  |  |  |                 "countNumber": 30, | 
					
						
							|  |  |  |             }, | 
					
						
							|  |  |  |             ( | 
					
						
							|  |  |  |                 "instance value (unknown) not found in enum " | 
					
						
							|  |  |  |                 ':(possible values: ["any", "tagged", "untagged"])\'' | 
					
						
							|  |  |  |             ), | 
					
						
							|  |  |  |         ], | 
					
						
							|  |  |  |         [ | 
					
						
							|  |  |  |             {"tagStatus": "untagged", "countType": "unknown", "countNumber": 30}, | 
					
						
							|  |  |  |             "instance failed to match exactly one schema (matched 0 out of 2)", | 
					
						
							|  |  |  |         ], | 
					
						
							|  |  |  |         [ | 
					
						
							|  |  |  |             { | 
					
						
							|  |  |  |                 "tagStatus": "untagged", | 
					
						
							|  |  |  |                 "countType": "sinceImagePushed", | 
					
						
							|  |  |  |                 "countUnit": "unknown", | 
					
						
							|  |  |  |                 "countNumber": 30, | 
					
						
							|  |  |  |             }, | 
					
						
							|  |  |  |             ( | 
					
						
							|  |  |  |                 "instance value (unknown) not found in enum " | 
					
						
							|  |  |  |                 ':(possible values: ["days"])\'' | 
					
						
							|  |  |  |             ), | 
					
						
							|  |  |  |         ], | 
					
						
							|  |  |  |         [ | 
					
						
							|  |  |  |             { | 
					
						
							|  |  |  |                 "tagStatus": "untagged", | 
					
						
							|  |  |  |                 "countType": "imageCountMoreThan", | 
					
						
							|  |  |  |                 "countNumber": 0, | 
					
						
							|  |  |  |             }, | 
					
						
							|  |  |  |             ( | 
					
						
							|  |  |  |                 "numeric instance is lower than the required minimum " | 
					
						
							| 
									
										
										
										
											2022-04-27 11:58:59 +00:00
										 |  |  |                 "(minimum: 1, found: 0)" | 
					
						
							| 
									
										
										
										
											2021-08-09 22:55:29 +09:00
										 |  |  |             ), | 
					
						
							|  |  |  |         ], | 
					
						
							|  |  |  |         [ | 
					
						
							|  |  |  |             { | 
					
						
							|  |  |  |                 "tagStatus": "untagged", | 
					
						
							|  |  |  |                 "countType": "imageCountMoreThan", | 
					
						
							|  |  |  |                 "countNumber": -1, | 
					
						
							|  |  |  |             }, | 
					
						
							|  |  |  |             ( | 
					
						
							|  |  |  |                 "numeric instance is lower than the required minimum " | 
					
						
							| 
									
										
										
										
											2022-04-27 11:58:59 +00:00
										 |  |  |                 "(minimum: 1, found: -1)" | 
					
						
							| 
									
										
										
										
											2021-08-09 22:55:29 +09:00
										 |  |  |             ), | 
					
						
							|  |  |  |         ], | 
					
						
							|  |  |  |     ], | 
					
						
							|  |  |  |     ids=[ | 
					
						
							|  |  |  |         "missing_tagStatus", | 
					
						
							|  |  |  |         "missing_countType", | 
					
						
							|  |  |  |         "missing_countNumber", | 
					
						
							|  |  |  |         "unknown", | 
					
						
							|  |  |  |         "unknown_tagStatus_value", | 
					
						
							|  |  |  |         "unknown_countType_value", | 
					
						
							|  |  |  |         "unknown_countUnit_value", | 
					
						
							|  |  |  |         "zero_countNumber_value", | 
					
						
							|  |  |  |         "negative_countNumber_value", | 
					
						
							|  |  |  |     ], | 
					
						
							|  |  |  | ) | 
					
						
							|  |  |  | def test_validate_error_selection_properties(selection, error_msg): | 
					
						
							|  |  |  |     # given | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # when | 
					
						
							|  |  |  |     with pytest.raises(InvalidParameterException) as e: | 
					
						
							|  |  |  |         validator = EcrLifecyclePolicyValidator( | 
					
						
							|  |  |  |             json.dumps( | 
					
						
							|  |  |  |                 { | 
					
						
							|  |  |  |                     "rules": [ | 
					
						
							|  |  |  |                         { | 
					
						
							|  |  |  |                             "rulePriority": 1, | 
					
						
							|  |  |  |                             "selection": selection, | 
					
						
							|  |  |  |                             "action": {"type": "expire"}, | 
					
						
							|  |  |  |                         } | 
					
						
							|  |  |  |                     ] | 
					
						
							|  |  |  |                 } | 
					
						
							|  |  |  |             ) | 
					
						
							|  |  |  |         ) | 
					
						
							|  |  |  |         validator.validate() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # then | 
					
						
							|  |  |  |     ex = e.value | 
					
						
							|  |  |  |     ex.code.should.equal(400) | 
					
						
							|  |  |  |     ex.error_type.should.equal("InvalidParameterException") | 
					
						
							|  |  |  |     ex.message.should.equal( | 
					
						
							|  |  |  |         "".join( | 
					
						
							|  |  |  |             [ | 
					
						
							|  |  |  |                 "Invalid parameter at 'LifecyclePolicyText' failed to satisfy constraint: " | 
					
						
							|  |  |  |                 "'Lifecycle policy validation failure: ", | 
					
						
							|  |  |  |                 error_msg, | 
					
						
							|  |  |  |             ] | 
					
						
							|  |  |  |         ) | 
					
						
							|  |  |  |     ) |