mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-01-24 20:36:49 +00:00
Merge pull request #12525 from mkllnk/fdc-import
Add compatibility to the DFC product import for FDC (Shopify) API
This commit is contained in:
@@ -20,7 +20,7 @@ module Admin
|
||||
|
||||
catalog_url = params.require(:catalog_url)
|
||||
|
||||
json_catalog = DfcRequest.new(spree_current_user).get(catalog_url)
|
||||
json_catalog = fetch_catalog(catalog_url)
|
||||
graph = DfcIo.import(json_catalog)
|
||||
|
||||
# * First step: import all products for given enterprise.
|
||||
@@ -34,6 +34,16 @@ module Admin
|
||||
|
||||
private
|
||||
|
||||
def fetch_catalog(url)
|
||||
if url =~ /food-data-collaboration/
|
||||
fdc_json = FdcRequest.new(spree_current_user).call(url)
|
||||
fdc_message = JSON.parse(fdc_json)
|
||||
fdc_message["products"]
|
||||
else
|
||||
DfcRequest.new(spree_current_user).call(url)
|
||||
end
|
||||
end
|
||||
|
||||
# Most of this code is the same as in the DfcProvider::SuppliedProductsController.
|
||||
def import_product(subject, enterprise)
|
||||
return unless subject.is_a? DataFoodConsortium::Connector::SuppliedProduct
|
||||
|
||||
@@ -13,33 +13,41 @@ class DfcRequest
|
||||
@user = user
|
||||
end
|
||||
|
||||
def get(url)
|
||||
response = request(url)
|
||||
def call(url, data = nil)
|
||||
response = request(url, data)
|
||||
|
||||
return response.body if response.status == 200
|
||||
if response.status >= 400 && token_stale?
|
||||
refresh_access_token!
|
||||
response = request(url, data)
|
||||
end
|
||||
|
||||
return "" if @user.oidc_account.updated_at > 15.minutes.ago
|
||||
|
||||
refresh_access_token!
|
||||
|
||||
response = request(url)
|
||||
response.body
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def request(url)
|
||||
connection = Faraday.new(
|
||||
def request(url, data = nil)
|
||||
only_public_connections do
|
||||
if data
|
||||
connection.post(url, data)
|
||||
else
|
||||
connection.get(url)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def token_stale?
|
||||
@user.oidc_account.updated_at < 15.minutes.ago
|
||||
end
|
||||
|
||||
def connection
|
||||
Faraday.new(
|
||||
request: { timeout: 30 },
|
||||
headers: {
|
||||
'Content-Type' => 'application/json',
|
||||
'Authorization' => "Bearer #{@user.oidc_account.token}",
|
||||
}
|
||||
)
|
||||
|
||||
only_public_connections do
|
||||
connection.get(url)
|
||||
end
|
||||
end
|
||||
|
||||
def only_public_connections(&)
|
||||
|
||||
33
engines/dfc_provider/app/services/fdc_request.rb
Normal file
33
engines/dfc_provider/app/services/fdc_request.rb
Normal file
@@ -0,0 +1,33 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "private_address_check"
|
||||
require "private_address_check/tcpsocket_ext"
|
||||
|
||||
# Request a JSON document from the FDC API with authentication.
|
||||
#
|
||||
# Currently, the API doesn't quite comply with the DFC standard and we need
|
||||
# to authenticate a little bit differently.
|
||||
#
|
||||
# And then we get slightly different data as well.
|
||||
class FdcRequest < DfcRequest
|
||||
# Override main method to POST authorization data.
|
||||
def call(url, data = {})
|
||||
response = request(url, auth_data.merge(data).to_json)
|
||||
|
||||
if response.status >= 400 && token_stale?
|
||||
refresh_access_token!
|
||||
response = request(url, auth_data.merge(data).to_json)
|
||||
end
|
||||
|
||||
response.body
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def auth_data
|
||||
{
|
||||
userId: @user.oidc_account.uid,
|
||||
accessToken: @user.oidc_account.token,
|
||||
}
|
||||
end
|
||||
end
|
||||
@@ -33,7 +33,7 @@ class QuantitativeValueBuilder < DfcBuilder
|
||||
product.variant_unit = measure
|
||||
product.variant_unit_name = unit_name if measure == "items"
|
||||
product.variant_unit_scale = unit_scale
|
||||
product.unit_value = quantity.value * unit_scale
|
||||
product.unit_value = quantity.value.to_f * unit_scale
|
||||
end
|
||||
|
||||
# Map DFC units to OFN fields:
|
||||
|
||||
@@ -12,7 +12,16 @@ RSpec.describe DfcRequest do
|
||||
stub_request(:get, "http://example.net/api").
|
||||
to_return(status: 200, body: '{"@context":"/"}')
|
||||
|
||||
expect(api.get("http://example.net/api")).to eq '{"@context":"/"}'
|
||||
expect(api.call("http://example.net/api")).to eq '{"@context":"/"}'
|
||||
end
|
||||
|
||||
it "posts a DFC document" do
|
||||
json = '{"name":"new season apples"}'
|
||||
stub_request(:post, "http://example.net/api").
|
||||
with(body: json).
|
||||
to_return(status: 201) # Created
|
||||
|
||||
expect(api.call("http://example.net/api", json)).to eq ""
|
||||
end
|
||||
|
||||
it "refreshes the access token on fail", vcr: true do
|
||||
@@ -30,7 +39,7 @@ RSpec.describe DfcRequest do
|
||||
account.updated_at = 1.day.ago
|
||||
|
||||
expect {
|
||||
api.get("http://example.net/api")
|
||||
api.call("http://example.net/api")
|
||||
}.to change {
|
||||
account.token
|
||||
}.and change {
|
||||
@@ -44,7 +53,7 @@ RSpec.describe DfcRequest do
|
||||
|
||||
user.oidc_account.updated_at = 1.minute.ago
|
||||
|
||||
expect(api.get("http://example.net/api")).to eq ""
|
||||
expect(api.call("http://example.net/api")).to eq ""
|
||||
|
||||
# Trying to reach the OIDC server via network request to refresh the token
|
||||
# would raise errors because we didn't setup Webmock or VCR.
|
||||
|
||||
36
engines/dfc_provider/spec/services/fdc_request_spec.rb
Normal file
36
engines/dfc_provider/spec/services/fdc_request_spec.rb
Normal file
@@ -0,0 +1,36 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative "../spec_helper"
|
||||
|
||||
RSpec.describe FdcRequest do
|
||||
subject(:api) { FdcRequest.new(user) }
|
||||
|
||||
let(:user) { build(:oidc_user) }
|
||||
let(:account) { user.oidc_account }
|
||||
let(:url) {
|
||||
"https://food-data-collaboration-produc-fe870152f634.herokuapp.com/fdc/products?shop=test-hodmedod.myshopify.com"
|
||||
}
|
||||
|
||||
it "refreshes the access token and retrieves a catalog", vcr: true do
|
||||
# A refresh is only attempted if the token is stale.
|
||||
account.uid = "testdfc@protonmail.com"
|
||||
account.refresh_token = ENV.fetch("OPENID_REFRESH_TOKEN")
|
||||
account.updated_at = 1.day.ago
|
||||
|
||||
response = nil
|
||||
expect {
|
||||
response = api.call(url)
|
||||
}.to change {
|
||||
account.token
|
||||
}.and change {
|
||||
account.refresh_token
|
||||
}
|
||||
|
||||
json = JSON.parse(response)
|
||||
expect(json["message"]).to eq "Products retrieved successfully"
|
||||
|
||||
graph = DfcIo.import(json["products"])
|
||||
products = graph.select { |s| s.semanticType == "dfc-b:SuppliedProduct" }
|
||||
expect(products).to be_present
|
||||
end
|
||||
end
|
||||
@@ -104,6 +104,20 @@ RSpec.describe QuantitativeValueBuilder do
|
||||
expect(product.unit_value).to eq 0.005
|
||||
end
|
||||
|
||||
it "interpretes values given as a string" do
|
||||
quantity = DataFoodConsortium::Connector::QuantitativeValue.new(
|
||||
unit: quantity_unit.KILOGRAM,
|
||||
value: "0.4",
|
||||
)
|
||||
|
||||
builder.apply(quantity, product)
|
||||
|
||||
expect(product.variant_unit).to eq "weight"
|
||||
expect(product.variant_unit_name).to eq nil
|
||||
expect(product.variant_unit_scale).to eq 1_000
|
||||
expect(product.unit_value).to eq 400
|
||||
end
|
||||
|
||||
it "knows imperial units" do
|
||||
quantity = DataFoodConsortium::Connector::QuantitativeValue.new(
|
||||
unit: quantity_unit.POUNDMASS,
|
||||
|
||||
206
spec/fixtures/vcr_cassettes/DFC_Product_Import/imports_from_a_FDC_catalog.yml
vendored
Normal file
206
spec/fixtures/vcr_cassettes/DFC_Product_Import/imports_from_a_FDC_catalog.yml
vendored
Normal file
File diff suppressed because one or more lines are too long
206
spec/fixtures/vcr_cassettes/FdcRequest/refreshes_the_access_token_and_retrieves_a_catalog.yml
vendored
Normal file
206
spec/fixtures/vcr_cassettes/FdcRequest/refreshes_the_access_token_and_retrieves_a_catalog.yml
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -40,4 +40,9 @@ VCR.configure do |config|
|
||||
config.filter_sensitive_data('<HIDDEN-OPENID-TOKEN>') { |interaction|
|
||||
interaction.response.body.match(/"refresh_token":"([^"]+)"/)&.public_send(:[], 1)
|
||||
}
|
||||
|
||||
# FDC specific parameter:
|
||||
config.filter_sensitive_data('<HIDDEN-OPENID-TOKEN>') { |interaction|
|
||||
interaction.request.body.match(/"accessToken":"([^"]+)"/)&.public_send(:[], 1)
|
||||
}
|
||||
end
|
||||
|
||||
@@ -41,4 +41,27 @@ RSpec.describe "DFC Product Import" do
|
||||
expect(page).to have_content "Importing a DFC product catalog"
|
||||
expect(page).to have_content "Imported products: 1"
|
||||
end
|
||||
|
||||
it "imports from a FDC catalog", vcr: true do
|
||||
user.oidc_account.update!(
|
||||
uid: "testdfc@protonmail.com",
|
||||
refresh_token: ENV.fetch("OPENID_REFRESH_TOKEN"),
|
||||
updated_at: 1.day.ago,
|
||||
)
|
||||
|
||||
visit admin_product_import_path
|
||||
|
||||
select enterprise.name, from: "Enterprise"
|
||||
|
||||
url = "https://food-data-collaboration-produc-fe870152f634.herokuapp.com/fdc/products?shop=test-hodmedod.myshopify.com"
|
||||
fill_in "catalog_url", with: url
|
||||
|
||||
expect {
|
||||
click_button "Import"
|
||||
}.to change {
|
||||
enterprise.supplied_products.count
|
||||
}
|
||||
|
||||
expect(page).to have_content "Importing a DFC product catalog"
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user