solidus_reserved_stock 0.0.1
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.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.rubocop.yml +21 -0
- data/.ruby-gemset +1 -0
- data/.travis.yml +4 -0
- data/CODE_OF_CONDUCT.md +13 -0
- data/Gemfile +21 -0
- data/LICENSE.txt +21 -0
- data/README.md +74 -0
- data/Rakefile +26 -0
- data/app/controllers/spree/admin/reserved_stock_items_controller.rb +88 -0
- data/app/controllers/spree/admin/stock_items_controller_decorator.rb +7 -0
- data/app/controllers/spree/admin/users_controller_decorator.rb +16 -0
- data/app/controllers/spree/api/products_controller_decorator.rb +12 -0
- data/app/controllers/spree/api/v1/reserved_stock_items_controller.rb +88 -0
- data/app/controllers/spree/api/v1/validators/reserve_stock_params_validator.rb +37 -0
- data/app/controllers/spree/api/variants_controller_decorator.rb +12 -0
- data/app/helpers/spree/admin/reserved_stock_items_helper.rb +13 -0
- data/app/helpers/spree/admin/stock_locations_helper_decorator.rb +14 -0
- data/app/helpers/spree/api/api_helpers_decorator.rb +7 -0
- data/app/models/spree/product_decorator.rb +18 -0
- data/app/models/spree/reserved_stock_item.rb +48 -0
- data/app/models/spree/stock/coordinator_decorator.rb +28 -0
- data/app/models/spree/stock/prioritizer_decorator.rb +16 -0
- data/app/models/spree/stock/quantifier_decorator.rb +29 -0
- data/app/models/spree/stock/reserver.rb +60 -0
- data/app/models/spree/stock_location_decorator.rb +46 -0
- data/app/models/spree/stock_reservation_ability.rb +11 -0
- data/app/models/spree/user_decorator.rb +42 -0
- data/app/models/spree/variant_decorator.rb +6 -0
- data/app/overrides/spree/admin/stock_items/_stock_management/adjust_number_of_rows.html.erb.deface +2 -0
- data/app/overrides/spree/admin/stock_items/_stock_management/show_user_for_reserved_stock.html.erb.deface +2 -0
- data/app/overrides/spree/admin/users/_sidebar/add_reserved_stock_menu_item.html.erb.deface +6 -0
- data/app/views/spree/admin/reserved_stock_items/_form.html.erb +32 -0
- data/app/views/spree/admin/reserved_stock_items/index.html.erb +104 -0
- data/app/views/spree/admin/reserved_stock_items/new.html.erb +19 -0
- data/app/views/spree/api/products/show.v1.rabl +35 -0
- data/app/views/spree/api/v1/reserved_stock_items/index.v1.rabl +7 -0
- data/app/views/spree/api/v1/reserved_stock_items/show.v1.rabl +5 -0
- data/app/views/spree/api/variants/big.v1.rabl +20 -0
- data/app/views/spree/api/variants/small.v1.rabl +17 -0
- data/bin/console +14 -0
- data/bin/rails +12 -0
- data/bin/setup +7 -0
- data/config/i18n-tasks.yml +103 -0
- data/config/locales/en.yml +23 -0
- data/config/routes.rb +27 -0
- data/db/migrate/20160105203812_add_reserved_items_to_stock_location.rb +6 -0
- data/db/migrate/20160105222821_add_type_to_spree_stock_items.rb +7 -0
- data/db/migrate/20160106215753_add_user_id_to_spree_stock_items.rb +5 -0
- data/db/migrate/20160229223744_add_original_stock_location_id_to_spree_stock_items.rb +5 -0
- data/db/migrate/20160301003702_add_expires_at_to_spree_stock_items.rb +5 -0
- data/db/migrate/20160309025334_modify_stock_item_unique_index.rb +14 -0
- data/lib/generators/solidus_reserved_stock/install/install_generator.rb +22 -0
- data/lib/solidus_reserved_stock/ability_initializer.rb +5 -0
- data/lib/solidus_reserved_stock/engine.rb +15 -0
- data/lib/solidus_reserved_stock/version.rb +18 -0
- data/lib/solidus_reserved_stock.rb +7 -0
- data/lib/tasks/solidus_reserved_stock_tasks.rake +4 -0
- data/solidus_reserved_stock.gemspec +56 -0
- data/spec/controllers/spree/api/stock_locations_controller_spec.rb +24 -0
- data/spec/controllers/spree/api/v1/reserved_stock_items_controller_spec.rb +196 -0
- data/spec/controllers/spree/api/v1/validators/reserve_stock_params_validator_spec.rb +41 -0
- data/spec/controllers/spree/api/variants_controller_spec.rb +62 -0
- data/spec/factories/reserved_stock_item_factory.rb +8 -0
- data/spec/models/spree/reserved_stock_item_spec.rb +101 -0
- data/spec/models/spree/stock/coordinator_decorator_spec.rb +68 -0
- data/spec/models/spree/stock/prioritizer_decorator_spec.rb +29 -0
- data/spec/models/spree/stock/quantifier_decorator_spec.rb +42 -0
- data/spec/models/spree/stock/reserver_spec.rb +103 -0
- data/spec/models/spree/stock_location_decorator_spec.rb +47 -0
- data/spec/models/spree/user_decorator_spec.rb +65 -0
- data/spec/spec_helper.rb +56 -0
- data/spec/support/api_helpers.rb +35 -0
- data/spec/support/database_cleaner.rb +14 -0
- data/spec/support/have_attributes_matcher.rb +10 -0
- metadata +396 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
require "spec_helper"
|
|
2
|
+
|
|
3
|
+
# TODO: Need to supply user argument in the appropriate places
|
|
4
|
+
# - LineItem#sufficient_stock?
|
|
5
|
+
# - Spree::Stock::AvailabilityValidator
|
|
6
|
+
# - Variant#can_supply?
|
|
7
|
+
# - _cart_form (give it the current user)
|
|
8
|
+
describe Spree::Stock::Quantifier, type: :model do
|
|
9
|
+
let(:user) { create(:user) }
|
|
10
|
+
let(:other_user) { create(:user) }
|
|
11
|
+
let(:original_stock_location) { create(:stock_location) }
|
|
12
|
+
let(:variant) { create(:variant) }
|
|
13
|
+
|
|
14
|
+
context "#total_on_hand" do
|
|
15
|
+
before(:each) do
|
|
16
|
+
stock_item = original_stock_location.stock_item_or_create(variant)
|
|
17
|
+
stock_item.adjust_count_on_hand(10)
|
|
18
|
+
Spree::Stock::Reserver.new.reserve(
|
|
19
|
+
variant,
|
|
20
|
+
original_stock_location,
|
|
21
|
+
user,
|
|
22
|
+
4
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
context "with a user argument" do
|
|
26
|
+
it "includes reserved stock for user" do
|
|
27
|
+
subject = Spree::Stock::Quantifier.new(variant, nil, user)
|
|
28
|
+
expect(subject.total_on_hand).to eq(10)
|
|
29
|
+
end
|
|
30
|
+
it "excludes reserved stock for other users" do
|
|
31
|
+
subject = Spree::Stock::Quantifier.new(variant, nil, other_user)
|
|
32
|
+
expect(subject.total_on_hand).to eq(6)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
context "with no user argument" do
|
|
36
|
+
it "excludes reserved stock" do
|
|
37
|
+
subject = Spree::Stock::Quantifier.new(variant)
|
|
38
|
+
expect(subject.total_on_hand).to eq(6)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
require "spec_helper"
|
|
2
|
+
|
|
3
|
+
# Specs ReservedStockItem subclass to StockItem
|
|
4
|
+
describe Spree::Stock::Reserver, type: :model do
|
|
5
|
+
let(:user) { create(:user) }
|
|
6
|
+
let(:original_stock_location) { create(:stock_location) }
|
|
7
|
+
let(:variant) { create(:variant) }
|
|
8
|
+
let(:reserved_stock_location) { Spree::StockLocation.reserved_items_location }
|
|
9
|
+
let(:reserved_stock_item) do
|
|
10
|
+
create(
|
|
11
|
+
:reserved_stock_item,
|
|
12
|
+
variant: variant,
|
|
13
|
+
stock_location: reserved_stock_location,
|
|
14
|
+
original_stock_location: original_stock_location,
|
|
15
|
+
user: user,
|
|
16
|
+
expires_at: 1.day.from_now,
|
|
17
|
+
backorderable: false
|
|
18
|
+
)
|
|
19
|
+
end
|
|
20
|
+
let(:expired_reserved_stock_item) do
|
|
21
|
+
create(
|
|
22
|
+
:reserved_stock_item,
|
|
23
|
+
variant: variant,
|
|
24
|
+
stock_location: reserved_stock_location,
|
|
25
|
+
original_stock_location: original_stock_location,
|
|
26
|
+
user: user,
|
|
27
|
+
expires_at: 1.day.ago,
|
|
28
|
+
backorderable: false
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
subject { Spree::Stock::Reserver.new }
|
|
33
|
+
|
|
34
|
+
context "#reserve" do
|
|
35
|
+
before do
|
|
36
|
+
stock_item = original_stock_location.stock_item_or_create(variant)
|
|
37
|
+
stock_item.adjust_count_on_hand(10)
|
|
38
|
+
end
|
|
39
|
+
it "moves stock items to the reserved stock location" do
|
|
40
|
+
subject.reserve(variant, original_stock_location, user, 4)
|
|
41
|
+
expect(user.reserved_count_on_hand(variant)).to eq 4
|
|
42
|
+
expect(original_stock_location.count_on_hand(variant)).to eq 6
|
|
43
|
+
end
|
|
44
|
+
it "remembers original stock location" do
|
|
45
|
+
subject.reserve(variant, original_stock_location, user, 4)
|
|
46
|
+
reserved_stock_item = user.reserved_stock_item(variant)
|
|
47
|
+
expect(
|
|
48
|
+
reserved_stock_item.original_stock_location
|
|
49
|
+
).to eq original_stock_location
|
|
50
|
+
end
|
|
51
|
+
it "won't reserve more stock than exists" do
|
|
52
|
+
expect do
|
|
53
|
+
subject.reserve(variant, original_stock_location, user, 1000)
|
|
54
|
+
end.to raise_error Spree::Stock::Reserver::InvalidQuantityError
|
|
55
|
+
end
|
|
56
|
+
it "won't reserve zero stock" do
|
|
57
|
+
expect do
|
|
58
|
+
subject.reserve(variant, original_stock_location, user, 0)
|
|
59
|
+
end.to raise_error Spree::Stock::Reserver::InvalidQuantityError
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
context "#restock" do
|
|
64
|
+
before(:each) do
|
|
65
|
+
reserved_stock_item
|
|
66
|
+
end
|
|
67
|
+
it "can restore all stock to the original stock location" do
|
|
68
|
+
subject.restock(variant, user)
|
|
69
|
+
expect(original_stock_location.count_on_hand(variant)).to eq 10
|
|
70
|
+
expect(user.reserved_count_on_hand(variant)).to eq 0
|
|
71
|
+
end
|
|
72
|
+
it "can restore a quantity of stock to the original stock location" do
|
|
73
|
+
subject.restock(variant, user, 4)
|
|
74
|
+
expect(original_stock_location.count_on_hand(variant)).to eq 4
|
|
75
|
+
expect(user.reserved_count_on_hand(variant)).to eq 6
|
|
76
|
+
end
|
|
77
|
+
it "won't restock more stock than exists"do
|
|
78
|
+
expect do
|
|
79
|
+
subject.restock(variant, user, 1000)
|
|
80
|
+
end.to raise_error Spree::Stock::Reserver::InvalidQuantityError
|
|
81
|
+
end
|
|
82
|
+
it "won't restock zero stock" do
|
|
83
|
+
expect do
|
|
84
|
+
subject.restock(variant, user, 0)
|
|
85
|
+
end.to raise_error Spree::Stock::Reserver::InvalidQuantityError
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
context "#restock_expired" do
|
|
90
|
+
it "restores expired ReservedStockItems" do
|
|
91
|
+
expired_reserved_stock_item
|
|
92
|
+
subject.restock_expired
|
|
93
|
+
expect(original_stock_location.count_on_hand(variant)).to eq 10
|
|
94
|
+
expect(user.reserved_count_on_hand(variant)).to eq 0
|
|
95
|
+
end
|
|
96
|
+
it "doesn't restore unexpired ReservedStockItems" do
|
|
97
|
+
reserved_stock_item
|
|
98
|
+
subject.restock_expired
|
|
99
|
+
expect(original_stock_location.count_on_hand(variant)).to eq 0
|
|
100
|
+
expect(user.reserved_count_on_hand(variant)).to eq 10
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
require "spec_helper"
|
|
2
|
+
|
|
3
|
+
# Specs modifications in stock_location_decorator
|
|
4
|
+
describe Spree::StockLocation, type: :model do
|
|
5
|
+
subject do
|
|
6
|
+
create(
|
|
7
|
+
:stock_location,
|
|
8
|
+
backorderable_default: false,
|
|
9
|
+
propagate_all_variants: false,
|
|
10
|
+
reserved_items: true
|
|
11
|
+
)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
context "validation" do
|
|
15
|
+
context "when reserved_items is true" do
|
|
16
|
+
it "is invalid if backorderable_default is true" do
|
|
17
|
+
reserved_stock_item = build(
|
|
18
|
+
:stock_location,
|
|
19
|
+
backorderable_default: true,
|
|
20
|
+
propagate_all_variants: false,
|
|
21
|
+
reserved_items: true
|
|
22
|
+
)
|
|
23
|
+
expect(reserved_stock_item).to be_invalid
|
|
24
|
+
end
|
|
25
|
+
it "is invalid if propagate_all_variants is true" do
|
|
26
|
+
reserved_stock_item = build(
|
|
27
|
+
:stock_location,
|
|
28
|
+
backorderable_default: false,
|
|
29
|
+
propagate_all_variants: true,
|
|
30
|
+
reserved_items: true
|
|
31
|
+
)
|
|
32
|
+
expect(reserved_stock_item).to be_invalid
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
context ".reserved_items_location" do
|
|
37
|
+
it "can return the existing reserved stock location" do
|
|
38
|
+
subject
|
|
39
|
+
expect(Spree::StockLocation.reserved_items_location).to eq subject
|
|
40
|
+
end
|
|
41
|
+
it "creates reserved stock location if not present" do
|
|
42
|
+
reserved_items_location = Spree::StockLocation.reserved_items_location
|
|
43
|
+
expect(reserved_items_location).to be_a Spree::StockLocation
|
|
44
|
+
expect(reserved_items_location.reserved_items?).to be true
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
require "spec_helper"
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
describe Spree.user_class, type: :model do
|
|
5
|
+
subject { create(:user) }
|
|
6
|
+
let(:original_stock_location) { create(:stock_location) }
|
|
7
|
+
let(:variant) { create(:variant) }
|
|
8
|
+
let(:reserved_stock_location) { Spree::StockLocation.reserved_items_location }
|
|
9
|
+
let(:reserved_stock_item) do
|
|
10
|
+
create(
|
|
11
|
+
:reserved_stock_item,
|
|
12
|
+
variant: variant,
|
|
13
|
+
stock_location: reserved_stock_location,
|
|
14
|
+
original_stock_location: original_stock_location,
|
|
15
|
+
user: subject,
|
|
16
|
+
expires_at: 1.day.from_now,
|
|
17
|
+
backorderable: false
|
|
18
|
+
)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
context "#reserved_count_on_hand" do
|
|
22
|
+
it "returns reserved quantity for a variant" do
|
|
23
|
+
reserved_stock_item
|
|
24
|
+
expect(
|
|
25
|
+
subject.reserved_count_on_hand(variant)
|
|
26
|
+
).to eq 10
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
context "#reserved_stock_item" do
|
|
31
|
+
it "returns the reserved_stock_item for a variant" do
|
|
32
|
+
reserved_stock_item
|
|
33
|
+
expect(
|
|
34
|
+
subject.reserved_stock_item(variant)
|
|
35
|
+
).to eq reserved_stock_item
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
context "#reserved_stock_item_or_create" do
|
|
40
|
+
it "returns an existing reserved_stock_item" do
|
|
41
|
+
reserved_stock_item
|
|
42
|
+
expect(
|
|
43
|
+
subject.reserved_stock_item_or_create(variant, original_stock_location)
|
|
44
|
+
).to eq reserved_stock_item
|
|
45
|
+
end
|
|
46
|
+
it "creates and returns a reserved_stock_item if none exists" do
|
|
47
|
+
expect(subject.reserved_stock_item(variant)).to be_nil
|
|
48
|
+
subject.reserved_stock_item_or_create(variant, original_stock_location, 1.week.from_now)
|
|
49
|
+
expect(subject.reserved_stock_item(variant)).not_to be_nil
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
context "when account is destroyed" do
|
|
54
|
+
it "restocks all reserved items" do
|
|
55
|
+
reserved_stock_item
|
|
56
|
+
subject.destroy
|
|
57
|
+
expect(
|
|
58
|
+
reserved_stock_location.stock_items.where(user_id: subject.id).count
|
|
59
|
+
).to eq 0
|
|
60
|
+
expect(
|
|
61
|
+
original_stock_location.count_on_hand(variant)
|
|
62
|
+
).to eq 10
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
data/spec/spec_helper.rb
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
require "simplecov"
|
|
2
|
+
SimpleCov.start "rails" do
|
|
3
|
+
add_filter "/.gems/"
|
|
4
|
+
end
|
|
5
|
+
|
|
6
|
+
ENV["RAILS_ENV"] ||= "test"
|
|
7
|
+
|
|
8
|
+
begin
|
|
9
|
+
require File.expand_path("../dummy/config/environment", __FILE__)
|
|
10
|
+
rescue LoadError
|
|
11
|
+
puts "Could not load dummy application."\
|
|
12
|
+
"Please ensure you have run `bundle exec rake test_app`"
|
|
13
|
+
exit
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
require "rspec/rails"
|
|
17
|
+
|
|
18
|
+
# Requires supporting ruby files with custom matchers and macros, etc,
|
|
19
|
+
# in spec/support/ and its subdirectories.
|
|
20
|
+
Dir[File.dirname(__FILE__) + "/support/**/*.rb"].each {|f| require f}
|
|
21
|
+
require "pry"
|
|
22
|
+
require "ffaker"
|
|
23
|
+
require "factory_girl_rails"
|
|
24
|
+
require "spree/testing_support/factories"
|
|
25
|
+
require "spree/testing_support/preferences"
|
|
26
|
+
require "spree/api/testing_support/helpers"
|
|
27
|
+
require "spree/api/testing_support/setup"
|
|
28
|
+
require 'rspec/active_model/mocks'
|
|
29
|
+
|
|
30
|
+
RSpec.configure do |config|
|
|
31
|
+
config.fail_fast = false
|
|
32
|
+
config.filter_run focus: true
|
|
33
|
+
config.infer_spec_type_from_file_location!
|
|
34
|
+
config.mock_with :rspec
|
|
35
|
+
config.raise_errors_for_deprecations!
|
|
36
|
+
config.run_all_when_everything_filtered = true
|
|
37
|
+
config.use_transactional_fixtures = true
|
|
38
|
+
config.include FactoryGirl::Syntax::Methods
|
|
39
|
+
config.include Spree::Api::TestingSupport::Helpers, type: :controller
|
|
40
|
+
config.extend Spree::Api::TestingSupport::Setup, type: :controller
|
|
41
|
+
config.include Spree::TestingSupport::Preferences
|
|
42
|
+
|
|
43
|
+
config.expect_with :rspec do |expectations|
|
|
44
|
+
expectations.syntax = :expect
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
config.before(:each) do
|
|
48
|
+
Rails.cache.clear
|
|
49
|
+
reset_spree_preferences
|
|
50
|
+
Spree::Api::Config.requires_authentication = true
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
Dir[File.join(File.dirname(__FILE__), "/support/**/*.rb")].each do |file|
|
|
55
|
+
require file
|
|
56
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
require "active_support/all"
|
|
2
|
+
|
|
3
|
+
# Convenience methods for API specs
|
|
4
|
+
module ApiHelpers
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
included do
|
|
8
|
+
routes { Spree::Core::Engine.routes }
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def api_get(action, params = {}, session = nil, flash = nil)
|
|
12
|
+
api_process(action, params, session, flash, "GET")
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def api_post(action, params = {}, session = nil, flash = nil)
|
|
16
|
+
api_process(action, params, session, flash, "POST")
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def api_put(action, params = {}, session = nil, flash = nil)
|
|
20
|
+
api_process(action, params, session, flash, "PUT")
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def api_delete(action, params = {}, session = nil, flash = nil)
|
|
24
|
+
api_process(action, params, session, flash, "DELETE")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def api_process(action, params = {}, session = nil, flash = nil, method = "get")
|
|
28
|
+
scoping = respond_to?(:resource_scoping) ? resource_scoping : {}
|
|
29
|
+
process(action, method, params.merge(scoping).reverse_merge!(format: :json), session, flash)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
RSpec.configure do |config|
|
|
34
|
+
config.include ApiHelpers, type: :controller
|
|
35
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
RSpec.configure do |config|
|
|
2
|
+
config.before(:suite) do
|
|
3
|
+
DatabaseCleaner.strategy = :transaction
|
|
4
|
+
DatabaseCleaner.clean_with(:truncation)
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
config.before(:each) do
|
|
8
|
+
DatabaseCleaner.start
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
config.after(:each) do
|
|
12
|
+
DatabaseCleaner.clean
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# TODO: Remove this file borrowed from Solidus. It doesn't seem like a great
|
|
2
|
+
# idea to override have_attributes
|
|
3
|
+
RSpec::Matchers.define :have_attributes do |expected_attributes|
|
|
4
|
+
match do |actual|
|
|
5
|
+
# actual is a Hash object representing an object, like this:
|
|
6
|
+
# { "name" => "Product #1" }
|
|
7
|
+
actual_attributes = actual.keys.map(&:to_sym)
|
|
8
|
+
expected_attributes.map(&:to_sym).all? { |attr| actual_attributes.include?(attr) }
|
|
9
|
+
end
|
|
10
|
+
end
|