conekta_event 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (77) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +11 -0
  3. data/.rspec +1 -0
  4. data/Gemfile +2 -0
  5. data/MIT-LICENSE +20 -0
  6. data/README.md +156 -0
  7. data/Rakefile +36 -0
  8. data/app/controllers/conekta_event/webhook_controller.rb +46 -0
  9. data/bin/rails +14 -0
  10. data/conekta_event.gemspec +26 -0
  11. data/config/routes.rb +3 -0
  12. data/lib/conekta_event/engine.rb +5 -0
  13. data/lib/conekta_event/version.rb +3 -0
  14. data/lib/conekta_event.rb +76 -0
  15. data/spec/controllers/webhook_controller_spec.rb +81 -0
  16. data/spec/dummy/Rakefile +6 -0
  17. data/spec/dummy/app/assets/config/manifest.js +0 -0
  18. data/spec/dummy/app/assets/images/.keep +0 -0
  19. data/spec/dummy/app/assets/javascripts/application.js +13 -0
  20. data/spec/dummy/app/assets/javascripts/cable.js +13 -0
  21. data/spec/dummy/app/assets/javascripts/channels/.keep +0 -0
  22. data/spec/dummy/app/assets/stylesheets/application.css +15 -0
  23. data/spec/dummy/app/channels/application_cable/channel.rb +4 -0
  24. data/spec/dummy/app/channels/application_cable/connection.rb +4 -0
  25. data/spec/dummy/app/controllers/application_controller.rb +3 -0
  26. data/spec/dummy/app/controllers/concerns/.keep +0 -0
  27. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  28. data/spec/dummy/app/jobs/application_job.rb +2 -0
  29. data/spec/dummy/app/mailers/application_mailer.rb +4 -0
  30. data/spec/dummy/app/models/application_record.rb +3 -0
  31. data/spec/dummy/app/models/concerns/.keep +0 -0
  32. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  33. data/spec/dummy/app/views/layouts/mailer.html.erb +13 -0
  34. data/spec/dummy/app/views/layouts/mailer.text.erb +1 -0
  35. data/spec/dummy/bin/bundle +3 -0
  36. data/spec/dummy/bin/rails +4 -0
  37. data/spec/dummy/bin/rake +4 -0
  38. data/spec/dummy/bin/setup +38 -0
  39. data/spec/dummy/bin/update +29 -0
  40. data/spec/dummy/bin/yarn +11 -0
  41. data/spec/dummy/config/application.rb +17 -0
  42. data/spec/dummy/config/boot.rb +5 -0
  43. data/spec/dummy/config/cable.yml +10 -0
  44. data/spec/dummy/config/database.yml +25 -0
  45. data/spec/dummy/config/environment.rb +5 -0
  46. data/spec/dummy/config/environments/development.rb +54 -0
  47. data/spec/dummy/config/environments/production.rb +91 -0
  48. data/spec/dummy/config/environments/test.rb +42 -0
  49. data/spec/dummy/config/initializers/application_controller_renderer.rb +6 -0
  50. data/spec/dummy/config/initializers/assets.rb +14 -0
  51. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  52. data/spec/dummy/config/initializers/cookies_serializer.rb +5 -0
  53. data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  54. data/spec/dummy/config/initializers/inflections.rb +16 -0
  55. data/spec/dummy/config/initializers/mime_types.rb +4 -0
  56. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  57. data/spec/dummy/config/locales/en.yml +33 -0
  58. data/spec/dummy/config/puma.rb +56 -0
  59. data/spec/dummy/config/routes.rb +3 -0
  60. data/spec/dummy/config/secrets.yml +32 -0
  61. data/spec/dummy/config/spring.rb +6 -0
  62. data/spec/dummy/config.ru +5 -0
  63. data/spec/dummy/lib/assets/.keep +0 -0
  64. data/spec/dummy/log/.keep +0 -0
  65. data/spec/dummy/package.json +5 -0
  66. data/spec/dummy/public/404.html +67 -0
  67. data/spec/dummy/public/422.html +67 -0
  68. data/spec/dummy/public/500.html +66 -0
  69. data/spec/dummy/public/apple-touch-icon-precomposed.png +0 -0
  70. data/spec/dummy/public/apple-touch-icon.png +0 -0
  71. data/spec/dummy/public/favicon.ico +0 -0
  72. data/spec/lib/conekta_event_spec.rb +206 -0
  73. data/spec/rails_helper.rb +45 -0
  74. data/spec/spec_helper.rb +31 -0
  75. data/spec/support/fixtures/evt_charge_paid.json +49 -0
  76. data/spec/support/fixtures/evt_invalid_id.json +7 -0
  77. metadata +189 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 4c610b788f5ef462feb3a9ce49a71fb3e91f8df8
