Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions app/controllers/spree/api/v2/platform/clone_stores_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
Copilot marked this conversation as resolved.
Comment thread
Elias-3817 marked this conversation as resolved.

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
Comment thread
Elias-3817 marked this conversation as resolved.
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!
Expand Down
24 changes: 24 additions & 0 deletions app/services/spree/olitt/clone_store/store_api_key_provisioner.rb
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Comment thread
Elias-3817 marked this conversation as resolved.
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
Original file line number Diff line number Diff line change
@@ -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
Comment thread
Elias-3817 marked this conversation as resolved.
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
Loading