phurni-is_less_paranoid 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,2 @@
1
+ # Include hook code here
2
+ require 'is_less_paranoid'
@@ -0,0 +1,3 @@
1
+ Reactive::Initializer.after_init do
2
+ require 'is_less_paranoid'
3
+ end
@@ -0,0 +1,3 @@
1
+ test:
2
+ :adapter: sqlite3
3
+ :dbfile: is_paranoid.db
@@ -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