strong_presenter 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -2,56 +2,61 @@ module StrongPresenter
2
2
  # @private
3
3
  #
4
4
  # Storage format:
5
- # The permissions object is shared by collection presenters with its constituent presenters,
6
- # and it is also shared with all of its associations. Each attribute path is stored as an array
7
- # of symbols in a Set. There is one top level presenter - the one which initialized the
8
- # Permissions object.
5
+ # Each attribute path is stored as an array of objects, usually strings in a Set. For indifferent access,
6
+ # all symbols are converted to strings.
9
7
  #
10
- # When a presenter checks for permissions, the attribute path relative to the top
11
- # presenter is prepended to each attribute path, and its existence checked in the Set.
12
- #
13
8
  # Arguments can also be part of permissions control. They are simply additional elements in the attribute path array,
14
- # and need not be symbols. If they are symbols, there is no way for Permissions to know whether they
15
- # are part of the attribute path, or additional arguments. Only the presenter knows that.
9
+ # and need not be strings or symbols. Permitting a string or symbol argument automatically permits both.
10
+ #
11
+ # When checking if paths with tainted strings/elements are permitted, only exact matches are allowed
16
12
  class Permissions
17
13
 
18
- # Checks whether everything is permitted.
19
- #
14
+ def prefix_path
15
+ @prefix_path || []
16
+ end
17
+
18
+ # Initialize, optionally with link to permitted paths, and the prefix to that (with copy on write semantics)
19
+ def initialize(permissions = nil, prefix_path = [])
20
+ unless permissions.nil?
21
+ @permitted_paths = permissions.permitted_paths
22
+ @prefix_path = permissions.prefix_path + canonicalize(prefix_path) # copy on write
23
+ end
24
+ end
25
+
26
+ # Checks whether everything is permitted. Considers :*, which permits all methods
27
+ # but not association methods to be complete.
20
28
  # @return [Boolean]
21
29
  def complete?
22
- permitted_paths.include? [] and ((@permitted_paths = Set[[]] if permitted_paths.count > 1) or true)
30
+ permitted? prefix_path + [:*]
23
31
  end
24
32
 
25
- # Permits everything
26
- #
33
+ # Permits wildcard method, but not association methods.
27
34
  # @return self
28
35
  def permit_all!
29
- permitted_paths.clear
30
- permitted_paths << []
36
+ copy_on_write!
37
+ permitted_paths << [:*]
31
38
  self
32
39
  end
33
40
 
34
- # @overload permitted? prefix_path = nil, attribute_path
35
- #
36
41
  # Checks if the attribute path is permitted. This is the case if
37
42
  # any array prefix has been permitted.
38
- #
39
- # @param [Symbol, Array<Symbol>] prefix_path
40
- # @param [Symbol, Array<Symbol,Object>] attribute_path
43
+ # @param [Object, Array<Object>] prefix_path
44
+ # @param [Object, Array<Object>] attribute_path
41
45
  # @return [Boolean]
42
- def permitted? prefix_path, attribute_path = nil
43
- raw_permitted? Array(prefix_path), Array(attribute_path)
46
+ def permitted? attribute_path
47
+ attribute_path = canonicalize(attribute_path)
48
+ return true if permitted_paths.include? prefix_path + attribute_path # exact match
49
+ !path_tainted?(attribute_path) and permitted_by_wildcard?(prefix_path + attribute_path) # wildcard match only if not tainted
44
50
  end
45
51
 
46
52
  # Selects the attribute paths which are permitted.
47
- #
48
53
  # @param [Array] prefix_path
49
54
  # namespace in which each of the given attribute paths are in
50
- # @param [[Symbol, Array<Symbol,Object>]*] *attribute_paths
55
+ # @param [[Object, Array<Object>]*] *attribute_paths
51
56
  # each attribute path is a symbol or array of symbols
52
- # @return [Array<Symbol, Array<Symbol,Object>>] array of attribute paths permitted
53
- def select_permitted prefix_path, *attribute_paths
54
- raw_select_permitted Array(prefix_path), nested_array(attribute_paths)
57
+ # @return [Array<Object, Array<Object>>] array of attribute paths permitted
58
+ def select_permitted *attribute_paths
59
+ attribute_paths.select { |attribute_path| permitted?(attribute_path) }
55
60
  end
56
61
 
57
62
  # Rejects the attribute paths which are permitted. Opposite of select_permitted.
