Merge pull request #8 from spulec/master

Upstream merge
This commit is contained in:
Bert Blommers 2019-10-14 19:16:24 +01:00 committed by GitHub
commit e261ddb063
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 253 additions and 19 deletions

View File

@ -24,6 +24,16 @@ class UserNotFoundError(BadRequest):
}) })
class UsernameExistsException(BadRequest):
def __init__(self, message):
super(UsernameExistsException, self).__init__()
self.description = json.dumps({
"message": message,
'__type': 'UsernameExistsException',
})
class GroupExistsException(BadRequest): class GroupExistsException(BadRequest):
def __init__(self, message): def __init__(self, message):

View File

@ -14,7 +14,8 @@ from jose import jws
from moto.compat import OrderedDict from moto.compat import OrderedDict
from moto.core import BaseBackend, BaseModel from moto.core import BaseBackend, BaseModel
from .exceptions import GroupExistsException, NotAuthorizedError, ResourceNotFoundError, UserNotFoundError from .exceptions import GroupExistsException, NotAuthorizedError, ResourceNotFoundError, UserNotFoundError, \
UsernameExistsException
UserStatus = { UserStatus = {
"FORCE_CHANGE_PASSWORD": "FORCE_CHANGE_PASSWORD", "FORCE_CHANGE_PASSWORD": "FORCE_CHANGE_PASSWORD",
@ -561,6 +562,9 @@ class CognitoIdpBackend(BaseBackend):
if not user_pool: if not user_pool:
raise ResourceNotFoundError(user_pool_id) raise ResourceNotFoundError(user_pool_id)
if username in user_pool.users:
raise UsernameExistsException(username)
user = CognitoIdpUser(user_pool_id, username, temporary_password, UserStatus["FORCE_CHANGE_PASSWORD"], attributes) user = CognitoIdpUser(user_pool_id, username, temporary_password, UserStatus["FORCE_CHANGE_PASSWORD"], attributes)
user_pool.users[user.username] = user user_pool.users[user.username] = user
return user return user

View File

@ -2,6 +2,9 @@ class InvalidIndexNameError(ValueError):
pass pass
class InvalidUpdateExpression(ValueError):
pass
class ItemSizeTooLarge(Exception): class ItemSizeTooLarge(Exception):
message = 'Item size has exceeded the maximum allowed size' message = 'Item size has exceeded the maximum allowed size'
pass

View File

@ -16,7 +16,7 @@ from moto.core.exceptions import JsonRESTError
from .comparisons import get_comparison_func from .comparisons import get_comparison_func
from .comparisons import get_filter_expression from .comparisons import get_filter_expression
from .comparisons import get_expected from .comparisons import get_expected
from .exceptions import InvalidIndexNameError, ItemSizeTooLarge from .exceptions import InvalidIndexNameError, InvalidUpdateExpression, ItemSizeTooLarge
class DynamoJsonEncoder(json.JSONEncoder): class DynamoJsonEncoder(json.JSONEncoder):
@ -237,7 +237,16 @@ class Item(BaseModel):
if action == "REMOVE": if action == "REMOVE":
key = value key = value
if '.' not in key: if '.' not in key:
self.attrs.pop(value, None) list_index_update = re.match('(.+)\\[([0-9]+)\\]', key)
if list_index_update:
# We need to remove an item from a list (REMOVE listattr[0])
key_attr = self.attrs[list_index_update.group(1)]
list_index = int(list_index_update.group(2))
if key_attr.is_list():
if len(key_attr.value) > list_index:
del key_attr.value[list_index]
else:
self.attrs.pop(value, None)
else: else:
# Handle nested dict updates # Handle nested dict updates
key_parts = key.split('.') key_parts = key.split('.')
@ -247,6 +256,9 @@ class Item(BaseModel):
last_val = self.attrs[attr].value last_val = self.attrs[attr].value
for key_part in key_parts[:-1]: for key_part in key_parts[:-1]:
list_index_update = re.match('(.+)\\[([0-9]+)\\]', key_part)
if list_index_update:
key_part = list_index_update.group(1) # listattr[1] ==> listattr
# Hack but it'll do, traverses into a dict # Hack but it'll do, traverses into a dict
last_val_type = list(last_val.keys()) last_val_type = list(last_val.keys())
if last_val_type and last_val_type[0] == 'M': if last_val_type and last_val_type[0] == 'M':
@ -256,12 +268,23 @@ class Item(BaseModel):
last_val[key_part] = {'M': {}} last_val[key_part] = {'M': {}}
last_val = last_val[key_part] last_val = last_val[key_part]
if list_index_update:
last_val = last_val['L'][int(list_index_update.group(2))]
last_val_type = list(last_val.keys()) last_val_type = list(last_val.keys())
if last_val_type and last_val_type[0] == 'M': list_index_update = re.match('(.+)\\[([0-9]+)\\]', key_parts[-1])
last_val['M'].pop(key_parts[-1], None) if list_index_update:
# We need to remove an item from a list (REMOVE attr.listattr[0])
key_part = list_index_update.group(1) # listattr[1] ==> listattr
list_to_update = last_val[key_part]['L']
index_to_remove = int(list_index_update.group(2))
if index_to_remove < len(list_to_update):
del list_to_update[index_to_remove]
else: else:
last_val.pop(key_parts[-1], None) if last_val_type and last_val_type[0] == 'M':
last_val['M'].pop(key_parts[-1], None)
else:
last_val.pop(key_parts[-1], None)
elif action == 'SET': elif action == 'SET':
key, value = value.split("=", 1) key, value = value.split("=", 1)
key = key.strip() key = key.strip()
@ -282,38 +305,61 @@ class Item(BaseModel):
if type(value) != DynamoType: if type(value) != DynamoType:
if value in expression_attribute_values: if value in expression_attribute_values:
value = DynamoType(expression_attribute_values[value]) dyn_value = DynamoType(expression_attribute_values[value])
else: else:
value = DynamoType({"S": value}) dyn_value = DynamoType({"S": value})
else:
dyn_value = value
if '.' not in key: if '.' not in key:
self.attrs[key] = value list_index_update = re.match('(.+)\\[([0-9]+)\\]', key)
if list_index_update:
key_attr = self.attrs[list_index_update.group(1)]
list_index = int(list_index_update.group(2))
if key_attr.is_list():
if len(key_attr.value) > list_index:
key_attr.value[list_index] = expression_attribute_values[value]
else:
key_attr.value.append(expression_attribute_values[value])
else:
raise InvalidUpdateExpression
else:
self.attrs[key] = dyn_value
else: else:
# Handle nested dict updates # Handle nested dict updates
key_parts = key.split('.') key_parts = key.split('.')
attr = key_parts.pop(0) attr = key_parts.pop(0)
if attr not in self.attrs: if attr not in self.attrs:
raise ValueError raise ValueError
last_val = self.attrs[attr].value last_val = self.attrs[attr].value
for key_part in key_parts: for key_part in key_parts:
list_index_update = re.match('(.+)\\[([0-9]+)\\]', key_part)
if list_index_update:
key_part = list_index_update.group(1) # listattr[1] ==> listattr
# Hack but it'll do, traverses into a dict # Hack but it'll do, traverses into a dict
last_val_type = list(last_val.keys()) last_val_type = list(last_val.keys())
if last_val_type and last_val_type[0] == 'M': if last_val_type and last_val_type[0] == 'M':
last_val = last_val['M'] last_val = last_val['M']
if key_part not in last_val: if key_part not in last_val:
last_val[key_part] = {'M': {}} last_val[key_part] = {'M': {}}
last_val = last_val[key_part] last_val = last_val[key_part]
# We have reference to a nested object but we cant just assign to it
current_type = list(last_val.keys())[0] current_type = list(last_val.keys())[0]
if current_type == value.type: if list_index_update:
last_val[current_type] = value.value # We need to add an item to a list
list_index = int(list_index_update.group(2))
if len(last_val['L']) > list_index:
last_val['L'][list_index] = expression_attribute_values[value]
else:
last_val['L'].append(expression_attribute_values[value])
else: else:
last_val[value.type] = value.value # We have reference to a nested object but we cant just assign to it
del last_val[current_type] if current_type == dyn_value.type:
last_val[current_type] = dyn_value.value
else:
last_val[dyn_value.type] = dyn_value.value
del last_val[current_type]
elif action == 'ADD': elif action == 'ADD':
key, value = value.split(" ", 1) key, value = value.split(" ", 1)

View File

@ -6,7 +6,7 @@ import re
from moto.core.responses import BaseResponse from moto.core.responses import BaseResponse
from moto.core.utils import camelcase_to_underscores, amzn_request_id from moto.core.utils import camelcase_to_underscores, amzn_request_id
from .exceptions import InvalidIndexNameError, ItemSizeTooLarge from .exceptions import InvalidIndexNameError, InvalidUpdateExpression, ItemSizeTooLarge
from .models import dynamodb_backends, dynamo_json_dump from .models import dynamodb_backends, dynamo_json_dump
@ -683,6 +683,9 @@ class DynamoHandler(BaseResponse):
name, key, update_expression, attribute_updates, expression_attribute_names, name, key, update_expression, attribute_updates, expression_attribute_names,
expression_attribute_values, expected, condition_expression expression_attribute_values, expected, condition_expression
) )
except InvalidUpdateExpression:
er = 'com.amazonaws.dynamodb.v20111205#ValidationException'
return self.error(er, 'The document path provided in the update expression is invalid for update')
except ItemSizeTooLarge: except ItemSizeTooLarge:
er = 'com.amazonaws.dynamodb.v20111205#ValidationException' er = 'com.amazonaws.dynamodb.v20111205#ValidationException'
return self.error(er, ItemSizeTooLarge.message) return self.error(er, ItemSizeTooLarge.message)

