diff --git a/lib/intelligent_foods.rb b/lib/intelligent_foods.rb index a2a9d78..20e980b 100644 --- a/lib/intelligent_foods.rb +++ b/lib/intelligent_foods.rb @@ -7,7 +7,9 @@ require "intelligent_foods/api_client" require "intelligent_foods/api/v1" +require "intelligent_foods/api/v2" require "intelligent_foods/api/v1/authenticator" +require "intelligent_foods/api/v2/authenticator" require "intelligent_foods/api_operations/retrieve" require "intelligent_foods/authorization/base" require "intelligent_foods/authorization/basic" @@ -31,7 +33,7 @@ module IntelligentFoods class Error < StandardError; end class << self - attr_accessor :client_id, :client_secret, :environment + attr_accessor :client_id, :client_secret, :environment, :username, :password def configure yield self diff --git a/lib/intelligent_foods/api/v2.rb b/lib/intelligent_foods/api/v2.rb new file mode 100644 index 0000000..61f1a2b --- /dev/null +++ b/lib/intelligent_foods/api/v2.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +module IntelligentFoods + class V2 + attr_accessor :authentication + attr_reader :environment, :username, :password + + PRODUCTION = "production" + STAGING = "staging" + + def initialize(username: nil, password: nil, authentication: nil, + environment: STAGING) + @authentication = authentication + @environment = environment + @username = username + @password = password + end + + def base_url + "https://core.intelligentfoods.#{tld}" + end + + def authenticate! + return if authenticated? + + authenticator = V2::Authenticator.build(self) + @authentication = authenticator.save! + end + + def client + build_api_client! + end + + def authenticated? + authentication.present? + end + + def reset_authentication + @authentication = nil + self + end + + def self.build(config: IntelligentFoods) + new(environment: config.environment, + username: config.username, + password: config.password) + end + + protected + + def build_api_client! + authenticate! + IntelligentFoods::ApiClient.build(self) + end + + def tld + @tld ||= determine_tld + end + + def determine_tld + if environment == PRODUCTION + "com" + else + "dev" + end + end + end +end diff --git a/lib/intelligent_foods/api/v2/authenticator.rb b/lib/intelligent_foods/api/v2/authenticator.rb new file mode 100644 index 0000000..57dc998 --- /dev/null +++ b/lib/intelligent_foods/api/v2/authenticator.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +module IntelligentFoods + class V2::Authenticator + attr_accessor :base_url, :authentication + + def initialize(base_url:, authentication:) + @base_url = base_url + @authentication = authentication + end + + def save! + path = "#{base_url}/auth/info" + client = IntelligentFoods::ApiClient.new(authentication: authentication) + response = client.get(path: path) + handle_authentication_response!(response: response) + end + + def self.build(api) + authentication = Authorization::Basic. + factory(client_id: api.username, + client_secret: api.password) + new(base_url: api.base_url, authentication: authentication) + end + + protected + + def handle_authentication_response!(response:) + if response.success? + authentication + else + handle_errors! + end + end + + def response_has_errors?(response) + response.has_key?(:error) + end + + def handle_errors! + raise AuthenticationError.new(status: 401, title: "Authentication Failed") + end + end +end diff --git a/lib/intelligent_foods/resources/api_error.rb b/lib/intelligent_foods/resources/api_error.rb index a45f679..a3b99bd 100644 --- a/lib/intelligent_foods/resources/api_error.rb +++ b/lib/intelligent_foods/resources/api_error.rb @@ -21,7 +21,9 @@ def message def self.build(response) data = response.data - data => { status:, title:, details: } + status = data.fetch(:status, data[:type]) + title = data.fetch(:title, data[:message]) + details = data[:details] new(status: status, title: title, details: details) end diff --git a/lib/intelligent_foods/resources/core_object.rb b/lib/intelligent_foods/resources/core_object.rb new file mode 100644 index 0000000..822cf8c --- /dev/null +++ b/lib/intelligent_foods/resources/core_object.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module IntelligentFoods + class CoreObject < IntelligentFoods::Object + def resources_path + "#{api.base_url}/api/#{object_name.downcase}" + end + + def resource_path + "#{api.base_url}/api/#{object_name.downcase}/#{id}" + end + + def api + @api ||= IntelligentFoods::V2.build + end + end +end diff --git a/spec/intelligent_foods/api/v2/authenticator_spec.rb b/spec/intelligent_foods/api/v2/authenticator_spec.rb new file mode 100644 index 0000000..b94f28a --- /dev/null +++ b/spec/intelligent_foods/api/v2/authenticator_spec.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +RSpec.describe IntelligentFoods::V2::Authenticator do + describe "#save!" do + it "returns a authorization scheme" do + basic = IntelligentFoods::Authorization::Basic.new(token: "test") + auth = IntelligentFoods::V2::Authenticator. + new(base_url: "http://test.com", authentication: basic) + stub_authentication + + result = auth.save! + + expect(result).to respond_to(:header) + end + + context "when authentication fails" do + it "raises a AuthenticationError" do + basic = IntelligentFoods::Authorization::Basic.new(token: "test") + auth = IntelligentFoods::V2::Authenticator. + new(base_url: "http://test.com", authentication: basic) + response_body = { error: "Authentication Failed" } + response = error_response(body: response_body) + stub_api_response response: response + + expect { + auth.save! + }.to raise_error(IntelligentFoods::AuthenticationError) + end + end + end + + describe ".build" do + it "sets the base_url" do + api = IntelligentFoods::V2.new + + result = IntelligentFoods::V2::Authenticator.build(api) + + expect(result.base_url).to eq(api.base_url) + end + + it "sets the authentication" do + api = IntelligentFoods::V2.new + basic = IntelligentFoods::Authorization::Basic.new(token: "test") + allow(IntelligentFoods::Authorization::Basic).to receive(:factory). + and_return(basic) + + result = IntelligentFoods::V2::Authenticator.build(api) + + expect(result.authentication).to eq(basic) + end + end + + def build_config(environment: "staging", client_id: "cid_123", + client_secret: "cs_123") + OpenStruct.new(environment: environment, client_id: client_id, + client_secret: client_secret) + end +end diff --git a/spec/intelligent_foods/api/v2_spec.rb b/spec/intelligent_foods/api/v2_spec.rb new file mode 100644 index 0000000..72c35c3 --- /dev/null +++ b/spec/intelligent_foods/api/v2_spec.rb @@ -0,0 +1,129 @@ +# frozen_string_literal: true + +RSpec.describe IntelligentFoods::V2 do + describe "#base_url" do + context "when the environment is production" do + it "returns the correct url" do + api = IntelligentFoods::V2.new(environment: "production") + + result = api.base_url + + expect(result).to eq("https://core.intelligentfoods.com") + end + end + + context "when the environment is not production" do + it "returns the correct url" do + api = IntelligentFoods::V2.new(environment: "staging") + + result = api.base_url + + expect(result).to eq("https://core.intelligentfoods.dev") + end + end + end + + describe "#client" do + it "returns a api client" do + api = IntelligentFoods::V2.new + build_stubbed_authenticator(api) + + result = api.client + + expect(result).to respond_to(:post) + end + end + + describe "#authenticate" do + it "builds a authenticator" do + api = IntelligentFoods::V2.new + build_stubbed_authenticator(api) + + api.authenticate! + + expect(IntelligentFoods::V2::Authenticator).to have_received(:build).once + end + + it "authenticates" do + api = IntelligentFoods::V2.new + authenticator = build_stubbed_authenticator(api) + + api.authenticate! + + expect(authenticator).to have_received(:save!).once + end + + context "when the authentication is present" do + it "is authenticated" do + api = IntelligentFoods::V2.new + build_stubbed_authenticator(api) + + api.authenticate! + + expect(api).to be_authenticated + end + end + end + + describe "reset authentication" do + it "removes the present authentication" do + auth = IntelligentFoods::Authorization::Basic.new(token: "1234") + api = IntelligentFoods::V2.new(authentication: auth) + + result = api.reset_authentication + + expect(result.authentication).to be_blank + end + + it "is not authenticated" do + auth = IntelligentFoods::Authorization::Basic.new(token: "1234") + api = IntelligentFoods::V2.new(authentication: auth) + + result = api.reset_authentication + + expect(result).not_to be_authenticated + end + end + + describe ".build" do + it "sets the environment" do + config = build_config(environment: "preview") + + result = IntelligentFoods::V2.build(config: config) + + expect(result.environment).to eq("preview") + end + + it "sets the username" do + config = build_config(username: "secretUsername") + + result = IntelligentFoods::V2.build(config: config) + + expect(result.username).to eq("secretUsername") + end + + it "sets the password" do + config = build_config(password: "secretPassword") + + result = IntelligentFoods::V2.build(config: config) + + expect(result.password).to eq("secretPassword") + end + end + + def build_config(environment: "staging", username: "string", + password: "string") + OpenStruct.new(environment: environment, username: username, + password: password) + end + + def build_stubbed_authenticator(api) + authenticator = IntelligentFoods::V2::Authenticator.build(api) + authentication = authenticator.authentication + client = IntelligentFoods::ApiClient.new(authentication: authentication) + allow(IntelligentFoods::V2::Authenticator).to receive(:build). + and_return(authenticator) + allow(authenticator).to receive(:save!).and_return(client) + authenticator + end +end diff --git a/spec/support/helpers/api_helper.rb b/spec/support/helpers/api_helper.rb index 67422ac..687016e 100644 --- a/spec/support/helpers/api_helper.rb +++ b/spec/support/helpers/api_helper.rb @@ -28,6 +28,12 @@ def stub_api_v1_authentication(authenticated: true) allow(api).to receive(:authenticated?).and_return(authenticated) end + def stub_api_v2_authentication(authenticated: true) + api = IntelligentFoods::V2.build + allow(IntelligentFoods::V2).to receive(:new).and_return(api) + allow(api).to receive(:authenticated?).and_return(authenticated) + end + def stub_api_response(response: OpenStruct.new(body: "{}", code: 200), http: double) allow(Net::HTTP).to receive(:start).and_yield(http)