@@ -59,38 +64,27 @@ module StrongPresenter
59
64
  #
60
65
  # @param [Array] prefix_path
61
66
  # namespace in which each of the given attribute paths are in
62
- # @param [[Symbol, Array<Symbol,Object>]*] *attribute_paths
63
- # each attribute path is a symbol or array of symbols
64
- # @return [Array<Symbol, Array<Symbol,Object>>] array of attribute paths remaining
65
- def reject_permitted prefix_path, *attribute_paths
66
- raw_reject_permitted Array(prefix_path), nested_array(attribute_paths)
67
+ # @param [[Object, Array<Object>]*] *attribute_paths
68
+ # each attribute path is an object(string) or array
69
+ # @return [Array<Object, Array<Object>>] array of attribute paths remaining
70
+ def reject_permitted *attribute_paths
71
+ attribute_paths.reject { |attribute_path| permitted?(attribute_path) }
67
72
  end
68
73
 
69
74
  # Permits some attribute paths
70
75
  #
71
- # @param [Array<Symbol>] prefix_path
76
+ # @param [Array] prefix_path
72
77
  # path to prepend to each attribute path
73
- # @param [[Symbol, Array<Symbol,Object>]*] *attribute_paths
74
- def permit prefix_path, *attribute_paths
75
- prefix_path = Array(prefix_path)
76
- # don't permit if already permitted
77
- raw_reject_permitted(prefix_path, nested_array(attribute_paths)).each do |attribute_path|
78
- permitted_paths << prefix_path + attribute_path
78
+ # @param [[Object, Array]*] *attribute_paths
79
+ def permit *attribute_paths
80
+ copy_on_write!
81
+ attribute_paths = attribute_paths.map{|path|canonicalize(path).taint} # exact match
82
+ reject_permitted(*attribute_paths).each do |attribute_path| # don't permit if already permitted
83
+ permitted_paths << attribute_path # prefix_path = [] because of copy on write
79
84
  end
80
85
  self
81
86
  end
82
87
 
83
- # Merges the permissions from another Permissions object
84
- #
85
- # @param [Permissions] permissions
86
- # @param [Array<Symbol>] prefix
87
- # prefix to prepend to paths in permissions
88
- # @return self
89
- def merge permissions, prefix = []
90
- permitted_paths.merge permissions.permitted_paths.map{|path| prefix+path} if permissions.is_a? self.class
91
- self
92
- end
93
-
94
88
  protected
95
89
 
96
90
  def permitted_paths
@@ -98,40 +92,49 @@ module StrongPresenter
98
92
  end
99
93
 
100
94
  private
101
- # We trust path parameters are arrays
102
- def raw_permitted? prefix_path, attribute_path = nil # const - does not alter arguments
103
- return true if complete?
104
- permitted_partial?([], prefix_path + Array(attribute_path))
95
+ # Is this still referencing another objects permissions?
96
+ def reference?
97
+ !@prefix_path.nil?
105
98
  end
106
99
 
107
- def raw_reject_permitted prefix_path, attribute_paths # const - does not alter arguments
108
- return [] if raw_permitted? prefix_path
109
- attribute_paths.reject do |attribute_path|
110
- permitted_partial? prefix_path.dup, attribute_path
100
+ # Make a copy if this still references something else, since we are planning on writing soon
101
+ def copy_on_write!
102
+ if prefix_path == []
103
+ @permitted_paths = permitted_paths.dup
104
+ elsif reference?
105
+ @permitted_paths, old_set = Set.new, permitted_paths
106
+ old_set.each do |path|
107
+ @permitted_paths << path[prefix_path.size...path.size] if path[0...prefix_path.size] == prefix_path
108
+ end
111
109
  end
110
+ @prefix_path = nil
112
111
  end
113
112
 
114
- def raw_select_permitted prefix_path, attribute_paths # const - does not alter arguments
115
- return attribute_paths if raw_permitted? prefix_path
116
- attribute_paths.select do |attribute_path|
117
- permitted_partial? prefix_path.dup, attribute_path
118
- end
113
+ def path_tainted? attribute_path
114
+ attribute_path.tainted? or attribute_path.any? { |element| element.tainted? }
119
115
  end
120
116
 
