aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/test/cases/lifecycle_test.rb
blob: 643e94908755c954d62363dc356b74b71a70845e (plain) (blame)
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
242
243
244
require 'cases/helper'
require 'models/topic'
require 'models/developer'
require 'models/reply'
require 'models/minimalistic'
require 'models/comment'

class SpecialDeveloper < Developer; end

class DeveloperObserver < ActiveRecord::Observer
  def calls
    @calls ||= []
  end

  def before_save(developer)
    calls << developer
  end
end

class SalaryChecker < ActiveRecord::Observer
  observe :special_developer
  attr_accessor :last_saved

  def before_save(developer)
    return developer.salary > 80000
  end

  module Implementation
    def after_save(developer)
      self.last_saved = developer
    end
  end
  include Implementation

end

class TopicaAuditor < ActiveRecord::Observer
  observe :topic

  attr_reader :topic

  def after_find(topic)
    @topic = topic
  end
end

class TopicObserver < ActiveRecord::Observer
  attr_reader :topic

  def after_find(topic)
    @topic = topic
  end

  # Create an after_save callback, so a notify_observer hook is created
  # on :topic.
  def after_save(nothing)
  end
end

class MinimalisticObserver < ActiveRecord::Observer
  attr_reader :minimalistic

  def after_find(minimalistic)
    @minimalistic = minimalistic
  end
end

class MultiObserver < ActiveRecord::Observer
  attr_reader :record

  def self.observed_class() [ Topic, Developer ] end

  cattr_reader :last_inherited
  @@last_inherited = nil

  def observed_class_inherited_with_testing(subclass)
    observed_class_inherited_without_testing(subclass)
    @@last_inherited = subclass
  end

  alias_method_chain :observed_class_inherited, :testing

  def after_find(record)
    @record = record
  end
end

class ValidatedComment < Comment
  attr_accessor :callers

  before_validation :record_callers

  after_validation do
    record_callers
  end

  def record_callers
    callers << self.class if callers
  end
end

class ValidatedCommentObserver < ActiveRecord::Observer
  attr_accessor :callers

  def after_validation(model)
    callers << self.class if callers
  end
end


class AroundTopic < Topic
end

class AroundTopicObserver < ActiveRecord::Observer
  observe :around_topic
  def topic_ids
    @topic_ids ||= []
  end

  def around_save(topic)
    topic_ids << topic.id
    yield(topic)
    topic_ids << topic.id
  end
end

class LifecycleTest < ActiveRecord::TestCase
  fixtures :topics, :developers, :minimalistics

  def test_before_destroy
    topic = Topic.find(1)
    assert_difference 'Topic.count', -(1 + topic.replies.size) do
      topic.destroy
    end
  end

  def test_auto_observer
    topic_observer = TopicaAuditor.instance
    assert_nil TopicaAuditor.observed_class
    assert_equal [Topic], TopicaAuditor.instance.observed_classes.to_a

    topic = Topic.find(1)
    assert_equal topic.title, topic_observer.topic.title
  end

  def test_inferred_auto_observer
    topic_observer = TopicObserver.instance
    assert_equal Topic, TopicObserver.observed_class

    topic = Topic.find(1)
    assert_equal topic.title, topic_observer.topic.title
  end

  def test_observing_two_classes
    multi_observer = MultiObserver.instance

    topic = Topic.find(1)
    assert_equal topic.title, multi_observer.record.title

    developer = Developer.find(1)
    assert_equal developer.name, multi_observer.record.name
  end

  def test_observing_subclasses
    multi_observer = MultiObserver.instance

    developer = SpecialDeveloper.find(1)
    assert_equal developer.name, multi_observer.record.name

    klass = Class.new(Developer)
    assert_equal klass, multi_observer.last_inherited

    developer = klass.find(1)
    assert_equal developer.name, multi_observer.record.name
  end

  def test_after_find_can_be_observed_when_its_not_defined_on_the_model
    observer = MinimalisticObserver.instance
    assert_equal Minimalistic, MinimalisticObserver.observed_class

    minimalistic = Minimalistic.find(1)
    assert_equal minimalistic, observer.minimalistic
  end

  def test_after_find_can_be_observed_when_its_defined_on_the_model
    observer = TopicObserver.instance
    assert_equal Topic, TopicObserver.observed_class

    topic = Topic.find(1)
    assert_equal topic, observer.topic
  end

  def test_invalid_observer
    assert_raise(ArgumentError) { Topic.observers = Object.new; Topic.instantiate_observers }
  end

  test "model callbacks fire before observers are notified" do
    callers = []

    comment = ValidatedComment.new
    comment.callers = ValidatedCommentObserver.instance.callers = callers

    comment.valid?
    assert_equal [ValidatedComment, ValidatedComment, ValidatedCommentObserver], callers,
      "model callbacks did not fire before observers were notified"
  end

  test "able to save developer" do
    SalaryChecker.instance # activate
    developer = SpecialDeveloper.new :name => 'Roger', :salary => 100000
    assert developer.save, "developer with normal salary failed to save"
  end

  test "unable to save developer with low salary" do
    SalaryChecker.instance # activate
    developer = SpecialDeveloper.new :name => 'Rookie', :salary => 50000
    assert !developer.save, "allowed to save a developer with too low salary"
  end

  test "able to call methods defined with included module" do # https://rails.lighthouseapp.com/projects/8994/tickets/6065-activerecordobserver-is-not-aware-of-method-added-by-including-modules
    SalaryChecker.instance # activate
    developer = SpecialDeveloper.create! :name => 'Roger', :salary => 100000
    assert_equal developer, SalaryChecker.instance.last_saved
  end

  test "around filter from observer should accept block" do
    observer = AroundTopicObserver.instance
    topic = AroundTopic.new
    topic.save
    assert_nil observer.topic_ids.first
    assert_not_nil observer.topic_ids.last
  end

  def test_observer_is_called_once
    observer = DeveloperObserver.instance # activate
    observer.calls.clear

    developer = Developer.create! :name => 'Ancestor', :salary => 100000
    special_developer = SpecialDeveloper.create! :name => 'Descendent', :salary => 100000

    assert_equal [developer, special_developer], observer.calls
  end

end