mno-enterprise-core 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (138) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +1 -0
  3. data/Rakefile +12 -0
  4. data/app/assets/images/mno_enterprise/main-logo.png +0 -0
  5. data/app/controllers/mno_enterprise/application_controller.rb +116 -0
  6. data/app/helpers/mno_enterprise/application_helper.rb +67 -0
  7. data/app/helpers/mno_enterprise/impersonate_helper.rb +27 -0
  8. data/app/models/mno_enterprise/ability.rb +6 -0
  9. data/app/models/mno_enterprise/app.rb +72 -0
  10. data/app/models/mno_enterprise/app_instance.rb +36 -0
  11. data/app/models/mno_enterprise/app_instances_sync.rb +6 -0
  12. data/app/models/mno_enterprise/arrears_situation.rb +6 -0
  13. data/app/models/mno_enterprise/audit_event.rb +21 -0
  14. data/app/models/mno_enterprise/base_resource.rb +228 -0
  15. data/app/models/mno_enterprise/credit_card.rb +40 -0
  16. data/app/models/mno_enterprise/deletion_request.rb +35 -0
  17. data/app/models/mno_enterprise/impac/dashboard.rb +36 -0
  18. data/app/models/mno_enterprise/impac/dashboard_provisioner.rb +5 -0
  19. data/app/models/mno_enterprise/impac/kpi.rb +9 -0
  20. data/app/models/mno_enterprise/impac/widget.rb +13 -0
  21. data/app/models/mno_enterprise/invoice.rb +53 -0
  22. data/app/models/mno_enterprise/org_invite.rb +50 -0
  23. data/app/models/mno_enterprise/organization.rb +33 -0
  24. data/app/models/mno_enterprise/team.rb +50 -0
  25. data/app/models/mno_enterprise/tenant.rb +5 -0
  26. data/app/models/mno_enterprise/tenant_invoice.rb +5 -0
  27. data/app/models/mno_enterprise/user.rb +183 -0
  28. data/app/pdf/mno_enterprise/invoice_pdf.rb +516 -0
  29. data/config/initializers/audit_log.rb +5 -0
  30. data/config/locales/devise.en.yml +60 -0
  31. data/config/routes.rb +2 -0
  32. data/config/styleguide.yml +106 -0
  33. data/lib/accountingjs_serializer.rb +51 -0
  34. data/lib/devise/controllers/extension_helpers.rb +52 -0
  35. data/lib/devise/extension_routes.rb +11 -0
  36. data/lib/devise/hooks/password_expirable.rb +5 -0
  37. data/lib/devise/models/password_expirable.rb +28 -0
  38. data/lib/devise/models/remote_authenticatable.rb +48 -0
  39. data/lib/devise/strategies/remote_authenticatable.rb +44 -0
  40. data/lib/devise_extension.rb +36 -0
  41. data/lib/faraday/adapter/net_http_no_proxy.rb +19 -0
  42. data/lib/generators/mno_enterprise/database_extension/USAGE +11 -0
  43. data/lib/generators/mno_enterprise/database_extension/database_extension_generator.rb +36 -0
  44. data/lib/generators/mno_enterprise/database_extension/templates/model.rb +9 -0
  45. data/lib/generators/mno_enterprise/dummy/dummy_generator.rb +98 -0
  46. data/lib/generators/mno_enterprise/dummy/templates/rails/application.rb.erb +9 -0
  47. data/lib/generators/mno_enterprise/dummy/templates/rails/boot.rb.erb +6 -0
  48. data/lib/generators/mno_enterprise/dummy/templates/rails/database.yml +22 -0
  49. data/lib/generators/mno_enterprise/dummy/templates/rails/routes.rb +8 -0
  50. data/lib/generators/mno_enterprise/dummy/templates/rails/test-env.rb +45 -0
  51. data/lib/generators/mno_enterprise/install/install_generator.rb +140 -0
  52. data/lib/generators/mno_enterprise/install/templates/Procfile +1 -0
  53. data/lib/generators/mno_enterprise/install/templates/config/initializers/mno_enterprise.rb +135 -0
  54. data/lib/generators/mno_enterprise/install/templates/config/mno_enterprise_styleguide.yml +104 -0
  55. data/lib/generators/mno_enterprise/install/templates/javascripts/mno_enterprise_extensions.js +7 -0
  56. data/lib/generators/mno_enterprise/install/templates/stylesheets/main.less_erb +25 -0
  57. data/lib/generators/mno_enterprise/install/templates/stylesheets/theme.less_erb +59 -0
  58. data/lib/generators/mno_enterprise/install/templates/stylesheets/variables.less +337 -0
  59. data/lib/generators/mno_enterprise/install/templates/tasks/sprites.rake +14 -0
  60. data/lib/generators/mno_enterprise/puma_stack/puma_stack_generator.rb +58 -0
  61. data/lib/generators/mno_enterprise/templates/scripts/monit/app-server.conf +8 -0
  62. data/lib/generators/mno_enterprise/templates/scripts/nginx/app +51 -0
  63. data/lib/generators/mno_enterprise/templates/scripts/puma.rb +25 -0
  64. data/lib/generators/mno_enterprise/templates/scripts/setup.sh +27 -0
  65. data/lib/generators/mno_enterprise/templates/scripts/upstart/app-web-hotrestart.conf +26 -0
  66. data/lib/generators/mno_enterprise/templates/scripts/upstart/app-web-server.conf +34 -0
  67. data/lib/generators/mno_enterprise/templates/scripts/upstart/app-web.conf +2 -0
  68. data/lib/generators/mno_enterprise/templates/scripts/upstart/app.conf +11 -0
  69. data/lib/her_extension/her_orm_adapter.rb +54 -0
  70. data/lib/her_extension/middleware/mnoe_api_v1_parse_json.rb +54 -0
  71. data/lib/her_extension/model/associations/association.rb +61 -0
  72. data/lib/her_extension/model/associations/association_proxy.rb +34 -0
  73. data/lib/her_extension/model/associations/has_many_association.rb +115 -0
  74. data/lib/her_extension/model/attributes.rb +43 -0
  75. data/lib/her_extension/model/orm.rb +59 -0
  76. data/lib/her_extension/model/parse.rb +40 -0
  77. data/lib/her_extension/model/relation.rb +92 -0
  78. data/lib/her_extension/validations/remote_uniqueness_validation.rb +33 -0
  79. data/lib/html_processor.rb +106 -0
  80. data/lib/mandrill_client.rb +58 -0
  81. data/lib/mno-enterprise-core.rb +1 -0
  82. data/lib/mno_enterprise/concerns.rb +4 -0
  83. data/lib/mno_enterprise/concerns/controllers.rb +6 -0
  84. data/lib/mno_enterprise/concerns/controllers/angular_csrf.rb +59 -0
  85. data/lib/mno_enterprise/concerns/controllers/auth.rb +9 -0
  86. data/lib/mno_enterprise/concerns/controllers/auth/confirmations_controller.rb +187 -0
  87. data/lib/mno_enterprise/concerns/controllers/auth/passwords_controller.rb +54 -0
  88. data/lib/mno_enterprise/concerns/controllers/auth/registrations_controller.rb +136 -0
  89. data/lib/mno_enterprise/concerns/controllers/auth/sessions_controller.rb +54 -0
  90. data/lib/mno_enterprise/concerns/controllers/auth/unlocks_controller.rb +50 -0
  91. data/lib/mno_enterprise/concerns/models.rb +6 -0
  92. data/lib/mno_enterprise/concerns/models/ability.rb +108 -0
  93. data/lib/mno_enterprise/concerns/models/app_instance.rb +100 -0
  94. data/lib/mno_enterprise/concerns/models/organization.rb +102 -0
  95. data/lib/mno_enterprise/core.rb +279 -0
  96. data/lib/mno_enterprise/database_extendable.rb +57 -0
  97. data/lib/mno_enterprise/engine.rb +33 -0
  98. data/lib/mno_enterprise/testing_support/ability_test_helper.rb +10 -0
  99. data/lib/mno_enterprise/testing_support/common_rake.rb +19 -0
  100. data/lib/mno_enterprise/testing_support/factories.rb +13 -0
  101. data/lib/mno_enterprise/testing_support/factories/app_instances.rb +30 -0
  102. data/lib/mno_enterprise/testing_support/factories/apps.rb +45 -0
  103. data/lib/mno_enterprise/testing_support/factories/arrears_situation.rb +14 -0
  104. data/lib/mno_enterprise/testing_support/factories/audit_event.rb +15 -0
  105. data/lib/mno_enterprise/testing_support/factories/credit_card.rb +33 -0
  106. data/lib/mno_enterprise/testing_support/factories/deletion_request.rb +17 -0
  107. data/lib/mno_enterprise/testing_support/factories/impac/dashboards.rb +15 -0
  108. data/lib/mno_enterprise/testing_support/factories/impac/kpis.rb +20 -0
  109. data/lib/mno_enterprise/testing_support/factories/impac/widgets.rb +15 -0
  110. data/lib/mno_enterprise/testing_support/factories/invoices.rb +51 -0
  111. data/lib/mno_enterprise/testing_support/factories/org_invite.rb +24 -0
  112. data/lib/mno_enterprise/testing_support/factories/organizations.rb +25 -0
  113. data/lib/mno_enterprise/testing_support/factories/team.rb +17 -0
  114. data/lib/mno_enterprise/testing_support/factories/tenant.rb +12 -0
  115. data/lib/mno_enterprise/testing_support/factories/tenant_invoice.rb +29 -0
  116. data/lib/mno_enterprise/testing_support/factories/users.rb +48 -0
  117. data/lib/mno_enterprise/testing_support/jpi_v1_test_helper.rb +49 -0
  118. data/lib/mno_enterprise/testing_support/mno_enterprise_api_test_helper.rb +167 -0
  119. data/lib/mno_enterprise/testing_support/mnoe_faraday_test_adapter.rb +173 -0
  120. data/lib/mno_enterprise/testing_support/organizations_shared_helpers.rb +175 -0
  121. data/lib/mno_enterprise/testing_support/user_action_shared.rb +47 -0
  122. data/lib/mno_enterprise/version.rb +3 -0
  123. data/lib/tasks/mno_enterprise_tasks.rake +22 -0
  124. data/spec/controllers/mno_enterprise/angular_csrf_spec.rb +42 -0
  125. data/spec/lib/her_extension/her_orm_adapter.rb +7 -0
  126. data/spec/lib/her_extension/model/relation_spec.rb +7 -0
  127. data/spec/lib/mandrill_client_spec.rb +64 -0
  128. data/spec/mno_enterprise_spec.rb +79 -0
  129. data/spec/models/mno_enterprise/app_instance_spec.rb +7 -0
  130. data/spec/models/mno_enterprise/app_spec.rb +62 -0
  131. data/spec/models/mno_enterprise/base_resource_spec.rb +28 -0
  132. data/spec/models/mno_enterprise/deletion_request_spec.rb +26 -0
  133. data/spec/models/mno_enterprise/invoice_spec.rb +7 -0
  134. data/spec/models/mno_enterprise/organization_spec.rb +7 -0
  135. data/spec/models/mno_enterprise/user_spec.rb +44 -0
  136. data/spec/rails_helper.rb +73 -0
  137. data/spec/spec_helper.rb +78 -0
  138. metadata +421 -0
