require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') describe "Dynamoid::Persistence" do before do Random.stubs(:rand).with(Dynamoid::Config.partition_size).returns(0) @address = Address.new end context 'without AWS keys' do unless ENV['ACCESS_KEY'] && ENV['SECRET_KEY'] before do Dynamoid::Adapter.delete_table(Address.table_name) if Dynamoid::Adapter.list_tables.include?(Address.table_name) end it 'creates a table' do Address.create_table(:table_name => Address.table_name) Dynamoid::Adapter.list_tables.should include 'dynamoid_tests_addresses' end it 'checks if a table already exists' do Address.create_table(:table_name => Address.table_name) Address.table_exists?(Address.table_name).should be_true Address.table_exists?('crazytable').should be_false end end end it 'assigns itself an id on save' do @address.save Dynamoid::Adapter.read("dynamoid_tests_addresses", @address.id)[:id].should == @address.id end it 'prevents concurrent writes to tables with a lock_version' do @address.save! a1 = @address a2 = Address.find(@address.id) a1.city = 'Seattle' a2.city = 'San Francisco' a1.save! expect { a2.save! }.to raise_exception(Dynamoid::Errors::ConditionalCheckFailedException) end configured_with 'partitioning' do it 'raises an error when attempting to use optimistic locking' do expect { address.save! }.to raise_exception end end it 'assigns itself an id on save only if it does not have one' do @address.id = 'test123' @address.save Dynamoid::Adapter.read("dynamoid_tests_addresses", 'test123').should_not be_empty end it 'has a table name' do Address.table_name.should == 'dynamoid_tests_addresses' end it 'saves indexes along with itself' do @user = User.new(:name => 'Josh') @user.expects(:save_indexes).once.returns(true) @user.save end it 'deletes an item completely' do @user = User.create(:name => 'Josh') @user.destroy Dynamoid::Adapter.read("dynamoid_tests_users", @user.id).should be_nil end it 'keeps string attributes as strings' do @user = User.new(:name => 'Josh') @user.send(:dump)[:name].should == 'Josh' end it 'dumps datetime attributes' do @user = User.create(:name => 'Josh') @user.send(:dump)[:name].should == 'Josh' end it 'dumps integer attributes' do @subscription = Subscription.create(:length => 10) @subscription.send(:dump)[:length].should == 10 end it 'dumps set attributes' do @subscription = Subscription.create(:length => 10) @magazine = @subscription.magazine.create @subscription.send(:dump)[:magazine_ids].should == Set[@magazine.id] end it 'handles nil attributes properly' do Address.undump(nil).should be_a(Hash) end it 'dumps and undump a serialized field' do @address.options = (hash = {:x => [1, 2], "foobar" => 3.14}) Address.undump(@address.send(:dump))[:options].should == hash end [true, false].each do |bool| it "dumps a #{bool} boolean field" do @address.deliverable = bool Address.undump(@address.send(:dump))[:deliverable].should == bool end end it 'raises on an invalid boolean value' do expect do @address.deliverable = true data = @address.send(:dump) data[:deliverable] = 'foo' Address.undump(data) end.to raise_error(ArgumentError) end it 'loads a hash into a serialized field' do hash = {foo: :bar} Address.new(options: hash).options.should == hash end it 'loads attributes from a hash' do @time = DateTime.now @hash = {:name => 'Josh', :created_at => @time.to_f} User.undump(@hash)[:name].should == 'Josh' User.undump(@hash)[:created_at].to_f == @time.to_f end it 'runs the before_create callback only once' do CamelCase.any_instance.expects(:doing_before_create).once.returns(true) CamelCase.create end it 'runs after save callbacks when doing #create' do CamelCase.any_instance.expects(:doing_after_create).once.returns(true) CamelCase.create end it 'runs after save callbacks when doing #save' do CamelCase.any_instance.expects(:doing_after_create).once.returns(true) CamelCase.new.save end it 'works with a HashWithIndifferentAccess' do hash = ActiveSupport::HashWithIndifferentAccess.new("city" => "Atlanta") lambda {Address.create(hash)}.should_not raise_error end context 'create' do { Tweet => ['with range', { :tweet_id => 1, :group => 'abc' }], Message => ['without range', { :message_id => 1, :text => 'foo', :time => DateTime.now }] }.each_pair do |clazz, fields| it "checks for existence of an existing object #{fields[0]}" do t1 = clazz.new(fields[1]) t2 = clazz.new(fields[1]) t1.save expect do t2.save! end.to raise_exception Dynamoid::Errors::ConditionalCheckFailedException end end end context 'unknown fields' do let(:clazz) do Class.new do include Dynamoid::Document table :name => :addresses field :city field :options, :serialized field :deliverable, :bad_type_specifier end end it 'raises when undumping a column with an unknown field type' do expect do clazz.new(:deliverable => true) #undump is called here end.to raise_error(ArgumentError) end it 'raises when dumping a column with an unknown field type' do doc = clazz.new doc.deliverable = true expect do doc.dump end.to raise_error(ArgumentError) end end context 'update' do before :each do @tweet = Tweet.create(:tweet_id => 1, :group => 'abc', :count => 5, :tags => ['db', 'sql'], :user_name => 'john') end it 'runs before_update callbacks when doing #update' do CamelCase.any_instance.expects(:doing_before_update).once.returns(true) CamelCase.create(:color => 'blue').update do |t| t.set(:color => 'red') end end it 'runs after_update callbacks when doing #update' do CamelCase.any_instance.expects(:doing_after_update).once.returns(true) CamelCase.create(:color => 'blue').update do |t| t.set(:color => 'red') end end it 'support add/delete operation on a field' do @tweet.update do |t| t.add(:count => 3) t.delete(:tags => ['db']) end @tweet.count.should eq(8) @tweet.tags.to_a.should eq(['sql']) end it 'checks the conditions on update' do @tweet.update(:if => { :count => 5 }) do |t| t.add(:count => 3) end.should be_true @tweet.count.should eq(8) @tweet.update(:if => { :count => 5 }) do |t| t.add(:count => 3) end.should be_false @tweet.count.should eq(8) expect { @tweet.update!(:if => { :count => 5 }) do |t| t.add(:count => 3) end }.to raise_error(Dynamoid::Errors::ConditionalCheckFailedException) end it 'prevents concurrent saves to tables with a lock_version' do @address.save! a2 = Address.find(@address.id) a2.update! { |a| a.set(:city => "Chicago") } expect do @address.city = "Seattle" @address.save! end.to raise_error(Dynamoid::Errors::ConditionalCheckFailedException) end end context 'delete' do it 'deletes model with datetime range key' do lambda { msg = Message.create!(:message_id => 1, :time => DateTime.now, :text => "Hell yeah") msg.destroy }.should_not raise_error end end context 'single table inheritance' do let(:car) { Car.create(power_locks: false) } let(:sub) { NuclearSubmarine.create(torpedoes: 5) } it 'saves subclass objects in the parent table' do c = car Vehicle.find(c.id).should == c end it 'loads subclass item when querying the parent table' do c = car s = sub Vehicle.all.to_a.tap { |v| v.should include(c) v.should include(s) } end end end