strong_presenter 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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