permissioner 0.0.2.beta → 0.1.0.beta
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.
- checksums.yaml +4 -4
- data/lib/permissioner.rb +2 -2
- data/lib/permissioner/{permission_configurer.rb → configurer.rb} +2 -2
- data/lib/permissioner/controller_additions.rb +1 -2
- data/lib/permissioner/matchers.rb +39 -7
- data/lib/permissioner/matchers/exactly_allow_attributes.rb +137 -0
- data/lib/permissioner/matchers/exactly_allow_controllers.rb +48 -0
- data/lib/permissioner/matchers/exactly_allow_resources.rb +44 -0
- data/lib/permissioner/matchers/exactly_expect_actions.rb +100 -0
- data/lib/permissioner/{permission_service_additions.rb → service_additions.rb} +23 -7
- data/lib/permissioner/version.rb +1 -1
- data/permissioner.gemspec +1 -1
- data/spec/permissioner/{permission_configurer_spec.rb → configurer_spec.rb} +8 -2
- data/spec/permissioner/controller_additions_spec.rb +3 -20
- data/spec/permissioner/matchers/exactly_allow_attributes_spec.rb +227 -0
- data/spec/permissioner/matchers/exactly_allow_controllers_spec.rb +86 -0
- data/spec/permissioner/matchers/exactly_allow_resources_spec.rb +78 -0
- data/spec/permissioner/matchers/exactly_expect_actions_spec.rb +188 -0
- data/spec/permissioner/matchers_spec.rb +70 -27
- data/spec/permissioner/{permission_service_additions_spec.rb → service_additions_spec.rb} +117 -28
- data/spec/spec_helper.rb +1 -1
- metadata +22 -10
data/lib/permissioner/version.rb
CHANGED
data/permissioner.gemspec
CHANGED
@@ -19,6 +19,6 @@ Gem::Specification.new do |gem|
|
|
19
19
|
gem.require_paths = ["lib"]
|
20
20
|
|
21
21
|
gem.add_development_dependency "rspec", "~> 2.13"
|
22
|
-
gem.add_development_dependency "activesupport", "~>
|
22
|
+
gem.add_development_dependency "activesupport", "~>4.0.0"
|
23
23
|
gem.add_development_dependency "guard-rspec", "~>3.0.1"
|
24
24
|
end
|
@@ -1,10 +1,10 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
|
-
describe Permissioner::
|
3
|
+
describe Permissioner::Configurer do
|
4
4
|
|
5
5
|
let(:permission_configurer_class) do
|
6
6
|
permission_configurer_class = Class.new
|
7
|
-
permission_configurer_class.send(:include, Permissioner::
|
7
|
+
permission_configurer_class.send(:include, Permissioner::Configurer)
|
8
8
|
permission_configurer_class
|
9
9
|
end
|
10
10
|
|
@@ -27,6 +27,12 @@ describe Permissioner::PermissionConfigurer do
|
|
27
27
|
permission_service.should_receive(:add_filter).with(:comments, :create, &block)
|
28
28
|
permission_configurer.add_filter(:comments, :create, &block)
|
29
29
|
end
|
30
|
+
|
31
|
+
it 'should delegate call to clear_filter to permission_service' do
|
32
|
+
block = Proc.new {}
|
33
|
+
permission_service.should_receive(:clear_filters).with(no_args)
|
34
|
+
permission_configurer.clear_filters
|
35
|
+
end
|
30
36
|
end
|
31
37
|
|
32
38
|
context 'setters' do
|
@@ -33,40 +33,23 @@ describe Permissioner::ControllerAdditions do
|
|
33
33
|
|
34
34
|
before :each do
|
35
35
|
@params = {controller: 'comments', action: 'index'}
|
36
|
+
@controller.stub(:current_resource).and_return('resource')
|
36
37
|
@controller.stub(:params).and_return(@params)
|
37
38
|
end
|
38
39
|
|
39
40
|
it 'should call permit_params! if action allwed and filters passed' do
|
40
41
|
@controller.permission_service.should_receive(:allow_action?).and_return(true)
|
41
|
-
@controller.permission_service.should_receive(:passed_filters?).and_return(true)
|
42
42
|
@controller.permission_service.should_receive(:permit_params!).with(@params)
|
43
43
|
@controller.authorize
|
44
44
|
end
|
45
45
|
|
46
46
|
it 'should call allow_action? with correct parameters' do
|
47
|
-
@controller.should_receive(:
|
48
|
-
@controller.permission_service.should_receive(:allow_action?).with('comments', 'index', 'current_resource').and_return(true)
|
49
|
-
@controller.permission_service.stub(:passed_filters?).and_return(true)
|
50
|
-
@controller.authorize
|
51
|
-
end
|
52
|
-
|
53
|
-
it 'should call add_filters? with correct parameters' do
|
54
|
-
@controller.should_receive(:current_resource).and_return('current_resource')
|
55
|
-
@controller.permission_service.stub(:allow_action?).and_return(true)
|
56
|
-
@controller.permission_service.should_receive(:passed_filters?).with('comments', 'index', @params).and_return(true)
|
47
|
+
@controller.permission_service.should_receive(:allow_action?).with('comments', 'index', resource: 'resource', params: @params).and_return(true)
|
57
48
|
@controller.authorize
|
58
49
|
end
|
59
50
|
|
60
51
|
it 'should raise Permissioner::NotAuthorized when action not allowed' do
|
61
|
-
@controller.permission_service.should_receive(:allow_action?).with('comments', 'index',
|
62
|
-
expect {
|
63
|
-
@controller.authorize
|
64
|
-
}.to raise_error Permissioner::NotAuthorized
|
65
|
-
end
|
66
|
-
|
67
|
-
it 'should raise Permissioner::NotAuthorized when action are allowed but filters did not passed' do
|
68
|
-
@controller.permission_service.should_receive(:allow_action?).and_return(true)
|
69
|
-
@controller.permission_service.should_receive(:passed_filters?).and_return(false)
|
52
|
+
@controller.permission_service.should_receive(:allow_action?).with('comments', 'index', resource: 'resource', params: @params).and_return(false)
|
70
53
|
expect {
|
71
54
|
@controller.authorize
|
72
55
|
}.to raise_error Permissioner::NotAuthorized
|
@@ -0,0 +1,227 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Permissioner::Matchers::ExactlyAllowAttributes do
|
4
|
+
|
5
|
+
let(:permission_service) do
|
6
|
+
permission_service = PermissionService.new
|
7
|
+
permission_service.allow_attributes :user, [:name, :email, :phone]
|
8
|
+
permission_service.allow_attributes :comment, [:user_id, :post_id, :content]
|
9
|
+
permission_service.allow_attributes :post, [:user_id, :content]
|
10
|
+
permission_service
|
11
|
+
end
|
12
|
+
|
13
|
+
def create_matcher(*expected_attributes)
|
14
|
+
Permissioner::Matchers::ExactlyAllowAttributes.new(*expected_attributes)
|
15
|
+
end
|
16
|
+
|
17
|
+
describe '#initialize' do
|
18
|
+
|
19
|
+
it 'should ensure that all attributes are into an array' do
|
20
|
+
matcher = create_matcher [:resource, :attribute]
|
21
|
+
matcher.instance_variable_get(:@all_expected_attributes).should eq [[:resource, [:attribute]]]
|
22
|
+
matcher = create_matcher [:resource, [:attribute_1, :attribute_2]]
|
23
|
+
matcher.instance_variable_get(:@all_expected_attributes).should eq [[:resource, [:attribute_1, :attribute_2]]]
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'should raise an exception if multiple actions not stated as array' do
|
27
|
+
expect {
|
28
|
+
create_matcher [:resource, :attribute_1, :attribute_2]
|
29
|
+
}.to raise_exception 'multiple attributes for a resource must stated as array, e.g. [:user_id, :username]'
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
describe '#matches?' do
|
34
|
+
|
35
|
+
context 'on success' do
|
36
|
+
|
37
|
+
it 'should return true if all expected resources with attributes are exactly allowed' do
|
38
|
+
matcher = create_matcher(
|
39
|
+
[:user, [:name, :email, :phone]],
|
40
|
+
[:comment, [:user_id, :post_id, :content]],
|
41
|
+
[:post, [:user_id, :content]]
|
42
|
+
)
|
43
|
+
matcher.matches?(permission_service).should be_true
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'should accept attributs being a hash' do
|
47
|
+
permission_service = PermissionService.new
|
48
|
+
permission_service.allow_attributes(
|
49
|
+
:account,
|
50
|
+
[
|
51
|
+
{user: [:name, :email, :phone]},
|
52
|
+
{adresse: [:name, :street_with_number, :zip_code, :city]},
|
53
|
+
]
|
54
|
+
)
|
55
|
+
matcher = create_matcher(
|
56
|
+
[:account,
|
57
|
+
[
|
58
|
+
{user: [:name, :email, :phone]},
|
59
|
+
{adresse: [:name, :street_with_number, :zip_code, :city]},
|
60
|
+
]
|
61
|
+
]
|
62
|
+
)
|
63
|
+
matcher.matches?(permission_service).should be_true
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
context 'if resources did not match' do
|
68
|
+
|
69
|
+
it 'should return false if at least one expected resource is not allowed' do
|
70
|
+
matcher = create_matcher(
|
71
|
+
[:user, [:name, :email, :phone]],
|
72
|
+
[:comment, [:user_id, :post_id, :content]],
|
73
|
+
[:account, [:user_id, :content]]
|
74
|
+
)
|
75
|
+
matcher.matches?(permission_service).should be_false
|
76
|
+
end
|
77
|
+
|
78
|
+
it 'should return false if at least one resource is allowed but no expected' do
|
79
|
+
matcher = create_matcher(
|
80
|
+
[:user, [:name, :email, :phone]],
|
81
|
+
[:comment, [:user_id, :post_id, :content]]
|
82
|
+
)
|
83
|
+
matcher.matches?(permission_service).should be_false
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
context 'if attributes of ressrouces did not match' do
|
88
|
+
|
89
|
+
it 'should return false if at least one expected attribute is not allowed' do
|
90
|
+
matcher = create_matcher(
|
91
|
+
[:user, [:name, :email, :phone, :street]],
|
92
|
+
[:comment, [:user_id, :post_id, :content]],
|
93
|
+
[:post, [:user_id, :content]]
|
94
|
+
)
|
95
|
+
matcher.matches?(permission_service).should be_false
|
96
|
+
end
|
97
|
+
|
98
|
+
it 'should return false if at least one attribute is allowed but not expected' do
|
99
|
+
matcher = create_matcher(
|
100
|
+
[:user, [:name, :email]],
|
101
|
+
[:comment, [:user_id, :post_id, :content]],
|
102
|
+
[:post, [:user_id, :content]]
|
103
|
+
)
|
104
|
+
matcher.matches?(permission_service).should be_false
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
describe '#failure_message_for_should_not' do
|
110
|
+
|
111
|
+
it 'should be available' do
|
112
|
+
matcher = create_matcher
|
113
|
+
expected_messages = 'given attributes are exactly allowed although this is not expected'
|
114
|
+
matcher.failure_message_for_should_not.should eq expected_messages
|
115
|
+
end
|
116
|
+
|
117
|
+
it 'should be available' do
|
118
|
+
matcher = create_matcher
|
119
|
+
matcher.matches?(PermissionService.new)
|
120
|
+
matcher.failure_message_for_should_not.should be_kind_of(String)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
describe '#failure_message_for_should' do
|
125
|
+
|
126
|
+
context 'if resources did not match' do
|
127
|
+
|
128
|
+
it 'should be available' do
|
129
|
+
matcher = create_matcher(
|
130
|
+
[:user, []],
|
131
|
+
[:comment, []]
|
132
|
+
)
|
133
|
+
expected_messages =
|
134
|
+
"expected to find allowed attributes for resources\n"\
|
135
|
+
"[:comment, :user], but found allowed attributes for resources\n"\
|
136
|
+
"[:comment, :post, :user]"
|
137
|
+
#call is necessary because matches sets @permission_service
|
138
|
+
matcher.matches?(permission_service)
|
139
|
+
matcher.failure_message_for_should.should eq expected_messages
|
140
|
+
end
|
141
|
+
|
142
|
+
it 'should work if no controller allowed' do
|
143
|
+
matcher = create_matcher(
|
144
|
+
[:user, []],
|
145
|
+
[:comment, []],
|
146
|
+
[:post, []]
|
147
|
+
)
|
148
|
+
#call is necessary because matches sets @permission_service
|
149
|
+
matcher.matches?(PermissionService.new)
|
150
|
+
matcher.failure_message_for_should.should be_kind_of(String)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
|
155
|
+
context 'if attributes for resources did not match' do
|
156
|
+
|
157
|
+
it 'should be available' do
|
158
|
+
matcher = create_matcher(
|
159
|
+
[:user, [:name, :email]],
|
160
|
+
[:comment, [:user_id, :post_id]],
|
161
|
+
[:post, [:user_id, :content]]
|
162
|
+
)
|
163
|
+
expected_messages =
|
164
|
+
"expected attributes did not match for following resources:\n"\
|
165
|
+
"user:\n"\
|
166
|
+
"[:name, :email] were expected to be allowed, but attributes\n"\
|
167
|
+
"[:name, :email, :phone] are allowed\n"\
|
168
|
+
"comment:\n"\
|
169
|
+
"[:user_id, :post_id] were expected to be allowed, but attributes\n"\
|
170
|
+
"[:user_id, :post_id, :content] are allowed\n"
|
171
|
+
#call is necessary because matches sets @permission_service
|
172
|
+
matcher.matches?(permission_service)
|
173
|
+
matcher.failure_message_for_should.should eq expected_messages
|
174
|
+
end
|
175
|
+
|
176
|
+
|
177
|
+
it 'should work if no controller allowed' do
|
178
|
+
matcher = create_matcher(
|
179
|
+
[:comments, [:show, :new, :create]],
|
180
|
+
[:users, [:new, :create, :update]],
|
181
|
+
[:posts, [:show, :edit, :update]]
|
182
|
+
)
|
183
|
+
#call is necessary because matches sets @permission_service
|
184
|
+
matcher.matches?(PermissionService.new)
|
185
|
+
matcher.failure_message_for_should.should be_kind_of(String)
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
#describe '#failure_message_for_should' do
|
191
|
+
#
|
192
|
+
# it 'should be available' do
|
193
|
+
# matcher = create_matcher :user, [:name, :email, :street]
|
194
|
+
# expected_messages =
|
195
|
+
# "expected that for resource \"user\" attributes\n"\
|
196
|
+
# "[:name, :email, :street] are exactly allowed, but found attributes\n"\
|
197
|
+
# "[:name, :email, :phone] allowed"
|
198
|
+
# matcher.matches?(permission_service)
|
199
|
+
# matcher.failure_message_for_should.should eq expected_messages
|
200
|
+
# end
|
201
|
+
#
|
202
|
+
# it 'should work if no controller allowed' do
|
203
|
+
# matcher = create_matcher :user, [:name, :email, :street]
|
204
|
+
# matcher.matches?(PermissionService.new)
|
205
|
+
# matcher.failure_message_for_should.should be_kind_of(String)
|
206
|
+
# end
|
207
|
+
#end
|
208
|
+
#
|
209
|
+
#describe '#failure_message_for_should_not' do
|
210
|
+
#
|
211
|
+
# it 'should be available' do
|
212
|
+
# matcher = create_matcher :user, [:name, :email, :street]
|
213
|
+
# expected_messages =
|
214
|
+
# "expected that for resource \"user\" attributes\n"\
|
215
|
+
# "[:name, :email, :street] are exactly not allowed,\n"\
|
216
|
+
# "but those attributes are exactly allowed\n"
|
217
|
+
# matcher.matches?(permission_service)
|
218
|
+
# matcher.failure_message_for_should_not.should eq expected_messages
|
219
|
+
# end
|
220
|
+
#
|
221
|
+
# it 'should work if no controller allowed' do
|
222
|
+
# matcher = create_matcher :user, [:name, :email, :street]
|
223
|
+
# matcher.matches?(PermissionService.new)
|
224
|
+
# matcher.failure_message_for_should_not.should be_kind_of(String)
|
225
|
+
# end
|
226
|
+
#end
|
227
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Permissioner::Matchers::ExactlyAllowControllers do
|
4
|
+
|
5
|
+
let(:permission_service) do
|
6
|
+
permission_service = PermissionService.new
|
7
|
+
permission_service.allow_actions :comments, :index
|
8
|
+
permission_service.allow_actions :comments, :create
|
9
|
+
permission_service.allow_actions :users, :index
|
10
|
+
permission_service.allow_actions :posts, :update
|
11
|
+
permission_service
|
12
|
+
end
|
13
|
+
|
14
|
+
def create_matcher(*expected_controllers)
|
15
|
+
Permissioner::Matchers::ExactlyAllowControllers.new(*expected_controllers)
|
16
|
+
end
|
17
|
+
|
18
|
+
describe '#initialize' do
|
19
|
+
|
20
|
+
it 'should transform expected_controllers to strings' do
|
21
|
+
matcher = create_matcher(:comments, :users)
|
22
|
+
matcher.instance_variable_get(:@expected_controllers).should eq ['comments', 'users']
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
describe '#matches?' do
|
27
|
+
|
28
|
+
it 'should return true if exactly all expected controllers allowed' do
|
29
|
+
matcher = create_matcher(:comments, :users, :posts)
|
30
|
+
matcher.matches?(permission_service).should be_true
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'should return false if at least one controller is not allowed' do
|
34
|
+
matcher = create_matcher(:comments, :users, :posts, :blogs)
|
35
|
+
matcher.matches?(permission_service).should be_false
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'should return true if more controllers allowed than expected' do
|
39
|
+
matcher = create_matcher(:comments, :users)
|
40
|
+
matcher.matches?(permission_service).should be_false
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'should work if no controller allowed' do
|
44
|
+
matcher = create_matcher(:comments, :users)
|
45
|
+
matcher.matches?(PermissionService.new).should be_false
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
describe '#failure_message_for_should' do
|
50
|
+
|
51
|
+
it 'should be available' do
|
52
|
+
matcher = create_matcher(:comments)
|
53
|
+
expected_messages =
|
54
|
+
"expected to exactly allow controllers \n" \
|
55
|
+
"[\"comments\"], but found controllers\n"\
|
56
|
+
"[\"comments\", \"posts\", \"users\"] allowed"
|
57
|
+
#call is necessary because matches sets @permission_service
|
58
|
+
matcher.matches?(permission_service)
|
59
|
+
matcher.failure_message_for_should.should eq expected_messages
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'should work if no controller allowed' do
|
63
|
+
matcher = create_matcher(:comments)
|
64
|
+
#call is necessary because matches sets @permission_service
|
65
|
+
matcher.matches?((PermissionService.new))
|
66
|
+
matcher.failure_message_for_should.should be_kind_of(String)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
describe '#failure_message_for_should_not' do
|
71
|
+
|
72
|
+
it 'should be available' do
|
73
|
+
matcher = create_matcher(:users, :comments)
|
74
|
+
expected_messages =
|
75
|
+
"expected to exactly not allow controllers \n" \
|
76
|
+
"[\"comments\", \"users\"], but these controllers are exactly allowed\n"
|
77
|
+
matcher.failure_message_for_should_not.should eq expected_messages
|
78
|
+
end
|
79
|
+
|
80
|
+
it 'should work if no controller allowed' do
|
81
|
+
matcher = create_matcher(:comments, :users)
|
82
|
+
matcher.failure_message_for_should_not.should be_kind_of(String)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Permissioner::Matchers::ExactlyAllowResources do
|
4
|
+
|
5
|
+
let(:permission_service) do
|
6
|
+
permission_service = PermissionService.new
|
7
|
+
permission_service.allow_attributes :user, [:name, :email, :phone]
|
8
|
+
permission_service.allow_attributes :comment, [:user_id, :text]
|
9
|
+
permission_service.allow_attributes :post, [:user_id, :text]
|
10
|
+
permission_service
|
11
|
+
end
|
12
|
+
|
13
|
+
|
14
|
+
def create_matcher(*expected_resources)
|
15
|
+
Permissioner::Matchers::ExactlyAllowResources.new(*expected_resources)
|
16
|
+
end
|
17
|
+
|
18
|
+
describe '#matches?' do
|
19
|
+
|
20
|
+
it 'should return true if given resources exaclty allowed' do
|
21
|
+
matcher = create_matcher :user, :comment, :post
|
22
|
+
matcher.matches?(permission_service).should be_true
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'should return false if at least one resource is not allowed' do
|
26
|
+
matcher = create_matcher :user, :comment, :account
|
27
|
+
matcher.matches?(permission_service).should be_false
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'should return false if more resources allowed than expected' do
|
31
|
+
matcher = create_matcher :user, :comment
|
32
|
+
matcher.matches?(permission_service).should be_false
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'should work if no resource is allowed' do
|
36
|
+
matcher = create_matcher :user, :comment
|
37
|
+
matcher.matches?(PermissionService.new).should be_false
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
describe '#failure_message_for_should' do
|
42
|
+
|
43
|
+
it 'should be available' do
|
44
|
+
matcher = create_matcher(:user, :comment)
|
45
|
+
expected_messages =
|
46
|
+
"expected to exactly allow resources \n" \
|
47
|
+
"[:comment, :user], but found resources\n"\
|
48
|
+
"[:comment, :post, :user] allowed"
|
49
|
+
#call is necessary because matches sets @permission_service
|
50
|
+
matcher.matches?(permission_service)
|
51
|
+
matcher.failure_message_for_should.should eq expected_messages
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'should work if no controller allowed' do
|
55
|
+
matcher = create_matcher(:user, :comment)
|
56
|
+
#call is necessary because matches sets @permission_service
|
57
|
+
matcher.matches?((PermissionService.new))
|
58
|
+
matcher.failure_message_for_should.should be_kind_of(String)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
describe '#failure_message_for_should_not' do
|
63
|
+
|
64
|
+
it 'should be available' do
|
65
|
+
matcher = create_matcher(:user, :comment)
|
66
|
+
expected_messages =
|
67
|
+
"expected to exactly not allow resources \n" \
|
68
|
+
"[:comment, :user], but these resources are exactly allowed\n"
|
69
|
+
matcher.failure_message_for_should_not.should eq expected_messages
|
70
|
+
end
|
71
|
+
|
72
|
+
it 'should work if no controller allowed' do
|
73
|
+
matcher = create_matcher(:comment, :user)
|
74
|
+
matcher.failure_message_for_should_not.should be_kind_of(String)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|