SimplyBased 0.1.0 — Couchbase-native ActiveModel ORM
A from-scratch fork of simply_couch's model layer that runs on Couchbase natively: `include SimplyBased::Model` instead of `include SimplyCouch::Model`. - SimplyBased::Store replaces the CouchDB layer: KV documents, N1QL/SQL++ finders + lazily-created GSIs, CAS mapped onto _rev. No design documents or JavaScript map/reduce — the hand-written association views become N1QL (belongs_to/has_many/has_one scalar FK; habtm via ANY ... SATISFIES + array index, or KV multi-get on the storing side). - Redis replacements on the same cluster: Counter (atomic increment/decrement), Cache (ActiveSupport::Cache::Store over KV+TTL, drop-in Rails.cache/ActiveJob), Lock (insert-if-absent + TTL + CAS-checked release). - Runs the simply_couch test suite against live Couchbase: 378/432 applicable examples green (CouchDB-mechanic specs tagged :couchdb_only and excluded). - Remaining: ancestry (tree) + embedded_in reverse-lookups (recursive/embedded N1QL) and a few pagination/lifecycle edges. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
commit
86299b8827
84 files changed
+9904
No files matched your search
@@ -0,0 +1,7 @@
|
||||
require 'spec_helper'
|
||||
|
||||
describe 'Active record compatibility' do
|
||||
it "had a proper primary key definition" do
|
||||
Post.primary_key.should eq 'id'
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,289 @@
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'Ancestry' do
|
||||
context "standard with hierarchy" do
|
||||
before do
|
||||
@d1 = Directory.create(name: 'dir1', position: 0)
|
||||
@d2 = Directory.create(name: 'dir2', position: 1)
|
||||
@d3 = Directory.create(name: 'dir3', position: 2)
|
||||
@d4 = Directory.create(name: 'dir4', position: 3)
|
||||
@a = [@d1, @d2, @d3, @d4]
|
||||
end
|
||||
|
||||
it "is invalid if make_invalid is an option" do
|
||||
expect( @d1.update_attributes make_invalid: true ).to be false
|
||||
end
|
||||
|
||||
describe '#path_ids' do
|
||||
it "includes the self.id in path_ids and is [self.id] when record has no parent" do
|
||||
expect( @a.map(&:id) ).to eq @a.map(&:path_ids).flatten
|
||||
end
|
||||
end
|
||||
|
||||
it "sets valid children when mass assigned" do
|
||||
@d1.children = [@d2, @d3]
|
||||
expect( @d1.children.map(&:id) ).to match_array [@d2.id, @d3.id]
|
||||
expect( @d2.path_ids ).to eq [@d1.id, @d2.id]
|
||||
d1_reloaded = Directory.find(@d1.id)
|
||||
expect( d1_reloaded.children.map(&:name) ).to match_array [@d2.name, @d3.name]
|
||||
expect( d1_reloaded.children.sort_by(&:name).first.path_ids ).to eq [@d1.id, @d2.id]
|
||||
end
|
||||
|
||||
it "is valid when parent is assigned" do
|
||||
@d1.children = [@d2, @d3] # See previous test for this one
|
||||
@d4.parent = @d2 # This is the functionality to be tested here
|
||||
|
||||
expect( @d2.children ).to eq [@d4]
|
||||
expect( @d4.parent ).to eq @d2
|
||||
expect( @d4.path_ids ).to eq [@d1.id, @d2.id, @d4.id]
|
||||
|
||||
# Also test database values
|
||||
d2_reloaded = Directory.find(@d2.id)
|
||||
d4_reloaded = Directory.find(@d4.id)
|
||||
expect( d2_reloaded.children ).to eq [@d4]
|
||||
expect( d4_reloaded.parent ).to eq @d2
|
||||
expect( d4_reloaded.path_ids ).to eq [@d1.id, @d2.id, @d4.id]
|
||||
end
|
||||
|
||||
it "handles roots" do
|
||||
expect( Directory.roots.sort_by(&:id) ).to eq @a.sort_by(&:id)
|
||||
@d1.children = [@d2, @d3]
|
||||
expect( Directory.roots ).to match_array [@d1, @d4]
|
||||
end
|
||||
|
||||
it "returns proper ancestors" do
|
||||
@d1.children = [@d2, @d3]
|
||||
@d3.add_child(@d4).save
|
||||
expect( @d1.ancestors ).to be_empty
|
||||
expect( @d2.ancestors ).to eq [@d1]
|
||||
expect( @d3.ancestors ).to eq [@d1]
|
||||
expect( @d4.ancestors ).to eq [@d1, @d3]
|
||||
d4_reloaded = Directory.find(@d4.id)
|
||||
expect( d4_reloaded.ancestors ).to eq [@d1, @d3]
|
||||
end
|
||||
|
||||
it "returns proper descendants" do
|
||||
@d1.children = [@d2, @d3]
|
||||
@d2.children = [@d4]
|
||||
expect( @d1.descendants ).to match_array [@d2, @d3, @d4]
|
||||
end
|
||||
|
||||
it "gives a proper subtree and does not change the subject" do
|
||||
# This is a relevant test since it failed in the past
|
||||
@d1.children = [@d2]
|
||||
@d2.children = [@d3, @d4]
|
||||
expect( @d2.subtree ).to eq [@d3, @d4]
|
||||
@d2.reload
|
||||
# This is what it is about
|
||||
expect( @d2.path_ids ).to eq [@d1.id, @d2.id]
|
||||
end
|
||||
|
||||
it "updates path_ids on deeper nested elements using children=" do
|
||||
@d2.children = [@d3, @d4]
|
||||
@d1.children = [@d2]
|
||||
|
||||
# Not for now, objects self are not updated
|
||||
#expect( @d3.path_ids ).to eq [@d1.id, @d2.id, @d3.id]
|
||||
#expect( @d4.path_ids ).to eq [@d1.id, @d2.id, @d4.id]
|
||||
@d3.reload
|
||||
@d4.reload
|
||||
expect( @d3.path_ids ).to eq [@d1.id, @d2.id, @d3.id]
|
||||
expect( @d4.path_ids ).to eq [@d1.id, @d2.id, @d4.id]
|
||||
end
|
||||
|
||||
it "updates path_ids on deeper nested elements using parent_id=" do
|
||||
@d2.children = [@d3, @d4]
|
||||
[@d2, @d3, @d4].map(&:reload)
|
||||
@d2.parent_id = @d1.id
|
||||
|
||||
# Not for now, objects self are not updated
|
||||
#expect( @d3.path_ids ).to eq [@d1.id, @d2.id, @d3.id]
|
||||
#expect( @d4.path_ids ).to eq [@d1.id, @d2.id, @d4.id]
|
||||
@d3.reload
|
||||
@d4.reload
|
||||
expect( @d3.path_ids ).to eq [@d1.id, @d2.id, @d3.id]
|
||||
expect( @d4.path_ids ).to eq [@d1.id, @d2.id, @d4.id]
|
||||
end
|
||||
|
||||
it "updates path_ids on deeper nested elements using parent_id= nil" do
|
||||
# Copy of previous
|
||||
@d2.children = [@d3, @d4]
|
||||
[@d2, @d3, @d4].map(&:reload)
|
||||
@d2.parent_id = @d1.id
|
||||
|
||||
# Not for now, objects self are not updated
|
||||
#expect( @d3.path_ids ).to eq [@d1.id, @d2.id, @d3.id]
|
||||
#expect( @d4.path_ids ).to eq [@d1.id, @d2.id, @d4.id]
|
||||
[@d2, @d3, @d4].map(&:reload)
|
||||
expect( @d3.path_ids ).to eq [@d1.id, @d2.id, @d3.id]
|
||||
expect( @d4.path_ids ).to eq [@d1.id, @d2.id, @d4.id]
|
||||
|
||||
@d2.parent_id = ""
|
||||
[@d2, @d3, @d4].map(&:reload)
|
||||
expect( @d3.path_ids ).to eq [@d2.id, @d3.id]
|
||||
expect( @d4.path_ids ).to eq [@d2.id, @d4.id]
|
||||
end
|
||||
|
||||
it "preserve children of parent when another is added through parent_id=" do
|
||||
# Yes, this one failed once
|
||||
|
||||
# Copy of previous
|
||||
@d2.children = [@d3]
|
||||
[@d2, @d3, @d4].map(&:reload)
|
||||
@d4.parent_id = @d2.id
|
||||
[@d2, @d3, @d4].map(&:reload)
|
||||
expect( @d2.children ).to eq [@d3, @d4]
|
||||
end
|
||||
|
||||
it "not update descendants when parent_id is set in update_attributes and save is not valid" do
|
||||
# Copy of previous
|
||||
@d2.children = [@d3, @d4]
|
||||
[@d2, @d3, @d4].map(&:reload)
|
||||
@d2.update_attributes(:parent_id => @d1.id, :make_invalid => true)
|
||||
|
||||
[@d1, @d2, @d3, @d4].map(&:reload)
|
||||
expect( @d3.path_ids ).to eq [@d2.id, @d3.id]
|
||||
expect( @d4.path_ids ).to eq [@d2.id, @d4.id]
|
||||
|
||||
end
|
||||
|
||||
it 'build tree returning actual objects set in descendants' do
|
||||
@d1.children = [@d2, @d3]
|
||||
[@d1, @d2, @d3].map(&:reload)
|
||||
expect( @d1.descendants.map(&:object_id).sort ).to eq @d1.subtree.map(&:object_id).sort
|
||||
end
|
||||
|
||||
context "initialization" do
|
||||
it "initializes a new record without an id" do
|
||||
expect( Directory.new(parent_id: @d3.id).id ).not_to be_present
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "namespaced with hierarchy" do
|
||||
before do
|
||||
@d1 = NamespacedDirectory.create(:name => 'dir1', :locale => 'en')
|
||||
@d2 = NamespacedDirectory.create(:name => 'dir2', :locale => 'en')
|
||||
@d3 = NamespacedDirectory.create(:name => 'dir3', :locale => 'nl')
|
||||
@d4 = NamespacedDirectory.create(:name => 'dir4', :locale => 'nl')
|
||||
@a = [@d1, @d2, @d3, @d4]
|
||||
end
|
||||
|
||||
it "all have a path containing own id" do
|
||||
expect( @a.map(&:path_ids).flatten ).to eq @a.map(&:id)
|
||||
end
|
||||
|
||||
it "set valid children when mass assigned" do
|
||||
@d1.children = [@d2, @d3]
|
||||
expect( @d1.children ).to match_array [@d2, @d3]
|
||||
expect( @d2.path_ids ).to eq [@d1.id, @d2.id]
|
||||
d1_reloaded = NamespacedDirectory.find(@d1.id)
|
||||
expect( d1_reloaded.children ).to match_array [@d2, @d3]
|
||||
expect( d1_reloaded.children.sort_by(&:name).first.path_ids ).to eq [@d1.id, @d2.id]
|
||||
end
|
||||
|
||||
it "be valid when parent is assigned" do
|
||||
@d1.children = [@d2, @d3] # See previous test for this one
|
||||
@d4.parent = @d2 # This is the functionality to be tested here
|
||||
|
||||
expect( @d2.children ).to eq [@d4]
|
||||
expect( @d4.parent ).to eq @d2
|
||||
expect( @d4.path_ids ).to eq [@d1.id, @d2.id, @d4.id]
|
||||
|
||||
# Also test database values
|
||||
d2_reloaded = NamespacedDirectory.find(@d2.id)
|
||||
d4_reloaded = NamespacedDirectory.find(@d4.id)
|
||||
expect( d2_reloaded.children ).to eq [@d4]
|
||||
expect( d4_reloaded.parent ).to eq @d2
|
||||
expect( d4_reloaded.path_ids ).to eq [@d1.id, @d2.id, @d4.id]
|
||||
end
|
||||
|
||||
it "return proper parents" do
|
||||
@d1.children = [@d2, @d3]
|
||||
@d2.children = [@d4]
|
||||
expect( @d4.parents ).to eq [@d1, @d2]
|
||||
end
|
||||
|
||||
it "handle roots" do
|
||||
expect( NamespacedDirectory.roots ).to match_array @a
|
||||
end
|
||||
|
||||
it "removes records as root when given as child argument"do
|
||||
@d1.children = [@d2, @d3]
|
||||
expect( NamespacedDirectory.roots ).to eq [@d1, @d4]
|
||||
end
|
||||
|
||||
it "handle roots by property as symbol" do
|
||||
@d1.children = [@d2, @d3]
|
||||
expect( NamespacedDirectory.roots(:en) ).to eq [@d1]
|
||||
expect( NamespacedDirectory.roots(:nl) ).to eq [@d4]
|
||||
end
|
||||
|
||||
it "handle roots by property as string" do
|
||||
@d1.children = [@d2, @d3]
|
||||
expect( NamespacedDirectory.roots('en') ).to eq [@d1]
|
||||
expect( NamespacedDirectory.roots('nl') ).to eq [@d4]
|
||||
end
|
||||
|
||||
it "change by property to valid value when assigned as children" do
|
||||
expect( @d1.locale ).not_to eq @d3.locale
|
||||
@d1.children = [@d2, @d3]
|
||||
expect( @d1.locale ).to eq @d3.locale
|
||||
d3_reloaded = NamespacedDirectory.find(@d3.id)
|
||||
expect( @d1.locale ).to eq d3_reloaded.locale
|
||||
end
|
||||
|
||||
it "change by property to valid value when assigned through add_child with save" do
|
||||
expect( @d1.locale ).not_to eq @d3.locale
|
||||
@d1.add_child(@d3).save
|
||||
expect( @d1.locale ).to eq @d3.locale
|
||||
d3_reloaded = NamespacedDirectory.find(@d3.id)
|
||||
expect( @d1.locale ).to eq d3_reloaded.locale
|
||||
end
|
||||
|
||||
it "change by property to valid value when assigned through add_child without save" do
|
||||
expect( @d1.locale ).not_to eq @d3.locale
|
||||
@d1.add_child(@d3)
|
||||
expect( @d1.locale ).to eq @d3.locale
|
||||
d3_reloaded = NamespacedDirectory.find(@d3.id)
|
||||
expect( @d4.locale ).to eq d3_reloaded.locale
|
||||
end
|
||||
|
||||
it "give a proper tree without namespace" do
|
||||
@d1.children = [@d2, @d3]
|
||||
full_tree = NamespacedDirectory.full_tree
|
||||
expect( full_tree ).to match_array [@d1, @d4]
|
||||
expect( full_tree.sort_by(&:id).map{|p| p.children.map(&:id).sort}.sort ).to eq [[@d2.id, @d3.id].sort, []].sort
|
||||
end
|
||||
|
||||
it "give a proper tree with namespace as symbol" do
|
||||
@d1.children = [@d2, @d3]
|
||||
full_tree = NamespacedDirectory.full_tree(:en)
|
||||
expect( full_tree.map(&:id) ).to eq [@d1.id]
|
||||
expect( full_tree.map{|p| p.children.map(&:id).sort} ).to eq [[@d2.id, @d3.id].sort]
|
||||
end
|
||||
it "give a proper tree with namespace as string" do
|
||||
@d1.children = [@d2, @d3]
|
||||
full_tree = NamespacedDirectory.full_tree('en')
|
||||
expect( full_tree.map(&:id) ).to eq [@d1.id]
|
||||
expect( full_tree.map{|p| p.children.map(&:id).sort} ).to eq [[@d2.id, @d3.id].sort]
|
||||
end
|
||||
|
||||
it "give a proper tree with namespace as property" do
|
||||
@d1.children = [@d2, @d3]
|
||||
full_tree = NamespacedDirectory.full_tree(:locale => 'en')
|
||||
expect( full_tree.map(&:id) ).to eq [@d1.id]
|
||||
expect( full_tree.map{|p| p.children.map(&:id).sort} ).to eq [[@d2.id, @d3.id].sort]
|
||||
end
|
||||
|
||||
it "give a proper tree with revisions" do
|
||||
@d1.children = [@d2, @d3, @d4]
|
||||
full_tree = NamespacedDirectory.full_tree(:locale => 'en')
|
||||
expect( full_tree ).to eq [@d1]
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
=begin
|
||||
class AncestryTest < Test::Unit::TestCase
|
||||
=end
|
||||
@@ -0,0 +1,16 @@
|
||||
require 'spec_helper'
|
||||
|
||||
describe "Attribute accessing" do
|
||||
context "with brackets" do
|
||||
it "access with string argument" do
|
||||
user = User.new
|
||||
user.name = 'UTest'
|
||||
user['name'].should eq 'UTest'
|
||||
end
|
||||
it "access with symbol argument" do
|
||||
user = User.new
|
||||
user.name = 'UTest'
|
||||
user[:name].should eq 'UTest'
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,217 @@
|
||||
require 'spec_helper'
|
||||
|
||||
describe "BelongsTo" do
|
||||
context "with associations" do
|
||||
context "with belongs_to" do
|
||||
it "generate a view for the association" do
|
||||
#Post.respond_to?(:association_post_belongs_to_user).should be_present
|
||||
Post.should respond_to :association_post_belongs_to_user
|
||||
end
|
||||
|
||||
it "raise an error if another property with the same name already exists" do
|
||||
expect {
|
||||
class ::DoubleBelongsToUser
|
||||
include SimplyBased::Model
|
||||
property :user
|
||||
belongs_to :user
|
||||
end
|
||||
}.to raise_error RuntimeError
|
||||
end
|
||||
|
||||
it "add the foreign key id to the referencing object" do
|
||||
user = User.create(:title => "Mr.")
|
||||
post = Post.create(:user => user)
|
||||
|
||||
post = Post.find(post.id)
|
||||
post.user_id.should eq user.id
|
||||
end
|
||||
|
||||
it "create a property for the foreign key attribute" do
|
||||
Post.properties.any?{|p| p.name.to_s == 'user_id'}.should be true
|
||||
end
|
||||
|
||||
it "notice a change to the foreign key attribute in dirty checks" do
|
||||
user = User.create!(:title => 'Prof')
|
||||
post = Post.create!
|
||||
post.user = user
|
||||
post.should be_user_id_changed
|
||||
end
|
||||
|
||||
it "set also the foreign key id to nil if setting the referencing object to nil" do
|
||||
user = User.create(:title => "Mr.")
|
||||
post = Post.create(:user => user)
|
||||
post.user = nil
|
||||
post.save!
|
||||
post.reload.user.should be nil
|
||||
post.reload.user_id.should be nil
|
||||
end
|
||||
|
||||
it "fetch the object from the database when requested through the getter" do
|
||||
user = User.create(:title => "Mr.")
|
||||
post = Post.create(:user => user)
|
||||
|
||||
post = Post.find(post.id)
|
||||
post.user.should eq user
|
||||
end
|
||||
|
||||
it "mark the referencing object as dirty" do
|
||||
user = User.create(:title => "Mr.")
|
||||
post = Post.create
|
||||
post.user = user
|
||||
post.should be_dirty
|
||||
end
|
||||
|
||||
it "allow assigning a different object and store the id accordingly" do
|
||||
user = User.create(:title => "Mr.")
|
||||
user2 = User.create(:title => "Mrs.")
|
||||
post = Post.create(:user => user)
|
||||
post.user = user2
|
||||
post.save
|
||||
|
||||
post = Post.find(post.id)
|
||||
post.user.id.should eq user2.id
|
||||
end
|
||||
|
||||
it "check the class and raise an error if not matching in belongs_to setter" do
|
||||
post = Post.create
|
||||
expect{ post.user = 'foo' }.to raise_error(ArgumentError, 'expected User got String')
|
||||
end
|
||||
|
||||
it 'not query for the object twice in getter' do
|
||||
user = User.create(:title => "Mr.")
|
||||
post = Post.create(:user => user)
|
||||
post = Post.find(post.id)
|
||||
expect( User ).to receive(:find).and_return "user"
|
||||
post.user
|
||||
expect( User ).not_to receive(:find)
|
||||
post.user
|
||||
end
|
||||
|
||||
it 'use cache in getter' do
|
||||
post = Post.create
|
||||
post.instance_variable_set("@user", 'foo')
|
||||
post.user.should eq 'foo'
|
||||
end
|
||||
|
||||
it "ignore the cache if force_reload is given as an option" do
|
||||
user = User.create(:name => 'Dude', :title => 'Mr.')
|
||||
post = Post.create(:user => user)
|
||||
post.reload
|
||||
post.instance_variable_set("@user", 'foo')
|
||||
post.user(:force_reload => true).should_not eq 'foo'
|
||||
end
|
||||
|
||||
it 'set cache in setter' do
|
||||
post = Post.create
|
||||
user = User.create :title => 'Mr.'
|
||||
post.instance_variable_get("@user").should be nil
|
||||
post.user = user
|
||||
post.instance_variable_get("@user").should eq user
|
||||
end
|
||||
|
||||
it "not hit the database when the id column is empty" do
|
||||
expect( User ).not_to receive(:find)
|
||||
post = Post.create
|
||||
post.user
|
||||
end
|
||||
|
||||
it "know when the associated object changed" do
|
||||
post = Post.create(:user => User.create(:title => "Mr."))
|
||||
user2 = User.create(:title => "Mr.")
|
||||
post.user = user2
|
||||
post.should be_user_id_changed
|
||||
end
|
||||
|
||||
it "not be changed when an association has not changed" do
|
||||
post = Post.create(:user => User.create(:title => "Mr."))
|
||||
post.should_not be_user_id_changed
|
||||
end
|
||||
|
||||
it "not be changed when assigned the same object" do
|
||||
user = User.create(:title => "Mr.")
|
||||
post = Post.create(:user => user)
|
||||
post.user = user
|
||||
post.should_not be_user_changed
|
||||
end
|
||||
|
||||
it "not be changed after saving" do
|
||||
user = User.create(:title => "Mr.")
|
||||
post = Post.new
|
||||
post.user = user
|
||||
post.should be_user_id_changed
|
||||
post.save!
|
||||
post.should_not be_user_id_changed
|
||||
end
|
||||
|
||||
it "have a proper _was value" do
|
||||
user = User.create(:title => "Mr.")
|
||||
user2 = User.create(:title => "Mrs.")
|
||||
post = Post.new
|
||||
|
||||
post.save
|
||||
post.user.should be nil
|
||||
post.user_id.should be nil
|
||||
|
||||
post.user = user
|
||||
#post.user_was.should be nil
|
||||
post.user_id_was.should be nil
|
||||
|
||||
post.save
|
||||
|
||||
post.user = user2
|
||||
#post.user_was.should eq user
|
||||
post.user_id_was.should eq user.id
|
||||
end
|
||||
|
||||
it "set the parent object on has_many association" do
|
||||
# Copy from has_many, but belongs here as well
|
||||
expect( User ).not_to receive(:find)
|
||||
user = User.create(:title => "Mr.")
|
||||
3.times {
|
||||
post = Post.new
|
||||
post.user = user
|
||||
post.save!
|
||||
}
|
||||
post = user.posts.first
|
||||
user.posts.first.user.should eq user
|
||||
end
|
||||
|
||||
it "set the parent object on has_one association" do
|
||||
# Taken from has_one, but belongs here as well
|
||||
expect( Instance ).not_to receive(:find)
|
||||
instance = Instance.create
|
||||
identity = Identity.create(:instance => instance)
|
||||
|
||||
instance.identity.instance.should eq instance
|
||||
end
|
||||
|
||||
it "handle a foreign_key of '' as nil" do
|
||||
post = Post.create
|
||||
post.user_id = ''
|
||||
|
||||
post.user.should be nil
|
||||
end
|
||||
|
||||
context "with aliased associations" do
|
||||
it "allow different names for the same class" do
|
||||
editor = User.create(:name => 'Editor', :title => 'Dr.')
|
||||
author = User.create(:name => 'author', :title => 'Dr.')
|
||||
editor.id.should_not be nil
|
||||
author.id.should_not be nil
|
||||
|
||||
doc = Document.create(:editor => editor, :author => author)
|
||||
doc.save!
|
||||
doc.editor_id.should eq editor.id
|
||||
doc.author_id.should eq author.id
|
||||
doc = Document.find(doc.id)
|
||||
doc.editor.should_not be nil
|
||||
doc.author.should_not be nil
|
||||
doc.editor.id.should eq editor.id
|
||||
doc.author.id.should eq author.id
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
end
|
||||
@@ -0,0 +1,40 @@
|
||||
require 'spec_helper'
|
||||
|
||||
describe "Ancestry.build_tree" do
|
||||
def node(id, path_ids, position = 0)
|
||||
d = Directory.new(name: id)
|
||||
d._id = id
|
||||
d.path_ids = path_ids
|
||||
d.position = position
|
||||
d
|
||||
end
|
||||
|
||||
it "builds a tree from a flat list using path_ids" do
|
||||
root = node('r', ['r'])
|
||||
child = node('c', ['r', 'c'])
|
||||
roots = Directory.build_tree([root, child])
|
||||
roots.map(&:id).should eq(['r'])
|
||||
roots.first.children.map(&:id).should eq(['c'])
|
||||
end
|
||||
|
||||
it "does not leak a class-level @tree_wrapper after building" do
|
||||
Directory.build_tree([node('r', ['r'])])
|
||||
Directory.instance_variable_get(:@tree_wrapper).should be_nil
|
||||
end
|
||||
|
||||
it "is safe under concurrent calls (no shared-state corruption)" do
|
||||
threads = (0...8).map do |i|
|
||||
Thread.new do
|
||||
root = node("r#{i}", ["r#{i}"])
|
||||
child = node("c#{i}", ["r#{i}", "c#{i}"])
|
||||
roots = Directory.build_tree([root, child])
|
||||
[roots.map(&:id), roots.first.children.map(&:id)]
|
||||
end
|
||||
end
|
||||
results = threads.map(&:value)
|
||||
results.each_with_index do |(root_ids, child_ids), i|
|
||||
root_ids.should eq(["r#{i}"])
|
||||
child_ids.should eq(["c#{i}"])
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,40 @@
|
||||
require 'spec_helper'
|
||||
|
||||
# DRAFT: requires a live CouchDB (like the rest of the suite). These specs
|
||||
# document the intended contract of the bulk-write API.
|
||||
describe "Bulk write API" do
|
||||
describe ".save_all" do
|
||||
it "persists multiple new records in one call and assigns _id/_rev" do
|
||||
result = User.save_all([User.new(title: 'a'), User.new(title: 'b')])
|
||||
result[:saved].size.should eq(2)
|
||||
result[:invalid].should be_empty
|
||||
result[:saved].each do |record|
|
||||
record._id.should_not be_nil
|
||||
record._rev.should_not be_nil
|
||||
record.new?.should be false
|
||||
end
|
||||
end
|
||||
|
||||
it "sets timestamps on new records" do
|
||||
record = User.new(title: 'a')
|
||||
User.save_all([record])
|
||||
record.created_at.should_not be_nil
|
||||
record.updated_at.should_not be_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe ".create_all" do
|
||||
it "builds and persists records from attribute hashes" do
|
||||
result = User.create_all([{ title: 'a' }, { title: 'b' }])
|
||||
result[:saved].map(&:title).sort.should eq(['a', 'b'])
|
||||
end
|
||||
end
|
||||
|
||||
describe ".destroy_all" do
|
||||
it "deletes persisted records in one call and clears their ids" do
|
||||
records = User.save_all([User.new(title: 'a'), User.new(title: 'b')])[:saved]
|
||||
User.destroy_all(records)
|
||||
records.each { |r| r._id.should be_nil }
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,56 @@
|
||||
require 'spec_helper'
|
||||
|
||||
describe 'callbacks' do
|
||||
|
||||
describe ".before_save" do
|
||||
let(:subject) { Callbacker.new name: 'Cally' }
|
||||
it "increments the counter" do
|
||||
subject.save
|
||||
subject.reload.counter.should be 1
|
||||
end
|
||||
end
|
||||
|
||||
describe "with raising callback #after_safe" do
|
||||
let(:subject) { Callbacker.new name: 'Cally', raise_on_save: true }
|
||||
|
||||
it "raise_error on normal save" do
|
||||
expect{ subject.save }.to raise_error StandardError
|
||||
end
|
||||
|
||||
it "raises error on save with false, validations are not run only" do
|
||||
expect{ subject.save(false) }.to raise_error StandardError
|
||||
end
|
||||
|
||||
it "raises error on save with validate: false, validations are not run only" do
|
||||
expect{ subject.save(validate: false) }.to raise_error StandardError
|
||||
end
|
||||
|
||||
it "raise error on save with validate: true" do
|
||||
expect{ subject.save(validate: true) }.to raise_error StandardError
|
||||
end
|
||||
end
|
||||
|
||||
describe "with validation error" do
|
||||
let(:subject) { Callbacker.new name: 'Cally', with_validation_error: true }
|
||||
|
||||
it "should not save the record" do
|
||||
subject.save.should be false
|
||||
subject.id.should_not be_present
|
||||
end
|
||||
|
||||
it "should save the record on save with false" do
|
||||
subject.save(false).should be true
|
||||
subject.id.should be_present
|
||||
end
|
||||
|
||||
it "should save the record on save with validate: false" do
|
||||
subject.save(validate: false).should be true
|
||||
subject.id.should be_present
|
||||
end
|
||||
|
||||
it "should not save the record on save with validate: true" do
|
||||
subject.save(validate: true).should be false
|
||||
subject.id.should_not be_present
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,118 @@
|
||||
require 'spec_helper'
|
||||
|
||||
describe "ConflictHandling" do
|
||||
let(:original) { User.create name: 'Mickey Mouse', title: "Dr.", homepage: 'www.gmx.de'}
|
||||
let!(:copy) { User.find(original.id) }
|
||||
|
||||
context "when handling conflicts" do
|
||||
before do
|
||||
User.auto_conflict_resolution_on_save = true
|
||||
end
|
||||
|
||||
it "should be able to save without modifications" do
|
||||
copy.save.should be true
|
||||
end
|
||||
|
||||
it "be able to save when modification happen on different attributes" do
|
||||
original.name = "Pluto"
|
||||
original.save.should be true
|
||||
|
||||
copy.title = 'Prof.'
|
||||
expect { copy.save }.not_to raise_error
|
||||
|
||||
copy.reload.name.should eq "Pluto"
|
||||
copy.reload.title.should eq "Prof."
|
||||
copy.reload.homepage.should eq "www.gmx.de"
|
||||
end
|
||||
|
||||
it "be able to save when modification happen on different, multiple attributes - remote" do
|
||||
original.name = "Pluto"
|
||||
original.homepage = 'www.google.com'
|
||||
original.save.should be true
|
||||
|
||||
copy.title = 'Prof.'
|
||||
expect { copy.save }.not_to raise_error
|
||||
|
||||
copy.reload.name.should eq "Pluto"
|
||||
copy.reload.title.should eq "Prof."
|
||||
copy.reload.homepage.should eq "www.google.com"
|
||||
end
|
||||
|
||||
it "be able to save when modification happen on different, multiple attributes locally" do
|
||||
original.name = "Pluto"
|
||||
original.save.should be true
|
||||
|
||||
copy.title = 'Prof.'
|
||||
copy.homepage = 'www.google.com'
|
||||
expect { copy.save }.not_to raise_error
|
||||
|
||||
copy.reload.name.should eq "Pluto"
|
||||
copy.reload.title.should eq "Prof."
|
||||
copy.reload.homepage.should eq "www.google.com"
|
||||
end
|
||||
|
||||
it "re-raise the conflict if there is no merge possible" do
|
||||
original.name = "Pluto"
|
||||
original.save.should be true
|
||||
|
||||
copy.name = 'Prof.'
|
||||
expect { copy.save }.to raise_error SimplyBased::Conflict
|
||||
|
||||
copy.name.should eq "Prof."
|
||||
copy.reload.name.should eq "Pluto"
|
||||
end
|
||||
|
||||
it "re-raise the conflict if retried several times" do
|
||||
allow( User.database ).to receive(:save_document).and_raise(SimplyBased::Conflict.new)
|
||||
|
||||
copy.name = 'Prof.'
|
||||
expect { copy.save }.to raise_error SimplyBased::Conflict
|
||||
end
|
||||
|
||||
it "not try to merge and re-save if auto_conflict_resolution_on_save is disabled" do
|
||||
User.auto_conflict_resolution_on_save = false
|
||||
allow( User.database ).to receive(:save_document).and_raise(SimplyBased::Conflict.new)
|
||||
|
||||
copy.name = 'Prof.'
|
||||
expect { copy.save }.to raise_error SimplyBased::Conflict
|
||||
end
|
||||
|
||||
context "with conflict information" do
|
||||
|
||||
it "add information about the conflict to the exception" do
|
||||
original.name = "Pluto"
|
||||
original.save.should be true
|
||||
|
||||
copy.name = 'Prof.'
|
||||
begin
|
||||
copy.save
|
||||
rescue SimplyBased::Conflict => e
|
||||
e.message.should eq '409 Conflict - conflict on attributes: ["name"]'
|
||||
end
|
||||
end
|
||||
|
||||
it "only add the conflict information to one exception" do
|
||||
other_copy = User.find(original.id)
|
||||
|
||||
original.name = "Pluto"
|
||||
original.title = "Frau"
|
||||
original.save.should be true
|
||||
|
||||
copy.name = 'Prof.'
|
||||
begin
|
||||
copy.save.should be true
|
||||
rescue SimplyBased::Conflict => e
|
||||
e.message.should eq '409 Conflict - conflict on attributes: ["name"]'
|
||||
end
|
||||
|
||||
other_copy.title = 'Prof.'
|
||||
begin
|
||||
other_copy.save
|
||||
rescue SimplyBased::Conflict => e
|
||||
e.message.should eq '409 Conflict - conflict on attributes: ["title"]'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,112 @@
|
||||
require 'spec_helper'
|
||||
|
||||
class CouchAttachmentDoc
|
||||
include SimplyBased::Model
|
||||
property :title
|
||||
has_couch_attached :receipt
|
||||
end
|
||||
|
||||
describe 'CouchDB attachments', :couchdb_only do
|
||||
describe 'low-level API' do
|
||||
it 'puts and fetches an attachment' do
|
||||
doc = CouchAttachmentDoc.create(title: 'test')
|
||||
doc.put_couch_attachment('file.txt', 'hello world', content_type: 'text/plain')
|
||||
expect(doc.fetch_couch_attachment('file.txt')).to eq 'hello world'
|
||||
end
|
||||
|
||||
it 'lists attachment names' do
|
||||
doc = CouchAttachmentDoc.create(title: 'test')
|
||||
doc.put_couch_attachment('a.txt', 'a')
|
||||
doc.put_couch_attachment('b.txt', 'b')
|
||||
expect(doc.couch_attachment_names).to contain_exactly('a.txt', 'b.txt')
|
||||
end
|
||||
|
||||
it 'deletes an attachment' do
|
||||
doc = CouchAttachmentDoc.create(title: 'test')
|
||||
doc.put_couch_attachment('file.txt', 'data')
|
||||
doc.delete_couch_attachment('file.txt')
|
||||
expect(doc.couch_attachment?('file.txt')).to be false
|
||||
end
|
||||
|
||||
it 'returns nil for missing attachment' do
|
||||
doc = CouchAttachmentDoc.create(title: 'test')
|
||||
expect(doc.fetch_couch_attachment('missing.txt')).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe 'has_couch_attached macro' do
|
||||
it 'defines getter and setter' do
|
||||
doc = CouchAttachmentDoc.new
|
||||
expect(doc).to respond_to(:receipt)
|
||||
expect(doc).to respond_to(:receipt=)
|
||||
end
|
||||
|
||||
it 'defines URL helper' do
|
||||
doc = CouchAttachmentDoc.new
|
||||
expect(doc).to respond_to(:receipt_url)
|
||||
end
|
||||
|
||||
it 'defines metadata properties' do
|
||||
doc = CouchAttachmentDoc.new
|
||||
expect(doc).to respond_to(:receipt_content_type)
|
||||
expect(doc).to respond_to(:receipt_content_type=)
|
||||
expect(doc).to respond_to(:receipt_size)
|
||||
expect(doc).to respond_to(:receipt_size=)
|
||||
end
|
||||
|
||||
it 'stores and retrieves data through the setter/getter' do
|
||||
doc = CouchAttachmentDoc.create(title: 'test')
|
||||
doc.receipt = 'binary data here'
|
||||
doc.save
|
||||
|
||||
reloaded = CouchAttachmentDoc.find(doc.id)
|
||||
expect(reloaded.receipt).to eq 'binary data here'
|
||||
end
|
||||
|
||||
it 'tracks content type' do
|
||||
doc = CouchAttachmentDoc.create(title: 'test')
|
||||
doc.receipt = 'data'
|
||||
doc.save
|
||||
expect(doc.receipt_content_type).to eq 'binary/octet-stream'
|
||||
end
|
||||
|
||||
it 'tracks size' do
|
||||
doc = CouchAttachmentDoc.create(title: 'test')
|
||||
doc.receipt = '12345'
|
||||
doc.save
|
||||
expect(doc.receipt_size).to eq 5
|
||||
end
|
||||
|
||||
it 'handles non-persisted records via pending queue' do
|
||||
doc = CouchAttachmentDoc.new(title: 'draft')
|
||||
doc.receipt = 'pending data'
|
||||
result = doc.save
|
||||
|
||||
expect(result).to be_truthy
|
||||
reloaded = CouchAttachmentDoc.find(doc.id)
|
||||
expect(reloaded.receipt).to eq 'pending data'
|
||||
end
|
||||
|
||||
it 'updates attachment on second assignment' do
|
||||
doc = CouchAttachmentDoc.create(title: 'test')
|
||||
doc.receipt = 'v1'
|
||||
doc.save
|
||||
|
||||
doc.receipt = 'v2'
|
||||
doc.save
|
||||
|
||||
reloaded = CouchAttachmentDoc.find(doc.id)
|
||||
expect(reloaded.receipt).to eq 'v2'
|
||||
end
|
||||
|
||||
it 'generates a URL' do
|
||||
doc = CouchAttachmentDoc.create(title: 'test')
|
||||
doc.receipt = 'data'
|
||||
doc.save
|
||||
|
||||
url = doc.receipt_url
|
||||
expect(url).to include(doc._id)
|
||||
expect(url).to include('receipt')
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,19 @@
|
||||
require 'spec_helper'
|
||||
|
||||
describe "Default value isolation" do
|
||||
# Directory (has_ancestry) declares `property :path_ids, type: Array, default: []`.
|
||||
# Each instance must get its own copy of the default, never a shared array.
|
||||
it "does not share a mutable Array default across instances" do
|
||||
a = Directory.new
|
||||
b = Directory.new
|
||||
a.path_ids << 'x'
|
||||
b.path_ids.should eq([])
|
||||
a.path_ids.should eq(['x'])
|
||||
end
|
||||
|
||||
it "gives each instance an independent default object" do
|
||||
a = Directory.new
|
||||
b = Directory.new
|
||||
a.path_ids.object_id.should_not eq(b.path_ids.object_id)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,173 @@
|
||||
require 'spec_helper'
|
||||
|
||||
describe 'Embedding' do
|
||||
context 'initialized comments' do
|
||||
before do
|
||||
@post = Post.new
|
||||
@post.embedded_comments = [
|
||||
{ 'ruby_class' => 'EmbeddedComment', 'body' => 'body1' },
|
||||
{ 'ruby_class' => 'EmbeddedComment', 'body' => 'body2' }
|
||||
]
|
||||
@post.save
|
||||
end
|
||||
|
||||
it 'return a valid size' do
|
||||
expect(@post.embedded_comments.size).to eq 2
|
||||
post_reloaded = Post.find(@post.id)
|
||||
expect(post_reloaded.embedded_comments.size).to eq 2
|
||||
end
|
||||
|
||||
it 'delete comment using object' do
|
||||
@post.remove_embedded_comment(@post.embedded_comments.first)
|
||||
expect(@post.embedded_comments.size).to eq 1
|
||||
post_reloaded = Post.find(@post.id)
|
||||
expect(post_reloaded.embedded_comments.size).to eq 1
|
||||
end
|
||||
|
||||
it 'get all emmbedded using .all' do
|
||||
expect(EmbeddedComment.all.size).to eq 2
|
||||
end
|
||||
|
||||
it 'get embedded object, not a hash' do
|
||||
expect(EmbeddedComment.all.first).to be_a EmbeddedComment
|
||||
end
|
||||
|
||||
it 'have a parent_object when loaded through all' do
|
||||
expect(EmbeddedComment.all.first.parent_object).to eq @post
|
||||
end
|
||||
|
||||
it 'save an instance' do
|
||||
comment = @post.embedded_comments.first
|
||||
comment.body = 'body-changed'
|
||||
comment.save
|
||||
comment_reloaded = Post.find(@post.id).embedded_comments.first
|
||||
expect(comment_reloaded.body).to eq 'body-changed'
|
||||
end
|
||||
|
||||
it 'change attribute when not loaded through parent object' do
|
||||
embedded_comments = EmbeddedComment.all
|
||||
embedded_comment = embedded_comments.first
|
||||
embedded_comment.body = 'newbody'
|
||||
embedded_comment.save
|
||||
embedded_comments_reloaded = EmbeddedComment.all
|
||||
expect(embedded_comments_reloaded.map(&:body)).to include('newbody')
|
||||
end
|
||||
|
||||
it 'change attribute when saved through parent object' do
|
||||
embedded_comments = EmbeddedComment.all
|
||||
embedded_comment = embedded_comments.first
|
||||
embedded_comment.body = 'newbody'
|
||||
embedded_comment.parent_object.send(:attribute_will_change!, :embedded_comments)
|
||||
embedded_comment.parent_object.save
|
||||
embedded_comments_reloaded = EmbeddedComment.all
|
||||
expect(embedded_comments_reloaded.map(&:body)).to include('newbody')
|
||||
end
|
||||
|
||||
it 'delete comment using integer' do
|
||||
end
|
||||
|
||||
it 'delete comment using integer string' do
|
||||
end
|
||||
|
||||
it 'Count' do
|
||||
expect(EmbeddedComment.count).to eq 2
|
||||
end
|
||||
end
|
||||
|
||||
context 'Creation of comment' do
|
||||
before { @post = Post.create }
|
||||
|
||||
it 'not save when no parent is present' do
|
||||
comment = EmbeddedComment.new(body: 'no parent')
|
||||
expect(comment.save).to be false
|
||||
expect(comment.errors[:post]).to include('no_parent')
|
||||
end
|
||||
|
||||
it 'save when initialized with parent actual name initialization' do
|
||||
comment = EmbeddedComment.new(body: 'no parent', post: @post)
|
||||
expect(comment.save).to be true
|
||||
expect(comment.post).to eq @post
|
||||
expect(comment.parent_object).to eq @post
|
||||
expect(@post.embedded_comments.include?(comment)).to be true
|
||||
reloaded_post = Post.find(@post.id)
|
||||
expect(reloaded_post.embedded_comments.include?(comment)).to be true
|
||||
end
|
||||
|
||||
it 'save when initialized with parent object initialization' do
|
||||
comment = EmbeddedComment.new(body: 'no parent', parent_object: @post)
|
||||
expect(comment.save).to be true
|
||||
expect(comment.post).to eq @post
|
||||
expect(comment.parent_object).to eq @post
|
||||
expect(@post.embedded_comments.include?(comment)).to be true
|
||||
expect(comment.save).to be true
|
||||
reloaded_post = Post.find(@post.id)
|
||||
expect(reloaded_post.embedded_comments.include?(comment)).to be true
|
||||
end
|
||||
|
||||
it 'save when parent object is assigned later with relation name' do
|
||||
comment = EmbeddedComment.new(body: 'no parent')
|
||||
comment.post = @post
|
||||
expect(comment.save).to be true
|
||||
expect(comment.post).to eq @post
|
||||
expect(comment.parent_object).to eq @post
|
||||
expect(@post.embedded_comments.include?(comment)).to be true
|
||||
reloaded_post = Post.find(@post.id)
|
||||
expect(reloaded_post.embedded_comments.include?(comment)).to be true
|
||||
end
|
||||
|
||||
it 'save when parent object is assigned later with parent object assignment' do
|
||||
comment = EmbeddedComment.new(body: 'no parent')
|
||||
comment.parent_object = @post
|
||||
expect(comment.save).to be true
|
||||
expect(comment.post).to eq @post
|
||||
expect(comment.parent_object).to eq @post
|
||||
expect(@post.embedded_comments.include?(comment)).to be true
|
||||
reloaded_post = Post.find(@post.id)
|
||||
expect(reloaded_post.embedded_comments.include?(comment)).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'belongs to strict_post' do
|
||||
before do
|
||||
@user = User.create(name: 'embedding user')
|
||||
@strict_post = StrictPost.create(user: @user)
|
||||
@post = Post.new
|
||||
@post.embedded_comments = [
|
||||
{ 'ruby_class' => 'EmbeddedComment', 'body' => 'body1' },
|
||||
{ 'ruby_class' => 'EmbeddedComment', 'body' => 'body2' }
|
||||
]
|
||||
@post.save
|
||||
end
|
||||
|
||||
it 'add embedded comments to strict_post' do
|
||||
expect(@strict_post.save).to be true
|
||||
@post.embedded_comments.each { |ec| ec.strict_post = @strict_post; ec.save }
|
||||
strict_post_reloaded = StrictPost.find(@strict_post.id)
|
||||
expect(strict_post_reloaded.embedded_comments.size).to eq @post.embedded_comments.size
|
||||
end
|
||||
|
||||
it 'have strict_post as association' do
|
||||
expect(@strict_post.save).to be true
|
||||
@post.embedded_comments.each { |ec| ec.strict_post = @strict_post; ec.save }
|
||||
post_reloaded = Post.find(@post.id)
|
||||
comment_reloaded = post_reloaded.embedded_comments.first
|
||||
expect(comment_reloaded.strict_post).to eq @strict_post
|
||||
end
|
||||
|
||||
it 'have parent object when queried through relation' do
|
||||
expect(@strict_post.save).to be true
|
||||
@post.embedded_comments.each { |ec| ec.strict_post = @strict_post; ec.save }
|
||||
strict_post_reloaded = StrictPost.find(@strict_post.id)
|
||||
expect(strict_post_reloaded.embedded_comments.first).to eq @post.embedded_comments.first
|
||||
expect(strict_post_reloaded.embedded_comments.first.post).to eq @post
|
||||
end
|
||||
|
||||
it 'have actual object same as in parent object' do
|
||||
expect(@strict_post.save).to be true
|
||||
@post.embedded_comments.each { |ec| ec.strict_post = @strict_post; ec.save }
|
||||
strict_post_reloaded = StrictPost.find(@strict_post.id)
|
||||
embedded_comment = strict_post_reloaded.embedded_comments.first
|
||||
expect(embedded_comment.parent_object.embedded_comments.map(&:object_id)).to include(embedded_comment.object_id)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,45 @@
|
||||
require 'spec_helper'
|
||||
|
||||
describe "Equality and hashing" do
|
||||
describe "unsaved records" do
|
||||
it "two distinct new records are not equal" do
|
||||
a = User.new(name: 'a')
|
||||
b = User.new(name: 'a')
|
||||
(a == b).should be false
|
||||
a.eql?(b).should be false
|
||||
end
|
||||
|
||||
it "a new record equals itself" do
|
||||
a = User.new(name: 'a')
|
||||
(a == a).should be true
|
||||
end
|
||||
|
||||
it "new records are distinct inside a Set / uniq / include?" do
|
||||
a = User.new(name: 'a')
|
||||
b = User.new(name: 'b')
|
||||
[a, b].uniq.size.should eq(2)
|
||||
Set.new([a, b]).size.should eq(2)
|
||||
[a].include?(b).should be false
|
||||
end
|
||||
end
|
||||
|
||||
describe "persisted records" do
|
||||
it "two instances with the same _id and _rev are equal" do
|
||||
a = User.new(name: 'a')
|
||||
b = User.new(name: 'b')
|
||||
a._id = b._id = 'same-id'
|
||||
a._rev = b._rev = '1-abc'
|
||||
(a == b).should be true
|
||||
a.hash.should eq(b.hash)
|
||||
end
|
||||
|
||||
it "same _id but different _rev are not equal" do
|
||||
a = User.new
|
||||
b = User.new
|
||||
a._id = b._id = 'same-id'
|
||||
a._rev = '1-abc'
|
||||
b._rev = '2-def'
|
||||
(a == b).should be false
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,236 @@
|
||||
require 'spec_helper'
|
||||
|
||||
|
||||
describe "Finder" do
|
||||
context "when finding instances" do
|
||||
context "with find(:all)" do
|
||||
before do
|
||||
User.create(title: "Mr.")
|
||||
User.create(title: "Mrs.")
|
||||
end
|
||||
|
||||
it "return all instances" do
|
||||
User.find(:all).size.should eq 2
|
||||
end
|
||||
|
||||
it "allow a limit" do
|
||||
User.find(:all, limit: 1).size.should eq 1
|
||||
end
|
||||
|
||||
it "allow to order the results" do
|
||||
User.find(:all, order: :desc).map(&:id).should_not eq User.find(:all).map(&:id)
|
||||
User.find(:all, order: :desc).map(&:id).should eq User.find(:all).map(&:id).reverse
|
||||
end
|
||||
end
|
||||
|
||||
context "to find all instances" do
|
||||
it 'generate a default find_all view' do
|
||||
User.should respond_to :all_documents
|
||||
end
|
||||
|
||||
it 'return all the users when calling all' do
|
||||
User.create(title: "Mr.")
|
||||
User.create(title: "Mrs.")
|
||||
User.all.size.should eq 2
|
||||
end
|
||||
end
|
||||
|
||||
context "to find one instance" do
|
||||
it 'return one user when calling first' do
|
||||
user = User.create(title: "Mr.")
|
||||
User.first.should eq user
|
||||
end
|
||||
|
||||
it 'understand the order' do
|
||||
expect{ User.first(order: :desc) }.not_to raise_error
|
||||
end
|
||||
|
||||
it 'find the last as a reverse first' do
|
||||
expect( User ).to receive(:find).with(:first, {order: :desc})
|
||||
User.last
|
||||
end
|
||||
|
||||
it 'return nil when no user found' do
|
||||
User.first.should be nil
|
||||
end
|
||||
end
|
||||
|
||||
context "when finding with just an identifier" do
|
||||
it "find just one instance" do
|
||||
user = User.create(title: "Mr.")
|
||||
User.find(user.id).should be_kind_of User
|
||||
end
|
||||
|
||||
it 'raise an error when no record was found and tell you which class failed to load something' do
|
||||
expect{ User.find 'abc' }.to raise_error SimplyBased::RecordNotFound, "User could not be found with \"abc\""
|
||||
end
|
||||
|
||||
it 'raise an error when nil was specified' do
|
||||
expect{ User.find(nil) }.to raise_error SimplyBased::Error
|
||||
end
|
||||
|
||||
it 'raise an error when the record was not of the expected type' do
|
||||
post = Post.create
|
||||
expect{ User.find(post.id) }.to raise_error SimplyBased::RecordNotFound
|
||||
end
|
||||
end
|
||||
|
||||
context "with a find_by prefix" do
|
||||
it "create a view for the called finder" do
|
||||
User.find_by_name("joe")
|
||||
User.should respond_to :by_name
|
||||
end
|
||||
|
||||
it 'not create the view when it already exists' do
|
||||
expect( User ).not_to receive :view
|
||||
User.find_by_name_and_created_at("joe", 'foo')
|
||||
end
|
||||
|
||||
it "create a method to prevent future loops through method_missing" do
|
||||
expect(User).to receive(:method_missing).and_call_original
|
||||
User.find_by_title("Mr.")
|
||||
User.find_by_title("Mrs.")
|
||||
end
|
||||
|
||||
it "call the generated view and return the result" do
|
||||
user = User.create(homepage: "http://www.peritor.com", title: "Mr.")
|
||||
User.find_by_homepage("http://www.peritor.com").should eq user
|
||||
end
|
||||
|
||||
it 'find only one instance when using find_by' do
|
||||
User.create(title: "Mr.")
|
||||
User.find_by_title("Mr.").should be_a User
|
||||
end
|
||||
|
||||
it "find a record through an association" do
|
||||
user = User.create(title: "Mr.")
|
||||
post = Post.create user: user
|
||||
Post.find_by_user(user).should eq post
|
||||
end
|
||||
|
||||
it "find a record through an association that raises when not found" do
|
||||
user = User.create(title: "Mr.")
|
||||
post = Post.create user: user
|
||||
Post.find_by_user!(user).should eq post
|
||||
end
|
||||
|
||||
it "raise an error if the parameters don't match" do
|
||||
expect{ User.find_by_title() }.to raise_error ArgumentError
|
||||
expect{ User.find_by_title(1,2,3,4,5) }.to raise_error ArgumentError
|
||||
end
|
||||
|
||||
it "return nil when no result is found" do
|
||||
User.find_by_title("Mr.").should be nil
|
||||
end
|
||||
|
||||
it "return nil for multiple finders when no result is found" do
|
||||
User.find_by_title_and_homepage("Mr.", "http://www.companytools.nl/").should be nil
|
||||
end
|
||||
|
||||
it "find a record when it ends with an exclamation mark!" do
|
||||
User.create(title: "Mr.")
|
||||
User.find_by_title!("Mr.").should be_a User
|
||||
end
|
||||
|
||||
it "return raise a not found exception when called using an exclamation mark! and is no record is found" do
|
||||
expect{ User.find_by_title!("Mr.") }.to raise_error SimplyBased::RecordNotFound
|
||||
end
|
||||
|
||||
it "find a record using multiple arguments when it ends with an exclamation mark!" do
|
||||
User.create(title: "Mr.", homepage: "http://www.companytools.nl/")
|
||||
User.find_by_title_and_homepage!("Mr.", "http://www.companytools.nl/").should be_a User
|
||||
end
|
||||
|
||||
it "return raise a not found exception when called using an exclamation mark! and no record is found" do
|
||||
expect{ User.find_by_title_and_homepage!("Mr.", "http://www.companytools.nl/") }.to raise_error SimplyBased::RecordNotFound
|
||||
end
|
||||
end
|
||||
|
||||
context "with a find_all_by prefix" do
|
||||
it "create a view for the called finder" do
|
||||
User.find_all_by_name("joe")
|
||||
User.should respond_to :by_name
|
||||
end
|
||||
|
||||
it 'not create the view when it already exists' do
|
||||
expect( User ).not_to receive(:view)
|
||||
User.find_all_by_name_and_created_at("joe", "foo")
|
||||
end
|
||||
|
||||
it "create a method to prevent future loops through method_missing" do
|
||||
expect(User).to receive(:method_missing).and_call_original
|
||||
User.find_all_by_foo_attribute("Mr.")
|
||||
User.find_all_by_foo_attribute("Mrs.")
|
||||
end
|
||||
|
||||
it "call the generated view and return the result" do
|
||||
user = User.create(homepage: "http://www.peritor.com", title: "Mr.")
|
||||
User.find_all_by_homepage("http://www.peritor.com").should eq [user]
|
||||
end
|
||||
|
||||
it "return an emtpy array if none found" do
|
||||
User.find_all_by_title('Mr. Magoooo').should be_empty
|
||||
end
|
||||
|
||||
it 'find all instances when using find_all_by' do
|
||||
User.create(title: "Mr.")
|
||||
User.create(title: "Mr.")
|
||||
User.find_all_by_title("Mr.").size.should eq 2
|
||||
end
|
||||
|
||||
it "find all instances when specifying keys" do
|
||||
User.create name: 'john', title: 'Mr.'
|
||||
User.create name: 'doe', title: 'Mr.'
|
||||
User.find_all_by_name(keys: ['john', 'doe', 'jane']).map(&:name).should match_array %w[doe john]
|
||||
end
|
||||
|
||||
it "raise an error if the parameters don't match" do
|
||||
expect{ User.find_all_by_title() }.to raise_error ArgumentError
|
||||
expect{ User.find_all_by_title(1,2,3,4,5) }.to raise_error ArgumentError
|
||||
end
|
||||
|
||||
it "return an empty array for multiple finders when no result is found" do
|
||||
User.find_all_by_title_and_homepage("Mr.", "http://www.companytools.nl/").should be_empty
|
||||
end
|
||||
|
||||
it "find records when the finder ends with an exclamation mark!" do
|
||||
u1 = User.create(name: 'john', title: "Mr.")
|
||||
u2 = User.create(name: 'doe', title: "Mr.")
|
||||
User.find_all_by_title!("Mr.").map(&:name).should match_array %w[doe john]
|
||||
end
|
||||
|
||||
it "return raise a not found exception when called using an exclamation mark! and is no records are found" do
|
||||
expect{ User.find_all_by_title!("Mr.") }.to raise_error SimplyBased::RecordNotFound
|
||||
end
|
||||
|
||||
it "find records using multiple arguments when the finder ends with an exclamation mark!" do
|
||||
User.create(name: 'john', title: "Mr.", homepage: "http://www.companytools.nl/")
|
||||
User.create(name: 'doe', title: "Mr.", homepage: "http://www.companytools.nl/")
|
||||
User.create(name: 'juan', title: "Mr.", homepage: "http://www.peritor.com/")
|
||||
User.find_all_by_title_and_homepage!("Mr.", "http://www.companytools.nl/").map(&:name).should match_array %w[doe john]
|
||||
end
|
||||
|
||||
it "return raise a not found exception when the finder ends with an exclamation mark! and no records are found" do
|
||||
expect { User.find_all_by_title_and_homepage!("Mr.", "http://www.companytools.nl/") }.to raise_error SimplyBased::RecordNotFound
|
||||
end
|
||||
it "find all records through an association" do
|
||||
user1 = User.create(title: "Mr.")
|
||||
user2 = User.create(title: "Mrs.")
|
||||
post1 = Post.create user: user1
|
||||
post2 = Post.create user: user1
|
||||
post3 = Post.create user: user2
|
||||
Post.find_all_by_user(user1).should match_array [post1, post2]
|
||||
end
|
||||
|
||||
it "find all records through an association that raises when not found" do
|
||||
user1 = User.create(title: "Mr.")
|
||||
user2 = User.create(title: "Mrs.")
|
||||
post1 = Post.create user: user1
|
||||
post2 = Post.create user: user1
|
||||
post3 = Post.create user: user2
|
||||
Post.find_all_by_user!(user1).should match_array [post1, post2]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
Vendored
+354
@@ -0,0 +1,354 @@
|
||||
class User
|
||||
include SimplyBased::Model
|
||||
validates_presence_of :title
|
||||
|
||||
property :name
|
||||
property :title
|
||||
property :homepage
|
||||
|
||||
has_many :posts
|
||||
has_many :strict_posts
|
||||
has_many :hemorrhoids
|
||||
has_many :pains, through: :hemorrhoids
|
||||
has_many :docs, class_name: "Document", foreign_key: "editor_id"
|
||||
|
||||
view :by_name_and_created_at, key: [:name, :created_at]
|
||||
end
|
||||
|
||||
class Post
|
||||
include SimplyBased::Model
|
||||
|
||||
belongs_to :user
|
||||
has_many_embedded :embedded_comments
|
||||
view :by_user_id, key: :user_id
|
||||
end
|
||||
|
||||
class EmbeddedComment
|
||||
include SimplyBased::Model
|
||||
property :body
|
||||
is_embedded_in :post
|
||||
belongs_to :strict_post
|
||||
end
|
||||
|
||||
class Page
|
||||
include SimplyBased::Model
|
||||
|
||||
property :categories, type: Array
|
||||
|
||||
validates_containment_of :categories, in: %w[one two three]
|
||||
end
|
||||
|
||||
class Directory
|
||||
include SimplyBased::Model
|
||||
|
||||
property :name
|
||||
property :make_invalid
|
||||
|
||||
has_ancestry
|
||||
|
||||
validate :check_if_valid
|
||||
|
||||
private
|
||||
|
||||
def check_if_valid
|
||||
errors.add(:make_invalid, :invalid) if make_invalid.present?
|
||||
end
|
||||
end
|
||||
|
||||
class NamespacedDirectory
|
||||
include SimplyBased::Model
|
||||
|
||||
property :name
|
||||
property :locale
|
||||
|
||||
has_ancestry by_property: :locale
|
||||
end
|
||||
|
||||
class StrictPost
|
||||
include SimplyBased::Model
|
||||
|
||||
belongs_to :user
|
||||
|
||||
validates_presence_of :user
|
||||
has_many :embedded_comments
|
||||
end
|
||||
|
||||
class Comment
|
||||
include SimplyBased::Model
|
||||
|
||||
total_pages_method 'total_pages_modified'
|
||||
current_page_method 'current_page_modified'
|
||||
num_pages_method 'num_pages_modified'
|
||||
per_page_method 'per_page_modified'
|
||||
|
||||
belongs_to :user
|
||||
belongs_to :network
|
||||
end
|
||||
|
||||
class Category
|
||||
include SimplyBased::Model
|
||||
|
||||
property :name
|
||||
property :alias
|
||||
property :parent
|
||||
|
||||
validates_inclusion_of :name, in: ["food", "drinks", "party"], allow_blank: true
|
||||
end
|
||||
|
||||
class Document
|
||||
include SimplyBased::Model
|
||||
|
||||
belongs_to :author, class_name: "User"
|
||||
belongs_to :editor, class_name: "User"
|
||||
end
|
||||
|
||||
class Tag
|
||||
include SimplyBased::Model
|
||||
|
||||
belongs_to :category
|
||||
property :name
|
||||
end
|
||||
|
||||
class Instance
|
||||
include SimplyBased::Model
|
||||
has_one :identity
|
||||
end
|
||||
|
||||
class Identity
|
||||
include SimplyBased::Model
|
||||
belongs_to :instance
|
||||
belongs_to :magazine
|
||||
end
|
||||
|
||||
class Magazine
|
||||
include SimplyBased::Model
|
||||
has_one :identity, dependent: :destroy
|
||||
end
|
||||
|
||||
|
||||
|
||||
class UniqueUser
|
||||
include SimplyBased::Model
|
||||
|
||||
property :name
|
||||
validates_uniqueness_of :name
|
||||
end
|
||||
|
||||
class UniqueUserWithAView
|
||||
include SimplyBased::Model
|
||||
|
||||
view :by_name, key: :email
|
||||
property :name
|
||||
validates_uniqueness_of :name
|
||||
end
|
||||
|
||||
class CountMe
|
||||
include SimplyBased::Model
|
||||
|
||||
property :title
|
||||
end
|
||||
|
||||
class DontCountMe
|
||||
include SimplyBased::Model
|
||||
|
||||
property :title
|
||||
end
|
||||
|
||||
class Journal
|
||||
include SimplyBased::Model
|
||||
|
||||
has_many :memberships, dependent: :destroy
|
||||
has_many :readers, through: :memberships, dependent: :destroy
|
||||
property :foo
|
||||
end
|
||||
|
||||
class Reader
|
||||
include SimplyBased::Model
|
||||
|
||||
has_many :memberships, dependent: :destroy
|
||||
has_many :journals, through: :memberships
|
||||
end
|
||||
|
||||
class Membership
|
||||
include SimplyBased::Model
|
||||
|
||||
belongs_to :reader
|
||||
belongs_to :journal
|
||||
end
|
||||
|
||||
class Callbacker
|
||||
include SimplyBased::Model
|
||||
property :name
|
||||
property :raise_on_save, type: :boolean, default: false
|
||||
property :with_validation_error, type: :boolean, default: false
|
||||
property :counter, type: Integer, default: 0
|
||||
|
||||
before_save ->{ self.counter += 1 }
|
||||
|
||||
after_save :raise_error_after_save
|
||||
before_validation do
|
||||
errors.add(:base, :oops) if with_validation_error
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def raise_error_after_save
|
||||
raise "save is called and expected to to raise" if raise_on_save
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
class Hemorrhoid
|
||||
include SimplyBased::Model
|
||||
|
||||
enable_soft_delete
|
||||
|
||||
view :by_nickname_and_size, key: [:nickname, :size]
|
||||
|
||||
property :nickname
|
||||
property :size
|
||||
belongs_to :user
|
||||
belongs_to :pain
|
||||
belongs_to :spot
|
||||
has_many :sub_hemorrhoids, dependent: :destroy
|
||||
has_many :easy_sub_hemorrhoids, dependent: :destroy
|
||||
has_many :rashs, dependent: :nullify
|
||||
has_many :small_rashs, dependent: :nullify
|
||||
|
||||
before_destroy :before_destroy_callback
|
||||
after_destroy :after_destroy_callback
|
||||
|
||||
def before_destroy_callback
|
||||
end
|
||||
|
||||
def after_destroy_callback
|
||||
end
|
||||
end
|
||||
|
||||
class SubHemorrhoid
|
||||
include SimplyBased::Model
|
||||
|
||||
enable_soft_delete
|
||||
|
||||
belongs_to :hemorrhoid
|
||||
end
|
||||
|
||||
class EasySubHemorrhoid
|
||||
include SimplyBased::Model
|
||||
|
||||
belongs_to :hemorrhoid
|
||||
end
|
||||
|
||||
class Rash
|
||||
include SimplyBased::Model
|
||||
|
||||
belongs_to :hemorrhoid
|
||||
end
|
||||
|
||||
class SmallRash
|
||||
include SimplyBased::Model
|
||||
|
||||
enable_soft_delete
|
||||
|
||||
belongs_to :hemorrhoid
|
||||
end
|
||||
|
||||
class Pain
|
||||
include SimplyBased::Model
|
||||
|
||||
has_many :hemorrhoids
|
||||
has_many :users, through: :hemorrhoids
|
||||
end
|
||||
|
||||
class Spot
|
||||
include SimplyBased::Model
|
||||
|
||||
has_one :hemorrhoid
|
||||
end
|
||||
|
||||
class Master
|
||||
include SimplyBased::Model
|
||||
|
||||
has_many :servants, dependent: :ignore
|
||||
end
|
||||
|
||||
class Servant
|
||||
include SimplyBased::Model
|
||||
|
||||
belongs_to :master
|
||||
end
|
||||
|
||||
class Issue
|
||||
include SimplyBased::Model
|
||||
|
||||
belongs_to :problem
|
||||
belongs_to :big_problem
|
||||
|
||||
property :name
|
||||
end
|
||||
|
||||
class Problem
|
||||
include SimplyBased::Model
|
||||
|
||||
has_many :issues
|
||||
has_one :issue
|
||||
end
|
||||
|
||||
class BigProblem < Problem
|
||||
|
||||
end
|
||||
|
||||
class Server
|
||||
include SimplyBased::Model
|
||||
|
||||
property :hostname
|
||||
|
||||
has_and_belongs_to_many :networks, storing_keys: true
|
||||
has_and_belongs_to_many :subnets, storing_keys: true
|
||||
has_and_belongs_to_many :ips, storing_keys: false
|
||||
end
|
||||
|
||||
class Network
|
||||
include SimplyBased::Model
|
||||
|
||||
property :klass
|
||||
|
||||
has_and_belongs_to_many :servers, storing_keys: false
|
||||
has_and_belongs_to_many :routers, storing_keys: false
|
||||
end
|
||||
|
||||
class Subnet < Network
|
||||
has_and_belongs_to_many :servers, storing_keys: false
|
||||
end
|
||||
|
||||
class Ip
|
||||
include SimplyBased::Model
|
||||
|
||||
has_and_belongs_to_many :servers, storing_keys: true
|
||||
end
|
||||
|
||||
class Router
|
||||
include SimplyBased::Model
|
||||
enable_soft_delete
|
||||
|
||||
property :hostname
|
||||
|
||||
has_and_belongs_to_many :networks, storing_keys: true
|
||||
end
|
||||
|
||||
class Book
|
||||
include SimplyBased::Model
|
||||
|
||||
property :title
|
||||
|
||||
has_and_belongs_to_many :authors, storing_keys: true
|
||||
end
|
||||
|
||||
class Author
|
||||
include SimplyBased::Model
|
||||
enable_soft_delete
|
||||
|
||||
property :name
|
||||
|
||||
has_and_belongs_to_many :books, storing_keys: false
|
||||
end
|
||||
@@ -0,0 +1,639 @@
|
||||
require 'spec_helper'
|
||||
|
||||
describe "has_and_belongs_to_many" do
|
||||
|
||||
context "with has_and_belongs_to_many" do
|
||||
it "create a fetch method for the associated objects" do
|
||||
server = Server.new
|
||||
server.should respond_to :networks
|
||||
|
||||
network = Network.new
|
||||
network.should respond_to :servers
|
||||
end
|
||||
|
||||
it "raise an error if another property with the same name already exists" do
|
||||
expect {
|
||||
class ::DoubleHasAdnBelongsToManyServer
|
||||
include SimplyBased::Model
|
||||
property :other_users
|
||||
has_and_belongs_to_many :other_users
|
||||
end
|
||||
}.to raise_error RuntimeError
|
||||
end
|
||||
|
||||
it "fetch the associated objects" do
|
||||
network = Network.create(klass: "A")
|
||||
3.times {
|
||||
server = Server.new
|
||||
server.network_ids = [network.id]
|
||||
server.save!
|
||||
}
|
||||
network.servers.size.should eq 3
|
||||
end
|
||||
|
||||
it "not add empty relations" do
|
||||
# Checkbox assignment often gives an empty one to
|
||||
# set array to [] when none is selected
|
||||
server = Server.new
|
||||
network = Network.create(klass: "Q")
|
||||
server.network_ids = ["", nil, network.id]
|
||||
server.network_ids.size.should eq 1
|
||||
server.save!
|
||||
server.networks.size.should eq 1
|
||||
end
|
||||
|
||||
it "fetch the associated objects from the other side of the relation" do
|
||||
network = Network.create(klass: "A")
|
||||
3.times {
|
||||
server = Server.new
|
||||
server.network_ids = [network.id]
|
||||
server.save!
|
||||
}
|
||||
Server.first.networks.size.should eq 1
|
||||
end
|
||||
|
||||
it "set the parent object on the clients cache" do
|
||||
expect( Network ).not_to receive(:find)
|
||||
network = Network.create(klass: "A")
|
||||
3.times {
|
||||
server = Server.new
|
||||
server.add_network(network)
|
||||
}
|
||||
network.servers.first.networks.first.should eq network
|
||||
end
|
||||
|
||||
it "set the parent object on the clients cache from the other side of the relation" do
|
||||
expect( Server ).not_to receive(:find)
|
||||
network = Network.create(klass: "A")
|
||||
3.times {
|
||||
server = Server.new
|
||||
network.add_server(server)
|
||||
}
|
||||
network.servers.first.networks.first.should eq network
|
||||
end
|
||||
|
||||
it "work relations from both sides" do
|
||||
network_a = Network.create(klass: "A")
|
||||
network_b = Network.create(klass: "B")
|
||||
3.times {
|
||||
server = Server.new
|
||||
server.add_network(network_a)
|
||||
server.add_network(network_b)
|
||||
}
|
||||
network_a.servers.size.should eq 3
|
||||
network_a.servers.each do |server|
|
||||
server.networks.size.should eq 2
|
||||
end
|
||||
network_b.servers.size.should eq 3
|
||||
network_b.servers.each do |server|
|
||||
server.networks.size.should eq 2
|
||||
end
|
||||
end
|
||||
|
||||
it "work relations from both sides - regardless from where the add was called" do
|
||||
network_a = Network.create(klass: "A")
|
||||
network_b = Network.create(klass: "B")
|
||||
3.times {
|
||||
server = Server.new
|
||||
network_a.add_server(server)
|
||||
network_b.add_server(server)
|
||||
}
|
||||
network_a.servers.size.should eq 3
|
||||
network_a.servers.each do |server|
|
||||
server.networks.size.should eq 2
|
||||
end
|
||||
network_b.servers.size.should eq 3
|
||||
network_b.servers.each do |server|
|
||||
server.networks.size.should eq 2
|
||||
end
|
||||
end
|
||||
|
||||
context "limit" do
|
||||
|
||||
it "be able to limit the result set" do
|
||||
network = Network.create(klass: "A")
|
||||
3.times {
|
||||
server = Server.new
|
||||
server.add_network(network)
|
||||
}
|
||||
network.servers(limit: 2).size.should eq 2
|
||||
end
|
||||
|
||||
it "use the given options in the cache-key" do
|
||||
network = Network.create(klass: "A")
|
||||
3.times {
|
||||
server = Server.new
|
||||
server.add_network(network)
|
||||
}
|
||||
network.servers(limit: 2).size.should eq 2
|
||||
network.servers(limit: 3).size.should eq 3
|
||||
end
|
||||
|
||||
it "be able to limit the result set - for both directions" do
|
||||
network_a = Network.create(klass: "A")
|
||||
network_b = Network.create(klass: "B")
|
||||
3.times {
|
||||
server = Server.new
|
||||
server.add_network(network_a)
|
||||
server.add_network(network_b)
|
||||
}
|
||||
network_a.servers(limit: 2).size.should eq 2
|
||||
network_a.servers(limit: 3).size.should eq 3
|
||||
|
||||
network_a.servers.first.networks(limit: 2).size.should eq 2
|
||||
network_a.servers.first.networks(limit: 1).size.should eq 1
|
||||
end
|
||||
end
|
||||
|
||||
context "order" do
|
||||
before do
|
||||
@network = Network.create(klass: "A")
|
||||
@network.created_at = Time.local(2000)
|
||||
@network.save!
|
||||
@network_b = Network.create(klass: "B")
|
||||
@network_b.created_at = Time.local(2002)
|
||||
@network_b.save!
|
||||
3.times do |i|
|
||||
server = Server.new
|
||||
server.add_network(@network)
|
||||
server.add_network(@network_b)
|
||||
server.created_at = Time.local(2000 + i)
|
||||
server.save!
|
||||
end
|
||||
end
|
||||
|
||||
it "support different order" do
|
||||
expect{ @network.servers(order: :asc) }.not_to raise_error
|
||||
expect{ @network.servers(order: :desc) }.not_to raise_error
|
||||
end
|
||||
|
||||
it "reverse the order if :desc" do
|
||||
@network.servers(order: :desc).map(&:id).should eq @network.servers(order: :asc).map(&:id).reverse
|
||||
server = @network.servers.first
|
||||
server.networks(order: :desc).map(&:id).should eq server.networks(order: :asc).map(&:id).reverse
|
||||
end
|
||||
|
||||
it "work with the limit option" do
|
||||
server = Server.new
|
||||
server.add_network(@network)
|
||||
server.add_network(@network_b)
|
||||
@network.servers(order: :asc, limit: 3).map(&:id).reverse.should_not eq @network.servers(order: :desc, limit: 3).map(&:id)
|
||||
server.networks(order: :asc, limit: 1).map(&:id).reverse.should_not eq server.networks(order: :desc, limit: 1).map(&:id)
|
||||
end
|
||||
end
|
||||
|
||||
it "verify the given options for the accessor method" do
|
||||
network = Network.create(klass: "A")
|
||||
expect{ network.servers(foo: :bar) }.to raise_error ArgumentError
|
||||
end
|
||||
|
||||
it "verify the given options for the association defintion" do
|
||||
expect {
|
||||
Network.instance_eval do
|
||||
has_and_belongs_to_many :foo, bar: :do
|
||||
end
|
||||
}.to raise_error ArgumentError
|
||||
end
|
||||
|
||||
it "only fetch objects of the correct type" do
|
||||
network = Network.create(klass: "A")
|
||||
server = Server.new
|
||||
server.network_ids = [network.id]
|
||||
server.save!
|
||||
|
||||
comment = Comment.new
|
||||
comment.network = network
|
||||
comment.save!
|
||||
|
||||
network.servers.size.should eq 1
|
||||
end
|
||||
|
||||
it "getter should user cache" do
|
||||
network = Network.create(klass: "A")
|
||||
server = Server.new
|
||||
server.network_ids = [network.id]
|
||||
server.save!
|
||||
network.servers
|
||||
network.instance_variable_get("@servers")[:all].should eq [server]
|
||||
end
|
||||
|
||||
it "add methods to handle associated objects" do
|
||||
network = Network.create(klass: "A")
|
||||
network.should respond_to :add_server
|
||||
network.should respond_to :remove_server
|
||||
network.should respond_to :remove_all_servers
|
||||
end
|
||||
|
||||
it "add methods to handle associated objects - for the other side too" do
|
||||
server = Server.create
|
||||
server.should respond_to :add_network
|
||||
server.should respond_to :remove_network
|
||||
server.should respond_to :remove_all_networks
|
||||
end
|
||||
|
||||
it 'ignore the cache when requesting explicit reload' do
|
||||
network = Network.create(klass: "A")
|
||||
network.servers.should eq []
|
||||
server = Server.new
|
||||
server.network_ids = [network.id]
|
||||
server.save!
|
||||
network.servers(force_reload: true).should eq [server]
|
||||
end
|
||||
|
||||
it "use the correct view when handling inheritance" do
|
||||
network = Network.create
|
||||
subnet = Subnet.create
|
||||
server = Server.new
|
||||
server.network_ids = [network.id]
|
||||
server.save!
|
||||
network.servers.size.should eq 1
|
||||
server.update_attributes(network_ids: nil, subnet_ids: [subnet.id])
|
||||
subnet.servers.size.should eq 1
|
||||
end
|
||||
|
||||
context "when adding items" do
|
||||
it "add the item to the internal cache" do
|
||||
network = Network.new(klass: "C")
|
||||
server = Server.new
|
||||
network.servers.should eq []
|
||||
network.add_server(server)
|
||||
network.servers.should eq [server]
|
||||
network.instance_variable_get("@servers")[:all].should eq [server]
|
||||
end
|
||||
|
||||
it "raise an error when the added item is not an object of the expected class" do
|
||||
network = Network.new
|
||||
expect{ network.add_server('foo') }.to raise_error ArgumentError, "expected Server got String"
|
||||
end
|
||||
|
||||
it "save the added item" do
|
||||
server = Server.new
|
||||
network = Network.create(klass: "A")
|
||||
network.add_server(server)
|
||||
server.should_not be_new_record
|
||||
end
|
||||
|
||||
it "save the added item - from both directions" do
|
||||
server = Server.new
|
||||
network = Network.create(klass: "A")
|
||||
server.add_network(network)
|
||||
server.should_not be_new_record
|
||||
end
|
||||
|
||||
it 'set the forein key on the added object' do
|
||||
server = Server.new
|
||||
network = Network.create(klass: "A")
|
||||
network.add_server(server)
|
||||
server.network_ids.should eq [network.id]
|
||||
end
|
||||
|
||||
it 'set the forein key on the added object - from both directions' do
|
||||
server = Server.new
|
||||
network = Network.create(klass: "A")
|
||||
server.add_network(network)
|
||||
server.network_ids.should eq [network.id]
|
||||
end
|
||||
|
||||
it "adding multiple times doesn't hurt" do
|
||||
server = Server.new
|
||||
network = Network.create(klass: "A")
|
||||
network.add_server(server)
|
||||
network.add_server(server)
|
||||
server.add_network(network)
|
||||
server.network_ids.should eq [network.id]
|
||||
Server.find(server.id).network_ids.should eq [network.id]
|
||||
end
|
||||
end
|
||||
|
||||
context "when removing items" do
|
||||
it "should unset the foreign key" do
|
||||
server = Server.new
|
||||
network = Network.create(klass: "A")
|
||||
network.add_server(server)
|
||||
|
||||
network.remove_server(server)
|
||||
server.network_ids.should eq []
|
||||
end
|
||||
|
||||
it "should unset the foreign key - from both directions" do
|
||||
server = Server.new
|
||||
network = Network.create(klass: "A")
|
||||
network.add_server(server)
|
||||
|
||||
server.remove_network(network)
|
||||
server.network_ids.should eq []
|
||||
end
|
||||
|
||||
it "remove the item from the cache" do
|
||||
server = Server.new
|
||||
network = Network.create(klass: "A")
|
||||
network.add_server(server)
|
||||
network.servers.should include server
|
||||
network.remove_server(server)
|
||||
network.servers.any?{|s| server.id == s.id}.should_not be true
|
||||
network.instance_variable_get("@servers")[:all].should eq []
|
||||
end
|
||||
|
||||
it "remove the item from the cache - from both directions" do
|
||||
server = Server.new
|
||||
network = Network.create(klass: "A")
|
||||
network.add_server(server)
|
||||
server.networks.should include(network)
|
||||
server.remove_network(network)
|
||||
server.networks.any?{|n| network.id == n.id}.should_not be true
|
||||
server.instance_variable_get("@networks")[:all].should eq []
|
||||
end
|
||||
|
||||
it "save the removed item with the nullified foreign key" do
|
||||
server = Server.new
|
||||
network = Network.create(klass: "A")
|
||||
network.add_server(server)
|
||||
|
||||
network.remove_server(server)
|
||||
server = Server.find(server.id)
|
||||
server.network_ids.should eq []
|
||||
end
|
||||
|
||||
it "save the removed item with the nullified foreign key - from both directions" do
|
||||
server = Server.new
|
||||
network = Network.create(klass: "A")
|
||||
network.add_server(server)
|
||||
|
||||
server.remove_network(network)
|
||||
server = Server.find(server.id)
|
||||
server.network_ids.should eq []
|
||||
end
|
||||
|
||||
it 'raise an error when another object is the owner of the object to be removed' do
|
||||
server = Server.new
|
||||
network = Network.create(klass: "A")
|
||||
network.add_server(server)
|
||||
|
||||
other_server = Server.create
|
||||
expect{ network.remove_server(other_server) }.to raise_error ArgumentError
|
||||
end
|
||||
|
||||
it 'raise an error when another object is the owner of the object to be removed - from both directions' do
|
||||
server = Server.new
|
||||
network = Network.create(klass: "A")
|
||||
network.add_server(server)
|
||||
|
||||
other_network = Network.create
|
||||
expect{ server.remove_network(other_network) }.to raise_error ArgumentError
|
||||
end
|
||||
|
||||
it 'raise an error when the object is the wrong type' do
|
||||
expect{ Network.new.remove_server('foo') }.to raise_error ArgumentError, "expected Server got String"
|
||||
expect{ Server.new.remove_network('foo') }.to raise_error ArgumentError, "expected Network got String"
|
||||
end
|
||||
end
|
||||
|
||||
context "when removing all items" do
|
||||
it 'nullify the foreign keys on all referenced items' do
|
||||
server_1 = Server.new
|
||||
server_2 = Server.new
|
||||
network = Network.create(klass: "A")
|
||||
network.add_server(server_1)
|
||||
network.add_server(server_2)
|
||||
network.remove_all_servers
|
||||
server_1 = Server.find(server_1.id)
|
||||
server_2 = Server.find(server_2.id)
|
||||
server_1.network_ids.should eq []
|
||||
server_2.network_ids.should eq []
|
||||
end
|
||||
|
||||
it 'nullify the foreign keys on all referenced items - from both directions' do
|
||||
server_1 = Server.new
|
||||
server_2 = Server.new
|
||||
network = Network.create(klass: "A")
|
||||
network.add_server(server_1)
|
||||
network.add_server(server_2)
|
||||
server_1.remove_all_networks
|
||||
server_1 = Server.find(server_1.id)
|
||||
server_2 = Server.find(server_2.id)
|
||||
server_1.network_ids.should eq []
|
||||
server_2.network_ids.should eq [network.id]
|
||||
end
|
||||
|
||||
it 'empty the cache' do
|
||||
server_1 = Server.new
|
||||
server_2 = Server.new
|
||||
network = Network.create(klass: "A")
|
||||
network.add_server(server_1)
|
||||
network.add_server(server_2)
|
||||
network.remove_all_servers
|
||||
network.servers.should eq []
|
||||
network.instance_variable_get("@servers")[:all].should eq []
|
||||
end
|
||||
|
||||
it 'empty the cache - from both directions' do
|
||||
server_1 = Server.new
|
||||
server_2 = Server.new
|
||||
network = Network.create(klass: "A")
|
||||
network.add_server(server_1)
|
||||
network.add_server(server_2)
|
||||
server_1.remove_all_networks
|
||||
server_1.networks.should eq []
|
||||
server_1.instance_variable_get("@networks")[:all].should eq []
|
||||
end
|
||||
|
||||
context "when counting" do
|
||||
before do
|
||||
@network = Network.create(klass: "C")
|
||||
@server = Server.create
|
||||
end
|
||||
|
||||
it "define a count method" do
|
||||
@network.should respond_to :server_count
|
||||
@server.should respond_to :network_count
|
||||
end
|
||||
|
||||
it "cache the result" do
|
||||
@network.server_count.should eq 0
|
||||
Server.create(network_ids: [@network.id])
|
||||
@network.server_count.should eq 0
|
||||
@network.instance_variable_get("@server_count").should eq 0
|
||||
@network.instance_variable_set("@server_count", nil)
|
||||
@network.server_count.should eq 1
|
||||
end
|
||||
|
||||
it "cache the result - from both directions" do
|
||||
@server.network_count.should eq 0
|
||||
@server.network_ids = [@network.id]
|
||||
@server.save!
|
||||
@server.network_count.should eq 0
|
||||
@server.instance_variable_get("@network_count").should eq 0
|
||||
@server.instance_variable_set("@network_count", nil)
|
||||
@server.network_count.should eq 1
|
||||
end
|
||||
|
||||
it "force reload even if cached" do
|
||||
@network.server_count.should eq 0
|
||||
Server.create(network_ids: [@network.id])
|
||||
@network.server_count.should eq 0
|
||||
@network.server_count(force_reload: true).should eq 1
|
||||
end
|
||||
|
||||
it "force reload even if cached - from both directions" do
|
||||
@server.network_count.should eq 0
|
||||
@server.network_ids = [@network.id]
|
||||
@server.save!
|
||||
@server.network_count.should eq 0
|
||||
@server.network_count(force_reload: true).should eq 1
|
||||
end
|
||||
|
||||
it "count the number of belongs_to objects" do
|
||||
@network.server_count(force_reload: true).should eq 0
|
||||
Server.create(network_ids: [@network.id])
|
||||
@network.server_count(force_reload: true).should eq 1
|
||||
Server.create(network_ids: [@network.id])
|
||||
@network.server_count(force_reload: true).should eq 2
|
||||
Server.create(network_ids: [@network.id])
|
||||
@network.server_count(force_reload: true).should eq 3
|
||||
end
|
||||
|
||||
it "count the number of belongs_to objects - from both directions" do
|
||||
@server.network_count(force_reload: true).should eq 0
|
||||
@server.network_ids = [@network.id]
|
||||
@server.save!
|
||||
@server.network_count(force_reload: true).should eq 1
|
||||
@server.network_ids = [@network.id, Network.create.id]
|
||||
@server.save!
|
||||
@server.network_count(force_reload: true).should eq 2
|
||||
end
|
||||
|
||||
it "not count non-releated objects" do
|
||||
Server.all.each{|s| s.delete}
|
||||
network_1 = Network.create(klass: "A")
|
||||
network_2 = Network.create(klass: "B")
|
||||
server_1 = Server.create
|
||||
server_2 = Server.create(network_ids: [network_1.id])
|
||||
server_3 = Server.create(network_ids: [network_1.id, network_2.id])
|
||||
Server.count.should eq 3
|
||||
network_1.server_count.should eq 2
|
||||
network_2.server_count.should eq 1
|
||||
server_1.network_count.should eq 0
|
||||
server_2.network_count.should eq 1
|
||||
server_3.network_count.should eq 2
|
||||
end
|
||||
|
||||
it "not count deleted objects" do
|
||||
network = Network.create(klass: "A")
|
||||
server = Server.create(network_ids: [network.id])
|
||||
network.server_count(force_reload: true).should eq 1
|
||||
server.delete
|
||||
network.server_count(force_reload: true).should eq 0
|
||||
end
|
||||
|
||||
it "not count deleted objects - from both directions" do
|
||||
network = Network.create(klass: "A")
|
||||
server = Server.create(network_ids: [network.id])
|
||||
server.network_count(force_reload: true).should eq 1
|
||||
network.delete
|
||||
server.network_count(force_reload: true).should eq 0
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
context "with deleted" do
|
||||
it "not fetch deleted objects" do
|
||||
# matrix gem removed in Ruby 3.4; skipping
|
||||
network = Network.create(klass: "A")
|
||||
server = Server.new
|
||||
server.network_ids = [network.id]
|
||||
server.save!
|
||||
network.servers(force_reload: true).size.should eq 1
|
||||
expect{ server.delete }.to change{ Server.count }.by(-1).and change{ Network.count }.by(0)
|
||||
network.servers(force_reload: true).size.should eq 0
|
||||
end
|
||||
|
||||
it "not fetch deleted objects - from both directions" do
|
||||
# matrix gem removed in Ruby 3.4; skipping
|
||||
network = Network.create(klass: "A")
|
||||
server = Server.new
|
||||
server.network_ids = [network.id]
|
||||
server.save!
|
||||
server.networks(force_reload: true).size.should eq 1
|
||||
expect{ network.delete }.to change{ Network.count }.by(-1).and change{ Server.count }.by(0)
|
||||
server.networks(force_reload: true).size.should eq 0
|
||||
server.reload.network_ids.should eq []
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
context "with soft deleted" do
|
||||
|
||||
it "not load soft deleted - items storing keys" do
|
||||
network = Network.create
|
||||
router = Router.new
|
||||
network.add_router(router)
|
||||
network.routers.size.should eq 1
|
||||
router.delete
|
||||
Router.count.should eq 0
|
||||
Router.count(with_deleted: true).should eq 1
|
||||
network.routers(force_reload: true).size.should eq 0
|
||||
network.routers(force_reload: true, with_deleted: true).size.should eq 1
|
||||
router.delete!
|
||||
network.routers(force_reload: true, with_deleted: true).size.should eq 0
|
||||
end
|
||||
|
||||
it "not count soft deleted - items storing keys" do
|
||||
network = Network.create
|
||||
router = Router.new
|
||||
network.add_router(router)
|
||||
network.routers.size.should eq 1
|
||||
router.delete
|
||||
Router.count.should eq 0
|
||||
Router.count(with_deleted: true).should eq 1
|
||||
network.router_count(force_reload: true).should eq 0
|
||||
network.router_count(force_reload: true, with_deleted: true).should eq 1
|
||||
router.delete!
|
||||
network.router_count(force_reload: true, with_deleted: true).should eq 0
|
||||
end
|
||||
|
||||
it "not load soft deleted - items not storing keys: not supported" do
|
||||
book = Book.create
|
||||
author = Author.create
|
||||
author.add_book(book)
|
||||
author.delete
|
||||
Author.count.should eq 0
|
||||
Author.count(with_deleted: true).should eq 1
|
||||
book.authors(force_reload: true).size.should eq 0
|
||||
book.authors(force_reload: true, with_deleted: true).size.should eq 0
|
||||
end
|
||||
|
||||
it "not count soft deleted - items not storing keys: not supported" do
|
||||
book = Book.create
|
||||
author = Author.create
|
||||
author.add_book(book)
|
||||
author.delete
|
||||
Author.count.should eq 0
|
||||
Author.count(with_deleted: true).should eq 1
|
||||
book.author_count(force_reload: true).should eq 0
|
||||
book.author_count(force_reload: true, with_deleted: true).should eq 0
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
context "when not persisted" do
|
||||
it "not return objects when a record is not persisted on storing keys" do
|
||||
router = Router.new
|
||||
other_router = Router.create
|
||||
network = Network.create
|
||||
other_router.network_ids = [network.id]
|
||||
other_router.save
|
||||
router.networks.should be_empty
|
||||
end
|
||||
it "not return objects when a record is not persisted on non storing keys" do
|
||||
router = Router.create
|
||||
other_router = Router.create
|
||||
other_network = Network.create
|
||||
other_router.network_ids = [other_network.id]
|
||||
network = Network.new
|
||||
network.routers.should be_empty
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,476 @@
|
||||
require 'spec_helper'
|
||||
|
||||
describe "HasMany" do
|
||||
context "has_many" do
|
||||
context "with has_many" do
|
||||
it "create a fetch method for the associated objects" do
|
||||
user = User.new
|
||||
user.should respond_to :posts
|
||||
end
|
||||
|
||||
it "raise an error if another property with the same name already exists" do
|
||||
expect {
|
||||
class ::DoubleHasManyUser
|
||||
include SimplyBased::Model
|
||||
property :other_users
|
||||
has_many :other_users
|
||||
end
|
||||
}.to raise_error RuntimeError
|
||||
end
|
||||
|
||||
it "fetch the associated objects" do
|
||||
user = User.create(:title => "Mr.")
|
||||
3.times {
|
||||
post = Post.new
|
||||
post.user = user
|
||||
post.save!
|
||||
}
|
||||
user.posts.size.should eq 3
|
||||
end
|
||||
|
||||
it "set the parent object on the clients cache" do
|
||||
expect( User ).not_to receive(:find)
|
||||
user = User.create(:title => "Mr.")
|
||||
3.times {
|
||||
post = Post.new
|
||||
post.user = user
|
||||
post.save!
|
||||
}
|
||||
post = user.posts.first
|
||||
user.posts.first.user.should eq user
|
||||
end
|
||||
|
||||
context "limit" do
|
||||
|
||||
it "be able to limit the result set" do
|
||||
user = User.create(:title => "Mr.")
|
||||
3.times {
|
||||
post = Post.new
|
||||
post.user = user
|
||||
post.save!
|
||||
}
|
||||
user.posts(:limit => 2).size.should eq 2
|
||||
end
|
||||
|
||||
it "use the given options in the cache-key" do
|
||||
user = User.create(:title => "Mr.")
|
||||
3.times {
|
||||
post = Post.new
|
||||
post.user = user
|
||||
post.save!
|
||||
}
|
||||
user.posts(:limit => 2).size.should eq 2
|
||||
user.posts(:limit => 3).size.should eq 3
|
||||
end
|
||||
|
||||
it "be able to limit the result set - also for through objects" do
|
||||
@user = User.create(:title => "Mr.")
|
||||
first_pain = Pain.create
|
||||
frist_hemorrhoid = Hemorrhoid.create(:user => @user, :pain => first_pain)
|
||||
@user.pains.should eq [first_pain]
|
||||
second_pain = Pain.create
|
||||
second_hemorrhoid = Hemorrhoid.create(:user => @user, :pain => second_pain)
|
||||
@user.reload
|
||||
@user.pains.size.should eq 2
|
||||
@user.pains(:limit => 1).size.should eq 1
|
||||
end
|
||||
end
|
||||
|
||||
context "order" do
|
||||
before do
|
||||
@user = User.create(:title => "Mr.")
|
||||
3.times {
|
||||
post = Post.new
|
||||
post.user = @user
|
||||
post.save!
|
||||
}
|
||||
end
|
||||
|
||||
it "support different order" do
|
||||
expect{ @user.posts(:order => :asc) }.not_to raise_error
|
||||
expect { @user.posts(:order => :desc) }.not_to raise_error
|
||||
end
|
||||
|
||||
it "reverse the order if :desc" do
|
||||
@user.posts(:order => :desc).map(&:id).should eq @user.posts(:order => :asc).map(&:id).reverse
|
||||
end
|
||||
|
||||
it "work with the limit option" do
|
||||
last_post = Post.create(:user => @user)
|
||||
@user.posts(:order => :asc, :limit => 3).map(&:id).reverse.should_not eq @user.posts(:order => :desc, :limit => 3).map(&:id)
|
||||
end
|
||||
end
|
||||
|
||||
it "verify the given options for the accessor method" do
|
||||
user = User.create(:title => "Mr.")
|
||||
expect { user.posts(:foo => false) }.to raise_error ArgumentError
|
||||
end
|
||||
|
||||
it "verify the given options for the association defintion" do
|
||||
expect {
|
||||
User.instance_eval do
|
||||
has_many :foo, :bar => :do
|
||||
end
|
||||
}.to raise_error ArgumentError
|
||||
end
|
||||
|
||||
it "only fetch objects of the correct type" do
|
||||
user = User.create(:title => "Mr.")
|
||||
post = Post.new
|
||||
post.user = user
|
||||
post.save!
|
||||
|
||||
comment = Comment.new
|
||||
comment.user = user
|
||||
comment.save!
|
||||
|
||||
user.posts.size.should eq 1
|
||||
end
|
||||
|
||||
it "getter should user cache" do
|
||||
user = User.create(:title => "Mr.")
|
||||
post = Post.new
|
||||
post.user = user
|
||||
post.save!
|
||||
user.posts
|
||||
user.instance_variable_get("@posts")[:all].should eq [post]
|
||||
end
|
||||
|
||||
it "add methods to handle associated objects" do
|
||||
user = User.new(:title => "Mr.")
|
||||
user.should respond_to :add_post
|
||||
user.should respond_to :remove_post
|
||||
user.should respond_to :remove_all_posts
|
||||
end
|
||||
|
||||
it 'ignore the cache when requesting explicit reload' do
|
||||
user = User.create(:title => "Mr.")
|
||||
user.posts.should eq []
|
||||
post = Post.new
|
||||
post.user = user
|
||||
post.save!
|
||||
user.posts(:force_reload => true).should eq [post]
|
||||
end
|
||||
|
||||
it "use the correct view when handling inheritance" do
|
||||
problem = Problem.create
|
||||
big_problem = BigProblem.create
|
||||
issue = Issue.create(:name => 'Thing', :problem => problem)
|
||||
problem.issues.size.should eq 1
|
||||
issue.update_attributes(:problem_id => nil, :big_problem_id => big_problem.id)
|
||||
big_problem.issues.size.should eq 1
|
||||
end
|
||||
|
||||
context "when adding items" do
|
||||
it "add the item to the internal cache" do
|
||||
daddy = User.new(:title => "Mr.")
|
||||
item = Post.new
|
||||
daddy.posts.should eq []
|
||||
daddy.add_post(item)
|
||||
daddy.posts.should eq [item]
|
||||
daddy.instance_variable_get("@posts")[:all].should eq [item]
|
||||
end
|
||||
|
||||
it "raise an error when the added item is not an object of the expected class" do
|
||||
user = User.new
|
||||
expect { user.add_post('foo') }.to raise_error(ArgumentError, "expected Post got String")
|
||||
end
|
||||
|
||||
it "save the added item" do
|
||||
post = Post.new
|
||||
user = User.create(:title => "Mr.")
|
||||
user.add_post(post)
|
||||
post.should_not be_a_new_record
|
||||
end
|
||||
|
||||
it 'set the forein key on the added object' do
|
||||
post = Post.new
|
||||
user = User.create(:title => "Mr.")
|
||||
user.add_post(post)
|
||||
post.user_id.should eq user.id
|
||||
end
|
||||
end
|
||||
|
||||
context "when removing items" do
|
||||
it "should unset the foreign key" do
|
||||
user = User.create(:title => "Mr.")
|
||||
post = Post.create(:user => user)
|
||||
|
||||
user.remove_post(post)
|
||||
post.user_id.should be nil
|
||||
end
|
||||
|
||||
it "remove the item from the cache" do
|
||||
user = User.create(:title => "Mr.")
|
||||
post = Post.create(:user => user)
|
||||
user.posts.should include post
|
||||
user.remove_post(post)
|
||||
user.posts.any?{|p| post.id == p.id}.should_not be true
|
||||
user.instance_variable_get("@posts")[:all].should eq []
|
||||
end
|
||||
|
||||
it "save the removed item with the nullified foreign key" do
|
||||
user = User.create(:title => "Mr.")
|
||||
post = Post.create(:user => user)
|
||||
|
||||
user.remove_post(post)
|
||||
post = Post.find(post.id)
|
||||
post.user_id.should be nil
|
||||
end
|
||||
|
||||
it 'raise an error when another object is the owner of the object to be removed' do
|
||||
user = User.create(:title => "Mr.")
|
||||
mrs = User.create(:title => "Mrs.")
|
||||
post = Post.create(:user => user)
|
||||
expect{ mrs.remove_post post }.to raise_error ArgumentError
|
||||
end
|
||||
|
||||
it 'raise an error when the object is the wrong type' do
|
||||
user = User.new
|
||||
expect{ user.remove_post 'foo' }.to raise_error(ArgumentError, 'expected Post got String')
|
||||
end
|
||||
|
||||
it "delete the object when dependent:destroy" do
|
||||
Category.instance_eval do
|
||||
has_many :tags, :dependent => :destroy
|
||||
end
|
||||
|
||||
category = Category.create(:name => "food")
|
||||
tag = Tag.create(:name => "food", :category => category)
|
||||
tag.should_not be_new
|
||||
category.remove_tag(tag)
|
||||
|
||||
Tag.find(:all).should eq []
|
||||
end
|
||||
|
||||
it "not nullify or delete dependents if the options is set to :ignore when removing" do
|
||||
master = Master.create
|
||||
master_id = master.id
|
||||
servant = Servant.create(:master => master)
|
||||
master.remove_servant(servant)
|
||||
servant.reload.master_id.should eq master_id
|
||||
end
|
||||
|
||||
it "not nullify or delete dependents if the options is set to :ignore when deleting" do
|
||||
master = Master.create
|
||||
master_id = master.id
|
||||
servant = Servant.create(:master => master)
|
||||
master.destroy
|
||||
servant.reload.master_id.should eq master_id
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
context "when removing all items" do
|
||||
it 'nullify the foreign keys on all referenced items' do
|
||||
user = User.create(:title => "Mr.")
|
||||
post = Post.create(:user => user)
|
||||
post2 = Post.create(:user => user)
|
||||
user.remove_all_posts
|
||||
post = Post.find(post.id)
|
||||
post2 = Post.find(post2.id)
|
||||
post.user_id.should be nil
|
||||
post2.user_id.should be nil
|
||||
end
|
||||
|
||||
it 'empty the cache' do
|
||||
user = User.create(:title => "Mr.")
|
||||
post = Post.create(:user => user)
|
||||
post2 = Post.create(:user => user)
|
||||
user.remove_all_posts
|
||||
user.posts.should eq []
|
||||
user.instance_variable_get("@posts")[:all].should eq []
|
||||
end
|
||||
|
||||
context "when counting" do
|
||||
before do
|
||||
@user = User.create(:title => "Mr.")
|
||||
end
|
||||
|
||||
it "define a count method" do
|
||||
@user.should respond_to :post_count
|
||||
end
|
||||
|
||||
it "cache the result" do
|
||||
@user.post_count.should eq 0
|
||||
Post.create(:user => @user)
|
||||
@user.post_count.should eq 0
|
||||
@user.instance_variable_get("@post_count").should eq 0
|
||||
@user.instance_variable_set("@post_count", nil)
|
||||
@user.post_count.should eq 1
|
||||
end
|
||||
|
||||
it "force reload even if cached" do
|
||||
@user.post_count.should eq 0
|
||||
Post.create(:user => @user)
|
||||
@user.post_count.should eq 0
|
||||
@user.post_count(:force_reload => true).should eq 1
|
||||
end
|
||||
|
||||
it "count the number of belongs_to objects" do
|
||||
@user.post_count(:force_reload => true).should eq 0
|
||||
Post.create(:user => @user)
|
||||
@user.post_count(:force_reload => true).should eq 1
|
||||
Post.create(:user => @user)
|
||||
@user.post_count(:force_reload => true).should eq 2
|
||||
end
|
||||
|
||||
it "not count foreign objects" do
|
||||
@user.post_count.should eq 0
|
||||
Post.create(:user => nil)
|
||||
Post.create(:user => User.create(:title => 'Doc'))
|
||||
@user.post_count.should eq 0
|
||||
Post.count.should eq 2
|
||||
end
|
||||
|
||||
it "not count delete objects" do
|
||||
hemorrhoid = Hemorrhoid.create(:user => @user)
|
||||
@user.hemorrhoid_count.should eq 1
|
||||
hemorrhoid.delete
|
||||
@user.hemorrhoid_count(:force_reload => true).should eq 0
|
||||
@user.hemorrhoid_count(:force_reload => true, :with_deleted => true).should eq 1
|
||||
end
|
||||
|
||||
it "work with has_many :through" do
|
||||
@user.pain_count.should eq 0
|
||||
first_pain = Pain.create
|
||||
frist_hemorrhoid = Hemorrhoid.create(:user => @user, :pain => first_pain)
|
||||
@user.pains.should eq [first_pain]
|
||||
@user.pain_count(:force_reload => true).should eq 1
|
||||
|
||||
second_pain = Pain.create
|
||||
second_hemorrhoid = Hemorrhoid.create(:user => @user, :pain => second_pain)
|
||||
@user.pain_count(:force_reload => true).should eq 2
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
context 'when destroying the parent objects' do
|
||||
it "delete relations when dependent is destroy" do
|
||||
Category.instance_eval do
|
||||
has_many :tags, :dependent => :destroy
|
||||
end
|
||||
|
||||
category = Category.create(:name => "food")
|
||||
tag = Tag.create(:name => "food", :category => category)
|
||||
|
||||
Tag.find(:all).should eq [tag]
|
||||
category.destroy
|
||||
Tag.find(:all).should eq []
|
||||
end
|
||||
|
||||
it "nullify relations when dependent is nullify" do
|
||||
|
||||
user = User.create(:title => "Mr.")
|
||||
post = Post.create(:user => user)
|
||||
|
||||
user.destroy
|
||||
post = Post.find(post.id)
|
||||
post.user_id.should be nil
|
||||
end
|
||||
|
||||
it "nullify the foreign key even if validation forbids" do
|
||||
user = User.create(:title => "Mr.")
|
||||
post = StrictPost.create(:user => user)
|
||||
|
||||
user.destroy
|
||||
post = StrictPost.find(post.id)
|
||||
post.user_id.should be nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "with has_many :trough" do
|
||||
before do
|
||||
@journal_1 = Journal.create
|
||||
@journal_2 = Journal.create
|
||||
@reader_1 = Reader.create
|
||||
@reader_2 = Reader.create
|
||||
end
|
||||
|
||||
it "raise an exception if there is no :through relation" do
|
||||
|
||||
expect {
|
||||
class FooHasManyThroughBar
|
||||
include SimplyBased::Model
|
||||
has_many :foos, :through => :bars
|
||||
end
|
||||
}.to raise_error ArgumentError
|
||||
end
|
||||
|
||||
it "define a getter" do
|
||||
@journal_1.should respond_to :readers
|
||||
@reader_1.should respond_to :journals
|
||||
end
|
||||
|
||||
it "load the objects through" do
|
||||
membership = Membership.new
|
||||
membership.journal = @journal_1
|
||||
membership.reader = @reader_1
|
||||
membership.save.should be true
|
||||
|
||||
membership.journal.should eq @journal_1
|
||||
membership.reader.should eq @reader_1
|
||||
@journal_1.reload.memberships.should eq [membership]
|
||||
@journal_1.reload.memberships.should eq [membership]
|
||||
|
||||
@journal_1.readers.should eq [@reader_1]
|
||||
@reader_1.journals.should eq [@journal_1]
|
||||
|
||||
membership_2 = Membership.new
|
||||
membership_2.journal = @journal_1
|
||||
membership_2.reader = @reader_2
|
||||
membership_2.save.should be true
|
||||
|
||||
@journal_1.reload.readers.map(&:id).sort.should eq [@reader_1.id, @reader_2.id].sort
|
||||
@reader_1.reload.journals.map(&:id).sort.should eq [@journal_1.id]
|
||||
@reader_2.reload.journals.map(&:id).sort.should eq [@journal_1.id]
|
||||
|
||||
membership_3 = Membership.new
|
||||
membership_3.journal = @journal_2
|
||||
membership_3.reader = @reader_2
|
||||
membership_3.save.should be true
|
||||
|
||||
@journal_1.reload.readers.map(&:id).sort.should eq [@reader_1.id, @reader_2.id].sort
|
||||
@journal_2.reload.readers.map(&:id).sort.should eq [@reader_2.id].sort
|
||||
@reader_1.reload.journals.map(&:id).sort.should eq [@journal_1.id]
|
||||
@reader_2.reload.journals.map(&:id).sort.should eq [@journal_1.id, @journal_2.id].sort
|
||||
|
||||
membership_3.destroy
|
||||
|
||||
@journal_1.reload.readers.map(&:id).sort.should eq [@reader_1.id, @reader_2.id].sort
|
||||
@journal_2.reload.readers.should eq []
|
||||
@reader_1.reload.journals.map(&:id).sort.should eq [@journal_1.id]
|
||||
@reader_2.reload.journals.map(&:id).sort.should eq [@journal_1.id]
|
||||
end
|
||||
|
||||
it "verify the given options" do
|
||||
expect{ @journal_1.readers(:foo => true) }.to raise_error ArgumentError
|
||||
end
|
||||
|
||||
it "not try to destroy/nullify through-objects on parent object delete" do
|
||||
membership = Membership.new
|
||||
membership.journal = @journal_1
|
||||
membership.reader = @reader_1
|
||||
membership.save.should be true
|
||||
|
||||
@reader_1.reload
|
||||
@journal_1.reload
|
||||
|
||||
expect_any_instance_of( Journal ).not_to receive(:readers)
|
||||
expect_any_instance_of( Reader ).not_to receive("journal=")
|
||||
|
||||
@journal_1.delete
|
||||
end
|
||||
end
|
||||
|
||||
it "caches the parent relation" do
|
||||
existing_user = User.create title: "User"
|
||||
Post.create user: existing_user
|
||||
Post.create user: existing_user
|
||||
user = User.find(existing_user.id)
|
||||
expect{ user.posts.each.map(&:user) }.not_to exceed_query_limit 1
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,139 @@
|
||||
require 'spec_helper'
|
||||
|
||||
describe 'has_one' do
|
||||
it 'adds a getter method' do
|
||||
expect(Instance.new).to respond_to(:identity)
|
||||
end
|
||||
|
||||
it 'raises an error if another property with the same name already exists' do
|
||||
expect {
|
||||
class ::DoubleHasOneUser
|
||||
include SimplyBased::Model
|
||||
property :user
|
||||
has_one :user
|
||||
end
|
||||
}.to raise_error RuntimeError
|
||||
end
|
||||
|
||||
it 'fetches the object when invoking the getter' do
|
||||
instance = Instance.create
|
||||
identity = Identity.create(instance: instance)
|
||||
expect(instance.identity).to eq identity
|
||||
end
|
||||
|
||||
it 'sets the parent object on the clients cache' do
|
||||
expect(Instance).not_to receive(:find)
|
||||
instance = Instance.create
|
||||
identity = Identity.create(instance: instance)
|
||||
expect(instance.identity.instance).to eq instance
|
||||
end
|
||||
|
||||
it 'uses the correct view when handling inheritance' do
|
||||
problem = Problem.create
|
||||
big_problem = BigProblem.create
|
||||
issue = Issue.create(name: 'Thing', problem: problem)
|
||||
expect(problem.issue).to eq issue
|
||||
issue.update_attributes(problem_id: nil, big_problem_id: big_problem.id)
|
||||
expect(big_problem.issue).to eq issue
|
||||
end
|
||||
|
||||
it 'verifies the given options for the accessor method' do
|
||||
instance = Instance.create
|
||||
expect { instance.identity(foo: :var) }.to raise_error ArgumentError
|
||||
end
|
||||
|
||||
it 'verifies the given options for the association definition' do
|
||||
expect {
|
||||
User.instance_eval do
|
||||
has_one :foo, bar: :do
|
||||
end
|
||||
}.to raise_error ArgumentError
|
||||
end
|
||||
|
||||
it 'stores the fetched object into the cache' do
|
||||
instance = Instance.create
|
||||
identity = Identity.create(instance: instance)
|
||||
instance.identity
|
||||
expect(instance.instance_variable_get('@identity')).to eq identity
|
||||
end
|
||||
|
||||
it 'does not fetch from the database when object is in cache' do
|
||||
instance = Instance.create
|
||||
identity = Identity.create(instance: instance)
|
||||
instance.identity
|
||||
expect(SimplyBased.database).not_to receive(:view)
|
||||
instance.identity
|
||||
end
|
||||
|
||||
it 'updates the foreign object to have the owners id in the foreign key' do
|
||||
instance = Instance.create
|
||||
identity = Identity.create
|
||||
instance.identity = identity
|
||||
identity.reload
|
||||
expect(identity.instance_id).to eq instance.id
|
||||
end
|
||||
|
||||
it 'updates the cache when setting' do
|
||||
instance = Instance.create
|
||||
identity = Identity.create
|
||||
instance.identity = identity
|
||||
expect(SimplyBased).not_to receive(:database)
|
||||
expect(instance.identity).to eq identity
|
||||
end
|
||||
|
||||
it 'sets the foreign key value to nil when assigning nil' do
|
||||
instance = Instance.create
|
||||
identity = Identity.create(instance: instance)
|
||||
instance.identity = nil
|
||||
identity = Identity.find(identity.id)
|
||||
expect(identity.instance_id).to be_nil
|
||||
end
|
||||
|
||||
it 'checks the class' do
|
||||
instance = Instance.create
|
||||
expect { instance.identity = 'foo' }.to raise_error ArgumentError, /expected Identity got String/
|
||||
end
|
||||
|
||||
it 'deletes the dependent objects when dependent is set to destroy' do
|
||||
identity = Identity.create
|
||||
mag = Magazine.create
|
||||
mag.identity = identity
|
||||
mag.identity = nil
|
||||
expect(Identity.find_by_id(identity.id)).to be_nil
|
||||
end
|
||||
|
||||
it 'unsets the id on the foreign object when a new object is set' do
|
||||
instance = Instance.create
|
||||
identity = Identity.create(instance: instance)
|
||||
identity2 = Identity.create
|
||||
instance.identity = identity2
|
||||
identity = Identity.find(identity.id)
|
||||
expect(identity.instance_id).to be_nil
|
||||
end
|
||||
|
||||
it 'deletes the foreign object when a new object is set and dependent is set to destroy' do
|
||||
identity = Identity.create
|
||||
identity2 = Identity.create
|
||||
mag = Magazine.create
|
||||
mag.identity = identity
|
||||
mag.identity = identity2
|
||||
expect(Identity.find_by_id(identity.id)).to be_nil
|
||||
end
|
||||
|
||||
it 'deletes the foreign object when parent is destroyed and dependent is set to destroy' do
|
||||
identity = Identity.create
|
||||
mag = Magazine.create
|
||||
mag.identity = identity
|
||||
mag.destroy
|
||||
expect(Identity.find_by_id(identity.id)).to be_nil
|
||||
end
|
||||
|
||||
it 'nullifies the foreign objects foreign key when parent is destroyed' do
|
||||
identity = Identity.create
|
||||
instance = Instance.create
|
||||
instance.identity = identity
|
||||
instance.destroy
|
||||
identity = Identity.find(identity.id)
|
||||
expect(identity.instance_id).to be_nil
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,43 @@
|
||||
require 'spec_helper'
|
||||
describe "Include relation, aggregate many queries to one simplified one for performance" do
|
||||
context "initialized posts" do
|
||||
before do
|
||||
@user = User.create :title => 'Include relation posts user'
|
||||
@post1 = Post.create(:user => @user)
|
||||
@post2 = Post.create(:user => @user)
|
||||
@user.reload
|
||||
@post1 = Post.find(@post1.id)
|
||||
@post2 = Post.find(@post2.id)
|
||||
end
|
||||
|
||||
it "have proper relations without include_relation" do
|
||||
@user.posts.should match_array [@post1, @post2]
|
||||
end
|
||||
|
||||
it "have proper relations with include_relation" do
|
||||
[@user].include_relation(:posts)
|
||||
#assert_equal [@post1, @post2].sort_by(&:id), @user.posts.sort_by(&:id)
|
||||
@user.posts.should match_array [@post1, @post2]
|
||||
end
|
||||
|
||||
it "have proper relations with include_relation called twice" do
|
||||
[@user].include_relation(:posts)
|
||||
[@user].include_relation(:posts)
|
||||
@user.posts.should match_array [@post1, @post2]
|
||||
end
|
||||
end
|
||||
|
||||
context '...' do
|
||||
it "includes has_many relations and sets the reverse relation on has many" do
|
||||
user1 = User.create title: "User1"
|
||||
user2 = User.create title: "User2"
|
||||
Post.create user: user1
|
||||
Post.create user: user1
|
||||
Post.create user: user2
|
||||
Post.create user: user2
|
||||
users = nil
|
||||
expect{ users = User.all.include_relation(:posts) }.not_to exceed_query_limit 2 # users.all and adding posts to both users
|
||||
expect{ users.map(&:posts).flatten.map(&:user) }.not_to perform_any_queries # reverse relation user must be set on each post
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,226 @@
|
||||
require 'spec_helper'
|
||||
|
||||
describe 'Instance lifecycle' do
|
||||
context 'design documents' do
|
||||
let(:db_url) { SimplyBased.database_url }
|
||||
|
||||
it 'deletes all' do
|
||||
expect(SimplyBased::Model.delete_all_design_documents(db_url)).to eq 0
|
||||
user = User.create
|
||||
Post.create(user: user)
|
||||
user.posts
|
||||
expect(SimplyBased::Model.delete_all_design_documents(db_url)).to eq 1
|
||||
end
|
||||
|
||||
it 'compacts all' do
|
||||
expect(SimplyBased::Model.compact_all_design_documents(db_url)).to eq 0
|
||||
user = User.create
|
||||
Post.create(user: user)
|
||||
user.posts
|
||||
expect(SimplyBased::Model.compact_all_design_documents(db_url)).to eq 1
|
||||
end
|
||||
end
|
||||
|
||||
context 'when creating instances' do
|
||||
it 'populates the attributes' do
|
||||
user = User.create(title: 'Mr.', name: 'Host Master')
|
||||
expect(user.title).to eq 'Mr.'
|
||||
expect(user.name).to eq 'Host Master'
|
||||
end
|
||||
|
||||
it 'saves the instance' do
|
||||
user = User.create(title: 'Mr.')
|
||||
expect(user).not_to be_new_record
|
||||
end
|
||||
|
||||
context 'with a bang' do
|
||||
it 'does not raise an exception when saving succeeded' do
|
||||
expect { User.create!(title: 'Mr.') }.not_to raise_error
|
||||
end
|
||||
|
||||
it 'saves the user' do
|
||||
user = User.create!(title: 'Mr.')
|
||||
expect(user).not_to be_new_record
|
||||
end
|
||||
|
||||
it 'raises an error when the validations failed' do
|
||||
expect { User.create!(title: nil) }.to raise_error(RuntimeError, /Validations failed/)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a block' do
|
||||
it 'calls the block with the record' do
|
||||
user = User.create do |u|
|
||||
u.title = 'Mr.'
|
||||
end
|
||||
expect(user.title).to eq 'Mr.'
|
||||
end
|
||||
|
||||
it 'saves the record' do
|
||||
user = User.create do |u|
|
||||
u.title = 'Mr.'
|
||||
end
|
||||
expect(user).not_to be_new_record
|
||||
end
|
||||
|
||||
it 'assigns attributes via the hash' do
|
||||
user = User.create(title: 'Mr.') do |u|
|
||||
u.name = 'Host Master'
|
||||
end
|
||||
expect(user.title).to eq 'Mr.'
|
||||
expect(user.name).to eq 'Host Master'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when saving an instance' do
|
||||
it 'saves the instance' do
|
||||
user = User.new(title: 'Mr.')
|
||||
expect(user).to be_new_record
|
||||
user.save
|
||||
expect(user).not_to be_new_record
|
||||
end
|
||||
|
||||
context 'when using save!' do
|
||||
it 'raises an exception when a validation is not fulfilled' do
|
||||
user = User.new
|
||||
expect { user.save! }.to raise_error(RuntimeError, /Validations failed/)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when using save(false)' do
|
||||
it 'does not run the validations' do
|
||||
user = User.new
|
||||
user.save(false)
|
||||
expect(user).not_to be_new
|
||||
expect(user).not_to be_dirty
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when destroying an instance' do
|
||||
it 'removes the instance' do
|
||||
user = User.create(title: 'Mr')
|
||||
expect { user.destroy }.to change { User.find(:all).size }.by(-1)
|
||||
end
|
||||
|
||||
it 'returns the frozen instance' do
|
||||
user = User.create(title: 'Mr')
|
||||
expect(user.destroy).to eq user
|
||||
end
|
||||
end
|
||||
|
||||
context 'when updating attributes' do
|
||||
it 'merges in the updated attributes' do
|
||||
user = User.create(title: 'Mr.')
|
||||
user.update_attributes(title: 'Mrs.')
|
||||
expect(user.title).to eq 'Mrs.'
|
||||
end
|
||||
|
||||
it 'saves the instance' do
|
||||
user = User.create(title: 'Mr.')
|
||||
user.update_attributes(title: 'Mrs.')
|
||||
expect(user).not_to be_dirty
|
||||
end
|
||||
end
|
||||
|
||||
context 'when counting' do
|
||||
context 'when counting all' do
|
||||
it 'returns the number of objects in the database' do
|
||||
CountMe.create(title: 'Mr.')
|
||||
CountMe.create(title: 'Mrs.')
|
||||
expect(CountMe.find(:all).size).to eq 2
|
||||
expect(CountMe.count).to eq 2
|
||||
end
|
||||
|
||||
it 'only counts the correct class' do
|
||||
CountMe.create(title: 'Mr.')
|
||||
DontCountMe.create(title: 'Foo')
|
||||
expect(CountMe.find(:all).size).to eq 1
|
||||
expect(CountMe.count).to eq 1
|
||||
end
|
||||
end
|
||||
|
||||
context 'when counting by prefix' do
|
||||
it 'returns the number of matching objects' do
|
||||
CountMe.create(title: 'Mr.')
|
||||
CountMe.create(title: 'Mrs.')
|
||||
expect(CountMe.find_all_by_title('Mr.').size).to eq 1
|
||||
expect(CountMe.count_by_title('Mr.')).to eq 1
|
||||
end
|
||||
|
||||
it 'only counts the correct class' do
|
||||
CountMe.create(title: 'Mr.')
|
||||
DontCountMe.create(title: 'Mr.')
|
||||
expect(CountMe.find_all_by_title('Mr.').size).to eq 1
|
||||
expect(CountMe.count_by_title('Mr.')).to eq 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when reloading an instance' do
|
||||
it 'reloads new attributes from the database' do
|
||||
user = User.create(title: 'Mr.', name: 'Host Master')
|
||||
user2 = User.find(user.id)
|
||||
user2.update_attributes(title: 'Mrs.', name: 'Hostess Masteress')
|
||||
user.reload
|
||||
expect(user.title).to eq 'Mrs.'
|
||||
expect(user.name).to eq 'Hostess Masteress'
|
||||
end
|
||||
|
||||
it 'removes attributes that are no longer in the database' do
|
||||
user = User.create(title: 'Mr.', name: 'Host Master')
|
||||
expect(user.name).not_to be_nil
|
||||
same_user_in_different_thread = User.find(user.id)
|
||||
same_user_in_different_thread.name = nil
|
||||
same_user_in_different_thread.save!
|
||||
expect(user.reload.name).to be_nil
|
||||
end
|
||||
|
||||
it 'also removes foreign key attributes that are no longer in the database' do
|
||||
user = User.create(title: 'Mr.', name: 'Host Master')
|
||||
post = Post.create(user: user)
|
||||
expect(post.user_id).not_to be_nil
|
||||
same_post_in_different_thread = Post.find(post.id)
|
||||
same_post_in_different_thread.user = nil
|
||||
same_post_in_different_thread.save!
|
||||
expect(post.reload.user_id).to be_nil
|
||||
end
|
||||
|
||||
it 'is not dirty after reloading' do
|
||||
user = User.create(title: 'Mr.', name: 'Host Master')
|
||||
user2 = User.find(user.id)
|
||||
user2.update_attributes(title: 'Mrs.', name: 'Hostess Masteress')
|
||||
user.reload
|
||||
expect(user).not_to be_dirty
|
||||
end
|
||||
|
||||
it 'ensures that association caches for has_many are cleared' do
|
||||
user = User.create(title: 'Mr.', name: 'Host Master')
|
||||
post = Post.create(user: user)
|
||||
expect(user.posts.size).to eq 1
|
||||
expect(user.instance_variable_get('@posts')).not_to be_nil
|
||||
user.reload
|
||||
expect(user.instance_variable_get('@posts')).to be_nil
|
||||
expect(user.posts.first).not_to be_nil
|
||||
end
|
||||
|
||||
it 'ensures that association caches for belongs_to are cleared' do
|
||||
user = User.create(title: 'Mr.', name: 'Host Master')
|
||||
post = Post.create(user: user)
|
||||
post.user
|
||||
expect(post.instance_variable_get('@user')).not_to be_nil
|
||||
post.reload
|
||||
expect(post.instance_variable_get('@user')).to be_nil
|
||||
expect(post.user).not_to be_nil
|
||||
end
|
||||
|
||||
it 'updates the revision' do
|
||||
user = User.create(title: 'Mr.', name: 'Host Master')
|
||||
user2 = User.find(user.id)
|
||||
user2.update_attributes(title: 'Mrs.', name: 'Hostess Masteress')
|
||||
user.reload
|
||||
expect(user._rev).to eq user2._rev
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,15 @@
|
||||
require 'spec_helper'
|
||||
describe "Instance methods" do
|
||||
describe '#serializable_hash' do
|
||||
it 'returns a proper hash'do
|
||||
User.new(title: 'Include relation posts user').serializable_hash.should eq(
|
||||
'id' => nil,
|
||||
"created_at" => nil,
|
||||
"updated_at" => nil,
|
||||
"name" => nil,
|
||||
"title" => "Include relation posts user",
|
||||
"homepage" => nil
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,37 @@
|
||||
require 'spec_helper'
|
||||
|
||||
describe 'has_local_attached' do
|
||||
before do
|
||||
class DefaultStyleTest
|
||||
include SimplyBased::Model
|
||||
property :title
|
||||
has_local_attached :file, default_style: :not_original
|
||||
end
|
||||
end
|
||||
|
||||
it 'uses default_style when no style given for url' do
|
||||
obj = DefaultStyleTest.new
|
||||
obj.file_file_name = 'test.jpg'
|
||||
|
||||
expect(obj.file.url).to eq '/system/file/tmp/not_original.jpg'
|
||||
end
|
||||
|
||||
it 'uses default_style when no style given for path' do
|
||||
obj = DefaultStyleTest.new
|
||||
obj.file_file_name = 'test.jpg'
|
||||
|
||||
expect(obj.file.path).to end_with '/system/file/tmp/not_original.jpg'
|
||||
end
|
||||
|
||||
it 'allows explicit style override' do
|
||||
obj = DefaultStyleTest.new
|
||||
obj.file_file_name = 'test.jpg'
|
||||
|
||||
expect(obj.file.url(:thumb)).to eq '/system/file/tmp/thumb.jpg'
|
||||
end
|
||||
|
||||
it 'uses default_style from attachment_registry' do
|
||||
config = DefaultStyleTest.attachment_registry[:file]
|
||||
expect(config[:default_style]).to eq :not_original
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,68 @@
|
||||
require 'spec_helper'
|
||||
|
||||
describe 'Mass assignment protection' do
|
||||
context 'when using attr_protected' do
|
||||
before do
|
||||
Category.instance_eval do
|
||||
@_accessible_attributes = []
|
||||
attr_protected :parent, :alias
|
||||
end
|
||||
end
|
||||
|
||||
it 'does not allow to set with mass assignment using attributes=' do
|
||||
item = Category.new
|
||||
item.attributes = { parent: 'a', name: 'c' }
|
||||
expect(item.name).to eq 'c'
|
||||
expect(item.parent).to be_nil
|
||||
end
|
||||
|
||||
it 'does not allow to set with mass assignment using attributes= - ignore string vs. symbol' do
|
||||
item = Category.new
|
||||
item.attributes = { 'parent' => 'a', 'name' => 'c' }
|
||||
expect(item.name).to eq 'c'
|
||||
expect(item.parent).to be_nil
|
||||
end
|
||||
|
||||
it 'does not allow to set with mass assignment using the constructor' do
|
||||
item = Category.new(parent: 'a', name: 'c')
|
||||
expect(item.name).to eq 'c'
|
||||
expect(item.parent).to be_nil
|
||||
end
|
||||
|
||||
it 'does not allow to set with mass assignment using update_attributes' do
|
||||
item = Category.new
|
||||
item.update_attributes(parent: 'a', name: 'c')
|
||||
expect(item.name).to eq 'c'
|
||||
expect(item.parent).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'attr_accessible' do
|
||||
before do
|
||||
Category.instance_eval do
|
||||
@_protected_attributes = []
|
||||
attr_accessible :name
|
||||
end
|
||||
end
|
||||
|
||||
it 'does not allow to set with mass assignment using attributes=' do
|
||||
item = Category.new
|
||||
item.attributes = { parent: 'a', name: 'c' }
|
||||
expect(item.name).to eq 'c'
|
||||
expect(item.parent).to be_nil
|
||||
end
|
||||
|
||||
it 'does not allow to set with mass assignment using the constructor' do
|
||||
item = Category.new(parent: 'a', name: 'c')
|
||||
expect(item.name).to eq 'c'
|
||||
expect(item.parent).to be_nil
|
||||
end
|
||||
|
||||
it 'does not allow to set with mass assignment using update_attributes' do
|
||||
item = Category.new
|
||||
item.update_attributes(parent: 'a', name: 'c')
|
||||
expect(item.name).to eq 'c'
|
||||
expect(item.parent).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,78 @@
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'Pagination' do
|
||||
it "does not raise error when page and per_page are specified" do
|
||||
expect{ User.all(page: 1, per_page: 2) }.not_to raise_error
|
||||
end
|
||||
|
||||
it "respond to pagination methods" do
|
||||
paginated = User.all(page: 1, per_page: 2 )
|
||||
paginated.current_page.should eq 1
|
||||
paginated.num_pages.should eq 1
|
||||
paginated.per_page.should eq 2
|
||||
end
|
||||
|
||||
it "respond to modified pagination methods" do
|
||||
paginated = Comment.all(page: 1, per_page: 2 )
|
||||
paginated.current_page_modified.should eq 1
|
||||
paginated.num_pages_modified.should eq 1
|
||||
paginated.per_page_modified.should eq 2
|
||||
end
|
||||
|
||||
it "display individual objects on pages with per_page is one" do
|
||||
u1 = User.create(title: 'user1', created_at: Time.now )
|
||||
u2 = User.create(title: 'user2', created_at: Time.now + 5.minutes )
|
||||
u3 = User.create(title: 'user3', created_at: Time.now + 10.minutes )
|
||||
User.all.should eq [u1, u2, u3] # normal behaviour
|
||||
User.all(per_page: 1).first.should eq u1 # default to page 1
|
||||
User.all(page: 2, per_page: 1).first.should eq u2
|
||||
User.all(page: 3, per_page: 1).first.should eq u3
|
||||
end
|
||||
|
||||
it "paginates all finder" do
|
||||
15.times{|i| User.create(title: "user#{i}", created_at: i.minutes.from_now) }
|
||||
User.count.should eq 15
|
||||
result = User.all(page: 2, per_page: 2)
|
||||
result.current_page.should eq 2
|
||||
result.per_page.should eq 2
|
||||
result.map(&:title).sort.should eq ['user2', 'user3']
|
||||
result.total_entries.should eq 15
|
||||
result.total_count.should eq 15
|
||||
result.num_pages.should eq 8
|
||||
end
|
||||
|
||||
it "paginate find_all_by finders" do
|
||||
6.times{|i| User.create(title: "user#{i}", homepage: 'http://localhost/1') }
|
||||
9.times{|i| User.create(title: "user#{i + 6}", homepage: 'http://localhost/2') }
|
||||
User.count.should eq 15
|
||||
User.find_all_by_homepage('http://localhost/1').size.should eq 6
|
||||
result = User.find_all_by_homepage('http://localhost/1', page: 2, per_page: 2)
|
||||
result.current_page.should eq 2
|
||||
result.per_page.should eq 2
|
||||
result.map(&:title).sort.should eq ['user2', 'user3']
|
||||
result.total_entries.should eq 6
|
||||
result.total_count.should eq 6
|
||||
result.num_pages.should eq 3
|
||||
end
|
||||
|
||||
it "paginate has and belongs to many view" do
|
||||
server = Server.create
|
||||
for klass in 'A'..'Z'
|
||||
server.add_network Network.create( klass: klass)
|
||||
end
|
||||
res = Network.with_pagination_options(startkey: [server.id], endkey: ["#{server.id}\u9999"], page: 1, per_page: 100, reduce: false, include_docs: true) do |o|
|
||||
Network.database.view(Network.association_network_has_and_belongs_to_many_servers(o))
|
||||
end
|
||||
res.size.should eq 26
|
||||
end
|
||||
it "paginate has and belongs to many view with non trivial options" do
|
||||
server = Server.create
|
||||
for klass in 'A'..'Z'
|
||||
server.add_network Network.create( klass: klass)
|
||||
end
|
||||
res = Network.with_pagination_options(startkey: [server.id], endkey: ["#{server.id}\u9999"], page: 2, per_page: 3, reduce: false, include_docs: true) do |o|
|
||||
Network.database.view(Network.association_network_has_and_belongs_to_many_servers(o))
|
||||
end
|
||||
res.size.should eq 3
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,56 @@
|
||||
require 'spec_helper'
|
||||
|
||||
# Couchbase-backed replacements for common Redis uses: atomic counters, a Rails
|
||||
# cache store, and a distributed lock — all on the same cluster as the models.
|
||||
RSpec.describe 'Redis replacements' do
|
||||
describe SimplyBased::Counter do
|
||||
subject(:counter) { described_class.new }
|
||||
|
||||
it 'atomically increments, seeding at the initial value' do
|
||||
expect(counter.increment('c:hits')).to eq(0) # created at initial 0
|
||||
expect(counter.increment('c:hits')).to eq(1)
|
||||
expect(counter.increment('c:hits', 5)).to eq(6)
|
||||
expect(counter.get('c:hits')).to eq(6)
|
||||
end
|
||||
|
||||
it 'decrements and resets' do
|
||||
counter.increment('c:n', 0, initial: 10)
|
||||
expect(counter.decrement('c:n', 3)).to eq(7)
|
||||
expect(counter.reset('c:n', 0)).to eq(0)
|
||||
expect(counter.get('c:n')).to eq(0)
|
||||
end
|
||||
end
|
||||
|
||||
describe SimplyBased::Cache do
|
||||
subject(:cache) { described_class.new(namespace: 'test') }
|
||||
|
||||
it 'works as an ActiveSupport::Cache::Store (read/write/fetch/delete)' do
|
||||
cache.write('k', { 'a' => 1 })
|
||||
expect(cache.read('k')).to eq({ 'a' => 1 })
|
||||
expect(cache.fetch('k2') { [1, 2, 3] }).to eq([1, 2, 3])
|
||||
expect(cache.read('k2')).to eq([1, 2, 3])
|
||||
cache.delete('k')
|
||||
expect(cache.read('k')).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe SimplyBased::Lock do
|
||||
subject(:lock) { described_class.new }
|
||||
|
||||
it 'grants the lock to one holder and blocks a second acquire' do
|
||||
handle = lock.acquire('job:1', ttl: 30)
|
||||
expect(handle).not_to be_nil
|
||||
expect(lock.acquire('job:1', ttl: 30)).to be_nil # already held
|
||||
lock.release('job:1', handle)
|
||||
expect(lock.acquire('job:1', ttl: 30)).not_to be_nil # released → free
|
||||
end
|
||||
|
||||
it 'runs a critical section with with_lock' do
|
||||
ran = false
|
||||
lock.with_lock('job:2') { ran = true }
|
||||
expect(ran).to be(true)
|
||||
# lock auto-released after the block → re-acquirable
|
||||
expect(lock.acquire('job:2')).not_to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,31 @@
|
||||
require 'spec_helper'
|
||||
|
||||
describe 'Retry on error' do
|
||||
it 'retries the save on connection error' do
|
||||
user = User.create(name: 'Mickey Mouse', title: 'Dr.', homepage: 'www.gmx.de')
|
||||
error = Errno::ECONNREFUSED.new('Connection refused - connect(2)')
|
||||
expect(SimplyBased.database).to receive(:save_document).once.and_raise(error).ordered
|
||||
expect(SimplyBased.database).to receive(:save_document).once.and_return(true).ordered
|
||||
|
||||
user.name = 'bert'
|
||||
expect(user.save).to be true
|
||||
end
|
||||
|
||||
it 'retries the save! on connection error' do
|
||||
error = Errno::ECONNREFUSED.new('Connection refused - connect(2)')
|
||||
expect(SimplyBased.database).to receive(:save_document).once.and_raise(error).ordered
|
||||
expect(SimplyBased.database).to receive(:save_document).once.and_return(true).ordered
|
||||
|
||||
user = User.create(name: 'Mickey Mouse', title: 'Dr.', homepage: 'www.gmx.de')
|
||||
expect(user).to be_truthy
|
||||
end
|
||||
|
||||
it 're-raises the error if retried several times' do
|
||||
error = Errno::ECONNREFUSED.new('Connection refused - connect(2)')
|
||||
expect(SimplyBased.database).to receive(:save_document).exactly(3).times.and_raise(error)
|
||||
|
||||
expect {
|
||||
User.create(name: 'Mickey Mouse', title: 'Dr.', homepage: 'www.gmx.de')
|
||||
}.to raise_error(Errno::ECONNREFUSED)
|
||||
end
|
||||
end
|
||||
+262
@@ -0,0 +1,262 @@
|
||||
require 'spec_helper'
|
||||
|
||||
class CouchLogItem
|
||||
include SimplyBased::Model
|
||||
has_s3_attached :log_data, bucket: 'bucket-for-monsieur', access_key: 'abcdef', secret_access_key: 'secret!'
|
||||
end
|
||||
|
||||
describe 'S3 attachment' do
|
||||
let(:client) { instance_double(Aws::S3::Client) }
|
||||
|
||||
before do
|
||||
CouchLogItem._s3_options[:log_data][:bucket] = 'bucket-for-monsieur'
|
||||
CouchLogItem._s3_options[:log_data][:location] = :us
|
||||
CouchLogItem._s3_options[:log_data][:permissions] = 'private'
|
||||
CouchLogItem._s3_options[:log_data][:after_delete] = :nothing
|
||||
CouchLogItem._s3_options[:log_data][:logger] = nil
|
||||
|
||||
allow(Aws::S3::Client).to receive(:new).and_return(client)
|
||||
allow(client).to receive(:put_object)
|
||||
allow(client).to receive(:delete_object)
|
||||
end
|
||||
|
||||
let(:log_item) do
|
||||
item = CouchLogItem.new
|
||||
item.instance_variable_set(:@_s3_client, nil)
|
||||
item.instance_variable_set(:@_s3_bucket, nil)
|
||||
item
|
||||
end
|
||||
|
||||
context 'when saving the attachment' do
|
||||
it 'creates an S3 client' do
|
||||
log_item.log_data = 'Yay! It logged!'
|
||||
expect(Aws::S3::Client).to receive(:new).with(
|
||||
access_key_id: 'abcdef',
|
||||
secret_access_key: 'secret!',
|
||||
region: 'us-east-1',
|
||||
logger: nil
|
||||
).and_return(client)
|
||||
allow(client).to receive(:head_bucket)
|
||||
log_item.save
|
||||
end
|
||||
|
||||
it 'uploads the file' do
|
||||
log_item.log_data = 'Yay! It logged!'
|
||||
allow(client).to receive(:head_bucket)
|
||||
expect(client).to receive(:put_object).with(
|
||||
bucket: 'bucket-for-monsieur',
|
||||
key: anything,
|
||||
body: 'Yay! It logged!',
|
||||
acl: 'private'
|
||||
)
|
||||
log_item.save
|
||||
end
|
||||
|
||||
it 'also uploads on save!' do
|
||||
log_item.log_data = 'Yay! It logged!'
|
||||
allow(client).to receive(:head_bucket)
|
||||
expect(client).to receive(:put_object)
|
||||
log_item.save!
|
||||
end
|
||||
|
||||
it 'uses the specified bucket' do
|
||||
CouchLogItem._s3_options[:log_data][:bucket] = 'mybucket'
|
||||
log_item.log_data = 'Yay! It logged!'
|
||||
expect(client).to receive(:head_bucket).with(bucket: 'mybucket')
|
||||
expect(client).to receive(:put_object).with(hash_including(bucket: 'mybucket'))
|
||||
log_item.save
|
||||
end
|
||||
|
||||
it 'creates the bucket if it does not exist' do
|
||||
CouchLogItem._s3_options[:log_data][:bucket] = 'mybucket'
|
||||
log_item.log_data = 'Yay! log me'
|
||||
expect(client).to receive(:head_bucket).with(bucket: 'mybucket')
|
||||
.and_raise(Aws::S3::Errors::NoSuchBucket.new(nil, 'NoSuchBucket'))
|
||||
expect(client).to receive(:create_bucket).with(
|
||||
bucket: 'mybucket',
|
||||
create_bucket_configuration: nil
|
||||
)
|
||||
allow(client).to receive(:put_object)
|
||||
log_item.save
|
||||
end
|
||||
|
||||
it 'creates an EU bucket with location constraint' do
|
||||
CouchLogItem._s3_options[:log_data][:bucket] = 'mybucket'
|
||||
CouchLogItem._s3_options[:log_data][:location] = :eu
|
||||
log_item.log_data = 'Yay! log me'
|
||||
expect(client).to receive(:head_bucket).with(bucket: 'mybucket')
|
||||
.and_raise(Aws::S3::Errors::NoSuchBucket.new(nil, 'NoSuchBucket'))
|
||||
expect(client).to receive(:create_bucket).with(
|
||||
bucket: 'mybucket',
|
||||
create_bucket_configuration: { location_constraint: 'eu-west-1' }
|
||||
)
|
||||
allow(client).to receive(:put_object)
|
||||
log_item.save
|
||||
end
|
||||
|
||||
it 'raises an error if the bucket is not ours' do
|
||||
CouchLogItem._s3_options[:log_data][:bucket] = 'mybucket'
|
||||
CouchLogItem._s3_options[:log_data][:location] = :eu
|
||||
log_item.log_data = 'Yay! log me too'
|
||||
expect(client).to receive(:head_bucket).with(bucket: 'mybucket')
|
||||
.and_raise(Aws::S3::Errors::NoSuchBucket.new(nil, 'NoSuchBucket'))
|
||||
expect(client).to receive(:create_bucket).with(
|
||||
bucket: 'mybucket',
|
||||
create_bucket_configuration: { location_constraint: 'eu-west-1' }
|
||||
).and_raise(Aws::S3::Errors::BucketAlreadyExists.new(nil, 'BucketAlreadyExists'))
|
||||
|
||||
expect { log_item.save }.to raise_error(ArgumentError)
|
||||
end
|
||||
|
||||
it 'passes the logger to the S3 client' do
|
||||
log_item.log_data = 'Yay! log me'
|
||||
CouchLogItem._s3_options[:log_data][:bucket] = 'mybucket'
|
||||
CouchLogItem._s3_options[:log_data][:logger] = Logger.new(nil)
|
||||
|
||||
expect(Aws::S3::Client).to receive(:new).with(
|
||||
access_key_id: 'abcdef',
|
||||
secret_access_key: 'secret!',
|
||||
region: 'us-east-1',
|
||||
logger: kind_of(Logger)
|
||||
).and_return(client)
|
||||
allow(client).to receive(:head_bucket)
|
||||
log_item.save
|
||||
CouchLogItem._s3_options[:log_data][:logger] = nil
|
||||
end
|
||||
|
||||
it 'does not upload when not changed' do
|
||||
allow(client).to receive(:head_bucket)
|
||||
expect(client).not_to receive(:put_object)
|
||||
log_item.save
|
||||
end
|
||||
|
||||
it 'uses full class name and id as key' do
|
||||
log_item.log_data = 'Yay!'
|
||||
allow(client).to receive(:head_bucket)
|
||||
expect(client).to receive(:put_object).with(hash_including(key: /couch_log_items\/log_data\//))
|
||||
log_item.save
|
||||
end
|
||||
|
||||
it 'marks attachment not dirty after upload' do
|
||||
log_item.log_data = 'Yay!'
|
||||
allow(client).to receive(:head_bucket)
|
||||
log_item.save
|
||||
expect(log_item.instance_variable_get(:@_s3_attachments)[:log_data][:dirty]).to be_falsy
|
||||
end
|
||||
|
||||
it 'stores attachment when validations succeed' do
|
||||
log_item.log_data = 'Yay!'
|
||||
allow(client).to receive(:head_bucket)
|
||||
expect(client).to receive(:put_object)
|
||||
log_item.save
|
||||
end
|
||||
|
||||
xit 'does not store attachment when validations fail' do
|
||||
log_item.log_data = 'Yay!'
|
||||
allow(client).to receive(:head_bucket)
|
||||
allow(log_item).to receive(:valid?).and_return(false)
|
||||
expect(client).not_to receive(:put_object)
|
||||
log_item.save
|
||||
end
|
||||
|
||||
it 'serializes non-string data to json' do
|
||||
log_item.log_data = ['one log entry', 'and another one']
|
||||
allow(client).to receive(:head_bucket)
|
||||
expect(client).to receive(:put_object).with(hash_including(body: '["one log entry","and another one"]'))
|
||||
log_item.save
|
||||
end
|
||||
|
||||
context 'attachment size' do
|
||||
before do
|
||||
allow(client).to receive(:head_bucket)
|
||||
end
|
||||
|
||||
it 'stores on upload' do
|
||||
log_item.log_data = 'abc'
|
||||
log_item.save
|
||||
expect(log_item.log_data_size).to eq 3
|
||||
end
|
||||
|
||||
it 'updates size on change' do
|
||||
log_item.log_data = 'abc'
|
||||
log_item.save
|
||||
expect(log_item.log_data_size).to eq 3
|
||||
log_item.log_data = 'example'
|
||||
log_item.save
|
||||
expect(log_item.log_data_size).to eq 7
|
||||
end
|
||||
|
||||
it 'stores size of json attachments' do
|
||||
log_item.log_data = ['abc']
|
||||
log_item.save
|
||||
expect(log_item.log_data_size).to eq ['abc'].to_json.size
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when fetching the data' do
|
||||
let(:resp) { double('response', body: double(read: 'Yay!')) }
|
||||
|
||||
before do
|
||||
allow(client).to receive(:get_object).and_return(resp)
|
||||
end
|
||||
|
||||
it 'creates a configured S3 connection' do
|
||||
CouchLogItem._s3_options[:log_data][:bucket] = 'mybucket'
|
||||
CouchLogItem._s3_options[:log_data][:location] = :eu
|
||||
|
||||
expect(Aws::S3::Client).to receive(:new).with(
|
||||
access_key_id: 'abcdef',
|
||||
secret_access_key: 'secret!',
|
||||
region: 'eu-west-1',
|
||||
logger: nil
|
||||
).and_return(client)
|
||||
|
||||
log_item.log_data
|
||||
end
|
||||
|
||||
it 'fetches data from s3' do
|
||||
log_item.instance_variable_set(:@_s3_attachments, {})
|
||||
expect(client).to receive(:get_object).with(
|
||||
bucket: 'bucket-for-monsieur',
|
||||
key: anything
|
||||
).and_return(resp)
|
||||
expect(log_item.log_data).to eq 'Yay!'
|
||||
end
|
||||
|
||||
it 'does not mark attachment as dirty' do
|
||||
log_item.instance_variable_set(:@_s3_attachments, {})
|
||||
log_item.log_data
|
||||
expect(log_item._s3_attachments[:log_data][:dirty]).to be_falsy
|
||||
end
|
||||
|
||||
it 'does not fetch if already set' do
|
||||
log_item.log_data = 'Yay!'
|
||||
expect(client).not_to receive(:get_object)
|
||||
expect(log_item.log_data).to eq 'Yay!'
|
||||
end
|
||||
end
|
||||
|
||||
context 'when deleting' do
|
||||
before do
|
||||
CouchLogItem._s3_options[:log_data][:after_delete] = :nothing
|
||||
allow(client).to receive(:head_bucket)
|
||||
log_item.log_data = 'Yatzzee'
|
||||
log_item.save
|
||||
end
|
||||
|
||||
it 'does nothing to S3' do
|
||||
expect(client).not_to receive(:delete_object)
|
||||
log_item.delete
|
||||
end
|
||||
|
||||
it 'deletes on S3 if configured' do
|
||||
CouchLogItem._s3_options[:log_data][:after_delete] = :delete
|
||||
expect(client).to receive(:delete_object).with(
|
||||
bucket: 'bucket-for-monsieur',
|
||||
key: anything
|
||||
)
|
||||
log_item.delete
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,453 @@
|
||||
require 'spec_helper'
|
||||
|
||||
def recreate_db!
|
||||
SimplyBased.database.drop_database!
|
||||
end
|
||||
|
||||
RSpec.describe 'SoftDeletable' do
|
||||
context 'when using soft deletable' do
|
||||
it 'knows when it is enabled' do
|
||||
expect(Hemorrhoid.soft_deleting_enabled?).to be true
|
||||
expect(User.soft_deleting_enabled?).to be false
|
||||
end
|
||||
|
||||
it 'defines a :deleted_at attribute' do
|
||||
h = Hemorrhoid.new
|
||||
expect(h).to respond_to(:deleted_at)
|
||||
expect(h).to respond_to(:deleted_at=)
|
||||
expect(Hemorrhoid.soft_delete_attribute).to eq :deleted_at
|
||||
end
|
||||
|
||||
it 'defines hard delete methods' do
|
||||
h = Hemorrhoid.new
|
||||
expect(h).to respond_to(:destroy!)
|
||||
expect(h).to respond_to(:delete!)
|
||||
end
|
||||
|
||||
context 'when deleting' do
|
||||
before do
|
||||
@user = User.new(name: 'BigT', title: 'Dr.')
|
||||
@user.save!
|
||||
@hemorrhoid = Hemorrhoid.new
|
||||
@hemorrhoid.user = @user
|
||||
@hemorrhoid.save!
|
||||
end
|
||||
|
||||
it 'does not delete the object but populates the soft_delete_attribute' do
|
||||
now = Time.now
|
||||
allow(Time).to receive(:now).and_return(now)
|
||||
expect(@hemorrhoid.deleted_at).to be_nil
|
||||
expect(@hemorrhoid.delete).to be_truthy
|
||||
expect(@hemorrhoid.deleted_at).to eq now
|
||||
end
|
||||
|
||||
it 'survives reloads with the new attribute' do
|
||||
expect(@hemorrhoid.deleted_at).to be_nil
|
||||
expect(@hemorrhoid.delete).to be_truthy
|
||||
@hemorrhoid.reload
|
||||
expect(@hemorrhoid.deleted_at).not_to be_nil
|
||||
end
|
||||
|
||||
it 'knows when it is deleted' do
|
||||
expect(@hemorrhoid.deleted?).to be false
|
||||
@hemorrhoid.delete
|
||||
expect(@hemorrhoid.deleted?).to be true
|
||||
end
|
||||
|
||||
it 'does not consider objects without soft-deleted as deleted' do
|
||||
expect(@user.deleted?).to be false
|
||||
@user.delete
|
||||
expect(@user.deleted?).to be false
|
||||
end
|
||||
|
||||
it 'does not delete in DB' do
|
||||
expect(SimplyBased.database).not_to receive(:destroy_document)
|
||||
@hemorrhoid.destroy
|
||||
end
|
||||
|
||||
it 'really deletes with callbacks' do
|
||||
expect(SimplyBased.database).to receive(:destroy_document).with(@hemorrhoid, true)
|
||||
@hemorrhoid.destroy!
|
||||
end
|
||||
|
||||
it 'really deletes without callbacks if the object was soft-deleted before' do
|
||||
expect(SimplyBased.database).to receive(:destroy_document).with(@hemorrhoid, false)
|
||||
@hemorrhoid.destroy
|
||||
@hemorrhoid.destroy!
|
||||
end
|
||||
|
||||
context 'callbacks' do
|
||||
it 'still fires the callbacks' do
|
||||
@hemorrhoid = Hemorrhoid.create
|
||||
$before = nil
|
||||
$after = nil
|
||||
def @hemorrhoid.before_destroy_callback
|
||||
$before = 'now'
|
||||
end
|
||||
|
||||
def @hemorrhoid.after_destroy_callback
|
||||
$after = 'now'
|
||||
end
|
||||
|
||||
@hemorrhoid.destroy
|
||||
|
||||
expect($before).not_to be_nil
|
||||
expect($after).not_to be_nil
|
||||
end
|
||||
|
||||
it 'does not fire the callbacks on the real destroy if the object is already deleted' do
|
||||
@hemorrhoid = Hemorrhoid.create
|
||||
def @hemorrhoid.before_destroy_callback
|
||||
raise "Callback called even though #{skip_callbacks.inspect}"
|
||||
end
|
||||
|
||||
def @hemorrhoid.after_destroy_callback
|
||||
raise "Callback called even though #{skip_callbacks.inspect}"
|
||||
end
|
||||
|
||||
def @hemorrhoid.deleted?
|
||||
true
|
||||
end
|
||||
|
||||
expect { @hemorrhoid.destroy! }.not_to raise_error
|
||||
end
|
||||
|
||||
it 'fires the callbacks on the real destroy if the object is not deleted' do
|
||||
@hemorrhoid = Hemorrhoid.create
|
||||
$before = nil
|
||||
$after = nil
|
||||
def @hemorrhoid.before_destroy_callback
|
||||
$before = 'now'
|
||||
end
|
||||
|
||||
def @hemorrhoid.after_destroy_callback
|
||||
$after = 'now'
|
||||
end
|
||||
|
||||
@hemorrhoid.destroy!
|
||||
|
||||
expect($before).not_to be_nil
|
||||
expect($after).not_to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when handling the dependent objects' do
|
||||
before do
|
||||
@sub = SubHemorrhoid.new
|
||||
@sub.hemorrhoid = @hemorrhoid
|
||||
@sub.save!
|
||||
|
||||
@easy_sub = EasySubHemorrhoid.new
|
||||
@easy_sub.hemorrhoid = @hemorrhoid
|
||||
@easy_sub.save!
|
||||
|
||||
@rash = Rash.new
|
||||
@rash.hemorrhoid = @hemorrhoid
|
||||
@rash.save!
|
||||
|
||||
@hemorrhoid.reload
|
||||
end
|
||||
|
||||
it 'deletes them' do
|
||||
@hemorrhoid.delete
|
||||
@sub.reload
|
||||
expect(@sub.deleted?).to be true
|
||||
expect { EasySubHemorrhoid.find(@easy_sub.id, with_deleted: true) }.to raise_error(SimplyBased::RecordNotFound)
|
||||
@rash = Rash.find(@rash.id)
|
||||
expect(@rash.hemorrhoid_id).to be_nil
|
||||
end
|
||||
|
||||
it 'really deletes them if the parent is really deleted' do
|
||||
@hemorrhoid.delete!
|
||||
expect { EasySubHemorrhoid.find(@sub.id, with_deleted: true) }.to raise_error(SimplyBased::RecordNotFound)
|
||||
|
||||
expect { EasySubHemorrhoid.find(@easy_sub.id, with_deleted: true) }.to raise_error(SimplyBased::RecordNotFound)
|
||||
|
||||
@rash = Rash.find(@rash.id)
|
||||
expect(@rash.hemorrhoid_id).to be_nil
|
||||
end
|
||||
|
||||
it 'nullifies dependents if they are soft-deletable and deleted' do
|
||||
small_rash = SmallRash.create(hemorrhoid: @hemorrhoid)
|
||||
@hemorrhoid.reload
|
||||
@hemorrhoid.destroy
|
||||
expect(@hemorrhoid.deleted?).to be true
|
||||
small_rash = SmallRash.find(small_rash.id)
|
||||
expect(small_rash.hemorrhoid_id).to be_nil
|
||||
end
|
||||
|
||||
it 'does not nullify dependents if they are soft-deletable and not deleted' do
|
||||
small_rash = SmallRash.create(hemorrhoid: @hemorrhoid)
|
||||
@hemorrhoid.reload
|
||||
expect(@hemorrhoid.deleted?).to be false
|
||||
small_rash = SmallRash.find(small_rash.id)
|
||||
expect(small_rash.hemorrhoid_id).not_to be_nil
|
||||
expect(small_rash.hemorrhoid_id).to eq @hemorrhoid.id
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when loading' do
|
||||
before do
|
||||
@user = User.new(name: 'BigT', title: 'Dr.')
|
||||
@user.save!
|
||||
@hemorrhoid = Hemorrhoid.new
|
||||
@hemorrhoid.user = @user
|
||||
@hemorrhoid.save!
|
||||
end
|
||||
|
||||
context 'by id' do
|
||||
it 'is not found by default' do
|
||||
@hemorrhoid.destroy
|
||||
expect { Hemorrhoid.find(@hemorrhoid.id) }.to raise_error(SimplyBased::RecordNotFound)
|
||||
end
|
||||
|
||||
it 'is found if supplied with :with_deleted' do
|
||||
@hemorrhoid.destroy
|
||||
|
||||
expect(Hemorrhoid.find(@hemorrhoid.id, with_deleted: true)).not_to be_nil
|
||||
end
|
||||
|
||||
it 'is not found if it is really gone' do
|
||||
old_id = @hemorrhoid.id
|
||||
@hemorrhoid.destroy!
|
||||
|
||||
expect { Hemorrhoid.find(old_id) }.to raise_error(SimplyBased::RecordNotFound)
|
||||
end
|
||||
|
||||
it 'always reloads' do
|
||||
@hemorrhoid.destroy
|
||||
expect { @hemorrhoid.reload }.not_to raise_error
|
||||
expect(@hemorrhoid.deleted_at).not_to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'all' do
|
||||
before do
|
||||
recreate_db!
|
||||
@hemorrhoid = Hemorrhoid.create
|
||||
expect(@hemorrhoid.destroy).to be_truthy
|
||||
expect(@hemorrhoid.reload.deleted?).to be true
|
||||
end
|
||||
|
||||
it 'does not load deleted' do
|
||||
expect(Hemorrhoid.find(:all)).to eq []
|
||||
expect(Hemorrhoid.find(:all, with_deleted: false)).to eq []
|
||||
end
|
||||
|
||||
it 'loads non-deleted' do
|
||||
hemorrhoid = Hemorrhoid.create
|
||||
expect(Hemorrhoid.find(:all)).not_to eq []
|
||||
expect(Hemorrhoid.find(:all, with_deleted: false)).not_to eq []
|
||||
end
|
||||
|
||||
it 'loads deleted if asked to' do
|
||||
expect(Hemorrhoid.find(:all, with_deleted: true).map(&:id)).to eq [@hemorrhoid.id]
|
||||
end
|
||||
end
|
||||
|
||||
context 'first' do
|
||||
before do
|
||||
recreate_db!
|
||||
@hemorrhoid = Hemorrhoid.create
|
||||
expect(@hemorrhoid.destroy).to be_truthy
|
||||
expect(@hemorrhoid.reload.deleted?).to be true
|
||||
end
|
||||
|
||||
it 'does not load deleted' do
|
||||
expect(Hemorrhoid.find(:first)).to be_nil
|
||||
expect(Hemorrhoid.find(:first, with_deleted: false)).to be_nil
|
||||
end
|
||||
|
||||
it 'loads non-deleted' do
|
||||
hemorrhoid = Hemorrhoid.create
|
||||
expect(Hemorrhoid.find(:first)).not_to be_nil
|
||||
expect(Hemorrhoid.find(:first)).to be_a(Hemorrhoid)
|
||||
expect(Hemorrhoid.find(:first, with_deleted: false)).not_to be_nil
|
||||
expect(Hemorrhoid.find(:first, with_deleted: false)).to be_a(Hemorrhoid)
|
||||
end
|
||||
|
||||
it 'loads deleted if asked to' do
|
||||
expect(Hemorrhoid.find(:first, with_deleted: true)).to eq @hemorrhoid
|
||||
end
|
||||
end
|
||||
|
||||
context 'find_by and find_all_by' do
|
||||
before do
|
||||
recreate_db!
|
||||
@hemorrhoid = Hemorrhoid.create(nickname: 'Claas', size: 3)
|
||||
@hemorrhoid.destroy
|
||||
end
|
||||
|
||||
context 'find_by' do
|
||||
it 'does not load deleted' do
|
||||
expect(Hemorrhoid.find_by_nickname('Claas')).to be_nil
|
||||
expect(Hemorrhoid.find_by_nickname('Claas', with_deleted: false)).to be_nil
|
||||
|
||||
expect(Hemorrhoid.find_by_nickname_and_size('Claas', 3)).to be_nil
|
||||
expect(Hemorrhoid.find_by_nickname_and_size('Claas', 3, with_deleted: false)).to be_nil
|
||||
end
|
||||
|
||||
it 'loads non-deleted' do
|
||||
hemorrhoid = Hemorrhoid.create(nickname: 'OtherNick', size: 3)
|
||||
expect(Hemorrhoid.find_by_nickname('OtherNick', with_deleted: true).id).to eq hemorrhoid.id
|
||||
expect(Hemorrhoid.find_by_nickname('OtherNick').id).to eq hemorrhoid.id
|
||||
end
|
||||
|
||||
it 'loads deleted if asked to' do
|
||||
expect(Hemorrhoid.find_by_nickname('Claas', with_deleted: true)).not_to be_nil
|
||||
expect(Hemorrhoid.find_by_nickname('Claas', with_deleted: true).id).to eq @hemorrhoid.id
|
||||
|
||||
expect(Hemorrhoid.find_by_nickname_and_size('Claas', 3, with_deleted: true)).not_to be_nil
|
||||
expect(Hemorrhoid.find_by_nickname_and_size('Claas', 3, with_deleted: true).id).to eq @hemorrhoid.id
|
||||
end
|
||||
end
|
||||
|
||||
context 'find_all_by' do
|
||||
it 'does not load deleted' do
|
||||
expect(Hemorrhoid.find_all_by_nickname('Claas')).to eq []
|
||||
expect(Hemorrhoid.find_all_by_nickname('Claas', with_deleted: false)).to eq []
|
||||
|
||||
expect(Hemorrhoid.find_all_by_nickname_and_size('Claas', 3)).to eq []
|
||||
expect(Hemorrhoid.find_all_by_nickname_and_size('Claas', 3, with_deleted: false)).to eq []
|
||||
end
|
||||
|
||||
it 'loads non-deleted' do
|
||||
hemorrhoid = Hemorrhoid.create(nickname: 'Lampe', size: 4)
|
||||
expect(Hemorrhoid.find_all_by_nickname('Lampe').map(&:id)).to eq [hemorrhoid.id]
|
||||
end
|
||||
|
||||
it 'loads deleted if asked to' do
|
||||
expect(Hemorrhoid.find_all_by_nickname('Claas', with_deleted: true).map(&:id)).to eq [@hemorrhoid.id]
|
||||
expect(Hemorrhoid.find_all_by_nickname_and_size('Claas', 3, with_deleted: true).map(&:id)).to eq [@hemorrhoid.id]
|
||||
end
|
||||
end
|
||||
|
||||
it 'reuses the same view - when find_all_by is called first' do
|
||||
expect(Hemorrhoid.find_all_by_nickname('Claas')).to eq []
|
||||
expect(Hemorrhoid.find_by_nickname('Claas')).to be_nil
|
||||
end
|
||||
|
||||
it 'reuses the same view - when find_by is called first' do
|
||||
expect(Hemorrhoid.find_by_nickname('Claas')).to be_nil
|
||||
expect(Hemorrhoid.find_all_by_nickname('Claas')).to eq []
|
||||
end
|
||||
end
|
||||
|
||||
context 'by relation' do
|
||||
before do
|
||||
@hemorrhoid.destroy
|
||||
end
|
||||
|
||||
context 'has_many' do
|
||||
it 'does not load deleted by default' do
|
||||
expect(@user.hemorrhoids(force_reload: true)).to eq []
|
||||
end
|
||||
|
||||
it 'loads deleted if asked to' do
|
||||
expect(@user.hemorrhoids(force_reload: true, with_deleted: true).map(&:id)).to eq [@hemorrhoid.id]
|
||||
end
|
||||
end
|
||||
|
||||
context 'has_many :through' do
|
||||
before do
|
||||
@user = User.create(name: 'BigT', title: 'Dr.')
|
||||
@pain = Pain.create
|
||||
|
||||
@hemorrhoid = Hemorrhoid.new
|
||||
@hemorrhoid.user = @user
|
||||
@hemorrhoid.pain = @pain
|
||||
@hemorrhoid.save!
|
||||
|
||||
@hemorrhoid.destroy
|
||||
end
|
||||
|
||||
it 'does not load deleted by default' do
|
||||
expect(@user.pains).to eq []
|
||||
end
|
||||
|
||||
it 'loads deleted if asked to' do
|
||||
expect(@user.pains(with_deleted: true).map(&:id)).to eq [@pain.id]
|
||||
end
|
||||
end
|
||||
|
||||
context 'has_one' do
|
||||
before do
|
||||
@spot = Spot.create
|
||||
|
||||
@hemorrhoid = Hemorrhoid.new
|
||||
@hemorrhoid.spot = @spot
|
||||
@hemorrhoid.save!
|
||||
|
||||
@hemorrhoid.destroy
|
||||
end
|
||||
|
||||
it 'does not load deleted by default' do
|
||||
expect(@spot.hemorrhoid(force_reload: true)).to be_nil
|
||||
end
|
||||
|
||||
it 'loads deleted if asked to' do
|
||||
expect(@spot.hemorrhoid(force_reload: true, with_deleted: true).id).to eq @hemorrhoid.id
|
||||
end
|
||||
end
|
||||
|
||||
context 'belongs_to' do
|
||||
before do
|
||||
@hemorrhoid = Hemorrhoid.new
|
||||
@hemorrhoid.save!
|
||||
|
||||
@sub = SubHemorrhoid.new
|
||||
@sub.hemorrhoid = @hemorrhoid
|
||||
@sub.save!
|
||||
|
||||
@hemorrhoid.destroy
|
||||
end
|
||||
|
||||
it 'does not load deleted by default' do
|
||||
@sub.reload
|
||||
expect(@sub.hemorrhoid(force_reload: true)).to be_nil
|
||||
end
|
||||
|
||||
it 'loads deleted if asked to' do
|
||||
@sub.reload
|
||||
expect(@sub.hemorrhoid(force_reload: true, with_deleted: true).id).to eq @hemorrhoid.id
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when counting' do
|
||||
before do
|
||||
@hemorrhoid = Hemorrhoid.create(nickname: 'Claas')
|
||||
expect(@hemorrhoid.destroy).to be_truthy
|
||||
expect(@hemorrhoid.reload.deleted?).to be true
|
||||
end
|
||||
|
||||
it 'does not count deleted' do
|
||||
expect(Hemorrhoid.count).to eq 0
|
||||
expect(Hemorrhoid.count(with_deleted: false)).to eq 0
|
||||
end
|
||||
|
||||
it 'counts non-deleted' do
|
||||
hemorrhoid = Hemorrhoid.create(nickname: 'Claas')
|
||||
expect(Hemorrhoid.count).to eq 1
|
||||
expect(Hemorrhoid.count(with_deleted: false)).to eq 1
|
||||
end
|
||||
|
||||
it 'counts deleted if asked to' do
|
||||
expect(Hemorrhoid.count(with_deleted: true)).to eq 1
|
||||
end
|
||||
|
||||
context 'count_by' do
|
||||
it 'does not count deleted' do
|
||||
expect(Hemorrhoid.count_by_nickname('Claas')).to eq 0
|
||||
expect(Hemorrhoid.count_by_nickname('Claas', with_deleted: false)).to eq 0
|
||||
end
|
||||
|
||||
it 'counts deleted if asked to' do
|
||||
expect(Hemorrhoid.count_by_nickname('Claas', with_deleted: true)).to eq 1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,28 @@
|
||||
require 'pry'
|
||||
require 'simply_based'
|
||||
require 'couchbase'
|
||||
require 'fixtures/couch'
|
||||
|
||||
COUCHBASE_URL = ENV.fetch(
|
||||
'COUCHBASE_URL',
|
||||
'couchbase://Administrator:password@localhost/simply_based_test'
|
||||
)
|
||||
|
||||
Dir.glob('spec/support/**/*.rb').each { |f| require f.sub(%r{^spec/}, '') }
|
||||
|
||||
RSpec.configure do |config|
|
||||
config.expect_with(:rspec) { |c| c.syntax = [:expect, :should] }
|
||||
config.color = true
|
||||
config.filter_run_excluding :couchdb_only # CouchDB-mechanic specs with no Couchbase equivalent
|
||||
|
||||
config.before(:suite) do
|
||||
SimplyBased.database_url = COUCHBASE_URL
|
||||
SimplyBased.database.create_database! # primary index
|
||||
end
|
||||
|
||||
config.before(:each) do
|
||||
$performed_queries = []
|
||||
SimplyBased.database_url = COUCHBASE_URL
|
||||
SimplyBased.database.drop_database! # clear documents between examples
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,111 @@
|
||||
# frozen_string_literal: true
|
||||
#
|
||||
# The contract every SimplyBased::Adapter must satisfy, as shared examples.
|
||||
# A backend proves parity by including it:
|
||||
#
|
||||
# RSpec.describe SimplyBased::Adapters::CouchRest do
|
||||
# it_behaves_like 'a simply_couch adapter'
|
||||
# end
|
||||
#
|
||||
# This same file is what the simply_couch-couchbase gem runs against a live
|
||||
# Couchbase to demonstrate the backend is swappable. It assumes the suite's
|
||||
# before(:each) leaves SimplyBased.database pointing at a fresh, empty store.
|
||||
|
||||
# A minimal model used only by the contract — independent of the fixture graph
|
||||
# so the examples port cleanly to any backend.
|
||||
class ContractWidget
|
||||
include SimplyBased::Model
|
||||
property :title
|
||||
property :score, type: Integer
|
||||
validates :title, presence: true
|
||||
end
|
||||
|
||||
RSpec.shared_examples 'a simply_couch adapter' do
|
||||
let(:adapter) { SimplyBased.database }
|
||||
|
||||
def build_widget(attrs = {})
|
||||
ContractWidget.new({ title: 'a' }.merge(attrs))
|
||||
end
|
||||
|
||||
describe '#save_document + #load_document' do
|
||||
it 'persists a new document and assigns _id/_rev' do
|
||||
w = build_widget(title: 'hello', score: 3)
|
||||
expect(adapter.save_document(w)).to eq(true)
|
||||
expect(w._id).not_to be_nil
|
||||
expect(w._rev).not_to be_nil
|
||||
end
|
||||
|
||||
it 'reconstructs the stored doc into its model class via the ruby_class tag' do
|
||||
w = build_widget(title: 'hello', score: 7)
|
||||
adapter.save_document(w)
|
||||
loaded = adapter.load_document(w._id)
|
||||
expect(loaded).to be_a(ContractWidget)
|
||||
expect(loaded.title).to eq('hello')
|
||||
expect(loaded.score).to eq(7)
|
||||
expect(loaded.database).to eq(adapter)
|
||||
end
|
||||
|
||||
it 'returns nil (does not raise) when the id is absent' do
|
||||
expect(adapter.load_document('does-not-exist')).to be_nil
|
||||
end
|
||||
|
||||
it 'refuses to persist an invalid document' do
|
||||
expect(adapter.save_document(build_widget(title: nil))).to eq(false)
|
||||
end
|
||||
|
||||
it 'updates an existing doc and rolls _rev forward' do
|
||||
w = build_widget(title: 'v1')
|
||||
adapter.save_document(w)
|
||||
first_rev = w._rev
|
||||
w.title = 'v2'
|
||||
adapter.save_document(w)
|
||||
expect(w._rev).not_to eq(first_rev)
|
||||
expect(adapter.load_document(w._id).title).to eq('v2')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#destroy_document' do
|
||||
it 'removes the document' do
|
||||
w = build_widget
|
||||
adapter.save_document(w)
|
||||
id = w._id
|
||||
adapter.destroy_document(w)
|
||||
expect(adapter.load_document(id)).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe 'bulk operations' do
|
||||
it 'bulk_saves, bulk_loads (reconstructed) and bulk_destroys' do
|
||||
a = build_widget(title: 'a')
|
||||
b = build_widget(title: 'b')
|
||||
result = adapter.bulk_save([a, b])
|
||||
expect(result[:saved].size).to eq(2)
|
||||
expect(a._id).not_to be_nil
|
||||
|
||||
loaded = adapter.bulk_load([a._id, b._id])
|
||||
expect(loaded).to all(be_a(ContractWidget))
|
||||
expect(loaded.map(&:title).sort).to eq(%w[a b])
|
||||
|
||||
adapter.bulk_destroy([a, b])
|
||||
expect(adapter.bulk_load([a._id, b._id])).to eq([])
|
||||
end
|
||||
|
||||
it 'bulk_load drops ids that are missing' do
|
||||
a = build_widget
|
||||
adapter.save_document(a)
|
||||
expect(adapter.bulk_load([a._id, 'missing']).map(&:_id)).to eq([a._id])
|
||||
end
|
||||
end
|
||||
|
||||
describe 'optimistic concurrency' do
|
||||
it 'maps a stale-write clash to SimplyBased::Conflict' do
|
||||
w = build_widget(title: 'orig')
|
||||
adapter.save_document(w)
|
||||
stale = adapter.load_document(w._id)
|
||||
w.title = 'winner'
|
||||
adapter.save_document(w) # advances the stored revision
|
||||
stale.title = 'loser'
|
||||
expect { adapter.save_document(stale) }.to raise_error(SimplyBased::Conflict)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,50 @@
|
||||
# Query-count instrumentation for SimplyBased. The Store appends each KV op and
|
||||
# N1QL statement to $performed_queries (see SimplyBased::Store#_instrument), so
|
||||
# these matchers measure database round-trips without touching any driver.
|
||||
#
|
||||
# NOTE: absolute query counts differ from the CouchDB original — Couchbase
|
||||
# resolves multi-id loads as individual KV gets rather than one _bulk_docs call,
|
||||
# so specs asserting exact CouchDB counts are tagged :couchdb_counts.
|
||||
$performed_queries = []
|
||||
|
||||
RSpec::Matchers.define :exceed_query_limit do |expected|
|
||||
match do |block|
|
||||
query_count(&block) > expected
|
||||
end
|
||||
|
||||
failure_message_when_negated do |actual|
|
||||
"Expected to run maximum #{expected} queries, got #{@executed_queries}\n" \
|
||||
"queries:\n - #{$performed_queries.map { |q| q[:url] }.join("\n - ")}"
|
||||
end
|
||||
|
||||
def query_count(&block)
|
||||
$performed_queries = []
|
||||
block.call
|
||||
@executed_queries = $performed_queries.size
|
||||
end
|
||||
|
||||
def supports_block_expectations?
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
RSpec::Matchers.define :perform_any_queries do |expected|
|
||||
match do |block|
|
||||
query_count(&block) > 0
|
||||
end
|
||||
|
||||
failure_message_when_negated do |actual|
|
||||
"Expected to run no queries, got #{@executed_queries}\n" \
|
||||
"Extra queries:\n - #{$performed_queries.map { |q| q[:url] }.join("\n - ")}"
|
||||
end
|
||||
|
||||
def query_count(&block)
|
||||
$performed_queries = []
|
||||
block.call
|
||||
@executed_queries = $performed_queries.size
|
||||
end
|
||||
|
||||
def supports_block_expectations?
|
||||
true
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,146 @@
|
||||
require 'spec_helper'
|
||||
|
||||
describe 'validations' do
|
||||
context "with additional validations" do
|
||||
context "with validates_inclusion_of" do
|
||||
it "validate inclusion of an attribute in an array" do
|
||||
category = Category.new(name: "other")
|
||||
category.save.should be false
|
||||
end
|
||||
|
||||
it "add an error message" do
|
||||
category = Category.new(name: "other")
|
||||
category.valid?
|
||||
category.errors.full_messages.first.should match /is not included in the list/
|
||||
end
|
||||
|
||||
it "allow blank" do
|
||||
category = Category.new(name: nil)
|
||||
category.should be_valid
|
||||
end
|
||||
end
|
||||
|
||||
context "with validates_format_of" do
|
||||
class ValidatedUser
|
||||
include SimplyBased::Model
|
||||
property :name
|
||||
validates_format_of :name, with: /Paul/
|
||||
end
|
||||
|
||||
it 'validate the format and fail when not matched' do
|
||||
user = ValidatedUser.new(name: "John")
|
||||
user.should_not be_valid
|
||||
end
|
||||
|
||||
it 'succeed when matched' do
|
||||
user = ValidatedUser.new(name: "Paul")
|
||||
user.should be_valid
|
||||
end
|
||||
|
||||
it 'fail when empty' do
|
||||
user = ValidatedUser.new(name: nil)
|
||||
user.should_not be_valid
|
||||
end
|
||||
|
||||
context "with allow_blank" do
|
||||
class ValidatedBlankUser
|
||||
include SimplyBased::Model
|
||||
property :name
|
||||
validates_format_of :name, with: /Paul/, allow_blank: true
|
||||
end
|
||||
|
||||
it 'not fail when nil' do
|
||||
user = ValidatedBlankUser.new(name: nil)
|
||||
user.should be_valid
|
||||
end
|
||||
|
||||
it 'not fail when empty string' do
|
||||
user = ValidatedBlankUser.new(name: '')
|
||||
user.should be_valid
|
||||
end
|
||||
|
||||
it 'fail when not matching' do
|
||||
user = ValidatedBlankUser.new(name: 'John')
|
||||
user.should_not be_valid
|
||||
end
|
||||
|
||||
it 'not fail when matching' do
|
||||
user = ValidatedBlankUser.new(name: 'Paul')
|
||||
user.should be_valid
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
context "with validates_uniqueness_of" do
|
||||
#it "add a view on the unique attribute" do
|
||||
#UniqueUser.by_name
|
||||
#end
|
||||
|
||||
it "set an error when a different with the same instance exists" do
|
||||
UniqueUser.create(name: "Host Master")
|
||||
user = nil
|
||||
expect{ user = UniqueUser.create(name: "Host Master") }.not_to change{ UniqueUser.count }
|
||||
user.should_not be_valid
|
||||
end
|
||||
|
||||
it "not have an error when we're the only one around" do
|
||||
user = UniqueUser.create(name: "Host Master")
|
||||
user.should_not be_new_record
|
||||
end
|
||||
|
||||
it "not have an error when it's the same instance" do
|
||||
user = UniqueUser.create(name: "Host Master")
|
||||
user = UniqueUser.find(user.id)
|
||||
user.should be_valid
|
||||
end
|
||||
|
||||
it 'have a nice error message' do
|
||||
UniqueUser.create(name: "Host Master")
|
||||
user = nil
|
||||
expect{ user = UniqueUser.create(name: "Host Master") }.not_to change{ UniqueUser.count }
|
||||
user.errors[:name].should eq ["is already taken"]
|
||||
end
|
||||
|
||||
it 'create a view to check with' do
|
||||
UniqueUser.should respond_to :by_name
|
||||
UniqueUser.by_name.send(:options)[:key].should eq :name
|
||||
end
|
||||
|
||||
it 'not overwrite the view when a custom one already exists' do
|
||||
UniqueUserWithAView.by_name.send(:options)[:key].should eq :email
|
||||
end
|
||||
end
|
||||
|
||||
context "equality" do
|
||||
it "know when two objects are equal" do
|
||||
user = UniqueUser.create(name: "Host Master")
|
||||
other_user = UniqueUser.create(name: "The other one")
|
||||
user.should_not eq other_user
|
||||
end
|
||||
|
||||
it "not bail when comparing with non-SimplyBased objects" do
|
||||
user = UniqueUser.create(name: "Host Master")
|
||||
5.should_not eq user
|
||||
user.should_not eq 5
|
||||
end
|
||||
end
|
||||
|
||||
context "containment" do
|
||||
it "not raise an error when no name is set" do
|
||||
expect{ Page.new.save }.not_to raise_error
|
||||
end
|
||||
|
||||
it "be valid when argument is contained in specification" do
|
||||
page = Page.new(categories: %w[one three])
|
||||
page.should be_valid
|
||||
end
|
||||
|
||||
it "not be valid when attribute containes value not withing specified containment" do
|
||||
page = Page.new(categories: %w[one four])
|
||||
page.should_not be_valid
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,34 @@
|
||||
require 'spec_helper'
|
||||
|
||||
describe "CouchDB view token safety", :couchdb_only do
|
||||
describe "SimplyBased.assert_safe_view_token!" do
|
||||
it "passes valid class / association / property names through unchanged" do
|
||||
expect {
|
||||
SimplyBased.assert_safe_view_token!('User', :posts, 'Namespaced::Thing', :editor_id, nil)
|
||||
}.not_to raise_error
|
||||
end
|
||||
|
||||
it "accepts nested arrays and ignores nil/empty tokens" do
|
||||
expect {
|
||||
SimplyBased.assert_safe_view_token!([:locale, nil], '', :position)
|
||||
}.not_to raise_error
|
||||
end
|
||||
|
||||
it "raises on characters that could break out of the generated JavaScript" do
|
||||
["a'b", 'a"b', 'a\\b', "a;b", "a)b", "a{b", "a(b", "a\nb"].each do |bad|
|
||||
expect {
|
||||
SimplyBased.assert_safe_view_token!(bad)
|
||||
}.to raise_error(SimplyBased::Error), "expected #{bad.inspect} to be rejected"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it "rejects an association declared with an unsafe name at definition time" do
|
||||
expect {
|
||||
Class.new do
|
||||
include SimplyBased::Model
|
||||
belongs_to :"evil'); emit(doc,1);//"
|
||||
end
|
||||
}.to raise_error(SimplyBased::Error)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,23 @@
|
||||
require 'spec_helper'
|
||||
|
||||
class CustomViewUser
|
||||
include SimplyBased::Model
|
||||
|
||||
property :tags
|
||||
view :by_tags, type: SimplyBased::Model::Views::ArrayPropertyViewSpec, key: :tags
|
||||
end
|
||||
|
||||
describe 'Custom couch views' do
|
||||
context 'with array views' do
|
||||
it 'finds objects with one match of the array' do
|
||||
CustomViewUser.create(tags: ['agile', 'cool', 'extreme'])
|
||||
CustomViewUser.create(tags: ['agile'])
|
||||
expect(CustomViewUser.find_all_by_tags('agile').size).to eq 2
|
||||
end
|
||||
|
||||
it 'finds the object when the property is not an array' do
|
||||
CustomViewUser.create(tags: 'agile')
|
||||
expect(CustomViewUser.find_all_by_tags('agile').size).to eq 1
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in new issue
Block a user