Merge pull request #4101 from luisramos0/remove_variants_rabl

Convert spree/api/products and spree/api/variants views from rabl to AMS
This commit is contained in:
Pau Pérez Fabregat
2019-09-02 12:14:26 +02:00
committed by GitHub
17 changed files with 650 additions and 925 deletions

View File

@@ -0,0 +1,124 @@
require 'open_food_network/permissions'
module Api
class ProductsController < Api::BaseController
respond_to :json
skip_authorization_check only: [:show, :bulk_products, :overridable]
def show
@product = find_product(params[:id])
render json: @product, serializer: Api::Admin::ProductSerializer
end
def create
authorize! :create, Spree::Product
params[:product][:available_on] ||= Time.zone.now
@product = Spree::Product.new(params[:product])
begin
if @product.save
render json: @product, serializer: Api::Admin::ProductSerializer, status: 201
else
invalid_resource!(@product)
end
rescue ActiveRecord::RecordNotUnique
@product.permalink = nil
retry
end
end
def update
authorize! :update, Spree::Product
@product = find_product(params[:id])
if @product.update_attributes(params[:product])
render json: @product, serializer: Api::Admin::ProductSerializer, status: 200
else
invalid_resource!(@product)
end
end
def destroy
authorize! :delete, Spree::Product
@product = find_product(params[:id])
@product.update_attribute(:deleted_at, Time.zone.now)
@product.variants_including_master.update_all(deleted_at: Time.zone.now)
render json: @product, serializer: Api::Admin::ProductSerializer, status: 204
end
# TODO: This should be named 'managed'. Is the action above used? Maybe we should remove it.
def bulk_products
@products = OpenFoodNetwork::Permissions.new(current_api_user).editable_products.
merge(product_scope).
order('created_at DESC').
ransack(params[:q]).result.
page(params[:page]).per(params[:per_page])
render_paged_products @products
end
def overridable
producers = OpenFoodNetwork::Permissions.new(current_api_user).
variant_override_producers.by_name
@products = paged_products_for_producers producers
render_paged_products @products
end
def soft_delete
authorize! :delete, Spree::Product
@product = find_product(params[:product_id])
authorize! :delete, @product
@product.destroy
render json: @product, serializer: Api::Admin::ProductSerializer, status: 204
end
# POST /api/products/:product_id/clone
#
def clone
authorize! :create, Spree::Product
original_product = find_product(params[:product_id])
authorize! :update, original_product
@product = original_product.duplicate
render json: @product, serializer: Api::Admin::ProductSerializer, status: 201
end
private
# Copied and modified from SpreeApi::BaseController to allow
# enterprise users to access inactive products
def product_scope
# This line modified
if current_api_user.has_spree_role?("admin") || current_api_user.enterprises.present?
scope = Spree::Product
if params[:show_deleted]
scope = scope.with_deleted
end
else
scope = Spree::Product.active
end
scope.includes(:master)
end
def paged_products_for_producers(producers)
Spree::Product.scoped.
merge(product_scope).
where(supplier_id: producers).
by_producer.by_name.
ransack(params[:q]).result.
page(params[:page]).per(params[:per_page])
end
def render_paged_products(products)
serializer = ActiveModel::ArraySerializer.new(
products,
each_serializer: Api::Admin::ProductSerializer
)
render text: { products: serializer, pages: products.num_pages }.to_json
end
end
end

View File

@@ -0,0 +1,79 @@
module Api
class VariantsController < Api::BaseController
respond_to :json
skip_authorization_check only: [:index, :show]
before_filter :product
def index
@variants = scope.includes(:option_values).ransack(params[:q]).result
render json: @variants, each_serializer: Api::VariantSerializer
end
def show
@variant = scope.includes(:option_values).find(params[:id])
render json: @variant, serializer: Api::VariantSerializer
end
def create
authorize! :create, Spree::Variant
@variant = scope.new(params[:variant])
if @variant.save
render json: @variant, serializer: Api::VariantSerializer, status: 201
else
invalid_resource!(@variant)
end
end
def update
authorize! :update, Spree::Variant
@variant = scope.find(params[:id])
if @variant.update_attributes(params[:variant])
render json: @variant, serializer: Api::VariantSerializer, status: 200
else
invalid_resource!(@product)
end
end
def soft_delete
@variant = scope.find(params[:variant_id])
authorize! :delete, @variant
VariantDeleter.new.delete(@variant)
render json: @variant, serializer: Api::VariantSerializer, status: 204
end
def destroy
authorize! :delete, Spree::Variant
@variant = scope.find(params[:id])
@variant.destroy
render json: @variant, serializer: Api::VariantSerializer, status: 204
end
private
def product
@product ||= Spree::Product.find_by_permalink(params[:product_id]) if params[:product_id]
end
def scope
if @product
unless current_api_user.has_spree_role?("admin") || params[:show_deleted]
variants = @product.variants_including_master
else
variants = @product.variants_including_master.with_deleted
end
else
variants = Spree::Variant.scoped
if current_api_user.has_spree_role?("admin")
unless params[:show_deleted]
variants = Spree::Variant.active
end
else
variants = variants.active
end
end
variants
end
end
end

View File

