Skip to content

Commit f4bcdef

Browse files
authored
Merge pull request #829 from Shopify/define-api-access
Provide Scopes value object to encapsulate scope operations
2 parents 13f489b + 2d30f4d commit f4bcdef

File tree

4 files changed

+212
-1
lines changed

4 files changed

+212
-1
lines changed

lib/shopify_api.rb

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ module ShopifyAPI
2121
require 'shopify_api/countable'
2222
require 'shopify_api/resources'
2323
require 'shopify_api/session'
24+
require 'shopify_api/api_access'
2425
require 'shopify_api/message_enricher'
2526
require 'shopify_api/connection'
2627
require 'shopify_api/pagination_link_headers'

lib/shopify_api/api_access.rb

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# frozen_string_literal: true
2+
3+
module ShopifyAPI
4+
class ApiAccess
5+
SCOPE_DELIMITER = ','
6+
7+
def initialize(scope_names)
8+
if scope_names.is_a?(String)
9+
scope_names = scope_names.to_s.split(SCOPE_DELIMITER)
10+
end
11+
12+
store_scopes(scope_names)
13+
end
14+
15+
def covers?(scopes)
16+
scopes.compressed_scopes <= expanded_scopes
17+
end
18+
19+
def to_s
20+
to_a.join(SCOPE_DELIMITER)
21+
end
22+
23+
def to_a
24+
compressed_scopes.to_a
25+
end
26+
27+
def ==(other)
28+
other.class == self.class &&
29+
compressed_scopes == other.compressed_scopes
30+
end
31+
32+
alias :eql? :==
33+
34+
def hash
35+
compressed_scopes.hash
36+
end
37+
38+
protected
39+
40+
attr_reader :compressed_scopes, :expanded_scopes
41+
42+
private
43+
44+
def store_scopes(scope_names)
45+
scopes = scope_names.map(&:strip).reject(&:empty?).to_set
46+
implied_scopes = scopes.map { |scope| implied_scope(scope) }.compact
47+
48+
@compressed_scopes = scopes - implied_scopes
49+
@expanded_scopes = scopes + implied_scopes
50+
end
51+
52+
def implied_scope(scope)
53+
is_write_scope = scope =~ /\A(unauthenticated_)?write_(.*)\z/
54+
"#{$1}read_#{$2}" if is_write_scope
55+
end
56+
end
57+
end

