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 +5 -3
- data/CHANGELOG +5 -2
- data/Guardfile +1 -1
- data/README.rdoc +44 -3
- data/lib/mandrill-rails.rb +2 -2
- data/lib/mandrill-rails/version.rb +1 -1
- data/lib/mandrill-rails/web_hook_processor.rb +46 -6
- data/lib/mandrill/web_hook/processor.rb +37 -4
- data/spec/fixtures/webhook_examples/click_with_signature.json +21 -0
- data/spec/mandrill-rails/web_hook_processor_spec.rb +112 -6
- data/spec/mandrill/web_hook/processor_spec.rb +55 -2
- data/spec/support/fixtures_helper.rb +5 -1
- metadata +5 -4
- data/.rspec +0 -1
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
|
-
|
6
|
-
|
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
|
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.
|
15
|
-
*
|
16
|
-
* Requires Rails >= 3.0.3 (including 3.1
|
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
|
data/lib/mandrill-rails.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
47
|
-
|
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 :
|
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
|
-
|
8
|
-
|
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
|
-
|
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(:
|
13
|
-
let(:
|
14
|
-
before
|
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(:
|
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
|
-
|
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
|
+
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-
|
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.
|
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
|