mirror of
https://github.com/viq/NewsBlur.git
synced 2025-08-05 16:49:45 +00:00
1191 lines
38 KiB
Python
1191 lines
38 KiB
Python
import unittest
|
|
|
|
import json
|
|
|
|
import mock
|
|
|
|
from dynamodb_mapper.model import (ConnectionBorg, DynamoDBModel,
|
|
autoincrement_int, MaxRetriesExceededError, MAX_RETRIES,
|
|
ConflictError, _python_to_dynamodb, _dynamodb_to_python, utc_tz,
|
|
_get_default_value, SchemaError, MAGIC_KEY, OverwriteError, InvalidRegionError)
|
|
from boto.exception import DynamoDBResponseError
|
|
from boto.dynamodb.exceptions import DynamoDBConditionalCheckFailedError
|
|
import datetime
|
|
|
|
class mocked_region(object):
|
|
def __init__(self, name):
|
|
self.name = name
|
|
|
|
|
|
# Hash-only primary key
|
|
class DoomEpisode(DynamoDBModel):
|
|
__table__ = "doom_episode"
|
|
__hash_key__ = "id"
|
|
__schema__ = {
|
|
"id": int,
|
|
"name": unicode,
|
|
}
|
|
|
|
|
|
# Composite primary key
|
|
class DoomMap(DynamoDBModel):
|
|
__table__ = "doom_map"
|
|
__hash_key__ = "episode_id"
|
|
__range_key__ = "name"
|
|
__schema__ = {
|
|
"episode_id": int,
|
|
"name": unicode,
|
|
"world": unicode
|
|
}
|
|
|
|
|
|
# autoincrement_int primary key
|
|
class LogEntry(DynamoDBModel):
|
|
__table__ = "log_entry"
|
|
__hash_key__ = "id"
|
|
__schema__ = {
|
|
"id": autoincrement_int,
|
|
"text": unicode,
|
|
}
|
|
|
|
|
|
# set attribute
|
|
class DoomCampaign(DynamoDBModel):
|
|
__table__ = "doom_campaign"
|
|
__hash_key__ = "id"
|
|
__schema__ = {
|
|
"id": int,
|
|
"name": unicode,
|
|
"cheats": set,
|
|
}
|
|
|
|
|
|
# boolean regression test
|
|
class DoomCampaignStatus(DynamoDBModel):
|
|
__table__ = "doom_campaign_status"
|
|
__hash_key__ = "id"
|
|
__schema__ = {
|
|
"id": int,
|
|
"name": unicode,
|
|
"completed": bool,
|
|
}
|
|
|
|
# list attribute
|
|
class DoomMonster(DynamoDBModel):
|
|
__table__ = "doom_monster"
|
|
__hash_key__ = "id"
|
|
__schema__ = {
|
|
"id": int,
|
|
"attacks": list,
|
|
}
|
|
|
|
|
|
# dict attribute
|
|
class DoomMonsterMap(DynamoDBModel):
|
|
__table__ = "doom_monster_map"
|
|
__hash_key__ = "map_id"
|
|
__schema__ = {
|
|
"map_id": int,
|
|
"monsters": dict,
|
|
}
|
|
|
|
|
|
# datetime.datetime hash key
|
|
class Patch(DynamoDBModel):
|
|
__table__ = "patch"
|
|
__hash_key__ = "datetime"
|
|
__schema__ = {
|
|
"datetime": datetime.datetime,
|
|
"description": unicode,
|
|
}
|
|
|
|
|
|
# Composite key with list hash_key, datetime.datetime range_key
|
|
class GameReport(DynamoDBModel):
|
|
__table__ = "game_report"
|
|
__hash_key__ = "player_ids"
|
|
__range_key__ = "datetime"
|
|
__schema__ = {
|
|
"player_ids": list,
|
|
"datetime": datetime.datetime,
|
|
}
|
|
|
|
|
|
# Field with a default value
|
|
class PlayerStrength(DynamoDBModel):
|
|
__table__ = "player_strength"
|
|
__hash_key__ = "player_id"
|
|
__schema__ = {
|
|
"player_id": int,
|
|
"strength": unicode,
|
|
}
|
|
__defaults__ = {
|
|
"strength": u'weak'
|
|
}
|
|
|
|
|
|
class NoTableName(DynamoDBModel):
|
|
__hash_key__ = "id"
|
|
__schema__ = {
|
|
"id": int,
|
|
}
|
|
|
|
|
|
class NoHashKey(DynamoDBModel):
|
|
__table__ = "error"
|
|
__schema__ = {
|
|
"id": int,
|
|
}
|
|
|
|
|
|
class NoSchema(DynamoDBModel):
|
|
__table__ = "error"
|
|
__hash_key__ = "id"
|
|
|
|
|
|
class IncompatibleKeys(DynamoDBModel):
|
|
__table__ = "error"
|
|
__hash_key__ = "id"
|
|
__range_key__ = "date"
|
|
__schema__ = {
|
|
"id": autoincrement_int,
|
|
"date": datetime.datetime,
|
|
}
|
|
|
|
|
|
# Json export tester
|
|
class ToJsonTest(DynamoDBModel):
|
|
__table__ = "to_json_test"
|
|
__hash_key__ = "id"
|
|
__schema__ = {
|
|
"id": int,
|
|
"set": set,
|
|
"date": datetime.datetime,
|
|
}
|
|
|
|
|
|
def return_42():
|
|
return 42
|
|
|
|
|
|
class TestConnectionBorg(unittest.TestCase):
|
|
def setUp(self):
|
|
ConnectionBorg()._connections.clear()
|
|
ConnectionBorg._shared_state = {
|
|
"_aws_access_key_id": None,
|
|
"_aws_secret_access_key": None,
|
|
"_region": None,
|
|
"_connections": {},
|
|
}
|
|
|
|
def tearDown(self):
|
|
ConnectionBorg()._connections.clear()
|
|
ConnectionBorg._shared_state = {
|
|
"_aws_access_key_id": None,
|
|
"_aws_secret_access_key": None,
|
|
"_region": None,
|
|
"_connections": {},
|
|
}
|
|
|
|
def test_borgness(self):
|
|
aws_access_key_id = "foo"
|
|
aws_secret_access_key = "bar"
|
|
|
|
borg1 = ConnectionBorg()
|
|
borg2 = ConnectionBorg()
|
|
|
|
borg1.set_credentials(aws_access_key_id, aws_secret_access_key)
|
|
|
|
self.assertEquals(borg1._aws_access_key_id, aws_access_key_id)
|
|
self.assertEquals(borg1._aws_secret_access_key, aws_secret_access_key)
|
|
|
|
self.assertEquals(borg2._aws_access_key_id, aws_access_key_id)
|
|
self.assertEquals(borg2._aws_secret_access_key, aws_secret_access_key)
|
|
|
|
@mock.patch("dynamodb_mapper.model.boto")
|
|
def test_set_region_valid(self, m_boto):
|
|
# Make sure internal state is set and shared
|
|
m_regions = [
|
|
mocked_region('us-east-1'),
|
|
mocked_region('eu-west-1'),
|
|
]
|
|
|
|
m_boto.dynamodb.regions.return_value = m_regions
|
|
|
|
borg1 = ConnectionBorg()
|
|
borg2 = ConnectionBorg()
|
|
|
|
self.assertIs(None, borg1._region)
|
|
self.assertIs(None, borg2._region)
|
|
|
|
borg1.set_region("eu-west-1")
|
|
|
|
self.assertIs(m_regions[1], borg1._region)
|
|
self.assertIs(m_regions[1], borg2._region)
|
|
|
|
@mock.patch("dynamodb_mapper.model.boto")
|
|
def test_set_region_invalid(self, m_boto):
|
|
# Make sure internal state is set and shared
|
|
m_regions = [
|
|
mocked_region('us-east-1'),
|
|
mocked_region('eu-west-1'),
|
|
]
|
|
|
|
m_boto.dynamodb.regions.return_value = m_regions
|
|
|
|
borg1 = ConnectionBorg()
|
|
borg2 = ConnectionBorg()
|
|
|
|
self.assertIs(None, borg1._region)
|
|
self.assertIs(None, borg2._region)
|
|
|
|
self.assertRaises(
|
|
InvalidRegionError,
|
|
borg1.set_region,
|
|
"moon-hidden-1",
|
|
)
|
|
|
|
self.assertIs(None, borg1._region)
|
|
self.assertIs(None, borg2._region)
|
|
|
|
|
|
@mock.patch("dynamodb_mapper.model.boto")
|
|
def test_set_credentials(self, m_boto):
|
|
# Make sure internal state is set and shared
|
|
m_get_table = m_boto.connect_dynamodb.return_value.get_table
|
|
|
|
aws_access_key_id = "foo"
|
|
aws_secret_access_key = "bar"
|
|
table_name = "foo"
|
|
|
|
borg1 = ConnectionBorg()
|
|
borg2 = ConnectionBorg()
|
|
|
|
borg1.set_credentials(aws_access_key_id, aws_secret_access_key)
|
|
|
|
self.assertIs(borg1._aws_access_key_id ,aws_access_key_id)
|
|
self.assertIs(borg2._aws_access_key_id ,aws_access_key_id)
|
|
|
|
self.assertIs(borg1._aws_secret_access_key ,aws_secret_access_key)
|
|
self.assertIs(borg2._aws_secret_access_key ,aws_secret_access_key)
|
|
|
|
@mock.patch("dynamodb_mapper.model.boto")
|
|
def test_get_table_default(self, m_boto):
|
|
m_get_table = m_boto.connect_dynamodb.return_value.get_table
|
|
|
|
aws_access_key_id = "foo"
|
|
aws_secret_access_key = "bar"
|
|
table_name = "foo"
|
|
|
|
borg1 = ConnectionBorg()
|
|
|
|
borg1._aws_access_key_id = aws_access_key_id
|
|
borg1._aws_secret_access_key = aws_secret_access_key
|
|
|
|
ConnectionBorg().get_table(table_name)
|
|
m_get_table.assert_called_with(table_name)
|
|
|
|
m_boto.connect_dynamodb.assert_called_with(
|
|
aws_access_key_id=aws_access_key_id,
|
|
aws_secret_access_key=aws_secret_access_key,
|
|
region=None,
|
|
)
|
|
|
|
@mock.patch("dynamodb_mapper.model.boto")
|
|
def test_get_table_eu_region(self, m_boto):
|
|
m_get_table = m_boto.connect_dynamodb.return_value.get_table
|
|
m_region = mocked_region('eu-west-1')
|
|
|
|
aws_access_key_id = "foo"
|
|
aws_secret_access_key = "bar"
|
|
table_name = "foo"
|
|
|
|
borg1 = ConnectionBorg()
|
|
|
|
borg1._region = m_region
|
|
borg1._aws_access_key_id = aws_access_key_id
|
|
borg1._aws_secret_access_key = aws_secret_access_key
|
|
|
|
ConnectionBorg().get_table(table_name)
|
|
m_get_table.assert_called_with(table_name)
|
|
|
|
m_boto.connect_dynamodb.assert_called_with(
|
|
aws_access_key_id=aws_access_key_id,
|
|
aws_secret_access_key=aws_secret_access_key,
|
|
region=m_region,
|
|
)
|
|
|
|
@mock.patch("dynamodb_mapper.model.boto")
|
|
def test_new_batch_list_nominal(self, m_boto):
|
|
m_new_batch_list = m_boto.connect_dynamodb.return_value.new_batch_list
|
|
ConnectionBorg().new_batch_list()
|
|
|
|
m_new_batch_list.assert_called_once()
|
|
|
|
|
|
def test_create_table_no_name(self):
|
|
self.assertRaises(
|
|
SchemaError,
|
|
ConnectionBorg().create_table,
|
|
NoTableName, 10, 5, False
|
|
)
|
|
|
|
def test_create_table_no_hash(self):
|
|
self.assertRaises(
|
|
SchemaError,
|
|
ConnectionBorg().create_table,
|
|
NoHashKey, 10, 5, False
|
|
)
|
|
|
|
def test_create_table_no_schema(self):
|
|
self.assertRaises(
|
|
SchemaError,
|
|
ConnectionBorg().create_table,
|
|
NoSchema, 10, 5, False
|
|
)
|
|
|
|
def test_create_table_autoinc_with_range(self):
|
|
self.assertRaises(
|
|
SchemaError,
|
|
ConnectionBorg().create_table,
|
|
IncompatibleKeys, 10, 5, False
|
|
)
|
|
|
|
@mock.patch("dynamodb_mapper.model.boto")
|
|
def test_create_table_hash_key(self, m_boto):
|
|
m_connection = m_boto.connect_dynamodb.return_value
|
|
m_create_schema = m_connection.create_schema
|
|
m_create_table = m_connection.create_table
|
|
|
|
table = ConnectionBorg().create_table(DoomEpisode, 10, 5, False)
|
|
|
|
m_create_schema.assert_called_once_with(
|
|
hash_key_name=DoomEpisode.__hash_key__,
|
|
hash_key_proto_value=0,
|
|
range_key_name=None,
|
|
range_key_proto_value=None
|
|
)
|
|
|
|
m_create_table.assert_called_once_with(
|
|
DoomEpisode.__table__, m_create_schema.return_value, 10, 5)
|
|
|
|
table.refresh.assert_called_once_with(wait_for_active=False)
|
|
|
|
@mock.patch("dynamodb_mapper.model.boto")
|
|
def test_create_table_hash_key_autoinc(self, m_boto):
|
|
# create a table with autoinc index. Wait should automatically be set to True
|
|
m_connection = m_boto.connect_dynamodb.return_value
|
|
m_create_schema = m_connection.create_schema
|
|
m_create_table = m_connection.create_table
|
|
|
|
table = ConnectionBorg().create_table(LogEntry, 10, 5, False)
|
|
|
|
m_create_schema.assert_called_once_with(
|
|
hash_key_name=LogEntry.__hash_key__,
|
|
hash_key_proto_value=0,
|
|
range_key_name=None,
|
|
range_key_proto_value=None
|
|
)
|
|
|
|
m_create_table.assert_called_once_with(
|
|
LogEntry.__table__, m_create_schema.return_value, 10, 5)
|
|
|
|
table.refresh.assert_called_once_with(wait_for_active=True)
|
|
|
|
@mock.patch("dynamodb_mapper.model.boto")
|
|
def test_create_table_key_date_dict(self, m_boto):
|
|
# check that datetime/list may successfully be used as key
|
|
m_connection = m_boto.connect_dynamodb.return_value
|
|
m_create_schema = m_connection.create_schema
|
|
m_create_table = m_connection.create_table
|
|
|
|
table = ConnectionBorg().create_table(GameReport, 10, 5, False)
|
|
|
|
m_create_schema.assert_called_once_with(
|
|
hash_key_name=GameReport.__hash_key__,
|
|
hash_key_proto_value=u"",
|
|
range_key_name=GameReport.__range_key__,
|
|
range_key_proto_value=u""
|
|
)
|
|
|
|
m_create_table.assert_called_once_with(
|
|
GameReport.__table__, m_create_schema.return_value, 10, 5)
|
|
|
|
table.refresh.assert_called_once_with(wait_for_active=False)
|
|
|
|
@mock.patch("dynamodb_mapper.model.boto")
|
|
def test_create_table_composite_key(self, m_boto):
|
|
m_connection = m_boto.connect_dynamodb.return_value
|
|
m_create_schema = m_connection.create_schema
|
|
m_create_table = m_connection.create_table
|
|
|
|
table = ConnectionBorg().create_table(DoomMap, 10, 5, True)
|
|
|
|
m_create_schema.assert_called_once_with(
|
|
hash_key_name=DoomMap.__hash_key__,
|
|
hash_key_proto_value=0,
|
|
range_key_name=DoomMap.__range_key__,
|
|
range_key_proto_value=""
|
|
)
|
|
|
|
m_create_table.assert_called_once_with(
|
|
DoomMap.__table__, m_create_schema.return_value, 10, 5)
|
|
|
|
table.refresh.assert_called_once_with(wait_for_active=True)
|
|
|
|
|
|
class TestTypeConversions(unittest.TestCase):
|
|
def test_python_to_dynamodb_number(self):
|
|
self.assertEquals(0, _python_to_dynamodb(0))
|
|
self.assertEquals(0.0, _python_to_dynamodb(0.0))
|
|
self.assertEquals(10, _python_to_dynamodb(10))
|
|
self.assertEquals(10.0, _python_to_dynamodb(10.0))
|
|
self.assertEquals(0, _python_to_dynamodb(False))
|
|
self.assertEquals(1, _python_to_dynamodb(True))
|
|
|
|
def test_dynamodb_to_python_number(self):
|
|
self.assertEquals(0, _dynamodb_to_python(int, 0))
|
|
self.assertEquals(0.0, _dynamodb_to_python(float, 0.0))
|
|
self.assertEquals(10, _dynamodb_to_python(int, 10))
|
|
self.assertEquals(10.0, _dynamodb_to_python(float, 10.0))
|
|
self.assertEquals(False, _dynamodb_to_python(bool, 0))
|
|
self.assertEquals(True, _dynamodb_to_python(bool, 1))
|
|
|
|
def test_python_to_dynamodb_unicode(self):
|
|
self.assertEquals(u"hello", _python_to_dynamodb(u"hello"))
|
|
# Empty strings become missing attributes
|
|
self.assertIsNone(_python_to_dynamodb(u""))
|
|
|
|
def test_dynamodb_to_python_unicode(self):
|
|
self.assertEquals(u"hello", _dynamodb_to_python(unicode, u"hello"))
|
|
# Missing attributes become None
|
|
self.assertIsNone(_dynamodb_to_python(unicode, None))
|
|
|
|
def test_python_to_dynamodb_set(self):
|
|
self.assertEquals(set([1, 2, 3]), _python_to_dynamodb(set([1, 2, 3])))
|
|
self.assertEquals(
|
|
set(["foo", "bar", "baz"]),
|
|
_python_to_dynamodb(set(["foo", "bar", "baz"])))
|
|
# Empty sets become missing attributes
|
|
self.assertIsNone(_python_to_dynamodb(set()))
|
|
|
|
def test_dynamodb_to_python_set(self):
|
|
self.assertEquals(set([1, 2, 3]), _dynamodb_to_python(set, set([1, 2, 3])))
|
|
self.assertEquals(
|
|
set(["foo", "bar", "baz"]),
|
|
_dynamodb_to_python(set, set(["foo", "bar", "baz"])))
|
|
# Empty sets become None
|
|
self.assertIsNone(_dynamodb_to_python(set, None))
|
|
|
|
def test_python_to_dynamodb_list(self):
|
|
attacks = [
|
|
{'name': 'fireball', 'damage': 10},
|
|
{'name': 'punch', 'damage': 5}
|
|
]
|
|
|
|
self.assertEquals(
|
|
json.dumps(attacks, sort_keys=True),
|
|
_python_to_dynamodb(attacks))
|
|
self.assertEquals("[]", _python_to_dynamodb([]))
|
|
|
|
def test_dynamodb_to_python_list(self):
|
|
attacks = [
|
|
{'name': 'fireball', 'damage': 10},
|
|
{'name': 'punch', 'damage': 5}
|
|
]
|
|
|
|
self.assertEquals(
|
|
attacks,
|
|
_dynamodb_to_python(list, json.dumps(attacks, sort_keys=True)))
|
|
self.assertEquals([], _dynamodb_to_python(list, "[]"))
|
|
|
|
def test_python_to_dynamodb_dict(self):
|
|
monsters = {
|
|
"cacodemon": [
|
|
{"x": 10, "y": 20},
|
|
{"x": 10, "y": 30}
|
|
],
|
|
"imp": [],
|
|
"cyberdemon": []
|
|
}
|
|
|
|
self.assertEquals(
|
|
json.dumps(monsters, sort_keys=True),
|
|
_python_to_dynamodb(monsters))
|
|
self.assertEquals("{}", _python_to_dynamodb({}))
|
|
|
|
def test_dynamodb_to_python_dict(self):
|
|
monsters = {
|
|
"cacodemon": [
|
|
{"x": 10, "y": 20},
|
|
{"x": 10, "y": 30}
|
|
],
|
|
"imp": [],
|
|
"cyberdemon": []
|
|
}
|
|
|
|
self.assertEquals(
|
|
monsters,
|
|
_dynamodb_to_python(dict, json.dumps(monsters, sort_keys=True)))
|
|
self.assertEquals({}, _dynamodb_to_python(dict, "{}"))
|
|
|
|
def test_dynamodb_to_python_datetime(self):
|
|
self.assertEquals(
|
|
datetime.datetime(2012, 05, 31, 12, 0, 0, tzinfo=utc_tz),
|
|
_dynamodb_to_python(datetime.datetime,
|
|
"2012-05-31T12:00:00.000000+00:00"))
|
|
self.assertEquals(
|
|
datetime.datetime(2010, 11, 1, 4, 0, 0, tzinfo=utc_tz),
|
|
_dynamodb_to_python(datetime.datetime,
|
|
"2010-11-01T04:00:00.000000Z"))
|
|
|
|
def test_dynamodb_to_python_default(self):
|
|
self.assertIsNone(_dynamodb_to_python(datetime.datetime, None))
|
|
|
|
def test_dynamodb_to_python_datetime_notz(self):
|
|
# Timezone info is mandatory
|
|
self.assertRaises(
|
|
ValueError,
|
|
_dynamodb_to_python, datetime.datetime, "2012-05-31T12:00:00.000000")
|
|
|
|
def test_python_to_dynamodb_datetime(self):
|
|
self.assertEquals(
|
|
"2012-05-31T12:00:00.000000+00:00",
|
|
_python_to_dynamodb(datetime.datetime(2012, 05, 31, 12, 0, 0, tzinfo=utc_tz)))
|
|
|
|
def test_python_to_dynamodb_datetime_notz(self):
|
|
self.assertRaises(
|
|
ValueError,
|
|
_python_to_dynamodb, datetime.datetime(2012, 05, 31, 12, 0, 0))
|
|
|
|
|
|
class TestGetDefaultValue(unittest.TestCase):
|
|
@mock.patch("dynamodb_mapper.model.datetime")
|
|
def test_datetime(self, m_datetime):
|
|
# Plain default date should be now
|
|
m_datetime.now.return_value = now = datetime.datetime.now(tz=utc_tz)
|
|
self.assertEquals(
|
|
now,
|
|
_get_default_value(m_datetime))
|
|
self.assertEquals(utc_tz.tzname(now), "UTC")
|
|
|
|
# default date with supplied fallback
|
|
fallback = datetime.datetime(2012, 05, 31, 12, 0, 0, tzinfo=utc_tz)
|
|
self.assertEquals(
|
|
fallback,
|
|
_get_default_value(datetime.datetime, fallback))
|
|
|
|
def test_int(self):
|
|
# Plain default int
|
|
self.assertEquals(0, _get_default_value(int))
|
|
# Default int with fallback
|
|
self.assertEquals(42, _get_default_value(int, 42))
|
|
# Default int with callable fallback
|
|
self.assertEquals(42, _get_default_value(int, return_42))
|
|
|
|
def test_incompatible_types_crash_constant(self):
|
|
# Default unicode with int fallback should fail
|
|
self.assertRaises(
|
|
TypeError,
|
|
_get_default_value,
|
|
unicode, 42
|
|
)
|
|
|
|
def test_incompatible_types_crash_callable(self):
|
|
# Default unicode with callable int fallback should fail
|
|
self.assertRaises(
|
|
TypeError,
|
|
_get_default_value,
|
|
unicode, return_42
|
|
)
|
|
|
|
|
|
class TestDynamoDBModel(unittest.TestCase):
|
|
def setUp(self):
|
|
ConnectionBorg()._connections.clear()
|
|
|
|
def tearDown(self):
|
|
ConnectionBorg()._connections.clear()
|
|
|
|
def test_to_json_dict(self):
|
|
testdate = datetime.datetime(2012, 5, 31, 12, 0, 0, tzinfo=utc_tz)
|
|
testSet = set(["level 52","level 1"])
|
|
|
|
d = ToJsonTest()
|
|
d.id = 42
|
|
d.set = testSet
|
|
d.date = testdate
|
|
|
|
d_dict = d.to_json_dict()
|
|
self.assertEquals(d_dict["id"], 42)
|
|
self.assertEquals(d_dict["set"], sorted(testSet))
|
|
self.assertEquals(d_dict["date"], testdate.astimezone(utc_tz).isoformat())
|
|
|
|
def test_build_default_values(self):
|
|
d = DoomEpisode()
|
|
self.assertEquals(d.id, 0)
|
|
self.assertEquals(d.name, u"")
|
|
|
|
# Using to_json_dict because _to_db_dict omits empty values
|
|
d_dict = d.to_json_dict()
|
|
self.assertEquals(d_dict["id"], d.id)
|
|
self.assertEquals(d_dict["name"], d.name)
|
|
|
|
@mock.patch("dynamodb_mapper.model._get_default_value")
|
|
def test_build_default_values_with_defaults(self, m_defaulter):
|
|
m_defaulter.return_value = u"weak"
|
|
d = PlayerStrength()
|
|
|
|
m_defaulter.assert_called_with(unicode, u"weak")
|
|
self.assertEquals(d.strength, u"weak")
|
|
|
|
def test_init_from_args(self):
|
|
d = DoomEpisode(id=1, name=u"Knee-deep in the Dead")
|
|
self.assertEquals({}, d._raw_data)
|
|
self.assertEquals(1, d.id)
|
|
self.assertEquals(u"Knee-deep in the Dead", d.name)
|
|
|
|
def test_build_from_dict(self):
|
|
#FIXME: can it be removed as this feature is implicitly tested elsewhere ?
|
|
d_dict = {"id": 1, "name": "Knee-deep in the Dead"}
|
|
d = DoomEpisode._from_db_dict(d_dict)
|
|
self.assertEquals(d_dict, d._raw_data)
|
|
self.assertEquals(d_dict["id"], d.id)
|
|
self.assertEquals(d_dict["name"], d.name)
|
|
|
|
def test_build_from_dict_missing_attrs(self):
|
|
#FIXME: can it be removed as this feature is implicitly tested elsewhere ?
|
|
d = DoomCampaign._from_db_dict({})
|
|
self.assertEquals(d.id, 0)
|
|
self.assertEquals(d.name, u"")
|
|
self.assertEquals(d.cheats, set())
|
|
|
|
def test_build_from_dict_json_list(self):
|
|
#FIXME: can it be removed as this feature is implicitly tested elsewhere ?
|
|
attacks = [
|
|
{'name': 'fireball', 'damage': 10},
|
|
{'name': 'punch', 'damage': 5}
|
|
]
|
|
|
|
m = DoomMonster._from_db_dict({
|
|
"id": 1,
|
|
"attacks": json.dumps(attacks, sort_keys=True)
|
|
})
|
|
|
|
self.assertEquals(m.attacks, attacks)
|
|
|
|
# Test default values
|
|
m2 = DoomMonster._from_db_dict({"id": 1})
|
|
self.assertEquals(m2.attacks, [])
|
|
|
|
def test_build_from_dict_json_dict(self):
|
|
#FIXME: can it be removed as this feature is implicitly tested elsewhere ?
|
|
monsters = {
|
|
"cacodemon": [
|
|
{"x": 10, "y": 20},
|
|
{"x": 10, "y": 30}
|
|
],
|
|
"imp": [],
|
|
"cyberdemon": []
|
|
}
|
|
|
|
e = DoomMonsterMap._from_db_dict({
|
|
"map_id": 1,
|
|
"monsters": json.dumps(monsters, sort_keys=True)
|
|
})
|
|
self.assertEquals(e.monsters, monsters)
|
|
|
|
e2 = DoomMonsterMap._from_db_dict({"map_id": 1})
|
|
self.assertEquals(e2.monsters, {})
|
|
|
|
def test_build_from_db_dict(self):
|
|
#FIXME: can it be removed as this feature is implicitly tested elsewhere ?
|
|
d_dict = {"id": 1, "name": "Knee-deep in the Dead"}
|
|
d = DoomEpisode._from_db_dict(d_dict)
|
|
self.assertEquals(d_dict, d._raw_data)
|
|
|
|
def test_build_from_db_dict_json_dict(self):
|
|
#FIXME: can it be removed as this feature is implicitly tested elsewhere ?
|
|
monsters = {
|
|
"cacodemon": [
|
|
{"x": 10, "y": 20},
|
|
{"x": 10, "y": 30}
|
|
],
|
|
"imp": [],
|
|
"cyberdemon": []
|
|
}
|
|
|
|
raw_dict = {
|
|
"map_id": 1,
|
|
"monsters": json.dumps(monsters, sort_keys=True)
|
|
}
|
|
|
|
e = DoomMonsterMap._from_db_dict(raw_dict)
|
|
self.assertEquals(e.monsters, monsters)
|
|
self.assertEquals(raw_dict["map_id"], e._raw_data["map_id"])
|
|
self.assertEquals(raw_dict["monsters"], e._raw_data["monsters"])
|
|
|
|
def test_to_db_dict_json_list(self):
|
|
#FIXME: can it be removed as this feature is implicitly tested elsewhere ?
|
|
attacks = [
|
|
{'name': 'fireball', 'damage': 10},
|
|
{'name': 'punch', 'damage': 5}
|
|
]
|
|
|
|
m = DoomMonster()
|
|
m.id = 1
|
|
m.attacks = attacks
|
|
|
|
d = m._to_db_dict()
|
|
|
|
self.assertEquals(d["id"], 1)
|
|
self.assertEquals(d["attacks"], json.dumps(attacks, sort_keys=True))
|
|
|
|
def test_to_db_dict_json_dict(self):
|
|
#FIXME: can it be removed as this feature is implicitly tested elsewhere ?
|
|
monsters = {
|
|
"cacodemon": [
|
|
{"x": 10, "y": 20},
|
|
{"x": 10, "y": 30}
|
|
],
|
|
"imp": [],
|
|
"cyberdemon": []
|
|
}
|
|
|
|
e = DoomMonsterMap()
|
|
e.map_id = 1
|
|
e.monsters = monsters
|
|
|
|
d = e._to_db_dict()
|
|
|
|
self.assertEquals(d["map_id"], 1)
|
|
self.assertEquals(d["monsters"], json.dumps(monsters, sort_keys=True))
|
|
|
|
|
|
@mock.patch("dynamodb_mapper.model.Item")
|
|
@mock.patch("dynamodb_mapper.model.boto")
|
|
def test_save_simple_types(self, m_boto, m_item):
|
|
DoomEpisode().save()
|
|
|
|
m_item.return_value.put.assert_called_once()
|
|
|
|
@mock.patch("dynamodb_mapper.model.Item")
|
|
@mock.patch("dynamodb_mapper.model.boto")
|
|
def test_save_sets(self, m_boto, m_item):
|
|
l = DoomCampaign()
|
|
l.id = 1
|
|
l.name = "Knee-deep in the Dead"
|
|
l.cheats = set(["iddqd", "idkfa", "idclip"])
|
|
l.save()
|
|
|
|
m_item.assert_called_once_with(
|
|
mock.ANY, attrs={"id": l.id, "name": l.name, "cheats": l.cheats})
|
|
|
|
@mock.patch("dynamodb_mapper.model.Item")
|
|
@mock.patch("dynamodb_mapper.model.boto")
|
|
def test_save_empty_sets(self, m_boto, m_item):
|
|
l = DoomCampaign()
|
|
l.id = 1
|
|
l.cheats = set()
|
|
l.save()
|
|
|
|
m_item.assert_called_once_with(mock.ANY, attrs={"id": 1})
|
|
|
|
@mock.patch("dynamodb_mapper.model.Item")
|
|
@mock.patch("dynamodb_mapper.model.boto")
|
|
def test_save_empty_strings(self, m_boto, m_item):
|
|
e = DoomEpisode()
|
|
e.id = 0
|
|
e.save()
|
|
|
|
m_item.assert_called_once_with(mock.ANY, attrs={"id": 0})
|
|
|
|
@mock.patch("dynamodb_mapper.model.ConnectionBorg.get_table")
|
|
def test_get_by_hash_key(self, m_get_table):
|
|
m_table = m_get_table.return_value
|
|
|
|
DoomEpisode.get(1)
|
|
m_table.get_item.assert_called_once_with(hash_key=1, range_key=None, consistent_read=False)
|
|
|
|
@mock.patch("dynamodb_mapper.model.ConnectionBorg.get_table")
|
|
def test_get_by_composite_key(self, m_get_table):
|
|
m_table = m_get_table.return_value
|
|
|
|
DoomMap.get(1, "Knee-deep in the dead")
|
|
m_table.get_item.assert_called_once_with(
|
|
hash_key=1, range_key="Knee-deep in the dead", consistent_read=False)
|
|
|
|
@mock.patch("dynamodb_mapper.model.ConnectionBorg.get_table")
|
|
def test_get_by_hash_key_magic_types(self, m_get_table):
|
|
m_table = m_get_table.return_value
|
|
d = datetime.datetime(2012, 5, 31, 12, 0, 0, tzinfo=utc_tz)
|
|
d_text = "2012-05-31T12:00:00.000000+00:00"
|
|
m_table.get_item.return_value = {"datetime": d_text}
|
|
|
|
p = Patch.get(d)
|
|
self.assertEquals(d_text, p._raw_data["datetime"])
|
|
m_table.get_item.assert_called_once_with(
|
|
hash_key=d_text, range_key=None, consistent_read=False)
|
|
|
|
@mock.patch("dynamodb_mapper.model.ConnectionBorg.get_table")
|
|
def test_get_by_composite_key_magic_types(self, m_get_table):
|
|
m_table = m_get_table.return_value
|
|
d = datetime.datetime(2012, 5, 31, 12, 0, 0, tzinfo=utc_tz)
|
|
d_text = "2012-05-31T12:00:00.000000+00:00"
|
|
players = ["duke", "doomguy", "blackowicz"]
|
|
players_text = json.dumps(players)
|
|
m_table.get_item.return_value = {
|
|
"player_ids": players_text,
|
|
"datetime": d_text
|
|
}
|
|
|
|
r = GameReport.get(players, d)
|
|
self.assertEquals(d_text, r._raw_data["datetime"])
|
|
self.assertEquals(players_text, r._raw_data["player_ids"])
|
|
m_table.get_item.assert_called_once_with(
|
|
hash_key=players_text, range_key=d_text, consistent_read=False)
|
|
|
|
@mock.patch("dynamodb_mapper.model.Item")
|
|
@mock.patch("dynamodb_mapper.model.boto")
|
|
def test_save_autoincrement_int(self, m_boto, m_item):
|
|
m_item_instance = m_item.return_value
|
|
m_item_instance.hash_key_name = "id"
|
|
max_item = m_item_instance.table.new_item.return_value
|
|
|
|
max_item.save.return_value = {
|
|
'Attributes': {
|
|
'__max_hash_key__': 3
|
|
}
|
|
}
|
|
|
|
l = LogEntry()
|
|
l.text = "Everybody's dead, Dave."
|
|
l.save()
|
|
|
|
m_item.assert_called_with(mock.ANY, attrs={"id": 0, "text": l.text})
|
|
m_item.return_value.__setitem__.assert_called_with("id", 3)
|
|
m_item_instance.save.assert_called()
|
|
|
|
@mock.patch("dynamodb_mapper.model.Item")
|
|
@mock.patch("dynamodb_mapper.model.boto")
|
|
def test_save_autoincrement_int_conflict(self, m_boto, m_item):
|
|
m_item_instance = m_item.return_value
|
|
m_item_instance.hash_key_name = "id"
|
|
max_item = m_item_instance.table.new_item.return_value
|
|
|
|
max_item.save.return_value = {
|
|
'Attributes': {
|
|
'__max_hash_key__': 3
|
|
}
|
|
}
|
|
|
|
# The first call (id=3) will be rejected.
|
|
error = DynamoDBResponseError(
|
|
None, None, {"__type": "ConditionalCheckFailedException"})
|
|
|
|
def err(*args, **kw):
|
|
# Clear the error then fail
|
|
m_item_instance.put.side_effect = None
|
|
raise error
|
|
|
|
m_item_instance.put.side_effect = err
|
|
|
|
l = LogEntry()
|
|
l.text = "Everybody's dead, Dave."
|
|
l.save()
|
|
|
|
m_item.return_value.__setitem__.assert_called_with("id", 3)
|
|
self.assertEquals(m_item.return_value.__setitem__.call_count, 2)
|
|
|
|
@mock.patch("dynamodb_mapper.model.Item")
|
|
@mock.patch("dynamodb_mapper.model.boto")
|
|
def test_save_autoincrement_int_max_retries(self, m_boto, m_item):
|
|
m_item_instance = m_item.return_value
|
|
m_item_instance.hash_key_name = "id"
|
|
max_item = m_item_instance.table.get_item.return_value
|
|
max_item.__getitem__.return_value = 2
|
|
error = DynamoDBResponseError(
|
|
None, None, {"__type": "ConditionalCheckFailedException"})
|
|
# the put call will never succeed
|
|
m_item_instance.put.side_effect = error
|
|
|
|
l = LogEntry()
|
|
l.text = "Everybody's dead, Dave."
|
|
self.assertRaises(MaxRetriesExceededError, l.save)
|
|
|
|
self.assertEquals(m_item_instance.put.call_count, MAX_RETRIES)
|
|
|
|
@mock.patch("dynamodb_mapper.model.Item")
|
|
@mock.patch("dynamodb_mapper.model.boto")
|
|
def test_save_autoincrement_int_unhandled_exception(self, m_boto, m_item):
|
|
m_item_instance = m_item.return_value
|
|
m_item_instance.hash_key_name = "id"
|
|
max_item = m_item_instance.table.get_item.return_value
|
|
max_item.__getitem__.return_value = 2
|
|
error = DynamoDBResponseError(
|
|
None, None, {"__type": "ResourceNotFoundException"})
|
|
# the put call will note succeed
|
|
m_item_instance.put.side_effect = error
|
|
|
|
l = LogEntry()
|
|
l.text = "Everybody's dead, Dave."
|
|
self.assertRaises(DynamoDBResponseError, l.save)
|
|
|
|
@mock.patch("dynamodb_mapper.model.Item")
|
|
@mock.patch("dynamodb_mapper.model.boto")
|
|
def test_save_autoincrement_magic_overwrite(self, m_boto, m_item):
|
|
l = LogEntry()
|
|
l.text = "Everybody's dead, Dave."
|
|
l.id = MAGIC_KEY
|
|
|
|
self.assertRaises(SchemaError, l.save)
|
|
|
|
@mock.patch("dynamodb_mapper.model.Item")
|
|
@mock.patch("dynamodb_mapper.model.boto")
|
|
def test_save_raise_on_conflict(self, m_boto, m_item):
|
|
m_item_instance = m_item.return_value
|
|
error = DynamoDBResponseError(
|
|
None, None, {"__type": "ConditionalCheckFailedException"})
|
|
m_item_instance.put.side_effect = error
|
|
|
|
name = "Knee-deep in the Dead"
|
|
cheats = set(["iddqd", "idkfa"])
|
|
|
|
c = DoomCampaign()
|
|
|
|
c._raw_data["id"] = 1
|
|
c._raw_data["name"] = name
|
|
#simulate empty field in DB
|
|
#c._raw_data["cheats"] = cheats
|
|
|
|
c.id = 1
|
|
c.name = name
|
|
c.cheats = cheats
|
|
|
|
self.assertRaises(ConflictError, c.save, raise_on_conflict=True)
|
|
m_item_instance.put.assert_called_with({"id": 1, "name": name, "cheats": False})
|
|
|
|
@mock.patch("dynamodb_mapper.model.Item")
|
|
@mock.patch("dynamodb_mapper.model.boto")
|
|
def test_save_raise_on_conflict_boolean(self, m_boto, m_item):
|
|
m_item_instance = m_item.return_value
|
|
error = DynamoDBResponseError(
|
|
None, None, {"__type": "ConditionalCheckFailedException"})
|
|
m_item_instance.put.side_effect = error
|
|
|
|
name = "Knee-deep in the Dead"
|
|
completed = False
|
|
|
|
c = DoomCampaignStatus()
|
|
|
|
c._raw_data["id"] = 1
|
|
c._raw_data["name"] = name
|
|
c._raw_data["completed"] = int(False) # DynamoDB stores int and unicode only
|
|
|
|
c.id = 0
|
|
c.name = name
|
|
c.completed = completed
|
|
|
|
self.assertRaises(ConflictError, c.save, raise_on_conflict=True)
|
|
m_item_instance.put.assert_called_with({"id": 1, "name": name, "completed": 0})
|
|
|
|
@mock.patch("dynamodb_mapper.model.Item")
|
|
@mock.patch("dynamodb_mapper.model.boto")
|
|
def test_save_no_overwrite_composite_fails(self, m_boto, m_item):
|
|
"""Save raised OverwriteError when new object has the same ID"""
|
|
m_item_instance = m_item.return_value
|
|
error = DynamoDBResponseError(
|
|
None, None, {"__type": "ConditionalCheckFailedException"})
|
|
m_item_instance.put.side_effect = error
|
|
|
|
c = DoomMap()
|
|
c.episode_id = 0
|
|
c.name = "Knee-deep in the Dead"
|
|
|
|
self.assertRaises(
|
|
OverwriteError,
|
|
c.save, raise_on_conflict=True
|
|
)
|
|
m_item_instance.put.assert_called_with({"episode_id": False, "name": False})
|
|
|
|
@mock.patch("dynamodb_mapper.model.Item")
|
|
@mock.patch("dynamodb_mapper.model.boto")
|
|
def test_save_unhandled_exception(self, m_boto, m_item):
|
|
m_item_instance = m_item.return_value
|
|
error = DynamoDBResponseError(
|
|
None, None, {"__type": "ResourceNotFoundException"})
|
|
m_item_instance.put.side_effect = error
|
|
|
|
c = DoomMap()
|
|
c.episode_id = 0
|
|
c.name = "Knee-deep in the Dead"
|
|
|
|
self.assertRaises(DynamoDBResponseError, c.save)
|
|
|
|
@mock.patch("dynamodb_mapper.model.boto")
|
|
def test_get_batch_hash_key(self, m_boto):
|
|
m_batch_list = m_boto.connect_dynamodb.return_value.new_batch_list.return_value
|
|
m_table = m_boto.connect_dynamodb.return_value.get_table.return_value
|
|
# Mock empty answer as we only observe the way the request is build
|
|
# FIXME: make sure _raw_data is filled
|
|
m_batch_list.submit.return_value = {
|
|
u"Responses": {
|
|
DoomEpisode.__table__: {
|
|
u"Items": []
|
|
}
|
|
}
|
|
}
|
|
|
|
DoomEpisode.get_batch(range(2));
|
|
m_batch_list.add_batch.assert_called_with(m_table, [0,1])
|
|
assert m_batch_list.submit.called
|
|
|
|
@mock.patch("dynamodb_mapper.model.boto")
|
|
def test_get_batch_hash_range_key(self, m_boto):
|
|
m_batch_list = m_boto.connect_dynamodb.return_value.new_batch_list.return_value
|
|
m_table = m_boto.connect_dynamodb.return_value.get_table.return_value
|
|
# Mock empty answer as we only observe the way the request is build
|
|
# FIXME: make sure _raw_data is filled
|
|
m_batch_list.submit.return_value = {
|
|
u"Responses": {
|
|
DoomMap.__table__: {
|
|
u"Items": []
|
|
}
|
|
}
|
|
}
|
|
|
|
DoomMap.get_batch([(0, u"level1"), (1, u"level2")]);
|
|
m_batch_list.add_batch.assert_called_with(m_table, [(0, u'level1'), (1, u'level2')])
|
|
assert m_batch_list.submit.called
|
|
|
|
def test_get_batch_over_100(self):
|
|
self.assertRaises(
|
|
ValueError,
|
|
DoomEpisode.get_batch,
|
|
range(101)
|
|
)
|
|
|
|
@mock.patch("dynamodb_mapper.model.ConnectionBorg.get_table")
|
|
def test_query(self, m_get_table):
|
|
m_table = m_get_table.return_value
|
|
m_table.query.return_value = []
|
|
# FIXME: make sure _raw_data is filled
|
|
|
|
from boto.dynamodb import condition
|
|
DoomMap.query(1, condition.BEGINS_WITH(u"level"), True)
|
|
|
|
m_table.query.assert_called_with(1, condition.BEGINS_WITH(u"level"), consistent_read=True, scan_index_forward=True, max_results=None)
|
|
|
|
@mock.patch("dynamodb_mapper.model.ConnectionBorg.get_table")
|
|
def test_query_reverse_limit(self, m_get_table):
|
|
m_table = m_get_table.return_value
|
|
m_table.query.return_value = []
|
|
|
|
from boto.dynamodb import condition
|
|
DoomMap.query(1, condition.BEGINS_WITH(u"level"), True, reverse=True, limit=42)
|
|
|
|
m_table.query.assert_called_with(1, condition.BEGINS_WITH(u"level"), consistent_read=True, scan_index_forward=False, max_results=42)
|
|
|
|
@mock.patch("dynamodb_mapper.model.ConnectionBorg.get_table")
|
|
def test_scan(self, m_get_table):
|
|
# FIXME: test the generator. autoinc keys should be hidden from the results
|
|
# FIXME: make sure _raw_data is filled
|
|
m_table = m_get_table.return_value
|
|
m_table.scan.return_value = []
|
|
|
|
from boto.dynamodb import condition
|
|
scan_filter = {
|
|
'name': condition.BEGINS_WITH(u"level")
|
|
}
|
|
DoomEpisode.scan(scan_filter)
|
|
|
|
m_table.scan.assert_called_with(scan_filter)
|
|
|
|
@mock.patch("dynamodb_mapper.model.boto")
|
|
@mock.patch("dynamodb_mapper.model.Item")
|
|
def test_delete_hash_key(self, m_item, m_boto):
|
|
m_item_instance = m_item.return_value
|
|
m_table = m_boto.connect_dynamodb.return_value.get_table.return_value
|
|
|
|
python_date = datetime.datetime(2012, 05, 31, 12, 0, 0, tzinfo=utc_tz)
|
|
dynamodb_date = _python_to_dynamodb(python_date)
|
|
|
|
raw_data = {
|
|
u"datetime":dynamodb_date
|
|
}
|
|
|
|
# Simulate obj from DB
|
|
d = Patch._from_db_dict(raw_data)
|
|
|
|
d.delete()
|
|
|
|
self.assertEquals(d._raw_data, {})
|
|
m_item.assert_called_once_with(m_table, dynamodb_date, None)
|
|
assert m_item_instance.delete.called
|
|
|
|
@mock.patch("dynamodb_mapper.model.boto")
|
|
@mock.patch("dynamodb_mapper.model.Item")
|
|
def test_delete_composite_keys(self, m_item, m_boto):
|
|
m_item_instance = m_item.return_value
|
|
m_table = m_boto.connect_dynamodb.return_value.get_table.return_value
|
|
|
|
raw_data = {
|
|
u"episode_id": 42,
|
|
u"name": "level super geek"
|
|
}
|
|
|
|
# Simulate obj from DB, composite keys
|
|
d = DoomMap._from_db_dict(raw_data)
|
|
|
|
d.delete()
|
|
|
|
self.assertEquals(d._raw_data, {})
|
|
m_item.assert_called_once_with(m_table, 42, "level super geek")
|
|
assert m_item_instance.delete.called
|
|
|
|
@mock.patch("dynamodb_mapper.model.boto")
|
|
@mock.patch("dynamodb_mapper.model.Item")
|
|
def test_delete_new_object_roc(self, m_item, m_boto):
|
|
m_item_instance = m_item.return_value
|
|
m_table = m_boto.connect_dynamodb.return_value.get_table.return_value
|
|
|
|
# Create a new object
|
|
d = DoomEpisode()
|
|
d.id = "42"
|
|
|
|
self.assertRaises(
|
|
ConflictError,
|
|
d.delete,
|
|
raise_on_conflict=True
|
|
)
|
|
|
|
self.assertEquals(d._raw_data, {}) #still "new"
|
|
|
|
@mock.patch("dynamodb_mapper.model.boto")
|
|
@mock.patch("dynamodb_mapper.model.Item")
|
|
def test_delete_object_roc(self, m_item, m_boto):
|
|
m_item_instance = m_item.return_value
|
|
m_table = m_boto.connect_dynamodb.return_value.get_table.return_value
|
|
m_item_instance.delete.side_effect = DynamoDBConditionalCheckFailedError(404, "mock")
|
|
|
|
raw_data = {
|
|
u"episode_id": 42,
|
|
u"name": "level super geek"
|
|
}
|
|
|
|
# Simulate obj from DB
|
|
d = DoomMap._from_db_dict(raw_data)
|
|
|
|
self.assertRaises(
|
|
ConflictError,
|
|
d.delete,
|
|
raise_on_conflict=True
|
|
)
|
|
|
|
m_item_instance.delete.assert_called_with({
|
|
u'name': 'level super geek',
|
|
u'episode_id': 42,
|
|
u'world': False
|
|
})
|
|
|
|
self.assertEquals(d._raw_data, raw_data) #no change
|