# encoding: UTF-8 require File.join(File.dirname(__FILE__), "test_helper") require "ostruct" class Post < Ohm::Model attribute :body list :comments list :related, Post end class User < Ohm::Model attribute :email set :posts, Post end class Person < Ohm::Model attribute :name index :initial def validate assert_present :name end def initial name[0, 1].upcase end end class Event < Ohm::Model attribute :name counter :votes set :attendees, Person attribute :slug def write self.slug = name.to_s.downcase super end end class ModelTest < Test::Unit::TestCase setup do Ohm.flush end context "An event initialized with a hash of attributes" do should "assign the passed attributes" do event = Event.new(:name => "Ruby Tuesday") assert_equal event.name, "Ruby Tuesday" end end context "An event created from a hash of attributes" do should "assign an id and save the object" do event1 = Event.create(:name => "Ruby Tuesday") event2 = Event.create(:name => "Ruby Meetup") assert_equal "1", event1.id assert_equal "2", event2.id end should "return the unsaved object if validation fails" do assert Person.create(:name => nil).kind_of?(Person) end end context "An event updated from a hash of attributes" do class Meetup < Ohm::Model attribute :name attribute :location def validate assert_present :name end end should "assign an id and save the object" do event = Meetup.create(:name => "Ruby Tuesday") event.update(:name => "Ruby Meetup") assert_equal "Ruby Meetup", event.name end should "return false if the validation fails" do event = Meetup.create(:name => "Ruby Tuesday") assert !event.update(:name => nil) end should "save the attributes in UTF8" do event = Meetup.create(:name => "32° Kisei-sen") assert_equal "32° Kisei-sen", Meetup[event.id].name end should "delete the attribute if set to nil" do event = Meetup.create(:name => "Ruby Tuesday", :location => "Los Angeles") assert_equal "Los Angeles", Meetup[event.id].location assert event.update(:location => nil) assert_equal nil, Meetup[event.id].location end should "delete the attribute if set to an empty string" do event = Meetup.create(:name => "Ruby Tuesday", :location => "Los Angeles") assert_equal "Los Angeles", Meetup[event.id].location assert event.update(:location => "") assert_equal nil, Meetup[event.id].location end end context "Model definition" do should "not raise if an attribute is redefined" do assert_nothing_raised do class RedefinedModel < Ohm::Model attribute :name attribute :name end end end should "not raise if a counter is redefined" do assert_nothing_raised do class RedefinedModel < Ohm::Model counter :age counter :age end end end should "not raise if a list is redefined" do assert_nothing_raised do class RedefinedModel < Ohm::Model list :todo list :todo end end end should "not raise if a set is redefined" do assert_nothing_raised do class RedefinedModel < Ohm::Model set :friends set :friends end end end should "not raise if a collection is redefined" do assert_nothing_raised do class RedefinedModel < Ohm::Model list :toys set :toys end end end should "not raise if a index is redefined" do assert_nothing_raised do class RedefinedModel < Ohm::Model attribute :color index :color index :color end end end end context "Finding an event" do setup do Ohm.redis.sadd("Event:all", 1) Ohm.redis.hset("Event:1", "name", "Concert") end should "return an instance of Event" do assert Event[1].kind_of?(Event) assert_equal 1, Event[1].id assert_equal "Concert", Event[1].name end end context "Finding a user" do setup do Ohm.redis.sadd("User:all", 1) Ohm.redis.hset("User:1", "email", "albert@example.com") end should "return an instance of User" do assert User[1].kind_of?(User) assert_equal 1, User[1].id assert_equal "albert@example.com", User[1].email end should "allow to map ids to models" do assert_equal [User[1]], [1].map(&User) end end context "Updating a user" do setup do Ohm.redis.sadd("User:all", 1) Ohm.redis.set("User:1:email", "albert@example.com") @user = User[1] end should "change its attributes" do @user.email = "maria@example.com" assert_equal "maria@example.com", @user.email end should "save the new values" do @user.email = "maria@example.com" @user.save @user.email = "maria@example.com" @user.save assert_equal "maria@example.com", User[1].email end end context "Creating a new model" do should "assign a new id to the event" do event1 = Event.new event1.create event2 = Event.new event2.create assert !event1.new? assert !event2.new? assert_equal "1", event1.id assert_equal "2", event2.id end end context "Saving a model" do should "create the model if it is new" do event = Event.new(:name => "Foo").save assert_equal "Foo", Event[event.id].name end should "save it only if it was previously created" do event = Event.new event.name = "Lorem ipsum" event.create event.name = "Lorem" event.save assert_equal "Lorem", Event[event.id].name end should "allow to hook into write" do event = Event.create(:name => "Foo") assert_equal "foo", event.slug end end context "Delete" do should "delete an existing model" do class ModelToBeDeleted < Ohm::Model attribute :name set :foos list :bars end @model = ModelToBeDeleted.create(:name => "Lorem") @model.foos << "foo" @model.bars << "bar" id = @model.id @model.delete assert_nil Ohm.redis.get(ModelToBeDeleted.key(id)) assert_nil Ohm.redis.get(ModelToBeDeleted.key(id, :name)) assert_equal Array.new, Ohm.redis.smembers(ModelToBeDeleted.key(id, :foos)) assert_equal Array.new, Ohm.redis.lrange(ModelToBeDeleted.key(id, :bars), 0, -1) assert ModelToBeDeleted.all.empty? end should "be no leftover keys" do class ::Foo < Ohm::Model attribute :name index :name end assert_equal [], Ohm.redis.keys("*") Foo.create(:name => "Bar") assert_equal ["Foo:1:_indices", "Foo:1", "Foo:all", "Foo:id", "Foo:name:QmFy"].sort, Ohm.redis.keys("*").sort Foo[1].delete assert_equal ["Foo:id"], Ohm.redis.keys("*") end end context "Listing" do should "find all" do event1 = Event.new event1.name = "Ruby Meetup" event1.create event2 = Event.new event2.name = "Ruby Tuesday" event2.create all = Event.all assert all.detect {|e| e.name == "Ruby Meetup" } assert all.detect {|e| e.name == "Ruby Tuesday" } end end context "Sorting" do should "sort all" do Person.create :name => "D" Person.create :name => "C" Person.create :name => "B" Person.create :name => "A" assert_equal %w[A B C D], Person.all.sort_by(:name, :order => "ALPHA").map { |person| person.name } end should "return an empty array if there are no elements to sort" do assert_equal [], Person.all.sort_by(:name) end should "return the first element sorted by id when using first" do Person.create :name => "A" Person.create :name => "B" assert_equal "A", Person.all.first.name end should "return the first element sorted by name if first receives a sorting option" do Person.create :name => "B" Person.create :name => "A" assert_equal "A", Person.all.first(:by => :name, :order => "ALPHA").name end should "return attribute values when the get parameter is specified" do Person.create :name => "B" Person.create :name => "A" assert_equal "A", Person.all.sort_by(:name, :get => :name, :order => "ALPHA").first end end context "Loading attributes" do setup do event = Event.new event.name = "Ruby Tuesday" @id = event.create.id end should "load attributes lazily" do event = Event[@id] assert_nil event.send(:instance_variable_get, "@name") assert_equal "Ruby Tuesday", event.name end should "load attributes as a strings" do event = Event.create(:name => 1) assert_equal "1", Event[event.id].name end end context "Attributes of type Set" do setup do @person1 = Person.create(:name => "Albert") @person2 = Person.create(:name => "Bertrand") @person3 = Person.create(:name => "Charles") @event = Event.new @event.name = "Ruby Tuesday" end should "not be available if the model is new" do assert_raise Ohm::Model::MissingID do @event.attendees << Person.new end end should "remove an element if sent :delete" do @event.create @event.attendees << @person1 @event.attendees << @person2 @event.attendees << @person3 assert_equal ["1", "2", "3"], @event.attendees.raw.sort @event.attendees.delete(@person2) assert_equal ["1", "3"], Event[@event.id].attendees.raw.sort end should "return true if the set includes some member" do @event.create @event.attendees << @person1 @event.attendees << @person2 assert @event.attendees.include?(@person2) assert !@event.attendees.include?(@person3) end should "return instances of the passed model" do @event.create @event.attendees << @person1 assert_equal [@person1], @event.attendees.all assert_equal @person1, @event.attendees[0] end should "return the size of the set" do @event.create @event.attendees << @person1 @event.attendees << @person2 @event.attendees << @person3 assert_equal 3, @event.attendees.size end should "empty the set" do @event.create @event.attendees << @person1 @event.attendees.clear assert @event.attendees.empty? end should "replace the values in the set" do @event.create @event.attendees << @person1 assert_equal [@person1], @event.attendees.all @event.attendees.replace([@person2, @person3]) assert_equal [@person2, @person3], @event.attendees.sort end should "filter elements" do @event.create @event.attendees.add(@person1) @event.attendees.add(@person2) assert_equal [@person1], @event.attendees.find(:initial => "A").all assert_equal [@person2], @event.attendees.find(:initial => "B").all assert_equal [], @event.attendees.find(:initial => "Z").all end end context "Attributes of type List" do setup do @post = Post.new @post.body = "Hello world!" @post.create end should "return an array" do assert @post.comments.all.kind_of?(Array) end should "append elements with push" do @post.comments.push "1" @post.comments << "2" assert_equal ["1", "2"], @post.comments.all end should "keep the inserting order" do @post.comments << "1" @post.comments << "2" @post.comments << "3" assert_equal ["1", "2", "3"], @post.comments.all end should "keep the inserting order after saving" do @post.comments << "1" @post.comments << "2" @post.comments << "3" @post.save assert_equal ["1", "2", "3"], Post[@post.id].comments.all end should "respond to each" do @post.comments << "1" @post.comments << "2" @post.comments << "3" i = 1 @post.comments.each do |c| assert_equal i, c.to_i i += 1 end end should "return the size of the list" do @post.comments << "1" @post.comments << "2" @post.comments << "3" assert_equal 3, @post.comments.size end should "return the last element with pop" do @post.comments << "1" @post.comments << "2" assert_equal "2", @post.comments.pop assert_equal "1", @post.comments.pop assert @post.comments.empty? end should "return the first element with shift" do @post.comments << "1" @post.comments << "2" assert_equal "1", @post.comments.shift assert_equal "2", @post.comments.shift assert @post.comments.empty? end should "push to the head of the list with unshift" do @post.comments.unshift "1" @post.comments.unshift "2" assert_equal "1", @post.comments.pop assert_equal "2", @post.comments.pop assert @post.comments.empty? end should "empty the list" do @post.comments.unshift "1" @post.comments.clear assert @post.comments.empty? end should "replace the values in the list" do @post.comments.replace(["1", "2"]) assert_equal ["1", "2"], @post.comments end should "add models" do @post.related.add(Post.create(:body => "Hello")) assert_equal ["2"], @post.related.raw end should "find elements in the list" do another_post = Post.create @post.related.add(another_post) assert @post.related.include?(another_post) assert !@post.related.include?(Post.create) end should "unshift models" do @post.related.unshift(Post.create(:body => "Hello")) @post.related.unshift(Post.create(:body => "Goodbye")) assert_equal ["3", "2"], @post.related.raw assert_equal "3", @post.related.shift.id assert_equal "2", @post.related.pop.id assert_nil @post.related.pop end end context "Applying arbitrary transformations" do require "date" class MyActiveRecordModel def self.find(id) return new if id.to_i == 1 end def id 1 end def ==(other) id == other.id end end class ::Calendar < Ohm::Model list :holidays, lambda { |v| Date.parse(v) } list :subscribers, lambda { |id| MyActiveRecordModel.find(id) } list :appointments, Appointment end class ::Appointment < Ohm::Model attribute :text reference :subscriber, lambda { |id| MyActiveRecordModel.find(id) } end setup do @calendar = Calendar.create @calendar.holidays.raw << "2009-05-25" @calendar.holidays.raw << "2009-07-09" @calendar.subscribers << MyActiveRecordModel.find(1) end should "apply a transformation" do assert_equal [Date.new(2009, 5, 25), Date.new(2009, 7, 9)], @calendar.holidays.all assert_equal ["1"], @calendar.subscribers.raw.all assert_equal [MyActiveRecordModel.find(1)], @calendar.subscribers.all end should "allow lambdas in references" do appointment = Appointment.create(:subscriber => MyActiveRecordModel.find(1)) assert_equal MyActiveRecordModel.find(1), appointment.subscriber end should "work with models too" do @calendar.appointments.add(Appointment.create(:text => "Meet with Bertrand")) assert_equal [Appointment[1]], Calendar[1].appointments.sort end end context "Sorting lists and sets" do setup do @post = Post.create(:body => "Lorem") @post.comments << 2 @post.comments << 3 @post.comments << 1 end should "sort values" do assert_equal %w{1 2 3}, @post.comments.sort end end context "Sorting lists and sets by model attributes" do setup do @event = Event.create(:name => "Ruby Tuesday") @event.attendees << Person.create(:name => "D") @event.attendees << Person.create(:name => "C") @event.attendees << Person.create(:name => "B") @event.attendees << Person.create(:name => "A") end should "sort the model instances by the values provided" do people = @event.attendees.sort_by(:name, :order => "ALPHA") assert_equal %w[A B C D], people.map { |person| person.name } end should "accept a number in the limit parameter" do people = @event.attendees.sort_by(:name, :limit => 2, :order => "ALPHA") assert_equal %w[A B], people.map { |person| person.name } end should "use the start parameter as an offset if the limit is provided" do people = @event.attendees.sort_by(:name, :limit => 2, :start => 1, :order => "ALPHA") assert_equal %w[B C], people.map { |person| person.name } end end context "Collections initialized with a Model parameter" do setup do @user = User.create(:email => "albert@example.com") @user.posts.add Post.create(:body => "D") @user.posts.add Post.create(:body => "C") @user.posts.add Post.create(:body => "B") @user.posts.add Post.create(:body => "A") end should "return instances of the passed model" do assert_equal Post, @user.posts.first.class end end context "Counters" do setup do @event = Event.create(:name => "Ruby Tuesday") end should "raise ArgumentError if the attribute is not a counter" do assert_raise ArgumentError do @event.incr(:name) end end should "be zero if not initialized" do assert_equal 0, @event.votes end should "be able to increment a counter" do @event.incr(:votes) assert_equal 1, @event.votes @event.incr(:votes, 2) assert_equal 3, @event.votes end should "be able to decrement a counter" do @event.decr(:votes) assert_equal -1, @event.votes @event.decr(:votes, 2) assert_equal -3, @event.votes end end context "Comparison" do setup do @user = User.create(:email => "foo") end should "be comparable to other instances" do assert_equal @user, User[@user.id] assert_not_equal @user, User.create assert_not_equal User.new, User.new end should "not be comparable to instances of other models" do assert_not_equal @user, Event.create(:name => "Ruby Tuesday") end should "be comparable to non-models" do assert_not_equal @user, 1 assert_not_equal @user, true # Not equal although the other object responds to #key. assert_not_equal @user, OpenStruct.new(:key => @user.send(:key)) end end context "Debugging" do class ::Bar < Ohm::Model attribute :name counter :visits set :friends list :comments def foo bar.foo end def baz bar.new.foo end def bar SomeMissingConstant end end should "provide a meaningful inspect" do bar = Bar.new assert_equal "#", bar.inspect bar.update(:name => "Albert") bar.friends << 1 bar.friends << 2 bar.comments << "A" bar.incr(:visits) assert_equal %Q{# comments=# visits=1>}, Bar[bar.id].inspect end def assert_wrapper_exception(&block) begin block.call rescue NoMethodError => exception_raised end assert_match /You tried to call SomeMissingConstant#\w+, but SomeMissingConstant is not defined on #{__FILE__}:\d+:in `bar'/, exception_raised.message end should "inform about a miscatch by Wrapper when calling class methods" do assert_wrapper_exception { Bar.new.baz } end should "inform about a miscatch by Wrapper when calling instance methods" do assert_wrapper_exception { Bar.new.foo } end end context "Overwriting write" do class ::Baz < Ohm::Model attribute :name def write self.name = "Foobar" super end end should "work properly" do baz = Baz.new baz.name = "Foo" baz.save baz.name = "Foo" baz.save assert_equal "Foobar", Baz[baz.id].name end end context "References to other objects" do class ::Note < Ohm::Model attribute :content reference :source, Post collection :comments, Comment list :ratings, Rating end class ::Comment < Ohm::Model reference :note, Note end class ::Rating < Ohm::Model attribute :value end class ::Editor < Ohm::Model attribute :name reference :post, Post end class ::Post < Ohm::Model reference :author, Person collection :notes, Note, :source collection :editors, Editor end setup do @post = Post.create end context "a reference to another object" do should "return an instance of Person if author_id has a valid id" do @post.author_id = Person.create(:name => "Albert").id @post.save assert_equal "Albert", Post[@post.id].author.name end should "assign author_id if author is sent a valid instance" do @post.author = Person.create(:name => "Albert") @post.save assert_equal "Albert", Post[@post.id].author.name end should "assign nil if nil is passed to author" do @post.author = nil @post.save assert_nil Post[@post.id].author end should "be cached in an instance variable" do @author = Person.create(:name => "Albert") @post.update(:author => @author) assert_equal @author, @post.author assert @post.author.object_id == @post.author.object_id @post.update(:author => Person.create(:name => "Bertrand")) assert_equal "Bertrand", @post.author.name assert @post.author.object_id == @post.author.object_id @post.update(:author_id => Person.create(:name => "Charles").id) assert_equal "Charles", @post.author.name end end context "a collection of other objects" do setup do @note = Note.create(:content => "Interesting stuff", :source => @post) @comment = Comment.create(:note => @note) end should "return a set of notes" do assert_equal @note.source, @post assert_equal @note, @post.notes.first end should "return a set of comments" do assert_equal @comment, @note.comments.first end should "return a list of ratings" do @rating = Rating.create(:value => 5) @note.ratings << @rating assert_equal @rating, @note.ratings.first end should "default to the current class name" do @editor = Editor.create(:name => "Albert", :post => @post) assert_equal @editor, @post.editors.first end end end context "Models connected to different databases" do class ::Car < Ohm::Model attribute :name end class ::Make < Ohm::Model attribute :name end setup do Car.connect(:port => 6379, :db => 14) end teardown do Car.db.flushdb end should "save to the selected database" do car = Car.create(:name => "Twingo") make = Make.create(:name => "Renault") assert_equal ["1"], Redis.new(:db => 15).smembers("Make:all") assert_equal [], Redis.new(:db => 15).smembers("Car:all") assert_equal ["1"], Redis.new(:db => 14).smembers("Car:all") assert_equal [], Redis.new(:db => 14).smembers("Make:all") assert_equal car, Car[1] assert_equal make, Make[1] Make.db.flushdb assert_equal car, Car[1] assert_nil Make[1] end end context "Persistence" do should "persist attributes to a hash" do event = Event.create(:name => "Redis Meetup") event.incr(:votes) assert_equal "hash", Ohm.redis.type("Event:1") assert_equal [ "Event:1", "Event:all", "Event:id" ].sort, Ohm.redis.keys("Event:*").sort assert_equal "Redis Meetup", Event[1].name assert_equal 1, Event[1].votes end end end