@@ -1,149 +0,0 @@
require 'open_food_network/permissions'
module Spree
module Api
class ProductsController < Spree::Api::BaseController
respond_to :json
def index
if params[:ids]
@products = product_scope.where(id: params[:ids])
else
@products = product_scope.ransack(params[:q]).result
end
@products = @products.page(params[:page]).per(params[:per_page])
respond_with(@products)
end
def show
@product = find_product(params[:id])
respond_with(@product)
end
def new; end
def create
authorize! :create, Product
params[:product][:available_on] ||= Time.zone.now
@product = Product.new(params[:product])
begin
if @product.save
respond_with(@product, status: 201, default_template: :show)
else
invalid_resource!(@product)
end
rescue ActiveRecord::RecordNotUnique
@product.permalink = nil
retry
end
end
def update
authorize! :update, Product
@product = find_product(params[:id])
if @product.update_attributes(params[:product])
respond_with(@product, status: 200, default_template: :show)
else
invalid_resource!(@product)
end
end
def destroy
authorize! :delete, Product
@product = find_product(params[:id])
@product.update_attribute(:deleted_at, Time.zone.now)
@product.variants_including_master.update_all(deleted_at: Time.zone.now)
respond_with(@product, status: 204)
end
def managed
authorize! :admin, Spree::Product
authorize! :read, Spree::Product
@products = product_scope.
ransack(params[:q]).result.
managed_by(current_api_user).
page(params[:page]).per(params[:per_page])
respond_with(@products, default_template: :index)
end
# TODO: This should be named 'managed'. Is the action above used? Maybe we should remove it.
def bulk_products
@products = OpenFoodNetwork::Permissions.new(current_api_user).editable_products.
merge(product_scope).
order('created_at DESC').
ransack(params[:q]).result.
page(params[:page]).per(params[:per_page])
render_paged_products @products
end
def overridable
producers = OpenFoodNetwork::Permissions.new(current_api_user).
variant_override_producers.by_name
@products = paged_products_for_producers producers
render_paged_products @products
end
def soft_delete
authorize! :delete, Spree::Product
@product = find_product(params[:product_id])
authorize! :delete, @product
@product.destroy
respond_with(@product, status: 204)
end
# POST /api/products/:product_id/clone
#
def clone
authorize! :create, Spree::Product
original_product = find_product(params[:product_id])
authorize! :update, original_product
@product = original_product.duplicate
respond_with(@product, status: 201, default_template: :show)
end
private
# Copied and modified from Spree::Api::BaseController to allow
# enterprise users to access inactive products
def product_scope
# This line modified
if current_api_user.has_spree_role?("admin") || current_api_user.enterprises.present?
scope = Spree::Product
if params[:show_deleted]
scope = scope.with_deleted
end
else
scope = Spree::Product.active
end
scope.includes(:master)
end
def paged_products_for_producers(producers)
Spree::Product.scoped.
merge(product_scope).
where(supplier_id: producers).
by_producer.by_name.
ransack(params[:q]).result.
page(params[:page]).per(params[:per_page])
end
def render_paged_products(products)
serializer = ActiveModel::ArraySerializer.new(
products,
each_serializer: ::Api::Admin::ProductSerializer
)
render text: { products: serializer, pages: products.num_pages }.to_json
end
end
end
end

View File

@@ -1,83 +0,0 @@
module Spree
module Api
class VariantsController < Spree::Api::BaseController
respond_to :json
before_filter :product
def index
@variants = scope.includes(:option_values).ransack(params[:q]).result.
page(params[:page]).per(params[:per_page])
respond_with(@variants)
end
def show
@variant = scope.includes(:option_values).find(params[:id])
respond_with(@variant)
end
def new; end
def create
authorize! :create, Variant
@variant = scope.new(params[:variant])
if @variant.save
respond_with(@variant, status: 201, default_template: :show)
else
invalid_resource!(@variant)
end
end
def update
authorize! :update, Variant
@variant = scope.find(params[:id])
if @variant.update_attributes(params[:variant])
respond_with(@variant, status: 200, default_template: :show)
else
invalid_resource!(@product)
end
end
def soft_delete
@variant = scope.find(params[:variant_id])
authorize! :delete, @variant
VariantDeleter.new.delete(@variant)
respond_with @variant, status: 204
end
def destroy
authorize! :delete, Variant
@variant = scope.find(params[:id])
@variant.destroy
respond_with(@variant, status: 204)
end
private
def product
@product ||= Spree::Product.find_by_permalink(params[:product_id]) if params[:product_id]
end
def scope
if @product
unless current_api_user.has_spree_role?("admin") || params[:show_deleted]
variants = @product.variants_including_master
else
variants = @product.variants_including_master.with_deleted
end
else
variants = Variant.scoped
if current_api_user.has_spree_role?("admin")
unless params[:show_deleted]
variants = Variant.active
end
else
variants = variants.active
end
end
variants
end
end
end
end

View File

@@ -4,6 +4,12 @@ module Spree
private
def check_price
if currency.nil?
self.currency = Spree::Config[:currency]
end
end
def refresh_products_cache
variant.andand.refresh_products_cache
end

View File

@@ -1,6 +1,8 @@
class Api::VariantSerializer < ActiveModel::Serializer
attributes :id, :is_master, :on_hand, :name_to_display, :unit_to_display, :unit_value
attributes :options_text, :on_demand, :price, :fees, :price_with_fees, :product_name
attributes :id, :is_master, :product_name, :sku
attributes :options_text, :unit_value, :unit_description, :unit_to_display
attributes :display_as, :display_name, :name_to_display
attributes :price, :on_demand, :on_hand, :fees, :price_with_fees
attributes :tag_list
delegate :price, to: :object

View File

@@ -1,3 +0,0 @@
object @enterprise
attributes :id, :name

View File

@@ -1,2 +0,0 @@
collection @products.order('id ASC')
extends "spree/api/products/bulk_show"

View File

