diff --git a/.gitignore b/.gitignore index d3ad3d80b..38bd858dd 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,7 @@ coverage/ config/secrets.yml.key config/secrets.yml.enc config/rmt.local.yml +config/system_uuid .env .tags* diff --git a/app/models/hw_info.rb b/app/models/hw_info.rb index 9572b46df..6986062ee 100644 --- a/app/models/hw_info.rb +++ b/app/models/hw_info.rb @@ -5,7 +5,7 @@ class HwInfo < ApplicationRecord before_validation :make_invalid_uuid_nil # We store UUID as a downcased string. Please take that in account in finders - validates :uuid, uuid_format: true, uniqueness: { allow_nil: true } + validates :uuid, uuid_format: true validates :system, uniqueness: true, presence: true before_save -> { uuid.try(:downcase!) } diff --git a/db/migrate/20180420145408_remove_hw_info_uuid_index.rb b/db/migrate/20180420145408_remove_hw_info_uuid_index.rb new file mode 100644 index 000000000..c5d796151 --- /dev/null +++ b/db/migrate/20180420145408_remove_hw_info_uuid_index.rb @@ -0,0 +1,5 @@ +class RemoveHwInfoUuidIndex < ActiveRecord::Migration[5.1] + def change + remove_index :hw_infos, :uuid + end +end diff --git a/db/schema.rb b/db/schema.rb index cd4e241a2..929011243 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20180416124217) do +ActiveRecord::Schema.define(version: 20180420145408) do create_table "activations", force: :cascade, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8" do |t| t.bigint "service_id", null: false @@ -41,7 +41,6 @@ t.datetime "updated_at", null: false t.index ["hypervisor"], name: "index_hw_infos_on_hypervisor" t.index ["system_id"], name: "index_hw_infos_on_system_id", unique: true - t.index ["uuid"], name: "index_hw_infos_on_uuid", unique: true end create_table "product_predecessors", force: :cascade, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8" do |t| diff --git a/lib/suse/connect/api.rb b/lib/suse/connect/api.rb index 3623e00d0..8509be0b8 100644 --- a/lib/suse/connect/api.rb +++ b/lib/suse/connect/api.rb @@ -7,6 +7,8 @@ module Connect class Api class InvalidCredentialsError < StandardError; end + CONNECT_API_URL = 'https://scc.suse.com/connect'.freeze + UUID_FILE_LOCATION = File.expand_path('../../../config/system_uuid', __dir__).freeze def initialize(username, password) @username = username @@ -14,23 +16,23 @@ def initialize(username, password) end def list_orders - make_paginated_request(:get, 'https://scc.suse.com/connect/organizations/orders') + make_paginated_request(:get, "#{CONNECT_API_URL}/organizations/orders") end def list_products - make_paginated_request(:get, 'https://scc.suse.com/connect/organizations/products') + make_paginated_request(:get, "#{CONNECT_API_URL}/organizations/products") end def list_products_unscoped - make_paginated_request(:get, 'https://scc.suse.com/connect/organizations/products/unscoped') + make_paginated_request(:get, "#{CONNECT_API_URL}/organizations/products/unscoped") end def list_repositories - make_paginated_request(:get, 'https://scc.suse.com/connect/organizations/repositories') + make_paginated_request(:get, "#{CONNECT_API_URL}/organizations/repositories") end def list_subscriptions - make_paginated_request(:get, 'https://scc.suse.com/connect/organizations/subscriptions') + make_paginated_request(:get, "#{CONNECT_API_URL}/organizations/subscriptions") end protected @@ -47,6 +49,7 @@ def make_request(method, url, options) options[:userpwd] = "#{@username}:#{@password}" unless options[:userpwd] options[:method] = method options[:accept_encoding] = 'gzip, deflate' + options[:headers] = { 'RMT' => system_uuid } response = RMT::HttpRequest.new(url, options).run raise InvalidCredentialsError if (response.code == 401) @@ -79,6 +82,17 @@ def make_paginated_request(method, url, options = {}) @entities end + private + + def system_uuid + @system_uuid ||= if File.exist?(UUID_FILE_LOCATION) + File.read(UUID_FILE_LOCATION) + else + uuid = SecureRandom.uuid + File.write(UUID_FILE_LOCATION, uuid) + uuid + end + end end end end diff --git a/spec/models/hw_info_spec.rb b/spec/models/hw_info_spec.rb index a7e9753a5..f2320a021 100644 --- a/spec/models/hw_info_spec.rb +++ b/spec/models/hw_info_spec.rb @@ -11,7 +11,6 @@ it 'enforces uniqueness' do expect(hw_info).to validate_uniqueness_of(:system) - expect(hw_info).to validate_uniqueness_of(:uuid) end describe '.uuid' do diff --git a/spec/suse/connect/api_spec.rb b/spec/suse/connect/api_spec.rb index c719e193d..73bb00e10 100644 --- a/spec/suse/connect/api_spec.rb +++ b/spec/suse/connect/api_spec.rb @@ -2,125 +2,156 @@ require 'webmock/rspec' RSpec.describe SUSE::Connect::Api do - before do - stub_request(:GET, 'http://example.org/api_method') - .with(headers: expected_request_headers) - .to_return( - status: 200, - body: response_data.to_json, - headers: {} - ) - - stub_request(:GET, 'http://example.org/api_method?page=1') - .with(headers: expected_request_headers) - .to_return( - status: 200, - body: [ response_data ].to_json, - headers: { - 'Link' => '; rel="next"' - } - ) - - stub_request(:GET, 'http://example.org/api_method?page=2') - .with(headers: expected_request_headers) - .to_return( - status: 200, - body: [ response_data ].to_json, - headers: {} - ) - - stub_request(:get, 'https://scc.suse.com/connect/organizations/orders?page=1') - .with(headers: expected_request_headers) - .to_return( - status: 200, - body: [ { endpoint: 'organizations/orders' } ].to_json, - headers: {} - ) - - stub_request(:get, 'https://scc.suse.com/connect/organizations/products?page=1') - .with(headers: expected_request_headers) - .to_return( - status: 200, - body: [ { endpoint: 'organizations/products' } ].to_json, - headers: {} - ) - - stub_request(:get, 'https://scc.suse.com/connect/organizations/products/unscoped?page=1') - .with(headers: expected_request_headers) - .to_return( - status: 200, - body: [ { endpoint: 'organizations/products/unscoped' } ].to_json, - headers: {} - ) - - stub_request(:get, 'https://scc.suse.com/connect/organizations/repositories?page=1') - .with(headers: expected_request_headers) - .to_return( - status: 200, - body: [ { endpoint: 'organizations/repositories' } ].to_json, - headers: {} - ) - - stub_request(:get, 'https://scc.suse.com/connect/organizations/subscriptions?page=1') - .with(headers: expected_request_headers) - .to_return( - status: 200, - body: [ { endpoint: 'organizations/subscriptions' } ].to_json, - headers: {} - ) - end - - let(:url) { 'http://example.com' } let(:username) { 'scc_user' } let(:password) { 'scc_password' } let(:api_client) { described_class.new(username, password) } - let(:expected_request_headers) do - { - 'Authorization' => 'Basic ' + Base64.encode64("#{username}:#{password}").strip, - 'User-Agent' => "RMT/#{RMT::VERSION}" - } - end - let(:response_data) { { foo: 'bar' } } - - describe '#make_single_request' do - subject { api_client.send(:make_single_request, 'GET', 'http://example.org/api_method') } - - it { is_expected.to eq(response_data) } - end - - describe '#make_paginated_request' do - subject { api_client.send(:make_paginated_request, 'GET', 'http://example.org/api_method') } - - it { is_expected.to eq([response_data, response_data]) } - end - - describe '#list_orders' do - subject { api_client.list_orders } - - it { is_expected.to eq([ { endpoint: 'organizations/orders' } ]) } + let(:uuid) { 'test-uuid' } + + describe '#system_uuid' do + subject(:method_call) { api_client.send(:system_uuid) } + + context 'when system_uuid file exists' do + it 'reads a file' do + allow(File).to receive(:exist?).with(described_class::UUID_FILE_LOCATION).and_return(true) + expect(File).not_to receive(:write).with(described_class::UUID_FILE_LOCATION, uuid) + expect(File).to receive(:read).with(described_class::UUID_FILE_LOCATION).and_return(uuid) + expect(method_call).to be(uuid) + end + end + + context 'when system_uuid file does not exist' do + it 'creates a file' do + allow(File).to receive(:exist?).with(described_class::UUID_FILE_LOCATION).and_return(false) + allow(SecureRandom).to receive(:uuid).and_return(uuid) + + expect(File).to receive(:write).with(described_class::UUID_FILE_LOCATION, uuid).exactly(1).times + expect(File).not_to receive(:read).with(described_class::UUID_FILE_LOCATION) + expect(method_call).to be(uuid) + end + end end - describe '#list_products' do - subject { api_client.list_products } - - it { is_expected.to eq([ { endpoint: 'organizations/products' } ]) } - end - - describe '#list_products_unscoped' do - subject { api_client.list_products_unscoped } - - it { is_expected.to eq([ { endpoint: 'organizations/products/unscoped' } ]) } - end - - describe '#list_repositories' do - subject { api_client.list_repositories } - - it { is_expected.to eq([ { endpoint: 'organizations/repositories' } ]) } - end - - describe '#list_subscriptions' do - subject { api_client.list_subscriptions } - - it { is_expected.to eq([ { endpoint: 'organizations/subscriptions' } ]) } + context 'api requests' do + before do + allow_any_instance_of(described_class).to receive(:system_uuid).and_return(uuid) + + stub_request(:GET, 'http://example.org/api_method') + .with(headers: expected_request_headers) + .to_return( + status: 200, + body: response_data.to_json, + headers: {} + ) + + stub_request(:GET, 'http://example.org/api_method?page=1') + .with(headers: expected_request_headers) + .to_return( + status: 200, + body: [ response_data ].to_json, + headers: { + 'Link' => '; rel="next"' + } + ) + + stub_request(:GET, 'http://example.org/api_method?page=2') + .with(headers: expected_request_headers) + .to_return( + status: 200, + body: [ response_data ].to_json, + headers: {} + ) + + stub_request(:get, 'https://scc.suse.com/connect/organizations/orders?page=1') + .with(headers: expected_request_headers) + .to_return( + status: 200, + body: [ { endpoint: 'organizations/orders' } ].to_json, + headers: {} + ) + + stub_request(:get, 'https://scc.suse.com/connect/organizations/products?page=1') + .with(headers: expected_request_headers) + .to_return( + status: 200, + body: [ { endpoint: 'organizations/products' } ].to_json, + headers: {} + ) + + stub_request(:get, 'https://scc.suse.com/connect/organizations/products/unscoped?page=1') + .with(headers: expected_request_headers) + .to_return( + status: 200, + body: [ { endpoint: 'organizations/products/unscoped' } ].to_json, + headers: {} + ) + + stub_request(:get, 'https://scc.suse.com/connect/organizations/repositories?page=1') + .with(headers: expected_request_headers) + .to_return( + status: 200, + body: [ { endpoint: 'organizations/repositories' } ].to_json, + headers: {} + ) + + stub_request(:get, 'https://scc.suse.com/connect/organizations/subscriptions?page=1') + .with(headers: expected_request_headers) + .to_return( + status: 200, + body: [ { endpoint: 'organizations/subscriptions' } ].to_json, + headers: {} + ) + end + + let(:url) { 'http://example.com' } + let(:expected_request_headers) do + { + 'Authorization' => 'Basic ' + Base64.encode64("#{username}:#{password}").strip, + 'User-Agent' => "RMT/#{RMT::VERSION}", + 'RMT' => uuid + } + end + let(:response_data) { { foo: 'bar' } } + + describe '#make_single_request' do + subject { api_client.send(:make_single_request, 'GET', 'http://example.org/api_method') } + + it { is_expected.to eq(response_data) } + end + + describe '#make_paginated_request' do + subject { api_client.send(:make_paginated_request, 'GET', 'http://example.org/api_method') } + + it { is_expected.to eq([response_data, response_data]) } + end + + describe '#list_orders' do + subject { api_client.list_orders } + + it { is_expected.to eq([ { endpoint: 'organizations/orders' } ]) } + end + + describe '#list_products' do + subject { api_client.list_products } + + it { is_expected.to eq([ { endpoint: 'organizations/products' } ]) } + end + + describe '#list_products_unscoped' do + subject { api_client.list_products_unscoped } + + it { is_expected.to eq([ { endpoint: 'organizations/products/unscoped' } ]) } + end + + describe '#list_repositories' do + subject { api_client.list_repositories } + + it { is_expected.to eq([ { endpoint: 'organizations/repositories' } ]) } + end + + describe '#list_subscriptions' do + subject { api_client.list_subscriptions } + + it { is_expected.to eq([ { endpoint: 'organizations/subscriptions' } ]) } + end end end