/
is_paranoid_spec.rb
241 lines (206 loc) · 7.17 KB
/
is_paranoid_spec.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
require File.expand_path(File.dirname(__FILE__) + '/models')
describe IsParanoid do
before(:each) do
Android.delete_all
Person.delete_all
Component.delete_all
@luke = Person.create(:name => 'Luke Skywalker')
@r2d2 = Android.create(:name => 'R2D2', :owner_id => @luke.id)
@c3p0 = Android.create(:name => 'C3P0', :owner_id => @luke.id)
@r2d2.components.create(:name => 'Rotors')
@r2d2.memories.create(:name => 'A pretty sunset')
@c3p0.sticker = Sticker.create(:name => 'OMG, PONIES!')
end
describe 'non-is_paranoid models' do
it "should destroy as normal" do
lambda{
@luke.destroy
}.should change(Person, :count).by(-1)
lambda{
Person.count_with_destroyed
}.should raise_error(NoMethodError)
end
end
describe 'destroying' do
it "should soft-delete a record" do
lambda{
Android.destroy(@r2d2.id)
}.should change(Android, :count).from(2).to(1)
Android.count_with_destroyed.should == 2
end
it "should not hit update/save related callbacks" do
lambda{
Android.first.update_attribute(:name, 'Robocop')
}.should raise_error
lambda{
Android.first.destroy
}.should_not raise_error
end
it "should soft-delete matching items on Model.destroy_all" do
lambda{
Android.destroy_all("owner_id = #{@luke.id}")
}.should change(Android, :count).from(2).to(0)
Android.count_with_destroyed.should == 2
end
describe 'related models' do
it "should no longer show up in the relationship to the owner" do
@luke.androids.size.should == 2
@r2d2.destroy
@luke.androids.size.should == 1
end
it "should soft-delete on dependent destroys" do
lambda{
@luke.destroy
}.should change(Android, :count).from(2).to(0)
Android.count_with_destroyed.should == 2
end
end
end
describe 'finding destroyed models' do
it "should be able to find destroyed items via #find_with_destroyed" do
@r2d2.destroy
Android.find(:first, :conditions => {:name => 'R2D2'}).should be_blank
Android.first_with_destroyed(:conditions => {:name => 'R2D2'}).should_not be_blank
end
it "should be able to find only destroyed items via #find_destroyed_only" do
@r2d2.destroy
Android.all_destroyed_only.size.should == 1
Android.first_destroyed_only.should == @r2d2
end
end
describe 'calculations' do
it "should have a proper count inclusively and exclusively of destroyed items" do
@r2d2.destroy
@c3p0.destroy
Android.count.should == 0
Android.count_with_destroyed.should == 2
end
it "should respond to various calculations" do
@r2d2.destroy
Android.sum('id').should == @c3p0.id
Android.sum_with_destroyed('id').should == @r2d2.id + @c3p0.id
Android.average_with_destroyed('id').should == (@r2d2.id + @c3p0.id) / 2.0
end
end
describe 'deletion' do
it "should actually remove records on #delete_all" do
lambda{
Android.delete_all
}.should change(Android, :count_with_destroyed).from(2).to(0)
end
it "should actually remove records on #delete" do
lambda{
Android.first.delete
}.should change(Android, :count_with_destroyed).from(2).to(1)
end
end
describe 'restore' do
it "should allow restoring soft-deleted items" do
@r2d2.destroy
lambda{
@r2d2.restore
}.should change(Android, :count).from(1).to(2)
end
it "should not hit update/save related callbacks" do
@r2d2.destroy
lambda{
@r2d2.update_attribute(:name, 'Robocop')
}.should raise_error
lambda{
@r2d2.restore
}.should_not raise_error
end
it "should restore dependent models when being restored by default" do
@r2d2.destroy
lambda{
@r2d2.restore
}.should change(Component, :count).from(0).to(1)
end
it "should provide the option to not restore dependent models" do
@r2d2.destroy
lambda{
@r2d2.restore(:include_destroyed_dependents => false)
}.should_not change(Component, :count)
end
end
describe 'validations' do
it "should not ignore destroyed items in validation checks unless scoped" do
# Androids are not validates_uniqueness_of scoped
@r2d2.destroy
lambda{
Android.create!(:name => 'R2D2')
}.should raise_error(ActiveRecord::RecordInvalid)
lambda{
# creating shouldn't raise an error
another_r2d2 = AndroidWithScopedUniqueness.create!(:name => 'R2D2')
# neither should destroying the second incarnation since the
# validates_uniqueness_of is only applied on create
another_r2d2.destroy
}.should_not raise_error
end
end
describe 'accessing destroyed parent models' do
it "should be able to access destroyed parents via parent_with_destroyed" do
# Memory is has_many with a non-default primary key
# Sticker is a has_one with a default primary key
[Memory, Sticker].each do |klass|
instance = klass.last
parent = instance.android
instance.android.destroy
# reload so the model doesn't remember the parent
instance.reload
instance.android.should == nil
instance.android_with_destroyed.should == parent
end
end
end
describe 'alternate fields and field values' do
it "should properly function for boolean values" do
# ninjas are invisible by default. not being ninjas, we can only
# find those that are visible
ninja = Ninja.create(:name => 'Esteban', :visible => true)
ninja.vanish # aliased to destroy
Ninja.first.should be_blank
Ninja.find_with_destroyed(:first).should == ninja
Ninja.count.should == 0
# we're only interested in pirates who are alive by default
pirate = Pirate.create(:name => 'Reginald')
pirate.destroy
Pirate.first.should be_blank
Pirate.find_with_destroyed(:first).should == pirate
Pirate.count.should == 0
# we're only interested in pirates who are dead by default.
# zombie pirates ftw!
DeadPirate.first.id.should == pirate.id
lambda{
DeadPirate.first.destroy
}.should change(Pirate, :count).from(0).to(1)
DeadPirate.count.should == 0
end
end
describe 'after_destroy and before_destroy callbacks' do
it "should rollback if before_destroy fails" do
edward = UndestroyablePirate.create(:name => 'Edward')
lambda{
edward.destroy
}.should_not change(UndestroyablePirate, :count)
end
it "should rollback if after_destroy raises an error" do
raul = RandomPirate.create(:name => 'Raul')
lambda{
begin
raul.destroy
rescue => ex
ex.message.should == 'after_destroy works'
end
}.should_not change(RandomPirate, :count)
end
it "should handle callbacks normally assuming no failures are encountered" do
component = Component.first
lambda{
component.destroy
}.should change(component, :name).to(Component::NEW_NAME)
end
end
end