4
+ data.tar.gz: 0affdd53066a6488ac2e9f24fbc35cbb700c83b4
5
+ SHA512:
6
+ metadata.gz: 1ec8a8c8dc5bab475deece1f1166fb37e93089870a10b8c425475152aa94ba666bd2886fd635526e94d466960d9731866fe9748d0fa377772ce711d101b9592c
7
+ data.tar.gz: 29b524ae435e82ed6d1de96e7d4851aed83a8e4964a0518279fb5ec68f1e74f16518f87dd66555e4f5203c447ccc85f5715e9839c9fab017bacd6d84c6f983d4
data/.gitignore ADDED
@@ -0,0 +1,11 @@
1
+ .bundle/
2
+ .byebug_history
3
+ log/*.log
4
+ pkg/
5
+ spec/dummy/db/*.sqlite3
6
+ spec/dummy/log/*.log
7
+ spec/dummy/tmp/
8
+ spec/dummy/.sass-cache
9
+ Gemfile.lock
10
+ gemfiles/*.lock
11
+ coverage/*
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --require spec_helper
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2017 Jorge Najera
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,156 @@
1
+ # ConektaEvent
2
+ ConketaEvent is built on the [ActiveSupport::Notifications API](http://api.rubyonrails.org/classes/ActiveSupport/Notifications.html). Incoming webhook requests are authenticated by [retrieving the event object](https://developers.conekta.com/api#events) from Conketa. Define subscribers to handle specific event types. Subscribers can be a block or an object that responds to `#call`.
3
+
4
+ This gem is based on [StripeEvent](https://github.com/integrallis/stripe_event) by [Ryan McGeary](https://github.com/rmm5t) , [Pete Keen](https://github.com/peterkeen) and [Danny Whalen](https://github.com/invisiblefunnel) . ConektaEvent improves on the gem, updating for Rails 4, using [Conekta](https://www.conekta.com/en) as Payment Gateway.
5
+
6
+ ## Install
7
+
8
+ ```ruby
9
+ # Gemfile
10
+ gem 'conekta_event'
11
+ ```
12
+
13
+ ```ruby
14
+ # config/routes.rb
15
+ mount Conekta::Engine, at: '/my-chosen-path' # provide a custom path
16
+ ```
17
+
18
+ ## Usage
19
+
20
+ ```ruby
21
+ # config/initializers/conekta.rb
22
+ Conekta.api_key = ENV['CONKECTA_SECRET_KEY'] # e.g. key_eYvWV7gSDkNYXsmr
23
+
24
+ ConektaEvent.configure do |events|
25
+ events.subscribe 'charge.paid' do |event|
26
+ # Define subscriber behavior based on the event object
27
+ event.class #=> Conekta::Event
28
+ event.type #=> "charge.paid"
29
+ event.data['object'] #=> {"id"=>"55b2b487241229ca9c000026", ... }
30
+ end
31
+
32
+ events.all do |event|
33
+ # Handle all event types - logging, etc.
34
+ end
35
+ end
36
+ ```
37
+
38
+ ### Subscriber objects that respond to #call
39
+
40
+ ```ruby
41
+ class CustomerCreated
42
+ def call(event)
43
+ # Event handling
44
+ end
45
+ end
46
+
47
+ class BillingEventLogger
48
+ def initialize(logger)
49
+ @logger = logger
50
+ end
51
+
52
+ def call(event)
53
+ @logger.info "BILLING:#{event.type}:#{event.id}"
54
+ end
55
+ end
56
+ ```
57
+
58
+ ```ruby
59
+ ConektaEvent.configure do |events|
60
+ events.all BillingEventLogger.new(Rails.logger)
61
+ events.subscribe 'customer.created', CustomerCreated.new
62
+ end
63
+ ```
64
+
65
+ ### Subscribing to a namespace of event types
66
+
67
+ ```ruby
68
+ ConektaEvent.subscribe 'customer.card.' do |event|
69
+ # Will be triggered for any customer.card.* events
70
+ end
71
+ ```
72
+
73
+ ## Securing your webhook endpoint
74
+
75
+ ConektaEvent automatically fetches events from Conekta to ensure they haven't been forged. However, that doesn't prevent an attacker who knows your endpoint name and an event's ID from forcing your server to process a legitimate event twice. If that event triggers some useful action, like generating a license key or enabling a delinquent account, you could end up giving something the attacker is supposed to pay for away for free.
76
+
77
+ To prevent this, ConektEvent supports verification by signature on your webhook endpoint, this helps us to validate that data were not sent by a third-party. Here's what you do for use this feature:
78
+
79
+ 1. First need to retrieve your endpoint’s secret in your Conekta Dashboard so Conekta start to sign each webhook sent to the endpoint.
80
+ 2. Arrange for a secret key to be available in your application's environment variables or `secrets.yml` file. You can generate a suitable secret with the `rake secret` command. (Remember, the `secrets.yml` file shouldn't contain production secrets directly; it should use ERB to include them.)
81
+
82
+ 3. Configure ConektaEvent to require that secret be used as a basic authentication password, using code along the lines of these examples:
83
+
84
+ ```ruby
85
+ # CONEKTA_PRIVATE_SIGNATURE environment variable
86
+ ConektaEvent.private_signature = ENV['CONEKTA_PRIVATE_SIGNATURE']
87
+ # stripe_webhook_secret key in secrets.yml file
88
+ ConektaEvent.private_signature = Rails.application.secrets.conekta_private_signature
89
+ ```
90
+
91
+ 4. You are done!
92
+
93
+ Remember to add an extra layer of protection and secure your webhook endpoint with SSL.
94
+
95
+ ## Configuration
96
+
97
+ If you'd like to ignore particular webhook events (perhaps to ignore test webhooks in production, or to ignore webhooks for a non-paying customer), you can do so by returning `nil` in you custom `event_retriever`. For example:
98
+
99
+ ```ruby
100
+ ConektaEvent.event_retriever = lambda do |params|
101
+ return nil if Rails.env.production? && !params[:livemode]
102
+ Conekta::Event.retrieve(params[:id])
103
+ end
104
+ ```
105
+
106
+ ```ruby
107
+ ConektaEvent.event_retriever = lambda do |params|
108
+ account = Account.find_by!(conekta_user_id: params[:user_id])
109
+ return nil if account.delinquent?
110
+ Conekta::Event.find(params[:id], account.api_key)
111
+ end
112
+ ```
113
+
114
+ ## Testing
115
+
116
+ Handling webhooks is a critical piece of modern billing systems. Verifying the behavior of ConektaEvent subscribers can be done fairly easily by stubbing out the HTTP request used to authenticate the webhook request. Tools like [Webmock](https://github.com/bblimke/webmock) and [VCR](https://github.com/vcr/vcr) work well. [RequestBin](http://requestb.in/) is great for collecting the payloads. For exploratory phases of development, [UltraHook](http://www.ultrahook.com/) and other tools can forward webhook requests directly to localhost. You can check out [test-hooks](https://github.com/invisiblefunnel/test-hooks), an example Rails application to see how to test ConektaEvent subscribers with RSpec request specs and Webmock. A quick look:
117
+
118
+ ```ruby
119
+ # spec/requests/billing_events_spec.rb
120
+ require 'spec_helper'
121
+
122
+ describe "Billing Events" do
123
+ def stub_event(fixture_id, status = 200)
124
+ stub_request(:get, "https://api.conekta.io/events/#{fixture_id}").
125
+ to_return(status: status, body: File.read("spec/support/fixtures/#{fixture_id}.json"))
126
+ end
127
+
128
+ describe "customer.created" do
129
+ before do
130
+ stub_event 'evt_customer_created'
131
+ end
132
+
133
+ it "is successful" do
134
+ post '/_billing_events', id: 'evt_customer_created'
135
+ expect(response.code).to eq "200"
136
+ # Additional expectations...
137
+ end
138
+ end
139
+ end
140
+ ```
141
+
142
+ ### Note: 'Test Webhooks' Button on Conekta Dashboard
143
+
144
+ This button sends an example event to your webhook urls, including a random `id`. To confirm that Conekta sent the webhook, ConektaEvent attempts to retrieve the event details from Conekta using the given `id`. In this case the event does not exist and Conekta Event responds with `401 Unauthorized`. Instead of using the 'Test Webhooks' button, trigger webhooks by using the Conekta API or if you already have some events in your test account you can send them manually to create test payments, customers, etc.
145
+
146
+ ### Maintainers
147
+
148
+ * [Jorge Najera](https://github.com/jNajera)
149
+
150
+ ### Versioning
151
+
152
+ Semantic Versioning 2.0 as defined at <http://semver.org>.
153
+
154
+ ### License
155
+
156
+ [MIT License](https://github.com/Actiun/conkecta-event/blob/master/MIT-LICENSE).
data/Rakefile ADDED
@@ -0,0 +1,36 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rdoc/task'
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = 'rdoc'
11
+ rdoc.title = 'ConektaEvent'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.rdoc')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+ APP_RAKEFILE = File.expand_path("../spec/dummy/Rakefile", __FILE__)
18
+ load 'rails/tasks/engine.rake'
19
+
20
+
21
+ load 'rails/tasks/statistics.rake'
22
+
23
+
24
+
25
+ require 'bundler/gem_tasks'
26
+
27
+ require 'rake/testtask'
28
+
29
+ Rake::TestTask.new(:test) do |t|
30
+ t.libs << 'test'
31
+ t.pattern = 'test/**/*_test.rb'
32
+ t.verbose = false
33
+ end
34
+
35
+
36
+ task default: :test
@@ -0,0 +1,46 @@
1
+ module ConektaEvent
2
+ class WebhookController < ActionController::Base
3
+ if respond_to?(:before_action)
4
+ before_action :request_authentication
5
+ else
6
+ before_filter :request_authentication
7
+ end
8
+
9
+ def event
10
+ ConektaEvent.instrument(params)
11
+ head :ok
12
+ rescue ConektaEvent::UnauthorizedError => e
13
+ log_error(e)
14
+ head :unauthorized
15
+ end
16
+
17
+ private
18
+
19
+ def log_error(e)
20
+ logger.error e.message
21
+ e.backtrace.each { |line| logger.error " #{line}" }
22
+ end
23
+
24
+ def request_authentication
25
+ if ConektaEvent.private_signature
26
+ digest = request.env['HTTP_DIGEST']
27
+ if digest.blank?
28
+ logger.error "Conekta Error => No Digest"
29
+ head :unauthorized
30
+ return
31
+ end
32
+ begin
33
+ private_key = OpenSSL::PKey::RSA.new(ConektaEvent.private_signature)
34
+ sha256_message = private_key.private_decrypt(Base64.decode64(digest))
35
+ if sha256_message != Digest::SHA256.hexdigest(request.raw_post)
36
+ logger.error "Conekta Error => Unauthenticated Event"
37
+ head :unauthorized
38
+ end
39
+ rescue OpenSSL::PKey::RSAError => e
40
+ log_error(e)
41
+ head :unauthorized
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
data/bin/rails ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+ # This command will automatically be run when you run "rails" with Rails gems
3
+ # installed from the root of your application.
4
+
5
+ ENGINE_ROOT = File.expand_path('../..', __FILE__)
6
+ ENGINE_PATH = File.expand_path('../../lib/conekta_event/engine', __FILE__)
7
+ APP_PATH = File.expand_path('../../spec/dummy/config/application', __FILE__)
8
+
9
+ # Set up gems listed in the Gemfile.
10
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
11
+ require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
12
+
13
+ require 'rails/all'
14
+ require 'rails/engine/commands'
@@ -0,0 +1,26 @@
1
+ $:.push File.expand_path("../lib", __FILE__)
2
+
3
+ # Maintain your gem's version:
4
+ require "conekta_event/version"
5
+
6
+ # Describe your gem and declare its dependencies:
7
+ Gem::Specification.new do |s|
8
+ s.name = "conekta_event"
9
+ s.version = ConektaEvent::VERSION
10
+ s.authors = ["Jorge Najera"]
11
+ s.email = ["jorge.najera.t@gmail.com"]
12
+ # s.homepage = "TODO"
13
+ s.summary = "Conekta webhook integration for Rails applications."
14
+ s.description = "Conekta webhook integration for Rails applications."
15
+ s.license = "MIT"
16
+
17
+ s.files = `git ls-files`.split("\n")
18
+ s.test_files = `git ls-files -- test/*`.split("\n")
19
+
20
+ s.add_dependency "activesupport", ">= 4.2.5"
21
+ s.add_dependency "conekta", ">= 2.0.0"
22
+
23
+ s.add_development_dependency "rspec-rails", ">= 3.5"
24
+ s.add_development_dependency "webmock"
25
+ s.add_development_dependency "byebug"
26
+ end
data/config/routes.rb ADDED
@@ -0,0 +1,3 @@
1
+ ConektaEvent::Engine.routes.draw do
2
+ post '/', to: 'webhook#event'
3
+ end
@@ -0,0 +1,5 @@
1
+ module ConektaEvent
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace ConektaEvent
4
+ end
5
+ end
@@ -0,0 +1,3 @@
1
+ module ConektaEvent
2
+ VERSION = "1.0.0"
3
+ end
@@ -0,0 +1,76 @@
1
+ require "active_support/notifications"
2
+ require "conekta"
3
+ require "conekta_event/engine" if defined?(Rails)
4
+
5
+ module ConektaEvent
6
+ class << self
7
+ attr_accessor :adapter, :backend, :event_retriever, :namespace, :private_signature
8
+
9
+ def configure(&block)
10
+ raise ArgumentError, "must provide a block" unless block_given?
11
+ block.arity.zero? ? instance_eval(&block) : yield(self)
12
+ end
13
+ alias :setup :configure
14
+
15
+ def instrument(params)
16
+ begin
17
+ event = event_retriever.call(params)
18
+ rescue Conekta::ErrorList => error_list
19
+ for error_detail in error_list.details do
20
+ Rails.logger.info "Conekta Webhook Error => #{error_detail.message}"
21
+ end
22
+ if params[:type] == "account.application.deauthorized"
23
+ event = OpenStruct.new(params)
24
+ else
25
+ raise UnauthorizedError.new(error_list)
26
+ end
27
+ rescue Conekta::Error => e
28
+ raise UnauthorizedError.new(e)
29
+ end
30
+
31
+ backend.instrument namespace.call(event[:type]), event if event
32
+ end
33
+
34
+ def subscribe(name, callable = Proc.new)
35
+ backend.subscribe namespace.to_regexp(name), adapter.call(callable)
36
+ end
37
+
38
+ def all(callable = Proc.new)
39
+ subscribe nil, callable
40
+ end
41
+
42
+ def listening?(name)
43
+ namespaced_name = namespace.call(name)
44
+ backend.notifier.listening?(namespaced_name)
45
+ end
46
+ end
47
+
48
+ class Namespace < Struct.new(:value, :delimiter)
49
+ def call(name = nil)
50
+ "#{value}#{delimiter}#{name}"
51
+ end
52
+
53
+ def to_regexp(name = nil)
54
+ %r{^#{Regexp.escape call(name)}}
55
+ end
56
+ end
57
+
58
+ class NotificationAdapter < Struct.new(:subscriber)
59
+ def self.call(callable)
60
+ new(callable)
61
+ end
62
+
63
+ def call(*args)
64
+ payload = args.last
65
+ subscriber.call(payload)
66
+ end
67
+ end
68
+
69
+ class Error < StandardError; end
70
+ class UnauthorizedError < Error; end
71
+
72
+ self.adapter = NotificationAdapter
73
+ self.backend = ActiveSupport::Notifications
74
+ self.event_retriever = lambda { |params| Conekta::Event.find(params[:id]) }
75
+ self.namespace = Namespace.new("conekta_event", ".")
76
+ end
@@ -0,0 +1,81 @@
1
+ require 'rails_helper'
2
+
3
+ describe ConektaEvent::WebhookController, type: :controller do
4
+ routes { ConektaEvent::Engine.routes }
5
+
6
+ def stub_event(identifier, status = 200)
7
+ stub_request(:get, "https://api.conekta.io/events/#{identifier}").
8
+ to_return(status: status, body: File.read("spec/support/fixtures/#{identifier}.json"))
9
+ end
10
+
11
+ def webhook(params = {})
12
+ post :event, params: params
13
+ end
14
+
15
+ it "succeeds with valid event data" do
16
+ count = 0
17
+ ConektaEvent.subscribe('charge.paid') { |evt| count += 1 }
18
+ stub_event('evt_charge_paid')
19
+
20
+ webhook id: 'evt_charge_paid'
21
+
22
+ expect(response.code).to eq '200'
23
+ expect(count == 1)
24
+ end
25
+
26
+ it "succeeds when the event_retriever returns nil (simulating an ignored webhook event)" do
27
+ count = 0
28
+ ConektaEvent.event_retriever = lambda { |params| return nil }
29
+ ConektaEvent.subscribe('charge.paid') { |evt| count += 1 }
30
+ stub_event('evt_charge_paid')
31
+
32
+ webhook id: 'evt_charge_paid'
33
+
34
+ expect(response.code).to eq '200'
35
+ expect(count == 0)
36
+ end
37
+
38
+ it "denies access with invalid event data" do
39
+ count = 0
40
+ ConektaEvent.subscribe('charge.paid') { |evt| count += 1 }
41
+ stub_event('evt_invalid_id', 404)
42
+
43
+ webhook id: 'evt_invalid_id'
44
+
45
+ expect(response.code).to eq '401'
46
+ expect(count == 0)
47
+ end
48
+
49
+ context "with an authentication" do
50
+ def webhook_with_digest(digest, params)
51
+ request.env['HTTP_DIGEST'] = digest
52
+ request.env['RAW_POST_DATA'] = "{\"data\":{\"object\":{\"url\":\"http://f4888b71.ngrok.io/conekta/webooks\",\"status\":\"error\",\"production_enabled\":false,\"development_enabled\":true,\"subscribed_events\":[\"charge.created\",\"charge.paid\",\"charge.under_fraud_review\",\"charge.fraudulent\",\"charge.refunded\",\"charge.preauthorized\",\"charge.expired\",\"charge.declined\",\"charge.expired\",\"customer.created\",\"customer.updated\",\"customer.deleted\",\"webhook.created\",\"webhook.updated\",\"webhook.deleted\",\"charge.chargeback.created\",\"charge.chargeback.updated\",\"charge.chargeback.under_review\",\"charge.chargeback.lost\",\"charge.chargeback.won\",\"payout.created\",\"payout.retrying\",\"payout.paid_out\",\"payout.failed\",\"plan.created\",\"plan.updated\",\"plan.deleted\",\"subscription.created\",\"subscription.paused\",\"subscription.resumed\",\"subscription.canceled\",\"subscription.expired\",\"subscription.updated\",\"subscription.paid\",\"subscription.payment_failed\",\"payee.created\",\"payee.updated\",\"payee.deleted\",\"payee.payout_method.created\",\"payee.payout_method.updated\",\"payee.payout_method.deleted\",\"charge.score_updated\",\"order.canceled\",\"order.charged_back\",\"order.created\",\"order.expired\",\"order.fraudulent\",\"order.under_fraud_review\",\"order.paid\",\"order.partially_refunded\",\"order.pending_payment\",\"order.pre_authorized\",\"order.refunded\",\"order.updated\",\"order.voided\"],\"id\":\"592f95deffecf90ad50cbc3b\",\"object\":\"webhook\"},\"previous_attributes\":{}},\"livemode\":false,\"webhook_status\":\"successful\",\"webhook_logs\":[{\"id\":\"webhl_2gd82XRPWokwvFRZU\",\"url\":\"http://f4888b71.ngrok.io/conekta/webooks\",\"failed_attempts\":0,\"last_http_response_status\":-1,\"object\":\"webhook_log\",\"last_attempted_at\":1496412760}],\"id\":\"5931718eb795b0537661ddba\",\"object\":\"event\",\"type\":\"webhook.updated\",\"created_at\":1496412558}"
53
+ webhook params
54
+ end
55
+
56
+ before(:each) { ConektaEvent.private_signature = "-----BEGIN RSA PRIVATE KEY-----\nMIIEpQIBAAKCAQEAzuZQvFcZkzlQnC089JyIJLtq7vLEz+MgjsdQO5docuUdPvBP\ncV3wFxWbX4P9MQq6PHAM5EDhy2SAtIW7CrWV44n6zlEYVl5x5peJqee2Pbef9ROA\nrvP3Ja2q4BgxJnGKKoFbYK/FfpGIp+gDSJxy3NJ/KPtHlq88YjJAhja111N054Xl\nQSxeYvvOHl+G8Xg9G1ZU9U0rQfIEwS4WwcMI8OPoFxb26FJi9XlxlmaJ5c1gHSko\noaVczguCYE+JgoYUe6iQjbCheb3XKKav5/rd9grOWBTWwqVS4rALpJjYXERIBWY7\nUHsisKufSZiuTdiJnsYt+UZcFl7F0cma9xMPmwIDAQABAoIBAQC5e47LmgYqj0pu\nCLxJyv7ed0qhVvEMMeFhPtv14IHZ5v62CvgdeQqhl1RIZ+qXibd2MTnNc0E5dytP\nK0iIjEwIxg0b42W/IEJaaGYY9MrTP4heTJKjxcE+fRfgeK+veEBWZMuHvWx/UHdD\nl+NBuEfdIbSwB72hIA4xNj3UVL3mf6THWSOBkG2veUUOLW+WGFh5tn0sBT49cBVh\nTA5uOATnFnC2UsTtDgt2RK1FZv89x+XPHPzd1BDTDQP7jV1X5PUFunxkl/981PIG\njfXl1LsEvgFwa6feOudBOje0wnlzk5onuj/u6adysq/kNs6piQeF7mdmt8odTsMg\nQyoTi8FJAoGBAPP64IYcO1V/DjbE7TpO5/goZ0p3PehmuCaRsxURUu+7DRbe+M9q\np3iPj2pAMPa59eHDDIqhgcW5Nf5iKYGOJTki3XkZhEWJAiNo7+cT8RGpKuKAyVDD\nWoUFHWB4LCyvJvue8FeUwCJwjik8Uf76INSNcxO4EWHUlMRKhxEpUDlHAoGBANkX\nxi6kFwmP0QFpN62kVlVMCOftvxz9NgM3QrZi7xkUQap+RFpbntOQnCmnZo9zfud9\n5OoeIU3bk5K+bU+1T4G4WXIqoECSyQObHQZYL5ARfW8iqDsXun/e4eRZYI+bVRbp\n83h6wv6yn1I/vQpq69HHEJRRRW1D995D5ntTciENAoGAf3bfXFFdklI529VQVvko\nada5+AaKGmOn68aM+AHAAa0Irp05AiwnaG4gMBNvQUdwNU2QvNCaGvGjSs5//saD\nnfEgIgd5ulZU/qjxRRl/BYoK9KDyDDazkPFWIrNF6OZtCGJGEIuPQa7qJpL0B7En\n+8QWjgPJWQIV4uNI42dhGTsCgYEAmkptTeTNgrw1/Vy8d6reuQyrH7s3IvFLnAmA\nXoP+DsL40KWhCt8nCJI0it4w5C9fuEMfmM0FOoKeZaL1qbrg4P8Wgy+MaZhpSSjK\n/iFa3HexwHTPQABjSlIsFdD38diiJwDrS2tkfwSQezJVtru7EoL6Y49HWpr95Xg4\nrNnnuVkCgYEA8CNTLrgPM9GJGrY5xKxnn0LkwH4KTGKE3S9adfjguKDNnXvLQEK3\np+31QtkeYn0945GboUeP1DmCCSS12GqHHmNEFSEVEaV7dD0E25kZp3Jbvuf1shbi\nGjdgwrU7db7k67Mv9UKH7Pe3ZyZD/IGxfyTpMqkXAk8IxpBfX/Msg2k=\n-----END RSA PRIVATE KEY-----\n" }
57
+ after(:each) { ConektaEvent.private_signature = nil }
58
+
59
+ it "rejects requests with no secret" do
60
+ stub_event('evt_charge_paid')
61
+
62
+ webhook id: 'evt_charge_paid'
63
+ expect(response.code).to eq '401'
64
+ end
65
+
66
+ it "rejects requests with incorrect digest" do
67
+ stub_event('evt_charge_paid')
68
+
69
+ webhook_with_digest 'abcdef12345', id: 'evt_charge_paid'
70
+ expect(response.code).to eq '401'
71
+ end
72
+
73
+ it "accepts requests with correct digest" do
74
+ stub_event('evt_charge_paid')
75
+
76
+ webhook_with_digest "wrfDB6Oje19FRkgyhdjmpNq1e+Nlk3AFT+X20R8kImcZ2QI+MOkh6Bfgn/SDDKcAgmkUwZUKiO6xFRM9rGwgpQKeHj8sFKl3f1Gr6+/GEMsZO8h9QOLWfat2P4Tmj0bm6yvxBdXq4PIoRVuz3Pm4bZKz9wgFRQC7Xkj0v3fufJ6w6a+q/9rIV2FEy4SfcrPK5miFd97hmy6THKtLW2QcB5ykIn79JGwQyYP8Ys8S1gY+zhzxV3ZxDJRUabNejuAKA30RL6F3jvzHJZo7xPjaRvIPIaoHAZ5sBpc+N5UYyopa4jPDZZd5iJrEmo5k4SktQQFq8Uw/+0TzhM64n2X6tQ==", id: 'evt_charge_paid'
77
+ expect(response.code).to eq '200'
78
+ end
79
+ end
80
+
81
+ end
@@ -0,0 +1,6 @@
1
+ # Add your own tasks in files placed in lib/tasks ending in .rake,
2
+ # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
3
+
4
+ require_relative 'config/application'
5
+
6
+ Rails.application.load_tasks
File without changes
File without changes
@@ -0,0 +1,13 @@
1
+ // This is a manifest file that'll be compiled into application.js, which will include all the files
2
+ // listed below.
3
+ //
4
+ // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
5
+ // or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path.
6
+ //
7
+ // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
8
+ // compiled file. JavaScript code in this file should be added after the last require_* statement.
9
+ //
10
+ // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
11
+ // about supported directives.
12
+ //
13
+ //= require_tree .
@@ -0,0 +1,13 @@
1
+ // Action Cable provides the framework to deal with WebSockets in Rails.
2
+ // You can generate new channels where WebSocket features live using the `rails generate channel` command.
3
+ //
4
+ //= require action_cable
5
+ //= require_self
6
+ //= require_tree ./channels
7
+
8
+ (function() {
9
+ this.App || (this.App = {});
10
+
11
+ App.cable = ActionCable.createConsumer();
12
+
13
+ }).call(this);
File without changes
@@ -0,0 +1,15 @@
1
+ /*
2
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
3
+ * listed below.
4
+ *
5
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6
+ * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7
+ *
8
+ * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9
+ * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
10
+ * files in this directory. Styles in this file should be added after the last require_* statement.
11
+ * It is generally better to create a new file per style scope.
12
+ *
13
+ *= require_tree .
14
+ *= require_self
15
+ */
@@ -0,0 +1,4 @@
1
+ module ApplicationCable
2
+ class Channel < ActionCable::Channel::Base
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module ApplicationCable
2
+ class Connection < ActionCable::Connection::Base
3
+ end
4
+ end
@@ -0,0 +1,3 @@
1
+ class ApplicationController < ActionController::Base
2
+ protect_from_forgery with: :exception
3
+ end
File without changes
@@ -0,0 +1,2 @@
1
+ module ApplicationHelper
2
+ end
@@ -0,0 +1,2 @@
1
+ class ApplicationJob < ActiveJob::Base
2
+ end
@@ -0,0 +1,4 @@
1
+ class ApplicationMailer < ActionMailer::Base
2
+ default from: 'from@example.com'
3
+ layout 'mailer'
4
+ end
@@ -0,0 +1,3 @@
1
+ class ApplicationRecord < ActiveRecord::Base
2
+ self.abstract_class = true
3
+ end
File without changes
@@ -0,0 +1,14 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Dummy</title>
5
+ <%= csrf_meta_tags %>
6
+
7
+ <%= stylesheet_link_tag 'application', media: 'all' %>
8
+ <%= javascript_include_tag 'application' %>
9
+ </head>
10
+
11
+ <body>
12
+ <%= yield %>
13
+ </body>
14
+ </html>
@@ -0,0 +1,13 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
5
+ <style>
6
+ /* Email styles need to be inline */
7
+ </style>
8
+ </head>
9
+
10
+ <body>
11
+ <%= yield %>
12
+ </body>
13
+ </html>
@@ -0,0 +1 @@
1
+ <%= yield %>
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
3
+ load Gem.bin_path('bundler', 'bundle')
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ APP_PATH = File.expand_path('../config/application', __dir__)
3
+ require_relative '../config/boot'
4
+ require 'rails/commands'