Merge pull request #10861 from mkllnk/dfc-import

Update products with DFC Prototype
This commit is contained in:
Maikel
2023-06-01 08:48:39 +10:00
committed by GitHub
17 changed files with 350 additions and 344 deletions

View File

@@ -6,18 +6,16 @@ Openfoodnetwork::Application.routes.draw do
namespace :api do
constraints FeatureToggleConstraint.new(:dfc_provider) do
# Mount DFC API endpoints
#
# We're using the DFC Connector which produces DFC v1.7 data but the
# DFC prototype is still pointing to the old URL. We keep it for
# testing.
mount DfcProvider::Engine, at: '/dfc-v1.6/', as: :legacy_dfc_provider_engine
# Mount DFC API endpoints
#
# We're using the DFC Connector which produces DFC v1.7 data but the
# DFC prototype is still pointing to the old URL. We keep it for
# testing.
mount DfcProvider::Engine, at: '/dfc-v1.6/', as: :legacy_dfc_provider_engine
# The DFC API version depends on the version of the
# datafoodconsortium-connector gem.
mount DfcProvider::Engine, at: '/dfc-v1.7/'
end
# The DFC API version depends on the version of the
# datafoodconsortium-connector gem.
mount DfcProvider::Engine, at: '/dfc-v1.7/'
namespace :v0 do
resources :products do

View File

@@ -23,6 +23,14 @@ module DfcProvider
render json: DfcLoader.connector.export(catalog_item, *offers)
end
def update
dfc_request = JSON.parse(request.body.read)
variant.on_hand = dfc_request["dfc-b:stockLimitation"]
variant.sku = dfc_request["dfc-b:sku"]
variant.save!
end
private
def variant

View File

@@ -15,7 +15,14 @@ module DfcProvider
dfc_request = JSON.parse(request.body.read)
return unless dfc_request.key?("dfc-b:description")
variant.product.update!(name: dfc_request["dfc-b:description"])
variant.product.update!(
description: dfc_request["dfc-b:description"],
)
# This input is DFC v1.6 currently sent by the DFC Prototype.
variant.update!(
unit_value: dfc_request["dfc-b:quantity"],
)
end
private

View File

@@ -2,7 +2,7 @@
DfcProvider::Engine.routes.draw do
resources :enterprises, only: [:show] do
resources :catalog_items, only: [:index, :show]
resources :catalog_items, only: [:index, :show, :update]
resources :supplied_products, only: [:show, :update]
end
resources :persons, only: [:show]

View File

@@ -1,141 +0,0 @@
# frozen_string_literal: true
require DfcProvider::Engine.root.join("spec/spec_helper")
describe DfcProvider::CatalogItemsController, type: :controller do
include AuthorizationHelper
render_views
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 { authorise user.email }
context 'with an authenticated user' do
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 have_http_status :success
end
it 'renders the required content' do
expect(response.body)
.to include(variant.name)
expect(response.body)
.to include(variant.sku)
expect(response.body)
.to include("offers/#{variant.id}")
end
end
context 'not related to the user' do
let(:enterprise) { create(:enterprise) }
it 'returns not_found head' do
api_get :index, enterprise_id: enterprise.id
expect(response).to have_http_status :not_found
end
end
end
context 'as default' do
before { api_get :index, enterprise_id: 'default' }
it 'is successful' do
expect(response.status).to eq 200
end
it 'renders the required content' do
expect(response.body)
.to include(variant.name)
expect(response.body)
.to include(variant.sku)
expect(response.body)
.to include("offers/#{variant.id}")
end
end
end
context 'without a recorded enterprise' do
let(:enterprise) { create(:enterprise) }
it 'is not found' do
api_get :index, enterprise_id: 'default'
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
authorise "other@user.net"
api_get :index, enterprise_id: 'default'
expect(response.response_code).to eq(401)
end
end
end
context 'without an authorization token' 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 have_http_status :success
end
end
end
describe '.show' do
context 'with authorization token' do
before { authorise user.email }
context 'with an authenticated user' do
context 'with an enterprise' do
context 'given with an id' do
before do
api_get :show, enterprise_id: enterprise.id, id: variant.id
end
it 'is successful' do
expect(response).to have_http_status :success
end
it 'renders the required content' do
expect(response.body).to include('dfc-b:CatalogItem')
expect(response.body).to include("offers/#{variant.id}")
end
end
context 'with a variant not linked to the enterprise' do
before do
api_get :show,
enterprise_id: enterprise.id,
id: create(:simple_product).variants.first.id
end
it 'is not found' do
expect(response).to have_http_status :not_found
end
end
end
end
end
end
end