@@ -1,27 +0,0 @@
object @product
# TODO: This is used by bulk product edit when a product is cloned.
# But the list of products is serialized by Api::Admin::ProductSerializer.
# This should probably be unified.
attributes :id, :name, :sku, :variant_unit, :variant_unit_scale, :variant_unit_name, :on_demand, :inherits_properties
attributes :on_hand, :price, :available_on, :permalink_live, :tax_category_id
# Infinity is not a valid JSON object, but Rails encodes it anyway
node( :taxon_ids ) { |p| p.taxons.map(&:id).join(",") }
node( :on_hand ) { |p| p.on_hand.nil? ? 0 : p.on_hand.to_f.finite? ? p.on_hand : t(:on_demand) }
node( :price ) { |p| p.price.nil? ? '0.0' : p.price }
node( :available_on ) { |p| p.available_on.blank? ? "" : p.available_on.strftime("%F %T") }
node( :permalink_live, &:permalink )
node( :producer_id, &:supplier_id )
node( :category_id, &:primary_taxon_id )
node( :supplier ) do |p|
partial 'api/enterprises/bulk_show', object: p.supplier
end
node( :variants ) do |p|
partial 'spree/api/variants/bulk_index', object: p.variants.reorder('spree_variants.id ASC')
end
node( :master ) do |p|
partial 'spree/api/variants/bulk_show', object: p.master
end

View File

@@ -1,2 +0,0 @@
collection @variants
extends "spree/api/variants/bulk_show"

View File

@@ -1,7 +0,0 @@
object @variant
attributes :id, :options_text, :unit_value, :unit_description, :on_demand, :display_as, :display_name
# Infinity is not a valid JSON object, but Rails encodes it anyway
node( :on_hand ) { |v| v.on_hand.nil? ? 0 : ( v.on_hand.to_f.finite? ? v.on_hand : t(:on_demand) ) }
node( :price ) { |v| v.price.nil? ? 0.to_f : v.price }

View File

@@ -1,5 +1,20 @@
Openfoodnetwork::Application.routes.draw do
namespace :api do
resources :products do
collection do
get :bulk_products
get :overridable
end
delete :soft_delete
post :clone
resources :variants do
delete :soft_delete
end
end
resources :variants, :only => [:index]
resources :enterprises do
post :update_image, on: :member
get :managed, on: :collection

View File

@@ -62,22 +62,6 @@ Spree::Core::Engine.routes.prepend do
get :authorise_api, on: :collection
end
resources :products do
collection do
get :managed
get :bulk_products
get :overridable
end
delete :soft_delete
post :clone
resources :variants do
delete :soft_delete
end
end
resources :variants, :only => [:index]
resources :orders do
get :managed, on: :collection

View File

@@ -0,0 +1,225 @@
require 'spec_helper'
describe Api::ProductsController, type: :controller do
render_views
let(:supplier) { create(:supplier_enterprise) }
let(:supplier2) { create(:supplier_enterprise) }
let!(:product) { create(:product, supplier: supplier) }
let!(:inactive_product) { create(:product, available_on: Time.zone.now.tomorrow, name: "inactive") }
let(:product_other_supplier) { create(:product, supplier: supplier2) }
let(:product_with_image) { create(:product_with_image, supplier: supplier) }
let(:attributes) { ["id", "name", "supplier", "price", "on_hand", "available_on", "permalink_live"] }
let(:all_attributes) { ["id", "name", "price", "available_on", "variants"] }
let(:variants_attributes) { ["id", "options_text", "unit_value", "unit_description", "unit_to_display", "on_demand", "display_as", "display_name", "name_to_display", "sku", "on_hand", "price"] }
let(:current_api_user) { build(:user) }
before do
allow(controller).to receive(:spree_current_user) { current_api_user }
end
context "as a normal user" do
before do
allow(current_api_user)
.to receive(:has_spree_role?).with("admin").and_return(false)
end
it "gets a single product" do
product.master.images.create!(attachment: image("thinking-cat.jpg"))
product.variants.create!(unit_value: "1", unit_description: "thing")
product.variants.first.images.create!(attachment: image("thinking-cat.jpg"))
product.set_property("spree", "rocks")
api_get :show, id: product.to_param
expect(all_attributes.all?{ |attr| json_response.keys.include? attr }).to eq(true)
expect(variants_attributes.all?{ |attr| json_response['variants'].first.keys.include? attr }).to eq(true)
end
context "finds a product by permalink first then by id" do
let!(:other_product) { create(:product, permalink: "these-are-not-the-droids-you-are-looking-for") }
before do
product.update_attribute(:permalink, "#{other_product.id}-and-1-ways")
end
specify do
api_get :show, id: product.to_param
expect(json_response["permalink_live"]).to match(/and-1-ways/)
product.destroy
api_get :show, id: other_product.id
expect(json_response["permalink_live"]).to match(/droids/)
end
end
it "cannot see inactive products" do
api_get :show, id: inactive_product.to_param
expect(json_response["error"]).to eq("The resource you were looking for could not be found.")
expect(response.status).to eq(404)
end
it "returns a 404 error when it cannot find a product" do
api_get :show, id: "non-existant"
expect(json_response["error"]).to eq("The resource you were looking for could not be found.")
expect(response.status).to eq(404)
end
include_examples "modifying product actions are restricted"
end
context "as an enterprise user" do
let(:current_api_user) do
user = create(:user)
user.enterprise_roles.create(enterprise: supplier)
user
end
it "soft deletes my products" do
spree_delete :soft_delete, product_id: product.to_param, format: :json
expect(response.status).to eq(204)
expect { product.reload }.not_to raise_error
expect(product.deleted_at).not_to be_nil
end
it "is denied access to soft deleting another enterprises' product" do
spree_delete :soft_delete, product_id: product_other_supplier.to_param, format: :json
assert_unauthorized!
expect { product_other_supplier.reload }.not_to raise_error
expect(product_other_supplier.deleted_at).to be_nil
end
end
context "as an administrator" do
before do
allow(current_api_user)
.to receive(:has_spree_role?).with("admin").and_return(true)
end
it "soft deletes a product" do
spree_delete :soft_delete, product_id: product.to_param, format: :json
expect(response.status).to eq(204)
expect { product.reload }.not_to raise_error
expect(product.deleted_at).not_to be_nil
end
it "can create a new product" do
api_post :create, product: { name: "The Other Product",
price: 19.99,
shipping_category_id: create(:shipping_category).id,
supplier_id: supplier.id,
primary_taxon_id: FactoryBot.create(:taxon).id,
variant_unit: "items",
variant_unit_name: "things",
unit_description: "things" }
expect(all_attributes.all?{ |attr| json_response.keys.include? attr }).to eq(true)
expect(response.status).to eq(201)
end
it "cannot create a new product with invalid attributes" do
api_post :create, product: {}
expect(response.status).to eq(422)
expect(json_response["error"]).to eq("Invalid resource. Please fix errors and try again.")
errors = json_response["errors"]
expect(errors.keys).to match_array(["name", "price", "primary_taxon", "shipping_category_id", "supplier", "variant_unit"])
end
it "can update a product" do
api_put :update, id: product.to_param, product: { name: "New and Improved Product!" }
expect(response.status).to eq(200)
end
it "cannot update a product with an invalid attribute" do
api_put :update, id: product.to_param, product: { name: "" }
expect(response.status).to eq(422)
expect(json_response["error"]).to eq("Invalid resource. Please fix errors and try again.")
expect(json_response["errors"]["name"]).to eq(["can't be blank"])
end
it "can delete a product" do
expect(product.deleted_at).to be_nil
api_delete :destroy, id: product.to_param
expect(response.status).to eq(204)
expect(product.reload.deleted_at).not_to be_nil
end
end
describe '#clone' do
context 'as a normal user' do
before do
allow(current_api_user)
.to receive(:has_spree_role?).with("admin").and_return(false)
end
it 'denies access' do
spree_post :clone, product_id: product.id, format: :json
assert_unauthorized!
end
end
context 'as an enterprise user' do
let(:current_api_user) do
user = create(:user)
user.enterprise_roles.create(enterprise: supplier)
user
end
it 'responds with a successful response' do
spree_post :clone, product_id: product.id, format: :json
expect(response.status).to eq(201)
end
it 'clones the product' do
spree_post :clone, product_id: product.id, format: :json
expect(json_response['name']).to eq("COPY OF #{product.name}")
end
it 'clones a product with image' do
spree_post :clone, product_id: product_with_image.id, format: :json
expect(response.status).to eq(201)
expect(json_response['name']).to eq("COPY OF #{product_with_image.name}")
end
end
context 'as an administrator' do
before do
allow(current_api_user)
.to receive(:has_spree_role?).with("admin").and_return(true)
end
it 'responds with a successful response' do
spree_post :clone, product_id: product.id, format: :json
expect(response.status).to eq(201)
end
it 'clones the product' do
spree_post :clone, product_id: product.id, format: :json
expect(json_response['name']).to eq("COPY OF #{product.name}")
end
it 'clones a product with image' do
spree_post :clone, product_id: product_with_image.id, format: :json
expect(response.status).to eq(201)
expect(json_response['name']).to eq("COPY OF #{product_with_image.name}")
end
end
end
end

