forked from TrueCloudLab/frostfs-testcases
Add asserts for error status codes in grpc responses
Signed-off-by: Vladimir Domnich <v.domnich@yadro.com>
This commit is contained in:
parent
b6b1644fd6
commit
a76614b40d
7 changed files with 62 additions and 31 deletions
21
pytest_tests/helpers/grpc_responses.py
Normal file
21
pytest_tests/helpers/grpc_responses.py
Normal file
|
@ -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
|
|
@ -6,6 +6,7 @@ import pytest
|
||||||
|
|
||||||
import wallet
|
import wallet
|
||||||
from common import ASSETS_DIR
|
from common import ASSETS_DIR
|
||||||
|
from grpc_responses import OBJECT_ACCESS_DENIED
|
||||||
from python_keywords.acl import set_eacl
|
from python_keywords.acl import set_eacl
|
||||||
from python_keywords.container import create_container
|
from python_keywords.container import create_container
|
||||||
from python_keywords.neofs_verbs import (delete_object, get_object, get_range,
|
from python_keywords.neofs_verbs import (delete_object, get_object, get_range,
|
||||||
|
@ -77,26 +78,25 @@ class TestACL:
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def check_no_access_to_container(wallet: str, cid: str, oid: str, file_name: str):
|
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=OBJECT_ACCESS_DENIED):
|
||||||
with pytest.raises(Exception, match=err_pattern):
|
|
||||||
get_object(wallet, cid, oid)
|
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)
|
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)
|
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)
|
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')
|
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')
|
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)
|
search_object(wallet, cid)
|
||||||
|
|
||||||
def check_full_access(self, cid: str, file_name: str, wallet_to_check: Tuple = None) -> str:
|
def check_full_access(self, cid: str, file_name: str, wallet_to_check: Tuple = None) -> str:
|
||||||
|
|
|
@ -5,6 +5,7 @@ import allure
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from epoch import tick_epoch
|
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,
|
from python_keywords.container import (create_container, delete_container, get_container,
|
||||||
list_containers)
|
list_containers)
|
||||||
from utility import placement_policy_from_container
|
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)
|
json_wallet = json.load(file)
|
||||||
|
|
||||||
placement_rule = 'REP 2 IN X CBF 1 SELECT 2 FROM * AS X'
|
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)
|
cid = create_container(wallet, rule=placement_rule, options=options)
|
||||||
|
|
||||||
containers = list_containers(wallet)
|
containers = list_containers(wallet)
|
||||||
assert cid in containers, f'Expected container {cid} in containers: {containers}'
|
assert cid in containers, f'Expected container {cid} in containers: {containers}'
|
||||||
|
|
||||||
container_info = get_container(wallet, cid, flag='')
|
container_info: str = get_container(wallet, cid, flag='')
|
||||||
container_info = container_info.lower() # To ignore case when comparing with expected values
|
container_info = container_info.casefold() # To ignore case when comparing with expected values
|
||||||
|
|
||||||
info_to_check = {
|
info_to_check = {
|
||||||
f'basic ACL: {PRIVATE_ACL_F} (private)',
|
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}')
|
info_to_check.add(f'Name={name}')
|
||||||
|
|
||||||
with allure.step('Check container has correct information'):
|
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)
|
actual_policy = placement_policy_from_container(container_info)
|
||||||
assert actual_policy == expected_policy, \
|
assert actual_policy == expected_policy, \
|
||||||
f'Expected policy\n{expected_policy} but got policy\n{actual_policy}'
|
f'Expected policy\n{expected_policy} but got policy\n{actual_policy}'
|
||||||
|
|
||||||
for info in info_to_check:
|
for info in info_to_check:
|
||||||
expected_info = info.lower()
|
expected_info = info.casefold()
|
||||||
assert expected_info in container_info, \
|
assert expected_info in container_info, \
|
||||||
f'Expected {expected_info} in container info:\n{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)
|
sleep(sleep_interval)
|
||||||
continue
|
continue
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
if 'container not found' not in str(err):
|
if error_matches_status(err, CONTAINER_NOT_FOUND):
|
||||||
raise AssertionError(f'Expected "container not found" in error, got\n{err}')
|
return
|
||||||
return
|
raise AssertionError(f'Expected "{CONTAINER_NOT_FOUND}" error, got\n{err}')
|
||||||
|
|
||||||
raise AssertionError(f'Container was not deleted within {attempts * sleep_interval} sec')
|
raise AssertionError(f'Container was not deleted within {attempts * sleep_interval} sec')
|
||||||
|
|
|
@ -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,
|
from common import (COMPLEX_OBJ_SIZE, MAINNET_BLOCK_TIME, NEOFS_CONTRACT_CACHE_TIMEOUT,
|
||||||
NEOFS_NETMAP_DICT, STORAGE_RPC_ENDPOINT_1, STORAGE_WALLET_PASS)
|
NEOFS_NETMAP_DICT, STORAGE_RPC_ENDPOINT_1, STORAGE_WALLET_PASS)
|
||||||
from epoch import tick_epoch
|
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.container import create_container, get_container
|
||||||
from python_keywords.failover_utils import wait_object_replication_on_nodes
|
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
|
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)
|
checker(wallet, cid, oid)
|
||||||
wait_for_gc_pass_on_storage_nodes()
|
wait_for_gc_pass_on_storage_nodes()
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
if 'object not found' in str(err):
|
if error_matches_status(err, OBJECT_NOT_FOUND):
|
||||||
break
|
return
|
||||||
else:
|
raise AssertionError(f'Expected "{OBJECT_NOT_FOUND}" error, got\n{err}')
|
||||||
raise AssertionError(f'Object {oid} is not dropped from node')
|
|
||||||
|
raise AssertionError(f'Object {oid} was not dropped from node')
|
||||||
|
|
|
@ -3,9 +3,11 @@ from time import sleep
|
||||||
|
|
||||||
import allure
|
import allure
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from common import SIMPLE_OBJ_SIZE, COMPLEX_OBJ_SIZE
|
from common import SIMPLE_OBJ_SIZE, COMPLEX_OBJ_SIZE
|
||||||
from container import create_container
|
from container import create_container
|
||||||
from epoch import get_epoch, tick_epoch
|
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,
|
from python_keywords.neofs_verbs import (delete_object, get_object, get_range,
|
||||||
get_range_hash, head_object,
|
get_range_hash, head_object,
|
||||||
put_object, search_object)
|
put_object, search_object)
|
||||||
|
@ -93,8 +95,8 @@ def test_object_api(prepare_wallet_and_deposit, request, object_size):
|
||||||
sleep(CLEANUP_TIMEOUT)
|
sleep(CLEANUP_TIMEOUT)
|
||||||
|
|
||||||
with allure.step('Get objects and check errors'):
|
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[0], error_pattern=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[1], error_pattern=OBJECT_ALREADY_REMOVED)
|
||||||
|
|
||||||
|
|
||||||
@allure.title('Test object life time')
|
@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()
|
wait_for_gc_pass_on_storage_nodes()
|
||||||
|
|
||||||
with allure.step('Check object deleted because it expires-on epoch'):
|
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)
|
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:
|
try:
|
||||||
get_object(wallet=wallet, cid=cid, oid=oid)
|
get_object(wallet=wallet, cid=cid, oid=oid)
|
||||||
raise AssertionError(f'Expected object {oid} removed, but it is not')
|
raise AssertionError(f'Expected object {oid} removed, but it is not')
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
logger.info(f'Error is {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):
|
def check_header_is_presented(head_info: dict, object_header: dict):
|
||||||
|
|
|
@ -5,9 +5,11 @@ from time import sleep
|
||||||
|
|
||||||
import allure
|
import allure
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from common import COMPLEX_OBJ_SIZE
|
from common import COMPLEX_OBJ_SIZE
|
||||||
from container import create_container
|
from container import create_container
|
||||||
from epoch import get_epoch, tick_epoch
|
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,
|
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,
|
get_via_http_gate_by_attribute, get_via_zip_http_gate,
|
||||||
upload_via_http_gate, upload_via_http_gate_curl)
|
upload_via_http_gate, upload_via_http_gate_curl)
|
||||||
|
@ -128,7 +130,6 @@ class TestHttpGate:
|
||||||
def test_expiration_epoch_in_http(self):
|
def test_expiration_epoch_in_http(self):
|
||||||
cid = create_container(self.wallet, rule=self.PLACEMENT_RULE, basic_acl=PUBLIC_ACL)
|
cid = create_container(self.wallet, rule=self.PLACEMENT_RULE, basic_acl=PUBLIC_ACL)
|
||||||
file_path = generate_file()
|
file_path = generate_file()
|
||||||
object_not_found_err = 'object not found'
|
|
||||||
oids = []
|
oids = []
|
||||||
|
|
||||||
curr_epoch = get_epoch()
|
curr_epoch = get_epoch()
|
||||||
|
@ -156,7 +157,7 @@ class TestHttpGate:
|
||||||
self.try_to_get_object_and_expect_error(
|
self.try_to_get_object_and_expect_error(
|
||||||
cid=cid,
|
cid=cid,
|
||||||
oid=oid,
|
oid=oid,
|
||||||
expected_err=object_not_found_err
|
error_pattern=OBJECT_NOT_FOUND
|
||||||
)
|
)
|
||||||
|
|
||||||
with allure.step('Other objects can be get'):
|
with allure.step('Other objects can be get'):
|
||||||
|
@ -220,12 +221,13 @@ class TestHttpGate:
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@allure.step('Try to get object and expect error')
|
@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:
|
try:
|
||||||
get_via_http_gate(cid=cid, oid=oid)
|
get_via_http_gate(cid=cid, oid=oid)
|
||||||
raise AssertionError(f'Expected error on getting object with cid: {cid}')
|
raise AssertionError(f'Expected error on getting object with cid: {cid}')
|
||||||
except Exception as err:
|
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
|
@staticmethod
|
||||||
@allure.step('Verify object can be get using HTTP header attribute')
|
@allure.step('Verify object can be get using HTTP header attribute')
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
"""
|
"""
|
||||||
This module contains keywords which are used for asserting
|
This module contains keywords which are used for asserting
|
||||||
that storage policies are kept.
|
that storage policies are respected.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
@ -13,6 +13,8 @@ from robot.api.deco import keyword
|
||||||
import complex_object_actions
|
import complex_object_actions
|
||||||
import neofs_verbs
|
import neofs_verbs
|
||||||
from common import NEOFS_NETMAP
|
from common import NEOFS_NETMAP
|
||||||
|
from grpc_responses import OBJECT_NOT_FOUND, error_matches_status
|
||||||
|
|
||||||
|
|
||||||
ROBOT_AUTO_KEYWORDS = False
|
ROBOT_AUTO_KEYWORDS = False
|
||||||
|
|
||||||
|
@ -142,7 +144,7 @@ def get_nodes_without_object(wallet: str, cid: str, oid: str):
|
||||||
if res is None:
|
if res is None:
|
||||||
nodes_list.append(node)
|
nodes_list.append(node)
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
if 'object not found' in str(err):
|
if error_matches_status(err, OBJECT_NOT_FOUND):
|
||||||
nodes_list.append(node)
|
nodes_list.append(node)
|
||||||
else:
|
else:
|
||||||
raise Exception(f'Got error {err} on head object command') from err
|
raise Exception(f'Got error {err} on head object command') from err
|
||||||
|
|
Loading…
Reference in a new issue