mandrill-rails 0.0.4 → 1.0.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/.travis.yml CHANGED
@@ -2,10 +2,12 @@
2
2
  # see http://travis-ci.org/evendis/mandrill-rails
3
3
  language: ruby
4
4
  rvm:
5
- - 1.8.7
6
- - 1.9.2
5
+ # removing 1.8 and 1.9.2 tests because we don't really care anymore and activesupport 4 won't like them
6
+ #- 1.8.7
7
+ #- 1.9.2
8
+ #- jruby-18mode
7
9
  - 1.9.3
10
+ - 2.0.0
8
11
  # - rbx-18mode # removing for now; travis is having chronic intermittent issues building this properly at the moment
9
12
  - rbx-19mode
10
- - jruby-18mode
11
13
  - jruby-19mode
data/CHANGELOG CHANGED
@@ -1,3 +1,6 @@
1
- 0.0.4
2
- ===================================================================
1
+ == 1.0.0
2
+ * added support for Mandrill webhook authentication (thanks to @zshannon for the contribution)
3
+ * stepped to 1.0.0 (applying Semantic Versioning principles; see http://semver.org/)
4
+
5
+ == 0.0.4
3
6
  * removed manadrill gem as a dependency (thanks to @ssaunier for highlighting this)
data/Guardfile CHANGED
@@ -1,7 +1,7 @@
1
1
  # A sample Guardfile
2
2
  # More info at https://github.com/guard/guard#readme
3
3
 
4
- guard 'rspec', :version => 2 do
4
+ guard 'rspec', :version => 2, :all_on_start => false, :all_after_pass => false do
5
5
  watch(%r{^spec/.+_spec\.rb$})
6
6
  watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
7
7
  watch('spec/spec_helper.rb') { "spec" }
data/README.rdoc CHANGED
@@ -11,9 +11,9 @@ FYI, {Mandrill}[http://mandrill.com/] is the transactional email service by the
11
11
 
12
12
  == Requirements and Known Limitations
13
13
 
14
- * Tested with MRI 1.8.7, 1.9.2, 1.9.3, Rubinius (1.9 mode), JRuby (1.8 and 1.9 mode).
15
- * Rubinius 1.8 mode build temporarily removed since travis is having intermittent problems with this config.
16
- * Requires Rails >= 3.0.3 (including 3.1 and 3.2).
14
+ * Tested with MRI 1.9.3, 2.0.0, Rubinius (1.9 mode), JRuby (1.9 mode).
15
+ * MRI 1.8.7, 1.9.2, JRuby 1.8 mode and Rubinius 1.8 mode do work but you must force activesupport < 4.
16
+ * Requires Rails >= 3.0.3 (including 3.1, 3.2 and 4).
17
17
 
18
18
  Food for thought (upcoming features maybe)..
19
19
  * some generators may be handy to avoid the manual coding to wire up web hooks
@@ -112,6 +112,46 @@ your handler individually.
112
112
 
113
113
  And if you don't care to handle a specific payload type - then just don't implement the associated handler.
114
114
 
115
+ === How can I authenticate Mandrill Webhooks?
116
+
117
+ Mandrill now supports {webhook authentication}[http://help.mandrill.com/entries/23704122-Authenticating-webhook-requests] which can help prevent unauthorised posting to your webhook handlers. You can lookup and reset your API keys on the
118
+ {Mandrill WebHook settings}[https://mandrillapp.com/settings/webhooks] page.
119
+
120
+ If you do not configure your webhook API key, then the handlers will continue to work fine - they just won't be authenticated.
121
+
122
+ To enable authentication, use the <tt>authenticate_with_mandrill_keys!</tt> method to set your API key. It is recommended you pull
123
+ your API keys from environment settings, or use some other means to avoid committing the API keys in your source code.
124
+
125
+ For example, to handle inbound email:
126
+
127
+ class InboxController < ApplicationController
128
+ include Mandrill::Rails::WebHookProcessor
129
+ authenticate_with_mandrill_keys! 'YOUR_MANDRILL_WEBHOOK_KEY'
130
+
131
+ def handle_inbound(event_payload)
132
+ # .. handler methods will only be called if authentication has succeeded.
133
+ end
134
+
135
+ end
136
+
137
+ === How can I authenticate multiple Mandrill Webhooks in the same controller?
138
+
139
+ Sometimes you may have more than one WebHook sending requests to a single controller, for example if you have one handling 'click' events, and another sending inbound email. Mandrill assigns separate API keys to each of these.
140
+
141
+ In this case, just add all the valid API keys you will allow with <tt>authenticate_with_mandrill_keys!</tt>, for example:
142
+
143
+ class InboxController < ApplicationController
144
+ include Mandrill::Rails::WebHookProcessor
145
+ authenticate_with_mandrill_keys! 'MANDRILL_CLICK_WEBHOOK_KEY', 'MANDRILL_INBOUND_WEBHOOK_KEY', 'ANOTHER_WEBHOOK_KEY'
146
+
147
+ def handle_inbound(event_payload)
148
+ end
149
+
150
+ def handle_click(event_payload)
151
+ end
152
+
153
+ end
154
+
115
155
  === How do I pull apart the event_payload?
116
156
 
117
157
  The <tt>event_payload</tt> object passed to our handler represents a single event and is packaged
@@ -224,6 +264,7 @@ and {MailChimp}[https://rubygems.org/search?utf8=%E2%9C%93&query=mailchimp] gems
224
264
  If you need direct API integration in addition to Mandrill::Rails features,
225
265
  you can choose to add whichever best meets your needs and use as normal.
226
266
 
267
+
227
268
  == Contributing to Mandrill::Rails
228
269
 
229
270
  * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
@@ -1,6 +1,6 @@
1
1
  require 'json'
2
- require 'active_support/concern'
3
- require 'active_support/dependencies'
2
+ require 'openssl'
3
+ require 'active_support/core_ext'
4
4
 
5
5
  require "mandrill-rails/version"
6
6
  require 'mandrill/web_hook'
@@ -3,5 +3,5 @@ module Mandrill
3
3
  end
4
4
  end
5
5
  unless defined?(Mandrill::Rails::VERSION)
6
- Mandrill::Rails::VERSION = "0.0.4"
6
+ Mandrill::Rails::VERSION = "1.0.0"
7
7
  end
@@ -35,20 +35,60 @@ module Mandrill::Rails::WebHookProcessor
35
35
 
36
36
  included do
37
37
  skip_before_filter :verify_authenticity_token
38
+ before_filter :authenticate_mandrill_request!, :only => [:create]
38
39
  end
39
40
 
40
- # Returns 200 and does nothing else (this is a test done by the mandrill service)
41
+ module ClassMethods
42
+ # Gets/sets the current Mandrill WebHook Authentication key(s).
43
+ # Returns the current WebHook key(s) as an Array if called with no parameters.
44
+ # If called with parameters, add the params to the WebHook key array.
45
+ # If called with nil as the parameters, clears the WebHook key array.
46
+ def authenticate_with_mandrill_keys!(*keys)
47
+ @mandrill_webhook_keys ||= []
48
+ if keys.present?
49
+ if keys.compact.present?
50
+ @mandrill_webhook_keys.concat(keys.flatten)
51
+ else
52
+ @mandrill_webhook_keys = []
53
+ end
54
+ end
55
+ @mandrill_webhook_keys
56
+ end
57
+
58
+ # Gets the current Mandrill WebHook Authentication key(s).
59
+ def mandrill_webhook_keys
60
+ authenticate_with_mandrill_keys!
61
+ end
62
+
63
+ # Command: directly assigns the WebHook key array to +keys+.
64
+ def mandrill_webhook_keys=(keys)
65
+ @mandrill_webhook_keys = Array(keys)
66
+ end
67
+
68
+ end
69
+
70
+ # Handles controller :show action (corresponds to a Mandrill "are you there?" test ping).
71
+ # Returns 200 and does nothing else.
41
72
  def show
42
73
  head(:ok)
43
74
  end
44
75
 
76
+ # Handles controller :create action (corresponds to a POST from Mandrill).
45
77
  def create
46
- if processor = Mandrill::WebHook::Processor.new(params)
47
- processor.callback_host = self
48
- processor.run!
49
- end
78
+ processor = Mandrill::WebHook::Processor.new(params, self)
79
+ processor.run!
50
80
  head(:ok)
51
81
  end
52
82
 
83
+ def authenticate_mandrill_request!
84
+ expected_signature = request.headers['HTTP_X_MANDRILL_SIGNATURE']
85
+ mandrill_webhook_keys = self.class.mandrill_webhook_keys
86
+ if Mandrill::WebHook::Processor.authentic?(expected_signature,mandrill_webhook_keys,request.original_url,request.params)
87
+ true
88
+ else
89
+ head(:forbidden, :text => "Mandrill signature did not match.")
90
+ false
91
+ end
92
+ end
53
93
 
54
- end
94
+ end
@@ -1,11 +1,19 @@
1
1
  class Mandrill::WebHook::Processor
2
2
 
3
- attr_accessor :mandrill_events, :callback_host
3
+ attr_accessor :params, :callback_host, :mandrill_events
4
4
 
5
5
  # Command initialise the processor with +params+ Hash.
6
- # +params+ is expected to contain an array of mandrill_events
7
- def initialize(params={})
8
- @mandrill_events = JSON.parse(params['mandrill_events'] || '[]')
6
+ # +params+ is expected to contain an array of mandrill_events.
7
+ # +callback_host+ is a handle to the controller making the request.
8
+ def initialize(params={},callback_host=nil)
9
+ @params = params
10
+ @callback_host = callback_host
11
+ end
12
+
13
+ def mandrill_events
14
+ @mandrill_events ||= JSON.parse(params['mandrill_events'] || '[]')
15
+ rescue
16
+ @mandrill_events = []
9
17
  end
10
18
 
11
19
  # Command: processes all +mandrill_events+
@@ -28,4 +36,29 @@ class Mandrill::WebHook::Processor
28
36
  Mandrill::WebHook::EventDecorator[raw_event_payload]
29
37
  end
30
38
 
39
+ class << self
40
+
41
+ # Returns true if +params+ sent to +original_url+ are authentic given +expected_signature+ and +mandrill_webhook_keys+.
42
+ def authentic?(expected_signature, mandrill_webhook_keys, original_url, params)
43
+ result = true
44
+ Array(mandrill_webhook_keys).each do |key|
45
+ signature = generate_signature(key, original_url, params)
46
+ result = (signature == expected_signature)
47
+ break if result
48
+ end
49
+ result
50
+ end
51
+
52
+ # Method described in docs: http://help.mandrill.com/entries/23704122-Authenticating-webhook-requests
53
+ def generate_signature(webhook_key, original_url, params)
54
+ signed_data = original_url.dup
55
+ params.except(:action, :controller).keys.sort.each do |key|
56
+ signed_data << key
57
+ signed_data << params[key]
58
+ end
59
+ Base64.encode64("#{OpenSSL::HMAC.digest('sha1', webhook_key, signed_data)}").strip
60
+ end
61
+
62
+ end
63
+
31
64
  end
@@ -0,0 +1,21 @@
1
+ [
2
+ {
3
+ "private_key": "ntynTMaFpM_octE5R1DKAw",
4
+ "original_url": "http://requestb.in/q9a918q9",
5
+ "headers" : {
6
+ "X-Mandrill-Signature": "7rUa4lNJitlb178MgGOcHO6T4Bg=",
7
+ "Content-Type": "application/x-www-form-urlencoded",
8
+ "Host": "requestb.in",
9
+ "Connection": "close",
10
+ "Content-Length": 4091,
11
+ "User-Agent": "Mandrill-Webhook/1.0",
12
+ "Accept": "*/*"
13
+ },
14
+ "raw_params": {
15
+ "mandrill_events": "%5B%7B%22event%22%3A%22click%22%2C%22msg%22%3A%7B%22ts%22%3A1365109999%2C%22subject%22%3A%22This+an+example+webhook+message%22%2C%22email%22%3A%22example.webhook%40mandrillapp.com%22%2C%22sender%22%3A%22example.sender%40mandrillapp.com%22%2C%22tags%22%3A%5B%22webhook-example%22%5D%2C%22opens%22%3A%5B%7B%22ts%22%3A1365111111%7D%5D%2C%22clicks%22%3A%5B%7B%22ts%22%3A1365111111%2C%22url%22%3A%22http%3A%5C%2F%5C%2Fmandrill.com%22%7D%5D%2C%22state%22%3A%22sent%22%2C%22metadata%22%3A%7B%22user_id%22%3A111%7D%2C%22_id%22%3A%22exampleaaaaaaaaaaaaaaaaaaaaaaaaa%22%2C%22_version%22%3A%22exampleaaaaaaaaaaaaaaa%22%7D%2C%22ip%22%3A%22127.0.0.1%22%2C%22location%22%3A%7B%22country_short%22%3A%22US%22%2C%22country%22%3A%22United+States%22%2C%22region%22%3A%22Oklahoma%22%2C%22city%22%3A%22Oklahoma+City%22%2C%22latitude%22%3A35.4675598145%2C%22longitude%22%3A-97.5164337158%2C%22postal_code%22%3A%2273101%22%2C%22timezone%22%3A%22-05%3A00%22%7D%2C%22user_agent%22%3A%22Mozilla%5C%2F5.0+%28Macintosh%3B+U%3B+Intel+Mac+OS+X+10.6%3B+en-US%3B+rv%3A1.9.1.8%29+Gecko%5C%2F20100317+Postbox%5C%2F1.1.3%22%2C%22user_agent_parsed%22%3A%7B%22type%22%3A%22Email+Client%22%2C%22ua_family%22%3A%22Postbox%22%2C%22ua_name%22%3A%22Postbox+1.1.3%22%2C%22ua_version%22%3A%221.1.3%22%2C%22ua_url%22%3A%22http%3A%5C%2F%5C%2Fwww.postbox-inc.com%5C%2F%22%2C%22ua_company%22%3A%22Postbox%2C+Inc.%22%2C%22ua_company_url%22%3A%22http%3A%5C%2F%5C%2Fwww.postbox-inc.com%5C%2F%22%2C%22ua_icon%22%3A%22http%3A%5C%2F%5C%2Fcdn.mandrill.com%5C%2Fimg%5C%2Femail-client-icons%5C%2Fpostbox.png%22%2C%22os_family%22%3A%22OS+X%22%2C%22os_name%22%3A%22OS+X+10.6+Snow+Leopard%22%2C%22os_url%22%3A%22http%3A%5C%2F%5C%2Fwww.apple.com%5C%2Fosx%5C%2F%22%2C%22os_company%22%3A%22Apple+Computer%2C+Inc.%22%2C%22os_company_url%22%3A%22http%3A%5C%2F%5C%2Fwww.apple.com%5C%2F%22%2C%22os_icon%22%3A%22http%3A%5C%2F%5C%2Fcdn.mandrill.com%5C%2Fimg%5C%2Femail-client-icons%5C%2Fmacosx.png%22%2C%22mobile%22%3Afalse%7D%2C%22url%22%3A%22http%3A%5C%2F%5C%2Fmandrill.com%22%2C%22ts%22%3A1379233940%7D%2C%7B%22event%22%3A%22click%22%2C%22msg%22%3A%7B%22ts%22%3A1365109999%2C%22subject%22%3A%22This+an+example+webhook+message%22%2C%22email%22%3A%22example.webhook%40mandrillapp.com%22%2C%22sender%22%3A%22example.sender%40mandrillapp.com%22%2C%22tags%22%3A%5B%22webhook-example%22%5D%2C%22opens%22%3A%5B%7B%22ts%22%3A1365111111%7D%5D%2C%22clicks%22%3A%5B%7B%22ts%22%3A1365111111%2C%22url%22%3A%22http%3A%5C%2F%5C%2Fmandrill.com%22%7D%5D%2C%22state%22%3A%22sent%22%2C%22metadata%22%3A%7B%22user_id%22%3A111%7D%2C%22_id%22%3A%22exampleaaaaaaaaaaaaaaaaaaaaaaaaa%22%2C%22_version%22%3A%22exampleaaaaaaaaaaaaaaa%22%7D%2C%22ip%22%3A%22127.0.0.1%22%2C%22location%22%3A%7B%22country_short%22%3A%22US%22%2C%22country%22%3A%22United+States%22%2C%22region%22%3A%22Oklahoma%22%2C%22city%22%3A%22Oklahoma+City%22%2C%22latitude%22%3A35.4675598145%2C%22longitude%22%3A-97.5164337158%2C%22postal_code%22%3A%2273101%22%2C%22timezone%22%3A%22-05%3A00%22%7D%2C%22user_agent%22%3A%22Mozilla%5C%2F5.0+%28Macintosh%3B+U%3B+Intel+Mac+OS+X+10.6%3B+en-US%3B+rv%3A1.9.1.8%29+Gecko%5C%2F20100317+Postbox%5C%2F1.1.3%22%2C%22user_agent_parsed%22%3A%7B%22type%22%3A%22Email+Client%22%2C%22ua_family%22%3A%22Postbox%22%2C%22ua_name%22%3A%22Postbox+1.1.3%22%2C%22ua_version%22%3A%221.1.3%22%2C%22ua_url%22%3A%22http%3A%5C%2F%5C%2Fwww.postbox-inc.com%5C%2F%22%2C%22ua_company%22%3A%22Postbox%2C+Inc.%22%2C%22ua_company_url%22%3A%22http%3A%5C%2F%5C%2Fwww.postbox-inc.com%5C%2F%22%2C%22ua_icon%22%3A%22http%3A%5C%2F%5C%2Fcdn.mandrill.com%5C%2Fimg%5C%2Femail-client-icons%5C%2Fpostbox.png%22%2C%22os_family%22%3A%22OS+X%22%2C%22os_name%22%3A%22OS+X+10.6+Snow+Leopard%22%2C%22os_url%22%3A%22http%3A%5C%2F%5C%2Fwww.apple.com%5C%2Fosx%5C%2F%22%2C%22os_company%22%3A%22Apple+Computer%2C+Inc.%22%2C%22os_company_url%22%3A%22http%3A%5C%2F%5C%2Fwww.apple.com%5C%2F%22%2C%22os_icon%22%3A%22http%3A%5C%2F%5C%2Fcdn.mandrill.com%5C%2Fimg%5C%2Femail-client-icons%5C%2Fmacosx.png%22%2C%22mobile%22%3Afalse%7D%2C%22url%22%3A%22http%3A%5C%2F%5C%2Fmandrill.com%22%2C%22ts%22%3A1379233940%7D%5D"
16
+ },
17
+ "params": {
18
+ "mandrill_events": [{"event":"click","msg":{"ts":1365109999,"subject":"This an example webhook message","email":"example.webhook@mandrillapp.com","sender":"example.sender@mandrillapp.com","tags":["webhook-example"],"opens":[{"ts":1365111111}],"clicks":[{"ts":1365111111,"url":"http:\/\/mandrill.com"}],"state":"sent","metadata":{"user_id":111},"_id":"exampleaaaaaaaaaaaaaaaaaaaaaaaaa","_version":"exampleaaaaaaaaaaaaaaa"},"ip":"127.0.0.1","location":{"country_short":"US","country":"United States","region":"Oklahoma","city":"Oklahoma City","latitude":35.4675598145,"longitude":-97.5164337158,"postal_code":"73101","timezone":"-05:00"},"user_agent":"Mozilla\/5.0 (Macintosh; U; Intel Mac OS X 10.6; en-US; rv:1.9.1.8) Gecko\/20100317 Postbox\/1.1.3","user_agent_parsed":{"type":"Email Client","ua_family":"Postbox","ua_name":"Postbox 1.1.3","ua_version":"1.1.3","ua_url":"http:\/\/www.postbox-inc.com\/","ua_company":"Postbox, Inc.","ua_company_url":"http:\/\/www.postbox-inc.com\/","ua_icon":"http:\/\/cdn.mandrill.com\/img\/email-client-icons\/postbox.png","os_family":"OS X","os_name":"OS X 10.6 Snow Leopard","os_url":"http:\/\/www.apple.com\/osx\/","os_company":"Apple Computer, Inc.","os_company_url":"http:\/\/www.apple.com\/","os_icon":"http:\/\/cdn.mandrill.com\/img\/email-client-icons\/macosx.png","mobile":false},"url":"http:\/\/mandrill.com","ts":1379233940},{"event":"click","msg":{"ts":1365109999,"subject":"This an example webhook message","email":"example.webhook@mandrillapp.com","sender":"example.sender@mandrillapp.com","tags":["webhook-example"],"opens":[{"ts":1365111111}],"clicks":[{"ts":1365111111,"url":"http:\/\/mandrill.com"}],"state":"sent","metadata":{"user_id":111},"_id":"exampleaaaaaaaaaaaaaaaaaaaaaaaaa","_version":"exampleaaaaaaaaaaaaaaa"},"ip":"127.0.0.1","location":{"country_short":"US","country":"United States","region":"Oklahoma","city":"Oklahoma City","latitude":35.4675598145,"longitude":-97.5164337158,"postal_code":"73101","timezone":"-05:00"},"user_agent":"Mozilla\/5.0 (Macintosh; U; Intel Mac OS X 10.6; en-US; rv:1.9.1.8) Gecko\/20100317 Postbox\/1.1.3","user_agent_parsed":{"type":"Email Client","ua_family":"Postbox","ua_name":"Postbox 1.1.3","ua_version":"1.1.3","ua_url":"http:\/\/www.postbox-inc.com\/","ua_company":"Postbox, Inc.","ua_company_url":"http:\/\/www.postbox-inc.com\/","ua_icon":"http:\/\/cdn.mandrill.com\/img\/email-client-icons\/postbox.png","os_family":"OS X","os_name":"OS X 10.6 Snow Leopard","os_url":"http:\/\/www.apple.com\/osx\/","os_company":"Apple Computer, Inc.","os_company_url":"http:\/\/www.apple.com\/","os_icon":"http:\/\/cdn.mandrill.com\/img\/email-client-icons\/macosx.png","mobile":false},"url":"http:\/\/mandrill.com","ts":1379233940}]
19
+ }
20
+ }
21
+ ]
@@ -1,17 +1,76 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  class WebHookProcessorTestHarness
4
- def self.skip_before_filter(*args) ; end
4
+ # Mock some controller behaviour
5
+ # TODO: we should probably really start using a real controller harness for testing
6
+ def self.skip_before_filter(*args) ; @skip_before_filter_settings = args; end
7
+ def self.skip_before_filter_settings ; @skip_before_filter_settings; end
8
+ def self.before_filter(*args) ; @before_filter_settings = args ; end
9
+ def self.before_filter_settings ; @before_filter_settings; end
5
10
  def head(*args) ; end
6
- attr_accessor :params
11
+ attr_accessor :params, :request
7
12
 
8
13
  include Mandrill::Rails::WebHookProcessor
9
14
  end
10
15
 
11
16
  describe Mandrill::Rails::WebHookProcessor do
12
- let(:processor_instance) { WebHookProcessorTestHarness.new }
13
- let(:params) { {} }
14
- before { processor_instance.params = params}
17
+ let(:processor_class) { WebHookProcessorTestHarness }
18
+ let(:processor_instance) { processor_class.new }
19
+ before do
20
+ processor_class.authenticate_with_mandrill_keys! nil
21
+ end
22
+
23
+ describe "##skip_before_filter settings" do
24
+ subject { processor_class.skip_before_filter_settings }
25
+ it { should eql([:verify_authenticity_token]) }
26
+ end
27
+
28
+ describe "##before_filter settings" do
29
+ subject { processor_class.before_filter_settings }
30
+ it { should eql([:authenticate_mandrill_request!, {:only=>[:create]}]) }
31
+ end
32
+
33
+ describe "##authenticate_with_mandrill_keys!" do
34
+ subject { processor_class.mandrill_webhook_keys }
35
+ it { should eql([]) }
36
+ context "when set with a single value" do
37
+ let(:key_a) { "key_a" }
38
+ before { processor_class.authenticate_with_mandrill_keys! key_a }
39
+ it { should eql([key_a]) }
40
+ context "then called with nil" do
41
+ before { processor_class.authenticate_with_mandrill_keys! nil }
42
+ it { should eql([]) }
43
+ end
44
+ end
45
+ context "when set with a list of values" do
46
+ let(:key_a) { "key_a" }
47
+ let(:key_b) { "key_b" }
48
+ before { processor_class.authenticate_with_mandrill_keys! key_a, key_b }
49
+ it { should eql([key_a,key_b]) }
50
+ end
51
+ context "when set with an explicit array of values" do
52
+ let(:key_a) { "key_a" }
53
+ let(:key_b) { "key_b" }
54
+ before { processor_class.authenticate_with_mandrill_keys! [key_a, key_b] }
55
+ it { should eql([key_a,key_b]) }
56
+ end
57
+ end
58
+
59
+ describe "#mandrill_webhook_keys" do
60
+ subject { processor_class.mandrill_webhook_keys }
61
+ it { should eql([]) }
62
+ context "when set with mandrill_webhook_keys=" do
63
+ let(:expected_value) { [1,2,3] }
64
+ before { processor_class.mandrill_webhook_keys = expected_value }
65
+ it { should eql(expected_value) }
66
+ end
67
+ context "when set with authenticate_with_mandrill_keys!" do
68
+ let(:expected_value) { [4,5,6] }
69
+ before { processor_class.authenticate_with_mandrill_keys! expected_value }
70
+ it { should eql(expected_value) }
71
+ end
72
+ end
73
+
15
74
 
16
75
  subject { processor_instance }
17
76
 
@@ -23,11 +82,58 @@ describe Mandrill::Rails::WebHookProcessor do
23
82
  end
24
83
 
25
84
  describe "#create" do
85
+ let(:params) { {} }
86
+ before do
87
+ processor_instance.params = params
88
+ end
26
89
  it "should return head(:ok)" do
27
- Mandrill::WebHook::Processor.any_instance.should_receive(:run!)
28
90
  processor_instance.should_receive(:head).with(:ok)
91
+ Mandrill::WebHook::Processor.any_instance.should_receive(:run!)
29
92
  processor_instance.create
30
93
  end
31
94
  end
32
95
 
96
+ describe "#authenticate_mandrill_request! (protected)" do
97
+ let(:example_payload) { webhook_example_event('click_with_signature') }
98
+ let(:expected_signature) { example_payload['headers']['X-Mandrill-Signature'] }
99
+ let(:original_url) { example_payload['original_url'] }
100
+ let(:valid_webhook_key) { example_payload['private_key'] }
101
+ let(:raw_params) { example_payload['raw_params'] }
102
+ let(:params) { {} }
103
+ let(:headers) { { 'HTTP_X_MANDRILL_SIGNATURE' => expected_signature} }
104
+ let(:request) { double() }
105
+ before do
106
+ request.stub(:original_url).and_return(original_url)
107
+ request.stub(:params).and_return(raw_params)
108
+ request.stub(:headers).and_return(headers)
109
+ processor_instance.request = request
110
+ processor_instance.params = params
111
+ end
112
+ subject { processor_instance.send(:authenticate_mandrill_request!) }
113
+
114
+ context "when authentication not enabled" do
115
+ it { should be_true }
116
+ end
117
+ context "when authentication enabled" do
118
+ before do
119
+ processor_class.authenticate_with_mandrill_keys! mandrill_webhook_keys
120
+ end
121
+ context "with valid key" do
122
+ let(:mandrill_webhook_keys) { valid_webhook_key }
123
+ it { should be_true }
124
+ end
125
+ context "with mix of valid and invalid keys" do
126
+ let(:mandrill_webhook_keys) { ['bogative',valid_webhook_key] }
127
+ it { should be_true }
128
+ end
129
+ context "with invalid key" do
130
+ let(:mandrill_webhook_keys) { 'bogative' }
131
+ it "should call head(:forbidden) and return false" do
132
+ processor_instance.should_receive(:head).with(:forbidden, :text => "Mandrill signature did not match.")
133
+ subject.should be_false
134
+ end
135
+ end
136
+ end
137
+ end
138
+
33
139
  end
@@ -3,12 +3,13 @@ require 'spec_helper'
3
3
  describe Mandrill::WebHook::Processor do
4
4
 
5
5
  let(:params) { {} }
6
- let(:processor) { Mandrill::WebHook::Processor.new(params) }
6
+ let(:processor_class) { Mandrill::WebHook::Processor }
7
+ let(:processor) { processor_class.new(params) }
7
8
 
8
9
  describe "#run!" do
9
10
  context "with inbound events" do
10
11
  before do
11
- Mandrill::WebHook::Processor.stub(:handle_inbound)
12
+ processor_class.stub(:handle_inbound)
12
13
  end
13
14
  let(:event1) { { "event" => "inbound" } }
14
15
  let(:event2) { { "event" => "inbound" } }
@@ -18,6 +19,21 @@ describe Mandrill::WebHook::Processor do
18
19
  processor.run!
19
20
  end
20
21
  end
22
+ context "with callback host" do
23
+ let(:callback_host) do
24
+ host = double()
25
+ host.stub(:handle_inbound)
26
+ host
27
+ end
28
+ let(:processor) { processor_class.new(params,callback_host) }
29
+ let(:event1) { { "event" => "inbound" } }
30
+ let(:event2) { { "event" => "inbound" } }
31
+ let(:params) { { "mandrill_events" => [event1,event2].to_json } }
32
+ it "should pass event payload to the handler" do
33
+ callback_host.should_receive(:handle_inbound).twice
34
+ processor.run!
35
+ end
36
+ end
21
37
  end
22
38
 
23
39
  describe "#wrap_payload" do
@@ -26,4 +42,41 @@ describe Mandrill::WebHook::Processor do
26
42
  its(:class) { should eql(Mandrill::WebHook::EventDecorator) }
27
43
  end
28
44
 
45
+ describe "##authentic?" do
46
+ let(:example_payload) { webhook_example_event('click_with_signature') }
47
+ let(:expected_signature) { example_payload['headers']['X-Mandrill-Signature'] }
48
+ let(:original_url) { example_payload['original_url'] }
49
+ let(:webhook_key) { example_payload['private_key'] }
50
+ let(:mandrill_webhook_keys) { [webhook_key] }
51
+ let(:params) { example_payload['raw_params'] }
52
+ subject { processor_class.authentic?(expected_signature, mandrill_webhook_keys, original_url, params) }
53
+ context "when valid" do
54
+ it { should be_true }
55
+ end
56
+ context "when no keys" do
57
+ let(:mandrill_webhook_keys) { [] }
58
+ it { should be_true }
59
+ end
60
+ context "when keys don't match" do
61
+ let(:mandrill_webhook_keys) { ['bogative'] }
62
+ it { should be_false }
63
+ end
64
+ context "when signature don't match" do
65
+ let(:expected_signature) { 'bogative' }
66
+ it { should be_false }
67
+ end
68
+
69
+
70
+ end
71
+
72
+ describe "##generate_signature" do
73
+ let(:example_payload) { webhook_example_event('click_with_signature') }
74
+ let(:expected_signature) { example_payload['headers']['X-Mandrill-Signature'] }
75
+ let(:original_url) { example_payload['original_url'] }
76
+ let(:webhook_key) { example_payload['private_key'] }
77
+ let(:params) { example_payload['raw_params'] }
78
+ subject { processor_class.generate_signature(webhook_key, original_url, params) }
79
+ it { should eql(expected_signature) }
80
+ end
81
+
29
82
  end
@@ -15,7 +15,11 @@ module FixturesHelper
15
15
 
16
16
  # Returns the JSON representation of an +sample_name+ event
17
17
  def webhook_example_event(sample_name)
18
- webhook_example_events(sample_name).first
18
+ data = webhook_example_events(sample_name).first
19
+ if data['raw_params'] && (mandrill_events_data = data['raw_params']['mandrill_events'])
20
+ data['raw_params']['mandrill_events'] = URI.decode_www_form_component(mandrill_events_data)
21
+ end
22
+ data
19
23
  end
20
24
 
21
25
  def payload_examples_path
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mandrill-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.4
4
+ version: 1.0.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-04-29 00:00:00.000000000 Z
12
+ date: 2013-09-18 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activesupport
@@ -115,7 +115,6 @@ extensions: []
115
115
  extra_rdoc_files: []
116
116
  files:
117
117
  - .gitignore
118
- - .rspec
119
118
  - .travis.yml
120
119
  - CHANGELOG
121
120
  - Gemfile
@@ -135,6 +134,7 @@ files:
135
134
  - spec/fixtures/payload_examples/sample.pdf
136
135
  - spec/fixtures/payload_examples/sample.txt
137
136
  - spec/fixtures/webhook_examples/click.json
137
+ - spec/fixtures/webhook_examples/click_with_signature.json
138
138
  - spec/fixtures/webhook_examples/inbound.json
139
139
  - spec/fixtures/webhook_examples/inbound_reply.json
140
140
  - spec/fixtures/webhook_examples/inbound_with_multiple_attachments.json
@@ -168,7 +168,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
168
168
  version: '0'
169
169
  requirements: []
170
170
  rubyforge_project:
171
- rubygems_version: 1.8.23
171
+ rubygems_version: 1.8.25
172
172
  signing_key:
173
173
  specification_version: 3
174
174
  summary: Provides webhook processing and event decoration to make using Mandrill with
@@ -178,6 +178,7 @@ test_files:
178
178
  - spec/fixtures/payload_examples/sample.pdf
179
179
  - spec/fixtures/payload_examples/sample.txt
180
180
  - spec/fixtures/webhook_examples/click.json
181
+ - spec/fixtures/webhook_examples/click_with_signature.json
181
182
  - spec/fixtures/webhook_examples/inbound.json
182
183
  - spec/fixtures/webhook_examples/inbound_reply.json
183
184
  - spec/fixtures/webhook_examples/inbound_with_multiple_attachments.json
data/.rspec DELETED
@@ -1 +0,0 @@
1
- --color