mailgun-tracking 0.1.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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 9dca3bb2ecebb3b77835850e80c913df78b24c03
4
+ data.tar.gz: 3b468bcfc59e17b6bbad6d92dbc5f7b57d7d2be9
5
+ SHA512:
6
+ metadata.gz: eb8b9b8227712aea6e298118aa98a39d9b82590b611056aa56f914072183b55c98cf53e2f14ceec8215247a690bed332f0a2fbd2eda670cf462c13b75098fd6e
7
+ data.tar.gz: 10652711a22a8d7c8fcee8404cc3dc4524e189cf0ea208211d7e43429bda35d831fb62a58349364d47d8afa8d97ce6c5506a012cb780cd1b0eeddd6fff9fb666
@@ -0,0 +1,45 @@
1
+ require 'mailgun/tracking/exceptions'
2
+ require 'mailgun/tracking/listener'
3
+ require 'mailgun/tracking/rack'
4
+ require 'mailgun/tracking/signature'
5
+ require 'mailgun/tracking/version'
6
+
7
+ module Mailgun
8
+ module Tracking
9
+ DEFAULT_ENDPOINT = '/mailgun'.freeze
10
+
11
+ class << self
12
+ attr_accessor :api_key,
13
+ :endpoint
14
+
15
+ def configure
16
+ yield(self)
17
+ end
18
+ end
19
+
20
+ self.endpoint = DEFAULT_ENDPOINT
21
+
22
+ module_function
23
+
24
+ def subscribe(event, &block)
25
+ listener.add_subscriber(event, &block)
26
+ end
27
+
28
+ def broadcast(event, payload)
29
+ Signature.verify!(payload)
30
+ listener.broadcast(event, payload)
31
+ end
32
+
33
+ def clear_cached_variables
34
+ @listener = nil
35
+ end
36
+
37
+ private
38
+
39
+ module_function
40
+
41
+ def listener
42
+ @listener ||= Listener.new
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,6 @@
1
+ module Mailgun
2
+ module Tracking
3
+ Error = Class.new(StandardError)
4
+ InvalidSignature = Class.new(Error)
5
+ end
6
+ end
@@ -0,0 +1,19 @@
1
+ module Mailgun
2
+ module Tracking
3
+ class Listener
4
+ attr_reader :subscribers
5
+
6
+ def initialize
7
+ @subscribers = Hash.new { |h, k| h[k] = [] }
8
+ end
9
+
10
+ def add_subscriber(event, &block)
11
+ @subscribers[event.to_sym] << block
12
+ end
13
+
14
+ def broadcast(event, payload)
15
+ @subscribers[event.to_sym].each { |subscriber| subscriber.call(payload) }
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,32 @@
1
+ module Mailgun
2
+ module Tracking
3
+ class Rack
4
+ def initialize(app)
5
+ @app = app
6
+ end
7
+
8
+ def call(env)
9
+ @env = env
10
+
11
+ if mailgun_tracking_request?
12
+ Mailgun::Tracking.broadcast(request.params.fetch('event'), request.params)
13
+ [200, {}, []]
14
+ else
15
+ @app.call(env)
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def request
22
+ ::Rack::Request.new(@env)
23
+ end
24
+
25
+ def mailgun_tracking_request?
26
+ return false unless request.post?
27
+ return false unless request.path == Mailgun::Tracking.endpoint
28
+ true
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,33 @@
1
+ require 'openssl'
2
+
3
+ module Mailgun
4
+ module Tracking
5
+ class Signature
6
+ def self.verify!(payload)
7
+ signature = new(payload)
8
+ raise InvalidSignature unless signature.valid?
9
+ true
10
+ end
11
+
12
+ def initialize(payload)
13
+ @token = payload.fetch('token')
14
+ @timestamp = payload.fetch('timestamp')
15
+ @signature = payload.fetch('signature')
16
+ end
17
+
18
+ def valid?
19
+ @signature == OpenSSL::HMAC.hexdigest(digest, Mailgun::Tracking.api_key, data)
20
+ end
21
+
22
+ private
23
+
24
+ def digest
25
+ OpenSSL::Digest::SHA256.new
26
+ end
27
+
28
+ def data
29
+ [@timestamp, @token].join
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,5 @@
1
+ module Mailgun
2
+ module Tracking
3
+ VERSION = '0.1.0'.freeze
4
+ end
5
+ end
@@ -0,0 +1,8 @@
1
+ {
2
+ "domain": "mail.example.com",
3
+ "Message-Id": "<20160329071939.35138.9413.6915422C@mail.example.com>",
4
+ "event": "delivered",
5
+ "timestamp": "1499697910",
6
+ "token": "b5751a49a024483da8d41c3684f98b8f",
7
+ "signature": "374e0b1a3deeb57318c783d43ff71093fbf26406a452761dab91bf346a93b49e"
8
+ }
@@ -0,0 +1,42 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Mailgun::Tracking::Listener do
4
+ subject(:listener) { described_class.new }
5
+
6
+ let(:block) { proc {} }
7
+
8
+ describe '#add_subscriber' do
9
+ it 'adds subscriber' do
10
+ expect { listener.add_subscriber(:delivered, &block) }.to change(listener, :subscribers)
11
+ .from({})
12
+ .to(delivered: [block])
13
+ end
14
+
15
+ it 'adds multiple subscribers with the same name' do
16
+ expect do
17
+ listener.add_subscriber(:delivered, &block)
18
+ listener.add_subscriber('delivered', &block)
19
+ end.to change(listener, :subscribers).from({}).to(delivered: [block, block])
20
+ end
21
+ end
22
+
23
+ describe '#broadcast' do
24
+ let(:payload) { fixture('delivered.json') }
25
+
26
+ before do
27
+ allow(block).to receive(:call)
28
+ listener.add_subscriber(:delivered, &block)
29
+ end
30
+
31
+ it 'executes block' do
32
+ listener.broadcast(:delivered, payload)
33
+ expect(block).to have_received(:call).with(payload)
34
+ end
35
+
36
+ it 'executes multiple blocks' do
37
+ listener.add_subscriber(:delivered, &block)
38
+ listener.broadcast('delivered', payload)
39
+ expect(block).to have_received(:call).with(payload).twice
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,44 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Mailgun::Tracking::Rack do
4
+ subject(:rack) { described_class.new(app) }
5
+
6
+ describe '#call' do
7
+ let(:app) { proc { [200, {}, []] } }
8
+ let(:payload) { fixture('delivered.json') }
9
+ let(:code) { rack.call(env)[0] }
10
+
11
+ before { allow(Mailgun::Tracking).to receive(:broadcast) }
12
+
13
+ context 'when a request to a endpoint with a POST method' do
14
+ let(:env) { env_for('http://localhost:3000/mailgun', method: :post, params: payload) }
15
+
16
+ it { expect(code).to eq(200) }
17
+ it do
18
+ code
19
+ expect(Mailgun::Tracking).to have_received(:broadcast).with('delivered', payload)
20
+ end
21
+ end
22
+
23
+ context 'when a request to a endpoint with a POST method' do
24
+ let(:env) { env_for('http://localhost:3000/mailgun', method: :get, params: payload) }
25
+
26
+ it { expect(code).to eq(200) }
27
+ it do
28
+ code
29
+ expect(Mailgun::Tracking).not_to have_received(:broadcast)
30
+ end
31
+ end
32
+
33
+ context 'when the request is not to the endpoint' do
34
+ let(:env) { env_for('http://localhost:3000') }
35
+ let(:code) { rack.call(env)[0] }
36
+
37
+ it { expect(code).to eq(200) }
38
+ it do
39
+ code
40
+ expect(Mailgun::Tracking).not_to have_received(:broadcast)
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,31 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Mailgun::Tracking::Signature do
4
+ subject(:signature) { described_class.new(payload) }
5
+
6
+ let(:payload) { fixture('delivered.json') }
7
+
8
+ describe '.verify!' do
9
+ context 'when the signature comparison is successful' do
10
+ it { expect(described_class.verify!(payload)).to be true }
11
+ end
12
+
13
+ context 'when the signature comparison is unsuccessful' do
14
+ before { payload.merge!('timestamp' => '') }
15
+
16
+ it { expect { described_class.verify!(payload) }.to raise_error(Mailgun::Tracking::InvalidSignature) }
17
+ end
18
+ end
19
+
20
+ describe '#valid?' do
21
+ context 'when the signature comparison is successful' do
22
+ it { is_expected.to be_valid }
23
+ end
24
+
25
+ context 'when the signature comparison is unsuccessful' do
26
+ before { payload.merge!('timestamp' => '') }
27
+
28
+ it { is_expected.not_to be_valid }
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,49 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Mailgun::Tracking do
4
+ describe '.configure' do
5
+ it 'setup block yields self' do
6
+ described_class.configure do |config|
7
+ expect(described_class).to eq(config)
8
+ end
9
+ end
10
+ end
11
+
12
+ describe '.subscribe' do
13
+ let(:listener) { instance_double(Mailgun::Tracking::Listener) }
14
+
15
+ before do
16
+ described_class.clear_cached_variables
17
+ allow(listener).to receive(:add_subscriber)
18
+ allow(Mailgun::Tracking::Listener).to receive(:new) { listener }
19
+ end
20
+
21
+ it 'subscribes on event' do
22
+ described_class.subscribe(:delivered) {}
23
+
24
+ expect(listener).to have_received(:add_subscriber)
25
+ .with(:delivered)
26
+ end
27
+ end
28
+
29
+ describe '.broadcast' do
30
+ let(:payload) { fixture('delivered.json') }
31
+ let(:listener) { instance_double(Mailgun::Tracking::Listener) }
32
+
33
+ before do
34
+ described_class.clear_cached_variables
35
+ allow(Mailgun::Tracking::Signature).to receive(:verify!)
36
+ allow(listener).to receive(:broadcast)
37
+ allow(Mailgun::Tracking::Listener).to receive(:new) { listener }
38
+ described_class.broadcast(:delivered, payload)
39
+ end
40
+
41
+ it 'verify signature' do
42
+ expect(Mailgun::Tracking::Signature).to have_received(:verify!).with(payload)
43
+ end
44
+
45
+ it 'broadcasts an event' do
46
+ expect(listener).to have_received(:broadcast).with(:delivered, payload)
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,23 @@
1
+ require 'rack'
2
+ require 'simplecov'
3
+ require 'bundler/setup'
4
+ require 'mailgun/tracking'
5
+
6
+ require_relative 'support/fixture'
7
+ require_relative 'support/rack_helpers'
8
+
9
+ SimpleCov.start
10
+
11
+ Mailgun::Tracking.configure do |config|
12
+ config.api_key = 'key-qblubkqnkdn4lfes5oscf57ryllaia42'
13
+ end
14
+
15
+ RSpec.configure do |config|
16
+ config.include(RackHelpers)
17
+
18
+ config.expect_with :rspec do |c|
19
+ c.syntax = :expect
20
+ end
21
+
22
+ config.order = :random
23
+ end
@@ -0,0 +1,17 @@
1
+ require 'json'
2
+
3
+ class FixtureFinder
4
+ FIXTURE_PATH = File.expand_path('../fixtures', __dir__)
5
+
6
+ def initialize(file)
7
+ @file = file
8
+ end
9
+
10
+ def find
11
+ File.read(FIXTURE_PATH + '/' + @file)
12
+ end
13
+ end
14
+
15
+ def fixture(file)
16
+ JSON.parse(FixtureFinder.new(file).find)
17
+ end
@@ -0,0 +1,5 @@
1
+ module RackHelpers
2
+ def env_for(url, options = {})
3
+ Rack::MockRequest.env_for(url, options)
4
+ end
5
+ end
metadata ADDED
@@ -0,0 +1,66 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mailgun-tracking
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Artem Chubchenko
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-07-14 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Integration for Mailgun Webhooks
14
+ email:
15
+ - artem.chubchenko@gmail.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - lib/mailgun/tracking.rb
21
+ - lib/mailgun/tracking/exceptions.rb
22
+ - lib/mailgun/tracking/listener.rb
23
+ - lib/mailgun/tracking/rack.rb
24
+ - lib/mailgun/tracking/signature.rb
25
+ - lib/mailgun/tracking/version.rb
26
+ - spec/fixtures/delivered.json
27
+ - spec/mailgun/tracking/listener_spec.rb
28
+ - spec/mailgun/tracking/rack_spec.rb
29
+ - spec/mailgun/tracking/signature_spec.rb
30
+ - spec/mailgun/tracking_spec.rb
31
+ - spec/spec_helper.rb
32
+ - spec/support/fixture.rb
33
+ - spec/support/rack_helpers.rb
34
+ homepage: https://github.com/chubchenko/mailgun-tracking
35
+ licenses:
36
+ - MIT
37
+ metadata: {}
38
+ post_install_message:
39
+ rdoc_options: []
40
+ require_paths:
41
+ - lib
42
+ required_ruby_version: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: 2.0.0
47
+ required_rubygems_version: !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ version: '0'
52
+ requirements: []
53
+ rubyforge_project:
54
+ rubygems_version: 2.5.1
55
+ signing_key:
56
+ specification_version: 4
57
+ summary: Integration for Mailgun Webhooks
58
+ test_files:
59
+ - spec/fixtures/delivered.json
60
+ - spec/spec_helper.rb
61
+ - spec/mailgun/tracking_spec.rb
62
+ - spec/mailgun/tracking/listener_spec.rb
63
+ - spec/mailgun/tracking/rack_spec.rb
64
+ - spec/mailgun/tracking/signature_spec.rb
65
+ - spec/support/fixture.rb
66
+ - spec/support/rack_helpers.rb