diff --git a/app/controllers/spree/api/v2/platform/clone_stores_controller.rb b/app/controllers/spree/api/v2/platform/clone_stores_controller.rb index c70a2d3..7fece58 100644 --- a/app/controllers/spree/api/v2/platform/clone_stores_controller.rb +++ b/app/controllers/spree/api/v2/platform/clone_stores_controller.rb @@ -24,8 +24,53 @@ def show render_clone_request_status(params[:clone_request_id]) end + def ensure_api_key + store = resolve_ensure_api_key_store + return render_ensure_api_key_store_not_found if store.nil? + + api_key = Spree::Olitt::CloneStore::StoreApiKeyProvisioner.call(store) + render json: ensure_api_key_payload(store, api_key), status: :ok + end + private + def resolve_ensure_api_key_store + url = params[:url].to_s.strip + return nil if url.blank? + + Spree.current_store_finder.new(url: url).execute + end + + def render_ensure_api_key_store_not_found + render json: { + errors: ['Store not found for the provided url'], + meta: { status: 'not_found' } + }, status: :not_found + end + + def ensure_api_key_payload(store, api_key) + { + data: { id: store.id.to_s, type: 'store' }, + status: 'completed', + meta: { + status: 'completed', + public_api_key: serialize_public_api_key(api_key) + } + } + end + + def serialize_public_api_key(api_key) + return nil if api_key.nil? + + { + id: api_key.id, + name: api_key.name, + key_type: api_key.key_type, + token: api_key.token, + store_id: api_key.store_id + } + end + def authorize_clone_store_request! if api_key_header_present? authenticate_secret_key! diff --git a/app/services/spree/olitt/clone_store/store_api_key_provisioner.rb b/app/services/spree/olitt/clone_store/store_api_key_provisioner.rb new file mode 100644 index 0000000..1a19f21 --- /dev/null +++ b/app/services/spree/olitt/clone_store/store_api_key_provisioner.rb @@ -0,0 +1,24 @@ +module Spree + module Olitt + module CloneStore + class StoreApiKeyProvisioner + def self.call(store) + new(store).call + end + + def initialize(store) + @store = store + end + + def call + return nil unless @store.respond_to?(:api_keys) + + @store.api_keys.active.publishable.order(created_at: :desc).first || + @store.api_keys.create!(name: 'Storefront key', key_type: 'publishable') + rescue ActiveRecord::RecordNotUnique + @store.api_keys.active.publishable.order(created_at: :desc).first + end + end + end + end +end diff --git a/config/routes.rb b/config/routes.rb index 82d2701..12024ff 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -3,6 +3,7 @@ namespace :v2 do namespace :platform do post '/clone-store', to: 'clone_stores#create' + post '/clone-store/ensure-api-key', to: 'clone_stores#ensure_api_key' get '/clone-store/:clone_request_id', to: 'clone_stores#show' end end diff --git a/spec/controllers/spree/api/v2/platform/clone_stores_controller_spec.rb b/spec/controllers/spree/api/v2/platform/clone_stores_controller_spec.rb index 359ec55..a0d40f1 100644 --- a/spec/controllers/spree/api/v2/platform/clone_stores_controller_spec.rb +++ b/spec/controllers/spree/api/v2/platform/clone_stores_controller_spec.rb @@ -94,4 +94,73 @@ expect(JSON.parse(response.body)).to eq('error' => 'Valid secret API key required') end end + + describe '#ensure_api_key' do + let!(:live_store) do + create(:store, default: false, name: 'Live Store', url: 'live.example.com', code: 'live-store') + end + + before do + allow(Spree.current_store_finder).to receive(:new).and_return( + instance_double(Spree.current_store_finder, execute: live_store) + ) + end + + it 'mints a publishable key for the resolved store and returns it' do + expect(live_store.api_keys.active.publishable.count).to eq(0) + + post :ensure_api_key, params: { url: 'live.example.com' }, format: :json + + expect(response).to have_http_status(:ok) + payload = JSON.parse(response.body) + expect(payload.dig('data', 'id')).to eq(live_store.id.to_s) + expect(payload.dig('meta', 'status')).to eq('completed') + expect(payload.dig('meta', 'public_api_key', 'token')).to be_present + expect(payload.dig('meta', 'public_api_key', 'store_id')).to eq(live_store.id) + expect(live_store.api_keys.active.publishable.count).to eq(1) + end + + it 'is idempotent and returns the same key on a second call' do + post :ensure_api_key, params: { url: 'live.example.com' }, format: :json + first_token = JSON.parse(response.body).dig('meta', 'public_api_key', 'token') + + post :ensure_api_key, params: { url: 'live.example.com' }, format: :json + second_token = JSON.parse(response.body).dig('meta', 'public_api_key', 'token') + + expect(second_token).to eq(first_token) + expect(live_store.api_keys.active.publishable.count).to eq(1) + end + + it 'returns not found when no store resolves for the url' do + allow(Spree.current_store_finder).to receive(:new).and_return( + instance_double(Spree.current_store_finder, execute: nil) + ) + + post :ensure_api_key, params: { url: 'missing.example.com' }, format: :json + + expect(response).to have_http_status(:not_found) + expect(JSON.parse(response.body).dig('meta', 'status')).to eq('not_found') + end + end + + describe '#ensure_api_key duplicate store resolution' do + let!(:older_store) do + create(:store, default: false, name: 'Older Store', url: 'conflict.example.com', code: 'older-store') + end + + let!(:newer_store) do + create(:store, default: false, name: 'Newer Store', url: 'conflict.example.com', code: 'newer-store') + end + + it 'mints the key on the oldest store when two stores share the same url' do + post :ensure_api_key, params: { url: 'conflict.example.com' }, format: :json + + expect(response).to have_http_status(:ok) + payload = JSON.parse(response.body) + expect(payload.dig('data', 'id')).to eq(older_store.id.to_s) + expect(payload.dig('meta', 'public_api_key', 'store_id')).to eq(older_store.id) + expect(older_store.reload.api_keys.active.publishable.count).to eq(1) + expect(newer_store.reload.api_keys.active.publishable.count).to eq(0) + end + end end \ No newline at end of file diff --git a/spec/services/spree/olitt/clone_store/store_api_key_provisioner_spec.rb b/spec/services/spree/olitt/clone_store/store_api_key_provisioner_spec.rb new file mode 100644 index 0000000..b3f84b4 --- /dev/null +++ b/spec/services/spree/olitt/clone_store/store_api_key_provisioner_spec.rb @@ -0,0 +1,28 @@ +require 'spec_helper' + +describe Spree::Olitt::CloneStore::StoreApiKeyProvisioner do + describe '.call' do + let(:store) do + create(:store, default: false, url: 'shop.example.com', code: 'shop-store') + end + + it 'creates a publishable api key when the store has none' do + expect(store.api_keys.active.publishable.count).to eq(0) + + api_key = described_class.call(store) + + expect(api_key).to be_present + expect(api_key.key_type).to eq('publishable') + expect(api_key.store_id).to eq(store.id) + expect(store.api_keys.active.publishable.count).to eq(1) + end + + it 'returns the existing key instead of creating a second one' do + first_key = described_class.call(store) + second_key = described_class.call(store) + + expect(second_key.id).to eq(first_key.id) + expect(store.api_keys.active.publishable.count).to eq(1) + end + end +end