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:
GregandClaude Opus 4.8 committed 2026-06-20 13:58:47 -05:00
commit 86299b8827
84 files changed
+9904

No files matched your search

+7
View File
@@ -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
+289
View File
@@ -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
+16
View File
@@ -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
+217
View File
@@ -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
+40
View File
@@ -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
+40
View File
@@ -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
+56
View File
@@ -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
+118
View File
@@ -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
+112
View File
@@ -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
+19
View File
@@ -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
+173
View File
@@ -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
+45
View File
@@ -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
+236
View File
@@ -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
+354
View File
@@ -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
+639
View File
@@ -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
+476
View File
@@ -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
+139
View File
@@ -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
+43
View File
@@ -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
+226
View File
@@ -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
+15
View File
@@ -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
+37
View File
@@ -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
+68
View File
@@ -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
+78
View File
@@ -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
+56
View File
@@ -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
+31
View File
@@ -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
View File
@@ -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
+453
View File
@@ -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
+28
View File
@@ -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
+111
View File
@@ -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
+50
View File
@@ -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
+146
View File
@@ -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
+34
View File
@@ -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
+23
View File
@@ -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