Skip to content

Commit

Permalink
Add token generation service for parity check (#1810)
Browse files Browse the repository at this point in the history
* Add ECF models for ApiToken generation

Bring across the models from ECF that we need to be able to generate API tokens
for ECF in NPQ registration.

I haven't improved the test coverage here, just a quick lift and shift from ECF.

* Add token generation service for ECF/NPQ

As part of the parity check we will need to generate API tokens for lead
providers in ECF and NPQ reg on the fly to be able to make requests to the API
as these providers.

Add service to generate tokens for each lead provider.
  • Loading branch information
ethax-ross authored Oct 16, 2024
1 parent 17d82f8 commit f2ded5f
Show file tree
Hide file tree
Showing 14 changed files with 193 additions and 0 deletions.
24 changes: 24 additions & 0 deletions app/models/migration/ecf/api_token.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# frozen_string_literal: true

require "abstract_interface"

module Migration::Ecf
class APIToken < BaseRecord
has_paper_trail ignore: %i[updated_at last_used_at]
# This is meant to be an abstract class
# Since it is a base class for a STI, we can't actually make it abstract (not backed by a table)
include AbstractInterface
implement_instance_method :owner

def self.create_with_random_token!(**options)
unhashed_token, hashed_token = Devise.token_generator.generate(APIToken, :hashed_token)
create!(hashed_token:, **options)
unhashed_token
end

def self.find_by_unhashed_token(unhashed_token)
hashed_token = Devise.token_generator.digest(APIToken, :hashed_token, unhashed_token)
find_by(hashed_token:)
end
end
end
20 changes: 20 additions & 0 deletions app/models/migration/ecf/lead_provider_api_token.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# frozen_string_literal: true

module Migration::Ecf
class LeadProviderAPIToken < APIToken
belongs_to :lead_provider, optional: true
belongs_to :cpd_lead_provider, optional: true

def owner
cpd_lead_provider
end

def owner_description
"CPD lead provider: #{cpd_lead_provider.name}"
end

def readonly?
false
end
end
end
31 changes: 31 additions & 0 deletions app/services/migration/parity_check/token_provider.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
module Migration::ParityCheck
class TokenProvider
class UnsupportedEnvironmentError < RuntimeError; end

def generate!
raise UnsupportedEnvironmentError, "The parity check functionality is disabled for this environment" unless enabled?

LeadProvider.all.each_with_object({}) do |lead_provider, hash|
hash[lead_provider.ecf_id] = {
ecf: generate_ecf_token!(lead_provider),
npq: generate_npq_token!(lead_provider),
}
end
end

private

def generate_ecf_token!(lead_provider)
cpd_lead_provider = Migration::Ecf::NpqLeadProvider.find(lead_provider.ecf_id).cpd_lead_provider
Migration::Ecf::LeadProviderAPIToken.create_with_random_token!(cpd_lead_provider:)
end

def generate_npq_token!(lead_provider)
APIToken.create_with_random_token!(lead_provider:)
end

def enabled?
Rails.application.config.npq_separation[:parity_check][:enabled]
end
end
end
3 changes: 3 additions & 0 deletions config/environments/development.rb
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@
api_enabled: true,
migration_enabled: true,
ecf_api_disabled: false,
parity_check: {
enabled: true,
},
}

# Disable origin check for Cross-Site Request Forgery (CSRF) protection for codespaces.
Expand Down
3 changes: 3 additions & 0 deletions config/environments/migration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,8 @@
api_enabled: true,
migration_enabled: true,
ecf_api_disabled: true,
parity_check: {
enabled: true,
},
}
end
3 changes: 3 additions & 0 deletions config/environments/production.rb
Original file line number Diff line number Diff line change
Expand Up @@ -111,5 +111,8 @@
api_enabled: false,
migration_enabled: false,
ecf_api_disabled: false,
parity_check: {
enabled: false,
},
}
end
3 changes: 3 additions & 0 deletions config/environments/review.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,8 @@
api_enabled: true,
migration_enabled: false,
ecf_api_disabled: false,
parity_check: {
enabled: false,
},
}
end
3 changes: 3 additions & 0 deletions config/environments/sandbox.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,8 @@
api_enabled: false,
migration_enabled: false,
ecf_api_disabled: false,
parity_check: {
enabled: false,
},
}
end
3 changes: 3 additions & 0 deletions config/environments/separation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,8 @@
api_enabled: true,
migration_enabled: false,
ecf_api_disabled: true,
parity_check: {
enabled: false,
},
}
end
3 changes: 3 additions & 0 deletions config/environments/staging.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,8 @@
api_enabled: true,
migration_enabled: false,
ecf_api_disabled: false,
parity_check: {
enabled: false,
},
}
end
3 changes: 3 additions & 0 deletions config/environments/test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@
api_enabled: true,
migration_enabled: true,
ecf_api_disabled: false,
parity_check: {
enabled: true,
},
}

