Update of resource_definitions content implemented
Now it is able to add, update, delete keys in resource definition content. Change-Id: I9624f97bf35eae15d6a7ddc2d5d42768292db4f0
This commit is contained in:
parent
819fb9125a
commit
b2a878e536
@ -29,6 +29,7 @@ from tuning_box.middleware import keystone
|
|||||||
api_errors = {
|
api_errors = {
|
||||||
'IntegrityError': {'status': 409}, # sqlalchemy IntegrityError
|
'IntegrityError': {'status': 409}, # sqlalchemy IntegrityError
|
||||||
'TuningboxIntegrityError': {'status': 409},
|
'TuningboxIntegrityError': {'status': 409},
|
||||||
|
'KeysOperationError': {'status': 409},
|
||||||
'TuningboxNotFound': {'status': 404}
|
'TuningboxNotFound': {'status': 404}
|
||||||
}
|
}
|
||||||
api = flask_restful.Api(errors=api_errors)
|
api = flask_restful.Api(errors=api_errors)
|
||||||
@ -46,6 +47,11 @@ api.add_resource(
|
|||||||
resource_definitions.ResourceDefinition,
|
resource_definitions.ResourceDefinition,
|
||||||
'/resource_definition/<int:resource_definition_id>'
|
'/resource_definition/<int:resource_definition_id>'
|
||||||
)
|
)
|
||||||
|
api.add_resource(
|
||||||
|
resource_definitions.ResourceDefinitionKeys,
|
||||||
|
'/resource_definition/<int:resource_definition_id>/'
|
||||||
|
'keys/<keys_operation:operation>'
|
||||||
|
)
|
||||||
|
|
||||||
# Resource values
|
# Resource values
|
||||||
api.add_resource(
|
api.add_resource(
|
||||||
@ -81,6 +87,12 @@ def handle_object_not_found(exc):
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
def handle_keys_operation_error(exc):
|
||||||
|
response = flask.jsonify(msg=exc.args[0])
|
||||||
|
response.status_code = 409
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
def build_app(configure_logging=True, with_keystone=True):
|
def build_app(configure_logging=True, with_keystone=True):
|
||||||
app = flask.Flask(__name__)
|
app = flask.Flask(__name__)
|
||||||
app.url_map.converters.update(converters.ALL)
|
app.url_map.converters.update(converters.ALL)
|
||||||
@ -94,6 +106,8 @@ def build_app(configure_logging=True, with_keystone=True):
|
|||||||
handle_integrity_error)
|
handle_integrity_error)
|
||||||
app.register_error_handler(errors.TuningboxNotFound,
|
app.register_error_handler(errors.TuningboxNotFound,
|
||||||
handle_object_not_found)
|
handle_object_not_found)
|
||||||
|
app.register_error_handler(errors.KeysOperationError,
|
||||||
|
handle_keys_operation_error)
|
||||||
db.db.init_app(app)
|
db.db.init_app(app)
|
||||||
if configure_logging:
|
if configure_logging:
|
||||||
log_level = app.config.get('LOG_LEVEL', 'INFO')
|
log_level = app.config.get('LOG_LEVEL', 'INFO')
|
||||||
|
@ -15,6 +15,8 @@ import itertools
|
|||||||
from werkzeug import routing
|
from werkzeug import routing
|
||||||
from werkzeug import urls
|
from werkzeug import urls
|
||||||
|
|
||||||
|
from tuning_box.library import resource_keys_operation
|
||||||
|
|
||||||
|
|
||||||
class Levels(routing.BaseConverter):
|
class Levels(routing.BaseConverter):
|
||||||
"""Converter that maps nested levels to list of tuples.
|
"""Converter that maps nested levels to list of tuples.
|
||||||
@ -56,7 +58,20 @@ class IdOrName(routing.BaseConverter):
|
|||||||
def to_url(self, value):
|
def to_url(self, value):
|
||||||
return super(IdOrName, self).to_url(str(value))
|
return super(IdOrName, self).to_url(str(value))
|
||||||
|
|
||||||
|
|
||||||
|
class KeysOperation(routing.BaseConverter):
|
||||||
|
"""Converter that matches keys operations
|
||||||
|
|
||||||
|
Allowed operations: add, delete, erase
|
||||||
|
"""
|
||||||
|
|
||||||
|
regex = '(' + ')|('.join(
|
||||||
|
resource_keys_operation.KeysOperationMixin.OPERATIONS
|
||||||
|
) + ')'
|
||||||
|
|
||||||
|
|
||||||
ALL = {
|
ALL = {
|
||||||
'levels': Levels,
|
'levels': Levels,
|
||||||
'id_or_name': IdOrName,
|
'id_or_name': IdOrName,
|
||||||
|
'keys_operation': KeysOperation
|
||||||
}
|
}
|
||||||
|
@ -21,3 +21,23 @@ class TuningboxIntegrityError(BaseTuningboxError):
|
|||||||
|
|
||||||
class TuningboxNotFound(BaseTuningboxError):
|
class TuningboxNotFound(BaseTuningboxError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class KeysOperationError(BaseTuningboxError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class UnknownKeysOperation(KeysOperationError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class KeysPathNotExisted(KeysOperationError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class KeysPathInvalid(KeysOperationError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class KeysPathUnreachable(KeysOperationError):
|
||||||
|
pass
|
||||||
|
@ -10,12 +10,12 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
|
||||||
import flask
|
import flask
|
||||||
import flask_restful
|
import flask_restful
|
||||||
from flask_restful import fields
|
from flask_restful import fields
|
||||||
|
|
||||||
from tuning_box import db
|
from tuning_box import db
|
||||||
|
from tuning_box.library import resource_keys_operation
|
||||||
|
|
||||||
resource_definition_fields = {
|
resource_definition_fields = {
|
||||||
'id': fields.Integer,
|
'id': fields.Integer,
|
||||||
@ -87,3 +87,22 @@ class ResourceDefinition(flask_restful.Resource):
|
|||||||
resource_definition_id)
|
resource_definition_id)
|
||||||
db.db.session.delete(res_definition)
|
db.db.session.delete(res_definition)
|
||||||
return None, 204
|
return None, 204
|
||||||
|
|
||||||
|
|
||||||
|
class ResourceDefinitionKeys(flask_restful.Resource,
|
||||||
|
resource_keys_operation.KeysOperationMixin):
|
||||||
|
|
||||||
|
@db.with_transaction
|
||||||
|
def _do_update(self, resource_definition_id, operation):
|
||||||
|
res_definition = db.ResourceDefinition.query.get_or_404(
|
||||||
|
resource_definition_id)
|
||||||
|
result = self.perform_operation(operation, res_definition.content,
|
||||||
|
flask.request.json)
|
||||||
|
res_definition.content = result
|
||||||
|
|
||||||
|
def put(self, resource_definition_id, operation):
|
||||||
|
return self.patch(resource_definition_id, operation)
|
||||||
|
|
||||||
|
def patch(self, resource_definition_id, operation):
|
||||||
|
self._do_update(resource_definition_id, operation)
|
||||||
|
return None, 204
|
||||||
|
118
tuning_box/library/resource_keys_operation.py
Normal file
118
tuning_box/library/resource_keys_operation.py
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
import copy
|
||||||
|
|
||||||
|
from tuning_box import errors
|
||||||
|
|
||||||
|
|
||||||
|
class KeysOperationMixin(object):
|
||||||
|
|
||||||
|
OPERATION_SET = 'set'
|
||||||
|
OPERATION_DELETE = 'delete'
|
||||||
|
|
||||||
|
OPERATIONS = (OPERATION_SET, OPERATION_DELETE)
|
||||||
|
|
||||||
|
def _check_out_of_index(self, cur_point, key, keys_path):
|
||||||
|
if isinstance(cur_point, (list, tuple)) and key >= len(cur_point):
|
||||||
|
raise errors.KeysPathNotExisted(
|
||||||
|
"Keys path doesn't exist {0}. "
|
||||||
|
"Failed on the key {1}".format(keys_path, key)
|
||||||
|
)
|
||||||
|
|
||||||
|
def _check_key_existed(self, cur_point, key, keys_path):
|
||||||
|
if isinstance(cur_point, dict) and key not in cur_point:
|
||||||
|
raise errors.KeysPathNotExisted(
|
||||||
|
"Keys path doesn't exist {0}. "
|
||||||
|
"Failed on the key {1}".format(keys_path, key)
|
||||||
|
)
|
||||||
|
|
||||||
|
def _check_path_is_reachable(self, cur_point, key, keys_path):
|
||||||
|
if not isinstance(cur_point, (list, tuple, dict)):
|
||||||
|
raise errors.KeysPathUnreachable(
|
||||||
|
"Leaf value {0} found on key {1} "
|
||||||
|
"in keys path {2}".format(cur_point, key, keys_path)
|
||||||
|
)
|
||||||
|
|
||||||
|
def do_set(self, storage, keys_paths):
|
||||||
|
"""Sets values from keys paths to storage.
|
||||||
|
|
||||||
|
Keys path is list of keys paths. If we have keys_paths
|
||||||
|
[['a', 'b', 'val']], then storage['a']['b'] will be set to 'val'.
|
||||||
|
Last value in the keys path is value to be set.
|
||||||
|
|
||||||
|
:param storage: original data
|
||||||
|
:param keys_paths: lists of keys paths to be set
|
||||||
|
:returns: result of merging keys_paths and storage
|
||||||
|
"""
|
||||||
|
|
||||||
|
storage_copy = copy.deepcopy(storage)
|
||||||
|
for keys_path in keys_paths:
|
||||||
|
cur_point = storage_copy
|
||||||
|
if len(keys_path) < 2:
|
||||||
|
raise errors.KeysPathInvalid(
|
||||||
|
"Keys path {0} invalid. Keys path should contain "
|
||||||
|
"at least one key and value".format(keys_path)
|
||||||
|
)
|
||||||
|
|
||||||
|
for key in keys_path[:-2]:
|
||||||
|
self._check_path_is_reachable(cur_point, key, keys_path)
|
||||||
|
self._check_out_of_index(cur_point, key, keys_path)
|
||||||
|
self._check_key_existed(cur_point, key, keys_path)
|
||||||
|
cur_point = cur_point[key]
|
||||||
|
|
||||||
|
assign_to = keys_path[-2]
|
||||||
|
self._check_out_of_index(cur_point, assign_to, keys_path)
|
||||||
|
cur_point[assign_to] = keys_path[-1]
|
||||||
|
|
||||||
|
return storage_copy
|
||||||
|
|
||||||
|
def do_delete(self, storage, keys_paths):
|
||||||
|
"""Deletes keys paths from storage.
|
||||||
|
|
||||||
|
Keys path is list of keys paths. If we have keys_paths
|
||||||
|
[['a', 'b']], then storage['a']['b'] will be removed.
|
||||||
|
|
||||||
|
:param storage: data
|
||||||
|
:param keys_paths: lists of keys paths to be deleted
|
||||||
|
:returns: result of keys_paths deletion from storage
|
||||||
|
"""
|
||||||
|
|
||||||
|
storage_copy = copy.deepcopy(storage)
|
||||||
|
for keys_path in keys_paths:
|
||||||
|
cur_point = storage_copy
|
||||||
|
if not keys_path:
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
for key in keys_path[:-1]:
|
||||||
|
cur_point = cur_point[key]
|
||||||
|
key = keys_path[-1]
|
||||||
|
self._check_path_is_reachable(cur_point, key, keys_path)
|
||||||
|
del cur_point[key]
|
||||||
|
except (KeyError, IndexError):
|
||||||
|
raise errors.KeysPathNotExisted(
|
||||||
|
"Keys path doesn't exist {0}. "
|
||||||
|
"Failed on the key {1}".format(keys_path, key)
|
||||||
|
)
|
||||||
|
return storage_copy
|
||||||
|
|
||||||
|
def perform_operation(self, operation, storage, keys_paths):
|
||||||
|
if operation == self.OPERATION_SET:
|
||||||
|
return self.do_set(storage, keys_paths)
|
||||||
|
elif operation == self.OPERATION_DELETE:
|
||||||
|
return self.do_delete(storage, keys_paths)
|
||||||
|
else:
|
||||||
|
raise errors.UnknownKeysOperation(
|
||||||
|
"Unknown operation: {0}. "
|
||||||
|
"Allowed operations: {1}".format(operation, self.OPERATIONS)
|
||||||
|
)
|
@ -19,6 +19,7 @@ class TestResourceDefinitions(BaseTest):
|
|||||||
|
|
||||||
collection_url = '/resource_definitions'
|
collection_url = '/resource_definitions'
|
||||||
object_url = '/resource_definition/{0}'
|
object_url = '/resource_definition/{0}'
|
||||||
|
object_keys_url = object_url + '/keys/{1}'
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _resource_json(self):
|
def _resource_json(self):
|
||||||
@ -150,3 +151,63 @@ class TestResourceDefinitions(BaseTest):
|
|||||||
self.assertEqual(204, res.status_code)
|
self.assertEqual(204, res.status_code)
|
||||||
actual_res_def = self.client.get(self.object_url.format(res_id)).json
|
actual_res_def = self.client.get(self.object_url.format(res_id)).json
|
||||||
self.assertEqual(self._resource_json, actual_res_def)
|
self.assertEqual(self._resource_json, actual_res_def)
|
||||||
|
|
||||||
|
def test_put_resource_definition_set_operation_error(self):
|
||||||
|
self.app.config["PROPAGATE_EXCEPTIONS"] = True
|
||||||
|
self._fixture()
|
||||||
|
res_id = self._resource_json['id']
|
||||||
|
|
||||||
|
data = [['a', 'b', 'c', 'value']]
|
||||||
|
res = self.client.put(self.object_keys_url.format(res_id, 'set'),
|
||||||
|
data=data)
|
||||||
|
self.assertEqual(409, res.status_code)
|
||||||
|
|
||||||
|
def test_put_resource_definition_set(self):
|
||||||
|
self._fixture()
|
||||||
|
res_id = self._resource_json['id']
|
||||||
|
|
||||||
|
data = [['key', 'key_value'], ['key_x', 'key_x_value']]
|
||||||
|
res = self.client.put(
|
||||||
|
self.object_keys_url.format(res_id, 'set'),
|
||||||
|
data=data
|
||||||
|
)
|
||||||
|
self.assertEqual(204, res.status_code)
|
||||||
|
|
||||||
|
res = self.client.get(self.object_url.format(res_id))
|
||||||
|
self.assertEqual(200, res.status_code)
|
||||||
|
actual = res.json
|
||||||
|
self.assertEqual({'key': 'key_value', 'key_x': 'key_x_value'},
|
||||||
|
actual['content'])
|
||||||
|
|
||||||
|
def test_put_resource_definition_delete(self):
|
||||||
|
self._fixture()
|
||||||
|
res_id = self._resource_json['id']
|
||||||
|
|
||||||
|
data = [['key']]
|
||||||
|
res = self.client.put(
|
||||||
|
self.object_keys_url.format(res_id, 'delete'),
|
||||||
|
data=data
|
||||||
|
)
|
||||||
|
self.assertEqual(204, res.status_code)
|
||||||
|
|
||||||
|
res = self.client.get(self.object_url.format(res_id))
|
||||||
|
self.assertEqual(200, res.status_code)
|
||||||
|
actual = res.json
|
||||||
|
self.assertEqual({}, actual['content'])
|
||||||
|
|
||||||
|
def test_put_resource_definition_delete_no_key(self):
|
||||||
|
self.app.config["PROPAGATE_EXCEPTIONS"] = True
|
||||||
|
self._fixture()
|
||||||
|
res_id = self._resource_json['id']
|
||||||
|
|
||||||
|
data = [['fake_key']]
|
||||||
|
res = self.client.put(
|
||||||
|
self.object_keys_url.format(res_id, 'delete'),
|
||||||
|
data=data
|
||||||
|
)
|
||||||
|
self.assertEqual(409, res.status_code)
|
||||||
|
|
||||||
|
res = self.client.get(self.object_url.format(res_id))
|
||||||
|
self.assertEqual(200, res.status_code)
|
||||||
|
actual = res.json
|
||||||
|
self.assertEqual(self._resource_json['content'], actual['content'])
|
||||||
|
132
tuning_box/tests/library/test_resource_key_operations.py
Normal file
132
tuning_box/tests/library/test_resource_key_operations.py
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
from tuning_box import errors
|
||||||
|
from tuning_box.library import resource_keys_operation
|
||||||
|
from tuning_box.tests.test_app import BaseTest
|
||||||
|
|
||||||
|
|
||||||
|
class TestResourceDefinitions(BaseTest):
|
||||||
|
|
||||||
|
processor = resource_keys_operation.KeysOperationMixin()
|
||||||
|
|
||||||
|
def test_unknown_operation(self):
|
||||||
|
self.assertRaises(errors.UnknownKeysOperation,
|
||||||
|
self.processor.perform_operation,
|
||||||
|
'fake_operation', {}, [])
|
||||||
|
|
||||||
|
def test_set_new(self):
|
||||||
|
keys = [['a', {}]]
|
||||||
|
data = {}
|
||||||
|
result = self.processor.do_set(data, keys)
|
||||||
|
self.assertEqual({'a': {}}, result)
|
||||||
|
|
||||||
|
keys = [['a', {}], ['a', 'b', []]]
|
||||||
|
data = {}
|
||||||
|
result = self.processor.do_set(data, keys)
|
||||||
|
self.assertEqual({'a': {'b': []}}, result)
|
||||||
|
|
||||||
|
keys = [['a', 0, 'b', 'c_updated']]
|
||||||
|
data = {'a': [{'b': 'c'}]}
|
||||||
|
result = self.processor.do_set(data, keys)
|
||||||
|
self.assertEqual({'a': [{'b': 'c_updated'}]}, result)
|
||||||
|
|
||||||
|
keys = [['a', 'b']]
|
||||||
|
data = {'a': {'b': 'c'}}
|
||||||
|
result = self.processor.do_set(data, keys)
|
||||||
|
self.assertEqual({'a': 'b'}, result)
|
||||||
|
|
||||||
|
def test_set_empty(self):
|
||||||
|
keys = [['a', 'b', '']]
|
||||||
|
data = {'a': {'b': 'value'}}
|
||||||
|
result = self.processor.do_set(data, keys)
|
||||||
|
self.assertEqual({'a': {'b': ''}}, result)
|
||||||
|
|
||||||
|
def test_set_not_modifies_storage(self):
|
||||||
|
keys = [['a', 'c', 'value_c']]
|
||||||
|
data = {'a': {'b': 'value_b'}}
|
||||||
|
result = self.processor.do_set(data, keys)
|
||||||
|
self.assertEqual({'a': {'b': 'value_b'}}, data)
|
||||||
|
self.assertEqual({'a': {'c': 'value_c', 'b': 'value_b'}}, result)
|
||||||
|
|
||||||
|
def test_set_invalid_keys_path(self):
|
||||||
|
self.assertRaises(errors.KeysPathInvalid, self.processor.do_set,
|
||||||
|
{}, [[]])
|
||||||
|
self.assertRaises(errors.KeysPathInvalid, self.processor.do_set,
|
||||||
|
{}, [['a']])
|
||||||
|
|
||||||
|
def test_set_key_path_not_existed(self):
|
||||||
|
keys = [['a', 'b', 'c']]
|
||||||
|
data = {}
|
||||||
|
self.assertRaises(errors.KeysPathNotExisted, self.processor.do_set,
|
||||||
|
data, keys)
|
||||||
|
|
||||||
|
keys = [['a', 1, 'b']]
|
||||||
|
data = {'a': [{'b': 'c'}]}
|
||||||
|
self.assertRaises(errors.KeysPathNotExisted, self.processor.do_set,
|
||||||
|
data, keys)
|
||||||
|
|
||||||
|
def test_set_key_path_unreachable(self):
|
||||||
|
keys = [['a', 'b', 'c', 'd', 'e']]
|
||||||
|
data = {'a': {'b': 'c'}}
|
||||||
|
self.assertRaises(errors.KeysPathUnreachable, self.processor.do_set,
|
||||||
|
data, keys)
|
||||||
|
|
||||||
|
def test_delete_key_path_not_existed(self):
|
||||||
|
keys = [['a', 'b']]
|
||||||
|
data = {}
|
||||||
|
self.assertRaises(errors.KeysPathNotExisted, self.processor.do_delete,
|
||||||
|
data, keys)
|
||||||
|
|
||||||
|
keys = [[1]]
|
||||||
|
data = ['a']
|
||||||
|
self.assertRaises(errors.KeysPathNotExisted, self.processor.do_delete,
|
||||||
|
data, keys)
|
||||||
|
|
||||||
|
def test_delete_key_path_unreachable(self):
|
||||||
|
keys = [['a', 'b', 'value_b']]
|
||||||
|
data = {'a': {'b': 'value_b'}}
|
||||||
|
self.assertRaises(errors.KeysPathUnreachable, self.processor.do_delete,
|
||||||
|
data, keys)
|
||||||
|
|
||||||
|
keys = [['a', 'b', 'value_c']]
|
||||||
|
data = {'a': {'b': 'value_b'}}
|
||||||
|
self.assertRaises(errors.KeysPathUnreachable, self.processor.do_delete,
|
||||||
|
data, keys)
|
||||||
|
|
||||||
|
def test_delete(self):
|
||||||
|
keys = [['a']]
|
||||||
|
data = {'a': 'val_a', 'b': {'a': 'val_b_a'}}
|
||||||
|
result = self.processor.do_delete(data, keys)
|
||||||
|
self.assertEqual({'b': {'a': 'val_b_a'}}, result)
|
||||||
|
|
||||||
|
keys = [[0]]
|
||||||
|
data = ['a']
|
||||||
|
result = self.processor.do_delete(data, keys)
|
||||||
|
self.assertEqual([], result)
|
||||||
|
|
||||||
|
keys = [['a', 0, 'b']]
|
||||||
|
data = {'a': [{'b': 'val_a_0_b', 'c': 'val_a_0_c'}, 'd']}
|
||||||
|
result = self.processor.do_delete(data, keys)
|
||||||
|
self.assertEqual({'a': [{'c': 'val_a_0_c'}, 'd']}, result)
|
||||||
|
|
||||||
|
keys = [['a', 'b'], ['a']]
|
||||||
|
data = {'a': {'b': 'val_a_b', 'c': 'val_a_c'}, 'b': 'val_b'}
|
||||||
|
result = self.processor.do_delete(data, keys)
|
||||||
|
self.assertEqual({'b': 'val_b'}, result)
|
||||||
|
|
||||||
|
def test_delete_not_modifies_storage(self):
|
||||||
|
keys = [['a', 'b']]
|
||||||
|
data = {'a': {'b': 'value_b'}}
|
||||||
|
result = self.processor.do_delete(data, keys)
|
||||||
|
self.assertEqual({'a': {'b': 'value_b'}}, data)
|
||||||
|
self.assertEqual({'a': {}}, result)
|
Loading…
x
Reference in New Issue
Block a user