insales_api 0.0.13 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +6 -0
  3. data/CHANGELOG.md +20 -0
  4. data/Gemfile +1 -1
  5. data/README.md +57 -0
  6. data/Rakefile +8 -1
  7. data/insales_api.gemspec +12 -12
  8. data/lib/insales/controller/autologin.rb +45 -0
  9. data/lib/insales/controller/base_helpers.rb +52 -0
  10. data/lib/insales/controller/installer_actions.rb +19 -0
  11. data/lib/insales/controller/session_actions.rb +39 -0
  12. data/lib/insales/controller.rb +10 -0
  13. data/lib/insales.rb +7 -0
  14. data/lib/insales_api/account.rb +1 -9
  15. data/lib/insales_api/active_resource_proxy.rb +31 -0
  16. data/lib/insales_api/app.rb +57 -38
  17. data/lib/insales_api/application_charge.rb +3 -0
  18. data/lib/insales_api/asset.rb +3 -1
  19. data/lib/insales_api/base.rb +41 -5
  20. data/lib/insales_api/category.rb +8 -2
  21. data/lib/insales_api/client.rb +3 -1
  22. data/lib/insales_api/client_group.rb +3 -0
  23. data/lib/insales_api/collect.rb +8 -2
  24. data/lib/insales_api/discount_code.rb +3 -0
  25. data/lib/insales_api/helpers/has_insales_object.rb +34 -0
  26. data/lib/insales_api/helpers/init_api.rb +98 -0
  27. data/lib/insales_api/image.rb +5 -0
  28. data/lib/insales_api/order.rb +13 -5
  29. data/lib/insales_api/order_line.rb +2 -2
  30. data/lib/insales_api/product.rb +3 -1
  31. data/lib/insales_api/product_field.rb +3 -0
  32. data/lib/insales_api/product_field_value.rb +22 -0
  33. data/lib/insales_api/property.rb +3 -0
  34. data/lib/insales_api/resource/countable.rb +9 -0
  35. data/lib/insales_api/resource/paginated.rb +26 -0
  36. data/lib/insales_api/resource/with_updated_since.rb +28 -0
  37. data/lib/insales_api/theme.rb +7 -1
  38. data/lib/insales_api/variant.rb +18 -6
  39. data/lib/insales_api/version.rb +7 -1
  40. data/lib/insales_api.rb +74 -17
  41. data/spec/lib/insales_api/active_resource_proxy_spec.rb +59 -0
  42. data/spec/lib/insales_api/app_spec.rb +67 -0
  43. data/spec/lib/insales_api_spec.rb +110 -0
  44. data/spec/spec_helper.rb +3 -4
  45. metadata +54 -20
  46. data/README +0 -32
  47. data/spec/lib/insales_app_spec.rb +0 -63