121
- # For internal use, checks if permitted explicitly by a subpath of [prefix_path, attribute_partial+]
122
- # where attribute_partial is a prefix of at least one symbol from attribute_part
123
- # Caution: Will mutate prefix_path
124
- def permitted_partial? prefix_path, attribute_path
125
- !!Array(attribute_path).detect do |attr|
126
- break unless attr.is_a? Symbol
127
- prefix_path << attr
128
- permitted_paths.include? prefix_path
117
+ # Caution: Will mutate path
118
+ def permitted_by_wildcard? path
119
+ unless path.empty?
120
+ path[-1] = :*
121
+ return true if permitted_paths.include? path
122
+ end
123
+ until path.empty?
124
+ path[-1] = :**
125
+ return true if permitted_paths.include? path
126
+ path.pop
129
127
  end
128
+ false
130
129
  end
131
130
 
132
- # Ensures that every array element is an array
133
- def nested_array array
134
- array.map{|e|Array(e)}
131
+ # Converts symbols to strings (except for wildcard symbol)
132
+ def canonicalize array
133
+ array = Array(array)
134
+ canonical_array = array.map{|e|e.is_a?(Symbol) ? e.to_s : e}
135
+ canonical_array[-1] = array.last if [:*, :**].include? array.last
136
+ canonical_array.taint if array.tainted?
137
+ canonical_array
135
138
  end
136
139
 
137
140
  end
@@ -84,6 +84,7 @@ module StrongPresenter
84
84
  #
85
85
  def presents *attributes
86
86
  select_permitted(*attributes).map do |args|
87
+ args = Array(args)
87
88
  obj = self # drill into associations
88
89
  while (args.size > 1) && self.class.send(:presenter_associations).include?(args[0]) do
89
90
  obj = obj.public_send args.slice!(0)
@@ -146,7 +147,7 @@ module StrongPresenter
146
147
 
147
148
  def set_presenter_collection
148
149
  collection_presenter = get_collection_presenter
149
- const_set "Collection", collection_presenter unless const_defined?("Collection")
150
+ const_set "Collection", collection_presenter # will overwrite if constant only defined in superclass
150
151
  end
151
152
 
152
153
  private
@@ -174,8 +175,6 @@ module StrongPresenter
174
175
  # Checks whether this presenter class has a corresponding {object_class}.
175
176
  def object_class?
176
177
  !!(@object_class ||= Inferrer.new(name).chomp("Presenter").inferred_class)
177
- rescue NameError
178
- false
179
178
  end
180
179
 
181
180
  # Sets the model presented by the class
@@ -54,7 +54,7 @@ module StrongPresenter
54
54
 
55
55
  # wrap model with presenter and return
56
56
  def wrapped_object(controller)
57
- factory.wrap(controller.send :instance_variable_get, @object) { |presenter| self.instance_exec presenter, &block }
57
+ factory.wrap(controller.send :instance_variable_get, @object) { |presenter| self.instance_exec presenter, &block unless block.nil? }
58
58
  end
59
59
  end
60
60
  end
@@ -1,3 +1,3 @@
1
1
  module StrongPresenter
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
3
3
  end
@@ -1,7 +1,7 @@
1
1
  class PostsController < ApplicationController
2
2
  presents :post, :with => StrongPresenter::Presenter, :only => :show
3
3
  presents :post, :with => PostPresenter, :only => :show do |presenter|
4
- presenter.permit(:permit_to_present, :peek_a_boo)
4
+ presenter.permit!(:permit_to_present, :peek_a_boo)
5
5
  end
6
6
  presents :post, :with => StrongPresenter::Presenter, :only => [:index]
7
7
  presents :post, :with => StrongPresenter::Presenter, :except => [:show, :new]
@@ -6,7 +6,7 @@ class PostMailer < ApplicationMailer
6
6
  helper :application
7
7
 
8
8
  def presented_email(post)
9
- @post = PostPresenter.new(post).permit!
9
+ @post = PostPresenter.new(post).permit_all!
10
10
  mail to: "to@example.com", subject: "A presented post"
11
11
  end
12
12
 
@@ -7,12 +7,10 @@ module StrongPresenter
7
7
  protect_class ProductPresenter
8
8
 
9
9
  before(:each) do
10
- class Manufacturer < Model
11
- def name; "Factory"; end
12
- end
13
- class ManufacturerPresenter < StrongPresenter::Presenter
14
- def name(*args); "Presented #{object.name}#{(args+[""])[0]}"; end
15
- end
10
+ stub_const('Manufacturer', Class.new(Model))
11
+ Manufacturer.send(:define_method, :name) {"Factory"}
12
+ stub_const('ManufacturerPresenter', Class.new(StrongPresenter::Presenter))
13
+ ManufacturerPresenter.send(:define_method, :name) { |*args| "Presented #{object.name}#{(args+[""])[0]}" }
16
14
  Product.send(:define_method, :manufacturer) { @manufacturer ||= Manufacturer.new }
