mandrill-rails 0.0.4 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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