PageRenderTime 87ms CodeModel.GetById 5ms app.highlight 71ms RepoModel.GetById 1ms app.codeStats 1ms

/tests/unit/dynamodb2/test_table.py

https://bitbucket.org/jholkeboer/boto
Python | 3068 lines | 2821 code | 171 blank | 76 comment | 28 complexity | dd4ffca3cd58e4f194f6e82094e89047 MD5 | raw file

Large files files are truncated, but you can click here to view the full file

   1from tests.compat import mock, unittest
   2from boto.dynamodb2 import exceptions
   3from boto.dynamodb2.fields import (HashKey, RangeKey,
   4                                   AllIndex, KeysOnlyIndex, IncludeIndex,
   5                                   GlobalAllIndex, GlobalKeysOnlyIndex,
   6                                   GlobalIncludeIndex)
   7from boto.dynamodb2.items import Item
   8from boto.dynamodb2.layer1 import DynamoDBConnection
   9from boto.dynamodb2.results import ResultSet, BatchGetResultSet
  10from boto.dynamodb2.table import Table
  11from boto.dynamodb2.types import (STRING, NUMBER, BINARY,
  12                                  FILTER_OPERATORS, QUERY_OPERATORS)
  13from boto.exception import JSONResponseError
  14from boto.compat import six, long_type
  15
  16
  17FakeDynamoDBConnection = mock.create_autospec(DynamoDBConnection)
  18
  19
  20class SchemaFieldsTestCase(unittest.TestCase):
  21    def test_hash_key(self):
  22        hash_key = HashKey('hello')
  23        self.assertEqual(hash_key.name, 'hello')
  24        self.assertEqual(hash_key.data_type, STRING)
  25        self.assertEqual(hash_key.attr_type, 'HASH')
  26
  27        self.assertEqual(hash_key.definition(), {
  28            'AttributeName': 'hello',
  29            'AttributeType': 'S'
  30        })
  31        self.assertEqual(hash_key.schema(), {
  32            'AttributeName': 'hello',
  33            'KeyType': 'HASH'
  34        })
  35
  36    def test_range_key(self):
  37        range_key = RangeKey('hello')
  38        self.assertEqual(range_key.name, 'hello')
  39        self.assertEqual(range_key.data_type, STRING)
  40        self.assertEqual(range_key.attr_type, 'RANGE')
  41
  42        self.assertEqual(range_key.definition(), {
  43            'AttributeName': 'hello',
  44            'AttributeType': 'S'
  45        })
  46        self.assertEqual(range_key.schema(), {
  47            'AttributeName': 'hello',
  48            'KeyType': 'RANGE'
  49        })
  50
  51    def test_alternate_type(self):
  52        alt_key = HashKey('alt', data_type=NUMBER)
  53        self.assertEqual(alt_key.name, 'alt')
  54        self.assertEqual(alt_key.data_type, NUMBER)
  55        self.assertEqual(alt_key.attr_type, 'HASH')
  56
  57        self.assertEqual(alt_key.definition(), {
  58            'AttributeName': 'alt',
  59            'AttributeType': 'N'
  60        })
  61        self.assertEqual(alt_key.schema(), {
  62            'AttributeName': 'alt',
  63            'KeyType': 'HASH'
  64        })
  65
  66
  67class IndexFieldTestCase(unittest.TestCase):
  68    def test_all_index(self):
  69        all_index = AllIndex('AllKeys', parts=[
  70            HashKey('username'),
  71            RangeKey('date_joined')
  72        ])
  73        self.assertEqual(all_index.name, 'AllKeys')
  74        self.assertEqual([part.attr_type for part in all_index.parts], [
  75            'HASH',
  76            'RANGE'
  77        ])
  78        self.assertEqual(all_index.projection_type, 'ALL')
  79
  80        self.assertEqual(all_index.definition(), [
  81            {'AttributeName': 'username', 'AttributeType': 'S'},
  82            {'AttributeName': 'date_joined', 'AttributeType': 'S'}
  83        ])
  84        self.assertEqual(all_index.schema(), {
  85            'IndexName': 'AllKeys',
  86            'KeySchema': [
  87                {
  88                    'AttributeName': 'username',
  89                    'KeyType': 'HASH'
  90                },
  91                {
  92                    'AttributeName': 'date_joined',
  93                    'KeyType': 'RANGE'
  94                }
  95            ],
  96            'Projection': {
  97                'ProjectionType': 'ALL'
  98            }
  99        })
 100
 101    def test_keys_only_index(self):
 102        keys_only = KeysOnlyIndex('KeysOnly', parts=[
 103            HashKey('username'),
 104            RangeKey('date_joined')
 105        ])
 106        self.assertEqual(keys_only.name, 'KeysOnly')
 107        self.assertEqual([part.attr_type for part in keys_only.parts], [
 108            'HASH',
 109            'RANGE'
 110        ])
 111        self.assertEqual(keys_only.projection_type, 'KEYS_ONLY')
 112
 113        self.assertEqual(keys_only.definition(), [
 114            {'AttributeName': 'username', 'AttributeType': 'S'},
 115            {'AttributeName': 'date_joined', 'AttributeType': 'S'}
 116        ])
 117        self.assertEqual(keys_only.schema(), {
 118            'IndexName': 'KeysOnly',
 119            'KeySchema': [
 120                {
 121                    'AttributeName': 'username',
 122                    'KeyType': 'HASH'
 123                },
 124                {
 125                    'AttributeName': 'date_joined',
 126                    'KeyType': 'RANGE'
 127                }
 128            ],
 129            'Projection': {
 130                'ProjectionType': 'KEYS_ONLY'
 131            }
 132        })
 133
 134    def test_include_index(self):
 135        include_index = IncludeIndex('IncludeKeys', parts=[
 136            HashKey('username'),
 137            RangeKey('date_joined')
 138        ], includes=[
 139            'gender',
 140            'friend_count'
 141        ])
 142        self.assertEqual(include_index.name, 'IncludeKeys')
 143        self.assertEqual([part.attr_type for part in include_index.parts], [
 144            'HASH',
 145            'RANGE'
 146        ])
 147        self.assertEqual(include_index.projection_type, 'INCLUDE')
 148
 149        self.assertEqual(include_index.definition(), [
 150            {'AttributeName': 'username', 'AttributeType': 'S'},
 151            {'AttributeName': 'date_joined', 'AttributeType': 'S'}
 152        ])
 153        self.assertEqual(include_index.schema(), {
 154            'IndexName': 'IncludeKeys',
 155            'KeySchema': [
 156                {
 157                    'AttributeName': 'username',
 158                    'KeyType': 'HASH'
 159                },
 160                {
 161                    'AttributeName': 'date_joined',
 162                    'KeyType': 'RANGE'
 163                }
 164            ],
 165            'Projection': {
 166                'ProjectionType': 'INCLUDE',
 167                'NonKeyAttributes': [
 168                    'gender',
 169                    'friend_count',
 170                ]
 171            }
 172        })
 173
 174    def test_global_all_index(self):
 175        all_index = GlobalAllIndex('AllKeys', parts=[
 176            HashKey('username'),
 177            RangeKey('date_joined')
 178        ],
 179        throughput={
 180            'read': 6,
 181            'write': 2,
 182        })
 183        self.assertEqual(all_index.name, 'AllKeys')
 184        self.assertEqual([part.attr_type for part in all_index.parts], [
 185            'HASH',
 186            'RANGE'
 187        ])
 188        self.assertEqual(all_index.projection_type, 'ALL')
 189
 190        self.assertEqual(all_index.definition(), [
 191            {'AttributeName': 'username', 'AttributeType': 'S'},
 192            {'AttributeName': 'date_joined', 'AttributeType': 'S'}
 193        ])
 194        self.assertEqual(all_index.schema(), {
 195            'IndexName': 'AllKeys',
 196            'KeySchema': [
 197                {
 198                    'AttributeName': 'username',
 199                    'KeyType': 'HASH'
 200                },
 201                {
 202                    'AttributeName': 'date_joined',
 203                    'KeyType': 'RANGE'
 204                }
 205            ],
 206            'Projection': {
 207                'ProjectionType': 'ALL'
 208            },
 209            'ProvisionedThroughput': {
 210                'ReadCapacityUnits': 6,
 211                'WriteCapacityUnits': 2
 212            }
 213        })
 214
 215    def test_global_keys_only_index(self):
 216        keys_only = GlobalKeysOnlyIndex('KeysOnly', parts=[
 217            HashKey('username'),
 218            RangeKey('date_joined')
 219        ],
 220        throughput={
 221            'read': 3,
 222            'write': 4,
 223        })
 224        self.assertEqual(keys_only.name, 'KeysOnly')
 225        self.assertEqual([part.attr_type for part in keys_only.parts], [
 226            'HASH',
 227            'RANGE'
 228        ])
 229        self.assertEqual(keys_only.projection_type, 'KEYS_ONLY')
 230
 231        self.assertEqual(keys_only.definition(), [
 232            {'AttributeName': 'username', 'AttributeType': 'S'},
 233            {'AttributeName': 'date_joined', 'AttributeType': 'S'}
 234        ])
 235        self.assertEqual(keys_only.schema(), {
 236            'IndexName': 'KeysOnly',
 237            'KeySchema': [
 238                {
 239                    'AttributeName': 'username',
 240                    'KeyType': 'HASH'
 241                },
 242                {
 243                    'AttributeName': 'date_joined',
 244                    'KeyType': 'RANGE'
 245                }
 246            ],
 247            'Projection': {
 248                'ProjectionType': 'KEYS_ONLY'
 249            },
 250            'ProvisionedThroughput': {
 251                'ReadCapacityUnits': 3,
 252                'WriteCapacityUnits': 4
 253            }
 254        })
 255
 256    def test_global_include_index(self):
 257        # Lean on the default throughput
 258        include_index = GlobalIncludeIndex('IncludeKeys', parts=[
 259            HashKey('username'),
 260            RangeKey('date_joined')
 261        ], includes=[
 262            'gender',
 263            'friend_count'
 264        ])
 265        self.assertEqual(include_index.name, 'IncludeKeys')
 266        self.assertEqual([part.attr_type for part in include_index.parts], [
 267            'HASH',
 268            'RANGE'
 269        ])
 270        self.assertEqual(include_index.projection_type, 'INCLUDE')
 271
 272        self.assertEqual(include_index.definition(), [
 273            {'AttributeName': 'username', 'AttributeType': 'S'},
 274            {'AttributeName': 'date_joined', 'AttributeType': 'S'}
 275        ])
 276        self.assertEqual(include_index.schema(), {
 277            'IndexName': 'IncludeKeys',
 278            'KeySchema': [
 279                {
 280                    'AttributeName': 'username',
 281                    'KeyType': 'HASH'
 282                },
 283                {
 284                    'AttributeName': 'date_joined',
 285                    'KeyType': 'RANGE'
 286                }
 287            ],
 288            'Projection': {
 289                'ProjectionType': 'INCLUDE',
 290                'NonKeyAttributes': [
 291                    'gender',
 292                    'friend_count',
 293                ]
 294            },
 295            'ProvisionedThroughput': {
 296                'ReadCapacityUnits': 5,
 297                'WriteCapacityUnits': 5
 298            }
 299        })
 300
 301    def test_global_include_index_throughput(self):
 302        include_index = GlobalIncludeIndex('IncludeKeys', parts=[
 303            HashKey('username'),
 304            RangeKey('date_joined')
 305        ], includes=[
 306            'gender',
 307            'friend_count'
 308        ], throughput={
 309            'read': 10,
 310            'write': 8
 311        })
 312
 313        self.assertEqual(include_index.schema(), {
 314            'IndexName': 'IncludeKeys',
 315            'KeySchema': [
 316                {
 317                    'AttributeName': 'username',
 318                    'KeyType': 'HASH'
 319                },
 320                {
 321                    'AttributeName': 'date_joined',
 322                    'KeyType': 'RANGE'
 323                }
 324            ],
 325            'Projection': {
 326                'ProjectionType': 'INCLUDE',
 327                'NonKeyAttributes': [
 328                    'gender',
 329                    'friend_count',
 330                ]
 331            },
 332            'ProvisionedThroughput': {
 333                'ReadCapacityUnits': 10,
 334                'WriteCapacityUnits': 8
 335            }
 336        })
 337
 338
 339class ItemTestCase(unittest.TestCase):
 340    if six.PY2:
 341        assertCountEqual = unittest.TestCase.assertItemsEqual
 342
 343    def setUp(self):
 344        super(ItemTestCase, self).setUp()
 345        self.table = Table('whatever', connection=FakeDynamoDBConnection())
 346        self.johndoe = self.create_item({
 347            'username': 'johndoe',
 348            'first_name': 'John',
 349            'date_joined': 12345,
 350        })
 351
 352    def create_item(self, data):
 353        return Item(self.table, data=data)
 354
 355    def test_initialization(self):
 356        empty_item = Item(self.table)
 357        self.assertEqual(empty_item.table, self.table)
 358        self.assertEqual(empty_item._data, {})
 359
 360        full_item = Item(self.table, data={
 361            'username': 'johndoe',
 362            'date_joined': 12345,
 363        })
 364        self.assertEqual(full_item.table, self.table)
 365        self.assertEqual(full_item._data, {
 366            'username': 'johndoe',
 367            'date_joined': 12345,
 368        })
 369
 370    # The next couple methods make use of ``sorted(...)`` so we get consistent
 371    # ordering everywhere & no erroneous failures.
 372
 373    def test_keys(self):
 374        self.assertCountEqual(self.johndoe.keys(), [
 375            'date_joined',
 376            'first_name',
 377            'username',
 378        ])
 379
 380    def test_values(self):
 381        self.assertCountEqual(self.johndoe.values(),
 382                              [12345, 'John', 'johndoe'])
 383
 384    def test_contains(self):
 385        self.assertIn('username', self.johndoe)
 386        self.assertIn('first_name', self.johndoe)
 387        self.assertIn('date_joined', self.johndoe)
 388        self.assertNotIn('whatever', self.johndoe)
 389
 390    def test_iter(self):
 391        self.assertCountEqual(self.johndoe,
 392                              ['johndoe', 'John', 12345])
 393
 394    def test_get(self):
 395        self.assertEqual(self.johndoe.get('username'), 'johndoe')
 396        self.assertEqual(self.johndoe.get('first_name'), 'John')
 397        self.assertEqual(self.johndoe.get('date_joined'), 12345)
 398
 399        # Test a missing key. No default yields ``None``.
 400        self.assertEqual(self.johndoe.get('last_name'), None)
 401        # This time with a default.
 402        self.assertEqual(self.johndoe.get('last_name', True), True)
 403
 404    def test_items(self):
 405        self.assertCountEqual(
 406            self.johndoe.items(),
 407            [
 408                ('date_joined', 12345),
 409                ('first_name', 'John'),
 410                ('username', 'johndoe'),
 411            ])
 412
 413    def test_attribute_access(self):
 414        self.assertEqual(self.johndoe['username'], 'johndoe')
 415        self.assertEqual(self.johndoe['first_name'], 'John')
 416        self.assertEqual(self.johndoe['date_joined'], 12345)
 417
 418        # Test a missing key.
 419        self.assertEqual(self.johndoe['last_name'], None)
 420
 421        # Set a key.
 422        self.johndoe['last_name'] = 'Doe'
 423        # Test accessing the new key.
 424        self.assertEqual(self.johndoe['last_name'], 'Doe')
 425
 426        # Delete a key.
 427        del self.johndoe['last_name']
 428        # Test the now-missing-again key.
 429        self.assertEqual(self.johndoe['last_name'], None)
 430
 431    def test_needs_save(self):
 432        self.johndoe.mark_clean()
 433        self.assertFalse(self.johndoe.needs_save())
 434        self.johndoe['last_name'] = 'Doe'
 435        self.assertTrue(self.johndoe.needs_save())
 436
 437    def test_needs_save_set_changed(self):
 438        # First, ensure we're clean.
 439        self.johndoe.mark_clean()
 440        self.assertFalse(self.johndoe.needs_save())
 441        # Add a friends collection.
 442        self.johndoe['friends'] = set(['jane', 'alice'])
 443        self.assertTrue(self.johndoe.needs_save())
 444        # Now mark it clean, then change the collection.
 445        # This does NOT call ``__setitem__``, so the item used to be
 446        # incorrectly appearing to be clean, when it had in fact been changed.
 447        self.johndoe.mark_clean()
 448        self.assertFalse(self.johndoe.needs_save())
 449        self.johndoe['friends'].add('bob')
 450        self.assertTrue(self.johndoe.needs_save())
 451
 452    def test_mark_clean(self):
 453        self.johndoe['last_name'] = 'Doe'
 454        self.assertTrue(self.johndoe.needs_save())
 455        self.johndoe.mark_clean()
 456        self.assertFalse(self.johndoe.needs_save())
 457
 458    def test_load(self):
 459        empty_item = Item(self.table)
 460        empty_item.load({
 461            'Item': {
 462                'username': {'S': 'johndoe'},
 463                'first_name': {'S': 'John'},
 464                'last_name': {'S': 'Doe'},
 465                'date_joined': {'N': '1366056668'},
 466                'friend_count': {'N': '3'},
 467                'friends': {'SS': ['alice', 'bob', 'jane']},
 468            }
 469        })
 470        self.assertEqual(empty_item['username'], 'johndoe')
 471        self.assertEqual(empty_item['date_joined'], 1366056668)
 472        self.assertEqual(sorted(empty_item['friends']), sorted([
 473            'alice',
 474            'bob',
 475            'jane'
 476        ]))
 477
 478    def test_get_keys(self):
 479        # Setup the data.
 480        self.table.schema = [
 481            HashKey('username'),
 482            RangeKey('date_joined'),
 483        ]
 484        self.assertEqual(self.johndoe.get_keys(), {
 485            'username': 'johndoe',
 486            'date_joined': 12345,
 487        })
 488
 489    def test_get_raw_keys(self):
 490        # Setup the data.
 491        self.table.schema = [
 492            HashKey('username'),
 493            RangeKey('date_joined'),
 494        ]
 495        self.assertEqual(self.johndoe.get_raw_keys(), {
 496            'username': {'S': 'johndoe'},
 497            'date_joined': {'N': '12345'},
 498        })
 499
 500    def test_build_expects(self):
 501        # Pristine.
 502        self.assertEqual(self.johndoe.build_expects(), {
 503            'first_name': {
 504                'Exists': False,
 505            },
 506            'username': {
 507                'Exists': False,
 508            },
 509            'date_joined': {
 510                'Exists': False,
 511            },
 512        })
 513
 514        # Without modifications.
 515        self.johndoe.mark_clean()
 516        self.assertEqual(self.johndoe.build_expects(), {
 517            'first_name': {
 518                'Exists': True,
 519                'Value': {
 520                    'S': 'John',
 521                },
 522            },
 523            'username': {
 524                'Exists': True,
 525                'Value': {
 526                    'S': 'johndoe',
 527                },
 528            },
 529            'date_joined': {
 530                'Exists': True,
 531                'Value': {
 532                    'N': '12345',
 533                },
 534            },
 535        })
 536
 537        # Change some data.
 538        self.johndoe['first_name'] = 'Johann'
 539        # Add some data.
 540        self.johndoe['last_name'] = 'Doe'
 541        # Delete some data.
 542        del self.johndoe['date_joined']
 543
 544        # All fields (default).
 545        self.assertEqual(self.johndoe.build_expects(), {
 546            'first_name': {
 547                'Exists': True,
 548                'Value': {
 549                    'S': 'John',
 550                },
 551            },
 552            'last_name': {
 553                'Exists': False,
 554            },
 555            'username': {
 556                'Exists': True,
 557                'Value': {
 558                    'S': 'johndoe',
 559                },
 560            },
 561            'date_joined': {
 562                'Exists': True,
 563                'Value': {
 564                    'N': '12345',
 565                },
 566            },
 567        })
 568
 569        # Only a subset of the fields.
 570        self.assertEqual(self.johndoe.build_expects(fields=[
 571            'first_name',
 572            'last_name',
 573            'date_joined',
 574        ]), {
 575            'first_name': {
 576                'Exists': True,
 577                'Value': {
 578                    'S': 'John',
 579                },
 580            },
 581            'last_name': {
 582                'Exists': False,
 583            },
 584            'date_joined': {
 585                'Exists': True,
 586                'Value': {
 587                    'N': '12345',
 588                },
 589            },
 590        })
 591
 592    def test_prepare_full(self):
 593        self.assertEqual(self.johndoe.prepare_full(), {
 594            'username': {'S': 'johndoe'},
 595            'first_name': {'S': 'John'},
 596            'date_joined': {'N': '12345'}
 597        })
 598
 599        self.johndoe['friends'] = set(['jane', 'alice'])
 600        data = self.johndoe.prepare_full()
 601        self.assertEqual(data['username'], {'S': 'johndoe'})
 602        self.assertEqual(data['first_name'], {'S': 'John'})
 603        self.assertEqual(data['date_joined'], {'N': '12345'})
 604        self.assertCountEqual(data['friends']['SS'],
 605                              ['jane', 'alice'])
 606
 607    def test_prepare_full_empty_set(self):
 608        self.johndoe['friends'] = set()
 609        self.assertEqual(self.johndoe.prepare_full(), {
 610            'username': {'S': 'johndoe'},
 611            'first_name': {'S': 'John'},
 612            'date_joined': {'N': '12345'}
 613        })
 614
 615    def test_prepare_partial(self):
 616        self.johndoe.mark_clean()
 617        # Change some data.
 618        self.johndoe['first_name'] = 'Johann'
 619        # Add some data.
 620        self.johndoe['last_name'] = 'Doe'
 621        # Delete some data.
 622        del self.johndoe['date_joined']
 623
 624        final_data, fields = self.johndoe.prepare_partial()
 625        self.assertEqual(final_data, {
 626            'date_joined': {
 627                'Action': 'DELETE',
 628            },
 629            'first_name': {
 630                'Action': 'PUT',
 631                'Value': {'S': 'Johann'},
 632            },
 633            'last_name': {
 634                'Action': 'PUT',
 635                'Value': {'S': 'Doe'},
 636            },
 637        })
 638        self.assertEqual(fields, set([
 639            'first_name',
 640            'last_name',
 641            'date_joined'
 642        ]))
 643
 644    def test_prepare_partial_empty_set(self):
 645        self.johndoe.mark_clean()
 646        # Change some data.
 647        self.johndoe['first_name'] = 'Johann'
 648        # Add some data.
 649        self.johndoe['last_name'] = 'Doe'
 650        # Delete some data.
 651        del self.johndoe['date_joined']
 652        # Put an empty set on the ``Item``.
 653        self.johndoe['friends'] = set()
 654
 655        final_data, fields = self.johndoe.prepare_partial()
 656        self.assertEqual(final_data, {
 657            'date_joined': {
 658                'Action': 'DELETE',
 659            },
 660            'first_name': {
 661                'Action': 'PUT',
 662                'Value': {'S': 'Johann'},
 663            },
 664            'last_name': {
 665                'Action': 'PUT',
 666                'Value': {'S': 'Doe'},
 667            },
 668        })
 669        self.assertEqual(fields, set([
 670            'first_name',
 671            'last_name',
 672            'date_joined'
 673        ]))
 674
 675    def test_save_no_changes(self):
 676        # Unchanged, no save.
 677        with mock.patch.object(self.table, '_put_item', return_value=True) \
 678                as mock_put_item:
 679            # Pretend we loaded it via ``get_item``...
 680            self.johndoe.mark_clean()
 681            self.assertFalse(self.johndoe.save())
 682
 683        self.assertFalse(mock_put_item.called)
 684
 685    def test_save_with_changes(self):
 686        # With changed data.
 687        with mock.patch.object(self.table, '_put_item', return_value=True) \
 688                as mock_put_item:
 689            self.johndoe.mark_clean()
 690            self.johndoe['first_name'] = 'J'
 691            self.johndoe['new_attr'] = 'never_seen_before'
 692            self.assertTrue(self.johndoe.save())
 693            self.assertFalse(self.johndoe.needs_save())
 694
 695        self.assertTrue(mock_put_item.called)
 696        mock_put_item.assert_called_once_with({
 697            'username': {'S': 'johndoe'},
 698            'first_name': {'S': 'J'},
 699            'new_attr': {'S': 'never_seen_before'},
 700            'date_joined': {'N': '12345'}
 701        }, expects={
 702            'username': {
 703                'Value': {
 704                    'S': 'johndoe',
 705                },
 706                'Exists': True,
 707            },
 708            'first_name': {
 709                'Value': {
 710                    'S': 'John',
 711                },
 712                'Exists': True,
 713            },
 714            'new_attr': {
 715                'Exists': False,
 716            },
 717            'date_joined': {
 718                'Value': {
 719                    'N': '12345',
 720                },
 721                'Exists': True,
 722            },
 723        })
 724
 725    def test_save_with_changes_overwrite(self):
 726        # With changed data.
 727        with mock.patch.object(self.table, '_put_item', return_value=True) \
 728                as mock_put_item:
 729            self.johndoe['first_name'] = 'J'
 730            self.johndoe['new_attr'] = 'never_seen_before'
 731            # OVERWRITE ALL THE THINGS
 732            self.assertTrue(self.johndoe.save(overwrite=True))
 733            self.assertFalse(self.johndoe.needs_save())
 734
 735        self.assertTrue(mock_put_item.called)
 736        mock_put_item.assert_called_once_with({
 737            'username': {'S': 'johndoe'},
 738            'first_name': {'S': 'J'},
 739            'new_attr': {'S': 'never_seen_before'},
 740            'date_joined': {'N': '12345'}
 741        }, expects=None)
 742
 743    def test_partial_no_changes(self):
 744        # Unchanged, no save.
 745        with mock.patch.object(self.table, '_update_item', return_value=True) \
 746                as mock_update_item:
 747            # Pretend we loaded it via ``get_item``...
 748            self.johndoe.mark_clean()
 749            self.assertFalse(self.johndoe.partial_save())
 750
 751        self.assertFalse(mock_update_item.called)
 752
 753    def test_partial_with_changes(self):
 754        # Setup the data.
 755        self.table.schema = [
 756            HashKey('username'),
 757        ]
 758
 759        # With changed data.
 760        with mock.patch.object(self.table, '_update_item', return_value=True) \
 761                as mock_update_item:
 762            # Pretend we loaded it via ``get_item``...
 763            self.johndoe.mark_clean()
 764            # Now... MODIFY!!!
 765            self.johndoe['first_name'] = 'J'
 766            self.johndoe['last_name'] = 'Doe'
 767            del self.johndoe['date_joined']
 768            self.assertTrue(self.johndoe.partial_save())
 769            self.assertFalse(self.johndoe.needs_save())
 770
 771        self.assertTrue(mock_update_item.called)
 772        mock_update_item.assert_called_once_with({
 773            'username': 'johndoe',
 774        }, {
 775            'first_name': {
 776                'Action': 'PUT',
 777                'Value': {'S': 'J'},
 778            },
 779            'last_name': {
 780                'Action': 'PUT',
 781                'Value': {'S': 'Doe'},
 782            },
 783            'date_joined': {
 784                'Action': 'DELETE',
 785            }
 786        }, expects={
 787            'first_name': {
 788                'Value': {
 789                    'S': 'John',
 790                },
 791                'Exists': True
 792            },
 793            'last_name': {
 794                'Exists': False
 795            },
 796            'date_joined': {
 797                'Value': {
 798                    'N': '12345',
 799                },
 800                'Exists': True
 801            },
 802        })
 803
 804    def test_delete(self):
 805        # Setup the data.
 806        self.table.schema = [
 807            HashKey('username'),
 808            RangeKey('date_joined'),
 809        ]
 810
 811        with mock.patch.object(self.table, 'delete_item', return_value=True) \
 812                as mock_delete_item:
 813            self.johndoe.delete()
 814
 815        self.assertTrue(mock_delete_item.called)
 816        mock_delete_item.assert_called_once_with(
 817            username='johndoe',
 818            date_joined=12345
 819        )
 820
 821    def test_nonzero(self):
 822        self.assertTrue(self.johndoe)
 823        self.assertFalse(self.create_item({}))
 824
 825
 826class ItemFromItemTestCase(ItemTestCase):
 827    def setUp(self):
 828        super(ItemFromItemTestCase, self).setUp()
 829        self.johndoe = self.create_item(self.johndoe)
 830
 831
 832def fake_results(name, greeting='hello', exclusive_start_key=None, limit=None):
 833    if exclusive_start_key is None:
 834        exclusive_start_key = -1
 835
 836    if limit == 0:
 837        raise Exception("Web Service Returns '400 Bad Request'")
 838
 839    end_cap = 13
 840    results = []
 841    start_key = exclusive_start_key + 1
 842
 843    for i in range(start_key, start_key + 5):
 844        if i < end_cap:
 845            results.append("%s %s #%s" % (greeting, name, i))
 846
 847    # Don't return more than limit results
 848    if limit < len(results):
 849        results = results[:limit]
 850
 851    retval = {
 852        'results': results,
 853    }
 854
 855    if exclusive_start_key + 5 < end_cap:
 856        retval['last_key'] = exclusive_start_key + 5
 857
 858    return retval
 859
 860
 861class ResultSetTestCase(unittest.TestCase):
 862    def setUp(self):
 863        super(ResultSetTestCase, self).setUp()
 864        self.results = ResultSet()
 865        self.result_function = mock.MagicMock(side_effect=fake_results)
 866        self.results.to_call(self.result_function, 'john', greeting='Hello', limit=20)
 867
 868    def test_first_key(self):
 869        self.assertEqual(self.results.first_key, 'exclusive_start_key')
 870
 871    def test_max_page_size_fetch_more(self):
 872        self.results = ResultSet(max_page_size=10)
 873        self.results.to_call(self.result_function, 'john', greeting='Hello')
 874        self.results.fetch_more()
 875        self.result_function.assert_called_with('john', greeting='Hello', limit=10)
 876        self.result_function.reset_mock()
 877
 878    def test_max_page_size_and_smaller_limit_fetch_more(self):
 879        self.results = ResultSet(max_page_size=10)
 880        self.results.to_call(self.result_function, 'john', greeting='Hello', limit=5)
 881        self.results.fetch_more()
 882        self.result_function.assert_called_with('john', greeting='Hello', limit=5)
 883        self.result_function.reset_mock()
 884
 885    def test_max_page_size_and_bigger_limit_fetch_more(self):
 886        self.results = ResultSet(max_page_size=10)
 887        self.results.to_call(self.result_function, 'john', greeting='Hello', limit=15)
 888        self.results.fetch_more()
 889        self.result_function.assert_called_with('john', greeting='Hello', limit=10)
 890        self.result_function.reset_mock()
 891
 892    def test_fetch_more(self):
 893        # First "page".
 894        self.results.fetch_more()
 895        self.assertEqual(self.results._results, [
 896            'Hello john #0',
 897            'Hello john #1',
 898            'Hello john #2',
 899            'Hello john #3',
 900            'Hello john #4',
 901        ])
 902
 903        self.result_function.assert_called_with('john', greeting='Hello', limit=20)
 904        self.result_function.reset_mock()
 905
 906        # Fake in a last key.
 907        self.results._last_key_seen = 4
 908        # Second "page".
 909        self.results.fetch_more()
 910        self.assertEqual(self.results._results, [
 911            'Hello john #5',
 912            'Hello john #6',
 913            'Hello john #7',
 914            'Hello john #8',
 915            'Hello john #9',
 916        ])
 917
 918        self.result_function.assert_called_with('john', greeting='Hello', limit=20, exclusive_start_key=4)
 919        self.result_function.reset_mock()
 920
 921        # Fake in a last key.
 922        self.results._last_key_seen = 9
 923        # Last "page".
 924        self.results.fetch_more()
 925        self.assertEqual(self.results._results, [
 926            'Hello john #10',
 927            'Hello john #11',
 928            'Hello john #12',
 929        ])
 930
 931        # Fake in a key outside the range.
 932        self.results._last_key_seen = 15
 933        # Empty "page". Nothing new gets added
 934        self.results.fetch_more()
 935        self.assertEqual(self.results._results, [])
 936
 937        # Make sure we won't check for results in the future.
 938        self.assertFalse(self.results._results_left)
 939
 940    def test_iteration(self):
 941        # First page.
 942        self.assertEqual(next(self.results), 'Hello john #0')
 943        self.assertEqual(next(self.results), 'Hello john #1')
 944        self.assertEqual(next(self.results), 'Hello john #2')
 945        self.assertEqual(next(self.results), 'Hello john #3')
 946        self.assertEqual(next(self.results), 'Hello john #4')
 947        self.assertEqual(self.results._limit, 15)
 948        # Second page.
 949        self.assertEqual(next(self.results), 'Hello john #5')
 950        self.assertEqual(next(self.results), 'Hello john #6')
 951        self.assertEqual(next(self.results), 'Hello john #7')
 952        self.assertEqual(next(self.results), 'Hello john #8')
 953        self.assertEqual(next(self.results), 'Hello john #9')
 954        self.assertEqual(self.results._limit, 10)
 955        # Third page.
 956        self.assertEqual(next(self.results), 'Hello john #10')
 957        self.assertEqual(next(self.results), 'Hello john #11')
 958        self.assertEqual(next(self.results), 'Hello john #12')
 959        self.assertRaises(StopIteration, self.results.next)
 960        self.assertEqual(self.results._limit, 7)
 961
 962    def test_limit_smaller_than_first_page(self):
 963        results = ResultSet()
 964        results.to_call(fake_results, 'john', greeting='Hello', limit=2)
 965        self.assertEqual(next(results), 'Hello john #0')
 966        self.assertEqual(next(results), 'Hello john #1')
 967        self.assertRaises(StopIteration, results.next)
 968
 969    def test_limit_equals_page(self):
 970        results = ResultSet()
 971        results.to_call(fake_results, 'john', greeting='Hello', limit=5)
 972        # First page
 973        self.assertEqual(next(results), 'Hello john #0')
 974        self.assertEqual(next(results), 'Hello john #1')
 975        self.assertEqual(next(results), 'Hello john #2')
 976        self.assertEqual(next(results), 'Hello john #3')
 977        self.assertEqual(next(results), 'Hello john #4')
 978        self.assertRaises(StopIteration, results.next)
 979
 980    def test_limit_greater_than_page(self):
 981        results = ResultSet()
 982        results.to_call(fake_results, 'john', greeting='Hello', limit=6)
 983        # First page
 984        self.assertEqual(next(results), 'Hello john #0')
 985        self.assertEqual(next(results), 'Hello john #1')
 986        self.assertEqual(next(results), 'Hello john #2')
 987        self.assertEqual(next(results), 'Hello john #3')
 988        self.assertEqual(next(results), 'Hello john #4')
 989        # Second page
 990        self.assertEqual(next(results), 'Hello john #5')
 991        self.assertRaises(StopIteration, results.next)
 992
 993    def test_iteration_noresults(self):
 994        def none(limit=10):
 995            return {
 996                'results': [],
 997            }
 998
 999        results = ResultSet()
