def test_save_set_only(engine, session): user = User(id="user_id") # Expect a SET on email user.email = "*****@*****.**" expected = { "Key": { "id": { "S": user.id } }, "ExpressionAttributeNames": { "#n0": "email" }, "TableName": "User", "ReturnValues": "NONE", "UpdateExpression": "SET #n0=:v1", "ExpressionAttributeValues": { ":v1": { "S": "*****@*****.**" } } } engine.save(user) session.save_item.assert_called_once_with(expected)
def wx(engine): """prepared write tx with one item""" user = User(id="numberoverzero") other = User(id="other") items = [ TxItem.new("save", user, condition=User.id.is_(None)), TxItem.new("delete", other), TxItem.new("check", other, condition=User.email.begins_with("foo")) ] tx = PreparedTransaction() tx.prepare(engine, "w", items) return tx
def test_load_objects(engine, session): user1 = User(id="user1") user2 = User(id="user2") expected = { "User": { "Keys": [{ "id": { "S": "user1" } }, { "id": { "S": "user2" } }], "ConsistentRead": False } } response = { "User": [{ "age": { "N": 5 }, "name": { "S": "foo" }, "id": { "S": "user1" } }, { "age": { "N": 10 }, "name": { "S": "bar" }, "id": { "S": "user2" } }] } def respond(RequestItems): assert ordered(RequestItems) == ordered(expected) return response session.load_items.side_effect = respond engine.load(user1, user2) assert user1.age == 5 assert user1.name == "foo" assert user2.age == 10 assert user2.name == "bar"
def test_delete_atomic(engine, session): user = User(id="user_id") # Tell the tracking system the user's id was saved to DynamoDB object_saved.send(engine, engine=engine, obj=user) expected = { "ConditionExpression": "(#n0 = :v1)", "ExpressionAttributeValues": { ":v1": { "S": user.id } }, "TableName": "User", "ReturnValues": "NONE", "Key": { "id": { "S": user.id } }, "ExpressionAttributeNames": { "#n0": "id" } } engine.delete(user, atomic=True) session.delete_item.assert_called_once_with(expected)
def rx(engine): """prepared read tx with one item""" user = User(id="numberoverzero") items = [TxItem.new("get", user)] tx = PreparedTransaction() tx.prepare(engine, "r", items) return tx
def test_delete_multiple_condition(engine, session, caplog): users = [User(id=str(i)) for i in range(3)] condition = User.id == "foo" expected_calls = [{ "Key": { "id": { "S": user.id } }, "ExpressionAttributeValues": { ":v1": { "S": "foo" } }, "ExpressionAttributeNames": { "#n0": "id" }, "ConditionExpression": "(#n0 = :v1)", "TableName": "User", "ReturnValues": "NONE", } for user in users] engine.delete(*users, condition=condition) for expected in expected_calls: session.delete_item.assert_any_call(expected) assert session.delete_item.call_count == 3 assert caplog.record_tuples[-1] == ("bloop.engine", logging.INFO, "successfully deleted 3 objects")
def test_delete_atomic_condition(engine, session): user = User(id="user_id", email="*****@*****.**") # Tell the tracking system the user's id and email were saved to DynamoDB object_saved.send(engine, engine=engine, obj=user) expected = { "ConditionExpression": "((#n0 = :v1) AND (#n2 = :v3) AND (#n4 = :v5))", "ExpressionAttributeValues": { ":v1": { "S": "foo" }, ":v3": { "S": "*****@*****.**" }, ":v5": { "S": user.id } }, "ExpressionAttributeNames": { "#n0": "name", "#n2": "email", "#n4": "id" }, "Key": { "id": { "S": user.id } }, "TableName": "User", "ReturnValues": "NONE", } engine.delete(user, condition=User.name.is_("foo"), atomic=True) session.delete_item.assert_called_once_with(expected)
def test_delete_atomic_new(engine, session): """atomic delete on new object should expect no columns to exist""" user = User(id="user_id") expected = { "TableName": "User", "ReturnValues": "NONE", "ExpressionAttributeNames": { "#n4": "id", "#n0": "age", "#n8": "name", "#n6": "j", "#n2": "email" }, "Key": { "id": { "S": user.id } }, "ConditionExpression": ("((attribute_not_exists(#n0)) AND (attribute_not_exists(#n2)) " "AND (attribute_not_exists(#n4)) AND (attribute_not_exists(#n6))" " AND (attribute_not_exists(#n8)))") } engine.delete(user, atomic=True) session.delete_item.assert_called_once_with(expected)
def test_save_twice(engine, session): """Save sends full local values, not just deltas from last save""" user = User(id="user_id", age=5) expected = { "Key": { "id": { "S": user.id } }, "TableName": "User", "ReturnValues": "NONE", "ExpressionAttributeNames": { "#n0": "age" }, "ExpressionAttributeValues": { ":v1": { "N": "5" } }, "UpdateExpression": "SET #n0=:v1", } engine.save(user) engine.save(user) session.save_item.assert_called_with(expected) assert session.save_item.call_count == 2
def test_dump_key(engine): user = User(id="foo") user_key = {"id": {"S": "foo"}} assert dump_key(engine, user) == user_key obj = HashAndRange(foo=4, bar=5) obj_key = {"bar": {"N": "5"}, "foo": {"N": "4"}} assert dump_key(engine, obj) == obj_key
def test_read_item(engine): engine.bind(User) user = User(id="numberoverzero") tx = ReadTransaction(engine) tx.load(user) p = tx.prepare() expected_items = [TxItem.new("get", user, None, False)] assert tx._items == expected_items assert p.items == expected_items assert p.first_commit_at is None
def test_load_missing_attrs(engine, session): """When an instance of a Model is loaded into, existing attributes should be overwritten with new values, or if there is no new value, should be deleted """ obj = User(id="user_id", age=4, name="user") response = {"User": [{"age": {"N": 5}, "id": {"S": obj.id}}]} session.load_items.return_value = response engine.load(obj) assert obj.age == 5 assert obj.name == ""
def test_load_equivalent_objects(engine, session): """Two objects with the same key are both loaded""" user = User(id="user_id") same_user = User(id=user.id) expected = { "User": { "Keys": [{ "id": { "S": user.id } }], "ConsistentRead": False } } response = { "User": [{ "age": { "N": 5 }, "name": { "S": "foo" }, "id": { "S": user.id } }] } def respond(RequestItems): assert ordered(RequestItems) == ordered(expected) return response session.load_items.side_effect = respond engine.load(user, same_user) assert user.age == 5 assert user.name == "foo" assert same_user.age == 5 assert same_user.name == "foo"
def test_missing_objects(engine, session, caplog): """When objects aren't loaded, MissingObjects is raised with a list of missing objects""" # Patch batch_get_items to return no results session.load_items.return_value = {} users = [User(id=str(i)) for i in range(3)] with pytest.raises(MissingObjects) as excinfo: engine.load(*users) assert set(excinfo.value.objects) == set(users) assert caplog.record_tuples == [("bloop.engine", logging.INFO, "loaded 0 of 3 objects")]
def test_delete_new(engine, session): """When an object is first created, a non-atomic delete shouldn't expect anything.""" user = User(id="user_id") expected = { "TableName": "User", "ReturnValues": "NONE", "Key": { "id": { "S": user.id } } } engine.delete(user) session.delete_item.assert_called_once_with(expected)
def test_dump_key(engine): class HashAndRange(BaseModel): foo = Column(Integer, hash_key=True) bar = Column(Integer, range_key=True) engine.bind(HashAndRange) user = User(id="foo") user_key = {"id": {"S": "foo"}} assert dump_key(engine, user) == user_key obj = HashAndRange(foo=4, bar=5) obj_key = {"bar": {"N": "5"}, "foo": {"N": "4"}} assert dump_key(engine, obj) == obj_key
def test_load_missing_key(engine): """Trying to load objects with missing hash and range keys raises""" user = User(age=2) with pytest.raises(MissingKey): engine.load(user) complex_models = [ ComplexModel(), ComplexModel(name=uuid.uuid4()), ComplexModel(date="no hash") ] for model in complex_models: with pytest.raises(MissingKey): engine.load(model)
def test_save_atomic_condition(engine, session): user = User(id="user_id") # Tell the tracking system the user's id was saved to DynamoDB object_saved.send(engine, engine=engine, obj=user) # Mutate a field; part of the update but not an expected condition user.name = "new_foo" # Condition on the mutated field with a different value condition = User.name == "expect_foo" expected = { "ConditionExpression": "((#n0 = :v1) AND (#n2 = :v3))", "ExpressionAttributeNames": { "#n0": "name", "#n2": "id" }, "ExpressionAttributeValues": { ":v1": { "S": "expect_foo" }, ":v3": { "S": user.id }, ":v4": { "S": "new_foo" } }, "Key": { "id": { "S": user.id } }, "TableName": "User", "ReturnValues": "NONE", "UpdateExpression": "SET #n0=:v4" } engine.save(user, condition=condition, atomic=True) session.save_item.assert_called_once_with(expected)
def test_abstract_object_operations_raise(engine, op_name, plural): class Abstract(BaseModel): class Meta: abstract = True engine.bind(Abstract) abstract = Abstract(id=5) concrete = User(age=5) with pytest.raises(InvalidModel): operation = getattr(engine, op_name) operation(abstract) if plural: with pytest.raises(InvalidModel): operation = getattr(engine, op_name) operation(abstract, concrete)
def test_save_single_with_condition(engine, session): user = User(id="user_id") condition = User.id.is_(None) expected = { "TableName": "User", "ReturnValues": "NONE", "ExpressionAttributeNames": { "#n0": "id" }, "ConditionExpression": "(attribute_not_exists(#n0))", "Key": { "id": { "S": user.id } } } engine.save(user, condition=condition) session.save_item.assert_called_once_with(expected)
def test_delete_sync(engine, session): """Engine.delete(sync='old') the previous values should be loaded and the object should be marked dirty""" session.delete_item.return_value = { "id": { "S": "user_id" }, "age": { "N": "3" } } user = User(id="user_id", age=4) engine.delete(user, sync="old") assert user.age == 3 assert global_tracking.get_snapshot(user) == (User.age.is_(None) & User.email.is_(None) & User.id.is_(None) & User.joined.is_(None) & User.name.is_(None))
def test_save_sync(engine, session, sync): """Engine.save(sync='old'|'new') the returned values should be loaded and the object should not be marked dirty""" session.save_item.return_value = { "id": { "S": "user_id" }, "age": { "N": "3" } } user = User(id="user_id", age=4) engine.save(user, sync=sync) assert user.age == 3 assert global_tracking.get_snapshot(user) == ( User.age.is_({"N": "3"}) & User.email.is_(None) & User.id.is_({"S": "user_id"}) & User.joined.is_(None) & User.name.is_(None))
def test_delete_complex_item(engine): engine.bind(User) user = User(id="numberoverzero") tx = WriteTransaction(engine) condition = User.id.begins_with("foo") tx.delete(user, condition=condition, atomic=True) p = tx.prepare() expected_items = [TxItem.new("delete", user, condition, True)] assert tx._items == expected_items assert p.items == expected_items assert p.first_commit_at is None assert len(p._request) == 1 entry = p._request[0]["Delete"] expected_fields = { "Key", "TableName", "ConditionExpression", "ExpressionAttributeNames", "ExpressionAttributeValues" } assert set(entry.keys()) == expected_fields
def test_save_del_only(engine, session): user = User(id="user_id", age=4) # Expect a REMOVE on age del user.age expected = { "Key": { "id": { "S": user.id } }, "ExpressionAttributeNames": { "#n0": "age" }, "TableName": "User", "ReturnValues": "NONE", "UpdateExpression": "REMOVE #n0" } engine.save(user) session.save_item.assert_called_once_with(expected)
def test_save_condition_key_only(engine, session): """Even when the diff is empty, an UpdateItem should be issued (in case this is really a create - the item doesn't exist yet) """ user = User(id="user_id") condition = User.id.is_(None) expected = { "ConditionExpression": "(attribute_not_exists(#n0))", "TableName": "User", "ReturnValues": "NONE", "ExpressionAttributeNames": { "#n0": "id" }, "Key": { "id": { "S": user.id } } } engine.save(user, condition=condition) session.save_item.assert_called_once_with(expected)
def test_save_list_with_condition(engine, session, caplog): users = [User(id=str(i)) for i in range(3)] condition = User.id.is_(None) expected_calls = [{ "ConditionExpression": "(attribute_not_exists(#n0))", "ExpressionAttributeNames": { "#n0": "id" }, "Key": { "id": { "S": user.id } }, "TableName": "User", "ReturnValues": "NONE", } for user in users] engine.save(*users, condition=condition) for expected in expected_calls: session.save_item.assert_any_call(expected) assert session.save_item.call_count == 3 assert caplog.record_tuples[-1] == ("bloop.engine", logging.INFO, "successfully saved 3 objects")
def test_load_object(engine, session): user_id = "user_id" expected = { "User": { "Keys": [{ "id": { "S": "user_id" } }], "ConsistentRead": True } } response = { "User": [{ "age": { "N": 5 }, "name": { "S": "foo" }, "id": { "S": "user_id" } }] } def respond(RequestItems): assert RequestItems == expected return response session.load_items.side_effect = respond user = User(id=user_id) engine.load(user, consistent=True) assert user.age == 5 assert user.name == "foo" assert user.id == user_id
def test_load_repeated_objects(engine, session): """The same object is only loaded once""" user = User(id="user_id") expected = { "User": { "Keys": [{ "id": { "S": user.id } }], "ConsistentRead": False } } response = { "User": [{ "age": { "N": 5 }, "name": { "S": "foo" }, "id": { "S": user.id } }], } def respond(RequestItems): assert ordered(RequestItems) == ordered(expected) return response session.load_items.side_effect = respond engine.load(user, user) assert user.age == 5 assert user.name == "foo"
def test_delete_unknown_sync(engine, sync): user = User(id="user_id", age=4) with pytest.raises(ValueError): engine.delete(user, sync=sync)