phurni-is_less_paranoid 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|