@@ -0,0 +1,17 @@
1
+ # Read about factories at https://github.com/thoughtbot/factory_girl
2
+
3
+ FactoryGirl.define do
4
+ factory :mno_enterprise_team, :class => 'MnoEnterprise::Team' do
5
+
6
+ factory :team, class: MnoEnterprise::Team do
7
+ sequence(:id)
8
+ sequence(:name) { |n| "Team#{n}" }
9
+
10
+ created_at 3.days.ago
11
+ updated_at 1.hour.ago
12
+
13
+ # Properly build the resource with Her
14
+ initialize_with { new(attributes).tap { |e| e.clear_attribute_changes! } }
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,12 @@
1
+ FactoryGirl.define do
2
+
3
+ factory :tenant, class: MnoEnterprise::Tenant do
4
+ last_portfolio_amount Money.new(65644,'AUD')
5
+ last_customers_invoicing_amount Money.new(687994,'AUD')
6
+ last_customers_outstanding_amount Money.new(178986,'AUD')
7
+ last_commission_amount Money.new(412345,'AUD')
8
+
9
+ # Properly build the resource with Her
10
+ initialize_with { new(attributes).tap { |e| e.clear_attribute_changes! } }
11
+ end
12
+ end
@@ -0,0 +1,29 @@
1
+ # Read about factories at https://github.com/thoughtbot/factory_girl
2
+
3
+ FactoryGirl.define do
4
+ factory :mno_enterprise_tenant_invoice, :class => 'TenantInvoice' do
5
+
6
+
7
+ factory :tenant_invoice, class: MnoEnterprise::TenantInvoice do
8
+ sequence(:id)
9
+ sequence(:slug) { |n| "201504-NU#{n}" }
10
+ organization_id 265
11
+
12
+ started_at 28.days.ago
13
+ ended_at 3.days.ago
14
+ created_at 3.days.ago
15
+ updated_at 1.hour.ago
16
+ paid_at nil
17
+
18
+ total_commission_amount Money.new(0,'AUD')
19
+ total_portfolio_amount Money.new(0,'AUD')
20
+ non_commissionable_amount Money.new(0,'AUD')
21
+ mno_commission_amount Money.new(0,'AUD')
22
+
23
+ # Properly build the resource with Her
24
+ initialize_with { new(attributes).tap { |e| e.clear_attribute_changes! } }
25
+ end
26
+
27
+
28
+ end
29
+ end
@@ -0,0 +1,48 @@
1
+ # Read about factories at https://github.com/thoughtbot/factory_girl
2
+
3
+ # This is an API resource factory generating a Hash to be used in API stubs
4
+ # Use as such: build(:api_user)
5
+ # See http://stackoverflow.com/questions/10032760/how-to-define-an-array-hash-in-factory-girl
6
+ FactoryGirl.define do
7
+
8
+ factory :user, class: MnoEnterprise::User do
9
+ sequence(:id)
10
+ sequence(:uid) { |n| "usr-fda9#{n}" }
11
+ name "John"
12
+ surname "Doe"
13
+ sequence(:email) { |n| "john.doe#{n}@maestrano.com" }
14
+ company "Doe Inc."
15
+ phone "449 789 456"
16
+ phone_country_code "AU"
17
+ geo_country_code "AU"
18
+ geo_state_code "NSW"
19
+ geo_city "Sydney"
20
+ created_at 2.days.ago
21
+ updated_at 2.days.ago
22
+ sso_session "1fdd5sf5a73D7sd1as2a4sd541"
23
+ admin_role false
24
+
25
+ confirmation_sent_at 2.days.ago
26
+ confirmation_token "wky763pGjtzWR7dP44PD"
27
+ confirmed_at 1.days.ago
28
+
29
+ trait :unconfirmed do
30
+ confirmed_at nil
31
+ end
32
+
33
+ trait :admin do
34
+ admin_role true
35
+ end
36
+
37
+ trait :with_deletion_request do
38
+ deletion_request { build(:deletion_request).attributes }
39
+ end
40
+
41
+ trait :with_organizations do
42
+ organizations { [build(:organization).attributes] }
43
+ end
44
+
45
+ # Properly build the resource with Her
46
+ initialize_with { new(attributes).tap { |e| e.clear_attribute_changes! } }
47
+ end
48
+ end
@@ -0,0 +1,49 @@
1
+ module MnoEnterprise::TestingSupport::JpiV1TestHelper
2
+
3
+ # Expect 'user' to be assigned
4
+ shared_examples "jpi v1 protected action" do
5
+ context "with guest user" do
6
+ it "prevents access" do
7
+ sign_out user
8
+ expect(subject).to_not be_successful
9
+ expect(subject.code).to eq('401')
10
+ end
11
+ end
12
+
13
+ context 'with signed in user' do
14
+ it "authorizes access" do
15
+ sign_in user
16
+ expect(subject).to be_successful
17
+ end
18
+ end
19
+ end
20
+
21
+ # Expect 'user' to be assigned
22
+ # Expect 'ability' to be assigned
23
+ shared_examples "jpi v1 authorizable action" do
24
+ context "with guest user" do
25
+ it "prevents access" do
26
+ sign_out user
27
+ expect(subject).to_not be_successful
28
+ expect(subject.code).to eq('401')
29
+ end
30
+ end
31
+
32
+ context 'with unauthorized signed in user' do
33
+ it "prevents access" do
34
+ sign_in user
35
+ allow(ability).to receive(:can?).with(any_args).and_return(false)
36
+ expect(subject).to_not be_successful
37
+ end
38
+ end
39
+
40
+ context 'with authorized signed in user' do
41
+ it "authorizes access" do
42
+ sign_in user
43
+ allow(ability).to receive(:can?).with(any_args).and_return(true)
44
+ expect(subject).to be_successful
45
+ end
46
+ end
47
+ end
48
+
49
+ end
@@ -0,0 +1,167 @@
1
+ module MnoEnterpriseApiTestHelper
2
+
3
+ # Take a resource and transform it into a Hash describing
4
+ # the resource as if it had been returned by the MnoEnterprise
5
+ # API server
6
+ def from_api(res)
7
+ { data: serialize_type(res), metadata: {pagination: {count: entity_count(res)}} }
8
+ end
9
+
10
+ def serialize_type(res)
11
+ case
12
+ when res.kind_of?(Array)
13
+ return res.map { |e| serialize_type(e) }
14
+ when res.kind_of?(MnoEnterprise::BaseResource)
15
+ hash = res.attributes.dup
16
+ hash.each do |k,v|
17
+ hash[k] = serialize_type(v)
18
+ end
19
+ return hash
20
+ when res.kind_of?(Hash)
21
+ hash = res.dup
22
+ hash.each do |k,v|
23
+ hash[k] = serialize_type(v)
24
+ end
25
+ return hash
26
+ when res.kind_of?(Money)
27
+ return { cents: res.cents, currency: res.currency_as_string }
28
+ when res.respond_to?(:iso8601)
29
+ return res.iso8601
30
+ else
31
+ return res
32
+ end
33
+ end
34
+
35
+ def entity_count(res)
36
+ case
37
+ when res.kind_of?(Array)
38
+ return res.count
39
+ when res.kind_of?(Hash)
40
+ return res.count
41
+ else
42
+ return 1
43
+ end
44
+ end
45
+
46
+ # Reset all API stubs.
47
+ # Called before each test (see spec_helper)
48
+ def api_stub_reset
49
+ @_api_stub = nil
50
+ @_stub_list = {}
51
+ api_stub_configure(Her::API.new)
52
+ end
53
+
54
+ # Example usage:
55
+ #
56
+ # Without opts, it yields a faraday stub object which you can configure
57
+ # manually:
58
+ #
59
+ # You can also pass the response stub via opts
60
+ # api_stub_for(User,
61
+ # path: '/users/popular',
62
+ # response: [{ id: 1, name: "Tobias Fünke" }, { id: 2, name: "Lindsay Fünke" }]
63
+ # )
64
+ #
65
+ # You can also specify the response code:
66
+ # api_stub_for(User,
67
+ # path: '/users/popular',
68
+ # code: 200,
69
+ # response: [{ id: 1, name: "Tobias Fünke" }, { id: 2, name: "Lindsay Fünke" }]
70
+ # )
71
+ def api_stub_for(klass, opts = {})
72
+ real_opts = klass
73
+ if klass.is_a?(Class)
74
+ warn("DEPRECATION WARNING: api_stub_for(MyClass,{ some: 'opts'}) is deprecated. Please use api_stub_for({ some: 'opts' }) from now on")
75
+ real_opts = opts
76
+ end
77
+
78
+ set_api_stub
79
+ api_stub_add(real_opts)
80
+ api_stub_configure(@_api_stub)
81
+ end
82
+
83
+ private
84
+ # Set a stub api on the provider class
85
+ def set_api_stub
86
+ return @_api_stub if @_api_stub
87
+ @_api_stub = Her::API.new
88
+ allow(MnoEnterprise::BaseResource).to receive(:her_api).and_return(@_api_stub = Her::API.new)
89
+ @_api_stub
90
+ end
91
+
92
+ # Add a stub to the api
93
+ # E.g.:
94
+ # {
95
+ # path: '/users/popular',
96
+ # code: 200,
97
+ # response: [{ id: 1, name: "Tobias Fünke" }, { id: 2, name: "Lindsay Fünke" }]
98
+ # }
99
+ def api_stub_add(orig_opts)
100
+ @_stub_list ||= {}
101
+ opts = orig_opts.dup
102
+
103
+ # Expand options so that: { put: '/path' } becomes { path: '/path', method: :put }
104
+ unless opts[:method] && opts[:path]
105
+ [:get,:put,:post,:delete].each do |verb|
106
+ if path = opts.delete(verb)
107
+ opts[:path] = path
108
+ opts[:method] = verb
109
+ end
110
+ end
111
+ end
112
+
113
+ key = opts.to_param
114
+ @_stub_list[key] = opts
115
+ end
116
+
117
+ # Configure the api and apply a list of stubs
118
+ def api_stub_configure(api)
119
+ # This block should match the her.rb initializer
120
+ api.setup MnoEnterprise.send(:api_options).merge(url: "http://localhost:65000") do |c|
121
+ # Request
122
+ c.use Faraday::Request::BasicAuthentication, MnoEnterprise.tenant_id, MnoEnterprise.tenant_key
123
+ c.use Faraday::Request::UrlEncoded
124
+
125
+ # Response
126
+ c.use Her::Middleware::MnoeApiV1ParseJson
127
+
128
+ # Add stubs on the test adapter
129
+ c.use MnoeFaradayTestAdapter do |receiver|
130
+ @_stub_list.each do |key,stub|
131
+ params = stub[:params] && stub[:params].any? ? "?#{stub[:params].to_param}" : ""
132
+ path = "#{stub[:path]}#{params}"
133
+
134
+ receiver.send(stub[:method] || :get,path) { |env|
135
+ body = Rack::Utils.parse_nested_query(env.body)
136
+
137
+ # respond_with takes a model in argument and automatically responds with
138
+ # a json representation of the model
139
+ # If the action is an update, it attempts to update the model
140
+ if model = stub[:respond_with]
141
+ model.assign_attributes(body['data']) if stub[:method] == :put && model.respond_to?(:assign_attributes) && body['data']
142
+ resp = from_api(model)
143
+ else
144
+ if stub[:response].is_a?(Proc)
145
+ args = stub[:response].arity > 0 ? [body] : []
146
+ resp = stub[:response].call(*args)
147
+ else
148
+ resp = stub[:response] || {}
149
+ end
150
+ end
151
+
152
+ # Response code
153
+ if stub[:code].is_a?(Proc)
154
+ args = stub[:code].arity > 0 ? [body] : []
155
+ resp_code = stub[:code].call(*args)
156
+ else
157
+ resp_code = stub[:code] || 200
158
+ end
159
+
160
+
161
+ [resp_code, {}, resp.to_json]
162
+ }
163
+ end
164
+ end
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,173 @@
1
+ # Adapter and modified from: https://github.com/lostisland/faraday/blob/master/lib/faraday/adapter/test.rb
2
+ #
3
+ #
4
+ #
5
+ # test = Faraday::Connection.new do
6
+ # use MnoeFaradayTestAdapter do |stub|
7
+ # stub.get '/nigiri/sake.json' do
8
+ # [200, {}, 'hi world']
9
+ # end
10
+ # end
11
+ # end
12
+ #
13
+ # resp = test.get '/nigiri/sake.json'
14
+ # resp.body # => 'hi world'
15
+ #
16
+ class MnoeFaradayTestAdapter < Faraday::Adapter
17
+ attr_accessor :stubs
18
+
19
+ class Stubs
20
+ class NotFound < StandardError
21
+ end
22
+
23
+ def initialize
24
+ # {:get => [Stub, Stub]}
25
+ @stack, @consumed = {}, {}
26
+ yield(self) if block_given?
27
+ end
28
+
29
+ def empty?
30
+ @stack.empty?
31
+ end
32
+
33
+ def match(request_method, path, headers, body)
34
+ return false if !@stack.key?(request_method)
35
+ stack = @stack[request_method]
36
+ consumed = (@consumed[request_method] ||= [])
37
+
38
+ if stub = matches?(stack, path, headers, body)
39
+ consumed << stack.delete(stub)
40
+ stub
41
+ else
42
+ matches?(consumed, path, headers, body)
43
+ end
44
+ end
45
+
46
+ def get(path, headers = {}, &block)
47
+ new_stub(:get, path, headers, &block)
48
+ end
49
+
50
+ def head(path, headers = {}, &block)
51
+ new_stub(:head, path, headers, &block)
52
+ end
53
+
54
+ def post(path, body=nil, headers = {}, &block)
55
+ new_stub(:post, path, headers, body, &block)
56
+ end
57
+
58
+ def put(path, body=nil, headers = {}, &block)
59
+ new_stub(:put, path, headers, body, &block)
60
+ end
61
+
62
+ def patch(path, body=nil, headers = {}, &block)
63
+ new_stub(:patch, path, headers, body, &block)
64
+ end
65
+
66
+ def delete(path, headers = {}, &block)
67
+ new_stub(:delete, path, headers, &block)
68
+ end
69
+
70
+ def options(path, headers = {}, &block)
71
+ new_stub(:options, path, headers, &block)
72
+ end
73
+
74
+ # Raises an error if any of the stubbed calls have not been made.
75
+ def verify_stubbed_calls
76
+ failed_stubs = []
77
+ @stack.each do |method, stubs|
78
+ unless stubs.size == 0
79
+ failed_stubs.concat(stubs.map {|stub|
80
+ "Expected #{method} #{stub}."
81
+ })
82
+ end
83
+ end
84
+ raise failed_stubs.join(" ") unless failed_stubs.size == 0
85
+ end
86
+
87
+ protected
88
+
89
+ def new_stub(request_method, path, headers = {}, body=nil, &block)
90
+ normalized_path = Faraday::Utils.normalize_path(path)
91
+ (@stack[request_method] ||= []) << Stub.new(normalized_path, headers, body, block)
92
+ end
93
+
94
+ def matches?(stack, path, headers, body)
95
+ stack.detect { |stub| stub.matches?(path, headers, body) }
96
+ end
97
+ end
98
+
99
+ class Stub < Struct.new(:path, :params, :headers, :body, :block)
100
+ def initialize(full, headers, body, block)
101
+ path, query = full.split('?')
102
+ params = query ?
103
+ Faraday::Utils.parse_nested_query(query) :
104
+ {}
105
+ super(path, params, headers, body, block)
106
+ end
107
+
108
+ def matches?(request_uri, request_headers, request_body)
109
+ request_path, request_query = request_uri.split('?')
110
+ request_params = request_query ?
111
+ Faraday::Utils.parse_nested_query(request_query) :
112
+ {}
113
+ request_path == path &&
114
+ params_match?(request_params) &&
115
+ (body.to_s.size.zero? || request_body == body) &&
116
+ headers_match?(request_headers)
117
+ end
118
+
119
+ def params_match?(request_params)
120
+ params_deep_equal?(params,request_params)
121
+ # params.keys.all? do |key|
122
+ # request_params[key] == params[key]
123
+ # end
124
+ end
125
+
126
+ def params_deep_equal?(src,dst)
127
+ src.keys.all? do |key|
128
+ if src[key] && dst[key] && src[key].is_a?(Hash) && dst[key].is_a?(Hash)
129
+ params_deep_equal?(src[key],dst[key])
130
+ else
131
+ (src[key] == '**' && dst[key]) || src[key] == dst[key]
132
+ end
133
+ end
134
+ end
135
+
136
+ def headers_match?(request_headers)
137
+ headers.keys.all? do |key|
138
+ request_headers[key] == headers[key]
139
+ end
140
+ end
141
+
142
+ def to_s
143
+ "#{path} #{body}"
144
+ end
145
+ end
146
+
147
+ def initialize(app, stubs=nil, &block)
148
+ super(app)
149
+ @stubs = stubs || Stubs.new
150
+ configure(&block) if block
151
+ end
152
+
153
+ def configure
154
+ yield(stubs)
155
+ end
156
+
157
+ def call(env)
158
+ super
159
+ normalized_path = Faraday::Utils.normalize_path(env[:url])
160
+ params_encoder = env.request.params_encoder || Faraday::Utils.default_params_encoder
161
+
162
+ if stub = stubs.match(env[:method], normalized_path, env.request_headers, env[:body])
163
+ env[:params] = (query = env[:url].query) ?
164
+ params_encoder.decode(query) :
165
+ {}
166
+ status, headers, body = stub.block.call(env)
167
+ save_response(env, status, body, headers)
168
+ else
169
+ raise Stubs::NotFound, "no stubbed request for #{env[:method]} #{normalized_path} #{env[:body]}"
170
+ end
171
+ @app.call(env)
172
+ end
173
+ end