From 5a15f1d43aa0cde87c1f732c7e902bb4d0307da4 Mon Sep 17 00:00:00 2001 From: Rohan Likhite Date: Fri, 8 May 2026 11:49:57 -0400 Subject: [PATCH 1/2] Introduce CORE Api Authentication A new version of the API (CORE) is now available and will be used in the near future for new resources. This new API utilizes a basic authentication scheme. This PR lays the groundwork for future PR's to be able to interact with the CORE api, and adds the plumbing necessary to successfully configure the gem, and authenticate with CORE. This change addresses the need by: * Introducing a v2 authenticator * Introducing a v2 api * Introducing a CoreObject resource which can be used by all core (v2) objects --- lib/bls/core.rb | 5 +- lib/bls/core/api/v1/authenticator.rb | 2 +- lib/bls/core/api/v2.rb | 70 ++++++++++++++ lib/bls/core/api/v2/authenticator.rb | 42 ++++++++ lib/bls/core/authorization/basic.rb | 4 +- lib/bls/core/resources/api_error.rb | 4 +- spec/core/api/v2/authenticator_spec.rb | 58 +++++++++++ spec/core/api/v2_spec.rb | 129 +++++++++++++++++++++++++ spec/support/helpers/api_helper.rb | 6 ++ 9 files changed, 315 insertions(+), 5 deletions(-) create mode 100644 lib/bls/core/api/v2.rb create mode 100644 lib/bls/core/api/v2/authenticator.rb create mode 100644 spec/core/api/v2/authenticator_spec.rb create mode 100644 spec/core/api/v2_spec.rb diff --git a/lib/bls/core.rb b/lib/bls/core.rb index f672ea4..52c5c75 100644 --- a/lib/bls/core.rb +++ b/lib/bls/core.rb @@ -7,7 +7,9 @@ require_relative "core/api_client" require_relative "core/api/v1" +require_relative "core/api/v2" require_relative "core/api/v1/authenticator" +require_relative "core/api/v2/authenticator" require_relative "core/api_operations/retrieve" require_relative "core/authorization/base" require_relative "core/authorization/basic" @@ -32,7 +34,8 @@ module Core 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/bls/core/api/v1/authenticator.rb b/lib/bls/core/api/v1/authenticator.rb index 77c9e17..6490b18 100644 --- a/lib/bls/core/api/v1/authenticator.rb +++ b/lib/bls/core/api/v1/authenticator.rb @@ -15,7 +15,7 @@ def save! path = "#{base_url}/token" body = { client_id: client_id, client_secret: client_secret } auth = Authorization::Basic. - factory(client_id: client_id, client_secret: client_secret) + factory(username: client_id, password: client_secret) client = Core::ApiClient.new(authentication: auth) response = client.post(path: path, body: body) handle_authentication_response!(response: response.data) diff --git a/lib/bls/core/api/v2.rb b/lib/bls/core/api/v2.rb new file mode 100644 index 0000000..3f6dd40 --- /dev/null +++ b/lib/bls/core/api/v2.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +module Bls + module Core + 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.batchlinesolutions.#{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: Bls::Core) + new(environment: config.environment, + username: config.username, + password: config.password) + end + + protected + + def build_api_client! + authenticate! + Bls::Core::ApiClient.build(self) + end + + def tld + @tld ||= determine_tld + end + + def determine_tld + if environment == PRODUCTION + "com" + else + "dev" + end + end + end + end +end diff --git a/lib/bls/core/api/v2/authenticator.rb b/lib/bls/core/api/v2/authenticator.rb new file mode 100644 index 0000000..af9f157 --- /dev/null +++ b/lib/bls/core/api/v2/authenticator.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +module Bls + module Core + 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 = Core::ApiClient.new(authentication: authentication) + response = client.get(path: path) + handle_authentication_response!(response: response) + end + + def self.build(api) + authentication = Authorization::Basic. + factory(username: api.username, + password: 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 handle_errors! + raise AuthenticationError.new(status: 401, title: "Authentication Failed") + end + end + end +end diff --git a/lib/bls/core/authorization/basic.rb b/lib/bls/core/authorization/basic.rb index 010c8ea..dd6d422 100644 --- a/lib/bls/core/authorization/basic.rb +++ b/lib/bls/core/authorization/basic.rb @@ -8,8 +8,8 @@ def header "Basic #{token}" end - def self.factory(client_id:, client_secret:) - encoded_token = Base64.strict_encode64("#{client_id}:#{client_secret}") + def self.factory(username:, password:) + encoded_token = Base64.strict_encode64("#{username}:#{password}") new(token: encoded_token) end end diff --git a/lib/bls/core/resources/api_error.rb b/lib/bls/core/resources/api_error.rb index 1a7475f..07ba116 100644 --- a/lib/bls/core/resources/api_error.rb +++ b/lib/bls/core/resources/api_error.rb @@ -22,7 +22,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/spec/core/api/v2/authenticator_spec.rb b/spec/core/api/v2/authenticator_spec.rb new file mode 100644 index 0000000..e4b2cfc --- /dev/null +++ b/spec/core/api/v2/authenticator_spec.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +RSpec.describe Bls::Core::V2::Authenticator do + describe "#save!" do + it "returns a authorization scheme" do + basic = Bls::Core::Authorization::Basic.new(token: "test") + auth = Bls::Core::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 = Bls::Core::Authorization::Basic.new(token: "test") + auth = Bls::Core::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(Bls::Core::AuthenticationError) + end + end + end + + describe ".build" do + it "sets the base_url" do + api = Bls::Core::V2.new + + result = Bls::Core::V2::Authenticator.build(api) + + expect(result.base_url).to eq(api.base_url) + end + + it "sets the authentication" do + api = Bls::Core::V2.new + basic = Bls::Core::Authorization::Basic.new(token: "test") + allow(Bls::Core::Authorization::Basic).to receive(:factory). + and_return(basic) + + result = Bls::Core::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/core/api/v2_spec.rb b/spec/core/api/v2_spec.rb new file mode 100644 index 0000000..80915dc --- /dev/null +++ b/spec/core/api/v2_spec.rb @@ -0,0 +1,129 @@ +# frozen_string_literal: true + +RSpec.describe Bls::Core::V2 do + describe "#base_url" do + context "when the environment is production" do + it "returns the correct url" do + api = Bls::Core::V2.new(environment: "production") + + result = api.base_url + + expect(result).to eq("https://core.batchlinesolutions.com") + end + end + + context "when the environment is not production" do + it "returns the correct url" do + api = Bls::Core::V2.new(environment: "staging") + + result = api.base_url + + expect(result).to eq("https://core.batchlinesolutions.dev") + end + end + end + + describe "#client" do + it "returns a api client" do + api = Bls::Core::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 = Bls::Core::V2.new + build_stubbed_authenticator(api) + + api.authenticate! + + expect(Bls::Core::V2::Authenticator).to have_received(:build).once + end + + it "authenticates" do + api = Bls::Core::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 = Bls::Core::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 = Bls::Core::Authorization::Basic.new(token: "1234") + api = Bls::Core::V2.new(authentication: auth) + + result = api.reset_authentication + + expect(result.authentication).to be_blank + end + + it "is not authenticated" do + auth = Bls::Core::Authorization::Basic.new(token: "1234") + api = Bls::Core::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 = Bls::Core::V2.build(config: config) + + expect(result.environment).to eq("preview") + end + + it "sets the username" do + config = build_config(username: "secretUsername") + + result = Bls::Core::V2.build(config: config) + + expect(result.username).to eq("secretUsername") + end + + it "sets the password" do + config = build_config(password: "secretPassword") + + result = Bls::Core::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 = Bls::Core::V2::Authenticator.build(api) + authentication = authenticator.authentication + client = Bls::Core::ApiClient.new(authentication: authentication) + allow(Bls::Core::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 e64195b..f7bb825 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 = Bls::Core::V2.build + allow(Bls::Core::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) From 8b378925e5a23b8942e7785fe02425293e909678 Mon Sep 17 00:00:00 2001 From: Rohan Likhite Date: Wed, 12 Aug 2026 10:26:08 -0400 Subject: [PATCH 2/2] WIP remove auth/info call --- lib/bls/core/api/v2/authenticator.rb | 26 ++++--------------------- spec/core/api/v2/authenticator_spec.rb | 27 +------------------------- 2 files changed, 5 insertions(+), 48 deletions(-) diff --git a/lib/bls/core/api/v2/authenticator.rb b/lib/bls/core/api/v2/authenticator.rb index af9f157..ea2c316 100644 --- a/lib/bls/core/api/v2/authenticator.rb +++ b/lib/bls/core/api/v2/authenticator.rb @@ -3,39 +3,21 @@ module Bls module Core class V2::Authenticator - attr_accessor :base_url, :authentication + attr_accessor :authentication - def initialize(base_url:, authentication:) - @base_url = base_url + def initialize(authentication:) @authentication = authentication end def save! - path = "#{base_url}/auth/info" - client = Core::ApiClient.new(authentication: authentication) - response = client.get(path: path) - handle_authentication_response!(response: response) + authentication end def self.build(api) authentication = Authorization::Basic. factory(username: api.username, password: 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 handle_errors! - raise AuthenticationError.new(status: 401, title: "Authentication Failed") + new(authentication: authentication) end end end diff --git a/spec/core/api/v2/authenticator_spec.rb b/spec/core/api/v2/authenticator_spec.rb index e4b2cfc..39e7779 100644 --- a/spec/core/api/v2/authenticator_spec.rb +++ b/spec/core/api/v2/authenticator_spec.rb @@ -4,40 +4,15 @@ describe "#save!" do it "returns a authorization scheme" do basic = Bls::Core::Authorization::Basic.new(token: "test") - auth = Bls::Core::V2::Authenticator. - new(base_url: "http://test.com", authentication: basic) - stub_authentication + auth = Bls::Core::V2::Authenticator.new(authentication: basic) result = auth.save! expect(result).to respond_to(:header) end - - context "when authentication fails" do - it "raises a AuthenticationError" do - basic = Bls::Core::Authorization::Basic.new(token: "test") - auth = Bls::Core::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(Bls::Core::AuthenticationError) - end - end end describe ".build" do - it "sets the base_url" do - api = Bls::Core::V2.new - - result = Bls::Core::V2::Authenticator.build(api) - - expect(result.base_url).to eq(api.base_url) - end - it "sets the authentication" do api = Bls::Core::V2.new basic = Bls::Core::Authorization::Basic.new(token: "test")