config.dotenv.autorestore = false if config.respond_to?(:dotenv)
Expand Down
23 changes: 23 additions & 0 deletions lib/abstract_interface.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# frozen_string_literal: true

module AbstractInterface
extend ActiveSupport::Concern

included do
extend AbstractInterfaceClassMethods
end

module AbstractInterfaceClassMethods
def implement_class_method(*methods)
methods.each do |key|
define_singleton_method(key) { raise NotImplementedError, "Class method #{key} must be implemented" } unless self.class.respond_to?(key)
end
end

def implement_instance_method(*methods)
methods.each do |key|
define_method(key) { raise NotImplementedError, "Instance method #{key} must be implemented" } unless respond_to?(key)
end
end
end
end
13 changes: 13 additions & 0 deletions spec/models/migration/ecf/lead_provider_api_token_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
require "rails_helper"

RSpec.describe Migration::Ecf::LeadProviderAPIToken, type: :model do
it { expect(described_class.new).not_to be_readonly }

it "generates a hashed token that can be used" do
unhashed_token = described_class.create_with_random_token!(lead_provider: create(:lead_provider))

expect(
described_class.find_by_unhashed_token(unhashed_token),
).to eql(described_class.order(:created_at).last)
end
end
58 changes: 58 additions & 0 deletions spec/services/migration/parity_check/token_provider_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
require "rails_helper"

RSpec.describe Migration::ParityCheck::TokenProvider do
let(:instance) { described_class.new }

before do
LeadProvider.all.find_each do |lead_provider|
create(:ecf_migration_npq_lead_provider, id: lead_provider.ecf_id)
end
end

describe "#generate!" do
subject(:tokens_by_lead_provider) { instance.generate! }

it { is_expected.to be_present }
it { expect(tokens_by_lead_provider.keys).to match_array(LeadProvider.pluck(:ecf_id)) }

it "generates valid ECF tokens" do
tokens_by_lead_provider.each do |ecf_id, tokens|
cpd_lead_provider = Migration::Ecf::NpqLeadProvider.find(ecf_id).cpd_lead_provider

expect(Migration::Ecf::LeadProviderAPIToken.find_by_unhashed_token(tokens[:ecf]).cpd_lead_provider).to eq(cpd_lead_provider)
end
end

it "generates valid NPQ tokens" do
tokens_by_lead_provider.each do |ecf_id, tokens|
lead_provider = LeadProvider.find_by(ecf_id:)

expect(APIToken.find_by_unhashed_token(tokens[:npq]).lead_provider).to eq(lead_provider)
end
end

context "when not running in the test environment" do
before { allow(Rails).to receive(:env) { "migration".inquiry } }

it "generates valid ECF tokens" do
tokens_by_lead_provider.each do |ecf_id, tokens|
cpd_lead_provider = Migration::Ecf::NpqLeadProvider.find(ecf_id).cpd_lead_provider

expect(Migration::Ecf::LeadProviderAPIToken.find_by_unhashed_token(tokens[:ecf]).cpd_lead_provider).to eq(cpd_lead_provider)
end
end
end

context "when the parity check is disabled" do
before do
allow(Rails.application.config).to receive(:npq_separation).and_return({
parity_check: {
enabled: false,
},
})
end

it { expect { tokens_by_lead_provider }.to raise_error(described_class::UnsupportedEnvironmentError, "The parity check functionality is disabled for this environment") }
end
end
end

0 comments on commit f2ded5f

Please sign in to comment.