cangaroo 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (78) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/Rakefile +32 -0
  4. data/app/controllers/cangaroo/application_controller.rb +4 -0
  5. data/app/controllers/cangaroo/endpoint_controller.rb +49 -0
  6. data/app/interactors/cangaroo/authenticate_connection.rb +19 -0
  7. data/app/interactors/cangaroo/count_json_object.rb +20 -0
  8. data/app/interactors/cangaroo/handle_request.rb +10 -0
  9. data/app/interactors/cangaroo/perform_jobs.rb +31 -0
  10. data/app/interactors/cangaroo/validate_json_schema.rb +31 -0
  11. data/app/jobs/cangaroo/job.rb +63 -0
  12. data/app/models/cangaroo/connection.rb +12 -0
  13. data/config/routes.rb +3 -0
  14. data/db/migrate/20151028172151_create_cangaroo_connections.rb +12 -0
  15. data/db/migrate/20151030140821_add_parameters_to_cangaroo_connection.rb +5 -0
  16. data/lib/cangaroo.rb +10 -0
  17. data/lib/cangaroo/engine.rb +17 -0
  18. data/lib/cangaroo/version.rb +3 -0
  19. data/lib/cangaroo/webhook/client.rb +44 -0
  20. data/lib/cangaroo/webhook/error.rb +6 -0
  21. data/lib/tasks/cangaroo_tasks.rake +4 -0
  22. data/spec/controllers/cangaroo/endpoint_controller_spec.rb +72 -0
  23. data/spec/dummy/README.rdoc +28 -0
  24. data/spec/dummy/Rakefile +6 -0
  25. data/spec/dummy/app/assets/javascripts/application.js +13 -0
  26. data/spec/dummy/app/assets/stylesheets/application.css +15 -0
  27. data/spec/dummy/app/controllers/application_controller.rb +5 -0
  28. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  29. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  30. data/spec/dummy/bin/bundle +3 -0
  31. data/spec/dummy/bin/rails +4 -0
  32. data/spec/dummy/bin/rake +4 -0
  33. data/spec/dummy/bin/setup +29 -0
  34. data/spec/dummy/config.ru +4 -0
  35. data/spec/dummy/config/application.rb +31 -0
  36. data/spec/dummy/config/boot.rb +5 -0
  37. data/spec/dummy/config/database.yml +11 -0
  38. data/spec/dummy/config/environment.rb +5 -0
  39. data/spec/dummy/config/environments/development.rb +41 -0
  40. data/spec/dummy/config/environments/production.rb +79 -0
  41. data/spec/dummy/config/environments/test.rb +42 -0
  42. data/spec/dummy/config/initializers/assets.rb +11 -0
  43. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  44. data/spec/dummy/config/initializers/cookies_serializer.rb +3 -0
  45. data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  46. data/spec/dummy/config/initializers/inflections.rb +16 -0
  47. data/spec/dummy/config/initializers/mime_types.rb +4 -0
  48. data/spec/dummy/config/initializers/session_store.rb +3 -0
  49. data/spec/dummy/config/initializers/wrap_parameters.rb +9 -0
  50. data/spec/dummy/config/locales/en.yml +23 -0
  51. data/spec/dummy/config/routes.rb +4 -0
  52. data/spec/dummy/config/secrets.yml +22 -0
  53. data/spec/dummy/db/development.sqlite3 +0 -0
  54. data/spec/dummy/db/schema.rb +29 -0
  55. data/spec/dummy/db/test.sqlite3 +0 -0
  56. data/spec/dummy/log/cangaroo.log +0 -0
  57. data/spec/dummy/log/development.log +3466 -0
  58. data/spec/dummy/log/test.log +139640 -0
  59. data/spec/dummy/public/404.html +67 -0
  60. data/spec/dummy/public/422.html +67 -0
  61. data/spec/dummy/public/500.html +66 -0
  62. data/spec/dummy/public/favicon.ico +0 -0
  63. data/spec/factories/cangaroo_connections.rb +9 -0
  64. data/spec/fixtures/json_payload_no_id.json +21 -0
  65. data/spec/fixtures/json_payload_ok.json +22 -0
  66. data/spec/fixtures/json_payload_wrong_key.json +22 -0
  67. data/spec/interactors/cangaroo/authenticate_connection_spec.rb +46 -0
  68. data/spec/interactors/cangaroo/count_json_object_spec.rb +17 -0
  69. data/spec/interactors/cangaroo/handle_request_spec.rb +18 -0
  70. data/spec/interactors/cangaroo/perform_jobs_spec.rb +42 -0
  71. data/spec/interactors/cangaroo/validate_json_schema_spec.rb +51 -0
  72. data/spec/jobs/cangaroo/job_spec.rb +55 -0
  73. data/spec/lib/cangaroo/webhook/client_spec.rb +69 -0
  74. data/spec/models/cangaroo/connection_spec.rb +15 -0
  75. data/spec/rails_helper.rb +85 -0
  76. data/spec/spec_helper.rb +9 -0
  77. data/spec/support/spec_helpers.rb +11 -0
  78. metadata +345 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 2d7be05c09c24fab1c3762ff2987c408f29171af