1000        results.to_call(none, limit=20)
1001        self.assertRaises(StopIteration, results.next)
1002
1003    def test_iteration_sporadic_pages(self):
1004        # Some pages have no/incomplete results but have a ``LastEvaluatedKey``
1005        # (for instance, scans with filters), so we need to accommodate that.
1006        def sporadic():
1007            # A dict, because Python closures have read-only access to the
1008            # reference itself.
1009            count = {'value': -1}
1010
1011            def _wrapper(limit=10, exclusive_start_key=None):
1012                count['value'] = count['value'] + 1
1013
1014                if count['value'] == 0:
1015                    # Full page.
1016                    return {
1017                        'results': [
1018                            'Result #0',
1019                            'Result #1',
1020                            'Result #2',
1021                            'Result #3',
1022                        ],
1023                        'last_key': 'page-1'
1024                    }
1025                elif count['value'] == 1:
1026                    # Empty page but continue.
1027                    return {
1028                        'results': [],
1029                        'last_key': 'page-2'
1030                    }
1031                elif count['value'] == 2:
1032                    # Final page.
1033                    return {
1034                        'results': [
1035                            'Result #4',
1036                            'Result #5',
1037                            'Result #6',
1038                        ],
1039                    }
1040
1041            return _wrapper
1042
1043        results = ResultSet()
1044        results.to_call(sporadic(), limit=20)
1045        # First page
1046        self.assertEqual(next(results), 'Result #0')
1047        self.assertEqual(next(results), 'Result #1')
1048        self.assertEqual(next(results), 'Result #2')
1049        self.assertEqual(next(results), 'Result #3')
1050        # Second page (misses!)
1051        # Moves on to the third page
1052        self.assertEqual(next(results), 'Result #4')
1053        self.assertEqual(next(results), 'Result #5')
1054        self.assertEqual(next(results), 'Result #6')
1055        self.assertRaises(StopIteration, results.next)
1056
1057    def test_list(self):
1058        self.assertEqual(list(self.results), [
1059            'Hello john #0',
1060            'Hello john #1',
1061            'Hello john #2',
1062            'Hello john #3',
1063            'Hello john #4',
1064            'Hello john #5',
1065            'Hello john #6',
1066            'Hello john #7',
1067            'Hello john #8',
1068            'Hello john #9',
1069            'Hello john #10',
1070            'Hello john #11',
1071            'Hello john #12'
1072        ])
1073
1074
1075def fake_batch_results(keys):
1076    results = []
1077    simulate_unprocessed = True
1078
1079    if len(keys) and keys[0] == 'johndoe':
1080        simulate_unprocessed = False
1081
1082    for key in keys:
1083        if simulate_unprocessed and key == 'johndoe':
1084            continue
1085
1086        results.append("hello %s" % key)
1087
1088    retval = {
1089        'results': results,
1090        'last_key': None,
1091    }
1092
1093    if simulate_unprocessed:
1094        retval['unprocessed_keys'] = ['johndoe']
1095
1096    return retval
1097
1098
1099class BatchGetResultSetTestCase(unittest.TestCase):
1100    def setUp(self):
1101        super(BatchGetResultSetTestCase, self).setUp()
1102        self.results = BatchGetResultSet(keys=[
1103            'alice',
1104            'bob',
1105            'jane',
1106            'johndoe',
1107        ])
1108        self.results.to_call(fake_batch_results)
1109
1110    def test_fetch_more(self):
1111        # First "page".
1112        self.results.fetch_more()
1113        self.assertEqual(self.results._results, [
1114            'hello alice',
1115            'hello bob',
1116            'hello jane',
1117        ])
1118        self.assertEqual(self.results._keys_left, ['johndoe'])
1119
1120        # Second "page".
1121        self.results.fetch_more()
1122        self.assertEqual(self.results._results, [
1123            'hello johndoe',
1124        ])
1125
1126        # Empty "page". Nothing new gets added
1127        self.results.fetch_more()
1128        self.assertEqual(self.results._results, [])
1129
1130        # Make sure we won't check for results in the future.
1131        self.assertFalse(self.results._results_left)
1132
1133    def test_fetch_more_empty(self):
1134        self.results.to_call(lambda keys: {'results': [], 'last_key': None})
1135
1136        self.results.fetch_more()
1137        self.assertEqual(self.results._results, [])
1138        self.assertRaises(StopIteration, self.results.next)
1139
1140    def test_iteration(self):
1141        # First page.
1142        self.assertEqual(next(self.results), 'hello alice')
1143        self.assertEqual(next(self.results), 'hello bob')
1144        self.assertEqual(next(self.results), 'hello jane')
1145        self.assertEqual(next(self.results), 'hello johndoe')
1146        self.assertRaises(StopIteration, self.results.next)
1147
1148
1149class TableTestCase(unittest.TestCase):
1150    def setUp(self):
1151        super(TableTestCase, self).setUp()
1152        self.users = Table('users', connection=FakeDynamoDBConnection())
1153        self.default_connection = DynamoDBConnection(
1154            aws_access_key_id='access_key',
1155            aws_secret_access_key='secret_key'
1156        )
1157
1158    def test__introspect_schema(self):
1159        raw_schema_1 = [
1160            {
1161                "AttributeName": "username",
1162                "KeyType": "HASH"
1163            },
1164            {
1165                "AttributeName": "date_joined",
1166                "KeyType": "RANGE"
1167            }
1168        ]
1169        raw_attributes_1 = [
1170            {
1171                'AttributeName': 'username',
1172                'AttributeType': 'S'
1173            },
1174            {
1175                'AttributeName': 'date_joined',
1176                'AttributeType': 'S'
1177            },
1178        ]
1179        schema_1 = self.users._introspect_schema(raw_schema_1, raw_attributes_1)
1180        self.assertEqual(len(schema_1), 2)
1181        self.assertTrue(isinstance(schema_1[0], HashKey))
1182        self.assertEqual(schema_1[0].name, 'username')
1183        self.assertTrue(isinstance(schema_1[1], RangeKey))
1184        self.assertEqual(schema_1[1].name, 'date_joined')
1185
1186        raw_schema_2 = [
1187            {
1188                "AttributeName": "username",
1189                "KeyType": "BTREE"
1190            },
1191        ]
1192        raw_attributes_2 = [
1193            {
1194                'AttributeName': 'username',
1195                'AttributeType': 'S'
1196            },
1197        ]
1198        self.assertRaises(
1199            exceptions.UnknownSchemaFieldError,
1200            self.users._introspect_schema,
1201            raw_schema_2,
1202            raw_attributes_2
1203        )
1204
1205        # Test a complex schema & ensure the types come back correctly.
1206        raw_schema_3 = [
1207            {
1208                "AttributeName": "user_id",
1209                "KeyType": "HASH"
1210            },
1211            {
1212                "AttributeName": "junk",
1213                "KeyType": "RANGE"
1214            }
1215        ]
1216        raw_attributes_3 = [
1217            {
1218                'AttributeName': 'user_id',
1219                'AttributeType': 'N'
1220            },
1221            {
1222                'AttributeName': 'junk',
1223                'AttributeType': 'B'
1224            },
1225        ]
1226        schema_3 = self.users._introspect_schema(raw_schema_3, raw_attributes_3)
1227        self.assertEqual(len(schema_3), 2)
1228        self.assertTrue(isinstance(schema_3[0], HashKey))
1229        self.assertEqual(schema_3[0].name, 'user_id')
1230        self.assertEqual(schema_3[0].data_type, NUMBER)
1231        self.assertTrue(isinstance(schema_3[1], RangeKey))
1232        self.assertEqual(schema_3[1].name, 'junk')
1233        self.assertEqual(schema_3[1].data_type, BINARY)
1234
1235    def test__introspect_indexes(self):
1236        raw_indexes_1 = [
1237            {
1238                "IndexName": "MostRecentlyJoinedIndex",
1239                "KeySchema": [
1240                    {
1241                        "AttributeName": "username",
1242                        "KeyType": "HASH"
1243                    },
1244                    {
1245                        "AttributeName": "date_joined",
1246                        "KeyType": "RANGE"
1247                    }
1248                ],
1249                "Projection": {
1250                    "ProjectionType": "KEYS_ONLY"
1251                }
1252            },
1253            {
1254                "IndexName": "EverybodyIndex",
1255                "KeySchema": [
1256                    {
1257                        "AttributeName": "username",
1258                        "KeyType": "HASH"
1259                    },
1260                ],
1261                "Projection": {
1262                    "ProjectionType": "ALL"
1263                }
1264            },
1265            {
1266                "IndexName": "GenderIndex",
1267                "KeySchema": [
1268                    {
1269                        "AttributeName": "username",
1270                        "KeyType": "HASH"
1271                    },
1272                    {
1273                        "AttributeName": "date_joined",
1274                        "KeyType": "RANGE"
1275                    }
1276                ],
1277                "Projection": {
1278                    "ProjectionType": "INCLUDE",
1279                    "NonKeyAttributes": [
1280                        'gender',
1281                    ]
1282                }
1283            }
1284        ]
1285        indexes_1 = self.users._introspect_indexes(raw_indexes_1)
1286        self.assertEqual(len(indexes_1), 3)
1287        self.assertTrue(isinstance(indexes_1[0], KeysOnlyIndex))
1288        self.assertEqual(indexes_1[0].name, 'MostRecentlyJoinedIndex')
1289        self.assertEqual(len(indexes_1[0].parts), 2)
1290        self.assertTrue(isinstance(indexes_1[1], AllIndex))
1291        self.assertEqual(indexes_1[1].name, 'EverybodyIndex')
1292        self.assertEqual(len(indexes_1[1].parts), 1)
1293        self.assertTrue(isinstance(indexes_1[2], IncludeIndex))
1294        self.assertEqual(indexes_1[2].name, 'GenderIndex')
1295        self.assertEqual(len(indexes_1[2].parts), 2)
1296        self.assertEqual(indexes_1[2].includes_fields, ['gender'])
1297
1298        raw_indexes_2 = [
1299            {
1300                "IndexName": "MostRecentlyJoinedIndex",
1301                "KeySchema": [
1302                    {
1303                        "AttributeName": "username",
1304                        "KeyType": "HASH"
1305                    },
1306                    {
1307                        "AttributeName": "date_joined",
1308                        "KeyType": "RANGE"
1309                    }
1310                ],
1311                "Projection": {
1312                    "ProjectionType": "SOMETHING_CRAZY"
1313                }
1314            },
1315        ]
1316        self.assertRaises(
1317            exceptions.UnknownIndexFieldError,
1318            self.users._introspect_indexes,
1319            raw_indexes_2
1320        )
1321
1322    def test_initialization(self):
1323        users = Table('users', connection=self.default_connection)
1324        self.assertEqual(users.table_name, 'users')
1325        self.assertTrue(isinstance(users.connection, DynamoDBConnection))
1326        self.assertEqual(users.throughput['read'], 5)
1327        self.assertEqual(users.throughput['write'], 5)
1328        self.assertEqual(users.schema, None)
1329        self.assertEqual(users.indexes, None)
1330
1331        groups = Table('groups', connection=FakeDynamoDBConnection())
1332        self.assertEqual(groups.table_name, 'groups')
1333        self.assertTrue(hasattr(groups.connection, 'assert_called_once_with'))
1334
1335    def test_create_simple(self):
1336        conn = FakeDynamoDBConnection()
1337
1338        with mock.patch.object(conn, 'create_table', return_value={}) \
1339                as mock_create_table:
1340            retval = Table.create('users', schema=[
1341                HashKey('username'),
1342                RangeKey('date_joined', data_type=NUMBER)
1343            ], connection=conn)
1344            self.assertTrue(retval)
1345
1346        self.assertTrue(mock_create_table.called)
1347        mock_create_table.assert_called_once_with(attribute_definitions=[
1348            {
1349                'AttributeName': 'username',
1350                'AttributeType': 'S'
1351            },
1352            {
1353                'AttributeName': 'date_joined',
1354                'AttributeType': 'N'
1355            }
1356        ],
1357        table_name='users',
1358        key_schema=[
1359            {
1360                'KeyType': 'HASH',
1361                'AttributeName': 'username'
1362            },
1363            {
1364                'KeyType': 'RANGE',
1365                'AttributeName': 'date_joined'
1366            }
1367        ],
1368        provisioned_throughput={
1369            'WriteCapacityUnits': 5,
1370            'ReadCapacityUnits': 5
1371        })
1372
1373    def test_create_full(self):
1374        conn = FakeDynamoDBConnection()
1375
1376        with mock.patch.object(conn, 'create_table', return_value={}) \
1377                as mock_create_table:
1378            retval = Table.create('users', schema=[
1379                HashKey('username'),
1380                RangeKey('date_joined', data_type=NUMBER)
1381            ], throughput={
1382                'read':20,
1383                'write': 10,
1384            }, indexes=[
1385                KeysOnlyIndex('FriendCountIndex', parts=[
1386                    RangeKey('friend_count')
1387                ]),
1388            ], global_indexes=[
1389                GlobalKeysOnlyIndex('FullFriendCountIndex', parts=[
1390                    RangeKey('friend_count')
1391                ], throughput={
1392                    'read': 10,
1393                    'write': 8,
1394                }),
1395            ], connection=conn)
1396            self.assertTrue(retval)
1397
1398        self.assertTrue(mock_create_table.called)
1399        mock_create_table.assert_called_once_with(attribute_definitions=[
1400            {
1401                'AttributeName': 'username',
1402                'AttributeType': 'S'
1403            },
1404            {
1405                'AttributeName': 'date_joined',
1406                'AttributeType': 'N'
1407            },
1408            {
1409                'AttributeName': 'friend_count',
1410                'AttributeType': 'S'
1411            }
1412        ],
1413        key_schema=[
1414            {
1415                'KeyType': 'HASH',
1416                'AttributeName': 'username'
1417            },
1418            {
1419                'KeyType': 'RANGE',
1420                'AttributeName': 'date_joined'
1421            }
1422        ],
1423        table_name='users',
1424        provisioned_throughput={
1425            'WriteCapacityUnits': 10,
1426            'ReadCapacityUnits': 20
1427        },
1428        global_secondary_indexes=[
1429            {
1430                'KeySchema': [
1431                    {
1432                        'KeyType': 'RANGE',
1433                        'AttributeName': 'friend_count'
1434                    }
1435                ],
1436                'IndexName': 'FullFriendCountIndex',
1437                'Projection': {
1438                    'ProjectionType': 'KEYS_ONLY'
1439                },
1440                'ProvisionedThroughput': {
1441                    'WriteCapacityUnits': 8,
1442                    'ReadCapacityUnits': 10
1443                }
1444            }
1445        ],
1446        local_secondary_indexes=[
1447            {
1448                'KeySchema': [
1449                    {
1450                        'KeyType': 'RANGE',
1451                        'AttributeName': 'friend_count'
1452                    }
1453                ],
1454                'IndexName': 'FriendCountIndex',
1455                'Projection': {
1456                    'ProjectionType': 'KEYS_ONLY'
1457                }
1458            }
1459        ])
1460
1461    def test_describe(self):
1462        expected = {
1463            "Table": {
1464                "AttributeDefinitions": [
1465                    {
1466                        "AttributeName": "username",
1467                        "AttributeType": "S"
1468                    }
1469                ],
1470                "ItemCount": 5,
1471                "KeySchema": [
1472                    {
1473                        "AttributeName": "username",
1474                        "KeyType": "HASH"
1475                    }
1476                ],
1477                "LocalSecondaryIndexes": [
1478                    {
1479                        "IndexName": "UsernameIndex",
1480                        "KeySchema": [
1481                            {
1482                                "AttributeName": "username",
1483                                "KeyType": "HASH"
1484                            }
1485                        ],
1486                        "Projection": {
1487                            "ProjectionType": "KEYS_ONLY"
1488                        }
1489                    }
1490                ],
1491                "ProvisionedThroughput": {
1492                    "ReadCapacityUnits": 20,
1493                    "WriteCapacityUnits": 6
1494                },
1495                "TableName": "Thread",
1496                "TableStatus": "ACTIVE"
1497            }
1498        }
1499
1500        with mock.patch.object(
1501                self.users.connection,
1502                'describe_table',
1503                return_value=expected) as mock_describe:
1504            self.assertEqual(self.users.throughput['read'], 5)
1505            self.assertEqual(self.users.throughput['write'], 5)
1506            self.assertEqual(self.users.schema, None)
1507            self.assertEqual(self.users.indexes, None)
1508
1509            self.users.describe()
1510
1511            self.assertEqual(self.users.throughput['read'], 20)
1512            self.assertEqual(self.users.throughput['write'], 6)
1513            self.assertEqual(len(self.users.schema), 1)
1514            self.assertEqual(isinstance(self.users.schema[0], HashKey), 1)
1515            self.assertEqual(len(self.users.indexes), 1)

Large files files are truncated, but you can click here to view the full file