View File

@@ -0,0 +1,197 @@
require 'spec_helper'
describe Api::VariantsController, type: :controller do
render_views
let(:supplier) { FactoryBot.create(:supplier_enterprise) }
let!(:variant1) { FactoryBot.create(:variant) }
let!(:variant2) { FactoryBot.create(:variant) }
let!(:variant3) { FactoryBot.create(:variant) }
let(:attributes) { [:id, :options_text, :price, :on_hand, :unit_value, :unit_description, :on_demand, :display_as, :display_name] }
before do
allow(controller).to receive(:spree_current_user) { current_api_user }
end
context "as a normal user" do
sign_in_as_user!
let!(:product) { create(:product) }
let!(:variant) do
variant = product.master
variant.option_values << create(:option_value)
variant
end
it "retrieves a list of variants with appropriate attributes" do
spree_get :index, format: :json
keys = json_response.first.keys.map(&:to_sym)
expect(attributes.all?{ |attr| keys.include? attr }).to eq(true)
end
it "is denied access when trying to delete a variant" do
product = create(:product)
variant = product.master
spree_delete :soft_delete, variant_id: variant.to_param, product_id: product.to_param, format: :json
assert_unauthorized!
expect { variant.reload }.not_to raise_error
expect(variant.deleted_at).to be_nil
end
it 'can query the results through a parameter' do
expected_result = create(:variant, sku: 'FOOBAR')
api_get :index, q: { sku_cont: 'FOO' }
expect(json_response.size).to eq(1)
expect(json_response.first['sku']).to eq expected_result.sku
end
# Regression test for spree#2141
context "a deleted variant" do
before do
variant.update_column(:deleted_at, Time.zone.now)
end
it "is not returned in the results" do
api_get :index
expect(json_response.count).to eq(10) # there are 11 variants
end
it "is not returned even when show_deleted is passed" do
api_get :index, show_deleted: true
expect(json_response.count).to eq(10) # there are 11 variants
end
end
it "can see a single variant" do
api_get :show, id: variant.to_param
keys = json_response.keys.map(&:to_sym)
expect((attributes).all?{ |attr| keys.include? attr }).to eq(true)
end
it "cannot create a new variant if not an admin" do
api_post :create, variant: { sku: "12345" }
assert_unauthorized!
end
it "cannot update a variant" do
api_put :update, id: variant.to_param, variant: { sku: "12345" }
assert_unauthorized!
end
it "cannot delete a variant" do
api_delete :destroy, id: variant.to_param
assert_unauthorized!
expect { variant.reload }.not_to raise_error
end
end
context "as an enterprise user" do
sign_in_as_enterprise_user! [:supplier]
let(:supplier_other) { create(:supplier_enterprise) }
let(:product) { create(:product, supplier: supplier) }
let(:variant) { product.master }
let(:product_other) { create(:product, supplier: supplier_other) }
let(:variant_other) { product_other.master }
it "soft deletes a variant" do
spree_delete :soft_delete, variant_id: variant.to_param, product_id: product.to_param, format: :json
expect(response.status).to eq(204)
expect { variant.reload }.not_to raise_error
expect(variant.deleted_at).to be_present
end
it "is denied access to soft deleting another enterprises' variant" do
spree_delete :soft_delete, variant_id: variant_other.to_param, product_id: product_other.to_param, format: :json
assert_unauthorized!
expect { variant.reload }.not_to raise_error
expect(variant.deleted_at).to be_nil
end
context 'when the variant is not the master' do
before { variant.update_attribute(:is_master, false) }
it 'refreshes the cache' do
expect(OpenFoodNetwork::ProductsCache).to receive(:variant_destroyed).with(variant)
spree_delete :soft_delete, variant_id: variant.id, product_id: variant.product.permalink, format: :json
end
end
end
context "as an administrator" do
sign_in_as_admin!
let(:product) { create(:product) }
let(:variant) { product.master }
let(:resource_scoping) { { product_id: variant.product.to_param } }
it "soft deletes a variant" do
spree_delete :soft_delete, variant_id: variant.to_param, product_id: product.to_param, format: :json
expect(response.status).to eq(204)
expect { variant.reload }.not_to raise_error
expect(variant.deleted_at).not_to be_nil
end
it "doesn't delete the only variant of the product" do
product = create(:product)
variant = product.variants.first
spree_delete :soft_delete, variant_id: variant.to_param, product_id: product.to_param, format: :json
expect(variant.reload).to_not be_deleted
expect(assigns(:variant).errors[:product]).to include "must have at least one variant"
end
context 'when the variant is not the master' do
before { variant.update_attribute(:is_master, false) }
it 'refreshes the cache' do
expect(OpenFoodNetwork::ProductsCache).to receive(:variant_destroyed).with(variant)
spree_delete :soft_delete, variant_id: variant.id, product_id: variant.product.permalink, format: :json
end
end
context "deleted variants" do
before do
variant.update_column(:deleted_at, Time.zone.now)
end
it "are visible by admin" do
api_get :index, show_deleted: 1
expect(json_response.count).to eq(2)
end
end
it "can create a new variant" do
original_number_of_variants = variant.product.variants.count
api_post :create, variant: { sku: "12345", unit_value: "weight", unit_description: "L" }
expect(attributes.all?{ |attr| json_response.include? attr.to_s }).to eq(true)
expect(response.status).to eq(201)
expect(json_response["sku"]).to eq("12345")
expect(variant.product.variants.count).to eq(original_number_of_variants + 1)
end
it "can update a variant" do
api_put :update, id: variant.to_param, variant: { sku: "12345" }
expect(response.status).to eq(200)
end
it "can delete a variant" do
api_delete :destroy, id: variant.to_param
expect(response.status).to eq(204)
expect { Spree::Variant.find(variant.id) }.to raise_error(ActiveRecord::RecordNotFound)
end
end
end