lib/shopify_api/session.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ def initialize(domain:, token:, api_version: ShopifyAPI::Base.api_version, extra
100100
end
101101

102102
def create_permission_url(scope, redirect_uri, options = {})
103-
params = { client_id: api_key, scope: scope.join(','), redirect_uri: redirect_uri }
103+
params = { client_id: api_key, scope: ShopifyAPI::ApiAccess.new(scope).to_s, redirect_uri: redirect_uri }
104104
params[:state] = options[:state] if options[:state]
105105
construct_oauth_url("authorize", params)
106106
end

test/api_access_test.rb

+153
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
# frozen_string_literal: true
2+
require 'test_helper'
3+
4+
class ApiAccessTest < Minitest::Test
5+
def test_write_is_the_same_access_as_read_write_on_the_same_resource
6+
read_write_orders = ShopifyAPI::ApiAccess.new(%w(read_orders write_orders))
7+
write_orders = ShopifyAPI::ApiAccess.new(%w(write_orders))
8+
9+
assert_equal write_orders, read_write_orders
10+
end
11+
12+
def test_write_is_the_same_access_as_read_write_on_the_same_unauthenticated_resource
13+
unauthenticated_read_write_orders = ShopifyAPI::ApiAccess.new(%w(unauthenticated_read_orders unauthenticated_write_orders))
14+
unauthenticated_write_orders = ShopifyAPI::ApiAccess.new(%w(unauthenticated_write_orders))
15+
16+
assert_equal unauthenticated_write_orders, unauthenticated_read_write_orders
17+
end
18+
19+
def test_read_is_not_the_same_as_read_write_on_the_same_resource
20+
read_orders = ShopifyAPI::ApiAccess.new(%w(read_orders))
21+
read_write_orders = ShopifyAPI::ApiAccess.new(%w(write_orders read_orders))
22+
23+
refute_equal read_write_orders, read_orders
24+
end
25+
26+
def test_two_different_resources_are_not_equal
27+
read_orders = ShopifyAPI::ApiAccess.new(%w(read_orders))
28+
read_products = ShopifyAPI::ApiAccess.new(%w(read_products))
29+
30+
refute_equal read_orders, read_products
31+
end
32+
33+
def test_two_identical_scopes_are_equal
34+
read_orders = ShopifyAPI::ApiAccess.new(%w(read_orders))
35+
read_orders_identical = ShopifyAPI::ApiAccess.new(%w(read_orders))
36+
37+
assert_equal read_orders_identical, read_orders
38+
end
39+
40+
def test_unauthenticated_is_not_implied_by_authenticated_access
41+
unauthenticated_orders = ShopifyAPI::ApiAccess.new(%w(unauthenticated_read_orders))
42+
authenticated_read_orders = ShopifyAPI::ApiAccess.new(%w(read_orders))
43+
authenticated_write_orders = ShopifyAPI::ApiAccess.new(%w(write_orders))
44+
45+
refute_equal unauthenticated_orders, authenticated_read_orders
46+
refute_equal unauthenticated_orders, authenticated_write_orders
47+
end
48+
49+
def test_scopes_covers_is_truthy_for_same_scopes
50+
read_orders = ShopifyAPI::ApiAccess.new(%w(read_orders))
51+
read_orders_identical = ShopifyAPI::ApiAccess.new(%w(read_orders))
52+
53+
assert read_orders.covers?(read_orders_identical)
54+
end
55+
56+
def test_covers_is_falsy_for_different_scopes
57+
read_orders = ShopifyAPI::ApiAccess.new(%w(read_orders))
58+
read_products = ShopifyAPI::ApiAccess.new(%w(read_products))
59+
60+
refute read_orders.covers?(read_products)
61+
end
62+
63+
def test_covers_is_truthy_for_read_when_the_set_has_read_write
64+
write_products = ShopifyAPI::ApiAccess.new(%w(write_products))
65+
read_products = ShopifyAPI::ApiAccess.new(%w(read_products))
66+
67+
assert write_products.covers?(read_products)
68+
end
69+
70+
def test_covers_is_truthy_for_read_when_the_set_has_read_write_for_that_resource_and_others
71+
write_products_and_orders = ShopifyAPI::ApiAccess.new(%w(write_products, write_orders))
72+
read_orders = ShopifyAPI::ApiAccess.new(%w(read_orders))
73+
74+
assert write_products_and_orders.covers?(read_orders)
75+
end
76+
77+
def test_covers_is_truthy_for_write_when_the_set_has_read_write_for_that_resource_and_others
78+
write_products_and_orders = ShopifyAPI::ApiAccess.new(%w(write_products, write_orders))
79+
write_orders = ShopifyAPI::ApiAccess.new(%w(write_orders))
80+
81+
assert write_products_and_orders.covers?(write_orders)
82+
end
83+
84+
def test_covers_is_truthy_for_subset_of_scopes
85+
write_products_orders_customers = ShopifyAPI::ApiAccess.new(%w(write_products write_orders write_customers))
86+
write_orders_products = ShopifyAPI::ApiAccess.new(%w(write_orders read_products))
87+
88+
assert write_products_orders_customers.covers?(write_orders_products)
89+
end
90+
91+
def test_covers_is_falsy_for_sets_of_scopes_that_have_no_common_elements
92+
write_products_orders_customers = ShopifyAPI::ApiAccess.new(%w(write_products write_orders write_customers))
93+
write_images_read_content = ShopifyAPI::ApiAccess.new(%w(write_images read_content))
94+
95+
refute write_products_orders_customers.covers?(write_images_read_content)
96+
end
97+
98+
def test_covers_is_falsy_for_sets_of_scopes_that_have_only_some_common_access
99+
write_products_orders_customers = ShopifyAPI::ApiAccess.new(%w(write_products write_orders write_customers))
100+
write_products_read_content = ShopifyAPI::ApiAccess.new(%w(write_products read_content))
101+
102+
refute write_products_orders_customers.covers?(write_products_read_content)
103+
end
104+
105+
def test_duplicate_scopes_resolve_to_one_scope
106+
read_orders_duplicated = ShopifyAPI::ApiAccess.new(%w(read_orders read_orders read_orders read_orders))
107+
read_orders = ShopifyAPI::ApiAccess.new(%w(read_orders))
108+
109+
assert_equal read_orders, read_orders_duplicated
110+
end
111+
112+
def test_to_s_outputs_scopes_as_a_comma_separated_list_without_implied_read_scopes
113+
serialized_read_products_write_orders = "read_products,write_orders"
114+
read_products_write_orders = ShopifyAPI::ApiAccess.new(%w(read_products read_orders write_orders))
115+
116+
assert_equal read_products_write_orders.to_s, serialized_read_products_write_orders
117+
end
118+
119+
def test_to_a_outputs_scopes_as_an_array_of_strings_without_implied_read_scopes
120+
serialized_read_products_write_orders = %w(write_orders read_products)
121+
read_products_write_orders = ShopifyAPI::ApiAccess.new(%w(read_products read_orders write_orders))
122+
123+
assert_equal read_products_write_orders.to_a.sort, serialized_read_products_write_orders.sort
124+
end
125+
126+
def test_creating_scopes_removes_extra_whitespace_from_scope_name_and_blank_scope_names
127+
deserialized_read_products_write_orders = ShopifyAPI::ApiAccess.new([' read_products', ' ', 'write_orders '])
128+
serialized_read_products_write_orders = deserialized_read_products_write_orders.to_s
129+
expected_read_products_write_orders = ShopifyAPI::ApiAccess.new(%w(read_products write_orders))
130+
131+
assert_equal expected_read_products_write_orders, ShopifyAPI::ApiAccess.new(serialized_read_products_write_orders)
132+
end
133+
134+
def test_creating_scopes_from_a_string_works_with_a_comma_separated_list
135+
deserialized_read_products_write_orders = ShopifyAPI::ApiAccess.new("read_products,write_orders")
136+
serialized_read_products_write_orders = deserialized_read_products_write_orders.to_s
137+
expected_read_products_write_orders = ShopifyAPI::ApiAccess.new(%w(read_products write_orders))
138+
139+
assert_equal expected_read_products_write_orders, ShopifyAPI::ApiAccess.new(serialized_read_products_write_orders)
140+
end
141+
142+
def test_using_to_s_from_one_scopes_to_construct_another_will_be_equal
143+
read_products_write_orders = ShopifyAPI::ApiAccess.new(%w(read_products write_orders))
144+
145+
assert_equal read_products_write_orders, ShopifyAPI::ApiAccess.new(read_products_write_orders.to_s)
146+
end
147+
148+
def test_using_to_a_from_one_scopes_to_construct_another_will_be_equal
149+
read_products_write_orders = ShopifyAPI::ApiAccess.new(%w(read_products write_orders))
150+
151+
assert_equal read_products_write_orders, ShopifyAPI::ApiAccess.new(read_products_write_orders.to_a)
152+
end
153+
end

0 commit comments

Comments
 (0)