View File

@@ -1,54 +0,0 @@
# frozen_string_literal: true
require DfcProvider::Engine.root.join("spec/spec_helper")
describe DfcProvider::EnterprisesController, type: :controller do
render_views
let!(:user) { create(:user) }
let!(:enterprise) { create(:distributor_enterprise, owner: user) }
let!(:product) { create(:simple_product, supplier: enterprise ) }
describe '.show' do
context 'with authorization token' do
before do
request.headers['Authorization'] = 'Bearer 123456.abcdef.123456'
end
context 'with an authenticated user' do
before do
allow_any_instance_of(AuthorizationControl)
.to receive(:user)
.and_return(user)
end
context 'with an enterprise' do
context 'given with an id' do
before { api_get :show, id: 'default' }
it 'is successful' do
expect(response).to be_successful
end
it 'renders the required content' do
expect(response.body)
.to include(product.name)
expect(response.body)
.to include(product.sku)
expect(response.body)
.to include("offers/#{product.variants.first.id}")
end
end
context 'given with a wrong id' do
before { api_get :show, id: 999 }
it 'is not found' do
expect(response).to be_not_found
end
end
end
end
end
end
end

View File

@@ -1,45 +0,0 @@
# frozen_string_literal: true
require DfcProvider::Engine.root.join("spec/spec_helper")
describe DfcProvider::PersonsController, type: :controller do
render_views
let!(:user) { create(:user) }
describe '.show' do
context 'with authorization token' do
before do
request.headers['Authorization'] = 'Bearer 123456.abcdef.123456'
end
context 'with an authenticated user' do
before do
allow_any_instance_of(AuthorizationControl)
.to receive(:user)
.and_return(user)
end
context 'given with an accessible id' do
before { api_get :show, id: user.id }
it 'is successful' do
expect(response).to be_successful
end
it 'renders the required content' do
expect(response.body).to include('dfc-b:Person')
end
end
context 'with an other user id' do
before { api_get :show, id: create(:user).id }
it 'is not found' do
expect(response).to be_not_found
end
end
end
end
end
end

View File

@@ -1,80 +0,0 @@
# frozen_string_literal: true
require DfcProvider::Engine.root.join("spec/spec_helper")
describe DfcProvider::SuppliedProductsController, type: :controller do
include AuthorizationHelper
render_views
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 '.show' do
context 'with authorization token' do
before do
request.headers['Authorization'] = 'Bearer 123456.abcdef.123456'
end
context 'with an authenticated user' do
before do
allow_any_instance_of(AuthorizationControl)
.to receive(:user)
.and_return(user)
end
context 'with an enterprise' do
context 'given with an id' do
before do
api_get :show, enterprise_id: 'default', id: variant.id
end
it 'is successful' do
expect(response).to be_successful
end
it 'renders the required content' do
expect(response.body).to include(variant.name)
end
end
context 'given with a wrong id' do
before { api_get :show, enterprise_id: 'default', id: 999 }
it 'is not found' do
expect(response).to be_not_found
end
end
end
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

View File