17
15
  Product.send(:define_method, :name) { "Product" }
18
16
  ProductPresenter.presents_association :manufacturer
@@ -25,10 +23,10 @@ module StrongPresenter
25
23
  expect(@product_presenter.manufacturer.name).to eq "Presented Factory"
26
24
  end
27
25
 
28
- it 'does not allow presenting without permit' do
26
+ it 'does not allow presenting without permit!' do
29
27
  expect(@product_presenter.presents :manufacturer).to be_empty
30
28
  expect(@product_presenter.presents [:manufacturer, :name]).to be_empty
31
- @product_presenter.permit :name
29
+ @product_presenter.permit! :name
32
30
  expect(@product_presenter.presents [:manufacturer, :name]).to be_empty
33
31
  end
34
32
 
@@ -36,21 +34,23 @@ module StrongPresenter
36
34
  expect(@product_presenter.manufacturer.presents :name).to be_empty
37
35
  end
38
36
 
39
- context 'with association permitted' do
37
+ context 'with association methods permitted' do
40
38
  before(:each) do
41
- @product_presenter.permit :manufacturer
39
+ @product_presenter.permit! [:manufacturer, :*]
42
40
  end
43
41
 
44
- it 'allows presenting association' do
42
+ it 'allows presenting association if exact match' do
43
+ expect(@product_presenter.present(:manufacturer)).to be_nil
44
+ @product_presenter.permit! :manufacturer
45
45
  expect(@product_presenter.present(:manufacturer).class).to be ManufacturerPresenter
46
46
  end
47
47
 
48
- it 'allows presenting association attributes' do
48
+ it 'allows presenting association methods' do
49
49
  expect(@product_presenter.present [:manufacturer, :name]).to eq "Presented Factory"
50
50
  end
51
51
 
52
- it 'allows presenting association attributes with arguments' do
53
- expect(@product_presenter.present [:manufacturer, :name, " arg"]).to eq "Presented Factory arg"
52
+ it 'cannot present method with arguments' do
53
+ expect(@product_presenter.present [:manufacturer, :name, " arg"]).to be_nil
54
54
  end
55
55
 
56
56
  it 'allows presenting from association' do
@@ -59,19 +59,41 @@ module StrongPresenter
59
59
  end
60
60
 
61
61
  it 'rejects full path from association' do
62
- @product_presenter.permit [:manufacturer, :name]
62
+ @product_presenter.permit! [:manufacturer, :name]
63
63
  expect(@product_presenter.manufacturer.presents [:manufacturer, :name]).to be_empty
64
64
  end
65
65
  end
66
66
 
67
+ context 'with missing constants' do
68
+ protect_class Product
69
+ protect_class ProductPresenter
70
+
71
+ it 'can declare association without ActiveRecord' do
72
+ hide_const('ActiveRecord')
73
+ Product.send(:define_method, :inverse) { @inverse ||= Product.new }
74
+ ProductPresenter.presents_association :inverse
75
+ @product_presenter = ProductPresenter.new(Product.new)
76
+ expect(@product_presenter.inverse.class).to be ProductPresenter
77
+ end
78
+
79
+ it 'can declare association with empty ActiveRecord' do
80
+ stub_const('ActiveRecord', Module.new)
81
+ Product.send(:define_method, :inverse) { @inverse ||= Product.new }
82
+ ProductPresenter.presents_association :inverse
83
+ @product_presenter = ProductPresenter.new(Product.new)
84
+ expect(@product_presenter.inverse.class).to be ProductPresenter
85
+ end
86
+ end
87
+
67
88
  context 'with CollectionPresenter' do
68
89
  protect_class Product
69
90
  protect_class ProductPresenter
70
91
  before(:each) do
92
+ stub_const('ProductList', Class.new(Model))
71
93
  Product.send(:define_method, :initialize) { |name| @name = name }
72
94
  Product.send(:attr_reader, :name)
73
95
  ProductPresenter.send(:define_method, :name) { object.name }
74
- class ProductList < Model
96
+ class ProductList
75
97
  attr_accessor :products
