phurni-is_less_paranoid 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- data/IS_PARANOID_CHANGELOG +31 -0
- data/IS_PARANOID_README.textile +103 -0
- data/MIT-LICENSE +19 -0
- data/README.textile +220 -0
- data/Rakefile +24 -0
- data/VERSION.yml +4 -0
- data/init.rb +1 -0
- data/lib/is_less_paranoid.rb +304 -0
- data/rails/init.rb +2 -0
- data/reactive/init.rb +3 -0
- data/spec/database.yml +3 -0
- data/spec/is_less_paranoid_spec.rb +164 -0
- data/spec/is_paranoid_spec.rb +277 -0
- data/spec/lp_models.rb +28 -0
- data/spec/models.rb +94 -0
- data/spec/schema.rb +87 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +14 -0
- metadata +76 -0
data/rails/init.rb
ADDED
data/reactive/init.rb
ADDED
data/spec/database.yml
ADDED
@@ -0,0 +1,164 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
+
require File.expand_path(File.dirname(__FILE__) + '/lp_models')
|
3
|
+
|
4
|
+
describe IsLessParanoid do
|
5
|
+
before(:each) do
|
6
|
+
Company.delete_all
|
7
|
+
Contact.delete_all
|
8
|
+
Address.delete_all
|
9
|
+
Project.delete_all
|
10
|
+
|
11
|
+
@world = Company.create(:name => 'World')
|
12
|
+
@sly = @world.contacts.create(:firstname => 'Sly')
|
13
|
+
@george = @world.contacts.create(:firstname => 'George')
|
14
|
+
@big = Project.create(:company => @world, :manager => @sly)
|
15
|
+
end
|
16
|
+
|
17
|
+
describe 'clone class' do
|
18
|
+
it "should have the table name of the original class" do
|
19
|
+
CompanyAlive.table_name.should == 'companies'
|
20
|
+
end
|
21
|
+
|
22
|
+
it "should contain class methods of original class" do
|
23
|
+
Contact.get_my_name.should == 'Contact'
|
24
|
+
lambda {
|
25
|
+
ContactAlive.get_my_name.should == 'ContactAlive'
|
26
|
+
}.should_not raise_error(NoMethodError)
|
27
|
+
end
|
28
|
+
|
29
|
+
it "should store the original class name in STI type field" do
|
30
|
+
VipContact.create(:firstname => 'Babushka')
|
31
|
+
VipContactAlive.create(:firstname => 'Schwarzie')
|
32
|
+
VipContact.all.all? {|contact| contact.type == 'VipContact'}.should be_true
|
33
|
+
end
|
34
|
+
|
35
|
+
it "should equals record from the original class" do
|
36
|
+
Contact.first.should == ContactAlive.first
|
37
|
+
ContactAlive.first.should == Contact.first
|
38
|
+
end
|
39
|
+
|
40
|
+
it "should not equals record from another class" do
|
41
|
+
ContactAlive.first.should_not == Company.first
|
42
|
+
end
|
43
|
+
|
44
|
+
describe 'subclassing' do
|
45
|
+
it "should not create a derivative clone class" do
|
46
|
+
lambda {
|
47
|
+
class VeryVipContact < VipContactAlive
|
48
|
+
end
|
49
|
+
VeryVipContactAlive
|
50
|
+
}.should raise_error(NameError)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
describe 'destroying' do
|
56
|
+
it "should mark the record deleted" do
|
57
|
+
lambda{
|
58
|
+
Contact.destroy(@sly.id)
|
59
|
+
}.should_not change(Contact, :count)
|
60
|
+
Contact.count_with_destroyed.should == 2
|
61
|
+
end
|
62
|
+
|
63
|
+
it "should soft-delete a record" do
|
64
|
+
lambda{
|
65
|
+
ContactAlive.destroy(@sly.id)
|
66
|
+
}.should change(ContactAlive, :count).from(2).to(1)
|
67
|
+
Contact.count_with_destroyed.should == 2
|
68
|
+
end
|
69
|
+
|
70
|
+
it "should soft-delete matching items on Model.destroy_all" do
|
71
|
+
lambda{
|
72
|
+
ContactAlive.destroy_all("company_id = #{@world.id}")
|
73
|
+
}.should change(ContactAlive, :count).from(2).to(0)
|
74
|
+
ContactAlive.count_with_destroyed.should == 2
|
75
|
+
end
|
76
|
+
|
77
|
+
describe 'related models' do
|
78
|
+
it "should still show up in the relationship to the owner" do
|
79
|
+
@world.contacts.size.should == 2
|
80
|
+
@george.destroy
|
81
|
+
@world.contacts.size.should == 1
|
82
|
+
end
|
83
|
+
|
84
|
+
it "should soft-delete on dependent destroys" do
|
85
|
+
lambda{
|
86
|
+
@world.destroy
|
87
|
+
}.should change(ContactAlive, :count).from(2).to(0)
|
88
|
+
ContactAlive.count_with_destroyed.should == 2
|
89
|
+
end
|
90
|
+
|
91
|
+
it "should delete the alive association but preserve the normal one" do
|
92
|
+
@world.contacts.first.destroy
|
93
|
+
ContactAlive.count.should == 1
|
94
|
+
Contact.count.should == 2
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
describe 'subclassing' do
|
101
|
+
it "should create a clone class" do
|
102
|
+
lambda {
|
103
|
+
VipContactAlive
|
104
|
+
}.should_not raise_error(NameError)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
describe 'finding destroyed models' do
|
109
|
+
it "should be able to find destroyed items in the original class" do
|
110
|
+
@sly.destroy
|
111
|
+
Contact.find(:first, :conditions => {:firstname => 'Sly'}).should_not be_blank
|
112
|
+
end
|
113
|
+
|
114
|
+
it "should not be able to find destroyed items in the clone class" do
|
115
|
+
@sly.destroy
|
116
|
+
ContactAlive.find(:first, :conditions => {:firstname => 'Sly'}).should be_blank
|
117
|
+
ContactAlive.first_with_destroyed(:conditions => {:firstname => 'Sly'}).should_not be_blank
|
118
|
+
end
|
119
|
+
|
120
|
+
it "should be able to find destroyed items in the clone class with find_with_destroyed" do
|
121
|
+
@sly.destroy
|
122
|
+
ContactAlive.first_with_destroyed(:conditions => {:firstname => 'Sly'}).should_not be_blank
|
123
|
+
end
|
124
|
+
|
125
|
+
it "should show destroyed models via :include" do
|
126
|
+
Company.first(:conditions => {:name => 'World'}, :include => :contacts).contacts.size.should == 2
|
127
|
+
@sly.destroy
|
128
|
+
company = Company.first(:conditions => {:name => 'World'}, :include => :contacts)
|
129
|
+
# ensure that we're using the preload and not loading it via a find
|
130
|
+
Contact.should_not_receive(:find)
|
131
|
+
company.contacts.size.should == 2
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
describe 'calculations' do
|
136
|
+
it "should have no count difference of destroyed items on the original class" do
|
137
|
+
@sly.destroy
|
138
|
+
@george.destroy
|
139
|
+
Contact.count.should == 2
|
140
|
+
end
|
141
|
+
|
142
|
+
it "should have a proper count inclusively and exclusively of destroyed items on the clone class" do
|
143
|
+
@sly.destroy
|
144
|
+
@george.destroy
|
145
|
+
ContactAlive.count.should == 0
|
146
|
+
ContactAlive.count_with_destroyed.should == 2
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
describe 'deletion on clone class' do
|
151
|
+
it "should actually remove records on #delete_all" do
|
152
|
+
lambda{
|
153
|
+
ContactAlive.delete_all
|
154
|
+
}.should change(ContactAlive, :count_with_destroyed).from(2).to(0)
|
155
|
+
end
|
156
|
+
|
157
|
+
it "should actually remove records on #delete" do
|
158
|
+
lambda{
|
159
|
+
ContactAlive.first.delete
|
160
|
+
}.should change(ContactAlive, :count_with_destroyed).from(2).to(1)
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
end
|
@@ -0,0 +1,277 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
+
require File.expand_path(File.dirname(__FILE__) + '/models')
|
3
|
+
|
4
|
+
LUKE = 'Luke Skywalker'
|
5
|
+
|
6
|
+
describe IsLessParanoid do
|
7
|
+
before(:each) do
|
8
|
+
Android.delete_all
|
9
|
+
Person.delete_all
|
10
|
+
Component.delete_all
|
11
|
+
|
12
|
+
@luke = Person.create(:name => LUKE)
|
13
|
+
@r2d2 = Android.create(:name => 'R2D2', :owner_id => @luke.id)
|
14
|
+
@c3p0 = Android.create(:name => 'C3P0', :owner_id => @luke.id)
|
15
|
+
|
16
|
+
@r2d2.components.create(:name => 'Rotors')
|
17
|
+
|
18
|
+
@r2d2.memories.create(:name => 'A pretty sunset')
|
19
|
+
@c3p0.sticker = Sticker.create(:name => 'OMG, PONIES!')
|
20
|
+
end
|
21
|
+
|
22
|
+
describe 'non-is_paranoid models' do
|
23
|
+
it "should destroy as normal" do
|
24
|
+
lambda{
|
25
|
+
@luke.destroy
|
26
|
+
}.should change(Person, :count).by(-1)
|
27
|
+
|
28
|
+
lambda{
|
29
|
+
Person.count_with_destroyed
|
30
|
+
}.should raise_error(NoMethodError)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
describe 'destroying' do
|
35
|
+
it "should soft-delete a record" do
|
36
|
+
lambda{
|
37
|
+
Android.destroy(@r2d2.id)
|
38
|
+
}.should change(Android, :count).from(2).to(1)
|
39
|
+
Android.count_with_destroyed.should == 2
|
40
|
+
end
|
41
|
+
|
42
|
+
it "should not hit update/save related callbacks" do
|
43
|
+
lambda{
|
44
|
+
Android.first.update_attribute(:name, 'Robocop')
|
45
|
+
}.should raise_error
|
46
|
+
|
47
|
+
lambda{
|
48
|
+
Android.first.destroy
|
49
|
+
}.should_not raise_error
|
50
|
+
end
|
51
|
+
|
52
|
+
it "should soft-delete matching items on Model.destroy_all" do
|
53
|
+
lambda{
|
54
|
+
Android.destroy_all("owner_id = #{@luke.id}")
|
55
|
+
}.should change(Android, :count).from(2).to(0)
|
56
|
+
Android.count_with_destroyed.should == 2
|
57
|
+
end
|
58
|
+
|
59
|
+
describe 'related models' do
|
60
|
+
it "should no longer show up in the relationship to the owner" do
|
61
|
+
@luke.androids.size.should == 2
|
62
|
+
@r2d2.destroy
|
63
|
+
@luke.androids.size.should == 1
|
64
|
+
end
|
65
|
+
|
66
|
+
it "should soft-delete on dependent destroys" do
|
67
|
+
lambda{
|
68
|
+
@luke.destroy
|
69
|
+
}.should change(Android, :count).from(2).to(0)
|
70
|
+
Android.count_with_destroyed.should == 2
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
describe 'finding destroyed models' do
|
76
|
+
it "should be able to find destroyed items via #find_with_destroyed" do
|
77
|
+
@r2d2.destroy
|
78
|
+
Android.find(:first, :conditions => {:name => 'R2D2'}).should be_blank
|
79
|
+
Android.first_with_destroyed(:conditions => {:name => 'R2D2'}).should_not be_blank
|
80
|
+
end
|
81
|
+
|
82
|
+
it "should be able to find only destroyed items via #find_destroyed_only" do
|
83
|
+
@r2d2.destroy
|
84
|
+
Android.all_destroyed_only.size.should == 1
|
85
|
+
Android.first_destroyed_only.should == @r2d2
|
86
|
+
end
|
87
|
+
|
88
|
+
it "should not show destroyed models via :include" do
|
89
|
+
Person.first(:conditions => {:name => LUKE}, :include => :androids).androids.size.should == 2
|
90
|
+
@r2d2.destroy
|
91
|
+
person = Person.first(:conditions => {:name => LUKE}, :include => :androids)
|
92
|
+
# ensure that we're using the preload and not loading it via a find
|
93
|
+
Android.should_not_receive(:find)
|
94
|
+
person.androids.size.should == 1
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
describe 'calculations' do
|
99
|
+
it "should have a proper count inclusively and exclusively of destroyed items" do
|
100
|
+
@r2d2.destroy
|
101
|
+
@c3p0.destroy
|
102
|
+
Android.count.should == 0
|
103
|
+
Android.count_with_destroyed.should == 2
|
104
|
+
end
|
105
|
+
|
106
|
+
it "should respond to various calculations" do
|
107
|
+
@r2d2.destroy
|
108
|
+
Android.sum('id').should == @c3p0.id
|
109
|
+
Android.sum_with_destroyed('id').should == @r2d2.id + @c3p0.id
|
110
|
+
Android.average_with_destroyed('id').should == (@r2d2.id + @c3p0.id) / 2.0
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
describe 'deletion' do
|
115
|
+
it "should actually remove records on #delete_all" do
|
116
|
+
lambda{
|
117
|
+
Android.delete_all
|
118
|
+
}.should change(Android, :count_with_destroyed).from(2).to(0)
|
119
|
+
end
|
120
|
+
|
121
|
+
it "should actually remove records on #delete" do
|
122
|
+
lambda{
|
123
|
+
Android.first.delete
|
124
|
+
}.should change(Android, :count_with_destroyed).from(2).to(1)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
describe 'restore' do
|
129
|
+
it "should allow restoring soft-deleted items" do
|
130
|
+
@r2d2.destroy
|
131
|
+
lambda{
|
132
|
+
@r2d2.restore
|
133
|
+
}.should change(Android, :count).from(1).to(2)
|
134
|
+
end
|
135
|
+
|
136
|
+
it "should not hit update/save related callbacks" do
|
137
|
+
@r2d2.destroy
|
138
|
+
|
139
|
+
lambda{
|
140
|
+
@r2d2.update_attribute(:name, 'Robocop')
|
141
|
+
}.should raise_error
|
142
|
+
|
143
|
+
lambda{
|
144
|
+
@r2d2.restore
|
145
|
+
}.should_not raise_error
|
146
|
+
end
|
147
|
+
|
148
|
+
it "should restore dependent models when being restored by default" do
|
149
|
+
@r2d2.destroy
|
150
|
+
lambda{
|
151
|
+
@r2d2.restore
|
152
|
+
}.should change(Component, :count).from(0).to(1)
|
153
|
+
end
|
154
|
+
|
155
|
+
it "should provide the option to not restore dependent models" do
|
156
|
+
@r2d2.destroy
|
157
|
+
lambda{
|
158
|
+
@r2d2.restore(:include_destroyed_dependents => false)
|
159
|
+
}.should_not change(Component, :count)
|
160
|
+
end
|
161
|
+
|
162
|
+
it "should restore parent and child models specified via :include" do
|
163
|
+
sub_component = SubComponent.create(:name => 'part', :component_id => @r2d2.components.first.id)
|
164
|
+
@r2d2.destroy
|
165
|
+
SubComponent.first(:conditions => {:id => sub_component.id}).should be_nil
|
166
|
+
@r2d2.components.first.restore(:include => [:android, :sub_components])
|
167
|
+
SubComponent.first(:conditions => {:id => sub_component.id}).should_not be_nil
|
168
|
+
Android.find(@r2d2.id).should_not be_nil
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
describe 'validations' do
|
173
|
+
it "should not ignore destroyed items in validation checks unless scoped" do
|
174
|
+
# Androids are not validates_uniqueness_of scoped
|
175
|
+
@r2d2.destroy
|
176
|
+
lambda{
|
177
|
+
Android.create!(:name => 'R2D2')
|
178
|
+
}.should raise_error(ActiveRecord::RecordInvalid)
|
179
|
+
|
180
|
+
lambda{
|
181
|
+
# creating shouldn't raise an error
|
182
|
+
another_r2d2 = AndroidWithScopedUniqueness.create!(:name => 'R2D2')
|
183
|
+
# neither should destroying the second incarnation since the
|
184
|
+
# validates_uniqueness_of is only applied on create
|
185
|
+
another_r2d2.destroy
|
186
|
+
}.should_not raise_error
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
describe '(parent)_with_destroyed' do
|
191
|
+
it "should be able to access destroyed parents" do
|
192
|
+
# Memory is has_many with a non-default primary key
|
193
|
+
# Sticker is a has_one with a default primary key
|
194
|
+
[Memory, Sticker].each do |klass|
|
195
|
+
instance = klass.last
|
196
|
+
parent = instance.android
|
197
|
+
instance.android.destroy
|
198
|
+
|
199
|
+
# reload so the model doesn't remember the parent
|
200
|
+
instance.reload
|
201
|
+
instance.android.should == nil
|
202
|
+
instance.android_with_destroyed.should == parent
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
it "should return nil if no destroyed parent exists" do
|
207
|
+
sticker = Sticker.new(:name => 'Rainbows')
|
208
|
+
# because the default relationship works this way, i.e.
|
209
|
+
sticker.android.should == nil
|
210
|
+
sticker.android_with_destroyed.should == nil
|
211
|
+
end
|
212
|
+
|
213
|
+
it "should not break method_missing's defined before the is_paranoid call" do
|
214
|
+
# we've defined a method_missing on Sticker
|
215
|
+
# that changes the sticker name.
|
216
|
+
sticker = Sticker.new(:name => "Ponies!")
|
217
|
+
lambda{
|
218
|
+
sticker.some_crazy_method_that_we_certainly_do_not_respond_to
|
219
|
+
}.should change(sticker, :name).to(Sticker::MM_NAME)
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
describe 'alternate fields and field values' do
|
224
|
+
it "should properly function for boolean values" do
|
225
|
+
# ninjas are invisible by default. not being ninjas, we can only
|
226
|
+
# find those that are visible
|
227
|
+
ninja = Ninja.create(:name => 'Esteban', :visible => true)
|
228
|
+
ninja.vanish # aliased to destroy
|
229
|
+
Ninja.first.should be_blank
|
230
|
+
Ninja.find_with_destroyed(:first).should == ninja
|
231
|
+
Ninja.count.should == 0
|
232
|
+
|
233
|
+
# we're only interested in pirates who are alive by default
|
234
|
+
pirate = Pirate.create(:name => 'Reginald')
|
235
|
+
pirate.destroy
|
236
|
+
Pirate.first.should be_blank
|
237
|
+
Pirate.find_with_destroyed(:first).should == pirate
|
238
|
+
Pirate.count.should == 0
|
239
|
+
|
240
|
+
# we're only interested in pirates who are dead by default.
|
241
|
+
# zombie pirates ftw!
|
242
|
+
DeadPirate.first.id.should == pirate.id
|
243
|
+
lambda{
|
244
|
+
DeadPirate.first.destroy
|
245
|
+
}.should change(Pirate, :count).from(0).to(1)
|
246
|
+
DeadPirate.count.should == 0
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
describe 'after_destroy and before_destroy callbacks' do
|
251
|
+
it "should rollback if before_destroy fails" do
|
252
|
+
edward = UndestroyablePirate.create(:name => 'Edward')
|
253
|
+
lambda{
|
254
|
+
edward.destroy
|
255
|
+
}.should_not change(UndestroyablePirate, :count)
|
256
|
+
end
|
257
|
+
|
258
|
+
it "should rollback if after_destroy raises an error" do
|
259
|
+
raul = RandomPirate.create(:name => 'Raul')
|
260
|
+
lambda{
|
261
|
+
begin
|
262
|
+
raul.destroy
|
263
|
+
rescue => ex
|
264
|
+
ex.message.should == 'after_destroy works'
|
265
|
+
end
|
266
|
+
}.should_not change(RandomPirate, :count)
|
267
|
+
end
|
268
|
+
|
269
|
+
it "should handle callbacks normally assuming no failures are encountered" do
|
270
|
+
component = Component.first
|
271
|
+
lambda{
|
272
|
+
component.destroy
|
273
|
+
}.should change(component, :name).to(Component::NEW_NAME)
|
274
|
+
end
|
275
|
+
|
276
|
+
end
|
277
|
+
end
|