View File

@@ -1,361 +0,0 @@
require 'spec_helper'
module Spree
describe Spree::Api::ProductsController, type: :controller do
render_views
let(:supplier) { create(:supplier_enterprise) }
let(:supplier2) { create(:supplier_enterprise) }
let!(:product) { create(:product, supplier: supplier) }
let!(:inactive_product) { create(:product, available_on: Time.zone.now.tomorrow, name: "inactive") }
let(:product_other_supplier) { create(:product, supplier: supplier2) }
let(:product_with_image) { create(:product_with_image, supplier: supplier) }
let(:attributes) { ["id", "name", "supplier", "price", "on_hand", "available_on", "permalink_live"] }
let(:all_attributes) { ["id", "name", "description", "price", "available_on", "permalink", "meta_description", "meta_keywords", "shipping_category_id", "taxon_ids", "variants", "option_types", "product_properties"] }
let(:current_api_user) { build(:user) }
before do
allow(controller).to receive(:spree_current_user) { current_api_user }
end
context "as a normal user" do
before do
allow(current_api_user)
.to receive(:has_spree_role?).with("admin").and_return(false)
end
it "should deny me access to managed products" do
spree_get :managed, template: 'bulk_index', format: :json
assert_unauthorized!
end
it "retrieves a list of products" do
api_get :index
expect(json_response["products"].first).to have_attributes(keys: all_attributes)
expect(json_response["count"]).to eq(1)
expect(json_response["current_page"]).to eq(1)
expect(json_response["pages"]).to eq(1)
end
it "retrieves a list of products by id" do
api_get :index, ids: [product.id]
expect(json_response["products"].first).to have_attributes(keys: all_attributes)
expect(json_response["count"]).to eq(1)
expect(json_response["current_page"]).to eq(1)
expect(json_response["pages"]).to eq(1)
end
it "does not return inactive products when queried by ids" do
api_get :index, ids: [inactive_product.id]
expect(json_response["count"]).to eq(0)
end
it "does not list unavailable products" do
api_get :index
expect(json_response["products"].first["name"]).not_to eq("inactive")
end
context "pagination" do
it "can select the next page of products" do
second_product = create(:product)
api_get :index, page: 2, per_page: 1
expect(json_response["products"].first).to have_attributes(keys: all_attributes)
expect(json_response["total_count"]).to eq(2)
expect(json_response["current_page"]).to eq(2)
expect(json_response["pages"]).to eq(2)
end
it 'can control the page size through a parameter' do
create(:product)
api_get :index, per_page: 1
expect(json_response['count']).to eq(1)
expect(json_response['total_count']).to eq(2)
expect(json_response['current_page']).to eq(1)
expect(json_response['pages']).to eq(2)
end
end
context "jsonp" do
it "retrieves a list of products of jsonp" do
api_get :index, callback: 'callback'
expect(response.body).to match(/^callback\(.*\)$/)
expect(response.header['Content-Type']).to include('application/javascript')
end
end
it "can search for products" do
create(:product, name: "The best product in the world")
api_get :index, q: { name_cont: "best" }
expect(json_response["products"].first).to have_attributes(keys: all_attributes)
expect(json_response["count"]).to eq(1)
end
it "gets a single product" do
product.master.images.create!(attachment: image("thinking-cat.jpg"))
product.variants.create!(unit_value: "1", unit_description: "thing")
product.variants.first.images.create!(attachment: image("thinking-cat.jpg"))
product.set_property("spree", "rocks")
api_get :show, id: product.to_param
expect(json_response).to have_attributes(keys: all_attributes)
expect(json_response['variants'].first).to have_attributes(keys: ["id", "name", "sku", "price", "weight", "height", "width", "depth", "is_master", "cost_price", "permalink", "option_values", "images"])
expect(json_response['variants'].first['images'].first).to have_attributes(keys: ["id", "position", "attachment_content_type", "attachment_file_name", "type", "attachment_updated_at", "attachment_width", "attachment_height", "alt", "viewable_type", "viewable_id", "attachment_url"])
expect(json_response["product_properties"].first).to have_attributes(keys: ["id", "product_id", "property_id", "value", "property_name"])
end
context "finds a product by permalink first then by id" do
let!(:other_product) { create(:product, permalink: "these-are-not-the-droids-you-are-looking-for") }
before do
product.update_attribute(:permalink, "#{other_product.id}-and-1-ways")
end
specify do
api_get :show, id: product.to_param
expect(json_response["permalink"]).to match(/and-1-ways/)
product.destroy
api_get :show, id: other_product.id
expect(json_response["permalink"]).to match(/droids/)
end
end
it "cannot see inactive products" do
api_get :show, id: inactive_product.to_param
expect(json_response["error"]).to eq("The resource you were looking for could not be found.")
expect(response.status).to eq(404)
end
it "returns a 404 error when it cannot find a product" do
api_get :show, id: "non-existant"
expect(json_response["error"]).to eq("The resource you were looking for could not be found.")
expect(response.status).to eq(404)
end
it "can learn how to create a new product" do
api_get :new
expect(json_response["attributes"]).to eq(["id", "name", "description", "price", "available_on", "permalink", "meta_description", "meta_keywords", "shipping_category_id", "taxon_ids"])
required_attributes = json_response["required_attributes"]
expect(required_attributes).to include("name")
expect(required_attributes).to include("price")
expect(required_attributes).to include("shipping_category_id")
end
include_examples "modifying product actions are restricted"
end
context "as an enterprise user" do
let(:current_api_user) do
user = create(:user)
user.enterprise_roles.create(enterprise: supplier)
user
end
it "retrieves a list of managed products" do
spree_get :managed, template: 'bulk_index', format: :json
response_keys = json_response.first.keys
expect(attributes.all?{ |attr| response_keys.include? attr }).to eq(true)
end
it "soft deletes my products" do
spree_delete :soft_delete, product_id: product.to_param, format: :json
expect(response.status).to eq(204)
expect { product.reload }.not_to raise_error
expect(product.deleted_at).not_to be_nil
end
it "is denied access to soft deleting another enterprises' product" do
spree_delete :soft_delete, product_id: product_other_supplier.to_param, format: :json
assert_unauthorized!
expect { product_other_supplier.reload }.not_to raise_error
expect(product_other_supplier.deleted_at).to be_nil
end
end
context "as an administrator" do
before do
allow(current_api_user)
.to receive(:has_spree_role?).with("admin").and_return(true)
end
it "retrieves a list of managed products" do
spree_get :managed, template: 'bulk_index', format: :json
response_keys = json_response.first.keys
expect(attributes.all?{ |attr| response_keys.include? attr }).to eq(true)
end
it "retrieves a list of products with appropriate attributes" do
spree_get :index, template: 'bulk_index', format: :json
response_keys = json_response.first.keys
expect(attributes.all?{ |attr| response_keys.include? attr }).to eq(true)
end
it "sorts products in ascending id order" do
FactoryBot.create(:product, supplier: supplier)
FactoryBot.create(:product, supplier: supplier)
spree_get :index, template: 'bulk_index', format: :json
ids = json_response.map{ |product| product['id'] }
expect(ids[0]).to be < ids[1]
expect(ids[1]).to be < ids[2]
end
it "formats available_on to 'yyyy-mm-dd hh:mm'" do
spree_get :index, template: 'bulk_index', format: :json
expect(json_response.map{ |product| product['available_on'] }.all?{ |a| a.match("^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}$") }).to eq(true)
end
it "returns permalink as permalink_live" do
spree_get :index, template: 'bulk_index', format: :json
expect(json_response.detect{ |product_in_response| product_in_response['id'] == product.id }['permalink_live']).to eq(product.permalink)
end
it "should allow available_on to be nil" do
spree_get :index, template: 'bulk_index', format: :json
expect(json_response.size).to eq(2)
another_product = FactoryBot.create(:product)
another_product.available_on = nil
another_product.save!
spree_get :index, template: 'bulk_index', format: :json
expect(json_response.size).to eq(3)
end
it "soft deletes a product" do
spree_delete :soft_delete, product_id: product.to_param, format: :json
expect(response.status).to eq(204)
expect { product.reload }.not_to raise_error
expect(product.deleted_at).not_to be_nil
end
it "can see all products" do
api_get :index
expect(json_response["products"].count).to eq(2)
expect(json_response["count"]).to eq(2)
expect(json_response["current_page"]).to eq(1)
expect(json_response["pages"]).to eq(1)
end
# Regression test for #1626
context "deleted products" do
before do
create(:product, deleted_at: 1.day.ago)
end
it "does not include deleted products" do
api_get :index
expect(json_response["products"].count).to eq(2)
end
it "can include deleted products" do
api_get :index, show_deleted: 1
expect(json_response["products"].count).to eq(3)
end
end
it "can create a new product" do
api_post :create, product: { name: "The Other Product",
price: 19.99,
shipping_category_id: create(:shipping_category).id,
supplier_id: supplier.id,
primary_taxon_id: FactoryBot.create(:taxon).id,
variant_unit: "items",
variant_unit_name: "things",
unit_description: "things" }
expect(json_response).to have_attributes(keys: all_attributes)
expect(response.status).to eq(201)
end
it "cannot create a new product with invalid attributes" do
api_post :create, product: {}
expect(response.status).to eq(422)
expect(json_response["error"]).to eq("Invalid resource. Please fix errors and try again.")
errors = json_response["errors"]
expect(errors.keys).to match_array(["name", "price", "primary_taxon", "shipping_category_id", "supplier", "variant_unit"])
end
it "can update a product" do
api_put :update, id: product.to_param, product: { name: "New and Improved Product!" }
expect(response.status).to eq(200)
end
it "cannot update a product with an invalid attribute" do
api_put :update, id: product.to_param, product: { name: "" }
expect(response.status).to eq(422)
expect(json_response["error"]).to eq("Invalid resource. Please fix errors and try again.")
expect(json_response["errors"]["name"]).to eq(["can't be blank"])
end
it "can delete a product" do
expect(product.deleted_at).to be_nil
api_delete :destroy, id: product.to_param
expect(response.status).to eq(204)
expect(product.reload.deleted_at).not_to be_nil
end
end
describe '#clone' do
context 'as a normal user' do
before do
allow(current_api_user)
.to receive(:has_spree_role?).with("admin").and_return(false)
end
it 'denies access' do
spree_post :clone, product_id: product.id, format: :json
assert_unauthorized!
end
end
context 'as an enterprise user' do
let(:current_api_user) do
user = create(:user)
user.enterprise_roles.create(enterprise: supplier)
user
end
it 'responds with a successful response' do
spree_post :clone, product_id: product.id, format: :json
expect(response.status).to eq(201)
end
it 'clones the product' do
spree_post :clone, product_id: product.id, format: :json
expect(json_response['name']).to eq("COPY OF #{product.name}")
end
it 'clones a product with image' do
spree_post :clone, product_id: product_with_image.id, format: :json
expect(response.status).to eq(201)
expect(json_response['name']).to eq("COPY OF #{product_with_image.name}")
end
end
context 'as an administrator' do
before do
allow(current_api_user)
.to receive(:has_spree_role?).with("admin").and_return(true)
end
it 'responds with a successful response' do
spree_post :clone, product_id: product.id, format: :json
expect(response.status).to eq(201)
end
it 'clones the product' do
spree_post :clone, product_id: product.id, format: :json
expect(json_response['name']).to eq("COPY OF #{product.name}")
end
it 'clones a product with image' do
spree_post :clone, product_id: product_with_image.id, format: :json
expect(response.status).to eq(201)
expect(json_response['name']).to eq("COPY OF #{product_with_image.name}")
end
end
end
end
end

