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.
@@ -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