diff --git a/pytest_tests/helpers/grpc_responses.py b/pytest_tests/helpers/grpc_responses.py new file mode 100644 index 0000000..e7bc572 --- /dev/null +++ b/pytest_tests/helpers/grpc_responses.py @@ -0,0 +1,21 @@ +import re + + +# Regex patterns of status codes of Container service (https://github.com/nspcc-dev/neofs-spec/blob/98b154848116223e486ce8b43eaa35fec08b4a99/20-api-v2/container.md) +CONTAINER_NOT_FOUND = "code = 3072.*message = container not found" + + +# Regex patterns of status codes of Object service (https://github.com/nspcc-dev/neofs-spec/blob/98b154848116223e486ce8b43eaa35fec08b4a99/20-api-v2/object.md) +OBJECT_ACCESS_DENIED = "code = 2048.*message = access to object operation denied" +OBJECT_NOT_FOUND = "code = 2049.*message = object not found" +OBJECT_ALREADY_REMOVED = "code = 2052.*message = object already removed" + + +def error_matches_status(error: Exception, status_pattern: str) -> bool: + """ + Determines whether exception matches specified status pattern. + + We use re.search to be consistent with pytest.raises. + """ + match = re.search(status_pattern, str(error)) + return match is not None diff --git a/pytest_tests/testsuites/acl/test_acl.py b/pytest_tests/testsuites/acl/test_acl.py index 9963379..e659c5b 100644 --- a/pytest_tests/testsuites/acl/test_acl.py +++ b/pytest_tests/testsuites/acl/test_acl.py @@ -6,6 +6,7 @@ import pytest import wallet from common import ASSETS_DIR +from grpc_responses import OBJECT_ACCESS_DENIED from python_keywords.acl import set_eacl from python_keywords.container import create_container from python_keywords.neofs_verbs import (delete_object, get_object, get_range, @@ -77,26 +78,25 @@ class TestACL: @staticmethod def check_no_access_to_container(wallet: str, cid: str, oid: str, file_name: str): - err_pattern = '.*access to object operation denied.*' - with pytest.raises(Exception, match=err_pattern): + with pytest.raises(Exception, match=OBJECT_ACCESS_DENIED): get_object(wallet, cid, oid) - with pytest.raises(Exception, match=err_pattern): + with pytest.raises(Exception, match=OBJECT_ACCESS_DENIED): put_object(wallet, file_name, cid) - with pytest.raises(Exception, match=err_pattern): + with pytest.raises(Exception, match=OBJECT_ACCESS_DENIED): delete_object(wallet, cid, oid) - with pytest.raises(Exception, match=err_pattern): + with pytest.raises(Exception, match=OBJECT_ACCESS_DENIED): head_object(wallet, cid, oid) - with pytest.raises(Exception, match=err_pattern): + with pytest.raises(Exception, match=OBJECT_ACCESS_DENIED): get_range(wallet, cid, oid, bearer='', range_cut='0:10') - with pytest.raises(Exception, match=err_pattern): + with pytest.raises(Exception, match=OBJECT_ACCESS_DENIED): get_range_hash(wallet, cid, oid, bearer_token='', range_cut='0:10') - with pytest.raises(Exception, match=err_pattern): + with pytest.raises(Exception, match=OBJECT_ACCESS_DENIED): search_object(wallet, cid) def check_full_access(self, cid: str, file_name: str, wallet_to_check: Tuple = None) -> str: diff --git a/pytest_tests/testsuites/container/test_container.py b/pytest_tests/testsuites/container/test_container.py index f8ca21d..8f96bc8 100644 --- a/pytest_tests/testsuites/container/test_container.py +++ b/pytest_tests/testsuites/container/test_container.py @@ -5,6 +5,7 @@ import allure import pytest from epoch import tick_epoch +from grpc_responses import CONTAINER_NOT_FOUND, error_matches_status from python_keywords.container import (create_container, delete_container, get_container, list_containers) from utility import placement_policy_from_container @@ -23,14 +24,14 @@ def test_container_creation(prepare_wallet_and_deposit, name): json_wallet = json.load(file) placement_rule = 'REP 2 IN X CBF 1 SELECT 2 FROM * AS X' - options = f" --name {name}" if name else "" + options = f"--name {name}" if name else "" cid = create_container(wallet, rule=placement_rule, options=options) containers = list_containers(wallet) assert cid in containers, f'Expected container {cid} in containers: {containers}' - container_info = get_container(wallet, cid, flag='') - container_info = container_info.lower() # To ignore case when comparing with expected values + container_info: str = get_container(wallet, cid, flag='') + container_info = container_info.casefold() # To ignore case when comparing with expected values info_to_check = { f'basic ACL: {PRIVATE_ACL_F} (private)', @@ -41,13 +42,13 @@ def test_container_creation(prepare_wallet_and_deposit, name): info_to_check.add(f'Name={name}') with allure.step('Check container has correct information'): - expected_policy = placement_rule.lower() + expected_policy = placement_rule.casefold() actual_policy = placement_policy_from_container(container_info) assert actual_policy == expected_policy, \ f'Expected policy\n{expected_policy} but got policy\n{actual_policy}' for info in info_to_check: - expected_info = info.lower() + expected_info = info.casefold() assert expected_info in container_info, \ f'Expected {expected_info} in container info:\n{container_info}' @@ -66,7 +67,8 @@ def wait_for_container_deletion(wallet: str, cid: str) -> None: sleep(sleep_interval) continue except Exception as err: - if 'container not found' not in str(err): - raise AssertionError(f'Expected "container not found" in error, got\n{err}') - return + if error_matches_status(err, CONTAINER_NOT_FOUND): + return + raise AssertionError(f'Expected "{CONTAINER_NOT_FOUND}" error, got\n{err}') + raise AssertionError(f'Container was not deleted within {attempts * sleep_interval} sec') diff --git a/pytest_tests/testsuites/network/test_node_management.py b/pytest_tests/testsuites/network/test_node_management.py index 8efe2e0..9379f54 100644 --- a/pytest_tests/testsuites/network/test_node_management.py +++ b/pytest_tests/testsuites/network/test_node_management.py @@ -8,6 +8,7 @@ from data_formatters import get_wallet_public_key from common import (COMPLEX_OBJ_SIZE, MAINNET_BLOCK_TIME, NEOFS_CONTRACT_CACHE_TIMEOUT, NEOFS_NETMAP_DICT, STORAGE_RPC_ENDPOINT_1, STORAGE_WALLET_PASS) from epoch import tick_epoch +from grpc_responses import OBJECT_NOT_FOUND, error_matches_status from python_keywords.container import create_container, get_container from python_keywords.failover_utils import wait_object_replication_on_nodes from python_keywords.neofs_verbs import delete_object, get_object, head_object, put_object @@ -400,7 +401,8 @@ def wait_for_obj_dropped(wallet: str, cid: str, oid: str, checker) -> None: checker(wallet, cid, oid) wait_for_gc_pass_on_storage_nodes() except Exception as err: - if 'object not found' in str(err): - break - else: - raise AssertionError(f'Object {oid} is not dropped from node') + if error_matches_status(err, OBJECT_NOT_FOUND): + return + raise AssertionError(f'Expected "{OBJECT_NOT_FOUND}" error, got\n{err}') + + raise AssertionError(f'Object {oid} was not dropped from node') diff --git a/pytest_tests/testsuites/object/test_object_api.py b/pytest_tests/testsuites/object/test_object_api.py index 1dd7976..62ca523 100644 --- a/pytest_tests/testsuites/object/test_object_api.py +++ b/pytest_tests/testsuites/object/test_object_api.py @@ -3,9 +3,11 @@ from time import sleep import allure import pytest + from common import SIMPLE_OBJ_SIZE, COMPLEX_OBJ_SIZE from container import create_container from epoch import get_epoch, tick_epoch +from grpc_responses import OBJECT_ALREADY_REMOVED, OBJECT_NOT_FOUND, error_matches_status from python_keywords.neofs_verbs import (delete_object, get_object, get_range, get_range_hash, head_object, put_object, search_object) @@ -93,8 +95,8 @@ def test_object_api(prepare_wallet_and_deposit, request, object_size): sleep(CLEANUP_TIMEOUT) with allure.step('Get objects and check errors'): - get_object_and_check_error(**wallet_cid, oid=oids[0], err_msg='object already removed') - get_object_and_check_error(**wallet_cid, oid=oids[1], err_msg='object already removed') + get_object_and_check_error(**wallet_cid, oid=oids[0], error_pattern=OBJECT_ALREADY_REMOVED) + get_object_and_check_error(**wallet_cid, oid=oids[1], error_pattern=OBJECT_ALREADY_REMOVED) @allure.title('Test object life time') @@ -126,17 +128,17 @@ def test_object_api_lifetime(prepare_wallet_and_deposit, request, object_size): wait_for_gc_pass_on_storage_nodes() with allure.step('Check object deleted because it expires-on epoch'): - with pytest.raises(Exception, match='.*object not found.*'): + with pytest.raises(Exception, match=OBJECT_NOT_FOUND): get_object(wallet, cid, oid) -def get_object_and_check_error(wallet: str, cid: str, oid: str, err_msg: str): +def get_object_and_check_error(wallet: str, cid: str, oid: str, error_pattern: str) -> None: try: get_object(wallet=wallet, cid=cid, oid=oid) raise AssertionError(f'Expected object {oid} removed, but it is not') except Exception as err: logger.info(f'Error is {err}') - assert err_msg in str(err), f'Expected message {err_msg} in error: {err}' + assert error_matches_status(err, error_pattern), f'Expected {err} to match {error_pattern}' def check_header_is_presented(head_info: dict, object_header: dict): diff --git a/pytest_tests/testsuites/services/test_http_gate.py b/pytest_tests/testsuites/services/test_http_gate.py index 9a0cf53..a1a81e9 100644 --- a/pytest_tests/testsuites/services/test_http_gate.py +++ b/pytest_tests/testsuites/services/test_http_gate.py @@ -5,9 +5,11 @@ from time import sleep import allure import pytest + from common import COMPLEX_OBJ_SIZE from container import create_container from epoch import get_epoch, tick_epoch +from grpc_responses import OBJECT_NOT_FOUND, error_matches_status from python_keywords.http_gate import (get_via_http_curl, get_via_http_gate, get_via_http_gate_by_attribute, get_via_zip_http_gate, upload_via_http_gate, upload_via_http_gate_curl) @@ -128,7 +130,6 @@ class TestHttpGate: def test_expiration_epoch_in_http(self): cid = create_container(self.wallet, rule=self.PLACEMENT_RULE, basic_acl=PUBLIC_ACL) file_path = generate_file() - object_not_found_err = 'object not found' oids = [] curr_epoch = get_epoch() @@ -156,7 +157,7 @@ class TestHttpGate: self.try_to_get_object_and_expect_error( cid=cid, oid=oid, - expected_err=object_not_found_err + error_pattern=OBJECT_NOT_FOUND ) with allure.step('Other objects can be get'): @@ -220,12 +221,13 @@ class TestHttpGate: @staticmethod @allure.step('Try to get object and expect error') - def try_to_get_object_and_expect_error(cid: str, oid: str, expected_err: str): + def try_to_get_object_and_expect_error(cid: str, oid: str, error_pattern: str) -> None: try: get_via_http_gate(cid=cid, oid=oid) raise AssertionError(f'Expected error on getting object with cid: {cid}') except Exception as err: - assert expected_err in str(err), f'Expected error {expected_err} in {err}' + assert error_matches_status(err, error_pattern), \ + f'Expected {err} to match {error_pattern}' @staticmethod @allure.step('Verify object can be get using HTTP header attribute') diff --git a/robot/resources/lib/python_keywords/storage_policy.py b/robot/resources/lib/python_keywords/storage_policy.py index 70324c7..42fede3 100644 --- a/robot/resources/lib/python_keywords/storage_policy.py +++ b/robot/resources/lib/python_keywords/storage_policy.py @@ -2,7 +2,7 @@ """ This module contains keywords which are used for asserting - that storage policies are kept. + that storage policies are respected. """ from typing import Optional @@ -13,6 +13,8 @@ from robot.api.deco import keyword import complex_object_actions import neofs_verbs from common import NEOFS_NETMAP +from grpc_responses import OBJECT_NOT_FOUND, error_matches_status + ROBOT_AUTO_KEYWORDS = False @@ -142,7 +144,7 @@ def get_nodes_without_object(wallet: str, cid: str, oid: str): if res is None: nodes_list.append(node) except Exception as err: - if 'object not found' in str(err): + if error_matches_status(err, OBJECT_NOT_FOUND): nodes_list.append(node) else: raise Exception(f'Got error {err} on head object command') from err