View File

@ -886,6 +886,36 @@ def test_admin_create_user():
result["User"]["Enabled"].should.equal(True) result["User"]["Enabled"].should.equal(True)
@mock_cognitoidp
def test_admin_create_existing_user():
conn = boto3.client("cognito-idp", "us-west-2")
username = str(uuid.uuid4())
value = str(uuid.uuid4())
user_pool_id = conn.create_user_pool(PoolName=str(uuid.uuid4()))["UserPool"]["Id"]
conn.admin_create_user(
UserPoolId=user_pool_id,
Username=username,
UserAttributes=[
{"Name": "thing", "Value": value}
],
)
caught = False
try:
conn.admin_create_user(
UserPoolId=user_pool_id,
Username=username,
UserAttributes=[
{"Name": "thing", "Value": value}
],
)
except conn.exceptions.UsernameExistsException:
caught = True
caught.should.be.true
@mock_cognitoidp @mock_cognitoidp
def test_admin_get_user(): def test_admin_get_user():
conn = boto3.client("cognito-idp", "us-west-2") conn = boto3.client("cognito-idp", "us-west-2")

View File

@ -2506,6 +2506,144 @@ def test_index_with_unknown_attributes_should_fail():
ex.exception.response['Error']['Message'].should.contain(expected_exception) ex.exception.response['Error']['Message'].should.contain(expected_exception)
@mock_dynamodb2
def test_update_list_index__set_existing_index():
table_name = 'test_list_index_access'
client = create_table_with_list(table_name)
client.update_item(TableName=table_name, Key={'id': {'S': 'foo'}},
UpdateExpression='set itemlist[1]=:Item',
ExpressionAttributeValues={':Item': {'S': 'bar2_update'}})
#
result = client.get_item(TableName=table_name, Key={'id': {'S': 'foo'}})['Item']
assert result['id'] == {'S': 'foo'}
assert result['itemlist'] == {'L': [{'S': 'bar1'}, {'S': 'bar2_update'}, {'S': 'bar3'}]}
@mock_dynamodb2
def test_update_list_index__set_existing_nested_index():
table_name = 'test_list_index_access'
client = create_table_with_list(table_name)
client.put_item(TableName=table_name,
Item={'id': {'S': 'foo2'}, 'itemmap': {'M': {'itemlist': {'L': [{'S': 'bar1'}, {'S': 'bar2'}, {'S': 'bar3'}]}}}})
client.update_item(TableName=table_name, Key={'id': {'S': 'foo2'}},
UpdateExpression='set itemmap.itemlist[1]=:Item',
ExpressionAttributeValues={':Item': {'S': 'bar2_update'}})
#
result = client.get_item(TableName=table_name, Key={'id': {'S': 'foo2'}})['Item']
assert result['id'] == {'S': 'foo2'}
assert result['itemmap']['M']['itemlist']['L'] == [{'S': 'bar1'}, {'S': 'bar2_update'}, {'S': 'bar3'}]
@mock_dynamodb2
def test_update_list_index__set_index_out_of_range():
table_name = 'test_list_index_access'
client = create_table_with_list(table_name)
client.update_item(TableName=table_name, Key={'id': {'S': 'foo'}},
UpdateExpression='set itemlist[10]=:Item',
ExpressionAttributeValues={':Item': {'S': 'bar10'}})
#
result = client.get_item(TableName=table_name, Key={'id': {'S': 'foo'}})['Item']
assert result['id'] == {'S': 'foo'}
assert result['itemlist'] == {'L': [{'S': 'bar1'}, {'S': 'bar2'}, {'S': 'bar3'}, {'S': 'bar10'}]}
@mock_dynamodb2
def test_update_list_index__set_nested_index_out_of_range():
table_name = 'test_list_index_access'
client = create_table_with_list(table_name)
client.put_item(TableName=table_name,
Item={'id': {'S': 'foo2'}, 'itemmap': {'M': {'itemlist': {'L': [{'S': 'bar1'}, {'S': 'bar2'}, {'S': 'bar3'}]}}}})
client.update_item(TableName=table_name, Key={'id': {'S': 'foo2'}},
UpdateExpression='set itemmap.itemlist[10]=:Item',
ExpressionAttributeValues={':Item': {'S': 'bar10'}})
#
result = client.get_item(TableName=table_name, Key={'id': {'S': 'foo2'}})['Item']
assert result['id'] == {'S': 'foo2'}
assert result['itemmap']['M']['itemlist']['L'] == [{'S': 'bar1'}, {'S': 'bar2'}, {'S': 'bar3'}, {'S': 'bar10'}]
@mock_dynamodb2
def test_update_list_index__set_index_of_a_string():
table_name = 'test_list_index_access'
client = create_table_with_list(table_name)
client.put_item(TableName=table_name, Item={'id': {'S': 'foo2'}, 'itemstr': {'S': 'somestring'}})
with assert_raises(ClientError) as ex:
client.update_item(TableName=table_name, Key={'id': {'S': 'foo2'}},
UpdateExpression='set itemstr[1]=:Item',
ExpressionAttributeValues={':Item': {'S': 'string_update'}})
result = client.get_item(TableName=table_name, Key={'id': {'S': 'foo2'}})['Item']
ex.exception.response['Error']['Code'].should.equal('ValidationException')
ex.exception.response['Error']['Message'].should.equal(
'The document path provided in the update expression is invalid for update')
@mock_dynamodb2
def test_remove_list_index__remove_existing_index():
table_name = 'test_list_index_access'
client = create_table_with_list(table_name)
client.update_item(TableName=table_name, Key={'id': {'S': 'foo'}}, UpdateExpression='REMOVE itemlist[1]')
#
result = client.get_item(TableName=table_name, Key={'id': {'S': 'foo'}})['Item']
assert result['id'] == {'S': 'foo'}
assert result['itemlist'] == {'L': [{'S': 'bar1'}, {'S': 'bar3'}]}
@mock_dynamodb2
def test_remove_list_index__remove_existing_nested_index():
table_name = 'test_list_index_access'
client = create_table_with_list(table_name)
client.put_item(TableName=table_name,
Item={'id': {'S': 'foo2'}, 'itemmap': {'M': {'itemlist': {'L': [{'S': 'bar1'}, {'S': 'bar2'}]}}}})
client.update_item(TableName=table_name, Key={'id': {'S': 'foo2'}}, UpdateExpression='REMOVE itemmap.itemlist[1]')
#
result = client.get_item(TableName=table_name, Key={'id': {'S': 'foo2'}})['Item']
assert result['id'] == {'S': 'foo2'}
assert result['itemmap']['M']['itemlist']['L'] == [{'S': 'bar1'}]
@mock_dynamodb2
def test_remove_list_index__remove_existing_double_nested_index():
table_name = 'test_list_index_access'
client = create_table_with_list(table_name)
client.put_item(TableName=table_name,
Item={'id': {'S': 'foo2'},
'itemmap': {'M': {'itemlist': {'L': [{'M': {'foo00': {'S': 'bar1'},
'foo01': {'S': 'bar2'}}},
{'M': {'foo10': {'S': 'bar1'},
'foo11': {'S': 'bar2'}}}]}}}})
client.update_item(TableName=table_name, Key={'id': {'S': 'foo2'}},
UpdateExpression='REMOVE itemmap.itemlist[1].foo10')
#
result = client.get_item(TableName=table_name, Key={'id': {'S': 'foo2'}})['Item']
assert result['id'] == {'S': 'foo2'}
assert result['itemmap']['M']['itemlist']['L'][0]['M'].should.equal({'foo00': {'S': 'bar1'},
'foo01': {'S': 'bar2'}}) # untouched
assert result['itemmap']['M']['itemlist']['L'][1]['M'].should.equal({'foo11': {'S': 'bar2'}}) # changed
@mock_dynamodb2
def test_remove_list_index__remove_index_out_of_range():
table_name = 'test_list_index_access'
client = create_table_with_list(table_name)
client.update_item(TableName=table_name, Key={'id': {'S': 'foo'}}, UpdateExpression='REMOVE itemlist[10]')
#
result = client.get_item(TableName=table_name, Key={'id': {'S': 'foo'}})['Item']
assert result['id'] == {'S': 'foo'}
assert result['itemlist'] == {'L': [{'S': 'bar1'}, {'S': 'bar2'}, {'S': 'bar3'}]}
def create_table_with_list(table_name):
client = boto3.client('dynamodb', region_name='us-east-1')
client.create_table(TableName=table_name,
KeySchema=[{'AttributeName': 'id', 'KeyType': 'HASH'}],
AttributeDefinitions=[{'AttributeName': 'id', 'AttributeType': 'S'}],
BillingMode='PAY_PER_REQUEST')
client.put_item(TableName=table_name,
Item={'id': {'S': 'foo'}, 'itemlist': {'L': [{'S': 'bar1'}, {'S': 'bar2'}, {'S': 'bar3'}]}})
return client
@mock_dynamodb2 @mock_dynamodb2
def test_sorted_query_with_numerical_sort_key(): def test_sorted_query_with_numerical_sort_key():
dynamodb = boto3.resource('dynamodb', region_name='us-east-1') dynamodb = boto3.resource('dynamodb', region_name='us-east-1')