@@ -0,0 +1,98 @@
1
+ module InsalesApi::Helpers
2
+ module InitApi
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ class_attribute :insales_app_class
7
+ self.insales_app_class = InsalesApi::App
8
+ end
9
+
10
+ module ClassMethods
11
+ # Wraps methods into +init_api+ block. So you can be sure that method
12
+ # will run for certain account.
13
+ #
14
+ # class Account < ActiveRecord::Base
15
+ # include InsalesApi::Helpers
16
+ #
17
+ # def find_products_by_name(name)
18
+ # # ...
19
+ # end
20
+ #
21
+ # init_api_for :find_products_by_name
22
+ # end
23
+ #
24
+ # account1 = Account.find(1)
25
+ # account2 = Account.find(2)
26
+ #
27
+ # products1 = account1.find_products_by_name('smth')
28
+ # products2 = account2.find_products_by_name('smth_else')
29
+ #
30
+ # # instead of
31
+ # # products1 = account1.init_api { find_products_by_name('smth') }
32
+ # # products2 = account2.init_api { find_products_by_name('smth_else') }
33
+ #
34
+ # Can be used in nessted classes like this:
35
+ #
36
+ # class Order < ActiveRecord::Base
37
+ # extend InsalesApi::Helpers::ClassMethods
38
+ #
39
+ # belongs_to :account
40
+ #
41
+ # delegate :init_api, to: :account
42
+ #
43
+ # def insales_order
44
+ # InsalesApi::Order.find(insales_id)
45
+ # end
46
+ #
47
+ # init_api_for :insales_order
48
+ # end
49
+ #
50
+ # insales_order = Order.first.insales_order
51
+ #
52
+ def init_api_for(*methods)
53
+ file, line = caller.first.split(':', 2)
54
+ methods.each do |method|
55
+ alias_method_chain method, :init_api do |target, punct|
56
+ class_eval <<-RUBY, file, line.to_i - 1
57
+ def #{target}_with_init_api#{punct}(*args, &block) # def sell_with_init_api(*args, &block)
58
+ init_api { #{target}_without_init_api#{punct}(*args, &block) } # init_api { sell_without_init_api(*args, &block) }
59
+ end # end
60
+ RUBY
61
+ end
62
+ end
63
+ end
64
+ end
65
+
66
+ # Configures api with credentials taken from +self.insales_domain+ and
67
+ # +self.insales_password+.
68
+ #
69
+ # If block is given, it is evaluated and its result is returned.
70
+ # After this old configuration is restored.
71
+ #
72
+ # account1 = Account.find(1)
73
+ # account2 = Account.find(2)
74
+ #
75
+ # account1.init_api
76
+ # # account1 credentials are used
77
+ # product1 = InsalesApi::Product.find(1)
78
+ # # will search within second account
79
+ # product2 = account2.init_api { InsalesApi::Product.find(2) }
80
+ # # configuration is restored
81
+ # variant1 = InsalesApi::Variants.find(1)
82
+ #
83
+ def init_api
84
+ if block_given?
85
+ old_config = insales_app_class.dump_config
86
+ begin
87
+ init_api
88
+ yield
89
+ ensure
90
+ insales_app_class.restore_config(old_config)
91
+ end
92
+ else
93
+ insales_app_class.configure_api(insales_domain, insales_password)
94
+ self
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,5 @@
1
+ module InsalesApi
2
+ class Image < Base
3
+ self.prefix = "#{prefix}products/:product_id/"
4
+ end
5
+ end
@@ -1,15 +1,23 @@
1
+ # coding: utf-8
1
2
  module InsalesApi
2
3
  class Order < Base
4
+ extend Resource::WithUpdatedSince
5
+
3
6
  def order_lines_attributes
4
- @order_lines_attributes = []
5
- order_lines.each do |order_line|
6
- @order_lines_attributes << order_line.as_json['order_line']
7
+ @order_lines_attributes = order_lines.map do |order_line|
8
+ ol = order_line.as_json
9
+ # при смене версии рельсов (видимо) изменилась сериализация
10
+ ol = ol['order_line'] if ol['order_line']
11
+ ol
7
12
  end
8
- @order_lines_attributes
9
13
  end
10
14
 
11
15
  def to_xml(options = {})
12
- super(options.merge({:methods => :order_lines_attributes}))
16
+ super(options.merge(methods: :order_lines_attributes))
17
+ end
18
+
19
+ def paid?
20
+ financial_status == 'paid'
13
21
  end
14
22
  end
15
23
  end
@@ -1,5 +1,5 @@
1
1
  module InsalesApi
2
2
  class OrderLine < Base
3
- self.prefix = '/admin/orders/:order_id/'
3
+ self.prefix = "#{prefix}orders/:order_id/"
4
4
  end
5
- end
5
+ end
@@ -1,3 +1,5 @@
1
1
  module InsalesApi
2
- class Product < Base; end
2
+ class Product < Base
3
+ extend Resource::WithUpdatedSince
4
+ end
3
5
  end
@@ -0,0 +1,3 @@
1
+ module InsalesApi
2
+ class ProductField < Base; end
3
+ end
@@ -0,0 +1,22 @@
1
+ module InsalesApi
2
+ class ProductFieldValue < Base
3
+ self.prefix = "#{prefix}products/:product_id/"
4
+
5
+ class << self
6
+ def find_by_field_id(params)
7
+ field_id = params[:product_field_id]
8
+ all(params: {product_id: params[:product_id]}).
9
+ find { |x| x.product_field_id == field_id }
10
+ end
11
+
12
+ def create_or_update(params)
13
+ if value = find_by_field_id(params)
14
+ value.update_attribute(:value, params[:value])
15
+ value
16
+ else
17
+ create(params)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,3 @@
1
+ module InsalesApi
2
+ class Property < Base; end
3
+ end
@@ -0,0 +1,9 @@
1
+ module InsalesApi
2
+ module Resource
3
+ module Countable
4
+ def count(options = {})
5
+ get(:count, options).to_i
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,26 @@
1
+ module InsalesApi
2
+ PER_PAGE_DEFAULT = 100
3
+
4
+ module Resource
5
+ module Paginated
6
+ def find_each(*args)
7
+ find_in_batches(*args) do |batch|
8
+ batch.each { |record| yield record }
9
+ end
10
+ end
11
+
12
+ def find_in_batches(options = {})
13
+ per_page = options[:per_page] || PER_PAGE_DEFAULT
14
+ params = {per_page: per_page}.merge(options[:params] || {})
15
+ page = 1
16
+ loop do
17
+ items = all(params: params.merge(page: page))
18
+ return unless items.any?
19
+ yield items
20
+ return if items.count < per_page
21
+ page += 1
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,28 @@
1
+ module InsalesApi
2
+ module Resource
3
+ module WithUpdatedSince
4
+ def find_in_batches(options = {}, &block)
5
+ return super unless updated_since = options.delete(:updated_since)
6
+ find_updated_since(updated_since, options, &block)
7
+ end
8
+
9
+ def find_updated_since(updated_since, options = {})
10
+ per_page = options[:per_page] || PER_PAGE_DEFAULT
11
+ params = { per_page: per_page }.merge(options[:params] || {})
12
+ last_id = nil
13
+ loop do
14
+ items = all(params: params.merge(
15
+ updated_since: updated_since,
16
+ from_id: last_id,
17
+ ))
18
+ return unless items.any?
19
+ yield items
20
+ return if items.count < per_page
21
+ last_item = items.last
22
+ last_id = last_item.id
23
+ updated_since = last_item.updated_at
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -1,3 +1,9 @@
1
1
  module InsalesApi
2
- class Theme < Base; end
2
+ class Theme < Base
3
+
4
+ def assets
5
+ InsalesApi::Asset.all(params: {theme_id: id})
6
+ end
7
+
8
+ end
3
9
  end
@@ -1,11 +1,23 @@
1
1
  module InsalesApi
2
2
  class Variant < Base
3
- self.prefix = "/admin/products/:product_id/"
4
-
5
- # variants_attrs - массив c модицикациями в формате [{:id => 1, :price => 340, :quantity => 4}, {:id => 2, :price => 350, :quantity => 5}]
6
- def self.group_update variants_attrs
7
- connection.put("/admin/products/variants_group_update.xml", { :variants => variants_attrs }.to_xml, headers)
3
+ class << self
4
+ # Updates all given variants. +variants+ should be array:
5
+ #
6
+ # [
7
+ # {
8
+ # id: 1,
9
+ # price: 340,
10
+ # quantity: 4,
11
+ # },
12
+ # {
13
+ # id: 2,
14
+ # price: 350,
15
+ # quantity: 5,
16
+ # },
17
+ # ]
18
+ def group_update(variants)
19
+ put(:group_update, {}, format.encode(variants, root: :variants))
20
+ end
8
21
  end
9
-
10
22
  end
11
23
  end
@@ -1,3 +1,9 @@
1
1
  module InsalesApi
2
- VERSION = "0.0.13"
2
+ module VERSION
3
+ MAJOR = 0
4
+ MINOR = 1
5
+ TINY = 0
6
+
7
+ STRING = [MAJOR, MINOR, TINY].compact.join('.')
8
+ end
3
9
  end
data/lib/insales_api.rb CHANGED
@@ -1,40 +1,97 @@
1
+ require 'digest/md5'
2
+ require 'active_support'
1
3
  require 'active_support/core_ext'
2
4
  require 'active_resource'
3
5
  # backport from 4.0
4
6
  require 'active_resource/singleton' unless ActiveResource.const_defined?(:Singleton, false)
5
- require 'digest/md5'
6
7
 
7
8
  module InsalesApi
8
9
  extend ActiveSupport::Autoload
9
10
 
11
+ Deprecator = ActiveSupport::Deprecation.new('1.0', name)
12
+
10
13
  eager_autoload do
11
- autoload :Version
12
- autoload :App
14
+ autoload :VERSION
15
+ autoload :Base
13
16
  autoload :Password
17
+ autoload :App
14
18
 
15
- autoload :Base
16
19
  autoload :Account
20
+ autoload :ApplicationCharge
21
+ autoload :ApplicationWidget
22
+ autoload :Asset
17
23
  autoload :Category
18
24
  autoload :Client
19
- autoload :Collection
25
+ autoload :ClientGroup
20
26
  autoload :Collect
27
+ autoload :Collection
28
+ autoload :DeliveryVariant
29
+ autoload :DiscountCode
30
+ autoload :Domain
31
+ autoload :Field
32
+ autoload :File
33
+ autoload :Image
34
+ autoload :JsTag
21
35
  autoload :OptionName
22
36
  autoload :OptionValue
23
- autoload :Product
24
- autoload :Variant
25
- autoload :Webhook
26
37
  autoload :Order
27
38
  autoload :OrderLine
28
- autoload :ApplicationWidget
29
- autoload :Field
30
- autoload :DeliveryVariant
31
- autoload :PaymentGateway
32
- autoload :JsTag
33
- autoload :Domain
34
39
  autoload :Page
35
- autoload :Theme
36
- autoload :Asset
40
+ autoload :PaymentGateway
41
+ autoload :Product
42
+ autoload :ProductField
43
+ autoload :ProductFieldValue
44
+ autoload :Property
37
45
  autoload :RecurringApplicationCharge
38
- autoload :File
46
+ autoload :Theme
47
+ autoload :Variant
48
+ autoload :Webhook
39
49
  end
50
+
51
+ class << self
52
+ # Calls the supplied block. If the block raises <tt>ActiveResource::ServerError</tt> with 503
53
+ # code which means Insales API request limit is reached, it will wait for the amount of seconds
54
+ # specified in 'Retry-After' response header. The called block will receive a parameter with
55
+ # current attempt number.
56
+ #
57
+ # ==== Params:
58
+ #
59
+ # +max_attempts+:: maximum number of attempts. Defaults to +nil+ (unlimited).
60
+ # +callback+:: +Proc+ or lambda to execute before waiting. Will receive four arguments: number
61
+ # of seconds we are going to wait, number of failed attempts, maximum number of
62
+ # attempts and the caught <tt>ActiveResource::ServerError</tt>. Defaults to +nil+
63
+ # (no callback).
64
+ #
65
+ # ==== Example
66
+ #
67
+ # notify_user = Proc.new do |wait_for, attempt, max_attempts, ex|
68
+ # puts "API limit reached. Waiting for #{wait_for} seconds. Attempt #{attempt}/#{max_attempts}"
69
+ # end
70
+ #
71
+ # InsalesApi.wait_retry(10, notify_user) do |x|
72
+ # puts "Attempt ##{x}."
73
+ # products = InsalesApi::Products.all
74
+ # end
75
+ def wait_retry(max_attempts = nil, callback = nil, &block)
76
+ attempts = 0
77
+
78
+ begin
79
+ attempts += 1
80
+ yield attempts
81
+ rescue ActiveResource::ServerError => ex
82
+ raise ex if '503' != ex.response.code.to_s
83
+ raise ex if max_attempts && attempts >= max_attempts
84
+ retry_after = (ex.response['Retry-After'] || 150).to_i
85
+ callback.call(retry_after, attempts, max_attempts, ex) if callback
86
+ sleep(retry_after)
87
+ retry
88
+ end
89
+ end
90
+ end
91
+
40
92
  end
93
+
94
+ require 'insales_api/helpers/init_api'
95
+ require 'insales_api/helpers/has_insales_object'
96
+
97
+ ActiveSupport.run_load_hooks(:insales_api, InsalesApi)
@@ -0,0 +1,59 @@
1
+ require 'spec_helper'
2
+
3
+ describe InsalesApi::ActiveResourceProxy do
4
+ let(:proxy) { described_class.new(configurer, object) }
5
+ let(:configurer) { Object.new.tap { |x| x.stub(:init_api).and_yield } }
6
+
7
+ describe '#method_missing' do
8
+ let(:object) { {} }
9
+ let(:method_name) { :some_method }
10
+ subject { proxy.send(method_name) }
11
+
12
+ it 'should proxy method to object & pass result through #proxy_for' do
13
+ result1 = {test: :resut1}
14
+ result2 = {test: :resut2}
15
+ object.should_receive(method_name).and_return(result1)
16
+ proxy.should_receive(:proxy_for).with(result1).and_return(result2)
17
+ subject.should eq(result2)
18
+ end
19
+ end
20
+
21
+ describe '::need_proxy?' do
22
+ subject { described_class.need_proxy?(object) }
23
+
24
+ context 'for scalar' do
25
+ let(:object) { true }
26
+ it { should be false }
27
+ end
28
+
29
+ context 'for array' do
30
+ let(:object) { [] }
31
+ it { should be true }
32
+ end
33
+
34
+ context 'for hash' do
35
+ let(:object) { {} }
36
+ it { should be true }
37
+ end
38
+
39
+ context 'for InsalesApi::Base class' do
40
+ let(:object) { InsalesApi::Account }
41
+ it { should be true }
42
+ end
43
+
44
+ context 'for InsalesApi::Base object' do
45
+ let(:object) { InsalesApi::Account.new }
46
+ it { should be true }
47
+ end
48
+
49
+ context 'for ActiveResource::Collection object' do
50
+ let(:object) { ActiveResource::Collection.new }
51
+ it { should be true }
52
+ end
53
+
54
+ context 'for ActiveResource::Base object' do
55
+ let(:object) { ActiveResource::Base.new }
56
+ it { should be false }
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,67 @@
1
+ require 'spec_helper'
2
+
3
+ describe InsalesApi::App do
4
+ let(:domain) { 'my.shop.com' }
5
+ let(:password) { 'password' }
6
+ let(:app) { InsalesApi::App.new(domain, password) }
7
+
8
+ subject { app }
9
+
10
+ describe '#initialize' do
11
+ its(:domain) { should eq(domain) }
12
+ its(:password) { should eq(password) }
13
+ its(:authorized?) { should be false }
14
+ end
15
+
16
+ describe '#configure_api' do
17
+ it 'configures InsalesApi::Base' do
18
+ InsalesApi::Base.should_receive(:configure).with(described_class.api_key, domain, password)
19
+ app.configure_api
20
+ end
21
+ end
22
+
23
+ describe '#salt' do
24
+ its(:salt) { should be }
25
+ end
26
+
27
+ describe '#auth_token' do
28
+ its(:auth_token) { should eq(InsalesApi::Password.create(app.password, app.salt)) }
29
+ end
30
+
31
+ describe 'authorization_url' do
32
+ subject { app.authorization_url }
33
+ let(:expected) do
34
+ URI::Generic.build(
35
+ scheme: 'http',
36
+ host: domain,
37
+ path: "/admin/applications/#{app.api_key}/login",
38
+ query: {
39
+ token: app.salt,
40
+ login: app.api_autologin_url,
41
+ }.to_query,
42
+ ).to_s
43
+ end
44
+ it { should eq(expected) }
45
+ end
46
+
47
+ describe '#authorize' do
48
+ before { app.auth_token }
49
+ subject { app.authorize(token) }
50
+
51
+ context 'when valid token is given' do
52
+ let(:token) { app.auth_token }
53
+ it { should be true }
54
+ end
55
+
56
+ context 'when invalid token is given' do
57
+ let(:token) { 'bad_token' }
58
+ it { should be false }
59
+ end
60
+ end
61
+
62
+ describe '::password_by_token' do
63
+ let(:token) { 'test' }
64
+ subject { InsalesApi::App.password_by_token(token) }
65
+ it { should eq(InsalesApi::Password.create(InsalesApi::App.api_secret, token)) }
66
+ end
67
+ end
@@ -0,0 +1,110 @@
1
+ require 'spec_helper'
2
+
3
+ describe InsalesApi do
4
+
5
+ describe '.wait_retry' do
6
+
7
+ response_stub = Struct.new(:code, :retry_after) do
8
+ def [](key)
9
+ {'Retry-After' => retry_after}[key]
10
+ end
11
+ end
12
+
13
+ it 'should not run without block' do
14
+ expect do
15
+ described_class.wait_retry(20, nil)
16
+ end.to raise_error
17
+ end
18
+
19
+ it 'should handle succesfull request' do
20
+ counter = 0
21
+ described_class.wait_retry do
22
+ counter += 1
23
+ end
24
+
25
+ expect(counter).to eq(1)
26
+ end
27
+
28
+ it 'should make specified amount of attempts' do
29
+ counter = 0
30
+ described_class.wait_retry(3) do
31
+ counter += 1
32
+
33
+ if counter < 3
34
+ raise ActiveResource::ServerError.new(response_stub.new("503", "0"))
35
+ end
36
+ end
37
+
38
+ expect(counter).to eq(3)
39
+ end
40
+
41
+ it 'should use callback proc' do
42
+ callback = Proc.new{}
43
+ counter = 0
44
+ callback.should_receive(:call).with(0, 1, 3, instance_of(ActiveResource::ServerError))
45
+ callback.should_receive(:call).with(0, 2, 3, instance_of(ActiveResource::ServerError))
46
+
47
+ described_class.wait_retry(3, callback) do
48
+ counter += 1
49
+
50
+ if counter < 3
51
+ raise ActiveResource::ServerError.new(response_stub.new("503", "0"))
52
+ end
53
+ end
54
+ end
55
+
56
+ it 'should pass attempt number to block' do
57
+ last_attempt_no = 0
58
+ described_class.wait_retry(3) do |x|
59
+ last_attempt_no = x
60
+
61
+ if last_attempt_no < 3
62
+ raise ActiveResource::ServerError.new(response_stub.new("503", "0"))
63
+ end
64
+ end
65
+
66
+ expect(last_attempt_no).to eq(3)
67
+ end
68
+
69
+ it 'should raise if no attempts left' do
70
+ expect do
71
+ described_class.wait_retry(3) do |x|
72
+ raise ActiveResource::ServerError.new(response_stub.new("503", "0"))
73
+ end
74
+ end.to raise_error(ActiveResource::ServerError)
75
+ end
76
+
77
+ it 'should raise on user errors' do
78
+ attempts = 0
79
+ expect do
80
+ described_class.wait_retry(3) do |x|
81
+ attempts = x
82
+ raise 'Some other error'
83
+ end
84
+ end.to raise_error(StandardError)
85
+ expect(attempts).to eq(1)
86
+ end
87
+
88
+ it 'should raise on other server errors' do
89
+ attempts = 0
90
+ expect do
91
+ described_class.wait_retry(3) do |x|
92
+ attempts = x
93
+ raise ActiveResource::ServerError.new(response_stub.new("502", "0"))
94
+ end
95
+ end.to raise_error(ActiveResource::ServerError)
96
+ expect(attempts).to eq(1)
97
+ end
98
+
99
+ it 'should run until success' do |x|
100
+ success = false
101
+ described_class.wait_retry do |x|
102
+ raise ActiveResource::ServerError.new(response_stub.new("503", "0")) if x < 10
103
+ success = true
104
+ end
105
+
106
+ expect(success).to be_true
107
+ end
108
+ end
109
+
110
+ end
data/spec/spec_helper.rb CHANGED
@@ -2,10 +2,9 @@
2
2
  $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
3
3
  require 'insales_api'
4
4
 
5
- InsalesApi::App.api_key = 'test'
6
- InsalesApi::App.api_secret = 'test'
7
- InsalesApi::App.api_host = 'myshop.insales.ru'
8
- InsalesApi::App.api_autologin_path = 'session/autologin'
5
+ InsalesApi::App.api_key = 'test'
6
+ InsalesApi::App.api_secret = 'test'
7
+ InsalesApi::App.api_autologin_url = 'https://host.com/session/autologin'
9
8
 
10
9
  RSpec.configure do |config|
11
10
  # == Mock Framework