@@ -0,0 +1,115 @@
# frozen_string_literal: true
require DfcProvider::Engine.root.join("spec/spec_helper")
describe "CatalogItems", type: :request do
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
it "returns not_found without enterprise" do
items_path = enterprise_catalog_items_path(enterprise_id: "default")
get items_path, headers: auth_header(user.uid)
expect(response).to have_http_status :not_found
end
context "with existing variant" do
before { variant }
it "lists catalog items with offers of default enterprise" do
items_path = enterprise_catalog_items_path(enterprise_id: "default")
get items_path, headers: auth_header(user.uid)
expect(response).to have_http_status :ok
expect(response.body).to include variant.name
expect(response.body).to include variant.sku
expect(response.body).to include "offers/#{variant.id}"
end
it "lists catalog items with offers of requested enterprise" do
items_path = enterprise_catalog_items_path(enterprise_id: enterprise.id)
get items_path, headers: auth_header(user.uid)
expect(response).to have_http_status :ok
expect(response.body).to include variant.name
expect(response.body).to include variant.sku
expect(response.body).to include "offers/#{variant.id}"
end
it "returns not_found for unrelated enterprises" do
other_enterprise = create(:enterprise)
items_path = enterprise_catalog_items_path(enterprise_id: other_enterprise.id)
get items_path, headers: auth_header(user.uid)
expect(response).to have_http_status :not_found
end
it "returns unauthorized for unauthenticated users" do
items_path = enterprise_catalog_items_path(enterprise_id: "default")
get items_path, headers: {}
expect(response).to have_http_status :unauthorized
end
it "recognises app user sessions as logins" do
items_path = enterprise_catalog_items_path(enterprise_id: "default")
login_as user
get items_path, headers: {}
expect(response).to have_http_status :ok
end
end
end
describe :show do
it "returns a catalog item with offer" do
item_path = enterprise_catalog_item_path(
variant,
enterprise_id: enterprise.id
)
get item_path, headers: auth_header(user.uid)
expect(response).to have_http_status :ok
expect(response.body).to include "dfc-b:CatalogItem"
expect(response.body).to include "offers/#{variant.id}"
end
it "returns not_found for unrelated variant" do
item_path = enterprise_catalog_item_path(
create(:variant),
enterprise_id: enterprise.id
)
get item_path, headers: auth_header(user.uid)
expect(response).to have_http_status :not_found
end
end
describe :update do
it "updates a variant's attributes" do
params = { enterprise_id: enterprise.id, id: variant.id }
request_body = DfcProvider::Engine.root.join("spec/support/patch_catalog_item.json").read
expect {
put(
enterprise_catalog_item_path(params),
params: request_body,
headers: auth_header(user.uid)
)
expect(response).to have_http_status :success
variant.reload
}.to change { variant.on_hand }.to(3)
.and change { variant.sku }.to("new-sku")
end
end
end

View File

@@ -0,0 +1,35 @@
# frozen_string_literal: true
require DfcProvider::Engine.root.join("spec/spec_helper")
describe "Enterprises", type: :request do
let!(:user) { create(:oidc_user) }
let!(:enterprise) { create(:distributor_enterprise, owner: user) }
let!(:product) { create(:simple_product, supplier: enterprise ) }
describe :show do
it "returns the default enterprise" do
get enterprise_path("default"), headers: auth_header(user.uid)
expect(response).to have_http_status :ok
expect(response.body).to include(product.name)
expect(response.body).to include(product.sku)
expect(response.body).to include("offers/#{product.variants.first.id}")
end
it "returns the requested enterprise" do
get enterprise_path(enterprise.id), headers: auth_header(user.uid)
expect(response).to have_http_status :ok
expect(response.body).to include(product.name)
end
it "returns not found for unrelated enterprise" do
other_enterprise = create(:distributor_enterprise)
get enterprise_path(other_enterprise.id), headers: auth_header(user.uid)
expect(response).to have_http_status :not_found
expect(response.body).to_not include(product.name)
end
end
end

View File