76
98
  def initialize; @products = [Product.new("X"), Product.new("Y"), Product.new("Z")]; end
77
99
  end
@@ -104,19 +126,176 @@ module StrongPresenter
104
126
  expect(presenter.wheels.class).to be WheelPresenter::Collection
105
127
  end
106
128
 
107
- it 'infers presenter of polymorphic association repeatedly' do
129
+ it 'cannot infer without presenter' do
130
+ hide_const('WheelPresenter')
131
+ CarPresenter.presents_association :wheels
132
+
133
+ car = Car.new
134
+ car.wheels << Wheel.new
135
+ presenter = CarPresenter.new(car)
136
+ expect(presenter.wheels.class).to be StrongPresenter::CollectionPresenter
137
+ expect{presenter.wheels[0]}.to raise_error StrongPresenter::UninferrablePresenterError
138
+ end
139
+
140
+ it 'infers collection presenter on factory construction' do
141
+ hide_const('WheelPresenter')
142
+ stub_const('WheelsPresenter', Class.new(StrongPresenter::CollectionPresenter))
143
+ WheelsPresenter.send(:define_method, :number_of){object.size}
144
+ CarPresenter.presents_association :wheels
145
+
146
+ car = Car.new
147
+ car.wheels = [Wheel.new, Wheel.new, Wheel.new]
148
+ presenter = CarPresenter.new(car)
149
+ expect(presenter.wheels.class).to be WheelsPresenter
150
+ expect(presenter.wheels.number_of).to be 3
151
+ end
152
+
153
+ it 'infers presenter of polymorphic association' do
108
154
  WheelPresenter.presents_association :vehicle
109
155
 
110
156
  wheel = Wheel.new
111
157
  wheel.vehicle = Car.new
112
158
  presenter = WheelPresenter.new(wheel)
113
159
  expect(presenter.vehicle.class).to be CarPresenter
160
+ end
114
161
 
115
- # if presenter.vehicle is cached, the next example will be incorrect
162
+ it 'infers new association after reload' do
163
+ WheelPresenter.presents_association :vehicle
164
+ wheel = Wheel.new
165
+ wheel.vehicle = Car.new
166
+ presenter = WheelPresenter.new(wheel)
167
+ presenter.vehicle
116
168
  wheel.vehicle = Wheel.new
169
+ expect(presenter.vehicle.class).to be CarPresenter
170
+ expect(presenter.reload!.vehicle.class).to be WheelPresenter
117
171
  expect(presenter.vehicle.class).to be WheelPresenter
118
172
  end
119
173
  end