4
+ data.tar.gz: 627378f34edd49f3dd92283f736b2b07635057cd
5
+ SHA512:
6
+ metadata.gz: 12591483c6345eb3b72f1d61ebd8f309a7241d10f50699a441536b762276168987171a43735e6e12e7181c230b28770f4848b88f72e308b5e8c293c8f0cbe43e
7
+ data.tar.gz: c46a47b09ca10ca5d4cb562c314c7a93e300ff86f5a6d95061074165b3cbcb64ac459d3eaeb89d525fc030a3e3708b2fb752d168aa494631166ac9eb57d51e1b
@@ -0,0 +1,20 @@
1
+ Copyright 2015 Nebulab
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.
@@ -0,0 +1,32 @@
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 = 'Cangaroo'
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
+ load 'rails/tasks/statistics.rake'
21
+
22
+ Bundler::GemHelper.install_tasks
23
+
24
+ Dir[File.join(File.dirname(__FILE__), 'tasks/**/*.rake')].each { |f| load f }
25
+
26
+ require 'rspec/core'
27
+ require 'rspec/core/rake_task'
28
+
29
+ desc 'Run all specs in spec directory (excluding plugin specs)'
30
+ RSpec::Core::RakeTask.new(spec: 'app:db:test:prepare')
31
+
32
+ task default: :spec
@@ -0,0 +1,4 @@
1
+ module Cangaroo
2
+ class ApplicationController < ActionController::Base
3
+ end
4
+ end
@@ -0,0 +1,49 @@
1
+ require_dependency 'cangaroo/application_controller'
2
+
3
+ module Cangaroo
4
+ class EndpointController < ApplicationController
5
+ before_action :ensure_json_request
6
+ before_action :handle_request
7
+
8
+ rescue_from Exception, with: :handle_error
9
+
10
+ def create
11
+ if @command.success?
12
+ render json: @command.object_count, status: 202
13
+ else
14
+ render json: { error: @command.message },
15
+ status: @command.error_code
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def handle_error
22
+ unless Rails.env.development?
23
+ render json: { error: 'Something went wrong!' }, status: 500
24
+ end
25
+ end
26
+
27
+ def handle_request
28
+ @command = HandleRequest.call(
29
+ key: key,
30
+ token: token,
31
+ json_body: params[:endpoint].to_json,
32
+ jobs: Rails.configuration.cangaroo.jobs
33
+ )
34
+ end
35
+
36
+ def ensure_json_request
37
+ return if request.headers['Content-Type'] == 'application/json'
38
+ render nothing: true, status: 406
39
+ end
40
+
41
+ def key
42
+ request.headers['X-Hub-Store']
43
+ end
44
+
45
+ def token
46
+ request.headers['X-Hub-Access-Token']
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,19 @@
1
+ module Cangaroo
2
+ class AuthenticateConnection
3
+ include Interactor
4
+
5
+ before :prepare_context
6
+
7
+ def call
8
+ context.source_connection ||
9
+ context.fail!(message: 'wrong credentials', error_code: 401)
10
+ end
11
+
12
+ private
13
+
14
+ def prepare_context
15
+ context.source_connection =
16
+ Cangaroo::Connection.authenticate(context.key, context.token)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,20 @@
1
+ module Cangaroo
2
+ class CountJsonObject
3
+ include Interactor
4
+
5
+ before :prepare_context
6
+
7
+ def call
8
+ context.object_count = context.data.inject({}) do |o, (k, v)|
9
+ o[k] = v.size
10
+ o
11
+ end
12
+ end
13
+
14
+ private
15
+
16
+ def prepare_context
17
+ context.data = JSON.parse(context.json_body)
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,10 @@
1
+ module Cangaroo
2
+ class HandleRequest
3
+ include Interactor::Organizer
4
+
5
+ organize Cangaroo::AuthenticateConnection,
6
+ Cangaroo::ValidateJsonSchema,
7
+ Cangaroo::CountJsonObject,
8
+ Cangaroo::PerformJobs
9
+ end
10
+ end
@@ -0,0 +1,31 @@
1
+ module Cangaroo
2
+ class PerformJobs
3
+ include Interactor
4
+
5
+ def call
6
+ data.each do |type, payloads|
7
+ payloads.each { |payload| enqueue_jobs(type, payload) }
8
+ end
9
+ end
10
+
11
+ private
12
+
13
+ def data
14
+ @data ||= JSON.parse(context.json_body)
15
+ end
16
+
17
+ def enqueue_jobs(type, payload)
18
+ initialize_jobs(type, payload).select(&:perform?).each(&:enqueue)
19
+ end
20
+
21
+ def initialize_jobs(type, payload)
22
+ context.jobs.map do |klass|
23
+ klass.new(
24
+ connection: context.source_connection,
25
+ type: type,
26
+ payload: payload
27
+ )
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,31 @@
1
+ module Cangaroo
2
+ class ValidateJsonSchema
3
+ include Interactor
4
+
5
+ SCHEMA = {
6
+ 'type': 'object',
7
+ 'minProperties': 1,
8
+ 'additionalProperties': false,
9
+ 'patternProperties': {
10
+ '^[a-z]*$': {
11
+ 'type': 'array',
12
+ 'minItems': 1,
13
+ 'items': {
14
+ 'type': 'object',
15
+ 'required': ['id'],
16
+ 'properties': {
17
+ 'id': {
18
+ 'type': 'string'
19
+ }
20
+ }
21
+ }
22
+ }
23
+ }
24
+ }.freeze
25
+
26
+ def call
27
+ JSON::Validator.fully_validate(SCHEMA, context.json_body).empty? ||
28
+ context.fail!(message: 'wrong json schema', error_code: 500)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,63 @@
1
+ module Cangaroo
2
+ class Job < ActiveJob::Base
3
+ queue_as :cangaroo
4
+
5
+ class_attribute :connection_name, :webhook_path, :webhook_parameters
6
+ class << self
7
+ def connection(name)
8
+ self.connection_name = name
9
+ end
10
+
11
+ def path(path)
12
+ self.webhook_path = path
13
+ end
14
+
15
+ def parameters(parameters)
16
+ self.webhook_parameters = parameters
17
+ end
18
+ end
19
+
20
+ def perform(*)
21
+ Cangaroo::Webhook::Client.new(destination_connection, path)
22
+ .post(transform, @job_id, parameters)
23
+ end
24
+
25
+ def perform?
26
+ fail NotImplementedError
27
+ end
28
+
29
+ def transform
30
+ { type.singularize => payload }
31
+ end
32
+
33
+ private
34
+
35
+ def source_connection
36
+ arguments.first.fetch(:connection)
37
+ end
38
+
39
+ def type
40
+ arguments.first.fetch(:type)
41
+ end
42
+
43
+ def payload
44
+ arguments.first.fetch(:payload)
45
+ end
46
+
47
+ def destination_connection
48
+ @connection ||= Cangaroo::Connection.find_by!(name: connection_name)
49
+ end
50
+
51
+ def connection_name
52
+ self.class.connection_name
53
+ end
54
+
55
+ def path
56
+ self.class.webhook_path || ''
57
+ end
58
+
59
+ def parameters
60
+ self.class.webhook_parameters || {}
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,12 @@
1
+ module Cangaroo
2
+ class Connection < ActiveRecord::Base
3
+ serialize :parameters
4
+
5
+ validates :name, :url, :key, :token, presence: true
6
+ validates :name, :url, :key, :token, uniqueness: true
7
+
8
+ def self.authenticate(key, token)
9
+ where(key: key, token: token).first
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,3 @@
1
+ Cangaroo::Engine.routes.draw do
2
+ resources :endpoint, only: [:create], constraints: { format: :json }
3
+ end
@@ -0,0 +1,12 @@
1
+ class CreateCangarooConnections < ActiveRecord::Migration
2
+ def change
3
+ create_table :cangaroo_connections do |t|
4
+ t.string :name
5
+ t.string :url
6
+ t.string :key
7
+ t.string :token
8
+
9
+ t.timestamps null: false
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,5 @@
1
+ class AddParametersToCangarooConnection < ActiveRecord::Migration
2
+ def change
3
+ add_column :cangaroo_connections, :parameters, :text, default: {}
4
+ end
5
+ end
@@ -0,0 +1,10 @@
1
+ require 'cangaroo/engine'
2
+ require 'interactor'
3
+ require 'json-schema'
4
+ require 'httparty'
5
+
6
+ require 'cangaroo/webhook/error'
7
+ require 'cangaroo/webhook/client'
8
+
9
+ module Cangaroo
10
+ end
@@ -0,0 +1,17 @@
1
+ module Cangaroo
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace Cangaroo
4
+
5
+ config.generators do |g|
6
+ g.test_framework :rspec, fixture: false
7
+ g.fixture_replacement :factory_girl, dir: 'spec/factories'
8
+ g.assets false
9
+ g.helper false
10
+ end
11
+
12
+ config.before_configuration do
13
+ Rails.configuration.cangaroo = ActiveSupport::OrderedOptions.new
14
+ Rails.configuration.cangaroo.jobs = []
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,3 @@
1
+ module Cangaroo
2
+ VERSION = '1.0.0'
3
+ end
@@ -0,0 +1,44 @@
1
+ module Cangaroo
2
+ module Webhook
3
+ class Client
4
+ include HTTParty
5
+
6
+ format :json
7
+
8
+ attr_accessor :connection, :path
9
+
10
+ def initialize(connection, path)
11
+ @connection = connection
12
+ @path = path
13
+ end
14
+
15
+ def post(payload, request_id, parameters)
16
+ request_body = body(payload, request_id, parameters).to_json
17
+ req = self.class.post(url, headers: headers, body: request_body)
18
+ if req.response.code == '200'
19
+ req.parsed_response
20
+ else
21
+ fail Cangaroo::Webhook::Error, req.parsed_response['summary']
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def url
28
+ URI.parse(
29
+ HTTParty.normalize_base_uri(connection.url)
30
+ ).merge(path.to_s).to_s
31
+ end
32
+
33
+ def headers
34
+ { 'X_HUB_TOKEN' => connection.token }
35
+ end
36
+
37
+ def body(payload, request_id, parameters)
38
+ { request_id: request_id,
39
+ parameters: connection.parameters.deep_merge(parameters)
40
+ }.merge(payload)
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,6 @@
1
+ module Cangaroo
2
+ module Webhook
3
+ class Error < StandardError
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :cangaroo do
3
+ # # Task goes here
4
+ # end
@@ -0,0 +1,72 @@
1
+ require 'rails_helper'
2
+
3
+ module Cangaroo
4
+ RSpec.describe EndpointController, type: :controller do
5
+ routes { Cangaroo::Engine.routes }
6
+ let(:connection) { create :cangaroo_connection }
7
+ let(:request_payload) { JSON.parse(load_fixture('json_payload_ok.json')) }
8
+
9
+ before do
10
+ request.headers['Content-Type'] = 'application/json'
11
+ request.headers['X-Hub-Store'] = connection.key
12
+ request.headers['X-Hub-Access-Token'] = connection.token
13
+ end
14
+
15
+ describe '#create' do
16
+ before do
17
+ post :create, request_payload
18
+ end
19
+
20
+ it 'accepts only application/json requests' do
21
+ expect(response.status).to eq(202)
22
+
23
+ request.headers['Content-Type'] = 'application/text'
24
+ post :create
25
+ expect(response.status).to eq(406)
26
+ end
27
+
28
+ context 'when success' do
29
+ let(:auth_headers) {}
30
+
31
+ it 'responds with 200' do
32
+ expect(response.status).to eq(202)
33
+ end
34
+
35
+ it 'responds with the number of objects received in payload' do
36
+ res = JSON.parse(response.body)
37
+ expect(res).to eq('orders' => 2, 'shipments' => 2)
38
+ end
39
+ end
40
+
41
+ context 'when error' do
42
+ before do
43
+ request.headers['X-Hub-Access-Token'] = 'wrongtoken'
44
+ post :create, request_payload
45
+ end
46
+
47
+ it 'responds with the command error code' do
48
+ expect(response.status).to eq(401)
49
+ end
50
+
51
+ it 'responds with error messages in the body' do
52
+ expect(JSON.parse(response.body)['error']).to be_present
53
+ end
54
+ end
55
+
56
+ context 'when an exception was raised' do
57
+ before do
58
+ HandleRequest.stub(:call).and_raise('An error')
59
+ post :create, request_payload
60
+ end
61
+
62
+ it 'responds with 500' do
63
+ expect(response.status).to eq(500)
64
+ end
65
+
66
+ it 'responds with error messages in the body' do
67
+ expect(JSON.parse(response.body)['error']).to eq 'Something went wrong!'
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end