@@ -0,0 +1,23 @@
# frozen_string_literal: true
require DfcProvider::Engine.root.join("spec/spec_helper")
describe "Persons", type: :request do
let(:user) { create(:oidc_user) }
let(:other_user) { create(:oidc_user) }
describe :show do
it "returns the authenticated user" do
get person_path(user), headers: auth_header(user.uid)
expect(response).to have_http_status :ok
expect(response.body).to include "dfc-b:Person"
expect(response.body).to include "persons/#{user.id}"
end
it "doesn't find another user" do
get person_path(other_user), headers: auth_header(user.uid)
expect(response).to have_http_status :not_found
expect(response.body).to_not include "dfc-b:Person"
end
end
end

View File

@@ -0,0 +1,57 @@
# frozen_string_literal: true
require DfcProvider::Engine.root.join("spec/spec_helper")
describe "SuppliedProducts", type: :request do
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 :show do
it "returns variants" do
get enterprise_supplied_product_path(
variant.id, enterprise_id: enterprise.id
), headers: auth_header(user.uid)
expect(response).to have_http_status :ok
expect(response.body).to include variant.name
end
it "doesn't find unrelated variants" do
other_variant = create(:variant)
get enterprise_supplied_product_path(
other_variant.id, enterprise_id: enterprise.id
), headers: auth_header(user.uid)
expect(response).to have_http_status :not_found
end
end
describe :update do
it "requires authorisation" do
put enterprise_supplied_product_path(
variant.id, enterprise_id: enterprise.id
), headers: {}
expect(response).to have_http_status :unauthorized
end
it "updates a variant's attributes" do
params = { enterprise_id: enterprise.id, id: variant.id }
request_body = DfcProvider::Engine.root.join("spec/support/patch_supplied_product.json").read
expect {
put(
enterprise_supplied_product_path(params),
params: request_body,
headers: auth_header(user.uid)
)
expect(response).to have_http_status :success
variant.reload
}.to change { variant.description }.to("DFC-Pesto updated")
.and change { variant.unit_value }.to(17)
end
end
end

View File

@@ -5,6 +5,10 @@ require_relative '../../../spec/spec_helper'
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].sort.each { |f| require f }
RSpec.configure do |config|
config.include AuthorizationHelper, type: :request
config.include DfcProvider::Engine.routes.url_helpers, type: :request
config.include Warden::Test::Helpers, type: :request
config.around(:each) do |example|
# The DFC Connector fetches the context when loaded.
VCR.use_cassette("dfc-context") do

View File

@@ -1,9 +1,9 @@
# frozen_string_literal: true
module AuthorizationHelper
def authorise(email)
def auth_header(email)
token = allow_token_for(email: email)
request.headers["Authorization"] = "JWT #{token}"
{ "Authorization" => "JWT #{token}" }
end
def allow_token_for(payload)

View File

@@ -0,0 +1,85 @@
{
"@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-m": "http://static.datafoodconsortium.org/data/measures.rdf#",
"dfc-pt": "http://static.datafoodconsortium.org/data/productTypes.rdf#",
"dfc-f": "http://static.datafoodconsortium.org/data/facets.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:stockLimitation": "3",
"dfc-b:sku": "new-sku"
}

View File

@@ -7,12 +7,9 @@
"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-m": "http://static.datafoodconsortium.org/data/measures.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-f": "http://static.datafoodconsortium.org/data/facets.rdf#",
"dfc-p:hasUnit": {
"@type": "@id"
},
@@ -84,5 +81,5 @@
}
},
"dfc-b:description": "DFC-Pesto updated",
"dfc-b:quantity": 0
"dfc-b:quantity": 17
}

View File

@@ -24,9 +24,6 @@ module OpenFoodNetwork
"background_reports" => <<~DESC,
Generate reports in a background process to limit memory consumption.
DESC
"dfc_provider" => <<~DESC,
Enable the DFC compatible endpoint at <code>/api/dfc-*</code>.
DESC
"match_shipping_categories" => <<~DESC,
During checkout, show only shipping methods that support <em>all</em>
shipping categories. Activating this feature for an enterprise owner