From 2d30f4da929a75f54554c929b4c46ee5376e113e Mon Sep 17 00:00:00 2001 From: Rezaan Syed Date: Sun, 10 Jan 2021 21:27:21 -0500 Subject: [PATCH] Add ShopifyAPI::ApiAccess for encapsulating scope operations --- lib/shopify_api.rb | 1 + lib/shopify_api/api_access.rb | 57 +++++++++++++ lib/shopify_api/session.rb | 2 +- test/api_access_test.rb | 153 ++++++++++++++++++++++++++++++++++ 4 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 lib/shopify_api/api_access.rb create mode 100644 test/api_access_test.rb diff --git a/lib/shopify_api.rb b/lib/shopify_api.rb index dc2dd1383..f5f8d6421 100644 --- a/lib/shopify_api.rb +++ b/lib/shopify_api.rb @@ -21,6 +21,7 @@ module ShopifyAPI require 'shopify_api/countable' require 'shopify_api/resources' require 'shopify_api/session' +require 'shopify_api/api_access' require 'shopify_api/message_enricher' require 'shopify_api/connection' require 'shopify_api/pagination_link_headers' diff --git a/lib/shopify_api/api_access.rb b/lib/shopify_api/api_access.rb new file mode 100644 index 000000000..fc54fc960 --- /dev/null +++ b/lib/shopify_api/api_access.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +module ShopifyAPI + class ApiAccess + SCOPE_DELIMITER = ',' + + def initialize(scope_names) + if scope_names.is_a?(String) + scope_names = scope_names.to_s.split(SCOPE_DELIMITER) + end + + store_scopes(scope_names) + end + + def covers?(scopes) + scopes.compressed_scopes <= expanded_scopes + end + + def to_s + to_a.join(SCOPE_DELIMITER) + end + + def to_a + compressed_scopes.to_a + end + + def ==(other) + other.class == self.class && + compressed_scopes == other.compressed_scopes + end + + alias :eql? :== + + def hash + compressed_scopes.hash + end + + protected + + attr_reader :compressed_scopes, :expanded_scopes + + private + + def store_scopes(scope_names) + scopes = scope_names.map(&:strip).reject(&:empty?).to_set + implied_scopes = scopes.map { |scope| implied_scope(scope) }.compact + + @compressed_scopes = scopes - implied_scopes + @expanded_scopes = scopes + implied_scopes + end + + def implied_scope(scope) + is_write_scope = scope =~ /\A(unauthenticated_)?write_(.*)\z/ + "#{$1}read_#{$2}" if is_write_scope + end + end +end diff --git a/lib/shopify_api/session.rb b/lib/shopify_api/session.rb index 418215f25..66a7fb0f7 100644 --- a/lib/shopify_api/session.rb +++ b/lib/shopify_api/session.rb @@ -100,7 +100,7 @@ def initialize(domain:, token:, api_version: ShopifyAPI::Base.api_version, extra end def create_permission_url(scope, redirect_uri, options = {}) - params = { client_id: api_key, scope: scope.join(','), redirect_uri: redirect_uri } + params = { client_id: api_key, scope: ShopifyAPI::ApiAccess.new(scope).to_s, redirect_uri: redirect_uri } params[:state] = options[:state] if options[:state] construct_oauth_url("authorize", params) end diff --git a/test/api_access_test.rb b/test/api_access_test.rb new file mode 100644 index 000000000..fc1f5a51f --- /dev/null +++ b/test/api_access_test.rb @@ -0,0 +1,153 @@ +# frozen_string_literal: true +require 'test_helper' + +class ApiAccessTest < Minitest::Test + def test_write_is_the_same_access_as_read_write_on_the_same_resource + read_write_orders = ShopifyAPI::ApiAccess.new(%w(read_orders write_orders)) + write_orders = ShopifyAPI::ApiAccess.new(%w(write_orders)) + + assert_equal write_orders, read_write_orders + end + + def test_write_is_the_same_access_as_read_write_on_the_same_unauthenticated_resource + unauthenticated_read_write_orders = ShopifyAPI::ApiAccess.new(%w(unauthenticated_read_orders unauthenticated_write_orders)) + unauthenticated_write_orders = ShopifyAPI::ApiAccess.new(%w(unauthenticated_write_orders)) + + assert_equal unauthenticated_write_orders, unauthenticated_read_write_orders + end + + def test_read_is_not_the_same_as_read_write_on_the_same_resource + read_orders = ShopifyAPI::ApiAccess.new(%w(read_orders)) + read_write_orders = ShopifyAPI::ApiAccess.new(%w(write_orders read_orders)) + + refute_equal read_write_orders, read_orders + end + + def test_two_different_resources_are_not_equal + read_orders = ShopifyAPI::ApiAccess.new(%w(read_orders)) + read_products = ShopifyAPI::ApiAccess.new(%w(read_products)) + + refute_equal read_orders, read_products + end + + def test_two_identical_scopes_are_equal + read_orders = ShopifyAPI::ApiAccess.new(%w(read_orders)) + read_orders_identical = ShopifyAPI::ApiAccess.new(%w(read_orders)) + + assert_equal read_orders_identical, read_orders + end + + def test_unauthenticated_is_not_implied_by_authenticated_access + unauthenticated_orders = ShopifyAPI::ApiAccess.new(%w(unauthenticated_read_orders)) + authenticated_read_orders = ShopifyAPI::ApiAccess.new(%w(read_orders)) + authenticated_write_orders = ShopifyAPI::ApiAccess.new(%w(write_orders)) + + refute_equal unauthenticated_orders, authenticated_read_orders + refute_equal unauthenticated_orders, authenticated_write_orders + end + + def test_scopes_covers_is_truthy_for_same_scopes + read_orders = ShopifyAPI::ApiAccess.new(%w(read_orders)) + read_orders_identical = ShopifyAPI::ApiAccess.new(%w(read_orders)) + + assert read_orders.covers?(read_orders_identical) + end + + def test_covers_is_falsy_for_different_scopes + read_orders = ShopifyAPI::ApiAccess.new(%w(read_orders)) + read_products = ShopifyAPI::ApiAccess.new(%w(read_products)) + + refute read_orders.covers?(read_products) + end + + def test_covers_is_truthy_for_read_when_the_set_has_read_write + write_products = ShopifyAPI::ApiAccess.new(%w(write_products)) + read_products = ShopifyAPI::ApiAccess.new(%w(read_products)) + + assert write_products.covers?(read_products) + end + + def test_covers_is_truthy_for_read_when_the_set_has_read_write_for_that_resource_and_others + write_products_and_orders = ShopifyAPI::ApiAccess.new(%w(write_products, write_orders)) + read_orders = ShopifyAPI::ApiAccess.new(%w(read_orders)) + + assert write_products_and_orders.covers?(read_orders) + end + + def test_covers_is_truthy_for_write_when_the_set_has_read_write_for_that_resource_and_others + write_products_and_orders = ShopifyAPI::ApiAccess.new(%w(write_products, write_orders)) + write_orders = ShopifyAPI::ApiAccess.new(%w(write_orders)) + + assert write_products_and_orders.covers?(write_orders) + end + + def test_covers_is_truthy_for_subset_of_scopes + write_products_orders_customers = ShopifyAPI::ApiAccess.new(%w(write_products write_orders write_customers)) + write_orders_products = ShopifyAPI::ApiAccess.new(%w(write_orders read_products)) + + assert write_products_orders_customers.covers?(write_orders_products) + end + + def test_covers_is_falsy_for_sets_of_scopes_that_have_no_common_elements + write_products_orders_customers = ShopifyAPI::ApiAccess.new(%w(write_products write_orders write_customers)) + write_images_read_content = ShopifyAPI::ApiAccess.new(%w(write_images read_content)) + + refute write_products_orders_customers.covers?(write_images_read_content) + end + + def test_covers_is_falsy_for_sets_of_scopes_that_have_only_some_common_access + write_products_orders_customers = ShopifyAPI::ApiAccess.new(%w(write_products write_orders write_customers)) + write_products_read_content = ShopifyAPI::ApiAccess.new(%w(write_products read_content)) + + refute write_products_orders_customers.covers?(write_products_read_content) + end + + def test_duplicate_scopes_resolve_to_one_scope + read_orders_duplicated = ShopifyAPI::ApiAccess.new(%w(read_orders read_orders read_orders read_orders)) + read_orders = ShopifyAPI::ApiAccess.new(%w(read_orders)) + + assert_equal read_orders, read_orders_duplicated + end + + def test_to_s_outputs_scopes_as_a_comma_separated_list_without_implied_read_scopes + serialized_read_products_write_orders = "read_products,write_orders" + read_products_write_orders = ShopifyAPI::ApiAccess.new(%w(read_products read_orders write_orders)) + + assert_equal read_products_write_orders.to_s, serialized_read_products_write_orders + end + + def test_to_a_outputs_scopes_as_an_array_of_strings_without_implied_read_scopes + serialized_read_products_write_orders = %w(write_orders read_products) + read_products_write_orders = ShopifyAPI::ApiAccess.new(%w(read_products read_orders write_orders)) + + assert_equal read_products_write_orders.to_a.sort, serialized_read_products_write_orders.sort + end + + def test_creating_scopes_removes_extra_whitespace_from_scope_name_and_blank_scope_names + deserialized_read_products_write_orders = ShopifyAPI::ApiAccess.new([' read_products', ' ', 'write_orders ']) + serialized_read_products_write_orders = deserialized_read_products_write_orders.to_s + expected_read_products_write_orders = ShopifyAPI::ApiAccess.new(%w(read_products write_orders)) + + assert_equal expected_read_products_write_orders, ShopifyAPI::ApiAccess.new(serialized_read_products_write_orders) + end + + def test_creating_scopes_from_a_string_works_with_a_comma_separated_list + deserialized_read_products_write_orders = ShopifyAPI::ApiAccess.new("read_products,write_orders") + serialized_read_products_write_orders = deserialized_read_products_write_orders.to_s + expected_read_products_write_orders = ShopifyAPI::ApiAccess.new(%w(read_products write_orders)) + + assert_equal expected_read_products_write_orders, ShopifyAPI::ApiAccess.new(serialized_read_products_write_orders) + end + + def test_using_to_s_from_one_scopes_to_construct_another_will_be_equal + read_products_write_orders = ShopifyAPI::ApiAccess.new(%w(read_products write_orders)) + + assert_equal read_products_write_orders, ShopifyAPI::ApiAccess.new(read_products_write_orders.to_s) + end + + def test_using_to_a_from_one_scopes_to_construct_another_will_be_equal + read_products_write_orders = ShopifyAPI::ApiAccess.new(%w(read_products write_orders)) + + assert_equal read_products_write_orders, ShopifyAPI::ApiAccess.new(read_products_write_orders.to_a) + end +end