View File

@@ -1,273 +0,0 @@
require 'spec_helper'
module Spree
describe Spree::Api::VariantsController, type: :controller do
render_views
let(:supplier) { FactoryBot.create(:supplier_enterprise) }
let!(:variant1) { FactoryBot.create(:variant) }
let!(:variant2) { FactoryBot.create(:variant) }
let!(:variant3) { FactoryBot.create(:variant) }
let(:attributes) { [:id, :options_text, :price, :on_hand, :unit_value, :unit_description, :on_demand, :display_as, :display_name] }
let!(:standard_attributes) {
[:id, :name, :sku, :price, :weight, :height,
:width, :depth, :is_master, :cost_price, :permalink]
}
before do
allow(controller).to receive(:spree_current_user) { current_api_user }
end
context "as a normal user" do
sign_in_as_user!
let!(:product) { create(:product) }
let!(:variant) do
variant = product.master
variant.option_values << create(:option_value)
variant
end
it "retrieves a list of variants with appropriate attributes" do
spree_get :index, template: 'bulk_index', format: :json
keys = json_response.first.keys.map(&:to_sym)
expect(attributes.all?{ |attr| keys.include? attr }).to eq(true)
end
it "is denied access when trying to delete a variant" do
product = create(:product)
variant = product.master
spree_delete :soft_delete, variant_id: variant.to_param, product_id: product.to_param, format: :json
assert_unauthorized!
expect { variant.reload }.not_to raise_error
expect(variant.deleted_at).to be_nil
end
it "can see a paginated list of variants" do
api_get :index
keys = json_response["variants"].first.keys.map(&:to_sym)
expect(standard_attributes.all?{ |attr| keys.include? attr }).to eq(true)
expect(json_response["count"]).to eq(11)
expect(json_response["current_page"]).to eq(1)
expect(json_response["pages"]).to eq(1)
end
it 'can control the page size through a parameter' do
create(:variant)
api_get :index, per_page: 1
expect(json_response['count']).to eq(1)
expect(json_response['current_page']).to eq(1)
expect(json_response['pages']).to eq(14)
end
it 'can query the results through a paramter' do
expected_result = create(:variant, sku: 'FOOBAR')
api_get :index, q: { sku_cont: 'FOO' }
expect(json_response['count']).to eq(1)
expect(json_response['variants'].first['sku']).to eq expected_result.sku
end
it "variants returned contain option values data" do
api_get :index
option_values = json_response["variants"].last["option_values"]
expect(option_values.first).to have_attributes(keys: ["id",
"name",
"presentation",
"option_type_name",
"option_type_id"])
end
it "variants returned contain images data" do
variant.images.create!(attachment: image("thinking-cat.jpg"))
api_get :index
expect(json_response["variants"].last["images"]).not_to be_nil
end
# Regression test for spree#2141
context "a deleted variant" do
before do
variant.update_column(:deleted_at, Time.zone.now)
end
it "is not returned in the results" do
api_get :index
expect(json_response["variants"].count).to eq(10) # there are 11 variants
end
it "is not returned even when show_deleted is passed" do
api_get :index, show_deleted: true
expect(json_response["variants"].count).to eq(10) # there are 11 variants
end
end
context "pagination" do
it "can select the next page of variants" do
second_variant = create(:variant)
api_get :index, page: 2, per_page: 1
keys = json_response["variants"].first.keys.map(&:to_sym)
expect(standard_attributes.all?{ |attr| keys.include? attr }).to eq(true)
expect(json_response["total_count"]).to eq(14)
expect(json_response["current_page"]).to eq(2)
expect(json_response["pages"]).to eq(14)
end
end
it "can see a single variant" do
api_get :show, id: variant.to_param
keys = json_response.keys.map(&:to_sym)
expect((standard_attributes + [:options_text, :option_values, :images]).all?{ |attr| keys.include? attr }).to eq(true)
option_values = json_response["option_values"]
expect(option_values.first).to have_attributes(keys: ["id", "name", "presentation", "option_type_name", "option_type_id"])
end
it "can see a single variant with images" do
variant.images.create!(attachment: image("thinking-cat.jpg"))
api_get :show, id: variant.to_param
keys = json_response.keys.map(&:to_sym)
expect((standard_attributes + [:images]).all?{ |attr| keys.include? attr }).to eq(true)
option_values_keys = json_response["option_values"].first.keys.map(&:to_sym)
expect([:name, :presentation, :option_type_id].all?{ |attr| option_values_keys.include? attr }).to eq(true)
end
it "can learn how to create a new variant" do
api_get :new
expect(json_response["attributes"]).to eq(standard_attributes.map(&:to_s))
expect(json_response["required_attributes"]).to be_empty
end
it "cannot create a new variant if not an admin" do
api_post :create, variant: { sku: "12345" }
assert_unauthorized!
end
it "cannot update a variant" do
api_put :update, id: variant.to_param, variant: { sku: "12345" }
assert_unauthorized!
end
it "cannot delete a variant" do
api_delete :destroy, id: variant.to_param
assert_unauthorized!
expect { variant.reload }.not_to raise_error
end
end
context "as an enterprise user" do
sign_in_as_enterprise_user! [:supplier]
let(:supplier_other) { create(:supplier_enterprise) }
let(:product) { create(:product, supplier: supplier) }
let(:variant) { product.master }
let(:product_other) { create(:product, supplier: supplier_other) }
let(:variant_other) { product_other.master }
it "soft deletes a variant" do
spree_delete :soft_delete, variant_id: variant.to_param, product_id: product.to_param, format: :json
expect(response.status).to eq(204)
expect { variant.reload }.not_to raise_error
expect(variant.deleted_at).to be_present
end
it "is denied access to soft deleting another enterprises' variant" do
spree_delete :soft_delete, variant_id: variant_other.to_param, product_id: product_other.to_param, format: :json
assert_unauthorized!
expect { variant.reload }.not_to raise_error
expect(variant.deleted_at).to be_nil
end
context 'when the variant is not the master' do
before { variant.update_attribute(:is_master, false) }
it 'refreshes the cache' do
expect(OpenFoodNetwork::ProductsCache).to receive(:variant_destroyed).with(variant)
spree_delete :soft_delete, variant_id: variant.id, product_id: variant.product.permalink, format: :json
end
end
end
context "as an administrator" do
sign_in_as_admin!
let(:product) { create(:product) }
let(:variant) { product.master }
let(:resource_scoping) { { product_id: variant.product.to_param } }
it "soft deletes a variant" do
spree_delete :soft_delete, variant_id: variant.to_param, product_id: product.to_param, format: :json
expect(response.status).to eq(204)
expect { variant.reload }.not_to raise_error
expect(variant.deleted_at).not_to be_nil
end
it "doesn't delete the only variant of the product" do
product = create(:product)
variant = product.variants.first
spree_delete :soft_delete, variant_id: variant.to_param, product_id: product.to_param, format: :json
expect(variant.reload).to_not be_deleted
expect(assigns(:variant).errors[:product]).to include "must have at least one variant"
end
context 'when the variant is not the master' do
before { variant.update_attribute(:is_master, false) }
it 'refreshes the cache' do
expect(OpenFoodNetwork::ProductsCache).to receive(:variant_destroyed).with(variant)
spree_delete :soft_delete, variant_id: variant.id, product_id: variant.product.permalink, format: :json
end
end
context "deleted variants" do
before do
variant.update_column(:deleted_at, Time.zone.now)
end
it "are visible by admin" do
api_get :index, show_deleted: 1
expect(json_response["variants"].count).to eq(2)
end
end
it "can create a new variant" do
original_number_of_variants = variant.product.variants.count
api_post :create, variant: { sku: "12345", unit_value: "weight", unit_description: "L" }
expect(standard_attributes.all?{ |attr| json_response.include? attr.to_s }).to eq(true)
expect(response.status).to eq(201)
expect(json_response["sku"]).to eq("12345")
expect(variant.product.variants.count).to eq(original_number_of_variants + 1)
end
it "can update a variant" do
api_put :update, id: variant.to_param, variant: { sku: "12345" }
expect(response.status).to eq(200)
end
it "can delete a variant" do
api_delete :destroy, id: variant.to_param
expect(response.status).to eq(204)
expect { Spree::Variant.find(variant.id) }.to raise_error(ActiveRecord::RecordNotFound)
end
end
end
end