diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index d42c7b3a2c2..0e57ab39afd 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -681,7 +681,7 @@ Rails/ActiveRecordOverride: # This cop supports unsafe autocorrection (--autocorrect-all). Rails/ApplicationController: Exclude: - - 'engines/dfc_provider/app/controllers/dfc_provider/api/base_controller.rb' + - 'engines/dfc_provider/app/controllers/dfc_provider/base_controller.rb' # Offense count: 6 # This cop supports unsafe autocorrection (--autocorrect-all). diff --git a/config/routes.rb b/config/routes.rb index 9c449ed11d6..e1bc3394d1b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -117,11 +117,6 @@ get 'sitemap.xml', to: 'sitemap#index', defaults: { format: 'xml' } - constraints FeatureToggleConstraint.new(:dfc_provider) do - # Mount DFC API endpoints - mount DfcProvider::Engine, at: '/' - end - # Mount Spree's routes mount Spree::Core::Engine, :at => '/' diff --git a/config/routes/api.rb b/config/routes/api.rb index 0a830a60f23..08bfb6faa27 100644 --- a/config/routes/api.rb +++ b/config/routes/api.rb @@ -5,6 +5,12 @@ end namespace :api do + + constraints FeatureToggleConstraint.new(:dfc_provider) do + # Mount DFC API endpoints + mount DfcProvider::Engine, at: '/dfc-v1.6/' + end + namespace :v0 do resources :products do collection do diff --git a/engines/dfc_provider/app/controllers/dfc_provider/api/base_controller.rb b/engines/dfc_provider/app/controllers/dfc_provider/api/base_controller.rb deleted file mode 100644 index 7ff59bdb0ae..00000000000 --- a/engines/dfc_provider/app/controllers/dfc_provider/api/base_controller.rb +++ /dev/null @@ -1,65 +0,0 @@ -# frozen_string_literal: true - -# Controller used to provide the API products for the DFC application -module DfcProvider - module Api - class BaseController < ActionController::Base - rescue_from ActiveRecord::RecordNotFound, with: :not_found - - before_action :check_authorization, - :check_user - - respond_to :json - - private - - def check_authorization - return if access_token.present? - - head :unprocessable_entity - end - - def check_user - return if current_user.present? - - head :unauthorized - end - - def check_enterprise - return if current_enterprise.present? - - not_found - end - - def current_enterprise - @current_enterprise ||= - case params[enterprise_id_param_name] - when 'default' - current_user.enterprises.first! - else - current_user.enterprises.find(params[enterprise_id_param_name]) - end - end - - def enterprise_id_param_name - :enterprise_id - end - - def current_user - @current_user ||= authorization_control.process - end - - def access_token - request.headers['Authorization'].to_s.split(' ').last - end - - def authorization_control - DfcProvider::AuthorizationControl.new(access_token) - end - - def not_found - head :not_found - end - end - end -end diff --git a/engines/dfc_provider/app/controllers/dfc_provider/api/catalog_items_controller.rb b/engines/dfc_provider/app/controllers/dfc_provider/api/catalog_items_controller.rb deleted file mode 100644 index 9d4419d84f1..00000000000 --- a/engines/dfc_provider/app/controllers/dfc_provider/api/catalog_items_controller.rb +++ /dev/null @@ -1,30 +0,0 @@ -# frozen_string_literal: true - -# Controller used to provide the API products for the DFC application -# CatalogItems are items that are being sold by the entreprise. -module DfcProvider - module Api - class CatalogItemsController < DfcProvider::Api::BaseController - before_action :check_enterprise - - def index - # CatalogItem is nested into an entreprise which is also nested into - # an user on the DFC specifications, as defined here: - # https://datafoodconsortium.gitbook.io/dfc-standard-documentation - # /technical-specification/api-examples - render json: current_user, serializer: DfcProvider::PersonSerializer - end - - def show - render json: variant, serializer: DfcProvider::CatalogItemSerializer - end - - private - - def variant - @variant ||= - DfcProvider::VariantFetcher.new(current_enterprise).scope.find(params[:id]) - end - end - end -end diff --git a/engines/dfc_provider/app/controllers/dfc_provider/api/enterprises_controller.rb b/engines/dfc_provider/app/controllers/dfc_provider/api/enterprises_controller.rb deleted file mode 100644 index f68ffc30875..00000000000 --- a/engines/dfc_provider/app/controllers/dfc_provider/api/enterprises_controller.rb +++ /dev/null @@ -1,20 +0,0 @@ -# frozen_string_literal: true - -# Controller used to provide the CatalogItem API for the DFC application -module DfcProvider - module Api - class EnterprisesController < DfcProvider::Api::BaseController - before_action :check_enterprise - - def show - render json: current_enterprise, serializer: DfcProvider::EnterpriseSerializer - end - - private - - def enterprise_id_param_name - :id - end - end - end -end diff --git a/engines/dfc_provider/app/controllers/dfc_provider/api/persons_controller.rb b/engines/dfc_provider/app/controllers/dfc_provider/api/persons_controller.rb deleted file mode 100644 index 82b00b11f9c..00000000000 --- a/engines/dfc_provider/app/controllers/dfc_provider/api/persons_controller.rb +++ /dev/null @@ -1,26 +0,0 @@ -# frozen_string_literal: true - -# Controller used to provide the Persons API for the DFC application -module DfcProvider - module Api - class PersonsController < DfcProvider::Api::BaseController - before_action :check_user_accessibility - - def show - render json: user, serializer: DfcProvider::PersonSerializer - end - - private - - def user - @user ||= Spree::User.find(params[:id]) - end - - def check_user_accessibility - return if current_user == user - - not_found - end - end - end -end diff --git a/engines/dfc_provider/app/controllers/dfc_provider/api/supplied_products_controller.rb b/engines/dfc_provider/app/controllers/dfc_provider/api/supplied_products_controller.rb deleted file mode 100644 index c711017c132..00000000000 --- a/engines/dfc_provider/app/controllers/dfc_provider/api/supplied_products_controller.rb +++ /dev/null @@ -1,22 +0,0 @@ -# frozen_string_literal: true - -# Controller used to provide the SuppliedProducts API for the DFC application -# SuppliedProducts are products that are managed by an enterprise. -module DfcProvider - module Api - class SuppliedProductsController < DfcProvider::Api::BaseController - before_action :check_enterprise - - def show - render json: variant, serializer: DfcProvider::SuppliedProductSerializer - end - - private - - def variant - @variant ||= - DfcProvider::VariantFetcher.new(current_enterprise).scope.find(params[:id]) - end - end - end -end diff --git a/engines/dfc_provider/app/controllers/dfc_provider/base_controller.rb b/engines/dfc_provider/app/controllers/dfc_provider/base_controller.rb new file mode 100644 index 00000000000..bcee5818364 --- /dev/null +++ b/engines/dfc_provider/app/controllers/dfc_provider/base_controller.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +# Controller used to provide the API products for the DFC application +module DfcProvider + class BaseController < ActionController::Base + protect_from_forgery with: :null_session + + rescue_from ActiveRecord::RecordNotFound, with: :not_found + + before_action :check_authorization + + respond_to :json + + private + + def check_authorization + head :unauthorized if current_user.nil? + end + + def check_enterprise + return if current_enterprise.present? + + not_found + end + + def current_enterprise + @current_enterprise ||= + case params[enterprise_id_param_name] + when 'default' + current_user.enterprises.first! + else + current_user.enterprises.find(params[enterprise_id_param_name]) + end + end + + def enterprise_id_param_name + :enterprise_id + end + + def current_user + @current_user ||= authorization_control.user + end + + def authorization_control + DfcProvider::AuthorizationControl.new(request) + end + + def not_found + head :not_found + end + end +end diff --git a/engines/dfc_provider/app/controllers/dfc_provider/catalog_items_controller.rb b/engines/dfc_provider/app/controllers/dfc_provider/catalog_items_controller.rb new file mode 100644 index 00000000000..6851e7dd93c --- /dev/null +++ b/engines/dfc_provider/app/controllers/dfc_provider/catalog_items_controller.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +# Controller used to provide the API products for the DFC application +# CatalogItems are items that are being sold by the entreprise. +module DfcProvider + class CatalogItemsController < DfcProvider::BaseController + before_action :check_enterprise + + def index + # CatalogItem is nested into an entreprise which is also nested into + # an user on the DFC specifications, as defined here: + # https://datafoodconsortium.gitbook.io/dfc-standard-documentation + # /technical-specification/api-examples + render json: current_user, serializer: DfcProvider::PersonSerializer + end + + def show + render json: variant, serializer: DfcProvider::CatalogItemSerializer + end + + private + + def variant + @variant ||= + DfcProvider::VariantFetcher.new(current_enterprise).scope.find(params[:id]) + end + end +end diff --git a/engines/dfc_provider/app/controllers/dfc_provider/enterprises_controller.rb b/engines/dfc_provider/app/controllers/dfc_provider/enterprises_controller.rb new file mode 100644 index 00000000000..1298584da6a --- /dev/null +++ b/engines/dfc_provider/app/controllers/dfc_provider/enterprises_controller.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +# Controller used to provide the CatalogItem API for the DFC application +module DfcProvider + class EnterprisesController < DfcProvider::BaseController + before_action :check_enterprise + + def show + render json: current_enterprise, serializer: DfcProvider::EnterpriseSerializer + end + + private + + def enterprise_id_param_name + :id + end + end +end diff --git a/engines/dfc_provider/app/controllers/dfc_provider/persons_controller.rb b/engines/dfc_provider/app/controllers/dfc_provider/persons_controller.rb new file mode 100644 index 00000000000..952152e3236 --- /dev/null +++ b/engines/dfc_provider/app/controllers/dfc_provider/persons_controller.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +# Controller used to provide the Persons API for the DFC application +module DfcProvider + class PersonsController < DfcProvider::BaseController + before_action :check_user_accessibility + + def show + render json: user, serializer: DfcProvider::PersonSerializer + end + + private + + def user + @user ||= Spree::User.find(params[:id]) + end + + def check_user_accessibility + return if current_user == user + + not_found + end + end +end diff --git a/engines/dfc_provider/app/controllers/dfc_provider/supplied_products_controller.rb b/engines/dfc_provider/app/controllers/dfc_provider/supplied_products_controller.rb new file mode 100644 index 00000000000..f1ac6c08c07 --- /dev/null +++ b/engines/dfc_provider/app/controllers/dfc_provider/supplied_products_controller.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +# Controller used to provide the SuppliedProducts API for the DFC application +# SuppliedProducts are products that are managed by an enterprise. +module DfcProvider + class SuppliedProductsController < DfcProvider::BaseController + before_action :check_enterprise + + def show + render json: variant, serializer: DfcProvider::SuppliedProductSerializer + end + + def update + dfc_request = JSON.parse(request.body.read) + return unless dfc_request.key?("dfc-b:description") + + variant.product.update!(name: dfc_request["dfc-b:description"]) + end + + private + + def variant + @variant ||= + DfcProvider::VariantFetcher.new(current_enterprise).scope.find(params[:id]) + end + end +end diff --git a/engines/dfc_provider/app/serializers/dfc_provider/catalog_item_serializer.rb b/engines/dfc_provider/app/serializers/dfc_provider/catalog_item_serializer.rb index a91a3b51ca7..edaf72ae7c0 100644 --- a/engines/dfc_provider/app/serializers/dfc_provider/catalog_item_serializer.rb +++ b/engines/dfc_provider/app/serializers/dfc_provider/catalog_item_serializer.rb @@ -6,15 +6,15 @@ module DfcProvider class CatalogItemSerializer < BaseSerializer attribute :id, key: '@id' attribute :type, key: '@type' - attribute :references, key: 'dfc:references' - attribute :sku, key: 'dfc:sku' - attribute :stock_limitation, key: 'dfc:stockLimitation' + attribute :references, key: 'dfc-b:references' + attribute :sku, key: 'dfc-b:sku' + attribute :stock_limitation, key: 'dfc-b:stockLimitation' has_many :offered_through, serializer: DfcProvider::OfferSerializer, - key: 'dfc:offeredThrough' + key: 'dfc-b:offeredThrough' def id - dfc_provider_routes.api_dfc_provider_enterprise_catalog_item_url( + dfc_provider_routes.enterprise_catalog_item_url( enterprise_id: object.product.supplier_id, id: object.id, host: host @@ -22,7 +22,7 @@ def id end def type - 'dfc:CatalogItem' + 'dfc-b:CatalogItem' end def references @@ -41,7 +41,7 @@ def offered_through private def reference_id - dfc_provider_routes.api_dfc_provider_enterprise_supplied_product_url( + dfc_provider_routes.enterprise_supplied_product_url( enterprise_id: object.product.supplier_id, id: object.id, host: host diff --git a/engines/dfc_provider/app/serializers/dfc_provider/enterprise_serializer.rb b/engines/dfc_provider/app/serializers/dfc_provider/enterprise_serializer.rb index 19ad8fe19e1..7428a546520 100644 --- a/engines/dfc_provider/app/serializers/dfc_provider/enterprise_serializer.rb +++ b/engines/dfc_provider/app/serializers/dfc_provider/enterprise_serializer.rb @@ -9,14 +9,14 @@ class EnterpriseSerializer < BaseSerializer attribute :vat_number, key: 'dfc:VATnumber' has_many :defines, key: 'dfc:defines' has_many :supplies, - key: 'dfc:supplies', + key: 'dfc-b:supplies', serializer: DfcProvider::SuppliedProductSerializer has_many :manages, - key: 'dfc:manages', + key: 'dfc-b:manages', serializer: DfcProvider::CatalogItemSerializer def id - dfc_provider_routes.api_dfc_provider_enterprise_url( + dfc_provider_routes.enterprise_url( id: object.id, host: host ) diff --git a/engines/dfc_provider/app/serializers/dfc_provider/offer_serializer.rb b/engines/dfc_provider/app/serializers/dfc_provider/offer_serializer.rb index bf5c5daafe1..19bdfa75647 100644 --- a/engines/dfc_provider/app/serializers/dfc_provider/offer_serializer.rb +++ b/engines/dfc_provider/app/serializers/dfc_provider/offer_serializer.rb @@ -7,8 +7,8 @@ class OfferSerializer < BaseSerializer attribute :id, key: '@id' attribute :type, key: '@type' attribute :offers_to, key: 'dfc:offers_to' - attribute :price, key: 'dfc:price' - attribute :stock_limitation, key: 'dfc:stockLimitation' + attribute :price, key: 'dfc-b:price' + attribute :stock_limitation, key: 'dfc-b:stockLimitation' def id "/offers/#{object.id}" @@ -20,8 +20,7 @@ def type def offers_to { - '@type' => '@id', - '@id' => nil + '@type' => '@id' } end diff --git a/engines/dfc_provider/app/serializers/dfc_provider/person_serializer.rb b/engines/dfc_provider/app/serializers/dfc_provider/person_serializer.rb index bbf08773498..2e93775167f 100644 --- a/engines/dfc_provider/app/serializers/dfc_provider/person_serializer.rb +++ b/engines/dfc_provider/app/serializers/dfc_provider/person_serializer.rb @@ -13,27 +13,29 @@ class PersonSerializer < BaseSerializer key: 'dfc:hasAddress', serializer: DfcProvider::AddressSerializer has_many :affiliates, - key: 'dfc:affiliates', + key: 'dfc-b:affiliates', serializer: DfcProvider::EnterpriseSerializer # Context should be provided inside the controller, # but AMS doesn't not supported `meta` and `meta_key` with `root` to nil... def context { - 'dfc' => 'http://datafoodconsortium.org/ontologies/DFC_FullModel.owl#', + "dfc" => "http://static.datafoodconsortium.org/ontologies/DFC_FullModel.owl#", + "dfc-b" => "http://static.datafoodconsortium.org/ontologies/DFC_BusinessOntology.owl#", + "rdfs" => "http://www.w3.org/2000/01/rdf-schema#", '@base' => "#{root_url}api/dfc_provider" } end def id - dfc_provider_routes.api_dfc_provider_person_url( + dfc_provider_routes.person_url( id: object.id, host: host ) end def type - 'dfc:Person' + 'dfc-b:Person' end def family_name; end diff --git a/engines/dfc_provider/app/serializers/dfc_provider/supplied_product_serializer.rb b/engines/dfc_provider/app/serializers/dfc_provider/supplied_product_serializer.rb index 0613db72fb4..db6540535d6 100644 --- a/engines/dfc_provider/app/serializers/dfc_provider/supplied_product_serializer.rb +++ b/engines/dfc_provider/app/serializers/dfc_provider/supplied_product_serializer.rb @@ -6,18 +6,18 @@ module DfcProvider class SuppliedProductSerializer < BaseSerializer attribute :id, key: '@id' attribute :type, key: '@type' - attribute :unit, key: 'dfc:hasUnit' - attribute :quantity, key: 'dfc:quantity' - attribute :description, key: 'dfc:description' - attribute :total_theoritical_stock, key: 'dfc:totalTheoriticalStock' - attribute :brand, key: 'dfc:brand' - attribute :claim, key: 'dfc:claim' - attribute :image, key: 'dfc:image' + attribute :unit, key: 'dfc-b:hasUnit' + attribute :quantity, key: 'dfc-b:quantity' + attribute :description, key: 'dfc-b:description' + attribute :total_theoritical_stock, key: 'dfc-b:totalTheoriticalStock' + attribute :brand, key: 'dfc-b:brand' + attribute :claim, key: 'dfc-b:claim' + attribute :image, key: 'dfc-b:image' attribute :life_time, key: 'lifeTime' - has_many :physical_characteristics, key: 'dfc:physicalCharacterisctics' + has_many :physical_characteristics, key: 'dfc-b:physicalCharacterisctics' def id - dfc_provider_routes.api_dfc_provider_enterprise_supplied_product_url( + dfc_provider_routes.enterprise_supplied_product_url( enterprise_id: object.product.supplier_id, id: object.id, host: host @@ -50,7 +50,7 @@ def brand; end def claim; end def image - object.images.first.try(:attachment, :url) + object.images.first&.url(:product) end def life_time; end diff --git a/engines/dfc_provider/app/services/dfc_provider/authorization_control.rb b/engines/dfc_provider/app/services/dfc_provider/authorization_control.rb index beb1885816d..dcdbd8ad5fe 100644 --- a/engines/dfc_provider/app/services/dfc_provider/authorization_control.rb +++ b/engines/dfc_provider/app/services/dfc_provider/authorization_control.rb @@ -4,28 +4,53 @@ # It controls an OICD Access token and an enterprise. module DfcProvider class AuthorizationControl - def initialize(access_token) - @access_token = access_token + # Copied from: https://login.lescommuns.org/auth/realms/data-food-consortium/ + LES_COMMUNES_PUBLIC_KEY = <<~KEY + -----BEGIN PUBLIC KEY----- + MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAl68JGqAILFzoi/1+6siXXp2vylu+7mPjYKjKelTtHFYXWVkbmVptCsamHlY3jRhqSQYe6M1SKfw8D+uXrrWsWficYvpdlV44Vm7uETZOr1/XBOjpWOi1vLmBVtX6jFeqN1BxfE1PxLROAiGn+MeMg90AJKShD2c5RoNv26e20dgPhshRVFPUGru+0T1RoKyIa64z/qcTcTVD2V7KX+ANMweRODdoPAzQFGGjTnL1uUqIdUwSfHSpXYnKxXOsnPC3Mowkv8UIGWWDxS/yzhWc7sOk1NmC7pb+Cg7G8NKj+Pp9qQZnXF39Dg95ZsxJrl6fyPFvTo3zf9CPG/fUM1CkkwIDAQAB + -----END PUBLIC KEY----- + KEY + + def self.public_key + OpenSSL::PKey::RSA.new(LES_COMMUNES_PUBLIC_KEY) + end + + def initialize(request) + @request = request + end + + def user + oidc_user || ofn_user + rescue JWT::ExpiredSignature + nil end - def process - decode_token - find_ofn_user + private + + def oidc_user + find_ofn_user(decode_token) if access_token + end + + def ofn_user + @request.env['warden']&.user end def decode_token - data = JWT.decode( - @access_token, - nil, - false - ) + JWT.decode( + access_token, + self.class.public_key, + true, { algorithm: "RS256" } + ).first + end - @header = data.last - @payload = data.first + def access_token + @request.headers['Authorization'].to_s.split(' ').last end - def find_ofn_user - Spree::User.where(email: @payload['email']).first + def find_ofn_user(payload) + return if payload["email"].blank? + + Spree::User.find_by(uid: payload["email"]) end end end diff --git a/engines/dfc_provider/config/routes.rb b/engines/dfc_provider/config/routes.rb index b9ea25e15e2..a551e620a1c 100644 --- a/engines/dfc_provider/config/routes.rb +++ b/engines/dfc_provider/config/routes.rb @@ -1,13 +1,9 @@ # frozen_string_literal: true DfcProvider::Engine.routes.draw do - namespace :api do - scope :dfc_provider, as: :dfc_provider, path: '/dfc_provider' do - resources :enterprises, only: [:show] do - resources :catalog_items, only: [:index, :show] - resources :supplied_products, only: [:show] - end - resources :persons, only: [:show] - end + resources :enterprises, only: [:show] do + resources :catalog_items, only: [:index, :show] + resources :supplied_products, only: [:show, :update] end + resources :persons, only: [:show] end diff --git a/engines/dfc_provider/spec/controllers/dfc_provider/api/catalog_items_controller_spec.rb b/engines/dfc_provider/spec/controllers/dfc_provider/catalog_items_controller_spec.rb similarity index 73% rename from engines/dfc_provider/spec/controllers/dfc_provider/api/catalog_items_controller_spec.rb rename to engines/dfc_provider/spec/controllers/dfc_provider/catalog_items_controller_spec.rb index 29558dd00a2..40b22279074 100644 --- a/engines/dfc_provider/spec/controllers/dfc_provider/api/catalog_items_controller_spec.rb +++ b/engines/dfc_provider/spec/controllers/dfc_provider/catalog_items_controller_spec.rb @@ -1,35 +1,29 @@ # frozen_string_literal: true -require 'spec_helper' +require DfcProvider::Engine.root.join("spec/spec_helper") + +describe DfcProvider::CatalogItemsController, type: :controller do + include AuthorizationHelper -describe DfcProvider::Api::CatalogItemsController, type: :controller do render_views - let!(:user) { create(:user) } + let!(:user) { create(:oidc_user) } let!(:enterprise) { create(:distributor_enterprise, owner: user) } let!(:product) { create(:simple_product, supplier: enterprise ) } let!(:variant) { product.variants.first } describe '.index' do context 'with authorization token' do - before do - request.headers['Authorization'] = 'Bearer 123456.abcdef.123456' - end + before { authorise user.email } context 'with an authenticated user' do - before do - allow_any_instance_of(DfcProvider::AuthorizationControl) - .to receive(:process) - .and_return(user) - end - context 'with an enterprise' do context 'given with an id' do context 'related to the user' do before { api_get :index, enterprise_id: 'default' } it 'is successful' do - expect(response).to be_successful + expect(response).to have_http_status :success end it 'renders the required content' do @@ -47,7 +41,7 @@ it 'returns not_found head' do api_get :index, enterprise_id: enterprise.id - expect(response).to be_not_found + expect(response).to have_http_status :not_found end end end @@ -75,16 +69,16 @@ it 'is not found' do api_get :index, enterprise_id: 'default' - expect(response).to be_not_found + expect(response).to have_http_status :not_found end end end context 'without an authenticated user' do + before { authorise "other@user.net" } + it 'returns unauthorized head' do - allow_any_instance_of(DfcProvider::AuthorizationControl) - .to receive(:process) - .and_return(nil) + authorise "other@user.net" api_get :index, enterprise_id: 'default' expect(response.response_code).to eq(401) @@ -93,26 +87,26 @@ end context 'without an authorization token' do - it 'returns unprocessable_entity head' do + it 'returns unauthorized head' do + api_get :index, enterprise_id: enterprise.id + expect(response).to have_http_status :unauthorized + end + end + + context "when logged in as app user" do + it "is successful" do + sign_in user api_get :index, enterprise_id: enterprise.id - expect(response).to be_unprocessable + expect(response).to have_http_status :success end end end describe '.show' do context 'with authorization token' do - before do - request.headers['Authorization'] = 'Bearer 123456.abcdef.123456' - end + before { authorise user.email } context 'with an authenticated user' do - before do - allow_any_instance_of(DfcProvider::AuthorizationControl) - .to receive(:process) - .and_return(user) - end - context 'with an enterprise' do context 'given with an id' do before do @@ -120,11 +114,11 @@ end it 'is successful' do - expect(response).to be_successful + expect(response).to have_http_status :success end it 'renders the required content' do - expect(response.body).to include('dfc:CatalogItem') + expect(response.body).to include('dfc-b:CatalogItem') expect(response.body).to include("offers/#{variant.id}") end end @@ -137,7 +131,7 @@ end it 'is not found' do - expect(response).to be_not_found + expect(response).to have_http_status :not_found end end end diff --git a/engines/dfc_provider/spec/controllers/dfc_provider/api/enterprises_spec.rb b/engines/dfc_provider/spec/controllers/dfc_provider/enterprises_spec.rb similarity index 92% rename from engines/dfc_provider/spec/controllers/dfc_provider/api/enterprises_spec.rb rename to engines/dfc_provider/spec/controllers/dfc_provider/enterprises_spec.rb index 04c5003e3d0..b97e2d54f90 100644 --- a/engines/dfc_provider/spec/controllers/dfc_provider/api/enterprises_spec.rb +++ b/engines/dfc_provider/spec/controllers/dfc_provider/enterprises_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe DfcProvider::Api::EnterprisesController, type: :controller do +describe DfcProvider::EnterprisesController, type: :controller do render_views let!(:user) { create(:user) } @@ -18,7 +18,7 @@ context 'with an authenticated user' do before do allow_any_instance_of(DfcProvider::AuthorizationControl) - .to receive(:process) + .to receive(:user) .and_return(user) end diff --git a/engines/dfc_provider/spec/controllers/dfc_provider/api/persons_controller_spec.rb b/engines/dfc_provider/spec/controllers/dfc_provider/persons_controller_spec.rb similarity index 85% rename from engines/dfc_provider/spec/controllers/dfc_provider/api/persons_controller_spec.rb rename to engines/dfc_provider/spec/controllers/dfc_provider/persons_controller_spec.rb index 81498c1f46f..241bbd8d518 100644 --- a/engines/dfc_provider/spec/controllers/dfc_provider/api/persons_controller_spec.rb +++ b/engines/dfc_provider/spec/controllers/dfc_provider/persons_controller_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe DfcProvider::Api::PersonsController, type: :controller do +describe DfcProvider::PersonsController, type: :controller do render_views let!(:user) { create(:user) } @@ -16,7 +16,7 @@ context 'with an authenticated user' do before do allow_any_instance_of(DfcProvider::AuthorizationControl) - .to receive(:process) + .to receive(:user) .and_return(user) end @@ -28,7 +28,7 @@ end it 'renders the required content' do - expect(response.body).to include('dfc:Person') + expect(response.body).to include('dfc-b:Person') end end diff --git a/engines/dfc_provider/spec/controllers/dfc_provider/api/supplied_products_controller_spec.rb b/engines/dfc_provider/spec/controllers/dfc_provider/supplied_products_controller_spec.rb similarity index 56% rename from engines/dfc_provider/spec/controllers/dfc_provider/api/supplied_products_controller_spec.rb rename to engines/dfc_provider/spec/controllers/dfc_provider/supplied_products_controller_spec.rb index b739ae2176e..dd555262a28 100644 --- a/engines/dfc_provider/spec/controllers/dfc_provider/api/supplied_products_controller_spec.rb +++ b/engines/dfc_provider/spec/controllers/dfc_provider/supplied_products_controller_spec.rb @@ -1,11 +1,13 @@ # frozen_string_literal: true -require 'spec_helper' +require DfcProvider::Engine.root.join("spec/spec_helper") + +describe DfcProvider::SuppliedProductsController, type: :controller do + include AuthorizationHelper -describe DfcProvider::Api::SuppliedProductsController, type: :controller do render_views - let!(:user) { create(:user) } + let!(:user) { create(:oidc_user) } let!(:enterprise) { create(:distributor_enterprise, owner: user) } let!(:product) { create(:simple_product, supplier: enterprise ) } let!(:variant) { product.variants.first } @@ -19,7 +21,7 @@ context 'with an authenticated user' do before do allow_any_instance_of(DfcProvider::AuthorizationControl) - .to receive(:process) + .to receive(:user) .and_return(user) end @@ -49,4 +51,30 @@ end end end + + describe "#update" do + routes { DfcProvider::Engine.routes } + + it "requires authorisation" do + api_put :update, enterprise_id: "default", id: "0" + expect(response).to have_http_status :unauthorized + end + + describe "with authorisation" do + before { authorise user.email } + + it "updates the variant's name" do + params = { enterprise_id: enterprise.id, id: variant.id } + request_body = File.read(File.join(__dir__, "../../support/patch_product.json")) + + expect { + put(:update, params: params, body: request_body) + expect(response).to have_http_status :success + variant.reload + }.to change { + variant.name + } + end + end + end end diff --git a/engines/dfc_provider/spec/serializers/dfc_provider/catalog_item_serializer_spec.rb b/engines/dfc_provider/spec/serializers/dfc_provider/catalog_item_serializer_spec.rb index 83f392b77de..d8a0266dda5 100644 --- a/engines/dfc_provider/spec/serializers/dfc_provider/catalog_item_serializer_spec.rb +++ b/engines/dfc_provider/spec/serializers/dfc_provider/catalog_item_serializer_spec.rb @@ -11,12 +11,11 @@ describe '#id' do let(:catalog_item_id) { [ - 'http://test.host/api/dfc_provider', - 'enterprises', + "http://test.host/api/dfc-v1.6/enterprises/", product.supplier_id, - 'catalog_items', + "/catalog_items/", variant.id - ].join('/') + ].join } it 'returns the expected value' do @@ -27,12 +26,11 @@ describe '#references' do let(:supplied_product_id) { [ - 'http://test.host/api/dfc_provider', - 'enterprises', + "http://test.host/api/dfc-v1.6/enterprises/", product.supplier_id, - 'supplied_products', + "/supplied_products/", variant.id - ].join('/') + ].join } it 'returns the expected value' do diff --git a/engines/dfc_provider/spec/serializers/dfc_provider/supplied_product_serializer_spec.rb b/engines/dfc_provider/spec/serializers/dfc_provider/supplied_product_serializer_spec.rb index 8f5c348d04a..49b8e291ea5 100644 --- a/engines/dfc_provider/spec/serializers/dfc_provider/supplied_product_serializer_spec.rb +++ b/engines/dfc_provider/spec/serializers/dfc_provider/supplied_product_serializer_spec.rb @@ -11,12 +11,11 @@ describe '#id' do let(:supplied_product_id) { [ - 'http://test.host/api/dfc_provider', - 'enterprises', + "http://test.host/api/dfc-v1.6/enterprises/", product.supplier_id, - 'supplied_products', + "/supplied_products/", variant.id - ].join('/') + ].join } it 'returns the expected value' do diff --git a/engines/dfc_provider/spec/services/authorization_control_spec.rb b/engines/dfc_provider/spec/services/authorization_control_spec.rb new file mode 100644 index 00000000000..ea52c5806ee --- /dev/null +++ b/engines/dfc_provider/spec/services/authorization_control_spec.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require DfcProvider::Engine.root.join("spec/spec_helper") + +describe DfcProvider::AuthorizationControl do + include AuthorizationHelper + + let(:user) { create(:oidc_user) } + + describe "with OIDC token" do + it "finds the right user" do + create(:oidc_user) # another user + token = allow_token_for(email: user.email) + + expect(auth(token).user).to eq user + end + + it "ignores blank email" do + create(:user, uid: nil) + create(:user, uid: "") + token = allow_token_for(email: nil) + + expect(auth(token).user).to eq nil + end + + it "ignores non-existent user" do + user + token = allow_token_for(email: generate(:random_email)) + + expect(auth(token).user).to eq nil + end + + it "ignores expired signatures" do + token = allow_token_for(exp: Time.now.to_i, email: user.email) + + expect(auth(token).user).to eq nil + end + end + + def auth(token) + described_class.new( + double(:request, + headers: { "Authorization" => "Bearer #{token}" }, + env: { 'warden' => nil }) + ) + end +end diff --git a/engines/dfc_provider/spec/spec_helper.rb b/engines/dfc_provider/spec/spec_helper.rb index f6b5d7710ef..de38a84c632 100644 --- a/engines/dfc_provider/spec/spec_helper.rb +++ b/engines/dfc_provider/spec/spec_helper.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true -require '../../spec/spec_helper' +require_relative '../../../spec/spec_helper' Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].sort.each { |f| require f } diff --git a/engines/dfc_provider/spec/support/authorization_helper.rb b/engines/dfc_provider/spec/support/authorization_helper.rb new file mode 100644 index 00000000000..f35e028f1a0 --- /dev/null +++ b/engines/dfc_provider/spec/support/authorization_helper.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module AuthorizationHelper + def authorise(email) + token = allow_token_for(email: email) + request.headers["Authorization"] = "JWT #{token}" + end + + def allow_token_for(payload) + private_key = OpenSSL::PKey::RSA.generate 2048 + allow(DfcProvider::AuthorizationControl).to receive(:public_key). + and_return(private_key.public_key) + + JWT.encode(payload, private_key, "RS256") + end +end diff --git a/engines/dfc_provider/spec/support/patch_product.json b/engines/dfc_provider/spec/support/patch_product.json new file mode 100644 index 00000000000..9e415bec9ad --- /dev/null +++ b/engines/dfc_provider/spec/support/patch_product.json @@ -0,0 +1,88 @@ +{ + "@context": { + "rdfs": "http://www.w3.org/2000/01/rdf-schema#", + "skos": "http://www.w3.org/2004/02/skos/core#", + "dfc": "http://static.datafoodconsortium.org/ontologies/DFC_FullModel.owl#", + "dc": "http://purl.org/dc/elements/1.1/#", + "dfc-b": "http://static.datafoodconsortium.org/ontologies/DFC_BusinessOntology.owl#", + "dfc-p": "http://static.datafoodconsortium.org/ontologies/DFC_ProductOntology.owl#", + "dfc-t": "http://static.datafoodconsortium.org/ontologies/DFC_TechnicalOntology.owl#", + "dfc-u": "http://static.datafoodconsortium.org/data/units.rdf#", + "dfc-pt": "http://static.datafoodconsortium.org/data/productTypes.rdf#", + "dfc-a": "http://static.datafoodconsortium.org/data/claims.rdf#", + "dfc-d": "http://static.datafoodconsortium.org/data/dimensions.rdf#", + "dfc-c": "http://static.datafoodconsortium.org/data/certifications.rdf#", + "dfc-g": "http://static.datafoodconsortium.org/data/geoOrigin.rdf#", + "dfc-p:hasUnit": { + "@type": "@id" + }, + "dfc-b:hasUnit": { + "@type": "@id" + }, + "dfc-b:hasQuantity": { + "@type": "@id" + }, + "dfc-p:hasType": { + "@type": "@id" + }, + "dfc-b:hasType": { + "@type": "@id" + }, + "dfc-b:references": { + "@type": "@id" + }, + "dfc-b:referencedBy": { + "@type": "@id" + }, + "dfc-b:offeres": { + "@type": "@id" + }, + "dfc-b:supplies": { + "@type": "@id" + }, + "dfc-b:defines": { + "@type": "@id" + }, + "dfc-b:affiliates": { + "@type": "@id" + }, + "dfc-b:manages": { + "@type": "@id" + }, + "dfc-b:offeredThrough": { + "@type": "@id" + }, + "dfc-b:hasBrand": { + "@type": "@id" + }, + "dfc-b:hasGeographicalOrigin": { + "@type": "@id" + }, + "dfc-b:hasClaim": { + "@type": "@id" + }, + "dfc-b:hasAllergenDimension": { + "@type": "@id" + }, + "dfc-b:hasNutrimentDimension": { + "@type": "@id" + }, + "dfc-b:hasPhysicalDimension": { + "@type": "@id" + }, + "dfc:owner": { + "@type": "@id" + }, + "dfc-t:hostedBy": { + "@type": "@id" + }, + "dfc-t:hasPivot": { + "@type": "@id" + }, + "dfc-t:represent": { + "@type": "@id" + } + }, + "dfc-b:description": "DFC-Pesto updated", + "dfc-b:quantity": 0 +} diff --git a/spec/factories/user_factory.rb b/spec/factories/user_factory.rb index c31d148d4fa..8a5d7b4c3d4 100644 --- a/spec/factories/user_factory.rb +++ b/spec/factories/user_factory.rb @@ -46,5 +46,11 @@ user.spree_roles << Spree::Role.find_or_create_by!(name: 'admin') end end + + factory :oidc_user do + after(:create) do |user| + user.update uid: user.email + end + end end end