summaryrefslogtreecommitdiff
path: root/spec/functional/notifications_spec.rb
blob: d448ea175dd019d4b769943f699cf4a88b1d351f (plain)
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
require 'spec_helper'
require 'chef/recipe'


# The goal of these tests is to make sure that loading resources from a file creates the necessary notifications.
# Then once converge has started, both immediate and delayed notifications are called as the resources are converged.
# We want to do this WITHOUT actually converging any resources - we don't want to take time changing the system,
# we just want to make sure the run_context, the notification DSL and the converge hooks are working together
# to perform notifications.

# This test is extremely fragile since it mocks MANY different systems at once - any of them changes, this test
# breaks
describe "Notifications" do

  # We always pretend we are on OSx because that has a specific provider (HomebrewProvider) so it
  # tests the translation from Provider => HomebrewProvider
  let(:node) {
    n = Chef::Node.new
    n.override[:os] = "darwin"
    n
  }
  let(:cookbook_collection) { double("Chef::CookbookCollection").as_null_object }
  let(:events) { double("Chef::EventDispatch::Dispatcher").as_null_object }
  let(:run_context) { Chef::RunContext.new(node, cookbook_collection, events) }
  let(:recipe) { Chef::Recipe.new("notif", "test", run_context) }
  let(:runner) { Chef::Runner.new(run_context) }

  before do
    # By default, every provider will do nothing
    p = Chef::Provider.new(nil, run_context)
    allow_any_instance_of(Chef::Resource).to receive(:provider_for_action).and_return(p)
    allow(p).to receive(:run_action)
  end

  it "should subscribe from one resource to another" do
    log_resource = recipe.declare_resource(:log, "subscribed-log") do
      message "This is a log message"
      action :nothing
      subscribes :write, "package[vim]", :immediately
    end

    package_resource = recipe.declare_resource(:package, "vim") do
      action :install
    end

    expect(log_resource).to receive(:run_action).with(:nothing, nil, nil).and_call_original

    expect(package_resource).to receive(:run_action).with(:install, nil, nil).and_call_original
    update_action(package_resource)

    expect(log_resource).to receive(:run_action).with(:write, :immediate, package_resource).and_call_original

    runner.converge
  end

  it "should notify from one resource to another immediately" do
    log_resource = recipe.declare_resource(:log, "log") do
      message "This is a log message"
      action :write
      notifies :install, "package[vim]", :immediately
    end

    package_resource = recipe.declare_resource(:package, "vim") do
      action :nothing
    end

    expect(log_resource).to receive(:run_action).with(:write, nil, nil).and_call_original
    update_action(log_resource)

    expect(package_resource).to receive(:run_action).with(:install, :immediate, log_resource).ordered.and_call_original

    expect(package_resource).to receive(:run_action).with(:nothing, nil, nil).ordered.and_call_original

    runner.converge
  end

  it "should notify from one resource to another before" do
    log_resource = recipe.declare_resource(:log, "log") do
      message "This is a log message"
      action :write
      notifies :install, "package[vim]", :before
    end
    update_action(log_resource, 2)

    package_resource = recipe.declare_resource(:package, "vim") do
      action :nothing
    end

    actions = []
    [ log_resource, package_resource ].each do |resource|
      allow(resource).to receive(:run_action).and_wrap_original do |m, action, notification_type, notifying_resource|
        actions << { resource: resource.to_s, action: action }
        actions[-1][:why_run] = Chef::Config[:why_run] if Chef::Config[:why_run]
        actions[-1][:notification_type] = notification_type if notification_type
        actions[-1][:notifying_resource] = notifying_resource.to_s if notifying_resource
        m.call(action, notification_type, notifying_resource)
      end
    end

    runner.converge

    expect(actions).to eq [
      # First it runs why-run to check if the resource would update
      { resource: log_resource.to_s,     action: :write,   why_run: true },
      # Then it runs the before action
      { resource: package_resource.to_s, action: :install, notification_type: :before, notifying_resource: log_resource.to_s },
      # Then it runs the actual action
      { resource: log_resource.to_s,     action: :write },
      { resource: package_resource.to_s, action: :nothing },
    ]
  end

  it "should not notify from one resource to another before if the resource is not updated" do
    log_resource = recipe.declare_resource(:log, "log") do
      message "This is a log message"
      action :write
      notifies :install, "package[vim]", :before
    end

    package_resource = recipe.declare_resource(:package, "vim") do
      action :nothing
    end

    actions = []
    [ log_resource, package_resource ].each do |resource|
      allow(resource).to receive(:run_action).and_wrap_original do |m, action, notification_type, notifying_resource|
        actions << { resource: resource.to_s, action: action }
        actions[-1][:why_run] = Chef::Config[:why_run] if Chef::Config[:why_run]
        actions[-1][:notification_type] = notification_type if notification_type
        actions[-1][:notifying_resource] = notifying_resource.to_s if notifying_resource
        m.call(action, notification_type, notifying_resource)
      end
    end

    runner.converge

    expect(actions).to eq [
      # First it runs why-run to check if the resource would update
      { resource: log_resource.to_s,     action: :write,   why_run: true },
      # Then it does NOT run the before action
      # Then it runs the actual action
      { resource: log_resource.to_s,     action: :write },
      { resource: package_resource.to_s, action: :nothing },
    ]
  end

  it "should notify from one resource to another delayed" do
    log_resource = recipe.declare_resource(:log, "log") do
      message "This is a log message"
      action :write
      notifies :install, "package[vim]", :delayed
    end

    package_resource = recipe.declare_resource(:package, "vim") do
      action :nothing
    end

    expect(log_resource).to receive(:run_action).with(:write, nil, nil).and_call_original
    update_action(log_resource)

    expect(package_resource).to receive(:run_action).with(:nothing, nil, nil).ordered.and_call_original

    expect(package_resource).to receive(:run_action).with(:install, :delayed, nil).ordered.and_call_original

    runner.converge
  end

  describe "when one resource is defined lazily" do

    it "subscribes to a resource defined in a ruby block" do
      r = recipe
      t = self
      ruby_block = recipe.declare_resource(:ruby_block, "rblock") do
        block do
          log_resource = r.declare_resource(:log, "log") do
            message "This is a log message"
            action :write
          end
          t.expect(log_resource).to t.receive(:run_action).with(:write, nil, nil).and_call_original
          t.update_action(log_resource)
        end
      end

      package_resource = recipe.declare_resource(:package, "vim") do
        action :nothing
        subscribes :install, "log[log]", :delayed
      end

      # RubyBlock needs to be able to run for our lazy examples to work - and it alone cannot affect the system
      expect(ruby_block).to receive(:provider_for_action).and_call_original

      expect(package_resource).to receive(:run_action).with(:nothing, nil, nil).ordered.and_call_original

      expect(package_resource).to receive(:run_action).with(:install, :delayed, nil).ordered.and_call_original

      runner.converge
    end

    it "notifies from inside a ruby_block to a resource defined outside" do
      r = recipe
      t = self
      ruby_block = recipe.declare_resource(:ruby_block, "rblock") do
        block do
          log_resource = r.declare_resource(:log, "log") do
            message "This is a log message"
            action :write
            notifies :install, "package[vim]", :immediately
          end
          t.expect(log_resource).to t.receive(:run_action).with(:write, nil, nil).and_call_original
          t.update_action(log_resource)
        end
      end

      package_resource = recipe.declare_resource(:package, "vim") do
        action :nothing
      end

      # RubyBlock needs to be able to run for our lazy examples to work - and it alone cannot affect the system
      expect(ruby_block).to receive(:provider_for_action).and_call_original

      expect(package_resource).to receive(:run_action).with(:install, :immediate, instance_of(Chef::Resource::Log)).ordered.and_call_original

      expect(package_resource).to receive(:run_action).with(:nothing, nil, nil).ordered.and_call_original

      runner.converge
    end

  end

  # Mocks having the provider run successfully and update the resource
  def update_action(resource, times=1)
    p = Chef::Provider.new(resource, run_context)
    expect(resource).to receive(:provider_for_action).exactly(times).times.and_return(p)
    expect(p).to receive(:run_action).exactly(times).times {
      resource.updated_by_last_action(true)
    }
  end

end