174
+
175
+ describe 'without suitable presenter' do
176
+ protect_class Product
177
+ it 'throws uninferrable presenter' do
178
+ Product.send(:attr_accessor, :string)
179
+ ProductPresenter.presents_association :string
180
+ Product.send(:define_method, :initialize) { self.string = "String"}
181
+
182
+ presenter = ProductPresenter.new(Product.new)
183
+ expect{presenter.string}.to raise_error(StrongPresenter::UninferrablePresenterError)
184
+ end
185
+ end
186
+
187
+ describe 'Permissible' do
188
+ protect_class Product
189
+ protect_class ProductPresenter
190
+ before (:each) do
191
+ Product.send(:attr_accessor, :name, :description, :price, :subproducts, :other)
192
+ Product.send(:define_method, :initialize) do |name, description = "Description", price = 1, subproducts = []|
193
+ self.name = name
194
+ self.description = description
195
+ self.price = price
196
+ self.subproducts = Array(subproducts)
197
+ self.other = Wheel.new
198
+ end
199
+ ProductPresenter.presents_association :subproducts
200
+ ProductPresenter.presents_association :other
201
+ ProductPresenter.delegate :name, :description, :price
202
+ @product = Product.new("Main", "I, Main", 4.2, [Product.new("Sub A", "Small", 8), Product.new("Component B")])
203
+ @product_presenter = ProductPresenter.new(@product)
204
+ end
205
+
206
+ it 'collection association inherits permissions' do
207
+ @product_presenter.permit! [:subproducts, :name]
208
+ expect(@product_presenter.subproducts.select_permitted(:name, :price)).to eq [:name]
209
+ end
210
+
211
+ it 'association inherits permissions' do
212
+ @product_presenter.permit! [:other, :stuff]
213
+ expect(@product_presenter.other.select_permitted(:other, :stuff)).to eq [:stuff]
214
+ end
215
+
216
+ it 'collection item inherits permissions' do
217
+ @product_presenter.permit! [:subproducts, :name]
218
+ expect(@product_presenter.subproducts[0].presents(:name, :price)).to eq ["Sub A"]
219
+ end
220
+
221
+ it 'forwards permit to associations' do
222
+ other_presenter = @product_presenter.other
223
+ @product_presenter.permit! [:other, :stuff]
224
+ expect(other_presenter.select_permitted(:other, :stuff)).to eq [:stuff]
225
+ end
226
+
227
+ it 'forwards permit to association collection items' do
228
+ subproduct_presenter = @product_presenter.subproducts[0]
229
+ @product_presenter.permit! [:subproducts, :name]
230
+ expect(subproduct_presenter.presents(:name, :price)).to eq ["Sub A"]
231
+ end
232
+
233
+ it 'does not feed permissions to siblings' do
234
+ @product_presenter.subproducts[0].permit! :name
235
+ expect(@product_presenter.subproducts[0].presents(:name, :price)).to eq ["Sub A"]
236
+ expect(@product_presenter.subproducts[1].presents(:name, :price)).to be_empty
237
+ end
238
+
239
+ it 'appears to clear permissions on reload' do
240
+ @product_presenter.subproducts[0].permit! :name
241
+ expect(@product_presenter.reload!.subproducts[0].presents(:name, :price)).to be_empty
242
+ end
243
+
244
+ it 'can add to association permissions' do
245
+ @product_presenter.permit! [:subproducts, :name]
246
+ @product_presenter.subproducts[0].permit! :price
247
+ expect(@product_presenter.subproducts[0].presents(:name, :price)).to eq ["Sub A", 8]
248
+ end
249
+
250
+ it 'can add to collection permissions' do
251
+ @product_presenter.permit! [:subproducts, :name]
252
+ @product_presenter.subproducts.permit! :description
253
+ expect(@product_presenter.subproducts[0].presents(:name, :price, :description)).to eq ["Sub A", "Small"]
254
+ end
255
+
256
+ it 'can add to item permissions' do
257
+ @product_presenter.subproducts.permit! :description
258
+ @product_presenter.subproducts[0].permit! :price
259
+ expect(@product_presenter.subproducts[0].presents(:name, :price, :description)).to eq [8, "Small"]
260
+ expect(@product_presenter.subproducts[1].presents(:name, :price, :description)).to eq ["Description"]
261
+ end
262
+
263
+ it 'adds propagated permissions' do
264
+ @product_presenter.subproducts[0].permit! :price
265
+ @product_presenter.permit! [:subproducts, :name]
266
+ expect(@product_presenter.subproducts[0].presents(:name, :price, :description)).to eq ["Sub A", 8]
267
+ end
268
+
269
+ it 'propagates double wildcard' do
270
+ @product_presenter.subproducts[0].permit! :price
271
+ @product_presenter.permit! :**
272
+ expect(@product_presenter.subproducts[0].presents(:name, "price", :description)).to eq ["Sub A", 8, "Small"]
273
+ end
274
+
275
+ it 'does not propagate single wildcard' do
276
+ @product_presenter.subproducts[0].permit! :price
277
+ @product_presenter.permit! :*
278
+ expect(@product_presenter.subproducts[0].presents(:name, :price, :description)).to eq [8]
279
+ end
280
+
281
+ it 'can propagate string path to collection' do
282
+ @product_presenter.subproducts.permit! :price
283
+ @product_presenter.permit! ["subproducts", "name"]
284
+ expect(@product_presenter.subproducts[0].presents(:name, :price, :description)).to eq ["Sub A", 8]
285
+ end
286
+
287
+ it 'propagates wildcard ending' do
288
+ @product_presenter.subproducts[0].permit! :price
289
+ @product_presenter.permit! [:subproducts, :*]
290
+ expect(@product_presenter.subproducts[0].presents(:name, "price", :description)).to eq ["Sub A", 8, "Small"]
291
+ end
292
+
293
+ it 'handles taint with mixture of wildcard and exact matches' do
294
+ @product_presenter.subproducts[0].permit! :price
295
+ @product_presenter.permit! [:subproducts, :*]
296
+ expect(@product_presenter.subproducts[0].presents(:name, "price".taint, "description".taint)).to eq ["Sub A", 8]
297
+ end
298
+ end
120
299
  end
121
300
  end
122
301