mm_partial_update 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/test/models.rb ADDED
@@ -0,0 +1,20 @@
1
+ class Person
2
+ include MongoMapper::Document
3
+ key :name, String
4
+ many :pets
5
+ one :favorite_pet, :class_name=>'Pet'
6
+ end
7
+
8
+ class Pet
9
+ include MongoMapper::EmbeddedDocument
10
+ key :name, String
11
+ key :age, Integer
12
+ many :fleas
13
+ one :favorite_flea, :class_name=>'Flea'
14
+ end
15
+
16
+ class Flea
17
+ include MongoMapper::EmbeddedDocument
18
+ key :name, String
19
+ end
20
+
@@ -0,0 +1,96 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+
4
+ $:.unshift File.expand_path(File.dirname(__FILE__) + '/../lib')
5
+ require 'mm_partial_update'
6
+ require 'fileutils'
7
+ require 'ostruct'
8
+
9
+ require 'log_buddy'
10
+ require 'matchy'
11
+ require 'shoulda'
12
+
13
+ class Test::Unit::TestCase
14
+ def Doc(name='Class', &block)
15
+ klass = Class.new
16
+ klass.class_eval do
17
+ include MongoMapper::Document
18
+ set_collection_name :test
19
+
20
+ if name
21
+ class_eval "def self.name; '#{name}' end"
22
+ class_eval "def self.to_s; '#{name}' end"
23
+ end
24
+ end
25
+
26
+ klass.class_eval(&block) if block_given?
27
+ klass.collection.remove
28
+ klass
29
+ end
30
+
31
+ def EDoc(name='Class', &block)
32
+ klass = Class.new
33
+ klass.class_eval do
34
+ include MongoMapper::EmbeddedDocument
35
+
36
+ if name
37
+ class_eval "def self.name; '#{name}' end"
38
+ class_eval "def self.to_s; '#{name}' end"
39
+ end
40
+ end
41
+
42
+ klass.class_eval(&block) if block_given?
43
+ klass
44
+ end
45
+
46
+ def drop_indexes(klass)
47
+ if klass.database.collection_names.include?(klass.collection.name)
48
+ klass.collection.drop_indexes
49
+ end
50
+ end
51
+
52
+ custom_matcher :be_true do |receiver, matcher, args|
53
+ matcher.positive_failure_message = "Expected #{receiver} to be true but it wasn't"
54
+ matcher.negative_failure_message = "Expected #{receiver} not to be true but it was"
55
+ receiver.eql?(true)
56
+ end
57
+
58
+ custom_matcher :be_false do |receiver, matcher, args|
59
+ matcher.positive_failure_message = "Expected #{receiver} to be false but it wasn't"
60
+ matcher.negative_failure_message = "Expected #{receiver} not to be false but it was"
61
+ receiver.eql?(false)
62
+ end
63
+
64
+ custom_matcher :have_error_on do |receiver, matcher, args|
65
+ receiver.valid?
66
+ attribute = args[0]
67
+ expected_message = args[1]
68
+
69
+ if expected_message.nil?
70
+ matcher.positive_failure_message = "#{receiver} had no errors on #{attribute}"
71
+ matcher.negative_failure_message = "#{receiver} had errors on #{attribute} #{receiver.errors.inspect}"
72
+ !receiver.errors[attribute].blank?
73
+ else
74
+ actual = receiver.errors[attribute]
75
+ matcher.positive_failure_message = %Q(Expected error on #{attribute} to be "#{expected_message}" but was "#{actual}")
76
+ matcher.negative_failure_message = %Q(Expected error on #{attribute} not to be "#{expected_message}" but was "#{actual}")
77
+ actual.include? expected_message
78
+ end
79
+ end
80
+
81
+ custom_matcher :have_index do |receiver, matcher, args|
82
+ index_name = args[0]
83
+ matcher.positive_failure_message = "#{receiver} does not have index named #{index_name}, but should"
84
+ matcher.negative_failure_message = "#{receiver} does have index named #{index_name}, but should not"
85
+ !receiver.collection.index_information.detect { |index| index[0] == index_name }.nil?
86
+ end
87
+ end
88
+
89
+ log_dir = File.expand_path('../../log', __FILE__)
90
+ FileUtils.mkdir_p(log_dir) unless File.exist?(log_dir)
91
+ logger = Logger.new(log_dir + '/test.log')
92
+
93
+ LogBuddy.init(:logger => logger)
94
+ MongoMapper.connection = Mongo::Connection.new('127.0.0.1', 27017, :logger => logger)
95
+ MongoMapper.database = "mm-test-#{RUBY_VERSION.gsub('.', '-')}"
96
+ MongoMapper.database.collections.each { |c| c.drop_indexes }
@@ -0,0 +1,9 @@
1
+ require 'test_helper'
2
+
3
+ class TestMmPartialUpdate < Test::Unit::TestCase
4
+ context "including mm_partial_update" do
5
+ should "include something or other" do
6
+ assert_equal defined?(MmPartialUpdate::Plugins::PartialUpdate), "constant"
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,221 @@
1
+ require 'test_helper'
2
+ require "models"
3
+
4
+ class TestPartialUpdate < Test::Unit::TestCase
5
+
6
+ context "#prepare_update_command" do
7
+
8
+ context "on root documents" do
9
+
10
+ should "return an empty command if not changed" do
11
+ doc = Person.create!
12
+ doc.send(:prepare_update_command).to_h.empty?.should be_true
13
+ end
14
+
15
+ should "return to_mongo if the entity is new" do
16
+ doc = Person.new
17
+ doc.name = "hello"
18
+ doc.send(:prepare_update_command).to_h.should ==
19
+ {"$set"=>doc.to_mongo}
20
+ end
21
+
22
+ should "provide set changed fields" do
23
+ doc = Person.create
24
+ doc.name = "hello"
25
+ doc.send(:prepare_update_command).to_h.should ==
26
+ {"$set"=>{"name"=>"hello"}}
27
+ end
28
+
29
+ context "with changes to 'one' embedded associations" do
30
+ should "report a new child" do
31
+ doc = Person.create
32
+ doc.favorite_pet.build :name=>"Magma"
33
+ doc.send(:prepare_update_command).to_h.should == {
34
+ "$set"=>{'favorite_pet'=>doc.favorite_pet.to_mongo}
35
+ }
36
+ end
37
+
38
+ should "report changed child" do
39
+ doc = Person.new
40
+ doc.favorite_pet.build :name=>"Magma"
41
+ doc.save
42
+ doc.favorite_pet.name = "Ugly"
43
+ doc.send(:prepare_update_command).to_h.should == {
44
+ "$set"=>{"favorite_pet.name"=>"Ugly"}
45
+ }
46
+ end
47
+
48
+ should "report a deleted child" do
49
+ doc = Person.new
50
+ doc.favorite_pet.build :name=>"Magma"
51
+ doc.save
52
+ doc.favorite_pet = nil
53
+ doc.send(:prepare_update_command).to_h.should == {
54
+ "$set"=>{'favorite_pet'=>nil}
55
+ }
56
+ end
57
+
58
+ end
59
+ context "with changes to 'many' embedded associations" do
60
+ should "report a new child" do
61
+ doc = Person.create
62
+ doc.pets.build :name=>"Magma"
63
+ doc.send(:prepare_update_command).to_h.should == {
64
+ :pushes=>{"pets"=>[doc.pets[0].to_mongo]}
65
+ }
66
+ end
67
+
68
+ should "report a changed child" do
69
+ doc = Person.new
70
+ doc.pets.build :name=>"Magma"
71
+ doc.save!
72
+ doc.pets[0].name = "Ugly"
73
+ doc.send(:prepare_update_command).to_h.should == {
74
+ "$set"=>{"pets.0.name"=>"Ugly"}
75
+ }
76
+ end
77
+
78
+ should "report a deleted child" do
79
+ doc = Person.new
80
+ deleted = doc.pets.build :name=>"Magma"
81
+ doc.save!
82
+ doc.pets.pop
83
+ doc.send(:prepare_update_command).to_h.should == {
84
+ :pulls=>{"pets"=>[deleted._id]}
85
+ }
86
+ end
87
+
88
+ should "report a mix of added, modified and deleted" do
89
+ doc = Person.new
90
+ doc.pets.build :name => "Magma"
91
+ deleted = doc.pets.build :name => "Ugly"
92
+ doc.pets.build :name => "Bipolar"
93
+ doc.save!
94
+ doc.pets.reject! {|p|p.name=="Ugly"}
95
+ doc.pets[0].name = "Debris"
96
+ doc.pets.build :name => "Hades"
97
+ doc.send(:prepare_update_command).to_h.should == {
98
+ "$set"=>{"pets.0.name"=>"Debris"},
99
+ :pulls=>{"pets"=>[deleted._id]},
100
+ :pushes=>{"pets"=>[doc.pets[-1].to_mongo]}
101
+ }
102
+ end
103
+
104
+ should "update database selectors after save" do
105
+ doc = Person.new
106
+ doc.pets.build :name=>"Magma"
107
+ doc.pets.build :name=>"Ugly"
108
+ doc.pets.build :name=>"Bipolar"
109
+ doc.save!
110
+ doc.pets.reject! {|p|p.name=="Ugly"}
111
+ doc.save!
112
+ doc.pets.length.should == 2
113
+ doc.pets.each_with_index do |pet,index|
114
+ pet.instance_variable_get("@_database_position").should == index
115
+ end
116
+ end
117
+ end
118
+
119
+ context "with changes to nested one associations" do
120
+ should "report a new grandchild" do
121
+ doc = Person.new
122
+ pet = doc.pets.build :name=>"Magma"
123
+ doc.save!
124
+ pet.favorite_flea.build :name=>"Fleatus"
125
+ doc.send(:prepare_update_command).to_h.should == {
126
+ "$set"=>{"pets.0.favorite_flea"=>pet.favorite_flea.to_mongo}
127
+ }
128
+ end
129
+
130
+ should "report a modified grandchild" do
131
+ doc = Person.new
132
+ pet = doc.pets.build :name=>"Magma"
133
+ flea = pet.favorite_flea.build :name=>"Fleatus"
134
+ doc.save!
135
+ flea.name = "Fleatasia"
136
+ doc.send(:prepare_update_command).to_h.should == {
137
+ "$set"=>{"pets.0.favorite_flea.name"=>"Fleatasia"}
138
+ }
139
+ end
140
+
141
+ should "report a deleted grandchild" do
142
+ doc = Person.new
143
+ pet = doc.pets.build :name=>"Magma"
144
+ pet.favorite_flea.build :name=>"Fleatus"
145
+ doc.save!
146
+ pet.favorite_flea = nil
147
+ doc.send(:prepare_update_command).to_h.should == {
148
+ "$set"=>{"pets.0.favorite_flea"=>nil}
149
+ }
150
+ end
151
+ end
152
+
153
+ context "with changes to nested many associations" do
154
+ should "report a new grandchild" do
155
+ doc = Person.create
156
+ pet = doc.pets.build :name=>"Magma"
157
+ doc.save!
158
+ pet.fleas.build :name=>'Fleatus'
159
+ doc.send(:prepare_update_command).to_h.should == {
160
+ :pushes=>{"pets.0.fleas"=>[pet.fleas[0].to_mongo]}
161
+ }
162
+ end
163
+ should "report a changed grandchild" do
164
+ doc = Person.new
165
+ pet = doc.pets.build :name=>"Magma"
166
+ pet.fleas.build :name=>"Fleatus"
167
+ doc.save!
168
+ pet.fleas[0].name = "Fleatasia"
169
+ doc.send(:prepare_update_command).to_h.should == {
170
+ "$set"=>{"pets.0.fleas.0.name"=>"Fleatasia"}
171
+ }
172
+ end
173
+
174
+ should "report a deleted child" do
175
+ doc = Person.new
176
+ doc.pets.build :name=>"Magma"
177
+ doc.pets[0].fleas.build :name=>"Fleatus"
178
+ doc.save!
179
+ deleted = doc.pets[0].fleas.pop
180
+ doc.send(:prepare_update_command).to_h.should == {
181
+ :pulls=>{"pets.0.fleas"=>[deleted._id]}
182
+ }
183
+ end
184
+
185
+ should "report a mix of added, modified and deleted" do
186
+ doc = Person.new
187
+ pet = doc.pets.build :name=>"Magma"
188
+ pet.fleas.build :name => "Fleatus"
189
+ deleted = pet.fleas.build :name => "Fleatasia"
190
+ pet.fleas.build :name => "Soto"
191
+ doc.save!
192
+ pet.fleas.reject! {|f|f.name=="Fleatasia"}
193
+ pet.fleas[0].name = "Rinsai"
194
+ pet.fleas.build :name => "Dogen"
195
+ doc.send(:prepare_update_command).to_h.should == {
196
+ :pulls=>{"pets.0.fleas"=>[deleted._id]},
197
+ :pushes=>{"pets.0.fleas"=>[doc.pets[0].fleas[-1].to_mongo]},
198
+ "$set"=>{"pets.0.fleas.0.name"=>"Rinsai"}
199
+ }
200
+ end
201
+
202
+ should "update database selectors after save" do
203
+ doc = Person.new
204
+ pet = doc.pets.build :name=>"Magma"
205
+ pet.fleas.build :name=>"Fleatus"
206
+ pet.fleas.build :name=>"Soto"
207
+ pet.fleas.build :name=>"Rinsai"
208
+ doc.save!
209
+ pet.fleas.reject! {|p|p.name=="Soto"}
210
+ doc.save!
211
+ pet.fleas.length.should == 2
212
+ pet.fleas.each_with_index do |flea,index|
213
+ flea.instance_variable_get("@_database_position").should == index
214
+ end
215
+ end
216
+
217
+ end
218
+
219
+ end
220
+ end
221
+ end
@@ -0,0 +1,133 @@
1
+ require "test_helper"
2
+
3
+ class TestUpdateCommand < Test::Unit::TestCase
4
+
5
+ context "UpdateCommand" do
6
+ setup do
7
+ @edoc = EDoc { key :name, String }
8
+ @doc = Doc { key :name, String }
9
+ @doc.many :children, :class=>@edoc
10
+
11
+ @command = MmPartialUpdate::UpdateCommand.new(@doc.new)
12
+ @defaults = {"a_field"=>2, "another_field"=>2}
13
+ @command.set nil, @defaults
14
+ end
15
+
16
+ context "#document_selector" do
17
+ should "be derived from the id of the root document" do
18
+ doc = @doc.new
19
+ edoc = doc.children.build
20
+ MmPartialUpdate::UpdateCommand.new(doc).document_selector.should == {:_id=>doc._id}
21
+ MmPartialUpdate::UpdateCommand.new(edoc).document_selector.should == {:_id=>doc._id}
22
+ end
23
+ end
24
+
25
+ context "#set" do
26
+
27
+ context "with a blank selector and replace = true" do
28
+ should "set the fields hash directly onto $set" do
29
+ @command.set(nil, {"a_different_field"=>3}, :replace=>{})
30
+ @command.to_h["$set"].should == {"a_different_field"=>3}
31
+ end
32
+ end
33
+
34
+ context "with a blank selector" do
35
+ should "merge the fields hash directly into $set" do
36
+ @command.set nil, {"a_different_field"=>3}
37
+ @command.to_h["$set"].should == {"a_different_field"=>3}.merge(@defaults)
38
+ end
39
+ end
40
+
41
+ context "with replace = true" do
42
+ should "replace the key at 'selector' with the fields hash" do
43
+ @command.set "sel", {"this"=>:that}
44
+ @command.set "sel", {"that"=>:this}, :replace=>true
45
+ @command.to_h["$set"]["sel"].should == {"that"=>:this}
46
+ end
47
+ end
48
+
49
+ context "with a selector and replace = false" do
50
+ should "merge each key of fields into $set, namespaced with the selector" do
51
+ @command.set "sel", {"that"=>:this, "the"=>:other}
52
+ @command.to_h["$set"].should == {"sel.that"=>:this, "sel.the"=>:other}.merge(@defaults)
53
+ @command.set "sel", {"that"=>:not_this, "b"=>:c}
54
+ @command.to_h["$set"].should == {"sel.that" => :not_this, "sel.the"=>:other,"sel.b"=>:c}.merge(@defaults)
55
+ end
56
+ end
57
+
58
+ end
59
+
60
+ context "#unset" do
61
+ should "add the selector to the $unset hash" do
62
+ @command.unset "a_field"
63
+ @command.to_h["$unset"].should == {"a_field"=>true}
64
+ end
65
+
66
+ should "nullify a field when :nullify=>true" do
67
+ @command.unset "a_field", :nullify=>true
68
+ @command.to_h["$set"].should == @defaults.merge("a_field"=>nil)
69
+ end
70
+
71
+ end
72
+
73
+ context "#push" do
74
+ should "add the field hash to the $pushAll array for the selector" do
75
+ @command.push "a_collection", {"a"=>:b, "c"=>:d}
76
+ @command.to_h[:pushes].should == {"a_collection" => [{"a"=>:b, "c"=>:d}]}
77
+ end
78
+
79
+ should "append the field hash to the$pushAll array for the selector" do
80
+ @command.push "a_collection", {"a"=>:b, "c"=>:d}
81
+ @command.push "a_collection", {"a"=>:e, "f"=>:g}
82
+ @command.to_h[:pushes].should == {"a_collection" => [{"a"=>:b, "c"=>:d},
83
+ {"a"=>:e, "f"=>:g}]}
84
+ end
85
+ end
86
+
87
+ context "#pull" do
88
+ should "add the document id to the $pull array for the selector" do
89
+ @command.pull "a_collection", "123"
90
+ @command.to_h[:pulls].should == {"a_collection" => ["123"]}
91
+ end
92
+
93
+ should "append the doc id to the $pull array for the selector" do
94
+ @command.pull "a_collection", "123"
95
+ @command.pull "a_collection", "456"
96
+ @command.to_h[:pulls].should == {"a_collection" => ["123","456"]}
97
+ end
98
+ end
99
+
100
+ context "#merge" do
101
+ should "merge the commands hash with an incoming UpdateCommand" do
102
+ another = MmPartialUpdate::UpdateCommand.new(@doc.new)
103
+ another.set "this", {"that"=>"the other"}
104
+ another.unset "me"
105
+ another.push "a", {"a"=>:b}
106
+ another.pull "b", "c"
107
+ @command.merge(another).should == @command.to_h.merge(another.to_h)
108
+ end
109
+ end
110
+
111
+ context "#prepare_mongodb_commands" do
112
+ should "separate operations into sets, pulls and pushes" do
113
+ @command.set "a", {:b=>2}
114
+ @command.set "a.c", {"c"=>"d"}
115
+ @command.push "hi", {:ho=>true}
116
+ @command.push "hi", {:snack=>"yummy"}
117
+ @command.push "ho", {:cheeri=>0}
118
+ @command.pull "zig", "zag"
119
+ db_commands = @command.send(:prepare_mongodb_commands)
120
+ db_commands.length.should == 4
121
+
122
+ db_commands[0].should == {"$set"=>{"a.b"=>2, "a.c.c"=>"d"}.merge(@defaults)}
123
+ db_commands[1].should == {"$pull"=>{"zig"=>{"_id"=>{"$in"=>["zag"]}}}}
124
+ db_commands[2].should == {"$pushAll"=>{"hi"=>[{:ho=>true},{:snack=>"yummy"}]}}
125
+ db_commands[3].should == {"$pushAll"=>{"ho"=>[{:cheeri=>0}]}}
126
+ end
127
+
128
+ end
129
+
130
+
131
+ end
132
+ end
133
+