spec/unit/properties_spec.rb in couchrest_model-2.1.0.rc1 vs spec/unit/properties_spec.rb in couchrest_model-2.2.0.beta1

- old
+ new

@@ -1,37 +1,487 @@ # encoding: utf-8 require "spec_helper" describe CouchRest::Model::Properties do - before(:each) do - @obj = WithDefaultValues.new + context "general functionality" do + + before(:each) do + reset_test_db! + @card = Card.new(:first_name => "matt") + end + + it "should be accessible from the object" do + expect(@card.properties).to be_an_instance_of(Array) + expect(@card.properties.map{|p| p.name}).to include("first_name") + end + + it "should list object properties with values" do + expect(@card.attributes).to be_an_instance_of(Hash) + expect(@card.read_attributes).to be_an_instance_of(Hash) + expect(@card.attributes["first_name"]).to eql("matt") + end + + it "should let you access a property value (getter)" do + expect(@card.first_name).to eql("matt") + end + + it "should let you set a property value (setter)" do + @card.last_name = "Aimonetti" + expect(@card.last_name).to eq("Aimonetti") + end + + it "should not let you set a property value if it's read only" do + expect{@card.read_only_value = "test"}.to raise_error(NoMethodError) + end + + it "should let you use an alias for an attribute" do + @card.last_name = "Aimonetti" + expect(@card.family_name).to eq("Aimonetti") + expect(@card.family_name).to eq(@card.last_name) + end + + it "should let you use an alias for a casted attribute" do + @card.cast_alias = Person.new(:name => ["Aimonetti"]) + expect(@card.cast_alias.name).to eq(["Aimonetti"]) + expect(@card.calias.name).to eq(["Aimonetti"]) + card = Card.new(:first_name => "matt", :cast_alias => {:name => ["Aimonetti"]}) + expect(card.cast_alias.name).to eq(["Aimonetti"]) + expect(card.calias.name).to eq(["Aimonetti"]) + end + + it "should raise error if property name coincides with model type key" do + expect { Cat.property(Cat.model_type_key) }.to raise_error(/already used/) + end + + it "should not raise error if property name coincides with model type key on non-model" do + expect { Person.property(Article.model_type_key) }.not_to raise_error + end + + it "should be auto timestamped" do + expect(@card.created_at).to be_nil + expect(@card.updated_at).to be_nil + expect(@card.save).to be_truthy + expect(@card.created_at).not_to be_nil + expect(@card.updated_at).not_to be_nil + end + + describe "#as_couch_json" do + + it "should provide a simple hash from model" do + expect(@card.as_couch_json.class).to eql(Hash) + end + + it "should remove properties from Hash if value is nil" do + @card.last_name = nil + expect(@card.as_couch_json.keys.include?('last_name')).to be_falsey + end + + end + + describe "#as_json" do + + it "should provide a simple hash from model" do + expect(@card.as_json.class).to eql(Hash) + end + + it "should pass options to Active Support's as_json" do + @card.last_name = "Aimonetti" + expect(@card.as_json(:only => 'last_name')).to eql('last_name' => 'Aimonetti') + end + + end + + describe '#read_attribute' do + it "should let you use read_attribute method" do + @card.last_name = "Aimonetti" + expect(@card.read_attribute(:last_name)).to eql('Aimonetti') + expect(@card.read_attribute('last_name')).to eql('Aimonetti') + last_name_prop = @card.properties.find{|p| p.name == 'last_name'} + expect(@card.read_attribute(last_name_prop)).to eql('Aimonetti') + end + + it 'should raise an error if the property does not exist' do + expect { @card.read_attribute(:this_property_should_not_exist) }.to raise_error(ArgumentError) + end + end + + describe '#write_attribute' do + it "should let you use write_attribute method" do + @card.write_attribute(:last_name, 'Aimonetti 1') + expect(@card.last_name).to eql('Aimonetti 1') + @card.write_attribute('last_name', 'Aimonetti 2') + expect(@card.last_name).to eql('Aimonetti 2') + last_name_prop = @card.properties.find{|p| p.name == 'last_name'} + @card.write_attribute(last_name_prop, 'Aimonetti 3') + expect(@card.last_name).to eql('Aimonetti 3') + end + + it 'should raise an error if the property does not exist' do + expect { @card.write_attribute(:this_property_should_not_exist, 823) }.to raise_error(ArgumentError) + end + + it "should let you use write_attribute on readonly properties" do + expect { + @card.read_only_value = "foo" + }.to raise_error(NoMethodError) + @card.write_attribute(:read_only_value, "foo") + expect(@card.read_only_value).to eq('foo') + end + + it "should cast via write_attribute" do + @card.write_attribute(:cast_alias, {:name => ["Sam", "Lown"]}) + expect(@card.cast_alias.class).to eql(Person) + expect(@card.cast_alias.name.last).to eql("Lown") + end + + it "should not cast via write_attribute if property not casted" do + @card.write_attribute(:first_name, {:name => "Sam"}) + expect(@card.first_name.class).to eql(Hash) + expect(@card.first_name[:name]).to eql("Sam") + end + end + + # These tests are light as functionality is covered elsewhere + describe "#write_attributes" do + + let :obj do + Card.new(:first_name => "matt") + end + + it "should update attributes" do + obj.write_attributes( :last_name => 'foo' ) + expect(obj.last_name).to eql('foo') + end + + it "should not update protected attributes" do + obj.write_attributes(:bg_color => '#000000') + expect(obj.bg_color).to_not eql('#000000') + end + + it "should not update read_only attributes" do + obj.write_attributes(:read_only_value => 'bar') + expect(obj.read_only_value).to_not eql('bar') + end + + it "should have an #attributes= alias" do + expect { + obj.attributes = { :last_name => 'foo' } + }.to_not raise_error + end + + end + + # These tests are light as functionality is covered elsewhere + describe "#write_all_attributes" do + + let :obj do + Card.new(:first_name => "matt") + end + + it "should set regular properties" do + obj.write_all_attributes(:last_name => 'foo') + expect(obj.last_name).to eql('foo') + end + + it "should set read-only and protected properties" do + obj.write_all_attributes( + :read_only_value => 'foo', + :bg_color => '#111111' + ) + expect(obj.read_only_value).to eql('foo') + expect(obj.bg_color).to eql('#111111') + end + + end + + + describe "mass updating attributes without property" do + + describe "when mass_assign_any_attribute false" do + + it "should not allow them to be set" do + @card.attributes = {:test => 'fooobar'} + expect(@card['test']).to be_nil + end + + it 'should not allow them to be updated with update_attributes' do + @card.update_attributes(:test => 'fooobar') + expect(@card['test']).to be_nil + end + + it 'should not have a different revision after update_attributes' do + @card.save + rev = @card.rev + @card.update_attributes(:test => 'fooobar') + expect(@card.rev).to eql(rev) + end + + it 'should not have a different revision after save' do + @card.save + rev = @card.rev + @card.attributes = {:test => 'fooobar'} + @card.save + expect(@card.rev).to eql(rev) + end + + end + + describe "when mass_assign_any_attribute true" do + before(:each) do + # dup Card class so that no other tests are effected + card_class = Card.dup + card_class.class_eval do + mass_assign_any_attribute true + end + @card = card_class.new(:first_name => 'Sam') + end + + it 'should allow them to be updated' do + @card.attributes = {:testing => 'fooobar'} + expect(@card['testing']).to eql('fooobar') + end + + it 'should allow them to be updated with update_attributes' do + @card.update_attributes(:testing => 'fooobar') + expect(@card['testing']).to eql('fooobar') + end + + it 'should have a different revision after update_attributes' do + @card.save + rev = @card.rev + @card.update_attributes(:testing => 'fooobar') + expect(@card.rev).not_to eql(rev) + end + + it 'should have a different revision after save' do + @card.save + rev = @card.rev + @card.attributes = {:testing => 'fooobar'} + @card.save + expect(@card.rev).not_to eql(rev) + end + + end + end + + describe "mass assignment protection" do + + it "should not store protected attribute using mass assignment" do + cat_toy = CatToy.new(:name => "Zorro") + cat = Cat.create(:name => "Helena", :toys => [cat_toy], :favorite_toy => cat_toy, :number => 1) + expect(cat.number).to be_nil + cat.number = 1 + cat.save + expect(cat.number).to eq(1) + end + + it "should not store protected attribute when 'declare accessible poperties, assume all the rest are protected'" do + user = User.create(:name => "Marcos Tapajós", :admin => true) + expect(user.admin).to be_nil + end + + it "should not store protected attribute when 'declare protected properties, assume all the rest are accessible'" do + user = SpecialUser.create(:name => "Marcos Tapajós", :admin => true) + expect(user.admin).to be_nil + end + + end + + describe "validation" do + before(:each) do + @invoice = Invoice.new(:client_name => "matt", :employee_name => "Chris", :location => "San Diego, CA") + end + + it "should be able to be validated" do + expect(@card.valid?).to eq(true) + end + + it "should let you validate the presence of an attribute" do + @card.first_name = nil + expect(@card).not_to be_valid + expect(@card.errors).not_to be_empty + expect(@card.errors[:first_name]).to eq(["can't be blank"]) + end + + it "should let you look up errors for a field by a string name" do + @card.first_name = nil + expect(@card).not_to be_valid + expect(@card.errors['first_name']).to eq(["can't be blank"]) + end + + it "should validate the presence of 2 attributes" do + @invoice.clear + expect(@invoice).not_to be_valid + expect(@invoice.errors).not_to be_empty + expect(@invoice.errors[:client_name]).to eq(["can't be blank"]) + expect(@invoice.errors[:employee_name]).not_to be_empty + end + + it "should let you set an error message" do + @invoice.location = nil + @invoice.valid? + expect(@invoice.errors[:location]).to eq(["Hey stupid!, you forgot the location"]) + end + + it "should validate before saving" do + @invoice.location = nil + expect(@invoice).not_to be_valid + expect(@invoice.save).to be_falsey + expect(@invoice).to be_new + end + end + end + + context "casting" do + + describe "properties of hash of casted models" do + it "should be able to assign a casted hash to a hash property" do + chain = KeyChain.new + keys = {"House" => "8==$", "Office" => "<>==U"} + chain.keys = keys + chain.keys = chain.keys + expect(chain.keys).to eq(keys) + end + end + + describe "properties of array of casted models" do + + before(:each) do + @course = Course.new :title => 'Test Course' + end + + it "should allow attribute to be set from an array of objects" do + @course.questions = [Question.new(:q => "works?"), Question.new(:q => "Meaning of Life?")] + expect(@course.questions.length).to eql(2) + end + + it "should allow attribute to be set from an array of hashes" do + @course.questions = [{:q => "works?"}, {:q => "Meaning of Life?"}] + expect(@course.questions.length).to eql(2) + expect(@course.questions.last.q).to eql("Meaning of Life?") + expect(@course.questions.last.class).to eql(Question) # typecasting + end + + it "should allow attribute to be set from hash with ordered keys and objects" do + @course.questions = { '0' => Question.new(:q => "Test1"), '1' => Question.new(:q => 'Test2') } + expect(@course.questions.length).to eql(2) + expect(@course.questions.last.q).to eql('Test2') + expect(@course.questions.last.class).to eql(Question) + end + + it "should allow attribute to be set from hash with ordered keys and sub-hashes" do + @course.questions = { '10' => {:q => 'Test10'}, '0' => {:q => "Test1"}, '1' => {:q => 'Test2'} } + expect(@course.questions.length).to eql(3) + expect(@course.questions.last.q).to eql('Test10') + expect(@course.questions.last.class).to eql(Question) + end + + it "should allow attribute to be set from hash with ordered keys and HashWithIndifferentAccess" do + # This is similar to what you'd find in an HTML POST parameters + hash = HashWithIndifferentAccess.new({ '0' => {:q => "Test1"}, '1' => {:q => 'Test2'} }) + @course.questions = hash + expect(@course.questions.length).to eql(2) + expect(@course.questions.last.q).to eql('Test2') + expect(@course.questions.last.class).to eql(Question) + end + + it "should allow attribute to be set from Hash subclass with ordered keys" do + ourhash = Class.new(HashWithIndifferentAccess) + hash = ourhash.new({ '0' => {:q => "Test1"}, '1' => {:q => 'Test2'} }) + @course.questions = hash + expect(@course.questions.length).to eql(2) + expect(@course.questions.last.q).to eql('Test2') + expect(@course.questions.last.class).to eql(Question) + end + + it "should raise an error if attempting to set single value for array type" do + expect { + @course.questions = Question.new(:q => 'test1') + }.to raise_error(/Expecting an array/) + end + + + end + + describe "a casted model retrieved from the database" do + before(:each) do + reset_test_db! + @cat = Cat.new(:name => 'Stimpy') + @cat.favorite_toy = CatToy.new(:name => 'Stinky') + @cat.toys << CatToy.new(:name => 'Feather') + @cat.toys << CatToy.new(:name => 'Mouse') + @cat.save + @cat = Cat.get(@cat.id) + end + + describe "as a casted property" do + it "should already be casted_by its parent" do + expect(@cat.favorite_toy.casted_by).to be === @cat + end + end + + describe "from a casted collection" do + it "should already be casted_by its parent" do + expect(@cat.toys[0].casted_by).to be === @cat + expect(@cat.toys[1].casted_by).to be === @cat + end + end + end + + describe "nested models (not casted)" do + before(:each) do + reset_test_db! + @cat = ChildCat.new(:name => 'Stimpy') + @cat.mother = {:name => 'Stinky'} + @cat.siblings = [{:name => 'Feather'}, {:name => 'Felix'}] + @cat.save + @cat = ChildCat.get(@cat.id) + end + + it "should correctly save single relation" do + expect(@cat.mother.name).to eql('Stinky') + expect(@cat.mother.casted_by).to eql(@cat) + end + + it "should correctly save collection" do + expect(@cat.siblings.first.name).to eql("Feather") + expect(@cat.siblings.last.casted_by).to eql(@cat) + end + end + + end + + context "multipart attributes" do + + before(:each) do + @obj = WithDefaultValues.new + end - describe "multipart attributes" do context "with valid params" do it "should parse a legal date" do valid_date_params = { "exec_date(1i)"=>"2011", "exec_date(2i)"=>"10", "exec_date(3i)"=>"18"} @obj = WithDateAndTime.new valid_date_params - @obj.exec_date.should_not be_nil - @obj.exec_date.should be_kind_of(Date) - @obj.exec_date.should == Date.new(2011, 10 ,18) + expect(@obj.exec_date).not_to be_nil + expect(@obj.exec_date).to be_kind_of(Date) + expect(@obj.exec_date).to eq(Date.new(2011, 10 ,18)) end it "should parse a legal time" do valid_time_params = { "exec_time(1i)"=>"2011", "exec_time(2i)"=>"10", "exec_time(3i)"=>"18", "exec_time(4i)"=>"15", "exec_time(5i)"=>"15", "exec_time(6i)"=>"15",} @obj = WithDateAndTime.new valid_time_params - @obj.exec_time.should_not be_nil - @obj.exec_time.should be_kind_of(Time) - @obj.exec_time.should == Time.utc(2011, 10 ,18, 15, 15, 15) + expect(@obj.exec_time).not_to be_nil + expect(@obj.exec_time).to be_kind_of(Time) + expect(@obj.exec_time).to eq(Time.utc(2011, 10 ,18, 15, 15, 15)) end end context "with invalid params" do before(:each) do @@ -39,19 +489,19 @@ "exec_date(2i)"=>"foo", "exec_date(3i)"=>"18"} end it "should still create a model if there are invalid attributes" do @obj = WithDateAndTime.new @invalid_date_params - @obj.should_not be_nil - @obj.should be_kind_of(WithDateAndTime) + expect(@obj).not_to be_nil + expect(@obj).to be_kind_of(WithDateAndTime) end it "should not crash because of an empty value" do @invalid_date_params["exec_date(2i)"] = "" @obj = WithDateAndTime.new @invalid_date_params - @obj.should_not be_nil - @obj.exec_date.should_not be_kind_of(Date) - @obj.should be_kind_of(WithDateAndTime) + expect(@obj).not_to be_nil + expect(@obj.exec_date).not_to be_kind_of(Date) + expect(@obj).to be_kind_of(WithDateAndTime) end end # Specific use case for Ruby 2.0.0 context "with brackets in value" do @@ -62,12 +512,12 @@ end klass end it "should be accepted" do - lambda { + expect { @obj = klass.new(:name => 'test (object)') - }.should_not raise_error + }.not_to raise_error end end end