Merge pull request #12525 from mkllnk/fdc-import

Add compatibility to the DFC product import for FDC (Shopify) API
This commit is contained in:
Maikel
2024-06-13 09:51:05 +10:00
committed by GitHub
11 changed files with 569 additions and 19 deletions

View File

@@ -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

View File

@@ -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(&)

View 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

View File

@@ -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:

View File

@@ -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.

View 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

View File

@@ -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,

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -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

View File

@@ -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